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

Compare commits

...

82 Commits

Author SHA1 Message Date
79d9acb471 chore(release): 0.5.4 2022-05-10 18:13:30 +02:00
e02565c0d9 perf(UI): left alignment for numbers in result tables, closes #249 2022-05-10 15:19:47 +02:00
ff272440bd fix: unable to insert auto-generated datetime fields 2022-05-10 15:14:34 +02:00
e62f280528 fix: app blocked by BIT fields with no default, closes #256 2022-05-10 15:13:08 +02:00
6d6151814e fix: SSH tunnel not working 2022-05-10 14:44:45 +02:00
58611bf07f fix: file upload input not working 2022-05-10 14:43:08 +02:00
Ngô Quốc Đạt
e0f85f469f Added missing translations for vi-VN 2022-05-08 16:29:48 +02:00
a93e71d751 chore(release): 0.5.3 2022-05-08 09:25:59 +02:00
2a7433da16 refactor: removed pinia ipc plugins 2022-05-08 00:12:23 +02:00
fd5dca2450 ci: e2e tests on separate github action 2022-05-07 23:20:31 +02:00
da6481f458 build: fix compilation issues 2022-05-07 22:50:11 +02:00
c7417809f4 refactor: minor improvement in changelog parsing 2022-05-07 16:13:22 +02:00
5cb8ec65ad refactor: ts improvements 2022-05-07 09:46:07 +02:00
e4c5d9b404 fix(MySQL): creating temporary tables on export to overcome view dependency errors 2022-05-06 17:20:06 +02:00
bfdb463390 fix: unable to set an empty string as field default 2022-05-05 23:09:10 +02:00
9fcc7d1cef fix(MySQL): support to ANSI_QUOTES mode not working, closes #243 2022-05-05 17:18:18 +02:00
70cb92521f refactor: Invitation to contribute in translations on settings 2022-05-04 11:24:48 +02:00
981d31957f Merge pull request #232 from antares-sql/dependabot/npm_and_yarn/electron-updater-5.0.1
build(deps): bump electron-updater from 4.6.5 to 5.0.1
2022-05-03 18:12:04 +02:00
619478c072 Merge pull request #230 from antares-sql/dependabot/npm_and_yarn/electron-builder-23.0.3
build(deps-dev): bump electron-builder from 22.14.13 to 23.0.3
2022-05-03 17:01:40 +02:00
eb1afed108 fix: LIKE operator in table filter not properly working, closes #242 2022-05-03 14:34:27 +02:00
0fa22fbe72 feat: default open schema if only one present 2022-05-01 10:46:32 +02:00
5e4abd3e81 fix: prevents query tabs targets previous schema if not in explorebar, closes #236 2022-05-01 10:23:03 +02:00
dependabot[bot]
81d897c7b3 build(deps): bump electron-updater from 4.6.5 to 5.0.1
Bumps [electron-updater](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-updater) from 4.6.5 to 5.0.1.
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/electron-updater@5.0.1/packages/electron-updater)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-30 15:34:36 +00:00
dependabot[bot]
b148c210a6 build(deps-dev): bump electron-builder from 22.14.13 to 23.0.3
Bumps [electron-builder](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-builder) from 22.14.13 to 23.0.3.
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-builder/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/v23.0.3/packages/electron-builder)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-30 15:34:19 +00:00
75aa299f8d Merge pull request #238 from antares-sql/vue3-migration
Vue3 migration
2022-04-30 17:31:02 +02:00
f4795eb92a chore: update CONTRIBUTING.md 2022-04-30 17:26:08 +02:00
18b66b5032 fix: missing tabs uid 2022-04-30 17:15:33 +02:00
cdbadbb11a remove @vue/compat 2022-04-30 16:11:44 +02:00
47e1f27bb8 fix: unable to disconnect from database 2022-04-30 12:15:00 +02:00
4be0f6bbbc Merge branch 'master' of https://github.com/Fabio286/antares into vue3-migration 2022-04-30 12:03:32 +02:00
ff8bb45d6d fix: missing storeToRefs imports 2022-04-30 11:45:49 +02:00
0d52282aa8 refactor: migration to pinia 2022-04-30 00:47:37 +02:00
e9dedfaf32 fix: query editor table fields suggestion 2022-04-28 13:22:15 +02:00
9f0ec5e0ce vue devtools integrated in chromium devtools 2022-04-28 10:34:44 +02:00
Giulio Ganci
1174bab0cc fix: verbose devtool loggin with missing sourcemap 2022-04-27 22:11:30 +02:00
9dd685b062 refactor: initial pinia implementation 2022-04-27 18:23:48 +02:00
0821586bb3 standalone vue-devtools 2022-04-27 18:23:05 +02:00
350d53642e fix: unable to add parameters to new routines/functions 2022-04-26 13:01:21 +02:00
Giulio Ganci
88eb9f7ab8 fix: can not use vue teleport with props as root element 2022-04-25 22:51:24 +02:00
Giulio Ganci
b0195260c3 fix foreign key dropdown with wrong value 2022-04-25 22:27:26 +02:00
Giulio Ganci
dfe3728269 no more this.$set needed in vue3 2022-04-25 22:11:53 +02:00
daef6f91b0 test: improved e2e tests 2022-04-25 18:26:11 +02:00
d61acf8a00 refactor(SQLite): improved sqlite window title 2022-04-25 17:37:12 +02:00
8b04a2b7dd refactor: teleport for modals 2022-04-25 13:45:07 +02:00
6780401cb7 refactor: vue3 transitions 2022-04-24 23:24:21 +02:00
b8e734d827 refactor: v-model on custom components 2022-04-24 23:10:25 +02:00
4f8bc26349 refactor: improved Proxy handlement on ipc 2022-04-24 12:17:29 +02:00
Giulio Ganci
3eb781021c enabling vue compat 2022-04-23 16:15:54 +02:00
Giulio Ganci
3cca3ed2b3 fix: undefined uid variable in the setting bar 2022-04-23 09:03:28 +02:00
434b9f8284 refactor: toRaw to handle Proxy objects on ipc 2022-04-22 23:33:12 +02:00
690cdcb2eb fix(SQLite): tables with sqlite in name not visible, closes #239 2022-04-22 23:01:30 +02:00
c50cbc577f fix: various warnings and exceptions 2022-04-22 17:49:13 +02:00
6af0c33461 fix: locale change 2022-04-22 17:45:12 +02:00
e60789f320 refactor: unproxify ipc params 2022-04-22 12:17:27 +02:00
afa23f3ef1 refactor: emits in components 2022-04-22 12:16:02 +02:00
7c4b605467 Merge branch 'master' of https://github.com/Fabio286/antares into vue3-migration 2022-04-21 14:42:33 +02:00
443170bbb1 refactor: vue3 migration in progress 2022-04-21 14:39:24 +02:00
3450497010 Merge pull request #237 from SmileYzn/patch-1
Fixed a typo
2022-04-21 13:29:39 +02:00
Cleverson
eaf38df1b7 Fixed a typo
Fixed a typo
2022-04-20 16:14:14 -03:00
a9f88e5784 chore: update bug_report.md [skip ci] 2022-04-19 11:47:50 +02:00
472fa6f430 fix: wrong path module importation 2022-04-17 13:00:53 +02:00
f82db96f34 fix: importer webpack config 2022-04-17 13:00:53 +02:00
79f32ca442 refactor: db import ts refactor 2022-04-17 13:00:53 +02:00
ce0f278caf refactor: db exporter ts refactor 2022-04-17 13:00:53 +02:00
6adc93e1cd refactor: ipc handlers ts refactor 2022-04-17 13:00:53 +02:00
5e2ad8c377 refactor: sqlite client ts refactor 2022-04-17 13:00:53 +02:00
d85662cb7d refactor: posgtre client ts refactor 2022-04-17 13:00:53 +02:00
e3907914f2 build: fixed module resolution and workers webpack config 2022-04-17 13:00:53 +02:00
c6c14fbf2b refactor: mysql client ts refactor 2022-04-17 13:00:53 +02:00
3975359292 build: typescript config 2022-04-17 13:00:53 +02:00
9a0f982723 fix: wrong path module importation 2022-04-17 12:27:46 +02:00
b4d9821300 fix: importer webpack config 2022-04-17 12:11:00 +02:00
75a41769bf refactor: db import ts refactor 2022-04-17 12:01:07 +02:00
5dbc127b51 refactor: db exporter ts refactor 2022-04-15 23:13:23 +02:00
a315eeae6c refactor: ipc handlers ts refactor 2022-04-15 14:56:13 +02:00
25a6fded2e refactor: sqlite client ts refactor 2022-04-14 18:25:13 +02:00
42fcded9f1 refactor: posgtre client ts refactor 2022-04-14 09:15:16 +02:00
186cb85d2e build: fixed module resolution and workers webpack config 2022-04-12 17:59:22 +02:00
fc1d6fba7f refactor: mysql client ts refactor 2022-04-12 17:08:05 +02:00
cbef7489b2 build: typescript config 2022-04-12 17:04:11 +02:00
96d6913a7a Merge pull request #229 from SmileYzn/patch-1
Update pt-BR.js
2022-04-11 15:18:26 +02:00
Cleverson
744c623914 Update pt-BR.js 2022-04-11 09:22:24 -03:00
174 changed files with 7648 additions and 6568 deletions

View File

@@ -6,15 +6,23 @@
}, },
"extends": [ "extends": [
"standard", "standard",
"plugin:vue/recommended" "plugin:@typescript-eslint/recommended",
"plugin:vue/vue3-recommended"
], ],
"parser": "vue-eslint-parser",
"parserOptions": { "parserOptions": {
"parser": "@babel/eslint-parser", "parser": "@typescript-eslint/parser",
"ecmaVersion": 9, "ecmaVersion": 9,
"sourceType": "module", "sourceType": "module",
"requireConfigFile": false "requireConfigFile": false
}, },
"plugins": [
"vue",
"@typescript-eslint"
],
"rules": { "rules": {
"space-infix-ops": "off",
"object-curly-newline": "off",
"indent": [ "indent": [
"error", "error",
3, 3,
@@ -69,6 +77,20 @@
"max": 1 "max": 1
} }
} }
] ],
"@typescript-eslint/member-delimiter-style": [
"warn",
{
"multiline": {
"delimiter": "semi",
"requireLast": true
},
"singleline": {
"delimiter": "semi",
"requireLast": false
}
}
],
"@typescript-eslint/no-var-requires": "off"
} }
} }

View File

@@ -26,14 +26,16 @@ If applicable, add screenshots to help explain your problem.
**Application (please complete the following information):** **Application (please complete the following information):**
- Client [e.g. MySQL] - App client [e.g. MySQL]
- Version [e.g. 0.14.0] - App version [e.g. 0.5.2]
- Distribution: [e.g. exe, Linux Store, AppImage, dmg] - Installation source: [e.g. exe, Linux Store, AppImage, dmg]
**Desktop (please complete the following information):** **Environment (please complete the following information):**
- OS: [e.g. Windows 11] - OS name: [e.g. Windows 11]
- Version [e.g. 21H2] - OS version [e.g. 21H2]
- DB name [e.g. MariaDB]
- DB version [e.g. 10.3.34]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

View File

@@ -21,9 +21,6 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm i run: npm i
- name: Run tests
run: npm run test
- name: Build/release Electron app - name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1 uses: samuelmeuli/action-electron-builder@v1

View File

@@ -22,9 +22,6 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm i run: npm i
- name: Run tests
run: npm run test
- name: Build/release Electron app - name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1 uses: samuelmeuli/action-electron-builder@v1
with: with:

View File

@@ -22,9 +22,6 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm i run: npm i
- name: Run tests
run: npm run test
- name: Build/release Electron app - name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1 uses: samuelmeuli/action-electron-builder@v1
with: with:

26
.github/workflows/test-e2e-linux.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Test end-to-end [linux]
on: push
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
steps:
- name: Check out Git repository
uses: actions/checkout@v2
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 14
- name: Install dependencies
run: npm i
- name: Run tests
run: npm run test:e2e

2
.gitignore vendored
View File

@@ -1,6 +1,8 @@
.DS_Store .DS_Store
dist dist
build build
misc/*
!misc/.gitkeep
node_modules node_modules
thumbs.db thumbs.db
NOTES.md NOTES.md

2
.vscode/launch.json vendored
View File

@@ -28,7 +28,7 @@
"sourceMaps": true, "sourceMaps": true,
"type": "node", "type": "node",
"timeout": 1000000 "timeout": 1000000
}, }
], ],
"compounds": [ "compounds": [
{ {

View File

@@ -2,6 +2,52 @@
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. 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.5.4](https://github.com/antares-sql/antares/compare/v0.5.3...v0.5.4) (2022-05-10)
### Bug Fixes
* app blocked by BIT fields with no default, closes [#256](https://github.com/antares-sql/antares/issues/256) ([e62f280](https://github.com/antares-sql/antares/commit/e62f280528edb0ff4550ee75038ea216e81e4f10))
* file upload input not working ([58611bf](https://github.com/antares-sql/antares/commit/58611bf07f343e6899a7446bfcd1247b0c75fc7f))
* SSH tunnel not working ([6d61518](https://github.com/antares-sql/antares/commit/6d6151814e5006935d493b9b83dbda1aa5b35391))
* unable to insert auto-generated datetime fields ([ff27244](https://github.com/antares-sql/antares/commit/ff272440bdc2a7fe699e04f8809bd5af8f9529c0))
### Improvements
* **UI:** left alignment for numbers in result tables, closes [#249](https://github.com/antares-sql/antares/issues/249) ([e02565c](https://github.com/antares-sql/antares/commit/e02565c0d9bb63efa76a79f38e3ed3586a30ad1c))
### [0.5.3](https://github.com/antares-sql/antares/compare/v0.5.2...v0.5.3) (2022-05-08)
### Features
* default open schema if only one present ([0fa22fb](https://github.com/antares-sql/antares/commit/0fa22fbe72fd4e3b2cfff6fc1847347f69fafda6))
### Bug Fixes
* can not use vue teleport with props as root element ([88eb9f7](https://github.com/antares-sql/antares/commit/88eb9f7ab80c407ec6517206c27cce715c41eb24))
* importer webpack config ([f82db96](https://github.com/antares-sql/antares/commit/f82db96f34579c5f5f0c025ba82aff13047eb045))
* importer webpack config ([b4d9821](https://github.com/antares-sql/antares/commit/b4d9821300991bf000846ae38ed18d79564db67b))
* LIKE operator in table filter not properly working, closes [#242](https://github.com/antares-sql/antares/issues/242) ([eb1afed](https://github.com/antares-sql/antares/commit/eb1afed108d97e2acb7ea5279aabd582014c3c58))
* locale change ([6af0c33](https://github.com/antares-sql/antares/commit/6af0c33461dfab868bca757e55ae62e6c6094c6c))
* missing storeToRefs imports ([ff8bb45](https://github.com/antares-sql/antares/commit/ff8bb45d6d19303b2e7701639d22e52e4ae2976c))
* missing tabs uid ([18b66b5](https://github.com/antares-sql/antares/commit/18b66b50323e69ed9d0090a4cbbe0ada9e10838d))
* **MySQL:** creating temporary tables on export to overcome view dependency errors ([e4c5d9b](https://github.com/antares-sql/antares/commit/e4c5d9b404c2b20cf113607a80ee928b810e6f05))
* **MySQL:** support to ANSI_QUOTES mode not working, closes [#243](https://github.com/antares-sql/antares/issues/243) ([9fcc7d1](https://github.com/antares-sql/antares/commit/9fcc7d1cefc3c035a63e843616c3138c772037f1))
* prevents query tabs targets previous schema if not in explorebar, closes [#236](https://github.com/antares-sql/antares/issues/236) ([5e4abd3](https://github.com/antares-sql/antares/commit/5e4abd3e81d70bcb9b3a897d38d6f8ec3ff0706b))
* query editor table fields suggestion ([e9dedfa](https://github.com/antares-sql/antares/commit/e9dedfaf3203f5621e54fd707003a47e6e5a5c4d))
* **SQLite:** tables with sqlite in name not visible, closes [#239](https://github.com/antares-sql/antares/issues/239) ([690cdcb](https://github.com/antares-sql/antares/commit/690cdcb2eb1c7bf514271000c31211d1c0b33e54))
* unable to add parameters to new routines/functions ([350d536](https://github.com/antares-sql/antares/commit/350d53642e2491fd2dedbbf999579ba9c9bc59a0))
* unable to disconnect from database ([47e1f27](https://github.com/antares-sql/antares/commit/47e1f27bb83b99288157185ad8b62d190a9e7881))
* unable to set an empty string as field default ([bfdb463](https://github.com/antares-sql/antares/commit/bfdb463390453a55963af29ef0e7cd3aea26f4cf))
* undefined uid variable in the setting bar ([3cca3ed](https://github.com/antares-sql/antares/commit/3cca3ed2b31c5763794c52cce34ccba1e848efe2))
* various warnings and exceptions ([c50cbc5](https://github.com/antares-sql/antares/commit/c50cbc577f667c17ba676960cf2c840d24366c80))
* verbose devtool loggin with missing sourcemap ([1174bab](https://github.com/antares-sql/antares/commit/1174bab0cc895c4e294804d810b0789166602725))
* wrong path module importation ([472fa6f](https://github.com/antares-sql/antares/commit/472fa6f4300b7e3fbf8c8079a548ca34d941f5fb))
* wrong path module importation ([9a0f982](https://github.com/antares-sql/antares/commit/9a0f98272342d201e9bc979d5bc5768cfd20f3fb))
### [0.5.2](https://github.com/antares-sql/antares/compare/v0.5.1...v0.5.2) (2022-04-10) ### [0.5.2](https://github.com/antares-sql/antares/compare/v0.5.1...v0.5.2) (2022-04-10)

View File

@@ -2,7 +2,7 @@
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. 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/). 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 application uses [Pinia🍍](https://pinia.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. 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). For every other question related to this project please [contact me](https://github.com/Fabio286).
@@ -14,7 +14,7 @@ The main files of the application are located inside `src` folder and are groupp
This folder contains small libraries, classes and objects. The purpose of `common` folder is to group together utilities used by **renderer** and **main** processes. 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. 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`. Client related customizations are stored on Pinia 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: An use case of customizations object can be the following:
@@ -62,12 +62,6 @@ The command to build Antares SQL locally is `npm run build:local`.
- [Order of words in component names](https://vuejs.org/v2/style-guide/#Order-of-words-in-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. - **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 ### 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. 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.

0
misc/.gitkeep Normal file
View File

View File

@@ -1,7 +1,7 @@
{ {
"name": "antares", "name": "antares",
"productName": "Antares", "productName": "Antares",
"version": "0.5.2", "version": "0.5.4",
"description": "A modern, fast and productivity driven SQL client with a focus in UX.", "description": "A modern, fast and productivity driven SQL client with a focus in UX.",
"license": "MIT", "license": "MIT",
"repository": "https://github.com/antares-sql/antares.git", "repository": "https://github.com/antares-sql/antares.git",
@@ -18,8 +18,10 @@
"rebuild:electron": "rimraf ./dist && npm run postinstall", "rebuild:electron": "rimraf ./dist && npm run postinstall",
"release": "standard-version", "release": "standard-version",
"release:pre": "npm run release -- --prerelease alpha", "release:pre": "npm run release -- --prerelease alpha",
"postinstall": "electron-builder install-app-deps", "devtools:install": "node scripts/devtoolsInstaller",
"test": "npm run compile && node tests/app.spec.js", "postinstall": "electron-builder install-app-deps && npm run devtools:install",
"test:e2e": "npm run compile && npm run test:e2e-dry",
"test:e2e-dry": "xvfb-maybe -- playwright test",
"lint": "eslint . --ext .js,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"", "lint": "eslint . --ext .js,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"",
"lint:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix", "lint:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix",
"contributors:add": "all-contributors add", "contributors:add": "all-contributors add",
@@ -27,6 +29,9 @@
}, },
"author": "Fabio Di Stasio <fabio286@gmail.com>", "author": "Fabio Di Stasio <fabio286@gmail.com>",
"main": "./dist/main.js", "main": "./dist/main.js",
"antares": {
"devtoolsId": "nhdogjmejiglipccpnnnanhbledajbpd"
},
"build": { "build": {
"appId": "com.fabio286.antares", "appId": "com.fabio286.antares",
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}", "artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
@@ -104,70 +109,83 @@
} }
}, },
"dependencies": { "dependencies": {
"@electron/remote": "^2.0.1", "@electron/remote": "~2.0.1",
"@mdi/font": "^6.1.95", "@faker-js/faker": "~6.1.2",
"@turf/helpers": "^6.5.0", "@mdi/font": "~6.1.95",
"@vscode/vscode-languagedetection": "^1.0.21", "@turf/helpers": "~6.5.0",
"ace-builds": "^1.4.13", "@vscode/vscode-languagedetection": "~1.0.21",
"better-sqlite3": "^7.5.0", "ace-builds": "~1.4.13",
"electron-log": "^4.4.1", "better-sqlite3": "~7.5.0",
"electron-store": "^8.0.1", "electron-log": "~4.4.1",
"electron-updater": "^4.6.1", "electron-store": "~8.0.1",
"electron-window-state": "^5.0.3", "electron-updater": "~5.0.1",
"faker": "^5.5.3", "electron-window-state": "~5.0.3",
"leaflet": "^1.7.1", "encoding": "~0.1.13",
"marked": "^4.0.0", "leaflet": "~1.7.1",
"moment": "^2.29.1", "marked": "~4.0.0",
"mysql2": "^2.3.2", "moment": "~2.29.1",
"pg": "^8.7.1", "mysql2": "~2.3.2",
"pg-query-stream": "^4.2.3", "pg": "~8.7.1",
"pgsql-ast-parser": "^7.2.1", "pg-query-stream": "~4.2.3",
"source-map-support": "^0.5.20", "pgsql-ast-parser": "~7.2.1",
"spectre.css": "^0.5.9", "pinia": "~2.0.13",
"sql-formatter": "^4.0.2", "source-map-support": "~0.5.20",
"ssh2-promise": "^1.0.2", "spectre.css": "~0.5.9",
"v-mask": "^2.3.0", "sql-formatter": "~4.0.2",
"vue-i18n": "^8.26.5", "ssh2-promise": "~1.0.2",
"vuedraggable": "^2.24.3", "v-mask": "~2.3.0",
"vuex": "^3.6.2" "vue": "~3.2.33",
"vue-i18n": "~9.1.9",
"vuedraggable": "~4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.15.7", "@babel/eslint-parser": "~7.15.7",
"@babel/preset-env": "^7.15.8", "@babel/preset-env": "~7.15.8",
"all-contributors-cli": "^6.20.0", "@babel/preset-typescript": "~7.16.7",
"babel-loader": "^8.2.3", "@playwright/test": "~1.21.1",
"chalk": "^4.1.2", "@types/better-sqlite3": "~7.5.0",
"cross-env": "^7.0.2", "@types/node": "~17.0.23",
"css-loader": "^6.5.0", "@types/pg": "~8.6.5",
"electron": "^17.0.1", "@typescript-eslint/eslint-plugin": "~5.18.0",
"electron-builder": "^22.14.11", "@typescript-eslint/parser": "~5.18.0",
"electron-devtools-installer": "^3.2.0", "@vue/compiler-sfc": "~3.2.33",
"eslint": "^7.32.0", "all-contributors-cli": "~6.20.0",
"eslint-config-standard": "^16.0.3", "babel-loader": "~8.2.3",
"eslint-plugin-import": "^2.24.2", "chalk": "~4.1.2",
"eslint-plugin-node": "^11.1.0", "cross-env": "~7.0.2",
"eslint-plugin-promise": "^5.2.0", "css-loader": "~6.5.0",
"eslint-plugin-vue": "^8.0.3", "electron": "~17.4.3",
"file-loader": "^6.2.0", "electron-builder": "~23.0.3",
"html-webpack-plugin": "^5.5.0", "eslint": "~7.32.0",
"eslint-config-standard": "~16.0.3",
"eslint-plugin-import": "~2.24.2",
"eslint-plugin-node": "~11.1.0",
"eslint-plugin-promise": "~5.2.0",
"eslint-plugin-vue": "~8.0.3",
"file-loader": "~6.2.0",
"html-webpack-plugin": "~5.5.0",
"mini-css-extract-plugin": "~2.4.5", "mini-css-extract-plugin": "~2.4.5",
"node-loader": "^2.0.0", "node-loader": "~2.0.0",
"playwright": "^1.18.1", "playwright": "~1.21.1",
"progress-webpack-plugin": "^1.0.12", "playwright-core": "~1.21.1",
"rimraf": "^3.0.2", "progress-webpack-plugin": "~1.0.12",
"sass": "^1.42.1", "rimraf": "~3.0.2",
"sass-loader": "^12.3.0", "sass": "~1.42.1",
"standard-version": "^9.3.1", "sass-loader": "~12.3.0",
"style-loader": "^3.3.1", "standard-version": "~9.3.1",
"stylelint": "^13.13.1", "style-loader": "~3.3.1",
"stylelint-config-standard": "^22.0.0", "stylelint": "~13.13.1",
"stylelint-scss": "^3.21.0", "stylelint-config-standard": "~22.0.0",
"tree-kill": "^1.2.2", "stylelint-scss": "~3.21.0",
"vue": "^2.6.14", "tree-kill": "~1.2.2",
"vue-loader": "^15.9.8", "ts-loader": "~9.2.8",
"vue-template-compiler": "^2.6.14", "typescript": "~4.6.3",
"webpack": "^5.60.0", "unzip-crx-3": "~0.2.0",
"webpack-cli": "^4.9.1", "vue-eslint-parser": "~8.3.0",
"webpack-dev-server": "^4.4.0" "vue-loader": "~16.8.3",
"webpack": "~5.60.0",
"webpack-cli": "~4.9.1",
"webpack-dev-server": "~4.4.0",
"xvfb-maybe": "~0.2.1"
} }
} }

View File

@@ -1,5 +1,6 @@
process.env.NODE_ENV = 'development'; process.env.NODE_ENV = 'development';
// process.env.ELECTRON_ENABLE_LOGGING = true // process.env.ELECTRON_ENABLE_LOGGING = true
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = false;
const chalk = require('chalk'); const chalk = require('chalk');
const electron = require('electron'); const electron = require('electron');
@@ -19,7 +20,7 @@ let manualRestart = null;
const remoteDebugging = process.argv.includes('--remote-debug'); const remoteDebugging = process.argv.includes('--remote-debug');
if (remoteDebugging) { if (remoteDebugging) {
// disable dvtools open in electron // disable devtools open in electron
process.env.RENDERER_REMOTE_DEBUGGING = true; process.env.RENDERER_REMOTE_DEBUGGING = true;
} }

View File

@@ -0,0 +1,48 @@
// @ts-check
const fs = require('fs');
const path = require('path');
const https = require('https');
const unzip = require('unzip-crx-3');
const { antares } = require('../package.json');
const extensionID = antares.devtoolsId;
const destFolder = path.resolve(__dirname, `../misc/${extensionID}`);
const filePath = path.resolve(__dirname, `${destFolder}${extensionID}.crx`);
const fileUrl = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${extensionID}%26uc&prodversion=32`;
const fileStream = fs.createWriteStream(filePath);
const downloadFile = url => {
return new Promise((resolve, reject) => {
const request = https.get(url);
request.on('response', response => {
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
return downloadFile(response.headers.location)
.then(resolve)
.catch(reject);
}
response.pipe(fileStream);
response.on('close', () => {
console.log('Devtools download completed!');
resolve();
});
response.on('error', reject);
});
request.on('error', reject);
request.end();
});
};
(async () => {
try {
await downloadFile(fileUrl);
await unzip(filePath, destFolder);
fs.unlinkSync(filePath);
fs.unlinkSync(`${destFolder}/package.json`);// <- Avoid to display annoyng npm script in vscode
}
catch (error) {
console.log(error);
}
})();

View File

@@ -134,7 +134,7 @@ export default class {
{ name: 'phoneNumberFormat', group: 'phone', types: ['string'] }, { name: 'phoneNumberFormat', group: 'phone', types: ['string'] },
{ name: 'phoneFormats', group: 'phone', types: ['string'] }, { name: 'phoneFormats', group: 'phone', types: ['string'] },
{ name: 'number', group: 'datatype', types: ['string', 'number'], params: ['min', 'max'] }, { name: 'number', group: 'random', types: ['string', 'number'], params: ['min', 'max'] },
{ name: 'float', group: 'random', types: ['string', 'float'], params: ['min', 'max'] }, { name: 'float', group: 'random', types: ['string', 'float'], params: ['min', 'max'] },
{ name: 'arrayElement', group: 'random', types: ['string'] }, { name: 'arrayElement', group: 'random', types: ['string'] },
{ name: 'arrayElements', group: 'random', types: ['string'] }, { name: 'arrayElements', group: 'random', types: ['string'] },
@@ -195,7 +195,7 @@ export default class {
return 1; return 1;
return 0; return 0;
}); ; });
} }
static getGroupsByType (type) { static getGroupsByType (type) {

View File

@@ -58,7 +58,7 @@ module.exports = [
}, },
{ {
name: 'DOUBLE', name: 'DOUBLE',
length: true, length: false,
collation: false, collation: false,
unsigned: false, unsigned: false,
zerofill: false zerofill: false

View File

@@ -0,0 +1,321 @@
import * as mysql from 'mysql2/promise';
import * as pg from 'pg';
import MysqlExporter from 'src/main/libs/exporters/sql/MysqlExporter';
import PostgreSQLExporter from 'src/main/libs/exporters/sql/PostgreSQLExporter';
import MySQLImporter from 'src/main/libs/importers/sql/MySQLlImporter';
import PostgreSQLImporter from 'src/main/libs/importers/sql/PostgreSQLImporter';
import SSHConfig from 'ssh2-promise/lib/sshConfig';
import { MySQLClient } from '../../main/libs/clients/MySQLClient';
import { PostgreSQLClient } from '../../main/libs/clients/PostgreSQLClient';
import { SQLiteClient } from '../../main/libs/clients/SQLiteClient';
export type Client = MySQLClient | PostgreSQLClient | SQLiteClient
export type ClientCode = 'mysql' | 'maria' | 'pg' | 'sqlite'
export type Exporter = MysqlExporter | PostgreSQLExporter
export type Importer = MySQLImporter | PostgreSQLImporter
/**
* Pasameters needed to create a new Antares connection to a database
*/
export interface ClientParams {
client: ClientCode;
params:
mysql.ConnectionOptions & {schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean}
| pg.ClientConfig & {schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean}
| { databasePath: string; readonly: boolean };
poolSize?: number;
logger?: () => void;
}
/**
* Paramenets insered by user in connection mask
*/
export interface ConnectionParams {
uid: string;
name?: string;
client: ClientCode;
host: string;
database?: string;
schema?: string;
databasePath?: string;
port: number;
user: string;
password: string;
ask: boolean;
readonly: boolean;
ssl: boolean;
cert?: string;
key?: string;
ca?: string;
untrustedConnection: boolean;
ciphers?: string;
ssh: boolean;
sshHost?: string;
sshUser?: string;
sshPass?: string;
sshKey?: string;
sshPort?: number;
sshPassphrase?: string;
}
export interface TypeInformations {
name: string;
length: boolean;
scale: boolean;
collation: boolean;
unsigned: boolean;
zerofill: boolean;
}
// Tables
export interface TableField {
name: string;
key: string;
type: string;
schema: string;
numPrecision?: number;
numLength?: number;
datePrecision?: number;
charLength?: number;
numScale?: number;
nullable?: boolean;
unsigned?: boolean;
zerofill?: boolean;
order?: number;
default?: number | string;
enumValues?: string;
charset?: string;
collation?: string;
autoIncrement?: boolean;
isArray?: boolean;
onUpdate?: string;
comment?: string;
after?: string;
orgName?: string;
}
export interface TableIndex {
name: string;
fields: string[];
type: string;
comment?: string;
indexType?: string;
indexComment?: string;
cardinality?: number;
oldType?: string;
oldName?: string;
}
export interface TableForeign {
constraintName: string;
refSchema: string;
table: string;
refTable: string;
field: string;
refField: string;
onUpdate: string;
onDelete: string;
oldName?: string;
}
export interface TableOptions {
name: string;
type?: 'table' | 'view';
engine?: string;
comment?: string;
collation?: string;
autoIncrement?: number;
}
export interface CreateTableParams {
/** Connection UID */
uid?: string;
schema: string;
fields: TableField[];
foreigns: TableForeign[];
indexes: TableIndex[];
options: TableOptions;
}
export interface AlterTableParams {
/** Connection UID */
uid?: string;
schema: string;
table: string;
additions: TableField[];
changes: TableField[];
deletions: TableField[];
tableStructure: {
name: string;
fields: TableField[];
foreigns: TableForeign[];
indexes: TableIndex[];
};
indexChanges: {
additions: TableIndex[];
changes: TableIndex[];
deletions: TableIndex[];
};
foreignChanges: {
additions: TableForeign[];
changes: TableForeign[];
deletions: TableForeign[];
};
options: TableOptions;
}
// Views
export interface CreateViewParams {
schema: string;
name: string;
algorithm: string;
definer: string;
security: string;
sql: string;
updateOption: string;
}
export interface AlterViewParams extends CreateViewParams {
oldName?: string;
}
// Triggers
export interface CreateTriggerParams {
definer?: string;
schema: string;
name: string;
activation: string;
event: string;
table: string;
sql: string;
}
export interface AlterTriggerParams extends CreateTriggerParams {
oldName?: string;
}
// Routines & Functions
export interface FunctionParam {
context: string;
name: string;
type: string;
length: number;
}
export interface CreateRoutineParams {
name: string;
parameters?: FunctionParam[];
definer: string;
schema: string;
deterministic: boolean;
dataAccess: string;
security: string;
comment?: string;
language?: string;
sql: string;
}
export interface AlterRoutineParams extends CreateRoutineParams {
oldName?: string;
}
export interface CreateFunctionParams {
name: string;
parameters?: FunctionParam[];
definer: string;
schema: string;
deterministic: boolean;
dataAccess: string;
security: string;
comment?: string;
sql: string;
returns: string;
returnsLength: number;
language?: string;
}
export interface AlterFunctionParams extends CreateFunctionParams {
oldName?: string;
}
// Events
export interface CreateEventParams {
definer?: string;
schema: string;
name: string;
execution: string;
every: string[];
starts: string;
ends: string;
at: string;
preserve: string;
state: string;
comment: string;
sql: string;
}
export interface AlterEventParams extends CreateEventParams {
oldName?: string;
}
// Query
export interface QueryBuilderObject {
schema: string;
select: string[];
from: string;
where: string[];
groupBy: string[];
orderBy: string[];
limit: number;
offset: number;
join: string[];
update: string[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
insert: {[key: string]: any}[];
delete: boolean;
}
export interface QueryParams {
nest?: boolean;
details?: boolean;
split?: boolean;
comments?: boolean;
autocommit?: boolean;
schema?: string;
tabUid?: string;
}
export interface QueryField {
name: string;
alias: string;
orgName: string;
schema: string;
table: string;
tableAlias: string;
orgTable: string;
type: string;
length: number;
}
export interface QueryForeign {
schema: string;
table: string;
field: string;
position: number;
constraintPosition: number;
constraintName: string;
refSchema: string;
refTable: string;
refField: string;
onUpdate: string;
onDelete: string;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface QueryResult<T = any> {
rows: T[];
report: { affectedRows: number };
fields: QueryField[];
keys: QueryForeign[];
duration: number;
}

View File

@@ -0,0 +1,28 @@
export interface TableParams {
table: string;
includeStructure: boolean;
includeContent: boolean;
includeDropStatement: boolean;
}
export interface ExportOptions {
schema: string;
includes: {
functions: boolean;
views: boolean;
triggers: boolean;
routines: boolean;
schedulers: boolean;
};
outputFormat: 'sql' | 'sql.zip';
outputFile: string;
sqlInsertAfter: number;
sqlInsertDivider: 'bytes' | 'rows';
}
export interface ExportState {
totalItems?: number;
currentItemIndex?: number;
currentItem?: string;
op?: string;
}

View File

@@ -0,0 +1,16 @@
import * as antares from './antares';
export interface ImportOptions {
uid: string;
schema: string;
type: antares.ClientCode;
file: string;
}
export interface ImportState {
fileSize?: number;
readPosition?: number;
percentage?: number;
queryCount?: number;
op?: string;
}

View File

View File

@@ -0,0 +1,20 @@
import { UsableLocale } from '@faker-js/faker';
export interface InsertRowsParams {
uid: string;
schema: string;
table: string;
row: {[key: string]: {
group: string;
method: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
params: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
length: number;
};
};
repeat: number;
fields: {[key: string]: string};
locale: UsableLocale;
}

View File

@@ -0,0 +1,7 @@
export type WorkerEvent = 'export-progress' | 'import-progress' | 'query-error' | 'end' | 'cancel' | 'error'
export interface WorkerIpcMessage {
type: WorkerEvent;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
payload: any;
}

View File

@@ -43,4 +43,4 @@ export function mimeFromHex (hex) {
} }
} }
} }
}; }

