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

Compare commits

...

90 Commits

Author SHA1 Message Date
allcontributors[bot]
d163cbfac8 docs: update .all-contributorsrc [skip ci] 2024-04-08 10:49:32 +00:00
allcontributors[bot]
4537d96f3e docs: update README.md [skip ci] 2024-04-08 10:49:31 +00:00
a752dcb6a9 chore(release): 0.7.23 2024-04-07 16:54:05 +02:00
1875e895ae chore(release): 0.7.23-beta.1 2024-04-02 09:10:17 +02:00
2064294119 feat: add the page reference in the export file name, closes #772 2024-03-25 09:08:30 +01:00
62e3115860 feat: move connections out of folder from context menu, related to #773 2024-03-24 11:10:00 +01:00
9aef287a98 feat: move connections to folders from context menu, related to #773 2024-03-23 18:45:38 +01:00
65ec4c5da6 fix: bad format of timestamp fields on CSV export, fixes 776 2024-03-23 16:33:19 +01:00
e19118982b chore(release): 0.7.23-beta.0 2024-03-21 23:09:09 +01:00
11f130d91c Merge pull request #778 from dyaskur/fix_shortcut_on_macos
fix: shortcut not working on mac os
2024-03-14 09:05:07 +01:00
Yaskur
0bb5cedda6 fix: shortcut not working on mac os 2024-03-13 15:48:59 +07:00
de9dac3e8a fix: query result sort not working with aliased tables, fixes #765 2024-03-10 16:04:24 +01:00
dd5b41716a fix: CSV export does not escape strings when needed, fixes #770 2024-03-09 15:42:36 +01:00
86acb390ac build: add husky and commitlint 2024-03-09 15:05:55 +01:00
2884ec3dd6 chore(release): 0.7.22 2024-02-26 18:20:31 +01:00
b542a09c01 Merge branch 'beta' of https://github.com/antares-sql/antares 2024-02-26 18:19:35 +01:00
6d94a04b67 Merge branch 'develop' of https://github.com/antares-sql/antares into beta 2024-02-26 18:19:14 +01:00
8500fc40a1 refactor: improved note tab selection 2024-02-26 18:17:15 +01:00
586f901bae fix: delete record modal pressing del when editing a field, fixes #767 2024-02-23 18:08:02 +01:00
04e4d21e20 chore(release): 0.7.22-beta.2 2024-02-18 14:49:58 +01:00
fd3dd03eb2 chore: moved electron in devDependencies 2024-02-18 14:47:56 +01:00
d3f71e65ce feat(MySQL): option to enable single connection mode 2024-02-18 14:37:45 +01:00
90b9b87b1d chore(deps): update various dependencies 2024-02-18 13:44:22 +01:00
e9b42c3edb chore(release): 0.7.22-beta.1 2024-02-12 18:31:01 +01:00
259d051a21 fix: some issues related to previous commit 2024-02-12 18:30:07 +01:00
876d5ea481 perf(MySQL): improvements in connection handling 2024-02-11 16:38:06 +01:00
6d002efaf5 Update FUNDING.yml 2024-02-09 12:07:16 +01:00
58be1abf5f Update README.md 2024-02-09 09:27:16 +01:00
da56905572 refactor: improved SET field edit 2024-02-07 17:37:19 +01:00
d698f2798a fix: unable to edit tables containing SET fields, fixes #755 2024-02-06 18:16:26 +01:00
9a41511c42 Merge pull request #758 from 64knl/feat/translation-spelling
feat: update dutch translation + fix spelling mistake
2024-02-06 16:45:29 +01:00
Rene
30ada13663 feat: update dutch translation + fix spelling mistake 2024-02-06 15:38:42 +01:00
14eeaccb07 chore(release): 0.7.22-beta.0 2024-02-04 14:40:24 +01:00
1a0c5da2f1 feat(UI): resizable textarea in new/edito note, closes #747 2024-02-04 14:38:15 +01:00
bb36e98beb perf(UI): improved notes, fixes #746 2024-01-20 10:11:49 +01:00
8928510fb5 refactor: use Record to type objects 2024-01-19 18:03:20 +01:00
eb5d3f14f1 chore(release): 0.7.21 2024-01-13 16:31:14 +01:00
33c127b090 Merge branch 'master' of https://github.com/antares-sql/antares 2024-01-13 16:30:23 +01:00
4e98dc21d8 Merge branch 'beta' of https://github.com/antares-sql/antares 2024-01-13 16:30:21 +01:00
20b27343cd feat(SQLite): enable schema reloat button on sidebar 2024-01-13 16:28:55 +01:00
3b9228a723 fix(SQLite): unable to change integer fields length to 0, fixes #732 2024-01-13 16:28:31 +01:00
ab0f91b448 chore: remove Twitter links 2024-01-11 14:09:34 +01:00
0b6307c738 chore(release): 0.7.21-beta.1 2024-01-06 19:04:36 +01:00
dbf38fd99c ci: update create-generated-sources.yml 2024-01-06 18:53:47 +01:00
169fcb13da build(deps): downgrade better-sqlite3 2024-01-06 18:17:58 +01:00
97ece32988 ci: action to generate generated-sources.json 2024-01-05 11:14:58 +01:00
c946c3fcda ci: update node version 2024-01-05 11:14:05 +01:00
cdd2a11f8e fix(PostgreSQL): unhandled error on connection lost, fixes #740 2023-12-29 14:42:12 +01:00
23946ff2ce fix(PostgreSQL): exception deleting a table with one or less tabs open 2023-12-28 10:44:11 +01:00
0f8d2cb4ef fix(PostgreSQL): error adding MONEY fields to a table 2023-12-28 10:13:28 +01:00
219f89aa60 chore(release): 0.7.21-beta.0 2023-12-25 11:46:27 +01:00
eec29e99cc Merge branch 'master' of https://github.com/antares-sql/antares into beta 2023-12-25 11:46:13 +01:00
171caed8b5 chore: minor docs changes 2023-12-25 11:40:52 +01:00
88ec71c943 Merge pull request #735 from antares-sql/feat/new-scratchpad
Feat/new scratchpad
2023-12-25 11:19:42 +01:00
532002ca01 refactor: migrate old scratchpad into notes 2023-12-25 11:19:23 +01:00
9a732ea197 feat: open saved queries in a tab 2023-12-25 10:54:41 +01:00
b734b24679 fix: JavaScript error at first startup, fixes #736 2023-12-25 09:35:43 +01:00
a52fc3fd92 feat: buttons to save and access to saved queryes from query tab 2023-12-22 18:48:16 +01:00
bfa3924d57 feat: highlithg sql in notes, history and console 2023-12-22 18:06:27 +01:00
08e5a13f72 feat: ability to edit notes 2023-12-21 18:10:51 +01:00
eaaf1b756a feat: new notes system 2023-12-21 10:16:46 +01:00
84d221aaa7 chore: utility commit 2023-12-13 18:29:45 +01:00
ba6063e636 chore(release): 0.7.20 2023-12-08 13:08:29 +01:00
b055350726 fix: missing update indicator on setting icon 2023-12-08 13:02:15 +01:00
dbd533b229 Merge branch 'develop' of https://github.com/antares-sql/antares into feat/new-scratchpad 2023-12-06 08:53:35 +01:00
b5b35be45c chore(release): 0.7.20-beta.2 2023-12-06 08:52:48 +01:00
6a72f6b4ae fix: communication with worker thread not working 2023-12-06 08:51:48 +01:00
756786d72e chore: utility commit 2023-12-06 08:44:07 +01:00
861b704344 chore(release): 0.7.20-beta.1 2023-12-02 14:22:32 +01:00
9ce53165e8 chore: post merge cleanup 2023-12-02 14:21:34 +01:00
62614dceb9 Merge pull request #727 from antares-sql/flatpak-experiments
Flatpak experiments
2023-12-02 14:12:51 +01:00
8774dd44e6 Merge branch 'develop' into flatpak-experiments 2023-12-02 14:12:18 +01:00
f0ae01ca5e Merge branch 'develop' of https://github.com/antares-sql/antares into develop 2023-12-02 12:11:35 +01:00
03be777c2a refactor: worker threads to import sql dump instead of process 2023-12-02 11:35:20 +01:00
45a695ac0a refactor: improvements in worker implementation 2023-12-02 11:21:48 +01:00
c176841b75 refactor: worker threads to export sql dump instead of process 2023-12-02 09:31:54 +01:00
329246e2d8 refactor: minor refactor 2023-12-01 20:05:30 +01:00
e26809f260 fix(Flatpak): import/export schema not working 2023-12-01 14:18:40 +01:00
f13d4e6dce feat: copy element names on sidebar from context menu, closes #718 2023-11-29 18:15:22 +01:00
879de91516 refactor: minor refactor 2023-11-27 18:35:56 +01:00
38b32bfb28 build: process.exit on devtoolsInstaller 2023-11-27 13:37:24 +01:00
315d9d84c2 feat: logging errors on log file 2023-11-27 13:36:56 +01:00
c3d96cb35b refactor(Flatpak): temporarily disable import/export feature 2023-11-26 17:49:12 +01:00
05bd7672e1 chore: update package-lock.json 2023-11-26 17:28:45 +01:00
390bf88bb8 refactor: inport/export change for flatpak 2023-11-26 16:42:30 +01:00
984aa893d3 refactor: temporary disable windows process validation 2023-11-24 10:18:44 +01:00
1ac816eaa9 Merge branch 'master' of https://github.com/antares-sql/antares into develop 2023-11-16 18:30:05 +01:00
6f25fcbc05 ci: update workflow files 2023-11-16 18:16:41 +01:00
bc44465132 Merge branch 'master' of https://github.com/antares-sql/antares into develop 2023-11-16 18:09:51 +01:00
634a442213 ci: update workflow files 2023-11-16 18:01:40 +01:00
125 changed files with 7792 additions and 4282 deletions

View File

