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

Compare commits

..

1 Commits

Author SHA1 Message Date
d743d486b8 chore(release): 0.5.5 2022-05-24 14:21:56 +02:00
232 changed files with 13544 additions and 43184 deletions

View File

@@ -174,25 +174,6 @@
"contributions": [ "contributions": [
"translation" "translation"
] ]
},
{
"login": "brdtheo",
"name": "Théo Billardey",
"avatar_url": "https://avatars.githubusercontent.com/u/48206778?v=4",
"profile": "https://codepen.io/theo-billardey",
"contributions": [
"translation"
]
},
{
"login": "dyaskur",
"name": "Muhammad Dyas Yaskur",
"avatar_url": "https://avatars.githubusercontent.com/u/9539970?v=4",
"profile": "http://yaskur.net",
"contributions": [
"translation",
"code"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

View File

@@ -1,5 +1,4 @@
node_modules node_modules
assets assets
out out
dist dist
build

View File

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

29
.github/workflows/build-linux.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Build/release [linux]
on: push
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
steps:
- name: Check out Git repository
uses: actions/checkout@v2
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 14
- name: Install dependencies
run: npm i
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:
github_token: ${{ secrets.github_token }}
release: ${{ startsWith(github.ref, 'refs/tags/v') }}

29
.github/workflows/build-mac.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Build/release [mac]
on: push
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest]
steps:
- name: Check out Git repository
uses: actions/checkout@v2
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 14
- name: Install dependencies
run: npm i
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:
github_token: ${{ secrets.github_token }}
release: ${{ startsWith(github.ref, 'refs/tags/v') }}

29
.github/workflows/build-win.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Build/release [windows]
on: push
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-2019]
steps:
- name: Check out Git repository
uses: actions/checkout@v2
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 14
- name: Install dependencies
run: npm i
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:
github_token: ${{ secrets.github_token }}
release: ${{ startsWith(github.ref, 'refs/tags/v') }}

View File

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

View File

