1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-06 18:18:53 +08:00

47 Commits

Author SHA1 Message Date
lana-k
a37ed93306 update version 2021-06-17 12:24:41 +02:00
lana-k
cf4b83f7d4 fix csv result when column names have spaces #59 2021-06-17 12:23:44 +02:00
lana-k
2abd42c9c3 run tests on pull request 2021-06-07 13:20:42 +02:00
lana-k
1251c542cb update react-chart-editor 2021-05-25 11:52:37 +02:00
lana-k
ac89259924 Always create empty db on start #46 2021-05-24 21:45:51 +02:00
lana-k
179ff8b1e1 fix table layout on My queries #32 2021-05-24 20:50:52 +02:00
lana-k
99a10225a3 CSV import as a table and db connection rework
- Add csv to existing db #32
- [RFE] Simplify working with temporary tables #53
2021-05-24 19:40:47 +02:00
lana-k
c96deb5766 Fix overflow for Firefox #46 2021-05-22 22:26:23 +02:00
lana-k
700970e1cc Add addTable icon #32 2021-05-22 22:25:19 +02:00
lana-k
e2be61e2cf Make error in text field start with uppercase #32 2021-05-22 22:24:20 +02:00
lana-k
9c2c8f3692 Make Logs smaller #32 2021-05-22 22:23:12 +02:00
lana-k
414a116f94 Lost focus in SQL query editor #54 2021-05-19 22:50:56 +02:00
lana-k
3e503f85a9 Stub app-diagnostic-info in tests 2021-05-19 21:52:42 +02:00
lana-k
88257bfcf6 fix CSV dialog height typo 2021-05-19 16:53:30 +02:00
lana-k
bdcc494138 Add scrolling to App info , CSV import dialogs #46 2021-05-19 16:43:42 +02:00
lana-k
d750541c80 App diagnostic dialog #46 2021-05-19 15:46:18 +02:00
lana-k
75f743ff9e remove title in release notes 2021-05-18 15:38:44 +02:00
lana-k
8a9f4b3c0a add Help link 2021-05-18 15:04:35 +02:00
lana-k
77468d34ae add release notes 2021-05-18 15:04:19 +02:00
lana-k
a0577ec0ce Fix gradient for Safari 2021-05-17 21:34:42 +02:00
lana-k
e7d1398546 Rewrite reg exp: make them work in Safari #52 2021-05-17 21:34:24 +02:00
lana-k
aa52048d51 Fix file type detection #48
file.type is empty on some Windows machines (Registry settings affects)
2021-05-17 21:32:09 +02:00
lana-k
33913f8f5c fix lint 2021-05-14 16:47:01 +02:00
lana-k
51eb7a543c Merge branch 'master' of github.com:lana-k/sqliteviz 2021-05-14 16:43:13 +02:00
lana-k
a3fb38b23c SQL query execution state in UI #3
- use LoadingIndicator
- use Logs
2021-05-14 16:42:58 +02:00
lana-k
3bb40b4eb7 Improve LoadingIndicator
- size parameters
- smooth animation (use ony transform)
2021-05-14 16:40:55 +02:00
lana-k
6864bf84f8 Update README.md 2021-05-06 21:36:20 +02:00
lana-k
9f1b3823f6 Update README.md 2021-05-06 20:46:36 +02:00
lana-k
7574f529c3 add hidden state for file in animation 2021-05-06 15:17:15 +02:00
lana-k
653f8eff7b minor changes in animation 2021-05-06 15:05:58 +02:00
lana-k
9b3dda6cff delete .nojekyll from master 2021-05-05 23:43:51 +02:00
lana-k
d94604ebfb Merge branch 'master' of github.com:lana-k/sqliteviz 2021-05-05 23:31:17 +02:00
lana-k
16868ef430 add clean-exclude 2021-05-05 23:30:33 +02:00
lana-k
b162c7043e Create .nojekyll 2021-05-05 23:20:46 +02:00
lana-k
8e856063b8 allow files with underscores on GitHub pages 2021-05-05 23:12:47 +02:00
lana-k
8684b4cef9 remove console.log 2021-05-05 21:46:29 +02:00
lana-k
bcaebd4840 Create an empty database #44 2021-05-05 21:44:44 +02:00
lana-k
4619461af8 change period format 2021-05-05 15:08:54 +02:00
lana-k
9fff1d699a update sql.js #43 2021-05-05 15:08:12 +02:00
lana-k
5ab19c3fae show hint in codemirror on Ctrl+Space 2021-05-04 16:33:37 +02:00
lana-k
cc483f4720 change code structure 2021-05-04 14:13:58 +02:00
lana-k
a07f2d3d99 update plotly 2021-05-02 20:59:03 +02:00
lana-k
b9844b8696 refine pwa app icons 2021-05-02 20:46:27 +02:00
lana-k
464bff3db8 delete screenshots 2021-05-02 20:45:24 +02:00
lana-k
00e434e142 fix loading db after csv:
new tab is not opened now
2021-05-02 14:09:02 +02:00
lana-k
5d6280abec add default table in hint options 2021-05-02 14:04:46 +02:00
lana-k
7a39e905b9 trim csv column names 2021-04-30 20:49:37 +02:00
105 changed files with 2758 additions and 2556 deletions

17
.github/workflows/config.grenrc.js vendored Normal file
View File

@@ -0,0 +1,17 @@
module.exports = {
dataSource: 'milestones',
ignoreIssuesWith: [
'wontfix',
'duplicate'
],
milestoneMatch: 'v{{tag_name}}',
template: {
issue: '- {{name}} [{{text}}]({{url}})',
changelogTitle: "",
release: "{{body}}",
},
groupBy: {
'Enhancements': ["enhancement", "internal"],
'Bug fixes': ["bug"]
}
}

View File

@@ -25,16 +25,26 @@ jobs:
cd dist
zip -9 -r dist.zip . -x "js/*.map"
- name: Create Release Notes
run: |
npm install github-release-notes@0.16.0 -g
gren changelog --generate --config="/.github/workflows/config.grenrc.js"
env:
GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create release
uses: ncipollo/release-action@v1
with:
artifacts: "dist/dist.zip"
token: ${{ secrets.GITHUB_TOKEN }}
bodyFile: "CHANGELOG.md"
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@3.6.2
uses: JamesIves/github-pages-deploy-action@4.1.1
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: build # The branch the action should deploy to.
FOLDER: dist/ # The folder the action should deploy.
CLEAN: false # Automatically remove deleted files from the deploy branch
token: ${{ secrets.GITHUB_TOKEN }}
branch: build # The branch the action should deploy to.
folder: dist/ # The folder the action should deploy.
clean: true # Automatically remove deleted files from the deploy branch
clean-exclude: .nojekyll

View File

@@ -4,6 +4,9 @@ on:
push:
branches:
- 'master'
pull_request:
branches:
- 'master'
jobs:
test:

View File

