From 5017b55944db6dafecd6f6525c5c43f4d7da6aef Mon Sep 17 00:00:00 2001 From: lana-k Date: Wed, 4 Aug 2021 22:20:51 +0200 Subject: [PATCH] Pivot implementation and redesign (#69) - Pivot support implementation - Rename queries into inquiries - Rename editor into workspace - Change result set format - New JSON format for inquiries - Redesign panels --- README.md | 9 +- package-lock.json | 111 ++- package.json | 7 +- public/{queries.json => inquiries.json} | 0 src/assets/images/arrow-hover.svg | 3 + src/assets/images/arrow.svg | 3 + src/assets/images/delete-tag-hover.svg | 3 + src/assets/images/delete-tag.svg | 3 + src/assets/images/sort.svg | 11 + src/assets/styles/multiselect.css | 136 ++++ src/assets/styles/tables.css | 38 +- src/assets/styles/variables.css | 7 +- src/components/CsvImport/csv.js | 28 +- src/components/CsvImport/index.vue | 7 +- src/components/DbUploader.vue | 6 +- src/components/IconButton.vue | 110 +++ src/components/SqlTable/Pager.vue | 1 + src/components/SqlTable/index.vue | 49 +- src/components/svg/addTable.vue | 16 +- src/components/svg/changeDb.vue | 16 +- src/components/svg/chart.vue | 22 + src/components/svg/dataView.vue | 19 + src/components/svg/export.vue | 17 +- src/components/svg/hint.vue | 16 +- src/components/svg/pivot.vue | 20 + src/components/svg/png.svg | 5 + src/components/svg/png.vue | 18 + src/components/svg/run.vue | 17 + src/components/svg/sort.vue | 45 ++ src/components/svg/sqlEditor.vue | 25 + src/components/svg/table.vue | 21 + src/lib/database/_sql.js | 24 +- src/lib/database/_statements.js | 21 +- src/lib/database/_worker.js | 3 +- src/lib/database/index.js | 11 +- src/lib/storedInquiries/_migrations.js | 12 + src/lib/storedInquiries/index.js | 120 ++++ src/lib/storedQueries.js | 96 --- src/lib/utils/fileIo.js | 10 +- src/main.js | 2 + src/router.js | 16 +- src/store/actions.js | 8 +- src/store/mutations.js | 20 +- src/store/state.js | 2 +- src/tooltipMixin.js | 24 +- src/views/Main/AppDiagnosticInfo.vue | 4 +- .../Main/Editor/Tabs/Tab/Chart/index.vue | 98 --- .../Main/Editor/Tabs/Tab/ViewSwitcher.vue | 67 -- src/views/Main/Editor/Tabs/Tab/index.vue | 225 ------ .../Main/{MyQueries => Inquiries}/index.vue | 293 ++++---- .../{MyQueries => Inquiries}/svg/copy.vue | 19 +- .../{MyQueries => Inquiries}/svg/delete.vue | 19 +- .../{MyQueries => Inquiries}/svg/rename.vue | 19 +- src/views/Main/MainMenu.vue | 112 ++- .../Schema/TableDescription.vue | 0 .../{Editor => Workspace}/Schema/index.vue | 0 .../Tabs/Tab/DataView}/Chart/chartHelper.js | 23 +- .../Tabs/Tab/DataView/Chart/index.vue | 118 ++++ .../DataView/Pivot/PivotUi/PivotSortBtn.vue | 71 ++ .../Tabs/Tab/DataView/Pivot/PivotUi/index.vue | 302 ++++++++ .../Tab/DataView/Pivot/PivotUi/pivotHelper.js | 77 ++ .../Tabs/Tab/DataView/Pivot/index.vue | 228 ++++++ .../Workspace/Tabs/Tab/DataView/index.vue | 118 ++++ .../Main/Workspace/Tabs/Tab/RunResult.vue | 130 ++++ .../Main/Workspace/Tabs/Tab/SideToolBar.vue | 67 ++ .../Tabs/Tab/SqlEditor/hint.js | 0 .../Tabs/Tab/SqlEditor/index.vue | 51 +- src/views/Main/Workspace/Tabs/Tab/index.vue | 160 +++++ .../Main/{Editor => Workspace}/Tabs/index.vue | 40 +- .../Main/{Editor => Workspace}/index.vue | 2 +- src/views/Main/index.vue | 2 +- src/views/Welcome.vue | 2 +- tests/components/CsvImport/CsvImport.spec.js | 158 ++--- tests/components/CsvImport/csv.spec.js | 24 +- tests/components/DbUploader.spec.js | 16 +- tests/components/Logs.spec.js | 1 + tests/lib/database/_sql.spec.js | 65 +- tests/lib/database/_statements.spec.js | 26 +- tests/lib/database/database.spec.js | 44 +- tests/lib/database/sqliteExtensions.spec.js | 119 +++- tests/lib/storedInquiries/_migrations.spec.js | 42 ++ .../storedInquiries/storedInquiries.spec.js | 432 ++++++++++++ tests/lib/storedQueries.spec.js | 267 ------- tests/store/actions.spec.js | 44 +- tests/store/mutations.spec.js | 150 ++-- tests/tooltipMixin.spec.js | 83 ++- .../Inquiries/Inquiries.spec.js} | 655 +++++++++++------- .../views/{MainView => Main}/MainMenu.spec.js | 262 ++++--- tests/views/Main/Workspace/Editor.spec.js | 23 + .../Workspace}/Schema/Schema.spec.js | 14 +- .../Schema/TableDescription.spec.js | 2 +- .../Tabs/Tab/DataView/Chart/Chart.spec.js | 50 ++ .../Tabs/Tab/DataView/Chart/DataView.spec.js | 32 + .../Tab/DataView/Chart/Pivot/Pivot.spec.js | 214 ++++++ .../Chart/Pivot/PivotUi/PivotSortBtn.spec.js | 21 + .../Chart/Pivot/PivotUi/PivotUi.spec.js | 143 ++++ .../Chart/Pivot/PivotUi/pivotHelper.spec.js | 56 ++ .../Tab/DataView}/Chart/chartHelper.spec.js | 22 +- .../Tabs/Tab/SqlEditor/SqlEditor.spec.js | 44 ++ .../Tabs/Tab/SqlEditor/hint.spec.js | 2 +- .../Workspace}/Tabs/Tab/Tab.spec.js | 135 ++-- .../Workspace}/Tabs/Tabs.spec.js | 48 +- .../Editor/Tabs/Tab/Chart/Chart.spec.js | 68 -- .../Tabs/Tab/SqlEditor/SqlEditor.spec.js | 11 - vue.config.js | 2 +- 105 files changed, 4659 insertions(+), 2021 deletions(-) rename public/{queries.json => inquiries.json} (100%) create mode 100644 src/assets/images/arrow-hover.svg create mode 100644 src/assets/images/arrow.svg create mode 100644 src/assets/images/delete-tag-hover.svg create mode 100644 src/assets/images/delete-tag.svg create mode 100644 src/assets/images/sort.svg create mode 100644 src/assets/styles/multiselect.css create mode 100644 src/components/IconButton.vue create mode 100644 src/components/svg/chart.vue create mode 100644 src/components/svg/dataView.vue create mode 100644 src/components/svg/pivot.vue create mode 100644 src/components/svg/png.svg create mode 100644 src/components/svg/png.vue create mode 100644 src/components/svg/run.vue create mode 100644 src/components/svg/sort.vue create mode 100644 src/components/svg/sqlEditor.vue create mode 100644 src/components/svg/table.vue create mode 100644 src/lib/storedInquiries/_migrations.js create mode 100644 src/lib/storedInquiries/index.js delete mode 100644 src/lib/storedQueries.js delete mode 100644 src/views/Main/Editor/Tabs/Tab/Chart/index.vue delete mode 100644 src/views/Main/Editor/Tabs/Tab/ViewSwitcher.vue delete mode 100644 src/views/Main/Editor/Tabs/Tab/index.vue rename src/views/Main/{MyQueries => Inquiries}/index.vue (54%) rename src/views/Main/{MyQueries => Inquiries}/svg/copy.vue (76%) rename src/views/Main/{MyQueries => Inquiries}/svg/delete.vue (70%) rename src/views/Main/{MyQueries => Inquiries}/svg/rename.vue (72%) rename src/views/Main/{Editor => Workspace}/Schema/TableDescription.vue (100%) rename src/views/Main/{Editor => Workspace}/Schema/index.vue (100%) rename src/views/Main/{Editor/Tabs/Tab => Workspace/Tabs/Tab/DataView}/Chart/chartHelper.js (53%) create mode 100644 src/views/Main/Workspace/Tabs/Tab/DataView/Chart/index.vue create mode 100644 src/views/Main/Workspace/Tabs/Tab/DataView/Pivot/PivotUi/PivotSortBtn.vue create mode 100644 src/views/Main/Workspace/Tabs/Tab/DataView/Pivot/PivotUi/index.vue create mode 100644 src/views/Main/Workspace/Tabs/Tab/DataView/Pivot/PivotUi/pivotHelper.js create mode 100644 src/views/Main/Workspace/Tabs/Tab/DataView/Pivot/index.vue create mode 100644 src/views/Main/Workspace/Tabs/Tab/DataView/index.vue create mode 100644 src/views/Main/Workspace/Tabs/Tab/RunResult.vue create mode 100644 src/views/Main/Workspace/Tabs/Tab/SideToolBar.vue rename src/views/Main/{Editor => Workspace}/Tabs/Tab/SqlEditor/hint.js (100%) rename src/views/Main/{Editor => Workspace}/Tabs/Tab/SqlEditor/index.vue (51%) create mode 100644 src/views/Main/Workspace/Tabs/Tab/index.vue rename src/views/Main/{Editor => Workspace}/Tabs/index.vue (83%) rename src/views/Main/{Editor => Workspace}/index.vue (98%) create mode 100644 tests/lib/storedInquiries/_migrations.spec.js create mode 100644 tests/lib/storedInquiries/storedInquiries.spec.js delete mode 100644 tests/lib/storedQueries.spec.js rename tests/views/{MainView/MyQueries/MyQueries.spec.js => Main/Inquiries/Inquiries.spec.js} (52%) rename tests/views/{MainView => Main}/MainMenu.spec.js (66%) create mode 100644 tests/views/Main/Workspace/Editor.spec.js rename tests/views/{MainView/Editor => Main/Workspace}/Schema/Schema.spec.js (95%) rename tests/views/{MainView/Editor => Main/Workspace}/Schema/TableDescription.spec.js (94%) create mode 100644 tests/views/Main/Workspace/Tabs/Tab/DataView/Chart/Chart.spec.js create mode 100644 tests/views/Main/Workspace/Tabs/Tab/DataView/Chart/DataView.spec.js create mode 100644 tests/views/Main/Workspace/Tabs/Tab/DataView/Chart/Pivot/Pivot.spec.js create mode 100644 tests/views/Main/Workspace/Tabs/Tab/DataView/Chart/Pivot/PivotUi/PivotSortBtn.spec.js create mode 100644 tests/views/Main/Workspace/Tabs/Tab/DataView/Chart/Pivot/PivotUi/PivotUi.spec.js create mode 100644 tests/views/Main/Workspace/Tabs/Tab/DataView/Chart/Pivot/PivotUi/pivotHelper.spec.js rename tests/views/{MainView/Editor/Tabs/Tab => Main/Workspace/Tabs/Tab/DataView}/Chart/chartHelper.spec.js (68%) create mode 100644 tests/views/Main/Workspace/Tabs/Tab/SqlEditor/SqlEditor.spec.js rename tests/views/{MainView/Editor => Main/Workspace}/Tabs/Tab/SqlEditor/hint.spec.js (98%) rename tests/views/{MainView/Editor => Main/Workspace}/Tabs/Tab/Tab.spec.js (65%) rename tests/views/{MainView/Editor => Main/Workspace}/Tabs/Tabs.spec.js (90%) delete mode 100644 tests/views/MainView/Editor/Tabs/Tab/Chart/Chart.spec.js delete mode 100644 tests/views/MainView/Editor/Tabs/Tab/SqlEditor/SqlEditor.spec.js diff --git a/README.md b/README.md index 383e224..20c2fde 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ Sqliteviz is a single-page offline-first PWA for fully client-side visualisation of SQLite databases or CSV files. With sqliteviz you can: -- run SQL queries against a SQLite database and create [Plotly][11] charts based on the result sets +- run SQL queries against a SQLite database and create [Plotly][11] charts and pivot tables based on the result sets - import a CSV file into a SQLite database and visualize imported data -- manage queries and chart settings and run them against different databases -- import/export queries and chart settings to/from a JSON file +- manage inquiries and run them against different databases +- import/export inquiries to/from a JSON file - export a modified SQLite database - use it offline from your OS application menu like any other desktop app @@ -26,7 +26,7 @@ For user documentation, check out sqliteviz [Wiki][7]. It's a kind of middleground between [Plotly Falcon][1] and [Redash][2]. ## Components -It is built on top of [react-chart-editor][3], [sql.js][4] and [Vue-Codemirror][8] in [Vue.js][5]. CSV parsing is performed with [Papa Parse][9]. +It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4] and [Vue-Codemirror][8] in [Vue.js][5]. CSV parsing is performed with [Papa Parse][9]. [1]: https://github.com/plotly/falcon [2]: https://github.com/getredash/redash @@ -39,3 +39,4 @@ It is built on top of [react-chart-editor][3], [sql.js][4] and [Vue-Codemirror][ [9]: https://www.papaparse.com/ [10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries [11]: https://github.com/plotly/plotly.js +[12]: https://github.com/nicolaskruchten/pivottable diff --git a/package-lock.json b/package-lock.json index 979e308..f3cb556 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,21 @@ { "name": "sqliteviz", - "version": "0.14.0", + "version": "0.15.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "sqliteviz", - "version": "0.14.0", + "version": "0.15.0", "license": "Apache-2.0", "dependencies": { "codemirror": "^5.57.0", "core-js": "^3.6.5", + "html2canvas": "^1.1.4", + "jquery": "^3.6.0", "nanoid": "^3.1.12", "papaparse": "^5.3.1", + "pivottable": "^2.23.0", "plotly.js": "^1.58.4", "promise-worker": "^2.0.1", "react": "^16.13.1", @@ -23,7 +26,9 @@ "vue": "^2.6.11", "vue-codemirror": "^4.0.6", "vue-js-modal": "^2.0.0-rc.6", + "vue-multiselect": "^2.1.6", "vue-router": "^3.2.0", + "vue2-teleport": "^1.0.1", "vuejs-paginate": "^2.1.0", "vuera": "^0.2.7", "vuex": "^3.4.0" @@ -6329,6 +6334,22 @@ "resolved": "https://registry.npmjs.org/css-global-keywords/-/css-global-keywords-1.0.1.tgz", "integrity": "sha1-cqmupyeW0Bmx0qMlLeTlqqN+Smk=" }, + "node_modules/css-line-break": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz", + "integrity": "sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==", + "dependencies": { + "base64-arraybuffer": "^0.2.0" + } + }, + "node_modules/css-line-break/node_modules/base64-arraybuffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz", + "integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/css-loader": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz", @@ -11192,6 +11213,17 @@ "object.getownpropertydescriptors": "^2.0.3" } }, + "node_modules/html2canvas": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.1.4.tgz", + "integrity": "sha512-uHgQDwrXsRmFdnlOVFvHin9R7mdjjZvoBoXxicPR+NnucngkaLa5zIDW9fzMkiip0jSffyTyWedE8iVogYOeWg==", + "dependencies": { + "css-line-break": "1.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/htmlparser2": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", @@ -12695,6 +12727,11 @@ "node": ">=8" } }, + "node_modules/jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" + }, "node_modules/js-base64": { "version": "2.6.4", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", @@ -15909,6 +15946,14 @@ "node": ">=0.10.0" } }, + "node_modules/pivottable": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/pivottable/-/pivottable-2.23.0.tgz", + "integrity": "sha512-6WRaiiI0mU5JxzNMWbtf3vfrBvBhBPIUbwu2Q7Nv7fVCxIvlmFqXSldMwmHAsiEFwdZdUrpQHqIu+N3jZUezyg==", + "dependencies": { + "jquery": ">=1.9.0" + } + }, "node_modules/pkg-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", @@ -21924,6 +21969,15 @@ "integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=", "dev": true }, + "node_modules/vue-multiselect": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-2.1.6.tgz", + "integrity": "sha512-s7jmZPlm9FeueJg1RwJtnE9KNPtME/7C8uRWSfp9/yEN4M8XcS/d+bddoyVwVnvFyRh9msFo0HWeW0vTL8Qv+w==", + "engines": { + "node": ">= 4.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/vue-router": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.3.4.tgz", @@ -21961,6 +22015,11 @@ "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", "dev": true }, + "node_modules/vue2-teleport": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vue2-teleport/-/vue2-teleport-1.0.1.tgz", + "integrity": "sha512-hbY/Q0x8qXGFxo6h4KU4YYesUcN+uUjliqqC0PoNSgpcbS2QRb3qXi+7XMTgLYs0a8i7o1H6Mu43UV4Vbgkhgw==" + }, "node_modules/vuejs-paginate": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/vuejs-paginate/-/vuejs-paginate-2.1.0.tgz", @@ -22071,6 +22130,7 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, + "hasInstallScript": true, "optional": true, "os": [ "darwin" @@ -22410,6 +22470,7 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, + "hasInstallScript": true, "optional": true, "os": [ "darwin" @@ -28728,6 +28789,21 @@ "resolved": "https://registry.npmjs.org/css-global-keywords/-/css-global-keywords-1.0.1.tgz", "integrity": "sha1-cqmupyeW0Bmx0qMlLeTlqqN+Smk=" }, + "css-line-break": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz", + "integrity": "sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==", + "requires": { + "base64-arraybuffer": "^0.2.0" + }, + "dependencies": { + "base64-arraybuffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz", + "integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==" + } + } + }, "css-loader": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz", @@ -33017,6 +33093,14 @@ } } }, + "html2canvas": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.1.4.tgz", + "integrity": "sha512-uHgQDwrXsRmFdnlOVFvHin9R7mdjjZvoBoXxicPR+NnucngkaLa5zIDW9fzMkiip0jSffyTyWedE8iVogYOeWg==", + "requires": { + "css-line-break": "1.1.1" + } + }, "htmlparser2": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", @@ -34218,6 +34302,11 @@ } } }, + "jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" + }, "js-base64": { "version": "2.6.4", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", @@ -36942,6 +37031,14 @@ "pinkie": "^2.0.0" } }, + "pivottable": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/pivottable/-/pivottable-2.23.0.tgz", + "integrity": "sha512-6WRaiiI0mU5JxzNMWbtf3vfrBvBhBPIUbwu2Q7Nv7fVCxIvlmFqXSldMwmHAsiEFwdZdUrpQHqIu+N3jZUezyg==", + "requires": { + "jquery": ">=1.9.0" + } + }, "pkg-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", @@ -42205,6 +42302,11 @@ } } }, + "vue-multiselect": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-2.1.6.tgz", + "integrity": "sha512-s7jmZPlm9FeueJg1RwJtnE9KNPtME/7C8uRWSfp9/yEN4M8XcS/d+bddoyVwVnvFyRh9msFo0HWeW0vTL8Qv+w==" + }, "vue-router": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.3.4.tgz", @@ -42244,6 +42346,11 @@ "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", "dev": true }, + "vue2-teleport": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vue2-teleport/-/vue2-teleport-1.0.1.tgz", + "integrity": "sha512-hbY/Q0x8qXGFxo6h4KU4YYesUcN+uUjliqqC0PoNSgpcbS2QRb3qXi+7XMTgLYs0a8i7o1H6Mu43UV4Vbgkhgw==" + }, "vuejs-paginate": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/vuejs-paginate/-/vuejs-paginate-2.1.0.tgz", diff --git a/package.json b/package.json index e2ca23a..aaf56e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sqliteviz", - "version": "0.14.2", + "version": "0.15.0", "license": "Apache-2.0", "private": true, "scripts": { @@ -12,8 +12,11 @@ "dependencies": { "codemirror": "^5.57.0", "core-js": "^3.6.5", + "html2canvas": "^1.1.4", + "jquery": "^3.6.0", "nanoid": "^3.1.12", "papaparse": "^5.3.1", + "pivottable": "^2.23.0", "plotly.js": "^1.58.4", "promise-worker": "^2.0.1", "react": "^16.13.1", @@ -24,7 +27,9 @@ "vue": "^2.6.11", "vue-codemirror": "^4.0.6", "vue-js-modal": "^2.0.0-rc.6", + "vue-multiselect": "^2.1.6", "vue-router": "^3.2.0", + "vue2-teleport": "^1.0.1", "vuejs-paginate": "^2.1.0", "vuera": "^0.2.7", "vuex": "^3.4.0" diff --git a/public/queries.json b/public/inquiries.json similarity index 100% rename from public/queries.json rename to public/inquiries.json diff --git a/src/assets/images/arrow-hover.svg b/src/assets/images/arrow-hover.svg new file mode 100644 index 0000000..c879e47 --- /dev/null +++ b/src/assets/images/arrow-hover.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/arrow.svg b/src/assets/images/arrow.svg new file mode 100644 index 0000000..95d2b11 --- /dev/null +++ b/src/assets/images/arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/delete-tag-hover.svg b/src/assets/images/delete-tag-hover.svg new file mode 100644 index 0000000..988ad18 --- /dev/null +++ b/src/assets/images/delete-tag-hover.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/delete-tag.svg b/src/assets/images/delete-tag.svg new file mode 100644 index 0000000..03299fc --- /dev/null +++ b/src/assets/images/delete-tag.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/sort.svg b/src/assets/images/sort.svg new file mode 100644 index 0000000..deee3fc --- /dev/null +++ b/src/assets/images/sort.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/styles/multiselect.css b/src/assets/styles/multiselect.css new file mode 100644 index 0000000..1ce9ef2 --- /dev/null +++ b/src/assets/styles/multiselect.css @@ -0,0 +1,136 @@ +.sqliteviz-select, +.sqliteviz-select .multiselect__tags { + min-height: 36px; + color: var(--color-text-base); +} + +.sqliteviz-select .multiselect__select { + height: 34px; + min-height: 34px; + padding: 6px; + width: 32px; + height: 32px; + margin-top: 1px; +} + +.sqliteviz-select .multiselect__tags { + border-radius: var(--border-radius-medium-2); + border: 1px solid var(--color-border); + padding: 4px 32px 0 6px; +} + +.sqliteviz-select, +.sqliteviz-select .multiselect__input, +.sqliteviz-select .multiselect__single, +.sqliteviz-select .multiselect__placeholder { + font-size: 12px; +} + +.sqliteviz-select .multiselect__single, +.sqliteviz-select .multiselect__placeholder, +.sqliteviz-select .multiselect__input { + padding: 0; + margin-bottom: 0; + line-height: 28px; +} + +.sqliteviz-select .multiselect__input { + width: 0 !important; + color: var(--color-text-base); +} + +.sqliteviz-select.multiselect--active .multiselect__input { + width: auto !important; +} + +.sqliteviz-select .multiselect__placeholder, +.sqliteviz-select .multiselect__input::placeholder { + color: var(--color-text-light-2); +} + +.sqliteviz-select .multiselect__option.multiselect__option--highlight { + background-color: var(--color-bg-light); + color: var(--color-text-active); +} + +.sqliteviz-select .multiselect__tag { + background-color: var(--color-bg-light-4); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-small); + color: var(--color-text-active); + font-size: 11.05px; + margin: 2px; +} +.sqliteviz-select .multiselect__tag-icon:after { + content: url('~@/assets/images/delete-tag.svg'); + height: 14px; + width: 14px; +} + +.sqliteviz-select .multiselect__tag-icon:focus:after, +.sqliteviz-select .multiselect__tag-icon:hover:after { + content: url('~@/assets/images/delete-tag-hover.svg'); +} + +.sqliteviz-select .multiselect__tag-icon:focus, +.sqliteviz-select .multiselect__tag-icon:hover { + background-color: var(--color-bg-danger); + border-radius: var(--border-radius-small); +} + +.sqliteviz-select .multiselect__option { + min-height: 29px; + padding: 8px 12px; + line-height: 13px; +} + +.sqliteviz-select .multiselect__option:after { + line-height: 29px; +} + +.sqliteviz-select .multiselect__content-wrapper { + border-radius: var(--border-radius-medium-2); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-1); + top: calc(100% - 1px); + max-height: 292px !important; +} + +.sqliteviz-select.multiselect--above .multiselect__content-wrapper { + top: unset; + bottom: calc(100% - 1px); +} + +.sqliteviz-select .multiselect__select:before { + content: url('~@/assets/images/arrow.svg'); + border: none; + top: 0; +} + +.sqliteviz-select.multiselect--active .multiselect__select { + transform: none; +} + +.sqliteviz-select:hover .multiselect__tags { + border-color: var(--color-border-dark); +} + +.sqliteviz-select .multiselect__select:hover:before { + content: url('~@/assets/images/arrow-hover.svg'); +} + +.sqliteviz-select.multiselect--active .multiselect__tags { + border-radius: var(--border-radius-medium-2); +} + +.sqliteviz-select .multiselect__option .no-results { + color: var(--color-text-light-2); +} + +.sqliteviz-select.multiselect--disabled { + opacity: unset; +} + +.sqliteviz-select.multiselect--disabled .multiselect__select { + background: unset; +} \ No newline at end of file diff --git a/src/assets/styles/tables.css b/src/assets/styles/tables.css index 5511f00..59d1d69 100644 --- a/src/assets/styles/tables.css +++ b/src/assets/styles/tables.css @@ -6,6 +6,12 @@ border: 1px solid var(--color-border-light); box-sizing: border-box; } + +.straight .rounded-bg { + border-radius: 0; + border-width: 0 0 1px 0; +} + .header-container { overflow: hidden; position: absolute; @@ -18,6 +24,19 @@ border-radius: 5px 5px 0 0; } +.straight .header-container { + border-radius: 0; +} + +.straight { + height: 100%; +} + +.straight .rounded-bg { + /* 27 - height of table footer */ + height: calc(100% - 27px); +} + @supports (-moz-appearance:none) { .header-container { top: 0; @@ -32,22 +51,25 @@ } .table-container { width: 100%; + max-height: 100%; overflow: auto; } -table { +table.sqliteviz-table { min-width: 100%; margin-top: -35px; border-collapse: collapse; } -thead th, .fixed-header { +.sqliteviz-table thead th, .fixed-header { font-size: 14px; font-weight: 600; box-sizing: border-box; background-color: var(--color-bg-dark); color: var(--color-text-light); border-right: 1px solid var(--color-border-light); + overflow: hidden; + text-overflow: ellipsis; } -tbody td { +.sqliteviz-table tbody td { font-size: 13px; background-color:white; color: var(--color-text-base); @@ -55,18 +77,20 @@ tbody td { border-bottom: 1px solid var(--color-border-light); border-right: 1px solid var(--color-border-light); } -td, th, .fixed-header { +.sqliteviz-table td, +.sqliteviz-table th, +.fixed-header { padding: 8px 24px; white-space: nowrap; } -tbody tr td:last-child, -thead tr th:last-child, +.sqliteviz-table tbody tr td:last-child, +.sqliteviz-table thead tr th:last-child, .header-container div .fixed-header:last-child { border-right: none; } -td > div.cell-data { +.sqliteviz-table td > div.cell-data { width: -webkit-max-content; width: -moz-max-content; width: max-content; diff --git a/src/assets/styles/variables.css b/src/assets/styles/variables.css index 7c8d37a..91abd09 100644 --- a/src/assets/styles/variables.css +++ b/src/assets/styles/variables.css @@ -11,6 +11,8 @@ --color-blue-dark: #0D76BF; --color-blue-dark-2: #2A3F5F; --color-red: #EF553B; + --color-red-2: #DE350B; + --color-red-light: #FFBDAD; --color-yellow: #FBEFCB; @@ -18,13 +20,16 @@ --color-bg-light: var(--color-gray-light); --color-bg-light-2: var(--color-gray-light-2); --color-bg-light-3: var(--color-gray-light-5); + --color-bg-light-4: var(--color-gray-light-4); --color-bg-dark: var(--color-gray-dark); --color-bg-warning: var(--color-yellow); - --color-danger: var(--color-red); + --color-bg-danger: var(--color-red-light); + --color-danger: var(--color-red-2); --color-accent: var(--color-blue-medium); --color-accent-shade: var(--color-blue-dark); --color-border-light: var(--color-gray-light-2); --color-border: var(--color-gray-light-3); + --color-border-dark: var(--color-gray-medium); --color-text-light: var(--color-white); --color-text-light-2: var(--color-gray-medium); --color-text-base: var(--color-gray-dark); diff --git a/src/components/CsvImport/csv.js b/src/components/CsvImport/csv.js index 431cd49..338e114 100644 --- a/src/components/CsvImport/csv.js +++ b/src/components/CsvImport/csv.js @@ -10,29 +10,26 @@ export default { getResult (source) { const result = {} if (source.meta.fields) { - result.columns = source.meta.fields.map(col => col.trim()) - result.values = source.data.map(row => { - const resultRow = [] - source.meta.fields.forEach(col => { + source.meta.fields.forEach(col => { + result[col.trim()] = source.data.map(row => { let value = row[col] if (value instanceof Date) { value = value.toISOString() } - resultRow.push(value) + return value }) - - return resultRow }) } else { - result.values = source.data.map(row => row.map(value => - value instanceof Date ? value.toISOString() : value - )) - result.columns = [] - for (let i = 1; i <= source.data[0].length; i++) { - result.columns.push(`col${i}`) + for (let i = 0; i <= source.data[0].length - 1; i++) { + result[`col${i + 1}`] = source.data.map(row => { + let value = row[i] + if (value instanceof Date) { + value = value.toISOString() + } + return value + }) } } - return result }, @@ -55,7 +52,8 @@ export default { const res = { data: this.getResult(results), delimiter: results.meta.delimiter, - hasErrors: false + hasErrors: false, + rowCount: results.data.length } res.messages = results.errors.map(msg => { msg.type = msg.code === 'UndetectableDelimiter' ? 'info' : 'error' diff --git a/src/components/CsvImport/index.vue b/src/components/CsvImport/index.vue index 26ff7db..0d1ed9c 100644 --- a/src/components/CsvImport/index.vue +++ b/src/components/CsvImport/index.vue @@ -55,9 +55,10 @@ :disabled="disableDialog" /> @@ -255,7 +256,7 @@ export default { let end = new Date() if (!parseResult.hasErrors) { - const rowCount = parseResult.data.values.length + const rowCount = parseResult.rowCount let period = time.getPeriod(start, end) parseCsvMsg.type = 'success' diff --git a/src/components/DbUploader.vue b/src/components/DbUploader.vue index c6bb051..5dcb89d 100644 --- a/src/components/DbUploader.vue +++ b/src/components/DbUploader.vue @@ -1,6 +1,6 @@ + + + + diff --git a/src/components/SqlTable/Pager.vue b/src/components/SqlTable/Pager.vue index f61412d..1660bb8 100644 --- a/src/components/SqlTable/Pager.vue +++ b/src/components/SqlTable/Pager.vue @@ -49,6 +49,7 @@ export default { .paginator-continer { display: flex; align-items: center; + line-height: 10px; } >>> .paginator-page-link { padding: 2px 3px; diff --git a/src/components/SqlTable/index.vue b/src/components/SqlTable/index.vue index ebe7edc..ef2e063 100644 --- a/src/components/SqlTable/index.vue +++ b/src/components/SqlTable/index.vue @@ -17,20 +17,21 @@ class="table-container" ref="table-container" @scroll="onScrollTable" - :style="{maxHeight: `${height}px`}" > - +
- - - + @@ -39,7 +40,7 @@ @@ -26,7 +26,13 @@ import tooltipMixin from '@/tooltipMixin' export default { name: 'changeDbIcon', - mixins: [tooltipMixin] + mixins: [tooltipMixin], + methods: { + onClick () { + this.hideTooltip() + this.$emit('click') + } + } } diff --git a/src/components/svg/chart.vue b/src/components/svg/chart.vue new file mode 100644 index 0000000..fb1012b --- /dev/null +++ b/src/components/svg/chart.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/components/svg/dataView.vue b/src/components/svg/dataView.vue new file mode 100644 index 0000000..7ecd371 --- /dev/null +++ b/src/components/svg/dataView.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/components/svg/export.vue b/src/components/svg/export.vue index 34b4d17..d6cd99f 100644 --- a/src/components/svg/export.vue +++ b/src/components/svg/export.vue @@ -6,17 +6,16 @@ height="18" viewBox="0 0 18 18" fill="none" - xmlns="http://www.w3.org/2000/svg" - @click.stop="$emit('click')" - @mouseover="showTooltip" - @mouseout="hideTooltip" + @click.stop="onClick" + @mouseenter="showTooltip($event, tooltipPosition)" + @mouseleave="hideTooltip" > - + {{ tooltip }} @@ -28,7 +27,13 @@ import tooltipMixin from '@/tooltipMixin' export default { name: 'ExportIcon', mixins: [tooltipMixin], - props: ['tooltip'] + props: ['tooltip', 'tooltipPosition'], + methods: { + onClick () { + this.hideTooltip() + this.$emit('click') + } + } } diff --git a/src/components/svg/hint.vue b/src/components/svg/hint.vue index 57a1648..c6f6b77 100644 --- a/src/components/svg/hint.vue +++ b/src/components/svg/hint.vue @@ -6,14 +6,14 @@ height="20" viewBox="0 0 20 20" fill="none" - xmlns="http://www.w3.org/2000/svg" - @mouseover="showTooltip" - @mouseout="hideTooltip" + @click.stop="onClick" + @mouseenter="showTooltip" + @mouseleave="hideTooltip" > - + {{ hint }} @@ -25,7 +25,13 @@ import tooltipMixin from '@/tooltipMixin' export default { name: 'HintIcon', props: ['hint', 'maxWidth'], - mixins: [tooltipMixin] + mixins: [tooltipMixin], + methods: { + onClick () { + this.hideTooltip() + this.$emit('click') + } + } } diff --git a/src/components/svg/pivot.vue b/src/components/svg/pivot.vue new file mode 100644 index 0000000..364bda8 --- /dev/null +++ b/src/components/svg/pivot.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/components/svg/png.svg b/src/components/svg/png.svg new file mode 100644 index 0000000..da6a6cb --- /dev/null +++ b/src/components/svg/png.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/svg/png.vue b/src/components/svg/png.vue new file mode 100644 index 0000000..0cf17a2 --- /dev/null +++ b/src/components/svg/png.vue @@ -0,0 +1,18 @@ + + + diff --git a/src/components/svg/run.vue b/src/components/svg/run.vue new file mode 100644 index 0000000..23b4db7 --- /dev/null +++ b/src/components/svg/run.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/components/svg/sort.vue b/src/components/svg/sort.vue new file mode 100644 index 0000000..26384ba --- /dev/null +++ b/src/components/svg/sort.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/src/components/svg/sqlEditor.vue b/src/components/svg/sqlEditor.vue new file mode 100644 index 0000000..c209381 --- /dev/null +++ b/src/components/svg/sqlEditor.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/components/svg/table.vue b/src/components/svg/table.vue new file mode 100644 index 0000000..0df1091 --- /dev/null +++ b/src/components/svg/table.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/database/_sql.js b/src/lib/database/_sql.js index 0dfa5ae..ebe362f 100644 --- a/src/lib/database/_sql.js +++ b/src/lib/database/_sql.js @@ -4,6 +4,17 @@ import dbUtils from './_statements' let SQL = null const sqlModuleReady = initSqlJs().then(sqlModule => { SQL = sqlModule }) +function _getDataSourcesFromSqlResult (sqlResult) { + if (!sqlResult) { + return {} + } + const dataSorces = {} + sqlResult.columns.forEach((column, index) => { + dataSorces[column] = sqlResult.values.map(row => row[index]) + }) + return dataSorces +} + export default class Sql { constructor () { this.db = null @@ -36,16 +47,19 @@ export default class Sql { if (!sql) { throw new Error('exec: Missing query string') } - return this.db.exec(sql, params) + const sqlResults = this.db.exec(sql, params) + return sqlResults.map(result => _getDataSourcesFromSqlResult(result)) } - import (tabName, columns, values, progressCounterId, progressCallback, chunkSize = 1500) { + import (tabName, data, progressCounterId, progressCallback, chunkSize = 1500) { if (this.db === null) { this.createDb() } - this.db.exec(dbUtils.getCreateStatement(tabName, columns, values)) - const chunks = dbUtils.generateChunks(values, chunkSize) - const chunksAmount = Math.ceil(values.length / chunkSize) + const columns = Object.keys(data) + const rowCount = data[columns[0]].length + this.db.exec(dbUtils.getCreateStatement(tabName, data)) + const chunks = dbUtils.generateChunks(data, chunkSize) + const chunksAmount = Math.ceil(rowCount / chunkSize) let count = 0 const insertStr = dbUtils.getInsertStmt(tabName, columns) const insertStmt = this.db.prepare(insertStr) diff --git a/src/lib/database/_statements.js b/src/lib/database/_statements.js index 158a1fe..eb0652c 100644 --- a/src/lib/database/_statements.js +++ b/src/lib/database/_statements.js @@ -1,13 +1,17 @@ import sqliteParser from 'sqlite-parser' export default { - * generateChunks (arr, size) { - const count = Math.ceil(arr.length / size) + * generateChunks (data, size) { + const matrix = Object.keys(data).map(col => data[col]) + const [row] = matrix + const transposedMatrix = row.map((value, column) => matrix.map(row => row[column])) + + const count = Math.ceil(transposedMatrix.length / size) for (let i = 0; i <= count - 1; i++) { const start = size * i const end = start + size - yield arr.slice(start, end) + yield transposedMatrix.slice(start, end) } }, @@ -17,11 +21,11 @@ export default { return `INSERT INTO "${tabName}" (${colList}) VALUES (${params});` }, - getCreateStatement (tabName, columns, values) { + getCreateStatement (tabName, data) { let result = `CREATE table "${tabName}"(` - columns.forEach((col, index) => { - // Get the first row of values to determine types - const value = values[0][index] + for (const col in data) { + // Get the first row of values to determine types + const value = data[col][0] let type = '' switch (typeof value) { case 'number': { @@ -39,7 +43,8 @@ export default { default: type = 'TEXT' } result += `"${col}" ${type}, ` - }) + } + result = result.replace(/,\s$/, ');') return result }, diff --git a/src/lib/database/_worker.js b/src/lib/database/_worker.js index 7aa3877..fb6ef94 100644 --- a/src/lib/database/_worker.js +++ b/src/lib/database/_worker.js @@ -15,8 +15,7 @@ function processMsg (sql) { case 'import': return sql.import( data.tabName, - data.columns, - data.values, + data.data, data.progressCounterId, postMessage ) diff --git a/src/lib/database/index.js b/src/lib/database/index.js index 00006d8..a46a82c 100644 --- a/src/lib/database/index.js +++ b/src/lib/database/index.js @@ -55,8 +55,7 @@ class Database { async addTableFromCsv (tabName, data, progressCounterId) { const result = await this.pw.postMessage({ action: 'import', - columns: data.columns, - values: data.values, + data, progressCounterId, tabName }) @@ -89,11 +88,11 @@ class Database { const result = await this.execute(getSchemaSql) // Parse DDL statements to get column names and types const parsedSchema = [] - if (result && result.values) { - result.values.forEach(item => { + if (result && result.name) { + result.name.forEach((table, index) => { parsedSchema.push({ - name: item[0], - columns: stms.getColumns(item[1]) + name: table, + columns: stms.getColumns(result.sql[index]) }) }) } diff --git a/src/lib/storedInquiries/_migrations.js b/src/lib/storedInquiries/_migrations.js new file mode 100644 index 0000000..7288dda --- /dev/null +++ b/src/lib/storedInquiries/_migrations.js @@ -0,0 +1,12 @@ +export default { + _migrate (installedVersion, inquiries) { + if (installedVersion === 1) { + inquiries.forEach(inquire => { + inquire.viewType = 'chart' + inquire.viewOptions = inquire.chart + delete inquire.chart + }) + return inquiries + } + } +} diff --git a/src/lib/storedInquiries/index.js b/src/lib/storedInquiries/index.js new file mode 100644 index 0000000..05994c9 --- /dev/null +++ b/src/lib/storedInquiries/index.js @@ -0,0 +1,120 @@ +import { nanoid } from 'nanoid' +import fu from '@/lib/utils/fileIo' +import migration from './_migrations' + +const migrate = migration._migrate + +export default { + version: 2, + getStoredInquiries () { + let myInquiries = JSON.parse(localStorage.getItem('myInquiries')) + if (!myInquiries) { + const oldInquiries = localStorage.getItem('myQueries') + if (oldInquiries) { + myInquiries = migrate(1, JSON.parse(oldInquiries)) + this.updateStorage(myInquiries) + return myInquiries + } + return [] + } + + return (myInquiries && myInquiries.inquiries) || [] + }, + + duplicateInquiry (baseInquiry) { + const newInquiry = JSON.parse(JSON.stringify(baseInquiry)) + newInquiry.name = newInquiry.name + ' Copy' + newInquiry.id = nanoid() + newInquiry.createdAt = new Date() + delete newInquiry.isPredefined + + return newInquiry + }, + + isTabNeedName (inquiryTab) { + const isFromScratch = !inquiryTab.initName + return inquiryTab.isPredefined || isFromScratch + }, + + save (inquiryTab, newName) { + const value = { + id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id, + query: inquiryTab.query, + viewType: inquiryTab.$refs.dataView.mode, + viewOptions: inquiryTab.$refs.dataView.getOptionsForSave(), + name: newName || inquiryTab.initName + } + + // Get inquiries from local storage + const myInquiries = this.getStoredInquiries() + + // Set createdAt + if (newName) { + value.createdAt = new Date() + } else { + var inquiryIndex = myInquiries.findIndex(oldInquiry => oldInquiry.id === inquiryTab.id) + value.createdAt = myInquiries[inquiryIndex].createdAt + } + + // Insert in inquiries list + if (newName) { + myInquiries.push(value) + } else { + myInquiries[inquiryIndex] = value + } + + // Save to local storage + this.updateStorage(myInquiries) + return value + }, + + updateStorage (inquiries) { + localStorage.setItem('myInquiries', JSON.stringify({ version: this.version, inquiries })) + }, + + serialiseInquiries (inquiryList) { + const preparedData = JSON.parse(JSON.stringify(inquiryList)) + preparedData.forEach(inquiry => delete inquiry.isPredefined) + return JSON.stringify({ version: this.version, inquiries: preparedData }, null, 4) + }, + + deserialiseInquiries (str) { + const inquiries = JSON.parse(str) + let inquiryList = [] + if (!inquiries.version) { + // Turn data into array if they are not + inquiryList = !Array.isArray(inquiries) ? [inquiries] : inquiries + inquiryList = migrate(1, inquiryList) + } else { + inquiryList = inquiries.inquiries || [] + } + + // Generate new ids if they are the same as existing inquiries + inquiryList.forEach(inquiry => { + const allInquiriesIds = this.getStoredInquiries().map(inquiry => inquiry.id) + if (allInquiriesIds.includes(inquiry.id)) { + inquiry.id = nanoid() + } + }) + + return inquiryList + }, + + importInquiries () { + return fu.importFile() + .then(str => { + return this.deserialiseInquiries(str) + }) + }, + + async readPredefinedInquiries () { + const res = await fu.readFile('./inquiries.json') + const data = await res.json() + + if (!data.version) { + return data.length > 0 ? migrate(1, data) : [] + } else { + return data.inquiries + } + } +} diff --git a/src/lib/storedQueries.js b/src/lib/storedQueries.js deleted file mode 100644 index f3f8bfb..0000000 --- a/src/lib/storedQueries.js +++ /dev/null @@ -1,96 +0,0 @@ -import { nanoid } from 'nanoid' -import fu from '@/lib/utils/fileIo' - -export default { - getStoredQueries () { - return JSON.parse(localStorage.getItem('myQueries')) || [] - }, - - duplicateQuery (baseQuery) { - const newQuery = JSON.parse(JSON.stringify(baseQuery)) - newQuery.name = newQuery.name + ' Copy' - newQuery.id = nanoid() - newQuery.createdAt = new Date() - delete newQuery.isPredefined - - return newQuery - }, - - isTabNeedName (queryTab) { - const isFromScratch = !queryTab.initName - return queryTab.isPredefined || isFromScratch - }, - - save (queryTab, newName) { - const value = { - id: queryTab.isPredefined ? nanoid() : queryTab.id, - query: queryTab.query, - chart: queryTab.$refs.chart.getChartStateForSave(), - name: newName || queryTab.initName - } - - // Get queries from local storage - const myQueries = this.getStoredQueries() - - // Set createdAt - if (newName) { - value.createdAt = new Date() - } else { - var queryIndex = myQueries.findIndex(oldQuery => oldQuery.id === queryTab.id) - value.createdAt = myQueries[queryIndex].createdAt - } - - // Insert in queries list - if (newName) { - myQueries.push(value) - } else { - myQueries[queryIndex] = value - } - - // Save to local storage - this.updateStorage(myQueries) - return value - }, - - updateStorage (value) { - localStorage.setItem('myQueries', JSON.stringify(value)) - }, - - serialiseQueries (queryList) { - const preparedData = JSON.parse(JSON.stringify(queryList)) - preparedData.forEach(query => delete query.isPredefined) - return JSON.stringify(preparedData, null, 4) - }, - - deserialiseQueries (str) { - let queryList = JSON.parse(str) - // Turn data into array if they are not - if (!Array.isArray(queryList)) { - queryList = [queryList] - } - - // Generate new ids if they are the same as existing queries - queryList.forEach(query => { - const allQueriesIds = this.getStoredQueries().map(query => query.id) - if (allQueriesIds.includes(query.id)) { - query.id = nanoid() - } - }) - - return queryList - }, - - importQueries () { - return fu.importFile() - .then(data => { - return this.deserialiseQueries(data) - }) - }, - - readPredefinedQueries () { - return fu.readFile('./queries.json') - .then(resp => { - return resp.json() - }) - } -} diff --git a/src/lib/utils/fileIo.js b/src/lib/utils/fileIo.js index 5c227be..d4ba38d 100644 --- a/src/lib/utils/fileIo.js +++ b/src/lib/utils/fileIo.js @@ -10,11 +10,9 @@ export default { return file.name.replace(/\.[^.]+$/, '') }, - exportToFile (str, fileName, type = 'octet/stream') { + downloadFromUrl (url, fileName) { // Create downloader const downloader = document.createElement('a') - const blob = new Blob([str], { type }) - const url = URL.createObjectURL(blob) downloader.href = url downloader.download = fileName @@ -25,6 +23,12 @@ export default { URL.revokeObjectURL(url) }, + async exportToFile (str, fileName, type = 'octet/stream') { + const blob = new Blob([str], { type }) + const url = URL.createObjectURL(blob) + this.downloadFromUrl(url, fileName) + }, + /** * Note: if user press Cancel in file choosing dialog * it will be an unsettled promise. But it's grabbed by diff --git a/src/main.js b/src/main.js index 9601429..ed700da 100644 --- a/src/main.js +++ b/src/main.js @@ -11,6 +11,8 @@ import '@/assets/styles/tables.css' import '@/assets/styles/dialogs.css' import '@/assets/styles/tooltips.css' import '@/assets/styles/messages.css' +import 'vue-multiselect/dist/vue-multiselect.min.css' +import '@/assets/styles/multiselect.css' if (!['localhost', '127.0.0.1'].includes(location.hostname)) { import('./registerServiceWorker') // eslint-disable-line no-unused-expressions diff --git a/src/router.js b/src/router.js index 5fcb24f..b2f4c25 100644 --- a/src/router.js +++ b/src/router.js @@ -1,7 +1,7 @@ import Vue from 'vue' import VueRouter from 'vue-router' -import Editor from '@/views/Main/Editor' -import MyQueries from '@/views/Main/MyQueries' +import Workspace from '@/views/Main/Workspace' +import Inquiries from '@/views/Main/Inquiries' import Welcome from '@/views/Welcome' import Main from '@/views/Main' import store from '@/store' @@ -21,14 +21,14 @@ const routes = [ component: Main, children: [ { - path: '/editor', - name: 'Editor', - component: Editor + path: '/workspace', + name: 'Workspace', + component: Workspace }, { - path: '/my-queries', - name: 'MyQueries', - component: MyQueries + path: '/inquiries', + name: 'Inquiries', + component: Inquiries } ] } diff --git a/src/store/actions.js b/src/store/actions.js index abba11e..7096951 100644 --- a/src/store/actions.js +++ b/src/store/actions.js @@ -5,16 +5,18 @@ export default { const tab = data ? JSON.parse(JSON.stringify(data)) : {} // If no data then create a new blank one... // No data.id means to create new tab, but not blank, - // e.g. with 'select * from csv_import' query after csv import + // e.g. with 'select * from csv_import' inquiry after csv import if (!data || !data.id) { tab.id = nanoid() tab.name = null tab.tempName = state.untitledLastIndex ? `Untitled ${state.untitledLastIndex}` : 'Untitled' - tab.isUnsaved = true + tab.viewType = 'chart' + tab.viewOptions = undefined + tab.isSaved = false } else { - tab.isUnsaved = false + tab.isSaved = true } // add new tab only if was not already opened diff --git a/src/store/mutations.js b/src/store/mutations.js index adb8e70..def7451 100644 --- a/src/store/mutations.js +++ b/src/store/mutations.js @@ -8,7 +8,7 @@ export default { state.db = db }, - updateTab (state, { index, name, id, query, chart, isUnsaved }) { + updateTab (state, { index, name, id, query, viewType, viewOptions, isSaved }) { const tab = state.tabs[index] const oldId = tab.id @@ -19,15 +19,17 @@ export default { if (id) { tab.id = id } if (name) { tab.name = name } if (query) { tab.query = query } - if (chart) { tab.chart = chart } - if (isUnsaved !== undefined) { tab.isUnsaved = isUnsaved } - if (!isUnsaved) { - // Saved query is not predefined + if (viewType) { tab.viewType = viewType } + if (viewOptions) { tab.viewOptions = viewOptions } + if (isSaved !== undefined) { tab.isSaved = isSaved } + if (isSaved) { + // Saved inquiry is not predefined delete tab.isPredefined } Vue.set(state.tabs, index, tab) }, + deleteTab (state, index) { // If closing tab is the current opened if (state.tabs[index].id === state.currentTabId) { @@ -49,11 +51,7 @@ export default { setCurrentTab (state, tab) { state.currentTab = tab }, - updatePredefinedQueries (state, queries) { - if (Array.isArray(queries)) { - state.predefinedQueries = queries - } else { - state.predefinedQueries = [queries] - } + updatePredefinedInquiries (state, inquiries) { + state.predefinedInquiries = Array.isArray(inquiries) ? inquiries : [inquiries] } } diff --git a/src/store/state.js b/src/store/state.js index 5769379..bf0db86 100644 --- a/src/store/state.js +++ b/src/store/state.js @@ -3,6 +3,6 @@ export default { currentTab: null, currentTabId: null, untitledLastIndex: 0, - predefinedQueries: [], + predefinedInquiries: [], db: null } diff --git a/src/tooltipMixin.js b/src/tooltipMixin.js index c211413..c641e3b 100644 --- a/src/tooltipMixin.js +++ b/src/tooltipMixin.js @@ -6,10 +6,28 @@ export default { } } }, + computed: { + tooltipElement () { + return this.$refs.tooltip + } + }, methods: { - showTooltip (e) { - this.tooltipStyle.top = e.clientY - 12 + 'px' - this.tooltipStyle.left = e.clientX + 12 + 'px' + showTooltip (e, tooltipPosition) { + const position = tooltipPosition ? tooltipPosition.split('-') : ['top', 'right'] + const offset = 12 + + if (position[0] === 'top') { + this.tooltipStyle.top = e.clientY - offset + 'px' + } else { + this.tooltipStyle.top = e.clientY + offset + 'px' + } + + if (position[1] === 'right') { + this.tooltipStyle.left = e.clientX + offset + 'px' + } else { + this.tooltipStyle.left = e.clientX - offset - this.tooltipElement.offsetWidth + 'px' + } + this.tooltipStyle.visibility = 'visible' }, hideTooltip () { diff --git a/src/views/Main/AppDiagnosticInfo.vue b/src/views/Main/AppDiagnosticInfo.vue index bf74b12..d37bb35 100644 --- a/src/views/Main/AppDiagnosticInfo.vue +++ b/src/views/Main/AppDiagnosticInfo.vue @@ -47,13 +47,13 @@ export default { let result = await state.db.execute('select sqlite_version()') this.info.push({ name: 'SQLite version', - info: result.values[0] + info: result['sqlite_version()'] }) result = await state.db.execute('PRAGMA compile_options') this.info.push({ name: 'SQLite compile options', - info: result.values.map(row => row[0]) + info: result.compile_options }) } } diff --git a/src/views/Main/Editor/Tabs/Tab/Chart/index.vue b/src/views/Main/Editor/Tabs/Tab/Chart/index.vue deleted file mode 100644 index 167ac12..0000000 --- a/src/views/Main/Editor/Tabs/Tab/Chart/index.vue +++ /dev/null @@ -1,98 +0,0 @@ - - - - - diff --git a/src/views/Main/Editor/Tabs/Tab/ViewSwitcher.vue b/src/views/Main/Editor/Tabs/Tab/ViewSwitcher.vue deleted file mode 100644 index e999160..0000000 --- a/src/views/Main/Editor/Tabs/Tab/ViewSwitcher.vue +++ /dev/null @@ -1,67 +0,0 @@ - - - - - diff --git a/src/views/Main/Editor/Tabs/Tab/index.vue b/src/views/Main/Editor/Tabs/Tab/index.vue deleted file mode 100644 index 5f71869..0000000 --- a/src/views/Main/Editor/Tabs/Tab/index.vue +++ /dev/null @@ -1,225 +0,0 @@ - - - - - diff --git a/src/views/Main/MyQueries/index.vue b/src/views/Main/Inquiries/index.vue similarity index 54% rename from src/views/Main/MyQueries/index.vue rename to src/views/Main/Inquiries/index.vue index 4fa39bb..beadda1 100644 --- a/src/views/Main/MyQueries/index.vue +++ b/src/views/Main/Inquiries/index.vue @@ -1,22 +1,22 @@ @@ -27,7 +26,13 @@ import tooltipMixin from '@/tooltipMixin' export default { name: 'CopyIcon', - mixins: [tooltipMixin] + mixins: [tooltipMixin], + methods: { + onClick () { + this.hideTooltip() + this.$emit('click') + } + } } diff --git a/src/views/Main/MyQueries/svg/delete.vue b/src/views/Main/Inquiries/svg/delete.vue similarity index 70% rename from src/views/Main/MyQueries/svg/delete.vue rename to src/views/Main/Inquiries/svg/delete.vue index 785a00b..2d6270a 100644 --- a/src/views/Main/MyQueries/svg/delete.vue +++ b/src/views/Main/Inquiries/svg/delete.vue @@ -6,18 +6,17 @@ height="18" viewBox="0 0 18 18" fill="none" - xmlns="http://www.w3.org/2000/svg" - @click.stop="$emit('click')" - @mouseover="showTooltip" - @mouseout="hideTooltip" + @click.stop="onClick" + @mouseenter="showTooltip($event, 'top-left')" + @mouseleave="hideTooltip" > - - Delete query + + Delete inquiry @@ -27,7 +26,13 @@ import tooltipMixin from '@/tooltipMixin' export default { name: 'DeleteIcon', - mixins: [tooltipMixin] + mixins: [tooltipMixin], + methods: { + onClick () { + this.hideTooltip() + this.$emit('click') + } + } } diff --git a/src/views/Main/MyQueries/svg/rename.vue b/src/views/Main/Inquiries/svg/rename.vue similarity index 72% rename from src/views/Main/MyQueries/svg/rename.vue rename to src/views/Main/Inquiries/svg/rename.vue index e3795ec..99b70f7 100644 --- a/src/views/Main/MyQueries/svg/rename.vue +++ b/src/views/Main/Inquiries/svg/rename.vue @@ -6,18 +6,17 @@ height="18" viewBox="0 0 18 18" fill="none" - xmlns="http://www.w3.org/2000/svg" - @click.stop="$emit('click')" - @mouseover="showTooltip" - @mouseout="hideTooltip" + @click.stop="onClick" + @mouseenter="showTooltip" + @mouseleave="hideTooltip" > - - Rename query + + Rename inquiry @@ -27,7 +26,13 @@ import tooltipMixin from '@/tooltipMixin' export default { name: 'RenameIcon', - mixins: [tooltipMixin] + mixins: [tooltipMixin], + methods: { + onClick () { + this.hideTooltip() + this.$emit('click') + } + } } diff --git a/src/views/Main/MainMenu.vue b/src/views/Main/MainMenu.vue index 1741a30..d8c491e 100644 --- a/src/views/Main/MainMenu.vue +++ b/src/views/Main/MainMenu.vue @@ -1,53 +1,44 @@
+
{{ th }}
-
{{ value }}
+
+
+ {{ dataSet[col][rowIndex - 1 + currentPageData.start] }} +