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

Compare commits

...

140 Commits

Author SHA1 Message Date
d743d486b8 chore(release): 0.5.5 2022-05-24 14:21:56 +02:00
6c8a36e947 refactor(UI): double click to edit field type or collation 2022-05-23 16:35:50 +02:00
99b1c1be12 Merge pull request #245 from toriphes/feat/advanced-dropdown-list
feat/advanced-dropdown-list
2022-05-23 16:31:19 +02:00
3991382153 Merge branch 'feat/advanced-dropdown-list' of https://github.com/toriphes/antares into pr/toriphes/245 2022-05-23 16:19:35 +02:00
allcontributors[bot]
3b57b7ef3b docs: update .all-contributorsrc [skip ci] 2022-05-23 16:13:51 +02:00
allcontributors[bot]
45d599ad7f docs: update README.md [skip ci] 2022-05-23 16:13:51 +02:00
9082960310 feat(translation): russian translation, closes #266 2022-05-23 16:11:54 +02:00
Giulio Ganci
5398964190 feat: added dropdown animation 2022-05-22 15:45:16 +02:00
5d5f1da97b perf(UI): max height for query text area increased 2022-05-19 09:30:43 +02:00
c95c593c74 chore: create CODE_OF_CONDUCT.md 2022-05-15 19:08:06 +02:00
c5baf2b0d3 fix: query tab content disappears reordering or closing other tabs, closes #261 2022-05-15 18:15:05 +02:00
a082514f88 fix(PostgreSQL): idle timeout disabled 2022-05-15 17:37:54 +02:00
c826888b0d fix: SSH tunnel connection error with private key, closes #260 2022-05-14 11:24:24 +02:00
b0d464952f Merge branch 'master' of https://github.com/antares-sql/antares into pr/toriphes/245 2022-05-14 09:57:09 +02:00
Giulio Ganci
7c45203636 fix(UI): BaseSelect keyboard navigation 2022-05-13 18:35:02 +02:00
Giulio Ganci
71b0736d0d fix(UI): BaseSelect style 2022-05-13 18:20:47 +02:00
Giulio Ganci
42bc9196ff feat(UI): select tab replace with BaseSelect component 2022-05-11 23:30:31 +02:00
Giulio Ganci
f7e04d6333 feat(UI): BaseSelect supports disabled options 2022-05-11 22:57:13 +02:00
9ee1b3023d build: electron-updater downgrade [skip ci] 2022-05-10 19:16:06 +02:00
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
Giulio Ganci
2b436d8613 feat(UI): BaseSelect disabled state 2022-05-10 10:29:38 +02:00
Giulio Ganci
1869e6a148 feat(UI): BaseSelect supports option groups 2022-05-09 17:31:58 +02:00
Giulio Ganci
302c66457d feat(UI): ForeignKeySelect implements BaseSelect component 2022-05-09 11:29:25 +02:00
Giulio Ganci
0043d07708 feat(UI): BaseSelect option list scrolls automatically using up/down keys 2022-05-08 18:59:00 +02:00
Ngô Quốc Đạt
e0f85f469f Added missing translations for vi-VN 2022-05-08 16:29:48 +02:00
Giulio Ganci
a037d0cc01 feat(UI): BaseSelect in table filters 2022-05-08 13:15:39 +02:00
Giulio Ganci
5582a12bbf feat(UI): BaseSelect small variant 2022-05-08 13:14:40 +02:00
Giulio Ganci
22622df2cf feat(UI): initial BaseSelect integration 2022-05-08 09:45:37 +02:00
Giulio Ganci
745d551cc9 feat(UI): new BaseSelect component 2022-05-08 09:44:52 +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
0b861d962d chore(release): 0.5.2 2022-04-10 10:55:23 +02:00
6cc098c6f0 feat(core): option to allow untrusted SSL connections 2022-04-10 10:49:27 +02:00
40828cb3ff Merge pull request #208 from antares-sql/feat/postgre-import-export
feat(PostgreSQL): import/export
2022-04-07 17:56:37 +02:00
6086ca4a80 feat(PostgreSQL): sql dump importer 2022-04-07 12:49:34 +02:00
d92facf518 chore: update README.md 2022-04-05 14:06:36 +02:00
allcontributors[bot]
e9643a0d6b docs: update .all-contributorsrc [skip ci] 2022-04-04 10:32:23 +02:00
allcontributors[bot]
cc609a7051 docs: update README.md [skip ci] 2022-04-04 10:32:23 +02:00
796f61bf2f feat: french translation updated, closes #222 2022-04-04 10:29:11 +02:00
0f9c991f53 fix(PostgreSQL): wrong values exporting table content 2022-04-02 11:44:55 +02:00
638a88a1fb perf(PostgreSQL): improved views exportation 2022-04-01 18:36:02 +02:00
026d74c8c8 fix: ssh tunnel not properly working, closes #220 2022-04-01 09:51:03 +02:00
77f8cac6cf refactor: update repo links 2022-03-31 16:04:47 +02:00
d6fadf5db0 chore: update README.md 2022-03-29 15:47:50 +02:00
408ddeda56 perf(PostgreSQL): improved dump file 2022-03-27 11:41:35 +02:00
88cd097ec0 chore(release): 0.5.1 2022-03-25 09:26:04 +01:00
f12e6a96dd fix(UI): connection buttons out of screen on small displays, closes #213 2022-03-24 17:34:24 +01:00
a8ca8f2f76 feat(PostgreSQL): export functions and procedures 2022-03-23 13:26:46 +01:00
allcontributors[bot]
6fcae957e1 docs: update .all-contributorsrc [skip ci] 2022-03-22 21:38:32 +01:00
allcontributors[bot]
1cd2d8abf3 docs: update README.md [skip ci] 2022-03-22 21:38:32 +01:00
ed3c5fe559 Merge pull request #210 from SmileYzn/patch-1
Added some missing translations for pt-BR
2022-03-22 21:36:52 +01:00
Cleverson
9601c59392 Update pt-BR.js 2022-03-22 14:38:24 -03:00
Cleverson
14d20a30c1 Added some missing translations for pt-BR
Added some translations for pt-BR
2022-03-22 14:29:33 -03:00
42376b4bc6 feat(PostgreSQL): export triggers 2022-03-22 17:25:34 +01:00
e9079adb25 feat(UI): option to disable blur effects, closes #209 2022-03-22 16:13:44 +01:00
86f011f34f feat(PostgreSQL): export views 2022-03-22 12:59:13 +01:00
bb02479b71 feat(PostgreSQL): export user-defined types before tables 2022-03-22 12:40:14 +01:00
a67071e284 feat(PostgreSQL): export tables 2022-03-21 18:32:45 +01:00
8f3efabb69 feat: export database as zip sql file 2022-03-18 18:12:13 +01:00
db628f7722 fix: numeric scale displayed on non decimal fields 2022-03-17 18:17:59 +01:00
e4bd747381 chore: update bug_report.md 2022-03-15 18:39:46 +01:00
88eb113e53 chore: update README.md 2022-03-12 17:42:34 +01:00
186 changed files with 10550 additions and 7514 deletions

View File

@@ -1,6 +1,6 @@
{
"projectName": "antares",
"projectOwner": "Fabio286",
"projectOwner": "antares-sql",
"repoType": "github",
"repoHost": "https://github.com",
"files": [
@@ -147,6 +147,33 @@
"contributions": [
"code"
]
},
{
"login": "SmileYzn",
"name": "Cleverson",
"avatar_url": "https://avatars.githubusercontent.com/u/5851851?v=4",
"profile": "https://github.com/SmileYzn",
"contributions": [
"translation"
]
},
{
"login": "fredatgithub",
"name": "fred",
"avatar_url": "https://avatars.githubusercontent.com/u/6720055?v=4",
"profile": "https://github.com/fredatgithub",
"contributions": [
"translation"
]
},
{
"login": "xak666",
"name": "xaka_xak",
"avatar_url": "https://avatars.githubusercontent.com/u/38811437?v=4",
"profile": "https://github.com/xak666",
"contributions": [
"translation"
]
}
],
"contributorsPerLine": 7,

View File