@@ -14,6 +14,8 @@ With sqliteviz you can:
- export a modified SQLite database
- use it offline from your OS application menu like any other desktop app
https://user-images.githubusercontent.com/24638357/117355518-fa332680-aeb2-11eb-8a69-fbcea4f7aeb0.mp4
## Quickstart
The latest release of sqliteviz is deployed on GitHub Pages at [lana-k.github.io/sqliteviz][6].
@@ -36,4 +38,4 @@ It is built on top of [react-chart-editor][3], [sql.js][4] and [Vue-Codemirror][
[8]: https://github.com/surmon-china/vue-codemirror#readme
[9]: https://www.papaparse.com/
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries
[11]: https://github.com/plotly/plotly.js
[11]: https://github.com/plotly/plotly.js

View File

@@ -141,7 +141,7 @@ module.exports = function (config) {
]
},
{
test: /\.worker\.js$/,
test: /worker\.js$/,
loader: 'worker-loader'
},
{

739
package-lock.json generated
View File

@@ -1,25 +1,24 @@
{
"name": "sqliteviz",
"version": "1.0.0",
"version": "0.13.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "sqliteviz",
"version": "1.0.0",
"version": "0.13.0",
"license": "Apache-2.0",
"dependencies": {
"codemirror": "^5.57.0",
"core-js": "^3.6.5",
"debounce": "^1.2.0",
"nanoid": "^3.1.12",
"papaparse": "^5.3.0",
"plotly.js": "^1.57.1",
"papaparse": "^5.3.1",
"plotly.js": "^1.58.4",
"promise-worker": "^2.0.1",
"react": "^16.13.1",
"react-chart-editor": "^0.42.0",
"react-chart-editor": "^0.45.0",
"react-dom": "^16.13.1",
"sql.js": "^1.3.0",
"sql.js": "^1.5.0",
"sqlite-parser": "^1.0.1",
"vue": "^2.6.11",
"vue-codemirror": "^4.0.6",
@@ -1471,7 +1470,10 @@
"node_modules/@icons/material": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz",
"integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw=="
"integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==",
"peerDependencies": {
"react": "*"
}
},
"node_modules/@intervolga/optimize-cssnano-plugin": {
"version": "1.0.6",
@@ -3747,14 +3749,17 @@
}
},
"node_modules/babel-plugin-styled-components": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.11.1.tgz",
"integrity": "sha512-YwrInHyKUk1PU3avIRdiLyCpM++18Rs1NgyMXEAQC33rIXs/vro0A+stf4sT0Gf22Got+xRWB8Cm0tw+qkRzBA==",
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.12.0.tgz",
"integrity": "sha512-FEiD7l5ZABdJPpLssKXjBUJMYqzbcNzBowfXDCdJhOpbhWiewapUaY+LZGT8R4Jg2TwOjGjG4RKeyrO5p9sBkA==",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.0.0",
"@babel/helper-module-imports": "^7.0.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"lodash": "^4.17.11"
},
"peerDependencies": {
"styled-components": ">= 2"
}
},
"node_modules/babel-plugin-syntax-jsx": {
@@ -6748,11 +6753,6 @@
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=",
"dev": true
},
"node_modules/debounce": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
"integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg=="
},
"node_modules/debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@@ -10057,9 +10057,9 @@
}
},
"node_modules/gl-plot3d": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-2.4.6.tgz",
"integrity": "sha512-CkrNvDKu0p74Di2g2Oc9kU+s1Oe+wi4cIfHzXABp8DvfoRl0/bayqJ9q8EcRAqMeQQxQZYGvJkk4hlBwI758Jw==",
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-2.4.7.tgz",
"integrity": "sha512-mLDVWrl4Dj0O0druWyHUK5l7cBQrRIJRn2oROEgrRuOgbbrLAzsREKefwMO0bA0YqkiZMFMnV5VvPA9j57X5Xg==",
"dependencies": {
"3d-view": "^2.0.0",
"a-big-triangle": "^1.0.3",
@@ -13481,6 +13481,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -13819,7 +13824,10 @@
"node_modules/mdi-react": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/mdi-react/-/mdi-react-5.2.0.tgz",
"integrity": "sha512-q0zeUZbissoRVouq9JYSTrr/+2qk2P0dJI9N2m/TvZDX5RMcwHsVxffiqisjlo2m6cbXiCzAQaGaGmjoPfC4Pg=="
"integrity": "sha512-q0zeUZbissoRVouq9JYSTrr/+2qk2P0dJI9N2m/TvZDX5RMcwHsVxffiqisjlo2m6cbXiCzAQaGaGmjoPfC4Pg==",
"peerDependencies": {
"react": ">=0.14.0"
}
},
"node_modules/mdn-data": {
"version": "2.0.4",
@@ -15440,9 +15448,9 @@
"dev": true
},
"node_modules/papaparse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.3.0.tgz",
"integrity": "sha512-Lb7jN/4bTpiuGPrYy4tkKoUS8sTki8zacB5ke1p5zolhcSE4TlWgrlsxjrDTbG/dFVh07ck7X36hUf/b5V68pg=="
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.3.1.tgz",
"integrity": "sha512-Dbt2yjLJrCwH2sRqKFFJaN5XgIASO9YOFeFP8rIBRG2Ain8mqk5r1M6DkfvqEVozVcz3r3HaUGw253hA1nLIcA=="
},
"node_modules/parallel-transform": {
"version": "1.2.0",
@@ -15936,18 +15944,22 @@
}
},
"node_modules/plotly-icons": {
"version": "1.3.14",
"resolved": "https://registry.npmjs.org/plotly-icons/-/plotly-icons-1.3.14.tgz",
"integrity": "sha512-qglJLtQKeE0g5Zr08Je6Q16tbyOhSqiZ7eVvlUuxMxvNAFYqoYgqUXaagi3ytwYZdn+5SxSTscOt/lsKrAiEWQ==",
"version": "1.3.15",
"resolved": "https://registry.npmjs.org/plotly-icons/-/plotly-icons-1.3.15.tgz",
"integrity": "sha512-0k9zlvlFtXHzMvSSOhqt42d6jy13N5ueF8VLaL7S43SHE/+DTaO8W8jeFXQj5V1lRd7vkaYp9ACxNtMfByH04Q==",
"dependencies": {
"mdi-react": "5.2.0",
"prop-types": "^15.6.1"
"prop-types": "^15.7.2"
},
"peerDependencies": {
"react": ">15",
"react-dom": ">15"
}
},
"node_modules/plotly.js": {
"version": "1.57.1",
"resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-1.57.1.tgz",
"integrity": "sha512-23GlzClmOGT1lE86Ys0DLuxBM/fgRNzJqH9y7ZylO4VPwstPAlQd12DklXsuqOgCNSxnnWUaP+J7BaUOFplsUg==",
"version": "1.58.4",
"resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-1.58.4.tgz",
"integrity": "sha512-hdt/aEvkPjS1HJ7tJKcPqsqi9ErEZPhUFs4d2ANTLeBim+AmVcHzS1rtwr7ZrVCINgliW/+92u81omJoy+lbUw==",
"dependencies": {
"@plotly/d3-sankey": "0.7.2",
"@plotly/d3-sankey-circular": "0.33.1",
@@ -15979,7 +15991,7 @@
"gl-mat4": "^1.2.0",
"gl-mesh3d": "^2.3.1",
"gl-plot2d": "^1.4.5",
"gl-plot3d": "^2.4.6",
"gl-plot3d": "^2.4.7",
"gl-pointcloud2d": "^1.0.3",
"gl-scatter3d": "^1.2.3",
"gl-select-box": "^1.0.4",
@@ -16057,14 +16069,6 @@
"color-space": "^1.14.6"
}
},
"node_modules/plotly.js/node_modules/tinycolor2": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
"integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==",
"engines": {
"node": "*"
}
},
"node_modules/plotly.js/node_modules/to-px": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/to-px/-/to-px-1.0.1.tgz",
@@ -17289,260 +17293,56 @@
}
},
"node_modules/react-chart-editor": {
"version": "0.42.0",
"resolved": "https://registry.npmjs.org/react-chart-editor/-/react-chart-editor-0.42.0.tgz",
"integrity": "sha512-SepVBYHRUMajDwjlPPHVbrLjjy9rH1lWB98cDSeOSukupzWxi/x+gJ8cbfPSSYRUdw3GbTDOmMcu/9SjK7qinQ==",
"version": "0.45.0",
"resolved": "https://registry.npmjs.org/react-chart-editor/-/react-chart-editor-0.45.0.tgz",
"integrity": "sha512-/SurlIFait/BbWhq7sd8gIPr5MbhjPgrNY+d4V3sH6R/BjUocN/5SqUhQGknOUkxH8Fu4V+qn/8GsjYRFvk5NA==",
"dependencies": {
"@plotly/draft-js-export-html": "1.2.0",
"classnames": "^2.2.6",
"draft-js": "^0.11.7",
"draft-js-import-html": "^1.3.3",
"draft-js-utils": "^1.3.3",
"fast-isnumeric": "^1.1.4",
"immutability-helper": "^3.1.1",
"plotly-icons": "1.3.14",
"plotly.js": "1.55.x",
"prop-types": "^15.7.2",
"raf": "^3.4.1",
"react-color": "^2.18.1",
"classnames": "2.2.6",
"draft-js": "0.11.7",
"draft-js-import-html": "1.4.1",
"draft-js-utils": "1.4.0",
"fast-isnumeric": "1.1.4",
"immutability-helper": "3.1.1",
"plotly-icons": "1.3.15",
"plotly.js": "1.58.x",
"prop-types": "15.7.2",
"raf": "3.4.1",
"react-color": "2.19.3",
"react-colorscales": "0.7.3",
"react-day-picker": "^7.4.8",
"react-dropzone": "^10.2.2",
"react-plotly.js": "^2.4.0",
"react-rangeslider": "^2.2.0",
"react-resizable-rotatable-draggable": "^0.2.0",
"react-select": "^2.4.2",
"react-tabs": "^3.1.1",
"styled-components": "^5.2.0",
"tinycolor2": "^1.4.1"
"react-day-picker": "7.4.8",
"react-dropzone": "10.2.2",
"react-plotly.js": "2.5.1",
"react-rangeslider": "2.2.0",
"react-resizable-rotatable-draggable": "0.2.0",
"react-select": "2.4.4",
"react-tabs": "3.2.1",
"styled-components": "5.2.1",
"tinycolor2": "1.4.2"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/react-chart-editor/node_modules/@mapbox/geojson-rewind": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.0.tgz",
"integrity": "sha512-73l/qJQgj/T/zO1JXVfuVvvKDgikD/7D/rHAD28S9BG1OTstgmftrmqfCx4U+zQAmtsB6HcDA3a7ymdnJZAQgg==",
"dependencies": {
"concat-stream": "~2.0.0",
"minimist": "^1.2.5"
},
"bin": {
"geojson-rewind": "geojson-rewind"
}
},
"node_modules/react-chart-editor/node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
"node >= 6.0"
],
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"node_modules/react-chart-editor/node_modules/es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"node_modules/react-chart-editor/node_modules/mapbox-gl": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.10.1.tgz",
"integrity": "sha512-0aHt+lFUpYfvh0kMIqXqNXqoYMuhuAsMlw87TbhWrw78Tx2zfuPI0Lx31/YPUgJ+Ire0tzQ4JnuBL7acDNXmMg==",
"dependencies": {
"@mapbox/geojson-rewind": "^0.5.0",
"@mapbox/geojson-types": "^1.0.2",
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
"@mapbox/mapbox-gl-supported": "^1.5.0",
"@mapbox/point-geometry": "^0.1.0",
"@mapbox/tiny-sdf": "^1.1.1",
"@mapbox/unitbezier": "^0.0.0",
"@mapbox/vector-tile": "^1.3.1",
"@mapbox/whoots-js": "^3.1.0",
"csscolorparser": "~1.0.3",
"earcut": "^2.2.2",
"geojson-vt": "^3.2.1",
"gl-matrix": "^3.2.1",
"grid-index": "^1.1.0",
"minimist": "^1.2.5",
"murmurhash-js": "^1.0.0",
"pbf": "^3.2.1",
"potpack": "^1.0.1",
"quickselect": "^2.0.0",
"rw": "^1.3.3",
"supercluster": "^7.0.0",
"tinyqueue": "^2.0.3",
"vt-pbf": "^3.1.1"
},
"engines": {
"node": ">=6.4.0"
}
},
"node_modules/react-chart-editor/node_modules/plotly.js": {
"version": "1.55.2",
"resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-1.55.2.tgz",
"integrity": "sha512-bphh7nlQOa1j2t7X+4vdGBSz/QME4Puk+Cuj7n/mYThPVxJkPtBFsTForCrgg4tLJWucY5TV+6F3zHNr4hyWZw==",
"dependencies": {
"@plotly/d3-sankey": "0.7.2",
"@plotly/d3-sankey-circular": "0.33.1",
"@plotly/point-cluster": "^3.1.9",
"@turf/area": "^6.0.1",
"@turf/bbox": "^6.0.1",
"@turf/centroid": "^6.0.2",
"alpha-shape": "^1.0.0",
"canvas-fit": "^1.5.0",
"color-normalize": "^1.5.0",
"color-rgba": "^2.1.1",
"convex-hull": "^1.0.3",
"country-regex": "^1.1.0",
"d3": "^3.5.17",
"d3-force": "^1.2.1",
"d3-hierarchy": "^1.1.9",
"d3-interpolate": "^1.4.0",
"d3-time-format": "^2.2.3",
"delaunay-triangulate": "^1.1.6",
"es6-promise": "^4.2.8",
"fast-isnumeric": "^1.1.4",
"gl-cone3d": "^1.5.2",
"gl-contour2d": "^1.1.7",
"gl-error3d": "^1.0.16",
"gl-heatmap2d": "^1.1.0",
"gl-line3d": "1.2.1",
"gl-mat4": "^1.2.0",
"gl-mesh3d": "^2.3.1",
"gl-plot2d": "^1.4.5",
"gl-plot3d": "^2.4.6",
"gl-pointcloud2d": "^1.0.3",
"gl-scatter3d": "^1.2.3",
"gl-select-box": "^1.0.4",
"gl-spikes2d": "^1.0.2",
"gl-streamtube3d": "^1.4.1",
"gl-surface3d": "^1.5.2",
"gl-text": "^1.1.8",
"glslify": "^7.1.1",
"has-hover": "^1.0.1",
"has-passive-events": "^1.0.0",
"image-size": "^0.7.5",
"is-mobile": "^2.2.2",
"mapbox-gl": "1.10.1",
"matrix-camera-controller": "^2.1.3",
"mouse-change": "^1.4.0",
"mouse-event-offset": "^3.0.2",
"mouse-wheel": "^1.2.0",
"ndarray": "^1.0.19",
"ndarray-linear-interpolate": "^1.0.0",
"parse-svg-path": "^0.1.2",
"polybooljs": "^1.2.0",
"regl": "^1.6.1",
"regl-error2d": "^2.0.11",
"regl-line2d": "^3.0.18",
"regl-scatter2d": "3.2.0",
"regl-splom": "^1.0.12",
"right-now": "^1.0.0",
"robust-orientation": "^1.1.3",
"sane-topojson": "^4.0.0",
"strongly-connected-components": "^1.0.1",
"superscript-text": "^1.0.0",
"svg-path-sdf": "^1.1.3",
"tinycolor2": "^1.4.1",
"to-px": "1.0.1",
"topojson-client": "^3.1.0",
"webgl-context": "^2.2.0",
"world-calendars": "^1.0.3"
}
},
"node_modules/react-chart-editor/node_modules/readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/react-chart-editor/node_modules/regl-scatter2d": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/regl-scatter2d/-/regl-scatter2d-3.2.0.tgz",
"integrity": "sha512-c0MxiakVW50UBslsHRmnq41w53bhat5oGvugZEpIZGTdKHVeopRAR2FQHeJf8YrEhOsVn7TpOk9tjySoyHXWGA==",
"dependencies": {
"@plotly/point-cluster": "^3.1.9",
"array-range": "^1.0.1",
"array-rearrange": "^2.2.2",
"clamp": "^1.0.1",
"color-id": "^1.1.0",
"color-normalize": "1.5.0",
"color-rgba": "^2.1.1",
"flatten-vertex-data": "^1.0.2",
"glslify": "^7.0.0",
"image-palette": "^2.1.0",
"is-iexplorer": "^1.0.0",
"object-assign": "^4.1.1",
"parse-rect": "^1.2.0",
"pick-by-alias": "^1.2.0",
"to-float32": "^1.0.1",
"update-diff": "^1.1.0"
}
},
"node_modules/react-chart-editor/node_modules/regl-scatter2d/node_modules/color-normalize": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/color-normalize/-/color-normalize-1.5.0.tgz",
"integrity": "sha512-rUT/HDXMr6RFffrR53oX3HGWkDOP9goSAQGBkUaAYKjOE2JxozccdGyufageWDlInRAjm/jYPrf/Y38oa+7obw==",
"dependencies": {
"clamp": "^1.0.1",
"color-rgba": "^2.1.1",
"dtype": "^2.0.0"
}
},
"node_modules/react-chart-editor/node_modules/supercluster": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.0.tgz",
"integrity": "sha512-LDasImUAFMhTqhK+cUXfy9C2KTUqJ3gucLjmNLNFmKWOnDUBxLFLH9oKuXOTCLveecmxh8fbk8kgh6Q0gsfe2w==",
"dependencies": {
"kdbush": "^3.0.0"
}
},
"node_modules/react-chart-editor/node_modules/to-px": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/to-px/-/to-px-1.0.1.tgz",
"integrity": "sha1-W7rtXl1PdkRbzJA8KTojB90yRkY=",
"dependencies": {
"parse-unit": "^1.0.1"
}
},
"node_modules/react-chart-editor/node_modules/topojson-client": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
"integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==",
"dependencies": {
"commander": "2"
},
"bin": {
"topo2geo": "bin/topo2geo",
"topomerge": "bin/topomerge",
"topoquantize": "bin/topoquantize"
"peerDependencies": {
"react": ">15",
"react-dom": ">15"
}
},
"node_modules/react-color": {
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.18.1.tgz",
"integrity": "sha512-X5XpyJS6ncplZs74ak0JJoqPi+33Nzpv5RYWWxn17bslih+X7OlgmfpmGC1fNvdkK7/SGWYf1JJdn7D2n5gSuQ==",
"version": "2.19.3",
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",
"integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==",
"dependencies": {
"@icons/material": "^0.2.4",
"lodash": "^4.17.11",
"lodash": "^4.17.15",
"lodash-es": "^4.17.15",
"material-colors": "^1.2.1",
"prop-types": "^15.5.10",
"reactcss": "^1.2.0",
"tinycolor2": "^1.4.1"
},
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-colorscales": {
@@ -17616,11 +17416,15 @@
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"node_modules/react-plotly.js": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/react-plotly.js/-/react-plotly.js-2.5.0.tgz",
"integrity": "sha512-nzir3uf+tFO1YXVUH5lFfD2plbDuZJXKrCO88KmRVnha2/zEhZBmZO8yS6GcRnLmSrhJkfmj6GTqWWvrJDBCBQ==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/react-plotly.js/-/react-plotly.js-2.5.1.tgz",
"integrity": "sha512-Oya14whSHvPsYXdI0nHOGs1pZhMzV2edV7HAW1xFHD58Y73m/LbG2Encvyz1tztL0vfjph0JNhiwO8cGBJnlhg==",
"dependencies": {
"prop-types": "^15.7.2"
},
"peerDependencies": {
"plotly.js": ">1.34.0",
"react": ">0.13.0"
}
},
"node_modules/react-rangeslider": {
@@ -17659,12 +17463,15 @@
}
},
"node_modules/react-tabs": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-3.1.1.tgz",
"integrity": "sha512-HpySC29NN1BkzBAnOC+ajfzPbTaVZcSWzMSjk56uAhPC/rBGtli8lTysR4CfPAyEE/hfweIzagOIoJ7nu80yng==",
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-3.2.1.tgz",
"integrity": "sha512-M7ERQvJgBVLTyojFmC3G4tpaJuMmUtsnYenVQm2oA1NjDrGXq1UuzHgxhVTDwimkJcKEbzgWCybXFSHQ/+2bsA==",
"dependencies": {
"clsx": "^1.1.0",
"prop-types": "^15.5.0"
},
"peerDependencies": {
"react": "^16.3.0 || ^17.0.0-0"
}
},
"node_modules/react-transition-group": {
@@ -19633,9 +19440,9 @@
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"node_modules/sql.js": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.3.0.tgz",
"integrity": "sha512-bxrJ/9rqJ2SA6hpHnSodRjKBugZHewRvNTITTt74W1VZWmzODjdS68yQW0/J9oC0NWKylHEtV1ptkoTyOYO4Tw=="
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.5.0.tgz",
"integrity": "sha512-Qqr6HgX/hCDpLFWdN0BNoNpYQ2c1tOl1c3HGI0cshjaFSAWszKICuLZ9CyFUvRFPpEGW8RzHzwuXWWvXVGTKBg=="
},
"node_modules/sqlite-parser": {
"version": "1.0.1",
@@ -20039,9 +19846,9 @@
"integrity": "sha1-CSDitN9nyOrulsa2I0/inoc9upk="
},
"node_modules/styled-components": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.2.0.tgz",
"integrity": "sha512-9qE8Vgp8C5cpGAIdFaQVAl89Zgx1TDM4Yf4tlHbO9cPijtpSXTMLHy9lmP0lb+yImhgPFb1AmZ1qMUubmg3HLg==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.2.1.tgz",
"integrity": "sha512-sBdgLWrCFTKtmZm/9x7jkIabjFNVzCUeKfoQsM6R3saImkUnjx0QYdLwJHBjY9ifEcmjDamJDVfknWm1yxZPxQ==",
"dependencies": {
"@babel/helper-module-imports": "^7.0.0",
"@babel/traverse": "^7.4.5",
@@ -20056,6 +19863,15 @@
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/styled-components"
},
"peerDependencies": {
"react": ">= 16.8.0",
"react-dom": ">= 16.8.0",
"react-is": ">= 16.8.0"
}
},
"node_modules/styled-components/node_modules/@emotion/stylis": {
@@ -20525,9 +20341,9 @@
"dev": true
},
"node_modules/tinycolor2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz",
"integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
"integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==",
"engines": {
"node": "*"
}
@@ -24700,7 +24516,8 @@
"@icons/material": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz",
"integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw=="
"integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==",
"requires": {}
},
"@intervolga/optimize-cssnano-plugin": {
"version": "1.0.6",
@@ -26677,9 +26494,9 @@
}
},
"babel-plugin-styled-components": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.11.1.tgz",
"integrity": "sha512-YwrInHyKUk1PU3avIRdiLyCpM++18Rs1NgyMXEAQC33rIXs/vro0A+stf4sT0Gf22Got+xRWB8Cm0tw+qkRzBA==",
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.12.0.tgz",
"integrity": "sha512-FEiD7l5ZABdJPpLssKXjBUJMYqzbcNzBowfXDCdJhOpbhWiewapUaY+LZGT8R4Jg2TwOjGjG4RKeyrO5p9sBkA==",
"requires": {
"@babel/helper-annotate-as-pure": "^7.0.0",
"@babel/helper-module-imports": "^7.0.0",
@@ -29304,11 +29121,6 @@
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=",
"dev": true
},
"debounce": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
"integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg=="
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@@ -32186,9 +31998,9 @@
}
},
"gl-plot3d": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-2.4.6.tgz",
"integrity": "sha512-CkrNvDKu0p74Di2g2Oc9kU+s1Oe+wi4cIfHzXABp8DvfoRl0/bayqJ9q8EcRAqMeQQxQZYGvJkk4hlBwI758Jw==",
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-2.4.7.tgz",
"integrity": "sha512-mLDVWrl4Dj0O0druWyHUK5l7cBQrRIJRn2oROEgrRuOgbbrLAzsREKefwMO0bA0YqkiZMFMnV5VvPA9j57X5Xg==",
"requires": {
"3d-view": "^2.0.0",
"a-big-triangle": "^1.0.3",
@@ -35078,6 +34890,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -35381,7 +35198,8 @@
"mdi-react": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/mdi-react/-/mdi-react-5.2.0.tgz",
"integrity": "sha512-q0zeUZbissoRVouq9JYSTrr/+2qk2P0dJI9N2m/TvZDX5RMcwHsVxffiqisjlo2m6cbXiCzAQaGaGmjoPfC4Pg=="
"integrity": "sha512-q0zeUZbissoRVouq9JYSTrr/+2qk2P0dJI9N2m/TvZDX5RMcwHsVxffiqisjlo2m6cbXiCzAQaGaGmjoPfC4Pg==",
"requires": {}
},
"mdn-data": {
"version": "2.0.4",
@@ -36755,9 +36573,9 @@
"dev": true
},
"papaparse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.3.0.tgz",
"integrity": "sha512-Lb7jN/4bTpiuGPrYy4tkKoUS8sTki8zacB5ke1p5zolhcSE4TlWgrlsxjrDTbG/dFVh07ck7X36hUf/b5V68pg=="
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.3.1.tgz",
"integrity": "sha512-Dbt2yjLJrCwH2sRqKFFJaN5XgIASO9YOFeFP8rIBRG2Ain8mqk5r1M6DkfvqEVozVcz3r3HaUGw253hA1nLIcA=="
},
"parallel-transform": {
"version": "1.2.0",
@@ -37179,18 +36997,18 @@
}
},
"plotly-icons": {
"version": "1.3.14",
"resolved": "https://registry.npmjs.org/plotly-icons/-/plotly-icons-1.3.14.tgz",
"integrity": "sha512-qglJLtQKeE0g5Zr08Je6Q16tbyOhSqiZ7eVvlUuxMxvNAFYqoYgqUXaagi3ytwYZdn+5SxSTscOt/lsKrAiEWQ==",
"version": "1.3.15",
"resolved": "https://registry.npmjs.org/plotly-icons/-/plotly-icons-1.3.15.tgz",
"integrity": "sha512-0k9zlvlFtXHzMvSSOhqt42d6jy13N5ueF8VLaL7S43SHE/+DTaO8W8jeFXQj5V1lRd7vkaYp9ACxNtMfByH04Q==",
"requires": {
"mdi-react": "5.2.0",
"prop-types": "^15.6.1"
"prop-types": "^15.7.2"
}
},
"plotly.js": {
"version": "1.57.1",
"resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-1.57.1.tgz",
"integrity": "sha512-23GlzClmOGT1lE86Ys0DLuxBM/fgRNzJqH9y7ZylO4VPwstPAlQd12DklXsuqOgCNSxnnWUaP+J7BaUOFplsUg==",
"version": "1.58.4",
"resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-1.58.4.tgz",
"integrity": "sha512-hdt/aEvkPjS1HJ7tJKcPqsqi9ErEZPhUFs4d2ANTLeBim+AmVcHzS1rtwr7ZrVCINgliW/+92u81omJoy+lbUw==",
"requires": {
"@plotly/d3-sankey": "0.7.2",
"@plotly/d3-sankey-circular": "0.33.1",
@@ -37222,7 +37040,7 @@
"gl-mat4": "^1.2.0",
"gl-mesh3d": "^2.3.1",
"gl-plot2d": "^1.4.5",
"gl-plot3d": "^2.4.6",
"gl-plot3d": "^2.4.7",
"gl-pointcloud2d": "^1.0.3",
"gl-scatter3d": "^1.2.3",
"gl-select-box": "^1.0.4",
@@ -37300,11 +37118,6 @@
"color-space": "^1.14.6"
}
},
"tinycolor2": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
"integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA=="
},
"to-px": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/to-px/-/to-px-1.0.1.tgz",
@@ -38390,240 +38203,42 @@
}
},
"react-chart-editor": {
"version": "0.42.0",
"resolved": "https://registry.npmjs.org/react-chart-editor/-/react-chart-editor-0.42.0.tgz",
"integrity": "sha512-SepVBYHRUMajDwjlPPHVbrLjjy9rH1lWB98cDSeOSukupzWxi/x+gJ8cbfPSSYRUdw3GbTDOmMcu/9SjK7qinQ==",
"version": "0.45.0",
"resolved": "https://registry.npmjs.org/react-chart-editor/-/react-chart-editor-0.45.0.tgz",
"integrity": "sha512-/SurlIFait/BbWhq7sd8gIPr5MbhjPgrNY+d4V3sH6R/BjUocN/5SqUhQGknOUkxH8Fu4V+qn/8GsjYRFvk5NA==",
"requires": {
"@plotly/draft-js-export-html": "1.2.0",
"classnames": "^2.2.6",
"draft-js": "^0.11.7",
"draft-js-import-html": "^1.3.3",
"draft-js-utils": "^1.3.3",
"fast-isnumeric": "^1.1.4",
"immutability-helper": "^3.1.1",
"plotly-icons": "1.3.14",
"plotly.js": "1.55.x",
"prop-types": "^15.7.2",
"raf": "^3.4.1",
"react-color": "^2.18.1",
"classnames": "2.2.6",
"draft-js": "0.11.7",
"draft-js-import-html": "1.4.1",
"draft-js-utils": "1.4.0",
"fast-isnumeric": "1.1.4",
"immutability-helper": "3.1.1",
"plotly-icons": "1.3.15",
"plotly.js": "1.58.x",
"prop-types": "15.7.2",
"raf": "3.4.1",
"react-color": "2.19.3",
"react-colorscales": "0.7.3",
"react-day-picker": "^7.4.8",
"react-dropzone": "^10.2.2",
"react-plotly.js": "^2.4.0",
"react-rangeslider": "^2.2.0",
"react-resizable-rotatable-draggable": "^0.2.0",
"react-select": "^2.4.2",
"react-tabs": "^3.1.1",
"styled-components": "^5.2.0",
"tinycolor2": "^1.4.1"
},
"dependencies": {
"@mapbox/geojson-rewind": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.0.tgz",
"integrity": "sha512-73l/qJQgj/T/zO1JXVfuVvvKDgikD/7D/rHAD28S9BG1OTstgmftrmqfCx4U+zQAmtsB6HcDA3a7ymdnJZAQgg==",
"requires": {
"concat-stream": "~2.0.0",
"minimist": "^1.2.5"
}
},
"concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"requires": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"mapbox-gl": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.10.1.tgz",
"integrity": "sha512-0aHt+lFUpYfvh0kMIqXqNXqoYMuhuAsMlw87TbhWrw78Tx2zfuPI0Lx31/YPUgJ+Ire0tzQ4JnuBL7acDNXmMg==",
"requires": {
"@mapbox/geojson-rewind": "^0.5.0",
"@mapbox/geojson-types": "^1.0.2",
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
"@mapbox/mapbox-gl-supported": "^1.5.0",
"@mapbox/point-geometry": "^0.1.0",
"@mapbox/tiny-sdf": "^1.1.1",
"@mapbox/unitbezier": "^0.0.0",
"@mapbox/vector-tile": "^1.3.1",
"@mapbox/whoots-js": "^3.1.0",
"csscolorparser": "~1.0.3",
"earcut": "^2.2.2",
"geojson-vt": "^3.2.1",
"gl-matrix": "^3.2.1",
"grid-index": "^1.1.0",
"minimist": "^1.2.5",
"murmurhash-js": "^1.0.0",
"pbf": "^3.2.1",
"potpack": "^1.0.1",
"quickselect": "^2.0.0",
"rw": "^1.3.3",
"supercluster": "^7.0.0",
"tinyqueue": "^2.0.3",
"vt-pbf": "^3.1.1"
}
},
"plotly.js": {
"version": "1.55.2",
"resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-1.55.2.tgz",
"integrity": "sha512-bphh7nlQOa1j2t7X+4vdGBSz/QME4Puk+Cuj7n/mYThPVxJkPtBFsTForCrgg4tLJWucY5TV+6F3zHNr4hyWZw==",
"requires": {
"@plotly/d3-sankey": "0.7.2",
"@plotly/d3-sankey-circular": "0.33.1",
"@plotly/point-cluster": "^3.1.9",
"@turf/area": "^6.0.1",
"@turf/bbox": "^6.0.1",
"@turf/centroid": "^6.0.2",
"alpha-shape": "^1.0.0",
"canvas-fit": "^1.5.0",
"color-normalize": "^1.5.0",
"color-rgba": "^2.1.1",
"convex-hull": "^1.0.3",
"country-regex": "^1.1.0",
"d3": "^3.5.17",
"d3-force": "^1.2.1",
"d3-hierarchy": "^1.1.9",
"d3-interpolate": "^1.4.0",
"d3-time-format": "^2.2.3",
"delaunay-triangulate": "^1.1.6",
"es6-promise": "^4.2.8",
"fast-isnumeric": "^1.1.4",
"gl-cone3d": "^1.5.2",
"gl-contour2d": "^1.1.7",
"gl-error3d": "^1.0.16",
"gl-heatmap2d": "^1.1.0",
"gl-line3d": "1.2.1",
"gl-mat4": "^1.2.0",
"gl-mesh3d": "^2.3.1",
"gl-plot2d": "^1.4.5",
"gl-plot3d": "^2.4.6",
"gl-pointcloud2d": "^1.0.3",
"gl-scatter3d": "^1.2.3",
"gl-select-box": "^1.0.4",
"gl-spikes2d": "^1.0.2",
"gl-streamtube3d": "^1.4.1",
"gl-surface3d": "^1.5.2",
"gl-text": "^1.1.8",
"glslify": "^7.1.1",
"has-hover": "^1.0.1",
"has-passive-events": "^1.0.0",
"image-size": "^0.7.5",
"is-mobile": "^2.2.2",
"mapbox-gl": "1.10.1",
"matrix-camera-controller": "^2.1.3",
"mouse-change": "^1.4.0",
"mouse-event-offset": "^3.0.2",
"mouse-wheel": "^1.2.0",
"ndarray": "^1.0.19",
"ndarray-linear-interpolate": "^1.0.0",
"parse-svg-path": "^0.1.2",
"polybooljs": "^1.2.0",
"regl": "^1.6.1",
"regl-error2d": "^2.0.11",
"regl-line2d": "^3.0.18",
"regl-scatter2d": "3.2.0",
"regl-splom": "^1.0.12",
"right-now": "^1.0.0",
"robust-orientation": "^1.1.3",
"sane-topojson": "^4.0.0",
"strongly-connected-components": "^1.0.1",
"superscript-text": "^1.0.0",
"svg-path-sdf": "^1.1.3",
"tinycolor2": "^1.4.1",
"to-px": "1.0.1",
"topojson-client": "^3.1.0",
"webgl-context": "^2.2.0",
"world-calendars": "^1.0.3"
}
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
},
"regl-scatter2d": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/regl-scatter2d/-/regl-scatter2d-3.2.0.tgz",
"integrity": "sha512-c0MxiakVW50UBslsHRmnq41w53bhat5oGvugZEpIZGTdKHVeopRAR2FQHeJf8YrEhOsVn7TpOk9tjySoyHXWGA==",
"requires": {
"@plotly/point-cluster": "^3.1.9",
"array-range": "^1.0.1",
"array-rearrange": "^2.2.2",
"clamp": "^1.0.1",
"color-id": "^1.1.0",
"color-normalize": "1.5.0",
"color-rgba": "^2.1.1",
"flatten-vertex-data": "^1.0.2",
"glslify": "^7.0.0",
"image-palette": "^2.1.0",
"is-iexplorer": "^1.0.0",
"object-assign": "^4.1.1",
"parse-rect": "^1.2.0",
"pick-by-alias": "^1.2.0",
"to-float32": "^1.0.1",
"update-diff": "^1.1.0"
},
"dependencies": {
"color-normalize": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/color-normalize/-/color-normalize-1.5.0.tgz",
"integrity": "sha512-rUT/HDXMr6RFffrR53oX3HGWkDOP9goSAQGBkUaAYKjOE2JxozccdGyufageWDlInRAjm/jYPrf/Y38oa+7obw==",
"requires": {
"clamp": "^1.0.1",
"color-rgba": "^2.1.1",
"dtype": "^2.0.0"
}
}
}
},
"supercluster": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.0.tgz",
"integrity": "sha512-LDasImUAFMhTqhK+cUXfy9C2KTUqJ3gucLjmNLNFmKWOnDUBxLFLH9oKuXOTCLveecmxh8fbk8kgh6Q0gsfe2w==",
"requires": {
"kdbush": "^3.0.0"
}
},
"to-px": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/to-px/-/to-px-1.0.1.tgz",
"integrity": "sha1-W7rtXl1PdkRbzJA8KTojB90yRkY=",
"requires": {
"parse-unit": "^1.0.1"
}
},
"topojson-client": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
"integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==",
"requires": {
"commander": "2"
}
}
"react-day-picker": "7.4.8",
"react-dropzone": "10.2.2",
"react-plotly.js": "2.5.1",
"react-rangeslider": "2.2.0",
"react-resizable-rotatable-draggable": "0.2.0",
"react-select": "2.4.4",
"react-tabs": "3.2.1",
"styled-components": "5.2.1",
"tinycolor2": "1.4.2"
}
},
"react-color": {
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.18.1.tgz",
"integrity": "sha512-X5XpyJS6ncplZs74ak0JJoqPi+33Nzpv5RYWWxn17bslih+X7OlgmfpmGC1fNvdkK7/SGWYf1JJdn7D2n5gSuQ==",
"version": "2.19.3",
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",
"integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==",
"requires": {
"@icons/material": "^0.2.4",
"lodash": "^4.17.11",
"lodash": "^4.17.15",
"lodash-es": "^4.17.15",
"material-colors": "^1.2.1",
"prop-types": "^15.5.10",
"reactcss": "^1.2.0",
@@ -38700,9 +38315,9 @@
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"react-plotly.js": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/react-plotly.js/-/react-plotly.js-2.5.0.tgz",
"integrity": "sha512-nzir3uf+tFO1YXVUH5lFfD2plbDuZJXKrCO88KmRVnha2/zEhZBmZO8yS6GcRnLmSrhJkfmj6GTqWWvrJDBCBQ==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/react-plotly.js/-/react-plotly.js-2.5.1.tgz",
"integrity": "sha512-Oya14whSHvPsYXdI0nHOGs1pZhMzV2edV7HAW1xFHD58Y73m/LbG2Encvyz1tztL0vfjph0JNhiwO8cGBJnlhg==",
"requires": {
"prop-types": "^15.7.2"
}
@@ -38736,9 +38351,9 @@
}
},
"react-tabs": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-3.1.1.tgz",
"integrity": "sha512-HpySC29NN1BkzBAnOC+ajfzPbTaVZcSWzMSjk56uAhPC/rBGtli8lTysR4CfPAyEE/hfweIzagOIoJ7nu80yng==",
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-3.2.1.tgz",
"integrity": "sha512-M7ERQvJgBVLTyojFmC3G4tpaJuMmUtsnYenVQm2oA1NjDrGXq1UuzHgxhVTDwimkJcKEbzgWCybXFSHQ/+2bsA==",
"requires": {
"clsx": "^1.1.0",
"prop-types": "^15.5.0"
@@ -40491,9 +40106,9 @@
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"sql.js": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.3.0.tgz",
"integrity": "sha512-bxrJ/9rqJ2SA6hpHnSodRjKBugZHewRvNTITTt74W1VZWmzODjdS68yQW0/J9oC0NWKylHEtV1ptkoTyOYO4Tw=="
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.5.0.tgz",
"integrity": "sha512-Qqr6HgX/hCDpLFWdN0BNoNpYQ2c1tOl1c3HGI0cshjaFSAWszKICuLZ9CyFUvRFPpEGW8RzHzwuXWWvXVGTKBg=="
},
"sqlite-parser": {
"version": "1.0.1",
@@ -40832,9 +40447,9 @@
"integrity": "sha1-CSDitN9nyOrulsa2I0/inoc9upk="
},
"styled-components": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.2.0.tgz",
"integrity": "sha512-9qE8Vgp8C5cpGAIdFaQVAl89Zgx1TDM4Yf4tlHbO9cPijtpSXTMLHy9lmP0lb+yImhgPFb1AmZ1qMUubmg3HLg==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.2.1.tgz",
"integrity": "sha512-sBdgLWrCFTKtmZm/9x7jkIabjFNVzCUeKfoQsM6R3saImkUnjx0QYdLwJHBjY9ifEcmjDamJDVfknWm1yxZPxQ==",
"requires": {
"@babel/helper-module-imports": "^7.0.0",
"@babel/traverse": "^7.4.5",
@@ -41250,9 +40865,9 @@
"dev": true
},
"tinycolor2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz",
"integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g="
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
"integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA=="
},
"tinyqueue": {
"version": "2.0.3",

View File

@@ -1,6 +1,6 @@
{
"name": "sqliteviz",
"version": "1.0.0",
"version": "0.13.1",
"license": "Apache-2.0",
"private": true,
"scripts": {
@@ -12,15 +12,14 @@
"dependencies": {
"codemirror": "^5.57.0",
"core-js": "^3.6.5",
"debounce": "^1.2.0",
"nanoid": "^3.1.12",
"papaparse": "^5.3.0",
"plotly.js": "^1.57.1",
"papaparse": "^5.3.1",
"plotly.js": "^1.58.4",
"promise-worker": "^2.0.1",
"react": "^16.13.1",
"react-chart-editor": "^0.42.0",
"react-chart-editor": "^0.45.0",
"react-dom": "^16.13.1",
"sql.js": "^1.3.0",
"sql.js": "^1.5.0",
"sqlite-parser": "^1.0.1",
"vue": "^2.6.11",
"vue-codemirror": "^4.0.6",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/Logo48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 798 B

After

Width:  |  Height:  |  Size: 774 B

View File

@@ -3,6 +3,16 @@
"description": "Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases or CSV.",
"display": "fullscreen",
"icons": [
{
"src": "favicon.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "Logo48x48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "Logo192x192.png",
"sizes": "192x192",

View File

@@ -60,4 +60,7 @@ button,
body {
margin: 0;
}
.CodeMirror-hints {
z-index: 999 !important;
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -1,5 +1,5 @@
.rounded-bg {
padding: 40px 5px 5px;
padding: 35px 5px 5px;
background-color: white;
border-radius: 5px;
position: relative;
@@ -36,7 +36,7 @@
}
table {
min-width: 100%;
margin-top: -40px;
margin-top: -35px;
border-collapse: collapse;
}
thead th, .fixed-header {
@@ -56,7 +56,7 @@ tbody td {
border-right: 1px solid var(--color-border-light);
}
td, th, .fixed-header {
padding: 12px 24px;
padding: 8px 24px;
white-space: nowrap;
}

View File

@@ -40,7 +40,7 @@
</template>
<script>
import ascii from '@/ascii'
import ascii from './ascii'
import DropDownChevron from '@/components/svg/dropDownChevron'
import ClearIcon from '@/components/svg/clear'

View File

@@ -10,10 +10,10 @@ export default {
getResult (source) {
const result = {}
if (source.meta.fields) {
result.columns = source.meta.fields
result.columns = source.meta.fields.map(col => col.trim())
result.values = source.data.map(row => {
const resultRow = []
result.columns.forEach(col => { resultRow.push(row[col]) })
source.meta.fields.forEach(col => { resultRow.push(row[col]) })
return resultRow
})
} else {

View File

@@ -0,0 +1,381 @@
<template>
<modal
:name="dialogName"
classes="dialog"
height="auto"
width="80%"
scrollable
:clickToClose="false"
>
<div class="dialog-header">
CSV import
<close-icon @click="cancelCsvImport" :disabled="disableDialog"/>
</div>
<div class="dialog-body">
<text-field
label="Table name"
v-model="tableName"
width="484px"
:disabled="disableDialog"
:error-msg="tableNameError"
id="csv-table-name"
/>
<div class="chars">
<delimiter-selector
v-model="delimiter"
width="210px"
class="char-input"
@input="previewCsv"
:disabled="disableDialog"
/>
<text-field
label="Quote char"
hint="The character used to quote fields."
v-model="quoteChar"
width="93px"
:disabled="disableDialog"
class="char-input"
id="quote-char"
/>
<text-field
label="Escape char"
hint='The character used to escape the quote character within a field (e.g. "column with ""quotes"" in text").'
max-hint-width="242px"
v-model="escapeChar"
width="93px"
:disabled="disableDialog"
class="char-input"
id="escape-char"
/>
</div>
<check-box
@click="header = $event"
:init="true"
label="Use first row as column headers"
:disabled="disableDialog"
/>
<sql-table
v-if="previewData && (previewData.values.length > 0 || previewData.columns.length > 0)"
:data-set="previewData"
height="160"
class="preview-table"
:preview="true"
/>
<div v-else class="no-data">No data</div>
<logs
class="import-csv-errors"
:messages="importCsvMessages"
/>
</div>
<div class="dialog-buttons-container">
<button
class="secondary"
:disabled="disableDialog"
@click="cancelCsvImport"
id="csv-cancel"
>
Cancel
</button>
<button
v-show="!importCsvCompleted"
class="primary"
:disabled="disableDialog"
@click="loadFromCsv(file)"
id="csv-import"
>
Import
</button>
<button
v-show="importCsvCompleted"
class="primary"
:disabled="disableDialog"
@click="finish"
id="csv-finish"
>
Finish
</button>
</div>
</modal>
</template>
<script>
import csv from './csv'
import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField'
import DelimiterSelector from './DelimiterSelector'
import CheckBox from '@/components/CheckBox'
import SqlTable from '@/components/SqlTable'
import Logs from '@/components/Logs'
import time from '@/lib/utils/time'
import fIo from '@/lib/utils/fileIo'
export default {
name: 'CsvImport',
components: {
CloseIcon,
TextField,
DelimiterSelector,
CheckBox,
SqlTable,
Logs
},
props: ['file', 'db', 'dialogName'],
data () {
return {
disableDialog: false,
tableName: '',
delimiter: '',
quoteChar: '"',
escapeChar: '"',
header: true,
importCsvCompleted: false,
importCsvMessages: [],
previewData: null,
addedTable: null,
tableNameError: ''
}
},
watch: {
quoteChar () {
this.previewCsv()
},
escapeChar () {
this.previewCsv()
},
header () {
this.previewCsv()
},
tableName: time.debounce(function () {
this.tableNameError = ''
if (!this.tableName) {
return
}
this.db.validateTableName(this.tableName)
.catch(err => {
this.tableNameError = err.message + '. Try another table name.'
})
}, 400)
},
methods: {
cancelCsvImport () {
if (!this.disableDialog) {
if (this.addedTable) {
this.db.execute(`DROP TABLE "${this.addedTable}"`)
this.db.refreshSchema()
}
this.$modal.hide(this.dialogName)
this.$emit('cancel')
}
},
reset () {
this.header = true
this.quoteChar = '"'
this.escapeChar = '"'
this.delimiter = ''
this.tableName = ''
this.disableDialog = false
this.importCsvCompleted = false
this.importCsvMessages = []
this.previewData = null
this.addedTable = null
this.tableNameError = ''
},
open () {
this.tableName = this.db.sanitizeTableName(fIo.getFileName(this.file))
this.$modal.show(this.dialogName)
},
async previewCsv () {
this.importCsvCompleted = false
const config = {
preview: 3,
quoteChar: this.quoteChar || '"',
escapeChar: this.escapeChar,
header: this.header,
delimiter: this.delimiter
}
try {
const start = new Date()
const parseResult = await csv.parse(this.file, config)
const end = new Date()
this.previewData = parseResult.data
this.delimiter = parseResult.delimiter
// In parseResult.messages we can get parse errors
this.importCsvMessages = parseResult.messages || []
if (!parseResult.hasErrors) {
this.importCsvMessages.push({
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
type: 'success'
})
}
} catch (err) {
this.importCsvMessages = [{
message: err,
type: 'error'
}]
}
},
async loadFromCsv (file) {
if (!this.tableName) {
this.tableNameError = "Table name can't be empty"
return
}
this.disableDialog = true
const config = {
quoteChar: this.quoteChar || '"',
escapeChar: this.escapeChar,
header: this.header,
delimiter: this.delimiter
}
const parseCsvMsg = {
message: 'Parsing CSV...',
type: 'info'
}
this.importCsvMessages.push(parseCsvMsg)
const parseCsvLoadingIndicator = setTimeout(() => { parseCsvMsg.type = 'loading' }, 1000)
const importMsg = {
message: 'Importing CSV into a SQLite database...',
type: 'info'
}
let importLoadingIndicator = null
const updateProgress = progress => {
this.$set(importMsg, 'progress', progress)
}
const progressCounterId = this.db.createProgressCounter(updateProgress)
try {
let start = new Date()
const parseResult = await csv.parse(this.file, config)
let end = new Date()
if (!parseResult.hasErrors) {
const rowCount = parseResult.data.values.length
let period = time.getPeriod(start, end)
parseCsvMsg.type = 'success'
if (parseResult.messages.length > 0) {
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
parseCsvMsg.message = `${rowCount} rows are parsed in ${period}.`
} else {
// Inform about csv parsing success
parseCsvMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
}
// Loading indicator for csv parsing is not needed anymore
clearTimeout(parseCsvLoadingIndicator)
// Add info about import start
this.importCsvMessages.push(importMsg)
// Show import progress after 1 second
importLoadingIndicator = setTimeout(() => {
importMsg.type = 'loading'
}, 1000)
// Add table
start = new Date()
await this.db.addTableFromCsv(this.tableName, parseResult.data, progressCounterId)
end = new Date()
this.addedTable = this.tableName
// Inform about import success
period = time.getPeriod(start, end)
importMsg.message = `Importing CSV into a SQLite database is completed in ${period}.`
importMsg.type = 'success'
// Loading indicator for import is not needed anymore
clearTimeout(importLoadingIndicator)
this.importCsvCompleted = true
} else {
parseCsvMsg.message = 'Parsing ended with errors.'
parseCsvMsg.type = 'info'
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
}
} catch (err) {
if (parseCsvMsg.type === 'loading') {
parseCsvMsg.type = 'info'
}
if (importMsg.type === 'loading') {
importMsg.type = 'info'
}
this.importCsvMessages.push({
message: err,
type: 'error'
})
}
clearTimeout(parseCsvLoadingIndicator)
clearTimeout(importLoadingIndicator)
this.db.deleteProgressCounter(progressCounterId)
this.disableDialog = false
},
async finish () {
this.$modal.hide(this.dialogName)
const stmt = [
'/*',
` * Your CSV file has been imported into ${this.addedTable} table.`,
' * You can run this SQL query to make all CSV records available for charting.',
' */',
`SELECT * FROM "${this.addedTable}"`
].join('\n')
const tabId = await this.$store.dispatch('addTab', { query: stmt })
this.$store.commit('setCurrentTabId', tabId)
this.importCsvCompleted = false
this.$emit('finish')
}
}
}
</script>
<style scoped>
.dialog-body {
padding-bottom: 0;
}
.chars {
display: flex;
align-items: flex-end;
margin: 24px 0 20px;
}
.char-input {
margin-right: 44px;
}
.preview-table {
margin-top: 18px;
}
.import-csv-errors {
height: 136px;
margin-top: 8px;
}
.no-data {
margin-top: 32px;
background-color: white;
border-radius: 5px;
position: relative;
border: 1px solid var(--color-border-light);
box-sizing: border-box;
height: 147px;
font-size: 13px;
color: var(--color-text-base);
display: flex;
justify-content: center;
align-items: center;
}
/* https://github.com/euvl/vue-js-modal/issues/623 */
>>> .vm--modal {
max-width: 1152px;
margin: auto;
left: 0 !important;
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="db-uploader-container" :style="{ width }">
<change-db-icon v-if="type === 'small'" @click.native="browse"/>
<div v-if="['regular', 'illustrated'].includes(type)" class="drop-area-container">
<div v-if="type === 'illustrated'" class="drop-area-container">
<div
class="drop-area"
@dragover.prevent="state = 'dragover'"
@@ -26,7 +26,8 @@
ref="fileImg"
:class="{
'swing': state === 'dragover',
'fly': state === 'drop'
'fly': state === 'dropping',
'hidden': state === 'dropped'
}"
:src="require('@/assets/images/file.png')"
/>
@@ -41,112 +42,22 @@
<div id="error" class="error"></div>
<!--Parse csv dialog -->
<modal name="parse" classes="dialog" height="auto" width="60%" :clickToClose="false">
<div class="dialog-header">
Import CSV
<close-icon @click="cancelCsvImport" :disabled="disableDialog"/>
</div>
<div class="dialog-body">
<div class="chars">
<delimiter-selector
v-model="delimiter"
width="210px"
class="char-input"
@input="previewCSV"
:disabled="disableDialog"
/>
<text-field
label="Quote char"
hint="The character used to quote fields."
v-model="quoteChar"
width="93px"
:disabled="disableDialog"
class="char-input"
id="quote-char"
/>
<text-field
label="Escape char"
hint='The character used to escape the quote character within a field (e.g. "column with ""quotes"" in text").'
max-hint-width="242px"
v-model="escapeChar"
width="93px"
:disabled="disableDialog"
class="char-input"
id="escape-char"
/>
</div>
<check-box
@click="header = $event"
:init="true"
label="Use first row as column headers"
:disabled="disableDialog"
/>
<sql-table
v-if="previewData"
:data-set="previewData"
height="160"
class="preview-table"
:preview="true"
/>
<div v-if="!previewData" class="no-data">No data</div>
<logs
class="import-csv-errors"
:messages="importCsvMessages"
/>
</div>
<div class="dialog-buttons-container">
<button
class="secondary"
:disabled="disableDialog"
@click="cancelCsvImport"
id="csv-cancel"
>
Cancel
</button>
<button
v-show="!importCsvCompleted"
class="primary"
:disabled="disableDialog"
@click="loadFromCsv(file)"
id="csv-import"
>
Import
</button>
<button
v-show="importCsvCompleted"
class="primary"
:disabled="disableDialog"
@click="finish"
id="csv-finish"
>
Finish
</button>
</div>
</modal>
<csv-import
ref="addCsv"
:file="file"
:db="newDb"
dialog-name="importFromCsv"
@cancel="cancelCsvImport"
@finish="finish"
/>
</div>
</template>
<script>
import fu from '@/file.utils'
import csv from '@/csv'
import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField'
import DelimiterSelector from '@/components/DelimiterSelector'
import CheckBox from '@/components/CheckBox'
import SqlTable from '@/components/SqlTable'
import Logs from '@/components/Logs'
import fIo from '@/lib/utils/fileIo'
import ChangeDbIcon from '@/components/svg/changeDb'
import time from '@/time'
import database from '@/database'
const csvMimeTypes = [
'text/csv',
'text/x-csv',
'application/x-csv',
'application/csv',
'text/x-comma-separated-values',
'text/comma-separated-values'
]
import database from '@/lib/database'
import CsvImport from '@/components/CsvImport'
export default {
name: 'DbUploader',
@@ -154,9 +65,9 @@ export default {
type: {
type: String,
required: false,
default: 'regular',
default: 'small',
validator: (value) => {
return ['regular', 'illustrated', 'small'].includes(value)
return ['illustrated', 'small'].includes(value)
}
},
width: {
@@ -167,27 +78,13 @@ export default {
},
components: {
ChangeDbIcon,
TextField,
DelimiterSelector,
CloseIcon,
CheckBox,
SqlTable,
Logs
CsvImport
},
data () {
return {
state: '',
animationPromise: Promise.resolve(),
file: null,
schema: null,
delimiter: '',
quoteChar: '"',
escapeChar: '"',
header: true,
previewData: null,
importCsvMessages: [],
disableDialog: false,
importCsvCompleted: false,
newDb: null
}
},
@@ -196,206 +93,50 @@ export default {
this.animationPromise = new Promise((resolve) => {
this.$refs.fileImg.addEventListener('animationend', event => {
if (event.animationName.startsWith('fly')) {
this.state = 'dropped'
resolve()
}
})
})
}
},
watch: {
quoteChar () {
this.previewCSV()
},
escapeChar () {
this.previewCSV()
},
header () {
this.previewCSV()
}
},
methods: {
cancelCsvImport () {
if (!this.disableDialog) {
this.$modal.hide('parse')
if (this.newDb) {
this.newDb.shutDown()
this.newDb = null
}
if (this.newDb) {
this.newDb.shutDown()
this.newDb = null
}
},
async finish () {
this.$store.commit('setDb', this.newDb)
this.$store.commit('saveSchema', this.schema)
if (this.importCsvCompleted) {
this.$modal.hide('parse')
const tabId = await this.$store.dispatch('addTab', { query: 'select * from csv_import' })
this.$store.commit('setCurrentTabId', tabId)
}
if (this.$route.path !== '/editor') {
this.$router.push('/editor')
}
},
async previewCSV () {
this.importCsvCompleted = false
const config = {
preview: 3,
quoteChar: this.quoteChar || '"',
escapeChar: this.escapeChar,
header: this.header,
delimiter: this.delimiter
}
try {
const start = new Date()
const parseResult = await csv.parse(this.file, config)
const end = new Date()
this.previewData = parseResult.data
this.delimiter = parseResult.delimiter
// In parseResult.messages we can get parse errors
this.importCsvMessages = parseResult.messages || []
if (!parseResult.hasErrors) {
this.importCsvMessages.push({
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
type: 'success'
})
}
} catch (err) {
this.importCsvMessages = [{
message: err,
type: 'error'
}]
}
},
loadDb (file) {
this.newDb = database.getNewDatabase()
return Promise.all([this.newDb.loadDb(file), this.animationPromise])
.then(([schema]) => {
this.schema = schema
this.finish()
})
},
async loadFromCsv (file) {
this.disableDialog = true
const config = {
quoteChar: this.quoteChar || '"',
escapeChar: this.escapeChar,
header: this.header,
delimiter: this.delimiter
}
const parseCsvMsg = {
message: 'Parsing CSV...',
type: 'info'
}
this.importCsvMessages.push(parseCsvMsg)
const parseCsvLoadingIndicator = setTimeout(() => { parseCsvMsg.type = 'loading' }, 1000)
const importMsg = {
message: 'Importing CSV into a SQLite database...',
type: 'info'
}
let importLoadingIndicator = null
const updateProgress = progress => {
this.$set(importMsg, 'progress', progress)
}
this.newDb = database.getNewDatabase()
const progressCounterId = this.newDb.createProgressCounter(updateProgress)
try {
let start = new Date()
const parseResult = await csv.parse(this.file, config)
let end = new Date()
if (!parseResult.hasErrors) {
const rowCount = parseResult.data.values.length
let period = time.getPeriod(start, end)
parseCsvMsg.type = 'success'
if (parseResult.messages.length > 0) {
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
parseCsvMsg.message = `${rowCount} rows are parsed in ${period}.`
} else {
// Inform about csv parsing success
parseCsvMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
}
// Loading indicator for csv parsing is not needed anymore
clearTimeout(parseCsvLoadingIndicator)
// Add info about import start
this.importCsvMessages.push(importMsg)
// Show import progress after 1 second
importLoadingIndicator = setTimeout(() => {
importMsg.type = 'loading'
}, 1000)
// Create db with csv table and get schema
const name = file.name.replace(/\.[^.]+$/, '')
start = new Date()
this.schema = await this.newDb.createDb(name, parseResult.data, progressCounterId)
end = new Date()
// Inform about import success
period = time.getPeriod(start, end)
importMsg.message = `Importing CSV into a SQLite database is completed in ${period}.`
importMsg.type = 'success'
// Loading indicator for import is not needed anymore
clearTimeout(importLoadingIndicator)
this.importCsvCompleted = true
} else {
parseCsvMsg.message = 'Parsing ended with errors.'
parseCsvMsg.type = 'info'
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
}
} catch (err) {
if (parseCsvMsg.type === 'loading') {
parseCsvMsg.type = 'info'
}
if (importMsg.type === 'loading') {
importMsg.type = 'info'
}
this.importCsvMessages.push({
message: err,
type: 'error'
})
}
clearTimeout(parseCsvLoadingIndicator)
clearTimeout(importLoadingIndicator)
this.newDb.deleteProgressCounter(progressCounterId)
this.disableDialog = false
.then(this.finish)
},
async checkFile (file) {
this.state = 'drop'
if (csvMimeTypes.includes(file.type)) {
this.file = file
this.header = true
this.quoteChar = '"'
this.escapeChar = '"'
this.delimiter = ''
return Promise.all([this.previewCSV(), this.animationPromise])
.then(() => {
this.$modal.show('parse')
})
} else {
this.state = 'dropping'
this.newDb = database.getNewDatabase()
if (fIo.isDatabase(file)) {
this.loadDb(file)
} else {
this.file = file
await this.$nextTick()
const csvImport = this.$refs.addCsv
csvImport.reset()
return Promise.all([csvImport.previewCsv(), this.animationPromise])
.then(csvImport.open)
}
},
browse () {
fu.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
.then(this.checkFile)
},
@@ -431,6 +172,7 @@ export default {
align-items: center;
justify-content: center;
height: 100%;
cursor: pointer;
}
#img-container {
@@ -502,42 +244,16 @@ export default {
#file-img.fly {
animation: fly ease-in-out 1s 1 normal;
transform-origin: center center;
top: 183px;
left: 225px;
transition: top 1s ease-in-out, left 1s ease-in-out;
}
@keyframes fly {
100% { transform: rotate(360deg) scale(0.5); }
}
/* Parse CSV dialog */
.chars {
display: flex;
align-items: flex-end;
margin-bottom: 20px;
}
.char-input {
margin-right: 44px;
}
.preview-table {
margin-top: 32px;
100% {
transform: rotate(360deg) scale(0.5);
top: 183px;
left: 225px;
}
}
.import-csv-errors {
height: 160px;
margin-top: 32px;
}
.no-data {
margin-top: 32px;
background-color: white;
border-radius: 5px;
position: relative;
border: 1px solid var(--color-border-light);
box-sizing: border-box;
height: 160px;
font-size: 13px;
color: var(--color-text-base);
display: flex;
justify-content: center;
align-items: center;
#file-img.hidden {
display: none;
}
</style>

View File

@@ -1,17 +1,18 @@
<template>
<svg :class="animationClass" height="20" width="20" viewBox="0 0 20 20">
<svg :class="animationClass" :height="size" :width="size" :viewBox="`0 0 ${size} ${size}`">
<circle
class="loader-svg bg"
cx="10"
cy="10"
r="8"
:style="{ strokeWidth }"
:cx="size / 2"
:cy="size / 2"
:r="radius"
/>
<circle
class="loader-svg front"
:style="{ strokeDasharray: circleProgress }"
cx="10"
cy="10"
r="8"
:style="{ strokeDasharray: circleProgress, strokeDashoffset: offset, strokeWidth }"
:cx="size / 2"
:cy="size / 2"
:r="radius"
/>
</svg>
</template>
@@ -19,15 +20,35 @@
<script>
export default {
name: 'LoadingIndicator',
props: ['progress'],
props: {
progress: {
type: Number,
required: false
},
size: {
type: Number,
required: false,
default: 20
}
},
computed: {
circleProgress () {
const dash = (50.24 * this.progress) / 100
const space = 50.24 - dash
const circle = this.radius * 3.14 * 2
const dash = this.progress ? (circle * this.progress) / 100 : circle * 1 / 3
const space = circle - dash
return `${dash}px, ${space}px`
},
animationClass () {
return this.progress === undefined ? 'loading' : 'progress'
},
radius () {
return this.size / 2 - this.strokeWidth
},
offset () {
return this.radius * 3.14 / 2
},
strokeWidth () {
return this.size / 10
}
}
}
@@ -38,7 +59,6 @@ export default {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
fill: none;
stroke-width: 2px;
stroke-linecap: round;
stroke: var(--color-accent);
}
@@ -48,27 +68,30 @@ export default {
}
.loading .loader-svg.front {
stroke-dasharray: 40.24px;
will-change: transform;
animation: fill-animation-loading 1s cubic-bezier(1,1,1,1) 0s infinite;
transform-origin: center;
}
/*
We can't change anything in loading animation except transform, opacity and filter. Because in
our case the Main Thread can be busy and animation will be frozen (e. g. getting a result set
from the web-worker after query execution).
But transform, opacity and filter trigger changes only in the Composite Layer stage in rendering
waterfall. Hence they can be processed only with Compositor Thread while the Main Thread
processes something else.
https://www.viget.com/articles/animation-performance-101-browser-under-the-hood/
*/
@keyframes fill-animation-loading {
0% {
stroke-dasharray: 10px 40.24px;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 25.12px;
stroke-dashoffset: 25.12px;
transform: rotate(0deg);
}
100% {
stroke-dasharray: 10px 40.24px;
stroke-dashoffset: 50.24px;
transform: rotate(360deg);
}
}
.progress .loader-svg.front {
stroke-dashoffset: 12.56;
transition: stroke-dasharray 0.2s;
}

View File

@@ -63,14 +63,16 @@ export default {
border-radius: 5px;
border: 1px solid var(--color-border-light);
box-sizing: border-box;
overflow-y: scroll;
overflow-y: auto;
color: var(--color-text-base);
}
.msg {
padding: 16px 7px;
padding: 12px 7px;
border-bottom: 1px solid var(--color-border-light);
display: flex;
align-items: flex-start;
font-family: monospace;
font-size: 13px;
}
.msg:last-child {

View File

@@ -65,7 +65,7 @@
</template>
<script>
import splitter from '@/splitter'
import splitter from './splitter'
export default {
name: 'Splitpanes',

View File

@@ -41,6 +41,7 @@
<div class="table-footer-count">
{{ dataSet.values.length}} {{dataSet.values.length === 1 ? 'row' : 'rows'}} retrieved
<span v-if="preview">for preview</span>
<span v-if="time">in {{ time }}</span>
</div>
<pager v-show="pageCount > 1" :page-count="pageCount" v-model="currentPage" />
</div>
@@ -48,12 +49,12 @@
</template>
<script>
import Pager from '@/components/Pager'
import Pager from './Pager'
export default {
name: 'SqlTable',
components: { Pager },
props: ['dataSet', 'height', 'preview'],
props: ['dataSet', 'time', 'height', 'preview'],
data () {
return {
header: null,

View File

@@ -87,4 +87,7 @@ input.error {
margin-top: 2px;
position: absolute;
}
.text-field-error:first-letter {
text-transform: uppercase;
}
</style>

View File

@@ -0,0 +1,61 @@
<template>
<span>
<svg
class="icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
@click.stop="$emit('click')"
@mouseover="showTooltip"
@mouseout="hideTooltip"
>
<g clip-path="url(#clip0)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="
M13.6573 1.5H2.59985C1.77485 1.5 1.09985 2.175 1.09985 3V13.6649C1.09985 14.4899
1.77485 15.1649 2.59985
15.1649H9.84V13.6649H8.87866V9.08244H13.6573V9.83777H15.1573V3C15.1573
2.17 14.4873 1.5 13.6573 1.5ZM13.6573
7.58244V3H8.87866V7.58244H13.6573ZM7.37866 3H2.59985V7.58244H7.37866V3ZM2.59985
9.08244V13.6649H7.37866V9.08244H2.59985ZM13.1702
10.8434H15.6702V13.1717H18.0001V15.6717H15.6702V18H13.1702V15.6717H10.8401V13.1717H13.1702V10.8434Z
"
fill="#A2B1C6"
/>
</g>
<defs>
<clipPath id="clip0">
<rect width="18" height="18" fill="white"/>
</clipPath>
</defs>
</svg>
<span class="icon-tooltip" :style="tooltipStyle">
Add new table from CSV
</span>
</span>
</template>
<script>
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'AddTableIcon',
mixins: [tooltipMixin],
props: ['tooltip']
}
</script>
<style scoped>
.icon {
display: block;
margin: 0;
cursor: pointer;
}
.icon:hover path {
fill: var(--color-accent);
}
</style>

View File

@@ -16,13 +16,13 @@
/>
</svg>
<span class="icon-tooltip" :style="tooltipStyle">
Change database
Load another database or CSV
</span>
</div>
</template>
<script>
import tooltipMixin from '@/mixins/tooltips'
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'changeDbIcon',

View File

@@ -23,7 +23,7 @@
</template>
<script>
import tooltipMixin from '@/mixins/tooltips'
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'ExportIcon',

View File

@@ -20,7 +20,7 @@
</template>
<script>
import tooltipMixin from '@/mixins/tooltips'
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'HintIcon',

View File

@@ -1,44 +0,0 @@
export default {
* generateChunks (arr, size) {
const count = Math.ceil(arr.length / size)
for (let i = 0; i <= count - 1; i++) {
const start = size * i
const end = start + size
yield arr.slice(start, end)
}
},
getInsertStmt (columns) {
const colList = `"${columns.join('", "')}"`
const params = columns.map(() => '?').join(', ')
return `INSERT INTO csv_import (${colList}) VALUES (${params});`
},
getCreateStatement (columns, values) {
let result = 'CREATE table csv_import('
columns.forEach((col, index) => {
// Get the first row of values to determine types
const value = values[0][index]
let type = ''
switch (typeof value) {
case 'number': {
type = 'REAL'
break
}
case 'boolean': {
type = 'INTEGER'
break
}
case 'string': {
type = 'TEXT'
break
}
default: type = 'TEXT'
}
result += `"${col}" ${type}, `
})
result = result.replace(/,\s$/, ');')
return result
}
}

View File

@@ -1,5 +1,5 @@
import initSqlJs from 'sql.js/dist/sql-wasm.js'
import dbUtils from '@/db.utils'
import dbUtils from './_statements'
let SQL = null
const sqlModuleReady = initSqlJs().then(sqlModule => { SQL = sqlModule })
@@ -39,13 +39,15 @@ export default class Sql {
return this.db.exec(sql, params)
}
import (columns, values, progressCounterId, progressCallback, chunkSize = 1500) {
this.createDb()
this.db.exec(dbUtils.getCreateStatement(columns, values))
import (tabName, columns, values, 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)
let count = 0
const insertStr = dbUtils.getInsertStmt(columns)
const insertStr = dbUtils.getInsertStmt(tabName, columns)
const insertStmt = this.db.prepare(insertStr)
progressCallback({ progress: 0, id: progressCounterId })

View File

@@ -0,0 +1,90 @@
import sqliteParser from 'sqlite-parser'
export default {
* generateChunks (arr, size) {
const count = Math.ceil(arr.length / size)
for (let i = 0; i <= count - 1; i++) {
const start = size * i
const end = start + size
yield arr.slice(start, end)
}
},
getInsertStmt (tabName, columns) {
const colList = `"${columns.join('", "')}"`
const params = columns.map(() => '?').join(', ')
return `INSERT INTO "${tabName}" (${colList}) VALUES (${params});`
},
getCreateStatement (tabName, columns, values) {
let result = `CREATE table "${tabName}"(`
columns.forEach((col, index) => {
// Get the first row of values to determine types
const value = values[0][index]
let type = ''
switch (typeof value) {
case 'number': {
type = 'REAL'
break
}
case 'boolean': {
type = 'INTEGER'
break
}
case 'string': {
type = 'TEXT'
break
}
default: type = 'TEXT'
}
result += `"${col}" ${type}, `
})
result = result.replace(/,\s$/, ');')
return result
},
getAst (sql) {
// There is a bug is sqlite-parser
// It throws an error if tokenizer has an arguments:
// https://github.com/codeschool/sqlite-parser/issues/59
const fixedSql = sql
.replace(/(tokenize=[^,]+)"tokenchars=.+?"/, '$1')
.replace(/(tokenize=[^,]+)"remove_diacritics=.+?"/, '$1')
.replace(/(tokenize=[^,]+)"separators=.+?"/, '$1')
.replace(/tokenize=.+?(,|\))/, 'tokenize=unicode61$1')
return sqliteParser(fixedSql)
},
/*
* Return an array of columns with name and type. E.g.:
* [
* { name: 'id', type: 'INTEGER' },
* { name: 'title', type: 'NVARCHAR(30)' },
* ]
*/
getColumns (sql) {
const columns = []
const ast = this.getAst(sql)
const columnDefinition = ast.statement[0].format === 'table'
? ast.statement[0].definition
: ast.statement[0].result.args.expression // virtual table
columnDefinition.forEach(item => {
if (item.variant === 'column' && ['identifier', 'definition'].includes(item.type)) {
let type = item.datatype ? item.datatype.variant : 'N/A'
if (item.datatype && item.datatype.args) {
type = type + '(' + item.datatype.args.expression[0].value
if (item.datatype.args.expression.length === 2) {
type = type + ', ' + item.datatype.args.expression[1].value
}
type = type + ')'
}
columns.push({ name: item.name, type: type })
}
})
return columns
}
}

View File

@@ -1,5 +1,5 @@
import registerPromiseWorker from 'promise-worker/register'
import Sql from '@/sql'
import Sql from './_sql'
const sqlReady = Sql.build()
@@ -8,10 +8,18 @@ function processMsg (sql) {
switch (data && data.action) {
case 'open':
return sql.open(data.buffer)
case 'reopen':
return sql.open(sql.export())
case 'exec':
return sql.exec(data.sql, data.params)
case 'import':
return sql.import(data.columns, data.values, data.progressCounterId, postMessage)
return sql.import(
data.tabName,
data.columns,
data.values,
data.progressCounterId,
postMessage
)
case 'export':
return sql.export()
case 'close':

View File

@@ -1,8 +1,8 @@
import sqliteParser from 'sqlite-parser'
import fu from '@/file.utils'
import stms from './_statements'
import fu from '@/lib/utils/fileIo'
// We can import workers like so because of worker-loader:
// https://webpack.js.org/loaders/worker-loader/
import Worker from '@/db.worker.js'
import Worker from './_worker.js'
// Use promise-worker in order to turn worker into the promise based one:
// https://github.com/nolanlawson/promise-worker
@@ -20,6 +20,8 @@ export default {
let progressCounterIds = 0
class Database {
constructor (worker) {
this.dbName = null
this.schema = null
this.worker = worker
this.pw = new PromiseWorker(worker)
@@ -50,33 +52,35 @@ class Database {
delete this.importProgresses[id]
}
async createDb (name, data, progressCounterId) {
async addTableFromCsv (tabName, data, progressCounterId) {
const result = await this.pw.postMessage({
action: 'import',
columns: data.columns,
values: data.values,
progressCounterId
progressCounterId,
tabName
})
if (result.error) {
throw new Error(result.error)
}
return await this.getSchema(name)
this.dbName = this.dbName || 'database'
this.refreshSchema()
}
async loadDb (file) {
const fileContent = await fu.readAsArrayBuffer(file)
const fileContent = file ? await fu.readAsArrayBuffer(file) : null
const res = await this.pw.postMessage({ action: 'open', buffer: fileContent })
if (res.error) {
throw new Error(res.error)
}
return this.getSchema(file.name.replace(/\.[^.]+$/, ''))
this.dbName = file ? fu.getFileName(file) : 'database'
this.refreshSchema()
}
async getSchema (name) {
async refreshSchema () {
const getSchemaSql = `
SELECT name, sql
FROM sqlite_master
@@ -85,21 +89,21 @@ class Database {
const result = await this.execute(getSchemaSql)
// Parse DDL statements to get column names and types
const parsedSchema = []
result.values.forEach(item => {
parsedSchema.push({
name: item[0],
columns: getColumns(item[1])
if (result && result.values) {
result.values.forEach(item => {
parsedSchema.push({
name: item[0],
columns: stms.getColumns(item[1])
})
})
})
// Return db name and schema
return {
dbName: name,
schema: parsedSchema
}
// Refresh schema
this.schema = parsedSchema
}
async execute (commands) {
await this.pw.postMessage({ action: 'reopen' })
const results = await this.pw.postMessage({ action: 'exec', sql: commands })
if (results.error) {
@@ -117,48 +121,27 @@ class Database {
}
fu.exportToFile(data, fileName)
}
}
function getAst (sql) {
// There is a bug is sqlite-parser
// It throws an error if tokenizer has an arguments:
// https://github.com/codeschool/sqlite-parser/issues/59
const fixedSql = sql
.replace(/(?<=tokenize=.+)"tokenchars=.+"/, '')
.replace(/(?<=tokenize=.+)"remove_diacritics=.+"/, '')
.replace(/(?<=tokenize=.+)"separators=.+"/, '')
.replace(/tokenize=.+(?=(,|\)))/, 'tokenize=unicode61')
return sqliteParser(fixedSql)
}
/*
* Return an array of columns with name and type. E.g.:
* [
* { name: 'id', type: 'INTEGER' },
* { name: 'title', type: 'NVARCHAR(30)' },
* ]
*/
function getColumns (sql) {
const columns = []
const ast = getAst(sql)
const columnDefinition = ast.statement[0].format === 'table'
? ast.statement[0].definition
: ast.statement[0].result.args.expression // virtual table
columnDefinition.forEach(item => {
if (item.variant === 'column' && ['identifier', 'definition'].includes(item.type)) {
let type = item.datatype ? item.datatype.variant : 'N/A'
if (item.datatype && item.datatype.args) {
type = type + '(' + item.datatype.args.expression[0].value
if (item.datatype.args.expression.length === 2) {
type = type + ', ' + item.datatype.args.expression[1].value
}
type = type + ')'
}
columns.push({ name: item.name, type: type })
async validateTableName (name) {
if (name.startsWith('sqlite_')) {
throw new Error("Table name can't start with sqlite_")
}
})
return columns
if (/[^\w]/.test(name)) {
throw new Error('Table name can contain only letters, digits and underscores')
}
if (/^(\d)/.test(name)) {
throw new Error("Table name can't start with a digit")
}
await this.execute(`BEGIN; CREATE TABLE "${name}"(id); ROLLBACK;`)
}
sanitizeTableName (tabName) {
return tabName
.replace(/[^\w]/g, '_') // replace everything that is not letter, digit or _ with _
.replace(/^(\d)/, '_$1') // add _ at beginning if starts with digit
.replace(/_{2,}/g, '_') // replace multiple _ with one _
}
}

View File

@@ -1,5 +1,5 @@
import { nanoid } from 'nanoid'
import fu from '@/file.utils'
import fu from '@/lib/utils/fileIo'
export default {
getStoredQueries () {

View File

@@ -1,4 +1,15 @@
export default {
isDatabase (file) {
const dbTypes = ['application/vnd.sqlite3', 'application/x-sqlite3']
return file.type
? dbTypes.includes(file.type)
: /\.(db|sqlite(3)?)+$/.test(file.name)
},
getFileName (file) {
return file.name.replace(/\.[^.]+$/, '')
},
exportToFile (str, fileName, type = 'octet/stream') {
// Create downloader
const downloader = document.createElement('a')

15
src/lib/utils/time.js Normal file
View File

@@ -0,0 +1,15 @@
export default {
getPeriod (start, end) {
const diff = end.getTime() - start.getTime()
const seconds = diff / 1000
return seconds.toFixed(3) + 's'
},
debounce (func, ms) {
let timeout
return function () {
clearTimeout(timeout)
timeout = setTimeout(() => func.apply(this, arguments), ms)
}
}
}

View File

@@ -1,7 +1,7 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import App from '@/App.vue'
import router from '@/router'
import store from '@/store'
import { VuePlugin } from 'vuera'
import VModal from 'vue-js-modal'
@@ -13,7 +13,7 @@ import '@/assets/styles/tooltips.css'
import '@/assets/styles/messages.css'
if (!['localhost', '127.0.0.1'].includes(location.hostname)) {
import('../registerServiceWorker') // eslint-disable-line no-unused-expressions
import('./registerServiceWorker') // eslint-disable-line no-unused-expressions
}
Vue.use(VuePlugin)

50
src/router.js Normal file
View File

@@ -0,0 +1,50 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Editor from '@/views/Main/Editor'
import MyQueries from '@/views/Main/MyQueries'
import Welcome from '@/views/Welcome'
import Main from '@/views/Main'
import store from '@/store'
import database from '@/lib/database'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Welcome',
component: Welcome
},
{
path: '/',
name: 'Main',
component: Main,
children: [
{
path: '/editor',
name: 'Editor',
component: Editor
},
{
path: '/my-queries',
name: 'MyQueries',
component: MyQueries
}
]
}
]
const router = new VueRouter({
routes
})
router.beforeEach(async (to, from, next) => {
if (!store.state.db) {
const newDb = database.getNewDatabase()
await newDb.loadDb()
store.commit('setDb', newDb)
}
next()
})
export default router

View File

@@ -1,39 +0,0 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Editor from '@/views/Editor'
import MyQueries from '@/views/MyQueries'
import Home from '@/views/Home'
import MainView from '@/views/MainView'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Welcome',
component: Home
},
{
path: '/',
name: 'MainView',
component: MainView,
children: [
{
path: '/editor',
name: 'Editor',
component: Editor
},
{
path: '/my-queries',
name: 'MyQueries',
component: MyQueries
}
]
}
]
const router = new VueRouter({
routes
})
export default router

30
src/store/actions.js Normal file
View File

@@ -0,0 +1,30 @@
import { nanoid } from 'nanoid'
export default {
async addTab ({ state }, data) {
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
if (!data || !data.id) {
tab.id = nanoid()
tab.name = null
tab.tempName = state.untitledLastIndex
? `Untitled ${state.untitledLastIndex}`
: 'Untitled'
tab.isUnsaved = true
} else {
tab.isUnsaved = false
}
// add new tab only if was not already opened
if (!state.tabs.some(openedTab => openedTab.id === tab.id)) {
state.tabs.push(tab)
if (!tab.name) {
state.untitledLastIndex += 1
}
}
return tab.id
}
}

View File

@@ -1,112 +1,11 @@
import Vue from 'vue'
import Vuex from 'vuex'
import { nanoid } from 'nanoid'
import state from '@/store/state'
import mutations from '@/store/mutations'
import actions from '@/store/actions'
Vue.use(Vuex)
export const state = {
schema: null,
dbFile: null,
dbName: null,
tabs: [],
currentTab: null,
currentTabId: null,
untitledLastIndex: 0,
predefinedQueries: [],
db: null
}
export const mutations = {
setDb (state, db) {
if (state.db) {
state.db.shutDown()
}
state.db = db
},
saveSchema (state, { dbName, schema }) {
state.dbName = dbName
state.schema = schema
},
updateTab (state, { index, name, id, query, chart, isUnsaved }) {
const tab = state.tabs[index]
const oldId = tab.id
if (id && state.currentTabId === oldId) {
state.currentTabId = id
}
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
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) {
if (index < state.tabs.length - 1) {
state.currentTabId = state.tabs[index + 1].id
} else if (index > 0) {
state.currentTabId = state.tabs[index - 1].id
} else {
state.currentTabId = null
state.currentTab = null
state.untitledLastIndex = 0
}
}
state.tabs.splice(index, 1)
},
setCurrentTabId (state, id) {
state.currentTabId = id
},
setCurrentTab (state, tab) {
state.currentTab = tab
},
updatePredefinedQueries (state, queries) {
if (Array.isArray(queries)) {
state.predefinedQueries = queries
} else {
state.predefinedQueries = [queries]
}
}
}
export const actions = {
async addTab ({ state }, data) {
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
if (!data || !data.id) {
tab.id = nanoid()
tab.name = null
tab.tempName = state.untitledLastIndex
? `Untitled ${state.untitledLastIndex}`
: 'Untitled'
tab.isUnsaved = true
} else {
tab.isUnsaved = false
}
// add new tab only if was not already opened
if (!state.tabs.some(openedTab => openedTab.id === tab.id)) {
state.tabs.push(tab)
if (!tab.name) {
state.untitledLastIndex += 1
}
}
return tab.id
}
}
export default new Vuex.Store({
state,
mutations,

59
src/store/mutations.js Normal file
View File

@@ -0,0 +1,59 @@
import Vue from 'vue'
export default {
setDb (state, db) {
if (state.db) {
state.db.shutDown()
}
state.db = db
},
updateTab (state, { index, name, id, query, chart, isUnsaved }) {
const tab = state.tabs[index]
const oldId = tab.id
if (id && state.currentTabId === oldId) {
state.currentTabId = id
}
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
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) {
if (index < state.tabs.length - 1) {
state.currentTabId = state.tabs[index + 1].id
} else if (index > 0) {
state.currentTabId = state.tabs[index - 1].id
} else {
state.currentTabId = null
state.currentTab = null
state.untitledLastIndex = 0
}
}
state.tabs.splice(index, 1)
},
setCurrentTabId (state, id) {
state.currentTabId = id
},
setCurrentTab (state, tab) {
state.currentTab = tab
},
updatePredefinedQueries (state, queries) {
if (Array.isArray(queries)) {
state.predefinedQueries = queries
} else {
state.predefinedQueries = [queries]
}
}
}

8
src/store/state.js Normal file
View File

@@ -0,0 +1,8 @@
export default {
tabs: [],
currentTab: null,
currentTabId: null,
untitledLastIndex: 0,
predefinedQueries: [],
db: null
}

View File

@@ -1,36 +0,0 @@
export default {
getPeriod (start, end) {
let diff = end.getTime() - start.getTime()
let result = ''
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
diff -= days * (1000 * 60 * 60 * 24)
if (days) {
result += days + ' d '
}
const hours = Math.floor(diff / (1000 * 60 * 60))
diff -= hours * (1000 * 60 * 60)
if (hours) {
result += hours + ' h '
}
const mins = Math.floor(diff / (1000 * 60))
diff -= mins * (1000 * 60)
if (mins) {
result += mins + ' m '
}
const seconds = Math.floor(diff / (1000))
diff -= seconds * (1000)
if (seconds) {
result += seconds + ' s '
}
if (diff) {
result += diff + ' ms '
}
return result.replace(/\s$/, '')
}
}

View File

@@ -1,74 +0,0 @@
<template>
<div>
<splitpanes
class="schema-tabs-splitter"
:before="{ size: 20, max: 30 }"
:after="{ size: 80, max: 100 }"
>
<template #left-pane>
<schema v-if="$store.state.schema"/>
<div v-else id="empty-schema-container">
<div class="warning">
Database is not loaded. Queries cant be run without database.
</div>
<db-uploader id="db-uploader" width="100%"/>
</div>
</template>
<template #right-pane>
<tabs />
</template>
</splitpanes>
</div>
</template>
<script>
import Splitpanes from '@/components/Splitpanes'
import Schema from '@/components/Schema'
import Tabs from '@/components/Tabs'
import DbUploader from '@/components/DbUploader'
export default {
name: 'Editor',
components: {
Schema,
Splitpanes,
Tabs,
DbUploader
}
}
</script>
<style scoped>
.schema-tabs-splitter {
height: 100%;
background-color: var(--color-white);
}
#empty-schema-container {
display: flex;
flex-direction: column;
align-items: center;
min-width: 200px;
height: 100%;
}
#db-uploader {
flex-grow: 1;
padding: 24px;
width: 100%;
box-sizing: border-box;
}
.warning {
padding: 12px 24px;
width: 100%;
box-sizing: border-box;
}
>>>.drop-area {
padding: 0 15px;
}
>>>.drop-area .text {
max-width: 200px;
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<div id="app-info-container">
<img
id="app-info-icon"
:src="require('@/assets/images/info.svg')"
@click="$modal.show('app-info')"
/>
<modal name="app-info" classes="dialog" height="auto" width="400px">
<div class="dialog-header">
App info
<close-icon @click="$modal.hide('app-info')"/>
</div>
<div class="dialog-body">
<div v-for="(item, index) in info" :key="index" class="info-item">
{{item.name}}
<div class="divider"/>
<div class="options">
<div v-for="(opt, index) in item.info" :key="index">
{{opt}}
</div>
</div>
</div>
</div>
</modal>
</div>
</template>
<script>
import CloseIcon from '@/components/svg/close'
export default {
name: 'AppDiagnosticInfo',
components: { CloseIcon },
data () {
return {
info: [
{
name: 'sqliteviz version',
info: [require('../../../package.json').version]
}
]
}
},
async created () {
const state = this.$store.state
let result = await state.db.execute('select sqlite_version()')
this.info.push({
name: 'SQLite version',
info: result.values[0]
})
result = await state.db.execute('PRAGMA compile_options')
this.info.push({
name: 'SQLite compile options',
info: result.values.map(row => row[0])
})
}
}
</script>
<style scoped>
#app-info-icon {
cursor: pointer;
}
#app-info-container {
display: flex;
justify-content: center;
margin-left: 32px;
}
.divider {
height: 1px;
background-color: var(--color-border);
margin: 4px 0;
}
.options {
font-family: monospace;
font-size: 13px;
margin-left: 8px;
overflow: auto;
max-height: 170px;
}
.info-item {
margin-bottom: 32px;
font-size: 14px;
}
.info-item:last-child {
margin-bottom: 0;
}
</style>

View File

@@ -5,11 +5,12 @@
</div>
<div id="db">
<div @click="schemaVisible = !schemaVisible" class="db-name">
<tree-chevron :expanded="schemaVisible"/>
<tree-chevron v-show="schema.length > 0" :expanded="schemaVisible"/>
{{ dbName }}
</div>
<db-uploader id="db-edit" type="small" />
<export-icon tooltip="Export database" @click="exportToFile"/>
<add-table-icon @click="addCsv"/>
</div>
<div v-show="schemaVisible" class="schema">
<table-description
@@ -19,15 +20,26 @@
:columns="table.columns"
/>
</div>
<!--Parse csv dialog -->
<csv-import
ref="addCsv"
:file="file"
:db="$store.state.db"
dialog-name="addCsv"
/>
</div>
</template>
<script>
import TableDescription from '@/components/TableDescription'
import fIo from '@/lib/utils/fileIo'
import TableDescription from './TableDescription'
import TextField from '@/components/TextField'
import TreeChevron from '@/components/svg/treeChevron'
import DbUploader from '@/components/DbUploader'
import ExportIcon from '@/components/svg/export'
import AddTableIcon from '@/components/svg/addTable'
import CsvImport from '@/components/CsvImport'
export default {
name: 'Schema',
@@ -36,33 +48,44 @@ export default {
TextField,
TreeChevron,
DbUploader,
ExportIcon
ExportIcon,
AddTableIcon,
CsvImport
},
data () {
return {
schemaVisible: true,
filter: null
filter: null,
file: null
}
},
computed: {
schema () {
if (!this.$store.state.schema) {
if (!this.$store.state.db.schema) {
return []
}
return !this.filter
? this.$store.state.schema
: this.$store.state.schema.filter(
? this.$store.state.db.schema
: this.$store.state.db.schema.filter(
table => table.name.toUpperCase().indexOf(this.filter.toUpperCase()) !== -1
)
},
dbName () {
return this.$store.state.dbName
return this.$store.state.db.dbName
}
},
methods: {
exportToFile () {
this.$store.state.db.export(`${this.dbName}.sqlite`)
},
async addCsv () {
this.file = await fIo.getFileFromUser('.csv')
await this.$nextTick()
const csvImport = this.$refs.addCsv
csvImport.reset()
await csvImport.previewCsv()
csvImport.open()
}
}
}
@@ -86,7 +109,7 @@ export default {
width: 100%;
height: 100px;
box-sizing: border-box;
background-image: linear-gradient(white 73%, transparent);;
background-image: linear-gradient(white 73%, rgba(255, 255, 255, 0));
z-index: 2;
}
.schema, .db-name {

View File

@@ -28,7 +28,7 @@ import plotly from 'plotly.js/dist/plotly'
import 'react-chart-editor/lib/react-chart-editor.min.css'
import PlotlyEditor from 'react-chart-editor'
import chart from '@/chart'
import chartHelper from './chartHelper'
import dereference from 'react-chart-editor/lib/lib/dereference'
export default {
@@ -49,10 +49,10 @@ export default {
},
computed: {
dataSources () {
return chart.getDataSourcesFromSqlResult(this.sqlResult)
return chartHelper.getDataSourcesFromSqlResult(this.sqlResult)
},
dataSourceOptions () {
return chart.getOptionsFromDataSources(this.dataSources)
return chartHelper.getOptionsFromDataSources(this.dataSources)
}
},
watch: {
@@ -71,7 +71,7 @@ export default {
this.$emit('update')
},
getChartStateForSave () {
return chart.getChartStateForSave(this.state, this.dataSources)
return chartHelper.getChartStateForSave(this.state, this.dataSources)
}
}
}

View File

@@ -2,7 +2,6 @@ import CM from 'codemirror'
import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/hint/sql-hint.js'
import store from '@/store'
import { debounce } from 'debounce'
export function getHints (cm, options) {
const token = cm.getTokenAt(cm.getCursor()).string.toUpperCase()
@@ -18,28 +17,34 @@ export function getHints (cm, options) {
const hintOptions = {
get tables () {
const tables = {}
if (store.state.schema) {
store.state.schema.forEach(table => {
if (store.state.db.schema) {
store.state.db.schema.forEach(table => {
tables[table.name] = table.columns.map(column => column.name)
})
}
return tables
},
get defaultTable () {
const schema = store.state.db.schema
return schema && schema.length === 1 ? schema[0].name : null
},
completeSingle: false,
completeOnSingleClick: true,
alignWithWord: false
}
export default {
show: debounce(function (editor) {
// Don't show autocomplete after a space or semicolon or in string literals
const token = editor.getTokenAt(editor.getCursor())
const ch = token.string.slice(-1)
const tokenType = token.type
if (tokenType === 'string' || !ch || ch === ' ' || ch === ';') {
return
}
CM.showHint(editor, getHints, hintOptions)
}, 400)
export function showHintOnDemand (editor) {
CM.showHint(editor, getHints, hintOptions)
}
export default function showHint (editor) {
// Don't show autocomplete after a space or semicolon or in string literals
const token = editor.getTokenAt(editor.getCursor())
const ch = token.string.slice(-1)
const tokenType = token.type
if (tokenType === 'string' || !ch || ch === ' ' || ch === ';') {
return
}
CM.showHint(editor, getHints, hintOptions)
}

View File

@@ -1,11 +1,12 @@
<template>
<div class="codemirror-container">
<codemirror v-model="query" :options="cmOptions" @changes="onChange" />
<codemirror ref="cm" v-model="query" :options="cmOptions" @changes="onChange" />
</div>
</template>
<script>
import hint from '@/hint'
import showHint, { showHintOnDemand } from './hint'
import time from '@/lib/utils/time'
import { codemirror } from 'vue-codemirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/mode/sql/sql.js'
@@ -27,8 +28,8 @@ export default {
theme: 'neo',
lineNumbers: true,
line: true,
autofocus: true,
autoRefresh: true
autoRefresh: true,
extraKeys: { 'Ctrl-Space': showHintOnDemand }
}
}
},
@@ -38,7 +39,10 @@ export default {
}
},
methods: {
onChange: hint.show
onChange: time.debounce(showHint, 400),
focus () {
this.$refs.cm.codemirror.focus()
}
}
}
</script>

View File

@@ -8,7 +8,7 @@
>
<template #left-pane>
<div class="query-editor">
<sql-editor v-model="query" />
<sql-editor ref="sqlEditor" v-model="query" />
</div>
</template>
<template #right-pane>
@@ -21,7 +21,8 @@
>
Run your query and get results here
</div>
<div v-show="isGettingResults" class="table-preview result-in-progress">
<div v-if="isGettingResults" class="table-preview result-in-progress">
<loading-indicator :size="30"/>
Fetching results...
</div>
<div
@@ -30,10 +31,8 @@
>
No rows retrieved according to your query
</div>
<div v-show="error" class="table-preview error">
{{ error }}
</div>
<sql-table v-if="result" :data-set="result" :height="tableViewHeight" />
<logs v-if="error" :messages="[error]"/>
<sql-table v-if="result" :data-set="result" :time="time" :height="tableViewHeight" />
</div>
<chart
:visible="view === 'chart'"
@@ -50,10 +49,13 @@
<script>
import SqlTable from '@/components/SqlTable'
import SqlEditor from '@/components/SqlEditor'
import Splitpanes from '@/components/Splitpanes'
import ViewSwitcher from '@/components/ViewSwitcher'
import Chart from '@/components/Chart'
import LoadingIndicator from '@/components/LoadingIndicator'
import SqlEditor from './SqlEditor'
import ViewSwitcher from './ViewSwitcher'
import Chart from './Chart'
import Logs from '@/components/Logs'
import time from '@/lib/utils/time'
export default {
name: 'Tab',
@@ -63,7 +65,9 @@ export default {
SqlTable,
Splitpanes,
ViewSwitcher,
Chart
Chart,
LoadingIndicator,
Logs
},
data () {
return {
@@ -73,7 +77,8 @@ export default {
tableViewHeight: 0,
isGettingResults: false,
error: null,
resizeObserver: null
resizeObserver: null,
time: 0
}
},
computed: {
@@ -81,9 +86,6 @@ export default {
return this.id === this.$store.state.currentTabId
}
},
created () {
this.$store.commit('setCurrentTab', this)
},
mounted () {
this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.bottomPane)
@@ -93,9 +95,14 @@ export default {
this.resizeObserver.unobserve(this.$refs.bottomPane)
},
watch: {
isActive () {
if (this.isActive) {
this.$store.commit('setCurrentTab', this)
isActive: {
immediate: true,
async handler () {
if (this.isActive) {
this.$store.commit('setCurrentTab', this)
await this.$nextTick()
this.$refs.sqlEditor.focus()
}
}
},
query () {
@@ -110,12 +117,16 @@ export default {
this.error = null
const state = this.$store.state
try {
const start = new Date()
this.result = await state.db.execute(this.query + ';')
const schema = await state.db.getSchema(state.dbName)
this.$store.commit('saveSchema', schema)
this.time = time.getPeriod(start, new Date())
} catch (err) {
this.error = err
this.error = {
type: 'error',
message: err
}
}
state.db.refreshSchema()
this.isGettingResults = false
},
handleResize () {
@@ -131,12 +142,12 @@ export default {
calculateTableHeight () {
const bottomPane = this.$refs.bottomPane
// 88 - view swittcher height
// 42 - table footer width
// 30 - desirable space after the table
// 34 - table footer width
// 12 - desirable space after the table
// 5 - padding-bottom of rounded table container
// 40 - height of table header
const freeSpace = bottomPane.offsetHeight - 88 - 42 - 30 - 5 - 40
this.tableViewHeight = freeSpace - (freeSpace % 40)
// 35 - height of table header
const freeSpace = bottomPane.offsetHeight - 88 - 34 - 12 - 5 - 35
this.tableViewHeight = freeSpace - (freeSpace % 35)
}
}
}
@@ -183,11 +194,32 @@ export default {
font-size: 13px;
}
.table-preview.error {
color: var(--color-text-error);
.result-in-progress {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
will-change: opacity;
/*
We need to show loader in 1 sec after starting query execution. We can't do that with
setTimeout because the main thread can be busy by getting a result set from the web worker.
But we can use CSS animation for opacity. Opacity triggers changes only in the Composite Layer
stage in rendering waterfall. Hence it can be processed only with Compositor Thread while
the Main Thread processes a result set.
https://www.viget.com/articles/animation-performance-101-browser-under-the-hood/
*/
animation: show-loader 1s linear 0s 1;
}
.table-preview.error::first-letter {
text-transform: capitalize;
@keyframes show-loader {
0% {
opacity: 0;
}
99% {
opacity: 0;
}
100% {
opacity: 1;
}
}
</style>

View File

@@ -62,7 +62,7 @@
</template>
<script>
import Tab from '@/components/Tab'
import Tab from './Tab'
import CloseIcon from '@/components/svg/close'
export default {

View File

@@ -0,0 +1,62 @@
<template>
<div>
<splitpanes
class="schema-tabs-splitter"
:before="{ size: 20, max: 30 }"
:after="{ size: 80, max: 100 }"
>
<template #left-pane>
<schema/>
</template>
<template #right-pane>
<tabs />
</template>
</splitpanes>
</div>
</template>
<script>
import Splitpanes from '@/components/Splitpanes'
import Schema from './Schema'
import Tabs from './Tabs'
export default {
name: 'Editor',
components: {
Schema,
Splitpanes,
Tabs
},
async beforeCreate () {
const schema = this.$store.state.db.schema
if (!schema || schema.length === 0) {
const stmt = [
'/*',
' * Your database is empty. In order to start building charts',
' * you should create a table and insert data into it.',
' */',
'CREATE TABLE house',
'(',
' name TEXT,',
' points INTEGER',
');',
'INSERT INTO house VALUES',
"('Gryffindor', 100),",
"('Hufflepuff', 90),",
"('Ravenclaw', 95),",
"('Slytherin', 80);"
].join('\n')
const tabId = await this.$store.dispatch('addTab', { query: stmt })
this.$store.commit('setCurrentTabId', tabId)
}
}
}
</script>
<style scoped>
.schema-tabs-splitter {
height: 100%;
background-color: var(--color-white);
}
</style>

View File

@@ -3,8 +3,9 @@
<div>
<router-link to="/editor">Editor</router-link>
<router-link to="/my-queries">My queries</router-link>
<a href="https://github.com/lana-k/sqliteviz/wiki" target="_blank">Help</a>
</div>
<div>
<div id="nav-buttons">
<button
id="run-btn"
v-if="currentQuery && $route.path === '/editor'"
@@ -30,6 +31,7 @@
>
Create
</button>
<app-diagnostic-info />
</div>
<!--Save Query dialog -->
@@ -62,13 +64,15 @@
<script>
import TextField from '@/components/TextField'
import CloseIcon from '@/components/svg/close'
import storedQueries from '@/storedQueries'
import storedQueries from '@/lib/storedQueries'
import AppDiagnosticInfo from './AppDiagnosticInfo'
export default {
name: 'MainMenu',
components: {
TextField,
CloseIcon
CloseIcon,
AppDiagnosticInfo
},
data () {
return {
@@ -96,7 +100,7 @@ export default {
}
},
runDisabled () {
return this.currentQuery && (!this.$store.state.schema || !this.currentQuery.query)
return this.currentQuery && (!this.$store.state.db || !this.currentQuery.query)
}
},
created () {
@@ -212,7 +216,7 @@ nav {
top: 0;
left: 0;
width: 100vw;
padding: 0 52px;
padding: 0 16px 0 52px;
z-index: 999;
}
a {
@@ -237,4 +241,8 @@ button {
#save-note img {
margin: -3px 6px 0 0;
}
#nav-buttons {
display: flex;
}
</style>

View File

@@ -141,16 +141,16 @@
</template>
<script>
import RenameIcon from '@/components/svg/rename'
import CopyIcon from '@/components/svg/copy'
import RenameIcon from './svg/rename'
import CopyIcon from './svg/copy'
import ExportIcon from '@/components/svg/export'
import DeleteIcon from '@/components/svg/delete'
import DeleteIcon from './svg/delete'
import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField'
import CheckBox from '@/components/CheckBox'
import tooltipMixin from '@/mixins/tooltips'
import storedQueries from '@/storedQueries'
import fu from '@/file.utils'
import tooltipMixin from '@/tooltipMixin'
import storedQueries from '@/lib/storedQueries'
import fu from '@/lib/utils/fileIo'
export default {
name: 'MyQueries',
@@ -448,6 +448,7 @@ export default {
}
.rounded-bg {
padding-top: 40px;
margin: 0 auto;
max-width: 1500px;
width: 100%;

View File

@@ -23,7 +23,7 @@
</template>
<script>
import tooltipMixin from '@/mixins/tooltips'
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'CopyIcon',

View File

@@ -23,7 +23,7 @@
</template>
<script>
import tooltipMixin from '@/mixins/tooltips'
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'DeleteIcon',

View File

@@ -23,7 +23,7 @@
</template>
<script>
import tooltipMixin from '@/mixins/tooltips'
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'RenameIcon',

View File

@@ -8,11 +8,11 @@
</template>
<script>
import MainMenu from '@/components/MainMenu'
import MainMenu from './MainMenu'
import '@/assets/styles/scrollbars.css'
export default {
name: 'MainView',
name: 'Main',
components: { MainMenu }
}
</script>

View File

@@ -4,8 +4,8 @@
<div id="note">
Sqliteviz is fully client-side. Your database never leaves your computer.
</div>
<button id ="skip" class="secondary" @click="$router.push('/editor')">
Skip database loading
<button id="skip" class="secondary" @click="$router.push('/editor')">
Create empty database
</button>
</div>
</template>
@@ -14,7 +14,7 @@
import DbUploader from '@/components/DbUploader'
export default {
name: 'Home',
name: 'Welcome',
components: { DbUploader }
}
</script>

View File

@@ -0,0 +1,706 @@
import { expect } from 'chai'
import sinon from 'sinon'
import Vuex from 'vuex'
import { mount } from '@vue/test-utils'
import CsvImport from '@/components/CsvImport'
import csv from '@/components/CsvImport/csv'
describe('CsvImport.vue', () => {
let state = {}
let actions = {}
let mutations = {}
let store = {}
let clock
let wrapper
const newTabId = 1
const file = { name: 'my data.csv' }
beforeEach(() => {
clock = sinon.useFakeTimers()
// mock store state and mutations
state = {}
mutations = {
setDb: sinon.stub(),
setCurrentTabId: sinon.stub()
}
actions = {
addTab: sinon.stub().resolves(newTabId)
}
store = new Vuex.Store({ state, mutations, actions })
const db = {
sanitizeTableName: sinon.stub().returns('my_data'),
addTableFromCsv: sinon.stub().resolves(),
createProgressCounter: sinon.stub().returns(1),
deleteProgressCounter: sinon.stub(),
validateTableName: sinon.stub().resolves(),
execute: sinon.stub().resolves(),
refreshSchema: sinon.stub().resolves()
}
// mount the component
wrapper = mount(CsvImport, {
store,
propsData: {
file,
dialogName: 'addCsv',
db
}
})
})
afterEach(() => {
sinon.restore()
})
it('previews', async () => {
sinon.stub(csv, 'parse').resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo'],
[2, 'bar']
]
},
messages: [{
code: 'UndetectableDelimiter',
message: 'Comma was used as a standart delimiter',
row: 0,
type: 'info',
hint: undefined
}]
})
wrapper.vm.previewCsv()
await wrapper.vm.open()
await wrapper.vm.$nextTick()
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(true)
expect(wrapper.find('#csv-table-name input').element.value).to.equal('my_data')
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.value).to.equal('|')
expect(wrapper.find('#quote-char input').element.value).to.equal('"')
expect(wrapper.find('#escape-char input').element.value).to.equal('"')
expect(wrapper.findComponent({ name: 'check-box' }).vm.checked).to.equal(true)
const rows = wrapper.findAll('tbody tr')
expect(rows).to.have.lengthOf(2)
expect(rows.at(0).findAll('td').at(0).text()).to.equal('1')
expect(rows.at(0).findAll('td').at(1).text()).to.equal('foo')
expect(rows.at(1).findAll('td').at(0).text()).to.equal('2')
expect(rows.at(1).findAll('td').at(1).text()).to.equal('bar')
expect(wrapper.findComponent({ name: 'logs' }).text())
.to.include('Information about row 0. Comma was used as a standart delimiter.')
expect(wrapper.findComponent({ name: 'logs' }).text())
.to.include('Preview parsing is completed in')
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
})
it('reparses when parameters changes', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
}
})
wrapper.vm.previewCsv()
wrapper.vm.open()
await csv.parse.returnValues[0]
await wrapper.vm.$nextTick()
parse.onCall(1).resolves({
delimiter: ',',
data: {
columns: ['col1', 'col2'],
values: [
[2, 'bar']
]
},
hasErrors: false
})
await wrapper.find('.delimiter-selector-container input').setValue(',')
expect(parse.callCount).to.equal(2)
await csv.parse.returnValues[1]
let rows = wrapper.findAll('tbody tr')
expect(rows).to.have.lengthOf(1)
expect(rows.at(0).findAll('td').at(0).text()).to.equal('2')
expect(rows.at(0).findAll('td').at(1).text()).to.equal('bar')
expect(wrapper.findComponent({ name: 'logs' }).text())
.to.include('Preview parsing is completed in')
parse.onCall(2).resolves({
delimiter: ',',
data: {
columns: ['col1', 'col2'],
values: [
[3, 'baz']
]
},
hasErrors: true,
messages: [{
code: 'MissingQuotes',
message: 'Quote is missed',
row: 0,
type: 'error',
hint: 'Edit your CSV so that the field has a closing quote char.'
}]
})
await wrapper.find('#quote-char input').setValue("'")
expect(parse.callCount).to.equal(3)
await csv.parse.returnValues[2]
rows = wrapper.findAll('tbody tr')
expect(rows).to.have.lengthOf(1)
expect(rows.at(0).findAll('td').at(0).text()).to.equal('3')
expect(rows.at(0).findAll('td').at(1).text()).to.equal('baz')
expect(wrapper.findComponent({ name: 'logs' }).text())
.to.contain('Error in row 0. Quote is missed. Edit your CSV so that the field has a closing quote char.')
expect(wrapper.findComponent({ name: 'logs' }).text())
.to.not.contain('Preview parsing is completed in')
parse.onCall(3).resolves({
delimiter: ',',
data: {
columns: ['col1', 'col2'],
values: [
[4, 'qux']
]
},
hasErrors: false
})
await wrapper.find('#escape-char input').setValue("'")
expect(parse.callCount).to.equal(4)
await csv.parse.returnValues[3]
rows = wrapper.findAll('tbody tr')
expect(rows).to.have.lengthOf(1)
expect(rows.at(0).findAll('td').at(0).text()).to.equal('4')
expect(rows.at(0).findAll('td').at(1).text()).to.equal('qux')
expect(wrapper.findComponent({ name: 'logs' }).text())
.to.contain('Preview parsing is completed in')
parse.onCall(4).resolves({
delimiter: ',',
data: {
columns: ['col1', 'col2'],
values: [
[5, 'corge']
]
},
hasErrors: false
})
await wrapper.findComponent({ name: 'check-box' }).trigger('click')
expect(parse.callCount).to.equal(5)
await csv.parse.returnValues[4]
rows = wrapper.findAll('tbody tr')
expect(rows).to.have.lengthOf(1)
expect(rows.at(0).findAll('td').at(0).text()).to.equal('5')
expect(rows.at(0).findAll('td').at(1).text()).to.equal('corge')
expect(wrapper.findComponent({ name: 'logs' }).text())
.to.include('Preview parsing is completed in')
})
it('has proper state before parsing is complete', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
}
})
wrapper.vm.previewCsv()
wrapper.vm.open()
await wrapper.vm.$nextTick()
let resolveParsing
parse.onCall(1).returns(new Promise(resolve => {
resolveParsing = resolve
}))
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click')
await wrapper.vm.$nextTick()
// "Parsing CSV..." in the logs
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text())
.to.equal('Parsing CSV...')
// After 1 second - loading indicator is shown
await clock.tick(1000)
expect(
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
).to.equal(true)
// All the dialog controls are disabled
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
await resolveParsing()
await parse.returnValues[1]
// Loading indicator is not shown when parsing is compete
expect(
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
).to.equal(false)
})
it('parsing is completed successfully', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
parse.onCall(1).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo'],
[2, 'bar']
]
},
hasErrors: false,
messages: []
})
wrapper.vm.previewCsv()
wrapper.vm.open()
await wrapper.vm.$nextTick()
let resolveImport
wrapper.vm.db.addTableFromCsv.onCall(0).returns(new Promise(resolve => {
resolveImport = resolve
}))
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
// Parsing success in the logs
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text())
.to.include('2 rows are parsed successfully in')
// All the dialog controls are disabled
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
await resolveImport()
})
it('parsing is completed with notes', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
parse.onCall(1).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo'],
[2, 'bar']
]
},
hasErrors: false,
messages: [{
code: 'UndetectableDelimiter',
message: 'Comma was used as a standart delimiter',
type: 'info',
hint: undefined
}]
})
let resolveImport
wrapper.vm.db.addTableFromCsv.onCall(0).returns(new Promise(resolve => {
resolveImport = resolve
}))
wrapper.vm.previewCsv()
wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
// Parsing success in the logs
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
expect(logs).to.have.lengthOf(4)
expect(logs.at(1).text()).to.include('2 rows are parsed in')
expect(logs.at(2).text()).to.equals('Comma was used as a standart delimiter.')
// All the dialog controls are disabled
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
await resolveImport()
})
it('parsing is completed with errors', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
parse.onCall(1).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo'],
[2, 'bar']
]
},
hasErrors: true,
messages: [{
code: 'Error',
message: 'Something is wrong',
type: 'error',
hint: undefined
}]
})
wrapper.vm.previewCsv()
wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
// Parsing success in the logs
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
expect(logs).to.have.lengthOf(3)
expect(logs.at(1).text()).to.include('Parsing ended with errors.')
expect(logs.at(2).text()).to.equals('Something is wrong.')
// All the dialog controls are enabled
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(false)
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
})
it('has proper state before import is completed', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
parse.onCall(1).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo'],
[2, 'bar']
]
},
hasErrors: false,
messages: []
})
let resolveImport = sinon.stub()
wrapper.vm.db.addTableFromCsv = sinon.stub()
.resolves(new Promise(resolve => { resolveImport = resolve }))
wrapper.vm.previewCsv()
wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
// Parsing success in the logs
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(2).text())
.to.equal('Importing CSV into a SQLite database...')
// After 1 second - loading indicator is shown
await clock.tick(1000)
expect(
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
).to.equal(true)
// All the dialog controls are disabled
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
expect(wrapper.vm.db.addTableFromCsv.getCall(0).args[0]).to.equal('foo') // table name
// After resolving - loading indicator is not shown
await resolveImport()
await wrapper.vm.db.addTableFromCsv.returnValues[0]
expect(
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
).to.equal(false)
})
it('import success', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
// we need to separate calles because messages will mutate
parse.onCall(1).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo'],
[2, 'bar']
]
},
hasErrors: false,
messages: []
})
wrapper.vm.previewCsv()
wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
// Import success in the logs
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
expect(logs).to.have.lengthOf(3)
expect(logs.at(2).text()).to.contain('Importing CSV into a SQLite database is completed in')
// All the dialog controls are enabled
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(false)
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(true)
})
it('import fails', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
// we need to separate calles because messages will mutate
parse.onCall(1).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo'],
[2, 'bar']
]
},
hasErrors: false,
messages: []
})
wrapper.vm.db.addTableFromCsv = sinon.stub().rejects(new Error('fail'))
wrapper.vm.previewCsv()
wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
// Import success in the logs
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
expect(logs).to.have.lengthOf(4)
expect(logs.at(2).text()).to.contain('Importing CSV into a SQLite database...')
expect(logs.at(3).text()).to.equal('Error: fail.')
// All the dialog controls are enabled
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(false)
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
})
it('import finish', async () => {
sinon.stub(csv, 'parse').resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
wrapper.vm.previewCsv()
wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-import').trigger('click')
await wrapper.vm.$nextTick()
await wrapper.find('#csv-finish').trigger('click')
expect(actions.addTab.calledOnce).to.equal(true)
await actions.addTab.returnValues[0]
expect(mutations.setCurrentTabId.calledOnceWith(state, newTabId)).to.equal(true)
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
expect(wrapper.emitted('finish')).to.have.lengthOf(1)
})
it('import cancel', async () => {
sinon.stub(csv, 'parse').resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
await wrapper.vm.previewCsv()
await wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-import').trigger('click')
await wrapper.vm.$nextTick()
await wrapper.find('#csv-cancel').trigger('click')
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
expect(wrapper.vm.db.execute.calledOnceWith('DROP TABLE "my_data"')).to.equal(true)
expect(wrapper.vm.db.refreshSchema.calledOnce).to.equal(true)
expect(wrapper.emitted('cancel')).to.have.lengthOf(1)
})
it('checks table name', async () => {
sinon.stub(csv, 'parse').resolves()
await wrapper.vm.previewCsv()
await wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-table-name input').setValue('foo')
await clock.tick(400)
await wrapper.vm.$nextTick()
expect(wrapper.find('#csv-table-name .text-field-error').text()).to.equal('')
wrapper.vm.db.validateTableName = sinon.stub().rejects(new Error('this is a bad table name'))
await wrapper.find('#csv-table-name input').setValue('bar')
await clock.tick(400)
await wrapper.vm.$nextTick()
expect(wrapper.find('#csv-table-name .text-field-error').text())
.to.equal('this is a bad table name. Try another table name.')
await wrapper.find('#csv-table-name input').setValue('')
await clock.tick(400)
await wrapper.vm.$nextTick()
expect(wrapper.find('#csv-table-name .text-field-error').text()).to.equal('')
await wrapper.find('#csv-import').trigger('click')
expect(wrapper.find('#csv-table-name .text-field-error').text())
.to.equal("Table name can't be empty")
expect(wrapper.vm.db.addTableFromCsv.called).to.equal(false)
})
})

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { mount, shallowMount } from '@vue/test-utils'
import DelimiterSelector from '@/components/DelimiterSelector'
import DelimiterSelector from '@/components/CsvImport/DelimiterSelector'
describe('DelimiterSelector', async () => {
it('shows the name of value', async () => {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import sinon from 'sinon'
import csv from '@/csv'
import csv from '@/components/CsvImport/csv'
import Papa from 'papaparse'
describe('csv.js', () => {
@@ -11,11 +11,11 @@ describe('csv.js', () => {
it('getResult with fields', () => {
const source = {
data: [
{ id: 1, name: 'foo' },
{ id: 2, name: 'bar' }
{ id: 1, 'name ': 'foo' },
{ id: 2, 'name ': 'bar' }
],
meta: {
fields: ['id', 'name']
fields: ['id', 'name ']
}
}
expect(csv.getResult(source)).to.eql({

View File

@@ -2,39 +2,41 @@ import { expect } from 'chai'
import sinon from 'sinon'
import Vuex from 'vuex'
import { shallowMount, mount } from '@vue/test-utils'
import DbUploader from '@/components/DbUploader.vue'
import fu from '@/file.utils'
import database from '@/database'
import csv from '@/csv'
import DbUploader from '@/components/DbUploader'
import fu from '@/lib/utils/fileIo'
import database from '@/lib/database'
describe('DbUploader.vue', () => {
let state = {}
let mutations = {}
let store = {}
let place
beforeEach(() => {
// mock store state and mutations
state = {}
mutations = {
saveSchema: sinon.stub(),
setDb: sinon.stub()
}
store = new Vuex.Store({ state, mutations })
place = document.createElement('div')
document.body.appendChild(place)
})
afterEach(() => {
sinon.restore()
place.remove()
})
it('loads db on click and redirects to /editor', async () => {
// mock getting a file from user
const file = {}
const file = { name: 'test.db' }
sinon.stub(fu, 'getFileFromUser').resolves(file)
// mock db loading
const schema = {}
const db = {
loadDb: sinon.stub().resolves(schema)
loadDb: sinon.stub().resolves()
}
sinon.stub(database, 'getNewDatabase').returns(db)
@@ -44,22 +46,27 @@ describe('DbUploader.vue', () => {
// mount the component
const wrapper = shallowMount(DbUploader, {
attachTo: place,
store,
mocks: { $router, $route }
mocks: { $router, $route },
propsData: {
type: 'illustrated'
}
})
await wrapper.find('.drop-area').trigger('click')
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
await db.loadDb.returnValues[0]
expect(mutations.saveSchema.calledOnceWith(state, schema)).to.equal(true)
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
expect($router.push.calledOnceWith('/editor')).to.equal(true)
wrapper.destroy()
})
it('loads db on drop and redirects to /editor', async () => {
// mock db loading
const schema = {}
const db = {
loadDb: sinon.stub().resolves(schema)
loadDb: sinon.stub().resolves()
}
sinon.stub(database, 'getNewDatabase').returns(db)
@@ -69,12 +76,16 @@ describe('DbUploader.vue', () => {
// mount the component
const wrapper = shallowMount(DbUploader, {
attachTo: place,
store,
mocks: { $router, $route }
mocks: { $router, $route },
propsData: {
type: 'illustrated'
}
})
// mock a file dropped by a user
const file = {}
const file = { name: 'test.db' }
const dropData = { dataTransfer: new DataTransfer() }
Object.defineProperty(dropData.dataTransfer, 'files', {
value: [file],
@@ -84,19 +95,20 @@ describe('DbUploader.vue', () => {
await wrapper.find('.drop-area').trigger('drop', dropData)
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
await db.loadDb.returnValues[0]
expect(mutations.saveSchema.calledOnceWith(state, schema)).to.equal(true)
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
expect($router.push.calledOnceWith('/editor')).to.equal(true)
wrapper.destroy()
})
it("doesn't redirect if already on /editor", async () => {
// mock getting a file from user
const file = {}
const file = { name: 'test.db' }
sinon.stub(fu, 'getFileFromUser').resolves(file)
// mock db loading
const schema = {}
const db = {
loadDb: sinon.stub().resolves(schema)
loadDb: sinon.stub().resolves()
}
sinon.stub(database, 'getNewDatabase').returns(db)
@@ -106,736 +118,82 @@ describe('DbUploader.vue', () => {
// mount the component
const wrapper = shallowMount(DbUploader, {
attachTo: place,
store,
mocks: { $router, $route }
mocks: { $router, $route },
propsData: {
type: 'illustrated'
}
})
await wrapper.find('.drop-area').trigger('click')
await db.loadDb.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
expect($router.push.called).to.equal(false)
})
})
describe('DbUploader.vue import CSV', () => {
let state = {}
let mutations = {}
let actions = {}
const newTabId = 1
let store = {}
// mock router
const $router = { }
const $route = { path: '/' }
let clock
let wrapper
beforeEach(() => {
// mock getting a file from user
sinon.stub(fu, 'getFileFromUser').resolves({ type: 'text/csv', name: 'foo.csv' })
clock = sinon.useFakeTimers()
// mock store state and mutations
state = {}
mutations = {
saveSchema: sinon.stub(),
setDb: sinon.stub(),
setCurrentTabId: sinon.stub()
}
actions = {
addTab: sinon.stub().resolves(newTabId)
}
store = new Vuex.Store({ state, mutations, actions })
$router.push = sinon.stub()
// mount the component
wrapper = mount(DbUploader, {
store,
mocks: { $router, $route }
})
})
afterEach(() => {
sinon.restore()
wrapper.destroy()
})
it('shows parse dialog if gets csv file', async () => {
sinon.stub(csv, 'parse').resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo'],
[2, 'bar']
]
},
messages: [{
code: 'UndetectableDelimiter',
message: 'Comma was used as a standart delimiter',
row: 0,
type: 'info',
hint: undefined
}]
})
// mock getting a file from user
const file = { name: 'test.csv' }
sinon.stub(fu, 'getFileFromUser').resolves(file)
await wrapper.find('.drop-area').trigger('click')
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
expect(wrapper.find('[data-modal="parse"]').exists()).to.equal(true)
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.value).to.equal('|')
expect(wrapper.find('#quote-char input').element.value).to.equal('"')
expect(wrapper.find('#escape-char input').element.value).to.equal('"')
expect(wrapper.findComponent({ name: 'check-box' }).vm.checked).to.equal(true)
const rows = wrapper.findAll('tbody tr')
expect(rows).to.have.lengthOf(2)
expect(rows.at(0).findAll('td').at(0).text()).to.equal('1')
expect(rows.at(0).findAll('td').at(1).text()).to.equal('foo')
expect(rows.at(1).findAll('td').at(0).text()).to.equal('2')
expect(rows.at(1).findAll('td').at(1).text()).to.equal('bar')
expect(wrapper.findComponent({ name: 'logs' }).text())
.to.include('Information about row 0. Comma was used as a standart delimiter.')
expect(wrapper.findComponent({ name: 'logs' }).text())
.to.include('Preview parsing is completed in')
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
})
// mock router
const $router = { push: sinon.stub() }
const $route = { path: '/editor' }
it('reparses when parameters changes', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
// mount the component
const wrapper = mount(DbUploader, {
attachTo: place,
store,
mocks: { $router, $route },
propsData: {
type: 'illustrated'
}
})
const CsvImport = wrapper.vm.$refs.addCsv
sinon.stub(CsvImport, 'reset')
sinon.stub(CsvImport, 'previewCsv').resolves()
sinon.stub(CsvImport, 'open')
await wrapper.find('.drop-area').trigger('click')
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
parse.onCall(1).resolves({
delimiter: ',',
data: {
columns: ['col1', 'col2'],
values: [
[2, 'bar']
]
},
hasErrors: false
})
await wrapper.find('.delimiter-selector-container input').setValue(',')
expect(parse.callCount).to.equal(2)
await csv.parse.returnValues[1]
let rows = wrapper.findAll('tbody tr')
expect(rows).to.have.lengthOf(1)
expect(rows.at(0).findAll('td').at(0).text()).to.equal('2')
expect(rows.at(0).findAll('td').at(1).text()).to.equal('bar')
expect(wrapper.findComponent({ name: 'logs' }).text())
.to.include('Preview parsing is completed in')
parse.onCall(2).resolves({
delimiter: ',',
data: {
columns: ['col1', 'col2'],
values: [
[3, 'baz']
]
},
hasErrors: true,
messages: [{
code: 'MissingQuotes',
message: 'Quote is missed',
row: 0,
type: 'error',
hint: 'Edit your CSV so that the field has a closing quote char.'
}]
})
await wrapper.find('#quote-char input').setValue("'")
expect(parse.callCount).to.equal(3)
await csv.parse.returnValues[2]
rows = wrapper.findAll('tbody tr')
expect(rows).to.have.lengthOf(1)
expect(rows.at(0).findAll('td').at(0).text()).to.equal('3')
expect(rows.at(0).findAll('td').at(1).text()).to.equal('baz')
expect(wrapper.findComponent({ name: 'logs' }).text())
.to.contain('Error in row 0. Quote is missed. Edit your CSV so that the field has a closing quote char.')
expect(wrapper.findComponent({ name: 'logs' }).text())
.to.not.contain('Preview parsing is completed in')
parse.onCall(3).resolves({
delimiter: ',',
data: {
columns: ['col1', 'col2'],
values: [
[4, 'qux']
]
},
hasErrors: false
})
await wrapper.find('#escape-char input').setValue("'")
expect(parse.callCount).to.equal(4)
await csv.parse.returnValues[3]
rows = wrapper.findAll('tbody tr')
expect(rows).to.have.lengthOf(1)
expect(rows.at(0).findAll('td').at(0).text()).to.equal('4')
expect(rows.at(0).findAll('td').at(1).text()).to.equal('qux')
expect(wrapper.findComponent({ name: 'logs' }).text())
.to.contain('Preview parsing is completed in')
parse.onCall(4).resolves({
delimiter: ',',
data: {
columns: ['col1', 'col2'],
values: [
[5, 'corge']
]
},
hasErrors: false
})
await wrapper.findComponent({ name: 'check-box' }).trigger('click')
expect(parse.callCount).to.equal(5)
await csv.parse.returnValues[4]
rows = wrapper.findAll('tbody tr')
expect(rows).to.have.lengthOf(1)
expect(rows.at(0).findAll('td').at(0).text()).to.equal('5')
expect(rows.at(0).findAll('td').at(1).text()).to.equal('corge')
expect(wrapper.findComponent({ name: 'logs' }).text())
.to.include('Preview parsing is completed in')
expect(CsvImport.reset.calledOnce).to.equal(true)
await wrapper.vm.animationPromise
expect(CsvImport.previewCsv.calledOnce).to.equal(true)
await wrapper.vm.$nextTick()
expect(CsvImport.open.calledOnce).to.equal(true)
wrapper.destroy()
})
it('has proper state before parsing is complete', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
it('deletes temporary db if CSV import is canceled', async () => {
// mock getting a file from user
const file = { name: 'test.csv' }
sinon.stub(fu, 'getFileFromUser').resolves(file)
// mock router
const $router = { push: sinon.stub() }
const $route = { path: '/editor' }
// mount the component
const wrapper = mount(DbUploader, {
store,
mocks: { $router, $route },
propsData: {
type: 'illustrated'
}
})
await wrapper.find('.drop-area').trigger('click')
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
let resolveParsing
parse.onCall(1).returns(new Promise(resolve => {
resolveParsing = resolve
}))
await wrapper.find('#csv-import').trigger('click')
// "Parsing CSV..." in the logs
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text())
.to.equal('Parsing CSV...')
// After 1 second - loading indicator is shown
await clock.tick(1000)
expect(
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
).to.equal(true)
// All the dialog controls are disabled
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
await resolveParsing()
await parse.returnValues[1]
// Loading indicator is not shown when parsing is compete
expect(
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
).to.equal(false)
})
it('parsing is completed successfully', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
parse.onCall(1).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo'],
[2, 'bar']
]
},
hasErrors: false,
messages: []
})
const CsvImport = wrapper.vm.$refs.addCsv
sinon.stub(CsvImport, 'reset')
sinon.stub(CsvImport, 'previewCsv').resolves()
sinon.stub(CsvImport, 'open')
await wrapper.find('.drop-area').trigger('click')
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
// Parsing success in the logs
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text())
.to.include('2 rows are parsed successfully in')
// All the dialog controls are disabled
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
})
it('parsing is completed with notes', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
parse.onCall(1).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo'],
[2, 'bar']
]
},
hasErrors: false,
messages: [{
code: 'UndetectableDelimiter',
message: 'Comma was used as a standart delimiter',
type: 'info',
hint: undefined
}]
})
await wrapper.find('.drop-area').trigger('click')
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
// Parsing success in the logs
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
expect(logs).to.have.lengthOf(4)
expect(logs.at(1).text()).to.include('2 rows are parsed in')
expect(logs.at(2).text()).to.equals('Comma was used as a standart delimiter.')
// All the dialog controls are disabled
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
})
it('parsing is completed with errors', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
parse.onCall(1).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo'],
[2, 'bar']
]
},
hasErrors: true,
messages: [{
code: 'Error',
message: 'Something is wrong',
type: 'error',
hint: undefined
}]
})
await wrapper.find('.drop-area').trigger('click')
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
// Parsing success in the logs
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
expect(logs).to.have.lengthOf(3)
expect(logs.at(1).text()).to.include('Parsing ended with errors.')
expect(logs.at(2).text()).to.equals('Something is wrong.')
// All the dialog controls are enabled
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(false)
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
})
it('has proper state before import is completed', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
parse.onCall(1).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo'],
[2, 'bar']
]
},
hasErrors: false,
messages: []
})
let resolveImport = sinon.stub()
const newDb = {
createDb: sinon.stub().resolves(new Promise(resolve => { resolveImport = resolve })),
createProgressCounter: sinon.stub().returns(1),
deleteProgressCounter: sinon.stub()
}
sinon.stub(database, 'getNewDatabase').returns(newDb)
await wrapper.find('.drop-area').trigger('click')
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
// Parsing success in the logs
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(2).text())
.to.equal('Importing CSV into a SQLite database...')
// After 1 second - loading indicator is shown
await clock.tick(1000)
expect(
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
).to.equal(true)
// All the dialog controls are disabled
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
expect(newDb.createDb.getCall(0).args[0]).to.equal('foo') // file name
// After resolving - loading indicator is not shown
await resolveImport()
await newDb.createDb.returnValues[0]
expect(
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
).to.equal(false)
})
it('import success', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
parse.onCall(1).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo'],
[2, 'bar']
]
},
hasErrors: false,
messages: []
})
const schema = {}
const newDb = {
createDb: sinon.stub().resolves(schema),
createProgressCounter: sinon.stub().returns(1),
deleteProgressCounter: sinon.stub()
}
sinon.stub(database, 'getNewDatabase').returns(newDb)
await wrapper.find('.drop-area').trigger('click')
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
// Import success in the logs
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
expect(logs).to.have.lengthOf(3)
expect(logs.at(2).text()).to.contain('Importing CSV into a SQLite database is completed in')
// All the dialog controls are enabled
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(false)
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(true)
})
it('import fails', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
parse.onCall(1).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo'],
[2, 'bar']
]
},
hasErrors: false,
messages: []
})
const newDb = {
createDb: sinon.stub().rejects(new Error('fail')),
createProgressCounter: sinon.stub().returns(1),
deleteProgressCounter: sinon.stub()
}
sinon.stub(database, 'getNewDatabase').returns(newDb)
await wrapper.find('.drop-area').trigger('click')
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
// Import success in the logs
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
expect(logs).to.have.lengthOf(4)
expect(logs.at(2).text()).to.contain('Importing CSV into a SQLite database...')
expect(logs.at(3).text()).to.equal('Error: fail.')
// All the dialog controls are enabled
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(false)
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
})
it('import final', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
parse.onCall(1).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo'],
[2, 'bar']
]
},
hasErrors: false,
messages: []
})
const schema = {}
const newDb = {
createDb: sinon.stub().resolves(schema),
createProgressCounter: sinon.stub().returns(1),
deleteProgressCounter: sinon.stub()
}
sinon.stub(database, 'getNewDatabase').returns(newDb)
await wrapper.find('.drop-area').trigger('click')
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
await wrapper.find('#csv-finish').trigger('click')
expect(mutations.setDb.calledOnceWith(state, newDb)).to.equal(true)
expect(mutations.saveSchema.calledOnceWith(state, schema)).to.equal(true)
expect(actions.addTab.calledOnce).to.equal(true)
await actions.addTab.returnValues[0]
expect(mutations.setCurrentTabId.calledOnceWith(state, newTabId)).to.equal(true)
expect($router.push.calledOnceWith('/editor')).to.equal(true)
expect(wrapper.find('[data-modal="parse"]').exists()).to.equal(false)
})
it('import cancel', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
parse.onCall(1).resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo'],
[2, 'bar']
]
},
hasErrors: false,
messages: []
})
const schema = {}
const newDb = {
createDb: sinon.stub().resolves(schema),
createProgressCounter: sinon.stub().returns(1),
deleteProgressCounter: sinon.stub(),
shutDown: sinon.stub()
}
sinon.stub(database, 'getNewDatabase').returns(newDb)
await wrapper.find('.drop-area').trigger('click')
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
await wrapper.find('#csv-cancel').trigger('click')
expect(mutations.setDb.called).to.equal(false)
expect(mutations.saveSchema.called).to.equal(false)
expect(actions.addTab.called).to.equal(false)
expect(mutations.setCurrentTabId.called).to.equal(false)
expect($router.push.called).to.equal(false)
expect(newDb.shutDown.calledOnce).to.equal(true)
expect(wrapper.find('[data-modal="parse"]').exists()).to.equal(false)
await CsvImport.$emit('cancel')
expect(wrapper.vm.newDb).to.equal(null)
})
})

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import LoadingIndicator from '@/components/LoadingIndicator.vue'
import LoadingIndicator from '@/components/LoadingIndicator'
describe('LoadingIndicator.vue', () => {
it('Calculates animation class', async () => {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import Logs from '@/components/Logs.vue'
import Logs from '@/components/Logs'
let place
describe('Logs.vue', () => {

View File

@@ -1,115 +0,0 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { mount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import Schema from '@/components/Schema.vue'
import TableDescription from '@/components/TableDescription.vue'
const localVue = createLocalVue()
localVue.use(Vuex)
describe('Schema.vue', () => {
afterEach(() => {
sinon.restore()
})
it('Renders DB name on initial', () => {
// mock store state
const state = {
dbName: 'fooDB'
}
const store = new Vuex.Store({ state })
// mout the component
const wrapper = mount(Schema, { store, localVue })
// check DB name and schema visibility
expect(wrapper.find('.db-name').text()).to.equal('fooDB')
expect(wrapper.find('.schema').isVisible()).to.equal(true)
})
it('Schema visibility is toggled when click on DB name', async () => {
// mock store state
const state = {
dbName: 'fooDB'
}
const store = new Vuex.Store({ state })
// mout the component
const wrapper = mount(Schema, { store, localVue })
// click and check visibility
await wrapper.find('.db-name').trigger('click')
expect(wrapper.find('.schema').isVisible()).to.equal(false)
await wrapper.find('.db-name').trigger('click')
expect(wrapper.find('.schema').isVisible()).to.equal(true)
})
it('Schema filter', async () => {
// mock store state
const state = {
dbName: 'fooDB',
schema: [
{
name: 'foo',
columns: [
{ name: 'id', type: 'INTEGER' },
{ name: 'title', type: 'NVARCHAR(24)' }
]
},
{
name: 'bar',
columns: [
{ name: 'id', type: 'INTEGER' },
{ name: 'price', type: 'INTEGER' }
]
},
{
name: 'foobar',
columns: [
{ name: 'id', type: 'INTEGER' },
{ name: 'price', type: 'INTEGER' }
]
}
]
}
const store = new Vuex.Store({ state })
// mount the component
const wrapper = mount(Schema, { store, localVue })
// apply filters and check the list of tables
await wrapper.find('#schema-filter input').setValue('foo')
let tables = wrapper.findAllComponents(TableDescription)
expect(tables).to.have.lengthOf(2)
expect(tables.at(0).vm.name).to.equal('foo')
expect(tables.at(1).vm.name).to.equal('foobar')
await wrapper.find('#schema-filter input').setValue('bar')
tables = wrapper.findAllComponents(TableDescription)
expect(tables).to.have.lengthOf(2)
expect(tables.at(0).vm.name).to.equal('bar')
expect(tables.at(1).vm.name).to.equal('foobar')
await wrapper.find('#schema-filter input').setValue('')
tables = wrapper.findAllComponents(TableDescription)
expect(tables).to.have.lengthOf(3)
expect(tables.at(0).vm.name).to.equal('foo')
expect(tables.at(1).vm.name).to.equal('bar')
expect(tables.at(2).vm.name).to.equal('foobar')
})
it('exports db', async () => {
const state = {
dbName: 'fooDB',
db: {
export: sinon.stub().resolves()
}
}
const store = new Vuex.Store({ state })
const wrapper = mount(Schema, { store, localVue })
await wrapper.findComponent({ name: 'export-icon' }).trigger('click')
expect(state.db.export.calledOnceWith('fooDB'))
})
})

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import Splitpanes from '@/components/Splitpanes.vue'
import Splitpanes from '@/components/Splitpanes'
describe('Splitpanes.vue', () => {
it('renders correctly - vertical', () => {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import sinon from 'sinon'
import splitter from '@/splitter'
import splitter from '@/components/Splitpanes/splitter'
describe('splitter.js', () => {
afterEach(() => {

View File

@@ -1,7 +1,7 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { mount } from '@vue/test-utils'
import Pager from '@/components/Pager.vue'
import Pager from '@/components/SqlTable/Pager'
describe('Pager.vue', () => {
afterEach(() => {

View File

@@ -1,34 +0,0 @@
import { expect } from 'chai'
import dbUtils from '@/db.utils'
describe('db.utils.js', () => {
it('generateChunks', () => {
const arr = ['1', '2', '3', '4', '5']
const size = 2
const chunks = dbUtils.generateChunks(arr, size)
const output = []
for (const chunk of chunks) {
output.push(chunk)
}
expect(output[0]).to.eql(['1', '2'])
expect(output[1]).to.eql(['3', '4'])
expect(output[2]).to.eql(['5'])
})
it('getInsertStmt', () => {
const columns = ['id', 'name']
expect(dbUtils.getInsertStmt(columns))
.to.equal('INSERT INTO csv_import ("id", "name") VALUES (?, ?);')
})
it('getCreateStatement', () => {
const columns = ['id', 'name', 'isAdmin', 'startDate']
const values = [
[1, 'foo', true, new Date()],
[2, 'bar', false, new Date()]
]
expect(dbUtils.getCreateStatement(columns, values)).to.equal(
'CREATE table csv_import("id" REAL, "name" TEXT, "isAdmin" INTEGER, "startDate" TEXT);'
)
})
})

View File

@@ -2,14 +2,14 @@ import chai from 'chai'
import sinon from 'sinon'
import chaiAsPromised from 'chai-as-promised'
import initSqlJs from 'sql.js'
import Sql from '@/sql'
import Sql from '@/lib/database/_sql'
chai.use(chaiAsPromised)
const expect = chai.expect
chai.should()
const getSQL = initSqlJs()
describe('sql.js', () => {
describe('_sql.js', () => {
afterEach(() => {
sinon.restore()
})
@@ -74,8 +74,8 @@ describe('sql.js', () => {
const progressCallback = sinon.stub()
const progressCounterId = 1
const sql = await Sql.build()
sql.import(data.columns, data.values, progressCounterId, progressCallback, 2)
const result = sql.exec('SELECT * from csv_import')
sql.import('foo', data.columns, data.values, progressCounterId, progressCallback, 2)
const result = sql.exec('SELECT * from foo')
expect(result).to.have.lengthOf(1)
expect(result[0].columns).to.eql(['id', 'name'])
expect(result[0].values).to.have.lengthOf(4)
@@ -135,7 +135,7 @@ describe('sql.js', () => {
expect(sql.db.db).to.equal(null)
})
it('overwrites', async () => {
it('adds', async () => {
const sql = await Sql.build()
sql.exec(`
CREATE TABLE test (
@@ -160,12 +160,11 @@ describe('sql.js', () => {
[4, 'Ron Weasley']
]
}
// rewrite the database by import
sql.import(data.columns, data.values, 1, sinon.stub(), 2)
result = sql.exec('SELECT * from csv_import')
// import adds table
sql.import('foo', data.columns, data.values, 1, sinon.stub(), 2)
result = sql.exec('SELECT * from foo')
expect(result[0].values).to.have.lengthOf(4)
// test table oesn't exists anymore: the db was overwritten
expect(() => { sql.exec('SELECT * from test') }).to.throw('no such table: test')
result = sql.exec('SELECT * from test')
expect(result[0].values).to.have.lengthOf(2)
})
})

View File

@@ -0,0 +1,62 @@
import { expect } from 'chai'
import stmts from '@/lib/database/_statements'
describe('_statements.js', () => {
it('generateChunks', () => {
const arr = ['1', '2', '3', '4', '5']
const size = 2
const chunks = stmts.generateChunks(arr, size)
const output = []
for (const chunk of chunks) {
output.push(chunk)
}
expect(output[0]).to.eql(['1', '2'])
expect(output[1]).to.eql(['3', '4'])
expect(output[2]).to.eql(['5'])
})
it('getInsertStmt', () => {
const columns = ['id', 'name']
expect(stmts.getInsertStmt('foo', columns))
.to.equal('INSERT INTO "foo" ("id", "name") VALUES (?, ?);')
})
it('getCreateStatement', () => {
const columns = ['id', 'name', 'isAdmin', 'startDate']
const values = [
[1, 'foo', true, new Date()],
[2, 'bar', false, new Date()]
]
expect(stmts.getCreateStatement('foo', columns, values)).to.equal(
'CREATE table "foo"("id" REAL, "name" TEXT, "isAdmin" INTEGER, "startDate" TEXT);'
)
})
it('getColumns', () => {
const sql = `CREATE TABLE test (
col1,
col2 integer,
col3 decimal(5,2),
col4 varchar(30)
)`
expect(stmts.getColumns(sql)).to.eql([
{ name: 'col1', type: 'N/A' },
{ name: 'col2', type: 'integer' },
{ name: 'col3', type: 'decimal(5, 2)' },
{ name: 'col4', type: 'varchar(30)' }
])
})
it('getColumns with virtual table', async () => {
const sql = `
CREATE VIRTUAL TABLE test_virtual USING fts4(
col1, col2,
notindexed=col1, notindexed=col2,
tokenize=unicode61 "tokenchars=.+#")
`
expect(stmts.getColumns(sql)).to.eql([
{ name: 'col1', type: 'N/A' },
{ name: 'col2', type: 'N/A' }
])
})
})

View File

@@ -2,8 +2,8 @@ import chai from 'chai'
import sinon from 'sinon'
import chaiAsPromised from 'chai-as-promised'
import initSqlJs from 'sql.js'
import database from '@/database'
import fu from '@/file.utils'
import database from '@/lib/database'
import fu from '@/lib/utils/fileIo'
chai.use(chaiAsPromised)
const expect = chai.expect
@@ -27,58 +27,39 @@ describe('database.js', () => {
const tempDb = new SQL.Database()
tempDb.run(`CREATE TABLE test (
col1,
col2 integer,
col3 decimal(5,2),
col4 varchar(30)
col2 integer
)`)
const data = tempDb.export()
const buffer = new Blob([data])
buffer.name = 'foo.sqlite'
const { schema, dbName } = await db.loadDb(buffer)
expect(dbName).to.equal('foo')
sinon.spy(db, 'refreshSchema')
await db.loadDb(buffer)
await db.refreshSchema.returnValues[0]
const schema = db.schema
expect(db.dbName).to.equal('foo')
expect(schema).to.have.lengthOf(1)
expect(schema[0].name).to.equal('test')
expect(schema[0].columns[0].name).to.equal('col1')
expect(schema[0].columns[0].type).to.equal('N/A')
expect(schema[0].columns[1].name).to.equal('col2')
expect(schema[0].columns[1].type).to.equal('integer')
expect(schema[0].columns[2].name).to.equal('col3')
expect(schema[0].columns[2].type).to.equal('decimal(5, 2)')
expect(schema[0].columns[3].name).to.equal('col4')
expect(schema[0].columns[3].type).to.equal('varchar(30)')
})
it('creates schema with virtual table', async () => {
const SQL = await getSQL
const tempDb = new SQL.Database()
tempDb.run(`
CREATE VIRTUAL TABLE test_virtual USING fts4(
col1, col2,
notindexed=col1, notindexed=col2,
tokenize=unicode61 "tokenchars=.+#")
`)
it('creates empty db with name database', async () => {
sinon.spy(db, 'refreshSchema')
const data = tempDb.export()
const buffer = new Blob([data])
buffer.name = 'foo.sqlite'
const { schema } = await db.loadDb(buffer)
expect(schema[0].name).to.equal('test_virtual')
expect(schema[0].columns[0].name).to.equal('col1')
expect(schema[0].columns[0].type).to.equal('N/A')
expect(schema[0].columns[1].name).to.equal('col2')
expect(schema[0].columns[1].type).to.equal('N/A')
await db.loadDb()
await db.refreshSchema.returnValues[0]
expect(db.dbName).to.equal('database')
})
it('loadDb throws errors', async () => {
const SQL = await getSQL
const tempDb = new SQL.Database()
tempDb.run('CREATE TABLE test (col1, col2)')
const data = tempDb.export()
const buffer = new Blob([data])
const buffer = new Blob([])
buffer.name = 'foo.sqlite'
sinon.stub(db.pw, 'postMessage').resolves({ error: new Error('foo') })
@@ -136,7 +117,7 @@ describe('database.js', () => {
await expect(db.execute('SELECT * from foo')).to.be.rejectedWith(/^no such table: foo$/)
})
it('creates db', async () => {
it('adds table from csv', async () => {
const data = {
columns: ['id', 'name', 'faculty'],
values: [
@@ -146,16 +127,19 @@ describe('database.js', () => {
}
const progressHandler = sinon.spy()
const progressCounterId = db.createProgressCounter(progressHandler)
const { dbName, schema } = await db.createDb('foo', data, progressCounterId)
expect(dbName).to.equal('foo')
expect(schema).to.have.lengthOf(1)
expect(schema[0].name).to.equal('csv_import')
expect(schema[0].columns).to.have.lengthOf(3)
expect(schema[0].columns[0]).to.eql({ name: 'id', type: 'real' })
expect(schema[0].columns[1]).to.eql({ name: 'name', type: 'text' })
expect(schema[0].columns[2]).to.eql({ name: 'faculty', type: 'text' })
sinon.spy(db, 'refreshSchema')
const result = await db.execute('SELECT * from csv_import')
await db.addTableFromCsv('foo', data, progressCounterId)
await db.refreshSchema.returnValues[0]
expect(db.dbName).to.equal('database')
expect(db.schema).to.have.lengthOf(1)
expect(db.schema[0].name).to.equal('foo')
expect(db.schema[0].columns).to.have.lengthOf(3)
expect(db.schema[0].columns[0]).to.eql({ name: 'id', type: 'real' })
expect(db.schema[0].columns[1]).to.eql({ name: 'name', type: 'text' })
expect(db.schema[0].columns[2]).to.eql({ name: 'faculty', type: 'text' })
const result = await db.execute('SELECT * from foo')
expect(result.columns).to.eql(data.columns)
expect(result.values).to.eql(data.values)
@@ -164,7 +148,7 @@ describe('database.js', () => {
expect(progressHandler.secondCall.calledWith(100)).to.equal(true)
})
it('createDb throws errors', async () => {
it('addTableFromCsv throws errors', async () => {
const data = {
columns: ['id', 'name'],
values: [
@@ -174,7 +158,7 @@ describe('database.js', () => {
}
const progressHandler = sinon.stub()
const progressCounterId = db.createProgressCounter(progressHandler)
await expect(db.createDb('foo', data, progressCounterId))
await expect(db.addTableFromCsv('foo', data, progressCounterId))
.to.be.rejectedWith('column index out of range')
})
@@ -242,4 +226,23 @@ describe('database.js', () => {
expect(result.values).to.have.lengthOf(1)
expect(result.values[0]).to.eql([1, 'Harry Potter'])
})
it('sanitizeTableName', () => {
let name = 'foo[]bar'
expect(db.sanitizeTableName(name)).to.equal('foo_bar')
name = '1 foo(01.05.2020)'
expect(db.sanitizeTableName(name)).to.equal('_1_foo_01_05_2020_')
})
it('validateTableName', async () => {
await db.execute('CREATE TABLE foo(id)')
await expect(db.validateTableName('foo')).to.be.rejectedWith('table "foo" already exists')
await expect(db.validateTableName('1foo'))
.to.be.rejectedWith("Table name can't start with a digit")
await expect(db.validateTableName('foo(05.08.2020)'))
.to.be.rejectedWith('Table name can contain only letters, digits and underscores')
await expect(db.validateTableName('sqlite_foo'))
.to.be.rejectedWith("Table name can't start with sqlite_")
})
})

View File

@@ -1,7 +1,7 @@
import { expect } from 'chai'
import sinon from 'sinon'
import storedQueries from '@/storedQueries.js'
import fu from '@/file.utils'
import storedQueries from '@/lib/storedQueries'
import fu from '@/lib/utils/fileIo'
describe('storedQueries.js', () => {
beforeEach(() => {

View File

@@ -1,8 +1,8 @@
import { expect } from 'chai'
import fu from '@/file.utils'
import fIo from '@/lib/utils/fileIo'
import sinon from 'sinon'
describe('file.utils.js', () => {
describe('fileIo.js', () => {
afterEach(() => {
sinon.restore()
})
@@ -15,7 +15,7 @@ describe('file.utils.js', () => {
sinon.spy(URL, 'revokeObjectURL')
sinon.spy(window, 'Blob')
fu.exportToFile('foo', 'foo.txt')
fIo.exportToFile('foo', 'foo.txt')
expect(document.createElement.calledOnceWith('a')).to.equal(true)
@@ -40,7 +40,7 @@ describe('file.utils.js', () => {
sinon.spy(URL, 'revokeObjectURL')
sinon.spy(window, 'Blob')
fu.exportToFile('foo', 'foo.html', 'text/html')
fIo.exportToFile('foo', 'foo.html', 'text/html')
expect(document.createElement.calledOnceWith('a')).to.equal(true)
@@ -71,7 +71,7 @@ describe('file.utils.js', () => {
setTimeout(() => { spyInput.dispatchEvent(new Event('change')) })
const data = await fu.importFile()
const data = await fIo.importFile()
expect(data).to.equal('foo')
expect(document.createElement.calledOnceWith('input')).to.equal(true)
expect(spyInput.type).to.equal('file')
@@ -82,13 +82,13 @@ describe('file.utils.js', () => {
it('readFile', () => {
sinon.spy(window, 'fetch')
fu.readFile('./foo.bar')
fIo.readFile('./foo.bar')
expect(window.fetch.calledOnceWith('./foo.bar')).to.equal(true)
})
it('readAsArrayBuffer resolves', async () => {
const blob = new Blob(['foo'])
const buffer = await fu.readAsArrayBuffer(blob)
const buffer = await fIo.readAsArrayBuffer(blob)
const uint8Array = new Uint8Array(buffer)
const text = new TextDecoder().decode(uint8Array)
@@ -103,6 +103,34 @@ describe('file.utils.js', () => {
sinon.stub(window, 'FileReader').returns(r)
const blob = new Blob(['foo'])
await expect(fu.readAsArrayBuffer(blob)).to.be.rejectedWith('Problem parsing input file.')
await expect(fIo.readAsArrayBuffer(blob)).to.be.rejectedWith('Problem parsing input file.')
})
it('isDatabase', () => {
let file = { type: 'application/vnd.sqlite3' }
expect(fIo.isDatabase(file)).to.equal(true)
file = { type: 'application/x-sqlite3' }
expect(fIo.isDatabase(file)).to.equal(true)
file = { type: '', name: 'test.db' }
expect(fIo.isDatabase(file)).to.equal(true)
file = { type: '', name: 'test.sqlite' }
expect(fIo.isDatabase(file)).to.equal(true)
file = { type: '', name: 'test.sqlite3' }
expect(fIo.isDatabase(file)).to.equal(true)
file = { type: '', name: 'test.csv' }
expect(fIo.isDatabase(file)).to.equal(false)
file = { type: 'text', name: 'test.db' }
expect(fIo.isDatabase(file)).to.equal(false)
})
it('getFileName', () => {
expect(fIo.getFileName({ name: 'foo.csv' })).to.equal('foo')
expect(fIo.getFileName({ name: 'foo.bar.db' })).to.equal('foo.bar')
})
})

View File

@@ -1,23 +1,23 @@
import { expect } from 'chai'
import time from '@/time'
import time from '@/lib/utils/time'
describe('time.js', () => {
it('getPeriod', () => {
// 1.01.2021 13:00:00 000
let start = new Date(2021, 0, 1, 13, 0, 0, 0)
// 3.01.2021 22:15:20 500
let end = new Date(2021, 0, 3, 22, 15, 20, 500)
// 1.01.2021 13:01:00 500
let end = new Date(2021, 0, 1, 13, 1, 0, 500)
expect(time.getPeriod(start, end)).to.equal('2 d 9 h 15 m 20 s 500 ms')
expect(time.getPeriod(start, end)).to.equal('60.500s')
// 1.01.2021 13:00:00 000
start = new Date(2021, 0, 1, 13, 0, 0, 0)
// 1.01.2021 22:00:20 000
end = new Date(2021, 0, 1, 22, 0, 20, 0)
// 1.01.2021 13:00:20 500
end = new Date(2021, 0, 1, 13, 0, 20, 500)
expect(time.getPeriod(start, end)).to.equal('9 h 20 s')
expect(time.getPeriod(start, end)).to.equal('20.500s')
// 1.01.2021 13:00:00 000
start = new Date(2021, 0, 1, 13, 0, 0, 0)
@@ -25,6 +25,6 @@ describe('time.js', () => {
// 1.01.2021 13:00:00 45
end = new Date(2021, 0, 1, 13, 0, 0, 45)
expect(time.getPeriod(start, end)).to.equal('45 ms')
expect(time.getPeriod(start, end)).to.equal('0.045s')
})
})

View File

@@ -0,0 +1,67 @@
import { expect } from 'chai'
import actions from '@/store/actions'
const { addTab } = actions
describe('actions', () => {
it('addTab adds new blank tab', async () => {
const state = {
tabs: [],
untitledLastIndex: 0
}
const id = await addTab({ state })
expect(state.tabs[0].id).to.eql(id)
expect(state.tabs[0].name).to.eql(null)
expect(state.tabs[0].tempName).to.eql('Untitled')
expect(state.tabs[0].isUnsaved).to.eql(true)
expect(state.untitledLastIndex).to.equal(1)
})
it('addTab adds tab from saved queries', async () => {
const state = {
tabs: [],
untitledLastIndex: 0
}
const tab = {
id: 1,
name: 'test',
tempName: null,
query: 'SELECT * from foo',
chart: {},
isUnsaved: false
}
await addTab({ state }, tab)
expect(state.tabs[0]).to.eql(tab)
expect(state.untitledLastIndex).to.equal(0)
})
it("addTab doesn't add anything when the query is already opened", async () => {
const tab1 = {
id: 1,
name: 'test',
tempName: null,
query: 'SELECT * from foo',
chart: {},
isUnsaved: false
}
const tab2 = {
id: 2,
name: 'bar',
tempName: null,
query: 'SELECT * from bar',
chart: {},
isUnsaved: false
}
const state = {
tabs: [tab1, tab2],
untitledLastIndex: 0
}
await addTab({ state }, tab1)
expect(state.tabs).to.have.lengthOf(2)
expect(state.untitledLastIndex).to.equal(0)
})
})

View File

@@ -1,8 +1,7 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { mutations, actions } from '@/store'
import mutations from '@/store/mutations'
const {
saveSchema,
updateTab,
deleteTab,
setCurrentTabId,
@@ -11,8 +10,6 @@ const {
setDb
} = mutations
const { addTab } = actions
describe('mutations', () => {
it('setDb', () => {
const state = {
@@ -26,25 +23,6 @@ describe('mutations', () => {
expect(oldDb.shutDown.calledOnce).to.equal(true)
})
it('saveSchema', () => {
const state = {}
const schema = [
{
name: 'table1',
columns: [
{ name: 'id', type: 'INTEGER' }
]
}
]
saveSchema(state, {
dbName: 'test',
schema
})
expect(state.dbName).to.equal('test')
expect(state.schema).to.eql(schema)
})
it('updateTab (save)', () => {
const tab = {
id: 1,
@@ -376,66 +354,3 @@ describe('mutations', () => {
expect(state.predefinedQueries).to.eql(queries)
})
})
describe('actions', () => {
it('addTab adds new blank tab', async () => {
const state = {
tabs: [],
untitledLastIndex: 0
}
const id = await addTab({ state })
expect(state.tabs[0].id).to.eql(id)
expect(state.tabs[0].name).to.eql(null)
expect(state.tabs[0].tempName).to.eql('Untitled')
expect(state.tabs[0].isUnsaved).to.eql(true)
expect(state.untitledLastIndex).to.equal(1)
})
it('addTab adds tab from saved queries', async () => {
const state = {
tabs: [],
untitledLastIndex: 0
}
const tab = {
id: 1,
name: 'test',
tempName: null,
query: 'SELECT * from foo',
chart: {},
isUnsaved: false
}
await addTab({ state }, tab)
expect(state.tabs[0]).to.eql(tab)
expect(state.untitledLastIndex).to.equal(0)
})
it("addTab doesn't add anything when the query is already opened", async () => {
const tab1 = {
id: 1,
name: 'test',
tempName: null,
query: 'SELECT * from foo',
chart: {},
isUnsaved: false
}
const tab2 = {
id: 2,
name: 'bar',
tempName: null,
query: 'SELECT * from bar',
chart: {},
isUnsaved: false
}
const state = {
tabs: [tab1, tab2],
untitledLastIndex: 0
}
await addTab({ state }, tab1)
expect(state.tabs).to.have.lengthOf(2)
expect(state.untitledLastIndex).to.equal(0)
})
})

View File

@@ -1,8 +1,8 @@
import { expect } from 'chai'
import { mount } from '@vue/test-utils'
import tooltipMixin from '@/mixins/tooltips.js'
import tooltipMixin from '@/tooltipMixin'
describe('tooltips.js', () => {
describe('tooltipMixin.js', () => {
it('tooltip is hidden in initial', () => {
const component = {
template: '<div :style="tooltipStyle"></div>',

View File

@@ -0,0 +1,178 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { mount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import Schema from '@/views/Main/Editor/Schema'
import TableDescription from '@/views/Main/Editor/Schema/TableDescription'
import database from '@/lib/database'
import fIo from '@/lib/utils/fileIo'
import csv from '@/components/CsvImport/csv'
const localVue = createLocalVue()
localVue.use(Vuex)
describe('Schema.vue', () => {
afterEach(() => {
sinon.restore()
})
it('Renders DB name on initial', () => {
// mock store state
const state = {
db: {
dbName: 'fooDB'
}
}
const store = new Vuex.Store({ state })
// mout the component
const wrapper = mount(Schema, { store, localVue })
// check DB name and schema visibility
expect(wrapper.find('.db-name').text()).to.equal('fooDB')
expect(wrapper.find('.schema').isVisible()).to.equal(true)
})
it('Schema visibility is toggled when click on DB name', async () => {
// mock store state
const state = {
db: {
dbName: 'fooDB'
}
}
const store = new Vuex.Store({ state })
// mout the component
const wrapper = mount(Schema, { store, localVue })
// click and check visibility
await wrapper.find('.db-name').trigger('click')
expect(wrapper.find('.schema').isVisible()).to.equal(false)
await wrapper.find('.db-name').trigger('click')
expect(wrapper.find('.schema').isVisible()).to.equal(true)
})
it('Schema filter', async () => {
// mock store state
const state = {
db: {
dbName: 'fooDB',
schema: [
{
name: 'foo',
columns: [
{ name: 'id', type: 'INTEGER' },
{ name: 'title', type: 'NVARCHAR(24)' }
]
},
{
name: 'bar',
columns: [
{ name: 'id', type: 'INTEGER' },
{ name: 'price', type: 'INTEGER' }
]
},
{
name: 'foobar',
columns: [
{ name: 'id', type: 'INTEGER' },
{ name: 'price', type: 'INTEGER' }
]
}
]
}
}
const store = new Vuex.Store({ state })
// mount the component
const wrapper = mount(Schema, { store, localVue })
// apply filters and check the list of tables
await wrapper.find('#schema-filter input').setValue('foo')
let tables = wrapper.findAllComponents(TableDescription)
expect(tables).to.have.lengthOf(2)
expect(tables.at(0).vm.name).to.equal('foo')
expect(tables.at(1).vm.name).to.equal('foobar')
await wrapper.find('#schema-filter input').setValue('bar')
tables = wrapper.findAllComponents(TableDescription)
expect(tables).to.have.lengthOf(2)
expect(tables.at(0).vm.name).to.equal('bar')
expect(tables.at(1).vm.name).to.equal('foobar')
await wrapper.find('#schema-filter input').setValue('')
tables = wrapper.findAllComponents(TableDescription)
expect(tables).to.have.lengthOf(3)
expect(tables.at(0).vm.name).to.equal('foo')
expect(tables.at(1).vm.name).to.equal('bar')
expect(tables.at(2).vm.name).to.equal('foobar')
})
it('exports db', async () => {
const state = {
db: {
dbName: 'fooDB',
export: sinon.stub().resolves()
}
}
const store = new Vuex.Store({ state })
const wrapper = mount(Schema, { store, localVue })
await wrapper.findComponent({ name: 'export-icon' }).find('svg').trigger('click')
expect(state.db.export.calledOnceWith('fooDB'))
})
it('adds table', async () => {
const file = { name: 'test.csv' }
sinon.stub(fIo, 'getFileFromUser').resolves(file)
sinon.stub(csv, 'parse').resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
const state = {
db: database.getNewDatabase()
}
state.db.dbName = 'db'
state.db.execute('CREATE TABLE foo(id)')
state.db.refreshSchema()
sinon.spy(state.db, 'refreshSchema')
const store = new Vuex.Store({ state })
const wrapper = mount(Schema, { store, localVue })
sinon.spy(wrapper.vm.$refs.addCsv, 'previewCsv')
sinon.spy(wrapper.vm, 'addCsv')
sinon.spy(wrapper.vm.$refs.addCsv, 'loadFromCsv')
await wrapper.findComponent({ name: 'add-table-icon' }).find('svg').trigger('click')
await wrapper.vm.$refs.addCsv.previewCsv.returnValues[0]
await wrapper.vm.addCsv.returnValues[0]
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick()
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(true)
await wrapper.find('#csv-import').trigger('click')
await wrapper.vm.$refs.addCsv.loadFromCsv.returnValues[0]
await wrapper.find('#csv-finish').trigger('click')
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
await state.db.refreshSchema.returnValues[0]
expect(wrapper.vm.$store.state.db.schema).to.eql([
{ name: 'test', columns: [{ name: 'col1', type: 'real' }, { name: 'col2', type: 'text' }] },
{ name: 'foo', columns: [{ name: 'id', type: 'N/A' }] }
])
const res = await wrapper.vm.$store.state.db.execute('select * from test')
expect(res).to.eql({
columns: ['col1', 'col2'],
values: [[1, 'foo']]
})
})
})

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import TableDescription from '@/components/TableDescription.vue'
import TableDescription from '@/views/Main/Editor/Schema/TableDescription'
describe('TableDescription.vue', () => {
it('Initially the columns are hidden and table name is rendered', () => {

View File

@@ -1,8 +1,8 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { mount, shallowMount } from '@vue/test-utils'
import Chart from '@/components/Chart.vue'
import chart from '@/chart.js'
import Chart from '@/views/Main/Editor/Tabs/Tab/Chart'
import chartHelper from '@/views/Main/Editor/Tabs/Tab/Chart/chartHelper'
import * as dereference from 'react-chart-editor/lib/lib/dereference'
describe('Chart.vue', () => {
@@ -14,7 +14,7 @@ describe('Chart.vue', () => {
// mount the component
const wrapper = shallowMount(Chart)
const vm = wrapper.vm
const stub = sinon.stub(chart, 'getChartStateForSave').returns('result')
const stub = sinon.stub(chartHelper, 'getChartStateForSave').returns('result')
const chartData = vm.getChartStateForSave()
expect(stub.calledOnceWith(vm.state, vm.dataSources)).to.equal(true)
expect(chartData).to.equal('result')

View File

@@ -1,9 +1,9 @@
import { expect } from 'chai'
import sinon from 'sinon'
import * as chart from '@/chart'
import * as chartHelper from '@/views/Main/Editor/Tabs/Tab/Chart/chartHelper'
import * as dereference from 'react-chart-editor/lib/lib/dereference'
describe('chart.js', () => {
describe('chartHelper.js', () => {
afterEach(() => {
sinon.restore()
})
@@ -17,7 +17,7 @@ describe('chart.js', () => {
]
}
const ds = chart.getDataSourcesFromSqlResult(sqlResult)
const ds = chartHelper.getDataSourcesFromSqlResult(sqlResult)
expect(ds).to.eql({
id: [1, 2],
name: ['foo', 'bar']
@@ -30,7 +30,7 @@ describe('chart.js', () => {
name: ['foo', 'bar']
}
const ds = chart.getOptionsFromDataSources(dataSources)
const ds = chartHelper.getOptionsFromDataSources(dataSources)
expect(ds).to.eql([
{ value: 'id', label: 'id' },
{ value: 'name', label: 'name' }
@@ -53,7 +53,7 @@ describe('chart.js', () => {
sinon.stub(dereference, 'default')
sinon.spy(JSON, 'parse')
const ds = chart.getChartStateForSave(state, dataSources)
const ds = chartHelper.getChartStateForSave(state, dataSources)
expect(dereference.default.calledOnce).to.equal(true)

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { mount } from '@vue/test-utils'
import SqlEditor from '@/components/SqlEditor.vue'
import SqlEditor from '@/views/Main/Editor/Tabs/Tab/SqlEditor'
describe('SqlEditor.vue', () => {
it('Emits input event when a query is changed', async () => {

View File

@@ -1,7 +1,7 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { state } from '@/store'
import hint, { getHints } from '@/hint'
import state from '@/store/state'
import showHint, { getHints } from '@/views/Main/Editor/Tabs/Tab/SqlEditor/hint'
import CM from 'codemirror'
describe('hint.js', () => {
@@ -11,22 +11,24 @@ describe('hint.js', () => {
it('Calculates table list for hint', () => {
// mock store state
const schema = [
{
name: 'foo',
columns: [
{ name: 'fooId', type: 'INTEGER' },
{ name: 'name', type: 'NVARCHAR(20)' }
]
},
{
name: 'bar',
columns: [
{ name: 'barId', type: 'INTEGER' }
]
}
]
sinon.stub(state, 'schema').value(schema)
const db = {
schema: [
{
name: 'foo',
columns: [
{ name: 'fooId', type: 'INTEGER' },
{ name: 'name', type: 'NVARCHAR(20)' }
]
},
{
name: 'bar',
columns: [
{ name: 'barId', type: 'INTEGER' }
]
}
]
}
sinon.stub(state, 'db').value(db)
// mock showHint and editor
sinon.stub(CM, 'showHint')
@@ -40,15 +42,45 @@ describe('hint.js', () => {
getCursor: sinon.stub()
}
const clock = sinon.useFakeTimers()
hint.show(editor)
clock.tick(500)
showHint(editor)
expect(CM.showHint.called).to.equal(true)
expect(CM.showHint.firstCall.args[2].tables).to.eql({
foo: ['fooId', 'name'],
bar: ['barId']
})
expect(CM.showHint.firstCall.args[2].defaultTable).to.equal(null)
})
it('Add default table if there is only one table in schema', () => {
// mock store state
const db = {
schema: [
{
name: 'foo',
columns: [
{ name: 'fooId', type: 'INTEGER' },
{ name: 'name', type: 'NVARCHAR(20)' }
]
}
]
}
sinon.stub(state, 'db').value(db)
// mock showHint and editor
sinon.stub(CM, 'showHint')
const editor = {
getTokenAt () {
return {
string: 'SELECT',
type: 'keyword'
}
},
getCursor: sinon.stub()
}
showHint(editor)
expect(CM.showHint.firstCall.args[2].defaultTable).to.equal('foo')
})
it("Doesn't show hint when in string or space, or ';'", () => {
@@ -64,10 +96,7 @@ describe('hint.js', () => {
getCursor: sinon.stub()
}
const clock = sinon.useFakeTimers()
hint.show(editor)
clock.tick(500)
showHint(editor)
expect(CM.showHint.called).to.equal(false)
})
@@ -84,10 +113,7 @@ describe('hint.js', () => {
getCursor: sinon.stub()
}
const clock = sinon.useFakeTimers()
hint.show(editor)
clock.tick(500)
showHint(editor)
expect(CM.showHint.called).to.equal(false)
})
@@ -104,10 +130,7 @@ describe('hint.js', () => {
getCursor: sinon.stub()
}
const clock = sinon.useFakeTimers()
hint.show(editor)
clock.tick(500)
showHint(editor)
expect(CM.showHint.called).to.equal(false)
})
@@ -171,7 +194,7 @@ describe('hint.js', () => {
it('tables is empty object when schema is null', () => {
// mock store state
sinon.stub(state, 'schema').value(null)
sinon.stub(state, 'db').value({ schema: null })
// mock showHint and editor
sinon.stub(CM, 'showHint')
@@ -185,10 +208,7 @@ describe('hint.js', () => {
getCursor: sinon.stub()
}
const clock = sinon.useFakeTimers()
hint.show(editor)
clock.tick(500)
showHint(editor)
expect(CM.showHint.called).to.equal(true)
expect(CM.showHint.firstCall.args[2].tables).to.eql({})
})

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