Compare commits
272 Commits
Author | SHA1 | Date | |
---|---|---|---|
88cd097ec0 | |||
f12e6a96dd | |||
|
6fcae957e1 | ||
|
1cd2d8abf3 | ||
ed3c5fe559 | |||
|
9601c59392 | ||
|
14d20a30c1 | ||
e9079adb25 | |||
8f3efabb69 | |||
db628f7722 | |||
e4bd747381 | |||
88eb113e53 | |||
9109b2c328 | |||
dd070d008d | |||
ee415da127 | |||
f0d368e3e3 | |||
4be55f3fe9 | |||
fd00ea42ee | |||
50b4411e9a | |||
|
cd0a5dc034 | ||
|
9fd427c4ff | ||
f3759b6541 | |||
191a354020 | |||
|
7dc314eecf | ||
|
330a80fe70 | ||
|
b2ce533b82 | ||
|
12ce6b1135 | ||
|
71c5829702 | ||
1c4d5b05b3 | |||
e85197a2cc | |||
|
abf2b92e6e | ||
|
3be826df4b | ||
9db6bfd255 | |||
|
110adf90de | ||
|
40e4fbda15 | ||
8e983ad2cb | |||
|
6305752ad1 | ||
cc02e2c5a8 | |||
|
f4a63eae2a | ||
763be8532d | |||
|
a6f5645a22 | ||
|
bbe13f27dc | ||
f444746f46 | |||
b4af645941 | |||
a5c8daa5b8 | |||
1a9fc37285 | |||
f0351e5b94 | |||
9bda4e71b7 | |||
7c00055034 | |||
4479a9600b | |||
|
7a6bd8bdbd | ||
ad713fcf35 | |||
251795e2d2 | |||
45cda7a7cc | |||
b7039553cc | |||
ddee68b4c2 | |||
573ac6d42e | |||
265f28b4d9 | |||
1990d9a3d4 | |||
748d44977e | |||
4051eff382 | |||
4276586e11 | |||
832fb0fb03 | |||
328ab61757 | |||
95d15de1bd | |||
7dcd4441c4 | |||
d81e0911ab | |||
5bfff649e9 | |||
76743e8f7c | |||
4ed2f9a939 | |||
c5eb73ed3f | |||
fa3f3e1fd8 | |||
2c7c97852f | |||
48ebf23bd1 | |||
64deedc5ad | |||
9f033fb994 | |||
401cb49687 | |||
1356011ba3 | |||
0cfd7938ee | |||
745b55a942 | |||
eef7c1dcec | |||
aa8fc545d7 | |||
a780c7e0ff | |||
|
9a1bb0599f | ||
|
d847870f67 | ||
cd24371576 | |||
|
6ef565cf07 | ||
46b45c8ab6 | |||
f28531a225 | |||
8fb1f0803e | |||
020ce36312 | |||
b4545b178f | |||
d9a3eab015 | |||
2ab49c218d | |||
|
8f9385d508 | ||
|
4e9f8d16ee | ||
0c002918eb | |||
3d56ec7b49 | |||
48c3e6afc4 | |||
|
50a5c8fe1e | ||
b0d1f115c9 | |||
a59f77f618 | |||
e7a1858091 | |||
648a51efe8 | |||
38962c4807 | |||
|
43e4cdd4cf | ||
|
91046c1ac1 | ||
|
63f8b9b6a1 | ||
|
ed476d9b5c | ||
|
f41d8c0480 | ||
e3b54a8be1 | |||
|
c2c0394624 | ||
8a6f5eac59 | |||
a5fdcc1a85 | |||
1df21da47c | |||
8da0224876 | |||
359e14a9eb | |||
813aa320d9 | |||
aaa5549609 | |||
35cb7e1dc4 | |||
992a033cb2 | |||
5267b37eaf | |||
8fe30d8b6d | |||
37ccb6b00d | |||
e8af2d24a8 | |||
d7f1aa97af | |||
c46224635a | |||
cc99491fe4 | |||
fe8435531e | |||
5d48fe08c7 | |||
4437d44486 | |||
9fe3680bbb | |||
da1947e4ef | |||
37a848df9d | |||
|
9816965e18 | ||
98165cacaa | |||
7ad3096b4e | |||
7d345cf795 | |||
f40e9c592e | |||
7671c585f5 | |||
93b4a7063b | |||
3efeb45c46 | |||
3fc227d2de | |||
604b371920 | |||
fd321beece | |||
9fe4e6b9e3 | |||
|
57bf90481b | ||
|
6062a32c1c | ||
94c899eb82 | |||
44a4ca75bd | |||
|
c8e1605b08 | ||
60e5556a3e | |||
f2fcc98839 | |||
c54438d6d3 | |||
b3f10220b3 | |||
d19f475fc2 | |||
795db96319 | |||
b5fee79e90 | |||
|
5532ddbda9 | ||
3369d3dc2d | |||
fd25f881f9 | |||
5ca3a22dc5 | |||
5c668249cf | |||
39b9a59143 | |||
|
d25c62b4da | ||
|
8cf738bac8 | ||
534659f9ae | |||
c00fd1381f | |||
17c6686163 | |||
bc0c5a76ba | |||
2b16c0ece4 | |||
|
ba1416dce2 | ||
13adf0a767 | |||
cacab55f55 | |||
|
3189d625e3 | ||
|
f71ed39b88 | ||
409ed54608 | |||
d9d3bf2bc9 | |||
9e9de7b5c5 | |||
|
b2a5b40c03 | ||
729aa9781a | |||
cd1ebacf89 | |||
a08074b446 | |||
0cd182546b | |||
89fdd210ca | |||
|
0de2321920 | ||
|
e2cf6e8c21 | ||
|
430490ad93 | ||
|
a35566f273 | ||
3679121c25 | |||
|
7657d05edf | ||
|
1ddf8f0dbe | ||
9506bb862a | |||
4cfab365c2 | |||
8d0aa73d1e | |||
30b487c37f | |||
aef17be36c | |||
ea65d8eee7 | |||
|
7dc33c78aa | ||
|
69cd083054 | ||
|
91788054e6 | ||
|
968a67ce3d | ||
f9ee7d0450 | |||
|
0e15c39797 | ||
adf407c1ba | |||
8a86344484 | |||
d2d0c3ca41 | |||
|
9046b858b1 | ||
46987faea8 | |||
|
dd25827e40 | ||
4069ade36d | |||
c8594c0549 | |||
7359c3b5bd | |||
4195b8416f | |||
9407a29922 | |||
2fcd080bd4 | |||
26446fb7ed | |||
2480c76a08 | |||
1f0ec57789 | |||
|
76e5849c78 | ||
db1641b74f | |||
165c54f663 | |||
|
a5ca3ea204 | ||
|
add95292ad | ||
7a63608f54 | |||
7ea7b369ab | |||
258fffa958 | |||
0849c5131f | |||
|
cce59d0ca8 | ||
94a53fec6c | |||
13aa47cd44 | |||
85f625daf7 | |||
7de3bb9346 | |||
e43a0ba0b4 | |||
|
638cd4bfb7 | ||
3959333662 | |||
abd46aa322 | |||
d4888ad8fb | |||
6bfc229b77 | |||
d31b051f4b | |||
95b60df8fc | |||
ed5189fdc1 | |||
265ed66d25 | |||
09c07acd5c | |||
3c5a69adc9 | |||
0203f69e95 | |||
9a2498862c | |||
|
3a26d4f509 | ||
ce08cbd8b5 | |||
|
d91ffcccca | ||
6115eb9409 | |||
3fd26a0523 | |||
e217d5181b | |||
dcf368b350 | |||
21e3e79ddf | |||
2918c3cb92 | |||
3ad190b18c | |||
d5b2bde2ea | |||
a42348ef5c | |||
8b93c49778 | |||
0842e00098 | |||
6cef02bebb | |||
|
334c7a31d2 | ||
bc82289d54 | |||
c9fa941578 | |||
4048df3c7b | |||
198368605b | |||
8f0ac26b69 | |||
b35fc5b78b | |||
622b519cbb | |||
71e2c911ae | |||
756d49b259 |
163
.all-contributorsrc
Normal file
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"projectName": "antares",
|
||||
"projectOwner": "Fabio286",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"files": [
|
||||
"README.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": false,
|
||||
"commitConvention": "angular",
|
||||
"contributors": [
|
||||
{
|
||||
"login": "Fabio286",
|
||||
"name": "Fabio Di Stasio",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/31471771?v=4",
|
||||
"profile": "https://fabiodistasio.it/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"translation",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "toriphes",
|
||||
"name": "Giulio Ganci",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4192159?v=4",
|
||||
"profile": "https://www.linkedin.com/in/giulioganci/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "digitalgopnik",
|
||||
"name": "Christian Ratz",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2630316?v=4",
|
||||
"profile": "https://christianratz.de/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "reverb6821",
|
||||
"name": "Giuseppe Gigliotti",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/55198803?v=4",
|
||||
"profile": "https://reverb6821.github.io/",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Mohd-PH",
|
||||
"name": "Mohd-PH",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/9362157?v=4",
|
||||
"profile": "https://github.com/Mohd-PH",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hongkfui",
|
||||
"name": "hongkfui",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/37477191?v=4",
|
||||
"profile": "https://github.com/hongkfui",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "MrAnyx",
|
||||
"name": "Robin",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/44176707?v=4",
|
||||
"profile": "https://github.com/MrAnyx",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "daeleduardo",
|
||||
"name": "Daniel Eduardo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8599078?v=4",
|
||||
"profile": "https://github.com/daeleduardo",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "datlechin",
|
||||
"name": "Ngô Quốc Đạt",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/56961917?v=4",
|
||||
"profile": "https://ngoquocdat.com/",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "IsamuSugi",
|
||||
"name": "Isamu Sugiura",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7746658?v=4",
|
||||
"profile": "https://github.com/IsamuSugi",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Occhioverde",
|
||||
"name": "Riccardo Sacchetto",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/18429412?v=4",
|
||||
"profile": "http://rsacchetto.nexxontech.it/",
|
||||
"contributions": [
|
||||
"platform"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kilianstallz",
|
||||
"name": "Kilian Stallinger",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5290318?v=4",
|
||||
"profile": "https://kilianstallinger.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "wenj91",
|
||||
"name": "文杰",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/12549338?v=4",
|
||||
"profile": "https://github.com/wenj91",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "goYou",
|
||||
"name": "goYou",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/62732795?v=4",
|
||||
"profile": "https://github.com/goYou",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "raliqala",
|
||||
"name": "Topollo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/30502407?v=4",
|
||||
"profile": "https://github.com/raliqala",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "SmileYzn",
|
||||
"name": "Cleverson",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5851851?v=4",
|
||||
"profile": "https://github.com/SmileYzn",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
"skipCi": true
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
/node_modules
|
||||
/assets/vendor
|
||||
/out
|
||||
/dist
|
||||
node_modules
|
||||
assets
|
||||
out
|
||||
dist
|
@@ -45,7 +45,9 @@
|
||||
"no-console": "off",
|
||||
"no-undef": "off",
|
||||
"vue/no-side-effects-in-computed-properties": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/require-default-prop": "off",
|
||||
"vue/comment-directive": "off",
|
||||
"vue/no-v-html": "off",
|
||||
"vue/html-indent": [
|
||||
"error",
|
||||
@@ -60,10 +62,11 @@
|
||||
"vue/max-attributes-per-line": [
|
||||
"error",
|
||||
{
|
||||
"singleline": 2,
|
||||
"singleline": {
|
||||
"max": 2
|
||||
},
|
||||
"multiline": {
|
||||
"max": 1,
|
||||
"allowFirstLine": false
|
||||
"max": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
|
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -26,13 +26,14 @@ If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Application (please complete the following information):**
|
||||
|
||||
- Client [e.g. MySQL]
|
||||
- Version [e.g. 0.14.0]
|
||||
- Distribution: [e.g. exe, Linux Store, AppImage, dmg]
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
|
||||
- OS: [e.g. iOS]
|
||||
- Version [e.g. 22]
|
||||
- OS: [e.g. Windows 11]
|
||||
- Version [e.g. 21H2]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
8
.github/workflows/build-linux.yml
vendored
@@ -12,12 +12,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Node.js, NPM and Yarn
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm i
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: Build/release Electron app
|
||||
uses: samuelmeuli/action-electron-builder@v1
|
||||
|
8
.github/workflows/build-mac.yml
vendored
@@ -12,12 +12,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Node.js, NPM and Yarn
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm i
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: Build/release Electron app
|
||||
uses: samuelmeuli/action-electron-builder@v1
|
||||
|
10
.github/workflows/build-win.yml
vendored
@@ -8,17 +8,23 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest]
|
||||
os: [windows-2019]
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Node.js, NPM and Yarn
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm i
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: Build/release Electron app
|
||||
uses: samuelmeuli/action-electron-builder@v1
|
||||
with:
|
||||
|
12
.gitignore
vendored
@@ -1,9 +1,9 @@
|
||||
.DS_Store
|
||||
dist/
|
||||
node_modules/
|
||||
dist
|
||||
build
|
||||
node_modules
|
||||
thumbs.db
|
||||
.idea/
|
||||
.vscode
|
||||
TODO.md
|
||||
NOTES.md
|
||||
*.txt
|
||||
package-lock.json
|
||||
package-lock.json
|
||||
*.heapsnapshot
|
39
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Electron: Main",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"port": 9222,
|
||||
"protocol": "inspector",
|
||||
"request": "attach",
|
||||
"sourceMaps": true,
|
||||
"type": "node",
|
||||
"timeout": 1000000
|
||||
},
|
||||
{
|
||||
"name": "Electron: Renderer",
|
||||
"port": 9223,
|
||||
"request": "attach",
|
||||
"sourceMaps": true,
|
||||
"type": "chrome",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"name": "Electron: Worker",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"port": 9224,
|
||||
"protocol": "inspector",
|
||||
"request": "attach",
|
||||
"sourceMaps": true,
|
||||
"type": "node",
|
||||
"timeout": 1000000
|
||||
},
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Electron: All",
|
||||
"configurations": ["Electron: Main", "Electron: Renderer"]
|
||||
}
|
||||
]
|
||||
}
|
11
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"conventionalCommits.scopes": [
|
||||
"UI",
|
||||
"core",
|
||||
"MySQL",
|
||||
"PostgreSQL",
|
||||
"SQLite",
|
||||
"Windows"
|
||||
],
|
||||
"svg.preview.background": "transparent"
|
||||
}
|
301
CHANGELOG.md
@@ -2,6 +2,307 @@
|
||||
|
||||
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.5.1](https://github.com/Fabio286/antares/compare/v0.5.0...v0.5.1) (2022-03-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* export database as zip sql file ([8f3efab](https://github.com/Fabio286/antares/commit/8f3efabb6962c55c23a43c8da1433185dbc3fb41))
|
||||
* **UI:** option to disable blur effects, closes [#209](https://github.com/Fabio286/antares/issues/209) ([e9079ad](https://github.com/Fabio286/antares/commit/e9079adb25ec28e9546acd54bc2565b8d6e28120))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* numeric scale displayed on non decimal fields ([db628f7](https://github.com/Fabio286/antares/commit/db628f77226b161c3df31b7450b92a6e58754ab7))
|
||||
* **UI:** connection buttons out of screen on small displays, closes [#213](https://github.com/Fabio286/antares/issues/213) ([f12e6a9](https://github.com/Fabio286/antares/commit/f12e6a96dd66140b06c55eda775af48a666627dd))
|
||||
|
||||
## [0.5.0](https://github.com/Fabio286/antares/compare/v0.4.4...v0.5.0) (2022-03-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* delete dump file when the export is canceled ([d25c62b](https://github.com/Fabio286/antares/commit/d25c62b4da9480e040d0bfac8b76732a4c69a5f1))
|
||||
* initial db export implementation ([0de2321](https://github.com/Fabio286/antares/commit/0de232192076d1de827424c593ac9dff63903531))
|
||||
* initial mysql import support ([4e9f8d1](https://github.com/Fabio286/antares/commit/4e9f8d16ee3c204d5f0c2bed081206f8b38207a6))
|
||||
* mysql export for trigger, views, schedulers, functions and routines ([b2a5b40](https://github.com/Fabio286/antares/commit/b2a5b40c03d56bced5a7968c3454f36060e56dd0))
|
||||
* **MySQL:** enhance export characters escaping ([3be826d](https://github.com/Fabio286/antares/commit/3be826df4b02ff0df0aa922d96755b31b7155784))
|
||||
* **MySQL:** support to multi spatial fields export ([4be55f3](https://github.com/Fabio286/antares/commit/4be55f3fe9bb48324b780734762f2ff6da2ccb61))
|
||||
* **PostgreSQL:** :sparkles: Postgress connection string feature for local and server connection string ([6305752](https://github.com/Fabio286/antares/commit/6305752ad117cc29c04bce3ce3df321f743cdc44))
|
||||
* **PostgreSQL:** :sparkles: Postgress connection string feature for local and server connection string ([f4a63ea](https://github.com/Fabio286/antares/commit/f4a63eae2aca2a84647a5027137614950aef1eac))
|
||||
* **UI:** auto-refresh schema at the end of the import process ([abf2b92](https://github.com/Fabio286/antares/commit/abf2b92e6e66b6668e698c5addf4e3c00ae5157b))
|
||||
* **UI:** better real-time import stats ([a6f5645](https://github.com/Fabio286/antares/commit/a6f5645a226454cc2c415311ac321ba3d4db4454))
|
||||
* **UI:** toggle tables checkbox by column on export modal ([1c4d5b0](https://github.com/Fabio286/antares/commit/1c4d5b05b3f94b3e7bef930aa7f89bdaa596c0b9))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **MySQL:** exception exporting empty procedures/functions ([ee415da](https://github.com/Fabio286/antares/commit/ee415da127d6d0de95aac901a2a01af863736344))
|
||||
* **MySQL:** export crash with large databases ([8cf738b](https://github.com/Fabio286/antares/commit/8cf738bac85698fddd0504eef7844279e8c11f44))
|
||||
* **MySQL:** missing functions and procedures definer escapes on exporter ([f0351e5](https://github.com/Fabio286/antares/commit/f0351e5b94830f9f52256096c2601b0ca9cd811d))
|
||||
* **MySQL:** missing initial delimiter for exported procedures ([1a9fc37](https://github.com/Fabio286/antares/commit/1a9fc3728580f789727256d7893ca4bb90c16a50))
|
||||
* **MySQL:** procedures exportation ([4276586](https://github.com/Fabio286/antares/commit/4276586e1141500401ff1ab570b29e485f459987))
|
||||
* sql parser hangs during import ([7a6bd8b](https://github.com/Fabio286/antares/commit/7a6bd8bdbd69e3b5fe265d0bb0be844699dd77c2))
|
||||
* wrong schema and table size on explore bar ([4479a96](https://github.com/Fabio286/antares/commit/4479a9600b5e59ef1bcf9135d661b4d7900a4bde))
|
||||
* wrong soft sort algorithm for numeric fields, closes [#199](https://github.com/Fabio286/antares/issues/199) ([763be85](https://github.com/Fabio286/antares/commit/763be8532d2b61d0b4d45e72343f6a2e5fee1db9))
|
||||
|
||||
|
||||
### Improvements
|
||||
|
||||
* avoid to load schema elements if already loaded in export modal ([d9d3bf2](https://github.com/Fabio286/antares/commit/d9d3bf2bc9d39ce8eec5dffbecbf767fbcf47782))
|
||||
* **MySQL:** import performance improvement ([f444746](https://github.com/Fabio286/antares/commit/f444746f465ed0e8bd2e4c007faf17e167814278))
|
||||
* **MySQL:** import tasks managed with async queue ([bbe13f2](https://github.com/Fabio286/antares/commit/bbe13f27dc29f997898f8c13f36b5d582770b21d))
|
||||
* **MySQL:** improved several field types support on exporter ([1990d9a](https://github.com/Fabio286/antares/commit/1990d9a3d441f0e2075ac7e893d5b166275c48c0))
|
||||
* **MySQL:** prevent memory leak on large dump import ([f3759b6](https://github.com/Fabio286/antares/commit/f3759b65411a40d92b98208176cdf8e6dd8230ce))
|
||||
* **PostgreSQL:** :zap: Postgres connection update, better error handling and connection string accommodation. ([330a80f](https://github.com/Fabio286/antares/commit/330a80fe70b81f466f5e883029f42087b4b5c411))
|
||||
* split the export select query to avoid running out of memory ([409ed54](https://github.com/Fabio286/antares/commit/409ed54608ad402b63fcc26a6e724bc447ba89d2))
|
||||
* use fork() for the export process ([748d449](https://github.com/Fabio286/antares/commit/748d44977e76c6c8d6344df52e8e3ccfab84f670))
|
||||
* use fork() for the import process ([573ac6d](https://github.com/Fabio286/antares/commit/573ac6d42ef833f250d102e5b30ae6cf5877f330))
|
||||
|
||||
### [0.4.4](https://github.com/Fabio286/antares/compare/v0.4.3...v0.4.4) (2022-02-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* execution notification for ROLLBACK and COMMIT ([76743e8](https://github.com/Fabio286/antares/commit/76743e8f7c02b824cb21540bfbcbe66ba43de8fa))
|
||||
* **MySQL:** manual commit mode ([4ed2f9a](https://github.com/Fabio286/antares/commit/4ed2f9a93937b4293436a64318b7d6ae3a0d93c2))
|
||||
* **PostgreSQL:** manual commit mode ([d81e091](https://github.com/Fabio286/antares/commit/d81e0911ab82fb75745ab11e67e867a00d8ac273))
|
||||
* reminder for uncommitted changes closing a tab ([5bfff64](https://github.com/Fabio286/antares/commit/5bfff649e92f6fe5aba4b16aa4c8d5a5a70b31b2))
|
||||
* **SQLite:** manual commit mode ([7dcd444](https://github.com/Fabio286/antares/commit/7dcd4441c49fafc0f47e12c2129708fe1092e1a4))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bigint support, closes [#197](https://github.com/Fabio286/antares/issues/197) ([b703955](https://github.com/Fabio286/antares/commit/b7039553ccaac4fd59e521530c4a053922854130))
|
||||
* **MySQL:** default value not displayed for DECIMAL fields ([fa3f3e1](https://github.com/Fabio286/antares/commit/fa3f3e1fd8101f19f18df71e90d60fd37cdddaee))
|
||||
* zero-padded bit fields beyond length ([265f28b](https://github.com/Fabio286/antares/commit/265f28b4d94cde4608a1d6d3d306824134808ec2))
|
||||
|
||||
### [0.4.3](https://github.com/Fabio286/antares/compare/v0.4.2...v0.4.3) (2022-01-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add Simplified Chinese translation ([6ef565c](https://github.com/Fabio286/antares/commit/6ef565cf078cb3f5b7bcdc226894cddeb6239db9))
|
||||
* **MySQL:** spatial fields support ([#165](https://github.com/Fabio286/antares/issues/165)) ([48ebf23](https://github.com/Fabio286/antares/commit/48ebf23bd1574408f429f2e1200ce878352007f6))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* cell copy returns "undefined" in some conditions, closes [#170](https://github.com/Fabio286/antares/issues/170) ([8fb1f08](https://github.com/Fabio286/antares/commit/8fb1f0803efd9df0b66521e73bb6e1a229cf9691))
|
||||
* indexes and foreign keys not cleared after deletion of related field, closes [#182](https://github.com/Fabio286/antares/issues/182) ([9f033fb](https://github.com/Fabio286/antares/commit/9f033fb994916b4fb165e81e55e86127ca817791))
|
||||
* **PostgreSQL:** schema different than public not automatically selected, closes [#172](https://github.com/Fabio286/antares/issues/172) ([46b45c8](https://github.com/Fabio286/antares/commit/46b45c8ab64fb6837a532c4f8342167e4fd794bb))
|
||||
* scale on numeric fields that doesn't support it ([0cfd793](https://github.com/Fabio286/antares/commit/0cfd7938ee7d607dbad66ae452d0200223a6bab2))
|
||||
* **Windows:** temporary fix to Windows 7 style frame on app startup, closes [#169](https://github.com/Fabio286/antares/issues/169) ([1356011](https://github.com/Fabio286/antares/commit/1356011ba3b7dd72e12cb252a8787ce48a364fd4))
|
||||
|
||||
|
||||
### Improvements
|
||||
|
||||
* support of scale in field's length setting ([eef7c1d](https://github.com/Fabio286/antares/commit/eef7c1dcecc6593ab0e69ed678187a57fe0a4fb6))
|
||||
|
||||
### [0.4.2](https://github.com/Fabio286/antares/compare/v0.4.1...v0.4.2) (2022-01-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **MySQL:** ability to cancel queries ([a59f77f](https://github.com/Fabio286/antares/commit/a59f77f618aea6156fc80fb832d3efcb9848411f))
|
||||
* **PostgreSQL:** ability to cancel queries ([0c00291](https://github.com/Fabio286/antares/commit/0c002918eb0226f6b3f21ed62117495f86396fb1))
|
||||
* save window state ([8f9385d](https://github.com/Fabio286/antares/commit/8f9385d50815635d091758ecd5d00884e3297ca0))
|
||||
* **UI:** textarea autofocus selecting a query tab, closes [#166](https://github.com/Fabio286/antares/issues/166) ([b4545b1](https://github.com/Fabio286/antares/commit/b4545b178f795712c781a3f4fc35eec31b5ad902))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **SQLite:** exception with some fields ([e7a1858](https://github.com/Fabio286/antares/commit/e7a18580915e7739bfa97948c6a0c4fc90a7e78a))
|
||||
|
||||
|
||||
### Improvements
|
||||
|
||||
* hash for foreign key default names ([48c3e6a](https://github.com/Fabio286/antares/commit/48c3e6afc43c51f70a16703f1a71194f43da7a3e))
|
||||
* **MySQL:** support to ANSI_QUOTES sql_mode, closes [#158](https://github.com/Fabio286/antares/issues/158) ([d9a3eab](https://github.com/Fabio286/antares/commit/d9a3eab015302e9f23112f659658073ab3242191))
|
||||
|
||||
### [0.4.1](https://github.com/Fabio286/antares/compare/v0.4.0...v0.4.1) (2021-12-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* language format detection for text fields ([a5fdcc1](https://github.com/Fabio286/antares/commit/a5fdcc1a85aa188ff1b9a15b1a768aced026f360))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* cell disappear on edit in one column tables ([aaa5549](https://github.com/Fabio286/antares/commit/aaa5549609664665bd4513632d621cb249b379c1))
|
||||
* false positive with Windows Defender ([992a033](https://github.com/Fabio286/antares/commit/992a033cb2bede3d1eb52e19482d810f6692de1e))
|
||||
* **MySQL:** wrong datetime fields default in table filler in some cases ([8da0224](https://github.com/Fabio286/antares/commit/8da022487650039b7f34a9c86a7bd9045eba65e2))
|
||||
* **MySQL:** wrong value for fields "on update" in some conditions ([359e14a](https://github.com/Fabio286/antares/commit/359e14a9ebd48f86069ba7762fe00a7056f52d47))
|
||||
* select all rows with ctrl+a when editing a cell ([35cb7e1](https://github.com/Fabio286/antares/commit/35cb7e1dc48d3a74e9d106cb1a37f454c1b4a4d1))
|
||||
* **SQLite:** update rows with a text primary key ([d7f1aa9](https://github.com/Fabio286/antares/commit/d7f1aa97af32a4c51fc7022498bd47e15fa08430))
|
||||
|
||||
|
||||
### Improvements
|
||||
|
||||
* **UI:** avoid columns size change when editing cells or scrolling results ([813aa32](https://github.com/Fabio286/antares/commit/813aa320d9ab799efea38a7110b7c0bdf7549123))
|
||||
* **UI:** disable save button in table creation when no fields are added ([e8af2d2](https://github.com/Fabio286/antares/commit/e8af2d24a869f7667c069936648808952d2062ab))
|
||||
|
||||
## [0.4.0](https://github.com/Fabio286/antares/compare/v0.3.9...v0.4.0) (2021-11-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **MySQL:** read-only mode ([4437d44](https://github.com/Fabio286/antares/commit/4437d44486c4f20b0bec4bf89d56016b08e36e79))
|
||||
* **PostgreSQL:** read-only mode ([5d48fe0](https://github.com/Fabio286/antares/commit/5d48fe08c77755ed18b3f7a9ea834268e317e7ef))
|
||||
* **SQLite:** cell update in data tabs ([604b371](https://github.com/Fabio286/antares/commit/604b3719204f7473ce4846624f08f8be9eec8b8f))
|
||||
* **SQLite:** connection add/edit masks ([c54438d](https://github.com/Fabio286/antares/commit/c54438d6d3bad38bc76dfcd61f58929fe30279cb))
|
||||
* **SQLite:** keys support ([fd321be](https://github.com/Fabio286/antares/commit/fd321beece075d3ad23fdd8541f9beb5727045a5))
|
||||
* **SQLite:** readonly mode ([3fc227d](https://github.com/Fabio286/antares/commit/3fc227d2de53aae115226ad3c965bfb6e9f3eca6))
|
||||
* **SQLite:** table data visualization ([f2fcc98](https://github.com/Fabio286/antares/commit/f2fcc9883972402eab4d51ef2a9796638dde2d3d))
|
||||
* **SQLite:** tables management ([3efeb45](https://github.com/Fabio286/antares/commit/3efeb45c460f178b794de72367f8d542fd8ddd56))
|
||||
* **SQLite:** triggers management ([f40e9c5](https://github.com/Fabio286/antares/commit/f40e9c592eeffd204aba21a0a0767a0c523fca49))
|
||||
* **SQLite:** views management ([7671c58](https://github.com/Fabio286/antares/commit/7671c585f5f8049bd863db190d4fc60d8f0c6c66))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **SQLite:** hide schema creation ([98165ca](https://github.com/Fabio286/antares/commit/98165cacaa158c85ead0490d3caf579e2a17319f))
|
||||
* **UI:** hide tools menu if no tools available ([da1947e](https://github.com/Fabio286/antares/commit/da1947e4efa7f0a26d6a231fadf750be055fbdd5))
|
||||
* **UI:** notifications timeout anomalies ([cc99491](https://github.com/Fabio286/antares/commit/cc99491fe4a15812368f6c928b8c7801d7b255aa))
|
||||
|
||||
|
||||
### Improvements
|
||||
|
||||
* **SQLite:** improvements in data visualization ([94c899e](https://github.com/Fabio286/antares/commit/94c899eb8288b41a5962ac3d24365227e1f9f485))
|
||||
* **SQLite:** improvements in field length detection ([93b4a70](https://github.com/Fabio286/antares/commit/93b4a7063beeb5a7001cb06a74f05b23105212f5))
|
||||
* update italian traslation ([9fe3680](https://github.com/Fabio286/antares/commit/9fe3680bbb17c192cffa85348e68794ab49beb81))
|
||||
|
||||
### [0.3.9](https://github.com/Fabio286/antares/compare/v0.3.8...v0.3.9) (2021-11-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* added macos basic shortcusts and menu ([430490a](https://github.com/Fabio286/antares/commit/430490ad93f3148962ced1f13a5330c79cd86b3b))
|
||||
* **MySQL:** enable/disable schedulers from contextual menu ([5ca3a22](https://github.com/Fabio286/antares/commit/5ca3a22dc538b27a4bf6402f1288c4b9f5bc5a90))
|
||||
* **MySQL:** scheduler status indicator in explore bar ([5c66824](https://github.com/Fabio286/antares/commit/5c668249cf102cd9d601f9f7b4943c7155775217))
|
||||
* **PostgreSQL:** enable/disable triggers from contextual menu ([534659f](https://github.com/Fabio286/antares/commit/534659f9aee12eb5ac477f91bfe5d764387dc17e))
|
||||
* schema size in explore bar ([fd25f88](https://github.com/Fabio286/antares/commit/fd25f881f95779709156cbad93a41d6b391f1a45))
|
||||
* **UI:** double click on the title bar will toggle window fullscreen size ([a35566f](https://github.com/Fabio286/antares/commit/a35566f273322602abe434b8bd30817ba8885900))
|
||||
* **UI:** improved topbar look&feel on MacOS ([7657d05](https://github.com/Fabio286/antares/commit/7657d05edfbeaed6a14eb337fc562da5126e6ba0))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* copy&paste and basic usability on macOS ([1ddf8f0](https://github.com/Fabio286/antares/commit/1ddf8f0dbe22f94d6bffddf70636706d2d142ecf))
|
||||
* **PostgreSQL:** bigint fetched as string instead of number, closes [#134](https://github.com/Fabio286/antares/issues/134) ([39b9a59](https://github.com/Fabio286/antares/commit/39b9a59143b457a96f0711a3b8588c92dd80e28d))
|
||||
* row selection problems after a deletion fail, closes [#128](https://github.com/Fabio286/antares/issues/128) ([89fdd21](https://github.com/Fabio286/antares/commit/89fdd210ca48fc9ae399b195ea796c8523619627))
|
||||
* temporary solution on MacOS for unsigned app updates ([c00fd13](https://github.com/Fabio286/antares/commit/c00fd1381f451ba7aace7047b28b904ddcaf18f0))
|
||||
|
||||
|
||||
### Improvements
|
||||
|
||||
* **UI:** improved function and routine parameters modals ([d19f475](https://github.com/Fabio286/antares/commit/d19f475fc28c0367ada569cb634769fa618b48b4))
|
||||
|
||||
### [0.3.8](https://github.com/Fabio286/antares/compare/v0.3.7...v0.3.8) (2021-10-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **UI:** CTRL+A to select all result rows ([4069ade](https://github.com/Fabio286/antares/commit/4069ade36df43e58f198dd1305778b5811824315))
|
||||
* **UI:** ctrl|cmd+t, ctrl|cmd+w shortcut to open/close workspace tabs ([9046b85](https://github.com/Fabio286/antares/commit/9046b858b1e4608af4c01bc4d69e1a49d4009c07))
|
||||
* **UI:** hide filter bar if there are no more rows in it ([9178805](https://github.com/Fabio286/antares/commit/91788054e6302e83cb4a7501ad6c3f72809cb3bb))
|
||||
* **UI:** multi column table filters ([0e15c39](https://github.com/Fabio286/antares/commit/0e15c39797fe34f7a649f85ee62204682d45c98a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **PostgreSQL:** issue with uppercase characters in table field names ([aef17be](https://github.com/Fabio286/antares/commit/aef17be36cfcf3a6325a954e80f973623e250405))
|
||||
* query failure when a filter with a numeric value is used ([69cd083](https://github.com/Fabio286/antares/commit/69cd083054cae50d64475b9f1f5d7ebd39093e39))
|
||||
* regression during resize results table on filters change ([7dc33c7](https://github.com/Fabio286/antares/commit/7dc33c78aa4152264cc6833437be9af9b8621867))
|
||||
|
||||
|
||||
### Improvements
|
||||
|
||||
* **UI:** resize results table on filters change ([f9ee7d0](https://github.com/Fabio286/antares/commit/f9ee7d0450a1386800223d7b96849e06ae02aece))
|
||||
|
||||
### [0.3.7](https://github.com/Fabio286/antares/compare/v0.3.6...v0.3.7) (2021-10-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* support to SSH private keys with passphrase, closes [#118](https://github.com/Fabio286/antares/issues/118) ([9407a29](https://github.com/Fabio286/antares/commit/9407a29922812ab6aa3cf67569ba2f509433657c))
|
||||
* **UI:** auto detect system theme as default app theme ([7a63608](https://github.com/Fabio286/antares/commit/7a63608f54e387d45e655855666041f5602b54b1))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* closing ask credential modal during a connection doesn't stops loading, closes [#114](https://github.com/Fabio286/antares/issues/114) ([26446fb](https://github.com/Fabio286/antares/commit/26446fb7ed04216283736072d442786350252dbb))
|
||||
* **PostgreSQL:** issue with uppercase characters in table and field names, closes [#116](https://github.com/Fabio286/antares/issues/116) ([2fcd080](https://github.com/Fabio286/antares/commit/2fcd080bd47367a21590ea5a754410a975959bdd))
|
||||
* **UI:** window reload pressing enter in schema creation modal, closes [#113](https://github.com/Fabio286/antares/issues/113) ([db1641b](https://github.com/Fabio286/antares/commit/db1641b74fcd218f1f1d24163cba70b024cc6bd7))
|
||||
|
||||
### [0.3.6](https://github.com/Fabio286/antares/compare/v0.3.5...v0.3.6) (2021-09-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* copy cell/row or kill connections on context menu from processes list ([85f625d](https://github.com/Fabio286/antares/commit/85f625daf7026815dac6223a29c5a6479830edbb))
|
||||
* processes list exportation ([13aa47c](https://github.com/Fabio286/antares/commit/13aa47cd4441aa47c93038dbd91d6a0e54f6a60c))
|
||||
* workspace query history ([3959333](https://github.com/Fabio286/antares/commit/39593336626e6d9f3d3b65d2a4081388900e37d6))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adding a connection default values not change when switching clients, closes [#101](https://github.com/Fabio286/antares/issues/101) ([d4888ad](https://github.com/Fabio286/antares/commit/d4888ad8fba3c8e8ec2d6b6d9a78bb212d83eeed))
|
||||
|
||||
|
||||
### Improvements
|
||||
|
||||
* **core:** better communication of internal exceptions ([abd46aa](https://github.com/Fabio286/antares/commit/abd46aa32256f822e52eaac2fc698da378b8163f))
|
||||
|
||||
### [0.3.5](https://github.com/Fabio286/antares/compare/v0.3.4...v0.3.5) (2021-09-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **MySQL:** connections stuck at startup if 5 or more tabs are restored ([95b60df](https://github.com/Fabio286/antares/commit/95b60df8fc634b96a4c2c5c48dc6b10848888978))
|
||||
|
||||
### [0.3.4](https://github.com/Fabio286/antares/compare/v0.3.3...v0.3.4) (2021-09-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* new create function tabs ([0203f69](https://github.com/Fabio286/antares/commit/0203f69e95093d25a7ef3e66df1c70f76edcedf2))
|
||||
* new create routine tabs ([3fd26a0](https://github.com/Fabio286/antares/commit/3fd26a05238368ae375197c79d42162a5910bb07))
|
||||
* new create scheduler tabs ([3c5a69a](https://github.com/Fabio286/antares/commit/3c5a69adc9cecdc4b8f46097676005f2a60f06cf))
|
||||
* new create trigger function tabs ([09c07ac](https://github.com/Fabio286/antares/commit/09c07acd5c2a6ed2b75640dbb83e782ed432bc30))
|
||||
* new create trigger tabs ([e217d51](https://github.com/Fabio286/antares/commit/e217d5181b37ec6304151120b4a2aba9455c6a84))
|
||||
* start search when typing with focus on the left bar ([265ed66](https://github.com/Fabio286/antares/commit/265ed66d25d35be99ed0a6b1668dab9f246ed71e))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **UI:** context menu of tables cut if close to bottom edge ([21e3e79](https://github.com/Fabio286/antares/commit/21e3e79ddf9e292bcfc5881b8fa76a1dc58b207c))
|
||||
|
||||
### [0.3.3](https://github.com/Fabio286/antares/compare/v0.3.2...v0.3.3) (2021-08-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* new create table tabs ([c9fa941](https://github.com/Fabio286/antares/commit/c9fa9415787c3953043db5876a99b3664c69c071))
|
||||
* new create view tabs ([8b93c49](https://github.com/Fabio286/antares/commit/8b93c497784ea431f9747c5afb53f6ef075ea9d6))
|
||||
* new table empty state ([0842e00](https://github.com/Fabio286/antares/commit/0842e00098ba420412937aa52276ee33bda53693))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **MySQL:** editing a view causes error for missing database in some conditions ([4048df3](https://github.com/Fabio286/antares/commit/4048df3c7bc2d42a60f7a57c9a4b8b5b445fcd43))
|
||||
* table options not loaded on restored setting tabs at startup ([622b519](https://github.com/Fabio286/antares/commit/622b519cbb5fbe4e38a4baffb8eab169b21eed21))
|
||||
* **UI:** multiple temp tabs opened switching to tables from other elements ([b35fc5b](https://github.com/Fabio286/antares/commit/b35fc5b78bdbeff1422ef088441b17c8b0df663c))
|
||||
* **UI:** no round borders on left of file upload inputs ([d5b2bde](https://github.com/Fabio286/antares/commit/d5b2bde2eaf8ff3e14f49cc26acdcb201b4cb15c))
|
||||
|
||||
|
||||
### Improvements
|
||||
|
||||
* **UI:** element options in setting tabs accessible directly ([71e2c91](https://github.com/Fabio286/antares/commit/71e2c911ae23e86543b2af1fa885981ff271777d))
|
||||
* **UI:** improved view setting tab ([1983686](https://github.com/Fabio286/antares/commit/198368605b084bd58fd6f7ca0b19895ba23a45e6))
|
||||
* **UI:** primary app color on selected text backgrouns ([756d49b](https://github.com/Fabio286/antares/commit/756d49b2596dea58376c6afa8e0bad0cd62b146c))
|
||||
* **UI:** visual improvements of tables ([bc82289](https://github.com/Fabio286/antares/commit/bc82289d54550a93300fe66d7a660aa70db2fd23))
|
||||
|
||||
### [0.3.2](https://github.com/Fabio286/antares/compare/v0.3.1...v0.3.2) (2021-08-06)
|
||||
|
||||
|
||||
|
107
CONTRIBUTING.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Contributors Guide
|
||||
|
||||
Antares SQL is an application based on [Electron.js](https://www.electronjs.org/) that uses [Vue.js](https://vuejs.org/) and [Spectre.css](https://picturepan2.github.io/spectre/) as frontend frameworks.
|
||||
For the build process it takes advantage of [electron-builder](https://www.electron.build/).
|
||||
This application uses [Vuex](https://vuex.vuejs.org/) as application state manager and [electron-store](https://github.com/sindresorhus/electron-store) to save the various settings on disc.
|
||||
This guide aims to provide useful information and guidelines to everyone wants to contribute with this open-source project.
|
||||
For every other question related to this project please [contact me](https://github.com/Fabio286).
|
||||
|
||||
## Project Structure
|
||||
|
||||
The main files of the application are located inside `src` folder and are groupped in three subfolders.
|
||||
|
||||
### `common`
|
||||
|
||||
This folder contains small libraries, classes and objects. The purpose of `common` folder is to group together utilities used by **renderer** and **main** processes.
|
||||
Noteworthy is the `customizations` folder that contains clients related customizations. Those settings are merged with `default.js` that lists every option.
|
||||
Client related customizations are stored on Vuex and can be accessed by `customizations` property of current workspace object, or importing `common/customizations`.
|
||||
|
||||
An use case of customizations object can be the following:
|
||||
|
||||
```js
|
||||
computed: {
|
||||
defaultEngine () {
|
||||
if (this.workspace.customizations.engines)
|
||||
return this.workspace.engines.find(engine => engine.isDefault).name;
|
||||
return '';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this case the computed property `defaultEngine` returns the default engine for MySQL client, or an empty string with PostgreSQL that doesn't have engines.
|
||||
Customization properties are also useful **if some features are ready for one client but not others**.
|
||||
|
||||
### `main`
|
||||
|
||||
Inside this folder are located all files required by main process.
|
||||
`ipc-handlers` subfolder includes all IPC handlers for events sent from renderer process.
|
||||
`libs` subfolder includes classes related to clients and **query and connection logics**.
|
||||
**Everything above client's class level should be "client agnostic"** with a neutral and uniformed api interface
|
||||
|
||||
### `renderer`
|
||||
|
||||
In this folder is located the structure of Vue frontend application.
|
||||
|
||||
## Build
|
||||
|
||||
The command to build Antares SQL locally is `npm run build:local`.
|
||||
`build` command (without `:local`) is used exclusively by the GitHub Action.
|
||||
|
||||
## Conventions
|
||||
|
||||
### Electron
|
||||
|
||||
- **kebab-case** for IPC event names.
|
||||
|
||||
### Vue
|
||||
|
||||
- **PascalCase** for file names (with .vue extension) and including components inside others (`<MyComponent/>`).
|
||||
- "**Base**" prefix for [base component names](https://vuejs.org/v2/style-guide/#Base-component-names-strongly-recommended).
|
||||
- "**The**" prefix for [single-instance component names](https://vuejs.org/v2/style-guide/#Single-instance-component-names-strongly-recommended).
|
||||
- [Tightly coupled component names](https://vuejs.org/v2/style-guide/#Tightly-coupled-component-names-strongly-recommended).
|
||||
- [Order of words in component names](https://vuejs.org/v2/style-guide/#Order-of-words-in-component-names-strongly-recommended).
|
||||
- **kebab-case** in templates for property and event names.
|
||||
|
||||
### Vuex
|
||||
|
||||
- **snake_case** for state names.
|
||||
- **camelCase** for getter and action names.
|
||||
- **SNAKE_CASE (all caps)** for mutation names.
|
||||
|
||||
### Code Style
|
||||
|
||||
The project includes [ESlint](https://eslint.org/) and [StyleLint](https://stylelint.io/) config files with style rules. I recommend to set the lint on-save option in your code editor.
|
||||
Alternatively you can launch following commands to lint the project.
|
||||
|
||||
Check if all the style rules have been followed:
|
||||
|
||||
```console
|
||||
npm run lint
|
||||
```
|
||||
|
||||
Apply style rules globally if possible:
|
||||
|
||||
```console
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
### Other recommendations
|
||||
|
||||
Please, use if possible **template literals** to compose strings and **avoid unnecessary dependencies**.
|
||||
|
||||
### Commits
|
||||
|
||||
The commit style adopted for this project is [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
|
||||
Basicly it's important to have **single scoped commits with a prefix** that follows this style because Antares SQL uses [standard-version](https://github.com/conventional-changelog/standard-version) to generate new releases and [CHANGELOG.md](https://github.com/Fabio286/antares/blob/master/CHANGELOG.md) file to track all notable changes.
|
||||
For Visual Studio Code users may be useful [Conventional Commits](https://marketplace.visualstudio.com/items?itemName=vivaxy.vscode-conventional-commits) extension.
|
||||
|
||||
## Debug
|
||||
|
||||
**Debug mode**:
|
||||
|
||||
```console
|
||||
npm run debug
|
||||
```
|
||||
|
||||
After running the debug mode Antares will listen on port 9222 (main process) for a debugger.
|
||||
On **Visual Studio Code** just launch "*Electron: Main*" configurations after running Antares in debug mode.
|
62
README.md
@@ -1,15 +1,18 @@
|
||||
|
||||
<!-- markdownlint-disable -->
|
||||
<p align="center">
|
||||
<img width="800" src="https://raw.githubusercontent.com/Fabio286/antares/master/docs/gh-logo.png">
|
||||
</p>
|
||||
<!-- markdownlint-restore -->
|
||||
|
||||
# Antares SQL Client
|
||||
|
||||
 [](https://actions-badge.atrox.dev/fabio286/antares/goto)   [](https://snapcraft.io/antares) [](https://snapcraft.io/antares) [](https://twitter.com/AntaresSQL) [](https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet)
|
||||
  [](https://actions-badge.atrox.dev/fabio286/antares/goto) [](https://snapcraft.io/antares) [](https://snapcraft.io/antares) [](https://twitter.com/AntaresSQL) [](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.
|
||||
Our target is to support as many databases as possible, and all major operating systems, including the ARM versions.
|
||||
|
||||
**At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB and PostgreSQL.
|
||||
**At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB, PostgreSQL and SQLite.
|
||||
At the moment, however, there are all the features necessary to have a pleasant database management experience, so give it a chance and send us your feedback, we would really appreciate it.
|
||||
We are actively working on it, hoping to provide new cool features, improvements and fixes as soon as possible.
|
||||
|
||||
@@ -26,11 +29,12 @@ We are actively working on it, hoping to provide new cool features, improvements
|
||||
- A modern and friendly tab system; keep open every kind of tab you need in your workspace.
|
||||
- Fake table data filler to generate tons of data for test purpose.
|
||||
- Query suggestions and auto complete.
|
||||
- Query history: search through the last 1000 queries.
|
||||
- SSH tunnel support.
|
||||
- Manual commit mode.
|
||||
- Import and export database dumps.
|
||||
- Dark and light theme.
|
||||
- Editor themes.
|
||||
- Scratchpad.
|
||||
- Secure password storage.
|
||||
|
||||
## Philosophy
|
||||
|
||||
@@ -64,13 +68,11 @@ On macOS you can run `.dmg` distribution following [this guide](https://support.
|
||||
|
||||
This is a roadmap with major features will come in near future.
|
||||
|
||||
- Support for other databases.
|
||||
- Database tools.
|
||||
- Users management (add/edit/delete).
|
||||
- Query history and bookmarks.
|
||||
- More context menu shortcuts.
|
||||
- More keyboard shortcuts.
|
||||
- Import/export and migration.
|
||||
- Support for other databases.
|
||||
- Apple Silicon distribution
|
||||
|
||||
## Currently supported
|
||||
@@ -79,7 +81,7 @@ This is a roadmap with major features will come in near future.
|
||||
|
||||
- [x] MySQL/MariaDB
|
||||
- [x] PostgreSQL
|
||||
- [ ] SQLite
|
||||
- [x] SQLite
|
||||
- [ ] MSSQL
|
||||
- [ ] OracleDB
|
||||
- [ ] More...
|
||||
@@ -104,11 +106,41 @@ This is a roadmap with major features will come in near future.
|
||||
- 📖 [Contributors Guide](https://github.com/Fabio286/antares/wiki/Contributors-Guide)
|
||||
- 🚧 [Project Board](https://github.com/users/Fabio286/projects/1)
|
||||
|
||||
## Translations
|
||||
## Contributors ✨
|
||||
|
||||
**Italian Translation** / [Giuseppe Gigliotti](https://github.com/ReverbOD) [[#20](https://github.com/Fabio286/antares/pull/20)]
|
||||
**Arabic Translation** (needs updates) / [Mohd-PH](https://github.com/Mohd-PH) [[#29](https://github.com/Fabio286/antares/pull/29)]
|
||||
**Spanish Translation** (needs updates) / [hongkfui](https://github.com/hongkfui) [[#32](https://github.com/Fabio286/antares/pull/32)]
|
||||
**French Translation** (needs updates) / [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)]
|
||||
**Deutsch (Deutschland)** / [Christian Ratz](https://github.com/digitalgopnik) [[#74](https://github.com/Fabio286/antares/pull/74)]
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="https://fabiodistasio.it/"><img src="https://avatars.githubusercontent.com/u/31471771?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Fabio Di Stasio</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=Fabio286" title="Code">💻</a> <a href="#translation-Fabio286" title="Translation">🌍</a> <a href="https://github.com/Fabio286/antares/commits?author=Fabio286" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.linkedin.com/in/giulioganci/"><img src="https://avatars.githubusercontent.com/u/4192159?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Giulio Ganci</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=toriphes" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://christianratz.de/"><img src="https://avatars.githubusercontent.com/u/2630316?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Christian Ratz</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=digitalgopnik" title="Code">💻</a> <a href="#translation-digitalgopnik" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://reverb6821.github.io/"><img src="https://avatars.githubusercontent.com/u/55198803?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Giuseppe Gigliotti</b></sub></a><br /><a href="#translation-reverb6821" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/Mohd-PH"><img src="https://avatars.githubusercontent.com/u/9362157?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mohd-PH</b></sub></a><br /><a href="#translation-Mohd-PH" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/hongkfui"><img src="https://avatars.githubusercontent.com/u/37477191?v=4?s=100" width="100px;" alt=""/><br /><sub><b>hongkfui</b></sub></a><br /><a href="#translation-hongkfui" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/MrAnyx"><img src="https://avatars.githubusercontent.com/u/44176707?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Robin</b></sub></a><br /><a href="#translation-MrAnyx" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/daeleduardo"><img src="https://avatars.githubusercontent.com/u/8599078?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Daniel Eduardo</b></sub></a><br /><a href="#translation-daeleduardo" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://ngoquocdat.com/"><img src="https://avatars.githubusercontent.com/u/56961917?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ngô Quốc Đạt</b></sub></a><br /><a href="#translation-datlechin" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/IsamuSugi"><img src="https://avatars.githubusercontent.com/u/7746658?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Isamu Sugiura</b></sub></a><br /><a href="#translation-IsamuSugi" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="http://rsacchetto.nexxontech.it/"><img src="https://avatars.githubusercontent.com/u/18429412?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Riccardo Sacchetto</b></sub></a><br /><a href="#platform-Occhioverde" title="Packaging/porting to new platform">📦</a></td>
|
||||
<td align="center"><a href="https://kilianstallinger.com"><img src="https://avatars.githubusercontent.com/u/5290318?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kilian Stallinger</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=kilianstallz" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/wenj91"><img src="https://avatars.githubusercontent.com/u/12549338?v=4?s=100" width="100px;" alt=""/><br /><sub><b>文杰</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=wenj91" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/goYou"><img src="https://avatars.githubusercontent.com/u/62732795?v=4?s=100" width="100px;" alt=""/><br /><sub><b>goYou</b></sub></a><br /><a href="#translation-goYou" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/raliqala"><img src="https://avatars.githubusercontent.com/u/30502407?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Topollo</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=raliqala" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/SmileYzn"><img src="https://avatars.githubusercontent.com/u/5851851?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Cleverson</b></sub></a><br /><a href="#translation-SmileYzn" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
|
BIN
assets/appx/Square150x150Logo.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
assets/appx/Square44x44Logo.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
assets/appx/StoreLogo.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
assets/appx/Wide310x150Logo.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 17 KiB |
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"include": [
|
||||
"./src/renderer/**/*"
|
||||
]
|
||||
}
|
107
package.json
@@ -1,27 +1,47 @@
|
||||
{
|
||||
"name": "antares",
|
||||
"productName": "Antares",
|
||||
"version": "0.3.2",
|
||||
"description": "A cross-platform easy to use SQL client.",
|
||||
"version": "0.5.1",
|
||||
"description": "A modern, fast and productivity driven SQL client with a focus in UX.",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/Fabio286/antares.git",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development electron-webpack dev",
|
||||
"compile": "electron-webpack",
|
||||
"debug": "npm run rebuild:electron && npm run debug-runner",
|
||||
"debug-runner": "node scripts/devRunner.js --remote-debug",
|
||||
"compile": "npm run compile:main && npm run compile:workers && npm run compile:renderer",
|
||||
"compile:main": "webpack --mode=production --config webpack.main.config.js",
|
||||
"compile:workers": "webpack --mode=production --config webpack.workers.config.js",
|
||||
"compile:renderer": "webpack --mode=production --config webpack.renderer.config.js",
|
||||
"build": "cross-env NODE_ENV=production npm run compile",
|
||||
"build:local": "npm run build && electron-builder",
|
||||
"build:appx": "npm run build:local -- --win appx",
|
||||
"rebuild:electron": "rimraf ./dist && npm run postinstall",
|
||||
"release": "standard-version",
|
||||
"release:pre": "npm run release -- --prerelease alpha",
|
||||
"test": "npm run lint",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"test": "npm run compile && node tests/app.spec.js",
|
||||
"lint": "eslint . --ext .js,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"",
|
||||
"lint:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix",
|
||||
"postinstall": "electron-builder install-app-deps"
|
||||
"contributors:add": "all-contributors add",
|
||||
"contributors:generate": "all-contributors generate"
|
||||
},
|
||||
"author": "Fabio Di Stasio <fabio286@gmail.com>",
|
||||
"main": "./dist/main.js",
|
||||
"build": {
|
||||
"appId": "com.fabio286.antares",
|
||||
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
|
||||
"asar": true,
|
||||
"buildDependenciesFromSource": true,
|
||||
"directories": {
|
||||
"output": "build",
|
||||
"buildResources": "assets"
|
||||
},
|
||||
"asarUnpack": "**\\*.{node,dll}",
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"node_modules",
|
||||
"package.json"
|
||||
],
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis",
|
||||
@@ -61,7 +81,9 @@
|
||||
"artifactName": "${productName}-${version}-portable.exe"
|
||||
},
|
||||
"appx": {
|
||||
"displayName": "Antares SQL Client",
|
||||
"displayName": "Antares SQL",
|
||||
"backgroundColor": "transparent",
|
||||
"showNameOnTiles": true,
|
||||
"identityName": "62514FabioDiStasio.AntaresSQLClient",
|
||||
"publisher": "CN=1A2729ED-865C-41D2-9038-39AE2A63AA52",
|
||||
"applicationId": "FabioDiStasio.AntaresSQLClient"
|
||||
@@ -81,55 +103,70 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"electronWebpack": {
|
||||
"renderer": {
|
||||
"webpackConfig": "webpack.config.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "^1.2.1",
|
||||
"@mdi/font": "^5.9.55",
|
||||
"ace-builds": "^1.4.12",
|
||||
"@electron/remote": "^2.0.1",
|
||||
"@mdi/font": "^6.1.95",
|
||||
"@turf/helpers": "^6.5.0",
|
||||
"@vscode/vscode-languagedetection": "^1.0.21",
|
||||
"ace-builds": "^1.4.13",
|
||||
"better-sqlite3": "^7.5.0",
|
||||
"electron-log": "^4.4.1",
|
||||
"electron-store": "^8.0.0",
|
||||
"electron-updater": "^4.3.9",
|
||||
"electron-store": "^8.0.1",
|
||||
"electron-updater": "^4.6.1",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"faker": "^5.5.3",
|
||||
"marked": "^2.1.1",
|
||||
"leaflet": "^1.7.1",
|
||||
"marked": "^4.0.0",
|
||||
"moment": "^2.29.1",
|
||||
"mysql2": "^2.3.0",
|
||||
"mysql2": "^2.3.2",
|
||||
"pg": "^8.7.1",
|
||||
"pgsql-ast-parser": "^7.2.1",
|
||||
"source-map-support": "^0.5.16",
|
||||
"source-map-support": "^0.5.20",
|
||||
"spectre.css": "^0.5.9",
|
||||
"sql-formatter": "^4.0.2",
|
||||
"ssh2-promise": "^0.2.0",
|
||||
"v-mask": "^2.2.4",
|
||||
"vue-i18n": "^8.24.4",
|
||||
"ssh2-promise": "^1.0.2",
|
||||
"v-mask": "^2.3.0",
|
||||
"vue-i18n": "^8.26.5",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuex": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.15.0",
|
||||
"@babel/eslint-parser": "^7.15.7",
|
||||
"@babel/preset-env": "^7.15.8",
|
||||
"all-contributors-cli": "^6.20.0",
|
||||
"babel-loader": "^8.2.3",
|
||||
"chalk": "^4.1.2",
|
||||
"cross-env": "^7.0.2",
|
||||
"electron": "^13.1.8",
|
||||
"electron-builder": "^22.11.7",
|
||||
"css-loader": "^6.5.0",
|
||||
"electron": "^17.0.1",
|
||||
"electron-builder": "^22.14.11",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-webpack": "^2.8.2",
|
||||
"electron-webpack-vue": "^2.4.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-vue": "^7.15.1",
|
||||
"sass": "^1.37.5",
|
||||
"sass-loader": "^10.2.0",
|
||||
"eslint-plugin-promise": "^5.2.0",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"mini-css-extract-plugin": "~2.4.5",
|
||||
"node-loader": "^2.0.0",
|
||||
"playwright": "^1.18.1",
|
||||
"progress-webpack-plugin": "^1.0.12",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass": "^1.42.1",
|
||||
"sass-loader": "^12.3.0",
|
||||
"standard-version": "^9.3.1",
|
||||
"style-loader": "^3.3.1",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-standard": "^22.0.0",
|
||||
"stylelint-scss": "^3.20.1",
|
||||
"stylelint-scss": "^3.21.0",
|
||||
"tree-kill": "^1.2.2",
|
||||
"vue": "^2.6.14",
|
||||
"vue-loader": "^15.9.8",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"webpack": "^4.46.0"
|
||||
"webpack": "^5.60.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"webpack-dev-server": "^4.4.0"
|
||||
}
|
||||
}
|
||||
|
131
scripts/devRunner.js
Normal file
@@ -0,0 +1,131 @@
|
||||
process.env.NODE_ENV = 'development';
|
||||
// process.env.ELECTRON_ENABLE_LOGGING = true
|
||||
|
||||
const chalk = require('chalk');
|
||||
const electron = require('electron');
|
||||
const webpack = require('webpack');
|
||||
const WebpackDevServer = require('webpack-dev-server');
|
||||
const kill = require('tree-kill');
|
||||
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
const mainConfig = require('../webpack.main.config');
|
||||
const rendererConfig = require('../webpack.renderer.config');
|
||||
const workersConfig = require('../webpack.workers.config');
|
||||
|
||||
let electronProcess = null;
|
||||
let manualRestart = null;
|
||||
const remoteDebugging = process.argv.includes('--remote-debug');
|
||||
|
||||
if (remoteDebugging) {
|
||||
// disable dvtools open in electron
|
||||
process.env.RENDERER_REMOTE_DEBUGGING = true;
|
||||
}
|
||||
|
||||
async function killElectron (pid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (pid) {
|
||||
kill(pid, 'SIGKILL', err => {
|
||||
if (err) reject(err);
|
||||
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
else
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
async function restartElectron () {
|
||||
console.log(chalk.gray('\nStarting electron...'));
|
||||
|
||||
const { pid } = electronProcess || {};
|
||||
await killElectron(pid);
|
||||
|
||||
electronProcess = spawn(electron, [
|
||||
path.join(__dirname, '../dist/main.js'),
|
||||
// '--enable-logging', // Enable to show logs from all electron processes
|
||||
remoteDebugging ? '--inspect=9222' : '',
|
||||
remoteDebugging ? '--remote-debugging-port=9223' : ''
|
||||
]);
|
||||
|
||||
electronProcess.stdout.on('data', data => {
|
||||
console.log(chalk.white(data.toString()));
|
||||
});
|
||||
|
||||
electronProcess.stderr.on('data', data => {
|
||||
console.error(chalk.red(data.toString()));
|
||||
});
|
||||
|
||||
electronProcess.on('exit', (code, signal) => {
|
||||
if (!manualRestart) process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
function startMain () {
|
||||
const webpackSetup = webpack([mainConfig, workersConfig]);
|
||||
|
||||
webpackSetup.compilers.forEach((compiler) => {
|
||||
const { name } = compiler;
|
||||
|
||||
switch (name) {
|
||||
case 'workers':
|
||||
compiler.hooks.afterEmit.tap('afterEmit', async () => {
|
||||
console.log(chalk.gray(`\nCompiled ${name} script!`));
|
||||
console.log(
|
||||
chalk.gray(`\nWatching file changes for ${name} script...`)
|
||||
);
|
||||
});
|
||||
break;
|
||||
case 'main':
|
||||
default:
|
||||
compiler.hooks.afterEmit.tap('afterEmit', async () => {
|
||||
console.log(chalk.gray(`\nCompiled ${name} script!`));
|
||||
|
||||
manualRestart = true;
|
||||
await restartElectron();
|
||||
|
||||
setTimeout(() => {
|
||||
manualRestart = false;
|
||||
}, 2500);
|
||||
|
||||
console.log(
|
||||
chalk.gray(`\nWatching file changes for ${name} script...`)
|
||||
);
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
webpackSetup.watch({ aggregateTimeout: 500 }, err => {
|
||||
if (err) console.error(chalk.red(err));
|
||||
});
|
||||
}
|
||||
|
||||
function startRenderer (callback) {
|
||||
const compiler = webpack(rendererConfig);
|
||||
const { name } = compiler;
|
||||
|
||||
compiler.hooks.afterEmit.tap('afterEmit', () => {
|
||||
console.log(chalk.gray(`\nCompiled ${name} script!`));
|
||||
console.log(chalk.gray(`\nWatching file changes for ${name} script...`));
|
||||
});
|
||||
|
||||
const server = new WebpackDevServer(compiler, {
|
||||
hot: true,
|
||||
port: 9080,
|
||||
client: {
|
||||
overlay: true,
|
||||
logging: 'warn'
|
||||
}
|
||||
});
|
||||
|
||||
server.startCallback(err => {
|
||||
if (err) console.error(chalk.red(err));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
startRenderer(startMain);
|
@@ -134,7 +134,7 @@ export default class {
|
||||
{ name: 'phoneNumberFormat', group: 'phone', types: ['string'] },
|
||||
{ name: 'phoneFormats', group: 'phone', types: ['string'] },
|
||||
|
||||
{ name: 'number', group: 'random', types: ['string', 'number'], params: ['min', 'max'] },
|
||||
{ name: 'number', group: 'datatype', types: ['string', 'number'], params: ['min', 'max'] },
|
||||
{ name: 'float', group: 'random', types: ['string', 'float'], params: ['min', 'max'] },
|
||||
{ name: 'arrayElement', group: 'random', types: ['string'] },
|
||||
{ name: 'arrayElements', group: 'random', types: ['string'] },
|
||||
|
@@ -8,6 +8,10 @@ module.exports = {
|
||||
collations: false,
|
||||
engines: false,
|
||||
connectionSchema: false,
|
||||
sslConnection: false,
|
||||
sshConnection: false,
|
||||
fileConnection: false,
|
||||
cancelQueries: false,
|
||||
// Tools
|
||||
processesList: false,
|
||||
usersManagement: false,
|
||||
@@ -22,6 +26,8 @@ module.exports = {
|
||||
functions: false,
|
||||
schedulers: false,
|
||||
// Settings
|
||||
elementsWrapper: '',
|
||||
stringsWrapper: '"',
|
||||
tableAdd: false,
|
||||
viewAdd: false,
|
||||
triggerAdd: false,
|
||||
@@ -31,7 +37,13 @@ module.exports = {
|
||||
schedulerAdd: false,
|
||||
databaseEdit: false,
|
||||
schemaEdit: false,
|
||||
schemaDrop: false,
|
||||
schemaExport: false,
|
||||
schemaImport: false,
|
||||
tableSettings: false,
|
||||
tableOptions: false,
|
||||
tableArray: false,
|
||||
tableRealCount: false,
|
||||
viewSettings: false,
|
||||
triggerSettings: false,
|
||||
triggerFunctionSettings: false,
|
||||
@@ -43,13 +55,13 @@ module.exports = {
|
||||
sortableFields: false,
|
||||
unsigned: false,
|
||||
nullable: false,
|
||||
nullablePrimary: false,
|
||||
zerofill: false,
|
||||
autoIncrement: false,
|
||||
comment: false,
|
||||
collation: false,
|
||||
definer: false,
|
||||
onUpdate: false,
|
||||
tableArray: false,
|
||||
viewAlgorithm: false,
|
||||
viewSqlSecurity: false,
|
||||
viewUpdateOption: false,
|
||||
@@ -69,8 +81,10 @@ module.exports = {
|
||||
triggerTableInName: false,
|
||||
triggerUpdateColumns: false,
|
||||
triggerOnlyRename: false,
|
||||
triggerEnableDisable: false,
|
||||
triggerFunctionSql: false,
|
||||
triggerFunctionlanguages: false,
|
||||
parametersLength: false,
|
||||
languages: false
|
||||
languages: false,
|
||||
readOnlyMode: false
|
||||
};
|
||||
|
@@ -1,5 +1,6 @@
|
||||
module.exports = {
|
||||
maria: require('./mysql'),
|
||||
mysql: require('./mysql'),
|
||||
pg: require('./postgresql')
|
||||
pg: require('./postgresql'),
|
||||
sqlite: require('./sqlite')
|
||||
};
|
||||
|
@@ -10,6 +10,9 @@ module.exports = {
|
||||
connectionSchema: true,
|
||||
collations: true,
|
||||
engines: true,
|
||||
sslConnection: true,
|
||||
sshConnection: true,
|
||||
cancelQueries: true,
|
||||
// Tools
|
||||
processesList: true,
|
||||
// Structure
|
||||
@@ -21,6 +24,8 @@ module.exports = {
|
||||
functions: true,
|
||||
schedulers: true,
|
||||
// Settings
|
||||
elementsWrapper: '',
|
||||
stringsWrapper: '"',
|
||||
tableAdd: true,
|
||||
viewAdd: true,
|
||||
triggerAdd: true,
|
||||
@@ -28,6 +33,9 @@ module.exports = {
|
||||
functionAdd: true,
|
||||
schedulerAdd: true,
|
||||
schemaEdit: true,
|
||||
schemaDrop: true,
|
||||
schemaExport: true,
|
||||
schemaImport: true,
|
||||
tableSettings: true,
|
||||
viewSettings: true,
|
||||
triggerSettings: true,
|
||||
@@ -40,6 +48,7 @@ module.exports = {
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
zerofill: true,
|
||||
tableOptions: true,
|
||||
autoIncrement: true,
|
||||
comment: true,
|
||||
collation: true,
|
||||
@@ -56,5 +65,6 @@ module.exports = {
|
||||
functionDeterministic: true,
|
||||
functionDataAccess: true,
|
||||
functionSql: 'BEGIN\r\n\r\nEND',
|
||||
parametersLength: true
|
||||
parametersLength: true,
|
||||
readOnlyMode: true
|
||||
};
|
||||
|
@@ -8,9 +8,13 @@ module.exports = {
|
||||
defaultDatabase: 'postgres',
|
||||
// Core
|
||||
database: true,
|
||||
sslConnection: true,
|
||||
sshConnection: true,
|
||||
cancelQueries: true,
|
||||
// Tools
|
||||
processesList: true,
|
||||
// Structure
|
||||
schemas: true,
|
||||
tables: true,
|
||||
views: true,
|
||||
triggers: true,
|
||||
@@ -18,13 +22,18 @@ module.exports = {
|
||||
routines: true,
|
||||
functions: true,
|
||||
// Settings
|
||||
elementsWrapper: '"',
|
||||
stringsWrapper: '\'',
|
||||
tableAdd: true,
|
||||
viewAdd: true,
|
||||
triggerAdd: true,
|
||||
triggerFunctionAdd: true,
|
||||
routineAdd: true,
|
||||
functionAdd: true,
|
||||
schemaDrop: true,
|
||||
databaseEdit: false,
|
||||
schemaExport: false,
|
||||
schemaImport: false,
|
||||
tableSettings: true,
|
||||
viewSettings: true,
|
||||
triggerSettings: true,
|
||||
@@ -48,5 +57,7 @@ module.exports = {
|
||||
triggerMultipleEvents: true,
|
||||
triggerTableInName: true,
|
||||
triggerOnlyRename: false,
|
||||
languages: ['sql', 'plpgsql', 'c', 'internal']
|
||||
triggerEnableDisable: true,
|
||||
languages: ['sql', 'plpgsql', 'c', 'internal'],
|
||||
readOnlyMode: true
|
||||
};
|
||||
|
27
src/common/customizations/sqlite.js
Normal file
@@ -0,0 +1,27 @@
|
||||
module.exports = {
|
||||
// Core
|
||||
fileConnection: true,
|
||||
// Structure
|
||||
schemas: false,
|
||||
tables: true,
|
||||
views: true,
|
||||
triggers: true,
|
||||
// Settings
|
||||
elementsWrapper: '"',
|
||||
stringsWrapper: '\'',
|
||||
tableAdd: true,
|
||||
viewAdd: true,
|
||||
triggerAdd: true,
|
||||
schemaEdit: false,
|
||||
tableSettings: true,
|
||||
tableRealCount: true,
|
||||
viewSettings: true,
|
||||
triggerSettings: true,
|
||||
indexes: true,
|
||||
foreigns: true,
|
||||
sortableFields: true,
|
||||
nullable: true,
|
||||
nullablePrimary: true,
|
||||
triggerSql: 'BEGIN\r\n\r\nEND',
|
||||
readOnlyMode: true
|
||||
};
|
@@ -66,6 +66,7 @@ module.exports = [
|
||||
{
|
||||
name: 'DECIMAL',
|
||||
length: true,
|
||||
scale: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
@@ -120,7 +121,7 @@ module.exports = [
|
||||
{
|
||||
name: 'JSON',
|
||||
length: false,
|
||||
collation: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
}
|
||||
@@ -218,56 +219,56 @@ module.exports = [
|
||||
types: [
|
||||
{
|
||||
name: 'POINT',
|
||||
length: true,
|
||||
length: false,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'LINESTRING',
|
||||
length: true,
|
||||
length: false,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'POLYGON',
|
||||
length: true,
|
||||
length: false,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'GEOMETRY',
|
||||
length: true,
|
||||
length: false,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'MULTIPOINT',
|
||||
length: true,
|
||||
length: false,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'MULTILINESTRING',
|
||||
length: true,
|
||||
length: false,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'MULTIPOLYGON',
|
||||
length: true,
|
||||
length: false,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'GEOMETRYCOLLECTION',
|
||||
length: true,
|
||||
name: 'GEOMCOLLECTION',
|
||||
length: false,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
|
@@ -22,11 +22,6 @@ module.exports = [
|
||||
length: false,
|
||||
unsigned: true
|
||||
},
|
||||
{
|
||||
name: 'NUMERIC',
|
||||
length: true,
|
||||
unsigned: true
|
||||
},
|
||||
{
|
||||
name: 'SMALLSERIAL',
|
||||
length: false,
|
||||
@@ -52,6 +47,12 @@ module.exports = [
|
||||
length: false,
|
||||
unsigned: true
|
||||
},
|
||||
{
|
||||
name: 'NUMERIC',
|
||||
length: true,
|
||||
unsigned: true,
|
||||
scale: true
|
||||
},
|
||||
{
|
||||
name: 'DOUBLE PRECISION',
|
||||
length: false,
|
||||
|
137
src/common/data-types/sqlite.js
Normal file
@@ -0,0 +1,137 @@
|
||||
module.exports = [
|
||||
{
|
||||
group: 'integer',
|
||||
types: [
|
||||
{
|
||||
name: 'INT',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: true,
|
||||
zerofill: true
|
||||
},
|
||||
{
|
||||
name: 'INTEGER',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: true,
|
||||
zerofill: true
|
||||
},
|
||||
{
|
||||
name: 'BIGINT',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: true,
|
||||
zerofill: true
|
||||
},
|
||||
{
|
||||
name: 'NUMERIC',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: true,
|
||||
zerofill: true
|
||||
},
|
||||
{
|
||||
name: 'BOOLEAN',
|
||||
length: false,
|
||||
collation: false,
|
||||
unsigned: true,
|
||||
zerofill: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
group: 'float',
|
||||
types: [
|
||||
{
|
||||
name: 'FLOAT',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'REAL',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
group: 'string',
|
||||
types: [
|
||||
{
|
||||
name: 'CHAR',
|
||||
length: true,
|
||||
collation: true,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'VARCHAR',
|
||||
length: true,
|
||||
collation: true,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'TEXT',
|
||||
length: true,
|
||||
collation: true,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
group: 'binary',
|
||||
types: [
|
||||
{
|
||||
name: 'BLOB',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
group: 'time',
|
||||
types: [
|
||||
{
|
||||
name: 'DATE',
|
||||
length: false,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'TIME',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'DATETIME',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
group: 'other',
|
||||
types: [
|
||||
{
|
||||
name: 'NONE',
|
||||
length: false,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
@@ -4,10 +4,13 @@ export const TEXT = [
|
||||
'CHARACTER',
|
||||
'CHARACTER VARYING'
|
||||
];
|
||||
|
||||
export const LONG_TEXT = [
|
||||
'TEXT',
|
||||
'MEDIUMTEXT',
|
||||
'LONGTEXT'
|
||||
'LONGTEXT',
|
||||
'JSON',
|
||||
'VARBINARY'
|
||||
];
|
||||
|
||||
export const ARRAY = [
|
||||
@@ -38,6 +41,7 @@ export const NUMBER = [
|
||||
|
||||
export const FLOAT = [
|
||||
'FLOAT',
|
||||
'DECIMAL',
|
||||
'DOUBLE',
|
||||
'REAL',
|
||||
'DOUBLE PRECISION',
|
||||
@@ -50,6 +54,7 @@ export const BOOLEAN = [
|
||||
];
|
||||
|
||||
export const DATE = ['DATE'];
|
||||
|
||||
export const TIME = [
|
||||
'TIME',
|
||||
'TIME WITH TIME ZONE'
|
||||
@@ -80,3 +85,24 @@ export const BIT = [
|
||||
'BIT',
|
||||
'BIT VARYING'
|
||||
];
|
||||
|
||||
export const SPATIAL = [
|
||||
'POINT',
|
||||
'LINESTRING',
|
||||
'POLYGON',
|
||||
'GEOMETRY',
|
||||
'MULTIPOINT',
|
||||
'MULTILINESTRING',
|
||||
'MULTIPOLYGON',
|
||||
'GEOMCOLLECTION',
|
||||
'GEOMETRYCOLLECTION'
|
||||
];
|
||||
|
||||
// Used to check multi spatial fields only
|
||||
export const IS_MULTI_SPATIAL = [
|
||||
'MULTIPOINT',
|
||||
'MULTILINESTRING',
|
||||
'MULTIPOLYGON',
|
||||
'GEOMCOLLECTION',
|
||||
'GEOMETRYCOLLECTION'
|
||||
];
|
||||
|
5
src/common/index-types/sqlite.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = [
|
||||
'PRIMARY',
|
||||
'INDEX',
|
||||
'UNIQUE'
|
||||
];
|
10
src/common/libs/getArrayDepth.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
*
|
||||
* @param {any[]} array
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getArrayDepth (array) {
|
||||
return Array.isArray(array)
|
||||
? 1 + Math.max(0, ...array.map(getArrayDepth))
|
||||
: 0;
|
||||
}
|
@@ -33,7 +33,7 @@ const lookup = {
|
||||
*/
|
||||
export default function hexToBinary (hex) {
|
||||
let binary = '';
|
||||
for (let i = 0, len = hex.length; i < len; i++)
|
||||
for (let i = 0; i < hex.length; i++)
|
||||
binary += lookup[hex[i]];
|
||||
|
||||
return binary;
|
||||
|
@@ -23,7 +23,7 @@ export function mimeFromHex (hex) {
|
||||
case '425A68':
|
||||
return { ext: 'bz2', mime: 'application/x-bzip2' };
|
||||
default:
|
||||
switch (hex) { // 4 bites
|
||||
switch (hex) { // 4 bytes
|
||||
case '89504E47':
|
||||
return { ext: 'png', mime: 'image/png' };
|
||||
case '47494638':
|
||||
|
93
src/common/libs/sqlParser.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Transform } from 'stream';
|
||||
|
||||
export default class SqlParser extends Transform {
|
||||
constructor (opts) {
|
||||
opts = {
|
||||
delimiter: ';',
|
||||
encoding: 'utf8',
|
||||
writableObjectMode: true,
|
||||
readableObjectMode: true,
|
||||
...opts
|
||||
};
|
||||
super(opts);
|
||||
this._buffer = '';
|
||||
this._lastChar = '';
|
||||
this._last9Chars = '';
|
||||
this.encoding = opts.encoding;
|
||||
this.delimiter = opts.delimiter;
|
||||
|
||||
this.isEscape = false;
|
||||
this.currentQuote = null;
|
||||
this.isDelimiter = false;
|
||||
}
|
||||
|
||||
_transform (chunk, encoding, next) {
|
||||
for (const char of chunk.toString(this.encoding)) {
|
||||
this.checkEscape();
|
||||
this._buffer += char;
|
||||
this._lastChar = char;
|
||||
this._last9Chars += char.trim().toLocaleLowerCase();
|
||||
|
||||
if (this._last9Chars.length > 9)
|
||||
this._last9Chars = this._last9Chars.slice(-9);
|
||||
|
||||
this.checkNewDelimiter(char);
|
||||
this.checkQuote(char);
|
||||
const query = this.getQuery();
|
||||
|
||||
if (query)
|
||||
this.push(query);
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
checkEscape () {
|
||||
if (this._buffer.length > 0) {
|
||||
this.isEscape = this._lastChar === '\\'
|
||||
? !this.isEscape
|
||||
: false;
|
||||
}
|
||||
}
|
||||
|
||||
checkNewDelimiter (char) {
|
||||
if (this.currentQuote === null && this._last9Chars === 'delimiter') {
|
||||
this.isDelimiter = true;
|
||||
this._buffer = '';
|
||||
}
|
||||
else {
|
||||
const isNewLine = char === '\n' || char === '\r';
|
||||
if (isNewLine && this.isDelimiter) {
|
||||
this.isDelimiter = false;
|
||||
this.delimiter = this._buffer.trim();
|
||||
this._buffer = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkQuote (char) {
|
||||
const isQuote = !this.isEscape && (char === '\'' || char === '"');
|
||||
if (isQuote && this.currentQuote === char)
|
||||
this.currentQuote = null;
|
||||
|
||||
else if (isQuote && this.currentQuote === null)
|
||||
this.currentQuote = char;
|
||||
}
|
||||
|
||||
getQuery () {
|
||||
if (this.isDelimiter)
|
||||
return false;
|
||||
|
||||
let query = false;
|
||||
let demiliterFound = false;
|
||||
if (this.currentQuote === null && this._buffer.length >= this.delimiter.length)
|
||||
demiliterFound = this._last9Chars.slice(-this.delimiter.length) === this.delimiter;
|
||||
|
||||
if (demiliterFound) {
|
||||
const parsedStr = this._buffer.trim();
|
||||
query = parsedStr.slice(0, parsedStr.length - this.delimiter.length);
|
||||
this._buffer = '';
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
@@ -1,103 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import { app, BrowserWindow, /* session, */ nativeImage, Menu } from 'electron';
|
||||
import * as path from 'path';
|
||||
import Store from 'electron-store';
|
||||
|
||||
import ipcHandlers from './ipc-handlers';
|
||||
|
||||
Store.initRenderer();
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
|
||||
|
||||
// global reference to mainWindow (necessary to prevent window from being garbage collected)
|
||||
let mainWindow;
|
||||
|
||||
async function createMainWindow () {
|
||||
const icon = require('../renderer/images/logo-32.png');
|
||||
const window = new BrowserWindow({
|
||||
width: 1024,
|
||||
height: 800,
|
||||
minWidth: 900,
|
||||
minHeight: 550,
|
||||
title: 'Antares',
|
||||
autoHideMenuBar: true,
|
||||
icon: nativeImage.createFromDataURL(icon.default),
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
'web-security': false,
|
||||
enableRemoteModule: true,
|
||||
spellcheck: false
|
||||
},
|
||||
frame: false,
|
||||
backgroundColor: '#1d1d1d'
|
||||
});
|
||||
|
||||
try {
|
||||
if (isDevelopment) { //
|
||||
await window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`);
|
||||
|
||||
// const { default: installExtension, VUEJS3_DEVTOOLS } = require('electron-devtools-installer');
|
||||
|
||||
// const oldDevToolsID = session.defaultSession.getAllExtensions().find(ext => ext.name === 'Vue.js devtools').id;
|
||||
// session.defaultSession.removeExtension(oldDevToolsID);
|
||||
// const toolName = await installExtension(VUEJS3_DEVTOOLS);
|
||||
// console.log(toolName, 'installed');
|
||||
}
|
||||
else
|
||||
await window.loadURL(new URL(`file:///${path.join(__dirname, 'index.html')}`).href);
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
window.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
window.webContents.on('devtools-opened', () => {
|
||||
window.focus();
|
||||
setImmediate(() => {
|
||||
window.focus();
|
||||
});
|
||||
});
|
||||
|
||||
return window;
|
||||
};
|
||||
|
||||
if (!gotTheLock)
|
||||
app.quit();
|
||||
else {
|
||||
require('@electron/remote/main').initialize();
|
||||
|
||||
// Initialize ipcHandlers
|
||||
ipcHandlers();
|
||||
|
||||
// quit application when all windows are closed
|
||||
app.on('window-all-closed', () => {
|
||||
// on macOS it is common for applications to stay open until the user explicitly quits
|
||||
if (process.platform !== 'darwin')
|
||||
app.quit();
|
||||
});
|
||||
|
||||
app.on('activate', async () => {
|
||||
// on macOS it is common to re-create a window even after all windows have been closed
|
||||
if (mainWindow === null) {
|
||||
mainWindow = await createMainWindow();
|
||||
if (isDevelopment)
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
});
|
||||
|
||||
// create main BrowserWindow when electron is ready
|
||||
app.on('ready', async () => {
|
||||
mainWindow = await createMainWindow();
|
||||
Menu.setApplicationMenu(null);
|
||||
if (isDevelopment)
|
||||
mainWindow.webContents.openDevTools();
|
||||
});
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
import { app, ipcMain } from 'electron';
|
||||
import { app, ipcMain, dialog } from 'electron';
|
||||
|
||||
export default () => {
|
||||
ipcMain.on('close-app', () => {
|
||||
@@ -9,4 +9,12 @@ export default () => {
|
||||
const key = false;
|
||||
event.returnValue = key;
|
||||
});
|
||||
|
||||
ipcMain.handle('showOpenDialog', (event, options) => {
|
||||
return dialog.showOpenDialog(options);
|
||||
});
|
||||
|
||||
ipcMain.handle('get-download-dir-path', () => {
|
||||
return app.getPath('downloads');
|
||||
});
|
||||
};
|
||||
|
@@ -9,12 +9,16 @@ export default connections => {
|
||||
port: +conn.port,
|
||||
user: conn.user,
|
||||
password: conn.password,
|
||||
application_name: 'Antares SQL'
|
||||
application_name: 'Antares SQL',
|
||||
readonly: conn.readonly
|
||||
};
|
||||
|
||||
if (conn.database)
|
||||
params.database = conn.database;
|
||||
|
||||
if (conn.databasePath)
|
||||
params.databasePath = conn.databasePath;
|
||||
|
||||
if (conn.ssl) {
|
||||
params.ssl = {
|
||||
key: conn.key ? fs.readFileSync(conn.key) : null,
|
||||
@@ -30,12 +34,13 @@ export default connections => {
|
||||
username: conn.sshUser,
|
||||
password: conn.sshPass,
|
||||
port: conn.sshPort ? conn.sshPort : 22,
|
||||
identity: conn.sshKey
|
||||
privateKey: conn.sshKey ? fs.readFileSync(conn.sshKey) : null,
|
||||
passphrase: conn.sshPassphrase
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const connection = await ClientsFactory.getConnection({
|
||||
const connection = await ClientsFactory.getClient({
|
||||
client: conn.client,
|
||||
params
|
||||
});
|
||||
@@ -47,7 +52,7 @@ export default connections => {
|
||||
return { status: 'success' };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err };
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -61,12 +66,16 @@ export default connections => {
|
||||
port: +conn.port,
|
||||
user: conn.user,
|
||||
password: conn.password,
|
||||
application_name: 'Antares SQL'
|
||||
application_name: 'Antares SQL',
|
||||
readonly: conn.readonly
|
||||
};
|
||||
|
||||
if (conn.database)
|
||||
params.database = conn.database;
|
||||
|
||||
if (conn.databasePath)
|
||||
params.databasePath = conn.databasePath;
|
||||
|
||||
if (conn.schema)
|
||||
params.schema = conn.schema;
|
||||
|
||||
@@ -85,12 +94,13 @@ export default connections => {
|
||||
username: conn.sshUser,
|
||||
password: conn.sshPass,
|
||||
port: conn.sshPort ? conn.sshPort : 22,
|
||||
identity: conn.sshKey
|
||||
privateKey: conn.sshKey ? fs.readFileSync(conn.sshKey) : null,
|
||||
passphrase: conn.sshPassphrase
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const connection = ClientsFactory.getConnection({
|
||||
const connection = ClientsFactory.getClient({
|
||||
client: conn.client,
|
||||
params,
|
||||
poolSize: 5
|
||||
|
@@ -40,4 +40,17 @@ export default (connections) => {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('toggle-scheduler', async (event, params) => {
|
||||
try {
|
||||
if (!params.enabled)
|
||||
await connections[params.uid].enableEvent({ ...params });
|
||||
else
|
||||
await connections[params.uid].disableEvent({ ...params });
|
||||
return { status: 'success' };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -1,7 +1,15 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fork } from 'child_process';
|
||||
import { ipcMain, dialog } from 'electron';
|
||||
|
||||
import { ipcMain } from 'electron';
|
||||
// @TODO: need some factories
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
|
||||
export default connections => {
|
||||
let exporter = null;
|
||||
let importer = null;
|
||||
|
||||
ipcMain.handle('create-schema', async (event, params) => {
|
||||
try {
|
||||
await connections[params.uid].createSchema(params);
|
||||
@@ -37,9 +45,16 @@ export default connections => {
|
||||
|
||||
ipcMain.handle('get-schema-collation', async (event, params) => {
|
||||
try {
|
||||
const collation = await connections[params.uid].getDatabaseCollation(params);
|
||||
const collation = await connections[params.uid].getDatabaseCollation(
|
||||
params
|
||||
);
|
||||
|
||||
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
|
||||
: ''
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
@@ -48,7 +63,9 @@ export default connections => {
|
||||
|
||||
ipcMain.handle('get-structure', async (event, params) => {
|
||||
try {
|
||||
const structure = await connections[params.uid].getStructure(params.schemas);
|
||||
const structure = await connections[params.uid].getStructure(
|
||||
params.schemas
|
||||
);
|
||||
|
||||
return { status: 'success', response: structure };
|
||||
}
|
||||
@@ -112,6 +129,17 @@ export default connections => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('kill-process', async (event, { uid, pid }) => {
|
||||
try {
|
||||
const result = await connections[uid].killProcess(pid);
|
||||
|
||||
return { status: 'success', response: result };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('use-schema', async (event, { uid, schema }) => {
|
||||
if (!schema) return;
|
||||
|
||||
@@ -124,7 +152,7 @@ export default connections => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('raw-query', async (event, { uid, query, schema }) => {
|
||||
ipcMain.handle('raw-query', async (event, { uid, query, schema, tabUid, autocommit }) => {
|
||||
if (!query) return;
|
||||
|
||||
try {
|
||||
@@ -132,6 +160,8 @@ export default connections => {
|
||||
nest: true,
|
||||
details: true,
|
||||
schema,
|
||||
tabUid,
|
||||
autocommit,
|
||||
comments: false
|
||||
});
|
||||
|
||||
@@ -141,4 +171,215 @@ export default connections => {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('export', (event, { uid, type, tables, ...rest }) => {
|
||||
if (exporter !== null) return;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
if (fs.existsSync(rest.outputFile)) { // If file exists ask for replace
|
||||
const result = await dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
message: `File ${rest.outputFile} already exists. Do you want to replace it?`,
|
||||
detail: 'A file with the same name already exists in the target folder. Replacing it will overwrite its current contents.',
|
||||
buttons: ['Cancel', 'Replace'],
|
||||
defaultId: 0,
|
||||
cancelId: 0
|
||||
});
|
||||
|
||||
if (result.response !== 1) {
|
||||
resolve({
|
||||
type: 'error',
|
||||
response: 'Operation aborted'
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Init exporter process
|
||||
exporter = fork(isDevelopment ? './dist/exporter.js' : path.resolve(__dirname, './exporter.js'), [], {
|
||||
execArgv: isDevelopment ? ['--inspect=9224'] : undefined
|
||||
});
|
||||
exporter.send({
|
||||
type: 'init',
|
||||
client: {
|
||||
name: type,
|
||||
config: await connections[uid].getDbConfig()
|
||||
},
|
||||
tables,
|
||||
options: rest
|
||||
});
|
||||
|
||||
// Exporter message listener
|
||||
exporter.on('message', ({ type, payload }) => {
|
||||
switch (type) {
|
||||
case 'export-progress':
|
||||
event.sender.send('export-progress', payload);
|
||||
break;
|
||||
case 'end':
|
||||
setTimeout(() => { // Ensures that writing process has finished
|
||||
exporter.kill();
|
||||
exporter = null;
|
||||
}, 2000);
|
||||
resolve({ status: 'success', response: payload });
|
||||
break;
|
||||
case 'cancel':
|
||||
exporter.kill();
|
||||
exporter = null;
|
||||
resolve({ status: 'error', response: 'Operation cancelled' });
|
||||
break;
|
||||
case 'error':
|
||||
exporter.kill();
|
||||
exporter = null;
|
||||
resolve({ status: 'error', response: payload });
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
exporter.on('exit', code => {
|
||||
exporter = null;
|
||||
resolve({ status: 'error', response: `Operation ended with code: ${code}` });
|
||||
});
|
||||
})();
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('abort-export', async event => {
|
||||
let willAbort = false;
|
||||
|
||||
if (exporter) {
|
||||
const result = await dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
message: 'Are you sure you want to abort the export',
|
||||
buttons: ['Cancel', 'Abort'],
|
||||
defaultId: 0,
|
||||
cancelId: 0
|
||||
});
|
||||
|
||||
if (result.response === 1) {
|
||||
willAbort = true;
|
||||
exporter.send({ type: 'cancel' });
|
||||
}
|
||||
}
|
||||
|
||||
return { status: 'success', response: { willAbort } };
|
||||
});
|
||||
|
||||
ipcMain.handle('import-sql', async (event, options) => {
|
||||
if (importer !== null) return;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
const dbConfig = await connections[options.uid].getDbConfig();
|
||||
|
||||
// Init importer process
|
||||
importer = fork(isDevelopment ? './dist/importer.js' : path.resolve(__dirname, './importer.js'), [], {
|
||||
execArgv: isDevelopment ? ['--inspect=9224'] : undefined
|
||||
});
|
||||
importer.send({
|
||||
type: 'init',
|
||||
dbConfig,
|
||||
options
|
||||
});
|
||||
|
||||
// Importer message listener
|
||||
importer.on('message', ({ type, payload }) => {
|
||||
switch (type) {
|
||||
case 'import-progress':
|
||||
event.sender.send('import-progress', payload);
|
||||
break;
|
||||
case 'query-error':
|
||||
event.sender.send('query-error', payload);
|
||||
break;
|
||||
case 'end':
|
||||
setTimeout(() => { // Ensures that writing process has finished
|
||||
importer?.kill();
|
||||
importer = null;
|
||||
}, 2000);
|
||||
resolve({ status: 'success', response: payload });
|
||||
break;
|
||||
case 'cancel':
|
||||
importer.kill();
|
||||
importer = null;
|
||||
resolve({ status: 'error', response: 'Operation cancelled' });
|
||||
break;
|
||||
case 'error':
|
||||
importer.kill();
|
||||
importer = null;
|
||||
resolve({ status: 'error', response: payload });
|
||||
break;
|
||||
}
|
||||
});
|
||||
})();
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('abort-import-sql', async event => {
|
||||
let willAbort = false;
|
||||
|
||||
if (importer) {
|
||||
const result = await dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
message: 'Are you sure you want to abort the import',
|
||||
buttons: ['Cancel', 'Abort'],
|
||||
defaultId: 0,
|
||||
cancelId: 0
|
||||
});
|
||||
|
||||
if (result.response === 1) {
|
||||
willAbort = true;
|
||||
importer.send({ type: 'cancel' });
|
||||
}
|
||||
}
|
||||
|
||||
return { status: 'success', response: { willAbort } };
|
||||
});
|
||||
|
||||
ipcMain.handle('kill-tab-query', async (event, { uid, tabUid }) => {
|
||||
if (!tabUid) return;
|
||||
|
||||
try {
|
||||
await connections[uid].killTabQuery(tabUid);
|
||||
return { status: 'success' };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('commit-tab', async (event, { uid, tabUid }) => {
|
||||
if (!tabUid) return;
|
||||
|
||||
try {
|
||||
await connections[uid].commitTab(tabUid);
|
||||
return { status: 'success' };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('rollback-tab', async (event, { uid, tabUid }) => {
|
||||
if (!tabUid) return;
|
||||
|
||||
try {
|
||||
await connections[uid].rollbackTab(tabUid);
|
||||
return { status: 'success' };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('destroy-connection-to-commit', async (event, { uid, tabUid }) => {
|
||||
if (!tabUid) return;
|
||||
|
||||
try {
|
||||
await connections[uid].destroyConnectionToCommit(tabUid);
|
||||
return { status: 'success' };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -3,6 +3,7 @@ import faker from 'faker';
|
||||
import moment from 'moment';
|
||||
import { sqlEscaper } from 'common/libs/sqlEscaper';
|
||||
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes';
|
||||
import * as customizations from 'common/customizations';
|
||||
import fs from 'fs';
|
||||
|
||||
export default (connections) => {
|
||||
@@ -16,7 +17,7 @@ export default (connections) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-table-data', async (event, { uid, schema, table, limit, page, sortParams }) => {
|
||||
ipcMain.handle('get-table-data', async (event, { uid, schema, table, limit, page, sortParams, where }) => {
|
||||
try {
|
||||
const offset = (page - 1) * limit;
|
||||
const query = connections[uid]
|
||||
@@ -29,6 +30,9 @@ export default (connections) => {
|
||||
if (sortParams && sortParams.field && sortParams.dir)
|
||||
query.orderBy({ [sortParams.field]: sortParams.dir.toUpperCase() });
|
||||
|
||||
if (where)
|
||||
query.where(where);
|
||||
|
||||
const result = await query.run({ details: true, schema });
|
||||
|
||||
return { status: 'success', response: result };
|
||||
@@ -48,6 +52,16 @@ export default (connections) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-table-options', async (event, params) => {
|
||||
try {
|
||||
const result = await connections[params.uid].getTableOptions(params);
|
||||
return { status: 'success', response: result };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-table-indexes', async (event, params) => {
|
||||
try {
|
||||
const result = await connections[params.uid].getTableIndexes(params);
|
||||
@@ -71,12 +85,13 @@ export default (connections) => {
|
||||
});
|
||||
|
||||
ipcMain.handle('update-table-cell', async (event, params) => {
|
||||
delete params.row._id;
|
||||
delete params.row._antares_id;
|
||||
const { stringsWrapper: sw } = customizations[connections[params.uid]._client];
|
||||
|
||||
try { // TODO: move to client classes
|
||||
let escapedParam;
|
||||
let reload = false;
|
||||
const id = typeof params.id === 'number' ? params.id : `"${params.id}"`;
|
||||
const id = typeof params.id === 'number' ? params.id : `${sw}${params.id}${sw}`;
|
||||
|
||||
if ([...NUMBER, ...FLOAT].includes(params.type))
|
||||
escapedParam = params.content;
|
||||
@@ -89,6 +104,9 @@ export default (connections) => {
|
||||
case 'pg':
|
||||
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
|
||||
break;
|
||||
case 'sqlite':
|
||||
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (ARRAY.includes(params.type))
|
||||
@@ -109,6 +127,10 @@ export default (connections) => {
|
||||
fileBlob = fs.readFileSync(params.content);
|
||||
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
|
||||
break;
|
||||
case 'sqlite':
|
||||
fileBlob = fs.readFileSync(params.content);
|
||||
escapedParam = `X'${fileBlob.toString('hex')}'`;
|
||||
break;
|
||||
}
|
||||
reload = true;
|
||||
}
|
||||
@@ -121,10 +143,13 @@ export default (connections) => {
|
||||
case 'pg':
|
||||
escapedParam = 'decode(\'\', \'hex\')';
|
||||
break;
|
||||
case 'sqlite':
|
||||
escapedParam = 'X\'\'';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ([...BIT].includes(params.type)) {
|
||||
else if (BIT.includes(params.type)) {
|
||||
escapedParam = `b'${sqlEscaper(params.content)}'`;
|
||||
reload = true;
|
||||
}
|
||||
@@ -175,7 +200,7 @@ export default (connections) => {
|
||||
const fieldName = Object.keys(row)[0].includes('.') ? `${params.table}.${params.primary}` : params.primary;
|
||||
|
||||
return typeof row[fieldName] === 'string'
|
||||
? `"${row[fieldName]}"`
|
||||
? `'${row[fieldName]}'`
|
||||
: row[fieldName];
|
||||
}).join(',');
|
||||
|
||||
|
@@ -40,4 +40,17 @@ export default (connections) => {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('toggle-trigger', async (event, params) => {
|
||||
try {
|
||||
if (!params.enabled)
|
||||
await connections[params.uid].enableTrigger(params);
|
||||
else
|
||||
await connections[params.uid].disableTrigger(params);
|
||||
return { status: 'success' };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -2,6 +2,7 @@ import { ipcMain } from 'electron';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import Store from 'electron-store';
|
||||
const persistentStore = new Store({ name: 'settings' });
|
||||
const isMacOS = process.platform === 'darwin';
|
||||
|
||||
let mainWindow;
|
||||
autoUpdater.allowPrerelease = persistentStore.get('allow_prerelease', true);
|
||||
@@ -9,8 +10,11 @@ autoUpdater.allowPrerelease = persistentStore.get('allow_prerelease', true);
|
||||
export default () => {
|
||||
ipcMain.on('check-for-updates', event => {
|
||||
mainWindow = event;
|
||||
if (process.windowsStore)
|
||||
if (process.windowsStore || (process.platform === 'linux' && !process.env.APPIMAGE))
|
||||
mainWindow.reply('no-auto-update');
|
||||
else if (isMacOS) { // Temporary solution on MacOS for unsigned app updates
|
||||
autoUpdater.autoDownload = false;
|
||||
}
|
||||
else {
|
||||
autoUpdater.checkForUpdatesAndNotify().catch(() => {
|
||||
mainWindow.reply('check-failed');
|
||||
@@ -28,7 +32,10 @@ export default () => {
|
||||
});
|
||||
|
||||
autoUpdater.on('update-available', () => {
|
||||
mainWindow.reply('update-available');
|
||||
if (isMacOS)
|
||||
mainWindow.reply('link-to-download');
|
||||
else
|
||||
mainWindow.reply('update-available');
|
||||
});
|
||||
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
|
@@ -1,4 +1,10 @@
|
||||
'use strict';
|
||||
const queryLogger = sql => {
|
||||
// Remove comments, newlines and multiple spaces
|
||||
const escapedSql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '').replace(/\s\s+/g, ' ');
|
||||
console.log(escapedSql);
|
||||
};
|
||||
|
||||
/**
|
||||
* As Simple As Possible Query Builder Core
|
||||
*
|
||||
@@ -16,7 +22,8 @@ export class AntaresCore {
|
||||
this._params = args.params;
|
||||
this._poolSize = args.poolSize || false;
|
||||
this._connection = null;
|
||||
this._logger = args.logger || console.log;
|
||||
this._ssh = null;
|
||||
this._logger = args.logger || queryLogger;
|
||||
|
||||
this._queryDefaults = {
|
||||
schema: '',
|
||||
|
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
import { MySQLClient } from './clients/MySQLClient';
|
||||
import { PostgreSQLClient } from './clients/PostgreSQLClient';
|
||||
|
||||
import { SQLiteClient } from './clients/SQLiteClient';
|
||||
export class ClientsFactory {
|
||||
/**
|
||||
* Returns a database connection based on received args.
|
||||
@@ -22,13 +22,15 @@ export class ClientsFactory {
|
||||
* @returns Database Connection
|
||||
* @memberof ClientsFactory
|
||||
*/
|
||||
static getConnection (args) {
|
||||
static getClient (args) {
|
||||
switch (args.client) {
|
||||
case 'mysql':
|
||||
case 'maria':
|
||||
return new MySQLClient(args);
|
||||
case 'pg':
|
||||
return new PostgreSQLClient(args);
|
||||
case 'sqlite':
|
||||
return new SQLiteClient(args);
|
||||
default:
|
||||
throw new Error(`Unknown database client: ${args.client}`);
|
||||
}
|
||||
|
@@ -9,6 +9,8 @@ export class MySQLClient extends AntaresCore {
|
||||
super(args);
|
||||
|
||||
this._schema = null;
|
||||
this._runningConnections = new Map();
|
||||
this._connectionsToCommit = new Map();
|
||||
|
||||
this.types = {
|
||||
0: 'DECIMAL',
|
||||
@@ -99,10 +101,32 @@ export class MySQLClient extends AntaresCore {
|
||||
.filter(_type => _type.name === type.toUpperCase())[0];
|
||||
}
|
||||
|
||||
_reducer (acc, curr) {
|
||||
const type = typeof curr;
|
||||
|
||||
switch (type) {
|
||||
case 'number':
|
||||
case 'string':
|
||||
return [...acc, curr];
|
||||
case 'object':
|
||||
if (Array.isArray(curr))
|
||||
return [...acc, ...curr];
|
||||
else {
|
||||
const clausoles = [];
|
||||
for (const key in curr)
|
||||
clausoles.push(`\`${key}\` ${curr[key]}`);
|
||||
|
||||
return clausoles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns dbConfig
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async connect () {
|
||||
async getDbConfig () {
|
||||
delete this._params.application_name;
|
||||
|
||||
const dbConfig = {
|
||||
@@ -110,7 +134,9 @@ export class MySQLClient extends AntaresCore {
|
||||
port: this._params.port,
|
||||
user: this._params.user,
|
||||
password: this._params.password,
|
||||
ssl: null
|
||||
ssl: null,
|
||||
supportBigNumbers: true,
|
||||
bigNumberStrings: true
|
||||
};
|
||||
|
||||
if (this._params.schema?.length) dbConfig.database = this._params.schema;
|
||||
@@ -118,29 +144,32 @@ export class MySQLClient extends AntaresCore {
|
||||
if (this._params.ssl) dbConfig.ssl = { ...this._params.ssl };
|
||||
|
||||
if (this._params.ssh) {
|
||||
this._ssh = new SSH2Promise({ ...this._params.ssh });
|
||||
try {
|
||||
this._ssh = new SSH2Promise({ ...this._params.ssh });
|
||||
|
||||
this._tunnel = await this._ssh.addTunnel({
|
||||
remoteAddr: this._params.host,
|
||||
remotePort: this._params.port
|
||||
});
|
||||
dbConfig.port = this._tunnel.localPort;
|
||||
const tunnel = await this._ssh.addTunnel({
|
||||
remoteAddr: this._params.host,
|
||||
remotePort: this._params.port
|
||||
});
|
||||
dbConfig.port = tunnel.localPort;
|
||||
}
|
||||
catch (err) {
|
||||
if (this._ssh) this._ssh.close();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return dbConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async connect () {
|
||||
if (!this._poolSize)
|
||||
this._connection = await mysql.createConnection(dbConfig);
|
||||
else {
|
||||
this._connection = mysql.createPool({
|
||||
...dbConfig,
|
||||
connectionLimit: this._poolSize,
|
||||
typeCast: (field, next) => {
|
||||
if (field.type === 'DATETIME')
|
||||
return field.string();
|
||||
else
|
||||
return next();
|
||||
}
|
||||
});
|
||||
}
|
||||
this._connection = await this.getConnection();
|
||||
else
|
||||
this._connection = await this.getConnectionPool();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,6 +180,64 @@ export class MySQLClient extends AntaresCore {
|
||||
if (this._ssh) this._ssh.close();
|
||||
}
|
||||
|
||||
async getConnection () {
|
||||
const dbConfig = await this.getDbConfig();
|
||||
const connection = await mysql.createConnection({
|
||||
...dbConfig,
|
||||
typeCast: (field, next) => {
|
||||
if (field.type === 'DATETIME')
|
||||
return field.string();
|
||||
else
|
||||
return next();
|
||||
}
|
||||
});
|
||||
|
||||
// ANSI_QUOTES check
|
||||
const [res] = await connection.query('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\'');
|
||||
const sqlMode = res[0]?.Variable_name?.split(',');
|
||||
const hasAnsiQuotes = sqlMode.includes('ANSI_QUOTES');
|
||||
|
||||
if (this._params.readonly)
|
||||
await connection.query('SET SESSION TRANSACTION READ ONLY');
|
||||
|
||||
if (hasAnsiQuotes)
|
||||
await connection.query(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`);
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
async getConnectionPool () {
|
||||
const dbConfig = await this.getDbConfig();
|
||||
const connection = mysql.createPool({
|
||||
...dbConfig,
|
||||
connectionLimit: this._poolSize,
|
||||
typeCast: (field, next) => {
|
||||
if (field.type === 'DATETIME')
|
||||
return field.string();
|
||||
else
|
||||
return next();
|
||||
}
|
||||
});
|
||||
|
||||
// ANSI_QUOTES check
|
||||
const [res] = await connection.query('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\'');
|
||||
const sqlMode = res[0]?.Variable_name?.split(',');
|
||||
const hasAnsiQuotes = sqlMode.includes('ANSI_QUOTES');
|
||||
|
||||
if (hasAnsiQuotes)
|
||||
await connection.query(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`);
|
||||
|
||||
connection.on('connection', conn => {
|
||||
if (this._params.readonly)
|
||||
conn.query('SET SESSION TRANSACTION READ ONLY');
|
||||
|
||||
if (hasAnsiQuotes)
|
||||
conn.query(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`);
|
||||
});
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an USE query
|
||||
*
|
||||
@@ -181,6 +268,7 @@ export class MySQLClient extends AntaresCore {
|
||||
|
||||
const tablesArr = [];
|
||||
const triggersArr = [];
|
||||
let schemaSize = 0;
|
||||
|
||||
for (const db of filteredDatabases) {
|
||||
if (!schemas.has(db.Database)) continue;
|
||||
@@ -218,6 +306,9 @@ export class MySQLClient extends AntaresCore {
|
||||
break;
|
||||
}
|
||||
|
||||
const tableSize = Number(table.Data_length) + Number(table.Index_length);
|
||||
schemaSize += tableSize;
|
||||
|
||||
return {
|
||||
name: table.Name,
|
||||
type: tableType,
|
||||
@@ -226,7 +317,7 @@ export class MySQLClient extends AntaresCore {
|
||||
updated: table.Update_time,
|
||||
engine: table.Engine,
|
||||
comment: table.Comment,
|
||||
size: table.Data_length + table.Index_length,
|
||||
size: tableSize,
|
||||
autoIncrement: table.Auto_increment,
|
||||
collation: table.Collation
|
||||
};
|
||||
@@ -270,7 +361,7 @@ export class MySQLClient extends AntaresCore {
|
||||
body: scheduler.EVENT_BODY,
|
||||
starts: scheduler.STARTS,
|
||||
ends: scheduler.ENDS,
|
||||
status: scheduler.STATUS,
|
||||
enabled: scheduler.STATUS === 'ENABLED',
|
||||
executeAt: scheduler.EXECUTE_AT,
|
||||
intervalField: scheduler.INTERVAL_FIELD,
|
||||
intervalValue: scheduler.INTERVAL_VALUE,
|
||||
@@ -303,6 +394,7 @@ export class MySQLClient extends AntaresCore {
|
||||
|
||||
return {
|
||||
name: db.Database,
|
||||
size: schemaSize,
|
||||
tables: remappedTables,
|
||||
functions: remappedFunctions,
|
||||
procedures: remappedProcedures,
|
||||
@@ -313,6 +405,7 @@ export class MySQLClient extends AntaresCore {
|
||||
else {
|
||||
return {
|
||||
name: db.Database,
|
||||
size: 0,
|
||||
tables: [],
|
||||
functions: [],
|
||||
procedures: [],
|
||||
@@ -354,7 +447,7 @@ export class MySQLClient extends AntaresCore {
|
||||
return acc;
|
||||
}, '')
|
||||
.replaceAll('\n', '')
|
||||
.split(',')
|
||||
.split(/,\s?(?![^(]*\))/)
|
||||
.map(f => {
|
||||
try {
|
||||
const fieldArr = f.trim().split(' ');
|
||||
@@ -394,18 +487,25 @@ export class MySQLClient extends AntaresCore {
|
||||
|
||||
return rows.map(field => {
|
||||
let numLength = field.COLUMN_TYPE.match(/int\(([^)]+)\)/);
|
||||
numLength = numLength ? +numLength.pop() : null;
|
||||
numLength = numLength ? +numLength.pop() : field.NUMERIC_PRECISION || null;
|
||||
const enumValues = /(enum|set)/.test(field.COLUMN_TYPE)
|
||||
? field.COLUMN_TYPE.match(/\(([^)]+)\)/)[0].slice(1, -1)
|
||||
: null;
|
||||
|
||||
const defaultValue = (remappedFields && remappedFields[field.COLUMN_NAME])
|
||||
? remappedFields[field.COLUMN_NAME].default
|
||||
: field.COLUMN_DEFAULT;
|
||||
|
||||
return {
|
||||
name: field.COLUMN_NAME,
|
||||
key: field.COLUMN_KEY.toLowerCase(),
|
||||
type: remappedFields ? remappedFields[field.COLUMN_NAME].type : field.DATA_TYPE,
|
||||
type: (remappedFields && remappedFields[field.COLUMN_NAME])
|
||||
? remappedFields[field.COLUMN_NAME].type
|
||||
: field.DATA_TYPE.toUpperCase(),
|
||||
schema: field.TABLE_SCHEMA,
|
||||
table: field.TABLE_NAME,
|
||||
numPrecision: field.NUMERIC_PRECISION,
|
||||
numScale: Number(field.NUMERIC_SCALE),
|
||||
numLength,
|
||||
enumValues,
|
||||
datePrecision: field.DATETIME_PRECISION,
|
||||
@@ -414,11 +514,14 @@ export class MySQLClient extends AntaresCore {
|
||||
unsigned: field.COLUMN_TYPE.includes('unsigned'),
|
||||
zerofill: field.COLUMN_TYPE.includes('zerofill'),
|
||||
order: field.ORDINAL_POSITION,
|
||||
default: remappedFields ? remappedFields[field.COLUMN_NAME].default : field.COLUMN_DEFAULT,
|
||||
default: defaultValue,
|
||||
charset: field.CHARACTER_SET_NAME,
|
||||
collation: field.COLLATION_NAME,
|
||||
autoIncrement: field.EXTRA.includes('auto_increment'),
|
||||
onUpdate: field.EXTRA.toLowerCase().includes('on update') ? field.EXTRA.replace('on update', '') : '',
|
||||
generated: field.EXTRA.toLowerCase().includes('generated'),
|
||||
onUpdate: field.EXTRA.toLowerCase().includes('on update')
|
||||
? field.EXTRA.substr(field.EXTRA.indexOf('on update') + 9, field.EXTRA.length).trim()
|
||||
: '',
|
||||
comment: field.COLUMN_COMMENT
|
||||
};
|
||||
});
|
||||
@@ -437,6 +540,43 @@ export class MySQLClient extends AntaresCore {
|
||||
return rows.length ? rows[0].count : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {String} params.schema
|
||||
* @param {String} params.table
|
||||
* @returns {Object} table options
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async getTableOptions ({ schema, table }) {
|
||||
const { rows } = await this.raw(`SHOW TABLE STATUS FROM \`${schema}\` WHERE Name = '${table}'`);
|
||||
|
||||
if (rows.length) {
|
||||
let tableType;
|
||||
switch (rows[0].Comment) {
|
||||
case 'VIEW':
|
||||
tableType = 'view';
|
||||
break;
|
||||
default:
|
||||
tableType = 'table';
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
name: rows[0].Name,
|
||||
type: tableType,
|
||||
rows: rows[0].Rows,
|
||||
created: rows[0].Create_time,
|
||||
updated: rows[0].Update_time,
|
||||
engine: rows[0].Engine,
|
||||
comment: rows[0].Comment,
|
||||
size: rows[0].Data_length + rows[0].Index_length,
|
||||
autoIncrement: rows[0].Auto_increment,
|
||||
collation: rows[0].Collation
|
||||
};
|
||||
};
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {String} params.schema
|
||||
@@ -522,7 +662,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* CREATE DATABASE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async createSchema (params) {
|
||||
@@ -532,7 +672,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* ALTER DATABASE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async alterSchema (params) {
|
||||
@@ -542,7 +682,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* DROP DATABASE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async dropSchema (params) {
|
||||
@@ -582,7 +722,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* DROP VIEW
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async dropView (params) {
|
||||
@@ -593,12 +733,17 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* ALTER VIEW
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async alterView (params) {
|
||||
const { view } = params;
|
||||
let sql = `ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''} SQL SECURITY ${view.security} VIEW \`${view.schema}\`.\`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}`;
|
||||
let sql = `
|
||||
USE \`${view.schema}\`;
|
||||
ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''}
|
||||
SQL SECURITY ${view.security}
|
||||
VIEW \`${view.schema}\`.\`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}
|
||||
`;
|
||||
|
||||
if (view.name !== view.oldName)
|
||||
sql += `; RENAME TABLE \`${view.schema}\`.\`${view.oldName}\` TO \`${view.schema}\`.\`${view.name}\``;
|
||||
@@ -609,7 +754,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* CREATE VIEW
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async createView (params) {
|
||||
@@ -642,7 +787,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* DROP TRIGGER
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async dropTrigger (params) {
|
||||
@@ -653,7 +798,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* ALTER TRIGGER
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async alterTrigger (params) {
|
||||
@@ -675,7 +820,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* CREATE TRIGGER
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async createTrigger (params) {
|
||||
@@ -749,7 +894,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* DROP PROCEDURE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async dropRoutine (params) {
|
||||
@@ -760,7 +905,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* ALTER PROCEDURE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async alterRoutine (params) {
|
||||
@@ -782,7 +927,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* CREATE PROCEDURE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async createRoutine (params) {
|
||||
@@ -876,7 +1021,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* DROP FUNCTION
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async dropFunction (params) {
|
||||
@@ -887,7 +1032,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* ALTER FUNCTION
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async alterFunction (params) {
|
||||
@@ -909,14 +1054,16 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* CREATE FUNCTION
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async createFunction (params) {
|
||||
const parameters = params.parameters.reduce((acc, curr) => {
|
||||
acc.push(`\`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
|
||||
return acc;
|
||||
}, []).join(',');
|
||||
const parameters = 'parameters' in params
|
||||
? params.parameters.reduce((acc, curr) => {
|
||||
acc.push(`\`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
|
||||
return acc;
|
||||
}, []).join(',')
|
||||
: '';
|
||||
|
||||
const body = params.returns ? params.sql : 'BEGIN\n RETURN 0;\nEND';
|
||||
|
||||
@@ -968,7 +1115,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* DROP EVENT
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async dropEvent (params) {
|
||||
@@ -979,7 +1126,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* ALTER EVENT
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async alterEvent (params) {
|
||||
@@ -1005,7 +1152,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* CREATE EVENT
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async createEvent (params) {
|
||||
@@ -1022,6 +1169,16 @@ export class MySQLClient extends AntaresCore {
|
||||
return await this.raw(sql, { split: false });
|
||||
}
|
||||
|
||||
async enableEvent ({ schema, scheduler }) {
|
||||
const sql = `ALTER EVENT \`${schema}\`.\`${scheduler}\` ENABLE`;
|
||||
return await this.raw(sql, { split: false });
|
||||
}
|
||||
|
||||
async disableEvent ({ schema, scheduler }) {
|
||||
const sql = `ALTER EVENT \`${schema}\`.\`${scheduler}\` DISABLE`;
|
||||
return await this.raw(sql, { split: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* SHOW COLLATION
|
||||
*
|
||||
@@ -1061,6 +1218,26 @@ export class MySQLClient extends AntaresCore {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* SHOW VARIABLES LIKE %variable%
|
||||
*
|
||||
* @param {String} variable
|
||||
* @param {'global'|'session'|null} level
|
||||
* @returns {Object} variable
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async getVariable (variable, level) {
|
||||
const sql = `SHOW${level ? ' ' + level.toUpperCase() : ''} VARIABLES LIKE '%${variable}%'`;
|
||||
const results = await this.raw(sql);
|
||||
|
||||
if (results.rows.length) {
|
||||
return {
|
||||
name: results.rows[0].Variable_name,
|
||||
value: results.rows[0].Value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SHOW ENGINES
|
||||
*
|
||||
@@ -1132,14 +1309,114 @@ export class MySQLClient extends AntaresCore {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} id
|
||||
* @returns {Promise<null>}
|
||||
*/
|
||||
async killProcess (id) {
|
||||
return await this.raw(`KILL ${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} tabUid
|
||||
* @returns {Promise<null>}
|
||||
*/
|
||||
async killTabQuery (tabUid) {
|
||||
const id = this._runningConnections.get(tabUid);
|
||||
if (id)
|
||||
return await this.killProcess(id);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} tabUid
|
||||
* @returns {Promise<null>}
|
||||
*/
|
||||
async commitTab (tabUid) {
|
||||
const connection = this._connectionsToCommit.get(tabUid);
|
||||
if (connection)
|
||||
return await connection.query('COMMIT');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} tabUid
|
||||
* @returns {Promise<null>}
|
||||
*/
|
||||
async rollbackTab (tabUid) {
|
||||
const connection = this._connectionsToCommit.get(tabUid);
|
||||
if (connection)
|
||||
return await connection.query('ROLLBACK');
|
||||
}
|
||||
|
||||
destroyConnectionToCommit (tabUid) {
|
||||
const connection = this._connectionsToCommit.get(tabUid);
|
||||
if (connection) {
|
||||
connection.destroy();
|
||||
this._connectionsToCommit.delete(tabUid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CREATE TABLE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async createTable (params) {
|
||||
const sql = `CREATE TABLE \`${params.schema}\`.\`${params.name}\` (\`${params.name}_ID\` INT NULL) COMMENT='${params.comment}', COLLATE='${params.collation}', ENGINE=${params.engine}`;
|
||||
const {
|
||||
schema,
|
||||
fields,
|
||||
foreigns,
|
||||
indexes,
|
||||
options
|
||||
} = params;
|
||||
const newColumns = [];
|
||||
const newIndexes = [];
|
||||
const newForeigns = [];
|
||||
|
||||
let sql = `CREATE TABLE \`${schema}\`.\`${options.name}\``;
|
||||
|
||||
// ADD FIELDS
|
||||
fields.forEach(field => {
|
||||
const typeInfo = this._getTypeInfo(field.type);
|
||||
const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false;
|
||||
|
||||
newColumns.push(`\`${field.name}\`
|
||||
${field.type.toUpperCase()}${length ? `(${length}${field.numScale ? `,${field.numScale}` : ''})` : ''}
|
||||
${field.unsigned ? 'UNSIGNED' : ''}
|
||||
${field.zerofill ? 'ZEROFILL' : ''}
|
||||
${field.nullable ? 'NULL' : 'NOT NULL'}
|
||||
${field.autoIncrement ? 'AUTO_INCREMENT' : ''}
|
||||
${field.default ? `DEFAULT ${field.default}` : ''}
|
||||
${field.comment ? `COMMENT '${field.comment}'` : ''}
|
||||
${field.collation ? `COLLATE ${field.collation}` : ''}
|
||||
${field.onUpdate ? `ON UPDATE ${field.onUpdate}` : ''}`);
|
||||
});
|
||||
|
||||
// ADD INDEX
|
||||
indexes.forEach(index => {
|
||||
const fields = index.fields.map(field => `\`${field}\``).join(',');
|
||||
let type = index.type;
|
||||
|
||||
if (type === 'PRIMARY')
|
||||
newIndexes.push(`PRIMARY KEY (${fields})`);
|
||||
else {
|
||||
if (type === 'UNIQUE')
|
||||
type = 'UNIQUE INDEX';
|
||||
|
||||
newIndexes.push(`${type} \`${index.name}\` (${fields})`);
|
||||
}
|
||||
});
|
||||
|
||||
// ADD FOREIGN KEYS
|
||||
foreigns.forEach(foreign => {
|
||||
newForeigns.push(`CONSTRAINT \`${foreign.constraintName}\` FOREIGN KEY (\`${foreign.field}\`) REFERENCES \`${foreign.refTable}\` (\`${foreign.refField}\`) ON UPDATE ${foreign.onUpdate} ON DELETE ${foreign.onDelete}`);
|
||||
});
|
||||
|
||||
sql = `${sql} (${[...newColumns, ...newIndexes, ...newForeigns].join(', ')}) COMMENT='${options.comment}', COLLATE='${options.collation}', ENGINE=${options.engine}`;
|
||||
|
||||
return await this.raw(sql);
|
||||
}
|
||||
@@ -1147,7 +1424,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* ALTER TABLE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async alterTable (params) {
|
||||
@@ -1177,7 +1454,7 @@ export class MySQLClient extends AntaresCore {
|
||||
const length = typeInfo.length ? addition.enumValues || addition.numLength || addition.charLength || addition.datePrecision : false;
|
||||
|
||||
alterColumns.push(`ADD COLUMN \`${addition.name}\`
|
||||
${addition.type.toUpperCase()}${length ? `(${length})` : ''}
|
||||
${addition.type.toUpperCase()}${length ? `(${length}${addition.numScale ? `,${addition.numScale}` : ''})` : ''}
|
||||
${addition.unsigned ? 'UNSIGNED' : ''}
|
||||
${addition.zerofill ? 'ZEROFILL' : ''}
|
||||
${addition.nullable ? 'NULL' : 'NOT NULL'}
|
||||
@@ -1215,7 +1492,7 @@ export class MySQLClient extends AntaresCore {
|
||||
const length = typeInfo.length ? change.enumValues || change.numLength || change.charLength || change.datePrecision : false;
|
||||
|
||||
alterColumns.push(`CHANGE COLUMN \`${change.orgName}\` \`${change.name}\`
|
||||
${change.type.toUpperCase()}${length ? `(${length})` : ''}
|
||||
${change.type.toUpperCase()}${length ? `(${length}${change.numScale ? `,${change.numScale}` : ''})` : ''}
|
||||
${change.unsigned ? 'UNSIGNED' : ''}
|
||||
${change.zerofill ? 'ZEROFILL' : ''}
|
||||
${change.nullable ? 'NULL' : 'NOT NULL'}
|
||||
@@ -1282,7 +1559,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* DUPLICATE TABLE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async duplicateTable (params) {
|
||||
@@ -1293,7 +1570,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* TRUNCATE TABLE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async truncateTable (params) {
|
||||
@@ -1304,7 +1581,7 @@ export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* DROP TABLE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async dropTable (params) {
|
||||
@@ -1346,7 +1623,7 @@ export class MySQLClient extends AntaresCore {
|
||||
let insertRaw = '';
|
||||
|
||||
if (this._query.insert.length) {
|
||||
const fieldsList = Object.keys(this._query.insert[0]);
|
||||
const fieldsList = Object.keys(this._query.insert[0]).map(col => '`' + col + '`');
|
||||
const rowsList = this._query.insert.map(el => `(${Object.values(el).join(', ')})`);
|
||||
|
||||
insertRaw = `(${fieldsList.join(', ')}) VALUES ${rowsList.join(', ')} `;
|
||||
@@ -1386,6 +1663,7 @@ export class MySQLClient extends AntaresCore {
|
||||
details: false,
|
||||
split: true,
|
||||
comments: true,
|
||||
autocommit: true,
|
||||
...args
|
||||
};
|
||||
|
||||
@@ -1400,8 +1678,24 @@ export class MySQLClient extends AntaresCore {
|
||||
.filter(Boolean)
|
||||
.map(q => q.trim())
|
||||
: [sql];
|
||||
|
||||
let connection;
|
||||
const isPool = typeof this._connection.getConnection === 'function';
|
||||
const connection = isPool ? await this._connection.getConnection() : this._connection;
|
||||
|
||||
if (!args.autocommit && args.tabUid) { // autocommit OFF
|
||||
if (this._connectionsToCommit.has(args.tabUid))
|
||||
connection = this._connectionsToCommit.get(args.tabUid);
|
||||
else {
|
||||
connection = await this.getConnection();
|
||||
await connection.query('SET SESSION autocommit=0');
|
||||
this._connectionsToCommit.set(args.tabUid, connection);
|
||||
}
|
||||
}
|
||||
else// autocommit ON
|
||||
connection = isPool ? await this._connection.getConnection() : this._connection;
|
||||
|
||||
if (args.tabUid && isPool)
|
||||
this._runningConnections.set(args.tabUid, connection.connection.connectionId);
|
||||
|
||||
if (args.schema)
|
||||
await connection.query(`USE \`${args.schema}\``);
|
||||
@@ -1463,7 +1757,10 @@ export class MySQLClient extends AntaresCore {
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
if (isPool) connection.release();
|
||||
if (isPool && args.autocommit) {
|
||||
connection.release();
|
||||
this._runningConnections.delete(args.tabUid);
|
||||
}
|
||||
reject(err);
|
||||
}
|
||||
|
||||
@@ -1472,7 +1769,10 @@ export class MySQLClient extends AntaresCore {
|
||||
keysArr = keysArr ? [...keysArr, ...response] : response;
|
||||
}
|
||||
catch (err) {
|
||||
if (isPool) connection.release();
|
||||
if (isPool && args.autocommit) {
|
||||
connection.release();
|
||||
this._runningConnections.delete(args.tabUid);
|
||||
}
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
@@ -1487,7 +1787,10 @@ export class MySQLClient extends AntaresCore {
|
||||
keys: keysArr
|
||||
});
|
||||
}).catch((err) => {
|
||||
if (isPool) connection.release();
|
||||
if (isPool && args.autocommit) {
|
||||
connection.release();
|
||||
this._runningConnections.delete(args.tabUid);
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
@@ -1495,7 +1798,10 @@ export class MySQLClient extends AntaresCore {
|
||||
resultsArr.push({ rows, report, fields, keys, duration });
|
||||
}
|
||||
|
||||
if (isPool) connection.release();
|
||||
if (isPool && args.autocommit) {
|
||||
connection.release();
|
||||
this._runningConnections.delete(args.tabUid);
|
||||
}
|
||||
|
||||
return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
|
||||
}
|
||||
|
@@ -20,6 +20,8 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
super(args);
|
||||
|
||||
this._schema = null;
|
||||
this._runningConnections = new Map();
|
||||
this._connectionsToCommit = new Map();
|
||||
|
||||
this.types = {};
|
||||
for (const key in types.builtins)
|
||||
@@ -36,6 +38,26 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
};
|
||||
}
|
||||
|
||||
_reducer (acc, curr) {
|
||||
const type = typeof curr;
|
||||
|
||||
switch (type) {
|
||||
case 'number':
|
||||
case 'string':
|
||||
return [...acc, curr];
|
||||
case 'object':
|
||||
if (Array.isArray(curr))
|
||||
return [...acc, ...curr];
|
||||
else {
|
||||
const clausoles = [];
|
||||
for (const key in curr)
|
||||
clausoles.push(`"${key}" ${curr[key]}`);
|
||||
|
||||
return clausoles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_getTypeInfo (type) {
|
||||
return dataTypes
|
||||
.reduce((acc, group) => [...acc, ...group.types], [])
|
||||
@@ -49,9 +71,11 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns dbConfig
|
||||
* @memberof PostgreSQLClient
|
||||
*/
|
||||
async connect () {
|
||||
async getDbConfig () {
|
||||
const dbConfig = {
|
||||
host: this._params.host,
|
||||
port: this._params.port,
|
||||
@@ -65,24 +89,58 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
if (this._params.ssl) dbConfig.ssl = { ...this._params.ssl };
|
||||
|
||||
if (this._params.ssh) {
|
||||
this._ssh = new SSH2Promise({ ...this._params.ssh });
|
||||
try {
|
||||
this._ssh = new SSH2Promise({ ...this._params.ssh });
|
||||
|
||||
this._tunnel = await this._ssh.addTunnel({
|
||||
remoteAddr: this._params.host,
|
||||
remotePort: this._params.port
|
||||
const tunnel = await this._ssh.addTunnel({
|
||||
remoteAddr: this._params.host,
|
||||
remotePort: this._params.port
|
||||
});
|
||||
dbConfig.port = tunnel.localPort;
|
||||
}
|
||||
catch (err) {
|
||||
if (this._ssh) this._ssh.close();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return dbConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @memberof PostgreSQLClient
|
||||
*/
|
||||
async connect () {
|
||||
if (!this._poolSize)
|
||||
this._connection = await this.getConnection();
|
||||
else
|
||||
this._connection = await this.getConnectionPool();
|
||||
}
|
||||
|
||||
async getConnection () {
|
||||
const dbConfig = await this.getDbConfig();
|
||||
const client = new Client(dbConfig);
|
||||
await client.connect();
|
||||
const connection = client;
|
||||
|
||||
if (this._params.readonly)
|
||||
await connection.query('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY');
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
async getConnectionPool () {
|
||||
const dbConfig = await this.getDbConfig();
|
||||
const pool = new Pool({ ...dbConfig, max: this._poolSize });
|
||||
const connection = pool;
|
||||
|
||||
if (this._params.readonly) {
|
||||
connection.on('connect', conn => {
|
||||
conn.query('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY');
|
||||
});
|
||||
dbConfig.port = this._tunnel.localPort;
|
||||
}
|
||||
|
||||
if (!this._poolSize) {
|
||||
const client = new Client(dbConfig);
|
||||
await client.connect();
|
||||
this._connection = client;
|
||||
}
|
||||
else {
|
||||
const pool = new Pool({ ...dbConfig, max: this._poolSize });
|
||||
this._connection = pool;
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,15 +152,23 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an USE query
|
||||
* Executes an 'SET search_path TO "${schema}"' query
|
||||
*
|
||||
* @param {String} schema
|
||||
* @param {Object?} connection optional
|
||||
* @memberof PostgreSQLClient
|
||||
*/
|
||||
use (schema) {
|
||||
use (schema, connection) {
|
||||
this._schema = schema;
|
||||
if (schema)
|
||||
return this.raw(`SET search_path TO ${schema}`);
|
||||
|
||||
if (schema) {
|
||||
const sql = `SET search_path TO "${schema}"`;
|
||||
|
||||
if (connection === undefined)
|
||||
return this.raw(sql);
|
||||
else
|
||||
return connection.query(sql);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,6 +183,7 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
|
||||
const tablesArr = [];
|
||||
const triggersArr = [];
|
||||
let schemaSize = 0;
|
||||
|
||||
for (const db of databases) {
|
||||
if (!schemas.has(db.database)) continue;
|
||||
@@ -142,19 +209,20 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
}
|
||||
|
||||
let { rows: triggers } = await this.raw(`
|
||||
SELECT event_object_schema AS table_schema,
|
||||
event_object_table AS table_name,
|
||||
trigger_schema,
|
||||
trigger_name,
|
||||
string_agg(event_manipulation, ',') AS event,
|
||||
action_timing AS activation,
|
||||
action_condition AS condition,
|
||||
action_statement AS definition
|
||||
FROM information_schema.triggers
|
||||
SELECT
|
||||
pg_class.relname AS table_name,
|
||||
pg_trigger.tgname AS trigger_name,
|
||||
pg_namespace.nspname AS trigger_schema,
|
||||
(pg_trigger.tgenabled != 'D')::bool AS enabled
|
||||
FROM pg_trigger
|
||||
JOIN pg_class ON pg_trigger.tgrelid = pg_class.oid
|
||||
JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace
|
||||
JOIN information_schema.triggers ON information_schema.triggers.trigger_schema = pg_namespace.nspname
|
||||
AND information_schema.triggers.event_object_table = pg_class.relname
|
||||
AND information_schema.triggers.trigger_name = pg_trigger.tgname
|
||||
WHERE trigger_schema = '${db.database}'
|
||||
GROUP BY 1,2,3,4,6,7,8
|
||||
ORDER BY table_schema,
|
||||
table_name
|
||||
GROUP BY 1, 2, 3, 4
|
||||
ORDER BY table_name
|
||||
`);
|
||||
|
||||
if (triggers.length) {
|
||||
@@ -170,11 +238,14 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
if (schemas.has(db.database)) {
|
||||
// TABLES
|
||||
const remappedTables = tablesArr.filter(table => table.Db === db.database).map(table => {
|
||||
const tableSize = Number(table.data_length) + Number(table.index_length);
|
||||
schemaSize += tableSize;
|
||||
|
||||
return {
|
||||
name: table.table_name,
|
||||
type: table.table_type === 'VIEW' ? 'view' : 'table',
|
||||
rows: table.reltuples,
|
||||
size: +table.data_length + +table.index_length,
|
||||
size: tableSize,
|
||||
collation: table.Collation,
|
||||
comment: table.comment,
|
||||
engine: ''
|
||||
@@ -213,17 +284,16 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
return {
|
||||
name: `${trigger.table_name}.${trigger.trigger_name}`,
|
||||
orgName: trigger.trigger_name,
|
||||
timing: trigger.activation,
|
||||
definer: '',
|
||||
definition: trigger.definition,
|
||||
event: trigger.event,
|
||||
table: trigger.table_name,
|
||||
sqlMode: ''
|
||||
sqlMode: '',
|
||||
enabled: trigger.enabled
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
name: db.database,
|
||||
size: schemaSize,
|
||||
tables: remappedTables,
|
||||
functions: remappedFunctions,
|
||||
procedures: remappedProcedures,
|
||||
@@ -235,6 +305,7 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
else {
|
||||
return {
|
||||
name: db.database,
|
||||
size: 0,
|
||||
tables: [],
|
||||
functions: [],
|
||||
procedures: [],
|
||||
@@ -276,6 +347,7 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
isArray,
|
||||
schema: field.table_schema,
|
||||
table: field.table_name,
|
||||
numScale: field.numeric_scale,
|
||||
numPrecision: field.numeric_precision,
|
||||
datePrecision: field.datetime_precision,
|
||||
charLength: field.character_maximum_length,
|
||||
@@ -306,6 +378,40 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
return rows.length ? rows[0].count : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {String} params.schema
|
||||
* @param {String} params.table
|
||||
* @returns {Object} table options
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async getTableOptions ({ schema, table }) {
|
||||
const { rows } = await this.raw(`
|
||||
SELECT *,
|
||||
pg_table_size(QUOTE_IDENT(t.TABLE_SCHEMA) || '.' || QUOTE_IDENT(t.TABLE_NAME))::bigint AS data_length,
|
||||
pg_relation_size(QUOTE_IDENT(t.TABLE_SCHEMA) || '.' || QUOTE_IDENT(t.TABLE_NAME))::bigint AS index_length,
|
||||
c.reltuples, obj_description(c.oid) AS comment
|
||||
FROM "information_schema"."tables" AS t
|
||||
LEFT JOIN "pg_namespace" n ON t.table_schema = n.nspname
|
||||
LEFT JOIN "pg_class" c ON n.oid = c.relnamespace AND c.relname=t.table_name
|
||||
WHERE t."table_schema" = '${schema}'
|
||||
AND table_name = '${table}'
|
||||
`);
|
||||
|
||||
if (rows.length) {
|
||||
return {
|
||||
name: rows[0].table_name,
|
||||
type: rows[0].table_type === 'VIEW' ? 'view' : 'table',
|
||||
rows: rows[0].reltuples,
|
||||
size: +rows[0].data_length + +rows[0].index_length,
|
||||
collation: rows[0].Collation,
|
||||
comment: rows[0].comment,
|
||||
engine: ''
|
||||
};
|
||||
};
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {String} params.schema
|
||||
@@ -440,7 +546,7 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
/**
|
||||
* CREATE SCHEMA
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async createSchema (params) {
|
||||
@@ -450,7 +556,7 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
/**
|
||||
* ALTER DATABASE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async alterSchema (params) {
|
||||
@@ -460,7 +566,7 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
/**
|
||||
* DROP DATABASE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async dropSchema (params) {
|
||||
@@ -492,26 +598,26 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
/**
|
||||
* DROP VIEW
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof PostgreSQLClient
|
||||
*/
|
||||
async dropView (params) {
|
||||
const sql = `DROP VIEW ${params.schema}.${params.view}`;
|
||||
const sql = `DROP VIEW "${params.schema}"."${params.view}"`;
|
||||
return await this.raw(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* ALTER VIEW
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof PostgreSQLClient
|
||||
*/
|
||||
async alterView (params) {
|
||||
const { view } = params;
|
||||
let sql = `CREATE OR REPLACE VIEW ${view.schema}.${view.oldName} AS ${view.sql}`;
|
||||
let sql = `CREATE OR REPLACE VIEW "${view.schema}"."${view.oldName}" AS ${view.sql}`;
|
||||
|
||||
if (view.name !== view.oldName)
|
||||
sql += `; ALTER VIEW ${view.schema}.${view.oldName} RENAME TO ${view.name}`;
|
||||
sql += `; ALTER VIEW "${view.schema}"."${view.oldName}" RENAME TO "${view.name}"`;
|
||||
|
||||
return await this.raw(sql);
|
||||
}
|
||||
@@ -519,11 +625,11 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
/**
|
||||
* CREATE VIEW
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof PostgreSQLClient
|
||||
*/
|
||||
async createView (params) {
|
||||
const sql = `CREATE VIEW ${params.schema}.${params.name} AS ${params.sql}`;
|
||||
const sql = `CREATE VIEW "${params.schema}"."${params.name}" AS ${params.sql}`;
|
||||
return await this.raw(sql);
|
||||
}
|
||||
|
||||
@@ -537,19 +643,25 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
const [table, triggerName] = trigger.split('.');
|
||||
|
||||
const results = await this.raw(`
|
||||
SELECT event_object_schema AS table_schema,
|
||||
event_object_table AS table_name,
|
||||
trigger_schema,
|
||||
trigger_name,
|
||||
string_agg(event_manipulation, ',') AS event,
|
||||
SELECT
|
||||
information_schema.triggers.event_object_schema AS table_schema,
|
||||
information_schema.triggers.event_object_table AS table_name,
|
||||
information_schema.triggers.trigger_schema,
|
||||
information_schema.triggers.trigger_name,
|
||||
string_agg(event_manipulation, ',') AS EVENT,
|
||||
action_timing AS activation,
|
||||
action_condition AS condition,
|
||||
action_statement AS definition
|
||||
FROM information_schema.triggers
|
||||
action_statement AS definition,
|
||||
(pg_trigger.tgenabled != 'D')::bool AS enabled
|
||||
FROM pg_trigger
|
||||
JOIN pg_class ON pg_trigger.tgrelid = pg_class.oid
|
||||
JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace
|
||||
JOIN information_schema.triggers ON pg_namespace.nspname = information_schema.triggers.trigger_schema
|
||||
AND pg_class.relname = information_schema.triggers.event_object_table
|
||||
WHERE trigger_schema = '${schema}'
|
||||
AND trigger_name = '${triggerName}'
|
||||
AND event_object_table = '${table}'
|
||||
GROUP BY 1,2,3,4,6,7,8
|
||||
GROUP BY 1,2,3,4,6,7,8,9
|
||||
ORDER BY table_schema,
|
||||
table_name
|
||||
`);
|
||||
@@ -559,7 +671,7 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
sql: row.definition,
|
||||
name: row.trigger_name,
|
||||
table: row.table_name,
|
||||
event: row.event.split(','),
|
||||
event: [...new Set(row.event.split(','))],
|
||||
activation: row.activation
|
||||
};
|
||||
})[0];
|
||||
@@ -568,7 +680,7 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
/**
|
||||
* DROP TRIGGER
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof PostgreSQLClient
|
||||
*/
|
||||
async dropTrigger (params) {
|
||||
@@ -580,7 +692,7 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
/**
|
||||
* ALTER TRIGGER
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof PostgreSQLClient
|
||||
*/
|
||||
async alterTrigger (params) {
|
||||
@@ -602,7 +714,7 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
/**
|
||||
* CREATE TRIGGER
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof PostgreSQLClient
|
||||
*/
|
||||
async createTrigger (params) {
|
||||
@@ -611,6 +723,18 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
return await this.raw(sql, { split: false });
|
||||
}
|
||||
|
||||
async enableTrigger ({ schema, trigger }) {
|
||||
const [table, triggerName] = trigger.split('.');
|
||||
const sql = `ALTER TABLE "${schema}"."${table}" ENABLE TRIGGER "${triggerName}"`;
|
||||
return await this.raw(sql, { split: false });
|
||||
}
|
||||
|
||||
async disableTrigger ({ schema, trigger }) {
|
||||
const [table, triggerName] = trigger.split('.');
|
||||
const sql = `ALTER TABLE "${schema}"."${table}" DISABLE TRIGGER "${triggerName}"`;
|
||||
return await this.raw(sql, { split: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* SHOW CREATE PROCEDURE
|
||||
*
|
||||
@@ -724,7 +848,7 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
async createRoutine (routine) {
|
||||
const parameters = 'parameters' in routine
|
||||
? routine.parameters.reduce((acc, curr) => {
|
||||
acc.push(`${curr.context} ${curr.name} ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
|
||||
acc.push(`${curr.context} ${curr.name} ${curr.type}`);
|
||||
return acc;
|
||||
}, []).join(',')
|
||||
: '';
|
||||
@@ -853,7 +977,7 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
async createFunction (func) {
|
||||
const parameters = 'parameters' in func
|
||||
? func.parameters.reduce((acc, curr) => {
|
||||
acc.push(`${curr.context} ${curr.name || ''} ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
|
||||
acc.push(`${curr.context} ${curr.name || ''} ${curr.type}`);
|
||||
return acc;
|
||||
}, []).join(',')
|
||||
: '';
|
||||
@@ -995,22 +1119,122 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} id
|
||||
* @returns {Promise<null>}
|
||||
*/
|
||||
async killProcess (id) {
|
||||
return await this.raw(`SELECT pg_terminate_backend(${id})`);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} tabUid
|
||||
* @returns {Promise<null>}
|
||||
*/
|
||||
async killTabQuery (tabUid) {
|
||||
const id = this._runningConnections.get(tabUid);
|
||||
if (id)
|
||||
return await this.raw(`SELECT pg_cancel_backend(${id})`);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} tabUid
|
||||
* @returns {Promise<null>}
|
||||
*/
|
||||
async commitTab (tabUid) {
|
||||
const connection = this._connectionsToCommit.get(tabUid);
|
||||
if (connection) {
|
||||
await connection.query('COMMIT');
|
||||
return this.destroyConnectionToCommit(tabUid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} tabUid
|
||||
* @returns {Promise<null>}
|
||||
*/
|
||||
async rollbackTab (tabUid) {
|
||||
const connection = this._connectionsToCommit.get(tabUid);
|
||||
if (connection) {
|
||||
await connection.query('ROLLBACK');
|
||||
return this.destroyConnectionToCommit(tabUid);
|
||||
}
|
||||
}
|
||||
|
||||
destroyConnectionToCommit (tabUid) {
|
||||
const connection = this._connectionsToCommit.get(tabUid);
|
||||
if (connection) {
|
||||
connection.end();
|
||||
this._connectionsToCommit.delete(tabUid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CREATE TABLE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof PostgreSQLClient
|
||||
*/
|
||||
async createTable (params) {
|
||||
const sql = `CREATE TABLE "${params.schema}"."${params.name}" (${params.name}_id INTEGER NULL); ALTER TABLE "${params.schema}"."${params.name}" DROP COLUMN ${params.name}_id`;
|
||||
const {
|
||||
schema,
|
||||
fields,
|
||||
foreigns,
|
||||
indexes,
|
||||
options
|
||||
} = params;
|
||||
const newColumns = [];
|
||||
const newIndexes = [];
|
||||
const manageIndexes = [];
|
||||
const newForeigns = [];
|
||||
|
||||
let sql = `CREATE TABLE "${schema}"."${options.name}"`;
|
||||
|
||||
// ADD FIELDS
|
||||
fields.forEach(field => {
|
||||
const typeInfo = this._getTypeInfo(field.type);
|
||||
const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false;
|
||||
|
||||
newColumns.push(`"${field.name}"
|
||||
${field.type.toUpperCase()}${length ? `(${length}${field.numScale !== null ? `,${field.numScale}` : ''})` : ''}
|
||||
${field.unsigned ? 'UNSIGNED' : ''}
|
||||
${field.zerofill ? 'ZEROFILL' : ''}
|
||||
${field.nullable ? 'NULL' : 'NOT NULL'}
|
||||
${field.default ? `DEFAULT ${field.default}` : ''}
|
||||
${field.onUpdate ? `ON UPDATE ${field.onUpdate}` : ''}`);
|
||||
});
|
||||
|
||||
// ADD INDEX
|
||||
indexes.forEach(index => {
|
||||
const fields = index.fields.map(field => `${field}`).join(',');
|
||||
const type = index.type;
|
||||
|
||||
if (type === 'PRIMARY')
|
||||
newIndexes.push(`PRIMARY KEY (${fields})`);
|
||||
else if (type === 'UNIQUE')
|
||||
newIndexes.push(`CONSTRAINT "${index.name}" UNIQUE (${fields})`);
|
||||
else
|
||||
manageIndexes.push(`CREATE INDEX "${index.name}" ON "${schema}"."${options.name}" (${fields})`);
|
||||
});
|
||||
|
||||
// ADD FOREIGN KEYS
|
||||
foreigns.forEach(foreign => {
|
||||
newForeigns.push(`CONSTRAINT "${foreign.constraintName}" FOREIGN KEY ("${foreign.field}") REFERENCES "${schema}"."${foreign.refTable}" ("${foreign.refField}") ON UPDATE ${foreign.onUpdate} ON DELETE ${foreign.onDelete}`);
|
||||
});
|
||||
|
||||
sql = `${sql} (${[...newColumns, ...newIndexes, ...newForeigns].join(', ')})`;
|
||||
if (manageIndexes.length) sql = `${sql}; ${manageIndexes.join(';')}`;
|
||||
return await this.raw(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* ALTER TABLE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof PostgreSQLClient
|
||||
*/
|
||||
async alterTable (params) {
|
||||
@@ -1034,45 +1258,36 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
const createSequences = [];
|
||||
const manageIndexes = [];
|
||||
|
||||
// OPTIONS
|
||||
if ('comment' in options) alterColumns.push(`COMMENT='${options.comment}'`);
|
||||
if ('engine' in options) alterColumns.push(`ENGINE=${options.engine}`);
|
||||
if ('autoIncrement' in options) alterColumns.push(`AUTO_INCREMENT=${+options.autoIncrement}`);
|
||||
if ('collation' in options) alterColumns.push(`COLLATE='${options.collation}'`);
|
||||
|
||||
// ADD FIELDS
|
||||
additions.forEach(addition => {
|
||||
const typeInfo = this._getTypeInfo(addition.type);
|
||||
const length = typeInfo.length ? addition.numLength || addition.charLength || addition.datePrecision : false;
|
||||
|
||||
alterColumns.push(`ADD COLUMN ${addition.name}
|
||||
${addition.type.toUpperCase()}${length ? `(${length})` : ''}${addition.isArray ? '[]' : ''}
|
||||
alterColumns.push(`ADD COLUMN "${addition.name}"
|
||||
${addition.type.toUpperCase()}${length ? `(${length}${addition.numScale !== null ? `,${addition.numScale}` : ''})` : ''}${addition.isArray ? '[]' : ''}
|
||||
${addition.unsigned ? 'UNSIGNED' : ''}
|
||||
${addition.zerofill ? 'ZEROFILL' : ''}
|
||||
${addition.nullable ? 'NULL' : 'NOT NULL'}
|
||||
${addition.autoIncrement ? 'AUTO_INCREMENT' : ''}
|
||||
${addition.default ? `DEFAULT ${addition.default}` : ''}
|
||||
${addition.comment ? `COMMENT '${addition.comment}'` : ''}
|
||||
${addition.collation ? `COLLATE ${addition.collation}` : ''}
|
||||
${addition.onUpdate ? `ON UPDATE ${addition.onUpdate}` : ''}`);
|
||||
});
|
||||
|
||||
// ADD INDEX
|
||||
indexChanges.additions.forEach(addition => {
|
||||
const fields = addition.fields.map(field => `${field}`).join(',');
|
||||
const fields = addition.fields.map(field => `"${field}"`).join(',');
|
||||
const type = addition.type;
|
||||
|
||||
if (type === 'PRIMARY')
|
||||
alterColumns.push(`ADD PRIMARY KEY (${fields})`);
|
||||
else if (type === 'UNIQUE')
|
||||
alterColumns.push(`ADD CONSTRAINT ${addition.name} UNIQUE (${fields})`);
|
||||
alterColumns.push(`ADD CONSTRAINT "${addition.name}" UNIQUE (${fields})`);
|
||||
else
|
||||
manageIndexes.push(`CREATE INDEX ${addition.name} ON "${schema}"."${table}" (${fields})`);
|
||||
manageIndexes.push(`CREATE INDEX "${addition.name}" ON "${schema}"."${table}" (${fields})`);
|
||||
});
|
||||
|
||||
// ADD FOREIGN KEYS
|
||||
foreignChanges.additions.forEach(addition => {
|
||||
alterColumns.push(`ADD CONSTRAINT ${addition.constraintName} FOREIGN KEY (${addition.field}) REFERENCES ${addition.refTable} (${addition.refField}) ON UPDATE ${addition.onUpdate} ON DELETE ${addition.onDelete}`);
|
||||
alterColumns.push(`ADD CONSTRAINT "${addition.constraintName}" FOREIGN KEY ("${addition.field}") REFERENCES "${schema}"."${addition.refTable}" (${addition.refField}) ON UPDATE ${addition.onUpdate} ON DELETE ${addition.onDelete}`);
|
||||
});
|
||||
|
||||
// CHANGE FIELDS
|
||||
@@ -1095,9 +1310,10 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
localType = change.type.toLowerCase();
|
||||
}
|
||||
|
||||
alterColumns.push(`ALTER COLUMN "${change.name}" TYPE ${localType}${length ? `(${length})` : ''}${change.isArray ? '[]' : ''} USING "${change.name}"::${localType}`);
|
||||
alterColumns.push(`ALTER COLUMN "${change.name}" TYPE ${localType}${length ? `(${length}${change.numScale ? `,${change.numScale}` : ''})` : ''}${change.isArray ? '[]' : ''} USING "${change.name}"::${localType}`);
|
||||
alterColumns.push(`ALTER COLUMN "${change.name}" ${change.nullable ? 'DROP NOT NULL' : 'SET NOT NULL'}`);
|
||||
alterColumns.push(`ALTER COLUMN "${change.name}" ${change.default ? `SET DEFAULT ${change.default}` : 'DROP DEFAULT'}`);
|
||||
|
||||
if (['SERIAL', 'SMALLSERIAL', 'BIGSERIAL'].includes(change.type)) {
|
||||
const sequenceName = `${table}_${change.name}_seq`.replace(' ', '_');
|
||||
createSequences.push(`CREATE SEQUENCE IF NOT EXISTS ${sequenceName} OWNED BY "${table}"."${change.name}"`);
|
||||
@@ -1115,39 +1331,39 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
else
|
||||
manageIndexes.push(`DROP INDEX ${change.oldName}`);
|
||||
|
||||
const fields = change.fields.map(field => `${field}`).join(',');
|
||||
const fields = change.fields.map(field => `"${field}"`).join(',');
|
||||
const type = change.type;
|
||||
|
||||
if (type === 'PRIMARY')
|
||||
alterColumns.push(`ADD PRIMARY KEY (${fields})`);
|
||||
else if (type === 'UNIQUE')
|
||||
alterColumns.push(`ADD CONSTRAINT ${change.name} UNIQUE (${fields})`);
|
||||
alterColumns.push(`ADD CONSTRAINT "${change.name}" UNIQUE (${fields})`);
|
||||
else
|
||||
manageIndexes.push(`CREATE INDEX ${change.name} ON "${schema}"."${table}" (${fields})`);
|
||||
manageIndexes.push(`CREATE INDEX "${change.name}" ON "${schema}"."${table}" (${fields})`);
|
||||
});
|
||||
|
||||
// CHANGE FOREIGN KEYS
|
||||
foreignChanges.changes.forEach(change => {
|
||||
alterColumns.push(`DROP CONSTRAINT ${change.oldName}`);
|
||||
alterColumns.push(`ADD CONSTRAINT ${change.constraintName} FOREIGN KEY (${change.field}) REFERENCES ${change.refTable} (${change.refField}) ON UPDATE ${change.onUpdate} ON DELETE ${change.onDelete}`);
|
||||
alterColumns.push(`DROP CONSTRAINT "${change.oldName}"`);
|
||||
alterColumns.push(`ADD CONSTRAINT "${change.constraintName}" FOREIGN KEY (${change.field}) REFERENCES "${schema}"."${change.refTable}" ("${change.refField}") ON UPDATE ${change.onUpdate} ON DELETE ${change.onDelete}`);
|
||||
});
|
||||
|
||||
// DROP FIELDS
|
||||
deletions.forEach(deletion => {
|
||||
alterColumns.push(`DROP COLUMN ${deletion.name}`);
|
||||
alterColumns.push(`DROP COLUMN "${deletion.name}"`);
|
||||
});
|
||||
|
||||
// DROP INDEX
|
||||
indexChanges.deletions.forEach(deletion => {
|
||||
if (['PRIMARY', 'UNIQUE'].includes(deletion.type))
|
||||
alterColumns.push(`DROP CONSTRAINT ${deletion.name}`);
|
||||
alterColumns.push(`DROP CONSTRAINT "${deletion.name}"`);
|
||||
else
|
||||
manageIndexes.push(`DROP INDEX ${deletion.name}`);
|
||||
manageIndexes.push(`DROP INDEX "${deletion.name}"`);
|
||||
});
|
||||
|
||||
// DROP FOREIGN KEYS
|
||||
foreignChanges.deletions.forEach(deletion => {
|
||||
alterColumns.push(`DROP CONSTRAINT ${deletion.constraintName}`);
|
||||
alterColumns.push(`DROP CONSTRAINT "${deletion.constraintName}"`);
|
||||
});
|
||||
|
||||
if (alterColumns.length) sql += `ALTER TABLE "${schema}"."${table}" ${alterColumns.join(', ')}; `;
|
||||
@@ -1164,33 +1380,33 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
/**
|
||||
* DUPLICATE TABLE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof PostgreSQLClient
|
||||
*/
|
||||
async duplicateTable (params) {
|
||||
const sql = `CREATE TABLE ${params.schema}.${params.table}_copy (LIKE ${params.schema}.${params.table} INCLUDING ALL)`;
|
||||
const sql = `CREATE TABLE "${params.schema}"."${params.table}_copy" (LIKE "${params.schema}"."${params.table}" INCLUDING ALL)`;
|
||||
return await this.raw(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* TRUNCATE TABLE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof PostgreSQLClient
|
||||
*/
|
||||
async truncateTable (params) {
|
||||
const sql = `TRUNCATE TABLE ${params.schema}.${params.table}`;
|
||||
const sql = `TRUNCATE TABLE "${params.schema}"."${params.table}"`;
|
||||
return await this.raw(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* DROP TABLE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @returns {Promise<null>}
|
||||
* @memberof PostgreSQLClient
|
||||
*/
|
||||
async dropTable (params) {
|
||||
const sql = `DROP TABLE ${params.schema}.${params.table}`;
|
||||
const sql = `DROP TABLE "${params.schema}"."${params.table}"`;
|
||||
return await this.raw(sql);
|
||||
}
|
||||
|
||||
@@ -1214,7 +1430,7 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
else if (Object.keys(this._query.insert).length)
|
||||
fromRaw = 'INTO';
|
||||
|
||||
fromRaw += this._query.from ? ` ${this._query.schema ? `${this._query.schema}.` : ''}${this._query.from} ` : '';
|
||||
fromRaw += this._query.from ? ` ${this._query.schema ? `"${this._query.schema}".` : ''}"${this._query.from}" ` : '';
|
||||
|
||||
// WHERE
|
||||
const whereArray = this._query.where.reduce(this._reducer, []);
|
||||
@@ -1261,17 +1477,17 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
* @memberof PostgreSQLClient
|
||||
*/
|
||||
async raw (sql, args) {
|
||||
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
|
||||
|
||||
args = {
|
||||
nest: false,
|
||||
details: false,
|
||||
split: true,
|
||||
comments: true,
|
||||
autocommit: true,
|
||||
...args
|
||||
};
|
||||
|
||||
if (args.schema && args.schema !== 'public')
|
||||
await this.use(args.schema);
|
||||
|
||||
if (!args.comments)
|
||||
sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');// Remove comments
|
||||
|
||||
@@ -1283,7 +1499,26 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
.map(q => q.trim())
|
||||
: [sql];
|
||||
|
||||
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
|
||||
let connection;
|
||||
const isPool = this._connection instanceof Pool;
|
||||
|
||||
if (!args.autocommit && args.tabUid) { // autocommit OFF
|
||||
if (this._connectionsToCommit.has(args.tabUid))
|
||||
connection = this._connectionsToCommit.get(args.tabUid);
|
||||
else {
|
||||
connection = await this.getConnection();
|
||||
await connection.query('START TRANSACTION');
|
||||
this._connectionsToCommit.set(args.tabUid, connection);
|
||||
}
|
||||
}
|
||||
else// autocommit ON
|
||||
connection = isPool ? await this._connection.connect() : this._connection;
|
||||
|
||||
if (args.tabUid && isPool)
|
||||
this._runningConnections.set(args.tabUid, connection.processID);
|
||||
|
||||
if (args.schema && args.schema !== 'public')
|
||||
await this.use(args.schema, connection);
|
||||
|
||||
for (const query of queries) {
|
||||
if (!query) continue;
|
||||
@@ -1293,15 +1528,12 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
let keysArr = [];
|
||||
|
||||
const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => {
|
||||
this._connection.query({
|
||||
rowMode: args.nest ? 'array' : null,
|
||||
text: query
|
||||
}, async (err, res) => {
|
||||
timeStop = new Date();
|
||||
(async () => {
|
||||
try {
|
||||
const res = await connection.query({ rowMode: args.nest ? 'array' : null, text: query });
|
||||
|
||||
timeStop = new Date();
|
||||
|
||||
if (err)
|
||||
reject(err);
|
||||
else {
|
||||
let ast;
|
||||
|
||||
try {
|
||||
@@ -1390,6 +1622,10 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
if (isPool && args.autocommit) {
|
||||
connection.release();
|
||||
this._runningConnections.delete(args.tabUid);
|
||||
}
|
||||
reject(err);
|
||||
}
|
||||
|
||||
@@ -1398,6 +1634,10 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
keysArr = keysArr ? [...keysArr, ...response] : response;
|
||||
}
|
||||
catch (err) {
|
||||
if (isPool && args.autocommit) {
|
||||
connection.release();
|
||||
this._runningConnections.delete(args.tabUid);
|
||||
}
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
@@ -1412,12 +1652,24 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
keys: keysArr
|
||||
});
|
||||
}
|
||||
});
|
||||
catch (err) {
|
||||
if (isPool && args.autocommit) {
|
||||
connection.release();
|
||||
this._runningConnections.delete(args.tabUid);
|
||||
}
|
||||
reject(err);
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
resultsArr.push({ rows, report, fields, keys, duration });
|
||||
}
|
||||
|
||||
if (isPool && args.autocommit) {
|
||||
connection.release();
|
||||
this._runningConnections.delete(args.tabUid);
|
||||
}
|
||||
|
||||
return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
|
||||
}
|
||||
}
|
||||
|
859
src/main/libs/clients/SQLiteClient.js
Normal file
@@ -0,0 +1,859 @@
|
||||
'use strict';
|
||||
import sqlite from 'better-sqlite3';
|
||||
import { AntaresCore } from '../AntaresCore';
|
||||
import dataTypes from 'common/data-types/sqlite';
|
||||
import { NUMBER, FLOAT, TIME, DATETIME } from 'common/fieldTypes';
|
||||
|
||||
export class SQLiteClient extends AntaresCore {
|
||||
constructor (args) {
|
||||
super(args);
|
||||
|
||||
this._schema = null;
|
||||
this._connectionsToCommit = new Map();
|
||||
}
|
||||
|
||||
_getTypeInfo (type) {
|
||||
return dataTypes
|
||||
.reduce((acc, group) => [...acc, ...group.types], [])
|
||||
.filter(_type => _type.name === type.toUpperCase())[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async connect () {
|
||||
this._connection = this.getConnection();
|
||||
}
|
||||
|
||||
getConnection () {
|
||||
return sqlite(this._params.databasePath, {
|
||||
fileMustExist: true,
|
||||
readonly: this._params.readonly
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
destroy () {}
|
||||
|
||||
/**
|
||||
* Executes an USE query
|
||||
*
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
use () {}
|
||||
|
||||
/**
|
||||
* @param {Array} schemas list
|
||||
* @returns {Array.<Object>} databases scructure
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async getStructure (schemas) {
|
||||
const { rows: databases } = await this.raw('SELECT * FROM pragma_database_list');
|
||||
|
||||
const filteredDatabases = databases;
|
||||
|
||||
const tablesArr = [];
|
||||
const triggersArr = [];
|
||||
let schemaSize = 0;
|
||||
|
||||
for (const db of filteredDatabases) {
|
||||
if (!schemas.has(db.name)) continue;
|
||||
|
||||
let { rows: tables } = await this.raw(`
|
||||
SELECT *
|
||||
FROM "${db.name}".sqlite_master
|
||||
WHERE type IN ('table', 'view')
|
||||
AND name NOT LIKE 'sqlite_%'
|
||||
ORDER BY name
|
||||
`);
|
||||
if (tables.length) {
|
||||
tables = tables.map(table => {
|
||||
table.Db = db.name;
|
||||
return table;
|
||||
});
|
||||
tablesArr.push(...tables);
|
||||
}
|
||||
|
||||
let { rows: triggers } = await this.raw(`SELECT * FROM "${db.name}".sqlite_master WHERE type='trigger'`);
|
||||
if (triggers.length) {
|
||||
triggers = triggers.map(trigger => {
|
||||
trigger.Db = db.name;
|
||||
return trigger;
|
||||
});
|
||||
triggersArr.push(...triggers);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredDatabases.map(db => {
|
||||
if (schemas.has(db.name)) {
|
||||
// TABLES
|
||||
const remappedTables = tablesArr.filter(table => table.Db === db.name).map(table => {
|
||||
const tableSize = 0;
|
||||
schemaSize += tableSize;
|
||||
|
||||
return {
|
||||
name: table.name,
|
||||
type: table.type,
|
||||
rows: false,
|
||||
size: false
|
||||
};
|
||||
});
|
||||
|
||||
// TRIGGERS
|
||||
const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.name).map(trigger => {
|
||||
return {
|
||||
name: trigger.name,
|
||||
table: trigger.tbl_name
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
name: db.name,
|
||||
size: schemaSize,
|
||||
tables: remappedTables,
|
||||
functions: [],
|
||||
procedures: [],
|
||||
triggers: remappedTriggers,
|
||||
schedulers: []
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
name: db.name,
|
||||
size: 0,
|
||||
tables: [],
|
||||
functions: [],
|
||||
procedures: [],
|
||||
triggers: [],
|
||||
schedulers: []
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {String} params.schema
|
||||
* @param {String} params.table
|
||||
* @returns {Object} table scructure
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async getTableColumns ({ schema, table }) {
|
||||
const { rows: fields } = await this.raw(`SELECT * FROM "${schema}".pragma_table_info('${table}')`);
|
||||
|
||||
return fields.map(field => {
|
||||
const [type, length] = field.type.includes('(')
|
||||
? field.type.replace(')', '').split('(').map(el => {
|
||||
if (!isNaN(el)) el = +el;
|
||||
return el;
|
||||
})
|
||||
: [field.type, null];
|
||||
|
||||
return {
|
||||
name: field.name,
|
||||
key: null,
|
||||
type: type.trim(),
|
||||
schema: schema,
|
||||
table: table,
|
||||
numPrecision: [...NUMBER, ...FLOAT].includes(type) ? length : null,
|
||||
datePrecision: null,
|
||||
charLength: ![...NUMBER, ...FLOAT].includes(type) ? length : null,
|
||||
nullable: !field.notnull,
|
||||
unsigned: null,
|
||||
zerofill: null,
|
||||
order: typeof field.cid === 'string' ? +field.cid + 1 : field.cid + 1,
|
||||
default: field.dflt_value,
|
||||
charset: null,
|
||||
collation: null,
|
||||
autoIncrement: false,
|
||||
onUpdate: null,
|
||||
comment: ''
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {String} params.schema
|
||||
* @param {String} params.table
|
||||
* @returns {Object} table row count
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async getTableApproximateCount ({ schema, table }) {
|
||||
const { rows } = await this.raw(`SELECT COUNT(*) AS count FROM "${schema}"."${table}"`);
|
||||
|
||||
return rows.length ? rows[0].count : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {String} params.schema
|
||||
* @param {String} params.table
|
||||
* @returns {Object} table options
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async getTableOptions ({ schema, table }) {
|
||||
return { name: table };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {String} params.schema
|
||||
* @param {String} params.table
|
||||
* @returns {Object} table indexes
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async getTableIndexes ({ schema, table }) {
|
||||
const remappedIndexes = [];
|
||||
const { rows: primaryKeys } = await this.raw(`SELECT * FROM "${schema}".pragma_table_info('${table}') WHERE pk != 0`);
|
||||
|
||||
for (const key of primaryKeys) {
|
||||
remappedIndexes.push({
|
||||
name: 'PRIMARY',
|
||||
column: key.name,
|
||||
indexType: null,
|
||||
type: 'PRIMARY',
|
||||
cardinality: null,
|
||||
comment: '',
|
||||
indexComment: ''
|
||||
});
|
||||
}
|
||||
|
||||
const { rows: indexes } = await this.raw(`SELECT * FROM "${schema}".pragma_index_list('${table}');`);
|
||||
|
||||
for (const index of indexes) {
|
||||
const { rows: details } = await this.raw(`SELECT * FROM "${schema}".pragma_index_info('${index.name}');`);
|
||||
|
||||
for (const detail of details) {
|
||||
remappedIndexes.push({
|
||||
name: index.name,
|
||||
column: detail.name,
|
||||
indexType: null,
|
||||
type: index.unique === 1 ? 'UNIQUE' : 'INDEX',
|
||||
cardinality: null,
|
||||
comment: '',
|
||||
indexComment: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return remappedIndexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {String} params.schema
|
||||
* @param {String} params.table
|
||||
* @returns {Object} table key usage
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async getKeyUsage ({ schema, table }) {
|
||||
const { rows } = await this.raw(`SELECT * FROM "${schema}".pragma_foreign_key_list('${table}');`);
|
||||
|
||||
return rows.map(field => {
|
||||
return {
|
||||
schema: schema,
|
||||
table: table,
|
||||
field: field.from,
|
||||
position: field.id + 1,
|
||||
constraintPosition: null,
|
||||
constraintName: field.id,
|
||||
refSchema: schema,
|
||||
refTable: field.table,
|
||||
refField: field.to,
|
||||
onUpdate: field.on_update,
|
||||
onDelete: field.on_delete
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getUsers () {}
|
||||
|
||||
/**
|
||||
* SHOW CREATE VIEW
|
||||
*
|
||||
* @returns {Array.<Object>} view informations
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async getViewInformations ({ schema, view }) {
|
||||
const sql = `SELECT "sql" FROM "${schema}".sqlite_master WHERE "type"='view' AND name='${view}'`;
|
||||
const results = await this.raw(sql);
|
||||
|
||||
return results.rows.map(row => {
|
||||
return {
|
||||
sql: row.sql.match(/(?<=AS ).*?$/gs)[0],
|
||||
name: view
|
||||
};
|
||||
})[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* DROP VIEW
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async dropView (params) {
|
||||
const sql = `DROP VIEW "${params.schema}"."${params.view}"`;
|
||||
return await this.raw(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* ALTER VIEW
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async alterView (params) {
|
||||
const { view } = params;
|
||||
try {
|
||||
await this.dropView({ schema: view.schema, view: view.oldName });
|
||||
await this.createView(view);
|
||||
}
|
||||
catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CREATE VIEW
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async createView (params) {
|
||||
const sql = `CREATE VIEW "${params.schema}"."${params.name}" AS ${params.sql}`;
|
||||
return await this.raw(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* SHOW CREATE TRIGGER
|
||||
*
|
||||
* @returns {Array.<Object>} view informations
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async getTriggerInformations ({ schema, trigger }) {
|
||||
const sql = `SELECT "sql" FROM "${schema}".sqlite_master WHERE "type"='trigger' AND name='${trigger}'`;
|
||||
const results = await this.raw(sql);
|
||||
|
||||
return results.rows.map(row => {
|
||||
return {
|
||||
sql: row.sql.match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
|
||||
name: trigger,
|
||||
table: row.sql.match(/(?<=ON `).*?(?=`)/gs)[0],
|
||||
activation: row.sql.match(/(BEFORE|AFTER)/gs)[0],
|
||||
event: row.sql.match(/(INSERT|UPDATE|DELETE)/gs)[0]
|
||||
};
|
||||
})[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* DROP TRIGGER
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async dropTrigger (params) {
|
||||
const sql = `DROP TRIGGER \`${params.schema}\`.\`${params.trigger}\``;
|
||||
return await this.raw(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* ALTER TRIGGER
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async alterTrigger (params) {
|
||||
const { trigger } = params;
|
||||
const tempTrigger = Object.assign({}, trigger);
|
||||
tempTrigger.name = `Antares_${tempTrigger.name}_tmp`;
|
||||
|
||||
try {
|
||||
await this.createTrigger(tempTrigger);
|
||||
await this.dropTrigger({ schema: trigger.schema, trigger: tempTrigger.name });
|
||||
await this.dropTrigger({ schema: trigger.schema, trigger: trigger.oldName });
|
||||
await this.createTrigger(trigger);
|
||||
}
|
||||
catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CREATE TRIGGER
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async createTrigger (params) {
|
||||
const sql = `CREATE ${params.definer ? `DEFINER=${params.definer} ` : ''}TRIGGER \`${params.schema}\`.\`${params.name}\` ${params.activation} ${params.event} ON \`${params.table}\` FOR EACH ROW ${params.sql}`;
|
||||
return await this.raw(sql, { split: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* SHOW COLLATION
|
||||
*
|
||||
* @returns {Array.<Object>} collations list
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async getCollations () {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* SHOW VARIABLES
|
||||
*
|
||||
* @returns {Array.<Object>} variables list
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async getVariables () {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* SHOW ENGINES
|
||||
*
|
||||
* @returns {Array.<Object>} engines list
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async getEngines () {
|
||||
return {
|
||||
name: 'SQLite',
|
||||
support: 'YES',
|
||||
comment: '',
|
||||
isDefault: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* SHOW VARIABLES LIKE '%vers%'
|
||||
*
|
||||
* @returns {Array.<Object>} version parameters
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async getVersion () {
|
||||
const os = require('os');
|
||||
const sql = 'SELECT sqlite_version() AS version';
|
||||
const { rows } = await this.raw(sql);
|
||||
|
||||
return {
|
||||
number: rows[0].version,
|
||||
name: 'SQLite',
|
||||
arch: process.arch,
|
||||
os: `${os.type()} ${os.release()}`
|
||||
};
|
||||
}
|
||||
|
||||
async getProcesses () {}
|
||||
|
||||
async killProcess () {}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} tabUid
|
||||
* @returns {Promise<null>}
|
||||
*/
|
||||
async commitTab (tabUid) {
|
||||
const connection = this._connectionsToCommit.get(tabUid);
|
||||
if (connection) {
|
||||
connection.prepare('COMMIT').run();
|
||||
return this.destroyConnectionToCommit(tabUid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} tabUid
|
||||
* @returns {Promise<null>}
|
||||
*/
|
||||
async rollbackTab (tabUid) {
|
||||
const connection = this._connectionsToCommit.get(tabUid);
|
||||
if (connection) {
|
||||
connection.prepare('ROLLBACK').run();
|
||||
return this.destroyConnectionToCommit(tabUid);
|
||||
}
|
||||
}
|
||||
|
||||
destroyConnectionToCommit (tabUid) {
|
||||
const connection = this._connectionsToCommit.get(tabUid);
|
||||
if (connection) {
|
||||
connection.close();
|
||||
this._connectionsToCommit.delete(tabUid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CREATE TABLE
|
||||
*
|
||||
* @returns {Promise<null>}
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async createTable (params) {
|
||||
const {
|
||||
schema,
|
||||
fields,
|
||||
foreigns,
|
||||
indexes,
|
||||
options
|
||||
} = params;
|
||||
const newColumns = [];
|
||||
const newIndexes = [];
|
||||
const manageIndexes = [];
|
||||
const newForeigns = [];
|
||||
|
||||
let sql = `CREATE TABLE "${schema}"."${options.name}"`;
|
||||
|
||||
// ADD FIELDS
|
||||
fields.forEach(field => {
|
||||
const typeInfo = this._getTypeInfo(field.type);
|
||||
const length = typeInfo?.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false;
|
||||
|
||||
newColumns.push(`"${field.name}"
|
||||
${field.type.toUpperCase()}${length && length !== true ? `(${length})` : ''}
|
||||
${field.unsigned ? 'UNSIGNED' : ''}
|
||||
${field.nullable ? 'NULL' : 'NOT NULL'}
|
||||
${field.autoIncrement ? 'AUTO_INCREMENT' : ''}
|
||||
${field.default ? `DEFAULT ${field.default}` : ''}
|
||||
${field.onUpdate ? `ON UPDATE ${field.onUpdate}` : ''}`);
|
||||
});
|
||||
|
||||
// ADD INDEX
|
||||
indexes.forEach(index => {
|
||||
const fields = index.fields.map(field => `"${field}"`).join(',');
|
||||
const type = index.type;
|
||||
|
||||
if (type === 'PRIMARY')
|
||||
newIndexes.push(`PRIMARY KEY (${fields})`);
|
||||
else
|
||||
manageIndexes.push(`CREATE ${type === 'UNIQUE' ? type : ''} INDEX "${index.name}" ON "${options.name}" (${fields})`);
|
||||
});
|
||||
|
||||
// ADD FOREIGN KEYS
|
||||
foreigns.forEach(foreign => {
|
||||
newForeigns.push(`CONSTRAINT "${foreign.constraintName}" FOREIGN KEY ("${foreign.field}") REFERENCES "${foreign.refTable}" ("${foreign.refField}") ON UPDATE ${foreign.onUpdate} ON DELETE ${foreign.onDelete}`);
|
||||
});
|
||||
|
||||
sql = `${sql} (${[...newColumns, ...newIndexes, ...newForeigns].join(', ')})`;
|
||||
if (manageIndexes.length) sql = `${sql}; ${manageIndexes.join(';')}`;
|
||||
|
||||
return await this.raw(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* ALTER TABLE
|
||||
*
|
||||
* @returns {Promise<null>}
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async alterTable (params) {
|
||||
try {
|
||||
await this.raw('BEGIN TRANSACTION');
|
||||
await this.raw('PRAGMA foreign_keys = 0');
|
||||
|
||||
const tmpName = `Antares_${params.table}_tmp`;
|
||||
await this.raw(`CREATE TABLE "${tmpName}" AS SELECT * FROM "${params.table}"`);
|
||||
await this.dropTable(params);
|
||||
|
||||
const createTableParams = {
|
||||
schema: params.schema,
|
||||
fields: params.tableStructure.fields,
|
||||
foreigns: params.tableStructure.foreigns,
|
||||
indexes: params.tableStructure.indexes.filter(index => !index.name.includes('sqlite_autoindex')),
|
||||
options: { name: params.tableStructure.name }
|
||||
};
|
||||
await this.createTable(createTableParams);
|
||||
const insertFields = createTableParams.fields
|
||||
.filter(field => {
|
||||
return (
|
||||
params.additions.every(add => add.name !== field.name) &&
|
||||
params.deletions.every(del => del.name !== field.name)
|
||||
);
|
||||
})
|
||||
.reduce((acc, curr) => {
|
||||
acc.push(`"${curr.name}"`);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const selectFields = insertFields.map(field => {
|
||||
const renamedField = params.changes.find(change => `"${change.name}"` === field);
|
||||
if (renamedField)
|
||||
return `"${renamedField.orgName}"`;
|
||||
return field;
|
||||
});
|
||||
|
||||
await this.raw(`INSERT INTO "${createTableParams.options.name}" (${insertFields.join(',')}) SELECT ${selectFields.join(',')} FROM "${tmpName}"`);
|
||||
|
||||
await this.dropTable({ schema: params.schema, table: tmpName });
|
||||
await this.raw('PRAGMA foreign_keys = 1');
|
||||
await this.raw('COMMIT');
|
||||
}
|
||||
catch (err) {
|
||||
await this.raw('ROLLBACK');
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DUPLICATE TABLE
|
||||
*
|
||||
* @returns {Promise<null>}
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async duplicateTable (params) { // TODO: retrive table informations and create a copy
|
||||
const sql = `CREATE TABLE "${params.schema}"."${params.table}_copy" AS SELECT * FROM "${params.schema}"."${params.table}"`;
|
||||
return await this.raw(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* TRUNCATE TABLE
|
||||
*
|
||||
* @returns {Promise<null>}
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async truncateTable (params) {
|
||||
const sql = `DELETE FROM "${params.schema}"."${params.table}"`;
|
||||
return await this.raw(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* DROP TABLE
|
||||
*
|
||||
* @returns {Promise<null>}
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async dropTable (params) {
|
||||
const sql = `DROP TABLE "${params.schema}"."${params.table}"`;
|
||||
return await this.raw(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {String} SQL string
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
getSQL () {
|
||||
// SELECT
|
||||
const selectArray = this._query.select.reduce(this._reducer, []);
|
||||
let selectRaw = '';
|
||||
|
||||
if (selectArray.length)
|
||||
selectRaw = selectArray.length ? `SELECT ${selectArray.join(', ')} ` : 'SELECT * ';
|
||||
|
||||
// FROM
|
||||
let fromRaw = '';
|
||||
|
||||
if (!this._query.update.length && !Object.keys(this._query.insert).length && !!this._query.from)
|
||||
fromRaw = 'FROM';
|
||||
else if (Object.keys(this._query.insert).length)
|
||||
fromRaw = 'INTO';
|
||||
|
||||
fromRaw += this._query.from ? ` ${this._query.schema ? `"${this._query.schema}".` : ''}"${this._query.from}" ` : '';
|
||||
|
||||
// WHERE
|
||||
const whereArray = this._query.where
|
||||
.reduce(this._reducer, [])
|
||||
?.map(clausole => clausole.replace('= null', 'IS NULL'));
|
||||
const whereRaw = whereArray.length ? `WHERE ${whereArray.join(' AND ')} ` : '';
|
||||
|
||||
// UPDATE
|
||||
const updateArray = this._query.update.reduce(this._reducer, []);
|
||||
const updateRaw = updateArray.length ? `SET ${updateArray.join(', ')} ` : '';
|
||||
|
||||
// INSERT
|
||||
let insertRaw = '';
|
||||
|
||||
if (this._query.insert.length) {
|
||||
const fieldsList = Object.keys(this._query.insert[0]);
|
||||
const rowsList = this._query.insert.map(el => `(${Object.values(el).join(', ')})`);
|
||||
|
||||
insertRaw = `(${fieldsList.join(', ')}) VALUES ${rowsList.join(', ')} `;
|
||||
}
|
||||
|
||||
// GROUP BY
|
||||
const groupByArray = this._query.groupBy.reduce(this._reducer, []);
|
||||
const groupByRaw = groupByArray.length ? `GROUP BY ${groupByArray.join(', ')} ` : '';
|
||||
|
||||
// ORDER BY
|
||||
const orderByArray = this._query.orderBy.reduce(this._reducer, []);
|
||||
const orderByRaw = orderByArray.length ? `ORDER BY ${orderByArray.join(', ')} ` : '';
|
||||
|
||||
// LIMIT
|
||||
const limitRaw = this._query.limit.length ? `LIMIT ${this._query.limit.join(', ')} ` : '';
|
||||
|
||||
// OFFSET
|
||||
const offsetRaw = this._query.offset.length ? `OFFSET ${this._query.offset.join(', ')} ` : '';
|
||||
|
||||
return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}${offsetRaw}${insertRaw}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} sql raw SQL query
|
||||
* @param {object} args
|
||||
* @param {boolean} args.nest
|
||||
* @param {boolean} args.details
|
||||
* @param {boolean} args.split
|
||||
* @returns {Promise}
|
||||
* @memberof SQLiteClient
|
||||
*/
|
||||
async raw (sql, args) {
|
||||
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
|
||||
|
||||
args = {
|
||||
nest: false,
|
||||
details: false,
|
||||
split: true,
|
||||
comments: true,
|
||||
autocommit: true,
|
||||
...args
|
||||
};
|
||||
|
||||
if (!args.comments)
|
||||
sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');// Remove comments
|
||||
|
||||
const resultsArr = [];
|
||||
let paramsArr = [];
|
||||
const queries = args.split
|
||||
? sql.split(/((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/gm)
|
||||
.filter(Boolean)
|
||||
.map(q => q.trim())
|
||||
: [sql];
|
||||
|
||||
let connection;
|
||||
|
||||
if (!args.autocommit && args.tabUid) { // autocommit OFF
|
||||
if (this._connectionsToCommit.has(args.tabUid))
|
||||
connection = this._connectionsToCommit.get(args.tabUid);
|
||||
else {
|
||||
connection = this.getConnection();
|
||||
connection.prepare('BEGIN TRANSACTION').run();
|
||||
this._connectionsToCommit.set(args.tabUid, connection);
|
||||
}
|
||||
}
|
||||
else// autocommit ON
|
||||
connection = this._connection;
|
||||
|
||||
for (const query of queries) {
|
||||
if (!query) continue;
|
||||
const timeStart = new Date();
|
||||
let timeStop;
|
||||
const keysArr = [];
|
||||
|
||||
const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
let queryResult;
|
||||
let affectedRows;
|
||||
let fields;
|
||||
const detectedTypes = {};
|
||||
|
||||
try {
|
||||
const stmt = connection.prepare(query);
|
||||
|
||||
if (stmt.reader) {
|
||||
queryResult = stmt.all();
|
||||
fields = stmt.columns();
|
||||
|
||||
if (queryResult.length) {
|
||||
fields.forEach(field => {
|
||||
detectedTypes[field.name] = typeof queryResult[0][field.name];
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
const info = queryResult = stmt.run();
|
||||
affectedRows = info.changes;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
timeStop = new Date();
|
||||
|
||||
let remappedFields = fields
|
||||
? fields.map(field => {
|
||||
let [parsedType, length] = field.type?.includes('(')
|
||||
? field.type.replace(')', '').split('(').map(el => {
|
||||
if (!isNaN(el))
|
||||
el = +el;
|
||||
else
|
||||
el = el.trim();
|
||||
return el;
|
||||
})
|
||||
: [field.type, null];
|
||||
|
||||
if ([...TIME, ...DATETIME].includes(parsedType)) {
|
||||
const firstNotNull = queryResult.find(res => res[field.name] !== null);
|
||||
if (firstNotNull && firstNotNull[field.name].includes('.'))
|
||||
length = firstNotNull[field.name].split('.').pop().length;
|
||||
}
|
||||
|
||||
return {
|
||||
name: field.name,
|
||||
alias: field.name,
|
||||
orgName: field.column,
|
||||
schema: field.database,
|
||||
table: field.table,
|
||||
tableAlias: field.table,
|
||||
orgTable: field.table,
|
||||
type: field.type !== null ? parsedType : detectedTypes[field.name],
|
||||
length
|
||||
};
|
||||
}).filter(Boolean)
|
||||
: [];
|
||||
|
||||
if (args.details) {
|
||||
paramsArr = remappedFields.map(field => {
|
||||
return {
|
||||
table: field.table,
|
||||
schema: field.schema
|
||||
};
|
||||
}).filter((val, i, arr) => arr.findIndex(el => el.schema === val.schema && el.table === val.table) === i);
|
||||
|
||||
for (const paramObj of paramsArr) {
|
||||
if (!paramObj.table || !paramObj.schema) continue;
|
||||
|
||||
try {
|
||||
const indexes = await this.getTableIndexes(paramObj);
|
||||
|
||||
remappedFields = remappedFields.map(field => {
|
||||
// const detailedField = columns.find(f => f.name === field.name);
|
||||
const fieldIndex = indexes.find(i => i.column === field.name);
|
||||
if (field.table === paramObj.table && field.schema === paramObj.schema) {
|
||||
// if (detailedField) {
|
||||
// const length = detailedField.numPrecision || detailedField.charLength || detailedField.datePrecision || null;
|
||||
// field = { ...field, ...detailedField, length };
|
||||
// }
|
||||
|
||||
if (fieldIndex) {
|
||||
const key = fieldIndex.type === 'PRIMARY' ? 'pri' : fieldIndex.type === 'UNIQUE' ? 'uni' : 'mul';
|
||||
field = { ...field, key };
|
||||
};
|
||||
}
|
||||
|
||||
return field;
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolve({
|
||||
duration: timeStop - timeStart,
|
||||
rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false,
|
||||
report: affectedRows !== undefined ? { affectedRows } : null,
|
||||
fields: remappedFields,
|
||||
keys: keysArr
|
||||
});
|
||||
})();
|
||||
});
|
||||
|
||||
resultsArr.push({ rows, report, fields, keys, duration });
|
||||
}
|
||||
|
||||
return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
|
||||
}
|
||||
}
|
85
src/main/libs/exporters/BaseExporter.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import fs from 'fs';
|
||||
import { createGzip } from 'zlib';
|
||||
import path from 'path';
|
||||
import EventEmitter from 'events';
|
||||
|
||||
export class BaseExporter extends EventEmitter {
|
||||
constructor (tables, options) {
|
||||
super();
|
||||
this._tables = tables;
|
||||
this._options = options;
|
||||
this._isCancelled = false;
|
||||
this._outputFileStream = fs.createWriteStream(this._options.outputFile, { flags: 'w' });
|
||||
this._processedStream = null;
|
||||
this._state = {};
|
||||
|
||||
if (this._options.outputFormat === 'sql.zip') {
|
||||
const outputZipStream = createGzip();
|
||||
outputZipStream.pipe(this._outputFileStream);
|
||||
this._processedStream = outputZipStream;
|
||||
}
|
||||
else
|
||||
this._processedStream = this._outputFileStream;
|
||||
|
||||
this._processedStream.once('error', err => {
|
||||
this._isCancelled = true;
|
||||
this.emit('error', err);
|
||||
});
|
||||
}
|
||||
|
||||
async run () {
|
||||
try {
|
||||
this.emit('start', this);
|
||||
await this.dump();
|
||||
}
|
||||
catch (err) {
|
||||
this.emit('error', err);
|
||||
throw err;
|
||||
}
|
||||
finally {
|
||||
this._processedStream.end();
|
||||
this.emit('end');
|
||||
}
|
||||
}
|
||||
|
||||
get isCancelled () {
|
||||
return this._isCancelled;
|
||||
}
|
||||
|
||||
get outputFile () {
|
||||
return this._options.outputFile;
|
||||
}
|
||||
|
||||
outputFileExists () {
|
||||
return fs.existsSync(this._options.outputFile);
|
||||
}
|
||||
|
||||
cancel () {
|
||||
this._isCancelled = true;
|
||||
this.emit('cancel');
|
||||
this.emitUpdate({ op: 'cancelling' });
|
||||
}
|
||||
|
||||
emitUpdate (state) {
|
||||
this.emit('progress', { ...this._state, ...state });
|
||||
}
|
||||
|
||||
writeString (data) {
|
||||
if (this._isCancelled) return;
|
||||
|
||||
try {
|
||||
fs.accessSync(this._options.outputFile);
|
||||
}
|
||||
catch (err) {
|
||||
this._isCancelled = true;
|
||||
|
||||
const fileName = path.basename(this._options.outputFile);
|
||||
this.emit('error', `The file ${fileName} is not accessible`);
|
||||
}
|
||||
this._processedStream.write(data);
|
||||
}
|
||||
|
||||
dump () {
|
||||
throw new Error('Exporter must implement the "dump" method');
|
||||
}
|
||||
}
|
412
src/main/libs/exporters/sql/MysqlExporter.js
Normal file
@@ -0,0 +1,412 @@
|
||||
import { SqlExporter } from './SqlExporter';
|
||||
import { BLOB, BIT, DATE, DATETIME, FLOAT, SPATIAL, IS_MULTI_SPATIAL, NUMBER } from 'common/fieldTypes';
|
||||
import hexToBinary from 'common/libs/hexToBinary';
|
||||
import { getArrayDepth } from 'common/libs/getArrayDepth';
|
||||
import moment from 'moment';
|
||||
import { lineString, point, polygon } from '@turf/helpers';
|
||||
|
||||
export default class MysqlExporter extends SqlExporter {
|
||||
async getSqlHeader () {
|
||||
let dump = await super.getSqlHeader();
|
||||
dump += `
|
||||
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
SET NAMES utf8mb4;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;`;
|
||||
|
||||
return dump;
|
||||
}
|
||||
|
||||
async getFooter () {
|
||||
const footer = await super.getFooter();
|
||||
|
||||
return `/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
|
||||
${footer}
|
||||
`;
|
||||
}
|
||||
|
||||
async getCreateTable (tableName) {
|
||||
const { rows } = await this._client.raw(
|
||||
`SHOW CREATE TABLE \`${this.schemaName}\`.\`${tableName}\``
|
||||
);
|
||||
|
||||
if (rows.length !== 1) return '';
|
||||
|
||||
const col = 'Create View' in rows[0] ? 'Create View' : 'Create Table';
|
||||
|
||||
return rows[0][col] + ';';
|
||||
}
|
||||
|
||||
getDropTable (tableName) {
|
||||
return `DROP TABLE IF EXISTS \`${tableName}\`;`;
|
||||
}
|
||||
|
||||
async * getTableInsert (tableName) {
|
||||
let rowCount = 0;
|
||||
let sqlStr = '';
|
||||
|
||||
const countResults = await this._client.raw(`SELECT COUNT(1) as count FROM \`${this.schemaName}\`.\`${tableName}\``);
|
||||
if (countResults.rows.length === 1) rowCount = countResults.rows[0].count;
|
||||
|
||||
if (rowCount > 0) {
|
||||
let queryLength = 0;
|
||||
let rowsWritten = 0;
|
||||
let rowIndex = 0;
|
||||
const { sqlInsertDivider, sqlInsertAfter } = this._options;
|
||||
const columns = await this._client.getTableColumns({
|
||||
table: tableName,
|
||||
schema: this.schemaName
|
||||
});
|
||||
|
||||
const notGeneratedColumns = columns.filter(col => !col.generated);
|
||||
const columnNames = notGeneratedColumns.map(col => '`' + col.name + '`');
|
||||
const insertStmt = `INSERT INTO \`${tableName}\` (${columnNames.join(
|
||||
', '
|
||||
)}) VALUES`;
|
||||
|
||||
sqlStr += `LOCK TABLES \`${tableName}\` WRITE;\n`;
|
||||
sqlStr += `/*!40000 ALTER TABLE \`${tableName}\` DISABLE KEYS */;`;
|
||||
sqlStr += '\n\n';
|
||||
yield sqlStr;
|
||||
|
||||
yield insertStmt;
|
||||
|
||||
const stream = await this._queryStream(
|
||||
`SELECT ${columnNames.join(', ')} FROM \`${this.schemaName}\`.\`${tableName}\``
|
||||
);
|
||||
|
||||
for await (const row of stream) {
|
||||
if (this.isCancelled) {
|
||||
stream.destroy();
|
||||
yield null;
|
||||
return;
|
||||
}
|
||||
|
||||
let sqlInsertString = '';
|
||||
|
||||
if (
|
||||
(sqlInsertDivider === 'bytes' && queryLength >= sqlInsertAfter * 1024) ||
|
||||
(sqlInsertDivider === 'rows' && rowsWritten === sqlInsertAfter)
|
||||
) {
|
||||
sqlInsertString += `;\n${insertStmt}\n\t(`;
|
||||
queryLength = 0;
|
||||
rowsWritten = 0;
|
||||
}
|
||||
else if (parseInt(rowIndex) === 0) sqlInsertString += '\n\t(';
|
||||
else sqlInsertString += ',\n\t(';
|
||||
|
||||
for (const i in notGeneratedColumns) {
|
||||
const column = notGeneratedColumns[i];
|
||||
const val = row[column.name];
|
||||
|
||||
if (val === null) sqlInsertString += 'NULL';
|
||||
else if (DATE.includes(column.type)) {
|
||||
sqlInsertString += moment(val).isValid()
|
||||
? this.escapeAndQuote(moment(val).format('YYYY-MM-DD'))
|
||||
: val;
|
||||
}
|
||||
else if (DATETIME.includes(column.type)) {
|
||||
let datePrecision = '';
|
||||
for (let i = 0; i < column.precision; i++)
|
||||
datePrecision += i === 0 ? '.S' : 'S';
|
||||
|
||||
sqlInsertString += moment(val).isValid()
|
||||
? this.escapeAndQuote(moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`))
|
||||
: this.escapeAndQuote(val);
|
||||
}
|
||||
else if (BIT.includes(column.type))
|
||||
sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex'))}'`;
|
||||
else if (BLOB.includes(column.type))
|
||||
sqlInsertString += `X'${val.toString('hex').toUpperCase()}'`;
|
||||
else if (NUMBER.includes(column.type))
|
||||
sqlInsertString += val;
|
||||
else if (FLOAT.includes(column.type))
|
||||
sqlInsertString += parseFloat(val);
|
||||
else if (SPATIAL.includes(column.type)) {
|
||||
let geoJson;
|
||||
if (IS_MULTI_SPATIAL.includes(column.type)) {
|
||||
const features = [];
|
||||
for (const element of val)
|
||||
features.push(this.getMarkers(element));
|
||||
|
||||
geoJson = {
|
||||
type: 'FeatureCollection',
|
||||
features
|
||||
};
|
||||
}
|
||||
else
|
||||
geoJson = this._getGeoJSON(val);
|
||||
|
||||
sqlInsertString += `ST_GeomFromGeoJSON('${JSON.stringify(geoJson)}')`;
|
||||
}
|
||||
else if (val === '') sqlInsertString += '\'\'';
|
||||
else {
|
||||
sqlInsertString += typeof val === 'string'
|
||||
? this.escapeAndQuote(val)
|
||||
: typeof val === 'object'
|
||||
? this.escapeAndQuote(JSON.stringify(val))
|
||||
: val;
|
||||
}
|
||||
|
||||
if (parseInt(i) !== notGeneratedColumns.length - 1)
|
||||
sqlInsertString += ', ';
|
||||
}
|
||||
|
||||
sqlInsertString += ')';
|
||||
|
||||
queryLength += sqlInsertString.length;
|
||||
rowsWritten++;
|
||||
rowIndex++;
|
||||
yield sqlInsertString;
|
||||
}
|
||||
|
||||
sqlStr = ';\n\n';
|
||||
sqlStr += `/*!40000 ALTER TABLE \`${tableName}\` ENABLE KEYS */;\n`;
|
||||
sqlStr += 'UNLOCK TABLES;';
|
||||
|
||||
yield sqlStr;
|
||||
}
|
||||
}
|
||||
|
||||
async getViews () {
|
||||
const { rows: views } = await this._client.raw(
|
||||
`SHOW TABLE STATUS FROM \`${this.schemaName}\` WHERE Comment = 'VIEW'`
|
||||
);
|
||||
let sqlString = '';
|
||||
|
||||
for (const view of views) {
|
||||
sqlString += `DROP VIEW IF EXISTS \`${view.Name}\`;\n`;
|
||||
const viewSyntax = await this.getCreateTable(view.Name);
|
||||
sqlString += viewSyntax.replaceAll('`' + this.schemaName + '`.', '');
|
||||
sqlString += '\n';
|
||||
}
|
||||
|
||||
return sqlString;
|
||||
}
|
||||
|
||||
async getTriggers () {
|
||||
const { rows: triggers } = await this._client.raw(
|
||||
`SHOW TRIGGERS FROM \`${this.schemaName}\``
|
||||
);
|
||||
const generatedTables = this._tables
|
||||
.filter(t => t.includeStructure)
|
||||
.map(t => t.table);
|
||||
|
||||
let sqlString = '';
|
||||
|
||||
for (const trigger of triggers) {
|
||||
const {
|
||||
Trigger: name,
|
||||
Timing: timing,
|
||||
Event: event,
|
||||
Table: table,
|
||||
Statement: statement,
|
||||
sql_mode: sqlMode
|
||||
} = trigger;
|
||||
|
||||
if (!generatedTables.includes(table)) continue;
|
||||
|
||||
const definer = this.getEscapedDefiner(trigger.Definer);
|
||||
sqlString += '/*!50003 SET @OLD_SQL_MODE=@@SQL_MODE*/;;\n';
|
||||
sqlString += `/*!50003 SET SQL_MODE="${sqlMode}" */;\n`;
|
||||
sqlString += 'DELIMITER ;;\n';
|
||||
sqlString += '/*!50003 CREATE*/ ';
|
||||
sqlString += `/*!50017 DEFINER=${definer}*/ `;
|
||||
sqlString += `/*!50003 TRIGGER \`${name}\` ${timing} ${event} ON \`${table}\` FOR EACH ROW ${statement}*/;;\n`;
|
||||
sqlString += 'DELIMITER ;\n';
|
||||
sqlString += '/*!50003 SET SQL_MODE=@OLD_SQL_MODE */;\n\n';
|
||||
}
|
||||
|
||||
return sqlString;
|
||||
}
|
||||
|
||||
async getSchedulers () {
|
||||
const { rows: schedulers } = await this._client.raw(
|
||||
`SELECT *, EVENT_SCHEMA AS \`Db\`, EVENT_NAME AS \`Name\` FROM information_schema.\`EVENTS\` WHERE EVENT_SCHEMA = '${this.schemaName}'`
|
||||
);
|
||||
let sqlString = '';
|
||||
|
||||
for (const scheduler of schedulers) {
|
||||
const {
|
||||
EVENT_NAME: name,
|
||||
SQL_MODE: sqlMode,
|
||||
EVENT_TYPE: type,
|
||||
INTERVAL_VALUE: intervalValue,
|
||||
INTERVAL_FIELD: intervalField,
|
||||
STARTS: starts,
|
||||
ENDS: ends,
|
||||
EXECUTE_AT: at,
|
||||
ON_COMPLETION: onCompletion,
|
||||
STATUS: status,
|
||||
EVENT_DEFINITION: definition
|
||||
} = scheduler;
|
||||
|
||||
const definer = this.getEscapedDefiner(scheduler.DEFINER);
|
||||
const comment = this.escapeAndQuote(scheduler.EVENT_COMMENT);
|
||||
|
||||
sqlString += `/*!50106 DROP EVENT IF EXISTS \`${name}\` */;\n`;
|
||||
sqlString += '/*!50003 SET @OLD_SQL_MODE=@@SQL_MODE*/;;\n';
|
||||
sqlString += `/*!50003 SET SQL_MODE='${sqlMode}' */;\n`;
|
||||
sqlString += 'DELIMITER ;;\n';
|
||||
sqlString += '/*!50106 CREATE*/ ';
|
||||
sqlString += `/*!50117 DEFINER=${definer}*/ `;
|
||||
sqlString += `/*!50106 EVENT \`${name}\` ON SCHEDULE `;
|
||||
if (type === 'RECURRING') {
|
||||
sqlString += `EVERY ${intervalValue} ${intervalField} STARTS '${starts}' `;
|
||||
|
||||
if (ends) sqlString += `ENDS '${ends}' `;
|
||||
}
|
||||
else sqlString += `AT '${at}' `;
|
||||
sqlString += `ON COMPLETION ${onCompletion} ${
|
||||
status === 'disabled' ? 'DISABLE' : 'ENABLE'
|
||||
} COMMENT ${comment || '\'\''} DO ${definition}*/;;\n`;
|
||||
sqlString += 'DELIMITER ;\n';
|
||||
sqlString += '/*!50003 SET SQL_MODE=@OLD_SQL_MODE*/;;\n';
|
||||
}
|
||||
|
||||
return sqlString;
|
||||
}
|
||||
|
||||
async getFunctions () {
|
||||
const { rows: functions } = await this._client.raw(
|
||||
`SHOW FUNCTION STATUS WHERE \`Db\` = '${this.schemaName}';`
|
||||
);
|
||||
|
||||
let sqlString = '';
|
||||
|
||||
for (const func of functions) {
|
||||
const definer = this.getEscapedDefiner(func.Definer);
|
||||
sqlString += await this.getRoutineSyntax(
|
||||
func.Name,
|
||||
func.Type,
|
||||
definer
|
||||
);
|
||||
}
|
||||
|
||||
return sqlString;
|
||||
}
|
||||
|
||||
async getRoutines () {
|
||||
const { rows: routines } = await this._client.raw(
|
||||
`SHOW PROCEDURE STATUS WHERE \`Db\` = '${this.schemaName}';`
|
||||
);
|
||||
|
||||
let sqlString = '';
|
||||
|
||||
for (const routine of routines) {
|
||||
const definer = this.getEscapedDefiner(routine.Definer);
|
||||
|
||||
sqlString += await this.getRoutineSyntax(
|
||||
routine.Name,
|
||||
routine.Type,
|
||||
definer
|
||||
);
|
||||
}
|
||||
|
||||
return sqlString;
|
||||
}
|
||||
|
||||
async getRoutineSyntax (name, type, definer) {
|
||||
const { rows: routines } = await this._client.raw(
|
||||
`SHOW CREATE ${type} \`${this.schemaName}\`.\`${name}\``
|
||||
);
|
||||
|
||||
if (routines.length === 0) return '';
|
||||
|
||||
const routine = routines[0];
|
||||
|
||||
const fieldName = `Create ${type === 'PROCEDURE' ? 'Procedure' : 'Function'}`;
|
||||
const sqlMode = routine.sql_mode;
|
||||
const createProcedure = routine[fieldName];
|
||||
let sqlString = '';
|
||||
|
||||
if (createProcedure) { // If procedure body not empty
|
||||
const startOffset = createProcedure.indexOf(type);
|
||||
const procedureBody = createProcedure.substring(startOffset);
|
||||
|
||||
sqlString += `/*!50003 DROP ${type} IF EXISTS ${name}*/;;\n`;
|
||||
sqlString += '/*!50003 SET @OLD_SQL_MODE=@@SQL_MODE*/;;\n';
|
||||
sqlString += `/*!50003 SET SQL_MODE="${sqlMode}"*/;;\n`;
|
||||
sqlString += 'DELIMITER ;;\n';
|
||||
sqlString += `/*!50003 CREATE*/ /*!50020 DEFINER=${definer}*/ /*!50003 ${procedureBody}*/;;\n`;
|
||||
sqlString += 'DELIMITER ;\n';
|
||||
sqlString += '/*!50003 SET SQL_MODE=@OLD_SQL_MODE*/;\n';
|
||||
}
|
||||
|
||||
return sqlString;
|
||||
}
|
||||
|
||||
async _queryStream (sql) {
|
||||
if (process.env.NODE_ENV === 'development') console.log('EXPORTER:', sql);
|
||||
const isPool = typeof this._client._connection.getConnection === 'function';
|
||||
const connection = isPool ? await this._client._connection.getConnection() : this._client._connection;
|
||||
const stream = connection.connection.query(sql).stream();
|
||||
const dispose = () => connection.destroy();
|
||||
|
||||
stream.on('end', dispose);
|
||||
stream.on('error', dispose);
|
||||
stream.on('close', dispose);
|
||||
return stream;
|
||||
}
|
||||
|
||||
getEscapedDefiner (definer) {
|
||||
return definer
|
||||
.split('@')
|
||||
.map(part => '`' + part + '`')
|
||||
.join('@');
|
||||
}
|
||||
|
||||
escapeAndQuote (val) {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g;
|
||||
const CHARS_ESCAPE_MAP = {
|
||||
'\0': '\\0',
|
||||
'\b': '\\b',
|
||||
'\t': '\\t',
|
||||
'\n': '\\n',
|
||||
'\r': '\\r',
|
||||
'\x1a': '\\Z',
|
||||
'"': '\\"',
|
||||
'\'': '\\\'',
|
||||
'\\': '\\\\'
|
||||
};
|
||||
let chunkIndex = CHARS_TO_ESCAPE.lastIndex = 0;
|
||||
let escapedVal = '';
|
||||
let match;
|
||||
|
||||
while ((match = CHARS_TO_ESCAPE.exec(val))) {
|
||||
escapedVal += val.slice(chunkIndex, match.index) + CHARS_ESCAPE_MAP[match[0]];
|
||||
chunkIndex = CHARS_TO_ESCAPE.lastIndex;
|
||||
}
|
||||
|
||||
if (chunkIndex === 0)
|
||||
return `'${val}'`;
|
||||
|
||||
if (chunkIndex < val.length)
|
||||
return `'${escapedVal + val.slice(chunkIndex)}'`;
|
||||
|
||||
return `'${escapedVal}'`;
|
||||
}
|
||||
|
||||
_getGeoJSON (val) {
|
||||
if (Array.isArray(val)) {
|
||||
if (getArrayDepth(val) === 1)
|
||||
return lineString(val.reduce((acc, curr) => [...acc, [curr.x, curr.y]], []));
|
||||
else
|
||||
return polygon(val.map(arr => arr.reduce((acc, curr) => [...acc, [curr.x, curr.y]], [])));
|
||||
}
|
||||
else
|
||||
return point([val.x, val.y]);
|
||||
}
|
||||
}
|
162
src/main/libs/exporters/sql/SqlExporter.js
Normal file
@@ -0,0 +1,162 @@
|
||||
import moment from 'moment';
|
||||
import { BaseExporter } from '../BaseExporter';
|
||||
|
||||
export class SqlExporter extends BaseExporter {
|
||||
constructor (client, tables, options) {
|
||||
super(tables, options);
|
||||
this._client = client;
|
||||
this._commentChar = '#';
|
||||
}
|
||||
|
||||
get schemaName () {
|
||||
return this._options.schema;
|
||||
}
|
||||
|
||||
get host () {
|
||||
return this._client._params.host;
|
||||
}
|
||||
|
||||
async getServerVersion () {
|
||||
const version = await this._client.getVersion();
|
||||
return `${version.name} ${version.number}`;
|
||||
}
|
||||
|
||||
async dump () {
|
||||
const { includes } = this._options;
|
||||
const extraItems = Object.keys(includes).filter(key => includes[key]);
|
||||
const totalTableToProcess = this._tables.filter(
|
||||
t => t.includeStructure || t.includeContent || t.includeDropStatement
|
||||
).length;
|
||||
const processingItemCount = totalTableToProcess + extraItems.length;
|
||||
|
||||
const exportState = {
|
||||
totalItems: processingItemCount,
|
||||
currentItemIndex: 0,
|
||||
currentItem: '',
|
||||
op: ''
|
||||
};
|
||||
|
||||
const header = await this.getSqlHeader();
|
||||
this.writeString(header);
|
||||
this.writeString('\n\n\n');
|
||||
|
||||
for (const item of this._tables) {
|
||||
// user abort operation
|
||||
if (this.isCancelled) return;
|
||||
|
||||
// skip item if not set to output any detail for them
|
||||
if (
|
||||
!item.includeStructure &&
|
||||
!item.includeContent &&
|
||||
!item.includeDropStatement
|
||||
)
|
||||
continue;
|
||||
|
||||
exportState.currentItemIndex++;
|
||||
exportState.currentItem = item.table;
|
||||
exportState.op = 'FETCH';
|
||||
|
||||
this.emitUpdate(exportState);
|
||||
|
||||
const tableHeader = this.buildComment(
|
||||
`Dump of table ${item.table}\n------------------------------------------------------------`
|
||||
);
|
||||
this.writeString(tableHeader);
|
||||
this.writeString('\n\n');
|
||||
|
||||
if (item.includeDropStatement) {
|
||||
const dropTableSyntax = this.getDropTable(item.table);
|
||||
this.writeString(dropTableSyntax);
|
||||
this.writeString('\n\n');
|
||||
}
|
||||
|
||||
if (item.includeStructure) {
|
||||
const createTableSyntax = await this.getCreateTable(item.table);
|
||||
this.writeString(createTableSyntax);
|
||||
this.writeString('\n\n');
|
||||
}
|
||||
|
||||
if (item.includeContent) {
|
||||
exportState.op = 'WRITE';
|
||||
this.emitUpdate(exportState);
|
||||
for await (const sqlStr of this.getTableInsert(item.table)) {
|
||||
if (this.isCancelled) return;
|
||||
this.writeString(sqlStr);
|
||||
}
|
||||
|
||||
this.writeString('\n\n');
|
||||
}
|
||||
|
||||
this.writeString('\n\n');
|
||||
}
|
||||
|
||||
for (const item of extraItems) {
|
||||
const processingMethod = `get${item.charAt(0).toUpperCase() + item.slice(1)}`;
|
||||
exportState.currentItemIndex++;
|
||||
exportState.currentItem = item;
|
||||
exportState.op = 'PROCESSING';
|
||||
this.emitUpdate(exportState);
|
||||
|
||||
if (this[processingMethod]) {
|
||||
const data = await this[processingMethod]();
|
||||
if (data !== '') {
|
||||
const header =
|
||||
this.buildComment(
|
||||
`Dump of ${item}\n------------------------------------------------------------`
|
||||
) + '\n\n';
|
||||
|
||||
this.writeString(header);
|
||||
this.writeString(data);
|
||||
this.writeString('\n\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const footer = await this.getFooter();
|
||||
this.writeString(footer);
|
||||
}
|
||||
|
||||
buildComment (text) {
|
||||
return text
|
||||
.split('\n')
|
||||
.map(txt => `${this._commentChar} ${txt}`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
async getSqlHeader () {
|
||||
const serverVersion = await this.getServerVersion();
|
||||
const header = `************************************************************
|
||||
Antares - SQL Client
|
||||
Version ${process.env.PACKAGE_VERSION}
|
||||
|
||||
https://antares-sql.app/
|
||||
https://github.com/Fabio286/antares
|
||||
|
||||
Host: ${this.host} (${serverVersion})
|
||||
Database: ${this.schemaName}
|
||||
Generation time: ${moment().format()}
|
||||
************************************************************`;
|
||||
|
||||
return this.buildComment(header);
|
||||
}
|
||||
|
||||
async getFooter () {
|
||||
return this.buildComment(`Dump completed on ${moment().format()}`);
|
||||
}
|
||||
|
||||
getCreateTable (tableName) {
|
||||
throw new Error(
|
||||
'Sql Exporter must implement the "getCreateTable" method'
|
||||
);
|
||||
}
|
||||
|
||||
getDropTable (tableName) {
|
||||
throw new Error('Sql Exporter must implement the "getDropTable" method');
|
||||
}
|
||||
|
||||
getTableInsert (tableName) {
|
||||
throw new Error(
|
||||
'Sql Exporter must implement the "getTableInsert" method'
|
||||
);
|
||||
}
|
||||
}
|
53
src/main/libs/importers/BaseImporter.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import fs from 'fs';
|
||||
import EventEmitter from 'events';
|
||||
|
||||
export class BaseImporter extends EventEmitter {
|
||||
constructor (options) {
|
||||
super();
|
||||
this._options = options;
|
||||
this._isCancelled = false;
|
||||
this._fileHandler = fs.createReadStream(this._options.file, {
|
||||
flags: 'r',
|
||||
highWaterMark: 4 * 1024
|
||||
});
|
||||
this._state = {};
|
||||
|
||||
this._fileHandler.once('error', err => {
|
||||
this._isCancelled = true;
|
||||
this.emit('error', err);
|
||||
});
|
||||
}
|
||||
|
||||
async run () {
|
||||
try {
|
||||
this.emit('start', this);
|
||||
await this.import();
|
||||
}
|
||||
catch (err) {
|
||||
this.emit('error', err);
|
||||
throw err;
|
||||
}
|
||||
finally {
|
||||
this._fileHandler.close();
|
||||
this.emit('end');
|
||||
}
|
||||
}
|
||||
|
||||
get isCancelled () {
|
||||
return this._isCancelled;
|
||||
}
|
||||
|
||||
cancel () {
|
||||
this._isCancelled = true;
|
||||
this.emit('cancel');
|
||||
this.emitUpdate({ op: 'cancelling' });
|
||||
}
|
||||
|
||||
emitUpdate (state) {
|
||||
this.emit('progress', { ...this._state, ...state });
|
||||
}
|
||||
|
||||
import () {
|
||||
throw new Error('Exporter must implement the "import" method');
|
||||
}
|
||||
}
|
85
src/main/libs/importers/sql/MysqlImporter.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import fs from 'fs/promises';
|
||||
import SqlParser from '../../../../common/libs/sqlParser';
|
||||
import { BaseImporter } from '../BaseImporter';
|
||||
|
||||
export default class MysqlImporter extends BaseImporter {
|
||||
constructor (client, options) {
|
||||
super(options);
|
||||
this._client = client;
|
||||
}
|
||||
|
||||
async import () {
|
||||
try {
|
||||
const { size: totalFileSize } = await fs.stat(this._options.file);
|
||||
const parser = new SqlParser();
|
||||
let readPosition = 0;
|
||||
let queryCount = 0;
|
||||
|
||||
this.emitUpdate({
|
||||
fileSize: totalFileSize,
|
||||
readPosition: 0,
|
||||
percentage: 0,
|
||||
queryCount: 0
|
||||
});
|
||||
|
||||
// 1. detect file encoding
|
||||
// 2. set fh encoding
|
||||
// 3. detect sql mode
|
||||
// 4. restore sql mode in case of exception
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this._fileHandler.pipe(parser);
|
||||
|
||||
parser.on('error', reject);
|
||||
|
||||
parser.on('close', async () => {
|
||||
console.log('TOTAL QUERIES', queryCount);
|
||||
console.log('import end');
|
||||
resolve();
|
||||
});
|
||||
|
||||
parser.on('data', async (query) => {
|
||||
queryCount++;
|
||||
parser.pause();
|
||||
|
||||
try {
|
||||
await this._client.query(query);
|
||||
}
|
||||
catch (error) {
|
||||
this.emit('query-error', {
|
||||
sql: query,
|
||||
message: error.sqlMessage,
|
||||
sqlSnippet: error.sql,
|
||||
time: new Date().getTime()
|
||||
});
|
||||
}
|
||||
|
||||
this.emitUpdate({
|
||||
queryCount,
|
||||
readPosition,
|
||||
percentage: readPosition / totalFileSize * 100
|
||||
});
|
||||
this._fileHandler.pipe(parser);
|
||||
parser.resume();
|
||||
});
|
||||
|
||||
parser.on('pause', () => {
|
||||
this._fileHandler.unpipe(parser);
|
||||
this._fileHandler.readableFlowing = false;
|
||||
});
|
||||
|
||||
this._fileHandler.on('data', (chunk) => {
|
||||
readPosition += chunk.length;
|
||||
});
|
||||
|
||||
this._fileHandler.on('error', (err) => {
|
||||
console.log(err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
}
|
172
src/main/main.js
Normal file
@@ -0,0 +1,172 @@
|
||||
'use strict';
|
||||
|
||||
import { app, BrowserWindow, /* session, */ nativeImage, Menu } from 'electron';
|
||||
import * as path from 'path';
|
||||
import Store from 'electron-store';
|
||||
import * as windowStateKeeper from 'electron-window-state';
|
||||
import * as remoteMain from '@electron/remote/main';
|
||||
|
||||
import ipcHandlers from './ipc-handlers';
|
||||
|
||||
// remoteMain.initialize();
|
||||
Store.initRenderer();
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
const isMacOS = process.platform === 'darwin';
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
|
||||
|
||||
// global reference to mainWindow (necessary to prevent window from being garbage collected)
|
||||
let mainWindow;
|
||||
let mainWindowState;
|
||||
|
||||
async function createMainWindow () {
|
||||
const icon = require('../renderer/images/logo-32.png');
|
||||
const window = new BrowserWindow({
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
minWidth: 900,
|
||||
minHeight: 550,
|
||||
title: 'Antares SQL',
|
||||
autoHideMenuBar: true,
|
||||
icon: nativeImage.createFromDataURL(icon.default),
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
'web-security': false,
|
||||
spellcheck: false
|
||||
},
|
||||
frame: false,
|
||||
titleBarStyle: isMacOS ? 'hidden' : 'default',
|
||||
trafficLightPosition: isMacOS ? { x: 10, y: 8 } : undefined,
|
||||
backgroundColor: '#1d1d1d'
|
||||
});
|
||||
|
||||
mainWindowState.manage(window);
|
||||
window.on('moved', saveWindowState);
|
||||
|
||||
remoteMain.enable(window.webContents);
|
||||
|
||||
try {
|
||||
if (isDevelopment) {
|
||||
const { default: installExtension, VUEJS3_DEVTOOLS } = require('electron-devtools-installer');
|
||||
const options = {
|
||||
loadExtensionOptions: { allowFileAccess: true }
|
||||
};
|
||||
|
||||
try {
|
||||
const name = await installExtension(VUEJS3_DEVTOOLS, options);
|
||||
console.log(`Added Extension: ${name}`);
|
||||
}
|
||||
catch (err) {
|
||||
console.log('An error occurred: ', err);
|
||||
}
|
||||
|
||||
await window.loadURL('http://localhost:9080');
|
||||
}
|
||||
else {
|
||||
const indexPath = path.resolve(__dirname, 'index.html');
|
||||
await window.loadFile(indexPath);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
window.on('closed', () => {
|
||||
window.removeListener('moved', saveWindowState);
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
if (!gotTheLock) app.quit();
|
||||
else {
|
||||
require('@electron/remote/main').initialize();
|
||||
|
||||
// Initialize ipcHandlers
|
||||
ipcHandlers();
|
||||
|
||||
// quit application when all windows are closed
|
||||
app.on('window-all-closed', () => {
|
||||
// on macOS it is common for applications to stay open until the user explicitly quits
|
||||
if (isMacOS) app.quit();
|
||||
});
|
||||
|
||||
app.on('activate', async () => {
|
||||
// on macOS it is common to re-create a window even after all windows have been closed
|
||||
if (mainWindow === null)
|
||||
mainWindow = await createMainWindow();
|
||||
});
|
||||
|
||||
// create main BrowserWindow when electron is ready
|
||||
app.on('ready', async () => {
|
||||
mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1024,
|
||||
defaultHeight: 800
|
||||
});
|
||||
|
||||
mainWindow = await createMainWindow();
|
||||
createAppMenu();
|
||||
|
||||
// if (isDevelopment)
|
||||
// mainWindow.webContents.openDevTools();
|
||||
|
||||
process.on('uncaughtException', error => {
|
||||
mainWindow.webContents.send('unhandled-exception', error);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', error => {
|
||||
mainWindow.webContents.send('unhandled-exception', error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createAppMenu () {
|
||||
let menu = null;
|
||||
|
||||
if (isMacOS) {
|
||||
menu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: app.name,
|
||||
submenu: [
|
||||
{ role: 'about' },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Check for Updates...',
|
||||
click: (_menuItem, win) => win.webContents.send('open-updates-preferences')
|
||||
},
|
||||
{
|
||||
label: 'Preferences',
|
||||
click: (_menuItem, win) => win.webContents.send('toggle-preferences'),
|
||||
accelerator: 'CmdOrCtrl+,'
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide' },
|
||||
{ role: 'hideOthers' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'quit' }
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'editMenu'
|
||||
},
|
||||
{
|
||||
role: 'viewMenu'
|
||||
},
|
||||
{
|
||||
role: 'windowMenu'
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
|
||||
function saveWindowState () {
|
||||
mainWindowState.saveState(mainWindow);
|
||||
}
|
60
src/main/workers/exporter.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { ClientsFactory } from '../libs/ClientsFactory';
|
||||
import MysqlExporter from '../libs/exporters/sql/MysqlExporter.js';
|
||||
import fs from 'fs';
|
||||
let exporter;
|
||||
|
||||
process.on('message', async ({ type, client, tables, options }) => {
|
||||
if (type === 'init') {
|
||||
const connection = await ClientsFactory.getClient({
|
||||
client: client.name,
|
||||
params: client.config,
|
||||
poolSize: 5
|
||||
});
|
||||
await connection.connect();
|
||||
|
||||
switch (client.name) {
|
||||
case 'mysql':
|
||||
case 'maria':
|
||||
exporter = new MysqlExporter(connection, tables, options);
|
||||
break;
|
||||
default:
|
||||
process.send({
|
||||
type: 'error',
|
||||
payload: `"${client.name}" exporter not aviable`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
exporter.once('error', err => {
|
||||
console.error(err);
|
||||
process.send({
|
||||
type: 'error',
|
||||
payload: err.toString()
|
||||
});
|
||||
});
|
||||
|
||||
exporter.once('end', () => {
|
||||
process.send({
|
||||
type: 'end',
|
||||
payload: { cancelled: exporter.isCancelled }
|
||||
});
|
||||
connection.destroy();
|
||||
});
|
||||
|
||||
exporter.once('cancel', () => {
|
||||
fs.unlinkSync(exporter.outputFile);
|
||||
process.send({ type: 'cancel' });
|
||||
});
|
||||
|
||||
exporter.on('progress', state => {
|
||||
process.send({
|
||||
type: 'export-progress',
|
||||
payload: state
|
||||
});
|
||||
});
|
||||
|
||||
exporter.run();
|
||||
}
|
||||
else if (type === 'cancel')
|
||||
exporter.cancel();
|
||||
});
|
68
src/main/workers/importer.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import { ClientsFactory } from '../libs/ClientsFactory';
|
||||
import MysqlImporter from '../libs/importers/sql/MysqlImporter';
|
||||
let importer;
|
||||
|
||||
process.on('message', async ({ type, dbConfig, options }) => {
|
||||
if (type === 'init') {
|
||||
const connection = await ClientsFactory.getClient({
|
||||
client: options.type,
|
||||
params: {
|
||||
...dbConfig,
|
||||
schema: options.schema
|
||||
},
|
||||
poolSize: 1
|
||||
});
|
||||
|
||||
const pool = await connection.getConnectionPool();
|
||||
|
||||
switch (options.type) {
|
||||
case 'mysql':
|
||||
case 'maria':
|
||||
importer = new MysqlImporter(pool, options);
|
||||
break;
|
||||
default:
|
||||
process.send({
|
||||
type: 'error',
|
||||
payload: `"${options.type}" importer not aviable`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
importer.once('error', err => {
|
||||
console.error(err);
|
||||
process.send({
|
||||
type: 'error',
|
||||
payload: err.toString()
|
||||
});
|
||||
});
|
||||
|
||||
importer.once('end', () => {
|
||||
process.send({
|
||||
type: 'end',
|
||||
payload: { cancelled: importer.isCancelled }
|
||||
});
|
||||
});
|
||||
|
||||
importer.once('cancel', () => {
|
||||
process.send({ type: 'cancel' });
|
||||
});
|
||||
|
||||
importer.on('progress', state => {
|
||||
process.send({
|
||||
type: 'import-progress',
|
||||
payload: state
|
||||
});
|
||||
});
|
||||
|
||||
importer.on('query-error', state => {
|
||||
process.send({
|
||||
type: 'query-error',
|
||||
payload: state
|
||||
});
|
||||
});
|
||||
|
||||
importer.run();
|
||||
}
|
||||
else if (type === 'cancel')
|
||||
importer.cancel();
|
||||
});
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div id="wrapper" :class="`theme-${applicationTheme}`">
|
||||
<div id="wrapper" :class="[`theme-${applicationTheme}`, !disableBlur || 'no-blur']">
|
||||
<TheTitleBar />
|
||||
<div id="window-content">
|
||||
<TheSettingBar />
|
||||
@@ -10,7 +10,9 @@
|
||||
:key="connection.uid"
|
||||
:connection="connection"
|
||||
/>
|
||||
<WorkspaceAddConnectionPanel v-if="selectedWorkspace === 'NEW'" />
|
||||
<div class="connection-panel-wrapper">
|
||||
<WorkspaceAddConnectionPanel v-if="selectedWorkspace === 'NEW'" />
|
||||
</div>
|
||||
</div>
|
||||
<TheFooter />
|
||||
<TheNotificationsBoard />
|
||||
@@ -51,6 +53,7 @@ export default {
|
||||
isScratchpad: 'application/isScratchpad',
|
||||
connections: 'connections/getConnections',
|
||||
applicationTheme: 'settings/getApplicationTheme',
|
||||
disableBlur: 'settings/getDisableBlur',
|
||||
isUnsavedDiscardModal: 'workspaces/isUnsavedDiscardModal'
|
||||
})
|
||||
},
|
||||
@@ -130,5 +133,15 @@ export default {
|
||||
> .columns {
|
||||
height: calc(100vh - #{$footer-height});
|
||||
}
|
||||
|
||||
.connection-panel-wrapper{
|
||||
height: calc(100vh - #{$excluding-size});
|
||||
width: 100%;
|
||||
padding-top: 15vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -110,5 +110,4 @@ export default {
|
||||
.modal.modal-sm .modal-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
108
src/renderer/components/BaseMap.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div id="map" class="map" />
|
||||
</template>
|
||||
<script>
|
||||
import L from 'leaflet';
|
||||
import {
|
||||
point,
|
||||
lineString,
|
||||
polygon
|
||||
} from '@turf/helpers';
|
||||
import { getArrayDepth } from 'common/libs/getArrayDepth';
|
||||
|
||||
export default {
|
||||
name: 'BaseMap',
|
||||
props: {
|
||||
points: [Object, Array],
|
||||
isMultiSpatial: Boolean
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
map: null,
|
||||
markers: [],
|
||||
center: null
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
if (this.isMultiSpatial) {
|
||||
for (const element of this.points)
|
||||
this.markers.push(this.getMarkers(element));
|
||||
}
|
||||
else {
|
||||
this.markers = this.getMarkers(this.points);
|
||||
|
||||
if (!Array.isArray(this.points))
|
||||
this.center = [this.points.y, this.points.x];
|
||||
}
|
||||
|
||||
this.map = L.map('map', {
|
||||
center: this.center || [0, 0],
|
||||
zoom: 15,
|
||||
minZoom: 1,
|
||||
attributionControl: false
|
||||
});
|
||||
|
||||
L.control.attribution({ prefix: '<b>Leaflet</b>' }).addTo(this.map);
|
||||
|
||||
const geoJsonObj = L.geoJSON(this.markers, {
|
||||
style: function () {
|
||||
return {
|
||||
weight: 2,
|
||||
fillColor: '#ff7800',
|
||||
color: '#ff7800',
|
||||
opacity: 0.8,
|
||||
fillOpacity: 0.4
|
||||
};
|
||||
},
|
||||
pointToLayer: function (feature, latlng) {
|
||||
return L.circleMarker(latlng, {
|
||||
radius: 7,
|
||||
weight: 2,
|
||||
fillColor: '#ff7800',
|
||||
color: '#ff7800',
|
||||
opacity: 0.8,
|
||||
fillOpacity: 0.4
|
||||
});
|
||||
}
|
||||
}).addTo(this.map);
|
||||
|
||||
const southWest = L.latLng(-90, -180);
|
||||
const northEast = L.latLng(90, 180);
|
||||
const bounds = L.latLngBounds(southWest, northEast);
|
||||
this.map.setMaxBounds(bounds);
|
||||
|
||||
if (!this.center) this.map.fitBounds(geoJsonObj.getBounds());
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <b>OpenStreetMap</b>'
|
||||
}).addTo(this.map);
|
||||
},
|
||||
methods: {
|
||||
getMarkers (points) {
|
||||
if (Array.isArray(points)) {
|
||||
if (getArrayDepth(points) === 1)
|
||||
return lineString(points.reduce((acc, curr) => [...acc, [curr.x, curr.y]], []));
|
||||
else
|
||||
return polygon(points.map(arr => arr.reduce((acc, curr) => [...acc, [curr.x, curr.y]], [])));
|
||||
}
|
||||
else
|
||||
return point([points.x, points.y]);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.map {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.marker-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: $primary-color;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 5px 1px darken($body-font-color-dark, 40%);
|
||||
}
|
||||
</style>
|
@@ -78,6 +78,8 @@ export default {
|
||||
|
||||
.file-uploader-message {
|
||||
display: flex;
|
||||
word-break: keep-all;
|
||||
border-radius: $border-radius 0 0 $border-radius;
|
||||
}
|
||||
|
||||
.file-uploader-input {
|
||||
|
@@ -89,7 +89,7 @@
|
||||
:type="inputProps().type"
|
||||
:disabled="!isChecked"
|
||||
>
|
||||
<template v-if="methodData && 'params' in methodData" class="columns">
|
||||
<template v-if="methodData && 'params' in methodData">
|
||||
<input
|
||||
v-for="(option, key) in methodData.params"
|
||||
:key="key"
|
||||
|
@@ -7,7 +7,7 @@
|
||||
@blur="$emit('blur')"
|
||||
>
|
||||
<option v-if="!isValidDefault" :value="value">
|
||||
{{ value }} - {{ $t('message.invalidDefault') }}
|
||||
{{ value === null ? 'NULL' : value }}
|
||||
</option>
|
||||
<option
|
||||
v-for="row in foreignList"
|
||||
|
@@ -6,18 +6,18 @@
|
||||
@confirm="runRoutine"
|
||||
@hide="closeModal"
|
||||
>
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-play mr-1" />
|
||||
<span class="cut-text">{{ $t('word.parameters') }}: {{ localRoutine.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div slot="body">
|
||||
<template #body>
|
||||
<div class="content">
|
||||
<form class="form-horizontal">
|
||||
<div
|
||||
v-for="(parameter, i) in inParameters"
|
||||
:key="parameter._id"
|
||||
:key="parameter._antares_id"
|
||||
class="form-group"
|
||||
>
|
||||
<div class="col-4">
|
||||
@@ -31,7 +31,11 @@
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
<span class="input-group-addon field-type" :class="typeClass(parameter.type)">
|
||||
<span
|
||||
:title="`${parameter.type} ${parameter.length}`"
|
||||
class="input-group-addon field-type cut-text"
|
||||
:class="typeClass(parameter.type)"
|
||||
>
|
||||
{{ parameter.type }} {{ parameter.length | wrapNumber }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -39,7 +43,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
@@ -127,4 +131,8 @@ export default {
|
||||
.field-type {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.input-group-addon {
|
||||
max-width: 100px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -5,16 +5,16 @@
|
||||
@confirm="$emit('confirm')"
|
||||
@hide="$emit('close')"
|
||||
>
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-content-save-alert mr-1" /> {{ $t('message.unsavedChanges') }}
|
||||
</div>
|
||||
</template>
|
||||
<div slot="body">
|
||||
<template #body>
|
||||
<div>
|
||||
{{ $t('message.discardUnsavedChanges') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
|
507
src/renderer/components/ModalExportSchema.vue
Normal file
@@ -0,0 +1,507 @@
|
||||
<template>
|
||||
<div class="modal active">
|
||||
<a class="modal-overlay" @click.stop="closeModal" />
|
||||
<div class="modal-container p-0">
|
||||
<div class="modal-header pl-2">
|
||||
<div class="modal-title h6">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-database-arrow-down mr-1" />
|
||||
<span class="cut-text">{{ $t('message.exportSchema') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
|
||||
</div>
|
||||
<div class="modal-body pb-0">
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
<div class="col-3">
|
||||
<label class="form-label">{{ $t('message.directoryPath') }}</label>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<fieldset class="input-group">
|
||||
<input
|
||||
v-model="basePath"
|
||||
class="form-input"
|
||||
type="text"
|
||||
required
|
||||
readonly
|
||||
:placeholder="$t('message.schemaName')"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary input-group-btn"
|
||||
@click.prevent="openPathDialog"
|
||||
>
|
||||
{{ $t('word.change') }}
|
||||
</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns export-options">
|
||||
<div class="column col-8 left">
|
||||
<div class="columns mb-2">
|
||||
<div class="column col-auto d-flex text-italic ">
|
||||
<i class="mdi mdi-file-document-outline mr-2" />
|
||||
{{ filename }}
|
||||
</div>
|
||||
|
||||
<div class="column col-auto col-ml-auto ">
|
||||
<button
|
||||
class="btn btn-dark btn-sm"
|
||||
:title="$t('word.refresh')"
|
||||
@click="refresh"
|
||||
>
|
||||
<i class="mdi mdi-database-refresh" />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-dark btn-sm"
|
||||
:title="$t('message.uncheckAllTables')"
|
||||
:disabled="isRefreshing"
|
||||
@click="uncheckAllTables"
|
||||
>
|
||||
<i class="mdi mdi-file-tree-outline" />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-dark btn-sm"
|
||||
:title="$t('message.checkAllTables')"
|
||||
:disabled="isRefreshing"
|
||||
@click="checkAllTables"
|
||||
>
|
||||
<i class="mdi mdi-file-tree" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workspace-query-results">
|
||||
<div ref="table" class="table table-hover">
|
||||
<div class="thead">
|
||||
<div class="tr text-center">
|
||||
<div class="th no-border" style="width: 50%;" />
|
||||
<div class="th no-border">
|
||||
<label
|
||||
class="form-checkbox m-0 px-2 form-inline"
|
||||
@click.prevent="toggleAllTablesOption('includeStructure')"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:indeterminate.prop="includeStructureStatus === 2"
|
||||
:checked.prop="!!includeStructureStatus"
|
||||
>
|
||||
<i class="form-icon" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="th no-border">
|
||||
<label
|
||||
class="form-checkbox m-0 px-2 form-inline"
|
||||
@click.prevent="toggleAllTablesOption('includeContent')"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:indeterminate.prop="includeContentStatus === 2"
|
||||
:checked.prop="!!includeContentStatus"
|
||||
>
|
||||
<i class="form-icon" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="th no-border">
|
||||
<label
|
||||
class="form-checkbox m-0 px-2 form-inline"
|
||||
@click.prevent="toggleAllTablesOption('includeDropStatement')"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:indeterminate.prop="includeDropStatementStatus === 2"
|
||||
:checked.prop="!!includeDropStatementStatus"
|
||||
>
|
||||
<i class="form-icon" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tr">
|
||||
<div class="th" style="width: 50%;">
|
||||
<div class="table-column-title">
|
||||
<span>{{ $t('word.table') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="th text-center">
|
||||
<div class="table-column-title">
|
||||
<span>{{ $t('word.structure') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="th text-center">
|
||||
<div class="table-column-title">
|
||||
<span>{{ $t('word.content') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="th text-center">
|
||||
<div class="table-column-title">
|
||||
<span>{{ $t('word.drop') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tbody">
|
||||
<div
|
||||
v-for="item in tables"
|
||||
:key="item.name"
|
||||
class="tr"
|
||||
>
|
||||
<div class="td">
|
||||
{{ item.table }}
|
||||
</div>
|
||||
<div class="td text-center">
|
||||
<label class="form-checkbox m-0 px-2 form-inline">
|
||||
<input
|
||||
v-model="item.includeStructure"
|
||||
type="checkbox"
|
||||
><i class="form-icon" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="td text-center">
|
||||
<label class="form-checkbox m-0 px-2 form-inline">
|
||||
<input
|
||||
v-model="item.includeContent"
|
||||
type="checkbox"
|
||||
><i class="form-icon" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="td text-center">
|
||||
<label class="form-checkbox m-0 px-2 form-inline">
|
||||
<input
|
||||
v-model="item.includeDropStatement"
|
||||
type="checkbox"
|
||||
><i class="form-icon" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-4">
|
||||
<h5 class="h5">
|
||||
{{ $t('word.options') }}
|
||||
</h5>
|
||||
<span class="h6">{{ $t('word.includes') }}:</span>
|
||||
<label
|
||||
v-for="(_, key) in options.includes"
|
||||
:key="key"
|
||||
class="form-checkbox"
|
||||
>
|
||||
<input v-model="options.includes[key]" type="checkbox"><i class="form-icon" /> {{ $t(`word.${key}`) }}
|
||||
</label>
|
||||
|
||||
<div class="h6 mt-4 mb-2">
|
||||
{{ $t('message.newInserStmtEvery') }}:
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column col-6">
|
||||
<input
|
||||
v-model.number="options.sqlInsertAfter"
|
||||
type="number"
|
||||
class="form-input"
|
||||
value="250"
|
||||
>
|
||||
</div>
|
||||
<div class="column col-6">
|
||||
<select v-model="options.sqlInsertDivider" class="form-select">
|
||||
<option value="bytes">
|
||||
KiB
|
||||
</option>
|
||||
<option value="rows">
|
||||
{{ $tc('word.row', 2) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h6 mb-2 mt-4">
|
||||
{{ $t('message.ourputFormat') }}:
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column h5 mb-4">
|
||||
<select v-model="options.outputFormat" class="form-select">
|
||||
<option value="sql">
|
||||
{{ $t('message.singleFile', {ext: '.sql'}) }}
|
||||
</option>
|
||||
<option value="sql.zip">
|
||||
{{ $t('message.zipCompressedFile', {ext: '.sql'}) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer columns">
|
||||
<div class="column col modal-progress-wrapper text-left">
|
||||
<div v-if="progressPercentage > 0" class="export-progress">
|
||||
<span class="progress-status">
|
||||
{{ progressPercentage }}% - {{ progressStatus }}
|
||||
</span>
|
||||
<progress
|
||||
class="progress d-block"
|
||||
:value="progressPercentage"
|
||||
max="100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-auto px-0">
|
||||
<button class="btn btn-link" @click.stop="closeModal">
|
||||
{{ $t('word.close') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary mr-2"
|
||||
:class="{'loading': isExporting}"
|
||||
:disabled="isExporting || isRefreshing"
|
||||
autofocus
|
||||
@click.prevent="startExport"
|
||||
>
|
||||
{{ $t('word.export') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import moment from 'moment';
|
||||
import customizations from 'common/customizations';
|
||||
import Application from '@/ipc-api/Application';
|
||||
import Schema from '@/ipc-api/Schema';
|
||||
|
||||
export default {
|
||||
name: 'ModalExportSchema',
|
||||
|
||||
props: {
|
||||
selectedSchema: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isExporting: false,
|
||||
isRefreshing: false,
|
||||
progressPercentage: 0,
|
||||
progressStatus: '',
|
||||
tables: [],
|
||||
options: {
|
||||
includes: {},
|
||||
outputFormat: 'sql',
|
||||
sqlInsertAfter: 250,
|
||||
sqlInsertDivider: 'bytes'
|
||||
},
|
||||
basePath: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedWorkspace: 'workspaces/getSelected',
|
||||
getWorkspace: 'workspaces/getWorkspace',
|
||||
getDatabaseVariable: 'workspaces/getDatabaseVariable'
|
||||
}),
|
||||
currentWorkspace () {
|
||||
return this.getWorkspace(this.selectedWorkspace);
|
||||
},
|
||||
schemaItems () {
|
||||
const db = this.currentWorkspace.structure.find(db => db.name === this.selectedSchema);
|
||||
if (db)
|
||||
return db.tables.filter(table => table.type === 'table');
|
||||
|
||||
return [];
|
||||
},
|
||||
filename () {
|
||||
const date = moment().format('YYYY-MM-DD');
|
||||
return `${this.selectedSchema}_${date}.${this.options.outputFormat}`;
|
||||
},
|
||||
dumpFilePath () {
|
||||
return `${this.basePath}/${this.filename}`;
|
||||
},
|
||||
includeStructureStatus () {
|
||||
if (this.tables.every(item => item.includeStructure)) return 1;
|
||||
else if (this.tables.some(item => item.includeStructure)) return 2;
|
||||
else return 0;
|
||||
},
|
||||
includeContentStatus () {
|
||||
if (this.tables.every(item => item.includeContent)) return 1;
|
||||
else if (this.tables.some(item => item.includeContent)) return 2;
|
||||
else return 0;
|
||||
},
|
||||
includeDropStatementStatus () {
|
||||
if (this.tables.every(item => item.includeDropStatement)) return 1;
|
||||
else if (this.tables.some(item => item.includeDropStatement)) return 2;
|
||||
else return 0;
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
if (!this.schemaItems.length) await this.refresh();
|
||||
|
||||
window.addEventListener('keydown', this.onKey);
|
||||
|
||||
this.basePath = await Application.getDownloadPathDirectory();
|
||||
this.tables = this.schemaItems.map(item => ({
|
||||
table: item.name,
|
||||
includeStructure: true,
|
||||
includeContent: true,
|
||||
includeDropStatement: true
|
||||
}));
|
||||
|
||||
const structure = ['views', 'triggers', 'routines', 'functions', 'schedulers', 'triggerFunctions'];
|
||||
|
||||
structure.forEach(feat => {
|
||||
const val = customizations[this.currentWorkspace.client][feat];
|
||||
if (val)
|
||||
this.$set(this.options.includes, feat, true);
|
||||
});
|
||||
|
||||
ipcRenderer.on('export-progress', this.updateProgress);
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
ipcRenderer.off('export-progress', this.updateProgress);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addNotification: 'notifications/addNotification',
|
||||
refreshSchema: 'workspaces/refreshSchema'
|
||||
}),
|
||||
async startExport () {
|
||||
this.isExporting = true;
|
||||
const { uid, client } = this.currentWorkspace;
|
||||
const params = {
|
||||
uid,
|
||||
type: client,
|
||||
schema: this.selectedSchema,
|
||||
outputFile: this.dumpFilePath,
|
||||
tables: [...this.tables],
|
||||
...this.options
|
||||
};
|
||||
|
||||
try {
|
||||
const { status, response } = await Schema.export(params);
|
||||
if (status === 'success')
|
||||
this.progressStatus = response.cancelled ? this.$t('word.aborted') : this.$t('word.completed');
|
||||
else {
|
||||
this.progressStatus = response;
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
this.isExporting = false;
|
||||
},
|
||||
updateProgress (event, state) {
|
||||
this.progressPercentage = Number((state.currentItemIndex / state.totalItems * 100).toFixed(1));
|
||||
switch (state.op) {
|
||||
case 'PROCESSING':
|
||||
this.progressStatus = this.$t('message.processingTableExport', { table: state.currentItem });
|
||||
break;
|
||||
case 'FETCH':
|
||||
this.progressStatus = this.$t('message.fechingTableExport', { table: state.currentItem });
|
||||
break;
|
||||
case 'WRITE':
|
||||
this.progressStatus = this.$t('message.writingTableExport', { table: state.currentItem });
|
||||
break;
|
||||
}
|
||||
},
|
||||
async closeModal () {
|
||||
let willClose = true;
|
||||
if (this.isExporting) {
|
||||
willClose = false;
|
||||
const { response } = await Schema.abortExport();
|
||||
willClose = response.willAbort;
|
||||
}
|
||||
|
||||
if (willClose)
|
||||
this.$emit('close');
|
||||
},
|
||||
onKey (e) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape')
|
||||
this.closeModal();
|
||||
},
|
||||
checkAllTables () {
|
||||
this.tables = this.tables.map(item => ({ ...item, includeStructure: true, includeContent: true, includeDropStatement: true }));
|
||||
},
|
||||
uncheckAllTables () {
|
||||
this.tables = this.tables.map(item => ({ ...item, includeStructure: false, includeContent: false, includeDropStatement: false }));
|
||||
},
|
||||
toggleAllTablesOption (option) {
|
||||
const options = ['includeStructure', 'includeContent', 'includeDropStatement'];
|
||||
if (!options.includes(option)) return;
|
||||
|
||||
if (this[`${option}Status`] !== 1)
|
||||
this.tables = this.tables.map(item => ({ ...item, [option]: true }));
|
||||
else
|
||||
this.tables = this.tables.map(item => ({ ...item, [option]: false }));
|
||||
},
|
||||
async refresh () {
|
||||
this.isRefreshing = true;
|
||||
await this.refreshSchema({ uid: this.currentWorkspace.uid, schema: this.selectedSchema });
|
||||
this.isRefreshing = false;
|
||||
},
|
||||
async openPathDialog () {
|
||||
const result = await Application.showOpenDialog({ properties: ['openDirectory'] });
|
||||
if (result && !result.canceled)
|
||||
this.basePath = result.filePaths[0];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.export-options {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-query-results {
|
||||
flex: 1 0 1px;
|
||||
.table {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
min-height: 0.8rem;
|
||||
padding: 0;
|
||||
|
||||
.form-icon {
|
||||
top: 0.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
|
||||
.modal-container {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
max-height: 60vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-status {
|
||||
font-style: italic;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
</style>
|
@@ -6,7 +6,7 @@
|
||||
<div class="modal-title h6">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-playlist-plus mr-1" />
|
||||
<span class="cut-text">{{ $t('message.tableFiller') }}</span>
|
||||
<span class="cut-text">{{ $tc('message.insertRow', 2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
|
||||
@@ -41,7 +41,7 @@
|
||||
<label class="form-checkbox ml-3" :title="$t('word.insert')">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="!field.autoIncrement"
|
||||
:checked="!fieldsToExclude.includes(field.name)"
|
||||
@change.prevent="toggleFields($event, field)"
|
||||
><i class="form-icon" />
|
||||
</label>
|
||||
@@ -251,7 +251,7 @@ export default {
|
||||
if (field.default === 'NULL') fieldDefault = null;
|
||||
else {
|
||||
if ([...NUMBER, ...FLOAT].includes(field.type))
|
||||
fieldDefault = Number.isNaN(+field.default) ? null : +field.default;
|
||||
fieldDefault = !field.default || Number.isNaN(+field.default.replaceAll('\'', '')) ? null : +field.default.replaceAll('\'', '');
|
||||
else if ([...TEXT, ...LONG_TEXT].includes(field.type)) {
|
||||
fieldDefault = field.default
|
||||
? field.default.includes('\'')
|
||||
@@ -264,7 +264,7 @@ export default {
|
||||
else if (BIT.includes(field.type))
|
||||
fieldDefault = field.default.replaceAll('\'', '').replaceAll('b', '');
|
||||
else if (DATETIME.includes(field.type)) {
|
||||
if (field.default && ['current_timestamp', 'now()'].includes(field.default.toLowerCase())) {
|
||||
if (field.default && ['current_timestamp', 'now()'].some(term => field.default.toLowerCase().includes(term))) {
|
||||
let datePrecision = '';
|
||||
for (let i = 0; i < field.datePrecision; i++)
|
||||
datePrecision += i === 0 ? '.S' : 'S';
|
||||
@@ -281,7 +281,7 @@ export default {
|
||||
|
||||
rowObj[field.name] = { value: fieldDefault };
|
||||
|
||||
if (field.autoIncrement)// Disable by default auto increment fields
|
||||
if (field.autoIncrement || !!field.onUpdate)// Disable by default auto increment or "on update" fields
|
||||
this.fieldsToExclude = [...this.fieldsToExclude, field.name];
|
||||
}
|
||||
|
||||
|
283
src/renderer/components/ModalHistory.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<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-history mr-1" />
|
||||
<span class="cut-text">{{ $t('word.history') }}: {{ connectionName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
|
||||
</div>
|
||||
<div class="modal-body p-0 workspace-query-results">
|
||||
<div
|
||||
v-if="history.length"
|
||||
ref="searchForm"
|
||||
class="form-group has-icon-right p-2 m-0"
|
||||
>
|
||||
<input
|
||||
v-model="searchTerm"
|
||||
class="form-input"
|
||||
type="text"
|
||||
:placeholder="$t('message.searchForQueries')"
|
||||
>
|
||||
<i v-if="!searchTerm" class="form-icon mdi mdi-magnify mdi-18px pr-4" />
|
||||
<i
|
||||
v-else
|
||||
class="form-icon c-hand mdi mdi-backspace mdi-18px pr-4"
|
||||
@click="searchTerm = ''"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="history.length"
|
||||
ref="tableWrapper"
|
||||
class="vscroll px-1 "
|
||||
:style="{'height': resultsSize+'px'}"
|
||||
>
|
||||
<div ref="table">
|
||||
<BaseVirtualScroll
|
||||
ref="resultTable"
|
||||
:items="filteredHistory"
|
||||
:item-height="66"
|
||||
:visible-height="resultsSize"
|
||||
:scroll-element="scrollElement"
|
||||
>
|
||||
<template slot-scope="{ items }">
|
||||
<div
|
||||
v-for="query in items"
|
||||
:key="query.uid"
|
||||
class="tile my-2"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="tile-icon">
|
||||
<i class="mdi mdi-code-tags pr-1" />
|
||||
</div>
|
||||
<div class="tile-content">
|
||||
<div class="tile-title">
|
||||
<code
|
||||
class="cut-text"
|
||||
:title="query.sql"
|
||||
v-html="highlightWord(query.sql)"
|
||||
/>
|
||||
</div>
|
||||
<div class="tile-bottom-content">
|
||||
<small class="tile-subtitle">{{ query.schema }} · {{ formatDate(query.date) }}</small>
|
||||
<div class="tile-history-buttons">
|
||||
<button class="btn btn-link pl-1" @click.stop="$emit('select-query', query.sql)">
|
||||
<i class="mdi mdi-open-in-app pr-1" /> {{ $t('word.select') }}
|
||||
</button>
|
||||
<button class="btn btn-link pl-1" @click="copyQuery(query.sql)">
|
||||
<i class="mdi mdi-content-copy pr-1" /> {{ $t('word.copy') }}
|
||||
</button>
|
||||
<button class="btn btn-link pl-1" @click="deleteQuery(query)">
|
||||
<i class="mdi mdi-delete-forever pr-1" /> {{ $t('word.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BaseVirtualScroll>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty">
|
||||
<div class="empty-icon">
|
||||
<i class="mdi mdi-history mdi-48px" />
|
||||
</div>
|
||||
<p class="empty-title h5">
|
||||
{{ $t('message.thereIsNoQueriesYet') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
|
||||
|
||||
export default {
|
||||
name: 'ModalHistory',
|
||||
components: {
|
||||
BaseVirtualScroll
|
||||
},
|
||||
props: {
|
||||
connection: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
resultsSize: 1000,
|
||||
isQuering: false,
|
||||
scrollElement: null,
|
||||
searchTermInterval: null,
|
||||
searchTerm: '',
|
||||
localSearchTerm: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getConnectionName: 'connections/getConnectionName',
|
||||
getHistoryByWorkspace: 'history/getHistoryByWorkspace'
|
||||
}),
|
||||
connectionName () {
|
||||
return this.getConnectionName(this.connection.uid);
|
||||
},
|
||||
history () {
|
||||
return this.getHistoryByWorkspace(this.connection.uid) || [];
|
||||
},
|
||||
filteredHistory () {
|
||||
return this.history.filter(q => q.sql.toLowerCase().search(this.searchTerm.toLowerCase()) >= 0);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
searchTerm () {
|
||||
clearTimeout(this.searchTermInterval);
|
||||
|
||||
this.searchTermInterval = setTimeout(() => {
|
||||
this.localSearchTerm = this.searchTerm;
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
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.resizeResults();
|
||||
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',
|
||||
deleteQueryFromHistory: 'history/deleteQueryFromHistory'
|
||||
}),
|
||||
copyQuery (sql) {
|
||||
navigator.clipboard.writeText(sql);
|
||||
},
|
||||
deleteQuery (query) {
|
||||
this.deleteQueryFromHistory({
|
||||
workspace: this.connection.uid,
|
||||
...query
|
||||
});
|
||||
},
|
||||
resizeResults () {
|
||||
if (this.$refs.resultTable) {
|
||||
const el = this.$refs.tableWrapper.parentElement;
|
||||
|
||||
if (el)
|
||||
this.resultsSize = el.offsetHeight - this.$refs.searchForm.offsetHeight;
|
||||
|
||||
this.$refs.resultTable.updateWindow();
|
||||
}
|
||||
},
|
||||
formatDate (date) {
|
||||
return moment(date).isValid() ? moment(date).format('HH:mm:ss - YYYY/MM/DD') : date;
|
||||
},
|
||||
refreshScroller () {
|
||||
this.resizeResults();
|
||||
},
|
||||
closeModal () {
|
||||
this.$emit('close');
|
||||
},
|
||||
highlightWord (string) {
|
||||
string = string.replaceAll('<', '<').replaceAll('>', '>');
|
||||
|
||||
if (this.searchTerm) {
|
||||
const regexp = new RegExp(`(${this.searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
||||
return string.replace(regexp, '<span class="text-primary text-bold">$1</span>');
|
||||
}
|
||||
else
|
||||
return string;
|
||||
},
|
||||
onKey (e) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape')
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vscroll {
|
||||
height: 1000px;
|
||||
overflow: auto;
|
||||
overflow-anchor: none;
|
||||
}
|
||||
|
||||
.tile {
|
||||
border-radius: $border-radius;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
.tile-content {
|
||||
.tile-bottom-content {
|
||||
.tile-history-buttons {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tile-icon {
|
||||
font-size: 1.2rem;
|
||||
margin-left: 0.3rem;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.tile-content {
|
||||
padding: 0.3rem;
|
||||
padding-left: 0.1rem;
|
||||
max-width: calc(100% - 30px);
|
||||
|
||||
code {
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
font-size: 100%;
|
||||
// color: $primary-color;
|
||||
opacity: 0.8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tile-subtitle {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.tile-bottom-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.tile-history-buttons {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
button {
|
||||
font-size: 0.7rem;
|
||||
height: 1rem;
|
||||
line-height: 1rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
181
src/renderer/components/ModalImportSchema.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div class="modal active">
|
||||
<a class="modal-overlay" @click.stop="closeModal" />
|
||||
<div class="modal-container p-0">
|
||||
<div class="modal-header pl-2">
|
||||
<div class="modal-title h6">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-database-arrow-up mr-1" />
|
||||
<span class="cut-text">{{ $t('message.importSchema') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
|
||||
</div>
|
||||
<div class="modal-body pb-0">
|
||||
{{ sqlFile }}
|
||||
<div v-if="queryErrors.length > 0" class="mt-2">
|
||||
<label>{{ $tc('message.importQueryErrors', queryErrors.length) }}</label>
|
||||
<textarea
|
||||
v-model="formattedQueryErrors"
|
||||
class="form-input"
|
||||
rows="5"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer columns">
|
||||
<div class="column col modal-progress-wrapper text-left">
|
||||
<div class="import-progress">
|
||||
<span class="progress-status">
|
||||
{{ progressPercentage }}% - {{ progressStatus }} - {{ $tc('message.executedQueries', queryCount) }}
|
||||
</span>
|
||||
<progress
|
||||
class="progress d-block"
|
||||
:value="progressPercentage"
|
||||
max="100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-auto px-0">
|
||||
<button class="btn btn-link" @click.stop="closeModal">
|
||||
{{ completed ? $t('word.close') : $t('word.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import moment from 'moment';
|
||||
import Schema from '@/ipc-api/Schema';
|
||||
|
||||
export default {
|
||||
name: 'ModalImportSchema',
|
||||
|
||||
props: {
|
||||
selectedSchema: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
sqlFile: '',
|
||||
isImporting: false,
|
||||
progressPercentage: 0,
|
||||
queryCount: 0,
|
||||
completed: false,
|
||||
progressStatus: 'Reading',
|
||||
queryErrors: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedWorkspace: 'workspaces/getSelected',
|
||||
getWorkspace: 'workspaces/getWorkspace'
|
||||
}),
|
||||
currentWorkspace () {
|
||||
return this.getWorkspace(this.selectedWorkspace);
|
||||
},
|
||||
formattedQueryErrors () {
|
||||
return this.queryErrors.map(err =>
|
||||
`Time: ${moment(err.time).format('HH:mm:ss.S')} (${err.time})\nError: ${err.message}`
|
||||
).join('\n\n');
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
window.addEventListener('keydown', this.onKey);
|
||||
|
||||
ipcRenderer.on('import-progress', this.updateProgress);
|
||||
ipcRenderer.on('query-error', this.handleQueryError);
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
ipcRenderer.off('import-progress', this.updateProgress);
|
||||
ipcRenderer.off('query-error', this.handleQueryError);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addNotification: 'notifications/addNotification',
|
||||
refreshSchema: 'workspaces/refreshSchema'
|
||||
}),
|
||||
async startImport (sqlFile) {
|
||||
this.isImporting = true;
|
||||
this.sqlFile = sqlFile;
|
||||
|
||||
const { uid, client } = this.currentWorkspace;
|
||||
const params = {
|
||||
uid,
|
||||
type: client,
|
||||
schema: this.selectedSchema,
|
||||
file: sqlFile
|
||||
};
|
||||
|
||||
try {
|
||||
this.completed = false;
|
||||
const { status, response } = await Schema.import(params);
|
||||
if (status === 'success')
|
||||
this.progressStatus = response.cancelled ? this.$t('word.aborted') : this.$t('word.completed');
|
||||
else {
|
||||
this.progressStatus = response;
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
this.refreshSchema({ uid, schema: this.selectedSchema });
|
||||
this.completed = true;
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
this.isImporting = false;
|
||||
},
|
||||
updateProgress (event, state) {
|
||||
this.progressPercentage = Number(state.percentage).toFixed(1);
|
||||
this.queryCount = Number(state.queryCount);
|
||||
},
|
||||
handleQueryError (event, err) {
|
||||
this.queryErrors.push(err);
|
||||
},
|
||||
async closeModal () {
|
||||
let willClose = true;
|
||||
if (this.isImporting) {
|
||||
willClose = false;
|
||||
const { response } = await Schema.abortImport();
|
||||
willClose = response.willAbort;
|
||||
}
|
||||
|
||||
if (willClose)
|
||||
this.$emit('close');
|
||||
},
|
||||
onKey (e) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape')
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.modal {
|
||||
|
||||
.modal-container {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
max-height: 60vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-status {
|
||||
font-style: italic;
|
||||
font-size: 80%;
|
||||
}
|
||||
</style>
|
@@ -1,170 +0,0 @@
|
||||
<template>
|
||||
<ConfirmModal
|
||||
:confirm-text="$t('word.confirm')"
|
||||
size="400"
|
||||
@confirm="confirmNewFunction"
|
||||
@hide="$emit('close')"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewFunction') }}
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.name') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<input
|
||||
ref="firstInput"
|
||||
v-model="localFunction.name"
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
{{ $t('word.definer') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<select
|
||||
v-if="workspace.users.length"
|
||||
v-model="localFunction.definer"
|
||||
class="form-select"
|
||||
>
|
||||
<option value="">
|
||||
{{ $t('message.currentUser') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="user in workspace.users"
|
||||
:key="`${user.name}@${user.host}`"
|
||||
:value="`\`${user.name}\`@\`${user.host}\``"
|
||||
>
|
||||
{{ user.name }}@{{ user.host }}
|
||||
</option>
|
||||
</select>
|
||||
<select v-if="!workspace.users.length" class="form-select">
|
||||
<option value="">
|
||||
{{ $t('message.currentUser') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="customizations.comment" class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.comment') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<input
|
||||
v-model="localFunction.comment"
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('message.sqlSecurity') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<select v-model="localFunction.security" class="form-select">
|
||||
<option>DEFINER</option>
|
||||
<option>INVOKER</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="customizations.functionDataAccess" class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('message.dataAccess') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<select v-model="localFunction.dataAccess" class="form-select">
|
||||
<option>CONTAINS SQL</option>
|
||||
<option>NO SQL</option>
|
||||
<option>READS SQL DATA</option>
|
||||
<option>MODIFIES SQL DATA</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="customizations.functionDeterministic" class="form-group">
|
||||
<div class="col-4" />
|
||||
<div class="column">
|
||||
<label class="form-checkbox form-inline">
|
||||
<input v-model="localFunction.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
|
||||
export default {
|
||||
name: 'ModalNewFunction',
|
||||
components: {
|
||||
ConfirmModal
|
||||
},
|
||||
props: {
|
||||
workspace: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
localFunction: {
|
||||
definer: '',
|
||||
sql: '',
|
||||
parameters: [],
|
||||
name: '',
|
||||
comment: '',
|
||||
language: null,
|
||||
returns: null,
|
||||
returnsLength: 10,
|
||||
security: 'DEFINER',
|
||||
deterministic: false,
|
||||
dataAccess: 'CONTAINS SQL'
|
||||
},
|
||||
isOptionsChanging: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
schema () {
|
||||
return this.workspace.breadcrumbs.schema;
|
||||
},
|
||||
customizations () {
|
||||
return this.workspace.customizations;
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.customizations.languages)
|
||||
this.localFunction.language = this.customizations.languages[0];
|
||||
|
||||
if (this.customizations.functionSql)
|
||||
this.localFunction.sql = this.customizations.functionSql;
|
||||
setTimeout(() => {
|
||||
this.$refs.firstInput.focus();
|
||||
}, 20);
|
||||
},
|
||||
methods: {
|
||||
confirmNewFunction () {
|
||||
this.$emit('open-create-function-editor', this.localFunction);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@@ -1,168 +0,0 @@
|
||||
<template>
|
||||
<ConfirmModal
|
||||
:confirm-text="$t('word.confirm')"
|
||||
size="400"
|
||||
@confirm="confirmNewRoutine"
|
||||
@hide="$emit('close')"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewRoutine') }}
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.name') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<input
|
||||
ref="firstInput"
|
||||
v-model="localRoutine.name"
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
{{ $t('word.definer') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<select
|
||||
v-if="workspace.users.length"
|
||||
v-model="localRoutine.definer"
|
||||
class="form-select"
|
||||
>
|
||||
<option value="">
|
||||
{{ $t('message.currentUser') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="user in workspace.users"
|
||||
:key="`${user.name}@${user.host}`"
|
||||
:value="`\`${user.name}\`@\`${user.host}\``"
|
||||
>
|
||||
{{ user.name }}@{{ user.host }}
|
||||
</option>
|
||||
</select>
|
||||
<select v-if="!workspace.users.length" class="form-select">
|
||||
<option value="">
|
||||
{{ $t('message.currentUser') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="customizations.comment" class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.comment') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<input
|
||||
v-model="localRoutine.comment"
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('message.sqlSecurity') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<select v-model="localRoutine.security" class="form-select">
|
||||
<option>DEFINER</option>
|
||||
<option>INVOKER</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="customizations.comment" class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('message.dataAccess') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<select v-model="localRoutine.dataAccess" class="form-select">
|
||||
<option>CONTAINS SQL</option>
|
||||
<option>NO SQL</option>
|
||||
<option>READS SQL DATA</option>
|
||||
<option>MODIFIES SQL DATA</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="customizations.procedureDeterministic" class="form-group">
|
||||
<div class="col-4" />
|
||||
<div class="column">
|
||||
<label class="form-checkbox form-inline">
|
||||
<input v-model="localRoutine.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
|
||||
export default {
|
||||
name: 'ModalNewRoutine',
|
||||
components: {
|
||||
ConfirmModal
|
||||
},
|
||||
props: {
|
||||
workspace: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
localRoutine: {
|
||||
definer: '',
|
||||
sql: '',
|
||||
parameters: [],
|
||||
name: '',
|
||||
comment: '',
|
||||
language: null,
|
||||
security: 'DEFINER',
|
||||
deterministic: false,
|
||||
dataAccess: 'CONTAINS SQL'
|
||||
},
|
||||
isOptionsChanging: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
schema () {
|
||||
return this.workspace.breadcrumbs.schema;
|
||||
},
|
||||
customizations () {
|
||||
return this.workspace.customizations;
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.customizations.languages)
|
||||
this.localRoutine.language = this.customizations.languages[0];
|
||||
|
||||
if (this.customizations.procedureSql)
|
||||
this.localRoutine.sql = this.customizations.procedureSql;
|
||||
setTimeout(() => {
|
||||
this.$refs.firstInput.focus();
|
||||
}, 20);
|
||||
},
|
||||
methods: {
|
||||
confirmNewRoutine () {
|
||||
this.$emit('open-create-routine-editor', this.localRoutine);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@@ -1,109 +0,0 @@
|
||||
<template>
|
||||
<ConfirmModal
|
||||
:confirm-text="$t('word.confirm')"
|
||||
size="400"
|
||||
@confirm="confirmNewTrigger"
|
||||
@hide="$emit('close')"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewScheduler') }}
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.name') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<input
|
||||
ref="firstInput"
|
||||
v-model="localScheduler.name"
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.definer') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<select
|
||||
v-if="workspace.users.length"
|
||||
v-model="localScheduler.definer"
|
||||
class="form-select"
|
||||
>
|
||||
<option value="">
|
||||
{{ $t('message.currentUser') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="user in workspace.users"
|
||||
:key="`${user.name}@${user.host}`"
|
||||
:value="`\`${user.name}\`@\`${user.host}\``"
|
||||
>
|
||||
{{ user.name }}@{{ user.host }}
|
||||
</option>
|
||||
</select>
|
||||
<select v-if="!workspace.users.length" class="form-select">
|
||||
<option value="">
|
||||
{{ $t('message.currentUser') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.comment') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<input
|
||||
v-model="localScheduler.comment"
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
|
||||
export default {
|
||||
name: 'ModalNewScheduler',
|
||||
components: {
|
||||
ConfirmModal
|
||||
},
|
||||
props: {
|
||||
workspace: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
localScheduler: {
|
||||
definer: '',
|
||||
sql: 'BEGIN\r\n\r\nEND',
|
||||
name: '',
|
||||
comment: '',
|
||||
execution: 'EVERY',
|
||||
every: ['1', 'DAY'],
|
||||
preserve: true,
|
||||
state: 'DISABLE'
|
||||
}
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
setTimeout(() => {
|
||||
this.$refs.firstInput.focus();
|
||||
}, 20);
|
||||
},
|
||||
methods: {
|
||||
confirmNewTrigger () {
|
||||
this.$emit('open-create-scheduler-editor', this.localScheduler);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
<div class="modal-body pb-0">
|
||||
<div class="content">
|
||||
<form class="form-horizontal">
|
||||
<form class="form-horizontal" @submit.prevent="createSchema">
|
||||
<div class="form-group">
|
||||
<div class="col-3">
|
||||
<label class="form-label">{{ $t('word.name') }}</label>
|
||||
|
@@ -1,130 +0,0 @@
|
||||
<template>
|
||||
<ConfirmModal
|
||||
:confirm-text="$t('word.confirm')"
|
||||
size="400"
|
||||
@confirm="confirmOptionsChange"
|
||||
@hide="$emit('close')"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-table-plus mr-1" /> {{ $t('message.createNewTable') }}
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.name') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<input
|
||||
ref="firstInput"
|
||||
v-model="localOptions.name"
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="workspace.customizations.comment" class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.comment') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<input
|
||||
v-model="localOptions.comment"
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="workspace.customizations.collations" class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.collation') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<select v-model="localOptions.collation" class="form-select">
|
||||
<option
|
||||
v-for="collation in workspace.collations"
|
||||
:key="collation.id"
|
||||
:value="collation.collation"
|
||||
>
|
||||
{{ collation.collation }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="workspace.customizations.engines" class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.engine') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<select v-model="localOptions.engine" class="form-select">
|
||||
<option
|
||||
v-for="engine in workspace.engines"
|
||||
:key="engine.name"
|
||||
:value="engine.name"
|
||||
>
|
||||
{{ engine.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
|
||||
export default {
|
||||
name: 'ModalNewTable',
|
||||
components: {
|
||||
ConfirmModal
|
||||
},
|
||||
props: {
|
||||
workspace: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
localOptions: {
|
||||
name: '',
|
||||
comment: '',
|
||||
collation: '',
|
||||
engine: ''
|
||||
},
|
||||
isOptionsChanging: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedWorkspace: 'workspaces/getSelected',
|
||||
getDatabaseVariable: 'workspaces/getDatabaseVariable'
|
||||
}),
|
||||
defaultCollation () {
|
||||
if (this.workspace.customizations.collations)
|
||||
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
|
||||
return '';
|
||||
},
|
||||
defaultEngine () {
|
||||
if (this.workspace.customizations.engines)
|
||||
return this.workspace.engines.find(engine => engine.isDefault).name;
|
||||
return '';
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.localOptions.collation = this.defaultCollation;
|
||||
this.localOptions.engine = this.defaultEngine;
|
||||
|
||||
setTimeout(() => {
|
||||
this.$refs.firstInput.focus();
|
||||
}, 20);
|
||||
},
|
||||
methods: {
|
||||
confirmOptionsChange () {
|
||||
this.$emit('open-create-table-editor', this.localOptions);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@@ -1,183 +0,0 @@
|
||||
<template>
|
||||
<ConfirmModal
|
||||
:confirm-text="$t('word.confirm')"
|
||||
size="400"
|
||||
@confirm="confirmNewTrigger"
|
||||
@hide="$emit('close')"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewTrigger') }}
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.name') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<input
|
||||
ref="firstInput"
|
||||
v-model="localTrigger.name"
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="customizations.definer" class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.definer') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<select
|
||||
v-if="workspace.users.length"
|
||||
v-model="localTrigger.definer"
|
||||
class="form-select"
|
||||
>
|
||||
<option value="">
|
||||
{{ $t('message.currentUser') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="user in workspace.users"
|
||||
:key="`${user.name}@${user.host}`"
|
||||
:value="`\`${user.name}\`@\`${user.host}\``"
|
||||
>
|
||||
{{ user.name }}@{{ user.host }}
|
||||
</option>
|
||||
</select>
|
||||
<select v-if="!workspace.users.length" class="form-select">
|
||||
<option value="">
|
||||
{{ $t('message.currentUser') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.table') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<select v-model="localTrigger.table" class="form-select">
|
||||
<option v-for="table in schemaTables" :key="table.name">
|
||||
{{ table.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.event') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<div class="input-group">
|
||||
<select v-model="localTrigger.activation" class="form-select">
|
||||
<option>BEFORE</option>
|
||||
<option>AFTER</option>
|
||||
</select>
|
||||
<select
|
||||
v-if="!customizations.triggerMultipleEvents"
|
||||
v-model="localTrigger.event"
|
||||
class="form-select"
|
||||
>
|
||||
<option v-for="event in Object.keys(localEvents)" :key="event">
|
||||
{{ event }}
|
||||
</option>
|
||||
</select>
|
||||
<div v-if="customizations.triggerMultipleEvents" class="px-4">
|
||||
<label
|
||||
v-for="event in Object.keys(localEvents)"
|
||||
:key="event"
|
||||
class="form-checkbox form-inline"
|
||||
@change.prevent="changeEvents(event)"
|
||||
>
|
||||
<input :checked="localEvents[event]" type="checkbox"><i class="form-icon" /> {{ event }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div v-if="customizations.triggerStatementInCreation" class="workspace-query-results column col-12 mt-2">
|
||||
<label class="form-label ml-2">{{ $t('message.triggerStatement') }}</label>
|
||||
<QueryEditor
|
||||
ref="queryEditor"
|
||||
:value.sync="localTrigger.sql"
|
||||
:workspace="workspace"
|
||||
:schema="schema"
|
||||
:height="editorHeight"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
import QueryEditor from '@/components/QueryEditor';
|
||||
|
||||
export default {
|
||||
name: 'ModalNewTrigger',
|
||||
components: {
|
||||
ConfirmModal,
|
||||
QueryEditor
|
||||
},
|
||||
props: {
|
||||
workspace: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
localTrigger: {
|
||||
definer: '',
|
||||
sql: '',
|
||||
name: '',
|
||||
table: '',
|
||||
activation: 'BEFORE',
|
||||
event: 'INSERT'
|
||||
},
|
||||
isOptionsChanging: false,
|
||||
localEvents: { INSERT: false, UPDATE: false, DELETE: false },
|
||||
editorHeight: 150
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
schema () {
|
||||
return this.workspace.breadcrumbs.schema;
|
||||
},
|
||||
schemaTables () {
|
||||
const schemaTables = this.workspace.structure
|
||||
.filter(schema => schema.name === this.schema)
|
||||
.map(schema => schema.tables);
|
||||
|
||||
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
|
||||
},
|
||||
customizations () {
|
||||
return this.workspace.customizations;
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.localTrigger.table = this.schemaTables.length ? this.schemaTables[0].name : '';
|
||||
this.localTrigger.sql = this.customizations.triggerSql;
|
||||
},
|
||||
mounted () {
|
||||
setTimeout(() => {
|
||||
this.$refs.firstInput.focus();
|
||||
}, 20);
|
||||
},
|
||||
methods: {
|
||||
confirmNewTrigger () {
|
||||
this.$emit('open-create-trigger-editor', this.localTrigger);
|
||||
},
|
||||
changeEvents (event) {
|
||||
if (this.customizations.triggerMultipleEvents) {
|
||||
this.localEvents[event] = !this.localEvents[event];
|
||||
this.localTrigger.event = [];
|
||||
for (const key in this.localEvents) {
|
||||
if (this.localEvents[key])
|
||||
this.localTrigger.event.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@@ -1,132 +0,0 @@
|
||||
<template>
|
||||
<ConfirmModal
|
||||
:confirm-text="$t('word.confirm')"
|
||||
size="400"
|
||||
@confirm="confirmNewFunction"
|
||||
@hide="$emit('close')"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewFunction') }}
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.name') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<input
|
||||
ref="firstInput"
|
||||
v-model="localFunction.name"
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<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.triggerFunctionlanguages" :key="language">
|
||||
{{ language }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="customizations.definer" class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.definer') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<select
|
||||
v-if="workspace.users.length"
|
||||
v-model="localFunction.definer"
|
||||
class="form-select"
|
||||
>
|
||||
<option value="">
|
||||
{{ $t('message.currentUser') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="user in workspace.users"
|
||||
:key="`${user.name}@${user.host}`"
|
||||
:value="`\`${user.name}\`@\`${user.host}\``"
|
||||
>
|
||||
{{ user.name }}@{{ user.host }}
|
||||
</option>
|
||||
</select>
|
||||
<select v-if="!workspace.users.length" class="form-select">
|
||||
<option value="">
|
||||
{{ $t('message.currentUser') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="customizations.comment" class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.comment') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<input
|
||||
v-model="localFunction.comment"
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
|
||||
export default {
|
||||
name: 'ModalNewTriggerFunction',
|
||||
components: {
|
||||
ConfirmModal
|
||||
},
|
||||
props: {
|
||||
workspace: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
localFunction: {
|
||||
definer: '',
|
||||
sql: '',
|
||||
name: '',
|
||||
comment: '',
|
||||
language: null
|
||||
},
|
||||
isOptionsChanging: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
schema () {
|
||||
return this.workspace.breadcrumbs.schema;
|
||||
},
|
||||
customizations () {
|
||||
return this.workspace.customizations;
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.customizations.triggerFunctionlanguages)
|
||||
this.localFunction.language = this.customizations.triggerFunctionlanguages[0];
|
||||
|
||||
if (this.customizations.triggerFunctionSql)
|
||||
this.localFunction.sql = this.customizations.triggerFunctionSql;
|
||||
setTimeout(() => {
|
||||
this.$refs.firstInput.focus();
|
||||
}, 20);
|
||||
},
|
||||
methods: {
|
||||
confirmNewFunction () {
|
||||
this.$emit('open-create-function-editor', this.localFunction);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@@ -1,192 +0,0 @@
|
||||
<template>
|
||||
<ConfirmModal
|
||||
:confirm-text="$t('word.confirm')"
|
||||
size="medium"
|
||||
@confirm="confirmOptionsChange"
|
||||
@hide="$emit('close')"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-eye-plus mr-1" /> {{ $t('message.createNewView') }}
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<div class="container">
|
||||
<div class="columns mb-4">
|
||||
<div class="column col-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('word.name') }}</label>
|
||||
<input
|
||||
ref="firstInput"
|
||||
v-model="localView.name"
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-6">
|
||||
<div v-if="workspace.customizations.definer" class="form-group">
|
||||
<label class="form-label">{{ $t('word.definer') }}</label>
|
||||
<select v-model="localView.definer" class="form-select">
|
||||
<option value="">
|
||||
{{ $t('message.currentUser') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="user in workspace.users"
|
||||
:key="`${user.name}@${user.host}`"
|
||||
:value="`\`${user.name}\`@\`${user.host}\``"
|
||||
>
|
||||
{{ user.name }}@{{ user.host }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column col-4">
|
||||
<div v-if="workspace.customizations.viewSqlSecurity" class="form-group">
|
||||
<label class="form-label">{{ $t('message.sqlSecurity') }}</label>
|
||||
<label class="form-radio">
|
||||
<input
|
||||
v-model="localView.security"
|
||||
type="radio"
|
||||
name="security"
|
||||
value="DEFINER"
|
||||
>
|
||||
<i class="form-icon" /> DEFINER
|
||||
</label>
|
||||
<label class="form-radio">
|
||||
<input
|
||||
v-model="localView.security"
|
||||
type="radio"
|
||||
name="security"
|
||||
value="INVOKER"
|
||||
>
|
||||
<i class="form-icon" /> INVOKER
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-4">
|
||||
<div v-if="workspace.customizations.viewAlgorithm" class="form-group">
|
||||
<label class="form-label">{{ $t('word.algorithm') }}</label>
|
||||
<label class="form-radio">
|
||||
<input
|
||||
v-model="localView.algorithm"
|
||||
type="radio"
|
||||
name="algorithm"
|
||||
value="UNDEFINED"
|
||||
>
|
||||
<i class="form-icon" /> UNDEFINED
|
||||
</label>
|
||||
<label class="form-radio">
|
||||
<input
|
||||
v-model="localView.algorithm"
|
||||
type="radio"
|
||||
value="MERGE"
|
||||
name="algorithm"
|
||||
>
|
||||
<i class="form-icon" /> MERGE
|
||||
</label>
|
||||
<label class="form-radio">
|
||||
<input
|
||||
v-model="localView.algorithm"
|
||||
type="radio"
|
||||
value="TEMPTABLE"
|
||||
name="algorithm"
|
||||
>
|
||||
<i class="form-icon" /> TEMPTABLE
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-4">
|
||||
<div v-if="workspace.customizations.viewUpdateOption" class="form-group">
|
||||
<label class="form-label">{{ $t('message.updateOption') }}</label>
|
||||
<label class="form-radio">
|
||||
<input
|
||||
v-model="localView.updateOption"
|
||||
type="radio"
|
||||
name="update"
|
||||
value=""
|
||||
>
|
||||
<i class="form-icon" /> None
|
||||
</label>
|
||||
<label class="form-radio">
|
||||
<input
|
||||
v-model="localView.updateOption"
|
||||
type="radio"
|
||||
name="update"
|
||||
value="CASCADED"
|
||||
>
|
||||
<i class="form-icon" /> CASCADED
|
||||
</label>
|
||||
<label class="form-radio">
|
||||
<input
|
||||
v-model="localView.updateOption"
|
||||
type="radio"
|
||||
name="update"
|
||||
value="LOCAL"
|
||||
>
|
||||
<i class="form-icon" /> LOCAL
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workspace-query-results column col-12 mt-2">
|
||||
<label class="form-label ml-2">{{ $t('message.selectStatement') }}</label>
|
||||
<QueryEditor
|
||||
ref="queryEditor"
|
||||
:value.sync="localView.sql"
|
||||
:workspace="workspace"
|
||||
:schema="schema"
|
||||
:height="editorHeight"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
import QueryEditor from '@/components/QueryEditor';
|
||||
|
||||
export default {
|
||||
name: 'ModalNewView',
|
||||
components: {
|
||||
ConfirmModal,
|
||||
QueryEditor
|
||||
},
|
||||
props: {
|
||||
workspace: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
localView: {
|
||||
algorithm: 'UNDEFINED',
|
||||
definer: '',
|
||||
security: 'DEFINER',
|
||||
updateOption: '',
|
||||
sql: '',
|
||||
name: ''
|
||||
},
|
||||
isOptionsChanging: false,
|
||||
editorHeight: 300
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
schema () {
|
||||
return this.workspace.breadcrumbs.schema;
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
setTimeout(() => {
|
||||
this.$refs.firstInput.focus();
|
||||
}, 20);
|
||||
},
|
||||
methods: {
|
||||
confirmOptionsChange () {
|
||||
this.$emit('open-create-view-editor', this.localView);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@@ -1,5 +1,15 @@
|
||||
<template>
|
||||
<div class="modal active">
|
||||
<ModalProcessesListContext
|
||||
v-if="isContext"
|
||||
:context-event="contextEvent"
|
||||
:selected-row="selectedRow"
|
||||
:selected-cell="selectedCell"
|
||||
@copy-cell="copyCell"
|
||||
@copy-row="copyRow"
|
||||
@kill-process="killProcess"
|
||||
@close-context="closeContext"
|
||||
/>
|
||||
<a class="modal-overlay" @click.stop="closeModal" />
|
||||
<div class="modal-container p-0 pb-4">
|
||||
<div class="modal-header pl-2">
|
||||
@@ -12,34 +22,54 @@
|
||||
<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"
|
||||
>
|
||||
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh mr-1" />
|
||||
<i v-else class="mdi mdi-24px mdi-history mdi-flip-h mr-1" />
|
||||
<span>{{ $t('word.refresh') }}</span>
|
||||
</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 class="workspace-query-buttons">
|
||||
<div class="dropdown pr-1">
|
||||
<div class="btn-group">
|
||||
<button
|
||||
class="btn btn-dark btn-sm mr-0 pr-1 d-flex"
|
||||
:class="{'loading':isQuering}"
|
||||
:title="`${$t('word.refresh')} (F5)`"
|
||||
@click="getProcessesList"
|
||||
>
|
||||
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh mr-1" />
|
||||
<i v-else class="mdi mdi-24px mdi-history mdi-flip-h mr-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="dropdown table-dropdown">
|
||||
<button
|
||||
:disabled="isQuering"
|
||||
class="btn btn-dark btn-sm dropdown-toggle d-flex mr-0 pr-0"
|
||||
tabindex="0"
|
||||
>
|
||||
<i class="mdi mdi-24px mdi-file-export mr-1" />
|
||||
<span>{{ $t('word.export') }}</span>
|
||||
<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>
|
||||
</div>
|
||||
<div class="workspace-query-info">
|
||||
<div v-if="sortedResults.length">
|
||||
@@ -83,11 +113,12 @@
|
||||
:scroll-element="scrollElement"
|
||||
>
|
||||
<template slot-scope="{ items }">
|
||||
<ProcessesListRow
|
||||
<ModalProcessesListRow
|
||||
v-for="row in items"
|
||||
:key="row._id"
|
||||
:key="row.id"
|
||||
class="process-row"
|
||||
:row="row"
|
||||
@select-row="selectRow(row.id)"
|
||||
@contextmenu="contextMenu"
|
||||
@stop-refresh="stopRefresh"
|
||||
/>
|
||||
@@ -102,15 +133,18 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import arrayToFile from '../libs/arrayToFile';
|
||||
import Schema from '@/ipc-api/Schema';
|
||||
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
|
||||
import ProcessesListRow from '@/components/ProcessesListRow';
|
||||
import ModalProcessesListRow from '@/components/ModalProcessesListRow';
|
||||
import ModalProcessesListContext from '@/components/ModalProcessesListContext';
|
||||
|
||||
export default {
|
||||
name: 'ModalProcessesList',
|
||||
components: {
|
||||
BaseVirtualScroll,
|
||||
ProcessesListRow
|
||||
ModalProcessesListRow,
|
||||
ModalProcessesListContext
|
||||
},
|
||||
props: {
|
||||
connection: Object
|
||||
@@ -119,8 +153,12 @@ export default {
|
||||
return {
|
||||
resultsSize: 1000,
|
||||
isQuering: false,
|
||||
isContext: false,
|
||||
autorefreshTimer: 0,
|
||||
refreshInterval: null,
|
||||
contextEvent: null,
|
||||
selectedCell: null,
|
||||
selectedRow: null,
|
||||
results: [],
|
||||
fields: [],
|
||||
currentSort: '',
|
||||
@@ -245,10 +283,55 @@ export default {
|
||||
this.autorefreshTimer = 0;
|
||||
this.clearRefresh();
|
||||
},
|
||||
contextMenu () {},
|
||||
selectRow (row) {
|
||||
this.selectedRow = row;
|
||||
},
|
||||
contextMenu (event, cell) {
|
||||
if (event.target.localName === 'input') return;
|
||||
this.stopRefresh();
|
||||
|
||||
this.selectedCell = cell;
|
||||
this.selectedRow = cell.id;
|
||||
this.contextEvent = event;
|
||||
this.isContext = true;
|
||||
},
|
||||
async killProcess () {
|
||||
try { // Table data
|
||||
const { status, response } = await Schema.killProcess({ uid: this.connection.uid, pid: this.selectedRow });
|
||||
|
||||
if (status === 'success')
|
||||
this.getProcessesList();
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
},
|
||||
closeContext () {
|
||||
this.isContext = false;
|
||||
},
|
||||
copyCell () {
|
||||
const row = this.results.find(row => row.id === this.selectedRow);
|
||||
const valueToCopy = row[this.selectedCell.field];
|
||||
navigator.clipboard.writeText(valueToCopy);
|
||||
},
|
||||
copyRow () {
|
||||
const row = this.results.find(row => row.id === this.selectedRow);
|
||||
const rowToCopy = JSON.parse(JSON.stringify(row));
|
||||
navigator.clipboard.writeText(JSON.stringify(rowToCopy));
|
||||
},
|
||||
closeModal () {
|
||||
this.$emit('close');
|
||||
},
|
||||
downloadTable (format) {
|
||||
if (!this.sortedResults) return;
|
||||
arrayToFile({
|
||||
type: format,
|
||||
content: this.sortedResults,
|
||||
filename: 'processes'
|
||||
});
|
||||
},
|
||||
onKey (e) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape')
|
||||
|
75
src/renderer/components/ModalProcessesListContext.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<BaseContextMenu
|
||||
:context-event="contextEvent"
|
||||
@close-context="closeContext"
|
||||
>
|
||||
<div v-if="selectedRow" class="context-element">
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-content-copy text-light pr-1" /> {{ $t('word.copy') }}</span>
|
||||
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
|
||||
<div class="context-submenu">
|
||||
<div
|
||||
v-if="selectedRow"
|
||||
class="context-element"
|
||||
@click="copyCell"
|
||||
>
|
||||
<span class="d-flex">
|
||||
<i class="mdi mdi-18px mdi-numeric-0 mdi-rotate-90 text-light pr-1" /> {{ $tc('word.cell', 1) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedRow"
|
||||
class="context-element"
|
||||
@click="copyRow"
|
||||
>
|
||||
<span class="d-flex">
|
||||
<i class="mdi mdi-18px mdi-table-row text-light pr-1" /> {{ $tc('word.row', 1) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedRow"
|
||||
class="context-element"
|
||||
@click="killProcess"
|
||||
>
|
||||
<span class="d-flex">
|
||||
<i class="mdi mdi-18px mdi-close-circle-outline text-light pr-1" /> {{ $t('message.killProcess') }}
|
||||
</span>
|
||||
</div>
|
||||
</BaseContextMenu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseContextMenu from '@/components/BaseContextMenu';
|
||||
|
||||
export default {
|
||||
name: 'ModalProcessesListContext',
|
||||
components: {
|
||||
BaseContextMenu
|
||||
},
|
||||
props: {
|
||||
contextEvent: MouseEvent,
|
||||
selectedRow: Number,
|
||||
selectedCell: Object
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
methods: {
|
||||
closeContext () {
|
||||
this.$emit('close-context');
|
||||
},
|
||||
copyCell () {
|
||||
this.$emit('copy-cell');
|
||||
this.closeContext();
|
||||
},
|
||||
copyRow () {
|
||||
this.$emit('copy-row');
|
||||
this.closeContext();
|
||||
},
|
||||
killProcess () {
|
||||
this.$emit('kill-process');
|
||||
this.closeContext();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@@ -1,21 +1,18 @@
|
||||
<template>
|
||||
<div class="tr" @click="selectRow($event, row._id)">
|
||||
<div class="tr" @click="selectRow()">
|
||||
<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 })"
|
||||
@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>
|
||||
<span
|
||||
v-if="!isInlineEditor[cKey]"
|
||||
class="cell-content"
|
||||
:class="`${isNull(col)} type-${typeof col === 'number' ? 'int' : 'varchar'}`"
|
||||
@dblclick="dblClick(cKey)"
|
||||
>{{ col | cutText }}</span>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
v-if="isInfoModal"
|
||||
@@ -25,12 +22,12 @@
|
||||
:hide-footer="true"
|
||||
@hide="hideInfoModal"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-information-outline mr-1" /> {{ $t('message.processInfo') }}
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<template #body>
|
||||
<div>
|
||||
<div>
|
||||
<TextEditor
|
||||
@@ -41,7 +38,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
</template>
|
||||
@@ -51,7 +48,7 @@ import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
import TextEditor from '@/components/BaseTextEditor';
|
||||
|
||||
export default {
|
||||
name: 'ProcessesListRow',
|
||||
name: 'ModalProcessesListRow',
|
||||
components: {
|
||||
ConfirmModal,
|
||||
TextEditor
|
||||
@@ -73,25 +70,15 @@ export default {
|
||||
};
|
||||
},
|
||||
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);
|
||||
selectRow () {
|
||||
this.$emit('select-row');
|
||||
},
|
||||
openContext (event, payload) {
|
||||
if (this.isEditable) {
|
||||
payload.field = this.fields[payload.field].name;// Ensures field name only
|
||||
this.$emit('contextmenu', event, payload);
|
||||
}
|
||||
this.$emit('contextmenu', event, payload);
|
||||
},
|
||||
hideInfoModal () {
|
||||
this.isInfoModal = false;
|
||||
@@ -126,6 +113,7 @@ export default {
|
||||
|
||||
.cell-content {
|
||||
display: block;
|
||||
padding: 0 0.2rem;
|
||||
min-height: 0.8rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
@@ -117,6 +117,19 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<div class="col-7 col-sm-12">
|
||||
<label class="form-label">
|
||||
{{ $t('message.disableBlur') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-5 col-sm-12">
|
||||
<label class="form-switch d-inline-block" @click.prevent="toggleDisableBlur">
|
||||
<input type="checkbox" :checked="disableBlur">
|
||||
<i class="form-icon" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-7 col-sm-12">
|
||||
<label class="form-label">
|
||||
@@ -184,7 +197,7 @@
|
||||
:class="{'selected': applicationTheme === 'dark'}"
|
||||
@click="changeApplicationTheme('dark')"
|
||||
>
|
||||
<img :src="require('@/images/dark.png').default" class="img-responsive img-fit-cover s-rounded">
|
||||
<img src="../images/dark.png" class="img-responsive img-fit-cover s-rounded">
|
||||
<div class="theme-name text-light">
|
||||
<i class="mdi mdi-moon-waning-crescent mdi-48px" />
|
||||
<div class="h6 mt-4">
|
||||
@@ -197,7 +210,7 @@
|
||||
:class="{'selected': applicationTheme === 'light'}"
|
||||
@click="changeApplicationTheme('light')"
|
||||
>
|
||||
<img :src="require('@/images/light.png').default" class="img-responsive img-fit-cover s-rounded">
|
||||
<img src="../images/light.png" class="img-responsive img-fit-cover s-rounded">
|
||||
<div class="theme-name text-dark">
|
||||
<i class="mdi mdi-white-balance-sunny mdi-48px" />
|
||||
<div class="h6 mt-4">
|
||||
@@ -280,14 +293,20 @@
|
||||
|
||||
<div v-show="selectedTab === 'about'" class="panel-body py-4">
|
||||
<div class="text-center">
|
||||
<img :src="require('@/images/logo.svg').default" width="128">
|
||||
<img src="../images/logo.svg" width="128">
|
||||
<h4>{{ appName }}</h4>
|
||||
<p>
|
||||
<p class="mb-2">
|
||||
{{ $t('word.version') }} {{ appVersion }}<br>
|
||||
<a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares')"><i class="mdi mdi-github d-inline" /> GitHub</a> • <a class="c-hand" @click="openOutside('https://twitter.com/AntaresSQL')"><i class="mdi mdi-twitter d-inline" /> Twitter</a> • <a class="c-hand" @click="openOutside('https://antares-sql.app/')"><i class="mdi mdi-web d-inline" /> 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('message.madeWithJS') }}</small>
|
||||
<small>{{ $t('word.author') }} <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">{{ appAuthor }}</a></small><br>
|
||||
</p>
|
||||
<div class="mb-2">
|
||||
<small class="d-block text-uppercase">{{ $t('word.contributors') }}:</small>
|
||||
<div class="d-block py-1">
|
||||
<small v-for="(contributor, i) in otherContributors" :key="i">{{ i !== 0 ? ', ' : '' }}{{ contributor }}</small>
|
||||
</div>
|
||||
<small>{{ $t('message.madeWithJS') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -313,6 +332,7 @@ export default {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
appAuthor: 'Fabio Di Stasio',
|
||||
localLocale: null,
|
||||
localPageSize: null,
|
||||
localTimeout: null,
|
||||
@@ -367,7 +387,8 @@ export default {
|
||||
{ code: 'vibrant_ink', name: 'Vibrant Ink' }
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
contributors: process.env.APP_CONTRIBUTORS
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -381,6 +402,7 @@ export default {
|
||||
selectedLineWrap: 'settings/getLineWrap',
|
||||
notificationsTimeout: 'settings/getNotificationsTimeout',
|
||||
restoreTabs: 'settings/getRestoreTabs',
|
||||
disableBlur: 'settings/getDisableBlur',
|
||||
applicationTheme: 'settings/getApplicationTheme',
|
||||
editorTheme: 'settings/getEditorTheme',
|
||||
editorFontSize: 'settings/getEditorFontSize',
|
||||
@@ -396,7 +418,7 @@ export default {
|
||||
return locales;
|
||||
},
|
||||
hasUpdates () {
|
||||
return ['available', 'downloading', 'downloaded'].includes(this.updateStatus);
|
||||
return ['available', 'downloading', 'downloaded', 'link'].includes(this.updateStatus);
|
||||
},
|
||||
workspace () {
|
||||
return this.getWorkspace(this.selectedWorkspace);
|
||||
@@ -417,6 +439,12 @@ GROUP BY
|
||||
ORDER BY
|
||||
employee.id ASC;
|
||||
`;
|
||||
},
|
||||
otherContributors () {
|
||||
return this.contributors
|
||||
.split(',')
|
||||
.filter(c => !c.includes(this.appAuthor))
|
||||
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
||||
}
|
||||
},
|
||||
created () {
|
||||
@@ -436,6 +464,7 @@ ORDER BY
|
||||
changeLocale: 'settings/changeLocale',
|
||||
changePageSize: 'settings/changePageSize',
|
||||
changeRestoreTabs: 'settings/changeRestoreTabs',
|
||||
changeDisableBlur: 'settings/changeDisableBlur',
|
||||
changeAutoComplete: 'settings/changeAutoComplete',
|
||||
changeLineWrap: 'settings/changeLineWrap',
|
||||
changeApplicationTheme: 'settings/changeApplicationTheme',
|
||||
@@ -463,6 +492,9 @@ ORDER BY
|
||||
toggleRestoreSession () {
|
||||
this.changeRestoreTabs(!this.restoreTabs);
|
||||
},
|
||||
toggleDisableBlur () {
|
||||
this.changeDisableBlur(!this.disableBlur);
|
||||
},
|
||||
toggleAutoComplete () {
|
||||
this.changeAutoComplete(!this.selectedAutoComplete);
|
||||
},
|
||||
|
@@ -16,7 +16,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import marked from 'marked';
|
||||
import { marked } from 'marked';
|
||||
import BaseLoader from '@/components/BaseLoader';
|
||||
|
||||
export default {
|
||||
|
@@ -29,12 +29,19 @@
|
||||
{{ $t('message.checkForUpdates') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="updateStatus === 'downloaded'"
|
||||
v-else-if="updateStatus === 'downloaded'"
|
||||
class="btn btn-primary"
|
||||
@click="restartToUpdate"
|
||||
>
|
||||
{{ $t('message.restartToInstall') }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="updateStatus === 'link'"
|
||||
class="btn btn-primary"
|
||||
@click="openOutside('https://antares-sql.app/download.html')"
|
||||
>
|
||||
{{ $t('message.goToDownloadPage') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-group mt-4">
|
||||
<label class="form-switch d-inline-block disabled" @click.prevent="toggleAllowPrerelease">
|
||||
@@ -47,7 +54,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { ipcRenderer, shell } from 'electron';
|
||||
|
||||
export default {
|
||||
name: 'ModalSettingsUpdate',
|
||||
@@ -71,6 +78,8 @@ export default {
|
||||
return this.$t('message.downloadingUpdate');
|
||||
case 'downloaded':
|
||||
return this.$t('message.updateDownloaded');
|
||||
case 'link':
|
||||
return this.$t('message.updateAvailable');
|
||||
default:
|
||||
return this.updateStatus;
|
||||
}
|
||||
@@ -80,6 +89,9 @@ export default {
|
||||
...mapActions({
|
||||
changeAllowPrerelease: 'settings/changeAllowPrerelease'
|
||||
}),
|
||||
openOutside (link) {
|
||||
shell.openExternal(link);
|
||||
},
|
||||
checkForUpdates () {
|
||||
ipcRenderer.send('check-for-updates');
|
||||
},
|
||||
|
@@ -15,16 +15,16 @@
|
||||
@confirm="confirmDeleteConnection"
|
||||
@hide="hideConfirmModal"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-server-remove mr-1" /> {{ $t('message.deleteConnection') }}
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<template #body>
|
||||
<div class="mb-2">
|
||||
{{ $t('message.deleteCorfirm') }} <b>{{ connectionName }}</b>?
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
</BaseContextMenu>
|
||||
</template>
|
||||
|
@@ -11,9 +11,9 @@
|
||||
|
||||
<div class="footer-right-elements">
|
||||
<ul class="footer-elements">
|
||||
<li class="footer-element footer-link" @click="openOutside('https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet')">
|
||||
<i class="mdi mdi-18px mdi-tree mr-1" />
|
||||
<small>{{ $t('message.plantATree') }}</small>
|
||||
<li class="footer-element footer-link" @click="openOutside('https://www.paypal.com/paypalme/fabiodistasio')">
|
||||
<i class="mdi mdi-18px mdi-coffee mr-1" />
|
||||
<small>{{ $t('word.donate') }}</small>
|
||||
</li>
|
||||
<li class="footer-element footer-link" @click="openOutside('https://github.com/Fabio286/antares/issues')">
|
||||
<i class="mdi mdi-18px mdi-bug" />
|
||||
|
@@ -40,15 +40,13 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
notifications: {
|
||||
deep: true,
|
||||
handler: function (notification) {
|
||||
if (notification.length) {
|
||||
this.timeouts[notification[0].uid] = setTimeout(() => {
|
||||
this.removeNotification(notification[0].uid);
|
||||
delete this.timeouts[notification.uid];
|
||||
}, this.notificationsTimeout * 1000);
|
||||
}
|
||||
'notifications.length': function (val) {
|
||||
if (val > 0) {
|
||||
const nUid = this.notifications[0].uid;
|
||||
this.timeouts[nUid] = setTimeout(() => {
|
||||
this.removeNotification(nUid);
|
||||
delete this.timeouts[nUid];
|
||||
}, this.notificationsTimeout * 1000);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -63,11 +61,14 @@ export default {
|
||||
}
|
||||
},
|
||||
rearmTimeouts () {
|
||||
const delay = 50;
|
||||
let i = this.notifications.length * delay;
|
||||
for (const notification of this.notifications) {
|
||||
this.timeouts[notification.uid] = setTimeout(() => {
|
||||
this.removeNotification(notification.uid);
|
||||
delete this.timeouts[notification.uid];
|
||||
}, this.notificationsTimeout * 1000);
|
||||
}, (this.notificationsTimeout * 1000) + i);
|
||||
i = i > delay ? i - delay : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,12 +6,12 @@
|
||||
:hide-footer="true"
|
||||
@hide="hideScratchpad"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-notebook-edit-outline mr-1" /> {{ $t('word.scratchpad') }}
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<template #body>
|
||||
<div>
|
||||
<div>
|
||||
<TextEditor
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
<small class="text-gray">{{ $t('message.markdownSupported') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
|
@@ -92,7 +92,7 @@ export default {
|
||||
}
|
||||
},
|
||||
hasUpdates () {
|
||||
return ['available', 'downloading', 'downloaded'].includes(this.updateStatus);
|
||||
return ['available', 'downloading', 'downloaded', 'link'].includes(this.updateStatus);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -200,7 +200,7 @@ export default {
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
content: "";
|
||||
height: 0;
|
||||
width: 3px;
|
||||
transition: height 0.2s;
|
||||
|
@@ -1,8 +1,12 @@
|
||||
<template>
|
||||
<div id="titlebar">
|
||||
<div id="titlebar" @dblclick="toggleFullScreen">
|
||||
<div class="titlebar-resizer" />
|
||||
<div class="titlebar-elements">
|
||||
<img class="titlebar-logo" :src="require('@/images/logo.svg').default">
|
||||
<img
|
||||
v-if="!isMacOS"
|
||||
class="titlebar-logo"
|
||||
src="@/images/logo.svg"
|
||||
>
|
||||
</div>
|
||||
<div class="titlebar-elements titlebar-title">
|
||||
{{ windowTitle }}
|
||||
@@ -22,14 +26,26 @@
|
||||
>
|
||||
<i class="mdi mdi-24px mdi-refresh" />
|
||||
</div>
|
||||
<div class="titlebar-element" @click="minimizeApp">
|
||||
<div
|
||||
v-if="!isMacOS"
|
||||
class="titlebar-element"
|
||||
@click="minimizeApp"
|
||||
>
|
||||
<i class="mdi mdi-24px mdi-minus" />
|
||||
</div>
|
||||
<div class="titlebar-element" @click="toggleFullScreen">
|
||||
<div
|
||||
v-if="!isMacOS"
|
||||
class="titlebar-element"
|
||||
@click="toggleFullScreen"
|
||||
>
|
||||
<i v-if="isMaximized" class="mdi mdi-24px mdi-fullscreen-exit" />
|
||||
<i v-else class="mdi mdi-24px mdi-fullscreen" />
|
||||
</div>
|
||||
<div class="titlebar-element close-button" @click="closeApp">
|
||||
<div
|
||||
v-if="!isMacOS"
|
||||
class="titlebar-element close-button"
|
||||
@click="closeApp"
|
||||
>
|
||||
<i class="mdi mdi-24px mdi-close" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,7 +63,8 @@ export default {
|
||||
return {
|
||||
w: getCurrentWindow(),
|
||||
isMaximized: getCurrentWindow().isMaximized(),
|
||||
isDevelopment: process.env.NODE_ENV === 'development'
|
||||
isDevelopment: process.env.NODE_ENV === 'development',
|
||||
isMacOS: process.platform === 'darwin'
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@@ -18,14 +18,17 @@
|
||||
<li
|
||||
v-for="(tab, i) of draggableTabs"
|
||||
:key="i"
|
||||
:ref="selectedTab === tab.uid ? 'tab-selected' : ''"
|
||||
class="tab-item tab-draggable"
|
||||
draggable="true"
|
||||
:class="{'active': selectedTab === tab.uid}"
|
||||
@mousedown.left="selectTab({uid: workspace.uid, tab: tab.uid})"
|
||||
@mouseup.middle="closeTab(tab)"
|
||||
>
|
||||
<a v-if="tab.type === 'query'" class="tab-link">
|
||||
<a
|
||||
v-if="tab.type === 'query'"
|
||||
class="tab-link"
|
||||
:class="{'badge': tab.isChanged}"
|
||||
>
|
||||
<i class="mdi mdi-18px mdi-code-tags mr-1" />
|
||||
<span>
|
||||
<span>{{ tab.content || 'Query' | cutText }} #{{ tab.index }}</span>
|
||||
@@ -68,6 +71,23 @@
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-else-if="tab.type === 'new-table'"
|
||||
class="tab-link"
|
||||
:class="{'badge': tab.isChanged}"
|
||||
>
|
||||
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
|
||||
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
|
||||
{{ $t('message.newTable') }}
|
||||
<span
|
||||
class="btn btn-clear"
|
||||
:title="$t('word.close')"
|
||||
@mousedown.left.stop
|
||||
@click.stop="closeTab(tab)"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-else-if="tab.type === 'table-props'"
|
||||
class="tab-link"
|
||||
@@ -91,7 +111,7 @@
|
||||
:class="{'badge': tab.isChanged}"
|
||||
>
|
||||
<i class="mdi mdi-tune-vertical-variant mdi-18px mr-1" />
|
||||
<span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
|
||||
<span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.view`)}`">
|
||||
{{ tab.elementName | cutText }}
|
||||
<span
|
||||
class="btn btn-clear"
|
||||
@@ -102,6 +122,108 @@
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-else-if="tab.type === 'new-view'"
|
||||
class="tab-link"
|
||||
:class="{'badge': tab.isChanged}"
|
||||
>
|
||||
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
|
||||
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
|
||||
{{ $t('message.newView') }}
|
||||
<span
|
||||
class="btn btn-clear"
|
||||
:title="$t('word.close')"
|
||||
@mousedown.left.stop
|
||||
@click.stop="closeTab(tab)"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-else-if="tab.type === 'new-trigger'"
|
||||
class="tab-link"
|
||||
:class="{'badge': tab.isChanged}"
|
||||
>
|
||||
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
|
||||
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
|
||||
{{ $t('message.newTrigger') }}
|
||||
<span
|
||||
class="btn btn-clear"
|
||||
:title="$t('word.close')"
|
||||
@mousedown.left.stop
|
||||
@click.stop="closeTab(tab)"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-else-if="tab.type === 'new-routine'"
|
||||
class="tab-link"
|
||||
:class="{'badge': tab.isChanged}"
|
||||
>
|
||||
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
|
||||
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
|
||||
{{ $t('message.newRoutine') }}
|
||||
<span
|
||||
class="btn btn-clear"
|
||||
:title="$t('word.close')"
|
||||
@mousedown.left.stop
|
||||
@click.stop="closeTab(tab)"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-else-if="tab.type === 'new-function'"
|
||||
class="tab-link"
|
||||
:class="{'badge': tab.isChanged}"
|
||||
>
|
||||
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
|
||||
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
|
||||
{{ $t('message.newFunction') }}
|
||||
<span
|
||||
class="btn btn-clear"
|
||||
:title="$t('word.close')"
|
||||
@mousedown.left.stop
|
||||
@click.stop="closeTab(tab)"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-else-if="tab.type === 'new-trigger-function'"
|
||||
class="tab-link"
|
||||
:class="{'badge': tab.isChanged}"
|
||||
>
|
||||
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
|
||||
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
|
||||
{{ $t('message.newTriggerFunction') }}
|
||||
<span
|
||||
class="btn btn-clear"
|
||||
:title="$t('word.close')"
|
||||
@mousedown.left.stop
|
||||
@click.stop="closeTab(tab)"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-else-if="tab.type === 'new-scheduler'"
|
||||
class="tab-link"
|
||||
:class="{'badge': tab.isChanged}"
|
||||
>
|
||||
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
|
||||
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
|
||||
{{ $t('message.newScheduler') }}
|
||||
<span
|
||||
class="btn btn-clear"
|
||||
:title="$t('word.close')"
|
||||
@mousedown.left.stop
|
||||
@click.stop="closeTab(tab)"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-else-if="tab.type.includes('temp-')"
|
||||
class="tab-link"
|
||||
@@ -137,63 +259,70 @@
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li slot="header" class="tab-item dropdown tools-dropdown">
|
||||
<a
|
||||
class="tab-link workspace-tools-link dropdown-toggle"
|
||||
tabindex="0"
|
||||
:title="$t('word.tools')"
|
||||
<template #header>
|
||||
<li
|
||||
v-if="workspace.customizations.processesList"
|
||||
class="tab-item dropdown tools-dropdown"
|
||||
>
|
||||
<i class="mdi mdi-24px mdi-tools" />
|
||||
</a>
|
||||
<ul 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="tab-link workspace-tools-link dropdown-toggle"
|
||||
tabindex="0"
|
||||
:title="$t('word.tools')"
|
||||
>
|
||||
<a class="c-hand p-vcentered disabled">
|
||||
<i class="mdi mdi-shape mr-1 tool-icon" />
|
||||
<span>{{ $t('word.variables') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
v-if="workspace.customizations.usersManagement"
|
||||
class="menu-item"
|
||||
title="Coming..."
|
||||
<i class="mdi mdi-24px mdi-tools" />
|
||||
</a>
|
||||
<ul v-if="hasTools" class="menu text-left text-uppercase">
|
||||
<li class="menu-item">
|
||||
<a class="c-hand p-vcentered" @click="showProcessesModal">
|
||||
<i class="mdi mdi-memory mr-1 tool-icon" />
|
||||
<span>{{ $t('message.processesList') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
v-if="workspace.customizations.variables"
|
||||
class="menu-item"
|
||||
title="Coming..."
|
||||
>
|
||||
<a class="c-hand p-vcentered disabled">
|
||||
<i class="mdi mdi-shape mr-1 tool-icon" />
|
||||
<span>{{ $t('word.variables') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
v-if="workspace.customizations.usersManagement"
|
||||
class="menu-item"
|
||||
title="Coming..."
|
||||
>
|
||||
<a class="c-hand p-vcentered disabled">
|
||||
<i class="mdi mdi-account-group mr-1 tool-icon" />
|
||||
<span>{{ $t('message.manageUsers') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
<template #footer>
|
||||
<li class="tab-item">
|
||||
<a
|
||||
class="tab-add"
|
||||
:title="$t('message.openNewTab')"
|
||||
@click="addQueryTab"
|
||||
>
|
||||
<a class="c-hand p-vcentered disabled">
|
||||
<i class="mdi mdi-account-group mr-1 tool-icon" />
|
||||
<span>{{ $t('message.manageUsers') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li slot="footer" class="tab-item">
|
||||
<a
|
||||
class="tab-add"
|
||||
:title="$t('message.openNewTab')"
|
||||
@click="addQueryTab"
|
||||
>
|
||||
<i class="mdi mdi-24px mdi-plus" />
|
||||
</a>
|
||||
</li>
|
||||
<i class="mdi mdi-24px mdi-plus" />
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</Draggable>
|
||||
<WorkspaceEmptyState v-if="!workspace.tabs.length" @new-tab="addQueryTab" />
|
||||
<template v-for="tab of workspace.tabs">
|
||||
<WorkspaceQueryTab
|
||||
<WorkspaceTabQuery
|
||||
v-if="tab.type==='query'"
|
||||
:key="tab.uid"
|
||||
:tab="tab"
|
||||
:is-selected="selectedTab === tab.uid"
|
||||
:connection="connection"
|
||||
/>
|
||||
<WorkspaceTableTab
|
||||
<WorkspaceTabTable
|
||||
v-else-if="['temp-data', 'data'].includes(tab.type)"
|
||||
:key="tab.uid"
|
||||
:connection="connection"
|
||||
@@ -202,7 +331,15 @@
|
||||
:schema="tab.schema"
|
||||
:element-type="tab.elementType"
|
||||
/>
|
||||
<WorkspacePropsTab
|
||||
<WorkspaceTabNewTable
|
||||
v-else-if="tab.type === 'new-table'"
|
||||
:key="tab.uid"
|
||||
:tab="tab"
|
||||
:connection="connection"
|
||||
:is-selected="selectedTab === tab.uid"
|
||||
:schema="tab.schema"
|
||||
/>
|
||||
<WorkspaceTabPropsTable
|
||||
v-else-if="tab.type === 'table-props'"
|
||||
:key="tab.uid"
|
||||
:connection="connection"
|
||||
@@ -210,7 +347,15 @@
|
||||
:table="tab.elementName"
|
||||
:schema="tab.schema"
|
||||
/>
|
||||
<WorkspacePropsTabView
|
||||
<WorkspaceTabNewView
|
||||
v-else-if="tab.type === 'new-view'"
|
||||
:key="tab.uid"
|
||||
:tab="tab"
|
||||
:connection="connection"
|
||||
:is-selected="selectedTab === tab.uid"
|
||||
:schema="tab.schema"
|
||||
/>
|
||||
<WorkspaceTabPropsView
|
||||
v-else-if="tab.type === 'view-props'"
|
||||
:key="tab.uid"
|
||||
:is-selected="selectedTab === tab.uid"
|
||||
@@ -218,7 +363,16 @@
|
||||
:view="tab.elementName"
|
||||
:schema="tab.schema"
|
||||
/>
|
||||
<WorkspacePropsTabTrigger
|
||||
<WorkspaceTabNewTrigger
|
||||
v-else-if="tab.type === 'new-trigger'"
|
||||
:key="tab.uid"
|
||||
:tab="tab"
|
||||
:connection="connection"
|
||||
:is-selected="selectedTab === tab.uid"
|
||||
:trigger="tab.elementName"
|
||||
:schema="tab.schema"
|
||||
/>
|
||||
<WorkspaceTabPropsTrigger
|
||||
v-else-if="['temp-trigger-props', 'trigger-props'].includes(tab.type)"
|
||||
:key="tab.uid"
|
||||
:connection="connection"
|
||||
@@ -226,7 +380,16 @@
|
||||
:trigger="tab.elementName"
|
||||
:schema="tab.schema"
|
||||
/>
|
||||
<WorkspacePropsTabTriggerFunction
|
||||
<WorkspaceTabNewTriggerFunction
|
||||
v-else-if="tab.type === 'new-trigger-function'"
|
||||
:key="tab.uid"
|
||||
:tab="tab"
|
||||
:connection="connection"
|
||||
:is-selected="selectedTab === tab.uid"
|
||||
:trigger="tab.elementName"
|
||||
:schema="tab.schema"
|
||||
/>
|
||||
<WorkspaceTabPropsTriggerFunction
|
||||
v-else-if="['temp-trigger-function-props', 'trigger-function-props'].includes(tab.type)"
|
||||
:key="tab.uid"
|
||||
:connection="connection"
|
||||
@@ -234,7 +397,16 @@
|
||||
:function="tab.elementName"
|
||||
:schema="tab.schema"
|
||||
/>
|
||||
<WorkspacePropsTabRoutine
|
||||
<WorkspaceTabNewRoutine
|
||||
v-else-if="tab.type === 'new-routine'"
|
||||
:key="tab.uid"
|
||||
:tab="tab"
|
||||
:connection="connection"
|
||||
:is-selected="selectedTab === tab.uid"
|
||||
:trigger="tab.elementName"
|
||||
:schema="tab.schema"
|
||||
/>
|
||||
<WorkspaceTabPropsRoutine
|
||||
v-else-if="['temp-routine-props', 'routine-props'].includes(tab.type)"
|
||||
:key="tab.uid"
|
||||
:connection="connection"
|
||||
@@ -242,7 +414,16 @@
|
||||
:routine="tab.elementName"
|
||||
:schema="tab.schema"
|
||||
/>
|
||||
<WorkspacePropsTabFunction
|
||||
<WorkspaceTabNewFunction
|
||||
v-else-if="tab.type === 'new-function'"
|
||||
:key="tab.uid"
|
||||
:tab="tab"
|
||||
:connection="connection"
|
||||
:is-selected="selectedTab === tab.uid"
|
||||
:trigger="tab.elementName"
|
||||
:schema="tab.schema"
|
||||
/>
|
||||
<WorkspaceTabPropsFunction
|
||||
v-else-if="['temp-function-props', 'function-props'].includes(tab.type)"
|
||||
:key="tab.uid"
|
||||
:connection="connection"
|
||||
@@ -250,7 +431,16 @@
|
||||
:function="tab.elementName"
|
||||
:schema="tab.schema"
|
||||
/>
|
||||
<WorkspacePropsTabScheduler
|
||||
<WorkspaceTabNewScheduler
|
||||
v-else-if="tab.type === 'new-scheduler'"
|
||||
:key="tab.uid"
|
||||
:tab="tab"
|
||||
:connection="connection"
|
||||
:is-selected="selectedTab === tab.uid"
|
||||
:trigger="tab.elementName"
|
||||
:schema="tab.schema"
|
||||
/>
|
||||
<WorkspaceTabPropsScheduler
|
||||
v-else-if="['temp-scheduler-props', 'scheduler-props'].includes(tab.type)"
|
||||
:key="tab.uid"
|
||||
:connection="connection"
|
||||
@@ -260,7 +450,9 @@
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<WorkspaceEditConnectionPanel v-else :connection="connection" />
|
||||
<div v-else class="connection-panel-wrapper">
|
||||
<WorkspaceEditConnectionPanel :connection="connection" />
|
||||
</div>
|
||||
<ModalProcessesList
|
||||
v-if="isProcessesModal"
|
||||
:connection="connection"
|
||||
@@ -282,15 +474,24 @@ import Connection from '@/ipc-api/Connection';
|
||||
import WorkspaceEmptyState from '@/components/WorkspaceEmptyState';
|
||||
import WorkspaceExploreBar from '@/components/WorkspaceExploreBar';
|
||||
import WorkspaceEditConnectionPanel from '@/components/WorkspaceEditConnectionPanel';
|
||||
import WorkspaceQueryTab from '@/components/WorkspaceQueryTab';
|
||||
import WorkspaceTableTab from '@/components/WorkspaceTableTab';
|
||||
import WorkspacePropsTab from '@/components/WorkspacePropsTab';
|
||||
import WorkspacePropsTabView from '@/components/WorkspacePropsTabView';
|
||||
import WorkspacePropsTabTrigger from '@/components/WorkspacePropsTabTrigger';
|
||||
import WorkspacePropsTabTriggerFunction from '@/components/WorkspacePropsTabTriggerFunction';
|
||||
import WorkspacePropsTabRoutine from '@/components/WorkspacePropsTabRoutine';
|
||||
import WorkspacePropsTabFunction from '@/components/WorkspacePropsTabFunction';
|
||||
import WorkspacePropsTabScheduler from '@/components/WorkspacePropsTabScheduler';
|
||||
import WorkspaceTabQuery from '@/components/WorkspaceTabQuery';
|
||||
import WorkspaceTabTable from '@/components/WorkspaceTabTable';
|
||||
|
||||
import WorkspaceTabNewTable from '@/components/WorkspaceTabNewTable';
|
||||
import WorkspaceTabNewView from '@/components/WorkspaceTabNewView';
|
||||
import WorkspaceTabNewTrigger from '@/components/WorkspaceTabNewTrigger';
|
||||
import WorkspaceTabNewRoutine from '@/components/WorkspaceTabNewRoutine';
|
||||
import WorkspaceTabNewFunction from '@/components/WorkspaceTabNewFunction';
|
||||
import WorkspaceTabNewScheduler from '@/components/WorkspaceTabNewScheduler';
|
||||
import WorkspaceTabNewTriggerFunction from '@/components/WorkspaceTabNewTriggerFunction';
|
||||
|
||||
import WorkspaceTabPropsTable from '@/components/WorkspaceTabPropsTable';
|
||||
import WorkspaceTabPropsView from '@/components/WorkspaceTabPropsView';
|
||||
import WorkspaceTabPropsTrigger from '@/components/WorkspaceTabPropsTrigger';
|
||||
import WorkspaceTabPropsTriggerFunction from '@/components/WorkspaceTabPropsTriggerFunction';
|
||||
import WorkspaceTabPropsRoutine from '@/components/WorkspaceTabPropsRoutine';
|
||||
import WorkspaceTabPropsFunction from '@/components/WorkspaceTabPropsFunction';
|
||||
import WorkspaceTabPropsScheduler from '@/components/WorkspaceTabPropsScheduler';
|
||||
import ModalProcessesList from '@/components/ModalProcessesList';
|
||||
import ModalDiscardChanges from '@/components/ModalDiscardChanges';
|
||||
|
||||
@@ -301,15 +502,22 @@ export default {
|
||||
WorkspaceEmptyState,
|
||||
WorkspaceExploreBar,
|
||||
WorkspaceEditConnectionPanel,
|
||||
WorkspaceQueryTab,
|
||||
WorkspaceTableTab,
|
||||
WorkspacePropsTab,
|
||||
WorkspacePropsTabView,
|
||||
WorkspacePropsTabTrigger,
|
||||
WorkspacePropsTabTriggerFunction,
|
||||
WorkspacePropsTabRoutine,
|
||||
WorkspacePropsTabFunction,
|
||||
WorkspacePropsTabScheduler,
|
||||
WorkspaceTabQuery,
|
||||
WorkspaceTabTable,
|
||||
WorkspaceTabNewTable,
|
||||
WorkspaceTabPropsTable,
|
||||
WorkspaceTabNewView,
|
||||
WorkspaceTabPropsView,
|
||||
WorkspaceTabNewTrigger,
|
||||
WorkspaceTabPropsTrigger,
|
||||
WorkspaceTabNewTriggerFunction,
|
||||
WorkspaceTabPropsTriggerFunction,
|
||||
WorkspaceTabNewRoutine,
|
||||
WorkspaceTabNewFunction,
|
||||
WorkspaceTabPropsRoutine,
|
||||
WorkspaceTabPropsFunction,
|
||||
WorkspaceTabNewScheduler,
|
||||
WorkspaceTabPropsScheduler,
|
||||
ModalProcessesList,
|
||||
ModalDiscardChanges
|
||||
},
|
||||
@@ -365,7 +573,7 @@ export default {
|
||||
return this.workspace ? this.workspace.selectedTab : null;
|
||||
},
|
||||
queryTabs () {
|
||||
return this.workspace.tabs.filter(tab => tab.type === 'query');
|
||||
return this.workspace ? this.workspace.tabs.filter(tab => tab.type === 'query') : [];
|
||||
},
|
||||
schemaChild () {
|
||||
for (const key in this.workspace.breadcrumbs) {
|
||||
@@ -373,28 +581,33 @@ export default {
|
||||
if (this.workspace.breadcrumbs[key]) return this.workspace.breadcrumbs[key];
|
||||
}
|
||||
return false;
|
||||
},
|
||||
hasTools () {
|
||||
return this.workspace.customizations.processesList ||
|
||||
this.workspace.customizations.usersManagement ||
|
||||
this.workspace.customizations.variables;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedTab (newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
queryTabs: function (newVal, oldVal) {
|
||||
if (newVal.length > oldVal.length) {
|
||||
setTimeout(() => {
|
||||
const element = this.$refs['tab-selected'] ? this.$refs['tab-selected'][0] : null;
|
||||
if (element) {
|
||||
element.setAttribute('tabindex', '-1');
|
||||
element.focus();
|
||||
element.removeAttribute('tabindex');
|
||||
}
|
||||
}, 50);
|
||||
const scroller = this.$refs.tabWrap;
|
||||
if (scroller) scroller.$el.scrollLeft = scroller.$el.scrollWidth;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
window.addEventListener('keydown', this.onKey);
|
||||
await this.addWorkspace(this.connection.uid);
|
||||
const isInitiated = await Connection.checkConnection(this.connection.uid);
|
||||
if (isInitiated)
|
||||
this.connectWorkspace(this.connection);
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addWorkspace: 'workspaces/addWorkspace',
|
||||
@@ -408,6 +621,25 @@ export default {
|
||||
addQueryTab () {
|
||||
this.newTab({ uid: this.connection.uid, type: 'query' });
|
||||
},
|
||||
getSelectedTab () {
|
||||
return this.workspace.tabs.find(tab => tab.uid === this.selectedTab);
|
||||
},
|
||||
onKey (e) {
|
||||
e.stopPropagation();
|
||||
|
||||
if (!this.isSelected)
|
||||
return;
|
||||
|
||||
if ((e.ctrlKey || e.metaKey) && e.keyCode === 84 && !e.altKey) { // CTRL|Command + t
|
||||
this.addQueryTab();
|
||||
}
|
||||
|
||||
if ((e.ctrlKey || e.metaKey) && e.keyCode === 87 && !e.altKey) { // CTRL|Command + w
|
||||
const currentTab = this.getSelectedTab();
|
||||
if (currentTab)
|
||||
this.closeTab(currentTab);
|
||||
}
|
||||
},
|
||||
openAsPermanentTab (tab) {
|
||||
const permanentTabs = {
|
||||
table: 'data',
|
||||
@@ -589,7 +821,7 @@ export default {
|
||||
.th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
border: 1px solid;
|
||||
border: 2px solid;
|
||||
border-left: none;
|
||||
border-bottom-width: 2px;
|
||||
padding: 0;
|
||||
@@ -598,15 +830,15 @@ export default {
|
||||
z-index: 1;
|
||||
|
||||
> div {
|
||||
padding: 0.1rem 0.4rem;
|
||||
padding: 0.1rem 0.2rem;
|
||||
min-width: -webkit-fill-available;
|
||||
}
|
||||
}
|
||||
|
||||
.td {
|
||||
border-right: 1px solid;
|
||||
border-bottom: 1px solid;
|
||||
padding: 0 0.4rem;
|
||||
border-right: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
padding: 0 0.2rem;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 200px;
|
||||
white-space: nowrap;
|
||||
|
@@ -11,6 +11,7 @@
|
||||
<a class="tab-link">{{ $t('word.general') }}</a>
|
||||
</li>
|
||||
<li
|
||||
v-if="customizations.sslConnection"
|
||||
class="tab-item c-hand"
|
||||
:class="{'active': selectedTab === 'ssl'}"
|
||||
@click="selectTab('ssl')"
|
||||
@@ -18,6 +19,7 @@
|
||||
<a class="tab-link">{{ $t('word.ssl') }}</a>
|
||||
</li>
|
||||
<li
|
||||
v-if="customizations.sshConnection"
|
||||
class="tab-item c-hand"
|
||||
:class="{'active': selectedTab === 'ssh'}"
|
||||
@click="selectTab('ssh')"
|
||||
@@ -32,7 +34,7 @@
|
||||
<fieldset class="m-0" :disabled="isBusy">
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.connectionName') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.connectionName') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -45,31 +47,36 @@
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.client') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.client') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<select v-model="connection.client" class="form-select">
|
||||
<option value="mysql">
|
||||
MySQL
|
||||
<option
|
||||
v-for="client in clients"
|
||||
:key="client.slug"
|
||||
:value="client.slug"
|
||||
>
|
||||
{{ client.name }}
|
||||
</option>
|
||||
<option value="maria">
|
||||
MariaDB
|
||||
</option>
|
||||
<option value="pg">
|
||||
PostgreSQL
|
||||
</option>
|
||||
<!-- <option value="mssql">
|
||||
Microsoft SQL
|
||||
</option>
|
||||
<option value="oracledb">
|
||||
Oracle DB
|
||||
</option> -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div v-if="connection.client === 'pg'" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
|
||||
<label class="form-label cut-text">{{ $t('word.connectionString') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
ref="pgString"
|
||||
v-model="connection.pgConnString"
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!customizations.fileConnection" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -79,9 +86,22 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div v-if="customizations.fileConnection" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.port') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.database') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<BaseUploadInput
|
||||
:value="connection.databasePath"
|
||||
:message="$t('word.browse')"
|
||||
@clear="pathClear('databasePath')"
|
||||
@change="pathSelection($event, 'databasePath')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!customizations.fileConnection" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label cut-text">{{ $t('word.port') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -95,7 +115,7 @@
|
||||
</div>
|
||||
<div v-if="customizations.database" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.database') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.database') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -105,9 +125,9 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div v-if="!customizations.fileConnection" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.user') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.user') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -118,9 +138,9 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div v-if="!customizations.fileConnection" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.password') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.password') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -133,7 +153,7 @@
|
||||
</div>
|
||||
<div v-if="customizations.connectionSchema" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.schema') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.schema') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -144,7 +164,15 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div v-if="customizations.readOnlyMode" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12" />
|
||||
<div class="column col-8 col-sm-12">
|
||||
<label class="form-checkbox form-inline">
|
||||
<input v-model="connection.readonly" type="checkbox"><i class="form-icon" /> {{ $t('message.readOnlyMode') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!customizations.fileConnection" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12" />
|
||||
<div class="column col-8 col-sm-12">
|
||||
<label class="form-checkbox form-inline">
|
||||
@@ -161,7 +189,7 @@
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">
|
||||
<label class="form-label cut-text">
|
||||
{{ $t('message.enableSsl') }}
|
||||
</label>
|
||||
</div>
|
||||
@@ -175,7 +203,7 @@
|
||||
<fieldset class="m-0" :disabled="isBusy || !connection.ssl">
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.privateKey') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<BaseUploadInput
|
||||
@@ -188,7 +216,7 @@
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.certificate') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.certificate') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<BaseUploadInput
|
||||
@@ -201,7 +229,7 @@
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.caCertificate') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.caCertificate') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<BaseUploadInput
|
||||
@@ -214,7 +242,7 @@
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.ciphers') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.ciphers') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -234,7 +262,7 @@
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">
|
||||
<label class="form-label cut-text">
|
||||
{{ $t('message.enableSsh') }}
|
||||
</label>
|
||||
</div>
|
||||
@@ -248,7 +276,7 @@
|
||||
<fieldset class="m-0" :disabled="isBusy || !connection.ssh">
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
|
||||
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -260,7 +288,7 @@
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.user') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.user') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -272,7 +300,7 @@
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.password') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.password') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -284,7 +312,7 @@
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.port') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.port') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -298,7 +326,7 @@
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.privateKey') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<BaseUploadInput
|
||||
@@ -309,6 +337,18 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label cut-text">{{ $t('word.passphrase') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
v-model="connection.sshPassphrase"
|
||||
class="form-input"
|
||||
type="password"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
@@ -357,15 +397,23 @@ export default {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
clients: [
|
||||
{ name: 'MySQL', slug: 'mysql' },
|
||||
{ name: 'MariaDB', slug: 'maria' },
|
||||
{ name: 'PostgreSQL', slug: 'pg' },
|
||||
{ name: 'SQLite', slug: 'sqlite' }
|
||||
],
|
||||
connection: {
|
||||
name: '',
|
||||
client: 'mysql',
|
||||
host: '127.0.0.1',
|
||||
database: null,
|
||||
databasePath: '',
|
||||
port: null,
|
||||
user: null,
|
||||
password: '',
|
||||
ask: false,
|
||||
readonly: false,
|
||||
uid: uidGen('C'),
|
||||
ssl: false,
|
||||
cert: '',
|
||||
@@ -377,7 +425,8 @@ export default {
|
||||
sshUser: '',
|
||||
sshPass: '',
|
||||
sshKey: '',
|
||||
sshPort: 22
|
||||
sshPort: 22,
|
||||
pgConnString: ''
|
||||
},
|
||||
isConnecting: false,
|
||||
isTesting: false,
|
||||
@@ -393,6 +442,13 @@ export default {
|
||||
return this.isConnecting || this.isTesting;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'connection.client' () {
|
||||
this.connection.user = this.customizations.defaultUser;
|
||||
this.connection.port = this.customizations.defaultPort;
|
||||
this.connection.database = this.customizations.defaultDatabase;
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.setDefaults();
|
||||
|
||||
@@ -432,7 +488,7 @@ export default {
|
||||
try {
|
||||
const res = await Connection.makeTest(this.connection);
|
||||
if (res.status === 'error')
|
||||
this.addNotification({ status: 'error', message: res.response.message });
|
||||
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
|
||||
else
|
||||
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
|
||||
}
|
||||
@@ -455,7 +511,7 @@ export default {
|
||||
else {
|
||||
const res = await Connection.makeTest(params);
|
||||
if (res.status === 'error')
|
||||
this.addNotification({ status: 'error', message: res.response.message });
|
||||
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
|
||||
else
|
||||
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
|
||||
}
|
||||
@@ -498,12 +554,12 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.connection-panel {
|
||||
margin-top: 15vh;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.panel {
|
||||
width: 450px;
|
||||
min-width: 450px;
|
||||
border-radius: $border-radius;
|
||||
|
||||
.panel-body {
|
||||
|
@@ -11,6 +11,7 @@
|
||||
<a class="tab-link">{{ $t('word.general') }}</a>
|
||||
</li>
|
||||
<li
|
||||
v-if="customizations.sslConnection"
|
||||
class="tab-item c-hand"
|
||||
:class="{'active': selectedTab === 'ssl'}"
|
||||
@click="selectTab('ssl')"
|
||||
@@ -18,6 +19,7 @@
|
||||
<a class="tab-link">{{ $t('word.ssl') }}</a>
|
||||
</li>
|
||||
<li
|
||||
v-if="customizations.sshConnection"
|
||||
class="tab-item c-hand"
|
||||
:class="{'active': selectedTab === 'ssh'}"
|
||||
@click="selectTab('ssh')"
|
||||
@@ -32,7 +34,7 @@
|
||||
<fieldset class="m-0" :disabled="isBusy">
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.connectionName') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.connectionName') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -45,25 +47,36 @@
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.client') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.client') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<select v-model="localConnection.client" class="form-select">
|
||||
<option value="mysql">
|
||||
MySQL
|
||||
</option>
|
||||
<option value="maria">
|
||||
MariaDB
|
||||
</option>
|
||||
<option value="pg">
|
||||
PostgreSQL
|
||||
<option
|
||||
v-for="client in clients"
|
||||
:key="client.slug"
|
||||
:value="client.slug"
|
||||
>
|
||||
{{ client.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div v-if="connection.client === 'pg'" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
|
||||
<label class="form-label cut-text">{{ $t('word.connectionString') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
ref="pgString"
|
||||
v-model="localConnection.pgConnString"
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!customizations.fileConnection" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -73,9 +86,22 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div v-if="customizations.fileConnection" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.port') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.database') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<BaseUploadInput
|
||||
:value="localConnection.databasePath"
|
||||
:message="$t('word.browse')"
|
||||
@clear="pathClear('databasePath')"
|
||||
@change="pathSelection($event, 'databasePath')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!customizations.fileConnection" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label cut-text">{{ $t('word.port') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -89,7 +115,7 @@
|
||||
</div>
|
||||
<div v-if="customizations.database" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.database') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.database') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -99,9 +125,9 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div v-if="!customizations.fileConnection" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.user') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.user') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -112,9 +138,9 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div v-if="!customizations.fileConnection" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.password') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.password') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -127,7 +153,7 @@
|
||||
</div>
|
||||
<div v-if="customizations.connectionSchema" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.schema') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.schema') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -138,7 +164,15 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div v-if="customizations.readOnlyMode" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12" />
|
||||
<div class="column col-8 col-sm-12">
|
||||
<label class="form-checkbox form-inline">
|
||||
<input v-model="localConnection.readonly" type="checkbox"><i class="form-icon" /> {{ $t('message.readOnlyMode') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!customizations.fileConnection" class="form-group columns">
|
||||
<div class="column col-4 col-sm-12" />
|
||||
<div class="column col-8 col-sm-12">
|
||||
<label class="form-checkbox form-inline">
|
||||
@@ -155,7 +189,7 @@
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">
|
||||
<label class="form-label cut-text">
|
||||
{{ $t('message.enableSsl') }}
|
||||
</label>
|
||||
</div>
|
||||
@@ -169,7 +203,7 @@
|
||||
<fieldset class="m-0" :disabled="isBusy || !localConnection.ssl">
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.privateKey') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<BaseUploadInput
|
||||
@@ -182,7 +216,7 @@
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.certificate') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.certificate') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<BaseUploadInput
|
||||
@@ -195,7 +229,7 @@
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.caCertificate') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.caCertificate') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<BaseUploadInput
|
||||
@@ -208,7 +242,7 @@
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.ciphers') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.ciphers') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -228,7 +262,7 @@
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">
|
||||
<label class="form-label cut-text">
|
||||
{{ $t('message.enableSsh') }}
|
||||
</label>
|
||||
</div>
|
||||
@@ -242,7 +276,7 @@
|
||||
<fieldset class="m-0" :disabled="isBusy || !localConnection.ssh">
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
|
||||
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -254,7 +288,7 @@
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.user') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.user') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -266,7 +300,7 @@
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.password') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.password') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -278,7 +312,7 @@
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.port') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.port') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
@@ -292,7 +326,7 @@
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label">{{ $t('word.privateKey') }}</label>
|
||||
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<BaseUploadInput
|
||||
@@ -303,6 +337,18 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group columns">
|
||||
<div class="column col-4 col-sm-12">
|
||||
<label class="form-label cut-text">{{ $t('word.passphrase') }}</label>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<input
|
||||
v-model="localConnection.sshPassphrase"
|
||||
class="form-input"
|
||||
type="password"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
@@ -362,6 +408,12 @@ export default {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
clients: [
|
||||
{ name: 'MySQL', slug: 'mysql' },
|
||||
{ name: 'MariaDB', slug: 'maria' },
|
||||
{ name: 'PostgreSQL', slug: 'pg' },
|
||||
{ name: 'SQLite', slug: 'sqlite' }
|
||||
],
|
||||
isConnecting: false,
|
||||
isTesting: false,
|
||||
isAsking: false,
|
||||
@@ -371,7 +423,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
customizations () {
|
||||
return customizations[this.connection.client];
|
||||
return customizations[this.localConnection.client];
|
||||
},
|
||||
isBusy () {
|
||||
return this.isConnecting || this.isTesting;
|
||||
@@ -414,7 +466,7 @@ export default {
|
||||
try {
|
||||
const res = await Connection.makeTest(this.localConnection);
|
||||
if (res.status === 'error')
|
||||
this.addNotification({ status: 'error', message: res.response.message });
|
||||
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
|
||||
else
|
||||
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
|
||||
}
|
||||
@@ -437,7 +489,7 @@ export default {
|
||||
else {
|
||||
const res = await Connection.makeTest(params);
|
||||
if (res.status === 'error')
|
||||
this.addNotification({ status: 'error', message: res.response.message });
|
||||
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
|
||||
else
|
||||
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
|
||||
}
|
||||
@@ -454,6 +506,7 @@ export default {
|
||||
closeAsking () {
|
||||
this.isTesting = false;
|
||||
this.isAsking = false;
|
||||
this.isConnecting = false;
|
||||
},
|
||||
selectTab (tab) {
|
||||
this.selectedTab = tab;
|
||||
@@ -479,12 +532,12 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.connection-panel {
|
||||
margin-top: 15vh;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.panel {
|
||||
width: 450px;
|
||||
min-width: 450px;
|
||||
border-radius: $border-radius;
|
||||
|
||||
.panel-body {
|
||||
|