@@ -6,15 +6,23 @@
},
"extends": [
"standard",
"plugin:vue/recommended"
"plugin:@typescript-eslint/recommended",
"plugin:vue/vue3-recommended"
],
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@babel/eslint-parser",
"parser": "@typescript-eslint/parser",
"ecmaVersion": 9,
"sourceType": "module",
"requireConfigFile": false
},
"plugins": [
"vue",
"@typescript-eslint"
],
"rules": {
"space-infix-ops": "off",
"object-curly-newline": "off",
"indent": [
"error",
3,
@@ -69,6 +77,20 @@
"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,13 +26,16 @@ If applicable, add screenshots to help explain your problem.
**Application (please complete the following information):**
- Version [e.g. 0.14.0]
- Distribution: [e.g. exe, Linux Store, AppImage, dmg]
- App client [e.g. MySQL]
- App version [e.g. 0.5.2]
- Installation source: [e.g. exe, Linux Store, AppImage, dmg]
**Desktop (please complete the following information):**
**Environment (please complete the following information):**
- OS: [e.g. iOS]
- Version [e.g. 22]
- OS name: [e.g. Windows 11]
- OS version [e.g. 21H2]
- DB name [e.g. MariaDB]
- DB version [e.g. 10.3.34]
**Additional context**
Add any other context about the problem here.

View File

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

View File

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

View File

@@ -22,9 +22,6 @@ jobs:
- name: Install dependencies
run: npm i
- name: Run tests
run: npm run test
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
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
dist
build
misc/*
!misc/.gitkeep
node_modules
thumbs.db
NOTES.md

2
.vscode/launch.json vendored
View File

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

View File

@@ -5,7 +5,8 @@
"MySQL",
"PostgreSQL",
"SQLite",
"Windows"
"Windows",
"translation"
],
"svg.preview.background": "transparent"
}

View File

@@ -2,6 +2,124 @@
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.5](https://github.com/antares-sql/antares/compare/v0.5.4...v0.5.5) (2022-05-24)
### Features
* added dropdown animation ([5398964](https://github.com/antares-sql/antares/commit/539896419064db9127f6a72acdbb11af2c4aa60a))
* **translation:** russian translation, closes [#266](https://github.com/antares-sql/antares/issues/266) ([9082960](https://github.com/antares-sql/antares/commit/9082960310573a6e4d14bfbe82ed2eb1489f308d))
* **UI:** BaseSelect disabled state ([2b436d8](https://github.com/antares-sql/antares/commit/2b436d8613a1e3dff55d73adbddf5d2cd2452f27))
* **UI:** BaseSelect in table filters ([a037d0c](https://github.com/antares-sql/antares/commit/a037d0cc0148444e8e6c5b87c79f6ba9c2a6f0fe))
* **UI:** BaseSelect option list scrolls automatically using up/down keys ([0043d07](https://github.com/antares-sql/antares/commit/0043d077081fc49724722a5d5a74986d990c539d))
* **UI:** BaseSelect small variant ([5582a12](https://github.com/antares-sql/antares/commit/5582a12bbfade75dbcc7f9d71ada7190ed08d3c2))
* **UI:** BaseSelect supports disabled options ([f7e04d6](https://github.com/antares-sql/antares/commit/f7e04d633340a53420ce1c434e906c9434620e6e))
* **UI:** BaseSelect supports option groups ([1869e6a](https://github.com/antares-sql/antares/commit/1869e6a1482daf9381d9ac2244bf0aeffa758edc))
* **UI:** ForeignKeySelect implements BaseSelect component ([302c664](https://github.com/antares-sql/antares/commit/302c66457deeb50facf4735291640fcf48b78f66))
* **UI:** initial BaseSelect integration ([22622df](https://github.com/antares-sql/antares/commit/22622df2cfcb71054c6f6110b7ad9d4f635553dc))
* **UI:** new BaseSelect component ([745d551](https://github.com/antares-sql/antares/commit/745d551cc9253eae4e39e5d3406ceee051a7d6c1))
* **UI:** select tab replace with BaseSelect component ([42bc919](https://github.com/antares-sql/antares/commit/42bc9196ffc2f64b77f9cb42136255fc74815034))
### Bug Fixes
* **PostgreSQL:** idle timeout disabled ([a082514](https://github.com/antares-sql/antares/commit/a082514f88040c7e0ffdf4e8357bab45370a4c39))
* query tab content disappears reordering or closing other tabs, closes [#261](https://github.com/antares-sql/antares/issues/261) ([c5baf2b](https://github.com/antares-sql/antares/commit/c5baf2b0d379fdd28ee8cb907628bbfca940e2f6))
* SSH tunnel connection error with private key, closes [#260](https://github.com/antares-sql/antares/issues/260) ([c826888](https://github.com/antares-sql/antares/commit/c826888b0dd0908958a4f727ddfa642e846269cf))
* **UI:** BaseSelect keyboard navigation ([7c45203](https://github.com/antares-sql/antares/commit/7c452036368fa0db6b9cde7c35e60a8e57bfece7))
* **UI:** BaseSelect style ([71b0736](https://github.com/antares-sql/antares/commit/71b0736d0ddbd599ab41cde0a6b0823e2bb7da2f))
### Improvements
* **UI:** max height for query text area increased ([5d5f1da](https://github.com/antares-sql/antares/commit/5d5f1da97b9adfa743197d8fa0bbb6addd565a7a))
### [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)
### Features
* **core:** option to allow untrusted SSL connections ([6cc098c](https://github.com/antares-sql/antares/commit/6cc098c6f02fb52cc71c0141431ab75f12744a1c))
* french translation updated, closes [#222](https://github.com/antares-sql/antares/issues/222) ([796f61b](https://github.com/antares-sql/antares/commit/796f61bf2feab0da515901e2137dc7bf04371d7d))
* **PostgreSQL:** export functions and procedures ([a8ca8f2](https://github.com/antares-sql/antares/commit/a8ca8f2f76ab36c4afe84d602709386315f4b7d1))
* **PostgreSQL:** export tables ([a67071e](https://github.com/antares-sql/antares/commit/a67071e28470bcbd0ec26780bb86f3c65750ded8))
* **PostgreSQL:** export triggers ([42376b4](https://github.com/antares-sql/antares/commit/42376b4bc6dd8b630402d09b026d9fbc0b8646bb))
* **PostgreSQL:** export user-defined types before tables ([bb02479](https://github.com/antares-sql/antares/commit/bb02479b71bf75a6e69e28af57c5fe213d3f30bc))
* **PostgreSQL:** export views ([86f011f](https://github.com/antares-sql/antares/commit/86f011f34fec9d6829bce324493fea888a863ffc))
* **PostgreSQL:** sql dump importer ([6086ca4](https://github.com/antares-sql/antares/commit/6086ca4a80b9ad6a07086446253d781f052d3abc))
### Bug Fixes
* **PostgreSQL:** wrong values exporting table content ([0f9c991](https://github.com/antares-sql/antares/commit/0f9c991f539560913fa0e9361a16e6448a066a27))
* ssh tunnel not properly working, closes [#220](https://github.com/antares-sql/antares/issues/220) ([026d74c](https://github.com/antares-sql/antares/commit/026d74c8c88c605a3c8c963c211078f5b3dcfda1))
### Improvements
* **PostgreSQL:** improved dump file ([408dded](https://github.com/antares-sql/antares/commit/408ddeda5634ab6bf41eff760271669170b60eb6))
* **PostgreSQL:** improved views exportation ([638a88a](https://github.com/antares-sql/antares/commit/638a88a1fb35c048ff4c6d120aaaef831c846f58))
### [0.5.1](https://github.com/Fabio286/antares/compare/v0.5.0...v0.5.1) (2022-03-25)
### Features
* export database as zip sql file ([8f3efab](https://github.com/Fabio286/antares/commit/8f3efabb6962c55c23a43c8da1433185dbc3fb41))
* **UI:** option to disable blur effects, closes [#209](https://github.com/Fabio286/antares/issues/209) ([e9079ad](https://github.com/Fabio286/antares/commit/e9079adb25ec28e9546acd54bc2565b8d6e28120))
### Bug Fixes
* numeric scale displayed on non decimal fields ([db628f7](https://github.com/Fabio286/antares/commit/db628f77226b161c3df31b7450b92a6e58754ab7))
* **UI:** connection buttons out of screen on small displays, closes [#213](https://github.com/Fabio286/antares/issues/213) ([f12e6a9](https://github.com/Fabio286/antares/commit/f12e6a96dd66140b06c55eda775af48a666627dd))
## [0.5.0](https://github.com/Fabio286/antares/compare/v0.4.4...v0.5.0) (2022-03-12)

133
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,133 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
fabio286@gmail.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

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.
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.
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.
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:
@@ -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).
- **kebab-case** in templates for property and event names.
### Vuex
- **snake_case** for state names.
- **camelCase** for getter and action names.
- **SNAKE_CASE (all caps)** for mutation names.
### Code Style
The project includes [ESlint](https://eslint.org/) and [StyleLint](https://stylelint.io/) config files with style rules. I recommend to set the lint on-save option in your code editor.

View File

@@ -32,13 +32,14 @@ We are actively working on it, hoping to provide new cool features, improvements
- Query history: search through the last 1000 queries.
- SSH tunnel support.
- Manual commit mode.
- Import and export database dumps.
- Dark and light theme.
- Editor themes.
## Philosophy
Why are we developing an SQL client when there are a lot of them on the market?
The main goal is to develop a totally free, full featured, cross platform and open source alternative, empowered by JavaScript's ecosystem.
The main goal is to develop a **forever 100% free (without paid premium feature)**, full featured, as possible community driven, cross platform and open source alternative, empowered by JavaScript ecosystem.
A modern application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons, nested tabs or submenu; productivity comes first.
## Installation
@@ -71,7 +72,6 @@ This is a roadmap with major features will come in near future.
- Users management (add/edit/delete).
- More context menu shortcuts.
- More keyboard shortcuts.
- Import/export and migration.
- Support for other databases.
- Apple Silicon distribution
@@ -104,7 +104,7 @@ This is a roadmap with major features will come in near future.
- 🌍 [Translate Antares](https://github.com/Fabio286/antares/wiki/Translate-Antares)
- 📖 [Contributors Guide](https://github.com/Fabio286/antares/wiki/Contributors-Guide)
- 🚧 [Project Board](https://github.com/users/Fabio286/projects/1)
- 🚧 [Project Board](https://github.com/antares-sql/antares/projects/1)
## Contributors ✨
@@ -115,9 +115,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://fabiodistasio.it/"><img src="https://avatars.githubusercontent.com/u/31471771?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Fabio Di Stasio</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=Fabio286" title="Code">💻</a> <a href="#translation-Fabio286" title="Translation">🌍</a> <a href="https://github.com/Fabio286/antares/commits?author=Fabio286" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/giulioganci/"><img src="https://avatars.githubusercontent.com/u/4192159?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Giulio Ganci</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=toriphes" title="Code">💻</a></td>
<td align="center"><a href="https://christianratz.de/"><img src="https://avatars.githubusercontent.com/u/2630316?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Christian Ratz</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=digitalgopnik" title="Code">💻</a> <a href="#translation-digitalgopnik" title="Translation">🌍</a></td>
<td align="center"><a href="https://fabiodistasio.it/"><img src="https://avatars.githubusercontent.com/u/31471771?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Fabio Di Stasio</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=Fabio286" title="Code">💻</a> <a href="#translation-Fabio286" title="Translation">🌍</a> <a href="https://github.com/antares-sql/antares/commits?author=Fabio286" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/giulioganci/"><img src="https://avatars.githubusercontent.com/u/4192159?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Giulio Ganci</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=toriphes" title="Code">💻</a></td>
<td align="center"><a href="https://christianratz.de/"><img src="https://avatars.githubusercontent.com/u/2630316?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Christian Ratz</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=digitalgopnik" title="Code">💻</a> <a href="#translation-digitalgopnik" title="Translation">🌍</a></td>
<td align="center"><a href="https://reverb6821.github.io/"><img src="https://avatars.githubusercontent.com/u/55198803?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Giuseppe Gigliotti</b></sub></a><br /><a href="#translation-reverb6821" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/Mohd-PH"><img src="https://avatars.githubusercontent.com/u/9362157?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mohd-PH</b></sub></a><br /><a href="#translation-Mohd-PH" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/hongkfui"><img src="https://avatars.githubusercontent.com/u/37477191?v=4?s=100" width="100px;" alt=""/><br /><sub><b>hongkfui</b></sub></a><br /><a href="#translation-hongkfui" title="Translation">🌍</a></td>
@@ -128,12 +128,15 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://ngoquocdat.com/"><img src="https://avatars.githubusercontent.com/u/56961917?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ngô Quốc Đạt</b></sub></a><br /><a href="#translation-datlechin" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/IsamuSugi"><img src="https://avatars.githubusercontent.com/u/7746658?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Isamu Sugiura</b></sub></a><br /><a href="#translation-IsamuSugi" title="Translation">🌍</a></td>
<td align="center"><a href="http://rsacchetto.nexxontech.it/"><img src="https://avatars.githubusercontent.com/u/18429412?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Riccardo Sacchetto</b></sub></a><br /><a href="#platform-Occhioverde" title="Packaging/porting to new platform">📦</a></td>
<td align="center"><a href="https://kilianstallinger.com"><img src="https://avatars.githubusercontent.com/u/5290318?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kilian Stallinger</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=kilianstallz" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/wenj91"><img src="https://avatars.githubusercontent.com/u/12549338?v=4?s=100" width="100px;" alt=""/><br /><sub><b>文杰</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=wenj91" title="Code">💻</a></td>
<td align="center"><a href="https://kilianstallinger.com"><img src="https://avatars.githubusercontent.com/u/5290318?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kilian Stallinger</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=kilianstallz" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/wenj91"><img src="https://avatars.githubusercontent.com/u/12549338?v=4?s=100" width="100px;" alt=""/><br /><sub><b>文杰</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=wenj91" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/goYou"><img src="https://avatars.githubusercontent.com/u/62732795?v=4?s=100" width="100px;" alt=""/><br /><sub><b>goYou</b></sub></a><br /><a href="#translation-goYou" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/raliqala"><img src="https://avatars.githubusercontent.com/u/30502407?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Topollo</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=raliqala" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/raliqala"><img src="https://avatars.githubusercontent.com/u/30502407?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Topollo</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=raliqala" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/SmileYzn"><img src="https://avatars.githubusercontent.com/u/5851851?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Cleverson</b></sub></a><br /><a href="#translation-SmileYzn" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/fredatgithub"><img src="https://avatars.githubusercontent.com/u/6720055?v=4?s=100" width="100px;" alt=""/><br /><sub><b>fred</b></sub></a><br /><a href="#translation-fredatgithub" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/xak666"><img src="https://avatars.githubusercontent.com/u/38811437?v=4?s=100" width="100px;" alt=""/><br /><sub><b>xaka_xak</b></sub></a><br /><a href="#translation-xak666" title="Translation">🌍</a></td>
</tr>
</table>

0
misc/.gitkeep Normal file
View File

View File

@@ -1,10 +1,10 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.5.0",
"version": "0.5.5",
"description": "A modern, fast and productivity driven SQL client with a focus in UX.",
"license": "MIT",
"repository": "https://github.com/Fabio286/antares.git",
"repository": "https://github.com/antares-sql/antares.git",
"scripts": {
"debug": "npm run rebuild:electron && npm run debug-runner",
"debug-runner": "node scripts/devRunner.js --remote-debug",
@@ -18,8 +18,10 @@
"rebuild:electron": "rimraf ./dist && npm run postinstall",
"release": "standard-version",
"release:pre": "npm run release -- --prerelease alpha",
"postinstall": "electron-builder install-app-deps",
"test": "npm run compile && node tests/app.spec.js",
"devtools:install": "node scripts/devtoolsInstaller",
"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:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix",
"contributors:add": "all-contributors add",
@@ -27,6 +29,9 @@
},
"author": "Fabio Di Stasio <fabio286@gmail.com>",
"main": "./dist/main.js",
"antares": {
"devtoolsId": "nhdogjmejiglipccpnnnanhbledajbpd"
},
"build": {
"appId": "com.fabio286.antares",
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
@@ -52,8 +57,7 @@
"target": {
"target": "default",
"arch": [
"x64",
"arm64"
"x64"
]
}
},
@@ -105,69 +109,83 @@
}
},
"dependencies": {
"@electron/remote": "^2.0.1",
"@mdi/font": "^6.1.95",
"@turf/helpers": "^6.5.0",
"@vscode/vscode-languagedetection": "^1.0.21",
"ace-builds": "^1.4.13",
"better-sqlite3": "^7.5.0",
"electron-log": "^4.4.1",
"electron-store": "^8.0.1",
"electron-updater": "^4.6.1",
"electron-window-state": "^5.0.3",
"faker": "^5.5.3",
"leaflet": "^1.7.1",
"marked": "^4.0.0",
"moment": "^2.29.1",
"mysql2": "^2.3.2",
"pg": "^8.7.1",
"pgsql-ast-parser": "^7.2.1",
"source-map-support": "^0.5.20",
"spectre.css": "^0.5.9",
"sql-formatter": "^4.0.2",
"ssh2-promise": "^1.0.2",
"v-mask": "^2.3.0",
"vue-i18n": "^8.26.5",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2"
"@electron/remote": "~2.0.1",
"@faker-js/faker": "~6.1.2",
"@mdi/font": "~6.1.95",
"@turf/helpers": "~6.5.0",
"@vscode/vscode-languagedetection": "~1.0.21",
"ace-builds": "~1.4.13",
"better-sqlite3": "~7.5.0",
"electron-log": "~4.4.1",
"electron-store": "~8.0.1",
"electron-updater": "~4.6.5",
"electron-window-state": "~5.0.3",
"encoding": "~0.1.13",
"leaflet": "~1.7.1",
"marked": "~4.0.0",
"moment": "~2.29.1",
"mysql2": "~2.3.2",
"pg": "~8.7.1",
"pg-query-stream": "~4.2.3",
"pgsql-ast-parser": "~7.2.1",
"pinia": "~2.0.13",
"source-map-support": "~0.5.20",
"spectre.css": "~0.5.9",
"sql-formatter": "~4.0.2",
"ssh2-promise": "~1.0.2",
"v-mask": "~2.3.0",
"vue": "~3.2.33",
"vue-i18n": "~9.1.9",
"vuedraggable": "~4.1.0"
},
"devDependencies": {
"@babel/eslint-parser": "^7.15.7",
"@babel/preset-env": "^7.15.8",
"all-contributors-cli": "^6.20.0",
"babel-loader": "^8.2.3",
"chalk": "^4.1.2",
"cross-env": "^7.0.2",
"css-loader": "^6.5.0",
"electron": "^17.0.1",
"electron-builder": "^22.14.11",
"electron-devtools-installer": "^3.2.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",
"@babel/eslint-parser": "~7.15.7",
"@babel/preset-env": "~7.15.8",
"@babel/preset-typescript": "~7.16.7",
"@playwright/test": "~1.21.1",
"@types/better-sqlite3": "~7.5.0",
"@types/node": "~17.0.23",
"@types/pg": "~8.6.5",
"@typescript-eslint/eslint-plugin": "~5.18.0",
"@typescript-eslint/parser": "~5.18.0",
"@vue/compiler-sfc": "~3.2.33",
"all-contributors-cli": "~6.20.0",
"babel-loader": "~8.2.3",
"chalk": "~4.1.2",
"cross-env": "~7.0.2",
"css-loader": "~6.5.0",
"electron": "~17.4.3",
"electron-builder": "~23.0.3",
"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",
"node-loader": "^2.0.0",
"playwright": "^1.18.1",
"progress-webpack-plugin": "^1.0.12",
"rimraf": "^3.0.2",
"sass": "^1.42.1",
"sass-loader": "^12.3.0",
"standard-version": "^9.3.1",
"style-loader": "^3.3.1",
"stylelint": "^13.13.1",
"stylelint-config-standard": "^22.0.0",
"stylelint-scss": "^3.21.0",
"tree-kill": "^1.2.2",
"vue": "^2.6.14",
"vue-loader": "^15.9.8",
"vue-template-compiler": "^2.6.14",
"webpack": "^5.60.0",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.4.0"
"node-loader": "~2.0.0",
"playwright": "~1.21.1",
"playwright-core": "~1.21.1",
"progress-webpack-plugin": "~1.0.12",
"rimraf": "~3.0.2",
"sass": "~1.42.1",
"sass-loader": "~12.3.0",
"standard-version": "~9.3.1",
"style-loader": "~3.3.1",
"stylelint": "~13.13.1",
"stylelint-config-standard": "~22.0.0",
"stylelint-scss": "~3.21.0",
"tree-kill": "~1.2.2",
"ts-loader": "~9.2.8",
"typescript": "~4.6.3",
"unzip-crx-3": "~0.2.0",
"vue-eslint-parser": "~8.3.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.ELECTRON_ENABLE_LOGGING = true
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = false;
const chalk = require('chalk');
const electron = require('electron');
@@ -19,7 +20,7 @@ let manualRestart = null;
const remoteDebugging = process.argv.includes('--remote-debug');
if (remoteDebugging) {
// disable dvtools open in electron
// disable devtools open in electron
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: '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: 'arrayElement', group: 'random', types: ['string'] },
{ name: 'arrayElements', group: 'random', types: ['string'] },
@@ -195,7 +195,7 @@ export default class {
return 1;
return 0;
}); ;
});
}
static getGroupsByType (type) {

View File

@@ -39,6 +39,7 @@ module.exports = {
schemaEdit: false,
schemaDrop: false,
schemaExport: false,
exportByChunks: false,
schemaImport: false,
tableSettings: false,
tableOptions: false,

View File

@@ -35,6 +35,7 @@ module.exports = {
schemaEdit: true,
schemaDrop: true,
schemaExport: true,
exportByChunks: true,
schemaImport: true,
tableSettings: true,
viewSettings: true,

View File

@@ -31,9 +31,9 @@ module.exports = {
routineAdd: true,
functionAdd: true,
schemaDrop: true,
schemaExport: true,
schemaImport: true,
databaseEdit: false,
schemaExport: false,
schemaImport: false,
tableSettings: true,
viewSettings: true,
triggerSettings: true,

View File

@@ -58,7 +58,7 @@ module.exports = [
},
{
name: 'DOUBLE',
length: true,
length: false,
collation: false,
unsigned: 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) {
return (prefix ? `${prefix}:` : '') + Math.random().toString(36).substr(2, 9).toUpperCase();
};
}

View File

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

View File

@@ -1,16 +1,29 @@
import fs from 'fs';
import * as antares from 'common/interfaces/antares';
import * as fs from 'fs';
import { ipcMain } from 'electron';
import { ClientsFactory } from '../libs/ClientsFactory';
import { SslOptions } from 'mysql2';
export default connections => {
ipcMain.handle('test-connection', async (event, conn) => {
export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('test-connection', async (event, conn: antares.ConnectionParams) => {
const params = {
host: conn.host,
port: +conn.port,
user: conn.user,
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)
@@ -21,10 +34,11 @@ export default connections => {
if (conn.ssl) {
params.ssl = {
key: conn.key ? fs.readFileSync(conn.key) : null,
cert: conn.cert ? fs.readFileSync(conn.cert) : null,
ca: conn.ca ? fs.readFileSync(conn.ca) : null,
ciphers: conn.ciphers
key: conn.key ? fs.readFileSync(conn.key).toString() : null,
cert: conn.cert ? fs.readFileSync(conn.cert).toString() : null,
ca: conn.ca ? fs.readFileSync(conn.ca).toString() : null,
ciphers: conn.ciphers,
rejectUnauthorized: !conn.untrustedConnection
};
}
@@ -34,7 +48,7 @@ export default connections => {
username: conn.sshUser,
password: conn.sshPass,
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
};
}
@@ -60,14 +74,26 @@ export default connections => {
return uid in connections;
});
ipcMain.handle('connect', async (event, conn) => {
ipcMain.handle('connect', async (event, conn: antares.ConnectionParams) => {
const params = {
host: conn.host,
port: +conn.port,
user: conn.user,
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)
@@ -81,10 +107,11 @@ export default connections => {
if (conn.ssl) {
params.ssl = {
key: conn.key ? fs.readFileSync(conn.key) : null,
cert: conn.cert ? fs.readFileSync(conn.cert) : null,
ca: conn.ca ? fs.readFileSync(conn.ca) : null,
ciphers: conn.ciphers
key: conn.key ? fs.readFileSync(conn.key).toString() : null,
cert: conn.cert ? fs.readFileSync(conn.cert).toString() : null,
ca: conn.ca ? fs.readFileSync(conn.ca).toString() : null,
ciphers: conn.ciphers,
rejectUnauthorized: !conn.untrustedConnection
};
}
@@ -94,7 +121,7 @@ export default connections => {
username: conn.sshUser,
password: conn.sshPass,
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
};
}

View File

@@ -1,6 +1,7 @@
import * as antares from 'common/interfaces/antares';
import { ipcMain } from 'electron';
export default (connections) => {
export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('get-function-informations', async (event, params) => {
try {
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 tables from './tables';
import views from './views';
@@ -10,7 +12,7 @@ import application from './application';
import schema from './schema';
import users from './users';
const connections = {};
const connections: {[key: string]: antares.Client} = {};
export default () => {
connection(connections);

View File

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

View File

@@ -1,14 +1,15 @@
import fs from 'fs';
import path from 'path';
import { fork } from 'child_process';
import * as antares from 'common/interfaces/antares';
import * as workers from 'common/interfaces/workers';
import * as fs from 'fs';
import * as path from 'path';
import { ChildProcess, fork } from 'child_process';
import { ipcMain, dialog } from 'electron';
// @TODO: need some factories
const isDevelopment = process.env.NODE_ENV !== 'production';
export default connections => {
let exporter = null;
let importer = null;
export default (connections: {[key: string]: antares.Client}) => {
let exporter: ChildProcess = null;
let importer: ChildProcess = null;
ipcMain.handle('create-schema', async (event, params) => {
try {
@@ -51,9 +52,7 @@ export default connections => {
return {
status: 'success',
response: collation.rows.length
? collation.rows[0].DEFAULT_COLLATION_NAME
: ''
response: collation
};
}
catch (err) {
@@ -175,7 +174,7 @@ export default connections => {
ipcMain.handle('export', (event, { uid, type, tables, ...rest }) => {
if (exporter !== null) return;
return new Promise((resolve, reject) => {
return new Promise((resolve/*, reject */) => {
(async () => {
if (fs.existsSync(rest.outputFile)) { // If file exists ask for replace
const result = await dialog.showMessageBox({
@@ -211,7 +210,7 @@ export default connections => {
});
// Exporter message listener
exporter.on('message', ({ type, payload }) => {
exporter.on('message', ({ type, payload }: workers.WorkerIpcMessage) => {
switch (type) {
case 'export-progress':
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;
if (exporter) {
@@ -268,7 +267,7 @@ export default connections => {
ipcMain.handle('import-sql', async (event, options) => {
if (importer !== null) return;
return new Promise((resolve, reject) => {
return new Promise((resolve/*, reject */) => {
(async () => {
const dbConfig = await connections[options.uid].getDbConfig();
@@ -283,7 +282,7 @@ export default connections => {
});
// Importer message listener
importer.on('message', ({ type, payload }) => {
importer.on('message', ({ type, payload }: workers.WorkerIpcMessage) => {
switch (type) {
case 'import-progress':
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;
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 faker from 'faker';
import moment from 'moment';
import { faker } from '@faker-js/faker';
import * as moment from 'moment';
import { sqlEscaper } from 'common/libs/sqlEscaper';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes';
import * as customizations from 'common/customizations';
import fs from 'fs';
export default (connections) => {
export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('get-table-columns', async (event, params) => {
try {
const result = await connections[params.uid].getTableColumns(params);
@@ -196,7 +198,8 @@ export default (connections) => {
ipcMain.handle('delete-table-rows', async (event, params) => {
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;
return typeof row[fieldName] === 'string'
@@ -245,7 +248,8 @@ export default (connections) => {
ipcMain.handle('insert-table-rows', async (event, params) => {
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) {
const type = params.fields[key];
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
const rows = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rows: {[key: string]: any}[] = [];
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) {
const type = params.fields[key];
@@ -375,7 +381,8 @@ export default (connections) => {
insertObj[key] = escapedParam;
}
else { // Faker value
const parsedParams = {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const parsedParams: {[key: string]: any} = {};
let fakeValue;
if (params.locale)
@@ -386,10 +393,12 @@ export default (connections) => {
if (!isNaN(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
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 (params.row[key].length)

View File

@@ -1,6 +1,7 @@
import * as antares from 'common/interfaces/antares';
import { ipcMain } from 'electron';
export default (connections) => {
export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('get-trigger-informations', async (event, params) => {
try {
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 isMacOS = process.platform === 'darwin';
let mainWindow;
autoUpdater.allowPrerelease = persistentStore.get('allow_prerelease', true);
let mainWindow: Electron.IpcMainEvent;
autoUpdater.allowPrerelease = persistentStore.get('allow_prerelease', true) as boolean;
export default () => {
ipcMain.on('check-for-updates', event => {
@@ -50,7 +50,7 @@ export default () => {
mainWindow.reply('update-downloaded');
});
autoUpdater.logger = require('electron-log');
autoUpdater.logger.transports.console.format = '{h}:{i}:{s} {text}';
autoUpdater.logger.transports.file.level = 'info';
// autoUpdater.logger = require('electron-log');
// autoUpdater.logger.transports.console.format = '{h}:{i}:{s} {text}';
// autoUpdater.logger.transports.file.level = 'info';
};

View File

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

View File

@@ -1,6 +1,7 @@
import * as antares from 'common/interfaces/antares';
import { ipcMain } from 'electron';
export default (connections) => {
export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('get-view-informations', async (event, params) => {
try {
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 sqlite from 'better-sqlite3';
import * as antares from 'common/interfaces/antares';
import * as sqlite from 'better-sqlite3';
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';
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);
this._schema = null;
this._connectionsToCommit = new Map();
}
_getTypeInfo (type) {
getTypeInfo (type: string): antares.TypeInformations {
return dataTypes
.reduce((acc, group) => [...acc, ...group.types], [])
.filter(_type => _type.name === type.toUpperCase())[0];
}
/**
* @memberof SQLiteClient
*/
async connect () {
this._connection = this.getConnection();
}
@@ -32,40 +34,44 @@ export class SQLiteClient extends AntaresCore {
});
}
/**
* @memberof SQLiteClient
*/
destroy () {}
destroy (): void {
return null;
}
/**
* Executes an USE query
*
* @memberof SQLiteClient
*/
use () {}
use (): void {
return null;
}
/**
* @param {Array} schemas list
* @returns {Array.<Object>} databases scructure
* @memberof SQLiteClient
*/
async getStructure (schemas) {
const { rows: databases } = await this.raw('SELECT * FROM pragma_database_list');
async getStructure (schemas: Set<string>) {
/* eslint-disable camelcase */
interface ShowTableResult {
Db?: string;
type: string;
name: string;
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 tablesArr = [];
const triggersArr = [];
const tablesArr: ShowTableResult[] = [];
const triggersArr: ShowTriggersResult[] = [];
let schemaSize = 0;
for (const db of filteredDatabases) {
if (!schemas.has(db.name)) continue;
let { rows: tables } = await this.raw(`
let { rows: tables } = await this.raw<antares.QueryResult<ShowTableResult>>(`
SELECT *
FROM "${db.name}".sqlite_master
WHERE type IN ('table', 'view')
AND name NOT LIKE 'sqlite_%'
AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\'
ORDER BY name
`);
if (tables.length) {
@@ -76,7 +82,7 @@ export class SQLiteClient extends AntaresCore {
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) {
triggers = triggers.map(trigger => {
trigger.Db = db.name;
@@ -133,22 +139,24 @@ export class SQLiteClient extends AntaresCore {
});
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table scructure
* @memberof SQLiteClient
*/
async getTableColumns ({ schema, table }) {
const { rows: fields } = await this.raw(`SELECT * FROM "${schema}".pragma_table_info('${table}')`);
async getTableColumns ({ schema, table }: { schema: string; table: string }) {
interface TableColumnsResult {
cid: number;
name: string;
type: string;
notnull: 0 | 1;
// eslint-disable-next-line camelcase
dflt_value: string;
pk: 0 | 1;
}
const { rows: fields } = await this.raw<antares.QueryResult<TableColumnsResult>>(`SELECT * FROM "${schema}".pragma_table_info('${table}')`);
return fields.map(field => {
const [type, length] = field.type.includes('(')
? field.type.replace(')', '').split('(').map(el => {
if (!isNaN(el)) el = +el;
const [type, length]: [string, number?] = field.type.includes('(')
? field.type.replace(')', '').split('(').map((el: string | number) => {
if (!isNaN(Number(el))) el = Number(el);
return el;
})
}) as [string, number?]
: [field.type, null];
return {
@@ -174,54 +182,50 @@ export class SQLiteClient extends AntaresCore {
});
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table row count
* @memberof SQLiteClient
*/
async getTableApproximateCount ({ schema, table }) {
async getTableApproximateCount ({ schema, table }: { schema: string; table: string }): Promise<number> {
const { rows } = await this.raw(`SELECT COUNT(*) AS count FROM "${schema}"."${table}"`);
return rows.length ? rows[0].count : 0;
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table options
* @memberof SQLiteClient
*/
async getTableOptions ({ schema, table }) {
async getTableOptions ({ table }: { table: string }) {
return { name: table };
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table indexes
* @memberof SQLiteClient
*/
async getTableIndexes ({ schema, table }) {
async getTableIndexes ({ schema, table }: { schema: string; table: string }) {
interface TableColumnsResult {
type: string;
name: string;
// eslint-disable-next-line camelcase
tbl_name: string;
rootpage:4;
sql: string;
}
interface ShowIndexesResult {
seq: number;
name: string;
unique: 0 | 1;
origin: string;
partial: 0 | 1;
}
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) {
remappedIndexes.push({
name: 'PRIMARY',
column: key.name,
indexType: null,
indexType: null as never,
type: 'PRIMARY',
cardinality: null,
cardinality: null as never,
comment: '',
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) {
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({
name: index.name,
column: detail.name,
indexType: null,
indexType: null as never,
type: index.unique === 1 ? 'UNIQUE' : 'INDEX',
cardinality: null,
cardinality: null as never,
comment: '',
indexComment: ''
});
@@ -242,15 +246,19 @@ export class SQLiteClient extends AntaresCore {
return remappedIndexes;
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table key usage
* @memberof SQLiteClient
*/
async getKeyUsage ({ schema, table }) {
const { rows } = await this.raw(`SELECT * FROM "${schema}".pragma_foreign_key_list('${table}');`);
async getKeyUsage ({ schema, table }: { schema: string; table: string }) {
/* eslint-disable camelcase */
interface KeyResult {
from: string;
id: number;
table: string;
to: string;
on_update: string;
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 {
@@ -269,229 +277,11 @@ export class SQLiteClient extends AntaresCore {
});
}
async getUsers () {}
/**
* 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 getUsers (): Promise<void> {
return null;
}
/**
* 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) {
async createTable (params: antares.CreateTableParams) {
const {
schema,
fields,
@@ -499,24 +289,24 @@ export class SQLiteClient extends AntaresCore {
indexes,
options
} = params;
const newColumns = [];
const newIndexes = [];
const manageIndexes = [];
const newForeigns = [];
const newColumns: string[] = [];
const newIndexes: string[] = [];
const manageIndexes: string[] = [];
const newForeigns: string[] = [];
let sql = `CREATE TABLE "${schema}"."${options.name}"`;
// ADD FIELDS
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;
newColumns.push(`"${field.name}"
${field.type.toUpperCase()}${length && length !== true ? `(${length})` : ''}
${field.type.toUpperCase()}${length ? `(${length})` : ''}
${field.unsigned ? 'UNSIGNED' : ''}
${field.nullable ? 'NULL' : 'NOT NULL'}
${field.autoIncrement ? 'AUTO_INCREMENT' : ''}
${field.default ? `DEFAULT ${field.default}` : ''}
${field.default !== null ? `DEFAULT ${field.default || '\'\''}` : ''}
${field.onUpdate ? `ON UPDATE ${field.onUpdate}` : ''}`);
});
@@ -542,34 +332,37 @@ export class SQLiteClient extends AntaresCore {
return await this.raw(sql);
}
/**
* ALTER TABLE
*
* @returns {Promise<null>}
* @memberof SQLiteClient
*/
async alterTable (params) {
async alterTable (params: antares.AlterTableParams) {
const {
table,
schema,
additions,
deletions,
changes,
tableStructure
} = params;
try {
await this.raw('BEGIN TRANSACTION');
await this.raw('PRAGMA foreign_keys = 0');
const tmpName = `Antares_${params.table}_tmp`;
await this.raw(`CREATE TABLE "${tmpName}" AS SELECT * FROM "${params.table}"`);
const tmpName = `Antares_${table}_tmp`;
await this.raw(`CREATE TABLE "${tmpName}" AS SELECT * FROM "${table}"`);
await this.dropTable(params);
const createTableParams = {
schema: params.schema,
fields: params.tableStructure.fields,
foreigns: params.tableStructure.foreigns,
indexes: params.tableStructure.indexes.filter(index => !index.name.includes('sqlite_autoindex')),
options: { name: params.tableStructure.name }
schema: schema,
fields: tableStructure.fields,
foreigns: tableStructure.foreigns,
indexes: tableStructure.indexes.filter(index => !index.name.includes('sqlite_autoindex')),
options: { name: tableStructure.name }
};
await this.createTable(createTableParams);
const insertFields = createTableParams.fields
.filter(field => {
return (
params.additions.every(add => add.name !== field.name) &&
params.deletions.every(del => del.name !== field.name)
additions.every(add => add.name !== field.name) &&
deletions.every(del => del.name !== field.name)
);
})
.reduce((acc, curr) => {
@@ -578,7 +371,7 @@ export class SQLiteClient extends AntaresCore {
}, []);
const selectFields = insertFields.map(field => {
const renamedField = params.changes.find(change => `"${change.name}"` === field);
const renamedField = changes.find(change => `"${change.name}"` === field);
if (renamedField)
return `"${renamedField.orgName}"`;
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.dropTable({ schema: params.schema, table: tmpName });
await this.dropTable({ schema: schema, table: tmpName });
await this.raw('PRAGMA foreign_keys = 1');
await this.raw('COMMIT');
}
@@ -596,43 +389,147 @@ export class SQLiteClient extends AntaresCore {
}
}
/**
* DUPLICATE TABLE
*
* @returns {Promise<null>}
* @memberof SQLiteClient
*/
async duplicateTable (params) { // TODO: retrive table informations and create a copy
async duplicateTable (params: { schema: string; table: string }) { // TODO: retrive table informations and create a copy
const sql = `CREATE TABLE "${params.schema}"."${params.table}_copy" AS SELECT * FROM "${params.schema}"."${params.table}"`;
return await this.raw(sql);
}
/**
* TRUNCATE TABLE
*
* @returns {Promise<null>}
* @memberof SQLiteClient
*/
async truncateTable (params) {
async truncateTable (params: { schema: string; table: string }) {
const sql = `DELETE FROM "${params.schema}"."${params.table}"`;
return await this.raw(sql);
}
/**
* DROP TABLE
*
* @returns {Promise<null>}
* @memberof SQLiteClient
*/
async dropTable (params) {
async dropTable (params: { schema: string; table: string }) {
const sql = `DROP TABLE "${params.schema}"."${params.table}"`;
return await this.raw(sql);
}
/**
* @returns {String} SQL string
* @memberof SQLiteClient
*/
async getViewInformations ({ schema, view }: { schema: string; view: string }) {
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 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 () {
// SELECT
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(', ')} ` : '';
// LIMIT
const limitRaw = this._query.limit.length ? `LIMIT ${this._query.limit.join(', ')} ` : '';
const limitRaw = this._query.limit ? `LIMIT ${this._query.limit} ` : '';
// 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}`;
}
/**
* @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) {
async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) {
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
args = {
@@ -720,7 +608,7 @@ export class SQLiteClient extends AntaresCore {
.map(q => q.trim())
: [sql];
let connection;
let connection: sqlite.Database;
if (!args.autocommit && args.tabUid) { // autocommit OFF
if (this._connectionsToCommit.has(args.tabUid))
@@ -738,31 +626,33 @@ export class SQLiteClient extends AntaresCore {
if (!query) continue;
const timeStart = new Date();
let timeStop;
const keysArr = [];
const keysArr: antares.QueryForeign[] = [];
const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => {
(async () => {
let queryResult;
let queryRunResult: sqlite.RunResult;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let queryAllResult: any[];
let affectedRows;
let fields;
const detectedTypes = {};
const detectedTypes: {[key: string]: string} = {};
try {
const stmt = connection.prepare(query);
if (stmt.reader) {
queryResult = stmt.all();
queryAllResult = stmt.all();
fields = stmt.columns();
if (queryResult.length) {
if (queryAllResult.length) {
fields.forEach(field => {
detectedTypes[field.name] = typeof queryResult[0][field.name];
detectedTypes[field.name] = typeof queryAllResult[0][field.name];
});
}
}
else {
const info = queryResult = stmt.run();
affectedRows = info.changes;
queryRunResult = stmt.run();
affectedRows = queryRunResult.changes;
}
}
catch (err) {
@@ -773,18 +663,18 @@ export class SQLiteClient extends AntaresCore {
let remappedFields = fields
? fields.map(field => {
let [parsedType, length] = field.type?.includes('(')
? field.type.replace(')', '').split('(').map(el => {
if (!isNaN(el))
el = +el;
let [parsedType, length]: [string, number?] = field.type?.includes('(')
? field.type.replace(')', '').split('(').map((el: string | number) => {
if (!isNaN(Number(el)))
el = Number(el);
else
el = el.trim();
el = (el as string).trim();
return el;
})
}) as [string, number?]
: [field.type, null];
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('.'))
length = firstNotNull[field.name].split('.').pop().length;
}
@@ -798,7 +688,8 @@ export class SQLiteClient extends AntaresCore {
tableAlias: field.table,
orgTable: field.table,
type: field.type !== null ? parsedType : detectedTypes[field.name],
length
length,
key: undefined as string
};
}).filter(Boolean)
: [];
@@ -818,18 +709,12 @@ export class SQLiteClient extends AntaresCore {
const indexes = await this.getTableIndexes(paramObj);
remappedFields = remappedFields.map(field => {
// const detailedField = columns.find(f => f.name === field.name);
const fieldIndex = indexes.find(i => i.column === field.name);
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) {
const key = fieldIndex.type === 'PRIMARY' ? 'pri' : fieldIndex.type === 'UNIQUE' ? 'uni' : 'mul';
field = { ...field, key };
};
}
}
return field;
@@ -842,8 +727,8 @@ export class SQLiteClient extends AntaresCore {
}
resolve({
duration: timeStop - timeStart,
rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false,
duration: timeStop.getTime() - timeStart.getTime(),
rows: Array.isArray(queryAllResult) ? queryAllResult.some(el => Array.isArray(el)) ? [] : queryAllResult : false,
report: affectedRows !== undefined ? { affectedRows } : null,
fields: remappedFields,
keys: keysArr
@@ -854,6 +739,16 @@ export class SQLiteClient extends AntaresCore {
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,19 +1,35 @@
import fs from 'fs';
import path from 'path';
import EventEmitter from 'events';
import * as exporter from 'common/interfaces/exporter';
import * as fs from 'fs';
import { createGzip, Gzip } from 'zlib';
import * as path from 'path';
import * as EventEmitter from 'events';
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();
this._tables = tables;
this._options = options;
this._isCancelled = false;
this._outputStream = fs.createWriteStream(this._options.outputFile, {
flags: 'w'
});
this._outputFileStream = fs.createWriteStream(this._options.outputFile, { flags: 'w' });
this._processedStream = null;
this._state = {};
this._outputStream.once('error', err => {
if (this._options.outputFormat === 'sql.zip') {
const outputZipStream = createGzip();
outputZipStream.pipe(this._outputFileStream);
this._processedStream = outputZipStream;
}
else
this._processedStream = this._outputFileStream;
this._processedStream.once('error', err => {
this._isCancelled = true;
this.emit('error', err);
});
@@ -29,7 +45,7 @@ export class BaseExporter extends EventEmitter {
throw err;
}
finally {
this._outputStream.end();
this._processedStream.end();
this.emit('end');
}
}
@@ -52,11 +68,11 @@ export class BaseExporter extends EventEmitter {
this.emitUpdate({ op: 'cancelling' });
}
emitUpdate (state) {
emitUpdate (state: exporter.ExportState) {
this.emit('progress', { ...this._state, ...state });
}
writeString (data) {
writeString (data: string) {
if (this._isCancelled) return;
try {
@@ -68,7 +84,7 @@ export class BaseExporter extends EventEmitter {
const fileName = path.basename(this._options.outputFile);
this.emit('error', `The file ${fileName} is not accessible`);
}
this._outputStream.write(data);
this._processedStream.write(data);
}
dump () {

View File

@@ -1,11 +1,23 @@
import * as exporter from 'common/interfaces/exporter';
import * as mysql from 'mysql2/promise';
import { SqlExporter } from './SqlExporter';
import { BLOB, BIT, DATE, DATETIME, FLOAT, SPATIAL, IS_MULTI_SPATIAL, NUMBER } from 'common/fieldTypes';
import hexToBinary from 'common/libs/hexToBinary';
import { getArrayDepth } from 'common/libs/getArrayDepth';
import moment from 'moment';
import * as moment from 'moment';
import { lineString, point, polygon } from '@turf/helpers';
import { MySQLClient } from '../../clients/MySQLClient';
export default class MysqlExporter extends SqlExporter {
protected _client: MySQLClient;
constructor (client: MySQLClient, tables: exporter.TableParams[], options: exporter.ExportOptions) {
super(tables, options);
this._client = client;
this._commentChar = '#';
}
async getSqlHeader () {
let dump = await super.getSqlHeader();
dump += `
@@ -36,7 +48,7 @@ ${footer}
`;
}
async getCreateTable (tableName) {
async getCreateTable (tableName: string) {
const { rows } = await this._client.raw(
`SHOW CREATE TABLE \`${this.schemaName}\`.\`${tableName}\``
);
@@ -48,11 +60,11 @@ ${footer}
return rows[0][col] + ';';
}
getDropTable (tableName) {
getDropTable (tableName: string) {
return `DROP TABLE IF EXISTS \`${tableName}\`;`;
}
async * getTableInsert (tableName) {
async * getTableInsert (tableName: string) {
let rowCount = 0;
let sqlStr = '';
@@ -103,7 +115,7 @@ ${footer}
queryLength = 0;
rowsWritten = 0;
}
else if (parseInt(rowIndex) === 0) sqlInsertString += '\n\t(';
else if (rowIndex === 0) sqlInsertString += '\n\t(';
else sqlInsertString += ',\n\t(';
for (const i in notGeneratedColumns) {
@@ -118,7 +130,7 @@ ${footer}
}
else if (DATETIME.includes(column.type)) {
let datePrecision = '';
for (let i = 0; i < column.precision; i++)
for (let i = 0; i < column.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
sqlInsertString += moment(val).isValid()
@@ -138,7 +150,7 @@ ${footer}
if (IS_MULTI_SPATIAL.includes(column.type)) {
const features = [];
for (const element of val)
features.push(this.getMarkers(element));
features.push(this._getGeoJSON(element));
geoJson = {
type: 'FeatureCollection',
@@ -185,11 +197,35 @@ ${footer}
);
let sqlString = '';
sqlString += this.buildComment('Creating temporary tables to overcome VIEW dependency errors');
// Temporary tables
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);
sqlString += viewSyntax.replaceAll('`' + this.schemaName + '`.', '');
sqlString += '\n';
sqlString += '\n\n';
}
return sqlString;
@@ -317,7 +353,7 @@ ${footer}
return sqlString;
}
async getRoutineSyntax (name, type, definer) {
async getRoutineSyntax (name: string, type: string, definer: string) {
const { rows: routines } = await this._client.raw(
`SHOW CREATE ${type} \`${this.schemaName}\`.\`${name}\``
);
@@ -347,12 +383,13 @@ ${footer}
return sqlString;
}
async _queryStream (sql) {
async _queryStream (sql: string) {
if (process.env.NODE_ENV === 'development') console.log('EXPORTER:', sql);
const isPool = typeof this._client._connection.getConnection === 'function';
const connection = isPool ? await this._client._connection.getConnection() : this._client._connection;
const stream = connection.connection.query(sql).stream();
const dispose = () => connection.destroy();
const isPool = 'getConnection' in this._client._connection;
const connection = isPool ? await (this._client._connection as mysql.Pool).getConnection() : this._client._connection;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const stream = (connection as any).connection.query(sql).stream();
const dispose = () => (connection as mysql.PoolConnection).release();
stream.on('end', dispose);
stream.on('error', dispose);
@@ -360,17 +397,17 @@ ${footer}
return stream;
}
getEscapedDefiner (definer) {
getEscapedDefiner (definer: string) {
return definer
.split('@')
.map(part => '`' + part + '`')
.join('@');
}
escapeAndQuote (val) {
escapeAndQuote (val: string) {
// eslint-disable-next-line no-control-regex
const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g;
const CHARS_ESCAPE_MAP = {
const CHARS_ESCAPE_MAP: {[key: string]: string} = {
'\0': '\\0',
'\b': '\\b',
'\t': '\\t',
@@ -399,14 +436,16 @@ ${footer}
return `'${escapedVal}'`;
}
_getGeoJSON (val) {
/* eslint-disable @typescript-eslint/no-explicit-any */
_getGeoJSON (val: any) {
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]], [])));
return polygon(val.map(arr => arr.reduce((acc: any, curr: any) => [...acc, [curr.x, curr.y]], [])));
}
else
return point([val.x, val.y]);
}
/* eslint-enable @typescript-eslint/no-explicit-any */
}

View File

@@ -0,0 +1,513 @@
import * as antares from 'common/interfaces/antares';
import * as exporter from 'common/interfaces/exporter';
import { SqlExporter } from './SqlExporter';
import { BLOB, BIT, DATE, DATETIME, FLOAT, NUMBER, TEXT_SEARCH } from 'common/fieldTypes';
import hexToBinary from 'common/libs/hexToBinary';
import * as moment from 'moment';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import * as QueryStream from 'pg-query-stream';
import { PostgreSQLClient } from '../../clients/PostgreSQLClient';
export default class PostgreSQLExporter extends SqlExporter {
constructor (client: PostgreSQLClient, tables: exporter.TableParams[], options: exporter.ExportOptions) {
super(tables, options);
this._client = client;
}
async getSqlHeader () {
let dump = await super.getSqlHeader();
dump += `
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;\n\n\n`;
if (this.schemaName !== 'public') dump += `CREATE SCHEMA "${this.schemaName}";\n\n`;
dump += await this.getCreateTypes();
return dump;
}
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 = '';
const sequences = [];
const columnsSql = [];
const arrayTypes: {[key: string]: string} = {
_int2: 'smallint',
_int4: 'integer',
_int8: 'bigint',
_float4: 'real',
_float8: 'double precision',
_char: '"char"',
_varchar: 'character varying'
};
// Table columns
const { rows } = await this._client.raw(`
SELECT *
FROM "information_schema"."columns"
WHERE "table_schema" = '${this.schemaName}'
AND "table_name" = '${tableName}'
ORDER BY "ordinal_position" ASC
`, { schema: 'information_schema' });
if (!rows.length) return '';
for (const column of rows) {
let fieldType = column.data_type;
if (fieldType === 'USER-DEFINED') fieldType = `"${this.schemaName}".${column.udt_name}`;
else if (fieldType === 'ARRAY') {
if (Object.keys(arrayTypes).includes(fieldType))
fieldType = arrayTypes[column.udt_name] + '[]';
else
fieldType = column.udt_name.replaceAll('_', '') + '[]';
}
const columnArr = [
`"${column.column_name}"`,
`${fieldType}${column.character_maximum_length ? `(${column.character_maximum_length})` : ''}`
];
if (column.column_default) {
columnArr.push(`DEFAULT ${column.column_default}`);
if (column.column_default.includes('nextval')) {
const sequenceName = column.column_default.split('\'')[1];
sequences.push(sequenceName);
}
}
if (column.is_nullable === 'NO') columnArr.push('NOT NULL');
columnsSql.push(columnArr.join(' '));
}
// Table sequences
for (let sequence of sequences) {
if (sequence.includes('.')) sequence = sequence.split('.')[1];
const { rows } = await this._client
.select('*')
.schema('information_schema')
.from('sequences')
.where({ sequence_schema: `= '${this.schemaName}'`, sequence_name: `= '${sequence}'` })
.run<SequenceRecord>();
if (rows.length) {
createSql += `CREATE SEQUENCE "${this.schemaName}"."${sequence}"
START WITH ${rows[0].start_value}
INCREMENT BY ${rows[0].increment}
MINVALUE ${rows[0].minimum_value}
MAXVALUE ${rows[0].maximum_value}
CACHE 1;\n`;
// createSql += `\nALTER TABLE "${sequence}" OWNER TO ${this._client._params.user};\n\n`;
}
}
// Table create
createSql += `\nCREATE TABLE "${this.schemaName}"."${tableName}"(
${columnsSql.join(',\n ')}
);\n`;
// createSql += `\nALTER TABLE "${tableName}" OWNER TO ${this._client._params.user};\n\n`;
// Table indexes
createSql += '\n';
const { rows: indexes } = await this._client
.select('*')
.schema('pg_catalog')
.from('pg_indexes')
.where({ schemaname: `= '${this.schemaName}'`, tablename: `= '${tableName}'` })
.run<{indexdef: string}>();
for (const index of indexes)
createSql += `${index.indexdef};\n`;
// Table foreigns
const { rows: foreigns } = await this._client.raw(`
SELECT
tc.table_schema,
tc.constraint_name,
tc.table_name,
kcu.column_name,
ccu.table_schema AS foreign_table_schema,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name,
rc.update_rule,
rc.delete_rule
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
AND ccu.table_schema = tc.table_schema
JOIN information_schema.referential_constraints AS rc
ON rc.constraint_name = kcu.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = '${this.schemaName}'
AND tc.table_name = '${tableName}'
`);
for (const foreign of foreigns) {
this._postTablesSql += `\nALTER TABLE ONLY "${this.schemaName}"."${tableName}"
ADD CONSTRAINT "${foreign.constraint_name}" FOREIGN KEY ("${foreign.column_name}") REFERENCES "${this.schemaName}"."${foreign.foreign_table_name}" ("${foreign.foreign_column_name}") ON UPDATE ${foreign.update_rule} ON DELETE ${foreign.delete_rule};\n`;
}
return createSql;
}
getDropTable (tableName: string) {
return `DROP TABLE IF EXISTS "${this.schemaName}"."${tableName}";`;
}
async * getTableInsert (tableName: string) {
let rowCount = 0;
const sqlStr = '';
const countResults = await this._client.raw(`SELECT COUNT(1) as count FROM "${this.schemaName}"."${tableName}"`);
if (countResults.rows.length === 1) rowCount = countResults.rows[0].count;
if (rowCount > 0) {
const columns = await this._client.getTableColumns({
table: tableName,
schema: this.schemaName
});
const columnNames = columns.map(col => '"' + col.name + '"').join(', ');
yield sqlStr;
const stream = await this._queryStream(
`SELECT ${columnNames} FROM "${this.schemaName}"."${tableName}"`
);
for await (const row of stream) {
if (this.isCancelled) {
stream.destroy();
yield null;
return;
}
let sqlInsertString = `INSERT INTO "${this.schemaName}"."${tableName}" (${columnNames}) VALUES`;
sqlInsertString += ' (';
for (const i in columns) {
const column = columns[i];
const val = row[column.name];
if (val === null) sqlInsertString += 'NULL';
else if (DATE.includes(column.type)) {
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format('YYYY-MM-DD'))
: val;
}
else if (DATETIME.includes(column.type)) {
let datePrecision = '';
for (let i = 0; i < column.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`))
: this.escapeAndQuote(val);
}
else if ('isArray' in column) {
let parsedVal;
if (Array.isArray(val))
parsedVal = JSON.stringify(val).replaceAll('[', '{').replaceAll(']', '}');
else
parsedVal = typeof val === 'string' ? val.replaceAll('[', '{').replaceAll(']', '}') : '';
sqlInsertString += `'${parsedVal}'`;
}
else if (TEXT_SEARCH.includes(column.type))
sqlInsertString += `'${val.replaceAll('\'', '\'\'')}'`;
else if (BIT.includes(column.type))
sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex'))}'`;
else if (BLOB.includes(column.type))
sqlInsertString += `decode('${val.toString('hex').toUpperCase()}', 'hex')`;
else if (NUMBER.includes(column.type))
sqlInsertString += val;
else if (FLOAT.includes(column.type))
sqlInsertString += parseFloat(val);
else if (val === '') sqlInsertString += '\'\'';
else {
sqlInsertString += typeof val === 'string'
? this.escapeAndQuote(val)
: typeof val === 'object'
? this.escapeAndQuote(JSON.stringify(val))
: val;
}
if (parseInt(i) !== columns.length - 1)
sqlInsertString += ', ';
}
sqlInsertString += ');\n';
yield sqlInsertString;
}
yield sqlStr;
}
}
async getCreateTypes () {
let sqlString = '';
const { rows: types } = await this._client.raw<antares.QueryResult<{typname: string; enumlabel: string}>>(`
SELECT pg_type.typname, pg_enum.enumlabel
FROM pg_type
JOIN pg_enum ON pg_enum.enumtypid = pg_type.oid;
`);
if (types.length) { // TODO: refactor
sqlString += this.buildComment('Dump of types\n------------------------------------------------------------') + '\n\n';
const typesArr = types.reduce((arr, type) => {
if (arr.every(el => el.name !== type.typname))
arr.push({ name: type.typname, enums: [this.escapeAndQuote(type.enumlabel)] });
else {
const i = arr.findIndex(el => el.name === type.typname);
arr[i].enums.push(this.escapeAndQuote(type.enumlabel));
}
return arr;
}, []);
for (const type of typesArr) {
sqlString += `CREATE TYPE "${this.schemaName}"."${type.name}" AS ENUM (
${type.enums.join(',\n\t')}
);`;
}
// sqlString += `\nALTER TYPE "${tableName}" OWNER TO ${this._client._params.user};\n`
}
return sqlString;
}
async getCreateAggregates () {
let sqlString = '';
const { rows: aggregates } = await this._client.raw(`
SELECT proname
FROM pg_proc
WHERE prokind = 'a'
AND pronamespace::regnamespace::text = '${this.schemaName}'
ORDER BY 1;
`);
if (aggregates.length) {
for (const aggregate of aggregates) {
const { rows: aggregateDef } = await this._client.raw(
`SELECT
format(
E'CREATE AGGREGATE %s (\n%s\n);'
, (pg_identify_object('pg_proc'::regclass, aggfnoid, 0)).identity
, array_to_string(
ARRAY[
format(E'\tSFUNC = %s', aggtransfn::regproc)
, format(E'\tSTYPE = %s', format_type(aggtranstype, NULL))
, CASE aggfinalfn WHEN '-'::regproc THEN NULL ELSE format(E'\tFINALFUNC = %s',aggfinalfn::text) END
, CASE aggsortop WHEN 0 THEN NULL ELSE format(E'\tSORTOP = %s', oprname) END
, CASE WHEN agginitval IS NULL THEN NULL ELSE format(E'\tINITCOND = %s', agginitval) END
]
, E',\n'
)
)
FROM pg_aggregate
LEFT JOIN pg_operator ON pg_operator.oid = aggsortop
WHERE aggfnoid = '${this.schemaName}.${aggregate.proname}'::regproc;`
);
if (aggregateDef.length)
sqlString += '\n\n' + aggregateDef[0].format;
}
}
return sqlString + '\n\n\n';
}
async getViews () {
const { rows: views } = await this._client.raw(`SELECT * FROM "pg_views" WHERE "schemaname"='${this.schemaName}'`);
let sqlString = '';
for (const view of views) {
sqlString += `\nDROP VIEW IF EXISTS "${view.viewname}";\n`;
// const { rows: columns } = await this._client
// .select('*')
// .schema('information_schema')
// .from('columns')
// .where({ table_schema: `= '${this.schemaName}'`, table_name: `= '${view.viewname}'` })
// .orderBy({ ordinal_position: 'ASC' })
// .run();
// sqlString += `
// CREATE VIEW "${this.schemaName}"."${view.viewname}" AS
// SELECT
// ${columns.reduce((acc, curr) => {
// const fieldType = curr.data_type === 'USER-DEFINED' ? curr.udt_name : curr.data_type;
// acc.push(`NULL::${fieldType}${curr.character_maximum_length ? `(${curr.character_maximum_length})` : ''} AS "${curr.column_name}"`);
// return acc;
// }, []).join(',\n ')};
// `;
sqlString += `\nCREATE OR REPLACE VIEW "${this.schemaName}"."${view.viewname}" AS \n${view.definition}\n`;
}
return sqlString;
}
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 = '';
// Trigger functions
const { rows: triggerFunctions } = await this._client.raw(
`SELECT DISTINCT routine_name AS name FROM information_schema.routines WHERE routine_type = 'FUNCTION' AND routine_schema = '${this.schemaName}' AND data_type = 'trigger'`
);
for (const func of triggerFunctions) {
const { rows: functionDef } = await this._client.raw(
`SELECT pg_get_functiondef((SELECT oid FROM pg_proc WHERE proname = '${func.name}')) AS definition`
);
sqlString += `\n${functionDef[0].definition};\n`;
}
const { rows: triggers } = await this._client.raw<antares.QueryResult<TriggersResult>>(
`SELECT * FROM "information_schema"."triggers" WHERE "trigger_schema"='${this.schemaName}'`
);
const remappedTriggers = triggers.reduce((acc, trigger) => {
const i = acc.findIndex(t => t.trigger_name === trigger.trigger_name && t.event_object_table === trigger.event_object_table);
if (i === -1) {
trigger.events = [trigger.event_manipulation];
acc.push(trigger);
}
else
acc[i].events.push(trigger.event_manipulation);
return acc;
}, []);
for (const trigger of remappedTriggers)
sqlString += `\nCREATE TRIGGER "${trigger.trigger_name}" ${trigger.action_timing} ${trigger.events.join(' OR ')} ON "${this.schemaName}"."${trigger.event_object_table}" FOR EACH ${trigger.action_orientation} ${trigger.action_statement};\n`;
return sqlString;
}
async getFunctions () {
let sqlString = '';
const { rows: functions } = await this._client.raw(
`SELECT DISTINCT routine_name AS name FROM information_schema.routines WHERE routine_type = 'FUNCTION' AND routine_schema = '${this.schemaName}' AND data_type != 'trigger'`
);
for (const func of functions) {
const { rows: functionDef } = await this._client.raw(
`SELECT pg_get_functiondef((SELECT oid FROM pg_proc WHERE proname = '${func.name}')) AS definition`
);
sqlString += `\n${functionDef[0].definition};\n`;
}
sqlString += await this.getCreateAggregates();
return sqlString;
}
async getRoutines () {
let sqlString = '';
const { rows: functions } = await this._client.raw(
`SELECT DISTINCT routine_name AS name FROM information_schema.routines WHERE routine_type = 'PROCEDURE' AND routine_schema = '${this.schemaName}'`
);
for (const func of functions) {
const { rows: functionDef } = await this._client.raw(
`SELECT pg_get_functiondef((SELECT oid FROM pg_proc WHERE proname = '${func.name}')) AS definition`
);
sqlString += `\n${functionDef[0].definition};\n`;
}
return sqlString;
}
async _queryStream (sql: string) {
if (process.env.NODE_ENV === 'development') console.log('EXPORTER:', sql);
const connection = await this._client.getConnection();
const query = new QueryStream(sql, null);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const stream = (connection as any).query(query);
const dispose = () => connection.end();
stream.on('end', dispose);
stream.on('error', dispose);
stream.on('close', dispose);
return stream;
}
escapeAndQuote (val: string) {
// eslint-disable-next-line no-control-regex
const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g;
const CHARS_ESCAPE_MAP: {[key: string]: string} = {
'\0': '\\0',
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\r': '\\r',
'\x1a': '\\Z',
'"': '\\"',
'\'': '\\\'',
'\\': '\\\\'
};
let chunkIndex = CHARS_TO_ESCAPE.lastIndex = 0;
let escapedVal = '';
let match;
while ((match = CHARS_TO_ESCAPE.exec(val))) {
escapedVal += val.slice(chunkIndex, match.index) + CHARS_ESCAPE_MAP[match[0]];
chunkIndex = CHARS_TO_ESCAPE.lastIndex;
}
if (chunkIndex === 0)
return `'${val}'`;
if (chunkIndex < val.length)
return `'${escapedVal + val.slice(chunkIndex)}'`;
return `'${escapedVal}'`;
}
}

View File

@@ -1,12 +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';
export class SqlExporter extends BaseExporter {
constructor (client, tables, options) {
super(tables, options);
this._client = client;
this._commentChar = '#';
}
protected _client: MySQLClient | PostgreSQLClient;
protected _commentChar = '--'
protected _postTablesSql = ''
get schemaName () {
return this._options.schema;
@@ -23,7 +23,7 @@ export class SqlExporter extends BaseExporter {
async dump () {
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(
t => t.includeStructure || t.includeContent || t.includeDropStatement
).length;
@@ -90,15 +90,22 @@ export class SqlExporter extends BaseExporter {
this.writeString('\n\n');
}
// SQL to execute after tables creation
if (this._postTablesSql) {
this.writeString(this._postTablesSql);
this.writeString('\n\n');
}
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.currentItem = item;
exportState.op = 'PROCESSING';
this.emitUpdate(exportState);
if (this[processingMethod]) {
const data = await this[processingMethod]();
const data = await this[processingMethod]() as unknown as string;
if (data !== '') {
const header =
this.buildComment(
@@ -116,7 +123,7 @@ export class SqlExporter extends BaseExporter {
this.writeString(footer);
}
buildComment (text) {
buildComment (text: string) {
return text
.split('\n')
.map(txt => `${this._commentChar} ${txt}`)
@@ -130,7 +137,7 @@ Antares - SQL Client
Version ${process.env.PACKAGE_VERSION}
https://antares-sql.app/
https://github.com/Fabio286/antares
https://github.com/antares-sql/antares
Host: ${this.host} (${serverVersion})
Database: ${this.schemaName}
@@ -144,19 +151,37 @@ Generation time: ${moment().format()}
return this.buildComment(`Dump completed on ${moment().format()}`);
}
getCreateTable (tableName) {
throw new Error(
'Sql Exporter must implement the "getCreateTable" method'
);
/* eslint-disable @typescript-eslint/no-unused-vars */
getCreateTable (_tableName: string): Promise<string> {
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');
}
getTableInsert (tableName) {
throw new Error(
'Sql Exporter must implement the "getTableInsert" method'
);
getTableInsert (_tableName: string): AsyncGenerator<string> {
throw new Error('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 EventEmitter from 'events';
import * as importer from 'common/interfaces/importer';
import * as fs from 'fs';
import * as EventEmitter from 'events';
export class BaseImporter extends EventEmitter {
constructor (options) {
protected _options;
protected _isCancelled;
protected _fileHandler;
protected _state;
constructor (options: importer.ImportOptions) {
super();
this._options = options;
this._isCancelled = false;
@@ -43,7 +49,7 @@ export class BaseImporter extends EventEmitter {
this.emitUpdate({ op: 'cancelling' });
}
emitUpdate (state) {
emitUpdate (state: importer.ImportState) {
this.emit('progress', { ...this._state, ...state });
}

View File

@@ -1,17 +1,21 @@
import fs from 'fs/promises';
import SqlParser from '../../../../common/libs/sqlParser';
import * as mysql from 'mysql2';
import * as importer from 'common/interfaces/importer';
import * as fs from 'fs/promises';
import MySQLParser from '../../parsers/MySQLParser';
import { BaseImporter } from '../BaseImporter';
export default class MysqlImporter extends BaseImporter {
constructor (client, options) {
export default class MySQLImporter extends BaseImporter {
protected _client: mysql.Pool
constructor (client: mysql.Pool, options: importer.ImportOptions) {
super(options);
this._client = client;
}
async import () {
async import (): Promise<void> {
try {
const { size: totalFileSize } = await fs.stat(this._options.file);
const parser = new SqlParser();
const parser = new MySQLParser();
let readPosition = 0;
let queryCount = 0;
@@ -22,11 +26,6 @@ export default class MysqlImporter extends BaseImporter {
queryCount: 0
});
// 1. detect file encoding
// 2. set fh encoding
// 3. detect sql mode
// 4. restore sql mode in case of exception
return new Promise((resolve, reject) => {
this._fileHandler.pipe(parser);
@@ -65,7 +64,8 @@ export default class MysqlImporter extends BaseImporter {
parser.on('pause', () => {
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) => {

View File

@@ -0,0 +1,85 @@
import * as pg from 'pg';
import * as importer from 'common/interfaces/importer';
import fs from 'fs/promises';
import PostgreSQLParser from '../../parsers/PostgreSQLParser';
import { BaseImporter } from '../BaseImporter';
export default class PostgreSQLImporter extends BaseImporter {
protected _client: pg.PoolClient;
constructor (client: pg.PoolClient, options: importer.ImportOptions) {
super(options);
this._client = client;
}
async import (): Promise<void> {
try {
const { size: totalFileSize } = await fs.stat(this._options.file);
const parser = new PostgreSQLParser();
let readPosition = 0;
let queryCount = 0;
this.emitUpdate({
fileSize: totalFileSize,
readPosition: 0,
percentage: 0,
queryCount: 0
});
return new Promise((resolve, reject) => {
this._fileHandler.pipe(parser);
parser.on('error', reject);
parser.on('close', async () => {
console.log('TOTAL QUERIES', queryCount);
console.log('import end');
resolve();
});
parser.on('data', async (query) => {
queryCount++;
parser.pause();
try {
await this._client.query(query);
}
catch (error) {
this.emit('query-error', {
sql: query,
message: error.hint || error.toString(),
sqlSnippet: error.sql,
time: new Date().getTime()
});
}
this.emitUpdate({
queryCount,
readPosition,
percentage: readPosition / totalFileSize * 100
});
this._fileHandler.pipe(parser);
parser.resume();
});
parser.on('pause', () => {
this._fileHandler.unpipe(parser);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this._fileHandler as any).readableFlowing = false;
});
this._fileHandler.on('data', (chunk) => {
readPosition += chunk.length;
});
this._fileHandler.on('error', (err) => {
console.log(err);
reject(err);
});
});
}
catch (err) {
console.log(err);
}
}
}

View File

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

View File

@@ -0,0 +1,158 @@
import { Transform, TransformCallback, TransformOptions } from 'stream';
export default class PostgreSQLParser extends Transform {
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 = {
delimiter: ';',
encoding: 'utf8',
writableObjectMode: true,
readableObjectMode: true,
...opts
};
super(opts);
this._buffer = '';
this._lastChar = '';
this._lastChars = '';
this.encoding = opts.encoding;
this.delimiter = opts.delimiter;// ';'
this._bodyWrapper = '';
this._bodyWrapperBuffer = '';
this.isEscape = false;
this.currentQuote = null;
this._firstDollarFound = false;
this._isBody = false;
this._isSingleLineComment = false;
this._isMultiLineComment = false;
}
get _isComment () {
return this._isSingleLineComment || this._isMultiLineComment;
}
_transform (chunk: Buffer, encoding: BufferEncoding, next: TransformCallback) {
for (const char of chunk.toString(this.encoding)) {
this.checkEscape();
this._buffer += char;
this._lastChar = char;
this._lastChars += char;
if (this._lastChars.length > this._bodyWrapper.length)
this._lastChars = this._lastChars.slice(-(this._bodyWrapper.length || 2));
this.checkBodyWrapper(char);
this.checkQuote(char);
this.checkCommentRow();
const query = this.getQuery();
if (query)
this.push(query);
}
next();
}
checkEscape () {
if (this._buffer.length > 0) {
this.isEscape = this._lastChar === '\\'
? !this.isEscape
: false;
}
}
checkCommentRow () {
if (this._isBody) return;
if (!this._isComment) {
if (this.currentQuote === null && this._lastChars.includes('--'))
this._isSingleLineComment = true;
if (this.currentQuote === null && this._lastChars.includes('/*'))
this._isMultiLineComment = true;
}
else {
if (this._isSingleLineComment && (this._lastChar === '\n' || this._lastChar === '\r')) {
this._buffer = '';
this._isSingleLineComment = false;
}
if (this._isMultiLineComment && this._lastChars.includes('*/')) {
this._buffer = '';
this._isMultiLineComment = false;
}
}
}
checkBodyWrapper (char: string) {
if (this._isBody)
this._isBody = this._lastChars !== this._bodyWrapper;
if (this.currentQuote === null && char === '$' && !this._firstDollarFound && !this._bodyWrapper) {
this._firstDollarFound = true;
this._bodyWrapperBuffer += char;
this._isBody = true;
}
else if (this._firstDollarFound) {
if (char === '\n' || char === ' ') {
this._firstDollarFound = false;
this._bodyWrapperBuffer = '';
this._bodyWrapper = '';
this._isBody = false;
return;
}
this._bodyWrapperBuffer += char;
const isEndDollar = char === '$';
if (isEndDollar) {
this._firstDollarFound = false;
this._bodyWrapper = this._bodyWrapperBuffer;
this._bodyWrapperBuffer = '';
}
}
}
checkQuote (char: string) {
const isQuote = !this.isEscape && (char === '\'' || char === '"');
if (isQuote && this.currentQuote === char)
this.currentQuote = null;
else if (isQuote && this.currentQuote === null)
this.currentQuote = char;
}
getQuery () {
if (this._isBody || this._isComment)
return false;
let query: false | string = false;
let demiliterFound = false;
if (this.currentQuote === null && this._buffer.length >= this.delimiter.length)
demiliterFound = this._lastChars.slice(-this.delimiter.length) === this.delimiter;
if (demiliterFound) {
const parsedStr = this._buffer.trim();
query = parsedStr;
this._buffer = '';
this._bodyWrapper = '';
}
return query;
}
}

View File

@@ -1,14 +1,11 @@
'use strict';
import { app, BrowserWindow, /* session, */ nativeImage, Menu } from 'electron';
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 remoteMain from '@electron/remote/main';
import ipcHandlers from './ipc-handlers';
// remoteMain.initialize();
Store.initRenderer();
const isDevelopment = process.env.NODE_ENV !== 'production';
@@ -18,8 +15,8 @@ const gotTheLock = app.requestSingleInstanceLock();
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
// global reference to mainWindow (necessary to prevent window from being garbage collected)
let mainWindow;
let mainWindowState;
let mainWindow: BrowserWindow;
let mainWindowState: windowStateKeeper.State;
async function createMainWindow () {
const icon = require('../renderer/images/logo-32.png');
@@ -36,7 +33,6 @@ async function createMainWindow () {
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
'web-security': false,
spellcheck: false
},
frame: false,
@@ -51,22 +47,8 @@ async function createMainWindow () {
remoteMain.enable(window.webContents);
try {
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);
}
if (isDevelopment)
await window.loadURL('http://localhost:9080');
}
else {
const indexPath = path.resolve(__dirname, 'index.html');
await window.loadFile(indexPath);
@@ -113,8 +95,8 @@ else {
mainWindow = await createMainWindow();
createAppMenu();
// if (isDevelopment)
// mainWindow.webContents.openDevTools();
if (isDevelopment)
mainWindow.webContents.openDevTools();
process.on('uncaughtException', error => {
mainWindow.webContents.send('unhandled-exception', error);
@@ -124,10 +106,18 @@ else {
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 () {
let menu = null;
let menu: Electron.Menu = null;
if (isMacOS) {
menu = Menu.buildFromTemplate([

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
<template>
<div id="wrapper" :class="`theme-${applicationTheme}`">
<div id="wrapper" :class="[`theme-${applicationTheme}`, !disableBlur || 'no-blur']">
<TheTitleBar />
<div id="window-content">
<TheSettingBar />
@@ -10,7 +10,9 @@
:key="connection.uid"
:connection="connection"
/>
<WorkspaceAddConnectionPanel v-if="selectedWorkspace === 'NEW'" />
<div class="connection-panel-wrapper">
<WorkspaceAddConnectionPanel v-if="selectedWorkspace === 'NEW'" />
</div>
</div>
<TheFooter />
<TheNotificationsBoard />
@@ -23,36 +25,56 @@
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { defineAsyncComponent } from 'vue';
import { storeToRefs } from 'pinia';
import { ipcRenderer } from 'electron';
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 {
name: 'App',
components: {
TheTitleBar: () => import(/* webpackChunkName: "TheTitleBar" */'@/components/TheTitleBar'),
TheSettingBar: () => import(/* webpackChunkName: "TheSettingBar" */'@/components/TheSettingBar'),
TheFooter: () => import(/* webpackChunkName: "TheFooter" */'@/components/TheFooter'),
TheNotificationsBoard: () => import(/* webpackChunkName: "TheNotificationsBoard" */'@/components/TheNotificationsBoard'),
Workspace: () => import(/* webpackChunkName: "Workspace" */'@/components/Workspace'),
WorkspaceAddConnectionPanel: () => import(/* webpackChunkName: "WorkspaceAddConnectionPanel" */'@/components/WorkspaceAddConnectionPanel'),
ModalSettings: () => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings'),
TheScratchpad: () => import(/* webpackChunkName: "TheScratchpad" */'@/components/TheScratchpad'),
BaseTextEditor: () => import(/* webpackChunkName: "BaseTextEditor" */'@/components/BaseTextEditor')
TheTitleBar: defineAsyncComponent(() => import(/* webpackChunkName: "TheTitleBar" */'@/components/TheTitleBar')),
TheSettingBar,
TheFooter: defineAsyncComponent(() => import(/* webpackChunkName: "TheFooter" */'@/components/TheFooter')),
TheNotificationsBoard: defineAsyncComponent(() => import(/* webpackChunkName: "TheNotificationsBoard" */'@/components/TheNotificationsBoard')),
Workspace: defineAsyncComponent(() => import(/* webpackChunkName: "Workspace" */'@/components/Workspace')),
WorkspaceAddConnectionPanel: defineAsyncComponent(() => import(/* webpackChunkName: "WorkspaceAddConnectionPanel" */'@/components/WorkspaceAddConnectionPanel')),
ModalSettings: defineAsyncComponent(() => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings')),
TheScratchpad: defineAsyncComponent(() => import(/* webpackChunkName: "TheScratchpad" */'@/components/TheScratchpad')),
BaseTextEditor: defineAsyncComponent(() => import(/* webpackChunkName: "BaseTextEditor" */'@/components/BaseTextEditor'))
},
data () {
return {};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
isLoading: 'application/isLoading',
isSettingModal: 'application/isSettingModal',
isScratchpad: 'application/isScratchpad',
connections: 'connections/getConnections',
applicationTheme: 'settings/getApplicationTheme',
isUnsavedDiscardModal: 'workspaces/isUnsavedDiscardModal'
})
setup () {
const applicationStore = useApplicationStore();
const connectionsStore = useConnectionsStore();
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
const {
isLoading,
isSettingModal,
isScratchpad
} = storeToRefs(applicationStore);
const { connections } = storeToRefs(connectionsStore);
const { applicationTheme, disableBlur } = storeToRefs(settingsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { checkVersionUpdate } = applicationStore;
return {
isLoading,
isSettingModal,
isScratchpad,
checkVersionUpdate,
connections,
applicationTheme,
disableBlur,
selectedWorkspace
};
},
mounted () {
ipcRenderer.send('check-for-updates');
@@ -94,12 +116,6 @@ export default {
node = node.parentNode;
}
});
},
methods: {
...mapActions({
showNewConnModal: 'application/showNewConnModal',
checkVersionUpdate: 'application/checkVersionUpdate'
})
}
};
</script>
@@ -130,5 +146,15 @@ export default {
> .columns {
height: calc(100vh - #{$footer-height});
}
.connection-panel-wrapper{
height: calc(100vh - #{$excluding-size});
width: 100%;
padding-top: 15vh;
display: flex;
justify-content: center;
align-items: flex-start;
overflow: auto;
}
}
</style>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,409 @@
<template>
<div
ref="el"
class="select"
:class="{'select--open': isOpen, 'select--disabled': disabled}"
role="combobox"
:tabindex="searchable || disabled ? -1 : tabindex"
@focus="activate()"
@blur="searchable ? false : handleBlurEvent()"
@keyup.esc="deactivate()"
@keydown.self.down.prevent="moveDown()"
@keydown.self.up.prevent="moveUp"
>
<div class="select__item-text">
<input
v-if="searchable"
ref="searchInput"
class="select__search-input"
:style="searchInputStyle"
type="text"
autocomplete="off"
spellcheck="false"
:tabindex="tabindex"
:value="searchText"
@input="searchText = $event.target.value"
@focus.prevent="!isOpen ? activate() : false"
@blur.prevent="handleBlurEvent()"
@keyup.esc="deactivate()"
@keydown.down.prevent="keyArrows('down')"
@keydown.up.prevent="keyArrows('up')"
@keypress.enter.prevent.stop.self="select(filteredOptions[hightlightedIndex])"
>
<span v-if="searchable && !isOpen || !searchable">{{ currentOptionLabel }}</span>
</div>
<Transition :name="animation">
<div
v-if="isOpen"
ref="optionList"
:class="`select__list-wrapper ${dropdownClass ? dropdownClass : '' }`"
>
<ul class="select__list" @mousedown.prevent>
<li
v-for="(opt, index) of filteredOptions"
:key="opt.id"
:ref="(el) => optionRefs[index] = el"
:class="{
'select__item': true,
'select__group': opt.$type === 'group',
'select__option--highlight': opt.$type === 'option' && !opt.disabled && index === hightlightedIndex,
'select__option--selected': opt.$type === 'option' && isSelected(opt),
'select__option--disabled': opt.disabled
}"
@click.stop="select(opt)"
@mousemove.self="hightlightedIndex = index"
>
<slot
name="option"
:option="opt"
:index="index"
>
{{ opt.label }}
</slot>
</li>
</ul>
</div>
</Transition>
</div>
</template>
<script>
import { defineComponent, computed, ref, watch, nextTick, onMounted, onUnmounted } from 'vue';
export default defineComponent({
name: 'BaseSelect',
props: {
modelValue: {
type: [String, Number, Object, Boolean]
},
value: {
type: [String, Number, Object, Boolean]
},
searchable: {
type: Boolean,
default: true
},
preserveSearch: {
type: Boolean,
default: false
},
tabindex: {
type: Number,
default: 0
},
options: {
type: Array,
default: () => []
},
optionTrackBy: {
type: [String, Function],
default: 'value'
},
optionLabel: {
type: [String, Function],
default: 'label'
},
optionDisabled: {
type: Function
},
groupLabel: {
type: String
},
groupValues: {
type: String
},
closeOnSelect: {
type: Boolean,
default: true
},
animation: {
type: String,
default: 'fade-slide-down'
},
dropdownOffsets: {
type: Object,
default: () => ({ top: 10, left: 0 })
},
dropdownClass: {
type: String
},
disabled: {
type: Boolean,
default: false
}
},
emits: ['select', 'open', 'close', 'update:modelValue', 'change', 'blur'],
setup (props, { emit }) {
const hightlightedIndex = ref(0);
const isOpen = ref(false);
const internalValue = ref(props.modelValue || props.value);
const el = ref(null);
const searchInput = ref(null);
const optionList = ref(null);
const optionRefs = [];
const searchText = ref('');
const getOptionValue = (opt) => _guess('optionTrackBy', opt);
const getOptionLabel = (opt) => _guess('optionLabel', opt);
const getOptionDisabled = (opt) => _guess('optionDisabled', opt);
const _guess = (name, item) => {
const prop = props[name];
if (typeof prop === 'function')
return prop(item);
return item[prop] || item;
};
const flattenOptions = computed(() => {
return [...props.options].reduce((prev, curr) => {
if (curr[props.groupValues] && curr[props.groupValues].length) {
prev.push({
$type: 'group',
label: curr[props.groupLabel],
id: `group-${curr[props.groupLabel]}`,
count: curr[props.groupLabel].length
});
return prev.concat(curr[props.groupValues].map(el => {
const value = getOptionValue(el);
return {
$type: 'option',
label: getOptionLabel(el),
id: `option-${value}`,
disabled: getOptionDisabled(el) === true,
value,
$data: {
...el
}
};
}));
}
else {
const value = getOptionValue(curr);
prev.push({
$type: 'option',
label: getOptionLabel(curr),
id: `option-${value}`,
disabled: getOptionDisabled(curr) === true,
value,
$data: {
...curr
}
});
}
return prev;
}, []);
});
const filteredOptions = computed(() => {
const normalizedSearch = (searchText.value || '').toLowerCase().trim();
return normalizedSearch
? flattenOptions.value.filter(opt => opt.$type === 'group' || opt.label.trim().toLowerCase().indexOf(normalizedSearch) !== -1)
: flattenOptions.value;
});
const searchInputStyle = computed(() => {
if (props.searchable)
// just hide the input and give the ability to receive focus
return isOpen.value ? { with: '100%' } : { width: 0, position: 'absolute', padding: 0, margin: 0 };
return '';
});
watch(filteredOptions, (options) => {
if (hightlightedIndex.value >= options.length -1)
hightlightedIndex.value = options.length ? options.length -1 : 0;
else
hightlightedIndex.value = 0;
});
const currentOptionLabel = computed(() =>
flattenOptions.value.find(d => d.value === props.modelValue)?.label
);
const select = (opt) => {
if (opt.$type === 'group' || opt.disabled) return;
internalValue.value = opt.value;
emit('select', opt);
emit('update:modelValue', opt.value);
emit('change', opt);
if (props.closeOnSelect)
deactivate();
};
const isSelected = (opt) => {
return internalValue.value === opt.value;
};
const activate = () => {
if (isOpen.value || props.disabled) return;
isOpen.value = true;
hightlightedIndex.value = flattenOptions.value.findIndex(el => el.value === internalValue.value) || 0;
if (props.searchable)
searchInput.value.focus();
else
el.value.focus();
nextTick(() => {
adjustListPosition();
scrollTo(optionRefs[hightlightedIndex.value]);
});
emit('open');
};
const deactivate = () => {
if (!isOpen.value) return;
isOpen.value = false;
if (props.searchable)
searchInput.value?.blur();
else
el.value?.blur();
if (!props.preserveSearch) searchText.value = '';
emit('close');
};
const adjustListPosition = () => {
const element = el.value;
let { left, top } = element.getBoundingClientRect();
const { left: offsetLeft = 0, top: offsetTop = 0 } = props.dropdownOffsets;
top = top + element.clientHeight + offsetTop;
const openBottom = top >= 0 && top + optionList.value.clientHeight <= window.innerHeight;
if (!openBottom) {
top -= (offsetTop * 2 + element.clientHeight);
optionList.value.style.transform = 'translate(0, -100%)';
}
optionList.value.style.left = `${left + offsetLeft}px`;
optionList.value.style.top = `${top}px`;
optionList.value.style.minWidth = `${element.clientWidth}px`;
};
const keyArrows = (direction) => {
const sum = direction === 'down' ? +1 : -1;
let index = hightlightedIndex.value + sum;
index = Math.max(0, index > filteredOptions.value.length - 1 ? filteredOptions.value.length - 1 : index);
if (filteredOptions.value[index].$type === 'group')
index=Math.max(1, index+sum);
hightlightedIndex.value = index;
const optEl = optionRefs[hightlightedIndex.value];
if (!optEl)
return;
scrollTo(optEl);
};
const scrollTo = (optEl) => {
if (!optEl) return;
const visMin = optionList.value.scrollTop;
const visMax = optionList.value.scrollTop + optionList.value.clientHeight - optEl.clientHeight;
if (optEl.offsetTop < visMin)
optionList.value.scrollTop = optEl.offsetTop;
else if (optEl.offsetTop >= visMax)
optionList.value.scrollTop = optEl.offsetTop - optionList.value.clientHeight + optEl.clientHeight;
};
const handleBlurEvent = () => {
deactivate();
emit('blur');
};
onMounted(() => {
window.addEventListener('resize', adjustListPosition);
nextTick(() => {
// fix position when the component is created and opened at the same time
if (isOpen.value) {
setTimeout(() => {
adjustListPosition();
}, 50);
}
});
});
onUnmounted(() => {
window.removeEventListener('resize', adjustListPosition);
});
return {
el,
searchInput,
searchText,
searchInputStyle,
filteredOptions,
currentOptionLabel,
activate,
deactivate,
select,
isSelected,
keyArrows,
isOpen,
hightlightedIndex,
optionList,
optionRefs,
handleBlurEvent
};
}
});
</script>
<style lang="scss" scoped>
.select {
display: block;
&:focus, &--open {
z-index: 10;
}
&__search-input {
appearance: none;
border: none;
background: transparent;
outline: none;
color: currentColor;
max-width: 100%;
width: 100%;
}
&__list-wrapper {
cursor: pointer;
position: fixed;
display: block;
z-index: 5;
-webkit-overflow-scrolling: touch;
max-height: 240px;
overflow: auto;
left: 0;
top: 40px;
}
&__list {
list-style: none;
}
&__option {
&--disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
&--disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
</style>

View File

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

View File

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

View File

@@ -4,10 +4,10 @@
<i class="mdi mdi-folder-open mr-1" />{{ message }}
</span>
<span class="text-ellipsis file-uploader-value">
{{ value | lastPart }}
{{ lastPart(modelValue) }}
</span>
<i
v-if="value.length"
v-if="modelValue.length"
class="file-uploader-reset mdi mdi-close"
@click.prevent="clear"
/>
@@ -23,9 +23,30 @@
</template>
<script>
import { uidGen } from 'common/libs/uidGen';
export default {
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) {
if (!string) return '';
@@ -34,29 +55,6 @@ export default {
string = `...${string.slice(-19)}`;
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>
@@ -78,6 +76,7 @@ export default {
.file-uploader-message {
display: flex;
word-break: keep-all;
border-radius: $border-radius 0 0 $border-radius;
}

View File

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

View File

@@ -1,43 +1,31 @@
<template>
<fieldset class="input-group mb-0">
<select
<BaseSelect
v-model="selectedGroup"
class="form-select"
:options="[{name: 'manual'}, ...fakerGroups]"
:option-label="(opt) => opt.name === 'manual' ? $t('message.manualValue') : $t(`faker.${opt.name}`)"
option-track-by="name"
:disabled="!isChecked"
style="flex-grow: 0;"
@change="onChange"
>
<option value="manual">
{{ $t('message.manualValue') }}
</option>
<option
v-for="group in fakerGroups"
:key="group.name"
:value="group.name"
>
{{ $t(`faker.${group.name}`) }}
</option>
</select>
<select
/>
<BaseSelect
v-if="selectedGroup !== 'manual'"
v-model="selectedMethod"
:options="fakerMethods"
:option-label="(opt) => $t(`faker.${opt.name}`)"
option-track-by="name"
class="form-select"
:disabled="!isChecked"
@change="onChange"
>
<option
v-for="method in fakerMethods"
:key="method.name"
:value="method.name"
>
{{ $t(`faker.${method.name}`) }}
</option>
</select>
/>
<ForeignKeySelect
v-else-if="foreignKeys.includes(field.name)"
ref="formInput"
v-model="selectedValue"
class="form-select"
:value.sync="selectedValue"
:key-usage="getKeyUsage(field.name)"
:disabled="!isChecked"
/>
@@ -52,7 +40,7 @@
>
<BaseUploadInput
v-else-if="inputProps().type === 'file'"
:value="selectedValue"
:model-value="selectedValue"
:message="$t('word.browse')"
@clear="clearValue"
@change="filesChange($event)"
@@ -66,21 +54,14 @@
:type="inputProps().type"
:disabled="!isChecked"
>
<select
<BaseSelect
v-else-if="enumArray"
v-model="selectedValue"
:options="enumArray"
class="form-select"
:disabled="!isChecked"
@change="onChange"
>
<option
v-for="val in enumArray"
:key="val"
:value="val"
>
{{ val }}
</option>
</select>
/>
<input
v-else
ref="formInput"
@@ -105,20 +86,18 @@
</template>
<script>
import { VueMaskDirective } from 'v-mask';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import BaseUploadInput from '@/components/BaseUploadInput';
import ForeignKeySelect from '@/components/ForeignKeySelect';
import FakerMethods from 'common/FakerMethods';
import BaseSelect from '@/components/BaseSelect.vue';
export default {
name: 'FakerSelect',
components: {
ForeignKeySelect,
BaseUploadInput
},
directives: {
mask: VueMaskDirective
BaseUploadInput,
BaseSelect
},
props: {
type: String,
@@ -129,6 +108,7 @@ export default {
fieldLength: Number,
fieldObj: Object
},
emits: ['update:modelValue'],
data () {
return {
localType: null,
@@ -244,7 +224,7 @@ export default {
this.selectedValue = '';
},
onChange () {
this.$emit('update:value', {
this.$emit('update:modelValue', {
group: this.selectedGroup,
method: this.selectedMethod,
params: this.methodParams,

View File

@@ -1,58 +1,67 @@
<template>
<select
<BaseSelect
ref="editField"
:options="foreigns"
class="form-select pl-1 pr-4"
:class="{'small-select': size === 'small'}"
:value="currentValue"
dropdown-class="select-sm"
dropdown-container=".workspace-query-results > .vscroll"
@change="onChange"
@blur="$emit('blur')"
>
<option v-if="!isValidDefault" :value="value">
{{ value === null ? 'NULL' : value }}
</option>
<option
v-for="row in foreignList"
:key="row.foreign_column"
:value="row.foreign_column"
:selected="row.foreign_column === value"
>
{{ row.foreign_column }} {{ 'foreign_description' in row ? ` - ${row.foreign_description}` : '' | cutText }}
</option>
</select>
/>
</template>
<script>
import { storeToRefs } from 'pinia';
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 BaseSelect from '@/components/BaseSelect.vue';
export default {
name: 'ForeignKeySelect',
filters: {
cutText (val) {
if (typeof val !== 'string') return val;
return val.length > 15 ? `${val.substring(0, 15)}...` : val;
}
},
components: { BaseSelect },
props: {
value: [String, Number],
modelValue: [String, Number],
keyUsage: Object,
size: {
type: String,
default: ''
}
},
emits: ['update:modelValue', 'blur'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
return { addNotification, selectedWorkspace };
},
data () {
return {
foreignList: []
foreignList: [],
currentValue: this.modelValue
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected'
}),
isValidDefault () {
if (!this.foreignList.length) return true;
if (this.value === null) return false;
return this.foreignList.some(foreign => foreign.foreign_column.toString() === this.value.toString());
if (this.modelValue === null) return false;
return this.foreignList.some(foreign => foreign.foreign_column.toString() === this.modelValue.toString());
},
foreigns () {
const list = [];
if (!this.isValidDefault)
list.push({ value: this.modelValue, label: this.modelValue === null ? 'NULL' : this.modelValue });
for (const row of this.foreignList)
list.push({ value: row.foreign_column, label: `${row.foreign_column} ${this.cutText('foreign_description' in row ? ` - ${row.foreign_description}` : '')}` });
return list;
}
},
async created () {
@@ -93,11 +102,12 @@ export default {
}
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
onChange () {
this.$emit('update:value', this.$refs.editField.value);
onChange (opt) {
this.$emit('update:modelValue', opt.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>
<div class="modal active modal-sm">
<a class="modal-overlay" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-key-variant mr-1" /> {{ $t('word.credentials') }}
<Teleport to="#window-content">
<div class="modal active modal-sm">
<a class="modal-overlay" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-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>
<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 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 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>
</Teleport>
</template>
<script>
export default {
name: 'ModalAskCredentials',
emits: ['close-asking', 'credentials'],
data () {
return {
credentials: {

View File

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

View File

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

View File

@@ -1,79 +1,98 @@
<template>
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-edit mr-1" />
<span class="cut-text">{{ $t('message.editSchema') }}</span>
<Teleport to="#window-content">
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-edit mr-1" />
<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
ref="firstInput"
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">
<BaseSelect
v-model="database.collation"
class="form-select"
:options="collations"
option-label="collation"
option-track-by="collation"
/>
<small>{{ $t('message.serverDefault') }}: {{ defaultCollation }}</small>
</div>
</div>
</form>
</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 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 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>
</Teleport>
</template>
<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 BaseSelect from '@/components/BaseSelect.vue';
export default {
name: 'ModalEditSchema',
components: {
BaseSelect
},
props: {
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 () {
return {
database: {
@@ -84,11 +103,6 @@ export default {
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
getDatabaseVariable: 'workspaces/getDatabaseVariable'
}),
collations () {
return this.getWorkspace(this.selectedWorkspace).collations;
},
@@ -124,13 +138,10 @@ export default {
this.$refs.firstInput.focus();
}, 20);
},
beforeDestroy () {
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
async updateSchema () {
if (this.database.collation !== this.database.prevCollation) {
try {

View File

@@ -1,271 +1,308 @@
<template>
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-arrow-down mr-1" />
<span class="cut-text">{{ $t('message.exportSchema') }}</span>
<Teleport to="#window-content">
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-arrow-down mr-1" />
<span class="cut-text">{{ $t('message.exportSchema') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</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 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"
: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">
{{ item.table }}
</div>
<div class="td text-center">
<label class="form-checkbox m-0 px-2 form-inline">
<input
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 class="column col-4">
<h4>
{{ $t('word.options') }}
</h4>
<span>{{ $t('word.includes') }}:</span>
<label
v-for="(_, key) in options.includes"
:key="key"
class="form-checkbox"
>
<input v-model="options.includes[key]" type="checkbox"><i class="form-icon" /> {{ $t(`word.${key}`) }}
</label>
<div class="mt-4 mb-2">
{{ $t('message.newInserStmtEvery') }}:
</div>
<div class="modal-body pb-0">
<div class="container">
<div class="columns">
<div class="column col-6">
<input
v-model.number="options.sqlInsertAfter"
type="number"
class="form-input"
value="250"
>
<div class="col-3">
<label class="form-label">{{ $t('message.directoryPath') }}</label>
</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 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 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 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">
{{ item.table }}
</div>
<div class="td text-center">
<label class="form-checkbox m-0 px-2 form-inline">
<input
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 class="column col-4">
<h5 class="h5">
{{ $t('word.options') }}
</h5>
<span class="h6">{{ $t('word.includes') }}:</span>
<label
v-for="(_, key) in options.includes"
:key="key"
class="form-checkbox"
>
<input v-model="options.includes[key]" type="checkbox"><i class="form-icon" /> {{ $tc(`word.${key}`, 2) }}
</label>
<div v-if="customizations.exportByChunks">
<div class="h6 mt-4 mb-2">
{{ $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">
<BaseSelect
v-model="options.sqlInsertDivider"
class="form-select"
:options="[{value: 'bytes', label: 'KiB'}, {value: 'rows', label: $tc('word.row', 2)}]"
/>
</div>
</div>
</div>
<div class="h6 mb-2 mt-4">
{{ $t('message.ourputFormat') }}:
</div>
<div class="columns">
<div class="column h5 mb-4">
<BaseSelect
v-model="options.outputFormat"
class="form-select"
:options="[{value: 'sql', label: $t('message.singleFile', {ext: '.sql'})}, {value: 'sql.zip', label: $t('message.zipCompressedFile', {ext: '.sql'})}]"
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer columns">
<div class="column col modal-progress-wrapper text-left">
<div v-if="progressPercentage > 0" class="export-progress">
<span class="progress-status">
{{ progressPercentage }}% - {{ progressStatus }}
</span>
<progress
class="progress d-block"
:value="progressPercentage"
max="100"
/>
<div class="modal-footer columns">
<div class="column col modal-progress-wrapper text-left">
<div v-if="progressPercentage > 0" class="export-progress">
<span class="progress-status">
{{ progressPercentage }}% - {{ progressStatus }}
</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">
{{ $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 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>
</Teleport>
</template>
<script>
import { ipcRenderer } from 'electron';
import { mapActions, mapGetters } from 'vuex';
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 Application from '@/ipc-api/Application';
import Schema from '@/ipc-api/Schema';
import BaseSelect from '@/components/BaseSelect.vue';
export default {
name: 'ModalExportSchema',
components: {
BaseSelect
},
props: {
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 () {
return {
isExporting: false,
@@ -275,6 +312,7 @@ export default {
tables: [],
options: {
includes: {},
outputFormat: 'sql',
sqlInsertAfter: 250,
sqlInsertDivider: 'bytes'
},
@@ -282,14 +320,12 @@ export default {
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
getDatabaseVariable: 'workspaces/getDatabaseVariable'
}),
currentWorkspace () {
return this.getWorkspace(this.selectedWorkspace);
},
customizations () {
return this.currentWorkspace.customizations;
},
schemaItems () {
const db = this.currentWorkspace.structure.find(db => db.name === this.selectedSchema);
if (db)
@@ -299,7 +335,7 @@ export default {
},
filename () {
const date = moment().format('YYYY-MM-DD');
return `${this.selectedSchema}_${date}.sql`;
return `${this.selectedSchema}_${date}.${this.options.outputFormat}`;
},
dumpFilePath () {
return `${this.basePath}/${this.filename}`;
@@ -333,25 +369,21 @@ export default {
includeDropStatement: true
}));
const structure = ['views', 'triggers', 'routines', 'functions', 'schedulers', 'triggerFunctions'];
const structure = ['functions', 'views', 'triggers', 'routines', 'schedulers'];
structure.forEach(feat => {
const val = customizations[this.currentWorkspace.client][feat];
if (val)
this.$set(this.options.includes, feat, true);
this.options.includes[feat] = true;
});
ipcRenderer.on('export-progress', this.updateProgress);
},
beforeDestroy () {
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
ipcRenderer.off('export-progress', this.updateProgress);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshSchema: 'workspaces/refreshSchema'
}),
async startExport () {
this.isExporting = true;
const { uid, client } = this.currentWorkspace;

View File

@@ -1,211 +1,187 @@
<template>
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span class="cut-text">{{ $tc('message.insertRow', 2) }}</span>
<Teleport to="#window-content">
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-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>
<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="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"
>
<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
: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]"
: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">
<i class="mdi mdi-24px mdi-repeat" />
</span>
</div>
<div
v-if="hasFakes"
class="tooltip tooltip-right ml-2"
:data-tooltip="$t('message.fakeDataLanguage')"
>
<span class="input-group-addon">
<i class="mdi mdi-24px mdi-repeat" />
</span>
<BaseSelect
v-model="fakerLocale"
:options="locales"
class="form-select"
/>
</div>
</div>
<div
v-if="hasFakes"
class="tooltip tooltip-right ml-2"
:data-tooltip="$t('message.fakeDataLanguage')"
>
<select v-model="fakerLocale" class="form-select">
<option value="ar">
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 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 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>
</Teleport>
</template>
<script>
import moment from 'moment';
import { storeToRefs } from 'pinia';
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 FakerSelect from '@/components/FakerSelect';
import BaseSelect from '@/components/BaseSelect.vue';
export default {
name: 'ModalFakerRows',
components: {
FakerSelect
},
filters: {
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
FakerSelect,
BaseSelect
},
props: {
tabUid: [String, Number],
fields: Array,
keyUsage: Array
},
emits: ['reload', 'hide'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, getWorkspaceTab } = workspacesStore;
const locales = [
{ value: 'ar', label: 'Arabic' },
{ value: 'az', label: 'Azerbaijani' },
{ value: 'zh_CN', label: 'Chinese' },
{ value: 'zh_TW', label: 'Chinese (Taiwan)' },
{ value: 'cz', label: 'Czech' },
{ value: 'nl', label: 'Dutch' },
{ value: 'nl_BE', label: 'Dutch (Belgium)' },
{ value: 'en', label: 'English' },
{ value: 'en_AU_ocker', label: 'English (Australia Ocker)' },
{ value: 'en_AU', label: 'English (Australia)' },
{ value: 'en_BORK', label: 'English (Bork)' },
{ value: 'en_CA', label: 'English (Canada)' },
{ value: 'en_GB', label: 'English (Great Britain)' },
{ value: 'en_IND', label: 'English (India)' },
{ value: 'en_IE', label: 'English (Ireland)' },
{ value: 'en_ZA', label: 'English (South Africa)' },
{ value: 'en_US', label: 'English (United States)' },
{ value: 'fa', label: 'Farsi' },
{ value: 'fi', label: 'Finnish' },
{ value: 'fr', label: 'French' },
{ value: 'fr_CA', label: 'French (Canada)' },
{ value: 'fr_CH', label: 'French (Switzerland)' },
{ value: 'ge', label: 'Georgian' },
{ value: 'de', label: 'German' },
{ value: 'de_AT', label: 'German (Austria)' },
{ value: 'de_CH', label: 'German (Switzerland)' },
{ value: 'hr', label: 'Hrvatski' },
{ value: 'id_ID', label: 'Indonesia' },
{ value: 'it', label: 'Italian' },
{ value: 'ja', label: 'Japanese' },
{ value: 'ko', label: 'Korean' },
{ value: 'nep', label: 'Nepalese' },
{ value: 'nb_NO', label: 'Norwegian' },
{ value: 'pl', label: 'Polish' },
{ value: 'pt_BR', label: 'Portuguese (Brazil)' },
{ value: 'pt_PT', label: 'Portuguese (Portugal)' },
{ value: 'ro', label: 'Romanian' },
{ value: 'ru', label: 'Russian' },
{ value: 'sk', label: 'Slovakian' },
{ value: 'es', label: 'Spanish' },
{ value: 'es_MX', label: 'Spanish (Mexico)' },
{ value: 'sv', label: 'Swedish' },
{ value: 'tr', label: 'Turkish' },
{ value: 'uk', label: 'Ukrainian' },
{ value: 'vi', label: 'Vietnamese' }
];
return {
addNotification,
selectedWorkspace,
getWorkspace,
getWorkspaceTab,
locales
};
},
data () {
return {
localRow: {},
@@ -216,11 +192,6 @@ export default {
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
getWorkspaceTab: 'workspaces/getWorkspaceTab'
}),
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
@@ -262,7 +233,7 @@ export default {
else if ([...TIME, ...DATE].includes(field.type))
fieldDefault = field.default;
else if (BIT.includes(field.type))
fieldDefault = field.default.replaceAll('\'', '').replaceAll('b', '');
fieldDefault = field.default?.replaceAll('\'', '').replaceAll('b', '');
else if (DATETIME.includes(field.type)) {
if (field.default && ['current_timestamp', 'now()'].some(term => field.default.toLowerCase().includes(term))) {
let datePrecision = '';
@@ -287,13 +258,10 @@ export default {
this.localRow = { ...rowObj };
},
beforeDestroy () {
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
@@ -345,8 +313,8 @@ export default {
},
fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
else if (TEXT.includes(field.type)) return field.charLength;
return field.length;
else if (TEXT.includes(field.type)) return Number(field.charLength);
return Number(field.length);
},
toggleFields (event, field) {
if (event.target.checked)
@@ -367,6 +335,10 @@ export default {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
}
};

View File

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

View File

@@ -1,56 +1,61 @@
<template>
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-arrow-up mr-1" />
<span class="cut-text">{{ $t('message.importSchema') }}</span>
<Teleport to="#window-content">
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-arrow-up mr-1" />
<span class="cut-text">{{ $t('message.importSchema') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body pb-0">
{{ sqlFile }}
<div v-if="queryErrors.length > 0" class="mt-2">
<label>{{ $tc('message.importQueryErrors', queryErrors.length) }}</label>
<textarea
v-model="formattedQueryErrors"
class="form-input"
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 class="modal-body pb-0">
{{ sqlFile }}
<div v-if="queryErrors.length > 0" class="mt-2">
<label>{{ $tc('message.importQueryErrors', queryErrors.length) }}</label>
<textarea
v-model="formattedQueryErrors"
class="form-input"
rows="5"
readonly
/>
</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 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 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>
<Teleport to="#window-content" />
</teleport>
</template>
<script>
import { ipcRenderer } from 'electron';
import { mapActions, mapGetters } from 'vuex';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import moment from 'moment';
import Schema from '@/ipc-api/Schema';
import { storeToRefs } from 'pinia';
export default {
name: 'ModalImportSchema',
@@ -58,6 +63,22 @@ export default {
props: {
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 () {
return {
sqlFile: '',
@@ -70,10 +91,6 @@ export default {
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
currentWorkspace () {
return this.getWorkspace(this.selectedWorkspace);
},
@@ -89,16 +106,12 @@ export default {
ipcRenderer.on('import-progress', this.updateProgress);
ipcRenderer.on('query-error', this.handleQueryError);
},
beforeDestroy () {
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
ipcRenderer.off('import-progress', this.updateProgress);
ipcRenderer.off('query-error', this.handleQueryError);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshSchema: 'workspaces/refreshSchema'
}),
async startImport (sqlFile) {
this.isImporting = true;
this.sqlFile = sqlFile;

View File

@@ -1,76 +1,96 @@
<template>
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-plus mr-1" />
<span class="cut-text">{{ $t('message.createNewSchema') }}</span>
<Teleport to="#window-content">
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-plus mr-1" />
<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">
<BaseSelect
v-model="database.collation"
class="form-select"
:options="collations"
option-label="collation"
option-track-by="collation"
/>
<small>{{ $t('message.serverDefault') }}: {{ defaultCollation }}</small>
</div>
</div>
</form>
</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 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 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>
</Teleport>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import Schema from '@/ipc-api/Schema';
import { storeToRefs } from 'pinia';
import BaseSelect from '@/components/BaseSelect.vue';
export default {
name: 'ModalNewSchema',
components: { BaseSelect },
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 () {
return {
isLoading: false,
@@ -81,11 +101,6 @@ export default {
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
getDatabaseVariable: 'workspaces/getDatabaseVariable'
}),
collations () {
return this.getWorkspace(this.selectedWorkspace).collations;
},
@@ -103,13 +118,10 @@ export default {
this.$refs.firstInput.focus();
}, 20);
},
beforeDestroy () {
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
async createSchema () {
this.isLoading = true;
try {

View File

@@ -1,148 +1,158 @@
<template>
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span class="cut-text">{{ $t('message.addNewRow') }}</span>
<Teleport to="#window-content">
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-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>
<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"
class="form-select"
:value.sync="localRow[field.name]"
: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 }} {{ 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 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>
</Teleport>
</template>
<script>
import moment from 'moment';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { VueMaskDirective } from 'v-mask';
import { mapGetters, mapActions } from 'vuex';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import Tables from '@/ipc-api/Tables';
import ForeignKeySelect from '@/components/ForeignKeySelect';
import { storeToRefs } from 'pinia';
export default {
name: 'ModalNewTableRow',
components: {
ForeignKeySelect
},
directives: {
mask: VueMaskDirective
},
filters: {
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
},
props: {
tabUid: [String, Number],
fields: 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 () {
return {
localRow: {},
@@ -152,11 +162,6 @@ export default {
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
getWorkspaceTab: 'workspaces/getWorkspaceTab'
}),
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
@@ -216,13 +221,10 @@ export default {
firstSelectableInput.focus();
}, 20);
},
beforeDestroy () {
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
@@ -331,6 +333,10 @@ export default {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
}
};

View File

@@ -1,140 +1,143 @@
<template>
<div class="modal active">
<ModalProcessesListContext
v-if="isContext"
:context-event="contextEvent"
:selected-row="selectedRow"
:selected-cell="selectedCell"
@copy-cell="copyCell"
@copy-row="copyRow"
@kill-process="killProcess"
@close-context="closeContext"
/>
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0 pb-4">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-memory mr-1" />
<span class="cut-text">{{ $t('message.processesList') }}: {{ connectionName }}</span>
</div>
</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>
<Teleport to="#window-content">
<div class="modal active">
<ModalProcessesListContext
v-if="isContext"
:context-event="contextEvent"
:selected-row="selectedRow"
:selected-cell="selectedCell"
@copy-cell="copyCell"
@copy-row="copyRow"
@kill-process="killProcess"
@close-context="closeContext"
/>
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0 pb-4">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-memory mr-1" />
<span class="cut-text">{{ $t('message.processesList') }}: {{ connectionName }}</span>
</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>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</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 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"
>
<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'"
/>
<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 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>
<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>
<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>
</Teleport>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import arrayToFile from '../libs/arrayToFile';
import { useNotificationsStore } from '@/stores/notifications';
import Schema from '@/ipc-api/Schema';
import { useConnectionsStore } from '@/stores/connections';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import ModalProcessesListRow from '@/components/ModalProcessesListRow';
import ModalProcessesListContext from '@/components/ModalProcessesListContext';
@@ -149,6 +152,13 @@ export default {
props: {
connection: Object
},
emits: ['close'],
setup () {
const { addNotification } = useNotificationsStore();
const { getConnectionName } = useConnectionsStore();
return { addNotification, getConnectionName };
},
data () {
return {
resultsSize: 1000,
@@ -167,9 +177,6 @@ export default {
};
},
computed: {
...mapGetters({
getConnectionName: 'connections/getConnectionName'
}),
connectionName () {
return this.getConnectionName(this.connection.uid);
},
@@ -203,15 +210,12 @@ export default {
this.getProcessesList();
window.addEventListener('resize', this.resizeResults);
},
beforeDestroy () {
beforeUnmount () {
window.removeEventListener('keydown', this.onKey, { capture: true });
window.removeEventListener('resize', this.resizeResults);
clearInterval(this.refreshInterval);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
async getProcessesList () {
this.isQuering = true;
@@ -284,14 +288,14 @@ export default {
this.clearRefresh();
},
selectRow (row) {
this.selectedRow = row;
this.selectedRow = Number(row);
},
contextMenu (event, cell) {
if (event.target.localName === 'input') return;
this.stopRefresh();
this.selectedCell = cell;
this.selectedRow = cell.id;
this.selectedRow = Number(cell.id);
this.contextEvent = event;
this.isContext = true;
},

View File

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

View File

@@ -12,7 +12,7 @@
class="cell-content"
:class="`${isNull(col)} type-${typeof col === 'number' ? 'int' : 'varchar'}`"
@dblclick="dblClick(cKey)"
>{{ col | cutText }}</span>
>{{ cutText(col) }}</span>
</div>
<ConfirmModal
v-if="isInfoModal"
@@ -53,15 +53,10 @@ export default {
ConfirmModal,
TextEditor
},
filters: {
cutText (val) {
if (typeof val !== 'string') return val;
return val.length > 250 ? `${val.substring(0, 250)}[...]` : val;
}
},
props: {
row: Object
},
emits: ['select-row', 'contextmenu', 'stop-refresh'],
data () {
return {
isInlineEditor: {},
@@ -95,6 +90,10 @@ export default {
this.editingField = null;
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,321 +1,398 @@
<template>
<div id="settings" class="modal active">
<a class="modal-overlay c-hand" @click="closeModal" />
<div class="modal-container">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-cog mr-1" />
<span class="cut-text">{{ $t('word.settings') }}</span>
<Teleport to="#window-content">
<div id="settings" class="modal active">
<a class="modal-overlay c-hand" @click="closeModal" />
<div class="modal-container">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-cog mr-1" />
<span class="cut-text">{{ $t('word.settings') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click="closeModal" />
</div>
<a class="btn btn-clear c-hand" @click="closeModal" />
</div>
<div class="modal-body p-0">
<div class="panel">
<div class="panel-nav">
<ul class="tab tab-block">
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'general'}"
@click="selectTab('general')"
>
<a class="tab-link">{{ $t('word.general') }}</a>
</li>
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'themes'}"
@click="selectTab('themes')"
>
<a class="tab-link">{{ $t('word.themes') }}</a>
</li>
<li
v-if="updateStatus !== 'disabled'"
class="tab-item c-hand"
:class="{'active': selectedTab === 'update'}"
@click="selectTab('update')"
>
<a class="tab-link" :class="{'badge badge-update': hasUpdates}">{{ $t('word.update') }}</a>
</li>
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'changelog'}"
@click="selectTab('changelog')"
>
<a class="tab-link">{{ $t('word.changelog') }}</a>
</li>
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'about'}"
@click="selectTab('about')"
>
<a class="tab-link">{{ $t('word.about') }}</a>
</li>
</ul>
</div>
<div v-show="selectedTab === 'general'" class="panel-body py-4">
<div class="container">
<form class="form-horizontal columns">
<div class="column col-12 h6 text-uppercase mb-1">
{{ $t('word.application') }}
</div>
<div class="column col-9 col-sm-12 mb-2">
<div class="form-group">
<div class="col-7 col-sm-12">
<label class="form-label">
<i class="mdi mdi-18px mdi-translate mr-1" />
{{ $t('word.language') }}
</label>
<div class="modal-body p-0">
<div class="panel">
<div class="panel-nav">
<ul class="tab tab-block">
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'general'}"
@click="selectTab('general')"
>
<a class="tab-link">{{ $t('word.general') }}</a>
</li>
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'themes'}"
@click="selectTab('themes')"
>
<a class="tab-link">{{ $t('word.themes') }}</a>
</li>
<li
v-if="updateStatus !== 'disabled'"
class="tab-item c-hand"
:class="{'active': selectedTab === 'update'}"
@click="selectTab('update')"
>
<a class="tab-link" :class="{'badge badge-update': hasUpdates}">{{ $t('word.update') }}</a>
</li>
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'changelog'}"
@click="selectTab('changelog')"
>
<a class="tab-link">{{ $t('word.changelog') }}</a>
</li>
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'about'}"
@click="selectTab('about')"
>
<a class="tab-link">{{ $t('word.about') }}</a>
</li>
</ul>
</div>
<div v-show="selectedTab === 'general'" class="panel-body py-4">
<div class="container">
<form class="form-horizontal columns">
<div class="column col-12 h6 text-uppercase mb-1">
{{ $t('word.application') }}
</div>
<div class="column col-12 col-sm-12 mb-2 columns">
<div class="form-group column col-12">
<div class="col-5 col-sm-12">
<label class="form-label">
<i class="mdi mdi-18px mdi-translate mr-1" />
{{ $t('word.language') }}
</label>
</div>
<div class="col-3 col-sm-12">
<BaseSelect
v-model="localLocale"
class="form-select"
:options="locales"
option-track-by="code"
option-label="name"
@change="changeLocale(localLocale)"
/>
</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 class="col-5 col-sm-12">
<select
v-model="localLocale"
class="form-select"
@change="changeLocale(localLocale)"
>
<option
v-for="(locale, key) in locales"
:key="key"
:value="locale.code"
>
{{ locale.name }}
</option>
</select>
<div class="form-group column col-12">
<div class="col-5 col-sm-12">
<label class="form-label">
{{ $t('message.dataTabPageSize') }}
</label>
</div>
<div class="col-3 col-sm-12">
<BaseSelect
v-model="localPageSize"
class="form-select"
:options="pageSizes"
@change="changePageSize(+localPageSize)"
/>
</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 class="form-group">
<div class="col-7 col-sm-12">
<label class="form-label">
{{ $t('message.dataTabPageSize') }}
</label>
<div class="column col-12 h6 mt-4 text-uppercase mb-1">
{{ $t('word.editor') }}
</div>
<div class="column col-12 col-sm-12 columns">
<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 class="col-5 col-sm-12">
<select
v-model="localPageSize"
class="form-select"
@change="changePageSize(+localPageSize)"
>
<option
v-for="size in pageSizes"
:key="size"
>
{{ size }}
</option>
</select>
<div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12">
<label class="form-label">
{{ $t('message.wrapLongLines') }}
</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 class="form-group mb-0">
<div class="col-7 col-sm-12">
<label class="form-label">
{{ $t('message.restorePreviourSession') }}
</label>
</div>
<div class="col-5 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleRestoreSession">
<input type="checkbox" :checked="restoreTabs">
<i class="form-icon" />
</label>
</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="form-group">
<div class="col-7 col-sm-12">
<label class="form-label">
{{ $t('message.notificationsTimeout') }}
</label>
</div>
<div class="col-5 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
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="column col-12 h6 mt-4 text-uppercase mb-1">
{{ $t('word.editor') }}
</div>
<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 class="columns mt-4">
<div class="column col-12 h6 text-uppercase mb-2 mt-4">
{{ $t('message.editorTheme') }}
</div>
<div class="form-group mb-0">
<div class="col-7 col-sm-12">
<label class="form-label">
{{ $t('message.wrapLongLines') }}
</label>
</div>
<div class="col-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 class="column col-6 h5 mb-4">
<BaseSelect
v-model="localEditorTheme"
class="form-select"
:options="editorThemes"
option-label="name"
option-track-by="code"
group-label="group"
group-values="themes"
@change="changeEditorTheme(localEditorTheme)"
/>
</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
v-for="theme in group.themes"
:key="theme.name"
:value="theme.code"
:selected="editorTheme === theme.code"
<div class="column col-6 mb-4">
<div class="btn-group btn-group-block">
<button
class="btn btn-dark cut-text"
:class="{'active': editorFontSize === 'small'}"
@click="changeEditorFontSize('small')"
>
{{ theme.name }}
</option>
</optgroup>
</select>
</div>
<div class="column col-6 mb-4">
<div class="btn-group btn-group-block">
<button
class="btn btn-dark cut-text"
:class="{'active': editorFontSize === 'small'}"
@click="changeEditorFontSize('small')"
>
{{ $t('word.small') }}
</button>
<button
class="btn btn-dark cut-text"
:class="{'active': editorFontSize === 'medium'}"
@click="changeEditorFontSize('medium')"
>
{{ $t('word.medium') }}
</button>
<button
class="btn btn-dark cut-text"
:class="{'active': editorFontSize === 'large'}"
@click="changeEditorFontSize('large')"
>
{{ $t('word.large') }}
</button>
{{ $t('word.small') }}
</button>
<button
class="btn btn-dark cut-text"
:class="{'active': editorFontSize === 'medium'}"
@click="changeEditorFontSize('medium')"
>
{{ $t('word.medium') }}
</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 class="column col-12">
<BaseTextEditor
:value="exampleQuery"
mode="sql"
:workspace="workspace"
:read-only="true"
:height="270"
/>
</div>
</div>
</div>
</div>
<div v-show="selectedTab === 'update'" class="panel-body py-4">
<ModalSettingsUpdate />
</div>
<div v-show="selectedTab === 'changelog'" class="panel-body py-4">
<ModalSettingsChangelog />
</div>
<div v-show="selectedTab === 'update'" class="panel-body py-4">
<ModalSettingsUpdate />
</div>
<div v-show="selectedTab === 'changelog'" class="panel-body py-4">
<ModalSettingsChangelog />
</div>
<div v-show="selectedTab === 'about'" class="panel-body py-4">
<div class="text-center">
<img src="../images/logo.svg" width="128">
<h4>{{ appName }}</h4>
<p class="mb-2">
{{ $t('word.version') }} {{ appVersion }}<br>
<a class="c-hand" @click="openOutside('https://github.com/Fabio286/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>
</p>
<div class="mb-2">
<small class="d-block text-uppercase">{{ $t('word.contributors') }}:</small>
<div class="d-block py-1">
<small v-for="(contributor, i) in otherContributors" :key="i">{{ i !== 0 ? ', ' : '' }}{{ contributor }}</small>
<div v-show="selectedTab === 'about'" class="panel-body py-4">
<div class="text-center">
<img src="../images/logo.svg" width="128">
<h4>{{ appName }}</h4>
<p class="mb-2">
{{ $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>
<small>{{ $t('word.author') }} <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">{{ appAuthor }}</a></small><br>
</p>
<div class="mb-2">
<small class="d-block text-uppercase">{{ $t('word.contributors') }}:</small>
<div class="d-block py-1">
<small v-for="(contributor, i) in otherContributors" :key="i">{{ i !== 0 ? ', ' : '' }}{{ contributor }}</small>
</div>
<small>{{ $t('message.madeWithJS') }}</small>
</div>
<small>{{ $t('message.madeWithJS') }}</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</Teleport>
</template>
<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 ModalSettingsUpdate from '@/components/ModalSettingsUpdate';
import ModalSettingsChangelog from '@/components/ModalSettingsChangelog';
import BaseTextEditor from '@/components/BaseTextEditor';
const { shell } = require('electron');
import BaseSelect from '@/components/BaseSelect.vue';
export default {
name: 'ModalSettings',
components: {
ModalSettingsUpdate,
ModalSettingsChangelog,
BaseTextEditor
BaseTextEditor,
BaseSelect
},
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 () {
return {
@@ -379,23 +456,6 @@ export default {
};
},
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',
applicationTheme: 'settings/getApplicationTheme',
editorTheme: 'settings/getEditorTheme',
editorFontSize: 'settings/getEditorFontSize',
updateStatus: 'application/getUpdateStatus',
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
locales () {
const locales = [];
for (const locale of this.$i18n.availableLocales)
@@ -441,22 +501,10 @@ ORDER BY
this.selectedTab = this.selectedSettingTab;
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
closeModal: 'application/hideSettingModal',
changeLocale: 'settings/changeLocale',
changePageSize: 'settings/changePageSize',
changeRestoreTabs: 'settings/changeRestoreTabs',
changeAutoComplete: 'settings/changeAutoComplete',
changeLineWrap: 'settings/changeLineWrap',
changeApplicationTheme: 'settings/changeApplicationTheme',
changeEditorTheme: 'settings/changeEditorTheme',
changeEditorFontSize: 'settings/changeEditorFontSize',
updateNotificationsTimeout: 'settings/updateNotificationsTimeout'
}),
selectTab (tab) {
this.selectedTab = tab;
},
@@ -477,6 +525,9 @@ ORDER BY
toggleRestoreSession () {
this.changeRestoreTabs(!this.restoreTabs);
},
toggleDisableBlur () {
this.changeDisableBlur(!this.disableBlur);
},
toggleAutoComplete () {
this.changeAutoComplete(!this.selectedAutoComplete);
},

View File

@@ -15,15 +15,19 @@
</template>
<script>
import { mapGetters } from 'vuex';
import { marked } from 'marked';
import BaseLoader from '@/components/BaseLoader';
import { useApplicationStore } from '@/stores/application';
export default {
name: 'ModalSettingsChangelog',
components: {
BaseLoader
},
setup () {
const { appVersion } = useApplicationStore();
return { appVersion };
},
data () {
return {
changelog: '',
@@ -32,21 +36,22 @@ export default {
isError: false
};
},
computed: {
...mapGetters({ appVersion: 'application/appVersion' })
},
created () {
this.getChangelog();
},
methods: {
async getChangelog () {
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'
});
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 = {
link (href, title, text) {
return text;

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,7 @@
<i class="mdi mdi-18px mdi-coffee mr-1" />
<small>{{ $t('word.donate') }}</small>
</li>
<li class="footer-element footer-link" @click="openOutside('https://github.com/Fabio286/antares/issues')">
<li class="footer-element footer-link" @click="openOutside('https://github.com/antares-sql/antares/issues')">
<i class="mdi mdi-18px mdi-bug" />
</li>
<li class="footer-element footer-link" @click="showSettingModal('about')">
@@ -27,17 +27,30 @@
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { useApplicationStore } from '@/stores/application';
import { useWorkspacesStore } from '@/stores/workspaces';
import { storeToRefs } from 'pinia';
const { shell } = require('electron');
export default {
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: {
...mapGetters({
workspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
appVersion: 'application/appVersion'
}),
version () {
return this.getWorkspace(this.workspace) ? this.getWorkspace(this.workspace).version : null;
},
@@ -48,9 +61,6 @@ export default {
}
},
methods: {
...mapActions({
showSettingModal: 'application/showSettingModal'
}),
openOutside (link) {
shell.openExternal(link);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,7 +34,7 @@
<fieldset class="m-0" :disabled="isBusy">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.connectionName') }}</label>
<label class="form-label cut-text">{{ $t('word.connectionName') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -47,23 +47,21 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.client') }}</label>
<label class="form-label cut-text">{{ $t('word.client') }}</label>
</div>
<div class="column col-8 col-sm-12">
<select v-model="connection.client" class="form-select">
<option
v-for="client in clients"
:key="client.slug"
:value="client.slug"
>
{{ client.name }}
</option>
</select>
<BaseSelect
v-model="connection.client"
:options="clients"
option-track-by="slug"
option-label="name"
class="form-select"
/>
</div>
</div>
<div v-if="connection.client === 'pg'" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.connectionString') }}</label>
<label class="form-label cut-text">{{ $t('word.connectionString') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -76,7 +74,7 @@
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -88,11 +86,11 @@
</div>
<div v-if="customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
<label class="form-label cut-text">{{ $t('word.database') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="connection.databasePath"
:model-value="connection.databasePath"
:message="$t('word.browse')"
@clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')"
@@ -101,7 +99,7 @@
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
<label class="form-label cut-text">{{ $t('word.port') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -115,7 +113,7 @@
</div>
<div v-if="customizations.database" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
<label class="form-label cut-text">{{ $t('word.database') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -127,7 +125,7 @@
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
<label class="form-label cut-text">{{ $t('word.user') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -140,7 +138,7 @@
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
<label class="form-label cut-text">{{ $t('word.password') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -153,7 +151,7 @@
</div>
<div v-if="customizations.connectionSchema" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.schema') }}</label>
<label class="form-label cut-text">{{ $t('word.schema') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -189,7 +187,7 @@
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">
<label class="form-label cut-text">
{{ $t('message.enableSsl') }}
</label>
</div>
@@ -203,11 +201,11 @@
<fieldset class="m-0" :disabled="isBusy || !connection.ssl">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.privateKey') }}</label>
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="connection.key"
:model-value="connection.key"
:message="$t('word.browse')"
@clear="pathClear('key')"
@change="pathSelection($event, 'key')"
@@ -216,11 +214,11 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.certificate') }}</label>
<label class="form-label cut-text">{{ $t('word.certificate') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="connection.cert"
:model-value="connection.cert"
:message="$t('word.browse')"
@clear="pathClear('cert')"
@change="pathSelection($event, 'cert')"
@@ -229,11 +227,11 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.caCertificate') }}</label>
<label class="form-label cut-text">{{ $t('word.caCertificate') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="connection.ca"
:model-value="connection.ca"
:message="$t('word.browse')"
@clear="pathClear('ca')"
@change="pathSelection($event, 'ca')"
@@ -242,7 +240,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.ciphers') }}</label>
<label class="form-label cut-text">{{ $t('word.ciphers') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -253,6 +251,14 @@
>
</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="connection.untrustedConnection" type="checkbox"><i class="form-icon" /> {{ $t('message.untrustedConnection') }}
</label>
</div>
</div>
</fieldset>
</form>
</div>
@@ -262,7 +268,7 @@
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">
<label class="form-label cut-text">
{{ $t('message.enableSsh') }}
</label>
</div>
@@ -276,7 +282,7 @@
<fieldset class="m-0" :disabled="isBusy || !connection.ssh">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -288,7 +294,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
<label class="form-label cut-text">{{ $t('word.user') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -300,7 +306,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
<label class="form-label cut-text">{{ $t('word.password') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -312,7 +318,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
<label class="form-label cut-text">{{ $t('word.port') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -326,11 +332,11 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.privateKey') }}</label>
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="connection.sshKey"
:model-value="connection.sshKey"
:message="$t('word.browse')"
@clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')"
@@ -339,7 +345,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.passphrase') }}</label>
<label class="form-label cut-text">{{ $t('word.passphrase') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -355,6 +361,7 @@
</div>
<div class="panel-footer">
<button
id="connection-test"
class="btn btn-gray mr-2 d-flex"
:class="{'loading': isTesting}"
:disabled="isBusy"
@@ -364,6 +371,7 @@
{{ $t('message.testConnection') }}
</button>
<button
id="connection-save"
class="btn btn-primary mr-2 d-flex"
:disabled="isBusy"
@click="saveConnection"
@@ -382,18 +390,36 @@
</template>
<script>
import { mapActions } from 'vuex';
import customizations from 'common/customizations';
import Connection from '@/ipc-api/Connection';
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 BaseUploadInput from '@/components/BaseUploadInput';
import BaseSelect from '@/components/BaseSelect.vue';
export default {
name: 'WorkspaceAddConnectionPanel',
components: {
ModalAskCredentials,
BaseUploadInput
BaseUploadInput,
BaseSelect
},
setup () {
const { addConnection } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { connectWorkspace, selectWorkspace } = workspacesStore;
return {
addConnection,
addNotification,
connectWorkspace,
selectWorkspace
};
},
data () {
return {
@@ -420,6 +446,7 @@ export default {
key: '',
ca: '',
ciphers: '',
untrustedConnection: false,
ssh: false,
sshHost: '',
sshUser: '',
@@ -457,12 +484,6 @@ export default {
}, 20);
},
methods: {
...mapActions({
addConnection: 'connections/addConnection',
connectWorkspace: 'workspaces/connectWorkspace',
addNotification: 'notifications/addNotification',
selectWorkspace: 'workspaces/selectWorkspace'
}),
setDefaults () {
this.connection.user = this.customizations.defaultUser;
this.connection.port = this.customizations.defaultPort;
@@ -554,12 +575,12 @@ export default {
<style lang="scss" scoped>
.connection-panel {
margin-top: 15vh;
margin-left: auto;
margin-right: auto;
margin-bottom: 1rem;
.panel {
width: 450px;
min-width: 450px;
border-radius: $border-radius;
.panel-body {

View File

@@ -34,7 +34,7 @@
<fieldset class="m-0" :disabled="isBusy">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.connectionName') }}</label>
<label class="form-label cut-text">{{ $t('word.connectionName') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -47,23 +47,23 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.client') }}</label>
<label class="form-label cut-text">{{ $t('word.client') }}</label>
</div>
<div class="column col-8 col-sm-12">
<select v-model="localConnection.client" class="form-select">
<option
v-for="client in clients"
:key="client.slug"
:value="client.slug"
>
{{ client.name }}
</option>
</select>
<BaseSelect
v-model="localConnection.client"
:options="clients"
option-track-by="slug"
option-label="name"
class="form-select"
dropdown-container=".workspace .connection-panel-wrapper"
:dropdown-offsets="{top: 10}"
/>
</div>
</div>
<div v-if="connection.client === 'pg'" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.connectionString') }}</label>
<label class="form-label cut-text">{{ $t('word.connectionString') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -76,7 +76,7 @@
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -88,11 +88,11 @@
</div>
<div v-if="customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
<label class="form-label cut-text">{{ $t('word.database') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="localConnection.databasePath"
:model-value="localConnection.databasePath"
:message="$t('word.browse')"
@clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')"
@@ -101,7 +101,7 @@
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
<label class="form-label cut-text">{{ $t('word.port') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -115,7 +115,7 @@
</div>
<div v-if="customizations.database" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
<label class="form-label cut-text">{{ $t('word.database') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -127,7 +127,7 @@
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
<label class="form-label cut-text">{{ $t('word.user') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -140,7 +140,7 @@
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
<label class="form-label cut-text">{{ $t('word.password') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -153,7 +153,7 @@
</div>
<div v-if="customizations.connectionSchema" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.schema') }}</label>
<label class="form-label cut-text">{{ $t('word.schema') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -189,7 +189,7 @@
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">
<label class="form-label cut-text">
{{ $t('message.enableSsl') }}
</label>
</div>
@@ -203,11 +203,11 @@
<fieldset class="m-0" :disabled="isBusy || !localConnection.ssl">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.privateKey') }}</label>
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="localConnection.key"
:model-value="localConnection.key"
:message="$t('word.browse')"
@clear="pathClear('key')"
@change="pathSelection($event, 'key')"
@@ -216,11 +216,11 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.certificate') }}</label>
<label class="form-label cut-text">{{ $t('word.certificate') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="localConnection.cert"
:model-value="localConnection.cert"
:message="$t('word.browse')"
@clear="pathClear('cert')"
@change="pathSelection($event, 'cert')"
@@ -229,11 +229,11 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.caCertificate') }}</label>
<label class="form-label cut-text">{{ $t('word.caCertificate') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="localConnection.ca"
:model-value="localConnection.ca"
:message="$t('word.browse')"
@clear="pathClear('ca')"
@change="pathSelection($event, 'ca')"
@@ -242,7 +242,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.ciphers') }}</label>
<label class="form-label cut-text">{{ $t('word.ciphers') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -262,7 +262,7 @@
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">
<label class="form-label cut-text">
{{ $t('message.enableSsh') }}
</label>
</div>
@@ -276,7 +276,7 @@
<fieldset class="m-0" :disabled="isBusy || !localConnection.ssh">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -288,7 +288,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
<label class="form-label cut-text">{{ $t('word.user') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -300,7 +300,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
<label class="form-label cut-text">{{ $t('word.password') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -312,7 +312,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
<label class="form-label cut-text">{{ $t('word.port') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -326,11 +326,11 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.privateKey') }}</label>
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="localConnection.sshKey"
:model-value="localConnection.sshKey"
:message="$t('word.browse')"
@clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')"
@@ -339,7 +339,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.passphrase') }}</label>
<label class="form-label cut-text">{{ $t('word.passphrase') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -355,6 +355,7 @@
</div>
<div class="panel-footer">
<button
id="connection-test"
class="btn btn-gray mr-2 d-flex"
:class="{'loading': isTesting}"
:disabled="isBusy"
@@ -364,6 +365,7 @@
{{ $t('message.testConnection') }}
</button>
<button
id="connection-save"
class="btn btn-primary mr-2 d-flex"
:disabled="isBusy || !hasChanges"
@click="saveConnection"
@@ -372,6 +374,7 @@
{{ $t('word.save') }}
</button>
<button
id="connection-connect"
class="btn btn-success d-flex"
:class="{'loading': isConnecting}"
:disabled="isBusy"
@@ -391,21 +394,36 @@
</template>
<script>
import { mapActions } from 'vuex';
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 ModalAskCredentials from '@/components/ModalAskCredentials';
import BaseUploadInput from '@/components/BaseUploadInput';
import BaseSelect from '@/components/BaseSelect.vue';
export default {
name: 'WorkspaceEditConnectionPanel',
components: {
ModalAskCredentials,
BaseUploadInput
BaseUploadInput,
BaseSelect
},
props: {
connection: Object
},
setup () {
const { editConnection } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const { connectWorkspace } = useWorkspacesStore();
return {
editConnection,
addNotification,
connectWorkspace
};
},
data () {
return {
clients: [
@@ -441,11 +459,6 @@ export default {
this.localConnection = JSON.parse(JSON.stringify(this.connection));
},
methods: {
...mapActions({
editConnection: 'connections/editConnection',
connectWorkspace: 'workspaces/connectWorkspace',
addNotification: 'notifications/addNotification'
}),
async startConnection () {
await this.saveConnection();
this.isConnecting = true;
@@ -532,12 +545,12 @@ export default {
<style lang="scss" scoped>
.connection-panel {
margin-top: 15vh;
margin-left: auto;
margin-right: auto;
margin-bottom: 1rem;
.panel {
width: 450px;
min-width: 450px;
border-radius: $border-radius;
.panel-body {

View File

@@ -25,26 +25,35 @@
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces';
import { storeToRefs } from 'pinia';
export default {
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: {
...mapGetters({
applicationTheme: 'settings/getApplicationTheme',
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected'
}),
workspace () {
return this.getWorkspace(this.selectedWorkspace);
}
},
created () {
this.changeBreadcrumbs({ schema: this.workspace.breadcrumbs.schema });
},
methods: {
...mapActions({
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
})
}
};
</script>

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