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

Compare commits

...

163 Commits

Author SHA1 Message Date
b4b2f3d0cf chore(release): 0.1.8 2021-05-22 16:25:52 +02:00
7a766f04e6 perf: improved the way how field default value are handled 2021-05-22 16:24:19 +02:00
f6b77d1243 build: update dependencies 2021-05-22 16:12:08 +02:00
840241c3cc refactor: better description in foreign key value select 2021-05-20 15:07:51 +02:00
0f10c9e824 fix: internal id not removed before row update 2021-05-20 12:45:49 +02:00
1e37f2a96f fix: unable to add new ENUM fields 2021-05-20 12:38:05 +02:00
0d6137195d fix: unable to delete table rows 2021-05-19 16:55:38 +02:00
d26f168250 build: update build script 2021-05-17 19:08:36 +02:00
5efcad4d3f chore(release): 0.1.7 2021-05-16 21:08:52 +02:00
0a872e7023 build: separate build configurations 2021-05-16 21:03:04 +02:00
0e425a9c6d build: removed linux arm64 build from build process 2021-05-16 19:50:06 +02:00
434711a360 perf(MySQL): improved connections pool handling 2021-05-15 21:47:30 +02:00
854472c7a3 fix: row loses internal id after cell update 2021-05-15 19:33:31 +02:00
9a7cd90d5c build: arm mac (apple silicon) build configuration 2021-05-14 22:24:50 +02:00
026400d242 build: arm linux build configuration 2021-05-14 18:59:58 +02:00
4bd8cbbf6c build: moved to sass from node-sass 2021-05-14 17:27:56 +02:00
1382bc9300 Merge pull request #64 from Fabio286/dependabot/npm_and_yarn/pgsql-ast-parser-8.1.1
build(deps): bump pgsql-ast-parser from 7.2.1 to 8.1.1
2021-05-14 17:21:10 +02:00
ae103e5477 perf(core): increased connection pool size to improve performance 2021-05-14 17:17:37 +02:00
6b0b8b19d7 fix(MySQL): connection loses schema in some conditions 2021-05-14 17:02:27 +02:00
475397ca34 fix: issue with ENUM and SET fields on table filler modal 2021-05-13 20:46:44 +02:00
7a62131cc7 fix: issue with ENUM and SET length when creating a new field 2021-05-13 15:45:54 +02:00
c7663be338 fix: multiple row select on sorted tables not work properly 2021-05-11 18:32:11 +02:00
496490b14a build: update dependencies and build config 2021-05-11 12:00:22 +02:00
dependabot[bot]
672573d8c2 build(deps): bump pgsql-ast-parser from 7.2.1 to 8.1.1
Bumps [pgsql-ast-parser](https://github.com/oguimbal/pgsql-ast-parser) from 7.2.1 to 8.1.1.
- [Release notes](https://github.com/oguimbal/pgsql-ast-parser/releases)
- [Changelog](https://github.com/oguimbal/pgsql-ast-parser/blob/master/changelog.md)
- [Commits](https://github.com/oguimbal/pgsql-ast-parser/compare/7.2.1...8.1.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 06:16:49 +00:00
d923453919 chore(release): 0.1.6 2021-05-08 17:33:25 +02:00
0435279604 build: temporarily disabled Windows builds due issues with dependencies 2021-05-08 17:22:45 +02:00
79cef61fea build: revert to electron-builder@22.9.1 due Windows build issues 2021-05-08 17:13:24 +02:00
dec334737f chore: update dependencies 2021-05-08 16:41:18 +02:00
84c3692e95 chore: icons for appx build 2021-05-08 16:04:37 +02:00
5dc0b0bea4 perf: italian translation updated 2021-05-07 12:11:55 +02:00
19d16e46bb build: update dependencies 2021-05-06 22:55:10 +02:00
3baf6fa173 fix: better detection and handling of field default type 2021-05-06 22:21:42 +02:00
29e2d92b5b fix: no quotes around strings in field default custom value 2021-05-05 17:13:12 +02:00
bebba64d06 feat(MySQL): ENUM and SET fields support, closes #61 2021-05-04 21:50:41 +02:00
9dfe7cca22 fix(UI): data type figure twice on type select 2021-05-03 22:55:39 +02:00
35ef070725 fix: support to mDNS/zeroconf in snap build, closes #58 2021-05-01 12:16:57 +02:00
370ad6a536 chore(release): 0.1.5 2021-04-30 17:37:04 +02:00
5822b3df43 perf(UI): new application icon 2021-04-30 14:14:01 +02:00
5208ec171b fix(MySQL): multiple queries non properly split in some cases 2021-04-29 21:03:32 +02:00
6e332da425 chore: update issue template 2021-04-28 16:56:04 +02:00
bf3367b41d build: minor changes to build and dependencies 2021-04-28 12:11:39 +02:00
ecfb732c26 fix: % character not properly escaped, closes #60 2021-04-28 12:10:43 +02:00
fd4f032a6f build: mssql temporarily removed from dependencies 2021-04-28 11:55:29 +02:00
1b09909126 fix: semicolon inside strings breaks queries, closes #59 2021-04-28 11:50:07 +02:00
04cd806954 chore: update README.md 2021-04-26 10:16:55 +02:00
773cb36ca1 Merge pull request #57 from Fabio286/dependabot/npm_and_yarn/electron-store-8.0.0
build(deps): bump electron-store from 7.0.3 to 8.0.0
2021-04-26 10:12:32 +02:00
ccacd3e2c3 Merge pull request #56 from Fabio286/dependabot/npm_and_yarn/electron-12.0.5
build(deps-dev): bump electron from 11.4.3 to 12.0.5
2021-04-26 10:08:32 +02:00
15948b30c9 refactor: modifications for electron 12 support 2021-04-26 10:07:47 +02:00
3dcb5d4f14 Merge pull request #55 from Fabio286/dependabot/npm_and_yarn/stylelint-config-standard-22.0.0
build(deps-dev): bump stylelint-config-standard from 21.0.0 to 22.0.0
2021-04-26 08:59:35 +02:00
dependabot[bot]
64d93d7c40 build(deps): bump electron-store from 7.0.3 to 8.0.0
Bumps [electron-store](https://github.com/sindresorhus/electron-store) from 7.0.3 to 8.0.0.
- [Release notes](https://github.com/sindresorhus/electron-store/releases)
- [Commits](https://github.com/sindresorhus/electron-store/compare/v7.0.3...v8.0.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-26 06:26:31 +00:00
dependabot[bot]
2d85295093 build(deps-dev): bump electron from 11.4.3 to 12.0.5
Bumps [electron](https://github.com/electron/electron) from 11.4.3 to 12.0.5.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/master/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v11.4.3...v12.0.5)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-26 06:25:55 +00:00
dependabot[bot]
cd58e2d8ca build(deps-dev): bump stylelint-config-standard from 21.0.0 to 22.0.0
Bumps [stylelint-config-standard](https://github.com/stylelint/stylelint-config-standard) from 21.0.0 to 22.0.0.
- [Release notes](https://github.com/stylelint/stylelint-config-standard/releases)
- [Changelog](https://github.com/stylelint/stylelint-config-standard/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint-config-standard/compare/21.0.0...22.0.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-26 06:25:16 +00:00
d87495822e Merge pull request #54 from daeleduardo/master
feat: portugues (Brasil) translation
2021-04-25 11:44:55 +02:00
Daniel Aguiar
8720bcdad6 New Translation: Portugues (Brasil) 2021-04-25 03:53:21 -03:00
1df57dc705 chore(release): 0.1.4 2021-04-22 15:18:07 +02:00
86240fb53c refactor(PostgreSQL): preparing code to support triggers 2021-04-22 15:15:08 +02:00
1d363f755e feat: query results export 2021-04-22 15:08:22 +02:00
0d77aee3eb refactor: Improved pulse animation code 2021-04-22 14:24:34 +02:00
a41cf1ab56 fix: wrong changelog in some cases 2021-04-22 11:35:59 +02:00
5ceddb8e00 perf(UI): improved connection status indicator 2021-04-21 16:41:42 +02:00
16e17b39b6 feat(UI): ctrl+s shortcut to save changes 2021-04-20 17:39:15 +02:00
20cba6ee9b feat(UI): canc press to delete selected rows 2021-04-20 16:30:10 +02:00
9ffd443a66 feat(UI): format and clear queries 2021-04-19 19:15:06 +02:00
f82dbd24dc fix: launch from shortcut of procedures or functions with parameters without name dont works 2021-04-19 15:40:25 +02:00
6eb2977568 fix(UI): data type not listed in selection if not present in global types 2021-04-19 11:07:29 +02:00
cafb65560a chore: update README.md 2021-04-17 12:28:55 +02:00
532d963019 chore: update README.md 2021-04-17 11:28:02 +02:00
1b0a63ff31 chore(release): 0.1.3 2021-04-17 10:36:07 +02:00
c22187c305 perf(UI): improved table fields suggestion in query editor 2021-04-17 10:33:15 +02:00
dcccb544f9 fix(MySQL): invalid JavaScript datetime values not shown 2021-04-16 18:48:56 +02:00
7d2ace9456 fix: field apparently loses index or foreign key on rename in table editor 2021-04-16 17:42:16 +02:00
2584c9b9c2 chore: replaced link for donations with Treedom 2021-04-15 14:55:37 +02:00
a6b75ad0dc fix: approximate row count shown for results less than 1000 2021-04-15 10:13:55 +02:00
90fd9db917 perf(MySQL): improved the way to get routine and functions parameters 2021-04-14 18:06:20 +02:00
c0f54b9514 build: update dependencies 2021-04-14 10:42:00 +02:00
cd31413256 feat(PostgreSQL): functions management 2021-04-13 18:05:03 +02:00
b33199ea59 feat(PostgreSQL): procedure language select 2021-04-12 18:46:35 +02:00
dea5ec7513 chore(release): 0.1.2 2021-04-11 12:39:56 +02:00
be816e8588 perf(UI): improved setting modal rendering 2021-04-11 12:38:50 +02:00
1e938adc5d feat: in-app last release changelog 2021-04-11 12:35:16 +02:00
8735a0c5f9 feat(PostgreSQL): edit timezone in cell editor 2021-04-11 10:55:22 +02:00
3dde1c109e feat(PostgreSQL): procedures management 2021-04-10 20:38:46 +02:00
d0b3e1b1b8 feat(PostgreSQL): support of arrays in table settings 2021-04-09 19:31:41 +02:00
c20bff7bcb fix: deletion of rows from query results 2021-04-08 21:49:38 +02:00
9f5ec0276c fix: no foreign key select when cell value is NULL, closes #50 2021-04-08 18:02:16 +02:00
55932fe115 fix: cell edit doesn't properly use primary or unique index to update if both present, closes #51 2021-04-08 17:47:10 +02:00
d374372e20 fix: wrong datetime conversion when updating a row without an unique key 2021-04-07 15:05:11 +02:00
bb5f44681f fix(UI): white readonly inputs with dark theme 2021-04-07 09:20:11 +02:00
49a4e1cb7b fix(PostgreSQL): issue with selected schema different than public 2021-04-06 12:48:40 +02:00
c2f76e490a chore(release): 0.1.1 2021-04-03 12:21:34 +02:00
e349dd5eab feat: scratchpad to save persistent notes 2021-04-03 12:17:40 +02:00
280697698e feat(UI): light theme 2021-04-03 11:21:58 +02:00
0783f8b57e chore: update README.md 2021-04-02 15:27:29 +02:00
c981244d7a fix(UI): editor theme preview not properly loaded in some cases 2021-04-01 15:26:49 +02:00
dcb135dd01 fix: hide update tab for Windows Store distributions 2021-04-01 14:12:49 +02:00
99f7511c4d feat(PostgreSQL): views management 2021-03-31 16:54:06 +02:00
fe4c8e12b3 feat(PostgreSQL): foreign keys management 2021-03-31 15:57:23 +02:00
21728a663d chore: update README.md 2021-03-31 10:59:19 +02:00
9ca03f4625 feat(PostgreSQL): indexes management 2021-03-30 19:07:04 +02:00
affb7288b0 chore: appx logos 2021-03-30 19:06:48 +02:00
614e0d3275 feat(PostgreSQL): unique keys management 2021-03-29 20:18:44 +02:00
feef5e30ee feat(PostgreSQL): tables addition 2021-03-28 11:55:15 +02:00
82c25711b6 ci: moving to GitHub actions 2021-03-26 18:03:44 +01:00
2ca2988832 chore: appx build configuration 2021-03-26 16:52:49 +01:00
e3f259c6e8 feat(PostgreSQL): table fields edit 2021-03-25 18:33:29 +01:00
e7401cc96e fix: fields of ref. table not automatically loaded in foreign keys modal 2021-03-22 18:04:19 +01:00
13b9840f3d chore: update README.md 2021-03-21 14:39:11 +01:00
d20414b692 chore(release): 0.1.0 2021-03-21 13:01:35 +01:00
22a8c25717 fix: update or delete rows with more than one primary key 2021-03-21 13:00:27 +01:00
db47b4040a fix(PostgreSQL): issue getting foreign keys informations 2021-03-21 11:51:22 +01:00
e89911b185 fix: remove last char from datetime and time if is a dot 2021-03-20 16:29:56 +01:00
fccfe92453 fix(PostgreSQL): various issues in query results 2021-03-19 18:49:26 +01:00
d465e18dba feat(PostgreSQL): support to microseconds 2021-03-18 15:56:52 +01:00
ffb1712a59 feat(UI): support to boolean fields 2021-03-18 12:59:46 +01:00
9f6a183d9b fix(PostgreSQL): single quote escape 2021-03-18 12:30:06 +01:00
1f80a64fe1 feat(PostgreSQL): insert and edit blob fields 2021-03-18 11:09:50 +01:00
fc651149b9 feat(PostgreSQL): edit array and text search fields 2021-03-17 18:06:17 +01:00
964570247f feat(PostgreSQL): database in connection parameters 2021-03-17 16:51:26 +01:00
8a6c59f7ce fix: schema content not loaded if selected with right click 2021-03-17 11:57:47 +01:00
4d844fe2c9 refactor: rename database to schema 2021-03-17 11:15:14 +01:00
d892fa6fb3 feat(PostgreSQL): partial postgre implementation 2021-03-16 18:42:03 +01:00
8c9e4f6e96 chore: update issue templates 2021-03-16 15:55:11 +01:00
966ca60c89 chore: update README.md 2021-03-16 15:51:21 +01:00
9bbe218f90 chore: update README.md 2021-03-14 15:38:55 +01:00
a1c6be372b fix(MySQL): handle NEWDECIMAL data type 2021-03-14 15:04:20 +01:00
7d0c929fb8 chore(release): 0.0.20 2021-03-13 19:30:57 +01:00
25d72e3952 fix(UI): row mark not applied on first click 2021-03-10 15:55:34 +01:00
b232a3bb5f feat(UI): loader layers on query and data tabs 2021-03-09 19:14:02 +01:00
e9a26c1bc0 fix(UI): avoid unnecessary updates when cell content not change 2021-03-09 18:08:57 +01:00
76c5c0c680 fix(UI): table rows lose internal id after an update 2021-03-09 18:07:48 +01:00
ddfb713124 feat(UI): row markers in sql editors 2021-03-08 18:11:00 +01:00
0081a4167c refactor: moving from keytar to local storage due issues on Linux 2021-03-08 17:35:43 +01:00
239cb4488f ci: snap store config 2021-03-08 12:40:56 +01:00
fb5adbe676 chore: update package.json to support Windows portable build 2021-03-06 17:29:00 +01:00
9cd51c8d8b chore: update package.json 2021-03-06 17:11:03 +01:00
8dfaa3b7be chore(release): 0.0.19 2021-03-05 17:29:12 +01:00
5d7efa75b7 fix(UI): modal processes list does not regain size on window resize 2021-03-05 17:23:13 +01:00
4862d51fba fix(UI): wrong height in scrolling tables in some cases 2021-03-05 17:10:52 +01:00
07f60c3917 feat(UI): modal that shows process query 2021-03-04 19:34:18 +01:00
049143d143 feat: processes list tool 2021-03-03 19:31:05 +01:00
db4430609e feat(MySQL): support to new mysql8 authentication, closes #45 2021-03-02 12:03:01 +01:00
71b4310117 feat: context menu shortcut to set NULL a table cell 2021-02-28 21:45:38 +01:00
201fad9265 fix(MySQL): wrong TIMESTAMP fields length 2021-02-27 18:30:34 +01:00
45351faeae feat(UI): esc key to cancel cell edit 2021-02-27 18:29:47 +01:00
b4ead6992c perf(UI): improvements of date time inputs 2021-02-27 17:52:54 +01:00
b1ea32b680 feat: setting to enable beta updates (future use) 2021-02-27 17:28:01 +01:00
39ca1974bc perf(UI): big performance improvement in tables rendering 2021-02-26 22:31:05 +01:00
777b73fa6f feat(UI): query duration calc 2021-02-26 18:45:00 +01:00
4494e637f7 chore(release): 0.0.18 2021-02-25 19:09:02 +01:00
219da0aba4 feat(UI): run procedures/functions from sidebar context menu 2021-02-25 17:43:23 +01:00
7e8167154f feat(UI): run routines/functions from settings tab 2021-02-25 12:39:50 +01:00
3aa2159a1a fix(MySQL): issue obtaining routine/function parameters 2021-02-24 19:45:27 +01:00
76d92cd106 fix: issue managing function/routine parameters 2021-02-24 12:46:31 +01:00
c8545a250b fix(UI): elements from previous selected schemas in query suggestions 2021-02-22 19:14:02 +01:00
dbab06fcb8 fix(UI): data tab opened when non-table element is selected 2021-02-22 11:10:04 +01:00
b54fefbf25 feat(UI): context menu for input and textarea tags 2021-02-21 21:24:25 +01:00
9a1bf32128 feat(UI): html, xml, json, svg and yaml editor modes in long text fields edit 2021-02-21 19:22:03 +01:00
110b0b414c feat(UI): sticky schema names in explore bar 2021-02-20 18:55:08 +01:00
2f58007af4 feat(UI): search filter in explore bar 2021-02-20 11:55:34 +01:00
9b60bfff8d build: dropped use of lodash 2021-02-19 17:41:33 +01:00
3b37b7432e fix: disabled sort for fields without a name property 2021-02-18 18:12:36 +01:00
7c4ca999ce fix: prevents F5 shortcut to run in non-selected workspaces 2021-02-18 18:01:12 +01:00
94c4952319 fix: support of bit fields in table filler 2021-02-18 15:26:17 +01:00
126 changed files with 7593 additions and 1270 deletions

View File

@@ -9,22 +9,25 @@
"plugin:vue/recommended" "plugin:vue/recommended"
], ],
"parserOptions": { "parserOptions": {
"parser": "babel-eslint", "parser": "@babel/eslint-parser",
"ecmaVersion": 9, "ecmaVersion": 9,
"sourceType": "module" "sourceType": "module",
"requireConfigFile": false
}, },
"rules": { "rules": {
"indent": [ "indent": [
"error", "error",
3, 3,
{ "SwitchCase": 1 } {
"SwitchCase": 1
}
], ],
"linebreak-style": [ "linebreak-style": [
"error", "error",
"unix" "unix"
], ],
"brace-style": [ "brace-style": [
"error", "error",
"stroustrup" "stroustrup"
], ],
"quotes": [ "quotes": [
@@ -36,7 +39,7 @@
"always" "always"
], ],
"curly": [ "curly": [
"error", "error",
"multi-or-nest" "multi-or-nest"
], ],
"no-console": "off", "no-console": "off",
@@ -44,18 +47,25 @@
"vue/no-side-effects-in-computed-properties": "off", "vue/no-side-effects-in-computed-properties": "off",
"vue/require-default-prop": "off", "vue/require-default-prop": "off",
"vue/no-v-html": "off", "vue/no-v-html": "off",
"vue/html-indent": ["error", 3, { "vue/html-indent": [
"attribute": 1, "error",
"baseIndent": 1, 3,
"closeBracket": 0, {
"ignores": [] "attribute": 1,
}], "baseIndent": 1,
"vue/max-attributes-per-line": ["error", { "closeBracket": 0,
"singleline": 2, "ignores": []
"multiline": {
"max": 1,
"allowFirstLine": false
} }
}] ],
"vue/max-attributes-per-line": [
"error",
{
"singleline": 2,
"multiline": {
"max": 1,
"allowFirstLine": false
}
}
]
} }
} }

2
.github/FUNDING.yml vendored
View File

@@ -1,7 +1,7 @@
# These are supported funding model platforms # These are supported funding model platforms
github: [fabio286] github: [fabio286]
patreon: fabio286 patreon: #fabio286
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel

View File

@@ -3,7 +3,7 @@ name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ''
labels: '' labels: ''
assignees: '' assignees: Fabio286
--- ---
@@ -12,6 +12,7 @@ A clear and concise description of what the bug is.
**To Reproduce** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
@@ -23,16 +24,15 @@ A clear and concise description of what you expected to happen.
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):** **Application (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):** - Version [e.g. 0.14.0]
- Device: [e.g. iPhone6] - Distribution: [e.g. exe, Linux Store, AppImage, dmg]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari] **Desktop (please complete the following information):**
- Version [e.g. 22]
- OS: [e.g. iOS]
- Version [e.g. 22]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

View File

@@ -3,7 +3,7 @@ name: Feature request
about: Suggest an idea for this project about: Suggest an idea for this project
title: '' title: ''
labels: '' labels: ''
assignees: '' assignees: Fabio286
--- ---

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

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

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

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

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

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

View File

@@ -8,7 +8,8 @@
"stylelint-scss" "stylelint-scss"
], ],
"rules": { "rules": {
"at-rule-no-unknown": null "at-rule-no-unknown": null,
"no-descending-specificity": null
}, },
"syntax": "scss" "syntax": "scss"
} }

View File

@@ -1,52 +0,0 @@
language: node_js
node_js: 12
cache:
directories:
- node_modules
- app/node_modules
- $HOME/.cache/electron
- $HOME/.cache/electron-builder
- $HOME/.npm/_prebuilds
env:
global:
- ELECTRON_CACHE=$HOME/.cache/electron
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
jobs:
include:
- stage: Test
before_install:
- sudo apt-get install libsecret-1-dev
- npm install
script:
- npm test
- stage: Deploy Linux & Windows
if: tag IS present
os: linux
services: docker
before_install:
- sudo apt-get install libsecret-1-dev
- npm install
script:
- docker run --rm --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|_TOKEN|_KEY|AWS_|STRIP|BUILD_') -v ${PWD}:/project -v ~/.cache/electron:/root/.cache/electron -v ~/.cache/electron-builder:/root/.cache/electron-builder electronuserland/builder:wine /bin/bash -c "npm run build -- --linux --win -p always"
before_cache:
- rm -rf $HOME/.cache/electron-builder/wine
- stage: Deploy Mac
if: tag IS present
os: osx
before_install:
- npm install
osx_image: xcode10.2
script:
- npm run build -- -p always
# - stage: Deploy ARM Linux
# if: tag IS present
# os: linux
# arch: arm64
# script:
# - npm run build -- --linux AppImage -p always

View File

@@ -2,6 +2,247 @@
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.1.8](https://github.com/Fabio286/antares/compare/v0.1.7...v0.1.8) (2021-05-22)
### Bug Fixes
* internal id not removed before row update ([0f10c9e](https://github.com/Fabio286/antares/commit/0f10c9e824b679cfebd8a31085236da3d38a7953))
* unable to add new ENUM fields ([1e37f2a](https://github.com/Fabio286/antares/commit/1e37f2a96f660a36f2739fea57ca298059e9c31a))
* unable to delete table rows ([0d61371](https://github.com/Fabio286/antares/commit/0d6137195da9e0e9afe68f6d3ebcd50e913a888e))
### Improvements
* improved the way how field default value are handled ([7a766f0](https://github.com/Fabio286/antares/commit/7a766f04e668868e8844aac1d7d445bc2da21558))
### [0.1.7](https://github.com/Fabio286/antares/compare/v0.1.6...v0.1.7) (2021-05-16)
### Bug Fixes
* row loses internal id after cell update ([854472c](https://github.com/Fabio286/antares/commit/854472c7a3d8442b1eede12f978cad5aa684094e))
* **MySQL:** connection loses schema in some conditions ([6b0b8b1](https://github.com/Fabio286/antares/commit/6b0b8b19d7176ef8647d6f401a33315f8732fdf3))
* issue with ENUM and SET fields on table filler modal ([475397c](https://github.com/Fabio286/antares/commit/475397ca34c5b7d8925e2d97d32216d5b80d8211))
* issue with ENUM and SET length when creating a new field ([7a62131](https://github.com/Fabio286/antares/commit/7a62131cc707aa1c98bb12513de76229472b7c38))
* multiple row select on sorted tables not work properly ([c7663be](https://github.com/Fabio286/antares/commit/c7663be338ccb5ff31e241e824593531ad95bd32))
### Improvements
* **core:** increased connection pool size to improve performance ([ae103e5](https://github.com/Fabio286/antares/commit/ae103e5477ad190c541d0f29e277c41de6947063))
* **MySQL:** improved connections pool handling ([434711a](https://github.com/Fabio286/antares/commit/434711a360bd7539d2ec3adec92e7ccc96fa828e))
### [0.1.6](https://github.com/Fabio286/antares/compare/v0.1.5...v0.1.6) (2021-05-08)
### Features
* **MySQL:** ENUM and SET fields support, closes [#61](https://github.com/Fabio286/antares/issues/61) ([bebba64](https://github.com/Fabio286/antares/commit/bebba64d06532990405763284e27cb768dc050f7))
### Bug Fixes
* better detection and handling of field default type ([3baf6fa](https://github.com/Fabio286/antares/commit/3baf6fa1736a405c95fb02d17c11514861ca9e04))
* no quotes around strings in field default custom value ([29e2d92](https://github.com/Fabio286/antares/commit/29e2d92b5bf66ff9e80bb1fee1274967c4418601))
* **UI:** data type figure twice on type select ([9dfe7cc](https://github.com/Fabio286/antares/commit/9dfe7cca2234ce6de512bb4c21205c2217e7f765))
* support to mDNS/zeroconf in snap build, closes [#58](https://github.com/Fabio286/antares/issues/58) ([35ef070](https://github.com/Fabio286/antares/commit/35ef070725f5779923bf3ad428b44accddf22dbe))
### Improvements
* italian translation updated ([5dc0b0b](https://github.com/Fabio286/antares/commit/5dc0b0bea40f9d5a5944af2f1c5dc6ff3e60c396))
### [0.1.5](https://github.com/Fabio286/antares/compare/v0.1.4...v0.1.5) (2021-04-30)
### Bug Fixes
* **MySQL:** multiple queries non properly split in some cases ([5208ec1](https://github.com/Fabio286/antares/commit/5208ec171b44da0e6bfa93f15bfedd03ef2aa868))
* % character not properly escaped, closes [#60](https://github.com/Fabio286/antares/issues/60) ([ecfb732](https://github.com/Fabio286/antares/commit/ecfb732c265a5485e131e75f3d20ff07d9409753))
* semicolon inside strings breaks queries, closes [#59](https://github.com/Fabio286/antares/issues/59) ([1b09909](https://github.com/Fabio286/antares/commit/1b0990912627a3a4a4e8d62b4593f8a7aa3a7fe5))
### Improvements
* **UI:** new application icon ([5822b3d](https://github.com/Fabio286/antares/commit/5822b3df432e0a2b305d0ff37a20dc466c3a3992))
### [0.1.4](https://github.com/Fabio286/antares/compare/v0.1.3...v0.1.4) (2021-04-22)
### Features
* query results export ([1d363f7](https://github.com/Fabio286/antares/commit/1d363f755e025d0fc6fec61cbd47ff87a8f25728))
* **UI:** canc press to delete selected rows ([20cba6e](https://github.com/Fabio286/antares/commit/20cba6ee9bc1daa902b04d6e2ddcb31d04fbf805))
* **UI:** ctrl+s shortcut to save changes ([16e17b3](https://github.com/Fabio286/antares/commit/16e17b39b6c8b561cc018d02afee2276190ce304))
* **UI:** format and clear queries ([9ffd443](https://github.com/Fabio286/antares/commit/9ffd443a66303f88fc4529896f6d1d7917454f7a))
### Bug Fixes
* launch from shortcut of procedures or functions with parameters without name dont works ([f82dbd2](https://github.com/Fabio286/antares/commit/f82dbd24dcef7b4d8d127a604e256b3f79a6c617))
* wrong changelog in some cases ([a41cf1a](https://github.com/Fabio286/antares/commit/a41cf1ab5662f5f5fdedff4a9e1c626c23071377))
* **UI:** data type not listed in selection if not present in global types ([6eb2977](https://github.com/Fabio286/antares/commit/6eb2977568987b9440b62ae7dbd7183338bfcc9b))
### Improvements
* **UI:** improved connection status indicator ([5ceddb8](https://github.com/Fabio286/antares/commit/5ceddb8e00f3bc1984b8e47de270dc39b367903f))
### [0.1.3](https://github.com/Fabio286/antares/compare/v0.1.2...v0.1.3) (2021-04-17)
### Features
* **PostgreSQL:** functions management ([cd31413](https://github.com/Fabio286/antares/commit/cd3141325681ea572c06b8998dd7bd334ceb3236))
* **PostgreSQL:** procedure language select ([b33199e](https://github.com/Fabio286/antares/commit/b33199ea59c60b467601f333857494aa40adf4e8))
### Bug Fixes
* **MySQL:** invalid JavaScript datetime values not shown ([dcccb54](https://github.com/Fabio286/antares/commit/dcccb544f9ec24ad693c9e81fb4bcfbdbb7cc4e1))
* approximate row count shown for results less than 1000 ([a6b75ad](https://github.com/Fabio286/antares/commit/a6b75ad0dc0d884332464c277e8542b2698630b9))
* field apparently loses index or foreign key on rename in table editor ([7d2ace9](https://github.com/Fabio286/antares/commit/7d2ace94562f8da307b15b83c89d919727d800c8))
### Improvements
* **MySQL:** improved the way to get routine and functions parameters ([90fd9db](https://github.com/Fabio286/antares/commit/90fd9db917c40262f2bc2501ab86f5feba3d8db4))
* **UI:** improved table fields suggestion in query editor ([c22187c](https://github.com/Fabio286/antares/commit/c22187c3053aef368a351cc35e2f1d407ecde209))
### [0.1.2](https://github.com/Fabio286/antares/compare/v0.1.1...v0.1.2) (2021-04-11)
### Features
* in-app last release changelog ([1e938ad](https://github.com/Fabio286/antares/commit/1e938adc5d8eb5ad16ab16342375eecd88f68d20))
* **PostgreSQL:** edit timezone in cell editor ([8735a0c](https://github.com/Fabio286/antares/commit/8735a0c5f9e5b6b3bcaadf37ce158aa7beae2c48))
* **PostgreSQL:** procedures management ([3dde1c1](https://github.com/Fabio286/antares/commit/3dde1c109e23342d94362626ef7350dc123ea859))
* **PostgreSQL:** support of arrays in table settings ([d0b3e1b](https://github.com/Fabio286/antares/commit/d0b3e1b1b8be9d2c038d70e16d4478671315de8f))
### Bug Fixes
* cell edit doesn't properly use primary or unique index to update if both present, closes [#51](https://github.com/Fabio286/antares/issues/51) ([55932fe](https://github.com/Fabio286/antares/commit/55932fe11583bd5ff48f82b8408965adba4f5071))
* deletion of rows from query results ([c20bff7](https://github.com/Fabio286/antares/commit/c20bff7bcbe340ac99ebcacaba3359edd61c068a))
* no foreign key select when cell value is NULL, closes [#50](https://github.com/Fabio286/antares/issues/50) ([9f5ec02](https://github.com/Fabio286/antares/commit/9f5ec0276c92904975fdaea34b4c845c92bfe8d4))
* wrong datetime conversion when updating a row without an unique key ([d374372](https://github.com/Fabio286/antares/commit/d374372e208318d7e50b258a8041145bdf7992c5))
* **PostgreSQL:** issue with selected schema different than public ([49a4e1c](https://github.com/Fabio286/antares/commit/49a4e1cb7b24642641265d5830d3fee370cceeb4))
* **UI:** white readonly inputs with dark theme ([bb5f446](https://github.com/Fabio286/antares/commit/bb5f44681f87aacf2cd2f60a6d958c5732289790))
### Improvements
* **UI:** improved setting modal rendering ([be816e8](https://github.com/Fabio286/antares/commit/be816e85888b4f3d26cbb9caac0adbc4dde0ea94))
### [0.1.1](https://github.com/Fabio286/antares/compare/v0.1.0...v0.1.1) (2021-04-03)
### Features
* scratchpad to save persistent notes ([e349dd5](https://github.com/Fabio286/antares/commit/e349dd5eaba608591257f2799b830805e4936c27))
* **PostgreSQL:** foreign keys management ([fe4c8e1](https://github.com/Fabio286/antares/commit/fe4c8e12b39dd3cdfc233f07e3fe2ff0676252b0))
* **PostgreSQL:** indexes management ([9ca03f4](https://github.com/Fabio286/antares/commit/9ca03f462560b634970a19d3d97b878d60509acc))
* **PostgreSQL:** table fields edit ([e3f259c](https://github.com/Fabio286/antares/commit/e3f259c6e8327d71bd7dd0a9c33d957dc6ca1fb8))
* **PostgreSQL:** tables addition ([feef5e3](https://github.com/Fabio286/antares/commit/feef5e30eec915cbb219223cc428bd4e98d2e9c5))
* **PostgreSQL:** unique keys management ([614e0d3](https://github.com/Fabio286/antares/commit/614e0d32758c13b59139d349d4682a5bafc3ca88))
* **PostgreSQL:** views management ([99f7511](https://github.com/Fabio286/antares/commit/99f7511c4d5fab4030b30d5134cd03248167faea))
* **UI:** light theme ([2806976](https://github.com/Fabio286/antares/commit/280697698ea5fae6d54326970c823878888c196c))
### Bug Fixes
* **UI:** editor theme preview not properly loaded in some cases ([c981244](https://github.com/Fabio286/antares/commit/c981244d7aa93ca18ca2de44bf8df06f253b9d20))
* fields of ref. table not automatically loaded in foreign keys modal ([e7401cc](https://github.com/Fabio286/antares/commit/e7401cc96e76e00100a88eea9f40541fd8027adb))
* hide update tab for Windows Store distributions ([dcb135d](https://github.com/Fabio286/antares/commit/dcb135dd015b8f8c6cfb44021211bb8cf3089192))
## [0.1.0](https://github.com/Fabio286/antares/compare/v0.0.20...v0.1.0) (2021-03-21)
### Features
* **PostgreSQL:** database in connection parameters ([9645702](https://github.com/Fabio286/antares/commit/964570247ff5b7b8317419730eec5bed4f0f0580))
* **PostgreSQL:** edit array and text search fields ([fc65114](https://github.com/Fabio286/antares/commit/fc651149b95399c52d2d63e946731e9c1b0303a9))
* **PostgreSQL:** insert and edit blob fields ([1f80a64](https://github.com/Fabio286/antares/commit/1f80a64fe1400baacca26f1a762c5aeb4ef6350d))
* **PostgreSQL:** partial postgre implementation ([d892fa6](https://github.com/Fabio286/antares/commit/d892fa6fb3e86fbb96887d8eb67319ae855260a1))
* **PostgreSQL:** support to microseconds ([d465e18](https://github.com/Fabio286/antares/commit/d465e18dba8ea3aa00726e33f9b1f70ca4c0683c))
* **UI:** support to boolean fields ([ffb1712](https://github.com/Fabio286/antares/commit/ffb1712a593d1421793011e48a17369b884ea3c0))
### Bug Fixes
* update or delete rows with more than one primary key ([22a8c25](https://github.com/Fabio286/antares/commit/22a8c25717a4d4b285855426098a3a2846ce7448))
* **MySQL:** handle NEWDECIMAL data type ([a1c6be3](https://github.com/Fabio286/antares/commit/a1c6be372b570cf13e89ef7ecf9b7a7c033a9293))
* **PostgreSQL:** issue getting foreign keys informations ([db47b40](https://github.com/Fabio286/antares/commit/db47b4040a5282a6ac0711b1926c4c2ac867999e))
* remove last char from datetime and time if is a dot ([e89911b](https://github.com/Fabio286/antares/commit/e89911b1851c19813d4acf2c79adfbc2ac7c1112))
* **PostgreSQL:** single quote escape ([9f6a183](https://github.com/Fabio286/antares/commit/9f6a183d9b293dfe9ad9f3759f2375f05f37db8e))
* **PostgreSQL:** various issues in query results ([fccfe92](https://github.com/Fabio286/antares/commit/fccfe92453325cd54c0331cc5670af0a56822c5b))
* schema content not loaded if selected with right click ([8a6c59f](https://github.com/Fabio286/antares/commit/8a6c59f7ce7d051315b04cea38a96e4739b5b9d3))
### [0.0.20](https://github.com/Fabio286/antares/compare/v0.0.19...v0.0.20) (2021-03-13)
### Features
* **UI:** loader layers on query and data tabs ([b232a3b](https://github.com/Fabio286/antares/commit/b232a3bb5ff7e38c83aa33e8b96ec7202bc4881e))
* **UI:** row markers in sql editors ([ddfb713](https://github.com/Fabio286/antares/commit/ddfb7131248a47fa2055ccfb72e223986a17f986))
### Bug Fixes
* **UI:** avoid unnecessary updates when cell content not change ([e9a26c1](https://github.com/Fabio286/antares/commit/e9a26c1bc0641b7087d8143cc948405850d7552f))
* **UI:** row mark not applied on first click ([25d72e3](https://github.com/Fabio286/antares/commit/25d72e39529884c09cf1286ff64ba00c8a5c7b24))
* **UI:** table rows lose internal id after an update ([76c5c0c](https://github.com/Fabio286/antares/commit/76c5c0c680521b4a20de1f12bab25314ac084d5c))
### [0.0.19](https://github.com/Fabio286/antares/compare/v0.0.18...v0.0.19) (2021-03-05)
### Features
* **UI:** modal that shows process query ([07f60c3](https://github.com/Fabio286/antares/commit/07f60c39173e9db452909d74573c3aecf4b6466b))
* processes list tool ([049143d](https://github.com/Fabio286/antares/commit/049143d143d8187bfcb6377f2bf374c471c28046))
* **MySQL:** support to new mysql8 authentication, closes [#45](https://github.com/Fabio286/antares/issues/45) ([db44306](https://github.com/Fabio286/antares/commit/db4430609e22816f4a3a8ecdbf61e9b51cde2579))
* context menu shortcut to set NULL a table cell ([71b4310](https://github.com/Fabio286/antares/commit/71b43101172c27d810d533c936534bcf089dbca2))
* **UI:** esc key to cancel cell edit ([45351fa](https://github.com/Fabio286/antares/commit/45351faeaea6ad4af8280da6c0ec1b4ded0f86fe))
* setting to enable beta updates (future use) ([b1ea32b](https://github.com/Fabio286/antares/commit/b1ea32b68024593481120e2ef67642e127554888))
* **UI:** query duration calc ([777b73f](https://github.com/Fabio286/antares/commit/777b73fa6f9d6f7806e0d3d07589dfaee0c40786))
### Bug Fixes
* **MySQL:** wrong TIMESTAMP fields length ([201fad9](https://github.com/Fabio286/antares/commit/201fad9265e01e19ae4c8dc16bff84ee9dc9c894))
* **UI:** modal processes list does not regain size on window resize ([5d7efa7](https://github.com/Fabio286/antares/commit/5d7efa75b76f59b85654603b4df8035a36af0576))
* **UI:** wrong height in scrolling tables in some cases ([4862d51](https://github.com/Fabio286/antares/commit/4862d51fba863e055ca6735586b0abe4894037eb))
### Improvements
* **UI:** big performance improvement in tables rendering ([39ca197](https://github.com/Fabio286/antares/commit/39ca1974bcea84c9047779893ed3f458300474e3))
* **UI:** improvements of date time inputs ([b4ead69](https://github.com/Fabio286/antares/commit/b4ead6992c8ddc172380a97e7aa125e0dd078f1e))
### [0.0.18](https://github.com/Fabio286/antares/compare/v0.0.17...v0.0.18) (2021-02-25)
### Features
* **UI:** context menu for input and textarea tags ([b54fefb](https://github.com/Fabio286/antares/commit/b54fefbf255c91f3cdd0c52b917b239ea40dae16))
* **UI:** html, xml, json, svg and yaml editor modes in long text fields edit ([9a1bf32](https://github.com/Fabio286/antares/commit/9a1bf3212850d3783ac6382f8a980eb25c4f4226))
* **UI:** run procedures/functions from sidebar context menu ([219da0a](https://github.com/Fabio286/antares/commit/219da0aba46839295807a01056199d42584e0af9))
* **UI:** run routines/functions from settings tab ([7e81671](https://github.com/Fabio286/antares/commit/7e8167154ffd64e46d78a50cd13cf90cee61370c))
* **UI:** search filter in explore bar ([2f58007](https://github.com/Fabio286/antares/commit/2f58007af4f32207fe4be9fc15bb43916e8e0abc))
* **UI:** sticky schema names in explore bar ([110b0b4](https://github.com/Fabio286/antares/commit/110b0b414cef25395ae28493032c2048307e4783))
### Bug Fixes
* **MySQL:** issue obtaining routine/function parameters ([3aa2159](https://github.com/Fabio286/antares/commit/3aa2159a1ae3b6785646fc7ac1e866cb0277d087))
* issue managing function/routine parameters ([76d92cd](https://github.com/Fabio286/antares/commit/76d92cd106b0409e8752c43d7d587d09dd3e1e32))
* **UI:** data tab opened when non-table element is selected ([dbab06f](https://github.com/Fabio286/antares/commit/dbab06fcb80ccdae85215ceae9ba24af3e7ff263))
* **UI:** elements from previous selected schemas in query suggestions ([c8545a2](https://github.com/Fabio286/antares/commit/c8545a250bc696c339e418a69a2942d5bac10cbf))
* disabled sort for fields without a name property ([3b37b74](https://github.com/Fabio286/antares/commit/3b37b7432e969e6315ebd6ed74905ec3980a049c))
* prevents F5 shortcut to run in non-selected workspaces ([7c4ca99](https://github.com/Fabio286/antares/commit/7c4ca999ce64af8077d29e419f430d5beaaead93))
* support of bit fields in table filler ([94c4952](https://github.com/Fabio286/antares/commit/94c49523197284510361ebbb6984816a3bb1b243))
### [0.0.17](https://github.com/Fabio286/antares/compare/v0.0.16...v0.0.17) (2021-02-17) ### [0.0.17](https://github.com/Fabio286/antares/compare/v0.0.16...v0.0.17) (2021-02-17)

View File

@@ -1,19 +1,19 @@
<p align="center"> <p align="center">
<img width="800" src="https://raw.githubusercontent.com/Fabio286/antares/master/docs/screen-alpha.png"> <img width="800" src="https://raw.githubusercontent.com/Fabio286/antares/master/docs/gh-logo.png">
</p> </p>
# Antares SQL Client # Antares SQL Client
![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) [![Build Status](https://travis-ci.com/Fabio286/antares.svg?branch=master)](https://travis-ci.com/Fabio286/antares) ![GitHub All Releases](https://img.shields.io/github/downloads/fabio286/antares/total) ![GitHub](https://img.shields.io/github/license/fabio286/antares) ![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) ![GitHub All Releases](https://img.shields.io/github/downloads/fabio286/antares/total) ![GitHub](https://img.shields.io/github/license/fabio286/antares) [![antares](https://snapcraft.io/antares/badge.svg)](https://snapcraft.io/antares) [![antares](https://snapcraft.io/antares/trending.svg?name=0)](https://snapcraft.io/antares) [![Plant a Tree](https://raw.githubusercontent.com/Fabio286/treedom-badge/master/svg/plant-a-tree.svg)](https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet)
Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers. Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers.
My target is to support as many databases as possible, and all major operating systems, including the ARM versions. My target is to support as many databases as possible, and all major operating systems, including the ARM versions.
**At the moment this application is an alpha, it lacks many features** and supports only MySQL. **At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB and PostgreSQL (partially).
Most of its current features might be enough for basic MySQL use, so give it a chance and send me your feedback, I would really appreciate it. Many of its current features are enough to have a pleasant user experience with MySQL/MariaDB, and basic functionalites with PostgreSQL, so give it a chance and send me your feedback, I would really appreciate it.
I'm actively working on it (yes, i'm a lone dev), hoping to provide cool features and fixes as soon as possible. I'm actively working on it, hoping to provide cool features and fixes as soon as possible.
🔗 If you are curious to try this early state of Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases). 🔗 If you are curious to try Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases/latest).
👁 To stay tuned for new releases watch this repo on **Release only** channel. 👁 To stay tuned for new releases watch this repo on **Release only** channel.
🌟 Don't forget to **leave a star** if you appreciate this project. 🌟 Don't forget to **leave a star** if you appreciate this project.
@@ -21,7 +21,12 @@ I'm actively working on it (yes, i'm a lone dev), hoping to provide cool feature
Why am I developing an SQL client when there are a lot of them on the market? Why am I developing an SQL client when there are a lot of them on the market?
The main goal is to develop a totally free, full featured, cross platform and open source alternative, empowered by JavaScript's ecosystem. The main goal is to develop a totally free, full featured, cross platform and open source alternative, empowered by JavaScript's ecosystem.
An application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons or submenu. A modern application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons, tabs or submenu.
## Download
[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/antares) [![Get it from Microsoft Store](https://raw.githubusercontent.com/Fabio286/antares/gh-pages/src/assets/ms-store.png)](https://www.microsoft.com/p/antares-sql-client/9nhtb9sq51r1?cid=storebadge&ocid=badge&rtc=1&activetab=pivot:overviewtab)
🚀 **[Other Downloads](https://github.com/Fabio286/antares/releases/latest)**
## How to contribute ## How to contribute
@@ -36,17 +41,17 @@ An application created with minimalism and semplicity in mind, with features in
- Fake table data filler. - Fake table data filler.
- Run queries on multiple tabs. - Run queries on multiple tabs.
- Query suggestions and auto complete. - Query suggestions and auto complete.
- Native dark theme. - Dark and light theme.
- Scratchpad.
- Multi language. - Multi language.
- Secure password storage. - Secure password storage.
- Auto updates.
## Coming soon ## Coming soon
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. - Support for other databases.
- Database tools (variables, process list...). - Database tools.
- SSH tunnel support. - SSH tunnel support.
- Users management (add/edit/delete). - Users management (add/edit/delete).
- UI/UX improvements. - UI/UX improvements.
@@ -55,27 +60,15 @@ This is a roadmap with major features will come in near future.
- More keyboard shortcuts. - More keyboard shortcuts.
- Query logs console. - Query logs console.
- Import/export and migration. - Import/export and migration.
- Light theme.
## Troubleshooting
### **Linux**
With KDE may need necessary installation of the additional `gnome-keyring` package.
Depending on your distribution, you will need to run the following command:
- Debian/Ubuntu: `sudo apt-get install gnome-keyring`
- Red Hat-based: `sudo yum install gnome-keyring`
- Arch Linux: `sudo pacman -S gnome-keyring`
## Currently supported ## Currently supported
### Databases ### Databases
- [x] MySQL/MariaDB - [x] MySQL/MariaDB
- [ ] PostgreSQL - [x] PostgreSQL (partially, work in progress)
- [ ] MSSQL
- [ ] SQLite - [ ] SQLite
- [ ] MSSQL
- [ ] OracleDB - [ ] OracleDB
- [ ] More... - [ ] More...
@@ -85,20 +78,21 @@ Depending on your distribution, you will need to run the following command:
- [x] Windows - [x] Windows
- [x] Linux - [x] Linux
- [x] MacOS (i need feedbacks) - [x] MacOS (not tested due lack of hardware)
#### • ARM #### • ARM
- [ ] Windows - [ ] Windows
- [ ] Linux - [x] Linux
- [ ] MacOS - [ ] MacOS
## Translations ## Translations
**Italian Translation** (46%) / [Giuseppe Gigliotti](https://github.com/ReverbOD) [[#20](https://github.com/Fabio286/antares/pull/20)] **Italian Translation** / [Giuseppe Gigliotti](https://github.com/ReverbOD) [[#20](https://github.com/Fabio286/antares/pull/20)]
**Arabic Translation** (45%) / [Mohd-PH](https://github.com/Mohd-PH) [[#29](https://github.com/Fabio286/antares/pull/29)] **Arabic Translation** / [Mohd-PH](https://github.com/Mohd-PH) [[#29](https://github.com/Fabio286/antares/pull/29)]
**Spanish Translation** (46%) / [hongkfui](https://github.com/hongkfui) [[#32](https://github.com/Fabio286/antares/pull/32)] **Spanish Translation** / [hongkfui](https://github.com/hongkfui) [[#32](https://github.com/Fabio286/antares/pull/32)]
**French Translation** (100%) / [MrAnyx](https://github.com/MrAnyx) [[#44](https://github.com/Fabio286/antares/pull/44)] **French Translation** / [MrAnyx](https://github.com/MrAnyx) [[#44](https://github.com/Fabio286/antares/pull/44)]
**Portugues (Brasil)** / [Daniel Eduardo](https://github.com/daeleduardo) [[#54](https://github.com/Fabio286/antares/pull/54)]
## Reviews ## Reviews

BIN
build/appx/LargeTile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
build/appx/SmallTile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
build/appx/StoreLogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 50 KiB

BIN
docs/gh-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 172 KiB

View File

@@ -1,24 +1,71 @@
{ {
"name": "antares", "name": "antares",
"productName": "Antares", "productName": "Antares",
"version": "0.0.17", "version": "0.1.8",
"description": "A cross-platform easy to use SQL client.", "description": "A cross-platform easy to use SQL client.",
"license": "MIT", "license": "MIT",
"repository": "https://github.com/Fabio286/antares.git", "repository": "https://github.com/Fabio286/antares.git",
"scripts": { "scripts": {
"dev": "cross-env NODE_ENV=development electron-webpack dev", "dev": "cross-env NODE_ENV=development electron-webpack dev",
"compile": "electron-webpack", "compile": "electron-webpack",
"build": "cross-env NODE_ENV=production npm run compile && electron-builder", "build": "cross-env NODE_ENV=production npm run compile",
"build:local": "npm run build && electron-builder",
"build:appx": "npm run build:local -- --win appx",
"release": "standard-version", "release": "standard-version",
"release:pre": "npm run release -- --prerelease alpha", "release:pre": "npm run release -- --prerelease alpha",
"test": "npm run lint", "test": "npm run lint",
"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",
"postinstall": "electron-builder install-app-deps"
}, },
"author": "Fabio Di Stasio <fabio286@gmail.com>", "author": "Fabio Di Stasio <fabio286@gmail.com>",
"build": { "build": {
"appId": "com.fabio286.antares", "appId": "com.fabio286.antares",
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}", "artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
"win": {
"target": [
"nsis",
"portable"
]
},
"mac": {
"target": {
"target": "default",
"arch": [
"x64"
]
}
},
"linux": {
"target": [
{
"target": "deb",
"arch": "x64"
},
{
"target": "AppImage",
"arch": [
"x64",
"armv7l",
"arm64"
]
}
],
"category": "Development"
},
"appImage": {
"license": "./LICENSE",
"category": "Development"
},
"portable": {
"artifactName": "${productName}-${version}-portable.exe"
},
"appx": {
"displayName": "Antares SQL Client",
"identityName": "62514FabioDiStasio.AntaresSQLClient",
"publisher": "CN=1A2729ED-865C-41D2-9038-39AE2A63AA52",
"applicationId": "FabioDiStasio.AntaresSQLClient"
},
"dmg": { "dmg": {
"contents": [ "contents": [
{ {
@@ -32,17 +79,6 @@
"path": "/Applications" "path": "/Applications"
} }
] ]
},
"linux": {
"target": [
"deb",
"AppImage"
],
"category": "Development"
},
"appImage": {
"license": "./LICENSE",
"category": "Development"
} }
}, },
"electronWebpack": { "electronWebpack": {
@@ -51,45 +87,46 @@
} }
}, },
"dependencies": { "dependencies": {
"@electron/remote": "^1.1.0",
"@mdi/font": "^5.9.55", "@mdi/font": "^5.9.55",
"ace-builds": "^1.4.12", "ace-builds": "^1.4.12",
"electron-log": "^4.3.0", "electron-log": "^4.3.5",
"electron-store": "^7.0.0", "electron-store": "^8.0.0",
"electron-updater": "^4.3.5", "electron-updater": "^4.3.9",
"faker": "^5.3.1", "faker": "^5.5.3",
"keytar": "^7.3.0", "marked": "^2.0.2",
"lodash": "^4.17.20",
"moment": "^2.29.1", "moment": "^2.29.1",
"mssql": "^6.2.3", "mysql2": "^2.2.5",
"mysql": "^2.18.1",
"pg": "^8.5.1", "pg": "^8.5.1",
"pgsql-ast-parser": "^7.2.1",
"source-map-support": "^0.5.16", "source-map-support": "^0.5.16",
"spectre.css": "^0.5.9", "spectre.css": "^0.5.9",
"vue-i18n": "^8.22.4", "sql-formatter": "^4.0.2",
"vue-the-mask": "^0.11.1", "v-mask": "^2.2.4",
"vue-i18n": "^8.24.4",
"vuedraggable": "^2.24.3", "vuedraggable": "^2.24.3",
"vuex": "^3.6.0" "vuex": "^3.6.2"
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^10.1.0", "@babel/eslint-parser": "^7.14.3",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"electron": "^11.2.1", "electron": "^12.0.9",
"electron-builder": "^22.9.1", "electron-builder": "22.10.5",
"electron-devtools-installer": "^3.1.1", "electron-devtools-installer": "^3.2.0",
"electron-webpack": "^2.8.2", "electron-webpack": "^2.8.2",
"electron-webpack-vue": "^2.4.0", "electron-webpack-vue": "^2.4.0",
"eslint": "^7.18.0", "eslint": "^7.26.0",
"eslint-config-standard": "^16.0.2", "eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^7.5.0", "eslint-plugin-vue": "^7.9.0",
"node-sass": "^5.0.0", "sass": "^1.32.13",
"sass-loader": "^10.1.1", "sass-loader": "^10.2.0",
"standard-version": "^9.1.0", "standard-version": "^9.3.0",
"stylelint": "^13.9.0", "stylelint": "^13.13.1",
"stylelint-config-standard": "^20.0.0", "stylelint-config-standard": "^22.0.0",
"stylelint-scss": "^3.18.0", "stylelint-scss": "^3.19.0",
"vue": "^2.6.12", "vue": "^2.6.12",
"vue-template-compiler": "^2.6.12", "vue-template-compiler": "^2.6.12",
"webpack": "^4.46.0" "webpack": "^4.46.0"

116
snap/snapcraft.yaml Normal file
View File

@@ -0,0 +1,116 @@
name: antares
adopt-info: antares
summary: Open source SQL client made to be simple and complete.
description: |
Antares is an SQL client that aims to become an useful and complete tool, especially for developers.
The target is to support as many databases as possible, and all major operating systems, including the ARM versions.
At the moment this application is an alpha and supports only MySQL and x86 architecture.
Most of its current features might be enough for MySQL management, so give it a chance and send us your feedback, we would really appreciate it.
base: core18
grade: stable
confinement: strict
architectures:
- build-on: amd64
compression: lzo
layout:
/etc/nsswitch.conf:
bind-file: $SNAP/etc/nsswitch.conf
parts:
antares:
plugin: dump
source: .
override-build: |
snapcraftctl build
ARCHITECTURE=$(dpkg --print-architecture)
if [ "${ARCHITECTURE}" = "amd64" ]; then
FILTER="amd64.deb"
else
echo "ERROR! Antares only produces debs for amd64. Failing the build here."
exit 1
fi
# Get the latest releases json
echo "Get GitHub releases..."
wget --quiet https://api.github.com/repos/fabio286/antares/releases/latest -O releases.json
# Get the version from the tag_name and the download URL.
VERSION=$(jq . releases.json | grep tag_name | cut -d'"' -f4 | sed s'/release-//')
DEB_URL=$(cat releases.json | jq -r ".assets[] | select(.name | test(\"${FILTER}\")) | .browser_download_url")
DEB=$(basename "${DEB_URL}")
echo "Downloading ${DEB_URL}..."
wget --quiet "${DEB_URL}" -O "${SNAPCRAFT_PART_INSTALL}/${DEB}"
echo "Unpacking ${DEB}..."
dpkg -x "${SNAPCRAFT_PART_INSTALL}/${DEB}" ${SNAPCRAFT_PART_INSTALL}
rm -f releases.json 2>/dev/null
rm -f "${SNAPCRAFT_PART_INSTALL}/${DEB}" 2>/dev/null
echo $VERSION > $SNAPCRAFT_STAGE/version
# Correct path to icon.
sed -i 's|Icon=antares|Icon=/usr/share/icons/hicolor/256x256/apps/antares\.png|g' ${SNAPCRAFT_PART_INSTALL}/usr/share/applications/antares.desktop
# Delete usr/bin/antares, it's a broken symlink pointing outside the snap.
rm -f ${SNAPCRAFT_PART_INSTALL}/usr/bin/antares
chmod -s ${SNAPCRAFT_PART_INSTALL}/opt/Antares/chrome-sandbox
snapcraftctl set-version "$(echo $VERSION)"
build-packages:
- dpkg
- jq
- sed
- wget
stage-packages:
- fcitx-frontend-gtk3
- libappindicator3-1
- libasound2
- libgconf-2-4
- libgtk-3-0
- libnotify4
- libnspr4
- libnss3
- libpcre3
- libpulse0
- libxss1
- libsecret-1-0
- libxtst6
- libxkbfile1
cleanup:
after: [antares]
plugin: nil
build-snaps: [gnome-3-28-1804]
override-prime: |
set -eux
cd /snap/gnome-3-28-1804/current
find . -type f,l -exec rm -f $SNAPCRAFT_PRIME/{} \;
mdns-lookup:
# Make resolution of ".local" host names (Zero-Conf/mDNS/DNS-SD)
# working: Take the original nsswitch.conf file from the base
# Snap and add "mdns4_minimal [NOTFOUND=return]" to its "hosts:" line
# Also install corresponding mdns4_minimal plug-in
# See: https://forum.snapcraft.io/t/no-mdns-support-in-snaps-should-core-have-a-modified-nsswitch-conf/
plugin: nil
stage-packages:
- libnss-mdns
override-prime: |
set -eux
sed -Ee 's/^\s*hosts:(\s+)files/hosts:\1files mdns4_minimal \[NOTFOUND=return\]/' /snap/core18/current/etc/nsswitch.conf > $SNAPCRAFT_STAGE/etc/nsswitch.conf
snapcraftctl prime
prime:
- lib/$SNAPCRAFT_ARCH_TRIPLET/libnss_mdns4_minimal*
- etc/nsswitch.conf
apps:
antares:
command: opt/Antares/antares --no-sandbox
desktop: usr/share/applications/antares.desktop
extensions: [gnome-3-28]
environment:
# Fallback to XWayland if running in a Wayland session.
DISABLE_WAYLAND: 1
plugs:
- browser-support
- cups-control
- home
- network
- opengl
- pulseaudio
- removable-media
- unity7

View File

@@ -0,0 +1,69 @@
module.exports = {
// Defaults
defaultPort: null,
defaultUser: null,
defaultDatabase: null,
// Core
database: false,
collations: false,
engines: false,
// Tools
processesList: false,
usersManagement: false,
variables: false,
// Structure
schemas: false,
tables: false,
views: false,
triggers: false,
triggerFunctions: false,
routines: false,
functions: false,
schedulers: false,
// Settings
tableAdd: false,
viewAdd: false,
triggerAdd: false,
triggerFunctionAdd: false,
routineAdd: false,
functionAdd: false,
schedulerAdd: false,
databaseEdit: false,
schemaEdit: false,
tableSettings: false,
viewSettings: false,
triggerSettings: false,
triggerFunctionSettings: false,
routineSettings: false,
functionSettings: false,
schedulerSettings: false,
indexes: false,
foreigns: false,
sortableFields: false,
unsigned: false,
nullable: false,
zerofill: false,
autoIncrement: false,
comment: false,
collation: false,
definer: false,
onUpdate: false,
tableArray: false,
viewAlgorithm: false,
viewSqlSecurity: false,
viewUpdateOption: false,
procedureDeterministic: false,
procedureDataAccess: false,
procedureSql: false,
procedureContext: false,
procedureLanguage: false,
functionDeterministic: false,
functionDataAccess: false,
functionSql: false,
functionContext: false,
functionLanguage: false,
triggerMiltipleEvents: false,
triggerUpdateColumns: false,
parametersLength: false,
languages: false
};

View File

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

View File

@@ -0,0 +1,58 @@
const defaults = require('./defaults');
module.exports = {
...defaults,
// Defaults
defaultPort: 3306,
defaultUser: 'root',
defaultDatabase: null,
// Core
collations: true,
engines: true,
// Tools
processesList: true,
// Structure
schemas: true,
tables: true,
views: true,
triggers: true,
routines: true,
functions: true,
schedulers: true,
// Settings
tableAdd: true,
viewAdd: true,
triggerAdd: true,
routineAdd: true,
functionAdd: true,
schedulerAdd: true,
schemaEdit: true,
tableSettings: true,
viewSettings: true,
triggerSettings: true,
routineSettings: true,
functionSettings: true,
schedulerSettings: true,
indexes: true,
foreigns: true,
sortableFields: true,
unsigned: true,
nullable: true,
zerofill: true,
autoIncrement: true,
comment: true,
collation: true,
definer: true,
onUpdate: true,
viewAlgorithm: true,
viewSqlSecurity: true,
viewUpdateOption: true,
procedureDeterministic: true,
procedureDataAccess: true,
procedureSql: 'BEGIN\r\n\r\nEND',
procedureContext: true,
functionDeterministic: true,
functionDataAccess: true,
functionSql: 'BEGIN\r\n\r\nEND',
parametersLength: true
};

View File

@@ -0,0 +1,42 @@
const defaults = require('./defaults');
module.exports = {
...defaults,
// Defaults
defaultPort: 5432,
defaultUser: 'postgres',
defaultDatabase: 'postgres',
// Core
database: true,
// Tools
processesList: true,
// Structure
tables: true,
views: true,
triggers: false,
routines: true,
functions: true,
// Settings
tableAdd: true,
viewAdd: true,
triggerAdd: false,
routineAdd: true,
functionAdd: true,
databaseEdit: false,
tableSettings: true,
viewSettings: true,
triggerSettings: false,
routineSettings: true,
functionSettings: true,
indexes: true,
foreigns: true,
nullable: true,
tableArray: true,
procedureSql: '$BODY$\r\n\r\n$BODY$',
procedureContext: true,
procedureLanguage: true,
functionSql: '$BODY$\r\n\r\n$BODY$',
functionContext: true,
functionLanguage: true,
languages: ['sql', 'plpgsql', 'c', 'internal']
};

View File

@@ -4,7 +4,7 @@ module.exports = [
types: [ types: [
{ {
name: 'TINYINT', name: 'TINYINT',
length: true, length: 4,
collation: false, collation: false,
unsigned: true, unsigned: true,
zerofill: true zerofill: true
@@ -277,13 +277,6 @@ module.exports = [
{ {
group: 'other', group: 'other',
types: [ types: [
{
name: 'UNKNOWN',
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{ {
name: 'ENUM', name: 'ENUM',
length: true, length: true,
@@ -299,5 +292,17 @@ module.exports = [
zerofill: false zerofill: false
} }
] ]
},
{
group: 'unknown',
types: [
{
name: 'UNKNOWN',
length: false,
collation: false,
unsigned: false,
zerofill: false
}
]
} }
]; ];

View File

@@ -0,0 +1,292 @@
module.exports = [
{
group: 'integer',
types: [
{
name: 'SMALLINT',
length: false,
unsigned: true
},
{
name: 'INTEGER',
length: false,
unsigned: true
},
{
name: 'BIGINT',
length: false,
unsigned: true
},
{
name: 'DECIMAL',
length: false,
unsigned: true
},
{
name: 'NUMERIC',
length: true,
unsigned: true
},
{
name: 'SMALLSERIAL',
length: false,
unsigned: true
},
{
name: 'SERIAL',
length: false,
unsigned: true
},
{
name: 'BIGSERIAL',
length: false,
unsigned: true
}
]
},
{
group: 'float',
types: [
{
name: 'REAL',
length: false,
unsigned: true
},
{
name: 'DOUBLE PRECISION',
length: false,
unsigned: true
}
]
},
{
group: 'monetary',
types: [
{
name: 'money',
length: false,
unsigned: true
}
]
},
{
group: 'string',
types: [
{
name: 'CHARACTER VARYING',
length: true,
unsigned: false
},
{
name: 'CHARACTER',
length: true,
unsigned: false
},
{
name: 'TEXT',
length: false,
unsigned: false
},
{
name: '"CHAR"',
length: false,
unsigned: false
},
{
name: 'NAME',
length: false,
unsigned: false
}
]
},
{
group: 'binary',
types: [
{
name: 'BYTEA',
length: false,
unsigned: false
}
]
},
{
group: 'time',
types: [
{
name: 'TIMESTAMP WITHOUT TIME ZONE',
length: false,
unsigned: false
},
{
name: 'TIMESTAMP WITH TIME ZONE',
length: false,
unsigned: false
},
{
name: 'DATE',
length: false,
unsigned: false
},
{
name: 'TIME WITHOUT TIME ZONE',
length: false,
unsigned: false
},
{
name: 'TIME WITH TIME ZONE',
length: false,
unsigned: false
},
{
name: 'INTERVAL',
length: false,
unsigned: false
}
]
},
{
group: 'boolean',
types: [
{
name: 'BOOLEAN',
length: false,
unsigned: false
}
]
},
{
group: 'geometric',
types: [
{
name: 'POINT',
length: false,
unsigned: false
},
{
name: 'LINE',
length: false,
unsigned: false
},
{
name: 'LSEG',
length: false,
unsigned: false
},
{
name: 'BOX',
length: false,
unsigned: false
},
{
name: 'PATH',
length: false,
unsigned: false
},
{
name: 'POLYGON',
length: false,
unsigned: false
},
{
name: 'CIRCLE',
length: false,
unsigned: false
}
]
},
{
group: 'network',
types: [
{
name: 'CIDR',
length: false,
unsigned: false
},
{
name: 'INET',
length: false,
unsigned: false
},
{
name: 'MACADDR',
length: false,
unsigned: false
},
{
name: 'MACADDR8',
length: false,
unsigned: false
}
]
},
{
group: 'bit',
types: [
{
name: 'BIT',
length: true,
unsigned: false
},
{
name: 'BIT VARYING',
length: true,
unsigned: false
}
]
},
{
group: 'text search',
types: [
{
name: 'TSVECTOR',
length: false,
unsigned: false
},
{
name: 'TSQUERY',
length: false,
unsigned: false
}
]
},
{
group: 'uuid',
types: [
{
name: 'UUID',
length: false,
unsigned: false
}
]
},
{
group: 'xml',
types: [
{
name: 'XML',
length: false,
unsigned: false
}
]
},
{
group: 'json',
types: [
{
name: 'JSON',
length: false,
unsigned: false
},
{
name: 'JSONB',
length: false,
unsigned: false
},
{
name: 'JSONPATH',
length: false,
unsigned: false
}
]
}
];

View File

@@ -1,13 +1,82 @@
export const TEXT = ['CHAR', 'VARCHAR']; export const TEXT = [
export const LONG_TEXT = ['TEXT', 'MEDIUMTEXT', 'LONGTEXT']; 'CHAR',
'VARCHAR',
'CHARACTER',
'CHARACTER VARYING'
];
export const LONG_TEXT = [
'TEXT',
'MEDIUMTEXT',
'LONGTEXT'
];
export const NUMBER = ['INT', 'TINYINT', 'SMALLINT', 'MEDIUMINT', 'BIGINT', 'DECIMAL', 'BOOL']; export const ARRAY = [
export const FLOAT = ['FLOAT', 'DOUBLE']; 'ARRAY',
'ANYARRAY'
];
export const TEXT_SEARCH = [
'TSVECTOR',
'TSQUERY'
];
export const NUMBER = [
'INT',
'TINYINT',
'SMALLINT',
'MEDIUMINT',
'BIGINT',
'DECIMAL',
'NUMERIC',
'INTEGER',
'SMALLSERIAL',
'SERIAL',
'BIGSERIAL',
'OID',
'XID'
];
export const FLOAT = [
'FLOAT',
'DOUBLE',
'REAL',
'DOUBLE PRECISION',
'MONEY'
];
export const BOOLEAN = [
'BOOL',
'BOOLEAN'
];
export const DATE = ['DATE']; export const DATE = ['DATE'];
export const TIME = ['TIME']; export const TIME = [
export const DATETIME = ['DATETIME', 'TIMESTAMP']; 'TIME',
'TIME WITH TIME ZONE'
];
export const BLOB = ['BLOB', 'TINYBLOB', 'MEDIUMBLOB', 'LONGBLOB']; export const DATETIME = [
'DATETIME',
'TIMESTAMP',
'TIMESTAMP WITHOUT TIME ZONE',
'TIMESTAMP WITH TIME ZONE'
];
export const BIT = ['BIT']; // Used to check datetime fields only
export const HAS_TIMEZONE = [
'TIMESTAMP WITH TIME ZONE',
'TIME WITH TIME ZONE'
];
export const BLOB = [
'BLOB',
'TINYBLOB',
'MEDIUMBLOB',
'LONGBLOB',
'BYTEA'
];
export const BIT = [
'BIT',
'BIT VARYING'
];

View File

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

View File

@@ -12,7 +12,7 @@ const regex = new RegExp(pattern);
function sqlEscaper (string) { function sqlEscaper (string) {
return string.replace(regex, char => { return string.replace(regex, char => {
const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '\"', '\\', '\\\\', '%']; const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '\"', '\\', '\\\\', '%'];
const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\\\'', '\\\"', '\\\\', '\\\\\\\\', '\\%']; const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\\\'', '\\\"', '\\\\', '\\\\\\\\', '\%'];
return r[m.indexOf(char)] || char; return r[m.indexOf(char)] || char;
}); });
} }

View File

@@ -2,9 +2,7 @@
import { app, BrowserWindow, nativeImage } from 'electron'; import { app, BrowserWindow, nativeImage } from 'electron';
import * as path from 'path'; import * as path from 'path';
import crypto from 'crypto';
import { format as formatUrl } from 'url'; import { format as formatUrl } from 'url';
import keytar from 'keytar';
import Store from 'electron-store'; import Store from 'electron-store';
import ipcHandlers from './ipc-handlers'; import ipcHandlers from './ipc-handlers';
@@ -31,6 +29,7 @@ async function createMainWindow () {
icon: nativeImage.createFromDataURL(icon.default), icon: nativeImage.createFromDataURL(icon.default),
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
contextIsolation: false,
'web-security': false, 'web-security': false,
enableRemoteModule: true, enableRemoteModule: true,
spellcheck: false spellcheck: false
@@ -39,26 +38,25 @@ async function createMainWindow () {
backgroundColor: '#1d1d1d' backgroundColor: '#1d1d1d'
}); });
if (isDevelopment) { try {
await window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`); if (isDevelopment) {
await window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`);
const { default: installExtension, VUEJS_DEVTOOLS } = require('electron-devtools-installer'); const { default: installExtension, VUEJS_DEVTOOLS } = require('electron-devtools-installer');
window.webContents.openDevTools();
installExtension(VUEJS_DEVTOOLS) const toolName = await installExtension(VUEJS_DEVTOOLS);
.then(name => { console.log(toolName, 'installed');
console.log(name, 'installed'); }
}) else {
.catch(err => { await window.loadURL(formatUrl({
console.log(err); pathname: path.join(__dirname, 'index.html'),
}); protocol: 'file',
slashes: true
}));
}
} }
else { catch (err) {
await window.loadURL(formatUrl({ console.log(err);
pathname: path.join(__dirname, 'index.html'),
protocol: 'file',
slashes: true
}));
} }
window.on('closed', () => { window.on('closed', () => {
@@ -78,6 +76,8 @@ async function createMainWindow () {
if (!gotTheLock) if (!gotTheLock)
app.quit(); app.quit();
else { else {
require('@electron/remote/main').initialize();
// Initialize ipcHandlers // Initialize ipcHandlers
ipcHandlers(); ipcHandlers();
@@ -88,21 +88,19 @@ else {
app.quit(); app.quit();
}); });
app.on('activate', () => { app.on('activate', async () => {
// on macOS it is common to re-create a window even after all windows have been closed // on macOS it is common to re-create a window even after all windows have been closed
if (mainWindow === null) if (mainWindow === null) {
mainWindow = createMainWindow(); mainWindow = await createMainWindow();
if (isDevelopment)
mainWindow.webContents.openDevTools();
}
}); });
// create main BrowserWindow when electron is ready // create main BrowserWindow when electron is ready
app.on('ready', async () => { app.on('ready', async () => {
let key = await keytar.getPassword('antares', 'user'); mainWindow = await createMainWindow();
if (isDevelopment)
if (!key) { mainWindow.webContents.openDevTools();
key = crypto.randomBytes(16).toString('hex');
keytar.setPassword('antares', 'user', key);
}
mainWindow = createMainWindow();
}); });
} }

View File

@@ -1,4 +1,3 @@
import keytar from 'keytar';
import { app, ipcMain } from 'electron'; import { app, ipcMain } from 'electron';
export default () => { export default () => {
@@ -7,7 +6,7 @@ export default () => {
}); });
ipcMain.on('get-key', async event => { ipcMain.on('get-key', async event => {
const key = await keytar.getPassword('antares', 'user'); const key = false;
event.returnValue = key; event.returnValue = key;
}); });
}; };

View File

@@ -8,9 +8,13 @@ export default connections => {
host: conn.host, host: conn.host,
port: +conn.port, port: +conn.port,
user: conn.user, user: conn.user,
password: conn.password password: conn.password,
application_name: 'Antares SQL'
}; };
if (conn.database)
params.database = conn.database;
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,
@@ -47,9 +51,13 @@ export default connections => {
host: conn.host, host: conn.host,
port: +conn.port, port: +conn.port,
user: conn.user, user: conn.user,
password: conn.password password: conn.password,
application_name: 'Antares SQL'
}; };
if (conn.database)
params.database = conn.database;
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,
@@ -59,13 +67,13 @@ export default connections => {
}; };
} }
const connection = ClientsFactory.getConnection({
client: conn.client,
params,
poolSize: 1
});
try { try {
const connection = ClientsFactory.getConnection({
client: conn.client,
params,
poolSize: 5
});
await connection.connect(); await connection.connect();
const structure = await connection.getStructure(new Set()); const structure = await connection.getStructure(new Set());

View File

@@ -7,7 +7,7 @@ import functions from './functions';
import schedulers from './schedulers'; import schedulers from './schedulers';
import updates from './updates'; import updates from './updates';
import application from './application'; import application from './application';
import database from './database'; import schema from './schema';
import users from './users'; import users from './users';
const connections = {}; const connections = {};
@@ -20,7 +20,7 @@ export default () => {
routines(connections); routines(connections);
functions(connections); functions(connections);
schedulers(connections); schedulers(connections);
database(connections); schema(connections);
users(connections); users(connections);
updates(); updates();
application(); application();

View File

@@ -2,10 +2,9 @@
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
export default connections => { export default connections => {
ipcMain.handle('create-database', async (event, params) => { ipcMain.handle('create-schema', async (event, params) => {
try { try {
const query = `CREATE DATABASE \`${params.name}\` COLLATE ${params.collation}`; await connections[params.uid].createSchema(params);
await connections[params.uid].raw(query);
return { status: 'success' }; return { status: 'success' };
} }
@@ -14,10 +13,9 @@ export default connections => {
} }
}); });
ipcMain.handle('update-database', async (event, params) => { ipcMain.handle('update-schema', async (event, params) => {
try { try {
const query = `ALTER DATABASE \`${params.name}\` COLLATE ${params.collation}`; await connections[params.uid].alterSchema(params);
await connections[params.uid].raw(query);
return { status: 'success' }; return { status: 'success' };
} }
@@ -26,10 +24,9 @@ export default connections => {
} }
}); });
ipcMain.handle('delete-database', async (event, params) => { ipcMain.handle('delete-schema', async (event, params) => {
try { try {
const query = `DROP DATABASE \`${params.database}\``; await connections[params.uid].dropSchema(params);
await connections[params.uid].raw(query);
return { status: 'success' }; return { status: 'success' };
} }
@@ -38,10 +35,9 @@ export default connections => {
} }
}); });
ipcMain.handle('get-database-collation', async (event, params) => { // TODO: move to mysql class ipcMain.handle('get-schema-collation', async (event, params) => {
try { try {
const query = `SELECT \`DEFAULT_COLLATION_NAME\` FROM \`information_schema\`.\`SCHEMATA\` WHERE \`SCHEMA_NAME\`='${params.database}'`; const collation = await connections[params.uid].getDatabaseCollation(params);
const collation = await connections[params.uid].raw(query);
return { status: 'success', response: collation.rows.length ? collation.rows[0].DEFAULT_COLLATION_NAME : '' }; return { status: 'success', response: collation.rows.length ? collation.rows[0].DEFAULT_COLLATION_NAME : '' };
} }
@@ -105,6 +101,17 @@ export default connections => {
} }
}); });
ipcMain.handle('get-processes', async (event, uid) => {
try {
const result = await connections[uid].getProcesses();
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('use-schema', async (event, { uid, schema }) => { ipcMain.handle('use-schema', async (event, { uid, schema }) => {
if (!schema) return; if (!schema) return;
@@ -121,7 +128,7 @@ export default connections => {
if (!query) return; if (!query) return;
try { try {
const result = await connections[uid].raw(query, { nest: true, details: true }); const result = await connections[uid].raw(query, { nest: true, details: true, schema });
return { status: 'success', response: result }; return { status: 'success', response: result };
} }

View File

@@ -2,7 +2,7 @@ import { ipcMain } from 'electron';
import faker from 'faker'; 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, 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 fs from 'fs'; import fs from 'fs';
export default (connections) => { export default (connections) => {
@@ -59,55 +59,91 @@ export default (connections) => {
}); });
ipcMain.handle('update-table-cell', async (event, params) => { ipcMain.handle('update-table-cell', async (event, params) => {
try { 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 : `"${params.id}"`;
if ([...NUMBER, ...FLOAT].includes(params.type)) if ([...NUMBER, ...FLOAT].includes(params.type))
escapedParam = params.content; escapedParam = params.content;
else if ([...TEXT, ...LONG_TEXT].includes(params.type)) else if ([...TEXT, ...LONG_TEXT].includes(params.type)) {
escapedParam = `"${sqlEscaper(params.content)}"`; switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = `"${sqlEscaper(params.content)}"`;
break;
case 'pg':
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
break;
}
}
else if (ARRAY.includes(params.type))
escapedParam = `'${params.content}'`;
else if (TEXT_SEARCH.includes(params.type))
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
else if (BLOB.includes(params.type)) { else if (BLOB.includes(params.type)) {
if (params.content) { if (params.content) {
const fileBlob = fs.readFileSync(params.content); let fileBlob;
escapedParam = `0x${fileBlob.toString('hex')}`;
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
fileBlob = fs.readFileSync(params.content);
escapedParam = `0x${fileBlob.toString('hex')}`;
break;
case 'pg':
fileBlob = fs.readFileSync(params.content);
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
break;
}
reload = true; reload = true;
} }
else else {
escapedParam = '""'; switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = '\'\'';
break;
case 'pg':
escapedParam = 'decode(\'\', \'hex\')';
break;
}
}
} }
else if ([...BIT].includes(params.type)) { else if ([...BIT].includes(params.type)) {
escapedParam = `b'${sqlEscaper(params.content)}'`; escapedParam = `b'${sqlEscaper(params.content)}'`;
reload = true; reload = true;
} }
else if (params.content === null)
escapedParam = 'NULL';
else else
escapedParam = `"${sqlEscaper(params.content)}"`; escapedParam = `'${sqlEscaper(params.content)}'`;
if (params.primary) { if (params.primary) { // TODO: handle multiple primary
await connections[params.uid] await connections[params.uid]
.update({ [params.field]: `= ${escapedParam}` }) .update({ [params.field]: `= ${escapedParam}` })
.schema(params.schema) .schema(params.schema)
.from(params.table) .from(params.table)
.where({ [params.primary]: `= ${id}` }) .where({ [params.primary]: `= ${id}` })
.limit(1)
.run(); .run();
} }
else { else {
const { row } = params; const { orgRow } = params;
reload = true; reload = true;
for (const key in row) { for (const key in orgRow) {
if (typeof row[key] === 'string') if (typeof orgRow[key] === 'string')
row[key] = `'${row[key]}'`; orgRow[key] = `'${orgRow[key]}'`;
row[key] = `= ${row[key]}`; orgRow[key] = `= ${orgRow[key]}`;
} }
await connections[params.uid] await connections[params.uid]
.schema(params.schema) .schema(params.schema)
.update({ [params.field]: `= ${escapedParam}` }) .update({ [params.field]: `= ${escapedParam}` })
.from(params.table) .from(params.table)
.where(row) .where(orgRow)
.limit(1) .limit(1)
.run(); .run();
} }
@@ -121,9 +157,13 @@ export default (connections) => {
ipcMain.handle('delete-table-rows', async (event, params) => { ipcMain.handle('delete-table-rows', async (event, params) => {
if (params.primary) { if (params.primary) {
const idString = params.rows.map(row => typeof row[params.primary] === 'string' const idString = params.rows.map(row => {
? `"${row[params.primary]}"` const fieldName = Object.keys(row)[0].includes('.') ? `${params.table}.${params.primary}` : params.primary;
: row[params.primary]).join(',');
return typeof row[fieldName] === 'string'
? `"${row[fieldName]}"`
: row[fieldName];
}).join(',');
try { try {
const result = await connections[params.uid] const result = await connections[params.uid]
@@ -165,7 +205,7 @@ export default (connections) => {
}); });
ipcMain.handle('insert-table-rows', async (event, params) => { ipcMain.handle('insert-table-rows', async (event, params) => {
try { try { // TODO: move to client classes
const insertObj = {}; const insertObj = {};
for (const key in params.row) { for (const key in params.row) {
const type = params.fields[key]; const type = params.fields[key];
@@ -174,19 +214,46 @@ export default (connections) => {
if (params.row[key] === null) if (params.row[key] === null)
escapedParam = 'NULL'; escapedParam = 'NULL';
else if ([...NUMBER, ...FLOAT].includes(type)) else if ([...NUMBER, ...FLOAT].includes(type))
escapedParam = params.row[key]; escapedParam = +params.row[key];
else if ([...TEXT, ...LONG_TEXT].includes(type)) else if ([...TEXT, ...LONG_TEXT].includes(type)) {
escapedParam = `"${sqlEscaper(params.row[key])}"`; switch (connections[params.uid]._client) {
else if (BLOB.includes(type)) { case 'mysql':
if (params.row[key]) { case 'maria':
const fileBlob = fs.readFileSync(params.row[key]); escapedParam = `"${sqlEscaper(params.row[key].value)}"`;
escapedParam = `0x${fileBlob.toString('hex')}`; break;
case 'pg':
escapedParam = `'${params.row[key].value.replaceAll('\'', '\'\'')}'`;
break;
}
}
else if (BLOB.includes(type)) {
if (params.row[key].value) {
let fileBlob;
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `0x${fileBlob.toString('hex')}`;
break;
case 'pg':
fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
break;
}
}
else {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = '""';
break;
case 'pg':
escapedParam = 'decode(\'\', \'hex\')';
break;
}
} }
else
escapedParam = '""';
} }
else
escapedParam = `"${sqlEscaper(params.row[key])}"`;
insertObj[key] = escapedParam; insertObj[key] = escapedParam;
} }
@@ -207,7 +274,7 @@ export default (connections) => {
}); });
ipcMain.handle('insert-table-fake-rows', async (event, params) => { ipcMain.handle('insert-table-fake-rows', async (event, params) => {
try { try { // TODO: move to client classes
const rows = []; const rows = [];
for (let i = 0; i < +params.repeat; i++) { for (let i = 0; i < +params.repeat; i++) {
@@ -222,18 +289,49 @@ export default (connections) => {
escapedParam = 'NULL'; escapedParam = 'NULL';
else if ([...NUMBER, ...FLOAT].includes(type)) else if ([...NUMBER, ...FLOAT].includes(type))
escapedParam = params.row[key].value; escapedParam = params.row[key].value;
else if ([...TEXT, ...LONG_TEXT].includes(type)) else if ([...TEXT, ...LONG_TEXT].includes(type)) {
escapedParam = `"${sqlEscaper(params.row[key].value)}"`; switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = `"${sqlEscaper(params.row[key].value)}"`;
break;
case 'pg':
escapedParam = `'${params.row[key].value.replaceAll('\'', '\'\'')}'`;
break;
}
}
else if (BLOB.includes(type)) { else if (BLOB.includes(type)) {
if (params.row[key].value) { if (params.row[key].value) {
const fileBlob = fs.readFileSync(params.row[key].value); let fileBlob;
escapedParam = `0x${fileBlob.toString('hex')}`;
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `0x${fileBlob.toString('hex')}`;
break;
case 'pg':
fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
break;
}
}
else {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = '""';
break;
case 'pg':
escapedParam = 'decode(\'\', \'hex\')';
break;
}
} }
else
escapedParam = '""';
} }
else if (BIT.includes(type))
escapedParam = `b'${sqlEscaper(params.row[key].value)}'`;
else else
escapedParam = `"${sqlEscaper(params.row[key].value)}"`; escapedParam = `'${sqlEscaper(params.row[key].value)}'`;
insertObj[key] = escapedParam; insertObj[key] = escapedParam;
} }
@@ -257,10 +355,10 @@ export default (connections) => {
if (typeof fakeValue === 'string') { if (typeof fakeValue === 'string') {
if (params.row[key].length) if (params.row[key].length)
fakeValue = fakeValue.substr(0, params.row[key].length); fakeValue = fakeValue.substr(0, params.row[key].length);
fakeValue = `"${sqlEscaper(fakeValue)}"`; fakeValue = `'${sqlEscaper(fakeValue)}'`;
} }
else if ([...DATE, ...DATETIME].includes(type)) else if ([...DATE, ...DATETIME].includes(type))
fakeValue = `"${moment(fakeValue).format('YYYY-MM-DD HH:mm:ss.SSSSSS')}"`; fakeValue = `'${moment(fakeValue).format('YYYY-MM-DD HH:mm:ss.SSSSSS')}'`;
insertObj[key] = fakeValue; insertObj[key] = fakeValue;
} }
@@ -285,13 +383,13 @@ export default (connections) => {
ipcMain.handle('get-foreign-list', async (event, { uid, schema, table, column, description }) => { ipcMain.handle('get-foreign-list', async (event, { uid, schema, table, column, description }) => {
try { try {
const query = connections[uid] const query = connections[uid]
.select(`${column} AS foreignColumn`) .select(`${column} AS foreign_column`)
.schema(schema) .schema(schema)
.from(table) .from(table)
.orderBy('foreignColumn ASC'); .orderBy('foreign_column ASC');
if (description) if (description)
query.select(`LEFT(${description}, 20) AS foreignDescription`); query.select(`LEFT(${description}, 20) AS foreign_description`);
const results = await query.run(); const results = await query.run();

View File

@@ -1,16 +1,21 @@
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
import Store from 'electron-store';
const persistentStore = new Store({ name: 'settings' });
let mainWindow; let mainWindow;
autoUpdater.allowPrerelease = true; autoUpdater.allowPrerelease = persistentStore.get('allow_prerelease', true);
export default () => { export default () => {
ipcMain.on('check-for-updates', event => { ipcMain.on('check-for-updates', event => {
mainWindow = event; mainWindow = event;
if (process.windowsStore)
autoUpdater.checkForUpdatesAndNotify().catch(() => { mainWindow.reply('no-auto-update');
mainWindow.reply('check-failed'); else {
}); autoUpdater.checkForUpdatesAndNotify().catch(() => {
mainWindow.reply('check-failed');
});
}
}); });
ipcMain.on('restart-to-update', () => { ipcMain.on('restart-to-update', () => {

View File

@@ -1,5 +1,6 @@
'use strict'; 'use strict';
import { MySQLClient } from './clients/MySQLClient'; import { MySQLClient } from './clients/MySQLClient';
import { PostgreSQLClient } from './clients/PostgreSQLClient';
export class ClientsFactory { export class ClientsFactory {
/** /**
@@ -20,8 +21,10 @@ export class ClientsFactory {
case 'mysql': case 'mysql':
case 'maria': case 'maria':
return new MySQLClient(args); return new MySQLClient(args);
case 'pg':
return new PostgreSQLClient(args);
default: default:
return new Error(`Unknown database client: ${args.client}`); throw new Error(`Unknown database client: ${args.client}`);
} }
} }
} }

View File

@@ -1,5 +1,5 @@
'use strict'; 'use strict';
import mysql from 'mysql'; import mysql from 'mysql2/promise';
import { AntaresCore } from '../AntaresCore'; import { AntaresCore } from '../AntaresCore';
import dataTypes from 'common/data-types/mysql'; import dataTypes from 'common/data-types/mysql';
@@ -7,6 +7,8 @@ export class MySQLClient extends AntaresCore {
constructor (args) { constructor (args) {
super(args); super(args);
this._schema = null;
this.types = { this.types = {
0: 'DECIMAL', 0: 'DECIMAL',
1: 'TINYINT', 1: 'TINYINT',
@@ -43,17 +45,21 @@ export class MySQLClient extends AntaresCore {
} }
_getType (field) { _getType (field) {
let name = this.types[field.type]; let name = this.types[field.columnType];
let length = field.length; let length = field.columnLength;
if (['DATE', 'TIME', 'YEAR', 'DATETIME'].includes(name)) if (['DATE', 'TIME', 'YEAR', 'DATETIME'].includes(name))
length = field.decimals; length = field.decimals;
if (name === 'CHAR' && field.charsetNr === 63)// if binary if (name === 'TIMESTAMP')
name = 'BINARY'; length = 0;
if (name === 'VARCHAR' && field.charsetNr === 63)// if binary if (field.charsetNr === 63) { // if binary
name = 'VARBINARY'; if (name === 'CHAR')
name = 'BINARY';
else if (name === 'VARCHAR')
name = 'VARBINARY';
}
if (name === 'BLOB') { if (name === 'BLOB') {
switch (length) { switch (length) {
@@ -96,10 +102,22 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async connect () { async connect () {
delete this._params.application_name;
if (!this._poolSize) if (!this._poolSize)
this._connection = mysql.createConnection(this._params); this._connection = await mysql.createConnection(this._params);
else else {
this._connection = mysql.createPool({ ...this._params, connectionLimit: this._poolSize }); this._connection = mysql.createPool({
...this._params,
connectionLimit: this._poolSize,
typeCast: (field, next) => {
if (field.type === 'DATETIME')
return field.string();
else
return next();
}
});
}
} }
/** /**
@@ -116,6 +134,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient * @memberof MySQLClient
*/ */
use (schema) { use (schema) {
this._schema = schema;
return this.raw(`USE \`${schema}\``); return this.raw(`USE \`${schema}\``);
} }
@@ -290,25 +309,67 @@ export class MySQLClient extends AntaresCore {
.orderBy({ ORDINAL_POSITION: 'ASC' }) .orderBy({ ORDINAL_POSITION: 'ASC' })
.run(); .run();
const { rows: fields } = await this.raw(`SHOW CREATE TABLE ${schema}.${table}`);
const remappedFields = fields.map(row => {
let n = 0;
return row['Create Table']
.split('')
.reduce((acc, curr) => {
if (curr === ')') n--;
if (n !== 0) acc += curr;
if (curr === '(') n++;
return acc;
}, '')
.replaceAll('\n', '')
.split(',')
.map(f => {
const fieldArr = f.trim().split(' ');
const nameAndType = fieldArr.slice(0, 2);
if (!nameAndType[0].includes('`')) return false;
const details = fieldArr.slice(2).join(' ');
const defaultValue = details.includes('DEFAULT') ? details.match(/(?<=DEFAULT ).*?$/gs)[0] : null;
const typeAndLength = nameAndType[1].replace(')', '').split('(');
return {
name: nameAndType[0].replaceAll('`', ''),
type: typeAndLength[0].toUpperCase(),
length: typeAndLength[1] ? typeAndLength[1] : null,
default: defaultValue
};
})
.filter(Boolean)
.reduce((acc, curr) => {
acc[curr.name] = curr;
return acc;
}, {});
})[0];
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() : null;
const enumValues = /(enum|set)/.test(field.COLUMN_TYPE)
? field.COLUMN_TYPE.match(/\(([^)]+)\)/)[0].slice(1, -1)
: null;
return { return {
name: field.COLUMN_NAME, name: field.COLUMN_NAME,
key: field.COLUMN_KEY.toLowerCase(), key: field.COLUMN_KEY.toLowerCase(),
type: field.DATA_TYPE.toUpperCase(), type: remappedFields[field.COLUMN_NAME].type,
schema: field.TABLE_SCHEMA, schema: field.TABLE_SCHEMA,
table: field.TABLE_NAME, table: field.TABLE_NAME,
numPrecision: field.NUMERIC_PRECISION, numPrecision: field.NUMERIC_PRECISION,
numLength, numLength,
enumValues,
datePrecision: field.DATETIME_PRECISION, datePrecision: field.DATETIME_PRECISION,
charLength: field.CHARACTER_MAXIMUM_LENGTH, charLength: field.CHARACTER_MAXIMUM_LENGTH,
nullable: field.IS_NULLABLE.includes('YES'), nullable: field.IS_NULLABLE.includes('YES'),
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: field.COLUMN_DEFAULT, default: remappedFields[field.COLUMN_NAME].default,
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'),
@@ -400,6 +461,44 @@ export class MySQLClient extends AntaresCore {
}); });
} }
/**
* CREATE DATABASE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async createSchema (params) {
return await this.raw(`CREATE DATABASE \`${params.name}\` COLLATE ${params.collation}`);
}
/**
* ALTER DATABASE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async alterSchema (params) {
return await this.raw(`ALTER DATABASE \`${params.name}\` COLLATE ${params.collation}`);
}
/**
* DROP DATABASE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async dropSchema (params) {
return await this.raw(`DROP DATABASE \`${params.database}\``);
}
/**
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async getDatabaseCollation (params) {
return await this.raw(`SELECT \`DEFAULT_COLLATION_NAME\` FROM \`information_schema\`.\`SCHEMATA\` WHERE \`SCHEMA_NAME\`='${params.database}'`);
}
/** /**
* SHOW CREATE VIEW * SHOW CREATE VIEW
* *
@@ -429,7 +528,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropView (params) { async dropView (params) {
const sql = `DROP VIEW \`${params.view}\``; const sql = `DROP VIEW \`${this._schema}\`.\`${params.view}\``;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -441,10 +540,10 @@ export class MySQLClient extends AntaresCore {
*/ */
async alterView (params) { async alterView (params) {
const { view } = params; const { view } = params;
let sql = `ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''} SQL SECURITY ${view.security} VIEW \`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}`; let sql = `ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''} SQL SECURITY ${view.security} VIEW \`${this._schema}\`.\`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}`;
if (view.name !== view.oldName) if (view.name !== view.oldName)
sql += `; RENAME TABLE \`${view.oldName}\` TO \`${view.name}\``; sql += `; RENAME TABLE \`${this._schema}\`.\`${view.oldName}\` TO \`${this._schema}\`.\`${view.name}\``;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -456,7 +555,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createView (view) { async createView (view) {
const sql = `CREATE ALGORITHM = ${view.algorithm} ${view.definer ? `DEFINER=${view.definer} ` : ''}SQL SECURITY ${view.security} VIEW \`${view.name}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}`; const sql = `CREATE ALGORITHM = ${view.algorithm} ${view.definer ? `DEFINER=${view.definer} ` : ''}SQL SECURITY ${view.security} VIEW \`${this._schema}\`.\`${view.name}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}`;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -489,7 +588,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropTrigger (params) { async dropTrigger (params) {
const sql = `DROP TRIGGER \`${params.trigger}\``; const sql = `DROP TRIGGER \`${this._schema}\`.\`${params.trigger}\``;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -522,7 +621,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createTrigger (trigger) { async createTrigger (trigger) {
const sql = `CREATE ${trigger.definer ? `DEFINER=${trigger.definer} ` : ''}TRIGGER \`${trigger.name}\` ${trigger.event1} ${trigger.event2} ON \`${trigger.table}\` FOR EACH ROW ${trigger.sql}`; const sql = `CREATE ${trigger.definer ? `DEFINER=${trigger.definer} ` : ''}TRIGGER \`${this._schema}\`.\`${trigger.name}\` ${trigger.event1} ${trigger.event2} ON \`${trigger.table}\` FOR EACH ROW ${trigger.sql}`;
return await this.raw(sql, { split: false }); return await this.raw(sql, { split: false });
} }
@@ -536,7 +635,7 @@ export class MySQLClient extends AntaresCore {
const sql = `SHOW CREATE PROCEDURE \`${schema}\`.\`${routine}\``; const sql = `SHOW CREATE PROCEDURE \`${schema}\`.\`${routine}\``;
const results = await this.raw(sql); const results = await this.raw(sql);
return results.rows.map(row => { return results.rows.map(async row => {
if (!row['Create Procedure']) { if (!row['Create Procedure']) {
return { return {
definer: null, definer: null,
@@ -550,22 +649,23 @@ export class MySQLClient extends AntaresCore {
}; };
} }
const parameters = row['Create Procedure'] const sql = `SELECT *
.match(/(?<=\().*?(?=\))/s)[0] FROM information_schema.parameters
.replaceAll('\r', '') WHERE SPECIFIC_NAME = '${routine}'
.replaceAll('\t', '') AND SPECIFIC_SCHEMA = '${schema}'
.split(',') ORDER BY ORDINAL_POSITION
.map(el => { `;
const param = el.split(' ');
const type = param[2] ? param[2].replace(')', '').split('(') : ['', null];
return { const results = await this.raw(sql);
name: param[1] ? param[1].replaceAll('`', '') : '',
type: type[0], const parameters = results.rows.map(row => {
length: +type[1], return {
context: param[0] ? param[0].replace('\n', '') : '' name: row.PARAMETER_NAME,
}; type: row.DATA_TYPE.toUpperCase(),
}).filter(el => el.name); length: row.NUMERIC_PRECISION || row.DATETIME_PRECISION || row.CHARACTER_MAXIMUM_LENGTH || '',
context: row.PARAMETER_MODE
};
});
let dataAccess = 'CONTAINS SQL'; let dataAccess = 'CONTAINS SQL';
if (row['Create Procedure'].includes('NO SQL')) if (row['Create Procedure'].includes('NO SQL'))
@@ -578,7 +678,7 @@ export class MySQLClient extends AntaresCore {
return { return {
definer: row['Create Procedure'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0], definer: row['Create Procedure'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
sql: row['Create Procedure'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0], sql: row['Create Procedure'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
parameters, parameters: parameters || [],
name: row.Procedure, name: row.Procedure,
comment: row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '', comment: row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '',
security: row['Create Procedure'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER', security: row['Create Procedure'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER',
@@ -595,7 +695,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropRoutine (params) { async dropRoutine (params) {
const sql = `DROP PROCEDURE \`${params.routine}\``; const sql = `DROP PROCEDURE \`${this._schema}\`.\`${params.routine}\``;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -628,12 +728,14 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createRoutine (routine) { async createRoutine (routine) {
const parameters = routine.parameters.reduce((acc, curr) => { const parameters = 'parameters' in routine
acc.push(`${curr.context} \`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`); ? routine.parameters.reduce((acc, curr) => {
return acc; acc.push(`${curr.context} \`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
}, []).join(','); return acc;
}, []).join(',')
: '';
const sql = `CREATE ${routine.definer ? `DEFINER=${routine.definer} ` : ''}PROCEDURE \`${routine.name}\`(${parameters}) const sql = `CREATE ${routine.definer ? `DEFINER=${routine.definer} ` : ''}PROCEDURE \`${this._schema}\`.\`${routine.name}\`(${parameters})
LANGUAGE SQL LANGUAGE SQL
${routine.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'} ${routine.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'}
${routine.dataAccess} ${routine.dataAccess}
@@ -654,7 +756,7 @@ export class MySQLClient extends AntaresCore {
const sql = `SHOW CREATE FUNCTION \`${schema}\`.\`${func}\``; const sql = `SHOW CREATE FUNCTION \`${schema}\`.\`${func}\``;
const results = await this.raw(sql); const results = await this.raw(sql);
return results.rows.map(row => { return results.rows.map(async row => {
if (!row['Create Function']) { if (!row['Create Function']) {
return { return {
definer: null, definer: null,
@@ -670,21 +772,23 @@ export class MySQLClient extends AntaresCore {
}; };
} }
const parameters = row['Create Function'] const sql = `SELECT *
.match(/(?<=\().*?(?=\))/s)[0] FROM information_schema.parameters
.replaceAll('\r', '') WHERE SPECIFIC_NAME = '${func}'
.replaceAll('\t', '') AND SPECIFIC_SCHEMA = '${schema}'
.split(',') ORDER BY ORDINAL_POSITION
.map(el => { `;
const param = el.split(' ');
const type = param[1] ? param[1].replace(')', '').split('(') : ['', null];
return { const results = await this.raw(sql);
name: param[0] ? param[0].replaceAll('`', '') : '',
type: type[0], const parameters = results.rows.filter(row => row.PARAMETER_MODE).map(row => {
length: +type[1] return {
}; name: row.PARAMETER_NAME,
}).filter(el => el.name); type: row.DATA_TYPE.toUpperCase(),
length: row.NUMERIC_PRECISION || row.DATETIME_PRECISION || row.CHARACTER_MAXIMUM_LENGTH || '',
context: row.PARAMETER_MODE
};
});
let dataAccess = 'CONTAINS SQL'; let dataAccess = 'CONTAINS SQL';
if (row['Create Function'].includes('NO SQL')) if (row['Create Function'].includes('NO SQL'))
@@ -699,7 +803,7 @@ export class MySQLClient extends AntaresCore {
return { return {
definer: row['Create Function'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0], definer: row['Create Function'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
sql: row['Create Function'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0], sql: row['Create Function'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
parameters, parameters: parameters || [],
name: row.Function, name: row.Function,
comment: row['Create Function'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Function'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '', comment: row['Create Function'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Function'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '',
security: row['Create Function'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER', security: row['Create Function'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER',
@@ -718,7 +822,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropFunction (params) { async dropFunction (params) {
const sql = `DROP FUNCTION \`${params.func}\``; const sql = `DROP FUNCTION \`${this._schema}\`.\`${params.func}\``;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -756,13 +860,15 @@ export class MySQLClient extends AntaresCore {
return acc; return acc;
}, []).join(','); }, []).join(',');
const sql = `CREATE ${func.definer ? `DEFINER=${func.definer} ` : ''}FUNCTION \`${func.name}\`(${parameters}) RETURNS ${func.returns}${func.returnsLength ? `(${func.returnsLength})` : ''} const body = func.returns ? func.sql : 'BEGIN\n RETURN 0;\nEND';
const sql = `CREATE ${func.definer ? `DEFINER=${func.definer} ` : ''}FUNCTION \`${this._schema}\`.\`${func.name}\`(${parameters}) RETURNS ${func.returns || 'SMALLINT'}${func.returnsLength ? `(${func.returnsLength})` : ''}
LANGUAGE SQL LANGUAGE SQL
${func.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'} ${func.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'}
${func.dataAccess} ${func.dataAccess}
SQL SECURITY ${func.security} SQL SECURITY ${func.security}
COMMENT '${func.comment}' COMMENT '${func.comment}'
${func.sql}`; ${body}`;
return await this.raw(sql, { split: false }); return await this.raw(sql, { split: false });
} }
@@ -808,7 +914,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropEvent (params) { async dropEvent (params) {
const sql = `DROP EVENT \`${params.scheduler}\``; const sql = `DROP EVENT \`${this._schema}\`.\`${params.scheduler}\``;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -824,13 +930,13 @@ export class MySQLClient extends AntaresCore {
if (scheduler.execution === 'EVERY' && scheduler.every[0].includes('-')) if (scheduler.execution === 'EVERY' && scheduler.every[0].includes('-'))
scheduler.every[0] = `'${scheduler.every[0]}'`; scheduler.every[0] = `'${scheduler.every[0]}'`;
const sql = `ALTER ${scheduler.definer ? ` DEFINER=${scheduler.definer}` : ''} EVENT \`${scheduler.oldName}\` const sql = `ALTER ${scheduler.definer ? ` DEFINER=${scheduler.definer}` : ''} EVENT \`${this._schema}\`.\`${scheduler.oldName}\`
ON SCHEDULE ON SCHEDULE
${scheduler.execution === 'EVERY' ${scheduler.execution === 'EVERY'
? `EVERY ${scheduler.every.join(' ')}${scheduler.starts ? ` STARTS '${scheduler.starts}'` : ''}${scheduler.ends ? ` ENDS '${scheduler.ends}'` : ''}` ? `EVERY ${scheduler.every.join(' ')}${scheduler.starts ? ` STARTS '${scheduler.starts}'` : ''}${scheduler.ends ? ` ENDS '${scheduler.ends}'` : ''}`
: `AT '${scheduler.at}'`} : `AT '${scheduler.at}'`}
ON COMPLETION${!scheduler.preserve ? ' NOT' : ''} PRESERVE ON COMPLETION${!scheduler.preserve ? ' NOT' : ''} PRESERVE
${scheduler.name !== scheduler.oldName ? `RENAME TO \`${scheduler.name}\`` : ''} ${scheduler.name !== scheduler.oldName ? `RENAME TO \`${this._schema}\`.\`${scheduler.name}\`` : ''}
${scheduler.state} ${scheduler.state}
COMMENT '${scheduler.comment}' COMMENT '${scheduler.comment}'
DO ${scheduler.sql}`; DO ${scheduler.sql}`;
@@ -845,7 +951,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createEvent (scheduler) { async createEvent (scheduler) {
const sql = `CREATE ${scheduler.definer ? ` DEFINER=${scheduler.definer}` : ''} EVENT \`${scheduler.name}\` const sql = `CREATE ${scheduler.definer ? ` DEFINER=${scheduler.definer}` : ''} EVENT \`${this._schema}\`.\`${scheduler.name}\`
ON SCHEDULE ON SCHEDULE
${scheduler.execution === 'EVERY' ${scheduler.execution === 'EVERY'
? `EVERY ${scheduler.every.join(' ')}${scheduler.starts ? ` STARTS '${scheduler.starts}'` : ''}${scheduler.ends ? ` ENDS '${scheduler.ends}'` : ''}` ? `EVERY ${scheduler.every.join(' ')}${scheduler.starts ? ` STARTS '${scheduler.starts}'` : ''}${scheduler.ends ? ` ENDS '${scheduler.ends}'` : ''}`
@@ -949,6 +1055,25 @@ export class MySQLClient extends AntaresCore {
}, {}); }, {});
} }
async getProcesses () {
const sql = 'SELECT `ID`, `USER`, `HOST`, `DB`, `COMMAND`, `TIME`, `STATE`, LEFT(`INFO`, 51200) AS `INFO` FROM `information_schema`.`PROCESSLIST`';
const { rows } = await this.raw(sql);
return rows.map(row => {
return {
id: row.ID,
user: row.USER,
host: row.HOST,
db: row.DB,
command: row.COMMAND,
time: row.TIME,
state: row.STATE,
info: row.INFO
};
});
}
/** /**
* CREATE TABLE * CREATE TABLE
* *
@@ -963,7 +1088,7 @@ export class MySQLClient extends AntaresCore {
engine engine
} = params; } = params;
const sql = `CREATE TABLE \`${name}\` (\`${name}_ID\` INT NULL) COMMENT='${comment}', COLLATE='${collation}', ENGINE=${engine}`; const sql = `CREATE TABLE \`${this._schema}\`.\`${name}\` (\`${name}_ID\` INT NULL) COMMENT='${comment}', COLLATE='${collation}', ENGINE=${engine}`;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -985,7 +1110,7 @@ export class MySQLClient extends AntaresCore {
options options
} = params; } = params;
let sql = `ALTER TABLE \`${table}\` `; let sql = `ALTER TABLE \`${this._schema}\`.\`${table}\` `;
const alterColumns = []; const alterColumns = [];
// OPTIONS // OPTIONS
@@ -997,7 +1122,7 @@ export class MySQLClient extends AntaresCore {
// ADD FIELDS // ADD FIELDS
additions.forEach(addition => { additions.forEach(addition => {
const typeInfo = this._getTypeInfo(addition.type); const typeInfo = this._getTypeInfo(addition.type);
const length = typeInfo.length ? 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})` : ''}
@@ -1035,7 +1160,7 @@ export class MySQLClient extends AntaresCore {
// CHANGE FIELDS // CHANGE FIELDS
changes.forEach(change => { changes.forEach(change => {
const typeInfo = this._getTypeInfo(change.type); const typeInfo = this._getTypeInfo(change.type);
const length = typeInfo.length ? 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})` : ''}
@@ -1097,7 +1222,7 @@ export class MySQLClient extends AntaresCore {
sql += alterColumns.join(', '); sql += alterColumns.join(', ');
// RENAME // RENAME
if (options.name) sql += `; RENAME TABLE \`${table}\` TO \`${options.name}\``; if (options.name) sql += `; RENAME TABLE \`${this._schema}\`.\`${table}\` TO \`${this._schema}\`.\`${options.name}\``;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -1109,7 +1234,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async truncateTable (params) { async truncateTable (params) {
const sql = `TRUNCATE TABLE \`${params.table}\``; const sql = `TRUNCATE TABLE \`${this._schema}\`.\`${params.table}\``;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -1120,7 +1245,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropTable (params) { async dropTable (params) {
const sql = `DROP TABLE \`${params.table}\``; const sql = `DROP TABLE \`${this._schema}\`.\`${params.table}\``;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -1188,104 +1313,111 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient * @memberof MySQLClient
*/ */
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,
...args ...args
}; };
const nestTables = args.nest ? '.' : false; const nestTables = args.nest ? '.' : false;
const resultsArr = []; const resultsArr = [];
let paramsArr = []; let paramsArr = [];
const queries = args.split ? sql.split(';') : [sql]; const queries = args.split ? sql.split(/((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/gm) : [sql];
const isPool = typeof this._connection.getConnection === 'function';
const connection = isPool ? await this._connection.getConnection() : this._connection;
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder if (args.schema)
await connection.query(`USE \`${args.schema}\``);
for (const query of queries) { for (const query of queries) {
if (!query) continue; if (!query) continue;
const timeStart = new Date();
let timeStop;
let keysArr = []; let keysArr = [];
const { rows, report, fields, keys } = await new Promise((resolve, reject) => { const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => {
this._connection.query({ sql: query, nestTables }, async (err, response, fields) => { connection.query({ sql: query, nestTables }).then(async ([response, fields]) => {
timeStop = new Date();
const queryResult = response; const queryResult = response;
if (err) let remappedFields = fields
reject(err); ? fields.map(field => {
else { if (!field || Array.isArray(field))
let remappedFields = fields return false;
? fields.map(field => {
if (!field || Array.isArray(field))
return false;
const type = this._getType(field); const type = this._getType(field);
return {
name: field.orgName,
alias: field.name,
orgName: field.orgName,
schema: field.schema,
table: field.table,
tableAlias: field.table,
orgTable: field.orgTable,
type: type.name,
length: type.length
};
}).filter(Boolean)
: [];
if (args.details) {
let cachedTable;
if (remappedFields.length) {
paramsArr = remappedFields.map(field => {
if (field.orgTable) cachedTable = field.orgTable;// Needed for some queries on information_schema
return { return {
name: field.orgName, table: field.orgTable || cachedTable,
alias: field.name, schema: field.schema || 'INFORMATION_SCHEMA'
orgName: field.orgName,
schema: field.db,
table: field.table,
tableAlias: field.table,
zerofill: field.zerofill,
orgTable: field.orgTable,
type: type.name,
length: type.length
}; };
}).filter(Boolean) }).filter((val, i, arr) => arr.findIndex(el => el.schema === val.schema && el.table === val.table) === i);
: [];
if (args.details) { for (const paramObj of paramsArr) {
let cachedTable; if (!paramObj.table || !paramObj.schema) continue;
if (remappedFields.length) { try { // Column details
paramsArr = remappedFields.map(field => { const response = await this.getTableColumns(paramObj);
if (field.orgTable) cachedTable = field.orgTable;// Needed for some queries on information_schema remappedFields = remappedFields.map(field => {
return { const detailedField = response.find(f => f.name === field.name);
table: field.orgTable || cachedTable, if (detailedField && field.orgTable === paramObj.table && field.schema === paramObj.schema)
schema: field.schema || 'INFORMATION_SCHEMA' field = { ...field, ...detailedField };
}; return field;
}).filter((val, i, arr) => arr.findIndex(el => el.schema === val.schema && el.table === val.table) === i); });
}
catch (err) {
reject(err);
}
for (const paramObj of paramsArr) { try { // Key usage (foreign keys)
if (!paramObj.table || !paramObj.schema) continue; const response = await this.getKeyUsage(paramObj);
keysArr = keysArr ? [...keysArr, ...response] : response;
try { // Column details }
const response = await this.getTableColumns(paramObj); catch (err) {
remappedFields = remappedFields.map(field => { reject(err);
const detailedField = response.find(f => f.name === field.name);
if (detailedField && field.orgTable === paramObj.table && field.schema === paramObj.schema && detailedField.name === field.orgName)
field = { ...detailedField, ...field };
return field;
});
}
catch (err) {
reject(err);
}
try { // Key usage (foreign keys)
const response = await this.getKeyUsage(paramObj);
keysArr = keysArr ? [...keysArr, ...response] : response;
}
catch (err) {
reject(err);
}
} }
} }
} }
resolve({
rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false,
report: !Array.isArray(queryResult) ? queryResult : false,
fields: remappedFields,
keys: keysArr
});
} }
});
resolve({
duration: timeStop - timeStart,
rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false,
report: !Array.isArray(queryResult) ? queryResult : false,
fields: remappedFields,
keys: keysArr
});
}).catch(reject);
}); });
resultsArr.push({ rows, report, fields, keys }); resultsArr.push({ rows, report, fields, keys, duration });
} }
if (isPool) connection.release();
return resultsArr.length === 1 ? resultsArr[0] : resultsArr; return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
<template> <template>
<div id="wrapper"> <div id="wrapper" :class="`theme-${applicationTheme}`">
<TheTitleBar /> <TheTitleBar />
<div id="window-content"> <div id="window-content">
<TheSettingBar /> <TheSettingBar />
@@ -16,6 +16,7 @@
<TheFooter /> <TheFooter />
<TheNotificationsBoard /> <TheNotificationsBoard />
<ModalNewConnection v-if="isNewConnModal" /> <ModalNewConnection v-if="isNewConnModal" />
<TheScratchpad v-if="isScratchpad" />
<ModalSettings v-if="isSettingModal" /> <ModalSettings v-if="isSettingModal" />
<ModalDiscardChanges v-if="isUnsavedDiscardModal" /> <ModalDiscardChanges v-if="isUnsavedDiscardModal" />
</div> </div>
@@ -25,6 +26,7 @@
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { Menu, getCurrentWindow } from '@electron/remote';
export default { export default {
name: 'App', name: 'App',
@@ -37,6 +39,7 @@ export default {
Workspace: () => import(/* webpackChunkName: "Workspace" */'@/components/Workspace'), Workspace: () => import(/* webpackChunkName: "Workspace" */'@/components/Workspace'),
ModalNewConnection: () => import(/* webpackChunkName: "ModalNewConnection" */'@/components/ModalNewConnection'), ModalNewConnection: () => import(/* webpackChunkName: "ModalNewConnection" */'@/components/ModalNewConnection'),
ModalSettings: () => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings'), ModalSettings: () => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings'),
TheScratchpad: () => import(/* webpackChunkName: "TheScratchpad" */'@/components/TheScratchpad'),
ModalDiscardChanges: () => import(/* webpackChunkName: "ModalDiscardChanges" */'@/components/ModalDiscardChanges') ModalDiscardChanges: () => import(/* webpackChunkName: "ModalDiscardChanges" */'@/components/ModalDiscardChanges')
}, },
data () { data () {
@@ -48,16 +51,57 @@ export default {
isNewConnModal: 'application/isNewModal', isNewConnModal: 'application/isNewModal',
isEditModal: 'application/isEditModal', isEditModal: 'application/isEditModal',
isSettingModal: 'application/isSettingModal', isSettingModal: 'application/isSettingModal',
isScratchpad: 'application/isScratchpad',
connections: 'connections/getConnections', connections: 'connections/getConnections',
applicationTheme: 'settings/getApplicationTheme',
isUnsavedDiscardModal: 'workspaces/isUnsavedDiscardModal' isUnsavedDiscardModal: 'workspaces/isUnsavedDiscardModal'
}) })
}, },
mounted () { mounted () {
ipcRenderer.send('check-for-updates'); ipcRenderer.send('check-for-updates');
this.checkVersionUpdate();
const InputMenu = Menu.buildFromTemplate([
{
label: this.$t('word.cut'),
role: 'cut'
},
{
label: this.$t('word.copy'),
role: 'copy'
},
{
label: this.$t('word.paste'),
role: 'paste'
},
{
type: 'separator'
},
{
label: this.$t('message.selectAll'),
role: 'selectall'
}
]);
document.body.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
let node = e.target;
while (node) {
if (node.nodeName.match(/^(input|textarea)$/i) || node.isContentEditable) {
InputMenu.popup(getCurrentWindow());
break;
}
node = node.parentNode;
}
});
}, },
methods: { methods: {
...mapActions({ ...mapActions({
showNewConnModal: 'application/showNewConnModal' showNewConnModal: 'application/showNewConnModal',
checkVersionUpdate: 'application/checkVersionUpdate'
}) })
} }
}; };

View File

@@ -24,7 +24,7 @@
<slot name="body" /> <slot name="body" />
</div> </div>
</div> </div>
<div class="modal-footer"> <div v-if="!hideFooter" class="modal-footer">
<button <button
class="btn btn-primary mr-2" class="btn btn-primary mr-2"
@click.stop="confirmModal" @click.stop="confirmModal"
@@ -51,6 +51,10 @@ export default {
validator: prop => ['small', 'medium', '400', 'large'].includes(prop), validator: prop => ['small', 'medium', '400', 'large'].includes(prop),
default: 'small' default: 'small'
}, },
hideFooter: {
type: Boolean,
default: false
},
confirmText: String, confirmText: String,
cancelText: String cancelText: String
}, },
@@ -74,6 +78,12 @@ export default {
else return ''; else return '';
} }
}, },
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: { methods: {
confirmModal () { confirmModal () {
this.$emit('confirm'); this.$emit('confirm');
@@ -82,6 +92,11 @@ export default {
hideModal () { hideModal () {
this.$emit('hide'); this.$emit('hide');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.hideModal();
} }
} }
}; };

View File

@@ -71,7 +71,6 @@ export default {
<style lang="scss"> <style lang="scss">
.context { .context {
display: flex; display: flex;
color: $body-font-color;
font-size: 16px; font-size: 16px;
z-index: 400; z-index: 400;
justify-content: center; justify-content: center;
@@ -87,7 +86,6 @@ export default {
.context-container { .context-container {
min-width: 100px; min-width: 100px;
z-index: 10; z-index: 10;
box-shadow: 0 0 2px 0 #000;
padding: 0; padding: 0;
background: #1d1d1d; background: #1d1d1d;
border-radius: 0.1rem; border-radius: 0.1rem;
@@ -111,14 +109,10 @@ export default {
position: absolute; position: absolute;
left: 100%; left: 100%;
top: 0; top: 0;
background: #1d1d1d;
box-shadow: 0 0 2px 0 #000;
min-width: 100px; min-width: 100px;
} }
&:hover { &:hover {
background: $primary-color;
.context-submenu { .context-submenu {
display: block; display: block;
visibility: visible; visibility: visible;

View File

@@ -0,0 +1,117 @@
<template>
<div class="editor-wrapper">
<div
:id="`editor-${id}`"
class="editor"
:class="editorClass"
:style="{height: `${height}px`}"
/>
</div>
</template>
<script>
import * as ace from 'ace-builds';
import 'ace-builds/webpack-resolver';
import { mapGetters } from 'vuex';
export default {
name: 'BaseTextEditor',
props: {
value: String,
mode: { type: String, default: 'text' },
editorClass: { type: String, default: '' },
autoFocus: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false },
showLineNumbers: { type: Boolean, default: true },
height: { type: Number, default: 200 }
},
data () {
return {
editor: null,
id: null
};
},
computed: {
...mapGetters({
editorTheme: 'settings/getEditorTheme',
autoComplete: 'settings/getAutoComplete',
lineWrap: 'settings/getLineWrap'
})
},
watch: {
mode () {
if (this.editor)
this.editor.session.setMode(`ace/mode/${this.mode}`);
},
editorTheme () {
if (this.editor)
this.editor.setTheme(`ace/theme/${this.editorTheme}`);
},
autoComplete () {
if (this.editor) {
this.editor.setOptions({
enableLiveAutocompletion: this.autoComplete
});
}
},
lineWrap () {
if (this.editor) {
this.editor.setOptions({
wrap: this.lineWrap
});
}
}
},
created () {
this.id = this._uid;
},
mounted () {
this.editor = ace.edit(`editor-${this.id}`, {
mode: `ace/mode/${this.mode}`,
theme: `ace/theme/${this.editorTheme}`,
value: this.value || '',
fontSize: '14px',
printMargin: false,
readOnly: this.readOnly,
showLineNumbers: this.showLineNumbers,
showGutter: this.showLineNumbers
});
this.editor.setOptions({
enableBasicAutocompletion: false,
wrap: this.lineWrap,
enableSnippets: false,
enableLiveAutocompletion: false
});
this.editor.session.on('change', () => {
const content = this.editor.getValue();
this.$emit('update:value', content);
});
if (this.autoFocus) {
setTimeout(() => {
this.editor.focus();
this.editor.resize();
}, 20);
}
setTimeout(() => {
this.editor.resize();
}, 20);
}
};
</script>
<style lang="scss" scoped>
.editor-wrapper {
.editor {
width: 100%;
}
}
.ace_.mdi {
display: inline-block;
width: 17px;
}
</style>

View File

@@ -63,13 +63,11 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.file-uploader { .file-uploader {
border: 0.05rem solid $bg-color-light;
border-radius: 0.1rem; border-radius: 0.1rem;
height: 1.8rem; height: 1.8rem;
line-height: 1.2rem; line-height: 1.2rem;
display: flex; display: flex;
cursor: pointer; cursor: pointer;
background-color: $bg-color-gray;
transition: background 0.2s, border 0.2s, box-shadow 0.2s, color 0.2s; transition: background 0.2s, border 0.2s, box-shadow 0.2s, color 0.2s;
position: relative; position: relative;
flex: 1 1 auto; flex: 1 1 auto;
@@ -80,8 +78,6 @@ export default {
.file-uploader-message { .file-uploader-message {
display: flex; display: flex;
border-right: 0.05rem solid $bg-color-light;
background-color: $bg-color;
} }
.file-uploader-input { .file-uploader-input {
@@ -105,7 +101,6 @@ export default {
:disabled { :disabled {
.file-uploader { .file-uploader {
cursor: not-allowed; cursor: not-allowed;
background-color: #151515;
opacity: 0.5; opacity: 0.5;
} }
} }

View File

@@ -41,14 +41,16 @@ export default {
localScrollElement: null localScrollElement: null
}; };
}, },
watch: {
scrollElement () {
this.setScrollElement();
}
},
mounted () { mounted () {
this._checkScrollPosition = this.checkScrollPosition.bind(this); this.setScrollElement();
this.localScrollElement = this.scrollElement ? this.scrollElement : this.$el;
this.updateWindow();
this.localScrollElement.addEventListener('scroll', this._checkScrollPosition);
}, },
beforeDestroy () { beforeDestroy () {
this.localScrollElement.removeEventListener('scroll', this._checkScrollPosition); this.localScrollElement.removeEventListener('scroll', this.checkScrollPosition);
}, },
methods: { methods: {
checkScrollPosition (e) { checkScrollPosition (e) {
@@ -58,7 +60,7 @@ export default {
this.updateWindow(e); this.updateWindow(e);
}, 200); }, 200);
}, },
updateWindow (e) { updateWindow () {
const visibleItemsCount = Math.ceil(this.visibleHeight / this.itemHeight); const visibleItemsCount = Math.ceil(this.visibleHeight / this.itemHeight);
const totalScrollHeight = this.items.length * this.itemHeight; const totalScrollHeight = this.items.length * this.itemHeight;
const offset = 50; const offset = 50;
@@ -74,6 +76,14 @@ export default {
this.topHeight = firstCutIndex * this.itemHeight; this.topHeight = firstCutIndex * this.itemHeight;
this.bottomHeight = totalScrollHeight - this.visibleItems.length * this.itemHeight - this.topHeight; this.bottomHeight = totalScrollHeight - this.visibleItems.length * this.itemHeight - this.topHeight;
},
setScrollElement () {
if (this.localScrollElement)
this.localScrollElement.removeEventListener('scroll', this.checkScrollPosition);
this.localScrollElement = this.scrollElement ? this.scrollElement : this.$el;
this.updateWindow();
this.localScrollElement.addEventListener('scroll', this.checkScrollPosition);
} }
} }
}; };

View File

@@ -66,6 +66,21 @@
:type="inputProps().type" :type="inputProps().type"
:disabled="!isChecked" :disabled="!isChecked"
> >
<select
v-else-if="enumArray"
v-model="selectedValue"
class="form-select"
:disabled="!isChecked"
@change="onChange"
>
<option
v-for="val in enumArray"
:key="val"
:value="val"
>
{{ val }}
</option>
</select>
<input <input
v-else v-else
ref="formInput" ref="formInput"
@@ -90,7 +105,7 @@
</template> </template>
<script> <script>
import { mask } from 'vue-the-mask'; import { VueMaskDirective } from 'v-mask';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import BaseUploadInput from '@/components/BaseUploadInput'; import BaseUploadInput from '@/components/BaseUploadInput';
import ForeignKeySelect from '@/components/ForeignKeySelect'; import ForeignKeySelect from '@/components/ForeignKeySelect';
@@ -103,7 +118,7 @@ export default {
BaseUploadInput BaseUploadInput
}, },
directives: { directives: {
mask mask: VueMaskDirective
}, },
props: { props: {
type: String, type: String,
@@ -121,7 +136,8 @@ export default {
selectedMethod: '', selectedMethod: '',
selectedValue: '', selectedValue: '',
debounceTimeout: null, debounceTimeout: null,
methodParams: {} methodParams: {},
enumArray: null
}; };
}, },
computed: { computed: {
@@ -136,6 +152,8 @@ export default {
this.localType = 'datetime'; this.localType = 'datetime';
else if (TIME.includes(this.type)) else if (TIME.includes(this.type))
this.localType = 'time'; this.localType = 'time';
else
this.localType = 'none';
return FakerMethods.getGroupsByType(this.localType); return FakerMethods.getGroupsByType(this.localType);
}, },
@@ -148,8 +166,14 @@ export default {
}, },
watch: { watch: {
fieldObj () { fieldObj () {
if (this.fieldObj) if (this.fieldObj) {
this.selectedValue = this.fieldObj.value; if (Array.isArray(this.fieldObj.value)) {
this.enumArray = this.fieldObj.value;
this.selectedValue = this.fieldObj.value[0];
}
else
this.selectedValue = this.fieldObj.value;
}
}, },
selectedGroup () { selectedGroup () {
if (this.fakerMethods.length) if (this.fakerMethods.length)

View File

@@ -11,11 +11,11 @@
</option> </option>
<option <option
v-for="row in foreignList" v-for="row in foreignList"
:key="row.foreignColumn" :key="row.foreign_column"
:value="row.foreignColumn" :value="row.foreign_column"
:selected="row.foreignColumn === value" :selected="row.foreign_column === value"
> >
{{ row.foreignColumn }} {{ 'foreignDescription' in row ? ` - ${row.foreignDescription}` : '' | cutText }} {{ row.foreign_column }} {{ 'foreign_description' in row ? ` - ${row.foreign_description}` : '' | cutText }}
</option> </option>
</select> </select>
</template> </template>
@@ -51,11 +51,12 @@ export default {
}), }),
isValidDefault () { isValidDefault () {
if (!this.foreignList.length) return true; if (!this.foreignList.length) return true;
return this.foreignList.some(foreign => foreign.foreignColumn.toString() === this.value.toString()); if (this.value === null) return false;
return this.foreignList.some(foreign => foreign.foreign_column.toString() === this.value.toString());
} }
}, },
async created () { async created () {
let firstTextField; let foreignDesc;
const params = { const params = {
uid: this.selectedWorkspace, uid: this.selectedWorkspace,
schema: this.keyUsage.refSchema, schema: this.keyUsage.refSchema,
@@ -64,8 +65,10 @@ export default {
try { // Field data try { // Field data
const { status, response } = await Tables.getTableColumns(params); const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') if (status === 'success') {
firstTextField = response.find(field => [...TEXT, ...LONG_TEXT].includes(field.type)).name || false; const textField = response.find(field => [...TEXT, ...LONG_TEXT].includes(field.type) && field.name !== this.keyUsage.refField);
foreignDesc = textField ? textField.name : false;
}
else else
this.addNotification({ status: 'error', message: response }); this.addNotification({ status: 'error', message: response });
} }
@@ -77,7 +80,7 @@ export default {
const { status, response } = await Tables.getForeignList({ const { status, response } = await Tables.getForeignList({
...params, ...params,
column: this.keyUsage.refField, column: this.keyUsage.refField,
description: firstTextField description: foreignDesc
}); });
if (status === 'success') if (status === 'success')

View File

@@ -0,0 +1,129 @@
<template>
<ConfirmModal
:confirm-text="$t('word.run')"
:cancel-text="$t('word.cancel')"
size="400"
@confirm="runRoutine"
@hide="closeModal"
>
<template slot="header">
<div class="d-flex">
<i class="mdi mdi-24px mdi-play mr-1" /> {{ $t('word.parameters') }}: {{ localRoutine.name }}
</div>
</template>
<div slot="body">
<div class="content">
<form class="form-horizontal">
<div
v-for="(parameter, i) in inParameters"
:key="parameter._id"
class="form-group"
>
<div class="col-3">
<label class="form-label">{{ parameter.name }}</label>
</div>
<div class="col-9">
<div class="input-group">
<input
:ref="i === 0 ? 'firstInput' : ''"
v-model="values[`${i}-${parameter.name}`]"
class="form-input"
type="text"
>
<span class="input-group-addon field-type" :class="typeClass(parameter.type)">
{{ parameter.type }} {{ parameter.length | wrapNumber }}
</span>
</div>
</div>
</div>
</form>
</div>
</div>
</ConfirmModal>
</template>
<script>
import { NUMBER, FLOAT } from 'common/fieldTypes';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalAskParameters',
components: {
ConfirmModal
},
filters: {
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
},
props: {
localRoutine: Object,
client: String
},
data () {
return {
values: {}
};
},
computed: {
inParameters () {
return this.localRoutine.parameters.filter(param => param.context === 'IN');
}
},
created () {
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput[0].focus();
}, 20);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
runRoutine () {
const valArr = Object.keys(this.values).reduce((acc, curr, i) => {
let qc;
switch (this.client) {
case 'maria':
case 'mysql':
qc = '"';
break;
case 'pg':
qc = '\'';
break;
default:
qc = '"';
}
const param = this.localRoutine.parameters.find(param => `${i}-${param.name}` === curr);
const value = [...NUMBER, ...FLOAT].includes(param.type) ? this.values[curr] : `${qc}${this.values[curr]}${qc}`;
acc.push(value);
return acc;
}, []);
this.$emit('confirm', valArr);
},
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
};
</script>
<style scoped>
.field-type {
font-size: 0.6rem;
}
</style>

View File

@@ -59,15 +59,15 @@
<option value="maria"> <option value="maria">
MariaDB MariaDB
</option> </option>
<option value="pg">
PostgreSQL
</option>
<!-- <option value="mssql"> <!-- <option value="mssql">
Microsoft SQL Microsoft SQL
</option> </option>
<option value="pg"> <option value="oracledb">
PostgreSQL Oracle DB
</option> </option> -->
<option value="oracledb">
Oracle DB
</option> -->
</select> </select>
</div> </div>
</div> </div>
@@ -97,6 +97,18 @@
> >
</div> </div>
</div> </div>
<div v-if="customizations.database" class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.database"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group"> <div class="form-group">
<div class="col-4 col-sm-12"> <div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label> <label class="form-label">{{ $t('word.user') }}</label>
@@ -247,6 +259,7 @@
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import customizations from 'common/customizations';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import ModalAskCredentials from '@/components/ModalAskCredentials'; import ModalAskCredentials from '@/components/ModalAskCredentials';
import BaseToast from '@/components/BaseToast'; import BaseToast from '@/components/BaseToast';
@@ -274,6 +287,11 @@ export default {
selectedTab: 'general' selectedTab: 'general'
}; };
}, },
computed: {
customizations () {
return customizations[this.connection.client];
}
},
created () { created () {
this.localConnection = Object.assign({}, this.connection); this.localConnection = Object.assign({}, this.connection);
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);

View File

@@ -5,7 +5,7 @@
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-database-edit mr-1" /> {{ $t('message.editDatabase') }} <i class="mdi mdi-24px mdi-database-edit mr-1" /> {{ $t('message.editSchema') }}
</div> </div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -23,7 +23,7 @@
class="form-input" class="form-input"
type="text" type="text"
required required
:placeholder="$t('message.databaseName')" :placeholder="$t('message.schemaName')"
readonly readonly
> >
</div> </div>
@@ -53,7 +53,7 @@
</div> </div>
</div> </div>
<div class="modal-footer text-light"> <div class="modal-footer text-light">
<button class="btn btn-primary mr-2" @click.stop="updateDatabase"> <button class="btn btn-primary mr-2" @click.stop="updateSchema">
{{ $t('word.update') }} {{ $t('word.update') }}
</button> </button>
<button class="btn btn-link" @click.stop="closeModal"> <button class="btn btn-link" @click.stop="closeModal">
@@ -66,10 +66,10 @@
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import Database from '@/ipc-api/Database'; import Schema from '@/ipc-api/Schema';
export default { export default {
name: 'ModalEditDatabase', name: 'ModalEditSchema',
props: { props: {
selectedDatabase: String selectedDatabase: String
}, },
@@ -98,7 +98,7 @@ export default {
async created () { async created () {
let actualCollation; let actualCollation;
try { try {
const { status, response } = await Database.getDatabaseCollation({ uid: this.selectedWorkspace, database: this.selectedDatabase }); const { status, response } = await Schema.getDatabaseCollation({ uid: this.selectedWorkspace, database: this.selectedDatabase });
if (status === 'success') if (status === 'success')
actualCollation = response; actualCollation = response;
@@ -130,10 +130,10 @@ export default {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification' addNotification: 'notifications/addNotification'
}), }),
async updateDatabase () { async updateSchema () {
if (this.database.collation !== this.database.prevCollation) { if (this.database.collation !== this.database.prevCollation) {
try { try {
const { status, response } = await Database.updateDatabase({ const { status, response } = await Schema.updateSchema({
uid: this.selectedWorkspace, uid: this.selectedWorkspace,
...this.database ...this.database
}); });

View File

@@ -34,7 +34,7 @@
:field-obj="localRow[field.name]" :field-obj="localRow[field.name]"
:value.sync="localRow[field.name]" :value.sync="localRow[field.name]"
> >
<span class="input-group-addon field-type" :class="`type-${field.type.toLowerCase()}`"> <span class="input-group-addon field-type" :class="typeClass(field.type)">
{{ field.type }} {{ fieldLength(field) | wrapNumber }} {{ field.type }} {{ fieldLength(field) | wrapNumber }}
</span> </span>
<label class="form-checkbox ml-3" :title="$t('word.insert')"> <label class="form-checkbox ml-3" :title="$t('word.insert')">
@@ -184,8 +184,7 @@
<script> <script>
import moment from 'moment'; import moment from 'moment';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { mask } from 'vue-the-mask';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import FakerSelect from '@/components/FakerSelect'; import FakerSelect from '@/components/FakerSelect';
@@ -195,9 +194,6 @@ export default {
components: { components: {
FakerSelect FakerSelect
}, },
directives: {
mask
},
filters: { filters: {
wrapNumber (num) { wrapNumber (num) {
if (!num) return ''; if (!num) return '';
@@ -254,14 +250,22 @@ export default {
if (field.default === 'NULL') fieldDefault = null; if (field.default === 'NULL') fieldDefault = null;
else { else {
if ([...NUMBER, ...FLOAT].includes(field.type)) if ([...NUMBER, ...FLOAT].includes(field.type))
fieldDefault = +field.default; fieldDefault = Number.isNaN(+field.default) ? null : +field.default;
if ([...TEXT, ...LONG_TEXT].includes(field.type)) if ([...TEXT, ...LONG_TEXT].includes(field.type)) {
fieldDefault = field.default ? field.default.substring(1, field.default.length - 1) : ''; fieldDefault = field.default
? field.default.includes('\'')
? field.default.split('\'')[1]
: field.default
: '';
}
if ([...TIME, ...DATE].includes(field.type)) if ([...TIME, ...DATE].includes(field.type))
fieldDefault = field.default; fieldDefault = field.default;
if (BIT.includes(field.type))
fieldDefault = field.default.replaceAll('\'', '').replaceAll('b', '');
if (DATETIME.includes(field.type)) { if (DATETIME.includes(field.type)) {
if (field.default && field.default.toLowerCase().includes('current_timestamp')) { if (field.default && field.default.toLowerCase().includes('current_timestamp')) {
let datePrecision = ''; let datePrecision = '';
@@ -270,6 +274,9 @@ export default {
fieldDefault = moment().format(`YYYY-MM-DD HH:mm:ss${datePrecision}`); fieldDefault = moment().format(`YYYY-MM-DD HH:mm:ss${datePrecision}`);
} }
} }
if (field.enumValues)
fieldDefault = field.enumValues.replaceAll('\'', '').split(',');
} }
rowObj[field.name] = { value: fieldDefault }; rowObj[field.name] = { value: fieldDefault };
@@ -287,6 +294,11 @@ export default {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification' addNotification: 'notifications/addNotification'
}), }),
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
async insertRows () { async insertRows () {
this.isInserting = true; this.isInserting = true;
const rowToInsert = this.localRow; const rowToInsert = this.localRow;

View File

@@ -63,12 +63,12 @@
<option value="maria"> <option value="maria">
MariaDB MariaDB
</option> </option>
<option value="pg">
PostgreSQL
</option>
<!-- <option value="mssql"> <!-- <option value="mssql">
Microsoft SQL Microsoft SQL
</option> </option>
<option value="pg">
PostgreSQL
</option>
<option value="oracledb"> <option value="oracledb">
Oracle DB Oracle DB
</option> --> </option> -->
@@ -101,6 +101,18 @@
> >
</div> </div>
</div> </div>
<div v-if="customizations.database" class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.database"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group"> <div class="form-group">
<div class="col-4 col-sm-12"> <div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label> <label class="form-label">{{ $t('word.user') }}</label>
@@ -251,6 +263,7 @@
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import customizations from 'common/customizations';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import ModalAskCredentials from '@/components/ModalAskCredentials'; import ModalAskCredentials from '@/components/ModalAskCredentials';
@@ -270,8 +283,9 @@ export default {
name: '', name: '',
client: 'mysql', client: 'mysql',
host: '127.0.0.1', host: '127.0.0.1',
port: '3306', database: null,
user: 'root', port: null,
user: null,
password: '', password: '',
ask: false, ask: false,
uid: uidGen('C'), uid: uidGen('C'),
@@ -291,7 +305,13 @@ export default {
selectedTab: 'general' selectedTab: 'general'
}; };
}, },
computed: {
customizations () {
return customizations[this.connection.client];
}
},
created () { created () {
this.setDefaults();
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
setTimeout(() => { setTimeout(() => {
@@ -307,20 +327,9 @@ export default {
addConnection: 'connections/addConnection' addConnection: 'connections/addConnection'
}), }),
setDefaults () { setDefaults () {
switch (this.connection.client) { this.connection.user = this.customizations.defaultUser;
case 'mysql': this.connection.port = this.customizations.defaultPort;
this.connection.port = '3306'; this.connection.database = this.customizations.defaultDatabase;
break;
case 'mssql':
this.connection.port = '1433';
break;
case 'pg':
this.connection.port = '5432';
break;
case 'oracledb':
this.connection.port = '1521';
break;
}
}, },
async startTest () { async startTest () {
this.isTesting = true; this.isTesting = true;

View File

@@ -7,7 +7,7 @@
> >
<template :slot="'header'"> <template :slot="'header'">
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewRoutine') }} <i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewFunction') }}
</div> </div>
</template> </template>
<div :slot="'body'"> <div :slot="'body'">
@@ -25,7 +25,19 @@
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.languages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="localFunction.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.definer') }} {{ $t('word.definer') }}
</label> </label>
@@ -53,42 +65,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.returns') }}
</label>
<div class="column">
<div class="input-group">
<select
v-model="localFunction.returns"
class="form-select text-uppercase"
style="width: 0;"
>
<optgroup
v-for="group in workspace.dataTypes"
:key="group.group"
:label="group.group"
>
<option
v-for="type in group.types"
:key="type.name"
:selected="localFunction.returns === type.name"
:value="type.name"
>
{{ type.name }}
</option>
</optgroup>
</select>
<input
v-model="localFunction.returnsLength"
class="form-input"
type="number"
min="0"
>
</div>
</div>
</div>
<div class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.comment') }} {{ $t('word.comment') }}
</label> </label>
@@ -111,7 +88,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.functionDataAccess" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('message.dataAccess') }} {{ $t('message.dataAccess') }}
</label> </label>
@@ -124,7 +101,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.functionDeterministic" class="form-group">
<div class="col-4" /> <div class="col-4" />
<div class="column"> <div class="column">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
@@ -152,11 +129,12 @@ export default {
return { return {
localFunction: { localFunction: {
definer: '', definer: '',
sql: 'BEGIN\r\n RETURN NULL;\r\nEND', sql: '',
parameters: [], parameters: [],
name: '', name: '',
comment: '', comment: '',
returns: 'INT', language: null,
returns: null,
returnsLength: 10, returnsLength: 10,
security: 'DEFINER', security: 'DEFINER',
deterministic: false, deterministic: false,
@@ -168,9 +146,17 @@ export default {
computed: { computed: {
schema () { schema () {
return this.workspace.breadcrumbs.schema; return this.workspace.breadcrumbs.schema;
},
customizations () {
return this.workspace.customizations;
} }
}, },
mounted () { mounted () {
if (this.customizations.languages)
this.localFunction.language = this.customizations.languages[0];
if (this.customizations.procedureSql)
this.localFunction.sql = this.customizations.procedureSql;
setTimeout(() => { setTimeout(() => {
this.$refs.firstInput.focus(); this.$refs.firstInput.focus();
}, 20); }, 20);

View File

@@ -25,7 +25,19 @@
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.languages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="localRoutine.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.definer') }} {{ $t('word.definer') }}
</label> </label>
@@ -53,7 +65,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.comment" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.comment') }} {{ $t('word.comment') }}
</label> </label>
@@ -76,7 +88,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.comment" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('message.dataAccess') }} {{ $t('message.dataAccess') }}
</label> </label>
@@ -89,7 +101,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.procedureDeterministic" class="form-group">
<div class="col-4" /> <div class="col-4" />
<div class="column"> <div class="column">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
@@ -117,10 +129,11 @@ export default {
return { return {
localRoutine: { localRoutine: {
definer: '', definer: '',
sql: 'BEGIN\r\n\r\nEND', sql: '',
parameters: [], parameters: [],
name: '', name: '',
comment: '', comment: '',
language: null,
security: 'DEFINER', security: 'DEFINER',
deterministic: false, deterministic: false,
dataAccess: 'CONTAINS SQL' dataAccess: 'CONTAINS SQL'
@@ -131,9 +144,17 @@ export default {
computed: { computed: {
schema () { schema () {
return this.workspace.breadcrumbs.schema; return this.workspace.breadcrumbs.schema;
},
customizations () {
return this.workspace.customizations;
} }
}, },
mounted () { mounted () {
if (this.customizations.languages)
this.localRoutine.language = this.customizations.languages[0];
if (this.customizations.procedureSql)
this.localRoutine.sql = this.customizations.procedureSql;
setTimeout(() => { setTimeout(() => {
this.$refs.firstInput.focus(); this.$refs.firstInput.focus();
}, 20); }, 20);

View File

@@ -5,7 +5,7 @@
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-database-plus mr-1" /> {{ $t('message.createNewDatabase') }} <i class="mdi mdi-24px mdi-database-plus mr-1" /> {{ $t('message.createNewSchema') }}
</div> </div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -24,11 +24,11 @@
class="form-input" class="form-input"
type="text" type="text"
required required
:placeholder="$t('message.databaseName')" :placeholder="$t('message.schemaName')"
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.collations" class="form-group">
<div class="col-3"> <div class="col-3">
<label class="form-label">{{ $t('word.collation') }}</label> <label class="form-label">{{ $t('word.collation') }}</label>
</div> </div>
@@ -49,7 +49,11 @@
</div> </div>
</div> </div>
<div class="modal-footer text-light"> <div class="modal-footer text-light">
<button class="btn btn-primary mr-2" @click.stop="createDatabase"> <button
class="btn btn-primary mr-2"
:class="{'loading': isLoading}"
@click.stop="createSchema"
>
{{ $t('word.add') }} {{ $t('word.add') }}
</button> </button>
<button class="btn btn-link" @click.stop="closeModal"> <button class="btn btn-link" @click.stop="closeModal">
@@ -62,12 +66,13 @@
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import Database from '@/ipc-api/Database'; import Schema from '@/ipc-api/Schema';
export default { export default {
name: 'ModalNewDatabase', name: 'ModalNewSchema',
data () { data () {
return { return {
isLoading: false,
database: { database: {
name: '', name: '',
collation: '' collation: ''
@@ -83,8 +88,11 @@ export default {
collations () { collations () {
return this.getWorkspace(this.selectedWorkspace).collations; return this.getWorkspace(this.selectedWorkspace).collations;
}, },
customizations () {
return this.getWorkspace(this.selectedWorkspace).customizations;
},
defaultCollation () { defaultCollation () {
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || ''; return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server') ? this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value : '';
} }
}, },
created () { created () {
@@ -101,9 +109,10 @@ export default {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification' addNotification: 'notifications/addNotification'
}), }),
async createDatabase () { async createSchema () {
this.isLoading = true;
try { try {
const { status, response } = await Database.createDatabase({ const { status, response } = await Schema.createSchema({
uid: this.selectedWorkspace, uid: this.selectedWorkspace,
...this.database ...this.database
}); });
@@ -118,6 +127,7 @@ export default {
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); this.addNotification({ status: 'error', message: err.stack });
} }
this.isLoading = false;
}, },
closeModal () { closeModal () {
this.$emit('close'); this.$emit('close');

View File

@@ -25,7 +25,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="workspace.customizations.comment" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.comment') }} {{ $t('word.comment') }}
</label> </label>
@@ -37,7 +37,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="workspace.customizations.collations" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.collation') }} {{ $t('word.collation') }}
</label> </label>
@@ -53,7 +53,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="workspace.customizations.engines" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.engine') }} {{ $t('word.engine') }}
</label> </label>
@@ -103,10 +103,14 @@ export default {
getDatabaseVariable: 'workspaces/getDatabaseVariable' getDatabaseVariable: 'workspaces/getDatabaseVariable'
}), }),
defaultCollation () { defaultCollation () {
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || ''; if (this.workspace.customizations.collations)
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
return '';
}, },
defaultEngine () { defaultEngine () {
return this.workspace.engines.find(engine => engine.isDefault).name; if (this.workspace.customizations.engines)
return this.workspace.engines.find(engine => engine.isDefault).name;
return '';
} }
}, },
mounted () { mounted () {

View File

@@ -69,7 +69,7 @@
:disabled="fieldsToExclude.includes(field.name)" :disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1" :tabindex="key+1"
> >
<span class="input-group-addon" :class="`type-${field.type.toLowerCase()}`"> <span class="input-group-addon" :class="typeCLass(field.type)">
{{ field.type }} {{ fieldLength(field) | wrapNumber }} {{ field.type }} {{ fieldLength(field) | wrapNumber }}
</span> </span>
<label class="form-checkbox ml-3" :title="$t('word.insert')"> <label class="form-checkbox ml-3" :title="$t('word.insert')">
@@ -118,7 +118,7 @@
<script> <script>
import moment from 'moment'; import moment from 'moment';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { mask } from 'vue-the-mask'; import { VueMaskDirective } from 'v-mask';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import ForeignKeySelect from '@/components/ForeignKeySelect'; import ForeignKeySelect from '@/components/ForeignKeySelect';
@@ -129,7 +129,7 @@ export default {
ForeignKeySelect ForeignKeySelect
}, },
directives: { directives: {
mask mask: VueMaskDirective
}, },
filters: { filters: {
wrapNumber (num) { wrapNumber (num) {
@@ -222,6 +222,11 @@ export default {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification' addNotification: 'notifications/addNotification'
}), }),
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
async insertRows () { async insertRows () {
this.isInserting = true; this.isInserting = true;
const rowToInsert = this.localRow; const rowToInsert = this.localRow;

View File

@@ -25,7 +25,7 @@
</div> </div>
</div> </div>
<div class="column col-6"> <div class="column col-6">
<div class="form-group"> <div v-if="workspace.customizations.definer" class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label> <label class="form-label">{{ $t('word.definer') }}</label>
<select v-model="localView.definer" class="form-select"> <select v-model="localView.definer" class="form-select">
<option value=""> <option value="">
@@ -44,7 +44,7 @@
</div> </div>
<div class="columns"> <div class="columns">
<div class="column col-4"> <div class="column col-4">
<div class="form-group"> <div v-if="workspace.customizations.viewSqlSecurity" class="form-group">
<label class="form-label">{{ $t('message.sqlSecurity') }}</label> <label class="form-label">{{ $t('message.sqlSecurity') }}</label>
<label class="form-radio"> <label class="form-radio">
<input <input
@@ -67,7 +67,7 @@
</div> </div>
</div> </div>
<div class="column col-4"> <div class="column col-4">
<div class="form-group"> <div v-if="workspace.customizations.viewAlgorithm" class="form-group">
<label class="form-label">{{ $t('word.algorithm') }}</label> <label class="form-label">{{ $t('word.algorithm') }}</label>
<label class="form-radio"> <label class="form-radio">
<input <input
@@ -99,7 +99,7 @@
</div> </div>
</div> </div>
<div class="column col-4"> <div class="column col-4">
<div class="form-group"> <div v-if="workspace.customizations.viewUpdateOption" class="form-group">
<label class="form-label">{{ $t('message.updateOption') }}</label> <label class="form-label">{{ $t('message.updateOption') }}</label>
<label class="form-radio"> <label class="form-radio">
<input <input

View File

@@ -0,0 +1,310 @@
<template>
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0 pb-4">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-memory mr-1" /> {{ $t('message.processesList') }}: {{ connectionName }}
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="processes-toolbar py-2 px-4">
<div class="dropdown">
<div class="btn-group">
<button
class="btn btn-dark btn-sm mr-0 pr-1 d-flex"
:class="{'loading':isQuering}"
title="F5"
@click="getProcessesList"
>
<span>{{ $t('word.refresh') }}</span>
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh ml-1" />
<i v-else class="mdi mdi-24px mdi-history mdi-flip-h ml-1" />
</button>
<div class="btn btn-dark btn-sm dropdown-toggle pl-0 pr-0" tabindex="0">
<i class="mdi mdi-24px mdi-menu-down" />
</div>
<div class="menu px-3">
<span>{{ $t('word.autoRefresh') }}: <b>{{ +autorefreshTimer ? `${autorefreshTimer}s` : 'OFF' }}</b></span>
<input
v-model="autorefreshTimer"
class="slider no-border"
type="range"
min="0"
max="15"
step="0.5"
@change="setRefreshInterval"
>
</div>
</div>
</div>
<div class="workspace-query-info">
<div v-if="sortedResults.length">
{{ $t('word.processes') }}: <b>{{ sortedResults.length.toLocaleString() }}</b>
</div>
</div>
</div>
<div class="modal-body py-0 workspace-query-results">
<div
ref="tableWrapper"
class="vscroll"
:style="{'height': resultsSize+'px'}"
>
<div ref="table" class="table table-hover">
<div class="thead">
<div class="tr">
<div
v-for="(field, index) in fields"
:key="index"
class="th c-hand"
>
<div ref="columnResize" class="column-resizable">
<div class="table-column-title" @click="sort(field)">
<span>{{ field.toUpperCase() }}</span>
<i
v-if="currentSort === field"
class="mdi sort-icon"
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'"
/>
</div>
</div>
</div>
</div>
</div>
<BaseVirtualScroll
ref="resultTable"
:items="sortedResults"
:item-height="22"
class="tbody"
:visible-height="resultsSize"
:scroll-element="scrollElement"
>
<template slot-scope="{ items }">
<ProcessesListRow
v-for="row in items"
:key="row._id"
class="process-row"
:row="row"
@contextmenu="contextMenu"
@stop-refresh="stopRefresh"
/>
</template>
</BaseVirtualScroll>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import Schema from '@/ipc-api/Schema';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import ProcessesListRow from '@/components/ProcessesListRow';
export default {
name: 'ModalProcessesList',
components: {
BaseVirtualScroll,
ProcessesListRow
},
props: {
connection: Object
},
data () {
return {
resultsSize: 1000,
isQuering: false,
autorefreshTimer: 0,
refreshInterval: null,
results: [],
fields: [],
currentSort: '',
currentSortDir: 'asc',
scrollElement: null
};
},
computed: {
...mapGetters({
getConnectionName: 'connections/getConnectionName'
}),
connectionName () {
return this.getConnectionName(this.connection.uid);
},
sortedResults () {
if (this.currentSort) {
return [...this.results].sort((a, b) => {
let modifier = 1;
const valA = typeof a[this.currentSort] === 'string' ? a[this.currentSort].toLowerCase() : a[this.currentSort];
const valB = typeof b[this.currentSort] === 'string' ? b[this.currentSort].toLowerCase() : b[this.currentSort];
if (this.currentSortDir === 'desc') modifier = -1;
if (valA < valB) return -1 * modifier;
if (valA > valB) return 1 * modifier;
return 0;
});
}
else
return this.results;
}
},
created () {
window.addEventListener('keydown', this.onKey, { capture: true });
},
updated () {
if (this.$refs.table)
this.refreshScroller();
if (this.$refs.tableWrapper)
this.scrollElement = this.$refs.tableWrapper;
},
mounted () {
this.getProcessesList();
window.addEventListener('resize', this.resizeResults);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey, { capture: true });
window.removeEventListener('resize', this.resizeResults);
clearInterval(this.refreshInterval);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
async getProcessesList () {
this.isQuering = true;
// if table changes clear cached values
if (this.lastTable !== this.table)
this.results = [];
try { // Table data
const { status, response } = await Schema.getProcesses(this.connection.uid);
if (status === 'success') {
this.results = response;
this.fields = response.length ? Object.keys(response[0]) : [];
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false;
},
setRefreshInterval () {
this.clearRefresh();
if (+this.autorefreshTimer) {
this.refreshInterval = setInterval(() => {
if (!this.isQuering)
this.getProcessesList();
}, this.autorefreshTimer * 1000);
}
},
clearRefresh () {
if (this.refreshInterval)
clearInterval(this.refreshInterval);
},
resizeResults () {
if (this.$refs.resultTable) {
const el = this.$refs.tableWrapper.parentElement;
if (el) {
const size = el.offsetHeight;
this.resultsSize = size;
}
this.$refs.resultTable.updateWindow();
}
},
refreshScroller () {
this.resizeResults();
},
sort (field) {
if (field === this.currentSort) {
if (this.currentSortDir === 'asc')
this.currentSortDir = 'desc';
else
this.resetSort();
}
else {
this.currentSortDir = 'asc';
this.currentSort = field;
}
},
resetSort () {
this.currentSort = '';
this.currentSortDir = 'asc';
},
stopRefresh () {
this.autorefreshTimer = 0;
this.clearRefresh();
},
contextMenu () {},
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
if (e.key === 'F5')
this.getProcessesList();
}
}
};
</script>
<style lang="scss" scoped>
.vscroll {
height: 1000px;
overflow: auto;
overflow-anchor: none;
}
.column-resizable {
&:hover,
&:active {
resize: horizontal;
overflow: hidden;
}
}
.table-column-title {
display: flex;
align-items: center;
}
.sort-icon {
font-size: 0.7rem;
line-height: 1;
margin-left: 0.2rem;
}
.result-tabs {
background: transparent !important;
margin: 0;
}
.modal {
align-items: flex-start;
.modal-container {
max-width: 75vw;
margin-top: 10vh;
.modal-body {
height: 80vh;
}
}
}
.processes-toolbar {
display: flex;
justify-content: space-between;
}
</style>

View File

@@ -30,12 +30,20 @@
<a class="c-hand">{{ $t('word.themes') }}</a> <a class="c-hand">{{ $t('word.themes') }}</a>
</li> </li>
<li <li
v-if="updateStatus !== 'disabled'"
class="tab-item" class="tab-item"
:class="{'active': selectedTab === 'update'}" :class="{'active': selectedTab === 'update'}"
@click="selectTab('update')" @click="selectTab('update')"
> >
<a class="c-hand" :class="{'badge badge-update': hasUpdates}">{{ $t('word.update') }}</a> <a class="c-hand" :class="{'badge badge-update': hasUpdates}">{{ $t('word.update') }}</a>
</li> </li>
<li
class="tab-item"
:class="{'active': selectedTab === 'changelog'}"
@click="selectTab('changelog')"
>
<a class="c-hand">{{ $t('word.changelog') }}</a>
</li>
<li <li
class="tab-item" class="tab-item"
:class="{'active': selectedTab === 'about'}" :class="{'active': selectedTab === 'about'}"
@@ -45,7 +53,7 @@
</li> </li>
</ul> </ul>
</div> </div>
<div v-if="selectedTab === 'general'" class="panel-body py-4"> <div v-show="selectedTab === 'general'" class="panel-body py-4">
<div class="container"> <div class="container">
<form class="form-horizontal columns"> <form class="form-horizontal columns">
<div class="column col-12 h6 text-uppercase mb-1"> <div class="column col-12 h6 text-uppercase mb-1">
@@ -133,26 +141,35 @@
</div> </div>
</div> </div>
<div v-if="selectedTab === 'themes'" class="panel-body py-4"> <div v-show="selectedTab === 'themes'" class="panel-body py-4">
<div class="container"> <div class="container">
<div class="columns"> <div class="columns">
<div class="column col-12 h6 text-uppercase mb-2"> <div class="column col-12 h6 text-uppercase mb-2">
{{ $t('message.applicationTheme') }} {{ $t('message.applicationTheme') }}
</div> </div>
<div class="column col-6 c-hand theme-block" :class="{'selected': applicationTheme === 'dark'}"> <div
class="column col-6 c-hand theme-block"
:class="{'selected': applicationTheme === 'dark'}"
@click="changeApplicationTheme('dark')"
>
<img :src="require('@/images/dark.png').default" class="img-responsive img-fit-cover s-rounded"> <img :src="require('@/images/dark.png').default" class="img-responsive img-fit-cover s-rounded">
<div class="theme-name"> <div class="theme-name text-light">
<i class="mdi mdi-moon-waning-crescent mdi-48px" /> <i class="mdi mdi-moon-waning-crescent mdi-48px" />
<div class="h6 mt-4"> <div class="h6 mt-4">
{{ $t('word.dark') }} {{ $t('word.dark') }}
</div> </div>
</div> </div>
</div> </div>
<div class="column col-6 theme-block disabled" :class="{'selected': applicationTheme === 'light'}"> <div
<div class="theme-name"> class="column col-6 c-hand theme-block"
:class="{'selected': applicationTheme === 'light'}"
@click="changeApplicationTheme('light')"
>
<img :src="require('@/images/light.png').default" class="img-responsive img-fit-cover s-rounded">
<div class="theme-name text-dark">
<i class="mdi mdi-white-balance-sunny mdi-48px" /> <i class="mdi mdi-white-balance-sunny mdi-48px" />
<div class="h6 mt-4"> <div class="h6 mt-4">
{{ $t('word.light') }} (Coming) {{ $t('word.light') }}
</div> </div>
</div> </div>
</div> </div>
@@ -185,8 +202,9 @@
</select> </select>
</div> </div>
<div class="column col-12"> <div class="column col-12">
<QueryEditor <BaseTextEditor
:value="exampleQuery" :value="exampleQuery"
mode="sql"
:workspace="workspace" :workspace="workspace"
:read-only="true" :read-only="true"
:height="270" :height="270"
@@ -196,17 +214,20 @@
</div> </div>
</div> </div>
<div v-if="selectedTab === 'update'" class="panel-body py-4"> <div v-show="selectedTab === 'update'" class="panel-body py-4">
<ModalSettingsUpdate /> <ModalSettingsUpdate />
</div> </div>
<div v-show="selectedTab === 'changelog'" class="panel-body py-4">
<ModalSettingsChangelog />
</div>
<div v-if="selectedTab === 'about'" class="panel-body py-4"> <div v-show="selectedTab === 'about'" class="panel-body py-4">
<div class="text-center"> <div class="text-center">
<img :src="require('@/images/logo.svg').default" width="128"> <img :src="require('@/images/logo.svg').default" width="128">
<h4>{{ appName }}</h4> <h4>{{ appName }}</h4>
<p> <p>
{{ $t('word.version') }} {{ appVersion }}<br> {{ $t('word.version') }} {{ appVersion }}<br>
<a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares')">GitHub</a> | <a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares/blob/master/CHANGELOG.md')">CHANGELOG</a><br> <a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares')">GitHub</a> | <a class="c-hand" @click="openOutside('https://antares-sql.app/')">Website</a><br>
<small>{{ $t('word.author') }} <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">Fabio Di Stasio</a></small><br> <small>{{ $t('word.author') }} <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">Fabio Di Stasio</a></small><br>
<small>{{ $t('message.madeWithJS') }}</small> <small>{{ $t('message.madeWithJS') }}</small>
</p> </p>
@@ -222,14 +243,16 @@
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import localesNames from '@/i18n/supported-locales'; import localesNames from '@/i18n/supported-locales';
import ModalSettingsUpdate from '@/components/ModalSettingsUpdate'; import ModalSettingsUpdate from '@/components/ModalSettingsUpdate';
import QueryEditor from '@/components/QueryEditor'; import ModalSettingsChangelog from '@/components/ModalSettingsChangelog';
import BaseTextEditor from '@/components/BaseTextEditor';
const { shell } = require('electron'); const { shell } = require('electron');
export default { export default {
name: 'ModalSettings', name: 'ModalSettings',
components: { components: {
ModalSettingsUpdate, ModalSettingsUpdate,
QueryEditor ModalSettingsChangelog,
BaseTextEditor
}, },
data () { data () {
return { return {
@@ -350,6 +373,7 @@ ORDER BY
changeLocale: 'settings/changeLocale', changeLocale: 'settings/changeLocale',
changeAutoComplete: 'settings/changeAutoComplete', changeAutoComplete: 'settings/changeAutoComplete',
changeLineWrap: 'settings/changeLineWrap', changeLineWrap: 'settings/changeLineWrap',
changeApplicationTheme: 'settings/changeApplicationTheme',
changeEditorTheme: 'settings/changeEditorTheme', changeEditorTheme: 'settings/changeEditorTheme',
updateNotificationsTimeout: 'settings/updateNotificationsTimeout' updateNotificationsTimeout: 'settings/updateNotificationsTimeout'
}), }),
@@ -382,54 +406,59 @@ ORDER BY
<style lang="scss"> <style lang="scss">
#settings { #settings {
.modal-body { .modal-container {
overflow: hidden; position: absolute;
top: 17.5vh;
.panel-body { .modal-body {
height: calc(70vh - 70px); overflow: hidden;
overflow: auto;
.theme-block { .panel-body {
position: relative; min-height: calc(25vh - 70px);
text-align: center; max-height: 65vh;
overflow: auto;
&.selected { .theme-block {
img { position: relative;
box-shadow: 0 0 0 3px $primary-color; text-align: center;
&.selected {
img {
box-shadow: 0 0 0 3px $primary-color;
}
}
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
.theme-name {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
top: 0;
height: 100%;
width: 100%;
} }
} }
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
.theme-name {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
top: 0;
height: 100%;
width: 100%;
text-shadow: 0 0 8px #000;
}
} }
}
.badge::after { .badge::after {
background: #32b643; background: #32b643;
} }
.badge-update::after { .badge-update::after {
bottom: initial; bottom: initial;
background: $primary-color; background: $primary-color;
} }
.form-label { .form-label {
display: flex; display: flex;
align-items: center; align-items: center;
}
} }
} }
} }

View File

@@ -0,0 +1,82 @@
<template>
<div class="p-relative">
<BaseLoader v-if="isLoading" />
<div
id="changelog"
class="container"
v-html="changelog"
/>
<div v-if="isError" class="empty">
<div class="empty-icon">
<i class="mdi mdi-48px mdi-alert-outline" />
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import marked from 'marked';
import BaseLoader from '@/components/BaseLoader';
export default {
name: 'ModalSettingsChangelog',
components: {
BaseLoader
},
data () {
return {
changelog: '',
isLoading: true,
error: '',
isError: false
};
},
computed: {
...mapGetters({ appVersion: 'application/appVersion' })
},
created () {
this.getChangelog();
},
methods: {
async getChangelog () {
try {
const apiRes = await fetch(`https://api.github.com/repos/Fabio286/antares/releases/tags/v${this.appVersion}`, {
method: 'GET'
});
const { body } = await apiRes.json();
const markdown = body.substr(0, body.indexOf('### Download'));
const renderer = {
link (href, title, text) {
return text;
},
listitem (text) {
return `<li>${text.replace(/ *\([^)]*\) */g, '')}</li>`;
}
};
marked.use({ renderer });
this.changelog = marked(markdown);
}
catch (err) {
this.error = err.message;
this.isError = true;
}
this.isLoading = false;
}
}
};
</script>
<style lang="scss">
#changelog {
h3 {
font-size: 1rem;
}
li {
margin-top: 0;
}
}
</style>

View File

@@ -36,11 +36,17 @@
{{ $t('message.restartToInstall') }} {{ $t('message.restartToInstall') }}
</button> </button>
</div> </div>
<div class="form-group mt-4">
<label class="form-switch d-inline-block disabled" @click.prevent="toggleAllowPrerelease">
<input type="checkbox" :checked="allowPrerelease">
<i class="form-icon" /> {{ $t('message.includeBetaUpdates') }}
</label>
</div>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
export default { export default {
@@ -48,7 +54,8 @@ export default {
computed: { computed: {
...mapGetters({ ...mapGetters({
updateStatus: 'application/getUpdateStatus', updateStatus: 'application/getUpdateStatus',
downloadPercentage: 'application/getDownloadProgress' downloadPercentage: 'application/getDownloadProgress',
allowPrerelease: 'settings/getAllowPrerelease'
}), }),
updateMessage () { updateMessage () {
switch (this.updateStatus) { switch (this.updateStatus) {
@@ -70,18 +77,18 @@ export default {
} }
}, },
methods: { methods: {
...mapActions({
changeAllowPrerelease: 'settings/changeAllowPrerelease'
}),
checkForUpdates () { checkForUpdates () {
ipcRenderer.send('check-for-updates'); ipcRenderer.send('check-for-updates');
}, },
restartToUpdate () { restartToUpdate () {
ipcRenderer.send('restart-to-update'); ipcRenderer.send('restart-to-update');
},
toggleAllowPrerelease () {
this.changeAllowPrerelease(!this.allowPrerelease);
} }
} }
}; };
</script> </script>
<style lang="scss">
.empty {
color: $body-font-color;
}
</style>

View File

@@ -0,0 +1,155 @@
<template>
<div class="tr" @click="selectRow($event, row._id)">
<div
v-for="(col, cKey) in row"
v-show="cKey !== '_id'"
:key="cKey"
class="td p-0"
tabindex="0"
@contextmenu.prevent="openContext($event, { id: row._id, field: cKey })"
>
<template v-if="cKey !== '_id'">
<span
v-if="!isInlineEditor[cKey]"
class="cell-content px-2"
:class="`${isNull(col)} type-${typeof col === 'number' ? 'int' : 'varchar'}`"
@dblclick="dblClick(cKey)"
>{{ col | cutText }}</span>
</template>
</div>
<ConfirmModal
v-if="isInfoModal"
:confirm-text="$t('word.update')"
:cancel-text="$t('word.close')"
size="medium"
:hide-footer="true"
@hide="hideInfoModal"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-information-outline mr-1" /> {{ $t('message.processInfo') }}
</div>
</template>
<div :slot="'body'">
<div>
<div>
<TextEditor
:value="row.info || ''"
editor-class="textarea-editor"
:mode="editorMode"
:read-only="true"
/>
</div>
</div>
</div>
</ConfirmModal>
</div>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
export default {
name: 'ProcessesListRow',
components: {
ConfirmModal,
TextEditor
},
filters: {
cutText (val) {
if (typeof val !== 'string') return val;
return val.length > 250 ? `${val.substring(0, 250)}[...]` : val;
}
},
props: {
row: Object
},
data () {
return {
isInlineEditor: {},
isInfoModal: false,
editorMode: 'sql'
};
},
computed: {},
watch: {
fields () {
Object.keys(this.fields).forEach(field => {
this.isInlineEditor[field.name] = false;
});
}
},
methods: {
isNull (value) {
return value === null ? ' is-null' : '';
},
selectRow (event, row) {
this.$emit('select-row', event, row);
},
openContext (event, payload) {
if (this.isEditable) {
payload.field = this.fields[payload.field].name;// Ensures field name only
this.$emit('contextmenu', event, payload);
}
},
hideInfoModal () {
this.isInfoModal = false;
},
dblClick (col) {
if (col !== 'info') return;
this.$emit('stop-refresh');
this.isInfoModal = true;
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape') {
this.isInlineEditor[this.editingField] = false;
this.editingField = null;
window.removeEventListener('keydown', this.onKey);
}
}
}
};
</script>
<style lang="scss">
.editable-field {
margin: 0;
border: none;
line-height: 1;
width: 100%;
position: absolute;
left: 0;
right: 0;
}
.cell-content {
display: block;
min-height: 0.8rem;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.textarea-editor {
height: 50vh !important;
}
.editor-field-info {
margin-top: 0.4rem;
display: flex;
justify-content: space-between;
white-space: normal;
}
.editor-buttons {
display: flex;
justify-content: space-evenly;
.btn {
display: flex;
align-items: center;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="editor-wrapper"> <div class="editor-wrapper">
<div <div
ref="editor" :id="`editor-${id}`"
class="editor" class="editor"
:style="{height: `${height}px`}" :style="{height: `${height}px`}"
/> />
@@ -12,7 +12,7 @@
import * as ace from 'ace-builds'; import * as ace from 'ace-builds';
import 'ace-builds/webpack-resolver'; import 'ace-builds/webpack-resolver';
import '../libs/ext-language_tools'; import '../libs/ext-language_tools';
import { mapGetters } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
export default { export default {
@@ -20,6 +20,7 @@ export default {
props: { props: {
value: String, value: String,
workspace: Object, workspace: Object,
isSelected: Boolean,
schema: { type: String, default: '' }, schema: { type: String, default: '' },
autoFocus: { type: Boolean, default: false }, autoFocus: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false }, readOnly: { type: Boolean, default: false },
@@ -29,14 +30,17 @@ export default {
return { return {
editor: null, editor: null,
fields: [], fields: [],
baseCompleter: [] customCompleter: [],
id: null,
lastSchema: null
}; };
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
editorTheme: 'settings/getEditorTheme', editorTheme: 'settings/getEditorTheme',
autoComplete: 'settings/getAutoComplete', autoComplete: 'settings/getAutoComplete',
lineWrap: 'settings/getLineWrap' lineWrap: 'settings/getLineWrap',
baseCompleter: 'application/getBaseCompleter'
}), }),
tables () { tables () {
return this.workspace return this.workspace
@@ -122,9 +126,13 @@ export default {
return 'sql'; return 'sql';
} }
}, },
cursorPosition () {
return this.editor.session.doc.positionToIndex(this.editor.getCursorPosition());
},
lastWord () { lastWord () {
const words = this.value.split(' '); const charsBefore = this.value.slice(0, this.cursorPosition);
return words[words.length - 1]; const words = charsBefore.replaceAll('\n', ' ').split(' ').filter(Boolean);
return words.pop();
}, },
isLastWordATable () { isLastWordATable () {
return /\w+\.\w*/gm.test(this.lastWord); return /\w+\.\w*/gm.test(this.lastWord);
@@ -163,10 +171,24 @@ export default {
wrap: this.lineWrap wrap: this.lineWrap
}); });
} }
},
isSelected () {
if (this.isSelected)
this.lastSchema = this.schema;
},
lastSchema () {
if (this.editor) {
this.editor.completers = this.baseCompleter.map(el => Object.assign({}, el));
this.setCustomCompleter();
}
} }
}, },
created () {
this.id = this._uid;
this.lastSchema = this.schema;
},
mounted () { mounted () {
this.editor = ace.edit(this.$refs.editor, { this.editor = ace.edit(`editor-${this.id}`, {
mode: `ace/mode/${this.mode}`, mode: `ace/mode/${this.mode}`,
theme: `ace/theme/${this.editorTheme}`, theme: `ace/theme/${this.editorTheme}`,
value: this.value, value: this.value,
@@ -182,32 +204,16 @@ export default {
enableLiveAutocompletion: this.autoComplete enableLiveAutocompletion: this.autoComplete
}); });
this.editor.completers.push({ if (!this.baseCompleter.length)
getCompletions: (editor, session, pos, prefix, callback) => { this.setBaseCompleters(this.editor.completers.map(el => Object.assign({}, el)));
const completions = [];
[
...this.tables,
...this.triggers,
...this.procedures,
...this.functions,
...this.schedulers
].forEach(el => {
completions.push({
value: el.name,
meta: el.type
});
});
callback(null, completions);
}
});
this.baseCompleter = this.editor.completers; this.setCustomCompleter();
this.editor.commands.on('afterExec', e => { this.editor.commands.on('afterExec', e => {
if (['insertstring', 'backspace', 'del'].includes(e.command.name)) { if (['insertstring', 'backspace', 'del'].includes(e.command.name)) {
if (this.isLastWordATable || e.args === '.') { if (this.isLastWordATable || e.args === '.') {
if (e.args !== ' ') { if (e.args !== ' ') {
const table = this.tables.find(t => t.name === this.lastWord.split('.').pop()); const table = this.tables.find(t => t.name === this.lastWord.split('.').pop().trim());
if (table) { if (table) {
const params = { const params = {
@@ -224,13 +230,13 @@ export default {
}).catch(console.log); }).catch(console.log);
} }
else else
this.editor.completers = this.baseCompleter; this.editor.completers = this.customCompleter;
} }
else else
this.editor.completers = this.baseCompleter; this.editor.completers = this.customCompleter;
} }
else else
this.editor.completers = this.baseCompleter; this.editor.completers = this.customCompleter;
} }
}); });
@@ -239,6 +245,22 @@ export default {
this.$emit('update:value', content); this.$emit('update:value', content);
}); });
this.editor.on('guttermousedown', e => {
const target = e.domEvent.target;
if (target.className.indexOf('ace_gutter-cell') === -1)
return;
if (e.clientX > 25 + target.getBoundingClientRect().left)
return;
const row = e.getDocumentPosition().row;
const breakpoints = e.editor.session.getBreakpoints(row, 0);
if (typeof breakpoints[row] === typeof undefined)
e.editor.session.setBreakpoint(row);
else
e.editor.session.clearBreakpoint(row);
e.stop();
});
if (this.autoFocus) { if (this.autoFocus) {
setTimeout(() => { setTimeout(() => {
this.editor.focus(); this.editor.focus();
@@ -249,6 +271,33 @@ export default {
setTimeout(() => { setTimeout(() => {
this.editor.resize(); this.editor.resize();
}, 20); }, 20);
},
methods: {
...mapActions({
setBaseCompleters: 'application/setBaseCompleter'
}),
setCustomCompleter () {
this.editor.completers.push({
getCompletions: (editor, session, pos, prefix, callback) => {
const completions = [];
[
...this.tables,
...this.triggers,
...this.procedures,
...this.functions,
...this.schedulers
].forEach(el => {
completions.push({
value: el.name,
meta: el.type
});
});
callback(null, completions);
}
});
this.customCompleter = this.editor.completers;
}
} }
}; };
</script> </script>
@@ -267,16 +316,20 @@ export default {
width: 17px; width: 17px;
} }
.ace_dark.ace_editor.ace_autocomplete .ace_marker-layer .ace_active-line { .ace_gutter-cell.ace_breakpoint {
background-color: #c9561a99; &::before {
} content: '\F0403';
position: absolute;
.ace_dark.ace_editor.ace_autocomplete .ace_marker-layer .ace_line-hover { left: 3px;
background-color: #c9571a33; top: 2px;
border: none; color: $primary-color;
} display: inline-block;
font: normal normal normal 24px/1 "Material Design Icons", sans-serif;
.ace_dark.ace_editor.ace_autocomplete .ace_completion-highlight { font-size: inherit;
color: #e0d00c; text-rendering: auto;
line-height: inherit;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
} }
</style> </style>

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://github.com/sponsors/Fabio286')"> <li class="footer-element footer-link" @click="openOutside('https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet')">
<i class="mdi mdi-18px mdi-coffee mr-1" /> <i class="mdi mdi-18px mdi-tree mr-1" />
<small>{{ $t('word.donate') }}</small> <small>{{ $t('message.plantATree') }}</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" />
@@ -64,13 +64,11 @@ export default {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background: $primary-color;
padding: 0 0.2rem; padding: 0 0.2rem;
position: fixed; position: fixed;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
box-shadow: 0 0 1px 0 #000;
.footer-elements { .footer-elements {
list-style: none; list-style: none;
@@ -88,10 +86,6 @@ export default {
&.footer-link { &.footer-link {
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;
&:hover {
background: rgba($color: #fff, $alpha: 0.1);
}
} }
} }
} }

View File

@@ -0,0 +1,75 @@
<template>
<ConfirmModal
:confirm-text="$t('word.update')"
:cancel-text="$t('word.close')"
size="large"
:hide-footer="true"
@hide="hideScratchpad"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-notebook-edit-outline mr-1" /> {{ $t('word.scratchpad') }}
</div>
</template>
<div :slot="'body'">
<div>
<div>
<TextEditor
:value.sync="localNotes"
editor-class="textarea-editor"
mode="markdown"
:auto-focus="true"
:show-line-numbers="false"
/>
</div>
<small class="text-gray">{{ $t('message.markdownSupported') }}</small>
</div>
</div>
</ConfirmModal>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
export default {
name: 'TheScratchpad',
components: {
ConfirmModal,
TextEditor
},
data () {
return {
localNotes: '',
debounceTimeout: null
};
},
computed: {
...mapGetters({
notes: 'scratchpad/getNotes'
})
},
watch: {
localNotes () {
clearTimeout(this.debounceTimeout);
this.debounceTimeout = setTimeout(() => {
this.changeNotes(this.localNotes);
}, 200);
}
},
created () {
this.localNotes = this.notes;
},
methods: {
...mapActions({
hideScratchpad: 'application/hideScratchpad',
changeNotes: 'scratchpad/changeNotes'
}),
hideModal () {
this.$emit('hide');
}
}
};
</script>

View File

@@ -19,7 +19,7 @@
@contextmenu.prevent="contextMenu($event, connection)" @contextmenu.prevent="contextMenu($event, connection)"
@mouseover.self="tooltipPosition" @mouseover.self="tooltipPosition"
> >
<i class="settingbar-element-icon dbi" :class="`dbi-${connection.client} ${connected.includes(connection.uid) ? 'badge' : ''}`" /> <i class="settingbar-element-icon dbi" :class="`dbi-${connection.client} ${getStatusBadge(connection.uid)}`" />
<span class="ex-tooltip-content">{{ getConnectionName(connection.uid) }}</span> <span class="ex-tooltip-content">{{ getConnectionName(connection.uid) }}</span>
</li> </li>
</draggable> </draggable>
@@ -36,6 +36,10 @@
<div class="settingbar-bottom-elements"> <div class="settingbar-bottom-elements">
<ul class="settingbar-elements"> <ul class="settingbar-elements">
<li class="settingbar-element btn btn-link ex-tooltip" @click="showScratchpad">
<i class="settingbar-element-icon mdi mdi-24px mdi-notebook-edit-outline text-light" />
<span class="ex-tooltip-content">{{ $t('word.scratchpad') }}</span>
</li>
<li class="settingbar-element btn btn-link ex-tooltip" @click="showSettingModal('general')"> <li class="settingbar-element btn btn-link ex-tooltip" @click="showSettingModal('general')">
<i class="settingbar-element-icon mdi mdi-24px mdi-cog text-light" :class="{' badge badge-update': hasUpdates}" /> <i class="settingbar-element-icon mdi mdi-24px mdi-cog text-light" :class="{' badge badge-update': hasUpdates}" />
<span class="ex-tooltip-content">{{ $t('word.settings') }}</span> <span class="ex-tooltip-content">{{ $t('word.settings') }}</span>
@@ -69,7 +73,7 @@ export default {
...mapGetters({ ...mapGetters({
getConnections: 'connections/getConnections', getConnections: 'connections/getConnections',
getConnectionName: 'connections/getConnectionName', getConnectionName: 'connections/getConnectionName',
connected: 'workspaces/getConnected', getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected', selectedWorkspace: 'workspaces/getSelected',
updateStatus: 'application/getUpdateStatus' updateStatus: 'application/getUpdateStatus'
}), }),
@@ -90,6 +94,7 @@ export default {
updateConnections: 'connections/updateConnections', updateConnections: 'connections/updateConnections',
showNewConnModal: 'application/showNewConnModal', showNewConnModal: 'application/showNewConnModal',
showSettingModal: 'application/showSettingModal', showSettingModal: 'application/showSettingModal',
showScratchpad: 'application/showScratchpad',
selectWorkspace: 'workspaces/selectWorkspace' selectWorkspace: 'workspaces/selectWorkspace'
}), }),
contextMenu (event, connection) { contextMenu (event, connection) {
@@ -104,6 +109,22 @@ export default {
const el = e.target; const el = e.target;
const fromTop = window.pageYOffset + el.getBoundingClientRect().top - (el.offsetHeight / 4); const fromTop = window.pageYOffset + el.getBoundingClientRect().top - (el.offsetHeight / 4);
el.querySelector('.ex-tooltip-content').style.top = `${fromTop}px`; el.querySelector('.ex-tooltip-content').style.top = `${fromTop}px`;
},
getStatusBadge (uid) {
if (this.getWorkspace(uid)) {
const status = this.getWorkspace(uid).connection_status;
switch (status) {
case 'connected':
return 'badge badge-connected';
case 'connecting':
return 'badge badge-connecting';
case 'failed':
return 'badge badge-failed';
default:
return '';
}
}
} }
} }
}; };
@@ -117,9 +138,7 @@ export default {
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background: $bg-color-light;
padding: 0; padding: 0;
box-shadow: 0 0 1px 0 #000;
z-index: 9; z-index: 9;
.settingbar-top-elements { .settingbar-top-elements {
@@ -134,7 +153,6 @@ export default {
.settingbar-bottom-elements { .settingbar-bottom-elements {
padding-top: 0.5rem; padding-top: 0.5rem;
background: $bg-color-light;
z-index: 1; z-index: 1;
} }
@@ -162,7 +180,6 @@ export default {
} }
&.selected { &.selected {
border-left-color: $body-font-color;
opacity: 1; opacity: 1;
} }
@@ -171,12 +188,10 @@ export default {
bottom: -10px; bottom: -10px;
right: 0; right: 0;
position: absolute; position: absolute;
background: $success-color;
} }
&.badge-update::after { &.badge-update::after {
bottom: initial; bottom: initial;
background: $primary-color;
} }
} }
} }

View File

@@ -37,15 +37,16 @@
</template> </template>
<script> <script>
import { remote, ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { getCurrentWindow } from '@electron/remote';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
export default { export default {
name: 'TheTitleBar', name: 'TheTitleBar',
data () { data () {
return { return {
w: remote.getCurrentWindow(), w: getCurrentWindow(),
isMaximized: remote.getCurrentWindow().isMaximized(), isMaximized: getCurrentWindow().isMaximized(),
isDevelopment: process.env.NODE_ENV === 'development' isDevelopment: process.env.NODE_ENV === 'development'
}; };
}, },
@@ -102,12 +103,10 @@ export default {
display: flex; display: flex;
position: relative; position: relative;
justify-content: space-between; justify-content: space-between;
background: $bg-color-light;
align-items: center; align-items: center;
height: $titlebar-height; height: $titlebar-height;
-webkit-app-region: drag; -webkit-app-region: drag;
user-select: none; user-select: none;
box-shadow: 0 0 1px 0 #000;
z-index: 9999; z-index: 9999;
.titlebar-resizer { .titlebar-resizer {
@@ -134,7 +133,7 @@ export default {
.titlebar-logo { .titlebar-logo {
height: $titlebar-height; height: $titlebar-height;
padding: 0 0.4rem; padding: 0.3rem 0.4rem;
} }
.titlebar-element { .titlebar-element {
@@ -149,11 +148,6 @@ export default {
&:hover { &:hover {
opacity: 1; opacity: 1;
background: rgba($color: #fff, $alpha: 0.2);
}
&.close-button:hover {
background: red;
} }
} }
} }

View File

@@ -1,25 +1,57 @@
<template> <template>
<div v-show="isSelected" class="workspace column columns col-gapless"> <div v-show="isSelected" class="workspace column columns col-gapless">
<WorkspaceExploreBar :connection="connection" :is-selected="isSelected" /> <WorkspaceExploreBar :connection="connection" :is-selected="isSelected" />
<div v-if="workspace.connected" class="workspace-tabs column columns col-gapless"> <div v-if="workspace.connection_status === 'connected'" class="workspace-tabs column columns col-gapless">
<ul <ul
id="tabWrap" id="tabWrap"
ref="tabWrap" ref="tabWrap"
class="tab tab-block column col-12" class="tab tab-block column col-12"
> >
<li class="tab-item d-none"> <li class="tab-item dropdown tools-dropdown">
<a class="tab-link workspace-tools-link"> <a
class="tab-link workspace-tools-link dropdown-toggle"
tabindex="0"
:title="$t('word.tools')"
>
<i class="mdi mdi-24px mdi-tools" /> <i class="mdi mdi-24px mdi-tools" />
</a> </a>
<ul class="menu text-left text-uppercase">
<li v-if="workspace.customizations.processesList" class="menu-item">
<a class="c-hand p-vcentered" @click="showProcessesModal">
<i class="mdi mdi-memory mr-1 tool-icon" />
<span>{{ $t('message.processesList') }}</span>
</a>
</li>
<li
v-if="workspace.customizations.variables"
class="menu-item"
title="Coming..."
>
<a class="c-hand p-vcentered disabled">
<i class="mdi mdi-shape mr-1 tool-icon" />
<span>{{ $t('word.variables') }}</span>
</a>
</li>
<li
v-if="workspace.customizations.usersManagement"
class="menu-item"
title="Coming..."
>
<a class="c-hand p-vcentered disabled">
<i class="mdi mdi-account-group mr-1 tool-icon" />
<span>{{ $t('message.manageUsers') }}</span>
</a>
</li>
</ul>
</li> </li>
<li <li
v-if="schemaChild" v-if="schemaChild && isSettingSupported"
class="tab-item" class="tab-item"
:class="{'active': selectedTab === 'prop'}" :class="{'active': selectedTab === 'prop'}"
@click="selectTab({uid: workspace.uid, tab: 'prop'})" @click="selectTab({uid: workspace.uid, tab: 'prop'})"
> >
<a class="tab-link"> <a class="tab-link">
<i class="mdi mdi-18px mdi-tune mr-1" /> <i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" />
<span :title="schemaChild">{{ $t('word.settings').toUpperCase() }}: {{ schemaChild }}</span> <span :title="schemaChild">{{ $t('word.settings').toUpperCase() }}: {{ schemaChild }}</span>
</a> </a>
</li> </li>
@@ -109,11 +141,16 @@
<WorkspaceQueryTab <WorkspaceQueryTab
v-for="tab of queryTabs" v-for="tab of queryTabs"
:key="tab.uid" :key="tab.uid"
:tab-uid="tab.uid" :tab="tab"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
:connection="connection" :connection="connection"
/> />
</div> </div>
<ModalProcessesList
v-if="isProcessesModal"
:connection="connection"
@close="hideProcessesModal"
/>
</div> </div>
</template> </template>
@@ -129,6 +166,7 @@ import WorkspacePropsTabTrigger from '@/components/WorkspacePropsTabTrigger';
import WorkspacePropsTabRoutine from '@/components/WorkspacePropsTabRoutine'; import WorkspacePropsTabRoutine from '@/components/WorkspacePropsTabRoutine';
import WorkspacePropsTabFunction from '@/components/WorkspacePropsTabFunction'; import WorkspacePropsTabFunction from '@/components/WorkspacePropsTabFunction';
import WorkspacePropsTabScheduler from '@/components/WorkspacePropsTabScheduler'; import WorkspacePropsTabScheduler from '@/components/WorkspacePropsTabScheduler';
import ModalProcessesList from '@/components/ModalProcessesList';
export default { export default {
name: 'Workspace', name: 'Workspace',
@@ -141,14 +179,16 @@ export default {
WorkspacePropsTabTrigger, WorkspacePropsTabTrigger,
WorkspacePropsTabRoutine, WorkspacePropsTabRoutine,
WorkspacePropsTabFunction, WorkspacePropsTabFunction,
WorkspacePropsTabScheduler WorkspacePropsTabScheduler,
ModalProcessesList
}, },
props: { props: {
connection: Object connection: Object
}, },
data () { data () {
return { return {
hasWheelEvent: false hasWheelEvent: false,
isProcessesModal: false
}; };
}, },
computed: { computed: {
@@ -162,15 +202,31 @@ export default {
isSelected () { isSelected () {
return this.selectedWorkspace === this.connection.uid; return this.selectedWorkspace === this.connection.uid;
}, },
isSettingSupported () {
if (this.workspace.breadcrumbs.table && this.workspace.customizations.tableSettings) return true;
if (this.workspace.breadcrumbs.view && this.workspace.customizations.viewSettings) return true;
if (this.workspace.breadcrumbs.trigger && this.workspace.customizations.triggerSettings) return true;
if (this.workspace.breadcrumbs.procedure && this.workspace.customizations.routineSettings) return true;
if (this.workspace.breadcrumbs.function && this.workspace.customizations.functionSettings) return true;
if (this.workspace.breadcrumbs.scheduler && this.workspace.customizations.schedulerSettings) return true;
return false;
},
selectedTab () { selectedTab () {
if ( if (
this.workspace.breadcrumbs.table === null && (
this.workspace.breadcrumbs.view === null && this.workspace.breadcrumbs.table === null &&
this.workspace.breadcrumbs.trigger === null && this.workspace.breadcrumbs.view === null &&
this.workspace.breadcrumbs.procedure === null && this.workspace.breadcrumbs.trigger === null &&
this.workspace.breadcrumbs.function === null && this.workspace.breadcrumbs.procedure === null &&
this.workspace.breadcrumbs.scheduler === null && this.workspace.breadcrumbs.function === null &&
['data', 'prop'].includes(this.workspace.selected_tab) this.workspace.breadcrumbs.scheduler === null &&
['data', 'prop'].includes(this.workspace.selected_tab)
) ||
(
this.workspace.breadcrumbs.table === null &&
this.workspace.breadcrumbs.view === null &&
this.workspace.selected_tab === 'data'
)
) )
return this.queryTabs[0].uid; return this.queryTabs[0].uid;
@@ -214,7 +270,7 @@ export default {
removeTab: 'workspaces/removeTab' removeTab: 'workspaces/removeTab'
}), }),
addTab () { addTab () {
this.newTab(this.connection.uid); this.newTab({ uid: this.connection.uid });
if (!this.hasWheelEvent) { if (!this.hasWheelEvent) {
this.$refs.tabWrap.addEventListener('wheel', e => { this.$refs.tabWrap.addEventListener('wheel', e => {
@@ -227,6 +283,12 @@ export default {
closeTab (tUid) { closeTab (tUid) {
if (this.queryTabs.length === 1) return; if (this.queryTabs.length === 1) return;
this.removeTab({ uid: this.connection.uid, tab: tUid }); this.removeTab({ uid: this.connection.uid, tab: tUid });
},
showProcessesModal () {
this.isProcessesModal = true;
},
hideProcessesModal () {
this.isProcessesModal = false;
} }
} }
}; };
@@ -242,7 +304,6 @@ export default {
height: calc(100vh - #{$excluding-size}); height: calc(100vh - #{$excluding-size});
.tab-block { .tab-block {
background: $bg-color-light;
margin-top: 0; margin-top: 0;
flex-direction: row; flex-direction: row;
align-items: flex-start; align-items: flex-start;
@@ -262,7 +323,6 @@ export default {
> a { > a {
padding: 0.2rem 0.8rem; padding: 0.2rem 0.8rem;
color: $body-font-color;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -291,6 +351,42 @@ export default {
opacity: 1; opacity: 1;
} }
&.tools-dropdown {
.tab-link:focus {
opacity: 1;
outline: 0;
box-shadow: none;
}
.menu {
min-width: 100%;
.menu-item a {
border-radius: 0.1rem;
color: inherit;
display: block;
margin: 0 -0.4rem;
padding: 0.2rem 0.4rem;
text-decoration: none;
white-space: nowrap;
border: 0;
.tool-icon {
line-height: 1;
display: inline-block;
font-size: 20px;
}
}
}
z-index: 9;
position: absolute;
}
&.tools-dropdown + .tab-item {
margin-left: 56px;
}
.workspace-tools-link { .workspace-tools-link {
padding-bottom: 0; padding-bottom: 0;
padding-top: 0.3rem; padding-top: 0.3rem;
@@ -310,11 +406,9 @@ export default {
.th { .th {
position: sticky; position: sticky;
top: 0; top: 0;
background: $bg-color;
border: 1px solid; border: 1px solid;
border-left: none; border-left: none;
border-bottom-width: 2px; border-bottom-width: 2px;
border-color: $bg-color-light;
padding: 0; padding: 0;
font-weight: 700; font-weight: 700;
font-size: 0.7rem; font-size: 0.7rem;
@@ -329,7 +423,6 @@ export default {
.td { .td {
border-right: 1px solid; border-right: 1px solid;
border-bottom: 1px solid; border-bottom: 1px solid;
border-color: $bg-color-light;
padding: 0 0.4rem; padding: 0 0.4rem;
text-overflow: ellipsis; text-overflow: ellipsis;
max-width: 200px; max-width: 200px;
@@ -339,8 +432,6 @@ export default {
position: relative; position: relative;
&:focus { &:focus {
box-shadow: inset 0 0 0 1px $body-font-color;
background: rgba($color: #000, $alpha: 0.3);
outline: none; outline: none;
} }
} }

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="columns"> <div class="columns">
<div class="column col-12 empty text-light"> <div class="column col-12 empty">
<div class="empty-icon"> <div class="empty-icon">
<i class="mdi mdi-48px mdi-power-plug-off" /> <i class="mdi mdi-48px mdi-power-plug-off" />
</div> </div>

View File

@@ -8,10 +8,10 @@
> >
<div class="workspace-explorebar-header"> <div class="workspace-explorebar-header">
<span class="workspace-explorebar-title">{{ connectionName }}</span> <span class="workspace-explorebar-title">{{ connectionName }}</span>
<span v-if="workspace.connected" class="workspace-explorebar-tools"> <span v-if="workspace.connection_status === 'connected'" class="workspace-explorebar-tools">
<i <i
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.createNewDatabase')" :title="$t('message.createNewSchema')"
@click="showNewDBModal" @click="showNewDBModal"
/> />
<i <i
@@ -27,24 +27,35 @@
/> />
</span> </span>
</div> </div>
<div class="workspace-explorebar-search">
<div v-if="workspace.connection_status === 'connected'" class="has-icon-right">
<input
v-model="searchTerm"
class="form-input input-sm"
type="text"
:placeholder="$t('message.searchForElements')"
>
<i class="form-icon mdi mdi-magnify mdi-18px" />
</div>
</div>
<WorkspaceConnectPanel <WorkspaceConnectPanel
v-if="!workspace.connected" v-if="workspace.connection_status !== 'connected'"
class="workspace-explorebar-body" class="workspace-explorebar-body"
:connection="connection" :connection="connection"
/> />
<div v-else class="workspace-explorebar-body"> <div v-else class="workspace-explorebar-body">
<WorkspaceExploreBarDatabase <WorkspaceExploreBarSchema
v-for="db of workspace.structure" v-for="db of workspace.structure"
:key="db.name" :key="db.name"
:database="db" :database="db"
:connection="connection" :connection="connection"
@show-database-context="openDatabaseContext" @show-schema-context="openSchemaContext"
@show-table-context="openTableContext" @show-table-context="openTableContext"
@show-misc-context="openMiscContext" @show-misc-context="openMiscContext"
/> />
</div> </div>
</div> </div>
<ModalNewDatabase <ModalNewSchema
v-if="isNewDBModal" v-if="isNewDBModal"
@close="hideNewDBModal" @close="hideNewDBModal"
@reload="refresh" @reload="refresh"
@@ -117,7 +128,6 @@
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import _ from 'lodash';// TODO: remove
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import Views from '@/ipc-api/Views'; import Views from '@/ipc-api/Views';
@@ -127,11 +137,11 @@ import Functions from '@/ipc-api/Functions';
import Schedulers from '@/ipc-api/Schedulers'; import Schedulers from '@/ipc-api/Schedulers';
import WorkspaceConnectPanel from '@/components/WorkspaceConnectPanel'; import WorkspaceConnectPanel from '@/components/WorkspaceConnectPanel';
import WorkspaceExploreBarDatabase from '@/components/WorkspaceExploreBarDatabase'; import WorkspaceExploreBarSchema from '@/components/WorkspaceExploreBarSchema';
import DatabaseContext from '@/components/WorkspaceExploreBarDatabaseContext'; import DatabaseContext from '@/components/WorkspaceExploreBarSchemaContext';
import TableContext from '@/components/WorkspaceExploreBarTableContext'; import TableContext from '@/components/WorkspaceExploreBarTableContext';
import MiscContext from '@/components/WorkspaceExploreBarMiscContext'; import MiscContext from '@/components/WorkspaceExploreBarMiscContext';
import ModalNewDatabase from '@/components/ModalNewDatabase'; import ModalNewSchema from '@/components/ModalNewSchema';
import ModalNewTable from '@/components/ModalNewTable'; import ModalNewTable from '@/components/ModalNewTable';
import ModalNewView from '@/components/ModalNewView'; import ModalNewView from '@/components/ModalNewView';
import ModalNewTrigger from '@/components/ModalNewTrigger'; import ModalNewTrigger from '@/components/ModalNewTrigger';
@@ -143,11 +153,11 @@ export default {
name: 'WorkspaceExploreBar', name: 'WorkspaceExploreBar',
components: { components: {
WorkspaceConnectPanel, WorkspaceConnectPanel,
WorkspaceExploreBarDatabase, WorkspaceExploreBarSchema,
DatabaseContext, DatabaseContext,
TableContext, TableContext,
MiscContext, MiscContext,
ModalNewDatabase, ModalNewSchema,
ModalNewTable, ModalNewTable,
ModalNewView, ModalNewView,
ModalNewTrigger, ModalNewTrigger,
@@ -172,6 +182,8 @@ export default {
isNewSchedulerModal: false, isNewSchedulerModal: false,
localWidth: null, localWidth: null,
explorebarWidthInterval: null,
searchTermInterval: null,
isDatabaseContext: false, isDatabaseContext: false,
isTableContext: false, isTableContext: false,
isMiscContext: false, isMiscContext: false,
@@ -182,7 +194,8 @@ export default {
selectedDatabase: '', selectedDatabase: '',
selectedTable: null, selectedTable: null,
selectedMisc: null selectedMisc: null,
searchTerm: ''
}; };
}, },
computed: { computed: {
@@ -199,11 +212,22 @@ export default {
} }
}, },
watch: { watch: {
localWidth: _.debounce(function (val) { localWidth (val) {
this.changeExplorebarSize(val); clearTimeout(this.explorebarWidthInterval);
}, 500),
this.explorebarWidthInterval = setTimeout(() => {
this.changeExplorebarSize(val);
}, 500);
},
isSelected (val) { isSelected (val) {
if (val) this.localWidth = this.explorebarSize; if (val) this.localWidth = this.explorebarSize;
},
searchTerm () {
clearTimeout(this.searchTermInterval);
this.searchTermInterval = setTimeout(() => {
this.setSearchTerm(this.searchTerm);
}, 200);
} }
}, },
created () { created () {
@@ -225,6 +249,7 @@ export default {
refreshStructure: 'workspaces/refreshStructure', refreshStructure: 'workspaces/refreshStructure',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs', changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
selectTab: 'workspaces/selectTab', selectTab: 'workspaces/selectTab',
setSearchTerm: 'workspaces/setSearchTerm',
addNotification: 'notifications/addNotification', addNotification: 'notifications/addNotification',
changeExplorebarSize: 'settings/changeExplorebarSize' changeExplorebarSize: 'settings/changeExplorebarSize'
}), }),
@@ -274,8 +299,8 @@ export default {
else else
this.addNotification({ status: 'error', message: response }); this.addNotification({ status: 'error', message: response });
}, },
openDatabaseContext (payload) { openSchemaContext (payload) {
this.selectedDatabase = payload.database; this.selectedDatabase = payload.schema;
this.databaseContextEvent = payload.event; this.databaseContextEvent = payload.event;
this.isDatabaseContext = true; this.isDatabaseContext = true;
}, },
@@ -435,8 +460,6 @@ export default {
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
text-align: left; text-align: left;
background: $bg-color-gray;
box-shadow: 0 0 1px 0 #000;
z-index: 8; z-index: 8;
flex: initial; flex: initial;
position: relative; position: relative;
@@ -477,9 +500,40 @@ export default {
} }
} }
.workspace-explorebar-search {
width: 100%;
display: flex;
justify-content: space-between;
font-size: 0.6rem;
height: 28px;
.has-icon-right {
width: 100%;
padding: 0.1rem;
.form-icon {
opacity: 0.5;
transition: opacity 0.2s;
}
.form-input {
height: 1.2rem;
padding-left: 0.2rem;
&:focus + .form-icon {
opacity: 0.9;
}
&::placeholder {
opacity: 0.6;
}
}
}
}
.workspace-explorebar-body { .workspace-explorebar-body {
width: 100%; width: 100%;
height: calc((100vh - 30px) - #{$excluding-size}); height: calc((100vh - 58px) - #{$excluding-size});
overflow: overlay; overflow: overlay;
padding: 0 0.1rem; padding: 0 0.1rem;
} }

View File

@@ -5,8 +5,8 @@
> >
<div <div
v-if="['procedure', 'function'].includes(selectedMisc.type)" v-if="['procedure', 'function'].includes(selectedMisc.type)"
class="context-element disabled" class="context-element"
@click="showRunModal" @click="runElementCheck"
> >
<span class="d-flex"><i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ $t('word.run') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ $t('word.run') }}</span>
</div> </div>
@@ -29,6 +29,13 @@
</div> </div>
</div> </div>
</ConfirmModal> </ConfirmModal>
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localElement"
:client="workspace.client"
@confirm="runElement"
@close="hideAskParamsModal"
/>
</BaseContextMenu> </BaseContextMenu>
</template> </template>
@@ -36,6 +43,7 @@
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Triggers from '@/ipc-api/Triggers'; import Triggers from '@/ipc-api/Triggers';
import Routines from '@/ipc-api/Routines'; import Routines from '@/ipc-api/Routines';
import Functions from '@/ipc-api/Functions'; import Functions from '@/ipc-api/Functions';
@@ -45,7 +53,8 @@ export default {
name: 'WorkspaceExploreBarMiscContext', name: 'WorkspaceExploreBarMiscContext',
components: { components: {
BaseContextMenu, BaseContextMenu,
ConfirmModal ConfirmModal,
ModalAskParameters
}, },
props: { props: {
contextEvent: MouseEvent, contextEvent: MouseEvent,
@@ -54,7 +63,9 @@ export default {
data () { data () {
return { return {
isDeleteModal: false, isDeleteModal: false,
isRunModal: false isRunModal: false,
isAskingParameters: false,
localElement: {}
}; };
}, },
computed: { computed: {
@@ -83,7 +94,8 @@ export default {
methods: { methods: {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification', addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs' changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
newTab: 'workspaces/newTab'
}), }),
showCreateTableModal () { showCreateTableModal () {
this.$emit('show-create-table-modal'); this.$emit('show-create-table-modal');
@@ -94,11 +106,12 @@ export default {
hideDeleteModal () { hideDeleteModal () {
this.isDeleteModal = false; this.isDeleteModal = false;
}, },
showRunModal () { showAskParamsModal () {
this.isRunModal = true; this.isAskingParameters = true;
}, },
hideRunModal () { hideAskParamsModal () {
this.isRunModal = false; this.isAskingParameters = false;
this.closeContext();
}, },
closeContext () { closeContext () {
this.$emit('close-context'); this.$emit('close-context');
@@ -148,6 +161,107 @@ export default {
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); this.addNotification({ status: 'error', message: err.stack });
} }
},
runElementCheck () {
if (this.selectedMisc.type === 'procedure')
this.runRoutineCheck();
else if (this.selectedMisc.type === 'function')
this.runFunctionCheck();
},
runElement (params) {
if (this.selectedMisc.type === 'procedure')
this.runRoutine(params);
else if (this.selectedMisc.type === 'function')
this.runFunction(params);
},
async runRoutineCheck () {
const params = {
uid: this.selectedWorkspace,
schema: this.workspace.breadcrumbs.schema,
routine: this.workspace.breadcrumbs.procedure
};
try {
const { status, response } = await Routines.getRoutineInformations(params);
if (status === 'success')
this.localElement = response;
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
if (this.localElement.parameters.length)
this.showAskParamsModal();
else
this.runRoutine();
},
runRoutine (params) {
if (!params) params = [];
let sql;
switch (this.workspace.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
case 'pg':
sql = `CALL ${this.localElement.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `EXEC ${this.localElement.name} ${params.join(',')}`;
break;
default:
sql = `CALL \`${this.localElement.name}\`(${params.join(',')})`;
}
this.newTab({ uid: this.workspace.uid, content: sql, autorun: true });
this.closeContext();
},
async runFunctionCheck () {
const params = {
uid: this.selectedWorkspace,
schema: this.workspace.breadcrumbs.schema,
func: this.workspace.breadcrumbs.function
};
try {
const { status, response } = await Functions.getFunctionInformations(params);
if (status === 'success')
this.localElement = response;
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
if (this.localElement.parameters.length)
this.showAskParamsModal();
else
this.runFunction();
},
runFunction (params) {
if (!params) params = [];
let sql;
switch (this.workspace.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
sql = `SELECT \`${this.localElement.name}\` (${params.join(',')})`;
break;
case 'pg':
sql = `SELECT ${this.localElement.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `SELECT ${this.localElement.name} ${params.join(',')}`;
break;
default:
sql = `SELECT \`${this.localElement.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.workspace.uid, content: sql, autorun: true });
this.closeContext();
} }
} }
}; };

View File

@@ -4,7 +4,7 @@
class="accordion-header database-name" class="accordion-header database-name"
:class="{'text-bold': breadcrumbs.schema === database.name}" :class="{'text-bold': breadcrumbs.schema === database.name}"
@click="selectSchema(database.name)" @click="selectSchema(database.name)"
@contextmenu.prevent="showDatabaseContext($event, database.name)" @contextmenu.prevent="showSchemaContext($event, database.name)"
> >
<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" />
@@ -15,7 +15,7 @@
<div class="database-tables"> <div class="database-tables">
<ul class="menu menu-nav pt-0"> <ul class="menu menu-nav pt-0">
<li <li
v-for="table of database.tables" v-for="table of filteredTables"
:key="table.name" :key="table.name"
class="menu-item" class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}" :class="{'text-bold': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}"
@@ -24,7 +24,7 @@
> >
<a class="table-name"> <a class="table-name">
<i class="table-icon mdi mdi-18px mr-1" :class="table.type === 'view' ? 'mdi-table-eye' : 'mdi-table'" /> <i class="table-icon mdi mdi-18px mr-1" :class="table.type === 'view' ? 'mdi-table-eye' : 'mdi-table'" />
<span>{{ table.name }}</span> <span v-html="highlightWord(table.name)" />
</a> </a>
<div <div
v-if="table.type === 'table'" v-if="table.type === 'table'"
@@ -37,7 +37,7 @@
</ul> </ul>
</div> </div>
<div v-if="database.triggers.length" class="database-misc"> <div v-if="filteredTriggers.length && customizations.triggers" class="database-misc">
<details class="accordion"> <details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger}"> <summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger}">
<i class="misc-icon mdi mdi-18px mdi-folder-cog mr-1" /> <i class="misc-icon mdi mdi-18px mdi-folder-cog mr-1" />
@@ -47,7 +47,7 @@
<div> <div>
<ul class="menu menu-nav pt-0"> <ul class="menu menu-nav pt-0">
<li <li
v-for="trigger of database.triggers" v-for="trigger of filteredTriggers"
:key="trigger.name" :key="trigger.name"
class="menu-item" class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}"
@@ -56,7 +56,7 @@
> >
<a class="table-name"> <a class="table-name">
<i class="table-icon mdi mdi-table-cog mdi-18px mr-1" /> <i class="table-icon mdi mdi-table-cog mdi-18px mr-1" />
<span>{{ trigger.name }}</span> <span v-html="highlightWord(trigger.name)" />
</a> </a>
</li> </li>
</ul> </ul>
@@ -65,7 +65,7 @@
</details> </details>
</div> </div>
<div v-if="database.procedures.length" class="database-misc"> <div v-if="filteredProcedures.length && customizations.routines" class="database-misc">
<details class="accordion"> <details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure}"> <summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure}">
<i class="misc-icon mdi mdi-18px mdi-folder-sync mr-1" /> <i class="misc-icon mdi mdi-18px mdi-folder-sync mr-1" />
@@ -75,8 +75,8 @@
<div> <div>
<ul class="menu menu-nav pt-0"> <ul class="menu menu-nav pt-0">
<li <li
v-for="procedure of database.procedures" v-for="(procedure, i) of filteredProcedures"
:key="procedure.name" :key="`${procedure.name}-${i}`"
class="menu-item" class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure === procedure.name}" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure === procedure.name}"
@click="setBreadcrumbs({schema: database.name, procedure: procedure.name})" @click="setBreadcrumbs({schema: database.name, procedure: procedure.name})"
@@ -84,7 +84,7 @@
> >
<a class="table-name"> <a class="table-name">
<i class="table-icon mdi mdi-sync-circle mdi-18px mr-1" /> <i class="table-icon mdi mdi-sync-circle mdi-18px mr-1" />
<span>{{ procedure.name }}</span> <span v-html="highlightWord(procedure.name)" />
</a> </a>
</li> </li>
</ul> </ul>
@@ -93,7 +93,7 @@
</details> </details>
</div> </div>
<div v-if="database.functions.length" class="database-misc"> <div v-if="filteredFunctions.length && customizations.functions" class="database-misc">
<details class="accordion"> <details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function}"> <summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function}">
<i class="misc-icon mdi mdi-18px mdi-folder-move mr-1" /> <i class="misc-icon mdi mdi-18px mdi-folder-move mr-1" />
@@ -103,8 +103,8 @@
<div> <div>
<ul class="menu menu-nav pt-0"> <ul class="menu menu-nav pt-0">
<li <li
v-for="func of database.functions" v-for="(func, i) of filteredFunctions"
:key="func.name" :key="`${func.name}-${i}`"
class="menu-item" class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
@click="setBreadcrumbs({schema: database.name, function: func.name})" @click="setBreadcrumbs({schema: database.name, function: func.name})"
@@ -112,7 +112,7 @@
> >
<a class="table-name"> <a class="table-name">
<i class="table-icon mdi mdi-arrow-right-bold-box mdi-18px mr-1" /> <i class="table-icon mdi mdi-arrow-right-bold-box mdi-18px mr-1" />
<span>{{ func.name }}</span> <span v-html="highlightWord(func.name)" />
</a> </a>
</li> </li>
</ul> </ul>
@@ -121,7 +121,7 @@
</details> </details>
</div> </div>
<div v-if="database.schedulers.length" class="database-misc"> <div v-if="filteredSchedulers.length && customizations.schedulers" class="database-misc">
<details class="accordion"> <details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler}"> <summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler}">
<i class="misc-icon mdi mdi-18px mdi-folder-clock mr-1" /> <i class="misc-icon mdi mdi-18px mdi-folder-clock mr-1" />
@@ -131,7 +131,7 @@
<div> <div>
<ul class="menu menu-nav pt-0"> <ul class="menu menu-nav pt-0">
<li <li
v-for="scheduler of database.schedulers" v-for="scheduler of filteredSchedulers"
:key="scheduler.name" :key="scheduler.name"
class="menu-item" class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}"
@@ -140,7 +140,7 @@
> >
<a class="table-name"> <a class="table-name">
<i class="table-icon mdi mdi-calendar-clock mdi-18px mr-1" /> <i class="table-icon mdi mdi-calendar-clock mdi-18px mr-1" />
<span>{{ scheduler.name }}</span> <span v-html="highlightWord(scheduler.name)" />
</a> </a>
</li> </li>
</ul> </ul>
@@ -157,7 +157,7 @@ import { mapActions, mapGetters } from 'vuex';
import { formatBytes } from 'common/libs/formatBytes'; import { formatBytes } from 'common/libs/formatBytes';
export default { export default {
name: 'WorkspaceExploreBarDatabase', name: 'WorkspaceExploreBarSchema',
props: { props: {
database: Object, database: Object,
connection: Object connection: Object
@@ -170,11 +170,34 @@ export default {
computed: { computed: {
...mapGetters({ ...mapGetters({
getLoadedSchemas: 'workspaces/getLoadedSchemas', getLoadedSchemas: 'workspaces/getLoadedSchemas',
getWorkspace: 'workspaces/getWorkspace' getWorkspace: 'workspaces/getWorkspace',
getSearchTerm: 'workspaces/getSearchTerm',
applicationTheme: 'settings/getApplicationTheme'
}), }),
searchTerm () {
return this.getSearchTerm(this.connection.uid);
},
filteredTables () {
return this.database.tables.filter(table => table.name.search(this.searchTerm) >= 0);
},
filteredTriggers () {
return this.database.triggers.filter(trigger => trigger.name.search(this.searchTerm) >= 0);
},
filteredProcedures () {
return this.database.procedures.filter(procedure => procedure.name.search(this.searchTerm) >= 0);
},
filteredFunctions () {
return this.database.functions.filter(func => func.name.search(this.searchTerm) >= 0);
},
filteredSchedulers () {
return this.database.schedulers.filter(scheduler => scheduler.name.search(this.searchTerm) >= 0);
},
breadcrumbs () { breadcrumbs () {
return this.getWorkspace(this.connection.uid).breadcrumbs; return this.getWorkspace(this.connection.uid).breadcrumbs;
}, },
customizations () {
return this.getWorkspace(this.connection.uid).customizations;
},
loadedSchemas () { loadedSchemas () {
return this.getLoadedSchemas(this.connection.uid); return this.getLoadedSchemas(this.connection.uid);
}, },
@@ -203,9 +226,9 @@ export default {
this.changeBreadcrumbs({ schema, table: null }); this.changeBreadcrumbs({ schema, table: null });
}, },
showDatabaseContext (event, database) { showSchemaContext (event, schema) {
this.changeBreadcrumbs({ schema: database, table: null }); this.selectSchema(schema);
this.$emit('show-database-context', { event, database }); this.$emit('show-schema-context', { event, schema });
}, },
showTableContext (event, table) { showTableContext (event, table) {
this.setBreadcrumbs({ schema: this.database.name, [table.type]: table.name }); this.setBreadcrumbs({ schema: this.database.name, [table.type]: table.name });
@@ -217,11 +240,22 @@ export default {
}, },
piePercentage (val) { piePercentage (val) {
const perc = val / this.maxSize * 100; const perc = val / this.maxSize * 100;
return { background: `conic-gradient(lime ${perc}%, white 0)` }; if (this.applicationTheme === 'dark')
return { background: `conic-gradient(lime ${perc}%, white 0)` };
else
return { background: `conic-gradient(teal ${perc}%, silver 0)` };
}, },
setBreadcrumbs (payload) { setBreadcrumbs (payload) {
if (this.breadcrumbs.schema === payload.schema && this.breadcrumbs.table === payload.table) return; if (this.breadcrumbs.schema === payload.schema && this.breadcrumbs.table === payload.table) return;
this.changeBreadcrumbs(payload); this.changeBreadcrumbs(payload);
},
highlightWord (string) {
if (this.searchTerm) {
const regexp = new RegExp(`(${this.searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return string.replace(regexp, '<span class="text-primary">$1</span>');
}
else
return string;
} }
} }
}; };
@@ -229,6 +263,12 @@ export default {
<style lang="scss"> <style lang="scss">
.workspace-explorebar-database { .workspace-explorebar-database {
.database-name {
position: sticky;
top: 0;
z-index: 2;
}
.database-name, .database-name,
.misc-name, .misc-name,
a.table-name { a.table-name {
@@ -271,26 +311,15 @@ export default {
.database-name, .database-name,
.misc-name { .misc-name {
&:hover { &:hover {
color: $body-font-color;
background: rgba($color: #fff, $alpha: 0.05);
border-radius: 2px; border-radius: 2px;
} }
} }
a.table-name {
&:hover {
color: inherit;
background: inherit;
}
}
.menu-item { .menu-item {
line-height: 1.2; line-height: 1.2;
position: relative; position: relative;
&:hover { &:hover {
color: $body-font-color;
background: rgba($color: #fff, $alpha: 0.05);
border-radius: 2px; border-radius: 2px;
} }
} }

View File

@@ -7,27 +7,55 @@
<span class="d-flex"><i class="mdi mdi-18px mdi-plus text-light pr-1" /> {{ $t('word.add') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-plus text-light pr-1" /> {{ $t('word.add') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" /> <i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu"> <div class="context-submenu">
<div class="context-element" @click="showCreateTableModal"> <div
v-if="workspace.customizations.tableAdd"
class="context-element"
@click="showCreateTableModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table text-light pr-1" /> {{ $t('word.table') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-table text-light pr-1" /> {{ $t('word.table') }}</span>
</div> </div>
<div class="context-element" @click="showCreateViewModal"> <div
v-if="workspace.customizations.viewAdd"
class="context-element"
@click="showCreateViewModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-eye text-light pr-1" /> {{ $t('word.view') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-table-eye text-light pr-1" /> {{ $t('word.view') }}</span>
</div> </div>
<div class="context-element" @click="showCreateTriggerModal"> <div
v-if="workspace.customizations.triggerAdd"
class="context-element"
@click="showCreateTriggerModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-cog text-light pr-1" /> {{ $tc('word.trigger', 1) }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-table-cog text-light pr-1" /> {{ $tc('word.trigger', 1) }}</span>
</div> </div>
<div class="context-element" @click="showCreateRoutineModal"> <div
v-if="workspace.customizations.routineAdd"
class="context-element"
@click="showCreateRoutineModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle pr-1" /> {{ $tc('word.storedRoutine', 1) }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle pr-1" /> {{ $tc('word.storedRoutine', 1) }}</span>
</div> </div>
<div class="context-element" @click="showCreateFunctionModal"> <div
v-if="workspace.customizations.functionAdd"
class="context-element"
@click="showCreateFunctionModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box pr-1" /> {{ $tc('word.function', 1) }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box pr-1" /> {{ $tc('word.function', 1) }}</span>
</div> </div>
<div class="context-element" @click="showCreateSchedulerModal"> <div
v-if="workspace.customizations.schedulerAdd"
class="context-element"
@click="showCreateSchedulerModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-calendar-clock text-light pr-1" /> {{ $tc('word.scheduler', 1) }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-calendar-clock text-light pr-1" /> {{ $tc('word.scheduler', 1) }}</span>
</div> </div>
</div> </div>
</div> </div>
<div class="context-element" @click="showEditModal"> <div
v-if="workspace.customizations.schemaEdit"
class="context-element"
@click="showEditModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-edit text-light pr-1" /> {{ $t('word.edit') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-database-edit text-light pr-1" /> {{ $t('word.edit') }}</span>
</div> </div>
<div class="context-element" @click="showDeleteModal"> <div class="context-element" @click="showDeleteModal">
@@ -36,12 +64,12 @@
<ConfirmModal <ConfirmModal
v-if="isDeleteModal" v-if="isDeleteModal"
@confirm="deleteDatabase" @confirm="deleteSchema"
@hide="hideDeleteModal" @hide="hideDeleteModal"
> >
<template slot="header"> <template slot="header">
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-database-remove mr-1" /> {{ $t('message.deleteDatabase') }} <i class="mdi mdi-24px mdi-database-remove mr-1" /> {{ $t('message.deleteSchema') }}
</div> </div>
</template> </template>
<div slot="body"> <div slot="body">
@@ -50,7 +78,7 @@
</div> </div>
</div> </div>
</ConfirmModal> </ConfirmModal>
<ModalEditDatabase <ModalEditSchema
v-if="isEditModal" v-if="isEditModal"
:selected-database="selectedDatabase" :selected-database="selectedDatabase"
@close="hideEditModal" @close="hideEditModal"
@@ -62,15 +90,15 @@
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
import ModalEditDatabase from '@/components/ModalEditDatabase'; import ModalEditSchema from '@/components/ModalEditSchema';
import Database from '@/ipc-api/Database'; import Schema from '@/ipc-api/Schema';
export default { export default {
name: 'WorkspaceExploreBarDatabaseContext', name: 'WorkspaceExploreBarSchemaContext',
components: { components: {
BaseContextMenu, BaseContextMenu,
ConfirmModal, ConfirmModal,
ModalEditDatabase ModalEditSchema
}, },
props: { props: {
contextEvent: MouseEvent, contextEvent: MouseEvent,
@@ -130,9 +158,9 @@ export default {
closeContext () { closeContext () {
this.$emit('close-context'); this.$emit('close-context');
}, },
async deleteDatabase () { async deleteSchema () {
try { try {
const { status, response } = await Database.deleteDatabase({ const { status, response } = await Schema.deleteSchema({
uid: this.selectedWorkspace, uid: this.selectedWorkspace,
database: this.selectedDatabase database: this.selectedDatabase
}); });

View File

@@ -2,6 +2,7 @@
<ConfirmModal <ConfirmModal
:confirm-text="$t('word.confirm')" :confirm-text="$t('word.confirm')"
size="medium" size="medium"
class="options-modal"
@confirm="confirmForeignsChange" @confirm="confirmForeignsChange"
@hide="$emit('hide')" @hide="$emit('hide')"
> >
@@ -36,7 +37,7 @@
v-for="foreign in foreignProxy" v-for="foreign in foreignProxy"
:key="foreign._id" :key="foreign._id"
class="tile tile-centered c-hand mb-1 p-1" class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-foreign': selectedForeignID === foreign._id}" :class="{'selected-element': selectedForeignID === foreign._id}"
@click="selectForeign($event, foreign._id)" @click="selectForeign($event, foreign._id)"
> >
<div class="tile-icon"> <div class="tile-icon">
@@ -268,8 +269,10 @@ export default {
this.$emit('foreigns-update', this.foreignProxy); this.$emit('foreigns-update', this.foreignProxy);
}, },
selectForeign (event, id) { selectForeign (event, id) {
if (this.selectedForeignID !== id && !event.target.classList.contains('remove-field')) if (this.selectedForeignID !== id && !event.target.classList.contains('remove-field')) {
this.selectedForeignID = id; this.selectedForeignID = id;
this.getRefFields();
}
}, },
getModalInnerHeight () { getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body'); const modalBody = document.querySelector('.modal-body');
@@ -369,23 +372,16 @@ export default {
} }
&:hover { &:hover {
background: $bg-color-light;
.tile-action { .tile-action {
opacity: 1; opacity: 1;
} }
} }
&.selected-foreign { &.selected-element {
background: $bg-color-light;
opacity: 1; opacity: 1;
} }
} }
.editor-col {
border-left: 2px solid $bg-color-light;
}
.fields-list { .fields-list {
max-height: 80px; max-height: 80px;
overflow: auto; overflow: auto;

View File

@@ -26,7 +26,19 @@
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.languages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="optionsProxy.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.definer') }} {{ $t('word.definer') }}
</label> </label>
@@ -65,6 +77,12 @@
class="form-select text-uppercase" class="form-select text-uppercase"
style="width: 0;" style="width: 0;"
> >
<option v-if="localOptions.returns === 'VOID'">
VOID
</option>
<option v-if="!isInDataTypes">
{{ localOptions.returns }}
</option>
<optgroup <optgroup
v-for="group in workspace.dataTypes" v-for="group in workspace.dataTypes"
:key="group.group" :key="group.group"
@@ -81,6 +99,7 @@
</optgroup> </optgroup>
</select> </select>
<input <input
v-if="customizations.parametersLength"
v-model="optionsProxy.returnsLength" v-model="optionsProxy.returnsLength"
class="form-input" class="form-input"
type="number" type="number"
@@ -89,7 +108,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.comment" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.comment') }} {{ $t('word.comment') }}
</label> </label>
@@ -112,7 +131,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.functionDataAccess" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('message.dataAccess') }} {{ $t('message.dataAccess') }}
</label> </label>
@@ -125,7 +144,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.functionDeterministic" class="form-group">
<div class="col-4" /> <div class="col-4" />
<div class="column"> <div class="column">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
@@ -159,6 +178,19 @@ export default {
computed: { computed: {
isTableNameValid () { isTableNameValid () {
return this.optionsProxy.name !== ''; return this.optionsProxy.name !== '';
},
customizations () {
return this.workspace.customizations;
},
isInDataTypes () {
let typeNames = [];
for (const group of this.workspace.dataTypes) {
typeNames = group.types.reduce((acc, curr) => {
acc.push(curr.name);
return acc;
}, []);
}
return typeNames.includes(this.localOptions.returns);
} }
}, },
created () { created () {

View File

@@ -2,7 +2,8 @@
<ConfirmModal <ConfirmModal
:confirm-text="$t('word.confirm')" :confirm-text="$t('word.confirm')"
size="medium" size="medium"
@confirm="confirmIndexesChange" class="options-modal"
@confirm="confirmParametersChange"
@hide="$emit('hide')" @hide="$emit('hide')"
> >
<template :slot="'header'"> <template :slot="'header'">
@@ -34,27 +35,27 @@
<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.name" :key="param._id"
class="tile tile-centered c-hand mb-1 p-1" class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-param': selectedParam === param.name}" :class="{'selected-element': selectedParam === param._id}"
@click="selectParameter($event, param.name)" @click="selectParameter($event, param._id)"
> >
<div class="tile-icon"> <div class="tile-icon">
<div> <div>
<i class="mdi mdi-hexagon mdi-24px" :class="`type-${param.type.toLowerCase()}`" /> <i class="mdi mdi-hexagon mdi-24px" :class="typeClass(param.type)" />
</div> </div>
</div> </div>
<div class="tile-content"> <div class="tile-content">
<div class="tile-title"> <div class="tile-title">
{{ param.name }} {{ param.name }}
</div> </div>
<small class="tile-subtitle text-gray">{{ param.type }}{{ param.length ? `(${param.length})` : '' }}</small> <small class="tile-subtitle text-gray">{{ param.type }}{{ param.length ? `(${param.length})` : '' }} · {{ param.context }}</small>
</div> </div>
<div class="tile-action"> <div class="tile-action">
<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.name)" @click.prevent="removeParameter(param._id)"
> >
<i class="mdi mdi-close" /> <i class="mdi mdi-close" />
</button> </button>
@@ -105,7 +106,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.parametersLength" class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.length') }} {{ $t('word.length') }}
</label> </label>
@@ -118,6 +119,37 @@
> >
</div> </div>
</div> </div>
<div v-if="customizations.functionContext" class="form-group">
<label class="form-label col-3">
{{ $t('word.context') }}
</label>
<div class="column">
<label class="form-radio">
<input
v-model="selectedParamObj.context"
type="radio"
name="context"
value="IN"
> <i class="form-icon" /> IN
</label>
<label class="form-radio">
<input
v-model="selectedParamObj.context"
type="radio"
value="OUT"
name="context"
> <i class="form-icon" /> OUT
</label>
<label class="form-radio">
<input
v-model="selectedParamObj.context"
type="radio"
value="INOUT"
name="context"
> <i class="form-icon" /> INOUT
</label>
</div>
</div>
</form> </form>
<div v-if="!parametersProxy.length" class="empty"> <div v-if="!parametersProxy.length" class="empty">
<div class="empty-icon"> <div class="empty-icon">
@@ -139,6 +171,7 @@
</template> </template>
<script> <script>
import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
export default { export default {
@@ -162,10 +195,13 @@ export default {
}, },
computed: { computed: {
selectedParamObj () { selectedParamObj () {
return this.parametersProxy.find(param => param.name === this.selectedParam); return this.parametersProxy.find(param => param._id === this.selectedParam);
}, },
isChanged () { isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy); return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
},
customizations () {
return this.workspace.customizations;
} }
}, },
mounted () { mounted () {
@@ -182,12 +218,17 @@ export default {
window.removeEventListener('resize', this.getModalInnerHeight); window.removeEventListener('resize', this.getModalInnerHeight);
}, },
methods: { methods: {
confirmIndexesChange () { typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
confirmParametersChange () {
this.$emit('parameters-update', this.parametersProxy); this.$emit('parameters-update', this.parametersProxy);
}, },
selectParameter (event, name) { selectParameter (event, uid) {
if (this.selectedParam !== name && !event.target.classList.contains('remove-field')) if (this.selectedParam !== uid && !event.target.classList.contains('remove-field'))
this.selectedParam = name; this.selectedParam = uid;
}, },
getModalInnerHeight () { getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body'); const modalBody = document.querySelector('.modal-body');
@@ -196,6 +237,7 @@ export default {
}, },
addParameter () { addParameter () {
this.parametersProxy = [...this.parametersProxy, { this.parametersProxy = [...this.parametersProxy, {
_id: uidGen(),
name: `Param${this.i++}`, name: `Param${this.i++}`,
type: 'INT', type: 'INT',
context: 'IN', context: 'IN',
@@ -209,8 +251,8 @@ export default {
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60; this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60;
}, 20); }, 20);
}, },
removeParameter (name) { removeParameter (uid) {
this.parametersProxy = this.parametersProxy.filter(param => param.name !== name); this.parametersProxy = this.parametersProxy.filter(param => param._id !== uid);
if (this.selectedParam === name && this.parametersProxy.length) if (this.selectedParam === name && this.parametersProxy.length)
this.resetSelectedID(); this.resetSelectedID();
@@ -223,7 +265,7 @@ export default {
this.resetSelectedID(); this.resetSelectedID();
}, },
resetSelectedID () { resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0].name : ''; this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._id : '';
} }
} }
}; };
@@ -242,23 +284,16 @@ export default {
} }
&:hover { &:hover {
background: $bg-color-light;
.tile-action { .tile-action {
opacity: 1; opacity: 1;
} }
} }
&.selected-param { &.selected-element {
background: $bg-color-light;
opacity: 1; opacity: 1;
} }
} }
.editor-col {
border-left: 2px solid $bg-color-light;
}
.fields-list { .fields-list {
max-height: 300px; max-height: 300px;
overflow: auto; overflow: auto;

View File

@@ -2,6 +2,7 @@
<ConfirmModal <ConfirmModal
:confirm-text="$t('word.confirm')" :confirm-text="$t('word.confirm')"
size="medium" size="medium"
class="options-modal"
@confirm="confirmIndexesChange" @confirm="confirmIndexesChange"
@hide="$emit('hide')" @hide="$emit('hide')"
> >
@@ -36,7 +37,7 @@
v-for="index in indexesProxy" v-for="index in indexesProxy"
:key="index._id" :key="index._id"
class="tile tile-centered c-hand mb-1 p-1" class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-index': selectedIndexID === index._id}" :class="{'selected-element': selectedIndexID === index._id}"
@click="selectIndex($event, index._id)" @click="selectIndex($event, index._id)"
> >
<div class="tile-icon"> <div class="tile-icon">
@@ -256,23 +257,16 @@ export default {
} }
&:hover { &:hover {
background: $bg-color-light;
.tile-action { .tile-action {
opacity: 1; opacity: 1;
} }
} }
&.selected-index { &.selected-element {
background: $bg-color-light;
opacity: 1; opacity: 1;
} }
} }
.editor-col {
border-left: 2px solid $bg-color-light;
}
.fields-list { .fields-list {
max-height: 300px; max-height: 300px;
overflow: auto; overflow: auto;

View File

@@ -26,7 +26,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="workspace.customizations.comment" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.comment') }} {{ $t('word.comment') }}
</label> </label>
@@ -38,7 +38,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="workspace.customizations.autoIncrement" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.autoIncrement') }} {{ $t('word.autoIncrement') }}
</label> </label>
@@ -50,7 +50,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="workspace.customizations.collations" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.collation') }} {{ $t('word.collation') }}
</label> </label>
@@ -66,7 +66,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="workspace.customizations.engines" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.engine') }} {{ $t('word.engine') }}
</label> </label>

View File

@@ -26,7 +26,19 @@
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.languages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="optionsProxy.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.definer') }} {{ $t('word.definer') }}
</label> </label>
@@ -54,7 +66,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.comment" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.comment') }} {{ $t('word.comment') }}
</label> </label>
@@ -77,7 +89,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.procedureDataAccess" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('message.dataAccess') }} {{ $t('message.dataAccess') }}
</label> </label>
@@ -90,7 +102,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.procedureDeterministic" class="form-group">
<div class="col-4" /> <div class="col-4" />
<div class="column"> <div class="column">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
@@ -124,6 +136,9 @@ export default {
computed: { computed: {
isTableNameValid () { isTableNameValid () {
return this.optionsProxy.name !== ''; return this.optionsProxy.name !== '';
},
customizations () {
return this.workspace.customizations;
} }
}, },
created () { created () {

View File

@@ -2,7 +2,8 @@
<ConfirmModal <ConfirmModal
:confirm-text="$t('word.confirm')" :confirm-text="$t('word.confirm')"
size="medium" size="medium"
@confirm="confirmIndexesChange" class="options-modal"
@confirm="confirmParametersChange"
@hide="$emit('hide')" @hide="$emit('hide')"
> >
<template :slot="'header'"> <template :slot="'header'">
@@ -34,14 +35,14 @@
<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.name" :key="param._id"
class="tile tile-centered c-hand mb-1 p-1" class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-param': selectedParam === param.name}" :class="{'selected-element': selectedParam === param._id}"
@click="selectParameter($event, param.name)" @click="selectParameter($event, param._id)"
> >
<div class="tile-icon"> <div class="tile-icon">
<div> <div>
<i class="mdi mdi-hexagon mdi-24px" :class="`type-${param.type.toLowerCase()}`" /> <i class="mdi mdi-hexagon mdi-24px" :class="typeClass(param.type)" />
</div> </div>
</div> </div>
<div class="tile-content"> <div class="tile-content">
@@ -54,7 +55,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.name)" @click.prevent="removeParameter(param._id)"
> >
<i class="mdi mdi-close" /> <i class="mdi mdi-close" />
</button> </button>
@@ -105,7 +106,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.parametersLength" class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.length') }} {{ $t('word.length') }}
</label> </label>
@@ -118,7 +119,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.procedureContext" class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.context') }} {{ $t('word.context') }}
</label> </label>
@@ -170,6 +171,7 @@
</template> </template>
<script> <script>
import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
export default { export default {
@@ -193,10 +195,13 @@ export default {
}, },
computed: { computed: {
selectedParamObj () { selectedParamObj () {
return this.parametersProxy.find(param => param.name === this.selectedParam); return this.parametersProxy.find(param => param._id === this.selectedParam);
}, },
isChanged () { isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy); return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
},
customizations () {
return this.workspace.customizations;
} }
}, },
mounted () { mounted () {
@@ -213,12 +218,17 @@ export default {
window.removeEventListener('resize', this.getModalInnerHeight); window.removeEventListener('resize', this.getModalInnerHeight);
}, },
methods: { methods: {
confirmIndexesChange () { typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
confirmParametersChange () {
this.$emit('parameters-update', this.parametersProxy); this.$emit('parameters-update', this.parametersProxy);
}, },
selectParameter (event, name) { selectParameter (event, uid) {
if (this.selectedParam !== name && !event.target.classList.contains('remove-field')) if (this.selectedParam !== uid && !event.target.classList.contains('remove-field'))
this.selectedParam = name; this.selectedParam = uid;
}, },
getModalInnerHeight () { getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body'); const modalBody = document.querySelector('.modal-body');
@@ -227,10 +237,11 @@ export default {
}, },
addParameter () { addParameter () {
this.parametersProxy = [...this.parametersProxy, { this.parametersProxy = [...this.parametersProxy, {
name: `Param${this.i++}`, _id: uidGen(),
type: 'INT', name: `param${this.i++}`,
type: this.workspace.dataTypes[0].types[0].name,
context: 'IN', context: 'IN',
length: 10 length: ''
}]; }];
if (this.parametersProxy.length === 1) if (this.parametersProxy.length === 1)
@@ -240,8 +251,8 @@ export default {
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60; this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60;
}, 20); }, 20);
}, },
removeParameter (name) { removeParameter (uid) {
this.parametersProxy = this.parametersProxy.filter(param => param.name !== name); this.parametersProxy = this.parametersProxy.filter(param => param._id !== uid);
if (this.selectedParam === name && this.parametersProxy.length) if (this.selectedParam === name && this.parametersProxy.length)
this.resetSelectedID(); this.resetSelectedID();
@@ -254,7 +265,7 @@ export default {
this.resetSelectedID(); this.resetSelectedID();
}, },
resetSelectedID () { resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0].name : ''; this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._id : '';
} }
} }
}; };
@@ -273,23 +284,16 @@ export default {
} }
&:hover { &:hover {
background: $bg-color-light;
.tile-action { .tile-action {
opacity: 1; opacity: 1;
} }
} }
&.selected-param { &.selected-element {
background: $bg-color-light;
opacity: 1; opacity: 1;
} }
} }
.editor-col {
border-left: 2px solid $bg-color-light;
}
.fields-list { .fields-list {
max-height: 300px; max-height: 300px;
overflow: auto; overflow: auto;

View File

@@ -138,7 +138,7 @@
<script> <script>
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
import { mask } from 'vue-the-mask'; import { VueMaskDirective } from 'v-mask';
import moment from 'moment'; import moment from 'moment';
export default { export default {
@@ -147,7 +147,7 @@ export default {
ConfirmModal ConfirmModal
}, },
directives: { directives: {
mask mask: VueMaskDirective
}, },
props: { props: {
localOptions: Object, localOptions: Object,

View File

@@ -7,6 +7,7 @@
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm"
:disabled="!isChanged" :disabled="!isChanged"
:class="{'loading':isSaving}" :class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges" @click="saveChanges"
> >
<span>{{ $t('word.save') }}</span> <span>{{ $t('word.save') }}</span>
@@ -68,6 +69,7 @@
@remove-field="removeField" @remove-field="removeField"
@add-new-index="addNewIndex" @add-new-index="addNewIndex"
@add-to-index="addToIndex" @add-to-index="addToIndex"
@rename-field="renameField"
/> />
</div> </div>
<WorkspacePropsOptionsModal <WorkspacePropsOptionsModal
@@ -149,6 +151,7 @@ export default {
computed: { computed: {
...mapGetters({ ...mapGetters({
getWorkspace: 'workspaces/getWorkspace', getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected',
getDatabaseVariable: 'workspaces/getDatabaseVariable' getDatabaseVariable: 'workspaces/getDatabaseVariable'
}), }),
workspace () { workspace () {
@@ -162,7 +165,7 @@ export default {
return this.getDatabaseVariable(this.connection.uid, 'default_storage_engine').value || ''; return this.getDatabaseVariable(this.connection.uid, 'default_storage_engine').value || '';
}, },
isSelected () { isSelected () {
return this.workspace.selected_tab === 'prop'; return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.table;
}, },
schema () { schema () {
return this.workspace.breadcrumbs.schema; return this.workspace.breadcrumbs.schema;
@@ -199,6 +202,12 @@ export default {
this.setUnsavedChanges(val); this.setUnsavedChanges(val);
} }
}, },
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: { methods: {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification', addNotification: 'notifications/addNotification',
@@ -224,6 +233,20 @@ export default {
const { status, response } = await Tables.getTableColumns(params); const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') { if (status === 'success') {
this.originalFields = response.map(field => { this.originalFields = response.map(field => {
if (field.autoIncrement)
field.defaultType = 'autoincrement';
else if (field.default === null)
field.defaultType = 'noval';
else if (field.default === 'NULL')
field.defaultType = 'null';
else if (field.default.match(/^\s*(\w+)\s*\((.*)\)$/))
field.defaultType = 'expression';
else {
field.defaultType = 'custom';
if (isNaN(field.default) && !field.default.includes('\''))
field.default = `'${field.default}'`;
}
return { ...field, _id: uidGen() }; return { ...field, _id: uidGen() };
}); });
this.localFields = JSON.parse(JSON.stringify(this.originalFields)); this.localFields = JSON.parse(JSON.stringify(this.originalFields));
@@ -433,11 +456,11 @@ export default {
_id: uidGen(), _id: uidGen(),
name: `${this.$tc('word.field', 1)}_${++this.newFieldsCounter}`, name: `${this.$tc('word.field', 1)}_${++this.newFieldsCounter}`,
key: '', key: '',
type: 'int', type: this.workspace.dataTypes[0].types[0].name,
schema: this.schema, schema: this.schema,
table: this.table, table: this.table,
numPrecision: null, numPrecision: null,
numLength: 11, numLength: this.workspace.dataTypes[0].types[0].length,
datePrecision: null, datePrecision: null,
charLength: null, charLength: null,
nullable: false, nullable: false,
@@ -457,6 +480,20 @@ export default {
scrollable.scrollTop = scrollable.scrollHeight + 30; scrollable.scrollTop = scrollable.scrollHeight + 30;
}, 20); }, 20);
}, },
renameField (payload) {
this.localIndexes = this.localIndexes.map(index => {
const fi = index.fields.findIndex(field => field === payload.old);
if (fi !== -1)
index.fields[fi] = payload.new;
return index;
});
this.localKeyUsage = this.localKeyUsage.map(key => {
if (key.field === payload.old)
key.field = payload.new;
return key;
});
},
removeField (uid) { removeField (uid) {
this.localFields = this.localFields.filter(field => field._id !== uid); this.localFields = this.localFields.filter(field => field._id !== uid);
}, },
@@ -504,6 +541,15 @@ export default {
}, },
foreignsUpdate (foreigns) { foreignsUpdate (foreigns) {
this.localKeyUsage = foreigns; this.localKeyUsage = foreigns;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
} }
} }
}; };

View File

@@ -7,6 +7,7 @@
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm"
:disabled="!isChanged" :disabled="!isChanged"
:class="{'loading':isSaving}" :class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges" @click="saveChanges"
> >
<span>{{ $t('word.save') }}</span> <span>{{ $t('word.save') }}</span>
@@ -25,9 +26,9 @@
<div class="divider-vert py-3" /> <div class="divider-vert py-3" />
<button <button
class="btn btn-dark btn-sm disabled" class="btn btn-dark btn-sm"
:disabled="isChanged" :disabled="isChanged"
@click="false" @click="runFunctionCheck"
> >
<span>{{ $t('word.run') }}</span> <span>{{ $t('word.run') }}</span>
<i class="mdi mdi-24px mdi-play ml-1" /> <i class="mdi mdi-24px mdi-play ml-1" />
@@ -47,7 +48,7 @@
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.functionBody') }}</label> <label class="form-label ml-2">{{ $t('message.functionBody') }}</label>
<QueryEditor <QueryEditor
v-if="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
:value.sync="localFunction.sql" :value.sync="localFunction.sql"
:workspace="workspace" :workspace="workspace"
@@ -70,15 +71,24 @@
@hide="hideParamsModal" @hide="hideParamsModal"
@parameters-update="parametersUpdate" @parameters-update="parametersUpdate"
/> />
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localFunction"
:client="workspace.client"
@confirm="runFunction"
@close="hideAskParamsModal"
/>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor';
import WorkspacePropsFunctionOptionsModal from '@/components/WorkspacePropsFunctionOptionsModal'; import WorkspacePropsFunctionOptionsModal from '@/components/WorkspacePropsFunctionOptionsModal';
import WorkspacePropsFunctionParamsModal from '@/components/WorkspacePropsFunctionParamsModal'; import WorkspacePropsFunctionParamsModal from '@/components/WorkspacePropsFunctionParamsModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Functions from '@/ipc-api/Functions'; import Functions from '@/ipc-api/Functions';
export default { export default {
@@ -87,7 +97,8 @@ export default {
BaseLoader, BaseLoader,
QueryEditor, QueryEditor,
WorkspacePropsFunctionOptionsModal, WorkspacePropsFunctionOptionsModal,
WorkspacePropsFunctionParamsModal WorkspacePropsFunctionParamsModal,
ModalAskParameters
}, },
props: { props: {
connection: Object, connection: Object,
@@ -100,6 +111,7 @@ export default {
isSaving: false, isSaving: false,
isOptionsModal: false, isOptionsModal: false,
isParamsModal: false, isParamsModal: false,
isAskingParameters: false,
originalFunction: null, originalFunction: null,
localFunction: { sql: '' }, localFunction: { sql: '' },
lastFunction: null, lastFunction: null,
@@ -109,13 +121,14 @@ export default {
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace' getWorkspace: 'workspaces/getWorkspace'
}), }),
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
}, },
isSelected () { isSelected () {
return this.workspace.selected_tab === 'prop'; return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.function;
}, },
schema () { schema () {
return this.workspace.breadcrumbs.schema; return this.workspace.breadcrumbs.schema;
@@ -160,12 +173,19 @@ export default {
destroyed () { destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor); window.removeEventListener('resize', this.resizeQueryEditor);
}, },
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: { methods: {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification', addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure', refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges', setUnsavedChanges: 'workspaces/setUnsavedChanges',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs' changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
newTab: 'workspaces/newTab'
}), }),
async getFunctionData () { async getFunctionData () {
if (!this.function) return; if (!this.function) return;
@@ -183,6 +203,12 @@ export default {
const { status, response } = await Functions.getFunctionInformations(params); const { status, response } = await Functions.getFunctionInformations(params);
if (status === 'success') { if (status === 'success') {
this.originalFunction = response; this.originalFunction = response;
this.originalFunction.parameters = [...this.originalFunction.parameters.map(param => {
param._id = uidGen();
return param;
})];
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction)); this.localFunction = JSON.parse(JSON.stringify(this.originalFunction));
this.sqlProxy = this.localFunction.sql; this.sqlProxy = this.localFunction.sql;
} }
@@ -250,6 +276,33 @@ export default {
parametersUpdate (parameters) { parametersUpdate (parameters) {
this.localFunction = { ...this.localFunction, parameters }; this.localFunction = { ...this.localFunction, parameters };
}, },
runFunctionCheck () {
if (this.localFunction.parameters.length)
this.showAskParamsModal();
else
this.runFunction();
},
runFunction (params) {
if (!params) params = [];
let sql;
switch (this.connection.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
sql = `SELECT \`${this.originalFunction.name}\` (${params.join(',')})`;
break;
case 'pg':
sql = `SELECT ${this.originalFunction.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `SELECT ${this.originalFunction.name} ${params.join(',')}`;
break;
default:
sql = `SELECT \`${this.originalFunction.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.connection.uid, content: sql, autorun: true });
},
showOptionsModal () { showOptionsModal () {
this.isOptionsModal = true; this.isOptionsModal = true;
}, },
@@ -261,6 +314,21 @@ export default {
}, },
hideParamsModal () { hideParamsModal () {
this.isParamsModal = false; this.isParamsModal = false;
},
showAskParamsModal () {
this.isAskingParameters = true;
},
hideAskParamsModal () {
this.isAskingParameters = false;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
} }
} }
}; };

View File

@@ -7,6 +7,7 @@
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm"
:disabled="!isChanged" :disabled="!isChanged"
:class="{'loading':isSaving}" :class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges" @click="saveChanges"
> >
<span>{{ $t('word.save') }}</span> <span>{{ $t('word.save') }}</span>
@@ -25,9 +26,9 @@
<div class="divider-vert py-3" /> <div class="divider-vert py-3" />
<button <button
class="btn btn-dark btn-sm disabled" class="btn btn-dark btn-sm"
:disabled="isChanged" :disabled="isChanged"
@click="false" @click="runRoutineCheck"
> >
<span>{{ $t('word.run') }}</span> <span>{{ $t('word.run') }}</span>
<i class="mdi mdi-24px mdi-play ml-1" /> <i class="mdi mdi-24px mdi-play ml-1" />
@@ -47,7 +48,8 @@
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.routineBody') }}</label> <label class="form-label ml-2">{{ $t('message.routineBody') }}</label>
<QueryEditor <QueryEditor
v-if="isSelected" v-show="isSelected"
:key="`${routine}-${_uid}`"
ref="queryEditor" ref="queryEditor"
:value.sync="localRoutine.sql" :value.sync="localRoutine.sql"
:workspace="workspace" :workspace="workspace"
@@ -70,15 +72,24 @@
@hide="hideParamsModal" @hide="hideParamsModal"
@parameters-update="parametersUpdate" @parameters-update="parametersUpdate"
/> />
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localRoutine"
:client="workspace.client"
@confirm="runRoutine"
@close="hideAskParamsModal"
/>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader';
import WorkspacePropsRoutineOptionsModal from '@/components/WorkspacePropsRoutineOptionsModal'; import WorkspacePropsRoutineOptionsModal from '@/components/WorkspacePropsRoutineOptionsModal';
import WorkspacePropsRoutineParamsModal from '@/components/WorkspacePropsRoutineParamsModal'; import WorkspacePropsRoutineParamsModal from '@/components/WorkspacePropsRoutineParamsModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Routines from '@/ipc-api/Routines'; import Routines from '@/ipc-api/Routines';
export default { export default {
@@ -87,7 +98,8 @@ export default {
QueryEditor, QueryEditor,
BaseLoader, BaseLoader,
WorkspacePropsRoutineOptionsModal, WorkspacePropsRoutineOptionsModal,
WorkspacePropsRoutineParamsModal WorkspacePropsRoutineParamsModal,
ModalAskParameters
}, },
props: { props: {
connection: Object, connection: Object,
@@ -100,6 +112,7 @@ export default {
isSaving: false, isSaving: false,
isOptionsModal: false, isOptionsModal: false,
isParamsModal: false, isParamsModal: false,
isAskingParameters: false,
originalRoutine: null, originalRoutine: null,
localRoutine: { sql: '' }, localRoutine: { sql: '' },
lastRoutine: null, lastRoutine: null,
@@ -109,13 +122,14 @@ export default {
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace' getWorkspace: 'workspaces/getWorkspace'
}), }),
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
}, },
isSelected () { isSelected () {
return this.workspace.selected_tab === 'prop'; return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.routine;
}, },
schema () { schema () {
return this.workspace.breadcrumbs.schema; return this.workspace.breadcrumbs.schema;
@@ -160,12 +174,19 @@ export default {
destroyed () { destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor); window.removeEventListener('resize', this.resizeQueryEditor);
}, },
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: { methods: {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification', addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure', refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges', setUnsavedChanges: 'workspaces/setUnsavedChanges',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs' changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
newTab: 'workspaces/newTab'
}), }),
async getRoutineData () { async getRoutineData () {
if (!this.routine) return; if (!this.routine) return;
@@ -182,6 +203,12 @@ export default {
const { status, response } = await Routines.getRoutineInformations(params); const { status, response } = await Routines.getRoutineInformations(params);
if (status === 'success') { if (status === 'success') {
this.originalRoutine = response; this.originalRoutine = response;
this.originalRoutine.parameters = [...this.originalRoutine.parameters.map(param => {
param._id = uidGen();
return param;
})];
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine)); this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine));
this.sqlProxy = this.localRoutine.sql; this.sqlProxy = this.localRoutine.sql;
} }
@@ -249,6 +276,31 @@ export default {
parametersUpdate (parameters) { parametersUpdate (parameters) {
this.localRoutine = { ...this.localRoutine, parameters }; this.localRoutine = { ...this.localRoutine, parameters };
}, },
runRoutineCheck () {
if (this.localRoutine.parameters.length)
this.showAskParamsModal();
else
this.runRoutine();
},
runRoutine (params) {
if (!params) params = [];
let sql;
switch (this.connection.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
case 'pg':
sql = `CALL ${this.originalRoutine.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `EXEC ${this.originalRoutine.name} ${params.join(',')}`;
break;
default:
sql = `CALL \`${this.originalRoutine.name}\`(${params.join(',')})`;
}
this.newTab({ uid: this.connection.uid, content: sql, autorun: true });
},
showOptionsModal () { showOptionsModal () {
this.isOptionsModal = true; this.isOptionsModal = true;
}, },
@@ -260,6 +312,21 @@ export default {
}, },
hideParamsModal () { hideParamsModal () {
this.isParamsModal = false; this.isParamsModal = false;
},
showAskParamsModal () {
this.isAskingParameters = true;
},
hideAskParamsModal () {
this.isAskingParameters = false;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
} }
} }
}; };

View File

@@ -7,6 +7,7 @@
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm"
:disabled="!isChanged" :disabled="!isChanged"
:class="{'loading':isSaving}" :class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges" @click="saveChanges"
> >
<span>{{ $t('word.save') }}</span> <span>{{ $t('word.save') }}</span>
@@ -118,7 +119,7 @@
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.schedulerBody') }}</label> <label class="form-label ml-2">{{ $t('message.schedulerBody') }}</label>
<QueryEditor <QueryEditor
v-if="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
:value.sync="localScheduler.sql" :value.sync="localScheduler.sql"
:workspace="workspace" :workspace="workspace"
@@ -169,13 +170,14 @@ export default {
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace' getWorkspace: 'workspaces/getWorkspace'
}), }),
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
}, },
isSelected () { isSelected () {
return this.workspace.selected_tab === 'prop'; return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.scheduler;
}, },
schema () { schema () {
return this.workspace.breadcrumbs.schema; return this.workspace.breadcrumbs.schema;
@@ -220,6 +222,12 @@ export default {
destroyed () { destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor); window.removeEventListener('resize', this.resizeQueryEditor);
}, },
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: { methods: {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification', addNotification: 'notifications/addNotification',
@@ -310,6 +318,15 @@ export default {
}, },
timingUpdate (options) { timingUpdate (options) {
this.localScheduler = options; this.localScheduler = options;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
} }
} }
}; };

View File

@@ -7,6 +7,7 @@
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm"
:disabled="!isChanged" :disabled="!isChanged"
:class="{'loading':isSaving}" :class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges" @click="saveChanges"
> >
<span>{{ $t('word.save') }}</span> <span>{{ $t('word.save') }}</span>
@@ -99,7 +100,7 @@
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.triggerStatement') }}</label> <label class="form-label ml-2">{{ $t('message.triggerStatement') }}</label>
<QueryEditor <QueryEditor
v-if="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
:value.sync="localTrigger.sql" :value.sync="localTrigger.sql"
:workspace="workspace" :workspace="workspace"
@@ -140,13 +141,14 @@ export default {
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace' getWorkspace: 'workspaces/getWorkspace'
}), }),
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
}, },
isSelected () { isSelected () {
return this.workspace.selected_tab === 'prop'; return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.trigger;
}, },
schema () { schema () {
return this.workspace.breadcrumbs.schema; return this.workspace.breadcrumbs.schema;
@@ -191,6 +193,12 @@ export default {
destroyed () { destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor); window.removeEventListener('resize', this.resizeQueryEditor);
}, },
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: { methods: {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification', addNotification: 'notifications/addNotification',
@@ -274,6 +282,15 @@ export default {
this.editorHeight = size; this.editorHeight = size;
this.$refs.queryEditor.editor.resize(); this.$refs.queryEditor.editor.resize();
} }
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
} }
} }
}; };

View File

@@ -7,6 +7,7 @@
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm"
:disabled="!isChanged" :disabled="!isChanged"
:class="{'loading':isSaving}" :class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges" @click="saveChanges"
> >
<span>{{ $t('word.save') }}</span> <span>{{ $t('word.save') }}</span>
@@ -37,7 +38,7 @@
</div> </div>
</div> </div>
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div v-if="workspace.customizations.definer" class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label> <label class="form-label">{{ $t('word.definer') }}</label>
<select <select
v-if="workspace.users.length" v-if="workspace.users.length"
@@ -68,7 +69,7 @@
</div> </div>
<div class="columns"> <div class="columns">
<div class="column col-auto mr-2"> <div class="column col-auto mr-2">
<div class="form-group"> <div v-if="workspace.customizations.viewSqlSecurity" class="form-group">
<label class="form-label">{{ $t('message.sqlSecurity') }}</label> <label class="form-label">{{ $t('message.sqlSecurity') }}</label>
<label class="form-radio"> <label class="form-radio">
<input <input
@@ -91,7 +92,7 @@
</div> </div>
</div> </div>
<div class="column col-auto mr-2"> <div class="column col-auto mr-2">
<div class="form-group"> <div v-if="workspace.customizations.viewAlgorithm" class="form-group">
<label class="form-label">{{ $t('word.algorithm') }}</label> <label class="form-label">{{ $t('word.algorithm') }}</label>
<label class="form-radio"> <label class="form-radio">
<input <input
@@ -122,7 +123,7 @@
</label> </label>
</div> </div>
</div> </div>
<div class="column col-auto mr-2"> <div v-if="workspace.customizations.viewUpdateOption" class="column col-auto mr-2">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('message.updateOption') }}</label> <label class="form-label">{{ $t('message.updateOption') }}</label>
<label class="form-radio"> <label class="form-radio">
@@ -201,13 +202,14 @@ export default {
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace' getWorkspace: 'workspaces/getWorkspace'
}), }),
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
}, },
isSelected () { isSelected () {
return this.workspace.selected_tab === 'prop'; return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.view;
}, },
schema () { schema () {
return this.workspace.breadcrumbs.schema; return this.workspace.breadcrumbs.schema;
@@ -245,6 +247,12 @@ export default {
destroyed () { destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor); window.removeEventListener('resize', this.resizeQueryEditor);
}, },
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: { methods: {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification', addNotification: 'notifications/addNotification',
@@ -327,6 +335,15 @@ export default {
this.editorHeight = size; this.editorHeight = size;
this.$refs.queryEditor.editor.resize(); this.$refs.queryEditor.editor.resize();
} }
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
} }
} }
}; };

View File

@@ -42,6 +42,13 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="customizations.tableArray" class="th">
<div class="column-resizable">
<div class="table-column-title">
{{ $t('word.array') }}
</div>
</div>
</div>
<div class="th"> <div class="th">
<div class="column-resizable"> <div class="column-resizable">
<div class="table-column-title"> <div class="table-column-title">
@@ -49,21 +56,21 @@
</div> </div>
</div> </div>
</div> </div>
<div class="th"> <div v-if="customizations.unsigned" class="th">
<div class="column-resizable"> <div class="column-resizable">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.unsigned') }} {{ $t('word.unsigned') }}
</div> </div>
</div> </div>
</div> </div>
<div class="th"> <div v-if="customizations.nullable" class="th">
<div class="column-resizable"> <div class="column-resizable">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('message.allowNull') }} {{ $t('message.allowNull') }}
</div> </div>
</div> </div>
</div> </div>
<div class="th"> <div v-if="customizations.zerofill" class="th">
<div class="column-resizable"> <div class="column-resizable">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('message.zeroFill') }} {{ $t('message.zeroFill') }}
@@ -77,14 +84,14 @@
</div> </div>
</div> </div>
</div> </div>
<div class="th"> <div v-if="customizations.comment" class="th">
<div class="column-resizable"> <div class="column-resizable">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.comment') }} {{ $t('word.comment') }}
</div> </div>
</div> </div>
</div> </div>
<div class="th"> <div v-if="customizations.collation" class="th">
<div class="column-resizable min-100"> <div class="column-resizable min-100">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.collation') }} {{ $t('word.collation') }}
@@ -106,7 +113,9 @@
:indexes="getIndexes(row.name)" :indexes="getIndexes(row.name)"
:foreigns="getForeigns(row.name)" :foreigns="getForeigns(row.name)"
:data-types="dataTypes" :data-types="dataTypes"
:customizations="customizations"
@contextmenu="contextMenu" @contextmenu="contextMenu"
@rename-field="$emit('rename-field', $event)"
/> />
</draggable> </draggable>
</div> </div>
@@ -154,6 +163,9 @@ export default {
workspaceSchema () { workspaceSchema () {
return this.getWorkspace(this.connUid).breadcrumbs.schema; return this.getWorkspace(this.connUid).breadcrumbs.schema;
}, },
customizations () {
return this.getWorkspace(this.connUid).customizations;
},
dataTypes () { dataTypes () {
return this.getWorkspace(this.connUid).dataTypes; return this.getWorkspace(this.connUid).dataTypes;
}, },

View File

@@ -1,8 +1,8 @@
<template> <template>
<div class="tr" @contextmenu.prevent="$emit('contextmenu', $event, localRow._id)"> <div class="tr" @contextmenu.prevent="$emit('contextmenu', $event, localRow._id)">
<div class="td" tabindex="0"> <div class="td" tabindex="0">
<div class="row-draggable"> <div :class="customizations.sortableFields ? 'row-draggable' : 'text-center'">
<i class="mdi mdi-drag-horizontal row-draggable-icon" /> <i v-if="customizations.sortableFields" class="mdi mdi-drag-horizontal row-draggable-icon" />
{{ localRow.order }} {{ localRow.order }}
</div> </div>
</div> </div>
@@ -48,7 +48,7 @@
<span <span
v-if="!isInlineEditor.type" v-if="!isInlineEditor.type"
class="cell-content text-left" class="cell-content text-left"
:class="`type-${lowerCase(localRow.type)}`" :class="typeClass(localRow.type)"
@click="editON($event, localRow.type.toUpperCase(), 'type')" @click="editON($event, localRow.type.toUpperCase(), 'type')"
> >
{{ localRow.type }} {{ localRow.type }}
@@ -60,6 +60,9 @@
class="form-select editable-field small-select text-uppercase" class="form-select editable-field small-select text-uppercase"
@blur="editOFF" @blur="editOFF"
> >
<option v-if="!isInDataTypes">
{{ row.type }}
</option>
<optgroup <optgroup
v-for="group in dataTypes" v-for="group in dataTypes"
:key="group.group" :key="group.group"
@@ -68,7 +71,7 @@
<option <option
v-for="type in group.types" v-for="type in group.types"
:key="type.name" :key="type.name"
:selected="localRow.type.toUpperCase() === type.name" :selected="localRow.type === type.name"
:value="type.name" :value="type.name"
> >
{{ type.name }} {{ type.name }}
@@ -76,6 +79,16 @@
</optgroup> </optgroup>
</select> </select>
</div> </div>
<div
v-if="customizations.tableArray"
class="td"
tabindex="0"
>
<label class="form-checkbox">
<input v-model="localRow.isArray" type="checkbox">
<i class="form-icon" />
</label>
</div>
<div class="td type-int" tabindex="0"> <div class="td type-int" tabindex="0">
<template v-if="fieldType.length"> <template v-if="fieldType.length">
<span <span
@@ -83,8 +96,22 @@
class="cell-content" class="cell-content"
@dblclick="editON($event, localLength, 'length')" @dblclick="editON($event, localLength, 'length')"
> >
{{ localLength }} <span v-if="localRow.enumValues">
{{ localRow.enumValues }}
</span>
<span v-else>
{{ localLength }}
</span>
</span> </span>
<input
v-else-if="localRow.enumValues"
ref="editField"
v-model="editingContent"
type="text"
autofocus
class="editable-field px-2"
@blur="editOFF"
>
<input <input
v-else v-else
ref="editField" ref="editField"
@@ -96,7 +123,11 @@
> >
</template> </template>
</div> </div>
<div class="td" tabindex="0"> <div
v-if="customizations.unsigned"
class="td"
tabindex="0"
>
<label class="form-checkbox"> <label class="form-checkbox">
<input <input
v-model="localRow.unsigned" v-model="localRow.unsigned"
@@ -106,7 +137,11 @@
<i class="form-icon" /> <i class="form-icon" />
</label> </label>
</div> </div>
<div class="td" tabindex="0"> <div
v-if="customizations.nullable"
class="td"
tabindex="0"
>
<label class="form-checkbox"> <label class="form-checkbox">
<input <input
v-model="localRow.nullable" v-model="localRow.nullable"
@@ -116,7 +151,11 @@
<i class="form-icon" /> <i class="form-icon" />
</label> </label>
</div> </div>
<div class="td" tabindex="0"> <div
v-if="customizations.zerofill"
class="td"
tabindex="0"
>
<label class="form-checkbox"> <label class="form-checkbox">
<input <input
v-model="localRow.zerofill" v-model="localRow.zerofill"
@@ -131,7 +170,11 @@
{{ fieldDefault }} {{ fieldDefault }}
</span> </span>
</div> </div>
<div class="td type-varchar" tabindex="0"> <div
v-if="customizations.comment"
class="td type-varchar"
tabindex="0"
>
<span <span
v-if="!isInlineEditor.comment" v-if="!isInlineEditor.comment"
class="cell-content" class="cell-content"
@@ -149,7 +192,11 @@
@blur="editOFF" @blur="editOFF"
> >
</div> </div>
<div class="td" tabindex="0"> <div
v-if="customizations.collation"
class="td"
tabindex="0"
>
<template v-if="fieldType.collation"> <template v-if="fieldType.collation">
<span <span
v-if="!isInlineEditor.collation" v-if="!isInlineEditor.collation"
@@ -220,7 +267,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="mb-2"> <div v-if="customizations.nullable" class="mb-2">
<label class="form-radio form-inline"> <label class="form-radio form-inline">
<input <input
v-model="defaultValue.type" v-model="defaultValue.type"
@@ -230,7 +277,7 @@
><i class="form-icon" /> NULL ><i class="form-icon" /> NULL
</label> </label>
</div> </div>
<div class="mb-2"> <div v-if="customizations.autoIncrement" class="mb-2">
<label class="form-radio form-inline"> <label class="form-radio form-inline">
<input <input
v-model="defaultValue.type" v-model="defaultValue.type"
@@ -261,7 +308,7 @@
</div> </div>
</div> </div>
</div> </div>
<div> <div v-if="customizations.onUpdate">
<div class="form-group"> <div class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('message.onUpdate') }} {{ $t('message.onUpdate') }}
@@ -294,7 +341,8 @@ export default {
row: Object, row: Object,
dataTypes: Array, dataTypes: Array,
indexes: Array, indexes: Array,
foreigns: Array foreigns: Array,
customizations: Object
}, },
data () { data () {
return { return {
@@ -343,6 +391,18 @@ export default {
}, },
isNullable () { isNullable () {
return !this.indexes.some(index => ['PRIMARY'].includes(index.type)); return !this.indexes.some(index => ['PRIMARY'].includes(index.type));
},
isInDataTypes () {
let typeNames = [];
for (const group of this.dataTypes) {
const groupTypeNames = group.types.reduce((acc, curr) => {
acc.push(curr.name);
return acc;
}, []);
typeNames = [...groupTypeNames, ...typeNames];
}
return typeNames.includes(this.row.type);
} }
}, },
watch: { watch: {
@@ -378,10 +438,10 @@ export default {
return 'UNKNOWN ' + key; return 'UNKNOWN ' + key;
} }
}, },
lowerCase (val) { typeClass (type) {
if (val) if (type)
return val.toLowerCase(); return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return val; return '';
}, },
initLocalRow () { initLocalRow () {
Object.keys(this.localRow).forEach(key => { Object.keys(this.localRow).forEach(key => {
@@ -389,40 +449,35 @@ export default {
}); });
this.defaultValue.onUpdate = this.localRow.onUpdate; this.defaultValue.onUpdate = this.localRow.onUpdate;
this.defaultValue.type = this.localRow.defaultType;
if (this.localRow.autoIncrement) if (this.defaultValue.type === 'custom') {
this.defaultValue.type = 'autoincrement'; this.defaultValue.custom = this.localRow.default
else if (this.localRow.default === null) ? this.localRow.default.includes('\'')
this.defaultValue.type = 'noval'; ? this.localRow.default.split('\'')[1]
else if (this.localRow.default === 'NULL') : this.localRow.default
this.defaultValue.type = 'null'; : '';
else if (this.localRow.default.match(/^'.*'$/g)) {
this.defaultValue.type = 'custom';
this.defaultValue.custom = this.localRow.default.replace(/(^')|('$)/g, '');
} }
else if (!isNaN(this.localRow.default.replace(/[:.-\s]/g, ''))) { if (this.defaultValue.type === 'expression')
this.defaultValue.type = 'custom';
this.defaultValue.custom = this.localRow.default;
}
else {
this.defaultValue.type = 'expression';
this.defaultValue.expression = this.localRow.default; this.defaultValue.expression = this.localRow.default;
}
},
updateRow () {
this.$emit('input', this.localRow);
}, },
editON (event, content, field) { editON (event, content, field) {
if (field === 'length') { if (field === 'length') {
if (['integer', 'float', 'binary', 'spatial'].includes(this.fieldType.group)) this.editingField = 'numLength'; if (['integer', 'float', 'binary', 'spatial'].includes(this.fieldType.group)) this.editingField = 'numLength';
if (['string', 'other'].includes(this.fieldType.group)) this.editingField = 'charLength'; else if (['string', 'unknown'].includes(this.fieldType.group)) this.editingField = 'charLength';
if (['time'].includes(this.fieldType.group)) this.editingField = 'datePrecision'; else if (['other'].includes(this.fieldType.group)) this.editingField = 'enumValues';
else if (['time'].includes(this.fieldType.group)) this.editingField = 'datePrecision';
} }
else else
this.editingField = field; this.editingField = field;
this.editingContent = content; if (this.localRow.enumValues && field === 'length') {
this.originalContent = content; this.editingContent = this.localRow.enumValues;
this.originalContent = this.localRow.enumValues;
}
else {
this.editingContent = content;
this.originalContent = content;
}
const obj = { [field]: true }; const obj = { [field]: true };
this.isInlineEditor = { ...this.isInlineEditor, ...obj }; this.isInlineEditor = { ...this.isInlineEditor, ...obj };
@@ -438,17 +493,22 @@ export default {
} }
}, },
editOFF () { editOFF () {
if (this.editingField === 'name')
this.$emit('rename-field', { old: this.localRow[this.editingField], new: this.editingContent });
this.localRow[this.editingField] = this.editingContent; 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.charLength = null; this.localRow.charLength = null;
this.localRow.datePrecision = null; this.localRow.datePrecision = null;
this.localRow.enumValues = '';
if (this.fieldType.length) { if (this.fieldType.length) {
if (['integer', 'float', 'binary', 'spatial'].includes(this.fieldType.group)) this.localRow.numLength = 11; if (['integer', 'float', 'binary', 'spatial'].includes(this.fieldType.group)) this.localRow.numLength = 11;
if (['string', 'other'].includes(this.fieldType.group)) this.localRow.charLength = 15; if (['string'].includes(this.fieldType.group)) this.localRow.charLength = 15;
if (['time'].includes(this.fieldType.group)) this.localRow.datePrecision = 0; if (['time'].includes(this.fieldType.group)) this.localRow.datePrecision = 0;
if (['other'].includes(this.fieldType.group)) this.localRow.enumValues = '\'valA\',\'valB\'';
} }
if (!this.fieldType.collation) if (!this.fieldType.collation)
@@ -460,8 +520,7 @@ export default {
if (!this.fieldType.zerofill) if (!this.fieldType.zerofill)
this.localRow.zerofill = false; this.localRow.zerofill = false;
} }
else if (this.editingField === 'default') {
if (this.editingField === 'default') {
switch (this.defaultValue.type) { switch (this.defaultValue.type) {
case 'autoincrement': case 'autoincrement':
this.localRow.autoIncrement = true; this.localRow.autoIncrement = true;
@@ -476,7 +535,7 @@ export default {
break; break;
case 'custom': case 'custom':
this.localRow.autoIncrement = false; this.localRow.autoIncrement = false;
this.localRow.default = `'${this.defaultValue.custom}'`; this.localRow.default = Number.isNaN(+this.defaultValue.custom) ? `'${this.defaultValue.custom}'` : this.defaultValue.custom;
break; break;
case 'expression': case 'expression':
this.localRow.autoIncrement = false; this.localRow.autoIncrement = false;

View File

@@ -1,13 +1,21 @@
<template> <template>
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless"> <div
v-show="isSelected"
class="workspace-query-tab column col-12 columns col-gapless no-outline"
tabindex="0"
@keydown.116="runQuery(query)"
@keydown.ctrl.87="clear"
@keydown.ctrl.119="beautify"
>
<div class="workspace-query-runner column col-12"> <div class="workspace-query-runner column col-12">
<QueryEditor <QueryEditor
v-if="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
:auto-focus="true" :auto-focus="true"
:value.sync="query" :value.sync="query"
:workspace="workspace" :workspace="workspace"
:schema="schema" :schema="schema"
:is-selected="isSelected"
:height="editorHeight" :height="editorHeight"
/> />
<div ref="resizer" class="query-area-resizer" /> <div ref="resizer" class="query-area-resizer" />
@@ -17,14 +25,58 @@
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm"
:class="{'loading':isQuering}" :class="{'loading':isQuering}"
:disabled="!query" :disabled="!query"
title="F9" title="F5"
@click="runQuery(query)" @click="runQuery(query)"
> >
<span>{{ $t('word.run') }}</span> <span>{{ $t('word.run') }}</span>
<i class="mdi mdi-24px mdi-play" /> <i class="mdi mdi-24px mdi-play" />
</button> </button>
<div class="dropdown export-dropdown pr-2">
<button
:disabled="!results.length || isQuering"
class="btn btn-dark btn-sm dropdown-toggle mr-0 pr-0"
tabindex="0"
>
<span>{{ $t('word.export') }}</span>
<i class="mdi mdi-24px mdi-file-export ml-1" />
<i class="mdi mdi-24px mdi-menu-down" />
</button>
<ul class="menu text-left">
<li class="menu-item">
<a class="c-hand" @click="downloadTable('json')">JSON</a>
</li>
<li class="menu-item">
<a class="c-hand" @click="downloadTable('csv')">CSV</a>
</li>
</ul>
</div>
<button
class="btn btn-dark btn-sm"
:disabled="!query || isQuering"
title="CTRL+F8"
@click="beautify()"
>
<span>{{ $t('word.format') }}</span>
<i class="mdi mdi-24px mdi-brush pl-1" />
</button>
<button
class="btn btn-link btn-sm"
:disabled="!query || isQuering"
title="CTRL+W"
@click="clear()"
>
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep pl-1" />
</button>
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div
v-if="results.length"
class="d-flex"
:title="$t('message.queryDuration')"
>
<i class="mdi mdi-timer-sand mdi-rotate-180 pr-1" /> <b>{{ durationsCount / 1000 }}s</b>
</div>
<div v-if="resultsCount"> <div v-if="resultsCount">
{{ $t('word.results') }}: <b>{{ resultsCount.toLocaleString() }}</b> {{ $t('word.results') }}: <b>{{ resultsCount.toLocaleString() }}</b>
</div> </div>
@@ -41,14 +93,16 @@
</div> </div>
</div> </div>
</div> </div>
<div class="workspace-query-results column col-12"> <div class="workspace-query-results p-relative column col-12">
<BaseLoader v-if="isQuering" />
<WorkspaceQueryTable <WorkspaceQueryTable
v-if="results" v-if="results"
v-show="!isQuering" v-show="!isQuering"
ref="queryTable" ref="queryTable"
:results="results" :results="results"
:tab-uid="tabUid" :tab-uid="tab.uid"
:conn-uid="connection.uid" :conn-uid="connection.uid"
:is-selected="isSelected"
mode="query" mode="query"
@update-field="updateField" @update-field="updateField"
@delete-selected="deleteSelected" @delete-selected="deleteSelected"
@@ -58,8 +112,10 @@
</template> </template>
<script> <script>
import Database from '@/ipc-api/Database'; import { format } from 'sql-formatter';
import Schema from '@/ipc-api/Schema';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable'; import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import tableTabs from '@/mixins/tableTabs'; import tableTabs from '@/mixins/tableTabs';
@@ -67,13 +123,14 @@ import tableTabs from '@/mixins/tableTabs';
export default { export default {
name: 'WorkspaceQueryTab', name: 'WorkspaceQueryTab',
components: { components: {
BaseLoader,
QueryEditor, QueryEditor,
WorkspaceQueryTable WorkspaceQueryTable
}, },
mixins: [tableTabs], mixins: [tableTabs],
props: { props: {
connection: Object, connection: Object,
tabUid: String, tab: Object,
isSelected: Boolean isSelected: Boolean
}, },
data () { data () {
@@ -83,19 +140,26 @@ export default {
isQuering: false, isQuering: false,
results: [], results: [],
resultsCount: 0, resultsCount: 0,
durationsCount: 0,
affectedCount: 0, affectedCount: 0,
editorHeight: 200 editorHeight: 200
}; };
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
getWorkspace: 'workspaces/getWorkspace' getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected'
}), }),
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
},
isWorkspaceSelected () {
return this.workspace.uid === this.selectedWorkspace;
} }
}, },
created () { created () {
this.query = this.tab.content;
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
}, },
mounted () { mounted () {
@@ -107,6 +171,9 @@ export default {
window.addEventListener('mousemove', this.resize); window.addEventListener('mousemove', this.resize);
window.addEventListener('mouseup', this.stopResize); window.addEventListener('mouseup', this.stopResize);
}); });
if (this.tab.autorun)
this.runQuery(this.query);
}, },
beforeDestroy () { beforeDestroy () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
@@ -128,11 +195,12 @@ export default {
query query
}; };
const { status, response } = await Database.rawQuery(params); const { status, response } = await Schema.rawQuery(params);
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.affectedCount += this.results.reduce((acc, curr) => acc + (curr.report ? curr.report.affectedRows : 0), 0); this.affectedCount += this.results.reduce((acc, curr) => acc + (curr.report ? curr.report.affectedRows : 0), 0);
} }
else else
@@ -151,6 +219,7 @@ export default {
clearTabData () { clearTabData () {
this.results = []; this.results = [];
this.resultsCount = 0; this.resultsCount = 0;
this.durationsCount = 0;
this.affectedCount = 0; this.affectedCount = 0;
}, },
resize (e) { resize (e) {
@@ -168,12 +237,36 @@ export default {
if (this.$refs.queryEditor) if (this.$refs.queryEditor)
this.$refs.queryEditor.editor.resize(); this.$refs.queryEditor.editor.resize();
}, },
onKey (e) { beautify () {
if (this.isSelected) { if (this.$refs.queryEditor) {
e.stopPropagation(); let language = 'sql';
if (e.key === 'F9')
this.runQuery(this.query); switch (this.workspace.client) {
case 'mysql':
language = 'mysql';
break;
case 'maria':
language = 'mariadb';
break;
case 'pg':
language = 'postgresql';
break;
}
const formattedQuery = format(this.query, {
language,
uppercase: true
});
this.$refs.queryEditor.editor.session.setValue(formattedQuery);
} }
},
clear () {
if (this.$refs.queryEditor)
this.$refs.queryEditor.editor.session.setValue('');
this.clearTabData();
},
downloadTable (format) {
this.$refs.queryTable.downloadTable(format, `${this.tab.type}-${this.tab.index}`);
} }
} }
}; };
@@ -208,7 +301,6 @@ export default {
.btn { .btn {
display: flex; display: flex;
align-self: center; align-self: center;
color: $body-font-color;
margin-right: 0.4rem; margin-right: 0.4rem;
} }
} }
@@ -222,6 +314,15 @@ export default {
} }
} }
} }
.workspace-query-results {
min-height: 200px;
}
} }
.export-dropdown {
.menu {
min-width: 100%;
}
}
</style> </style>

View File

@@ -1,15 +1,18 @@
<template> <template>
<div <div
ref="tableWrapper" ref="tableWrapper"
class="vscroll" class="vscroll no-outline"
tabindex="0"
:style="{'height': resultsSize+'px'}" :style="{'height': resultsSize+'px'}"
@keyup.46="showDeleteConfirmModal"
> >
<TableContext <TableContext
v-if="isContext" v-if="isContext"
:context-event="contextEvent" :context-event="contextEvent"
:selected-rows="selectedRows" :selected-rows="selectedRows"
@delete-selected="deleteSelected" @show-delete-modal="showDeleteConfirmModal"
@close-context="isContext = false" @set-null="setNull"
@close-context="closeContext"
/> />
<ul v-if="resultsWithRows.length > 1" class="tab tab-block result-tabs"> <ul v-if="resultsWithRows.length > 1" class="tab tab-block result-tabs">
<li <li
@@ -41,7 +44,7 @@
/> />
<span>{{ field.alias || field.name }}</span> <span>{{ field.alias || field.name }}</span>
<i <i
v-if="currentSort === field.name || currentSort === `${field.table}.${field.name}`" v-if="isSortable && currentSort === field.name || currentSort === `${field.table}.${field.name}`"
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'"
/> />
@@ -64,9 +67,8 @@
v-for="row in items" v-for="row in items"
:key="row._id" :key="row._id"
:row="row" :row="row"
:fields="fields" :fields="fieldsObj"
:key-usage="keyUsage" :key-usage="keyUsage"
class="tr"
:class="{'selected': selectedRows.includes(row._id)}" :class="{'selected': selectedRows.includes(row._id)}"
@select-row="selectRow($event, row._id)" @select-row="selectRow($event, row._id)"
@update-field="updateField($event, row)" @update-field="updateField($event, row)"
@@ -75,6 +77,23 @@
</template> </template>
</BaseVirtualScroll> </BaseVirtualScroll>
</div> </div>
<ConfirmModal
v-if="isDeleteConfirmModal"
@confirm="deleteSelected"
@hide="hideDeleteConfirmModal"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-delete mr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}
</div>
</template>
<div :slot="'body'">
<div class="mb-2">
{{ $tc('message.confirmToDeleteRows', selectedRows.length) }}
</div>
</div>
</ConfirmModal>
</div> </div>
</template> </template>
@@ -85,26 +104,30 @@ import { TEXT, LONG_TEXT, BLOB } from 'common/fieldTypes';
import BaseVirtualScroll from '@/components/BaseVirtualScroll'; import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import WorkspaceQueryTableRow from '@/components/WorkspaceQueryTableRow'; import WorkspaceQueryTableRow from '@/components/WorkspaceQueryTableRow';
import TableContext from '@/components/WorkspaceQueryTableContext'; import TableContext from '@/components/WorkspaceQueryTableContext';
import ConfirmModal from '@/components/BaseConfirmModal';
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import moment from 'moment';
export default { export default {
name: 'WorkspaceQueryTable', name: 'WorkspaceQueryTable',
components: { components: {
BaseVirtualScroll, BaseVirtualScroll,
WorkspaceQueryTableRow, WorkspaceQueryTableRow,
TableContext TableContext,
ConfirmModal
}, },
props: { props: {
results: Array, results: Array,
tabUid: [String, Number],
connUid: String, connUid: String,
mode: String mode: String,
isSelected: Boolean
}, },
data () { data () {
return { return {
resultsSize: 1000, resultsSize: 1000,
localResults: [], localResults: [],
isContext: false, isContext: false,
isDeleteConfirmModal: false,
contextEvent: null, contextEvent: null,
selectedCell: null, selectedCell: null,
selectedRows: [], selectedRows: [],
@@ -122,7 +145,16 @@ export default {
return this.getWorkspace(this.connUid).breadcrumbs.schema; return this.getWorkspace(this.connUid).breadcrumbs.schema;
}, },
primaryField () { primaryField () {
return this.fields.filter(field => ['pri', 'uni'].includes(field.key))[0] || false; const primaryFields = this.fields.filter(field => field.key === 'pri');
const uniqueFields = this.fields.filter(field => field.key === 'uni');
if ((primaryFields.length > 1 || !primaryFields.length) && (uniqueFields.length > 1 || !uniqueFields.length))
return false;
return primaryFields[0] || uniqueFields[0];
},
isSortable () {
return this.fields.every(field => field.name);
}, },
isHardSort () { isHardSort () {
return this.mode === 'table' && this.localResults.length === 1000; return this.mode === 'table' && this.localResults.length === 1000;
@@ -150,6 +182,37 @@ export default {
}, },
keyUsage () { keyUsage () {
return this.resultsWithRows.length ? this.resultsWithRows[this.resultsetIndex].keys : []; return this.resultsWithRows.length ? this.resultsWithRows[this.resultsetIndex].keys : [];
},
fieldsObj () {
if (this.sortedResults.length) {
const fieldsObj = {};
for (const key in this.sortedResults[0]) {
if (key === '_id') continue;
const fieldObj = this.fields.find(field => {
let fieldNames = [
field.name,
field.alias,
`${field.table}.${field.name}`,
`${field.table}.${field.alias}`,
`${field.tableAlias}.${field.name}`,
`${field.tableAlias}.${field.alias}`
];
if (field.table)
fieldNames = [...fieldNames, `${field.table.toLowerCase()}.${field.name}`, `${field.table.toLowerCase()}.${field.alias}`];
if (field.tableAlias)
fieldNames = [...fieldNames, `${field.tableAlias.toLowerCase()}.${field.name}`, `${field.tableAlias.toLowerCase()}.${field.alias}`];
return fieldNames.includes(key);
});
fieldsObj[key] = fieldObj;
}
return fieldsObj;
}
return {};
} }
}, },
watch: { watch: {
@@ -240,7 +303,7 @@ export default {
: []; : [];
}, },
resizeResults () { resizeResults () {
if (this.$refs.resultTable) { if (this.$refs.resultTable && this.isSelected) {
const el = this.$refs.tableWrapper; const el = this.$refs.tableWrapper;
if (el) { if (el) {
@@ -255,19 +318,41 @@ export default {
this.resizeResults(); this.resizeResults();
}, },
updateField (payload, row) { updateField (payload, row) {
delete row._id; const orgRow = this.localResults.find(lr => lr._id === row._id);
Object.keys(orgRow).forEach(key => { // remap the row
if (orgRow[key] instanceof Date && moment(orgRow[key]).isValid()) { // if datetime
let datePrecision = '';
const precision = this.fields.find(field => field.name === key).datePrecision;
for (let i = 0; i < precision; i++)
datePrecision += i === 0 ? '.S' : 'S';
orgRow[key] = moment(orgRow[key]).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`);
}
});
const params = { const params = {
primary: this.primaryField.name, primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex), schema: this.getSchema(this.resultsetIndex),
table: this.getTable(this.resultsetIndex), table: this.getTable(this.resultsetIndex),
id: this.getPrimaryValue(row), id: this.getPrimaryValue(orgRow),
row, row,
orgRow,
...payload ...payload
}; };
this.$emit('update-field', params); this.$emit('update-field', params);
}, },
closeContext () {
this.isContext = false;
},
showDeleteConfirmModal () {
this.isDeleteConfirmModal = true;
},
hideDeleteConfirmModal () {
this.isDeleteConfirmModal = false;
},
deleteSelected () { deleteSelected () {
this.closeContext();
const rows = this.localResults.filter(row => this.selectedRows.includes(row._id)).map(row => { const rows = this.localResults.filter(row => this.selectedRows.includes(row._id)).map(row => {
delete row._id; delete row._id;
return row; return row;
@@ -281,6 +366,21 @@ export default {
}; };
this.$emit('delete-selected', params); this.$emit('delete-selected', params);
}, },
setNull () {
const row = this.localResults.find(row => this.selectedRows.includes(row._id));
const params = {
primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex),
table: this.getTable(this.resultsetIndex),
id: this.getPrimaryValue(row),
row,
orgRow: row,
field: this.selectedCell.field,
content: null
};
this.$emit('update-field', params);
},
applyUpdate (params) { applyUpdate (params) {
const { primary, id, field, table, content } = params; const { primary, id, field, table, content } = params;
@@ -305,15 +405,15 @@ 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.localResults.findIndex(el => el._id === lastID); const lastIndex = this.sortedResults.findIndex(el => el._id === lastID);
const clickedIndex = this.localResults.findIndex(el => el._id === row); const clickedIndex = this.sortedResults.findIndex(el => el._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.localResults[i]._id); this.selectedRows.push(this.sortedResults[i]._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.localResults[i]._id); this.selectedRows.push(this.sortedResults[i]._id);
} }
} }
} }
@@ -321,6 +421,8 @@ export default {
this.selectedRows = [row]; this.selectedRows = [row];
}, },
contextMenu (event, cell) { contextMenu (event, cell) {
if (event.target.localName === 'input') return;
this.selectedCell = cell; this.selectedCell = cell;
if (!this.selectedRows.includes(cell.id)) if (!this.selectedRows.includes(cell.id))
this.selectedRows = [cell.id]; this.selectedRows = [cell.id];
@@ -328,6 +430,10 @@ export default {
this.isContext = true; this.isContext = true;
}, },
sort (field) { sort (field) {
if (!this.isSortable) return;
this.selectedRows = [];
if (this.mode === 'query') if (this.mode === 'query')
field = `${this.getTable(this.resultsetIndex)}.${field}`; field = `${this.getTable(this.resultsetIndex)}.${field}`;

View File

@@ -3,62 +3,46 @@
:context-event="contextEvent" :context-event="contextEvent"
@close-context="closeContext" @close-context="closeContext"
> >
<div class="context-element" @click="showConfirmModal"> <div
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}</span> v-if="selectedRows.length === 1"
</div> class="context-element"
@click="setNull"
<ConfirmModal
v-if="isConfirmModal"
@confirm="deleteRows"
@hide="hideConfirmModal"
> >
<template :slot="'header'"> <span class="d-flex">
<div class="d-flex"> <i class="mdi mdi-18px mdi-null text-light pr-1" /> {{ $t('message.setNull') }}
<i class="mdi mdi-24px mdi-delete mr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }} </span>
</div> </div>
</template> <div class="context-element" @click="showConfirmModal">
<div :slot="'body'"> <span class="d-flex">
<div class="mb-2"> <i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}
{{ $tc('message.confirmToDeleteRows', selectedRows.length) }} </span>
</div> </div>
</div>
</ConfirmModal>
</BaseContextMenu> </BaseContextMenu>
</template> </template>
<script> <script>
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal';
export default { export default {
name: 'WorkspaceQueryTableContext', name: 'WorkspaceQueryTableContext',
components: { components: {
BaseContextMenu, BaseContextMenu
ConfirmModal
}, },
props: { props: {
contextEvent: MouseEvent, contextEvent: MouseEvent,
selectedRows: Array selectedRows: Array
}, },
data () {
return {
isConfirmModal: false
};
},
computed: { computed: {
}, },
methods: { methods: {
showConfirmModal () { showConfirmModal () {
this.isConfirmModal = true; this.$emit('show-delete-modal');
},
hideConfirmModal () {
this.isConfirmModal = false;
}, },
closeContext () { closeContext () {
this.$emit('close-context'); this.$emit('close-context');
}, },
deleteRows () { setNull () {
this.$emit('delete-selected'); this.$emit('set-null');
this.closeContext(); this.closeContext();
} }
} }

View File

@@ -6,15 +6,15 @@
:key="cKey" :key="cKey"
class="td p-0" class="td p-0"
tabindex="0" tabindex="0"
@contextmenu.prevent="$emit('contextmenu', $event, {id: row._id, field: cKey})" @contextmenu.prevent="openContext($event, { id: row._id, field: cKey })"
> >
<template v-if="cKey !== '_id'"> <template v-if="cKey !== '_id'">
<span <span
v-if="!isInlineEditor[cKey]" v-if="!isInlineEditor[cKey]"
class="cell-content px-2" class="cell-content px-2"
:class="`${isNull(col)} type-${getFieldType(cKey)}`" :class="`${isNull(col)} ${typeClass(fields[cKey].type)}`"
@dblclick="editON($event, col, cKey)" @dblclick="editON($event, col, cKey)"
>{{ col | typeFormat(getFieldType(cKey), getFieldPrecision(cKey)) | cutText }}</span> >{{ col | typeFormat(fields[cKey].type.toLowerCase(), fields[cKey].length) | cutText }}</span>
<ForeignKeySelect <ForeignKeySelect
v-else-if="isForeignKey(cKey)" v-else-if="isForeignKey(cKey)"
class="editable-field" class="editable-field"
@@ -34,6 +34,25 @@
class="editable-field px-2" class="editable-field px-2"
@blur="editOFF" @blur="editOFF"
> >
<select
v-else-if="inputProps.type === 'boolean'"
v-model="editingContent"
class="form-select small-select editable-field"
@blur="editOFF"
>
<option>true</option>
<option>false</option>
</select>
<select
v-else-if="enumArray"
v-model="editingContent"
class="form-select small-select editable-field"
@blur="editOFF"
>
<option v-for="value in enumArray" :key="value">
{{ value }}
</option>
</select>
<input <input
v-else v-else
ref="editField" ref="editField"
@@ -61,14 +80,52 @@
<div :slot="'body'"> <div :slot="'body'">
<div class="mb-2"> <div class="mb-2">
<div> <div>
<textarea <TextEditor
v-model="editingContent" :value.sync="editingContent"
class="form-input textarea-editor" editor-class="textarea-editor"
:mode="editorMode"
/> />
</div> </div>
<div class="editor-field-info"> <div class="editor-field-info p-vcentered">
<div><b>{{ $t('word.size') }}</b>: {{ editingContent ? editingContent.length : 0 }}</div> <div class="d-flex p-vcentered">
<div><b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}</div> <label for="editorMode" class="form-label mr-2">
<b>{{ $t('word.content') }}</b>:
</label>
<select
id="editorMode"
v-model="editorMode"
class="form-select select-sm"
>
<option value="text">
TEXT
</option>
<option value="html">
HTML
</option>
<option value="xml">
XML
</option>
<option value="json">
JSON
</option>
<option value="svg">
SVG
</option>
<option value="yaml">
YAML
</option>
</select>
</div>
<div class="d-flex">
<div class="p-vcentered">
<div class="mr-4">
<b>{{ $t('word.size') }}</b>: {{ editingContent ? editingContent.length : 0 }}
</div>
<div>
<b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -135,19 +192,21 @@ 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, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BOOLEAN, DATE, TIME, DATETIME, BLOB, BIT, HAS_TIMEZONE } from 'common/fieldTypes';
import { mask } from 'vue-the-mask'; import { VueMaskDirective } from 'v-mask';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
import ForeignKeySelect from '@/components/ForeignKeySelect'; import ForeignKeySelect from '@/components/ForeignKeySelect';
export default { export default {
name: 'WorkspaceQueryTableRow', name: 'WorkspaceQueryTableRow',
components: { components: {
ConfirmModal, ConfirmModal,
TextEditor,
ForeignKeySelect ForeignKeySelect
}, },
directives: { directives: {
mask mask: VueMaskDirective
}, },
filters: { filters: {
formatBytes, formatBytes,
@@ -164,6 +223,9 @@ export default {
return moment(val).isValid() ? moment(val).format('YYYY-MM-DD') : val; return moment(val).isValid() ? moment(val).format('YYYY-MM-DD') : val;
if (DATETIME.includes(type)) { if (DATETIME.includes(type)) {
if (typeof val === 'string')
return val;
let datePrecision = ''; let datePrecision = '';
for (let i = 0; i < precision; i++) for (let i = 0; i < precision; i++)
datePrecision += i === 0 ? '.S' : 'S'; datePrecision += i === 0 ? '.S' : 'S';
@@ -185,15 +247,18 @@ export default {
return hexToBinary(hex); return hexToBinary(hex);
} }
if (ARRAY.includes(type)) {
if (Array.isArray(val))
return JSON.stringify(val).replaceAll('[', '{').replaceAll(']', '}');
return val;
}
return val; return val;
} }
}, },
props: { props: {
row: Object, row: Object,
fields: { fields: Object,
type: Array,
default: () => []
},
keyUsage: Array keyUsage: Array
}, },
data () { data () {
@@ -206,6 +271,8 @@ export default {
editingContent: null, editingContent: null,
editingType: null, editingType: null,
editingField: null, editingField: null,
editingLength: null,
editorMode: 'text',
contentInfo: { contentInfo: {
ext: '', ext: '',
mime: '', mime: '',
@@ -224,11 +291,14 @@ export default {
if (TIME.includes(this.editingType)) { if (TIME.includes(this.editingType)) {
let timeMask = '##:##:##'; let timeMask = '##:##:##';
const precision = this.getFieldPrecision(this.editingField); const precision = this.fields[this.editingField].length;
for (let i = 0; i < precision; i++) for (let i = 0; i < precision; i++)
timeMask += i === 0 ? '.#' : '#'; timeMask += i === 0 ? '.#' : '#';
if (HAS_TIMEZONE.includes(this.editingType))
timeMask += 'X##';
return { type: 'text', mask: timeMask }; return { type: 'text', mask: timeMask };
} }
@@ -237,19 +307,22 @@ export default {
if (DATETIME.includes(this.editingType)) { if (DATETIME.includes(this.editingType)) {
let datetimeMask = '####-##-## ##:##:##'; let datetimeMask = '####-##-## ##:##:##';
const precision = this.getFieldPrecision(this.editingField); const precision = this.fields[this.editingField].length;
for (let i = 0; i < precision; i++) for (let i = 0; i < precision; i++)
datetimeMask += i === 0 ? '.#' : '#'; datetimeMask += i === 0 ? '.#' : '#';
if (HAS_TIMEZONE.includes(this.editingType))
datetimeMask += 'X##';
return { type: 'text', mask: datetimeMask }; return { type: 'text', mask: datetimeMask };
} }
if (BLOB.includes(this.editingType)) if (BLOB.includes(this.editingType))
return { type: 'file', mask: false }; return { type: 'file', mask: false };
if (BIT.includes(this.editingType)) if (BOOLEAN.includes(this.editingType))
return { type: 'text', mask: false }; return { type: 'boolean', mask: false };
return { type: 'text', mask: false }; return { type: 'text', mask: false };
}, },
@@ -261,23 +334,28 @@ export default {
}, },
isEditable () { isEditable () {
if (this.fields) { if (this.fields) {
const nElements = this.fields.reduce((acc, curr) => { const nElements = Object.keys(this.fields).reduce((acc, curr) => {
acc.add(curr.table); acc.add(this.fields[curr].table);
acc.add(curr.schema); acc.add(this.fields[curr].schema);
return acc; return acc;
}, new Set([])); }, new Set([]));
if (nElements.size > 2) return false; if (nElements.size > 2) return false;
return !!(this.fields[0].schema && this.fields[0].table); return !!(this.fields[Object.keys(this.fields)[0]].schema && this.fields[Object.keys(this.fields)[0]].table);
} }
return false; return false;
},
enumArray () {
if (this.fields[this.editingField].enumValues)
return this.fields[this.editingField].enumValues.replaceAll('\'', '').split(',');
return false;
} }
}, },
watch: { watch: {
fields () { fields () {
this.fields.forEach(field => { Object.keys(this.fields).forEach(field => {
this.isInlineEditor[field.name] = false; this.isInlineEditor[field.name] = false;
}); });
} }
@@ -289,69 +367,41 @@ export default {
return this.foreignKeys.includes(key); return this.foreignKeys.includes(key);
}, },
getFieldType (cKey) {
let type = 'unknown';
const field = this.getFieldObj(cKey);
if (field)
type = field.type;
return type.toLowerCase();
},
getFieldPrecision (cKey) {
let length = 0;
const field = this.getFieldObj(cKey);
if (field)
length = field.datePrecision;
return length;
},
getFieldObj (cKey) {
return this.fields.filter(field => {
let fieldNames = [
field.name,
field.alias,
`${field.table}.${field.name}`,
`${field.table}.${field.alias}`,
`${field.tableAlias}.${field.name}`,
`${field.tableAlias}.${field.alias}`
];
if (field.table)
fieldNames = [...fieldNames, `${field.table.toLowerCase()}.${field.name}`, `${field.table.toLowerCase()}.${field.alias}`];
if (field.tableAlias)
fieldNames = [...fieldNames, `${field.tableAlias.toLowerCase()}.${field.name}`, `${field.tableAlias.toLowerCase()}.${field.alias}`];
return fieldNames.includes(cKey);
})[0];
},
isNull (value) { isNull (value) {
return value === null ? ' is-null' : ''; return value === null ? ' is-null' : '';
}, },
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
bufferToBase64 (val) { bufferToBase64 (val) {
return bufferToBase64(val); return bufferToBase64(val);
}, },
editON (event, content, field) { editON (event, content, field) {
if (!this.isEditable) return; if (!this.isEditable) return;
const type = this.getFieldType(field).toUpperCase(); ; window.addEventListener('keydown', this.onKey);
this.originalContent = content;
const type = this.fields[field].type.toUpperCase(); ;
this.originalContent = this.$options.filters.typeFormat(content, type, this.fields[field].length);
this.editingType = type; this.editingType = type;
this.editingField = field; this.editingField = field;
this.editingLength = this.fields[field].length;
if (LONG_TEXT.includes(type)) { if ([...LONG_TEXT, ...ARRAY, ...TEXT_SEARCH].includes(type)) {
this.isTextareaEditor = true; this.isTextareaEditor = true;
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type); this.editingContent = this.$options.filters.typeFormat(content, type);
return; return;
} }
if (BLOB.includes(type)) { if (BLOB.includes(type)) {
this.isBlobEditor = true; this.isBlobEditor = true;
this.editingContent = this.originalContent || ''; this.editingContent = content || '';
this.fileToUpload = null; this.fileToUpload = null;
this.willBeDeleted = false; this.willBeDeleted = false;
if (this.originalContent !== null) { if (content !== null) {
const buff = Buffer.from(this.editingContent); const buff = Buffer.from(this.editingContent);
if (buff.length) { if (buff.length) {
const hex = buff.toString('hex').substring(0, 8).toUpperCase(); const hex = buff.toString('hex').substring(0, 8).toUpperCase();
@@ -367,7 +417,7 @@ export default {
} }
// Inline editable fields // Inline editable fields
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type, this.getFieldPrecision(field)); this.editingContent = this.originalContent;
this.$nextTick(() => { // Focus on input this.$nextTick(() => { // Focus on input
event.target.blur(); event.target.blur();
@@ -378,10 +428,18 @@ export default {
this.isInlineEditor = { ...this.isInlineEditor, ...obj }; this.isInlineEditor = { ...this.isInlineEditor, ...obj };
}, },
editOFF () { editOFF () {
if (!this.editingField) return;
this.isInlineEditor[this.editingField] = false; this.isInlineEditor[this.editingField] = false;
let content; let content;
if (!BLOB.includes(this.editingType)) { if (!BLOB.includes(this.editingType)) {
if (this.editingContent === this.$options.filters.typeFormat(this.originalContent, this.editingType)) return;// If not changed if ([...DATETIME, ...TIME].includes(this.editingType)) {
if (this.editingContent.substring(this.editingContent.length - 1) === '.')
this.editingContent = this.editingContent.slice(0, -1);
}
if (this.editingContent === this.$options.filters.typeFormat(this.originalContent, this.editingType, this.editingLength)) return;// If not changed
content = this.editingContent; content = this.editingContent;
} }
else { // Handle file upload else { // Handle file upload
@@ -396,13 +454,14 @@ export default {
} }
this.$emit('update-field', { this.$emit('update-field', {
field: this.getFieldObj(this.editingField).name, field: this.fields[this.editingField].name,
type: this.editingType, type: this.editingType,
content content
}); });
this.editingType = null; this.editingType = null;
this.editingField = null; this.editingField = null;
window.removeEventListener('keydown', this.onKey);
}, },
hideEditorModal () { hideEditorModal () {
this.isTextareaEditor = false; this.isTextareaEditor = false;
@@ -434,9 +493,6 @@ export default {
}; };
this.willBeDeleted = true; this.willBeDeleted = true;
}, },
contextMenu (event, cell) {
this.$emit('update-field', event, cell);
},
selectRow (event, row) { selectRow (event, row) {
this.$emit('select-row', event, row); this.$emit('select-row', event, row);
}, },
@@ -444,6 +500,20 @@ export default {
if (keyName.includes('.')) if (keyName.includes('.'))
return this.keyUsage.find(key => key.field === keyName.split('.').pop()); return this.keyUsage.find(key => key.field === keyName.split('.').pop());
return this.keyUsage.find(key => key.field === keyName); return this.keyUsage.find(key => key.field === keyName);
},
openContext (event, payload) {
if (this.isEditable) {
payload.field = this.fields[payload.field].name;// Ensures field name only
this.$emit('contextmenu', event, payload);
}
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape') {
this.isInlineEditor[this.editingField] = false;
this.editingField = null;
window.removeEventListener('keydown', this.onKey);
}
} }
} }
}; };
@@ -473,7 +543,7 @@ export default {
} }
.editor-field-info { .editor-field-info {
margin-top: 0.6rem; margin-top: 0.4rem;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
white-space: normal; white-space: normal;

View File

@@ -36,6 +36,7 @@
<button <button
v-if="isTable" v-if="isTable"
class="btn btn-dark btn-sm" class="btn btn-dark btn-sm"
:disabled="isQuering"
@click="showFakerModal" @click="showFakerModal"
> >
<span>{{ $t('message.tableFiller') }}</span> <span>{{ $t('message.tableFiller') }}</span>
@@ -63,10 +64,17 @@
</div> </div>
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div
v-if="results.length"
class="d-flex"
:title="$t('message.queryDuration')"
>
<i class="mdi mdi-timer-sand mdi-rotate-180 pr-1" /> <b>{{ results[0].duration / 1000 }}s</b>
</div>
<div v-if="results.length && results[0].rows"> <div v-if="results.length && results[0].rows">
{{ $t('word.results') }}: <b>{{ results[0].rows.length.toLocaleString() }}</b> {{ $t('word.results') }}: <b>{{ results[0].rows.length.toLocaleString() }}</b>
</div> </div>
<div v-if="results.length && results[0].rows && tableInfo && results[0].rows.length < tableInfo.rows"> <div v-if="hasApproximately">
{{ $t('word.total') }}: <b>{{ tableInfo.rows.toLocaleString() }}</b> <small>({{ $t('word.approximately') }})</small> {{ $t('word.total') }}: <b>{{ tableInfo.rows.toLocaleString() }}</b> <small>({{ $t('word.approximately') }})</small>
</div> </div>
<div v-if="workspace.breadcrumbs.database"> <div v-if="workspace.breadcrumbs.database">
@@ -75,13 +83,15 @@
</div> </div>
</div> </div>
</div> </div>
<div class="workspace-query-results column col-12"> <div class="workspace-query-results p-relative column col-12">
<BaseLoader v-if="isQuering" />
<WorkspaceQueryTable <WorkspaceQueryTable
v-if="results" v-if="results"
ref="queryTable" ref="queryTable"
:results="results" :results="results"
:tab-uid="tabUid" :tab-uid="tabUid"
:conn-uid="connection.uid" :conn-uid="connection.uid"
:is-selected="isSelected"
mode="table" mode="table"
@update-field="updateField" @update-field="updateField"
@delete-selected="deleteSelected" @delete-selected="deleteSelected"
@@ -109,6 +119,7 @@
<script> <script>
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import BaseLoader from '@/components/BaseLoader';
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable'; import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
import ModalNewTableRow from '@/components/ModalNewTableRow'; import ModalNewTableRow from '@/components/ModalNewTableRow';
import ModalFakerRows from '@/components/ModalFakerRows'; import ModalFakerRows from '@/components/ModalFakerRows';
@@ -118,6 +129,7 @@ import tableTabs from '@/mixins/tableTabs';
export default { export default {
name: 'WorkspaceTableTab', name: 'WorkspaceTableTab',
components: { components: {
BaseLoader,
WorkspaceQueryTable, WorkspaceQueryTable,
ModalNewTableRow, ModalNewTableRow,
ModalFakerRows ModalFakerRows
@@ -142,13 +154,14 @@ export default {
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
getWorkspace: 'workspaces/getWorkspace' getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected'
}), }),
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
}, },
isSelected () { isSelected () {
return this.workspace.selected_tab === 'data'; return this.workspace.selected_tab === 'data' && this.workspace.uid === this.selectedWorkspace;
}, },
isTable () { isTable () {
return !!this.workspace.breadcrumbs.table; return !!this.workspace.breadcrumbs.table;
@@ -166,6 +179,13 @@ export default {
catch (err) { catch (err) {
return { rows: 0 }; return { rows: 0 };
} }
},
hasApproximately () {
return this.results.length &&
this.results[0].rows &&
this.tableInfo &&
this.results[0].rows.length === 1000 &&
this.results[0].rows.length < this.tableInfo.rows;
} }
}, },
watch: { watch: {
@@ -242,6 +262,7 @@ export default {
this.isAddModal = false; this.isAddModal = false;
}, },
showFakerModal () { showFakerModal () {
if (this.isQuering) return;
this.isFakerModal = true; this.isFakerModal = true;
}, },
hideFakerModal () { hideFakerModal () {
@@ -275,10 +296,6 @@ export default {
.export-dropdown { .export-dropdown {
.menu { .menu {
min-width: 100%; min-width: 100%;
.menu-item a:hover {
background: $bg-color-gray;
}
} }
} }
</style> </style>

View File

@@ -93,7 +93,19 @@ module.exports = {
ciphers: 'Ciphers', ciphers: 'Ciphers',
upload: 'Upload', upload: 'Upload',
browse: 'Browse', browse: 'Browse',
faker: 'Faker' faker: 'Faker',
content: 'Content',
cut: 'Cut',
copy: 'Copy',
paste: 'Paste',
tools: 'Tools',
variables: 'Variables',
processes: 'Processes',
database: 'Database',
scratchpad: 'Scratchpad',
array: 'Array',
changelog: 'Changelog',
format: 'Format'
}, },
message: { message: {
appWelcome: 'Welcome to Antares SQL Client!', appWelcome: 'Welcome to Antares SQL Client!',
@@ -184,7 +196,21 @@ module.exports = {
enableSsl: 'Enable SSL', enableSsl: 'Enable SSL',
manualValue: 'Manual value', manualValue: 'Manual value',
tableFiller: 'Table Filler', tableFiller: 'Table Filler',
fakeDataLanguage: 'Fake data language' fakeDataLanguage: 'Fake data language',
searchForElements: 'Search for elements',
selectAll: 'Select all',
queryDuration: 'Query duration',
includeBetaUpdates: 'Include beta updates',
setNull: 'Set NULL',
processesList: 'Processes list',
processInfo: 'Process info',
manageUsers: 'Manage users',
createNewSchema: 'Create new schema',
schemaName: 'Schema name',
editSchema: 'Edit schema',
deleteSchema: 'Delete schema',
markdownSupported: 'Markdown supported',
plantATree: 'Plant a Tree'
}, },
faker: { faker: {
address: 'Address', address: 'Address',

View File

@@ -9,7 +9,8 @@ const i18n = new VueI18n({
'it-IT': require('./it-IT'), 'it-IT': require('./it-IT'),
'ar-SA': require('./ar-SA'), 'ar-SA': require('./ar-SA'),
'es-ES': require('./es-ES'), 'es-ES': require('./es-ES'),
'fr-FR': require('./fr-FR') 'fr-FR': require('./fr-FR'),
'pt-BR': require('./pt-BR')
} }
}); });
export default i18n; export default i18n;

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