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

Compare commits

...

108 Commits

Author SHA1 Message Date
76dc66c14d chore(release): 0.3.10 2021-11-24 17:26:42 +01:00
cc99491fe4 fix(UI): notifications timeout anomalies 2021-11-24 16:59:07 +01:00
fe8435531e Merge pull request #143 from Fabio286/feat/read-only-mode-on-connections
feat: read-only mode
2021-11-24 14:41:31 +01:00
5d48fe08c7 feat(PostgreSQL): read-only mode 2021-11-24 14:24:52 +01:00
4437d44486 feat(MySQL): read-only mode 2021-11-24 13:04:14 +01:00
9fe3680bbb perf: update italian traslation 2021-11-24 09:52:10 +01:00
da1947e4ef fix(UI): hide tools menu if no tools available 2021-11-24 09:19:42 +01:00
37a848df9d Merge pull request #142 from Fabio286/dependabot/npm_and_yarn/electron-16.0.1
build(deps-dev): bump electron from 15.3.2 to 16.0.1
2021-11-23 17:38:21 +01:00
dependabot[bot]
9816965e18 build(deps-dev): bump electron from 15.3.2 to 16.0.1
Bumps [electron](https://github.com/electron/electron) from 15.3.2 to 16.0.1.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v15.3.2...v16.0.1)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-22 19:05:55 +00:00
98165cacaa fix(SQLite): hide schema creation 2021-11-19 16:50:15 +01:00
7ad3096b4e chore: update README.md 2021-11-19 16:14:11 +01:00
7d345cf795 Merge pull request #141 from Fabio286/feat/sqlite-implementation
feat:sqlite implementation
2021-11-19 16:11:00 +01:00
f40e9c592e feat(SQLite): triggers management 2021-11-19 15:36:07 +01:00
7671c585f5 feat(SQLite): views management 2021-11-19 13:13:35 +01:00
93b4a7063b perf(SQLite): improvements in field length detection 2021-11-18 19:43:08 +01:00
3efeb45c46 feat(SQLite): tables management 2021-11-18 11:36:46 +01:00
3fc227d2de feat(SQLite): readonly mode 2021-11-16 13:21:33 +01:00
604b371920 feat(SQLite): cell update in data tabs 2021-11-16 12:56:03 +01:00
fd321beece feat(SQLite): keys support 2021-11-16 12:27:51 +01:00
9fe4e6b9e3 Merge pull request #140 from Fabio286/all-contributors/add-kilianstallz
docs: add kilianstallz as a contributor for code
2021-11-15 22:38:24 +01:00
allcontributors[bot]
57bf90481b docs: update .all-contributorsrc [skip ci] 2021-11-15 21:37:49 +00:00
allcontributors[bot]
6062a32c1c docs: update README.md [skip ci] 2021-11-15 21:37:48 +00:00
94c899eb82 perf(SQLite): improvements in data visualization 2021-11-15 18:09:34 +01:00
44a4ca75bd Merge pull request #139 from kilianstallz/kilianstallz-patch-table-rows
Hotfix quote style on delete-table-rows handler
2021-11-15 17:01:10 +01:00
Kilian Stallinger
c8e1605b08 Hotfix quote style on delete-table-rows handler
Use single quote as intended for ID in delete query.
2021-11-15 16:21:09 +01:00
60e5556a3e chore(release): 0.3.9 2021-11-14 10:51:45 +01:00
f2fcc98839 feat(SQLite): table data visualization 2021-11-13 23:00:53 +01:00
c54438d6d3 feat(SQLite): connection add/edit masks 2021-11-13 11:34:30 +01:00
b3f10220b3 build: transparent background for appx icon 2021-11-12 13:12:05 +01:00
d19f475fc2 perf(UI): improved function and routine parameters modals 2021-11-12 12:22:39 +01:00
795db96319 Merge pull request #135 from Fabio286/dependabot/npm_and_yarn/marked-4.0.0
build(deps): bump marked from 3.0.8 to 4.0.0
2021-11-10 12:00:20 +01:00
b5fee79e90 refactor: minor changes to support marked 4.0.0 2021-11-10 11:48:06 +01:00
dependabot[bot]
5532ddbda9 build(deps): bump marked from 3.0.8 to 4.0.0
Bumps [marked](https://github.com/markedjs/marked) from 3.0.8 to 4.0.0.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Changelog](https://github.com/markedjs/marked/blob/master/.releaserc.json)
- [Commits](https://github.com/markedjs/marked/compare/v3.0.8...v4.0.0)

---
updated-dependencies:
- dependency-name: marked
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-08 19:06:33 +00:00
3369d3dc2d refactor: prefix to internal rows id 2021-11-08 15:12:37 +01:00
fd25f881f9 feat: schema size in explore bar 2021-11-06 16:36:54 +01:00
5ca3a22dc5 feat(MySQL): enable/disable schedulers from contextual menu 2021-11-06 12:21:33 +01:00
5c668249cf feat(MySQL): scheduler status indicator in explore bar 2021-11-05 18:23:02 +01:00
39b9a59143 fix(PostgreSQL): bigint fetched as string instead of number, closes #134 2021-11-05 11:41:12 +01:00
534659f9ae feat(PostgreSQL): enable/disable triggers from contextual menu 2021-11-04 21:54:42 +01:00
c00fd1381f fix: temporary solution on MacOS for unsigned app updates 2021-11-03 14:46:13 +01:00
17c6686163 Merge pull request #133 from Fabio286/dependabot/npm_and_yarn/webpack-dev-server-4.4.0
build(deps-dev): bump webpack-dev-server from 3.11.2 to 4.4.0
2021-11-02 22:23:56 +01:00
bc0c5a76ba refactor: minor changes to support webpack-dev-server 4.4.0 2021-11-02 22:23:10 +01:00
2b16c0ece4 Merge pull request #132 from Fabio286/dependabot/npm_and_yarn/sass-loader-12.3.0
build(deps-dev): bump sass-loader from 10.2.0 to 12.3.0
2021-11-02 22:02:07 +01:00
dependabot[bot]
ba1416dce2 build(deps-dev): bump webpack-dev-server from 3.11.2 to 4.4.0
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 3.11.2 to 4.4.0.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v3.11.2...v4.4.0)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-02 20:55:42 +00:00
13adf0a767 Merge pull request #131 from Fabio286/dependabot/npm_and_yarn/eslint-plugin-vue-8.0.3
build(deps-dev): bump eslint-plugin-vue from 7.20.0 to 8.0.3
2021-11-02 21:52:38 +01:00
cacab55f55 refactor: minor changes to support eslint-plugin-vue 8 2021-11-02 21:51:53 +01:00
dependabot[bot]
3189d625e3 build(deps-dev): bump sass-loader from 10.2.0 to 12.3.0
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 10.2.0 to 12.3.0.
- [Release notes](https://github.com/webpack-contrib/sass-loader/releases)
- [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/sass-loader/compare/v10.2.0...v12.3.0)

---
updated-dependencies:
- dependency-name: sass-loader
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 19:07:14 +00:00
dependabot[bot]
f71ed39b88 build(deps-dev): bump eslint-plugin-vue from 7.20.0 to 8.0.3
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 7.20.0 to 8.0.3.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v7.20.0...v8.0.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 19:05:26 +00:00
729aa9781a chore: update README.md and CONTRIBUTING.md 2021-10-31 16:07:53 +01:00
cd1ebacf89 Merge pull request #126 from toriphes/master
feat: added macos basic shortcusts and menu
2021-10-31 15:24:56 +01:00
a08074b446 Merge branch 'master' of https://github.com/Fabio286/antares into pr/toriphes/126 2021-10-31 15:14:49 +01:00
0cd182546b build: electron-webpack replacement (#130)
* some changes

* improvements and dedicated webpeck configs for render and main

* added debugging setup

* vscode main process debug config

* vue3 devtools
2021-10-31 10:36:45 +01:00
89fdd210ca fix: row selection problems after a deletion fail, closes #128 2021-10-29 22:43:22 +02:00
Giulio Ganci
e2cf6e8c21 Merge branch 'Fabio286:master' into master 2021-10-29 12:43:00 +02:00
Giulio Ganci
430490ad93 feat: added macos basic shortcusts and menu 2021-10-24 13:02:37 +02:00
Giulio Ganci
a35566f273 feat(UI): double click on the title bar will toggle window fullscreen size 2021-10-24 13:00:36 +02:00
3679121c25 Merge pull request #125 from toriphes/master
MacOS improvements
2021-10-24 10:32:46 +02:00
Giulio Ganci
7657d05edf feat(UI): improved topbar look&feel on MacOS 2021-10-23 18:15:32 +02:00
Giulio Ganci
1ddf8f0dbe fix: copy&paste and basic usability on macOS 2021-10-23 17:56:42 +02:00
9506bb862a chore(release): 0.3.8 2021-10-23 10:18:46 +02:00
4cfab365c2 build: added .nvmrc 2021-10-21 18:17:40 +02:00
8d0aa73d1e chore: update dependencies 2021-10-21 10:51:07 +02:00
30b487c37f refactor(UI): removed text from refresh buttons 2021-10-20 18:14:02 +02:00
aef17be36c fix(PostgreSQL): issue with uppercase characters in table field names 2021-10-19 17:42:31 +02:00
ea65d8eee7 Merge pull request #123 from toriphes/master
feat(UI): multi column table filters
2021-10-19 09:11:29 +02:00
Giulio Ganci
7dc33c78aa fix: regression during resize results table on filters change 2021-10-18 21:23:22 +02:00
Giulio Ganci
69cd083054 fix: query failure when a filter with a numeric value is used 2021-10-18 21:13:19 +02:00
Giulio Ganci
91788054e6 feat(UI): hide filter bar if there are no more rows in it 2021-10-18 21:11:09 +02:00
Giulio Ganci
968a67ce3d refactor(UI): removed duplicate add filter button 2021-10-18 19:15:37 +02:00
f9ee7d0450 perf(UI): resize results table on filters change 2021-10-18 11:58:29 +02:00
Giulio Ganci
0e15c39797 feat(UI): multi column table filters 2021-10-17 23:54:00 +02:00
adf407c1ba Merge pull request #122 from toriphes/master
feat(UI): ctrl|cmd+t, ctrl|cmd+w shortcut to open/close workspace tabs
2021-10-16 18:50:51 +02:00
8a86344484 refactor(UI): display new shortcuts in empty query tab 2021-10-16 18:46:17 +02:00
d2d0c3ca41 refactor(UI): changed query clear shortcut 2021-10-16 18:43:23 +02:00
Giulio Ganci
9046b858b1 feat(UI): ctrl|cmd+t, ctrl|cmd+w shortcut to open/close workspace tabs 2021-10-16 17:05:26 +02:00
46987faea8 Merge pull request #121 from datlechin/master
Add "passphrase" to Vietnamese translation
2021-10-16 14:18:23 +02:00
Ngô Quốc Đạt
dd25827e40 Add "passphrase" to Vietnamese translation 2021-10-16 18:52:39 +07:00
4069ade36d feat(UI): CTRL+A to select all result rows 2021-10-16 09:58:32 +02:00
c8594c0549 chore(release): 0.3.7 2021-10-08 09:13:23 +02:00
7359c3b5bd Merge pull request #107 from Fabio286/dependabot/npm_and_yarn/ssh2-promise-1.0.2
build(deps): bump ssh2-promise from 0.2.0 to 1.0.2
2021-10-07 15:27:40 +02:00
4195b8416f Create CONTRIBUTING.md 2021-10-07 15:15:12 +02:00
9407a29922 feat: support to SSH private keys with passphrase, closes #118 2021-10-07 14:58:31 +02:00
2fcd080bd4 fix(PostgreSQL): issue with uppercase characters in table and field names, closes #116 2021-10-06 12:08:37 +02:00
26446fb7ed fix: closing ask credential modal during a connection doesn't stops loading, closes #114 2021-10-05 18:20:38 +02:00
2480c76a08 chore: update README.md 2021-10-05 10:53:45 +02:00
1f0ec57789 Merge pull request #115 from IsamuSugi/Japanese-translation
Add new language: Japanese
2021-10-05 10:49:47 +02:00
isamu
76e5849c78 Add new language: Japanese 2021-10-05 17:22:07 +09:00
db1641b74f fix(UI): window reload pressing enter in schema creation modal, closes #113 2021-10-05 09:16:45 +02:00
165c54f663 Merge pull request #111 from datlechin/master
Add new language: Tiếng Việt
2021-10-05 09:04:19 +02:00
Ngo Quoc Dat
a5ca3ea204 Add new language: Tiếng Việt 2021-10-04 18:30:44 +07:00
dependabot[bot]
add95292ad build(deps): bump ssh2-promise from 0.2.0 to 1.0.2
Bumps [ssh2-promise](https://github.com/sanketbajoria/ssh2-promise) from 0.2.0 to 1.0.2.
- [Release notes](https://github.com/sanketbajoria/ssh2-promise/releases)
- [Commits](https://github.com/sanketbajoria/ssh2-promise/commits)

---
updated-dependencies:
- dependency-name: ssh2-promise
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-01 13:52:48 +00:00
7a63608f54 feat(UI): auto detect system theme as default app theme 2021-10-01 15:48:02 +02:00
7ea7b369ab chore: update dependencies 2021-10-01 15:33:10 +02:00
258fffa958 chore: update README.md 2021-09-30 09:05:40 +02:00
0849c5131f Merge pull request #106 from Fabio286/dependabot/npm_and_yarn/electron-15.0.0
build(deps-dev): bump electron from 14.0.1 to 15.0.0
2021-09-28 09:19:16 +02:00
dependabot[bot]
cce59d0ca8 build(deps-dev): bump electron from 14.0.1 to 15.0.0
Bumps [electron](https://github.com/electron/electron) from 14.0.1 to 15.0.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v14.0.1...v15.0.0)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-27 19:06:30 +00:00
94a53fec6c chore(release): 0.3.6 2021-09-26 11:39:19 +02:00
13aa47cd44 feat: processes list exportation 2021-09-26 11:36:42 +02:00
85f625daf7 feat: copy cell/row or kill connections on context menu from processes list 2021-09-26 11:19:48 +02:00
7de3bb9346 refactor(core): compatibility with electron/remote 2.x 2021-09-21 11:16:07 +02:00
e43a0ba0b4 Merge pull request #102 from Fabio286/dependabot/npm_and_yarn/electron/remote-2.0.1
build(deps): bump @electron/remote from 1.2.2 to 2.0.1
2021-09-21 10:03:35 +02:00
dependabot[bot]
638cd4bfb7 build(deps): bump @electron/remote from 1.2.2 to 2.0.1
Bumps [@electron/remote](https://github.com/electron/remote) from 1.2.2 to 2.0.1.
- [Release notes](https://github.com/electron/remote/releases)
- [Changelog](https://github.com/electron/remote/blob/main/.releaserc.json)
- [Commits](https://github.com/electron/remote/compare/v1.2.2...v2.0.1)

---
updated-dependencies:
- dependency-name: "@electron/remote"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-20 19:06:45 +00:00
3959333662 feat: workspace query history 2021-09-17 18:32:28 +02:00
abd46aa322 perf(core): better communication of internal exceptions 2021-09-15 15:31:57 +02:00
d4888ad8fb fix: adding a connection default values not change when switching clients, closes #101 2021-09-15 11:08:00 +02:00
6bfc229b77 chore(release): 0.3.5 2021-09-13 15:03:02 +02:00
d31b051f4b chore: update dependencies 2021-09-13 15:02:28 +02:00
95b60df8fc fix(MySQL): connections stuck at startup if 5 or more tabs are restored 2021-09-13 10:03:27 +02:00
108 changed files with 4636 additions and 587 deletions

127
.all-contributorsrc Normal file
View File

@@ -0,0 +1,127 @@
{
"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"
]
}
],
"contributorsPerLine": 7,
"skipCi": true
}

View File

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

View File

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

7
.gitignore vendored
View File

@@ -1,9 +1,8 @@
.DS_Store .DS_Store
dist/ dist
node_modules/ build
node_modules
thumbs.db thumbs.db
.idea/
.vscode
NOTES.md NOTES.md
*.txt *.txt
package-lock.json package-lock.json

1
.nvmrc Normal file
View File

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

29
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,29 @@
{
"version": "0.2.0",
"configurations": [
{
"cwd": "${workspaceFolder}",
"name": "Electron: Main",
"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}"
}
],
"compounds": [
{
"name": "Electron: All",
"configurations": ["Electron: Main", "Electron: Renderer"]
}
]
}

10
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"conventionalCommits.scopes": [
"UI",
"core",
"MySQL",
"PostgreSQL",
"SQLite"
],
"svg.preview.background": "transparent"
}

View File

@@ -2,6 +2,125 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.3.10](https://github.com/Fabio286/antares/compare/v0.3.9...v0.3.10) (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) ### [0.3.4](https://github.com/Fabio286/antares/compare/v0.3.3...v0.3.4) (2021-09-12)

107
CONTRIBUTING.md Normal file
View 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.

View File

@@ -1,6 +1,9 @@
<!-- markdownlint-disable -->
<p align="center"> <p align="center">
<img width="800" src="https://raw.githubusercontent.com/Fabio286/antares/master/docs/gh-logo.png"> <img width="800" src="https://raw.githubusercontent.com/Fabio286/antares/master/docs/gh-logo.png">
</p> </p>
<!-- markdownlint-restore -->
# Antares SQL Client # Antares SQL Client
@@ -9,7 +12,7 @@
Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers. Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers.
Our target is to support as many databases as possible, and all major operating systems, including the ARM versions. Our target is to support as many databases as possible, and all major operating systems, including the ARM versions.
**At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB and PostgreSQL. **At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB, PostgreSQL and SQLite.
At the moment, however, there are all the features necessary to have a pleasant database management experience, so give it a chance and send us your feedback, we would really appreciate it. At the moment, however, there are all the features necessary to have a pleasant database management experience, so give it a chance and send us your feedback, we would really appreciate it.
We are actively working on it, hoping to provide new cool features, improvements and fixes as soon as possible. We are actively working on it, hoping to provide new cool features, improvements and fixes as soon as possible.
@@ -26,6 +29,7 @@ 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. - 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. - Fake table data filler to generate tons of data for test purpose.
- Query suggestions and auto complete. - Query suggestions and auto complete.
- Query history: search through the last 1000 queries.
- SSH tunnel support. - SSH tunnel support.
- Dark and light theme. - Dark and light theme.
- Editor themes. - Editor themes.
@@ -64,13 +68,12 @@ On macOS you can run `.dmg` distribution following [this guide](https://support.
This is a roadmap with major features will come in near future. This is a roadmap with major features will come in near future.
- Support for other databases.
- Database tools. - Database tools.
- Users management (add/edit/delete). - Users management (add/edit/delete).
- Query history and bookmarks.
- More context menu shortcuts. - More context menu shortcuts.
- More keyboard shortcuts. - More keyboard shortcuts.
- Import/export and migration. - Import/export and migration.
- Support for other databases.
- Apple Silicon distribution - Apple Silicon distribution
## Currently supported ## Currently supported
@@ -79,7 +82,7 @@ This is a roadmap with major features will come in near future.
- [x] MySQL/MariaDB - [x] MySQL/MariaDB
- [x] PostgreSQL - [x] PostgreSQL
- [ ] SQLite - [x] SQLite
- [ ] MSSQL - [ ] MSSQL
- [ ] OracleDB - [ ] OracleDB
- [ ] More... - [ ] More...
@@ -104,11 +107,35 @@ This is a roadmap with major features will come in near future.
- 📖 [Contributors Guide](https://github.com/Fabio286/antares/wiki/Contributors-Guide) - 📖 [Contributors Guide](https://github.com/Fabio286/antares/wiki/Contributors-Guide)
- 🚧 [Project Board](https://github.com/users/Fabio286/projects/1) - 🚧 [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)] Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
**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)] <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
**French Translation** (needs updates) / [MrAnyx](https://github.com/MrAnyx) [[#44](https://github.com/Fabio286/antares/pull/44)] <!-- prettier-ignore-start -->
**Portugues (Brasil)** / [Daniel Eduardo](https://github.com/daeleduardo) [[#54](https://github.com/Fabio286/antares/pull/54)] <!-- markdownlint-disable -->
**Deutsch (Deutschland)** / [Christian Ratz](https://github.com/digitalgopnik) [[#74](https://github.com/Fabio286/antares/pull/74)] <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>
</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!

View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -1,5 +0,0 @@
{
"include": [
"./src/renderer/**/*"
]
}

View File

@@ -1,27 +1,46 @@
{ {
"name": "antares", "name": "antares",
"productName": "Antares", "productName": "Antares",
"version": "0.3.4", "version": "0.3.10",
"description": "A cross-platform easy to use SQL client.", "description": "A modern, fast and productivity driven SQL client with a focus in UX.",
"license": "MIT", "license": "MIT",
"repository": "https://github.com/Fabio286/antares.git", "repository": "https://github.com/Fabio286/antares.git",
"scripts": { "scripts": {
"dev": "cross-env NODE_ENV=development electron-webpack dev", "debug": "npm run rebuild:electron && npm run debug-runner",
"compile": "electron-webpack", "debug-runner": "node scripts/devRunner.js --remote-debug",
"compile": "npm run compile:main && npm run compile:renderer",
"compile:main": "webpack --mode=production --config webpack.main.config.js",
"compile:renderer": "webpack --mode=production --config webpack.renderer.config.js",
"build": "cross-env NODE_ENV=production npm run compile", "build": "cross-env NODE_ENV=production npm run compile",
"build:local": "npm run build && electron-builder", "build:local": "npm run build && electron-builder",
"build:appx": "npm run build:local -- --win appx", "build:appx": "npm run build:local -- --win appx",
"rebuild:electron": "npm run postinstall",
"release": "standard-version", "release": "standard-version",
"release:pre": "npm run release -- --prerelease alpha", "release:pre": "npm run release -- --prerelease alpha",
"postinstall": "electron-builder install-app-deps",
"test": "npm run lint", "test": "npm run lint",
"lint": "eslint . --ext .js,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"", "lint": "eslint . --ext .js,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"",
"lint:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix", "lint:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix",
"postinstall": "electron-builder install-app-deps" "contributors:add": "all-contributors add",
"contributors:generate": "all-contributors generate"
}, },
"author": "Fabio Di Stasio <fabio286@gmail.com>", "author": "Fabio Di Stasio <fabio286@gmail.com>",
"main": "./dist/main.js",
"build": { "build": {
"appId": "com.fabio286.antares", "appId": "com.fabio286.antares",
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}", "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": { "win": {
"target": [ "target": [
"nsis", "nsis",
@@ -61,7 +80,8 @@
"artifactName": "${productName}-${version}-portable.exe" "artifactName": "${productName}-${version}-portable.exe"
}, },
"appx": { "appx": {
"displayName": "Antares SQL Client", "displayName": "Antares SQL",
"backgroundColor": "transparent",
"identityName": "62514FabioDiStasio.AntaresSQLClient", "identityName": "62514FabioDiStasio.AntaresSQLClient",
"publisher": "CN=1A2729ED-865C-41D2-9038-39AE2A63AA52", "publisher": "CN=1A2729ED-865C-41D2-9038-39AE2A63AA52",
"applicationId": "FabioDiStasio.AntaresSQLClient" "applicationId": "FabioDiStasio.AntaresSQLClient"
@@ -81,55 +101,65 @@
] ]
} }
}, },
"electronWebpack": {
"renderer": {
"webpackConfig": "webpack.config.js"
}
},
"dependencies": { "dependencies": {
"@electron/remote": "^1.2.1", "@electron/remote": "^2.0.1",
"@mdi/font": "^6.1.95", "@mdi/font": "^6.1.95",
"ace-builds": "^1.4.12", "ace-builds": "^1.4.13",
"better-sqlite3": "^7.4.4",
"electron-log": "^4.4.1", "electron-log": "^4.4.1",
"electron-store": "^8.0.0", "electron-store": "^8.0.1",
"electron-updater": "^4.3.9", "electron-updater": "^4.3.9",
"faker": "^5.5.3", "faker": "^5.5.3",
"marked": "^3.0.0", "marked": "^4.0.0",
"moment": "^2.29.1", "moment": "^2.29.1",
"mysql2": "^2.3.0", "mysql2": "^2.3.2",
"pg": "^8.7.1", "pg": "^8.7.1",
"pgsql-ast-parser": "^7.2.1", "pgsql-ast-parser": "^7.2.1",
"source-map-support": "^0.5.16", "source-map-support": "^0.5.20",
"spectre.css": "^0.5.9", "spectre.css": "^0.5.9",
"sql-formatter": "^4.0.2", "sql-formatter": "^4.0.2",
"ssh2-promise": "^0.2.0", "ssh2-promise": "^1.0.2",
"v-mask": "^2.2.4", "v-mask": "^2.3.0",
"vue-i18n": "^8.24.4", "vue-i18n": "^8.26.5",
"vuedraggable": "^2.24.3", "vuedraggable": "^2.24.3",
"vuex": "^3.6.2" "vuex": "^3.6.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.15.4", "@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",
"clean-webpack-plugin": "^4.0.0",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"electron": "^14.0.0", "css-loader": "^6.5.0",
"electron-builder": "^22.11.7", "electron": "^16.0.1",
"electron-builder": "^22.13.1",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-webpack": "^2.8.2",
"electron-webpack-vue": "^2.4.0",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-standard": "^16.0.3", "eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.24.2", "eslint-plugin-import": "^2.24.2",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0", "eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^7.17.0", "eslint-plugin-vue": "^8.0.3",
"sass": "^1.39.0", "file-loader": "^6.2.0",
"sass-loader": "^10.2.0", "html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.4.3",
"node-loader": "^2.0.0",
"progress-webpack-plugin": "^1.0.12",
"sass": "^1.42.1",
"sass-loader": "^12.3.0",
"standard-version": "^9.3.1", "standard-version": "^9.3.1",
"style-loader": "^3.3.1",
"stylelint": "^13.13.1", "stylelint": "^13.13.1",
"stylelint-config-standard": "^22.0.0", "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": "^2.6.14",
"vue-loader": "^15.9.8",
"vue-template-compiler": "^2.6.14", "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
View 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);
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);

View File

@@ -8,6 +8,9 @@ module.exports = {
collations: false, collations: false,
engines: false, engines: false,
connectionSchema: false, connectionSchema: false,
sslConnection: false,
sshConnection: false,
fileConnection: false,
// Tools // Tools
processesList: false, processesList: false,
usersManagement: false, usersManagement: false,
@@ -22,6 +25,8 @@ module.exports = {
functions: false, functions: false,
schedulers: false, schedulers: false,
// Settings // Settings
elementsWrapper: '',
stringsWrapper: '"',
tableAdd: false, tableAdd: false,
viewAdd: false, viewAdd: false,
triggerAdd: false, triggerAdd: false,
@@ -31,7 +36,11 @@ module.exports = {
schedulerAdd: false, schedulerAdd: false,
databaseEdit: false, databaseEdit: false,
schemaEdit: false, schemaEdit: false,
schemaDrop: false,
tableSettings: false, tableSettings: false,
tableOptions: false,
tableArray: false,
tableRealCount: false,
viewSettings: false, viewSettings: false,
triggerSettings: false, triggerSettings: false,
triggerFunctionSettings: false, triggerFunctionSettings: false,
@@ -43,14 +52,13 @@ module.exports = {
sortableFields: false, sortableFields: false,
unsigned: false, unsigned: false,
nullable: false, nullable: false,
nullablePrimary: false,
zerofill: false, zerofill: false,
tableOptions: false,
autoIncrement: false, autoIncrement: false,
comment: false, comment: false,
collation: false, collation: false,
definer: false, definer: false,
onUpdate: false, onUpdate: false,
tableArray: false,
viewAlgorithm: false, viewAlgorithm: false,
viewSqlSecurity: false, viewSqlSecurity: false,
viewUpdateOption: false, viewUpdateOption: false,
@@ -70,8 +78,10 @@ module.exports = {
triggerTableInName: false, triggerTableInName: false,
triggerUpdateColumns: false, triggerUpdateColumns: false,
triggerOnlyRename: false, triggerOnlyRename: false,
triggerEnableDisable: false,
triggerFunctionSql: false, triggerFunctionSql: false,
triggerFunctionlanguages: false, triggerFunctionlanguages: false,
parametersLength: false, parametersLength: false,
languages: false languages: false,
readOnlyMode: false
}; };

View File

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

View File

@@ -10,6 +10,8 @@ module.exports = {
connectionSchema: true, connectionSchema: true,
collations: true, collations: true,
engines: true, engines: true,
sslConnection: true,
sshConnection: true,
// Tools // Tools
processesList: true, processesList: true,
// Structure // Structure
@@ -21,6 +23,8 @@ module.exports = {
functions: true, functions: true,
schedulers: true, schedulers: true,
// Settings // Settings
elementsWrapper: '',
stringsWrapper: '"',
tableAdd: true, tableAdd: true,
viewAdd: true, viewAdd: true,
triggerAdd: true, triggerAdd: true,
@@ -28,6 +32,7 @@ module.exports = {
functionAdd: true, functionAdd: true,
schedulerAdd: true, schedulerAdd: true,
schemaEdit: true, schemaEdit: true,
schemaDrop: true,
tableSettings: true, tableSettings: true,
viewSettings: true, viewSettings: true,
triggerSettings: true, triggerSettings: true,
@@ -57,5 +62,6 @@ module.exports = {
functionDeterministic: true, functionDeterministic: true,
functionDataAccess: true, functionDataAccess: true,
functionSql: 'BEGIN\r\n\r\nEND', functionSql: 'BEGIN\r\n\r\nEND',
parametersLength: true parametersLength: true,
readOnlyMode: true
}; };

View File

@@ -8,9 +8,12 @@ module.exports = {
defaultDatabase: 'postgres', defaultDatabase: 'postgres',
// Core // Core
database: true, database: true,
sslConnection: true,
sshConnection: true,
// Tools // Tools
processesList: true, processesList: true,
// Structure // Structure
schemas: true,
tables: true, tables: true,
views: true, views: true,
triggers: true, triggers: true,
@@ -18,12 +21,15 @@ module.exports = {
routines: true, routines: true,
functions: true, functions: true,
// Settings // Settings
elementsWrapper: '"',
stringsWrapper: '\'',
tableAdd: true, tableAdd: true,
viewAdd: true, viewAdd: true,
triggerAdd: true, triggerAdd: true,
triggerFunctionAdd: true, triggerFunctionAdd: true,
routineAdd: true, routineAdd: true,
functionAdd: true, functionAdd: true,
schemaDrop: true,
databaseEdit: false, databaseEdit: false,
tableSettings: true, tableSettings: true,
viewSettings: true, viewSettings: true,
@@ -48,5 +54,7 @@ module.exports = {
triggerMultipleEvents: true, triggerMultipleEvents: true,
triggerTableInName: true, triggerTableInName: true,
triggerOnlyRename: false, triggerOnlyRename: false,
languages: ['sql', 'plpgsql', 'c', 'internal'] triggerEnableDisable: true,
languages: ['sql', 'plpgsql', 'c', 'internal'],
readOnlyMode: true
}; };

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ export const TEXT = [
'CHARACTER', 'CHARACTER',
'CHARACTER VARYING' 'CHARACTER VARYING'
]; ];
export const LONG_TEXT = [ export const LONG_TEXT = [
'TEXT', 'TEXT',
'MEDIUMTEXT', 'MEDIUMTEXT',
@@ -50,6 +51,7 @@ export const BOOLEAN = [
]; ];
export const DATE = ['DATE']; export const DATE = ['DATE'];
export const TIME = [ export const TIME = [
'TIME', 'TIME',
'TIME WITH TIME ZONE' 'TIME WITH TIME ZONE'

View File

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

View File

@@ -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();
});
}

View File

@@ -9,12 +9,16 @@ export default connections => {
port: +conn.port, port: +conn.port,
user: conn.user, user: conn.user,
password: conn.password, password: conn.password,
application_name: 'Antares SQL' application_name: 'Antares SQL',
readonly: conn.readonly
}; };
if (conn.database) if (conn.database)
params.database = conn.database; params.database = conn.database;
if (conn.databasePath)
params.databasePath = conn.databasePath;
if (conn.ssl) { if (conn.ssl) {
params.ssl = { params.ssl = {
key: conn.key ? fs.readFileSync(conn.key) : null, key: conn.key ? fs.readFileSync(conn.key) : null,
@@ -30,7 +34,8 @@ export default connections => {
username: conn.sshUser, username: conn.sshUser,
password: conn.sshPass, password: conn.sshPass,
port: conn.sshPort ? conn.sshPort : 22, port: conn.sshPort ? conn.sshPort : 22,
identity: conn.sshKey privateKey: conn.sshKey ? fs.readFileSync(conn.sshKey) : null,
passphrase: conn.sshPassphrase
}; };
} }
@@ -61,12 +66,16 @@ export default connections => {
port: +conn.port, port: +conn.port,
user: conn.user, user: conn.user,
password: conn.password, password: conn.password,
application_name: 'Antares SQL' application_name: 'Antares SQL',
readonly: conn.readonly
}; };
if (conn.database) if (conn.database)
params.database = conn.database; params.database = conn.database;
if (conn.databasePath)
params.databasePath = conn.databasePath;
if (conn.schema) if (conn.schema)
params.schema = conn.schema; params.schema = conn.schema;
@@ -85,7 +94,8 @@ export default connections => {
username: conn.sshUser, username: conn.sshUser,
password: conn.sshPass, password: conn.sshPass,
port: conn.sshPort ? conn.sshPort : 22, port: conn.sshPort ? conn.sshPort : 22,
identity: conn.sshKey privateKey: conn.sshKey ? fs.readFileSync(conn.sshKey) : null,
passphrase: conn.sshPassphrase
}; };
} }

View File

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

View File

@@ -112,6 +112,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 }) => { ipcMain.handle('use-schema', async (event, { uid, schema }) => {
if (!schema) return; if (!schema) return;

View File

@@ -16,7 +16,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 { try {
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
const query = connections[uid] const query = connections[uid]
@@ -29,6 +29,9 @@ export default (connections) => {
if (sortParams && sortParams.field && sortParams.dir) if (sortParams && sortParams.field && sortParams.dir)
query.orderBy({ [sortParams.field]: sortParams.dir.toUpperCase() }); query.orderBy({ [sortParams.field]: sortParams.dir.toUpperCase() });
if (where)
query.where(where);
const result = await query.run({ details: true, schema }); const result = await query.run({ details: true, schema });
return { status: 'success', response: result }; return { status: 'success', response: result };
@@ -81,7 +84,7 @@ export default (connections) => {
}); });
ipcMain.handle('update-table-cell', async (event, params) => { ipcMain.handle('update-table-cell', async (event, params) => {
delete params.row._id; delete params.row._antares_id;
try { // TODO: move to client classes try { // TODO: move to client classes
let escapedParam; let escapedParam;
@@ -99,6 +102,9 @@ export default (connections) => {
case 'pg': case 'pg':
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`; escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
break; break;
case 'sqlite':
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
break;
} }
} }
else if (ARRAY.includes(params.type)) else if (ARRAY.includes(params.type))
@@ -119,6 +125,10 @@ export default (connections) => {
fileBlob = fs.readFileSync(params.content); fileBlob = fs.readFileSync(params.content);
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`; escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
break; break;
case 'sqlite':
fileBlob = fs.readFileSync(params.content);
escapedParam = `X'${fileBlob.toString('hex')}'`;
break;
} }
reload = true; reload = true;
} }
@@ -131,6 +141,9 @@ export default (connections) => {
case 'pg': case 'pg':
escapedParam = 'decode(\'\', \'hex\')'; escapedParam = 'decode(\'\', \'hex\')';
break; break;
case 'sqlite':
escapedParam = 'X\'\'';
break;
} }
} }
} }
@@ -185,7 +198,7 @@ export default (connections) => {
const fieldName = Object.keys(row)[0].includes('.') ? `${params.table}.${params.primary}` : params.primary; const fieldName = Object.keys(row)[0].includes('.') ? `${params.table}.${params.primary}` : params.primary;
return typeof row[fieldName] === 'string' return typeof row[fieldName] === 'string'
? `"${row[fieldName]}"` ? `'${row[fieldName]}'`
: row[fieldName]; : row[fieldName];
}).join(','); }).join(',');

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ export class AntaresCore {
this._params = args.params; this._params = args.params;
this._poolSize = args.poolSize || false; this._poolSize = args.poolSize || false;
this._connection = null; this._connection = null;
this._ssh = null;
this._logger = args.logger || console.log; this._logger = args.logger || console.log;
this._queryDefaults = { this._queryDefaults = {

View File

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

View File

@@ -118,17 +118,27 @@ export class MySQLClient extends AntaresCore {
if (this._params.ssl) dbConfig.ssl = { ...this._params.ssl }; if (this._params.ssl) dbConfig.ssl = { ...this._params.ssl };
if (this._params.ssh) { 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({ const tunnel = await this._ssh.addTunnel({
remoteAddr: this._params.host, remoteAddr: this._params.host,
remotePort: this._params.port remotePort: this._params.port
}); });
dbConfig.port = this._tunnel.localPort; dbConfig.port = tunnel.localPort;
}
catch (err) {
if (this._ssh) this._ssh.close();
throw err;
}
} }
if (!this._poolSize) if (!this._poolSize) {
this._connection = await mysql.createConnection(dbConfig); this._connection = await mysql.createConnection(dbConfig);
if (this._params.readonly)
await this.raw('SET SESSION TRANSACTION READ ONLY');
}
else { else {
this._connection = mysql.createPool({ this._connection = mysql.createPool({
...dbConfig, ...dbConfig,
@@ -140,6 +150,12 @@ export class MySQLClient extends AntaresCore {
return next(); return next();
} }
}); });
if (this._params.readonly) {
this._connection.on('connection', connection => {
connection.query('SET SESSION TRANSACTION READ ONLY');
});
}
} }
} }
@@ -181,6 +197,7 @@ export class MySQLClient extends AntaresCore {
const tablesArr = []; const tablesArr = [];
const triggersArr = []; const triggersArr = [];
let schemaSize = 0;
for (const db of filteredDatabases) { for (const db of filteredDatabases) {
if (!schemas.has(db.Database)) continue; if (!schemas.has(db.Database)) continue;
@@ -218,6 +235,9 @@ export class MySQLClient extends AntaresCore {
break; break;
} }
const tableSize = table.Data_length + table.Index_length;
schemaSize += tableSize;
return { return {
name: table.Name, name: table.Name,
type: tableType, type: tableType,
@@ -226,7 +246,7 @@ export class MySQLClient extends AntaresCore {
updated: table.Update_time, updated: table.Update_time,
engine: table.Engine, engine: table.Engine,
comment: table.Comment, comment: table.Comment,
size: table.Data_length + table.Index_length, size: tableSize,
autoIncrement: table.Auto_increment, autoIncrement: table.Auto_increment,
collation: table.Collation collation: table.Collation
}; };
@@ -270,7 +290,7 @@ export class MySQLClient extends AntaresCore {
body: scheduler.EVENT_BODY, body: scheduler.EVENT_BODY,
starts: scheduler.STARTS, starts: scheduler.STARTS,
ends: scheduler.ENDS, ends: scheduler.ENDS,
status: scheduler.STATUS, enabled: scheduler.STATUS === 'ENABLED',
executeAt: scheduler.EXECUTE_AT, executeAt: scheduler.EXECUTE_AT,
intervalField: scheduler.INTERVAL_FIELD, intervalField: scheduler.INTERVAL_FIELD,
intervalValue: scheduler.INTERVAL_VALUE, intervalValue: scheduler.INTERVAL_VALUE,
@@ -303,6 +323,7 @@ export class MySQLClient extends AntaresCore {
return { return {
name: db.Database, name: db.Database,
size: schemaSize,
tables: remappedTables, tables: remappedTables,
functions: remappedFunctions, functions: remappedFunctions,
procedures: remappedProcedures, procedures: remappedProcedures,
@@ -313,6 +334,7 @@ export class MySQLClient extends AntaresCore {
else { else {
return { return {
name: db.Database, name: db.Database,
size: 0,
tables: [], tables: [],
functions: [], functions: [],
procedures: [], procedures: [],
@@ -559,7 +581,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* CREATE DATABASE * CREATE DATABASE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createSchema (params) { async createSchema (params) {
@@ -569,7 +591,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER DATABASE * ALTER DATABASE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterSchema (params) { async alterSchema (params) {
@@ -579,7 +601,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP DATABASE * DROP DATABASE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropSchema (params) { async dropSchema (params) {
@@ -619,7 +641,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP VIEW * DROP VIEW
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropView (params) { async dropView (params) {
@@ -630,7 +652,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER VIEW * ALTER VIEW
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterView (params) { async alterView (params) {
@@ -651,7 +673,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* CREATE VIEW * CREATE VIEW
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createView (params) { async createView (params) {
@@ -684,7 +706,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP TRIGGER * DROP TRIGGER
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropTrigger (params) { async dropTrigger (params) {
@@ -695,7 +717,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER TRIGGER * ALTER TRIGGER
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterTrigger (params) { async alterTrigger (params) {
@@ -717,7 +739,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* CREATE TRIGGER * CREATE TRIGGER
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createTrigger (params) { async createTrigger (params) {
@@ -791,7 +813,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP PROCEDURE * DROP PROCEDURE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropRoutine (params) { async dropRoutine (params) {
@@ -802,7 +824,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER PROCEDURE * ALTER PROCEDURE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterRoutine (params) { async alterRoutine (params) {
@@ -824,7 +846,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* CREATE PROCEDURE * CREATE PROCEDURE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createRoutine (params) { async createRoutine (params) {
@@ -918,7 +940,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP FUNCTION * DROP FUNCTION
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropFunction (params) { async dropFunction (params) {
@@ -929,7 +951,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER FUNCTION * ALTER FUNCTION
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterFunction (params) { async alterFunction (params) {
@@ -951,7 +973,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* CREATE FUNCTION * CREATE FUNCTION
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createFunction (params) { async createFunction (params) {
@@ -1012,7 +1034,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP EVENT * DROP EVENT
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropEvent (params) { async dropEvent (params) {
@@ -1023,7 +1045,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER EVENT * ALTER EVENT
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterEvent (params) { async alterEvent (params) {
@@ -1049,7 +1071,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* CREATE EVENT * CREATE EVENT
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createEvent (params) { async createEvent (params) {
@@ -1066,6 +1088,16 @@ export class MySQLClient extends AntaresCore {
return await this.raw(sql, { split: false }); return await this.raw(sql, { split: false });
} }
async enableEvent ({ schema, scheduler }) {
const sql = `ALTER EVENT \`${schema}\`.\`${scheduler}\` ENABLE`;
return await this.raw(sql, { split: false });
}
async disableEvent ({ schema, scheduler }) {
const sql = `ALTER EVENT \`${schema}\`.\`${scheduler}\` DISABLE`;
return await this.raw(sql, { split: false });
}
/** /**
* SHOW COLLATION * SHOW COLLATION
* *
@@ -1176,10 +1208,14 @@ export class MySQLClient extends AntaresCore {
}); });
} }
async killProcess (id) {
return await this.raw(`KILL ${id}`);
}
/** /**
* CREATE TABLE * CREATE TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createTable (params) { async createTable (params) {
@@ -1241,7 +1277,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER TABLE * ALTER TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterTable (params) { async alterTable (params) {
@@ -1376,7 +1412,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DUPLICATE TABLE * DUPLICATE TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async duplicateTable (params) { async duplicateTable (params) {
@@ -1387,7 +1423,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* TRUNCATE TABLE * TRUNCATE TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async truncateTable (params) { async truncateTable (params) {
@@ -1398,7 +1434,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP TABLE * DROP TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropTable (params) { async dropTable (params) {

View File

@@ -9,6 +9,7 @@ function pgToString (value) {
return value.toString(); return value.toString();
} }
types.setTypeParser(20, a => parseInt(a));// bigint string to number
types.setTypeParser(1082, pgToString); // date types.setTypeParser(1082, pgToString); // date
types.setTypeParser(1083, pgToString); // time types.setTypeParser(1083, pgToString); // time
types.setTypeParser(1114, pgToString); // timestamp types.setTypeParser(1114, pgToString); // timestamp
@@ -36,6 +37,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) { _getTypeInfo (type) {
return dataTypes return dataTypes
.reduce((acc, group) => [...acc, ...group.types], []) .reduce((acc, group) => [...acc, ...group.types], [])
@@ -65,23 +86,38 @@ export class PostgreSQLClient extends AntaresCore {
if (this._params.ssl) dbConfig.ssl = { ...this._params.ssl }; if (this._params.ssl) dbConfig.ssl = { ...this._params.ssl };
if (this._params.ssh) { 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({ const tunnel = await this._ssh.addTunnel({
remoteAddr: this._params.host, remoteAddr: this._params.host,
remotePort: this._params.port remotePort: this._params.port
}); });
dbConfig.port = this._tunnel.localPort; dbConfig.port = tunnel.localPort;
}
catch (err) {
if (this._ssh) this._ssh.close();
throw err;
}
} }
if (!this._poolSize) { if (!this._poolSize) {
const client = new Client(dbConfig); const client = new Client(dbConfig);
await client.connect(); await client.connect();
this._connection = client; this._connection = client;
if (this._params.readonly)
await this.raw('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY');
} }
else { else {
const pool = new Pool({ ...dbConfig, max: this._poolSize }); const pool = new Pool({ ...dbConfig, max: this._poolSize });
this._connection = pool; this._connection = pool;
if (this._params.readonly) {
this._connection.on('connect', connection => {
connection.query('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY');
});
}
} }
} }
@@ -102,7 +138,7 @@ export class PostgreSQLClient extends AntaresCore {
use (schema) { use (schema) {
this._schema = schema; this._schema = schema;
if (schema) if (schema)
return this.raw(`SET search_path TO ${schema}`); return this.raw(`SET search_path TO "${schema}"`);
} }
/** /**
@@ -117,6 +153,7 @@ export class PostgreSQLClient extends AntaresCore {
const tablesArr = []; const tablesArr = [];
const triggersArr = []; const triggersArr = [];
let schemaSize = 0;
for (const db of databases) { for (const db of databases) {
if (!schemas.has(db.database)) continue; if (!schemas.has(db.database)) continue;
@@ -142,19 +179,20 @@ export class PostgreSQLClient extends AntaresCore {
} }
let { rows: triggers } = await this.raw(` let { rows: triggers } = await this.raw(`
SELECT event_object_schema AS table_schema, SELECT
event_object_table AS table_name, pg_class.relname AS table_name,
trigger_schema, pg_trigger.tgname AS trigger_name,
trigger_name, pg_namespace.nspname AS trigger_schema,
string_agg(event_manipulation, ',') AS event, (pg_trigger.tgenabled != 'D')::bool AS enabled
action_timing AS activation, FROM pg_trigger
action_condition AS condition, JOIN pg_class ON pg_trigger.tgrelid = pg_class.oid
action_statement AS definition JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace
FROM information_schema.triggers JOIN information_schema.triggers ON information_schema.triggers.trigger_schema = pg_namespace.nspname
AND information_schema.triggers.event_object_table = pg_class.relname
AND information_schema.triggers.trigger_name = pg_trigger.tgname
WHERE trigger_schema = '${db.database}' WHERE trigger_schema = '${db.database}'
GROUP BY 1,2,3,4,6,7,8 GROUP BY 1, 2, 3, 4
ORDER BY table_schema, ORDER BY table_name
table_name
`); `);
if (triggers.length) { if (triggers.length) {
@@ -170,11 +208,14 @@ export class PostgreSQLClient extends AntaresCore {
if (schemas.has(db.database)) { if (schemas.has(db.database)) {
// TABLES // TABLES
const remappedTables = tablesArr.filter(table => table.Db === db.database).map(table => { const remappedTables = tablesArr.filter(table => table.Db === db.database).map(table => {
const tableSize = +table.data_length + table.index_length;
schemaSize += tableSize;
return { return {
name: table.table_name, name: table.table_name,
type: table.table_type === 'VIEW' ? 'view' : 'table', type: table.table_type === 'VIEW' ? 'view' : 'table',
rows: table.reltuples, rows: table.reltuples,
size: +table.data_length + +table.index_length, size: tableSize,
collation: table.Collation, collation: table.Collation,
comment: table.comment, comment: table.comment,
engine: '' engine: ''
@@ -213,17 +254,16 @@ export class PostgreSQLClient extends AntaresCore {
return { return {
name: `${trigger.table_name}.${trigger.trigger_name}`, name: `${trigger.table_name}.${trigger.trigger_name}`,
orgName: trigger.trigger_name, orgName: trigger.trigger_name,
timing: trigger.activation,
definer: '', definer: '',
definition: trigger.definition,
event: trigger.event,
table: trigger.table_name, table: trigger.table_name,
sqlMode: '' sqlMode: '',
enabled: trigger.enabled
}; };
}); });
return { return {
name: db.database, name: db.database,
size: schemaSize,
tables: remappedTables, tables: remappedTables,
functions: remappedFunctions, functions: remappedFunctions,
procedures: remappedProcedures, procedures: remappedProcedures,
@@ -235,6 +275,7 @@ export class PostgreSQLClient extends AntaresCore {
else { else {
return { return {
name: db.database, name: db.database,
size: 0,
tables: [], tables: [],
functions: [], functions: [],
procedures: [], procedures: [],
@@ -474,7 +515,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* CREATE SCHEMA * CREATE SCHEMA
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createSchema (params) { async createSchema (params) {
@@ -484,7 +525,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* ALTER DATABASE * ALTER DATABASE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterSchema (params) { async alterSchema (params) {
@@ -494,7 +535,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* DROP DATABASE * DROP DATABASE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropSchema (params) { async dropSchema (params) {
@@ -526,26 +567,26 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* DROP VIEW * DROP VIEW
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async dropView (params) { async dropView (params) {
const sql = `DROP VIEW ${params.schema}.${params.view}`; const sql = `DROP VIEW "${params.schema}"."${params.view}"`;
return await this.raw(sql); return await this.raw(sql);
} }
/** /**
* ALTER VIEW * ALTER VIEW
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async alterView (params) { async alterView (params) {
const { view } = 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) 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); return await this.raw(sql);
} }
@@ -553,11 +594,11 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* CREATE VIEW * CREATE VIEW
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async createView (params) { async createView (params) {
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); return await this.raw(sql);
} }
@@ -571,19 +612,25 @@ export class PostgreSQLClient extends AntaresCore {
const [table, triggerName] = trigger.split('.'); const [table, triggerName] = trigger.split('.');
const results = await this.raw(` const results = await this.raw(`
SELECT event_object_schema AS table_schema, SELECT
event_object_table AS table_name, information_schema.triggers.event_object_schema AS table_schema,
trigger_schema, information_schema.triggers.event_object_table AS table_name,
trigger_name, information_schema.triggers.trigger_schema,
string_agg(event_manipulation, ',') AS event, information_schema.triggers.trigger_name,
string_agg(event_manipulation, ',') AS EVENT,
action_timing AS activation, action_timing AS activation,
action_condition AS condition, action_condition AS condition,
action_statement AS definition action_statement AS definition,
FROM information_schema.triggers (pg_trigger.tgenabled != 'D')::bool AS enabled
FROM pg_trigger
JOIN pg_class ON pg_trigger.tgrelid = pg_class.oid
JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace
JOIN information_schema.triggers ON pg_namespace.nspname = information_schema.triggers.trigger_schema
AND pg_class.relname = information_schema.triggers.event_object_table
WHERE trigger_schema = '${schema}' WHERE trigger_schema = '${schema}'
AND trigger_name = '${triggerName}' AND trigger_name = '${triggerName}'
AND event_object_table = '${table}' AND event_object_table = '${table}'
GROUP BY 1,2,3,4,6,7,8 GROUP BY 1,2,3,4,6,7,8,9
ORDER BY table_schema, ORDER BY table_schema,
table_name table_name
`); `);
@@ -593,7 +640,7 @@ export class PostgreSQLClient extends AntaresCore {
sql: row.definition, sql: row.definition,
name: row.trigger_name, name: row.trigger_name,
table: row.table_name, table: row.table_name,
event: row.event.split(','), event: [...new Set(row.event.split(','))],
activation: row.activation activation: row.activation
}; };
})[0]; })[0];
@@ -602,7 +649,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* DROP TRIGGER * DROP TRIGGER
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async dropTrigger (params) { async dropTrigger (params) {
@@ -614,7 +661,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* ALTER TRIGGER * ALTER TRIGGER
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async alterTrigger (params) { async alterTrigger (params) {
@@ -636,7 +683,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* CREATE TRIGGER * CREATE TRIGGER
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async createTrigger (params) { async createTrigger (params) {
@@ -645,6 +692,18 @@ export class PostgreSQLClient extends AntaresCore {
return await this.raw(sql, { split: false }); return await this.raw(sql, { split: false });
} }
async enableTrigger ({ schema, trigger }) {
const [table, triggerName] = trigger.split('.');
const sql = `ALTER TABLE "${schema}"."${table}" ENABLE TRIGGER "${triggerName}"`;
return await this.raw(sql, { split: false });
}
async disableTrigger ({ schema, trigger }) {
const [table, triggerName] = trigger.split('.');
const sql = `ALTER TABLE "${schema}"."${table}" DISABLE TRIGGER "${triggerName}"`;
return await this.raw(sql, { split: false });
}
/** /**
* SHOW CREATE PROCEDURE * SHOW CREATE PROCEDURE
* *
@@ -1029,10 +1088,14 @@ export class PostgreSQLClient extends AntaresCore {
}); });
} }
async killProcess (id) {
return await this.raw(`SELECT pg_terminate_backend(${id})`);
}
/** /**
* CREATE TABLE * CREATE TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async createTable (params) { async createTable (params) {
@@ -1055,7 +1118,7 @@ export class PostgreSQLClient extends AntaresCore {
const typeInfo = this._getTypeInfo(field.type); const typeInfo = this._getTypeInfo(field.type);
const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false; const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false;
newColumns.push(`${field.name} newColumns.push(`"${field.name}"
${field.type.toUpperCase()}${length ? `(${length})` : ''} ${field.type.toUpperCase()}${length ? `(${length})` : ''}
${field.unsigned ? 'UNSIGNED' : ''} ${field.unsigned ? 'UNSIGNED' : ''}
${field.zerofill ? 'ZEROFILL' : ''} ${field.zerofill ? 'ZEROFILL' : ''}
@@ -1072,14 +1135,14 @@ export class PostgreSQLClient extends AntaresCore {
if (type === 'PRIMARY') if (type === 'PRIMARY')
newIndexes.push(`PRIMARY KEY (${fields})`); newIndexes.push(`PRIMARY KEY (${fields})`);
else if (type === 'UNIQUE') else if (type === 'UNIQUE')
newIndexes.push(`CONSTRAINT ${index.name} UNIQUE (${fields})`); newIndexes.push(`CONSTRAINT "${index.name}" UNIQUE (${fields})`);
else else
manageIndexes.push(`CREATE INDEX ${index.name} ON "${schema}"."${options.name}" (${fields})`); manageIndexes.push(`CREATE INDEX "${index.name}" ON "${schema}"."${options.name}" (${fields})`);
}); });
// ADD FOREIGN KEYS // ADD FOREIGN KEYS
foreigns.forEach(foreign => { 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}`); 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(', ')})`; sql = `${sql} (${[...newColumns, ...newIndexes, ...newForeigns].join(', ')})`;
@@ -1090,7 +1153,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* ALTER TABLE * ALTER TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async alterTable (params) { async alterTable (params) {
@@ -1119,7 +1182,7 @@ export class PostgreSQLClient extends AntaresCore {
const typeInfo = this._getTypeInfo(addition.type); const typeInfo = this._getTypeInfo(addition.type);
const length = typeInfo.length ? addition.numLength || addition.charLength || addition.datePrecision : false; const length = typeInfo.length ? addition.numLength || addition.charLength || addition.datePrecision : false;
alterColumns.push(`ADD COLUMN ${addition.name} alterColumns.push(`ADD COLUMN "${addition.name}"
${addition.type.toUpperCase()}${length ? `(${length})` : ''}${addition.isArray ? '[]' : ''} ${addition.type.toUpperCase()}${length ? `(${length})` : ''}${addition.isArray ? '[]' : ''}
${addition.unsigned ? 'UNSIGNED' : ''} ${addition.unsigned ? 'UNSIGNED' : ''}
${addition.zerofill ? 'ZEROFILL' : ''} ${addition.zerofill ? 'ZEROFILL' : ''}
@@ -1130,20 +1193,20 @@ export class PostgreSQLClient extends AntaresCore {
// ADD INDEX // ADD INDEX
indexChanges.additions.forEach(addition => { indexChanges.additions.forEach(addition => {
const fields = addition.fields.map(field => `${field}`).join(','); const fields = addition.fields.map(field => `"${field}"`).join(',');
const type = addition.type; const type = addition.type;
if (type === 'PRIMARY') if (type === 'PRIMARY')
alterColumns.push(`ADD PRIMARY KEY (${fields})`); alterColumns.push(`ADD PRIMARY KEY (${fields})`);
else if (type === 'UNIQUE') else if (type === 'UNIQUE')
alterColumns.push(`ADD CONSTRAINT ${addition.name} UNIQUE (${fields})`); alterColumns.push(`ADD CONSTRAINT "${addition.name}" UNIQUE (${fields})`);
else else
manageIndexes.push(`CREATE INDEX ${addition.name} ON "${schema}"."${table}" (${fields})`); manageIndexes.push(`CREATE INDEX "${addition.name}" ON "${schema}"."${table}" (${fields})`);
}); });
// ADD FOREIGN KEYS // ADD FOREIGN KEYS
foreignChanges.additions.forEach(addition => { foreignChanges.additions.forEach(addition => {
alterColumns.push(`ADD CONSTRAINT ${addition.constraintName} FOREIGN KEY (${addition.field}) REFERENCES "${schema}"."${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 // CHANGE FIELDS
@@ -1169,6 +1232,7 @@ export class PostgreSQLClient extends AntaresCore {
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.isArray ? '[]' : ''} USING "${change.name}"::${localType}`);
alterColumns.push(`ALTER COLUMN "${change.name}" ${change.nullable ? 'DROP NOT NULL' : 'SET NOT NULL'}`); alterColumns.push(`ALTER COLUMN "${change.name}" ${change.nullable ? 'DROP NOT NULL' : 'SET NOT NULL'}`);
alterColumns.push(`ALTER COLUMN "${change.name}" ${change.default ? `SET DEFAULT ${change.default}` : 'DROP DEFAULT'}`); alterColumns.push(`ALTER COLUMN "${change.name}" ${change.default ? `SET DEFAULT ${change.default}` : 'DROP DEFAULT'}`);
if (['SERIAL', 'SMALLSERIAL', 'BIGSERIAL'].includes(change.type)) { if (['SERIAL', 'SMALLSERIAL', 'BIGSERIAL'].includes(change.type)) {
const sequenceName = `${table}_${change.name}_seq`.replace(' ', '_'); const sequenceName = `${table}_${change.name}_seq`.replace(' ', '_');
createSequences.push(`CREATE SEQUENCE IF NOT EXISTS ${sequenceName} OWNED BY "${table}"."${change.name}"`); createSequences.push(`CREATE SEQUENCE IF NOT EXISTS ${sequenceName} OWNED BY "${table}"."${change.name}"`);
@@ -1186,39 +1250,39 @@ export class PostgreSQLClient extends AntaresCore {
else else
manageIndexes.push(`DROP INDEX ${change.oldName}`); 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; const type = change.type;
if (type === 'PRIMARY') if (type === 'PRIMARY')
alterColumns.push(`ADD PRIMARY KEY (${fields})`); alterColumns.push(`ADD PRIMARY KEY (${fields})`);
else if (type === 'UNIQUE') else if (type === 'UNIQUE')
alterColumns.push(`ADD CONSTRAINT ${change.name} UNIQUE (${fields})`); alterColumns.push(`ADD CONSTRAINT "${change.name}" UNIQUE (${fields})`);
else else
manageIndexes.push(`CREATE INDEX ${change.name} ON "${schema}"."${table}" (${fields})`); manageIndexes.push(`CREATE INDEX "${change.name}" ON "${schema}"."${table}" (${fields})`);
}); });
// CHANGE FOREIGN KEYS // CHANGE FOREIGN KEYS
foreignChanges.changes.forEach(change => { foreignChanges.changes.forEach(change => {
alterColumns.push(`DROP CONSTRAINT ${change.oldName}`); 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}`); 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 // DROP FIELDS
deletions.forEach(deletion => { deletions.forEach(deletion => {
alterColumns.push(`DROP COLUMN ${deletion.name}`); alterColumns.push(`DROP COLUMN "${deletion.name}"`);
}); });
// DROP INDEX // DROP INDEX
indexChanges.deletions.forEach(deletion => { indexChanges.deletions.forEach(deletion => {
if (['PRIMARY', 'UNIQUE'].includes(deletion.type)) if (['PRIMARY', 'UNIQUE'].includes(deletion.type))
alterColumns.push(`DROP CONSTRAINT ${deletion.name}`); alterColumns.push(`DROP CONSTRAINT "${deletion.name}"`);
else else
manageIndexes.push(`DROP INDEX ${deletion.name}`); manageIndexes.push(`DROP INDEX "${deletion.name}"`);
}); });
// DROP FOREIGN KEYS // DROP FOREIGN KEYS
foreignChanges.deletions.forEach(deletion => { 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(', ')}; `; if (alterColumns.length) sql += `ALTER TABLE "${schema}"."${table}" ${alterColumns.join(', ')}; `;
@@ -1235,33 +1299,33 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* DUPLICATE TABLE * DUPLICATE TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async duplicateTable (params) { async duplicateTable (params) {
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); return await this.raw(sql);
} }
/** /**
* TRUNCATE TABLE * TRUNCATE TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async truncateTable (params) { async truncateTable (params) {
const sql = `TRUNCATE TABLE ${params.schema}.${params.table}`; const sql = `TRUNCATE TABLE "${params.schema}"."${params.table}"`;
return await this.raw(sql); return await this.raw(sql);
} }
/** /**
* DROP TABLE * DROP TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async dropTable (params) { async dropTable (params) {
const sql = `DROP TABLE ${params.schema}.${params.table}`; const sql = `DROP TABLE "${params.schema}"."${params.table}"`;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -1285,7 +1349,7 @@ export class PostgreSQLClient extends AntaresCore {
else if (Object.keys(this._query.insert).length) else if (Object.keys(this._query.insert).length)
fromRaw = 'INTO'; 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 // WHERE
const whereArray = this._query.where.reduce(this._reducer, []); const whereArray = this._query.where.reduce(this._reducer, []);

View File

@@ -0,0 +1,806 @@
'use strict';
import sqlite from 'better-sqlite3';
import { AntaresCore } from '../AntaresCore';
import dataTypes from 'common/data-types/mysql';
import { NUMBER, FLOAT, TIME, DATETIME } from 'common/fieldTypes';
export class SQLiteClient extends AntaresCore {
constructor (args) {
super(args);
this._schema = null;
}
_getTypeInfo (type) {
return dataTypes
.reduce((acc, group) => [...acc, ...group.types], [])
.filter(_type => _type.name === type.toUpperCase())[0];
}
/**
* @memberof SQLiteClient
*/
async connect () {
this._connection = sqlite(this._params.databasePath, {
fileMustExist: true,
readonly: this._params.readonly
});
}
/**
* @memberof SQLiteClient
*/
destroy () {}
/**
* Executes an USE query
*
* @memberof SQLiteClient
*/
use () {}
/**
* @param {Array} schemas list
* @returns {Array.<Object>} databases scructure
* @memberof SQLiteClient
*/
async getStructure (schemas) {
const { rows: databases } = await this.raw('SELECT * FROM pragma_database_list');
const filteredDatabases = databases;
const tablesArr = [];
const triggersArr = [];
let schemaSize = 0;
for (const db of filteredDatabases) {
if (!schemas.has(db.name)) continue;
let { rows: tables } = await this.raw(`
SELECT *
FROM "${db.name}".sqlite_master
WHERE type IN ('table', 'view')
AND name NOT LIKE 'sqlite_%'
ORDER BY name
`);
if (tables.length) {
tables = tables.map(table => {
table.Db = db.name;
return table;
});
tablesArr.push(...tables);
}
let { rows: triggers } = await this.raw(`SELECT * FROM "${db.name}".sqlite_master WHERE type='trigger'`);
if (triggers.length) {
triggers = triggers.map(trigger => {
trigger.Db = db.name;
return trigger;
});
triggersArr.push(...triggers);
}
}
return filteredDatabases.map(db => {
if (schemas.has(db.name)) {
// TABLES
const remappedTables = tablesArr.filter(table => table.Db === db.name).map(table => {
const tableSize = 0;
schemaSize += tableSize;
return {
name: table.name,
type: table.type,
rows: false,
size: false
};
});
// TRIGGERS
const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.name).map(trigger => {
return {
name: trigger.name,
table: trigger.tbl_name
};
});
return {
name: db.name,
size: schemaSize,
tables: remappedTables,
functions: [],
procedures: [],
triggers: remappedTriggers,
schedulers: []
};
}
else {
return {
name: db.name,
size: 0,
tables: [],
functions: [],
procedures: [],
triggers: [],
schedulers: []
};
}
});
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table scructure
* @memberof SQLiteClient
*/
async getTableColumns ({ schema, table }) {
const { rows: fields } = await this.raw(`SELECT * FROM "${schema}".pragma_table_info('${table}')`);
return fields.map(field => {
const [type, length] = field.type.includes('(')
? field.type.replace(')', '').split('(').map(el => {
if (!isNaN(el)) el = +el;
return el;
})
: [field.type, null];
return {
name: field.name,
key: null,
type: type.trim(),
schema: schema,
table: table,
numPrecision: [...NUMBER, ...FLOAT].includes(type) ? length : null,
datePrecision: null,
charLength: ![...NUMBER, ...FLOAT].includes(type) ? length : null,
nullable: !field.notnull,
unsigned: null,
zerofill: null,
order: field.cid + 1,
default: field.dflt_value,
charset: null,
collation: null,
autoIncrement: false,
onUpdate: null,
comment: ''
};
});
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table row count
* @memberof SQLiteClient
*/
async getTableApproximateCount ({ schema, table }) {
const { rows } = await this.raw(`SELECT COUNT(*) AS count FROM "${schema}"."${table}"`);
return rows.length ? rows[0].count : 0;
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table options
* @memberof SQLiteClient
*/
async getTableOptions ({ schema, table }) {
return { name: table };
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table indexes
* @memberof SQLiteClient
*/
async getTableIndexes ({ schema, table }) {
const remappedIndexes = [];
const { rows: primaryKeys } = await this.raw(`SELECT * FROM "${schema}".pragma_table_info('${table}') WHERE pk != 0`);
for (const key of primaryKeys) {
remappedIndexes.push({
name: 'PRIMARY',
column: key.name,
indexType: null,
type: 'PRIMARY',
cardinality: null,
comment: '',
indexComment: ''
});
}
const { rows: indexes } = await this.raw(`SELECT * FROM "${schema}".pragma_index_list('${table}');`);
for (const index of indexes) {
const { rows: details } = await this.raw(`SELECT * FROM "${schema}".pragma_index_info('${index.name}');`);
for (const detail of details) {
remappedIndexes.push({
name: index.name,
column: detail.name,
indexType: null,
type: index.unique === 1 ? 'UNIQUE' : 'INDEX',
cardinality: null,
comment: '',
indexComment: ''
});
}
}
return remappedIndexes;
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table key usage
* @memberof SQLiteClient
*/
async getKeyUsage ({ schema, table }) {
const { rows } = await this.raw(`SELECT * FROM "${schema}".pragma_foreign_key_list('${table}');`);
return rows.map(field => {
return {
schema: schema,
table: table,
field: field.from,
position: field.id + 1,
constraintPosition: null,
constraintName: field.id,
refSchema: schema,
refTable: field.table,
refField: field.to,
onUpdate: field.on_update,
onDelete: field.on_delete
};
});
}
async getUsers () {}
/**
* SHOW CREATE VIEW
*
* @returns {Array.<Object>} view informations
* @memberof SQLiteClient
*/
async getViewInformations ({ schema, view }) {
const sql = `SELECT "sql" FROM "${schema}".sqlite_master WHERE "type"='view' AND name='${view}'`;
const results = await this.raw(sql);
return results.rows.map(row => {
return {
sql: row.sql.match(/(?<=AS ).*?$/gs)[0],
name: view
};
})[0];
}
/**
* DROP VIEW
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async dropView (params) {
const sql = `DROP VIEW "${params.schema}"."${params.view}"`;
return await this.raw(sql);
}
/**
* ALTER VIEW
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async alterView (params) {
const { view } = params;
try {
await this.dropView({ schema: view.schema, view: view.oldName });
await this.createView(view);
}
catch (err) {
return Promise.reject(err);
}
}
/**
* CREATE VIEW
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async createView (params) {
const sql = `CREATE VIEW "${params.schema}"."${params.name}" AS ${params.sql}`;
return await this.raw(sql);
}
/**
* SHOW CREATE TRIGGER
*
* @returns {Array.<Object>} view informations
* @memberof SQLiteClient
*/
async getTriggerInformations ({ schema, trigger }) {
const sql = `SELECT "sql" FROM "${schema}".sqlite_master WHERE "type"='trigger' AND name='${trigger}'`;
const results = await this.raw(sql);
return results.rows.map(row => {
return {
sql: row.sql.match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
name: trigger,
table: row.sql.match(/(?<=ON `).*?(?=`)/gs)[0],
activation: row.sql.match(/(BEFORE|AFTER)/gs)[0],
event: row.sql.match(/(INSERT|UPDATE|DELETE)/gs)[0]
};
})[0];
}
/**
* DROP TRIGGER
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async dropTrigger (params) {
const sql = `DROP TRIGGER \`${params.schema}\`.\`${params.trigger}\``;
return await this.raw(sql);
}
/**
* ALTER TRIGGER
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async alterTrigger (params) {
const { trigger } = params;
const tempTrigger = Object.assign({}, trigger);
tempTrigger.name = `Antares_${tempTrigger.name}_tmp`;
try {
await this.createTrigger(tempTrigger);
await this.dropTrigger({ schema: trigger.schema, trigger: tempTrigger.name });
await this.dropTrigger({ schema: trigger.schema, trigger: trigger.oldName });
await this.createTrigger(trigger);
}
catch (err) {
return Promise.reject(err);
}
}
/**
* CREATE TRIGGER
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async createTrigger (params) {
const sql = `CREATE ${params.definer ? `DEFINER=${params.definer} ` : ''}TRIGGER \`${params.schema}\`.\`${params.name}\` ${params.activation} ${params.event} ON \`${params.table}\` FOR EACH ROW ${params.sql}`;
return await this.raw(sql, { split: false });
}
/**
* SHOW COLLATION
*
* @returns {Array.<Object>} collations list
* @memberof SQLiteClient
*/
async getCollations () {
return [];
}
/**
* SHOW VARIABLES
*
* @returns {Array.<Object>} variables list
* @memberof SQLiteClient
*/
async getVariables () {
return [];
}
/**
* SHOW ENGINES
*
* @returns {Array.<Object>} engines list
* @memberof SQLiteClient
*/
async getEngines () {
return {
name: 'SQLite',
support: 'YES',
comment: '',
isDefault: true
};
}
/**
* SHOW VARIABLES LIKE '%vers%'
*
* @returns {Array.<Object>} version parameters
* @memberof SQLiteClient
*/
async getVersion () {
const os = require('os');
const sql = 'SELECT sqlite_version() AS version';
const { rows } = await this.raw(sql);
return {
number: rows[0].version,
name: 'SQLite',
arch: process.arch,
os: `${os.type()} ${os.release()}`
};
}
async getProcesses () {}
async killProcess () {}
/**
* 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,
...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];
const 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[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;
}
}

162
src/main/main.js Normal file
View File

@@ -0,0 +1,162 @@
'use strict';
import { app, BrowserWindow, /* session, */ nativeImage, Menu } from 'electron';
import * as path from 'path';
import Store from 'electron-store';
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;
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 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'
});
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', () => {
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 (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 () => {
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);
}

View File

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

View File

@@ -17,7 +17,7 @@
<form class="form-horizontal"> <form class="form-horizontal">
<div <div
v-for="(parameter, i) in inParameters" v-for="(parameter, i) in inParameters"
:key="parameter._id" :key="parameter._antares_id"
class="form-group" class="form-group"
> >
<div class="col-4"> <div class="col-4">

View 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('<', '&lt;').replaceAll('>', '&gt;');
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>

View File

@@ -13,7 +13,7 @@
</div> </div>
<div class="modal-body pb-0"> <div class="modal-body pb-0">
<div class="content"> <div class="content">
<form class="form-horizontal"> <form class="form-horizontal" @submit.prevent="createSchema">
<div class="form-group"> <div class="form-group">
<div class="col-3"> <div class="col-3">
<label class="form-label">{{ $t('word.name') }}</label> <label class="form-label">{{ $t('word.name') }}</label>

View File

@@ -1,5 +1,15 @@
<template> <template>
<div class="modal active"> <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" /> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0 pb-4"> <div class="modal-container p-0 pb-4">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
@@ -12,34 +22,54 @@
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div> </div>
<div class="processes-toolbar py-2 px-4"> <div class="processes-toolbar py-2 px-4">
<div class="dropdown"> <div class="workspace-query-buttons">
<div class="btn-group"> <div class="dropdown pr-1">
<button <div class="btn-group">
class="btn btn-dark btn-sm mr-0 pr-1 d-flex" <button
:class="{'loading':isQuering}" class="btn btn-dark btn-sm mr-0 pr-1 d-flex"
title="F5" :class="{'loading':isQuering}"
@click="getProcessesList" :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" />
<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"
> >
<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> </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>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div v-if="sortedResults.length"> <div v-if="sortedResults.length">
@@ -85,9 +115,10 @@
<template slot-scope="{ items }"> <template slot-scope="{ items }">
<ModalProcessesListRow <ModalProcessesListRow
v-for="row in items" v-for="row in items"
:key="row._id" :key="row.id"
class="process-row" class="process-row"
:row="row" :row="row"
@select-row="selectRow(row.id)"
@contextmenu="contextMenu" @contextmenu="contextMenu"
@stop-refresh="stopRefresh" @stop-refresh="stopRefresh"
/> />
@@ -102,15 +133,18 @@
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import arrayToFile from '../libs/arrayToFile';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import BaseVirtualScroll from '@/components/BaseVirtualScroll'; import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import ModalProcessesListRow from '@/components/ModalProcessesListRow'; import ModalProcessesListRow from '@/components/ModalProcessesListRow';
import ModalProcessesListContext from '@/components/ModalProcessesListContext';
export default { export default {
name: 'ModalProcessesList', name: 'ModalProcessesList',
components: { components: {
BaseVirtualScroll, BaseVirtualScroll,
ModalProcessesListRow ModalProcessesListRow,
ModalProcessesListContext
}, },
props: { props: {
connection: Object connection: Object
@@ -119,8 +153,12 @@ export default {
return { return {
resultsSize: 1000, resultsSize: 1000,
isQuering: false, isQuering: false,
isContext: false,
autorefreshTimer: 0, autorefreshTimer: 0,
refreshInterval: null, refreshInterval: null,
contextEvent: null,
selectedCell: null,
selectedRow: null,
results: [], results: [],
fields: [], fields: [],
currentSort: '', currentSort: '',
@@ -245,10 +283,55 @@ export default {
this.autorefreshTimer = 0; this.autorefreshTimer = 0;
this.clearRefresh(); 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 () { closeModal () {
this.$emit('close'); this.$emit('close');
}, },
downloadTable (format) {
if (!this.sortedResults) return;
arrayToFile({
type: format,
content: this.sortedResults,
filename: 'processes'
});
},
onKey (e) { onKey (e) {
e.stopPropagation(); e.stopPropagation();
if (e.key === 'Escape') if (e.key === 'Escape')

View 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>

View File

@@ -1,21 +1,18 @@
<template> <template>
<div class="tr" @click="selectRow($event, row._id)"> <div class="tr" @click="selectRow()">
<div <div
v-for="(col, cKey) in row" v-for="(col, cKey) in row"
v-show="cKey !== '_id'"
:key="cKey" :key="cKey"
class="td p-0" class="td p-0"
tabindex="0" tabindex="0"
@contextmenu.prevent="openContext($event, { id: row._id, field: cKey })" @contextmenu.prevent="openContext($event, { id: row.id, field: cKey })"
> >
<template v-if="cKey !== '_id'"> <span
<span v-if="!isInlineEditor[cKey]"
v-if="!isInlineEditor[cKey]" class="cell-content"
class="cell-content" :class="`${isNull(col)} type-${typeof col === 'number' ? 'int' : 'varchar'}`"
:class="`${isNull(col)} type-${typeof col === 'number' ? 'int' : 'varchar'}`" @dblclick="dblClick(cKey)"
@dblclick="dblClick(cKey)" >{{ col | cutText }}</span>
>{{ col | cutText }}</span>
</template>
</div> </div>
<ConfirmModal <ConfirmModal
v-if="isInfoModal" v-if="isInfoModal"
@@ -73,25 +70,15 @@ export default {
}; };
}, },
computed: {}, computed: {},
watch: {
fields () {
Object.keys(this.fields).forEach(field => {
this.isInlineEditor[field.name] = false;
});
}
},
methods: { methods: {
isNull (value) { isNull (value) {
return value === null ? ' is-null' : ''; return value === null ? ' is-null' : '';
}, },
selectRow (event, row) { selectRow () {
this.$emit('select-row', event, row); this.$emit('select-row');
}, },
openContext (event, payload) { openContext (event, payload) {
if (this.isEditable) { this.$emit('contextmenu', event, payload);
payload.field = this.fields[payload.field].name;// Ensures field name only
this.$emit('contextmenu', event, payload);
}
}, },
hideInfoModal () { hideInfoModal () {
this.isInfoModal = false; this.isInfoModal = false;

View File

@@ -184,7 +184,7 @@
:class="{'selected': applicationTheme === 'dark'}" :class="{'selected': applicationTheme === 'dark'}"
@click="changeApplicationTheme('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"> <div class="theme-name text-light">
<i class="mdi mdi-moon-waning-crescent mdi-48px" /> <i class="mdi mdi-moon-waning-crescent mdi-48px" />
<div class="h6 mt-4"> <div class="h6 mt-4">
@@ -197,7 +197,7 @@
:class="{'selected': applicationTheme === 'light'}" :class="{'selected': applicationTheme === 'light'}"
@click="changeApplicationTheme('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"> <div class="theme-name text-dark">
<i class="mdi mdi-white-balance-sunny mdi-48px" /> <i class="mdi mdi-white-balance-sunny mdi-48px" />
<div class="h6 mt-4"> <div class="h6 mt-4">
@@ -280,7 +280,7 @@
<div v-show="selectedTab === 'about'" class="panel-body py-4"> <div v-show="selectedTab === 'about'" class="panel-body py-4">
<div class="text-center"> <div class="text-center">
<img :src="require('@/images/logo.svg').default" width="128"> <img src="../images/logo.svg" width="128">
<h4>{{ appName }}</h4> <h4>{{ appName }}</h4>
<p> <p>
{{ $t('word.version') }} {{ appVersion }}<br> {{ $t('word.version') }} {{ appVersion }}<br>
@@ -396,7 +396,7 @@ export default {
return locales; return locales;
}, },
hasUpdates () { hasUpdates () {
return ['available', 'downloading', 'downloaded'].includes(this.updateStatus); return ['available', 'downloading', 'downloaded', 'link'].includes(this.updateStatus);
}, },
workspace () { workspace () {
return this.getWorkspace(this.selectedWorkspace); return this.getWorkspace(this.selectedWorkspace);

View File

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

View File

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

View File

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

View File

@@ -92,7 +92,7 @@ export default {
} }
}, },
hasUpdates () { hasUpdates () {
return ['available', 'downloading', 'downloaded'].includes(this.updateStatus); return ['available', 'downloading', 'downloaded', 'link'].includes(this.updateStatus);
} }
}, },
methods: { methods: {

View File

@@ -1,8 +1,12 @@
<template> <template>
<div id="titlebar"> <div id="titlebar" @dblclick="toggleFullScreen">
<div class="titlebar-resizer" /> <div class="titlebar-resizer" />
<div class="titlebar-elements"> <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>
<div class="titlebar-elements titlebar-title"> <div class="titlebar-elements titlebar-title">
{{ windowTitle }} {{ windowTitle }}
@@ -22,14 +26,26 @@
> >
<i class="mdi mdi-24px mdi-refresh" /> <i class="mdi mdi-24px mdi-refresh" />
</div> </div>
<div class="titlebar-element" @click="minimizeApp"> <div
v-if="!isMacOS"
class="titlebar-element"
@click="minimizeApp"
>
<i class="mdi mdi-24px mdi-minus" /> <i class="mdi mdi-24px mdi-minus" />
</div> </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-if="isMaximized" class="mdi mdi-24px mdi-fullscreen-exit" />
<i v-else class="mdi mdi-24px mdi-fullscreen" /> <i v-else class="mdi mdi-24px mdi-fullscreen" />
</div> </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" /> <i class="mdi mdi-24px mdi-close" />
</div> </div>
</div> </div>
@@ -47,7 +63,8 @@ export default {
return { return {
w: getCurrentWindow(), w: getCurrentWindow(),
isMaximized: getCurrentWindow().isMaximized(), isMaximized: getCurrentWindow().isMaximized(),
isDevelopment: process.env.NODE_ENV === 'development' isDevelopment: process.env.NODE_ENV === 'development',
isMacOS: process.platform === 'darwin'
}; };
}, },
computed: { computed: {

View File

@@ -256,7 +256,11 @@
</span> </span>
</a> </a>
</li> </li>
<li slot="header" class="tab-item dropdown tools-dropdown"> <li
v-if="workspace.customizations.processesList"
slot="header"
class="tab-item dropdown tools-dropdown"
>
<a <a
class="tab-link workspace-tools-link dropdown-toggle" class="tab-link workspace-tools-link dropdown-toggle"
tabindex="0" tabindex="0"
@@ -264,8 +268,8 @@
> >
<i class="mdi mdi-24px mdi-tools" /> <i class="mdi mdi-24px mdi-tools" />
</a> </a>
<ul class="menu text-left text-uppercase"> <ul v-if="hasTools" class="menu text-left text-uppercase">
<li v-if="workspace.customizations.processesList" class="menu-item"> <li class="menu-item">
<a class="c-hand p-vcentered" @click="showProcessesModal"> <a class="c-hand p-vcentered" @click="showProcessesModal">
<i class="mdi mdi-memory mr-1 tool-icon" /> <i class="mdi mdi-memory mr-1 tool-icon" />
<span>{{ $t('message.processesList') }}</span> <span>{{ $t('message.processesList') }}</span>
@@ -569,6 +573,11 @@ export default {
if (this.workspace.breadcrumbs[key]) return this.workspace.breadcrumbs[key]; if (this.workspace.breadcrumbs[key]) return this.workspace.breadcrumbs[key];
} }
return false; return false;
},
hasTools () {
return this.workspace.customizations.processesList ||
this.workspace.customizations.usersManagement ||
this.workspace.customizations.variables;
} }
}, },
watch: { watch: {
@@ -586,11 +595,15 @@ export default {
} }
}, },
async created () { async created () {
window.addEventListener('keydown', this.onKey);
await this.addWorkspace(this.connection.uid); await this.addWorkspace(this.connection.uid);
const isInitiated = await Connection.checkConnection(this.connection.uid); const isInitiated = await Connection.checkConnection(this.connection.uid);
if (isInitiated) if (isInitiated)
this.connectWorkspace(this.connection); this.connectWorkspace(this.connection);
}, },
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: { methods: {
...mapActions({ ...mapActions({
addWorkspace: 'workspaces/addWorkspace', addWorkspace: 'workspaces/addWorkspace',
@@ -604,6 +617,25 @@ export default {
addQueryTab () { addQueryTab () {
this.newTab({ uid: this.connection.uid, type: 'query' }); 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) { openAsPermanentTab (tab) {
const permanentTabs = { const permanentTabs = {
table: 'data', table: 'data',

View File

@@ -11,6 +11,7 @@
<a class="tab-link">{{ $t('word.general') }}</a> <a class="tab-link">{{ $t('word.general') }}</a>
</li> </li>
<li <li
v-if="customizations.sslConnection"
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'ssl'}" :class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')" @click="selectTab('ssl')"
@@ -18,6 +19,7 @@
<a class="tab-link">{{ $t('word.ssl') }}</a> <a class="tab-link">{{ $t('word.ssl') }}</a>
</li> </li>
<li <li
v-if="customizations.sshConnection"
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'ssh'}" :class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')" @click="selectTab('ssh')"
@@ -49,25 +51,17 @@
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<select v-model="connection.client" class="form-select"> <select v-model="connection.client" class="form-select">
<option value="mysql"> <option
MySQL v-for="client in clients"
:key="client.slug"
:value="client.slug"
>
{{ client.name }}
</option> </option>
<option value="maria">
MariaDB
</option>
<option value="pg">
PostgreSQL
</option>
<!-- <option value="mssql">
Microsoft SQL
</option>
<option value="oracledb">
Oracle DB
</option> -->
</select> </select>
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label> <label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div> </div>
@@ -79,7 +73,20 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="connection.databasePath"
:message="$t('word.browse')"
@clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')"
/>
</div>
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label> <label class="form-label">{{ $t('word.port') }}</label>
</div> </div>
@@ -105,7 +112,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label> <label class="form-label">{{ $t('word.user') }}</label>
</div> </div>
@@ -118,7 +125,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label> <label class="form-label">{{ $t('word.password') }}</label>
</div> </div>
@@ -144,7 +151,15 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="customizations.readOnlyMode" class="form-group columns">
<div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="connection.readonly" type="checkbox"><i class="form-icon" /> {{ $t('message.readOnlyMode') }}
</label>
</div>
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12" /> <div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
@@ -309,6 +324,18 @@
/> />
</div> </div>
</div> </div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $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> </fieldset>
</form> </form>
</div> </div>
@@ -357,15 +384,23 @@ export default {
}, },
data () { data () {
return { return {
clients: [
{ name: 'MySQL', slug: 'mysql' },
{ name: 'MariaDB', slug: 'maria' },
{ name: 'PostgreSQL', slug: 'pg' },
{ name: 'SQLite', slug: 'sqlite' }
],
connection: { connection: {
name: '', name: '',
client: 'mysql', client: 'mysql',
host: '127.0.0.1', host: '127.0.0.1',
database: null, database: null,
databasePath: '',
port: null, port: null,
user: null, user: null,
password: '', password: '',
ask: false, ask: false,
readonly: false,
uid: uidGen('C'), uid: uidGen('C'),
ssl: false, ssl: false,
cert: '', cert: '',
@@ -393,6 +428,13 @@ export default {
return this.isConnecting || this.isTesting; 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 () { created () {
this.setDefaults(); this.setDefaults();
@@ -432,7 +474,7 @@ export default {
try { try {
const res = await Connection.makeTest(this.connection); const res = await Connection.makeTest(this.connection);
if (res.status === 'error') if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message }); this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') }); this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
} }
@@ -455,7 +497,7 @@ export default {
else { else {
const res = await Connection.makeTest(params); const res = await Connection.makeTest(params);
if (res.status === 'error') if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message }); this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') }); this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
} }

View File

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

View File

@@ -1,7 +1,16 @@
<template> <template>
<div class="column col-12 empty"> <div class="column col-12 empty">
<div class="empty-icon"> <div class="empty-icon">
<img :src="require(`@/images/logo-${applicationTheme}.svg`).default" width="200"> <img
v-if="applicationTheme === 'dark'"
src="../images/logo-dark.svg"
width="200"
>
<img
v-if="applicationTheme === 'light'"
src="../images/logo-light.svg"
width="200"
>
</div> </div>
<p class="h6 empty-subtitle"> <p class="h6 empty-subtitle">
{{ $t('message.noOpenTabs') }} {{ $t('message.noOpenTabs') }}

View File

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

View File

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

View File

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

View File

@@ -65,7 +65,11 @@
> >
<span class="d-flex"><i class="mdi mdi-18px mdi-database-edit text-light pr-1" /> {{ $t('word.edit') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-database-edit text-light pr-1" /> {{ $t('word.edit') }}</span>
</div> </div>
<div class="context-element" @click="showDeleteModal"> <div
v-if="workspace.customizations.schemaDrop"
class="context-element"
@click="showDeleteModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-remove text-light pr-1" /> {{ $t('word.delete') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-database-remove text-light pr-1" /> {{ $t('word.delete') }}</span>
</div> </div>

View File

@@ -344,7 +344,7 @@ export default {
}, },
addField () { addField () {
this.localFields.push({ this.localFields.push({
_id: uidGen(), _antares_id: uidGen(),
name: `${this.$tc('word.field', 1)}_${++this.newFieldsCounter}`, name: `${this.$tc('word.field', 1)}_${++this.newFieldsCounter}`,
key: '', key: '',
type: this.workspace.dataTypes[0].types[0].name, type: this.workspace.dataTypes[0].types[0].name,
@@ -385,8 +385,8 @@ export default {
}); });
}, },
duplicateField (uid) { duplicateField (uid) {
const fieldToClone = Object.assign({}, this.localFields.find(field => field._id === uid)); const fieldToClone = Object.assign({}, this.localFields.find(field => field._antares_id === uid));
fieldToClone._id = uidGen(); fieldToClone._antares_id = uidGen();
fieldToClone.name = `${fieldToClone.name}_copy`; fieldToClone.name = `${fieldToClone.name}_copy`;
fieldToClone.order = this.localFields.length + 1; fieldToClone.order = this.localFields.length + 1;
this.localFields = [...this.localFields, fieldToClone]; this.localFields = [...this.localFields, fieldToClone];
@@ -397,11 +397,11 @@ export default {
}, 20); }, 20);
}, },
removeField (uid) { removeField (uid) {
this.localFields = this.localFields.filter(field => field._id !== uid); this.localFields = this.localFields.filter(field => field._antares_id !== uid);
}, },
addNewIndex (payload) { addNewIndex (payload) {
this.localIndexes = [...this.localIndexes, { this.localIndexes = [...this.localIndexes, {
_id: uidGen(), _antares_id: uidGen(),
name: payload.index === 'PRIMARY' ? 'PRIMARY' : payload.field, name: payload.index === 'PRIMARY' ? 'PRIMARY' : payload.field,
fields: [payload.field], fields: [payload.field],
type: payload.index, type: payload.index,
@@ -413,7 +413,7 @@ export default {
}, },
addToIndex (payload) { addToIndex (payload) {
this.localIndexes = this.localIndexes.map(index => { this.localIndexes = this.localIndexes.map(index => {
if (index._id === payload.index) index.fields.push(payload.field); if (index._antares_id === payload.index) index.fields.push(payload.field);
return index; return index;
}); });
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,10 +36,10 @@
<div ref="indexesPanel" class="panel-body p-0 pr-1"> <div ref="indexesPanel" class="panel-body p-0 pr-1">
<div <div
v-for="foreign in foreignProxy" v-for="foreign in foreignProxy"
:key="foreign._id" :key="foreign._antares_id"
class="tile tile-centered c-hand mb-1 p-1" class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-element': selectedForeignID === foreign._id}" :class="{'selected-element': selectedForeignID === foreign._antares_id}"
@click="selectForeign($event, foreign._id)" @click="selectForeign($event, foreign._antares_id)"
> >
<div class="tile-icon"> <div class="tile-icon">
<div> <div>
@@ -68,7 +68,7 @@
<button <button
class="btn btn-link remove-field p-0 mr-2" class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')" :title="$t('word.delete')"
@click.prevent="removeIndex(foreign._id)" @click.prevent="removeIndex(foreign._antares_id)"
> >
<i class="mdi mdi-close" /> <i class="mdi mdi-close" />
</button> </button>
@@ -238,7 +238,7 @@ export default {
}, },
computed: { computed: {
selectedForeignObj () { selectedForeignObj () {
return this.foreignProxy.find(foreign => foreign._id === this.selectedForeignID); return this.foreignProxy.find(foreign => foreign._antares_id === this.selectedForeignID);
}, },
isChanged () { isChanged () {
return JSON.stringify(this.localKeyUsage) !== JSON.stringify(this.foreignProxy); return JSON.stringify(this.localKeyUsage) !== JSON.stringify(this.foreignProxy);
@@ -288,7 +288,7 @@ export default {
}, },
addForeign () { addForeign () {
this.foreignProxy = [...this.foreignProxy, { this.foreignProxy = [...this.foreignProxy, {
_id: uidGen(), _antares_id: uidGen(),
constraintName: `FK_${this.foreignProxy.length + 1}`, constraintName: `FK_${this.foreignProxy.length + 1}`,
refSchema: this.schema, refSchema: this.schema,
table: this.table, table: this.table,
@@ -307,19 +307,19 @@ export default {
}, 20); }, 20);
}, },
removeIndex (id) { removeIndex (id) {
this.foreignProxy = this.foreignProxy.filter(foreign => foreign._id !== id); this.foreignProxy = this.foreignProxy.filter(foreign => foreign._antares_id !== id);
if (this.selectedForeignID === id && this.foreignProxy.length) if (this.selectedForeignID === id && this.foreignProxy.length)
this.resetSelectedID(); this.resetSelectedID();
}, },
clearChanges () { clearChanges () {
this.foreignProxy = JSON.parse(JSON.stringify(this.localKeyUsage)); this.foreignProxy = JSON.parse(JSON.stringify(this.localKeyUsage));
if (!this.foreignProxy.some(foreign => foreign._id === this.selectedForeignID)) if (!this.foreignProxy.some(foreign => foreign._antares_id === this.selectedForeignID))
this.resetSelectedID(); this.resetSelectedID();
}, },
toggleField (field) { toggleField (field) {
this.foreignProxy = this.foreignProxy.map(foreign => { this.foreignProxy = this.foreignProxy.map(foreign => {
if (foreign._id === this.selectedForeignID) if (foreign._antares_id === this.selectedForeignID)
foreign.field = field; foreign.field = field;
return foreign; return foreign;
@@ -327,14 +327,14 @@ export default {
}, },
toggleRefField (field) { toggleRefField (field) {
this.foreignProxy = this.foreignProxy.map(foreign => { this.foreignProxy = this.foreignProxy.map(foreign => {
if (foreign._id === this.selectedForeignID) if (foreign._antares_id === this.selectedForeignID)
foreign.refField = field; foreign.refField = field;
return foreign; return foreign;
}); });
}, },
resetSelectedID () { resetSelectedID () {
this.selectedForeignID = this.foreignProxy.length ? this.foreignProxy[0]._id : ''; this.selectedForeignID = this.foreignProxy.length ? this.foreignProxy[0]._antares_id : '';
}, },
async getRefFields () { async getRefFields () {
if (!this.selectedForeignObj.refTable) return; if (!this.selectedForeignObj.refTable) return;

View File

@@ -36,10 +36,10 @@
<div ref="indexesPanel" class="panel-body p-0 pr-1"> <div ref="indexesPanel" class="panel-body p-0 pr-1">
<div <div
v-for="index in indexesProxy" v-for="index in indexesProxy"
:key="index._id" :key="index._antares_id"
class="tile tile-centered c-hand mb-1 p-1" class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-element': selectedIndexID === index._id}" :class="{'selected-element': selectedIndexID === index._antares_id}"
@click="selectIndex($event, index._id)" @click="selectIndex($event, index._antares_id)"
> >
<div class="tile-icon"> <div class="tile-icon">
<div> <div>
@@ -56,7 +56,7 @@
<button <button
class="btn btn-link remove-field p-0 mr-2" class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')" :title="$t('word.delete')"
@click.prevent="removeIndex(index._id)" @click.prevent="removeIndex(index._antares_id)"
> >
<i class="mdi mdi-close" /> <i class="mdi mdi-close" />
</button> </button>
@@ -163,7 +163,7 @@ export default {
}, },
computed: { computed: {
selectedIndexObj () { selectedIndexObj () {
return this.indexesProxy.find(index => index._id === this.selectedIndexID); return this.indexesProxy.find(index => index._antares_id === this.selectedIndexID);
}, },
isChanged () { isChanged () {
return JSON.stringify(this.localIndexes) !== JSON.stringify(this.indexesProxy); return JSON.stringify(this.localIndexes) !== JSON.stringify(this.indexesProxy);
@@ -200,7 +200,7 @@ export default {
}, },
addIndex () { addIndex () {
this.indexesProxy = [...this.indexesProxy, { this.indexesProxy = [...this.indexesProxy, {
_id: uidGen(), _antares_id: uidGen(),
name: 'NEW_INDEX', name: 'NEW_INDEX',
fields: [], fields: [],
type: 'INDEX', type: 'INDEX',
@@ -218,19 +218,19 @@ export default {
}, 20); }, 20);
}, },
removeIndex (id) { removeIndex (id) {
this.indexesProxy = this.indexesProxy.filter(index => index._id !== id); this.indexesProxy = this.indexesProxy.filter(index => index._antares_id !== id);
if (this.selectedIndexID === id && this.indexesProxy.length) if (this.selectedIndexID === id && this.indexesProxy.length)
this.resetSelectedID(); this.resetSelectedID();
}, },
clearChanges () { clearChanges () {
this.indexesProxy = JSON.parse(JSON.stringify(this.localIndexes)); this.indexesProxy = JSON.parse(JSON.stringify(this.localIndexes));
if (!this.indexesProxy.some(index => index._id === this.selectedIndexID)) if (!this.indexesProxy.some(index => index._antares_id === this.selectedIndexID))
this.resetSelectedID(); this.resetSelectedID();
}, },
toggleField (field) { toggleField (field) {
this.indexesProxy = this.indexesProxy.map(index => { this.indexesProxy = this.indexesProxy.map(index => {
if (index._id === this.selectedIndexID) { if (index._antares_id === this.selectedIndexID) {
if (index.fields.includes(field)) if (index.fields.includes(field))
index.fields = index.fields.filter(f => f !== field); index.fields = index.fields.filter(f => f !== field);
else else
@@ -240,7 +240,7 @@ export default {
}); });
}, },
resetSelectedID () { resetSelectedID () {
this.selectedIndexID = this.indexesProxy.length ? this.indexesProxy[0]._id : ''; this.selectedIndexID = this.indexesProxy.length ? this.indexesProxy[0]._antares_id : '';
} }
} }
}; };

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="tr" @contextmenu.prevent="$emit('contextmenu', $event, localRow._id)"> <div class="tr" @contextmenu.prevent="$emit('contextmenu', $event, localRow._antares_id)">
<div class="td p-0" tabindex="0"> <div class="td p-0" tabindex="0">
<div :class="customizations.sortableFields ? 'row-draggable' : 'text-center'"> <div :class="customizations.sortableFields ? 'row-draggable' : 'text-center'">
<i v-if="customizations.sortableFields" class="mdi mdi-drag-horizontal row-draggable-icon" /> <i v-if="customizations.sortableFields" class="mdi mdi-drag-horizontal row-draggable-icon" />
@@ -367,7 +367,8 @@ export default {
getWorkspace: 'workspaces/getWorkspace' getWorkspace: 'workspaces/getWorkspace'
}), }),
localLength () { localLength () {
return this.localRow.numLength || this.localRow.charLength || this.localRow.datePrecision || this.localRow.numPrecision || 0; const localLength = this.localRow.numLength || this.localRow.charLength || this.localRow.datePrecision || this.localRow.numPrecision || 0;
return localLength === true ? null : localLength;
}, },
fieldType () { fieldType () {
const fieldType = this.dataTypes.reduce((acc, group) => [...acc, ...group.types], []).filter(type => const fieldType = this.dataTypes.reduce((acc, group) => [...acc, ...group.types], []).filter(type =>
@@ -391,7 +392,7 @@ export default {
return this.indexes.some(index => ['PRIMARY', 'UNIQUE'].includes(index.type)); return this.indexes.some(index => ['PRIMARY', 'UNIQUE'].includes(index.type));
}, },
isNullable () { isNullable () {
return !this.indexes.some(index => ['PRIMARY'].includes(index.type)); return this.customizations.nullablePrimary || !this.indexes.some(index => ['PRIMARY'].includes(index.type));
}, },
isInDataTypes () { isInDataTypes () {
let typeNames = []; let typeNames = [];

View File

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

View File

@@ -4,8 +4,9 @@
class="workspace-query-tab column col-12 columns col-gapless no-outline p-0" class="workspace-query-tab column col-12 columns col-gapless no-outline p-0"
tabindex="0" tabindex="0"
@keydown.116="runQuery(query)" @keydown.116="runQuery(query)"
@keydown.ctrl.87="clear" @keydown.ctrl.alt.87="clear"
@keydown.ctrl.119="beautify" @keydown.ctrl.66="beautify"
@keydown.ctrl.71="openHistoryModal"
> >
<div class="workspace-query-runner column col-12"> <div class="workspace-query-runner column col-12">
<QueryEditor <QueryEditor
@@ -32,16 +33,7 @@
<span>{{ $t('word.run') }}</span> <span>{{ $t('word.run') }}</span>
</button> </button>
<button <button
class="btn btn-dark btn-sm" class="btn btn-link btn-sm mr-0"
:disabled="!query || isQuering"
title="CTRL+F8"
@click="beautify()"
>
<i class="mdi mdi-24px mdi-brush pr-1" />
<span>{{ $t('word.format') }}</span>
</button>
<button
class="btn btn-link btn-sm"
:disabled="!query || isQuering" :disabled="!query || isQuering"
title="CTRL+W" title="CTRL+W"
@click="clear()" @click="clear()"
@@ -52,6 +44,24 @@
<div class="divider-vert py-3" /> <div class="divider-vert py-3" />
<button
class="btn btn-dark btn-sm"
:disabled="!query || isQuering"
title="CTRL+B"
@click="beautify()"
>
<i class="mdi mdi-24px mdi-brush pr-1" />
<span>{{ $t('word.format') }}</span>
</button>
<button
class="btn btn-dark btn-sm"
:disabled="isQuering"
title="CTRL+G"
@click="openHistoryModal()"
>
<i class="mdi mdi-24px mdi-history pr-1" />
<span>{{ $t('word.history') }}</span>
</button>
<div class="dropdown table-dropdown pr-2"> <div class="dropdown table-dropdown pr-2">
<button <button
:disabled="!results.length || isQuering" :disabled="!results.length || isQuering"
@@ -83,7 +93,7 @@
<div v-if="resultsCount"> <div v-if="resultsCount">
{{ $t('word.results') }}: <b>{{ resultsCount.toLocaleString() }}</b> {{ $t('word.results') }}: <b>{{ resultsCount.toLocaleString() }}</b>
</div> </div>
<div v-if="affectedCount"> <div v-if="affectedCount !== null">
{{ $t('message.affectedRows') }}: <b>{{ affectedCount }}</b> {{ $t('message.affectedRows') }}: <b>{{ affectedCount }}</b>
</div> </div>
<div class="input-group" :title="$t('word.schema')"> <div class="input-group" :title="$t('word.schema')">
@@ -116,17 +126,24 @@
@delete-selected="deleteSelected" @delete-selected="deleteSelected"
/> />
</div> </div>
<ModalHistory
v-if="isHistoryOpen"
:connection="connection"
@select-query="selectQuery"
@close="isHistoryOpen = false"
/>
</div> </div>
</template> </template>
<script> <script>
import { format } from 'sql-formatter'; import { format } from 'sql-formatter';
import { mapGetters, mapActions } from 'vuex';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader';
import WorkspaceTabQueryTable from '@/components/WorkspaceTabQueryTable'; import WorkspaceTabQueryTable from '@/components/WorkspaceTabQueryTable';
import WorkspaceTabQueryEmptyState from '@/components/WorkspaceTabQueryEmptyState'; import WorkspaceTabQueryEmptyState from '@/components/WorkspaceTabQueryEmptyState';
import { mapGetters, mapActions } from 'vuex'; import ModalHistory from '@/components/ModalHistory';
import tableTabs from '@/mixins/tableTabs'; import tableTabs from '@/mixins/tableTabs';
export default { export default {
@@ -135,7 +152,8 @@ export default {
BaseLoader, BaseLoader,
QueryEditor, QueryEditor,
WorkspaceTabQueryTable, WorkspaceTabQueryTable,
WorkspaceTabQueryEmptyState WorkspaceTabQueryEmptyState,
ModalHistory
}, },
mixins: [tableTabs], mixins: [tableTabs],
props: { props: {
@@ -152,14 +170,16 @@ export default {
selectedSchema: null, selectedSchema: null,
resultsCount: 0, resultsCount: 0,
durationsCount: 0, durationsCount: 0,
affectedCount: 0, affectedCount: null,
editorHeight: 200 editorHeight: 200,
isHistoryOpen: false
}; };
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
getWorkspace: 'workspaces/getWorkspace', getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected' selectedWorkspace: 'workspaces/getSelected',
getHistoryByWorkspace: 'history/getHistoryByWorkspace'
}), }),
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
@@ -175,6 +195,9 @@ export default {
}, },
isWorkspaceSelected () { isWorkspaceSelected () {
return this.workspace.uid === this.selectedWorkspace; return this.workspace.uid === this.selectedWorkspace;
},
history () {
return this.getHistoryByWorkspace(this.connection.uid) || [];
} }
}, },
watch: { watch: {
@@ -189,7 +212,6 @@ export default {
created () { created () {
this.query = this.tab.content; this.query = this.tab.content;
this.selectedSchema = this.tab.schema || this.breadcrumbsSchema; this.selectedSchema = this.tab.schema || this.breadcrumbsSchema;
// this.changeBreadcrumbs({ schema: this.selectedSchema, query: `Query #${this.tab.index}` });
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
}, },
@@ -213,7 +235,8 @@ export default {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification', addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs', changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
updateTabContent: 'workspaces/updateTabContent' updateTabContent: 'workspaces/updateTabContent',
saveHistory: 'history/saveHistory'
}), }),
async runQuery (query) { async runQuery (query) {
if (!query || this.isQuering) return; if (!query || this.isQuering) return;
@@ -232,11 +255,23 @@ export default {
if (status === 'success') { if (status === 'success') {
this.results = Array.isArray(response) ? response : [response]; this.results = Array.isArray(response) ? response : [response];
this.resultsCount += this.results.reduce((acc, curr) => acc + (curr.rows ? curr.rows.length : 0), 0); this.resultsCount = this.results.reduce((acc, curr) => acc + (curr.rows ? curr.rows.length : 0), 0);
this.durationsCount += this.results.reduce((acc, curr) => acc + curr.duration, 0); this.durationsCount = this.results.reduce((acc, curr) => acc + curr.duration, 0);
this.affectedCount += this.results.reduce((acc, curr) => acc + (curr.report ? curr.report.affectedRows : 0), 0); this.affectedCount = this.results
.filter(result => result.report !== null)
.reduce((acc, curr) => {
if (acc === null) acc = 0;
return acc + (curr.report ? curr.report.affectedRows : 0);
}, null);
this.updateTabContent({ uid: this.connection.uid, tab: this.tab.uid, type: 'query', schema: this.selectedSchema, content: query }); this.updateTabContent({
uid: this.connection.uid,
tab: this.tab.uid,
type: 'query',
schema: this.selectedSchema,
content: query
});
this.saveHistory(params);
} }
else else
this.addNotification({ status: 'error', message: response }); this.addNotification({ status: 'error', message: response });
@@ -255,7 +290,7 @@ export default {
this.results = []; this.results = [];
this.resultsCount = 0; this.resultsCount = 0;
this.durationsCount = 0; this.durationsCount = 0;
this.affectedCount = 0; this.affectedCount = null;
}, },
resize (e) { resize (e) {
const el = this.$refs.queryEditor.$el; const el = this.$refs.queryEditor.$el;
@@ -295,6 +330,15 @@ export default {
this.$refs.queryEditor.editor.session.setValue(formattedQuery); this.$refs.queryEditor.editor.session.setValue(formattedQuery);
} }
}, },
openHistoryModal () {
this.isHistoryOpen = true;
},
selectQuery (sql) {
if (this.$refs.queryEditor)
this.$refs.queryEditor.editor.session.setValue(sql);
this.isHistoryOpen = false;
},
clear () { clear () {
if (this.$refs.queryEditor) if (this.$refs.queryEditor)
this.$refs.queryEditor.editor.session.setValue(''); this.$refs.queryEditor.editor.session.setValue('');

View File

@@ -11,13 +11,31 @@
<div class="mb-4"> <div class="mb-4">
{{ $t('word.clear') }} {{ $t('word.clear') }}
</div> </div>
<div class="mb-4">
{{ $t('word.history') }}
</div>
<div class="mb-4">
{{ $t('message.openNewTab') }}
</div>
<div class="mb-4">
{{ $t('message.closeTab') }}
</div>
</div> </div>
<div class="column col-16"> <div class="column col-16">
<div class="mb-4"> <div class="mb-4">
<code>F5</code> <code>F5</code>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<code>CTRL</code> + <code>F8</code> <code>CTRL</code> + <code>B</code>
</div>
<div class="mb-4">
<code>CTRL</code> + <code>ALT</code> + <code>W</code>
</div>
<div class="mb-4">
<code>CTRL</code> + <code>G</code>
</div>
<div class="mb-4">
<code>CTRL</code> + <code>T</code>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<code>CTRL</code> + <code>W</code> <code>CTRL</code> + <code>W</code>

View File

@@ -5,6 +5,8 @@
tabindex="0" tabindex="0"
:style="{'height': resultsSize+'px'}" :style="{'height': resultsSize+'px'}"
@keyup.46="showDeleteConfirmModal" @keyup.46="showDeleteConfirmModal"
@keydown.ctrl.65="selectAllRows"
@keydown.esc="deselectRows"
> >
<TableContext <TableContext
v-if="isContext" v-if="isContext"
@@ -60,7 +62,7 @@
v-if="resultsWithRows[resultsetIndex] && resultsWithRows[resultsetIndex].rows" v-if="resultsWithRows[resultsetIndex] && resultsWithRows[resultsetIndex].rows"
ref="resultTable" ref="resultTable"
:items="sortedResults" :items="sortedResults"
:item-height="22" :item-height="23"
class="tbody" class="tbody"
:visible-height="resultsSize" :visible-height="resultsSize"
:scroll-element="scrollElement" :scroll-element="scrollElement"
@@ -68,13 +70,13 @@
<template slot-scope="{ items }"> <template slot-scope="{ items }">
<WorkspaceTabQueryTableRow <WorkspaceTabQueryTableRow
v-for="row in items" v-for="row in items"
:key="row._id" :key="row._antares_id"
:row="row" :row="row"
:fields="fieldsObj" :fields="fieldsObj"
:key-usage="keyUsage" :key-usage="keyUsage"
:element-type="elementType" :element-type="elementType"
:class="{'selected': selectedRows.includes(row._id)}" :class="{'selected': selectedRows.includes(row._antares_id)}"
@select-row="selectRow($event, row._id)" @select-row="selectRow($event, row._antares_id)"
@update-field="updateField($event, row)" @update-field="updateField($event, row)"
@contextmenu="contextMenu" @contextmenu="contextMenu"
/> />
@@ -194,7 +196,7 @@ export default {
if (this.sortedResults.length) { if (this.sortedResults.length) {
const fieldsObj = {}; const fieldsObj = {};
for (const key in this.sortedResults[0]) { for (const key in this.sortedResults[0]) {
if (key === '_id') continue; if (key === '_antares_id') continue;
const fieldObj = this.fields.find(field => { const fieldObj = this.fields.find(field => {
let fieldNames = [ let fieldNames = [
@@ -308,7 +310,7 @@ export default {
setLocalResults () { setLocalResults () {
this.localResults = this.resultsWithRows[this.resultsetIndex] && this.resultsWithRows[this.resultsetIndex].rows this.localResults = this.resultsWithRows[this.resultsetIndex] && this.resultsWithRows[this.resultsetIndex].rows
? this.resultsWithRows[this.resultsetIndex].rows.map(item => { ? this.resultsWithRows[this.resultsetIndex].rows.map(item => {
return { ...item, _id: uidGen() }; return { ...item, _antares_id: uidGen() };
}) })
: []; : [];
}, },
@@ -328,7 +330,7 @@ export default {
this.resizeResults(); this.resizeResults();
}, },
updateField (payload, row) { updateField (payload, row) {
const orgRow = this.localResults.find(lr => lr._id === row._id); const orgRow = this.localResults.find(lr => lr._antares_id === row._antares_id);
Object.keys(orgRow).forEach(key => { // remap the row Object.keys(orgRow).forEach(key => { // remap the row
if (orgRow[key] instanceof Date && moment(orgRow[key]).isValid()) { // if datetime if (orgRow[key] instanceof Date && moment(orgRow[key]).isValid()) { // if datetime
@@ -365,8 +367,8 @@ export default {
}, },
deleteSelected () { deleteSelected () {
this.closeContext(); this.closeContext();
const rows = this.localResults.filter(row => this.selectedRows.includes(row._id)).map(row => { const rows = JSON.parse(JSON.stringify(this.localResults)).filter(row => this.selectedRows.includes(row._antares_id)).map(row => {
delete row._id; delete row._antares_id;
return row; return row;
}); });
@@ -379,7 +381,7 @@ export default {
this.$emit('delete-selected', params); this.$emit('delete-selected', params);
}, },
setNull () { setNull () {
const row = this.localResults.find(row => this.selectedRows.includes(row._id)); const row = this.localResults.find(row => this.selectedRows.includes(row._antares_id));
const params = { const params = {
primary: this.primaryField.name, primary: this.primaryField.name,
@@ -394,7 +396,7 @@ export default {
this.$emit('update-field', params); this.$emit('update-field', params);
}, },
copyCell () { copyCell () {
const row = this.localResults.find(row => this.selectedRows.includes(row._id)); const row = this.localResults.find(row => this.selectedRows.includes(row._antares_id));
const cellName = Object.keys(row).find(prop => [ const cellName = Object.keys(row).find(prop => [
this.selectedCell.field, this.selectedCell.field,
`${this.fields[0].table}.${this.selectedCell.field}`, `${this.fields[0].table}.${this.selectedCell.field}`,
@@ -404,9 +406,9 @@ export default {
navigator.clipboard.writeText(valueToCopy); navigator.clipboard.writeText(valueToCopy);
}, },
copyRow () { copyRow () {
const row = this.localResults.find(row => this.selectedRows.includes(row._id)); const row = this.localResults.find(row => this.selectedRows.includes(row._antares_id));
const rowToCopy = JSON.parse(JSON.stringify(row)); const rowToCopy = JSON.parse(JSON.stringify(row));
delete rowToCopy._id; delete rowToCopy._antares_id;
navigator.clipboard.writeText(JSON.stringify(rowToCopy)); navigator.clipboard.writeText(JSON.stringify(rowToCopy));
}, },
applyUpdate (params) { applyUpdate (params) {
@@ -433,21 +435,30 @@ export default {
this.selectedRows.push(row); this.selectedRows.push(row);
else { else {
const lastID = this.selectedRows.slice(-1)[0]; const lastID = this.selectedRows.slice(-1)[0];
const lastIndex = this.sortedResults.findIndex(el => el._id === lastID); const lastIndex = this.sortedResults.findIndex(el => el._antares_id === lastID);
const clickedIndex = this.sortedResults.findIndex(el => el._id === row); const clickedIndex = this.sortedResults.findIndex(el => el._antares_id === row);
if (lastIndex > clickedIndex) { if (lastIndex > clickedIndex) {
for (let i = clickedIndex; i < lastIndex; i++) for (let i = clickedIndex; i < lastIndex; i++)
this.selectedRows.push(this.sortedResults[i]._id); this.selectedRows.push(this.sortedResults[i]._antares_id);
} }
else if (lastIndex < clickedIndex) { else if (lastIndex < clickedIndex) {
for (let i = clickedIndex; i > lastIndex; i--) for (let i = clickedIndex; i > lastIndex; i--)
this.selectedRows.push(this.sortedResults[i]._id); this.selectedRows.push(this.sortedResults[i]._antares_id);
} }
} }
} }
else else
this.selectedRows = [row]; this.selectedRows = [row];
}, },
selectAllRows () {
this.selectedRows = this.localResults.reduce((acc, curr) => {
acc.push(curr._antares_id);
return acc;
}, []);
},
deselectRows () {
this.selectedRows = [];
},
contextMenu (event, cell) { contextMenu (event, cell) {
if (event.target.localName === 'input') return; if (event.target.localName === 'input') return;
@@ -490,7 +501,7 @@ export default {
if (!this.sortedResults) return; if (!this.sortedResults) return;
const rows = JSON.parse(JSON.stringify(this.sortedResults)).map(row => { const rows = JSON.parse(JSON.stringify(this.sortedResults)).map(row => {
delete row._id; delete row._antares_id;
return row; return row;
}); });

View File

@@ -1,14 +1,14 @@
<template> <template>
<div class="tr" @click="selectRow($event, row._id)"> <div class="tr" @click="selectRow($event, row._antares_id)">
<div <div
v-for="(col, cKey) in row" v-for="(col, cKey) in row"
v-show="cKey !== '_id'" v-show="cKey !== '_antares_id'"
:key="cKey" :key="cKey"
class="td p-0" class="td p-0"
tabindex="0" tabindex="0"
@contextmenu.prevent="openContext($event, { id: row._id, field: cKey })" @contextmenu.prevent="openContext($event, { id: row._antares_id, field: cKey })"
> >
<template v-if="cKey !== '_id'"> <template v-if="cKey !== '_antares_id'">
<span <span
v-if="!isInlineEditor[cKey] && fields[cKey]" v-if="!isInlineEditor[cKey] && fields[cKey]"
class="cell-content" class="cell-content"

View File

@@ -8,12 +8,11 @@
<button <button
class="btn btn-dark btn-sm mr-0 pr-1" class="btn btn-dark btn-sm mr-0 pr-1"
:class="{'loading':isQuering}" :class="{'loading':isQuering}"
title="F5" :title="`${$t('word.refresh')} (F5)`"
@click="reloadTable" @click="reloadTable"
> >
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh mr-1" /> <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" /> <i v-else class="mdi mdi-24px mdi-history mdi-flip-h mr-1" />
<span>{{ $t('word.refresh') }}</span>
</button> </button>
<div class="btn btn-dark btn-sm dropdown-toggle pl-0 pr-0" tabindex="0"> <div class="btn btn-dark btn-sm dropdown-toggle pl-0 pr-0" tabindex="0">
<i class="mdi mdi-24px mdi-menu-down" /> <i class="mdi mdi-24px mdi-menu-down" />
@@ -71,6 +70,14 @@
<div class="divider-vert py-3" /> <div class="divider-vert py-3" />
<button
class="btn btn-sm"
:title="`${$t('word.filter')} (CTRL+F)`"
:class="{'btn-primary': isSearch, 'btn-dark': !isSearch}"
@click="isSearch = !isSearch"
>
<i class="mdi mdi-24px mdi-magnify" />
</button>
<button <button
v-if="isTable" v-if="isTable"
class="btn btn-dark btn-sm" class="btn btn-dark btn-sm"
@@ -113,7 +120,12 @@
{{ $t('word.results') }}: <b>{{ results[0].rows.length | localeString }}</b> {{ $t('word.results') }}: <b>{{ results[0].rows.length | localeString }}</b>
</div> </div>
<div v-if="hasApproximately || (page > 1 && approximateCount)"> <div v-if="hasApproximately || (page > 1 && approximateCount)">
{{ $t('word.total') }}: <b :title="$t('word.approximately')"> {{ approximateCount | localeString }}</b> {{ $t('word.total') }}: <b
:title="!customizations.tableRealCount ? $t('word.approximately') : ''"
>
<span v-if="!customizations.tableRealCount"></span>
{{ approximateCount | localeString }}
</b>
</div> </div>
<div class="d-flex" :title="$t('word.schema')"> <div class="d-flex" :title="$t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b> <i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
@@ -121,6 +133,13 @@
</div> </div>
</div> </div>
</div> </div>
<WorkspaceTabTableFilters
v-if="isSearch"
:fields="fields"
:conn-client="connection.client"
@filter="updateFilters"
@filter-change="onFilterChange"
/>
<div class="workspace-query-results p-relative column col-12"> <div class="workspace-query-results p-relative column col-12">
<BaseLoader v-if="isQuering" /> <BaseLoader v-if="isQuering" />
<WorkspaceTabQueryTable <WorkspaceTabQueryTable
@@ -160,6 +179,7 @@
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader';
import WorkspaceTabQueryTable from '@/components/WorkspaceTabQueryTable'; import WorkspaceTabQueryTable from '@/components/WorkspaceTabQueryTable';
import WorkspaceTabTableFilters from '@/components/WorkspaceTabTableFilters';
import ModalNewTableRow from '@/components/ModalNewTableRow'; import ModalNewTableRow from '@/components/ModalNewTableRow';
import ModalFakerRows from '@/components/ModalFakerRows'; import ModalFakerRows from '@/components/ModalFakerRows';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
@@ -170,6 +190,7 @@ export default {
components: { components: {
BaseLoader, BaseLoader,
WorkspaceTabQueryTable, WorkspaceTabQueryTable,
WorkspaceTabTableFilters,
ModalNewTableRow, ModalNewTableRow,
ModalFakerRows ModalFakerRows
}, },
@@ -192,6 +213,7 @@ export default {
tabUid: 'data', // ??? tabUid: 'data', // ???
isQuering: false, isQuering: false,
isPageMenu: false, isPageMenu: false,
isSearch: false,
results: [], results: [],
lastTable: null, lastTable: null,
isAddModal: false, isAddModal: false,
@@ -199,6 +221,7 @@ export default {
autorefreshTimer: 0, autorefreshTimer: 0,
refreshInterval: null, refreshInterval: null,
sortParams: {}, sortParams: {},
filters: [],
page: 1, page: 1,
pageProxy: 1, pageProxy: 1,
approximateCount: 0 approximateCount: 0
@@ -213,6 +236,9 @@ export default {
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
}, },
customizations () {
return this.workspace.customizations;
},
isTable () { isTable () {
return !!this.workspace.breadcrumbs.table; return !!this.workspace.breadcrumbs.table;
}, },
@@ -271,6 +297,13 @@ export default {
if (this.lastTable !== this.table) if (this.lastTable !== this.table)
this.getTableData(); this.getTableData();
} }
},
isSearch (val) {
if (this.filters.length > 0 && !val) {
this.filters = [];
this.getTableData();
}
this.resizeScroller();
} }
}, },
created () { created () {
@@ -287,7 +320,7 @@ export default {
changeBreadcrumbs: 'workspaces/changeBreadcrumbs' changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}), }),
async getTableData () { async getTableData () {
if (!this.table) return; if (!this.table || !this.isSelected) return;
this.isQuering = true; this.isQuering = true;
// if table changes clear cached values // if table changes clear cached values
@@ -302,7 +335,8 @@ export default {
table: this.table, table: this.table,
limit: this.limit, limit: this.limit,
page: this.page, page: this.page,
sortParams: this.sortParams sortParams: this.sortParams,
where: this.filters || []
}; };
try { // Table data try { // Table data
@@ -389,11 +423,13 @@ export default {
if (e.key === 'F5') if (e.key === 'F5')
this.reloadTable(); this.reloadTable();
if (e.ctrlKey) { if (e.ctrlKey || e.metaKey) {
if (e.key === 'ArrowRight') if (e.key === 'ArrowRight')
this.pageChange('next'); this.pageChange('next');
if (e.key === 'ArrowLeft') if (e.key === 'ArrowLeft')
this.pageChange('prev'); this.pageChange('prev');
if (e.keyCode === 70) // f
this.isSearch = !this.isSearch;
} }
} }
}, },
@@ -410,6 +446,18 @@ export default {
}, },
downloadTable (format) { downloadTable (format) {
this.$refs.queryTable.downloadTable(format, this.table); this.$refs.queryTable.downloadTable(format, this.table);
},
onFilterChange (clausoles) {
this.resizeScroller();
if (clausoles.length === 0)
this.isSearch = false;
},
resizeScroller () {
setTimeout(() => this.$refs.queryTable.refreshScroller(), 1);
},
updateFilters (clausoles) {
this.filters = clausoles;
this.getTableData();
} }
} }
}; };

View File

@@ -0,0 +1,175 @@
<template>
<form class="workspace-table-filters" @submit.prevent="doFilter">
<div
v-for="(row, index) of rows"
:key="index"
class="workspace-table-filters-row"
>
<label class="form-checkbox my-0">
<input
v-model="row.active"
type="checkbox"
@change="doFilter"
><i class="form-icon" />
</label>
<select v-model="row.field" class="form-select col-auto select-sm">
<option
v-for="(item, j) of fields"
:key="j"
:value="item.name"
>
{{ item.name }}
</option>
</select>
<select v-model="row.op" class="form-select ml-2 col-auto select-sm">
<option
v-for="(operator, k) of operators"
:key="k"
:value="operator"
>
{{ operator }}
</option>
</select>
<div class="workspace-table-filters-row-value ml-2">
<input
v-if="!row.op.includes('NULL')"
v-model="row.value"
type="text"
class="form-input input-sm"
>
<input
v-if="row.op === 'BETWEEN'"
v-model="row.value2"
type="text"
class="form-input ml-2 input-sm"
>
</div>
<button
class="btn btn-sm btn-dark mr-0 ml-2"
type="button"
@click="removeRow(index)"
>
<i class="mdi mdi-minus-circle-outline" />
</button>
</div>
<div class="workspace-table-filters-buttons">
<button
class="btn btn-sm btn-primary mr-0 ml-2"
type="submit"
>
{{ $t('word.filter') }}
</button>
<button
class="btn btn-sm btn-dark mr-0 ml-2"
type="button"
@click="addRow"
>
<i class="mdi mdi-plus-circle-outline" />
</button>
</div>
</form>
</template>
<script>
import customizations from 'common/customizations';
import { NUMBER, FLOAT } from 'common/fieldTypes';
export default {
props: {
fields: Array,
connClient: String
},
data () {
return {
rows: [],
operators: [
'=', '!=', '>', '<', '>=', '<=', 'IN', 'NOT IN', 'LIKE', 'BETWEEN', 'IS NULL', 'IS NOT NULL'
]
};
},
computed: {
customizations () {
return customizations[this.connClient];
}
},
created () {
this.addRow();
},
methods: {
addRow () {
this.rows.push({ active: true, field: this.fields[0].name, op: '=', value: '', value2: '' });
this.$emit('filter-change', this.rows);
},
removeRow (i) {
this.rows = this.rows.filter((_, idx) => idx !== i);
this.$emit('filter-change', this.rows);
},
doFilter () {
const clausoles = this.rows.filter(el => el.active).map(el => this.createClausole(el));
this.$emit('filter', clausoles);
},
createClausole (filter) {
const field = this.fields.find(field => field.name === filter.field);
const isNumeric = [...NUMBER, ...FLOAT].includes(field.type);
const { elementsWrapper: ew, stringsWrapper: sw } = this.customizations;
let value;
switch (filter.op) {
case '=':
case '!=':
value = isNumeric ? filter.value : `${sw}${filter.value}${sw}`;
break;
case 'BETWEEN':
value = isNumeric ? filter.value : `${sw}${filter.value}${sw}`;
value += ' AND ';
value += isNumeric ? filter.value2 : `${sw}${filter.value2}${sw}`;
break;
case 'IN':
case 'NOT IN':
value = filter.value.split(',').map(val => {
val = val.trim();
return isNumeric ? val : `${sw}${val}${sw}`;
}).join(',');
value = `(${filter.value})`;
break;
case 'IS NULL':
case 'IS NOT NULL':
value = '';
break;
default:
value = `${sw}${filter.value}${sw}`;
}
if (isNumeric && !value.length && !['IS NULL', 'IS NOT NULL'].includes(filter.op))
value = `${sw}${sw}`;
return `${ew}${filter.field}${ew} ${filter.op} ${value}`;
}
}
};
</script>
<style lang="scss">
.workspace-table-filters {
padding: 0 0.6rem;
width: 100%;
}
.workspace-table-filters-buttons {
display: flex;
flex-direction: row-reverse;
padding-bottom: 0.4rem;
}
.workspace-table-filters-row {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 0.4rem;
}
.workspace-table-filters-row-value {
width: 100%;
display: flex;
}
</style>

View File

@@ -117,7 +117,14 @@ module.exports = {
all: 'All', all: 'All',
duplicate: 'Duplicate', duplicate: 'Duplicate',
routine: 'Routine', routine: 'Routine',
new: 'New' new: 'New',
history: 'History',
select: 'Select',
passphrase: 'Passphrase',
filter: 'Filter',
disabled: 'Disabled',
enable: 'Enable',
disable: 'Disable'
}, },
message: { message: {
appWelcome: 'Welcome to Antares SQL Client!', appWelcome: 'Welcome to Antares SQL Client!',
@@ -238,7 +245,13 @@ module.exports = {
newRoutine: 'New routine', newRoutine: 'New routine',
newFunction: 'New function', newFunction: 'New function',
newScheduler: 'New scheduler', newScheduler: 'New scheduler',
newTriggerFunction: 'New trigger function' newTriggerFunction: 'New trigger function',
thereIsNoQueriesYet: 'There is no queries yet',
searchForQueries: 'Search for queries',
killProcess: 'Kill process',
closeTab: 'Close tab',
goToDownloadPage: 'Go to download page',
readOnlyMode: 'Read-only mode'
}, },
faker: { faker: {
address: 'Address', address: 'Address',

View File

@@ -11,7 +11,9 @@ const i18n = new VueI18n({
'es-ES': require('./es-ES'), 'es-ES': require('./es-ES'),
'fr-FR': require('./fr-FR'), 'fr-FR': require('./fr-FR'),
'pt-BR': require('./pt-BR'), 'pt-BR': require('./pt-BR'),
'de-DE': require('./de-DE') 'de-DE': require('./de-DE'),
'vi-VN': require('./vi-VN'),
'ja-JP': require('./ja-JP')
} }
}); });
export default i18n; export default i18n;

View File

@@ -115,7 +115,16 @@ module.exports = {
cell: 'Cella | Celle', cell: 'Cella | Celle',
triggerFunction: 'Funzione di trigger | Funzioni di trigger', triggerFunction: 'Funzione di trigger | Funzioni di trigger',
all: 'Tutto', all: 'Tutto',
duplicate: 'Duplica' duplicate: 'Duplica',
routine: 'Routine',
new: 'Nuovo',
history: 'Cronologia',
select: 'Seleziona',
passphrase: 'Passphrase',
filter: 'Filtra',
disabled: 'Disabilitato',
enable: 'Abilita',
disable: 'Disabilita'
}, },
message: { message: {
appWelcome: 'Benvenuto in Antares SQL Client!', appWelcome: 'Benvenuto in Antares SQL Client!',
@@ -227,7 +236,22 @@ module.exports = {
duplicateTable: 'Duplica tabella', duplicateTable: 'Duplica tabella',
noOpenTabs: 'Non ci sono tab aperte, naviga nella barra sinistra o:', noOpenTabs: 'Non ci sono tab aperte, naviga nella barra sinistra o:',
noSchema: 'Nessuno schema', noSchema: 'Nessuno schema',
restorePreviourSession: 'Ripristina sessione precedente' restorePreviourSession: 'Ripristina sessione precedente',
runQuery: 'Esegui query',
thereAreNoTableFields: 'Non ci sono campi della tabella',
newTable: 'Nuova tabella',
newView: 'Nuova vista',
newTrigger: 'Nuovo trigger',
newRoutine: 'Nuova routine',
newFunction: 'Nuova funzione',
newScheduler: 'Nuovo scheduler',
newTriggerFunction: 'Nuova funzione di trigger',
thereIsNoQueriesYet: 'Non ci sono ancora query',
searchForQueries: 'Cerca query',
killProcess: 'Uccidi processo',
closeTab: 'Chiudi tab',
goToDownloadPage: 'Vai alla pagina di download',
readOnlyMode: 'Modalità sola lettura'
}, },
faker: { faker: {
address: 'Indirizzo', address: 'Indirizzo',

412
src/renderer/i18n/ja-JP.js Normal file
View File

@@ -0,0 +1,412 @@
module.exports = {
word: {
edit: '編集',
save: '保存',
close: '閉じる',
delete: '削除',
confirm: '確認',
cancel: 'キャンセル',
send: '送信',
connectionName: '接続名',
client: 'クライアント',
hostName: 'ホスト名',
port: 'ポート',
user: 'ユーザー名',
password: 'パスワード',
credentials: '認証情報',
connect: '接続',
connected: '接続中',
disconnect: '接続解除',
disconnected: '接続解除',
refresh: 'リフレッシュ',
settings: '設定',
general: '一般',
themes: 'テーマ',
update: '更新情報',
// about: 'お問い合わせ',
language: '言語',
version: 'バージョン',
donate: '寄付する',
run: '実行',
schema: 'スキーマ',
results: '結果',
size: 'サイズ',
seconds: '秒数',
type: 'タイプ',
mimeType: 'マイムタイプ',
download: 'ダウンロード',
add: '追加',
data: 'データ',
properties: 'プロパティ',
insert: '挿入',
connecting: '接続',
name: '名称',
collation: '照合',
clear: 'クリア',
options: 'オプション',
autoRefresh: 'オートリフレシュ',
indexes: 'インデックス',
foreignKeys: '外部キー',
length: '長さ',
unsigned: '符号なし',
default: 'デフォルト',
comment: 'コメント',
key: 'キー | キー',
order: '順序',
expression: '表現',
autoIncrement: 'オートインクリメント',
engine: 'エンジン',
field: 'フィールド | フィールド',
approximately: '約',
total: '合計',
table: 'テーブル',
discard: '破棄',
stay: 'ステイ',
author: '作者',
light: 'ライト',
dark: 'ダーク',
autoCompletion: 'オートコンプリート',
application: 'アプリケーション',
editor: 'エディター',
view: 'ビュー',
definer: 'デファイナー',
algorithm: 'アルゴリズム',
trigger: 'トリガー | トリガー',
storedRoutine: 'ストアド・ルーチン | ストアド・ルーチン',
scheduler: 'スケジューラー | スケジューラー',
event: 'イベント',
parameters: 'パラメータ',
function: '関数 | 関数',
// deterministic: '決定論的',
context: 'コンテキスト',
export: 'エクスポート',
returns: '戻り値',
timing: 'タイミング',
state: '状態',
execution: '実行',
starts: '開始',
ends: '終了',
ssl: 'SSL',
privateKey: '秘密鍵',
certificate: '証明書',
caCertificate: 'CA 証明書',
ciphers: '暗号',
upload: 'アップロード',
browse: '閲覧',
faker: 'フェイカー',
content: 'コンテンツ',
cut: 'カット',
copy: 'コピー',
paste: '貼り付け',
tools: 'ツール',
variables: '変数',
processes: 'プロセス',
database: 'データベース',
scratchpad: 'スクラッチパッド',
array: '配列',
changelog: '変更履歴',
format: 'フォーマット',
sshTunnel: 'SSH トンネル',
structure: '構造',
// small: '小規模',
// medium: '中型',
// large: 'ラージ',
row: 'ロウ | ロウ',
cell: 'セル | セル',
triggerFunction: 'トリガー関数 | トリガー関数',
all: 'すべて',
duplicate: 'デュプリケート',
routine: 'ルーチン',
// new: '新機能',
history: '履歴',
select: '選択'
},
message: {
appWelcome: 'Antares SQL Client へようこそ!',
appFirstStep: '最初のステップは、新しいデータベース接続を作成することです。',
addConnection: '接続の追加',
createConnection: '接続の作成',
createNewConnection: '新しい接続の作成',
askCredentials: '認証情報の入力',
testConnection: '接続のテスト',
editConnection: '接続の編集',
deleteConnection: '接続の削除',
deleteCorfirm: 'のキャンセルを確認しますか?',
connectionSuccessfullyMade: '接続に成功しました。',
madeWithJS: '💛 と JavaScript で作られています。',
checkForUpdates: '更新情報の確認',
noUpdatesAvailable: 'アップデートがありません',
checkingForUpdate: 'アップデートを確認中',
checkFailure: 'チェックに失敗しました、後で試してください',
updateAvailable: 'アップデートが利用可能です',
downloadingUpdate: 'アップデートのダウンロード',
updateDownloaded: 'アップデートのダウンロード',
restartToInstall: 'Antares を再起動してインストールしてください',
unableEditFieldWithoutPrimary: '主キーのないフィールドを結果セットで編集できない',
editCell: 'セルの編集',
deleteRows: '行の削除 | {count} 行の削除',
confirmToDeleteRows: '1つの行を削除することを確認しますか | {count} 行を削除することを確認しますか?',
notificationsTimeout: '通知のタイムアウト',
uploadFile: 'ファイルのアップロード',
addNewRow: '新しい行の追加',
numberOfInserts: 'インサート数',
openNewTab: '新しいタブを開く',
affectedRows: '影響を受ける行',
createNewDatabase: '新規データベースの作成',
databaseName: 'データベース名',
serverDefault: 'サーバーのデフォルト',
deleteDatabase: 'データベースの削除',
editDatabase: 'データベースの編集',
clearChanges: '変更の消去',
addNewField: '新しいフィールドの追加',
manageIndexes: 'インデックスの管理',
manageForeignKeys: '外部キーの管理',
allowNull: 'NULL を許可する',
zeroFill: 'ゼロフィル',
customValue: 'カスタム値',
onUpdate: '更新時',
deleteField: 'フィールドの削除',
createNewIndex: '新しいインデックスの作成',
addToIndex: 'インデックスへの追加',
createNewTable: '新しいテーブルの作成',
emptyTable: '空のテーブル',
deleteTable: 'テーブルの削除',
emptyCorfirm: '空にすることを確認しますか?',
unsavedChanges: '保存されていない変更',
discardUnsavedChanges: '保存されていない変更があります。このタブを閉じると、これらの変更は破棄されます。',
thereAreNoIndexes: 'インデックスがありません',
thereAreNoForeign: '外部キーがありません。',
createNewForeign: '新しい外部キーの作成',
referenceTable: '参照テーブル',
referenceField: '参照フィールド',
foreignFields: '外部フィールド',
invalidDefault: '無効なデフォルト',
onDelete: '削除時',
applicationTheme: 'アプリケーションテーマ',
editorTheme: 'エディターテーマ',
wrapLongLines: '長い行の折り返し',
selectStatement: '選択文',
triggerStatement: 'トリガー文',
sqlSecurity: 'SQL セキュリティ',
updateOption: '更新オプション',
deleteView: 'ビューの削除',
createNewView: '新規ビューの作成',
deleteTrigger: 'トリガーの削除',
createNewTrigger: '新しいトリガの作成',
currentUser: '現在のユーザー',
routineBody: 'ルーチン本体',
dataAccess: 'データアクセス',
thereAreNoParameters: 'パラメータはありません',
createNewParameter: '新しいパラメータの作成',
createNewRoutine: 'ストアド・ルーチンの新規作成',
deleteRoutine: 'ストアド・ルーチンの削除',
functionBody: '関数本体',
createNewFunction: '新しい関数の作成',
deleteFunction: '関数の削除',
schedulerBody: 'スケジューラ本体',
createNewScheduler: 'スケジューラの新規作成',
deleteScheduler: 'スケジューラの削除',
preserveOnCompletion: '完了時に保存する',
enableSsl: 'SSL 対応',
manualValue: 'マニュアル値',
tableFiller: 'テーブルフィラー',
fakeDataLanguage: 'フェイクデータの言語',
searchForElements: '要素の検索',
selectAll: 'すべてを選択する',
queryDuration: '問い合わせ期間',
includeBetaUpdates: 'ベータ版アップデートを含む',
setNull: 'NULL の設定',
processesList: 'プロセス一覧',
processInfo: 'プロセス情報',
manageUsers: 'ユーザーの管理',
createNewSchema: '新しいスキーマの作成',
schemaName: 'スキーマ名',
editSchema: 'スキーマの編集',
deleteSchema: 'スキーマの削除',
markdownSupported: 'マークダウン対応',
// plantATree: '木を植える',
dataTabPageSize: 'DATA タブのページサイズ',
enableSsh: 'SSH を有効にする',
pageNumber: 'ページ番号',
duplicateTable: 'テーブルを複製する',
noOpenTabs: '開いているタブがありません。左のバーでナビゲートするか',
noSchema: 'スキーマなし',
restorePreviourSession: '前のセッションに戻す',
runQuery: 'クエリの実行',
thereAreNoTableFields: 'テーブルのフィールドがありません',
newTable: '新しいテーブル',
newView: '新しいビュー',
newTrigger: '新しいトリガー',
newRoutine: '新しいルーチン',
newFunction: '新しい関数',
newScheduler: '新規スケジューラ',
newTriggerFunction: '新しいトリガー機能',
thereIsNoQueriesYet: 'まだ問い合わせはありません',
searchForQueries: 'クエリの検索',
killProcess: 'プロセスの停止'
},
faker: {
address: '住所',
commerce: 'コマース',
company: '会社名',
database: 'データベース',
date: '日付',
finance: 'ファイナンス',
// git: 'ギット',
hacker: 'ハッカー',
internet: 'インターネット',
// lorem: 'ローレム',
name: '名前',
music: '音楽',
phone: '電話',
random: 'ランダム',
system: 'システム',
time: '時間',
vehicle: '車',
zipCode: '郵便番号',
zipCodeByState: '都道府県別郵便番号',
city: '都市名',
cityPrefix: '市のプレフィックス',
citySuffix: '市の接尾辞',
streetName: '通りの名前',
streetAddress: 'ストリートアドレス',
streetSuffix: '通りの接尾辞',
streetPrefix: 'ストリートプレフィックス',
secondaryAddress: '副住所',
county: '郡',
country: '国名',
countryCode: '国コード',
state: '州',
stateAbbr: '州の略語',
latitude: '緯度',
longitude: '経度',
direction: '方向',
cardinalDirection: '枢機卿の方向',
ordinalDirection: '序列方向',
nearbyGPSCoordinate: '近くのGPS座標',
timeZone: 'タイムゾーン',
color: '色',
department: '部門',
productName: '商品名',
price: '価格',
productAdjective: '製品の形容詞',
productMaterial: '製品の素材',
product: '製品',
productDescription: '製品の説明',
suffixes: 'サフィックス',
companyName: '会社名',
companySuffix: '会社のサフィックス',
catchPhrase: 'キャッチフレーズ',
// bs: 'BS',
catchPhraseAdjective: 'キャッチフレーズ形容詞',
catchPhraseDescriptor: 'キャッチフレーズの説明文',
catchPhraseNoun: 'キャッチフレーズの名詞',
bsAdjective: 'BS 形容詞',
bsBuzz: 'BS の話題',
bsNoun: 'BS の名詞',
column: 'コラム',
type: 'タイプ',
collation: '照合',
engine: 'エンジン',
past: '過去',
future: '未来',
between: '間',
recent: '最近',
soon: 'すぐ',
month: '月',
weekday: '曜日',
account: 'アカウント',
accountName: '口座名',
routingNumber: 'ルーティング番号',
mask: 'マスク',
amount: '金額',
transactionType: '取引の種類',
currencyCode: '通貨コード',
currencyName: '通貨名',
currencySymbol: '通貨記号',
bitcoinAddress: 'Bitcoin アドレス',
litecoinAddress: 'ライトコインのアドレス',
creditCardNumber: 'クレジットカード番号',
creditCardCVV: 'クレジットカードの CVV',
ethereumAddress: 'イーサリアムのアドレス',
iban: 'アイバン',
bic: 'ビック',
transactionDescription: '取引内容',
branch: 'ブランチ',
commitEntry: 'コミットエントリ',
commitMessage: 'コミットメッセージ',
commitSha: 'コミット SHA',
shortSha: 'ショート SHA',
abbreviation: '省略形',
adjective: '形容詞',
noun: '名詞',
verb: '動詞',
ingverb: '動詞',
phrase: 'フレーズ',
avatar: 'アバター',
email: 'メール',
exampleEmail: 'メールの例',
userName: 'ユーザー名',
protocol: 'プロトコル',
url: 'URL',
domainName: 'ドメイン名',
domainSuffix: 'ドメインのサフィックス',
domainWord: 'ドメイン名',
ip: 'Ip',
ipv6: 'Ipv6',
userAgent: 'ユーザーエージェント',
// mac: 'Mac',
password: 'パスワード',
word: 'ワード',
words: '単語',
sentence: '文章',
slug: 'スラッグ',
sentences: 'センテンス',
paragraph: 'パラグラフ',
paragraphs: 'パラグラフ',
text: 'テキスト',
lines: '行',
genre: 'ジャンル',
firstName: 'ファーストネーム',
lastName: '苗字',
middleName: 'ミドルネーム',
findName: 'フルネーム',
jobTitle: '役職名',
gender: '性別',
prefix: 'プレフィックス',
suffix: 'サフィックス',
title: '役職名',
jobDescriptor: '職務記述書',
jobArea: '職務領域',
jobType: '仕事の種類',
phoneNumber: '電話番号',
phoneNumberFormat: '電話番号のフォーマット',
phoneFormats: '電話番号のフォーマット',
// number: '番号',
// float: 'フロート',
arrayElement: '配列要素',
arrayElements: '配列要素',
objectElement: 'オブジェクトの要素',
// uuid: 'Uuid',
// boolean: 'ブール',
image: '画像',
locale: 'ロケール',
alpha: '英字',
alphaNumeric: '英数字',
hexaDecimal: '16進法',
fileName: 'ファイル名',
commonFileName: '一般的なファイル名',
mimeType: 'Mimeタイプ',
commonFileType: '共通のファイルタイプ',
commonFileExt: '共通のファイル拡張子',
fileType: 'ファイルタイプ',
fileExt: 'ファイル拡張子',
directoryPath: 'ディレクトリパス',
filePath: 'ファイルパス',
// semver: 'セムバー',
manufacturer: 'メーカー名',
model: 'モデル',
fuel: '燃料'
// vin: 'Vin'
}
};

View File

@@ -5,5 +5,7 @@ export default {
'es-ES': 'Español', 'es-ES': 'Español',
'fr-FR': 'Français', 'fr-FR': 'Français',
'pt-BR': 'Português (Brasil)', 'pt-BR': 'Português (Brasil)',
'de-DE': 'Deutsch (Deutschland)' 'de-DE': 'Deutsch (Deutschland)',
'vi-VN': 'Tiếng Việt',
'ja-JP': '日本語'
}; };

413
src/renderer/i18n/vi-VN.js Normal file
View File

@@ -0,0 +1,413 @@
module.exports = {
word: {
edit: 'Chỉnh sửa',
save: 'Lưu',
close: 'Đóng',
delete: 'Xoá',
confirm: 'Xác nhận',
cancel: 'Huỷ',
send: 'Gửi',
connectionName: 'Tên kết nối',
client: 'Client',
hostName: 'Tên máy chủ',
port: 'Cổng',
user: 'Người dùng',
password: 'Mật khẩu',
credentials: 'Thông tin xác thực',
connect: 'Kết nối',
connected: 'Đã kết nối',
disconnect: 'Ngắt kết nối',
disconnected: 'Đã ngắt kết nối',
refresh: 'Làm mới',
settings: 'Cài đặt',
general: 'Chung',
themes: 'Giao diện',
update: 'Cập nhật',
about: 'Giới thiệu',
language: 'Ngôn ngữ',
version: 'Phiên bản',
donate: 'Ủng hộ',
run: 'Chạy',
schema: 'Schema',
results: 'Kết quả',
size: 'Kích thước',
seconds: 'Giây',
type: 'Kiểu',
mimeType: 'Mime-Type',
download: 'Tải xuống',
add: 'Thêm',
data: 'Dữ liệu',
properties: 'Thuộc tính',
insert: 'Nhập',
connecting: 'Đang kết nối',
name: 'Tên',
collation: 'Đối chiếu',
clear: 'Xoá',
options: 'Tuỳ chọn',
autoRefresh: 'Tự động làm mới',
indexes: 'Index',
foreignKeys: 'Khoá ngoại',
length: 'Độ dài',
unsigned: 'Unsigned',
default: 'Mặc định',
comment: 'Nhận xét',
key: 'Khoá | Khoá',
order: 'Sắp xếp',
expression: 'Biểu hiện',
autoIncrement: 'Tự động tăng',
engine: 'Engine',
field: 'Trường | Trường',
approximately: 'Khoảng',
total: 'Toàn bộ',
table: 'Bảng',
discard: 'Bỏ',
stay: 'Ở lại',
author: 'Tác giả',
light: 'Sáng',
dark: 'Tối',
autoCompletion: 'Tự động hoàn thành',
application: 'Ứng dụng',
editor: 'Người chỉnh sửa',
view: 'Xem',
definer: 'Định nghĩa',
algorithm: 'Thuật toán',
trigger: 'Kích hoạt | Kích hoạt',
storedRoutine: 'Quy trình đã lưu | Quy trình đã lưu',
scheduler: 'Lập lịch trình | Lập lịch trình',
event: 'Sự kiện',
parameters: 'Tham số',
function: 'Chức năng | Chức năng',
deterministic: 'Xác định',
context: 'Context',
export: 'Xuất',
returns: 'Returns',
timing: 'Thời gian',
state: 'Trạng thái',
execution: 'Thực thi',
starts: 'Bắt đầu',
ends: 'Kết thúc',
ssl: 'SSL',
privateKey: 'Mã khoá riêng tư',
certificate: 'Chứng chỉ',
caCertificate: 'Chứng chỉ CA',
ciphers: 'Ciphers',
upload: 'Tải lên',
browse: 'Duyệt',
faker: 'Faker',
content: 'Nội dung',
cut: 'Cắt',
copy: 'Sao chép',
paste: 'Dán',
tools: 'Công cụ',
variables: 'Biến',
processes: 'Quá trình',
database: 'Cơ sở dữ liệu',
scratchpad: 'Scratchpad',
array: 'Mảng',
changelog: 'Nhật ký thay đổi',
format: 'Định dạng',
sshTunnel: 'SSH tunnel',
structure: 'Structure',
small: 'Nhỏ',
medium: 'Vừa',
large: 'Lớn',
row: 'Hàng | Hàng',
cell: 'Ô | Ô',
triggerFunction: 'Trigger function | Trigger functions',
all: 'Tất cả',
duplicate: 'Bản sao',
routine: 'Routine',
new: 'Mới',
history: 'Lịch sử',
select: 'Chọn',
passphrase: 'Cụm mật khẩu'
},
message: {
appWelcome: 'Chào bạn đến với Antares SQL Client!',
appFirstStep: 'Bước đầu tiên: tạo một kết nối tới cơ sở dữ liệu.',
addConnection: 'Thêm kết nối',
createConnection: 'Tạo kết nối',
createNewConnection: 'Tạo kết nối mới',
askCredentials: 'Yêu cầu thông tin đăng nhập',
testConnection: 'Chạy thử kết nối',
editConnection: 'Sửa kết nối',
deleteConnection: 'Xoá kết nối',
deleteCorfirm: 'Bạn có xác nhận việc hủy bỏ',
connectionSuccessfullyMade: 'Kết nối được thực hiện thành công!',
madeWithJS: 'Được tạo bằng 💛 và JavaScript!',
checkForUpdates: 'Kiểm tra cập nhật',
noUpdatesAvailable: 'Không có bản cập nhật nào',
checkingForUpdate: 'Đang kiểm tra cập nhật',
checkFailure: 'Kiểm tra thất bại, vui lòng thử lại sau',
updateAvailable: 'Cập nhật có sẵn',
downloadingUpdate: 'Đang tải bản cập nhật',
updateDownloaded: 'Đã tải bản cập nhạt',
restartToInstall: 'Khởi động lại Antares để cài đặt',
unableEditFieldWithoutPrimary: 'Không thể chỉnh sửa trường mà không có khóa chính trong kết quả',
editCell: 'Sửa ô',
deleteRows: 'Xoá hàng | Xoá {count} hàng',
confirmToDeleteRows: 'Bạn có xác nhận xóa một hàng không? | Bạn có xác nhận xóa {count} hàng không?',
notificationsTimeout: 'Thông báo hết giờ',
uploadFile: 'Tải lên tệp',
addNewRow: 'Thêm hàng mới',
numberOfInserts: 'Số lần nhập',
openNewTab: 'Mở trong tab mới',
affectedRows: 'Các hàng bị ảnh hưởng',
createNewDatabase: 'Tạo Cơ sở dữ liệu mới',
databaseName: 'Tên cơ sở dữ liệu',
serverDefault: 'Máy chủ mặc định',
deleteDatabase: 'Xoá cơ sở dữ liệu',
editDatabase: 'Sửa cơ sở dữ liệu',
clearChanges: 'Xóa các thay đổi',
addNewField: 'Thêm trường mới',
manageIndexes: 'Quản lý index',
manageForeignKeys: 'Quản lý khoá ngoại',
allowNull: 'Cho phép NULL',
zeroFill: 'Không điền',
customValue: 'Tuỳ chỉnh giá trị',
onUpdate: 'Đang cập nhật',
deleteField: 'Xoá trường',
createNewIndex: 'Tạo index mới',
addToIndex: 'Thêm vào index',
createNewTable: 'Tạo bảng mới',
emptyTable: 'Bảng trống',
deleteTable: 'Xoá bảng',
emptyCorfirm: 'Bạn có xác nhận để làm trống không',
unsavedChanges: 'Chưa lưu lại thay đổi',
discardUnsavedChanges: 'Bạn có một số thay đổi chưa được lưu. Đóng tab này, những thay đổi này sẽ bị huỷ bỏ.',
thereAreNoIndexes: 'Không có index',
thereAreNoForeign: 'Không có khoá ngoại',
createNewForeign: 'Tạo khoá ngoại mới',
referenceTable: 'Tham khảo bảng',
referenceField: 'Tham khảo trường',
foreignFields: 'Trường ngoại',
invalidDefault: 'Mặc định không hợp lệ',
onDelete: 'Đang xoá',
applicationTheme: 'Chủ đề ứng dụng',
editorTheme: 'Trình chỉnh sửa chủ đề',
wrapLongLines: 'Wrap long lines',
selectStatement: 'Chọn câu lệnh',
triggerStatement: 'Kích hoạt câu lệnh',
sqlSecurity: 'Bảo mật SQL',
updateOption: 'Cập nhật tuỳ chọn',
deleteView: 'Xóa chế độ xem',
createNewView: 'Tạo chế độ xem mới',
deleteTrigger: 'Xóa trình kích hoạt',
createNewTrigger: 'Tạo trình kích hoạt mới',
currentUser: 'Người dùng hiện tại',
routineBody: 'Body quy trình',
dataAccess: 'Truy cập dữ liệu',
thereAreNoParameters: 'Không có tham số',
createNewParameter: 'Tạo tham số mới',
createNewRoutine: 'Tạo quy trình lưu trữ mới',
deleteRoutine: 'Xoá quy trình lưu trữ',
functionBody: 'Body chức năng',
createNewFunction: 'Tạo chức năng mới',
deleteFunction: 'Xoá chức năng',
schedulerBody: 'Body trình lập lịch',
createNewScheduler: 'Tạo lịch trình mới',
deleteScheduler: 'Xóa trình lên lịch',
preserveOnCompletion: 'Bảo tồn khi hoàn thành',
enableSsl: 'Bật SSL',
manualValue: 'Giá trị thủ công',
tableFiller: 'Bộ lọc bảng',
fakeDataLanguage: 'Ngôn ngữ dữ liệu giả mạo',
searchForElements: 'Tìm kiếm yếu tố',
selectAll: 'Chọn tất cả',
queryDuration: 'Thời lượng truy vấn',
includeBetaUpdates: 'Bao gồm các bản cập nhật beta',
setNull: 'Đặt NULL',
processesList: 'Danh sách quy trình',
processInfo: 'Thông tin quá trình',
manageUsers: 'Quản lý người dùng',
createNewSchema: 'Tạo schema mới',
schemaName: 'Tên schema',
editSchema: 'Sửa schema',
deleteSchema: 'Xoá schema',
markdownSupported: 'Hỗ trợ Markdown',
plantATree: 'Trồng cây',
dataTabPageSize: 'Kích thước trang tab DATA',
enableSsh: 'Bật SSH',
pageNumber: 'Số trang',
duplicateTable: 'Sao chép bản',
noOpenTabs: 'Không có tab nào đang mở, điều hướng trên thanh bên trái hoặc:',
noSchema: 'Không có schema',
restorePreviourSession: 'Khôi phục phiên trước',
runQuery: 'Chạy truy vấn',
thereAreNoTableFields: 'Không có trường bảng',
newTable: 'Bảng mới',
newView: 'Chế độ xem mới',
newTrigger: 'Trình kích hoạt mới',
newRoutine: 'Quy trình mới',
newFunction: 'Chức năng mới',
newScheduler: 'Bộ lập lịch mới',
newTriggerFunction: 'Chức năng kích hoạt mới',
thereIsNoQueriesYet: 'Không có truy vấn nào',
searchForQueries: 'Tìm kiếm truy vấn',
killProcess: 'Huỷ quá trình'
},
faker: {
address: 'Địa chỉ',
commerce: 'Thương mại',
company: 'Công ty',
database: 'Cơ sở dữ liệu',
date: 'Ngày',
finance: 'Tài chánh',
git: 'Git',
hacker: 'Tin tặc',
internet: 'Mạng Internet',
lorem: 'Lorem',
name: 'Tên',
music: 'Âm nhạc',
phone: 'Số điện thoại',
random: 'Ngẫu nhiên',
system: 'Hệ thống',
time: 'Thời gian',
vehicle: 'Phương tiện giao thông',
zipCode: 'Mã Bưu Chính',
zipCodeByState: 'Mã Bưu Chính theo tiểu bang',
city: 'Thành phố',
cityPrefix: 'Tiền tố thành phố',
citySuffix: 'Hậu tố thành phố',
streetName: 'Tên đường',
streetAddress: 'Địa chỉ đường',
streetSuffix: 'Hậu tố đường',
streetPrefix: 'Tiền tố đường',
secondaryAddress: 'Địa chỉ phụ',
county: 'Quận',
country: 'Quốc gia',
countryCode: 'Mã quốc gia',
state: 'Tiểu bang',
stateAbbr: 'Viết tắt của tiểu bang',
latitude: 'Vĩ độ',
longitude: 'Kinh độ',
direction: 'Hướng',
cardinalDirection: 'Hướng cốt yếu',
ordinalDirection: 'Hướng thứ tự',
nearbyGPSCoordinate: 'Tọa độ GPS lân cận',
timeZone: 'Múi giờ',
color: 'Màu',
department: 'Phòng',
productName: 'Tên sản phẩm',
price: 'Giá',
productAdjective: 'Tính từ sản phẩm',
productMaterial: 'Chất liệu sản phẩm',
product: 'Sản phẩm',
productDescription: 'Mô tả sản phẩm',
suffixes: 'Hậu tố',
companyName: 'Tên công ty',
companySuffix: 'Hậu tố công ty',
catchPhrase: 'Khẩu hiệu',
bs: 'BS',
catchPhraseAdjective: 'Bắt cụm từ tính từ',
catchPhraseDescriptor: 'Bắt bộ mô tả cụm từ',
catchPhraseNoun: 'Bắt cụm từ danh từ',
bsAdjective: 'BS tính từ',
bsBuzz: 'BS buzz',
bsNoun: 'BS danh từ',
column: 'Cột',
type: 'Loại',
collation: 'Đối chiếu',
engine: 'Engine',
past: 'Quá khứ',
future: 'Tương lai',
between: 'Giữa',
recent: 'Gần đây',
soon: 'Sớm',
month: 'Tháng',
weekday: 'Ngày trong tuần',
account: 'Tài khoản',
accountName: 'Tên tài khoản',
routingNumber: 'Số định tuyến',
mask: 'Mặt nạ',
amount: 'Số tiền',
transactionType: 'Loại giao dịch',
currencyCode: 'Mã tiền tệ',
currencyName: 'Tên tiền tệ',
currencySymbol: 'Ký hiệu tiền tệ',
bitcoinAddress: 'Địa chỉ Bitcoin',
litecoinAddress: 'Địa chỉ Litecoin',
creditCardNumber: 'Số thẻ tín dụng',
creditCardCVV: 'CVV thẻ tín dụng',
ethereumAddress: 'Địa chỉ Ethereum',
iban: 'Iban',
bic: 'Bic',
transactionDescription: 'Mô tả giao dịch',
branch: 'Nhánh',
commitEntry: 'Nhập cam kết',
commitMessage: 'Thông báo cam kết',
commitSha: 'Cam kết SHA',
shortSha: 'SHA ngắn',
abbreviation: 'Viết tắt',
adjective: 'Tính từ',
noun: 'Danh từ',
verb: 'Động từ',
ingverb: 'Động từ ing',
phrase: 'Cụm từ',
avatar: 'Ảnh đại diện',
email: 'Email',
exampleEmail: 'Email ví dụ',
userName: 'Tên người dùng',
protocol: 'Giao thức',
url: 'Url',
domainName: 'Tên miền',
domainSuffix: 'Hậu tố miền',
domainWord: 'Từ miền',
ip: 'Ip',
ipv6: 'Ipv6',
userAgent: 'User agent',
mac: 'Mac',
password: 'Mật khẩu',
word: 'Từ',
words: 'Từ',
sentence: 'Câu',
slug: 'Slug',
sentences: 'Câu',
paragraph: 'Đoạn văn',
paragraphs: 'Đoạn văn',
text: 'Văn bản',
lines: 'Dòng',
genre: 'Thể loại',
firstName: 'Tên',
lastName: 'Họ',
middleName: 'Tên đệm',
findName: 'Tên đầy đủ',
jobTitle: 'Chức vụ',
gender: 'Giới tính',
prefix: 'Tiền tố',
suffix: 'Hậu tố',
title: 'Tiêu đề',
jobDescriptor: 'Mô tả công việc',
jobArea: 'Lĩnh vực việc làm',
jobType: 'Loại công việc',
phoneNumber: 'Số điện thoại',
phoneNumberFormat: 'Định dạng số điện thoại',
phoneFormats: 'Định dạng điện thoại',
number: 'Số',
float: 'Float',
arrayElement: 'Phân tử array',
arrayElements: 'Phân tử array',
objectElement: 'Phần tử object',
uuid: 'Uuid',
boolean: 'Boolean',
image: 'Hình ảnh',
locale: 'Ngôn ngữ',
alpha: 'Alpha',
alphaNumeric: 'Chữ và số',
hexaDecimal: 'Hệ thập lục phân',
fileName: 'File name',
commonFileName: 'Tên tệp chung',
mimeType: 'Kiểu mine',
commonFileType: 'Loại tệp chung',
commonFileExt: 'Phần mở rộng tệp chung',
fileType: 'Loại tệp',
fileExt: 'Phần mở rộng tệp',
directoryPath: 'Đường dẫn thư mục',
filePath: 'Đường dẫn tệp',
semver: 'Semver',
manufacturer: 'Manufacturer',
model: 'Model',
fuel: 'Fuel',
vin: 'Vin'
}
};

View File

Before

Width:  |  Height:  |  Size: 1004 B

After

Width:  |  Height:  |  Size: 1004 B

22
src/renderer/index.ejs Normal file
View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
<script>
global = globalThis
</script>
<% if (htmlWebpackPlugin.options.nodeModules) { %>
<script>
require('module').globalPaths.push(
`<%= htmlWebpackPlugin.options.nodeModules.replace(/\\/g, '\\\\') %>`
)
</script>
<% } %>
</head>
<body>
<div id="app"></div>
<!-- webpack builds are automatically injected -->
</body>
</html>

View File

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

View File

@@ -42,6 +42,10 @@ export default class {
return ipcRenderer.invoke('get-processes', uid); return ipcRenderer.invoke('get-processes', uid);
} }
static killProcess (params) {
return ipcRenderer.invoke('kill-process', params);
}
static useSchema (params) { static useSchema (params) {
return ipcRenderer.invoke('use-schema', params); return ipcRenderer.invoke('use-schema', params);
} }

View File

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

View File

@@ -17,10 +17,12 @@
( (
"char": $string-color, "char": $string-color,
"varchar": $string-color, "varchar": $string-color,
"longvarchar": $string-color,
"text": $string-color, "text": $string-color,
"tinytext": $string-color, "tinytext": $string-color,
"mediumtext": $string-color, "mediumtext": $string-color,
"longtext": $string-color, "longtext": $string-color,
"string": $string-color,
"json": $string-color, "json": $string-color,
"name": $string-color, "name": $string-color,
"character": $string-color, "character": $string-color,
@@ -50,6 +52,7 @@
"oid": $number-color, "oid": $number-color,
"xid": $number-color, "xid": $number-color,
"money": $number-color, "money": $number-color,
"number": $number-color,
"datetime": $date-color, "datetime": $date-color,
"date": $date-color, "date": $date-color,
"time": $date-color, "time": $date-color,
@@ -70,6 +73,7 @@
"bytea": $blob-color, "bytea": $blob-color,
"enum": $enum-color, "enum": $enum-color,
"set": $enum-color, "set": $enum-color,
"bool": $enum-color,
"boolean": $enum-color, "boolean": $enum-color,
"interval": $array-color, "interval": $array-color,
"array": $array-color, "array": $array-color,

View File

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

View File

@@ -142,7 +142,7 @@
code { code {
background-color: #000; background-color: #000;
color: $body-font-color-dark; color: rgba($body-font-color-dark, 0.7);
} }
// Antares // Antares
@@ -283,6 +283,12 @@
} }
.tile { .tile {
transition: background 0.2s;
&:focus {
background: rgba($bg-color-light-dark, 60%);
}
&:hover { &:hover {
background: $bg-color-light-dark; background: $bg-color-light-dark;
} }

View File

@@ -79,6 +79,12 @@
} }
.tile { .tile {
transition: background 0.2s;
&:focus {
background: rgba($bg-color-light-gray, 70%);
}
&:hover { &:hover {
background: $bg-color-light-gray; background: $bg-color-light-gray;
} }

View File

@@ -5,12 +5,15 @@ import Vuex from 'vuex';
import application from './modules/application.store'; import application from './modules/application.store';
import settings from './modules/settings.store'; import settings from './modules/settings.store';
import history from './modules/history.store';
import scratchpad from './modules/scratchpad.store'; import scratchpad from './modules/scratchpad.store';
import connections from './modules/connections.store'; import connections from './modules/connections.store';
import workspaces from './modules/workspaces.store'; import workspaces from './modules/workspaces.store';
import notifications from './modules/notifications.store'; import notifications from './modules/notifications.store';
import ipcUpdates from './plugins/ipcUpdates'; import ipcUpdates from './plugins/ipcUpdates';
import ipcExceptions from './plugins/ipcExceptions';
import ipcShortcuts from './plugins/ipcShortcuts';
Vue.use(Vuex); Vue.use(Vuex);
@@ -19,12 +22,15 @@ export default new Vuex.Store({
modules: { modules: {
application, application,
settings, settings,
history,
scratchpad, scratchpad,
connections, connections,
workspaces, workspaces,
notifications notifications
}, },
plugins: [ plugins: [
ipcUpdates ipcUpdates,
ipcExceptions,
ipcShortcuts
] ]
}); });

View File

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

View File

@@ -0,0 +1,54 @@
'use strict';
import Store from 'electron-store';
import { uidGen } from 'common/libs/uidGen';
const persistentStore = new Store({ name: 'history' });
const historySize = 1000;
export default {
namespaced: true,
strict: true,
state: {
history: persistentStore.get('history', {}),
favorites: persistentStore.get('favorites', {})
},
getters: {
getHistoryByWorkspace: state => uid => state.history[uid]
},
mutations: {
SET_HISTORY (state, args) {
if (!(args.uid in state.history))
state.history[args.uid] = [];
state.history[args.uid] = [
{
uid: uidGen('H'),
sql: args.query,
date: new Date(),
schema: args.schema
},
...state.history[args.uid]
];
if (state.history[args.uid].length > historySize)
state.history[args.uid] = state.history[args.uid].slice(0, historySize);
persistentStore.set('history', state.history);
},
DELETE_QUERY_FROM_HISTORY (state, query) {
state.history[query.workspace] = state.history[query.workspace].filter(q => q.uid !== query.uid);
persistentStore.set('history', state.history);
}
},
actions: {
saveHistory ({ commit, getters }, args) {
if (getters.getHistoryByWorkspace(args.uid) &&
getters.getHistoryByWorkspace(args.uid).length &&
getters.getHistoryByWorkspace(args.uid)[0].sql === args.query
) return;
commit('SET_HISTORY', args);
},
deleteQueryFromHistory ({ commit }, query) {
commit('DELETE_QUERY_FROM_HISTORY', query);
}
}
};

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