@@ -1,31 +0,0 @@
name: Create artifact [LINUX]
on:
workflow_dispatch: {}
jobs:
build:
runs-on: ubuntu-20.04
steps:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 16
- name: npm install & build
run: |
npm install
npm run build
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: linux-build
retention-days: 3
path: |
build
!build/*-unpacked
!build/.icon-ico

View File

@@ -1,31 +0,0 @@
name: Create artifact [MAC]
on:
workflow_dispatch: {}
jobs:
build:
runs-on: macos-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 16
- name: npm install & build
run: |
npm install
npm run build
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: macos-build
retention-days: 3
path: |
build
!build/*-unpacked
!build/.icon-ico

View File

@@ -1,4 +1,4 @@
name: Test end-to-end [WINDOWS] name: Test end-to-end [linux]
on: push on: push
@@ -8,16 +8,16 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [windows-latest] os: [ubuntu-latest]
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Install Node.js - name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v3 uses: actions/setup-node@v1
with: with:
node-version: 16 node-version: 14
- name: Install dependencies - name: Install dependencies
run: npm i run: npm i

1
.gitignore vendored
View File

@@ -7,4 +7,5 @@ node_modules
thumbs.db thumbs.db
NOTES.md NOTES.md
*.txt *.txt
package-lock.json
*.heapsnapshot *.heapsnapshot

1
.nvmrc Normal file
View File

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

View File

@@ -1,7 +1,6 @@
{ {
"extends": [ "extends": [
"stylelint-config-standard", "stylelint-config-standard"
"stylelint-config-recommended-vue"
], ],
"fix": true, "fix": true,
"formatter": "verbose", "formatter": "verbose",
@@ -11,7 +10,6 @@
"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,
"declaration-colon-newline-after": "always-multi-line" "declaration-colon-newline-after": "always-multi-line"
}, },
"syntax": "scss" "syntax": "scss"

2
.vscode/launch.json vendored
View File

@@ -5,6 +5,7 @@
"name": "Electron: Main", "name": "Electron: Main",
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"port": 9222, "port": 9222,
"protocol": "inspector",
"request": "attach", "request": "attach",
"sourceMaps": true, "sourceMaps": true,
"type": "node", "type": "node",
@@ -22,6 +23,7 @@
"name": "Electron: Worker", "name": "Electron: Worker",
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"port": 9224, "port": 9224,
"protocol": "inspector",
"request": "attach", "request": "attach",
"sourceMaps": true, "sourceMaps": true,
"type": "node", "type": "node",

View File

@@ -5,12 +5,8 @@
"MySQL", "MySQL",
"PostgreSQL", "PostgreSQL",
"SQLite", "SQLite",
"Firebird SQL",
"Windows", "Windows",
"translation", "translation"
"Linux",
"MacOS",
"deps"
], ],
"svg.preview.background": "transparent" "svg.preview.background": "transparent"
} }

View File

@@ -2,377 +2,6 @@
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.4](https://github.com/antares-sql/antares/compare/v0.7.3...v0.7.4) (2023-02-10)
### Bug Fixes
* tables sort in sidebar ([ddb7ead](https://github.com/antares-sql/antares/commit/ddb7ead0832708713ba4bb2717661b8b93a47e3f))
### [0.7.3](https://github.com/antares-sql/antares/compare/v0.7.2...v0.7.3) (2023-02-09)
### Features
* **SQLite:** added support to INTEGER UNSIGNED ([e7e4913](https://github.com/antares-sql/antares/commit/e7e491340a037b64d6d8e538376415779d54332e))
### Bug Fixes
* longtext edit modal opens when it shouldn't, fixes [#524](https://github.com/antares-sql/antares/issues/524) ([6decba3](https://github.com/antares-sql/antares/commit/6decba316ca46106520cb4dba44409ceb4a4af75))
* select of table type stuck when editing an unknown type ([e8447e5](https://github.com/antares-sql/antares/commit/e8447e56551871a200517bdaa747ae215ad83cf4))
* **SQLite:** error with integer timestamp fields ([2b5e1e7](https://github.com/antares-sql/antares/commit/2b5e1e7b39c25f536b6139a4d01b9f7f17069ea8))
* **SQLite:** triggers disappear after editing related table, fixes [#523](https://github.com/antares-sql/antares/issues/523) ([d934ae1](https://github.com/antares-sql/antares/commit/d934ae1e6c0747698b4973d9cad217379076a6cf))
### [0.7.2](https://github.com/antares-sql/antares/compare/v0.7.1...v0.7.2) (2023-01-22)
### Features
* add copy shortcut and default copy type setting ([9aca894](https://github.com/antares-sql/antares/commit/9aca89477f1fd7b7f55f1e5c290d495c46f61d8e))
* connection info icons in footer ([ff4bc6c](https://github.com/antares-sql/antares/commit/ff4bc6c39b05a827cebde84466814cf246908208))
### Bug Fixes
* allow comments in queies, fixes [#519](https://github.com/antares-sql/antares/issues/519) ([c7ab3b7](https://github.com/antares-sql/antares/commit/c7ab3b77a22e85cee6fb93064eaad5a8e8ad9fd2))
* **SQLite:** exception saving tables without INT fields length ([0e80e82](https://github.com/antares-sql/antares/commit/0e80e823d059dfe24995b5848d88cc84235e6275))
* ssh connection closed after idle time, fixes [#425](https://github.com/antares-sql/antares/issues/425) ([6fa430a](https://github.com/antares-sql/antares/commit/6fa430adf68013a9d0a093031f56dd741bdc0299))
### [0.7.1](https://github.com/antares-sql/antares/compare/v0.7.0...v0.7.1) (2022-12-23)
### Features
* Copy rows as html table, so we can paste it to spreadsheet ([c32f463](https://github.com/antares-sql/antares/commit/c32f463ea5ac3f54cba32929f77442f1e0ba934a))
* option to disable selected query execution, closes [#477](https://github.com/antares-sql/antares/issues/477) ([1bd26ce](https://github.com/antares-sql/antares/commit/1bd26ceaa68fe66f26c76b3b60fa6eeccea91729))
### Bug Fixes
* bahasa indonesia typos ([897795d](https://github.com/antares-sql/antares/commit/897795ddbb4ade2652b0471f18288b8b3aaf0eb9))
* connection default icon not change after client change ([a6bdf69](https://github.com/antares-sql/antares/commit/a6bdf69a281c8614c41274b6dc2f3563aa89c57e))
* context submenu outside view when near the edge, fixes [#506](https://github.com/antares-sql/antares/issues/506) ([c08946e](https://github.com/antares-sql/antares/commit/c08946e932884e5f0253df2545f98315ab7e5219))
* **i18n:** add missing keys for french translation ([fd129a2](https://github.com/antares-sql/antares/commit/fd129a2ad1c3401372c9172b38f4406254d134df))
* **MySQL:** not every connection gets read-only option ([843c15e](https://github.com/antares-sql/antares/commit/843c15e428c4a0412f19a93ab05d2fcbb60da09b))
* **UI:** white background dragging connections inside folder on Linux ([dd971d7](https://github.com/antares-sql/antares/commit/dd971d70e04faf0d5b239586b12e4a9a42407433))
* white background dragging connections or tabs on Linux, fixes [#486](https://github.com/antares-sql/antares/issues/486) ([669d7e8](https://github.com/antares-sql/antares/commit/669d7e8d4d062ed5bdafe1d5cde8ec51a2f68b26))
## [0.7.0](https://github.com/antares-sql/antares/compare/v0.6.0...v0.7.0) (2022-11-30)
### Features
* **UI:** connections customization ([212b2bd](https://github.com/antares-sql/antares/commit/212b2bdba933db1af22967a057d64f41c96b9930))
* **UI:** folders customization ([72accb7](https://github.com/antares-sql/antares/commit/72accb7b0e62977dde2c82312aee5b9d025e5182))
* **UI:** folders implementation ([ece6c64](https://github.com/antares-sql/antares/commit/ece6c6401def4a9b42280f23efaa038b9ad98de8))
* **UI:** footer color based on folder color ([4fe9dfc](https://github.com/antares-sql/antares/commit/4fe9dfc4d7ca19a798b8a8a74d979ab88aebeaac))
* **UI:** new settimgbar tooltips ([6728964](https://github.com/antares-sql/antares/commit/672896414e901635c83ca037663e355a31ce013b))
### Bug Fixes
* deletion of connections inside folder ([b06bafe](https://github.com/antares-sql/antares/commit/b06bafe06c060233ebe901979b9fc4455a25cb89))
* missing sidebar data after update ([0a1f50a](https://github.com/antares-sql/antares/commit/0a1f50a9b92c9705784b93f14be40a01a78cb0da))
* **UI:** folder to folder drag glitches ([0fca70e](https://github.com/antares-sql/antares/commit/0fca70ebec1af3d594db4e1ce159e52e12224850))
* **UI:** wrong copnnection icons color with light theme ([d010d5a](https://github.com/antares-sql/antares/commit/d010d5aa8f07a5def1183567ee767fd144696ed3))
* wrong position moving elements outside folder ([7af178a](https://github.com/antares-sql/antares/commit/7af178a1e400876e6c45dbe31a198a12d29227a8))
## [0.6.0](https://github.com/antares-sql/antares/compare/v0.5.19...v0.6.0) (2022-11-18)
### Features
* **Firebird SQL:** connections pool ([76df631](https://github.com/antares-sql/antares/commit/76df6319c242ea42f93d4e5d811d96ec2c282aa8))
* **Firebird SQL:** display table content and query results ([95bb41e](https://github.com/antares-sql/antares/commit/95bb41e9db255a780aae1ae32ce4a53ee3bab20e))
* **Firebird SQL:** manual commit mode ([27566c1](https://github.com/antares-sql/antares/commit/27566c1dfae55f72d3f870c50410e5ecda256037))
* **Firebird SQL:** procedure add/edit/delete support ([ae312ef](https://github.com/antares-sql/antares/commit/ae312efbbc3a9941380477b9849bdd8edc5b9fbf))
* **Firebird SQL:** support to blob fields ([0827a04](https://github.com/antares-sql/antares/commit/0827a04d61af75b4366394e5f0289df461d02c98))
* **Firebird SQL:** support to indexes and foreign keys ([2c8509f](https://github.com/antares-sql/antares/commit/2c8509ff4173fbebeff92ab472a37edd3d80a2ac))
* **Firebird SQL:** table add/edit/delete support ([1b5cc31](https://github.com/antares-sql/antares/commit/1b5cc315dddca6b753fb6fe6e196e29441ffed79))
* **Firebird SQL:** trigger add/edit/delete support ([8e422e3](https://github.com/antares-sql/antares/commit/8e422e3f07323f388523621a05f0403a87f19e47))
* **Firebird SQL:** view add/edit/delete support ([7d1967a](https://github.com/antares-sql/antares/commit/7d1967a60977b2ce1095a37b7135f429a83f163d))
* support to text blob fields ([e6f6a02](https://github.com/antares-sql/antares/commit/e6f6a022d1a5bbc3f5303f635a2115813601c61a))
### Bug Fixes
* **Firebird SQL:** connection pool issue ([7ff8e21](https://github.com/antares-sql/antares/commit/7ff8e2149ef911a235b4a1dcc329775af1d2a72b))
* incomplete list of collations, fixes [#478](https://github.com/antares-sql/antares/issues/478) ([1c1403f](https://github.com/antares-sql/antares/commit/1c1403f58641f7b5f8a7c29fc430673ffa88f969))
* loss of precision updating BIGINT values, fixes [#467](https://github.com/antares-sql/antares/issues/467) ([d190a2d](https://github.com/antares-sql/antares/commit/d190a2dd61040d1748dfb97403f9d56015d938fe))
### [0.5.19](https://github.com/antares-sql/antares/compare/v0.5.18...v0.5.19) (2022-10-22)
### Features
* context menu option to fill cell with random values ([0a2124f](https://github.com/antares-sql/antares/commit/0a2124f2c2bdadc7c753db11d1e29f8acb9ddcac))
* uuid fill for string cells ([24edc82](https://github.com/antares-sql/antares/commit/24edc82b1be7299a09df18611b2a0ba361a6b46f))
### Bug Fixes
* app stuck inserting a random value if field length high ([440f74d](https://github.com/antares-sql/antares/commit/440f74dfc1f4942ba585b9bdae7517fe6ab04a81))
* error joining tables with different schema ([88408da](https://github.com/antares-sql/antares/commit/88408da745e45c70de977bc4270e5f61825be65f))
* **SQLite:** save boolean as integer to improve compativility, fixes [#463](https://github.com/antares-sql/antares/issues/463) ([d52b7af](https://github.com/antares-sql/antares/commit/d52b7af2978bc8beafd2d22078c72abb62e9e532))
* unable to edit text fields if value is NULL, fixes [#466](https://github.com/antares-sql/antares/issues/466) ([8621ca5](https://github.com/antares-sql/antares/commit/8621ca5333b5c51dc7a2ea1fcc0c5ec7f752a00a))
### [0.5.18](https://github.com/antares-sql/antares/compare/v0.5.17...v0.5.18) (2022-10-14)
### Features
* **PostgreSQL:** UUID random generation option on UUID fields, closes [#424](https://github.com/antares-sql/antares/issues/424) ([a521274](https://github.com/antares-sql/antares/commit/a521274d01031c1411bbbb136369802d43368089))
### Bug Fixes
* auto-scroll on sidebar not working, fixes [#447](https://github.com/antares-sql/antares/issues/447) ([dd9c089](https://github.com/antares-sql/antares/commit/dd9c089d27c61ab76d49887c7de2849cbb6e88a6))
* trackpad horizontal scroll on tabs not working properly ([ba5a1b6](https://github.com/antares-sql/antares/commit/ba5a1b68ab2d56777a5c94eede26e9bded5819e6))
### [0.5.17](https://github.com/antares-sql/antares/compare/v0.5.16...v0.5.17) (2022-09-22)
### Features
* added more editor font sizes, closes [#440](https://github.com/antares-sql/antares/issues/440) ([d114f8a](https://github.com/antares-sql/antares/commit/d114f8a65164f702b23175095e6fc2b021e0e038))
### Bug Fixes
* "run or reload" shortcut triggers on all connections open ([01f607c](https://github.com/antares-sql/antares/commit/01f607cd40c18ab0f9761b2a05705a966aaae43a))
* cant run procedures with parameters from leftbar ([efe134a](https://github.com/antares-sql/antares/commit/efe134a059700ca87333dc6e66166d6ec8d289e8))
* editor font size doesn't change on new tabs, fixes [#442](https://github.com/antares-sql/antares/issues/442) ([84168d1](https://github.com/antares-sql/antares/commit/84168d1d75460acc2c844bfece7d85f0c977e74c))
* empty definer when editing a view, fixes [#437](https://github.com/antares-sql/antares/issues/437) ([498a9b4](https://github.com/antares-sql/antares/commit/498a9b48e25ee061960f5f649c953cdaf6ff1a58))
* **MacOS:** empty options on macos menubar ([a142d3c](https://github.com/antares-sql/antares/commit/a142d3c4d77e31375dfbea148eec54ce1f635192))
### [0.5.16](https://github.com/antares-sql/antares/compare/v0.5.15...v0.5.16) (2022-08-26)
### Bug Fixes
* CTRL+Right/Left not working on text editor, closes [#427](https://github.com/antares-sql/antares/issues/427) ([ffc645b](https://github.com/antares-sql/antares/commit/ffc645ba5efb1c52670096e4f8c7f992b7335dae))
* issue updating datetime cells with null value, closes [#423](https://github.com/antares-sql/antares/issues/423) ([ebc325a](https://github.com/antares-sql/antares/commit/ebc325ae0c656dca2eb8f7544ab271beaee9b47e))
* ts exceptions ([df68114](https://github.com/antares-sql/antares/commit/df681147aaf0bfca69f3ffdc474cc1846541b1d8))
* **UI:** editor themes group not visible in select element ([9dc700e](https://github.com/antares-sql/antares/commit/9dc700e13ea65bb8c6feac4ff4ffeadd32053614))
* **UI:** wrong position of fields resizable area ([c90ab0e](https://github.com/antares-sql/antares/commit/c90ab0e8807ff30a9ab58e9aa3515cf427dd6e86))
* unable to set null or delete rows without primary key ([39326eb](https://github.com/antares-sql/antares/commit/39326eb52e038728b5419d4a8de8024c7ead3002))
### [0.5.15](https://github.com/antares-sql/antares/compare/v0.5.14...v0.5.15) (2022-08-17)
### Features
* ability to add new shortcuts ([d044a02](https://github.com/antares-sql/antares/commit/d044a02cb79a9d06aadc34cdbf6e81da84360559))
* ability to edit shortcuts ([8eb127e](https://github.com/antares-sql/antares/commit/8eb127e45838bc01ba12f0740fec077fcd975532))
* added more events in shortcuts setting ([5043faf](https://github.com/antares-sql/antares/commit/5043fafa934844ebc2f59cabcec830c6a4d5ca8e))
* delete shortcuts and restore defaults ([c22413f](https://github.com/antares-sql/antares/commit/c22413fde9dfe5501a5f220070cfe552a318c70b))
* dynamic shortcut suggestions on empty query tabs ([4df14c3](https://github.com/antares-sql/antares/commit/4df14c3693955bd7801b4b99103fca85f00f3e8c))
* list of available shortcuts in settings window ([44bb75b](https://github.com/antares-sql/antares/commit/44bb75bc60d7d31bbd99a9ba57f30fd354f7581c))
* **UI:** connection name on left bar, closes [#382](https://github.com/antares-sql/antares/issues/382) [#414](https://github.com/antares-sql/antares/issues/414) ([4887753](https://github.com/antares-sql/antares/commit/48877534d1a41d351b267c0dab925046ca984179))
* **UI:** shortcuts setting UI improved ([49b63bc](https://github.com/antares-sql/antares/commit/49b63bc6f28fc6031e6a892d0a48cd35ae2f26cd))
### Bug Fixes
* startup exception ([c50d17e](https://github.com/antares-sql/antares/commit/c50d17e82b7fd337d4037ddf646cd1a8fc765bae))
### Improvements
* improved keypress detector ([0f219cf](https://github.com/antares-sql/antares/commit/0f219cf9b796b4369c609fb0e8e3b84346a30b07))
* **translation:** updated italian translation ([c05be83](https://github.com/antares-sql/antares/commit/c05be8304f3cf299cf338f67c00184305e022919))
### [0.5.14](https://github.com/antares-sql/antares/compare/v0.5.13...v0.5.14) (2022-08-09)
### Bug Fixes
* unable to open settingbar context menu ([44eb507](https://github.com/antares-sql/antares/commit/44eb507a12bad028a4fa8a8bb0f6442a3e8dde91))
### [0.5.13](https://github.com/antares-sql/antares/compare/v0.5.12...v0.5.13) (2022-08-09)
### Features
* copy row as CSV, closes [#394](https://github.com/antares-sql/antares/issues/394) ([1c3d7aa](https://github.com/antares-sql/antares/commit/1c3d7aa30bb9c2bd900a764ee6b97960729e9263))
* new macos icon ([0bfa14e](https://github.com/antares-sql/antares/commit/0bfa14e1c90320578597df030941530b670a4131))
### Bug Fixes
* **MySQL:** error with ANSI sql_mode ([f64a12a](https://github.com/antares-sql/antares/commit/f64a12a8e9c5f764c3a692f1a032736e008058b5))
* set legacy: false ([104b7c9](https://github.com/antares-sql/antares/commit/104b7c928b9c2abfc056880f16c606a0b1fa7c67))
### [0.5.12](https://github.com/antares-sql/antares/compare/v0.5.11...v0.5.12) (2022-07-26)
### Features
* ability to copy multiple selected rows ([9551afb](https://github.com/antares-sql/antares/commit/9551afbd2d7e525c81f28e98e788b92609ce9de4))
* context menu option to duplicate a table row ([985e5d3](https://github.com/antares-sql/antares/commit/985e5d352793d1b3e1981d004b6f494bfbb049bf))
* copy row as SQL INSERT ([d3da15a](https://github.com/antares-sql/antares/commit/d3da15aa1377dcba73927047563f1d0c2d1284ca))
* execute selected query ([7890263](https://github.com/antares-sql/antares/commit/78902639ebb29a8c53f8aa0d2045c74e0646febc))
* export table content as SQL INSERT ([f3b5de3](https://github.com/antares-sql/antares/commit/f3b5de38c4abfd2c1d738e179fc22e6c8b6f9080))
### Bug Fixes
* disable ctrl+alt+(left/right) shortcut on linux ([8ecaedb](https://github.com/antares-sql/antares/commit/8ecaedbf6c2fc0dc56ff2177a87dd6ede74bdd22))
* error on schema export ([1d151e9](https://github.com/antares-sql/antares/commit/1d151e9349fd97576ccd8ef7f88ca789a1f28b65))
* issue with logger on import/export ([cb038b3](https://github.com/antares-sql/antares/commit/cb038b374a4fe85ad569e42eee7af123c925e775))
* missing defaults on insert row window ([1ead76c](https://github.com/antares-sql/antares/commit/1ead76c02889f48bd91cae702820b082ca2ff54b))
* missing table on insert new records on session restored tabs ([8c83b3f](https://github.com/antares-sql/antares/commit/8c83b3f1447354ec63b2a308db05ad4d54659aa7))
* **MySQL:** missing quoted identifier for column names in table filter, closes [#380](https://github.com/antares-sql/antares/issues/380) ([eb60899](https://github.com/antares-sql/antares/commit/eb60899e6e17879c79a7ee7108061e9aca8596f7))
* prevent ctrl+a in console ([a00c19d](https://github.com/antares-sql/antares/commit/a00c19d3003cd43d3ee6e3132728122bb2b24c97))
### [0.5.11](https://github.com/antares-sql/antares/compare/v0.5.10...v0.5.11) (2022-07-19)
### Bug Fixes
* console events disabled in production ([0b1aa3d](https://github.com/antares-sql/antares/commit/0b1aa3dd299db641df3d4c56c7ee56a187fc3ab3))
* filter persists switching temporary table tabs ([bf768c3](https://github.com/antares-sql/antares/commit/bf768c380087b65604b5b571a9858a7f07bd681d))
* unable to edit table fields content on tables with datetime fields ([91e0630](https://github.com/antares-sql/antares/commit/91e06305133c97ea02dcfdc4e739a4b0a7e7049d))
### [0.5.10](https://github.com/antares-sql/antares/compare/v0.5.9...v0.5.10) (2022-07-18)
### Features
* context menu to copy queries from console ([c21bd60](https://github.com/antares-sql/antares/commit/c21bd6075c1203607c05e45b76233d57e3008190))
* Ctrl+PgUp & Ctrl+PgDn to navigate between tabs ([abf8298](https://github.com/antares-sql/antares/commit/abf829867e567354e534cff3e02a6d43f4c7a262))
* field names suggestion for tables in the query ([b71f04e](https://github.com/antares-sql/antares/commit/b71f04e5aa3c37eaa160dfbc76d1b84789e3543e))
* initial console implementation ([6a6f43a](https://github.com/antares-sql/antares/commit/6a6f43a718561e0abd2cb89048b7fe45d08736ae))
* ipc event channel to send logs to renderer ([f12a04b](https://github.com/antares-sql/antares/commit/f12a04b0524f1172334c89afeb27675c19ff68d2))
* open/close console on single connection ([44647f5](https://github.com/antares-sql/antares/commit/44647f5b5508965bf5a7264add89e175c725e877))
### Bug Fixes
* exception on QueryEditor with null modelValue ([9bc9adb](https://github.com/antares-sql/antares/commit/9bc9adb7cff19b86a99491d968485a4cd7b47b99))
* fields content language detection not working properly ([a91fa8f](https://github.com/antares-sql/antares/commit/a91fa8ff54bbf1f8475666efd3a268a3a4f07f0c))
* **Linux:** ctrl+space shortcut not working ([ed3d35f](https://github.com/antares-sql/antares/commit/ed3d35f1319a1e2edcb8104f2045a71b9e9754a2))
* unable to delete by select all in left bar search, closes [#368](https://github.com/antares-sql/antares/issues/368) ([7725faf](https://github.com/antares-sql/antares/commit/7725fafe852479720fa619ced0970f2fa0099191))
* unable to update data on tables missing primary or unique key ([e0946f0](https://github.com/antares-sql/antares/commit/e0946f04f792d25c187ea56d4714bdacc016ada3))
### Improvements
* improved resize of text editor resizing console height ([3f9e6d8](https://github.com/antares-sql/antares/commit/3f9e6d85ca445eea1028effa32418eee4980f87d))
* **UI:** improved visibility of explore bar tooltips ([f312cf5](https://github.com/antares-sql/antares/commit/f312cf5f855deddd562c26d1835f78d16499b93b))
### [0.5.9](https://github.com/antares-sql/antares/compare/v0.5.8...v0.5.9) (2022-07-06)
### Features
* ability to pin/unpin and delete connections from the "all connections" modal ([8e70570](https://github.com/antares-sql/antares/commit/8e705706aecc5c9790329e63e61a1c02fa5d0342))
* connections sorted by last usage by default and option to pin them ([36e98e0](https://github.com/antares-sql/antares/commit/36e98e0742657e25df7768aa5b3b7cb350df5509))
* ctrl/cmd+space to open all connections modal ([a9a4344](https://github.com/antares-sql/antares/commit/a9a4344a71cc0f8f156b839733f6ddc200a26268))
* modal with all connections ([a703dcc](https://github.com/antares-sql/antares/commit/a703dcc53eb920117bc346a3c21f0c729c0ad96d))
* option to disable scratchpad ([56b0a48](https://github.com/antares-sql/antares/commit/56b0a4815c6f54eef164d849f6ca25af1e142b16))
* search form in all connections modal ([ec5ab73](https://github.com/antares-sql/antares/commit/ec5ab73b19d99e9971ae87e5f0a8d1bd1c34ef00))
### Bug Fixes
* error on export schema ([cf9c7c6](https://github.com/antares-sql/antares/commit/cf9c7c600aa915cef1ec3777866badb7ab1312ee))
* missing option for untrusted ssl connection on connections edit panel ([71a5b5c](https://github.com/antares-sql/antares/commit/71a5b5c8285fb777c43e7f6516006bfe9f52591c))
### Improvements
* **UI:** improved focus visibility for buttons ([d2eb31a](https://github.com/antares-sql/antares/commit/d2eb31a63d612323f8738eded1e1ce7b23554001))
### [0.5.8](https://github.com/antares-sql/antares/compare/v0.5.7...v0.5.8) (2022-07-02)
### Features
* add max visible options prop ([067a6f3](https://github.com/antares-sql/antares/commit/067a6f350757c1e6b4df51f801ae832b47bd3484))
* context shortcut to disconnect from left bar ([e97da37](https://github.com/antares-sql/antares/commit/e97da3710385690b85391938e40145a1591bc2e8))
* **MySQL:** option to disable foreign key checks when empty a table ([902c29f](https://github.com/antares-sql/antares/commit/902c29ffa551bc3489fa1d9136ee926d135ea14f))
### Bug Fixes
* connection string field doesn't appear switching to postgre when editing a connection ([6573fe6](https://github.com/antares-sql/antares/commit/6573fe69aca2b99c7a700879fb0d0930e864cbe6))
* ctrl+a on results doesn't work properly ([5f57a9f](https://github.com/antares-sql/antares/commit/5f57a9f60d281e24e5bee4330c081fa5d8651b36))
* double context menu on table settings rows ([91d0735](https://github.com/antares-sql/antares/commit/91d0735a5f4861bc6ad13b9285ea7a9bd7be9538))
* editor gutter pin not working ([cfd82c8](https://github.com/antares-sql/antares/commit/cfd82c8f419952879b386187eb146847098263fe))
* error on modals missing focusable elements ([7702ca0](https://github.com/antares-sql/antares/commit/7702ca025fcae6209ae3851d0ccd25579f93e243))
* exception on new scheduler tab ([a45d76e](https://github.com/antares-sql/antares/commit/a45d76e8b4ecdecf53438fe174f61ea32f4e10ac))
* focus goes outside modals with tab key navigation ([e42c424](https://github.com/antares-sql/antares/commit/e42c424a13a6901414a1a1c4e2f68cb4ddef7d59))
* reactivity problem on BaseVirtualScroll component ([45b2eb2](https://github.com/antares-sql/antares/commit/45b2eb2934b9f7a08f379ad4d7a44b1c89585449))
* result table cells/rows not loses focus clicking outside ([0a3a482](https://github.com/antares-sql/antares/commit/0a3a4827dd75539666fa2c827415af3bfa224543))
* **UI:** wrong tables scrollable height after switching tabs ([8f01740](https://github.com/antares-sql/antares/commit/8f01740475ea6d5d9b5eefabdbf27099df76f2cf))
* **Windows:** white window buttons with dark theme ([a80d227](https://github.com/antares-sql/antares/commit/a80d22740045a61fd14fd5da401c0d123d54f4de))
* **Windows:** Windows 7 style window frame at startup ([93ce619](https://github.com/antares-sql/antares/commit/93ce619782d58cfb8fb1ecce2ca2137a61ec6181))
### [0.5.7](https://github.com/antares-sql/antares/compare/v0.5.4...v0.5.7) (2022-06-19)
### Features
* added dropdown animation ([5398964](https://github.com/antares-sql/antares/commit/539896419064db9127f6a72acdbb11af2c4aa60a))
* dynamic app window title ([0024269](https://github.com/antares-sql/antares/commit/00242697a102f82dd0c731a3529c984fbdf83b3e))
* hotkeys to navigate forward or backward between tabs ([d3b9e08](https://github.com/antares-sql/antares/commit/d3b9e08446708654b3c6fad565b734d93effe683))
* hotkeys to navigate inside a table resultset ([49abd1e](https://github.com/antares-sql/antares/commit/49abd1ea7f5ec368e9a9201f8fd5b6520c4bd0a8))
* **translation:** russian translation, closes [#266](https://github.com/antares-sql/antares/issues/266) ([9082960](https://github.com/antares-sql/antares/commit/9082960310573a6e4d14bfbe82ed2eb1489f308d))
* **UI:** BaseSelect disabled state ([2b436d8](https://github.com/antares-sql/antares/commit/2b436d8613a1e3dff55d73adbddf5d2cd2452f27))
* **UI:** BaseSelect in table filters ([a037d0c](https://github.com/antares-sql/antares/commit/a037d0cc0148444e8e6c5b87c79f6ba9c2a6f0fe))
* **UI:** BaseSelect option list scrolls automatically using up/down keys ([0043d07](https://github.com/antares-sql/antares/commit/0043d077081fc49724722a5d5a74986d990c539d))
* **UI:** BaseSelect small variant ([5582a12](https://github.com/antares-sql/antares/commit/5582a12bbfade75dbcc7f9d71ada7190ed08d3c2))
* **UI:** BaseSelect supports disabled options ([f7e04d6](https://github.com/antares-sql/antares/commit/f7e04d633340a53420ce1c434e906c9434620e6e))
* **UI:** BaseSelect supports option groups ([1869e6a](https://github.com/antares-sql/antares/commit/1869e6a1482daf9381d9ac2244bf0aeffa758edc))
* **UI:** ForeignKeySelect implements BaseSelect component ([302c664](https://github.com/antares-sql/antares/commit/302c66457deeb50facf4735291640fcf48b78f66))
* **UI:** initial BaseSelect integration ([22622df](https://github.com/antares-sql/antares/commit/22622df2cfcb71054c6f6110b7ad9d4f635553dc))
* **UI:** new BaseSelect component ([745d551](https://github.com/antares-sql/antares/commit/745d551cc9253eae4e39e5d3406ceee051a7d6c1))
* **UI:** select tab replace with BaseSelect component ([42bc919](https://github.com/antares-sql/antares/commit/42bc9196ffc2f64b77f9cb42136255fc74815034))
### Bug Fixes
* empty query tab schema select if no schema selected ([31b7999](https://github.com/antares-sql/antares/commit/31b7999bba5d115913d42087614b9888bc761068))
* exception on app start setting window title ([5b33419](https://github.com/antares-sql/antares/commit/5b33419b6421d7d198a978e79e22d0a76306cdb4))
* fields sorting in table setting tabs ([77d9cac](https://github.com/antares-sql/antares/commit/77d9cac092fbb806810c3463ca066395fcab5307))
* inline field update not working with tables missing primary key ([caf776b](https://github.com/antares-sql/antares/commit/caf776bd55606c793c9763c204aa9f05d1feb27f))
* **Linux:** setting bar tooltip position ([6bad032](https://github.com/antares-sql/antares/commit/6bad032f0d1094736f651b6c06a60d2a0df36c98))
* main process not closed after window close on some conditions ([23acf00](https://github.com/antares-sql/antares/commit/23acf00def77b5662e48b84591a31760737774a7))
* **PostgreSQL:** idle timeout disabled ([a082514](https://github.com/antares-sql/antares/commit/a082514f88040c7e0ffdf4e8357bab45370a4c39))
* query tab content disappears reordering or closing other tabs, closes [#261](https://github.com/antares-sql/antares/issues/261) ([c5baf2b](https://github.com/antares-sql/antares/commit/c5baf2b0d379fdd28ee8cb907628bbfca940e2f6))
* reload tab content on tab sort ([d214c1f](https://github.com/antares-sql/antares/commit/d214c1f35ba231a8a01dbe8c0faad07d4b337752))
* selected foreign key value not visible in the insert row modal ([cba2ce2](https://github.com/antares-sql/antares/commit/cba2ce2e37cedbf0b242cc474b37bf052009ae62))
* **SQLite:** unable to insert rows with TEXT fields ([a7d5e19](https://github.com/antares-sql/antares/commit/a7d5e1973cd59d7d0ef1e74bdcf44d87fba43559))
* SSH tunnel connection error with private key, closes [#260](https://github.com/antares-sql/antares/issues/260) ([c826888](https://github.com/antares-sql/antares/commit/c826888b0dd0908958a4f727ddfa642e846269cf))
* **UI:** BaseSelect keyboard navigation ([7c45203](https://github.com/antares-sql/antares/commit/7c452036368fa0db6b9cde7c35e60a8e57bfece7))
* **UI:** BaseSelect style ([71b0736](https://github.com/antares-sql/antares/commit/71b0736d0ddbd599ab41cde0a6b0823e2bb7da2f))
* **UI:** select closes clicking on scrollbar ([8870304](https://github.com/antares-sql/antares/commit/8870304c15346257a90193807b9ae07c1393e3e2))
* unable to add new table fields ([ee623b0](https://github.com/antares-sql/antares/commit/ee623b0a0f121df0ac53d49d8be437c76ddb8539))
### Improvements
* improved precision of MariaDB or MySQL auto detection ([26aad51](https://github.com/antares-sql/antares/commit/26aad519df6ea1bbc7dffbf540193a7b2ed9ae2a))
* **Linux:** title bar improvements ([85cec05](https://github.com/antares-sql/antares/commit/85cec05f7037a1339ee223554cf127693a527aa1))
* **UI:** max height for query text area increased ([5d5f1da](https://github.com/antares-sql/antares/commit/5d5f1da97b9adfa743197d8fa0bbb6addd565a7a))
* **Windows:** title bar improvements ([5fa8bf3](https://github.com/antares-sql/antares/commit/5fa8bf38e433ef2fb31bcb893cd9e75549bd6a49))
### [0.5.6](https://github.com/antares-sql/antares/compare/v0.5.4...v0.5.6) (2022-06-02)
### Bug Fixes
* empty query tab schema select if no schema selected ([31b7999](https://github.com/antares-sql/antares/commit/31b7999bba5d115913d42087614b9888bc761068))
* inline field update not working with tables missing primary key ([caf776b](https://github.com/antares-sql/antares/commit/caf776bd55606c793c9763c204aa9f05d1feb27f))
* **SQLite:** unable to insert rows with TEXT fields ([a7d5e19](https://github.com/antares-sql/antares/commit/a7d5e1973cd59d7d0ef1e74bdcf44d87fba43559))
* **UI:** select closes clicking on scrollbar ([8870304](https://github.com/antares-sql/antares/commit/8870304c15346257a90193807b9ae07c1393e3e2))
### Improvements
* improved precision of MariaDB or MySQL auto detection ([26aad51](https://github.com/antares-sql/antares/commit/26aad519df6ea1bbc7dffbf540193a7b2ed9ae2a))
### [0.5.5](https://github.com/antares-sql/antares/compare/v0.5.4...v0.5.5) (2022-05-24) ### [0.5.5](https://github.com/antares-sql/antares/compare/v0.5.4...v0.5.5) (2022-05-24)

View File

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

View File

@@ -12,14 +12,13 @@
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.
**At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB, PostgreSQL, SQLite and Firebird SQL. **At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB, PostgreSQL and SQLite.
However, there are all the features necessary to have a pleasant database management experience, so give it a chance and send us your feedback, we would really appreciate it. At the moment, however, there are all the features necessary to have a pleasant database management experience, so give it a chance and send us your feedback, we would really appreciate it.
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](https://twitter.com/AntaresSQL) on Twitter. 👁 To stay tuned for new releases [follow Antares SQL](https://twitter.com/AntaresSQL) on Twitter.
🌟 Don't forget to **leave a star** if you appreciate this project. 🌟 Don't forget to **leave a star** if you appreciate this project.
🗳️ Poll: **[Which is the main OS you use Antares on?](https://github.com/antares-sql/antares/discussions/379)**
## Current key features ## Current key features
@@ -34,7 +33,6 @@ We are actively working on it, hoping to provide new cool features, improvements
- SSH tunnel support. - SSH tunnel support.
- Manual commit mode. - Manual commit mode.
- Import and export database dumps. - Import and export database dumps.
- Customizable keyboard shortcuts.
- Dark and light theme. - Dark and light theme.
- Editor themes. - Editor themes.
@@ -42,20 +40,20 @@ We are actively working on it, hoping to provide new cool features, improvements
Why are we developing an SQL client when there are a lot of them on the market? Why are we developing an SQL client when there are a lot of them on the market?
The main goal is to develop a **forever 100% free (without paid premium feature)**, full featured, as possible community driven, cross platform and open source alternative, empowered by JavaScript ecosystem. The main goal is to develop a **forever 100% free (without paid premium feature)**, full featured, as possible community driven, cross platform and open source alternative, empowered by JavaScript ecosystem.
A modern application created with minimalism and simplicity in mind, with features in the right places, not hundreds of tiny buttons, nested tabs or submenues; productivity comes first. A modern application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons, nested tabs or submenu; productivity comes first.
## Installation ## Installation
Based on your operating system you can have one or more distribution formats to choose based on your preferences. Based on your operating system you can have one or more distribution formats to choose based on your preferences.
Since Antares SQL is a free software we don't have a budget to spend on annual licenses or certificates. This can result that on some platforms you might need to put in some additional work to install this app. Since Antares SQL is a free software we haven't a budget to spend in annual licenses or certificates. This can result that on some platforms you need some additional passages to install this app.
### Linux ### Linux
On Linux you can simply download and run the `.AppImage` distribution, install from Snap Store, from AUR or from our [PPA repository](https://github.com/antares-sql/antares-ppa). On Linux you can simply download and run `.AppImage` distributions, install from Snap Store or from AUR.
### Windows ### Windows
On Windows you can choose between downloading the app from Microsoft Store or downloading the `.exe` from our [website](https://antares-sql.app/downloads) or [this github repo](https://github.com/Fabio286/antares/releases/latest). Distributions that are not from Microsoft Store are not signed with a certificate, so to install you need to click on "More info" and then "Run anyway" on SmartScreen prompt. On Windows you can choose between Microsoft Store and download `.exe` distribution. The latter lacks of a certificate, so to install you need to click on "More info" and then "Run anyway" on SmartScreen prompt.
### MacOS ### MacOS
@@ -63,7 +61,7 @@ On macOS you can run `.dmg` distribution following [this guide](https://support.
## Download ## Download
[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/antares) [![Get it from AUR](https://raw.githubusercontent.com/Fabio286/antares/3e00c4bae6e036300c752c1a40c5a038fea9c169/docs/aur-badge.svg)](https://aur.archlinux.org/packages/antares-sql/) [<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) [![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/antares) [![Get it from AUR](https://raw.githubusercontent.com/Fabio286/antares/3e00c4bae6e036300c752c1a40c5a038fea9c169/docs/aur-badge.svg)](https://aur.archlinux.org/packages/antares-sql/) [![Get it from Microsoft Store](https://raw.githubusercontent.com/Fabio286/antares/gh-pages/src/assets/ms-store.png)](https://www.microsoft.com/p/antares-sql-client/9nhtb9sq51r1?cid=storebadge&ocid=badge&rtc=1&activetab=pivot:overviewtab)
🚀 **[Other Downloads](https://github.com/Fabio286/antares/releases/latest)** 🚀 **[Other Downloads](https://github.com/Fabio286/antares/releases/latest)**
## Coming soon ## Coming soon
@@ -84,8 +82,8 @@ This is a roadmap with major features will come in near future.
- [x] MySQL/MariaDB - [x] MySQL/MariaDB
- [x] PostgreSQL - [x] PostgreSQL
- [x] SQLite - [x] SQLite
- [x] Firebird SQL - [ ] MSSQL
- [ ] SQL Server - [ ] OracleDB
- [ ] More... - [ ] More...
### Operating Systems ### Operating Systems
@@ -116,34 +114,30 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
<!-- markdownlint-disable --> <!-- markdownlint-disable -->
<table> <table>
<tbody> <tr>
<tr> <td align="center"><a href="https://fabiodistasio.it/"><img src="https://avatars.githubusercontent.com/u/31471771?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Fabio Di Stasio</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=Fabio286" title="Code">💻</a> <a href="#translation-Fabio286" title="Translation">🌍</a> <a href="https://github.com/antares-sql/antares/commits?author=Fabio286" title="Documentation">📖</a></td>
<td align="center"><a href="https://fabiodistasio.it/"><img src="https://avatars.githubusercontent.com/u/31471771?v=4?s=100" width="100px;" alt="Fabio Di Stasio"/><br /><sub><b>Fabio Di Stasio</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=Fabio286" title="Code">💻</a> <a href="#translation-Fabio286" title="Translation">🌍</a> <a href="https://github.com/antares-sql/antares/commits?author=Fabio286" title="Documentation">📖</a></td> <td align="center"><a href="https://www.linkedin.com/in/giulioganci/"><img src="https://avatars.githubusercontent.com/u/4192159?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Giulio Ganci</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=toriphes" title="Code">💻</a></td>
<td align="center"><a href="https://www.linkedin.com/in/giulioganci/"><img src="https://avatars.githubusercontent.com/u/4192159?v=4?s=100" width="100px;" alt="Giulio Ganci"/><br /><sub><b>Giulio Ganci</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=toriphes" title="Code">💻</a></td> <td align="center"><a href="https://christianratz.de/"><img src="https://avatars.githubusercontent.com/u/2630316?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Christian Ratz</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=digitalgopnik" title="Code">💻</a> <a href="#translation-digitalgopnik" title="Translation">🌍</a></td>
<td align="center"><a href="https://christianratz.de/"><img src="https://avatars.githubusercontent.com/u/2630316?v=4?s=100" width="100px;" alt="Christian Ratz"/><br /><sub><b>Christian Ratz</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=digitalgopnik" title="Code">💻</a> <a href="#translation-digitalgopnik" title="Translation">🌍</a></td> <td align="center"><a href="https://reverb6821.github.io/"><img src="https://avatars.githubusercontent.com/u/55198803?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Giuseppe Gigliotti</b></sub></a><br /><a href="#translation-reverb6821" title="Translation">🌍</a></td>
<td align="center"><a href="https://reverb6821.github.io/"><img src="https://avatars.githubusercontent.com/u/55198803?v=4?s=100" width="100px;" alt="Giuseppe Gigliotti"/><br /><sub><b>Giuseppe Gigliotti</b></sub></a><br /><a href="#translation-reverb6821" title="Translation">🌍</a></td> <td align="center"><a href="https://github.com/Mohd-PH"><img src="https://avatars.githubusercontent.com/u/9362157?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mohd-PH</b></sub></a><br /><a href="#translation-Mohd-PH" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/Mohd-PH"><img src="https://avatars.githubusercontent.com/u/9362157?v=4?s=100" width="100px;" alt="Mohd-PH"/><br /><sub><b>Mohd-PH</b></sub></a><br /><a href="#translation-Mohd-PH" title="Translation">🌍</a></td> <td align="center"><a href="https://github.com/hongkfui"><img src="https://avatars.githubusercontent.com/u/37477191?v=4?s=100" width="100px;" alt=""/><br /><sub><b>hongkfui</b></sub></a><br /><a href="#translation-hongkfui" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/hongkfui"><img src="https://avatars.githubusercontent.com/u/37477191?v=4?s=100" width="100px;" alt="hongkfui"/><br /><sub><b>hongkfui</b></sub></a><br /><a href="#translation-hongkfui" title="Translation">🌍</a></td> <td align="center"><a href="https://github.com/MrAnyx"><img src="https://avatars.githubusercontent.com/u/44176707?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Robin</b></sub></a><br /><a href="#translation-MrAnyx" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/MrAnyx"><img src="https://avatars.githubusercontent.com/u/44176707?v=4?s=100" width="100px;" alt="Robin"/><br /><sub><b>Robin</b></sub></a><br /><a href="#translation-MrAnyx" title="Translation">🌍</a></td> </tr>
</tr> <tr>
<tr> <td align="center"><a href="https://github.com/daeleduardo"><img src="https://avatars.githubusercontent.com/u/8599078?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Daniel Eduardo</b></sub></a><br /><a href="#translation-daeleduardo" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/daeleduardo"><img src="https://avatars.githubusercontent.com/u/8599078?v=4?s=100" width="100px;" alt="Daniel Eduardo"/><br /><sub><b>Daniel Eduardo</b></sub></a><br /><a href="#translation-daeleduardo" title="Translation">🌍</a></td> <td align="center"><a href="https://ngoquocdat.com/"><img src="https://avatars.githubusercontent.com/u/56961917?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ngô Quốc Đạt</b></sub></a><br /><a href="#translation-datlechin" title="Translation">🌍</a></td>
<td align="center"><a href="https://ngoquocdat.com/"><img src="https://avatars.githubusercontent.com/u/56961917?v=4?s=100" width="100px;" alt="Ngô Quốc Đạt"/><br /><sub><b>Ngô Quốc Đạt</b></sub></a><br /><a href="#translation-datlechin" title="Translation">🌍</a></td> <td align="center"><a href="https://github.com/IsamuSugi"><img src="https://avatars.githubusercontent.com/u/7746658?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Isamu Sugiura</b></sub></a><br /><a href="#translation-IsamuSugi" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/IsamuSugi"><img src="https://avatars.githubusercontent.com/u/7746658?v=4?s=100" width="100px;" alt="Isamu Sugiura"/><br /><sub><b>Isamu Sugiura</b></sub></a><br /><a href="#translation-IsamuSugi" title="Translation">🌍</a></td> <td align="center"><a href="http://rsacchetto.nexxontech.it/"><img src="https://avatars.githubusercontent.com/u/18429412?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Riccardo Sacchetto</b></sub></a><br /><a href="#platform-Occhioverde" title="Packaging/porting to new platform">📦</a></td>
<td align="center"><a href="http://rsacchetto.nexxontech.it/"><img src="https://avatars.githubusercontent.com/u/18429412?v=4?s=100" width="100px;" alt="Riccardo Sacchetto"/><br /><sub><b>Riccardo Sacchetto</b></sub></a><br /><a href="#platform-Occhioverde" title="Packaging/porting to new platform">📦</a></td> <td align="center"><a href="https://kilianstallinger.com"><img src="https://avatars.githubusercontent.com/u/5290318?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kilian Stallinger</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=kilianstallz" title="Code">💻</a></td>
<td align="center"><a href="https://kilianstallinger.com"><img src="https://avatars.githubusercontent.com/u/5290318?v=4?s=100" width="100px;" alt="Kilian Stallinger"/><br /><sub><b>Kilian Stallinger</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=kilianstallz" title="Code">💻</a></td> <td align="center"><a href="https://github.com/wenj91"><img src="https://avatars.githubusercontent.com/u/12549338?v=4?s=100" width="100px;" alt=""/><br /><sub><b>文杰</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=wenj91" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/wenj91"><img src="https://avatars.githubusercontent.com/u/12549338?v=4?s=100" width="100px;" alt="文杰"/><br /><sub><b>文杰</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=wenj91" title="Code">💻</a></td> <td align="center"><a href="https://github.com/goYou"><img src="https://avatars.githubusercontent.com/u/62732795?v=4?s=100" width="100px;" alt=""/><br /><sub><b>goYou</b></sub></a><br /><a href="#translation-goYou" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/goYou"><img src="https://avatars.githubusercontent.com/u/62732795?v=4?s=100" width="100px;" alt="goYou"/><br /><sub><b>goYou</b></sub></a><br /><a href="#translation-goYou" title="Translation">🌍</a></td> </tr>
</tr> <tr>
<tr> <td align="center"><a href="https://github.com/raliqala"><img src="https://avatars.githubusercontent.com/u/30502407?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Topollo</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=raliqala" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/raliqala"><img src="https://avatars.githubusercontent.com/u/30502407?v=4?s=100" width="100px;" alt="Topollo"/><br /><sub><b>Topollo</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=raliqala" title="Code">💻</a></td> <td align="center"><a href="https://github.com/SmileYzn"><img src="https://avatars.githubusercontent.com/u/5851851?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Cleverson</b></sub></a><br /><a href="#translation-SmileYzn" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/SmileYzn"><img src="https://avatars.githubusercontent.com/u/5851851?v=4?s=100" width="100px;" alt="Cleverson"/><br /><sub><b>Cleverson</b></sub></a><br /><a href="#translation-SmileYzn" title="Translation">🌍</a></td> <td align="center"><a href="https://github.com/fredatgithub"><img src="https://avatars.githubusercontent.com/u/6720055?v=4?s=100" width="100px;" alt=""/><br /><sub><b>fred</b></sub></a><br /><a href="#translation-fredatgithub" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/fredatgithub"><img src="https://avatars.githubusercontent.com/u/6720055?v=4?s=100" width="100px;" alt="fred"/><br /><sub><b>fred</b></sub></a><br /><a href="#translation-fredatgithub" title="Translation">🌍</a></td> <td align="center"><a href="https://github.com/xak666"><img src="https://avatars.githubusercontent.com/u/38811437?v=4?s=100" width="100px;" alt=""/><br /><sub><b>xaka_xak</b></sub></a><br /><a href="#translation-xak666" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/xak666"><img src="https://avatars.githubusercontent.com/u/38811437?v=4?s=100" width="100px;" alt="xaka_xak"/><br /><sub><b>xaka_xak</b></sub></a><br /><a href="#translation-xak666" title="Translation">🌍</a></td> </tr>
<td align="center"><a href="https://codepen.io/theo-billardey"><img src="https://avatars.githubusercontent.com/u/48206778?v=4?s=100" width="100px;" alt="Théo Billardey"/><br /><sub><b>Théo Billardey</b></sub></a><br /><a href="#translation-brdtheo" title="Translation">🌍</a></td>
<td align="center"><a href="http://yaskur.net"><img src="https://avatars.githubusercontent.com/u/9539970?v=4?s=100" width="100px;" alt="Muhammad Dyas Yaskur"/><br /><sub><b>Muhammad Dyas Yaskur</b></sub></a><br /><a href="#translation-dyaskur" title="Translation">🌍</a> <a href="https://github.com/antares-sql/antares/commits?author=dyaskur" title="Code">💻</a></td>
</tr>
</tbody>
</table> </table>
<!-- markdownlint-restore --> <!-- markdownlint-restore -->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 889 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

23642
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.4", "version": "0.5.5",
"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",
@@ -12,17 +12,18 @@
"compile:main": "webpack --mode=production --config webpack.main.config.js", "compile:main": "webpack --mode=production --config webpack.main.config.js",
"compile:workers": "webpack --mode=production --config webpack.workers.config.js", "compile:workers": "webpack --mode=production --config webpack.workers.config.js",
"compile:renderer": "webpack --mode=production --config webpack.renderer.config.js", "compile:renderer": "webpack --mode=production --config webpack.renderer.config.js",
"build": "cross-env NODE_ENV=production npm run compile && electron-builder --publish never", "build": "cross-env NODE_ENV=production npm run compile",
"build:appx": "npm run build -- --win appx", "build:local": "npm run build && electron-builder",
"rebuild:electron": "rimraf ./dist && npm run postinstall && npm run devtools:install", "build:appx": "npm run build:local -- --win appx",
"rebuild:electron": "rimraf ./dist && npm run postinstall",
"release": "standard-version", "release": "standard-version",
"release:pre": "npm run release -- --prerelease alpha", "release:pre": "npm run release -- --prerelease alpha",
"devtools:install": "node scripts/devtoolsInstaller", "devtools:install": "node scripts/devtoolsInstaller",
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps && npm run devtools:install",
"test:e2e": "npm run compile && npm run test:e2e-dry", "test:e2e": "npm run compile && npm run test:e2e-dry",
"test:e2e-dry": "xvfb-maybe -- playwright test", "test:e2e-dry": "xvfb-maybe -- playwright test",
"lint": "eslint . --ext .js,.ts,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"", "lint": "eslint . --ext .js,.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,.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"
}, },
@@ -64,11 +65,7 @@
"target": [ "target": [
{ {
"target": "deb", "target": "deb",
"arch": [ "arch": "x64"
"x64",
"armv7l",
"arm64"
]
}, },
{ {
"target": "AppImage", "target": "AppImage",
@@ -79,19 +76,12 @@
] ]
} }
], ],
"icon": "assets/linux",
"category": "Development" "category": "Development"
}, },
"appImage": { "appImage": {
"license": "./LICENSE", "license": "./LICENSE",
"category": "Development" "category": "Development"
}, },
"nsis": {
"license": "./LICENSE",
"installerIcon": "assets/icon.ico",
"uninstallerIcon": "assets/icon.ico",
"installerHeader": "assets/icon.ico"
},
"portable": { "portable": {
"artifactName": "${productName}-${version}-portable.exe" "artifactName": "${productName}-${version}-portable.exe"
}, },
@@ -121,47 +111,41 @@
"dependencies": { "dependencies": {
"@electron/remote": "~2.0.1", "@electron/remote": "~2.0.1",
"@faker-js/faker": "~6.1.2", "@faker-js/faker": "~6.1.2",
"@mdi/font": "~7.1.96", "@mdi/font": "~6.1.95",
"@turf/helpers": "~6.5.0", "@turf/helpers": "~6.5.0",
"@vueuse/core": "~8.7.5", "@vscode/vscode-languagedetection": "~1.0.21",
"ace-builds": "~1.14.0", "ace-builds": "~1.4.13",
"better-sqlite3": "~8.0.0", "better-sqlite3": "~7.5.0",
"electron-log": "~4.4.1", "electron-log": "~4.4.1",
"electron-store": "~8.1.0", "electron-store": "~8.0.1",
"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",
"floating-vue": "~2.0.0-beta.20",
"leaflet": "~1.7.1", "leaflet": "~1.7.1",
"marked": "~4.0.19", "marked": "~4.0.0",
"moment": "~2.29.4", "moment": "~2.29.1",
"mysql2": "~2.3.2", "mysql2": "~2.3.2",
"node-firebird": "~1.1.4",
"pg": "~8.7.1", "pg": "~8.7.1",
"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.0.28", "pinia": "~2.0.13",
"source-map-support": "~0.5.20", "source-map-support": "~0.5.20",
"spectre.css": "~0.5.9", "spectre.css": "~0.5.9",
"sql-formatter": "~12.0.3", "sql-formatter": "~4.0.2",
"ssh2-promise": "~1.0.2", "ssh2-promise": "~1.0.2",
"v-mask": "~2.3.0", "v-mask": "~2.3.0",
"vue": "~3.2.45", "vue": "~3.2.33",
"vue-i18n": "~9.2.2", "vue-i18n": "~9.1.9",
"vuedraggable": "~4.1.0" "vuedraggable": "~4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "~7.15.7", "@babel/eslint-parser": "~7.15.7",
"@babel/preset-env": "~7.15.8", "@babel/preset-env": "~7.15.8",
"@babel/preset-typescript": "~7.16.7", "@babel/preset-typescript": "~7.16.7",
"@playwright/test": "~1.28.1", "@playwright/test": "~1.21.1",
"@types/better-sqlite3": "~7.5.0", "@types/better-sqlite3": "~7.5.0",
"@types/leaflet": "~1.7.9",
"@types/marked": "~4.0.7",
"@types/node": "~17.0.23", "@types/node": "~17.0.23",
"@types/pg": "~8.6.5", "@types/pg": "~8.6.5",
"@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", "@vue/compiler-sfc": "~3.2.33",
@@ -170,8 +154,8 @@
"chalk": "~4.1.2", "chalk": "~4.1.2",
"cross-env": "~7.0.2", "cross-env": "~7.0.2",
"css-loader": "~6.5.0", "css-loader": "~6.5.0",
"electron": "~22.0.3", "electron": "~17.4.3",
"electron-builder": "~22.10.3", "electron-builder": "~23.0.3",
"eslint": "~7.32.0", "eslint": "~7.32.0",
"eslint-config-standard": "~16.0.3", "eslint-config-standard": "~16.0.3",
"eslint-plugin-import": "~2.24.2", "eslint-plugin-import": "~2.24.2",
@@ -182,33 +166,26 @@
"html-webpack-plugin": "~5.5.0", "html-webpack-plugin": "~5.5.0",
"mini-css-extract-plugin": "~2.4.5", "mini-css-extract-plugin": "~2.4.5",
"node-loader": "~2.0.0", "node-loader": "~2.0.0",
"playwright": "~1.28.1", "playwright": "~1.21.1",
"playwright-core": "~1.28.1", "playwright-core": "~1.21.1",
"postcss-html": "~1.5.0",
"progress-webpack-plugin": "~1.0.12", "progress-webpack-plugin": "~1.0.12",
"rimraf": "~3.0.2", "rimraf": "~3.0.2",
"sass": "~1.42.1", "sass": "~1.42.1",
"sass-loader": "~12.3.0", "sass-loader": "~12.3.0",
"standard-version": "~9.3.1", "standard-version": "~9.3.1",
"style-loader": "~3.3.1", "style-loader": "~3.3.1",
"stylelint": "~14.9.1", "stylelint": "~13.13.1",
"stylelint-config-recommended-vue": "~1.4.0", "stylelint-config-standard": "~22.0.0",
"stylelint-config-standard": "~26.0.0", "stylelint-scss": "~3.21.0",
"stylelint-scss": "~4.3.0",
"tree-kill": "~1.2.2", "tree-kill": "~1.2.2",
"ts-loader": "~9.2.8", "ts-loader": "~9.2.8",
"typescript": "~4.6.3", "typescript": "~4.6.3",
"unzip-crx-3": "~0.2.0", "unzip-crx-3": "~0.2.0",
"vue-eslint-parser": "~8.3.0", "vue-eslint-parser": "~8.3.0",
"vue-loader": "~16.8.3", "vue-loader": "~16.8.3",
"webpack": "~5.72.0", "webpack": "~5.60.0",
"webpack-cli": "~4.9.1", "webpack-cli": "~4.9.1",
"webpack-dev-server": "~4.11.1", "webpack-dev-server": "~4.4.0",
"xvfb-maybe": "~0.2.1" "xvfb-maybe": "~0.2.1"
},
"overrides": {
"ssh2-promise": {
"ssh2": "github:Fabio286/ssh2"
}
} }
} }

View File

@@ -59,7 +59,7 @@ async function restartElectron () {
console.error(chalk.red(data.toString())); console.error(chalk.red(data.toString()));
}); });
electronProcess.on('exit', () => { electronProcess.on('exit', (code, signal) => {
if (!manualRestart) process.exit(0); if (!manualRestart) process.exit(0);
}); });
} }
@@ -114,6 +114,7 @@ function startRenderer (callback) {
}); });
const server = new WebpackDevServer(compiler, { const server = new WebpackDevServer(compiler, {
hot: true,
port: 9080, port: 9080,
client: { client: {
overlay: true, overlay: true,

View File

@@ -1,5 +1,4 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-check
// @ts-nocheck
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const https = require('https'); const https = require('https');

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export default class { export default class {
static get _methods () { static get _methods () {
return [ return [
@@ -140,7 +139,7 @@ export default class {
{ name: 'arrayElement', group: 'random', types: ['string'] }, { name: 'arrayElement', group: 'random', types: ['string'] },
{ name: 'arrayElements', group: 'random', types: ['string'] }, { name: 'arrayElements', group: 'random', types: ['string'] },
{ name: 'objectElement', group: 'random', types: ['string'] }, { name: 'objectElement', group: 'random', types: ['string'] },
{ name: 'uuid', group: 'random', types: ['string', 'uuid'] }, { name: 'uuid', group: 'random', types: ['string'] },
{ name: 'boolean', group: 'random', types: ['string'] }, { name: 'boolean', group: 'random', types: ['string'] },
{ name: 'word', group: 'random', types: ['string'] }, { name: 'word', group: 'random', types: ['string'] },
{ name: 'words', group: 'random', types: ['string'] }, { name: 'words', group: 'random', types: ['string'] },
@@ -181,7 +180,7 @@ export default class {
acc[curr.group] = new Set(curr.types); acc[curr.group] = new Set(curr.types);
return acc; return acc;
}, {} as any); }, {});
const groupsArr = []; const groupsArr = [];
@@ -199,12 +198,12 @@ export default class {
}); });
} }
static getGroupsByType (type: string) { static getGroupsByType (type) {
if (!type) return []; if (!type) return [];
return this.getGroups().filter(group => group.types.includes(type)); return this.getGroups().filter(group => group.types.includes(type));
} }
static getMethods ({ type, group }: {type: string; group: string}) { static getMethods ({ type, group }) {
return this._methods.filter(method => method.group === group && method.types.includes(type)).sort((a, b) => { return this._methods.filter(method => method.group === group && method.types.includes(type)).sort((a, b) => {
if (a.name < b.name) if (a.name < b.name)
return -1; return -1;

View File

@@ -1,14 +1,8 @@
import { Customizations } from '../interfaces/customizations'; module.exports = {
// Everything OFF
export const defaults: Customizations = {
// Defaults // Defaults
defaultPort: null, defaultPort: null,
defaultUser: null, defaultUser: null,
defaultDatabase: null, defaultDatabase: null,
dataTypes: [],
indexTypes: [],
foreignActions: [],
// Core // Core
database: false, database: false,
collations: false, collations: false,
@@ -35,7 +29,6 @@ export const defaults: Customizations = {
elementsWrapper: '', elementsWrapper: '',
stringsWrapper: '"', stringsWrapper: '"',
tableAdd: false, tableAdd: false,
tableTruncateDisableFKCheck: false,
viewAdd: false, viewAdd: false,
triggerAdd: false, triggerAdd: false,
triggerFunctionAdd: false, triggerFunctionAdd: false,
@@ -49,9 +42,9 @@ export const defaults: Customizations = {
exportByChunks: false, exportByChunks: false,
schemaImport: false, schemaImport: false,
tableSettings: false, tableSettings: false,
tableOptions: false,
tableArray: false, tableArray: false,
tableRealCount: false, tableRealCount: false,
tableDuplicate: false,
viewSettings: false, viewSettings: false,
triggerSettings: false, triggerSettings: false,
triggerFunctionSettings: false, triggerFunctionSettings: false,
@@ -75,25 +68,24 @@ export const defaults: Customizations = {
viewUpdateOption: false, viewUpdateOption: false,
procedureDeterministic: false, procedureDeterministic: false,
procedureDataAccess: false, procedureDataAccess: false,
procedureSql: null, procedureSql: false,
procedureContext: false, procedureContext: false,
procedureContextValues: [],
procedureLanguage: false, procedureLanguage: false,
functionDeterministic: false, functionDeterministic: false,
functionDataAccess: false, functionDataAccess: false,
functionSql: null, functionSql: false,
functionContext: false, functionContext: false,
functionLanguage: false, functionLanguage: false,
triggerSql: null, triggerSql: false,
triggerStatementInCreation: false, triggerStatementInCreation: false,
triggerMultipleEvents: false, triggerMultipleEvents: false,
triggerTableInName: false, triggerTableInName: false,
triggerUpdateColumns: false, triggerUpdateColumns: false,
triggerOnlyRename: false, triggerOnlyRename: false,
triggerEnableDisable: false, triggerEnableDisable: false,
triggerFunctionSql: null, triggerFunctionSql: false,
triggerFunctionlanguages: null, triggerFunctionlanguages: false,
parametersLength: false, parametersLength: false,
languages: null, languages: false,
readOnlyMode: false readOnlyMode: false
}; };

View File

@@ -1,63 +0,0 @@
import { Customizations } from '../interfaces/customizations';
import { defaults } from './defaults';
import firebirdTypes from '../data-types/firebird';
export const customizations: Customizations = {
...defaults,
// Defaults
defaultPort: 3050,
defaultUser: 'SYSDBA',
defaultDatabase: null,
dataTypes: firebirdTypes,
indexTypes: [
'PRIMARY',
// 'CHECK',
'UNIQUE'
],
foreignActions: [
'RESTRICT',
'NO ACTION',
'CASCADE',
'SET NULL',
'SET DEFAULT'
],
// Core
database: true,
collations: false,
engines: false,
connectionSchema: false,
sslConnection: false,
sshConnection: false,
fileConnection: false,
cancelQueries: false,
// Tools
processesList: false,
usersManagement: false,
variables: false,
// Structure
schemas: false,
tables: true,
views: true,
triggers: true,
routines: true,
functions: false,
// Settings
elementsWrapper: '"',
stringsWrapper: '\'',
tableAdd: true,
tableSettings: true,
tableRealCount: true,
viewAdd: true,
viewSettings: true,
triggerAdd: true,
triggerMultipleEvents: true,
triggerSql: 'BEGIN\r\n\r\nEND',
routineAdd: true,
procedureContext: true,
procedureContextValues: ['IN', 'OUT'],
procedureSql: 'BEGIN\r\n\r\nEND',
parametersLength: true,
indexes: true,
foreigns: true,
nullable: true
};

View File

@@ -0,0 +1,6 @@
module.exports = {
maria: require('./mysql'),
mysql: require('./mysql'),
pg: require('./postgresql'),
sqlite: require('./sqlite')
};

View File

@@ -1,19 +0,0 @@
import * as mysql from 'common/customizations/mysql';
import * as postgresql from 'common/customizations/postgresql';
import * as sqlite from 'common/customizations/sqlite';
import * as firebird from 'common/customizations/firebird';
import { Customizations } from 'common/interfaces/customizations';
export default {
maria: mysql.customizations,
mysql: mysql.customizations,
pg: postgresql.customizations,
sqlite: sqlite.customizations,
firebird: firebird.customizations
} as {
maria: Customizations;
mysql: Customizations;
pg: Customizations;
sqlite: Customizations;
firebird: Customizations;
};

View File

@@ -1,26 +1,11 @@
import { Customizations } from '../interfaces/customizations'; const defaults = require('./defaults');
import { defaults } from './defaults';
import mysqlTypes from '../data-types/mysql';
export const customizations: Customizations = { module.exports = {
...defaults, ...defaults,
// Defaults // Defaults
defaultPort: 3306, defaultPort: 3306,
defaultUser: 'root', defaultUser: 'root',
defaultDatabase: null, defaultDatabase: null,
dataTypes: mysqlTypes,
indexTypes: [
'PRIMARY',
'INDEX',
'UNIQUE',
'FULLTEXT'
],
foreignActions: [
'RESTRICT',
'CASCADE',
'SET NULL',
'NO ACTION'
],
// Core // Core
connectionSchema: true, connectionSchema: true,
collations: true, collations: true,
@@ -39,11 +24,9 @@ export const customizations: Customizations = {
functions: true, functions: true,
schedulers: true, schedulers: true,
// Settings // Settings
elementsWrapper: '`', elementsWrapper: '',
stringsWrapper: '"', stringsWrapper: '"',
tableAdd: true, tableAdd: true,
tableTruncateDisableFKCheck: true,
tableDuplicate: true,
viewAdd: true, viewAdd: true,
triggerAdd: true, triggerAdd: true,
routineAdd: true, routineAdd: true,
@@ -66,6 +49,7 @@ export const customizations: Customizations = {
unsigned: true, unsigned: true,
nullable: true, nullable: true,
zerofill: true, zerofill: true,
tableOptions: true,
autoIncrement: true, autoIncrement: true,
comment: true, comment: true,
collation: true, collation: true,
@@ -78,7 +62,6 @@ export const customizations: Customizations = {
procedureDataAccess: true, procedureDataAccess: true,
procedureSql: 'BEGIN\r\n\r\nEND', procedureSql: 'BEGIN\r\n\r\nEND',
procedureContext: true, procedureContext: true,
procedureContextValues: ['IN', 'OUT', 'INOUT'],
triggerSql: 'BEGIN\r\n\r\nEND', triggerSql: 'BEGIN\r\n\r\nEND',
functionDeterministic: true, functionDeterministic: true,
functionDataAccess: true, functionDataAccess: true,

View File

@@ -1,25 +1,11 @@
import { Customizations } from '../interfaces/customizations'; const defaults = require('./defaults');
import { defaults } from './defaults';
import postgresqlTypes from '../data-types/postgresql';
export const customizations: Customizations = { module.exports = {
...defaults, ...defaults,
// Defaults // Defaults
defaultPort: 5432, defaultPort: 5432,
defaultUser: 'postgres', defaultUser: 'postgres',
defaultDatabase: 'postgres', defaultDatabase: 'postgres',
dataTypes: postgresqlTypes,
indexTypes: [
'PRIMARY',
'INDEX',
'UNIQUE'
],
foreignActions: [
'RESTRICT',
'CASCADE',
'SET NULL',
'NO ACTION'
],
// Core // Core
database: true, database: true,
sslConnection: true, sslConnection: true,
@@ -39,7 +25,6 @@ export const customizations: Customizations = {
elementsWrapper: '"', elementsWrapper: '"',
stringsWrapper: '\'', stringsWrapper: '\'',
tableAdd: true, tableAdd: true,
tableDuplicate: true,
viewAdd: true, viewAdd: true,
triggerAdd: true, triggerAdd: true,
triggerFunctionAdd: true, triggerFunctionAdd: true,
@@ -61,7 +46,6 @@ export const customizations: Customizations = {
tableArray: true, tableArray: true,
procedureSql: '$procedure$\r\n\r\n$procedure$', procedureSql: '$procedure$\r\n\r\n$procedure$',
procedureContext: true, procedureContext: true,
procedureContextValues: ['IN', 'OUT', 'INOUT'],
procedureLanguage: true, procedureLanguage: true,
functionSql: '$function$\r\n\r\n$function$', functionSql: '$function$\r\n\r\n$function$',
triggerFunctionSql: '$function$\r\nBEGIN\r\n\r\nEND\r\n$function$', triggerFunctionSql: '$function$\r\nBEGIN\r\n\r\nEND\r\n$function$',

View File

@@ -1,21 +1,4 @@
import { Customizations } from '../interfaces/customizations'; module.exports = {
import { defaults } from './defaults';
import sqliteTypes from '../data-types/sqlite';
export const customizations: Customizations = {
...defaults,
dataTypes: sqliteTypes,
indexTypes: [
'PRIMARY',
'INDEX',
'UNIQUE'
],
foreignActions: [
'RESTRICT',
'CASCADE',
'SET NULL',
'NO ACTION'
],
// Core // Core
fileConnection: true, fileConnection: true,
// Structure // Structure
@@ -27,7 +10,6 @@ export const customizations: Customizations = {
elementsWrapper: '"', elementsWrapper: '"',
stringsWrapper: '\'', stringsWrapper: '\'',
tableAdd: true, tableAdd: true,
tableDuplicate: true,
viewAdd: true, viewAdd: true,
triggerAdd: true, triggerAdd: true,
schemaEdit: false, schemaEdit: false,

View File

@@ -1,136 +0,0 @@
import { TypesGroup } from 'common/interfaces/antares';
export default [
{
group: 'integer',
types: [
{
name: 'SMALLINT',
length: false,
collation: false,
unsigned: true,
zerofill: true
},
{
name: 'INTEGER',
length: false,
collation: false,
unsigned: true,
zerofill: true
},
{
name: 'BIGINT',
length: false,
collation: false,
unsigned: true,
zerofill: true
}
]
},
{
group: 'float',
types: [
{
name: 'DECIMAL',
length: true,
scale: true,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'NUMERIC',
length: true,
scale: true,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'FLOAT',
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'DOUBLE PRECISION',
length: false,
collation: false,
unsigned: false,
zerofill: false
}
]
},
{
group: 'string',
types: [
{
name: 'CHAR',
length: true,
collation: true,
unsigned: false,
zerofill: false
},
{
name: 'VARCHAR',
length: true,
collation: true,
unsigned: false,
zerofill: false
},
{
name: 'BLOB-TEXT',
length: false,
collation: true,
unsigned: false,
zerofill: false
}
]
},
{
group: 'binary',
types: [
{
name: 'BLOB',
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'CHAR-BINARY',
length: false,
collation: false,
unsigned: false,
zerofill: false
}
]
},
{
group: 'time',
types: [
{
name: 'DATE',
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'TIME',
length: true,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'TIMESTAMP',
length: false,
collation: false,
unsigned: false,
zerofill: false
}
]
}
] as TypesGroup[];

View File

@@ -1,6 +1,4 @@
import { TypesGroup } from 'common/interfaces/antares'; module.exports = [
export default [
{ {
group: 'integer', group: 'integer',
types: [ types: [
@@ -308,4 +306,4 @@ export default [
} }
] ]
} }
] as TypesGroup[]; ];

View File

@@ -1,6 +1,4 @@
import { TypesGroup } from 'common/interfaces/antares'; module.exports = [
export default [
{ {
group: 'integer', group: 'integer',
types: [ types: [
@@ -292,4 +290,4 @@ export default [
} }
] ]
} }
] as TypesGroup[]; ];

View File

@@ -1,25 +1,16 @@
import { TypesGroup } from 'common/interfaces/antares'; module.exports = [
export default [
{ {
group: 'integer', group: 'integer',
types: [ types: [
{ {
name: 'INT', name: 'INT',
length: 10,
collation: false,
unsigned: true,
zerofill: true
},
{
name: 'INTEGER',
length: true, length: true,
collation: false, collation: false,
unsigned: true, unsigned: true,
zerofill: true zerofill: true
}, },
{ {
name: 'INTEGER UNSIGNED', name: 'INTEGER',
length: true, length: true,
collation: false, collation: false,
unsigned: true, unsigned: true,
@@ -143,4 +134,4 @@ export default [
} }
] ]
} }
] as TypesGroup[]; ];

View File

@@ -10,8 +10,7 @@ export const LONG_TEXT = [
'MEDIUMTEXT', 'MEDIUMTEXT',
'LONGTEXT', 'LONGTEXT',
'JSON', 'JSON',
'VARBINARY', 'VARBINARY'
'BLOB-TEXT'
]; ];
export const ARRAY = [ export const ARRAY = [
@@ -30,14 +29,14 @@ export const NUMBER = [
'SMALLINT', 'SMALLINT',
'MEDIUMINT', 'MEDIUMINT',
'BIGINT', 'BIGINT',
'DECIMAL',
'NUMERIC', 'NUMERIC',
'INTEGER', 'INTEGER',
'SMALLSERIAL', 'SMALLSERIAL',
'SERIAL', 'SERIAL',
'BIGSERIAL', 'BIGSERIAL',
'OID', 'OID',
'XID', 'XID'
'INT64'
]; ];
export const FLOAT = [ export const FLOAT = [
@@ -49,12 +48,6 @@ export const FLOAT = [
'MONEY' 'MONEY'
]; ];
export const IS_BIGINT = [
'BIGINT',
'BIGSERIAL',
'DOUBLE PRECISION'
];
export const BOOLEAN = [ export const BOOLEAN = [
'BOOL', 'BOOL',
'BOOLEAN' 'BOOLEAN'
@@ -85,9 +78,7 @@ export const BLOB = [
'TINYBLOB', 'TINYBLOB',
'MEDIUMBLOB', 'MEDIUMBLOB',
'LONGBLOB', 'LONGBLOB',
'LONG_BLOB', 'BYTEA'
'BYTEA',
'CHAR-BINARY'
]; ];
export const BIT = [ export const BIT = [
@@ -95,14 +86,6 @@ export const BIT = [
'BIT VARYING' 'BIT VARYING'
]; ];
export const BINARY = [
'BINARY'
];
export const UUID = [
'UUID'
];
export const SPATIAL = [ export const SPATIAL = [
'POINT', 'POINT',
'LINESTRING', 'LINESTRING',

View File

@@ -0,0 +1,6 @@
module.exports = [
'PRIMARY',
'INDEX',
'UNIQUE',
'FULLTEXT'
];

View File

@@ -0,0 +1,5 @@
module.exports = [
'PRIMARY',
'INDEX',
'UNIQUE'
];

View File

@@ -0,0 +1,5 @@
module.exports = [
'PRIMARY',
'INDEX',
'UNIQUE'
];

View File

@@ -8,25 +8,17 @@ import SSHConfig from 'ssh2-promise/lib/sshConfig';
import { MySQLClient } from '../../main/libs/clients/MySQLClient'; import { MySQLClient } from '../../main/libs/clients/MySQLClient';
import { PostgreSQLClient } from '../../main/libs/clients/PostgreSQLClient'; import { PostgreSQLClient } from '../../main/libs/clients/PostgreSQLClient';
import { SQLiteClient } from '../../main/libs/clients/SQLiteClient'; import { SQLiteClient } from '../../main/libs/clients/SQLiteClient';
import { FirebirdSQLClient } from 'src/main/libs/clients/FirebirdSQLClient';
export type Client = MySQLClient | PostgreSQLClient | SQLiteClient | FirebirdSQLClient export type Client = MySQLClient | PostgreSQLClient | SQLiteClient
export type ClientCode = 'mysql' | 'maria' | 'pg' | 'sqlite' | 'firebird' export type ClientCode = 'mysql' | 'maria' | 'pg' | 'sqlite'
export type Exporter = MysqlExporter | PostgreSQLExporter export type Exporter = MysqlExporter | PostgreSQLExporter
export type Importer = MySQLImporter | PostgreSQLImporter export type Importer = MySQLImporter | PostgreSQLImporter
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface IpcResponse<T = any> {
status: 'success' | 'error';
response?: T;
}
/** /**
* Pasameters needed to create a new Antares connection to a database * Pasameters needed to create a new Antares connection to a database
*/ */
export interface ClientParams { export interface ClientParams {
client: ClientCode; client: ClientCode;
uid?: string;
params: params:
mysql.ConnectionOptions & {schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean} 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}
@@ -75,32 +67,12 @@ export interface TypeInformations {
zerofill: boolean; zerofill: boolean;
} }
export interface TypesGroup {
group: string;
types: TypeInformations[];
}
// Tables // Tables
export interface TableInfos {
name: string;
type: string;
rows: number;
engine: string;
comment: string;
size: number | false;
collation: string;
autoIncrement?: boolean;
}
export type TableOptions = Partial<TableInfos>;
export interface TableField { export interface TableField {
// eslint-disable-next-line camelcase
_antares_id?: string;
name: string; name: string;
key: string;
type: string; type: string;
schema: string; schema: string;
table?: string;
numPrecision?: number; numPrecision?: number;
numLength?: number; numLength?: number;
datePrecision?: number; datePrecision?: number;
@@ -110,8 +82,7 @@ export interface TableField {
unsigned?: boolean; unsigned?: boolean;
zerofill?: boolean; zerofill?: boolean;
order?: number; order?: number;
default?: string; default?: number | string;
defaultType?: string;
enumValues?: string; enumValues?: string;
charset?: string; charset?: string;
collation?: string; collation?: string;
@@ -121,16 +92,9 @@ export interface TableField {
comment?: string; comment?: string;
after?: string; after?: string;
orgName?: string; orgName?: string;
length?: number | false;
alias: string;
tableAlias: string;
orgTable: string;
key?: 'pri' | 'uni' | '';
} }
export interface TableIndex { export interface TableIndex {
// eslint-disable-next-line camelcase
_antares_id?: string;
name: string; name: string;
fields: string[]; fields: string[];
type: string; type: string;
@@ -143,8 +107,6 @@ export interface TableIndex {
} }
export interface TableForeign { export interface TableForeign {
// eslint-disable-next-line camelcase
_antares_id?: string;
constraintName: string; constraintName: string;
refSchema: string; refSchema: string;
table: string; table: string;
@@ -156,6 +118,15 @@ export interface TableForeign {
oldName?: string; oldName?: string;
} }
export interface TableOptions {
name: string;
type?: 'table' | 'view';
engine?: string;
comment?: string;
collation?: string;
autoIncrement?: number;
}
export interface CreateTableParams { export interface CreateTableParams {
/** Connection UID */ /** Connection UID */
uid?: string; uid?: string;
@@ -194,7 +165,6 @@ export interface AlterTableParams {
} }
// Views // Views
export type ViewInfos = TableInfos
export interface CreateViewParams { export interface CreateViewParams {
schema: string; schema: string;
name: string; name: string;
@@ -210,19 +180,6 @@ export interface AlterViewParams extends CreateViewParams {
} }
// Triggers // Triggers
export interface TriggerInfos {
name: string;
statement: string;
timing: string;
definer: string;
event: string;
table: string;
sqlMode: string;
created: Date;
charset: string;
enabled?: boolean;
}
export interface CreateTriggerParams { export interface CreateTriggerParams {
definer?: string; definer?: string;
schema: string; schema: string;
@@ -238,38 +195,13 @@ export interface AlterTriggerParams extends CreateTriggerParams {
} }
// Routines & Functions // Routines & Functions
export interface FunctionParam { export interface FunctionParam {
// eslint-disable-next-line camelcase
_antares_id: string;
context: string; context: string;
name: string; name: string;
type: string; type: string;
length: number; length: number;
} }
export interface RoutineInfos {
name: string;
type?: string;
definer: string;
created?: string;
sql?: string;
updated?: string;
comment?: string;
charset?: string;
security?: string;
language?: string;
dataAccess?: string;
deterministic?: boolean;
parameters?: FunctionParam[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
returns?: any;
returnsLength?: number;
}
export type FunctionInfos = RoutineInfos
export type TriggerFunctionInfos = FunctionInfos
export interface CreateRoutineParams { export interface CreateRoutineParams {
name: string; name: string;
parameters?: FunctionParam[]; parameters?: FunctionParam[];
@@ -307,7 +239,7 @@ export interface AlterFunctionParams extends CreateFunctionParams {
} }
// Events // Events
export interface EventInfos { export interface CreateEventParams {
definer?: string; definer?: string;
schema: string; schema: string;
name: string; name: string;
@@ -316,39 +248,16 @@ export interface EventInfos {
starts: string; starts: string;
ends: string; ends: string;
at: string; at: string;
preserve: boolean; preserve: string;
state: string; state: string;
comment: string; comment: string;
enabled?: boolean;
sql: string; sql: string;
} }
export type CreateEventParams = EventInfos;
export interface AlterEventParams extends CreateEventParams { export interface AlterEventParams extends CreateEventParams {
oldName?: string; oldName?: string;
} }
// Schema
export interface SchemaInfos {
name: string;
size: number;
tables: TableInfos[];
functions: FunctionInfos[];
procedures: RoutineInfos[];
triggers: TriggerInfos[];
schedulers: EventInfos[];
}
export interface CollationInfos {
charset: string;
collation: string;
compiled: boolean;
default: boolean;
id: string | number;
sortLen: number;
}
// Query // Query
export interface QueryBuilderObject { export interface QueryBuilderObject {
schema: string; schema: string;
@@ -376,10 +285,17 @@ export interface QueryParams {
tabUid?: string; tabUid?: string;
} }
/** export interface QueryField {
* @deprecated Use TableFIeld name: string;
*/ alias: string;
export type QueryField = TableField orgName: string;
schema: string;
table: string;
tableAlias: string;
orgTable: string;
type: string;
length: number;
}
export interface QueryForeign { export interface QueryForeign {
schema: string; schema: string;

View File

@@ -1,98 +0,0 @@
import { TypesGroup } from './antares';
export interface Customizations {
// Defaults
defaultPort?: number;
defaultUser?: string;
defaultDatabase?: string;
dataTypes?: TypesGroup[];
indexTypes?: string[];
foreignActions?: string[];
// Core
database?: boolean;
collations?: boolean;
engines?: boolean;
connectionSchema?: boolean;
sslConnection?: boolean;
sshConnection?: boolean;
fileConnection?: boolean;
cancelQueries?: boolean;
// Tools
processesList?: boolean;
usersManagement?: boolean;
variables?: boolean;
// Structure
schemas?: boolean;
tables?: boolean;
views?: boolean;
triggers?: boolean;
triggerFunctions?: boolean;
routines?: boolean;
functions?: boolean;
schedulers?: boolean;
// Settings
elementsWrapper: string;
stringsWrapper: string;
tableAdd?: boolean;
tableSettings?: boolean;
tableDuplicate?: boolean;
tableArray?: boolean;
tableRealCount?: boolean;
tableTruncateDisableFKCheck?: boolean;
viewAdd?: boolean;
viewSettings?: boolean;
triggerAdd?: boolean;
triggerFunctionAdd?: boolean;
routineAdd?: boolean;
functionAdd?: boolean;
schedulerAdd?: boolean;
databaseEdit?: boolean;
schemaEdit?: boolean;
schemaDrop?: boolean;
schemaExport?: boolean;
exportByChunks?: boolean;
schemaImport?: boolean;
triggerSettings?: boolean;
triggerFunctionSettings?: boolean;
routineSettings?: boolean;
functionSettings?: boolean;
schedulerSettings?: boolean;
indexes?: boolean;
foreigns?: boolean;
sortableFields?: boolean;
unsigned?: boolean;
nullable?: boolean;
nullablePrimary?: boolean;
zerofill?: boolean;
autoIncrement?: boolean;
comment?: boolean;
collation?: boolean;
definer?: boolean;
onUpdate?: boolean;
viewAlgorithm?: boolean;
viewSqlSecurity?: boolean;
viewUpdateOption?: boolean;
procedureDeterministic?: boolean;
procedureDataAccess?: boolean;
procedureSql?: string;
procedureContext?: boolean;
procedureContextValues?: string[];
procedureLanguage?: boolean;
functionDeterministic?: boolean;
functionDataAccess?: boolean;
functionSql?: string;
functionContext?: boolean;
functionLanguage?: boolean;
triggerSql?: string;
triggerStatementInCreation?: boolean;
triggerMultipleEvents?: boolean;
triggerTableInName?: boolean;
triggerUpdateColumns?: boolean;
triggerOnlyRename?: boolean;
triggerEnableDisable?: boolean;
triggerFunctionSql?: string;
triggerFunctionlanguages?: string[];
parametersLength?: boolean;
languages?: string[];
readOnlyMode?: boolean;
}

View File

@@ -7,13 +7,13 @@ export interface TableParams {
export interface ExportOptions { export interface ExportOptions {
schema: string; schema: string;
tables: { includes: {
table: string; functions: boolean;
includeStructure: boolean; views: boolean;
includeContent: boolean; triggers: boolean;
includeDropStatement: boolean; routines: boolean;
}[]; schedulers: boolean;
includes: {[key: string]: boolean}; };
outputFormat: 'sql' | 'sql.zip'; outputFormat: 'sql' | 'sql.zip';
outputFile: string; outputFile: string;
sqlInsertAfter: number; sqlInsertAfter: number;

View File

@@ -1,34 +1,5 @@
import { UsableLocale } from '@faker-js/faker'; import { UsableLocale } from '@faker-js/faker';
export interface TableUpdateParams {
uid: string;
schema: string;
table: string;
primary?: string;
id: number | string;
content: number | string | boolean | Date | Blob | null;
type: string;
field: string;
}
export interface TableDeleteParams {
uid: string;
schema: string;
table: string;
primary?: string;
field: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rows: {[key: string]: any};
}
export interface TableFilterClausole {
active: boolean;
field: string;
op: '=' | '!=' | '>' | '<' | '>=' | '<=' | 'IN' | 'NOT IN' | 'LIKE' | 'BETWEEN' | 'IS NULL' | 'IS NOT NULL';
value: '';
value2: '';
}
export interface InsertRowsParams { export interface InsertRowsParams {
uid: string; uid: string;
schema: string; schema: string;

View File

@@ -0,0 +1,7 @@
'use strict';
export function bufferToBase64 (buf) {
const binstr = Array.prototype.map.call(buf, ch => {
return String.fromCharCode(ch);
}).join('');
return btoa(binstr);
}

View File

@@ -1,6 +0,0 @@
export function bufferToBase64 (buf: Buffer) {
const binstr = Array.prototype.map.call(buf, (ch: number) => {
return String.fromCharCode(ch);
}).join('');
return Buffer.from(binstr, 'binary').toString('base64');
}

View File

@@ -1,4 +1,5 @@
export function formatBytes (bytes: number, decimals = 2) { 'use strict';
export function formatBytes (bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes'; if (bytes === 0) return '0 Bytes';
const k = 1024; const k = 1024;

View File

@@ -0,0 +1,10 @@
/**
*
* @param {any[]} array
* @returns {number}
*/
export function getArrayDepth (array) {
return Array.isArray(array)
? 1 + Math.max(0, ...array.map(getArrayDepth))
: 0;
}

View File

@@ -1,6 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export function getArrayDepth (array: any[]): number {
return Array.isArray(array)
? 1 + Math.max(0, ...array.map(getArrayDepth))
: 0;
}

View File

@@ -1,3 +1,5 @@
'use strict';
const lookup = { const lookup = {
0: '0000', 0: '0000',
1: '0001', 1: '0001',
@@ -21,11 +23,15 @@ const lookup = {
D: '1101', D: '1101',
E: '1110', E: '1110',
F: '1111' F: '1111'
} as const; };
export type HexChar = keyof typeof lookup /**
* Converts hexadecimal string to binary string
export default function hexToBinary (hex: HexChar[]) { *
* @param {string} hex Hexadecimal string
* @returns {string} Binary string
*/
export default function hexToBinary (hex) {
let binary = ''; let binary = '';
for (let i = 0; i < hex.length; i++) for (let i = 0; i < hex.length; i++)
binary += lookup[hex[i]]; binary += lookup[hex[i]];

View File

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

View File

@@ -1,4 +1,5 @@
export function mimeFromHex (hex: string) { 'use strict';
export function mimeFromHex (hex) {
switch (hex.substring(0, 4)) { // 2 bytes switch (hex.substring(0, 4)) { // 2 bytes
case '424D': case '424D':
return { ext: 'bmp', mime: 'image/bmp' }; return { ext: 'bmp', mime: 'image/bmp' };
@@ -22,7 +23,7 @@ export function mimeFromHex (hex: string) {
case '425A68': case '425A68':
return { ext: 'bz2', mime: 'application/x-bzip2' }; return { ext: 'bz2', mime: 'application/x-bzip2' };
default: default:
switch (hex) { // 4 bites switch (hex) { // 4 bytes
case '89504E47': case '89504E47':
return { ext: 'png', mime: 'image/png' }; return { ext: 'png', mime: 'image/png' };
case '47494638': case '47494638':

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
/**
* @export
* @param {String} [prefix]
* @returns {String} Unique ID
*/
export function uidGen (prefix) {
return (prefix ? `${prefix}:` : '') + Math.random().toString(36).substr(2, 9).toUpperCase();
}

View File

@@ -1,3 +0,0 @@
export function uidGen (prefix?: string) {
return (prefix ? `${prefix}:` : '') + Math.random().toString(36).substring(2, 11).toUpperCase();
}

View File

@@ -1,138 +0,0 @@
export const shortcutEvents: { [key: string]: { l18n: string; l18nParam?: string | number; context?: 'tab' }} = {
'run-or-reload': { l18n: 'message.runOrReload', context: 'tab' },
'open-new-tab': { l18n: 'message.openNewTab', context: 'tab' },
'close-tab': { l18n: 'message.closeTab', context: 'tab' },
'format-query': { l18n: 'message.formatQuery', context: 'tab' },
'kill-query': { l18n: 'message.killQuery', context: 'tab' },
'query-history': { l18n: 'message.queryHistory', context: 'tab' },
'clear-query': { l18n: 'message.clearQuery', context: 'tab' },
'next-tab': { l18n: 'message.nextTab' },
'prev-tab': { l18n: 'message.previousTab' },
'open-all-connections': { l18n: 'message.openAllConnections' },
'open-filter': { l18n: 'message.openFilter' },
'next-page': { l18n: 'message.nextResultsPage' },
'prev-page': { l18n: 'message.previousResultsPage' },
'toggle-console': { l18n: 'message.toggleConsole' },
'save-content': { l18n: 'message.saveContent' },
'create-connection': { l18n: 'message.createNewConnection' },
'open-settings': { l18n: 'message.openSettings' },
'open-scratchpad': { l18n: 'message.openScratchpad' }
};
interface ShortcutRecord {
event: string;
keys: Electron.Accelerator[] | string[];
/** Needed for default shortcuts */
os: NodeJS.Platform[];
}
/**
* Default shortcuts
*/
const shortcuts: ShortcutRecord[] = [
{
event: 'run-or-reload',
keys: ['F5'],
os: ['darwin', 'linux', 'win32']
},
{
event: 'save-content',
keys: ['CommandOrControl+S'],
os: ['darwin', 'linux', 'win32']
},
{
event: 'kill-query',
keys: ['CommandOrControl+K'],
os: ['darwin', 'linux', 'win32']
},
{
event: 'format-query',
keys: ['CommandOrControl+B'],
os: ['darwin', 'linux', 'win32']
},
{
event: 'clear-query',
keys: ['CommandOrControl+Alt+W'],
os: ['darwin', 'linux', 'win32']
},
{
event: 'query-history',
keys: ['CommandOrControl+G'],
os: ['darwin', 'linux', 'win32']
},
{
event: 'open-new-tab',
keys: ['CommandOrControl+T'],
os: ['darwin', 'linux', 'win32']
},
{
event: 'close-tab',
keys: ['CommandOrControl+W'],
os: ['darwin', 'linux', 'win32']
},
{
event: 'next-tab',
keys: ['Alt+CommandOrControl+Right'],
os: ['darwin', 'win32']
},
{
event: 'prev-tab',
keys: ['Alt+CommandOrControl+Left'],
os: ['darwin', 'win32']
},
{
event: 'next-tab',
keys: ['CommandOrControl+PageDown'],
os: ['linux', 'win32']
},
{
event: 'prev-tab',
keys: ['CommandOrControl+PageUp'],
os: ['linux', 'win32']
},
{
event: 'open-filter',
keys: ['CommandOrControl+F'],
os: ['darwin', 'linux', 'win32']
},
{
event: 'next-page',
keys: ['CommandOrControl+Right'],
os: ['darwin', 'linux', 'win32']
},
{
event: 'prev-page',
keys: ['CommandOrControl+Left'],
os: ['darwin', 'linux', 'win32']
},
{
event: 'open-all-connections',
keys: ['Shift+CommandOrControl+Space'],
os: ['darwin', 'linux', 'win32']
},
{
event: 'toggle-console',
keys: ['CommandOrControl+F12'],
os: ['darwin', 'linux', 'win32']
},
{
event: 'toggle-console',
keys: ['CommandOrControl+`'],
os: ['darwin', 'linux', 'win32']
}
];
for (let i = 1; i <= 9; i++) {
shortcutEvents[`select-tab-${i}`] = {
l18n: 'message.selectTabNumber',
l18nParam: i
};
shortcuts.push({
event: `select-tab-${i}`,
keys: [`CommandOrControl+${i}`],
os: ['darwin', 'linux', 'win32']
});
}
export { shortcuts, ShortcutRecord };

View File

@@ -1,11 +1,15 @@
import { app, ipcMain, dialog } from 'electron'; import { app, ipcMain, dialog } from 'electron';
import { ShortcutRegister } from '../libs/ShortcutRegister';
export default () => { export default () => {
ipcMain.on('close-app', () => { ipcMain.on('close-app', () => {
app.exit(); app.exit();
}); });
ipcMain.on('get-key', async event => {
const key = false;
event.returnValue = key;
});
ipcMain.handle('show-open-dialog', (event, options) => { ipcMain.handle('show-open-dialog', (event, options) => {
return dialog.showOpenDialog(options); return dialog.showOpenDialog(options);
}); });
@@ -13,24 +17,4 @@ export default () => {
ipcMain.handle('get-download-dir-path', () => { ipcMain.handle('get-download-dir-path', () => {
return app.getPath('downloads'); return app.getPath('downloads');
}); });
ipcMain.handle('resotre-default-shortcuts', () => {
const shortCutRegister = ShortcutRegister.getInstance();
shortCutRegister.restoreDefaults();
});
ipcMain.handle('reload-shortcuts', () => {
const shortCutRegister = ShortcutRegister.getInstance();
shortCutRegister.reload();
});
ipcMain.handle('update-shortcuts', (event, shortcuts) => {
const shortCutRegister = ShortcutRegister.getInstance();
shortCutRegister.updateShortcuts(shortcuts);
});
ipcMain.handle('unregister-shortcuts', () => {
const shortCutRegister = ShortcutRegister.getInstance();
shortCutRegister.unregister();
});
}; };

View File

@@ -55,17 +55,12 @@ export default (connections: {[key: string]: antares.Client}) => {
try { try {
const connection = await ClientsFactory.getClient({ const connection = await ClientsFactory.getClient({
uid: conn.uid,
client: conn.client, client: conn.client,
params params
}); });
await connection.connect(); await connection.connect();
if (conn.client === 'firebird') await connection.select('1+1').run();
connection.raw('SELECT rdb$get_context(\'SYSTEM\', \'DB_NAME\') FROM rdb$database');
else
await connection.select('1+1').run();
connection.destroy(); connection.destroy();
return { status: 'success' }; return { status: 'success' };
@@ -133,7 +128,6 @@ export default (connections: {[key: string]: antares.Client}) => {
try { try {
const connection = ClientsFactory.getClient({ const connection = ClientsFactory.getClient({
uid: conn.uid,
client: conn.client, client: conn.client,
params, params,
poolSize: 5 poolSize: 5

View File

@@ -62,7 +62,7 @@ export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('get-structure', async (event, params) => { ipcMain.handle('get-structure', async (event, params) => {
try { try {
const structure: unknown = await connections[params.uid].getStructure( const structure = await connections[params.uid].getStructure(
params.schemas params.schemas
); );
@@ -97,7 +97,7 @@ export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('get-engines', async (event, uid) => { ipcMain.handle('get-engines', async (event, uid) => {
try { try {
const result: unknown = await connections[uid].getEngines(); const result = await connections[uid].getEngines();
return { status: 'success', response: result }; return { status: 'success', response: result };
} }
@@ -160,7 +160,8 @@ export default (connections: {[key: string]: antares.Client}) => {
details: true, details: true,
schema, schema,
tabUid, tabUid,
autocommit autocommit,
comments: false
}); });
return { status: 'success', response: result }; return { status: 'success', response: result };
@@ -171,10 +172,7 @@ export default (connections: {[key: string]: antares.Client}) => {
}); });
ipcMain.handle('export', (event, { uid, type, tables, ...rest }) => { ipcMain.handle('export', (event, { uid, type, tables, ...rest }) => {
if (exporter !== null) { if (exporter !== null) return;
exporter.kill();
return;
}
return new Promise((resolve/*, reject */) => { return new Promise((resolve/*, reject */) => {
(async () => { (async () => {
@@ -267,10 +265,7 @@ export default (connections: {[key: string]: antares.Client}) => {
}); });
ipcMain.handle('import-sql', async (event, options) => { ipcMain.handle('import-sql', async (event, options) => {
if (importer !== null) { if (importer !== null) return;
importer.kill();
return;
}
return new Promise((resolve/*, reject */) => { return new Promise((resolve/*, reject */) => {
(async () => { (async () => {

View File

@@ -1,12 +1,12 @@
import * as fs from 'fs';
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import { InsertRowsParams } from 'common/interfaces/tableApis'; import { InsertRowsParams } from 'common/interfaces/tableApis';
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import * as moment from 'moment'; import * as moment from 'moment';
import { sqlEscaper } from 'common/libs/sqlUtils'; import { sqlEscaper } from 'common/libs/sqlEscaper';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME, BOOLEAN } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes';
import customizations from 'common/customizations'; import * as customizations from 'common/customizations';
import fs from 'fs';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('get-table-columns', async (event, params) => { ipcMain.handle('get-table-columns', async (event, params) => {
@@ -104,8 +104,9 @@ export default (connections: {[key: string]: antares.Client}) => {
escapedParam = `"${sqlEscaper(params.content)}"`; escapedParam = `"${sqlEscaper(params.content)}"`;
break; break;
case 'pg': case 'pg':
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
break;
case 'sqlite': case 'sqlite':
case 'firebird':
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`; escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
break; break;
} }
@@ -125,7 +126,6 @@ export default (connections: {[key: string]: antares.Client}) => {
escapedParam = `0x${fileBlob.toString('hex')}`; escapedParam = `0x${fileBlob.toString('hex')}`;
break; break;
case 'pg': case 'pg':
case 'firebird':
fileBlob = fs.readFileSync(params.content); fileBlob = fs.readFileSync(params.content);
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`; escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
break; break;
@@ -143,7 +143,6 @@ export default (connections: {[key: string]: antares.Client}) => {
escapedParam = '\'\''; escapedParam = '\'\'';
break; break;
case 'pg': case 'pg':
case 'firebird':
escapedParam = 'decode(\'\', \'hex\')'; escapedParam = 'decode(\'\', \'hex\')';
break; break;
case 'sqlite': case 'sqlite':
@@ -156,19 +155,6 @@ export default (connections: {[key: string]: antares.Client}) => {
escapedParam = `b'${sqlEscaper(params.content)}'`; escapedParam = `b'${sqlEscaper(params.content)}'`;
reload = true; reload = true;
} }
else if (BOOLEAN.includes(params.type)) {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
case 'pg':
case 'firebird':
escapedParam = params.content;
break;
case 'sqlite':
escapedParam = Number(params.content === 'true');
break;
}
}
else if (params.content === null) else if (params.content === null)
escapedParam = 'NULL'; escapedParam = 'NULL';
else else
@@ -185,18 +171,13 @@ export default (connections: {[key: string]: antares.Client}) => {
} }
else { else {
const { orgRow } = params; const { orgRow } = params;
delete orgRow._antares_id;
reload = true; reload = true;
for (const key in orgRow) { for (const key in orgRow) {
if (typeof orgRow[key] === 'string') if (typeof orgRow[key] === 'string')
orgRow[key] = `'${orgRow[key]}'`; orgRow[key] = `'${orgRow[key]}'`;
if (orgRow[key] === null) orgRow[key] = `= ${orgRow[key]}`;
orgRow[key] = `IS ${orgRow[key]}`;
else
orgRow[key] = `= ${orgRow[key]}`;
} }
await connections[params.uid] await connections[params.uid]
@@ -227,11 +208,10 @@ export default (connections: {[key: string]: antares.Client}) => {
}).join(','); }).join(',');
try { try {
const result: unknown = await connections[params.uid] const result = await connections[params.uid]
.schema(params.schema) .schema(params.schema)
.delete(params.table) .delete(params.table)
.where({ [params.primary]: `IN (${idString})` }) .where({ [params.primary]: `IN (${idString})` })
.limit(params.rows.length)
.run(); .run();
return { status: 'success', response: result }; return { status: 'success', response: result };
@@ -266,12 +246,84 @@ export default (connections: {[key: string]: antares.Client}) => {
} }
}); });
ipcMain.handle('insert-table-rows', async (event, params) => {
try { // TODO: move to client classes
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const insertObj: {[key: string]: any} = {};
for (const key in params.row) {
const type = params.fields[key];
let escapedParam;
if (params.row[key] === null)
escapedParam = 'NULL';
else if ([...NUMBER, ...FLOAT].includes(type))
escapedParam = +params.row[key];
else if ([...TEXT, ...LONG_TEXT].includes(type)) {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = `"${sqlEscaper(params.row[key].value)}"`;
break;
case 'pg':
escapedParam = `'${params.row[key].value.replaceAll('\'', '\'\'')}'`;
break;
}
}
else if (BLOB.includes(type)) {
if (params.row[key].value) {
let fileBlob;
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `0x${fileBlob.toString('hex')}`;
break;
case 'pg':
fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
break;
}
}
else {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = '""';
break;
case 'pg':
escapedParam = 'decode(\'\', \'hex\')';
break;
}
}
}
insertObj[key] = escapedParam;
}
const rows = new Array(+params.repeat).fill(insertObj);
await connections[params.uid]
.schema(params.schema)
.into(params.table)
.insert(rows)
.run();
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('insert-table-fake-rows', async (event, params: InsertRowsParams) => { ipcMain.handle('insert-table-fake-rows', async (event, params: InsertRowsParams) => {
try { // TODO: move to client classes try { // TODO: move to client classes
const rows: {[key: string]: string | number | boolean | Date | Buffer}[] = []; // eslint-disable-next-line @typescript-eslint/no-explicit-any
const rows: {[key: string]: any}[] = [];
for (let i = 0; i < +params.repeat; i++) { for (let i = 0; i < +params.repeat; i++) {
const insertObj: {[key: string]: string | number | boolean | Date | Buffer} = {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any
const insertObj: {[key: string]: any} = {};
for (const key in params.row) { for (const key in params.row) {
const type = params.fields[key]; const type = params.fields[key];
@@ -289,8 +341,6 @@ export default (connections: {[key: string]: antares.Client}) => {
escapedParam = `"${sqlEscaper(params.row[key].value)}"`; escapedParam = `"${sqlEscaper(params.row[key].value)}"`;
break; break;
case 'pg': case 'pg':
case 'sqlite':
case 'firebird':
escapedParam = `'${params.row[key].value.replaceAll('\'', '\'\'')}'`; escapedParam = `'${params.row[key].value.replaceAll('\'', '\'\'')}'`;
break; break;
} }
@@ -331,7 +381,8 @@ 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} = {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any
const parsedParams: {[key: string]: any} = {};
let fakeValue; let fakeValue;
if (params.locale) if (params.locale)
@@ -351,7 +402,7 @@ export default (connections: {[key: string]: antares.Client}) => {
if (typeof fakeValue === 'string') { if (typeof fakeValue === 'string') {
if (params.row[key].length) if (params.row[key].length)
fakeValue = fakeValue.substring(0, params.row[key].length); fakeValue = fakeValue.substr(0, params.row[key].length);
fakeValue = `'${sqlEscaper(fakeValue)}'`; fakeValue = `'${sqlEscaper(fakeValue)}'`;
} }
else if ([...DATE, ...DATETIME].includes(type)) else if ([...DATE, ...DATETIME].includes(type))
@@ -388,20 +439,7 @@ 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();
const parsedResults: {[key: string]: string}[] = [];
for (const row of results.rows) {
const remappedRow: {[key: string]: string} = {};
for (const key in row)
remappedRow[key.toLowerCase()] = row[key];// Thanks Firebird -.-
parsedResults.push(remappedRow);
}
results.rows = parsedResults;
return { status: 'success', response: results }; return { status: 'success', response: results };
} }

View File

@@ -1,7 +1,7 @@
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
import * as Store from 'electron-store'; import Store from 'electron-store';
const persistentStore = new Store({ name: 'settings', clearInvalidConfig: true }); const persistentStore = new Store({ name: 'settings' });
const isMacOS = process.platform === 'darwin'; const isMacOS = process.platform === 'darwin';
let mainWindow: Electron.IpcMainEvent; let mainWindow: Electron.IpcMainEvent;

View File

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

View File

@@ -2,7 +2,6 @@ import * as antares from 'common/interfaces/antares';
import { MySQLClient } from './clients/MySQLClient'; import { MySQLClient } from './clients/MySQLClient';
import { PostgreSQLClient } from './clients/PostgreSQLClient'; import { PostgreSQLClient } from './clients/PostgreSQLClient';
import { SQLiteClient } from './clients/SQLiteClient'; import { SQLiteClient } from './clients/SQLiteClient';
import { FirebirdSQLClient } from './clients/FirebirdSQLClient';
export class ClientsFactory { export class ClientsFactory {
static getClient (args: antares.ClientParams) { static getClient (args: antares.ClientParams) {
@@ -14,8 +13,6 @@ export class ClientsFactory {
return new PostgreSQLClient(args); return new PostgreSQLClient(args);
case 'sqlite': case 'sqlite':
return new SQLiteClient(args); return new SQLiteClient(args);
case 'firebird':
return new FirebirdSQLClient(args);
default: default:
throw new Error(`Unknown database client: ${args.client}`); throw new Error(`Unknown database client: ${args.client}`);
} }

View File

@@ -1,139 +0,0 @@
import { BrowserWindow, globalShortcut, Menu, MenuItem, MenuItemConstructorOptions } from 'electron';
import * as Store from 'electron-store';
import { ShortcutRecord, shortcuts } from 'common/shortcuts';
const shortcutsStore = new Store({ name: 'shortcuts' });
const isDevelopment = process.env.NODE_ENV !== 'production';
const defaultShortcuts = shortcuts.filter(s => s.os.includes(process.platform));
export type ShortcutMode = 'local' | 'global'
export type OsMenu = {
[key in NodeJS.Platform]?: MenuItemConstructorOptions[];
};
export class ShortcutRegister {
private _shortcuts: ShortcutRecord[];
private _mainWindow: BrowserWindow;
private _menu: Menu;
private _menuTemplate: OsMenu;
private _mode: ShortcutMode;
private static _instance: ShortcutRegister;
private constructor (args: { mainWindow: BrowserWindow; menuTemplate?: OsMenu; mode: ShortcutMode }) {
this._mainWindow = args.mainWindow;
this._menuTemplate = args.menuTemplate || {};
this._mode = args.mode;
this.shortcuts = shortcutsStore.get('shortcuts', defaultShortcuts) as ShortcutRecord[];
}
public static getInstance (args?: { mainWindow?: BrowserWindow; menuTemplate?: OsMenu; mode?: ShortcutMode }) {
if (!ShortcutRegister._instance && args.menuTemplate !== undefined && args.mode !== undefined) {
ShortcutRegister._instance = new ShortcutRegister({
mainWindow: args.mainWindow,
menuTemplate: args.menuTemplate,
mode: args.mode
});
}
return ShortcutRegister._instance;
}
get shortcuts () {
return this._shortcuts;
}
private set shortcuts (value: ShortcutRecord[]) {
this._shortcuts = value;
shortcutsStore.set('shortcuts', value);
}
init () {
this._mainWindow.webContents.send('update-shortcuts', this.shortcuts);
this.buildBaseMenu();
if (this._mode === 'global')
this.setGlobalShortcuts();
else if (this._mode === 'local')
this.setLocalShortcuts();
else
throw new Error(`Unknown mode "${this._mode}"`);
Menu.setApplicationMenu(this._menu);
}
private buildBaseMenu () {
if (Object.keys(this._menuTemplate).includes(process.platform))
this._menu = Menu.buildFromTemplate(this._menuTemplate[process.platform]);
else
this._menu = new Menu();
}
private setLocalShortcuts () {
for (const shortcut of this.shortcuts) {
if (shortcut.os.includes(process.platform)) {
for (const key of shortcut.keys) {
try {
this._menu.append(new MenuItem({
label: 'Shortcuts',
visible: false,
submenu: [{
label: String(key),
accelerator: key,
visible: false,
click: () => {
this._mainWindow.webContents.send(shortcut.event);
if (isDevelopment) console.log('LOCAL EVENT:', shortcut);
}
}]
}));
}
catch (error) {
if (isDevelopment) console.log(error);
this.restoreDefaults();
throw error;
}
}
}
}
}
private setGlobalShortcuts () {
for (const shortcut of this.shortcuts) {
if (shortcut.os.includes(process.platform)) {
for (const key of shortcut.keys) {
try {
globalShortcut.register(key, () => {
this._mainWindow.webContents.send(shortcut.event);
if (isDevelopment) console.log('GLOBAL EVENT:', shortcut);
});
}
catch (error) {
if (isDevelopment) console.log(error);
this.restoreDefaults();
throw error;
}
}
}
}
}
reload () {
this.unregister();
this.init();
}
updateShortcuts (shortcuts: ShortcutRecord[]) {
this.shortcuts = shortcuts;
this.reload();
}
restoreDefaults () {
this.shortcuts = defaultShortcuts;
this.reload();
}
unregister () {
if (this._mode === 'global') globalShortcut.unregisterAll();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import * as mysql from 'mysql2/promise'; import * as mysql from 'mysql2/promise';
import { AntaresCore } from '../AntaresCore'; import { AntaresCore } from '../AntaresCore';
import dataTypes from 'common/data-types/mysql'; import * as dataTypes from 'common/data-types/mysql';
import SSH2Promise = require('ssh2-promise'); import SSH2Promise = require('ssh2-promise');
import SSHConfig from 'ssh2-promise/lib/sshConfig'; import SSHConfig from 'ssh2-promise/lib/sshConfig';
@@ -147,14 +147,7 @@ export class MySQLClient extends AntaresCore {
if (this._params.ssh) { if (this._params.ssh) {
try { try {
if (this._params.ssh.password === '') delete this._params.ssh.password; this._ssh = new SSH2Promise({ ...this._params.ssh });
if (this._params.ssh.passphrase === '') delete this._params.ssh.passphrase;
this._ssh = new SSH2Promise({
...this._params.ssh,
keepaliveInterval: 30*60*1000,
debug: process.env.NODE_ENV !== 'production' ? (s) => console.log(s) : null
});
const tunnel = await this._ssh.addTunnel({ const tunnel = await this._ssh.addTunnel({
remoteAddr: this._params.host, remoteAddr: this._params.host,
@@ -199,14 +192,14 @@ export class MySQLClient extends AntaresCore {
// ANSI_QUOTES check // ANSI_QUOTES check
const [response] = await connection.query<mysql.RowDataPacket[]>('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\''); const [response] = await connection.query<mysql.RowDataPacket[]>('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\'');
const sqlMode: string[] = response[0]?.Value?.split(','); const sqlMode = response[0]?.Value?.split(',');
const hasAnsiQuotes = sqlMode.includes('ANSI') || sqlMode.includes('ANSI_QUOTES'); const hasAnsiQuotes = sqlMode.includes('ANSI_QUOTES');
if (this._params.readonly) if (this._params.readonly)
await connection.query('SET SESSION TRANSACTION READ ONLY'); await connection.query('SET SESSION TRANSACTION READ ONLY');
if (hasAnsiQuotes) if (hasAnsiQuotes)
await connection.query(`SET SESSION sql_mode = '${sqlMode.filter((m: string) => !['ANSI', 'ANSI_QUOTES'].includes(m)).join(',')}'`); await connection.query(`SET SESSION sql_mode = "${sqlMode.filter((m: string) => m !== 'ANSI_QUOTES').join(',')}"`);
return connection; return connection;
} }
@@ -226,21 +219,18 @@ export class MySQLClient extends AntaresCore {
// ANSI_QUOTES check // ANSI_QUOTES check
const [res] = await connection.query<mysql.RowDataPacket[]>('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\''); const [res] = await connection.query<mysql.RowDataPacket[]>('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\'');
const sqlMode: string[] = res[0]?.Value?.split(','); const sqlMode = res[0]?.Value?.split(',');
const hasAnsiQuotes = sqlMode.includes('ANSI') || sqlMode.includes('ANSI_QUOTES'); const hasAnsiQuotes = sqlMode.includes('ANSI_QUOTES');
if (hasAnsiQuotes) if (hasAnsiQuotes)
await connection.query(`SET SESSION sql_mode = '${sqlMode.filter((m: string) => !['ANSI', 'ANSI_QUOTES'].includes(m)).join(',')}'`); await connection.query(`SET SESSION sql_mode = "${sqlMode.filter((m: string) => m !== 'ANSI_QUOTES').join(',')}"`);
if (this._params.readonly)
await connection.query('SET SESSION TRANSACTION READ ONLY');
connection.on('connection', conn => { connection.on('connection', conn => {
if (this._params.readonly) if (this._params.readonly)
conn.query('SET SESSION TRANSACTION READ ONLY'); conn.query('SET SESSION TRANSACTION READ ONLY');
if (hasAnsiQuotes) if (hasAnsiQuotes)
conn.query(`SET SESSION sql_mode = '${sqlMode.filter((m: string) => !['ANSI', 'ANSI_QUOTES'].includes(m)).join(',')}'`); conn.query(`SET SESSION sql_mode = "${sqlMode.filter((m: string) => m !== 'ANSI_QUOTES').join(',')}"`);
}); });
return connection; return connection;
@@ -254,15 +244,25 @@ export class MySQLClient extends AntaresCore {
async getStructure (schemas: Set<string>) { async getStructure (schemas: Set<string>) {
/* eslint-disable camelcase */ /* eslint-disable camelcase */
interface ShowTableResult { interface ShowTableResult {
TABLE_SCHEMA?: string; Db?: string;
TABLE_NAME: string; Name: string;
TABLE_TYPE: string; Engine: string;
TABLE_ROWS: number; Version: number;
ENGINE: string; Row_format: string;
DATA_LENGTH: number; Rows: number;
INDEX_LENGTH: number; Avg_row_length: number;
TABLE_COLLATION: string; Data_length: number;
TABLE_COMMENT: string; Max_data_length: number;
Index_length: number;
Data_free: number;
Auto_increment: number;
Create_time: Date;
Update_time: Date;
Check_time?: number;
Collation: string;
Checksum?: number;
Create_options: string;
Comment: string;
} }
interface ShowTriggersResult { interface ShowTriggersResult {
@@ -299,25 +299,10 @@ export class MySQLClient extends AntaresCore {
for (const db of filteredDatabases) { for (const db of filteredDatabases) {
if (!schemas.has(db.Database)) continue; if (!schemas.has(db.Database)) continue;
let { rows: tables } = await this.raw<antares.QueryResult<ShowTableResult>>(` let { rows: tables } = await this.raw<antares.QueryResult<ShowTableResult>>(`SHOW TABLE STATUS FROM \`${db.Database}\``);
SELECT
TABLE_NAME,
TABLE_TYPE,
ENGINE,
DATA_LENGTH,
INDEX_LENGTH,
TABLE_COMMENT,
TABLE_COLLATION,
CREATE_TIME,
UPDATE_TIME
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = "${db.Database}"
ORDER BY TABLE_NAME
`);
if (tables.length) { if (tables.length) {
tables = tables.map(table => { tables = tables.map(table => {
table.TABLE_SCHEMA = db.Database; table.Db = db.Database;
return table; return table;
}); });
tablesArr.push(...tables); tablesArr.push(...tables);
@@ -336,9 +321,9 @@ export class MySQLClient extends AntaresCore {
return filteredDatabases.map(db => { return filteredDatabases.map(db => {
if (schemas.has(db.Database)) { if (schemas.has(db.Database)) {
// TABLES // TABLES
const remappedTables: antares.TableInfos[] = tablesArr.filter(table => table.TABLE_SCHEMA === db.Database).map(table => { const remappedTables = tablesArr.filter(table => table.Db === db.Database).map(table => {
let tableType; let tableType;
switch (table.TABLE_TYPE) { switch (table.Comment) {
case 'VIEW': case 'VIEW':
tableType = 'view'; tableType = 'view';
break; break;
@@ -347,22 +332,25 @@ export class MySQLClient extends AntaresCore {
break; break;
} }
const tableSize = Number(table.DATA_LENGTH) + Number(table.INDEX_LENGTH); const tableSize = Number(table.Data_length) + Number(table.Index_length);
schemaSize += tableSize; schemaSize += tableSize;
return { return {
name: table.TABLE_NAME, name: table.Name,
type: tableType, type: tableType,
rows: table.TABLE_ROWS, rows: table.Rows,
engine: table.ENGINE, created: table.Create_time,
comment: table.TABLE_COMMENT, updated: table.Update_time,
engine: table.Engine,
comment: table.Comment,
size: tableSize, size: tableSize,
collation: table.TABLE_COLLATION autoIncrement: table.Auto_increment,
collation: table.Collation
}; };
}); });
// PROCEDURES // PROCEDURES
const remappedProcedures: antares.RoutineInfos[] = procedures.filter(procedure => procedure.Db === db.Database).map(procedure => { const remappedProcedures = procedures.filter(procedure => procedure.Db === db.Database).map(procedure => {
return { return {
name: procedure.Name, name: procedure.Name,
type: procedure.Type, type: procedure.Type,
@@ -376,7 +364,7 @@ export class MySQLClient extends AntaresCore {
}); });
// FUNCTIONS // FUNCTIONS
const remappedFunctions: antares.FunctionInfos[] = functions.filter(func => func.Db === db.Database).map(func => { const remappedFunctions = functions.filter(func => func.Db === db.Database).map(func => {
return { return {
name: func.Name, name: func.Name,
type: func.Type, type: func.Type,
@@ -390,26 +378,33 @@ export class MySQLClient extends AntaresCore {
}); });
// SCHEDULERS // SCHEDULERS
const remappedSchedulers: antares.EventInfos[] = schedulers.filter(scheduler => scheduler.Db === db.Database).map(scheduler => { const remappedSchedulers = schedulers.filter(scheduler => scheduler.Db === db.Database).map(scheduler => {
return { return {
name: scheduler.EVENT_NAME, name: scheduler.EVENT_NAME,
schema: scheduler.Db, definition: scheduler.EVENT_DEFINITION,
sql: scheduler.EVENT_DEFINITION, type: scheduler.EVENT_TYPE,
execution: scheduler.EVENT_TYPE === 'RECURRING' ? 'EVERY' : 'ONCE',
definer: scheduler.DEFINER, definer: scheduler.DEFINER,
body: scheduler.EVENT_BODY,
starts: scheduler.STARTS, starts: scheduler.STARTS,
ends: scheduler.ENDS, ends: scheduler.ENDS,
state: scheduler.STATUS === 'ENABLED' ? 'ENABLE' : scheduler.STATE === 'DISABLED' ? 'DISABLE' : 'DISABLE ON SLAVE',
enabled: scheduler.STATUS === 'ENABLED', enabled: scheduler.STATUS === 'ENABLED',
at: scheduler.EXECUTE_AT, executeAt: scheduler.EXECUTE_AT,
every: [scheduler.INTERVAL_FIELD, scheduler.INTERVAL_VALUE], intervalField: scheduler.INTERVAL_FIELD,
preserve: scheduler.ON_COMPLETION.includes('PRESERVE'), intervalValue: scheduler.INTERVAL_VALUE,
comment: scheduler.EVENT_COMMENT onCompletion: scheduler.ON_COMPLETION,
originator: scheduler.ORIGINATOR,
sqlMode: scheduler.SQL_MODE,
created: scheduler.CREATED,
updated: scheduler.LAST_ALTERED,
lastExecuted: scheduler.LAST_EXECUTED,
comment: scheduler.EVENT_COMMENT,
charset: scheduler.CHARACTER_SET_CLIENT,
timezone: scheduler.TIME_ZONE
}; };
}); });
// TRIGGERS // TRIGGERS
const remappedTriggers: antares.TriggerInfos[] = triggersArr.filter(trigger => trigger.Db === db.Database).map(trigger => { const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.Database).map(trigger => {
return { return {
name: trigger.Trigger, name: trigger.Trigger,
statement: trigger.Statement, statement: trigger.Statement,
@@ -481,12 +476,7 @@ export class MySQLClient extends AntaresCore {
.orderBy({ ORDINAL_POSITION: 'ASC' }) .orderBy({ ORDINAL_POSITION: 'ASC' })
.run<TableColumnsResult>(); .run<TableColumnsResult>();
let fields: CreateTableResult[] = []; const { rows: fields } = await this.raw<antares.QueryResult<CreateTableResult>>(`SHOW CREATE TABLE \`${schema}\`.\`${table}\``);
try {
const { rows } = await this.raw<antares.QueryResult<CreateTableResult>>(`SHOW CREATE TABLE \`${schema}\`.\`${table}\``);
fields = rows;
}
catch (_) {}
const remappedFields = fields.map(row => { const remappedFields = fields.map(row => {
if (!row['Create Table']) return false; if (!row['Create Table']) return false;
@@ -929,15 +919,8 @@ export class MySQLClient extends AntaresCore {
return await this.raw(sql); return await this.raw(sql);
} }
async truncateTable (params: { schema: string; table: string; force: boolean }) { async truncateTable (params: { schema: string; table: string }) {
let sql = `TRUNCATE TABLE \`${params.schema}\`.\`${params.table}\`;`; const sql = `TRUNCATE TABLE \`${params.schema}\`.\`${params.table}\``;
if (params.force) {
sql = `
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
${sql}
SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1);
`;
}
return await this.raw(sql); return await this.raw(sql);
} }
@@ -947,22 +930,19 @@ export class MySQLClient extends AntaresCore {
} }
async getViewInformations ({ schema, view }: { schema: string; view: string }) { async getViewInformations ({ schema, view }: { schema: string; view: string }) {
const { rows: algorithm } = await this.raw(`SHOW CREATE VIEW \`${schema}\`.\`${view}\``); const sql = `SHOW CREATE VIEW \`${schema}\`.\`${view}\``;
const { rows: viewInfo } = await this.raw(` const results = await this.raw(sql);
SELECT *
FROM INFORMATION_SCHEMA.VIEWS
WHERE TABLE_SCHEMA = '${schema}'
AND TABLE_NAME = '${view}'
`);
return { return results.rows.map(row => {
algorithm: algorithm[0]['Create View'].match(/(?<=CREATE ALGORITHM=).*?(?=\s)/gs)[0], return {
definer: viewInfo[0].DEFINER.split('@').map((str: string) => `\`${str}\``).join('@'), algorithm: row['Create View'].match(/(?<=CREATE ALGORITHM=).*?(?=\s)/gs)[0],
security: viewInfo[0].SECURITY_TYPE, definer: row['Create View'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
updateOption: viewInfo[0].CHECK_OPTION === 'NONE' ? '' : viewInfo[0].CHECK_OPTION, security: row['Create View'].match(/(?<=SQL SECURITY ).*?(?=\s)/gs)[0],
sql: viewInfo[0].VIEW_DEFINITION, updateOption: row['Create View'].match(/(?<=WITH ).*?(?=\s)/gs) ? row['Create View'].match(/(?<=WITH ).*?(?=\s)/gs)[0] : '',
name: viewInfo[0].TABLE_NAME sql: row['Create View'].match(/(?<=AS ).*?$/gs)[0],
}; name: row.View
};
})[0];
} }
async dropView (params: { schema: string; view: string }) { async dropView (params: { schema: string; view: string }) {
@@ -975,7 +955,7 @@ export class MySQLClient extends AntaresCore {
USE \`${view.schema}\`; USE \`${view.schema}\`;
ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''} ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''}
SQL SECURITY ${view.security} SQL SECURITY ${view.security}
VIEW \`${view.schema}\`.\`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''} params \`${view.schema}\`.\`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}
`; `;
if (view.name !== view.oldName) if (view.name !== view.oldName)
@@ -1401,20 +1381,12 @@ export class MySQLClient extends AntaresCore {
xa: row.XA, xa: row.XA,
savepoints: row.Savepoints, savepoints: row.Savepoints,
isDefault: row.Support.includes('DEFAULT') isDefault: row.Support.includes('DEFAULT')
} as {
name: string;
support: string;
comment: string;
transactions: string;
xa: string;
savepoints: string;
isDefault: boolean;
}; };
}); });
} }
async getVersion () { async getVersion () {
const sql = 'SHOW VARIABLES LIKE \'%vers%\''; const sql = 'SHOW VARIABLES LIKE "%vers%"';
const { rows } = await this.raw(sql); const { rows } = await this.raw(sql);
return rows.reduce((acc, curr) => { return rows.reduce((acc, curr) => {
@@ -1433,12 +1405,7 @@ export class MySQLClient extends AntaresCore {
break; break;
} }
return acc; return acc;
}, {}) as { }, {});
number: string;
name: string;
arch: string;
os: string;
};
} }
async getProcesses () { async getProcesses () {
@@ -1456,15 +1423,6 @@ export class MySQLClient extends AntaresCore {
time: row.TIME, time: row.TIME,
state: row.STATE, state: row.STATE,
info: row.INFO info: row.INFO
} as {
id: number;
user: string;
host: string;
db: string;
command: string;
time: number;
state: string;
info: string;
}; };
}); });
} }
@@ -1553,7 +1511,7 @@ export class MySQLClient extends AntaresCore {
} }
async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) { async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) {
this._logger({ cUid: this._cUid, sql }); if (process.env.NODE_ENV === 'development') this._logger(sql);
args = { args = {
nest: false, nest: false,
@@ -1605,8 +1563,7 @@ export class MySQLClient extends AntaresCore {
let timeStop: Date; let timeStop: Date;
let keysArr: antares.QueryForeign[] = []; let keysArr: antares.QueryForeign[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => {
const { rows, report, fields, keys, duration }: any = await new Promise((resolve, reject) => {
connection.query({ sql: query, nestTables }).then(async ([response, fields]) => { connection.query({ sql: query, nestTables }).then(async ([response, fields]) => {
timeStop = new Date(); timeStop = new Date();
const queryResult = response; const queryResult = response;
@@ -1616,7 +1573,7 @@ export class MySQLClient extends AntaresCore {
if (!field || Array.isArray(field)) if (!field || Array.isArray(field))
return undefined; return undefined;
const type = this._getType(field as undefined); const type = this._getType(field);
return { return {
name: field.orgName, name: field.orgName,

View File

@@ -1,9 +1,10 @@
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import * as mysql from 'mysql2'; import * as mysql from 'mysql2';
import { builtinsTypes } from 'pg-types';
import * as pg from 'pg'; import * as pg from 'pg';
import * as pgAst from 'pgsql-ast-parser'; import * as pgAst from 'pgsql-ast-parser';
import { AntaresCore } from '../AntaresCore'; import { AntaresCore } from '../AntaresCore';
import dataTypes from 'common/data-types/postgresql'; import * as dataTypes from 'common/data-types/postgresql';
import SSH2Promise = require('ssh2-promise'); import SSH2Promise = require('ssh2-promise');
import SSHConfig from 'ssh2-promise/lib/sshConfig'; import SSHConfig from 'ssh2-promise/lib/sshConfig';
@@ -18,68 +19,6 @@ pg.types.setTypeParser(1114, pgToString); // timestamp
pg.types.setTypeParser(1184, pgToString); // timestamptz pg.types.setTypeParser(1184, pgToString); // timestamptz
pg.types.setTypeParser(1266, pgToString); // timetz pg.types.setTypeParser(1266, pgToString); // timetz
// from pg-types
type builtinsTypes =
'BOOL' |
'BYTEA' |
'CHAR' |
'INT8' |
'INT2' |
'INT4' |
'REGPROC' |
'TEXT' |
'OID' |
'TID' |
'XID' |
'CID' |
'JSON' |
'XML' |
'PG_NODE_TREE' |
'SMGR' |
'PATH' |
'POLYGON' |
'CIDR' |
'FLOAT4' |
'FLOAT8' |
'ABSTIME' |
'RELTIME' |
'TINTERVAL' |
'CIRCLE' |
'MACADDR8' |
'MONEY' |
'MACADDR' |
'INET' |
'ACLITEM' |
'BPCHAR' |
'VARCHAR' |
'DATE' |
'TIME' |
'TIMESTAMP' |
'TIMESTAMPTZ' |
'INTERVAL' |
'TIMETZ' |
'BIT' |
'VARBIT' |
'NUMERIC' |
'REFCURSOR' |
'REGPROCEDURE' |
'REGOPER' |
'REGOPERATOR' |
'REGCLASS' |
'REGTYPE' |
'UUID' |
'TXID_SNAPSHOT' |
'PG_LSN' |
'PG_NDISTINCT' |
'PG_DEPENDENCIES' |
'TSVECTOR' |
'TSQUERY' |
'GTSVECTOR' |
'REGCONFIG' |
'REGDICTIONARY' |
'JSONB' |
'REGNAMESPACE' |
'REGROLE';
export class PostgreSQLClient extends AntaresCore { export class PostgreSQLClient extends AntaresCore {
private _schema?: string; private _schema?: string;
private _runningConnections: Map<string, number>; private _runningConnections: Map<string, number>;
@@ -162,11 +101,7 @@ export class PostgreSQLClient extends AntaresCore {
if (this._params.ssh) { if (this._params.ssh) {
try { try {
this._ssh = new SSH2Promise({ this._ssh = new SSH2Promise({ ...this._params.ssh });
...this._params.ssh,
keepaliveInterval: 30*60*1000,
debug: process.env.NODE_ENV !== 'production' ? (s) => console.log(s) : null
});
const tunnel = await this._ssh.addTunnel({ const tunnel = await this._ssh.addTunnel({
remoteAddr: this._params.host, remoteAddr: this._params.host,
@@ -544,7 +479,11 @@ export class PostgreSQLClient extends AntaresCore {
return { return {
name: row.constraint_name, name: row.constraint_name,
column: row.column_name, column: row.column_name,
type: row.constraint_type indexType: null as null,
type: row.constraint_type,
cardinality: null as null,
comment: '',
indexComment: ''
}; };
}); });
} }
@@ -1375,7 +1314,7 @@ export class PostgreSQLClient extends AntaresCore {
} }
async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) { async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) {
this._logger({ cUid: this._cUid, sql }); if (process.env.NODE_ENV === 'development') this._logger(sql);
args = { args = {
nest: false, nest: false,
@@ -1426,8 +1365,7 @@ export class PostgreSQLClient extends AntaresCore {
let timeStop: Date; let timeStop: Date;
let keysArr: antares.QueryForeign[] = []; let keysArr: antares.QueryForeign[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => {
const { rows, report, fields, keys, duration }: any = await new Promise((resolve, reject) => {
(async () => { (async () => {
try { try {
const res = await connection.query({ rowMode: args.nest ? 'array' : null, text: query }); const res = await connection.query({ rowMode: args.nest ? 'array' : null, text: query });

View File

@@ -1,7 +1,7 @@
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import * as sqlite from 'better-sqlite3'; import * as sqlite from 'better-sqlite3';
import { AntaresCore } from '../AntaresCore'; import { AntaresCore } from '../AntaresCore';
import dataTypes from 'common/data-types/sqlite'; import * as dataTypes from 'common/data-types/sqlite';
import { NUMBER, FLOAT, TIME, DATETIME } from 'common/fieldTypes'; import { NUMBER, FLOAT, TIME, DATETIME } from 'common/fieldTypes';
export class SQLiteClient extends AntaresCore { export class SQLiteClient extends AntaresCore {
@@ -217,7 +217,11 @@ export class SQLiteClient extends AntaresCore {
remappedIndexes.push({ remappedIndexes.push({
name: 'PRIMARY', name: 'PRIMARY',
column: key.name, column: key.name,
type: 'PRIMARY' indexType: null as never,
type: 'PRIMARY',
cardinality: null as never,
comment: '',
indexComment: ''
}); });
} }
@@ -344,20 +348,6 @@ export class SQLiteClient extends AntaresCore {
const tmpName = `Antares_${table}_tmp`; const tmpName = `Antares_${table}_tmp`;
await this.raw(`CREATE TABLE "${tmpName}" AS SELECT * FROM "${table}"`); await this.raw(`CREATE TABLE "${tmpName}" AS SELECT * FROM "${table}"`);
// Get table triggers before drop
const { rows: triggers } = await this.raw(`SELECT * FROM "${schema}".sqlite_master WHERE type='trigger' AND tbl_name = '${table}'`);
const remappedTriggers = triggers.map((row) => {
return {
schema,
sql: row.sql.match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
name: row.name,
table: row.sql.match(/(?<=ON `).*?(?=`)/gs)[0],
activation: row.sql.match(/(BEFORE|AFTER)/gs)[0],
event: row.sql.match(/(INSERT|UPDATE|DELETE)/gs)[0]
};
});
await this.dropTable(params); await this.dropTable(params);
const createTableParams = { const createTableParams = {
@@ -390,11 +380,6 @@ export class SQLiteClient extends AntaresCore {
await this.raw(`INSERT INTO "${createTableParams.options.name}" (${insertFields.join(',')}) SELECT ${selectFields.join(',')} FROM "${tmpName}"`); await this.raw(`INSERT INTO "${createTableParams.options.name}" (${insertFields.join(',')}) SELECT ${selectFields.join(',')} FROM "${tmpName}"`);
await this.dropTable({ schema: schema, table: tmpName }); await this.dropTable({ schema: schema, table: tmpName });
// Recreates triggers
for (const trigger of remappedTriggers)
await this.createTrigger(trigger);
await this.raw('PRAGMA foreign_keys = 1'); await this.raw('PRAGMA foreign_keys = 1');
await this.raw('COMMIT'); await this.raw('COMMIT');
} }
@@ -601,7 +586,7 @@ export class SQLiteClient extends AntaresCore {
} }
async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) { async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) {
this._logger({ cUid: this._cUid, sql });// TODO: replace BLOB content with a placeholder if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
args = { args = {
nest: false, nest: false,
@@ -643,8 +628,7 @@ export class SQLiteClient extends AntaresCore {
let timeStop; let timeStop;
const keysArr: antares.QueryForeign[] = []; const keysArr: antares.QueryForeign[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => {
const { rows, report, fields, keys, duration }: any = await new Promise((resolve, reject) => {
(async () => { (async () => {
let queryRunResult: sqlite.RunResult; let queryRunResult: sqlite.RunResult;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -691,8 +675,8 @@ export class SQLiteClient extends AntaresCore {
if ([...TIME, ...DATETIME].includes(parsedType)) { if ([...TIME, ...DATETIME].includes(parsedType)) {
const firstNotNull = queryAllResult.find(res => res[field.name] !== null); const firstNotNull = queryAllResult.find(res => res[field.name] !== null);
if (firstNotNull && String(firstNotNull[field.name]).includes('.')) if (firstNotNull && firstNotNull[field.name].includes('.'))
length = String(firstNotNull[field.name]).split('.').pop().length; length = firstNotNull[field.name].split('.').pop().length;
} }
return { return {

View File

@@ -1,8 +1,12 @@
import * as exporter from 'common/interfaces/exporter'; import * as exporter from 'common/interfaces/exporter';
import * as mysql from 'mysql2/promise'; import * as mysql from 'mysql2/promise';
import { SqlExporter } from './SqlExporter'; import { SqlExporter } from './SqlExporter';
import { BLOB, BIT, DATE, DATETIME, FLOAT, SPATIAL, IS_MULTI_SPATIAL, NUMBER } from 'common/fieldTypes';
import hexToBinary from 'common/libs/hexToBinary';
import { getArrayDepth } from 'common/libs/getArrayDepth';
import * as moment from 'moment';
import { lineString, point, polygon } from '@turf/helpers';
import { MySQLClient } from '../../clients/MySQLClient'; import { MySQLClient } from '../../clients/MySQLClient';
import { valueToSqlString } from 'common/libs/sqlUtils';
export default class MysqlExporter extends SqlExporter { export default class MysqlExporter extends SqlExporter {
protected _client: MySQLClient; protected _client: MySQLClient;
@@ -118,7 +122,54 @@ ${footer}
const column = notGeneratedColumns[i]; const column = notGeneratedColumns[i];
const val = row[column.name]; const val = row[column.name];
sqlInsertString += valueToSqlString({ val, client: 'mysql', field: column }); if (val === null) sqlInsertString += 'NULL';
else if (DATE.includes(column.type)) {
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format('YYYY-MM-DD'))
: val;
}
else if (DATETIME.includes(column.type)) {
let datePrecision = '';
for (let i = 0; i < column.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`))
: this.escapeAndQuote(val);
}
else if (BIT.includes(column.type))
sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex'))}'`;
else if (BLOB.includes(column.type))
sqlInsertString += `X'${val.toString('hex').toUpperCase()}'`;
else if (NUMBER.includes(column.type))
sqlInsertString += val;
else if (FLOAT.includes(column.type))
sqlInsertString += parseFloat(val);
else if (SPATIAL.includes(column.type)) {
let geoJson;
if (IS_MULTI_SPATIAL.includes(column.type)) {
const features = [];
for (const element of val)
features.push(this._getGeoJSON(element));
geoJson = {
type: 'FeatureCollection',
features
};
}
else
geoJson = this._getGeoJSON(val);
sqlInsertString += `ST_GeomFromGeoJSON('${JSON.stringify(geoJson)}')`;
}
else if (val === '') sqlInsertString += '\'\'';
else {
sqlInsertString += typeof val === 'string'
? this.escapeAndQuote(val)
: typeof val === 'object'
? this.escapeAndQuote(JSON.stringify(val))
: val;
}
if (parseInt(i) !== notGeneratedColumns.length - 1) if (parseInt(i) !== notGeneratedColumns.length - 1)
sqlInsertString += ', '; sqlInsertString += ', ';
@@ -384,4 +435,17 @@ CREATE TABLE \`${view.Name}\`(
return `'${escapedVal}'`; return `'${escapedVal}'`;
} }
/* eslint-disable @typescript-eslint/no-explicit-any */
_getGeoJSON (val: any) {
if (Array.isArray(val)) {
if (getArrayDepth(val) === 1)
return lineString(val.reduce((acc, curr) => [...acc, [curr.x, curr.y]], []));
else
return polygon(val.map(arr => arr.reduce((acc: any, curr: any) => [...acc, [curr.x, curr.y]], [])));
}
else
return point([val.x, val.y]);
}
/* eslint-enable @typescript-eslint/no-explicit-any */
} }

View File

@@ -1,11 +1,13 @@
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import * as exporter from 'common/interfaces/exporter'; import * as exporter from 'common/interfaces/exporter';
import { SqlExporter } from './SqlExporter'; import { SqlExporter } from './SqlExporter';
import { BLOB, BIT, DATE, DATETIME, FLOAT, NUMBER, TEXT_SEARCH } from 'common/fieldTypes';
import hexToBinary from 'common/libs/hexToBinary';
import * as moment from 'moment';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
import * as QueryStream from 'pg-query-stream'; import * as QueryStream from 'pg-query-stream';
import { PostgreSQLClient } from '../../clients/PostgreSQLClient'; import { PostgreSQLClient } from '../../clients/PostgreSQLClient';
import { valueToSqlString } from 'common/libs/sqlUtils';
export default class PostgreSQLExporter extends SqlExporter { export default class PostgreSQLExporter extends SqlExporter {
constructor (client: PostgreSQLClient, tables: exporter.TableParams[], options: exporter.ExportOptions) { constructor (client: PostgreSQLClient, tables: exporter.TableParams[], options: exporter.ExportOptions) {
@@ -221,7 +223,47 @@ SET row_security = off;\n\n\n`;
const column = columns[i]; const column = columns[i];
const val = row[column.name]; const val = row[column.name];
sqlInsertString += valueToSqlString({ val, client: 'pg', field: column }); if (val === null) sqlInsertString += 'NULL';
else if (DATE.includes(column.type)) {
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format('YYYY-MM-DD'))
: val;
}
else if (DATETIME.includes(column.type)) {
let datePrecision = '';
for (let i = 0; i < column.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`))
: this.escapeAndQuote(val);
}
else if ('isArray' in column) {
let parsedVal;
if (Array.isArray(val))
parsedVal = JSON.stringify(val).replaceAll('[', '{').replaceAll(']', '}');
else
parsedVal = typeof val === 'string' ? val.replaceAll('[', '{').replaceAll(']', '}') : '';
sqlInsertString += `'${parsedVal}'`;
}
else if (TEXT_SEARCH.includes(column.type))
sqlInsertString += `'${val.replaceAll('\'', '\'\'')}'`;
else if (BIT.includes(column.type))
sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex'))}'`;
else if (BLOB.includes(column.type))
sqlInsertString += `decode('${val.toString('hex').toUpperCase()}', 'hex')`;
else if (NUMBER.includes(column.type))
sqlInsertString += val;
else if (FLOAT.includes(column.type))
sqlInsertString += parseFloat(val);
else if (val === '') sqlInsertString += '\'\'';
else {
sqlInsertString += typeof val === 'string'
? this.escapeAndQuote(val)
: typeof val === 'object'
? this.escapeAndQuote(JSON.stringify(val))
: val;
}
if (parseInt(i) !== columns.length - 1) if (parseInt(i) !== columns.length - 1)
sqlInsertString += ', '; sqlInsertString += ', ';

View File

@@ -1,6 +1,6 @@
import * as pg from 'pg'; import * as pg from 'pg';
import * as importer from 'common/interfaces/importer'; import * as importer from 'common/interfaces/importer';
import * as fs from 'fs/promises'; import fs from 'fs/promises';
import PostgreSQLParser from '../../parsers/PostgreSQLParser'; import PostgreSQLParser from '../../parsers/PostgreSQLParser';
import { BaseImporter } from '../BaseImporter'; import { BaseImporter } from '../BaseImporter';

View File

@@ -1,19 +1,15 @@
import { app, BrowserWindow, nativeImage, ipcMain } from 'electron'; import { app, BrowserWindow, /* session, */ nativeImage, Menu } from 'electron';
import * as path from 'path'; import * as path from 'path';
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 remoteMain from '@electron/remote/main'; import * as remoteMain from '@electron/remote/main';
import ipcHandlers from './ipc-handlers'; import ipcHandlers from './ipc-handlers';
import { OsMenu, ShortcutRegister } from './libs/ShortcutRegister';
Store.initRenderer(); Store.initRenderer();
const settingsStore = new Store({ name: 'settings' });
const appTheme = settingsStore.get('application_theme');
const isDevelopment = process.env.NODE_ENV !== 'production'; const isDevelopment = process.env.NODE_ENV !== 'production';
const isMacOS = process.platform === 'darwin'; const isMacOS = process.platform === 'darwin';
const isLinux = process.platform === 'linux';
const isWindows = process.platform === 'win32';
const gotTheLock = app.requestSingleInstanceLock(); const gotTheLock = app.requestSingleInstanceLock();
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'; process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
@@ -23,7 +19,7 @@ let mainWindow: BrowserWindow;
let mainWindowState: windowStateKeeper.State; let mainWindowState: windowStateKeeper.State;
async function createMainWindow () { async function createMainWindow () {
const icon = require('../renderer/images/logo-64.png'); const icon = require('../renderer/images/logo-32.png');
const window = new BrowserWindow({ const window = new BrowserWindow({
width: mainWindowState.width, width: mainWindowState.width,
height: mainWindowState.height, height: mainWindowState.height,
@@ -31,24 +27,16 @@ async function createMainWindow () {
y: mainWindowState.y, y: mainWindowState.y,
minWidth: 900, minWidth: 900,
minHeight: 550, minHeight: 550,
show: !isWindows,
title: 'Antares SQL', title: 'Antares SQL',
autoHideMenuBar: true,
icon: nativeImage.createFromDataURL(icon.default), icon: nativeImage.createFromDataURL(icon.default),
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
contextIsolation: false, contextIsolation: false,
devTools: isDevelopment,
spellcheck: false spellcheck: false
}, },
autoHideMenuBar: true, frame: false,
titleBarStyle: isLinux ? 'default' :'hidden', titleBarStyle: isMacOS ? 'hidden' : 'default',
titleBarOverlay: isWindows
? {
color: appTheme === 'dark' ? '#3f3f3f' : '#fff',
symbolColor: appTheme === 'dark' ? '#fff' : '#000',
height: 30
}
: false,
trafficLightPosition: isMacOS ? { x: 10, y: 8 } : undefined, trafficLightPosition: isMacOS ? { x: 10, y: 8 } : undefined,
backgroundColor: '#1d1d1d' backgroundColor: '#1d1d1d'
}); });
@@ -85,24 +73,10 @@ else {
// Initialize ipcHandlers // Initialize ipcHandlers
ipcHandlers(); ipcHandlers();
ipcMain.on('refresh-theme-settings', () => {
const appTheme = settingsStore.get('application_theme');
if (isWindows && mainWindow) {
mainWindow.setTitleBarOverlay({
color: appTheme === 'dark' ? '#3f3f3f' : '#fff',
symbolColor: appTheme === 'dark' ? '#fff' : '#000'
});
}
});
ipcMain.on('change-window-title', (_, title: string) => {
if (mainWindow) mainWindow.setTitle(title);
});
// quit application when all windows are closed // quit application when all windows are closed
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
// on macOS it is common for applications to stay open until the user explicitly quits // on macOS it is common for applications to stay open until the user explicitly quits
if (!isMacOS) app.quit(); if (isMacOS) app.quit();
}); });
app.on('activate', async () => { app.on('activate', async () => {
@@ -121,11 +95,8 @@ else {
mainWindow = await createMainWindow(); mainWindow = await createMainWindow();
createAppMenu(); createAppMenu();
if (isWindows) if (isDevelopment)
mainWindow.show(); mainWindow.webContents.openDevTools();
// if (isDevelopment)
// mainWindow.webContents.openDevTools();
process.on('uncaughtException', error => { process.on('uncaughtException', error => {
mainWindow.webContents.send('unhandled-exception', error); mainWindow.webContents.send('unhandled-exception', error);
@@ -146,8 +117,10 @@ else {
} }
function createAppMenu () { function createAppMenu () {
const menuTemplate: OsMenu = { let menu: Electron.Menu = null;
darwin: [
if (isMacOS) {
menu = Menu.buildFromTemplate([
{ {
label: app.name, label: app.name,
submenu: [ submenu: [
@@ -178,11 +151,10 @@ function createAppMenu () {
{ {
role: 'windowMenu' role: 'windowMenu'
} }
] ]);
}; }
const shortCutRegister = ShortcutRegister.getInstance({ mainWindow, menuTemplate, mode: 'local' }); Menu.setApplicationMenu(menu);
shortCutRegister.init();
} }
function saveWindowState () { function saveWindowState () {

View File

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

View File

@@ -3,7 +3,7 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div class="modal active" :class="modalSizeClass"> <div class="modal active" :class="modalSizeClass">
<a class="modal-overlay" @click="hideModal" /> <a class="modal-overlay" @click="hideModal" />
<div ref="trapRef" class="modal-container"> <div class="modal-container">
<div v-if="hasHeader" class="modal-header pl-2"> <div v-if="hasHeader" class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<slot name="header" /> <slot name="header" />
@@ -31,13 +31,13 @@
class="btn btn-primary mr-2" class="btn btn-primary mr-2"
@click.stop="confirmModal" @click.stop="confirmModal"
> >
{{ confirmText || t('word.confirm') }} {{ confirmText || $t('word.confirm') }}
</button> </button>
<button <button
class="btn btn-link" class="btn btn-link"
@click="hideModal" @click="hideModal"
> >
{{ cancelText || t('word.cancel') }} {{ cancelText || $t('word.cancel') }}
</button> </button>
</div> </div>
</div> </div>
@@ -46,72 +46,65 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { useFocusTrap } from '@/composables/useFocusTrap'; export default {
import { computed, onBeforeUnmount, PropType, useSlots } from 'vue'; name: 'BaseConfirmModal',
import { useI18n } from 'vue-i18n'; props: {
size: {
type: String,
validator: prop => ['small', 'medium', '400', 'large'].includes(prop),
default: 'small'
},
hideFooter: {
type: Boolean,
default: false
},
confirmText: String,
cancelText: String
},
emits: ['confirm', 'hide'],
computed: {
hasHeader () {
return !!this.$slots.header;
},
hasBody () {
return !!this.$slots.body;
},
hasDefault () {
return !!this.$slots.default;
},
modalSizeClass () {
if (this.size === 'small')
return 'modal-sm';
if (this.size === '400')
return 'modal-400';
else if (this.size === 'large')
return 'modal-lg';
else return '';
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
confirmModal () {
this.$emit('confirm');
this.hideModal();
},
const { t } = useI18n(); hideModal () {
this.$emit('hide');
const props = defineProps({ },
size: { onKey (e) {
type: String as PropType<'small' | 'medium' | '400' | 'large'>, e.stopPropagation();
validator: (prop: string) => ['small', 'medium', '400', 'large'].includes(prop), if (e.key === 'Escape')
default: 'small' this.hideModal();
}, }
hideFooter: {
type: Boolean,
default: false
},
confirmText: String,
cancelText: String,
disableAutofocus: {
type: Boolean,
default: false
},
closeOnConfirm: {
type: Boolean,
default: true
} }
});
const emit = defineEmits(['confirm', 'hide']);
const slots = useSlots();
const { trapRef } = useFocusTrap({ disableAutofocus: props.disableAutofocus });
const hasHeader = computed(() => !!slots.header);
const hasBody = computed(() => !!slots.body);
const hasDefault = computed(() => !!slots.default);
const modalSizeClass = computed(() => {
if (props.size === 'small')
return 'modal-sm';
if (props.size === '400')
return 'modal-400';
else if (props.size === 'large')
return 'modal-lg';
else return '';
});
const confirmModal = () => {
emit('confirm');
if (props.closeOnConfirm) hideModal();
}; };
const hideModal = () => {
emit('hide');
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
hideModal();
};
window.addEventListener('keydown', onKey);
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="context" :class="[{ 'bottom': isBottom }, { 'right': isRight }]"> <div class="context" :class="{'bottom': isBottom}">
<a <a
class="context-overlay" class="context-overlay"
@click="close" @click="close"
@@ -15,173 +15,153 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { computed, onBeforeUnmount, onMounted, Ref, ref } from 'vue'; export default {
name: 'BaseContextMenu',
props: {
contextEvent: MouseEvent
},
emits: ['close-context'],
data () {
return {
contextSize: null,
isBottom: false
};
},
computed: {
position () {
let topCord = 0;
let leftCord = 0;
const contextContent: Ref<HTMLDivElement> = ref(null); if (this.contextEvent) {
const contextSize: Ref<{height: number; width: number; subHeight?: number; subWidth?: number}> = ref(null); const { clientY, clientX } = this.contextEvent;
const isBottom: Ref<boolean> = ref(false); topCord = `${clientY + 2}px`;
const isRight: Ref<boolean> = ref(false); leftCord = `${clientX + 5}px`;
const props = defineProps<{ contextEvent: MouseEvent }>();
const emit = defineEmits(['close-context']);
const position = computed(() => { if (this.contextSize) {
let topCord = '0px'; if (clientY + (this.contextSize.height < 200 ? 200 : this.contextSize.height) + 5 >= window.innerHeight) {
let leftCord = '0px'; topCord = `${clientY + 3 - this.contextSize.height}px`;
this.isBottom = true;
}
if (props.contextEvent) { if (clientX + this.contextSize.width + 5 >= window.innerWidth)
const { clientY, clientX } = props.contextEvent; leftCord = `${clientX - this.contextSize.width}px`;
topCord = `${clientY + 2}px`; }
leftCord = `${clientX + 5}px`;
if (contextSize.value) {
if (clientY + (contextSize.value.height < 200 ? 200 : contextSize.value.height) + 5 >= window.innerHeight) {
topCord = `${clientY + 3 - contextSize.value.height}px`;
isBottom.value = true;
} }
if (clientY + contextSize.value.subHeight + contextSize.value.height >= window.innerHeight) return {
isBottom.value = true; top: topCord,
left: leftCord
if (clientX + contextSize.value.width + 5 >= window.innerWidth) { };
leftCord = `${clientX - contextSize.value.width}px`; }
isRight.value = true; },
} created () {
window.addEventListener('keydown', this.onKey);
if (clientX + contextSize.value.subWidth + contextSize.value.width >= window.innerWidth) },
isRight.value = true; mounted () {
if (this.$refs.contextContent)
this.contextSize = this.$refs.contextContent.getBoundingClientRect();
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
close () {
this.$emit('close-context');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.close();
} }
} }
return {
top: topCord,
left: leftCord
};
});
const close = () => {
emit('close-context');
}; };
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
close();
};
window.addEventListener('keydown', onKey);
onMounted(() => {
if (contextContent.value) {
contextSize.value = contextContent.value.getBoundingClientRect();
const submenus = contextContent.value.querySelectorAll<HTMLDivElement>('.context-submenu');
for (const submenu of submenus) {
const submenuSize = submenu.getBoundingClientRect();
if (!contextSize.value.subHeight || submenuSize.height > contextSize.value.subHeight)
contextSize.value.subHeight = submenuSize.height;
if (!contextSize.value.subWidth || submenuSize.width > contextSize.value.subWidth)
contextSize.value.subWidth = submenuSize.width;
}
}
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style lang="scss"> <style lang="scss">
.context { .context {
display: flex; display: flex;
font-size: 16px; font-size: 16px;
z-index: 400; z-index: 400;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
overflow: hidden; overflow: hidden;
position: fixed; position: fixed;
height: 100vh; height: 100vh;
right: 0; right: 0;
top: 0; top: 0;
left: 0; left: 0;
bottom: 0; bottom: 0;
&:not(.bottom) .context-submenu { &:not(.bottom) .context-submenu {
top: -0.2rem; top: -0.2rem;
} }
&.bottom .context-submenu { &.bottom .context-submenu {
bottom: -0.2rem; bottom: -0.2rem;
} }
&:not(.right) .context-submenu { .context-container {
left: 100%; min-width: 100px;
} z-index: 10;
padding: 0;
background: #1d1d1d;
border-radius: $border-radius;
border: 1px solid $bg-color-light-dark;
display: flex;
flex-direction: column;
position: absolute;
pointer-events: initial;
&.right .context-submenu { .context-element {
right: 100%;
}
.context-container {
min-width: 100px;
z-index: 10;
padding: 0;
background: #1d1d1d;
border-radius: $border-radius;
border: 1px solid $bg-color-light-dark;
display: flex; display: flex;
flex-direction: column; align-items: center;
position: absolute; margin: 0.2rem;
pointer-events: initial; padding: 0.1rem 0.3rem;
border-radius: $border-radius;
cursor: pointer;
justify-content: space-between;
position: relative;
white-space: nowrap;
.context-element { .context-submenu {
display: flex; border-radius: $border-radius;
align-items: center; border: 1px solid $bg-color-light-dark;
margin: 0.2rem; opacity: 0;
padding: 0.1rem 0.3rem; visibility: hidden;
border-radius: $border-radius; transition: opacity 0.2s;
cursor: pointer; position: absolute;
justify-content: space-between; left: 100%;
position: relative; min-width: 100px;
white-space: nowrap; background: #1d1d1d;
.context-submenu {
border-radius: $border-radius;
border: 1px solid $bg-color-light-dark;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s;
position: absolute;
min-width: 100px;
background: #1d1d1d;
}
&:hover {
.context-submenu {
display: block;
visibility: visible;
opacity: 1;
}
}
} }
}
.context-overlay { &:hover {
background: transparent; .context-submenu {
bottom: 0; display: block;
cursor: default; visibility: visible;
display: block; opacity: 1;
left: 0; }
position: absolute; }
right: 0; }
top: 0; }
}
.context-overlay {
background: transparent;
bottom: 0;
cursor: default;
display: block;
left: 0;
position: absolute;
right: 0;
top: 0;
}
} }
.disabled { .disabled {
pointer-events: none; pointer-events: none;
filter: grayscale(100%); filter: grayscale(100%);
opacity: 0.5; opacity: 0.5;
} }
</style> </style>

View File

@@ -4,6 +4,11 @@
</div> </div>
</template> </template>
<script>
export default {
name: 'BaseLoader'
};
</script>
<style scoped> <style scoped>
.empty { .empty {
position: absolute; position: absolute;

View File

@@ -1,93 +1,95 @@
<template> <template>
<div id="map" class="map" /> <div id="map" class="map" />
</template> </template>
<script>
<script setup lang="ts"> import L from 'leaflet';
import { onMounted, PropType, Ref, ref } from 'vue';
import * as L from 'leaflet';
import { import {
point, point,
lineString, lineString,
polygon polygon
} from '@turf/helpers'; } from '@turf/helpers';
import { GeoJsonObject } from 'geojson';
import { getArrayDepth } from 'common/libs/getArrayDepth'; import { getArrayDepth } from 'common/libs/getArrayDepth';
interface Coordinates { x: number; y: number } export default {
name: 'BaseMap',
const props = defineProps({ props: {
points: [Object, Array] as PropType<Coordinates | Coordinates[]>, points: [Object, Array],
isMultiSpatial: Boolean isMultiSpatial: Boolean
}); },
const map: Ref<L.Map> = ref(null); data () {
const markers: Ref<GeoJsonObject | GeoJsonObject[]> = ref(null); return {
const center: Ref<[number, number]> = ref(null); map: null,
markers: [],
const getMarkers = (points: Coordinates) => { center: null
if (Array.isArray(points)) { };
if (getArrayDepth(points) === 1) },
return lineString(points.reduce((acc, curr) => [...acc, [curr.x, curr.y]], [])); mounted () {
else if (this.isMultiSpatial) {
return polygon(points.map(arr => arr.reduce((acc: Coordinates[], curr: Coordinates) => [...acc, [curr.x, curr.y]], []))); for (const element of this.points)
} this.markers.push(this.getMarkers(element));
else
return point([points.x, points.y]);
};
onMounted(() => {
if (props.isMultiSpatial) {
for (const element of props.points as Coordinates[])
(markers.value as GeoJsonObject[]).push(getMarkers(element));
}
else {
markers.value = getMarkers(props.points as Coordinates);
if (!Array.isArray(props.points))
center.value = [props.points.y, props.points.x];
}
map.value = L.map('map', {
center: center.value || [0, 0],
zoom: 15,
minZoom: 1,
attributionControl: false
});
L.control.attribution({ prefix: '<b>Leaflet</b>' }).addTo(map.value);
const geoJsonObj = L.geoJSON((markers.value as GeoJsonObject), {
style: function () {
return {
weight: 2,
fillColor: '#ff7800',
color: '#ff7800',
opacity: 0.8,
fillOpacity: 0.4
};
},
pointToLayer: function (feature, latlng) {
return L.circleMarker(latlng, {
radius: 7,
weight: 2,
fillColor: '#ff7800',
color: '#ff7800',
opacity: 0.8,
fillOpacity: 0.4
});
} }
}).addTo(map.value); else {
this.markers = this.getMarkers(this.points);
const southWest = L.latLng(-90, -180); if (!Array.isArray(this.points))
const northEast = L.latLng(90, 180); this.center = [this.points.y, this.points.x];
const bounds = L.latLngBounds(southWest, northEast); }
map.value.setMaxBounds(bounds);
if (!center.value) map.value.fitBounds(geoJsonObj.getBounds()); this.map = L.map('map', {
center: this.center || [0, 0],
zoom: 15,
minZoom: 1,
attributionControl: false
});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { L.control.attribution({ prefix: '<b>Leaflet</b>' }).addTo(this.map);
attribution: '&copy; <b>OpenStreetMap</b>'
}).addTo(map.value); const geoJsonObj = L.geoJSON(this.markers, {
}); style: function () {
return {
weight: 2,
fillColor: '#ff7800',
color: '#ff7800',
opacity: 0.8,
fillOpacity: 0.4
};
},
pointToLayer: function (feature, latlng) {
return L.circleMarker(latlng, {
radius: 7,
weight: 2,
fillColor: '#ff7800',
color: '#ff7800',
opacity: 0.8,
fillOpacity: 0.4
});
}
}).addTo(this.map);
const southWest = L.latLng(-90, -180);
const northEast = L.latLng(90, 180);
const bounds = L.latLngBounds(southWest, northEast);
this.map.setMaxBounds(bounds);
if (!this.center) this.map.fitBounds(geoJsonObj.getBounds());
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <b>OpenStreetMap</b>'
}).addTo(this.map);
},
methods: {
getMarkers (points) {
if (Array.isArray(points)) {
if (getArrayDepth(points) === 1)
return lineString(points.reduce((acc, curr) => [...acc, [curr.x, curr.y]], []));
else
return polygon(points.map(arr => arr.reduce((acc, curr) => [...acc, [curr.x, curr.y]], [])));
}
else
return point([points.x, points.y]);
}
}
};
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -14,58 +14,64 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { computed, ref } from 'vue'; export default {
name: 'BaseNotification',
const props = defineProps({ props: {
message: { message: {
type: String, type: String,
default: '' default: ''
},
status: {
type: String,
default: ''
}
}, },
status: { emits: ['close'],
type: String, data () {
default: '' return {
isExpanded: false
};
},
computed: {
notificationStatus () {
let className = '';
let iconName = '';
switch (this.status) {
case 'success':
className = 'toast-success';
iconName = 'mdi-check';
break;
case 'error':
className = 'toast-error';
iconName = 'mdi-alert-rhombus';
break;
case 'warning':
className = 'toast-warning';
iconName = 'mdi-alert';
break;
case 'primary':
className = 'toast-primary';
iconName = 'mdi-information-outline';
break;
}
return { className, iconName };
},
isExpandable () {
return this.message.length > 80;
}
},
methods: {
hideToast () {
this.$emit('close');
},
toggleExpand () {
this.isExpanded = !this.isExpanded;
}
} }
});
const isExpanded = ref(false);
const emit = defineEmits(['close']);
const notificationStatus = computed(() => {
let className = '';
let iconName = '';
switch (props.status) {
case 'success':
className = 'toast-success';
iconName = 'mdi-check';
break;
case 'error':
className = 'toast-error';
iconName = 'mdi-alert-rhombus';
break;
case 'warning':
className = 'toast-warning';
iconName = 'mdi-alert';
break;
case 'primary':
className = 'toast-primary';
iconName = 'mdi-information-outline';
break;
}
return { className, iconName };
});
const isExpandable = computed(() => props.message.length > 80);
const hideToast = () => {
emit('close');
};
const toggleExpand = () => {
isExpanded.value = !isExpanded.value;
}; };
</script> </script>
<style scoped> <style scoped>
.toast { .toast {
display: flex; display: flex;

View File

@@ -37,8 +37,6 @@
v-if="isOpen" v-if="isOpen"
ref="optionList" ref="optionList"
:class="`select__list-wrapper ${dropdownClass ? dropdownClass : '' }`" :class="`select__list-wrapper ${dropdownClass ? dropdownClass : '' }`"
@mousedown="isMouseDown = true"
@mouseup="handleMouseUpEvent()"
> >
<ul class="select__list" @mousedown.prevent> <ul class="select__list" @mousedown.prevent>
<li <li
@@ -132,18 +130,13 @@ export default defineComponent({
disabled: { disabled: {
type: Boolean, type: Boolean,
default: false default: false
},
maxVisibleOptions: {
type: Number,
default: 100
} }
}, },
emits: ['select', 'open', 'close', 'update:modelValue', 'change', 'blur'], emits: ['select', 'open', 'close', 'update:modelValue', 'change', 'blur'],
setup (props, { emit }) { setup (props, { emit }) {
const hightlightedIndex = ref(0); const hightlightedIndex = ref(0);
const isOpen = ref(false); const isOpen = ref(false);
const isMouseDown = ref(false); const internalValue = ref(props.modelValue || props.value);
const internalValue = ref(props.modelValue !== false ? props.modelValue : props.value);
const el = ref(null); const el = ref(null);
const searchInput = ref(null); const searchInput = ref(null);
const optionList = ref(null); const optionList = ref(null);
@@ -151,14 +144,14 @@ export default defineComponent({
const searchText = ref(''); const searchText = ref('');
const getOptionValue = (opt) => _guess('optionTrackBy', opt); const getOptionValue = (opt) => _guess('optionTrackBy', opt);
const getOptionLabel = (opt) => _guess('optionLabel', opt) + ''; const getOptionLabel = (opt) => _guess('optionLabel', opt);
const getOptionDisabled = (opt) => _guess('optionDisabled', opt); const getOptionDisabled = (opt) => _guess('optionDisabled', opt);
const _guess = (name, item) => { const _guess = (name, item) => {
const prop = props[name]; const prop = props[name];
if (typeof prop === 'function') if (typeof prop === 'function')
return prop(item); return prop(item);
return item[prop] !== undefined ? item[prop] : item; return item[prop] || item;
}; };
const flattenOptions = computed(() => { const flattenOptions = computed(() => {
@@ -204,30 +197,11 @@ export default defineComponent({
}); });
const filteredOptions = computed(() => { const filteredOptions = computed(() => {
const searchTerms = (searchText.value || '').toLowerCase().trim(); const normalizedSearch = (searchText.value || '').toLowerCase().trim();
let options = searchTerms return normalizedSearch
? flattenOptions.value.filter(opt => opt.$type === 'group' || opt.label.trim().toLowerCase().indexOf(searchTerms) !== -1) ? flattenOptions.value.filter(opt => opt.$type === 'group' || opt.label.trim().toLowerCase().indexOf(normalizedSearch) !== -1)
: flattenOptions.value; : flattenOptions.value;
if (options.length > props.maxVisibleOptions) {
let sliceStart = 0;
let sliceEnd = sliceStart + props.maxVisibleOptions;
// if no search active try to open the dropdown showing options around the selected one
if (searchTerms === '') {
const index = internalValue.value ? flattenOptions.value.findIndex(el => el.value === internalValue.value) : -1;
if (index < options.length -1) {
sliceStart = Math.max(0, index - Math.floor(props.maxVisibleOptions / 2));
sliceEnd = Math.min(sliceStart + sliceEnd, options.length -1);
}
}
options = options.slice(sliceStart, sliceEnd);
}
return options;
}); });
const searchInputStyle = computed(() => { const searchInputStyle = computed(() => {
@@ -245,16 +219,8 @@ export default defineComponent({
hightlightedIndex.value = 0; hightlightedIndex.value = 0;
}); });
watch(() => props.modelValue, (val) => {
internalValue.value = val;
});
watch(() => props.value, (val) => {
internalValue.value = val;
});
const currentOptionLabel = computed(() => const currentOptionLabel = computed(() =>
flattenOptions.value.find(d => d.value === internalValue.value)?.label flattenOptions.value.find(d => d.value === props.modelValue)?.label
); );
const select = (opt) => { const select = (opt) => {
@@ -276,7 +242,7 @@ export default defineComponent({
const activate = () => { const activate = () => {
if (isOpen.value || props.disabled) return; if (isOpen.value || props.disabled) return;
isOpen.value = true; isOpen.value = true;
hightlightedIndex.value = filteredOptions.value.findIndex(el => el.value === internalValue.value) || 0; hightlightedIndex.value = flattenOptions.value.findIndex(el => el.value === internalValue.value) || 0;
if (props.searchable) if (props.searchable)
searchInput.value.focus(); searchInput.value.focus();
@@ -308,8 +274,6 @@ export default defineComponent({
}; };
const adjustListPosition = () => { const adjustListPosition = () => {
if (!optionList.value) return;
const element = el.value; const element = el.value;
let { left, top } = element.getBoundingClientRect(); let { left, top } = element.getBoundingClientRect();
const { left: offsetLeft = 0, top: offsetTop = 0 } = props.dropdownOffsets; const { left: offsetLeft = 0, top: offsetTop = 0 } = props.dropdownOffsets;
@@ -355,24 +319,12 @@ export default defineComponent({
}; };
const handleBlurEvent = () => { const handleBlurEvent = () => {
if (isMouseDown.value) return;
deactivate(); deactivate();
emit('blur'); emit('blur');
}; };
const handleMouseUpEvent = () => {
isMouseDown.value = false;
searchInput.value.focus();
};
const handleWheelEvent = (e) => {
if (!e.target.className.includes('select__')) deactivate();
};
onMounted(() => { onMounted(() => {
window.addEventListener('resize', adjustListPosition); window.addEventListener('resize', adjustListPosition);
window.addEventListener('wheel', handleWheelEvent);
nextTick(() => { nextTick(() => {
// fix position when the component is created and opened at the same time // fix position when the component is created and opened at the same time
if (isOpen.value) { if (isOpen.value) {
@@ -384,7 +336,6 @@ export default defineComponent({
}); });
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('resize', adjustListPosition); window.removeEventListener('resize', adjustListPosition);
window.removeEventListener('wheel', handleWheelEvent);
}); });
return { return {
@@ -400,13 +351,10 @@ export default defineComponent({
isSelected, isSelected,
keyArrows, keyArrows,
isOpen, isOpen,
isMouseDown,
hightlightedIndex, hightlightedIndex,
optionList, optionList,
optionRefs, optionRefs,
handleBlurEvent, handleBlurEvent
handleMouseUpEvent,
internalValue
}; };
} }
}); });
@@ -414,55 +362,48 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.select { .select {
display: block; display: block;
&:focus, &:focus, &--open {
&--open { z-index: 10;
z-index: 10; }
}
&__search-input { &__search-input {
appearance: none; appearance: none;
border: none; border: none;
background: transparent; background: transparent;
outline: none; outline: none;
color: currentColor; color: currentColor;
max-width: 100%; max-width: 100%;
width: 100%; width: 100%;
} }
&__item-text { &__list-wrapper {
overflow: hidden; cursor: pointer;
white-space: nowrap; position: fixed;
text-overflow: ellipsis; display: block;
} z-index: 5;
-webkit-overflow-scrolling: touch;
max-height: 240px;
overflow: auto;
left: 0;
top: 40px;
}
&__list-wrapper { &__list {
cursor: pointer; list-style: none;
position: fixed; }
display: block;
z-index: 5;
-webkit-overflow-scrolling: touch;
max-height: 240px;
overflow: auto;
left: 0;
top: 40px;
}
&__list { &__option {
list-style: none; &--disabled {
} opacity: 0.6;
cursor: not-allowed;
}
}
&__option { &--disabled {
&--disabled {
opacity: 0.6; opacity: 0.6;
cursor: not-allowed; cursor: not-allowed;
} }
}
&--disabled {
opacity: 0.6;
cursor: not-allowed;
}
} }
</style> </style>

View File

@@ -9,114 +9,121 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { onMounted, watch } from 'vue';
import * as ace from 'ace-builds'; import * as ace from 'ace-builds';
import 'ace-builds/webpack-resolver';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import 'ace-builds/webpack-resolver';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
const props = defineProps({ export default {
modelValue: String, name: 'BaseTextEditor',
mode: { type: String, default: 'text' }, props: {
editorClass: { type: String, default: '' }, modelValue: String,
autoFocus: { type: Boolean, default: false }, mode: { type: String, default: 'text' },
readOnly: { type: Boolean, default: false }, editorClass: { type: String, default: '' },
showLineNumbers: { type: Boolean, default: true }, autoFocus: { type: Boolean, default: false },
height: { type: Number, default: 200 } readOnly: { type: Boolean, default: false },
}); showLineNumbers: { type: Boolean, default: true },
const emit = defineEmits(['update:modelValue']); height: { type: Number, default: 200 }
const settingsStore = useSettingsStore(); },
emits: ['update:modelValue'],
setup () {
const settingsStore = useSettingsStore();
const { const {
editorTheme, editorTheme,
editorFontSize, editorFontSize,
autoComplete, autoComplete,
lineWrap lineWrap
} = storeToRefs(settingsStore); } = storeToRefs(settingsStore);
let editor: ace.Ace.Editor; return {
const id = uidGen(); editorTheme,
editorFontSize,
autoComplete,
lineWrap
};
},
data () {
return {
editor: null,
id: uidGen()
};
},
watch: {
mode () {
if (this.editor)
this.editor.session.setMode(`ace/mode/${this.mode}`);
},
editorTheme () {
if (this.editor)
this.editor.setTheme(`ace/theme/${this.editorTheme}`);
},
editorFontSize () {
const sizes = {
small: '12px',
medium: '14px',
large: '16px'
};
watch(() => props.mode, () => { if (this.editor) {
if (editor) this.editor.setOptions({
editor.session.setMode(`ace/mode/${props.mode}`); fontSize: sizes[this.editorFontSize]
}); });
}
watch(editorTheme, () => { },
if (editor) autoComplete () {
editor.setTheme(`ace/theme/${editorTheme.value}`); if (this.editor) {
}); this.editor.setOptions({
enableLiveAutocompletion: this.autoComplete
watch(editorFontSize, () => { });
const sizes = { }
xsmall: '10px', },
small: '12px', lineWrap () {
medium: '14px', if (this.editor) {
large: '16px', this.editor.setOptions({
xlarge: '18px', wrap: this.lineWrap
xxlarge: '20px' });
}; }
}
if (editor) { },
editor.setOptions({ mounted () {
fontSize: sizes[editorFontSize.value] this.editor = ace.edit(`editor-${this.id}`, {
mode: `ace/mode/${this.mode}`,
theme: `ace/theme/${this.editorTheme}`,
value: this.modelValue || '',
fontSize: '14px',
printMargin: false,
readOnly: this.readOnly,
showLineNumbers: this.showLineNumbers,
showGutter: this.showLineNumbers
}); });
}
});
watch(autoComplete, () => { this.editor.setOptions({
if (editor) { enableBasicAutocompletion: false,
editor.setOptions({ wrap: this.lineWrap,
enableLiveAutocompletion: autoComplete.value enableSnippets: false,
enableLiveAutocompletion: false
}); });
}
});
watch(lineWrap, () => { this.editor.session.on('change', () => {
if (editor) { const content = this.editor.getValue();
editor.setOptions({ this.$emit('update:modelValue', content);
wrap: lineWrap.value
}); });
}
});
onMounted(() => { if (this.autoFocus) {
editor = ace.edit(`editor-${id}`, { setTimeout(() => {
mode: `ace/mode/${props.mode}`, this.editor.focus();
theme: `ace/theme/${editorTheme.value}`, this.editor.resize();
value: props.modelValue || '', }, 20);
fontSize: 14, }
printMargin: false,
readOnly: props.readOnly,
showLineNumbers: props.showLineNumbers,
showGutter: props.showLineNumbers
});
editor.setOptions({
enableBasicAutocompletion: false,
wrap: lineWrap,
enableSnippets: false,
enableLiveAutocompletion: false
});
(editor.session as unknown as ace.Ace.Editor).on('change', () => {
const content = editor.getValue();
emit('update:modelValue', content);
});
if (props.autoFocus) {
setTimeout(() => { setTimeout(() => {
editor.focus(); this.editor.resize();
editor.resize();
}, 20); }, 20);
} }
};
setTimeout(() => {
editor.resize();
}, 20);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -9,63 +9,67 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { computed, ref, watch } from 'vue'; export default {
name: 'BaseToast',
const props = defineProps({ props: {
message: { message: {
type: String, type: String,
default: '' default: ''
},
status: {
type: String,
default: ''
}
}, },
status: { emits: ['close'],
type: String, data () {
default: '' return {
isVisible: false
};
},
computed: {
toastStatus () {
let className = '';
let iconName = '';
switch (this.status) {
case 'success':
className = 'toast-success';
iconName = 'mdi-check';
break;
case 'error':
className = 'toast-error';
iconName = 'mdi-alert-rhombus';
break;
case 'warning':
className = 'toast-warning';
iconName = 'mdi-alert';
break;
case 'primary':
className = 'toast-primary';
iconName = 'mdi-information-outline';
break;
}
return { className, iconName };
}
},
watch: {
message: function () {
if (this.message)
this.isVisible = true;
else
this.isVisible = false;
}
},
methods: {
hideToast () {
this.isVisible = false;
this.$emit('close');
}
} }
});
const isVisible = ref(false);
const message = ref(props.message);
const emit = defineEmits(['close']);
const toastStatus = computed(() => {
let className = '';
let iconName = '';
switch (props.status) {
case 'success':
className = 'toast-success';
iconName = 'mdi-check';
break;
case 'error':
className = 'toast-error';
iconName = 'mdi-alert-rhombus';
break;
case 'warning':
className = 'toast-warning';
iconName = 'mdi-alert';
break;
case 'primary':
className = 'toast-primary';
iconName = 'mdi-information-outline';
break;
}
return { className, iconName };
});
watch(message, () => {
if (message.value)
isVisible.value = true;
else
isVisible.value = false;
});
const hideToast = () => {
isVisible.value = false;
emit('close');
}; };
</script> </script>
<style scoped> <style scoped>
.toast { .toast {
display: flex; display: flex;

View File

@@ -4,10 +4,10 @@
<i class="mdi mdi-folder-open mr-1" />{{ message }} <i class="mdi mdi-folder-open mr-1" />{{ message }}
</span> </span>
<span class="text-ellipsis file-uploader-value"> <span class="text-ellipsis file-uploader-value">
{{ lastPart(modelValue, 19) }} {{ lastPart(modelValue) }}
</span> </span>
<i <i
v-if="modelValue" v-if="modelValue.length"
class="file-uploader-reset mdi mdi-close" class="file-uploader-reset mdi mdi-close"
@click.prevent="clear" @click.prevent="clear"
/> />
@@ -22,29 +22,40 @@
</label> </label>
</template> </template>
<script setup lang="ts"> <script>
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import { useFilters } from '@/composables/useFilters';
const { lastPart } = useFilters(); export default {
name: 'BaseUploadInput',
defineProps({ props: {
message: { message: {
default: 'Browse', default: 'Browse',
type: String type: String
},
modelValue: {
default: '',
type: String
}
}, },
modelValue: { emits: ['change', 'clear'],
default: '', data () {
type: String return {
id: uidGen()
};
},
methods: {
clear () {
this.$emit('clear');
},
lastPart (string) {
if (!string) return '';
string = string.split(/[/\\]+/).pop();
if (string.length >= 19)
string = `...${string.slice(-19)}`;
return string;
}
} }
});
const emit = defineEmits(['change', 'clear']);
const id = uidGen();
const clear = () => {
emit('clear');
}; };
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div ref="root" class="vscroll-holder"> <div class="vscroll-holder">
<div <div
class="vscroll-spacer" class="vscroll-spacer"
:style="{ :style="{
@@ -20,76 +20,71 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { onBeforeUnmount, onMounted, Ref, ref, watch } from 'vue'; export default {
name: 'BaseVirtualScroll',
props: {
items: Array,
itemHeight: Number,
visibleHeight: Number,
scrollElement: {
type: HTMLDivElement,
default: null
}
},
data () {
return {
topHeight: 0,
bottomHeight: 0,
visibleItems: [],
renderTimeout: null,
localScrollElement: null
};
},
watch: {
scrollElement () {
this.setScrollElement();
}
},
mounted () {
this.setScrollElement();
},
beforeUnmount () {
this.localScrollElement.removeEventListener('scroll', this.checkScrollPosition);
},
methods: {
checkScrollPosition (e) {
clearTimeout(this.renderTimeout);
const props = defineProps({ this.renderTimeout = setTimeout(() => {
items: Array, this.updateWindow(e);
itemHeight: Number, }, 200);
visibleHeight: Number, },
scrollElement: { updateWindow () {
type: HTMLDivElement, const visibleItemsCount = Math.ceil(this.visibleHeight / this.itemHeight);
default: null const totalScrollHeight = this.items.length * this.itemHeight;
const offset = 50;
const scrollTop = this.localScrollElement.scrollTop;
const firstVisibleIndex = Math.floor(scrollTop / this.itemHeight);
const lastVisibleIndex = firstVisibleIndex + visibleItemsCount;
const firstCutIndex = Math.max(firstVisibleIndex - offset, 0);
const lastCutIndex = lastVisibleIndex + offset;
this.visibleItems = this.items.slice(firstCutIndex, lastCutIndex);
this.topHeight = firstCutIndex * this.itemHeight;
this.bottomHeight = totalScrollHeight - this.visibleItems.length * this.itemHeight - this.topHeight;
},
setScrollElement () {
if (this.localScrollElement)
this.localScrollElement.removeEventListener('scroll', this.checkScrollPosition);
this.localScrollElement = this.scrollElement ? this.scrollElement : this.$el;
this.updateWindow();
this.localScrollElement.addEventListener('scroll', this.checkScrollPosition);
}
} }
});
const root = ref(null);
const topHeight: Ref<number> = ref(0);
const bottomHeight: Ref<number> = ref(0);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const visibleItems: Ref<any[]> = ref([]);
const renderTimeout: Ref<NodeJS.Timeout> = ref(null);
const localScrollElement: Ref<HTMLDivElement> = ref(null);
const checkScrollPosition = () => {
clearTimeout(renderTimeout.value);
renderTimeout.value = setTimeout(() => {
updateWindow();
}, 200);
}; };
const updateWindow = () => {
const visibleItemsCount = Math.ceil(props.visibleHeight / props.itemHeight);
const totalScrollHeight = props.items.length * props.itemHeight;
const offset = 50;
const scrollTop = localScrollElement.value.scrollTop;
const firstVisibleIndex = Math.floor(scrollTop / props.itemHeight);
const lastVisibleIndex = firstVisibleIndex + visibleItemsCount;
const firstCutIndex = Math.max(firstVisibleIndex - offset, 0);
const lastCutIndex = lastVisibleIndex + offset;
visibleItems.value = props.items.slice(firstCutIndex, lastCutIndex);
topHeight.value = firstCutIndex * props.itemHeight;
bottomHeight.value = totalScrollHeight - visibleItems.value.length * props.itemHeight - topHeight.value;
};
const setScrollElement = () => {
if (localScrollElement.value)
localScrollElement.value.removeEventListener('scroll', checkScrollPosition);
localScrollElement.value = props.scrollElement ? props.scrollElement : root.value;
updateWindow();
localScrollElement.value.addEventListener('scroll', checkScrollPosition);
};
watch(() => props.scrollElement, () => {
setScrollElement();
});
onMounted(() => {
setScrollElement();
});
onBeforeUnmount(() => {
localScrollElement.value.removeEventListener('scroll', checkScrollPosition);
});
defineExpose({
updateWindow
});
</script> </script>

View File

@@ -4,10 +4,10 @@
v-model="selectedGroup" v-model="selectedGroup"
class="form-select" class="form-select"
:options="[{name: 'manual'}, ...fakerGroups]" :options="[{name: 'manual'}, ...fakerGroups]"
:option-label="(opt: any) => opt.name === 'manual' ? t('message.manualValue') : t(`faker.${opt.name}`)" :option-label="(opt) => opt.name === 'manual' ? $t('message.manualValue') : $t(`faker.${opt.name}`)"
option-track-by="name" option-track-by="name"
:disabled="!isChecked" :disabled="!isChecked"
:style="'flex-grow: 0;'" style="flex-grow: 0;"
@change="onChange" @change="onChange"
/> />
@@ -15,7 +15,7 @@
v-if="selectedGroup !== 'manual'" v-if="selectedGroup !== 'manual'"
v-model="selectedMethod" v-model="selectedMethod"
:options="fakerMethods" :options="fakerMethods"
:option-label="(opt: any) => t(`faker.${opt.name}`)" :option-label="(opt) => $t(`faker.${opt.name}`)"
option-track-by="name" option-track-by="name"
class="form-select" class="form-select"
:disabled="!isChecked" :disabled="!isChecked"
@@ -41,7 +41,7 @@
<BaseUploadInput <BaseUploadInput
v-else-if="inputProps().type === 'file'" v-else-if="inputProps().type === 'file'"
:model-value="selectedValue" :model-value="selectedValue"
:message="t('word.browse')" :message="$t('word.browse')"
@clear="clearValue" @clear="clearValue"
@change="filesChange($event)" @change="filesChange($event)"
/> />
@@ -85,158 +85,153 @@
</fieldset> </fieldset>
</template> </template>
<script setup lang="ts"> <script>
import { computed, PropType, Ref, ref, watch } from 'vue'; import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT, UUID, IS_BIGINT } from 'common/fieldTypes'; import BaseUploadInput from '@/components/BaseUploadInput';
import BaseUploadInput from '@/components/BaseUploadInput.vue'; import ForeignKeySelect from '@/components/ForeignKeySelect';
import ForeignKeySelect from '@/components/ForeignKeySelect.vue';
import FakerMethods from 'common/FakerMethods'; import FakerMethods from 'common/FakerMethods';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n(); export default {
name: 'FakerSelect',
components: {
ForeignKeySelect,
BaseUploadInput,
BaseSelect
},
props: {
type: String,
field: Object,
isChecked: Boolean,
foreignKeys: Array,
keyUsage: Array,
fieldLength: Number,
fieldObj: Object
},
emits: ['update:modelValue'],
data () {
return {
localType: null,
selectedGroup: 'manual',
selectedMethod: '',
selectedValue: '',
debounceTimeout: null,
methodParams: {},
enumArray: null
};
},
computed: {
fakerGroups () {
if ([...TEXT, ...LONG_TEXT].includes(this.type))
this.localType = 'string';
else if (NUMBER.includes(this.type))
this.localType = 'number';
else if (FLOAT.includes(this.type))
this.localType = 'float';
else if ([...DATE, ...DATETIME].includes(this.type))
this.localType = 'datetime';
else if (TIME.includes(this.type))
this.localType = 'time';
else
this.localType = 'none';
const props = defineProps({ return FakerMethods.getGroupsByType(this.localType);
type: String, },
field: Object, fakerMethods () {
isChecked: Boolean, return FakerMethods.getMethods({ type: this.localType, group: this.selectedGroup });
foreignKeys: Array, },
keyUsage: Array as PropType<{field: string}[]>, methodData () {
fieldLength: Number, return this.fakerMethods.find(method => method.name === this.selectedMethod);
fieldObj: Object }
}); },
const emit = defineEmits(['update:modelValue']); watch: {
fieldObj () {
const localType: Ref<string> = ref(null); if (this.fieldObj) {
const selectedGroup: Ref<string> = ref('manual'); if (Array.isArray(this.fieldObj.value)) {
const selectedMethod: Ref<string> = ref(''); this.enumArray = this.fieldObj.value;
const selectedValue: Ref<string> = ref(''); this.selectedValue = this.fieldObj.value[0];
const debounceTimeout: Ref<NodeJS.Timeout> = ref(null); }
const methodParams: Ref<{[key: string]: string}> = ref({}); else
const enumArray: Ref<string[]> = ref(null); this.selectedValue = this.fieldObj.value;
}
const fakerGroups = computed(() => { },
if ([...TEXT, ...LONG_TEXT].includes(props.type)) selectedGroup () {
localType.value = 'string'; if (this.fakerMethods.length)
else if (NUMBER.includes(props.type)) this.selectedMethod = this.fakerMethods[0].name;
localType.value = 'number'; else
else if (FLOAT.includes(props.type)) this.selectedMethod = '';
localType.value = 'float'; },
else if ([...DATE, ...DATETIME].includes(props.type)) selectedMethod () {
localType.value = 'datetime'; this.onChange();
else if (TIME.includes(props.type)) },
localType.value = 'time'; selectedValue () {
else if (UUID.includes(props.type)) clearTimeout(this.debounceTimeout);
localType.value = 'uuid'; this.debounceTimeout = null;
else this.debounceTimeout = setTimeout(() => {
localType.value = 'none'; this.onChange();
}, 200);
return FakerMethods.getGroupsByType(localType.value); }
}); },
methods: {
const fakerMethods = computed(() => { inputProps () {
return FakerMethods.getMethods({ type: localType.value, group: selectedGroup.value }); if ([...TEXT, ...LONG_TEXT].includes(this.type))
}); return { type: 'text', mask: false };
const methodData = computed(() => { if ([...NUMBER, ...FLOAT].includes(this.type))
return fakerMethods.value.find(method => method.name === selectedMethod.value); return { type: 'number', mask: false };
});
if (TIME.includes(this.type)) {
const inputProps = () => { let timeMask = '##:##:##';
if ([...TEXT, ...LONG_TEXT].includes(props.type)) const precision = this.fieldLength;
return { type: 'text', mask: false };
for (let i = 0; i < precision; i++)
if ([...NUMBER, ...FLOAT].includes(props.type)) { timeMask += i === 0 ? '.#' : '#';
if (IS_BIGINT.includes(props.type))
return { type: 'text', mask: false }; return { type: 'text', mask: timeMask };
else }
return { type: 'number', mask: false };
} if (DATE.includes(this.type))
return { type: 'text', mask: '####-##-##' };
if (TIME.includes(props.type)) {
let timeMask = '##:##:##'; if (DATETIME.includes(this.type)) {
const precision = props.fieldLength; let datetimeMask = '####-##-## ##:##:##';
const precision = this.fieldLength;
for (let i = 0; i < precision; i++)
timeMask += i === 0 ? '.#' : '#'; for (let i = 0; i < precision; i++)
datetimeMask += i === 0 ? '.#' : '#';
return { type: 'text', mask: timeMask };
} return { type: 'text', mask: datetimeMask };
}
if (DATE.includes(props.type))
return { type: 'text', mask: '####-##-##' }; if (BLOB.includes(this.type))
return { type: 'file', mask: false };
if (DATETIME.includes(props.type)) {
let datetimeMask = '####-##-## ##:##:##'; if (BIT.includes(this.type))
const precision = props.fieldLength; return { type: 'text', mask: false };
for (let i = 0; i < precision; i++) return { type: 'text', mask: false };
datetimeMask += i === 0 ? '.#' : '#'; },
getKeyUsage (keyName) {
return { type: 'text', mask: datetimeMask }; return this.keyUsage.find(key => key.field === keyName);
} },
filesChange (event) {
if (BLOB.includes(props.type)) const { files } = event.target;
return { type: 'file', mask: false }; if (!files.length) return;
if (BIT.includes(props.type)) this.selectedValue = files[0].path;
return { type: 'text', mask: false }; },
clearValue () {
return { type: 'text', mask: false }; this.selectedValue = '';
}; },
onChange () {
const getKeyUsage = (keyName: string) => { this.$emit('update:modelValue', {
return props.keyUsage.find(key => key.field === keyName); group: this.selectedGroup,
}; method: this.selectedMethod,
params: this.methodParams,
const filesChange = ({ target } : {target: HTMLInputElement }) => { value: this.selectedValue,
const { files } = target; length: this.fieldLength
if (!files.length) return; });
selectedValue.value = files[0].path;
};
const clearValue = () => {
selectedValue.value = '';
};
const onChange = () => {
emit('update:modelValue', {
group: selectedGroup.value,
method: selectedMethod.value,
params: methodParams.value,
value: selectedValue.value,
length: props.fieldLength
});
};
watch(() => props.fieldObj, () => {
if (props.fieldObj) {
if (Array.isArray(props.fieldObj.value)) {
enumArray.value = props.fieldObj.value;
selectedValue.value = props.fieldObj.value[0];
} }
else
selectedValue.value = props.fieldObj.value;
} }
}); };
watch(selectedGroup, () => {
if (fakerMethods.value.length)
selectedMethod.value = fakerMethods.value[0].name;
else
selectedMethod.value = '';
});
watch(selectedMethod, () => {
onChange();
});
watch(selectedValue, () => {
clearTimeout(debounceTimeout.value);
debounceTimeout.value = null;
debounceTimeout.value = setTimeout(() => {
onChange();
}, 200);
});
</script> </script>

View File

@@ -8,101 +8,107 @@
dropdown-class="select-sm" dropdown-class="select-sm"
dropdown-container=".workspace-query-results > .vscroll" dropdown-container=".workspace-query-results > .vscroll"
@change="onChange" @change="onChange"
@blur="emit('blur')" @blur="$emit('blur')"
/> />
</template> </template>
<script setup lang="ts"> <script>
import { computed, Ref, ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { TEXT, LONG_TEXT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT } from 'common/fieldTypes';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { TableField } from 'common/interfaces/antares';
import { useFilters } from '@/composables/useFilters';
const props = defineProps({ export default {
modelValue: [String, Number], name: 'ForeignKeySelect',
keyUsage: Object, components: { BaseSelect },
size: { props: {
type: String, modelValue: [String, Number],
default: '' keyUsage: Object,
} size: {
}); type: String,
default: ''
const emit = defineEmits(['update:modelValue', 'blur']);
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { cutText } = useFilters();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const editField: Ref<HTMLSelectElement> = ref(null);
const foreignList = ref([]);
const currentValue = ref(null);
const isValidDefault = computed(() => {
if (!foreignList.value.length) return true;
if (props.modelValue === null) return false;
return foreignList.value.some(foreign => foreign.foreign_column.toString() === props.modelValue?.toString());
});
const foreigns = computed(() => {
const list = [];
if (!isValidDefault.value)
list.push({ value: props.modelValue, label: props.modelValue === null ? 'NULL' : props.modelValue });
for (const row of foreignList.value)
list.push({ value: row.foreign_column, label: `${row.foreign_column} ${cutText('foreign_description' in row ? ` - ${row.foreign_description}` : '', 15)}` });
return list;
});
const onChange = (opt: HTMLSelectElement) => {
emit('update:modelValue', opt.value);
};
watch(() => props.modelValue, () => {
currentValue.value = props.modelValue;
});
let foreignDesc: string | false;
const params = {
uid: selectedWorkspace.value,
schema: props.keyUsage.refSchema,
table: props.keyUsage.refTable
};
(async () => {
try { // Field data
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') {
const textField = (response as TableField[]).find((field: {type: string; name: string}) => [...TEXT, ...LONG_TEXT].includes(field.type) && field.name !== props.keyUsage.refField);
foreignDesc = textField ? textField.name : false;
} }
else },
addNotification({ status: 'error', message: response }); emits: ['update:modelValue', 'blur'],
} setup () {
catch (err) { const { addNotification } = useNotificationsStore();
addNotification({ status: 'error', message: err.stack }); const workspacesStore = useWorkspacesStore();
}
try { // Foregn list const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { status, response } = await Tables.getForeignList({
...params,
column: props.keyUsage.refField,
description: foreignDesc
});
if (status === 'success') return { addNotification, selectedWorkspace };
foreignList.value = response.rows; },
else data () {
addNotification({ status: 'error', message: response }); return {
foreignList: [],
currentValue: this.modelValue
};
},
computed: {
isValidDefault () {
if (!this.foreignList.length) return true;
if (this.modelValue === null) return false;
return this.foreignList.some(foreign => foreign.foreign_column.toString() === this.modelValue.toString());
},
foreigns () {
const list = [];
if (!this.isValidDefault)
list.push({ value: this.modelValue, label: this.modelValue === null ? 'NULL' : this.modelValue });
for (const row of this.foreignList)
list.push({ value: row.foreign_column, label: `${row.foreign_column} ${this.cutText('foreign_description' in row ? ` - ${row.foreign_description}` : '')}` });
return list;
}
},
async created () {
let foreignDesc;
const params = {
uid: this.selectedWorkspace,
schema: this.keyUsage.refSchema,
table: this.keyUsage.refTable
};
try { // Field data
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') {
const textField = response.find(field => [...TEXT, ...LONG_TEXT].includes(field.type) && field.name !== this.keyUsage.refField);
foreignDesc = textField ? textField.name : false;
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
try { // Foregn list
const { status, response } = await Tables.getForeignList({
...params,
column: this.keyUsage.refField,
description: foreignDesc
});
if (status === 'success')
this.foreignList = response.rows;
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
methods: {
onChange (opt) {
this.$emit('update:modelValue', opt.value);
},
cutText (val) {
if (typeof val !== 'string') return val;
return val.length > 15 ? `${val.substring(0, 15)}...` : val;
}
} }
catch (err) { };
addNotification({ status: 'error', message: err.stack });
}
})();
</script> </script>

View File

@@ -1,122 +0,0 @@
<template>
<div class="form-group has-icon-right m-0">
<input
class="form-input"
type="text"
:value="pressedKeys"
:placeholder="t('message.registerAShortcut')"
@focus="isFocus = true"
@blur="isFocus = false"
@keydown.prevent.stop="onKey"
>
<i class="form-icon mdi mdi-keyboard-outline mdi-24px" />
</div>
</template>
<script setup lang="ts">
import { computed, PropType, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import Application from '@/ipc-api/Application';
const { t } = useI18n();
const emit = defineEmits(['update:modelValue']);
const isMacOS = process.platform === 'darwin';
const props = defineProps({
modelValue: String as PropType<string | Electron.Accelerator>
});
const isFocus = ref(false);
const keyboardEvent: Ref<KeyboardEvent> = ref(null);
const pressedKeys = computed(() => {
const keys: string[] = [];
const singleKeysToIgnore = ['Dead', 'Backspace', 'ArrotLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
const specialKeys = ['Control', 'Alt', 'AltGraph', 'Shift', 'Meta', 'CapsLock', 'ContextMenu', 'Escape'];
const keysFromCode = ['Space', 'Minus', 'Equal', 'Slash', 'Quote', 'Semicolon', 'Comma', 'Period', 'Backslash', 'BracketLeft', 'BracketRight'];
if (props.modelValue && !keyboardEvent.value)
return props.modelValue;
else if (keyboardEvent.value) {
if (keyboardEvent.value.altKey)
keys.push('Alt');
if (keyboardEvent.value.ctrlKey)
keys.push('Control');
if (keyboardEvent.value.metaKey && isMacOS)
keys.push('Command');
if (keyboardEvent.value.shiftKey && keys.length)
keys.push('Shift');
if (keyboardEvent.value.code) {
if (keys.length === 0 && (keyboardEvent.value.key.length === 1 || singleKeysToIgnore.includes(keyboardEvent.value.key)))
return t('message.invalidShortcutMessage');
else if (!specialKeys.includes(keyboardEvent.value.key)) {
if (keyboardEvent.value.key === 'Dead') {
keys.push(keyboardEvent.value.code
.replace('Digit', '')
.replace('Key', '')
.replace('Quote', '\'')
.replace('Backquote', '`'));
}
else if (keysFromCode.includes(keyboardEvent.value.code) || keyboardEvent.value.code.includes('Digit')) {
keys.push(keyboardEvent.value.code
.replace('Quote', '\'')
.replace('Semicolon', ';')
.replace('Slash', '/')
.replace('Backslash', '\\')
.replace('BracketLeft', '[')
.replace('BracketRight', ']')
.replace('Comma', ',')
.replace('Period', '.')
.replace('Minus', '-')
.replace('Equal', '=')
.replace('Digit', '')
.replace('Key', ''));
}
else {
keys.push(keyboardEvent.value.key.length === 1
? keyboardEvent.value.key.toUpperCase()
: keyboardEvent.value.key
.replace('Arrow', '')
);
}
}
else
return t('message.invalidShortcutMessage');
}
}
return keys.join('+');
});
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
e.preventDefault();
keyboardEvent.value = e;
};
watch(pressedKeys, (value) => {
if (value !== t('message.invalidShortcutMessage'))
emit('update:modelValue', pressedKeys.value);
});
watch(isFocus, (val) => {
if (val)
Application.unregisterShortcuts();
else
Application.reloadShortcuts();
});
</script>
<style lang="scss" scoped>
.has-icon-right {
.form-input {
padding-right: 1.8rem;
overflow: hidden;
text-overflow: ellipsis;
caret-color: transparent;
}
.form-icon {
right: 0.4rem;
}
}
</style>

View File

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

View File

@@ -2,11 +2,11 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div class="modal active modal-sm"> <div class="modal active modal-sm">
<a class="modal-overlay" /> <a class="modal-overlay" />
<div ref="trapRef" class="modal-container p-0"> <div class="modal-container p-0">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-key-variant mr-1" /> {{ t('word.credentials') }} <i class="mdi mdi-24px mdi-key-variant mr-1" /> {{ $t('word.credentials') }}
</div> </div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -16,7 +16,7 @@
<form class="form-horizontal"> <form class="form-horizontal">
<div class="form-group"> <div class="form-group">
<div class="col-3"> <div class="col-3">
<label class="form-label">{{ t('word.user') }}</label> <label class="form-label">{{ $t('word.user') }}</label>
</div> </div>
<div class="col-9"> <div class="col-9">
<input <input
@@ -29,7 +29,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-3"> <div class="col-3">
<label class="form-label">{{ t('word.password') }}</label> <label class="form-label">{{ $t('word.password') }}</label>
</div> </div>
<div class="col-9"> <div class="col-9">
<input <input
@@ -44,10 +44,10 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-primary mr-2" @click.stop="sendCredentials"> <button class="btn btn-primary mr-2" @click.stop="sendCredentials">
{{ t('word.send') }} {{ $t('word.send') }}
</button> </button>
<button class="btn btn-link" @click.stop="closeModal"> <button class="btn btn-link" @click.stop="closeModal">
{{ t('word.close') }} {{ $t('word.close') }}
</button> </button>
</div> </div>
</div> </div>
@@ -55,31 +55,30 @@
</Teleport> </Teleport>
</template> </template>
<script setup lang="ts"> <script>
import { Ref, ref } from 'vue'; export default {
import { useFocusTrap } from '@/composables/useFocusTrap'; name: 'ModalAskCredentials',
import { useI18n } from 'vue-i18n'; emits: ['close-asking', 'credentials'],
data () {
const { t } = useI18n(); return {
credentials: {
const { trapRef } = useFocusTrap(); user: '',
password: ''
const credentials = ref({ }
user: '', };
password: '' },
}); created () {
const firstInput: Ref<HTMLInputElement> = ref(null); setTimeout(() => {
const emit = defineEmits(['close-asking', 'credentials']); this.$refs.firstInput.focus();
}, 20);
const closeModal = () => { },
emit('close-asking'); methods: {
closeModal () {
this.$emit('close-asking');
},
sendCredentials () {
this.$emit('credentials', this.credentials);
}
}
}; };
const sendCredentials = () => {
emit('credentials', credentials.value);
};
setTimeout(() => {
firstInput.value.focus();
}, 20);
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<ConfirmModal <ConfirmModal
:confirm-text="t('word.run')" :confirm-text="$t('word.run')"
:cancel-text="t('word.cancel')" :cancel-text="$t('word.cancel')"
size="400" size="400"
@confirm="runRoutine" @confirm="runRoutine"
@hide="closeModal" @hide="closeModal"
@@ -9,7 +9,7 @@
<template #header> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-play mr-1" /> <i class="mdi mdi-24px mdi-play mr-1" />
<span class="cut-text">{{ t('word.parameters') }}: {{ localRoutine.name }}</span> <span class="cut-text">{{ $t('word.parameters') }}: {{ localRoutine.name }}</span>
</div> </div>
</template> </template>
<template #body> <template #body>
@@ -47,76 +47,83 @@
</ConfirmModal> </ConfirmModal>
</template> </template>
<script setup lang="ts"> <script>
import { computed, PropType, Ref, ref } from 'vue';
import { NUMBER, FLOAT } from 'common/fieldTypes'; import { NUMBER, FLOAT } from 'common/fieldTypes';
import { FunctionInfos, RoutineInfos } from 'common/interfaces/antares'; import ConfirmModal from '@/components/BaseConfirmModal';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import { useFilters } from '@/composables/useFilters';
import { useI18n } from 'vue-i18n';
const { t } = useI18n(); export default {
name: 'ModalAskParameters',
const { wrapNumber } = useFilters(); components: {
ConfirmModal
const props = defineProps({ },
localRoutine: Object as PropType<RoutineInfos | FunctionInfos>, props: {
client: String localRoutine: Object,
}); client: String
},
const emit = defineEmits(['confirm', 'close']); emits: ['confirm', 'close'],
data () {
const firstInput: Ref<HTMLInputElement[]> = ref(null); return {
const values: Ref<{[key: string]: string}> = ref({}); values: {}
};
const inParameters = computed(() => { },
return props.localRoutine.parameters.filter(param => param.context === 'IN'); computed: {
}); inParameters () {
return this.localRoutine.parameters.filter(param => param.context === 'IN');
const typeClass = (type: string) => {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
};
const runRoutine = () => {
const valArr = Object.keys(values.value).reduce((acc, curr, i) => {
let qc;
switch (props.client) {
case 'maria':
case 'mysql':
qc = '"';
break;
case 'pg':
qc = '\'';
break;
default:
qc = '"';
} }
},
created () {
window.addEventListener('keydown', this.onKey);
const param = props.localRoutine.parameters.find(param => `${i}-${param.name}` === curr); setTimeout(() => {
this.$refs.firstInput[0].focus();
}, 20);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
runRoutine () {
const valArr = Object.keys(this.values).reduce((acc, curr, i) => {
let qc;
switch (this.client) {
case 'maria':
case 'mysql':
qc = '"';
break;
case 'pg':
qc = '\'';
break;
default:
qc = '"';
}
const value = [...NUMBER, ...FLOAT].includes(param.type) ? values.value[curr] : `${qc}${values.value[curr]}${qc}`; const param = this.localRoutine.parameters.find(param => `${i}-${param.name}` === curr);
acc.push(value);
return acc;
}, []);
emit('confirm', valArr); const value = [...NUMBER, ...FLOAT].includes(param.type) ? this.values[curr] : `${qc}${this.values[curr]}${qc}`;
acc.push(value);
return acc;
}, []);
this.$emit('confirm', valArr);
},
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
}
}; };
const closeModal = () => emit('close');
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
window.addEventListener('keydown', onKey);
setTimeout(() => {
firstInput.value[0].focus();
}, 20);
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,195 +0,0 @@
<template>
<Teleport to="#window-content">
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div ref="trapRef" class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-brush-variant mr-1" />
<span class="cut-text">{{ t('message.editConnectionAppearence') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal">
<div class="form-group mb-4">
<div class="col-3">
<label class="form-label">{{ t('word.label') }}</label>
</div>
<div class="col-9">
<input
ref="firstInput"
v-model="localConnection.name"
class="form-input"
type="text"
:placeholder="getConnectionName(localConnection.uid)"
>
</div>
</div>
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ t('word.icon') }}</label>
</div>
<div class="col-9 icons-wrapper">
<div
v-for="icon in icons"
:key="icon.name"
class="icon-box"
:title="icon.name"
:class="[icon.code ? `mdi ${icon.code} mdi-36px` : `dbi dbi-${connection.client}`, {'selected': localConnection.icon === icon.code}]"
@click="localConnection.icon = icon.code"
/>
</div>
</div>
</form>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary mr-2" @click.stop="editFolderAppearence">
{{ t('word.update') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
{{ t('word.close') }}
</button>
</div>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { onBeforeUnmount, PropType, Ref, ref } from 'vue';
import { useFocusTrap } from '@/composables/useFocusTrap';
import { useI18n } from 'vue-i18n';
import { SidebarElement, useConnectionsStore } from '@/stores/connections';
import { unproxify } from '@/libs/unproxify';
const connectionsStore = useConnectionsStore();
const { t } = useI18n();
const props = defineProps({
connection: {
type: Object as PropType<SidebarElement>,
required: true
}
});
const emit = defineEmits(['close']);
const { updateConnectionOrder, getConnectionName } = connectionsStore;
const icons = [
{ name: 'default', code: null },
// Symbols
{ name: 'account-group', code: 'mdi-account-group-outline' },
{ name: 'cloud', code: 'mdi-cloud-outline' },
{ name: 'key-chain', code: 'mdi-key-chain-variant' },
{ name: 'lightning-bolt', code: 'mdi-lightning-bolt' },
{ name: 'map-marker', code: 'mdi-map-marker-radius-outline' },
{ name: 'api', code: 'mdi-api' },
{ name: 'chart-line', code: 'mdi-chart-line' },
{ name: 'chat', code: 'mdi-chat-outline' },
{ name: 'bug', code: 'mdi-bug-outline' },
{ name: 'shield', code: 'mdi-shield-outline' },
{ name: 'cart', code: 'mdi-cart-variant' },
{ name: 'bank', code: 'mdi-bank-outline' },
{ name: 'receipt', code: 'mdi-receipt-text-outline' },
{ name: 'raspberry-pi', code: 'mdi-raspberry-pi' },
{ name: 'book', code: 'mdi-book-outline' },
{ name: 'web', code: 'mdi-web' },
{ name: 'multimedia', code: 'mdi-multimedia' },
{ name: 'qr-code', code: 'mdi-qrcode' },
{ name: 'flask', code: 'mdi-flask-outline' },
{ name: 'memory', code: 'mdi-memory' },
{ name: 'cube', code: 'mdi-cube-outline' },
{ name: 'weather', code: 'mdi-weather-partly-snowy-rainy' },
{ name: 'controller', code: 'mdi-controller' },
{ name: 'home-group', code: 'mdi-home-group' },
// Vehicles
{ name: 'truck', code: 'mdi-truck-outline' },
{ name: 'car', code: 'mdi-car' },
{ name: 'motorbike', code: 'mdi-atv' },
{ name: 'train', code: 'mdi-train' },
{ name: 'airplane', code: 'mdi-airplane' },
{ name: 'ferry', code: 'mdi-ferry' },
// Brand
{ name: 'docker', code: 'mdi-docker' },
{ name: 'open-source', code: 'mdi-open-source-initiative' },
{ name: 'aws', code: 'mdi-aws' },
{ name: 'google-cloud', code: 'mdi-google-cloud' },
{ name: 'microsoft-azure', code: 'mdi-microsoft-azure' },
{ name: 'linux', code: 'mdi-linux' },
{ name: 'microsoft-windows', code: 'mdi-microsoft-windows' },
{ name: 'apple', code: 'mdi-apple' },
{ name: 'android', code: 'mdi-android' }
];
const { trapRef } = useFocusTrap();
const firstInput: Ref<HTMLInputElement> = ref(null);
const localConnection: Ref<SidebarElement> = ref(unproxify(props.connection));
const editFolderAppearence = () => {
updateConnectionOrder(localConnection.value);
closeModal();
};
const closeModal = () => emit('close');
const onKey =(e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script>
<style scoped lang="scss">
.modal-container {
max-width: 360px;
}
.icons-wrapper{
display: grid;
grid-template-columns: repeat(auto-fill, 40px);
gap: 5px;
.icon-box {
height: 40px;
width: 40px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&.selected {
outline: 2px solid $primary-color;
border-radius: 8px;
}
}
}
.theme-light {
.icons-wrapper {
.dbi {
filter: invert(100%) opacity(.8);
&.selected {
outline-color: #1c96d6;
}
}
}
}
</style>

View File

@@ -1,43 +1,46 @@
<template> <template>
<ConfirmModal <ConfirmModal
:confirm-text="t('word.discard')" :confirm-text="$t('word.discard')"
:cancel-text="t('word.stay')" :cancel-text="$t('word.stay')"
@confirm="emit('confirm')" @confirm="$emit('confirm')"
@hide="emit('close')" @hide="$emit('close')"
> >
<template #header> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-content-save-alert mr-1" /> {{ t('message.unsavedChanges') }} <i class="mdi mdi-24px mdi-content-save-alert mr-1" /> {{ $t('message.unsavedChanges') }}
</div> </div>
</template> </template>
<template #body> <template #body>
<div> <div>
{{ t('message.discardUnsavedChanges') }} {{ $t('message.discardUnsavedChanges') }}
</div> </div>
</template> </template>
</ConfirmModal> </ConfirmModal>
</template> </template>
<script setup lang="ts"> <script>
import ConfirmModal from '@/components/BaseConfirmModal.vue'; import ConfirmModal from '@/components/BaseConfirmModal';
import { onBeforeUnmount } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n(); export default {
name: 'ModalDiscardChanges',
const emit = defineEmits(['confirm', 'close']); components: {
ConfirmModal
const onKey = (e: KeyboardEvent) => { },
e.stopPropagation(); emits: ['confirm', 'close'],
if (e.key === 'Escape') created () {
emit('close'); window.addEventListener('keydown', this.onKey);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
}; };
window.addEventListener('keydown', onKey);
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style scoped> <style scoped>

View File

@@ -2,12 +2,12 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div class="modal active"> <div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" /> <a class="modal-overlay" @click.stop="closeModal" />
<div ref="trapRef" class="modal-container p-0"> <div class="modal-container p-0">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-database-edit mr-1" /> <i class="mdi mdi-24px mdi-database-edit mr-1" />
<span class="cut-text">{{ t('message.editSchema') }}</span> <span class="cut-text">{{ $t('message.editSchema') }}</span>
</div> </div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -17,7 +17,7 @@
<form class="form-horizontal"> <form class="form-horizontal">
<div class="form-group"> <div class="form-group">
<div class="col-3"> <div class="col-3">
<label class="form-label">{{ t('word.name') }}</label> <label class="form-label">{{ $t('word.name') }}</label>
</div> </div>
<div class="col-9"> <div class="col-9">
<input <input
@@ -26,25 +26,24 @@
class="form-input" class="form-input"
type="text" type="text"
required required
:placeholder="t('message.schemaName')" :placeholder="$t('message.schemaName')"
readonly readonly
> >
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-3"> <div class="col-3">
<label class="form-label">{{ t('word.collation') }}</label> <label class="form-label">{{ $t('word.collation') }}</label>
</div> </div>
<div class="col-9"> <div class="col-9">
<BaseSelect <BaseSelect
v-model="database.collation" v-model="database.collation"
class="form-select" class="form-select"
:options="collations" :options="collations"
:max-visible-options="1000"
option-label="collation" option-label="collation"
option-track-by="collation" option-track-by="collation"
/> />
<small>{{ t('message.serverDefault') }}: {{ defaultCollation }}</small> <small>{{ $t('message.serverDefault') }}: {{ defaultCollation }}</small>
</div> </div>
</div> </div>
</form> </form>
@@ -52,10 +51,10 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-primary mr-2" @click.stop="updateSchema"> <button class="btn btn-primary mr-2" @click.stop="updateSchema">
{{ t('word.update') }} {{ $t('word.update') }}
</button> </button>
<button class="btn btn-link" @click.stop="closeModal"> <button class="btn btn-link" @click.stop="closeModal">
{{ t('word.close') }} {{ $t('word.close') }}
</button> </button>
</div> </div>
</div> </div>
@@ -63,108 +62,119 @@
</Teleport> </Teleport>
</template> </template>
<script setup lang="ts"> <script>
import { computed, onBeforeUnmount, Ref, ref } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { useFocusTrap } from '@/composables/useFocusTrap';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n(); export default {
name: 'ModalEditSchema',
components: {
BaseSelect
},
props: {
selectedSchema: String
},
emits: ['close'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const props = defineProps({ const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
selectedSchema: String
});
const emit = defineEmits(['close']); const { getWorkspace, getDatabaseVariable } = workspacesStore;
const { addNotification } = useNotificationsStore(); return {
const workspacesStore = useWorkspacesStore(); addNotification,
selectedWorkspace,
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); getWorkspace,
getDatabaseVariable
const { getWorkspace, getDatabaseVariable } = workspacesStore; };
},
const { trapRef } = useFocusTrap(); data () {
return {
const firstInput: Ref<HTMLInputElement> = ref(null); database: {
const database = ref({ name: '',
name: '', prevName: '',
prevName: '', collation: ''
collation: '', }
prevCollation: null };
}); },
computed: {
const collations = computed(() => getWorkspace(selectedWorkspace.value).collations); collations () {
const defaultCollation = computed(() => (getDatabaseVariable(selectedWorkspace.value, 'collation_server').value || '')); return this.getWorkspace(this.selectedWorkspace).collations;
},
const updateSchema = async () => { defaultCollation () {
if (database.value.collation !== database.value.prevCollation) { return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
}
},
async created () {
let actualCollation;
try { try {
const { status, response } = await Schema.updateSchema({ const { status, response } = await Schema.getDatabaseCollation({ uid: this.selectedWorkspace, database: this.selectedSchema });
uid: selectedWorkspace.value,
...database.value
});
if (status === 'success') if (status === 'success')
closeModal(); actualCollation = response;
else else
addNotification({ status: 'error', message: response }); this.addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
addNotification({ status: 'error', message: err.stack }); this.addNotification({ status: 'error', message: err.stack });
}
this.database = {
name: this.selectedSchema,
prevName: this.selectedSchema,
collation: actualCollation || this.defaultCollation,
prevCollation: actualCollation || this.defaultCollation
};
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async updateSchema () {
if (this.database.collation !== this.database.prevCollation) {
try {
const { status, response } = await Schema.updateSchema({
uid: this.selectedWorkspace,
...this.database
});
if (status === 'success')
this.closeModal();
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
}
else
this.closeModal();
},
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
} }
} }
else closeModal();
}; };
const closeModal = () => emit('close');
const onKey =(e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
(async () => {
let actualCollation;
try {
const { status, response } = await Schema.getDatabaseCollation({ uid: selectedWorkspace.value, database: props.selectedSchema });
if (status === 'success')
actualCollation = response;
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
database.value = {
name: props.selectedSchema,
prevName: props.selectedSchema,
collation: actualCollation || defaultCollation.value,
prevCollation: actualCollation || defaultCollation.value
};
window.addEventListener('keydown', onKey);
setTimeout(() => {
firstInput.value.focus();
}, 20);
})();
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style scoped lang="scss"> <style scoped>
.modal-container { .modal-container {
max-width: 360px; max-width: 360px;
} }

View File

@@ -2,12 +2,12 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div class="modal active"> <div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" /> <a class="modal-overlay" @click.stop="closeModal" />
<div ref="trapRef" class="modal-container p-0"> <div class="modal-container p-0">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-database-arrow-down mr-1" /> <i class="mdi mdi-24px mdi-database-arrow-down mr-1" />
<span class="cut-text">{{ t('message.exportSchema') }}</span> <span class="cut-text">{{ $t('message.exportSchema') }}</span>
</div> </div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -16,7 +16,7 @@
<div class="container"> <div class="container">
<div class="columns"> <div class="columns">
<div class="col-3"> <div class="col-3">
<label class="form-label">{{ t('message.directoryPath') }}</label> <label class="form-label">{{ $t('message.directoryPath') }}</label>
</div> </div>
<div class="col-9"> <div class="col-9">
<fieldset class="input-group"> <fieldset class="input-group">
@@ -26,14 +26,14 @@
type="text" type="text"
required required
readonly readonly
:placeholder="t('message.schemaName')" :placeholder="$t('message.schemaName')"
> >
<button <button
type="button" type="button"
class="btn btn-primary input-group-btn" class="btn btn-primary input-group-btn"
@click.prevent="openPathDialog" @click.prevent="openPathDialog"
> >
{{ t('word.change') }} {{ $t('word.change') }}
</button> </button>
</fieldset> </fieldset>
</div> </div>
@@ -51,14 +51,14 @@
<div class="column col-auto col-ml-auto "> <div class="column col-auto col-ml-auto ">
<button <button
class="btn btn-dark btn-sm" class="btn btn-dark btn-sm"
:title="t('word.refresh')" :title="$t('word.refresh')"
@click="refresh" @click="refresh"
> >
<i class="mdi mdi-database-refresh" /> <i class="mdi mdi-database-refresh" />
</button> </button>
<button <button
class="btn btn-dark btn-sm mx-1" class="btn btn-dark btn-sm mx-1"
:title="t('message.uncheckAllTables')" :title="$t('message.uncheckAllTables')"
:disabled="isRefreshing" :disabled="isRefreshing"
@click="uncheckAllTables" @click="uncheckAllTables"
> >
@@ -66,7 +66,7 @@
</button> </button>
<button <button
class="btn btn-dark btn-sm" class="btn btn-dark btn-sm"
:title="t('message.checkAllTables')" :title="$t('message.checkAllTables')"
:disabled="isRefreshing" :disabled="isRefreshing"
@click="checkAllTables" @click="checkAllTables"
> >
@@ -122,22 +122,22 @@
<div class="tr"> <div class="tr">
<div class="th" style="width: 50%;"> <div class="th" style="width: 50%;">
<div class="table-column-title"> <div class="table-column-title">
<span>{{ t('word.table') }}</span> <span>{{ $t('word.table') }}</span>
</div> </div>
</div> </div>
<div class="th text-center"> <div class="th text-center">
<div class="table-column-title"> <div class="table-column-title">
<span>{{ t('word.structure') }}</span> <span>{{ $t('word.structure') }}</span>
</div> </div>
</div> </div>
<div class="th text-center"> <div class="th text-center">
<div class="table-column-title"> <div class="table-column-title">
<span>{{ t('word.content') }}</span> <span>{{ $t('word.content') }}</span>
</div> </div>
</div> </div>
<div class="th text-center"> <div class="th text-center">
<div class="table-column-title"> <div class="table-column-title">
<span>{{ t('word.drop') }}</span> <span>{{ $t('word.drop') }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -146,7 +146,7 @@
<div class="tbody"> <div class="tbody">
<div <div
v-for="item in tables" v-for="item in tables"
:key="item.table" :key="item.name"
class="tr" class="tr"
> >
<div class="td"> <div class="td">
@@ -183,19 +183,19 @@
</div> </div>
<div class="column col-4"> <div class="column col-4">
<h5 class="h5"> <h5 class="h5">
{{ t('word.options') }} {{ $t('word.options') }}
</h5> </h5>
<span class="h6">{{ t('word.includes') }}:</span> <span class="h6">{{ $t('word.includes') }}:</span>
<label <label
v-for="(_, key) in options.includes" v-for="(_, key) in options.includes"
:key="key" :key="key"
class="form-checkbox" class="form-checkbox"
> >
<input v-model="options.includes[key]" type="checkbox"><i class="form-icon" /> {{ t(`word.${key}`, 2) }} <input v-model="options.includes[key]" type="checkbox"><i class="form-icon" /> {{ $tc(`word.${key}`, 2) }}
</label> </label>
<div v-if="clientCustoms.exportByChunks"> <div v-if="customizations.exportByChunks">
<div class="h6 mt-4 mb-2"> <div class="h6 mt-4 mb-2">
{{ t('message.newInserStmtEvery') }}: {{ $t('message.newInserStmtEvery') }}:
</div> </div>
<div class="columns"> <div class="columns">
<div class="column col-6"> <div class="column col-6">
@@ -209,21 +209,21 @@
<BaseSelect <BaseSelect
v-model="options.sqlInsertDivider" v-model="options.sqlInsertDivider"
class="form-select" class="form-select"
:options="[{value: 'bytes', label: 'KiB'}, {value: 'rows', label: t('word.row', 2)}]" :options="[{value: 'bytes', label: 'KiB'}, {value: 'rows', label: $tc('word.row', 2)}]"
/> />
</div> </div>
</div> </div>
</div> </div>
<div class="h6 mb-2 mt-4"> <div class="h6 mb-2 mt-4">
{{ t('message.ourputFormat') }}: {{ $t('message.ourputFormat') }}:
</div> </div>
<div class="columns"> <div class="columns">
<div class="column h5 mb-4"> <div class="column h5 mb-4">
<BaseSelect <BaseSelect
v-model="options.outputFormat" v-model="options.outputFormat"
class="form-select" class="form-select"
:options="[{value: 'sql', label: t('message.singleFile', {ext: '.sql'})}, {value: 'sql.zip', label: t('message.zipCompressedFile', {ext: '.sql'})}]" :options="[{value: 'sql', label: $t('message.singleFile', {ext: '.sql'})}, {value: 'sql.zip', label: $t('message.zipCompressedFile', {ext: '.sql'})}]"
/> />
</div> </div>
</div> </div>
@@ -245,7 +245,7 @@
</div> </div>
<div class="column col-auto px-0"> <div class="column col-auto px-0">
<button class="btn btn-link" @click.stop="closeModal"> <button class="btn btn-link" @click.stop="closeModal">
{{ t('word.close') }} {{ $t('word.close') }}
</button> </button>
<button <button
class="btn btn-primary mr-2" class="btn btn-primary mr-2"
@@ -254,7 +254,7 @@
autofocus autofocus
@click.prevent="startExport" @click.prevent="startExport"
> >
{{ t('word.export') }} {{ $t('word.export') }}
</button> </button>
</div> </div>
</div> </div>
@@ -263,213 +263,211 @@
</Teleport> </Teleport>
</template> </template>
<script setup lang="ts"> <script>
import { computed, onBeforeUnmount, Ref, ref } from 'vue'; import moment from 'moment';
import * as moment from 'moment';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { ClientCode, SchemaInfos } from 'common/interfaces/antares';
import { ExportOptions, ExportState } from 'common/interfaces/exporter';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { useFocusTrap } from '@/composables/useFocusTrap'; import customizations from 'common/customizations';
import Application from '@/ipc-api/Application'; import Application from '@/ipc-api/Application';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { Customizations } from 'common/interfaces/customizations';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
const props = defineProps({ export default {
selectedSchema: String name: 'ModalExportSchema',
}); components: {
BaseSelect
},
props: {
selectedSchema: String
},
emits: ['close'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const emit = defineEmits(['close']); const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { t } = useI18n();
const { addNotification } = useNotificationsStore(); const {
const workspacesStore = useWorkspacesStore(); getWorkspace,
getDatabaseVariable,
refreshSchema
} = workspacesStore;
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); return {
addNotification,
selectedWorkspace,
getWorkspace,
getDatabaseVariable,
refreshSchema
};
},
data () {
return {
isExporting: false,
isRefreshing: false,
progressPercentage: 0,
progressStatus: '',
tables: [],
options: {
includes: {},
outputFormat: 'sql',
sqlInsertAfter: 250,
sqlInsertDivider: 'bytes'
},
basePath: ''
};
},
computed: {
currentWorkspace () {
return this.getWorkspace(this.selectedWorkspace);
},
customizations () {
return this.currentWorkspace.customizations;
},
schemaItems () {
const db = this.currentWorkspace.structure.find(db => db.name === this.selectedSchema);
if (db)
return db.tables.filter(table => table.type === 'table');
const { trapRef } = useFocusTrap(); return [];
},
filename () {
const date = moment().format('YYYY-MM-DD');
return `${this.selectedSchema}_${date}.${this.options.outputFormat}`;
},
dumpFilePath () {
return `${this.basePath}/${this.filename}`;
},
includeStructureStatus () {
if (this.tables.every(item => item.includeStructure)) return 1;
else if (this.tables.some(item => item.includeStructure)) return 2;
else return 0;
},
includeContentStatus () {
if (this.tables.every(item => item.includeContent)) return 1;
else if (this.tables.some(item => item.includeContent)) return 2;
else return 0;
},
includeDropStatementStatus () {
if (this.tables.every(item => item.includeDropStatement)) return 1;
else if (this.tables.some(item => item.includeDropStatement)) return 2;
else return 0;
}
},
async created () {
if (!this.schemaItems.length) await this.refresh();
const { window.addEventListener('keydown', this.onKey);
getWorkspace,
refreshSchema
} = workspacesStore;
const isExporting = ref(false); this.basePath = await Application.getDownloadPathDirectory();
const isRefreshing = ref(false); this.tables = this.schemaItems.map(item => ({
const progressPercentage = ref(0); table: item.name,
const progressStatus = ref(''); includeStructure: true,
const tables: Ref<{ includeContent: true,
table: string; includeDropStatement: true
includeStructure: boolean; }));
includeContent: boolean;
includeDropStatement: boolean;
}[]> = ref([]);
const options: Ref<Partial<ExportOptions>> = ref({
schema: props.selectedSchema,
includes: {} as {[key: string]: boolean},
outputFormat: 'sql' as 'sql' | 'sql.zip',
sqlInsertAfter: 250,
sqlInsertDivider: 'bytes' as 'bytes' | 'rows'
});
const basePath = ref('');
const currentWorkspace = computed(() => getWorkspace(selectedWorkspace.value)); const structure = ['functions', 'views', 'triggers', 'routines', 'schedulers'];
const clientCustoms: Ref<Customizations> = computed(() => currentWorkspace.value.customizations);
const schemaItems = computed(() => {
const db: SchemaInfos = currentWorkspace.value.structure.find((db: SchemaInfos) => db.name === props.selectedSchema);
if (db)
return db.tables.filter(table => table.type === 'table');
return []; structure.forEach(feat => {
}); const val = customizations[this.currentWorkspace.client][feat];
const filename = computed(() => { if (val)
const date = moment().format('YYYY-MM-DD'); this.options.includes[feat] = true;
return `${props.selectedSchema}_${date}.${options.value.outputFormat}`; });
});
const dumpFilePath = computed(() => `${basePath.value}/${filename.value}`);
const includeStructureStatus = computed(() => {
if (tables.value.every(item => item.includeStructure)) return 1;
else if (tables.value.some(item => item.includeStructure)) return 2;
else return 0;
});
const includeContentStatus = computed(() => {
if (tables.value.every(item => item.includeContent)) return 1;
else if (tables.value.some(item => item.includeContent)) return 2;
else return 0;
});
const includeDropStatementStatus = computed(() => {
if (tables.value.every(item => item.includeDropStatement)) return 1;
else if (tables.value.some(item => item.includeDropStatement)) return 2;
else return 0;
});
const startExport = async () => { ipcRenderer.on('export-progress', this.updateProgress);
isExporting.value = true; },
const { uid, client } = currentWorkspace.value; beforeUnmount () {
const params = { window.removeEventListener('keydown', this.onKey);
uid, ipcRenderer.off('export-progress', this.updateProgress);
type: client, },
schema: props.selectedSchema, methods: {
outputFile: dumpFilePath.value, async startExport () {
tables: [...tables.value], this.isExporting = true;
...options.value const { uid, client } = this.currentWorkspace;
} as ExportOptions & { uid: string; type: ClientCode }; const params = {
uid,
type: client,
schema: this.selectedSchema,
outputFile: this.dumpFilePath,
tables: [...this.tables],
...this.options
};
try { try {
const { status, response } = await Schema.export(params); const { status, response } = await Schema.export(params);
if (status === 'success') if (status === 'success')
progressStatus.value = response.cancelled ? t('word.aborted') : t('word.completed'); this.progressStatus = response.cancelled ? this.$t('word.aborted') : this.$t('word.completed');
else { else {
progressStatus.value = response; this.progressStatus = response;
addNotification({ status: 'error', message: response }); this.addNotification({ status: 'error', message: response });
}
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isExporting = false;
},
updateProgress (event, state) {
this.progressPercentage = Number((state.currentItemIndex / state.totalItems * 100).toFixed(1));
switch (state.op) {
case 'PROCESSING':
this.progressStatus = this.$t('message.processingTableExport', { table: state.currentItem });
break;
case 'FETCH':
this.progressStatus = this.$t('message.fechingTableExport', { table: state.currentItem });
break;
case 'WRITE':
this.progressStatus = this.$t('message.writingTableExport', { table: state.currentItem });
break;
}
},
async closeModal () {
let willClose = true;
if (this.isExporting) {
willClose = false;
const { response } = await Schema.abortExport();
willClose = response.willAbort;
}
if (willClose)
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
checkAllTables () {
this.tables = this.tables.map(item => ({ ...item, includeStructure: true, includeContent: true, includeDropStatement: true }));
},
uncheckAllTables () {
this.tables = this.tables.map(item => ({ ...item, includeStructure: false, includeContent: false, includeDropStatement: false }));
},
toggleAllTablesOption (option) {
const options = ['includeStructure', 'includeContent', 'includeDropStatement'];
if (!options.includes(option)) return;
if (this[`${option}Status`] !== 1)
this.tables = this.tables.map(item => ({ ...item, [option]: true }));
else
this.tables = this.tables.map(item => ({ ...item, [option]: false }));
},
async refresh () {
this.isRefreshing = true;
await this.refreshSchema({ uid: this.currentWorkspace.uid, schema: this.selectedSchema });
this.isRefreshing = false;
},
async openPathDialog () {
const result = await Application.showOpenDialog({ properties: ['openDirectory'] });
if (result && !result.canceled)
this.basePath = result.filePaths[0];
} }
} }
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
isExporting.value = false;
}; };
const updateProgress = (event: Event, state: ExportState) => {
progressPercentage.value = Number((state.currentItemIndex / state.totalItems * 100).toFixed(1));
switch (state.op) {
case 'PROCESSING':
progressStatus.value = t('message.processingTableExport', { table: state.currentItem });
break;
case 'FETCH':
progressStatus.value = t('message.fechingTableExport', { table: state.currentItem });
break;
case 'WRITE':
progressStatus.value = t('message.writingTableExport', { table: state.currentItem });
break;
}
};
const closeModal = async () => {
let willClose = true;
if (isExporting.value) {
willClose = false;
const { response } = await Schema.abortExport();
willClose = response.willAbort;
}
if (willClose)
emit('close');
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
const checkAllTables = () => {
tables.value = tables.value.map(item => ({ ...item, includeStructure: true, includeContent: true, includeDropStatement: true }));
};
const uncheckAllTables = () => {
tables.value = tables.value.map(item => ({ ...item, includeStructure: false, includeContent: false, includeDropStatement: false }));
};
const toggleAllTablesOption = (option: 'includeStructure' | 'includeContent' |'includeDropStatement') => {
const options = {
includeStructure: includeStructureStatus.value,
includeContent: includeContentStatus.value,
includeDropStatement: includeDropStatementStatus.value
};
if (options[option] !== 1)
tables.value = tables.value.map(item => ({ ...item, [option]: true }));
else
tables.value = tables.value.map(item => ({ ...item, [option]: false }));
};
const refresh = async () => {
isRefreshing.value = true;
await refreshSchema({ uid: currentWorkspace.value.uid, schema: props.selectedSchema });
isRefreshing.value = false;
};
const openPathDialog = async () => {
const result = await Application.showOpenDialog({ properties: ['openDirectory'] });
if (result && !result.canceled)
basePath.value = result.filePaths[0];
};
(async () => {
if (!schemaItems.value.length) await refresh();
window.addEventListener('keydown', onKey);
basePath.value = await Application.getDownloadPathDirectory();
tables.value = schemaItems.value.map(item => ({
table: item.name,
includeStructure: true,
includeContent: true,
includeDropStatement: true
}));
const structure = ['functions', 'views', 'triggers', 'routines', 'schedulers'];
structure.forEach((feat: keyof Customizations) => {
const val = clientCustoms.value[feat];
if (val)
options.value.includes[feat] = true;
});
ipcRenderer.on('export-progress', updateProgress);
})();
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
ipcRenderer.off('export-progress', updateProgress);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -478,15 +476,14 @@ onBeforeUnmount(() => {
overflow: hidden; overflow: hidden;
.left { .left {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
} }
} }
.workspace-query-results { .workspace-query-results {
flex: 1 0 1px; flex: 1 0 1px;
.table { .table {
width: 100% !important; width: 100% !important;
} }
@@ -502,24 +499,25 @@ onBeforeUnmount(() => {
} }
.modal { .modal {
.modal-container { .modal-container {
max-width: 800px; max-width: 800px;
} }
.modal-body { .modal-body {
max-height: 60vh; max-height: 60vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.modal-footer { .modal-footer {
display: flex; display: flex;
} }
} }
.progress-status { .progress-status {
font-style: italic; font-style: italic;
font-size: 80%; font-size: 80%;
} }
</style> </style>

View File

@@ -2,12 +2,12 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div class="modal active"> <div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" /> <a class="modal-overlay" @click.stop="closeModal" />
<div ref="trapRef" class="modal-container p-0"> <div class="modal-container p-0">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-plus mr-1" /> <i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span class="cut-text">{{ t('message.insertRow', 2) }}</span> <span class="cut-text">{{ $tc('message.insertRow', 2) }}</span>
</div> </div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -39,7 +39,7 @@
<span class="input-group-addon field-type" :class="typeClass(field.type)"> <span class="input-group-addon field-type" :class="typeClass(field.type)">
{{ field.type }} {{ wrapNumber(fieldLength(field)) }} {{ field.type }} {{ wrapNumber(fieldLength(field)) }}
</span> </span>
<label class="form-checkbox ml-3" :title="t('word.insert')"> <label class="form-checkbox ml-3" :title="$t('word.insert')">
<input <input
type="checkbox" type="checkbox"
:checked="!fieldsToExclude.includes(field.name)" :checked="!fieldsToExclude.includes(field.name)"
@@ -55,7 +55,7 @@
</div> </div>
<div class="modal-footer columns"> <div class="modal-footer columns">
<div class="column d-flex" :class="hasFakes ? 'col-4' : 'col-2'"> <div class="column d-flex" :class="hasFakes ? 'col-4' : 'col-2'">
<div class="input-group tooltip tooltip-right" :data-tooltip="t('message.numberOfInserts')"> <div class="input-group tooltip tooltip-right" :data-tooltip="$t('message.numberOfInserts')">
<input <input
v-model="nInserts" v-model="nInserts"
type="number" type="number"
@@ -70,7 +70,7 @@
<div <div
v-if="hasFakes" v-if="hasFakes"
class="tooltip tooltip-right ml-2" class="tooltip tooltip-right ml-2"
:data-tooltip="t('message.fakeDataLanguage')" :data-tooltip="$t('message.fakeDataLanguage')"
> >
<BaseSelect <BaseSelect
v-model="fakerLocale" v-model="fakerLocale"
@@ -85,10 +85,10 @@
:class="{'loading': isInserting}" :class="{'loading': isInserting}"
@click.stop="insertRows" @click.stop="insertRows"
> >
{{ t('word.insert') }} {{ $t('word.insert') }}
</button> </button>
<button class="btn btn-link" @click.stop="closeModal"> <button class="btn btn-link" @click.stop="closeModal">
{{ t('word.close') }} {{ $t('word.close') }}
</button> </button>
</div> </div>
</div> </div>
@@ -97,198 +97,126 @@
</Teleport> </Teleport>
</template> </template>
<script setup lang="ts"> <script>
import { computed, onBeforeMount, onMounted, Prop, Ref, ref, watch } from 'vue'; import moment from 'moment';
import * as moment from 'moment';
import { TableField, TableForeign } from 'common/interfaces/antares';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { useFocusTrap } from '@/composables/useFocusTrap';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import FakerSelect from '@/components/FakerSelect.vue'; import FakerSelect from '@/components/FakerSelect';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { useFilters } from '@/composables/useFilters';
import { useI18n } from 'vue-i18n';
const { t } = useI18n(); export default {
name: 'ModalFakerRows',
components: {
FakerSelect,
BaseSelect
},
props: {
tabUid: [String, Number],
fields: Array,
keyUsage: Array
},
emits: ['reload', 'hide'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { wrapNumber } = useFilters(); const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, getWorkspaceTab } = workspacesStore;
const locales = [
{ value: 'ar', label: 'Arabic' },
{ value: 'az', label: 'Azerbaijani' },
{ value: 'zh_CN', label: 'Chinese' },
{ value: 'zh_TW', label: 'Chinese (Taiwan)' },
{ value: 'cz', label: 'Czech' },
{ value: 'nl', label: 'Dutch' },
{ value: 'nl_BE', label: 'Dutch (Belgium)' },
{ value: 'en', label: 'English' },
{ value: 'en_AU_ocker', label: 'English (Australia Ocker)' },
{ value: 'en_AU', label: 'English (Australia)' },
{ value: 'en_BORK', label: 'English (Bork)' },
{ value: 'en_CA', label: 'English (Canada)' },
{ value: 'en_GB', label: 'English (Great Britain)' },
{ value: 'en_IND', label: 'English (India)' },
{ value: 'en_IE', label: 'English (Ireland)' },
{ value: 'en_ZA', label: 'English (South Africa)' },
{ value: 'en_US', label: 'English (United States)' },
{ value: 'fa', label: 'Farsi' },
{ value: 'fi', label: 'Finnish' },
{ value: 'fr', label: 'French' },
{ value: 'fr_CA', label: 'French (Canada)' },
{ value: 'fr_CH', label: 'French (Switzerland)' },
{ value: 'ge', label: 'Georgian' },
{ value: 'de', label: 'German' },
{ value: 'de_AT', label: 'German (Austria)' },
{ value: 'de_CH', label: 'German (Switzerland)' },
{ value: 'hr', label: 'Hrvatski' },
{ value: 'id_ID', label: 'Indonesia' },
{ value: 'it', label: 'Italian' },
{ value: 'ja', label: 'Japanese' },
{ value: 'ko', label: 'Korean' },
{ value: 'nep', label: 'Nepalese' },
{ value: 'nb_NO', label: 'Norwegian' },
{ value: 'pl', label: 'Polish' },
{ value: 'pt_BR', label: 'Portuguese (Brazil)' },
{ value: 'pt_PT', label: 'Portuguese (Portugal)' },
{ value: 'ro', label: 'Romanian' },
{ value: 'ru', label: 'Russian' },
{ value: 'sk', label: 'Slovakian' },
{ value: 'es', label: 'Spanish' },
{ value: 'es_MX', label: 'Spanish (Mexico)' },
{ value: 'sv', label: 'Swedish' },
{ value: 'tr', label: 'Turkish' },
{ value: 'uk', label: 'Ukrainian' },
{ value: 'vi', label: 'Vietnamese' }
const props = defineProps({ ];
tabUid: [String, Number],
schema: String,
table: String,
fields: Array as Prop<TableField[]>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rowToDuplicate: Object as Prop<any>,
keyUsage: Array as Prop<TableForeign[]>
});
const emit = defineEmits(['reload', 'hide']); return {
addNotification,
const { addNotification } = useNotificationsStore(); selectedWorkspace,
const workspacesStore = useWorkspacesStore(); getWorkspace,
getWorkspaceTab,
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); locales
};
const { trapRef } = useFocusTrap({ disableAutofocus: true }); },
data () {
// eslint-disable-next-line @typescript-eslint/no-explicit-any return {
const localRow: Ref<{[key: string]: any}> = ref({}); localRow: {},
const fieldsToExclude = ref([]); fieldsToExclude: [],
const nInserts = ref(1); nInserts: 1,
const isInserting = ref(false); isInserting: false,
const fakerLocale = ref('en'); fakerLocale: 'en'
};
const foreignKeys = computed(() => props.keyUsage.map(key => key.field)); },
const hasFakes = computed(() => Object.keys(localRow.value).some(field => 'group' in localRow.value[field] && localRow.value[field].group !== 'manual')); computed: {
workspace () {
const locales = [ return this.getWorkspace(this.selectedWorkspace);
{ value: 'ar', label: 'Arabic' }, },
{ value: 'az', label: 'Azerbaijani' }, foreignKeys () {
{ value: 'zh_CN', label: 'Chinese' }, return this.keyUsage.map(key => key.field);
{ value: 'zh_TW', label: 'Chinese (Taiwan)' }, },
{ value: 'cz', label: 'Czech' }, hasFakes () {
{ value: 'nl', label: 'Dutch' }, return Object.keys(this.localRow).some(field => 'group' in this.localRow[field] && this.localRow[field].group !== 'manual');
{ value: 'nl_BE', label: 'Dutch (Belgium)' },
{ value: 'en', label: 'English' },
{ value: 'en_AU_ocker', label: 'English (Australia Ocker)' },
{ value: 'en_AU', label: 'English (Australia)' },
{ value: 'en_BORK', label: 'English (Bork)' },
{ value: 'en_CA', label: 'English (Canada)' },
{ value: 'en_GB', label: 'English (Great Britain)' },
{ value: 'en_IND', label: 'English (India)' },
{ value: 'en_IE', label: 'English (Ireland)' },
{ value: 'en_ZA', label: 'English (South Africa)' },
{ value: 'en_US', label: 'English (United States)' },
{ value: 'fa', label: 'Farsi' },
{ value: 'fi', label: 'Finnish' },
{ value: 'fr', label: 'French' },
{ value: 'fr_CA', label: 'French (Canada)' },
{ value: 'fr_CH', label: 'French (Switzerland)' },
{ value: 'ge', label: 'Georgian' },
{ value: 'de', label: 'German' },
{ value: 'de_AT', label: 'German (Austria)' },
{ value: 'de_CH', label: 'German (Switzerland)' },
{ value: 'hr', label: 'Hrvatski' },
{ value: 'id_ID', label: 'Indonesia' },
{ value: 'it', label: 'Italian' },
{ value: 'ja', label: 'Japanese' },
{ value: 'ko', label: 'Korean' },
{ value: 'nep', label: 'Nepalese' },
{ value: 'nb_NO', label: 'Norwegian' },
{ value: 'pl', label: 'Polish' },
{ value: 'pt_BR', label: 'Portuguese (Brazil)' },
{ value: 'pt_PT', label: 'Portuguese (Portugal)' },
{ value: 'ro', label: 'Romanian' },
{ value: 'ru', label: 'Russian' },
{ value: 'sk', label: 'Slovakian' },
{ value: 'es', label: 'Spanish' },
{ value: 'es_MX', label: 'Spanish (Mexico)' },
{ value: 'sv', label: 'Swedish' },
{ value: 'tr', label: 'Turkish' },
{ value: 'uk', label: 'Ukrainian' },
{ value: 'vi', label: 'Vietnamese' }
];
watch(nInserts, (val) => {
if (!val || val < 1)
nInserts.value = 1;
else if (val > 1000)
nInserts.value = 1000;
});
const typeClass = (type: string) => {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
};
const insertRows = async () => {
isInserting.value = true;
const rowToInsert = localRow.value;
Object.keys(rowToInsert).forEach(key => {
if (fieldsToExclude.value.includes(key))
delete rowToInsert[key];
if (typeof rowToInsert[key] === 'undefined')
delete rowToInsert[key];
});
const fieldTypes: {[key: string]: string} = {};
props.fields.forEach(field => {
fieldTypes[field.name] = field.type;
});
try {
const { status, response } = await Tables.insertTableFakeRows({
uid: selectedWorkspace.value,
schema: props.schema,
table: props.table,
row: rowToInsert,
repeat: nInserts.value,
fields: fieldTypes,
locale: fakerLocale.value
});
if (status === 'success') {
closeModal();
emit('reload');
} }
else },
addNotification({ status: 'error', message: response }); watch: {
} nInserts (val) {
catch (err) { if (!val || val < 1)
addNotification({ status: 'error', message: err.stack }); this.nInserts = 1;
} else if (val > 1000)
this.nInserts = 1000;
isInserting.value = false;
};
const closeModal = () => {
emit('hide');
};
const fieldLength = (field: TableField) => {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
else if (TEXT.includes(field.type)) return Number(field.charLength);
return Number(field.length);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const toggleFields = (event: any, field: TableField) => {
if (event.target.checked)
fieldsToExclude.value = fieldsToExclude.value.filter(f => f !== field.name);
else
fieldsToExclude.value = [...fieldsToExclude.value, field.name];
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
window.addEventListener('keydown', onKey);
onMounted(() => {
setTimeout(() => {
const inputs = Array.from(document.querySelectorAll<HTMLInputElement>('.modal-container .form-input'));
if (inputs?.length) {
const firstEnabledInput = inputs.find((el) => !el.disabled);
firstEnabledInput?.focus();
} }
}, 50); },
created () {
window.addEventListener('keydown', this.onKey);
},
mounted () {
const rowObj = {};
const rowObj: {[key: string]: unknown} = {}; for (const field of this.fields) {
if (!props.rowToDuplicate) {
// Set default values
for (const field of props.fields) {
let fieldDefault; let fieldDefault;
if (field.default === 'NULL') fieldDefault = null; if (field.default === 'NULL') fieldDefault = null;
@@ -325,26 +253,95 @@ onMounted(() => {
rowObj[field.name] = { value: fieldDefault }; rowObj[field.name] = { value: fieldDefault };
if (field.autoIncrement || !!field.onUpdate)// Disable by default auto increment or "on update" fields if (field.autoIncrement || !!field.onUpdate)// Disable by default auto increment or "on update" fields
fieldsToExclude.value = [...fieldsToExclude.value, field.name]; this.fieldsToExclude = [...this.fieldsToExclude, field.name];
}
this.localRow = { ...rowObj };
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
async insertRows () {
this.isInserting = true;
const rowToInsert = this.localRow;
Object.keys(rowToInsert).forEach(key => {
if (this.fieldsToExclude.includes(key))
delete rowToInsert[key];
if (typeof rowToInsert[key] === 'undefined')
delete rowToInsert[key];
});
const fieldTypes = {};
this.fields.forEach(field => {
fieldTypes[field.name] = field.type;
});
try {
const { status, response } = await Tables.insertTableFakeRows({
uid: this.selectedWorkspace,
schema: this.workspace.breadcrumbs.schema,
table: this.workspace.breadcrumbs.table,
row: rowToInsert,
repeat: this.nInserts,
fields: fieldTypes,
locale: this.fakerLocale
});
if (status === 'success') {
this.closeModal();
this.$emit('reload');
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isInserting = false;
},
closeModal () {
this.$emit('hide');
},
fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
else if (TEXT.includes(field.type)) return Number(field.charLength);
return Number(field.length);
},
toggleFields (event, field) {
if (event.target.checked)
this.fieldsToExclude = this.fieldsToExclude.filter(f => f !== field.name);
else
this.fieldsToExclude = [...this.fieldsToExclude, field.name];
},
filesChange (event, field) {
const { files } = event.target;
if (!files.length) return;
this.localRow[field] = files[0].path;
},
getKeyUsage (keyName) {
return this.keyUsage.find(key => key.field === keyName);
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
} }
} }
else { };
// Set values to duplicate
for (const field of props.fields) {
if (typeof props.rowToDuplicate[field.name] !== 'object')
rowObj[field.name] = { value: props.rowToDuplicate[field.name] };
if (field.autoIncrement || !!field.onUpdate)// Disable by default auto increment or "on update" fields
fieldsToExclude.value = [...fieldsToExclude.value, field.name];
}
}
localRow.value = { ...rowObj };
});
onBeforeMount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style scoped> <style scoped>

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