Merge branch 'master' of https://github.com/Fabio286/antares into pr/toriphes/129

This commit is contained in:
Fabio Di Stasio 2022-02-16 09:14:46 +01:00
parent d25c62b4da
commit 328ab61757
102 changed files with 3499 additions and 563 deletions

View File

@ -1,5 +1,5 @@
{ {
"projectName": "Antares", "projectName": "antares",
"projectOwner": "Fabio286", "projectOwner": "Fabio286",
"repoType": "github", "repoType": "github",
"repoHost": "https://github.com", "repoHost": "https://github.com",
@ -111,7 +111,35 @@
"contributions": [ "contributions": [
"platform" "platform"
] ]
},
{
"login": "kilianstallz",
"name": "Kilian Stallinger",
"avatar_url": "https://avatars.githubusercontent.com/u/5290318?v=4",
"profile": "https://kilianstallinger.com",
"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 "contributorsPerLine": 7,
"skipCi": true
} }

View File

@ -1,4 +1,4 @@
/node_modules node_modules
/assets/vendor assets
/out out
/dist dist

View File

@ -45,6 +45,7 @@
"no-console": "off", "no-console": "off",
"no-undef": "off", "no-undef": "off",
"vue/no-side-effects-in-computed-properties": "off", "vue/no-side-effects-in-computed-properties": "off",
"vue/multi-word-component-names": "off",
"vue/require-default-prop": "off", "vue/require-default-prop": "off",
"vue/comment-directive": "off", "vue/comment-directive": "off",
"vue/no-v-html": "off", "vue/no-v-html": "off",
@ -61,10 +62,11 @@
"vue/max-attributes-per-line": [ "vue/max-attributes-per-line": [
"error", "error",
{ {
"singleline": 2, "singleline": {
"max": 2
},
"multiline": { "multiline": {
"max": 1, "max": 1
"allowFirstLine": false
} }
} }
] ]

View File

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

View File

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

View File

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

View File

@ -3,7 +3,9 @@
"UI", "UI",
"core", "core",
"MySQL", "MySQL",
"PostgreSQL" "PostgreSQL",
"SQLite",
"Windows"
], ],
"svg.preview.background": "transparent" "svg.preview.background": "transparent"
} }

View File