@@ -266,6 +266,15 @@
"contributions": [ "contributions": [
"translation" "translation"
] ]
},
{
"login": "bagusindrayana",
"name": "Bagus Indrayana",
"avatar_url": "https://avatars.githubusercontent.com/u/36830534?v=4",
"profile": "https://github.com/bagusindrayana",
"contributions": [
"code"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

4
.github/FUNDING.yml vendored
View File

@@ -1,6 +1,6 @@
# These are supported funding model platforms # These are supported funding model platforms
github: [fabio286] github: [antares-sql,fabio286]
patreon: #fabio286 patreon: #fabio286
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username
@@ -9,4 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
liberapay: # Replace with a single Liberapay username liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username otechie: # Replace with a single Otechie username
custom: ['https://paypal.me/fabiodistasio'] custom: ['https://paypal.me/fabiodistasio']

View File

@@ -15,7 +15,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [macos-11, ubuntu-latest, windows-latest] os: [macos-latest, ubuntu-latest, windows-latest]
steps: steps:
- name: Check out Git repository - name: Check out Git repository
@@ -26,7 +26,7 @@ jobs:
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 20
- name: Install dependencies - name: Install dependencies
run: npm i run: npm i

View File

@@ -15,7 +15,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [macos-11, ubuntu-latest, windows-latest] os: [macos-latest, ubuntu-latest, windows-latest]
steps: steps:
- name: Exit if not on master branch - name: Exit if not on master branch
@@ -32,7 +32,7 @@ jobs:
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 20
- name: Install dependencies - name: Install dependencies
run: npm i run: npm i

View File

@@ -15,7 +15,7 @@ jobs:
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 20
- name: Install dependencies - name: Install dependencies
run: npm i run: npm i

View File

@@ -5,7 +5,7 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@v3 uses: actions/checkout@v3
@@ -13,7 +13,7 @@ jobs:
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 20
- name: Install dependencies - name: Install dependencies
run: npm i run: npm i

View File

@@ -13,7 +13,7 @@ jobs:
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 20
- name: npm install & build - name: npm install & build
run: | run: |
@@ -29,3 +29,30 @@ jobs:
build build
!build/*-unpacked !build/*-unpacked
!build/.icon-ico !build/.icon-ico
build-beta:
runs-on: macos-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v3
with:
ref: beta
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 20
- name: npm install & build
run: |
npm install
npm run build
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: macos-build-beta
retention-days: 3
path: |
build
!build/*-unpacked
!build/.icon-ico

View File

@@ -0,0 +1,50 @@
name: Create generated-rources.json
on:
workflow_dispatch: {}
jobs:
build:
runs-on: ubuntu-latest
steps:
# Install flatpak-node-generator
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: '3.8'
- name: Install pipx
uses: CfirTsabari/actions-pipx@v1
- name: Install flatpak-node-generator
run: |
cd ../
git clone https://github.com/flatpak/flatpak-builder-tools.git
cd flatpak-builder-tools/node
pipx install .
# Install Antares
- name: Check out Git repository
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 18
# - name: Delete old package-lock.json
# run: rm package-lock.json
- name: Install dependencies
run: npm i --lockfile-version 2
- name: Generate generated-sources.json
run: flatpak-node-generator npm -r package-lock.json --electron-node-headers
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: generated-sources
retention-days: 3
path: |
generated-sources.json

View File

@@ -1,9 +1,9 @@
name: Test end-to-end [WINDOWS] name: Test end-to-end
on: on:
push: push:
branches: branches:
- master - develop
jobs: jobs:
release: release:
@@ -20,7 +20,7 @@ jobs:
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 20
- name: Install dependencies - name: Install dependencies
run: npm i run: npm i

1
.husky/commit-msg Normal file
View File

@@ -0,0 +1 @@
npx --no -- commitlint --edit $1

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
npm run lint

View File

@@ -5,14 +5,14 @@
], ],
"fix": true, "fix": true,
"formatter": "verbose", "formatter": "verbose",
"customSyntax": "postcss-html",
"plugins": [ "plugins": [
"stylelint-scss" "stylelint-scss"
], ],
"rules": { "rules": {
"at-rule-no-unknown": null, "at-rule-no-unknown": null,
"no-descending-specificity": null, "no-descending-specificity": null,
"font-family-no-missing-generic-family-keyword": null, "font-family-no-missing-generic-family-keyword": null
"declaration-colon-newline-after": "always-multi-line"
}, },
"syntax": "scss" "syntax": "scss"
} }

9
.vscode/launch.json vendored
View File

@@ -17,15 +17,6 @@
"sourceMaps": true, "sourceMaps": true,
"type": "chrome", "type": "chrome",
"webRoot": "${workspaceFolder}" "webRoot": "${workspaceFolder}"
},
{
"name": "Electron: Worker",
"cwd": "${workspaceFolder}",
"port": 9224,
"request": "attach",
"sourceMaps": true,
"type": "node",
"timeout": 1000000
} }
], ],
"compounds": [ "compounds": [

View File

@@ -10,7 +10,8 @@
"translation", "translation",
"Linux", "Linux",
"MacOS", "MacOS",
"deps" "deps",
"Flatpak"
], ],
"svg.preview.background": "transparent" "svg.preview.background": "transparent"
} }

View File

@@ -2,6 +2,139 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.7.23](https://github.com/antares-sql/antares/compare/v0.7.23-beta.1...v0.7.23) (2024-04-07)
### [0.7.23-beta.1](https://github.com/antares-sql/antares/compare/v0.7.23-beta.0...v0.7.23-beta.1) (2024-04-02)
### Features
* add the page reference in the export file name, closes [#772](https://github.com/antares-sql/antares/issues/772) ([2064294](https://github.com/antares-sql/antares/commit/2064294119ed9dfab2a9968dfb5b35d52e2ae03b))
* move connections out of folder from context menu, related to [#773](https://github.com/antares-sql/antares/issues/773) ([62e3115](https://github.com/antares-sql/antares/commit/62e311586073ae7ee4896305198c7168f637c1af))
* move connections to folders from context menu, related to [#773](https://github.com/antares-sql/antares/issues/773) ([9aef287](https://github.com/antares-sql/antares/commit/9aef287a983754158cdbdc9b2a72db9ab82f76c8))
### Bug Fixes
* bad format of timestamp fields on CSV export, fixes 776 ([65ec4c5](https://github.com/antares-sql/antares/commit/65ec4c5da6187a7ab2dfff59326cd12bfa788c3b))
### [0.7.23-beta.0](https://github.com/antares-sql/antares/compare/v0.7.22...v0.7.23-beta.0) (2024-03-21)
### Bug Fixes
* CSV export does not escape strings when needed, fixes [#770](https://github.com/antares-sql/antares/issues/770) ([dd5b417](https://github.com/antares-sql/antares/commit/dd5b41716a10cf9500f2c611b232f5b5b0756a68))
* query result sort not working with aliased tables, fixes [#765](https://github.com/antares-sql/antares/issues/765) ([de9dac3](https://github.com/antares-sql/antares/commit/de9dac3e8abf3b3261f8c54c88cf2386a5be2207))
* shortcut not working on mac os ([0bb5ced](https://github.com/antares-sql/antares/commit/0bb5cedda6a67ccbeea8c127b799f533395101a2))
### [0.7.22](https://github.com/antares-sql/antares/compare/v0.7.22-beta.2...v0.7.22) (2024-02-26)
### Bug Fixes
* delete record modal pressing del when editing a field, fixes [#767](https://github.com/antares-sql/antares/issues/767) ([586f901](https://github.com/antares-sql/antares/commit/586f901bae9a80c0e53ac1d804cbae3f05e26d8e))
### [0.7.22-beta.2](https://github.com/antares-sql/antares/compare/v0.7.22-beta.1...v0.7.22-beta.2) (2024-02-18)
### Features
* **MySQL:** option to enable single connection mode ([d3f71e6](https://github.com/antares-sql/antares/commit/d3f71e65cef88838f03f95a4b34e197fb61878f8))
### [0.7.22-beta.1](https://github.com/antares-sql/antares/compare/v0.7.22-beta.0...v0.7.22-beta.1) (2024-02-12)
### Features
* update dutch translation + fix spelling mistake ([30ada13](https://github.com/antares-sql/antares/commit/30ada13663e88f89beb3dd7291010837059585d5))
### Bug Fixes
* some issues related to previous commit ([259d051](https://github.com/antares-sql/antares/commit/259d051a21e334496d3a52b662f1855ba9a9046d))
* unable to edit tables containing SET fields, fixes [#755](https://github.com/antares-sql/antares/issues/755) ([d698f27](https://github.com/antares-sql/antares/commit/d698f2798a2423f86e6d786dd3ab80439b372a08))
### Improvements
* **MySQL:** improvements in connection handling ([876d5ea](https://github.com/antares-sql/antares/commit/876d5ea48185334e9e2fc981c4282a9c42d22b10))
### [0.7.22-beta.0](https://github.com/antares-sql/antares/compare/v0.7.21...v0.7.22-beta.0) (2024-02-04)
### Features
* **UI:** resizable textarea in new/edito note, closes [#747](https://github.com/antares-sql/antares/issues/747) ([1a0c5da](https://github.com/antares-sql/antares/commit/1a0c5da2f14b99d6f5581b2bf6e916d67d097245))
### Improvements
* **UI:** improved notes, fixes [#746](https://github.com/antares-sql/antares/issues/746) ([bb36e98](https://github.com/antares-sql/antares/commit/bb36e98bebc5e1e55735e98d272428df2ab682e8))
### [0.7.21](https://github.com/antares-sql/antares/compare/v0.7.21-beta.1...v0.7.21) (2024-01-13)
### Features
* **SQLite:** enable schema reloat button on sidebar ([20b2734](https://github.com/antares-sql/antares/commit/20b27343cd95998bd83403b7556ea35fcad9fa1b))
### Bug Fixes
* **SQLite:** unable to change integer fields length to 0, fixes [#732](https://github.com/antares-sql/antares/issues/732) ([3b9228a](https://github.com/antares-sql/antares/commit/3b9228a7230dcd9f47f5794a83b60d28207bdce1))
### [0.7.21-beta.1](https://github.com/antares-sql/antares/compare/v0.7.21-beta.0...v0.7.21-beta.1) (2024-01-06)
### Bug Fixes
* **PostgreSQL:** error adding MONEY fields to a table ([0f8d2cb](https://github.com/antares-sql/antares/commit/0f8d2cb4ef5c327f96f788179be0b309689b4ce8))
* **PostgreSQL:** exception deleting a table with one or less tabs open ([23946ff](https://github.com/antares-sql/antares/commit/23946ff2cef6d63e1529e2c8c4357d7fdedc3284))
* **PostgreSQL:** unhandled error on connection lost, fixes [#740](https://github.com/antares-sql/antares/issues/740) ([cdd2a11](https://github.com/antares-sql/antares/commit/cdd2a11f8e33d6607337989723774d60c7c1a030))
### [0.7.21-beta.0](https://github.com/antares-sql/antares/compare/v0.7.20...v0.7.21-beta.0) (2023-12-25)
### Features
* ability to edit notes ([08e5a13](https://github.com/antares-sql/antares/commit/08e5a13f723bc3ae95b0f529b79f0b558bc2a377))
* buttons to save and access to saved queryes from query tab ([a52fc3f](https://github.com/antares-sql/antares/commit/a52fc3fd923fec30cfdd3d804554e6fe4534c400))
* highlithg sql in notes, history and console ([bfa3924](https://github.com/antares-sql/antares/commit/bfa3924d57c2ea2cc2857006d6bd6279865dbc99))
* new notes system ([eaaf1b7](https://github.com/antares-sql/antares/commit/eaaf1b756a6b5ffb77f7f07f3e4c0971822d48c3))
* open saved queries in a tab ([9a732ea](https://github.com/antares-sql/antares/commit/9a732ea1971d223f3278ad02d3dd77837fecb377))
### Bug Fixes
* JavaScript error at first startup, fixes [#736](https://github.com/antares-sql/antares/issues/736) ([b734b24](https://github.com/antares-sql/antares/commit/b734b246795fb240f6728714be68c22cc221bbe9))
### [0.7.20](https://github.com/antares-sql/antares/compare/v0.7.20-beta.2...v0.7.20) (2023-12-08)
### Bug Fixes
* missing update indicator on setting icon ([b055350](https://github.com/antares-sql/antares/commit/b055350726774e05a4e04ea6d890c46f64f2112e))
### [0.7.20-beta.2](https://github.com/antares-sql/antares/compare/v0.7.20-beta.1...v0.7.20-beta.2) (2023-12-06)
### Bug Fixes
* communication with worker thread not working ([6a72f6b](https://github.com/antares-sql/antares/commit/6a72f6b4ae3f4c1d6c42ca7a817d2f2c135696a7))
### [0.7.20-beta.1](https://github.com/antares-sql/antares/compare/v0.7.20-beta.0...v0.7.20-beta.1) (2023-12-02)
### Features
* copy element names on sidebar from context menu, closes [#718](https://github.com/antares-sql/antares/issues/718) ([f13d4e6](https://github.com/antares-sql/antares/commit/f13d4e6dceb70b0c7cb8d56ddfb5f00e938571cc))
* logging errors on log file ([315d9d8](https://github.com/antares-sql/antares/commit/315d9d84c2caa29852d68bd189750b2a4028d953))
### Bug Fixes
* **Flatpak:** import/export schema not working ([e26809f](https://github.com/antares-sql/antares/commit/e26809f260099ba194bf5d00671cae14d438197b))
### [0.7.20-beta.0](https://github.com/antares-sql/antares/compare/v0.7.19...v0.7.20-beta.0) (2023-11-15) ### [0.7.20-beta.0](https://github.com/antares-sql/antares/compare/v0.7.19...v0.7.20-beta.0) (2023-11-15)

View File

@@ -7,7 +7,7 @@
# Antares SQL Client # Antares SQL Client
![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) ![GitHub](https://img.shields.io/github/license/fabio286/antares) [![Build Status](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Ffabio286%2Fantares%2Fbadge&style=flat)](https://actions-badge.atrox.dev/fabio286/antares/goto) ![Mastodon Follow](https://img.shields.io/mastodon/follow/%20110860460902482117?domain=https%3A%2F%2Ffosstodon.org&style=social) [![Twitter Follow](https://img.shields.io/twitter/follow/AntaresSQL?style=social)](https://twitter.com/AntaresSQL) [![Plant a Tree](https://raw.githubusercontent.com/Fabio286/treedom-badge/master/svg/plant-a-tree.svg)](https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet) ![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) ![GitHub](https://img.shields.io/github/license/fabio286/antares) ![Test e2e](https://github.com/antares-sql/antares/actions/workflows/test-e2e-win.yml/badge.svg?branch=develop) ![Mastodon Follow](https://img.shields.io/mastodon/follow/%20110860460902482117?domain=https%3A%2F%2Ffosstodon.org&style=social) [![Plant a Tree](https://raw.githubusercontent.com/Fabio286/treedom-badge/master/svg/plant-a-tree.svg)](https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet)
Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers. Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers.
Our target is to support as many databases as possible, and all major operating systems, including the ARM versions. Our target is to support as many databases as possible, and all major operating systems, including the ARM versions.
@@ -17,7 +17,7 @@ However, there are all the features necessary to have a pleasant database manage
We are actively working on it, hoping to provide new cool features, improvements and fixes as soon as possible. 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). 🔗 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 on [Mastodon](https://fosstodon.org/@AntaresSQL) or [Twitter](https://twitter.com/AntaresSQL). 👁 To stay tuned for new releases follow Antares SQL on [Mastodon](https://fosstodon.org/@AntaresSQL).
🌟 Don't forget to **leave a star** if you appreciate this project. 🌟 Don't forget to **leave a star** if you appreciate this project.
🗳️ Polls: 🗳️ Polls:
@@ -35,6 +35,7 @@ We are actively working on it, hoping to provide new cool features, improvements
- Fake table data filler to generate tons of data for test purpose. - Fake table data filler to generate tons of data for test purpose.
- Query suggestions and auto complete. - Query suggestions and auto complete.
- Query history: search through the last 1000 queries. - Query history: search through the last 1000 queries.
- Save queries, notes or todo.
- SSH tunnel support. - SSH tunnel support.
- Manual commit mode. - Manual commit mode.
- Import and export database dumps. - Import and export database dumps.
@@ -70,17 +71,6 @@ On macOS you can run `.dmg` distribution following [this guide](https://support.
[<img height='56' alt='Download on Flathub' src='https://dl.flathub.org/assets/badges/flathub-badge-en.svg'/>](https://flathub.org/apps/it.fabiodistasio.AntaresSQL) [![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/antares-sql/antares/master/docs/aur-badge.svg)](https://aur.archlinux.org/packages/antares-sql-bin) [<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) [<img height='56' alt='Download on Flathub' src='https://dl.flathub.org/assets/badges/flathub-badge-en.svg'/>](https://flathub.org/apps/it.fabiodistasio.AntaresSQL) [![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/antares-sql/antares/master/docs/aur-badge.svg)](https://aur.archlinux.org/packages/antares-sql-bin) [<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/antares-sql/antares/releases/latest)** 🚀 **[Other Downloads](https://github.com/antares-sql/antares/releases/latest)**
## Coming soon
This is a roadmap with major features will come in near future.
- Database tools.
- Users management (add/edit/delete).
- More context menu shortcuts.
- More keyboard shortcuts.
- Support for other databases.
- Apple Silicon distribution
## Currently supported ## Currently supported
### Databases ### Databases
@@ -89,6 +79,7 @@ This is a roadmap with major features will come in near future.
- [x] PostgreSQL - [x] PostgreSQL
- [x] SQLite - [x] SQLite
- [x] Firebird SQL - [x] Firebird SQL
- [ ] DuckDB
- [ ] SQL Server - [ ] SQL Server
- [ ] More... - [ ] More...
@@ -110,7 +101,7 @@ This is a roadmap with major features will come in near future.
- 🌍 [Translate Antares](https://github.com/Fabio286/antares/wiki/Translate-Antares) - 🌍 [Translate Antares](https://github.com/Fabio286/antares/wiki/Translate-Antares)
- 📖 [Contributors Guide](https://github.com/Fabio286/antares/wiki/Contributors-Guide) - 📖 [Contributors Guide](https://github.com/Fabio286/antares/wiki/Contributors-Guide)
- 🚧 [Project Board](https://github.com/antares-sql/antares/projects/1) - 🚧 [Project Board](https://github.com/orgs/antares-sql/projects/3/views/2)
## Contributors ✨ ## Contributors ✨
@@ -157,6 +148,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Lawondyss"><img src="https://avatars.githubusercontent.com/u/272130?v=4?s=100" width="100px;" alt="Ladislav Vondráček"/><br /><sub><b>Ladislav Vondráček</b></sub></a><br /><a href="#translation-Lawondyss" title="Translation">🌍</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Lawondyss"><img src="https://avatars.githubusercontent.com/u/272130?v=4?s=100" width="100px;" alt="Ladislav Vondráček"/><br /><sub><b>Ladislav Vondráček</b></sub></a><br /><a href="#translation-Lawondyss" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zvlad"><img src="https://avatars.githubusercontent.com/u/9055134?v=4?s=100" width="100px;" alt="Vladyslav"/><br /><sub><b>Vladyslav</b></sub></a><br /><a href="#translation-zvlad" title="Translation">🌍</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/zvlad"><img src="https://avatars.githubusercontent.com/u/9055134?v=4?s=100" width="100px;" alt="Vladyslav"/><br /><sub><b>Vladyslav</b></sub></a><br /><a href="#translation-zvlad" title="Translation">🌍</a></td>
</tr> </tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bagusindrayana"><img src="https://avatars.githubusercontent.com/u/36830534?v=4?s=100" width="100px;" alt="Bagus Indrayana"/><br /><sub><b>Bagus Indrayana</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=bagusindrayana" title="Code">💻</a></td>
</tr>
</tbody> </tbody>
</table> </table>

33
commitlint.config.js Normal file
View File

@@ -0,0 +1,33 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
// TODO Add Scope Enum Here
// 'scope-enum': [2, 'always', ['yourscope', 'yourscope']],
'type-enum': [
2,
'always',
[
'feat',
'fix',
'docs',
'chore',
'style',
'refactor',
'build',
'ci',
'test',
'revert',
'perf'
]
],
'subject-case': [
2,
'never',
[
'upper-case',
'pascal-case',
'start-case'
]
]
}
};

9303
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "antares", "name": "antares",
"productName": "Antares", "productName": "Antares",
"version": "0.7.20-beta.0", "version": "0.7.23",
"description": "A modern, fast and productivity driven SQL client with a focus in UX.", "description": "A modern, fast and productivity driven SQL client with a focus in UX.",
"license": "MIT", "license": "MIT",
"repository": "https://github.com/antares-sql/antares.git", "repository": "https://github.com/antares-sql/antares.git",
@@ -25,7 +25,8 @@
"lint": "eslint . --ext .js,.ts,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"", "lint": "eslint . --ext .js,.ts,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"",
"lint:fix": "eslint . --ext .js,.ts,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix", "lint:fix": "eslint . --ext .js,.ts,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix",
"contributors:add": "all-contributors add", "contributors:add": "all-contributors add",
"contributors:generate": "all-contributors generate" "contributors:generate": "all-contributors generate",
"prepare": "husky"
}, },
"author": "Fabio Di Stasio <info@fabiodistasio.it>", "author": "Fabio Di Stasio <info@fabiodistasio.it>",
"main": "./dist/main.js", "main": "./dist/main.js",
@@ -124,38 +125,63 @@
"@jamescoyle/vue-icon": "~0.1.2", "@jamescoyle/vue-icon": "~0.1.2",
"@mdi/js": "~7.2.96", "@mdi/js": "~7.2.96",
"@turf/helpers": "~6.5.0", "@turf/helpers": "~6.5.0",
"@vue/compiler-sfc": "~3.2.33",
"@vueuse/core": "~10.4.1", "@vueuse/core": "~10.4.1",
"ace-builds": "~1.24.1", "ace-builds": "~1.24.1",
"better-sqlite3": "~9.1.1", "babel-loader": "~8.2.3",
"electron-log": "~4.4.1", "better-sqlite3": "~9.4.1",
"chalk": "~4.1.2",
"cross-env": "~7.0.2",
"css-loader": "~6.5.0",
"electron-log": "~5.0.1",
"electron-store": "~8.1.0", "electron-store": "~8.1.0",
"electron-updater": "~4.6.5", "electron-updater": "~4.6.5",
"electron-window-state": "~5.0.3", "electron-window-state": "~5.0.3",
"encoding": "~0.1.13", "encoding": "~0.1.13",
"file-loader": "~6.2.0",
"floating-vue": "~2.0.0-beta.20", "floating-vue": "~2.0.0-beta.20",
"html-webpack-plugin": "~5.5.0",
"json2php": "~0.0.7", "json2php": "~0.0.7",
"leaflet": "~1.7.1", "leaflet": "~1.7.1",
"marked": "~4.0.19", "marked": "~12.0.0",
"moment": "~2.29.4", "mini-css-extract-plugin": "~2.4.5",
"mysql2": "~3.5.2", "moment": "~2.30.1",
"mysql2": "~3.9.1",
"node-firebird": "~1.1.4", "node-firebird": "~1.1.4",
"pg": "~8.11.1", "node-loader": "~2.0.0",
"pg": "~8.11.3",
"pg-connection-string": "~2.5.0", "pg-connection-string": "~2.5.0",
"pg-query-stream": "~4.2.3", "pg-query-stream": "~4.2.3",
"pgsql-ast-parser": "~7.2.1", "pgsql-ast-parser": "~7.2.1",
"pinia": "~2.1.6", "pinia": "~2.1.7",
"postcss-html": "~1.5.0",
"progress-webpack-plugin": "~1.0.12",
"rimraf": "~3.0.2",
"sass": "~1.42.1",
"sass-loader": "~12.3.0",
"source-map-support": "~0.5.20", "source-map-support": "~0.5.20",
"spectre.css": "~0.5.9", "spectre.css": "~0.5.9",
"sql-formatter": "~13.0.0", "sql-formatter": "~13.0.0",
"sql-highlight": "~4.4.0",
"style-loader": "~3.3.1",
"tree-kill": "~1.2.2",
"ts-loader": "~9.2.8",
"typescript": "~4.6.3",
"unzip-crx-3": "~0.2.0",
"v-mask": "~2.3.0", "v-mask": "~2.3.0",
"vue": "~3.3.4", "vue": "~3.4.19",
"vue-i18n": "~9.2.2", "vue-i18n": "~9.2.2",
"vuedraggable": "~4.1.0" "vue-loader": "~16.8.3",
"vuedraggable": "~4.1.0",
"webpack": "~5.72.0",
"webpack-cli": "~4.9.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "~7.15.7", "@babel/eslint-parser": "~7.15.7",
"@babel/preset-env": "~7.15.8", "@babel/preset-env": "~7.15.8",
"@babel/preset-typescript": "~7.16.7", "@babel/preset-typescript": "~7.16.7",
"@commitlint/cli": "~19.0.3",
"@commitlint/config-conventional": "~19.0.3",
"@playwright/test": "~1.28.1", "@playwright/test": "~1.28.1",
"@types/better-sqlite3": "~7.5.0", "@types/better-sqlite3": "~7.5.0",
"@types/leaflet": "~1.7.9", "@types/leaflet": "~1.7.9",
@@ -165,13 +191,8 @@
"@types/ssh2": "~1.11.6", "@types/ssh2": "~1.11.6",
"@typescript-eslint/eslint-plugin": "~5.18.0", "@typescript-eslint/eslint-plugin": "~5.18.0",
"@typescript-eslint/parser": "~5.18.0", "@typescript-eslint/parser": "~5.18.0",
"@vue/compiler-sfc": "~3.2.33",
"all-contributors-cli": "~6.20.0", "all-contributors-cli": "~6.20.0",
"babel-loader": "~8.2.3", "electron": "~26.6.9",
"chalk": "~4.1.2",
"cross-env": "~7.0.2",
"css-loader": "~6.5.0",
"electron": "~22.3.27",
"electron-builder": "~24.6.4", "electron-builder": "~24.6.4",
"eslint": "~7.32.0", "eslint": "~7.32.0",
"eslint-config-standard": "~16.0.3", "eslint-config-standard": "~16.0.3",
@@ -180,32 +201,16 @@
"eslint-plugin-promise": "~5.2.0", "eslint-plugin-promise": "~5.2.0",
"eslint-plugin-simple-import-sort": "~10.0.0", "eslint-plugin-simple-import-sort": "~10.0.0",
"eslint-plugin-vue": "~8.0.3", "eslint-plugin-vue": "~8.0.3",
"file-loader": "~6.2.0", "husky": "~9.0.11",
"html-webpack-plugin": "~5.5.0",
"mini-css-extract-plugin": "~2.4.5",
"node-loader": "~2.0.0",
"playwright": "~1.28.1", "playwright": "~1.28.1",
"playwright-core": "~1.28.1", "playwright-core": "~1.28.1",
"postcss-html": "~1.5.0",
"progress-webpack-plugin": "~1.0.12",
"rimraf": "~3.0.2",
"sass": "~1.42.1",
"sass-loader": "~12.3.0",
"standard-version": "~9.3.1", "standard-version": "~9.3.1",
"style-loader": "~3.3.1",
"stylelint": "^15.11.0", "stylelint": "^15.11.0",
"stylelint-config-recommended-vue": "~1.5.0", "stylelint-config-recommended-vue": "~1.5.0",
"stylelint-config-standard": "~34.0.0", "stylelint-config-standard": "~34.0.0",
"stylelint-scss": "~5.3.0", "stylelint-scss": "~5.3.0",
"tree-kill": "~1.2.2",
"ts-loader": "~9.2.8",
"ts-node": "~10.9.1", "ts-node": "~10.9.1",
"typescript": "~4.6.3",
"unzip-crx-3": "~0.2.0",
"vue-eslint-parser": "~8.3.0", "vue-eslint-parser": "~8.3.0",
"vue-loader": "~16.8.3",
"webpack": "~5.72.0",
"webpack-cli": "~4.9.1",
"webpack-dev-server": "~4.11.1", "webpack-dev-server": "~4.11.1",
"xvfb-maybe": "~0.2.1" "xvfb-maybe": "~0.2.1"
} }

View File

@@ -63,43 +63,39 @@ async function restartElectron () {
if (!manualRestart) process.exit(0); if (!manualRestart) process.exit(0);
}); });
} }
function startWorkers () {
const compiler = webpack(workersConfig);
const { name } = compiler;
function startMain () { compiler.hooks.afterEmit.tap('afterEmit', () => {
const webpackSetup = webpack([mainConfig, workersConfig]); console.log(chalk.gray(`\nCompiled ${name} script!`));
console.log(chalk.gray(`\nWatching file changes for ${name} script...`));
webpackSetup.compilers.forEach((compiler) => {
const { name } = compiler;
switch (name) {
case 'workers':
compiler.hooks.afterEmit.tap('afterEmit', async () => {
console.log(chalk.gray(`\nCompiled ${name} script!`));
console.log(
chalk.gray(`\nWatching file changes for ${name} script...`)
);
});
break;
case 'main':
default:
compiler.hooks.afterEmit.tap('afterEmit', async () => {
console.log(chalk.gray(`\nCompiled ${name} script!`));
manualRestart = true;
await restartElectron();
setTimeout(() => {
manualRestart = false;
}, 2500);
console.log(
chalk.gray(`\nWatching file changes for ${name} script...`)
);
});
break;
}
}); });
webpackSetup.watch({ aggregateTimeout: 500 }, err => { compiler.watch({ aggregateTimeout: 500 }, err => {
if (err) console.error(chalk.red(err));
});
}
function startMain () {
const compiler = webpack(mainConfig);
const { name } = compiler;
compiler.hooks.afterEmit.tap('afterEmit', async () => {
console.log(chalk.gray(`\nCompiled ${name} script!`));
manualRestart = true;
await restartElectron();
startWorkers();
setTimeout(() => {
manualRestart = false;
}, 2500);
console.log(chalk.gray(`\nWatching file changes for ${name} script...`));
});
compiler.watch({ aggregateTimeout: 500 }, err => {
if (err) console.error(chalk.red(err)); if (err) console.error(chalk.red(err));
}); });
} }
@@ -115,6 +111,7 @@ function startRenderer (callback) {
const server = new WebpackDevServer(compiler, { const server = new WebpackDevServer(compiler, {
port: 9080, port: 9080,
hot: true,
client: { client: {
overlay: true, overlay: true,
logging: 'warn' logging: 'warn'

View File

@@ -42,6 +42,7 @@ const downloadFile = url => {
await unzip(filePath, destFolder); await unzip(filePath, destFolder);
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
fs.unlinkSync(`${destFolder}/package.json`);// <- Avoid to display annoyng npm script in vscode fs.unlinkSync(`${destFolder}/package.json`);// <- Avoid to display annoyng npm script in vscode
process.exit();
} }
catch (error) { catch (error) {
console.log(error); console.log(error);

View File

@@ -19,6 +19,7 @@ export const defaults: Customizations = {
sshConnection: false, sshConnection: false,
fileConnection: false, fileConnection: false,
cancelQueries: false, cancelQueries: false,
singleConnectionMode: false,
// Tools // Tools
processesList: false, processesList: false,
usersManagement: false, usersManagement: false,

View File

@@ -29,6 +29,7 @@ export const customizations: Customizations = {
sslConnection: true, sslConnection: true,
sshConnection: true, sshConnection: true,
cancelQueries: true, cancelQueries: true,
singleConnectionMode: true,
// Tools // Tools
processesList: true, processesList: true,
// Structure // Structure

View File

@@ -66,7 +66,7 @@ export default [
group: 'monetary', group: 'monetary',
types: [ types: [
{ {
name: 'money', name: 'MONEY',
length: false, length: false,
unsigned: true unsigned: true
} }

View File

@@ -52,6 +52,7 @@ export interface ConnectionParams {
password: string; password: string;
ask: boolean; ask: boolean;
readonly: boolean; readonly: boolean;
singleConnectionMode: boolean;
ssl: boolean; ssl: boolean;
cert?: string; cert?: string;
key?: string; key?: string;
@@ -363,7 +364,7 @@ export interface QueryBuilderObject {
offset: number; offset: number;
join: string[]; join: string[];
update: string[]; update: string[];
insert: {[key: string]: string | boolean | number }[]; insert: Record<string, string | boolean | number>[];
delete: boolean; delete: boolean;
} }

View File

@@ -19,6 +19,7 @@ export interface Customizations {
sshConnection?: boolean; sshConnection?: boolean;
fileConnection?: boolean; fileConnection?: boolean;
cancelQueries?: boolean; cancelQueries?: boolean;
singleConnectionMode?: boolean;
// Tools // Tools
processesList?: boolean; processesList?: boolean;
usersManagement?: boolean; usersManagement?: boolean;

View File

@@ -13,7 +13,7 @@ export interface ExportOptions {
includeContent: boolean; includeContent: boolean;
includeDropStatement: boolean; includeDropStatement: boolean;
}[]; }[];
includes: {[key: string]: boolean}; includes: Record<string, boolean>;
outputFormat: 'sql' | 'sql.zip'; outputFormat: 'sql' | 'sql.zip';
outputFile: string; outputFile: string;
sqlInsertAfter: number; sqlInsertAfter: number;

View File

@@ -18,7 +18,7 @@ export interface TableDeleteParams {
primary?: string; primary?: string;
field: string; field: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
rows: {[key: string]: any}; rows: Record<string, any>;
} }
export type TableFilterOperator = '=' | '!=' | '>' | '<' | '>=' | '<=' | 'IN' | 'NOT IN' | 'LIKE' | 'NOT LIKE' | 'RLIKE' | 'NOT RLIKE' | 'BETWEEN' | 'IS NULL' | 'IS NOT NULL' export type TableFilterOperator = '=' | '!=' | '>' | '<' | '>=' | '<=' | 'IN' | 'NOT IN' | 'LIKE' | 'NOT LIKE' | 'RLIKE' | 'NOT RLIKE' | 'BETWEEN' | 'IS NULL' | 'IS NOT NULL'
@@ -35,17 +35,16 @@ export interface InsertRowsParams {
uid: string; uid: string;
schema: string; schema: string;
table: string; table: string;
row: {[key: string]: { row: Record<string, {
group: string; group: string;
method: string; method: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
params: any; params: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any; value: any;
length: number; length: number;
}; }>;
};
repeat: number; repeat: number;
fields: {[key: string]: string}; fields: Record<string, string>;
locale: UsableLocale; locale: UsableLocale;
} }

View File

@@ -41,7 +41,7 @@ export const escapeAndQuote = (val: string, client: ClientCode) => {
const { stringsWrapper: sw } = customizations[client]; const { stringsWrapper: sw } = customizations[client];
// eslint-disable-next-line no-control-regex // eslint-disable-next-line no-control-regex
const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g; const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g;
const CHARS_ESCAPE_MAP: {[key: string]: string} = { const CHARS_ESCAPE_MAP: Record<string, string> = {
'\0': '\\0', '\0': '\\0',
'\b': '\\b', '\b': '\\b',
'\t': '\\t', '\t': '\\t',
@@ -153,9 +153,9 @@ export const valueToSqlString = (args: {
}; };
export const jsonToSqlInsert = (args: { export const jsonToSqlInsert = (args: {
json: { [key: string]: any}[]; json: Record<string, any>[];
client: ClientCode; client: ClientCode;
fields: { [key: string]: {type: string; datePrecision: number}}; fields: Record<string, {type: string; datePrecision: number}>;
table: string; table: string;
options?: {sqlInsertAfter: number; sqlInsertDivider: 'bytes' | 'rows'}; options?: {sqlInsertAfter: number; sqlInsertDivider: 'bytes' | 'rows'};
}) => { }) => {

View File

@@ -1,4 +1,4 @@
export const shortcutEvents: { [key: string]: { l18n: string; l18nParam?: string | number; context?: 'tab' }} = { export const shortcutEvents: Record<string, { l18n: string; l18nParam?: string | number; context?: 'tab' }> = {
'run-or-reload': { l18n: 'application.runOrReload', context: 'tab' }, 'run-or-reload': { l18n: 'application.runOrReload', context: 'tab' },
'open-new-tab': { l18n: 'application.openNewTab', context: 'tab' }, 'open-new-tab': { l18n: 'application.openNewTab', context: 'tab' },
'close-tab': { l18n: 'application.closeTab', context: 'tab' }, 'close-tab': { l18n: 'application.closeTab', context: 'tab' },

View File

@@ -36,9 +36,15 @@ export default () => {
name: 'session', name: 'session',
fileExtension: '' fileExtension: ''
}); });
const encrypted = sessionStore.get('key') as string;
const key = safeStorage.decryptString(Buffer.from(encrypted, 'utf-8')); try {
event.returnValue = key; const encrypted = sessionStore.get('key') as string;
const key = safeStorage.decryptString(Buffer.from(encrypted, 'utf-8'));
event.returnValue = key;
}
catch (error) {
event.returnValue = false;
}
}); });
ipcMain.handle('show-open-dialog', (event, options) => { ipcMain.handle('show-open-dialog', (event, options) => {

View File

@@ -6,7 +6,7 @@ import { SslOptions } from 'mysql2';
import { ClientsFactory } from '../libs/ClientsFactory'; import { ClientsFactory } from '../libs/ClientsFactory';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: Record<string, antares.Client>) => {
ipcMain.handle('test-connection', async (event, conn: antares.ConnectionParams) => { ipcMain.handle('test-connection', async (event, conn: antares.ConnectionParams) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
@@ -146,7 +146,7 @@ export default (connections: {[key: string]: antares.Client}) => {
uid: conn.uid, uid: conn.uid,
client: conn.client, client: conn.client,
params, params,
poolSize: 5 poolSize: conn.singleConnectionMode ? 0 : 5
}); });
await connection.connect(); await connection.connect();

View File

@@ -3,7 +3,7 @@ import { ipcMain } from 'electron';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: Record<string, antares.Client>) => {
ipcMain.handle('get-databases', async (event, uid) => { ipcMain.handle('get-databases', async (event, uid) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };

View File

@@ -3,7 +3,7 @@ import { ipcMain } from 'electron';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: Record<string, antares.Client>) => {
ipcMain.handle('get-function-informations', async (event, params) => { ipcMain.handle('get-function-informations', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };

View File

@@ -13,7 +13,7 @@ import updates from './updates';
import users from './users'; import users from './users';
import views from './views'; import views from './views';
const connections: {[key: string]: antares.Client} = {}; const connections: Record<string, antares.Client> = {};
export default () => { export default () => {
connection(connections); connection(connections);

View File

@@ -3,7 +3,7 @@ import { ipcMain } from 'electron';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: Record<string, antares.Client>) => {
ipcMain.handle('get-routine-informations', async (event, params) => { ipcMain.handle('get-routine-informations', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };

View File

@@ -3,7 +3,7 @@ import { ipcMain } from 'electron';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: Record<string, antares.Client>) => {
ipcMain.handle('get-scheduler-informations', async (event, params) => { ipcMain.handle('get-scheduler-informations', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };

View File

@@ -1,17 +1,14 @@
import { ChildProcess, fork } from 'child_process';
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import * as workers from 'common/interfaces/workers'; import * as workers from 'common/interfaces/workers';
import { dialog, ipcMain } from 'electron'; import { dialog, ipcMain } from 'electron';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import { Worker } from 'worker_threads';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
const isDevelopment = process.env.NODE_ENV !== 'production'; export default (connections: Record<string, antares.Client>) => {
let exporter: Worker = null;
export default (connections: {[key: string]: antares.Client}) => { let importer: Worker = null;
let exporter: ChildProcess = null;
let importer: ChildProcess = null;
ipcMain.handle('create-schema', async (event, params) => { ipcMain.handle('create-schema', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
@@ -202,7 +199,7 @@ export default (connections: {[key: string]: antares.Client}) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
if (exporter !== null) { if (exporter !== null) {
exporter.kill(); exporter.terminate();
return; return;
} }
@@ -227,11 +224,12 @@ export default (connections: {[key: string]: antares.Client}) => {
} }
} }
// Init exporter process // Init exporter thread
exporter = fork(isDevelopment ? './dist/exporter.js' : path.resolve(__dirname, './exporter.js'), [], { // eslint-disable-next-line @typescript-eslint/ban-ts-comment
execArgv: isDevelopment ? ['--inspect=9224'] : undefined // @ts-ignore
}); exporter = new Worker(new URL('../workers/exporter', import.meta.url));
exporter.send({
exporter.postMessage({
type: 'init', type: 'init',
client: { client: {
name: type, name: type,
@@ -242,32 +240,34 @@ export default (connections: {[key: string]: antares.Client}) => {
}); });
// Exporter message listener // Exporter message listener
exporter.on('message', ({ type, payload }: workers.WorkerIpcMessage) => { exporter.on('message', (message: workers.WorkerIpcMessage) => {
const { type, payload } = message;
switch (type) { switch (type) {
case 'export-progress': case 'export-progress':
event.sender.send('export-progress', payload); event.sender.send('export-progress', payload);
break; break;
case 'end': case 'end':
setTimeout(() => { // Ensures that writing process has finished setTimeout(() => { // Ensures that writing thread has finished
exporter.kill(); exporter?.terminate();
exporter = null; exporter = null;
}, 2000); }, 2000);
resolve({ status: 'success', response: payload }); resolve({ status: 'success', response: payload });
break; break;
case 'cancel': case 'cancel':
exporter.kill(); exporter?.terminate();
exporter = null; exporter = null;
resolve({ status: 'error', response: 'Operation cancelled' }); resolve({ status: 'error', response: 'Operation cancelled' });
break; break;
case 'error': case 'error':
exporter.kill(); exporter?.terminate();
exporter = null; exporter = null;
resolve({ status: 'error', response: payload }); resolve({ status: 'error', response: payload });
break; break;
} }
}); });
exporter.on('exit', code => { exporter.on('close', code => {
exporter = null; exporter = null;
resolve({ status: 'error', response: `Operation ended with code: ${code}` }); resolve({ status: 'error', response: `Operation ended with code: ${code}` });
}); });
@@ -291,7 +291,7 @@ export default (connections: {[key: string]: antares.Client}) => {
if (result.response === 1) { if (result.response === 1) {
willAbort = true; willAbort = true;
exporter.send({ type: 'cancel' }); exporter.postMessage({ type: 'cancel' });
} }
} }
@@ -302,7 +302,7 @@ export default (connections: {[key: string]: antares.Client}) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
if (importer !== null) { if (importer !== null) {
importer.kill(); importer.terminate();
return; return;
} }
@@ -310,18 +310,21 @@ export default (connections: {[key: string]: antares.Client}) => {
(async () => { (async () => {
const dbConfig = await connections[options.uid].getDbConfig(); const dbConfig = await connections[options.uid].getDbConfig();
// Init importer process // Init importer thread
importer = fork(isDevelopment ? './dist/importer.js' : path.resolve(__dirname, './importer.js'), [], { // eslint-disable-next-line @typescript-eslint/ban-ts-comment
execArgv: isDevelopment ? ['--inspect=9224'] : undefined // @ts-ignore
}); importer = new Worker(new URL('../workers/importer', import.meta.url));
importer.send({
importer.postMessage({
type: 'init', type: 'init',
dbConfig, dbConfig,
options options
}); });
// Importer message listener // Importer message listener
importer.on('message', ({ type, payload }: workers.WorkerIpcMessage) => { importer.on('message', (message: workers.WorkerIpcMessage) => {
const { type, payload } = message;
switch (type) { switch (type) {
case 'import-progress': case 'import-progress':
event.sender.send('import-progress', payload); event.sender.send('import-progress', payload);
@@ -331,23 +334,28 @@ export default (connections: {[key: string]: antares.Client}) => {
break; break;
case 'end': case 'end':
setTimeout(() => { // Ensures that writing process has finished setTimeout(() => { // Ensures that writing process has finished
importer?.kill(); importer?.terminate();
importer = null; importer = null;
}, 2000); }, 2000);
resolve({ status: 'success', response: payload }); resolve({ status: 'success', response: payload });
break; break;
case 'cancel': case 'cancel':
importer.kill(); importer.terminate();
importer = null; importer = null;
resolve({ status: 'error', response: 'Operation cancelled' }); resolve({ status: 'error', response: 'Operation cancelled' });
break; break;
case 'error': case 'error':
importer.kill(); importer.terminate();
importer = null; importer = null;
resolve({ status: 'error', response: payload }); resolve({ status: 'error', response: payload });
break; break;
} }
}); });
importer.on('close', code => {
importer = null;
resolve({ status: 'error', response: `Operation ended with code: ${code}` });
});
})(); })();
}); });
}); });
@@ -368,7 +376,7 @@ export default (connections: {[key: string]: antares.Client}) => {
if (result.response === 1) { if (result.response === 1) {
willAbort = true; willAbort = true;
importer.send({ type: 'cancel' }); importer.postMessage({ type: 'cancel' });
} }
} }

View File

@@ -10,7 +10,7 @@ import * as moment from 'moment';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: Record<string, antares.Client>) => {
ipcMain.handle('get-table-columns', async (event, params) => { ipcMain.handle('get-table-columns', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
@@ -249,7 +249,7 @@ export default (connections: {[key: string]: antares.Client}) => {
if (params.primary) { if (params.primary) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const idString = params.rows.map((row: {[key: string]: any}) => { const idString = params.rows.map((row: Record<string, any>) => {
const fieldName = Object.keys(row)[0].includes('.') ? `${params.table}.${params.primary}` : params.primary; const fieldName = Object.keys(row)[0].includes('.') ? `${params.table}.${params.primary}` : params.primary;
return typeof row[fieldName] === 'string' return typeof row[fieldName] === 'string'
@@ -304,10 +304,10 @@ export default (connections: {[key: string]: antares.Client}) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
try { // TODO: move to client classes try { // TODO: move to client classes
const rows: {[key: string]: string | number | boolean | Date | Buffer}[] = []; const rows: Record<string, string | number | boolean | Date | Buffer>[] = [];
for (let i = 0; i < +params.repeat; i++) { for (let i = 0; i < +params.repeat; i++) {
const insertObj: {[key: string]: string | number | boolean | Date | Buffer} = {}; const insertObj: Record<string, string | number | boolean | Date | Buffer> = {};
for (const key in params.row) { for (const key in params.row) {
const type = params.fields[key]; const type = params.fields[key];
@@ -367,7 +367,7 @@ export default (connections: {[key: string]: antares.Client}) => {
insertObj[key] = escapedParam; insertObj[key] = escapedParam;
} }
else { // Faker value else { // Faker value
const parsedParams: {[key: string]: string | number | boolean | Date | Buffer} = {}; const parsedParams: Record<string, string | number | boolean | Date | Buffer> = {};
let fakeValue; let fakeValue;
if (params.locale) if (params.locale)
@@ -437,12 +437,12 @@ export default (connections: {[key: string]: antares.Client}) => {
if (description) if (description)
query.select(`LEFT(${description}, 20) AS foreign_description`); query.select(`LEFT(${description}, 20) AS foreign_description`);
const results = await query.run<{[key: string]: string}>(); const results = await query.run<Record<string, string>>();
const parsedResults: {[key: string]: string}[] = []; const parsedResults: Record<string, string>[] = [];
for (const row of results.rows) { for (const row of results.rows) {
const remappedRow: {[key: string]: string} = {}; const remappedRow: Record<string, string> = {};
for (const key in row) for (const key in row)
remappedRow[key.toLowerCase()] = row[key];// Thanks Firebird -.- remappedRow[key.toLowerCase()] = row[key];// Thanks Firebird -.-

View File

@@ -3,7 +3,7 @@ import { ipcMain } from 'electron';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: Record<string, antares.Client>) => {
ipcMain.handle('get-trigger-informations', async (event, params) => { ipcMain.handle('get-trigger-informations', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };

View File

@@ -1,4 +1,5 @@
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import * as log from 'electron-log/main';
import * as Store from 'electron-store'; import * as Store from 'electron-store';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
@@ -59,7 +60,7 @@ export default () => {
mainWindow.reply('update-downloaded'); mainWindow.reply('update-downloaded');
}); });
// autoUpdater.logger = require('electron-log'); log.transports.file.level = 'info';
// autoUpdater.logger.transports.console.format = '{h}:{i}:{s} {text}'; // log.transports.console.format = '{h}:{i}:{s} {text}';
// autoUpdater.logger.transports.file.level = 'info'; autoUpdater.logger = log;
}; };

View File

@@ -3,7 +3,7 @@ import { ipcMain } from 'electron';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: Record<string, antares.Client>) => {
ipcMain.handle('get-users', async (event, uid) => { ipcMain.handle('get-users', async (event, uid) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };

View File

@@ -3,7 +3,7 @@ import { ipcMain } from 'electron';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: Record<string, antares.Client>) => {
ipcMain.handle('get-view-informations', async (event, params) => { ipcMain.handle('get-view-informations', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };

View File

@@ -70,23 +70,21 @@ export class ShortcutRegister {
} }
private setLocalShortcuts () { private setLocalShortcuts () {
const isMenuVisible = process.platform === 'darwin';
const submenu = [];
for (const shortcut of this.shortcuts) { for (const shortcut of this.shortcuts) {
if (shortcut.os.includes(process.platform)) { if (shortcut.os.includes(process.platform)) {
for (const key of shortcut.keys) { for (const key of shortcut.keys) {
try { try {
this._menu.append(new MenuItem({ submenu.push({
label: '.', label: String(shortcut.event),
visible: false, accelerator: key,
submenu: [{ visible: isMenuVisible,
label: String(key), click: () => {
accelerator: key, this._mainWindow.webContents.send(shortcut.event);
visible: false, if (isDevelopment) console.log('LOCAL EVENT:', shortcut);
click: () => { }
this._mainWindow.webContents.send(shortcut.event); });
if (isDevelopment) console.log('LOCAL EVENT:', shortcut);
}
}]
}));
} }
catch (error) { catch (error) {
if (isDevelopment) console.log(error); if (isDevelopment) console.log(error);
@@ -96,6 +94,11 @@ export class ShortcutRegister {
} }
} }
} }
this._menu.append(new MenuItem({
label: 'Shortcut',
visible: isMenuVisible,
submenu
}));
} }
private setGlobalShortcuts () { private setGlobalShortcuts () {

View File

@@ -10,7 +10,7 @@ const queryLogger = ({ sql, cUid }: {sql: string; cUid: string}) => {
const mainWindow = require('electron').webContents.fromId(1); const mainWindow = require('electron').webContents.fromId(1);
mainWindow.send('query-log', { cUid, sql: escapedSql, date: new Date() }); mainWindow.send('query-log', { cUid, sql: escapedSql, date: new Date() });
} }
if (process.env.NODE_ENV === 'development') console.log(escapedSql); if (process.env.NODE_ENV === 'development' && process.type === 'browser') console.log(escapedSql);
}; };
/** /**
@@ -136,7 +136,7 @@ export abstract class BaseClient {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
insert (arr: {[key: string]: any}[]) { insert (arr: Record<string, any>[]) {
this._query.insert = [...this._query.insert, ...arr]; this._query.insert = [...this._query.insert, ...arr];
return this; return this;
} }

View File

@@ -13,7 +13,7 @@ export class FirebirdSQLClient extends BaseClient {
protected _connection?: firebird.Database | firebird.ConnectionPool; protected _connection?: firebird.Database | firebird.ConnectionPool;
_params: firebird.Options; _params: firebird.Options;
private _types: {[key: number]: string} ={ private _types: Record<number, string> ={
452: 'CHAR', // Array of char 452: 'CHAR', // Array of char
448: 'VARCHAR', 448: 'VARCHAR',
500: 'SMALLINT', 500: 'SMALLINT',

View File

@@ -12,10 +12,11 @@ export class MySQLClient extends BaseClient {
private _connectionsToCommit: Map<string, mysql.Connection | mysql.PoolConnection>; private _connectionsToCommit: Map<string, mysql.Connection | mysql.PoolConnection>;
private _keepaliveTimer: NodeJS.Timer; private _keepaliveTimer: NodeJS.Timer;
private _keepaliveMs: number; private _keepaliveMs: number;
private sqlMode?: string[];
_connection?: mysql.Connection | mysql.Pool; _connection?: mysql.Connection | mysql.Pool;
_params: mysql.ConnectionOptions & {schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean}; _params: mysql.ConnectionOptions & {schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean};
private types: {[key: number]: string} = { private types: Record<number, string> = {
0: 'DECIMAL', 0: 'DECIMAL',
1: 'TINYINT', 1: 'TINYINT',
2: 'SMALLINT', 2: 'SMALLINT',
@@ -58,6 +59,10 @@ export class MySQLClient extends BaseClient {
this._keepaliveMs = 10*60*1000; this._keepaliveMs = 10*60*1000;
} }
private get isPool () {
return 'getConnection' in this._connection;
}
private _getType (field: mysql.FieldPacket & { columnType?: number; columnLength?: number }) { private _getType (field: mysql.FieldPacket & { columnType?: number; columnLength?: number }) {
let name = this.types[field.columnType]; let name = this.types[field.columnType];
let length = field.columnLength; let length = field.columnLength;
@@ -181,9 +186,32 @@ export class MySQLClient extends BaseClient {
async connect () { async connect () {
if (!this._poolSize) if (!this._poolSize)
this._connection = await this.getConnection(); this._connection = await this.getSingleConnection();
else else
this._connection = await this.getConnectionPool(); this._connection = await this.getConnectionPool();
// ANSI_QUOTES check
const [response] = await this._connection.query<mysql.RowDataPacket[]>('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\'');
this.sqlMode = response[0]?.Value?.split(',');
const hasAnsiQuotes = this.sqlMode.includes('ANSI') || this.sqlMode.includes('ANSI_QUOTES');
if (hasAnsiQuotes)
await this._connection.query(`SET SESSION sql_mode = '${this.sqlMode.filter((m: string) => !['ANSI', 'ANSI_QUOTES'].includes(m)).join(',')}'`);
if (this._params.readonly)
await this._connection.query('SET SESSION TRANSACTION READ ONLY');
if (this._poolSize) {
const hasAnsiQuotes = this.sqlMode.includes('ANSI') || this.sqlMode.includes('ANSI_QUOTES');
this._connection.on('connection', conn => {
if (this._params.readonly)
conn.query('SET SESSION TRANSACTION READ ONLY');
if (hasAnsiQuotes)
conn.query(`SET SESSION sql_mode = '${this.sqlMode.filter((m: string) => !['ANSI', 'ANSI_QUOTES'].includes(m)).join(',')}'`);
});
}
} }
destroy () { destroy () {
@@ -196,7 +224,7 @@ export class MySQLClient extends BaseClient {
} }
} }
async getConnection () { async getSingleConnection () {
const dbConfig = await this.getDbConfig(); const dbConfig = await this.getDbConfig();
const connection = await mysql.createConnection({ const connection = await mysql.createConnection({
...dbConfig, ...dbConfig,
@@ -208,17 +236,6 @@ export class MySQLClient extends BaseClient {
} }
}); });
// ANSI_QUOTES check
const [response] = await connection.query<mysql.RowDataPacket[]>('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\'');
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) => !['ANSI', 'ANSI_QUOTES'].includes(m)).join(',')}'`);
return connection; return connection;
} }
@@ -227,6 +244,7 @@ export class MySQLClient extends BaseClient {
const connection = mysql.createPool({ const connection = mysql.createPool({
...dbConfig, ...dbConfig,
connectionLimit: this._poolSize, connectionLimit: this._poolSize,
enableKeepAlive: true,
typeCast: (field, next) => { typeCast: (field, next) => {
if (field.type === 'DATETIME') if (field.type === 'DATETIME')
return field.string(); return field.string();
@@ -235,25 +253,6 @@ export class MySQLClient extends BaseClient {
} }
}); });
// ANSI_QUOTES check
const [res] = await connection.query<mysql.RowDataPacket[]>('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\'');
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) => !['ANSI', 'ANSI_QUOTES'].includes(m)).join(',')}'`);
if (this._params.readonly)
await connection.query('SET SESSION TRANSACTION READ ONLY');
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) => !['ANSI', 'ANSI_QUOTES'].includes(m)).join(',')}'`);
});
this._keepaliveTimer = setInterval(async () => { this._keepaliveTimer = setInterval(async () => {
await this.keepAlive(); await this.keepAlive();
}, this._keepaliveMs); }, this._keepaliveMs);
@@ -261,6 +260,43 @@ export class MySQLClient extends BaseClient {
return connection; return connection;
} }
async getConnection (args?: antares.QueryParams, retry?: boolean): Promise<mysql.Pool | mysql.PoolConnection | mysql.Connection> {
let connection;
try {
if (args && !args.autocommit && args.tabUid) { // autocommit OFF
if (this._connectionsToCommit.has(args.tabUid))
connection = this._connectionsToCommit.get(args.tabUid);
else {
connection = await this.getSingleConnection();
await connection.query('SET SESSION autocommit=0');
this._connectionsToCommit.set(args.tabUid, connection);
}
}
else// autocommit ON
connection = this.isPool ? await (this._connection as mysql.Pool).getConnection() : this._connection;
if (args && args.tabUid && this.isPool) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this._runningConnections.set(args.tabUid, (connection as any).connection.connectionId);
}
if (args && args.schema)
await connection.query(`USE \`${args.schema}\``);
return connection;
}
catch (error) {
if (error.code === 'ECONNRESET' && !retry) {
this.destroy();
await this.connect();
return this.getConnection(args, true);
}
else
throw new Error(error.message);
}
}
private async keepAlive () { private async keepAlive () {
try { try {
const connection = await (this._connection as mysql.Pool).getConnection(); const connection = await (this._connection as mysql.Pool).getConnection();
@@ -582,7 +618,7 @@ export class MySQLClient extends BaseClient {
} }
}) })
.filter(Boolean) .filter(Boolean)
.reduce((acc: {[key: string]: { name: string; type: string; length: string; default: string}}, curr) => { .reduce((acc: Record<string, { name: string; type: string; length: string; default: string}>, curr) => {
acc[curr.name] = curr; acc[curr.name] = curr;
return acc; return acc;
}, {}); }, {});
@@ -591,7 +627,7 @@ export class MySQLClient extends BaseClient {
return rows.map((field) => { return rows.map((field) => {
const numLengthMatch = field.COLUMN_TYPE.match(/int\(([^)]+)\)/); const numLengthMatch = field.COLUMN_TYPE.match(/int\(([^)]+)\)/);
const numLength = numLengthMatch ? +numLengthMatch.pop() : field.NUMERIC_PRECISION || null; const numLength = numLengthMatch ? +numLengthMatch.pop() : field.NUMERIC_PRECISION || null;
const enumValues = /(enum)/.test(field.COLUMN_TYPE) const enumValues = /(enum|set)/.test(field.COLUMN_TYPE)
? field.COLUMN_TYPE.match(/\(([^)]+)\)/)[0].slice(1, -1) ? field.COLUMN_TYPE.match(/\(([^)]+)\)/)[0].slice(1, -1)
: null; : null;
@@ -1648,28 +1684,7 @@ export class MySQLClient extends BaseClient {
.map(q => q.trim()) .map(q => q.trim())
: [sql]; : [sql];
let connection: mysql.Connection | mysql.Pool | mysql.PoolConnection; const connection = await this.getConnection(args);
const isPool = 'getConnection' in this._connection;
if (!args.autocommit && args.tabUid) { // autocommit OFF
if (this._connectionsToCommit.has(args.tabUid))
connection = this._connectionsToCommit.get(args.tabUid);
else {
connection = await this.getConnection();
await connection.query('SET SESSION autocommit=0');
this._connectionsToCommit.set(args.tabUid, connection);
}
}
else// autocommit ON
connection = isPool ? await (this._connection as mysql.Pool).getConnection() : this._connection;
if (args.tabUid && isPool) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this._runningConnections.set(args.tabUid, (connection as any).connection.connectionId);
}
if (args.schema)
await connection.query(`USE \`${args.schema}\``);
for (const query of queries) { for (const query of queries) {
if (!query) continue; if (!query) continue;
@@ -1729,7 +1744,7 @@ export class MySQLClient extends BaseClient {
}); });
} }
catch (err) { catch (err) {
if (isPool && args.autocommit) { if (this.isPool && args.autocommit) {
(connection as mysql.PoolConnection).release(); (connection as mysql.PoolConnection).release();
this._runningConnections.delete(args.tabUid); this._runningConnections.delete(args.tabUid);
} }
@@ -1741,7 +1756,7 @@ export class MySQLClient extends BaseClient {
keysArr = keysArr ? [...keysArr, ...response] : response; keysArr = keysArr ? [...keysArr, ...response] : response;
} }
catch (err) { catch (err) {
if (isPool && args.autocommit) { if (this.isPool && args.autocommit) {
(connection as mysql.PoolConnection).release(); (connection as mysql.PoolConnection).release();
this._runningConnections.delete(args.tabUid); this._runningConnections.delete(args.tabUid);
} }
@@ -1759,7 +1774,7 @@ export class MySQLClient extends BaseClient {
keys: keysArr keys: keysArr
}); });
}).catch((err) => { }).catch((err) => {
if (isPool && args.autocommit) { if (this.isPool && args.autocommit) {
(connection as mysql.PoolConnection).release(); (connection as mysql.PoolConnection).release();
this._runningConnections.delete(args.tabUid); this._runningConnections.delete(args.tabUid);
} }
@@ -1776,7 +1791,7 @@ export class MySQLClient extends BaseClient {
}); });
} }
if (isPool && args.autocommit) { if (this.isPool && args.autocommit) {
(connection as mysql.PoolConnection).release(); (connection as mysql.PoolConnection).release();
this._runningConnections.delete(args.tabUid); this._runningConnections.delete(args.tabUid);
} }

View File

@@ -88,8 +88,8 @@ export class PostgreSQLClient extends BaseClient {
private _keepaliveTimer: NodeJS.Timer; private _keepaliveTimer: NodeJS.Timer;
private _keepaliveMs: number; private _keepaliveMs: number;
protected _connection?: pg.Client | pg.Pool; protected _connection?: pg.Client | pg.Pool;
private types: {[key: string]: string} = {}; private types: Record<string, string> = {};
private _arrayTypes: {[key: string]: string} = { private _arrayTypes: Record<string, string> = {
_int2: 'SMALLINT', _int2: 'SMALLINT',
_int4: 'INTEGER', _int4: 'INTEGER',
_int8: 'BIGINT', _int8: 'BIGINT',
@@ -232,6 +232,10 @@ export class PostgreSQLClient extends BaseClient {
await this.keepAlive(); await this.keepAlive();
}, this._keepaliveMs); }, this._keepaliveMs);
connection.on('error', err => { // Intercepts errors and converts to rejections
Promise.reject(err);
});
return connection; return connection;
} }
@@ -652,7 +656,7 @@ export class PostgreSQLClient extends BaseClient {
let createSql = ''; let createSql = '';
const sequences = []; const sequences = [];
const columnsSql = []; const columnsSql = [];
const arrayTypes: {[key: string]: string} = { const arrayTypes: Record<string, string> = {
_int2: 'smallint', _int2: 'smallint',
_int4: 'integer', _int4: 'integer',
_int8: 'bigint', _int8: 'bigint',

View File

@@ -166,7 +166,7 @@ export class SQLiteClient extends BaseClient {
type: type.trim(), type: type.trim(),
schema: schema, schema: schema,
table: table, table: table,
numPrecision: [...NUMBER, ...FLOAT].includes(type) ? length : null, numLength: [...NUMBER, ...FLOAT].includes(type) ? length : null,
datePrecision: null, datePrecision: null,
charLength: ![...NUMBER, ...FLOAT].includes(type) ? length : null, charLength: ![...NUMBER, ...FLOAT].includes(type) ? length : null,
nullable: !field.notnull, nullable: !field.notnull,
@@ -658,7 +658,7 @@ export class SQLiteClient extends BaseClient {
let queryAllResult: any[]; let queryAllResult: any[];
let affectedRows; let affectedRows;
let fields; let fields;
const detectedTypes: {[key: string]: string} = {}; const detectedTypes: Record<string, string> = {};
try { try {
const stmt = connection.prepare(query); const stmt = connection.prepare(query);

View File

@@ -1,6 +1,5 @@
import * as exporter from 'common/interfaces/exporter'; import * as exporter from 'common/interfaces/exporter';
import { valueToSqlString } from 'common/libs/sqlUtils'; import { valueToSqlString } from 'common/libs/sqlUtils';
import * as mysql from 'mysql2/promise';
import { MySQLClient } from '../../clients/MySQLClient'; import { MySQLClient } from '../../clients/MySQLClient';
import { SqlExporter } from './SqlExporter'; import { SqlExporter } from './SqlExporter';
@@ -334,12 +333,10 @@ CREATE TABLE \`${view.Name}\`(
} }
async _queryStream (sql: string) { async _queryStream (sql: string) {
if (process.env.NODE_ENV === 'development') console.log('EXPORTER:', sql); const connection = await this._client.getConnection();
const isPool = 'getConnection' in this._client._connection;
const connection = isPool ? await (this._client._connection as mysql.Pool).getConnection() : this._client._connection;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const stream = (connection as any).connection.query(sql).stream(); const stream = (connection as any).connection.query(sql).stream();
const dispose = () => (connection as mysql.PoolConnection).release(); const dispose = () => connection.end();
stream.on('end', dispose); stream.on('end', dispose);
stream.on('error', dispose); stream.on('error', dispose);
@@ -357,7 +354,7 @@ CREATE TABLE \`${view.Name}\`(
escapeAndQuote (val: string) { escapeAndQuote (val: string) {
// eslint-disable-next-line no-control-regex // eslint-disable-next-line no-control-regex
const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g; const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g;
const CHARS_ESCAPE_MAP: {[key: string]: string} = { const CHARS_ESCAPE_MAP: Record<string, string> = {
'\0': '\\0', '\0': '\\0',
'\b': '\\b', '\b': '\\b',
'\t': '\\t', '\t': '\\t',

View File

@@ -59,7 +59,7 @@ SET row_security = off;\n\n\n`;
let createSql = ''; let createSql = '';
const sequences = []; const sequences = [];
const columnsSql = []; const columnsSql = [];
const arrayTypes: {[key: string]: string} = { const arrayTypes: Record<string, string> = {
_int2: 'smallint', _int2: 'smallint',
_int4: 'integer', _int4: 'integer',
_int8: 'bigint', _int8: 'bigint',
@@ -425,7 +425,6 @@ SET row_security = off;\n\n\n`;
} }
async _queryStream (sql: string) { async _queryStream (sql: string) {
if (process.env.NODE_ENV === 'development') console.log('EXPORTER:', sql);
const connection = await this._client.getConnection(); const connection = await this._client.getConnection();
const query = new QueryStream(sql, null); const query = new QueryStream(sql, null);
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -441,7 +440,7 @@ SET row_security = off;\n\n\n`;
escapeAndQuote (val: string) { escapeAndQuote (val: string) {
// eslint-disable-next-line no-control-regex // eslint-disable-next-line no-control-regex
const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g; const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g;
const CHARS_ESCAPE_MAP: {[key: string]: string} = { const CHARS_ESCAPE_MAP: Record<string, string> = {
'\0': '\\0', '\0': '\\0',
'\b': '\\b', '\b': '\\b',
'\t': '\\t', '\t': '\\t',

View File

@@ -33,8 +33,8 @@ export default class MySQLImporter extends BaseImporter {
parser.on('error', reject); parser.on('error', reject);
parser.on('close', async () => { parser.on('close', async () => {
console.log('TOTAL QUERIES', queryCount); // console.log('TOTAL QUERIES', queryCount);
console.log('import end'); // console.log('import end');
resolve(); resolve();
}); });

View File

@@ -33,8 +33,8 @@ export default class PostgreSQLImporter extends BaseImporter {
parser.on('error', reject); parser.on('error', reject);
parser.on('close', async () => { parser.on('close', async () => {
console.log('TOTAL QUERIES', queryCount); // console.log('TOTAL QUERIES', queryCount);
console.log('import end'); // console.log('import end');
resolve(); resolve();
}); });

View File

@@ -6,7 +6,7 @@ const isWindows = process.platform === 'win32';
const indexPath = path.resolve(__dirname, 'index.html').split(path.sep).join('/'); const indexPath = path.resolve(__dirname, 'index.html').split(path.sep).join('/');
export function validateSender (frame: WebFrameMain) { export function validateSender (frame: WebFrameMain) {
if (process.windowsStore) return true; // TEMP HOTFIX if (isWindows) return true; // TEMP HOTFIX
const frameUrl = new URL(frame.url); const frameUrl = new URL(frame.url);
const prefix = isWindows ? 'file:///' : 'file://'; const prefix = isWindows ? 'file:///' : 'file://';
const framePath = frameUrl.href.replace(prefix, ''); const framePath = frameUrl.href.replace(prefix, '');

View File

@@ -1,5 +1,6 @@
import * as remoteMain from '@electron/remote/main'; import * as remoteMain from '@electron/remote/main';
import { app, BrowserWindow, ipcMain, nativeImage, safeStorage } from 'electron'; import { app, BrowserWindow, ipcMain, nativeImage, safeStorage } from 'electron';
import * as log from 'electron-log/main';
import * as Store from 'electron-store'; import * as Store from 'electron-store';
import * as windowStateKeeper from 'electron-window-state'; import * as windowStateKeeper from 'electron-window-state';
import * as path from 'path'; import * as path from 'path';
@@ -8,6 +9,7 @@ import ipcHandlers from './ipc-handlers';
import { OsMenu, ShortcutRegister } from './libs/ShortcutRegister'; import { OsMenu, ShortcutRegister } from './libs/ShortcutRegister';
Store.initRenderer(); Store.initRenderer();
log.errorHandler.startCatching();
const settingsStore = new Store({ name: 'settings' }); const settingsStore = new Store({ name: 'settings' });
const appTheme = settingsStore.get('application_theme'); const appTheme = settingsStore.get('application_theme');
const isDevelopment = process.env.NODE_ENV !== 'production'; const isDevelopment = process.env.NODE_ENV !== 'production';
@@ -125,8 +127,8 @@ app.on('ready', async () => {
if (isWindows) if (isWindows)
mainWindow.show(); mainWindow.show();
// if (isDevelopment) if (isDevelopment && !isWindows)// Because on Windows you can open devtools from title-bar
// mainWindow.webContents.openDevTools(); mainWindow.webContents.openDevTools();
process.on('uncaughtException', error => { process.on('uncaughtException', error => {
mainWindow.webContents.send('unhandled-exception', error); mainWindow.webContents.send('unhandled-exception', error);

View File

@@ -1,5 +1,7 @@
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import * as log from 'electron-log/main';
import * as fs from 'fs'; import * as fs from 'fs';
import { parentPort } from 'worker_threads';
import { MySQLClient } from '../libs/clients/MySQLClient'; import { MySQLClient } from '../libs/clients/MySQLClient';
import { PostgreSQLClient } from '../libs/clients/PostgreSQLClient'; import { PostgreSQLClient } from '../libs/clients/PostgreSQLClient';
@@ -8,63 +10,78 @@ import MysqlExporter from '../libs/exporters/sql/MysqlExporter';
import PostgreSQLExporter from '../libs/exporters/sql/PostgreSQLExporter'; import PostgreSQLExporter from '../libs/exporters/sql/PostgreSQLExporter';
let exporter: antares.Exporter; let exporter: antares.Exporter;
process.on('message', async ({ type, client, tables, options }: any) => { log.transports.file.fileName = 'workers.log';
log.transports.console = null;
log.errorHandler.startCatching();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const exportHandler = async (data: any) => {
const { type, client, tables, options } = data;
if (type === 'init') { if (type === 'init') {
const connection = await ClientsFactory.getClient({ try {
client: client.name, const connection = await ClientsFactory.getClient({
params: client.config, client: client.name,
poolSize: 5 params: client.config,
}) as MySQLClient | PostgreSQLClient; poolSize: 5
await connection.connect(); }) as MySQLClient | PostgreSQLClient;
await connection.connect();
switch (client.name) { switch (client.name) {
case 'mysql': case 'mysql':
case 'maria': case 'maria':
exporter = new MysqlExporter(connection as MySQLClient, tables, options); exporter = new MysqlExporter(connection as MySQLClient, tables, options);
break; break;
case 'pg': case 'pg':
exporter = new PostgreSQLExporter(connection as PostgreSQLClient, tables, options); exporter = new PostgreSQLExporter(connection as PostgreSQLClient, tables, options);
break; break;
default: default:
process.send({ parentPort.postMessage({
type: 'error',
payload: `"${client.name}" exporter not aviable`
});
return;
}
exporter.once('error', err => {
log.error(err.toString());
parentPort.postMessage({
type: 'error', type: 'error',
payload: `"${client.name}" exporter not aviable` payload: err.toString()
}); });
return; });
}
exporter.once('error', err => { exporter.once('end', () => {
console.error(err); parentPort.postMessage({
process.send({ type: 'end',
payload: { cancelled: exporter.isCancelled }
});
});
exporter.once('cancel', () => {
fs.unlinkSync(exporter.outputFile);
parentPort.postMessage({ type: 'cancel' });
});
exporter.on('progress', state => {
parentPort.postMessage({
type: 'export-progress',
payload: state
});
});
exporter.run();
}
catch (err) {
log.error(err.toString());
parentPort.postMessage({
type: 'error', type: 'error',
payload: err.toString() payload: err.toString()
}); });
}); }
exporter.once('end', () => {
process.send({
type: 'end',
payload: { cancelled: exporter.isCancelled }
});
connection.destroy();
});
exporter.once('cancel', () => {
fs.unlinkSync(exporter.outputFile);
process.send({ type: 'cancel' });
});
exporter.on('progress', state => {
process.send({
type: 'export-progress',
payload: state
});
});
exporter.run();
} }
else if (type === 'cancel') else if (type === 'cancel')
exporter.cancel(); exporter.cancel();
}); };
process.on('beforeExit', console.log); parentPort.on('message', exportHandler);

View File

@@ -1,8 +1,10 @@
import SSHConfig from '@fabio286/ssh2-promise/lib/sshConfig'; import SSHConfig from '@fabio286/ssh2-promise/lib/sshConfig';
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import { ImportOptions } from 'common/interfaces/importer'; import { ImportOptions } from 'common/interfaces/importer';
import * as log from 'electron-log/main';
import * as mysql from 'mysql2'; import * as mysql from 'mysql2';
import * as pg from 'pg'; import * as pg from 'pg';
import { parentPort } from 'worker_threads';
import { MySQLClient } from '../libs/clients/MySQLClient'; import { MySQLClient } from '../libs/clients/MySQLClient';
import { PostgreSQLClient } from '../libs/clients/PostgreSQLClient'; import { PostgreSQLClient } from '../libs/clients/PostgreSQLClient';
@@ -11,14 +13,18 @@ import MySQLImporter from '../libs/importers/sql/MySQLlImporter';
import PostgreSQLImporter from '../libs/importers/sql/PostgreSQLImporter'; import PostgreSQLImporter from '../libs/importers/sql/PostgreSQLImporter';
let importer: antares.Importer; let importer: antares.Importer;
// eslint-disable-next-line @typescript-eslint/no-explicit-any log.transports.file.fileName = 'workers.log';
process.on('message', async ({ type, dbConfig, options }: { log.transports.console = null;
log.errorHandler.startCatching();
const importHandler = async (data: {
type: string; type: string;
dbConfig: mysql.ConnectionOptions & { schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean } dbConfig: mysql.ConnectionOptions & { schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean }
| pg.ClientConfig & { schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean } | pg.ClientConfig & { schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean }
| { databasePath: string; readonly: boolean }; | { databasePath: string; readonly: boolean };
options: ImportOptions; options: ImportOptions;
}) => { }) => {
const { type, dbConfig, options } = data;
if (type === 'init') { if (type === 'init') {
try { try {
const connection = await ClientsFactory.getClient({ const connection = await ClientsFactory.getClient({
@@ -41,7 +47,7 @@ process.on('message', async ({ type, dbConfig, options }: {
importer = new PostgreSQLImporter(pool as unknown as pg.PoolClient, options); importer = new PostgreSQLImporter(pool as unknown as pg.PoolClient, options);
break; break;
default: default:
process.send({ parentPort.postMessage({
type: 'error', type: 'error',
payload: `"${options.type}" importer not aviable` payload: `"${options.type}" importer not aviable`
}); });
@@ -49,33 +55,33 @@ process.on('message', async ({ type, dbConfig, options }: {
} }
importer.once('error', err => { importer.once('error', err => {
console.error(err); log.error(err.toString());
process.send({ parentPort.postMessage({
type: 'error', type: 'error',
payload: err.toString() payload: err.toString()
}); });
}); });
importer.once('end', () => { importer.once('end', () => {
process.send({ parentPort.postMessage({
type: 'end', type: 'end',
payload: { cancelled: importer.isCancelled } payload: { cancelled: importer.isCancelled }
}); });
}); });
importer.once('cancel', () => { importer.once('cancel', () => {
process.send({ type: 'cancel' }); parentPort.postMessage({ type: 'cancel' });
}); });
importer.on('progress', state => { importer.on('progress', state => {
process.send({ parentPort.postMessage({
type: 'import-progress', type: 'import-progress',
payload: state payload: state
}); });
}); });
importer.on('query-error', state => { importer.on('query-error', state => {
process.send({ parentPort.postMessage({
type: 'query-error', type: 'query-error',
payload: state payload: state
}); });
@@ -84,8 +90,8 @@ process.on('message', async ({ type, dbConfig, options }: {
importer.run(); importer.run();
} }
catch (err) { catch (err) {
console.error(err); log.error(err.toString());
process.send({ parentPort.postMessage({
type: 'error', type: 'error',
payload: err.toString() payload: err.toString()
}); });
@@ -93,20 +99,6 @@ process.on('message', async ({ type, dbConfig, options }: {
} }
else if (type === 'cancel') else if (type === 'cancel')
importer.cancel(); importer.cancel();
}); };
process.on('uncaughtException', (err) => { parentPort.on('message', importHandler);
console.error(err);
process.send({
type: 'error',
payload: err.toString()
});
});
process.on('unhandledRejection', (err) => {
console.error(err);
process.send({
type: 'error',
payload: err.toString()
});
});

View File

@@ -56,8 +56,8 @@ const { t } = useI18n();
const props = defineProps({ const props = defineProps({
size: { size: {
type: String as PropType<'small' | 'medium' | '400' | 'large'>, type: String as PropType<'small' | 'medium' | '400' | 'large' | 'resize'>,
validator: (prop: string) => ['small', 'medium', '400', 'large'].includes(prop), validator: (prop: string) => ['small', 'medium', '400', 'large', 'resize'].includes(prop),
default: 'small' default: 'small'
}, },
hideFooter: { hideFooter: {
@@ -88,6 +88,8 @@ const modalSizeClass = computed(() => {
return 'modal-sm'; return 'modal-sm';
if (props.size === '400') if (props.size === '400')
return 'modal-400'; return 'modal-400';
if (props.size === 'resize')
return 'modal-resize';
else if (props.size === 'large') else if (props.size === 'large')
return 'modal-lg'; return 'modal-lg';
else return ''; else return '';
@@ -120,6 +122,12 @@ onBeforeUnmount(() => {
max-width: 400px; max-width: 400px;
} }
.modal-resize .modal-container {
max-width: 95vw;
max-height: 95vh;
width: auto;
}
.modal.modal-sm .modal-container { .modal.modal-sm .modal-container {
padding: 0; padding: 0;
} }

View File

@@ -280,7 +280,6 @@ export default defineComponent({
if (props.searchable) if (props.searchable)
searchInput.value.focus(); searchInput.value.focus();
else else
el.value.focus(); el.value.focus();

View File

@@ -4,7 +4,11 @@
:id="`editor-${id}`" :id="`editor-${id}`"
class="editor" class="editor"
:class="editorClass" :class="editorClass"
:style="{height: `${height}px`}" :style="{
height: `${height}px`,
width: width ? `${width}px` : null,
resize: resizable ? 'both' : 'none'
}"
/> />
</div> </div>
</template> </template>
@@ -17,7 +21,7 @@ import 'ace-builds/webpack-resolver';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { onMounted, watch } from 'vue'; import { PropType, onMounted, watch } from 'vue';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
@@ -25,10 +29,12 @@ const props = defineProps({
modelValue: String, modelValue: String,
mode: { type: String, default: 'text' }, mode: { type: String, default: 'text' },
editorClass: { type: String, default: '' }, editorClass: { type: String, default: '' },
resizable: { type: Boolean, default: false },
autoFocus: { type: Boolean, default: false }, autoFocus: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false }, readOnly: { type: Boolean, default: false },
showLineNumbers: { type: Boolean, default: true }, showLineNumbers: { type: Boolean, default: true },
height: { type: Number, default: 200 } height: { type: Number, default: 200 },
width: { type: [Number, Boolean] as PropType<number|false>, default: false }
}); });
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
@@ -132,8 +138,10 @@ onMounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.editor-wrapper { .editor-wrapper {
.editor { .editor {
width: 100%; width: 100%;
} height: 100%;
max-width: 90vw;
}
} }
</style> </style>

View File

@@ -54,7 +54,7 @@ const updateWindow = () => {
const totalScrollHeight = props.items.length * props.itemHeight; const totalScrollHeight = props.items.length * props.itemHeight;
const offset = 50; const offset = 50;
const scrollTop = localScrollElement.value.scrollTop; const scrollTop = localScrollElement.value?.scrollTop;
const firstVisibleIndex = Math.floor(scrollTop / props.itemHeight); const firstVisibleIndex = Math.floor(scrollTop / props.itemHeight);
const lastVisibleIndex = firstVisibleIndex + visibleItemsCount; const lastVisibleIndex = firstVisibleIndex + visibleItemsCount;

View File

@@ -113,7 +113,7 @@ const selectedGroup: Ref<string> = ref('manual');
const selectedMethod: Ref<string> = ref(''); const selectedMethod: Ref<string> = ref('');
const selectedValue: Ref<string> = ref(''); const selectedValue: Ref<string> = ref('');
const debounceTimeout: Ref<NodeJS.Timeout> = ref(null); const debounceTimeout: Ref<NodeJS.Timeout> = ref(null);
const methodParams: Ref<{[key: string]: string}> = ref({}); const methodParams: Ref<Record<string, string>> = ref({});
const enumArray: Ref<string[]> = ref(null); const enumArray: Ref<string[]> = ref(null);
const fakerGroups = computed(() => { const fakerGroups = computed(() => {

View File

@@ -152,6 +152,14 @@
/> />
SSH SSH
</div> </div>
<div v-if="connection.readonly" class="chip bg-success mt-2">
<BaseIcon
icon-name="mdiLock"
class="mr-1"
:size="18"
/>
Read-only
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -73,7 +73,7 @@ const props = defineProps({
const emit = defineEmits(['confirm', 'close']); const emit = defineEmits(['confirm', 'close']);
const firstInput: Ref<HTMLInputElement[]> = ref(null); const firstInput: Ref<HTMLInputElement[]> = ref(null);
const values: Ref<{[key: string]: string}> = ref({}); const values: Ref<Record<string, string>> = ref({});
const inParameters = computed(() => { const inParameters = computed(() => {
return props.localRoutine.parameters.filter(param => param.context === 'IN'); return props.localRoutine.parameters.filter(param => param.context === 'IN');

View File

@@ -327,7 +327,7 @@ const tables: Ref<{
}[]> = ref([]); }[]> = ref([]);
const options: Ref<Partial<ExportOptions>> = ref({ const options: Ref<Partial<ExportOptions>> = ref({
schema: selectedSchema.value, schema: selectedSchema.value,
includes: {} as {[key: string]: boolean}, includes: {} as Record<string, boolean>,
outputFormat: 'sql' as 'sql' | 'sql.zip', outputFormat: 'sql' as 'sql' | 'sql.zip',
sqlInsertAfter: 250, sqlInsertAfter: 250,
sqlInsertDivider: 'bytes' as 'bytes' | 'rows' sqlInsertDivider: 'bytes' as 'bytes' | 'rows'

View File

@@ -142,7 +142,7 @@ const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { trapRef } = useFocusTrap({ disableAutofocus: true }); const { trapRef } = useFocusTrap({ disableAutofocus: true });
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const localRow: Ref<{[key: string]: any}> = ref({}); const localRow: Ref<Record<string, any>> = ref({});
const fieldsToExclude = ref([]); const fieldsToExclude = ref([]);
const nInserts = ref(1); const nInserts = ref(1);
const isInserting = ref(false); const isInserting = ref(false);
@@ -225,7 +225,7 @@ const insertRows = async () => {
delete rowToInsert[key]; delete rowToInsert[key];
}); });
const fieldTypes: {[key: string]: string} = {}; const fieldTypes: Record<string, string> = {};
props.fields.forEach(field => { props.fields.forEach(field => {
fieldTypes[field.name] = field.type; fieldTypes[field.name] = field.type;
}); });
@@ -290,7 +290,7 @@ onMounted(() => {
} }
}, 50); }, 50);
const rowObj: {[key: string]: unknown} = {}; const rowObj: Record<string, unknown> = {};
if (!props.rowToDuplicate) { if (!props.rowToDuplicate) {
// Set default values // Set default values

View File

@@ -75,7 +75,7 @@
<code <code
class="cut-text" class="cut-text"
:title="query.sql" :title="query.sql"
v-html="highlightWord(query.sql)" v-html="highlight(highlightWord(query.sql), {html: true})"
/> />
</div> </div>
<div class="tile-bottom-content"> <div class="tile-bottom-content">
@@ -115,7 +115,7 @@
<BaseIcon icon-name="mdiHistory" :size="48" /> <BaseIcon icon-name="mdiHistory" :size="48" />
</div> </div>
<p class="empty-title h5"> <p class="empty-title h5">
{{ t('database.thereIsNoQueriesYet') }} {{ t('database.thereAreNoQueriesYet') }}
</p> </p>
</div> </div>
</div> </div>
@@ -126,13 +126,26 @@
<script setup lang="ts"> <script setup lang="ts">
import { ConnectionParams } from 'common/interfaces/antares'; import { ConnectionParams } from 'common/interfaces/antares';
import { Component, computed, ComputedRef, onBeforeUnmount, onMounted, onUpdated, Prop, Ref, ref, watch } from 'vue'; import { highlight } from 'sql-highlight';
import {
Component,
computed,
ComputedRef,
onBeforeUnmount,
onMounted,
onUpdated,
Prop,
Ref,
ref,
watch
} from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import BaseIcon from '@/components/BaseIcon.vue'; import BaseIcon from '@/components/BaseIcon.vue';
import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue'; import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
import { useFilters } from '@/composables/useFilters'; import { useFilters } from '@/composables/useFilters';
import { useFocusTrap } from '@/composables/useFocusTrap'; import { useFocusTrap } from '@/composables/useFocusTrap';
import { copyText } from '@/libs/copyText';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { HistoryRecord, useHistoryStore } from '@/stores/history'; import { HistoryRecord, useHistoryStore } from '@/stores/history';
@@ -162,7 +175,7 @@ const localSearchTerm = ref('');
const connectionName = computed(() => getConnectionName(props.connection.uid)); const connectionName = computed(() => getConnectionName(props.connection.uid));
const history: ComputedRef<HistoryRecord[]> = computed(() => (getHistoryByWorkspace(props.connection.uid) || [])); const history: ComputedRef<HistoryRecord[]> = computed(() => (getHistoryByWorkspace(props.connection.uid) || []));
const filteredHistory = computed(() => history.value.filter(q => q.sql.toLowerCase().search(searchTerm.value.toLowerCase()) >= 0)); const filteredHistory = computed(() => history.value.filter(q => q.sql.toLowerCase().search(localSearchTerm.value.toLowerCase()) >= 0));
watch(searchTerm, () => { watch(searchTerm, () => {
clearTimeout(searchTermInterval.value); clearTimeout(searchTermInterval.value);
@@ -173,7 +186,7 @@ watch(searchTerm, () => {
}); });
const copyQuery = (sql: string) => { const copyQuery = (sql: string) => {
navigator.clipboard.writeText(sql); copyText(sql);
}; };
const deleteQuery = (query: HistoryRecord[]) => { const deleteQuery = (query: HistoryRecord[]) => {

View File

@@ -0,0 +1,124 @@
<template>
<ConfirmModal
size="resize"
:disable-autofocus="true"
:close-on-confirm="!!localNote.note.length"
:confirm-text="t('general.save')"
@confirm="updateNote"
@hide="$emit('hide')"
>
<template #header>
<div class="d-flex">
<BaseIcon
icon-name="mdiNoteEditOutline"
class="mr-1"
:size="24"
/> {{ t('application.editNote') }}
</div>
</template>
<template #body>
<form class="form">
<div class="form-group columns">
<div class="column col-8">
<label class="form-label">{{ t('connection.connection') }}</label>
<BaseSelect
v-model="localNote.cUid"
class="form-select"
:options="connectionOptions"
option-track-by="code"
option-label="name"
@change="null"
/>
</div>
<div class="column col-4">
<label class="form-label">{{ t('application.tag') }}</label>
<BaseSelect
v-model="localNote.type"
class="form-select"
:options="noteTags"
option-track-by="code"
option-label="name"
@change="null"
/>
</div>
</div>
<div class="form-group">
<label class="form-label">{{ t('general.content') }} <small
v-if="localNote.type !== 'query'"
style="line-height: 1;"
class="text-gray"
>({{ t('application.markdownSupported') }})</small></label>
<BaseTextEditor
v-model="localNote.note"
:mode="editorMode"
:show-line-numbers="false"
:auto-focus="true"
:height="400"
:width="640"
:resizable="true"
/>
</div>
</form>
</template>
</ConfirmModal>
</template>
<script lang="ts" setup>
import { inject, onBeforeMount, PropType, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseIcon from '@/components/BaseIcon.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import BaseTextEditor from '@/components/BaseTextEditor.vue';
import { ConnectionNote, TagCode, useScratchpadStore } from '@/stores/scratchpad';
const { t } = useI18n();
const { editNote } = useScratchpadStore();
const emit = defineEmits(['hide']);
const props = defineProps({
note: {
type: Object as PropType<ConnectionNote>,
required: true
}
});
const noteTags = inject<{code: TagCode; name: string}[]>('noteTags');
const connectionOptions = inject<{code: string; name: string}[]>('connectionOptions');
const editorMode = ref('markdown');
const localNote: Ref<ConnectionNote> = ref({
uid: 'dummy',
cUid: null,
title: undefined,
note: '',
date: new Date(),
type: 'note',
isArchived: false
});
const updateNote = () => {
if (localNote.value.note) {
if (!localNote.value.title)// Set a default title
localNote.value.title = `${localNote.value.type.toLocaleUpperCase()}: ${localNote.value.uid}`;
localNote.value.date = new Date();
editNote(localNote.value);
emit('hide');
}
};
watch(() => localNote.value.type, () => {
if (localNote.value.type === 'query')
editorMode.value = 'sql';
else
editorMode.value = 'markdown';
});
onBeforeMount(() => {
localNote.value = JSON.parse(JSON.stringify(props.note));
});
</script>

View File

@@ -0,0 +1,122 @@
<template>
<ConfirmModal
size="resize"
:disable-autofocus="true"
:close-on-confirm="!!newNote.note.length"
:confirm-text="t('general.save')"
@confirm="createNote"
@hide="$emit('hide')"
>
<template #header>
<div class="d-flex">
<BaseIcon
icon-name="mdiNotePlusOutline"
class="mr-1"
:size="24"
/> {{ t('application.addNote') }}
</div>
</template>
<template #body>
<form class="form">
<div class="form-group columns">
<div class="column col-8">
<label class="form-label">{{ t('connection.connection') }}</label>
<BaseSelect
v-model="newNote.cUid"
class="form-select"
:options="connectionOptions"
option-track-by="code"
option-label="name"
@change="null"
/>
</div>
<div class="column col-4">
<label class="form-label">{{ t('application.tag') }}</label>
<BaseSelect
v-model="newNote.type"
class="form-select"
:options="noteTags"
option-track-by="code"
option-label="name"
@change="null"
/>
</div>
</div>
<div class="form-group">
<label class="form-label">{{ t('general.content') }} <small
v-if="newNote.type !== 'query'"
style="line-height: 1;"
class="text-gray"
>({{ t('application.markdownSupported') }})</small></label>
<BaseTextEditor
v-model="newNote.note"
:mode="editorMode"
:show-line-numbers="false"
:auto-focus="true"
:height="400"
:width="640"
:resizable="true"
/>
</div>
</form>
</template>
</ConfirmModal>
</template>
<script lang="ts" setup>
import { uidGen } from 'common/libs/uidGen';
import { inject, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseIcon from '@/components/BaseIcon.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import BaseTextEditor from '@/components/BaseTextEditor.vue';
import { ConnectionNote, TagCode, useScratchpadStore } from '@/stores/scratchpad';
const { t } = useI18n();
const { addNote } = useScratchpadStore();
const emit = defineEmits(['hide']);
const noteTags = inject<{code: TagCode; name: string}[]>('noteTags');
const selectedConnection = inject<Ref<null | string>>('selectedConnection');
const selectedTag = inject<Ref<TagCode>>('selectedTag');
const connectionOptions = inject<{code: string; name: string}[]>('connectionOptions');
const editorMode = ref('markdown');
const newNote: Ref<ConnectionNote> = ref({
uid: uidGen('N'),
cUid: null,
title: undefined,
note: '',
date: new Date(),
type: 'note',
isArchived: false
});
const createNote = () => {
if (newNote.value.note) {
if (!newNote.value.title)// Set a default title
newNote.value.title = `${newNote.value.type.toLocaleUpperCase()}: ${newNote.value.uid}`;
newNote.value.date = new Date();
addNote(newNote.value);
emit('hide');
}
};
watch(() => newNote.value.type, () => {
if (newNote.value.type === 'query')
editorMode.value = 'sql';
else
editorMode.value = 'markdown';
});
newNote.value.cUid = selectedConnection.value;
if (selectedTag.value !== 'all')
newNote.value.type = selectedTag.value;
</script>

View File

@@ -161,6 +161,7 @@ import ModalProcessesListContext from '@/components/ModalProcessesListContext.vu
import ModalProcessesListRow from '@/components/ModalProcessesListRow.vue'; import ModalProcessesListRow from '@/components/ModalProcessesListRow.vue';
import { useFocusTrap } from '@/composables/useFocusTrap'; import { useFocusTrap } from '@/composables/useFocusTrap';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { copyText } from '@/libs/copyText';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
@@ -322,13 +323,13 @@ const closeContext = () => {
const copyCell = () => { const copyCell = () => {
const row = results.value.find(row => Number(row.id) === selectedRow.value); const row = results.value.find(row => Number(row.id) === selectedRow.value);
const valueToCopy = row[selectedCell.value.field]; const valueToCopy = row[selectedCell.value.field];
navigator.clipboard.writeText(valueToCopy); copyText(valueToCopy);
}; };
const copyRow = () => { const copyRow = () => {
const row = results.value.find(row => Number(row.id) === selectedRow.value); const row = results.value.find(row => Number(row.id) === selectedRow.value);
const rowToCopy = JSON.parse(JSON.stringify(row)); const rowToCopy = JSON.parse(JSON.stringify(row));
navigator.clipboard.writeText(JSON.stringify(rowToCopy)); copyText(JSON.stringify(rowToCopy));
}; };
const closeModal = () => emit('close'); const closeModal = () => emit('close');

View File

@@ -67,7 +67,7 @@ const props = defineProps({
const emit = defineEmits(['select-row', 'contextmenu', 'stop-refresh']); const emit = defineEmits(['select-row', 'contextmenu', 'stop-refresh']);
const isInlineEditor: Ref<{[key: string]: boolean}> = ref({}); const isInlineEditor: Ref<Record<string, boolean>> = ref({});
const isInfoModal = ref(false); const isInfoModal = ref(false);
const editorMode = ref('sql'); const editorMode = ref('sql');

View File

@@ -166,19 +166,6 @@
</label> </label>
</div> </div>
</div> </div>
<div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12">
<label class="form-label">
{{ t('application.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="form-group column col-12">
<div class="col-5 col-sm-12"> <div class="col-5 col-sm-12">
<label class="form-label"> <label class="form-label">
@@ -422,14 +409,6 @@
class="d-inline mr-1" class="d-inline mr-1"
:size="16" :size="16"
/> Mastodon</a> <a /> Mastodon</a> <a
class="c-hand"
:style="'align-items: center; display: inline-flex;'"
@click="openOutside('https://twitter.com/AntaresSQL')"
><BaseIcon
icon-name="mdiTwitter"
class="d-inline mr-1"
:size="16"
/> Twitter</a> <a
class="c-hand" class="c-hand"
:style="'align-items: center; display: inline-flex;'" :style="'align-items: center; display: inline-flex;'"
@click="openOutside('https://antares-sql.app/')" @click="openOutside('https://antares-sql.app/')"
@@ -499,7 +478,6 @@ const {
restoreTabs, restoreTabs,
showTableSize, showTableSize,
disableBlur, disableBlur,
disableScratchpad,
applicationTheme, applicationTheme,
editorTheme, editorTheme,
editorFontSize editorFontSize
@@ -512,7 +490,6 @@ const {
changePageSize, changePageSize,
changeRestoreTabs, changeRestoreTabs,
changeDisableBlur, changeDisableBlur,
changeDisableScratchpad,
changeAutoComplete, changeAutoComplete,
changeLineWrap, changeLineWrap,
changeExecuteSelected, changeExecuteSelected,
@@ -671,10 +648,6 @@ const toggleDisableBlur = () => {
changeDisableBlur(!disableBlur.value); changeDisableBlur(!disableBlur.value);
}; };
const toggleDisableScratchpad = () => {
changeDisableScratchpad(!disableScratchpad.value);
};
const toggleAutoComplete = () => { const toggleAutoComplete = () => {
changeAutoComplete(!selectedAutoComplete.value); changeAutoComplete(!selectedAutoComplete.value);
}; };

View File

@@ -0,0 +1,352 @@
<template>
<div
class="tile my-2"
tabindex="0"
@click="$emit('select-note', note.uid)"
>
<BaseIcon
v-if="isBig"
class="tile-compress c-hand"
:icon-name="isSelected ? 'mdiChevronUp' : 'mdiChevronDown'"
:size="36"
@click.stop="$emit('toggle-note', note.uid)"
/>
<div class="tile-icon">
<BaseIcon
:icon-name="note.type === 'query'
? 'mdiStarOutline'
: note.type === 'todo'
? note.isArchived
? 'mdiCheckboxMarkedOutline'
: 'mdiCheckboxBlankOutline'
: 'mdiNoteEditOutline'"
:size="36"
/>
<div class="tile-icon-type">
{{ note.type }}
</div>
</div>
<div class="tile-content">
<div class="tile-content-message" :class="[{'opened': isSelected}]">
<code
v-if="note.type === 'query'"
ref="noteParagraph"
class="tile-paragraph sql"
v-html="highlight(highlightWord(note.note), {html: true})"
/>
<div
v-else
ref="noteParagraph"
class="tile-paragraph"
v-html="parseMarkdown(highlightWord(note.note))"
/>
<div v-if="isBig && !isSelected" class="tile-paragraph-overlay" />
</div>
<div class="tile-bottom-content">
<small class="tile-subtitle">{{ getConnectionName(note.cUid) || t('general.all') }} · {{ formatDate(note.date) }}</small>
<div class="tile-history-buttons">
<button
v-if="note.type === 'todo' && !note.isArchived"
class="btn btn-link pl-1"
@click.stop="$emit('archive-note', note.uid)"
>
<BaseIcon
icon-name="mdiCheck"
class="pr-1"
:size="22"
/> {{ t('general.archive') }}
</button>
<button
v-if="note.type === 'todo' && note.isArchived"
class="btn btn-link pl-1"
@click.stop="$emit('restore-note', note.uid)"
>
<BaseIcon
icon-name="mdiRestore"
class="pr-1"
:size="22"
/> {{ t('general.undo') }}
</button>
<button
v-if="note.type === 'query'"
class="btn btn-link pl-1"
@click.stop="$emit('select-query', note.note)"
>
<BaseIcon
icon-name="mdiOpenInApp"
class="pr-1"
:size="22"
/> {{ t('general.select') }}
</button>
<button
v-if="note.type === 'query'"
class="btn btn-link pl-1"
@click.stop="copyText(note.note)"
>
<BaseIcon
icon-name="mdiContentCopy"
class="pr-1"
:size="22"
/> {{ t('general.copy') }}
</button>
<button
v-if=" !note.isArchived"
class="btn btn-link pl-1"
@click.stop="$emit('edit-note')"
>
<BaseIcon
icon-name="mdiPencil"
class="pr-1"
:size="22"
/> {{ t('general.edit') }}
</button>
<button class="btn btn-link pl-1" @click.stop="$emit('delete-note', note.uid)">
<BaseIcon
icon-name="mdiDeleteForever"
class="pr-1"
:size="22"
/> {{ t('general.delete') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useElementBounding } from '@vueuse/core';
import { marked } from 'marked';
import { highlight } from 'sql-highlight';
import { computed, PropType, Ref, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import BaseIcon from '@/components/BaseIcon.vue';
import { useFilters } from '@/composables/useFilters';
import { copyText } from '@/libs/copyText';
import { useConnectionsStore } from '@/stores/connections';
import { ConnectionNote } from '@/stores/scratchpad';
const props = defineProps({
note: {
type: Object as PropType<ConnectionNote>,
required: true
},
searchTerm: {
type: String,
default: () => ''
},
selectedNote: {
type: String,
default: () => ''
}
});
const { t } = useI18n();
const { formatDate } = useFilters();
const { getConnectionName } = useConnectionsStore();
defineEmits([
'edit-note',
'delete-note',
'select-note',
'toggle-note',
'archive-note',
'restore-note',
'select-query'
]);
const noteParagraph: Ref<HTMLDivElement> = ref(null);
const noteHeight = ref(useElementBounding(noteParagraph)?.height);
const isSelected = computed(() => props.selectedNote === props.note.uid);
const isBig = computed(() => noteHeight.value > 75);
const parseMarkdown = (text: string) => {
const renderer = {
listitem (text: string) {
return `<li>${text.replace(/ *\([^)]*\) */g, '')}</li>`;
},
link (href: string, title: string, text: string) {
return `<a>${text}</a>`;
}
};
marked.use({ renderer });
return marked(text);
};
const highlightWord = (string: string) => {
string = string.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
if (props.searchTerm) {
const regexp = new RegExp(`(${props.searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return string.replace(regexp, '<span class="text-primary text-bold">$1</span>');
}
else
return string;
};
</script>
<style scoped lang="scss">
.tile {
border-radius: $border-radius;
display: flex;
position: relative;
transition: none;
&:hover,
&:focus {
.tile-content {
.tile-bottom-content {
.tile-history-buttons {
opacity: 1;
}
}
}
}
.tile-compress {
position: absolute;
right: 2px;
top: 0px;
opacity: .7;
z-index: 2;
}
.tile-icon {
font-size: 1.2rem;
margin-left: 0.3rem;
margin-right: 0.3rem;
margin-top: 0.6rem;
width: 40px;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
opacity: .8;
.tile-icon-type {
text-transform: uppercase;
font-size: .5rem;
}
}
.tile-content {
padding: 0.3rem;
padding-left: 0.1rem;
min-height: 75px;
display: flex;
flex-direction: column;
justify-content: space-between;
.tile-content-message{
position: relative;
&:not(.opened) {
max-height: 36px;
overflow: hidden;
}
.tile-paragraph-overlay {
height: 36px;
width: 100%;
position: absolute;
top: 0;
}
}
code, pre {
max-width: 100%;
width: 100%;
display: inline-block;
font-size: 100%;
opacity: 0.8;
font-weight: 600;
white-space: break-spaces;
}
.tile-subtitle {
opacity: 0.8;
}
.tile-bottom-content {
display: flex;
justify-content: space-between;
.tile-history-buttons {
opacity: 0;
transition: opacity 0.2s;
button {
font-size: 0.7rem;
height: 1rem;
line-height: 1rem;
display: inline-flex;
align-items: center;
justify-content: center;
}
}
}
}
}
.theme-dark {
.tile {
.tile-paragraph-overlay {
background-image: linear-gradient(
to bottom,
rgba(255,0,0,0) 70%,
$body-bg-dark);
}
&:focus {
.tile-paragraph-overlay {
background-image: linear-gradient(
to bottom,
rgba(255,0,0,0)70%,
#323232);
}
}
&:hover{
.tile-paragraph-overlay {
background-image: linear-gradient(
to bottom,
rgba(255,0,0,0) 70%,
$bg-color-light-dark);
}
}
}
}
.theme-light {
.tile {
.tile-paragraph-overlay {
background-image: linear-gradient(
to bottom,
rgba(255,0,0,0) 70%,
#FFFF);
}
&:hover,
&:focus {
.tile-paragraph-overlay {
background-image: linear-gradient(
to bottom,
rgba(255,0,0,0) 70%,
$bg-color-light-gray);
}
}
}
}
</style>
<style lang="scss">
.tile-paragraph {
white-space: initial;
word-break: break-word;
user-select: text;
h1, h2, h3, h4, h5, h6, p, li {
margin: 0;
}
}
</style>

View File

@@ -15,6 +15,64 @@
:size="18" :size="18"
/> {{ t('connection.disconnect') }}</span> /> {{ t('connection.disconnect') }}</span>
</div> </div>
<div v-if="!contextConnection.isFolder" class="context-element">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiFolderMove"
:size="18"
/> {{ t('general.moveTo') }}</span>
<BaseIcon
class="text-light ml-1"
icon-name="mdiChevronRight"
:size="18"
/>
<div class="context-submenu">
<div class="context-element" @click.stop="moveToFolder(null)">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiFolderPlus"
:size="18"
/> {{ t('application.newFolder') }}</span>
</div>
<div
v-for="folder in filteredFolders"
:key="folder.uid"
class="context-element"
@click.stop="moveToFolder(folder.uid)"
>
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiFolder"
:size="18"
:style="`color: ${folder.color}!important`"
/> {{ folder.name || t('general.folder') }}</span>
</div>
<div
v-if="isInFolder"
class="context-element"
@click="outOfFolder"
>
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiFolderOff"
:size="18"
/> {{ t('application.outOfFolder') }}</span>
</div>
</div>
</div>
<div class="context-element" @click.stop="showAppearanceModal">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiBrushVariant"
:size="18"
/> {{ t('application.appearance') }}</span>
</div>
<div <div
v-if="!contextConnection.isFolder" v-if="!contextConnection.isFolder"
class="context-element" class="context-element"
@@ -27,14 +85,6 @@
:size="18" :size="18"
/> {{ t('general.duplicate') }}</span> /> {{ t('general.duplicate') }}</span>
</div> </div>
<div class="context-element" @click.stop="showAppearanceModal">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiBrushVariant"
:size="18"
/> {{ t('application.appearance') }}</span>
</div>
<div class="context-element" @click="showConfirmModal"> <div class="context-element" @click="showConfirmModal">
<span class="d-flex"> <span class="d-flex">
<BaseIcon <BaseIcon
@@ -79,6 +129,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import { storeToRefs } from 'pinia';
import { computed, Prop, ref } from 'vue'; import { computed, Prop, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -98,9 +149,14 @@ const {
getConnectionByUid, getConnectionByUid,
getConnectionName, getConnectionName,
addConnection, addConnection,
deleteConnection deleteConnection,
addFolder,
addToFolder,
removeFromFolders
} = connectionsStore; } = connectionsStore;
const { getFolders: folders } = storeToRefs(connectionsStore);
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const { const {
@@ -121,6 +177,8 @@ const isConnectionEdit = ref(false);
const connectionName = computed(() => props.contextConnection.name || getConnectionName(props.contextConnection.uid) || t('general.folder', 1)); const connectionName = computed(() => props.contextConnection.name || getConnectionName(props.contextConnection.uid) || t('general.folder', 1));
const isConnected = computed(() => getWorkspace(props.contextConnection.uid)?.connectionStatus === 'connected'); const isConnected = computed(() => getWorkspace(props.contextConnection.uid)?.connectionStatus === 'connected');
const filteredFolders = computed(() => folders.value.filter(f => !f.connections.includes(props.contextConnection.uid)));
const isInFolder = computed(() => folders.value.some(f => f.connections.includes(props.contextConnection.uid)));
const confirmDeleteConnection = () => { const confirmDeleteConnection = () => {
if (isConnected.value) if (isConnected.value)
@@ -129,6 +187,27 @@ const confirmDeleteConnection = () => {
closeContext(); closeContext();
}; };
const moveToFolder = (folderUid?: string) => {
if (!folderUid) {
addFolder({
connections: [props.contextConnection.uid]
});
}
else {
addToFolder({
folder: folderUid,
connection: props.contextConnection.uid
});
}
closeContext();
};
const outOfFolder = () => {
removeFromFolders(props.contextConnection.uid);
closeContext();
};
const duplicateConnection = () => { const duplicateConnection = () => {
let connectionCopy = getConnectionByUid(props.contextConnection.uid); let connectionCopy = getConnectionByUid(props.contextConnection.uid);
connectionCopy = { connectionCopy = {

View File

@@ -32,7 +32,7 @@ const { removeNotification } = notificationsStore;
const { notifications } = storeToRefs(notificationsStore); const { notifications } = storeToRefs(notificationsStore);
const { notificationsTimeout } = storeToRefs(settingsStore); const { notificationsTimeout } = storeToRefs(settingsStore);
const timeouts: Ref<{[key: string]: NodeJS.Timeout}> = ref({}); const timeouts: Ref<Record<string, NodeJS.Timeout>> = ref({});
const latestNotifications = computed(() => notifications.value.slice(0, 10)); const latestNotifications = computed(() => notifications.value.slice(0, 10));

View File

@@ -1,66 +1,368 @@
<template> <template>
<ConfirmModal <Teleport to="#window-content">
:confirm-text="t('application.update')" <div class="modal active">
:cancel-text="t('general.close')" <a class="modal-overlay" @click.stop="hideScratchpad" />
size="large" <div ref="trapRef" class="modal-container p-0 pb-4">
:hide-footer="true" <div class="modal-header pl-2">
@hide="hideScratchpad" <div class="modal-title h6">
> <div class="d-flex">
<template #header> <BaseIcon
<div class="d-flex"> icon-name="mdiNotebookOutline"
<BaseIcon class="mr-1"
icon-name="mdiNotebookEditOutline" :size="24"
class="mr-1" />
:size="24" <span>{{ t('application.note', 2) }}</span>
/> {{ t('application.scratchpad') }} </div>
</div> </div>
</template> <a class="btn btn-clear c-hand" @click.stop="hideScratchpad" />
<template #body> </div>
<div> <div class="modal-body p-0 workspace-query-results">
<div> <div
<TextEditor ref="noteFilters"
v-model="localNotes" class="d-flex p-vcentered p-2"
editor-class="textarea-editor" style="gap: 0 10px"
mode="markdown" >
:auto-focus="true" <div style="flex: 1;">
:show-line-numbers="false" <BaseSelect
/> v-model="localConnection"
class="form-select"
:options="connectionOptions"
option-track-by="code"
option-label="name"
@change="null"
/>
</div>
<div class="btn-group btn-group-block text-uppercase">
<div
v-for="tag in [{ code: 'all', name: t('general.all') }, ...noteTags]"
:key="tag.code"
class="btn"
:class="[selectedTag === tag.code ? 'btn-primary': 'btn-dark']"
@click="setTag(tag.code)"
>
{{ tag.name }}
</div>
</div>
<div class="">
<div
class="btn px-1 tooltip tooltip-left s-rounded archived-button"
:class="[showArchived ? 'btn-primary' : 'btn-link']"
:data-tooltip="showArchived ? t('application.hideArchivedNotes') : t('application.showArchivedNotes')"
@click="showArchived = !showArchived"
>
<BaseIcon
:icon-name="!showArchived ? 'mdiArchiveEyeOutline' : 'mdiArchiveCancelOutline'"
class=""
:size="24"
/>
</div>
</div>
</div>
<div>
<div
v-show="filteredNotes.length || searchTerm.length"
ref="searchForm"
class="form-group has-icon-right m-0 p-2"
>
<input
v-model="searchTerm"
class="form-input"
type="text"
:placeholder="t('general.search')"
>
<BaseIcon
v-if="!searchTerm"
icon-name="mdiMagnify"
class="form-icon pr-2"
:size="18"
/>
<BaseIcon
v-else
icon-name="mdiBackspace"
class="form-icon c-hand pr-2"
:size="18"
@click="searchTerm = ''"
/>
</div>
</div>
<div
v-if="filteredNotes.length"
ref="tableWrapper"
class="vscroll px-2"
:style="{'height': resultsSize+'px'}"
>
<div ref="table">
<BaseVirtualScroll
ref="resultTable"
:items="filteredNotes"
:item-height="83"
:visible-height="resultsSize"
:scroll-element="scrollElement"
>
<template #default="{ items }">
<ScratchpadNote
v-for="note in items"
:key="note.uid"
:search-term="searchTerm"
:note="note"
:selected-note="selectedNote"
@select-note="selectedNote = note.uid"
@toggle-note="toggleNote"
@edit-note="startEditNote(note)"
@delete-note="deleteNote"
@archive-note="archiveNote"
@restore-note="restoreNote"
@select-query="selectQuery"
/>
</template>
</BaseVirtualScroll>
</div>
</div>
<div v-else class="empty">
<div class="empty-icon">
<BaseIcon icon-name="mdiNoteSearch" :size="48" />
</div>
<p class="empty-title h5">
{{ t('application.thereAreNoNotesYet') }}
</p>
</div>
<div
class="btn btn-primary p-0 add-button tooltip tooltip-left"
:data-tooltip="t('application.addNote')"
@click="isAddModal = true"
>
<BaseIcon
icon-name="mdiPlus"
:size="48"
/>
</div>
</div> </div>
<small class="text-gray">{{ t('application.markdownSupported') }}</small>
</div> </div>
</template> </div>
</ConfirmModal> </Teleport>
<ModalNoteNew v-if="isAddModal" @hide="isAddModal = false" />
<ModalNoteEdit
v-if="isEditModal"
:note="noteToEdit"
@hide="closeEditModal"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { Ref, ref, watch } from 'vue'; import {
Component,
computed,
ComputedRef,
onBeforeUnmount,
onMounted,
onUpdated,
provide,
Ref,
ref,
watch
} from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseIcon from '@/components/BaseIcon.vue'; import BaseIcon from '@/components/BaseIcon.vue';
import TextEditor from '@/components/BaseTextEditor.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
import ModalNoteEdit from '@/components/ModalNoteEdit.vue';
import ModalNoteNew from '@/components/ModalNoteNew.vue';
import ScratchpadNote from '@/components/ScratchpadNote.vue';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { useScratchpadStore } from '@/stores/scratchpad'; import { useConnectionsStore } from '@/stores/connections';
import { ConnectionNote, TagCode, useScratchpadStore } from '@/stores/scratchpad';
import { useWorkspacesStore } from '@/stores/workspaces';
const { t } = useI18n(); const { t } = useI18n();
const applicationStore = useApplicationStore(); const applicationStore = useApplicationStore();
const scratchpadStore = useScratchpadStore(); const scratchpadStore = useScratchpadStore();
const workspacesStore = useWorkspacesStore();
const { notes } = storeToRefs(scratchpadStore); const { connectionNotes, selectedTag } = storeToRefs(scratchpadStore);
const { changeNotes } = scratchpadStore; const { changeNotes } = scratchpadStore;
const { hideScratchpad } = applicationStore; const { hideScratchpad } = applicationStore;
const { getConnectionName } = useConnectionsStore();
const { connections } = storeToRefs(useConnectionsStore());
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspaceTab, getWorkspace, newTab, updateTabContent } = workspacesStore;
const localNotes = ref(notes.value); const localConnection = ref(null);
const debounceTimeout: Ref<NodeJS.Timeout> = ref(null); const table: Ref<HTMLDivElement> = ref(null);
const resultTable: Ref<Component & { updateWindow: () => void }> = ref(null);
const tableWrapper: Ref<HTMLDivElement> = ref(null);
const noteFilters: Ref<HTMLInputElement> = ref(null);
const searchForm: Ref<HTMLInputElement> = ref(null);
const resultsSize = ref(1000);
const searchTermInterval: Ref<NodeJS.Timeout> = ref(null);
const scrollElement: Ref<HTMLDivElement> = ref(null);
const searchTerm = ref('');
const localSearchTerm = ref('');
const showArchived = ref(false);
const isAddModal = ref(false);
const isEditModal = ref(false);
const noteToEdit: Ref<ConnectionNote> = ref(null);
const selectedNote = ref(null);
watch(localNotes, () => { const noteTags: ComputedRef<{code: TagCode; name: string}[]> = computed(() => [
clearTimeout(debounceTimeout.value); { code: 'note', name: t('application.note') },
{ code: 'todo', name: 'TODO' },
{ code: 'query', name: 'Query' }
]);
const filteredNotes = computed(() => connectionNotes.value.filter(n => (
(n.type === selectedTag.value || selectedTag.value === 'all') &&
(n.cUid === localConnection.value || localConnection.value === null) &&
(!n.isArchived || showArchived.value) &&
(n.note.toLowerCase().search(localSearchTerm.value.toLowerCase()) >= 0)
)));
const connectionOptions = computed(() => {
return [
{ code: null, name: t('general.all') },
...connections.value.map(c => ({ code: c.uid, name: getConnectionName(c.uid) }))
];
});
debounceTimeout.value = setTimeout(() => { provide('noteTags', noteTags);
changeNotes(localNotes.value); provide('connectionOptions', connectionOptions);
provide('selectedConnection', localConnection);
provide('selectedTag', selectedTag);
const resizeResults = () => {
if (resultTable.value) {
const el = tableWrapper.value.parentElement;
if (el)
resultsSize.value = el.offsetHeight - searchForm.value.offsetHeight - noteFilters.value.offsetHeight;
resultTable.value.updateWindow();
}
};
const refreshScroller = () => resizeResults();
const setTag = (tag: string) => {
selectedTag.value = tag;
};
const toggleNote = (uid: string) => {
selectedNote.value = selectedNote.value !== uid ? uid : null;
};
const startEditNote = (note: ConnectionNote) => {
isEditModal.value = true;
noteToEdit.value = note;
};
const archiveNote = (uid: string) => {
const remappedNotes = connectionNotes.value.map(n => {
if (n.uid === uid)
n.isArchived = true;
return n;
});
changeNotes(remappedNotes);
};
const restoreNote = (uid: string) => {
const remappedNotes = connectionNotes.value.map(n => {
if (n.uid === uid)
n.isArchived = false;
return n;
});
changeNotes(remappedNotes);
};
const deleteNote = (uid: string) => {
const otherNotes = connectionNotes.value.filter(n => n.uid !== uid);
changeNotes(otherNotes);
};
const selectQuery = (query: string) => {
const workspace = getWorkspace(selectedWorkspace.value);
const selectedTab = getWorkspaceTab(workspace.selectedTab);
if (workspace.connectionStatus !== 'connected') return;
if (selectedTab.type === 'query') {
updateTabContent({
tab: selectedTab.uid,
uid: selectedWorkspace.value,
type: 'query',
content: query,
schema: workspace.breadcrumbs.schema
});
}
else {
newTab({
uid: selectedWorkspace.value,
type: 'query',
content: query,
autorun: false,
schema: workspace.breadcrumbs.schema
});
}
hideScratchpad();
};
const closeEditModal = () => {
isEditModal.value = false;
noteToEdit.value = null;
};
watch(searchTerm, () => {
clearTimeout(searchTermInterval.value);
searchTermInterval.value = setTimeout(() => {
localSearchTerm.value = searchTerm.value;
}, 200); }, 200);
}); });
onUpdated(() => {
if (table.value)
refreshScroller();
if (tableWrapper.value)
scrollElement.value = tableWrapper.value;
});
onMounted(() => {
resizeResults();
window.addEventListener('resize', resizeResults);
if (selectedWorkspace.value && selectedWorkspace.value !== 'NEW')
localConnection.value = selectedWorkspace.value;
});
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeResults);
clearInterval(searchTermInterval.value);
});
</script> </script>
<style lang="scss" scoped>
.vscroll {
height: 1000px;
overflow: auto;
overflow-anchor: none;
}
.add-button{
border: none;
height: 48px;
width: 48px;
border-radius: 50%;
position: fixed;
margin-top: -40px;
margin-left: 580px;
z-index: 9;
}
.archived-button {
border-radius: 50%;
width: 36px;
height: 36px;
}
</style>

View File

@@ -59,17 +59,16 @@
<div class="settingbar-bottom-elements"> <div class="settingbar-bottom-elements">
<ul class="settingbar-elements"> <ul class="settingbar-elements">
<li <li
v-if="!disableScratchpad"
v-tooltip="{ v-tooltip="{
strategy: 'fixed', strategy: 'fixed',
placement: 'right', placement: 'right',
content: t('application.scratchpad') content: t('application.note', 2)
}" }"
class="settingbar-element btn btn-link" class="settingbar-element btn btn-link"
@click="showScratchpad" @click="showScratchpad()"
> >
<BaseIcon <BaseIcon
icon-name="mdiNotebookEditOutline" icon-name="mdiNotebookOutline"
class="settingbar-element-icon text-light" class="settingbar-element-icon text-light"
:size="24" :size="24"
/> />
@@ -84,12 +83,15 @@
@click="showSettingModal('general')" @click="showSettingModal('general')"
> >
<div class="settingbar-element-icon-wrapper"> <div class="settingbar-element-icon-wrapper">
<BaseIcon <div
icon-name="mdiCog"
class="settingbar-element-icon text-light" class="settingbar-element-icon text-light"
:class="{ 'badge badge-update': hasUpdates }" :class="{ 'badge badge-update': hasUpdates }"
:size="24" >
/> <BaseIcon
icon-name="mdiCog"
:size="24"
/>
</div>
</div> </div>
</li> </li>
</ul> </ul>
@@ -108,7 +110,6 @@ import SettingBarConnections from '@/components/SettingBarConnections.vue';
import SettingBarContext from '@/components/SettingBarContext.vue'; import SettingBarContext from '@/components/SettingBarContext.vue';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { SidebarElement, useConnectionsStore } from '@/stores/connections'; import { SidebarElement, useConnectionsStore } from '@/stores/connections';
import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
const { t } = useI18n(); const { t } = useI18n();
@@ -117,12 +118,10 @@ localStorage.setItem('opened-folders', '[]');
const applicationStore = useApplicationStore(); const applicationStore = useApplicationStore();
const connectionsStore = useConnectionsStore(); const connectionsStore = useConnectionsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const settingsStore = useSettingsStore();
const { updateStatus } = storeToRefs(applicationStore); const { updateStatus } = storeToRefs(applicationStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { connectionsOrder } = storeToRefs(connectionsStore); const { connectionsOrder } = storeToRefs(connectionsStore);
const { disableScratchpad } = storeToRefs(settingsStore);
const { showSettingModal, showScratchpad } = applicationStore; const { showSettingModal, showScratchpad } = applicationStore;
const { updateConnectionsOrder, initConnectionsOrder } = connectionsStore; const { updateConnectionsOrder, initConnectionsOrder } = connectionsStore;
@@ -187,7 +186,7 @@ if (!connectionsArr.value.length)
.settingbar-top-elements { .settingbar-top-elements {
overflow-x: hidden; overflow-x: hidden;
overflow-y: overlay; overflow-y: overlay;
// max-height: calc((100vh - 3.5rem) - #{$excluding-size}); width: 100%;
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 3px; width: 3px;
@@ -266,7 +265,7 @@ if (!connectionsArr.value.length)
.settingbar-element-icon { .settingbar-element-icon {
&.badge::after { &.badge::after {
top: 10px; top: 10px;
right: -6px; right: -3px;
position: absolute; position: absolute;
} }

View File

@@ -21,7 +21,7 @@
class="titlebar-element" class="titlebar-element"
@click="openDevTools" @click="openDevTools"
> >
<BaseIcon icon-name="mdiCodeTags" :size="24" /> <BaseIcon icon-name="mdiBugPlayOutline" :size="24" />
</div> </div>
<div <div
v-if="isDevelopment" v-if="isDevelopment"

View File

@@ -695,7 +695,7 @@ const openAsPermanentTab = (tab: WorkspaceTab) => {
routine: 'routine-props', routine: 'routine-props',
procedure: 'routine-props', procedure: 'routine-props',
scheduler: 'scheduler-props' scheduler: 'scheduler-props'
} as {[key: string]: string}; } as Record<string, string>;
newTab({ newTab({
uid: props.connection.uid, uid: props.connection.uid,

View File

@@ -163,22 +163,30 @@
> >
</div> </div>
</div> </div>
<div v-if="clientCustomizations.readOnlyMode" class="form-group columns"> <div v-if="clientCustomizations.readOnlyMode" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" /> <div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12"> <div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline my-0">
<input v-model="connection.readonly" type="checkbox"><i class="form-icon" /> {{ t('connection.readOnlyMode') }} <input v-model="connection.readonly" type="checkbox"><i class="form-icon" /> {{ t('connection.readOnlyMode') }}
</label> </label>
</div> </div>
</div> </div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" /> <div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12"> <div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline my-0">
<input v-model="connection.ask" type="checkbox"><i class="form-icon" /> {{ t('connection.askCredentials') }} <input v-model="connection.ask" type="checkbox"><i class="form-icon" /> {{ t('connection.askCredentials') }}
</label> </label>
</div> </div>
</div> </div>
<div v-if="clientCustomizations.singleConnectionMode" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline my-0">
<input v-model="connection.singleConnectionMode" type="checkbox"><i class="form-icon" /> {{ t('connection.singleConnection') }}
</label>
</div>
</div>
</fieldset> </fieldset>
</form> </form>
</div> </div>
@@ -572,11 +580,11 @@ const pathSelection = (event: Event & {target: {files: {path: string}[]}}, name:
const { files } = event.target; const { files } = event.target;
if (!files.length) return; if (!files.length) return;
(connection.value as unknown as {[key: string]: string})[name] = files[0].path as string; (connection.value as unknown as Record<string, string>)[name] = files[0].path as string;
}; };
const pathClear = (name: keyof ConnectionParams) => { const pathClear = (name: keyof ConnectionParams) => {
(connection.value as unknown as {[key: string]: string})[name] = ''; (connection.value as unknown as Record<string, string>)[name] = '';
}; };
setDefaults(); setDefaults();

View File

@@ -165,22 +165,30 @@
> >
</div> </div>
</div> </div>
<div v-if="clientCustomizations.readOnlyMode" class="form-group columns"> <div v-if="clientCustomizations.readOnlyMode" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" /> <div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12"> <div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline my-0">
<input v-model="localConnection.readonly" type="checkbox"><i class="form-icon" /> {{ t('connection.readOnlyMode') }} <input v-model="localConnection.readonly" type="checkbox"><i class="form-icon" /> {{ t('connection.readOnlyMode') }}
</label> </label>
</div> </div>
</div> </div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" /> <div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12"> <div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline my-0">
<input v-model="localConnection.ask" type="checkbox"><i class="form-icon" /> {{ t('connection.askCredentials') }} <input v-model="localConnection.ask" type="checkbox"><i class="form-icon" /> {{ t('connection.askCredentials') }}
</label> </label>
</div> </div>
</div> </div>
<div v-if="clientCustomizations.singleConnectionMode" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline my-0">
<input v-model="localConnection.singleConnectionMode" type="checkbox"><i class="form-icon" /> {{ t('connection.singleConnection') }}
</label>
</div>
</div>
</fieldset> </fieldset>
</form> </form>
</div> </div>
@@ -569,11 +577,11 @@ const pathSelection = (event: Event & {target: {files: {path: string}[]}}, name:
const { files } = event.target; const { files } = event.target;
if (!files.length) return; if (!files.length) return;
(localConnection.value as unknown as {[key: string]: string})[name] = files[0].path; (localConnection.value as unknown as Record<string, string>)[name] = files[0].path;
}; };
const pathClear = (name: keyof ConnectionParams) => { const pathClear = (name: keyof ConnectionParams) => {
(localConnection.value as unknown as {[key: string]: string})[name] = ''; (localConnection.value as unknown as Record<string, string>)[name] = '';
}; };
localConnection.value = JSON.parse(JSON.stringify(props.connection)); localConnection.value = JSON.parse(JSON.stringify(props.connection));

View File

@@ -34,7 +34,6 @@
</div> </div>
<div :title="t('general.refresh')"> <div :title="t('general.refresh')">
<BaseIcon <BaseIcon
v-if="customizations.schemas"
icon-name="mdiRefresh" icon-name="mdiRefresh"
:size="18" :size="18"
class="c-hand mr-2" class="c-hand mr-2"

View File

@@ -55,6 +55,14 @@
/> {{ t('general.disable') }} /> {{ t('general.disable') }}
</span> </span>
</div> </div>
<div class="context-element" @click="copyName(selectedMisc.name)">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiContentCopy"
:size="18"
/> {{ t('general.copyName') }}</span>
</div>
<div class="context-element" @click="showDeleteModal"> <div class="context-element" @click="showDeleteModal">
<span class="d-flex"> <span class="d-flex">
<BaseIcon <BaseIcon
@@ -108,6 +116,7 @@ import Functions from '@/ipc-api/Functions';
import Routines from '@/ipc-api/Routines'; import Routines from '@/ipc-api/Routines';
import Schedulers from '@/ipc-api/Schedulers'; import Schedulers from '@/ipc-api/Schedulers';
import Triggers from '@/ipc-api/Triggers'; import Triggers from '@/ipc-api/Triggers';
import { copyText } from '@/libs/copyText';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
@@ -163,6 +172,11 @@ const deleteMessage = computed(() => {
} }
}); });
const copyName = (name: string) => {
copyText(name);
closeContext();
};
const showDeleteModal = () => { const showDeleteModal = () => {
isDeleteModal.value = true; isDeleteModal.value = true;
}; };

View File

@@ -102,6 +102,14 @@
</div> </div>
</div> </div>
</div> </div>
<div class="context-element" @click="copyName(selectedSchema)">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiContentCopy"
:size="18"
/> {{ t('general.copyName') }}</span>
</div>
<div <div
v-if="workspace.customizations.schemaExport" v-if="workspace.customizations.schemaExport"
class="context-element" class="context-element"
@@ -198,6 +206,7 @@ import ModalEditSchema from '@/components/ModalEditSchema.vue';
import ModalImportSchema from '@/components/ModalImportSchema.vue'; import ModalImportSchema from '@/components/ModalImportSchema.vue';
import Application from '@/ipc-api/Application'; import Application from '@/ipc-api/Application';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { copyText } from '@/libs/copyText';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useSchemaExportStore } from '@/stores/schemaExport'; import { useSchemaExportStore } from '@/stores/schemaExport';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
@@ -268,6 +277,11 @@ const openCreateSchedulerTab = () => {
emit('open-create-scheduler-tab'); emit('open-create-scheduler-tab');
}; };
const copyName = (name: string) => {
copyText(name);
closeContext();
};
const showDeleteModal = () => { const showDeleteModal = () => {
isDeleteModal.value = true; isDeleteModal.value = true;
}; };

View File

@@ -15,6 +15,14 @@
:size="18" :size="18"
/> {{ t('application.settings') }}</span> /> {{ t('application.settings') }}</span>
</div> </div>
<div class="context-element" @click="copyName(selectedTable.name)">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiContentCopy"
:size="18"
/> {{ t('general.copyName') }}</span>
</div>
<div <div
v-if="selectedTable && selectedTable.type === 'table' && customizations.schemaExport" v-if="selectedTable && selectedTable.type === 'table' && customizations.schemaExport"
class="context-element" class="context-element"
@@ -130,6 +138,7 @@ import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseContextMenu from '@/components/BaseContextMenu.vue'; import BaseContextMenu from '@/components/BaseContextMenu.vue';
import BaseIcon from '@/components/BaseIcon.vue'; import BaseIcon from '@/components/BaseIcon.vue';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import { copyText } from '@/libs/copyText';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useSchemaExportStore } from '@/stores/schemaExport'; import { useSchemaExportStore } from '@/stores/schemaExport';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
@@ -170,6 +179,11 @@ const showTableExportModal = () => {
closeContext(); closeContext();
}; };
const copyName = (name: string) => {
copyText(name);
closeContext();
};
const showDeleteModal = () => { const showDeleteModal = () => {
isDeleteModal.value = true; isDeleteModal.value = true;
}; };

View File

@@ -24,7 +24,7 @@
tabindex="0" tabindex="0"
@contextmenu.prevent="contextMenu($event, wLog)" @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> <span class="type-datetime">{{ moment(wLog.date).format('HH:mm:ss') }}</span>: <code class="query-console-log-sql" v-html="highlight(wLog.sql, {html: true})" />
</div> </div>
</div> </div>
</div> </div>
@@ -47,11 +47,13 @@
<script setup lang="ts"> <script setup lang="ts">
import * as moment from 'moment'; import * as moment from 'moment';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { highlight } from 'sql-highlight';
import { computed, nextTick, onMounted, Ref, ref, watch } from 'vue'; import { computed, nextTick, onMounted, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import BaseContextMenu from '@/components/BaseContextMenu.vue'; import BaseContextMenu from '@/components/BaseContextMenu.vue';
import BaseIcon from '@/components/BaseIcon.vue'; import BaseIcon from '@/components/BaseIcon.vue';
import { copyText } from '@/libs/copyText';
import { useConsoleStore } from '@/stores/console'; import { useConsoleStore } from '@/stores/console';
const { t } = useI18n(); const { t } = useI18n();
@@ -100,7 +102,7 @@ const contextMenu = (event: MouseEvent, wLog: {date: Date; sql: string}) => {
}; };
const copyQuery = () => { const copyQuery = () => {
navigator.clipboard.writeText(contextQuery.value); copyText(contextQuery.value);
isContext.value = false; isContext.value = false;
}; };

View File

@@ -382,7 +382,7 @@ const getFieldsData = async () => {
if (status === 'success') { if (status === 'success') {
const indexesObj = response const indexesObj = response
.filter((index: TableIndex) => index.type !== 'FOREIGN KEY') .filter((index: TableIndex) => index.type !== 'FOREIGN KEY')
.reduce((acc: {[key: string]: TableIndex[]}, curr: TableIndex) => { .reduce((acc: Record<string, TableIndex[]>, curr: TableIndex) => {
acc[curr.name] = acc[curr.name] || []; acc[curr.name] = acc[curr.name] || [];
acc[curr.name].push(curr); acc[curr.name].push(curr);
return acc; return acc;

View File

@@ -101,7 +101,13 @@ const props = defineProps({
selectedField: Object selectedField: Object
}); });
const emit = defineEmits(['close-context', 'duplicate-selected', 'delete-selected', 'add-new-index', 'add-to-index']); const emit = defineEmits([
'close-context',
'duplicate-selected',
'delete-selected',
'add-new-index',
'add-to-index'
]);
const hasPrimary = computed(() => props.indexes.some(index => index.type === 'PRIMARY')); const hasPrimary = computed(() => props.indexes.some(index => index.type === 'PRIMARY'));

View File

@@ -150,7 +150,13 @@ const props = defineProps({
mode: String mode: String
}); });
const emit = defineEmits(['add-new-index', 'add-to-index', 'rename-field', 'duplicate-field', 'remove-field']); const emit = defineEmits([
'add-new-index',
'add-to-index',
'rename-field',
'duplicate-field',
'remove-field'
]);
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const consoleStore = useConsoleStore(); const consoleStore = useConsoleStore();

View File

@@ -258,7 +258,7 @@ const indexesPanel: Ref<HTMLDivElement> = ref(null);
const foreignProxy = ref([]); const foreignProxy = ref([]);
const selectedForeignID = ref(''); const selectedForeignID = ref('');
const modalInnerHeight = ref(400); const modalInnerHeight = ref(400);
const refFields = ref({} as {[key: string]: TableField[]}); const refFields = ref({} as Record<string, TableField[]>);
const foreignActions = computed(() => props.workspace.customizations.foreignActions); const foreignActions = computed(() => props.workspace.customizations.foreignActions);
const selectedForeignObj = computed(() => foreignProxy.value.find(foreign => foreign._antares_id === selectedForeignID.value)); const selectedForeignObj = computed(() => foreignProxy.value.find(foreign => foreign._antares_id === selectedForeignID.value));

View File

@@ -89,27 +89,37 @@
<button <button
class="btn btn-dark btn-sm" class="btn btn-dark btn-sm"
:disabled="!query || isQuering" :disabled="!query || isQuering"
:title="t('general.format')"
@click="beautify()" @click="beautify()"
> >
<BaseIcon <BaseIcon icon-name="mdiBrush" :size="24" />
class="mr-1"
icon-name="mdiBrush"
:size="24"
/>
<span>{{ t('general.format') }}</span>
</button> </button>
<button <button
class="btn btn-dark btn-sm" class="btn btn-dark btn-sm"
:disabled="isQuering" :disabled="isQuering"
:title="t('general.history')"
@click="openHistoryModal()" @click="openHistoryModal()"
> >
<BaseIcon <BaseIcon icon-name="mdiHistory" :size="24" />
class="mr-1"
icon-name="mdiHistory"
:size="24"
/>
<span>{{ t('general.history') }}</span>
</button> </button>
<div class="btn-group">
<button
class="btn btn-dark btn-sm mr-0"
:disabled="isQuering || (isQuerySaved || query.length < 5)"
:title="t('general.save')"
@click="saveQuery()"
>
<BaseIcon icon-name="mdiContentSaveOutline" :size="24" />
</button>
<button
class="btn btn-dark btn-sm"
:disabled="isQuering"
:title="t('database.savedQueries')"
@click="openSavedModal()"
>
<BaseIcon icon-name="mdiStarOutline" :size="24" />
</button>
</div>
<div class="dropdown table-dropdown pr-2"> <div class="dropdown table-dropdown pr-2">
<button <button
:disabled="!hasResults || isQuering" :disabled="!hasResults || isQuering"
@@ -237,6 +247,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Ace } from 'ace-builds'; import { Ace } from 'ace-builds';
import { ConnectionParams } from 'common/interfaces/antares'; import { ConnectionParams } from 'common/interfaces/antares';
import { uidGen } from 'common/libs/uidGen';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { format } from 'sql-formatter'; import { format } from 'sql-formatter';
@@ -252,9 +263,11 @@ import WorkspaceTabQueryEmptyState from '@/components/WorkspaceTabQueryEmptyStat
import WorkspaceTabQueryTable from '@/components/WorkspaceTabQueryTable.vue'; import WorkspaceTabQueryTable from '@/components/WorkspaceTabQueryTable.vue';
import { useResultTables } from '@/composables/useResultTables'; import { useResultTables } from '@/composables/useResultTables';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { useApplicationStore } from '@/stores/application';
import { useConsoleStore } from '@/stores/console'; import { useConsoleStore } from '@/stores/console';
import { useHistoryStore } from '@/stores/history'; import { useHistoryStore } from '@/stores/history';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useScratchpadStore } from '@/stores/scratchpad';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
@@ -279,6 +292,8 @@ const {
const { saveHistory } = useHistoryStore(); const { saveHistory } = useHistoryStore();
const { addNotification } = useNotificationsStore(); const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const { showScratchpad } = useApplicationStore();
const { addNote } = useScratchpadStore();
const { consoleHeight } = storeToRefs(useConsoleStore()); const { consoleHeight } = storeToRefs(useConsoleStore());
const { executeSelected } = storeToRefs(useSettingsStore()); const { executeSelected } = storeToRefs(useSettingsStore());
@@ -304,6 +319,7 @@ const resultsCount = ref(0);
const durationsCount = ref(0); const durationsCount = ref(0);
const affectedCount = ref(null); const affectedCount = ref(null);
const editorHeight = ref(200); const editorHeight = ref(200);
const isQuerySaved = ref(false);
const isHistoryOpen = ref(false); const isHistoryOpen = ref(false);
const debounceTimeout = ref(null); const debounceTimeout = ref(null);
@@ -329,6 +345,8 @@ watch(query, (val) => {
schema: selectedSchema.value, schema: selectedSchema.value,
content: val content: val
}); });
isQuerySaved.value = false;
}, 200); }, 200);
}); });
@@ -351,6 +369,14 @@ watch(databaseSchemas, () => {
selectedSchema.value = null; selectedSchema.value = null;
}, { deep: true }); }, { deep: true });
watch(() => props.tab.content, () => {
query.value = props.tab.content;
const editorValue = queryEditor.value.editor.session.getValue();
if (editorValue !== query.value)// If change not rendered in editor
queryEditor.value.editor.session.setValue(query.value);
});
const runQuery = async (query: string) => { const runQuery = async (query: string) => {
if (!query || isQuering.value) return; if (!query || isQuering.value) return;
isQuering.value = true; isQuering.value = true;
@@ -496,6 +522,22 @@ const openHistoryModal = () => {
isHistoryOpen.value = true; isHistoryOpen.value = true;
}; };
const saveQuery = () => {
addNote({
uid: uidGen('N'),
cUid: workspace.value.uid,
type: 'query',
date: new Date(),
note: query.value,
isArchived: false
});
isQuerySaved.value = true;
};
const openSavedModal = () => {
showScratchpad('query');
};
const selectQuery = (sql: string) => { const selectQuery = (sql: string) => {
if (queryEditor.value) if (queryEditor.value)
queryEditor.value.editor.session.setValue(sql); queryEditor.value.editor.session.setValue(sql);

View File

@@ -31,7 +31,7 @@
:class="{ 'active': resultsetIndex === index }" :class="{ 'active': resultsetIndex === index }"
@click="selectResultset(index)" @click="selectResultset(index)"
> >
<a>{{ result.fields ? result.fields[0]?.table : '' }} ({{ result.rows.length }})</a> <a>{{ result.fields ? result.fields[0]?.tableAlias ?? result.fields[0]?.table : `${t('general.results')} #${index}` }} ({{ result.rows.length }})</a>
</li> </li>
</ul> </ul>
<div ref="table" class="table table-hover"> <div ref="table" class="table table-hover">
@@ -44,7 +44,7 @@
:title="`${field.type} ${fieldLength(field) ? `(${fieldLength(field)})` : ''}`" :title="`${field.type} ${fieldLength(field) ? `(${fieldLength(field)})` : ''}`"
> >
<div ref="columnResize" class="column-resizable"> <div ref="columnResize" class="column-resizable">
<div class="table-column-title" @click="sort(field.name)"> <div class="table-column-title" @click="sort(field)">
<div v-if="field.key" :title="keyName(field.key)"> <div v-if="field.key" :title="keyName(field.key)">
<BaseIcon <BaseIcon
icon-name="mdiKey" icon-name="mdiKey"
@@ -56,8 +56,8 @@
</div> </div>
<span>{{ field.alias || field.name }}</span> <span>{{ field.alias || field.name }}</span>
<BaseIcon <BaseIcon
v-if="isSortable && currentSort === field.name || currentSort === `${field.table}.${field.name}`" v-if="isSortable && currentSort[resultsetIndex]?.field === field.name || currentSort[resultsetIndex]?.field === `${field.tableAlias || field.table}.${field.name}`"
:icon-name="currentSortDir === 'asc' ? 'mdiSortAscending' : 'mdiSortDescending'" :icon-name="currentSort[resultsetIndex].dir === 'asc' ? 'mdiSortAscending' : 'mdiSortDescending'"
:size="18" :size="18"
class="sort-icon ml-1" class="sort-icon ml-1"
/> />
@@ -270,6 +270,7 @@ import BaseSelect from '@/components/BaseSelect.vue';
import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue'; import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
import TableContext from '@/components/WorkspaceTabQueryTableContext.vue'; import TableContext from '@/components/WorkspaceTabQueryTableContext.vue';
import WorkspaceTabQueryTableRow from '@/components/WorkspaceTabQueryTableRow.vue'; import WorkspaceTabQueryTableRow from '@/components/WorkspaceTabQueryTableRow.vue';
import { copyText } from '@/libs/copyText';
import { unproxify } from '@/libs/unproxify'; import { unproxify } from '@/libs/unproxify';
import { useConsoleStore } from '@/stores/console'; import { useConsoleStore } from '@/stores/console';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
@@ -291,6 +292,10 @@ const props = defineProps({
results: Array as Prop<QueryResult[]>, results: Array as Prop<QueryResult[]>,
connUid: String, connUid: String,
mode: String as Prop<'table' | 'query'>, mode: String as Prop<'table' | 'query'>,
page: {
type: Number,
required: false
},
isSelected: Boolean, isSelected: Boolean,
elementType: { type: String, default: 'table' } elementType: { type: String, default: 'table' }
}); });
@@ -313,8 +318,7 @@ const hasFocus = ref(false);
const contextEvent = ref(null); const contextEvent = ref(null);
const selectedCell = ref(null); const selectedCell = ref(null);
const selectedRows = ref([]); const selectedRows = ref([]);
const currentSort = ref(''); const currentSort: Ref<{field: string; dir: 'asc' | 'desc'}[]> = ref([]);
const currentSortDir = ref('asc');
const resultsetIndex = ref(0); const resultsetIndex = ref(0);
const scrollElement = ref(null); const scrollElement = ref(null);
const rowHeight = ref(23); const rowHeight = ref(23);
@@ -357,14 +361,16 @@ const isHardSort = computed(() => {
}); });
const sortedResults = computed(() => { const sortedResults = computed(() => {
if (currentSort.value && !isHardSort.value) { if (currentSort.value[resultsetIndex.value] && !isHardSort.value) {
const sortObj = currentSort.value[resultsetIndex.value];
return [...localResults.value].sort((a: any, b: any) => { return [...localResults.value].sort((a: any, b: any) => {
let modifier = 1; let modifier = 1;
let valA = typeof a[currentSort.value] === 'string' ? a[currentSort.value].toLowerCase() : a[currentSort.value]; let valA = typeof a[sortObj.field] === 'string' ? a[sortObj.field].toLowerCase() : a[sortObj.field];
if (!isNaN(valA)) valA = Number(valA); if (!isNaN(valA)) valA = Number(valA);
let valB = typeof b[currentSort.value] === 'string' ? b[currentSort.value].toLowerCase() : b[currentSort.value]; let valB = typeof b[sortObj.field] === 'string' ? b[sortObj.field].toLowerCase() : b[sortObj.field];
if (!isNaN(valB)) valB = Number(valB); if (!isNaN(valB)) valB = Number(valB);
if (currentSortDir.value === 'desc') modifier = -1; if (sortObj.dir === 'desc') modifier = -1;
if (valA < valB) return -1 * modifier; if (valA < valB) return -1 * modifier;
if (valA > valB) return 1 * modifier; if (valA > valB) return 1 * modifier;
return 0; return 0;
@@ -571,7 +577,7 @@ const copyCell = () => {
let valueToCopy = row[cellName]; let valueToCopy = row[cellName];
if (typeof valueToCopy === 'object') if (typeof valueToCopy === 'object')
valueToCopy = JSON.stringify(valueToCopy); valueToCopy = JSON.stringify(valueToCopy);
navigator.clipboard.writeText(valueToCopy); copyText(valueToCopy);
}; };
const copyRow = (format: string) => { const copyRow = (format: string) => {
@@ -593,7 +599,7 @@ const copyRow = (format: string) => {
} }
if (format === 'json') if (format === 'json')
navigator.clipboard.writeText(JSON.stringify(contentToCopy)); copyText(JSON.stringify(contentToCopy));
else if (format === 'sql') { else if (format === 'sql') {
if (!Array.isArray(contentToCopy)) contentToCopy = [contentToCopy]; if (!Array.isArray(contentToCopy)) contentToCopy = [contentToCopy];
@@ -605,7 +611,7 @@ const copyRow = (format: string) => {
}, },
table: getTable(resultsetIndex.value) table: getTable(resultsetIndex.value)
}); });
navigator.clipboard.writeText(sqlInserts); copyText(sqlInserts);
} }
else if (format === 'csv') { else if (format === 'csv') {
const csv = []; const csv = [];
@@ -617,7 +623,7 @@ const copyRow = (format: string) => {
for (const row of contentToCopy) for (const row of contentToCopy)
csv.push(Object.values(row).map(col => typeof col === 'string' ? `"${col}"` : col).join(';')); csv.push(Object.values(row).map(col => typeof col === 'string' ? `"${col}"` : col).join(';'));
navigator.clipboard.writeText(csv.join('\n')); copyText(csv.join('\n'));
} }
else if (format === 'html') { else if (format === 'html') {
const arrayContent = new Array<string[]>(); const arrayContent = new Array<string[]>();
@@ -640,7 +646,7 @@ const copyRow = (format: string) => {
if (!Array.isArray(contentToCopy)) contentToCopy = [contentToCopy]; if (!Array.isArray(contentToCopy)) contentToCopy = [contentToCopy];
const printer = json2php.make({ linebreak: '\n', indent: '\t', shortArraySyntax: true }); const printer = json2php.make({ linebreak: '\n', indent: '\t', shortArraySyntax: true });
const phpString = printer(contentToCopy); const phpString = printer(contentToCopy);
navigator.clipboard.writeText(phpString); copyText(phpString);
} }
}; };
@@ -783,32 +789,42 @@ const contextMenu = (event: MouseEvent, cell: any) => {
isContext.value = true; isContext.value = true;
}; };
const sort = (field: string) => { const sort = (field: TableField) => {
if (!isSortable.value) return; if (!isSortable.value) return;
selectedRows.value = []; selectedRows.value = [];
let fieldName = field.name;
const hasTableInFieldname = Object.keys(localResults.value[0]).find(k => k !== '_antares_id').includes('.');
if (props.mode === 'query') if (props.mode === 'query' && hasTableInFieldname)
field = `${getTable(resultsetIndex.value)}.${field}`; fieldName = `${field.tableAlias || field.table}.${field.name}`;
if (field === currentSort.value) { if (fieldName === currentSort.value[resultsetIndex.value]?.field) {
if (currentSortDir.value === 'asc') if (currentSort.value[resultsetIndex.value].dir === 'asc')
currentSortDir.value = 'desc'; currentSort.value[resultsetIndex.value].dir = 'desc';
else else
resetSort(); resetSort();
} }
else { else {
currentSortDir.value = 'asc'; currentSort.value[resultsetIndex.value] = {
currentSort.value = field; field: fieldName,
dir: 'asc'
};
} }
if (isHardSort.value) if (isHardSort.value) {
emit('hard-sort', { field: currentSort.value, dir: currentSortDir.value }); emit('hard-sort', {
field: currentSort.value[resultsetIndex.value].field,
dir: currentSort.value[resultsetIndex.value].dir
});
}
}; };
const resetSort = () => { const resetSort = () => {
currentSort.value = ''; currentSort.value[resultsetIndex.value] = {
currentSortDir.value = 'asc'; field: null,
dir: 'asc'
};
}; };
const selectResultset = (index: number) => { const selectResultset = (index: number) => {
@@ -856,6 +872,7 @@ const downloadTable = (format: 'csv' | 'json' | 'sql' | 'php', table: string, po
}, },
client: workspaceClient.value, client: workspaceClient.value,
table, table,
page: props.page,
sqlOptions: popup ? { ...sqlExportOptions.value } : null, sqlOptions: popup ? { ...sqlExportOptions.value } : null,
csvOptions: popup ? { ...csvExportOptions.value } : null csvOptions: popup ? { ...csvExportOptions.value } : null
}); });
@@ -878,7 +895,7 @@ const onKey = async (e: KeyboardEvent) => {
const copyType = defaultCopyType.value; const copyType = defaultCopyType.value;
if (selectedRows.value.length >= 1) { if (selectedRows.value.length >= 1) {
if (selectedRows.value.length === 1 && copyType === 'cell') if (selectedRows.value.length === 1 && copyType === 'cell')
await navigator.clipboard.writeText(scrollElement.value.querySelector('.td.selected').innerText); await copyText(scrollElement.value.querySelector('.td.selected').innerText);
else if (selectedRows.value.length > 1 && copyType === 'cell') else if (selectedRows.value.length > 1 && copyType === 'cell')
copyRow('html'); copyRow('html');
else else

View File

@@ -43,6 +43,7 @@
autofocus autofocus
class="editable-field form-input input-sm px-1" class="editable-field form-input input-sm px-1"
@blur="editOFF" @blur="editOFF"
@keyup.delete.stop
> >
<BaseSelect <BaseSelect
v-else-if="inputProps.type === 'boolean'" v-else-if="inputProps.type === 'boolean'"
@@ -50,6 +51,7 @@
:options="['true', 'false']" :options="['true', 'false']"
class="form-select small-select editable-field" class="form-select small-select editable-field"
@blur="editOFF" @blur="editOFF"
@keyup.delete.stop
/> />
<BaseSelect <BaseSelect
v-else-if="enumArray" v-else-if="enumArray"
@@ -58,6 +60,7 @@
class="form-select small-select editable-field" class="form-select small-select editable-field"
dropdown-class="small-select" dropdown-class="small-select"
@blur="editOFF" @blur="editOFF"
@keyup.delete.stop
/> />
<input <input
v-else v-else
@@ -67,6 +70,7 @@
autofocus autofocus
class="editable-field form-input input-sm px-1" class="editable-field form-input input-sm px-1"
@blur="editOFF" @blur="editOFF"
@keyup.delete.stop
> >
</template> </template>
</template> </template>
@@ -382,7 +386,7 @@ const isBaseSelectField = computed(() => {
}); });
const enumArray = computed(() => { const enumArray = computed(() => {
if (props.fields[editingField.value] && props.fields[editingField.value].enumValues) if (props.fields[editingField.value] && props.fields[editingField.value].enumValues && props.fields[editingField.value].type !== 'SET')
return props.fields[editingField.value].enumValues.replaceAll('\'', '').split(','); return props.fields[editingField.value].enumValues.replaceAll('\'', '').split(',');
return false; return false;
}); });

View File

@@ -202,6 +202,7 @@
v-if="results" v-if="results"
ref="queryTable" ref="queryTable"
:results="results" :results="results"
:page="page"
:tab-uid="tabUid" :tab-uid="tabUid"
:conn-uid="connection.uid" :conn-uid="connection.uid"
:is-selected="isSelected" :is-selected="isSelected"

View File

@@ -28,7 +28,7 @@ export function useFilters () {
return `(${num})`; return `(${num})`;
}; };
const parseKeys = (keys: {[key: number]: string}[]) => { const parseKeys = (keys: Record<number, string>[]) => {
const isMacOS = process.platform === 'darwin'; const isMacOS = process.platform === 'darwin';
return (keys as string[]).map(k => ( return (keys as string[]).map(k => (
k.split('+') k.split('+')

View File

@@ -232,7 +232,7 @@ export const caES = {
newFunction: 'Nova funció', newFunction: 'Nova funció',
newScheduler: 'Nou planificador', newScheduler: 'Nou planificador',
newTriggerFunction: 'Nova funció de disparador', newTriggerFunction: 'Nova funció de disparador',
thereIsNoQueriesYet: 'Encara no hi ha consultes', thereAreNoQueriesYet: 'Encara no hi ha consultes',
searchForQueries: 'Cerca consultes', searchForQueries: 'Cerca consultes',
killProcess: 'Mata procés', killProcess: 'Mata procés',
exportSchema: 'Exporta esquema', exportSchema: 'Exporta esquema',

View File

@@ -233,7 +233,7 @@ export const csCZ = {
newFunction: 'Nová funkce', newFunction: 'Nová funkce',
newScheduler: 'Nový scheduler', newScheduler: 'Nový scheduler',
newTriggerFunction: 'Nová trigger funkce', newTriggerFunction: 'Nová trigger funkce',
thereIsNoQueriesYet: 'Nejsou tu žádné dotazy', thereAreNoQueriesYet: 'Nejsou tu žádné dotazy',
searchForQueries: 'Hledání dotazů', searchForQueries: 'Hledání dotazů',
killProcess: 'Zabít proces', killProcess: 'Zabít proces',
exportSchema: 'Exportovat schéma', exportSchema: 'Exportovat schéma',

View File

@@ -1,3 +1,12 @@
/**
* [TRANSLATION UPDATE HELPER]
* - Open a terminal in antares folder and run `npm run translation:check short-code` replacing short-code with the one you are updating.
* - The command will output which terms are missing or not translated from english.
* - Open antares folder with your editor of choice.
* - Go to antares/src/renderer/i18n/ and open the locale file you want to translate.
* - Add and translate missing terms and consider whether to translate untranslated terms.
*/
export const enUS = { export const enUS = {
general: { // General purpose terms general: { // General purpose terms
edit: 'Edit', edit: 'Edit',
@@ -65,9 +74,16 @@ export const enUS = {
actionSuccessful: '{action} successful', actionSuccessful: '{action} successful',
outputFormat: 'Output format', outputFormat: 'Output format',
singleFile: 'Single {ext} file', singleFile: 'Single {ext} file',
zipCompressedFile: 'ZIP compressed {ext} file' zipCompressedFile: 'ZIP compressed {ext} file',
copyName: 'Copy name',
search: 'Search',
title: 'Title',
archive: 'Archive', // verb
undo: 'Undo',
moveTo: 'Move to'
}, },
connection: { // Database connection connection: { // Database connection
connection: 'Connection',
connectionName: 'Connection name', connectionName: 'Connection name',
hostName: 'Host name', hostName: 'Host name',
client: 'Client', client: 'Client',
@@ -101,7 +117,8 @@ export const enUS = {
readOnlyMode: 'Read-only mode', readOnlyMode: 'Read-only mode',
allConnections: 'All connections', allConnections: 'All connections',
searchForConnections: 'Search for connections', searchForConnections: 'Search for connections',
keepAliveInterval: 'Keep alive interval' keepAliveInterval: 'Keep alive interval',
singleConnection: 'Single connection'
}, },
database: { // Database related terms database: { // Database related terms
schema: 'Schema', schema: 'Schema',
@@ -236,7 +253,7 @@ export const enUS = {
newFunction: 'New function', newFunction: 'New function',
newScheduler: 'New scheduler', newScheduler: 'New scheduler',
newTriggerFunction: 'New trigger function', newTriggerFunction: 'New trigger function',
thereIsNoQueriesYet: 'There is no queries yet', thereAreNoQueriesYet: 'There are no queries yet',
searchForQueries: 'Search for queries', searchForQueries: 'Search for queries',
killProcess: 'Kill process', killProcess: 'Kill process',
exportSchema: 'Export schema', exportSchema: 'Export schema',
@@ -265,12 +282,11 @@ export const enUS = {
targetTable: 'Target table', targetTable: 'Target table',
switchDatabase: 'Switch the database', switchDatabase: 'Switch the database',
searchForElements: 'Search for elements', searchForElements: 'Search for elements',
searchForSchemas: 'Search for schemas' searchForSchemas: 'Search for schemas',
savedQueries: 'Saved queries'
}, },
application: { // Application related terms application: { // Application related terms
settings: 'Settings', settings: 'Settings',
scratchpad: 'Scratchpad',
disableScratchpad: 'Disable scratchpad',
console: 'Console', console: 'Console',
general: 'General', general: 'General',
themes: 'Themes', themes: 'Themes',
@@ -317,7 +333,7 @@ export const enUS = {
wrapLongLines: 'Wrap long lines', wrapLongLines: 'Wrap long lines',
markdownSupported: 'Markdown supported', markdownSupported: 'Markdown supported',
plantATree: 'Plant a Tree', plantATree: 'Plant a Tree',
dataTabPageSize: 'DATA tab page size', dataTabPageSize: 'Results per page',
noOpenTabs: 'There are no open tabs, navigate on the left bar or:', noOpenTabs: 'There are no open tabs, navigate on the left bar or:',
restorePreviousSession: 'Restore previous session', restorePreviousSession: 'Restore previous session',
closeTab: 'Close tab', closeTab: 'Close tab',
@@ -341,7 +357,6 @@ export const enUS = {
saveContent: 'Save content', saveContent: 'Save content',
openAllConnections: 'Open all connections', openAllConnections: 'Open all connections',
openSettings: 'Open settings', openSettings: 'Open settings',
openScratchpad: 'Open scratchpad',
runOrReload: 'Run or reload', runOrReload: 'Run or reload',
openFilter: 'Open filter', openFilter: 'Open filter',
nextResultsPage: 'Next results page', nextResultsPage: 'Next results page',
@@ -349,6 +364,8 @@ export const enUS = {
editFolder: 'Edit folder', editFolder: 'Edit folder',
folderName: 'Folder name', folderName: 'Folder name',
deleteFolder: 'Delete folder', deleteFolder: 'Delete folder',
newFolder: 'New folder',
outOfFolder: 'Out of folder',
editConnectionAppearance: 'Edit connection appearance', editConnectionAppearance: 'Edit connection appearance',
defaultCopyType: 'Default copy type', defaultCopyType: 'Default copy type',
showTableSize: 'Show table size in sidebar', showTableSize: 'Show table size in sidebar',
@@ -375,7 +392,14 @@ export const enUS = {
ignoreDuplicates: 'Ignore duplicates', ignoreDuplicates: 'Ignore duplicates',
wrongImportPassword: 'Wrong import password', wrongImportPassword: 'Wrong import password',
wrongFileFormat: 'Wrong file format', wrongFileFormat: 'Wrong file format',
dataImportSuccess: 'Data successfully imported' dataImportSuccess: 'Data successfully imported',
note: 'Note | Notes',
thereAreNoNotesYet: 'There are no notes yet',
addNote: 'Add note',
editNote: 'Edit note',
showArchivedNotes: 'Show archived notes',
hideArchivedNotes: 'Hide archived notes',
tag: 'Tag' // Note tag
}, },
faker: { // Faker.js methods, used in random generated content faker: { // Faker.js methods, used in random generated content
address: 'Address', address: 'Address',

View File

@@ -229,7 +229,7 @@ export const frFR = {
newFunction: 'Nouvelle function', newFunction: 'Nouvelle function',
newScheduler: 'Nouveau déclencheur', newScheduler: 'Nouveau déclencheur',
newTriggerFunction: 'Nouvelle fonction de déclencheur', newTriggerFunction: 'Nouvelle fonction de déclencheur',
thereIsNoQueriesYet: 'Il n\'y a pas encore de requête', thereAreNoQueriesYet: 'Il n\'y a pas encore de requête',
searchForQueries: 'Rechercher des requêtes', searchForQueries: 'Rechercher des requêtes',
killProcess: 'Terminer un processus', killProcess: 'Terminer un processus',
exportSchema: 'Exporter un schéma', exportSchema: 'Exporter un schéma',

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