View File

@@ -5,4 +5,4 @@
*/ */
export function uidGen (prefix) { export function uidGen (prefix) {
return (prefix ? `${prefix}:` : '') + Math.random().toString(36).substr(2, 9).toUpperCase(); return (prefix ? `${prefix}:` : '') + Math.random().toString(36).substr(2, 9).toUpperCase();
}; }

View File

@@ -10,7 +10,7 @@ export default () => {
event.returnValue = key; event.returnValue = key;
}); });
ipcMain.handle('showOpenDialog', (event, options) => { ipcMain.handle('show-open-dialog', (event, options) => {
return dialog.showOpenDialog(options); return dialog.showOpenDialog(options);
}); });

View File

@@ -1,16 +1,29 @@
import * as antares from 'common/interfaces/antares';
import fs from 'fs'; import fs from 'fs';
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import { ClientsFactory } from '../libs/ClientsFactory'; import { ClientsFactory } from '../libs/ClientsFactory';
import { SslOptions } from 'mysql2';
export default connections => { export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('test-connection', async (event, conn) => { ipcMain.handle('test-connection', async (event, conn: antares.ConnectionParams) => {
const params = { const params = {
host: conn.host, host: conn.host,
port: +conn.port, port: +conn.port,
user: conn.user, user: conn.user,
password: conn.password, password: conn.password,
application_name: 'Antares SQL', readonly: conn.readonly,
readonly: conn.readonly database: '',
schema: '',
databasePath: '',
ssl: undefined as SslOptions,
ssh: undefined as {
host: string;
username: string;
password: string;
port: number;
privateKey: string;
passphrase: string;
}
}; };
if (conn.database) if (conn.database)
@@ -21,9 +34,9 @@ export default connections => {
if (conn.ssl) { if (conn.ssl) {
params.ssl = { params.ssl = {
key: conn.key ? fs.readFileSync(conn.key) : null, key: conn.key ? fs.readFileSync(conn.key).toString() : null,
cert: conn.cert ? fs.readFileSync(conn.cert) : null, cert: conn.cert ? fs.readFileSync(conn.cert).toString() : null,
ca: conn.ca ? fs.readFileSync(conn.ca) : null, ca: conn.ca ? fs.readFileSync(conn.ca).toString() : null,
ciphers: conn.ciphers, ciphers: conn.ciphers,
rejectUnauthorized: !conn.untrustedConnection rejectUnauthorized: !conn.untrustedConnection
}; };
@@ -35,7 +48,7 @@ export default connections => {
username: conn.sshUser, username: conn.sshUser,
password: conn.sshPass, password: conn.sshPass,
port: conn.sshPort ? conn.sshPort : 22, port: conn.sshPort ? conn.sshPort : 22,
privateKey: conn.sshKey ? fs.readFileSync(conn.sshKey) : null, privateKey: conn.sshKey ? fs.readFileSync(conn.sshKey).toString() : null,
passphrase: conn.sshPassphrase passphrase: conn.sshPassphrase
}; };
} }
@@ -61,14 +74,26 @@ export default connections => {
return uid in connections; return uid in connections;
}); });
ipcMain.handle('connect', async (event, conn) => { ipcMain.handle('connect', async (event, conn: antares.ConnectionParams) => {
const params = { const params = {
host: conn.host, host: conn.host,
port: +conn.port, port: +conn.port,
user: conn.user, user: conn.user,
password: conn.password, password: conn.password,
application_name: 'Antares SQL', application_name: 'Antares SQL',
readonly: conn.readonly readonly: conn.readonly,
database: '',
schema: '',
databasePath: '',
ssl: undefined as SslOptions,
ssh: undefined as {
host: string;
username: string;
password: string;
port: number;
privateKey: string;
passphrase: string;
}
}; };
if (conn.database) if (conn.database)
@@ -82,9 +107,9 @@ export default connections => {
if (conn.ssl) { if (conn.ssl) {
params.ssl = { params.ssl = {
key: conn.key ? fs.readFileSync(conn.key) : null, key: conn.key ? fs.readFileSync(conn.key).toString() : null,
cert: conn.cert ? fs.readFileSync(conn.cert) : null, cert: conn.cert ? fs.readFileSync(conn.cert).toString() : null,
ca: conn.ca ? fs.readFileSync(conn.ca) : null, ca: conn.ca ? fs.readFileSync(conn.ca).toString() : null,
ciphers: conn.ciphers, ciphers: conn.ciphers,
rejectUnauthorized: !conn.untrustedConnection rejectUnauthorized: !conn.untrustedConnection
}; };
@@ -96,7 +121,7 @@ export default connections => {
username: conn.sshUser, username: conn.sshUser,
password: conn.sshPass, password: conn.sshPass,
port: conn.sshPort ? conn.sshPort : 22, port: conn.sshPort ? conn.sshPort : 22,
privateKey: conn.sshKey ? fs.readFileSync(conn.sshKey) : null, privateKey: conn.sshKey ? fs.readFileSync(conn.sshKey).toString() : null,
passphrase: conn.sshPassphrase passphrase: conn.sshPassphrase
}; };
} }

View File

@@ -1,6 +1,7 @@
import * as antares from 'common/interfaces/antares';
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
export default (connections) => { export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('get-function-informations', async (event, params) => { ipcMain.handle('get-function-informations', async (event, params) => {
try { try {
const result = await connections[params.uid].getFunctionInformations(params); const result = await connections[params.uid].getFunctionInformations(params);

View File

@@ -1,3 +1,5 @@
import * as antares from 'common/interfaces/antares';
import connection from './connection'; import connection from './connection';
import tables from './tables'; import tables from './tables';
import views from './views'; import views from './views';
@@ -10,7 +12,7 @@ import application from './application';
import schema from './schema'; import schema from './schema';
import users from './users'; import users from './users';
const connections = {}; const connections: {[key: string]: antares.Client} = {};
export default () => { export default () => {
connection(connections); connection(connections);

View File

@@ -1,6 +1,7 @@
import * as antares from 'common/interfaces/antares';
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
export default (connections) => { export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('get-routine-informations', async (event, params) => { ipcMain.handle('get-routine-informations', async (event, params) => {
try { try {
const result = await connections[params.uid].getRoutineInformations(params); const result = await connections[params.uid].getRoutineInformations(params);

View File

@@ -1,6 +1,7 @@
import * as antares from 'common/interfaces/antares';
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
export default (connections) => { export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('get-scheduler-informations', async (event, params) => { ipcMain.handle('get-scheduler-informations', async (event, params) => {
try { try {
const result = await connections[params.uid].getEventInformations(params); const result = await connections[params.uid].getEventInformations(params);

View File

@@ -1,14 +1,15 @@
import fs from 'fs'; import * as antares from 'common/interfaces/antares';
import path from 'path'; import * as workers from 'common/interfaces/workers';
import { fork } from 'child_process'; import * as fs from 'fs';
import * as path from 'path';
import { ChildProcess, fork } from 'child_process';
import { ipcMain, dialog } from 'electron'; import { ipcMain, dialog } from 'electron';
// @TODO: need some factories
const isDevelopment = process.env.NODE_ENV !== 'production'; const isDevelopment = process.env.NODE_ENV !== 'production';
export default connections => { export default (connections: {[key: string]: antares.Client}) => {
let exporter = null; let exporter: ChildProcess = null;
let importer = null; let importer: ChildProcess = null;
ipcMain.handle('create-schema', async (event, params) => { ipcMain.handle('create-schema', async (event, params) => {
try { try {
@@ -51,9 +52,7 @@ export default connections => {
return { return {
status: 'success', status: 'success',
response: collation.rows.length response: collation
? collation.rows[0].DEFAULT_COLLATION_NAME
: ''
}; };
} }
catch (err) { catch (err) {
@@ -175,7 +174,7 @@ export default connections => {
ipcMain.handle('export', (event, { uid, type, tables, ...rest }) => { ipcMain.handle('export', (event, { uid, type, tables, ...rest }) => {
if (exporter !== null) return; if (exporter !== null) return;
return new Promise((resolve, reject) => { return new Promise((resolve/*, reject */) => {
(async () => { (async () => {
if (fs.existsSync(rest.outputFile)) { // If file exists ask for replace if (fs.existsSync(rest.outputFile)) { // If file exists ask for replace
const result = await dialog.showMessageBox({ const result = await dialog.showMessageBox({
@@ -211,7 +210,7 @@ export default connections => {
}); });
// Exporter message listener // Exporter message listener
exporter.on('message', ({ type, payload }) => { exporter.on('message', ({ type, payload }: workers.WorkerIpcMessage) => {
switch (type) { switch (type) {
case 'export-progress': case 'export-progress':
event.sender.send('export-progress', payload); event.sender.send('export-progress', payload);
@@ -244,7 +243,7 @@ export default connections => {
}); });
}); });
ipcMain.handle('abort-export', async event => { ipcMain.handle('abort-export', async () => {
let willAbort = false; let willAbort = false;
if (exporter) { if (exporter) {
@@ -268,7 +267,7 @@ export default connections => {
ipcMain.handle('import-sql', async (event, options) => { ipcMain.handle('import-sql', async (event, options) => {
if (importer !== null) return; if (importer !== null) return;
return new Promise((resolve, reject) => { return new Promise((resolve/*, reject */) => {
(async () => { (async () => {
const dbConfig = await connections[options.uid].getDbConfig(); const dbConfig = await connections[options.uid].getDbConfig();
@@ -283,7 +282,7 @@ export default connections => {
}); });
// Importer message listener // Importer message listener
importer.on('message', ({ type, payload }) => { importer.on('message', ({ type, payload }: workers.WorkerIpcMessage) => {
switch (type) { switch (type) {
case 'import-progress': case 'import-progress':
event.sender.send('import-progress', payload); event.sender.send('import-progress', payload);
@@ -314,7 +313,7 @@ export default connections => {
}); });
}); });
ipcMain.handle('abort-import-sql', async event => { ipcMain.handle('abort-import-sql', async () => {
let willAbort = false; let willAbort = false;
if (importer) { if (importer) {

View File

@@ -1,12 +1,14 @@
import * as antares from 'common/interfaces/antares';
import { InsertRowsParams } from 'common/interfaces/tableApis';
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import faker from 'faker'; import { faker } from '@faker-js/faker';
import moment from 'moment'; import * as moment from 'moment';
import { sqlEscaper } from 'common/libs/sqlEscaper'; import { sqlEscaper } from 'common/libs/sqlEscaper';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes';
import * as customizations from 'common/customizations'; import * as customizations from 'common/customizations';
import fs from 'fs'; import fs from 'fs';
export default (connections) => { export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('get-table-columns', async (event, params) => { ipcMain.handle('get-table-columns', async (event, params) => {
try { try {
const result = await connections[params.uid].getTableColumns(params); const result = await connections[params.uid].getTableColumns(params);
@@ -196,7 +198,8 @@ export default (connections) => {
ipcMain.handle('delete-table-rows', async (event, params) => { ipcMain.handle('delete-table-rows', async (event, params) => {
if (params.primary) { if (params.primary) {
const idString = params.rows.map(row => { // eslint-disable-next-line @typescript-eslint/no-explicit-any
const idString = params.rows.map((row: {[key: string]: any}) => {
const fieldName = Object.keys(row)[0].includes('.') ? `${params.table}.${params.primary}` : params.primary; const fieldName = Object.keys(row)[0].includes('.') ? `${params.table}.${params.primary}` : params.primary;
return typeof row[fieldName] === 'string' return typeof row[fieldName] === 'string'
@@ -245,7 +248,8 @@ export default (connections) => {
ipcMain.handle('insert-table-rows', async (event, params) => { ipcMain.handle('insert-table-rows', async (event, params) => {
try { // TODO: move to client classes try { // TODO: move to client classes
const insertObj = {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any
const insertObj: {[key: string]: any} = {};
for (const key in params.row) { for (const key in params.row) {
const type = params.fields[key]; const type = params.fields[key];
let escapedParam; let escapedParam;
@@ -312,12 +316,14 @@ export default (connections) => {
} }
}); });
ipcMain.handle('insert-table-fake-rows', async (event, params) => { ipcMain.handle('insert-table-fake-rows', async (event, params: InsertRowsParams) => {
try { // TODO: move to client classes try { // TODO: move to client classes
const rows = []; // eslint-disable-next-line @typescript-eslint/no-explicit-any
const rows: {[key: string]: any}[] = [];
for (let i = 0; i < +params.repeat; i++) { for (let i = 0; i < +params.repeat; i++) {
const insertObj = {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any
const insertObj: {[key: string]: any} = {};
for (const key in params.row) { for (const key in params.row) {
const type = params.fields[key]; const type = params.fields[key];
@@ -375,7 +381,8 @@ export default (connections) => {
insertObj[key] = escapedParam; insertObj[key] = escapedParam;
} }
else { // Faker value else { // Faker value
const parsedParams = {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any
const parsedParams: {[key: string]: any} = {};
let fakeValue; let fakeValue;
if (params.locale) if (params.locale)
@@ -386,10 +393,12 @@ export default (connections) => {
if (!isNaN(params.row[key].params[param])) if (!isNaN(params.row[key].params[param]))
parsedParams[param] = +params.row[key].params[param]; parsedParams[param] = +params.row[key].params[param];
}); });
fakeValue = faker[params.row[key].group][params.row[key].method](parsedParams); // eslint-disable-next-line @typescript-eslint/no-explicit-any
fakeValue = (faker as any)[params.row[key].group][params.row[key].method](parsedParams);
} }
else else
fakeValue = faker[params.row[key].group][params.row[key].method](); // eslint-disable-next-line @typescript-eslint/no-explicit-any
fakeValue = (faker as any)[params.row[key].group][params.row[key].method]();
if (typeof fakeValue === 'string') { if (typeof fakeValue === 'string') {
if (params.row[key].length) if (params.row[key].length)

View File

@@ -1,6 +1,7 @@
import * as antares from 'common/interfaces/antares';
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
export default (connections) => { export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('get-trigger-informations', async (event, params) => { ipcMain.handle('get-trigger-informations', async (event, params) => {
try { try {
const result = await connections[params.uid].getTriggerInformations(params); const result = await connections[params.uid].getTriggerInformations(params);

View File

@@ -4,8 +4,8 @@ import Store from 'electron-store';
const persistentStore = new Store({ name: 'settings' }); const persistentStore = new Store({ name: 'settings' });
const isMacOS = process.platform === 'darwin'; const isMacOS = process.platform === 'darwin';
let mainWindow; let mainWindow: Electron.IpcMainEvent;
autoUpdater.allowPrerelease = persistentStore.get('allow_prerelease', true); autoUpdater.allowPrerelease = persistentStore.get('allow_prerelease', true) as boolean;
export default () => { export default () => {
ipcMain.on('check-for-updates', event => { ipcMain.on('check-for-updates', event => {
@@ -50,7 +50,7 @@ export default () => {
mainWindow.reply('update-downloaded'); mainWindow.reply('update-downloaded');
}); });
autoUpdater.logger = require('electron-log'); // autoUpdater.logger = require('electron-log');
autoUpdater.logger.transports.console.format = '{h}:{i}:{s} {text}'; // autoUpdater.logger.transports.console.format = '{h}:{i}:{s} {text}';
autoUpdater.logger.transports.file.level = 'info'; // autoUpdater.logger.transports.file.level = 'info';
}; };

View File

@@ -1,6 +1,7 @@
import * as antares from 'common/interfaces/antares';
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
export default (connections) => { export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('get-users', async (event, uid) => { ipcMain.handle('get-users', async (event, uid) => {
try { try {
const result = await connections[uid].getUsers(); const result = await connections[uid].getUsers();

View File

@@ -1,6 +1,7 @@
import * as antares from 'common/interfaces/antares';
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
export default (connections) => { export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('get-view-informations', async (event, params) => { ipcMain.handle('get-view-informations', async (event, params) => {
try { try {
const result = await connections[params.uid].getViewInformations(params); const result = await connections[params.uid].getViewInformations(params);

View File

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

View File

@@ -0,0 +1,260 @@
import * as antares from 'common/interfaces/antares';
import mysql from 'mysql2/promise';
import * as pg from 'pg';
import SSH2Promise from 'ssh2-promise';
const queryLogger = (sql: string) => {
// Remove comments, newlines and multiple spaces
const escapedSql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '').replace(/\s\s+/g, ' ');
console.log(escapedSql);
};
/**
* As Simple As Possible Query Builder Core
*/
export class AntaresCore {
_client: antares.ClientCode;
protected _params: mysql.ConnectionOptions | pg.ClientConfig | { databasePath: string; readonly: boolean};
protected _poolSize: number;
protected _ssh?: SSH2Promise;
protected _logger: (sql: string) => void;
protected _queryDefaults: antares.QueryBuilderObject;
protected _query: antares.QueryBuilderObject;
constructor (args: antares.ClientParams) {
this._client = args.client;
this._params = args.params;
this._poolSize = args.poolSize || undefined;
this._logger = args.logger || queryLogger;
this._queryDefaults = {
schema: '',
select: [],
from: '',
where: [],
groupBy: [],
orderBy: [],
limit: null,
offset: null,
join: [],
update: [],
insert: [],
delete: false
};
this._query = Object.assign({}, this._queryDefaults);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected _reducer (acc: string[], curr: any) {
const type = typeof curr;
switch (type) {
case 'number':
case 'string':
return [...acc, curr];
case 'object':
if (Array.isArray(curr))
return [...acc, ...curr];
else {
const clausoles = [];
for (const key in curr)
clausoles.push(`${key} ${curr[key]}`);
return clausoles;
}
}
}
private _resetQuery () {
this._query = Object.assign({}, this._queryDefaults);
}
schema (schema: string) {
this._query.schema = schema;
return this;
}
select (...args: string[]) {
this._query.select = [...this._query.select, ...args];
return this;
}
from (table: string) {
this._query.from = table;
return this;
}
into (table: string) {
this._query.from = table;
return this;
}
delete (table: string) {
this._query.delete = true;
this.from(table);
return this;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
where (...args: any) {
this._query.where = [...this._query.where, ...args];
return this;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
groupBy (...args: any) {
this._query.groupBy = [...this._query.groupBy, ...args];
return this;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
orderBy (...args: any) {
this._query.orderBy = [...this._query.orderBy, ...args];
return this;
}
limit (limit: number) {
this._query.limit = limit;
return this;
}
offset (offset: number) {
this._query.offset = offset;
return this;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
update (...args: any) {
this._query.update = [...this._query.update, ...args];
return this;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
insert (arr: {[key: string]: any}[]) {
this._query.insert = [...this._query.insert, ...arr];
return this;
}
getSQL (): string {
throw new Error('Client must implement the "getSQL" method');
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
raw<T = antares.QueryResult> (_sql: string, _args?: antares.QueryParams): Promise<T> {
throw new Error('Client must implement the "raw" method');
}
run<RowType> (args?: antares.QueryParams) {
const rawQuery = this.getSQL();
this._resetQuery();
return this.raw<antares.QueryResult<RowType>>(rawQuery, args);
}
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
getDbConfig () {
throw new Error('Method "getDbConfig" not implemented');
}
createSchema (...args: any) {
throw new Error('Method "createSchema" not implemented');
}
alterSchema (...args: any) {
throw new Error('Method "alterSchema" not implemented');
}
dropSchema (...args: any) {
throw new Error('Method "dropSchema" not implemented');
}
getDatabaseCollation (...args: any) {
throw new Error('Method "getDatabaseCollation" not implemented');
}
getFunctionInformations (...args: any) {
throw new Error('Method "getFunctionInformations" not implemented');
}
alterFunction (...args: any) {
throw new Error('Method "alterFunction" not implemented');
}
createTriggerFunction (...args: any) {
throw new Error('Method "createTriggerFunction" not implemented');
}
alterTriggerFunction (...args: any) {
throw new Error('Method "alterTriggerFunction" not implemented');
}
createFunction (...args: any) {
throw new Error('Method "createFunction" not implemented');
}
dropFunction (...args: any) {
throw new Error('Method "dropFunction" not implemented');
}
getCollations () {
throw new Error('Method "getCollations" not implemented');
}
getRoutineInformations (...args: any) {
throw new Error('Method "getRoutineInformations" not implemented');
}
dropRoutine (...args: any) {
throw new Error('Method "dropRoutine" not implemented');
}
alterRoutine (...args: any) {
throw new Error('Method "alterRoutine" not implemented');
}
createRoutine (...args: any) {
throw new Error('Method "createRoutine" not implemented');
}
getVariables () {
throw new Error('Method "getVariables" not implemented');
}
getEventInformations (...args: any) {
throw new Error('Method "getEventInformations" not implemented');
}
dropEvent (...args: any) {
throw new Error('Method "dropEvent" not implemented');
}
alterEvent (...args: any) {
throw new Error('Method "alterEvent" not implemented');
}
createEvent (...args: any) {
throw new Error('Method "createEvent" not implemented');
}
enableEvent (...args: any) {
throw new Error('Method "enableEvent" not implemented');
}
disableEvent (...args: any) {
throw new Error('Method "disableEvent" not implemented');
}
enableTrigger (...args: any) {
throw new Error('Method "enableTrigger" not implemented');
}
disableTrigger (...args: any) {
throw new Error('Method "disableTrigger" not implemented');
}
killTabQuery (...args: any) {
throw new Error('Method "killTabQuery" not implemented');
}
/* eslint-enable @typescript-eslint/no-unused-vars */
/* eslint-enable @typescript-eslint/no-explicit-any */
}

View File

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

View File

@@ -0,0 +1,20 @@
import * as antares from 'common/interfaces/antares';
import { MySQLClient } from './clients/MySQLClient';
import { PostgreSQLClient } from './clients/PostgreSQLClient';
import { SQLiteClient } from './clients/SQLiteClient';
export class ClientsFactory {
static getClient (args: antares.ClientParams) {
switch (args.client) {
case 'mysql':
case 'maria':
return new MySQLClient(args);
case 'pg':
return new PostgreSQLClient(args);
case 'sqlite':
return new SQLiteClient(args);
default:
throw new Error(`Unknown database client: ${args.client}`);
}
}
}

View File

@@ -1,26 +1,28 @@
'use strict'; import * as antares from 'common/interfaces/antares';
import sqlite from 'better-sqlite3'; import * as sqlite from 'better-sqlite3';
import { AntaresCore } from '../AntaresCore'; import { AntaresCore } from '../AntaresCore';
import dataTypes from 'common/data-types/sqlite'; import * as dataTypes from 'common/data-types/sqlite';
import { NUMBER, FLOAT, TIME, DATETIME } from 'common/fieldTypes'; import { NUMBER, FLOAT, TIME, DATETIME } from 'common/fieldTypes';
export class SQLiteClient extends AntaresCore { export class SQLiteClient extends AntaresCore {
constructor (args) { private _schema?: string;
private _connectionsToCommit: Map<string, sqlite.Database>;
protected _connection?: sqlite.Database;
_params: { databasePath: string; readonly: boolean};
constructor (args: antares.ClientParams) {
super(args); super(args);
this._schema = null; this._schema = null;
this._connectionsToCommit = new Map(); this._connectionsToCommit = new Map();
} }
_getTypeInfo (type) { getTypeInfo (type: string): antares.TypeInformations {
return dataTypes return dataTypes
.reduce((acc, group) => [...acc, ...group.types], []) .reduce((acc, group) => [...acc, ...group.types], [])
.filter(_type => _type.name === type.toUpperCase())[0]; .filter(_type => _type.name === type.toUpperCase())[0];
} }
/**
* @memberof SQLiteClient
*/
async connect () { async connect () {
this._connection = this.getConnection(); this._connection = this.getConnection();
} }
@@ -32,40 +34,44 @@ export class SQLiteClient extends AntaresCore {
}); });
} }
/** destroy (): void {
* @memberof SQLiteClient return null;
*/ }
destroy () {}
/** use (): void {
* Executes an USE query return null;
* }
* @memberof SQLiteClient
*/
use () {}
/** async getStructure (schemas: Set<string>) {
* @param {Array} schemas list /* eslint-disable camelcase */
* @returns {Array.<Object>} databases scructure interface ShowTableResult {
* @memberof SQLiteClient Db?: string;
*/ type: string;
async getStructure (schemas) { name: string;
const { rows: databases } = await this.raw('SELECT * FROM pragma_database_list'); tbl_name: string;
rootpage:4;
sql: string;
}
type ShowTriggersResult = ShowTableResult
/* eslint-enable camelcase */
const { rows: databases } = await this.raw<antares.QueryResult<{ name: string}>>('SELECT * FROM pragma_database_list');
const filteredDatabases = databases; const filteredDatabases = databases;
const tablesArr = []; const tablesArr: ShowTableResult[] = [];
const triggersArr = []; const triggersArr: ShowTriggersResult[] = [];
let schemaSize = 0; let schemaSize = 0;
for (const db of filteredDatabases) { for (const db of filteredDatabases) {
if (!schemas.has(db.name)) continue; if (!schemas.has(db.name)) continue;
let { rows: tables } = await this.raw(` let { rows: tables } = await this.raw<antares.QueryResult<ShowTableResult>>(`
SELECT * SELECT *
FROM "${db.name}".sqlite_master FROM "${db.name}".sqlite_master
WHERE type IN ('table', 'view') WHERE type IN ('table', 'view')
AND name NOT LIKE 'sqlite_%' AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\'
ORDER BY name ORDER BY name
`); `);
if (tables.length) { if (tables.length) {
@@ -76,7 +82,7 @@ export class SQLiteClient extends AntaresCore {
tablesArr.push(...tables); tablesArr.push(...tables);
} }
let { rows: triggers } = await this.raw(`SELECT * FROM "${db.name}".sqlite_master WHERE type='trigger'`); let { rows: triggers } = await this.raw<antares.QueryResult<ShowTriggersResult>>(`SELECT * FROM "${db.name}".sqlite_master WHERE type='trigger'`);
if (triggers.length) { if (triggers.length) {
triggers = triggers.map(trigger => { triggers = triggers.map(trigger => {
trigger.Db = db.name; trigger.Db = db.name;
@@ -133,22 +139,24 @@ export class SQLiteClient extends AntaresCore {
}); });
} }
/** async getTableColumns ({ schema, table }: { schema: string; table: string }) {
* @param {Object} params interface TableColumnsResult {
* @param {String} params.schema cid: number;
* @param {String} params.table name: string;
* @returns {Object} table scructure type: string;
* @memberof SQLiteClient notnull: 0 | 1;
*/ // eslint-disable-next-line camelcase
async getTableColumns ({ schema, table }) { dflt_value: string;
const { rows: fields } = await this.raw(`SELECT * FROM "${schema}".pragma_table_info('${table}')`); pk: 0 | 1;
}
const { rows: fields } = await this.raw<antares.QueryResult<TableColumnsResult>>(`SELECT * FROM "${schema}".pragma_table_info('${table}')`);
return fields.map(field => { return fields.map(field => {
const [type, length] = field.type.includes('(') const [type, length]: [string, number?] = field.type.includes('(')
? field.type.replace(')', '').split('(').map(el => { ? field.type.replace(')', '').split('(').map((el: string | number) => {
if (!isNaN(el)) el = +el; if (!isNaN(Number(el))) el = Number(el);
return el; return el;
}) }) as [string, number?]
: [field.type, null]; : [field.type, null];
return { return {
@@ -174,54 +182,50 @@ export class SQLiteClient extends AntaresCore {
}); });
} }
/** async getTableApproximateCount ({ schema, table }: { schema: string; table: string }): Promise<number> {
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table row count
* @memberof SQLiteClient
*/
async getTableApproximateCount ({ schema, table }) {
const { rows } = await this.raw(`SELECT COUNT(*) AS count FROM "${schema}"."${table}"`); const { rows } = await this.raw(`SELECT COUNT(*) AS count FROM "${schema}"."${table}"`);
return rows.length ? rows[0].count : 0; return rows.length ? rows[0].count : 0;
} }
/** async getTableOptions ({ table }: { table: string }) {
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table options
* @memberof SQLiteClient
*/
async getTableOptions ({ schema, table }) {
return { name: table }; return { name: table };
} }
/** async getTableIndexes ({ schema, table }: { schema: string; table: string }) {
* @param {Object} params interface TableColumnsResult {
* @param {String} params.schema type: string;
* @param {String} params.table name: string;
* @returns {Object} table indexes // eslint-disable-next-line camelcase
* @memberof SQLiteClient tbl_name: string;
*/ rootpage:4;
async getTableIndexes ({ schema, table }) { sql: string;
}
interface ShowIndexesResult {
seq: number;
name: string;
unique: 0 | 1;
origin: string;
partial: 0 | 1;
}
const remappedIndexes = []; const remappedIndexes = [];
const { rows: primaryKeys } = await this.raw(`SELECT * FROM "${schema}".pragma_table_info('${table}') WHERE pk != 0`); const { rows: primaryKeys } = await this.raw<antares.QueryResult<TableColumnsResult>>(`SELECT * FROM "${schema}".pragma_table_info('${table}') WHERE pk != 0`);
for (const key of primaryKeys) { for (const key of primaryKeys) {
remappedIndexes.push({ remappedIndexes.push({
name: 'PRIMARY', name: 'PRIMARY',
column: key.name, column: key.name,
indexType: null, indexType: null as never,
type: 'PRIMARY', type: 'PRIMARY',
cardinality: null, cardinality: null as never,
comment: '', comment: '',
indexComment: '' indexComment: ''
}); });
} }
const { rows: indexes } = await this.raw(`SELECT * FROM "${schema}".pragma_index_list('${table}');`); const { rows: indexes } = await this.raw<antares.QueryResult<ShowIndexesResult>>(`SELECT * FROM "${schema}".pragma_index_list('${table}');`);
for (const index of indexes) { for (const index of indexes) {
const { rows: details } = await this.raw(`SELECT * FROM "${schema}".pragma_index_info('${index.name}');`); const { rows: details } = await this.raw(`SELECT * FROM "${schema}".pragma_index_info('${index.name}');`);
@@ -230,9 +234,9 @@ export class SQLiteClient extends AntaresCore {
remappedIndexes.push({ remappedIndexes.push({
name: index.name, name: index.name,
column: detail.name, column: detail.name,
indexType: null, indexType: null as never,
type: index.unique === 1 ? 'UNIQUE' : 'INDEX', type: index.unique === 1 ? 'UNIQUE' : 'INDEX',
cardinality: null, cardinality: null as never,
comment: '', comment: '',
indexComment: '' indexComment: ''
}); });
@@ -242,15 +246,19 @@ export class SQLiteClient extends AntaresCore {
return remappedIndexes; return remappedIndexes;
} }
/** async getKeyUsage ({ schema, table }: { schema: string; table: string }) {
* @param {Object} params /* eslint-disable camelcase */
* @param {String} params.schema interface KeyResult {
* @param {String} params.table from: string;
* @returns {Object} table key usage id: number;
* @memberof SQLiteClient table: string;
*/ to: string;
async getKeyUsage ({ schema, table }) { on_update: string;
const { rows } = await this.raw(`SELECT * FROM "${schema}".pragma_foreign_key_list('${table}');`); on_delete: string;
}
/* eslint-enable camelcase */
const { rows } = await this.raw<antares.QueryResult<KeyResult>>(`SELECT * FROM "${schema}".pragma_foreign_key_list('${table}');`);
return rows.map(field => { return rows.map(field => {
return { return {
@@ -269,229 +277,11 @@ export class SQLiteClient extends AntaresCore {
}); });
} }
async getUsers () {} async getUsers (): Promise<void> {
return null;
/**
* SHOW CREATE VIEW
*
* @returns {Array.<Object>} view informations
* @memberof SQLiteClient
*/
async getViewInformations ({ schema, view }) {
const sql = `SELECT "sql" FROM "${schema}".sqlite_master WHERE "type"='view' AND name='${view}'`;
const results = await this.raw(sql);
return results.rows.map(row => {
return {
sql: row.sql.match(/(?<=AS ).*?$/gs)[0],
name: view
};
})[0];
} }
/** async createTable (params: antares.CreateTableParams) {
* DROP VIEW
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async dropView (params) {
const sql = `DROP VIEW "${params.schema}"."${params.view}"`;
return await this.raw(sql);
}
/**
* ALTER VIEW
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async alterView (params) {
const { view } = params;
try {
await this.dropView({ schema: view.schema, view: view.oldName });
await this.createView(view);
}
catch (err) {
return Promise.reject(err);
}
}
/**
* CREATE VIEW
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async createView (params) {
const sql = `CREATE VIEW "${params.schema}"."${params.name}" AS ${params.sql}`;
return await this.raw(sql);
}
/**
* SHOW CREATE TRIGGER
*
* @returns {Array.<Object>} view informations
* @memberof SQLiteClient
*/
async getTriggerInformations ({ schema, trigger }) {
const sql = `SELECT "sql" FROM "${schema}".sqlite_master WHERE "type"='trigger' AND name='${trigger}'`;
const results = await this.raw(sql);
return results.rows.map(row => {
return {
sql: row.sql.match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
name: trigger,
table: row.sql.match(/(?<=ON `).*?(?=`)/gs)[0],
activation: row.sql.match(/(BEFORE|AFTER)/gs)[0],
event: row.sql.match(/(INSERT|UPDATE|DELETE)/gs)[0]
};
})[0];
}
/**
* DROP TRIGGER
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async dropTrigger (params) {
const sql = `DROP TRIGGER \`${params.schema}\`.\`${params.trigger}\``;
return await this.raw(sql);
}
/**
* ALTER TRIGGER
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async alterTrigger (params) {
const { trigger } = params;
const tempTrigger = Object.assign({}, trigger);
tempTrigger.name = `Antares_${tempTrigger.name}_tmp`;
try {
await this.createTrigger(tempTrigger);
await this.dropTrigger({ schema: trigger.schema, trigger: tempTrigger.name });
await this.dropTrigger({ schema: trigger.schema, trigger: trigger.oldName });
await this.createTrigger(trigger);
}
catch (err) {
return Promise.reject(err);
}
}
/**
* CREATE TRIGGER
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async createTrigger (params) {
const sql = `CREATE ${params.definer ? `DEFINER=${params.definer} ` : ''}TRIGGER \`${params.schema}\`.\`${params.name}\` ${params.activation} ${params.event} ON \`${params.table}\` FOR EACH ROW ${params.sql}`;
return await this.raw(sql, { split: false });
}
/**
* SHOW COLLATION
*
* @returns {Array.<Object>} collations list
* @memberof SQLiteClient
*/
async getCollations () {
return [];
}
/**
* SHOW VARIABLES
*
* @returns {Array.<Object>} variables list
* @memberof SQLiteClient
*/
async getVariables () {
return [];
}
/**
* SHOW ENGINES
*
* @returns {Array.<Object>} engines list
* @memberof SQLiteClient
*/
async getEngines () {
return {
name: 'SQLite',
support: 'YES',
comment: '',
isDefault: true
};
}
/**
* SHOW VARIABLES LIKE '%vers%'
*
* @returns {Array.<Object>} version parameters
* @memberof SQLiteClient
*/
async getVersion () {
const os = require('os');
const sql = 'SELECT sqlite_version() AS version';
const { rows } = await this.raw(sql);
return {
number: rows[0].version,
name: 'SQLite',
arch: process.arch,
os: `${os.type()} ${os.release()}`
};
}
async getProcesses () {}
async killProcess () {}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async commitTab (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
connection.prepare('COMMIT').run();
return this.destroyConnectionToCommit(tabUid);
}
}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async rollbackTab (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
connection.prepare('ROLLBACK').run();
return this.destroyConnectionToCommit(tabUid);
}
}
destroyConnectionToCommit (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
connection.close();
this._connectionsToCommit.delete(tabUid);
}
}
/**
* CREATE TABLE
*
* @returns {Promise<null>}
* @memberof SQLiteClient
*/
async createTable (params) {
const { const {
schema, schema,
fields, fields,
@@ -499,24 +289,24 @@ export class SQLiteClient extends AntaresCore {
indexes, indexes,
options options
} = params; } = params;
const newColumns = []; const newColumns: string[] = [];
const newIndexes = []; const newIndexes: string[] = [];
const manageIndexes = []; const manageIndexes: string[] = [];
const newForeigns = []; const newForeigns: string[] = [];
let sql = `CREATE TABLE "${schema}"."${options.name}"`; let sql = `CREATE TABLE "${schema}"."${options.name}"`;
// ADD FIELDS // ADD FIELDS
fields.forEach(field => { fields.forEach(field => {
const typeInfo = this._getTypeInfo(field.type); const typeInfo = this.getTypeInfo(field.type);
const length = typeInfo?.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false; const length = typeInfo?.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false;
newColumns.push(`"${field.name}" newColumns.push(`"${field.name}"
${field.type.toUpperCase()}${length && length !== true ? `(${length})` : ''} ${field.type.toUpperCase()}${length ? `(${length})` : ''}
${field.unsigned ? 'UNSIGNED' : ''} ${field.unsigned ? 'UNSIGNED' : ''}
${field.nullable ? 'NULL' : 'NOT NULL'} ${field.nullable ? 'NULL' : 'NOT NULL'}
${field.autoIncrement ? 'AUTO_INCREMENT' : ''} ${field.autoIncrement ? 'AUTO_INCREMENT' : ''}
${field.default ? `DEFAULT ${field.default}` : ''} ${field.default !== null ? `DEFAULT ${field.default || '\'\''}` : ''}
${field.onUpdate ? `ON UPDATE ${field.onUpdate}` : ''}`); ${field.onUpdate ? `ON UPDATE ${field.onUpdate}` : ''}`);
}); });
@@ -542,34 +332,37 @@ export class SQLiteClient extends AntaresCore {
return await this.raw(sql); return await this.raw(sql);
} }
/** async alterTable (params: antares.AlterTableParams) {
* ALTER TABLE const {
* table,
* @returns {Promise<null>} schema,
* @memberof SQLiteClient additions,
*/ deletions,
async alterTable (params) { changes,
tableStructure
} = params;
try { try {
await this.raw('BEGIN TRANSACTION'); await this.raw('BEGIN TRANSACTION');
await this.raw('PRAGMA foreign_keys = 0'); await this.raw('PRAGMA foreign_keys = 0');
const tmpName = `Antares_${params.table}_tmp`; const tmpName = `Antares_${table}_tmp`;
await this.raw(`CREATE TABLE "${tmpName}" AS SELECT * FROM "${params.table}"`); await this.raw(`CREATE TABLE "${tmpName}" AS SELECT * FROM "${table}"`);
await this.dropTable(params); await this.dropTable(params);
const createTableParams = { const createTableParams = {
schema: params.schema, schema: schema,
fields: params.tableStructure.fields, fields: tableStructure.fields,
foreigns: params.tableStructure.foreigns, foreigns: tableStructure.foreigns,
indexes: params.tableStructure.indexes.filter(index => !index.name.includes('sqlite_autoindex')), indexes: tableStructure.indexes.filter(index => !index.name.includes('sqlite_autoindex')),
options: { name: params.tableStructure.name } options: { name: tableStructure.name }
}; };
await this.createTable(createTableParams); await this.createTable(createTableParams);
const insertFields = createTableParams.fields const insertFields = createTableParams.fields
.filter(field => { .filter(field => {
return ( return (
params.additions.every(add => add.name !== field.name) && additions.every(add => add.name !== field.name) &&
params.deletions.every(del => del.name !== field.name) deletions.every(del => del.name !== field.name)
); );
}) })
.reduce((acc, curr) => { .reduce((acc, curr) => {
@@ -578,7 +371,7 @@ export class SQLiteClient extends AntaresCore {
}, []); }, []);
const selectFields = insertFields.map(field => { const selectFields = insertFields.map(field => {
const renamedField = params.changes.find(change => `"${change.name}"` === field); const renamedField = changes.find(change => `"${change.name}"` === field);
if (renamedField) if (renamedField)
return `"${renamedField.orgName}"`; return `"${renamedField.orgName}"`;
return field; return field;
@@ -586,7 +379,7 @@ export class SQLiteClient extends AntaresCore {
await this.raw(`INSERT INTO "${createTableParams.options.name}" (${insertFields.join(',')}) SELECT ${selectFields.join(',')} FROM "${tmpName}"`); await this.raw(`INSERT INTO "${createTableParams.options.name}" (${insertFields.join(',')}) SELECT ${selectFields.join(',')} FROM "${tmpName}"`);
await this.dropTable({ schema: params.schema, table: tmpName }); await this.dropTable({ schema: schema, table: tmpName });
await this.raw('PRAGMA foreign_keys = 1'); await this.raw('PRAGMA foreign_keys = 1');
await this.raw('COMMIT'); await this.raw('COMMIT');
} }
@@ -596,43 +389,147 @@ export class SQLiteClient extends AntaresCore {
} }
} }
/** async duplicateTable (params: { schema: string; table: string }) { // TODO: retrive table informations and create a copy
* DUPLICATE TABLE
*
* @returns {Promise<null>}
* @memberof SQLiteClient
*/
async duplicateTable (params) { // TODO: retrive table informations and create a copy
const sql = `CREATE TABLE "${params.schema}"."${params.table}_copy" AS SELECT * FROM "${params.schema}"."${params.table}"`; const sql = `CREATE TABLE "${params.schema}"."${params.table}_copy" AS SELECT * FROM "${params.schema}"."${params.table}"`;
return await this.raw(sql); return await this.raw(sql);
} }
/** async truncateTable (params: { schema: string; table: string }) {
* TRUNCATE TABLE
*
* @returns {Promise<null>}
* @memberof SQLiteClient
*/
async truncateTable (params) {
const sql = `DELETE FROM "${params.schema}"."${params.table}"`; const sql = `DELETE FROM "${params.schema}"."${params.table}"`;
return await this.raw(sql); return await this.raw(sql);
} }
/** async dropTable (params: { schema: string; table: string }) {
* DROP TABLE
*
* @returns {Promise<null>}
* @memberof SQLiteClient
*/
async dropTable (params) {
const sql = `DROP TABLE "${params.schema}"."${params.table}"`; const sql = `DROP TABLE "${params.schema}"."${params.table}"`;
return await this.raw(sql); return await this.raw(sql);
} }
/** async getViewInformations ({ schema, view }: { schema: string; view: string }) {
* @returns {String} SQL string const sql = `SELECT "sql" FROM "${schema}".sqlite_master WHERE "type"='view' AND name='${view}'`;
* @memberof SQLiteClient const results = await this.raw(sql);
*/
return results.rows.map(row => {
return {
sql: row.sql.match(/(?<=AS ).*?$/gs)[0],
name: view
};
})[0];
}
async dropView (params: { schema: string; view: string }) {
const sql = `DROP VIEW "${params.schema}"."${params.view}"`;
return await this.raw(sql);
}
async alterView ({ view }: { view: antares.AlterViewParams }) {
try {
await this.dropView({ schema: view.schema, view: view.oldName });
await this.createView(view);
}
catch (err) {
return Promise.reject(err);
}
}
async createView (params: antares.CreateViewParams) {
const sql = `CREATE VIEW "${params.schema}"."${params.name}" AS ${params.sql}`;
return await this.raw(sql);
}
async getTriggerInformations ({ schema, trigger }: { schema: string; trigger: string }) {
const sql = `SELECT "sql" FROM "${schema}".sqlite_master WHERE "type"='trigger' AND name='${trigger}'`;
const results = await this.raw(sql);
return results.rows.map(row => {
return {
sql: row.sql.match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
name: trigger,
table: row.sql.match(/(?<=ON `).*?(?=`)/gs)[0],
activation: row.sql.match(/(BEFORE|AFTER)/gs)[0],
event: row.sql.match(/(INSERT|UPDATE|DELETE)/gs)[0]
};
})[0];
}
async dropTrigger (params: { schema: string; trigger: string }) {
const sql = `DROP TRIGGER \`${params.schema}\`.\`${params.trigger}\``;
return await this.raw(sql);
}
async alterTrigger ({ trigger } : {trigger: antares.AlterTriggerParams}) {
const tempTrigger = Object.assign({}, trigger);
tempTrigger.name = `Antares_${tempTrigger.name}_tmp`;
try {
await this.createTrigger(tempTrigger);
await this.dropTrigger({ schema: trigger.schema, trigger: tempTrigger.name });
await this.dropTrigger({ schema: trigger.schema, trigger: trigger.oldName });
await this.createTrigger(trigger);
}
catch (err) {
return Promise.reject(err);
}
}
async createTrigger (params: antares.CreateTriggerParams) {
const sql = `CREATE ${params.definer ? `DEFINER=${params.definer} ` : ''}TRIGGER \`${params.schema}\`.\`${params.name}\` ${params.activation} ${params.event} ON \`${params.table}\` FOR EACH ROW ${params.sql}`;
return await this.raw(sql, { split: false });
}
async getEngines () {
return {
name: 'SQLite',
support: 'YES',
comment: '',
isDefault: true
};
}
async getVersion () {
const os = require('os');
const sql = 'SELECT sqlite_version() AS version';
const { rows } = await this.raw(sql);
return {
number: rows[0].version,
name: 'SQLite',
arch: process.arch,
os: `${os.type()} ${os.release()}`
};
}
async getProcesses (): Promise<void> {
return null;
}
async killProcess (): Promise<void> {
return null;
}
async commitTab (tabUid: string) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
connection.prepare('COMMIT').run();
return this.destroyConnectionToCommit(tabUid);
}
}
async rollbackTab (tabUid: string) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
connection.prepare('ROLLBACK').run();
return this.destroyConnectionToCommit(tabUid);
}
}
destroyConnectionToCommit (tabUid: string) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
connection.close();
this._connectionsToCommit.delete(tabUid);
}
}
getSQL () { getSQL () {
// SELECT // SELECT
const selectArray = this._query.select.reduce(this._reducer, []); const selectArray = this._query.select.reduce(this._reducer, []);
@@ -680,24 +577,15 @@ export class SQLiteClient extends AntaresCore {
const orderByRaw = orderByArray.length ? `ORDER BY ${orderByArray.join(', ')} ` : ''; const orderByRaw = orderByArray.length ? `ORDER BY ${orderByArray.join(', ')} ` : '';
// LIMIT // LIMIT
const limitRaw = this._query.limit.length ? `LIMIT ${this._query.limit.join(', ')} ` : ''; const limitRaw = this._query.limit ? `LIMIT ${this._query.limit} ` : '';
// OFFSET // OFFSET
const offsetRaw = this._query.offset.length ? `OFFSET ${this._query.offset.join(', ')} ` : ''; const offsetRaw = this._query.offset ? `OFFSET ${this._query.offset} ` : '';
return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}${offsetRaw}${insertRaw}`; return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}${offsetRaw}${insertRaw}`;
} }
/** async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) {
* @param {string} sql raw SQL query
* @param {object} args
* @param {boolean} args.nest
* @param {boolean} args.details
* @param {boolean} args.split
* @returns {Promise}
* @memberof SQLiteClient
*/
async raw (sql, args) {
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
args = { args = {
@@ -720,7 +608,7 @@ export class SQLiteClient extends AntaresCore {
.map(q => q.trim()) .map(q => q.trim())
: [sql]; : [sql];
let connection; let connection: sqlite.Database;
if (!args.autocommit && args.tabUid) { // autocommit OFF if (!args.autocommit && args.tabUid) { // autocommit OFF
if (this._connectionsToCommit.has(args.tabUid)) if (this._connectionsToCommit.has(args.tabUid))
@@ -738,31 +626,33 @@ export class SQLiteClient extends AntaresCore {
if (!query) continue; if (!query) continue;
const timeStart = new Date(); const timeStart = new Date();
let timeStop; let timeStop;
const keysArr = []; const keysArr: antares.QueryForeign[] = [];
const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => { const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => {
(async () => { (async () => {
let queryResult; let queryRunResult: sqlite.RunResult;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let queryAllResult: any[];
let affectedRows; let affectedRows;
let fields; let fields;
const detectedTypes = {}; const detectedTypes: {[key: string]: string} = {};
try { try {
const stmt = connection.prepare(query); const stmt = connection.prepare(query);
if (stmt.reader) { if (stmt.reader) {
queryResult = stmt.all(); queryAllResult = stmt.all();
fields = stmt.columns(); fields = stmt.columns();
if (queryResult.length) { if (queryAllResult.length) {
fields.forEach(field => { fields.forEach(field => {
detectedTypes[field.name] = typeof queryResult[0][field.name]; detectedTypes[field.name] = typeof queryAllResult[0][field.name];
}); });
} }
} }
else { else {
const info = queryResult = stmt.run(); queryRunResult = stmt.run();
affectedRows = info.changes; affectedRows = queryRunResult.changes;
} }
} }
catch (err) { catch (err) {
@@ -773,18 +663,18 @@ export class SQLiteClient extends AntaresCore {
let remappedFields = fields let remappedFields = fields
? fields.map(field => { ? fields.map(field => {
let [parsedType, length] = field.type?.includes('(') let [parsedType, length]: [string, number?] = field.type?.includes('(')
? field.type.replace(')', '').split('(').map(el => { ? field.type.replace(')', '').split('(').map((el: string | number) => {
if (!isNaN(el)) if (!isNaN(Number(el)))
el = +el; el = Number(el);
else else
el = el.trim(); el = (el as string).trim();
return el; return el;
}) }) as [string, number?]
: [field.type, null]; : [field.type, null];
if ([...TIME, ...DATETIME].includes(parsedType)) { if ([...TIME, ...DATETIME].includes(parsedType)) {
const firstNotNull = queryResult.find(res => res[field.name] !== null); const firstNotNull = queryAllResult.find(res => res[field.name] !== null);
if (firstNotNull && firstNotNull[field.name].includes('.')) if (firstNotNull && firstNotNull[field.name].includes('.'))
length = firstNotNull[field.name].split('.').pop().length; length = firstNotNull[field.name].split('.').pop().length;
} }
@@ -798,7 +688,8 @@ export class SQLiteClient extends AntaresCore {
tableAlias: field.table, tableAlias: field.table,
orgTable: field.table, orgTable: field.table,
type: field.type !== null ? parsedType : detectedTypes[field.name], type: field.type !== null ? parsedType : detectedTypes[field.name],
length length,
key: undefined as string
}; };
}).filter(Boolean) }).filter(Boolean)
: []; : [];
@@ -818,18 +709,12 @@ export class SQLiteClient extends AntaresCore {
const indexes = await this.getTableIndexes(paramObj); const indexes = await this.getTableIndexes(paramObj);
remappedFields = remappedFields.map(field => { remappedFields = remappedFields.map(field => {
// const detailedField = columns.find(f => f.name === field.name);
const fieldIndex = indexes.find(i => i.column === field.name); const fieldIndex = indexes.find(i => i.column === field.name);
if (field.table === paramObj.table && field.schema === paramObj.schema) { if (field.table === paramObj.table && field.schema === paramObj.schema) {
// if (detailedField) {
// const length = detailedField.numPrecision || detailedField.charLength || detailedField.datePrecision || null;
// field = { ...field, ...detailedField, length };
// }
if (fieldIndex) { if (fieldIndex) {
const key = fieldIndex.type === 'PRIMARY' ? 'pri' : fieldIndex.type === 'UNIQUE' ? 'uni' : 'mul'; const key = fieldIndex.type === 'PRIMARY' ? 'pri' : fieldIndex.type === 'UNIQUE' ? 'uni' : 'mul';
field = { ...field, key }; field = { ...field, key };
}; }
} }
return field; return field;
@@ -842,8 +727,8 @@ export class SQLiteClient extends AntaresCore {
} }
resolve({ resolve({
duration: timeStop - timeStart, duration: timeStop.getTime() - timeStart.getTime(),
rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false, rows: Array.isArray(queryAllResult) ? queryAllResult.some(el => Array.isArray(el)) ? [] : queryAllResult : false,
report: affectedRows !== undefined ? { affectedRows } : null, report: affectedRows !== undefined ? { affectedRows } : null,
fields: remappedFields, fields: remappedFields,
keys: keysArr keys: keysArr
@@ -854,6 +739,16 @@ export class SQLiteClient extends AntaresCore {
resultsArr.push({ rows, report, fields, keys, duration }); resultsArr.push({ rows, report, fields, keys, duration });
} }
return resultsArr.length === 1 ? resultsArr[0] : resultsArr; const result = resultsArr.length === 1 ? resultsArr[0] : resultsArr;
return result as unknown as T;
}
getVariables (): null[] {
return [];
}
getCollations (): null[] {
return [];
} }
} }

View File

@@ -1,10 +1,18 @@
import fs from 'fs'; import * as exporter from 'common/interfaces/exporter';
import { createGzip } from 'zlib'; import * as fs from 'fs';
import path from 'path'; import { createGzip, Gzip } from 'zlib';
import EventEmitter from 'events'; import * as path from 'path';
import * as EventEmitter from 'events';
export class BaseExporter extends EventEmitter { export class BaseExporter extends EventEmitter {
constructor (tables, options) { protected _tables;
protected _options;
protected _isCancelled;
protected _outputFileStream: fs.WriteStream;
protected _processedStream: fs.WriteStream | Gzip;
protected _state;
constructor (tables: exporter.TableParams[], options: exporter.ExportOptions) {
super(); super();
this._tables = tables; this._tables = tables;
this._options = options; this._options = options;
@@ -60,11 +68,11 @@ export class BaseExporter extends EventEmitter {
this.emitUpdate({ op: 'cancelling' }); this.emitUpdate({ op: 'cancelling' });
} }
emitUpdate (state) { emitUpdate (state: exporter.ExportState) {
this.emit('progress', { ...this._state, ...state }); this.emit('progress', { ...this._state, ...state });
} }
writeString (data) { writeString (data: string) {
if (this._isCancelled) return; if (this._isCancelled) return;
try { try {

View File

@@ -1,14 +1,20 @@
import * as exporter from 'common/interfaces/exporter';
import * as mysql from 'mysql2/promise';
import { SqlExporter } from './SqlExporter'; import { SqlExporter } from './SqlExporter';
import { BLOB, BIT, DATE, DATETIME, FLOAT, SPATIAL, IS_MULTI_SPATIAL, NUMBER } from 'common/fieldTypes'; import { BLOB, BIT, DATE, DATETIME, FLOAT, SPATIAL, IS_MULTI_SPATIAL, NUMBER } from 'common/fieldTypes';
import hexToBinary from 'common/libs/hexToBinary'; import hexToBinary from 'common/libs/hexToBinary';
import { getArrayDepth } from 'common/libs/getArrayDepth'; import { getArrayDepth } from 'common/libs/getArrayDepth';
import moment from 'moment'; import * as moment from 'moment';
import { lineString, point, polygon } from '@turf/helpers'; import { lineString, point, polygon } from '@turf/helpers';
import { MySQLClient } from '../../clients/MySQLClient';
export default class MysqlExporter extends SqlExporter { export default class MysqlExporter extends SqlExporter {
constructor (...args) { protected _client: MySQLClient;
super(...args);
constructor (client: MySQLClient, tables: exporter.TableParams[], options: exporter.ExportOptions) {
super(tables, options);
this._client = client;
this._commentChar = '#'; this._commentChar = '#';
} }
@@ -42,7 +48,7 @@ ${footer}
`; `;
} }
async getCreateTable (tableName) { async getCreateTable (tableName: string) {
const { rows } = await this._client.raw( const { rows } = await this._client.raw(
`SHOW CREATE TABLE \`${this.schemaName}\`.\`${tableName}\`` `SHOW CREATE TABLE \`${this.schemaName}\`.\`${tableName}\``
); );
@@ -54,11 +60,11 @@ ${footer}
return rows[0][col] + ';'; return rows[0][col] + ';';
} }
getDropTable (tableName) { getDropTable (tableName: string) {
return `DROP TABLE IF EXISTS \`${tableName}\`;`; return `DROP TABLE IF EXISTS \`${tableName}\`;`;
} }
async * getTableInsert (tableName) { async * getTableInsert (tableName: string) {
let rowCount = 0; let rowCount = 0;
let sqlStr = ''; let sqlStr = '';
@@ -109,7 +115,7 @@ ${footer}
queryLength = 0; queryLength = 0;
rowsWritten = 0; rowsWritten = 0;
} }
else if (parseInt(rowIndex) === 0) sqlInsertString += '\n\t('; else if (rowIndex === 0) sqlInsertString += '\n\t(';
else sqlInsertString += ',\n\t('; else sqlInsertString += ',\n\t(';
for (const i in notGeneratedColumns) { for (const i in notGeneratedColumns) {
@@ -124,7 +130,7 @@ ${footer}
} }
else if (DATETIME.includes(column.type)) { else if (DATETIME.includes(column.type)) {
let datePrecision = ''; let datePrecision = '';
for (let i = 0; i < column.precision; i++) for (let i = 0; i < column.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S'; datePrecision += i === 0 ? '.S' : 'S';
sqlInsertString += moment(val).isValid() sqlInsertString += moment(val).isValid()
@@ -144,7 +150,7 @@ ${footer}
if (IS_MULTI_SPATIAL.includes(column.type)) { if (IS_MULTI_SPATIAL.includes(column.type)) {
const features = []; const features = [];
for (const element of val) for (const element of val)
features.push(this.getMarkers(element)); features.push(this._getGeoJSON(element));
geoJson = { geoJson = {
type: 'FeatureCollection', type: 'FeatureCollection',
@@ -191,11 +197,35 @@ ${footer}
); );
let sqlString = ''; let sqlString = '';
sqlString += this.buildComment('Creating temporary tables to overcome VIEW dependency errors');
// Temporary tables
for (const view of views) { for (const view of views) {
sqlString += `DROP VIEW IF EXISTS \`${view.Name}\`;\n`; const viewFields = await this._client.getTableColumns({ schema: this.schemaName, table: view.Name });
const tableFields: string[] = [];
for (const field of viewFields) {
const typeInfo = this._client.getTypeInfo(field.type);
const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false;
tableFields.push(`\`${field.name}\` ${field.type.toUpperCase()}${length ? `(${length}${field.numScale ? `,${field.numScale}` : ''})` : ''} ${field.unsigned ? 'UNSIGNED' : ''} ${field.zerofill ? 'ZEROFILL' : ''} ${field.nullable ? 'NULL' : 'NOT NULL'} ${field.autoIncrement ? 'AUTO_INCREMENT' : ''} ${field.collation ? `COLLATE ${field.collation}` : ''}`);
}
sqlString +=
`
CREATE TABLE \`${view.Name}\`(
${tableFields.join(',\n\t')}
);`;
sqlString += '\n';
}
sqlString += '\n';
for (const view of views) {
sqlString += `DROP TABLE IF EXISTS \`${view.Name}\`;\n`;
const viewSyntax = await this.getCreateTable(view.Name); const viewSyntax = await this.getCreateTable(view.Name);
sqlString += viewSyntax.replaceAll('`' + this.schemaName + '`.', ''); sqlString += viewSyntax.replaceAll('`' + this.schemaName + '`.', '');
sqlString += '\n'; sqlString += '\n\n';
} }
return sqlString; return sqlString;
@@ -323,7 +353,7 @@ ${footer}
return sqlString; return sqlString;
} }
async getRoutineSyntax (name, type, definer) { async getRoutineSyntax (name: string, type: string, definer: string) {
const { rows: routines } = await this._client.raw( const { rows: routines } = await this._client.raw(
`SHOW CREATE ${type} \`${this.schemaName}\`.\`${name}\`` `SHOW CREATE ${type} \`${this.schemaName}\`.\`${name}\``
); );
@@ -353,12 +383,13 @@ ${footer}
return sqlString; return sqlString;
} }
async _queryStream (sql) { async _queryStream (sql: string) {
if (process.env.NODE_ENV === 'development') console.log('EXPORTER:', sql); if (process.env.NODE_ENV === 'development') console.log('EXPORTER:', sql);
const isPool = typeof this._client._connection.getConnection === 'function'; const isPool = 'getConnection' in this._client._connection;
const connection = isPool ? await this._client._connection.getConnection() : this._client._connection; const connection = isPool ? await (this._client._connection as mysql.Pool).getConnection() : this._client._connection;
const stream = connection.connection.query(sql).stream(); // eslint-disable-next-line @typescript-eslint/no-explicit-any
const dispose = () => connection.destroy(); const stream = (connection as any).connection.query(sql).stream();
const dispose = () => (connection as mysql.PoolConnection).release();
stream.on('end', dispose); stream.on('end', dispose);
stream.on('error', dispose); stream.on('error', dispose);
@@ -366,17 +397,17 @@ ${footer}
return stream; return stream;
} }
getEscapedDefiner (definer) { getEscapedDefiner (definer: string) {
return definer return definer
.split('@') .split('@')
.map(part => '`' + part + '`') .map(part => '`' + part + '`')
.join('@'); .join('@');
} }
escapeAndQuote (val) { escapeAndQuote (val: string) {
// eslint-disable-next-line no-control-regex // eslint-disable-next-line no-control-regex
const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g; const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g;
const CHARS_ESCAPE_MAP = { const CHARS_ESCAPE_MAP: {[key: string]: string} = {
'\0': '\\0', '\0': '\\0',
'\b': '\\b', '\b': '\\b',
'\t': '\\t', '\t': '\\t',
@@ -405,14 +436,16 @@ ${footer}
return `'${escapedVal}'`; return `'${escapedVal}'`;
} }
_getGeoJSON (val) { /* eslint-disable @typescript-eslint/no-explicit-any */
_getGeoJSON (val: any) {
if (Array.isArray(val)) { if (Array.isArray(val)) {
if (getArrayDepth(val) === 1) if (getArrayDepth(val) === 1)
return lineString(val.reduce((acc, curr) => [...acc, [curr.x, curr.y]], [])); return lineString(val.reduce((acc, curr) => [...acc, [curr.x, curr.y]], []));
else else
return polygon(val.map(arr => arr.reduce((acc, curr) => [...acc, [curr.x, curr.y]], []))); return polygon(val.map(arr => arr.reduce((acc: any, curr: any) => [...acc, [curr.x, curr.y]], [])));
} }
else else
return point([val.x, val.y]); return point([val.x, val.y]);
} }
/* eslint-enable @typescript-eslint/no-explicit-any */
} }

View File

@@ -1,12 +1,21 @@
import * as antares from 'common/interfaces/antares';
import * as exporter from 'common/interfaces/exporter';
import { SqlExporter } from './SqlExporter'; import { SqlExporter } from './SqlExporter';
import { BLOB, BIT, DATE, DATETIME, FLOAT, NUMBER, TEXT_SEARCH } from 'common/fieldTypes'; import { BLOB, BIT, DATE, DATETIME, FLOAT, NUMBER, TEXT_SEARCH } from 'common/fieldTypes';
import hexToBinary from 'common/libs/hexToBinary'; import hexToBinary from 'common/libs/hexToBinary';
import { getArrayDepth } from 'common/libs/getArrayDepth'; import * as moment from 'moment';
import moment from 'moment'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment
import { lineString, point, polygon } from '@turf/helpers'; // @ts-ignore
import QueryStream from 'pg-query-stream'; import * as QueryStream from 'pg-query-stream';
import { PostgreSQLClient } from '../../clients/PostgreSQLClient';
export default class PostgreSQLExporter extends SqlExporter { export default class PostgreSQLExporter extends SqlExporter {
constructor (client: PostgreSQLClient, tables: exporter.TableParams[], options: exporter.ExportOptions) {
super(tables, options);
this._client = client;
}
async getSqlHeader () { async getSqlHeader () {
let dump = await super.getSqlHeader(); let dump = await super.getSqlHeader();
dump += ` dump += `
@@ -30,11 +39,28 @@ SET row_security = off;\n\n\n`;
return dump; return dump;
} }
async getCreateTable (tableName) { async getCreateTable (tableName: string) {
/* eslint-disable camelcase */
interface SequenceRecord {
sequence_catalog: string;
sequence_schema: string;
sequence_name: string;
data_type: string;
numeric_precision: number;
numeric_precision_radix: number;
numeric_scale: number;
start_value: string;
minimum_value: string;
maximum_value: string;
increment: string;
cycle_option: string;
}
/* eslint-enable camelcase */
let createSql = ''; let createSql = '';
const sequences = []; const sequences = [];
const columnsSql = []; const columnsSql = [];
const arrayTypes = { const arrayTypes: {[key: string]: string} = {
_int2: 'smallint', _int2: 'smallint',
_int4: 'integer', _int4: 'integer',
_int8: 'bigint', _int8: 'bigint',
@@ -60,7 +86,7 @@ SET row_security = off;\n\n\n`;
if (fieldType === 'USER-DEFINED') fieldType = `"${this.schemaName}".${column.udt_name}`; if (fieldType === 'USER-DEFINED') fieldType = `"${this.schemaName}".${column.udt_name}`;
else if (fieldType === 'ARRAY') { else if (fieldType === 'ARRAY') {
if (Object.keys(arrayTypes).includes(fieldType)) if (Object.keys(arrayTypes).includes(fieldType))
fieldType = arrayTypes[type] + '[]'; fieldType = arrayTypes[column.udt_name] + '[]';
else else
fieldType = column.udt_name.replaceAll('_', '') + '[]'; fieldType = column.udt_name.replaceAll('_', '') + '[]';
} }
@@ -91,7 +117,7 @@ SET row_security = off;\n\n\n`;
.schema('information_schema') .schema('information_schema')
.from('sequences') .from('sequences')
.where({ sequence_schema: `= '${this.schemaName}'`, sequence_name: `= '${sequence}'` }) .where({ sequence_schema: `= '${this.schemaName}'`, sequence_name: `= '${sequence}'` })
.run(); .run<SequenceRecord>();
if (rows.length) { if (rows.length) {
createSql += `CREATE SEQUENCE "${this.schemaName}"."${sequence}" createSql += `CREATE SEQUENCE "${this.schemaName}"."${sequence}"
@@ -119,7 +145,7 @@ SET row_security = off;\n\n\n`;
.schema('pg_catalog') .schema('pg_catalog')
.from('pg_indexes') .from('pg_indexes')
.where({ schemaname: `= '${this.schemaName}'`, tablename: `= '${tableName}'` }) .where({ schemaname: `= '${this.schemaName}'`, tablename: `= '${tableName}'` })
.run(); .run<{indexdef: string}>();
for (const index of indexes) for (const index of indexes)
createSql += `${index.indexdef};\n`; createSql += `${index.indexdef};\n`;
@@ -157,11 +183,11 @@ SET row_security = off;\n\n\n`;
return createSql; return createSql;
} }
getDropTable (tableName) { getDropTable (tableName: string) {
return `DROP TABLE IF EXISTS "${this.schemaName}"."${tableName}";`; return `DROP TABLE IF EXISTS "${this.schemaName}"."${tableName}";`;
} }
async * getTableInsert (tableName) { async * getTableInsert (tableName: string) {
let rowCount = 0; let rowCount = 0;
const sqlStr = ''; const sqlStr = '';
@@ -205,14 +231,14 @@ SET row_security = off;\n\n\n`;
} }
else if (DATETIME.includes(column.type)) { else if (DATETIME.includes(column.type)) {
let datePrecision = ''; let datePrecision = '';
for (let i = 0; i < column.precision; i++) for (let i = 0; i < column.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S'; datePrecision += i === 0 ? '.S' : 'S';
sqlInsertString += moment(val).isValid() sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`)) ? this.escapeAndQuote(moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`))
: this.escapeAndQuote(val); : this.escapeAndQuote(val);
} }
else if (column.isArray) { else if ('isArray' in column) {
let parsedVal; let parsedVal;
if (Array.isArray(val)) if (Array.isArray(val))
parsedVal = JSON.stringify(val).replaceAll('[', '{').replaceAll(']', '}'); parsedVal = JSON.stringify(val).replaceAll('[', '{').replaceAll(']', '}');
@@ -254,7 +280,7 @@ SET row_security = off;\n\n\n`;
async getCreateTypes () { async getCreateTypes () {
let sqlString = ''; let sqlString = '';
const { rows: types } = await this._client.raw(` const { rows: types } = await this._client.raw<antares.QueryResult<{typname: string; enumlabel: string}>>(`
SELECT pg_type.typname, pg_enum.enumlabel SELECT pg_type.typname, pg_enum.enumlabel
FROM pg_type FROM pg_type
JOIN pg_enum ON pg_enum.enumtypid = pg_type.oid; JOIN pg_enum ON pg_enum.enumtypid = pg_type.oid;
@@ -360,6 +386,15 @@ SET row_security = off;\n\n\n`;
} }
async getTriggers () { async getTriggers () {
/* eslint-disable camelcase */
interface TriggersResult {
event_object_table: string;
table_name: string;
trigger_name: string;
events: string[];
event_manipulation: string;
}
/* eslint-enable camelcase */
let sqlString = ''; let sqlString = '';
// Trigger functions // Trigger functions
@@ -374,7 +409,7 @@ SET row_security = off;\n\n\n`;
sqlString += `\n${functionDef[0].definition};\n`; sqlString += `\n${functionDef[0].definition};\n`;
} }
const { rows: triggers } = await this._client.raw( const { rows: triggers } = await this._client.raw<antares.QueryResult<TriggersResult>>(
`SELECT * FROM "information_schema"."triggers" WHERE "trigger_schema"='${this.schemaName}'` `SELECT * FROM "information_schema"."triggers" WHERE "trigger_schema"='${this.schemaName}'`
); );
@@ -430,11 +465,12 @@ SET row_security = off;\n\n\n`;
return sqlString; return sqlString;
} }
async _queryStream (sql) { async _queryStream (sql: string) {
if (process.env.NODE_ENV === 'development') console.log('EXPORTER:', sql); if (process.env.NODE_ENV === 'development') console.log('EXPORTER:', sql);
const connection = await this._client.getConnection(); const connection = await this._client.getConnection();
const query = new QueryStream(sql, null); const query = new QueryStream(sql, null);
const stream = connection.query(query); // eslint-disable-next-line @typescript-eslint/no-explicit-any
const stream = (connection as any).query(query);
const dispose = () => connection.end(); const dispose = () => connection.end();
stream.on('end', dispose); stream.on('end', dispose);
@@ -443,17 +479,10 @@ SET row_security = off;\n\n\n`;
return stream; return stream;
} }
getEscapedDefiner (definer) { escapeAndQuote (val: string) {
return definer
.split('@')
.map(part => '`' + part + '`')
.join('@');
}
escapeAndQuote (val) {
// eslint-disable-next-line no-control-regex // eslint-disable-next-line no-control-regex
const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g; const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g;
const CHARS_ESCAPE_MAP = { const CHARS_ESCAPE_MAP: {[key: string]: string} = {
'\0': '\\0', '\0': '\\0',
'\b': '\\b', '\b': '\\b',
'\t': '\\t', '\t': '\\t',
@@ -481,15 +510,4 @@ SET row_security = off;\n\n\n`;
return `'${escapedVal}'`; return `'${escapedVal}'`;
} }
_getGeoJSON (val) {
if (Array.isArray(val)) {
if (getArrayDepth(val) === 1)
return lineString(val.reduce((acc, curr) => [...acc, [curr.x, curr.y]], []));
else
return polygon(val.map(arr => arr.reduce((acc, curr) => [...acc, [curr.x, curr.y]], [])));
}
else
return point([val.x, val.y]);
}
} }

View File

@@ -1,13 +1,12 @@
import moment from 'moment'; import * as moment from 'moment';
import { MySQLClient } from '../../clients/MySQLClient';
import { PostgreSQLClient } from '../../clients/PostgreSQLClient';
import { BaseExporter } from '../BaseExporter'; import { BaseExporter } from '../BaseExporter';
export class SqlExporter extends BaseExporter { export class SqlExporter extends BaseExporter {
constructor (client, tables, options) { protected _client: MySQLClient | PostgreSQLClient;
super(tables, options); protected _commentChar = '--'
this._client = client; protected _postTablesSql = ''
this._commentChar = '--';
this._postTablesSql = '';
}
get schemaName () { get schemaName () {
return this._options.schema; return this._options.schema;
@@ -24,7 +23,7 @@ export class SqlExporter extends BaseExporter {
async dump () { async dump () {
const { includes } = this._options; const { includes } = this._options;
const extraItems = Object.keys(includes).filter(key => includes[key]); const extraItems = Object.keys(includes).filter((key: 'functions' | 'views' | 'triggers' | 'routines' | 'schedulers') => includes[key]);
const totalTableToProcess = this._tables.filter( const totalTableToProcess = this._tables.filter(
t => t.includeStructure || t.includeContent || t.includeDropStatement t => t.includeStructure || t.includeContent || t.includeDropStatement
).length; ).length;
@@ -98,14 +97,15 @@ export class SqlExporter extends BaseExporter {
} }
for (const item of extraItems) { for (const item of extraItems) {
const processingMethod = `get${item.charAt(0).toUpperCase() + item.slice(1)}`; type exporterMethods = 'getViews' | 'getTriggers' | 'getSchedulers' | 'getFunctions' | 'getRoutines'
const processingMethod = `get${item.charAt(0).toUpperCase() + item.slice(1)}` as exporterMethods;
exportState.currentItemIndex++; exportState.currentItemIndex++;
exportState.currentItem = item; exportState.currentItem = item;
exportState.op = 'PROCESSING'; exportState.op = 'PROCESSING';
this.emitUpdate(exportState); this.emitUpdate(exportState);
if (this[processingMethod]) { if (this[processingMethod]) {
const data = await this[processingMethod](); const data = await this[processingMethod]() as unknown as string;
if (data !== '') { if (data !== '') {
const header = const header =
this.buildComment( this.buildComment(
@@ -123,7 +123,7 @@ export class SqlExporter extends BaseExporter {
this.writeString(footer); this.writeString(footer);
} }
buildComment (text) { buildComment (text: string) {
return text return text
.split('\n') .split('\n')
.map(txt => `${this._commentChar} ${txt}`) .map(txt => `${this._commentChar} ${txt}`)
@@ -151,19 +151,37 @@ Generation time: ${moment().format()}
return this.buildComment(`Dump completed on ${moment().format()}`); return this.buildComment(`Dump completed on ${moment().format()}`);
} }
getCreateTable (tableName) { /* eslint-disable @typescript-eslint/no-unused-vars */
throw new Error( getCreateTable (_tableName: string): Promise<string> {
'Sql Exporter must implement the "getCreateTable" method' throw new Error('Sql Exporter must implement the "getCreateTable" method');
);
} }
getDropTable (tableName) { getDropTable (_tableName: string): string {
throw new Error('Sql Exporter must implement the "getDropTable" method'); throw new Error('Sql Exporter must implement the "getDropTable" method');
} }
getTableInsert (tableName) { getTableInsert (_tableName: string): AsyncGenerator<string> {
throw new Error( throw new Error('Sql Exporter must implement the "getTableInsert" method');
'Sql Exporter must implement the "getTableInsert" method'
);
} }
getViews () {
throw new Error('Method "getViews" not implemented');
}
getTriggers () {
throw new Error('Method "getTriggers" not implemented');
}
getSchedulers () {
throw new Error('Method "getSchedulers" not implemented');
}
getFunctions () {
throw new Error('Method "getFunctions" not implemented');
}
getRoutines () {
throw new Error('Method "getRoutines" not implemented');
}
/* eslint-enable @typescript-eslint/no-unused-vars */
} }

View File

@@ -1,8 +1,14 @@
import fs from 'fs'; import * as importer from 'common/interfaces/importer';
import EventEmitter from 'events'; import * as fs from 'fs';
import * as EventEmitter from 'events';
export class BaseImporter extends EventEmitter { export class BaseImporter extends EventEmitter {
constructor (options) { protected _options;
protected _isCancelled;
protected _fileHandler;
protected _state;
constructor (options: importer.ImportOptions) {
super(); super();
this._options = options; this._options = options;
this._isCancelled = false; this._isCancelled = false;
@@ -43,7 +49,7 @@ export class BaseImporter extends EventEmitter {
this.emitUpdate({ op: 'cancelling' }); this.emitUpdate({ op: 'cancelling' });
} }
emitUpdate (state) { emitUpdate (state: importer.ImportState) {
this.emit('progress', { ...this._state, ...state }); this.emit('progress', { ...this._state, ...state });
} }

View File

@@ -1,14 +1,18 @@
import fs from 'fs/promises'; import * as mysql from 'mysql2';
import * as importer from 'common/interfaces/importer';
import * as fs from 'fs/promises';
import MySQLParser from '../../parsers/MySQLParser'; import MySQLParser from '../../parsers/MySQLParser';
import { BaseImporter } from '../BaseImporter'; import { BaseImporter } from '../BaseImporter';
export default class MySQLImporter extends BaseImporter { export default class MySQLImporter extends BaseImporter {
constructor (client, options) { protected _client: mysql.Pool
constructor (client: mysql.Pool, options: importer.ImportOptions) {
super(options); super(options);
this._client = client; this._client = client;
} }
async import () { async import (): Promise<void> {
try { try {
const { size: totalFileSize } = await fs.stat(this._options.file); const { size: totalFileSize } = await fs.stat(this._options.file);
const parser = new MySQLParser(); const parser = new MySQLParser();
@@ -60,7 +64,8 @@ export default class MySQLImporter extends BaseImporter {
parser.on('pause', () => { parser.on('pause', () => {
this._fileHandler.unpipe(parser); this._fileHandler.unpipe(parser);
this._fileHandler.readableFlowing = false; // eslint-disable-next-line @typescript-eslint/no-explicit-any
(this._fileHandler as any).readableFlowing = false;
}); });
this._fileHandler.on('data', (chunk) => { this._fileHandler.on('data', (chunk) => {

View File

@@ -1,14 +1,18 @@
import * as pg from 'pg';
import * as importer from 'common/interfaces/importer';
import fs from 'fs/promises'; import fs from 'fs/promises';
import PostgreSQLParser from '../../parsers/PostgreSQLParser'; import PostgreSQLParser from '../../parsers/PostgreSQLParser';
import { BaseImporter } from '../BaseImporter'; import { BaseImporter } from '../BaseImporter';
export default class PostgreSQLImporter extends BaseImporter { export default class PostgreSQLImporter extends BaseImporter {
constructor (client, options) { protected _client: pg.PoolClient;
constructor (client: pg.PoolClient, options: importer.ImportOptions) {
super(options); super(options);
this._client = client; this._client = client;
} }
async import () { async import (): Promise<void> {
try { try {
const { size: totalFileSize } = await fs.stat(this._options.file); const { size: totalFileSize } = await fs.stat(this._options.file);
const parser = new PostgreSQLParser(); const parser = new PostgreSQLParser();
@@ -60,7 +64,8 @@ export default class PostgreSQLImporter extends BaseImporter {
parser.on('pause', () => { parser.on('pause', () => {
this._fileHandler.unpipe(parser); this._fileHandler.unpipe(parser);
this._fileHandler.readableFlowing = false; // eslint-disable-next-line @typescript-eslint/no-explicit-any
(this._fileHandler as any).readableFlowing = false;
}); });
this._fileHandler.on('data', (chunk) => { this._fileHandler.on('data', (chunk) => {

View File

@@ -1,7 +1,17 @@
import { Transform } from 'stream'; import { Transform, TransformCallback, TransformOptions } from 'stream';
export default class MySQLParser extends Transform { export default class MySQLParser extends Transform {
constructor (opts) { private _buffer: string;
private _lastChar: string;
private _last9Chars: string;
encoding: BufferEncoding;
delimiter: string;
isEscape: boolean;
currentQuote: string;
isDelimiter: boolean;
constructor (opts?: TransformOptions & { delimiter: string }) {
opts = { opts = {
delimiter: ';', delimiter: ';',
encoding: 'utf8', encoding: 'utf8',
@@ -21,7 +31,7 @@ export default class MySQLParser extends Transform {
this.isDelimiter = false; this.isDelimiter = false;
} }
_transform (chunk, encoding, next) { _transform (chunk: Buffer, encoding: BufferEncoding, next: TransformCallback) {
for (const char of chunk.toString(this.encoding)) { for (const char of chunk.toString(this.encoding)) {
this.checkEscape(); this.checkEscape();
this._buffer += char; this._buffer += char;
@@ -49,7 +59,7 @@ export default class MySQLParser extends Transform {
} }
} }
checkNewDelimiter (char) { checkNewDelimiter (char: string) {
if (this.currentQuote === null && this._last9Chars === 'delimiter') { if (this.currentQuote === null && this._last9Chars === 'delimiter') {
this.isDelimiter = true; this.isDelimiter = true;
this._buffer = ''; this._buffer = '';
@@ -64,7 +74,7 @@ export default class MySQLParser extends Transform {
} }
} }
checkQuote (char) { checkQuote (char: string) {
const isQuote = !this.isEscape && (char === '\'' || char === '"'); const isQuote = !this.isEscape && (char === '\'' || char === '"');
if (isQuote && this.currentQuote === char) if (isQuote && this.currentQuote === char)
this.currentQuote = null; this.currentQuote = null;
@@ -77,7 +87,7 @@ export default class MySQLParser extends Transform {
if (this.isDelimiter) if (this.isDelimiter)
return false; return false;
let query = false; let query: false | string = false;
let demiliterFound = false; let demiliterFound = false;
if (this.currentQuote === null && this._buffer.length >= this.delimiter.length) if (this.currentQuote === null && this._buffer.length >= this.delimiter.length)
demiliterFound = this._last9Chars.slice(-this.delimiter.length) === this.delimiter; demiliterFound = this._last9Chars.slice(-this.delimiter.length) === this.delimiter;

View File

@@ -1,7 +1,23 @@
import { Transform } from 'stream'; import { Transform, TransformCallback, TransformOptions } from 'stream';
export default class PostgreSQLParser extends Transform { export default class PostgreSQLParser extends Transform {
constructor (opts) { private _buffer: string;
private _lastChar: string;
private _lastChars: string;
private _bodyWrapper: string;
private _bodyWrapperBuffer: string;
private _firstDollarFound: boolean;
private _isBody: boolean;
private _isSingleLineComment: boolean;
private _isMultiLineComment: boolean;
encoding: BufferEncoding;
delimiter: string;
isEscape: boolean;
currentQuote: string;
isDelimiter: boolean;
constructor (opts?: TransformOptions & { delimiter: string }) {
opts = { opts = {
delimiter: ';', delimiter: ';',
encoding: 'utf8', encoding: 'utf8',
@@ -30,7 +46,7 @@ export default class PostgreSQLParser extends Transform {
return this._isSingleLineComment || this._isMultiLineComment; return this._isSingleLineComment || this._isMultiLineComment;
} }
_transform (chunk, encoding, next) { _transform (chunk: Buffer, encoding: BufferEncoding, next: TransformCallback) {
for (const char of chunk.toString(this.encoding)) { for (const char of chunk.toString(this.encoding)) {
this.checkEscape(); this.checkEscape();
this._buffer += char; this._buffer += char;
@@ -82,7 +98,7 @@ export default class PostgreSQLParser extends Transform {
} }
} }
checkBodyWrapper (char) { checkBodyWrapper (char: string) {
if (this._isBody) if (this._isBody)
this._isBody = this._lastChars !== this._bodyWrapper; this._isBody = this._lastChars !== this._bodyWrapper;
@@ -111,7 +127,7 @@ export default class PostgreSQLParser extends Transform {
} }
} }
checkQuote (char) { checkQuote (char: string) {
const isQuote = !this.isEscape && (char === '\'' || char === '"'); const isQuote = !this.isEscape && (char === '\'' || char === '"');
if (isQuote && this.currentQuote === char) if (isQuote && this.currentQuote === char)
this.currentQuote = null; this.currentQuote = null;
@@ -124,7 +140,7 @@ export default class PostgreSQLParser extends Transform {
if (this._isBody || this._isComment) if (this._isBody || this._isComment)
return false; return false;
let query = false; let query: false | string = false;
let demiliterFound = false; let demiliterFound = false;
if (this.currentQuote === null && this._buffer.length >= this.delimiter.length) if (this.currentQuote === null && this._buffer.length >= this.delimiter.length)

View File

@@ -1,14 +1,11 @@
'use strict';
import { app, BrowserWindow, /* session, */ nativeImage, Menu } from 'electron'; import { app, BrowserWindow, /* session, */ nativeImage, Menu } from 'electron';
import * as path from 'path'; import * as path from 'path';
import Store from 'electron-store'; import * as Store from 'electron-store';
import * as windowStateKeeper from 'electron-window-state'; import * as windowStateKeeper from 'electron-window-state';
import * as remoteMain from '@electron/remote/main'; import * as remoteMain from '@electron/remote/main';
import ipcHandlers from './ipc-handlers'; import ipcHandlers from './ipc-handlers';
// remoteMain.initialize();
Store.initRenderer(); Store.initRenderer();
const isDevelopment = process.env.NODE_ENV !== 'production'; const isDevelopment = process.env.NODE_ENV !== 'production';
@@ -18,8 +15,8 @@ const gotTheLock = app.requestSingleInstanceLock();
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'; process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
// global reference to mainWindow (necessary to prevent window from being garbage collected) // global reference to mainWindow (necessary to prevent window from being garbage collected)
let mainWindow; let mainWindow: BrowserWindow;
let mainWindowState; let mainWindowState: windowStateKeeper.State;
async function createMainWindow () { async function createMainWindow () {
const icon = require('../renderer/images/logo-32.png'); const icon = require('../renderer/images/logo-32.png');
@@ -36,7 +33,6 @@ async function createMainWindow () {
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
contextIsolation: false, contextIsolation: false,
'web-security': false,
spellcheck: false spellcheck: false
}, },
frame: false, frame: false,
@@ -51,22 +47,8 @@ async function createMainWindow () {
remoteMain.enable(window.webContents); remoteMain.enable(window.webContents);
try { try {
if (isDevelopment) { if (isDevelopment)
const { default: installExtension, VUEJS3_DEVTOOLS } = require('electron-devtools-installer');
const options = {
loadExtensionOptions: { allowFileAccess: true }
};
try {
const name = await installExtension(VUEJS3_DEVTOOLS, options);
console.log(`Added Extension: ${name}`);
}
catch (err) {
console.log('An error occurred: ', err);
}
await window.loadURL('http://localhost:9080'); await window.loadURL('http://localhost:9080');
}
else { else {
const indexPath = path.resolve(__dirname, 'index.html'); const indexPath = path.resolve(__dirname, 'index.html');
await window.loadFile(indexPath); await window.loadFile(indexPath);
@@ -113,8 +95,8 @@ else {
mainWindow = await createMainWindow(); mainWindow = await createMainWindow();
createAppMenu(); createAppMenu();
// if (isDevelopment) if (isDevelopment)
// mainWindow.webContents.openDevTools(); mainWindow.webContents.openDevTools();
process.on('uncaughtException', error => { process.on('uncaughtException', error => {
mainWindow.webContents.send('unhandled-exception', error); mainWindow.webContents.send('unhandled-exception', error);
@@ -124,10 +106,18 @@ else {
mainWindow.webContents.send('unhandled-exception', error); mainWindow.webContents.send('unhandled-exception', error);
}); });
}); });
app.on('browser-window-created', (event, window) => {
if (isDevelopment) {
const { antares } = require('../../package.json');
const extensionPath = path.resolve(__dirname, `../../misc/${antares.devtoolsId}`);
window.webContents.session.loadExtension(extensionPath, { allowFileAccess: true }).catch(console.error);
}
});
} }
function createAppMenu () { function createAppMenu () {
let menu = null; let menu: Electron.Menu = null;
if (isMacOS) { if (isMacOS) {
menu = Menu.buildFromTemplate([ menu = Menu.buildFromTemplate([

View File

@@ -1,8 +1,11 @@
import fs from 'fs'; import * as antares from 'common/interfaces/antares';
import * as fs from 'fs';
import { MySQLClient } from '../libs/clients/MySQLClient';
import { PostgreSQLClient } from '../libs/clients/PostgreSQLClient';
import { ClientsFactory } from '../libs/ClientsFactory'; import { ClientsFactory } from '../libs/ClientsFactory';
import MysqlExporter from '../libs/exporters/sql/MysqlExporter.js'; import MysqlExporter from '../libs/exporters/sql/MysqlExporter';
import PostgreSQLExporter from '../libs/exporters/sql/PostgreSQLExporter'; import PostgreSQLExporter from '../libs/exporters/sql/PostgreSQLExporter';
let exporter; let exporter: antares.Exporter;
process.on('message', async ({ type, client, tables, options }) => { process.on('message', async ({ type, client, tables, options }) => {
if (type === 'init') { if (type === 'init') {
@@ -10,16 +13,16 @@ process.on('message', async ({ type, client, tables, options }) => {
client: client.name, client: client.name,
params: client.config, params: client.config,
poolSize: 5 poolSize: 5
}); }) as MySQLClient | PostgreSQLClient;
await connection.connect(); await connection.connect();
switch (client.name) { switch (client.name) {
case 'mysql': case 'mysql':
case 'maria': case 'maria':
exporter = new MysqlExporter(connection, tables, options); exporter = new MysqlExporter(connection as MySQLClient, tables, options);
break; break;
case 'pg': case 'pg':
exporter = new PostgreSQLExporter(connection, tables, options); exporter = new PostgreSQLExporter(connection as PostgreSQLClient, tables, options);
break; break;
default: default:
process.send({ process.send({
@@ -62,3 +65,5 @@ process.on('message', async ({ type, client, tables, options }) => {
else if (type === 'cancel') else if (type === 'cancel')
exporter.cancel(); exporter.cancel();
}); });
process.on('beforeExit', console.log);

View File

@@ -1,7 +1,12 @@
import * as antares from 'common/interfaces/antares';
import * as pg from 'pg';
import * as mysql from 'mysql2';
import { MySQLClient } from '../libs/clients/MySQLClient';
import { PostgreSQLClient } from '../libs/clients/PostgreSQLClient';
import { ClientsFactory } from '../libs/ClientsFactory'; import { ClientsFactory } from '../libs/ClientsFactory';
import MySQLImporter from '../libs/importers/sql/MysqlImporter'; import MySQLImporter from '../libs/importers/sql/MySQLlImporter';
import PostgreSQLImporter from '../libs/importers/sql/PostgreSQLImporter'; import PostgreSQLImporter from '../libs/importers/sql/PostgreSQLImporter';
let importer; let importer: antares.Importer;
process.on('message', async ({ type, dbConfig, options }) => { process.on('message', async ({ type, dbConfig, options }) => {
if (type === 'init') { if (type === 'init') {
@@ -12,17 +17,17 @@ process.on('message', async ({ type, dbConfig, options }) => {
schema: options.schema schema: options.schema
}, },
poolSize: 1 poolSize: 1
}); }) as MySQLClient | PostgreSQLClient;
const pool = await connection.getConnectionPool(); const pool = await connection.getConnectionPool();
switch (options.type) { switch (options.type) {
case 'mysql': case 'mysql':
case 'maria': case 'maria':
importer = new MySQLImporter(pool, options); importer = new MySQLImporter(pool as unknown as mysql.Pool, options);
break; break;
case 'pg': case 'pg':
importer = new PostgreSQLImporter(pool, options); importer = new PostgreSQLImporter(pool as unknown as pg.PoolClient, options);
break; break;
default: default:
process.send({ process.send({

View File

@@ -25,37 +25,56 @@
</template> </template>
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { defineAsyncComponent } from 'vue';
import { storeToRefs } from 'pinia';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { Menu, getCurrentWindow } from '@electron/remote'; import { Menu, getCurrentWindow } from '@electron/remote';
import { useApplicationStore } from '@/stores/application';
import { useConnectionsStore } from '@/stores/connections';
import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces';
import TheSettingBar from '@/components/TheSettingBar';
export default { export default {
name: 'App', name: 'App',
components: { components: {
TheTitleBar: () => import(/* webpackChunkName: "TheTitleBar" */'@/components/TheTitleBar'), TheTitleBar: defineAsyncComponent(() => import(/* webpackChunkName: "TheTitleBar" */'@/components/TheTitleBar')),
TheSettingBar: () => import(/* webpackChunkName: "TheSettingBar" */'@/components/TheSettingBar'), TheSettingBar,
TheFooter: () => import(/* webpackChunkName: "TheFooter" */'@/components/TheFooter'), TheFooter: defineAsyncComponent(() => import(/* webpackChunkName: "TheFooter" */'@/components/TheFooter')),
TheNotificationsBoard: () => import(/* webpackChunkName: "TheNotificationsBoard" */'@/components/TheNotificationsBoard'), TheNotificationsBoard: defineAsyncComponent(() => import(/* webpackChunkName: "TheNotificationsBoard" */'@/components/TheNotificationsBoard')),
Workspace: () => import(/* webpackChunkName: "Workspace" */'@/components/Workspace'), Workspace: defineAsyncComponent(() => import(/* webpackChunkName: "Workspace" */'@/components/Workspace')),
WorkspaceAddConnectionPanel: () => import(/* webpackChunkName: "WorkspaceAddConnectionPanel" */'@/components/WorkspaceAddConnectionPanel'), WorkspaceAddConnectionPanel: defineAsyncComponent(() => import(/* webpackChunkName: "WorkspaceAddConnectionPanel" */'@/components/WorkspaceAddConnectionPanel')),
ModalSettings: () => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings'), ModalSettings: defineAsyncComponent(() => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings')),
TheScratchpad: () => import(/* webpackChunkName: "TheScratchpad" */'@/components/TheScratchpad'), TheScratchpad: defineAsyncComponent(() => import(/* webpackChunkName: "TheScratchpad" */'@/components/TheScratchpad')),
BaseTextEditor: () => import(/* webpackChunkName: "BaseTextEditor" */'@/components/BaseTextEditor') BaseTextEditor: defineAsyncComponent(() => import(/* webpackChunkName: "BaseTextEditor" */'@/components/BaseTextEditor'))
}, },
data () { setup () {
return {}; const applicationStore = useApplicationStore();
}, const connectionsStore = useConnectionsStore();
computed: { const settingsStore = useSettingsStore();
...mapGetters({ const workspacesStore = useWorkspacesStore();
selectedWorkspace: 'workspaces/getSelected',
isLoading: 'application/isLoading', const {
isSettingModal: 'application/isSettingModal', isLoading,
isScratchpad: 'application/isScratchpad', isSettingModal,
connections: 'connections/getConnections', isScratchpad
applicationTheme: 'settings/getApplicationTheme', } = storeToRefs(applicationStore);
disableBlur: 'settings/getDisableBlur', const { connections } = storeToRefs(connectionsStore);
isUnsavedDiscardModal: 'workspaces/isUnsavedDiscardModal' const { applicationTheme, disableBlur } = storeToRefs(settingsStore);
}) const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { checkVersionUpdate } = applicationStore;
return {
isLoading,
isSettingModal,
isScratchpad,
checkVersionUpdate,
connections,
applicationTheme,
disableBlur,
selectedWorkspace
};
}, },
mounted () { mounted () {
ipcRenderer.send('check-for-updates'); ipcRenderer.send('check-for-updates');
@@ -97,12 +116,6 @@ export default {
node = node.parentNode; node = node.parentNode;
} }
}); });
},
methods: {
...mapActions({
showNewConnModal: 'application/showNewConnModal',
checkVersionUpdate: 'application/checkVersionUpdate'
})
} }
}; };
</script> </script>

View File

@@ -1,44 +1,48 @@
<template> <template>
<div class="modal active" :class="modalSizeClass"> <div class="dummy-wrapper">
<a class="modal-overlay" @click="hideModal" /> <Teleport to="#window-content">
<div class="modal-container"> <div class="modal active" :class="modalSizeClass">
<div v-if="hasHeader" class="modal-header pl-2"> <a class="modal-overlay" @click="hideModal" />
<div class="modal-title h6"> <div class="modal-container">
<slot name="header" /> <div v-if="hasHeader" class="modal-header pl-2">
</div> <div class="modal-title h6">
<a class="btn btn-clear float-right" @click="hideModal" /> <slot name="header" />
</div> </div>
<div v-if="hasDefault" class="modal-header"> <a class="btn btn-clear float-right" @click="hideModal" />
<div class="modal-title h6"> </div>
<slot /> <div v-if="hasDefault" class="modal-header">
</div> <div class="modal-title h6">
<a class="btn btn-clear float-right" @click="hideModal" /> <slot />
</div> </div>
<div v-if="hasBody" class="modal-body"> <a class="btn btn-clear float-right" @click="hideModal" />
<a </div>
v-if="!hasHeader && !hasDefault" <div v-if="hasBody" class="modal-body pb-0">
class="btn btn-clear float-right" <a
@click="hideModal" v-if="!hasHeader && !hasDefault"
/> class="btn btn-clear float-right"
<div class="content"> @click="hideModal"
<slot name="body" /> />
<div class="content">
<slot name="body" />
</div>
</div>
<div v-if="!hideFooter" class="modal-footer">
<button
class="btn btn-primary mr-2"
@click.stop="confirmModal"
>
{{ confirmText || $t('word.confirm') }}
</button>
<button
class="btn btn-link"
@click="hideModal"
>
{{ cancelText || $t('word.cancel') }}
</button>
</div>
</div> </div>
</div> </div>
<div v-if="!hideFooter" class="modal-footer"> </Teleport>
<button
class="btn btn-primary mr-2"
@click.stop="confirmModal"
>
{{ confirmText || $t('word.confirm') }}
</button>
<button
class="btn btn-link"
@click="hideModal"
>
{{ cancelText || $t('word.cancel') }}
</button>
</div>
</div>
</div> </div>
</template> </template>
@@ -58,6 +62,7 @@ export default {
confirmText: String, confirmText: String,
cancelText: String cancelText: String
}, },
emits: ['confirm', 'hide'],
computed: { computed: {
hasHeader () { hasHeader () {
return !!this.$slots.header; return !!this.$slots.header;
@@ -81,7 +86,7 @@ export default {
created () { created () {
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
}, },
beforeDestroy () { beforeUnmount () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
}, },
methods: { methods: {

View File

@@ -21,6 +21,7 @@ export default {
props: { props: {
contextEvent: MouseEvent contextEvent: MouseEvent
}, },
emits: ['close-context'],
data () { data () {
return { return {
contextSize: null, contextSize: null,
@@ -61,7 +62,7 @@ export default {
if (this.$refs.contextContent) if (this.$refs.contextContent)
this.contextSize = this.$refs.contextContent.getBoundingClientRect(); this.contextSize = this.$refs.contextContent.getBoundingClientRect();
}, },
beforeDestroy () { beforeUnmount () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
}, },
methods: { methods: {

View File

@@ -27,6 +27,7 @@ export default {
default: '' default: ''
} }
}, },
emits: ['close'],
data () { data () {
return { return {
isExpanded: false isExpanded: false

View File

@@ -11,13 +11,15 @@
<script> <script>
import * as ace from 'ace-builds'; import * as ace from 'ace-builds';
import { storeToRefs } from 'pinia';
import 'ace-builds/webpack-resolver'; import 'ace-builds/webpack-resolver';
import { mapGetters } from 'vuex'; import { useSettingsStore } from '@/stores/settings';
import { uidGen } from 'common/libs/uidGen';
export default { export default {
name: 'BaseTextEditor', name: 'BaseTextEditor',
props: { props: {
value: String, modelValue: String,
mode: { type: String, default: 'text' }, mode: { type: String, default: 'text' },
editorClass: { type: String, default: '' }, editorClass: { type: String, default: '' },
autoFocus: { type: Boolean, default: false }, autoFocus: { type: Boolean, default: false },
@@ -25,20 +27,30 @@ export default {
showLineNumbers: { type: Boolean, default: true }, showLineNumbers: { type: Boolean, default: true },
height: { type: Number, default: 200 } height: { type: Number, default: 200 }
}, },
emits: ['update:modelValue'],
setup () {
const settingsStore = useSettingsStore();
const {
editorTheme,
editorFontSize,
autoComplete,
lineWrap
} = storeToRefs(settingsStore);
return {
editorTheme,
editorFontSize,
autoComplete,
lineWrap
};
},
data () { data () {
return { return {
editor: null, editor: null,
id: null id: uidGen()
}; };
}, },
computed: {
...mapGetters({
editorTheme: 'settings/getEditorTheme',
editorFontSize: 'settings/getEditorFontSize',
autoComplete: 'settings/getAutoComplete',
lineWrap: 'settings/getLineWrap'
})
},
watch: { watch: {
mode () { mode () {
if (this.editor) if (this.editor)
@@ -76,14 +88,11 @@ export default {
} }
} }
}, },
created () {
this.id = this._uid;
},
mounted () { mounted () {
this.editor = ace.edit(`editor-${this.id}`, { this.editor = ace.edit(`editor-${this.id}`, {
mode: `ace/mode/${this.mode}`, mode: `ace/mode/${this.mode}`,
theme: `ace/theme/${this.editorTheme}`, theme: `ace/theme/${this.editorTheme}`,
value: this.value || '', value: this.modelValue || '',
fontSize: '14px', fontSize: '14px',
printMargin: false, printMargin: false,
readOnly: this.readOnly, readOnly: this.readOnly,
@@ -100,7 +109,7 @@ export default {
this.editor.session.on('change', () => { this.editor.session.on('change', () => {
const content = this.editor.getValue(); const content = this.editor.getValue();
this.$emit('update:value', content); this.$emit('update:modelValue', content);
}); });
if (this.autoFocus) { if (this.autoFocus) {

View File

@@ -22,6 +22,7 @@ export default {
default: '' default: ''
} }
}, },
emits: ['close'],
data () { data () {
return { return {
isVisible: false isVisible: false

View File

@@ -4,10 +4,10 @@
<i class="mdi mdi-folder-open mr-1" />{{ message }} <i class="mdi mdi-folder-open mr-1" />{{ message }}
</span> </span>
<span class="text-ellipsis file-uploader-value"> <span class="text-ellipsis file-uploader-value">
{{ value | lastPart }} {{ lastPart(modelValue) }}
</span> </span>
<i <i
v-if="value.length" v-if="modelValue.length"
class="file-uploader-reset mdi mdi-close" class="file-uploader-reset mdi mdi-close"
@click.prevent="clear" @click.prevent="clear"
/> />
@@ -23,9 +23,30 @@
</template> </template>
<script> <script>
import { uidGen } from 'common/libs/uidGen';
export default { export default {
name: 'BaseUploadInput', name: 'BaseUploadInput',
filters: { props: {
message: {
default: 'Browse',
type: String
},
modelValue: {
default: '',
type: String
}
},
emits: ['change', 'clear'],
data () {
return {
id: uidGen()
};
},
methods: {
clear () {
this.$emit('clear');
},
lastPart (string) { lastPart (string) {
if (!string) return ''; if (!string) return '';
@@ -34,29 +55,6 @@ export default {
string = `...${string.slice(-19)}`; string = `...${string.slice(-19)}`;
return string; return string;
} }
},
props: {
message: {
default: 'Browse',
type: String
},
value: {
default: '',
type: String
}
},
data () {
return {
id: null
};
},
mounted () {
this.id = this._uid;
},
methods: {
clear () {
this.$emit('clear');
}
} }
}; };
</script> </script>

View File

@@ -49,7 +49,7 @@ export default {
mounted () { mounted () {
this.setScrollElement(); this.setScrollElement();
}, },
beforeDestroy () { beforeUnmount () {
this.localScrollElement.removeEventListener('scroll', this.checkScrollPosition); this.localScrollElement.removeEventListener('scroll', this.checkScrollPosition);
}, },
methods: { methods: {

View File

@@ -36,8 +36,8 @@
<ForeignKeySelect <ForeignKeySelect
v-else-if="foreignKeys.includes(field.name)" v-else-if="foreignKeys.includes(field.name)"
ref="formInput" ref="formInput"
v-model="selectedValue"
class="form-select" class="form-select"
:value.sync="selectedValue"
:key-usage="getKeyUsage(field.name)" :key-usage="getKeyUsage(field.name)"
:disabled="!isChecked" :disabled="!isChecked"
/> />
@@ -52,7 +52,7 @@
> >
<BaseUploadInput <BaseUploadInput
v-else-if="inputProps().type === 'file'" v-else-if="inputProps().type === 'file'"
:value="selectedValue" :model-value="selectedValue"
:message="$t('word.browse')" :message="$t('word.browse')"
@clear="clearValue" @clear="clearValue"
@change="filesChange($event)" @change="filesChange($event)"
@@ -105,7 +105,6 @@
</template> </template>
<script> <script>
import { VueMaskDirective } from 'v-mask';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import BaseUploadInput from '@/components/BaseUploadInput'; import BaseUploadInput from '@/components/BaseUploadInput';
import ForeignKeySelect from '@/components/ForeignKeySelect'; import ForeignKeySelect from '@/components/ForeignKeySelect';
@@ -117,9 +116,6 @@ export default {
ForeignKeySelect, ForeignKeySelect,
BaseUploadInput BaseUploadInput
}, },
directives: {
mask: VueMaskDirective
},
props: { props: {
type: String, type: String,
field: Object, field: Object,
@@ -129,6 +125,7 @@ export default {
fieldLength: Number, fieldLength: Number,
fieldObj: Object fieldObj: Object
}, },
emits: ['update:modelValue'],
data () { data () {
return { return {
localType: null, localType: null,
@@ -244,7 +241,7 @@ export default {
this.selectedValue = ''; this.selectedValue = '';
}, },
onChange () { onChange () {
this.$emit('update:value', { this.$emit('update:modelValue', {
group: this.selectedGroup, group: this.selectedGroup,
method: this.selectedMethod, method: this.selectedMethod,
params: this.methodParams, params: this.methodParams,

View File

@@ -6,53 +6,55 @@
@change="onChange" @change="onChange"
@blur="$emit('blur')" @blur="$emit('blur')"
> >
<option v-if="!isValidDefault" :value="value"> <option v-if="!isValidDefault" :value="modelValue">
{{ value === null ? 'NULL' : value }} {{ modelValue === null ? 'NULL' : modelValue }}
</option> </option>
<option <option
v-for="row in foreignList" v-for="row in foreignList"
:key="row.foreign_column" :key="row.foreign_column"
:value="row.foreign_column" :value="row.foreign_column"
:selected="row.foreign_column === value" :selected="row.foreign_column === modelValue"
> >
{{ row.foreign_column }} {{ 'foreign_description' in row ? ` - ${row.foreign_description}` : '' | cutText }} {{ row.foreign_column }} {{ cutText('foreign_description' in row ? ` - ${row.foreign_description}` : '') }}
</option> </option>
</select> </select>
</template> </template>
<script> <script>
import { storeToRefs } from 'pinia';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import { mapGetters, mapActions } from 'vuex'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import { TEXT, LONG_TEXT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT } from 'common/fieldTypes';
export default { export default {
name: 'ForeignKeySelect', name: 'ForeignKeySelect',
filters: {
cutText (val) {
if (typeof val !== 'string') return val;
return val.length > 15 ? `${val.substring(0, 15)}...` : val;
}
},
props: { props: {
value: [String, Number], modelValue: [String, Number],
keyUsage: Object, keyUsage: Object,
size: { size: {
type: String, type: String,
default: '' default: ''
} }
}, },
emits: ['update:modelValue', 'blur'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
return { addNotification, selectedWorkspace };
},
data () { data () {
return { return {
foreignList: [] foreignList: []
}; };
}, },
computed: { computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected'
}),
isValidDefault () { isValidDefault () {
if (!this.foreignList.length) return true; if (!this.foreignList.length) return true;
if (this.value === null) return false; if (this.modelValue === null) return false;
return this.foreignList.some(foreign => foreign.foreign_column.toString() === this.value.toString()); return this.foreignList.some(foreign => foreign.foreign_column.toString() === this.modelValue.toString());
} }
}, },
async created () { async created () {
@@ -93,11 +95,12 @@ export default {
} }
}, },
methods: { methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
onChange () { onChange () {
this.$emit('update:value', this.$refs.editField.value); this.$emit('update:modelValue', this.$refs.editField.value);
},
cutText (val) {
if (typeof val !== 'string') return val;
return val.length > 15 ? `${val.substring(0, 15)}...` : val;
} }
} }
}; };

View File

@@ -1,61 +1,64 @@
<template> <template>
<div class="modal active modal-sm"> <Teleport to="#window-content">
<a class="modal-overlay" /> <div class="modal active modal-sm">
<div class="modal-container p-0"> <a class="modal-overlay" />
<div class="modal-header pl-2"> <div class="modal-container p-0">
<div class="modal-title h6"> <div class="modal-header pl-2">
<div class="d-flex"> <div class="modal-title h6">
<i class="mdi mdi-24px mdi-key-variant mr-1" /> {{ $t('word.credentials') }} <div class="d-flex">
<i class="mdi mdi-24px mdi-key-variant mr-1" /> {{ $t('word.credentials') }}
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal">
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.user') }}</label>
</div>
<div class="col-9">
<input
ref="firstInput"
v-model="credentials.user"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.password') }}</label>
</div>
<div class="col-9">
<input
v-model="credentials.password"
class="form-input"
type="password"
>
</div>
</div>
</form>
</div> </div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <div class="modal-footer">
</div> <button class="btn btn-primary mr-2" @click.stop="sendCredentials">
<div class="modal-body pb-0"> {{ $t('word.send') }}
<div class="content"> </button>
<form class="form-horizontal"> <button class="btn btn-link" @click.stop="closeModal">
<div class="form-group"> {{ $t('word.close') }}
<div class="col-3"> </button>
<label class="form-label">{{ $t('word.user') }}</label>
</div>
<div class="col-9">
<input
ref="firstInput"
v-model="credentials.user"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.password') }}</label>
</div>
<div class="col-9">
<input
v-model="credentials.password"
class="form-input"
type="password"
>
</div>
</div>
</form>
</div> </div>
</div> </div>
<div class="modal-footer">
<button class="btn btn-primary mr-2" @click.stop="sendCredentials">
{{ $t('word.send') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
</button>
</div>
</div> </div>
</div> </Teleport>
</template> </template>
<script> <script>
export default { export default {
name: 'ModalAskCredentials', name: 'ModalAskCredentials',
emits: ['close-asking', 'credentials'],
data () { data () {
return { return {
credentials: { credentials: {

View File

@@ -36,7 +36,7 @@
class="input-group-addon field-type cut-text" class="input-group-addon field-type cut-text"
:class="typeClass(parameter.type)" :class="typeClass(parameter.type)"
> >
{{ parameter.type }} {{ parameter.length | wrapNumber }} {{ parameter.type }} {{ wrapNumber(parameter.length) }}
</span> </span>
</div> </div>
</div> </div>
@@ -56,16 +56,11 @@ export default {
components: { components: {
ConfirmModal ConfirmModal
}, },
filters: {
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
},
props: { props: {
localRoutine: Object, localRoutine: Object,
client: String client: String
}, },
emits: ['confirm', 'close'],
data () { data () {
return { return {
values: {} values: {}
@@ -83,7 +78,7 @@ export default {
this.$refs.firstInput[0].focus(); this.$refs.firstInput[0].focus();
}, 20); }, 20);
}, },
beforeDestroy () { beforeUnmount () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
}, },
methods: { methods: {
@@ -122,6 +117,10 @@ export default {
e.stopPropagation(); e.stopPropagation();
if (e.key === 'Escape') if (e.key === 'Escape')
this.closeModal(); this.closeModal();
},
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
} }
} }
}; };

View File

@@ -26,10 +26,11 @@ export default {
components: { components: {
ConfirmModal ConfirmModal
}, },
emits: ['confirm', 'close'],
created () { created () {
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
}, },
beforeDestroy () { beforeUnmount () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
}, },
methods: { methods: {

View File

@@ -1,72 +1,76 @@
<template> <template>
<div class="modal active"> <Teleport to="#window-content">
<a class="modal-overlay" @click.stop="closeModal" /> <div class="modal active">
<div class="modal-container p-0"> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-header pl-2"> <div class="modal-container p-0">
<div class="modal-title h6"> <div class="modal-header pl-2">
<div class="d-flex"> <div class="modal-title h6">
<i class="mdi mdi-24px mdi-database-edit mr-1" /> <div class="d-flex">
<span class="cut-text">{{ $t('message.editSchema') }}</span> <i class="mdi mdi-24px mdi-database-edit mr-1" />
<span class="cut-text">{{ $t('message.editSchema') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal">
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.name') }}</label>
</div>
<div class="col-9">
<input
v-model="database.name"
class="form-input"
type="text"
required
:placeholder="$t('message.schemaName')"
readonly
>
</div>
</div>
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.collation') }}</label>
</div>
<div class="col-9">
<select
ref="firstInput"
v-model="database.collation"
class="form-select"
>
<option
v-for="collation in collations"
:key="collation.id"
:value="collation.collation"
>
{{ collation.collation }}
</option>
</select>
<small>{{ $t('message.serverDefault') }}: {{ defaultCollation }}</small>
</div>
</div>
</form>
</div> </div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <div class="modal-footer">
</div> <button class="btn btn-primary mr-2" @click.stop="updateSchema">
<div class="modal-body pb-0"> {{ $t('word.update') }}
<div class="content"> </button>
<form class="form-horizontal"> <button class="btn btn-link" @click.stop="closeModal">
<div class="form-group"> {{ $t('word.close') }}
<div class="col-3"> </button>
<label class="form-label">{{ $t('word.name') }}</label>
</div>
<div class="col-9">
<input
v-model="database.name"
class="form-input"
type="text"
required
:placeholder="$t('message.schemaName')"
readonly
>
</div>
</div>
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.collation') }}</label>
</div>
<div class="col-9">
<select
ref="firstInput"
v-model="database.collation"
class="form-select"
>
<option
v-for="collation in collations"
:key="collation.id"
:value="collation.collation"
>
{{ collation.collation }}
</option>
</select>
<small>{{ $t('message.serverDefault') }}: {{ defaultCollation }}</small>
</div>
</div>
</form>
</div> </div>
</div> </div>
<div class="modal-footer">
<button class="btn btn-primary mr-2" @click.stop="updateSchema">
{{ $t('word.update') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
</button>
</div>
</div> </div>
</div> </Teleport>
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
export default { export default {
@@ -74,6 +78,22 @@ export default {
props: { props: {
selectedSchema: String selectedSchema: String
}, },
emits: ['close'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, getDatabaseVariable } = workspacesStore;
return {
addNotification,
selectedWorkspace,
getWorkspace,
getDatabaseVariable
};
},
data () { data () {
return { return {
database: { database: {
@@ -84,11 +104,6 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
getDatabaseVariable: 'workspaces/getDatabaseVariable'
}),
collations () { collations () {
return this.getWorkspace(this.selectedWorkspace).collations; return this.getWorkspace(this.selectedWorkspace).collations;
}, },
@@ -124,13 +139,10 @@ export default {
this.$refs.firstInput.focus(); this.$refs.firstInput.focus();
}, 20); }, 20);
}, },
beforeDestroy () { beforeUnmount () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
}, },
methods: { methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
async updateSchema () { async updateSchema () {
if (this.database.collation !== this.database.prevCollation) { if (this.database.collation !== this.database.prevCollation) {
try { try {

View File

@@ -1,287 +1,310 @@
<template> <template>
<div class="modal active"> <Teleport to="#window-content">
<a class="modal-overlay" @click.stop="closeModal" /> <div class="modal active">
<div class="modal-container p-0"> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-header pl-2"> <div class="modal-container p-0">
<div class="modal-title h6"> <div class="modal-header pl-2">
<div class="d-flex"> <div class="modal-title h6">
<i class="mdi mdi-24px mdi-database-arrow-down mr-1" /> <div class="d-flex">
<span class="cut-text">{{ $t('message.exportSchema') }}</span> <i class="mdi mdi-24px mdi-database-arrow-down mr-1" />
</div> <span class="cut-text">{{ $t('message.exportSchema') }}</span>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body pb-0">
<div class="container">
<div class="columns">
<div class="col-3">
<label class="form-label">{{ $t('message.directoryPath') }}</label>
</div>
<div class="col-9">
<fieldset class="input-group">
<input
v-model="basePath"
class="form-input"
type="text"
required
readonly
:placeholder="$t('message.schemaName')"
>
<button
type="button"
class="btn btn-primary input-group-btn"
@click.prevent="openPathDialog"
>
{{ $t('word.change') }}
</button>
</fieldset>
</div> </div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div> </div>
<div class="modal-body pb-0">
<div class="columns export-options"> <div class="container">
<div class="column col-8 left"> <div class="columns">
<div class="columns mb-2"> <div class="col-3">
<div class="column col-auto d-flex text-italic "> <label class="form-label">{{ $t('message.directoryPath') }}</label>
<i class="mdi mdi-file-document-outline mr-2" />
{{ filename }}
</div> </div>
<div class="col-9">
<div class="column col-auto col-ml-auto "> <fieldset class="input-group">
<button <input
class="btn btn-dark btn-sm" v-model="basePath"
:title="$t('word.refresh')" class="form-input"
@click="refresh" type="text"
> required
<i class="mdi mdi-database-refresh" /> readonly
</button> :placeholder="$t('message.schemaName')"
<button
class="btn btn-dark btn-sm"
:title="$t('message.uncheckAllTables')"
:disabled="isRefreshing"
@click="uncheckAllTables"
>
<i class="mdi mdi-file-tree-outline" />
</button>
<button
class="btn btn-dark btn-sm"
:title="$t('message.checkAllTables')"
:disabled="isRefreshing"
@click="checkAllTables"
>
<i class="mdi mdi-file-tree" />
</button>
</div>
</div>
<div class="workspace-query-results">
<div ref="table" class="table table-hover">
<div class="thead">
<div class="tr text-center">
<div class="th no-border" style="width: 50%;" />
<div class="th no-border">
<label
class="form-checkbox m-0 px-2 form-inline"
@click.prevent="toggleAllTablesOption('includeStructure')"
>
<input
type="checkbox"
:indeterminate.prop="includeStructureStatus === 2"
:checked.prop="!!includeStructureStatus"
>
<i class="form-icon" />
</label>
</div>
<div class="th no-border">
<label
class="form-checkbox m-0 px-2 form-inline"
@click.prevent="toggleAllTablesOption('includeContent')"
>
<input
type="checkbox"
:indeterminate.prop="includeContentStatus === 2"
:checked.prop="!!includeContentStatus"
>
<i class="form-icon" />
</label>
</div>
<div class="th no-border">
<label
class="form-checkbox m-0 px-2 form-inline"
@click.prevent="toggleAllTablesOption('includeDropStatement')"
>
<input
type="checkbox"
:indeterminate.prop="includeDropStatementStatus === 2"
:checked.prop="!!includeDropStatementStatus"
>
<i class="form-icon" />
</label>
</div>
</div>
<div class="tr">
<div class="th" style="width: 50%;">
<div class="table-column-title">
<span>{{ $t('word.table') }}</span>
</div>
</div>
<div class="th text-center">
<div class="table-column-title">
<span>{{ $t('word.structure') }}</span>
</div>
</div>
<div class="th text-center">
<div class="table-column-title">
<span>{{ $t('word.content') }}</span>
</div>
</div>
<div class="th text-center">
<div class="table-column-title">
<span>{{ $t('word.drop') }}</span>
</div>
</div>
</div>
</div>
<div class="tbody">
<div
v-for="item in tables"
:key="item.name"
class="tr"
> >
<div class="td"> <button
{{ item.table }} type="button"
class="btn btn-primary input-group-btn"
@click.prevent="openPathDialog"
>
{{ $t('word.change') }}
</button>
</fieldset>
</div>
</div>
</div>
<div class="columns export-options">
<div class="column col-8 left">
<div class="columns mb-2">
<div class="column col-auto d-flex text-italic ">
<i class="mdi mdi-file-document-outline mr-2" />
{{ filename }}
</div>
<div class="column col-auto col-ml-auto ">
<button
class="btn btn-dark btn-sm"
:title="$t('word.refresh')"
@click="refresh"
>
<i class="mdi mdi-database-refresh" />
</button>
<button
class="btn btn-dark btn-sm mx-1"
:title="$t('message.uncheckAllTables')"
:disabled="isRefreshing"
@click="uncheckAllTables"
>
<i class="mdi mdi-file-tree-outline" />
</button>
<button
class="btn btn-dark btn-sm"
:title="$t('message.checkAllTables')"
:disabled="isRefreshing"
@click="checkAllTables"
>
<i class="mdi mdi-file-tree" />
</button>
</div>
</div>
<div class="workspace-query-results">
<div ref="table" class="table table-hover">
<div class="thead">
<div class="tr text-center">
<div class="th no-border" style="width: 50%;" />
<div class="th no-border">
<label
class="form-checkbox m-0 px-2 form-inline"
@click.prevent="toggleAllTablesOption('includeStructure')"
>
<input
type="checkbox"
:indeterminate="includeStructureStatus === 2"
:checked="!!includeStructureStatus"
>
<i class="form-icon" />
</label>
</div>
<div class="th no-border">
<label
class="form-checkbox m-0 px-2 form-inline"
@click.prevent="toggleAllTablesOption('includeContent')"
>
<input
type="checkbox"
:indeterminate="includeContentStatus === 2"
:checked="!!includeContentStatus"
>
<i class="form-icon" />
</label>
</div>
<div class="th no-border">
<label
class="form-checkbox m-0 px-2 form-inline"
@click.prevent="toggleAllTablesOption('includeDropStatement')"
>
<input
type="checkbox"
:indeterminate="includeDropStatementStatus === 2"
:checked="!!includeDropStatementStatus"
>
<i class="form-icon" />
</label>
</div>
</div> </div>
<div class="td text-center"> <div class="tr">
<label class="form-checkbox m-0 px-2 form-inline"> <div class="th" style="width: 50%;">
<input <div class="table-column-title">
v-model="item.includeStructure" <span>{{ $t('word.table') }}</span>
type="checkbox" </div>
><i class="form-icon" /> </div>
</label> <div class="th text-center">
<div class="table-column-title">
<span>{{ $t('word.structure') }}</span>
</div>
</div>
<div class="th text-center">
<div class="table-column-title">
<span>{{ $t('word.content') }}</span>
</div>
</div>
<div class="th text-center">
<div class="table-column-title">
<span>{{ $t('word.drop') }}</span>
</div>
</div>
</div> </div>
<div class="td text-center"> </div>
<label class="form-checkbox m-0 px-2 form-inline">
<input <div class="tbody">
v-model="item.includeContent" <div
type="checkbox" v-for="item in tables"
><i class="form-icon" /> :key="item.name"
</label> class="tr"
</div> >
<div class="td text-center"> <div class="td">
<label class="form-checkbox m-0 px-2 form-inline"> {{ item.table }}
<input </div>
v-model="item.includeDropStatement" <div class="td text-center">
type="checkbox" <label class="form-checkbox m-0 px-2 form-inline">
><i class="form-icon" /> <input
</label> v-model="item.includeStructure"
type="checkbox"
><i class="form-icon" />
</label>
</div>
<div class="td text-center">
<label class="form-checkbox m-0 px-2 form-inline">
<input
v-model="item.includeContent"
type="checkbox"
><i class="form-icon" />
</label>
</div>
<div class="td text-center">
<label class="form-checkbox m-0 px-2 form-inline">
<input
v-model="item.includeDropStatement"
type="checkbox"
><i class="form-icon" />
</label>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="column col-4">
<div class="column col-4"> <h5 class="h5">
<h5 class="h5"> {{ $t('word.options') }}
{{ $t('word.options') }} </h5>
</h5> <span class="h6">{{ $t('word.includes') }}:</span>
<span class="h6">{{ $t('word.includes') }}:</span> <label
<label v-for="(_, key) in options.includes"
v-for="(_, key) in options.includes" :key="key"
:key="key" class="form-checkbox"
class="form-checkbox" >
> <input v-model="options.includes[key]" type="checkbox"><i class="form-icon" /> {{ $tc(`word.${key}`, 2) }}
<input v-model="options.includes[key]" type="checkbox"><i class="form-icon" /> {{ $tc(`word.${key}`, 2) }} </label>
</label> <div v-if="customizations.exportByChunks">
<div v-if="customizations.exportByChunks"> <div class="h6 mt-4 mb-2">
<div class="h6 mt-4 mb-2"> {{ $t('message.newInserStmtEvery') }}:
{{ $t('message.newInserStmtEvery') }}: </div>
<div class="columns">
<div class="column col-6">
<input
v-model.number="options.sqlInsertAfter"
type="number"
class="form-input"
>
</div>
<div class="column col-6">
<select v-model="options.sqlInsertDivider" class="form-select">
<option value="bytes">
KiB
</option>
<option value="rows">
{{ $tc('word.row', 2) }}
</option>
</select>
</div>
</div>
</div>
<div class="h6 mb-2 mt-4">
{{ $t('message.ourputFormat') }}:
</div> </div>
<div class="columns"> <div class="columns">
<div class="column col-6"> <div class="column h5 mb-4">
<input <select v-model="options.outputFormat" class="form-select">
v-model.number="options.sqlInsertAfter" <option value="sql">
type="number" {{ $t('message.singleFile', {ext: '.sql'}) }}
class="form-input"
value="250"
>
</div>
<div class="column col-6">
<select v-model="options.sqlInsertDivider" class="form-select">
<option value="bytes">
KiB
</option> </option>
<option value="rows"> <option value="sql.zip">
{{ $tc('word.row', 2) }} {{ $t('message.zipCompressedFile', {ext: '.sql'}) }}
</option> </option>
</select> </select>
</div> </div>
</div> </div>
</div> </div>
<div class="h6 mb-2 mt-4">
{{ $t('message.ourputFormat') }}:
</div>
<div class="columns">
<div class="column h5 mb-4">
<select v-model="options.outputFormat" class="form-select">
<option value="sql">
{{ $t('message.singleFile', {ext: '.sql'}) }}
</option>
<option value="sql.zip">
{{ $t('message.zipCompressedFile', {ext: '.sql'}) }}
</option>
</select>
</div>
</div>
</div> </div>
</div> </div>
</div> <div class="modal-footer columns">
<div class="modal-footer columns"> <div class="column col modal-progress-wrapper text-left">
<div class="column col modal-progress-wrapper text-left"> <div v-if="progressPercentage > 0" class="export-progress">
<div v-if="progressPercentage > 0" class="export-progress"> <span class="progress-status">
<span class="progress-status"> {{ progressPercentage }}% - {{ progressStatus }}
{{ progressPercentage }}% - {{ progressStatus }} </span>
</span> <progress
<progress class="progress d-block"
class="progress d-block" :value="progressPercentage"
:value="progressPercentage" max="100"
max="100" />
/> </div>
</div>
<div class="column col-auto px-0">
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
</button>
<button
class="btn btn-primary mr-2"
:class="{'loading': isExporting}"
:disabled="isExporting || isRefreshing"
autofocus
@click.prevent="startExport"
>
{{ $t('word.export') }}
</button>
</div> </div>
</div>
<div class="column col-auto px-0">
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
</button>
<button
class="btn btn-primary mr-2"
:class="{'loading': isExporting}"
:disabled="isExporting || isRefreshing"
autofocus
@click.prevent="startExport"
>
{{ $t('word.export') }}
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </Teleport>
</template> </template>
<script> <script>
import { ipcRenderer } from 'electron';
import { mapActions, mapGetters } from 'vuex';
import moment from 'moment'; import moment from 'moment';
import { ipcRenderer } from 'electron';
import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import customizations from 'common/customizations'; import customizations from 'common/customizations';
import Application from '@/ipc-api/Application'; import Application from '@/ipc-api/Application';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
export default { export default {
name: 'ModalExportSchema', name: 'ModalExportSchema',
props: { props: {
selectedSchema: String selectedSchema: String
}, },
emits: ['close'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const {
getWorkspace,
getDatabaseVariable,
refreshSchema
} = workspacesStore;
return {
addNotification,
selectedWorkspace,
getWorkspace,
getDatabaseVariable,
refreshSchema
};
},
data () { data () {
return { return {
isExporting: false, isExporting: false,
@@ -299,11 +322,6 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
getDatabaseVariable: 'workspaces/getDatabaseVariable'
}),
currentWorkspace () { currentWorkspace () {
return this.getWorkspace(this.selectedWorkspace); return this.getWorkspace(this.selectedWorkspace);
}, },
@@ -358,20 +376,16 @@ export default {
structure.forEach(feat => { structure.forEach(feat => {
const val = customizations[this.currentWorkspace.client][feat]; const val = customizations[this.currentWorkspace.client][feat];
if (val) if (val)
this.$set(this.options.includes, feat, true); this.options.includes[feat] = true;
}); });
ipcRenderer.on('export-progress', this.updateProgress); ipcRenderer.on('export-progress', this.updateProgress);
}, },
beforeDestroy () { beforeUnmount () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
ipcRenderer.off('export-progress', this.updateProgress); ipcRenderer.off('export-progress', this.updateProgress);
}, },
methods: { methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshSchema: 'workspaces/refreshSchema'
}),
async startExport () { async startExport () {
this.isExporting = true; this.isExporting = true;
const { uid, client } = this.currentWorkspace; const { uid, client } = this.currentWorkspace;

View File

@@ -1,192 +1,196 @@
<template> <template>
<div class="modal active"> <Teleport to="#window-content">
<a class="modal-overlay" @click.stop="closeModal" /> <div class="modal active">
<div class="modal-container p-0"> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-header pl-2"> <div class="modal-container p-0">
<div class="modal-title h6"> <div class="modal-header pl-2">
<div class="d-flex"> <div class="modal-title h6">
<i class="mdi mdi-24px mdi-playlist-plus mr-1" /> <div class="d-flex">
<span class="cut-text">{{ $tc('message.insertRow', 2) }}</span> <i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span class="cut-text">{{ $tc('message.insertRow', 2) }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal">
<fieldset :disabled="isInserting">
<div
v-for="field in fields"
:key="field.name"
class="form-group"
>
<div class="col-3 col-sm-12">
<label class="form-label" :title="field.name">{{ field.name }}</label>
</div>
<div class="column columns col-sm-12">
<FakerSelect
v-model="localRow[field.name]"
:type="field.type"
class="column columns pr-0"
:is-checked="!fieldsToExclude.includes(field.name)"
:foreign-keys="foreignKeys"
:key-usage="keyUsage"
:field="field"
:field-length="fieldLength(field)"
:field-obj="localRow[field.name]"
>
<span class="input-group-addon field-type" :class="typeClass(field.type)">
{{ field.type }} {{ wrapNumber(fieldLength(field)) }}
</span>
<label class="form-checkbox ml-3" :title="$t('word.insert')">
<input
type="checkbox"
:checked="!fieldsToExclude.includes(field.name)"
@change.prevent="toggleFields($event, field)"
><i class="form-icon" />
</label>
</FakerSelect>
</div>
</div>
</fieldset>
</form>
</div> </div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <div class="modal-footer columns">
</div> <div class="column d-flex" :class="hasFakes ? 'col-4' : 'col-2'">
<div class="modal-body pb-0"> <div class="input-group tooltip tooltip-right" :data-tooltip="$t('message.numberOfInserts')">
<div class="content"> <input
<form class="form-horizontal"> v-model="nInserts"
<fieldset :disabled="isInserting"> type="number"
<div class="form-input"
v-for="field in fields" min="1"
:key="field.name" :disabled="isInserting"
class="form-group"
> >
<div class="col-3 col-sm-12"> <span class="input-group-addon">
<label class="form-label" :title="field.name">{{ field.name }}</label> <i class="mdi mdi-24px mdi-repeat" />
</div> </span>
<div class="column columns col-sm-12"> </div>
<FakerSelect <div
:type="field.type" v-if="hasFakes"
class="column columns pr-0" class="tooltip tooltip-right ml-2"
:is-checked="!fieldsToExclude.includes(field.name)" :data-tooltip="$t('message.fakeDataLanguage')"
:foreign-keys="foreignKeys"
:key-usage="keyUsage"
:field="field"
:field-length="fieldLength(field)"
:field-obj="localRow[field.name]"
:value.sync="localRow[field.name]"
>
<span class="input-group-addon field-type" :class="typeClass(field.type)">
{{ field.type }} {{ fieldLength(field) | wrapNumber }}
</span>
<label class="form-checkbox ml-3" :title="$t('word.insert')">
<input
type="checkbox"
:checked="!fieldsToExclude.includes(field.name)"
@change.prevent="toggleFields($event, field)"
><i class="form-icon" />
</label>
</FakerSelect>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div class="modal-footer columns">
<div class="column d-flex" :class="hasFakes ? 'col-4' : 'col-2'">
<div class="input-group tooltip tooltip-right" :data-tooltip="$t('message.numberOfInserts')">
<input
v-model="nInserts"
type="number"
class="form-input"
min="1"
:disabled="isInserting"
> >
<span class="input-group-addon"> <select v-model="fakerLocale" class="form-select">
<i class="mdi mdi-24px mdi-repeat" /> <option value="ar">
</span> Arabic
</option><option value="az">
Azerbaijani
</option><option value="zh_CN">
Chinese
</option><option value="zh_TW">
Chinese (Taiwan)
</option><option value="cz">
Czech
</option><option value="nl">
Dutch
</option><option value="nl_BE">
Dutch (Belgium)
</option><option value="en">
English
</option><option value="en_AU_ocker">
English (Australia Ocker)
</option><option value="en_AU">
English (Australia)
</option><option value="en_BORK">
English (Bork)
</option><option value="en_CA">
English (Canada)
</option><option value="en_GB">
English (Great Britain)
</option><option value="en_IND">
English (India)
</option><option value="en_IE">
English (Ireland)
</option><option value="en_ZA">
English (South Africa)
</option><option value="en_US">
English (United States)
</option><option value="fa">
Farsi
</option><option value="fi">
Finnish
</option><option value="fr">
French
</option><option value="fr_CA">
French (Canada)
</option><option value="fr_CH">
French (Switzerland)
</option><option value="ge">
Georgian
</option><option value="de">
German
</option><option value="de_AT">
German (Austria)
</option><option value="de_CH">
German (Switzerland)
</option><option value="hr">
Hrvatski
</option><option value="id_ID">
Indonesia
</option><option value="it">
Italian
</option><option value="ja">
Japanese
</option><option value="ko">
Korean
</option><option value="nep">
Nepalese
</option><option value="nb_NO">
Norwegian
</option><option value="pl">
Polish
</option><option value="pt_BR">
Portuguese (Brazil)
</option><option value="pt_PT">
Portuguese (Portugal)
</option><option value="ro">
Romanian
</option><option value="ru">
Russian
</option><option value="sk">
Slovakian
</option><option value="es">
Spanish
</option><option value="es_MX">
Spanish (Mexico)
</option><option value="sv">
Swedish
</option><option value="tr">
Turkish
</option><option value="uk">
Ukrainian
</option><option value="vi">
Vietnamese
</option>
</select>
</div>
</div> </div>
<div <div class="column col-auto">
v-if="hasFakes" <button
class="tooltip tooltip-right ml-2" class="btn btn-primary mr-2"
:data-tooltip="$t('message.fakeDataLanguage')" :class="{'loading': isInserting}"
> @click.stop="insertRows"
<select v-model="fakerLocale" class="form-select"> >
<option value="ar"> {{ $t('word.insert') }}
Arabic </button>
</option><option value="az"> <button class="btn btn-link" @click.stop="closeModal">
Azerbaijani {{ $t('word.close') }}
</option><option value="zh_CN"> </button>
Chinese
</option><option value="zh_TW">
Chinese (Taiwan)
</option><option value="cz">
Czech
</option><option value="nl">
Dutch
</option><option value="nl_BE">
Dutch (Belgium)
</option><option value="en">
English
</option><option value="en_AU_ocker">
English (Australia Ocker)
</option><option value="en_AU">
English (Australia)
</option><option value="en_BORK">
English (Bork)
</option><option value="en_CA">
English (Canada)
</option><option value="en_GB">
English (Great Britain)
</option><option value="en_IND">
English (India)
</option><option value="en_IE">
English (Ireland)
</option><option value="en_ZA">
English (South Africa)
</option><option value="en_US">
English (United States)
</option><option value="fa">
Farsi
</option><option value="fi">
Finnish
</option><option value="fr">
French
</option><option value="fr_CA">
French (Canada)
</option><option value="fr_CH">
French (Switzerland)
</option><option value="ge">
Georgian
</option><option value="de">
German
</option><option value="de_AT">
German (Austria)
</option><option value="de_CH">
German (Switzerland)
</option><option value="hr">
Hrvatski
</option><option value="id_ID">
Indonesia
</option><option value="it">
Italian
</option><option value="ja">
Japanese
</option><option value="ko">
Korean
</option><option value="nep">
Nepalese
</option><option value="nb_NO">
Norwegian
</option><option value="pl">
Polish
</option><option value="pt_BR">
Portuguese (Brazil)
</option><option value="pt_PT">
Portuguese (Portugal)
</option><option value="ro">
Romanian
</option><option value="ru">
Russian
</option><option value="sk">
Slovakian
</option><option value="es">
Spanish
</option><option value="es_MX">
Spanish (Mexico)
</option><option value="sv">
Swedish
</option><option value="tr">
Turkish
</option><option value="uk">
Ukrainian
</option><option value="vi">
Vietnamese
</option>
</select>
</div> </div>
</div> </div>
<div class="column col-auto">
<button
class="btn btn-primary mr-2"
:class="{'loading': isInserting}"
@click.stop="insertRows"
>
{{ $t('word.insert') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
</button>
</div>
</div> </div>
</div> </div>
</div> </Teleport>
</template> </template>
<script> <script>
import moment from 'moment'; import moment from 'moment';
import { storeToRefs } from 'pinia';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { mapGetters, mapActions } from 'vuex'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import FakerSelect from '@/components/FakerSelect'; import FakerSelect from '@/components/FakerSelect';
@@ -195,17 +199,27 @@ export default {
components: { components: {
FakerSelect FakerSelect
}, },
filters: {
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
},
props: { props: {
tabUid: [String, Number], tabUid: [String, Number],
fields: Array, fields: Array,
keyUsage: Array keyUsage: Array
}, },
emits: ['reload', 'hide'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, getWorkspaceTab } = workspacesStore;
return {
addNotification,
selectedWorkspace,
getWorkspace,
getWorkspaceTab
};
},
data () { data () {
return { return {
localRow: {}, localRow: {},
@@ -216,11 +230,6 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
getWorkspaceTab: 'workspaces/getWorkspaceTab'
}),
workspace () { workspace () {
return this.getWorkspace(this.selectedWorkspace); return this.getWorkspace(this.selectedWorkspace);
}, },
@@ -262,7 +271,7 @@ export default {
else if ([...TIME, ...DATE].includes(field.type)) else if ([...TIME, ...DATE].includes(field.type))
fieldDefault = field.default; fieldDefault = field.default;
else if (BIT.includes(field.type)) else if (BIT.includes(field.type))
fieldDefault = field.default.replaceAll('\'', '').replaceAll('b', ''); fieldDefault = field.default?.replaceAll('\'', '').replaceAll('b', '');
else if (DATETIME.includes(field.type)) { else if (DATETIME.includes(field.type)) {
if (field.default && ['current_timestamp', 'now()'].some(term => field.default.toLowerCase().includes(term))) { if (field.default && ['current_timestamp', 'now()'].some(term => field.default.toLowerCase().includes(term))) {
let datePrecision = ''; let datePrecision = '';
@@ -287,13 +296,10 @@ export default {
this.localRow = { ...rowObj }; this.localRow = { ...rowObj };
}, },
beforeDestroy () { beforeUnmount () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
}, },
methods: { methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
typeClass (type) { typeClass (type) {
if (type) if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`; return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
@@ -345,8 +351,8 @@ export default {
}, },
fieldLength (field) { fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null; if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
else if (TEXT.includes(field.type)) return field.charLength; else if (TEXT.includes(field.type)) return Number(field.charLength);
return field.length; return Number(field.length);
}, },
toggleFields (event, field) { toggleFields (event, field) {
if (event.target.checked) if (event.target.checked)
@@ -367,6 +373,10 @@ export default {
e.stopPropagation(); e.stopPropagation();
if (e.key === 'Escape') if (e.key === 'Escape')
this.closeModal(); this.closeModal();
},
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
} }
} }
}; };

View File

@@ -1,103 +1,107 @@
<template> <template>
<div class="modal active"> <Teleport to="#window-content">
<a class="modal-overlay" @click.stop="closeModal" /> <div class="modal active">
<div class="modal-container p-0 pb-4"> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-header pl-2"> <div class="modal-container p-0 pb-4">
<div class="modal-title h6"> <div class="modal-header pl-2">
<div class="d-flex"> <div class="modal-title h6">
<i class="mdi mdi-24px mdi-history mr-1" /> <div class="d-flex">
<span class="cut-text">{{ $t('word.history') }}: {{ connectionName }}</span> <i class="mdi mdi-24px mdi-history mr-1" />
<span class="cut-text">{{ $t('word.history') }}: {{ connectionName }}</span>
</div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <div class="modal-body p-0 workspace-query-results">
</div> <div
<div class="modal-body p-0 workspace-query-results"> v-if="history.length"
<div ref="searchForm"
v-if="history.length" class="form-group has-icon-right p-2 m-0"
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" /> <input
<i v-model="searchTerm"
v-else class="form-input"
class="form-icon c-hand mdi mdi-backspace mdi-18px pr-4" type="text"
@click="searchTerm = ''" :placeholder="$t('message.searchForQueries')"
/>
</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 }"> <i v-if="!searchTerm" class="form-icon mdi mdi-magnify mdi-18px pr-4" />
<div <i
v-for="query in items" v-else
:key="query.uid" class="form-icon c-hand mdi mdi-backspace mdi-18px pr-4"
class="tile my-2" @click="searchTerm = ''"
tabindex="0" />
> </div>
<div class="tile-icon"> <div
<i class="mdi mdi-code-tags pr-1" /> v-if="history.length"
</div> ref="tableWrapper"
<div class="tile-content"> class="vscroll px-1 "
<div class="tile-title"> :style="{'height': resultsSize+'px'}"
<code >
class="cut-text" <div ref="table">
:title="query.sql" <BaseVirtualScroll
v-html="highlightWord(query.sql)" ref="resultTable"
/> :items="filteredHistory"
:item-height="66"
:visible-height="resultsSize"
:scroll-element="scrollElement"
>
<template #default="{ 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>
<div class="tile-bottom-content"> <div class="tile-content">
<small class="tile-subtitle">{{ query.schema }} · {{ formatDate(query.date) }}</small> <div class="tile-title">
<div class="tile-history-buttons"> <code
<button class="btn btn-link pl-1" @click.stop="$emit('select-query', query.sql)"> class="cut-text"
<i class="mdi mdi-open-in-app pr-1" /> {{ $t('word.select') }} :title="query.sql"
</button> v-html="highlightWord(query.sql)"
<button class="btn btn-link pl-1" @click="copyQuery(query.sql)"> />
<i class="mdi mdi-content-copy pr-1" /> {{ $t('word.copy') }} </div>
</button> <div class="tile-bottom-content">
<button class="btn btn-link pl-1" @click="deleteQuery(query)"> <small class="tile-subtitle">{{ query.schema }} · {{ formatDate(query.date) }}</small>
<i class="mdi mdi-delete-forever pr-1" /> {{ $t('word.delete') }} <div class="tile-history-buttons">
</button> <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> </div>
</div> </div>
</div> </template>
</template> </BaseVirtualScroll>
</BaseVirtualScroll> </div>
</div> </div>
</div> <div v-else class="empty">
<div v-else class="empty"> <div class="empty-icon">
<div class="empty-icon"> <i class="mdi mdi-history mdi-48px" />
<i class="mdi mdi-history mdi-48px" /> </div>
<p class="empty-title h5">
{{ $t('message.thereIsNoQueriesYet') }}
</p>
</div> </div>
<p class="empty-title h5">
{{ $t('message.thereIsNoQueriesYet') }}
</p>
</div> </div>
</div> </div>
</div> </div>
</div> </Teleport>
</template> </template>
<script> <script>
import moment from 'moment'; import moment from 'moment';
import { mapGetters, mapActions } from 'vuex'; import { useHistoryStore } from '@/stores/history';
import { useConnectionsStore } from '@/stores/connections';
import { useNotificationsStore } from '@/stores/notifications';
import BaseVirtualScroll from '@/components/BaseVirtualScroll'; import BaseVirtualScroll from '@/components/BaseVirtualScroll';
export default { export default {
@@ -108,6 +112,19 @@ export default {
props: { props: {
connection: Object connection: Object
}, },
emits: ['select-query', 'close'],
setup () {
const { getHistoryByWorkspace, deleteQueryFromHistory } = useHistoryStore();
const { getConnectionName } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
return {
getHistoryByWorkspace,
deleteQueryFromHistory,
getConnectionName,
addNotification
};
},
data () { data () {
return { return {
resultsSize: 1000, resultsSize: 1000,
@@ -119,10 +136,6 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({
getConnectionName: 'connections/getConnectionName',
getHistoryByWorkspace: 'history/getHistoryByWorkspace'
}),
connectionName () { connectionName () {
return this.getConnectionName(this.connection.uid); return this.getConnectionName(this.connection.uid);
}, },
@@ -156,16 +169,12 @@ export default {
this.resizeResults(); this.resizeResults();
window.addEventListener('resize', this.resizeResults); window.addEventListener('resize', this.resizeResults);
}, },
beforeDestroy () { beforeUnmount () {
window.removeEventListener('keydown', this.onKey, { capture: true }); window.removeEventListener('keydown', this.onKey, { capture: true });
window.removeEventListener('resize', this.resizeResults); window.removeEventListener('resize', this.resizeResults);
clearInterval(this.refreshInterval); clearInterval(this.refreshInterval);
}, },
methods: { methods: {
...mapActions({
addNotification: 'notifications/addNotification',
deleteQueryFromHistory: 'history/deleteQueryFromHistory'
}),
copyQuery (sql) { copyQuery (sql) {
navigator.clipboard.writeText(sql); navigator.clipboard.writeText(sql);
}, },

View File

@@ -1,56 +1,61 @@
<template> <template>
<div class="modal active"> <Teleport to="#window-content">
<a class="modal-overlay" @click.stop="closeModal" /> <div class="modal active">
<div class="modal-container p-0"> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-header pl-2"> <div class="modal-container p-0">
<div class="modal-title h6"> <div class="modal-header pl-2">
<div class="d-flex"> <div class="modal-title h6">
<i class="mdi mdi-24px mdi-database-arrow-up mr-1" /> <div class="d-flex">
<span class="cut-text">{{ $t('message.importSchema') }}</span> <i class="mdi mdi-24px mdi-database-arrow-up mr-1" />
<span class="cut-text">{{ $t('message.importSchema') }}</span>
</div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <div class="modal-body pb-0">
</div> {{ sqlFile }}
<div class="modal-body pb-0"> <div v-if="queryErrors.length > 0" class="mt-2">
{{ sqlFile }} <label>{{ $tc('message.importQueryErrors', queryErrors.length) }}</label>
<div v-if="queryErrors.length > 0" class="mt-2"> <textarea
<label>{{ $tc('message.importQueryErrors', queryErrors.length) }}</label> v-model="formattedQueryErrors"
<textarea class="form-input"
v-model="formattedQueryErrors" rows="5"
class="form-input" readonly
rows="5"
readonly
/>
</div>
</div>
<div class="modal-footer columns">
<div class="column col modal-progress-wrapper text-left">
<div class="import-progress">
<span class="progress-status">
{{ progressPercentage }}% - {{ progressStatus }} - {{ $tc('message.executedQueries', queryCount) }}
</span>
<progress
class="progress d-block"
:value="progressPercentage"
max="100"
/> />
</div> </div>
</div> </div>
<div class="column col-auto px-0"> <div class="modal-footer columns">
<button class="btn btn-link" @click.stop="closeModal"> <div class="column col modal-progress-wrapper text-left">
{{ completed ? $t('word.close') : $t('word.cancel') }} <div class="import-progress">
</button> <span class="progress-status">
{{ progressPercentage }}% - {{ progressStatus }} - {{ $tc('message.executedQueries', queryCount) }}
</span>
<progress
class="progress d-block"
:value="progressPercentage"
max="100"
/>
</div>
</div>
<div class="column col-auto px-0">
<button class="btn btn-link" @click.stop="closeModal">
{{ completed ? $t('word.close') : $t('word.cancel') }}
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> <Teleport to="#window-content" />
</teleport>
</template> </template>
<script> <script>
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { mapActions, mapGetters } from 'vuex'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import moment from 'moment'; import moment from 'moment';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { storeToRefs } from 'pinia';
export default { export default {
name: 'ModalImportSchema', name: 'ModalImportSchema',
@@ -58,6 +63,22 @@ export default {
props: { props: {
selectedSchema: String selectedSchema: String
}, },
emits: ['close'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, refreshSchema } = workspacesStore;
return {
addNotification,
selectedWorkspace,
getWorkspace,
refreshSchema
};
},
data () { data () {
return { return {
sqlFile: '', sqlFile: '',
@@ -70,10 +91,6 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
currentWorkspace () { currentWorkspace () {
return this.getWorkspace(this.selectedWorkspace); return this.getWorkspace(this.selectedWorkspace);
}, },
@@ -89,16 +106,12 @@ export default {
ipcRenderer.on('import-progress', this.updateProgress); ipcRenderer.on('import-progress', this.updateProgress);
ipcRenderer.on('query-error', this.handleQueryError); ipcRenderer.on('query-error', this.handleQueryError);
}, },
beforeDestroy () { beforeUnmount () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
ipcRenderer.off('import-progress', this.updateProgress); ipcRenderer.off('import-progress', this.updateProgress);
ipcRenderer.off('query-error', this.handleQueryError); ipcRenderer.off('query-error', this.handleQueryError);
}, },
methods: { methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshSchema: 'workspaces/refreshSchema'
}),
async startImport (sqlFile) { async startImport (sqlFile) {
this.isImporting = true; this.isImporting = true;
this.sqlFile = sqlFile; this.sqlFile = sqlFile;

View File

@@ -1,76 +1,96 @@
<template> <template>
<div class="modal active"> <Teleport to="#window-content">
<a class="modal-overlay" @click.stop="closeModal" /> <div class="modal active">
<div class="modal-container p-0"> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-header pl-2"> <div class="modal-container p-0">
<div class="modal-title h6"> <div class="modal-header pl-2">
<div class="d-flex"> <div class="modal-title h6">
<i class="mdi mdi-24px mdi-database-plus mr-1" /> <div class="d-flex">
<span class="cut-text">{{ $t('message.createNewSchema') }}</span> <i class="mdi mdi-24px mdi-database-plus mr-1" />
<span class="cut-text">{{ $t('message.createNewSchema') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal" @submit.prevent="createSchema">
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.name') }}</label>
</div>
<div class="col-9">
<input
ref="firstInput"
v-model="database.name"
class="form-input"
type="text"
required
:placeholder="$t('message.schemaName')"
>
</div>
</div>
<div v-if="customizations.collations" class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.collation') }}</label>
</div>
<div class="col-9">
<select v-model="database.collation" class="form-select">
<option
v-for="collation in collations"
:key="collation.id"
:value="collation.collation"
>
{{ collation.collation }}
</option>
</select>
<small>{{ $t('message.serverDefault') }}: {{ defaultCollation }}</small>
</div>
</div>
</form>
</div> </div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <div class="modal-footer">
</div> <button
<div class="modal-body pb-0"> class="btn btn-primary mr-2"
<div class="content"> :class="{'loading': isLoading}"
<form class="form-horizontal" @submit.prevent="createSchema"> @click.stop="createSchema"
<div class="form-group"> >
<div class="col-3"> {{ $t('word.add') }}
<label class="form-label">{{ $t('word.name') }}</label> </button>
</div> <button class="btn btn-link" @click.stop="closeModal">
<div class="col-9"> {{ $t('word.close') }}
<input </button>
ref="firstInput"
v-model="database.name"
class="form-input"
type="text"
required
:placeholder="$t('message.schemaName')"
>
</div>
</div>
<div v-if="customizations.collations" class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.collation') }}</label>
</div>
<div class="col-9">
<select v-model="database.collation" class="form-select">
<option
v-for="collation in collations"
:key="collation.id"
:value="collation.collation"
>
{{ collation.collation }}
</option>
</select>
<small>{{ $t('message.serverDefault') }}: {{ defaultCollation }}</small>
</div>
</div>
</form>
</div> </div>
</div> </div>
<div class="modal-footer">
<button
class="btn btn-primary mr-2"
:class="{'loading': isLoading}"
@click.stop="createSchema"
>
{{ $t('word.add') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
</button>
</div>
</div> </div>
</div> </Teleport>
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { storeToRefs } from 'pinia';
export default { export default {
name: 'ModalNewSchema', name: 'ModalNewSchema',
emits: ['reload', 'close'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, getDatabaseVariable } = workspacesStore;
return {
addNotification,
selectedWorkspace,
getWorkspace,
getDatabaseVariable
};
},
data () { data () {
return { return {
isLoading: false, isLoading: false,
@@ -81,11 +101,6 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
getDatabaseVariable: 'workspaces/getDatabaseVariable'
}),
collations () { collations () {
return this.getWorkspace(this.selectedWorkspace).collations; return this.getWorkspace(this.selectedWorkspace).collations;
}, },
@@ -103,13 +118,10 @@ export default {
this.$refs.firstInput.focus(); this.$refs.firstInput.focus();
}, 20); }, 20);
}, },
beforeDestroy () { beforeUnmount () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
}, },
methods: { methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
async createSchema () { async createSchema () {
this.isLoading = true; this.isLoading = true;
try { try {

View File

@@ -1,148 +1,158 @@
<template> <template>
<div class="modal active"> <Teleport to="#window-content">
<a class="modal-overlay" @click.stop="closeModal" /> <div class="modal active">
<div class="modal-container p-0"> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-header pl-2"> <div class="modal-container p-0">
<div class="modal-title h6"> <div class="modal-header pl-2">
<div class="d-flex"> <div class="modal-title h6">
<i class="mdi mdi-24px mdi-playlist-plus mr-1" /> <div class="d-flex">
<span class="cut-text">{{ $t('message.addNewRow') }}</span> <i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span class="cut-text">{{ $t('message.addNewRow') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal">
<fieldset :disabled="isInserting">
<div
v-for="(field, key) in fields"
:key="field.name"
class="form-group"
>
<div class="col-4 col-sm-12">
<label class="form-label" :title="field.name">{{ field.name }}</label>
</div>
<div class="input-group col-8 col-sm-12">
<ForeignKeySelect
v-if="foreignKeys.includes(field.name)"
ref="formInput"
v-model="localRow[field.name]"
class="form-select"
:key-usage="getKeyUsage(field.name)"
:disabled="fieldsToExclude.includes(field.name)"
/>
<input
v-else-if="inputProps(field).mask"
ref="formInput"
v-model="localRow[field.name]"
v-mask="inputProps(field).mask"
class="form-input"
:type="inputProps(field).type"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
>
<input
v-else-if="inputProps(field).type === 'file'"
ref="formInput"
class="form-input"
type="file"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
@change="filesChange($event,field.name)"
>
<input
v-else-if="inputProps(field).type === 'number'"
ref="formInput"
v-model="localRow[field.name]"
class="form-input"
step="any"
:type="inputProps(field).type"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
>
<input
v-else
ref="formInput"
v-model="localRow[field.name]"
class="form-input"
:type="inputProps(field).type"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
>
<span class="input-group-addon" :class="typeCLass(field.type)">
{{ field.type }} {{ wrapNumber(fieldLength(field)) }}
</span>
<label class="form-checkbox ml-3" :title="$t('word.insert')">
<input
type="checkbox"
:checked="!field.autoIncrement"
@change.prevent="toggleFields($event, field)"
><i class="form-icon" />
</label>
</div>
</div>
</fieldset>
</form>
</div> </div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <div class="modal-footer">
</div> <div class="input-group col-3 tooltip tooltip-right" :data-tooltip="$t('message.numberOfInserts')">
<div class="modal-body pb-0"> <input
<div class="content"> v-model="nInserts"
<form class="form-horizontal"> type="number"
<fieldset :disabled="isInserting"> class="form-input"
<div min="1"
v-for="(field, key) in fields" :disabled="isInserting"
:key="field.name" >
class="form-group" <span class="input-group-addon">
> <i class="mdi mdi-24px mdi-repeat" />
<div class="col-4 col-sm-12"> </span>
<label class="form-label" :title="field.name">{{ field.name }}</label> </div>
</div> <div>
<div class="input-group col-8 col-sm-12"> <button
<ForeignKeySelect class="btn btn-primary mr-2"
v-if="foreignKeys.includes(field.name)" :class="{'loading': isInserting}"
ref="formInput" @click.stop="insertRows"
class="form-select" >
:value.sync="localRow[field.name]" {{ $t('word.insert') }}
:key-usage="getKeyUsage(field.name)" </button>
:disabled="fieldsToExclude.includes(field.name)" <button class="btn btn-link" @click.stop="closeModal">
/> {{ $t('word.close') }}
<input </button>
v-else-if="inputProps(field).mask" </div>
ref="formInput"
v-model="localRow[field.name]"
v-mask="inputProps(field).mask"
class="form-input"
:type="inputProps(field).type"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
>
<input
v-else-if="inputProps(field).type === 'file'"
ref="formInput"
class="form-input"
type="file"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
@change="filesChange($event,field.name)"
>
<input
v-else-if="inputProps(field).type === 'number'"
ref="formInput"
v-model="localRow[field.name]"
class="form-input"
step="any"
:type="inputProps(field).type"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
>
<input
v-else
ref="formInput"
v-model="localRow[field.name]"
class="form-input"
:type="inputProps(field).type"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
>
<span class="input-group-addon" :class="typeCLass(field.type)">
{{ field.type }} {{ fieldLength(field) | wrapNumber }}
</span>
<label class="form-checkbox ml-3" :title="$t('word.insert')">
<input
type="checkbox"
:checked="!field.autoIncrement"
@change.prevent="toggleFields($event, field)"
><i class="form-icon" />
</label>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div class="modal-footer">
<div class="input-group col-3 tooltip tooltip-right" :data-tooltip="$t('message.numberOfInserts')">
<input
v-model="nInserts"
type="number"
class="form-input"
min="1"
:disabled="isInserting"
>
<span class="input-group-addon">
<i class="mdi mdi-24px mdi-repeat" />
</span>
</div>
<div>
<button
class="btn btn-primary mr-2"
:class="{'loading': isInserting}"
@click.stop="insertRows"
>
{{ $t('word.insert') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </Teleport>
</template> </template>
<script> <script>
import moment from 'moment'; import moment from 'moment';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { VueMaskDirective } from 'v-mask'; import { useNotificationsStore } from '@/stores/notifications';
import { mapGetters, mapActions } from 'vuex'; import { useWorkspacesStore } from '@/stores/workspaces';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import ForeignKeySelect from '@/components/ForeignKeySelect'; import ForeignKeySelect from '@/components/ForeignKeySelect';
import { storeToRefs } from 'pinia';
export default { export default {
name: 'ModalNewTableRow', name: 'ModalNewTableRow',
components: { components: {
ForeignKeySelect ForeignKeySelect
}, },
directives: {
mask: VueMaskDirective
},
filters: {
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
},
props: { props: {
tabUid: [String, Number], tabUid: [String, Number],
fields: Array, fields: Array,
keyUsage: Array keyUsage: Array
}, },
emits: ['reload', 'hide'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, getWorkspaceTab } = workspacesStore;
return {
addNotification,
selectedWorkspace,
getWorkspace,
getWorkspaceTab
};
},
data () { data () {
return { return {
localRow: {}, localRow: {},
@@ -152,11 +162,6 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
getWorkspaceTab: 'workspaces/getWorkspaceTab'
}),
workspace () { workspace () {
return this.getWorkspace(this.selectedWorkspace); return this.getWorkspace(this.selectedWorkspace);
}, },
@@ -216,13 +221,10 @@ export default {
firstSelectableInput.focus(); firstSelectableInput.focus();
}, 20); }, 20);
}, },
beforeDestroy () { beforeUnmount () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
}, },
methods: { methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
typeClass (type) { typeClass (type) {
if (type) if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`; return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
@@ -331,6 +333,10 @@ export default {
e.stopPropagation(); e.stopPropagation();
if (e.key === 'Escape') if (e.key === 'Escape')
this.closeModal(); this.closeModal();
},
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
} }
} }
}; };

View File

@@ -1,140 +1,143 @@
<template> <template>
<div class="modal active"> <Teleport to="#window-content">
<ModalProcessesListContext <div class="modal active">
v-if="isContext" <ModalProcessesListContext
:context-event="contextEvent" v-if="isContext"
:selected-row="selectedRow" :context-event="contextEvent"
:selected-cell="selectedCell" :selected-row="selectedRow"
@copy-cell="copyCell" :selected-cell="selectedCell"
@copy-row="copyRow" @copy-cell="copyCell"
@kill-process="killProcess" @copy-row="copyRow"
@close-context="closeContext" @kill-process="killProcess"
/> @close-context="closeContext"
<a class="modal-overlay" @click.stop="closeModal" /> />
<div class="modal-container p-0 pb-4"> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-header pl-2"> <div class="modal-container p-0 pb-4">
<div class="modal-title h6"> <div class="modal-header pl-2">
<div class="d-flex"> <div class="modal-title h6">
<i class="mdi mdi-24px mdi-memory mr-1" /> <div class="d-flex">
<span class="cut-text">{{ $t('message.processesList') }}: {{ connectionName }}</span> <i class="mdi mdi-24px mdi-memory mr-1" />
</div> <span class="cut-text">{{ $t('message.processesList') }}: {{ connectionName }}</span>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="processes-toolbar py-2 px-4">
<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="`${$t('word.refresh')} (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" />
</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> </div>
<div class="dropdown table-dropdown"> <a class="btn btn-clear c-hand" @click.stop="closeModal" />
<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>
<div class="workspace-query-info"> <div class="processes-toolbar py-2 px-4">
<div v-if="sortedResults.length"> <div class="workspace-query-buttons">
{{ $t('word.processes') }}: <b>{{ sortedResults.length.toLocaleString() }}</b> <div class="dropdown pr-1">
</div> <div class="btn-group">
</div> <button
</div> class="btn btn-dark btn-sm mr-0 pr-1 d-flex"
<div class="modal-body py-0 workspace-query-results"> :class="{'loading':isQuering}"
<div :title="`${$t('word.refresh')} (F5)`"
ref="tableWrapper" @click="getProcessesList"
class="vscroll"
:style="{'height': resultsSize+'px'}"
>
<div ref="table" class="table table-hover">
<div class="thead">
<div class="tr">
<div
v-for="(field, index) in fields"
:key="index"
class="th c-hand"
> >
<div ref="columnResize" class="column-resizable"> <i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh mr-1" />
<div class="table-column-title" @click="sort(field)"> <i v-else class="mdi mdi-24px mdi-history mdi-flip-h mr-1" />
<span>{{ field.toUpperCase() }}</span> </button>
<i <div class="btn btn-dark btn-sm dropdown-toggle pl-0 pr-0" tabindex="0">
v-if="currentSort === field" <i class="mdi mdi-24px mdi-menu-down" />
class="mdi sort-icon" </div>
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'" <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">
{{ $t('word.processes') }}: <b>{{ sortedResults.length.toLocaleString() }}</b>
</div>
</div>
</div>
<div class="modal-body py-0 workspace-query-results">
<div
ref="tableWrapper"
class="vscroll"
:style="{'height': resultsSize+'px'}"
>
<div ref="table" class="table table-hover">
<div class="thead">
<div class="tr">
<div
v-for="(field, index) in fields"
:key="index"
class="th c-hand"
>
<div ref="columnResize" class="column-resizable">
<div class="table-column-title" @click="sort(field)">
<span>{{ field.toUpperCase() }}</span>
<i
v-if="currentSort === field"
class="mdi sort-icon"
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'"
/>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<BaseVirtualScroll
ref="resultTable"
:items="sortedResults"
:item-height="22"
class="tbody"
:visible-height="resultsSize"
:scroll-element="scrollElement"
>
<template #default="{ items }">
<ModalProcessesListRow
v-for="row in items"
:key="row.id"
class="process-row"
:row="row"
@select-row="selectRow(row.id)"
@contextmenu="contextMenu"
@stop-refresh="stopRefresh"
/>
</template>
</BaseVirtualScroll>
</div> </div>
<BaseVirtualScroll
ref="resultTable"
:items="sortedResults"
:item-height="22"
class="tbody"
:visible-height="resultsSize"
:scroll-element="scrollElement"
>
<template slot-scope="{ items }">
<ModalProcessesListRow
v-for="row in items"
:key="row.id"
class="process-row"
:row="row"
@select-row="selectRow(row.id)"
@contextmenu="contextMenu"
@stop-refresh="stopRefresh"
/>
</template>
</BaseVirtualScroll>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </Teleport>
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex';
import arrayToFile from '../libs/arrayToFile'; import arrayToFile from '../libs/arrayToFile';
import { useNotificationsStore } from '@/stores/notifications';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { useConnectionsStore } from '@/stores/connections';
import BaseVirtualScroll from '@/components/BaseVirtualScroll'; import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import ModalProcessesListRow from '@/components/ModalProcessesListRow'; import ModalProcessesListRow from '@/components/ModalProcessesListRow';
import ModalProcessesListContext from '@/components/ModalProcessesListContext'; import ModalProcessesListContext from '@/components/ModalProcessesListContext';
@@ -149,6 +152,13 @@ export default {
props: { props: {
connection: Object connection: Object
}, },
emits: ['close'],
setup () {
const { addNotification } = useNotificationsStore();
const { getConnectionName } = useConnectionsStore();
return { addNotification, getConnectionName };
},
data () { data () {
return { return {
resultsSize: 1000, resultsSize: 1000,
@@ -167,9 +177,6 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({
getConnectionName: 'connections/getConnectionName'
}),
connectionName () { connectionName () {
return this.getConnectionName(this.connection.uid); return this.getConnectionName(this.connection.uid);
}, },
@@ -203,15 +210,12 @@ export default {
this.getProcessesList(); this.getProcessesList();
window.addEventListener('resize', this.resizeResults); window.addEventListener('resize', this.resizeResults);
}, },
beforeDestroy () { beforeUnmount () {
window.removeEventListener('keydown', this.onKey, { capture: true }); window.removeEventListener('keydown', this.onKey, { capture: true });
window.removeEventListener('resize', this.resizeResults); window.removeEventListener('resize', this.resizeResults);
clearInterval(this.refreshInterval); clearInterval(this.refreshInterval);
}, },
methods: { methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
async getProcessesList () { async getProcessesList () {
this.isQuering = true; this.isQuering = true;
@@ -284,14 +288,14 @@ export default {
this.clearRefresh(); this.clearRefresh();
}, },
selectRow (row) { selectRow (row) {
this.selectedRow = row; this.selectedRow = Number(row);
}, },
contextMenu (event, cell) { contextMenu (event, cell) {
if (event.target.localName === 'input') return; if (event.target.localName === 'input') return;
this.stopRefresh(); this.stopRefresh();
this.selectedCell = cell; this.selectedCell = cell;
this.selectedRow = cell.id; this.selectedRow = Number(cell.id);
this.contextEvent = event; this.contextEvent = event;
this.isContext = true; this.isContext = true;
}, },

View File

@@ -52,6 +52,7 @@ export default {
selectedRow: Number, selectedRow: Number,
selectedCell: Object selectedCell: Object
}, },
emits: ['close-context', 'copy-cell', 'copy-row', 'kill-process'],
computed: { computed: {
}, },
methods: { methods: {

View File

@@ -12,7 +12,7 @@
class="cell-content" class="cell-content"
:class="`${isNull(col)} type-${typeof col === 'number' ? 'int' : 'varchar'}`" :class="`${isNull(col)} type-${typeof col === 'number' ? 'int' : 'varchar'}`"
@dblclick="dblClick(cKey)" @dblclick="dblClick(cKey)"
>{{ col | cutText }}</span> >{{ cutText(col) }}</span>
</div> </div>
<ConfirmModal <ConfirmModal
v-if="isInfoModal" v-if="isInfoModal"
@@ -53,15 +53,10 @@ export default {
ConfirmModal, ConfirmModal,
TextEditor TextEditor
}, },
filters: {
cutText (val) {
if (typeof val !== 'string') return val;
return val.length > 250 ? `${val.substring(0, 250)}[...]` : val;
}
},
props: { props: {
row: Object row: Object
}, },
emits: ['select-row', 'contextmenu', 'stop-refresh'],
data () { data () {
return { return {
isInlineEditor: {}, isInlineEditor: {},
@@ -95,6 +90,10 @@ export default {
this.editingField = null; this.editingField = null;
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
} }
},
cutText (val) {
if (typeof val !== 'string') return val;
return val.length > 250 ? `${val.substring(0, 250)}[...]` : val;
} }
} }
}; };

View File

@@ -1,327 +1,337 @@
<template> <template>
<div id="settings" class="modal active"> <Teleport to="#window-content">
<a class="modal-overlay c-hand" @click="closeModal" /> <div id="settings" class="modal active">
<div class="modal-container"> <a class="modal-overlay c-hand" @click="closeModal" />
<div class="modal-header pl-2"> <div class="modal-container">
<div class="modal-title h6"> <div class="modal-header pl-2">
<div class="d-flex"> <div class="modal-title h6">
<i class="mdi mdi-24px mdi-cog mr-1" /> <div class="d-flex">
<span class="cut-text">{{ $t('word.settings') }}</span> <i class="mdi mdi-24px mdi-cog mr-1" />
<span class="cut-text">{{ $t('word.settings') }}</span>
</div>
</div> </div>
<a class="btn btn-clear c-hand" @click="closeModal" />
</div> </div>
<a class="btn btn-clear c-hand" @click="closeModal" /> <div class="modal-body p-0">
</div> <div class="panel">
<div class="modal-body p-0"> <div class="panel-nav">
<div class="panel"> <ul class="tab tab-block">
<div class="panel-nav"> <li
<ul class="tab tab-block"> class="tab-item c-hand"
<li :class="{'active': selectedTab === 'general'}"
class="tab-item c-hand" @click="selectTab('general')"
:class="{'active': selectedTab === 'general'}" >
@click="selectTab('general')" <a class="tab-link">{{ $t('word.general') }}</a>
> </li>
<a class="tab-link">{{ $t('word.general') }}</a> <li
</li> class="tab-item c-hand"
<li :class="{'active': selectedTab === 'themes'}"
class="tab-item c-hand" @click="selectTab('themes')"
:class="{'active': selectedTab === 'themes'}" >
@click="selectTab('themes')" <a class="tab-link">{{ $t('word.themes') }}</a>
> </li>
<a class="tab-link">{{ $t('word.themes') }}</a> <li
</li> v-if="updateStatus !== 'disabled'"
<li class="tab-item c-hand"
v-if="updateStatus !== 'disabled'" :class="{'active': selectedTab === 'update'}"
class="tab-item c-hand" @click="selectTab('update')"
:class="{'active': selectedTab === 'update'}" >
@click="selectTab('update')" <a class="tab-link" :class="{'badge badge-update': hasUpdates}">{{ $t('word.update') }}</a>
> </li>
<a class="tab-link" :class="{'badge badge-update': hasUpdates}">{{ $t('word.update') }}</a> <li
</li> class="tab-item c-hand"
<li :class="{'active': selectedTab === 'changelog'}"
class="tab-item c-hand" @click="selectTab('changelog')"
:class="{'active': selectedTab === 'changelog'}" >
@click="selectTab('changelog')" <a class="tab-link">{{ $t('word.changelog') }}</a>
> </li>
<a class="tab-link">{{ $t('word.changelog') }}</a> <li
</li> class="tab-item c-hand"
<li :class="{'active': selectedTab === 'about'}"
class="tab-item c-hand" @click="selectTab('about')"
:class="{'active': selectedTab === 'about'}" >
@click="selectTab('about')" <a class="tab-link">{{ $t('word.about') }}</a>
> </li>
<a class="tab-link">{{ $t('word.about') }}</a> </ul>
</li> </div>
</ul> <div v-show="selectedTab === 'general'" class="panel-body py-4">
</div> <div class="container">
<div v-show="selectedTab === 'general'" class="panel-body py-4"> <form class="form-horizontal columns">
<div class="container"> <div class="column col-12 h6 text-uppercase mb-1">
<form class="form-horizontal columns"> {{ $t('word.application') }}
<div class="column col-12 h6 text-uppercase mb-1"> </div>
{{ $t('word.application') }} <div class="column col-12 col-sm-12 mb-2 columns">
</div> <div class="form-group column col-12">
<div class="column col-9 col-sm-12 mb-2"> <div class="col-5 col-sm-12">
<div class="form-group"> <label class="form-label">
<div class="col-7 col-sm-12"> <i class="mdi mdi-18px mdi-translate mr-1" />
<label class="form-label"> {{ $t('word.language') }}
<i class="mdi mdi-18px mdi-translate mr-1" /> </label>
{{ $t('word.language') }} </div>
</label> <div class="col-3 col-sm-12">
</div> <select
<div class="col-5 col-sm-12"> v-model="localLocale"
<select class="form-select"
v-model="localLocale" @change="changeLocale(localLocale)"
class="form-select"
@change="changeLocale(localLocale)"
>
<option
v-for="(locale, key) in locales"
:key="key"
:value="locale.code"
> >
{{ locale.name }} <option
</option> v-for="(locale, key) in locales"
</select> :key="key"
:value="locale.code"
>
{{ locale.name }}
</option>
</select>
</div>
<div class="col-4 col-sm-12 px-2 p-vcentered">
<small class="d-block" style="line-height:1.1; font-size:70%;">
{{ $t('message.missingOrIncompleteTranslation') }}<br>
<a class="text-bold c-hand" @click="openOutside('https://github.com/antares-sql/antares/wiki/Translate-Antares')">{{ $t('message.findOutHowToContribute') }}</a>
</small>
</div>
</div> </div>
</div> <div class="form-group column col-12">
<div class="form-group"> <div class="col-5 col-sm-12">
<div class="col-7 col-sm-12"> <label class="form-label">
<label class="form-label"> {{ $t('message.dataTabPageSize') }}
{{ $t('message.dataTabPageSize') }} </label>
</label> </div>
</div> <div class="col-3 col-sm-12">
<div class="col-5 col-sm-12"> <select
<select v-model="localPageSize"
v-model="localPageSize" class="form-select"
class="form-select" @change="changePageSize(+localPageSize)"
@change="changePageSize(+localPageSize)"
>
<option
v-for="size in pageSizes"
:key="size"
> >
{{ size }} <option
</option> v-for="size in pageSizes"
</select> :key="size"
>
{{ size }}
</option>
</select>
</div>
</div>
<div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12">
<label class="form-label">
{{ $t('message.restorePreviourSession') }}
</label>
</div>
<div class="col-3 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleRestoreSession">
<input type="checkbox" :checked="restoreTabs">
<i class="form-icon" />
</label>
</div>
</div>
<div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12">
<label class="form-label">
{{ $t('message.disableBlur') }}
</label>
</div>
<div class="col-3 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleDisableBlur">
<input type="checkbox" :checked="disableBlur">
<i class="form-icon" />
</label>
</div>
</div>
<div class="form-group column col-12">
<div class="col-5 col-sm-12">
<label class="form-label">
{{ $t('message.notificationsTimeout') }}
</label>
</div>
<div class="col-3 col-sm-12">
<div class="input-group">
<input
v-model="localTimeout"
class="form-input"
type="number"
min="1"
@focusout="checkNotificationsTimeout"
>
<span class="input-group-addon">{{ $t('word.seconds') }}</span>
</div>
</div>
</div> </div>
</div> </div>
<div class="form-group mb-0"> <div class="column col-12 h6 mt-4 text-uppercase mb-1">
<div class="col-7 col-sm-12"> {{ $t('word.editor') }}
<label class="form-label"> </div>
{{ $t('message.restorePreviourSession') }} <div class="column col-12 col-sm-12 columns">
</label> <div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12">
<label class="form-label">
{{ $t('word.autoCompletion') }}
</label>
</div>
<div class="col-3 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="col-5 col-sm-12"> <div class="form-group column col-12 mb-0">
<label class="form-switch d-inline-block" @click.prevent="toggleRestoreSession"> <div class="col-5 col-sm-12">
<input type="checkbox" :checked="restoreTabs"> <label class="form-label">
<i class="form-icon" /> {{ $t('message.wrapLongLines') }}
</label> </label>
</div>
<div class="col-3 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleLineWrap">
<input type="checkbox" :checked="selectedLineWrap">
<i class="form-icon" />
</label>
</div>
</div> </div>
</div> </div>
<div class="form-group mb-0"> </form>
<div class="col-7 col-sm-12"> </div>
<label class="form-label"> </div>
{{ $t('message.disableBlur') }}
</label> <div v-show="selectedTab === 'themes'" class="panel-body py-4">
</div> <div class="container">
<div class="col-5 col-sm-12"> <div class="columns">
<label class="form-switch d-inline-block" @click.prevent="toggleDisableBlur"> <div class="column col-12 h6 text-uppercase mb-2">
<input type="checkbox" :checked="disableBlur"> {{ $t('message.applicationTheme') }}
<i class="form-icon" /> </div>
</label> <div
class="column col-6 c-hand theme-block"
:class="{'selected': applicationTheme === 'dark'}"
@click="changeApplicationTheme('dark')"
>
<img src="../images/dark.png" class="img-responsive img-fit-cover s-rounded">
<div class="theme-name text-light">
<i class="mdi mdi-moon-waning-crescent mdi-48px" />
<div class="h6 mt-4">
{{ $t('word.dark') }}
</div>
</div> </div>
</div> </div>
<div class="form-group"> <div
<div class="col-7 col-sm-12"> class="column col-6 c-hand theme-block"
<label class="form-label"> :class="{'selected': applicationTheme === 'light'}"
{{ $t('message.notificationsTimeout') }} @click="changeApplicationTheme('light')"
</label> >
</div> <img src="../images/light.png" class="img-responsive img-fit-cover s-rounded">
<div class="col-5 col-sm-12"> <div class="theme-name text-dark">
<div class="input-group"> <i class="mdi mdi-white-balance-sunny mdi-48px" />
<input <div class="h6 mt-4">
v-model="localTimeout" {{ $t('word.light') }}
class="form-input"
type="number"
min="1"
@focusout="checkNotificationsTimeout"
>
<span class="input-group-addon">{{ $t('word.seconds') }}</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="column col-12 h6 mt-4 text-uppercase mb-1"> <div class="columns mt-4">
{{ $t('word.editor') }} <div class="column col-12 h6 text-uppercase mb-2 mt-4">
</div> {{ $t('message.editorTheme') }}
<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-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="form-group mb-0"> <div class="column col-6 h5 mb-4">
<div class="col-7 col-sm-12"> <select
<label class="form-label"> v-model="localEditorTheme"
{{ $t('message.wrapLongLines') }} class="form-select"
</label> @change="changeEditorTheme(localEditorTheme)"
</div>
<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" />
</label>
</div>
</div>
</div>
</form>
</div>
</div>
<div v-show="selectedTab === 'themes'" class="panel-body py-4">
<div class="container">
<div class="columns">
<div class="column col-12 h6 text-uppercase mb-2">
{{ $t('message.applicationTheme') }}
</div>
<div
class="column col-6 c-hand theme-block"
:class="{'selected': applicationTheme === 'dark'}"
@click="changeApplicationTheme('dark')"
>
<img src="../images/dark.png" class="img-responsive img-fit-cover s-rounded">
<div class="theme-name text-light">
<i class="mdi mdi-moon-waning-crescent mdi-48px" />
<div class="h6 mt-4">
{{ $t('word.dark') }}
</div>
</div>
</div>
<div
class="column col-6 c-hand theme-block"
:class="{'selected': applicationTheme === 'light'}"
@click="changeApplicationTheme('light')"
>
<img src="../images/light.png" class="img-responsive img-fit-cover s-rounded">
<div class="theme-name text-dark">
<i class="mdi mdi-white-balance-sunny mdi-48px" />
<div class="h6 mt-4">
{{ $t('word.light') }}
</div>
</div>
</div>
</div>
<div class="columns mt-4">
<div class="column col-12 h6 text-uppercase mb-2 mt-4">
{{ $t('message.editorTheme') }}
</div>
<div class="column col-6 h5 mb-4">
<select
v-model="localEditorTheme"
class="form-select"
@change="changeEditorTheme(localEditorTheme)"
>
<optgroup
v-for="group in editorThemes"
:key="group.group"
:label="group.group"
> >
<option <optgroup
v-for="theme in group.themes" v-for="group in editorThemes"
:key="theme.name" :key="group.group"
:value="theme.code" :label="group.group"
:selected="editorTheme === theme.code"
> >
{{ theme.name }} <option
</option> v-for="theme in group.themes"
</optgroup> :key="theme.name"
</select> :value="theme.code"
</div> :selected="editorTheme === theme.code"
<div class="column col-6 mb-4"> >
<div class="btn-group btn-group-block"> {{ theme.name }}
<button </option>
class="btn btn-dark cut-text" </optgroup>
:class="{'active': editorFontSize === 'small'}" </select>
@click="changeEditorFontSize('small')" </div>
> <div class="column col-6 mb-4">
{{ $t('word.small') }} <div class="btn-group btn-group-block">
</button> <button
<button class="btn btn-dark cut-text"
class="btn btn-dark cut-text" :class="{'active': editorFontSize === 'small'}"
:class="{'active': editorFontSize === 'medium'}" @click="changeEditorFontSize('small')"
@click="changeEditorFontSize('medium')" >
> {{ $t('word.small') }}
{{ $t('word.medium') }} </button>
</button> <button
<button class="btn btn-dark cut-text"
class="btn btn-dark cut-text" :class="{'active': editorFontSize === 'medium'}"
:class="{'active': editorFontSize === 'large'}" @click="changeEditorFontSize('medium')"
@click="changeEditorFontSize('large')" >
> {{ $t('word.medium') }}
{{ $t('word.large') }} </button>
</button> <button
class="btn btn-dark cut-text"
:class="{'active': editorFontSize === 'large'}"
@click="changeEditorFontSize('large')"
>
{{ $t('word.large') }}
</button>
</div>
</div>
<div class="column col-12">
<BaseTextEditor
:model-value="exampleQuery"
mode="sql"
:workspace="workspace"
:read-only="true"
:height="270"
/>
</div> </div>
</div>
<div class="column col-12">
<BaseTextEditor
:value="exampleQuery"
mode="sql"
:workspace="workspace"
:read-only="true"
:height="270"
/>
</div> </div>
</div> </div>
</div> </div>
</div>
<div v-show="selectedTab === 'update'" class="panel-body py-4"> <div v-show="selectedTab === 'update'" class="panel-body py-4">
<ModalSettingsUpdate /> <ModalSettingsUpdate />
</div> </div>
<div v-show="selectedTab === 'changelog'" class="panel-body py-4"> <div v-show="selectedTab === 'changelog'" class="panel-body py-4">
<ModalSettingsChangelog /> <ModalSettingsChangelog />
</div> </div>
<div v-show="selectedTab === 'about'" class="panel-body py-4"> <div v-show="selectedTab === 'about'" class="panel-body py-4">
<div class="text-center"> <div class="text-center">
<img src="../images/logo.svg" width="128"> <img src="../images/logo.svg" width="128">
<h4>{{ appName }}</h4> <h4>{{ appName }}</h4>
<p class="mb-2"> <p class="mb-2">
{{ $t('word.version') }} {{ appVersion }}<br> {{ $t('word.version') }} {{ appVersion }}<br>
<a class="c-hand" @click="openOutside('https://github.com/antares-sql/antares')"><i class="mdi mdi-github d-inline" /> GitHub</a> <a class="c-hand" @click="openOutside('https://twitter.com/AntaresSQL')"><i class="mdi mdi-twitter d-inline" /> Twitter</a> <a class="c-hand" @click="openOutside('https://antares-sql.app/')"><i class="mdi mdi-web d-inline" /> Website</a><br> <a class="c-hand" @click="openOutside('https://github.com/antares-sql/antares')"><i class="mdi mdi-github d-inline" /> GitHub</a> <a class="c-hand" @click="openOutside('https://twitter.com/AntaresSQL')"><i class="mdi mdi-twitter d-inline" /> Twitter</a> <a class="c-hand" @click="openOutside('https://antares-sql.app/')"><i class="mdi mdi-web d-inline" /> Website</a><br>
<small>{{ $t('word.author') }} <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">{{ appAuthor }}</a></small><br> <small>{{ $t('word.author') }} <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">{{ appAuthor }}</a></small><br>
</p> </p>
<div class="mb-2"> <div class="mb-2">
<small class="d-block text-uppercase">{{ $t('word.contributors') }}:</small> <small class="d-block text-uppercase">{{ $t('word.contributors') }}:</small>
<div class="d-block py-1"> <div class="d-block py-1">
<small v-for="(contributor, i) in otherContributors" :key="i">{{ i !== 0 ? ', ' : '' }}{{ contributor }}</small> <small v-for="(contributor, i) in otherContributors" :key="i">{{ i !== 0 ? ', ' : '' }}{{ contributor }}</small>
</div>
<small>{{ $t('message.madeWithJS') }}</small>
</div> </div>
<small>{{ $t('message.madeWithJS') }}</small>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </Teleport>
</template> </template>
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { shell } from 'electron';
import { storeToRefs } from 'pinia';
import { useApplicationStore } from '@/stores/application';
import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces';
import localesNames from '@/i18n/supported-locales'; import localesNames from '@/i18n/supported-locales';
import ModalSettingsUpdate from '@/components/ModalSettingsUpdate'; import ModalSettingsUpdate from '@/components/ModalSettingsUpdate';
import ModalSettingsChangelog from '@/components/ModalSettingsChangelog'; import ModalSettingsChangelog from '@/components/ModalSettingsChangelog';
import BaseTextEditor from '@/components/BaseTextEditor'; import BaseTextEditor from '@/components/BaseTextEditor';
const { shell } = require('electron');
export default { export default {
name: 'ModalSettings', name: 'ModalSettings',
@@ -330,6 +340,79 @@ export default {
ModalSettingsChangelog, ModalSettingsChangelog,
BaseTextEditor BaseTextEditor
}, },
setup () {
const applicationStore = useApplicationStore();
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
const {
selectedSettingTab,
updateStatus
} = storeToRefs(applicationStore);
const {
locale: selectedLocale,
dataTabLimit: pageSize,
autoComplete: selectedAutoComplete,
lineWrap: selectedLineWrap,
notificationsTimeout,
restoreTabs,
disableBlur,
applicationTheme,
editorTheme,
editorFontSize
} = storeToRefs(settingsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const {
changeLocale,
changePageSize,
changeRestoreTabs,
changeDisableBlur,
changeAutoComplete,
changeLineWrap,
changeApplicationTheme,
changeEditorTheme,
changeEditorFontSize,
updateNotificationsTimeout
} = settingsStore;
const {
hideSettingModal,
appName,
appVersion
} = applicationStore;
const { getWorkspace } = workspacesStore;
return {
appName,
appVersion,
selectedSettingTab,
updateStatus,
closeModal: hideSettingModal,
selectedLocale,
pageSize,
selectedAutoComplete,
selectedLineWrap,
notificationsTimeout,
restoreTabs,
disableBlur,
applicationTheme,
editorTheme,
editorFontSize,
changeLocale,
changePageSize,
changeRestoreTabs,
changeDisableBlur,
changeAutoComplete,
changeLineWrap,
changeApplicationTheme,
changeEditorTheme,
changeEditorFontSize,
updateNotificationsTimeout,
selectedWorkspace,
getWorkspace
};
},
data () { data () {
return { return {
appAuthor: 'Fabio Di Stasio', appAuthor: 'Fabio Di Stasio',
@@ -392,24 +475,6 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({
appName: 'application/appName',
appVersion: 'application/appVersion',
selectedSettingTab: 'application/selectedSettingTab',
selectedLocale: 'settings/getLocale',
pageSize: 'settings/getDataTabLimit',
selectedAutoComplete: 'settings/getAutoComplete',
selectedLineWrap: 'settings/getLineWrap',
notificationsTimeout: 'settings/getNotificationsTimeout',
restoreTabs: 'settings/getRestoreTabs',
disableBlur: 'settings/getDisableBlur',
applicationTheme: 'settings/getApplicationTheme',
editorTheme: 'settings/getEditorTheme',
editorFontSize: 'settings/getEditorFontSize',
updateStatus: 'application/getUpdateStatus',
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
locales () { locales () {
const locales = []; const locales = [];
for (const locale of this.$i18n.availableLocales) for (const locale of this.$i18n.availableLocales)
@@ -455,23 +520,10 @@ ORDER BY
this.selectedTab = this.selectedSettingTab; this.selectedTab = this.selectedSettingTab;
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
}, },
beforeDestroy () { beforeUnmount () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
}, },
methods: { methods: {
...mapActions({
closeModal: 'application/hideSettingModal',
changeLocale: 'settings/changeLocale',
changePageSize: 'settings/changePageSize',
changeRestoreTabs: 'settings/changeRestoreTabs',
changeDisableBlur: 'settings/changeDisableBlur',
changeAutoComplete: 'settings/changeAutoComplete',
changeLineWrap: 'settings/changeLineWrap',
changeApplicationTheme: 'settings/changeApplicationTheme',
changeEditorTheme: 'settings/changeEditorTheme',
changeEditorFontSize: 'settings/changeEditorFontSize',
updateNotificationsTimeout: 'settings/updateNotificationsTimeout'
}),
selectTab (tab) { selectTab (tab) {
this.selectedTab = tab; this.selectedTab = tab;
}, },

View File

@@ -15,15 +15,19 @@
</template> </template>
<script> <script>
import { mapGetters } from 'vuex';
import { marked } from 'marked'; import { marked } from 'marked';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader';
import { useApplicationStore } from '@/stores/application';
export default { export default {
name: 'ModalSettingsChangelog', name: 'ModalSettingsChangelog',
components: { components: {
BaseLoader BaseLoader
}, },
setup () {
const { appVersion } = useApplicationStore();
return { appVersion };
},
data () { data () {
return { return {
changelog: '', changelog: '',
@@ -32,21 +36,22 @@ export default {
isError: false isError: false
}; };
}, },
computed: {
...mapGetters({ appVersion: 'application/appVersion' })
},
created () { created () {
this.getChangelog(); this.getChangelog();
}, },
methods: { methods: {
async getChangelog () { async getChangelog () {
try { try {
const apiRes = await fetch(`https://api.github.com/repos/Fabio286/antares/releases/tags/v${this.appVersion}`, { const apiRes = await fetch(`https://api.github.com/repos/antares-sql/antares/releases/tags/v${this.appVersion}`, {
method: 'GET' method: 'GET'
}); });
const { body } = await apiRes.json(); const { body } = await apiRes.json();
const markdown = body.substr(0, body.indexOf('### Download')); const cutOffset = body.indexOf('### Download');
const markdown = cutOffset >= 0
? body.substr(0, cutOffset)
: body;
const renderer = { const renderer = {
link (href, title, text) { link (href, title, text) {
return text; return text;

View File

@@ -53,17 +53,33 @@
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex';
import { ipcRenderer, shell } from 'electron'; import { ipcRenderer, shell } from 'electron';
import { storeToRefs } from 'pinia';
import { useApplicationStore } from '@/stores/application';
import { useSettingsStore } from '@/stores/settings';
export default { export default {
name: 'ModalSettingsUpdate', name: 'ModalSettingsUpdate',
setup () {
const applicationStore = useApplicationStore();
const settingsStore = useSettingsStore();
const {
updateStatus,
getDownloadProgress
} = storeToRefs(applicationStore);
const { allowPrerelease } = storeToRefs(settingsStore);
const { changeAllowPrerelease } = settingsStore;
return {
updateStatus,
downloadPercentage: getDownloadProgress,
allowPrerelease,
changeAllowPrerelease
};
},
computed: { computed: {
...mapGetters({
updateStatus: 'application/getUpdateStatus',
downloadPercentage: 'application/getDownloadProgress',
allowPrerelease: 'settings/getAllowPrerelease'
}),
updateMessage () { updateMessage () {
switch (this.updateStatus) { switch (this.updateStatus) {
case 'noupdate': case 'noupdate':
@@ -86,9 +102,6 @@ export default {
} }
}, },
methods: { methods: {
...mapActions({
changeAllowPrerelease: 'settings/changeAllowPrerelease'
}),
openOutside (link) { openOutside (link) {
shell.openExternal(link); shell.openExternal(link);
}, },

View File

@@ -12,13 +12,16 @@
import * as ace from 'ace-builds'; import * as ace from 'ace-builds';
import 'ace-builds/webpack-resolver'; import 'ace-builds/webpack-resolver';
import '../libs/ext-language_tools'; import '../libs/ext-language_tools';
import { mapGetters, mapActions } from 'vuex'; import { storeToRefs } from 'pinia';
import { uidGen } from 'common/libs/uidGen';
import { useApplicationStore } from '@/stores/application';
import { useSettingsStore } from '@/stores/settings';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
export default { export default {
name: 'QueryEditor', name: 'QueryEditor',
props: { props: {
value: String, modelValue: String,
workspace: Object, workspace: Object,
isSelected: Boolean, isSelected: Boolean,
schema: { type: String, default: '' }, schema: { type: String, default: '' },
@@ -26,23 +29,42 @@ export default {
readOnly: { type: Boolean, default: false }, readOnly: { type: Boolean, default: false },
height: { type: Number, default: 200 } height: { type: Number, default: 200 }
}, },
emits: ['update:modelValue'],
setup () {
const editor = null;
const applicationStore = useApplicationStore();
const settingsStore = useSettingsStore();
const { setBaseCompleters } = applicationStore;
const { baseCompleter } = storeToRefs(applicationStore);
const {
editorTheme,
editorFontSize,
autoComplete,
lineWrap
} = storeToRefs(settingsStore);
return {
editor,
baseCompleter,
setBaseCompleters,
editorTheme,
editorFontSize,
autoComplete,
lineWrap
};
},
data () { data () {
return { return {
editor: null, cursorPosition: 0,
fields: [], fields: [],
customCompleter: [], customCompleter: [],
id: null, id: uidGen(),
lastSchema: null lastSchema: null
}; };
}, },
computed: { computed: {
...mapGetters({
editorTheme: 'settings/getEditorTheme',
editorFontSize: 'settings/getEditorFontSize',
autoComplete: 'settings/getAutoComplete',
lineWrap: 'settings/getLineWrap',
baseCompleter: 'application/getBaseCompleter'
}),
tables () { tables () {
return this.workspace return this.workspace
? this.workspace.structure.filter(schema => schema.name === this.schema) ? this.workspace.structure.filter(schema => schema.name === this.schema)
@@ -127,11 +149,8 @@ export default {
return 'sql'; return 'sql';
} }
}, },
cursorPosition () {
return this.editor.session.doc.positionToIndex(this.editor.getCursorPosition());
},
lastWord () { lastWord () {
const charsBefore = this.value.slice(0, this.cursorPosition); const charsBefore = this.modelValue.slice(0, this.cursorPosition);
const words = charsBefore.replaceAll('\n', ' ').split(' ').filter(Boolean); const words = charsBefore.replaceAll('\n', ' ').split(' ').filter(Boolean);
return words.pop(); return words.pop();
}, },
@@ -155,6 +174,9 @@ export default {
} }
}, },
watch: { watch: {
modelValue () {
this.cursorPosition = this.editor.session.doc.positionToIndex(this.editor.getCursorPosition());
},
editorTheme () { editorTheme () {
if (this.editor) if (this.editor)
this.editor.setTheme(`ace/theme/${this.editorTheme}`); this.editor.setTheme(`ace/theme/${this.editorTheme}`);
@@ -205,14 +227,13 @@ export default {
} }
}, },
created () { created () {
this.id = this._uid;
this.lastSchema = this.schema; this.lastSchema = this.schema;
}, },
mounted () { mounted () {
this.editor = ace.edit(`editor-${this.id}`, { this.editor = ace.edit(`editor-${this.id}`, {
mode: `ace/mode/${this.mode}`, mode: `ace/mode/${this.mode}`,
theme: `ace/theme/${this.editorTheme}`, theme: `ace/theme/${this.editorTheme}`,
value: this.value, value: this.modelValue,
fontSize: '14px', fontSize: '14px',
printMargin: false, printMargin: false,
readOnly: this.readOnly readOnly: this.readOnly
@@ -263,7 +284,7 @@ export default {
this.editor.session.on('change', () => { this.editor.session.on('change', () => {
const content = this.editor.getValue(); const content = this.editor.getValue();
this.$emit('update:value', content); this.$emit('update:modelValue', content);
}); });
this.editor.on('guttermousedown', e => { this.editor.on('guttermousedown', e => {
@@ -294,9 +315,6 @@ export default {
}, 20); }, 20);
}, },
methods: { methods: {
...mapActions({
setBaseCompleters: 'application/setBaseCompleter'
}),
setCustomCompleter () { setCustomCompleter () {
this.editor.completers.push({ this.editor.completers.push({
getCompletions: (editor, session, pos, prefix, callback) => { getCompletions: (editor, session, pos, prefix, callback) => {

View File

@@ -30,10 +30,12 @@
</template> </template>
<script> <script>
import { mapActions, mapGetters } from 'vuex';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import { useConnectionsStore } from '@/stores/connections';
import { useWorkspacesStore } from '@/stores/workspaces';
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
import { storeToRefs } from 'pinia';
export default { export default {
name: 'SettingBarContext', name: 'SettingBarContext',
@@ -45,6 +47,26 @@ export default {
contextEvent: MouseEvent, contextEvent: MouseEvent,
contextConnection: Object contextConnection: Object
}, },
emits: ['close-context'],
setup () {
const {
getConnectionName,
addConnection,
deleteConnection
} = useConnectionsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { selectWorkspace } = workspacesStore;
return {
getConnectionName,
addConnection,
deleteConnection,
selectedWorkspace,
selectWorkspace
};
},
data () { data () {
return { return {
isConfirmModal: false, isConfirmModal: false,
@@ -52,20 +74,11 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({
getConnectionName: 'connections/getConnectionName',
selectedWorkspace: 'workspaces/getSelected'
}),
connectionName () { connectionName () {
return this.getConnectionName(this.contextConnection.uid); return this.getConnectionName(this.contextConnection.uid);
} }
}, },
methods: { methods: {
...mapActions({
addConnection: 'connections/addConnection',
deleteConnection: 'connections/deleteConnection',
selectWorkspace: 'workspaces/selectWorkspace'
}),
confirmDeleteConnection () { confirmDeleteConnection () {
if (this.selectedWorkspace === this.contextConnection.uid) if (this.selectedWorkspace === this.contextConnection.uid)
this.selectWorkspace(); this.selectWorkspace();
@@ -77,7 +90,7 @@ export default {
connectionCopy = { connectionCopy = {
...connectionCopy, ...connectionCopy,
uid: uidGen('C'), uid: uidGen('C'),
name: connectionCopy.name ? `${connectionCopy.name}_copy` : '' name: connectionCopy.name ? `${connectionCopy?.name}_copy` : ''
}; };
this.addConnection(connectionCopy); this.addConnection(connectionCopy);

View File

@@ -27,17 +27,30 @@
</template> </template>
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { useApplicationStore } from '@/stores/application';
import { useWorkspacesStore } from '@/stores/workspaces';
import { storeToRefs } from 'pinia';
const { shell } = require('electron'); const { shell } = require('electron');
export default { export default {
name: 'TheFooter', name: 'TheFooter',
setup () {
const applicationStore = useApplicationStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: workspace } = storeToRefs(workspacesStore);
const { appVersion, showSettingModal } = applicationStore;
const { getWorkspace } = workspacesStore;
return {
appVersion,
showSettingModal,
workspace,
getWorkspace
};
},
computed: { computed: {
...mapGetters({
workspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
appVersion: 'application/appVersion'
}),
version () { version () {
return this.getWorkspace(this.workspace) ? this.getWorkspace(this.workspace).version : null; return this.getWorkspace(this.workspace) ? this.getWorkspace(this.workspace).version : null;
}, },
@@ -48,9 +61,6 @@ export default {
} }
}, },
methods: { methods: {
...mapActions({
showSettingModal: 'application/showSettingModal'
}),
openOutside (link) { openOutside (link) {
shell.openExternal(link); shell.openExternal(link);
} }

View File

@@ -4,7 +4,7 @@
@mouseenter="clearTimeouts" @mouseenter="clearTimeouts"
@mouseleave="rearmTimeouts" @mouseleave="rearmTimeouts"
> >
<transition-group name="slide-fade"> <TransitionGroup tag="div" name="slide-fade">
<BaseNotification <BaseNotification
v-for="notification in latestNotifications" v-for="notification in latestNotifications"
:key="notification.uid" :key="notification.uid"
@@ -12,29 +12,42 @@
:status="notification.status" :status="notification.status"
@close="removeNotification(notification.uid)" @close="removeNotification(notification.uid)"
/> />
</transition-group> </TransitionGroup>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { useNotificationsStore } from '@/stores/notifications';
import { useSettingsStore } from '@/stores/settings';
import BaseNotification from '@/components/BaseNotification'; import BaseNotification from '@/components/BaseNotification';
import { storeToRefs } from 'pinia';
export default { export default {
name: 'TheNotificationsBoard', name: 'TheNotificationsBoard',
components: { components: {
BaseNotification BaseNotification
}, },
setup () {
const notificationsStore = useNotificationsStore();
const settingsStore = useSettingsStore();
const { removeNotification } = notificationsStore;
const { notifications } = storeToRefs(notificationsStore);
const { notificationsTimeout } = storeToRefs(settingsStore);
return {
removeNotification,
notifications,
notificationsTimeout
};
},
data () { data () {
return { return {
timeouts: {} timeouts: {}
}; };
}, },
computed: { computed: {
...mapGetters({
notifications: 'notifications/getNotifications',
notificationsTimeout: 'settings/getNotificationsTimeout'
}),
latestNotifications () { latestNotifications () {
return this.notifications.slice(0, 10); return this.notifications.slice(0, 10);
} }
@@ -51,9 +64,6 @@ export default {
} }
}, },
methods: { methods: {
...mapActions({
removeNotification: 'notifications/removeNotification'
}),
clearTimeouts () { clearTimeouts () {
for (const uid in this.timeouts) { for (const uid in this.timeouts) {
clearTimeout(this.timeouts[uid]); clearTimeout(this.timeouts[uid]);

View File

@@ -15,7 +15,7 @@
<div> <div>
<div> <div>
<TextEditor <TextEditor
:value.sync="localNotes" v-model="localNotes"
editor-class="textarea-editor" editor-class="textarea-editor"
mode="markdown" mode="markdown"
:auto-focus="true" :auto-focus="true"
@@ -29,9 +29,11 @@
</template> </template>
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { storeToRefs } from 'pinia';
import { useApplicationStore } from '@/stores/application';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor'; import TextEditor from '@/components/BaseTextEditor';
import { useScratchpadStore } from '@/stores/scratchpad';
export default { export default {
name: 'TheScratchpad', name: 'TheScratchpad',
@@ -39,17 +41,26 @@ export default {
ConfirmModal, ConfirmModal,
TextEditor TextEditor
}, },
emits: ['hide'],
setup () {
const applicationStore = useApplicationStore();
const scratchpadStore = useScratchpadStore();
const { notes } = storeToRefs(scratchpadStore);
const { changeNotes } = scratchpadStore;
return {
notes,
hideScratchpad: applicationStore.hideScratchpad,
changeNotes
};
},
data () { data () {
return { return {
localNotes: '', localNotes: '',
debounceTimeout: null debounceTimeout: null
}; };
}, },
computed: {
...mapGetters({
notes: 'scratchpad/getNotes'
})
},
watch: { watch: {
localNotes () { localNotes () {
clearTimeout(this.debounceTimeout); clearTimeout(this.debounceTimeout);
@@ -63,10 +74,6 @@ export default {
this.localNotes = this.notes; this.localNotes = this.notes;
}, },
methods: { methods: {
...mapActions({
hideScratchpad: 'application/hideScratchpad',
changeNotes: 'scratchpad/changeNotes'
}),
hideModal () { hideModal () {
this.$emit('hide'); this.$emit('hide');
} }

View File

@@ -10,22 +10,23 @@
<ul class="settingbar-elements"> <ul class="settingbar-elements">
<Draggable <Draggable
v-model="connections" v-model="connections"
:item-key="'uid'"
@start="isDragging = true" @start="isDragging = true"
@end="dragStop" @end="dragStop"
> >
<li <template #item="{element}">
v-for="connection in connections" <li
:key="connection.uid" :draggable="true"
draggable="true" class="settingbar-element btn btn-link ex-tooltip"
class="settingbar-element btn btn-link ex-tooltip" :class="{'selected': element.uid === selectedWorkspace}"
:class="{'selected': connection.uid === selectedWorkspace}" @click.stop="selectWorkspace(element.uid)"
@click.stop="selectWorkspace(connection.uid)" @contextmenu.prevent="contextMenu($event, element)"
@contextmenu.prevent="contextMenu($event, connection)" @mouseover.self="tooltipPosition"
@mouseover.self="tooltipPosition" >
> <i class="settingbar-element-icon dbi" :class="`dbi-${element.client} ${getStatusBadge(element.uid)}`" />
<i class="settingbar-element-icon dbi" :class="`dbi-${connection.client} ${getStatusBadge(connection.uid)}`" /> <span v-if="!isDragging" class="ex-tooltip-content">{{ getConnectionName(element.uid) }}</span>
<span v-if="!isDragging" class="ex-tooltip-content">{{ getConnectionName(connection.uid) }}</span> </li>
</li> </template>
</Draggable> </Draggable>
<li <li
class="settingbar-element btn btn-link ex-tooltip" class="settingbar-element btn btn-link ex-tooltip"
@@ -55,7 +56,10 @@
</template> </template>
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { storeToRefs } from 'pinia';
import { useApplicationStore } from '@/stores/application';
import { useConnectionsStore } from '@/stores/connections';
import { useWorkspacesStore } from '@/stores/workspaces';
import Draggable from 'vuedraggable'; import Draggable from 'vuedraggable';
import SettingBarContext from '@/components/SettingBarContext'; import SettingBarContext from '@/components/SettingBarContext';
@@ -65,6 +69,32 @@ export default {
Draggable, Draggable,
SettingBarContext SettingBarContext
}, },
setup () {
const applicationStore = useApplicationStore();
const connectionsStore = useConnectionsStore();
const workspacesStore = useWorkspacesStore();
const { updateStatus } = storeToRefs(applicationStore);
const { connections: getConnections } = storeToRefs(connectionsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { showSettingModal, showScratchpad } = applicationStore;
const { getConnectionName, updateConnections } = connectionsStore;
const { getWorkspace, selectWorkspace } = workspacesStore;
return {
applicationStore,
updateStatus,
showSettingModal,
showScratchpad,
getConnections,
getConnectionName,
updateConnections,
selectedWorkspace,
getWorkspace,
selectWorkspace
};
},
data () { data () {
return { return {
dragElement: null, dragElement: null,
@@ -76,13 +106,6 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({
getConnections: 'connections/getConnections',
getConnectionName: 'connections/getConnectionName',
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected',
updateStatus: 'application/getUpdateStatus'
}),
connections: { connections: {
get () { get () {
return this.getConnections; return this.getConnections;
@@ -96,12 +119,6 @@ export default {
} }
}, },
methods: { methods: {
...mapActions({
updateConnections: 'connections/updateConnections',
showSettingModal: 'application/showSettingModal',
showScratchpad: 'application/showScratchpad',
selectWorkspace: 'workspaces/selectWorkspace'
}),
contextMenu (event, connection) { contextMenu (event, connection) {
this.contextEvent = event; this.contextEvent = event;
this.contextConnection = connection; this.contextConnection = connection;

View File

@@ -55,10 +55,26 @@
<script> <script>
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { getCurrentWindow } from '@electron/remote'; import { getCurrentWindow } from '@electron/remote';
import { mapGetters } from 'vuex'; import { useConnectionsStore } from '@/stores/connections';
import { useWorkspacesStore } from '@/stores/workspaces';
import { storeToRefs } from 'pinia';
export default { export default {
name: 'TheTitleBar', name: 'TheTitleBar',
setup () {
const { getConnectionName } = useConnectionsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace } = workspacesStore;
return {
getConnectionName,
selectedWorkspace,
getWorkspace
};
},
data () { data () {
return { return {
w: getCurrentWindow(), w: getCurrentWindow(),
@@ -68,18 +84,13 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({
getConnectionName: 'connections/getConnectionName',
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
windowTitle () { windowTitle () {
if (!this.selectedWorkspace) return ''; if (!this.selectedWorkspace) return '';
if (this.selectedWorkspace === 'NEW') return this.$t('message.createNewConnection'); if (this.selectedWorkspace === 'NEW') return this.$t('message.createNewConnection');
const connectionName = this.getConnectionName(this.selectedWorkspace); const connectionName = this.getConnectionName(this.selectedWorkspace);
const workspace = this.getWorkspace(this.selectedWorkspace); const workspace = this.getWorkspace(this.selectedWorkspace);
const breadcrumbs = Object.values(workspace.breadcrumbs).filter(breadcrumb => breadcrumb); const breadcrumbs = Object.values(workspace.breadcrumbs).filter(breadcrumb => breadcrumb) || [workspace.client];
return [connectionName, ...breadcrumbs].join(' • '); return [connectionName, ...breadcrumbs].join(' • ');
} }
@@ -87,7 +98,7 @@ export default {
created () { created () {
window.addEventListener('resize', this.onResize); window.addEventListener('resize', this.onResize);
}, },
destroyed () { unmounted () {
window.removeEventListener('resize', this.onResize); window.removeEventListener('resize', this.onResize);
}, },
methods: { methods: {

View File

@@ -1,264 +1,265 @@
<template> <template>
<div v-show="isSelected" class="workspace column columns col-gapless"> <div v-show="isSelected" class="workspace column columns col-gapless">
<WorkspaceExploreBar <WorkspaceExploreBar
v-if="workspace.connectionStatus === 'connected'" v-if="workspace?.connectionStatus === 'connected'"
:connection="connection" :connection="connection"
:is-selected="isSelected" :is-selected="isSelected"
/> />
<div v-if="workspace.connectionStatus === 'connected'" class="workspace-tabs column columns col-gapless"> <div v-if="workspace?.connectionStatus === 'connected'" class="workspace-tabs column columns col-gapless">
<Draggable <Draggable
ref="tabWrap" ref="tabWrap"
v-model="draggableTabs" v-model="draggableTabs"
tag="ul" tag="ul"
item-key="uid"
group="tabs" group="tabs"
class="tab tab-block column col-12" class="tab tab-block column col-12"
draggable=".tab-draggable" draggable=".tab-draggable"
@mouseover.native="addWheelEvent" @mouseover="addWheelEvent"
> >
<li <template #item="{element}">
v-for="(tab, i) of draggableTabs" <li
:key="i" class="tab-item tab-draggable"
class="tab-item tab-draggable" :draggable="true"
draggable="true" :class="{'active': selectedTab === element.uid}"
:class="{'active': selectedTab === tab.uid}" @mousedown.left="selectTab({uid: workspace.uid, tab: element.uid})"
@mousedown.left="selectTab({uid: workspace.uid, tab: tab.uid})" @mouseup.middle="closeTab(element)"
@mouseup.middle="closeTab(tab)"
>
<a
v-if="tab.type === 'query'"
class="tab-link"
:class="{'badge': tab.isChanged}"
> >
<i class="mdi mdi-18px mdi-code-tags mr-1" /> <a
<span> v-if="element.type === 'query'"
<span>{{ tab.content || 'Query' | cutText }} #{{ tab.index }}</span> class="tab-link"
<span :class="{'badge': element.isChanged}"
class="btn btn-clear" >
:title="$t('word.close')" <i class="mdi mdi-18px mdi-code-tags mr-1" />
@mousedown.left.stop <span>
@click.stop="closeTab(tab)" <span>{{ cutText(element.content || 'Query') }} #{{ element.index }}</span>
/> <span
</span> class="btn btn-clear"
</a> :title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(element)"
/>
</span>
</a>
<a <a
v-else-if="tab.type === 'temp-data'" v-else-if="element.type === 'temp-data'"
class="tab-link" class="tab-link"
@dblclick="openAsPermanentTab(tab)" @dblclick="openAsPermanentTab(element)"
> >
<i class="mdi mdi-18px mr-1" :class="tab.elementType === 'view' ? 'mdi-table-eye' : 'mdi-table'" /> <i class="mdi mdi-18px mr-1" :class="element.elementType === 'view' ? 'mdi-table-eye' : 'mdi-table'" />
<span :title="`${$t('word.data').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`"> <span :title="`${$t('word.data').toUpperCase()}: ${$tc(`word.${element.elementType}`)}`">
<span class=" text-italic">{{ tab.elementName | cutText }}</span> <span class=" text-italic">{{ cutText(element.elementName) }}</span>
<span <span
class="btn btn-clear" class="btn btn-clear"
:title="$t('word.close')" :title="$t('word.close')"
@mousedown.left.stop @mousedown.left.stop
@click.stop="closeTab(tab)" @click.stop="closeTab(element)"
/> />
</span> </span>
</a> </a>
<a v-else-if="tab.type === 'data'" class="tab-link"> <a v-else-if="element.type === 'data'" class="tab-link">
<i class="mdi mdi-18px mr-1" :class="tab.elementType === 'view' ? 'mdi-table-eye' : 'mdi-table'" /> <i class="mdi mdi-18px mr-1" :class="element.elementType === 'view' ? 'mdi-table-eye' : 'mdi-table'" />
<span :title="`${$t('word.data').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`"> <span :title="`${$t('word.data').toUpperCase()}: ${$tc(`word.${element.elementType}`)}`">
{{ tab.elementName | cutText }} {{ cutText(element.elementName) }}
<span <span
class="btn btn-clear" class="btn btn-clear"
:title="$t('word.close')" :title="$t('word.close')"
@mousedown.left.stop @mousedown.left.stop
@click.stop="closeTab(tab)" @click.stop="closeTab(element)"
/> />
</span> </span>
</a> </a>
<a <a
v-else-if="tab.type === 'new-table'" v-else-if="element.type === 'new-table'"
class="tab-link" class="tab-link"
:class="{'badge': tab.isChanged}" :class="{'badge': element.isChanged}"
> >
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" /> <i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`"> <span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${element.elementType}`)}`">
{{ $t('message.newTable') }} {{ $t('message.newTable') }}
<span <span
class="btn btn-clear" class="btn btn-clear"
:title="$t('word.close')" :title="$t('word.close')"
@mousedown.left.stop @mousedown.left.stop
@click.stop="closeTab(tab)" @click.stop="closeTab(element)"
/> />
</span> </span>
</a> </a>
<a <a
v-else-if="tab.type === 'table-props'" v-else-if="element.type === 'table-props'"
class="tab-link" class="tab-link"
:class="{'badge': tab.isChanged}" :class="{'badge': element.isChanged}"
> >
<i class="mdi mdi-tune-vertical-variant mdi-18px mr-1" /> <i class="mdi mdi-tune-vertical-variant mdi-18px mr-1" />
<span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`"> <span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.${element.elementType}`)}`">
{{ tab.elementName | cutText }} {{ cutText(element.elementName) }}
<span <span
class="btn btn-clear" class="btn btn-clear"
:title="$t('word.close')" :title="$t('word.close')"
@mousedown.left.stop @mousedown.left.stop
@click.stop="closeTab(tab)" @click.stop="closeTab(element)"
/> />
</span> </span>
</a> </a>
<a <a
v-else-if="tab.type === 'view-props'" v-else-if="element.type === 'view-props'"
class="tab-link" class="tab-link"
:class="{'badge': tab.isChanged}" :class="{'badge': element.isChanged}"
> >
<i class="mdi mdi-tune-vertical-variant mdi-18px mr-1" /> <i class="mdi mdi-tune-vertical-variant mdi-18px mr-1" />
<span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.view`)}`"> <span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.view`)}`">
{{ tab.elementName | cutText }} {{ cutText(element.elementName) }}
<span <span
class="btn btn-clear" class="btn btn-clear"
:title="$t('word.close')" :title="$t('word.close')"
@mousedown.left.stop @mousedown.left.stop
@click.stop="closeTab(tab)" @click.stop="closeTab(element)"
/> />
</span> </span>
</a> </a>
<a <a
v-else-if="tab.type === 'new-view'" v-else-if="element.type === 'new-view'"
class="tab-link" class="tab-link"
:class="{'badge': tab.isChanged}" :class="{'badge': element.isChanged}"
> >
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" /> <i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`"> <span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${element.elementType}`)}`">
{{ $t('message.newView') }} {{ $t('message.newView') }}
<span <span
class="btn btn-clear" class="btn btn-clear"
:title="$t('word.close')" :title="$t('word.close')"
@mousedown.left.stop @mousedown.left.stop
@click.stop="closeTab(tab)" @click.stop="closeTab(element)"
/> />
</span> </span>
</a> </a>
<a <a
v-else-if="tab.type === 'new-trigger'" v-else-if="element.type === 'new-trigger'"
class="tab-link" class="tab-link"
:class="{'badge': tab.isChanged}" :class="{'badge': element.isChanged}"
> >
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" /> <i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`"> <span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${element.elementType}`)}`">
{{ $t('message.newTrigger') }} {{ $t('message.newTrigger') }}
<span <span
class="btn btn-clear" class="btn btn-clear"
:title="$t('word.close')" :title="$t('word.close')"
@mousedown.left.stop @mousedown.left.stop
@click.stop="closeTab(tab)" @click.stop="closeTab(element)"
/> />
</span> </span>
</a> </a>
<a <a
v-else-if="tab.type === 'new-routine'" v-else-if="element.type === 'new-routine'"
class="tab-link" class="tab-link"
:class="{'badge': tab.isChanged}" :class="{'badge': element.isChanged}"
> >
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" /> <i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`"> <span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${element.elementType}`)}`">
{{ $t('message.newRoutine') }} {{ $t('message.newRoutine') }}
<span <span
class="btn btn-clear" class="btn btn-clear"
:title="$t('word.close')" :title="$t('word.close')"
@mousedown.left.stop @mousedown.left.stop
@click.stop="closeTab(tab)" @click.stop="closeTab(element)"
/> />
</span> </span>
</a> </a>
<a <a
v-else-if="tab.type === 'new-function'" v-else-if="element.type === 'new-function'"
class="tab-link" class="tab-link"
:class="{'badge': tab.isChanged}" :class="{'badge': element.isChanged}"
> >
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" /> <i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`"> <span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${element.elementType}`)}`">
{{ $t('message.newFunction') }} {{ $t('message.newFunction') }}
<span <span
class="btn btn-clear" class="btn btn-clear"
:title="$t('word.close')" :title="$t('word.close')"
@mousedown.left.stop @mousedown.left.stop
@click.stop="closeTab(tab)" @click.stop="closeTab(element)"
/> />
</span> </span>
</a> </a>
<a <a
v-else-if="tab.type === 'new-trigger-function'" v-else-if="element.type === 'new-trigger-function'"
class="tab-link" class="tab-link"
:class="{'badge': tab.isChanged}" :class="{'badge': element.isChanged}"
> >
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" /> <i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`"> <span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${element.elementType}`)}`">
{{ $t('message.newTriggerFunction') }} {{ $t('message.newTriggerFunction') }}
<span <span
class="btn btn-clear" class="btn btn-clear"
:title="$t('word.close')" :title="$t('word.close')"
@mousedown.left.stop @mousedown.left.stop
@click.stop="closeTab(tab)" @click.stop="closeTab(element)"
/> />
</span> </span>
</a> </a>
<a <a
v-else-if="tab.type === 'new-scheduler'" v-else-if="element.type === 'new-scheduler'"
class="tab-link" class="tab-link"
:class="{'badge': tab.isChanged}" :class="{'badge': element.isChanged}"
> >
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" /> <i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`"> <span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${element.elementType}`)}`">
{{ $t('message.newScheduler') }} {{ $t('message.newScheduler') }}
<span <span
class="btn btn-clear" class="btn btn-clear"
:title="$t('word.close')" :title="$t('word.close')"
@mousedown.left.stop @mousedown.left.stop
@click.stop="closeTab(tab)" @click.stop="closeTab(element)"
/> />
</span> </span>
</a> </a>
<a <a
v-else-if="tab.type.includes('temp-')" v-else-if="element.type.includes('temp-')"
class="tab-link" class="tab-link"
:class="{'badge': tab.isChanged}" :class="{'badge': element.isChanged}"
@dblclick="openAsPermanentTab(tab)" @dblclick="openAsPermanentTab(element)"
> >
<i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" /> <i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" />
<span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`"> <span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.${element.elementType}`)}`">
<span class=" text-italic">{{ tab.elementName | cutText }}</span> <span class=" text-italic">{{ cutText(element.elementName) }}</span>
<span <span
class="btn btn-clear" class="btn btn-clear"
:title="$t('word.close')" :title="$t('word.close')"
@mousedown.left.stop @mousedown.left.stop
@click.stop="closeTab(tab)" @click.stop="closeTab(element)"
/> />
</span> </span>
</a> </a>
<a <a
v-else v-else
class="tab-link" class="tab-link"
:class="{'badge': tab.isChanged}" :class="{'badge': element.isChanged}"
> >
<i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" /> <i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" />
<span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`"> <span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.${element.elementType}`)}`">
{{ tab.elementName | cutText }} {{ cutText(element.elementName) }}
<span <span
class="btn btn-clear" class="btn btn-clear"
:title="$t('word.close')" :title="$t('word.close')"
@mousedown.left.stop @mousedown.left.stop
@click.stop="closeTab(tab)" @click.stop="closeTab(element)"
/> />
</span> </span>
</a> </a>
</li> </li>
</template>
<template #header> <template #header>
<li <li
v-if="workspace.customizations.processesList" v-if="workspace.customizations.processesList"
@@ -318,6 +319,7 @@
<WorkspaceTabQuery <WorkspaceTabQuery
v-if="tab.type==='query'" v-if="tab.type==='query'"
:key="tab.uid" :key="tab.uid"
:tab-uid="tab.uid"
:tab="tab" :tab="tab"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
:connection="connection" :connection="connection"
@@ -325,6 +327,7 @@
<WorkspaceTabTable <WorkspaceTabTable
v-else-if="['temp-data', 'data'].includes(tab.type)" v-else-if="['temp-data', 'data'].includes(tab.type)"
:key="tab.uid" :key="tab.uid"
:tab-uid="tab.uid"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
:table="tab.elementName" :table="tab.elementName"
@@ -334,6 +337,7 @@
<WorkspaceTabNewTable <WorkspaceTabNewTable
v-else-if="tab.type === 'new-table'" v-else-if="tab.type === 'new-table'"
:key="tab.uid" :key="tab.uid"
:tab-uid="tab.uid"
:tab="tab" :tab="tab"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
@@ -342,6 +346,7 @@
<WorkspaceTabPropsTable <WorkspaceTabPropsTable
v-else-if="tab.type === 'table-props'" v-else-if="tab.type === 'table-props'"
:key="tab.uid" :key="tab.uid"
:tab-uid="tab.uid"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
:table="tab.elementName" :table="tab.elementName"
@@ -350,6 +355,7 @@
<WorkspaceTabNewView <WorkspaceTabNewView
v-else-if="tab.type === 'new-view'" v-else-if="tab.type === 'new-view'"
:key="tab.uid" :key="tab.uid"
:tab-uid="tab.uid"
:tab="tab" :tab="tab"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
@@ -358,6 +364,7 @@
<WorkspaceTabPropsView <WorkspaceTabPropsView
v-else-if="tab.type === 'view-props'" v-else-if="tab.type === 'view-props'"
:key="tab.uid" :key="tab.uid"
:tab-uid="tab.uid"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
:connection="connection" :connection="connection"
:view="tab.elementName" :view="tab.elementName"
@@ -366,6 +373,7 @@
<WorkspaceTabNewTrigger <WorkspaceTabNewTrigger
v-else-if="tab.type === 'new-trigger'" v-else-if="tab.type === 'new-trigger'"
:key="tab.uid" :key="tab.uid"
:tab-uid="tab.uid"
:tab="tab" :tab="tab"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
@@ -375,6 +383,7 @@
<WorkspaceTabPropsTrigger <WorkspaceTabPropsTrigger
v-else-if="['temp-trigger-props', 'trigger-props'].includes(tab.type)" v-else-if="['temp-trigger-props', 'trigger-props'].includes(tab.type)"
:key="tab.uid" :key="tab.uid"
:tab-uid="tab.uid"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
:trigger="tab.elementName" :trigger="tab.elementName"
@@ -383,6 +392,7 @@
<WorkspaceTabNewTriggerFunction <WorkspaceTabNewTriggerFunction
v-else-if="tab.type === 'new-trigger-function'" v-else-if="tab.type === 'new-trigger-function'"
:key="tab.uid" :key="tab.uid"
:tab-uid="tab.uid"
:tab="tab" :tab="tab"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
@@ -392,6 +402,7 @@
<WorkspaceTabPropsTriggerFunction <WorkspaceTabPropsTriggerFunction
v-else-if="['temp-trigger-function-props', 'trigger-function-props'].includes(tab.type)" v-else-if="['temp-trigger-function-props', 'trigger-function-props'].includes(tab.type)"
:key="tab.uid" :key="tab.uid"
:tab-uid="tab.uid"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
:function="tab.elementName" :function="tab.elementName"
@@ -400,6 +411,7 @@
<WorkspaceTabNewRoutine <WorkspaceTabNewRoutine
v-else-if="tab.type === 'new-routine'" v-else-if="tab.type === 'new-routine'"
:key="tab.uid" :key="tab.uid"
:tab-uid="tab.uid"
:tab="tab" :tab="tab"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
@@ -409,6 +421,7 @@
<WorkspaceTabPropsRoutine <WorkspaceTabPropsRoutine
v-else-if="['temp-routine-props', 'routine-props'].includes(tab.type)" v-else-if="['temp-routine-props', 'routine-props'].includes(tab.type)"
:key="tab.uid" :key="tab.uid"
:tab-uid="tab.uid"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
:routine="tab.elementName" :routine="tab.elementName"
@@ -417,6 +430,7 @@
<WorkspaceTabNewFunction <WorkspaceTabNewFunction
v-else-if="tab.type === 'new-function'" v-else-if="tab.type === 'new-function'"
:key="tab.uid" :key="tab.uid"
:tab-uid="tab.uid"
:tab="tab" :tab="tab"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
@@ -426,6 +440,7 @@
<WorkspaceTabPropsFunction <WorkspaceTabPropsFunction
v-else-if="['temp-function-props', 'function-props'].includes(tab.type)" v-else-if="['temp-function-props', 'function-props'].includes(tab.type)"
:key="tab.uid" :key="tab.uid"
:tab-uid="tab.uid"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
:function="tab.elementName" :function="tab.elementName"
@@ -434,6 +449,7 @@
<WorkspaceTabNewScheduler <WorkspaceTabNewScheduler
v-else-if="tab.type === 'new-scheduler'" v-else-if="tab.type === 'new-scheduler'"
:key="tab.uid" :key="tab.uid"
:tab-uid="tab.uid"
:tab="tab" :tab="tab"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
@@ -443,6 +459,7 @@
<WorkspaceTabPropsScheduler <WorkspaceTabPropsScheduler
v-else-if="['temp-scheduler-props', 'scheduler-props'].includes(tab.type)" v-else-if="['temp-scheduler-props', 'scheduler-props'].includes(tab.type)"
:key="tab.uid" :key="tab.uid"
:tab-uid="tab.uid"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
:scheduler="tab.elementName" :scheduler="tab.elementName"
@@ -468,9 +485,11 @@
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { storeToRefs } from 'pinia';
import Draggable from 'vuedraggable'; import Draggable from 'vuedraggable';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import { useWorkspacesStore } from '@/stores/workspaces';
import WorkspaceEmptyState from '@/components/WorkspaceEmptyState'; import WorkspaceEmptyState from '@/components/WorkspaceEmptyState';
import WorkspaceExploreBar from '@/components/WorkspaceExploreBar'; import WorkspaceExploreBar from '@/components/WorkspaceExploreBar';
import WorkspaceEditConnectionPanel from '@/components/WorkspaceEditConnectionPanel'; import WorkspaceEditConnectionPanel from '@/components/WorkspaceEditConnectionPanel';
@@ -521,18 +540,37 @@ export default {
ModalProcessesList, ModalProcessesList,
ModalDiscardChanges 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: { props: {
connection: Object connection: Object
}, },
setup () {
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const {
getWorkspace,
addWorkspace,
connectWorkspace,
removeConnected,
selectTab,
newTab,
removeTab,
updateTabs
} = workspacesStore;
return {
selectedWorkspace,
getWorkspace,
addWorkspace,
connectWorkspace,
removeConnected,
selectTab,
newTab,
removeTab,
updateTabs
};
},
data () { data () {
return { return {
hasWheelEvent: false, hasWheelEvent: false,
@@ -541,10 +579,6 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
}, },
@@ -583,19 +617,25 @@ export default {
return false; return false;
}, },
hasTools () { hasTools () {
return this.workspace.customizations.processesList || if (!this.workspace.customizations) return false;
else {
return this.workspace.customizations.processesList ||
this.workspace.customizations.usersManagement || this.workspace.customizations.usersManagement ||
this.workspace.customizations.variables; this.workspace.customizations.variables;
}
} }
}, },
watch: { watch: {
queryTabs: function (newVal, oldVal) { queryTabs: {
if (newVal.length > oldVal.length) { handler (newVal, oldVal) {
setTimeout(() => { if (newVal.length > oldVal.length) {
const scroller = this.$refs.tabWrap; setTimeout(() => {
if (scroller) scroller.$el.scrollLeft = scroller.$el.scrollWidth; const scroller = this.$refs.tabWrap;
}, 0); if (scroller) scroller.$el.scrollLeft = scroller.$el.scrollWidth;
} }, 0);
}
},
deep: true
} }
}, },
async created () { async created () {
@@ -605,19 +645,10 @@ export default {
if (isInitiated) if (isInitiated)
this.connectWorkspace(this.connection); this.connectWorkspace(this.connection);
}, },
beforeDestroy () { beforeUnmount () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
}, },
methods: { methods: {
...mapActions({
addWorkspace: 'workspaces/addWorkspace',
connectWorkspace: 'workspaces/connectWorkspace',
removeConnected: 'workspaces/removeConnected',
selectTab: 'workspaces/selectTab',
newTab: 'workspaces/newTab',
removeTab: 'workspaces/removeTab',
updateTabs: 'workspaces/updateTabs'
}),
addQueryTab () { addQueryTab () {
this.newTab({ uid: this.connection.uid, type: 'query' }); this.newTab({ uid: this.connection.uid, type: 'query' });
}, },
@@ -683,6 +714,13 @@ export default {
}); });
this.hasWheelEvent = true; this.hasWheelEvent = true;
} }
},
cutText (string) {
const limit = 20;
const escapedString = string.replace(/\s{2,}/g, ' ');
if (escapedString.length > limit)
return `${escapedString.substr(0, limit)}...`;
return escapedString;
} }
} }
}; };
@@ -809,48 +847,5 @@ export default {
} }
} }
} }
.workspace-query-results {
overflow: auto;
white-space: nowrap;
.table {
width: auto;
border-collapse: separate;
.th {
position: sticky;
top: 0;
border: 2px solid;
border-left: none;
border-bottom-width: 2px;
padding: 0;
font-weight: 700;
font-size: 0.7rem;
z-index: 1;
> div {
padding: 0.1rem 0.2rem;
min-width: -webkit-fill-available;
}
}
.td {
border-right: 2px solid;
border-bottom: 2px solid;
padding: 0 0.2rem;
text-overflow: ellipsis;
max-width: 200px;
white-space: nowrap;
overflow: hidden;
font-size: 0.7rem;
position: relative;
&:focus {
outline: none;
}
}
}
}
} }
</style> </style>

View File

@@ -50,7 +50,11 @@
<label class="form-label cut-text">{{ $t('word.client') }}</label> <label class="form-label cut-text">{{ $t('word.client') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<select v-model="connection.client" class="form-select"> <select
id="connection-client"
v-model="connection.client"
class="form-select"
>
<option <option
v-for="client in clients" v-for="client in clients"
:key="client.slug" :key="client.slug"
@@ -92,7 +96,7 @@
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="connection.databasePath" :model-value="connection.databasePath"
:message="$t('word.browse')" :message="$t('word.browse')"
@clear="pathClear('databasePath')" @clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')" @change="pathSelection($event, 'databasePath')"
@@ -207,7 +211,7 @@
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="connection.key" :model-value="connection.key"
:message="$t('word.browse')" :message="$t('word.browse')"
@clear="pathClear('key')" @clear="pathClear('key')"
@change="pathSelection($event, 'key')" @change="pathSelection($event, 'key')"
@@ -220,7 +224,7 @@
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="connection.cert" :model-value="connection.cert"
:message="$t('word.browse')" :message="$t('word.browse')"
@clear="pathClear('cert')" @clear="pathClear('cert')"
@change="pathSelection($event, 'cert')" @change="pathSelection($event, 'cert')"
@@ -233,7 +237,7 @@
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="connection.ca" :model-value="connection.ca"
:message="$t('word.browse')" :message="$t('word.browse')"
@clear="pathClear('ca')" @clear="pathClear('ca')"
@change="pathSelection($event, 'ca')" @change="pathSelection($event, 'ca')"
@@ -338,7 +342,7 @@
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="connection.sshKey" :model-value="connection.sshKey"
:message="$t('word.browse')" :message="$t('word.browse')"
@clear="pathClear('sshKey')" @clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')" @change="pathSelection($event, 'sshKey')"
@@ -363,6 +367,7 @@
</div> </div>
<div class="panel-footer"> <div class="panel-footer">
<button <button
id="connection-test"
class="btn btn-gray mr-2 d-flex" class="btn btn-gray mr-2 d-flex"
:class="{'loading': isTesting}" :class="{'loading': isTesting}"
:disabled="isBusy" :disabled="isBusy"
@@ -372,6 +377,7 @@
{{ $t('message.testConnection') }} {{ $t('message.testConnection') }}
</button> </button>
<button <button
id="connection-save"
class="btn btn-primary mr-2 d-flex" class="btn btn-primary mr-2 d-flex"
:disabled="isBusy" :disabled="isBusy"
@click="saveConnection" @click="saveConnection"
@@ -390,10 +396,12 @@
</template> </template>
<script> <script>
import { mapActions } from 'vuex';
import customizations from 'common/customizations'; import customizations from 'common/customizations';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import { useConnectionsStore } from '@/stores/connections';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import ModalAskCredentials from '@/components/ModalAskCredentials'; import ModalAskCredentials from '@/components/ModalAskCredentials';
import BaseUploadInput from '@/components/BaseUploadInput'; import BaseUploadInput from '@/components/BaseUploadInput';
@@ -403,6 +411,20 @@ export default {
ModalAskCredentials, ModalAskCredentials,
BaseUploadInput BaseUploadInput
}, },
setup () {
const { addConnection } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { connectWorkspace, selectWorkspace } = workspacesStore;
return {
addConnection,
addNotification,
connectWorkspace,
selectWorkspace
};
},
data () { data () {
return { return {
clients: [ clients: [
@@ -466,12 +488,6 @@ export default {
}, 20); }, 20);
}, },
methods: { methods: {
...mapActions({
addConnection: 'connections/addConnection',
connectWorkspace: 'workspaces/connectWorkspace',
addNotification: 'notifications/addNotification',
selectWorkspace: 'workspaces/selectWorkspace'
}),
setDefaults () { setDefaults () {
this.connection.user = this.customizations.defaultUser; this.connection.user = this.customizations.defaultUser;
this.connection.port = this.customizations.defaultPort; this.connection.port = this.customizations.defaultPort;

View File

@@ -92,7 +92,7 @@
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="localConnection.databasePath" :model-value="localConnection.databasePath"
:message="$t('word.browse')" :message="$t('word.browse')"
@clear="pathClear('databasePath')" @clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')" @change="pathSelection($event, 'databasePath')"
@@ -207,7 +207,7 @@
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="localConnection.key" :model-value="localConnection.key"
:message="$t('word.browse')" :message="$t('word.browse')"
@clear="pathClear('key')" @clear="pathClear('key')"
@change="pathSelection($event, 'key')" @change="pathSelection($event, 'key')"
@@ -220,7 +220,7 @@
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="localConnection.cert" :model-value="localConnection.cert"
:message="$t('word.browse')" :message="$t('word.browse')"
@clear="pathClear('cert')" @clear="pathClear('cert')"
@change="pathSelection($event, 'cert')" @change="pathSelection($event, 'cert')"
@@ -233,7 +233,7 @@
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="localConnection.ca" :model-value="localConnection.ca"
:message="$t('word.browse')" :message="$t('word.browse')"
@clear="pathClear('ca')" @clear="pathClear('ca')"
@change="pathSelection($event, 'ca')" @change="pathSelection($event, 'ca')"
@@ -253,14 +253,6 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns">
<div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="localConnection.untrustedConnection" type="checkbox"><i class="form-icon" /> {{ $t('message.untrustedConnection') }}
</label>
</div>
</div>
</fieldset> </fieldset>
</form> </form>
</div> </div>
@@ -338,7 +330,7 @@
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="localConnection.sshKey" :model-value="localConnection.sshKey"
:message="$t('word.browse')" :message="$t('word.browse')"
@clear="pathClear('sshKey')" @clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')" @change="pathSelection($event, 'sshKey')"
@@ -363,6 +355,7 @@
</div> </div>
<div class="panel-footer"> <div class="panel-footer">
<button <button
id="connection-test"
class="btn btn-gray mr-2 d-flex" class="btn btn-gray mr-2 d-flex"
:class="{'loading': isTesting}" :class="{'loading': isTesting}"
:disabled="isBusy" :disabled="isBusy"
@@ -372,6 +365,7 @@
{{ $t('message.testConnection') }} {{ $t('message.testConnection') }}
</button> </button>
<button <button
id="connection-save"
class="btn btn-primary mr-2 d-flex" class="btn btn-primary mr-2 d-flex"
:disabled="isBusy || !hasChanges" :disabled="isBusy || !hasChanges"
@click="saveConnection" @click="saveConnection"
@@ -380,6 +374,7 @@
{{ $t('word.save') }} {{ $t('word.save') }}
</button> </button>
<button <button
id="connection-connect"
class="btn btn-success d-flex" class="btn btn-success d-flex"
:class="{'loading': isConnecting}" :class="{'loading': isConnecting}"
:disabled="isBusy" :disabled="isBusy"
@@ -399,8 +394,10 @@
</template> </template>
<script> <script>
import { mapActions } from 'vuex';
import customizations from 'common/customizations'; import customizations from 'common/customizations';
import { useConnectionsStore } from '@/stores/connections';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import ModalAskCredentials from '@/components/ModalAskCredentials'; import ModalAskCredentials from '@/components/ModalAskCredentials';
import BaseUploadInput from '@/components/BaseUploadInput'; import BaseUploadInput from '@/components/BaseUploadInput';
@@ -414,6 +411,17 @@ export default {
props: { props: {
connection: Object connection: Object
}, },
setup () {
const { editConnection } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const { connectWorkspace } = useWorkspacesStore();
return {
editConnection,
addNotification,
connectWorkspace
};
},
data () { data () {
return { return {
clients: [ clients: [
@@ -449,11 +457,6 @@ export default {
this.localConnection = JSON.parse(JSON.stringify(this.connection)); this.localConnection = JSON.parse(JSON.stringify(this.connection));
}, },
methods: { methods: {
...mapActions({
editConnection: 'connections/editConnection',
connectWorkspace: 'workspaces/connectWorkspace',
addNotification: 'notifications/addNotification'
}),
async startConnection () { async startConnection () {
await this.saveConnection(); await this.saveConnection();
this.isConnecting = true; this.isConnecting = true;

View File

@@ -25,26 +25,35 @@
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces';
import { storeToRefs } from 'pinia';
export default { export default {
name: 'WorkspaceEmptyState', name: 'WorkspaceEmptyState',
emits: ['new-tab'],
setup () {
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
const { applicationTheme } = storeToRefs(settingsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, changeBreadcrumbs } = workspacesStore;
return {
applicationTheme,
selectedWorkspace,
getWorkspace,
changeBreadcrumbs
};
},
computed: { computed: {
...mapGetters({
applicationTheme: 'settings/getApplicationTheme',
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected'
}),
workspace () { workspace () {
return this.getWorkspace(this.selectedWorkspace); return this.getWorkspace(this.selectedWorkspace);
} }
}, },
created () { created () {
this.changeBreadcrumbs({ schema: this.workspace.breadcrumbs.schema }); this.changeBreadcrumbs({ schema: this.workspace.breadcrumbs.schema });
},
methods: {
...mapActions({
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
})
} }
}; };
</script> </script>

View File

@@ -52,6 +52,7 @@
<WorkspaceExploreBarSchema <WorkspaceExploreBarSchema
v-for="db of workspace.structure" v-for="db of workspace.structure"
:key="db.name" :key="db.name"
ref="schema"
:database="db" :database="db"
:connection="connection" :connection="connection"
@show-schema-context="openSchemaContext" @show-schema-context="openSchemaContext"
@@ -115,7 +116,12 @@
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { storeToRefs } from 'pinia';
import { useConnectionsStore } from '@/stores/connections';
import { useNotificationsStore } from '@/stores/notifications';
import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import Views from '@/ipc-api/Views'; import Views from '@/ipc-api/Views';
@@ -143,6 +149,45 @@ export default {
connection: Object, connection: Object,
isSelected: Boolean isSelected: Boolean
}, },
setup () {
const { getConnectionName } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
const { explorebarSize } = storeToRefs(settingsStore);
const { changeExplorebarSize } = settingsStore;
const {
getWorkspace,
removeConnected: disconnectWorkspace,
refreshStructure,
changeBreadcrumbs,
selectTab,
newTab,
removeTabs,
setSearchTerm,
addLoadingElement,
removeLoadingElement
} = workspacesStore;
return {
getConnectionName,
addNotification,
explorebarSize,
changeExplorebarSize,
getWorkspace,
disconnectWorkspace,
refreshStructure,
changeBreadcrumbs,
selectTab,
newTab,
removeTabs,
setSearchTerm,
addLoadingElement,
removeLoadingElement
};
},
data () { data () {
return { return {
isRefreshing: false, isRefreshing: false,
@@ -174,11 +219,6 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace',
explorebarSize: 'settings/getExplorebarSize',
getConnectionName: 'connections/getConnectionName'
}),
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
}, },
@@ -220,21 +260,13 @@ export default {
window.addEventListener('mousemove', this.resize); window.addEventListener('mousemove', this.resize);
window.addEventListener('mouseup', this.stopResize); window.addEventListener('mouseup', this.stopResize);
}); });
if (this.workspace.structure.length === 1) { // Auto-open if juust one schema
this.$refs.schema[0].selectSchema(this.workspace.structure[0].name);
this.$refs.schema[0].$refs.schemaAccordion.open = true;
}
}, },
methods: { methods: {
...mapActions({
disconnectWorkspace: 'workspaces/removeConnected',
refreshStructure: 'workspaces/refreshStructure',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
selectTab: 'workspaces/selectTab',
newTab: 'workspaces/newTab',
removeTabs: 'workspaces/removeTabs',
setSearchTerm: 'workspaces/setSearchTerm',
addNotification: 'notifications/addNotification',
changeExplorebarSize: 'settings/changeExplorebarSize',
addLoadingElement: 'workspaces/addLoadingElement',
removeLoadingElement: 'workspaces/removeLoadingElement'
}),
async refresh () { async refresh () {
if (!this.isRefreshing) { if (!this.isRefreshing) {
this.isRefreshing = true; this.isRefreshing = true;

View File

@@ -65,7 +65,8 @@
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
import ModalAskParameters from '@/components/ModalAskParameters'; import ModalAskParameters from '@/components/ModalAskParameters';
@@ -73,6 +74,7 @@ import Triggers from '@/ipc-api/Triggers';
import Routines from '@/ipc-api/Routines'; import Routines from '@/ipc-api/Routines';
import Functions from '@/ipc-api/Functions'; import Functions from '@/ipc-api/Functions';
import Schedulers from '@/ipc-api/Schedulers'; import Schedulers from '@/ipc-api/Schedulers';
import { storeToRefs } from 'pinia';
export default { export default {
name: 'WorkspaceExploreBarMiscContext', name: 'WorkspaceExploreBarMiscContext',
@@ -86,6 +88,33 @@ export default {
selectedMisc: Object, selectedMisc: Object,
selectedSchema: String selectedSchema: String
}, },
emits: ['close-context', 'reload'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const {
getWorkspace,
changeBreadcrumbs,
addLoadingElement,
removeLoadingElement,
removeTabs,
newTab
} = workspacesStore;
return {
addNotification,
selectedWorkspace,
getWorkspace,
changeBreadcrumbs,
addLoadingElement,
removeLoadingElement,
removeTabs,
newTab
};
},
data () { data () {
return { return {
isDeleteModal: false, isDeleteModal: false,
@@ -95,10 +124,6 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () { workspace () {
return this.getWorkspace(this.selectedWorkspace); return this.getWorkspace(this.selectedWorkspace);
}, },
@@ -122,14 +147,6 @@ export default {
} }
}, },
methods: { methods: {
...mapActions({
addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
addLoadingElement: 'workspaces/addLoadingElement',
removeLoadingElement: 'workspaces/removeLoadingElement',
removeTabs: 'workspaces/removeTabs',
newTab: 'workspaces/newTab'
}),
showDeleteModal () { showDeleteModal () {
this.isDeleteModal = true; this.isDeleteModal = true;
}, },

View File

@@ -42,8 +42,10 @@
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu';
import { storeToRefs } from 'pinia';
export default { export default {
name: 'WorkspaceExploreBarMiscContext', name: 'WorkspaceExploreBarMiscContext',
@@ -55,25 +57,40 @@ export default {
selectedMisc: String, selectedMisc: String,
selectedSchema: String selectedSchema: String
}, },
emits: [
'open-create-trigger-tab',
'open-create-routine-tab',
'open-create-function-tab',
'open-create-trigger-function-tab',
'open-create-scheduler-tab',
'close-context'
],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, changeBreadcrumbs } = workspacesStore;
return {
addNotification,
selectedWorkspace,
getWorkspace,
changeBreadcrumbs
};
},
data () { data () {
return { return {
localElement: {} localElement: {}
}; };
}, },
computed: { computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () { workspace () {
return this.getWorkspace(this.selectedWorkspace); return this.getWorkspace(this.selectedWorkspace);
} }
}, },
methods: { methods: {
...mapActions({
addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
showDeleteModal () { showDeleteModal () {
this.isDeleteModal = true; this.isDeleteModal = true;
}, },

View File

@@ -1,5 +1,5 @@
<template> <template>
<details class="accordion workspace-explorebar-database"> <details ref="schemaAccordion" class="accordion workspace-explorebar-database">
<summary <summary
class="accordion-header database-name" class="accordion-header database-name"
:class="{'text-bold': breadcrumbs.schema === database.name}" :class="{'text-bold': breadcrumbs.schema === database.name}"
@@ -243,8 +243,10 @@
</template> </template>
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces';
import { formatBytes } from 'common/libs/formatBytes'; import { formatBytes } from 'common/libs/formatBytes';
import { storeToRefs } from 'pinia';
export default { export default {
name: 'WorkspaceExploreBarSchema', name: 'WorkspaceExploreBarSchema',
@@ -252,18 +254,45 @@ export default {
database: Object, database: Object,
connection: Object connection: Object
}, },
emits: [
'show-schema-context',
'show-table-context',
'show-misc-context',
'show-misc-folder-context'
],
setup () {
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
const { applicationTheme } = storeToRefs(settingsStore);
const {
getLoadedSchemas,
getWorkspace,
getSearchTerm,
changeBreadcrumbs,
addLoadedSchema,
newTab,
refreshSchema
} = workspacesStore;
return {
applicationTheme,
getLoadedSchemas,
getWorkspace,
getSearchTerm,
changeBreadcrumbs,
addLoadedSchema,
newTab,
refreshSchema
};
},
data () { data () {
return { return {
isLoading: false isLoading: false
}; };
}, },
computed: { computed: {
...mapGetters({
getLoadedSchemas: 'workspaces/getLoadedSchemas',
getWorkspace: 'workspaces/getWorkspace',
getSearchTerm: 'workspaces/getSearchTerm',
applicationTheme: 'settings/getApplicationTheme'
}),
searchTerm () { searchTerm () {
return this.getSearchTerm(this.connection.uid); return this.getSearchTerm(this.connection.uid);
}, },
@@ -331,12 +360,6 @@ export default {
} }
}, },
methods: { methods: {
...mapActions({
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
addLoadedSchema: 'workspaces/addLoadedSchema',
newTab: 'workspaces/newTab',
refreshSchema: 'workspaces/refreshSchema'
}),
formatBytes, formatBytes,
async selectSchema (schema) { async selectSchema (schema) {
if (!this.loadedSchemas.has(schema) && !this.isLoading) { if (!this.loadedSchemas.has(schema) && !this.isLoading) {

View File

@@ -124,7 +124,8 @@
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
import ModalEditSchema from '@/components/ModalEditSchema'; import ModalEditSchema from '@/components/ModalEditSchema';
@@ -132,6 +133,7 @@ import ModalExportSchema from '@/components/ModalExportSchema';
import ModalImportSchema from '@/components/ModalImportSchema'; import ModalImportSchema from '@/components/ModalImportSchema';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import Application from '@/ipc-api/Application'; import Application from '@/ipc-api/Application';
import { storeToRefs } from 'pinia';
export default { export default {
name: 'WorkspaceExploreBarSchemaContext', name: 'WorkspaceExploreBarSchemaContext',
@@ -146,6 +148,35 @@ export default {
contextEvent: MouseEvent, contextEvent: MouseEvent,
selectedSchema: String selectedSchema: String
}, },
emits: [
'open-create-table-tab',
'open-create-view-tab',
'open-create-trigger-tab',
'open-create-routine-tab',
'open-create-function-tab',
'open-create-trigger-function-tab',
'open-create-scheduler-tab',
'close-context',
'reload'
],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const {
getWorkspace,
changeBreadcrumbs
} = workspacesStore;
return {
addNotification,
selectedWorkspace,
getWorkspace,
changeBreadcrumbs
};
},
data () { data () {
return { return {
isDeleteModal: false, isDeleteModal: false,
@@ -155,19 +186,11 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () { workspace () {
return this.getWorkspace(this.selectedWorkspace); return this.getWorkspace(this.selectedWorkspace);
} }
}, },
methods: { methods: {
...mapActions({
addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
openCreateTableTab () { openCreateTableTab () {
this.$emit('open-create-table-tab'); this.$emit('open-create-table-tab');
}, },

View File

@@ -72,10 +72,12 @@
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import { storeToRefs } from 'pinia';
export default { export default {
name: 'WorkspaceExploreBarTableContext', name: 'WorkspaceExploreBarTableContext',
@@ -88,6 +90,33 @@ export default {
selectedTable: Object, selectedTable: Object,
selectedSchema: String selectedSchema: String
}, },
emits: ['close-context', 'duplicate-table', 'reload', 'delete-table'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const {
getWorkspace,
newTab,
removeTabs,
addLoadingElement,
removeLoadingElement,
changeBreadcrumbs
} = workspacesStore;
return {
addNotification,
getWorkspace,
newTab,
removeTabs,
addLoadingElement,
removeLoadingElement,
changeBreadcrumbs,
selectedWorkspace
};
},
data () { data () {
return { return {
isDeleteModal: false, isDeleteModal: false,
@@ -95,10 +124,6 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () { workspace () {
return this.getWorkspace(this.selectedWorkspace); return this.getWorkspace(this.selectedWorkspace);
}, },
@@ -107,14 +132,6 @@ export default {
} }
}, },
methods: { methods: {
...mapActions({
addNotification: 'notifications/addNotification',
newTab: 'workspaces/newTab',
removeTabs: 'workspaces/removeTabs',
addLoadingElement: 'workspaces/addLoadingElement',
removeLoadingElement: 'workspaces/removeLoadingElement',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
showDeleteModal () { showDeleteModal () {
this.isDeleteModal = true; this.isDeleteModal = true;
}, },

View File

@@ -185,7 +185,7 @@
<QueryEditor <QueryEditor
v-show="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
:value.sync="localFunction.sql" v-model="localFunction.sql"
:workspace="workspace" :workspace="workspace"
:schema="schema" :schema="schema"
:height="editorHeight" :height="editorHeight"
@@ -203,11 +203,13 @@
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor';
import WorkspaceTabPropsFunctionParamsModal from '@/components/WorkspaceTabPropsFunctionParamsModal'; import WorkspaceTabPropsFunctionParamsModal from '@/components/WorkspaceTabPropsFunctionParamsModal';
import Functions from '@/ipc-api/Functions'; import Functions from '@/ipc-api/Functions';
import { storeToRefs } from 'pinia';
export default { export default {
name: 'WorkspaceTabNewFunction', name: 'WorkspaceTabNewFunction',
@@ -217,11 +219,40 @@ export default {
WorkspaceTabPropsFunctionParamsModal WorkspaceTabPropsFunctionParamsModal
}, },
props: { props: {
tabUid: String,
connection: Object, connection: Object,
tab: Object, tab: Object,
isSelected: Boolean, isSelected: Boolean,
schema: String schema: String
}, },
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const {
getWorkspace,
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
removeTab,
renameTabs
} = workspacesStore;
return {
addNotification,
selectedWorkspace,
getWorkspace,
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
removeTab,
renameTabs
};
},
data () { data () {
return { return {
isLoading: false, isLoading: false,
@@ -235,19 +266,12 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
}, },
customizations () { customizations () {
return this.workspace.customizations; return this.workspace.customizations;
}, },
tabUid () {
return this.$vnode.key;
},
isChanged () { isChanged () {
return JSON.stringify(this.originalFunction) !== JSON.stringify(this.localFunction); return JSON.stringify(this.originalFunction) !== JSON.stringify(this.localFunction);
}, },
@@ -302,22 +326,13 @@ export default {
this.$refs.firstInput.focus(); this.$refs.firstInput.focus();
}, 100); }, 100);
}, },
destroyed () { unmounted () {
window.removeEventListener('resize', this.resizeQueryEditor); window.removeEventListener('resize', this.resizeQueryEditor);
}, },
beforeDestroy () { beforeUnmount () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
}, },
methods: { 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 () { async saveChanges () {
if (this.isSaving) return; if (this.isSaving) return;
this.isSaving = true; this.isSaving = true;

View File

@@ -145,7 +145,7 @@
v-show="isSelected" v-show="isSelected"
:key="`new-${_uid}`" :key="`new-${_uid}`"
ref="queryEditor" ref="queryEditor"
:value.sync="localRoutine.sql" v-model="localRoutine.sql"
:workspace="workspace" :workspace="workspace"
:schema="schema" :schema="schema"
:height="editorHeight" :height="editorHeight"
@@ -163,11 +163,13 @@
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader';
import WorkspaceTabPropsRoutineParamsModal from '@/components/WorkspaceTabPropsRoutineParamsModal'; import WorkspaceTabPropsRoutineParamsModal from '@/components/WorkspaceTabPropsRoutineParamsModal';
import Routines from '@/ipc-api/Routines'; import Routines from '@/ipc-api/Routines';
import { storeToRefs } from 'pinia';
export default { export default {
name: 'WorkspaceTabNewRoutine', name: 'WorkspaceTabNewRoutine',
@@ -177,11 +179,40 @@ export default {
WorkspaceTabPropsRoutineParamsModal WorkspaceTabPropsRoutineParamsModal
}, },
props: { props: {
tabUid: String,
connection: Object, connection: Object,
tab: Object, tab: Object,
isSelected: Boolean, isSelected: Boolean,
schema: String schema: String
}, },
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const {
getWorkspace,
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
removeTab,
renameTabs
} = workspacesStore;
return {
addNotification,
selectedWorkspace,
getWorkspace,
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
removeTab,
renameTabs
};
},
data () { data () {
return { return {
isLoading: false, isLoading: false,
@@ -195,19 +226,12 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
}, },
customizations () { customizations () {
return this.workspace.customizations; return this.workspace.customizations;
}, },
tabUid () {
return this.$vnode.key;
},
isChanged () { isChanged () {
return JSON.stringify(this.originalRoutine) !== JSON.stringify(this.localRoutine); return JSON.stringify(this.originalRoutine) !== JSON.stringify(this.localRoutine);
}, },
@@ -261,22 +285,13 @@ export default {
window.addEventListener('resize', this.resizeQueryEditor); window.addEventListener('resize', this.resizeQueryEditor);
}, },
destroyed () { unmounted () {
window.removeEventListener('resize', this.resizeQueryEditor); window.removeEventListener('resize', this.resizeQueryEditor);
}, },
beforeDestroy () { beforeUnmount () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
}, },
methods: { 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 () { async saveChanges () {
if (this.isSaving) return; if (this.isSaving) return;
this.isSaving = true; this.isSaving = true;

Some files were not shown because too many files have changed in this diff Show More