@ -2,6 +2,128 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.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)
### Features
* **MySQL:** read-only mode ([4437d44](https://github.com/Fabio286/antares/commit/4437d44486c4f20b0bec4bf89d56016b08e36e79))
* **PostgreSQL:** read-only mode ([5d48fe0](https://github.com/Fabio286/antares/commit/5d48fe08c77755ed18b3f7a9ea834268e317e7ef))
* **SQLite:** cell update in data tabs ([604b371](https://github.com/Fabio286/antares/commit/604b3719204f7473ce4846624f08f8be9eec8b8f))
* **SQLite:** connection add/edit masks ([c54438d](https://github.com/Fabio286/antares/commit/c54438d6d3bad38bc76dfcd61f58929fe30279cb))
* **SQLite:** keys support ([fd321be](https://github.com/Fabio286/antares/commit/fd321beece075d3ad23fdd8541f9beb5727045a5))
* **SQLite:** readonly mode ([3fc227d](https://github.com/Fabio286/antares/commit/3fc227d2de53aae115226ad3c965bfb6e9f3eca6))
* **SQLite:** table data visualization ([f2fcc98](https://github.com/Fabio286/antares/commit/f2fcc9883972402eab4d51ef2a9796638dde2d3d))
* **SQLite:** tables management ([3efeb45](https://github.com/Fabio286/antares/commit/3efeb45c460f178b794de72367f8d542fd8ddd56))
* **SQLite:** triggers management ([f40e9c5](https://github.com/Fabio286/antares/commit/f40e9c592eeffd204aba21a0a0767a0c523fca49))
* **SQLite:** views management ([7671c58](https://github.com/Fabio286/antares/commit/7671c585f5f8049bd863db190d4fc60d8f0c6c66))
### Bug Fixes
* **SQLite:** hide schema creation ([98165ca](https://github.com/Fabio286/antares/commit/98165cacaa158c85ead0490d3caf579e2a17319f))
* **UI:** hide tools menu if no tools available ([da1947e](https://github.com/Fabio286/antares/commit/da1947e4efa7f0a26d6a231fadf750be055fbdd5))
* **UI:** notifications timeout anomalies ([cc99491](https://github.com/Fabio286/antares/commit/cc99491fe4a15812368f6c928b8c7801d7b255aa))
### Improvements
* **SQLite:** improvements in data visualization ([94c899e](https://github.com/Fabio286/antares/commit/94c899eb8288b41a5962ac3d24365227e1f9f485))
* **SQLite:** improvements in field length detection ([93b4a70](https://github.com/Fabio286/antares/commit/93b4a7063beeb5a7001cb06a74f05b23105212f5))
* update italian traslation ([9fe3680](https://github.com/Fabio286/antares/commit/9fe3680bbb17c192cffa85348e68794ab49beb81))
### [0.3.9](https://github.com/Fabio286/antares/compare/v0.3.8...v0.3.9) (2021-11-14)
### Features
* added macos basic shortcusts and menu ([430490a](https://github.com/Fabio286/antares/commit/430490ad93f3148962ced1f13a5330c79cd86b3b))
* **MySQL:** enable/disable schedulers from contextual menu ([5ca3a22](https://github.com/Fabio286/antares/commit/5ca3a22dc538b27a4bf6402f1288c4b9f5bc5a90))
* **MySQL:** scheduler status indicator in explore bar ([5c66824](https://github.com/Fabio286/antares/commit/5c668249cf102cd9d601f9f7b4943c7155775217))
* **PostgreSQL:** enable/disable triggers from contextual menu ([534659f](https://github.com/Fabio286/antares/commit/534659f9aee12eb5ac477f91bfe5d764387dc17e))
* schema size in explore bar ([fd25f88](https://github.com/Fabio286/antares/commit/fd25f881f95779709156cbad93a41d6b391f1a45))
* **UI:** double click on the title bar will toggle window fullscreen size ([a35566f](https://github.com/Fabio286/antares/commit/a35566f273322602abe434b8bd30817ba8885900))
* **UI:** improved topbar look&feel on MacOS ([7657d05](https://github.com/Fabio286/antares/commit/7657d05edfbeaed6a14eb337fc562da5126e6ba0))
### Bug Fixes
* copy&paste and basic usability on macOS ([1ddf8f0](https://github.com/Fabio286/antares/commit/1ddf8f0dbe22f94d6bffddf70636706d2d142ecf))
* **PostgreSQL:** bigint fetched as string instead of number, closes [#134](https://github.com/Fabio286/antares/issues/134) ([39b9a59](https://github.com/Fabio286/antares/commit/39b9a59143b457a96f0711a3b8588c92dd80e28d))
* row selection problems after a deletion fail, closes [#128](https://github.com/Fabio286/antares/issues/128) ([89fdd21](https://github.com/Fabio286/antares/commit/89fdd210ca48fc9ae399b195ea796c8523619627))
* temporary solution on MacOS for unsigned app updates ([c00fd13](https://github.com/Fabio286/antares/commit/c00fd1381f451ba7aace7047b28b904ddcaf18f0))
### Improvements
* **UI:** improved function and routine parameters modals ([d19f475](https://github.com/Fabio286/antares/commit/d19f475fc28c0367ada569cb634769fa618b48b4))
### [0.3.8](https://github.com/Fabio286/antares/compare/v0.3.7...v0.3.8) (2021-10-23) ### [0.3.8](https://github.com/Fabio286/antares/compare/v0.3.7...v0.3.8) (2021-10-23)

View File

@ -12,7 +12,7 @@
Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers. Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers.
Our target is to support as many databases as possible, and all major operating systems, including the ARM versions. Our target is to support as many databases as possible, and all major operating systems, including the ARM versions.
**At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB and PostgreSQL. **At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB, PostgreSQL and SQLite.
At the moment, however, there are all the features necessary to have a pleasant database management experience, so give it a chance and send us your feedback, we would really appreciate it. At the moment, however, there are all the features necessary to have a pleasant database management experience, so give it a chance and send us your feedback, we would really appreciate it.
We are actively working on it, hoping to provide new cool features, improvements and fixes as soon as possible. We are actively working on it, hoping to provide new cool features, improvements and fixes as soon as possible.
@ -31,10 +31,9 @@ We are actively working on it, hoping to provide new cool features, improvements
- Query suggestions and auto complete. - Query suggestions and auto complete.
- Query history: search through the last 1000 queries. - Query history: search through the last 1000 queries.
- SSH tunnel support. - SSH tunnel support.
- Manual commit mode.
- Dark and light theme. - Dark and light theme.
- Editor themes. - Editor themes.
- Scratchpad.
- Secure password storage.
## Philosophy ## Philosophy
@ -68,12 +67,12 @@ On macOS you can run `.dmg` distribution following [this guide](https://support.
This is a roadmap with major features will come in near future. This is a roadmap with major features will come in near future.
- Support for other databases.
- Database tools. - Database tools.
- Users management (add/edit/delete). - Users management (add/edit/delete).
- More context menu shortcuts. - More context menu shortcuts.
- More keyboard shortcuts. - More keyboard shortcuts.
- Import/export and migration. - Import/export and migration.
- Support for other databases.
- Apple Silicon distribution - Apple Silicon distribution
## Currently supported ## Currently supported
@ -82,7 +81,7 @@ This is a roadmap with major features will come in near future.
- [x] MySQL/MariaDB - [x] MySQL/MariaDB
- [x] PostgreSQL - [x] PostgreSQL
- [ ] SQLite - [x] SQLite
- [ ] MSSQL - [ ] MSSQL
- [ ] OracleDB - [ ] OracleDB
- [ ] More... - [ ] More...
@ -116,9 +115,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- markdownlint-disable --> <!-- markdownlint-disable -->
<table> <table>
<tr> <tr>
<td align="center"><a href="https://fabiodistasio.it/"><img src="https://avatars.githubusercontent.com/u/31471771?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Fabio Di Stasio</b></sub></a><br /><a href="https://github.com/Fabio286/Antares/commits?author=Fabio286" title="Code">💻</a> <a href="#translation-Fabio286" title="Translation">🌍</a> <a href="https://github.com/Fabio286/Antares/commits?author=Fabio286" title="Documentation">📖</a></td> <td align="center"><a href="https://fabiodistasio.it/"><img src="https://avatars.githubusercontent.com/u/31471771?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Fabio Di Stasio</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=Fabio286" title="Code">💻</a> <a href="#translation-Fabio286" title="Translation">🌍</a> <a href="https://github.com/Fabio286/antares/commits?author=Fabio286" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/giulioganci/"><img src="https://avatars.githubusercontent.com/u/4192159?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Giulio Ganci</b></sub></a><br /><a href="https://github.com/Fabio286/Antares/commits?author=toriphes" title="Code">💻</a></td> <td align="center"><a href="https://www.linkedin.com/in/giulioganci/"><img src="https://avatars.githubusercontent.com/u/4192159?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Giulio Ganci</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=toriphes" title="Code">💻</a></td>
<td align="center"><a href="https://christianratz.de/"><img src="https://avatars.githubusercontent.com/u/2630316?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Christian Ratz</b></sub></a><br /><a href="https://github.com/Fabio286/Antares/commits?author=digitalgopnik" title="Code">💻</a> <a href="#translation-digitalgopnik" title="Translation">🌍</a></td> <td align="center"><a href="https://christianratz.de/"><img src="https://avatars.githubusercontent.com/u/2630316?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Christian Ratz</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=digitalgopnik" title="Code">💻</a> <a href="#translation-digitalgopnik" title="Translation">🌍</a></td>
<td align="center"><a href="https://reverb6821.github.io/"><img src="https://avatars.githubusercontent.com/u/55198803?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Giuseppe Gigliotti</b></sub></a><br /><a href="#translation-reverb6821" title="Translation">🌍</a></td> <td align="center"><a href="https://reverb6821.github.io/"><img src="https://avatars.githubusercontent.com/u/55198803?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Giuseppe Gigliotti</b></sub></a><br /><a href="#translation-reverb6821" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/Mohd-PH"><img src="https://avatars.githubusercontent.com/u/9362157?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mohd-PH</b></sub></a><br /><a href="#translation-Mohd-PH" title="Translation">🌍</a></td> <td align="center"><a href="https://github.com/Mohd-PH"><img src="https://avatars.githubusercontent.com/u/9362157?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mohd-PH</b></sub></a><br /><a href="#translation-Mohd-PH" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/hongkfui"><img src="https://avatars.githubusercontent.com/u/37477191?v=4?s=100" width="100px;" alt=""/><br /><sub><b>hongkfui</b></sub></a><br /><a href="#translation-hongkfui" title="Translation">🌍</a></td> <td align="center"><a href="https://github.com/hongkfui"><img src="https://avatars.githubusercontent.com/u/37477191?v=4?s=100" width="100px;" alt=""/><br /><sub><b>hongkfui</b></sub></a><br /><a href="#translation-hongkfui" title="Translation">🌍</a></td>
@ -129,6 +128,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://ngoquocdat.com/"><img src="https://avatars.githubusercontent.com/u/56961917?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ngô Quốc Đạt</b></sub></a><br /><a href="#translation-datlechin" title="Translation">🌍</a></td> <td align="center"><a href="https://ngoquocdat.com/"><img src="https://avatars.githubusercontent.com/u/56961917?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ngô Quốc Đạt</b></sub></a><br /><a href="#translation-datlechin" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/IsamuSugi"><img src="https://avatars.githubusercontent.com/u/7746658?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Isamu Sugiura</b></sub></a><br /><a href="#translation-IsamuSugi" title="Translation">🌍</a></td> <td align="center"><a href="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="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> </tr>
</table> </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,8 +1,8 @@
{ {
"name": "antares", "name": "antares",
"productName": "Antares", "productName": "Antares",
"version": "0.3.8", "version": "0.4.3",
"description": "A cross-platform easy to use SQL client.", "description": "A modern, fast and productivity driven SQL client with a focus in UX.",
"license": "MIT", "license": "MIT",
"repository": "https://github.com/Fabio286/antares.git", "repository": "https://github.com/Fabio286/antares.git",
"scripts": { "scripts": {
@ -14,11 +14,11 @@
"build": "cross-env NODE_ENV=production npm run compile", "build": "cross-env NODE_ENV=production npm run compile",
"build:local": "npm run build && electron-builder", "build:local": "npm run build && electron-builder",
"build:appx": "npm run build:local -- --win appx", "build:appx": "npm run build:local -- --win appx",
"rebuild:electron": "npm run postinstall && electron-rebuild", "rebuild:electron": "npm run postinstall",
"release": "standard-version", "release": "standard-version",
"release:pre": "npm run release -- --prerelease alpha", "release:pre": "npm run release -- --prerelease alpha",
"postinstall": "electron-builder install-app-deps", "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": "eslint . --ext .js,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"",
"lint:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix", "lint:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix",
"contributors:add": "all-contributors add", "contributors:add": "all-contributors add",
@ -30,6 +30,7 @@
"appId": "com.fabio286.antares", "appId": "com.fabio286.antares",
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}", "artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
"asar": true, "asar": true,
"buildDependenciesFromSource": true,
"directories": { "directories": {
"output": "build", "output": "build",
"buildResources": "assets" "buildResources": "assets"
@ -50,7 +51,8 @@
"target": { "target": {
"target": "default", "target": "default",
"arch": [ "arch": [
"x64" "x64",
"arm64"
] ]
} }
}, },
@ -79,7 +81,9 @@
"artifactName": "${productName}-${version}-portable.exe" "artifactName": "${productName}-${version}-portable.exe"
}, },
"appx": { "appx": {
"displayName": "Antares SQL Client", "displayName": "Antares SQL",
"backgroundColor": "transparent",
"showNameOnTiles": true,
"identityName": "62514FabioDiStasio.AntaresSQLClient", "identityName": "62514FabioDiStasio.AntaresSQLClient",
"publisher": "CN=1A2729ED-865C-41D2-9038-39AE2A63AA52", "publisher": "CN=1A2729ED-865C-41D2-9038-39AE2A63AA52",
"applicationId": "FabioDiStasio.AntaresSQLClient" "applicationId": "FabioDiStasio.AntaresSQLClient"
@ -102,12 +106,17 @@
"dependencies": { "dependencies": {
"@electron/remote": "^2.0.1", "@electron/remote": "^2.0.1",
"@mdi/font": "^6.1.95", "@mdi/font": "^6.1.95",
"@turf/helpers": "^6.5.0",
"@vscode/vscode-languagedetection": "^1.0.21",
"ace-builds": "^1.4.13", "ace-builds": "^1.4.13",
"better-sqlite3": "^7.4.4",
"electron-log": "^4.4.1", "electron-log": "^4.4.1",
"electron-store": "^8.0.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", "faker": "^5.5.3",
"marked": "^3.0.4", "leaflet": "^1.7.1",
"marked": "^4.0.0",
"moment": "^2.29.1", "moment": "^2.29.1",
"mysql2": "^2.3.2", "mysql2": "^2.3.2",
"pg": "^8.7.1", "pg": "^8.7.1",
@ -130,23 +139,23 @@
"clean-webpack-plugin": "^4.0.0", "clean-webpack-plugin": "^4.0.0",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"css-loader": "^6.5.0", "css-loader": "^6.5.0",
"electron": "^15.3.0", "electron": "^17.0.1",
"electron-builder": "^22.13.1", "electron-builder": "^22.14.11",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-rebuild": "^3.2.3",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-standard": "^16.0.3", "eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.24.2", "eslint-plugin-import": "^2.24.2",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0", "eslint-plugin-promise": "^5.2.0",
"eslint-plugin-vue": "^7.18.0", "eslint-plugin-vue": "^8.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.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", "node-loader": "^2.0.0",
"playwright": "^1.18.1",
"progress-webpack-plugin": "^1.0.12", "progress-webpack-plugin": "^1.0.12",
"sass": "^1.42.1", "sass": "^1.42.1",
"sass-loader": "^10.2.0", "sass-loader": "^12.3.0",
"standard-version": "^9.3.1", "standard-version": "^9.3.1",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"stylelint": "^13.13.1", "stylelint": "^13.13.1",
@ -158,6 +167,6 @@
"vue-template-compiler": "^2.6.14", "vue-template-compiler": "^2.6.14",
"webpack": "^5.60.0", "webpack": "^5.60.0",
"webpack-cli": "^4.9.1", "webpack-cli": "^4.9.1",
"webpack-dev-server": "^3.11.2" "webpack-dev-server": "^4.4.0"
} }
} }

View File

@ -113,14 +113,15 @@ function startRenderer (callback) {
}); });
const server = new WebpackDevServer(compiler, { const server = new WebpackDevServer(compiler, {
contentBase: path.join(__dirname, '../'),
hot: true, hot: true,
noInfo: true, port: 9080,
overlay: true, client: {
clientLogLevel: 'warning' overlay: true,
logging: 'warn'
}
}); });
server.listen(9080, '', err => { server.startCallback(err => {
if (err) console.error(chalk.red(err)); if (err) console.error(chalk.red(err));
callback(); callback();

View File

@ -134,7 +134,7 @@ export default class {
{ name: 'phoneNumberFormat', group: 'phone', types: ['string'] }, { name: 'phoneNumberFormat', group: 'phone', types: ['string'] },
{ name: 'phoneFormats', group: 'phone', types: ['string'] }, { name: 'phoneFormats', group: 'phone', types: ['string'] },
{ name: 'number', group: 'random', types: ['string', 'number'], params: ['min', 'max'] }, { name: 'number', group: 'datatype', types: ['string', 'number'], params: ['min', 'max'] },
{ name: 'float', group: 'random', types: ['string', 'float'], params: ['min', 'max'] }, { name: 'float', group: 'random', types: ['string', 'float'], params: ['min', 'max'] },
{ name: 'arrayElement', group: 'random', types: ['string'] }, { name: 'arrayElement', group: 'random', types: ['string'] },
{ name: 'arrayElements', group: 'random', types: ['string'] }, { name: 'arrayElements', group: 'random', types: ['string'] },

View File

@ -8,6 +8,10 @@ module.exports = {
collations: false, collations: false,
engines: false, engines: false,
connectionSchema: false, connectionSchema: false,
sslConnection: false,
sshConnection: false,
fileConnection: false,
cancelQueries: false,
// Tools // Tools
processesList: false, processesList: false,
usersManagement: false, usersManagement: false,
@ -33,7 +37,12 @@ module.exports = {
schedulerAdd: false, schedulerAdd: false,
databaseEdit: false, databaseEdit: false,
schemaEdit: false, schemaEdit: false,
schemaDrop: false,
schemaExport: false,
tableSettings: false, tableSettings: false,
tableOptions: false,
tableArray: false,
tableRealCount: false,
viewSettings: false, viewSettings: false,
triggerSettings: false, triggerSettings: false,
triggerFunctionSettings: false, triggerFunctionSettings: false,
@ -45,14 +54,13 @@ module.exports = {
sortableFields: false, sortableFields: false,
unsigned: false, unsigned: false,
nullable: false, nullable: false,
nullablePrimary: false,
zerofill: false, zerofill: false,
tableOptions: false,
autoIncrement: false, autoIncrement: false,
comment: false, comment: false,
collation: false, collation: false,
definer: false, definer: false,
onUpdate: false, onUpdate: false,
tableArray: false,
viewAlgorithm: false, viewAlgorithm: false,
viewSqlSecurity: false, viewSqlSecurity: false,
viewUpdateOption: false, viewUpdateOption: false,
@ -72,8 +80,10 @@ module.exports = {
triggerTableInName: false, triggerTableInName: false,
triggerUpdateColumns: false, triggerUpdateColumns: false,
triggerOnlyRename: false, triggerOnlyRename: false,
triggerEnableDisable: false,
triggerFunctionSql: false, triggerFunctionSql: false,
triggerFunctionlanguages: false, triggerFunctionlanguages: false,
parametersLength: false, parametersLength: false,
languages: false languages: false,
readOnlyMode: false
}; };

View File

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

View File

@ -10,6 +10,9 @@ module.exports = {
connectionSchema: true, connectionSchema: true,
collations: true, collations: true,
engines: true, engines: true,
sslConnection: true,
sshConnection: true,
cancelQueries: true,
// Tools // Tools
processesList: true, processesList: true,
// Structure // Structure
@ -30,6 +33,7 @@ module.exports = {
functionAdd: true, functionAdd: true,
schedulerAdd: true, schedulerAdd: true,
schemaEdit: true, schemaEdit: true,
schemaDrop: true,
schemaExport: true, schemaExport: true,
tableSettings: true, tableSettings: true,
viewSettings: true, viewSettings: true,
@ -60,5 +64,6 @@ module.exports = {
functionDeterministic: true, functionDeterministic: true,
functionDataAccess: true, functionDataAccess: true,
functionSql: 'BEGIN\r\n\r\nEND', functionSql: 'BEGIN\r\n\r\nEND',
parametersLength: true parametersLength: true,
readOnlyMode: true
}; };

View File

@ -8,9 +8,13 @@ module.exports = {
defaultDatabase: 'postgres', defaultDatabase: 'postgres',
// Core // Core
database: true, database: true,
sslConnection: true,
sshConnection: true,
cancelQueries: true,
// Tools // Tools
processesList: true, processesList: true,
// Structure // Structure
schemas: true,
tables: true, tables: true,
views: true, views: true,
triggers: true, triggers: true,
@ -26,6 +30,7 @@ module.exports = {
triggerFunctionAdd: true, triggerFunctionAdd: true,
routineAdd: true, routineAdd: true,
functionAdd: true, functionAdd: true,
schemaDrop: true,
databaseEdit: false, databaseEdit: false,
schemaExport: true, schemaExport: true,
tableSettings: true, tableSettings: true,
@ -51,5 +56,7 @@ module.exports = {
triggerMultipleEvents: true, triggerMultipleEvents: true,
triggerTableInName: true, triggerTableInName: true,
triggerOnlyRename: false, triggerOnlyRename: false,
languages: ['sql', 'plpgsql', 'c', 'internal'] triggerEnableDisable: true,
languages: ['sql', 'plpgsql', 'c', 'internal'],
readOnlyMode: true
}; };

View File

@ -0,0 +1,27 @@
module.exports = {
// Core
fileConnection: true,
// Structure
schemas: false,
tables: true,
views: true,
triggers: true,
// Settings
elementsWrapper: '"',
stringsWrapper: '\'',
tableAdd: true,
viewAdd: true,
triggerAdd: true,
schemaEdit: false,
tableSettings: true,
tableRealCount: true,
viewSettings: true,
triggerSettings: true,
indexes: true,
foreigns: true,
sortableFields: true,
nullable: true,
nullablePrimary: true,
triggerSql: 'BEGIN\r\n\r\nEND',
readOnlyMode: true
};

View File

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

View File

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

View File

@ -0,0 +1,137 @@
module.exports = [
{
group: 'integer',
types: [
{
name: 'INT',
length: true,
collation: false,
unsigned: true,
zerofill: true
},
{
name: 'INTEGER',
length: true,
collation: false,
unsigned: true,
zerofill: true
},
{
name: 'BIGINT',
length: true,
collation: false,
unsigned: true,
zerofill: true
},
{
name: 'NUMERIC',
length: true,
collation: false,
unsigned: true,
zerofill: true
},
{
name: 'BOOLEAN',
length: false,
collation: false,
unsigned: true,
zerofill: true
}
]
},
{
group: 'float',
types: [
{
name: 'FLOAT',
length: true,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'REAL',
length: true,
collation: false,
unsigned: false,
zerofill: false
}
]
},
{
group: 'string',
types: [
{
name: 'CHAR',
length: true,
collation: true,
unsigned: false,
zerofill: false
},
{
name: 'VARCHAR',
length: true,
collation: true,
unsigned: false,
zerofill: false
},
{
name: 'TEXT',
length: true,
collation: true,
unsigned: false,
zerofill: false
}
]
},
{
group: 'binary',
types: [
{
name: 'BLOB',
length: true,
collation: false,
unsigned: false,
zerofill: false
}
]
},
{
group: 'time',
types: [
{
name: 'DATE',
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'TIME',
length: true,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'DATETIME',
length: true,
collation: false,
unsigned: false,
zerofill: false
}
]
},
{
group: 'other',
types: [
{
name: 'NONE',
length: false,
collation: false,
unsigned: false,
zerofill: false
}
]
}
];

View File

@ -8,7 +8,9 @@ export const TEXT = [
export const LONG_TEXT = [ export const LONG_TEXT = [
'TEXT', 'TEXT',
'MEDIUMTEXT', 'MEDIUMTEXT',
'LONGTEXT' 'LONGTEXT',
'JSON',
'VARBINARY'
]; ];
export const ARRAY = [ export const ARRAY = [
@ -82,3 +84,24 @@ export const BIT = [
'BIT', 'BIT',
'BIT VARYING' '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,5 @@
module.exports = [
'PRIMARY',
'INDEX',
'UNIQUE'
];

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

@ -9,12 +9,16 @@ export default connections => {
port: +conn.port, port: +conn.port,
user: conn.user, user: conn.user,
password: conn.password, password: conn.password,
application_name: 'Antares SQL' application_name: 'Antares SQL',
readonly: conn.readonly
}; };
if (conn.database) if (conn.database)
params.database = conn.database; params.database = conn.database;
if (conn.databasePath)
params.databasePath = conn.databasePath;
if (conn.ssl) { if (conn.ssl) {
params.ssl = { params.ssl = {
key: conn.key ? fs.readFileSync(conn.key) : null, key: conn.key ? fs.readFileSync(conn.key) : null,
@ -48,7 +52,7 @@ export default connections => {
return { status: 'success' }; return { status: 'success' };
} }
catch (err) { catch (err) {
return { status: 'error', response: err }; return { status: 'error', response: err.toString() };
} }
}); });
@ -62,12 +66,16 @@ export default connections => {
port: +conn.port, port: +conn.port,
user: conn.user, user: conn.user,
password: conn.password, password: conn.password,
application_name: 'Antares SQL' application_name: 'Antares SQL',
readonly: conn.readonly
}; };
if (conn.database) if (conn.database)
params.database = conn.database; params.database = conn.database;
if (conn.databasePath)
params.databasePath = conn.databasePath;
if (conn.schema) if (conn.schema)
params.schema = conn.schema; params.schema = conn.schema;

View File

@ -40,4 +40,17 @@ export default (connections) => {
return { status: 'error', response: err.toString() }; return { status: 'error', response: err.toString() };
} }
}); });
ipcMain.handle('toggle-scheduler', async (event, params) => {
try {
if (!params.enabled)
await connections[params.uid].enableEvent({ ...params });
else
await connections[params.uid].disableEvent({ ...params });
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
}; };

View File

@ -149,7 +149,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; if (!query) return;
try { try {
@ -157,6 +157,8 @@ export default connections => {
nest: true, nest: true,
details: true, details: true,
schema, schema,
tabUid,
autocommit,
comments: false comments: false
}); });
@ -263,4 +265,51 @@ export default connections => {
return { status: 'success', response: { willAbort } }; return { status: 'success', response: { willAbort } };
}); });
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 moment from 'moment';
import { sqlEscaper } from 'common/libs/sqlEscaper'; import { sqlEscaper } from 'common/libs/sqlEscaper';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes'; 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'; import fs from 'fs';
export default (connections) => { export default (connections) => {
@ -84,12 +85,13 @@ export default (connections) => {
}); });
ipcMain.handle('update-table-cell', async (event, params) => { ipcMain.handle('update-table-cell', async (event, params) => {
delete params.row._id; delete params.row._antares_id;
const { stringsWrapper: sw } = customizations[connections[params.uid]._client];
try { // TODO: move to client classes try { // TODO: move to client classes
let escapedParam; let escapedParam;
let reload = false; 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)) if ([...NUMBER, ...FLOAT].includes(params.type))
escapedParam = params.content; escapedParam = params.content;
@ -102,6 +104,9 @@ export default (connections) => {
case 'pg': case 'pg':
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`; escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
break; break;
case 'sqlite':
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
break;
} }
} }
else if (ARRAY.includes(params.type)) else if (ARRAY.includes(params.type))
@ -122,6 +127,10 @@ export default (connections) => {
fileBlob = fs.readFileSync(params.content); fileBlob = fs.readFileSync(params.content);
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`; escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
break; break;
case 'sqlite':
fileBlob = fs.readFileSync(params.content);
escapedParam = `X'${fileBlob.toString('hex')}'`;
break;
} }
reload = true; reload = true;
} }
@ -134,6 +143,9 @@ export default (connections) => {
case 'pg': case 'pg':
escapedParam = 'decode(\'\', \'hex\')'; escapedParam = 'decode(\'\', \'hex\')';
break; break;
case 'sqlite':
escapedParam = 'X\'\'';
break;
} }
} }
} }
@ -188,7 +200,7 @@ export default (connections) => {
const fieldName = Object.keys(row)[0].includes('.') ? `${params.table}.${params.primary}` : params.primary; const fieldName = Object.keys(row)[0].includes('.') ? `${params.table}.${params.primary}` : params.primary;
return typeof row[fieldName] === 'string' return typeof row[fieldName] === 'string'
? `"${row[fieldName]}"` ? `'${row[fieldName]}'`
: row[fieldName]; : row[fieldName];
}).join(','); }).join(',');

View File

@ -40,4 +40,17 @@ export default (connections) => {
return { status: 'error', response: err.toString() }; return { status: 'error', response: err.toString() };
} }
}); });
ipcMain.handle('toggle-trigger', async (event, params) => {
try {
if (!params.enabled)
await connections[params.uid].enableTrigger(params);
else
await connections[params.uid].disableTrigger(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
}; };

View File

@ -2,6 +2,7 @@ import { ipcMain } from 'electron';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
import Store from 'electron-store'; import Store from 'electron-store';
const persistentStore = new Store({ name: 'settings' }); const persistentStore = new Store({ name: 'settings' });
const isMacOS = process.platform === 'darwin';
let mainWindow; let mainWindow;
autoUpdater.allowPrerelease = persistentStore.get('allow_prerelease', true); autoUpdater.allowPrerelease = persistentStore.get('allow_prerelease', true);
@ -11,6 +12,9 @@ export default () => {
mainWindow = event; mainWindow = event;
if (process.windowsStore || (process.platform === 'linux' && !process.env.APPIMAGE)) if (process.windowsStore || (process.platform === 'linux' && !process.env.APPIMAGE))
mainWindow.reply('no-auto-update'); mainWindow.reply('no-auto-update');
else if (isMacOS) { // Temporary solution on MacOS for unsigned app updates
autoUpdater.autoDownload = false;
}
else { else {
autoUpdater.checkForUpdatesAndNotify().catch(() => { autoUpdater.checkForUpdatesAndNotify().catch(() => {
mainWindow.reply('check-failed'); mainWindow.reply('check-failed');
@ -28,7 +32,10 @@ export default () => {
}); });
autoUpdater.on('update-available', () => { autoUpdater.on('update-available', () => {
mainWindow.reply('update-available'); if (isMacOS)
mainWindow.reply('link-to-download');
else
mainWindow.reply('update-available');
}); });
autoUpdater.on('update-not-available', () => { autoUpdater.on('update-not-available', () => {

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
import { MySQLClient } from './clients/MySQLClient'; import { MySQLClient } from './clients/MySQLClient';
import { PostgreSQLClient } from './clients/PostgreSQLClient'; import { PostgreSQLClient } from './clients/PostgreSQLClient';
import { SQLiteClient } from './clients/SQLiteClient';
const queryLogger = sql => { const queryLogger = sql => {
// Remove comments, newlines and multiple spaces // Remove comments, newlines and multiple spaces
@ -37,6 +38,8 @@ export class ClientsFactory {
return new MySQLClient(args); return new MySQLClient(args);
case 'pg': case 'pg':
return new PostgreSQLClient(args); return new PostgreSQLClient(args);
case 'sqlite':
return new SQLiteClient(args);
default: default:
throw new Error(`Unknown database client: ${args.client}`); throw new Error(`Unknown database client: ${args.client}`);
} }

View File

@ -9,6 +9,8 @@ export class MySQLClient extends AntaresCore {
super(args); super(args);
this._schema = null; this._schema = null;
this._runningConnections = new Map();
this._connectionsToCommit = new Map();
this.types = { this.types = {
0: 'DECIMAL', 0: 'DECIMAL',
@ -100,9 +102,11 @@ export class MySQLClient extends AntaresCore {
} }
/** /**
*
* @returns dbConfig
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async connect () { async getDbConfig () {
delete this._params.application_name; delete this._params.application_name;
const dbConfig = { const dbConfig = {
@ -133,20 +137,17 @@ export class MySQLClient extends AntaresCore {
} }
} }
return dbConfig;
}
/**
* @memberof MySQLClient
*/
async connect () {
if (!this._poolSize) if (!this._poolSize)
this._connection = await mysql.createConnection(dbConfig); this._connection = await this.getConnection();
else { else
this._connection = mysql.createPool({ this._connection = await this.getConnectionPool();
...dbConfig,
connectionLimit: this._poolSize,
typeCast: (field, next) => {
if (field.type === 'DATETIME')
return field.string();
else
return next();
}
});
}
} }
/** /**
@ -157,6 +158,64 @@ export class MySQLClient extends AntaresCore {
if (this._ssh) this._ssh.close(); 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 * Executes an USE query
* *
@ -187,6 +246,7 @@ export class MySQLClient extends AntaresCore {
const tablesArr = []; const tablesArr = [];
const triggersArr = []; const triggersArr = [];
let schemaSize = 0;
for (const db of filteredDatabases) { for (const db of filteredDatabases) {
if (!schemas.has(db.Database)) continue; if (!schemas.has(db.Database)) continue;
@ -224,6 +284,9 @@ export class MySQLClient extends AntaresCore {
break; break;
} }
const tableSize = table.Data_length + table.Index_length;
schemaSize += tableSize;
return { return {
name: table.Name, name: table.Name,
type: tableType, type: tableType,
@ -232,7 +295,7 @@ export class MySQLClient extends AntaresCore {
updated: table.Update_time, updated: table.Update_time,
engine: table.Engine, engine: table.Engine,
comment: table.Comment, comment: table.Comment,
size: table.Data_length + table.Index_length, size: tableSize,
autoIncrement: table.Auto_increment, autoIncrement: table.Auto_increment,
collation: table.Collation collation: table.Collation
}; };
@ -276,7 +339,7 @@ export class MySQLClient extends AntaresCore {
body: scheduler.EVENT_BODY, body: scheduler.EVENT_BODY,
starts: scheduler.STARTS, starts: scheduler.STARTS,
ends: scheduler.ENDS, ends: scheduler.ENDS,
status: scheduler.STATUS, enabled: scheduler.STATUS === 'ENABLED',
executeAt: scheduler.EXECUTE_AT, executeAt: scheduler.EXECUTE_AT,
intervalField: scheduler.INTERVAL_FIELD, intervalField: scheduler.INTERVAL_FIELD,
intervalValue: scheduler.INTERVAL_VALUE, intervalValue: scheduler.INTERVAL_VALUE,
@ -309,6 +372,7 @@ export class MySQLClient extends AntaresCore {
return { return {
name: db.Database, name: db.Database,
size: schemaSize,
tables: remappedTables, tables: remappedTables,
functions: remappedFunctions, functions: remappedFunctions,
procedures: remappedProcedures, procedures: remappedProcedures,
@ -319,6 +383,7 @@ export class MySQLClient extends AntaresCore {
else { else {
return { return {
name: db.Database, name: db.Database,
size: 0,
tables: [], tables: [],
functions: [], functions: [],
procedures: [], procedures: [],
@ -360,7 +425,7 @@ export class MySQLClient extends AntaresCore {
return acc; return acc;
}, '') }, '')
.replaceAll('\n', '') .replaceAll('\n', '')
.split(',') .split(/,\s?(?![^(]*\))/)
.map(f => { .map(f => {
try { try {
const fieldArr = f.trim().split(' '); const fieldArr = f.trim().split(' ');
@ -400,18 +465,25 @@ export class MySQLClient extends AntaresCore {
return rows.map(field => { return rows.map(field => {
let numLength = field.COLUMN_TYPE.match(/int\(([^)]+)\)/); 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) const enumValues = /(enum|set)/.test(field.COLUMN_TYPE)
? field.COLUMN_TYPE.match(/\(([^)]+)\)/)[0].slice(1, -1) ? field.COLUMN_TYPE.match(/\(([^)]+)\)/)[0].slice(1, -1)
: null; : null;
const defaultValue = (remappedFields && remappedFields[field.COLUMN_NAME])
? remappedFields[field.COLUMN_NAME].default
: field.COLUMN_DEFAULT;
return { return {
name: field.COLUMN_NAME, name: field.COLUMN_NAME,
key: field.COLUMN_KEY.toLowerCase(), 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, schema: field.TABLE_SCHEMA,
table: field.TABLE_NAME, table: field.TABLE_NAME,
numPrecision: field.NUMERIC_PRECISION, numPrecision: field.NUMERIC_PRECISION,
numScale: field.NUMERIC_SCALE,
numLength, numLength,
enumValues, enumValues,
datePrecision: field.DATETIME_PRECISION, datePrecision: field.DATETIME_PRECISION,
@ -420,11 +492,13 @@ export class MySQLClient extends AntaresCore {
unsigned: field.COLUMN_TYPE.includes('unsigned'), unsigned: field.COLUMN_TYPE.includes('unsigned'),
zerofill: field.COLUMN_TYPE.includes('zerofill'), zerofill: field.COLUMN_TYPE.includes('zerofill'),
order: field.ORDINAL_POSITION, order: field.ORDINAL_POSITION,
default: remappedFields ? remappedFields[field.COLUMN_NAME].default : field.COLUMN_DEFAULT, default: defaultValue,
charset: field.CHARACTER_SET_NAME, charset: field.CHARACTER_SET_NAME,
collation: field.COLLATION_NAME, collation: field.COLLATION_NAME,
autoIncrement: field.EXTRA.includes('auto_increment'), 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 comment: field.COLUMN_COMMENT
}; };
}); });
@ -565,7 +639,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* CREATE DATABASE * CREATE DATABASE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createSchema (params) { async createSchema (params) {
@ -575,7 +649,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER DATABASE * ALTER DATABASE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterSchema (params) { async alterSchema (params) {
@ -585,7 +659,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP DATABASE * DROP DATABASE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropSchema (params) { async dropSchema (params) {
@ -625,7 +699,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP VIEW * DROP VIEW
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropView (params) { async dropView (params) {
@ -636,7 +710,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER VIEW * ALTER VIEW
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterView (params) { async alterView (params) {
@ -657,7 +731,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* CREATE VIEW * CREATE VIEW
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createView (params) { async createView (params) {
@ -690,7 +764,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP TRIGGER * DROP TRIGGER
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropTrigger (params) { async dropTrigger (params) {
@ -701,7 +775,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER TRIGGER * ALTER TRIGGER
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterTrigger (params) { async alterTrigger (params) {
@ -723,7 +797,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* CREATE TRIGGER * CREATE TRIGGER
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createTrigger (params) { async createTrigger (params) {
@ -797,7 +871,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP PROCEDURE * DROP PROCEDURE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropRoutine (params) { async dropRoutine (params) {
@ -808,7 +882,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER PROCEDURE * ALTER PROCEDURE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterRoutine (params) { async alterRoutine (params) {
@ -830,7 +904,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* CREATE PROCEDURE * CREATE PROCEDURE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createRoutine (params) { async createRoutine (params) {
@ -924,7 +998,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP FUNCTION * DROP FUNCTION
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropFunction (params) { async dropFunction (params) {
@ -935,7 +1009,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER FUNCTION * ALTER FUNCTION
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterFunction (params) { async alterFunction (params) {
@ -957,7 +1031,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* CREATE FUNCTION * CREATE FUNCTION
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createFunction (params) { async createFunction (params) {
@ -1018,7 +1092,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP EVENT * DROP EVENT
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropEvent (params) { async dropEvent (params) {
@ -1029,7 +1103,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER EVENT * ALTER EVENT
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterEvent (params) { async alterEvent (params) {
@ -1055,7 +1129,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* CREATE EVENT * CREATE EVENT
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createEvent (params) { async createEvent (params) {
@ -1072,6 +1146,16 @@ export class MySQLClient extends AntaresCore {
return await this.raw(sql, { split: false }); return await this.raw(sql, { split: false });
} }
async enableEvent ({ schema, scheduler }) {
const sql = `ALTER EVENT \`${schema}\`.\`${scheduler}\` ENABLE`;
return await this.raw(sql, { split: false });
}
async disableEvent ({ schema, scheduler }) {
const sql = `ALTER EVENT \`${schema}\`.\`${scheduler}\` DISABLE`;
return await this.raw(sql, { split: false });
}
/** /**
* SHOW COLLATION * SHOW COLLATION
* *
@ -1111,6 +1195,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 * SHOW ENGINES
* *
@ -1182,14 +1286,60 @@ export class MySQLClient extends AntaresCore {
}); });
} }
/**
*
* @param {number} id
* @returns {Promise<null>}
*/
async killProcess (id) { async killProcess (id) {
return await this.raw(`KILL ${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 * CREATE TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createTable (params) { async createTable (params) {
@ -1212,7 +1362,7 @@ export class MySQLClient extends AntaresCore {
const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false; const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false;
newColumns.push(`\`${field.name}\` newColumns.push(`\`${field.name}\`
${field.type.toUpperCase()}${length ? `(${length})` : ''} ${field.type.toUpperCase()}${length ? `(${length}${field.numScale ? `,${field.numScale}` : ''})` : ''}
${field.unsigned ? 'UNSIGNED' : ''} ${field.unsigned ? 'UNSIGNED' : ''}
${field.zerofill ? 'ZEROFILL' : ''} ${field.zerofill ? 'ZEROFILL' : ''}
${field.nullable ? 'NULL' : 'NOT NULL'} ${field.nullable ? 'NULL' : 'NOT NULL'}
@ -1251,7 +1401,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER TABLE * ALTER TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterTable (params) { async alterTable (params) {
@ -1281,7 +1431,7 @@ export class MySQLClient extends AntaresCore {
const length = typeInfo.length ? addition.enumValues || addition.numLength || addition.charLength || addition.datePrecision : false; const length = typeInfo.length ? addition.enumValues || addition.numLength || addition.charLength || addition.datePrecision : false;
alterColumns.push(`ADD COLUMN \`${addition.name}\` alterColumns.push(`ADD COLUMN \`${addition.name}\`
${addition.type.toUpperCase()}${length ? `(${length})` : ''} ${addition.type.toUpperCase()}${length ? `(${length}${addition.numScale ? `,${addition.numScale}` : ''})` : ''}
${addition.unsigned ? 'UNSIGNED' : ''} ${addition.unsigned ? 'UNSIGNED' : ''}
${addition.zerofill ? 'ZEROFILL' : ''} ${addition.zerofill ? 'ZEROFILL' : ''}
${addition.nullable ? 'NULL' : 'NOT NULL'} ${addition.nullable ? 'NULL' : 'NOT NULL'}
@ -1319,7 +1469,7 @@ export class MySQLClient extends AntaresCore {
const length = typeInfo.length ? change.enumValues || change.numLength || change.charLength || change.datePrecision : false; const length = typeInfo.length ? change.enumValues || change.numLength || change.charLength || change.datePrecision : false;
alterColumns.push(`CHANGE COLUMN \`${change.orgName}\` \`${change.name}\` 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.unsigned ? 'UNSIGNED' : ''}
${change.zerofill ? 'ZEROFILL' : ''} ${change.zerofill ? 'ZEROFILL' : ''}
${change.nullable ? 'NULL' : 'NOT NULL'} ${change.nullable ? 'NULL' : 'NOT NULL'}
@ -1386,7 +1536,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DUPLICATE TABLE * DUPLICATE TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async duplicateTable (params) { async duplicateTable (params) {
@ -1397,7 +1547,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* TRUNCATE TABLE * TRUNCATE TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async truncateTable (params) { async truncateTable (params) {
@ -1408,7 +1558,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP TABLE * DROP TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropTable (params) { async dropTable (params) {
@ -1490,6 +1640,7 @@ export class MySQLClient extends AntaresCore {
details: false, details: false,
split: true, split: true,
comments: true, comments: true,
autocommit: true,
...args ...args
}; };
@ -1504,8 +1655,24 @@ export class MySQLClient extends AntaresCore {
.filter(Boolean) .filter(Boolean)
.map(q => q.trim()) .map(q => q.trim())
: [sql]; : [sql];
let connection;
const isPool = typeof this._connection.getConnection === 'function'; 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) if (args.schema)
await connection.query(`USE \`${args.schema}\``); await connection.query(`USE \`${args.schema}\``);
@ -1567,7 +1734,10 @@ export class MySQLClient extends AntaresCore {
}); });
} }
catch (err) { catch (err) {
if (isPool) connection.release(); if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err); reject(err);
} }
@ -1576,7 +1746,10 @@ export class MySQLClient extends AntaresCore {
keysArr = keysArr ? [...keysArr, ...response] : response; keysArr = keysArr ? [...keysArr, ...response] : response;
} }
catch (err) { catch (err) {
if (isPool) connection.release(); if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err); reject(err);
} }
} }
@ -1591,7 +1764,10 @@ export class MySQLClient extends AntaresCore {
keys: keysArr keys: keysArr
}); });
}).catch((err) => { }).catch((err) => {
if (isPool) connection.release(); if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err); reject(err);
}); });
}); });
@ -1599,7 +1775,10 @@ export class MySQLClient extends AntaresCore {
resultsArr.push({ rows, report, fields, keys, duration }); 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; return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
} }

View File

@ -9,6 +9,7 @@ function pgToString (value) {
return value.toString(); return value.toString();
} }
types.setTypeParser(20, a => parseInt(a));// bigint string to number
types.setTypeParser(1082, pgToString); // date types.setTypeParser(1082, pgToString); // date
types.setTypeParser(1083, pgToString); // time types.setTypeParser(1083, pgToString); // time
types.setTypeParser(1114, pgToString); // timestamp types.setTypeParser(1114, pgToString); // timestamp
@ -20,6 +21,8 @@ export class PostgreSQLClient extends AntaresCore {
super(args); super(args);
this._schema = null; this._schema = null;
this._runningConnections = new Map();
this._connectionsToCommit = new Map();
this.types = {}; this.types = {};
for (const key in types.builtins) for (const key in types.builtins)
@ -69,9 +72,11 @@ export class PostgreSQLClient extends AntaresCore {
} }
/** /**
*
* @returns dbConfig
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async connect () { async getDbConfig () {
const dbConfig = { const dbConfig = {
host: this._params.host, host: this._params.host,
port: this._params.port, port: this._params.port,
@ -100,15 +105,43 @@ export class PostgreSQLClient extends AntaresCore {
} }
} }
if (!this._poolSize) { return dbConfig;
const client = new Client(dbConfig); }
await client.connect();
this._connection = client; /**
} * @memberof PostgreSQLClient
else { */
const pool = new Pool({ ...dbConfig, max: this._poolSize }); async connect () {
this._connection = pool; if (!this._poolSize)
this._connection = await this.getConnection();
else
this._connection = await this.getConnectionPool();
}
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;
} }
/** /**
@ -120,15 +153,23 @@ export class PostgreSQLClient extends AntaresCore {
} }
/** /**
* Executes an "USE" query * Executes an 'SET search_path TO "${schema}"' query
* *
* @param {String} schema * @param {String} schema
* @param {Object?} connection optional
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
use (schema) { use (schema, connection) {
this._schema = schema; 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);
}
} }
/** /**
@ -143,6 +184,7 @@ export class PostgreSQLClient extends AntaresCore {
const tablesArr = []; const tablesArr = [];
const triggersArr = []; const triggersArr = [];
let schemaSize = 0;
for (const db of databases) { for (const db of databases) {
if (!schemas.has(db.database)) continue; if (!schemas.has(db.database)) continue;
@ -168,19 +210,20 @@ export class PostgreSQLClient extends AntaresCore {
} }
let { rows: triggers } = await this.raw(` let { rows: triggers } = await this.raw(`
SELECT event_object_schema AS table_schema, SELECT
event_object_table AS table_name, pg_class.relname AS table_name,
trigger_schema, pg_trigger.tgname AS trigger_name,
trigger_name, pg_namespace.nspname AS trigger_schema,
string_agg(event_manipulation, ',') AS event, (pg_trigger.tgenabled != 'D')::bool AS enabled
action_timing AS activation, FROM pg_trigger
action_condition AS condition, JOIN pg_class ON pg_trigger.tgrelid = pg_class.oid
action_statement AS definition JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace
FROM information_schema.triggers JOIN information_schema.triggers ON information_schema.triggers.trigger_schema = pg_namespace.nspname
AND information_schema.triggers.event_object_table = pg_class.relname
AND information_schema.triggers.trigger_name = pg_trigger.tgname
WHERE trigger_schema = '${db.database}' WHERE trigger_schema = '${db.database}'
GROUP BY 1,2,3,4,6,7,8 GROUP BY 1, 2, 3, 4
ORDER BY table_schema, ORDER BY table_name
table_name
`); `);
if (triggers.length) { if (triggers.length) {
@ -196,11 +239,14 @@ export class PostgreSQLClient extends AntaresCore {
if (schemas.has(db.database)) { if (schemas.has(db.database)) {
// TABLES // TABLES
const remappedTables = tablesArr.filter(table => table.Db === db.database).map(table => { const remappedTables = tablesArr.filter(table => table.Db === db.database).map(table => {
const tableSize = +table.data_length + table.index_length;
schemaSize += tableSize;
return { return {
name: table.table_name, name: table.table_name,
type: table.table_type === 'VIEW' ? 'view' : 'table', type: table.table_type === 'VIEW' ? 'view' : 'table',
rows: table.reltuples, rows: table.reltuples,
size: +table.data_length + +table.index_length, size: tableSize,
collation: table.Collation, collation: table.Collation,
comment: table.comment, comment: table.comment,
engine: '' engine: ''
@ -239,17 +285,16 @@ export class PostgreSQLClient extends AntaresCore {
return { return {
name: `${trigger.table_name}.${trigger.trigger_name}`, name: `${trigger.table_name}.${trigger.trigger_name}`,
orgName: trigger.trigger_name, orgName: trigger.trigger_name,
timing: trigger.activation,
definer: '', definer: '',
definition: trigger.definition,
event: trigger.event,
table: trigger.table_name, table: trigger.table_name,
sqlMode: '' sqlMode: '',
enabled: trigger.enabled
}; };
}); });
return { return {
name: db.database, name: db.database,
size: schemaSize,
tables: remappedTables, tables: remappedTables,
functions: remappedFunctions, functions: remappedFunctions,
procedures: remappedProcedures, procedures: remappedProcedures,
@ -261,6 +306,7 @@ export class PostgreSQLClient extends AntaresCore {
else { else {
return { return {
name: db.database, name: db.database,
size: 0,
tables: [], tables: [],
functions: [], functions: [],
procedures: [], procedures: [],
@ -302,6 +348,7 @@ export class PostgreSQLClient extends AntaresCore {
isArray, isArray,
schema: field.table_schema, schema: field.table_schema,
table: field.table_name, table: field.table_name,
numScale: field.numeric_scale,
numPrecision: field.numeric_precision, numPrecision: field.numeric_precision,
datePrecision: field.datetime_precision, datePrecision: field.datetime_precision,
charLength: field.character_maximum_length, charLength: field.character_maximum_length,
@ -500,7 +547,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* CREATE SCHEMA * CREATE SCHEMA
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createSchema (params) { async createSchema (params) {
@ -510,7 +557,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* ALTER DATABASE * ALTER DATABASE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterSchema (params) { async alterSchema (params) {
@ -520,7 +567,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* DROP DATABASE * DROP DATABASE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropSchema (params) { async dropSchema (params) {
@ -552,7 +599,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* DROP VIEW * DROP VIEW
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async dropView (params) { async dropView (params) {
@ -563,7 +610,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* ALTER VIEW * ALTER VIEW
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async alterView (params) { async alterView (params) {
@ -579,7 +626,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* CREATE VIEW * CREATE VIEW
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async createView (params) { async createView (params) {
@ -597,19 +644,25 @@ export class PostgreSQLClient extends AntaresCore {
const [table, triggerName] = trigger.split('.'); const [table, triggerName] = trigger.split('.');
const results = await this.raw(` const results = await this.raw(`
SELECT event_object_schema AS table_schema, SELECT
event_object_table AS table_name, information_schema.triggers.event_object_schema AS table_schema,
trigger_schema, information_schema.triggers.event_object_table AS table_name,
trigger_name, information_schema.triggers.trigger_schema,
string_agg(event_manipulation, ',') AS event, information_schema.triggers.trigger_name,
string_agg(event_manipulation, ',') AS EVENT,
action_timing AS activation, action_timing AS activation,
action_condition AS condition, action_condition AS condition,
action_statement AS definition action_statement AS definition,
FROM information_schema.triggers (pg_trigger.tgenabled != 'D')::bool AS enabled
FROM pg_trigger
JOIN pg_class ON pg_trigger.tgrelid = pg_class.oid
JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace
JOIN information_schema.triggers ON pg_namespace.nspname = information_schema.triggers.trigger_schema
AND pg_class.relname = information_schema.triggers.event_object_table
WHERE trigger_schema = '${schema}' WHERE trigger_schema = '${schema}'
AND trigger_name = '${triggerName}' AND trigger_name = '${triggerName}'
AND event_object_table = '${table}' AND event_object_table = '${table}'
GROUP BY 1,2,3,4,6,7,8 GROUP BY 1,2,3,4,6,7,8,9
ORDER BY table_schema, ORDER BY table_schema,
table_name table_name
`); `);
@ -619,7 +672,7 @@ export class PostgreSQLClient extends AntaresCore {
sql: row.definition, sql: row.definition,
name: row.trigger_name, name: row.trigger_name,
table: row.table_name, table: row.table_name,
event: row.event.split(','), event: [...new Set(row.event.split(','))],
activation: row.activation activation: row.activation
}; };
})[0]; })[0];
@ -628,7 +681,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* DROP TRIGGER * DROP TRIGGER
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async dropTrigger (params) { async dropTrigger (params) {
@ -640,7 +693,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* ALTER TRIGGER * ALTER TRIGGER
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async alterTrigger (params) { async alterTrigger (params) {
@ -662,7 +715,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* CREATE TRIGGER * CREATE TRIGGER
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async createTrigger (params) { async createTrigger (params) {
@ -671,6 +724,18 @@ export class PostgreSQLClient extends AntaresCore {
return await this.raw(sql, { split: false }); return await this.raw(sql, { split: false });
} }
async enableTrigger ({ schema, trigger }) {
const [table, triggerName] = trigger.split('.');
const sql = `ALTER TABLE "${schema}"."${table}" ENABLE TRIGGER "${triggerName}"`;
return await this.raw(sql, { split: false });
}
async disableTrigger ({ schema, trigger }) {
const [table, triggerName] = trigger.split('.');
const sql = `ALTER TABLE "${schema}"."${table}" DISABLE TRIGGER "${triggerName}"`;
return await this.raw(sql, { split: false });
}
/** /**
* SHOW CREATE PROCEDURE * SHOW CREATE PROCEDURE
* *
@ -1055,14 +1120,64 @@ export class PostgreSQLClient extends AntaresCore {
}); });
} }
/**
*
* @param {number} id
* @returns {Promise<null>}
*/
async killProcess (id) { async killProcess (id) {
return await this.raw(`SELECT pg_terminate_backend(${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 * CREATE TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async createTable (params) { async createTable (params) {
@ -1086,7 +1201,7 @@ export class PostgreSQLClient extends AntaresCore {
const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false; const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false;
newColumns.push(`"${field.name}" newColumns.push(`"${field.name}"
${field.type.toUpperCase()}${length ? `(${length})` : ''} ${field.type.toUpperCase()}${length ? `(${length}${field.numScale !== null ? `,${field.numScale}` : ''})` : ''}
${field.unsigned ? 'UNSIGNED' : ''} ${field.unsigned ? 'UNSIGNED' : ''}
${field.zerofill ? 'ZEROFILL' : ''} ${field.zerofill ? 'ZEROFILL' : ''}
${field.nullable ? 'NULL' : 'NOT NULL'} ${field.nullable ? 'NULL' : 'NOT NULL'}
@ -1120,7 +1235,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* ALTER TABLE * ALTER TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async alterTable (params) { async alterTable (params) {
@ -1150,7 +1265,7 @@ export class PostgreSQLClient extends AntaresCore {
const length = typeInfo.length ? addition.numLength || addition.charLength || addition.datePrecision : false; const length = typeInfo.length ? addition.numLength || addition.charLength || addition.datePrecision : false;
alterColumns.push(`ADD COLUMN "${addition.name}" 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.unsigned ? 'UNSIGNED' : ''}
${addition.zerofill ? 'ZEROFILL' : ''} ${addition.zerofill ? 'ZEROFILL' : ''}
${addition.nullable ? 'NULL' : 'NOT NULL'} ${addition.nullable ? 'NULL' : 'NOT NULL'}
@ -1196,7 +1311,7 @@ export class PostgreSQLClient extends AntaresCore {
localType = change.type.toLowerCase(); 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.nullable ? 'DROP NOT NULL' : 'SET NOT NULL'}`);
alterColumns.push(`ALTER COLUMN "${change.name}" ${change.default ? `SET DEFAULT ${change.default}` : 'DROP DEFAULT'}`); alterColumns.push(`ALTER COLUMN "${change.name}" ${change.default ? `SET DEFAULT ${change.default}` : 'DROP DEFAULT'}`);
@ -1266,7 +1381,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* DUPLICATE TABLE * DUPLICATE TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async duplicateTable (params) { async duplicateTable (params) {
@ -1277,7 +1392,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* TRUNCATE TABLE * TRUNCATE TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async truncateTable (params) { async truncateTable (params) {
@ -1288,7 +1403,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* DROP TABLE * DROP TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async dropTable (params) { async dropTable (params) {
@ -1363,17 +1478,17 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async raw (sql, args) { async raw (sql, args) {
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
args = { args = {
nest: false, nest: false,
details: false, details: false,
split: true, split: true,
comments: true, comments: true,
autocommit: true,
...args ...args
}; };
if (args.schema && args.schema !== 'public')
await this.use(args.schema);
if (!args.comments) if (!args.comments)
sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');// Remove comments sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');// Remove comments
@ -1385,7 +1500,26 @@ export class PostgreSQLClient extends AntaresCore {
.map(q => q.trim()) .map(q => q.trim())
: [sql]; : [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) { for (const query of queries) {
if (!query) continue; if (!query) continue;
@ -1395,15 +1529,12 @@ export class PostgreSQLClient extends AntaresCore {
let keysArr = []; let keysArr = [];
const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => { const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => {
this._connection.query({ (async () => {
rowMode: args.nest ? 'array' : null, try {
text: query const res = await connection.query({ rowMode: args.nest ? 'array' : null, text: query });
}, async (err, res) => {
timeStop = new Date(); timeStop = new Date();
if (err)
reject(err);
else {
let ast; let ast;
try { try {
@ -1492,6 +1623,10 @@ export class PostgreSQLClient extends AntaresCore {
}); });
} }
catch (err) { catch (err) {
if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err); reject(err);
} }
@ -1500,6 +1635,10 @@ export class PostgreSQLClient extends AntaresCore {
keysArr = keysArr ? [...keysArr, ...response] : response; keysArr = keysArr ? [...keysArr, ...response] : response;
} }
catch (err) { catch (err) {
if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err); reject(err);
} }
} }
@ -1514,12 +1653,24 @@ export class PostgreSQLClient extends AntaresCore {
keys: keysArr keys: keysArr
}); });
} }
}); catch (err) {
if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err);
}
})();
}); });
resultsArr.push({ rows, report, fields, keys, duration }); 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; return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
} }
} }

View File

@ -0,0 +1,859 @@
'use strict';
import sqlite from 'better-sqlite3';
import { AntaresCore } from '../AntaresCore';
import dataTypes from 'common/data-types/sqlite';
import { NUMBER, FLOAT, TIME, DATETIME } from 'common/fieldTypes';
export class SQLiteClient extends AntaresCore {
constructor (args) {
super(args);
this._schema = null;
this._connectionsToCommit = new Map();
}
_getTypeInfo (type) {
return dataTypes
.reduce((acc, group) => [...acc, ...group.types], [])
.filter(_type => _type.name === type.toUpperCase())[0];
}
/**
* @memberof SQLiteClient
*/
async connect () {
this._connection = this.getConnection();
}
getConnection () {
return sqlite(this._params.databasePath, {
fileMustExist: true,
readonly: this._params.readonly
});
}
/**
* @memberof SQLiteClient
*/
destroy () {}
/**
* Executes an USE query
*
* @memberof SQLiteClient
*/
use () {}
/**
* @param {Array} schemas list
* @returns {Array.<Object>} databases scructure
* @memberof SQLiteClient
*/
async getStructure (schemas) {
const { rows: databases } = await this.raw('SELECT * FROM pragma_database_list');
const filteredDatabases = databases;
const tablesArr = [];
const triggersArr = [];
let schemaSize = 0;
for (const db of filteredDatabases) {
if (!schemas.has(db.name)) continue;
let { rows: tables } = await this.raw(`
SELECT *
FROM "${db.name}".sqlite_master
WHERE type IN ('table', 'view')
AND name NOT LIKE 'sqlite_%'
ORDER BY name
`);
if (tables.length) {
tables = tables.map(table => {
table.Db = db.name;
return table;
});
tablesArr.push(...tables);
}
let { rows: triggers } = await this.raw(`SELECT * FROM "${db.name}".sqlite_master WHERE type='trigger'`);
if (triggers.length) {
triggers = triggers.map(trigger => {
trigger.Db = db.name;
return trigger;
});
triggersArr.push(...triggers);
}
}
return filteredDatabases.map(db => {
if (schemas.has(db.name)) {
// TABLES
const remappedTables = tablesArr.filter(table => table.Db === db.name).map(table => {
const tableSize = 0;
schemaSize += tableSize;
return {
name: table.name,
type: table.type,
rows: false,
size: false
};
});
// TRIGGERS
const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.name).map(trigger => {
return {
name: trigger.name,
table: trigger.tbl_name
};
});
return {
name: db.name,
size: schemaSize,
tables: remappedTables,
functions: [],
procedures: [],
triggers: remappedTriggers,
schedulers: []
};
}
else {
return {
name: db.name,
size: 0,
tables: [],
functions: [],
procedures: [],
triggers: [],
schedulers: []
};
}
});
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table scructure
* @memberof SQLiteClient
*/
async getTableColumns ({ schema, table }) {
const { rows: fields } = await this.raw(`SELECT * FROM "${schema}".pragma_table_info('${table}')`);
return fields.map(field => {
const [type, length] = field.type.includes('(')
? field.type.replace(')', '').split('(').map(el => {
if (!isNaN(el)) el = +el;
return el;
})
: [field.type, null];
return {
name: field.name,
key: null,
type: type.trim(),
schema: schema,
table: table,
numPrecision: [...NUMBER, ...FLOAT].includes(type) ? length : null,
datePrecision: null,
charLength: ![...NUMBER, ...FLOAT].includes(type) ? length : null,
nullable: !field.notnull,
unsigned: null,
zerofill: null,
order: field.cid + 1,
default: field.dflt_value,
charset: null,
collation: null,
autoIncrement: false,
onUpdate: null,
comment: ''
};
});
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table row count
* @memberof SQLiteClient
*/
async getTableApproximateCount ({ schema, table }) {
const { rows } = await this.raw(`SELECT COUNT(*) AS count FROM "${schema}"."${table}"`);
return rows.length ? rows[0].count : 0;
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table options
* @memberof SQLiteClient
*/
async getTableOptions ({ schema, table }) {
return { name: table };
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table indexes
* @memberof SQLiteClient
*/
async getTableIndexes ({ schema, table }) {
const remappedIndexes = [];
const { rows: primaryKeys } = await this.raw(`SELECT * FROM "${schema}".pragma_table_info('${table}') WHERE pk != 0`);
for (const key of primaryKeys) {
remappedIndexes.push({
name: 'PRIMARY',
column: key.name,
indexType: null,
type: 'PRIMARY',
cardinality: null,
comment: '',
indexComment: ''
});
}
const { rows: indexes } = await this.raw(`SELECT * FROM "${schema}".pragma_index_list('${table}');`);
for (const index of indexes) {
const { rows: details } = await this.raw(`SELECT * FROM "${schema}".pragma_index_info('${index.name}');`);
for (const detail of details) {
remappedIndexes.push({
name: index.name,
column: detail.name,
indexType: null,
type: index.unique === 1 ? 'UNIQUE' : 'INDEX',
cardinality: null,
comment: '',
indexComment: ''
});
}
}
return remappedIndexes;
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table key usage
* @memberof SQLiteClient
*/
async getKeyUsage ({ schema, table }) {
const { rows } = await this.raw(`SELECT * FROM "${schema}".pragma_foreign_key_list('${table}');`);
return rows.map(field => {
return {
schema: schema,
table: table,
field: field.from,
position: field.id + 1,
constraintPosition: null,
constraintName: field.id,
refSchema: schema,
refTable: field.table,
refField: field.to,
onUpdate: field.on_update,
onDelete: field.on_delete
};
});
}
async getUsers () {}
/**
* SHOW CREATE VIEW
*
* @returns {Array.<Object>} view informations
* @memberof SQLiteClient
*/
async getViewInformations ({ schema, view }) {
const sql = `SELECT "sql" FROM "${schema}".sqlite_master WHERE "type"='view' AND name='${view}'`;
const results = await this.raw(sql);
return results.rows.map(row => {
return {
sql: row.sql.match(/(?<=AS ).*?$/gs)[0],
name: view
};
})[0];
}
/**
* DROP VIEW
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async dropView (params) {
const sql = `DROP VIEW "${params.schema}"."${params.view}"`;
return await this.raw(sql);
}
/**
* ALTER VIEW
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async alterView (params) {
const { view } = params;
try {
await this.dropView({ schema: view.schema, view: view.oldName });
await this.createView(view);
}
catch (err) {
return Promise.reject(err);
}
}
/**
* CREATE VIEW
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async createView (params) {
const sql = `CREATE VIEW "${params.schema}"."${params.name}" AS ${params.sql}`;
return await this.raw(sql);
}
/**
* SHOW CREATE TRIGGER
*
* @returns {Array.<Object>} view informations
* @memberof SQLiteClient
*/
async getTriggerInformations ({ schema, trigger }) {
const sql = `SELECT "sql" FROM "${schema}".sqlite_master WHERE "type"='trigger' AND name='${trigger}'`;
const results = await this.raw(sql);
return results.rows.map(row => {
return {
sql: row.sql.match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
name: trigger,
table: row.sql.match(/(?<=ON `).*?(?=`)/gs)[0],
activation: row.sql.match(/(BEFORE|AFTER)/gs)[0],
event: row.sql.match(/(INSERT|UPDATE|DELETE)/gs)[0]
};
})[0];
}
/**
* DROP TRIGGER
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async dropTrigger (params) {
const sql = `DROP TRIGGER \`${params.schema}\`.\`${params.trigger}\``;
return await this.raw(sql);
}
/**
* ALTER TRIGGER
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async alterTrigger (params) {
const { trigger } = params;
const tempTrigger = Object.assign({}, trigger);
tempTrigger.name = `Antares_${tempTrigger.name}_tmp`;
try {
await this.createTrigger(tempTrigger);
await this.dropTrigger({ schema: trigger.schema, trigger: tempTrigger.name });
await this.dropTrigger({ schema: trigger.schema, trigger: trigger.oldName });
await this.createTrigger(trigger);
}
catch (err) {
return Promise.reject(err);
}
}
/**
* CREATE TRIGGER
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async createTrigger (params) {
const sql = `CREATE ${params.definer ? `DEFINER=${params.definer} ` : ''}TRIGGER \`${params.schema}\`.\`${params.name}\` ${params.activation} ${params.event} ON \`${params.table}\` FOR EACH ROW ${params.sql}`;
return await this.raw(sql, { split: false });
}
/**
* SHOW COLLATION
*
* @returns {Array.<Object>} collations list
* @memberof SQLiteClient
*/
async getCollations () {
return [];
}
/**
* SHOW VARIABLES
*
* @returns {Array.<Object>} variables list
* @memberof SQLiteClient
*/
async getVariables () {
return [];
}
/**
* SHOW ENGINES
*
* @returns {Array.<Object>} engines list
* @memberof SQLiteClient
*/
async getEngines () {
return {
name: 'SQLite',
support: 'YES',
comment: '',
isDefault: true
};
}
/**
* SHOW VARIABLES LIKE '%vers%'
*
* @returns {Array.<Object>} version parameters
* @memberof SQLiteClient
*/
async getVersion () {
const os = require('os');
const sql = 'SELECT sqlite_version() AS version';
const { rows } = await this.raw(sql);
return {
number: rows[0].version,
name: 'SQLite',
arch: process.arch,
os: `${os.type()} ${os.release()}`
};
}
async getProcesses () {}
async killProcess () {}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async commitTab (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
connection.prepare('COMMIT').run();
return this.destroyConnectionToCommit(tabUid);
}
}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async rollbackTab (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
connection.prepare('ROLLBACK').run();
return this.destroyConnectionToCommit(tabUid);
}
}
destroyConnectionToCommit (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
connection.close();
this._connectionsToCommit.delete(tabUid);
}
}
/**
* CREATE TABLE
*
* @returns {Promise<null>}
* @memberof SQLiteClient
*/
async createTable (params) {
const {
schema,
fields,
foreigns,
indexes,
options
} = params;
const newColumns = [];
const newIndexes = [];
const manageIndexes = [];
const newForeigns = [];
let sql = `CREATE TABLE "${schema}"."${options.name}"`;
// ADD FIELDS
fields.forEach(field => {
const typeInfo = this._getTypeInfo(field.type);
const length = typeInfo?.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false;
newColumns.push(`"${field.name}"
${field.type.toUpperCase()}${length && length !== true ? `(${length})` : ''}
${field.unsigned ? 'UNSIGNED' : ''}
${field.nullable ? 'NULL' : 'NOT NULL'}
${field.autoIncrement ? 'AUTO_INCREMENT' : ''}
${field.default ? `DEFAULT ${field.default}` : ''}
${field.onUpdate ? `ON UPDATE ${field.onUpdate}` : ''}`);
});
// ADD INDEX
indexes.forEach(index => {
const fields = index.fields.map(field => `"${field}"`).join(',');
const type = index.type;
if (type === 'PRIMARY')
newIndexes.push(`PRIMARY KEY (${fields})`);
else
manageIndexes.push(`CREATE ${type === 'UNIQUE' ? type : ''} INDEX "${index.name}" ON "${options.name}" (${fields})`);
});
// ADD FOREIGN KEYS
foreigns.forEach(foreign => {
newForeigns.push(`CONSTRAINT "${foreign.constraintName}" FOREIGN KEY ("${foreign.field}") REFERENCES "${foreign.refTable}" ("${foreign.refField}") ON UPDATE ${foreign.onUpdate} ON DELETE ${foreign.onDelete}`);
});
sql = `${sql} (${[...newColumns, ...newIndexes, ...newForeigns].join(', ')})`;
if (manageIndexes.length) sql = `${sql}; ${manageIndexes.join(';')}`;
return await this.raw(sql);
}
/**
* ALTER TABLE
*
* @returns {Promise<null>}
* @memberof SQLiteClient
*/
async alterTable (params) {
try {
await this.raw('BEGIN TRANSACTION');
await this.raw('PRAGMA foreign_keys = 0');
const tmpName = `Antares_${params.table}_tmp`;
await this.raw(`CREATE TABLE "${tmpName}" AS SELECT * FROM "${params.table}"`);
await this.dropTable(params);
const createTableParams = {
schema: params.schema,
fields: params.tableStructure.fields,
foreigns: params.tableStructure.foreigns,
indexes: params.tableStructure.indexes.filter(index => !index.name.includes('sqlite_autoindex')),
options: { name: params.tableStructure.name }
};
await this.createTable(createTableParams);
const insertFields = createTableParams.fields
.filter(field => {
return (
params.additions.every(add => add.name !== field.name) &&
params.deletions.every(del => del.name !== field.name)
);
})
.reduce((acc, curr) => {
acc.push(`"${curr.name}"`);
return acc;
}, []);
const selectFields = insertFields.map(field => {
const renamedField = params.changes.find(change => `"${change.name}"` === field);
if (renamedField)
return `"${renamedField.orgName}"`;
return field;
});
await this.raw(`INSERT INTO "${createTableParams.options.name}" (${insertFields.join(',')}) SELECT ${selectFields.join(',')} FROM "${tmpName}"`);
await this.dropTable({ schema: params.schema, table: tmpName });
await this.raw('PRAGMA foreign_keys = 1');
await this.raw('COMMIT');
}
catch (err) {
await this.raw('ROLLBACK');
return Promise.reject(err);
}
}
/**
* DUPLICATE TABLE
*
* @returns {Promise<null>}
* @memberof SQLiteClient
*/
async duplicateTable (params) { // TODO: retrive table informations and create a copy
const sql = `CREATE TABLE "${params.schema}"."${params.table}_copy" AS SELECT * FROM "${params.schema}"."${params.table}"`;
return await this.raw(sql);
}
/**
* TRUNCATE TABLE
*
* @returns {Promise<null>}
* @memberof SQLiteClient
*/
async truncateTable (params) {
const sql = `DELETE FROM "${params.schema}"."${params.table}"`;
return await this.raw(sql);
}
/**
* DROP TABLE
*
* @returns {Promise<null>}
* @memberof SQLiteClient
*/
async dropTable (params) {
const sql = `DROP TABLE "${params.schema}"."${params.table}"`;
return await this.raw(sql);
}
/**
* @returns {String} SQL string
* @memberof SQLiteClient
*/
getSQL () {
// SELECT
const selectArray = this._query.select.reduce(this._reducer, []);
let selectRaw = '';
if (selectArray.length)
selectRaw = selectArray.length ? `SELECT ${selectArray.join(', ')} ` : 'SELECT * ';
// FROM
let fromRaw = '';
if (!this._query.update.length && !Object.keys(this._query.insert).length && !!this._query.from)
fromRaw = 'FROM';
else if (Object.keys(this._query.insert).length)
fromRaw = 'INTO';
fromRaw += this._query.from ? ` ${this._query.schema ? `"${this._query.schema}".` : ''}"${this._query.from}" ` : '';
// WHERE
const whereArray = this._query.where
.reduce(this._reducer, [])
?.map(clausole => clausole.replace('= null', 'IS NULL'));
const whereRaw = whereArray.length ? `WHERE ${whereArray.join(' AND ')} ` : '';
// UPDATE
const updateArray = this._query.update.reduce(this._reducer, []);
const updateRaw = updateArray.length ? `SET ${updateArray.join(', ')} ` : '';
// INSERT
let insertRaw = '';
if (this._query.insert.length) {
const fieldsList = Object.keys(this._query.insert[0]);
const rowsList = this._query.insert.map(el => `(${Object.values(el).join(', ')})`);
insertRaw = `(${fieldsList.join(', ')}) VALUES ${rowsList.join(', ')} `;
}
// GROUP BY
const groupByArray = this._query.groupBy.reduce(this._reducer, []);
const groupByRaw = groupByArray.length ? `GROUP BY ${groupByArray.join(', ')} ` : '';
// ORDER BY
const orderByArray = this._query.orderBy.reduce(this._reducer, []);
const orderByRaw = orderByArray.length ? `ORDER BY ${orderByArray.join(', ')} ` : '';
// LIMIT
const limitRaw = this._query.limit.length ? `LIMIT ${this._query.limit.join(', ')} ` : '';
// OFFSET
const offsetRaw = this._query.offset.length ? `OFFSET ${this._query.offset.join(', ')} ` : '';
return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}${offsetRaw}${insertRaw}`;
}
/**
* @param {string} sql raw SQL query
* @param {object} args
* @param {boolean} args.nest
* @param {boolean} args.details
* @param {boolean} args.split
* @returns {Promise}
* @memberof SQLiteClient
*/
async raw (sql, args) {
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.comments)
sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');// Remove comments
const resultsArr = [];
let paramsArr = [];
const queries = args.split
? sql.split(/((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/gm)
.filter(Boolean)
.map(q => q.trim())
: [sql];
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;
const timeStart = new Date();
let timeStop;
const keysArr = [];
const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => {
(async () => {
let queryResult;
let affectedRows;
let fields;
const detectedTypes = {};
try {
const stmt = connection.prepare(query);
if (stmt.reader) {
queryResult = stmt.all();
fields = stmt.columns();
if (queryResult.length) {
fields.forEach(field => {
detectedTypes[field.name] = typeof queryResult[0][field.name];
});
}
}
else {
const info = queryResult = stmt.run();
affectedRows = info.changes;
}
}
catch (err) {
reject(err);
}
timeStop = new Date();
let remappedFields = fields
? fields.map(field => {
let [parsedType, length] = field.type?.includes('(')
? field.type.replace(')', '').split('(').map(el => {
if (!isNaN(el))
el = +el;
else
el = el.trim();
return el;
})
: [field.type, null];
if ([...TIME, ...DATETIME].includes(parsedType)) {
const firstNotNull = queryResult.find(res => res[field.name] !== null);
if (firstNotNull && firstNotNull[field.name].includes('.'))
length = firstNotNull[field.name].split('.').pop().length;
}
return {
name: field.name,
alias: field.name,
orgName: field.column,
schema: field.database,
table: field.table,
tableAlias: field.table,
orgTable: field.table,
type: field.type !== null ? parsedType : detectedTypes[field.name],
length
};
}).filter(Boolean)
: [];
if (args.details) {
paramsArr = remappedFields.map(field => {
return {
table: field.table,
schema: field.schema
};
}).filter((val, i, arr) => arr.findIndex(el => el.schema === val.schema && el.table === val.table) === i);
for (const paramObj of paramsArr) {
if (!paramObj.table || !paramObj.schema) continue;
try {
const indexes = await this.getTableIndexes(paramObj);
remappedFields = remappedFields.map(field => {
// const detailedField = columns.find(f => f.name === field.name);
const fieldIndex = indexes.find(i => i.column === field.name);
if (field.table === paramObj.table && field.schema === paramObj.schema) {
// if (detailedField) {
// const length = detailedField.numPrecision || detailedField.charLength || detailedField.datePrecision || null;
// field = { ...field, ...detailedField, length };
// }
if (fieldIndex) {
const key = fieldIndex.type === 'PRIMARY' ? 'pri' : fieldIndex.type === 'UNIQUE' ? 'uni' : 'mul';
field = { ...field, key };
};
}
return field;
});
}
catch (err) {
reject(err);
}
}
}
resolve({
duration: timeStop - timeStart,
rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false,
report: affectedRows !== undefined ? { affectedRows } : null,
fields: remappedFields,
keys: keysArr
});
})();
});
resultsArr.push({ rows, report, fields, keys, duration });
}
return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
}
}

View File

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

@ -89,7 +89,7 @@
:type="inputProps().type" :type="inputProps().type"
:disabled="!isChecked" :disabled="!isChecked"
> >
<template v-if="methodData && 'params' in methodData" class="columns"> <template v-if="methodData && 'params' in methodData">
<input <input
v-for="(option, key) in methodData.params" v-for="(option, key) in methodData.params"
:key="key" :key="key"

View File

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

View File

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

View File

@ -170,7 +170,7 @@
KiB KiB
</option> </option>
<option value="rows"> <option value="rows">
{{ $t('word.rows') }} {{ $tc('word.row', 2) }}
</option> </option>
</select> </select>
</div> </div>

View File

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

View File

@ -253,6 +253,7 @@ export default {
font-size: 100%; font-size: 100%;
// color: $primary-color; // color: $primary-color;
opacity: 0.8; opacity: 0.8;
font-weight: 600;
} }
.tile-subtitle { .tile-subtitle {

View File

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

View File

@ -396,7 +396,7 @@ export default {
return locales; return locales;
}, },
hasUpdates () { hasUpdates () {
return ['available', 'downloading', 'downloaded'].includes(this.updateStatus); return ['available', 'downloading', 'downloaded', 'link'].includes(this.updateStatus);
}, },
workspace () { workspace () {
return this.getWorkspace(this.selectedWorkspace); return this.getWorkspace(this.selectedWorkspace);

View File

@ -16,7 +16,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import marked from 'marked'; import { marked } from 'marked';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader';
export default { export default {

View File

@ -29,12 +29,19 @@
{{ $t('message.checkForUpdates') }} {{ $t('message.checkForUpdates') }}
</button> </button>
<button <button
v-if="updateStatus === 'downloaded'" v-else-if="updateStatus === 'downloaded'"
class="btn btn-primary" class="btn btn-primary"
@click="restartToUpdate" @click="restartToUpdate"
> >
{{ $t('message.restartToInstall') }} {{ $t('message.restartToInstall') }}
</button> </button>
<button
v-else-if="updateStatus === 'link'"
class="btn btn-primary"
@click="openOutside('https://antares-sql.app/download.html')"
>
{{ $t('message.goToDownloadPage') }}
</button>
</div> </div>
<div class="form-group mt-4"> <div class="form-group mt-4">
<label class="form-switch d-inline-block disabled" @click.prevent="toggleAllowPrerelease"> <label class="form-switch d-inline-block disabled" @click.prevent="toggleAllowPrerelease">
@ -47,7 +54,7 @@
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { ipcRenderer } from 'electron'; import { ipcRenderer, shell } from 'electron';
export default { export default {
name: 'ModalSettingsUpdate', name: 'ModalSettingsUpdate',
@ -71,6 +78,8 @@ export default {
return this.$t('message.downloadingUpdate'); return this.$t('message.downloadingUpdate');
case 'downloaded': case 'downloaded':
return this.$t('message.updateDownloaded'); return this.$t('message.updateDownloaded');
case 'link':
return this.$t('message.updateAvailable');
default: default:
return this.updateStatus; return this.updateStatus;
} }
@ -80,6 +89,9 @@ export default {
...mapActions({ ...mapActions({
changeAllowPrerelease: 'settings/changeAllowPrerelease' changeAllowPrerelease: 'settings/changeAllowPrerelease'
}), }),
openOutside (link) {
shell.openExternal(link);
},
checkForUpdates () { checkForUpdates () {
ipcRenderer.send('check-for-updates'); ipcRenderer.send('check-for-updates');
}, },

View File

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

View File

@ -11,9 +11,9 @@
<div class="footer-right-elements"> <div class="footer-right-elements">
<ul class="footer-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')"> <li class="footer-element footer-link" @click="openOutside('https://www.paypal.com/paypalme/fabiodistasio')">
<i class="mdi mdi-18px mdi-tree mr-1" /> <i class="mdi mdi-18px mdi-coffee mr-1" />
<small>{{ $t('message.plantATree') }}</small> <small>{{ $t('word.donate') }}</small>
</li> </li>
<li class="footer-element footer-link" @click="openOutside('https://github.com/Fabio286/antares/issues')"> <li class="footer-element footer-link" @click="openOutside('https://github.com/Fabio286/antares/issues')">
<i class="mdi mdi-18px mdi-bug" /> <i class="mdi mdi-18px mdi-bug" />

View File

@ -40,15 +40,13 @@ export default {
} }
}, },
watch: { watch: {
notifications: { 'notifications.length': function (val) {
deep: true, if (val > 0) {
handler: function (notification) { const nUid = this.notifications[0].uid;
if (notification.length) { this.timeouts[nUid] = setTimeout(() => {
this.timeouts[notification[0].uid] = setTimeout(() => { this.removeNotification(nUid);
this.removeNotification(notification[0].uid); delete this.timeouts[nUid];
delete this.timeouts[notification.uid]; }, this.notificationsTimeout * 1000);
}, this.notificationsTimeout * 1000);
}
} }
} }
}, },
@ -63,11 +61,14 @@ export default {
} }
}, },
rearmTimeouts () { rearmTimeouts () {
const delay = 50;
let i = this.notifications.length * delay;
for (const notification of this.notifications) { for (const notification of this.notifications) {
this.timeouts[notification.uid] = setTimeout(() => { this.timeouts[notification.uid] = setTimeout(() => {
this.removeNotification(notification.uid); this.removeNotification(notification.uid);
delete this.timeouts[notification.uid]; delete this.timeouts[notification.uid];
}, this.notificationsTimeout * 1000); }, (this.notificationsTimeout * 1000) + i);
i = i > delay ? i - delay : 0;
} }
} }
} }

View File

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

View File

@ -92,7 +92,7 @@ export default {
} }
}, },
hasUpdates () { hasUpdates () {
return ['available', 'downloading', 'downloaded'].includes(this.updateStatus); return ['available', 'downloading', 'downloaded', 'link'].includes(this.updateStatus);
} }
}, },
methods: { methods: {
@ -200,7 +200,7 @@ export default {
} }
&::before { &::before {
content: ''; content: "";
height: 0; height: 0;
width: 3px; width: 3px;
transition: height 0.2s; transition: height 0.2s;

View File

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

View File

@ -11,6 +11,7 @@
<a class="tab-link">{{ $t('word.general') }}</a> <a class="tab-link">{{ $t('word.general') }}</a>
</li> </li>
<li <li
v-if="customizations.sslConnection"
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'ssl'}" :class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')" @click="selectTab('ssl')"
@ -18,6 +19,7 @@
<a class="tab-link">{{ $t('word.ssl') }}</a> <a class="tab-link">{{ $t('word.ssl') }}</a>
</li> </li>
<li <li
v-if="customizations.sshConnection"
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'ssh'}" :class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')" @click="selectTab('ssh')"
@ -49,25 +51,17 @@
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<select v-model="connection.client" class="form-select"> <select v-model="connection.client" class="form-select">
<option value="mysql"> <option
MySQL v-for="client in clients"
:key="client.slug"
:value="client.slug"
>
{{ client.name }}
</option> </option>
<option value="maria">
MariaDB
</option>
<option value="pg">
PostgreSQL
</option>
<!-- <option value="mssql">
Microsoft SQL
</option>
<option value="oracledb">
Oracle DB
</option> -->
</select> </select>
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label> <label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div> </div>
@ -79,7 +73,20 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="connection.databasePath"
:message="$t('word.browse')"
@clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')"
/>
</div>
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label> <label class="form-label">{{ $t('word.port') }}</label>
</div> </div>
@ -105,7 +112,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label> <label class="form-label">{{ $t('word.user') }}</label>
</div> </div>
@ -118,7 +125,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label> <label class="form-label">{{ $t('word.password') }}</label>
</div> </div>
@ -144,7 +151,15 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="customizations.readOnlyMode" class="form-group columns">
<div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="connection.readonly" type="checkbox"><i class="form-icon" /> {{ $t('message.readOnlyMode') }}
</label>
</div>
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12" /> <div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
@ -369,15 +384,23 @@ export default {
}, },
data () { data () {
return { return {
clients: [
{ name: 'MySQL', slug: 'mysql' },
{ name: 'MariaDB', slug: 'maria' },
{ name: 'PostgreSQL', slug: 'pg' },
{ name: 'SQLite', slug: 'sqlite' }
],
connection: { connection: {
name: '', name: '',
client: 'mysql', client: 'mysql',
host: '127.0.0.1', host: '127.0.0.1',
database: null, database: null,
databasePath: '',
port: null, port: null,
user: null, user: null,
password: '', password: '',
ask: false, ask: false,
readonly: false,
uid: uidGen('C'), uid: uidGen('C'),
ssl: false, ssl: false,
cert: '', cert: '',

View File

@ -11,6 +11,7 @@
<a class="tab-link">{{ $t('word.general') }}</a> <a class="tab-link">{{ $t('word.general') }}</a>
</li> </li>
<li <li
v-if="customizations.sslConnection"
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'ssl'}" :class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')" @click="selectTab('ssl')"
@ -18,6 +19,7 @@
<a class="tab-link">{{ $t('word.ssl') }}</a> <a class="tab-link">{{ $t('word.ssl') }}</a>
</li> </li>
<li <li
v-if="customizations.sshConnection"
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'ssh'}" :class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')" @click="selectTab('ssh')"
@ -49,19 +51,17 @@
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<select v-model="localConnection.client" class="form-select"> <select v-model="localConnection.client" class="form-select">
<option value="mysql"> <option
MySQL v-for="client in clients"
</option> :key="client.slug"
<option value="maria"> :value="client.slug"
MariaDB >
</option> {{ client.name }}
<option value="pg">
PostgreSQL
</option> </option>
</select> </select>
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label> <label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div> </div>
@ -73,7 +73,20 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="localConnection.databasePath"
:message="$t('word.browse')"
@clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')"
/>
</div>
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label> <label class="form-label">{{ $t('word.port') }}</label>
</div> </div>
@ -99,7 +112,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label> <label class="form-label">{{ $t('word.user') }}</label>
</div> </div>
@ -112,7 +125,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label> <label class="form-label">{{ $t('word.password') }}</label>
</div> </div>
@ -138,7 +151,15 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="customizations.readOnlyMode" class="form-group columns">
<div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="localConnection.readonly" type="checkbox"><i class="form-icon" /> {{ $t('message.readOnlyMode') }}
</label>
</div>
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12" /> <div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
@ -374,6 +395,12 @@ export default {
}, },
data () { data () {
return { return {
clients: [
{ name: 'MySQL', slug: 'mysql' },
{ name: 'MariaDB', slug: 'maria' },
{ name: 'PostgreSQL', slug: 'pg' },
{ name: 'SQLite', slug: 'sqlite' }
],
isConnecting: false, isConnecting: false,
isTesting: false, isTesting: false,
isAsking: false, isAsking: false,
@ -383,7 +410,7 @@ export default {
}, },
computed: { computed: {
customizations () { customizations () {
return customizations[this.connection.client]; return customizations[this.localConnection.client];
}, },
isBusy () { isBusy () {
return this.isConnecting || this.isTesting; return this.isConnecting || this.isTesting;

View File

@ -13,6 +13,7 @@
<span class="workspace-explorebar-title">{{ connectionName }}</span> <span class="workspace-explorebar-title">{{ connectionName }}</span>
<span v-if="workspace.connectionStatus === 'connected'" class="workspace-explorebar-tools"> <span v-if="workspace.connectionStatus === 'connected'" class="workspace-explorebar-tools">
<i <i
v-if="customizations.schemas"
class="mdi mdi-18px mdi-database-plus c-hand mr-2" class="mdi mdi-18px mdi-database-plus c-hand mr-2"
:title="$t('message.createNewSchema')" :title="$t('message.createNewSchema')"
@click="showNewDBModal" @click="showNewDBModal"

View File

@ -10,6 +10,30 @@
> >
<span class="d-flex"><i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ $t('word.run') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ $t('word.run') }}</span>
</div> </div>
<div
v-if="selectedMisc.type === 'trigger' && customizations.triggerEnableDisable"
class="context-element"
@click="toggleTrigger"
>
<span v-if="!selectedMisc.enabled" class="d-flex">
<i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ $t('word.enable') }}
</span>
<span v-else class="d-flex">
<i class="mdi mdi-18px mdi-pause text-light pr-1" /> {{ $t('word.disable') }}
</span>
</div>
<div
v-if="selectedMisc.type === 'scheduler'"
class="context-element"
@click="toggleScheduler"
>
<span v-if="!selectedMisc.enabled" class="d-flex">
<i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ $t('word.enable') }}
</span>
<span v-else class="d-flex">
<i class="mdi mdi-18px mdi-pause text-light pr-1" /> {{ $t('word.disable') }}
</span>
</div>
<div class="context-element" @click="showDeleteModal"> <div class="context-element" @click="showDeleteModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-table-remove text-light pr-1" /> {{ $t('word.delete') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-table-remove text-light pr-1" /> {{ $t('word.delete') }}</span>
</div> </div>
@ -18,17 +42,17 @@
@confirm="deleteMisc" @confirm="deleteMisc"
@hide="hideDeleteModal" @hide="hideDeleteModal"
> >
<template slot="header"> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-delete mr-1" /> <i class="mdi mdi-24px mdi-delete mr-1" />
<span class="cut-text">{{ deleteMessage }}</span> <span class="cut-text">{{ deleteMessage }}</span>
</div> </div>
</template> </template>
<div slot="body"> <template #body>
<div class="mb-2"> <div class="mb-2">
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedMisc.name }}</b>"? {{ $t('message.deleteCorfirm') }} "<b>{{ selectedMisc.name }}</b>"?
</div> </div>
</div> </template>
</ConfirmModal> </ConfirmModal>
<ModalAskParameters <ModalAskParameters
v-if="isAskingParameters" v-if="isAskingParameters"
@ -78,6 +102,9 @@ export default {
workspace () { workspace () {
return this.getWorkspace(this.selectedWorkspace); return this.getWorkspace(this.selectedWorkspace);
}, },
customizations () {
return this.getWorkspace(this.selectedWorkspace).customizations;
},
deleteMessage () { deleteMessage () {
switch (this.selectedMisc.type) { switch (this.selectedMisc.type) {
case 'trigger': case 'trigger':
@ -98,6 +125,8 @@ export default {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification', addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs', changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
addLoadingElement: 'workspaces/addLoadingElement',
removeLoadingElement: 'workspaces/removeLoadingElement',
removeTabs: 'workspaces/removeTabs', removeTabs: 'workspaces/removeTabs',
newTab: 'workspaces/newTab' newTab: 'workspaces/newTab'
}), }),
@ -273,6 +302,68 @@ export default {
this.newTab({ uid: this.workspace.uid, content: sql, type: 'query', autorun: true }); this.newTab({ uid: this.workspace.uid, content: sql, type: 'query', autorun: true });
this.closeContext(); this.closeContext();
},
async toggleTrigger () {
this.addLoadingElement({
name: this.selectedMisc.name,
schema: this.selectedSchema,
type: 'trigger'
});
try {
const { status, response } = await Triggers.toggleTrigger({
uid: this.selectedWorkspace,
schema: this.selectedSchema,
trigger: this.selectedMisc.name,
enabled: this.selectedMisc.enabled
});
if (status !== 'success')
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.removeLoadingElement({
name: this.selectedMisc.name,
schema: this.selectedSchema,
type: 'trigger'
});
this.closeContext();
this.$emit('reload');
},
async toggleScheduler () {
this.addLoadingElement({
name: this.selectedMisc.name,
schema: this.selectedSchema,
type: 'scheduler'
});
try {
const { status, response } = await Schedulers.toggleScheduler({
uid: this.selectedWorkspace,
schema: this.selectedSchema,
scheduler: this.selectedMisc.name,
enabled: this.selectedMisc.enabled
});
if (status !== 'success')
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.removeLoadingElement({
name: this.selectedMisc.name,
schema: this.selectedSchema,
type: 'scheduler'
});
this.closeContext();
this.$emit('reload');
} }
} }
}; };

View File

@ -9,7 +9,16 @@
<div v-if="isLoading" class="icon loading" /> <div v-if="isLoading" class="icon loading" />
<i v-else class="icon mdi mdi-18px mdi-chevron-right" /> <i v-else class="icon mdi mdi-18px mdi-chevron-right" />
<i class="database-icon mdi mdi-18px mdi-database mr-1" /> <i class="database-icon mdi mdi-18px mdi-database mr-1" />
<span>{{ database.name }}</span> <div class="">
<span>{{ database.name }}</span>
<div
v-if="database.size"
class="schema-size tooltip tooltip-left mr-1"
:data-tooltip="formatBytes(database.size)"
>
<i class="mdi mdi-information-outline pr-2" />
</div>
</div>
</summary> </summary>
<div class="accordion-body"> <div class="accordion-body">
<div class="database-tables"> <div class="database-tables">
@ -34,7 +43,7 @@
<span v-html="highlightWord(table.name)" /> <span v-html="highlightWord(table.name)" />
</a> </a>
<div <div
v-if="table.type === 'table'" v-if="table.type === 'table' && table.size !== false"
class="table-size tooltip tooltip-left mr-1" class="table-size tooltip tooltip-left mr-1"
:data-tooltip="formatBytes(table.size)" :data-tooltip="formatBytes(table.size)"
> >
@ -68,9 +77,17 @@
@contextmenu.prevent="showMiscContext($event, {...trigger, type: 'trigger'})" @contextmenu.prevent="showMiscContext($event, {...trigger, type: 'trigger'})"
> >
<a class="table-name"> <a class="table-name">
<i class="table-icon mdi mdi-table-cog mdi-18px mr-1" /> <div v-if="checkLoadingStatus(trigger.name, 'trigger')" class="icon loading mr-1" />
<i v-else class="table-icon mdi mdi-table-cog mdi-18px mr-1" />
<span v-html="highlightWord(trigger.name)" /> <span v-html="highlightWord(trigger.name)" />
</a> </a>
<div
v-if="trigger.enabled === false"
class="tooltip tooltip-left disabled-indicator"
:data-tooltip="$t('word.disabled')"
>
<i class="table-icon mdi mdi-pause mdi-18px mr-1" />
</div>
</li> </li>
</ul> </ul>
</div> </div>
@ -204,9 +221,17 @@
@contextmenu.prevent="showMiscContext($event, {...scheduler, type: 'scheduler'})" @contextmenu.prevent="showMiscContext($event, {...scheduler, type: 'scheduler'})"
> >
<a class="table-name"> <a class="table-name">
<i class="table-icon mdi mdi-calendar-clock mdi-18px mr-1" /> <div v-if="checkLoadingStatus(scheduler.name, 'scheduler')" class="icon loading mr-1" />
<i v-else class="table-icon mdi mdi-calendar-clock mdi-18px mr-1" />
<span v-html="highlightWord(scheduler.name)" /> <span v-html="highlightWord(scheduler.name)" />
</a> </a>
<div
v-if="scheduler.enabled === false"
class="tooltip tooltip-left disabled-indicator"
:data-tooltip="$t('word.disabled')"
>
<i class="table-icon mdi mdi-pause mdi-18px mr-1" />
</div>
</li> </li>
</ul> </ul>
</div> </div>
@ -426,6 +451,11 @@ export default {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 2; z-index: 2;
.schema-size {
visibility: hidden;
width: 22.5px;
}
} }
.database-name, .database-name,
@ -471,6 +501,10 @@ export default {
.misc-name { .misc-name {
&:hover { &:hover {
border-radius: $border-radius; border-radius: $border-radius;
.schema-size {
visibility: visible;
}
} }
} }
@ -500,7 +534,9 @@ export default {
} }
} }
.table-size { .schema-size,
.table-size,
.disabled-indicator {
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;

View File

@ -72,7 +72,11 @@
> >
<span class="d-flex"><i class="mdi mdi-18px mdi-database-edit text-light pr-1" /> {{ $t('word.edit') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-database-edit text-light pr-1" /> {{ $t('word.edit') }}</span>
</div> </div>
<div class="context-element" @click="showDeleteModal"> <div
v-if="workspace.customizations.schemaDrop"
class="context-element"
@click="showDeleteModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-remove text-light pr-1" /> {{ $t('word.delete') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-database-remove text-light pr-1" /> {{ $t('word.delete') }}</span>
</div> </div>
@ -81,17 +85,17 @@
@confirm="deleteSchema" @confirm="deleteSchema"
@hide="hideDeleteModal" @hide="hideDeleteModal"
> >
<template slot="header"> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-database-remove mr-1" /> <i class="mdi mdi-24px mdi-database-remove mr-1" />
<span class="cut-text">{{ $t('message.deleteSchema') }}</span> <span class="cut-text">{{ $t('message.deleteSchema') }}</span>
</div> </div>
</template> </template>
<div slot="body"> <template #body>
<div class="mb-2"> <div class="mb-2">
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedSchema }}</b>"? {{ $t('message.deleteCorfirm') }} "<b>{{ selectedSchema }}</b>"?
</div> </div>
</div> </template>
</ConfirmModal> </ConfirmModal>
<ModalEditSchema <ModalEditSchema
v-if="isEditModal" v-if="isEditModal"

View File

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

View File

@ -5,7 +5,7 @@
<div class="workspace-query-buttons"> <div class="workspace-query-buttons">
<button <button
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm"
:disabled="!isChanged" :disabled="!isChanged || !isValid"
:class="{'loading':isSaving}" :class="{'loading':isSaving}"
title="CTRL+S" title="CTRL+S"
@click="saveChanges" @click="saveChanges"
@ -242,6 +242,9 @@ export default {
JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage) || JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage) ||
JSON.stringify(this.originalIndexes) !== JSON.stringify(this.localIndexes) || JSON.stringify(this.originalIndexes) !== JSON.stringify(this.localIndexes) ||
JSON.stringify(this.tableOptions) !== JSON.stringify(this.localOptions); JSON.stringify(this.tableOptions) !== JSON.stringify(this.localOptions);
},
isValid () {
return !!this.localFields.length && !!this.localOptions.name.trim().length;
} }
}, },
watch: { watch: {
@ -287,7 +290,7 @@ export default {
changeBreadcrumbs: 'workspaces/changeBreadcrumbs' changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}), }),
async saveChanges () { async saveChanges () {
if (this.isSaving) return; if (this.isSaving || !this.isValid) return;
this.isSaving = true; this.isSaving = true;
const params = { const params = {
@ -344,7 +347,7 @@ export default {
}, },
addField () { addField () {
this.localFields.push({ this.localFields.push({
_id: uidGen(), _antares_id: uidGen(),
name: `${this.$tc('word.field', 1)}_${++this.newFieldsCounter}`, name: `${this.$tc('word.field', 1)}_${++this.newFieldsCounter}`,
key: '', key: '',
type: this.workspace.dataTypes[0].types[0].name, type: this.workspace.dataTypes[0].types[0].name,
@ -385,8 +388,8 @@ export default {
}); });
}, },
duplicateField (uid) { duplicateField (uid) {
const fieldToClone = Object.assign({}, this.localFields.find(field => field._id === uid)); const fieldToClone = Object.assign({}, this.localFields.find(field => field._antares_id === uid));
fieldToClone._id = uidGen(); fieldToClone._antares_id = uidGen();
fieldToClone.name = `${fieldToClone.name}_copy`; fieldToClone.name = `${fieldToClone.name}_copy`;
fieldToClone.order = this.localFields.length + 1; fieldToClone.order = this.localFields.length + 1;
this.localFields = [...this.localFields, fieldToClone]; this.localFields = [...this.localFields, fieldToClone];
@ -397,11 +400,11 @@ export default {
}, 20); }, 20);
}, },
removeField (uid) { removeField (uid) {
this.localFields = this.localFields.filter(field => field._id !== uid); this.localFields = this.localFields.filter(field => field._antares_id !== uid);
}, },
addNewIndex (payload) { addNewIndex (payload) {
this.localIndexes = [...this.localIndexes, { this.localIndexes = [...this.localIndexes, {
_id: uidGen(), _antares_id: uidGen(),
name: payload.index === 'PRIMARY' ? 'PRIMARY' : payload.field, name: payload.index === 'PRIMARY' ? 'PRIMARY' : payload.field,
fields: [payload.field], fields: [payload.field],
type: payload.index, type: payload.index,
@ -413,7 +416,7 @@ export default {
}, },
addToIndex (payload) { addToIndex (payload) {
this.localIndexes = this.localIndexes.map(index => { this.localIndexes = this.localIndexes.map(index => {
if (index._id === payload.index) index.fields.push(payload.field); if (index._antares_id === payload.index) index.fields.push(payload.field);
return index; return index;
}); });
}, },

View File

@ -373,7 +373,7 @@ export default {
this.originalFunction = response; this.originalFunction = response;
this.originalFunction.parameters = [...this.originalFunction.parameters.map(param => { this.originalFunction.parameters = [...this.originalFunction.parameters.map(param => {
param._id = uidGen(); param._antares_id = uidGen();
return param; return param;
})]; })];

View File

@ -6,13 +6,13 @@
@confirm="confirmParametersChange" @confirm="confirmParametersChange"
@hide="$emit('hide')" @hide="$emit('hide')"
> >
<template :slot="'header'"> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" /> <i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span class="cut-text">{{ $t('word.parameters') }} "{{ func }}"</span> <span class="cut-text">{{ $t('word.parameters') }} "{{ func }}"</span>
</div> </div>
</template> </template>
<div :slot="'body'"> <template #body>
<div class="columns col-gapless"> <div class="columns col-gapless">
<div class="column col-5"> <div class="column col-5">
<div class="panel" :style="{ height: modalInnerHeight + 'px'}"> <div class="panel" :style="{ height: modalInnerHeight + 'px'}">
@ -36,10 +36,10 @@
<div ref="parametersPanel" class="panel-body p-0 pr-1"> <div ref="parametersPanel" class="panel-body p-0 pr-1">
<div <div
v-for="param in parametersProxy" v-for="param in parametersProxy"
:key="param._id" :key="param._antares_id"
class="tile tile-centered c-hand mb-1 p-1" class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-element': selectedParam === param._id}" :class="{'selected-element': selectedParam === param._antares_id}"
@click="selectParameter($event, param._id)" @click="selectParameter($event, param._antares_id)"
> >
<div class="tile-icon"> <div class="tile-icon">
<div> <div>
@ -56,7 +56,7 @@
<button <button
class="btn btn-link remove-field p-0 mr-2" class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')" :title="$t('word.delete')"
@click.prevent="removeParameter(param._id)" @click.prevent="removeParameter(param._antares_id)"
> >
<i class="mdi mdi-close" /> <i class="mdi mdi-close" />
</button> </button>
@ -167,7 +167,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </template>
</ConfirmModal> </ConfirmModal>
</template> </template>
@ -196,7 +196,7 @@ export default {
}, },
computed: { computed: {
selectedParamObj () { selectedParamObj () {
return this.parametersProxy.find(param => param._id === this.selectedParam); return this.parametersProxy.find(param => param._antares_id === this.selectedParam);
}, },
isChanged () { isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy); return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
@ -237,10 +237,11 @@ export default {
this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom)); this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
}, },
addParameter () { addParameter () {
const newUid = uidGen();
this.parametersProxy = [...this.parametersProxy, { this.parametersProxy = [...this.parametersProxy, {
_id: uidGen(), _antares_id: newUid,
name: `Param${this.i++}`, name: `param${this.i++}`,
type: 'INT', type: this.workspace.dataTypes[0].types[0].name,
context: 'IN', context: 'IN',
length: '' length: ''
}]; }];
@ -250,12 +251,13 @@ export default {
setTimeout(() => { setTimeout(() => {
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60; this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60;
this.selectedParam = newUid;
}, 20); }, 20);
}, },
removeParameter (uid) { removeParameter (uid) {
this.parametersProxy = this.parametersProxy.filter(param => param._id !== uid); this.parametersProxy = this.parametersProxy.filter(param => param._antares_id !== uid);
if (this.selectedParam === name && this.parametersProxy.length) if (this.parametersProxy.length && this.selectedParam === uid)
this.resetSelectedID(); this.resetSelectedID();
}, },
clearChanges () { clearChanges () {
@ -266,7 +268,7 @@ export default {
this.resetSelectedID(); this.resetSelectedID();
}, },
resetSelectedID () { resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._id : ''; this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._antares_id : '';
} }
} }
}; };

View File

@ -318,7 +318,7 @@ export default {
this.originalRoutine = response; this.originalRoutine = response;
this.originalRoutine.parameters = [...this.originalRoutine.parameters.map(param => { this.originalRoutine.parameters = [...this.originalRoutine.parameters.map(param => {
param._id = uidGen(); param._antares_id = uidGen();
return param; return param;
})]; })];

View File

@ -6,13 +6,13 @@
@confirm="confirmParametersChange" @confirm="confirmParametersChange"
@hide="$emit('hide')" @hide="$emit('hide')"
> >
<template :slot="'header'"> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" /> <i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span class="cut-text">{{ $t('word.parameters') }} "{{ routine }}"</span> <span class="cut-text">{{ $t('word.parameters') }} "{{ routine }}"</span>
</div> </div>
</template> </template>
<div :slot="'body'"> <template #body>
<div class="columns col-gapless"> <div class="columns col-gapless">
<div class="column col-5"> <div class="column col-5">
<div class="panel" :style="{ height: modalInnerHeight + 'px'}"> <div class="panel" :style="{ height: modalInnerHeight + 'px'}">
@ -36,10 +36,10 @@
<div ref="parametersPanel" class="panel-body p-0 pr-1"> <div ref="parametersPanel" class="panel-body p-0 pr-1">
<div <div
v-for="param in parametersProxy" v-for="param in parametersProxy"
:key="param._id" :key="param._antares_id"
class="tile tile-centered c-hand mb-1 p-1" class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-element': selectedParam === param._id}" :class="{'selected-element': selectedParam === param._antares_id}"
@click="selectParameter($event, param._id)" @click="selectParameter($event, param._antares_id)"
> >
<div class="tile-icon"> <div class="tile-icon">
<div> <div>
@ -56,7 +56,7 @@
<button <button
class="btn btn-link remove-field p-0 mr-2" class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')" :title="$t('word.delete')"
@click.prevent="removeParameter(param._id)" @click.prevent="removeParameter(param._antares_id)"
> >
<i class="mdi mdi-close" /> <i class="mdi mdi-close" />
</button> </button>
@ -167,7 +167,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </template>
</ConfirmModal> </ConfirmModal>
</template> </template>
@ -196,7 +196,7 @@ export default {
}, },
computed: { computed: {
selectedParamObj () { selectedParamObj () {
return this.parametersProxy.find(param => param._id === this.selectedParam); return this.parametersProxy.find(param => param._antares_id === this.selectedParam);
}, },
isChanged () { isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy); return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
@ -237,8 +237,9 @@ export default {
this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom)); this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
}, },
addParameter () { addParameter () {
const newUid = uidGen();
this.parametersProxy = [...this.parametersProxy, { this.parametersProxy = [...this.parametersProxy, {
_id: uidGen(), _antares_id: newUid,
name: `param${this.i++}`, name: `param${this.i++}`,
type: this.workspace.dataTypes[0].types[0].name, type: this.workspace.dataTypes[0].types[0].name,
context: 'IN', context: 'IN',
@ -250,12 +251,13 @@ export default {
setTimeout(() => { setTimeout(() => {
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60; this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60;
this.selectedParam = newUid;
}, 20); }, 20);
}, },
removeParameter (uid) { removeParameter (uid) {
this.parametersProxy = this.parametersProxy.filter(param => param._id !== uid); this.parametersProxy = this.parametersProxy.filter(param => param._antares_id !== uid);
if (this.selectedParam === name && this.parametersProxy.length) if (this.parametersProxy.length && this.selectedParam === uid)
this.resetSelectedID(); this.resetSelectedID();
}, },
clearChanges () { clearChanges () {
@ -266,7 +268,7 @@ export default {
this.resetSelectedID(); this.resetSelectedID();
}, },
resetSelectedID () { resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._id : ''; this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._antares_id : '';
} }
} }
}; };

View File

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

View File

@ -342,7 +342,7 @@ export default {
field.default = `'${field.default}'`; field.default = `'${field.default}'`;
} }
return { ...field, _id: uidGen() }; return { ...field, _antares_id: uidGen() };
}); });
this.localFields = JSON.parse(JSON.stringify(this.originalFields)); this.localFields = JSON.parse(JSON.stringify(this.originalFields));
} }
@ -365,7 +365,7 @@ export default {
this.originalIndexes = Object.keys(indexesObj).map(index => { this.originalIndexes = Object.keys(indexesObj).map(index => {
return { return {
_id: uidGen(), _antares_id: uidGen(),
name: index, name: index,
fields: indexesObj[index].map(field => field.column), fields: indexesObj[index].map(field => field.column),
type: indexesObj[index][0].type, type: indexesObj[index][0].type,
@ -391,7 +391,7 @@ export default {
if (status === 'success') { if (status === 'success') {
this.originalKeyUsage = response.map(foreign => { this.originalKeyUsage = response.map(foreign => {
return { return {
_id: uidGen(), _antares_id: uidGen(),
...foreign ...foreign
}; };
}); });
@ -411,25 +411,25 @@ export default {
this.isSaving = true; this.isSaving = true;
// FIELDS // FIELDS
const originalIDs = this.originalFields.reduce((acc, curr) => [...acc, curr._id], []); const originalIDs = this.originalFields.reduce((acc, curr) => [...acc, curr._antares_id], []);
const localIDs = this.localFields.reduce((acc, curr) => [...acc, curr._id], []); const localIDs = this.localFields.reduce((acc, curr) => [...acc, curr._antares_id], []);
// Fields Additions // Fields Additions
const additions = this.localFields.filter((field, i) => !originalIDs.includes(field._id)).map(field => { const additions = this.localFields.filter((field, i) => !originalIDs.includes(field._antares_id)).map(field => {
const lI = this.localFields.findIndex(localField => localField._id === field._id); const lI = this.localFields.findIndex(localField => localField._antares_id === field._antares_id);
const after = lI > 0 ? this.localFields[lI - 1].name : false; const after = lI > 0 ? this.localFields[lI - 1].name : false;
return { ...field, after }; return { ...field, after };
}); });
// Fields Deletions // Fields Deletions
const deletions = this.originalFields.filter(field => !localIDs.includes(field._id)); const deletions = this.originalFields.filter(field => !localIDs.includes(field._antares_id));
// Fields Changes // Fields Changes
const changes = []; const changes = [];
this.originalFields.forEach((originalField, oI) => { this.originalFields.forEach((originalField, oI) => {
const lI = this.localFields.findIndex(localField => localField._id === originalField._id); const lI = this.localFields.findIndex(localField => localField._antares_id === originalField._antares_id);
const originalSibling = oI > 0 ? this.originalFields[oI - 1]._id : false; const originalSibling = oI > 0 ? this.originalFields[oI - 1]._antares_id : false;
const localSibling = lI > 0 ? this.localFields[lI - 1]._id : false; const localSibling = lI > 0 ? this.localFields[lI - 1]._antares_id : false;
const after = lI > 0 ? this.localFields[lI - 1].name : false; const after = lI > 0 ? this.localFields[lI - 1].name : false;
const orgName = originalField.name; const orgName = originalField.name;
@ -450,15 +450,15 @@ export default {
changes: [], changes: [],
deletions: [] deletions: []
}; };
const originalIndexIDs = this.originalIndexes.reduce((acc, curr) => [...acc, curr._id], []); const originalIndexIDs = this.originalIndexes.reduce((acc, curr) => [...acc, curr._antares_id], []);
const localIndexIDs = this.localIndexes.reduce((acc, curr) => [...acc, curr._id], []); const localIndexIDs = this.localIndexes.reduce((acc, curr) => [...acc, curr._antares_id], []);
// Index Additions // Index Additions
indexChanges.additions = this.localIndexes.filter(index => !originalIndexIDs.includes(index._id)); indexChanges.additions = this.localIndexes.filter(index => !originalIndexIDs.includes(index._antares_id));
// Index Changes // Index Changes
this.originalIndexes.forEach(originalIndex => { this.originalIndexes.forEach(originalIndex => {
const lI = this.localIndexes.findIndex(localIndex => localIndex._id === originalIndex._id); const lI = this.localIndexes.findIndex(localIndex => localIndex._antares_id === originalIndex._antares_id);
if (JSON.stringify(originalIndex) !== JSON.stringify(this.localIndexes[lI])) { if (JSON.stringify(originalIndex) !== JSON.stringify(this.localIndexes[lI])) {
if (this.localIndexes[lI]) { if (this.localIndexes[lI]) {
indexChanges.changes.push({ indexChanges.changes.push({
@ -471,7 +471,7 @@ export default {
}); });
// Index Deletions // Index Deletions
indexChanges.deletions = this.originalIndexes.filter(index => !localIndexIDs.includes(index._id)); indexChanges.deletions = this.originalIndexes.filter(index => !localIndexIDs.includes(index._antares_id));
// FOREIGN KEYS // FOREIGN KEYS
const foreignChanges = { const foreignChanges = {
@ -479,15 +479,15 @@ export default {
changes: [], changes: [],
deletions: [] deletions: []
}; };
const originalForeignIDs = this.originalKeyUsage.reduce((acc, curr) => [...acc, curr._id], []); const originalForeignIDs = this.originalKeyUsage.reduce((acc, curr) => [...acc, curr._antares_id], []);
const localForeignIDs = this.localKeyUsage.reduce((acc, curr) => [...acc, curr._id], []); const localForeignIDs = this.localKeyUsage.reduce((acc, curr) => [...acc, curr._antares_id], []);
// Foreigns Additions // Foreigns Additions
foreignChanges.additions = this.localKeyUsage.filter(foreign => !originalForeignIDs.includes(foreign._id)); foreignChanges.additions = this.localKeyUsage.filter(foreign => !originalForeignIDs.includes(foreign._antares_id));
// Foreigns Changes // Foreigns Changes
this.originalKeyUsage.forEach(originalForeign => { this.originalKeyUsage.forEach(originalForeign => {
const lI = this.localKeyUsage.findIndex(localForeign => localForeign._id === originalForeign._id); const lI = this.localKeyUsage.findIndex(localForeign => localForeign._antares_id === originalForeign._antares_id);
if (JSON.stringify(originalForeign) !== JSON.stringify(this.localKeyUsage[lI])) { if (JSON.stringify(originalForeign) !== JSON.stringify(this.localKeyUsage[lI])) {
if (this.localKeyUsage[lI]) { if (this.localKeyUsage[lI]) {
foreignChanges.changes.push({ foreignChanges.changes.push({
@ -499,13 +499,19 @@ export default {
}); });
// Foreigns Deletions // Foreigns Deletions
foreignChanges.deletions = this.originalKeyUsage.filter(foreign => !localForeignIDs.includes(foreign._id)); foreignChanges.deletions = this.originalKeyUsage.filter(foreign => !localForeignIDs.includes(foreign._antares_id));
// ALTER // ALTER
const params = { const params = {
uid: this.connection.uid, uid: this.connection.uid,
schema: this.schema, schema: this.schema,
table: this.table, table: this.table,
tableStructure: {
name: this.localOptions.name,
fields: this.localFields,
foreigns: this.localKeyUsage,
indexes: this.localIndexes
},
additions, additions,
changes, changes,
deletions, deletions,
@ -555,7 +561,7 @@ export default {
}, },
addField () { addField () {
this.localFields.push({ this.localFields.push({
_id: uidGen(), _antares_id: uidGen(),
name: `${this.$tc('word.field', 1)}_${++this.newFieldsCounter}`, name: `${this.$tc('word.field', 1)}_${++this.newFieldsCounter}`,
key: '', key: '',
type: this.workspace.dataTypes[0].types[0].name, type: this.workspace.dataTypes[0].types[0].name,
@ -597,8 +603,8 @@ export default {
}); });
}, },
duplicateField (uid) { duplicateField (uid) {
const fieldToClone = Object.assign({}, this.localFields.find(field => field._id === uid)); const fieldToClone = Object.assign({}, this.localFields.find(field => field._antares_id === uid));
fieldToClone._id = uidGen(); fieldToClone._antares_id = uidGen();
fieldToClone.name = `${fieldToClone.name}_copy`; fieldToClone.name = `${fieldToClone.name}_copy`;
fieldToClone.order = this.localFields.length + 1; fieldToClone.order = this.localFields.length + 1;
this.localFields = [...this.localFields, fieldToClone]; this.localFields = [...this.localFields, fieldToClone];
@ -609,11 +615,19 @@ export default {
}, 20); }, 20);
}, },
removeField (uid) { removeField (uid) {
this.localFields = this.localFields.filter(field => field._id !== 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) { addNewIndex (payload) {
this.localIndexes = [...this.localIndexes, { this.localIndexes = [...this.localIndexes, {
_id: uidGen(), _antares_id: uidGen(),
name: payload.index === 'PRIMARY' ? 'PRIMARY' : payload.field, name: payload.index === 'PRIMARY' ? 'PRIMARY' : payload.field,
fields: [payload.field], fields: [payload.field],
type: payload.index, type: payload.index,
@ -625,7 +639,7 @@ export default {
}, },
addToIndex (payload) { addToIndex (payload) {
this.localIndexes = this.localIndexes.map(index => { this.localIndexes = this.localIndexes.map(index => {
if (index._id === payload.index) index.fields.push(payload.field); if (index._antares_id === payload.index) index.fields.push(payload.field);
return index; return index;
}); });
}, },

View File

@ -27,7 +27,7 @@
:key="index.name" :key="index.name"
class="context-element" class="context-element"
:class="{'disabled': index.fields.includes(selectedField.name)}" :class="{'disabled': index.fields.includes(selectedField.name)}"
@click="addToIndex(index._id)" @click="addToIndex(index._antares_id)"
> >
<span class="d-flex"><i class="mdi mdi-18px mdi-key column-key pr-1" :class="`key-${index.type}`" /> {{ index.name }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-key column-key pr-1" :class="`key-${index.type}`" /> {{ index.name }}</span>
</div> </div>

View File

@ -109,7 +109,7 @@
> >
<TableRow <TableRow
v-for="row in fields" v-for="row in fields"
:key="row._id" :key="row._antares_id"
:row="row" :row="row"
:indexes="getIndexes(row.name)" :indexes="getIndexes(row.name)"
:foreigns="getForeigns(row.name)" :foreigns="getForeigns(row.name)"
@ -217,15 +217,15 @@ export default {
this.resizeResults(); this.resizeResults();
}, },
contextMenu (event, uid) { contextMenu (event, uid) {
this.selectedField = this.fields.find(field => field._id === uid); this.selectedField = this.fields.find(field => field._antares_id === uid);
this.contextEvent = event; this.contextEvent = event;
this.isContext = true; this.isContext = true;
}, },
duplicateField () { duplicateField () {
this.$emit('duplicate-field', this.selectedField._id); this.$emit('duplicate-field', this.selectedField._antares_id);
}, },
removeField () { removeField () {
this.$emit('remove-field', this.selectedField._id); this.$emit('remove-field', this.selectedField._antares_id);
}, },
getIndexes (field) { getIndexes (field) {
return this.indexes.reduce((acc, curr) => { return this.indexes.reduce((acc, curr) => {

View File

@ -6,13 +6,13 @@
@confirm="confirmForeignsChange" @confirm="confirmForeignsChange"
@hide="$emit('hide')" @hide="$emit('hide')"
> >
<template :slot="'header'"> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-key-link mr-1" /> <i class="mdi mdi-24px mdi-key-link mr-1" />
<span class="cut-text">{{ $t('word.foreignKeys') }} "{{ table }}"</span> <span class="cut-text">{{ $t('word.foreignKeys') }} "{{ table }}"</span>
</div> </div>
</template> </template>
<div :slot="'body'"> <template #body>
<div class="columns col-gapless"> <div class="columns col-gapless">
<div class="column col-5"> <div class="column col-5">
<div class="panel" :style="{ height: modalInnerHeight + 'px'}"> <div class="panel" :style="{ height: modalInnerHeight + 'px'}">
@ -36,10 +36,10 @@
<div ref="indexesPanel" class="panel-body p-0 pr-1"> <div ref="indexesPanel" class="panel-body p-0 pr-1">
<div <div
v-for="foreign in foreignProxy" v-for="foreign in foreignProxy"
:key="foreign._id" :key="foreign._antares_id"
class="tile tile-centered c-hand mb-1 p-1" class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-element': selectedForeignID === foreign._id}" :class="{'selected-element': selectedForeignID === foreign._antares_id}"
@click="selectForeign($event, foreign._id)" @click="selectForeign($event, foreign._antares_id)"
> >
<div class="tile-icon"> <div class="tile-icon">
<div> <div>
@ -68,7 +68,7 @@
<button <button
class="btn btn-link remove-field p-0 mr-2" class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')" :title="$t('word.delete')"
@click.prevent="removeIndex(foreign._id)" @click.prevent="removeIndex(foreign._antares_id)"
> >
<i class="mdi mdi-close" /> <i class="mdi mdi-close" />
</button> </button>
@ -197,7 +197,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </template>
</ConfirmModal> </ConfirmModal>
</template> </template>
@ -238,7 +238,7 @@ export default {
}, },
computed: { computed: {
selectedForeignObj () { selectedForeignObj () {
return this.foreignProxy.find(foreign => foreign._id === this.selectedForeignID); return this.foreignProxy.find(foreign => foreign._antares_id === this.selectedForeignID);
}, },
isChanged () { isChanged () {
return JSON.stringify(this.localKeyUsage) !== JSON.stringify(this.foreignProxy); return JSON.stringify(this.localKeyUsage) !== JSON.stringify(this.foreignProxy);
@ -288,8 +288,8 @@ export default {
}, },
addForeign () { addForeign () {
this.foreignProxy = [...this.foreignProxy, { this.foreignProxy = [...this.foreignProxy, {
_id: uidGen(), _antares_id: uidGen(),
constraintName: `FK_${this.foreignProxy.length + 1}`, constraintName: `FK_${uidGen()}`,
refSchema: this.schema, refSchema: this.schema,
table: this.table, table: this.table,
refTable: '', refTable: '',
@ -307,19 +307,19 @@ export default {
}, 20); }, 20);
}, },
removeIndex (id) { removeIndex (id) {
this.foreignProxy = this.foreignProxy.filter(foreign => foreign._id !== id); this.foreignProxy = this.foreignProxy.filter(foreign => foreign._antares_id !== id);
if (this.selectedForeignID === id && this.foreignProxy.length) if (this.selectedForeignID === id && this.foreignProxy.length)
this.resetSelectedID(); this.resetSelectedID();
}, },
clearChanges () { clearChanges () {
this.foreignProxy = JSON.parse(JSON.stringify(this.localKeyUsage)); this.foreignProxy = JSON.parse(JSON.stringify(this.localKeyUsage));
if (!this.foreignProxy.some(foreign => foreign._id === this.selectedForeignID)) if (!this.foreignProxy.some(foreign => foreign._antares_id === this.selectedForeignID))
this.resetSelectedID(); this.resetSelectedID();
}, },
toggleField (field) { toggleField (field) {
this.foreignProxy = this.foreignProxy.map(foreign => { this.foreignProxy = this.foreignProxy.map(foreign => {
if (foreign._id === this.selectedForeignID) if (foreign._antares_id === this.selectedForeignID)
foreign.field = field; foreign.field = field;
return foreign; return foreign;
@ -327,14 +327,14 @@ export default {
}, },
toggleRefField (field) { toggleRefField (field) {
this.foreignProxy = this.foreignProxy.map(foreign => { this.foreignProxy = this.foreignProxy.map(foreign => {
if (foreign._id === this.selectedForeignID) if (foreign._antares_id === this.selectedForeignID)
foreign.refField = field; foreign.refField = field;
return foreign; return foreign;
}); });
}, },
resetSelectedID () { resetSelectedID () {
this.selectedForeignID = this.foreignProxy.length ? this.foreignProxy[0]._id : ''; this.selectedForeignID = this.foreignProxy.length ? this.foreignProxy[0]._antares_id : '';
}, },
async getRefFields () { async getRefFields () {
if (!this.selectedForeignObj.refTable) return; if (!this.selectedForeignObj.refTable) return;

View File

@ -6,13 +6,13 @@
@confirm="confirmIndexesChange" @confirm="confirmIndexesChange"
@hide="$emit('hide')" @hide="$emit('hide')"
> >
<template :slot="'header'"> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-key mdi-rotate-45 mr-1" /> <i class="mdi mdi-24px mdi-key mdi-rotate-45 mr-1" />
<span class="cut-text">{{ $t('word.indexes') }} "{{ table }}"</span> <span class="cut-text">{{ $t('word.indexes') }} "{{ table }}"</span>
</div> </div>
</template> </template>
<div :slot="'body'"> <template #body>
<div class="columns col-gapless"> <div class="columns col-gapless">
<div class="column col-5"> <div class="column col-5">
<div class="panel" :style="{ height: modalInnerHeight + 'px'}"> <div class="panel" :style="{ height: modalInnerHeight + 'px'}">
@ -36,10 +36,10 @@
<div ref="indexesPanel" class="panel-body p-0 pr-1"> <div ref="indexesPanel" class="panel-body p-0 pr-1">
<div <div
v-for="index in indexesProxy" v-for="index in indexesProxy"
:key="index._id" :key="index._antares_id"
class="tile tile-centered c-hand mb-1 p-1" class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-element': selectedIndexID === index._id}" :class="{'selected-element': selectedIndexID === index._antares_id}"
@click="selectIndex($event, index._id)" @click="selectIndex($event, index._antares_id)"
> >
<div class="tile-icon"> <div class="tile-icon">
<div> <div>
@ -56,7 +56,7 @@
<button <button
class="btn btn-link remove-field p-0 mr-2" class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')" :title="$t('word.delete')"
@click.prevent="removeIndex(index._id)" @click.prevent="removeIndex(index._antares_id)"
> >
<i class="mdi mdi-close" /> <i class="mdi mdi-close" />
</button> </button>
@ -133,7 +133,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </template>
</ConfirmModal> </ConfirmModal>
</template> </template>
@ -163,7 +163,7 @@ export default {
}, },
computed: { computed: {
selectedIndexObj () { selectedIndexObj () {
return this.indexesProxy.find(index => index._id === this.selectedIndexID); return this.indexesProxy.find(index => index._antares_id === this.selectedIndexID);
}, },
isChanged () { isChanged () {
return JSON.stringify(this.localIndexes) !== JSON.stringify(this.indexesProxy); return JSON.stringify(this.localIndexes) !== JSON.stringify(this.indexesProxy);
@ -200,7 +200,7 @@ export default {
}, },
addIndex () { addIndex () {
this.indexesProxy = [...this.indexesProxy, { this.indexesProxy = [...this.indexesProxy, {
_id: uidGen(), _antares_id: uidGen(),
name: 'NEW_INDEX', name: 'NEW_INDEX',
fields: [], fields: [],
type: 'INDEX', type: 'INDEX',
@ -218,19 +218,19 @@ export default {
}, 20); }, 20);
}, },
removeIndex (id) { removeIndex (id) {
this.indexesProxy = this.indexesProxy.filter(index => index._id !== id); this.indexesProxy = this.indexesProxy.filter(index => index._antares_id !== id);
if (this.selectedIndexID === id && this.indexesProxy.length) if (this.selectedIndexID === id && this.indexesProxy.length)
this.resetSelectedID(); this.resetSelectedID();
}, },
clearChanges () { clearChanges () {
this.indexesProxy = JSON.parse(JSON.stringify(this.localIndexes)); this.indexesProxy = JSON.parse(JSON.stringify(this.localIndexes));
if (!this.indexesProxy.some(index => index._id === this.selectedIndexID)) if (!this.indexesProxy.some(index => index._antares_id === this.selectedIndexID))
this.resetSelectedID(); this.resetSelectedID();
}, },
toggleField (field) { toggleField (field) {
this.indexesProxy = this.indexesProxy.map(index => { this.indexesProxy = this.indexesProxy.map(index => {
if (index._id === this.selectedIndexID) { if (index._antares_id === this.selectedIndexID) {
if (index.fields.includes(field)) if (index.fields.includes(field))
index.fields = index.fields.filter(f => f !== field); index.fields = index.fields.filter(f => f !== field);
else else
@ -240,7 +240,7 @@ export default {
}); });
}, },
resetSelectedID () { resetSelectedID () {
this.selectedIndexID = this.indexesProxy.length ? this.indexesProxy[0]._id : ''; this.selectedIndexID = this.indexesProxy.length ? this.indexesProxy[0]._antares_id : '';
} }
} }
}; };

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="tr" @contextmenu.prevent="$emit('contextmenu', $event, localRow._id)"> <div class="tr" @contextmenu.prevent="$emit('contextmenu', $event, localRow._antares_id)">
<div class="td p-0" tabindex="0"> <div class="td p-0" tabindex="0">
<div :class="customizations.sortableFields ? 'row-draggable' : 'text-center'"> <div :class="customizations.sortableFields ? 'row-draggable' : 'text-center'">
<i v-if="customizations.sortableFields" class="mdi mdi-drag-horizontal row-draggable-icon" /> <i v-if="customizations.sortableFields" class="mdi mdi-drag-horizontal row-draggable-icon" />
@ -99,6 +99,9 @@
<span v-if="localRow.enumValues"> <span v-if="localRow.enumValues">
{{ localRow.enumValues }} {{ localRow.enumValues }}
</span> </span>
<span v-else-if="localRow.numScale">
{{ localLength }}, {{ localRow.numScale }}
</span>
<span v-else> <span v-else>
{{ localLength }} {{ localLength }}
</span> </span>
@ -112,6 +115,16 @@
class="editable-field form-input input-sm px-1" class="editable-field form-input input-sm px-1"
@blur="editOFF" @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 <input
v-else v-else
ref="editField" ref="editField"
@ -230,13 +243,13 @@
@confirm="editOFF" @confirm="editOFF"
@hide="hideDefaultModal" @hide="hideDefaultModal"
> >
<template :slot="'header'"> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> <i class="mdi mdi-24px mdi-playlist-edit mr-1" />
<span class="cut-text">{{ $t('word.default') }} "{{ row.name }}"</span> <span class="cut-text">{{ $t('word.default') }} "{{ row.name }}"</span>
</div> </div>
</template> </template>
<div :slot="'body'"> <template #body>
<form class="form-horizontal"> <form class="form-horizontal">
<div class="mb-2"> <div class="mb-2">
<label class="form-radio form-inline"> <label class="form-radio form-inline">
@ -324,7 +337,7 @@
</div> </div>
</div> </div>
</form> </form>
</div> </template>
</ConfirmModal> </ConfirmModal>
</div> </div>
</template> </template>
@ -367,7 +380,8 @@ export default {
getWorkspace: 'workspaces/getWorkspace' getWorkspace: 'workspaces/getWorkspace'
}), }),
localLength () { localLength () {
return this.localRow.numLength || this.localRow.charLength || this.localRow.datePrecision || this.localRow.numPrecision || 0; const localLength = this.localRow.numLength || this.localRow.charLength || this.localRow.datePrecision || this.localRow.numPrecision || 0;
return localLength === true ? null : localLength;
}, },
fieldType () { fieldType () {
const fieldType = this.dataTypes.reduce((acc, group) => [...acc, ...group.types], []).filter(type => const fieldType = this.dataTypes.reduce((acc, group) => [...acc, ...group.types], []).filter(type =>
@ -391,7 +405,7 @@ export default {
return this.indexes.some(index => ['PRIMARY', 'UNIQUE'].includes(index.type)); return this.indexes.some(index => ['PRIMARY', 'UNIQUE'].includes(index.type));
}, },
isNullable () { isNullable () {
return !this.indexes.some(index => ['PRIMARY'].includes(index.type)); return this.customizations.nullablePrimary || !this.indexes.some(index => ['PRIMARY'].includes(index.type));
}, },
isInDataTypes () { isInDataTypes () {
let typeNames = []; let typeNames = [];
@ -479,6 +493,11 @@ export default {
this.editingContent = this.localRow.enumValues; this.editingContent = this.localRow.enumValues;
this.originalContent = 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 { else {
this.editingContent = content; this.editingContent = content;
this.originalContent = content; this.originalContent = content;
@ -501,10 +520,17 @@ export default {
if (this.editingField === 'name') if (this.editingField === 'name')
this.$emit('rename-field', { old: this.localRow[this.editingField], new: this.editingContent }); 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) { if (this.editingField === 'type' && this.editingContent !== this.originalContent) {
this.localRow.numLength = null; this.localRow.numLength = null;
this.localRow.numScale = null;
this.localRow.charLength = null; this.localRow.charLength = null;
this.localRow.datePrecision = null; this.localRow.datePrecision = null;
this.localRow.enumValues = ''; this.localRow.enumValues = '';
@ -559,6 +585,15 @@ export default {
this.originalContent = null; this.originalContent = null;
this.editingField = 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 () { hideDefaultModal () {
this.isDefaultModal = false; this.isDefaultModal = false;
} }

View File

@ -238,7 +238,7 @@ export default {
this.originalFunction = response; this.originalFunction = response;
this.originalFunction.parameters = [...this.originalFunction.parameters.map(param => { this.originalFunction.parameters = [...this.originalFunction.parameters.map(param => {
param._id = uidGen(); param._antares_id = uidGen();
return param; return param;
})]; })];

View File

@ -4,6 +4,7 @@
class="workspace-query-tab column col-12 columns col-gapless no-outline p-0" class="workspace-query-tab column col-12 columns col-gapless no-outline p-0"
tabindex="0" tabindex="0"
@keydown.116="runQuery(query)" @keydown.116="runQuery(query)"
@keydown.75="killTabQuery"
@keydown.ctrl.alt.87="clear" @keydown.ctrl.alt.87="clear"
@keydown.ctrl.66="beautify" @keydown.ctrl.66="beautify"
@keydown.ctrl.71="openHistoryModal" @keydown.ctrl.71="openHistoryModal"
@ -22,15 +23,46 @@
<div ref="resizer" class="query-area-resizer" /> <div ref="resizer" class="query-area-resizer" />
<div class="workspace-query-runner-footer"> <div class="workspace-query-runner-footer">
<div class="workspace-query-buttons"> <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 <button
class="btn btn-primary btn-sm" v-if="!autocommit"
class="btn btn-dark btn-sm"
:class="{'loading':isQuering}" :class="{'loading':isQuering}"
:disabled="!query" @click="commitTab()"
title="F5"
@click="runQuery(query)"
> >
<i class="mdi mdi-24px mdi-play pr-1" /> <i class="mdi mdi-24px mdi-cube-send pr-1" />
<span>{{ $t('word.run') }}</span> <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>
<button <button
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
@ -64,7 +96,7 @@
</button> </button>
<div class="dropdown table-dropdown pr-2"> <div class="dropdown table-dropdown pr-2">
<button <button
:disabled="!results.length || isQuering" :disabled="!hasResults || isQuering"
class="btn btn-dark btn-sm dropdown-toggle mr-0 pr-0" class="btn btn-dark btn-sm dropdown-toggle mr-0 pr-0"
tabindex="0" tabindex="0"
> >
@ -81,6 +113,17 @@
</li> </li>
</ul> </ul>
</div> </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>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div <div
@ -90,11 +133,19 @@
> >
<i class="mdi mdi-timer-sand mdi-rotate-180 pr-1" /> <b>{{ durationsCount / 1000 }}s</b> <i class="mdi mdi-timer-sand mdi-rotate-180 pr-1" /> <b>{{ durationsCount / 1000 }}s</b>
</div> </div>
<div v-if="resultsCount"> <div
{{ $t('word.results') }}: <b>{{ resultsCount.toLocaleString() }}</b> v-if="resultsCount"
class="d-flex"
:title="$t('word.results')"
>
<i class="mdi mdi-equal pr-1" /> <b>{{ resultsCount.toLocaleString() }}</b>
</div> </div>
<div v-if="affectedCount"> <div
{{ $t('message.affectedRows') }}: <b>{{ affectedCount }}</b> v-if="hasAffected"
class="d-flex"
:title="$t('message.affectedRows')"
>
<i class="mdi mdi-target pr-1" /> <b>{{ affectedCount }}</b>
</div> </div>
<div class="input-group" :title="$t('word.schema')"> <div class="input-group" :title="$t('word.schema')">
<i class="input-group-addon addon-sm mdi mdi-24px mdi-database" /> <i class="input-group-addon addon-sm mdi mdi-24px mdi-database" />
@ -110,7 +161,7 @@
</div> </div>
</div> </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"> <div class="workspace-query-results p-relative column col-12">
<BaseLoader v-if="isQuering" /> <BaseLoader v-if="isQuering" />
<WorkspaceTabQueryTable <WorkspaceTabQueryTable
@ -166,11 +217,14 @@ export default {
query: '', query: '',
lastQuery: '', lastQuery: '',
isQuering: false, isQuering: false,
isCancelling: false,
showCancel: false,
autocommit: true,
results: [], results: [],
selectedSchema: null, selectedSchema: null,
resultsCount: 0, resultsCount: 0,
durationsCount: 0, durationsCount: 0,
affectedCount: 0, affectedCount: null,
editorHeight: 200, editorHeight: 200,
isHistoryOpen: false isHistoryOpen: false
}; };
@ -184,6 +238,9 @@ export default {
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
}, },
tabUid () {
return this.$vnode.key;
},
breadcrumbsSchema () { breadcrumbsSchema () {
return this.workspace.breadcrumbs.schema || null; return this.workspace.breadcrumbs.schema || null;
}, },
@ -198,12 +255,23 @@ export default {
}, },
history () { history () {
return this.getHistoryByWorkspace(this.connection.uid) || []; 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: { watch: {
isSelected (val) { isSelected (val) {
if (val) if (val) {
this.changeBreadcrumbs({ schema: this.selectedSchema, query: `Query #${this.tab.index}` }); this.changeBreadcrumbs({ schema: this.selectedSchema, query: `Query #${this.tab.index}` });
setTimeout(() => {
if (this.$refs.queryEditor)
this.$refs.queryEditor.editor.focus();
}, 0);
}
}, },
selectedSchema () { selectedSchema () {
this.changeBreadcrumbs({ schema: this.selectedSchema, query: `Query #${this.tab.index}` }); this.changeBreadcrumbs({ schema: this.selectedSchema, query: `Query #${this.tab.index}` });
@ -230,12 +298,18 @@ export default {
}, },
beforeDestroy () { beforeDestroy () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
const params = {
uid: this.connection.uid,
tabUid: this.tab.uid
};
Schema.destroyConnectionToCommit(params);
}, },
methods: { methods: {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification', addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs', changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
updateTabContent: 'workspaces/updateTabContent', updateTabContent: 'workspaces/updateTabContent',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
saveHistory: 'history/saveHistory' saveHistory: 'history/saveHistory'
}), }),
async runQuery (query) { async runQuery (query) {
@ -248,6 +322,8 @@ export default {
const params = { const params = {
uid: this.connection.uid, uid: this.connection.uid,
schema: this.selectedSchema, schema: this.selectedSchema,
tabUid: this.tab.uid,
autocommit: this.autocommit,
query query
}; };
@ -255,9 +331,14 @@ export default {
if (status === 'success') { if (status === 'success') {
this.results = Array.isArray(response) ? response : [response]; this.results = Array.isArray(response) ? response : [response];
this.resultsCount += this.results.reduce((acc, curr) => acc + (curr.rows ? curr.rows.length : 0), 0); this.resultsCount = this.results.reduce((acc, curr) => acc + (curr.rows ? curr.rows.length : 0), 0);
this.durationsCount += this.results.reduce((acc, curr) => acc + curr.duration, 0); this.durationsCount = this.results.reduce((acc, curr) => acc + curr.duration, 0);
this.affectedCount += this.results.reduce((acc, curr) => acc + (curr.report ? curr.report.affectedRows : 0), 0); this.affectedCount = this.results
.filter(result => result.report !== null)
.reduce((acc, curr) => {
if (acc === null) acc = 0;
return acc + (curr.report ? curr.report.affectedRows : 0);
}, null);
this.updateTabContent({ this.updateTabContent({
uid: this.connection.uid, uid: this.connection.uid,
@ -267,6 +348,8 @@ export default {
content: query content: query
}); });
this.saveHistory(params); this.saveHistory(params);
if (!this.autocommit)
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: true });
} }
else else
this.addNotification({ status: 'error', message: response }); this.addNotification({ status: 'error', message: response });
@ -278,6 +361,29 @@ export default {
this.isQuering = false; this.isQuering = false;
this.lastQuery = query; 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 () { reloadTable () {
this.runQuery(this.lastQuery); this.runQuery(this.lastQuery);
}, },
@ -285,7 +391,7 @@ export default {
this.results = []; this.results = [];
this.resultsCount = 0; this.resultsCount = 0;
this.durationsCount = 0; this.durationsCount = 0;
this.affectedCount = 0; this.affectedCount = null;
}, },
resize (e) { resize (e) {
const el = this.$refs.queryEditor.$el; const el = this.$refs.queryEditor.$el;
@ -341,6 +447,42 @@ export default {
}, },
downloadTable (format) { downloadTable (format) {
this.$refs.queryTable.downloadTable(format, `${this.tab.type}-${this.tab.index}`); 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;
} }
} }
}; };
@ -369,10 +511,12 @@ export default {
.workspace-query-runner-footer { .workspace-query-runner-footer {
display: flex; display: flex;
flex-wrap: wrap;
row-gap: 0.4rem;
justify-content: space-between; justify-content: space-between;
padding: 0.3rem 0.6rem 0.4rem; padding: 0.3rem 0.6rem 0.4rem;
align-items: center; align-items: center;
height: 42px; min-height: 42px;
.workspace-query-buttons, .workspace-query-buttons,
.workspace-query-info { .workspace-query-info {

View File

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

View File

@ -5,7 +5,7 @@
tabindex="0" tabindex="0"
:style="{'height': resultsSize+'px'}" :style="{'height': resultsSize+'px'}"
@keyup.46="showDeleteConfirmModal" @keyup.46="showDeleteConfirmModal"
@keydown.ctrl.65="selectAllRows" @keydown.ctrl.65="selectAllRows($event)"
@keydown.esc="deselectRows" @keydown.esc="deselectRows"
> >
<TableContext <TableContext
@ -53,6 +53,7 @@
class="mdi sort-icon" class="mdi sort-icon"
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'" :class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'"
/> />
<i v-else class="mdi sort-icon mdi-minus d-invisible" />
</div> </div>
</div> </div>
</div> </div>
@ -62,7 +63,7 @@
v-if="resultsWithRows[resultsetIndex] && resultsWithRows[resultsetIndex].rows" v-if="resultsWithRows[resultsetIndex] && resultsWithRows[resultsetIndex].rows"
ref="resultTable" ref="resultTable"
:items="sortedResults" :items="sortedResults"
:item-height="22" :item-height="rowHeight"
class="tbody" class="tbody"
:visible-height="resultsSize" :visible-height="resultsSize"
:scroll-element="scrollElement" :scroll-element="scrollElement"
@ -70,13 +71,14 @@
<template slot-scope="{ items }"> <template slot-scope="{ items }">
<WorkspaceTabQueryTableRow <WorkspaceTabQueryTableRow
v-for="row in items" v-for="row in items"
:key="row._id" :key="row._antares_id"
:item-height="rowHeight"
:row="row" :row="row"
:fields="fieldsObj" :fields="fieldsObj"
:key-usage="keyUsage" :key-usage="keyUsage"
:element-type="elementType" :element-type="elementType"
:class="{'selected': selectedRows.includes(row._id)}" :class="{'selected': selectedRows.includes(row._antares_id)}"
@select-row="selectRow($event, row._id)" @select-row="selectRow($event, row._antares_id)"
@update-field="updateField($event, row)" @update-field="updateField($event, row)"
@contextmenu="contextMenu" @contextmenu="contextMenu"
/> />
@ -89,17 +91,17 @@
@confirm="deleteSelected" @confirm="deleteSelected"
@hide="hideDeleteConfirmModal" @hide="hideDeleteConfirmModal"
> >
<template :slot="'header'"> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-delete mr-1" /> <i class="mdi mdi-24px mdi-delete mr-1" />
<span class="cut-text">{{ $tc('message.deleteRows', selectedRows.length) }}</span> <span class="cut-text">{{ $tc('message.deleteRows', selectedRows.length) }}</span>
</div> </div>
</template> </template>
<div :slot="'body'"> <template #body>
<div class="mb-2"> <div class="mb-2">
{{ $tc('message.confirmToDeleteRows', selectedRows.length) }} {{ $tc('message.confirmToDeleteRows', selectedRows.length) }}
</div> </div>
</div> </template>
</ConfirmModal> </ConfirmModal>
</div> </div>
</template> </template>
@ -142,7 +144,8 @@ export default {
currentSort: '', currentSort: '',
currentSortDir: 'asc', currentSortDir: 'asc',
resultsetIndex: 0, resultsetIndex: 0,
scrollElement: null scrollElement: null,
rowHeight: 23
}; };
}, },
computed: { computed: {
@ -196,7 +199,7 @@ export default {
if (this.sortedResults.length) { if (this.sortedResults.length) {
const fieldsObj = {}; const fieldsObj = {};
for (const key in this.sortedResults[0]) { for (const key in this.sortedResults[0]) {
if (key === '_id') continue; if (key === '_antares_id') continue;
const fieldObj = this.fields.find(field => { const fieldObj = this.fields.find(field => {
let fieldNames = [ let fieldNames = [
@ -242,6 +245,11 @@ export default {
if (this.$refs.tableWrapper) if (this.$refs.tableWrapper)
this.scrollElement = 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 () { mounted () {
window.addEventListener('resize', this.resizeResults); window.addEventListener('resize', this.resizeResults);
@ -272,6 +280,7 @@ export default {
fieldLength (field) { fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null; if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
else if (TEXT.includes(field.type)) return field.charLength; else if (TEXT.includes(field.type)) return field.charLength;
else if (field.numScale) return `${field.numPrecision}, ${field.numScale}`;
return field.length; return field.length;
}, },
keyName (key) { keyName (key) {
@ -310,7 +319,7 @@ export default {
setLocalResults () { setLocalResults () {
this.localResults = this.resultsWithRows[this.resultsetIndex] && this.resultsWithRows[this.resultsetIndex].rows this.localResults = this.resultsWithRows[this.resultsetIndex] && this.resultsWithRows[this.resultsetIndex].rows
? this.resultsWithRows[this.resultsetIndex].rows.map(item => { ? this.resultsWithRows[this.resultsetIndex].rows.map(item => {
return { ...item, _id: uidGen() }; return { ...item, _antares_id: uidGen() };
}) })
: []; : [];
}, },
@ -330,7 +339,7 @@ export default {
this.resizeResults(); this.resizeResults();
}, },
updateField (payload, row) { updateField (payload, row) {
const orgRow = this.localResults.find(lr => lr._id === row._id); const orgRow = this.localResults.find(lr => lr._antares_id === row._antares_id);
Object.keys(orgRow).forEach(key => { // remap the row Object.keys(orgRow).forEach(key => { // remap the row
if (orgRow[key] instanceof Date && moment(orgRow[key]).isValid()) { // if datetime if (orgRow[key] instanceof Date && moment(orgRow[key]).isValid()) { // if datetime
@ -367,8 +376,8 @@ export default {
}, },
deleteSelected () { deleteSelected () {
this.closeContext(); this.closeContext();
const rows = JSON.parse(JSON.stringify(this.localResults)).filter(row => this.selectedRows.includes(row._id)).map(row => { const rows = JSON.parse(JSON.stringify(this.localResults)).filter(row => this.selectedRows.includes(row._antares_id)).map(row => {
delete row._id; delete row._antares_id;
return row; return row;
}); });
@ -381,7 +390,7 @@ export default {
this.$emit('delete-selected', params); this.$emit('delete-selected', params);
}, },
setNull () { setNull () {
const row = this.localResults.find(row => this.selectedRows.includes(row._id)); const row = this.localResults.find(row => this.selectedRows.includes(row._antares_id));
const params = { const params = {
primary: this.primaryField.name, primary: this.primaryField.name,
@ -396,19 +405,22 @@ export default {
this.$emit('update-field', params); this.$emit('update-field', params);
}, },
copyCell () { copyCell () {
const row = this.localResults.find(row => this.selectedRows.includes(row._id)); const row = this.localResults.find(row => this.selectedRows.includes(row._antares_id));
const cellName = Object.keys(row).find(prop => [ const cellName = Object.keys(row).find(prop => [
this.selectedCell.field, this.selectedCell.field,
this.selectedCell.orgField,
`${this.fields[0].table}.${this.selectedCell.field}`, `${this.fields[0].table}.${this.selectedCell.field}`,
`${this.fields[0].tableAlias}.${this.selectedCell.field}` `${this.fields[0].tableAlias}.${this.selectedCell.field}`
].includes(prop)); ].includes(prop));
const valueToCopy = row[cellName]; let valueToCopy = row[cellName];
if (typeof valueToCopy === 'object')
valueToCopy = JSON.stringify(valueToCopy);
navigator.clipboard.writeText(valueToCopy); navigator.clipboard.writeText(valueToCopy);
}, },
copyRow () { copyRow () {
const row = this.localResults.find(row => this.selectedRows.includes(row._id)); const row = this.localResults.find(row => this.selectedRows.includes(row._antares_id));
const rowToCopy = JSON.parse(JSON.stringify(row)); const rowToCopy = JSON.parse(JSON.stringify(row));
delete rowToCopy._id; delete rowToCopy._antares_id;
navigator.clipboard.writeText(JSON.stringify(rowToCopy)); navigator.clipboard.writeText(JSON.stringify(rowToCopy));
}, },
applyUpdate (params) { applyUpdate (params) {
@ -435,24 +447,26 @@ export default {
this.selectedRows.push(row); this.selectedRows.push(row);
else { else {
const lastID = this.selectedRows.slice(-1)[0]; const lastID = this.selectedRows.slice(-1)[0];
const lastIndex = this.sortedResults.findIndex(el => el._id === lastID); const lastIndex = this.sortedResults.findIndex(el => el._antares_id === lastID);
const clickedIndex = this.sortedResults.findIndex(el => el._id === row); const clickedIndex = this.sortedResults.findIndex(el => el._antares_id === row);
if (lastIndex > clickedIndex) { if (lastIndex > clickedIndex) {
for (let i = clickedIndex; i < lastIndex; i++) for (let i = clickedIndex; i < lastIndex; i++)
this.selectedRows.push(this.sortedResults[i]._id); this.selectedRows.push(this.sortedResults[i]._antares_id);
} }
else if (lastIndex < clickedIndex) { else if (lastIndex < clickedIndex) {
for (let i = clickedIndex; i > lastIndex; i--) for (let i = clickedIndex; i > lastIndex; i--)
this.selectedRows.push(this.sortedResults[i]._id); this.selectedRows.push(this.sortedResults[i]._antares_id);
} }
} }
} }
else else
this.selectedRows = [row]; this.selectedRows = [row];
}, },
selectAllRows () { selectAllRows (e) {
if (e.target.classList.contains('editable-field')) return;
this.selectedRows = this.localResults.reduce((acc, curr) => { this.selectedRows = this.localResults.reduce((acc, curr) => {
acc.push(curr._id); acc.push(curr._antares_id);
return acc; return acc;
}, []); }, []);
}, },
@ -501,7 +515,7 @@ export default {
if (!this.sortedResults) return; if (!this.sortedResults) return;
const rows = JSON.parse(JSON.stringify(this.sortedResults)).map(row => { const rows = JSON.parse(JSON.stringify(this.sortedResults)).map(row => {
delete row._id; delete row._antares_id;
return row; return row;
}); });

View File

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

View File

@ -1,14 +1,18 @@
<template> <template>
<div class="tr" @click="selectRow($event, row._id)"> <div
class="tr"
:style="{height: itemHeight+'px'}"
@click="selectRow($event, row._antares_id)"
>
<div <div
v-for="(col, cKey) in row" v-for="(col, cKey) in row"
v-show="cKey !== '_id'" v-show="cKey !== '_antares_id'"
:key="cKey" :key="cKey"
class="td p-0" class="td p-0"
tabindex="0" tabindex="0"
@contextmenu.prevent="openContext($event, { id: row._id, field: cKey })" @contextmenu.prevent="openContext($event, { id: row._antares_id, orgField: cKey })"
> >
<template v-if="cKey !== '_id'"> <template v-if="cKey !== '_antares_id'">
<span <span
v-if="!isInlineEditor[cKey] && fields[cKey]" v-if="!isInlineEditor[cKey] && fields[cKey]"
class="cell-content" class="cell-content"
@ -72,12 +76,12 @@
@confirm="editOFF" @confirm="editOFF"
@hide="hideEditorModal" @hide="hideEditorModal"
> >
<template :slot="'header'"> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> <span class="cut-text">{{ $t('word.edit') }} "{{ editingField }}"</span> <i class="mdi mdi-24px mdi-playlist-edit mr-1" /> <span class="cut-text">{{ $t('word.edit') }} "{{ editingField }}"</span>
</div> </div>
</template> </template>
<div :slot="'body'"> <template #body>
<div class="mb-2"> <div class="mb-2">
<div> <div>
<TextEditor <TextEditor
@ -96,23 +100,12 @@
v-model="editorMode" v-model="editorMode"
class="form-select select-sm" class="form-select select-sm"
> >
<option value="text"> <option
TEXT v-for="language in availableLanguages"
</option> :key="language.slug"
<option value="html"> :value="language.slug"
HTML >
</option> {{ language.name }}
<option value="xml">
XML
</option>
<option value="json">
JSON
</option>
<option value="svg">
SVG
</option>
<option value="yaml">
YAML
</option> </option>
</select> </select>
</div> </div>
@ -128,7 +121,22 @@
</div> </div>
</div> </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>
<ConfirmModal <ConfirmModal
v-if="isBlobEditor" v-if="isBlobEditor"
@ -136,13 +144,13 @@
@confirm="editOFF" @confirm="editOFF"
@hide="hideEditorModal" @hide="hideEditorModal"
> >
<template :slot="'header'"> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> <i class="mdi mdi-24px mdi-playlist-edit mr-1" />
<span class="cut-text">{{ $t('word.edit') }} "{{ editingField }}"</span> <span class="cut-text">{{ $t('word.edit') }} "{{ editingField }}"</span>
</div> </div>
</template> </template>
<div :slot="'body'"> <template #body>
<div class="mb-2"> <div class="mb-2">
<transition name="jump-down"> <transition name="jump-down">
<div v-if="contentInfo.size"> <div v-if="contentInfo.size">
@ -182,21 +190,39 @@
> >
</div> </div>
</div> </div>
</div> </template>
</ConfirmModal> </ConfirmModal>
</div> </div>
</template> </template>
<script> <script>
import moment from 'moment'; import moment from 'moment';
import { ModelOperations } from '@vscode/vscode-languagedetection';
import { mimeFromHex } from 'common/libs/mimeFromHex'; import { mimeFromHex } from 'common/libs/mimeFromHex';
import { formatBytes } from 'common/libs/formatBytes'; import { formatBytes } from 'common/libs/formatBytes';
import { bufferToBase64 } from 'common/libs/bufferToBase64'; import { bufferToBase64 } from 'common/libs/bufferToBase64';
import hexToBinary from 'common/libs/hexToBinary'; 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 { VueMaskDirective } from 'v-mask';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor'; import TextEditor from '@/components/BaseTextEditor';
import BaseMap from '@/components/BaseMap';
import ForeignKeySelect from '@/components/ForeignKeySelect'; import ForeignKeySelect from '@/components/ForeignKeySelect';
export default { export default {
@ -204,7 +230,8 @@ export default {
components: { components: {
ConfirmModal, ConfirmModal,
TextEditor, TextEditor,
ForeignKeySelect ForeignKeySelect,
BaseMap
}, },
directives: { directives: {
mask: VueMaskDirective mask: VueMaskDirective
@ -254,13 +281,17 @@ export default {
return val; return val;
} }
return val; if (SPATIAL.includes(type))
return val;
return typeof val === 'object' ? JSON.stringify(val) : val;
} }
}, },
props: { props: {
row: Object, row: Object,
fields: Object, fields: Object,
keyUsage: Array, keyUsage: Array,
itemHeight: Number,
elementType: { type: String, default: 'table' } elementType: { type: String, default: 'table' }
}, },
data () { data () {
@ -268,6 +299,8 @@ export default {
isInlineEditor: {}, isInlineEditor: {},
isTextareaEditor: false, isTextareaEditor: false,
isBlobEditor: false, isBlobEditor: false,
isMapModal: false,
isMultiSpatial: false,
willBeDeleted: false, willBeDeleted: false,
originalContent: null, originalContent: null,
editingContent: null, editingContent: null,
@ -280,7 +313,17 @@ export default {
mime: '', mime: '',
size: null 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: { computed: {
@ -326,6 +369,9 @@ export default {
if (BOOLEAN.includes(this.editingType)) if (BOOLEAN.includes(this.editingType))
return { type: 'boolean', mask: false }; return { type: 'boolean', mask: false };
if (SPATIAL.includes(this.editingType))
return { type: 'map', mask: false };
return { type: 'text', mask: false }; return { type: 'text', mask: false };
}, },
isImage () { isImage () {
@ -362,6 +408,21 @@ export default {
Object.keys(this.fields).forEach(field => { Object.keys(this.fields).forEach(field => {
this.isInlineEditor[field.name] = false; 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: { methods: {
@ -383,7 +444,7 @@ export default {
return bufferToBase64(val); return bufferToBase64(val);
}, },
editON (event, content, field) { editON (event, content, field) {
if (!this.isEditable) return; if (!this.isEditable || this.editingType === 'none') return;
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
@ -399,6 +460,15 @@ export default {
return; 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)) { if (BLOB.includes(type)) {
this.isBlobEditor = true; this.isBlobEditor = true;
this.editingContent = content || ''; this.editingContent = content || '';
@ -470,6 +540,8 @@ export default {
hideEditorModal () { hideEditorModal () {
this.isTextareaEditor = false; this.isTextareaEditor = false;
this.isBlobEditor = false; this.isBlobEditor = false;
this.isMapModal = false;
this.isMultiSpatial = false;
}, },
downloadFile () { downloadFile () {
const downloadLink = document.createElement('a'); const downloadLink = document.createElement('a');
@ -506,7 +578,7 @@ export default {
return this.keyUsage.find(key => key.field === keyName); return this.keyUsage.find(key => key.field === keyName);
}, },
openContext (event, payload) { 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; payload.isEditable = this.isEditable;
this.$emit('contextmenu', event, payload); this.$emit('contextmenu', event, payload);
}, },

View File

@ -85,7 +85,7 @@
@click="showFakerModal" @click="showFakerModal"
> >
<i class="mdi mdi-24px mdi-playlist-plus mr-1" /> <i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span>{{ $t('message.tableFiller') }}</span> <span>{{ $tc('message.insertRow', 2) }}</span>
</button> </button>
<div class="dropdown table-dropdown pr-2"> <div class="dropdown table-dropdown pr-2">
@ -120,7 +120,12 @@
{{ $t('word.results') }}: <b>{{ results[0].rows.length | localeString }}</b> {{ $t('word.results') }}: <b>{{ results[0].rows.length | localeString }}</b>
</div> </div>
<div v-if="hasApproximately || (page > 1 && approximateCount)"> <div v-if="hasApproximately || (page > 1 && approximateCount)">
{{ $t('word.total') }}: <b :title="$t('word.approximately')"> {{ approximateCount | localeString }}</b> {{ $t('word.total') }}: <b
:title="!customizations.tableRealCount ? $t('word.approximately') : ''"
>
<span v-if="!customizations.tableRealCount"></span>
{{ approximateCount | localeString }}
</b>
</div> </div>
<div class="d-flex" :title="$t('word.schema')"> <div class="d-flex" :title="$t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b> <i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
@ -231,6 +236,9 @@ export default {
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
}, },
customizations () {
return this.workspace.customizations;
},
isTable () { isTable () {
return !!this.workspace.breadcrumbs.table; return !!this.workspace.breadcrumbs.table;
}, },

View File

@ -130,9 +130,13 @@ module.exports = {
schedulers: 'Schedulers', schedulers: 'Schedulers',
includes: 'Includes', includes: 'Includes',
drop: 'Drop', drop: 'Drop',
rows: 'Rows',
completed: 'Completed', completed: 'Completed',
aborted: 'Aborted' aborted: 'Aborted',
disabled: 'Disabled',
enable: 'Enable',
disable: 'Disable',
commit: 'Commit',
rollback: 'Rollback'
}, },
message: { message: {
appWelcome: 'Welcome to Antares SQL Client!', appWelcome: 'Welcome to Antares SQL Client!',
@ -265,7 +269,15 @@ module.exports = {
fechingTableExport: 'Fetching {table} data', fechingTableExport: 'Fetching {table} data',
writingTableExport: 'Writing {table} data', writingTableExport: 'Writing {table} data',
checkAllTables: 'Check all tables', checkAllTables: 'Check all tables',
uncheckAllTables: 'Uncheck all tables' uncheckAllTables: 'Uncheck all tables',
goToDownloadPage: 'Go to download page',
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: { faker: {
address: 'Address', address: 'Address',

View File

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

View File

@ -130,9 +130,11 @@ module.exports = {
schedulers: 'Scheduler', schedulers: 'Scheduler',
includes: 'Includi', includes: 'Includi',
drop: 'Drop', drop: 'Drop',
rows: 'Righe',
completed: 'Completato', completed: 'Completato',
aborted: 'Annullato' aborted: 'Annullato',
disabled: 'Disabilitato',
enable: 'Abilita',
disable: 'Disabilita'
}, },
message: { message: {
appWelcome: 'Benvenuto in Antares SQL Client!', appWelcome: 'Benvenuto in Antares SQL Client!',
@ -252,7 +254,22 @@ module.exports = {
fechingTableExport: 'Ricavo i dati {table}', fechingTableExport: 'Ricavo i dati {table}',
writingTableExport: 'Scrittura dati {table}', writingTableExport: 'Scrittura dati {table}',
checkAllTables: 'Seleziona tutte le tabelle', checkAllTables: 'Seleziona tutte le tabelle',
uncheckAllTables: 'Deseleziona tutte le tabelle' uncheckAllTables: 'Deseleziona tutte le tabelle',
runQuery: 'Esegui query',
thereAreNoTableFields: 'Non ci sono campi della tabella',
newTable: 'Nuova tabella',
newView: 'Nuova vista',
newTrigger: 'Nuovo trigger',
newRoutine: 'Nuova routine',
newFunction: 'Nuova funzione',
newScheduler: 'Nuovo scheduler',
newTriggerFunction: 'Nuova funzione di trigger',
thereIsNoQueriesYet: 'Non ci sono ancora query',
searchForQueries: 'Cerca query',
killProcess: 'Uccidi processo',
closeTab: 'Chiudi tab',
goToDownloadPage: 'Vai alla pagina di download',
readOnlyMode: 'Modalità sola lettura'
}, },
faker: { faker: {
address: 'Indirizzo', address: 'Indirizzo',

View File

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

View File

@ -120,7 +120,11 @@ module.exports = {
new: 'Mới', new: 'Mới',
history: 'Lịch sử', history: 'Lịch sử',
select: 'Chọn', 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: { message: {
appWelcome: 'Chào bạn đến với Antares SQL Client!', 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', newTriggerFunction: 'Chức năng kích hoạt mới',
thereIsNoQueriesYet: 'Không có truy vấn nào', thereIsNoQueriesYet: 'Không có truy vấn nào',
searchForQueries: 'Tìm kiếm truy vấn', 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: { faker: {
address: 'Địa chỉ', 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

Before

Width:  |  Height:  |  Size: 1004 B

After

Width:  |  Height:  |  Size: 1004 B

View File

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

View File

@ -17,4 +17,8 @@ export default class {
static createScheduler (params) { static createScheduler (params) {
return ipcRenderer.invoke('create-scheduler', params); return ipcRenderer.invoke('create-scheduler', params);
} }
static toggleScheduler (params) {
return ipcRenderer.invoke('toggle-scheduler', params);
}
} }

View File

@ -46,6 +46,22 @@ export default class {
return ipcRenderer.invoke('kill-process', params); 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) { static useSchema (params) {
return ipcRenderer.invoke('use-schema', params); return ipcRenderer.invoke('use-schema', params);
} }

View File

@ -17,4 +17,8 @@ export default class {
static createTrigger (params) { static createTrigger (params) {
return ipcRenderer.invoke('create-trigger', params); return ipcRenderer.invoke('create-trigger', params);
} }
static toggleTrigger (params) {
return ipcRenderer.invoke('toggle-trigger', params);
}
} }

View File

@ -17,10 +17,12 @@
( (
"char": $string-color, "char": $string-color,
"varchar": $string-color, "varchar": $string-color,
"longvarchar": $string-color,
"text": $string-color, "text": $string-color,
"tinytext": $string-color, "tinytext": $string-color,
"mediumtext": $string-color, "mediumtext": $string-color,
"longtext": $string-color, "longtext": $string-color,
"string": $string-color,
"json": $string-color, "json": $string-color,
"name": $string-color, "name": $string-color,
"character": $string-color, "character": $string-color,
@ -50,6 +52,7 @@
"oid": $number-color, "oid": $number-color,
"xid": $number-color, "xid": $number-color,
"money": $number-color, "money": $number-color,
"number": $number-color,
"datetime": $date-color, "datetime": $date-color,
"date": $date-color, "date": $date-color,
"time": $date-color, "time": $date-color,
@ -70,6 +73,7 @@
"bytea": $blob-color, "bytea": $blob-color,
"enum": $enum-color, "enum": $enum-color,
"set": $enum-color, "set": $enum-color,
"bool": $enum-color,
"boolean": $enum-color, "boolean": $enum-color,
"interval": $array-color, "interval": $array-color,
"array": $array-color, "array": $array-color,
@ -77,6 +81,15 @@
"tsvector": $array-color, "tsvector": $array-color,
"tsquery": $array-color, "tsquery": $array-color,
"pg_node_tree": $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, "aclitem": $array-color,
"unknown": $unknown-color, "unknown": $unknown-color,
) )

View File

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

View File

@ -59,6 +59,34 @@ option:checked {
text-overflow: ellipsis; 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 { .workspace-tabs {
align-content: baseline; align-content: baseline;

View File

@ -142,7 +142,7 @@
code { code {
background-color: #000; background-color: #000;
color: $body-font-color-dark; color: rgba($body-font-color-dark, 0.7);
} }
// Antares // Antares

View File

@ -24,12 +24,24 @@ export default {
getConnections: state => state.connections, getConnections: state => state.connections,
getConnectionName: state => uid => { getConnectionName: state => uid => {
const connection = state.connections.filter(connection => connection.uid === uid)[0]; const connection = state.connections.filter(connection => connection.uid === uid)[0];
if (!connection) return ''; let connectionName = '';
return connection.name
? connection.name if (connection.name)
: connection.ask connectionName = connection.name;
? `${connection.host}:${connection.port}` else if (connection.ask)
: `${connection.user + '@'}${connection.host}:${connection.port}`; connectionName = `${connection.host}:${connection.port}`;
else if (connection.databasePath) {
let string = connection.databasePath.split(/[/\\]+/).pop();
if (string.length >= 30)
string = `...${string.slice(-30)}`;
connectionName = string;
}
else
connectionName = `${connection.user + '@'}${connection.host}:${connection.port}`;
return connectionName;
} }
}, },
mutations: { mutations: {

View File

@ -412,6 +412,11 @@ export default {
indexTypes = require('common/index-types/postgresql'); indexTypes = require('common/index-types/postgresql');
customizations = require('common/customizations/postgresql'); customizations = require('common/customizations/postgresql');
break; break;
case 'sqlite':
dataTypes = require('common/data-types/sqlite');
indexTypes = require('common/index-types/sqlite');
customizations = require('common/customizations/sqlite');
break;
} }
const { status, response: version } = await Schema.getVersion(connection.uid); const { status, response: version } = await Schema.getVersion(connection.uid);

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