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

Compare commits

..

65 Commits

Author SHA1 Message Date
ad713fcf35 chore(release): 0.4.4 2022-02-27 14:07:24 +01:00
b7039553cc fix: bigint support, closes #197 2022-02-26 10:02:23 +01:00
ddee68b4c2 ci: set windows-2019 for GitHub Actions 2022-02-26 10:01:46 +01:00
265f28b4d9 fix: zero-padded bit fields beyond length 2022-02-21 21:40:26 +01:00
95d15de1bd chore: update README.md 2022-02-16 08:57:54 +01:00
7dcd4441c4 feat(SQLite): manual commit mode 2022-02-15 09:23:07 +01:00
d81e0911ab feat(PostgreSQL): manual commit mode 2022-02-15 09:23:07 +01:00
5bfff649e9 feat: reminder for uncommitted changes closing a tab 2022-02-15 09:23:07 +01:00
76743e8f7c feat: execution notification for ROLLBACK and COMMIT 2022-02-15 09:23:07 +01:00
4ed2f9a939 feat(MySQL): manual commit mode 2022-02-15 09:23:07 +01:00
c5eb73ed3f chore: update electron 2022-02-15 09:21:14 +01:00
fa3f3e1fd8 fix(MySQL): default value not displayed for DECIMAL fields 2022-02-05 09:43:37 +01:00
2c7c97852f chore(release): 0.4.3 2022-01-30 12:39:54 +01:00
48ebf23bd1 feat(MySQL): spatial fields support (#165)
* feat: POINT field support

* feat(MySQL): support to LINESTRING, POLYGON and GEOMETRY fields

* refactor: removed links from map attribution

* feat(MySQL): support to MULTIPOINT, MULTILINESTRING, MULTIPOLYGON and GEOMCOLLECTION fields

* test: temporary fix on Windows tests
2022-01-30 11:45:24 +01:00
64deedc5ad test: temporary skip tests on WIndows 2022-01-30 11:44:01 +01:00
9f033fb994 fix: indexes and foreign keys not cleared after deletion of related field, closes #182 2022-01-28 23:57:53 +01:00
401cb49687 refactor: improved temporary fix to Windows 7 style frame 2022-01-28 09:19:49 +01:00
1356011ba3 fix(Windows): temporary fix to Windows 7 style frame on app startup, closes #169 2022-01-27 23:40:03 +01:00
0cfd7938ee fix: scale on numeric fields that doesn't support it 2022-01-27 09:12:01 +01:00
745b55a942 chore: update dependencies 2022-01-25 09:08:45 +01:00
eef7c1dcec perf: support of scale in field's length setting 2022-01-22 12:29:49 +01:00
aa8fc545d7 Rename zh_CN.js to zh-CN.js 2022-01-20 09:02:20 +01:00
a780c7e0ff Merge pull request #179 from Fabio286/all-contributors/add-goYou
docs: add goYou as a contributor for translation
2022-01-20 08:56:33 +01:00
allcontributors[bot]
9a1bb0599f docs: update .all-contributorsrc [skip ci] 2022-01-20 07:56:07 +00:00
allcontributors[bot]
d847870f67 docs: update README.md [skip ci] 2022-01-20 07:56:06 +00:00
cd24371576 Merge pull request #177 from goYou/master
feat: add Simplified Chinese translation
2022-01-20 08:54:56 +01:00
goYou
6ef565cf07 feat: add Simplified Chinese translation 2022-01-20 01:50:18 +08:00
46b45c8ab6 fix(PostgreSQL): schema different than public not automatically selected, closes #172 2022-01-17 09:15:18 +01:00
f28531a225 build: resolved dependency conflicts 2022-01-16 11:50:35 +01:00
8fb1f0803e fix: cell copy returns "undefined" in some conditions, closes #170 2022-01-14 18:37:37 +01:00
020ce36312 chore(release): 0.4.2 2022-01-10 08:50:00 +01:00
b4545b178f feat(UI): textarea autofocus selecting a query tab, closes #166 2022-01-09 12:28:01 +01:00
d9a3eab015 perf(MySQL): support to ANSI_QUOTES sql_mode, closes #158 2022-01-05 18:23:31 +01:00
2ab49c218d Merge pull request #164 from toriphes/feat/keep-window-state
feat: keep window state
2021-12-28 19:30:51 +01:00
Giulio Ganci
8f9385d508 feat: save window state
open the main window in the last used position of the screen
2021-12-28 17:12:10 +01:00
0c002918eb feat(PostgreSQL): ability to cancel queries 2021-12-26 21:13:02 +01:00
3d56ec7b49 Merge pull request #157 from Fabio286/dependabot/npm_and_yarn/eslint-plugin-promise-6.0.0
build(deps-dev): bump eslint-plugin-promise from 5.2.0 to 6.0.0
2021-12-24 18:04:58 +01:00
48c3e6afc4 perf: hash for foreign key default names 2021-12-23 11:47:17 +01:00
dependabot[bot]
50a5c8fe1e build(deps-dev): bump eslint-plugin-promise from 5.2.0 to 6.0.0
Bumps [eslint-plugin-promise](https://github.com/xjamundx/eslint-plugin-promise) from 5.2.0 to 6.0.0.
- [Release notes](https://github.com/xjamundx/eslint-plugin-promise/releases)
- [Changelog](https://github.com/xjamundx/eslint-plugin-promise/blob/development/CHANGELOG.md)
- [Commits](https://github.com/xjamundx/eslint-plugin-promise/commits)

---
updated-dependencies:
- dependency-name: eslint-plugin-promise
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-20 19:03:06 +00:00
b0d1f115c9 ci: update actions/checkout to v2 2021-12-20 09:27:36 +01:00
a59f77f618 feat(MySQL): ability to cancel queries 2021-12-19 11:59:09 +01:00
e7a1858091 fix(SQLite): exception with some fields 2021-12-16 09:16:15 +01:00
648a51efe8 Merge pull request #154 from Fabio286/all-contributors/add-wenj91
docs: add wenj91 as a contributor for code
2021-12-14 09:49:24 +01:00
38962c4807 Merge pull request #153 from wenj91/master
[TypeError: Cannot read properties of undefined (reading 'type') #152] bugfix
2021-12-14 09:48:58 +01:00
allcontributors[bot]
43e4cdd4cf docs: update .all-contributorsrc [skip ci] 2021-12-14 08:48:00 +00:00
allcontributors[bot]
91046c1ac1 docs: update README.md [skip ci] 2021-12-14 08:47:59 +00:00
文杰
63f8b9b6a1 Merge branch 'Fabio286:master' into master 2021-12-14 09:40:42 +08:00
文杰
ed476d9b5c Merge pull request #1 from wenj91:152-bugfix
[TypeError: Cannot read properties of undefined (reading 'type') #152] bugfix
2021-12-14 09:40:02 +08:00
文杰
f41d8c0480 [TypeError: Cannot read properties of undefined (reading 'type') #152] bugfix 2021-12-14 01:37:59 +00:00
e3b54a8be1 Merge pull request #151 from datlechin/master
Update vi-VN translation
2021-12-13 08:08:30 +01:00
Ngo Quoc Dat
c2c0394624 Update vi-VN translation 2021-12-13 08:48:07 +07:00
8a6f5eac59 chore(release): 0.4.1 2021-12-11 10:28:29 +01:00
a5fdcc1a85 feat: language format detection for text fields 2021-12-10 23:30:03 +01:00
1df21da47c refactor: moved to new vue slots API 2021-12-10 17:34:44 +01:00
8da0224876 fix(MySQL): wrong datetime fields default in table filler in some cases 2021-12-09 18:26:59 +01:00
359e14a9eb fix(MySQL): wrong value for fields "on update" in some conditions 2021-12-09 12:22:38 +01:00
813aa320d9 perf(UI): avoid columns size change when editing cells or scrolling results 2021-12-08 11:19:10 +01:00
aaa5549609 fix: cell disappear on edit in one column tables 2021-12-08 10:37:23 +01:00
35cb7e1dc4 fix: select all rows with ctrl+a when editing a cell 2021-12-08 10:09:01 +01:00
992a033cb2 fix: false positive with Windows Defender 2021-12-01 09:23:40 +01:00
5267b37eaf chore: updated appx icons 2021-11-28 16:28:54 +01:00
8fe30d8b6d ci: run tests before build 2021-11-25 21:37:48 +01:00
37ccb6b00d test: basic e2e tests 2021-11-25 18:34:33 +01:00
e8af2d24a8 perf(UI): disable save button in table creation when no fields are added 2021-11-25 17:23:46 +01:00
d7f1aa97af fix(SQLite): update rows with a text primary key 2021-11-25 16:25:40 +01:00
69 changed files with 1795 additions and 312 deletions

View File

@@ -120,6 +120,24 @@
"contributions": [
"code"
]
},
{
"login": "wenj91",
"name": "文杰",
"avatar_url": "https://avatars.githubusercontent.com/u/12549338?v=4",
"profile": "https://github.com/wenj91",
"contributions": [
"code"
]
},
{
"login": "goYou",
"name": "goYou",
"avatar_url": "https://avatars.githubusercontent.com/u/62732795?v=4",
"profile": "https://github.com/goYou",
"contributions": [
"translation"
]
}
],
"contributorsPerLine": 7,

View File

@@ -12,12 +12,18 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v1
uses: actions/checkout@v2
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 14
- name: Install dependencies
run: npm i
- name: Run tests
run: npm run test
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1

View File

@@ -12,12 +12,18 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v1
uses: actions/checkout@v2
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 14
- name: Install dependencies
run: npm i
- name: Run tests
run: npm run test
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1

View File

@@ -8,17 +8,23 @@ jobs:
strategy:
matrix:
os: [windows-latest]
os: [windows-2019]
steps:
- name: Check out Git repository
uses: actions/checkout@v1
uses: actions/checkout@v2
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 14
- name: Install dependencies
run: npm i
- name: Run tests
run: npm run test
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:

View File

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

View File

@@ -2,6 +2,90 @@
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.4.4](https://github.com/Fabio286/antares/compare/v0.4.3...v0.4.4) (2022-02-27)
### Features
* execution notification for ROLLBACK and COMMIT ([76743e8](https://github.com/Fabio286/antares/commit/76743e8f7c02b824cb21540bfbcbe66ba43de8fa))
* **MySQL:** manual commit mode ([4ed2f9a](https://github.com/Fabio286/antares/commit/4ed2f9a93937b4293436a64318b7d6ae3a0d93c2))
* **PostgreSQL:** manual commit mode ([d81e091](https://github.com/Fabio286/antares/commit/d81e0911ab82fb75745ab11e67e867a00d8ac273))
* reminder for uncommitted changes closing a tab ([5bfff64](https://github.com/Fabio286/antares/commit/5bfff649e92f6fe5aba4b16aa4c8d5a5a70b31b2))
* **SQLite:** manual commit mode ([7dcd444](https://github.com/Fabio286/antares/commit/7dcd4441c49fafc0f47e12c2129708fe1092e1a4))
### Bug Fixes
* bigint support, closes [#197](https://github.com/Fabio286/antares/issues/197) ([b703955](https://github.com/Fabio286/antares/commit/b7039553ccaac4fd59e521530c4a053922854130))
* **MySQL:** default value not displayed for DECIMAL fields ([fa3f3e1](https://github.com/Fabio286/antares/commit/fa3f3e1fd8101f19f18df71e90d60fd37cdddaee))
* zero-padded bit fields beyond length ([265f28b](https://github.com/Fabio286/antares/commit/265f28b4d94cde4608a1d6d3d306824134808ec2))
### [0.4.3](https://github.com/Fabio286/antares/compare/v0.4.2...v0.4.3) (2022-01-30)
### Features
* add Simplified Chinese translation ([6ef565c](https://github.com/Fabio286/antares/commit/6ef565cf078cb3f5b7bcdc226894cddeb6239db9))
* **MySQL:** spatial fields support ([#165](https://github.com/Fabio286/antares/issues/165)) ([48ebf23](https://github.com/Fabio286/antares/commit/48ebf23bd1574408f429f2e1200ce878352007f6))
### Bug Fixes
* cell copy returns "undefined" in some conditions, closes [#170](https://github.com/Fabio286/antares/issues/170) ([8fb1f08](https://github.com/Fabio286/antares/commit/8fb1f0803efd9df0b66521e73bb6e1a229cf9691))
* indexes and foreign keys not cleared after deletion of related field, closes [#182](https://github.com/Fabio286/antares/issues/182) ([9f033fb](https://github.com/Fabio286/antares/commit/9f033fb994916b4fb165e81e55e86127ca817791))
* **PostgreSQL:** schema different than public not automatically selected, closes [#172](https://github.com/Fabio286/antares/issues/172) ([46b45c8](https://github.com/Fabio286/antares/commit/46b45c8ab64fb6837a532c4f8342167e4fd794bb))
* scale on numeric fields that doesn't support it ([0cfd793](https://github.com/Fabio286/antares/commit/0cfd7938ee7d607dbad66ae452d0200223a6bab2))
* **Windows:** temporary fix to Windows 7 style frame on app startup, closes [#169](https://github.com/Fabio286/antares/issues/169) ([1356011](https://github.com/Fabio286/antares/commit/1356011ba3b7dd72e12cb252a8787ce48a364fd4))
### Improvements
* support of scale in field's length setting ([eef7c1d](https://github.com/Fabio286/antares/commit/eef7c1dcecc6593ab0e69ed678187a57fe0a4fb6))
### [0.4.2](https://github.com/Fabio286/antares/compare/v0.4.1...v0.4.2) (2022-01-10)
### Features
* **MySQL:** ability to cancel queries ([a59f77f](https://github.com/Fabio286/antares/commit/a59f77f618aea6156fc80fb832d3efcb9848411f))
* **PostgreSQL:** ability to cancel queries ([0c00291](https://github.com/Fabio286/antares/commit/0c002918eb0226f6b3f21ed62117495f86396fb1))
* save window state ([8f9385d](https://github.com/Fabio286/antares/commit/8f9385d50815635d091758ecd5d00884e3297ca0))
* **UI:** textarea autofocus selecting a query tab, closes [#166](https://github.com/Fabio286/antares/issues/166) ([b4545b1](https://github.com/Fabio286/antares/commit/b4545b178f795712c781a3f4fc35eec31b5ad902))
### Bug Fixes
* **SQLite:** exception with some fields ([e7a1858](https://github.com/Fabio286/antares/commit/e7a18580915e7739bfa97948c6a0c4fc90a7e78a))
### Improvements
* hash for foreign key default names ([48c3e6a](https://github.com/Fabio286/antares/commit/48c3e6afc43c51f70a16703f1a71194f43da7a3e))
* **MySQL:** support to ANSI_QUOTES sql_mode, closes [#158](https://github.com/Fabio286/antares/issues/158) ([d9a3eab](https://github.com/Fabio286/antares/commit/d9a3eab015302e9f23112f659658073ab3242191))
### [0.4.1](https://github.com/Fabio286/antares/compare/v0.4.0...v0.4.1) (2021-12-11)
### Features
* language format detection for text fields ([a5fdcc1](https://github.com/Fabio286/antares/commit/a5fdcc1a85aa188ff1b9a15b1a768aced026f360))
### Bug Fixes
* cell disappear on edit in one column tables ([aaa5549](https://github.com/Fabio286/antares/commit/aaa5549609664665bd4513632d621cb249b379c1))
* false positive with Windows Defender ([992a033](https://github.com/Fabio286/antares/commit/992a033cb2bede3d1eb52e19482d810f6692de1e))
* **MySQL:** wrong datetime fields default in table filler in some cases ([8da0224](https://github.com/Fabio286/antares/commit/8da022487650039b7f34a9c86a7bd9045eba65e2))
* **MySQL:** wrong value for fields "on update" in some conditions ([359e14a](https://github.com/Fabio286/antares/commit/359e14a9ebd48f86069ba7762fe00a7056f52d47))
* select all rows with ctrl+a when editing a cell ([35cb7e1](https://github.com/Fabio286/antares/commit/35cb7e1dc48d3a74e9d106cb1a37f454c1b4a4d1))
* **SQLite:** update rows with a text primary key ([d7f1aa9](https://github.com/Fabio286/antares/commit/d7f1aa97af32a4c51fc7022498bd47e15fa08430))
### Improvements
* **UI:** avoid columns size change when editing cells or scrolling results ([813aa32](https://github.com/Fabio286/antares/commit/813aa320d9ab799efea38a7110b7c0bdf7549123))
* **UI:** disable save button in table creation when no fields are added ([e8af2d2](https://github.com/Fabio286/antares/commit/e8af2d24a869f7667c069936648808952d2062ab))
## [0.4.0](https://github.com/Fabio286/antares/compare/v0.3.9...v0.4.0) (2021-11-24)

View File

@@ -31,10 +31,9 @@ We are actively working on it, hoping to provide new cool features, improvements
- Query suggestions and auto complete.
- Query history: search through the last 1000 queries.
- SSH tunnel support.
- Manual commit mode.
- Dark and light theme.
- Editor themes.
- Scratchpad.
- Secure password storage.
## Philosophy
@@ -130,6 +129,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://github.com/IsamuSugi"><img src="https://avatars.githubusercontent.com/u/7746658?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Isamu Sugiura</b></sub></a><br /><a href="#translation-IsamuSugi" title="Translation">🌍</a></td>
<td align="center"><a href="http://rsacchetto.nexxontech.it/"><img src="https://avatars.githubusercontent.com/u/18429412?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Riccardo Sacchetto</b></sub></a><br /><a href="#platform-Occhioverde" title="Packaging/porting to new platform">📦</a></td>
<td align="center"><a href="https://kilianstallinger.com"><img src="https://avatars.githubusercontent.com/u/5290318?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kilian Stallinger</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=kilianstallz" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/wenj91"><img src="https://avatars.githubusercontent.com/u/12549338?v=4?s=100" width="100px;" alt=""/><br /><sub><b>文杰</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=wenj91" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/goYou"><img src="https://avatars.githubusercontent.com/u/62732795?v=4?s=100" width="100px;" alt=""/><br /><sub><b>goYou</b></sub></a><br /><a href="#translation-goYou" title="Translation">🌍</a></td>
</tr>
</table>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -1,7 +1,7 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.4.0",
"version": "0.4.4",
"description": "A modern, fast and productivity driven SQL client with a focus in UX.",
"license": "MIT",
"repository": "https://github.com/Fabio286/antares.git",
@@ -18,7 +18,7 @@
"release": "standard-version",
"release:pre": "npm run release -- --prerelease alpha",
"postinstall": "electron-builder install-app-deps",
"test": "npm run lint",
"test": "npm run compile && node tests/app.spec.js",
"lint": "eslint . --ext .js,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"",
"lint:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix",
"contributors:add": "all-contributors add",
@@ -51,7 +51,8 @@
"target": {
"target": "default",
"arch": [
"x64"
"x64",
"arm64"
]
}
},
@@ -82,6 +83,7 @@
"appx": {
"displayName": "Antares SQL",
"backgroundColor": "transparent",
"showNameOnTiles": true,
"identityName": "62514FabioDiStasio.AntaresSQLClient",
"publisher": "CN=1A2729ED-865C-41D2-9038-39AE2A63AA52",
"applicationId": "FabioDiStasio.AntaresSQLClient"
@@ -104,12 +106,16 @@
"dependencies": {
"@electron/remote": "^2.0.1",
"@mdi/font": "^6.1.95",
"@turf/helpers": "^6.5.0",
"@vscode/vscode-languagedetection": "^1.0.21",
"ace-builds": "^1.4.13",
"better-sqlite3": "^7.4.4",
"electron-log": "^4.4.1",
"electron-store": "^8.0.1",
"electron-updater": "^4.3.9",
"electron-updater": "^4.6.1",
"electron-window-state": "^5.0.3",
"faker": "^5.5.3",
"leaflet": "^1.7.1",
"marked": "^4.0.0",
"moment": "^2.29.1",
"mysql2": "^2.3.2",
@@ -133,19 +139,20 @@
"clean-webpack-plugin": "^4.0.0",
"cross-env": "^7.0.2",
"css-loader": "^6.5.0",
"electron": "^16.0.1",
"electron-builder": "^22.13.1",
"electron": "^17.0.1",
"electron-builder": "^22.14.11",
"electron-devtools-installer": "^3.2.0",
"eslint": "^7.32.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-promise": "^5.2.0",
"eslint-plugin-vue": "^8.0.3",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.4.3",
"mini-css-extract-plugin": "~2.4.5",
"node-loader": "^2.0.0",
"playwright": "^1.18.1",
"progress-webpack-plugin": "^1.0.12",
"sass": "^1.42.1",
"sass-loader": "^12.3.0",

View File

@@ -11,6 +11,7 @@ module.exports = {
sslConnection: false,
sshConnection: false,
fileConnection: false,
cancelQueries: false,
// Tools
processesList: false,
usersManagement: false,

View File

@@ -12,6 +12,7 @@ module.exports = {
engines: true,
sslConnection: true,
sshConnection: true,
cancelQueries: true,
// Tools
processesList: true,
// Structure

View File

@@ -10,6 +10,7 @@ module.exports = {
database: true,
sslConnection: true,
sshConnection: true,
cancelQueries: true,
// Tools
processesList: true,
// Structure

View File

@@ -7,8 +7,8 @@ module.exports = {
views: true,
triggers: true,
// Settings
elementsWrapper: '',
stringsWrapper: '"',
elementsWrapper: '"',
stringsWrapper: '\'',
tableAdd: true,
viewAdd: true,
triggerAdd: true,

View File

@@ -66,6 +66,7 @@ module.exports = [
{
name: 'DECIMAL',
length: true,
scale: true,
collation: false,
unsigned: false,
zerofill: false
@@ -120,7 +121,7 @@ module.exports = [
{
name: 'JSON',
length: false,
collation: true,
collation: false,
unsigned: false,
zerofill: false
}
@@ -218,56 +219,56 @@ module.exports = [
types: [
{
name: 'POINT',
length: true,
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'LINESTRING',
length: true,
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'POLYGON',
length: true,
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'GEOMETRY',
length: true,
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'MULTIPOINT',
length: true,
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'MULTILINESTRING',
length: true,
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'MULTIPOLYGON',
length: true,
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'GEOMETRYCOLLECTION',
length: true,
name: 'GEOMCOLLECTION',
length: false,
collation: false,
unsigned: false,
zerofill: false

View File

@@ -22,11 +22,6 @@ module.exports = [
length: false,
unsigned: true
},
{
name: 'NUMERIC',
length: true,
unsigned: true
},
{
name: 'SMALLSERIAL',
length: false,
@@ -52,6 +47,12 @@ module.exports = [
length: false,
unsigned: true
},
{
name: 'NUMERIC',
length: true,
unsigned: true,
scale: true
},
{
name: 'DOUBLE PRECISION',
length: false,

View File

@@ -8,7 +8,9 @@ export const TEXT = [
export const LONG_TEXT = [
'TEXT',
'MEDIUMTEXT',
'LONGTEXT'
'LONGTEXT',
'JSON',
'VARBINARY'
];
export const ARRAY = [
@@ -82,3 +84,24 @@ export const BIT = [
'BIT',
'BIT VARYING'
];
export const SPATIAL = [
'POINT',
'LINESTRING',
'POLYGON',
'GEOMETRY',
'MULTIPOINT',
'MULTILINESTRING',
'MULTIPOLYGON',
'GEOMCOLLECTION',
'GEOMETRYCOLLECTION'
];
// Used to check multi spatial fields only
export const IS_MULTI_SPATIAL = [
'MULTIPOINT',
'MULTILINESTRING',
'MULTIPOLYGON',
'GEOMCOLLECTION',
'GEOMETRYCOLLECTION'
];

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

@@ -52,7 +52,7 @@ export default connections => {
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err };
return { status: 'error', response: err.toString() };
}
});

View File

@@ -135,7 +135,7 @@ export default connections => {
}
});
ipcMain.handle('raw-query', async (event, { uid, query, schema }) => {
ipcMain.handle('raw-query', async (event, { uid, query, schema, tabUid, autocommit }) => {
if (!query) return;
try {
@@ -143,6 +143,8 @@ export default connections => {
nest: true,
details: true,
schema,
tabUid,
autocommit,
comments: false
});
@@ -152,4 +154,52 @@ export default connections => {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('kill-tab-query', async (event, { uid, tabUid }) => {
if (!tabUid) return;
try {
await connections[uid].killTabQuery(tabUid);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('commit-tab', async (event, { uid, tabUid }) => {
if (!tabUid) return;
try {
await connections[uid].commitTab(tabUid);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('rollback-tab', async (event, { uid, tabUid }) => {
if (!tabUid) return;
try {
await connections[uid].rollbackTab(tabUid);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('destroy-connection-to-commit', async (event, { uid, tabUid }) => {
if (!tabUid) return;
try {
await connections[uid].destroyConnectionToCommit(tabUid);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
};

View File

@@ -3,6 +3,7 @@ import faker from 'faker';
import moment from 'moment';
import { sqlEscaper } from 'common/libs/sqlEscaper';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes';
import * as customizations from 'common/customizations';
import fs from 'fs';
export default (connections) => {
@@ -85,11 +86,12 @@ export default (connections) => {
ipcMain.handle('update-table-cell', async (event, params) => {
delete params.row._antares_id;
const { stringsWrapper: sw } = customizations[connections[params.uid]._client];
try { // TODO: move to client classes
let escapedParam;
let reload = false;
const id = typeof params.id === 'number' ? params.id : `"${params.id}"`;
const id = typeof params.id === 'number' ? params.id : `${sw}${params.id}${sw}`;
if ([...NUMBER, ...FLOAT].includes(params.type))
escapedParam = params.content;

View File

@@ -9,6 +9,8 @@ export class MySQLClient extends AntaresCore {
super(args);
this._schema = null;
this._runningConnections = new Map();
this._connectionsToCommit = new Map();
this.types = {
0: 'DECIMAL',
@@ -100,9 +102,11 @@ export class MySQLClient extends AntaresCore {
}
/**
*
* @returns dbConfig
* @memberof MySQLClient
*/
async connect () {
async getDbConfig () {
delete this._params.application_name;
const dbConfig = {
@@ -110,7 +114,9 @@ export class MySQLClient extends AntaresCore {
port: this._params.port,
user: this._params.user,
password: this._params.password,
ssl: null
ssl: null,
supportBigNumbers: true,
bigNumberStrings: true
};
if (this._params.schema?.length) dbConfig.database = this._params.schema;
@@ -133,30 +139,17 @@ export class MySQLClient extends AntaresCore {
}
}
if (!this._poolSize) {
this._connection = await mysql.createConnection(dbConfig);
return dbConfig;
}
if (this._params.readonly)
await this.raw('SET SESSION TRANSACTION READ ONLY');
}
else {
this._connection = mysql.createPool({
...dbConfig,
connectionLimit: this._poolSize,
typeCast: (field, next) => {
if (field.type === 'DATETIME')
return field.string();
else
return next();
}
});
if (this._params.readonly) {
this._connection.on('connection', connection => {
connection.query('SET SESSION TRANSACTION READ ONLY');
});
}
}
/**
* @memberof MySQLClient
*/
async connect () {
if (!this._poolSize)
this._connection = await this.getConnection();
else
this._connection = await this.getConnectionPool();
}
/**
@@ -167,6 +160,64 @@ export class MySQLClient extends AntaresCore {
if (this._ssh) this._ssh.close();
}
async getConnection () {
const dbConfig = await this.getDbConfig();
const connection = await mysql.createConnection({
...dbConfig,
typeCast: (field, next) => {
if (field.type === 'DATETIME')
return field.string();
else
return next();
}
});
// ANSI_QUOTES check
const [res] = await connection.query('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\'');
const sqlMode = res[0]?.Variable_name?.split(',');
const hasAnsiQuotes = sqlMode.includes('ANSI_QUOTES');
if (this._params.readonly)
await connection.query('SET SESSION TRANSACTION READ ONLY');
if (hasAnsiQuotes)
await connection.query(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`);
return connection;
}
async getConnectionPool () {
const dbConfig = await this.getDbConfig();
const connection = mysql.createPool({
...dbConfig,
connectionLimit: this._poolSize,
typeCast: (field, next) => {
if (field.type === 'DATETIME')
return field.string();
else
return next();
}
});
// ANSI_QUOTES check
const [res] = await connection.query('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\'');
const sqlMode = res[0]?.Variable_name?.split(',');
const hasAnsiQuotes = sqlMode.includes('ANSI_QUOTES');
if (hasAnsiQuotes)
await connection.query(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`);
connection.on('connection', conn => {
if (this._params.readonly)
conn.query('SET SESSION TRANSACTION READ ONLY');
if (hasAnsiQuotes)
conn.query(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`);
});
return connection;
}
/**
* Executes an USE query
*
@@ -376,7 +427,7 @@ export class MySQLClient extends AntaresCore {
return acc;
}, '')
.replaceAll('\n', '')
.split(',')
.split(/,\s?(?![^(]*\))/)
.map(f => {
try {
const fieldArr = f.trim().split(' ');
@@ -416,18 +467,25 @@ export class MySQLClient extends AntaresCore {
return rows.map(field => {
let numLength = field.COLUMN_TYPE.match(/int\(([^)]+)\)/);
numLength = numLength ? +numLength.pop() : null;
numLength = numLength ? +numLength.pop() : field.NUMERIC_PRECISION || null;
const enumValues = /(enum|set)/.test(field.COLUMN_TYPE)
? field.COLUMN_TYPE.match(/\(([^)]+)\)/)[0].slice(1, -1)
: null;
const defaultValue = (remappedFields && remappedFields[field.COLUMN_NAME])
? remappedFields[field.COLUMN_NAME].default
: field.COLUMN_DEFAULT;
return {
name: field.COLUMN_NAME,
key: field.COLUMN_KEY.toLowerCase(),
type: remappedFields ? remappedFields[field.COLUMN_NAME].type : field.DATA_TYPE,
type: (remappedFields && remappedFields[field.COLUMN_NAME])
? remappedFields[field.COLUMN_NAME].type
: field.DATA_TYPE.toUpperCase(),
schema: field.TABLE_SCHEMA,
table: field.TABLE_NAME,
numPrecision: field.NUMERIC_PRECISION,
numScale: field.NUMERIC_SCALE,
numLength,
enumValues,
datePrecision: field.DATETIME_PRECISION,
@@ -436,11 +494,13 @@ export class MySQLClient extends AntaresCore {
unsigned: field.COLUMN_TYPE.includes('unsigned'),
zerofill: field.COLUMN_TYPE.includes('zerofill'),
order: field.ORDINAL_POSITION,
default: remappedFields ? remappedFields[field.COLUMN_NAME].default : field.COLUMN_DEFAULT,
default: defaultValue,
charset: field.CHARACTER_SET_NAME,
collation: field.COLLATION_NAME,
autoIncrement: field.EXTRA.includes('auto_increment'),
onUpdate: field.EXTRA.toLowerCase().includes('on update') ? field.EXTRA.replace('on update', '') : '',
onUpdate: field.EXTRA.toLowerCase().includes('on update')
? field.EXTRA.substr(field.EXTRA.indexOf('on update') + 9, field.EXTRA.length).trim()
: '',
comment: field.COLUMN_COMMENT
};
});
@@ -1137,6 +1197,26 @@ export class MySQLClient extends AntaresCore {
});
}
/**
* SHOW VARIABLES LIKE %variable%
*
* @param {String} variable
* @param {'global'|'session'|null} level
* @returns {Object} variable
* @memberof MySQLClient
*/
async getVariable (variable, level) {
const sql = `SHOW${level ? ' ' + level.toUpperCase() : ''} VARIABLES LIKE '%${variable}%'`;
const results = await this.raw(sql);
if (results.rows.length) {
return {
name: results.rows[0].Variable_name,
value: results.rows[0].Value
};
}
}
/**
* SHOW ENGINES
*
@@ -1208,10 +1288,56 @@ export class MySQLClient extends AntaresCore {
});
}
/**
*
* @param {number} id
* @returns {Promise<null>}
*/
async killProcess (id) {
return await this.raw(`KILL ${id}`);
}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async killTabQuery (tabUid) {
const id = this._runningConnections.get(tabUid);
if (id)
return await this.killProcess(id);
}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async commitTab (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection)
return await connection.query('COMMIT');
}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async rollbackTab (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection)
return await connection.query('ROLLBACK');
}
destroyConnectionToCommit (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
connection.destroy();
this._connectionsToCommit.delete(tabUid);
}
}
/**
* CREATE TABLE
*
@@ -1238,7 +1364,7 @@ export class MySQLClient extends AntaresCore {
const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false;
newColumns.push(`\`${field.name}\`
${field.type.toUpperCase()}${length ? `(${length})` : ''}
${field.type.toUpperCase()}${length ? `(${length}${field.numScale ? `,${field.numScale}` : ''})` : ''}
${field.unsigned ? 'UNSIGNED' : ''}
${field.zerofill ? 'ZEROFILL' : ''}
${field.nullable ? 'NULL' : 'NOT NULL'}
@@ -1307,7 +1433,7 @@ export class MySQLClient extends AntaresCore {
const length = typeInfo.length ? addition.enumValues || addition.numLength || addition.charLength || addition.datePrecision : false;
alterColumns.push(`ADD COLUMN \`${addition.name}\`
${addition.type.toUpperCase()}${length ? `(${length})` : ''}
${addition.type.toUpperCase()}${length ? `(${length}${addition.numScale ? `,${addition.numScale}` : ''})` : ''}
${addition.unsigned ? 'UNSIGNED' : ''}
${addition.zerofill ? 'ZEROFILL' : ''}
${addition.nullable ? 'NULL' : 'NOT NULL'}
@@ -1345,7 +1471,7 @@ export class MySQLClient extends AntaresCore {
const length = typeInfo.length ? change.enumValues || change.numLength || change.charLength || change.datePrecision : false;
alterColumns.push(`CHANGE COLUMN \`${change.orgName}\` \`${change.name}\`
${change.type.toUpperCase()}${length ? `(${length})` : ''}
${change.type.toUpperCase()}${length ? `(${length}${change.numScale ? `,${change.numScale}` : ''})` : ''}
${change.unsigned ? 'UNSIGNED' : ''}
${change.zerofill ? 'ZEROFILL' : ''}
${change.nullable ? 'NULL' : 'NOT NULL'}
@@ -1516,6 +1642,7 @@ export class MySQLClient extends AntaresCore {
details: false,
split: true,
comments: true,
autocommit: true,
...args
};
@@ -1530,8 +1657,24 @@ export class MySQLClient extends AntaresCore {
.filter(Boolean)
.map(q => q.trim())
: [sql];
let connection;
const isPool = typeof this._connection.getConnection === 'function';
const connection = isPool ? await this._connection.getConnection() : this._connection;
if (!args.autocommit && args.tabUid) { // autocommit OFF
if (this._connectionsToCommit.has(args.tabUid))
connection = this._connectionsToCommit.get(args.tabUid);
else {
connection = await this.getConnection();
await connection.query('SET SESSION autocommit=0');
this._connectionsToCommit.set(args.tabUid, connection);
}
}
else// autocommit ON
connection = isPool ? await this._connection.getConnection() : this._connection;
if (args.tabUid && isPool)
this._runningConnections.set(args.tabUid, connection.connection.connectionId);
if (args.schema)
await connection.query(`USE \`${args.schema}\``);
@@ -1593,7 +1736,10 @@ export class MySQLClient extends AntaresCore {
});
}
catch (err) {
if (isPool) connection.release();
if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err);
}
@@ -1602,7 +1748,10 @@ export class MySQLClient extends AntaresCore {
keysArr = keysArr ? [...keysArr, ...response] : response;
}
catch (err) {
if (isPool) connection.release();
if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err);
}
}
@@ -1617,7 +1766,10 @@ export class MySQLClient extends AntaresCore {
keys: keysArr
});
}).catch((err) => {
if (isPool) connection.release();
if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err);
});
});
@@ -1625,7 +1777,10 @@ export class MySQLClient extends AntaresCore {
resultsArr.push({ rows, report, fields, keys, duration });
}
if (isPool) connection.release();
if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
}

View File

@@ -9,7 +9,6 @@ function pgToString (value) {
return value.toString();
}
types.setTypeParser(20, a => parseInt(a));// bigint string to number
types.setTypeParser(1082, pgToString); // date
types.setTypeParser(1083, pgToString); // time
types.setTypeParser(1114, pgToString); // timestamp
@@ -21,6 +20,8 @@ export class PostgreSQLClient extends AntaresCore {
super(args);
this._schema = null;
this._runningConnections = new Map();
this._connectionsToCommit = new Map();
this.types = {};
for (const key in types.builtins)
@@ -70,9 +71,11 @@ export class PostgreSQLClient extends AntaresCore {
}
/**
*
* @returns dbConfig
* @memberof PostgreSQLClient
*/
async connect () {
async getDbConfig () {
const dbConfig = {
host: this._params.host,
port: this._params.port,
@@ -101,24 +104,43 @@ export class PostgreSQLClient extends AntaresCore {
}
}
if (!this._poolSize) {
const client = new Client(dbConfig);
await client.connect();
this._connection = client;
return dbConfig;
}
if (this._params.readonly)
await this.raw('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY');
}
else {
const pool = new Pool({ ...dbConfig, max: this._poolSize });
this._connection = pool;
/**
* @memberof PostgreSQLClient
*/
async connect () {
if (!this._poolSize)
this._connection = await this.getConnection();
else
this._connection = await this.getConnectionPool();
}
if (this._params.readonly) {
this._connection.on('connect', connection => {
connection.query('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY');
});
}
async getConnection () {
const dbConfig = await this.getDbConfig();
const client = new Client(dbConfig);
await client.connect();
const connection = client;
if (this._params.readonly)
await connection.query('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY');
return connection;
}
async getConnectionPool () {
const dbConfig = await this.getDbConfig();
const pool = new Pool({ ...dbConfig, max: this._poolSize });
const connection = pool;
if (this._params.readonly) {
connection.on('connect', conn => {
conn.query('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY');
});
}
return connection;
}
/**
@@ -130,15 +152,23 @@ export class PostgreSQLClient extends AntaresCore {
}
/**
* Executes an "USE" query
* Executes an 'SET search_path TO "${schema}"' query
*
* @param {String} schema
* @param {Object?} connection optional
* @memberof PostgreSQLClient
*/
use (schema) {
use (schema, connection) {
this._schema = schema;
if (schema)
return this.raw(`SET search_path TO "${schema}"`);
if (schema) {
const sql = `SET search_path TO "${schema}"`;
if (connection === undefined)
return this.raw(sql);
else
return connection.query(sql);
}
}
/**
@@ -317,6 +347,7 @@ export class PostgreSQLClient extends AntaresCore {
isArray,
schema: field.table_schema,
table: field.table_name,
numScale: field.numeric_scale,
numPrecision: field.numeric_precision,
datePrecision: field.datetime_precision,
charLength: field.character_maximum_length,
@@ -1088,10 +1119,60 @@ export class PostgreSQLClient extends AntaresCore {
});
}
/**
*
* @param {number} id
* @returns {Promise<null>}
*/
async killProcess (id) {
return await this.raw(`SELECT pg_terminate_backend(${id})`);
}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async killTabQuery (tabUid) {
const id = this._runningConnections.get(tabUid);
if (id)
return await this.raw(`SELECT pg_cancel_backend(${id})`);
}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async commitTab (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
await connection.query('COMMIT');
return this.destroyConnectionToCommit(tabUid);
}
}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async rollbackTab (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
await connection.query('ROLLBACK');
return this.destroyConnectionToCommit(tabUid);
}
}
destroyConnectionToCommit (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
connection.end();
this._connectionsToCommit.delete(tabUid);
}
}
/**
* CREATE TABLE
*
@@ -1119,7 +1200,7 @@ export class PostgreSQLClient extends AntaresCore {
const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false;
newColumns.push(`"${field.name}"
${field.type.toUpperCase()}${length ? `(${length})` : ''}
${field.type.toUpperCase()}${length ? `(${length}${field.numScale !== null ? `,${field.numScale}` : ''})` : ''}
${field.unsigned ? 'UNSIGNED' : ''}
${field.zerofill ? 'ZEROFILL' : ''}
${field.nullable ? 'NULL' : 'NOT NULL'}
@@ -1183,7 +1264,7 @@ export class PostgreSQLClient extends AntaresCore {
const length = typeInfo.length ? addition.numLength || addition.charLength || addition.datePrecision : false;
alterColumns.push(`ADD COLUMN "${addition.name}"
${addition.type.toUpperCase()}${length ? `(${length})` : ''}${addition.isArray ? '[]' : ''}
${addition.type.toUpperCase()}${length ? `(${length}${addition.numScale !== null ? `,${addition.numScale}` : ''})` : ''}${addition.isArray ? '[]' : ''}
${addition.unsigned ? 'UNSIGNED' : ''}
${addition.zerofill ? 'ZEROFILL' : ''}
${addition.nullable ? 'NULL' : 'NOT NULL'}
@@ -1229,7 +1310,7 @@ export class PostgreSQLClient extends AntaresCore {
localType = change.type.toLowerCase();
}
alterColumns.push(`ALTER COLUMN "${change.name}" TYPE ${localType}${length ? `(${length})` : ''}${change.isArray ? '[]' : ''} USING "${change.name}"::${localType}`);
alterColumns.push(`ALTER COLUMN "${change.name}" TYPE ${localType}${length ? `(${length}${change.numScale ? `,${change.numScale}` : ''})` : ''}${change.isArray ? '[]' : ''} USING "${change.name}"::${localType}`);
alterColumns.push(`ALTER COLUMN "${change.name}" ${change.nullable ? 'DROP NOT NULL' : 'SET NOT NULL'}`);
alterColumns.push(`ALTER COLUMN "${change.name}" ${change.default ? `SET DEFAULT ${change.default}` : 'DROP DEFAULT'}`);
@@ -1396,17 +1477,17 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async raw (sql, args) {
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
args = {
nest: false,
details: false,
split: true,
comments: true,
autocommit: true,
...args
};
if (args.schema && args.schema !== 'public')
await this.use(args.schema);
if (!args.comments)
sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');// Remove comments
@@ -1418,7 +1499,26 @@ export class PostgreSQLClient extends AntaresCore {
.map(q => q.trim())
: [sql];
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
let connection;
const isPool = this._connection instanceof Pool;
if (!args.autocommit && args.tabUid) { // autocommit OFF
if (this._connectionsToCommit.has(args.tabUid))
connection = this._connectionsToCommit.get(args.tabUid);
else {
connection = await this.getConnection();
await connection.query('START TRANSACTION');
this._connectionsToCommit.set(args.tabUid, connection);
}
}
else// autocommit ON
connection = isPool ? await this._connection.connect() : this._connection;
if (args.tabUid && isPool)
this._runningConnections.set(args.tabUid, connection.processID);
if (args.schema && args.schema !== 'public')
await this.use(args.schema, connection);
for (const query of queries) {
if (!query) continue;
@@ -1428,15 +1528,12 @@ export class PostgreSQLClient extends AntaresCore {
let keysArr = [];
const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => {
this._connection.query({
rowMode: args.nest ? 'array' : null,
text: query
}, async (err, res) => {
timeStop = new Date();
(async () => {
try {
const res = await connection.query({ rowMode: args.nest ? 'array' : null, text: query });
timeStop = new Date();
if (err)
reject(err);
else {
let ast;
try {
@@ -1525,6 +1622,10 @@ export class PostgreSQLClient extends AntaresCore {
});
}
catch (err) {
if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err);
}
@@ -1533,6 +1634,10 @@ export class PostgreSQLClient extends AntaresCore {
keysArr = keysArr ? [...keysArr, ...response] : response;
}
catch (err) {
if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err);
}
}
@@ -1547,12 +1652,24 @@ export class PostgreSQLClient extends AntaresCore {
keys: keysArr
});
}
});
catch (err) {
if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err);
}
})();
});
resultsArr.push({ rows, report, fields, keys, duration });
}
if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
}
}

View File

@@ -1,7 +1,7 @@
'use strict';
import sqlite from 'better-sqlite3';
import { AntaresCore } from '../AntaresCore';
import dataTypes from 'common/data-types/mysql';
import dataTypes from 'common/data-types/sqlite';
import { NUMBER, FLOAT, TIME, DATETIME } from 'common/fieldTypes';
export class SQLiteClient extends AntaresCore {
@@ -9,6 +9,7 @@ export class SQLiteClient extends AntaresCore {
super(args);
this._schema = null;
this._connectionsToCommit = new Map();
}
_getTypeInfo (type) {
@@ -21,7 +22,11 @@ export class SQLiteClient extends AntaresCore {
* @memberof SQLiteClient
*/
async connect () {
this._connection = sqlite(this._params.databasePath, {
this._connection = this.getConnection();
}
getConnection () {
return sqlite(this._params.databasePath, {
fileMustExist: true,
readonly: this._params.readonly
});
@@ -158,7 +163,7 @@ export class SQLiteClient extends AntaresCore {
nullable: !field.notnull,
unsigned: null,
zerofill: null,
order: field.cid + 1,
order: typeof field.cid === 'string' ? +field.cid + 1 : field.cid + 1,
default: field.dflt_value,
charset: null,
collation: null,
@@ -446,6 +451,40 @@ export class SQLiteClient extends AntaresCore {
async killProcess () {}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async commitTab (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
connection.prepare('COMMIT').run();
return this.destroyConnectionToCommit(tabUid);
}
}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async rollbackTab (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
connection.prepare('ROLLBACK').run();
return this.destroyConnectionToCommit(tabUid);
}
}
destroyConnectionToCommit (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
connection.close();
this._connectionsToCommit.delete(tabUid);
}
}
/**
* CREATE TABLE
*
@@ -666,6 +705,7 @@ export class SQLiteClient extends AntaresCore {
details: false,
split: true,
comments: true,
autocommit: true,
...args
};
@@ -679,7 +719,20 @@ export class SQLiteClient extends AntaresCore {
.filter(Boolean)
.map(q => q.trim())
: [sql];
const connection = this._connection;
let connection;
if (!args.autocommit && args.tabUid) { // autocommit OFF
if (this._connectionsToCommit.has(args.tabUid))
connection = this._connectionsToCommit.get(args.tabUid);
else {
connection = this.getConnection();
connection.prepare('BEGIN TRANSACTION').run();
this._connectionsToCommit.set(args.tabUid, connection);
}
}
else// autocommit ON
connection = this._connection;
for (const query of queries) {
if (!query) continue;
@@ -732,7 +785,7 @@ export class SQLiteClient extends AntaresCore {
if ([...TIME, ...DATETIME].includes(parsedType)) {
const firstNotNull = queryResult.find(res => res[field.name] !== null);
if (firstNotNull[field.name].includes('.'))
if (firstNotNull && firstNotNull[field.name].includes('.'))
length = firstNotNull[field.name].split('.').pop().length;
}

View File

@@ -3,6 +3,7 @@
import { app, BrowserWindow, /* session, */ nativeImage, Menu } from 'electron';
import * as path from 'path';
import Store from 'electron-store';
import * as windowStateKeeper from 'electron-window-state';
import * as remoteMain from '@electron/remote/main';
import ipcHandlers from './ipc-handlers';
@@ -18,12 +19,15 @@ process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
// global reference to mainWindow (necessary to prevent window from being garbage collected)
let mainWindow;
let mainWindowState;
async function createMainWindow () {
const icon = require('../renderer/images/logo-32.png');
const window = new BrowserWindow({
width: 1024,
height: 800,
width: mainWindowState.width,
height: mainWindowState.height,
x: mainWindowState.x,
y: mainWindowState.y,
minWidth: 900,
minHeight: 550,
title: 'Antares SQL',
@@ -41,6 +45,9 @@ async function createMainWindow () {
backgroundColor: '#1d1d1d'
});
mainWindowState.manage(window);
window.on('moved', saveWindowState);
remoteMain.enable(window.webContents);
try {
@@ -70,16 +77,10 @@ async function createMainWindow () {
}
window.on('closed', () => {
window.removeListener('moved', saveWindowState);
mainWindow = null;
});
window.webContents.on('devtools-opened', () => {
window.focus();
setImmediate(() => {
window.focus();
});
});
return window;
}
@@ -104,6 +105,11 @@ else {
// create main BrowserWindow when electron is ready
app.on('ready', async () => {
mainWindowState = windowStateKeeper({
defaultWidth: 1024,
defaultHeight: 800
});
mainWindow = await createMainWindow();
createAppMenu();
@@ -160,3 +166,7 @@ function createAppMenu () {
Menu.setApplicationMenu(menu);
}
function saveWindowState () {
mainWindowState.saveState(mainWindow);
}

View File

@@ -0,0 +1,108 @@
<template>
<div id="map" class="map" />
</template>
<script>
import L from 'leaflet';
import {
point,
lineString,
polygon
} from '@turf/helpers';
import { getArrayDepth } from 'common/libs/getArrayDepth';
export default {
name: 'BaseMap',
props: {
points: [Object, Array],
isMultiSpatial: Boolean
},
data () {
return {
map: null,
markers: [],
center: null
};
},
mounted () {
if (this.isMultiSpatial) {
for (const element of this.points)
this.markers.push(this.getMarkers(element));
}
else {
this.markers = this.getMarkers(this.points);
if (!Array.isArray(this.points))
this.center = [this.points.y, this.points.x];
}
this.map = L.map('map', {
center: this.center || [0, 0],
zoom: 15,
minZoom: 1,
attributionControl: false
});
L.control.attribution({ prefix: '<b>Leaflet</b>' }).addTo(this.map);
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>
<style lang="scss">
.map{
height: 400px;
}
.marker-icon{
display: flex;
justify-content: center;
align-items: center;
background: $primary-color;
border-radius: 50%;
box-shadow: 0 0 5px 1px darken($body-font-color-dark, 40%);
}
</style>

View File

@@ -7,7 +7,7 @@
@blur="$emit('blur')"
>
<option v-if="!isValidDefault" :value="value">
{{ value }} - {{ $t('message.invalidDefault') }}
{{ value === null ? 'NULL' : value }}
</option>
<option
v-for="row in foreignList"

View File

@@ -6,13 +6,13 @@
@confirm="runRoutine"
@hide="closeModal"
>
<template slot="header">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-play mr-1" />
<span class="cut-text">{{ $t('word.parameters') }}: {{ localRoutine.name }}</span>
</div>
</template>
<div slot="body">
<template #body>
<div class="content">
<form class="form-horizontal">
<div
@@ -43,7 +43,7 @@
</div>
</form>
</div>
</div>
</template>
</ConfirmModal>
</template>

View File

@@ -5,16 +5,16 @@
@confirm="$emit('confirm')"
@hide="$emit('close')"
>
<template slot="header">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-content-save-alert mr-1" /> {{ $t('message.unsavedChanges') }}
</div>
</template>
<div slot="body">
<template #body>
<div>
{{ $t('message.discardUnsavedChanges') }}
</div>
</div>
</template>
</ConfirmModal>
</template>

View File

@@ -6,7 +6,7 @@
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span class="cut-text">{{ $t('message.tableFiller') }}</span>
<span class="cut-text">{{ $tc('message.insertRow', 2) }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -41,7 +41,7 @@
<label class="form-checkbox ml-3" :title="$t('word.insert')">
<input
type="checkbox"
:checked="!field.autoIncrement"
:checked="!fieldsToExclude.includes(field.name)"
@change.prevent="toggleFields($event, field)"
><i class="form-icon" />
</label>
@@ -264,7 +264,7 @@ export default {
else if (BIT.includes(field.type))
fieldDefault = field.default.replaceAll('\'', '').replaceAll('b', '');
else if (DATETIME.includes(field.type)) {
if (field.default && ['current_timestamp', 'now()'].includes(field.default.toLowerCase())) {
if (field.default && ['current_timestamp', 'now()'].some(term => field.default.toLowerCase().includes(term))) {
let datePrecision = '';
for (let i = 0; i < field.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
@@ -281,7 +281,7 @@ export default {
rowObj[field.name] = { value: fieldDefault };
if (field.autoIncrement)// Disable by default auto increment fields
if (field.autoIncrement || !!field.onUpdate)// Disable by default auto increment or "on update" fields
this.fieldsToExclude = [...this.fieldsToExclude, field.name];
}

View File

@@ -22,12 +22,12 @@
:hide-footer="true"
@hide="hideInfoModal"
>
<template :slot="'header'">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-information-outline mr-1" /> {{ $t('message.processInfo') }}
</div>
</template>
<div :slot="'body'">
<template #body>
<div>
<div>
<TextEditor
@@ -38,7 +38,7 @@
/>
</div>
</div>
</div>
</template>
</ConfirmModal>
</div>
</template>

View File

@@ -15,16 +15,16 @@
@confirm="confirmDeleteConnection"
@hide="hideConfirmModal"
>
<template :slot="'header'">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-server-remove mr-1" /> {{ $t('message.deleteConnection') }}
</div>
</template>
<div :slot="'body'">
<template #body>
<div class="mb-2">
{{ $t('message.deleteCorfirm') }} <b>{{ connectionName }}</b>?
</div>
</div>
</template>
</ConfirmModal>
</BaseContextMenu>
</template>

View File

@@ -11,9 +11,9 @@
<div class="footer-right-elements">
<ul class="footer-elements">
<li class="footer-element footer-link" @click="openOutside('https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet')">
<i class="mdi mdi-18px mdi-tree mr-1" />
<small>{{ $t('message.plantATree') }}</small>
<li class="footer-element footer-link" @click="openOutside('https://www.paypal.com/paypalme/fabiodistasio')">
<i class="mdi mdi-18px mdi-coffee mr-1" />
<small>{{ $t('word.donate') }}</small>
</li>
<li class="footer-element footer-link" @click="openOutside('https://github.com/Fabio286/antares/issues')">
<i class="mdi mdi-18px mdi-bug" />

View File

@@ -6,12 +6,12 @@
:hide-footer="true"
@hide="hideScratchpad"
>
<template :slot="'header'">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-notebook-edit-outline mr-1" /> {{ $t('word.scratchpad') }}
</div>
</template>
<div :slot="'body'">
<template #body>
<div>
<div>
<TextEditor
@@ -24,7 +24,7 @@
</div>
<small class="text-gray">{{ $t('message.markdownSupported') }}</small>
</div>
</div>
</template>
</ConfirmModal>
</template>

View File

@@ -200,7 +200,7 @@ export default {
}
&::before {
content: '';
content: "";
height: 0;
width: 3px;
transition: height 0.2s;

View File

@@ -18,14 +18,17 @@
<li
v-for="(tab, i) of draggableTabs"
:key="i"
:ref="selectedTab === tab.uid ? 'tab-selected' : ''"
class="tab-item tab-draggable"
draggable="true"
:class="{'active': selectedTab === tab.uid}"
@mousedown.left="selectTab({uid: workspace.uid, tab: tab.uid})"
@mouseup.middle="closeTab(tab)"
>
<a v-if="tab.type === 'query'" class="tab-link">
<a
v-if="tab.type === 'query'"
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-18px mdi-code-tags mr-1" />
<span>
<span>{{ tab.content || 'Query' | cutText }} #{{ tab.index }}</span>
@@ -256,56 +259,59 @@
</span>
</a>
</li>
<li
v-if="workspace.customizations.processesList"
slot="header"
class="tab-item dropdown tools-dropdown"
>
<a
class="tab-link workspace-tools-link dropdown-toggle"
tabindex="0"
:title="$t('word.tools')"
<template #header>
<li
v-if="workspace.customizations.processesList"
class="tab-item dropdown tools-dropdown"
>
<i class="mdi mdi-24px mdi-tools" />
</a>
<ul v-if="hasTools" class="menu text-left text-uppercase">
<li class="menu-item">
<a class="c-hand p-vcentered" @click="showProcessesModal">
<i class="mdi mdi-memory mr-1 tool-icon" />
<span>{{ $t('message.processesList') }}</span>
</a>
</li>
<li
v-if="workspace.customizations.variables"
class="menu-item"
title="Coming..."
<a
class="tab-link workspace-tools-link dropdown-toggle"
tabindex="0"
:title="$t('word.tools')"
>
<a class="c-hand p-vcentered disabled">
<i class="mdi mdi-shape mr-1 tool-icon" />
<span>{{ $t('word.variables') }}</span>
</a>
</li>
<li
v-if="workspace.customizations.usersManagement"
class="menu-item"
title="Coming..."
<i class="mdi mdi-24px mdi-tools" />
</a>
<ul v-if="hasTools" class="menu text-left text-uppercase">
<li class="menu-item">
<a class="c-hand p-vcentered" @click="showProcessesModal">
<i class="mdi mdi-memory mr-1 tool-icon" />
<span>{{ $t('message.processesList') }}</span>
</a>
</li>
<li
v-if="workspace.customizations.variables"
class="menu-item"
title="Coming..."
>
<a class="c-hand p-vcentered disabled">
<i class="mdi mdi-shape mr-1 tool-icon" />
<span>{{ $t('word.variables') }}</span>
</a>
</li>
<li
v-if="workspace.customizations.usersManagement"
class="menu-item"
title="Coming..."
>
<a class="c-hand p-vcentered disabled">
<i class="mdi mdi-account-group mr-1 tool-icon" />
<span>{{ $t('message.manageUsers') }}</span>
</a>
</li>
</ul>
</li>
</template>
<template #footer>
<li class="tab-item">
<a
class="tab-add"
:title="$t('message.openNewTab')"
@click="addQueryTab"
>
<a class="c-hand p-vcentered disabled">
<i class="mdi mdi-account-group mr-1 tool-icon" />
<span>{{ $t('message.manageUsers') }}</span>
</a>
</li>
</ul>
</li>
<li slot="footer" class="tab-item">
<a
class="tab-add"
:title="$t('message.openNewTab')"
@click="addQueryTab"
>
<i class="mdi mdi-24px mdi-plus" />
</a>
</li>
<i class="mdi mdi-24px mdi-plus" />
</a>
</li>
</template>
</Draggable>
<WorkspaceEmptyState v-if="!workspace.tabs.length" @new-tab="addQueryTab" />
<template v-for="tab of workspace.tabs">
@@ -565,7 +571,7 @@ export default {
return this.workspace ? this.workspace.selectedTab : null;
},
queryTabs () {
return this.workspace.tabs.filter(tab => tab.type === 'query');
return this.workspace ? this.workspace.tabs.filter(tab => tab.type === 'query') : [];
},
schemaChild () {
for (const key in this.workspace.breadcrumbs) {
@@ -581,16 +587,12 @@ export default {
}
},
watch: {
selectedTab (newVal, oldVal) {
if (newVal !== oldVal) {
queryTabs: function (newVal, oldVal) {
if (newVal.length > oldVal.length) {
setTimeout(() => {
const element = this.$refs['tab-selected'] ? this.$refs['tab-selected'][0] : null;
if (element) {
element.setAttribute('tabindex', '-1');
element.focus();
element.removeAttribute('tabindex');
}
}, 50);
const scroller = this.$refs.tabWrap;
if (scroller) scroller.$el.scrollLeft = scroller.$el.scrollWidth;
}, 0);
}
}
},

View File

@@ -42,17 +42,17 @@
@confirm="deleteMisc"
@hide="hideDeleteModal"
>
<template slot="header">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-delete mr-1" />
<span class="cut-text">{{ deleteMessage }}</span>
</div>
</template>
<div slot="body">
<template #body>
<div class="mb-2">
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedMisc.name }}</b>"?
</div>
</div>
</template>
</ConfirmModal>
<ModalAskParameters
v-if="isAskingParameters"

View File

@@ -452,9 +452,9 @@ export default {
top: 0;
z-index: 2;
.schema-size{
visibility: hidden;
width: 22.5px;
.schema-size {
visibility: hidden;
width: 22.5px;
}
}
@@ -502,8 +502,8 @@ export default {
&:hover {
border-radius: $border-radius;
.schema-size{
visibility: visible;
.schema-size {
visibility: visible;
}
}
}

View File

@@ -78,17 +78,17 @@
@confirm="deleteSchema"
@hide="hideDeleteModal"
>
<template slot="header">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-remove mr-1" />
<span class="cut-text">{{ $t('message.deleteSchema') }}</span>
</div>
</template>
<div slot="body">
<template #body>
<div class="mb-2">
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedSchema }}</b>"?
</div>
</div>
</template>
</ConfirmModal>
<ModalEditSchema
v-if="isEditModal"

View File

@@ -40,33 +40,33 @@
@confirm="emptyTable"
@hide="hideEmptyModal"
>
<template slot="header">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-table-off mr-1" /> <span class="cut-text">{{ $t('message.emptyTable') }}</span>
</div>
</template>
<div slot="body">
<template #body>
<div class="mb-2">
{{ $t('message.emptyCorfirm') }} "<b>{{ selectedTable.name }}</b>"?
</div>
</div>
</template>
</ConfirmModal>
<ConfirmModal
v-if="isDeleteModal"
@confirm="deleteTable"
@hide="hideDeleteModal"
>
<template slot="header">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-table-remove mr-1" />
<span class="cut-text">{{ selectedTable.type === 'table' ? $t('message.deleteTable') : $t('message.deleteView') }}</span>
</div>
</template>
<div slot="body">
<template #body>
<div class="mb-2">
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedTable.name }}</b>"?
</div>
</div>
</template>
</ConfirmModal>
</BaseContextMenu>
</template>

View File

@@ -5,7 +5,7 @@
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:disabled="!isChanged || !isValid"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
@@ -242,6 +242,9 @@ export default {
JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage) ||
JSON.stringify(this.originalIndexes) !== JSON.stringify(this.localIndexes) ||
JSON.stringify(this.tableOptions) !== JSON.stringify(this.localOptions);
},
isValid () {
return !!this.localFields.length && !!this.localOptions.name.trim().length;
}
},
watch: {
@@ -287,7 +290,7 @@ export default {
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
async saveChanges () {
if (this.isSaving) return;
if (this.isSaving || !this.isValid) return;
this.isSaving = true;
const params = {

View File

@@ -6,13 +6,13 @@
@confirm="confirmParametersChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span class="cut-text">{{ $t('word.parameters') }} "{{ func }}"</span>
</div>
</template>
<div :slot="'body'">
<template #body>
<div class="columns col-gapless">
<div class="column col-5">
<div class="panel" :style="{ height: modalInnerHeight + 'px'}">
@@ -167,7 +167,7 @@
</div>
</div>
</div>
</div>
</template>
</ConfirmModal>
</template>

View File

@@ -6,13 +6,13 @@
@confirm="confirmParametersChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span class="cut-text">{{ $t('word.parameters') }} "{{ routine }}"</span>
</div>
</template>
<div :slot="'body'">
<template #body>
<div class="columns col-gapless">
<div class="column col-5">
<div class="panel" :style="{ height: modalInnerHeight + 'px'}">
@@ -167,7 +167,7 @@
</div>
</div>
</div>
</div>
</template>
</ConfirmModal>
</template>

View File

@@ -5,13 +5,13 @@
@confirm="confirmOptionsChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-timer mr-1" />
<span class="cut-text">{{ $t('word.timing') }} "{{ localOptions.name }}"</span>
</div>
</template>
<div :slot="'body'">
<template #body>
<form class="form-horizontal">
<div class="form-group">
<label class="form-label col-4">
@@ -133,7 +133,7 @@
</div>
</div>
</form>
</div>
</template>
</ConfirmModal>
</template>

View File

@@ -616,6 +616,14 @@ export default {
},
removeField (uid) {
this.localFields = this.localFields.filter(field => field._antares_id !== uid);
this.localKeyUsage = this.localKeyUsage.filter(fk =>// Clear foreign keys
this.localFields.some(field => field.name === fk.field)
);
this.localIndexes = this.localIndexes.filter(index =>// Clear indexes
this.localFields.some(field =>
index.fields.includes(field.name)
)
);
},
addNewIndex (payload) {
this.localIndexes = [...this.localIndexes, {

View File

@@ -6,13 +6,13 @@
@confirm="confirmForeignsChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-key-link mr-1" />
<span class="cut-text">{{ $t('word.foreignKeys') }} "{{ table }}"</span>
</div>
</template>
<div :slot="'body'">
<template #body>
<div class="columns col-gapless">
<div class="column col-5">
<div class="panel" :style="{ height: modalInnerHeight + 'px'}">
@@ -197,7 +197,7 @@
</div>
</div>
</div>
</div>
</template>
</ConfirmModal>
</template>
@@ -289,7 +289,7 @@ export default {
addForeign () {
this.foreignProxy = [...this.foreignProxy, {
_antares_id: uidGen(),
constraintName: `FK_${this.foreignProxy.length + 1}`,
constraintName: `FK_${uidGen()}`,
refSchema: this.schema,
table: this.table,
refTable: '',

View File

@@ -6,13 +6,13 @@
@confirm="confirmIndexesChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-key mdi-rotate-45 mr-1" />
<span class="cut-text">{{ $t('word.indexes') }} "{{ table }}"</span>
</div>
</template>
<div :slot="'body'">
<template #body>
<div class="columns col-gapless">
<div class="column col-5">
<div class="panel" :style="{ height: modalInnerHeight + 'px'}">
@@ -133,7 +133,7 @@
</div>
</div>
</div>
</div>
</template>
</ConfirmModal>
</template>

View File

@@ -99,6 +99,9 @@
<span v-if="localRow.enumValues">
{{ localRow.enumValues }}
</span>
<span v-else-if="localRow.numScale">
{{ localLength }}, {{ localRow.numScale }}
</span>
<span v-else>
{{ localLength }}
</span>
@@ -112,6 +115,16 @@
class="editable-field form-input input-sm px-1"
@blur="editOFF"
>
<input
v-else-if="fieldType.scale"
ref="editField"
v-model="editingContent"
type="text"
autofocus
class="editable-field form-input input-sm px-1"
@keypress="checkLengthScale"
@blur="editOFF"
>
<input
v-else
ref="editField"
@@ -230,13 +243,13 @@
@confirm="editOFF"
@hide="hideDefaultModal"
>
<template :slot="'header'">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-edit mr-1" />
<span class="cut-text">{{ $t('word.default') }} "{{ row.name }}"</span>
</div>
</template>
<div :slot="'body'">
<template #body>
<form class="form-horizontal">
<div class="mb-2">
<label class="form-radio form-inline">
@@ -324,7 +337,7 @@
</div>
</div>
</form>
</div>
</template>
</ConfirmModal>
</div>
</template>
@@ -480,6 +493,11 @@ export default {
this.editingContent = this.localRow.enumValues;
this.originalContent = this.localRow.enumValues;
}
else if (this.fieldType.scale && field === 'length') {
const scale = this.localRow.numScale !== null ? this.localRow.numScale : 0;
this.editingContent = `${content}, ${scale}`;
this.originalContent = `${content}, ${scale}`;
}
else {
this.editingContent = content;
this.originalContent = content;
@@ -502,10 +520,17 @@ export default {
if (this.editingField === 'name')
this.$emit('rename-field', { old: this.localRow[this.editingField], new: this.editingContent });
this.localRow[this.editingField] = this.editingContent;
if (this.editingField === 'numLength' && this.fieldType.scale) {
const [length, scale] = this.editingContent.split(',');
this.localRow.numLength = +length;
this.localRow.numScale = scale ? +scale : null;
}
else
this.localRow[this.editingField] = this.editingContent;
if (this.editingField === 'type' && this.editingContent !== this.originalContent) {
this.localRow.numLength = null;
this.localRow.numScale = null;
this.localRow.charLength = null;
this.localRow.datePrecision = null;
this.localRow.enumValues = '';
@@ -560,6 +585,15 @@ export default {
this.originalContent = null;
this.editingField = null;
},
checkLengthScale (e) {
e = (e) || window.event;
const charCode = (e.which) ? e.which : e.keyCode;
if (((charCode > 31 && (charCode < 48 || charCode > 57)) && charCode !== 44) || (charCode === 44 && e.target.value.includes(',')))
e.preventDefault();
else
return true;
},
hideDefaultModal () {
this.isDefaultModal = false;
}

View File

@@ -4,6 +4,7 @@
class="workspace-query-tab column col-12 columns col-gapless no-outline p-0"
tabindex="0"
@keydown.116="runQuery(query)"
@keydown.75="killTabQuery"
@keydown.ctrl.alt.87="clear"
@keydown.ctrl.66="beautify"
@keydown.ctrl.71="openHistoryModal"
@@ -22,15 +23,46 @@
<div ref="resizer" class="query-area-resizer" />
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<div @mouseenter="setCancelButtonVisibility(true)" @mouseleave="setCancelButtonVisibility(false)">
<button
v-if="showCancel && isQuering"
class="btn btn-primary btn-sm cancellable"
:disabled="!query"
:title="$t('word.cancel')"
@click="killTabQuery()"
>
<i class="mdi mdi-24px mdi-window-close" />
<span class="d-invisible pr-1">{{ $t('word.run') }}</span>
</button>
<button
v-else
class="btn btn-primary btn-sm"
:class="{'loading':isQuering}"
:disabled="!query"
title="F5"
@click="runQuery(query)"
>
<i class="mdi mdi-24px mdi-play pr-1" />
<span>{{ $t('word.run') }}</span>
</button>
</div>
<button
class="btn btn-primary btn-sm"
v-if="!autocommit"
class="btn btn-dark btn-sm"
:class="{'loading':isQuering}"
:disabled="!query"
title="F5"
@click="runQuery(query)"
@click="commitTab()"
>
<i class="mdi mdi-24px mdi-play pr-1" />
<span>{{ $t('word.run') }}</span>
<i class="mdi mdi-24px mdi-cube-send pr-1" />
<span>{{ $t('word.commit') }}</span>
</button>
<button
v-if="!autocommit"
class="btn btn-dark btn-sm"
:class="{'loading':isQuering}"
@click="rollbackTab()"
>
<i class="mdi mdi-24px mdi-undo-variant pr-1" />
<span>{{ $t('word.rollback') }}</span>
</button>
<button
class="btn btn-link btn-sm mr-0"
@@ -64,7 +96,7 @@
</button>
<div class="dropdown table-dropdown pr-2">
<button
:disabled="!results.length || isQuering"
:disabled="!hasResults || isQuering"
class="btn btn-dark btn-sm dropdown-toggle mr-0 pr-0"
tabindex="0"
>
@@ -81,6 +113,17 @@
</li>
</ul>
</div>
<div class="input-group pr-2" :title="$t('message.commitMode')">
<i class="input-group-addon addon-sm mdi mdi-24px mdi-source-commit p-0" />
<select v-model="autocommit" class="form-select select-sm text-bold">
<option :value="true">
{{ $t('message.autoCommit') }}
</option>
<option :value="false">
{{ $t('message.manualCommit') }}
</option>
</select>
</div>
</div>
<div class="workspace-query-info">
<div
@@ -90,11 +133,19 @@
>
<i class="mdi mdi-timer-sand mdi-rotate-180 pr-1" /> <b>{{ durationsCount / 1000 }}s</b>
</div>
<div v-if="resultsCount">
{{ $t('word.results') }}: <b>{{ resultsCount.toLocaleString() }}</b>
<div
v-if="resultsCount"
class="d-flex"
:title="$t('word.results')"
>
<i class="mdi mdi-equal pr-1" /> <b>{{ resultsCount.toLocaleString() }}</b>
</div>
<div v-if="affectedCount !== null">
{{ $t('message.affectedRows') }}: <b>{{ affectedCount }}</b>
<div
v-if="hasAffected"
class="d-flex"
:title="$t('message.affectedRows')"
>
<i class="mdi mdi-target pr-1" /> <b>{{ affectedCount }}</b>
</div>
<div class="input-group" :title="$t('word.schema')">
<i class="input-group-addon addon-sm mdi mdi-24px mdi-database" />
@@ -110,7 +161,7 @@
</div>
</div>
</div>
<WorkspaceTabQueryEmptyState v-if="!results.length && !isQuering" />
<WorkspaceTabQueryEmptyState v-if="!results.length && !isQuering" :customizations="workspace.customizations" />
<div class="workspace-query-results p-relative column col-12">
<BaseLoader v-if="isQuering" />
<WorkspaceTabQueryTable
@@ -166,6 +217,9 @@ export default {
query: '',
lastQuery: '',
isQuering: false,
isCancelling: false,
showCancel: false,
autocommit: true,
results: [],
selectedSchema: null,
resultsCount: 0,
@@ -184,6 +238,9 @@ export default {
workspace () {
return this.getWorkspace(this.connection.uid);
},
tabUid () {
return this.$vnode.key;
},
breadcrumbsSchema () {
return this.workspace.breadcrumbs.schema || null;
},
@@ -198,12 +255,23 @@ export default {
},
history () {
return this.getHistoryByWorkspace(this.connection.uid) || [];
},
hasResults () {
return this.results.length && this.results[0].rows;
},
hasAffected () {
return this.affectedCount || (!this.resultsCount && this.affectedCount !== null);
}
},
watch: {
isSelected (val) {
if (val)
if (val) {
this.changeBreadcrumbs({ schema: this.selectedSchema, query: `Query #${this.tab.index}` });
setTimeout(() => {
if (this.$refs.queryEditor)
this.$refs.queryEditor.editor.focus();
}, 0);
}
},
selectedSchema () {
this.changeBreadcrumbs({ schema: this.selectedSchema, query: `Query #${this.tab.index}` });
@@ -230,12 +298,18 @@ export default {
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
const params = {
uid: this.connection.uid,
tabUid: this.tab.uid
};
Schema.destroyConnectionToCommit(params);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
updateTabContent: 'workspaces/updateTabContent',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
saveHistory: 'history/saveHistory'
}),
async runQuery (query) {
@@ -248,6 +322,8 @@ export default {
const params = {
uid: this.connection.uid,
schema: this.selectedSchema,
tabUid: this.tab.uid,
autocommit: this.autocommit,
query
};
@@ -272,6 +348,8 @@ export default {
content: query
});
this.saveHistory(params);
if (!this.autocommit)
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: true });
}
else
this.addNotification({ status: 'error', message: response });
@@ -283,6 +361,29 @@ export default {
this.isQuering = false;
this.lastQuery = query;
},
async killTabQuery () {
if (this.isCancelling) return;
this.isCancelling = true;
try {
const params = {
uid: this.connection.uid,
tabUid: this.tab.uid
};
await Schema.killTabQuery(params);
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isCancelling = false;
},
setCancelButtonVisibility (val) {
if (this.workspace.customizations.cancelQueries)
this.showCancel = val;
},
reloadTable () {
this.runQuery(this.lastQuery);
},
@@ -346,6 +447,42 @@ export default {
},
downloadTable (format) {
this.$refs.queryTable.downloadTable(format, `${this.tab.type}-${this.tab.index}`);
},
async commitTab () {
this.isQuering = true;
try {
const params = {
uid: this.connection.uid,
tabUid: this.tab.uid
};
await Schema.commitTab(params);
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: false });
this.addNotification({ status: 'success', message: this.$t('message.actionSuccessful', { action: 'COMMIT' }) });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false;
},
async rollbackTab () {
this.isQuering = true;
try {
const params = {
uid: this.connection.uid,
tabUid: this.tab.uid
};
await Schema.rollbackTab(params);
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: false });
this.addNotification({ status: 'success', message: this.$t('message.actionSuccessful', { action: 'ROLLBACK' }) });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false;
}
}
};
@@ -374,10 +511,12 @@ export default {
.workspace-query-runner-footer {
display: flex;
flex-wrap: wrap;
row-gap: 0.4rem;
justify-content: space-between;
padding: 0.3rem 0.6rem 0.4rem;
align-items: center;
height: 42px;
min-height: 42px;
.workspace-query-buttons,
.workspace-query-info {

View File

@@ -5,6 +5,9 @@
<div class="mb-4">
{{ $t('message.runQuery') }}
</div>
<div v-if="customizations.cancelQueries" class="mb-4">
{{ $t('message.killQuery') }}
</div>
<div class="mb-4">
{{ $t('word.format') }}
</div>
@@ -25,6 +28,9 @@
<div class="mb-4">
<code>F5</code>
</div>
<div v-if="customizations.cancelQueries" class="mb-4">
<code>CTRL</code> + <code>K</code>
</div>
<div class="mb-4">
<code>CTRL</code> + <code>B</code>
</div>
@@ -47,7 +53,10 @@
<script>
export default {
name: 'WorkspaceTabQueryEmptyState'
name: 'WorkspaceTabQueryEmptyState',
props: {
customizations: Object
}
};
</script>

View File

@@ -5,7 +5,7 @@
tabindex="0"
:style="{'height': resultsSize+'px'}"
@keyup.46="showDeleteConfirmModal"
@keydown.ctrl.65="selectAllRows"
@keydown.ctrl.65="selectAllRows($event)"
@keydown.esc="deselectRows"
>
<TableContext
@@ -53,6 +53,7 @@
class="mdi sort-icon"
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'"
/>
<i v-else class="mdi sort-icon mdi-minus d-invisible" />
</div>
</div>
</div>
@@ -62,7 +63,7 @@
v-if="resultsWithRows[resultsetIndex] && resultsWithRows[resultsetIndex].rows"
ref="resultTable"
:items="sortedResults"
:item-height="23"
:item-height="rowHeight"
class="tbody"
:visible-height="resultsSize"
:scroll-element="scrollElement"
@@ -71,6 +72,7 @@
<WorkspaceTabQueryTableRow
v-for="row in items"
:key="row._antares_id"
:item-height="rowHeight"
:row="row"
:fields="fieldsObj"
:key-usage="keyUsage"
@@ -89,17 +91,17 @@
@confirm="deleteSelected"
@hide="hideDeleteConfirmModal"
>
<template :slot="'header'">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-delete mr-1" />
<span class="cut-text">{{ $tc('message.deleteRows', selectedRows.length) }}</span>
</div>
</template>
<div :slot="'body'">
<template #body>
<div class="mb-2">
{{ $tc('message.confirmToDeleteRows', selectedRows.length) }}
</div>
</div>
</template>
</ConfirmModal>
</div>
</template>
@@ -142,7 +144,8 @@ export default {
currentSort: '',
currentSortDir: 'asc',
resultsetIndex: 0,
scrollElement: null
scrollElement: null,
rowHeight: 23
};
},
computed: {
@@ -242,6 +245,11 @@ export default {
if (this.$refs.tableWrapper)
this.scrollElement = this.$refs.tableWrapper;
document.querySelectorAll('.column-resizable').forEach(element => {
if (element.clientWidth !== 0)
element.style.width = element.clientWidth + 'px';
});
},
mounted () {
window.addEventListener('resize', this.resizeResults);
@@ -272,6 +280,7 @@ export default {
fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
else if (TEXT.includes(field.type)) return field.charLength;
else if (field.numScale) return `${field.numPrecision}, ${field.numScale}`;
return field.length;
},
keyName (key) {
@@ -399,10 +408,13 @@ export default {
const row = this.localResults.find(row => this.selectedRows.includes(row._antares_id));
const cellName = Object.keys(row).find(prop => [
this.selectedCell.field,
this.selectedCell.orgField,
`${this.fields[0].table}.${this.selectedCell.field}`,
`${this.fields[0].tableAlias}.${this.selectedCell.field}`
].includes(prop));
const valueToCopy = row[cellName];
let valueToCopy = row[cellName];
if (typeof valueToCopy === 'object')
valueToCopy = JSON.stringify(valueToCopy);
navigator.clipboard.writeText(valueToCopy);
},
copyRow () {
@@ -450,7 +462,9 @@ export default {
else
this.selectedRows = [row];
},
selectAllRows () {
selectAllRows (e) {
if (e.target.classList.contains('editable-field')) return;
this.selectedRows = this.localResults.reduce((acc, curr) => {
acc.push(curr._antares_id);
return acc;

View File

@@ -61,8 +61,6 @@ export default {
selectedRows: Array,
selectedCell: Object
},
computed: {
},
methods: {
showConfirmModal () {
this.$emit('show-delete-modal');

View File

@@ -1,12 +1,16 @@
<template>
<div class="tr" @click="selectRow($event, row._antares_id)">
<div
class="tr"
:style="{height: itemHeight+'px'}"
@click="selectRow($event, row._antares_id)"
>
<div
v-for="(col, cKey) in row"
v-show="cKey !== '_antares_id'"
:key="cKey"
class="td p-0"
tabindex="0"
@contextmenu.prevent="openContext($event, { id: row._antares_id, field: cKey })"
@contextmenu.prevent="openContext($event, { id: row._antares_id, orgField: cKey })"
>
<template v-if="cKey !== '_antares_id'">
<span
@@ -72,12 +76,12 @@
@confirm="editOFF"
@hide="hideEditorModal"
>
<template :slot="'header'">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> <span class="cut-text">{{ $t('word.edit') }} "{{ editingField }}"</span>
</div>
</template>
<div :slot="'body'">
<template #body>
<div class="mb-2">
<div>
<TextEditor
@@ -96,23 +100,12 @@
v-model="editorMode"
class="form-select select-sm"
>
<option value="text">
TEXT
</option>
<option value="html">
HTML
</option>
<option value="xml">
XML
</option>
<option value="json">
JSON
</option>
<option value="svg">
SVG
</option>
<option value="yaml">
YAML
<option
v-for="language in availableLanguages"
:key="language.slug"
:value="language.slug"
>
{{ language.name }}
</option>
</select>
</div>
@@ -128,7 +121,22 @@
</div>
</div>
</div>
</div>
</template>
</ConfirmModal>
<ConfirmModal
v-if="isMapModal"
:hide-footer="true"
size="medium"
@hide="hideEditorModal"
>
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-map mr-1" /> <span class="cut-text">"{{ editingField }}"</span>
</div>
</template>
<template #body>
<BaseMap :points="editingContent" :is-multi-spatial="isMultiSpatial" />
</template>
</ConfirmModal>
<ConfirmModal
v-if="isBlobEditor"
@@ -136,13 +144,13 @@
@confirm="editOFF"
@hide="hideEditorModal"
>
<template :slot="'header'">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-edit mr-1" />
<span class="cut-text">{{ $t('word.edit') }} "{{ editingField }}"</span>
</div>
</template>
<div :slot="'body'">
<template #body>
<div class="mb-2">
<transition name="jump-down">
<div v-if="contentInfo.size">
@@ -182,21 +190,39 @@
>
</div>
</div>
</div>
</template>
</ConfirmModal>
</div>
</template>
<script>
import moment from 'moment';
import { ModelOperations } from '@vscode/vscode-languagedetection';
import { mimeFromHex } from 'common/libs/mimeFromHex';
import { formatBytes } from 'common/libs/formatBytes';
import { bufferToBase64 } from 'common/libs/bufferToBase64';
import hexToBinary from 'common/libs/hexToBinary';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BOOLEAN, DATE, TIME, DATETIME, BLOB, BIT, HAS_TIMEZONE } from 'common/fieldTypes';
import {
TEXT,
LONG_TEXT,
ARRAY,
TEXT_SEARCH,
NUMBER,
FLOAT,
BOOLEAN,
DATE,
TIME,
DATETIME,
BLOB,
BIT,
HAS_TIMEZONE,
SPATIAL,
IS_MULTI_SPATIAL
} from 'common/fieldTypes';
import { VueMaskDirective } from 'v-mask';
import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
import BaseMap from '@/components/BaseMap';
import ForeignKeySelect from '@/components/ForeignKeySelect';
export default {
@@ -204,7 +230,8 @@ export default {
components: {
ConfirmModal,
TextEditor,
ForeignKeySelect
ForeignKeySelect,
BaseMap
},
directives: {
mask: VueMaskDirective
@@ -245,7 +272,8 @@ export default {
if (BIT.includes(type)) {
if (typeof val === 'number') val = [val];
const hex = Buffer.from(val).toString('hex');
return hexToBinary(hex);
const bitString = hexToBinary(hex);
return parseInt(bitString).toString().padStart(precision, '0');
}
if (ARRAY.includes(type)) {
@@ -254,13 +282,17 @@ export default {
return val;
}
return val;
if (SPATIAL.includes(type))
return val;
return typeof val === 'object' ? JSON.stringify(val) : val;
}
},
props: {
row: Object,
fields: Object,
keyUsage: Array,
itemHeight: Number,
elementType: { type: String, default: 'table' }
},
data () {
@@ -268,6 +300,8 @@ export default {
isInlineEditor: {},
isTextareaEditor: false,
isBlobEditor: false,
isMapModal: false,
isMultiSpatial: false,
willBeDeleted: false,
originalContent: null,
editingContent: null,
@@ -280,7 +314,17 @@ export default {
mime: '',
size: null
},
fileToUpload: null
fileToUpload: null,
availableLanguages: [
{ name: 'TEXT', slug: 'text', id: 'text' },
{ name: 'HTML', slug: 'html', id: 'html' },
{ name: 'XML', slug: 'xml', id: 'xml' },
{ name: 'JSON', slug: 'json', id: 'json' },
{ name: 'SVG', slug: 'svg', id: 'svg' },
{ name: 'INI', slug: 'ini', id: 'ini' },
{ name: 'MARKDOWN', slug: 'markdown', id: 'md' },
{ name: 'YAML', slug: 'yaml', id: 'yaml' }
]
};
},
computed: {
@@ -326,6 +370,9 @@ export default {
if (BOOLEAN.includes(this.editingType))
return { type: 'boolean', mask: false };
if (SPATIAL.includes(this.editingType))
return { type: 'map', mask: false };
return { type: 'text', mask: false };
},
isImage () {
@@ -362,6 +409,21 @@ export default {
Object.keys(this.fields).forEach(field => {
this.isInlineEditor[field.name] = false;
});
},
isTextareaEditor (val) {
if (val) {
const modelOperations = new ModelOperations();
(async () => {
const detected = await modelOperations.runModel(this.editingContent);
const filteredLanguages = detected.filter(dLang =>
this.availableLanguages.some(aLang => aLang.id === dLang.languageId) &&
dLang.confidence > 0.1
);
if (filteredLanguages.length)
this.editorMode = this.availableLanguages.find(lang => lang.id === filteredLanguages[0].languageId).slug;
})();
}
}
},
methods: {
@@ -383,7 +445,7 @@ export default {
return bufferToBase64(val);
},
editON (event, content, field) {
if (!this.isEditable) return;
if (!this.isEditable || this.editingType === 'none') return;
window.addEventListener('keydown', this.onKey);
@@ -399,6 +461,15 @@ export default {
return;
}
if (SPATIAL.includes(type)) {
if (content) {
this.isMultiSpatial = IS_MULTI_SPATIAL.includes(type);
this.isMapModal = true;
this.editingContent = this.$options.filters.typeFormat(content, type);
}
return;
}
if (BLOB.includes(type)) {
this.isBlobEditor = true;
this.editingContent = content || '';
@@ -470,6 +541,8 @@ export default {
hideEditorModal () {
this.isTextareaEditor = false;
this.isBlobEditor = false;
this.isMapModal = false;
this.isMultiSpatial = false;
},
downloadFile () {
const downloadLink = document.createElement('a');
@@ -506,7 +579,7 @@ export default {
return this.keyUsage.find(key => key.field === keyName);
},
openContext (event, payload) {
payload.field = this.fields[payload.field].name;// Ensures field name only
payload.field = this.fields[payload.orgField].name;// Ensures field name only
payload.isEditable = this.isEditable;
this.$emit('contextmenu', event, payload);
},

View File

@@ -85,7 +85,7 @@
@click="showFakerModal"
>
<i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span>{{ $t('message.tableFiller') }}</span>
<span>{{ $tc('message.insertRow', 2) }}</span>
</button>
<div class="dropdown table-dropdown pr-2">

View File

@@ -124,7 +124,9 @@ module.exports = {
filter: 'Filter',
disabled: 'Disabled',
enable: 'Enable',
disable: 'Disable'
disable: 'Disable',
commit: 'Commit',
rollback: 'Rollback'
},
message: {
appWelcome: 'Welcome to Antares SQL Client!',
@@ -251,7 +253,13 @@ module.exports = {
killProcess: 'Kill process',
closeTab: 'Close tab',
goToDownloadPage: 'Go to download page',
readOnlyMode: 'Read-only mode'
readOnlyMode: 'Read-only mode',
killQuery: 'Kill query',
insertRow: 'Insert row | Insert rows',
commitMode: 'Commit mode',
autoCommit: 'Auto commit',
manualCommit: 'Manual commit',
actionSuccessful: '{action} successful'
},
faker: {
address: 'Address',

View File

@@ -13,7 +13,8 @@ const i18n = new VueI18n({
'pt-BR': require('./pt-BR'),
'de-DE': require('./de-DE'),
'vi-VN': require('./vi-VN'),
'ja-JP': require('./ja-JP')
'ja-JP': require('./ja-JP'),
'zh-CN': require('./zh-CN')
}
});
export default i18n;

View File

@@ -7,5 +7,6 @@ export default {
'pt-BR': 'Português (Brasil)',
'de-DE': 'Deutsch (Deutschland)',
'vi-VN': 'Tiếng Việt',
'ja-JP': '日本語'
'ja-JP': '日本語',
'zh-CN': '简体中文'
};

View File

@@ -120,7 +120,11 @@ module.exports = {
new: 'Mới',
history: 'Lịch sử',
select: 'Chọn',
passphrase: 'Cụm mật khẩu'
passphrase: 'Cụm mật khẩu',
filter: 'Bộ lọc',
disabled: 'Đã tắt',
enable: 'Bật',
disable: 'Tắt'
},
message: {
appWelcome: 'Chào bạn đến với Antares SQL Client!',
@@ -244,7 +248,10 @@ module.exports = {
newTriggerFunction: 'Chức năng kích hoạt mới',
thereIsNoQueriesYet: 'Không có truy vấn nào',
searchForQueries: 'Tìm kiếm truy vấn',
killProcess: 'Huỷ quá trình'
killProcess: 'Huỷ quá trình',
closeTab: 'Đóng tab',
goToDownloadPage: 'Tới trang tải về',
readOnlyMode: 'Chế độ chỉ đọc'
},
faker: {
address: 'Địa chỉ',

421
src/renderer/i18n/zh-CN.js Normal file
View File

@@ -0,0 +1,421 @@
module.exports = {
word: {
edit: '编辑',
save: '保存',
close: '关闭',
delete: '删除',
confirm: '确定',
cancel: '取消',
send: '发送',
connectionName: '连接名称',
client: 'Client',
hostName: '主机名',
port: '端口',
user: '用户',
password: '密码',
credentials: '凭据',
connect: '连接',
connected: '已连接',
disconnect: '断开连接',
disconnected: '已断开',
refresh: '刷新',
settings: '设置',
general: '一般',
themes: '主题',
update: '更新',
about: '关于',
language: '语言',
version: '版本',
donate: '捐赠',
run: '运行',
schema: 'schema',
results: '结果',
size: '尺寸',
seconds: '秒',
type: '类型',
mimeType: 'MIME类型',
download: '下载',
add: '新增',
data: '数据',
properties: '属性',
insert: '插入',
connecting: '连接中',
name: '名称',
collation: '排序规则',
clear: '清除',
options: '选项',
autoRefresh: '自动刷新',
indexes: '索引',
foreignKeys: '外键',
length: '长度',
unsigned: '无符号',
default: '默认',
comment: '注释',
key: '键',
order: 'Order',
expression: '表达式',
autoIncrement: '自动增量',
engine: 'Engine',
field: '字段',
approximately: '大约',
total: '总计',
table: '表',
discard: '弃置',
stay: '等待',
author: '作者',
light: 'Light',
dark: 'Dark',
autoCompletion: '自动完成',
application: '应用程序',
editor: '编辑器',
view: '视图',
definer: '定义者',
algorithm: 'Algorithm',
trigger: '触发器',
storedRoutine: '存储例程',
scheduler: '调度器',
event: '事件',
parameters: '参数',
function: '函数',
deterministic: 'Deterministic',
context: '上下文',
export: '导出',
returns: '返回',
timing: '定时器',
state: '状态',
execution: '执行',
starts: '开始',
ends: '结束',
ssl: 'SSL',
privateKey: '私钥',
certificate: '证书',
caCertificate: 'CA 证书',
ciphers: 'Ciphers',
upload: '上传',
browse: '浏览',
faker: 'Faker',
content: '内容',
cut: '剪切',
copy: '复制',
paste: '粘贴',
tools: '工具',
variables: '变量',
processes: '进程',
database: '数据库',
scratchpad: 'Scratchpad',
array: '数组',
changelog: '更改日志',
format: '格式',
sshTunnel: 'SSH 隧道',
structure: '结构',
small: '小',
medium: '中',
large: '大',
row: '行',
cell: '单元格',
triggerFunction: '触发函数',
all: '全部',
duplicate: '重复',
routine: '例程',
new: 'New',
history: '历史记录',
select: '选择',
passphrase: '密码',
filter: '过滤器',
disabled: '禁用',
enable: '启用',
disable: '是否禁用'
},
message: {
appWelcome: '欢迎来到Antares SQL Client!',
appFirstStep: '你的第一步: 创建一个新的数据库连接.',
addConnection: '添加连接',
createConnection: '创建连接',
createNewConnection: '创建新的连接',
askCredentials: '询问凭证',
testConnection: '测试连接',
editConnection: '编辑连接',
deleteConnection: '删除连接',
deleteCorfirm: '您是否确认取消',
connectionSuccessfullyMade: '连接成功建立!',
madeWithJS: '用💛和JavaScript制造!',
checkForUpdates: '检查更新',
noUpdatesAvailable: '没有可用的更新',
checkingForUpdate: '正在检查更新',
checkFailure: '检查失败,请稍后再试',
updateAvailable: '可用的更新',
downloadingUpdate: '正在下载更新',
updateDownloaded: '更新已下载',
restartToInstall: '重启Antares完成更新',
unableEditFieldWithoutPrimary: '无法编辑一个在结果集中没有主键的字段',
editCell: '编辑单元格',
deleteRows: '删除行 | 删除{count}行',
confirmToDeleteRows: '你是否确认要删除一行? | 您是否确认要删除{count}行?',
notificationsTimeout: '通知超时',
uploadFile: '上传文件',
addNewRow: '添加新行',
numberOfInserts: '插入的数量',
openNewTab: '打开一个新标签',
affectedRows: '受影响的行',
createNewDatabase: '创建新的数据库',
databaseName: '数据库名称',
serverDefault: '默认服务器',
deleteDatabase: '删除数据库',
editDatabase: '编辑数据库',
clearChanges: '清除变化',
addNewField: '添加新字段',
manageIndexes: '管理索引',
manageForeignKeys: '管理外键',
allowNull: '允许NULL',
zeroFill: '填充零',
customValue: '自定义值',
onUpdate: '在更新时',
deleteField: '删除字段',
createNewIndex: '创建新的索引',
addToIndex: '添加到索引',
createNewTable: '创建新表',
emptyTable: '清空表',
deleteTable: '删除表',
emptyCorfirm: '你是否确认清空',
unsavedChanges: '未保存的更改',
discardUnsavedChanges: '你有一些未保存的修改。关闭这个标签,这些变化将被丢弃.',
thereAreNoIndexes: '没有索引',
thereAreNoForeign: '没有外键',
createNewForeign: '创建新的外键',
referenceTable: '参考表',
referenceField: '参考字段',
foreignFields: '外键字段',
invalidDefault: '无效的默认值',
onDelete: '在删除时',
applicationTheme: '应用主题',
editorTheme: '编辑器主题',
wrapLongLines: '超出换行显示',
selectStatement: '选择语句',
triggerStatement: '触发器语句',
sqlSecurity: 'SQL安全',
updateOption: '更新选项',
deleteView: '删除视图',
createNewView: '创建新视图',
deleteTrigger: '删除触发器',
createNewTrigger: '创建新的触发器',
currentUser: '当前用户',
routineBody: '例程主体',
dataAccess: '数据访问',
thereAreNoParameters: '没有参数',
createNewParameter: '创建新参数',
createNewRoutine: '创建新的例程',
deleteRoutine: '删除例程',
functionBody: '函数体',
createNewFunction: '创建新函数',
deleteFunction: '删除函数',
schedulerBody: '调度器主体',
createNewScheduler: '创建新的调度器',
deleteScheduler: '删除调度器',
preserveOnCompletion: '完成时保存',
enableSsl: '启用SSL',
manualValue: '手动值',
tableFiller: '表填充器',
fakeDataLanguage: '伪造的数据语言',
searchForElements: '搜索元素',
selectAll: '选择所有',
queryDuration: '查询时间',
includeBetaUpdates: '包括测试版更新',
setNull: '设置NULL',
processesList: '进程列表',
processInfo: '进程信息',
manageUsers: '管理用户',
createNewSchema: '创建新模式',
schemaName: '模式名称',
editSchema: '编辑模式',
deleteSchema: '删除模式',
markdownSupported: '支持Markdown',
plantATree: '种植一棵树',
dataTabPageSize: '数据标签的页面大小',
enableSsh: '启用SSH',
pageNumber: '页数',
duplicateTable: '重复的表格',
noOpenTabs: '没有打开的标签,在左栏导航或:',
noSchema: '没有模式',
restorePreviourSession: '恢复以前的会话',
runQuery: '运行查询',
thereAreNoTableFields: '没有表的字段',
newTable: '新表',
newView: '新视图',
newTrigger: '新触发器',
newRoutine: '新例程',
newFunction: '新函数',
newScheduler: '新调度器',
newTriggerFunction: '新触发函数',
thereIsNoQueriesYet: '还没有查询',
searchForQueries: '搜索查询',
killProcess: '杀死进程',
closeTab: '关闭标签',
goToDownloadPage: '跳转到下载页面',
readOnlyMode: '只读模式',
killQuery: '停止查询'
},
faker: {
address: '地址',
commerce: '商业',
company: '公司',
database: '数据库',
date: '日期',
finance: '财务',
git: 'Git',
hacker: '黑客',
internet: '互联网',
lorem: 'Lorem',
name: '姓名',
music: '音乐',
phone: '电话',
random: '随机',
system: '系统',
time: '时间',
vehicle: '车辆',
zipCode: '邮政编码',
zipCodeByState: '按州的邮编',
city: '城市',
cityPrefix: '城市前缀',
citySuffix: '城市后缀',
streetName: '街道名称',
streetAddress: '街道地址',
streetSuffix: '街道前缀',
streetPrefix: '街道后缀',
secondaryAddress: '次要地址',
county: '县',
country: '国家',
countryCode: '国家代码',
state: '州',
stateAbbr: '州的缩写',
latitude: '纬度',
longitude: '经度',
direction: '方向',
cardinalDirection: 'Cardinal direction',
ordinalDirection: 'Ordinal direction',
nearbyGPSCoordinate: '附近的GPS坐标',
timeZone: '时区',
color: '颜色',
department: '部门',
productName: '产品名称',
price: '价格',
productAdjective: '产品形容词',
productMaterial: '产品材料',
product: '产品',
productDescription: '产品描述',
suffixes: '后缀',
companyName: '公司名称',
companySuffix: '公司后缀',
catchPhrase: 'Catch phrase',
bs: 'BS',
catchPhraseAdjective: 'Catch phrase adjective',
catchPhraseDescriptor: 'Catch phrase descriptor',
catchPhraseNoun: 'Catch phrase noun',
bsAdjective: 'BS adjective',
bsBuzz: 'BS buzz',
bsNoun: 'BS noun',
column: '列',
type: '类型',
collation: '校对',
engine: 'Engine',
past: '过去',
future: '未来',
between: '之间',
recent: '最近',
soon: '很快',
month: '月',
weekday: '工作日',
account: '账户',
accountName: '账户名称',
routingNumber: '路由号码',
mask: '掩码',
amount: '金额',
transactionType: '交易类型',
currencyCode: '货币代码',
currencyName: '货币名称',
currencySymbol: '货币符号',
bitcoinAddress: '比特币地址',
litecoinAddress: '莱特币地址',
creditCardNumber: '信用卡号码',
creditCardCVV: '信用卡CVV',
ethereumAddress: '以太坊地址',
iban: 'Iban',
bic: 'Bic',
transactionDescription: '交易描述',
branch: '分支',
commitEntry: '提交条目',
commitMessage: '提交信息',
commitSha: '提交 SHA',
shortSha: 'Short SHA',
abbreviation: '缩写',
adjective: '形容词',
noun: '名词',
verb: '动词',
ingverb: 'Ingverb',
phrase: '短语',
avatar: '头像',
email: '电子邮箱',
exampleEmail: '电子邮件例子',
userName: '用户名',
protocol: '协议',
url: 'Url',
domainName: 'Domin name',
domainSuffix: '域名后缀',
domainWord: 'Domain word',
ip: 'Ip',
ipv6: 'Ipv6',
userAgent: 'User agent',
mac: 'Mac',
password: '密码',
word: 'Word',
words: 'Words',
sentence: '句子',
slug: 'Slug',
sentences: '句子',
paragraph: '段落',
paragraphs: '段落',
text: '文本',
lines: '行',
genre: 'Genre',
firstName: '名',
lastName: '姓氏',
middleName: '中间名',
findName: '全名',
jobTitle: '职位名称',
gender: '性别',
prefix: '前缀',
suffix: '后缀',
title: '标题',
jobDescriptor: '工作描述',
jobArea: '工作领域',
jobType: '工作类型',
phoneNumber: '电话号码',
phoneNumberFormat: '电话号码格式',
phoneFormats: '电话格式',
number: 'Number',
float: 'Float',
arrayElement: '数组元素',
arrayElements: '数组元素',
objectElement: '对象元素',
uuid: 'Uuid',
boolean: 'Boolean',
image: 'Image',
locale: 'Locale',
alpha: 'Alpha',
alphaNumeric: 'Alphanumeric',
hexaDecimal: 'Hexadecimal',
fileName: '文件名',
commonFileName: '普通文件名',
mimeType: 'MIME类型',
commonFileType: '常见的文件类型',
commonFileExt: '常见的文件扩展名',
fileType: '文件类型',
fileExt: '文件扩展名',
directoryPath: '目录路径',
filePath: '文件路径',
semver: 'Semver',
manufacturer: '制造商',
model: '型号',
fuel: 'Fuel',
vin: 'Vin'
}
};

View File

@@ -3,6 +3,7 @@
import Vue from 'vue';
import '@mdi/font/css/materialdesignicons.css';
import 'leaflet/dist/leaflet.css';
import '@/scss/main.scss';
import App from '@/App.vue';

View File

@@ -46,6 +46,22 @@ export default class {
return ipcRenderer.invoke('kill-process', params);
}
static killTabQuery (params) {
return ipcRenderer.invoke('kill-tab-query', params);
}
static commitTab (params) {
return ipcRenderer.invoke('commit-tab', params);
}
static rollbackTab (params) {
return ipcRenderer.invoke('rollback-tab', params);
}
static destroyConnectionToCommit (params) {
return ipcRenderer.invoke('destroy-connection-to-commit', params);
}
static useSchema (params) {
return ipcRenderer.invoke('use-schema', params);
}

View File

@@ -81,6 +81,15 @@
"tsvector": $array-color,
"tsquery": $array-color,
"pg_node_tree": $array-color,
"point": $array-color,
"linestring": $array-color,
"polygon": $array-color,
"geometry": $array-color,
"multipoint": $array-color,
"multilinestring": $array-color,
"multipolygon": $array-color,
"geomcollection": $array-color,
"geometrycollection": $array-color,
"aclitem": $array-color,
"unknown": $unknown-color,
)

View File

@@ -20,7 +20,7 @@
background-image: url("../images/svg/pg.svg");
}
&.dbi-sqlite {
&.dbi-sqlite {
background-image: url("../images/svg/sqlite.svg");
}

View File

@@ -59,6 +59,34 @@ option:checked {
text-overflow: ellipsis;
}
.cancellable {
color: transparent !important;
min-height: 0.8rem;
position: relative;
> .mdi,
> .span {
visibility: hidden;
}
&::after {
content: "\2715";
color: $light-color;
font-weight: 700;
top: 36%;
display: block;
height: 0.8rem;
left: 50%;
margin-left: -0.4rem;
margin-top: -0.4rem;
opacity: 1;
padding: 0;
position: absolute;
width: 0.8rem;
z-index: 1;
}
}
.workspace-tabs {
align-content: baseline;

49
tests/app.spec.js Normal file
View File

@@ -0,0 +1,49 @@
const { _electron: electron } = require('playwright');
const { strict: assert } = require('assert');
(async () => {
console.log('Starting tests');
// Launch Electron app.
const electronApp = await electron.launch({ args: ['dist/main.js'] });
/**
* App main window state
* @type {{isVisible: boolean; isDevToolsOpened: boolean; isCrashed: boolean}}
*/
const windowState = await electronApp.evaluate(({ BrowserWindow }) => {
const mainWindow = BrowserWindow.getAllWindows()[0];
const getState = () => ({
isVisible: mainWindow.isVisible(),
isDevToolsOpened: mainWindow.webContents.isDevToolsOpened()
});
return new Promise((resolve) => {
if (mainWindow.isVisible())
resolve(getState());
else
mainWindow.once('ready-to-show', () => setTimeout(() => resolve(getState()), 0));
});
});
// Check main window state
assert.ok(windowState.isVisible, 'Main window not visible');
assert.ok(!windowState.isDevToolsOpened, 'DevTools opened');
assert.ok(!windowState.isCrashed, 'Window crashed');
/**
* Rendered Main window web-page
* @type {Page}
*/
const page = await electronApp.firstWindow();
console.log(await page.title());
// Check web-page content
const element = await page.$('#wrapper', { strict: true });
assert.notStrictEqual(element, null, 'Can\'t find root element');
assert.notStrictEqual((await element.innerHTML()).trim(), '', 'Window content is empty');
// Close app
await electronApp.close();
console.log('Tests finished');
})();