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

Compare commits

...

91 Commits

Author SHA1 Message Date
3c1bae540f chore(release): 0.5.13 2022-08-09 09:13:11 +02:00
f64a12a8e9 fix(MySQL): error with ANSI sql_mode 2022-08-07 19:00:12 +02:00
a9fcfd57ec refactor: improved vue-i18n implementation 2022-08-05 17:03:16 +02:00
e2307341f3 Merge pull request #397 from antares-sql/dependabot/npm_and_yarn/vue-i18n-9.2.0
build(deps): bump vue-i18n from 9.1.10 to 9.2.0
2022-08-05 13:17:06 +02:00
09a372e96d refactor: vue-i18n ts improvements 2022-08-05 13:06:08 +02:00
f4da28cca0 refactor: vue-i18n ts improvements 2022-08-05 12:57:56 +02:00
89745b7391 Merge pull request #398 from antares-sql/dependabot/npm_and_yarn/mdi/font-7.0.96
build(deps): bump @mdi/font from 6.9.96 to 7.0.96
2022-08-05 12:26:44 +02:00
104b7c928b fix: set legacy: false 2022-08-05 12:25:14 +02:00
dependabot[bot]
427360d826 build(deps): bump sql-formatter from 4.0.2 to 8.2.0
Bumps [sql-formatter](https://github.com/sql-formatter-org/sql-formatter) from 4.0.2 to 8.2.0.
- [Release notes](https://github.com/sql-formatter-org/sql-formatter/releases)
- [Changelog](https://github.com/sql-formatter-org/sql-formatter/blob/master/.release-it.json)
- [Commits](https://github.com/sql-formatter-org/sql-formatter/compare/v4.0.2...v8.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-05 12:18:53 +02:00
Giulio Ganci
0bfa14e1c9 feat: new macos icon 2022-08-03 12:57:17 +02:00
88ba55ec02 build: added deb arm targets 2022-08-03 11:33:26 +02:00
aaff4cf4fe refactor: filter functions as composable 2022-08-03 11:10:16 +02:00
35c54aee84 chore: update README.md 2022-08-02 13:17:13 +02:00
be2e9b21f5 chore: update dependabot.yml 2022-08-02 10:55:19 +02:00
2262278393 Merge branch 'master' of https://github.com/antares-sql/antares 2022-08-02 10:51:43 +02:00
531e17889a chore: update dependabot.yml 2022-08-02 10:51:40 +02:00
a07ed58004 chore: update CONTRIBUTING.md 2022-08-02 10:41:05 +02:00
00dc59a76d ci: remove old ci configs 2022-08-02 10:37:27 +02:00
2f883bfeb2 ci: new ci config 2022-08-02 10:10:20 +02:00
7ff16fccce ci: minor change 2022-08-01 22:32:19 +02:00
dependabot[bot]
3625fbc1b0 build(deps): bump @mdi/font from 6.9.96 to 7.0.96
Bumps [@mdi/font](https://github.com/Templarian/MaterialDesign-Webfont) from 6.9.96 to 7.0.96.
- [Release notes](https://github.com/Templarian/MaterialDesign-Webfont/releases)
- [Commits](https://github.com/Templarian/MaterialDesign-Webfont/compare/v6.9.96...v7.0.96)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-01 19:10:35 +00:00
dependabot[bot]
deee0d637b build(deps): bump vue-i18n from 9.1.10 to 9.2.0
Bumps [vue-i18n](https://github.com/intlify/vue-i18n-next/tree/HEAD/packages/vue-i18n) from 9.1.10 to 9.2.0.
- [Release notes](https://github.com/intlify/vue-i18n-next/releases)
- [Changelog](https://github.com/intlify/vue-i18n-next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/intlify/vue-i18n-next/commits/v9.2.0/packages/vue-i18n)

---
updated-dependencies:
- dependency-name: vue-i18n
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-01 19:09:53 +00:00
8c6950cebd refactor: minor refactors 2022-08-01 18:13:14 +02:00
46167e4473 build: general ci config update 2022-08-01 18:06:57 +02:00
c32a4415d1 Update README.md 2022-07-31 10:43:32 +02:00
1c3d7aa30b feat: copy row as CSV, closes #394 2022-07-29 18:53:39 +02:00
664d18efc1 chore: update README.md 2022-07-26 17:21:43 +02:00
cc941dfc04 chore(release): 0.5.12 2022-07-26 14:59:06 +02:00
1d151e9349 fix: error on schema export 2022-07-26 13:33:57 +02:00
addd9fba28 build(deps): bump ace-builds from 1.4.14 to 1.8.1 2022-07-25 15:25:58 +02:00
a00c19d300 fix: prevent ctrl+a in console 2022-07-25 15:20:15 +02:00
9551afbd2d feat: ability to copy multiple selected rows 2022-07-25 14:56:00 +02:00
1ead76c028 fix: missing defaults on insert row window 2022-07-25 13:09:41 +02:00
d3da15aa13 feat: copy row as SQL INSERT 2022-07-25 12:42:22 +02:00
f3b5de38c4 feat: export table content as SQL INSERT 2022-07-25 12:19:58 +02:00
Askar Kanturin
d4b6d2e9d1 changed readme 2022-07-23 10:20:36 +02:00
Askar Kanturin
e2c106e4e0 fixed typo in readme
i'm no native speaker, but i feel this is a typo
2022-07-23 10:20:36 +02:00
eb60899e6e fix(MySQL): missing quoted identifier for column names in table filter, closes #380 2022-07-22 10:08:33 +02:00
1d367d468d Merge branch 'master' of https://github.com/antares-sql/antares 2022-07-21 11:01:13 +02:00
8ecaedbf6c fix: disable ctrl+alt+(left/right) shortcut on linux 2022-07-21 11:01:10 +02:00
dependabot[bot]
dd1eebd4ec build(deps): bump terser from 5.13.1 to 5.14.2
Bumps [terser](https://github.com/terser/terser) from 5.13.1 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-21 08:43:24 +02:00
8c83b3f144 fix: missing table on insert new records on session restored tabs 2022-07-20 10:41:44 +02:00
985e5d3527 feat: context menu option to duplicate a table row 2022-07-19 17:48:51 +02:00
78902639eb feat: execute selected query 2022-07-19 15:02:17 +02:00
cb038b374a fix: issue with logger on import/export 2022-07-19 10:10:24 +02:00
eafdb1cc3d chore(release): 0.5.11 2022-07-19 08:14:47 +02:00
91e0630513 fix: unable to edit table fields content on tables with datetime fields 2022-07-19 08:08:51 +02:00
bf768c3800 fix: filter persists switching temporary table tabs 2022-07-18 18:29:12 +02:00
0b1aa3dd29 fix: console events disabled in production 2022-07-18 17:21:34 +02:00
ec75f9546a chore(release): 0.5.10 2022-07-18 14:44:30 +02:00
9bc9adb7cf fix: exception on QueryEditor with null modelValue 2022-07-18 14:43:38 +02:00
b4d14d98db refactor: improved console with light theme 2022-07-18 11:58:22 +02:00
c21bd6075c feat: context menu to copy queries from console 2022-07-18 11:58:22 +02:00
44647f5b55 feat: open/close console on single connection 2022-07-18 11:58:22 +02:00
3f9e6d85ca perf: improved resize of text editor resizing console height 2022-07-18 11:58:22 +02:00
6a6f43a718 feat: initial console implementation 2022-07-18 11:58:22 +02:00
f12a04b052 feat: ipc event channel to send logs to renderer 2022-07-18 11:58:22 +02:00
abf829867e feat: Ctrl+PgUp & Ctrl+PgDn to navigate between tabs 2022-07-14 19:30:34 +02:00
b71f04e5aa feat: field names suggestion for tables in the query 2022-07-14 16:09:04 +02:00
7725fafe85 fix: unable to delete by select all in left bar search, closes #368 2022-07-13 18:50:16 +02:00
ed3d35f131 fix(Linux): ctrl+space shortcut not working 2022-07-13 18:25:42 +02:00
e0946f04f7 fix: unable to update data on tables missing primary or unique key 2022-07-13 09:30:30 +02:00
0891e7be8c refactor: disabled autofocus for scheduler timing modal 2022-07-11 11:35:30 +02:00
f312cf5f85 perf(UI): improved visibility of explore bar tooltips 2022-07-11 09:56:33 +02:00
05e0d310ec Merge pull request #363 from goYou/master
feat: Update zh-CN.ts
2022-07-10 09:13:35 +02:00
goYou
1e0b2b4cae Update zh-CN.ts 2022-07-10 14:49:30 +08:00
5ee728cfe4 Merge pull request #361 from antares-sql/dependabot/npm_and_yarn/moment-2.29.4
build(deps): bump moment from 2.29.3 to 2.29.4
2022-07-09 12:41:20 +02:00
a91fa8ff54 fix: fields content language detection not working properly 2022-07-09 12:39:44 +02:00
dependabot[bot]
2c13433900 build(deps): bump moment from 2.29.3 to 2.29.4
Bumps [moment](https://github.com/moment/moment) from 2.29.3 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.3...2.29.4)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-09 10:38:14 +00:00
dcc2a4c51c refactor: replaced @vscode/vscode-languagedetection with custom language detection 2022-07-09 12:37:30 +02:00
7537dff401 chore(release): 0.5.9 2022-07-06 14:42:32 +02:00
853ee1f572 revert: revert to better-sqlite 7.5.1 due build issues 2022-07-06 10:43:32 +02:00
cf9c7c600a fix: error on export schema 2022-07-06 10:26:24 +02:00
73b88cad9e Merge branch 'master' of https://github.com/antares-sql/antares 2022-07-06 09:37:21 +02:00
d2eb31a63d perf(UI): improved focus visibility for buttons 2022-07-06 09:37:18 +02:00
d6b36b1f80 Merge pull request #353 from antares-sql/dependabot/npm_and_yarn/better-sqlite3-7.5.3
build(deps): bump better-sqlite3 from 7.5.1 to 7.5.3
2022-07-05 18:20:56 +02:00
dependabot[bot]
d66b932683 build(deps): bump better-sqlite3 from 7.5.1 to 7.5.3
Bumps [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) from 7.5.1 to 7.5.3.
- [Release notes](https://github.com/WiseLibs/better-sqlite3/releases)
- [Commits](https://github.com/WiseLibs/better-sqlite3/compare/v7.5.1...v7.5.3)

---
updated-dependencies:
- dependency-name: better-sqlite3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-05 06:59:47 +00:00
20f5497034 Merge pull request #341 from antares-sql/dependabot/npm_and_yarn/vue-3.2.37
build(deps): bump vue from 3.2.33 to 3.2.37
2022-07-05 08:54:40 +02:00
d4ed886489 Merge pull request #339 from antares-sql/dependabot/npm_and_yarn/mdi/font-6.9.96
build(deps): bump @mdi/font from 6.1.95 to 6.9.96
2022-07-05 08:54:30 +02:00
dependabot[bot]
aaa14f112a build(deps): bump vue from 3.2.33 to 3.2.37
Bumps [vue](https://github.com/vuejs/core) from 3.2.33 to 3.2.37.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.2.33...v3.2.37)

---
updated-dependencies:
- dependency-name: vue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-04 15:41:08 +00:00
dependabot[bot]
e0f3ff6939 build(deps): bump @mdi/font from 6.1.95 to 6.9.96
Bumps [@mdi/font](https://github.com/Templarian/MaterialDesign-Webfont) from 6.1.95 to 6.9.96.
- [Release notes](https://github.com/Templarian/MaterialDesign-Webfont/releases)
- [Commits](https://github.com/Templarian/MaterialDesign-Webfont/compare/v6.1.95...v6.9.96)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-04 15:41:01 +00:00
72a328785c Merge pull request #333 from antares-sql/new-connection-management
New connections management
2022-07-04 17:39:56 +02:00
4c2a1998a9 Merge branch 'master' of https://github.com/antares-sql/antares into new-connection-management 2022-07-04 17:25:37 +02:00
8e705706ae feat: ability to pin/unpin and delete connections from the "all connections" modal 2022-07-04 17:03:24 +02:00
56b0a4815c feat: option to disable scratchpad 2022-07-04 15:10:40 +02:00
a9a4344a71 feat: ctrl/cmd+space to open all connections modal 2022-07-04 12:42:33 +02:00
ec5ab73b19 feat: search form in all connections modal 2022-07-04 12:27:04 +02:00
71a5b5c828 fix: missing option for untrusted ssl connection on connections edit panel 2022-07-03 22:07:28 +02:00
a703dcc53e feat: modal with all connections 2022-07-02 15:30:36 +02:00
36e98e0742 feat: connections sorted by last usage by default and option to pin them 2022-06-30 18:20:14 +02:00
f632a0f189 initial implementation of new feature 2022-06-29 19:05:45 +02:00
111 changed files with 8398 additions and 6882 deletions

View File

@@ -6,6 +6,8 @@
version: 2
updates:
- package-ecosystem: "npm"
allow:
- dependency-type: "production"
directory: "/"
schedule:
interval: "monthly"

View File

@@ -1,29 +0,0 @@
name: Build/release [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: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:
github_token: ${{ secrets.github_token }}
release: ${{ startsWith(github.ref, 'refs/tags/v') }}

View File

@@ -1,29 +0,0 @@
name: Build/release [MAC]
on: push
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-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: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:
github_token: ${{ secrets.github_token }}
release: ${{ startsWith(github.ref, 'refs/tags/v') }}

View File

@@ -1,29 +0,0 @@
name: Build/release [WINDOWS]
on: push
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-2019]
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: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:
github_token: ${{ secrets.github_token }}
release: ${{ startsWith(github.ref, 'refs/tags/v') }}

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

@@ -0,0 +1,37 @@
name: Build & release
on:
push:
tags:
- "v*"
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 16
- name: Install dependencies
run: npm i
- name: "Build"
run: npm run build
- name: Release
uses: ncipollo/release-action@v1
with:
artifacts: "build/*.AppImage,build/*.yml,build/*.deb,build/*.dmg,build/*.blockmap,build/*.zip,build/*.exe"
allowUpdates: true
draft: true
generateReleaseNotes: true

View File

@@ -10,10 +10,15 @@ jobs:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 16
- name: npm install & build
run: |
npm install
npm run build:local
npm run build
- name: Upload Artifact
uses: actions/upload-artifact@v3

View File

@@ -12,12 +12,12 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 14
node-version: 16
- name: Install dependencies
run: npm i

1
.nvmrc
View File

@@ -1 +0,0 @@
v16.13.0

View File

@@ -2,6 +2,101 @@
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.13](https://github.com/antares-sql/antares/compare/v0.5.12...v0.5.13) (2022-08-09)
### Features
* copy row as CSV, closes [#394](https://github.com/antares-sql/antares/issues/394) ([1c3d7aa](https://github.com/antares-sql/antares/commit/1c3d7aa30bb9c2bd900a764ee6b97960729e9263))
* new macos icon ([0bfa14e](https://github.com/antares-sql/antares/commit/0bfa14e1c90320578597df030941530b670a4131))
### Bug Fixes
* **MySQL:** error with ANSI sql_mode ([f64a12a](https://github.com/antares-sql/antares/commit/f64a12a8e9c5f764c3a692f1a032736e008058b5))
* set legacy: false ([104b7c9](https://github.com/antares-sql/antares/commit/104b7c928b9c2abfc056880f16c606a0b1fa7c67))
### [0.5.12](https://github.com/antares-sql/antares/compare/v0.5.11...v0.5.12) (2022-07-26)
### Features
* ability to copy multiple selected rows ([9551afb](https://github.com/antares-sql/antares/commit/9551afbd2d7e525c81f28e98e788b92609ce9de4))
* context menu option to duplicate a table row ([985e5d3](https://github.com/antares-sql/antares/commit/985e5d352793d1b3e1981d004b6f494bfbb049bf))
* copy row as SQL INSERT ([d3da15a](https://github.com/antares-sql/antares/commit/d3da15aa1377dcba73927047563f1d0c2d1284ca))
* execute selected query ([7890263](https://github.com/antares-sql/antares/commit/78902639ebb29a8c53f8aa0d2045c74e0646febc))
* export table content as SQL INSERT ([f3b5de3](https://github.com/antares-sql/antares/commit/f3b5de38c4abfd2c1d738e179fc22e6c8b6f9080))
### Bug Fixes
* disable ctrl+alt+(left/right) shortcut on linux ([8ecaedb](https://github.com/antares-sql/antares/commit/8ecaedbf6c2fc0dc56ff2177a87dd6ede74bdd22))
* error on schema export ([1d151e9](https://github.com/antares-sql/antares/commit/1d151e9349fd97576ccd8ef7f88ca789a1f28b65))
* issue with logger on import/export ([cb038b3](https://github.com/antares-sql/antares/commit/cb038b374a4fe85ad569e42eee7af123c925e775))
* missing defaults on insert row window ([1ead76c](https://github.com/antares-sql/antares/commit/1ead76c02889f48bd91cae702820b082ca2ff54b))
* missing table on insert new records on session restored tabs ([8c83b3f](https://github.com/antares-sql/antares/commit/8c83b3f1447354ec63b2a308db05ad4d54659aa7))
* **MySQL:** missing quoted identifier for column names in table filter, closes [#380](https://github.com/antares-sql/antares/issues/380) ([eb60899](https://github.com/antares-sql/antares/commit/eb60899e6e17879c79a7ee7108061e9aca8596f7))
* prevent ctrl+a in console ([a00c19d](https://github.com/antares-sql/antares/commit/a00c19d3003cd43d3ee6e3132728122bb2b24c97))
### [0.5.11](https://github.com/antares-sql/antares/compare/v0.5.10...v0.5.11) (2022-07-19)
### Bug Fixes
* console events disabled in production ([0b1aa3d](https://github.com/antares-sql/antares/commit/0b1aa3dd299db641df3d4c56c7ee56a187fc3ab3))
* filter persists switching temporary table tabs ([bf768c3](https://github.com/antares-sql/antares/commit/bf768c380087b65604b5b571a9858a7f07bd681d))
* unable to edit table fields content on tables with datetime fields ([91e0630](https://github.com/antares-sql/antares/commit/91e06305133c97ea02dcfdc4e739a4b0a7e7049d))
### [0.5.10](https://github.com/antares-sql/antares/compare/v0.5.9...v0.5.10) (2022-07-18)
### Features
* context menu to copy queries from console ([c21bd60](https://github.com/antares-sql/antares/commit/c21bd6075c1203607c05e45b76233d57e3008190))
* Ctrl+PgUp & Ctrl+PgDn to navigate between tabs ([abf8298](https://github.com/antares-sql/antares/commit/abf829867e567354e534cff3e02a6d43f4c7a262))
* field names suggestion for tables in the query ([b71f04e](https://github.com/antares-sql/antares/commit/b71f04e5aa3c37eaa160dfbc76d1b84789e3543e))
* initial console implementation ([6a6f43a](https://github.com/antares-sql/antares/commit/6a6f43a718561e0abd2cb89048b7fe45d08736ae))
* ipc event channel to send logs to renderer ([f12a04b](https://github.com/antares-sql/antares/commit/f12a04b0524f1172334c89afeb27675c19ff68d2))
* open/close console on single connection ([44647f5](https://github.com/antares-sql/antares/commit/44647f5b5508965bf5a7264add89e175c725e877))
### Bug Fixes
* exception on QueryEditor with null modelValue ([9bc9adb](https://github.com/antares-sql/antares/commit/9bc9adb7cff19b86a99491d968485a4cd7b47b99))
* fields content language detection not working properly ([a91fa8f](https://github.com/antares-sql/antares/commit/a91fa8ff54bbf1f8475666efd3a268a3a4f07f0c))
* **Linux:** ctrl+space shortcut not working ([ed3d35f](https://github.com/antares-sql/antares/commit/ed3d35f1319a1e2edcb8104f2045a71b9e9754a2))
* unable to delete by select all in left bar search, closes [#368](https://github.com/antares-sql/antares/issues/368) ([7725faf](https://github.com/antares-sql/antares/commit/7725fafe852479720fa619ced0970f2fa0099191))
* unable to update data on tables missing primary or unique key ([e0946f0](https://github.com/antares-sql/antares/commit/e0946f04f792d25c187ea56d4714bdacc016ada3))
### Improvements
* improved resize of text editor resizing console height ([3f9e6d8](https://github.com/antares-sql/antares/commit/3f9e6d85ca445eea1028effa32418eee4980f87d))
* **UI:** improved visibility of explore bar tooltips ([f312cf5](https://github.com/antares-sql/antares/commit/f312cf5f855deddd562c26d1835f78d16499b93b))
### [0.5.9](https://github.com/antares-sql/antares/compare/v0.5.8...v0.5.9) (2022-07-06)
### Features
* ability to pin/unpin and delete connections from the "all connections" modal ([8e70570](https://github.com/antares-sql/antares/commit/8e705706aecc5c9790329e63e61a1c02fa5d0342))
* connections sorted by last usage by default and option to pin them ([36e98e0](https://github.com/antares-sql/antares/commit/36e98e0742657e25df7768aa5b3b7cb350df5509))
* ctrl/cmd+space to open all connections modal ([a9a4344](https://github.com/antares-sql/antares/commit/a9a4344a71cc0f8f156b839733f6ddc200a26268))
* modal with all connections ([a703dcc](https://github.com/antares-sql/antares/commit/a703dcc53eb920117bc346a3c21f0c729c0ad96d))
* option to disable scratchpad ([56b0a48](https://github.com/antares-sql/antares/commit/56b0a4815c6f54eef164d849f6ca25af1e142b16))
* search form in all connections modal ([ec5ab73](https://github.com/antares-sql/antares/commit/ec5ab73b19d99e9971ae87e5f0a8d1bd1c34ef00))
### Bug Fixes
* error on export schema ([cf9c7c6](https://github.com/antares-sql/antares/commit/cf9c7c600aa915cef1ec3777866badb7ab1312ee))
* missing option for untrusted ssl connection on connections edit panel ([71a5b5c](https://github.com/antares-sql/antares/commit/71a5b5c8285fb777c43e7f6516006bfe9f52591c))
### Improvements
* **UI:** improved focus visibility for buttons ([d2eb31a](https://github.com/antares-sql/antares/commit/d2eb31a63d612323f8738eded1e1ce7b23554001))
### [0.5.8](https://github.com/antares-sql/antares/compare/v0.5.7...v0.5.8) (2022-07-02)

View File

@@ -44,8 +44,7 @@ In this folder is located the structure of Vue frontend application.
## Build
The command to build Antares SQL locally is `npm run build:local`.
`build` command (without `:local`) is used exclusively by the GitHub Action.
The command to build Antares SQL locally is `npm run build`.
## Conventions

View File

@@ -13,12 +13,13 @@ Antares is an SQL client based on [Electron.js](https://github.com/electron/elec
Our target is to support as many databases as possible, and all major operating systems, including the ARM versions.
**At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB, PostgreSQL and SQLite.
At the moment, however, there are all the features necessary to have a pleasant database management experience, so give it a chance and send us your feedback, we would really appreciate it.
However, there are all the features necessary to have a pleasant database management experience, so give it a chance and send us your feedback, we would really appreciate it.
We are actively working on it, hoping to provide new cool features, improvements and fixes as soon as possible.
🔗 If you are curious to try Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases/latest).
👁 To stay tuned for new releases [follow Antares SQL](https://twitter.com/AntaresSQL) on Twitter.
🌟 Don't forget to **leave a star** if you appreciate this project.
🌟 Don't forget to **leave a star** if you appreciate this project.
🗳️ Poll: **[Which is the main OS you use Antares on?](https://github.com/antares-sql/antares/discussions/379)**
## Current key features
@@ -40,20 +41,20 @@ We are actively working on it, hoping to provide new cool features, improvements
Why are we developing an SQL client when there are a lot of them on the market?
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.
A modern application created with minimalism and simplicity in mind, with features in the right places, not hundreds of tiny buttons, nested tabs or submenues; productivity comes first.
## Installation
Based on your operating system you can have one or more distribution formats to choose based on your preferences.
Since Antares SQL is a free software we haven't a budget to spend in annual licenses or certificates. This can result that on some platforms you need some additional passages to install this app.
Since Antares SQL is a free software we don't have a budget to spend on annual licenses or certificates. This can result that on some platforms you might need to put in some additional work to install this app.
### Linux
On Linux you can simply download and run `.AppImage` distributions, install from Snap Store or from AUR.
On Linux you can simply download and run the `.AppImage` distribution, install from Snap Store, from AUR or from our [PPA repository](https://github.com/antares-sql/antares-ppa).
### Windows
On Windows you can choose between Microsoft Store and download `.exe` distribution. The latter lacks of a certificate, so to install you need to click on "More info" and then "Run anyway" on SmartScreen prompt.
On Windows you can choose between downloading the app from Microsoft Store or downloading the `.exe` from our [website](https://antares-sql.app/downloads) or [this github repo](https://github.com/Fabio286/antares/releases/latest). Distributions that are not from Microsoft Store are not signed with a certificate, so to install you need to click on "More info" and then "Run anyway" on SmartScreen prompt.
### MacOS
@@ -61,7 +62,7 @@ On macOS you can run `.dmg` distribution following [this guide](https://support.
## Download
[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/antares) [![Get it from AUR](https://raw.githubusercontent.com/Fabio286/antares/3e00c4bae6e036300c752c1a40c5a038fea9c169/docs/aur-badge.svg)](https://aur.archlinux.org/packages/antares-sql/) [![Get it from Microsoft Store](https://raw.githubusercontent.com/Fabio286/antares/gh-pages/src/assets/ms-store.png)](https://www.microsoft.com/p/antares-sql-client/9nhtb9sq51r1?cid=storebadge&ocid=badge&rtc=1&activetab=pivot:overviewtab)
[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/antares) [![Get it from AUR](https://raw.githubusercontent.com/Fabio286/antares/3e00c4bae6e036300c752c1a40c5a038fea9c169/docs/aur-badge.svg)](https://aur.archlinux.org/packages/antares-sql/) [<img src="https://developer.microsoft.com/store/badges/images/English_get-it-from-MS.png" style="height: 56px">](https://www.microsoft.com/p/antares-sql-client/9nhtb9sq51r1?cid=storebadge&ocid=badge&rtc=1&activetab=pivot:overviewtab)
🚀 **[Other Downloads](https://github.com/Fabio286/antares/releases/latest)**
## Coming soon

Binary file not shown.

11651
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.5.8",
"version": "0.5.13",
"description": "A modern, fast and productivity driven SQL client with a focus in UX.",
"license": "MIT",
"repository": "https://github.com/antares-sql/antares.git",
@@ -12,14 +12,13 @@
"compile:main": "webpack --mode=production --config webpack.main.config.js",
"compile:workers": "webpack --mode=production --config webpack.workers.config.js",
"compile:renderer": "webpack --mode=production --config webpack.renderer.config.js",
"build": "cross-env NODE_ENV=production npm run compile",
"build:local": "npm run build && electron-builder --publish never",
"build:appx": "npm run build:local -- --win appx",
"rebuild:electron": "rimraf ./dist && npm run postinstall",
"build": "cross-env NODE_ENV=production npm run compile && electron-builder --publish never",
"build:appx": "npm run build -- --win appx",
"rebuild:electron": "rimraf ./dist && npm run postinstall && npm run devtools:install",
"release": "standard-version",
"release:pre": "npm run release -- --prerelease alpha",
"devtools:install": "node scripts/devtoolsInstaller",
"postinstall": "electron-builder install-app-deps && npm run devtools:install",
"postinstall": "electron-builder install-app-deps",
"test:e2e": "npm run compile && npm run test:e2e-dry",
"test:e2e-dry": "xvfb-maybe -- playwright test",
"lint": "eslint . --ext .js,.ts,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"",
@@ -65,7 +64,11 @@
"target": [
{
"target": "deb",
"arch": "x64"
"arch": [
"x64",
"armv7l",
"arm64"
]
},
{
"target": "AppImage",
@@ -117,11 +120,11 @@
"dependencies": {
"@electron/remote": "~2.0.1",
"@faker-js/faker": "~6.1.2",
"@mdi/font": "~6.1.95",
"@mdi/font": "~7.0.96",
"@turf/helpers": "~6.5.0",
"@vscode/vscode-languagedetection": "~1.0.21",
"ace-builds": "~1.4.13",
"better-sqlite3": "~7.5.0",
"@vueuse/core": "~8.7.5",
"ace-builds": "~1.8.1",
"better-sqlite3": "~7.5.1",
"electron-log": "~4.4.1",
"electron-store": "~8.0.1",
"electron-updater": "~4.6.5",
@@ -129,19 +132,20 @@
"encoding": "~0.1.13",
"leaflet": "~1.7.1",
"marked": "~4.0.0",
"moment": "~2.29.1",
"moment": "~2.29.4",
"mysql2": "~2.3.2",
"pg": "~8.7.1",
"pg-connection-string": "~2.5.0",
"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",
"sql-formatter": "~8.2.0",
"ssh2-promise": "~1.0.2",
"v-mask": "~2.3.0",
"vue": "~3.2.33",
"vue-i18n": "~9.1.9",
"vue": "~3.2.37",
"vue-i18n": "~9.2.0",
"vuedraggable": "~4.1.0"
},
"devDependencies": {

View File

@@ -25,7 +25,7 @@ export const customizations: Customizations = {
functions: true,
schedulers: true,
// Settings
elementsWrapper: '',
elementsWrapper: '`',
stringsWrapper: '"',
tableAdd: true,
tableTruncateDisableFKCheck: true,

View File

@@ -25,6 +25,7 @@ export interface IpcResponse<T = any> {
*/
export interface ClientParams {
client: ClientCode;
uid?: string;
params:
mysql.ConnectionOptions & {schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean}
| pg.ClientConfig & {schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean}

View File

@@ -7,6 +7,12 @@ export interface TableParams {
export interface ExportOptions {
schema: string;
tables: {
table: string;
includeStructure: boolean;
includeContent: boolean;
includeDropStatement: boolean;
}[];
includes: {[key: string]: boolean};
outputFormat: 'sql' | 'sql.zip';
outputFile: string;

View File

@@ -0,0 +1,192 @@
function isJSON (str: string) {
try {
if (!['{', '['].includes(str.trim()[0]))
return false;
JSON.parse(str);
return true;
}
catch (_) {
return false;
}
}
function isHTML (str: string) {
const tags = [
'a',
'abbr',
'address',
'area',
'article',
'aside',
'audio',
'b',
'base',
'bdi',
'bdo',
'blockquote',
'body',
'br',
'button',
'canvas',
'caption',
'cite',
'code',
'col',
'colgroup',
'data',
'datalist',
'dd',
'del',
'details',
'dfn',
'dialog',
'div',
'dl',
'dt',
'em',
'embed',
'fieldset',
'figcaption',
'figure',
'footer',
'form',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'head',
'header',
'hgroup',
'hr',
'html',
'i',
'iframe',
'img',
'input',
'ins',
'kbd',
'label',
'legend',
'li',
'link',
'main',
'map',
'mark',
'math',
'menu',
'menuitem',
'meta',
'meter',
'nav',
'noscript',
'object',
'ol',
'optgroup',
'option',
'output',
'p',
'param',
'picture',
'pre',
'progress',
'q',
'rb',
'rp',
'rt',
'rtc',
'ruby',
's',
'samp',
'script',
'section',
'select',
'slot',
'small',
'source',
'span',
'strong',
'style',
'sub',
'summary',
'sup',
'svg',
'table',
'tbody',
'td',
'template',
'textarea',
'tfoot',
'th',
'thead',
'time',
'title',
'tr',
'track',
'u',
'ul',
'var',
'video',
'wbr'
];
const doc = new DOMParser().parseFromString(str, 'text/html');
const lowerStr = str.toLowerCase();
if (Array.from(doc.body.childNodes).some(node => node.nodeType === 1))
return tags.some((tag) => lowerStr.includes(`<${tag}>`));
return false;
}
function isSVG (str: string) {
const doc = new DOMParser().parseFromString(str, 'text/xml');
const lowerStr = str.toLowerCase();
const errorNode = doc.querySelector('parsererror');
if (!errorNode)
return lowerStr.includes('<svg');
return false;
}
function isXML (str: string) {
const doc = new DOMParser().parseFromString(str, 'text/xml');
const errorNode = doc.querySelector('parsererror');
return !errorNode;
}
function isMD (str: string) {
const mdChecks = [
'# ',
'`',
'- ',
'+ ',
'* ',
'1. ',
'**',
'__',
'~~',
'>> ',
'](http',
'![',
'[ ]',
'[x]'
];
return mdChecks.some((tag) => str.includes(tag));
}
export function langDetector (str: string) {
if (!str.trim().length)
return 'text';
if (isJSON(str))
return 'json';
if (isHTML(str))
return 'html';
if (isSVG(str))
return 'svg';
if (isXML(str))
return 'xml';
if (isMD(str))
return 'markdown';
return 'text';
}

View File

@@ -1,14 +0,0 @@
/* eslint-disable no-useless-escape */
// eslint-disable-next-line no-control-regex
const pattern = /[\0\x08\x09\x1a\n\r"'\\\%]/gm;
const regex = new RegExp(pattern);
function sqlEscaper (string: string) {
return string.replace(regex, char => {
const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '\"', '\\', '\\\\', '%'];
const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\\\'', '\\\"', '\\\\', '\\\\\\\\', '\%'];
return r[m.indexOf(char)] || char;
});
}
export { sqlEscaper };

162
src/common/libs/sqlUtils.ts Normal file
View File

@@ -0,0 +1,162 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-useless-escape */
import * as moment from 'moment';
import { lineString, point, polygon } from '@turf/helpers';
import customizations from '../customizations';
import { ClientCode } from '../interfaces/antares';
import { BLOB, BIT, DATE, DATETIME, FLOAT, SPATIAL, IS_MULTI_SPATIAL, NUMBER, TEXT_SEARCH } from 'common/fieldTypes';
import hexToBinary, { HexChar } from './hexToBinary';
import { getArrayDepth } from './getArrayDepth';
/**
* Escapes a string fo SQL use
*
* @param { String } string
* @returns { String } Escaped string
*/
export const sqlEscaper = (string: string): string => {
// eslint-disable-next-line no-control-regex
const pattern = /[\0\x08\x09\x1a\n\r"'\\\%]/gm;
const regex = new RegExp(pattern);
return string.replace(regex, char => {
const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '\"', '\\', '\\\\', '%'];
const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\\\'', '\\\"', '\\\\', '\\\\\\\\', '\%'];
return r[m.indexOf(char)] || char;
});
};
export const objectToGeoJSON = (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: any, curr: any) => [...acc, [curr.x, curr.y]], [])));
}
else
return point([val.x, val.y]);
};
export const escapeAndQuote = (val: string, client: ClientCode) => {
const { stringsWrapper: sw } = customizations[client];
// 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 `${sw}${val}${sw}`;
if (chunkIndex < val.length)
return `${sw}${escapedVal + val.slice(chunkIndex)}${sw}`;
return `${sw}${escapedVal}${sw}`;
};
export const valueToSqlString = (args: {
val: any;
client: ClientCode;
field: {type: string; datePrecision: number};
}): string => {
let parsedValue;
const { val, client, field } = args;
const { stringsWrapper: sw } = customizations[client];
if (val === null)
parsedValue = 'NULL';
else if (DATE.includes(field.type)) {
parsedValue = moment(val).isValid()
? escapeAndQuote(moment(val).format('YYYY-MM-DD'), client)
: val;
}
else if (DATETIME.includes(field.type)) {
let datePrecision = '';
for (let i = 0; i < field.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
parsedValue = moment(val).isValid()
? escapeAndQuote(moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`), client)
: escapeAndQuote(val, client);
}
else if ('isArray' in field) {
let localVal;
if (Array.isArray(val))
localVal = JSON.stringify(val).replaceAll('[', '{').replaceAll(']', '}');
else
localVal = typeof val === 'string' ? val.replaceAll('[', '{').replaceAll(']', '}') : '';
parsedValue = `'${localVal}'`;
}
else if (TEXT_SEARCH.includes(field.type))
parsedValue = `'${val.replaceAll('\'', '\'\'')}'`;
else if (BIT.includes(field.type))
parsedValue = `b'${hexToBinary(Buffer.from(val).toString('hex') as undefined as HexChar[])}'`;
else if (BLOB.includes(field.type)) {
if (['mysql', 'maria'].includes(client))
parsedValue = `X'${val.toString('hex').toUpperCase()}'`;
else if (client === 'pg')
parsedValue = `decode('${val.toString('hex').toUpperCase()}', 'hex')`;
}
else if (NUMBER.includes(field.type))
parsedValue = val;
else if (FLOAT.includes(field.type))
parsedValue = parseFloat(val);
else if (SPATIAL.includes(field.type)) {
let geoJson;
if (IS_MULTI_SPATIAL.includes(field.type)) {
const features = [];
for (const element of val)
features.push(objectToGeoJSON(element));
geoJson = {
type: 'FeatureCollection',
features
};
}
else
geoJson = objectToGeoJSON(val);
parsedValue = `ST_GeomFromGeoJSON('${JSON.stringify(geoJson)}')`;
}
else if (val === '') parsedValue = `${sw}${sw}`;
else {
parsedValue = typeof val === 'string'
? escapeAndQuote(val, client)
: typeof val === 'object'
? escapeAndQuote(JSON.stringify(val), client)
: val;
}
return parsedValue;
};
export const jsonToSqlInsert = (args: {
json: { [key: string]: any};
client: ClientCode;
fields: { [key: string]: {type: string; datePrecision: number}};
table: string;
}) => {
const { client, json, fields, table } = args;
const { elementsWrapper: ew } = customizations[client];
const fieldNames = Object.keys(json).map(key => `${ew}${key}${ew}`);
const values = Object.keys(json).map(key => (
valueToSqlString({ val: json[key], client, field: fields[key] })
));
return `INSERT INTO ${ew}${table}${ew} (${fieldNames.join(', ')}) VALUES (${values.join(', ')});`;
};

69
src/common/shortcuts.ts Normal file
View File

@@ -0,0 +1,69 @@
interface ShortcutRecord {
event: string;
keys: Electron.Accelerator[];
description: string;
os: NodeJS.Platform[];
}
const shortcuts: ShortcutRecord[] = [
{
event: 'open-new-tab',
keys: ['CommandOrControl+T'],
description: 'Open a new query tab',
os: ['darwin', 'linux', 'win32']
},
{
event: 'close-tab',
keys: ['CommandOrControl+W'],
description: 'Close tab',
os: ['darwin', 'linux', 'win32']
},
{
event: 'next-tab',
keys: ['Alt+CommandOrControl+Right', 'CommandOrControl+PageDown'],
description: 'Next tab',
os: ['darwin', 'win32']
},
{
event: 'prev-tab',
keys: ['Alt+CommandOrControl+Left', 'CommandOrControl+PageUp'],
description: 'Previous tab',
os: ['darwin', 'win32']
},
{
event: 'next-tab',
keys: ['CommandOrControl+PageDown'],
description: 'Next tab',
os: ['linux']
},
{
event: 'prev-tab',
keys: ['CommandOrControl+PageUp'],
description: 'Previous tab',
os: ['linux']
},
{
event: 'open-connections-modal',
keys: ['Shift+CommandOrControl+Space'],
description: 'Show all connections',
os: ['darwin', 'linux', 'win32']
},
{
event: 'toggle-console',
keys: ['CommandOrControl+F12', 'CommandOrControl+`'],
description: 'Toggle console',
os: ['darwin', 'linux', 'win32']
}
];
for (let i = 1; i <= 9; i++) {
shortcuts.push(
{
event: `select-tab-${i}`,
keys: [`CommandOrControl+${i}`],
description: `Select tab number ${i}`,
os: ['darwin', 'linux', 'win32']
});
}
export { shortcuts, ShortcutRecord };

View File

@@ -55,6 +55,7 @@ export default (connections: {[key: string]: antares.Client}) => {
try {
const connection = await ClientsFactory.getClient({
uid: conn.uid,
client: conn.client,
params
});
@@ -128,6 +129,7 @@ export default (connections: {[key: string]: antares.Client}) => {
try {
const connection = ClientsFactory.getClient({
uid: conn.uid,
client: conn.client,
params,
poolSize: 5

View File

@@ -4,7 +4,7 @@ import { InsertRowsParams } from 'common/interfaces/tableApis';
import { ipcMain } from 'electron';
import { faker } from '@faker-js/faker';
import * as moment from 'moment';
import { sqlEscaper } from 'common/libs/sqlEscaper';
import { sqlEscaper } from 'common/libs/sqlUtils';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes';
import customizations from 'common/customizations';

View File

@@ -3,10 +3,14 @@ import mysql from 'mysql2/promise';
import * as pg from 'pg';
import SSH2Promise from 'ssh2-promise';
const queryLogger = (sql: string) => {
const queryLogger = ({ sql, cUid }: {sql: string; cUid: string}) => {
// Remove comments, newlines and multiple spaces
const escapedSql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '').replace(/\s\s+/g, ' ');
console.log(escapedSql);
if (process.type !== undefined) {
const mainWindow = require('electron').webContents.fromId(1);
mainWindow.send('query-log', { cUid, sql: escapedSql, date: new Date() });
}
if (process.env.NODE_ENV === 'development') console.log(escapedSql);
};
/**
@@ -14,15 +18,17 @@ const queryLogger = (sql: string) => {
*/
export class AntaresCore {
_client: antares.ClientCode;
protected _cUid: string
protected _params: mysql.ConnectionOptions | pg.ClientConfig | { databasePath: string; readonly: boolean};
protected _poolSize: number;
protected _ssh?: SSH2Promise;
protected _logger: (sql: string) => void;
protected _logger: (args: {sql: string; cUid: string}) => void;
protected _queryDefaults: antares.QueryBuilderObject;
protected _query: antares.QueryBuilderObject;
constructor (args: antares.ClientParams) {
this._client = args.client;
this._cUid = args.uid;
this._params = args.params;
this._poolSize = args.poolSize || undefined;
this._logger = args.logger || queryLogger;

View File

@@ -192,14 +192,14 @@ export class MySQLClient extends AntaresCore {
// ANSI_QUOTES check
const [response] = await connection.query<mysql.RowDataPacket[]>('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\'');
const sqlMode = response[0]?.Value?.split(',');
const hasAnsiQuotes = sqlMode.includes('ANSI_QUOTES');
const sqlMode: string[] = response[0]?.Value?.split(',');
const hasAnsiQuotes = sqlMode.includes('ANSI') || sqlMode.includes('ANSI_QUOTES');
if (this._params.readonly)
await connection.query('SET SESSION TRANSACTION READ ONLY');
if (hasAnsiQuotes)
await connection.query(`SET SESSION sql_mode = "${sqlMode.filter((m: string) => m !== 'ANSI_QUOTES').join(',')}"`);
await connection.query(`SET SESSION sql_mode = '${sqlMode.filter((m: string) => !['ANSI', 'ANSI_QUOTES'].includes(m)).join(',')}'`);
return connection;
}
@@ -219,18 +219,18 @@ export class MySQLClient extends AntaresCore {
// ANSI_QUOTES check
const [res] = await connection.query<mysql.RowDataPacket[]>('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\'');
const sqlMode = res[0]?.Value?.split(',');
const hasAnsiQuotes = sqlMode.includes('ANSI_QUOTES');
const sqlMode: string[] = res[0]?.Value?.split(',');
const hasAnsiQuotes = sqlMode.includes('ANSI') || sqlMode.includes('ANSI_QUOTES');
if (hasAnsiQuotes)
await connection.query(`SET SESSION sql_mode = "${sqlMode.filter((m: string) => m !== 'ANSI_QUOTES').join(',')}"`);
await connection.query(`SET SESSION sql_mode = '${sqlMode.filter((m: string) => !['ANSI', 'ANSI_QUOTES'].includes(m)).join(',')}'`);
connection.on('connection', conn => {
if (this._params.readonly)
conn.query('SET SESSION TRANSACTION READ ONLY');
if (hasAnsiQuotes)
conn.query(`SET SESSION sql_mode = "${sqlMode.filter((m: string) => m !== 'ANSI_QUOTES').join(',')}"`);
conn.query(`SET SESSION sql_mode = '${sqlMode.filter((m: string) => !['ANSI', 'ANSI_QUOTES'].includes(m)).join(',')}'`);
});
return connection;
@@ -1397,7 +1397,7 @@ export class MySQLClient extends AntaresCore {
}
async getVersion () {
const sql = 'SHOW VARIABLES LIKE "%vers%"';
const sql = 'SHOW VARIABLES LIKE \'%vers%\'';
const { rows } = await this.raw(sql);
return rows.reduce((acc, curr) => {
@@ -1536,7 +1536,7 @@ export class MySQLClient extends AntaresCore {
}
async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) {
if (process.env.NODE_ENV === 'development') this._logger(sql);
this._logger({ cUid: this._cUid, sql });
args = {
nest: false,

View File

@@ -1,6 +1,5 @@
import * as antares from 'common/interfaces/antares';
import * as mysql from 'mysql2';
import { builtinsTypes } from 'pg-types';
import * as pg from 'pg';
import * as pgAst from 'pgsql-ast-parser';
import { AntaresCore } from '../AntaresCore';
@@ -19,6 +18,68 @@ pg.types.setTypeParser(1114, pgToString); // timestamp
pg.types.setTypeParser(1184, pgToString); // timestamptz
pg.types.setTypeParser(1266, pgToString); // timetz
// from pg-types
type builtinsTypes =
'BOOL' |
'BYTEA' |
'CHAR' |
'INT8' |
'INT2' |
'INT4' |
'REGPROC' |
'TEXT' |
'OID' |
'TID' |
'XID' |
'CID' |
'JSON' |
'XML' |
'PG_NODE_TREE' |
'SMGR' |
'PATH' |
'POLYGON' |
'CIDR' |
'FLOAT4' |
'FLOAT8' |
'ABSTIME' |
'RELTIME' |
'TINTERVAL' |
'CIRCLE' |
'MACADDR8' |
'MONEY' |
'MACADDR' |
'INET' |
'ACLITEM' |
'BPCHAR' |
'VARCHAR' |
'DATE' |
'TIME' |
'TIMESTAMP' |
'TIMESTAMPTZ' |
'INTERVAL' |
'TIMETZ' |
'BIT' |
'VARBIT' |
'NUMERIC' |
'REFCURSOR' |
'REGPROCEDURE' |
'REGOPER' |
'REGOPERATOR' |
'REGCLASS' |
'REGTYPE' |
'UUID' |
'TXID_SNAPSHOT' |
'PG_LSN' |
'PG_NDISTINCT' |
'PG_DEPENDENCIES' |
'TSVECTOR' |
'TSQUERY' |
'GTSVECTOR' |
'REGCONFIG' |
'REGDICTIONARY' |
'JSONB' |
'REGNAMESPACE' |
'REGROLE';
export class PostgreSQLClient extends AntaresCore {
private _schema?: string;
private _runningConnections: Map<string, number>;
@@ -1314,7 +1375,7 @@ export class PostgreSQLClient extends AntaresCore {
}
async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) {
if (process.env.NODE_ENV === 'development') this._logger(sql);
this._logger({ cUid: this._cUid, sql });
args = {
nest: false,

View File

@@ -586,7 +586,7 @@ export class SQLiteClient extends AntaresCore {
}
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
this._logger({ cUid: this._cUid, sql });// TODO: replace BLOB content with a placeholder
args = {
nest: false,

View File

@@ -1,12 +1,8 @@
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, { HexChar } from 'common/libs/hexToBinary';
import { getArrayDepth } from 'common/libs/getArrayDepth';
import * as moment from 'moment';
import { lineString, point, polygon } from '@turf/helpers';
import { MySQLClient } from '../../clients/MySQLClient';
import { valueToSqlString } from 'common/libs/sqlUtils';
export default class MysqlExporter extends SqlExporter {
protected _client: MySQLClient;
@@ -122,54 +118,7 @@ ${footer}
const column = notGeneratedColumns[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 (BIT.includes(column.type))
sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex') as undefined as HexChar[])}'`;
else if (BLOB.includes(column.type))
sqlInsertString += `X'${val.toString('hex').toUpperCase()}'`;
else if (NUMBER.includes(column.type))
sqlInsertString += val;
else if (FLOAT.includes(column.type))
sqlInsertString += parseFloat(val);
else if (SPATIAL.includes(column.type)) {
let geoJson;
if (IS_MULTI_SPATIAL.includes(column.type)) {
const features = [];
for (const element of val)
features.push(this._getGeoJSON(element));
geoJson = {
type: 'FeatureCollection',
features
};
}
else
geoJson = this._getGeoJSON(val);
sqlInsertString += `ST_GeomFromGeoJSON('${JSON.stringify(geoJson)}')`;
}
else if (val === '') sqlInsertString += '\'\'';
else {
sqlInsertString += typeof val === 'string'
? this.escapeAndQuote(val)
: typeof val === 'object'
? this.escapeAndQuote(JSON.stringify(val))
: val;
}
sqlInsertString += valueToSqlString({ val, client: 'mysql', field: column });
if (parseInt(i) !== notGeneratedColumns.length - 1)
sqlInsertString += ', ';
@@ -435,17 +384,4 @@ CREATE TABLE \`${view.Name}\`(
return `'${escapedVal}'`;
}
/* 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: any, curr: any) => [...acc, [curr.x, curr.y]], [])));
}
else
return point([val.x, val.y]);
}
/* eslint-enable @typescript-eslint/no-explicit-any */
}

View File

@@ -1,13 +1,11 @@
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, { HexChar } 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';
import { valueToSqlString } from 'common/libs/sqlUtils';
export default class PostgreSQLExporter extends SqlExporter {
constructor (client: PostgreSQLClient, tables: exporter.TableParams[], options: exporter.ExportOptions) {
@@ -223,47 +221,7 @@ SET row_security = off;\n\n\n`;
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') as undefined as HexChar[])}'`;
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;
}
sqlInsertString += valueToSqlString({ val, client: 'pg', field: column });
if (parseInt(i) !== columns.length - 1)
sqlInsertString += ', ';

View File

@@ -1,10 +1,11 @@
import { app, BrowserWindow, /* session, */ nativeImage, Menu, ipcMain } from 'electron';
import { app, BrowserWindow, globalShortcut, nativeImage, Menu, ipcMain } from 'electron';
import * as path from 'path';
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';
import { shortcuts } from 'common/shortcuts';
Store.initRenderer();
const persistentStore = new Store({ name: 'settings' });
@@ -142,6 +143,33 @@ else {
window.webContents.session.loadExtension(extensionPath, { allowFileAccess: true }).catch(console.error);
}
});
app.on('browser-window-focus', () => {
// Send registered shortcut events to window
for (const shortcut of shortcuts) {
if (shortcut.os.includes(process.platform)) {
for (const key of shortcut.keys) {
globalShortcut.register(key, () => {
mainWindow.webContents.send(shortcut.event);
if (isDevelopment) console.log('EVENT:', shortcut);
});
}
}
}
if (isDevelopment) { // Dev shortcuts
globalShortcut.register('Shift+CommandOrControl+F5', () => {
mainWindow.reload();
});
globalShortcut.register('Shift+CommandOrControl+F12', () => {
mainWindow.webContents.openDevTools();
});
}
});
app.on('browser-window-blur', () => {
globalShortcut.unregisterAll();
});
}
function createAppMenu () {

View File

@@ -2,7 +2,7 @@
<div id="wrapper" :class="[`theme-${applicationTheme}`, !disableBlur || 'no-blur']">
<TheTitleBar />
<div id="window-content">
<TheSettingBar />
<TheSettingBar @show-connections-modal="isAllConnectionsModal = true" />
<div id="main-content" class="container">
<div class="columns col-gapless">
<Workspace
@@ -10,7 +10,7 @@
:key="connection.uid"
:connection="connection"
/>
<div class="connection-panel-wrapper">
<div class="connection-panel-wrapper p-relative">
<WorkspaceAddConnectionPanel v-if="selectedWorkspace === 'NEW'" />
</div>
</div>
@@ -21,13 +21,15 @@
<BaseTextEditor class="d-none" value="" />
</div>
</div>
<ModalAllConnections v-if="isAllConnectionsModal" @close="isAllConnectionsModal = false" />
</div>
</template>
<script lang="ts">
import { defineAsyncComponent } from 'vue';
<script setup lang="ts">
import { defineAsyncComponent, onMounted, Ref, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { ipcRenderer } from 'electron';
import { useI18n } from 'vue-i18n';
import { Menu, getCurrentWindow } from '@electron/remote';
import { useApplicationStore } from '@/stores/application';
import { useConnectionsStore } from '@/stores/connections';
@@ -35,98 +37,88 @@ import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces';
import TheSettingBar from '@/components/TheSettingBar.vue';
export default {
name: 'App',
components: {
TheTitleBar: defineAsyncComponent(() => import(/* webpackChunkName: "TheTitleBar" */'@/components/TheTitleBar.vue')),
TheSettingBar,
TheFooter: defineAsyncComponent(() => import(/* webpackChunkName: "TheFooter" */'@/components/TheFooter.vue')),
TheNotificationsBoard: defineAsyncComponent(() => import(/* webpackChunkName: "TheNotificationsBoard" */'@/components/TheNotificationsBoard.vue')),
Workspace: defineAsyncComponent(() => import(/* webpackChunkName: "Workspace" */'@/components/Workspace.vue')),
WorkspaceAddConnectionPanel: defineAsyncComponent(() => import(/* webpackChunkName: "WorkspaceAddConnectionPanel" */'@/components/WorkspaceAddConnectionPanel.vue')),
ModalSettings: defineAsyncComponent(() => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings.vue')),
TheScratchpad: defineAsyncComponent(() => import(/* webpackChunkName: "TheScratchpad" */'@/components/TheScratchpad.vue')),
BaseTextEditor: defineAsyncComponent(() => import(/* webpackChunkName: "BaseTextEditor" */'@/components/BaseTextEditor.vue'))
},
setup () {
const applicationStore = useApplicationStore();
const connectionsStore = useConnectionsStore();
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
const { t } = useI18n();
const {
isLoading,
isSettingModal,
isScratchpad
} = storeToRefs(applicationStore);
const { connections } = storeToRefs(connectionsStore);
const { applicationTheme, disableBlur } = storeToRefs(settingsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const TheTitleBar = defineAsyncComponent(() => import(/* webpackChunkName: "TheTitleBar" */'@/components/TheTitleBar.vue'));
const TheFooter = defineAsyncComponent(() => import(/* webpackChunkName: "TheFooter" */'@/components/TheFooter.vue'));
const TheNotificationsBoard = defineAsyncComponent(() => import(/* webpackChunkName: "TheNotificationsBoard" */'@/components/TheNotificationsBoard.vue'));
const Workspace = defineAsyncComponent(() => import(/* webpackChunkName: "Workspace" */'@/components/Workspace.vue'));
const WorkspaceAddConnectionPanel = defineAsyncComponent(() => import(/* webpackChunkName: "WorkspaceAddConnectionPanel" */'@/components/WorkspaceAddConnectionPanel.vue'));
const ModalSettings = defineAsyncComponent(() => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings.vue'));
const ModalAllConnections = defineAsyncComponent(() => import(/* webpackChunkName: "ModalAllConnections" */'@/components/ModalAllConnections.vue'));
const TheScratchpad = defineAsyncComponent(() => import(/* webpackChunkName: "TheScratchpad" */'@/components/TheScratchpad.vue'));
const BaseTextEditor = defineAsyncComponent(() => import(/* webpackChunkName: "BaseTextEditor" */'@/components/BaseTextEditor.vue'));
const { checkVersionUpdate } = applicationStore;
const { changeApplicationTheme } = settingsStore;
const applicationStore = useApplicationStore();
const connectionsStore = useConnectionsStore();
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
changeApplicationTheme(applicationTheme.value);// Forces persistentStore to save on file and mail process
}, 1000);
});
const {
isSettingModal,
isScratchpad
} = storeToRefs(applicationStore);
const { connections } = storeToRefs(connectionsStore);
const { applicationTheme, disableBlur } = storeToRefs(settingsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
return {
isLoading,
isSettingModal,
isScratchpad,
checkVersionUpdate,
changeApplicationTheme,
connections,
applicationTheme,
disableBlur,
selectedWorkspace
};
},
mounted () {
ipcRenderer.send('check-for-updates');
this.checkVersionUpdate();
const { checkVersionUpdate } = applicationStore;
const { changeApplicationTheme } = settingsStore;
const InputMenu = Menu.buildFromTemplate([
{
label: this.$t('word.cut'),
role: 'cut'
},
{
label: this.$t('word.copy'),
role: 'copy'
},
{
label: this.$t('word.paste'),
role: 'paste'
},
{
type: 'separator'
},
{
label: this.$t('message.selectAll'),
role: 'selectAll'
const isAllConnectionsModal: Ref<boolean> = ref(false);
ipcRenderer.on('open-connections-modal', () => {
isAllConnectionsModal.value = true;
});
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
changeApplicationTheme(applicationTheme.value);// Forces persistentStore to save on file and mail process
}, 1000);
});
onMounted(() => {
ipcRenderer.send('check-for-updates');
checkVersionUpdate();
const InputMenu = Menu.buildFromTemplate([
{
label: t('word.cut'),
role: 'cut'
},
{
label: t('word.copy'),
role: 'copy'
},
{
label: t('word.paste'),
role: 'paste'
},
{
type: 'separator'
},
{
label: t('message.selectAll'),
role: 'selectAll'
}
]);
document.body.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let node: any = e.target;
while (node) {
if (node.nodeName.match(/^(input|textarea)$/i) || node.isContentEditable) {
InputMenu.popup({ window: getCurrentWindow() });
break;
}
]);
document.body.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let node: any = e.target;
while (node) {
if (node.nodeName.match(/^(input|textarea)$/i) || node.isContentEditable) {
InputMenu.popup({ window: getCurrentWindow() });
break;
}
node = node.parentNode;
}
});
}
};
node = node.parentNode;
}
});
});
</script>
<style lang="scss">
@@ -159,7 +151,7 @@ export default {
.connection-panel-wrapper {
height: calc(100vh - #{$excluding-size});
width: 100%;
padding-top: 15vh;
padding-top: 10vh;
display: flex;
justify-content: center;
align-items: flex-start;

View File

@@ -31,13 +31,13 @@
class="btn btn-primary mr-2"
@click.stop="confirmModal"
>
{{ confirmText || $t('word.confirm') }}
{{ confirmText || t('word.confirm') }}
</button>
<button
class="btn btn-link"
@click="hideModal"
>
{{ cancelText || $t('word.cancel') }}
{{ cancelText || t('word.cancel') }}
</button>
</div>
</div>
@@ -49,6 +49,9 @@
<script setup lang="ts">
import { useFocusTrap } from '@/composables/useFocusTrap';
import { computed, onBeforeUnmount, PropType, useSlots } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
size: {

View File

@@ -431,6 +431,12 @@ export default defineComponent({
width: 100%;
}
&__item-text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&__list-wrapper {
cursor: pointer;
position: fixed;

View File

@@ -4,7 +4,7 @@
<i class="mdi mdi-folder-open mr-1" />{{ message }}
</span>
<span class="text-ellipsis file-uploader-value">
{{ lastPart(modelValue) }}
{{ lastPart(modelValue, 19) }}
</span>
<i
v-if="modelValue"
@@ -24,6 +24,9 @@
<script setup lang="ts">
import { uidGen } from 'common/libs/uidGen';
import { useFilters } from '@/composables/useFilters';
const { lastPart } = useFilters();
defineProps({
message: {
@@ -43,15 +46,6 @@ const id = uidGen();
const clear = () => {
emit('clear');
};
const lastPart = (string: string) => {
if (!string) return '';
string = string.split(/[/\\]+/).pop();
if (string.length >= 19)
string = `...${string.slice(-19)}`;
return string;
};
</script>
<style lang="scss" scoped>

View File

@@ -4,7 +4,7 @@
v-model="selectedGroup"
class="form-select"
:options="[{name: 'manual'}, ...fakerGroups]"
:option-label="(opt: any) => opt.name === 'manual' ? $t('message.manualValue') : $t(`faker.${opt.name}`)"
:option-label="(opt: any) => opt.name === 'manual' ? t('message.manualValue') : t(`faker.${opt.name}`)"
option-track-by="name"
:disabled="!isChecked"
style="flex-grow: 0;"
@@ -15,7 +15,7 @@
v-if="selectedGroup !== 'manual'"
v-model="selectedMethod"
:options="fakerMethods"
:option-label="(opt: any) => $t(`faker.${opt.name}`)"
:option-label="(opt: any) => t(`faker.${opt.name}`)"
option-track-by="name"
class="form-select"
:disabled="!isChecked"
@@ -41,7 +41,7 @@
<BaseUploadInput
v-else-if="inputProps().type === 'file'"
:model-value="selectedValue"
:message="$t('word.browse')"
:message="t('word.browse')"
@clear="clearValue"
@change="filesChange($event)"
/>
@@ -92,6 +92,9 @@ import BaseUploadInput from '@/components/BaseUploadInput.vue';
import ForeignKeySelect from '@/components/ForeignKeySelect.vue';
import FakerMethods from 'common/FakerMethods';
import BaseSelect from '@/components/BaseSelect.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
type: String,

View File

@@ -13,7 +13,7 @@
</template>
<script setup lang="ts">
import { computed, Ref, ref } from 'vue';
import { computed, Ref, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import Tables from '@/ipc-api/Tables';
import { useNotificationsStore } from '@/stores/notifications';
@@ -21,6 +21,7 @@ import { useWorkspacesStore } from '@/stores/workspaces';
import { TEXT, LONG_TEXT } from 'common/fieldTypes';
import BaseSelect from '@/components/BaseSelect.vue';
import { TableField } from 'common/interfaces/antares';
import { useFilters } from '@/composables/useFilters';
const props = defineProps({
modelValue: [String, Number],
@@ -35,17 +36,18 @@ const emit = defineEmits(['update:modelValue', 'blur']);
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { cutText } = useFilters();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const editField: Ref<HTMLSelectElement> = ref(null);
const foreignList = ref([]);
const currentValue = ref(props.modelValue);
const currentValue = ref(null);
const isValidDefault = computed(() => {
if (!foreignList.value.length) return true;
if (props.modelValue === null) return false;
return foreignList.value.some(foreign => foreign.foreign_column.toString() === props.modelValue.toString());
return foreignList.value.some(foreign => foreign.foreign_column.toString() === props.modelValue?.toString());
});
const foreigns = computed(() => {
@@ -53,7 +55,7 @@ const foreigns = computed(() => {
if (!isValidDefault.value)
list.push({ value: props.modelValue, label: props.modelValue === null ? 'NULL' : props.modelValue });
for (const row of foreignList.value)
list.push({ value: row.foreign_column, label: `${row.foreign_column} ${cutText('foreign_description' in row ? ` - ${row.foreign_description}` : '')}` });
list.push({ value: row.foreign_column, label: `${row.foreign_column} ${cutText('foreign_description' in row ? ` - ${row.foreign_description}` : '', 15)}` });
return list;
});
@@ -61,10 +63,9 @@ const onChange = (opt: HTMLSelectElement) => {
emit('update:modelValue', opt.value);
};
const cutText = (val: string) => {
if (typeof val !== 'string') return val;
return val.length > 15 ? `${val.substring(0, 15)}...` : val;
};
watch(() => props.modelValue, () => {
currentValue.value = props.modelValue;
});
let foreignDesc: string | false;
const params = {

View File

@@ -0,0 +1,358 @@
<template>
<Teleport to="#window-content">
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div ref="trapRef" 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-apps mr-1" />
<span class="cut-text">{{ t('message.allConnections') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body py-0">
<div class="columns">
<div class="connections-search column col-12 columns col-gapless">
<div class="column col-12 mt-2">
<div ref="searchForm" class="form-group has-icon-right p-2 m-0">
<input
v-model="searchTerm"
class="form-input"
type="text"
:placeholder="t('message.searchForConnections')"
@keypress.esc="searchTerm = ''"
>
<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>
</div>
<TransitionGroup name="fade" :duration="{ enter: 200, leave: 200 }">
<div
v-for="connection in filteredConnections"
:key="connection.uid"
class="connection-block column col-md-6 col-lg-4 col-3 p-3"
tabindex="0"
@click.stop="selectConnection(connection.uid)"
@keypress.stop.enter="selectConnection(connection.uid)"
@mouseover="connectionHover = connection.uid"
@mouseleave="connectionHover = null"
>
<div class="panel">
<div class="panel-header p-2 text-center p-relative">
<figure class="avatar avatar-lg pt-1 mb-1">
<i class="settingbar-element-icon dbi" :class="[`dbi-${connection.client}`]" />
</figure>
<div class="panel-title h6 text-ellipsis">
{{ getConnectionName(connection.uid) }}
</div>
<div class="panel-subtitle">
{{ clients.get(connection.client) || connection.client }}
</div>
<div class="all-connections-buttons p-absolute d-flex" style="top: 0; right: 0;">
<i
v-if="connection.isPinned"
class="all-connections-pinned mdi mdi-18px"
:class="connectionHover === connection.uid ? 'mdi-pin-off' : 'mdi-pin'"
:title="t('word.unpin')"
@click.stop="unpinConnection(connection.uid)"
/>
<i
v-else
class="all-connections-pin mdi mdi-18px mdi-pin mdi-rotate-45"
:title="t('word.pin')"
@click.stop="pinConnection(connection.uid)"
/>
<i
class="all-connections-delete mdi mdi-delete mdi-18px ml-2"
:title="t('word.delete')"
@click.stop="askToDelete(connection)"
/>
</div>
</div>
<div class="panel-body text-center">
<div v-if="connection.databasePath">
<div class="text-ellipsis" :title="connection.databasePath">
<i class="mdi mdi-database d-inline" /> <span class="text-bold">{{
connection.databasePath
}}</span>
</div>
</div>
<div v-else>
<div class="text-ellipsis" :title="`${connection.host}:${connection.port}`">
<i class="mdi mdi-server d-inline" /> <span class="text-bold">{{ connection.host
}}:{{ connection.port }}</span>
</div>
</div>
<div v-if="connection.user">
<div class="text-ellipsis">
<i class="mdi mdi-account d-inline" /> <span class="text-bold">{{ connection.user
}}</span>
</div>
</div>
<div v-if="connection.schema">
<div class="text-ellipsis">
<i class="mdi mdi-database d-inline" /> <span class="text-bold">{{ connection.schema
}}</span>
</div>
</div>
<div v-if="connection.database">
<div class="text-ellipsis">
<i class="mdi mdi-database d-inline" /> <span class="text-bold">{{
connection.database
}}</span>
</div>
</div>
</div>
<div class="panel-footer text-center py-0">
<div v-if="connection.ssl" class="chip bg-success mt-2">
<i class="mdi mdi-lock mdi-18px mr-1" />
SSL
</div>
<div v-if="connection.ssh" class="chip bg-success mt-2">
<i class="mdi mdi-console-network mdi-18px mr-1" />
SSH
</div>
</div>
</div>
</div>
<input
key="trick"
readonly
class="p-absolute"
style="width: 1px; height: 1px; opacity: 0"
type="text"
>
<!-- workaround for useFocusTrap $lastFocusable -->
</TransitionGroup>
</div>
</div>
</div>
</div>
<ConfirmModal
v-if="isConfirmModal"
@confirm="confirmDeleteConnection"
@hide="isConfirmModal = false"
>
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-server-remove mr-1" /> {{ t('message.deleteConnection') }}
</div>
</template>
<template #body>
<div class="mb-2">
{{ t('message.deleteCorfirm') }} <b>{{ selectedConnectionName }}</b>?
</div>
</template>
</ConfirmModal>
</Teleport>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, Ref, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useFocusTrap } from '@/composables/useFocusTrap';
import { useConnectionsStore } from '@/stores/connections';
import { useWorkspacesStore } from '@/stores/workspaces';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import { ConnectionParams } from 'common/interfaces/antares';
const { t } = useI18n();
const connectionsStore = useConnectionsStore();
const workspacesStore = useWorkspacesStore();
const { connections,
pinnedConnections,
lastConnections
} = storeToRefs(connectionsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const {
getConnectionName,
pinConnection,
unpinConnection,
deleteConnection
} = connectionsStore;
const { selectWorkspace } = workspacesStore;
const { trapRef } = useFocusTrap();
const emit = defineEmits(['close']);
const clients = new Map([
['mysql', 'MySQL'],
['maria', 'MariaDB'],
['pg', 'PostgreSQL'],
['sqlite', 'SQLite']
]);
const searchTerm = ref('');
const isConfirmModal = ref(false);
const connectionHover: Ref<string> = ref(null);
const selectedConnection: Ref<ConnectionParams> = ref(null);
const sortedConnections = computed(() => {
return connections.value
.map(c => {
const connTime = lastConnections.value.find((lc) => lc.uid === c.uid)?.time || 0;
return {
...c,
time: connTime,
isPinned: pinnedConnections.value.has(c.uid)
};
})
.sort((a, b) => {
if (a.isPinned < b.isPinned) return 1;
if (a.isPinned > b.isPinned) return -1;
if (a.time < b.time) return 1;
if (a.time > b.time) return -1;
return 0;
});
});
const filteredConnections = computed(() => {
return sortedConnections.value.filter(connection => {
return connection.name?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) ||
connection.host?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) ||
connection.database?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) ||
connection.databasePath?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) ||
connection.schema?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) ||
connection.user?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) ||
String(connection.port)?.includes(searchTerm.value);
});
});
const selectedConnectionName = computed(() => getConnectionName(selectedConnection.value?.uid));
const closeModal = () => emit('close');
const selectConnection = (uid: string) => {
selectWorkspace(uid);
closeModal();
};
const askToDelete = (connection: ConnectionParams) => {
selectedConnection.value = connection;
isConfirmModal.value = true;
};
const confirmDeleteConnection = () => {
if (selectedWorkspace.value === selectedConnection.value.uid)
selectWorkspace(null);
deleteConnection(selectedConnection.value);
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape') {
if ((e.target as HTMLInputElement).tagName === 'INPUT' && searchTerm.value.length > 0)
searchTerm.value = '';
else
closeModal();
}
};
window.addEventListener('keydown', onKey);
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script>
<style lang="scss" scoped>
.vscroll {
height: 1000px;
overflow: auto;
overflow-anchor: none;
}
.column-resizable {
&:hover,
&:active {
resize: horizontal;
overflow: hidden;
}
}
.table-column-title {
display: flex;
align-items: center;
}
.sort-icon {
font-size: 0.7rem;
line-height: 1;
margin-left: 0.2rem;
}
.modal {
align-items: flex-start;
.modal-container {
max-width: 75vw;
margin-top: 10vh;
.modal-body {
height: 80vh;
}
}
}
.connections-search {
display: flex;
justify-content: space-around;
}
.connection-block {
cursor: pointer;
transition: all .2s;
border-radius: $border-radius;
outline: none;
&:focus {
box-shadow: 0 0 3px .1rem rgba($primary-color, 80%);
}
&:hover {
.all-connections-buttons {
.all-connections-delete,
.all-connections-pinned,
.all-connections-pin {
opacity: .5;
}
}
}
.all-connections-buttons {
.all-connections-pinned {
opacity: .3;
transition: opacity .2s;
&:hover {
opacity: 1;
}
}
.all-connections-delete,
.all-connections-pin {
opacity: 0;
transition: opacity .2s;
&:hover {
opacity: 1;
}
}
}
}
</style>

View File

@@ -6,7 +6,7 @@
<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') }}
<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" />
@@ -16,7 +16,7 @@
<form class="form-horizontal">
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.user') }}</label>
<label class="form-label">{{ t('word.user') }}</label>
</div>
<div class="col-9">
<input
@@ -29,7 +29,7 @@
</div>
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.password') }}</label>
<label class="form-label">{{ t('word.password') }}</label>
</div>
<div class="col-9">
<input
@@ -44,10 +44,10 @@
</div>
<div class="modal-footer">
<button class="btn btn-primary mr-2" @click.stop="sendCredentials">
{{ $t('word.send') }}
{{ t('word.send') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
{{ t('word.close') }}
</button>
</div>
</div>
@@ -58,6 +58,9 @@
<script setup lang="ts">
import { Ref, ref } from 'vue';
import { useFocusTrap } from '@/composables/useFocusTrap';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { trapRef } = useFocusTrap();

View File

@@ -1,7 +1,7 @@
<template>
<ConfirmModal
:confirm-text="$t('word.run')"
:cancel-text="$t('word.cancel')"
:confirm-text="t('word.run')"
:cancel-text="t('word.cancel')"
size="400"
@confirm="runRoutine"
@hide="closeModal"
@@ -9,7 +9,7 @@
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-play mr-1" />
<span class="cut-text">{{ $t('word.parameters') }}: {{ localRoutine.name }}</span>
<span class="cut-text">{{ t('word.parameters') }}: {{ localRoutine.name }}</span>
</div>
</template>
<template #body>
@@ -52,6 +52,12 @@ import { computed, PropType, Ref, ref } from 'vue';
import { NUMBER, FLOAT } from 'common/fieldTypes';
import { FunctionInfos, RoutineInfos } from 'common/interfaces/antares';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import { useFilters } from '@/composables/useFilters';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { wrapNumber } = useFilters();
const props = defineProps({
localRoutine: Object as PropType<RoutineInfos | FunctionInfos>,
@@ -106,11 +112,6 @@ const onKey = (e: KeyboardEvent) => {
closeModal();
};
const wrapNumber = (num: number) => {
if (!num) return '';
return `(${num})`;
};
window.addEventListener('keydown', onKey);
setTimeout(() => {

View File

@@ -1,18 +1,18 @@
<template>
<ConfirmModal
:confirm-text="$t('word.discard')"
:cancel-text="$t('word.stay')"
:confirm-text="t('word.discard')"
:cancel-text="t('word.stay')"
@confirm="emit('confirm')"
@hide="emit('close')"
>
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-content-save-alert mr-1" /> {{ $t('message.unsavedChanges') }}
<i class="mdi mdi-24px mdi-content-save-alert mr-1" /> {{ t('message.unsavedChanges') }}
</div>
</template>
<template #body>
<div>
{{ $t('message.discardUnsavedChanges') }}
{{ t('message.discardUnsavedChanges') }}
</div>
</template>
</ConfirmModal>
@@ -21,6 +21,9 @@
<script setup lang="ts">
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import { onBeforeUnmount } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const emit = defineEmits(['confirm', 'close']);

View File

@@ -7,7 +7,7 @@
<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>
<span class="cut-text">{{ t('message.editSchema') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -17,7 +17,7 @@
<form class="form-horizontal">
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.name') }}</label>
<label class="form-label">{{ t('word.name') }}</label>
</div>
<div class="col-9">
<input
@@ -26,14 +26,14 @@
class="form-input"
type="text"
required
:placeholder="$t('message.schemaName')"
:placeholder="t('message.schemaName')"
readonly
>
</div>
</div>
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.collation') }}</label>
<label class="form-label">{{ t('word.collation') }}</label>
</div>
<div class="col-9">
<BaseSelect
@@ -43,7 +43,7 @@
option-label="collation"
option-track-by="collation"
/>
<small>{{ $t('message.serverDefault') }}: {{ defaultCollation }}</small>
<small>{{ t('message.serverDefault') }}: {{ defaultCollation }}</small>
</div>
</div>
</form>
@@ -51,10 +51,10 @@
</div>
<div class="modal-footer">
<button class="btn btn-primary mr-2" @click.stop="updateSchema">
{{ $t('word.update') }}
{{ t('word.update') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
{{ t('word.close') }}
</button>
</div>
</div>
@@ -70,6 +70,9 @@ import { useWorkspacesStore } from '@/stores/workspaces';
import { useFocusTrap } from '@/composables/useFocusTrap';
import Schema from '@/ipc-api/Schema';
import BaseSelect from '@/components/BaseSelect.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
selectedSchema: String

View File

@@ -7,7 +7,7 @@
<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>
<span class="cut-text">{{ t('message.exportSchema') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -16,7 +16,7 @@
<div class="container">
<div class="columns">
<div class="col-3">
<label class="form-label">{{ $t('message.directoryPath') }}</label>
<label class="form-label">{{ t('message.directoryPath') }}</label>
</div>
<div class="col-9">
<fieldset class="input-group">
@@ -26,14 +26,14 @@
type="text"
required
readonly
:placeholder="$t('message.schemaName')"
:placeholder="t('message.schemaName')"
>
<button
type="button"
class="btn btn-primary input-group-btn"
@click.prevent="openPathDialog"
>
{{ $t('word.change') }}
{{ t('word.change') }}
</button>
</fieldset>
</div>
@@ -51,14 +51,14 @@
<div class="column col-auto col-ml-auto ">
<button
class="btn btn-dark btn-sm"
:title="$t('word.refresh')"
: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')"
:title="t('message.uncheckAllTables')"
:disabled="isRefreshing"
@click="uncheckAllTables"
>
@@ -66,7 +66,7 @@
</button>
<button
class="btn btn-dark btn-sm"
:title="$t('message.checkAllTables')"
:title="t('message.checkAllTables')"
:disabled="isRefreshing"
@click="checkAllTables"
>
@@ -122,22 +122,22 @@
<div class="tr">
<div class="th" style="width: 50%;">
<div class="table-column-title">
<span>{{ $t('word.table') }}</span>
<span>{{ t('word.table') }}</span>
</div>
</div>
<div class="th text-center">
<div class="table-column-title">
<span>{{ $t('word.structure') }}</span>
<span>{{ t('word.structure') }}</span>
</div>
</div>
<div class="th text-center">
<div class="table-column-title">
<span>{{ $t('word.content') }}</span>
<span>{{ t('word.content') }}</span>
</div>
</div>
<div class="th text-center">
<div class="table-column-title">
<span>{{ $t('word.drop') }}</span>
<span>{{ t('word.drop') }}</span>
</div>
</div>
</div>
@@ -183,19 +183,19 @@
</div>
<div class="column col-4">
<h5 class="h5">
{{ $t('word.options') }}
{{ t('word.options') }}
</h5>
<span class="h6">{{ $t('word.includes') }}:</span>
<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) }}
<input v-model="options.includes[key]" type="checkbox"><i class="form-icon" /> {{ t(`word.${key}`, 2) }}
</label>
<div v-if="clientCustoms.exportByChunks">
<div class="h6 mt-4 mb-2">
{{ $t('message.newInserStmtEvery') }}:
{{ t('message.newInserStmtEvery') }}:
</div>
<div class="columns">
<div class="column col-6">
@@ -209,21 +209,21 @@
<BaseSelect
v-model="options.sqlInsertDivider"
class="form-select"
:options="[{value: 'bytes', label: 'KiB'}, {value: 'rows', label: $tc('word.row', 2)}]"
:options="[{value: 'bytes', label: 'KiB'}, {value: 'rows', label: t('word.row', 2)}]"
/>
</div>
</div>
</div>
<div class="h6 mb-2 mt-4">
{{ $t('message.ourputFormat') }}:
{{ 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'})}]"
:options="[{value: 'sql', label: t('message.singleFile', {ext: '.sql'})}, {value: 'sql.zip', label: t('message.zipCompressedFile', {ext: '.sql'})}]"
/>
</div>
</div>
@@ -245,7 +245,7 @@
</div>
<div class="column col-auto px-0">
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
{{ t('word.close') }}
</button>
<button
class="btn btn-primary mr-2"
@@ -254,7 +254,7 @@
autofocus
@click.prevent="startExport"
>
{{ $t('word.export') }}
{{ t('word.export') }}
</button>
</div>
</div>
@@ -269,8 +269,8 @@ import * as moment from 'moment';
import { ipcRenderer } from 'electron';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { SchemaInfos } from 'common/interfaces/antares';
import { ExportOptions, ExportState, TableParams } from 'common/interfaces/exporter';
import { ClientCode, SchemaInfos } from 'common/interfaces/antares';
import { ExportOptions, ExportState } from 'common/interfaces/exporter';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import { useFocusTrap } from '@/composables/useFocusTrap';
@@ -302,11 +302,15 @@ const isExporting = ref(false);
const isRefreshing = ref(false);
const progressPercentage = ref(0);
const progressStatus = ref('');
const tables: Ref<TableParams[]> = ref([]);
const options: Ref<ExportOptions> = ref({
const tables: Ref<{
table: string;
includeStructure: boolean;
includeContent: boolean;
includeDropStatement: boolean;
}[]> = ref([]);
const options: Ref<Partial<ExportOptions>> = ref({
schema: props.selectedSchema,
includes: {} as {[key: string]: boolean},
outputFile: '',
outputFormat: 'sql' as 'sql' | 'sql.zip',
sqlInsertAfter: 250,
sqlInsertDivider: 'bytes' as 'bytes' | 'rows'
@@ -353,7 +357,7 @@ const startExport = async () => {
outputFile: dumpFilePath.value,
tables: [...tables.value],
...options.value
};
} as ExportOptions & { uid: string; type: ClientCode };
try {
const { status, response } = await Schema.export(params);

View File

@@ -7,7 +7,7 @@
<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>
<span class="cut-text">{{ t('message.insertRow', 2) }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -39,7 +39,7 @@
<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')">
<label class="form-checkbox ml-3" :title="t('word.insert')">
<input
type="checkbox"
:checked="!fieldsToExclude.includes(field.name)"
@@ -55,7 +55,7 @@
</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')">
<div class="input-group tooltip tooltip-right" :data-tooltip="t('message.numberOfInserts')">
<input
v-model="nInserts"
type="number"
@@ -70,7 +70,7 @@
<div
v-if="hasFakes"
class="tooltip tooltip-right ml-2"
:data-tooltip="$t('message.fakeDataLanguage')"
:data-tooltip="t('message.fakeDataLanguage')"
>
<BaseSelect
v-model="fakerLocale"
@@ -85,10 +85,10 @@
:class="{'loading': isInserting}"
@click.stop="insertRows"
>
{{ $t('word.insert') }}
{{ t('word.insert') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
{{ t('word.close') }}
</button>
</div>
</div>
@@ -109,10 +109,20 @@ import { useFocusTrap } from '@/composables/useFocusTrap';
import Tables from '@/ipc-api/Tables';
import FakerSelect from '@/components/FakerSelect.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import { useFilters } from '@/composables/useFilters';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { wrapNumber } = useFilters();
const props = defineProps({
tabUid: [String, Number],
schema: String,
table: String,
fields: Array as Prop<TableField[]>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rowToDuplicate: Object as Prop<any>,
keyUsage: Array as Prop<TableForeign[]>
});
@@ -123,8 +133,6 @@ const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace } = workspacesStore;
const { trapRef } = useFocusTrap({ disableAutofocus: true });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -134,7 +142,6 @@ const nInserts = ref(1);
const isInserting = ref(false);
const fakerLocale = ref('en');
const workspace = computed(() => getWorkspace(selectedWorkspace.value));
const foreignKeys = computed(() => props.keyUsage.map(key => key.field));
const hasFakes = computed(() => Object.keys(localRow.value).some(field => 'group' in localRow.value[field] && localRow.value[field].group !== 'manual'));
@@ -220,8 +227,8 @@ const insertRows = async () => {
try {
const { status, response } = await Tables.insertTableFakeRows({
uid: selectedWorkspace.value,
schema: workspace.value.breadcrumbs.schema,
table: workspace.value.breadcrumbs.table,
schema: props.schema,
table: props.table,
row: rowToInsert,
repeat: nInserts.value,
fields: fieldTypes,
@@ -266,11 +273,6 @@ const onKey = (e: KeyboardEvent) => {
closeModal();
};
const wrapNumber = (num: number) => {
if (!num) return '';
return `(${num})`;
};
window.addEventListener('keydown', onKey);
onMounted(() => {
@@ -284,44 +286,57 @@ onMounted(() => {
const rowObj: {[key: string]: unknown} = {};
for (const field of props.fields) {
let fieldDefault;
if (!props.rowToDuplicate) {
// Set default values
for (const field of props.fields) {
let fieldDefault;
if (field.default === 'NULL') fieldDefault = null;
else {
if ([...NUMBER, ...FLOAT].includes(field.type))
fieldDefault = !field.default || Number.isNaN(+field.default.replaceAll('\'', '')) ? null : +field.default.replaceAll('\'', '');
else if ([...TEXT, ...LONG_TEXT].includes(field.type)) {
fieldDefault = field.default
? field.default.includes('\'')
? field.default.split('\'')[1]
: field.default
: '';
}
else if ([...TIME, ...DATE].includes(field.type))
fieldDefault = field.default;
else if (BIT.includes(field.type))
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 = '';
for (let i = 0; i < field.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
fieldDefault = moment().format(`YYYY-MM-DD HH:mm:ss${datePrecision}`);
if (field.default === 'NULL') fieldDefault = null;
else {
if ([...NUMBER, ...FLOAT].includes(field.type))
fieldDefault = !field.default || Number.isNaN(+field.default.replaceAll('\'', '')) ? null : +field.default.replaceAll('\'', '');
else if ([...TEXT, ...LONG_TEXT].includes(field.type)) {
fieldDefault = field.default
? field.default.includes('\'')
? field.default.split('\'')[1]
: field.default
: '';
}
else if ([...TIME, ...DATE].includes(field.type))
fieldDefault = field.default;
else if (BIT.includes(field.type))
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 = '';
for (let i = 0; i < field.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
fieldDefault = moment().format(`YYYY-MM-DD HH:mm:ss${datePrecision}`);
}
else
fieldDefault = field.default;
}
else if (field.enumValues)
fieldDefault = field.enumValues.replaceAll('\'', '').split(',');
else
fieldDefault = field.default;
}
else if (field.enumValues)
fieldDefault = field.enumValues.replaceAll('\'', '').split(',');
else
fieldDefault = field.default;
rowObj[field.name] = { value: fieldDefault };
if (field.autoIncrement || !!field.onUpdate)// Disable by default auto increment or "on update" fields
fieldsToExclude.value = [...fieldsToExclude.value, field.name];
}
}
else {
// Set values to duplicate
for (const field of props.fields) {
if (typeof props.rowToDuplicate[field.name] !== 'object')
rowObj[field.name] = { value: props.rowToDuplicate[field.name] };
rowObj[field.name] = { value: fieldDefault };
if (field.autoIncrement || !!field.onUpdate)// Disable by default auto increment or "on update" fields
fieldsToExclude.value = [...fieldsToExclude.value, field.name];
if (field.autoIncrement || !!field.onUpdate)// Disable by default auto increment or "on update" fields
fieldsToExclude.value = [...fieldsToExclude.value, field.name];
}
}
localRow.value = { ...rowObj };

View File

@@ -100,14 +100,15 @@
<script setup lang="ts">
import { Component, computed, ComputedRef, onBeforeUnmount, onMounted, onUpdated, Prop, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import * as moment from 'moment';
import { ConnectionParams } from 'common/interfaces/antares';
import { HistoryRecord, useHistoryStore } from '@/stores/history';
import { useConnectionsStore } from '@/stores/connections';
import { useFocusTrap } from '@/composables/useFocusTrap';
import { useFilters } from '@/composables/useFilters';
import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
const { t } = useI18n();
const { formatDate } = useFilters();
const { getHistoryByWorkspace, deleteQueryFromHistory } = useHistoryStore();
const { getConnectionName } = useConnectionsStore();
@@ -164,7 +165,6 @@ const resizeResults = () => {
}
};
const formatDate = (date: Date) => moment(date).isValid() ? moment(date).format('HH:mm:ss - YYYY/MM/DD') : date;
const refreshScroller = () => resizeResults();
const closeModal = () => emit('close');

View File

@@ -7,7 +7,7 @@
<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>
<span class="cut-text">{{ t('message.importSchema') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -15,7 +15,7 @@
<div class="modal-body pb-0">
{{ sqlFile }}
<div v-if="queryErrors.length > 0" class="mt-2">
<label>{{ $tc('message.importQueryErrors', queryErrors.length) }}</label>
<label>{{ t('message.importQueryErrors', queryErrors.length) }}</label>
<textarea
v-model="formattedQueryErrors"
class="form-input"
@@ -28,7 +28,7 @@
<div class="column col modal-progress-wrapper text-left">
<div class="import-progress">
<span class="progress-status">
{{ progressPercentage }}% - {{ progressStatus }} - {{ $tc('message.executedQueries', queryCount) }}
{{ progressPercentage }}% - {{ progressStatus }} - {{ t('message.executedQueries', queryCount) }}
</span>
<progress
class="progress d-block"
@@ -39,7 +39,7 @@
</div>
<div class="column col-auto px-0">
<button class="btn btn-link" @click.stop="closeModal">
{{ completed ? $t('word.close') : $t('word.cancel') }}
{{ completed ? t('word.close') : t('word.cancel') }}
</button>
</div>
</div>
@@ -57,8 +57,8 @@ import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import Schema from '@/ipc-api/Schema';
import { useI18n } from 'vue-i18n';
import { ImportState } from 'common/interfaces/importer';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();

View File

@@ -7,7 +7,7 @@
<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>
<span class="cut-text">{{ t('message.createNewSchema') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -17,7 +17,7 @@
<form class="form-horizontal" @submit.prevent="createSchema">
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.name') }}</label>
<label class="form-label">{{ t('word.name') }}</label>
</div>
<div class="col-9">
<input
@@ -26,13 +26,13 @@
class="form-input"
type="text"
required
:placeholder="$t('message.schemaName')"
: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>
<label class="form-label">{{ t('word.collation') }}</label>
</div>
<div class="col-9">
<BaseSelect
@@ -42,7 +42,7 @@
option-label="collation"
option-track-by="collation"
/>
<small>{{ $t('message.serverDefault') }}: {{ defaultCollation }}</small>
<small>{{ t('message.serverDefault') }}: {{ defaultCollation }}</small>
</div>
</div>
</form>
@@ -54,10 +54,10 @@
:class="{'loading': isLoading}"
@click.stop="createSchema"
>
{{ $t('word.add') }}
{{ t('word.add') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
{{ t('word.close') }}
</button>
</div>
</div>
@@ -73,6 +73,9 @@ import { useWorkspacesStore } from '@/stores/workspaces';
import { useFocusTrap } from '@/composables/useFocusTrap';
import Schema from '@/ipc-api/Schema';
import BaseSelect from '@/components/BaseSelect.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();

View File

@@ -17,7 +17,7 @@
<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>
<span class="cut-text">{{ t('message.processesList') }}: {{ connectionName }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -29,7 +29,7 @@
<button
class="btn btn-dark btn-sm mr-0 pr-1 d-flex"
:class="{'loading':isQuering}"
:title="`${$t('word.refresh')} (F5)`"
:title="`${t('word.refresh')} (F5)`"
@click="getProcessesList"
>
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh mr-1" />
@@ -39,7 +39,7 @@
<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>
<span>{{ t('word.autoRefresh') }}: <b>{{ +autorefreshTimer ? `${autorefreshTimer}s` : 'OFF' }}</b></span>
<input
v-model="autorefreshTimer"
class="slider no-border"
@@ -59,7 +59,7 @@
tabindex="0"
>
<i class="mdi mdi-24px mdi-file-export mr-1" />
<span>{{ $t('word.export') }}</span>
<span>{{ t('word.export') }}</span>
<i class="mdi mdi-24px mdi-menu-down" />
</button>
<ul class="menu text-left">
@@ -74,7 +74,7 @@
</div>
<div class="workspace-query-info">
<div v-if="sortedResults.length">
{{ $t('word.processes') }}: <b>{{ sortedResults.length.toLocaleString() }}</b>
{{ t('word.processes') }}: <b>{{ sortedResults.length.toLocaleString() }}</b>
</div>
</div>
</div>
@@ -136,7 +136,7 @@
<script setup lang="ts">
import { Component, computed, onBeforeUnmount, onMounted, onUpdated, Prop, Ref, ref } from 'vue';
import { ConnectionParams } from 'common/interfaces/antares';
import { arrayToFile } from '../libs/arrayToFile';
import { exportRows } from '../libs/exportRows';
import { useNotificationsStore } from '@/stores/notifications';
import { useFocusTrap } from '@/composables/useFocusTrap';
import Schema from '@/ipc-api/Schema';
@@ -144,6 +144,9 @@ import { useConnectionsStore } from '@/stores/connections';
import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
import ModalProcessesListRow from '@/components/ModalProcessesListRow.vue';
import ModalProcessesListContext from '@/components/ModalProcessesListContext.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { addNotification } = useNotificationsStore();
const { getConnectionName } = useConnectionsStore();
@@ -312,10 +315,10 @@ const closeModal = () => emit('close');
const downloadTable = (format: 'csv' | 'json') => {
if (!sortedResults.value) return;
arrayToFile({
exportRows({
type: format,
content: sortedResults.value,
filename: 'processes'
table: 'processes'
});
};

View File

@@ -4,7 +4,7 @@
@close-context="closeContext"
>
<div v-if="props.selectedRow" class="context-element">
<span class="d-flex"><i class="mdi mdi-18px mdi-content-copy text-light pr-1" /> {{ $t('word.copy') }}</span>
<span class="d-flex"><i class="mdi mdi-18px mdi-content-copy text-light pr-1" /> {{ t('word.copy') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu">
<div
@@ -13,7 +13,7 @@
@click="copyCell"
>
<span class="d-flex">
<i class="mdi mdi-18px mdi-numeric-0 mdi-rotate-90 text-light pr-1" /> {{ $tc('word.cell', 1) }}
<i class="mdi mdi-18px mdi-numeric-0 mdi-rotate-90 text-light pr-1" /> {{ t('word.cell', 1) }}
</span>
</div>
<div
@@ -22,7 +22,7 @@
@click="copyRow"
>
<span class="d-flex">
<i class="mdi mdi-18px mdi-table-row text-light pr-1" /> {{ $tc('word.row', 1) }}
<i class="mdi mdi-18px mdi-table-row text-light pr-1" /> {{ t('word.row', 1) }}
</span>
</div>
</div>
@@ -33,7 +33,7 @@
@click="killProcess"
>
<span class="d-flex">
<i class="mdi mdi-18px mdi-close-circle-outline text-light pr-1" /> {{ $t('message.killProcess') }}
<i class="mdi mdi-18px mdi-close-circle-outline text-light pr-1" /> {{ t('message.killProcess') }}
</span>
</div>
</BaseContextMenu>
@@ -41,6 +41,9 @@
<script setup lang="ts">
import BaseContextMenu from '@/components/BaseContextMenu.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
contextEvent: MouseEvent,

View File

@@ -12,19 +12,19 @@
class="cell-content"
:class="`${isNull(col)} type-${typeof col === 'number' ? 'int' : 'varchar'}`"
@dblclick="dblClick(cKey)"
>{{ cutText(col) }}</span>
>{{ cutText(col, 250) }}</span>
</div>
<ConfirmModal
v-if="isInfoModal"
:confirm-text="$t('word.update')"
:cancel-text="$t('word.close')"
:confirm-text="t('word.update')"
:cancel-text="t('word.close')"
size="medium"
:hide-footer="true"
@hide="hideInfoModal"
>
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-information-outline mr-1" /> {{ $t('message.processInfo') }}
<i class="mdi mdi-24px mdi-information-outline mr-1" /> {{ t('message.processInfo') }}
</div>
</template>
<template #body>
@@ -48,6 +48,12 @@
import { Ref, ref } from 'vue';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import TextEditor from '@/components/BaseTextEditor.vue';
import { useFilters } from '@/composables/useFilters';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { cutText } = useFilters();
const props = defineProps({
row: Object
@@ -79,11 +85,6 @@ const dblClick = (col: string) => {
isInfoModal.value = true;
};
const cutText = (val: string | number) => {
if (typeof val !== 'string') return val;
return val.length > 250 ? `${val.substring(0, 250)}[...]` : val;
};
</script>
<style lang="scss">

View File

@@ -126,6 +126,19 @@
</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.disableScratchpad') }}
</label>
</div>
<div class="col-3 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleDisableScratchpad">
<input type="checkbox" :checked="disableScratchpad">
<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">
@@ -302,7 +315,7 @@
</template>
<script setup lang="ts">
import { onBeforeUnmount, Ref, ref } from 'vue';
import { onBeforeUnmount, Ref, ref, computed } from 'vue';
import { shell } from 'electron';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
@@ -315,9 +328,9 @@ import ModalSettingsUpdate from '@/components/ModalSettingsUpdate.vue';
import ModalSettingsChangelog from '@/components/ModalSettingsChangelog.vue';
import BaseTextEditor from '@/components/BaseTextEditor.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import { computed } from '@vue/reactivity';
import { AvailableLocale } from '@/i18n';
const { t, availableLocales } = useI18n();
const { t } = useI18n();
const applicationStore = useApplicationStore();
const settingsStore = useSettingsStore();
@@ -337,6 +350,7 @@ const {
notificationsTimeout,
restoreTabs,
disableBlur,
disableScratchpad,
applicationTheme,
editorTheme,
editorFontSize
@@ -349,6 +363,7 @@ const {
changePageSize,
changeRestoreTabs,
changeDisableBlur,
changeDisableScratchpad,
changeAutoComplete,
changeLineWrap,
changeApplicationTheme,
@@ -434,7 +449,7 @@ ORDER BY
employee.id ASC;
`;
const localLocale: Ref<string> = ref(null);
const localLocale: Ref<AvailableLocale> = ref(null);
const localPageSize: Ref<number> = ref(null);
const localTimeout: Ref<number> = ref(null);
const localEditorTheme: Ref<string> = ref(null);
@@ -442,7 +457,7 @@ const selectedTab: Ref<string> = ref('general');
const locales = computed(() => {
const locales = [];
for (const locale of availableLocales)
for (const locale of Object.keys(localesNames))
locales.push({ code: locale, name: localesNames[locale] });
return locales;
@@ -490,6 +505,10 @@ const toggleDisableBlur = () => {
changeDisableBlur(!disableBlur.value);
};
const toggleDisableScratchpad = () => {
changeDisableScratchpad(!disableScratchpad.value);
};
const toggleAutoComplete = () => {
changeAutoComplete(!selectedAutoComplete.value);
};
@@ -498,7 +517,7 @@ const toggleLineWrap = () => {
changeLineWrap(!selectedLineWrap.value);
};
localLocale.value = selectedLocale.value as string;
localLocale.value = selectedLocale.value;
localPageSize.value = pageSize.value as number;
localTimeout.value = notificationsTimeout.value as number;
localEditorTheme.value = editorTheme.value as string;
@@ -519,6 +538,12 @@ onBeforeUnmount(() => {
.modal-body {
overflow: hidden;
.tab-link{
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.panel-body {
min-height: calc(25vh - 70px);
max-height: 65vh;

View File

@@ -26,27 +26,27 @@
:class="{'loading': updateStatus === 'checking'}"
@click="checkForUpdates"
>
{{ $t('message.checkForUpdates') }}
{{ t('message.checkForUpdates') }}
</button>
<button
v-else-if="updateStatus === 'downloaded'"
class="btn btn-primary"
@click="restartToUpdate"
>
{{ $t('message.restartToInstall') }}
{{ t('message.restartToInstall') }}
</button>
<button
v-else-if="updateStatus === 'link'"
class="btn btn-primary"
@click="openOutside('https://antares-sql.app/download.html')"
>
{{ $t('message.goToDownloadPage') }}
{{ t('message.goToDownloadPage') }}
</button>
</div>
<div class="form-group mt-4">
<label class="form-switch d-inline-block disabled" @click.prevent="toggleAllowPrerelease">
<input type="checkbox" :checked="allowPrerelease">
<i class="form-icon" /> {{ $t('message.includeBetaUpdates') }}
<i class="form-icon" /> {{ t('message.includeBetaUpdates') }}
</label>
</div>
</div>

View File

@@ -47,10 +47,11 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue']);
const cursorPosition = ref(0);
const fields = ref([]);
const lastTableFields = ref([]);
const customCompleter = ref([]);
const id = ref(uidGen());
const lastSchema: Ref<string> = ref(null);
const fields: Ref<{name: string; type: string}[]> = ref([]);
const tables = computed(() => {
return props.workspace
@@ -61,13 +62,27 @@ const tables = computed(() => {
}, []).map(table => {
return {
name: table.name as string,
type: table.type as string,
fields: []
type: table.type as string
};
})
: [];
});
const tablesInQuery = computed(() => {
if (!props.modelValue) return [];
const words = props.modelValue
.replaceAll(/[.'"`]/g, ' ')
.split(' ')
.filter(Boolean);
const includedTables = tables.value.reduce((acc, curr) => {
acc.push(curr.name);
return acc;
}, [] as string[]).filter((t) => words.includes(t));
return includedTables;
});
const triggers = computed(() => {
return props.workspace
? props.workspace.structure.filter(schema => schema.name === props.schema)
@@ -150,11 +165,11 @@ const lastWord = computed(() => {
const isLastWordATable = computed(() => /\w+\.\w*/gm.test(lastWord.value));
const fieldsCompleter = computed(() => {
const tableFieldsCompleter = computed(() => {
return {
getCompletions: (editor: never, session: never, pos: never, prefix: never, callback: (err: null, response: ace.Ace.Completion[]) => void) => {
const completions: ace.Ace.Completion[] = [];
fields.value.forEach(field => {
lastTableFields.value.forEach(field => {
completions.push({
value: field,
meta: 'column',
@@ -175,7 +190,8 @@ const setCustomCompleter = () => {
...triggers.value,
...procedures.value,
...functions.value,
...schedulers.value
...schedulers.value,
...fields.value
].forEach(el => {
completions.push({
value: el.name,
@@ -195,6 +211,29 @@ watch(() => props.modelValue, () => {
cursorPosition.value = (editor.value.session as any).doc.positionToIndex(editor.value.getCursorPosition());
});
watch(() => tablesInQuery.value.length, () => {
const localFields: {name: string; type: string}[] = [];
tablesInQuery.value.forEach(async table => {
const params = {
uid: props.workspace.uid,
schema: props.schema,
table: table
};
const { response } = await Tables.getTableColumns(params);
response.forEach((field: { name: string }) => {
localFields.push({
name: field.name,
type: 'column'
});
});
});
fields.value = localFields;
setCustomCompleter();
});
watch(editorTheme, () => {
if (editor.value)
editor.value.setTheme(`ace/theme/${editorTheme.value}`);
@@ -289,8 +328,8 @@ onMounted(() => {
Tables.getTableColumns(params).then(res => {
if (res.response.length)
fields.value = res.response.map((field: { name: string }) => field.name);
editor.value.completers = [fieldsCompleter.value];
lastTableFields.value = res.response.map((field: { name: string }) => field.name);
editor.value.completers = [tableFieldsCompleter.value];
editor.value.execCommand('startAutocomplete');
}).catch(console.log);
}

View File

@@ -3,18 +3,32 @@
:context-event="contextEvent"
@close-context="$emit('close-context')"
>
<div
v-if="isPinned"
class="context-element"
@click="unpin"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-pin-off text-light pr-1" /> {{ t('word.unpin') }}</span>
</div>
<div
v-else
class="context-element"
@click="pin"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-pin mdi-rotate-45 text-light pr-1" /> {{ t('word.pin') }}</span>
</div>
<div
v-if="isConnected"
class="context-element"
@click="disconnect"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-power text-light pr-1" /> {{ $t('word.disconnect') }}</span>
<span class="d-flex"><i class="mdi mdi-18px mdi-power text-light pr-1" /> {{ t('word.disconnect') }}</span>
</div>
<div class="context-element" @click="duplicateConnection">
<span class="d-flex"><i class="mdi mdi-18px mdi-content-duplicate text-light pr-1" /> {{ $t('word.duplicate') }}</span>
<span class="d-flex"><i class="mdi mdi-18px mdi-content-duplicate text-light pr-1" /> {{ t('word.duplicate') }}</span>
</div>
<div class="context-element" @click="showConfirmModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('word.delete') }}</span>
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ t('word.delete') }}</span>
</div>
<ConfirmModal
@@ -24,12 +38,12 @@
>
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-server-remove mr-1" /> {{ $t('message.deleteConnection') }}
<i class="mdi mdi-24px mdi-server-remove mr-1" /> {{ t('message.deleteConnection') }}
</div>
</template>
<template #body>
<div class="mb-2">
{{ $t('message.deleteCorfirm') }} <b>{{ connectionName }}</b>?
{{ t('message.deleteCorfirm') }} <b>{{ connectionName }}</b>?
</div>
</template>
</ConfirmModal>
@@ -46,11 +60,18 @@ import BaseContextMenu from '@/components/BaseContextMenu.vue';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import { ConnectionParams } from 'common/interfaces/antares';
const connectionsStore = useConnectionsStore();
const {
getConnectionName,
addConnection,
deleteConnection
} = useConnectionsStore();
deleteConnection,
pinConnection,
unpinConnection
} = connectionsStore;
const { pinnedConnections } = storeToRefs(connectionsStore);
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
@@ -71,6 +92,7 @@ const isConfirmModal = ref(false);
const connectionName = computed(() => getConnectionName(props.contextConnection.uid));
const isConnected = computed(() => getWorkspace(props.contextConnection.uid).connectionStatus === 'connected');
const isPinned = computed(() => pinnedConnections.value.has(props.contextConnection.uid));
const confirmDeleteConnection = () => {
if (selectedWorkspace.value === props.contextConnection.uid)
@@ -100,6 +122,16 @@ const hideConfirmModal = () => {
closeContext();
};
const pin = () => {
pinConnection(props.contextConnection.uid);
closeContext();
};
const unpin = () => {
unpinConnection(props.contextConnection.uid);
closeContext();
};
const disconnect = () => {
disconnectWorkspace(props.contextConnection.uid);
closeContext();

View File

@@ -11,14 +11,30 @@
<div class="footer-right-elements">
<ul class="footer-elements">
<li
v-if="workspace?.connectionStatus === 'connected' "
class="footer-element footer-link"
@click="toggleConsole()"
>
<i class="mdi mdi-18px mdi-console-line mr-1" />
<small>{{ t('word.console') }}</small>
</li>
<li class="footer-element footer-link" @click="openOutside('https://www.paypal.com/paypalme/fabiodistasio')">
<i class="mdi mdi-18px mdi-coffee mr-1" />
<small>{{ $t('word.donate') }}</small>
<small>{{ t('word.donate') }}</small>
</li>
<li class="footer-element footer-link" @click="openOutside('https://github.com/antares-sql/antares/issues')">
<li
class="footer-element footer-link"
:title="t('message.reportABug')"
@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')">
<li
class="footer-element footer-link"
:title="t('word.about')"
@click="showSettingModal('about')"
>
<i class="mdi mdi-18px mdi-information-outline" />
</li>
</ul>
@@ -29,9 +45,13 @@
<script setup lang="ts">
import { shell } from 'electron';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useApplicationStore } from '@/stores/application';
import { useWorkspacesStore } from '@/stores/workspaces';
import { computed, ComputedRef } from 'vue';
import { useConsoleStore } from '@/stores/console';
const { t } = useI18n();
interface DatabaseInfos {// TODO: temp
name: string;
@@ -43,13 +63,15 @@ interface DatabaseInfos {// TODO: temp
const applicationStore = useApplicationStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: workspace } = storeToRefs(workspacesStore);
const { getSelected: workspaceUid } = storeToRefs(workspacesStore);
const { toggleConsole } = useConsoleStore();
const { showSettingModal } = applicationStore;
const { getWorkspace } = workspacesStore;
const workspace = computed(() => getWorkspace(workspaceUid.value));
const version: ComputedRef<DatabaseInfos> = computed(() => {
return getWorkspace(workspace.value) ? getWorkspace(workspace.value).version : null;
return getWorkspace(workspaceUid.value) ? workspace.value.version : null;
});
const versionString = computed(() => {

View File

@@ -1,14 +1,14 @@
<template>
<ConfirmModal
:confirm-text="$t('word.update')"
:cancel-text="$t('word.close')"
:confirm-text="t('word.update')"
:cancel-text="t('word.close')"
size="large"
:hide-footer="true"
@hide="hideScratchpad"
>
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-notebook-edit-outline mr-1" /> {{ $t('word.scratchpad') }}
<i class="mdi mdi-24px mdi-notebook-edit-outline mr-1" /> {{ t('word.scratchpad') }}
</div>
</template>
<template #body>
@@ -22,7 +22,7 @@
:show-line-numbers="false"
/>
</div>
<small class="text-gray">{{ $t('message.markdownSupported') }}</small>
<small class="text-gray">{{ t('message.markdownSupported') }}</small>
</div>
</template>
</ConfirmModal>
@@ -35,6 +35,9 @@ import { useApplicationStore } from '@/stores/application';
import { useScratchpadStore } from '@/stores/scratchpad';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import TextEditor from '@/components/BaseTextEditor.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const applicationStore = useApplicationStore();
const scratchpadStore = useScratchpadStore();

View File

@@ -1,6 +1,6 @@
<template>
<div id="settingbar">
<div class="settingbar-top-elements">
<div ref="sidebarConnections" class="settingbar-top-elements">
<SettingBarContext
v-if="isContext"
:context-event="contextEvent"
@@ -9,7 +9,7 @@
/>
<ul class="settingbar-elements">
<Draggable
v-model="connections"
v-model="pinnedConnectionsArr"
:item-key="'uid'"
@start="isDragging = true"
@end="dragStop"
@@ -23,11 +23,40 @@
@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>
<i class="settingbar-element-icon dbi" :class="[`dbi-${element.client}`, getStatusBadge(element.uid), (pinnedConnections.has(element.uid) ? 'settingbar-element-pin' : false)]" />
<span v-if="!isDragging && !isScrolling" class="ex-tooltip-content">{{ getConnectionName(element.uid) }}</span>
</li>
</template>
</Draggable>
<div v-if="pinnedConnectionsArr.length" class="divider" />
<li
v-for="connection in unpinnedConnectionsArr"
:key="connection.uid"
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 && !isScrolling" class="ex-tooltip-content">{{ getConnectionName(connection.uid) }}</span>
</li>
</ul>
</div>
<div class="settingbar-middle-elements">
<ul class="settingbar-elements">
<li
v-if="isScrollable"
class="settingbar-element btn btn-link ex-tooltip"
@click="emit('show-connections-modal')"
@mouseover.self="tooltipPosition"
>
<i class="settingbar-element-icon mdi mdi-24px mdi-dots-horizontal text-light" />
<span class="ex-tooltip-content">{{ t('message.allConnections') }} (Shift+CTRL+Space)</span>
</li>
<li
class="settingbar-element btn btn-link ex-tooltip"
:class="{'selected': 'NEW' === selectedWorkspace}"
@@ -35,20 +64,24 @@
@mouseover.self="tooltipPosition"
>
<i class="settingbar-element-icon mdi mdi-24px mdi-plus text-light" />
<span class="ex-tooltip-content">{{ $t('message.addConnection') }}</span>
<span class="ex-tooltip-content">{{ t('message.addConnection') }}</span>
</li>
</ul>
</div>
<div class="settingbar-bottom-elements">
<ul class="settingbar-elements">
<li class="settingbar-element btn btn-link ex-tooltip" @click="showScratchpad">
<li
v-if="!disableScratchpad"
class="settingbar-element btn btn-link ex-tooltip"
@click="showScratchpad"
>
<i class="settingbar-element-icon mdi mdi-24px mdi-notebook-edit-outline text-light" />
<span class="ex-tooltip-content">{{ $t('word.scratchpad') }}</span>
<span class="ex-tooltip-content">{{ t('word.scratchpad') }}</span>
</li>
<li class="settingbar-element btn btn-link ex-tooltip" @click="showSettingModal('general')">
<i class="settingbar-element-icon mdi mdi-24px mdi-cog text-light" :class="{' badge badge-update': hasUpdates}" />
<span class="ex-tooltip-content">{{ $t('word.settings') }}</span>
<span class="ex-tooltip-content">{{ t('word.settings') }}</span>
</li>
</ul>
</div>
@@ -56,42 +89,73 @@
</template>
<script setup lang="ts">
import { ref, Ref, computed } from 'vue';
import { ref, Ref, computed, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useApplicationStore } from '@/stores/application';
import { useConnectionsStore } from '@/stores/connections';
import { useWorkspacesStore } from '@/stores/workspaces';
import { useSettingsStore } from '@/stores/settings';
import * as Draggable from 'vuedraggable';
import SettingBarContext from '@/components/SettingBarContext.vue';
import { ConnectionParams } from 'common/interfaces/antares';
import { useElementBounding, useScroll } from '@vueuse/core';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const applicationStore = useApplicationStore();
const connectionsStore = useConnectionsStore();
const workspacesStore = useWorkspacesStore();
const settingsStore = useSettingsStore();
const { updateStatus } = storeToRefs(applicationStore);
const { connections: getConnections } = storeToRefs(connectionsStore);
const { connections: storedConnections, pinnedConnections, lastConnections } = storeToRefs(connectionsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { disableScratchpad } = storeToRefs(settingsStore);
const { showSettingModal, showScratchpad } = applicationStore;
const { getConnectionName, updateConnections } = connectionsStore;
const { getConnectionName, updatePinnedConnections } = connectionsStore;
const { getWorkspace, selectWorkspace } = workspacesStore;
const emit = defineEmits(['show-connections-modal']);
const isLinux = process.platform === 'linux';
const sidebarConnections: Ref<HTMLDivElement> = ref(null);
const isContext: Ref<boolean> = ref(false);
const isDragging: Ref<boolean> = ref(false);
const isScrollable: Ref<boolean> = ref(false);
const isScrolling = ref(useScroll(sidebarConnections)?.isScrolling);
const contextEvent: Ref<MouseEvent> = ref(null);
const contextConnection: Ref<ConnectionParams> = ref(null);
const sidebarConnectionsHeight = ref(useElementBounding(sidebarConnections)?.height);
const connections = computed({
get () {
return getConnections.value;
},
set (value: ConnectionParams[]) {
updateConnections(value);
const pinnedConnectionsArr = computed({
get: () => [...pinnedConnections.value].map(c => storedConnections.value.find(sc => sc.uid === c)).filter(Boolean),
set: (value: ConnectionParams[]) => {
const pinnedUid = value.reduce((acc, curr) => {
acc.push(curr.uid);
return acc;
}, []);
updatePinnedConnections(pinnedUid);
}
});
const unpinnedConnectionsArr = computed(() => {
return storedConnections.value
.filter(c => !pinnedConnections.value.has(c.uid))
.map(c => {
const connTime = lastConnections.value.find((lc) => lc.uid === c.uid)?.time || 0;
return { ...c, time: connTime };
})
.sort((a, b) => {
if (a.time < b.time) return 1;
else if (a.time > b.time) return -1;
return 0;
});
});
const hasUpdates = computed(() => ['available', 'downloading', 'downloaded', 'link'].includes(updateStatus.value));
const contextMenu = (event: MouseEvent, connection: ConnectionParams) => {
@@ -101,11 +165,14 @@ const contextMenu = (event: MouseEvent, connection: ConnectionParams) => {
};
const tooltipPosition = (e: Event) => {
const el = e.target ? e.target : e;
const fromTop = isLinux
? window.scrollY + (el as HTMLElement).getBoundingClientRect().top + ((el as HTMLElement).offsetHeight / 4)
: window.scrollY + (el as HTMLElement).getBoundingClientRect().top - ((el as HTMLElement).offsetHeight / 4);
(el as HTMLElement).querySelector<HTMLElement>('.ex-tooltip-content').style.top = `${fromTop}px`;
const el = (e.target ? e.target : e) as unknown as HTMLElement;
const tooltip = el.querySelector<HTMLElement>('.ex-tooltip-content');
if (tooltip) {
const fromTop = isLinux
? window.scrollY + el.getBoundingClientRect().top + (el.offsetHeight / 4)
: window.scrollY + el.getBoundingClientRect().top - (el.offsetHeight / 4);
tooltip.style.top = `${fromTop}px`;
}
};
const getStatusBadge = (uid: string) => {
@@ -126,13 +193,50 @@ const getStatusBadge = (uid: string) => {
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dragStop = (e: any) => { // TODO: temp
const dragStop = (e: any) => {
isDragging.value = false;
setTimeout(() => {
tooltipPosition(e.originalEvent.target.parentNode);
}, 200);
};
watch(sidebarConnectionsHeight, (value) => {
isScrollable.value = value < sidebarConnections.value.scrollHeight;
});
watch(unpinnedConnectionsArr, (newVal, oldVal) => {
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
setTimeout(() => {
const element = document.querySelector<HTMLElement>('.settingbar-element.selected');
if (element) {
const rect = element.getBoundingClientRect();
const elemTop = rect.top;
const elemBottom = rect.bottom;
const isVisible = (elemTop >= 0) && (elemBottom <= window.innerHeight);
if (!isVisible) {
element.setAttribute('tabindex', '-1');
element.focus();
element.removeAttribute('tabindex');
}
}
}, 50);
}
});
watch(selectedWorkspace, (newVal, oldVal) => {
if (newVal !== oldVal) {
setTimeout(() => {
const element = document.querySelector<HTMLElement>('.settingbar-element.selected');
if (element) {
element.setAttribute('tabindex', '-1');
element.focus();
element.removeAttribute('tabindex');
}
}, 150);
}
});
</script>
<style lang="scss">
@@ -141,7 +245,7 @@ const dragStop = (e: any) => { // TODO: temp
height: calc(100vh - #{$excluding-size});
display: flex;
flex-direction: column;
justify-content: space-between;
// justify-content: space-between;
align-items: center;
padding: 0;
z-index: 9;
@@ -149,7 +253,7 @@ const dragStop = (e: any) => { // TODO: temp
.settingbar-top-elements {
overflow-x: hidden;
overflow-y: overlay;
max-height: calc((100vh - 3.5rem) - #{$excluding-size});
// max-height: calc((100vh - 3.5rem) - #{$excluding-size});
&::-webkit-scrollbar {
width: 3px;
@@ -157,8 +261,8 @@ const dragStop = (e: any) => { // TODO: temp
}
.settingbar-bottom-elements {
padding-top: 0.5rem;
z-index: 1;
margin-top: auto;
}
.settingbar-elements {
@@ -214,6 +318,21 @@ const dragStop = (e: any) => { // TODO: temp
bottom: initial;
}
}
.settingbar-element-pin{
margin: 0 auto;
&::before {
font: normal normal normal 14px/1 "Material Design Icons";
content: "\F0403";
color: $body-font-color-dark;
transform: rotate(45deg);
opacity: .25;
bottom: -8px;
left: -4px;
position: absolute;
}
}
}
}
}

View File

@@ -31,10 +31,10 @@
>
<i class="mdi mdi-18px mdi-code-tags mr-1" />
<span>
<span>{{ cutText(element.content || 'Query') }} #{{ element.index }}</span>
<span>{{ cutText(element.content || 'Query', 20, true) }} #{{ element.index }}</span>
<span
class="btn btn-clear"
:title="$t('word.close')"
:title="t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(element)"
/>
@@ -47,11 +47,11 @@
@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 :title="`${t('word.data').toUpperCase()}: ${t(`word.${element.elementType}`)}`">
<span class=" text-italic">{{ cutText(element.elementName, 20, true) }}</span>
<span
class="btn btn-clear"
:title="$t('word.close')"
:title="t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(element)"
/>
@@ -60,11 +60,11 @@
<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 :title="`${t('word.data').toUpperCase()}: ${t(`word.${element.elementType}`)}`">
{{ cutText(element.elementName, 20, true) }}
<span
class="btn btn-clear"
:title="$t('word.close')"
:title="t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(element)"
/>
@@ -77,11 +77,11 @@
: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 :title="`${t('word.new').toUpperCase()}: ${t(`word.${element.elementType}`)}`">
{{ t('message.newTable') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
:title="t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(element)"
/>
@@ -94,11 +94,11 @@
: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 :title="`${t('word.settings').toUpperCase()}: ${t(`word.${element.elementType}`)}`">
{{ cutText(element.elementName, 20, true) }}
<span
class="btn btn-clear"
:title="$t('word.close')"
:title="t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(element)"
/>
@@ -111,11 +111,11 @@
: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 :title="`${t('word.settings').toUpperCase()}: ${t(`word.view`)}`">
{{ cutText(element.elementName, 20, true) }}
<span
class="btn btn-clear"
:title="$t('word.close')"
:title="t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(element)"
/>
@@ -128,11 +128,11 @@
: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 :title="`${t('word.new').toUpperCase()}: ${t(`word.${element.elementType}`)}`">
{{ t('message.newView') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
:title="t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(element)"
/>
@@ -145,11 +145,11 @@
: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 :title="`${t('word.new').toUpperCase()}: ${t(`word.${element.elementType}`)}`">
{{ t('message.newTrigger') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
:title="t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(element)"
/>
@@ -162,11 +162,11 @@
: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 :title="`${t('word.new').toUpperCase()}: ${t(`word.${element.elementType}`)}`">
{{ t('message.newRoutine') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
:title="t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(element)"
/>
@@ -179,11 +179,11 @@
: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 :title="`${t('word.new').toUpperCase()}: ${t(`word.${element.elementType}`)}`">
{{ t('message.newFunction') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
:title="t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(element)"
/>
@@ -196,11 +196,11 @@
: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 :title="`${t('word.new').toUpperCase()}: ${t(`word.${element.elementType}`)}`">
{{ t('message.newTriggerFunction') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
:title="t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(element)"
/>
@@ -213,11 +213,11 @@
: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 :title="`${t('word.new').toUpperCase()}: ${t(`word.${element.elementType}`)}`">
{{ t('message.newScheduler') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
:title="t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(element)"
/>
@@ -231,11 +231,11 @@
@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 :title="`${t('word.settings').toUpperCase()}: ${t(`word.${element.elementType}`)}`">
<span class=" text-italic">{{ cutText(element.elementName, 20, true) }}</span>
<span
class="btn btn-clear"
:title="$t('word.close')"
:title="t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(element)"
/>
@@ -248,11 +248,11 @@
: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 :title="`${t('word.settings').toUpperCase()}: ${t(`word.${element.elementType}`)}`">
{{ cutText(element.elementName, 20, true) }}
<span
class="btn btn-clear"
:title="$t('word.close')"
:title="t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(element)"
/>
@@ -268,7 +268,7 @@
<a
class="tab-link workspace-tools-link dropdown-toggle"
tabindex="0"
:title="$t('word.tools')"
:title="t('word.tools')"
>
<i class="mdi mdi-24px mdi-tools" />
</a>
@@ -276,7 +276,13 @@
<li class="menu-item">
<a class="c-hand p-vcentered" @click="showProcessesModal">
<i class="mdi mdi-memory mr-1 tool-icon" />
<span>{{ $t('message.processesList') }}</span>
<span>{{ t('message.processesList') }}</span>
</a>
</li>
<li class="menu-item">
<a class="c-hand p-vcentered" @click="toggleConsole">
<i class="mdi mdi-console-line mr-1 tool-icon" />
<span>{{ t('word.console') }}</span>
</a>
</li>
<li
@@ -286,7 +292,7 @@
>
<a class="c-hand p-vcentered disabled">
<i class="mdi mdi-shape mr-1 tool-icon" />
<span>{{ $t('word.variables') }}</span>
<span>{{ t('word.variables') }}</span>
</a>
</li>
<li
@@ -296,7 +302,7 @@
>
<a class="c-hand p-vcentered disabled">
<i class="mdi mdi-account-group mr-1 tool-icon" />
<span>{{ $t('message.manageUsers') }}</span>
<span>{{ t('message.manageUsers') }}</span>
</a>
</li>
</ul>
@@ -306,7 +312,7 @@
<li class="tab-item">
<a
class="tab-add"
:title="$t('message.openNewTab')"
:title="t('message.openNewTab')"
@click="addQueryTab"
>
<i class="mdi mdi-24px mdi-plus" />
@@ -451,8 +457,9 @@
:schema="tab.schema"
/>
</template>
<WorkspaceQueryConsole v-if="isConsoleOpen(workspace.uid)" :uid="workspace.uid" />
</div>
<div v-else class="connection-panel-wrapper">
<div v-else class="connection-panel-wrapper p-relative">
<WorkspaceEditConnectionPanel :connection="connection" />
</div>
<ModalProcessesList
@@ -470,18 +477,22 @@
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, Prop, ref, watch } from 'vue';
import { ipcRenderer } from 'electron';
import { computed, onMounted, Prop, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import * as Draggable from 'vuedraggable';
import Connection from '@/ipc-api/Connection';
import { useWorkspacesStore, WorkspaceTab } from '@/stores/workspaces';
import { useConsoleStore } from '@/stores/console';
import { ConnectionParams } from 'common/interfaces/antares';
import { useFilters } from '@/composables/useFilters';
import WorkspaceEmptyState from '@/components/WorkspaceEmptyState.vue';
import WorkspaceExploreBar from '@/components/WorkspaceExploreBar.vue';
import WorkspaceEditConnectionPanel from '@/components/WorkspaceEditConnectionPanel.vue';
import WorkspaceTabQuery from '@/components/WorkspaceTabQuery.vue';
import WorkspaceTabTable from '@/components/WorkspaceTabTable.vue';
import WorkspaceQueryConsole from '@/components/WorkspaceQueryConsole.vue';
import WorkspaceTabNewTable from '@/components/WorkspaceTabNewTable.vue';
import WorkspaceTabNewView from '@/components/WorkspaceTabNewView.vue';
@@ -500,7 +511,11 @@ import WorkspaceTabPropsFunction from '@/components/WorkspaceTabPropsFunction.vu
import WorkspaceTabPropsScheduler from '@/components/WorkspaceTabPropsScheduler.vue';
import ModalProcessesList from '@/components/ModalProcessesList.vue';
import ModalDiscardChanges from '@/components/ModalDiscardChanges.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { cutText } = useFilters();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
@@ -512,9 +527,16 @@ const {
selectTab,
newTab,
removeTab,
updateTabs
updateTabs,
selectNextTab,
selectPrevTab
} = workspacesStore;
const consoleStore = useConsoleStore();
const { isConsoleOpen } = storeToRefs(consoleStore);
const { toggleConsole } = consoleStore;
const props = defineProps({
connection: Object as Prop<ConnectionParams>
});
@@ -566,30 +588,13 @@ watch(queryTabs, (newVal, oldVal) => {
});
const addQueryTab = () => {
newTab({ uid: props.connection.uid, type: 'query' });
newTab({ uid: props.connection.uid, type: 'query', schema: workspace.value.breadcrumbs.schema });
};
const getSelectedTab = () => {
return workspace.value.tabs.find(tab => tab.uid === selectedTab.value);
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (!isSelected.value)
return;
if ((e.ctrlKey || e.metaKey) && e.key === 't' && !e.altKey) { // CTRL|Command + t
addQueryTab();
}
if ((e.ctrlKey || e.metaKey) && e.key === 'w' && !e.altKey) { // CTRL|Command + w
const currentTab = getSelectedTab();
if (currentTab)
closeTab(currentTab);
}
};
const openAsPermanentTab = (tab: WorkspaceTab) => {
const permanentTabs = {
table: 'data',
@@ -640,24 +645,43 @@ const addWheelEvent = () => {
}
};
const cutText = (string: string) => {
const limit = 20;
const escapedString = string.replace(/\s{2,}/g, ' ');
if (escapedString.length > limit)
return `${escapedString.substr(0, limit)}...`;
return escapedString;
};
(async () => {
window.addEventListener('keydown', onKey);
await addWorkspace(props.connection.uid);
const isInitiated = await Connection.checkConnection(props.connection.uid);
if (isInitiated)
connectWorkspace(props.connection);
})();
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
onMounted(() => {
ipcRenderer.on('open-new-tab', () => {
if (!isSelected.value) return;
addQueryTab();
});
ipcRenderer.on('close-tab', () => {
if (!isSelected.value) return;
const currentTab = getSelectedTab();
if (currentTab)
closeTab(currentTab);
});
ipcRenderer.on('next-tab', () => {
if (!isSelected.value) return;
selectNextTab({ uid: props.connection.uid });
});
ipcRenderer.on('prev-tab', () => {
if (!isSelected.value) return;
selectPrevTab({ uid: props.connection.uid });
});
for (let i = 1; i <= 9; i++) {
ipcRenderer.on(`select-tab-${i}`, () => {
if (!isSelected.value) return;
if (workspace.value.tabs[i-1])
selectTab({ uid: props.connection.uid, tab: workspace.value.tabs[i-1].uid });
});
}
});
</script>
@@ -667,8 +691,9 @@ onBeforeUnmount(() => {
margin: 0;
.workspace-tabs {
overflow: hidden;
overflow-y: hidden;
height: calc(100vh - #{$excluding-size});
position: relative;
.tab-block {
margin-top: 0;

View File

@@ -411,12 +411,12 @@ const workspacesStore = useWorkspacesStore();
const { connectWorkspace, selectWorkspace } = workspacesStore;
const clients = ref([
const clients = [
{ name: 'MySQL', slug: 'mysql' },
{ name: 'MariaDB', slug: 'maria' },
{ name: 'PostgreSQL', slug: 'pg' },
{ name: 'SQLite', slug: 'sqlite' }
]);
];
const connection = ref({
name: '',
@@ -560,7 +560,8 @@ setTimeout(() => {
.connection-panel {
margin-left: auto;
margin-right: auto;
margin-bottom: 1rem;
margin-bottom: .5rem;
margin-top: 1.5rem;
.panel {
min-width: 450px;

View File

@@ -253,6 +253,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="localConnection.untrustedConnection" type="checkbox"><i class="form-icon" /> {{ t('message.untrustedConnection') }}
</label>
</div>
</div>
</fieldset>
</form>
</div>
@@ -416,12 +424,12 @@ const { editConnection } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const { connectWorkspace } = useWorkspacesStore();
const clients = ref([
const clients = [
{ name: 'MySQL', slug: 'mysql' },
{ name: 'MariaDB', slug: 'maria' },
{ name: 'PostgreSQL', slug: 'pg' },
{ name: 'SQLite', slug: 'sqlite' }
]);
];
const firstInput: Ref<HTMLInputElement> = ref(null);
const localConnection: Ref<ConnectionParams & { pgConnString: string }> = ref(null);
@@ -543,7 +551,8 @@ localConnection.value = JSON.parse(JSON.stringify(props.connection));
.connection-panel {
margin-left: auto;
margin-right: auto;
margin-bottom: 1rem;
margin-bottom: .5rem;
margin-top: 1.5rem;
.panel {
min-width: 450px;

View File

@@ -15,18 +15,18 @@
<i
v-if="customizations.schemas"
class="mdi mdi-18px mdi-database-plus c-hand mr-2"
:title="$t('message.createNewSchema')"
:title="t('message.createNewSchema')"
@click="showNewDBModal"
/>
<i
class="mdi mdi-18px mdi-refresh c-hand mr-2"
:class="{'rotate':isRefreshing}"
:title="$t('word.refresh')"
:title="t('word.refresh')"
@click="refresh"
/>
<i
class="mdi mdi-18px mdi-power c-hand"
:title="$t('word.disconnect')"
:title="t('word.disconnect')"
@click="disconnectWorkspace(connection.uid)"
/>
</span>
@@ -38,7 +38,7 @@
v-model="searchTerm"
class="form-input input-sm"
type="text"
:placeholder="$t('message.searchForElements')"
:placeholder="t('message.searchForElements')"
>
<i v-if="!searchTerm" class="form-icon mdi mdi-magnify mdi-18px" />
<i
@@ -133,6 +133,9 @@ import TableContext from '@/components/WorkspaceExploreBarTableContext.vue';
import MiscContext from '@/components/WorkspaceExploreBarMiscContext.vue';
import MiscFolderContext from '@/components/WorkspaceExploreBarMiscFolderContext.vue';
import ModalNewSchema from '@/components/ModalNewSchema.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
connection: Object,
@@ -235,17 +238,7 @@ const refresh = async () => {
}
};
const explorebarSearch = (e: KeyboardEvent) => {
if (e.code === 'Backspace') {
e.preventDefault();
if (searchTerm.value.length)
searchTerm.value = searchTerm.value.slice(0, -1);
else
return;
}
else if (e.key.length > 1)// Prevent non-alphanumerics
return;
const explorebarSearch = () => {
searchInput.value.focus();
};

View File

@@ -8,7 +8,7 @@
class="context-element"
@click="runElementCheck"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ $t('word.run') }}</span>
<span class="d-flex"><i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ t('word.run') }}</span>
</div>
<div
v-if="selectedMisc.type === 'trigger' && customizations.triggerEnableDisable"
@@ -16,10 +16,10 @@
@click="toggleTrigger"
>
<span v-if="!selectedMisc.enabled" class="d-flex">
<i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ $t('word.enable') }}
<i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ t('word.enable') }}
</span>
<span v-else class="d-flex">
<i class="mdi mdi-18px mdi-pause text-light pr-1" /> {{ $t('word.disable') }}
<i class="mdi mdi-18px mdi-pause text-light pr-1" /> {{ t('word.disable') }}
</span>
</div>
<div
@@ -28,14 +28,14 @@
@click="toggleScheduler"
>
<span v-if="!selectedMisc.enabled" class="d-flex">
<i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ $t('word.enable') }}
<i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ t('word.enable') }}
</span>
<span v-else class="d-flex">
<i class="mdi mdi-18px mdi-pause text-light pr-1" /> {{ $t('word.disable') }}
<i class="mdi mdi-18px mdi-pause text-light pr-1" /> {{ t('word.disable') }}
</span>
</div>
<div class="context-element" @click="showDeleteModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-table-remove text-light pr-1" /> {{ $t('word.delete') }}</span>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-remove text-light pr-1" /> {{ t('word.delete') }}</span>
</div>
<ConfirmModal
v-if="isDeleteModal"
@@ -50,7 +50,7 @@
</template>
<template #body>
<div class="mb-2">
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedMisc.name }}</b>"?
{{ t('message.deleteCorfirm') }} "<b>{{ selectedMisc.name }}</b>"?
</div>
</template>
</ConfirmModal>

View File

@@ -580,7 +580,7 @@ defineExpose({ selectSchema, schemaAccordion });
transition: opacity 0.2s;
&:hover {
opacity: 0.8;
opacity: 1;
}
&::after {

View File

@@ -4,7 +4,7 @@
@close-context="closeContext"
>
<div class="context-element">
<span class="d-flex"><i class="mdi mdi-18px mdi-plus text-light pr-1" /> {{ $t('word.add') }}</span>
<span class="d-flex"><i class="mdi mdi-18px mdi-plus text-light pr-1" /> {{ t('word.add') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu">
<div
@@ -12,49 +12,49 @@
class="context-element"
@click="openCreateTableTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table text-light pr-1" /> {{ $t('word.table') }}</span>
<span class="d-flex"><i class="mdi mdi-18px mdi-table text-light pr-1" /> {{ t('word.table') }}</span>
</div>
<div
v-if="workspace.customizations.viewAdd"
class="context-element"
@click="openCreateViewTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-eye text-light pr-1" /> {{ $t('word.view') }}</span>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-eye text-light pr-1" /> {{ t('word.view') }}</span>
</div>
<div
v-if="workspace.customizations.triggerAdd"
class="context-element"
@click="openCreateTriggerTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-cog text-light pr-1" /> {{ $tc('word.trigger', 1) }}</span>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-cog text-light pr-1" /> {{ t('word.trigger', 1) }}</span>
</div>
<div
v-if="workspace.customizations.routineAdd"
class="context-element"
@click="openCreateRoutineTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle pr-1" /> {{ $tc('word.storedRoutine', 1) }}</span>
<span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle pr-1" /> {{ t('word.storedRoutine', 1) }}</span>
</div>
<div
v-if="workspace.customizations.functionAdd"
class="context-element"
@click="openCreateFunctionTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box pr-1" /> {{ $tc('word.function', 1) }}</span>
<span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box pr-1" /> {{ t('word.function', 1) }}</span>
</div>
<div
v-if="workspace.customizations.triggerFunctionAdd"
class="context-element"
@click="openCreateTriggerFunctionTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-cog-clockwise pr-1" /> {{ $tc('word.triggerFunction', 1) }}</span>
<span class="d-flex"><i class="mdi mdi-18px mdi-cog-clockwise pr-1" /> {{ t('word.triggerFunction', 1) }}</span>
</div>
<div
v-if="workspace.customizations.schedulerAdd"
class="context-element"
@click="openCreateSchedulerTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-calendar-clock text-light pr-1" /> {{ $tc('word.scheduler', 1) }}</span>
<span class="d-flex"><i class="mdi mdi-18px mdi-calendar-clock text-light pr-1" /> {{ t('word.scheduler', 1) }}</span>
</div>
</div>
</div>
@@ -63,28 +63,28 @@
class="context-element"
@click="showExportSchemaModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-arrow-down text-light pr-1" /> {{ $t('word.export') }}</span>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-arrow-down text-light pr-1" /> {{ t('word.export') }}</span>
</div>
<div
v-if="workspace.customizations.schemaImport"
class="context-element"
@click="initImport"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-arrow-up text-light pr-1" /> {{ $t('word.import') }}</span>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-arrow-up text-light pr-1" /> {{ t('word.import') }}</span>
</div>
<div
v-if="workspace.customizations.schemaEdit"
class="context-element"
@click="showEditModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-edit text-light pr-1" /> {{ $t('word.edit') }}</span>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-edit text-light pr-1" /> {{ t('word.edit') }}</span>
</div>
<div
v-if="workspace.customizations.schemaDrop"
class="context-element"
@click="showDeleteModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-remove text-light pr-1" /> {{ $t('word.delete') }}</span>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-remove text-light pr-1" /> {{ t('word.delete') }}</span>
</div>
<ConfirmModal
@@ -95,12 +95,12 @@
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-remove mr-1" />
<span class="cut-text">{{ $t('message.deleteSchema') }}</span>
<span class="cut-text">{{ t('message.deleteSchema') }}</span>
</div>
</template>
<template #body>
<div class="mb-2">
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedSchema }}</b>"?
{{ t('message.deleteCorfirm') }} "<b>{{ selectedSchema }}</b>"?
</div>
</template>
</ConfirmModal>
@@ -135,6 +135,9 @@ import ModalExportSchema from '@/components/ModalExportSchema.vue';
import ModalImportSchema from '@/components/ModalImportSchema.vue';
import Schema from '@/ipc-api/Schema';
import Application from '@/ipc-api/Application';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
contextEvent: MouseEvent,

View File

@@ -0,0 +1,179 @@
<template>
<div
ref="wrapper"
class="query-console-wrapper"
@mouseenter="isHover = true"
@mouseleave="isHover = false"
>
<div ref="resizer" class="query-console-resizer" />
<div
id="query-console"
ref="queryConsole"
class="query-console column col-12"
:style="{height: localHeight ? localHeight+'px' : ''}"
>
<div class="query-console-header">
<div>{{ t('word.console') }}</div>
<button class="btn btn-clear mr-1" @click="resizeConsole(0)" />
</div>
<div ref="queryConsoleBody" class="query-console-body">
<div
v-for="(wLog, i) in workspaceLogs"
:key="i"
class="query-console-log"
tabindex="0"
@contextmenu.prevent="contextMenu($event, wLog)"
>
<span class="type-datetime">{{ moment(wLog.date).format('HH:mm:ss') }}</span>: <code class="query-console-log-sql">{{ wLog.sql }}</code>
</div>
</div>
</div>
</div>
<BaseContextMenu
v-if="isContext"
:context-event="contextEvent"
@close-context="isContext = false"
>
<div class="context-element" @click="copyQuery">
<span class="d-flex"><i class="mdi mdi-18px mdi-content-copy text-light pr-1" /> {{ t('word.copy') }}</span>
</div>
</BaseContextMenu>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, ref, Ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import * as moment from 'moment';
import { useConsoleStore } from '@/stores/console';
import { storeToRefs } from 'pinia';
import BaseContextMenu from '@/components/BaseContextMenu.vue';
const { t } = useI18n();
const consoleStore = useConsoleStore();
const { resizeConsole, getLogsByWorkspace } = consoleStore;
const { consoleHeight } = storeToRefs(consoleStore);
const props = defineProps({
uid: String
});
const wrapper: Ref<HTMLInputElement> = ref(null);
const queryConsole: Ref<HTMLInputElement> = ref(null);
const queryConsoleBody: Ref<HTMLInputElement> = ref(null);
const resizer: Ref<HTMLInputElement> = ref(null);
const localHeight = ref(250);
const isHover = ref(false);
const isContext = ref(false);
const contextQuery: Ref<string> = ref(null);
const contextEvent: Ref<MouseEvent> = ref(null);
const resize = (e: MouseEvent) => {
const el = queryConsole.value;
let consoleHeight = el.getBoundingClientRect().bottom - e.pageY;
if (consoleHeight > 400) consoleHeight = 400;
localHeight.value = consoleHeight;
};
const workspaceLogs = computed(() => {
return getLogsByWorkspace(props.uid);
});
const stopResize = () => {
if (localHeight.value < 0) localHeight.value = 0;
resizeConsole(localHeight.value);
window.removeEventListener('mousemove', resize);
window.removeEventListener('mouseup', stopResize);
};
const contextMenu = (event: MouseEvent, wLog: {date: Date; sql: string}) => {
contextEvent.value = event;
contextQuery.value = wLog.sql;
isContext.value = true;
};
const copyQuery = () => {
navigator.clipboard.writeText(contextQuery.value);
isContext.value = false;
};
watch(workspaceLogs, async () => {
if (!isHover.value) {
await nextTick();
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
}
});
onMounted(() => {
localHeight.value = consoleHeight.value;
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
});
onMounted(() => {
resizer.value.addEventListener('mousedown', (e: MouseEvent) => {
e.preventDefault();
window.addEventListener('mousemove', resize);
window.addEventListener('mouseup', stopResize);
});
});
</script>
<style lang="scss" scoped>
.query-console-wrapper{
width: 100%;
z-index: 9;
margin-top: auto;
position: absolute;
bottom: 0;
.query-console-resizer{
height: 4px;
top: -1px;
width: 100%;
cursor: ns-resize;
position: absolute;
z-index: 99;
transition: background 0.2s;
&:hover {
background: rgba($primary-color, 50%);
}
}
.query-console {
padding: 0;
padding-bottom: $footer-height;
.query-console-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px;
font-weight: 700;
}
.query-console-body {
overflow: auto;
display: flex;
flex-direction: column;
max-height: 100%;
padding: 0 6px 3px;
.query-console-log {
padding: 1px 3px;
margin: 1px 0;
border-radius: $border-radius;
.query-console-log-sql {
font-size: 95%;
opacity: .8;
font-weight: 700;
&:hover {
user-select: text;
}
}
}
}
}
}
</style>

View File

@@ -179,6 +179,8 @@
import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import Functions from '@/ipc-api/Functions';
import { storeToRefs } from 'pinia';
import { useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import BaseLoader from '@/components/BaseLoader.vue';
@@ -200,6 +202,7 @@ const props = defineProps({
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { consoleHeight } = storeToRefs(useConsoleStore());
const {
getWorkspace,
@@ -274,8 +277,12 @@ const clearChanges = () => {
const resizeQueryEditor = () => {
if (queryEditor.value) {
let sizeToSubtract = 0;
const footer = document.getElementById('footer');
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
if (footer) sizeToSubtract += footer.offsetHeight;
sizeToSubtract += consoleHeight.value;
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - sizeToSubtract;
editorHeight.value = size;
queryEditor.value.editor.resize();
}
@@ -311,6 +318,10 @@ watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
watch(consoleHeight, () => {
resizeQueryEditor();
});
originalFunction.value = {
sql: customizations.value.functionSql,
language: customizations.value.languages ? customizations.value.languages[0] : null,

View File

@@ -11,27 +11,27 @@
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
<span>{{ t('word.save') }}</span>
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
:title="t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<span>{{ t('word.clear') }}</span>
</button>
<div class="divider-vert py-3" />
<button class="btn btn-dark btn-sm" @click="showParamsModal">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span>
<span>{{ t('word.parameters') }}</span>
</button>
</div>
<div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')">
<div class="d-flex" :title="t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div>
</div>
@@ -42,7 +42,7 @@
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.name') }}
{{ t('word.name') }}
</label>
<input
ref="firstInput"
@@ -55,7 +55,7 @@
<div v-if="customizations.languages" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.language') }}
{{ t('word.language') }}
</label>
<BaseSelect
v-model="localRoutine.language"
@@ -67,11 +67,11 @@
<div v-if="customizations.definer" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.definer') }}
{{ t('word.definer') }}
</label>
<BaseSelect
v-model="localRoutine.definer"
:options="[{value: '', name:$t('message.currentUser')}, ...workspace.users]"
:options="[{value: '', name:t('message.currentUser')}, ...workspace.users]"
:option-label="(user: any) => user.value === '' ? user.name : `${user.name}@${user.host}`"
:option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select"
@@ -81,7 +81,7 @@
<div v-if="customizations.comment" class="column">
<div class="form-group">
<label class="form-label">
{{ $t('word.comment') }}
{{ t('word.comment') }}
</label>
<input
v-model="localRoutine.comment"
@@ -93,7 +93,7 @@
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.sqlSecurity') }}
{{ t('message.sqlSecurity') }}
</label>
<BaseSelect
v-model="localRoutine.security"
@@ -105,7 +105,7 @@
<div v-if="customizations.procedureDataAccess" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.dataAccess') }}
{{ t('message.dataAccess') }}
</label>
<BaseSelect
v-model="localRoutine.dataAccess"
@@ -118,7 +118,7 @@
<div class="form-group">
<label class="form-label d-invisible">.</label>
<label class="form-checkbox form-inline">
<input v-model="localRoutine.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
<input v-model="localRoutine.deterministic" type="checkbox"><i class="form-icon" /> {{ t('word.deterministic') }}
</label>
</div>
</div>
@@ -126,7 +126,7 @@
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.routineBody') }}</label>
<label class="form-label ml-2">{{ t('message.routineBody') }}</label>
<QueryEditor
v-show="isSelected"
ref="queryEditor"
@@ -151,6 +151,8 @@
import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import Routines from '@/ipc-api/Routines';
import { storeToRefs } from 'pinia';
import { useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import QueryEditor from '@/components/QueryEditor.vue';
@@ -158,6 +160,9 @@ import BaseLoader from '@/components/BaseLoader.vue';
import WorkspaceTabPropsRoutineParamsModal from '@/components/WorkspaceTabPropsRoutineParamsModal.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import { FunctionParam } from 'common/interfaces/antares';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
tabUid: String,
@@ -169,6 +174,7 @@ const props = defineProps({
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { consoleHeight } = storeToRefs(useConsoleStore());
const {
getWorkspace,
@@ -243,8 +249,12 @@ const clearChanges = () => {
const resizeQueryEditor = () => {
if (queryEditor.value) {
let sizeToSubtract = 0;
const footer = document.getElementById('footer');
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
if (footer) sizeToSubtract += footer.offsetHeight;
sizeToSubtract += consoleHeight.value;
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - sizeToSubtract;
editorHeight.value = size;
queryEditor.value.editor.resize();
}
@@ -280,6 +290,10 @@ watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
watch(consoleHeight, () => {
resizeQueryEditor();
});
originalRoutine.value = {
sql: customizations.value.functionSql,
language: customizations.value.languages ? customizations.value.languages[0] : null,

View File

@@ -125,10 +125,12 @@
</template>
<script setup lang="ts">
import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Prop, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { EventInfos } from 'common/interfaces/antares';
import { ConnectionParams, EventInfos } from 'common/interfaces/antares';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import BaseLoader from '@/components/BaseLoader.vue';
@@ -141,7 +143,7 @@ const { t } = useI18n();
const props = defineProps({
tabUid: String,
connection: Object,
connection: Object as Prop<ConnectionParams>,
tab: Object,
isSelected: Boolean,
schema: String
@@ -149,6 +151,7 @@ const props = defineProps({
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { consoleHeight } = storeToRefs(useConsoleStore());
const {
getWorkspace,
@@ -233,8 +236,12 @@ const clearChanges = () => {
const resizeQueryEditor = () => {
if (queryEditor.value) {
let sizeToSubtract = 0;
const footer = document.getElementById('footer');
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
if (footer) sizeToSubtract += footer.offsetHeight;
sizeToSubtract += consoleHeight.value;
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - sizeToSubtract;
editorHeight.value = size;
queryEditor.value.editor.resize();
}
@@ -270,6 +277,10 @@ watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
watch(consoleHeight, () => {
resizeQueryEditor();
});
originalScheduler.value = {
definer: '',
sql: 'BEGIN\r\n\r\nEND',

View File

@@ -332,7 +332,10 @@ const addField = () => {
collation: defaultCollation.value,
autoIncrement: false,
onUpdate: '',
comment: ''
comment: '',
alias: '',
tableAlias: '',
orgTable: ''
});
setTimeout(() => {

View File

@@ -30,7 +30,7 @@
</div>
</div>
</div>
<div class="container">
<div class="px-2">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
@@ -118,8 +118,10 @@
import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import { useConsoleStore } from '@/stores/console';
import QueryEditor from '@/components/QueryEditor.vue';
import BaseLoader from '@/components/BaseLoader.vue';
import Triggers from '@/ipc-api/Triggers';
@@ -137,6 +139,7 @@ const props = defineProps({
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { consoleHeight } = storeToRefs(useConsoleStore());
const {
getWorkspace,
@@ -254,8 +257,12 @@ const clearChanges = () => {
const resizeQueryEditor = () => {
if (queryEditor.value) {
let sizeToSubtract = 0;
const footer = document.getElementById('footer');
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
if (footer) sizeToSubtract += footer.offsetHeight;
sizeToSubtract += consoleHeight.value;
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - sizeToSubtract;
editorHeight.value = size;
queryEditor.value.editor.resize();
}
@@ -264,7 +271,7 @@ const resizeQueryEditor = () => {
const onKey = (e: KeyboardEvent) => {
if (props.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (e.ctrlKey && e.key === 's') { // CTRL + S
if (isChanged.value)
saveChanges();
}
@@ -288,6 +295,10 @@ originalTrigger.value = {
name: ''
};
watch(consoleHeight, () => {
resizeQueryEditor();
});
localTrigger.value = JSON.parse(JSON.stringify(originalTrigger.value));
setTimeout(() => {

View File

@@ -99,6 +99,8 @@ import { Ace } from 'ace-builds';
import { useI18n } from 'vue-i18n';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import { storeToRefs } from 'pinia';
import { useConsoleStore } from '@/stores/console';
import BaseLoader from '@/components/BaseLoader.vue';
import QueryEditor from '@/components/QueryEditor.vue';
import Functions from '@/ipc-api/Functions';
@@ -116,6 +118,7 @@ const props = defineProps({
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { consoleHeight } = storeToRefs(useConsoleStore());
const {
getWorkspace,
@@ -189,8 +192,12 @@ const clearChanges = () => {
const resizeQueryEditor = () => {
if (queryEditor.value) {
let sizeToSubtract = 0;
const footer = document.getElementById('footer');
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
if (footer) sizeToSubtract += footer.offsetHeight;
sizeToSubtract += consoleHeight.value;
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - sizeToSubtract;
editorHeight.value = size;
queryEditor.value.editor.resize();
}
@@ -212,6 +219,18 @@ originalFunction.value = {
name: ''
};
watch(() => props.isSelected, (val) => {
if (val) changeBreadcrumbs({ schema: props.schema });
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
watch(consoleHeight, () => {
resizeQueryEditor();
});
localFunction.value = JSON.parse(JSON.stringify(originalFunction.value));
setTimeout(() => {
@@ -238,12 +257,4 @@ onUnmounted(() => {
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
watch(() => props.isSelected, (val) => {
if (val) changeBreadcrumbs({ schema: props.schema });
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
</script>

View File

@@ -107,6 +107,8 @@
import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import BaseLoader from '@/components/BaseLoader.vue';
@@ -126,6 +128,7 @@ const props = defineProps({
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { consoleHeight } = storeToRefs(useConsoleStore());
const {
getWorkspace,
@@ -202,8 +205,12 @@ const clearChanges = () => {
const resizeQueryEditor = () => {
if (queryEditor.value) {
let sizeToSubtract = 0;
const footer = document.getElementById('footer');
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
if (footer) sizeToSubtract += footer.offsetHeight;
sizeToSubtract += consoleHeight.value;
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - sizeToSubtract;
editorHeight.value = size;
queryEditor.value.editor.resize();
}
@@ -233,6 +240,10 @@ watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
watch(consoleHeight, () => {
resizeQueryEditor();
});
originalView.value = {
algorithm: 'UNDEFINED',
definer: '',

View File

@@ -194,6 +194,8 @@
<script setup lang="ts">
import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { storeToRefs } from 'pinia';
import { useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import { uidGen } from 'common/libs/uidGen';
@@ -218,6 +220,7 @@ const props = defineProps({
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { consoleHeight } = storeToRefs(useConsoleStore());
const {
getWorkspace,
@@ -344,8 +347,12 @@ const clearChanges = () => {
const resizeQueryEditor = () => {
if (queryEditor.value) {
let sizeToSubtract = 0;
const footer = document.getElementById('footer');
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
if (footer) sizeToSubtract += footer.offsetHeight;
sizeToSubtract += consoleHeight.value;
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - sizeToSubtract;
editorHeight.value = size;
queryEditor.value.editor.resize();
}
@@ -443,6 +450,10 @@ watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
watch(consoleHeight, () => {
resizeQueryEditor();
});
(async () => {
await getFunctionData();
queryEditor.value.editor.session.setValue(localFunction.value.sql);

View File

@@ -168,6 +168,8 @@ import { Component, computed, onUnmounted, onBeforeUnmount, onMounted, Ref, ref,
import { AlterRoutineParams, FunctionParam, RoutineInfos } from 'common/interfaces/antares';
import { Ace } from 'ace-builds';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import { uidGen } from 'common/libs/uidGen';
@@ -190,6 +192,7 @@ const props = defineProps({
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { consoleHeight } = storeToRefs(useConsoleStore());
const {
getWorkspace,
@@ -316,8 +319,12 @@ const clearChanges = () => {
const resizeQueryEditor = () => {
if (queryEditor.value) {
let sizeToSubtract = 0;
const footer = document.getElementById('footer');
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
if (footer) sizeToSubtract += footer.offsetHeight;
sizeToSubtract += consoleHeight.value;
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - sizeToSubtract;
editorHeight.value = size;
queryEditor.value.editor.resize();
}
@@ -413,6 +420,10 @@ watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
watch(consoleHeight, () => {
resizeQueryEditor();
});
(async () => {
await getRoutineData();
queryEditor.value.editor.session.setValue(localRoutine.value.sql);

View File

@@ -127,6 +127,8 @@ import { AlterEventParams, EventInfos } from 'common/interfaces/antares';
import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import BaseLoader from '@/components/BaseLoader.vue';
@@ -147,6 +149,7 @@ const props = defineProps({
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { consoleHeight } = storeToRefs(useConsoleStore());
const {
getWorkspace,
@@ -269,8 +272,12 @@ const clearChanges = () => {
const resizeQueryEditor = () => {
if (queryEditor.value) {
let sizeToSubtract = 0;
const footer = document.getElementById('footer');
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
if (footer) sizeToSubtract += footer.offsetHeight;
sizeToSubtract += consoleHeight.value;
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - sizeToSubtract;
editorHeight.value = size;
queryEditor.value.editor.resize();
}
@@ -331,6 +338,10 @@ watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
watch(consoleHeight, () => {
resizeQueryEditor();
});
(async () => {
await getSchedulerData();
queryEditor.value.editor.session.setValue(localScheduler.value.sql);

View File

@@ -2,6 +2,7 @@
<ConfirmModal
:confirm-text="t('word.confirm')"
size="400"
:disable-autofocus="true"
@confirm="confirmOptionsChange"
@hide="$emit('hide')"
>

View File

@@ -126,8 +126,10 @@
<script setup lang="ts">
import { Component, computed, onMounted, onUnmounted, onUpdated, Prop, ref, Ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useWorkspacesStore } from '@/stores/workspaces';
import { useConsoleStore } from '@/stores/console';
import * as Draggable from 'vuedraggable';
import TableRow from '@/components/WorkspaceTabPropsTableRow.vue';
import TableContext from '@/components/WorkspaceTabPropsTableContext.vue';
@@ -150,9 +152,12 @@ const props = defineProps({
const emit = defineEmits(['add-new-index', 'add-to-index', 'rename-field', 'duplicate-field', 'remove-field']);
const workspacesStore = useWorkspacesStore();
const consoleStore = useConsoleStore();
const { getWorkspace } = workspacesStore;
const { consoleHeight } = storeToRefs(consoleStore);
const tableWrapper: Ref<HTMLDivElement> = ref(null);
const propTable: Ref<HTMLDivElement> = ref(null);
const resultTable: Ref<Component> = ref(null);
@@ -172,8 +177,13 @@ const resizeResults = () => {
const el = tableWrapper.value;
if (el) {
let sizeToSubtract = 0;
const footer = document.getElementById('footer');
const size = window.innerHeight - el.getBoundingClientRect().top - footer.offsetHeight;
if (footer) sizeToSubtract += footer.offsetHeight;
sizeToSubtract += consoleHeight.value;
const size = window.innerHeight - el.getBoundingClientRect().top - sizeToSubtract;
resultsSize.value = size;
}
}
@@ -216,6 +226,10 @@ watch(fieldsLength, () => {
refreshScroller();
});
watch(consoleHeight, () => {
resizeResults();
});
onUpdated(() => {
if (propTable.value)
refreshScroller();

View File

@@ -30,7 +30,7 @@
</div>
</div>
</div>
<div class="container">
<div class="px-2">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
@@ -118,6 +118,8 @@
import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import QueryEditor from '@/components/QueryEditor.vue';
@@ -139,6 +141,7 @@ const props = defineProps({
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { consoleHeight } = storeToRefs(useConsoleStore());
const {
getWorkspace,
@@ -304,8 +307,12 @@ const clearChanges = () => {
const resizeQueryEditor = () => {
if (queryEditor.value) {
let sizeToSubtract = 0;
const footer = document.getElementById('footer');
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
if (footer) sizeToSubtract += footer.offsetHeight;
sizeToSubtract += consoleHeight.value;
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - sizeToSubtract;
editorHeight.value = size;
queryEditor.value.editor.resize();
}
@@ -345,6 +352,10 @@ watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
watch(consoleHeight, () => {
resizeQueryEditor();
});
(async () => {
await getTriggerData();
queryEditor.value.editor.session.setValue(localTrigger.value.sql);

View File

@@ -84,8 +84,10 @@
import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import { useConsoleStore } from '@/stores/console';
import BaseLoader from '@/components/BaseLoader.vue';
import QueryEditor from '@/components/QueryEditor.vue';
import Functions from '@/ipc-api/Functions';
@@ -104,6 +106,7 @@ const props = defineProps({
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { consoleHeight } = storeToRefs(useConsoleStore());
const {
getWorkspace,
@@ -209,8 +212,12 @@ const clearChanges = () => {
const resizeQueryEditor = () => {
if (queryEditor.value) {
let sizeToSubtract = 0;
const footer = document.getElementById('footer');
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
if (footer) sizeToSubtract += footer.offsetHeight;
sizeToSubtract += consoleHeight.value;
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - sizeToSubtract;
editorHeight.value = size;
queryEditor.value.editor.resize();
}
@@ -242,6 +249,10 @@ watch(() => props.function, async () => {
}
});
watch(consoleHeight, () => {
resizeQueryEditor();
});
watch(() => props.isSelected, (val) => {
if (val) changeBreadcrumbs({ schema: props.schema });
});

View File

@@ -12,6 +12,7 @@
<div class="workspace-query-runner column col-12">
<QueryEditor
v-show="isSelected"
id="query-editor"
ref="queryEditor"
v-model="query"
:auto-focus="true"
@@ -111,6 +112,9 @@
<li class="menu-item">
<a class="c-hand" @click="downloadTable('csv')">CSV</a>
</li>
<li class="menu-item">
<a class="c-hand" @click="downloadTable('sql')">SQL INSERT</a>
</li>
</ul>
</div>
<div class="input-group pr-2" :title="t('message.commitMode')">
@@ -187,18 +191,20 @@
import { Component, computed, onBeforeUnmount, onMounted, Prop, ref, Ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { format } from 'sql-formatter';
import { ConnectionParams } from 'common/interfaces/antares';
import { useHistoryStore } from '@/stores/history';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import { useResultTables } from '@/composables/useResultTables';
import { useConsoleStore } from '@/stores/console';
import Schema from '@/ipc-api/Schema';
import QueryEditor from '@/components/QueryEditor.vue';
import BaseLoader from '@/components/BaseLoader.vue';
import WorkspaceTabQueryTable from '@/components/WorkspaceTabQueryTable.vue';
import WorkspaceTabQueryEmptyState from '@/components/WorkspaceTabQueryEmptyState.vue';
import ModalHistory from '@/components/ModalHistory.vue';
import { useResultTables } from '@/composables/useResultTables';
import BaseSelect from '@/components/BaseSelect.vue';
const { t } = useI18n();
@@ -223,6 +229,8 @@ const { saveHistory } = useHistoryStore();
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { consoleHeight } = storeToRefs(useConsoleStore());
const {
getWorkspace,
changeBreadcrumbs,
@@ -289,6 +297,10 @@ watch(selectedSchema, () => {
const runQuery = async (query: string) => {
if (!query || isQuering.value) return;
isQuering.value = true;
const selectedQuery = queryEditor.value.editor.getSelectedText();
if (selectedQuery) query = selectedQuery;
clearTabData();
queryTable.value.resetSort();
@@ -365,14 +377,17 @@ const resize = (e: MouseEvent) => {
const el = queryEditor.value.$el;
const queryFooterHeight = queryAreaFooter.value.clientHeight;
const bottom = e.pageY || resizer.value.getBoundingClientRect().bottom;
const maxHeight = window.innerHeight - 100 - queryFooterHeight;
const maxHeight = window.innerHeight - 100 - queryFooterHeight - consoleHeight.value;
let localEditorHeight = bottom - el.getBoundingClientRect().top;
if (localEditorHeight > maxHeight) localEditorHeight = maxHeight;
if (localEditorHeight < 50) localEditorHeight = 50;
editorHeight.value = localEditorHeight;
};
const resizeResults = () => queryTable.value.resizeResults();
const onWindowResize = (e: MouseEvent) => {
if (!queryEditor.value) return;
const el = queryEditor.value.$el;
const queryFooterHeight = queryAreaFooter.value.clientHeight;
const bottom = e.pageY || resizer.value.getBoundingClientRect().bottom;
@@ -386,7 +401,7 @@ const onWindowResize = (e: MouseEvent) => {
const stopResize = () => {
window.removeEventListener('mousemove', resize);
if (queryTable.value && results.value.length)
queryTable.value.resizeResults();
resizeResults();
if (queryEditor.value)
queryEditor.value.editor.resize();
@@ -434,7 +449,7 @@ const clear = () => {
clearTabData();
};
const downloadTable = (format: 'csv' | 'json') => {
const downloadTable = (format: 'csv' | 'json' | 'sql') => {
queryTable.value.downloadTable(format, `${props.tab.type}-${props.tab.index}`);
};
@@ -476,6 +491,8 @@ const rollbackTab = async () => {
isQuering.value = false;
};
defineExpose({ resizeResults });
query.value = props.tab.content as string;
selectedSchema.value = props.tab.schema || breadcrumbsSchema.value;

View File

@@ -14,10 +14,12 @@
:context-event="contextEvent"
:selected-rows="selectedRows"
:selected-cell="selectedCell"
:mode="mode"
@show-delete-modal="showDeleteConfirmModal"
@set-null="setNull"
@copy-cell="copyCell"
@copy-row="copyRow"
@duplicate-row="duplicateRow"
@close-context="closeContext"
/>
<ul v-if="resultsWithRows.length > 1" class="tab tab-block result-tabs">
@@ -118,33 +120,44 @@ import { storeToRefs } from 'pinia';
import { uidGen } from 'common/libs/uidGen';
import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces';
import { arrayToFile } from '../libs/arrayToFile';
import { useConsoleStore } from '@/stores/console';
import { exportRows } from '../libs/exportRows';
import { TEXT, LONG_TEXT, BLOB } from 'common/fieldTypes';
import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
import WorkspaceTabQueryTableRow from '@/components/WorkspaceTabQueryTableRow.vue';
import TableContext from '@/components/WorkspaceTabQueryTableContext.vue';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import moment from 'moment';
import * as moment from 'moment';
import { useI18n } from 'vue-i18n';
import { TableField, QueryResult } from 'common/interfaces/antares';
import { TableUpdateParams } from 'common/interfaces/tableApis';
import { jsonToSqlInsert } from 'common/libs/sqlUtils';
import { unproxify } from '@/libs/unproxify';
const { t } = useI18n();
const settingsStore = useSettingsStore();
const consoleStore = useConsoleStore();
const { getWorkspace } = useWorkspacesStore();
const { dataTabLimit: pageSize } = storeToRefs(settingsStore);
const { consoleHeight } = storeToRefs(consoleStore);
const props = defineProps({
results: Array as Prop<QueryResult[]>,
connUid: String,
mode: String,
mode: String as Prop<'table' | 'query'>,
isSelected: Boolean,
elementType: { type: String, default: 'table' }
});
const emit = defineEmits(['update-field', 'delete-selected', 'hard-sort']);
const emit = defineEmits([
'update-field',
'delete-selected',
'hard-sort',
'duplicate-row'
]);
const resultTable: Ref<Component & {updateWindow: () => void}> = ref(null);
const tableWrapper: Ref<HTMLDivElement> = ref(null);
@@ -166,6 +179,7 @@ const selectedField = ref(null);
const isEditingRow = ref(false);
const workspaceSchema = computed(() => getWorkspace(props.connUid).breadcrumbs.schema);
const workspaceClient = computed(() => getWorkspace(props.connUid).client);
const primaryField = computed(() => {
const primaryFields = fields.value.filter(field => field.key === 'pri');
@@ -272,6 +286,8 @@ const getSchema = (index: number) => {
};
const getPrimaryValue = (row: any) => {
if (!primaryField.value) return null;
const primaryFieldName = Object.keys(row).find(prop => [
primaryField.value.alias,
primaryField.value.name,
@@ -296,8 +312,13 @@ const resizeResults = () => {
const el = tableWrapper.value;
if (el) {
let sizeToSubtract = 0;
const footer = document.getElementById('footer');
const size = window.innerHeight - el.getBoundingClientRect().top - footer.offsetHeight;
if (footer) sizeToSubtract += footer.offsetHeight;
sizeToSubtract += consoleHeight.value;
const size = window.innerHeight - el.getBoundingClientRect().top - sizeToSubtract;
resultsSize.value = size;
}
resultTable.value.updateWindow();
@@ -321,7 +342,7 @@ const updateField = (payload: { field: string; type: string; content: any }, row
});
const params = {
primary: primaryField.value.name,
primary: primaryField.value?.name,
schema: getSchema(resultsetIndex.value),
table: getTable(resultsetIndex.value),
id: getPrimaryValue(orgRow),
@@ -394,11 +415,59 @@ const copyCell = () => {
navigator.clipboard.writeText(valueToCopy);
};
const copyRow = () => {
const copyRow = (format: string) => {
let contentToCopy;
if (selectedRows.value.length === 1) {
const row = localResults.value.find((row: any) => selectedRows.value.includes(row._antares_id));
const rowToCopy = unproxify(row);
delete rowToCopy._antares_id;
contentToCopy = rowToCopy;
}
else {
contentToCopy = unproxify(localResults.value).filter((row: any) => selectedRows.value.includes(row._antares_id)).map((row: any) => {
delete row._antares_id;
return row;
});
}
if (format === 'json')
navigator.clipboard.writeText(JSON.stringify(contentToCopy));
else if (format === 'sql') {
const sqlInserts = [];
if (!Array.isArray(contentToCopy)) contentToCopy = [contentToCopy];
for (const row of contentToCopy) {
sqlInserts.push(jsonToSqlInsert({
json: row,
client: workspaceClient.value,
fields: fieldsObj.value as {
[key: string]: {type: string; datePrecision: number};
},
table: getTable(resultsetIndex.value)
}));
}
navigator.clipboard.writeText(sqlInserts.join('\n'));
}
else if (format === 'csv') {
const csv = [];
if (!Array.isArray(contentToCopy)) contentToCopy = [contentToCopy];
if (contentToCopy.length)
csv.push(Object.keys(contentToCopy[0]).join(';'));
for (const row of contentToCopy)
csv.push(Object.values(row).map(col => typeof col === 'string' ? `"${col}"` : col).join(';'));
navigator.clipboard.writeText(csv.join('\n'));
}
};
const duplicateRow = () => {
const row = localResults.value.find((row: any) => selectedRows.value.includes(row._antares_id));
const rowToCopy = JSON.parse(JSON.stringify(row));
delete rowToCopy._antares_id;
navigator.clipboard.writeText(JSON.stringify(rowToCopy));
const rowToDuplicate = JSON.parse(JSON.stringify(row));
delete rowToDuplicate._antares_id;
emit('duplicate-row', rowToDuplicate);
};
const applyUpdate = (params: TableUpdateParams) => {
@@ -509,7 +578,7 @@ const selectResultset = (index: number) => {
resultsetIndex.value = index;
};
const downloadTable = (format: 'csv' | 'json', filename: string) => {
const downloadTable = (format: 'csv' | 'json' | 'sql', table: string) => {
if (!sortedResults.value) return;
const rows = JSON.parse(JSON.stringify(sortedResults.value)).map((row: any) => {
@@ -517,10 +586,14 @@ const downloadTable = (format: 'csv' | 'json', filename: string) => {
return row;
});
arrayToFile({
exportRows({
type: format,
content: rows,
filename
fields: fieldsObj.value as {
[key: string]: {type: string; datePrecision: number};
},
client: workspaceClient.value,
table
});
};
@@ -662,6 +735,10 @@ watch(() => props.isSelected, async (val) => {
}
});
watch(consoleHeight, () => {
resizeResults();
});
onUpdated(() => {
if (table.value)
refreshScroller();

View File

@@ -3,7 +3,7 @@
:context-event="contextEvent"
@close-context="closeContext"
>
<div v-if="selectedRows.length === 1" class="context-element">
<div class="context-element">
<span class="d-flex"><i class="mdi mdi-18px mdi-content-copy text-light pr-1" /> {{ t('word.copy') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu">
@@ -16,17 +16,32 @@
<i class="mdi mdi-18px mdi-numeric-0 mdi-rotate-90 text-light pr-1" /> {{ t('word.cell', 1) }}
</span>
</div>
<div
v-if="selectedRows.length === 1"
class="context-element"
@click="copyRow"
>
<div class="context-element" @click="copyRow('json')">
<span class="d-flex">
<i class="mdi mdi-18px mdi-table-row text-light pr-1" /> {{ t('word.row', 1) }}
<i class="mdi mdi-18px mdi-table-row text-light pr-1" /> {{ t('word.row', selectedRows.length) }} (JSON)
</span>
</div>
<div class="context-element" @click="copyRow('csv')">
<span class="d-flex">
<i class="mdi mdi-18px mdi-table-row text-light pr-1" /> {{ t('word.row', selectedRows.length) }} (CSV)
</span>
</div>
<div class="context-element" @click="copyRow('sql')">
<span class="d-flex">
<i class="mdi mdi-18px mdi-table-row text-light pr-1" /> {{ t('word.row', selectedRows.length) }} (SQL INSERT)
</span>
</div>
</div>
</div>
<div
v-if="selectedRows.length === 1 && selectedCell.isEditable && mode === 'table'"
class="context-element"
@click="duplicateRow"
>
<span class="d-flex">
<i class="mdi mdi-18px mdi-content-duplicate text-light pr-1" /> {{ t('word.duplicate') }}
</span>
</div>
<div
v-if="selectedRows.length === 1 && selectedCell.isEditable"
class="context-element"
@@ -49,6 +64,7 @@
</template>
<script setup lang="ts">
import { Prop } from 'vue';
import BaseContextMenu from '@/components/BaseContextMenu.vue';
import { useI18n } from 'vue-i18n';
@@ -57,10 +73,18 @@ const { t } = useI18n();
defineProps({
contextEvent: MouseEvent,
selectedRows: Array,
selectedCell: Object
selectedCell: Object,
mode: String as Prop<'table' | 'query'>
});
const emit = defineEmits(['show-delete-modal', 'close-context', 'set-null', 'copy-cell', 'copy-row']);
const emit = defineEmits([
'show-delete-modal',
'close-context',
'set-null',
'copy-cell',
'copy-row',
'duplicate-row'
]);
const showConfirmModal = () => {
emit('show-delete-modal');
@@ -80,8 +104,13 @@ const copyCell = () => {
closeContext();
};
const copyRow = () => {
emit('copy-row');
const copyRow = (format: string) => {
emit('copy-row', format);
closeContext();
};
const duplicateRow = () => {
emit('duplicate-row');
closeContext();
};
</script>

View File

@@ -19,7 +19,7 @@
class="cell-content"
:class="`${isNull(col)} ${typeClass(fields[cKey].type)}`"
@dblclick="editON(cKey)"
>{{ cutText(typeFormat(col, fields[cKey].type.toLowerCase(), fields[cKey].length) as string) }}</span>
>{{ cutText(typeFormat(col, fields[cKey].type.toLowerCase(), fields[cKey].length) as string, 250) }}</span>
<ForeignKeySelect
v-else-if="isForeignKey(cKey)"
v-model="editingContent"
@@ -194,9 +194,9 @@
import { computed, onBeforeUnmount, Prop, ref, Ref, watch, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import * as moment from 'moment';
import { ModelOperations } from '@vscode/vscode-languagedetection';
import { mimeFromHex } from 'common/libs/mimeFromHex';
import { formatBytes } from 'common/libs/formatBytes';
import { langDetector } from 'common/libs/langDetector';
import { bufferToBase64 } from 'common/libs/bufferToBase64';
import hexToBinary, { HexChar } from 'common/libs/hexToBinary';
import {
@@ -221,9 +221,11 @@ import TextEditor from '@/components/BaseTextEditor.vue';
import BaseMap from '@/components/BaseMap.vue';
import ForeignKeySelect from '@/components/ForeignKeySelect.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import { useFilters } from '@/composables/useFilters';
import { QueryForeign, TableField } from 'common/interfaces/antares';
const { t } = useI18n();
const { cutText } = useFilters();
const props = defineProps({
row: Object,
@@ -546,11 +548,6 @@ const onKey = (e: KeyboardEvent) => {
}
};
const cutText = (val: string) => {
if (typeof val !== 'string') return val;
return val.length > 128 ? `${val.substring(0, 128)}[...]` : val;
};
const typeFormat = (val: string | number | Date | number[], type: string, precision?: number | false) => {
if (!val) return val;
@@ -604,19 +601,8 @@ watch(() => props.fields, () => {
});
watch(isTextareaEditor, (val) => {
if (val) {
const modelOperations = new ModelOperations();
(async () => {
const detected = await modelOperations.runModel(editingContent.value);
const filteredLanguages = detected.filter(dLang =>
availableLanguages.value.some(aLang => aLang.id === dLang.languageId) &&
dLang.confidence > 0.1
);
if (filteredLanguages.length)
editorMode.value = availableLanguages.value.find(lang => lang.id === filteredLanguages[0].languageId).slug;
})();
}
if (val)
editorMode.value = langDetector(editingContent.value);
});
watch(() => props.selected, (isSelected) => {

View File

@@ -82,10 +82,10 @@
v-if="isTable"
class="btn btn-dark btn-sm"
:disabled="isQuering"
@click="showFakerModal"
@click="showFakerModal()"
>
<i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span>{{ $tc('message.insertRow', 2) }}</span>
<span>{{ t('message.insertRow', 2) }}</span>
</button>
<div class="dropdown table-dropdown pr-2">
@@ -105,6 +105,9 @@
<li class="menu-item">
<a class="c-hand" @click="downloadTable('csv')">CSV</a>
</li>
<li class="menu-item">
<a class="c-hand" @click="downloadTable('sql')">SQL INSERT</a>
</li>
</ul>
</div>
</div>
@@ -153,14 +156,18 @@
:element-type="elementType"
@update-field="updateField"
@delete-selected="deleteSelected"
@duplicate-row="showFakerModal"
@hard-sort="hardSort"
/>
</div>
<ModalFakerRows
v-if="isFakerModal"
:fields="fields"
:row-to-duplicate="rowToDuplicate"
:key-usage="keyUsage"
:tab-uid="tabUid"
:schema="schema"
:table="table"
@hide="hideFakerModal"
@reload="reloadTable"
/>
@@ -182,6 +189,9 @@ import WorkspaceTabTableFilters from '@/components/WorkspaceTabTableFilters.vue'
import ModalFakerRows from '@/components/ModalFakerRows.vue';
import { ConnectionParams } from 'common/interfaces/antares';
import { TableFilterClausole } from 'common/interfaces/tableApis';
import { useFilters } from '@/composables/useFilters';
const { localeString } = useFilters();
const { t } = useI18n();
@@ -224,6 +234,7 @@ const filters = ref([]);
const page = ref(1);
const pageProxy = ref(1);
const approximateCount = ref(0);
const rowToDuplicate = ref(null);
const workspace = computed(() => {
return getWorkspace(props.connection.uid);
@@ -234,7 +245,7 @@ const customizations = computed(() => {
});
const isTable = computed(() => {
return !!workspace.value.breadcrumbs.table;
return !workspace.value.breadcrumbs.view;
});
const fields = computed(() => {
@@ -329,13 +340,17 @@ const pageChange = (direction: 'prev' | 'next') => {
page.value--;
};
const showFakerModal = () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const showFakerModal = (row?: any) => {
console.log(row);
if (isQuering.value) return;
isFakerModal.value = true;
rowToDuplicate.value = row;
};
const hideFakerModal = () => {
isFakerModal.value = false;
rowToDuplicate.value = null;
};
const onKey = (e: KeyboardEvent) => {
@@ -349,7 +364,7 @@ const onKey = (e: KeyboardEvent) => {
pageChange('next');
if (e.key === 'ArrowLeft')
pageChange('prev');
if (e.key === 'f') // f
if (e.key === 'f')
isSearch.value = !isSearch.value;
}
}
@@ -367,7 +382,7 @@ const setRefreshInterval = () => {
}
};
const downloadTable = (format: 'csv' | 'json') => {
const downloadTable = (format: 'csv' | 'json' | 'sql') => {
queryTable.value.downloadTable(format, props.table);
};
@@ -386,11 +401,6 @@ const updateFilters = (clausoles: TableFilterClausole[]) => {
getTableData();
};
const localeString = (val: number | null) => {
if (val !== null)
return val.toLocaleString();
};
const hasApproximately = computed(() => {
return results.value.length &&
results.value[0].rows &&
@@ -412,6 +422,8 @@ watch(() => props.schema, () => {
watch(() => props.table, () => {
if (props.isSelected) {
page.value = 1;
filters.value = [];
isSearch.value = false;
approximateCount.value = 0;
sortParams.value = {} as { field: string; dir: 'asc' | 'desc'};
getTableData();

View File

@@ -51,7 +51,7 @@
class="btn btn-sm btn-primary mr-0 ml-2"
type="submit"
>
{{ $t('word.filter') }}
{{ t('word.filter') }}
</button>
<button
class="btn btn-sm btn-dark mr-0 ml-2"
@@ -71,6 +71,9 @@ import customizations from 'common/customizations';
import { NUMBER, FLOAT } from 'common/fieldTypes';
import BaseSelect from '@/components/BaseSelect.vue';
import { TableFilterClausole } from 'common/interfaces/tableApis';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
fields: Array as Prop<TableField[]>,
@@ -97,7 +100,9 @@ const removeRow = (i: number) => {
};
const doFilter = () => {
const clausoles = rows.value.filter(el => el.active).map(el => createClausole(el));
const clausoles = rows.value
.filter(el => el.active)
.map(el => createClausole(el));
emit('filter', clausoles);
};

View File

@@ -0,0 +1,38 @@
import * as moment from 'moment';
export function useFilters () {
const cutText = (string: string, length: number, escape?: boolean) => {
if (typeof string !== 'string') return string;
if (escape) string = string.replace(/\s{2,}/g, ' ');
return string.length > length ? `${string.substring(0, length)}...` : string;
};
const lastPart = (string: string, length: number) => {
if (!string) return '';
string = string.split(/[/\\]+/).pop();
if (string.length >= length)
string = `...${string.slice(-length)}`;
return string;
};
const formatDate = (date: Date) => moment(date).isValid() ? moment(date).format('HH:mm:ss - YYYY/MM/DD') : date;
const localeString = (number: number | null) => {
if (number !== null)
return number.toLocaleString();
};
const wrapNumber = (num: number) => {
if (!num) return '';
return `(${num})`;
};
return {
cutText,
formatDate,
wrapNumber,
lastPart,
localeString
};
}

View File

@@ -56,7 +56,8 @@ const useFocusTrap = (args?: {disableAutofocus?: boolean}) => {
}
function initFocusTrap () {
if (!trapRef.value || isInitiated.value) return;
if (!trapRef.value || (isInitiated.value)) return;
focusableElements = (trapRef.value as HTMLElement).querySelectorAll(
focusableElementsSelector
);

View File

@@ -1,4 +1,4 @@
module.exports = {
export const arSA = {
word: {
edit: 'تعديل',
save: 'حفظ',

View File

@@ -1,4 +1,4 @@
module.exports = {
export const deDE = {
word: {
edit: 'Bearbeiten',
save: 'Speichern',

View File

@@ -1,4 +1,4 @@
module.exports = {
export const enUS = {
word: {
edit: 'Edit',
save: 'Save',
@@ -139,7 +139,10 @@ module.exports = {
commit: 'Commit',
rollback: 'Rollback',
connectionString: 'Connection string',
contributors: 'Contributors'
contributors: 'Contributors',
pin: 'Pin',
unpin: 'Unpin',
console: 'Console'
},
message: {
appWelcome: 'Welcome to Antares SQL Client!',
@@ -291,7 +294,11 @@ module.exports = {
untrustedConnection: 'Untrusted connection',
missingOrIncompleteTranslation: 'Missing or incomplete translation?',
findOutHowToContribute: 'Find out how to contribute',
disableFKChecks: 'Disable foreigh key checks'
disableFKChecks: 'Disable foreigh key checks',
allConnections: 'All connections',
searchForConnections: 'Search for connections',
disableScratchpad: 'Disable scratchpad',
reportABug: 'Report a bug'
},
faker: {
address: 'Address',

View File

@@ -1,4 +1,4 @@
module.exports = {
export const esES = {
word: {
edit: 'Editar',
save: 'Guardar',

View File

@@ -1,4 +1,4 @@
module.exports = {
export const frFR = {
word: {
edit: 'Editer',
save: 'Sauver',

View File

@@ -1,19 +1,41 @@
import { createI18n } from 'vue-i18n';
import { enUS } from './en-US';
import { itIT } from './it-IT';
import { arSA } from './ar-SA';
import { esES } from './es-ES';
import { frFR } from './fr-FR';
import { ptBR } from './pt-BR';
import { deDE } from './de-DE';
import { viVN } from './vi-VN';
import { jaJP } from './ja-JP';
import { zhCN } from './zh-CN';
import { ruRU } from './ru-RU';
const i18n = createI18n({
const messages = {
'en-US': enUS,
'it-IT': itIT,
'ar-SA': arSA,
'es-ES': esES,
'fr-FR': frFR,
'pt-BR': ptBR,
'de-DE': deDE,
'vi-VN': viVN,
'ja-JP': jaJP,
'zh-CN': zhCN,
'ru-RU': ruRU
};
type NestedPartial<T> = {
[K in keyof T]?: T[K] extends Array<infer R> ? Array<NestedPartial<R>> : (T[K] extends unknown ? unknown : NestedPartial<T[K]>)
};
export type MessageSchema = typeof enUS
export type AvailableLocale = keyof typeof messages
const i18n = createI18n<[NestedPartial<MessageSchema>], AvailableLocale>({
fallbackLocale: 'en-US',
messages: {
'en-US': require('./en-US'),
'it-IT': require('./it-IT'),
'ar-SA': require('./ar-SA'),
'es-ES': require('./es-ES'),
'fr-FR': require('./fr-FR'),
'pt-BR': require('./pt-BR'),
'de-DE': require('./de-DE'),
'vi-VN': require('./vi-VN'),
'ja-JP': require('./ja-JP'),
'zh-CN': require('./zh-CN'),
'ru-RU': require('./ru-RU')
}
allowComposition: true,
messages
});
export default i18n;
export { i18n };

View File

@@ -1,4 +1,4 @@
module.exports = {
export const itIT = {
word: {
edit: 'Modifica',
save: 'Salva',

View File

@@ -1,4 +1,4 @@
module.exports = {
export const jaJP = {
word: {
edit: '編集',
save: '保存',

View File

@@ -1,4 +1,4 @@
module.exports = {
export const ptBR = {
word: {
edit: 'Editar',
save: 'Salvar',

View File

@@ -1,4 +1,4 @@
module.exports = {
export const ruRU = {
word: {
edit: 'Редактировать',
save: 'Сохранить',

View File

@@ -1,4 +1,4 @@
module.exports = {
export const viVN = {
word: {
edit: 'Chỉnh sửa',
save: 'Lưu',

View File

@@ -1,4 +1,4 @@
module.exports = {
export const zhCN = {
word: {
edit: '编辑',
save: '保存',
@@ -51,12 +51,12 @@ module.exports = {
unsigned: '无符号',
default: '默认',
comment: '注释',
key: '',
key: 'Key | Keys',
order: 'Order',
expression: '表达式',
autoIncrement: '自动增量',
engine: 'Engine',
field: '字段',
field: 'Field | 字段',
approximately: '大约',
total: '总计',
table: '表',
@@ -71,15 +71,16 @@ module.exports = {
view: '视图',
definer: '定义者',
algorithm: 'Algorithm',
trigger: '触发器',
storedRoutine: '存储例程',
scheduler: '调度器',
trigger: 'Trigger | 触发器',
storedRoutine: 'Stored routine | 存储例程',
scheduler: 'Scheduler | 调度器',
event: '事件',
parameters: '参数',
function: '函数',
function: 'Function | 函数',
deterministic: 'Deterministic',
context: '上下文',
export: '导出',
import: '导入',
returns: '返回',
timing: '定时器',
state: '状态',
@@ -111,9 +112,9 @@ module.exports = {
small: '小',
medium: '中',
large: '大',
row: '行',
cell: '单元格',
triggerFunction: '触发函数',
row: 'Row | 行',
cell: 'Cell | 单元格',
triggerFunction: 'Trigger function | 触发函数',
all: '全部',
duplicate: '重复',
routine: '例程',
@@ -122,9 +123,25 @@ module.exports = {
select: '选择',
passphrase: '密码',
filter: '过滤器',
change: '变更',
views: '视图',
triggers: '触发器',
routines: '例程',
functions: '函数',
schedulers: '调度器',
includes: '包括',
drop: '按下([使]掉下)',
completed: '完成',
aborted: '中止',
disabled: '禁用',
enable: '启用',
disable: '是否禁用'
disable: '是否禁用',
commit: '提交',
rollback: '回滚',
connectionString: '连接字符串',
contributors: '贡献者',
pin: 'Pin',
unpin: 'Unpin'
},
message: {
appWelcome: '欢迎来到Antares SQL Client!',
@@ -250,9 +267,36 @@ module.exports = {
searchForQueries: '搜索查询',
killProcess: '杀死进程',
closeTab: '关闭标签',
exportSchema: '导出 schema',
importSchema: '导入 schema',
directoryPath: '目录路径',
newInserStmtEvery: '新的 INSERT 语句每个',
processingTableExport: '处理 {table}',
fechingTableExport: '正在从 {table} 获取数据中',
writingTableExport: '正在将 {table} 数据写入中',
checkAllTables: '检查所有表格',
uncheckAllTables: '取消选中所有表格',
goToDownloadPage: '跳转到下载页面',
readOnlyMode: '只读模式',
killQuery: '停止查询'
killQuery: '停止查询',
insertRow: '插入行 | 插入多行',
commitMode: '提交模式',
autoCommit: '自动提交',
manualCommit: '手动提交',
actionSuccessful: '{action} 成功',
importQueryErrors: '警告: {n} 错误已发生 | 警告: 发生 {n} 个错误',
executedQueries: '{n} 个查询已执行 | {n} 个查询已执行',
ourputFormat: '输出格式',
singleFile: '单个 {ext} 文件',
zipCompressedFile: 'ZIP 压缩 {ext} 文件',
disableBlur: '禁用模糊',
untrustedConnection: '不受信任的连接',
missingOrIncompleteTranslation: '翻译缺失或不完整?',
findOutHowToContribute: '找出如何贡献',
disableFKChecks: '禁用外键检查',
allConnections: '所有连接',
searchForConnections: '搜索连接',
disableScratchpad: '禁用暂存器'
},
faker: {
address: '地址',
@@ -262,8 +306,8 @@ module.exports = {
date: '日期',
finance: '财务',
git: 'Git',
hacker: '黑客',
internet: '互联网',
hacker: 'Hacker',
internet: 'Internet',
lorem: 'Lorem',
name: '姓名',
music: '音乐',

View File

@@ -2,17 +2,18 @@
import { ipcRenderer } from 'electron';
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import { VueMaskDirective } from 'v-mask';
import '@mdi/font/css/materialdesignicons.css';
import 'leaflet/dist/leaflet.css';
import '@/scss/main.scss';
import { VueMaskDirective } from 'v-mask';
import { useApplicationStore } from '@/stores/application';
import { useSettingsStore } from '@/stores/settings';
import { useNotificationsStore } from '@/stores/notifications';
import { useConsoleStore } from '@/stores/console';
import App from '@/App.vue';
import i18n from '@/i18n';
import { i18n } from '@/i18n';
// https://github.com/probil/v-mask/issues/498#issuecomment-827027834
const vMaskV2 = VueMaskDirective;
@@ -36,6 +37,11 @@ ipcRenderer.on('unhandled-exception', (event, error) => {
useNotificationsStore().addNotification({ status: 'error', message: error.message });
});
// IPC query logs
ipcRenderer.on('query-log', (event, logRecord) => {
useConsoleStore().putLog(logRecord);
});
// IPC app updates
ipcRenderer.on('checking-for-update', () => {
useApplicationStore().updateStatus = 'checking';

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