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

Compare commits

..

119 Commits

Author SHA1 Message Date
51ccce3da4 chore(release): 0.0.6 2020-09-03 13:46:16 +02:00
a1a6f51f2f fix: error when launching queries without a result from query tabs 2020-09-03 13:44:58 +02:00
801a0de186 fix: field name displayed instead of alias 2020-09-02 18:14:30 +02:00
264de9c568 feat: aliases support 2020-09-01 19:23:13 +02:00
8390f8aa55 Merge pull request #31 from EStarium/dependabot/npm_and_yarn/electron-10.1.0
build(deps-dev): bump electron from 9.2.1 to 10.1.0
2020-08-31 17:39:49 +02:00
af7c0e90b8 Merge pull request #30 from EStarium/dependabot/npm_and_yarn/sass-loader-10.0.1
build(deps-dev): bump sass-loader from 9.0.3 to 10.0.1
2020-08-31 14:02:57 +02:00
dependabot[bot]
da33e77361 build(deps-dev): bump electron from 9.2.1 to 10.1.0
Bumps [electron](https://github.com/electron/electron) from 9.2.1 to 10.1.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/master/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v9.2.1...v10.1.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-31 06:47:39 +00:00
dependabot[bot]
a4841ab63b build(deps-dev): bump sass-loader from 9.0.3 to 10.0.1
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 9.0.3 to 10.0.1.
- [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/v9.0.3...v10.0.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-31 06:44:49 +00:00
de3f36a3fe docs: update README.md 2020-08-21 15:56:29 +02:00
8dc74ef2c3 feat: sql suggestions in query editor 2020-08-21 11:38:00 +02:00
256ec76588 feat: middle click to close tabs 2020-08-21 10:57:26 +02:00
196a3e0185 feat: monaco-editor as query editor 2020-08-20 18:06:02 +02:00
bc54fef0aa docs: Update README.md 2020-08-20 15:31:50 +02:00
a5b478e53d Merge pull request #29 from Mohd-PH/master
Add Arabic translation
2020-08-20 15:24:03 +02:00
Mohd-PH
2e235ad2fe Change the name of Arabic in the settings page 2020-08-20 16:21:11 +03:00
Mohd-PH
950bb17b1e Add Arabic translation 2020-08-20 12:37:26 +03:00
3a6ea76b93 feat: tabs horizontal scroll with mouse wheel 2020-08-20 10:38:18 +02:00
d7ed00f4a3 feat: support to multiple query tabs 2020-08-19 18:20:57 +02:00
fd6d5177ef fix: wrong table height calc in some cases 2020-08-19 16:25:42 +02:00
9599b43f78 refactor: changed event names to kebab-case 2020-08-18 18:03:59 +02:00
2cfb223ff6 chore: Improved changelog 2020-08-17 17:48:21 +02:00
69def94c88 chore(release): 0.0.5 2020-08-17 17:39:28 +02:00
e8141b6321 feat: badge on setting icon and update tab when update is available 2020-08-17 17:37:42 +02:00
0b6a188d19 feat: foreign key support in add/edit row 2020-08-17 15:10:19 +02:00
dca625fe5a Merge pull request #28 from EStarium/dependabot/npm_and_yarn/standard-version-9.0.0
build(deps-dev): bump standard-version from 8.0.2 to 9.0.0
2020-08-17 08:42:08 +02:00
dependabot[bot]
a4b94bc19c build(deps-dev): bump standard-version from 8.0.2 to 9.0.0
Bumps [standard-version](https://github.com/conventional-changelog/standard-version) from 8.0.2 to 9.0.0.
- [Release notes](https://github.com/conventional-changelog/standard-version/releases)
- [Changelog](https://github.com/conventional-changelog/standard-version/blob/master/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/standard-version/compare/v8.0.2...v9.0.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-17 06:28:16 +00:00
744728a14f refactor: moved table fields informations to vuex 2020-08-14 18:07:29 +02:00
6d0724dc90 fix: wrong schema passed in query tab when selected a different database 2020-08-14 11:25:50 +02:00
59e4a79f42 fix: newline replaced with undefined inside queries 2020-08-14 11:06:20 +02:00
7bc10092fe fix: query result table header didn't show just selected fields 2020-08-13 13:24:03 +02:00
eb348b3095 fix: update a row with a string key value 2020-08-13 13:22:04 +02:00
3c6e818ba0 fix: insert files via add row option 2020-08-13 12:42:19 +02:00
2f1dfdc654 feat: option to insert table rows 2020-08-12 18:12:30 +02:00
128a6cd9e8 style: UI improvements 2020-08-12 18:11:48 +02:00
5473858323 refactor: changed material design icon pack 2020-08-12 10:48:18 +02:00
7651d05b37 fix: window title not perfectly centered 2020-08-11 09:11:26 +02:00
c89c1ce83c docs: update README.md 2020-08-10 18:09:33 +02:00
771f8a2d68 fix: time and datetime precision 2020-08-10 18:07:16 +02:00
13b0816837 fix: table header not fixed on top when fast scrolling 2020-08-10 16:06:11 +02:00
a15e6249e1 chore: dependabot interval and minor changes in README.md 2020-08-07 17:27:25 +02:00
bbde2bd994 perf: improved scroll speed of result tables 2020-08-07 17:26:02 +02:00
949f7add8f chore(release): 0.0.4 2020-08-06 10:22:06 +02:00
968ec1edf7 ci: improvements on travis release config 2020-08-06 10:20:36 +02:00
a9d3a57281 Merge pull request #26 from EStarium/dependabot/npm_and_yarn/sass-loader-9.0.3
Bump sass-loader from 9.0.2 to 9.0.3
2020-08-06 08:54:45 +02:00
dependabot[bot]
f787439009 Bump sass-loader from 9.0.2 to 9.0.3
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 9.0.2 to 9.0.3.
- [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/v9.0.2...v9.0.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-06 06:11:22 +00:00
b03d461e21 build: fix to build configuration on Travis 2020-08-05 22:09:23 +02:00
5c05e3e9e9 refactor: moved queri fields mapping to main process 2020-08-05 22:08:20 +02:00
0089c0cbac feat: window title in app title bar 2020-08-05 13:53:30 +02:00
4fd72ec9e7 refactor: improvements to blob editor and code cleanup 2020-08-04 17:54:19 +02:00
712fe9f00d feat: blob fields edit/view/download 2020-08-03 18:07:08 +02:00
092e8a0732 style: 🎨 stylelint implementation 2020-07-31 18:16:28 +02:00
70908eb076 Minor Improvements 2020-07-31 15:45:32 +02:00
f8a0783769 Merge pull request #25 from EStarium/dependabot/npm_and_yarn/webpack-4.44.1
Bump webpack from 4.44.0 to 4.44.1
2020-07-31 08:10:30 +02:00
dependabot[bot]
cdf964ef07 Bump webpack from 4.44.0 to 4.44.1
Bumps [webpack](https://github.com/webpack/webpack) from 4.44.0 to 4.44.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v4.44.0...v4.44.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-31 05:22:32 +00:00
413b56916c Notifications timeout, large text editor 2020-07-30 19:12:29 +02:00
acd3310228 Merge pull request #24 from EStarium/dependabot/npm_and_yarn/vue-i18n-8.20.0
Bump vue-i18n from 8.19.0 to 8.20.0
2020-07-30 08:16:32 +02:00
dependabot[bot]
e014f81a9c Bump vue-i18n from 8.19.0 to 8.20.0
Bumps [vue-i18n](https://github.com/kazupon/vue-i18n) from 8.19.0 to 8.20.0.
- [Release notes](https://github.com/kazupon/vue-i18n/releases)
- [Changelog](https://github.com/kazupon/vue-i18n/blob/v8.x/CHANGELOG.md)
- [Commits](https://github.com/kazupon/vue-i18n/compare/v8.19.0...v8.20.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-30 05:32:52 +00:00
530361144e Merge branch 'master' of https://github.com/EStarium/antares 2020-07-29 15:56:32 +02:00
d69e411581 Moved to electron 9 2020-07-29 15:56:29 +02:00
9a3a0513e2 Merge pull request #23 from EStarium/dependabot/npm_and_yarn/electron-9.1.2
Bump electron from 8.4.0 to 9.1.2
2020-07-29 15:04:58 +02:00
dependabot[bot]
be8fa96c93 Bump electron from 8.4.0 to 9.1.2
Bumps [electron](https://github.com/electron/electron) from 8.4.0 to 9.1.2.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/master/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v8.4.0...v9.1.2)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-29 05:58:58 +00:00
587116bd20 Merge pull request #22 from EStarium/dependabot/npm_and_yarn/electron-updater-4.3.4
Bump electron-updater from 4.3.1 to 4.3.4
2020-07-28 09:53:45 +02:00
dependabot[bot]
145d1dd1ad Bump electron-updater from 4.3.1 to 4.3.4
Bumps [electron-updater](https://github.com/electron-userland/electron-builder) from 4.3.1 to 4.3.4.
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-28 07:50:07 +00:00
457410b65a Merge pull request #21 from EStarium/dependabot/npm_and_yarn/electron-builder-22.8.0
Bump electron-builder from 22.7.0 to 22.8.0
2020-07-28 08:09:09 +02:00
dependabot[bot]
b183eacae1 Bump electron-builder from 22.7.0 to 22.8.0
Bumps [electron-builder](https://github.com/electron-userland/electron-builder) from 22.7.0 to 22.8.0.
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/compare/v22.7.0...v22.8.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-28 05:20:18 +00:00
76cafdb69a Merge pull request #20 from ReverbOD/master
Update it-IT translation
2020-07-27 20:55:10 +02:00
Giuseppe Gigliotti
2d63ddb9c7 Update it-IT translation
fixed & updated italian translation
2020-07-27 20:12:48 +02:00
f12f00dbb7 Added .travis.yml 2020-07-27 16:27:33 +02:00
1ecb6d892c Merge pull request #19 from EStarium/dependabot/npm_and_yarn/webpack-4.44.0
Bump webpack from 4.43.0 to 4.44.0
2020-07-27 07:59:02 +02:00
fcd83f35d8 Merge pull request #18 from EStarium/dependabot/npm_and_yarn/vue-i18n-8.19.0
Bump vue-i18n from 8.18.2 to 8.19.0
2020-07-27 07:58:47 +02:00
dependabot[bot]
7a4d8286a6 Bump webpack from 4.43.0 to 4.44.0
Bumps [webpack](https://github.com/webpack/webpack) from 4.43.0 to 4.44.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v4.43.0...v4.44.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-27 05:41:17 +00:00
dependabot[bot]
dd5ec2c661 Bump vue-i18n from 8.18.2 to 8.19.0
Bumps [vue-i18n](https://github.com/kazupon/vue-i18n) from 8.18.2 to 8.19.0.
- [Release notes](https://github.com/kazupon/vue-i18n/releases)
- [Changelog](https://github.com/kazupon/vue-i18n/blob/v8.x/CHANGELOG.md)
- [Commits](https://github.com/kazupon/vue-i18n/compare/v8.18.2...v8.19.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-27 05:39:54 +00:00
3f0e5d3512 Fix typos 2020-07-26 15:10:01 +02:00
ac01511c10 Update version 2020-07-24 17:34:39 +02:00
4a83ae7e75 Update README.md 2020-07-24 14:05:14 +02:00
60132c94a1 Results table improvements 2020-07-24 13:26:56 +02:00
fdf5bef5ad Delete rows 2020-07-23 19:10:14 +02:00
67f55fbeb9 Merge pull request #17 from EStarium/dependabot/npm_and_yarn/mssql-6.2.1
Bump mssql from 6.2.0 to 6.2.1
2020-07-23 08:55:58 +02:00
dependabot[bot]
fd5a8548c7 Bump mssql from 6.2.0 to 6.2.1
Bumps [mssql](https://github.com/tediousjs/node-mssql) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/tediousjs/node-mssql/releases)
- [Changelog](https://github.com/tediousjs/node-mssql/blob/master/CHANGELOG.txt)
- [Commits](https://github.com/tediousjs/node-mssql/compare/v6.2.0...v6.2.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-23 05:45:56 +00:00
425ecf838d Row multi select 2020-07-22 18:30:52 +02:00
1a8a49eceb Merge pull request #15 from EStarium/dependabot/npm_and_yarn/codemirror-5.56.0
Bump codemirror from 5.55.0 to 5.56.0
2020-07-21 10:52:21 +02:00
dependabot[bot]
e9fffcc37e Bump codemirror from 5.55.0 to 5.56.0
Bumps [codemirror](https://github.com/codemirror/CodeMirror) from 5.55.0 to 5.56.0.
- [Release notes](https://github.com/codemirror/CodeMirror/releases)
- [Changelog](https://github.com/codemirror/CodeMirror/blob/master/CHANGELOG.md)
- [Commits](https://github.com/codemirror/CodeMirror/compare/5.55.0...5.56.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-21 05:56:20 +00:00
bba7c4af6f Merge pull request #14 from EStarium/dependabot/npm_and_yarn/electron-devtools-installer-3.1.1
Bump electron-devtools-installer from 3.1.0 to 3.1.1
2020-07-17 08:52:56 +02:00
dependabot[bot]
376d74c7dc Bump electron-devtools-installer from 3.1.0 to 3.1.1
Bumps [electron-devtools-installer](https://github.com/MarshallOfSound/electron-devtools-installer) from 3.1.0 to 3.1.1.
- [Release notes](https://github.com/MarshallOfSound/electron-devtools-installer/releases)
- [Changelog](https://github.com/MarshallOfSound/electron-devtools-installer/blob/master/.releaserc.json)
- [Commits](https://github.com/MarshallOfSound/electron-devtools-installer/compare/v3.1.0...v3.1.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-17 05:20:01 +00:00
307a32aff6 Starting implementation context on query Table 2020-07-10 19:51:36 +02:00
d1aaad276b Merge pull request #13 from EStarium/dependabot/npm_and_yarn/pg-8.3.0
Bump pg from 8.2.2 to 8.3.0
2020-07-10 08:57:31 +02:00
dependabot[bot]
b23b4f18b9 Bump pg from 8.2.2 to 8.3.0
Bumps [pg](https://github.com/brianc/node-postgres) from 8.2.2 to 8.3.0.
- [Release notes](https://github.com/brianc/node-postgres/releases)
- [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianc/node-postgres/compare/pg@8.2.2...pg@8.3.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-10 05:36:39 +00:00
334cfa9047 Merge pull request #12 from EStarium/dependabot/npm_and_yarn/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19
2020-07-09 08:40:12 +02:00
dependabot[bot]
2f6d16c730 Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-09 05:34:34 +00:00
b8e09e7003 Merge pull request #10 from EStarium/dependabot/npm_and_yarn/pg-8.2.2
Bump pg from 8.2.1 to 8.2.2
2020-07-08 09:02:13 +02:00
9caf424331 Merge pull request #11 from EStarium/dependabot/npm_and_yarn/sass-loader-9.0.2
Bump sass-loader from 9.0.1 to 9.0.2
2020-07-08 09:01:59 +02:00
dependabot[bot]
9e5e545478 Bump sass-loader from 9.0.1 to 9.0.2
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 9.0.1 to 9.0.2.
- [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/v9.0.1...v9.0.2)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-08 05:22:00 +00:00
dependabot[bot]
c9ab731bb4 Bump pg from 8.2.1 to 8.2.2
Bumps [pg](https://github.com/brianc/node-postgres) from 8.2.1 to 8.2.2.
- [Release notes](https://github.com/brianc/node-postgres/releases)
- [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianc/node-postgres/compare/pg@8.2.1...pg@8.2.2)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-08 05:18:40 +00:00
57832c43aa Merge pull request #8 from EStarium/dependabot/npm_and_yarn/vuedraggable-2.24.0
Bump vuedraggable from 2.23.2 to 2.24.0
2020-07-07 08:53:49 +02:00
dependabot[bot]
184363369b Bump vuedraggable from 2.23.2 to 2.24.0
Bumps [vuedraggable](https://github.com/SortableJS/Vue.Draggable) from 2.23.2 to 2.24.0.
- [Release notes](https://github.com/SortableJS/Vue.Draggable/releases)
- [Commits](https://github.com/SortableJS/Vue.Draggable/compare/v2.23.2...v2.24.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-07 05:38:42 +00:00
b221dd12ff Merge pull request #7 from EStarium/dependabot/npm_and_yarn/sass-loader-9.0.1
Bump sass-loader from 9.0.0 to 9.0.1
2020-07-06 08:56:30 +02:00
dependabot[bot]
1324464aa3 Bump sass-loader from 9.0.0 to 9.0.1
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 9.0.0 to 9.0.1.
- [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/v9.0.0...v9.0.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-06 05:40:58 +00:00
187b4f50f9 Datetime fields precision 2020-07-05 16:06:56 +02:00
262a476f50 Merge pull request #6 from EStarium/dependabot/npm_and_yarn/sass-loader-9.0.0
Bump sass-loader from 8.0.2 to 9.0.0
2020-07-03 08:51:07 +02:00
dependabot[bot]
f00c2600e5 Bump sass-loader from 8.0.2 to 9.0.0
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 8.0.2 to 9.0.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/v8.0.2...v9.0.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-03 06:17:52 +00:00
75a7db9c05 Merge branch 'master' of https://github.com/EStarium/antares 2020-07-02 19:17:28 +02:00
50cd852d01 Editable datetime fields 2020-07-02 19:17:25 +02:00
bb8cfa533b Merge pull request #5 from EStarium/dependabot/npm_and_yarn/spectre.css-0.5.9
Bump spectre.css from 0.5.8 to 0.5.9
2020-07-02 08:59:48 +02:00
dependabot[bot]
1ddc8b5dca Bump spectre.css from 0.5.8 to 0.5.9
Bumps [spectre.css](https://github.com/picturepan2/spectre) from 0.5.8 to 0.5.9.
- [Release notes](https://github.com/picturepan2/spectre/releases)
- [Changelog](https://github.com/picturepan2/spectre/blob/master/CHANGELOG.md)
- [Commits](https://github.com/picturepan2/spectre/compare/v0.5.8...v0.5.9)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-02 06:13:17 +00:00
8a4c628128 Minor fix 2020-06-30 18:20:07 +02:00
0076f146fa Merge branch 'master' of https://github.com/EStarium/antares 2020-06-30 18:15:01 +02:00
098d12f462 Update packages 2020-06-30 18:14:57 +02:00
b619285d94 Merge pull request #3 from EStarium/dependabot/npm_and_yarn/vuex-3.5.1
Bump vuex from 3.4.0 to 3.5.1
2020-06-30 09:02:46 +02:00
dependabot[bot]
a5fed1bb64 Bump vuex from 3.4.0 to 3.5.1
Bumps [vuex](https://github.com/vuejs/vuex) from 3.4.0 to 3.5.1.
- [Release notes](https://github.com/vuejs/vuex/releases)
- [Changelog](https://github.com/vuejs/vuex/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/vuejs/vuex/compare/v3.4.0...v3.5.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-30 06:13:53 +00:00
55ec03bd8e Merge pull request #2 from EStarium/dependabot/npm_and_yarn/electron-9.0.5
Bump electron from 8.3.4 to 9.0.5
2020-06-29 09:09:19 +02:00
dependabot[bot]
d1977fbd75 Bump electron from 8.3.4 to 9.0.5
Bumps [electron](https://github.com/electron/electron) from 8.3.4 to 9.0.5.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/master/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v8.3.4...v9.0.5)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-29 07:07:31 +00:00
3c20c0733c Update dependabot.yml 2020-06-29 09:05:39 +02:00
1d4a353d5c Create dependabot.yml 2020-06-29 09:04:39 +02:00
db71777cbc FIelds edit implemented 2020-06-28 15:31:16 +02:00
f350fe8203 Partial implementation of fields edit 2020-06-27 15:14:08 +02:00
28c3f87dd8 Create FUNDING.yml 2020-06-27 13:40:26 +02:00
cc8dbb8df7 Start implementing fields edit 2020-06-26 18:14:16 +02:00
85ac1bc85f Changed readme screenshot size 2020-06-20 22:00:10 +02:00
baea5c26e2 Text connection fix 2020-06-20 21:58:57 +02:00
87 changed files with 9655 additions and 15051 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: [fabio286]
patreon: fabio286
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: ['https://paypal.me/fabiodistasio']

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"

2
.gitignore vendored
View File

@@ -6,4 +6,4 @@ thumbs.db
.vscode .vscode
TODO.md TODO.md
*.txt *.txt
dev-app-update.yml package-lock.json

14
.stylelintrc Normal file
View File

@@ -0,0 +1,14 @@
{
"extends": [
"stylelint-config-standard"
],
"fix": true,
"formatter": "verbose",
"plugins": [
"stylelint-scss"
],
"rules": {
"at-rule-no-unknown": null
},
"syntax": "scss"
}

38
.travis.yml Normal file
View File

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

View File

@@ -1,7 +1,48 @@
# Changelog # Changelog
## [0.0.1-alpha]() - Coming Soon 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.0.6](https://github.com/EStarium/antares/compare/v0.0.5...v0.0.6) (2020-09-03)
### Features ### Features
- **Initial release:** * aliases support ([264de9c](https://github.com/EStarium/antares/commit/264de9c5686fb3a2ef22d96171f45b915ba1b34b))
* middle click to close tabs ([256ec76](https://github.com/EStarium/antares/commit/256ec765883fcf247355190827e943c76e95f13b))
* monaco-editor as query editor ([196a3e0](https://github.com/EStarium/antares/commit/196a3e0185a3d68b7c4ade8dbf187d2b216cc00b))
* sql suggestions in query editor ([8dc74ef](https://github.com/EStarium/antares/commit/8dc74ef2c335e8ae4a69f5d2651df65939139b1b))
* support to multiple query tabs ([d7ed00f](https://github.com/EStarium/antares/commit/d7ed00f4a3613da9015c9fc48c4d8062d292e416))
* tabs horizontal scroll with mouse wheel ([3a6ea76](https://github.com/EStarium/antares/commit/3a6ea76b93682ebd50908df7368c62c2c1e27958))
### Bug Fixes
* error when launching queries without a result from query tabs ([a1a6f51](https://github.com/EStarium/antares/commit/a1a6f51f2fba5140f5e3bd9cd6557c8a13dfaa2c))
* field name displayed instead of alias ([801a0de](https://github.com/EStarium/antares/commit/801a0de1865dea2a59ff057b7c2cc988cc9c87ed))
* wrong table height calc in some cases ([fd6d517](https://github.com/EStarium/antares/commit/fd6d5177efb6161aab01f9e108eda60df6c7d8c4))
### [0.0.5](https://github.com/EStarium/antares/compare/v0.0.4...v0.0.5) (2020-08-17)
### Features
* Badge on setting icon and update tab when update is available ([e8141b6](https://github.com/EStarium/antares/commit/e8141b632154f765ca73fa50b9b7120dc592ead0))
* Foreign key support in add/edit row ([0b6a188](https://github.com/EStarium/antares/commit/0b6a188d1959b80b4a66946cc79d2dd3853a428b))
* Option to insert table rows ([2f1dfdc](https://github.com/EStarium/antares/commit/2f1dfdc6543b4a6c1d595f0daa00c0832be49c77))
### Bug Fixes
* Insert files via add row option ([3c6e818](https://github.com/EStarium/antares/commit/3c6e818ba06f1b8b5db0ecf80c3b7498d6d2a841))
* Newline replaced with undefined inside queries ([59e4a79](https://github.com/EStarium/antares/commit/59e4a79f42076b3fce98a764e9ad6a01c674555b))
* Query result header didn't show just selected fields ([7bc1009](https://github.com/EStarium/antares/commit/7bc10092fe4823e03133e69e0a7bf86e44fde43b))
* Table header not fixed on top when fast scrolling ([13b0816](https://github.com/EStarium/antares/commit/13b0816837461119eaab79fdb7e92223e0950630))
* Time and datetime precision ([771f8a2](https://github.com/EStarium/antares/commit/771f8a2d682c64105231e3fef199f05150596298))
* Update a row with a string key value ([eb348b3](https://github.com/EStarium/antares/commit/eb348b3095b6905321b62eed6cea228374ebc3d1))
* Window title not perfectly centered ([7651d05](https://github.com/EStarium/antares/commit/7651d05b37970574d6ae4bdf75c20c69d59c1e6d))
* Wrong schema passed in query tab when a different database was selected ([6d0724d](https://github.com/EStarium/antares/commit/6d0724dc90cdebb10e0342d2c472bdd07aa345f8))
### [0.0.4](https://github.com/EStarium/antares/compare/v0.0.3-alpha...v0.0.4) (2020-08-06)
### Features
* Blob fields edit/view/download ([712fe9f](https://github.com/EStarium/antares/commit/712fe9f00d210db0f2317eca61e7fb648383e3fe))
* Window title in app title bar ([0089c0c](https://github.com/EStarium/antares/commit/0089c0cbac6caf0a6fd195849099f18713580228))

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2017 Jakub Szwacz Copyright (c) 2020 Fabio Di Stasio
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@@ -1,7 +1,71 @@
<p align="center"> <p align="center">
<img width="256" src="https://raw.githubusercontent.com/Fabio286/antares/master/docs/logo.png"> <img width="800" src="https://raw.githubusercontent.com/Fabio286/antares/master/docs/screen-alpha.png">
</p> </p>
# Antares # Antares SQL Client
🚧 Work in progress! 🚧 ![GitHub package.json version](https://img.shields.io/github/package-json/v/estarium/antares) [![Build Status](https://travis-ci.com/EStarium/antares.svg?branch=master)](https://travis-ci.com/EStarium/antares) ![GitHub All Releases](https://img.shields.io/github/downloads/estarium/antares/total) ![GitHub](https://img.shields.io/github/license/estarium/antares)
Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers.
My target is to support as many databases as possible, and all major operating systems, including the ARM versions.
**At the moment this application is an alpha, it lacks many features, and isn't ready as a main SQL client**. However i'm actively working on it, hoping to provide all essential features as soon as possible.
If you are curious to try this early state of Antares you can download and install the [latest release](https://github.com/EStarium/antares/releases), and stay tuned for updates.
## Philosophy
Why am I developing an SQL client when there are a lot of them on the market?
The main goal is to develop a totally free, cross platform and open source alternative, empowered by JavaScript's ecosystem.
An application created with minimalism and semplicity in mind, with features in the righ places, not hundreds of tiny buttons or submenu.
## How to contribute
- [Translate Antares](https://github.com/EStarium/antares/wiki/Translate-Antares)
## Roadmap
This is a roadmap with major features will come in near future.
- Improvements of query editor area.
- Multiple query tabs.
- Tables management (add/edit/delete).
- Stored procedures, views, schedulers and trigger support.
- Database tools.
- Context menu shortcuts.
- Keyboard shortcuts.
- More secure password storage.
- Query logs console.
- Fake data filler.
- Import/export and migration.
- Themes.
## Currently supported
### Databases
- [x] MySQL/MariaDB
- [ ] PostrgreSQL
- [ ] MSSQL
- [ ] SQLite
- [ ] OracleDB
- [ ] More...
### Operating Systems
#### • x86
- [x] Windows
- [x] Linux
- [x] MacOS (needs tests)
#### • ARM
- [ ] Windows
- [ ] Linux
- [ ] MacOS
## Translations
[Giuseppe Gigliotti](https://github.com/ReverbOD) / [Italian Translation](https://github.com/EStarium/antares/pull/20)
[Mohd-PH](https://github.com/Mohd-PH) / [Arabic Translation](https://github.com/EStarium/antares/pull/29)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 25 KiB

BIN
docs/screen-alpha.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

13749
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,69 +1,91 @@
{ {
"name": "antares", "name": "antares",
"productName": "Antares", "productName": "Antares",
"version": "0.0.1-alpha", "version": "0.0.6",
"description": "A cross-platform easy to use SQL client.", "description": "A cross-platform easy to use SQL client.",
"license": "MIT", "license": "MIT",
"repository": "https://github.com/Fabio286/antares.git", "repository": "https://github.com/EStarium/antares.git",
"scripts": { "scripts": {
"dev": "cross-env NODE_ENV=development electron-webpack dev", "dev": "cross-env NODE_ENV=development electron-webpack dev",
"compile": "electron-webpack", "compile": "electron-webpack",
"dist": "cross-env NODE_ENV=production npm run compile && electron-builder", "build": "cross-env NODE_ENV=production npm run compile && electron-builder",
"dist:dir": "cross-env NODE_ENV=production npm run dist --dir -c.compression=store -c.mac.identity=null", "release": "standard-version",
"publish": "build -p always" "release:pre": "npm run release -- --prerelease alpha",
"lint": "eslint ."
}, },
"author": "Fabio Di Stasio <fabio286@gmail.com>", "author": "Fabio Di Stasio <fabio286@gmail.com>",
"build": { "build": {
"appId": "com.estarium.antares", "appId": "com.estarium.antares",
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}", "artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
"files": [ "dmg": {
"static/*" "contents": [
] {
"x": 130,
"y": 220
},
{
"x": 410,
"y": 220,
"type": "link",
"path": "/Applications"
}
]
},
"linux": {
"target": [
"deb",
"AppImage"
],
"category": "Development"
}
}, },
"electronWebpack": { "electronWebpack": {
"whiteListedModules": [
"codemirror"
],
"renderer": { "renderer": {
"webpackConfig": "webpack.config.js" "webpackConfig": "webpack.config.js"
} }
}, },
"dependencies": { "dependencies": {
"codemirror": "^5.54.0", "@mdi/font": "^5.5.55",
"electron-log": "^4.2.1", "electron-log": "^4.2.4",
"electron-updater": "^4.3.1", "electron-updater": "^4.3.4",
"lodash": "^4.17.15", "lodash": "^4.17.20",
"material-design-icons": "^3.0.1", "moment": "^2.27.0",
"moment": "^2.26.0", "monaco-editor": "^0.20.0",
"mssql": "^6.2.0", "mssql": "^6.2.1",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"pg": "^8.2.1", "pg": "^8.3.2",
"source-map-support": "^0.5.16", "source-map-support": "^0.5.16",
"spectre.css": "^0.5.8", "spectre.css": "^0.5.9",
"vue-click-outside": "^1.1.0", "vue-click-outside": "^1.1.0",
"vue-i18n": "^8.18.2", "vue-i18n": "^8.21.0",
"vuedraggable": "^2.23.2", "vue-the-mask": "^0.11.1",
"vuex": "^3.4.0", "vuedraggable": "^2.24.0",
"vuex": "^3.5.1",
"vuex-persist": "^2.2.0" "vuex-persist": "^2.2.0"
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"electron": "^8.3.0", "electron": "^10.1.0",
"electron-builder": "^22.7.0", "electron-builder": "^22.8.0",
"electron-devtools-installer": "^3.0.0", "electron-devtools-installer": "^3.1.1",
"electron-webpack": "^2.8.2", "electron-webpack": "^2.8.2",
"electron-webpack-vue": "^2.4.0", "electron-webpack-vue": "^2.4.0",
"eslint": "^6.8.0", "eslint": "^7.7.0",
"eslint-config-standard": "^14.1.1", "eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "^2.21.1", "eslint-plugin-import": "^2.22.0",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1", "eslint-plugin-standard": "^4.0.1",
"eslint-plugin-vue": "^6.2.2", "eslint-plugin-vue": "^6.2.2",
"monaco-editor-webpack-plugin": "^1.9.0",
"node-sass": "^4.14.1", "node-sass": "^4.14.1",
"sass-loader": "^8.0.2", "sass-loader": "^10.0.1",
"standard-version": "^9.0.0",
"stylelint": "^13.6.1",
"stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0",
"vue": "^2.6.11", "vue": "^2.6.11",
"webpack": "^4.43.0" "webpack": "^4.44.1"
} }
} }

12
src/common/fieldTypes.js Normal file
View File

@@ -0,0 +1,12 @@
export const TEXT = ['char', 'varchar'];
export const LONG_TEXT = ['text', 'mediumtext', 'longtext'];
export const NUMBER = ['int', 'tinyint', 'smallint', 'mediumint', 'bigint', 'float', 'double', 'decimal'];
export const DATE = ['date'];
export const TIME = ['time'];
export const DATETIME = ['datetime', 'timestamp'];
export const BLOB = ['blob', 'mediumblob', 'longblob'];
export const BIT = ['bit'];

View File

@@ -0,0 +1,7 @@
'use strict';
export function bufferToBase64 (buf) {
const binstr = Array.prototype.map.call(buf, ch => {
return String.fromCharCode(ch);
}).join('');
return btoa(binstr);
}

View File

@@ -0,0 +1,12 @@
'use strict';
export function formatBytes (bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}

View File

@@ -1,13 +1,10 @@
export function uidGen () { 'use strict';
return Math.random().toString(36).substr(2, 9).toUpperCase();
};
export function mimeFromHex (hex) { export function mimeFromHex (hex) {
switch (hex.substring(0, 4)) { // 2 bytes switch (hex.substring(0, 4)) { // 2 bytes
case '424D': case '424D':
return { ext: 'bmp', mime: 'image/bmp' }; return { ext: 'bmp', mime: 'image/bmp' };
case '1F8B': case '1F8B':
return { ext: 'gz', mime: 'application/gzip' }; return { ext: 'tar.gz', mime: 'application/gzip' };
case '0B77': case '0B77':
return { ext: 'ac3', mime: 'audio/vnd.dolby.dd-raw' }; return { ext: 'ac3', mime: 'audio/vnd.dolby.dd-raw' };
case '7801': case '7801':
@@ -20,7 +17,7 @@ export function mimeFromHex (hex) {
default: default:
switch (hex.substring(0, 6)) { // 3 bytes switch (hex.substring(0, 6)) { // 3 bytes
case 'FFD8FF': case 'FFD8FF':
return { ext: 'jpj', mime: 'image/jpeg' }; return { ext: 'jpg', mime: 'image/jpeg' };
case '4949BC': case '4949BC':
return { ext: 'jxr', mime: 'image/vnd.ms-photo' }; return { ext: 'jxr', mime: 'image/vnd.ms-photo' };
case '425A68': case '425A68':
@@ -39,21 +36,11 @@ export function mimeFromHex (hex) {
return { ext: 'bpg', mime: 'image/bpg' }; return { ext: 'bpg', mime: 'image/bpg' };
case '4D4D002A': case '4D4D002A':
return { ext: 'tif', mime: 'image/tiff' }; return { ext: 'tif', mime: 'image/tiff' };
case '00000100':
return { ext: 'ico', mime: 'image/x-icon' };
default: default:
return { ext: '???', mime: 'unknown ' + hex }; return { ext: '', mime: 'unknown ' + hex };
} }
} }
} }
}; };
export function formatBytes (bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}

View File

@@ -0,0 +1,19 @@
/* eslint-disable no-useless-escape */
// eslint-disable-next-line no-control-regex
const regex = new RegExp(/[\0\x08\x09\x1a\n\r"'\\\%]/gm);
/**
* Escapes a string
*
* @param {String} string
* @returns {String}
*/
function sqlEscaper (string) {
return string.replace(regex, char => {
const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '"', '\\', '\\\\', '%'];
const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\'\'', '""', '\\\\', '\\\\\\\\', '\\%'];
return r[m.indexOf(char)] || char;
});
}
export { sqlEscaper };

View File

@@ -0,0 +1,8 @@
/**
* @export
* @param {String} [prefix]
* @returns {String} Unique ID
*/
export function uidGen (prefix) {
return (prefix ? `${prefix}:` : '') + Math.random().toString(36).substr(2, 9).toUpperCase();
};

View File

@@ -3,7 +3,6 @@
import { app, BrowserWindow, nativeImage } from 'electron'; import { app, BrowserWindow, nativeImage } from 'electron';
import * as path from 'path'; import * as path from 'path';
import { format as formatUrl } from 'url'; import { format as formatUrl } from 'url';
import ipcHandlers from './ipc-handlers'; import ipcHandlers from './ipc-handlers';
const isDevelopment = process.env.NODE_ENV !== 'production'; const isDevelopment = process.env.NODE_ENV !== 'production';
@@ -12,35 +11,29 @@ process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
// global reference to mainWindow (necessary to prevent window from being garbage collected) // global reference to mainWindow (necessary to prevent window from being garbage collected)
let mainWindow; let mainWindow;
function createMainWindow () { async function createMainWindow () {
const icon = require('../renderer/images/logo-32.png'); const icon = require('../renderer/images/logo-32.png');
const window = new BrowserWindow({ const window = new BrowserWindow({
width: 1600, width: 1024,
height: 1000, height: 800,
minHeight: 550,
minWidth: 900, minWidth: 900,
minHeight: 550,
title: 'Antares', title: 'Antares',
autoHideMenuBar: true, autoHideMenuBar: true,
icon: nativeImage.createFromDataURL(icon.default), icon: nativeImage.createFromDataURL(icon.default),
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
'web-security': false 'web-security': false,
enableRemoteModule: true,
spellcheck: false
}, },
frame: false, frame: false,
backgroundColor: '#1d1d1d' backgroundColor: '#1d1d1d'
}); });
if (isDevelopment)
window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`);
else {
window.loadURL(formatUrl({
pathname: path.join(__dirname, 'index.html'),
protocol: 'file',
slashes: true
}));
}
if (isDevelopment) { if (isDevelopment) {
await window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`);
const { default: installExtension, VUEJS_DEVTOOLS } = require('electron-devtools-installer'); const { default: installExtension, VUEJS_DEVTOOLS } = require('electron-devtools-installer');
window.webContents.openDevTools(); window.webContents.openDevTools();
@@ -52,6 +45,13 @@ function createMainWindow () {
console.log(err); console.log(err);
}); });
} }
else {
await window.loadURL(formatUrl({
pathname: path.join(__dirname, 'index.html'),
protocol: 'file',
slashes: true
}));
}
window.on('closed', () => { window.on('closed', () => {
mainWindow = null; mainWindow = null;
@@ -64,12 +64,12 @@ function createMainWindow () {
}); });
}); });
// Initialize ipcHandlers
ipcHandlers();
return window; return window;
}; };
// Initialize ipcHandlers
ipcHandlers();
// quit application when all windows are closed // quit application when all windows are closed
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
// on macOS it is common for applications to stay open until the user explicitly quits // on macOS it is common for applications to stay open until the user explicitly quits

View File

@@ -0,0 +1,7 @@
import { app, ipcMain } from 'electron';
export default () => {
ipcMain.on('close-app', () => {
app.exit();
});
};

View File

@@ -4,8 +4,8 @@ import { AntaresConnector } from '../libs/AntaresConnector';
import InformationSchema from '../models/InformationSchema'; import InformationSchema from '../models/InformationSchema';
import Generic from '../models/Generic'; import Generic from '../models/Generic';
export default (connections) => { export default connections => {
ipcMain.handle('testConnection', async (event, conn) => { ipcMain.handle('test-connection', async (event, conn) => {
const Connection = new AntaresConnector({ const Connection = new AntaresConnector({
client: conn.client, client: conn.client,
params: { params: {
@@ -28,7 +28,7 @@ export default (connections) => {
} }
}); });
ipcMain.handle('checkConnection', async (event, uid) => { ipcMain.handle('check-connection', async (event, uid) => {
return uid in connections; return uid in connections;
}); });
@@ -71,7 +71,7 @@ export default (connections) => {
} }
}); });
ipcMain.handle('rawQuery', async (event, { uid, query, schema }) => { ipcMain.handle('raw-query', async (event, { uid, query, schema }) => {
if (!query) return; if (!query) return;
try { try {
const result = await Generic.raw(connections[uid], query, schema); const result = await Generic.raw(connections[uid], query, schema);

View File

@@ -1,11 +1,13 @@
import connection from './connection'; import connection from './connection';
import structure from './structure'; import tables from './tables';
import updates from './updates'; import updates from './updates';
import application from './application';
const connections = {}; const connections = {};
export default () => { export default () => {
connection(connections); connection(connections);
structure(connections); tables(connections);
updates(); updates();
application();
}; };

View File

@@ -1,27 +0,0 @@
import { ipcMain } from 'electron';
import InformationSchema from '../models/InformationSchema';
import Generic from '../models/Generic';
// TODO: remap objects based on client
export default (connections) => {
ipcMain.handle('getTableColumns', async (event, { uid, schema, table }) => {
try {
const result = await InformationSchema.getTableColumns(connections[uid], schema, table);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('getTableData', async (event, { uid, schema, table }) => {
try {
const result = await Generic.getTableData(connections[uid], schema, table);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
};

View File

@@ -0,0 +1,77 @@
import { ipcMain } from 'electron';
import InformationSchema from '../models/InformationSchema';
import Tables from '../models/Tables';
// TODO: remap objects based on client
export default (connections) => {
ipcMain.handle('get-table-columns', async (event, { uid, schema, table }) => {
try {
const result = await InformationSchema.getTableColumns(connections[uid], schema, table);// TODO: uniform column properties
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('get-table-data', async (event, { uid, schema, table }) => {
try {
const result = await Tables.getTableData(connections[uid], schema, table);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('get-key-usage', async (event, { uid, schema, table }) => {
try {
const result = await InformationSchema.getKeyUsage(connections[uid], schema, table);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('update-table-cell', async (event, params) => {
try {
const result = await Tables.updateTableCell(connections[params.uid], params);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('delete-table-rows', async (event, params) => {
try {
const result = await Tables.deleteTableRows(connections[params.uid], params);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('insert-table-rows', async (event, params) => {
try {
await Tables.insertTableRows(connections[params.uid], params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('get-foreign-list', async (event, params) => {
try {
const results = await Tables.getForeignList(connections[params.uid], params);
return { status: 'success', response: results };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
};

View File

@@ -2,39 +2,40 @@ import { ipcMain } from 'electron';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
let mainWindow; let mainWindow;
autoUpdater.allowPrerelease = true;
export default () => { export default () => {
ipcMain.on('checkForUpdates', event => { ipcMain.on('check-for-updates', event => {
mainWindow = event; mainWindow = event;
autoUpdater.checkForUpdatesAndNotify().catch(() => { autoUpdater.checkForUpdatesAndNotify().catch(() => {
mainWindow.reply('checkFailed'); mainWindow.reply('check-failed');
}); });
}); });
ipcMain.on('restartToUpdate', () => { ipcMain.on('restart-to-update', () => {
autoUpdater.quitAndInstall(); autoUpdater.quitAndInstall();
}); });
// auto-updater events // auto-updater events
autoUpdater.on('checking-for-update', () => { autoUpdater.on('checking-for-update', () => {
mainWindow.reply('checkingForUpdate'); mainWindow.reply('checking-for-update');
}); });
autoUpdater.on('update-available', () => { autoUpdater.on('update-available', () => {
mainWindow.reply('updateAvailable'); mainWindow.reply('update-available');
}); });
autoUpdater.on('update-not-available', () => { autoUpdater.on('update-not-available', () => {
mainWindow.reply('updateNotAvailable'); mainWindow.reply('update-not-available');
}); });
autoUpdater.on('download-progress', (data) => { autoUpdater.on('download-progress', data => {
mainWindow.reply('downloadProgress', data); mainWindow.reply('download-progress', data);
}); });
autoUpdater.on('update-downloaded', () => { autoUpdater.on('update-downloaded', () => {
mainWindow.reply('updateDownloaded'); mainWindow.reply('update-downloaded');
}); });
autoUpdater.logger = require('electron-log'); autoUpdater.logger = require('electron-log');

View File

@@ -20,6 +20,7 @@ export class AntaresConnector {
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._logger = args.logger || console.log;
this._queryDefaults = { this._queryDefaults = {
schema: '', schema: '',
@@ -31,8 +32,8 @@ export class AntaresConnector {
limit: [], limit: [],
join: [], join: [],
update: [], update: [],
insert: [], insert: {},
delete: [] delete: false
}; };
this._query = Object.assign({}, this._queryDefaults); this._query = Object.assign({}, this._queryDefaults);
} }
@@ -73,14 +74,10 @@ export class AntaresConnector {
switch (this._client) { switch (this._client) {
case 'maria': case 'maria':
case 'mysql': case 'mysql':
if (!this._poolSize) { if (!this._poolSize)
const connection = mysql.createConnection(this._params); this._connection = mysql.createConnection(this._params);
this._connection = connection.promise();
}
else else
this._connection = mysql.createPool({ ...this._params, connectionLimit: this._poolSize }); this._connection = mysql.createPool({ ...this._params, connectionLimit: this._poolSize });
// this._connection = pool.promise();
break; break;
case 'mssql': { case 'mssql': {
const mssqlParams = { const mssqlParams = {
@@ -111,6 +108,17 @@ export class AntaresConnector {
return this; return this;
} }
into (table) {
this._query.from = table;
return this;
}
delete (table) {
this._query.delete = true;
this.from(table);
return this;
}
where (...args) { where (...args) {
this._query.where = [...this._query.where, ...args]; this._query.where = [...this._query.where, ...args];
return this; return this;
@@ -149,6 +157,26 @@ export class AntaresConnector {
return this.raw(sql); return this.raw(sql);
} }
/**
* @param {String | Array} args field = value
* @returns
* @memberof AntaresConnector
*/
update (...args) {
this._query.update = [...this._query.update, ...args];
return this;
}
/**
* @param {Object} obj field: value
* @returns
* @memberof AntaresConnector
*/
insert (obj) {
this._query.insert = { ...this._query.insert, ...obj };
return this;
}
/** /**
* @returns {string} SQL string * @returns {string} SQL string
* @memberof AntaresConnector * @memberof AntaresConnector
@@ -156,30 +184,37 @@ export class AntaresConnector {
getSQL () { getSQL () {
// SELECT // SELECT
const selectArray = this._query.select.reduce(this._reducer, []); const selectArray = this._query.select.reduce(this._reducer, []);
let selectRaw; let selectRaw = '';
switch (this._client) { if (selectArray.length) {
case 'maria': switch (this._client) {
case 'mysql': case 'maria':
selectRaw = selectArray.length ? `SELECT ${selectArray.join(', ')} ` : 'SELECT * '; case 'mysql':
break; selectRaw = selectArray.length ? `SELECT ${selectArray.join(', ')} ` : 'SELECT * ';
case 'mssql': { break;
const topRaw = this._query.limit.length ? ` TOP (${this._query.limit[0]}) ` : ''; case 'mssql': {
selectRaw = selectArray.length ? `SELECT${topRaw} ${selectArray.join(', ')} ` : 'SELECT * '; const topRaw = this._query.limit.length ? ` TOP (${this._query.limit[0]}) ` : '';
selectRaw = selectArray.length ? `SELECT${topRaw} ${selectArray.join(', ')} ` : 'SELECT * ';
}
break;
default:
break;
} }
break;
default:
break;
} }
// FROM // FROM
let fromRaw; 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';
switch (this._client) { switch (this._client) {
case 'maria': case 'maria':
case 'mysql': case 'mysql':
fromRaw = this._query.from ? `FROM ${this._query.schema ? `\`${this._query.schema}\`.` : ''}\`${this._query.from}\` ` : ''; fromRaw += this._query.from ? ` ${this._query.schema ? `\`${this._query.schema}\`.` : ''}\`${this._query.from}\` ` : '';
break; break;
case 'mssql': case 'mssql':
fromRaw = this._query.from ? `FROM ${this._query.schema ? `${this._query.schema}.` : ''}${this._query.from} ` : ''; fromRaw += this._query.from ? ` ${this._query.schema ? `${this._query.schema}.` : ''}${this._query.from} ` : '';
break; break;
default: default:
break; break;
@@ -187,8 +222,28 @@ export class AntaresConnector {
const whereArray = this._query.where.reduce(this._reducer, []); const whereArray = this._query.where.reduce(this._reducer, []);
const whereRaw = whereArray.length ? `WHERE ${whereArray.join(' AND ')} ` : ''; const whereRaw = whereArray.length ? `WHERE ${whereArray.join(' AND ')} ` : '';
const updateArray = this._query.update.reduce(this._reducer, []);
const updateRaw = updateArray.length ? `SET ${updateArray.join(', ')} ` : '';
let insertRaw = '';
if (Object.keys(this._query.insert).length) {
const fieldsList = [];
const valueList = [];
const fields = this._query.insert;
for (const key in fields) {
if (fields[key] === null) continue;
fieldsList.push(key);
valueList.push(fields[key]);
}
insertRaw = `(${fieldsList.join(', ')}) VALUES (${valueList.join(', ')}) `;
}
const groupByArray = this._query.groupBy.reduce(this._reducer, []); const groupByArray = this._query.groupBy.reduce(this._reducer, []);
const groupByRaw = groupByArray.length ? `GROUP BY ${groupByArray.join(', ')} ` : ''; const groupByRaw = groupByArray.length ? `GROUP BY ${groupByArray.join(', ')} ` : '';
const orderByArray = this._query.orderBy.reduce(this._reducer, []); const orderByArray = this._query.orderBy.reduce(this._reducer, []);
const orderByRaw = orderByArray.length ? `ORDER BY ${orderByArray.join(', ')} ` : ''; const orderByRaw = orderByArray.length ? `ORDER BY ${orderByArray.join(', ')} ` : '';
@@ -206,7 +261,7 @@ export class AntaresConnector {
break; break;
} }
return `${selectRaw}${fromRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}`; return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}${insertRaw}`;
} }
/** /**
@@ -225,20 +280,25 @@ export class AntaresConnector {
* @memberof AntaresConnector * @memberof AntaresConnector
*/ */
async raw (sql) { async raw (sql) {
if (process.env.NODE_ENV === 'development') console.log(sql); if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
switch (this._client) { switch (this._client) { // TODO: uniform fields with every client type, needed table name and fields array
case 'maria': case 'maria':
case 'mysql': { case 'mysql': {
const { rows, fields } = await new Promise((resolve, reject) => { const { rows, report, fields } = await new Promise((resolve, reject) => {
this._connection.query(sql, (err, rows, fields) => { this._connection.query(sql, (err, response, fields) => {
if (err) if (err)
reject(err); reject(err);
else else {
resolve({ rows, fields }); resolve({
rows: Array.isArray(response) ? response : false,
report: !Array.isArray(response) ? response : false,
fields
});
}
}); });
}); });
return { rows, fields }; return { rows, report, fields };
} }
case 'mssql': { case 'mssql': {
const results = await this._connection.request().query(sql); const results = await this._connection.request().query(sql);

View File

@@ -11,13 +11,4 @@ export default class {
} }
return connection.raw(query); return connection.raw(query);
} }
static async getTableData (connection, schema, table) {
return connection
.select('*')
.schema(schema)
.from(table)
.limit(1000)
.run();
}
} }

View File

@@ -13,13 +13,52 @@ export default class {
.run(); .run();
} }
static getTableColumns (connection, schema, table) { static async getTableColumns (connection, schema, table) {
return connection const { rows } = await connection
.select('*') .select('*')
.schema('information_schema') .schema('information_schema')
.from('COLUMNS') .from('COLUMNS')
.where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'` }) .where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'` })
.orderBy({ ORDINAL_POSITION: 'ASC' }) .orderBy({ ORDINAL_POSITION: 'ASC' })
.run(); .run();
return rows.map(field => {
return {
name: field.COLUMN_NAME,
key: field.COLUMN_KEY.toLowerCase(),
type: field.DATA_TYPE,
numPrecision: field.NUMERIC_PRECISION,
datePrecision: field.DATETIME_PRECISION,
charLength: field.CHARACTER_MAXIMUM_LENGTH,
isNullable: field.IS_NULLABLE,
default: field.COLUMN_DEFAULT,
charset: field.CHARACTER_SET_NAME,
collation: field.COLLATION_NAME,
autoIncrement: field.EXTRA.includes('auto_increment')
};
});
}
static async getKeyUsage (connection, schema, table) {
const { rows } = await connection
.select('*')
.schema('information_schema')
.from('KEY_COLUMN_USAGE')
.where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'`, REFERENCED_TABLE_NAME: 'IS NOT NULL' })
.run();
return rows.map(field => {
return {
schema: field.TABLE_SCHEMA,
table: field.TABLE_NAME,
column: field.COLUMN_NAME,
position: field.ORDINAL_POSITION,
constraintPosition: field.POSITION_IN_UNIQUE_CONSTRAINT,
constraintName: field.CONSTRAINT_NAME,
refSchema: field.REFERENCED_TABLE_SCHEMA,
refTable: field.REFERENCED_TABLE_NAME,
refColumn: field.REFERENCED_COLUMN_NAME
};
});
} }
} }

102
src/main/models/Tables.js Normal file
View File

@@ -0,0 +1,102 @@
'use strict';
import { sqlEscaper } from 'common/libs/sqlEscaper';
import { TEXT, LONG_TEXT, NUMBER, BLOB } from 'common/fieldTypes';
import fs from 'fs';
export default class {
static async getTableData (connection, schema, table) {
return connection
.select('*')
.schema(schema)
.from(table)
.limit(1000)
.run();
}
static async updateTableCell (connection, params) {
let escapedParam;
let reload = false;
const id = typeof params.id === 'number' ? params.id : `"${params.id}"`;
if (NUMBER.includes(params.type))
escapedParam = params.content;
else if ([...TEXT, ...LONG_TEXT].includes(params.type))
escapedParam = `"${sqlEscaper(params.content)}"`;
else if (BLOB.includes(params.type)) {
if (params.content) {
const fileBlob = fs.readFileSync(params.content);
escapedParam = `0x${fileBlob.toString('hex')}`;
reload = true;
}
else
escapedParam = '""';
}
else
escapedParam = `"${sqlEscaper(params.content)}"`;
await connection
.update({ [params.field]: `= ${escapedParam}` })
.schema(params.schema)
.from(params.table)
.where({ [params.primary]: `= ${id}` })
.run();
return { reload };
}
static async deleteTableRows (connection, params) {
return connection
.schema(params.schema)
.delete(params.table)
.where({ [params.primary]: `IN (${params.rows.join(',')})` })
.run();
}
static async insertTableRows (connection, params) {
const insertObj = {};
for (const key in params.row) {
const type = params.fields[key];
let escapedParam;
if (params.row[key] === null)
escapedParam = 'NULL';
else if (NUMBER.includes(type))
escapedParam = params.row[key];
else if ([...TEXT, ...LONG_TEXT].includes(type))
escapedParam = `"${sqlEscaper(params.row[key])}"`;
else if (BLOB.includes(type)) {
if (params.row[key]) {
const fileBlob = fs.readFileSync(params.row[key]);
escapedParam = `0x${fileBlob.toString('hex')}`;
}
else
escapedParam = '""';
}
else
escapedParam = `"${sqlEscaper(params.row[key])}"`;
insertObj[key] = escapedParam;
}
for (let i = 0; i < params.repeat; i++) {
await connection
.schema(params.schema)
.into(params.table)
.insert(insertObj)
.run();
}
}
static async getForeignList (connection, params) {
const query = connection
.select(`${params.column} AS foreignColumn`)
.schema(params.schema)
.from(params.table)
.orderBy('foreignColumn ASC');
if (params.description)
query.select(`LEFT(${params.description}, 20) AS foreignDescription`);
return query.run();
}
}

View File

@@ -4,7 +4,7 @@
<div id="window-content"> <div id="window-content">
<TheSettingBar /> <TheSettingBar />
<div id="main-content" class="container"> <div id="main-content" class="container">
<TheAppWelcome v-if="!connections.length" @newConn="showNewConnModal" /> <TheAppWelcome v-if="!connections.length" @new-conn="showNewConnModal" />
<div v-else class="columns col-gapless"> <div v-else class="columns col-gapless">
<Workspace <Workspace
v-for="connection in connections" v-for="connection in connections"
@@ -40,8 +40,7 @@ export default {
ModalSettings: () => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings') ModalSettings: () => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings')
}, },
data () { data () {
return { return {};
};
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
@@ -53,7 +52,7 @@ export default {
}) })
}, },
mounted () { mounted () {
ipcRenderer.send('checkForUpdates'); ipcRenderer.send('check-for-updates');
}, },
methods: { methods: {
...mapActions({ ...mapActions({
@@ -64,30 +63,30 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
html, html,
body{ body {
height: 100%; height: 100%;
} }
#wrapper{ #wrapper {
height: 100vh; height: 100vh;
position: relative; position: relative;
} }
#window-content{ #window-content {
display: flex; display: flex;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
#main-content { #main-content {
padding: 0; padding: 0;
justify-content: flex-start; justify-content: flex-start;
height: calc(100vh - #{$excluding-size}); height: calc(100vh - #{$excluding-size});
width: calc(100% - #{$settingbar-width}); width: calc(100% - #{$settingbar-width});
> .columns{ > .columns {
height: calc(100vh - #{$footer-height}); height: calc(100vh - #{$footer-height});
} }
} }
</style> </style>

View File

@@ -1,8 +1,8 @@
<template> <template>
<div class="modal modal-sm active"> <div class="modal active" :class="modalSizeClass">
<a class="modal-overlay" @click="hideModal" /> <a class="modal-overlay" @click="hideModal" />
<div class="modal-container"> <div class="modal-container">
<div v-if="hasHeader" class="modal-header"> <div v-if="hasHeader" class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<slot name="header" /> <slot name="header" />
</div> </div>
@@ -29,13 +29,13 @@
class="btn btn-primary mr-2" class="btn btn-primary mr-2"
@click="confirmModal" @click="confirmModal"
> >
{{ $t('word.confirm') }} {{ confirmText || $t('word.confirm') }}
</button> </button>
<button <button
class="btn btn-link" class="btn btn-link"
@click="hideModal" @click="hideModal"
> >
{{ $t('word.cancel') }} {{ cancelText || $t('word.cancel') }}
</button> </button>
</div> </div>
</div> </div>
@@ -45,6 +45,15 @@
<script> <script>
export default { export default {
name: 'BaseConfirmModal', name: 'BaseConfirmModal',
props: {
size: {
type: String,
validator: prop => ['small', 'medium', 'large'].includes(prop),
default: 'small'
},
confirmText: String,
cancelText: String
},
computed: { computed: {
hasHeader () { hasHeader () {
return !!this.$slots.header; return !!this.$slots.header;
@@ -54,6 +63,13 @@ export default {
}, },
hasDefault () { hasDefault () {
return !!this.$slots.default; return !!this.$slots.default;
},
modalSizeClass () {
if (this.size === 'small')
return 'modal-sm';
else if (this.size === 'large')
return 'modal-lg';
else return '';
} }
}, },
methods: { methods: {
@@ -70,7 +86,7 @@ export default {
</script> </script>
<style scoped> <style scoped>
.modal.modal-sm .modal-container{ .modal.modal-sm .modal-container {
padding: 0; padding: 0;
} }
</style> </style>

View File

@@ -31,63 +31,64 @@ export default {
}, },
methods: { methods: {
close () { close () {
this.$emit('closeContext'); this.$emit('close-context');
} }
} }
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.context{ .context {
display: flex;
color: $body-font-color;
font-size: 16px;
z-index: 400;
justify-content: center;
align-items: center;
overflow: hidden;
position: fixed;
height: 100vh;
right: 0;
top: 0;
left: 0;
bottom: 0;
pointer-events: none;
.context-container {
min-width: 100px;
max-width: 150px;
z-index: 10;
box-shadow: 0 0 1px 0 #000;
padding: 0;
background: #1d1d1d;
border-radius: 0.1rem;
display: flex; display: flex;
flex-direction: column;
position: absolute;
pointer-events: initial;
.context-element {
display: flex;
align-items: center;
padding: 0.1rem 0.3rem;
cursor: pointer;
&:hover {
background: $primary-color;
}
}
}
.context-overlay {
background: transparent;
bottom: 0;
cursor: default;
display: block;
left: 0;
position: absolute; position: absolute;
z-index: 400;
justify-content: center;
align-items: center;
overflow: hidden;
padding: 0.4rem;
position: fixed;
right: 0; right: 0;
top: 0; top: 0;
left: 0; }
bottom: 0; }
pointer-events: none;
.context-container{
min-width: 100px;
max-width: 150px;
z-index: 1;
box-shadow: 0px 0px 1px 0px #000;
padding: 0;
background: #1d1d1d;
border-radius: 0.1rem;
display: flex;
flex-direction: column;
position: absolute;
pointer-events: initial;
.context-element{
display: flex;
align-items: center;
padding: .1rem .3rem;
cursor: pointer;
&:hover{
background: $primary-color;
}
}
}
.context-overlay{
background: transparent;
bottom: 0;
cursor: default;
display: block;
left: 0;
position: absolute;
right: 0;
top: 0;
}
}
</style> </style>

View File

@@ -1,14 +1,15 @@
<template> <template>
<div class="toast mt-2" :class="notificationStatus.className"> <div class="toast mt-2" :class="notificationStatus.className">
<span class="p-vcentered text-left" :class="{'expanded': isExpanded}"> <span class="p-vcentered text-left" :class="{'expanded': isExpanded}">
<i class="material-icons mr-1">{{ notificationStatus.iconName }}</i> <i class="mdi mdi-24px mr-2" :class="notificationStatus.iconName" />
<span class="notification-message">{{ message }}</span> <span class="notification-message">{{ message }}</span>
</span> </span>
<i <i
v-if="isExpandable" v-if="isExpandable"
class="material-icons c-hand" class="mdi mdi-24px c-hand expand-btn"
:class="isExpanded ? 'mdi-chevron-up' : 'mdi-chevron-down'"
@click="toggleExpand" @click="toggleExpand"
>{{ isExpanded ? 'expand_less' : 'expand_more' }}</i> />
<button class="btn btn-clear ml-2" @click="hideToast" /> <button class="btn btn-clear ml-2" @click="hideToast" />
</div> </div>
</template> </template>
@@ -38,19 +39,19 @@ export default {
switch (this.status) { switch (this.status) {
case 'success': case 'success':
className = 'toast-success'; className = 'toast-success';
iconName = 'done'; iconName = 'mdi-check';
break; break;
case 'error': case 'error':
className = 'toast-error'; className = 'toast-error';
iconName = 'error'; iconName = 'mdi-alert-rhombus';
break; break;
case 'warning': case 'warning':
className = 'toast-warning'; className = 'toast-warning';
iconName = 'warning'; iconName = 'mdi-alert';
break; break;
case 'primary': case 'primary':
className = 'toast-primary'; className = 'toast-primary';
iconName = 'info_outline'; iconName = 'mdi-information-outline';
break; break;
} }
@@ -71,25 +72,28 @@ export default {
}; };
</script> </script>
<style scoped> <style scoped>
.toast{ .toast {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
user-select: text; user-select: text;
word-break: break-all; word-break: break-all;
width: fit-content; width: fit-content;
margin-left: auto; margin-left: auto;
} }
.notification-message{ .notification-message {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
display: inline-block; display: inline-block;
max-width: 30rem; max-width: 30rem;
user-select: none; }
}
.expanded .notification-message{ .expand-btn {
white-space: initial; align-items: initial;
} }
.expanded .notification-message {
white-space: initial;
}
</style> </style>

View File

@@ -34,19 +34,19 @@ export default {
switch (this.status) { switch (this.status) {
case 'success': case 'success':
className = 'toast-success'; className = 'toast-success';
iconTag = '<i class="material-icons mr-1">done</i>'; iconTag = '<i class="mdi mdi-24px mdi-check mr-1"></i>';
break; break;
case 'error': case 'error':
className = 'toast-error'; className = 'toast-error';
iconTag = '<i class="material-icons mr-1">error</i>'; iconTag = '<i class="mdi mdi-24px mdi-alert-rhombus mr-1"></i>';
break; break;
case 'warning': case 'warning':
className = 'toast-warning'; className = 'toast-warning';
iconTag = '<i class="material-icons mr-1">warning</i>'; iconTag = '<i class="mdi mdi-24px mdi-alert mr-1"></i>';
break; break;
case 'primary': case 'primary':
className = 'toast-primary'; className = 'toast-primary';
iconTag = '<i class="material-icons mr-1">info_outline</i>'; iconTag = '<i class="mdi mdi-24px mdi-information-outline mr-1"></i>';
break; break;
} }
@@ -70,10 +70,10 @@ export default {
}; };
</script> </script>
<style scoped> <style scoped>
.toast{ .toast {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
user-select: text; user-select: text;
word-break: break-all; word-break: break-all;
} }
</style> </style>

View File

@@ -21,47 +21,50 @@
</template> </template>
<script> <script>
// credits: https://github.com/xrado 👼
export default { export default {
name: 'BaseVirtualScroll', name: 'BaseVirtualScroll',
props: { props: {
items: Array, items: Array,
itemHeight: Number itemHeight: Number,
visibleHeight: Number,
scrollElement: {
type: HTMLDivElement,
default: null
}
}, },
data () { data () {
return { return {
topHeight: 0, topHeight: 0,
bottomHeight: 0, bottomHeight: 0,
visibleItems: [] visibleItems: [],
renderTimeout: null,
localScrollElement: null
}; };
}, },
mounted () { mounted () {
this._checkScrollPosition = this.checkScrollPosition.bind(this); this._checkScrollPosition = this.checkScrollPosition.bind(this);
this.checkScrollPosition(); this.localScrollElement = this.scrollElement ? this.scrollElement : this.$el;
this.$el.addEventListener('scroll', this._checkScrollPosition); this.updateWindow();
this.$el.addEventListener('wheel', this._checkScrollPosition); this.localScrollElement.addEventListener('scroll', this._checkScrollPosition);
}, },
beforeDestroy () { beforeDestroy () {
this.$el.removeEventListener('scroll', this._checkScrollPosition); this.localScrollElement.removeEventListener('scroll', this._checkScrollPosition);
this.$el.removeEventListener('wheel', this._checkScrollPosition);
}, },
methods: { methods: {
checkScrollPosition (e = {}) { checkScrollPosition (e) {
const el = this.$el; clearTimeout(this.renderTimeout);
// prevent parent scroll this.renderTimeout = setTimeout(() => {
if ((el.scrollTop === 0 && e.deltaY < 0) || (Math.abs(el.scrollTop - (el.scrollHeight - el.clientHeight)) <= 1 && e.deltaY > 0)) this.updateWindow(e);
e.preventDefault(); }, 200);
this.updateWindow(e);
}, },
updateWindow (e) { updateWindow (e) {
const visibleItemsCount = Math.ceil(this.$el.clientHeight / this.itemHeight); const visibleItemsCount = Math.ceil(this.visibleHeight / this.itemHeight);
const totalScrollHeight = this.items.length * this.itemHeight; const totalScrollHeight = this.items.length * this.itemHeight;
const offset = 50;
const scrollTop = this.localScrollElement.scrollTop;
const scrollTop = this.$el.scrollTop;
const offset = 5;
const firstVisibleIndex = Math.floor(scrollTop / this.itemHeight); const firstVisibleIndex = Math.floor(scrollTop / this.itemHeight);
const lastVisibleIndex = firstVisibleIndex + visibleItemsCount; const lastVisibleIndex = firstVisibleIndex + visibleItemsCount;
const firstCutIndex = Math.max(firstVisibleIndex - offset, 0); const firstCutIndex = Math.max(firstVisibleIndex - offset, 0);

View File

@@ -0,0 +1,89 @@
<template>
<select
ref="editField"
class="px-1"
@change="onChange"
@blur="$emit('blur')"
>
<option
v-for="row in foreignList"
:key="row.foreignColumn"
:value="row.foreignColumn"
:selected="row.foreignColumn === value"
>
{{ row.foreignColumn }} {{ 'foreignDescription' in row ? ` - ${row.foreignDescription}` : '' | cutText }}
</option>
</select>
</template>
<script>
import Tables from '@/ipc-api/Tables';
import { mapGetters, mapActions } from 'vuex';
import { TEXT, LONG_TEXT } from 'common/fieldTypes';
export default {
name: 'ForeignKeySelect',
filters: {
cutText (val) {
if (typeof val !== 'string') return val;
return val.length > 15 ? `${val.substring(0, 15)}...` : val;
}
},
props: {
value: [String, Number],
keyUsage: Object
},
data () {
return {
foreignList: []
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected'
})
},
async created () {
let firstTextField;
const params = {
uid: this.selectedWorkspace,
schema: this.keyUsage.refSchema,
table: this.keyUsage.refTable
};
try { // Field data
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success')
firstTextField = response.find(field => [...TEXT, ...LONG_TEXT].includes(field.type)).name || false;
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
try { // Foregn list
const { status, response } = await Tables.getForeignList({
...params,
column: this.keyUsage.refColumn,
description: firstTextField
});
if (status === 'success')
this.foreignList = response.rows;
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
onChange () {
this.$emit('update:value', this.$refs.editField.value);
}
}
};
</script>

View File

@@ -2,9 +2,11 @@
<div class="modal active modal-sm"> <div class="modal active modal-sm">
<a class="modal-overlay" /> <a class="modal-overlay" />
<div class="modal-container p-0"> <div class="modal-container p-0">
<div class="modal-header"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
{{ $t('word.credentials') }} <div class="d-flex">
<i class="mdi mdi-24px mdi-key-variant mr-1" /> {{ $t('word.credentials') }}
</div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div> </div>
@@ -63,7 +65,7 @@ export default {
}, },
methods: { methods: {
closeModal () { closeModal () {
this.$emit('closeAsking'); this.$emit('close-asking');
}, },
sendCredentials () { sendCredentials () {
this.$emit('credentials', this.credentials); this.$emit('credentials', this.credentials);
@@ -71,7 +73,3 @@ export default {
} }
}; };
</script> </script>
<style>
</style>

View File

@@ -2,9 +2,11 @@
<div class="modal active"> <div class="modal active">
<a class="modal-overlay c-hand" @click="closeModal" /> <a class="modal-overlay c-hand" @click="closeModal" />
<div class="modal-container"> <div class="modal-container">
<div class="modal-header"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
{{ $t('message.editConnection') }} <div class="d-flex">
<i class="mdi mdi-24px mdi-server mr-1" /> {{ $t('message.editConnection') }}
</div>
</div> </div>
<a class="btn btn-clear c-hand" @click="closeModal" /> <a class="btn btn-clear c-hand" @click="closeModal" />
</div> </div>
@@ -36,7 +38,7 @@
<option value="maria"> <option value="maria">
MariaDB MariaDB
</option> </option>
<option value="mssql"> <!-- <option value="mssql">
Microsoft SQL Microsoft SQL
</option> </option>
<option value="pg"> <option value="pg">
@@ -44,7 +46,7 @@
</option> </option>
<option value="oracledb"> <option value="oracledb">
Oracle DB Oracle DB
</option> </option> -->
</select> </select>
</div> </div>
</div> </div>
@@ -135,7 +137,7 @@
</div> </div>
<ModalAskCredentials <ModalAskCredentials
v-if="isAsking" v-if="isAsking"
@closeAsking="closeAsking" @close-asking="closeAsking"
@credentials="continueTest" @credentials="continueTest"
/> />
</div> </div>
@@ -230,7 +232,7 @@ export default {
</script> </script>
<style scoped> <style scoped>
.modal-container{ .modal-container {
max-width: 450px; max-width: 450px;
} }
</style> </style>

View File

@@ -2,9 +2,11 @@
<div class="modal active"> <div class="modal active">
<a class="modal-overlay c-hand" @click="closeModal" /> <a class="modal-overlay c-hand" @click="closeModal" />
<div class="modal-container"> <div class="modal-container">
<div class="modal-header"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
{{ $t('message.createNewConnection') }} <div class="d-flex">
<i class="mdi mdi-24px mdi-server-plus mr-1" /> {{ $t('message.createNewConnection') }}
</div>
</div> </div>
<a class="btn btn-clear c-hand" @click="closeModal" /> <a class="btn btn-clear c-hand" @click="closeModal" />
</div> </div>
@@ -40,7 +42,7 @@
<option value="maria"> <option value="maria">
MariaDB MariaDB
</option> </option>
<option value="mssql"> <!-- <option value="mssql">
Microsoft SQL Microsoft SQL
</option> </option>
<option value="pg"> <option value="pg">
@@ -48,7 +50,7 @@
</option> </option>
<option value="oracledb"> <option value="oracledb">
Oracle DB Oracle DB
</option> </option> -->
</select> </select>
</div> </div>
</div> </div>
@@ -139,7 +141,7 @@
</div> </div>
<ModalAskCredentials <ModalAskCredentials
v-if="isAsking" v-if="isAsking"
@closeAsking="closeAsking" @close-asking="closeAsking"
@credentials="continueTest" @credentials="continueTest"
/> />
</div> </div>
@@ -148,7 +150,7 @@
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import { uidGen } from 'common/libs/utilities'; import { uidGen } from 'common/libs/uidGen';
import ModalAskCredentials from '@/components/ModalAskCredentials'; import ModalAskCredentials from '@/components/ModalAskCredentials';
import BaseToast from '@/components/BaseToast'; import BaseToast from '@/components/BaseToast';
@@ -168,7 +170,7 @@ export default {
user: 'root', user: 'root',
password: '', password: '',
ask: false, ask: false,
uid: uidGen() uid: uidGen('C')
}, },
toast: { toast: {
status: '', status: '',
@@ -253,7 +255,7 @@ export default {
</script> </script>
<style scoped> <style scoped>
.modal-container{ .modal-container {
max-width: 450px; max-width: 450px;
} }
</style> </style>

View File

@@ -0,0 +1,326 @@
<template>
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-plus mr-1" /> {{ $t('message.addNewRow') }}
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body">
<div class="content">
<form class="form-horizontal">
<fieldset :disabled="isInserting">
<div
v-for="(field, key) in fields"
:key="field.name"
class="form-group"
>
<div class="col-4 col-sm-12">
<label class="form-label" :title="field.name">{{ field.name }}</label>
</div>
<div class="input-group col-8 col-sm-12">
<ForeignKeySelect
v-if="foreignKeys.includes(field.name)"
class="form-select"
:value.sync="localRow[field.name]"
:key-usage="getKeyUsage(field.name)"
:disabled="fieldsToExclude.includes(field.name)"
/>
<input
v-else-if="inputProps(field).mask"
v-model="localRow[field.name]"
v-mask="inputProps(field).mask"
class="form-input"
:type="inputProps(field).type"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
>
<input
v-else-if="inputProps(field).type === 'file'"
class="form-input"
type="file"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
@change="filesChange($event,field.name)"
>
<input
v-else
v-model="localRow[field.name]"
class="form-input"
:type="inputProps(field).type"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
>
<span class="input-group-addon" :class="`type-${field.type}`">
{{ field.type }} {{ fieldLength(field) | wrapNumber }}
</span>
<label class="form-checkbox ml-3" :title="$t('word.insert')">
<input
type="checkbox"
:checked="!field.autoIncrement"
@change.prevent="toggleFields($event, field)"
><i class="form-icon" />
</label>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div class="modal-footer text-light">
<div class="input-group col-3 tooltip tooltip-right" :data-tooltip="$t('message.numberOfInserts')">
<input
v-model="nInserts"
type="number"
class="form-input"
min="1"
:disabled="isInserting"
>
<span class="input-group-addon">
<i class="mdi mdi-24px mdi-repeat" />
</span>
</div>
<div>
<button
class="btn btn-primary mr-2"
:class="{'loading': isInserting}"
@click.stop="insertRows"
>
{{ $t('word.insert') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import moment from 'moment';
import { TEXT, LONG_TEXT, NUMBER, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { mask } from 'vue-the-mask';
import { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables';
import ForeignKeySelect from '@/components/ForeignKeySelect';
export default {
name: 'ModalNewTableRow',
components: {
ForeignKeySelect
},
directives: {
mask
},
filters: {
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
},
props: {
connection: Object,
tabUid: [String, Number]
},
data () {
return {
localRow: {},
fieldsToExclude: [],
nInserts: 1,
isInserting: false
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
getWorkspaceTab: 'workspaces/getWorkspaceTab'
}),
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
foreignKeys () {
return this.keyUsage.map(key => key.column);
},
fields () {
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).fields : [];
},
keyUsage () {
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).keyUsage : [];
}
},
watch: {
nInserts (val) {
if (!val || val < 1)
this.nInserts = 1;
}
},
mounted () {
const rowObj = {};
for (const field of this.fields) {
let fieldDefault;
if (field.default === 'NULL') fieldDefault = null;
else {
if (NUMBER.includes(field.type))
fieldDefault = +field.default;
if ([...TEXT, ...LONG_TEXT].includes(field.type))
fieldDefault = field.default ? field.default.substring(1, field.default.length - 1) : '';
if ([...TIME, ...DATE].includes(field.type))
fieldDefault = field.default;
if (DATETIME.includes(field.type)) {
if (field.default && field.default.toLowerCase().includes('current_timestamp')) {
let datePrecision = '';
for (let i = 0; i < field.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
fieldDefault = moment().format(`YYYY-MM-DD HH:mm:ss${datePrecision}`);
}
}
}
rowObj[field.name] = fieldDefault;
if (field.autoIncrement)// Disable by default auto increment fields
this.fieldsToExclude = [...this.fieldsToExclude, field.name];
}
this.localRow = { ...rowObj };
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
async insertRows () {
this.isInserting = true;
const rowToInsert = this.localRow;
Object.keys(rowToInsert).forEach(key => {
if (this.fieldsToExclude.includes(key))
delete rowToInsert[key];
if (typeof rowToInsert[key] === 'undefined')
delete rowToInsert[key];
});
const fieldTypes = {};
this.fields.forEach(field => {
fieldTypes[field.name] = field.type;
});
try {
const { status, response } = await Tables.insertTableRows({
uid: this.selectedWorkspace,
schema: this.workspace.breadcrumbs.schema,
table: this.workspace.breadcrumbs.table,
row: rowToInsert,
repeat: this.nInserts,
fields: fieldTypes
});
if (status === 'success') {
this.closeModal();
this.$emit('reload');
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isInserting = false;
},
closeModal () {
this.$emit('hide');
},
fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
return field.numPrecision || field.datePrecision || field.charLength || 0;
},
inputProps (field) {
if ([...TEXT, ...LONG_TEXT].includes(field.type))
return { type: 'text', mask: false };
if (NUMBER.includes(field.type))
return { type: 'number', mask: false };
if (TIME.includes(field.type)) {
let timeMask = '##:##:##';
const precision = this.fieldLength(field);
for (let i = 0; i < precision; i++)
timeMask += i === 0 ? '.#' : '#';
return { type: 'text', mask: timeMask };
}
if (DATE.includes(field.type))
return { type: 'text', mask: '####-##-##' };
if (DATETIME.includes(field.type)) {
let datetimeMask = '####-##-## ##:##:##';
const precision = this.fieldLength(field);
for (let i = 0; i < precision; i++)
datetimeMask += i === 0 ? '.#' : '#';
return { type: 'text', mask: datetimeMask };
}
if (BLOB.includes(field.type))
return { type: 'file', mask: false };
if (BIT.includes(field.type))
return { type: 'text', mask: false };
return { type: 'text', mask: false };
},
toggleFields (event, field) {
if (event.target.checked)
this.fieldsToExclude = this.fieldsToExclude.filter(f => f !== field.name);
else
this.fieldsToExclude = [...this.fieldsToExclude, field.name];
},
filesChange (event, field) {
const { files } = event.target;
if (!files.length) return;
this.localRow[field] = files[0].path;
},
getKeyUsage (keyName) {
return this.keyUsage.find(key => key.column === keyName);
}
}
};
</script>
<style scoped>
.modal-container {
max-width: 500px;
}
.form-label {
overflow: hidden;
white-space: normal;
text-overflow: ellipsis;
}
.input-group-addon {
display: flex;
align-items: center;
}
.modal-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@@ -2,9 +2,12 @@
<div id="settings" class="modal active"> <div id="settings" class="modal active">
<a class="modal-overlay c-hand" @click="closeModal" /> <a class="modal-overlay c-hand" @click="closeModal" />
<div class="modal-container"> <div class="modal-container">
<div class="modal-header"> <div class="modal-header pl-2">
<div class="modal-title h5"> <div class="modal-title h6">
{{ $t('word.settings') }} <div class="d-flex">
<i class="mdi mdi-24px mdi-cog mr-1" />
{{ $t('word.settings') }}
</div>
</div> </div>
<a class="btn btn-clear c-hand" @click="closeModal" /> <a class="btn btn-clear c-hand" @click="closeModal" />
</div> </div>
@@ -31,7 +34,7 @@
:class="{'active': selectedTab === 'update'}" :class="{'active': selectedTab === 'update'}"
@click="selectTab('update')" @click="selectTab('update')"
> >
<a class="c-hand" :class="{'badge': isUpdate}">{{ $t('word.update') }}</a> <a class="c-hand" :class="{'badge badge-update': hasUpdates}">{{ $t('word.update') }}</a>
</li> </li>
<li <li
class="tab-item" class="tab-item"
@@ -45,11 +48,11 @@
<div v-if="selectedTab === 'general'" class="panel-body py-4"> <div v-if="selectedTab === 'general'" class="panel-body py-4">
<form class="form-horizontal"> <form class="form-horizontal">
<div class="col-6 col-sm-12"> <div class="col-8 col-sm-12">
<div class="form-group"> <div class="form-group mb-4">
<div class="col-6 col-sm-12"> <div class="col-6 col-sm-12">
<label class="form-label"> <label class="form-label">
<i class="material-icons md-18 mr-1">translate</i> <i class="mdi mdi-18px mdi-translate mr-1" />
{{ $t('word.language') }}: {{ $t('word.language') }}:
</label> </label>
</div> </div>
@@ -69,12 +72,33 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group">
<div class="col-6 col-sm-12">
<label class="form-label">
{{ $t('message.notificationsTimeout') }}:
</label>
</div>
<div class="col-6 col-sm-12">
<div class="input-group">
<input
v-model="localTimeout"
class="form-input"
type="number"
min="1"
@focusout="checkNotificationsTimeout"
>
<span class="input-group-addon">{{ $t('word.seconds') }}</span>
</div>
</div>
</div>
</div> </div>
</form> </form>
</div> </div>
<div v-if="selectedTab === 'themes'" class="panel-body py-4"> <div v-if="selectedTab === 'themes'" class="panel-body py-4">
<!-- --> <div class="text-center">
<p>In future releases</p>
</div>
</div> </div>
<div v-if="selectedTab === 'update'" class="panel-body py-4"> <div v-if="selectedTab === 'update'" class="panel-body py-4">
@@ -87,7 +111,7 @@
<h4>{{ appName }}</h4> <h4>{{ appName }}</h4>
<p> <p>
{{ $t('word.version') }}: {{ appVersion }}<br> {{ $t('word.version') }}: {{ appVersion }}<br>
<a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares')">GitHub</a><br> <a class="c-hand" @click="openOutside('https://github.com/EStarium/antares')">GitHub</a><br>
<small>{{ $t('message.madeWithJS') }}</small> <small>{{ $t('message.madeWithJS') }}</small>
</p> </p>
</div> </div>
@@ -111,8 +135,8 @@ export default {
}, },
data () { data () {
return { return {
isUpdate: false,
localLocale: null, localLocale: null,
localTimeout: null,
selectedTab: 'general' selectedTab: 'general'
}; };
}, },
@@ -121,7 +145,9 @@ export default {
appName: 'application/appName', appName: 'application/appName',
appVersion: 'application/appVersion', appVersion: 'application/appVersion',
selectedSettingTab: 'application/selectedSettingTab', selectedSettingTab: 'application/selectedSettingTab',
selectedLocale: 'settings/getLocale' selectedLocale: 'settings/getLocale',
notificationsTimeout: 'settings/getNotificationsTimeout',
updateStatus: 'application/getUpdateStatus'
}), }),
locales () { locales () {
const locales = []; const locales = [];
@@ -129,46 +155,61 @@ export default {
locales.push({ code: locale, name: localesNames[locale] }); locales.push({ code: locale, name: localesNames[locale] });
return locales; return locales;
},
hasUpdates () {
return ['available', 'downloading', 'downloaded'].includes(this.updateStatus);
} }
}, },
created () { created () {
this.localLocale = this.selectedLocale; this.localLocale = this.selectedLocale;
this.localTimeout = this.notificationsTimeout;
this.selectedTab = this.selectedSettingTab; this.selectedTab = this.selectedSettingTab;
}, },
methods: { methods: {
...mapActions({ ...mapActions({
closeModal: 'application/hideSettingModal', closeModal: 'application/hideSettingModal',
changeLocale: 'settings/changeLocale' changeLocale: 'settings/changeLocale',
updateNotificationsTimeout: 'settings/updateNotificationsTimeout'
}), }),
selectTab (tab) { selectTab (tab) {
this.selectedTab = tab; this.selectedTab = tab;
}, },
openOutside (link) { openOutside (link) {
shell.openExternal(link); shell.openExternal(link);
},
checkNotificationsTimeout () {
if (!this.localTimeout)
this.localTimeout = 10;
this.updateNotificationsTimeout(+this.localTimeout);
} }
} }
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
#settings{ #settings {
.modal-body{ .modal-body {
overflow: hidden; overflow: hidden;
.panel-body{ .panel-body {
height: calc(70vh - 70px); height: calc(70vh - 70px);
overflow: auto; overflow: auto;
} }
.badge::after{ .badge::after {
background: #32b643; background: #32b643;
} }
.form-label{ .badge-update::after {
display: flex; bottom: initial;
align-items: center; background: $primary-color;
} }
}
.form-label {
display: flex;
align-items: center;
}
}
} }
</style> </style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="empty"> <div class="empty">
<div class="empty-icon"> <div class="empty-icon">
<i class="material-icons md-48">system_update_alt</i> <i class="mdi mdi-48px mdi-cloud-download" />
</div> </div>
<p class="empty-title h5"> <p class="empty-title h5">
{{ updateMessage }} {{ updateMessage }}
@@ -68,17 +68,17 @@ export default {
}, },
methods: { methods: {
checkForUpdates () { checkForUpdates () {
ipcRenderer.send('checkForUpdates'); ipcRenderer.send('check-for-updates');
}, },
restartToUpdate () { restartToUpdate () {
ipcRenderer.send('restartToUpdate'); ipcRenderer.send('restart-to-update');
} }
} }
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.empty{ .empty {
color: $body-font-color; color: $body-font-color;
} }
</style> </style>

View File

@@ -1,25 +1,15 @@
<template> <template>
<div class="editor-wrapper"> <div class="editor-wrapper">
<textarea <div ref="editor" class="editor" />
ref="codemirror"
:options="cmOptions"
/>
</div> </div>
</template> </template>
<script> <script>
import CodeMirror from 'codemirror';
import 'codemirror/lib/codemirror.css'; import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import 'codemirror/theme/material-darker.css'; import { completionItemProvider } from '@/suggestions/sql';
import 'codemirror/mode/sql/sql';
import 'codemirror/addon/edit/closebrackets';
import 'codemirror/addon/selection/active-line';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/hint/show-hint.css';
import 'codemirror/addon/hint/sql-hint';
CodeMirror.defineOption('sql-hint'); monaco.languages.registerCompletionItemProvider('sql', completionItemProvider(monaco));
export default { export default {
name: 'QueryEditor', name: 'QueryEditor',
@@ -28,62 +18,53 @@ export default {
}, },
data () { data () {
return { return {
cminstance: null, editor: null
content: '',
cmOptions: {
tabSize: 3,
smartIndent: true,
styleActiveLine: true,
lineNumbers: true,
line: true,
mode: 'text/x-sql',
theme: 'material-darker',
extraKeys: {
'Ctrl-Space': 'autocomplete'
},
hintOptions: {
tables: {
users: ['name', 'score', 'birthDate'],
countries: ['name', 'population', 'size']
}
},
autoCloseBrackets: true
}
}; };
}, },
mounted () { mounted () {
this.initialize(); this.editor = monaco.editor.create(this.$refs.editor, {
}, value: this.value,
methods: { language: 'sql',
initialize () { theme: 'vs-dark',
this.cminstance = CodeMirror.fromTextArea(this.$refs.codemirror, this.cmOptions); autoIndent: true,
this.cminstance.setValue(this.value || this.content); minimap: {
enabled: false
},
contextmenu: false,
wordBasedSuggestions: true,
acceptSuggestionOnEnter: 'smart',
quickSuggestions: true
});
this.cminstance.on('change', cm => { this.editor.onDidChangeModelContent(e => {
this.content = cm.getValue(); const content = this.editor.getValue();
this.$emit('input', this.content); this.$emit('update:value', content);
}); });
} },
beforeDestroy () {
this.editor && this.editor.dispose();
} }
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.editor-wrapper{ .editor-wrapper {
border-bottom: 1px solid #444444; border-bottom: 1px solid #444;
}
.CodeMirror{ .editor {
height: 200px; height: 200px;
width: 100%;
}
}
.CodeMirror-scroll{ .CodeMirror {
max-width: 100%; .CodeMirror-scroll {
} max-width: 100%;
}
.CodeMirror-line { .CodeMirror-line {
word-break: break-word!important; word-break: break-word !important;
white-space: pre-wrap!important; white-space: pre-wrap !important;
word-break: normal; }
} }
}
</style> </style>

View File

@@ -1,13 +1,13 @@
<template> <template>
<BaseContextMenu <BaseContextMenu
:context-event="contextEvent" :context-event="contextEvent"
@closeContext="$emit('closeContext')" @close-context="$emit('close-context')"
> >
<div class="context-element" @click="showEditModal(contextConnection)"> <div class="context-element" @click="showEditModal(contextConnection)">
<i class="material-icons md-18 text-light pr-1">edit</i> {{ $t('word.edit') }} <i class="mdi mdi-18px mdi-pencil text-light pr-1" /> {{ $t('word.edit') }}
</div> </div>
<div class="context-element" @click="showConfirmModal"> <div class="context-element" @click="showConfirmModal">
<i class="material-icons md-18 text-light pr-1">delete</i> {{ $t('word.delete') }} <i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('word.delete') }}
</div> </div>
<ConfirmModal <ConfirmModal
@@ -16,7 +16,9 @@
@hide="hideConfirmModal" @hide="hideConfirmModal"
> >
<template :slot="'header'"> <template :slot="'header'">
{{ $t('message.deleteConnection') }} <div class="d-flex">
<i class="mdi mdi-24px mdi-server-remove mr-1" /> {{ $t('message.deleteConnection') }}
</div>
</template> </template>
<div :slot="'body'"> <div :slot="'body'">
<div class="mb-2"> <div class="mb-2">
@@ -69,7 +71,3 @@ export default {
} }
}; };
</script> </script>
<style>
</style>

View File

@@ -2,7 +2,7 @@
<div class="columns"> <div class="columns">
<div class="column col-12 empty text-light"> <div class="column col-12 empty text-light">
<div class="empty-icon"> <div class="empty-icon">
<i class="material-icons md-48">mood</i> <i class="mdi mdi-48px mdi-emoticon" />
</div> </div>
<p class="empty-title h5"> <p class="empty-title h5">
{{ $t('message.appWelcome') }} {{ $t('message.appWelcome') }}
@@ -11,7 +11,7 @@
{{ $t('message.appFirstStep') }} {{ $t('message.appFirstStep') }}
</p> </p>
<div class="empty-action"> <div class="empty-action">
<button class="btn btn-primary" @click="$emit('newConn')"> <button class="btn btn-primary" @click="$emit('new-conn')">
{{ $t('message.createConnection') }} {{ $t('message.createConnection') }}
</button> </button>
</div> </div>
@@ -26,12 +26,12 @@ export default {
</script> </script>
<style scoped> <style scoped>
.empty{ .empty {
height: 100%; height: 100%;
border-radius: 0; border-radius: 0;
background: transparent; background: transparent;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
} }
</style> </style>

View File

@@ -3,7 +3,7 @@
<div class="footer-left-elements"> <div class="footer-left-elements">
<ul class="footer-elements"> <ul class="footer-elements">
<li class="footer-element"> <li class="footer-element">
<i class="material-icons md-18 mr-1">memory</i> <i class="mdi mdi-18px mdi-memory mr-1" />
<small>{{ appVersion }}</small> <small>{{ appVersion }}</small>
</li> </li>
</ul> </ul>
@@ -11,15 +11,15 @@
<div class="footer-right-elements"> <div class="footer-right-elements">
<ul class="footer-elements"> <ul class="footer-elements">
<li class="footer-element footer-link"> <li class="footer-element footer-link" @click="openOutside('https://github.com/sponsors/Fabio286')">
<i class="material-icons md-18 mr-1">favorite</i> <i class="mdi mdi-18px mdi-coffee mr-1" />
<small>{{ $t('word.donate') }}</small> <small>{{ $t('word.donate') }}</small>
</li> </li>
<li class="footer-element footer-link"> <li class="footer-element footer-link" @click="openOutside('https://github.com/EStarium/antares/issues')">
<i class="material-icons md-18">bug_report</i> <i class="mdi mdi-18px mdi-bug" />
</li> </li>
<li class="footer-element footer-link" @click="showSettingModal('about')"> <li class="footer-element footer-link" @click="showSettingModal('about')">
<i class="material-icons md-18">info_outline</i> <i class="mdi mdi-18px mdi-information-outline" />
</li> </li>
</ul> </ul>
</div> </div>
@@ -28,6 +28,7 @@
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
const { shell } = require('electron');
export default { export default {
name: 'TheFooter', name: 'TheFooter',
@@ -40,47 +41,50 @@ export default {
methods: { methods: {
...mapActions({ ...mapActions({
showSettingModal: 'application/showSettingModal' showSettingModal: 'application/showSettingModal'
}) }),
openOutside (link) {
shell.openExternal(link);
}
} }
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
#footer{ #footer {
height: $footer-height; height: $footer-height;
display: flex;
justify-content: space-between;
align-items: center;
background: $primary-color;
padding: 0 0.2rem;
position: fixed;
bottom: 0;
left: 0;
right: 0;
box-shadow: 0 0 1px 0 #000;
.footer-elements {
list-style: none;
margin: 0;
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
background: $primary-color;
padding: 0 .2rem;
position: fixed;
bottom: 0;
left: 0;
right: 0;
box-shadow: 0 0 1px 0px #000;
.footer-elements{ .footer-element {
list-style: none; height: $footer-height;
margin: 0; display: flex;
display: flex; align-items: center;
align-items: center; padding: 0 0.4rem;
margin: 0;
.footer-element{ &.footer-link {
height: $footer-height; cursor: pointer;
display: flex; transition: background 0.2s;
align-items: center;
padding: 0 .4rem;
margin: 0;
&.footer-link{ &:hover {
cursor: pointer; background: rgba($color: #fff, $alpha: 0.1);
transition: background .2s; }
}
&:hover{
background: rgba($color: #fff, $alpha: .1);
}
}
}
} }
} }
}
</style> </style>

View File

@@ -1,5 +1,9 @@
<template> <template>
<div id="notifications-board"> <div
id="notifications-board"
@mouseenter="clearTimeouts"
@mouseleave="rearmTimeouts"
>
<transition-group name="slide-fade"> <transition-group name="slide-fade">
<BaseNotification <BaseNotification
v-for="notification in latestNotifications" v-for="notification in latestNotifications"
@@ -21,27 +25,60 @@ export default {
components: { components: {
BaseNotification BaseNotification
}, },
data () {
return {
timeouts: {}
};
},
computed: { computed: {
...mapGetters({ ...mapGetters({
notifications: 'notifications/getNotifications' notifications: 'notifications/getNotifications',
notificationsTimeout: 'settings/getNotificationsTimeout'
}), }),
latestNotifications () { latestNotifications () {
return this.notifications.slice(0, 10); return this.notifications.slice(0, 10);
} }
}, },
watch: {
notifications: {
deep: true,
handler: function (notification) {
if (notification.length) {
this.timeouts[notification[0].uid] = setTimeout(() => {
this.removeNotification(notification[0].uid);
delete this.timeouts[notification.uid];
}, this.notificationsTimeout * 1000);
}
}
}
},
methods: { methods: {
...mapActions({ ...mapActions({
removeNotification: 'notifications/removeNotification' removeNotification: 'notifications/removeNotification'
}) }),
clearTimeouts () {
for (const uid in this.timeouts) {
clearTimeout(this.timeouts[uid]);
delete this.timeouts[uid];
}
},
rearmTimeouts () {
for (const notification of this.notifications) {
this.timeouts[notification.uid] = setTimeout(() => {
this.removeNotification(notification.uid);
delete this.timeouts[notification.uid];
}, this.notificationsTimeout * 1000);
}
}
} }
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
#notifications-board{ #notifications-board {
position: absolute; position: absolute;
z-index: 9; z-index: 999;
right: 1rem; right: 1rem;
bottom: 1rem; bottom: 1rem;
} }
</style> </style>

View File

@@ -5,7 +5,7 @@
v-if="isContext" v-if="isContext"
:context-event="contextEvent" :context-event="contextEvent"
:context-connection="contextConnection" :context-connection="contextConnection"
@closeContext="isContext = false" @close-context="isContext = false"
/> />
<ul class="settingbar-elements"> <ul class="settingbar-elements">
<draggable v-model="connections"> <draggable v-model="connections">
@@ -28,7 +28,7 @@
@click="showNewConnModal" @click="showNewConnModal"
@mouseover.self="tooltipPosition" @mouseover.self="tooltipPosition"
> >
<i class="settingbar-element-icon material-icons text-light">add</i> <i class="settingbar-element-icon mdi mdi-24px mdi-plus text-light" />
<span class="ex-tooltip-content">{{ $t('message.addConnection') }}</span> <span class="ex-tooltip-content">{{ $t('message.addConnection') }}</span>
</li> </li>
</ul> </ul>
@@ -37,7 +37,7 @@
<div class="settingbar-bottom-elements"> <div class="settingbar-bottom-elements">
<ul class="settingbar-elements"> <ul class="settingbar-elements">
<li class="settingbar-element btn btn-link ex-tooltip" @click="showSettingModal('general')"> <li class="settingbar-element btn btn-link ex-tooltip" @click="showSettingModal('general')">
<i class="settingbar-element-icon material-icons text-light">settings</i> <i class="settingbar-element-icon mdi mdi-24px mdi-cog text-light" :class="{' badge badge-update': hasUpdates}" />
<span class="ex-tooltip-content">{{ $t('word.settings') }}</span> <span class="ex-tooltip-content">{{ $t('word.settings') }}</span>
</li> </li>
</ul> </ul>
@@ -70,7 +70,8 @@ export default {
getConnections: 'connections/getConnections', getConnections: 'connections/getConnections',
getConnectionName: 'connections/getConnectionName', getConnectionName: 'connections/getConnectionName',
connected: 'workspaces/getConnected', connected: 'workspaces/getConnected',
selectedWorkspace: 'workspaces/getSelected' selectedWorkspace: 'workspaces/getSelected',
updateStatus: 'application/getUpdateStatus'
}), }),
connections: { connections: {
get () { get () {
@@ -79,6 +80,9 @@ export default {
set (value) { set (value) {
this.updateConnections(value); this.updateConnections(value);
} }
},
hasUpdates () {
return ['available', 'downloading', 'downloaded'].includes(this.updateStatus);
} }
}, },
methods: { methods: {
@@ -106,101 +110,104 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
#settingbar{ #settingbar {
width: $settingbar-width; width: $settingbar-width;
height: calc(100vh - #{$excluding-size}); height: calc(100vh - #{$excluding-size});
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background: $bg-color-light;
padding: 0;
box-shadow: 0 0 1px 0 #000;
z-index: 9;
.settingbar-top-elements {
overflow-x: hidden;
overflow-y: overlay;
max-height: calc((100vh - 3.5rem) - #{$excluding-size});
&::-webkit-scrollbar {
width: 3px;
}
}
.settingbar-bottom-elements {
padding-top: 0.5rem;
background: $bg-color-light; background: $bg-color-light;
z-index: 1;
}
.settingbar-elements {
list-style: none;
text-align: center;
width: $settingbar-width;
padding: 0; padding: 0;
box-shadow: 0 0 1px 0px #000; margin: 0;
z-index: 9;
.settingbar-top-elements{ .settingbar-element {
overflow-x: hidden; height: $settingbar-width;
overflow-y: overlay; width: 100%;
max-height: calc((100vh - 3.5rem) - #{$excluding-size}); margin: 0;
border-left: 3px solid transparent;
opacity: 0.5;
transition: opacity 0.2s;
display: flex;
align-content: center;
justify-content: center;
flex-direction: column;
&::-webkit-scrollbar { &:hover {
width: 3px; opacity: 1;
} }
&.selected {
border-left-color: $body-font-color;
opacity: 1;
}
.settingbar-element-icon {
&.badge::after {
bottom: -10px;
right: 0;
position: absolute;
background: $success-color;
}
&.badge-update::after {
bottom: initial;
background: $primary-color;
}
}
} }
}
}
.settingbar-bottom-elements{ .ex-tooltip {// Because both overflow-x: visible and overflow-y:auto are evil!!!
padding-top: .5rem; .ex-tooltip-content {
background: $bg-color-light; z-index: 999;
z-index: 1; visibility: hidden;
} opacity: 0;
display: block;
position: absolute;
text-align: center;
margin: 0 0 0 calc(#{$settingbar-width} - 5px);
left: 0;
padding: 0.2rem 0.4rem;
font-size: 0.7rem;
background: rgba(48, 55, 66, 0.95);
border-radius: 0.1rem;
color: #fff;
max-width: 320px;
pointer-events: none;
text-overflow: ellipsis;
overflow: hidden;
transition: opacity 0.2s;
}
.settingbar-elements{ &:hover .ex-tooltip-content {
list-style: none; visibility: visible;
text-align: center; opacity: 1;
width: $settingbar-width; }
padding: 0; }
margin: 0;
.settingbar-element{
height: $settingbar-width;
width: 100%;
margin: 0;
border-left: 3px solid transparent;
opacity: .5;
transition: opacity .2s;
display: flex;
align-content: center;
justify-content: center;
flex-direction: column;
&:hover{
opacity: 1;
}
&.selected{
border-left-color: $body-font-color;
opacity: 1;
}
.settingbar-element-icon{
&.badge::after{
bottom: -10px;
right: 0;
position: absolute;
background: $success-color;
}
}
}
}
}
.ex-tooltip{// Because both overflow-x: visible and overflow-y:auto are evil!!!
.ex-tooltip-content{
z-index: 999;
visibility: hidden;
opacity: 0;
display:block;
position:absolute;
background-color:#feffe1;
text-align: center;
margin:.0 0 0 calc(#{$settingbar-width} - 5px);
left: 0;
padding: .2rem .4rem;
font-size: .7rem;
background: rgba(48,55,66,.95);
border-radius: .1rem;
color: #fff;
max-width: 320px;
pointer-events: none;
text-overflow: ellipsis;
transition: opacity .2s;
}
&:hover .ex-tooltip-content{
visibility: visible;
opacity: 1;
}
}
</style> </style>

View File

@@ -4,8 +4,8 @@
<div class="titlebar-elements"> <div class="titlebar-elements">
<img class="titlebar-logo" :src="require('@/images/logo.svg').default"> <img class="titlebar-logo" :src="require('@/images/logo.svg').default">
</div> </div>
<div class="titlebar-elements"> <div class="titlebar-elements titlebar-title">
<!-- --> {{ windowTitle }}
</div> </div>
<div class="titlebar-elements"> <div class="titlebar-elements">
<div <div
@@ -13,31 +13,32 @@
class="titlebar-element" class="titlebar-element"
@click="openDevTools" @click="openDevTools"
> >
<i class="material-icons">code</i> <i class="mdi mdi-24px mdi-code-tags" />
</div> </div>
<div <div
v-if="isDevelopment" v-if="isDevelopment"
class="titlebar-element" class="titlebar-element"
@click="reload" @click="reload"
> >
<i class="material-icons">refresh</i> <i class="mdi mdi-24px mdi-refresh" />
</div> </div>
<div class="titlebar-element" @click="minimizeApp"> <div class="titlebar-element" @click="minimizeApp">
<i class="material-icons">remove</i> <i class="mdi mdi-24px mdi-minus" />
</div> </div>
<div class="titlebar-element" @click="toggleFullScreen"> <div class="titlebar-element" @click="toggleFullScreen">
<i v-if="isMaximized" class="material-icons">fullscreen_exit</i> <i v-if="isMaximized" class="mdi mdi-24px mdi-fullscreen-exit" />
<i v-else class="material-icons">fullscreen</i> <i v-else class="mdi mdi-24px mdi-fullscreen" />
</div> </div>
<div class="titlebar-element close-button" @click="closeApp"> <div class="titlebar-element close-button" @click="closeApp">
<i class="material-icons">close</i> <i class="mdi mdi-24px mdi-close" />
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { remote } from 'electron'; import { remote, ipcRenderer } from 'electron';
import { mapGetters } from 'vuex';
export default { export default {
name: 'TheTitleBar', name: 'TheTitleBar',
@@ -48,6 +49,22 @@ export default {
isDevelopment: process.env.NODE_ENV === 'development' isDevelopment: process.env.NODE_ENV === 'development'
}; };
}, },
computed: {
...mapGetters({
getConnectionName: 'connections/getConnectionName',
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
windowTitle () {
if (!this.selectedWorkspace) return '';
const connectionName = this.getConnectionName(this.selectedWorkspace);
const workspace = this.getWorkspace(this.selectedWorkspace);
const breadcrumbs = Object.values(workspace.breadcrumbs).filter(breadcrumb => breadcrumb);
return [connectionName, ...breadcrumbs].join(' • ');
}
},
created () { created () {
window.addEventListener('resize', this.onResize); window.addEventListener('resize', this.onResize);
}, },
@@ -56,7 +73,7 @@ export default {
}, },
methods: { methods: {
closeApp () { closeApp () {
this.w.close(); ipcRenderer.send('close-app');
}, },
minimizeApp () { minimizeApp () {
this.w.minimize(); this.w.minimize();
@@ -81,55 +98,64 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
#titlebar{ #titlebar {
display: flex;
position: relative;
justify-content: space-between;
background: $bg-color-light;
align-items: center;
height: $titlebar-height;
-webkit-app-region: drag;
user-select: none;
box-shadow: 0 0 1px 0 #000;
z-index: 9999;
.titlebar-resizer {
position: absolute;
top: 0;
width: 100%;
height: 4px;
z-index: 999;
-webkit-app-region: no-drag;
}
.titlebar-elements {
display: flex; display: flex;
position: relative;
justify-content: space-between;
background: $bg-color-light;
align-items: center; align-items: center;
height: $titlebar-height;
-webkit-app-region: drag;
user-select: none;
box-shadow: 0 0 1px 0px #000;
z-index: 9999;
.titlebar-resizer{ &.titlebar-title {
position: absolute; position: absolute;
top: 0; left: 0;
width: 100%; right: 0;
height: 4px; text-align: center;
z-index: 999; display: block;
-webkit-app-region: no-drag; pointer-events: none;
} }
.titlebar-elements{ .titlebar-logo {
display: flex; height: $titlebar-height;
align-items: center; padding: 0 0.4rem;
.titlebar-logo{
height: $titlebar-height;
padding: 0 .4rem;
}
.titlebar-element{
display: flex;
align-items: center;
height: $titlebar-height;
line-height: 0;
padding: 0 .7rem;
opacity: .7;
transition: opacity .2s;
-webkit-app-region: no-drag;
&:hover{
opacity: 1;
background: rgba($color: #fff, $alpha: .2);
}
&.close-button:hover{
background: red;
}
}
} }
}
.titlebar-element {
display: flex;
align-items: center;
height: $titlebar-height;
line-height: 0;
padding: 0 0.7rem;
opacity: 0.7;
transition: opacity 0.2s;
-webkit-app-region: no-drag;
&:hover {
opacity: 1;
background: rgba($color: #fff, $alpha: 0.2);
}
&.close-button:hover {
background: red;
}
}
}
}
</style> </style>

View File

@@ -2,37 +2,69 @@
<div v-show="isSelected" class="workspace column columns col-gapless"> <div v-show="isSelected" class="workspace column columns col-gapless">
<WorkspaceExploreBar :connection="connection" :is-selected="isSelected" /> <WorkspaceExploreBar :connection="connection" :is-selected="isSelected" />
<div v-if="workspace.connected" class="workspace-tabs column columns col-gapless"> <div v-if="workspace.connected" class="workspace-tabs column columns col-gapless">
<ul class="tab tab-block column col-12"> <ul ref="tabWrap" class="tab tab-block column col-12">
<li <li
v-if="workspace.breadcrumbs.table" v-if="workspace.breadcrumbs.table"
class="tab-item" class="tab-item"
:class="{'active': selectedTab === 1}" :class="{'active': selectedTab === 'prop'}"
@click="selectTab({uid: workspace.uid, tab: 1})" @click="selectTab({uid: workspace.uid, tab: 'prop'})"
> >
<a class="tab-link"> <a class="tab-link">
<i class="material-icons md-18 mr-1">grid_on</i> <i class="mdi mdi-18px mdi-tune mr-1" />
<span :title="workspace.breadcrumbs.table">{{ workspace.breadcrumbs.table }}</span> <span :title="workspace.breadcrumbs.table">{{ $t('word.properties').toUpperCase() }}: {{ workspace.breadcrumbs.table }}</span>
</a> </a>
</li> </li>
<li <li
v-for="(tab, key) of queryTabs" v-if="workspace.breadcrumbs.table"
class="tab-item"
:class="{'active': selectedTab === 'data'}"
@click="selectTab({uid: workspace.uid, tab: 'data'})"
>
<a class="tab-link">
<i class="mdi mdi-18px mdi-table mr-1" />
<span :title="workspace.breadcrumbs.table">{{ $t('word.data').toUpperCase() }}: {{ workspace.breadcrumbs.table }}</span>
</a>
</li>
<li
v-for="tab of queryTabs"
:key="tab.uid" :key="tab.uid"
class="tab-item" class="tab-item"
:class="{'active': selectedTab === tab.uid}" :class="{'active': selectedTab === tab.uid}"
@click="selectTab({uid: workspace.uid, tab: tab.uid})" @click="selectTab({uid: workspace.uid, tab: tab.uid})"
@mousedown.middle="closeTab(tab.uid)"
> >
<a><span>Query #{{ key+1 }} <span v-if="queryTabs.length > 1" class="btn btn-clear" /></span></a> <a>
<span>
Query #{{ tab.index }}
<span
v-if="queryTabs.length > 1"
class="btn btn-clear"
:title="$t('word.close')"
@click.stop="closeTab(tab.uid)"
/>
</span>
</a>
</li>
<li class="tab-item">
<a
class="tab-add"
:title="$t('message.openNewTab')"
@click="addTab"
>
<i class="mdi mdi-24px mdi-plus" />
</a>
</li> </li>
</ul> </ul>
<WorkspaceTableTab <WorkspaceTableTab
v-show="selectedTab === 1" v-show="selectedTab === 'data'"
:connection="connection" :connection="connection"
:table="workspace.breadcrumbs.table" :table="workspace.breadcrumbs.table"
/> />
<WorkspaceQueryTab <WorkspaceQueryTab
v-for="tab of queryTabs" v-for="tab of queryTabs"
v-show="selectedTab === tab.uid"
:key="tab.uid" :key="tab.uid"
:tab-uid="tab.uid"
:is-selected="selectedTab === tab.uid"
:connection="connection" :connection="connection"
/> />
</div> </div>
@@ -68,7 +100,7 @@ export default {
return this.selectedWorkspace === this.connection.uid; return this.selectedWorkspace === this.connection.uid;
}, },
selectedTab () { selectedTab () {
return this.workspace.selected_tab || this.queryTabs[0].uid; return this.queryTabs.find(tab => tab.uid === this.workspace.selected_tab) || ['data', 'prop'].includes(this.workspace.selected_tab) ? this.workspace.selected_tab : this.queryTabs[0].uid;
}, },
queryTabs () { queryTabs () {
return this.workspace.tabs.filter(tab => tab.type === 'query'); return this.workspace.tabs.filter(tab => tab.type === 'query');
@@ -80,102 +112,141 @@ export default {
if (isInitiated) if (isInitiated)
this.connectWorkspace(this.connection); this.connectWorkspace(this.connection);
}, },
mounted () {
if (this.$refs.tabWrap) {
this.$refs.tabWrap.addEventListener('wheel', e => {
if (e.deltaY > 0) this.$refs.tabWrap.scrollLeft += 50;
else this.$refs.tabWrap.scrollLeft -= 50;
});
}
},
methods: { methods: {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification',
addWorkspace: 'workspaces/addWorkspace', addWorkspace: 'workspaces/addWorkspace',
connectWorkspace: 'workspaces/connectWorkspace', connectWorkspace: 'workspaces/connectWorkspace',
removeConnected: 'workspaces/removeConnected', removeConnected: 'workspaces/removeConnected',
selectTab: 'workspaces/selectTab' selectTab: 'workspaces/selectTab',
}) newTab: 'workspaces/newTab',
removeTab: 'workspaces/removeTab'
}),
addTab () {
this.newTab(this.connection.uid);
},
closeTab (tUid) {
if (this.queryTabs.length === 1) return;
this.removeTab({ uid: this.connection.uid, tab: tUid });
}
} }
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.workspace{ .workspace {
padding: 0; padding: 0;
margin: 0; margin: 0;
.workspace-tabs{ .workspace-tabs {
overflow: hidden;
height: calc(100vh - #{$excluding-size});
.tab-block {
background: $bg-color-light;
margin-top: 0;
flex-direction: row;
align-items: flex-start;
flex-wrap: nowrap;
overflow: auto; overflow: auto;
height: calc(100vh - #{$excluding-size});
.tab-block{ &::-webkit-scrollbar {
background: $bg-color-light; width: 2px;
margin-top: 0; height: 2px;
.tab-item{
max-width: 12rem;
width: fit-content;
flex: initial;
&.active a{
opacity: 1;
}
> a{
padding: .2rem .8rem;
color: $body-font-color;
cursor: pointer;
display: flex;
align-items: center;
opacity: .7;
transition: opacity .2s;
&:hover{
opacity: 1;
}
> span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
} }
}
.workspace-query-results{ .tab-item {
overflow: auto; max-width: 12rem;
white-space: nowrap; width: fit-content;
flex: initial;
.table{ > a {
width: auto; padding: 0.2rem 0.8rem;
border-collapse: separate; color: $body-font-color;
cursor: pointer;
display: flex;
align-items: center;
opacity: 0.7;
transition: opacity 0.2s;
.th{ &:hover {
position: sticky; opacity: 1;
top: 0; }
background: $bg-color;
border: 1px solid;
border-left: none;
border-bottom-width: 2px;
border-color: $bg-color-light;
padding: .1rem .4rem;
font-weight: 700;
font-size: .7rem;
}
.td{ &.tab-add {
border-right: 1px solid; padding: 0.2rem 0.4rem;
border-bottom: 1px solid; margin-top: 2px;
border-color: $bg-color-light; border: 0;
padding: 0 .4rem; }
text-overflow: ellipsis;
max-width: 200px; > span {
white-space: nowrap;
overflow: hidden; overflow: hidden;
font-size: .7rem; white-space: nowrap;
text-overflow: ellipsis;
padding: 0 0.2rem;
}
}
&:focus{ &.active a {
box-shadow:inset 0px 0px 0px 1px $body-font-color; opacity: 1;
background: rgba($color: #000000, $alpha: .3); }
outline: none;
}
}
} }
} }
}
.workspace-query-results {
overflow: auto;
white-space: nowrap;
.table {
width: auto;
border-collapse: separate;
.th {
position: sticky;
top: 0;
background: $bg-color;
border: 1px solid;
border-left: none;
border-bottom-width: 2px;
border-color: $bg-color-light;
padding: 0;
font-weight: 700;
font-size: 0.7rem;
z-index: 1;
> div {
padding: 0.1rem 0.4rem;
min-width: -webkit-fill-available;
}
}
.td {
border-right: 1px solid;
border-bottom: 1px solid;
border-color: $bg-color-light;
padding: 0 0.4rem;
text-overflow: ellipsis;
max-width: 200px;
white-space: nowrap;
overflow: hidden;
font-size: 0.7rem;
position: relative;
&:focus {
box-shadow: inset 0 0 0 1px $body-font-color;
background: rgba($color: #000, $alpha: 0.3);
outline: none;
}
}
}
}
} }
</style> </style>

View File

@@ -2,7 +2,7 @@
<div class="columns"> <div class="columns">
<div class="column col-12 empty text-light"> <div class="column col-12 empty text-light">
<div class="empty-icon"> <div class="empty-icon">
<i class="material-icons md-48">cloud_off</i> <i class="mdi mdi-48px mdi-power-plug-off" />
</div> </div>
<p class="empty-title h5"> <p class="empty-title h5">
{{ $t('word.disconnected') }} {{ $t('word.disconnected') }}
@@ -19,7 +19,7 @@
</div> </div>
<ModalAskCredentials <ModalAskCredentials
v-if="isAsking" v-if="isAsking"
@closeAsking="closeAsking" @close-asking="closeAsking"
@credentials="continueTest" @credentials="continueTest"
/> />
</div> </div>
@@ -45,7 +45,6 @@ export default {
}, },
methods: { methods: {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification',
connectWorkspace: 'workspaces/connectWorkspace' connectWorkspace: 'workspaces/connectWorkspace'
}), }),
async startConnection () { async startConnection () {
@@ -73,11 +72,11 @@ export default {
</script> </script>
<style scoped> <style scoped>
.empty{ .empty {
height: 100%; height: 100%;
border-radius: 0; border-radius: 0;
background: transparent; background: transparent;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
</style> </style>

View File

@@ -10,16 +10,16 @@
<span class="workspace-explorebar-title">{{ connectionName }}</span> <span class="workspace-explorebar-title">{{ connectionName }}</span>
<span v-if="workspace.connected" class="workspace-explorebar-tools"> <span v-if="workspace.connected" class="workspace-explorebar-tools">
<i <i
class="material-icons md-18 c-hand" class="mdi mdi-18px mdi-refresh c-hand"
:class="{'rotate':isRefreshing}" :class="{'rotate':isRefreshing}"
:title="$t('word.refresh')" :title="$t('word.refresh')"
@click="refresh" @click="refresh"
>refresh</i> />
<i <i
class="material-icons md-18 c-hand mr-1 ml-2" class="mdi mdi-18px mdi-power-plug-off c-hand mr-1 ml-2"
:title="$t('word.disconnect')" :title="$t('word.disconnect')"
@click="disconnectWorkspace(connection.uid)" @click="disconnectWorkspace(connection.uid)"
>exit_to_app</i> />
</span> </span>
</div> </div>
<WorkspaceConnectPanel <WorkspaceConnectPanel
@@ -123,70 +123,70 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.workspace-explorebar-resizer{ .workspace-explorebar-resizer {
position: absolute; position: absolute;
width: 4px; width: 4px;
right: -2px; right: -2px;
top: 0; top: 0;
height: calc(100vh - #{$excluding-size}); height: calc(100vh - #{$excluding-size});
cursor: ew-resize; cursor: ew-resize;
z-index: 99; z-index: 99;
} }
.workspace-explorebar{ .workspace-explorebar {
width: $explorebar-width; width: $explorebar-width;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
text-align: left;
background: $bg-color-gray;
box-shadow: 0 0 1px 0 #000;
z-index: 8;
flex: initial;
position: relative;
padding: 0;
.workspace-explorebar-header {
width: 100%;
padding: 0.3rem;
display: flex; display: flex;
flex-direction: column; justify-content: space-between;
justify-content: flex-start; font-size: 0.6rem;
align-items: center; font-weight: 700;
text-align: left; text-transform: uppercase;
background: $bg-color-gray;
box-shadow: 0 0 1px 0px #000;
z-index: 8;
flex: initial;
position: relative;
padding: 0;
.workspace-explorebar-header{ .workspace-explorebar-title {
width: 100%; width: 80%;
padding: .3rem; white-space: nowrap;
display: flex; overflow: hidden;
justify-content: space-between; text-overflow: ellipsis;
font-size: .6rem; display: block;
font-weight: 700; align-items: center;
text-transform: uppercase;
.workspace-explorebar-title{
width: 80%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
align-items: center;
}
.workspace-explorebar-tools {
display: flex;
align-items: center;
> i{
opacity: .6;
transition: opacity .2s;
display: flex;
align-items: center;
&:hover{
opacity: 1;
}
}
}
} }
.workspace-explorebar-body{ .workspace-explorebar-tools {
width: 100%; display: flex;
height: calc((100vh - 30px) - #{$excluding-size}); align-items: center;
overflow: overlay;
padding: 0 .1rem; > i {
opacity: 0.6;
transition: opacity 0.2s;
display: flex;
align-items: center;
&:hover {
opacity: 1;
}
}
} }
} }
.workspace-explorebar-body {
width: 100%;
height: calc((100vh - 30px) - #{$excluding-size});
overflow: overlay;
padding: 0 0.1rem;
}
}
</style> </style>

View File

@@ -5,8 +5,8 @@
:class="{'text-bold': breadcrumbs.schema === database.name}" :class="{'text-bold': breadcrumbs.schema === database.name}"
@click="changeBreadcrumbs({schema: database.name, table:null})" @click="changeBreadcrumbs({schema: database.name, table:null})"
> >
<i class="icon material-icons md-18 mr-1">navigate_next</i> <i class="icon mdi mdi-18px mdi-chevron-right" />
<i class="material-icons md-18 mr-1">view_agenda</i> <i class="database-icon mdi mdi-18px mdi-database mr-1" />
<span>{{ database.name }}</span> <span>{{ database.name }}</span>
</summary> </summary>
<div class="accordion-body"> <div class="accordion-body">
@@ -20,7 +20,7 @@
@click="changeBreadcrumbs({schema: database.name, table: table.TABLE_NAME})" @click="changeBreadcrumbs({schema: database.name, table: table.TABLE_NAME})"
> >
<a class="table-name"> <a class="table-name">
<i class="material-icons md-18 mr-1">grid_on</i> <i class="table-icon mdi mdi-18px mdi-table mr-1" />
<span>{{ table.TABLE_NAME }}</span> <span>{{ table.TABLE_NAME }}</span>
</a> </a>
</li> </li>
@@ -56,35 +56,40 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.workspace-explorebar-database{ .workspace-explorebar-database {
.database-name, .database-name,
a.table-name{ a.table-name {
display: flex; display: flex;
align-items: center; align-items: center;
padding: .1rem; padding: 0.1rem;
cursor: pointer; cursor: pointer;
font-size: .7rem; font-size: 0.7rem;
> span{ > span {
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
display: block; display: block;
text-overflow: ellipsis; text-overflow: ellipsis;
}
&:hover{
color: $body-font-color;
background: rgba($color: #FFF, $alpha: .05);
border-radius: 2px;
}
} }
.menu-item{ &:hover {
line-height: 1.2; color: $body-font-color;
background: rgba($color: #fff, $alpha: 0.05);
border-radius: 2px;
} }
.database-tables{ .database-icon,
margin-left: 1.2rem; .table-icon {
opacity: 0.7;
} }
} }
.menu-item {
line-height: 1.2;
}
.database-tables {
margin-left: 1.2rem;
}
}
</style> </style>

View File

@@ -1,27 +1,26 @@
<template> <template>
<div class="workspace-query-tab column col-12 columns col-gapless"> <div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12"> <div class="workspace-query-runner column col-12">
<QueryEditor v-model="query" /> <QueryEditor v-if="isSelected" :value.sync="query" />
<div class="workspace-query-runner-footer"> <div class="workspace-query-runner-footer">
<div class="workspace-query-buttons"> <div class="workspace-query-buttons">
<button <button
class="btn btn-link btn-sm" class="btn btn-link btn-sm"
:class="{'loading':isQuering}" :class="{'loading':isQuering}"
:disabled="!query" :disabled="!query"
@click="runQuery" @click="runQuery(query)"
> >
<span>{{ $t('word.run') }}</span> <span>{{ $t('word.run') }}</span>
<i class="material-icons text-success">play_arrow</i> <i class="mdi mdi-24px mdi-play text-success" />
</button> </button>
<!-- <button class="btn btn-link btn-sm">
<span>{{ $t('word.save') }}</span>
<i class="material-icons ml-1">save</i>
</button> -->
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div v-if="results.rows"> <div v-if="results.rows">
{{ $t('word.results') }}: <b>{{ results.rows.length }}</b> {{ $t('word.results') }}: <b>{{ results.rows.length }}</b>
</div> </div>
<div v-if="results.report">
{{ $t('message.affectedRows') }}: <b>{{ results.report.affectedRows }}</b>
</div>
<div v-if="workspace.breadcrumbs.schema"> <div v-if="workspace.breadcrumbs.schema">
{{ $t('word.schema') }}: <b>{{ workspace.breadcrumbs.schema }}</b> {{ $t('word.schema') }}: <b>{{ workspace.breadcrumbs.schema }}</b>
</div> </div>
@@ -31,8 +30,12 @@
<div class="workspace-query-results column col-12"> <div class="workspace-query-results column col-12">
<WorkspaceQueryTable <WorkspaceQueryTable
v-if="results" v-if="results"
v-show="!isQuering"
ref="queryTable"
:results="results" :results="results"
:fields="resultsFields" :tab-uid="tabUid"
@update-field="updateField"
@delete-selected="deleteSelected"
/> />
</div> </div>
</div> </div>
@@ -40,9 +43,11 @@
<script> <script>
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import Tables from '@/ipc-api/Tables';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor';
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable'; import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import tableTabs from '@/mixins/tableTabs';
export default { export default {
name: 'WorkspaceQueryTab', name: 'WorkspaceQueryTab',
@@ -50,14 +55,19 @@ export default {
QueryEditor, QueryEditor,
WorkspaceQueryTable WorkspaceQueryTable
}, },
mixins: [tableTabs],
props: { props: {
connection: Object connection: Object,
tabUid: String,
isSelected: Boolean
}, },
data () { data () {
return { return {
query: '', query: '',
lastQuery: '',
isQuering: false, isQuering: false,
results: {} results: {},
selectedFields: []
}; };
}, },
computed: { computed: {
@@ -67,31 +77,87 @@ export default {
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
}, },
resultsFields () { table () {
return this.results.rows && this.results.rows.length ? Object.keys(this.results.rows[0]).map(field => { if ('fields' in this.results && this.results.fields.length)
return { name: field, key: '', type: '' }; // TODO: extract getting table name from query return this.results.fields[0].orgTable;
}) : []; return '';
},
schema () {
if ('fields' in this.results && this.results.fields.length)
return this.results.fields[0].db;
return this.workspace.breadcrumbs.schema;
} }
}, },
methods: { methods: {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification' addNotification: 'notifications/addNotification',
setTabFields: 'workspaces/setTabFields',
setTabKeyUsage: 'workspaces/setTabKeyUsage'
}), }),
async runQuery () { async runQuery (query) {
if (!this.query) return; if (!query) return;
this.isQuering = true; this.isQuering = true;
this.results = {}; this.clearTabData();
try { try { // Query Data
const params = { const params = {
uid: this.connection.uid, uid: this.connection.uid,
query: this.query, schema: this.schema,
schema: this.workspace.breadcrumbs.schema query
}; };
const { status, response } = await Connection.rawQuery(params); const { status, response } = await Connection.rawQuery(params);
if (status === 'success') if (status === 'success') {
this.results = response; this.results = response;
if (response.rows) { // if is a select
this.selectedFields = response.fields.map(field => field.orgName);
try { // Table data
const params = {
uid: this.connection.uid,
schema: this.schema,
table: this.table
};
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') {
let fields = response.filter(field => this.selectedFields.includes(field.name));
if (this.selectedFields.length) {
fields = fields.map((field, index) => {
return { ...field, alias: this.results.fields[index].name };
});
}
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
try { // Key usage (foreign keys)
const params = {
uid: this.connection.uid,
schema: this.schema,
table: this.table
};
const { status, response } = await Tables.getKeyUsage(params);
if (status === 'success')
this.setTabKeyUsage({ cUid: this.connection.uid, tUid: this.tabUid, keyUsage: response });
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
}
else { // if is a query without results
}
}
else else
this.addNotification({ status: 'error', message: response }); this.addNotification({ status: 'error', message: response });
} }
@@ -100,43 +166,50 @@ export default {
} }
this.isQuering = false; this.isQuering = false;
this.lastQuery = query;
},
reloadTable () {
this.runQuery(this.lastQuery);
},
clearTabData () {
this.results = {};
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: [] });
} }
} }
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.workspace-tabs{ .workspace-tabs {
align-content: baseline; align-content: baseline;
.workspace-query-runner{ .workspace-query-runner {
.workspace-query-runner-footer {
display: flex;
justify-content: space-between;
padding: 0.3rem 0.6rem 0.4rem;
align-items: center;
.workspace-query-runner-footer{ .workspace-query-buttons {
display: flex; display: flex;
justify-content: space-between;
padding: .3rem .6rem .4rem;
align-items: center;
.workspace-query-buttons{ .btn {
display: flex; display: flex;
align-self: center;
.btn{ color: $body-font-color;
display: flex; margin-right: 0.4rem;
align-self: center; }
color: $body-font-color;
margin-right: .4rem;
}
}
.workspace-query-info{
display: flex;
> div + div{
padding-left: .6rem;
}
}
} }
}
.workspace-query-info {
display: flex;
> div + div {
padding-left: 0.6rem;
}
}
}
}
} }
</style> </style>

View File

@@ -1,120 +1,144 @@
<template> <template>
<BaseVirtualScroll <div
v-if="results.rows" ref="tableWrapper"
ref="resultTable"
:items="localResults"
:item-height="25"
class="vscroll" class="vscroll"
:style="{'height': resultsSize+'px'}" :style="{'height': resultsSize+'px'}"
> >
<template slot-scope="{ items }"> <TableContext
<div class="table table-hover"> v-if="isContext"
<div class="thead"> :context-event="contextEvent"
<div class="tr"> :selected-rows="selectedRows"
<div @delete-selected="deleteSelected"
v-for="field in fields" @close-context="isContext = false"
:key="field.name" />
class="th" <div ref="table" class="table table-hover">
> <div class="thead">
<div class="table-column-title"> <div class="tr">
<div
v-for="field in fields"
:key="field.name"
class="th c-hand"
>
<div ref="columnResize" class="column-resizable">
<div class="table-column-title" @click="sort(field.name)">
<i <i
v-if="field.key" v-if="field.key"
class="material-icons column-key c-help" class="mdi mdi-key column-key c-help"
:class="`key-${field.key}`" :class="`key-${field.key}`"
:title="keyName(field.key)" :title="keyName(field.key)"
>vpn_key</i> />
<span>{{ field.name }}</span> <span>{{ field.alias || field.name }}</span>
<i
v-if="currentSort === field.name"
class="mdi sort-icon"
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'"
/>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="tbody"> </div>
<div <BaseVirtualScroll
v-if="results.rows"
ref="resultTable"
:items="sortedResults"
:item-height="22"
class="tbody"
:visible-height="resultsSize"
:scroll-element="scrollElement"
>
<template slot-scope="{ items }">
<WorkspaceQueryTableRow
v-for="row in items" v-for="row in items"
:key="row._id" :key="row._id"
:row="row"
:fields="fields"
:key-usage="keyUsage"
class="tr" class="tr"
> :class="{'selected': selectedRows.includes(row._id)}"
<div @select-row="selectRow($event, row._id)"
v-for="(col, cKey) in row" @update-field="updateField($event, row[primaryField.alias || primaryField.name])"
:key="cKey" @contextmenu="contextMenu"
class="td" />
:class="`type-${fieldType(cKey)}${isNull(col)}`" </template>
:style="{'display': cKey === '_id' ? 'none' : ''}" </basevirtualscroll>
tabindex="0" </div>
> </div>
{{ col | typeFormat(fieldType(cKey)) }}
</div>
</div>
</div>
</div>
</template>
</BaseVirtualScroll>
</template> </template>
<script> <script>
import { uidGen, mimeFromHex, formatBytes } from 'common/libs/utilities'; import { uidGen } from 'common/libs/uidGen';
import hexToBinary from 'common/libs/hexToBinary';
import moment from 'moment';
import BaseVirtualScroll from '@/components/BaseVirtualScroll'; import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import WorkspaceQueryTableRow from '@/components/WorkspaceQueryTableRow';
import TableContext from '@/components/WorkspaceQueryTableContext';
import { mapActions, mapGetters } from 'vuex';
export default { export default {
name: 'WorkspaceQueryTable', name: 'WorkspaceQueryTable',
components: { components: {
BaseVirtualScroll BaseVirtualScroll,
}, WorkspaceQueryTableRow,
filters: { TableContext
typeFormat (val, type) {
if (!val) return val;
switch (type) {
case 'char':
case 'varchar':
case 'text':
case 'mediumtext':
return val.substring(0, 128);
case 'date':
return moment(val).format('YYYY-MM-DD');
case 'datetime':
case 'timestamp':
return moment(val).format('YYYY-MM-DD HH:mm:ss.SSS');
case 'blob':
case 'mediumblob':
case 'longblob': {
const buff = Buffer.from(val);
if (!buff.length) return '';
const hex = buff.toString('hex').substring(0, 8).toUpperCase();
return `${mimeFromHex(hex).mime} (${formatBytes(buff.length)})`;
}
case 'bit': {
const hex = Buffer.from(val).toString('hex');
return hexToBinary(hex);
}
default:
return val;
}
}
}, },
props: { props: {
results: Object, results: Object,
fields: Array tabUid: [String, Number]
}, },
data () { data () {
return { return {
resultsSize: 1000, resultsSize: 1000,
localResults: [] localResults: [],
isContext: false,
contextEvent: null,
selectedCell: null,
selectedRows: [],
currentSort: '',
currentSortDir: 'asc'
}; };
}, },
computed: {
...mapGetters({
getWorkspaceTab: 'workspaces/getWorkspaceTab'
}),
primaryField () {
return this.fields.filter(field => ['pri', 'uni'].includes(field.key))[0] || false;
},
sortedResults () {
if (this.currentSort) {
return [...this.localResults].sort((a, b) => {
let modifier = 1;
const valA = typeof a[this.currentSort] === 'string' ? a[this.currentSort].toLowerCase() : a[this.currentSort];
const valB = typeof b[this.currentSort] === 'string' ? b[this.currentSort].toLowerCase() : b[this.currentSort];
if (this.currentSortDir === 'desc') modifier = -1;
if (valA < valB) return -1 * modifier;
if (valA > valB) return 1 * modifier;
return 0;
});
}
else
return this.localResults;
},
fields () {
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).fields : [];
},
keyUsage () {
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).keyUsage : [];
},
scrollElement () {
return this.$refs.tableWrapper;
}
},
watch: { watch: {
results () { results () {
this.resetSort();
this.localResults = this.results.rows ? this.results.rows.map(item => { this.localResults = this.results.rows ? this.results.rows.map(item => {
return { ...item, _id: uidGen() }; return { ...item, _id: uidGen() };
}) : []; }) : [];
} }
}, },
updated () { updated () {
if (this.$refs.resultTable) if (this.$refs.table)
this.resizeResults(); this.refreshScroller();
}, },
mounted () { mounted () {
window.addEventListener('resize', this.resizeResults); window.addEventListener('resize', this.resizeResults);
@@ -123,6 +147,9 @@ export default {
window.removeEventListener('resize', this.resizeResults); window.removeEventListener('resize', this.resizeResults);
}, },
methods: { methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
fieldType (cKey) { fieldType (cKey) {
let type = 'unknown'; let type = 'unknown';
const field = this.fields.filter(field => field.name === cKey)[0]; const field = this.fields.filter(field => field.name === cKey)[0];
@@ -131,8 +158,13 @@ export default {
return type; return type;
}, },
isNull (col) { fieldPrecision (cKey) {
return col === null ? ' is-null' : ''; let length = 0;
const field = this.fields.filter(field => field.name === cKey)[0];
if (field)
length = field.datePrecision;
return length;
}, },
keyName (key) { keyName (key) {
switch (key) { switch (key) {
@@ -146,16 +178,103 @@ export default {
return 'UNKNOWN ' + key; return 'UNKNOWN ' + key;
} }
}, },
resizeResults (e) { resizeResults () {
if (this.$refs.resultTable) { if (this.$refs.resultTable) {
const el = this.$refs.resultTable.$el; const el = this.$refs.tableWrapper;
const footer = document.getElementById('footer');
if (el) { if (el) {
const footer = document.getElementById('footer');
const size = window.innerHeight - el.getBoundingClientRect().top - footer.offsetHeight; const size = window.innerHeight - el.getBoundingClientRect().top - footer.offsetHeight;
this.resultsSize = size; this.resultsSize = size;
} }
this.$refs.resultTable.updateWindow();
} }
},
refreshScroller () {
this.resizeResults();
},
updateField (payload, id) {
if (!this.primaryField)
this.addNotification({ status: 'warning', message: this.$t('message.unableEditFieldWithoutPrimary') });
else {
const params = {
primary: this.primaryField.name,
id,
...payload
};
this.$emit('update-field', params);
}
},
deleteSelected () {
if (!this.primaryField)
this.addNotification({ status: 'warning', message: this.$t('message.unableEditFieldWithoutPrimary') });
else {
const rowIDs = this.localResults.filter(row => this.selectedRows.includes(row._id)).map(row => row[this.primaryField.name]);
const params = {
primary: this.primaryField.name,
rows: rowIDs
};
this.$emit('delete-selected', params);
}
},
applyUpdate (params) {
const { primary, id, field, content } = params;
this.localResults = this.localResults.map(row => {
if (row[primary] === id)
row[field] = content;
return row;
});
},
selectRow (event, row) {
if (event.ctrlKey) {
if (this.selectedRows.includes(row))
this.selectedRows = this.selectedRows.filter(el => el !== row);
else
this.selectedRows.push(row);
}
else if (event.shiftKey) {
if (!this.selectedRows.length)
this.selectedRows.push(row);
else {
const lastID = this.selectedRows.slice(-1)[0];
const lastIndex = this.localResults.findIndex(el => el._id === lastID);
const clickedIndex = this.localResults.findIndex(el => el._id === row);
if (lastIndex > clickedIndex) {
for (let i = clickedIndex; i < lastIndex; i++)
this.selectedRows.push(this.localResults[i]._id);
}
else if (lastIndex < clickedIndex) {
for (let i = clickedIndex; i > lastIndex; i--)
this.selectedRows.push(this.localResults[i]._id);
}
}
}
else
this.selectedRows = [row];
},
contextMenu (event, cell) {
this.selectedCell = cell;
if (!this.selectedRows.includes(cell.id))
this.selectedRows = [cell.id];
this.contextEvent = event;
this.isContext = true;
},
sort (field) {
if (field === this.currentSort) {
if (this.currentSortDir === 'asc')
this.currentSortDir = 'desc';
else
this.resetSort();
}
else {
this.currentSortDir = 'asc';
this.currentSort = field;
}
},
resetSort () {
this.currentSort = '';
this.currentSortDir = 'asc';
} }
} }
}; };
@@ -163,32 +282,27 @@ export default {
<style lang="scss"> <style lang="scss">
.vscroll { .vscroll {
height: 1000px; height: 1000px;
overflow: auto; overflow: auto;
overflow-anchor: none; overflow-anchor: none;
} }
.table-column-title{ .column-resizable {
display: flex; &:hover,
align-items: center; &:active {
resize: horizontal;
overflow: hidden;
}
} }
.column-key{ .table-column-title {
transform: rotate(90deg); display: flex;
font-size: .7rem; align-items: center;
line-height: 1.5; }
margin-right: .2rem;
&.key-pri{ .sort-icon {
color: goldenrod; font-size: 0.7rem;
} line-height: 1;
margin-left: 0.2rem;
&.key-uni{
color: deepskyblue;
}
&.key-mul{
color: palegreen;
}
} }
</style> </style>

View File

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

View File

@@ -0,0 +1,442 @@
<template>
<div class="tr" @click="selectRow($event, row._id)">
<div
v-for="(col, cKey) in row"
v-show="cKey !== '_id'"
:key="cKey"
class="td p-0"
tabindex="0"
@contextmenu.prevent="$emit('contextmenu', $event, {id: row._id, field: cKey})"
>
<template v-if="cKey !== '_id'">
<span
v-if="!isInlineEditor[cKey]"
class="cell-content px-2"
:class="`${isNull(col)} type-${getFieldType(cKey)}`"
@dblclick="editON($event, col, cKey)"
>{{ col | typeFormat(getFieldType(cKey), getFieldPrecision(cKey)) | cutText }}</span>
<ForeignKeySelect
v-else-if="foreignKeys.includes(cKey)"
class="editable-field"
:value.sync="editingContent"
:key-usage="getKeyUsage(cKey)"
@blur="editOFF"
/>
<template v-else>
<input
v-if="inputProps.mask"
ref="editField"
v-model="editingContent"
v-mask="inputProps.mask"
:type="inputProps.type"
autofocus
class="editable-field px-2"
@blur="editOFF"
>
<input
v-else
ref="editField"
v-model="editingContent"
:type="inputProps.type"
autofocus
class="editable-field px-2"
@blur="editOFF"
>
</template>
</template>
</div>
<ConfirmModal
v-if="isTextareaEditor"
:confirm-text="$t('word.update')"
size="medium"
@confirm="editOFF"
@hide="hideEditorModal"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> {{ $t('word.edit') }} "{{ editingField }}"
</div>
</template>
<div :slot="'body'">
<div class="mb-2">
<div>
<textarea
v-model="editingContent"
class="form-input textarea-editor"
/>
</div>
<div class="editor-field-info">
<div><b>{{ $t('word.size') }}</b>: {{ editingContent.length }}</div>
<div><b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}</div>
</div>
</div>
</div>
</ConfirmModal>
<ConfirmModal
v-if="isBlobEditor"
:confirm-text="$t('word.update')"
@confirm="editOFF"
@hide="hideEditorModal"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> {{ $t('word.edit') }} "{{ editingField }}"
</div>
</template>
<div :slot="'body'">
<div class="mb-2">
<transition name="jump-down">
<div v-if="contentInfo.size">
<img
v-if="isImage"
:src="`data:${contentInfo.mime};base64, ${bufferToBase64(editingContent)}`"
class="img-responsive p-centered bg-checkered"
>
<div v-else class="text-center">
<i class="mdi mdi-36px mdi-file" />
</div>
<div class="editor-buttons mt-2">
<button class="btn btn-link btn-sm" @click="downloadFile">
<span>{{ $t('word.download') }}</span>
<i class="mdi mdi-24px mdi-download ml-1" />
</button>
<button class="btn btn-link btn-sm" @click="prepareToDelete">
<span>{{ $t('word.delete') }}</span>
<i class="mdi mdi-24px mdi-delete-forever ml-1" />
</button>
</div>
</div>
</transition>
<div class="editor-field-info">
<div>
<b>{{ $t('word.size') }}</b>: {{ editingContent.length | formatBytes }}<br>
<b>{{ $t('word.mimeType') }}</b>: {{ contentInfo.mime }}
</div>
<div><b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}</div>
</div>
<div class="mt-3">
<label>{{ $t('message.uploadFile') }}</label>
<input
class="form-input"
type="file"
@change="filesChange($event)"
>
</div>
</div>
</div>
</ConfirmModal>
</div>
</template>
<script>
import moment from 'moment';
import { mimeFromHex } from 'common/libs/mimeFromHex';
import { formatBytes } from 'common/libs/formatBytes';
import { bufferToBase64 } from 'common/libs/bufferToBase64';
import hexToBinary from 'common/libs/hexToBinary';
import { TEXT, LONG_TEXT, NUMBER, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { mask } from 'vue-the-mask';
import ConfirmModal from '@/components/BaseConfirmModal';
import ForeignKeySelect from '@/components/ForeignKeySelect';
export default {
name: 'WorkspaceQueryTableRow',
components: {
ConfirmModal,
ForeignKeySelect
},
directives: {
mask
},
filters: {
formatBytes,
cutText (val) {
if (typeof val !== 'string') return val;
return val.length > 128 ? `${val.substring(0, 128)}[...]` : val;
},
typeFormat (val, type, precision) {
if (!val) return val;
if (DATE.includes(type))
return moment(val).isValid() ? moment(val).format('YYYY-MM-DD') : val;
if (DATETIME.includes(type)) {
let datePrecision = '';
for (let i = 0; i < precision; i++)
datePrecision += i === 0 ? '.S' : 'S';
return moment(val).isValid() ? moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`) : val;
}
if (BLOB.includes(type)) {
const buff = Buffer.from(val);
if (!buff.length) return '';
const hex = buff.toString('hex').substring(0, 8).toUpperCase();
return `${mimeFromHex(hex).mime} (${formatBytes(buff.length)})`;
}
if (BIT.includes(type)) {
const hex = Buffer.from(val).toString('hex');
return hexToBinary(hex);
}
return val;
}
},
props: {
row: Object,
fields: Array,
keyUsage: Array
},
data () {
return {
isInlineEditor: {},
isTextareaEditor: false,
isBlobEditor: false,
willBeDeleted: false,
originalContent: null,
editingContent: null,
editingType: null,
editingField: null,
contentInfo: {
ext: '',
mime: '',
size: null
},
fileToUpload: null
};
},
computed: {
inputProps () {
if ([...TEXT, ...LONG_TEXT].includes(this.editingType))
return { type: 'text', mask: false };
if (NUMBER.includes(this.editingType))
return { type: 'number', mask: false };
if (TIME.includes(this.editingType)) {
let timeMask = '##:##:##';
const precision = this.getFieldPrecision(this.editingField);
for (let i = 0; i < precision; i++)
timeMask += i === 0 ? '.#' : '#';
return { type: 'text', mask: timeMask };
}
if (DATE.includes(this.editingType))
return { type: 'text', mask: '####-##-##' };
if (DATETIME.includes(this.editingType)) {
let datetimeMask = '####-##-## ##:##:##';
const precision = this.getFieldPrecision(this.editingField);
for (let i = 0; i < precision; i++)
datetimeMask += i === 0 ? '.#' : '#';
return { type: 'text', mask: datetimeMask };
}
if (BLOB.includes(this.editingType))
return { type: 'file', mask: false };
if (BIT.includes(this.editingType))
return { type: 'text', mask: false };
return { type: 'text', mask: false };
},
isImage () {
return ['gif', 'jpg', 'png', 'bmp', 'ico', 'tif'].includes(this.contentInfo.ext);
},
foreignKeys () {
return this.keyUsage.map(key => key.column);
}
},
created () {
this.fields.forEach(field => {
this.isInlineEditor[field.name] = false;
});
},
methods: {
getFieldType (cKey) {
let type = 'unknown';
const field = this.getFieldObj(cKey);
if (field)
type = field.type;
return type;
},
getFieldPrecision (cKey) {
let length = 0;
const field = this.getFieldObj(cKey);
if (field)
length = field.datePrecision;
return length;
},
getFieldObj (cKey) {
return this.fields.filter(field => field.name === cKey || field.alias === cKey)[0];
},
isNull (value) {
return value === null ? ' is-null' : '';
},
bufferToBase64 (val) {
return bufferToBase64(val);
},
editON (event, content, field) {
const type = this.getFieldType(field);
this.originalContent = content;
this.editingType = type;
this.editingField = field;
if (LONG_TEXT.includes(type)) {
this.isTextareaEditor = true;
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type);
return;
}
if (BLOB.includes(type)) {
this.isBlobEditor = true;
this.editingContent = this.originalContent || '';
this.fileToUpload = null;
this.willBeDeleted = false;
if (this.originalContent !== null) {
const buff = Buffer.from(this.editingContent);
if (buff.length) {
const hex = buff.toString('hex').substring(0, 8).toUpperCase();
const { ext, mime } = mimeFromHex(hex);
this.contentInfo = {
ext,
mime,
size: this.editingContent.length
};
}
}
return;
}
// Inline editable fields
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type, this.getFieldPrecision(field));
this.$nextTick(() => { // Focus on input
event.target.blur();
this.$nextTick(() => document.querySelector('.editable-field').focus());
});
const obj = {
[field]: true
};
this.isInlineEditor = { ...this.isInlineEditor, ...obj };
},
editOFF () {
this.isInlineEditor[this.editingField] = false;
let content;
if (!BLOB.includes(this.editingType)) {
if (this.editingContent === this.$options.filters.typeFormat(this.originalContent, this.editingType)) return;// If not changed
content = this.editingContent;
}
else { // Handle file upload
if (this.willBeDeleted) {
content = '';
this.willBeDeleted = false;
}
else {
if (!this.fileToUpload) return;
content = this.fileToUpload.file.path;
}
}
this.$emit('update-field', {
field: this.getFieldObj(this.editingField).name,
type: this.editingType,
content
});
this.editingType = null;
this.editingField = null;
},
hideEditorModal () {
this.isTextareaEditor = false;
this.isBlobEditor = false;
},
downloadFile () {
const downloadLink = document.createElement('a');
downloadLink.href = `data:${this.contentInfo.mime};base64, ${bufferToBase64(this.editingContent)}`;
downloadLink.setAttribute('download', `${this.editingField}.${this.contentInfo.ext}`);
document.body.appendChild(downloadLink);
downloadLink.click();
downloadLink.remove();
},
filesChange (event) {
const { files } = event.target;
if (!files.length) return;
this.fileToUpload = { name: files[0].name, file: files[0] };
this.willBeDeleted = false;
},
prepareToDelete () {
this.editingContent = '';
this.contentInfo = {
ext: '',
mime: '',
size: null
};
this.willBeDeleted = true;
},
contextMenu (event, cell) {
this.$emit('update-field', event, cell);
},
selectRow (event, row) {
this.$emit('select-row', event, row);
},
getKeyUsage (keyName) {
return this.keyUsage.find(key => key.column === keyName);
}
}
};
</script>
<style lang="scss">
.editable-field {
margin: 0;
border: none;
line-height: 1;
width: 100%;
position: absolute;
left: 0;
right: 0;
}
.cell-content {
display: block;
min-height: 0.8rem;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.textarea-editor {
height: 50vh !important;
}
.editor-field-info {
margin-top: 0.6rem;
display: flex;
justify-content: space-between;
white-space: normal;
}
.editor-buttons {
display: flex;
justify-content: space-evenly;
.btn {
display: flex;
align-items: center;
}
}
</style>

View File

@@ -6,15 +6,19 @@
<button <button
class="btn btn-link btn-sm" class="btn btn-link btn-sm"
:class="{'loading':isQuering}" :class="{'loading':isQuering}"
@click="getTableData" @click="reloadTable"
> >
<span>{{ $t('word.refresh') }}</span> <span>{{ $t('word.refresh') }}</span>
<i class="material-icons ml-1">refresh</i> <i class="mdi mdi-24px mdi-refresh ml-1" />
</button>
<button
class="btn btn-link btn-sm"
:class="{'disabled':isQuering}"
@click="showAddModal"
>
<span>{{ $t('word.add') }}</span>
<i class="mdi mdi-24px mdi-playlist-plus ml-1" />
</button> </button>
<!-- <button class="btn btn-link btn-sm">
<span>{{ $t('word.save') }}</span>
<i class="material-icons ml-1">save</i>
</button> -->
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div v-if="results.rows"> <div v-if="results.rows">
@@ -29,33 +33,49 @@
<div class="workspace-query-results column col-12"> <div class="workspace-query-results column col-12">
<WorkspaceQueryTable <WorkspaceQueryTable
v-if="results" v-if="results"
ref="queryTable"
:results="results" :results="results"
:fields="resultsFields" :tab-uid="tabUid"
@update-field="updateField"
@delete-selected="deleteSelected"
/> />
</div> </div>
<ModalNewTableRow
v-if="isAddModal"
:tab-uid="tabUid"
@hide="hideAddModal"
@reload="reloadTable"
/>
</div> </div>
</template> </template>
<script> <script>
import Structure from '@/ipc-api/Structure'; import Tables from '@/ipc-api/Tables';
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable'; import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
import ModalNewTableRow from '@/components/ModalNewTableRow';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import tableTabs from '@/mixins/tableTabs';
export default { export default {
name: 'WorkspaceTableTab', name: 'WorkspaceTableTab',
components: { components: {
WorkspaceQueryTable WorkspaceQueryTable,
ModalNewTableRow
}, },
mixins: [tableTabs],
props: { props: {
connection: Object, connection: Object,
table: String table: String
}, },
data () { data () {
return { return {
tabUid: 'data',
isQuering: false, isQuering: false,
results: {}, results: {},
fields: [], fields: [],
lastTable: null keyUsage: [],
lastTable: null,
isAddModal: false
}; };
}, },
computed: { computed: {
@@ -66,26 +86,17 @@ export default {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
}, },
isSelected () { isSelected () {
return this.workspace.selected_tab === 1; return this.workspace.selected_tab === 'data';
},
resultsFields () {
return this.fields.map(field => { // TODO: move to main process
return {
name: field.COLUMN_NAME,
key: field.COLUMN_KEY.toLowerCase(),
type: field.DATA_TYPE
};
});
} }
}, },
watch: { watch: {
table: function () { table () {
if (this.isSelected) { if (this.isSelected) {
this.getTableData(); this.getTableData();
this.lastTable = this.table; this.lastTable = this.table;
} }
}, },
isSelected: function (val) { isSelected (val) {
if (val && this.lastTable !== this.table) { if (val && this.lastTable !== this.table) {
this.getTableData(); this.getTableData();
this.lastTable = this.table; this.lastTable = this.table;
@@ -97,12 +108,15 @@ export default {
}, },
methods: { methods: {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification' addNotification: 'notifications/addNotification',
setTabFields: 'workspaces/setTabFields',
setTabKeyUsage: 'workspaces/setTabKeyUsage'
}), }),
async getTableData () { async getTableData () {
if (!this.table) return; if (!this.table) return;
this.isQuering = true; this.isQuering = true;
this.results = {}; this.results = {};
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: [] });
const params = { const params = {
uid: this.connection.uid, uid: this.connection.uid,
@@ -110,10 +124,12 @@ export default {
table: this.workspace.breadcrumbs.table table: this.workspace.breadcrumbs.table
}; };
try { try { // Columns data
const { status, response } = await Structure.getTableColumns(params); const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') if (status === 'success') {
this.fields = response.rows; this.fields = response;// Needed to add new rows
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: response });
}
else else
this.addNotification({ status: 'error', message: response }); this.addNotification({ status: 'error', message: response });
} }
@@ -121,8 +137,8 @@ export default {
this.addNotification({ status: 'error', message: err.stack }); this.addNotification({ status: 'error', message: err.stack });
} }
try { try { // Table data
const { status, response } = await Structure.getTableData(params); const { status, response } = await Tables.getTableData(params);
if (status === 'success') if (status === 'success')
this.results = response; this.results = response;
@@ -133,44 +149,65 @@ export default {
this.addNotification({ status: 'error', message: err.stack }); this.addNotification({ status: 'error', message: err.stack });
} }
try { // Key usage (foreign keys)
const { status, response } = await Tables.getKeyUsage(params);
if (status === 'success') {
this.keyUsage = response;// Needed to add new rows
this.setTabKeyUsage({ cUid: this.connection.uid, tUid: this.tabUid, keyUsage: response });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false; this.isQuering = false;
},
reloadTable () {
this.getTableData();
},
showAddModal () {
this.isAddModal = true;
},
hideAddModal () {
this.isAddModal = false;
} }
} }
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.workspace-tabs{ .workspace-tabs {
align-content: baseline; align-content: baseline;
.workspace-query-runner{ .workspace-query-runner {
.workspace-query-runner-footer {
display: flex;
justify-content: space-between;
padding: 0.3rem 0.6rem 0.4rem;
align-items: center;
.workspace-query-runner-footer{ .workspace-query-buttons {
display: flex; display: flex;
justify-content: space-between;
padding: .3rem .6rem .4rem;
align-items: center;
.workspace-query-buttons{ .btn {
display: flex; display: flex;
align-self: center;
.btn{ color: $body-font-color;
display: flex; margin-right: 0.4rem;
align-self: center; }
color: $body-font-color;
margin-right: .4rem;
}
}
.workspace-query-info{
display: flex;
> div + div{
padding-left: .6rem;
}
}
} }
}
.workspace-query-info {
display: flex;
> div + div {
padding-left: 0.6rem;
}
}
}
}
} }
</style> </style>

View File

@@ -0,0 +1,87 @@
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: 'Schema',
results: 'النتائج',
size: 'الحجم',
seconds: 'ثواني',
type: 'نوع',
mimeType: 'نوع الميديا',
download: 'تحميل',
add: 'أضف',
data: 'بيانات',
properties: 'خصائص',
insert: 'أدرج'
},
message: {
appWelcome: 'مرحبا بك في عميل الSQL انتاريس!',
appFirstStep: 'خطوتك الأولى قم بإنشاء إتصال جديد بقاعدة بيانات.',
addConnection: 'إضافة إتصال',
createConnection: 'إنشاء إتصال',
createNewConnection: 'إنشاء إتصال جديد',
askCredentials: 'إطلب بيانات الدخول',
testConnection: 'إختبر الإتصال',
editConnection: 'عدل الإتصال',
deleteConnection: 'إحذف الإتصال',
deleteConnectionCorfirm: 'هل أنت متأكد من حذف الإتصال؟',
connectionSuccessfullyMade: 'تم الإتصال بنجاح!',
madeWithJS: 'بني بـ 💛 و جافاسكربت!',
checkForUpdates: 'تأكد من التحديثات',
noUpdatesAvailable: 'لا توجد تحديثات',
checkingForUpdate: 'البحث عن تحديثات',
checkFailure: 'فشل البحث, نرجوا المحاولة في وقت لاحق',
updateAvailable: 'تحديث جديد متوفر',
downloadingUpdate: 'جاري تحميل التحديث',
updateDownloaded: 'تم تحميل التحديث',
restartToInstall: 'قم بإعادة تشغيل انتاريس للتحديث',
unableEditFieldWithoutPrimary: 'لا يمكن تعديل الخانة بدون وجود مفتاح رئيسي في النتائج',
editCell: 'تعديل الخلية',
deleteRows: 'حذف صف | حذف {count} صفوف',
confirmToDeleteRows: 'هل أنت متأكد من حذف صف واحد؟? | هل أنت متأكد من حذف {count} صف?',
notificationsTimeout: 'إنتهاء التنبيهات',
uploadFile: 'رفع ملف',
addNewRow: 'إضافة صف جديد',
numberOfInserts: 'عدد الإدراجات'
},
// Date and Time
short: {
year: 'numeric',
month: 'short',
day: 'numeric'
},
long: {
year: 'numeric',
month: 'short',
day: 'numeric',
weekday: 'short',
hour: 'numeric',
minute: 'numeric'
}
};

View File

@@ -29,7 +29,16 @@ module.exports = {
donate: 'Donate', donate: 'Donate',
run: 'Run', run: 'Run',
schema: 'Schema', schema: 'Schema',
results: 'Results' results: 'Results',
size: 'Size',
seconds: 'Seconds',
type: 'Type',
mimeType: 'Mime-Type',
download: 'Download',
add: 'Add',
data: 'Data',
properties: 'Properties',
insert: 'Insert'
}, },
message: { message: {
appWelcome: 'Welcome to Antares SQL Client!', appWelcome: 'Welcome to Antares SQL Client!',
@@ -51,7 +60,17 @@ module.exports = {
updateAvailable: 'Update available', updateAvailable: 'Update available',
downloadingUpdate: 'Downloading update', downloadingUpdate: 'Downloading update',
updateDownloaded: 'Update downloaded', updateDownloaded: 'Update downloaded',
restartToInstall: 'Restart Antares to install' restartToInstall: 'Restart Antares to install',
unableEditFieldWithoutPrimary: 'Unable to edit a field without a primary key in resultset',
editCell: 'Edit cell',
deleteRows: 'Delete row | Delete {count} rows',
confirmToDeleteRows: 'Do you confirm to delete one row? | Do you confirm to delete {count} rows?',
notificationsTimeout: 'Notifications timeout',
uploadFile: 'Upload file',
addNewRow: 'Add new row',
numberOfInserts: 'Number of inserts',
openNewTab: 'Open a new tab',
affectedRows: 'Affected rows'
}, },
// Date and Time // Date and Time
short: { short: {

View File

@@ -6,7 +6,8 @@ Vue.use(VueI18n);
const i18n = new VueI18n({ const i18n = new VueI18n({
messages: { messages: {
'en-US': require('./en-US'), 'en-US': require('./en-US'),
'it-IT': require('./it-IT') 'it-IT': require('./it-IT'),
'ar-SA': require('./ar-SA')
} }
}); });
export default i18n; export default i18n;

View File

@@ -27,7 +27,9 @@ module.exports = {
language: 'Lingua', language: 'Lingua',
version: 'Versione', version: 'Versione',
donate: 'Dona', donate: 'Dona',
run: 'Esegui' run: 'Esegui',
schema: 'Schema',
results: 'Results'
}, },
message: { message: {
appWelcome: 'Benvenuto in Antares SQL Client!', appWelcome: 'Benvenuto in Antares SQL Client!',
@@ -41,7 +43,19 @@ module.exports = {
deleteConnection: 'Elimina connessione', deleteConnection: 'Elimina connessione',
deleteConnectionCorfirm: 'Confermi l\'eliminazione di', deleteConnectionCorfirm: 'Confermi l\'eliminazione di',
connectionSuccessfullyMade: 'Connessione avvenuta con successo!', connectionSuccessfullyMade: 'Connessione avvenuta con successo!',
madeWithJS: 'Fatto con 💛 e JavaScript!' madeWithJS: 'Fatto con 💛 e JavaScript!',
checkForUpdates: 'Cerca aggiornamenti',
noUpdatesAvailable: 'Nessun aggiornamento disponibile',
checkingForUpdate: 'Controllo aggiornamenti in corso',
checkFailure: 'Controllo fallito, riprova più tardi',
updateAvailable: 'Aggiornamento disponibile',
downloadingUpdate: 'Download dell\'aggiornamento',
updateDownloaded: 'Aggiornamento scaricato',
restartToInstall: 'Riavvia Antares per installare l\'aggiornamento',
unableEditFieldWithoutPrimary: 'Impossibile modificare il campo senza una primary key nel resultset',
editCell: 'Modifica cella',
deleteRows: 'Elimina riga | Elimina {count} righe',
confirmToDeleteRows: 'Confermi di voler cancellare una riga? | Confermi di voler cancellare {count} righe?'
}, },
// Date and Time // Date and Time
short: { short: {

View File

@@ -1,4 +1,5 @@
export default { export default {
'en-US': 'English', 'en-US': 'English',
'it-IT': 'Italiano' 'it-IT': 'Italiano',
'ar-SA': 'العربية'
}; };

View File

@@ -3,11 +3,11 @@ import { ipcRenderer } from 'electron';
export default class { export default class {
static makeTest (params) { static makeTest (params) {
return ipcRenderer.invoke('testConnection', params); return ipcRenderer.invoke('test-connection', params);
} }
static checkConnection (params) { static checkConnection (params) {
return ipcRenderer.invoke('checkConnection', params); return ipcRenderer.invoke('check-connection', params);
} }
static connect (params) { static connect (params) {
@@ -23,6 +23,6 @@ export default class {
} }
static rawQuery (params) { static rawQuery (params) {
return ipcRenderer.invoke('rawQuery', params); return ipcRenderer.invoke('raw-query', params);
} }
} }

View File

@@ -1,12 +0,0 @@
'use strict';
import { ipcRenderer } from 'electron';
export default class {
static getTableColumns (params) {
return ipcRenderer.invoke('getTableColumns', params);
}
static getTableData (params) {
return ipcRenderer.invoke('getTableData', params);
}
}

View File

@@ -0,0 +1,32 @@
'use strict';
import { ipcRenderer } from 'electron';
export default class {
static getTableColumns (params) {
return ipcRenderer.invoke('get-table-columns', params);
}
static getTableData (params) {
return ipcRenderer.invoke('get-table-data', params);
}
static getKeyUsage (params) {
return ipcRenderer.invoke('get-key-usage', params);
}
static updateTableCell (params) {
return ipcRenderer.invoke('update-table-cell', params);
}
static deleteTableRows (params) {
return ipcRenderer.invoke('delete-table-rows', params);
}
static insertTableRows (params) {
return ipcRenderer.invoke('insert-table-rows', params);
}
static getForeignList (params) {
return ipcRenderer.invoke('get-foreign-list', params);
}
}

View File

@@ -2,7 +2,7 @@
import Vue from 'vue'; import Vue from 'vue';
import 'material-design-icons/iconfont/material-icons.css'; import '@mdi/font/css/materialdesignicons.css';
import '@/scss/main.scss'; import '@/scss/main.scss';
import App from '@/App.vue'; import App from '@/App.vue';

View File

@@ -0,0 +1,59 @@
import Tables from '@/ipc-api/Tables';
export default {
methods: {
async updateField (payload) {
this.isQuering = true;
const params = {
uid: this.connection.uid,
schema: this.workspace.breadcrumbs.schema,
table: this.table,
...payload
};
try {
const { status, response } = await Tables.updateTableCell(params);
if (status === 'success') {
if (response.reload)// Needed for blob fields
this.reloadTable();
else
this.$refs.queryTable.applyUpdate(payload);
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false;
},
async deleteSelected (payload) {
this.isQuering = true;
const params = {
uid: this.connection.uid,
schema: this.workspace.breadcrumbs.schema,
table: this.workspace.breadcrumbs.table,
...payload
};
try {
const { status, response } = await Tables.deleteTableRows(params);
if (status === 'success') {
const { primary, rows } = params;
this.results = { ...this.results, rows: this.results.rows.filter(row => !rows.includes(row[primary])) };
this.$refs.queryTable.refreshScroller();// Necessary to re-render virtual scroller
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false;
}
}
};

View File

@@ -1,44 +1,48 @@
@mixin type-colors($types) { @mixin type-colors($types) {
@each $type, $color in $types { @each $type, $color in $types {
.type-#{$type} { .type-#{$type} {
color: $color; color: $color;
@if $type == 'number'{ @if $type == "number" {
text-align: right; text-align: right;
}
} }
} }
}
} }
@include type-colors(( @include type-colors(
"char": seagreen, (
"varchar": seagreen, "char": seagreen,
"text": seagreen, "varchar": seagreen,
"mediumtext": seagreen, "text": seagreen,
"mediumtext": seagreen,
"longtext": seagreen,
"int": cornflowerblue,
"tinyint": cornflowerblue,
"smallint": cornflowerblue,
"mediumint": cornflowerblue,
"float": cornflowerblue,
"double": cornflowerblue,
"decimal": cornflowerblue,
"bigint": cornflowerblue,
"datetime": coral,
"date": coral,
"time": coral,
"timestamp": coral,
"bit": lightskyblue,
"blob": darkorchid,
"mediumblob": darkorchid,
"longblob": darkorchid,
"enum": gold,
"set": gold,
"unknown": gray,
)
);
"int": cornflowerblue, .is-null {
"tinyint": cornflowerblue, color: gray;
"smallint": cornflowerblue,
"mediumint": cornflowerblue,
"datetime": coral,
"date": coral,
"time": coral,
"timestamp": coral,
"bit": lightskyblue, &::after {
content: "NULL";
"blob": darkorchid, }
"mediumblob": darkorchid, }
"longblob": darkorchid,
"unknown": gray,
));
.is-null{
color: gray;
&::after{
content: 'NULL';
}
}

View File

@@ -1,26 +1,26 @@
.dbi{ .dbi {
display: inline-block; display: inline-block;
width: 42px; width: 42px;
height: 42px; height: 42px;
background-size: cover; background-size: cover;
&.dbi-mysql{ &.dbi-mysql {
background-image: url('../images/svg/mysql.svg'); background-image: url("../images/svg/mysql.svg");
} }
&.dbi-maria{ &.dbi-maria {
background-image: url('../images/svg/mariadb.svg'); background-image: url("../images/svg/mariadb.svg");
} }
&.dbi-mssql{ &.dbi-mssql {
background-image: url('../images/svg/mssql.svg'); background-image: url("../images/svg/mssql.svg");
} }
&.dbi-pg{ &.dbi-pg {
background-image: url('../images/svg/pg.svg'); background-image: url("../images/svg/pg.svg");
} }
&.dbi-oracledb{ &.dbi-oracledb {
background-image: url('../images/svg/oracledb.svg'); background-image: url("../images/svg/oracledb.svg");
} }
} }

View File

@@ -1,65 +1,71 @@
.table { .table {
border-collapse: collapse; border-collapse: collapse;
border-spacing: 0; border-spacing: 0;
width: 100%; width: 100%;
display: table; display: table;
table-layout: fixed;
&.table-striped {
.tbody {
.tr:nth-of-type(odd) {
background: $bg-color;
}
}
}
&,
&.table-striped {
.tbody {
.tr {
&.active {
background: $bg-color-dark;
}
}
}
}
&.table-hover {
.tbody {
.tr {
&:hover {
background: $bg-color-dark;
}
}
}
}
// Scollable tables
&.table-scroll {
display: block;
overflow-x: auto;
padding-bottom: .75rem;
white-space: nowrap;
}
.thead{ .tbody {
display: table-header-group;
}
.tbody{
display: table-row-group; display: table-row-group;
} }
.tr{ .tr {
display: table-row; display: table-row;
} }
.td, // Scollable tables
.th { &.table-scroll {
border-bottom: $border-width solid $border-color; display: block;
padding: $unit-3 $unit-2; overflow-x: auto;
display: table-cell; padding-bottom: 0.75rem;
} white-space: nowrap;
.th { }
border-bottom-width: $border-width-lg;
} .thead {
} display: table-header-group;
}
.td,
.th {
border-bottom: $border-width solid $border-color;
padding: $unit-3 $unit-2;
display: table-cell;
}
.th {
border-bottom-width: $border-width-lg;
}
&,
&.table-striped {
.tbody {
.tr {
&.selected {
background: #333 !important;
}
&.active {
background: $bg-color-dark;
}
}
}
}
&.table-hover {
.tbody {
.tr {
&:hover {
background: $bg-color-dark;
}
}
}
}
&.table-striped {
.tbody {
.tr:nth-of-type(odd) {
background: $bg-color;
}
}
}
}

View File

@@ -1,16 +1,9 @@
.mdi {
display: flex;
align-items: center;
justify-content: center;
.material-icons{// TODO: rewrite with rem &::before {
/* Rules for sizing the icon. */ line-height: 1;
&.md-18 { font-size: 18px; } }
&.md-24 { font-size: 24px; } }
&.md-36 { font-size: 36px; }
&.md-48 { font-size: 48px; }
/* Rules for using icons as black on a light background. */
&.md-dark { color: rgba(0, 0, 0, 0.54); }
&.md-dark.md-inactive { color: rgba(0, 0, 0, 0.26); }
/* Rules for using icons as white on a dark background. */
&.md-light { color: rgba(255, 255, 255, 1); }
&.md-light.md-inactive { color: rgba(255, 255, 255, 0.3); }
}

View File

@@ -0,0 +1,18 @@
.column-key {
transform: rotate(90deg);
font-size: 0.7rem;
line-height: 1.5;
margin-right: 0.2rem;
&.key-pri {
color: goldenrod;
}
&.key-uni {
color: deepskyblue;
}
&.key-mul {
color: palegreen;
}
}

View File

@@ -1,10 +1,41 @@
.slide-fade-enter-active { .slide-fade-enter-active {
transition: all .3s ease; transition: all 0.3s ease;
} }
.slide-fade-leave-active {
transition: all .3s cubic-bezier(1.0, 0.5, 0.8, 1.0); .slide-fade-leave-active {
} transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
.slide-fade-enter, .slide-fade-leave-to { }
transform: translateX(10px);
opacity: 0; .slide-fade-enter,
} .slide-fade-leave-to {
transform: translateX(10px);
opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.jump-down-enter-active {
animation: jump-down-in 0.2s;
}
.jump-down-leave-active {
animation: jump-down-in 0.2s reverse;
}
@keyframes jump-down-in {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}

View File

@@ -1,4 +1,4 @@
/*Colors*/ /* Colors */
$body-bg: #1d1d1d; $body-bg: #1d1d1d;
$body-font-color: #fff; $body-font-color: #fff;
$bg-color: #1d1d1d; $bg-color: #1d1d1d;
@@ -7,10 +7,11 @@ $bg-color-gray: #272727;
$primary-color: #e36929; $primary-color: #e36929;
$success-color: #32b643; $success-color: #32b643;
$error-color: #de3b28; $error-color: #de3b28;
$warning-color: #e0a40c;
/*Sizes*/ /* Sizes */
$titlebar-height: 1.5rem; $titlebar-height: 1.5rem;
$settingbar-width: 3rem; $settingbar-width: 3rem;
$explorebar-width: 14rem; $explorebar-width: 14rem;
$footer-height: 1.5rem; $footer-height: 1.5rem;
$excluding-size: $footer-height + $titlebar-height; $excluding-size: $footer-height + $titlebar-height;

View File

@@ -1,150 +1,175 @@
@import "~spectre.css/src/variables"; @import "~spectre.css/src/variables";
@import "variables"; @import "variables";
@import "transitions"; @import "transitions";
@import "data-types"; @import "data-types";
@import "table-keys";
@import "fake-tables"; @import "fake-tables";
@import "mdi-additions"; @import "mdi-additions";
@import "db-icons"; @import "db-icons";
@import "~spectre.css/src/spectre"; @import "~spectre.css/src/spectre";
@import "~spectre.css/src/spectre-exp"; @import "~spectre.css/src/spectre-exp";
body{ body {
user-select: none; user-select: none;
} }
/*Additions*/ /* Additions */
@include margin-variant(3, $unit-3); @include margin-variant(3, $unit-3);
@include margin-variant(4, $unit-4); @include margin-variant(4, $unit-4);
@include padding-variant(3, $unit-3); @include padding-variant(3, $unit-3);
@include padding-variant(4, $unit-4); @include padding-variant(4, $unit-4);
.btn.btn-gray{ .btn.btn-gray {
color: #fff; color: #fff;
background: $bg-color-gray; background: $bg-color-gray;
&:hover{ &:hover {
background: $bg-color; background: $bg-color;
} }
} }
.p-vcentered{ .p-vcentered {
display: flex!important; display: flex !important;
align-items: center; align-items: center;
} }
.c-help{ .c-help {
cursor: help; cursor: help;
}
.bg-checkered {
background-image:
linear-gradient(to right, rgba(192, 192, 192, 0.75), rgba(192, 192, 192, 0.75)),
linear-gradient(to right, black 50%, white 50%),
linear-gradient(to bottom, black 50%, white 50%);
background-blend-mode: normal, difference, normal;
background-size: 2em 2em;
} }
// Scrollbars // Scrollbars
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 10px; width: 10px;
height: 10px; height: 10px;
} }
::-webkit-scrollbar-track {
background: $bg-color-light;
}
::-webkit-scrollbar-thumb {
background: rgba($color: #FFF, $alpha: .5);
&:hover { ::-webkit-scrollbar-track {
background: rgba($color: #FFF, $alpha: 1); background: $bg-color-light;
} }
::-webkit-scrollbar-thumb {
background: rgba($color: #fff, $alpha: 0.5);
&:hover {
background: rgba($color: #fff, $alpha: 1);
}
} }
// Animations // Animations
@keyframes rotation { @keyframes rotation {
from { from {
transform: rotate(0deg); transform: rotate(0deg);
} }
to {
transform: rotate(359deg); to {
} transform: rotate(359deg);
} }
}
.rotate { .rotate {
animation: rotation .8s infinite linear; animation: rotation 0.8s infinite linear;
} }
/*Override*/ /* Override */
.modal{ .modal {
.modal-overlay, .modal-overlay,
&.active .modal-overlay{ &.active .modal-overlay {
background: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.15);
} }
.modal-sm .modal-container, .modal-container,
.modal-container{ .modal-sm .modal-container {
box-shadow: 0 0 1px 0px #000; box-shadow: 0 0 1px 0 #000;
padding: 0; padding: 0;
background: $bg-color; background: $bg-color;
.modal-header{ .modal-header {
padding: .4rem .8rem; padding: 0.4rem 0.8rem;
text-transform: uppercase; text-transform: uppercase;
background: $bg-color-gray; background: $bg-color-gray;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
color: #FFF; color: #fff;
} }
} }
} }
.tab{ .tab {
border-color: #272727; border-color: #272727;
.tab-item {
.btn-clear {
margin-top: -0.1rem;
font-size: 0.6rem;
}
}
} }
.panel{ .panel {
border: none; border: none;
} }
.badge{ .badge {
&[data-badge], &[data-badge],
&:not([data-badge]){ &:not([data-badge]) {
&::after { &::after {
box-shadow: none; box-shadow: none;
} }
} }
} }
.form-select{ .form-select {
cursor: pointer; cursor: pointer;
} }
.form-select, .form-select,
.form-select:not([multiple]):not([size]),
.form-input, .form-input,
.form-checkbox .form-icon, .form-select:not([multiple]):not([size]),
.form-radio .form-icon{ .form-checkbox .form-icon,
border-color: $bg-color-light; .form-radio .form-icon {
background: $bg-color-gray; border-color: $bg-color-light;
background-color: $bg-color-gray;
} }
.form-select:not([multiple]):not([size]):focus{ .form-input:not(:placeholder-shown):invalid:focus {
border-color: $primary-color; background: $bg-color-gray;
} }
.menu{ .form-select:not([multiple]):not([size]):focus {
font-size: .7rem; border-color: $primary-color;
.menu-item { }
+ .menu-item{
margin-top: 0;
}
}
} .input-group .input-group-addon {
border-color: #3f3f3f;
}
.menu {
font-size: 0.7rem;
.menu-item {
+ .menu-item {
margin-top: 0;
}
}
}
.accordion-body { .accordion-body {
max-height: 500rem!important; max-height: 500rem !important;
} }
.btn.loading { .btn.loading {
> .material-icons, > .mdi,
> span{ > span {
visibility: hidden; visibility: hidden;
} }
} }

View File

@@ -24,7 +24,7 @@ export default {
isSettingModal: state => state.is_setting_modal, isSettingModal: state => state.is_setting_modal,
selectedSettingTab: state => state.selected_setting_tab, selectedSettingTab: state => state.selected_setting_tab,
getUpdateStatus: state => state.update_status, getUpdateStatus: state => state.update_status,
getDownloadProgress: state => state.download_progress getDownloadProgress: state => Number(state.download_progress.toFixed(1))
}, },
mutations: { mutations: {
SET_LOADING_STATUS (state, payload) { SET_LOADING_STATUS (state, payload) {

View File

@@ -1,5 +1,5 @@
'use strict'; 'use strict';
import { uidGen } from 'common/libs/utilities'; import { uidGen } from 'common/libs/uidGen';
export default { export default {
namespaced: true, namespaced: true,
@@ -20,7 +20,7 @@ export default {
}, },
actions: { actions: {
addNotification ({ commit }, payload) { addNotification ({ commit }, payload) {
payload.uid = uidGen(); payload.uid = uidGen('N');
commit('ADD_NOTIFICATION', payload); commit('ADD_NOTIFICATION', payload);
}, },
removeNotification ({ commit }, uid) { removeNotification ({ commit }, uid) {

View File

@@ -6,17 +6,22 @@ export default {
strict: true, strict: true,
state: { state: {
locale: 'en-US', locale: 'en-US',
explorebar_size: null explorebar_size: null,
notifications_timeout: 10
}, },
getters: { getters: {
getLocale: state => state.locale, getLocale: state => state.locale,
getExplorebarSize: state => state.explorebar_size getExplorebarSize: state => state.explorebar_size,
getNotificationsTimeout: state => state.notifications_timeout
}, },
mutations: { mutations: {
SET_LOCALE (state, locale) { SET_LOCALE (state, locale) {
state.locale = locale; state.locale = locale;
i18n.locale = locale; i18n.locale = locale;
}, },
SET_NOTIFICATIONS_TIMEOUT (state, timeout) {
state.notifications_timeout = timeout;
},
SET_EXPLOREBAR_SIZE (state, size) { SET_EXPLOREBAR_SIZE (state, size) {
state.explorebar_size = size; state.explorebar_size = size;
} }
@@ -25,6 +30,9 @@ export default {
changeLocale ({ commit }, locale) { changeLocale ({ commit }, locale) {
commit('SET_LOCALE', locale); commit('SET_LOCALE', locale);
}, },
updateNotificationsTimeout ({ commit }, timeout) {
commit('SET_NOTIFICATIONS_TIMEOUT', timeout);
},
changeExplorebarSize ({ commit }, size) { changeExplorebarSize ({ commit }, size) {
commit('SET_EXPLOREBAR_SIZE', size); commit('SET_EXPLOREBAR_SIZE', size);
} }

View File

@@ -1,8 +1,9 @@
'use strict'; 'use strict';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import { uidGen } from 'common/libs/utilities'; import { uidGen } from 'common/libs/uidGen';
const tabIndex = [];
function remapStructure (structure) { function remapStructure (structure) { // TODO: move to main process and add fields (for autocomplete purpose)
const databases = structure.map(table => table.TABLE_SCHEMA) const databases = structure.map(table => table.TABLE_SCHEMA)
.filter((value, index, self) => self.indexOf(value) === index); .filter((value, index, self) => self.indexOf(value) === index);
@@ -28,8 +29,14 @@ export default {
return null; return null;
}, },
getWorkspace: state => uid => { getWorkspace: state => uid => {
const workspace = state.workspaces.filter(workspace => workspace.uid === uid); return state.workspaces.find(workspace => workspace.uid === uid);
return workspace.length ? workspace[0] : {}; },
getWorkspaceTab: (state, getters) => tUid => {
if (!getters.getSelected) return;
const workspace = state.workspaces.find(workspace => workspace.uid === getters.getSelected);
if ('tabs' in workspace)
return workspace.tabs.find(tab => tab.uid === tUid);
return {};
}, },
getConnected: state => { getConnected: state => {
return state.workspaces return state.workspaces
@@ -57,10 +64,15 @@ export default {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, breadcrumbs } : workspace); state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, breadcrumbs } : workspace);
}, },
NEW_TAB (state, uid) { NEW_TAB (state, uid) {
tabIndex[uid] = tabIndex[uid] ? ++tabIndex[uid] : 1;
const newTab = { const newTab = {
uid: uidGen(), uid: uidGen('T'),
index: tabIndex[uid],
selected: false, selected: false,
type: 'query' type: 'query',
fields: [],
keyUsage: []
}; };
state.workspaces = state.workspaces.map(workspace => { state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === uid) { if (workspace.uid === uid) {
@@ -73,8 +85,54 @@ export default {
return workspace; return workspace;
}); });
}, },
REMOVE_TAB (state, { uid, tab: tUid }) {
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === uid) {
return {
...workspace,
tabs: workspace.tabs.filter(tab => tab.uid !== tUid)
};
}
else
return workspace;
});
},
SELECT_TAB (state, { uid, tab }) { SELECT_TAB (state, { uid, tab }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, selected_tab: tab } : workspace); state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, selected_tab: tab } : workspace);
},
SET_TAB_FIELDS (state, { cUid, tUid, fields }) {
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === cUid) {
return {
...workspace,
tabs: workspace.tabs.map(tab => {
if (tab.uid === tUid)
return { ...tab, fields };
else
return tab;
})
};
}
else
return workspace;
});
},
SET_TAB_KEY_USAGE (state, { cUid, tUid, keyUsage }) {
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === cUid) {
return {
...workspace,
tabs: workspace.tabs.map(tab => {
if (tab.uid === tUid)
return { ...tab, keyUsage };
else
return tab;
})
};
}
else
return workspace;
});
} }
}, },
actions: { actions: {
@@ -115,14 +173,25 @@ export default {
uid, uid,
connected: false, connected: false,
selected_tab: 0, selected_tab: 0,
tabs: [{ uid: 1, type: 'table' }], tabs: [{
uid: 'data',
type: 'table',
fields: [],
keyUsage: []
},
{
uid: 'prop',
type: 'table',
fields: [],
keyUsage: []
}],
structure: {}, structure: {},
breadcrumbs: {} breadcrumbs: {}
}; };
commit('ADD_WORKSPACE', workspace); commit('ADD_WORKSPACE', workspace);
if (getters.getWorkspace(uid).tabs.length < 2) if (getters.getWorkspace(uid).tabs.length < 3)
dispatch('newTab', uid); dispatch('newTab', uid);
}, },
changeBreadcrumbs ({ commit, getters }, payload) { changeBreadcrumbs ({ commit, getters }, payload) {
@@ -131,8 +200,17 @@ export default {
newTab ({ commit }, uid) { newTab ({ commit }, uid) {
commit('NEW_TAB', uid); commit('NEW_TAB', uid);
}, },
removeTab ({ commit }, payload) {
commit('REMOVE_TAB', payload);
},
selectTab ({ commit }, payload) { selectTab ({ commit }, payload) {
commit('SELECT_TAB', payload); commit('SELECT_TAB', payload);
},
setTabFields ({ commit }, payload) {
commit('SET_TAB_FIELDS', payload);
},
setTabKeyUsage ({ commit }, payload) {
commit('SET_TAB_KEY_USAGE', payload);
} }
} }
}; };

View File

@@ -1,23 +1,23 @@
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
export default store => { export default store => {
ipcRenderer.on('checkingForUpdate', () => { ipcRenderer.on('checking-for-update', () => {
store.commit('application/CHANGE_UPDATE_STATUS', 'checking'); store.commit('application/CHANGE_UPDATE_STATUS', 'checking');
}); });
ipcRenderer.on('updateAvailable', () => { ipcRenderer.on('update-available', () => {
store.commit('application/CHANGE_UPDATE_STATUS', 'available'); store.commit('application/CHANGE_UPDATE_STATUS', 'available');
}); });
ipcRenderer.on('updateNotAvailable', () => { ipcRenderer.on('update-not-available', () => {
store.commit('application/CHANGE_UPDATE_STATUS', 'noupdate'); store.commit('application/CHANGE_UPDATE_STATUS', 'noupdate');
}); });
ipcRenderer.on('checkFailed', () => { ipcRenderer.on('check-failed', () => {
store.commit('application/CHANGE_UPDATE_STATUS', 'nocheck'); store.commit('application/CHANGE_UPDATE_STATUS', 'nocheck');
}); });
ipcRenderer.on('downloadProgress', (event, data) => { ipcRenderer.on('download-progress', (event, data) => {
store.commit('application/CHANGE_UPDATE_STATUS', 'downloading'); store.commit('application/CHANGE_UPDATE_STATUS', 'downloading');
store.commit('application/CHANGE_PROGRESS_PERCENTAGE', data.percent); store.commit('application/CHANGE_PROGRESS_PERCENTAGE', data.percent);
}); });
ipcRenderer.on('updateDownloaded', () => { ipcRenderer.on('update-downloaded', () => {
store.commit('application/CHANGE_UPDATE_STATUS', 'downloaded'); store.commit('application/CHANGE_UPDATE_STATUS', 'downloaded');
}); });
}; };

View File

@@ -0,0 +1,12 @@
import { functions } from '@/suggestions/sql/sql-functions';
import { keywords } from '@/suggestions/sql/sql-keywords';
import { operators } from '@/suggestions/sql/sql-operators';
import { variables } from '@/suggestions/sql/sql-variables';
export const completionItemProvider = (monaco) => {
return {
provideCompletionItems () {
return { suggestions: [...functions(monaco), ...keywords(monaco), ...operators(monaco), ...variables(monaco)] };
}
};
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,142 @@
export const operators = (monaco) => {
return [{
label: 'ALL',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'ALL'
},
{
label: 'AND',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'AND'
},
{
label: 'ANY',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'ANY'
},
{
label: 'BETWEEN',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'BETWEEN'
},
{
label: 'EXISTS',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'EXISTS'
},
{
label: 'IN',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'IN'
},
{
label: 'LIKE',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'LIKE'
},
{
label: 'NOT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'NOT'
},
{
label: 'OR',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'OR'
},
{
label: 'SOME',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'SOME'
},
{
label: 'EXCEPT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'EXCEPT'
},
{
label: 'INTERSECT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'INTERSECT'
},
{
label: 'UNION',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'UNION'
},
{
label: 'APPLY',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'APPLY'
},
{
label: 'CROSS',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'CROSS'
},
{
label: 'FULL',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'FULL'
},
{
label: 'INNER',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'INNER'
},
{
label: 'JOIN',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'JOIN'
},
{
label: 'LEFT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'LEFT'
},
{
label: 'OUTER',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'OUTER'
},
{
label: 'RIGHT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'RIGHT'
},
{
label: 'CONTAINS',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'CONTAINS'
},
{
label: 'FREETEXT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'FREETEXT'
},
{
label: 'IS',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'IS'
},
{
label: 'NULL',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'NULL'
},
{
label: 'PIVOT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'PIVOT'
},
{
label: 'UNPIVOT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'UNPIVOT'
},
{
label: 'MATCHED',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'MATCHED'
}];
};

View File

@@ -0,0 +1,172 @@
export const variables = (monaco) => {
return [{
label: '@@DATEFIRST',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@DATEFIRST'
},
{
label: '@@DBTS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@DBTS'
},
{
label: '@@LANGID',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@LANGID'
},
{
label: '@@LANGUAGE',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@LANGUAGE'
},
{
label: '@@LOCK_TIMEOUT',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@LOCK_TIMEOUT'
},
{
label: '@@MAX_CONNECTIONS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@MAX_CONNECTIONS'
},
{
label: '@@MAX_PRECISION',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@MAX_PRECISION'
},
{
label: '@@NESTLEVEL',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@NESTLEVEL'
},
{
label: '@@OPTIONS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@OPTIONS'
},
{
label: '@@REMSERVER',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@REMSERVER'
},
{
label: '@@SERVERNAME',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@SERVERNAME'
},
{
label: '@@SERVICENAME',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@SERVICENAME'
},
{
label: '@@SPID',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@SPID'
},
{
label: '@@TEXTSIZE',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@TEXTSIZE'
},
{
label: '@@VERSION',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@VERSION'
},
{
label: '@@CURSOR_ROWS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@CURSOR_ROWS'
},
{
label: '@@FETCH_STATUS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@FETCH_STATUS'
},
{
label: '@@DATEFIRST',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@DATEFIRST'
},
{
label: '@@PROCID',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@PROCID'
},
{
label: '@@ERROR',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@ERROR'
},
{
label: '@@IDENTITY',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@IDENTITY'
},
{
label: '@@ROWCOUNT',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@ROWCOUNT'
},
{
label: '@@TRANCOUNT',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@TRANCOUNT'
},
{
label: '@@CONNECTIONS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@CONNECTIONS'
},
{
label: '@@CPU_BUSY',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@CPU_BUSY'
},
{
label: '@@IDLE',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@IDLE'
},
{
label: '@@IO_BUSY',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@IO_BUSY'
},
{
label: '@@PACKET_ERRORS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@PACKET_ERRORS'
},
{
label: '@@PACK_RECEIVED',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@PACK_RECEIVED'
},
{
label: '@@PACK_SENT',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@PACK_SENT'
},
{
label: '@@TIMETICKS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@TIMETICKS'
},
{
label: '@@TOTAL_ERRORS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@TOTAL_ERRORS'
},
{
label: '@@TOTAL_READ',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@TOTAL_READ'
},
{
label: '@@TOTAL_WRITE',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@TOTAL_WRITE'
}];
};

View File

@@ -1,8 +1,11 @@
const webpack = require('webpack'); const webpack = require('webpack');
const MonacoEditorPlugin = require('monaco-editor-webpack-plugin');
module.exports = { module.exports = {
plugins: [ plugins: [
new MonacoEditorPlugin({
languages: ['sql']
}),
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': { 'process.env': {
PACKAGE_VERSION: JSON.stringify(require('./package.json').version) PACKAGE_VERSION: JSON.stringify(require('./package.json').version)
@@ -17,7 +20,7 @@ module.exports = {
{ {
loader: 'sass-loader', loader: 'sass-loader',
options: { options: {
prependData: '@import "@/scss/_variables.scss";' additionalData: '@import "@/scss/_variables.scss";'
} }
} }
] ]