Compare commits
51 Commits
0.10.1
...
csv-chunks
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df4595f610 | ||
|
|
5b3f34cb63 | ||
|
|
379ee1a67b | ||
|
|
a26fdedc02 | ||
|
|
70450408dc | ||
|
|
dd3bd3da1d | ||
|
|
1251c542cb | ||
|
|
ac89259924 | ||
|
|
179ff8b1e1 | ||
|
|
99a10225a3 | ||
|
|
c96deb5766 | ||
|
|
700970e1cc | ||
|
|
e2be61e2cf | ||
|
|
9c2c8f3692 | ||
|
|
414a116f94 | ||
|
|
3e503f85a9 | ||
|
|
88257bfcf6 | ||
|
|
bdcc494138 | ||
|
|
d750541c80 | ||
|
|
75f743ff9e | ||
|
|
8a9f4b3c0a | ||
|
|
77468d34ae | ||
|
|
a0577ec0ce | ||
|
|
e7d1398546 | ||
|
|
aa52048d51 | ||
|
|
33913f8f5c | ||
|
|
51eb7a543c | ||
|
|
a3fb38b23c | ||
|
|
3bb40b4eb7 | ||
|
|
6864bf84f8 | ||
|
|
9f1b3823f6 | ||
|
|
7574f529c3 | ||
|
|
653f8eff7b | ||
|
|
9b3dda6cff | ||
|
|
d94604ebfb | ||
|
|
16868ef430 | ||
|
|
b162c7043e | ||
|
|
8e856063b8 | ||
|
|
8684b4cef9 | ||
|
|
bcaebd4840 | ||
|
|
4619461af8 | ||
|
|
9fff1d699a | ||
|
|
5ab19c3fae | ||
|
|
cc483f4720 | ||
|
|
a07f2d3d99 | ||
|
|
b9844b8696 | ||
|
|
464bff3db8 | ||
|
|
00e434e142 | ||
|
|
5d6280abec | ||
|
|
7a39e905b9 | ||
|
|
297ea2c18a |
17
.github/workflows/config.grenrc.js
vendored
Normal 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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
20
.github/workflows/main.yml
vendored
@@ -25,16 +25,26 @@ jobs:
|
|||||||
cd dist
|
cd dist
|
||||||
zip -9 -r dist.zip . -x "js/*.map"
|
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
|
- name: Create release
|
||||||
uses: ncipollo/release-action@v1
|
uses: ncipollo/release-action@v1
|
||||||
with:
|
with:
|
||||||
artifacts: "dist/dist.zip"
|
artifacts: "dist/dist.zip"
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
bodyFile: "CHANGELOG.md"
|
||||||
|
|
||||||
- name: Deploy 🚀
|
- name: Deploy 🚀
|
||||||
uses: JamesIves/github-pages-deploy-action@3.6.2
|
uses: JamesIves/github-pages-deploy-action@4.1.1
|
||||||
with:
|
with:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
BRANCH: build # The branch the action should deploy to.
|
branch: build # The branch the action should deploy to.
|
||||||
FOLDER: dist/ # The folder the action should deploy.
|
folder: dist/ # The folder the action should deploy.
|
||||||
CLEAN: false # Automatically remove deleted files from the deploy branch
|
clean: true # Automatically remove deleted files from the deploy branch
|
||||||
|
clean-exclude: .nojekyll
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ With sqliteviz you can:
|
|||||||
- export a modified SQLite database
|
- export a modified SQLite database
|
||||||
- use it offline from your OS application menu like any other desktop app
|
- 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
|
## Quickstart
|
||||||
The latest release of sqliteviz is deployed on GitHub Pages at [lana-k.github.io/sqliteviz][6].
|
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
|
[8]: https://github.com/surmon-china/vue-codemirror#readme
|
||||||
[9]: https://www.papaparse.com/
|
[9]: https://www.papaparse.com/
|
||||||
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries
|
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries
|
||||||
[11]: https://github.com/plotly/plotly.js
|
[11]: https://github.com/plotly/plotly.js
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ module.exports = function (config) {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.worker\.js$/,
|
test: /worker\.js$/,
|
||||||
loader: 'worker-loader'
|
loader: 'worker-loader'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
725
package-lock.json
generated
@@ -1,25 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "sqliteviz",
|
"name": "sqliteviz",
|
||||||
"version": "1.0.0",
|
"version": "0.13.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sqliteviz",
|
"name": "sqliteviz",
|
||||||
"version": "1.0.0",
|
"version": "0.13.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"codemirror": "^5.57.0",
|
"codemirror": "^5.57.0",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"debounce": "^1.2.0",
|
|
||||||
"nanoid": "^3.1.12",
|
"nanoid": "^3.1.12",
|
||||||
"papaparse": "^5.3.0",
|
"papaparse": "^5.3.0",
|
||||||
"plotly.js": "^1.57.1",
|
"plotly.js": "^1.58.4",
|
||||||
"promise-worker": "^2.0.1",
|
"promise-worker": "^2.0.1",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-chart-editor": "^0.42.0",
|
"react-chart-editor": "^0.45.0",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"sql.js": "^1.3.0",
|
"sql.js": "^1.5.0",
|
||||||
"sqlite-parser": "^1.0.1",
|
"sqlite-parser": "^1.0.1",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
"vue-codemirror": "^4.0.6",
|
"vue-codemirror": "^4.0.6",
|
||||||
@@ -1471,7 +1470,10 @@
|
|||||||
"node_modules/@icons/material": {
|
"node_modules/@icons/material": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz",
|
"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": {
|
"node_modules/@intervolga/optimize-cssnano-plugin": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
@@ -3747,14 +3749,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/babel-plugin-styled-components": {
|
"node_modules/babel-plugin-styled-components": {
|
||||||
"version": "1.11.1",
|
"version": "1.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.12.0.tgz",
|
||||||
"integrity": "sha512-YwrInHyKUk1PU3avIRdiLyCpM++18Rs1NgyMXEAQC33rIXs/vro0A+stf4sT0Gf22Got+xRWB8Cm0tw+qkRzBA==",
|
"integrity": "sha512-FEiD7l5ZABdJPpLssKXjBUJMYqzbcNzBowfXDCdJhOpbhWiewapUaY+LZGT8R4Jg2TwOjGjG4RKeyrO5p9sBkA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-annotate-as-pure": "^7.0.0",
|
"@babel/helper-annotate-as-pure": "^7.0.0",
|
||||||
"@babel/helper-module-imports": "^7.0.0",
|
"@babel/helper-module-imports": "^7.0.0",
|
||||||
"babel-plugin-syntax-jsx": "^6.18.0",
|
"babel-plugin-syntax-jsx": "^6.18.0",
|
||||||
"lodash": "^4.17.11"
|
"lodash": "^4.17.11"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"styled-components": ">= 2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/babel-plugin-syntax-jsx": {
|
"node_modules/babel-plugin-syntax-jsx": {
|
||||||
@@ -6748,11 +6753,6 @@
|
|||||||
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=",
|
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=",
|
||||||
"dev": true
|
"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": {
|
"node_modules/debug": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||||
@@ -10057,9 +10057,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/gl-plot3d": {
|
"node_modules/gl-plot3d": {
|
||||||
"version": "2.4.6",
|
"version": "2.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-2.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-2.4.7.tgz",
|
||||||
"integrity": "sha512-CkrNvDKu0p74Di2g2Oc9kU+s1Oe+wi4cIfHzXABp8DvfoRl0/bayqJ9q8EcRAqMeQQxQZYGvJkk4hlBwI758Jw==",
|
"integrity": "sha512-mLDVWrl4Dj0O0druWyHUK5l7cBQrRIJRn2oROEgrRuOgbbrLAzsREKefwMO0bA0YqkiZMFMnV5VvPA9j57X5Xg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"3d-view": "^2.0.0",
|
"3d-view": "^2.0.0",
|
||||||
"a-big-triangle": "^1.0.3",
|
"a-big-triangle": "^1.0.3",
|
||||||
@@ -13481,6 +13481,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
"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": {
|
"node_modules/lodash.debounce": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||||
@@ -13819,7 +13824,10 @@
|
|||||||
"node_modules/mdi-react": {
|
"node_modules/mdi-react": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/mdi-react/-/mdi-react-5.2.0.tgz",
|
"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": {
|
"node_modules/mdn-data": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
@@ -15936,18 +15944,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/plotly-icons": {
|
"node_modules/plotly-icons": {
|
||||||
"version": "1.3.14",
|
"version": "1.3.15",
|
||||||
"resolved": "https://registry.npmjs.org/plotly-icons/-/plotly-icons-1.3.14.tgz",
|
"resolved": "https://registry.npmjs.org/plotly-icons/-/plotly-icons-1.3.15.tgz",
|
||||||
"integrity": "sha512-qglJLtQKeE0g5Zr08Je6Q16tbyOhSqiZ7eVvlUuxMxvNAFYqoYgqUXaagi3ytwYZdn+5SxSTscOt/lsKrAiEWQ==",
|
"integrity": "sha512-0k9zlvlFtXHzMvSSOhqt42d6jy13N5ueF8VLaL7S43SHE/+DTaO8W8jeFXQj5V1lRd7vkaYp9ACxNtMfByH04Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mdi-react": "5.2.0",
|
"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": {
|
"node_modules/plotly.js": {
|
||||||
"version": "1.57.1",
|
"version": "1.58.4",
|
||||||
"resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-1.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-1.58.4.tgz",
|
||||||
"integrity": "sha512-23GlzClmOGT1lE86Ys0DLuxBM/fgRNzJqH9y7ZylO4VPwstPAlQd12DklXsuqOgCNSxnnWUaP+J7BaUOFplsUg==",
|
"integrity": "sha512-hdt/aEvkPjS1HJ7tJKcPqsqi9ErEZPhUFs4d2ANTLeBim+AmVcHzS1rtwr7ZrVCINgliW/+92u81omJoy+lbUw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@plotly/d3-sankey": "0.7.2",
|
"@plotly/d3-sankey": "0.7.2",
|
||||||
"@plotly/d3-sankey-circular": "0.33.1",
|
"@plotly/d3-sankey-circular": "0.33.1",
|
||||||
@@ -15979,7 +15991,7 @@
|
|||||||
"gl-mat4": "^1.2.0",
|
"gl-mat4": "^1.2.0",
|
||||||
"gl-mesh3d": "^2.3.1",
|
"gl-mesh3d": "^2.3.1",
|
||||||
"gl-plot2d": "^1.4.5",
|
"gl-plot2d": "^1.4.5",
|
||||||
"gl-plot3d": "^2.4.6",
|
"gl-plot3d": "^2.4.7",
|
||||||
"gl-pointcloud2d": "^1.0.3",
|
"gl-pointcloud2d": "^1.0.3",
|
||||||
"gl-scatter3d": "^1.2.3",
|
"gl-scatter3d": "^1.2.3",
|
||||||
"gl-select-box": "^1.0.4",
|
"gl-select-box": "^1.0.4",
|
||||||
@@ -16057,14 +16069,6 @@
|
|||||||
"color-space": "^1.14.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": {
|
"node_modules/plotly.js/node_modules/to-px": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-px/-/to-px-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-px/-/to-px-1.0.1.tgz",
|
||||||
@@ -17289,260 +17293,56 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-chart-editor": {
|
"node_modules/react-chart-editor": {
|
||||||
"version": "0.42.0",
|
"version": "0.45.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-chart-editor/-/react-chart-editor-0.42.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-chart-editor/-/react-chart-editor-0.45.0.tgz",
|
||||||
"integrity": "sha512-SepVBYHRUMajDwjlPPHVbrLjjy9rH1lWB98cDSeOSukupzWxi/x+gJ8cbfPSSYRUdw3GbTDOmMcu/9SjK7qinQ==",
|
"integrity": "sha512-/SurlIFait/BbWhq7sd8gIPr5MbhjPgrNY+d4V3sH6R/BjUocN/5SqUhQGknOUkxH8Fu4V+qn/8GsjYRFvk5NA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@plotly/draft-js-export-html": "1.2.0",
|
"@plotly/draft-js-export-html": "1.2.0",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "2.2.6",
|
||||||
"draft-js": "^0.11.7",
|
"draft-js": "0.11.7",
|
||||||
"draft-js-import-html": "^1.3.3",
|
"draft-js-import-html": "1.4.1",
|
||||||
"draft-js-utils": "^1.3.3",
|
"draft-js-utils": "1.4.0",
|
||||||
"fast-isnumeric": "^1.1.4",
|
"fast-isnumeric": "1.1.4",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "3.1.1",
|
||||||
"plotly-icons": "1.3.14",
|
"plotly-icons": "1.3.15",
|
||||||
"plotly.js": "1.55.x",
|
"plotly.js": "1.58.x",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "15.7.2",
|
||||||
"raf": "^3.4.1",
|
"raf": "3.4.1",
|
||||||
"react-color": "^2.18.1",
|
"react-color": "2.19.3",
|
||||||
"react-colorscales": "0.7.3",
|
"react-colorscales": "0.7.3",
|
||||||
"react-day-picker": "^7.4.8",
|
"react-day-picker": "7.4.8",
|
||||||
"react-dropzone": "^10.2.2",
|
"react-dropzone": "10.2.2",
|
||||||
"react-plotly.js": "^2.4.0",
|
"react-plotly.js": "2.5.1",
|
||||||
"react-rangeslider": "^2.2.0",
|
"react-rangeslider": "2.2.0",
|
||||||
"react-resizable-rotatable-draggable": "^0.2.0",
|
"react-resizable-rotatable-draggable": "0.2.0",
|
||||||
"react-select": "^2.4.2",
|
"react-select": "2.4.4",
|
||||||
"react-tabs": "^3.1.1",
|
"react-tabs": "3.2.1",
|
||||||
"styled-components": "^5.2.0",
|
"styled-components": "5.2.1",
|
||||||
"tinycolor2": "^1.4.1"
|
"tinycolor2": "1.4.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"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": {
|
"peerDependencies": {
|
||||||
"geojson-rewind": "geojson-rewind"
|
"react": ">15",
|
||||||
}
|
"react-dom": ">15"
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-color": {
|
"node_modules/react-color": {
|
||||||
"version": "2.18.1",
|
"version": "2.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",
|
||||||
"integrity": "sha512-X5XpyJS6ncplZs74ak0JJoqPi+33Nzpv5RYWWxn17bslih+X7OlgmfpmGC1fNvdkK7/SGWYf1JJdn7D2n5gSuQ==",
|
"integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@icons/material": "^0.2.4",
|
"@icons/material": "^0.2.4",
|
||||||
"lodash": "^4.17.11",
|
"lodash": "^4.17.15",
|
||||||
|
"lodash-es": "^4.17.15",
|
||||||
"material-colors": "^1.2.1",
|
"material-colors": "^1.2.1",
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "^15.5.10",
|
||||||
"reactcss": "^1.2.0",
|
"reactcss": "^1.2.0",
|
||||||
"tinycolor2": "^1.4.1"
|
"tinycolor2": "^1.4.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-colorscales": {
|
"node_modules/react-colorscales": {
|
||||||
@@ -17616,11 +17416,15 @@
|
|||||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||||
},
|
},
|
||||||
"node_modules/react-plotly.js": {
|
"node_modules/react-plotly.js": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-plotly.js/-/react-plotly.js-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-plotly.js/-/react-plotly.js-2.5.1.tgz",
|
||||||
"integrity": "sha512-nzir3uf+tFO1YXVUH5lFfD2plbDuZJXKrCO88KmRVnha2/zEhZBmZO8yS6GcRnLmSrhJkfmj6GTqWWvrJDBCBQ==",
|
"integrity": "sha512-Oya14whSHvPsYXdI0nHOGs1pZhMzV2edV7HAW1xFHD58Y73m/LbG2Encvyz1tztL0vfjph0JNhiwO8cGBJnlhg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prop-types": "^15.7.2"
|
"prop-types": "^15.7.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"plotly.js": ">1.34.0",
|
||||||
|
"react": ">0.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-rangeslider": {
|
"node_modules/react-rangeslider": {
|
||||||
@@ -17659,12 +17463,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-tabs": {
|
"node_modules/react-tabs": {
|
||||||
"version": "3.1.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-3.2.1.tgz",
|
||||||
"integrity": "sha512-HpySC29NN1BkzBAnOC+ajfzPbTaVZcSWzMSjk56uAhPC/rBGtli8lTysR4CfPAyEE/hfweIzagOIoJ7nu80yng==",
|
"integrity": "sha512-M7ERQvJgBVLTyojFmC3G4tpaJuMmUtsnYenVQm2oA1NjDrGXq1UuzHgxhVTDwimkJcKEbzgWCybXFSHQ/+2bsA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clsx": "^1.1.0",
|
"clsx": "^1.1.0",
|
||||||
"prop-types": "^15.5.0"
|
"prop-types": "^15.5.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.3.0 || ^17.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-transition-group": {
|
"node_modules/react-transition-group": {
|
||||||
@@ -19633,9 +19440,9 @@
|
|||||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||||
},
|
},
|
||||||
"node_modules/sql.js": {
|
"node_modules/sql.js": {
|
||||||
"version": "1.3.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.5.0.tgz",
|
||||||
"integrity": "sha512-bxrJ/9rqJ2SA6hpHnSodRjKBugZHewRvNTITTt74W1VZWmzODjdS68yQW0/J9oC0NWKylHEtV1ptkoTyOYO4Tw=="
|
"integrity": "sha512-Qqr6HgX/hCDpLFWdN0BNoNpYQ2c1tOl1c3HGI0cshjaFSAWszKICuLZ9CyFUvRFPpEGW8RzHzwuXWWvXVGTKBg=="
|
||||||
},
|
},
|
||||||
"node_modules/sqlite-parser": {
|
"node_modules/sqlite-parser": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@@ -20039,9 +19846,9 @@
|
|||||||
"integrity": "sha1-CSDitN9nyOrulsa2I0/inoc9upk="
|
"integrity": "sha1-CSDitN9nyOrulsa2I0/inoc9upk="
|
||||||
},
|
},
|
||||||
"node_modules/styled-components": {
|
"node_modules/styled-components": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.2.1.tgz",
|
||||||
"integrity": "sha512-9qE8Vgp8C5cpGAIdFaQVAl89Zgx1TDM4Yf4tlHbO9cPijtpSXTMLHy9lmP0lb+yImhgPFb1AmZ1qMUubmg3HLg==",
|
"integrity": "sha512-sBdgLWrCFTKtmZm/9x7jkIabjFNVzCUeKfoQsM6R3saImkUnjx0QYdLwJHBjY9ifEcmjDamJDVfknWm1yxZPxQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-module-imports": "^7.0.0",
|
"@babel/helper-module-imports": "^7.0.0",
|
||||||
"@babel/traverse": "^7.4.5",
|
"@babel/traverse": "^7.4.5",
|
||||||
@@ -20056,6 +19863,15 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"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": {
|
"node_modules/styled-components/node_modules/@emotion/stylis": {
|
||||||
@@ -20525,9 +20341,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/tinycolor2": {
|
"node_modules/tinycolor2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
|
||||||
"integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=",
|
"integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
@@ -24700,7 +24516,8 @@
|
|||||||
"@icons/material": {
|
"@icons/material": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz",
|
"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": {
|
"@intervolga/optimize-cssnano-plugin": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
@@ -26677,9 +26494,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"babel-plugin-styled-components": {
|
"babel-plugin-styled-components": {
|
||||||
"version": "1.11.1",
|
"version": "1.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.12.0.tgz",
|
||||||
"integrity": "sha512-YwrInHyKUk1PU3avIRdiLyCpM++18Rs1NgyMXEAQC33rIXs/vro0A+stf4sT0Gf22Got+xRWB8Cm0tw+qkRzBA==",
|
"integrity": "sha512-FEiD7l5ZABdJPpLssKXjBUJMYqzbcNzBowfXDCdJhOpbhWiewapUaY+LZGT8R4Jg2TwOjGjG4RKeyrO5p9sBkA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/helper-annotate-as-pure": "^7.0.0",
|
"@babel/helper-annotate-as-pure": "^7.0.0",
|
||||||
"@babel/helper-module-imports": "^7.0.0",
|
"@babel/helper-module-imports": "^7.0.0",
|
||||||
@@ -29304,11 +29121,6 @@
|
|||||||
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=",
|
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=",
|
||||||
"dev": true
|
"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": {
|
"debug": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||||
@@ -32186,9 +31998,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"gl-plot3d": {
|
"gl-plot3d": {
|
||||||
"version": "2.4.6",
|
"version": "2.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-2.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-2.4.7.tgz",
|
||||||
"integrity": "sha512-CkrNvDKu0p74Di2g2Oc9kU+s1Oe+wi4cIfHzXABp8DvfoRl0/bayqJ9q8EcRAqMeQQxQZYGvJkk4hlBwI758Jw==",
|
"integrity": "sha512-mLDVWrl4Dj0O0druWyHUK5l7cBQrRIJRn2oROEgrRuOgbbrLAzsREKefwMO0bA0YqkiZMFMnV5VvPA9j57X5Xg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"3d-view": "^2.0.0",
|
"3d-view": "^2.0.0",
|
||||||
"a-big-triangle": "^1.0.3",
|
"a-big-triangle": "^1.0.3",
|
||||||
@@ -35078,6 +34890,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
"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": {
|
"lodash.debounce": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||||
@@ -35381,7 +35198,8 @@
|
|||||||
"mdi-react": {
|
"mdi-react": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/mdi-react/-/mdi-react-5.2.0.tgz",
|
"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": {
|
"mdn-data": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
@@ -37179,18 +36997,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"plotly-icons": {
|
"plotly-icons": {
|
||||||
"version": "1.3.14",
|
"version": "1.3.15",
|
||||||
"resolved": "https://registry.npmjs.org/plotly-icons/-/plotly-icons-1.3.14.tgz",
|
"resolved": "https://registry.npmjs.org/plotly-icons/-/plotly-icons-1.3.15.tgz",
|
||||||
"integrity": "sha512-qglJLtQKeE0g5Zr08Je6Q16tbyOhSqiZ7eVvlUuxMxvNAFYqoYgqUXaagi3ytwYZdn+5SxSTscOt/lsKrAiEWQ==",
|
"integrity": "sha512-0k9zlvlFtXHzMvSSOhqt42d6jy13N5ueF8VLaL7S43SHE/+DTaO8W8jeFXQj5V1lRd7vkaYp9ACxNtMfByH04Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"mdi-react": "5.2.0",
|
"mdi-react": "5.2.0",
|
||||||
"prop-types": "^15.6.1"
|
"prop-types": "^15.7.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"plotly.js": {
|
"plotly.js": {
|
||||||
"version": "1.57.1",
|
"version": "1.58.4",
|
||||||
"resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-1.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-1.58.4.tgz",
|
||||||
"integrity": "sha512-23GlzClmOGT1lE86Ys0DLuxBM/fgRNzJqH9y7ZylO4VPwstPAlQd12DklXsuqOgCNSxnnWUaP+J7BaUOFplsUg==",
|
"integrity": "sha512-hdt/aEvkPjS1HJ7tJKcPqsqi9ErEZPhUFs4d2ANTLeBim+AmVcHzS1rtwr7ZrVCINgliW/+92u81omJoy+lbUw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@plotly/d3-sankey": "0.7.2",
|
"@plotly/d3-sankey": "0.7.2",
|
||||||
"@plotly/d3-sankey-circular": "0.33.1",
|
"@plotly/d3-sankey-circular": "0.33.1",
|
||||||
@@ -37222,7 +37040,7 @@
|
|||||||
"gl-mat4": "^1.2.0",
|
"gl-mat4": "^1.2.0",
|
||||||
"gl-mesh3d": "^2.3.1",
|
"gl-mesh3d": "^2.3.1",
|
||||||
"gl-plot2d": "^1.4.5",
|
"gl-plot2d": "^1.4.5",
|
||||||
"gl-plot3d": "^2.4.6",
|
"gl-plot3d": "^2.4.7",
|
||||||
"gl-pointcloud2d": "^1.0.3",
|
"gl-pointcloud2d": "^1.0.3",
|
||||||
"gl-scatter3d": "^1.2.3",
|
"gl-scatter3d": "^1.2.3",
|
||||||
"gl-select-box": "^1.0.4",
|
"gl-select-box": "^1.0.4",
|
||||||
@@ -37300,11 +37118,6 @@
|
|||||||
"color-space": "^1.14.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": {
|
"to-px": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-px/-/to-px-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-px/-/to-px-1.0.1.tgz",
|
||||||
@@ -38390,240 +38203,42 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-chart-editor": {
|
"react-chart-editor": {
|
||||||
"version": "0.42.0",
|
"version": "0.45.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-chart-editor/-/react-chart-editor-0.42.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-chart-editor/-/react-chart-editor-0.45.0.tgz",
|
||||||
"integrity": "sha512-SepVBYHRUMajDwjlPPHVbrLjjy9rH1lWB98cDSeOSukupzWxi/x+gJ8cbfPSSYRUdw3GbTDOmMcu/9SjK7qinQ==",
|
"integrity": "sha512-/SurlIFait/BbWhq7sd8gIPr5MbhjPgrNY+d4V3sH6R/BjUocN/5SqUhQGknOUkxH8Fu4V+qn/8GsjYRFvk5NA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@plotly/draft-js-export-html": "1.2.0",
|
"@plotly/draft-js-export-html": "1.2.0",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "2.2.6",
|
||||||
"draft-js": "^0.11.7",
|
"draft-js": "0.11.7",
|
||||||
"draft-js-import-html": "^1.3.3",
|
"draft-js-import-html": "1.4.1",
|
||||||
"draft-js-utils": "^1.3.3",
|
"draft-js-utils": "1.4.0",
|
||||||
"fast-isnumeric": "^1.1.4",
|
"fast-isnumeric": "1.1.4",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "3.1.1",
|
||||||
"plotly-icons": "1.3.14",
|
"plotly-icons": "1.3.15",
|
||||||
"plotly.js": "1.55.x",
|
"plotly.js": "1.58.x",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "15.7.2",
|
||||||
"raf": "^3.4.1",
|
"raf": "3.4.1",
|
||||||
"react-color": "^2.18.1",
|
"react-color": "2.19.3",
|
||||||
"react-colorscales": "0.7.3",
|
"react-colorscales": "0.7.3",
|
||||||
"react-day-picker": "^7.4.8",
|
"react-day-picker": "7.4.8",
|
||||||
"react-dropzone": "^10.2.2",
|
"react-dropzone": "10.2.2",
|
||||||
"react-plotly.js": "^2.4.0",
|
"react-plotly.js": "2.5.1",
|
||||||
"react-rangeslider": "^2.2.0",
|
"react-rangeslider": "2.2.0",
|
||||||
"react-resizable-rotatable-draggable": "^0.2.0",
|
"react-resizable-rotatable-draggable": "0.2.0",
|
||||||
"react-select": "^2.4.2",
|
"react-select": "2.4.4",
|
||||||
"react-tabs": "^3.1.1",
|
"react-tabs": "3.2.1",
|
||||||
"styled-components": "^5.2.0",
|
"styled-components": "5.2.1",
|
||||||
"tinycolor2": "^1.4.1"
|
"tinycolor2": "1.4.2"
|
||||||
},
|
|
||||||
"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-color": {
|
"react-color": {
|
||||||
"version": "2.18.1",
|
"version": "2.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",
|
||||||
"integrity": "sha512-X5XpyJS6ncplZs74ak0JJoqPi+33Nzpv5RYWWxn17bslih+X7OlgmfpmGC1fNvdkK7/SGWYf1JJdn7D2n5gSuQ==",
|
"integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@icons/material": "^0.2.4",
|
"@icons/material": "^0.2.4",
|
||||||
"lodash": "^4.17.11",
|
"lodash": "^4.17.15",
|
||||||
|
"lodash-es": "^4.17.15",
|
||||||
"material-colors": "^1.2.1",
|
"material-colors": "^1.2.1",
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "^15.5.10",
|
||||||
"reactcss": "^1.2.0",
|
"reactcss": "^1.2.0",
|
||||||
@@ -38700,9 +38315,9 @@
|
|||||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||||
},
|
},
|
||||||
"react-plotly.js": {
|
"react-plotly.js": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-plotly.js/-/react-plotly.js-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-plotly.js/-/react-plotly.js-2.5.1.tgz",
|
||||||
"integrity": "sha512-nzir3uf+tFO1YXVUH5lFfD2plbDuZJXKrCO88KmRVnha2/zEhZBmZO8yS6GcRnLmSrhJkfmj6GTqWWvrJDBCBQ==",
|
"integrity": "sha512-Oya14whSHvPsYXdI0nHOGs1pZhMzV2edV7HAW1xFHD58Y73m/LbG2Encvyz1tztL0vfjph0JNhiwO8cGBJnlhg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"prop-types": "^15.7.2"
|
"prop-types": "^15.7.2"
|
||||||
}
|
}
|
||||||
@@ -38736,9 +38351,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-tabs": {
|
"react-tabs": {
|
||||||
"version": "3.1.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-3.2.1.tgz",
|
||||||
"integrity": "sha512-HpySC29NN1BkzBAnOC+ajfzPbTaVZcSWzMSjk56uAhPC/rBGtli8lTysR4CfPAyEE/hfweIzagOIoJ7nu80yng==",
|
"integrity": "sha512-M7ERQvJgBVLTyojFmC3G4tpaJuMmUtsnYenVQm2oA1NjDrGXq1UuzHgxhVTDwimkJcKEbzgWCybXFSHQ/+2bsA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"clsx": "^1.1.0",
|
"clsx": "^1.1.0",
|
||||||
"prop-types": "^15.5.0"
|
"prop-types": "^15.5.0"
|
||||||
@@ -40491,9 +40106,9 @@
|
|||||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||||
},
|
},
|
||||||
"sql.js": {
|
"sql.js": {
|
||||||
"version": "1.3.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.5.0.tgz",
|
||||||
"integrity": "sha512-bxrJ/9rqJ2SA6hpHnSodRjKBugZHewRvNTITTt74W1VZWmzODjdS68yQW0/J9oC0NWKylHEtV1ptkoTyOYO4Tw=="
|
"integrity": "sha512-Qqr6HgX/hCDpLFWdN0BNoNpYQ2c1tOl1c3HGI0cshjaFSAWszKICuLZ9CyFUvRFPpEGW8RzHzwuXWWvXVGTKBg=="
|
||||||
},
|
},
|
||||||
"sqlite-parser": {
|
"sqlite-parser": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@@ -40832,9 +40447,9 @@
|
|||||||
"integrity": "sha1-CSDitN9nyOrulsa2I0/inoc9upk="
|
"integrity": "sha1-CSDitN9nyOrulsa2I0/inoc9upk="
|
||||||
},
|
},
|
||||||
"styled-components": {
|
"styled-components": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.2.1.tgz",
|
||||||
"integrity": "sha512-9qE8Vgp8C5cpGAIdFaQVAl89Zgx1TDM4Yf4tlHbO9cPijtpSXTMLHy9lmP0lb+yImhgPFb1AmZ1qMUubmg3HLg==",
|
"integrity": "sha512-sBdgLWrCFTKtmZm/9x7jkIabjFNVzCUeKfoQsM6R3saImkUnjx0QYdLwJHBjY9ifEcmjDamJDVfknWm1yxZPxQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/helper-module-imports": "^7.0.0",
|
"@babel/helper-module-imports": "^7.0.0",
|
||||||
"@babel/traverse": "^7.4.5",
|
"@babel/traverse": "^7.4.5",
|
||||||
@@ -41250,9 +40865,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"tinycolor2": {
|
"tinycolor2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
|
||||||
"integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g="
|
"integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA=="
|
||||||
},
|
},
|
||||||
"tinyqueue": {
|
"tinyqueue": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sqliteviz",
|
"name": "sqliteviz",
|
||||||
"version": "1.0.0",
|
"version": "0.13.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -12,15 +12,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"codemirror": "^5.57.0",
|
"codemirror": "^5.57.0",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"debounce": "^1.2.0",
|
|
||||||
"nanoid": "^3.1.12",
|
"nanoid": "^3.1.12",
|
||||||
"papaparse": "^5.3.0",
|
"papaparse": "^5.3.0",
|
||||||
"plotly.js": "^1.57.1",
|
"plotly.js": "^1.58.4",
|
||||||
"promise-worker": "^2.0.1",
|
"promise-worker": "^2.0.1",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-chart-editor": "^0.42.0",
|
"react-chart-editor": "^0.45.0",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"sql.js": "^1.3.0",
|
"sql.js": "^1.5.0",
|
||||||
"sqlite-parser": "^1.0.1",
|
"sqlite-parser": "^1.0.1",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
"vue-codemirror": "^4.0.6",
|
"vue-codemirror": "^4.0.6",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 11 KiB |
BIN
public/Logo48x48.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 798 B After Width: | Height: | Size: 774 B |
@@ -3,6 +3,16 @@
|
|||||||
"description": "Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases or CSV.",
|
"description": "Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases or CSV.",
|
||||||
"display": "fullscreen",
|
"display": "fullscreen",
|
||||||
"icons": [
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.png",
|
||||||
|
"sizes": "32x32",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "Logo48x48.png",
|
||||||
|
"sizes": "48x48",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"src": "Logo192x192.png",
|
"src": "Logo192x192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
|
|||||||
@@ -60,4 +60,7 @@ button,
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
.CodeMirror-hints {
|
||||||
|
z-index: 999 !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 42 KiB |
@@ -1,5 +1,5 @@
|
|||||||
.rounded-bg {
|
.rounded-bg {
|
||||||
padding: 40px 5px 5px;
|
padding: 35px 5px 5px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
}
|
}
|
||||||
table {
|
table {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
margin-top: -40px;
|
margin-top: -35px;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
thead th, .fixed-header {
|
thead th, .fixed-header {
|
||||||
@@ -56,7 +56,7 @@ tbody td {
|
|||||||
border-right: 1px solid var(--color-border-light);
|
border-right: 1px solid var(--color-border-light);
|
||||||
}
|
}
|
||||||
td, th, .fixed-header {
|
td, th, .fixed-header {
|
||||||
padding: 12px 24px;
|
padding: 8px 24px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ascii from '@/ascii'
|
import ascii from './ascii'
|
||||||
import DropDownChevron from '@/components/svg/dropDownChevron'
|
import DropDownChevron from '@/components/svg/dropDownChevron'
|
||||||
import ClearIcon from '@/components/svg/clear'
|
import ClearIcon from '@/components/svg/clear'
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ export default {
|
|||||||
getResult (source) {
|
getResult (source) {
|
||||||
const result = {}
|
const result = {}
|
||||||
if (source.meta.fields) {
|
if (source.meta.fields) {
|
||||||
result.columns = source.meta.fields
|
result.columns = source.meta.fields.map(col => col.trim())
|
||||||
result.values = source.data.map(row => {
|
result.values = source.data.map(row => {
|
||||||
const resultRow = []
|
const resultRow = []
|
||||||
result.columns.forEach(col => { resultRow.push(row[col]) })
|
result.columns.forEach(col => { resultRow.push(row[col]) })
|
||||||
@@ -28,6 +28,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
parse (file, config = {}) {
|
parse (file, config = {}) {
|
||||||
|
let parsedData = null
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
delimiter: '', // auto-detect
|
delimiter: '', // auto-detect
|
||||||
@@ -42,13 +43,22 @@ export default {
|
|||||||
worker: true,
|
worker: true,
|
||||||
comments: false,
|
comments: false,
|
||||||
step: undefined,
|
step: undefined,
|
||||||
complete: results => {
|
chunk: results => {
|
||||||
|
if (parsedData === null) {
|
||||||
|
parsedData = results
|
||||||
|
} else {
|
||||||
|
parsedData.data = parsedData.data.concat(results.data)
|
||||||
|
parsedData.errors = parsedData.errors.concat(results.errors)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
chunkSize: 1024 * 716,
|
||||||
|
complete: () => {
|
||||||
const res = {
|
const res = {
|
||||||
data: this.getResult(results),
|
data: this.getResult(parsedData),
|
||||||
delimiter: results.meta.delimiter,
|
delimiter: parsedData.meta.delimiter,
|
||||||
hasErrors: false
|
hasErrors: false
|
||||||
}
|
}
|
||||||
res.messages = results.errors.map(msg => {
|
res.messages = parsedData.errors.map(msg => {
|
||||||
msg.type = msg.code === 'UndetectableDelimiter' ? 'info' : 'error'
|
msg.type = msg.code === 'UndetectableDelimiter' ? 'info' : 'error'
|
||||||
if (msg.type === 'error') res.hasErrors = true
|
if (msg.type === 'error') res.hasErrors = true
|
||||||
msg.hint = hintsByCode[msg.code]
|
msg.hint = hintsByCode[msg.code]
|
||||||
@@ -63,8 +73,6 @@ export default {
|
|||||||
downloadRequestHeaders: undefined,
|
downloadRequestHeaders: undefined,
|
||||||
downloadRequestBody: undefined,
|
downloadRequestBody: undefined,
|
||||||
skipEmptyLines: 'greedy',
|
skipEmptyLines: 'greedy',
|
||||||
chunk: undefined,
|
|
||||||
chunkSize: undefined,
|
|
||||||
fastMode: undefined,
|
fastMode: undefined,
|
||||||
beforeFirstChunk: undefined,
|
beforeFirstChunk: undefined,
|
||||||
withCredentials: undefined,
|
withCredentials: undefined,
|
||||||
381
src/components/CsvImport/index.vue
Normal 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>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="db-uploader-container" :style="{ width }">
|
<div class="db-uploader-container" :style="{ width }">
|
||||||
<change-db-icon v-if="type === 'small'" @click.native="browse"/>
|
<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
|
<div
|
||||||
class="drop-area"
|
class="drop-area"
|
||||||
@dragover.prevent="state = 'dragover'"
|
@dragover.prevent="state = 'dragover'"
|
||||||
@@ -26,7 +26,8 @@
|
|||||||
ref="fileImg"
|
ref="fileImg"
|
||||||
:class="{
|
:class="{
|
||||||
'swing': state === 'dragover',
|
'swing': state === 'dragover',
|
||||||
'fly': state === 'drop'
|
'fly': state === 'dropping',
|
||||||
|
'hidden': state === 'dropped'
|
||||||
}"
|
}"
|
||||||
:src="require('@/assets/images/file.png')"
|
:src="require('@/assets/images/file.png')"
|
||||||
/>
|
/>
|
||||||
@@ -41,112 +42,22 @@
|
|||||||
<div id="error" class="error"></div>
|
<div id="error" class="error"></div>
|
||||||
|
|
||||||
<!--Parse csv dialog -->
|
<!--Parse csv dialog -->
|
||||||
<modal name="parse" classes="dialog" height="auto" width="60%" :clickToClose="false">
|
<csv-import
|
||||||
<div class="dialog-header">
|
ref="addCsv"
|
||||||
Import CSV
|
:file="file"
|
||||||
<close-icon @click="cancelCsvImport" :disabled="disableDialog"/>
|
:db="newDb"
|
||||||
</div>
|
dialog-name="importFromCsv"
|
||||||
<div class="dialog-body">
|
@cancel="cancelCsvImport"
|
||||||
<div class="chars">
|
@finish="finish"
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import fu from '@/file.utils'
|
import fIo from '@/lib/utils/fileIo'
|
||||||
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 ChangeDbIcon from '@/components/svg/changeDb'
|
import ChangeDbIcon from '@/components/svg/changeDb'
|
||||||
import time from '@/time'
|
import database from '@/lib/database'
|
||||||
import database from '@/database'
|
import CsvImport from '@/components/CsvImport'
|
||||||
|
|
||||||
const csvMimeTypes = [
|
|
||||||
'text/csv',
|
|
||||||
'text/x-csv',
|
|
||||||
'application/x-csv',
|
|
||||||
'application/csv',
|
|
||||||
'text/x-comma-separated-values',
|
|
||||||
'text/comma-separated-values'
|
|
||||||
]
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DbUploader',
|
name: 'DbUploader',
|
||||||
@@ -154,9 +65,9 @@ export default {
|
|||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'regular',
|
default: 'small',
|
||||||
validator: (value) => {
|
validator: (value) => {
|
||||||
return ['regular', 'illustrated', 'small'].includes(value)
|
return ['illustrated', 'small'].includes(value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
width: {
|
width: {
|
||||||
@@ -167,27 +78,13 @@ export default {
|
|||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
ChangeDbIcon,
|
ChangeDbIcon,
|
||||||
TextField,
|
CsvImport
|
||||||
DelimiterSelector,
|
|
||||||
CloseIcon,
|
|
||||||
CheckBox,
|
|
||||||
SqlTable,
|
|
||||||
Logs
|
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
state: '',
|
state: '',
|
||||||
animationPromise: Promise.resolve(),
|
animationPromise: Promise.resolve(),
|
||||||
file: null,
|
file: null,
|
||||||
schema: null,
|
|
||||||
delimiter: '',
|
|
||||||
quoteChar: '"',
|
|
||||||
escapeChar: '"',
|
|
||||||
header: true,
|
|
||||||
previewData: null,
|
|
||||||
importCsvMessages: [],
|
|
||||||
disableDialog: false,
|
|
||||||
importCsvCompleted: false,
|
|
||||||
newDb: null
|
newDb: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -196,206 +93,50 @@ export default {
|
|||||||
this.animationPromise = new Promise((resolve) => {
|
this.animationPromise = new Promise((resolve) => {
|
||||||
this.$refs.fileImg.addEventListener('animationend', event => {
|
this.$refs.fileImg.addEventListener('animationend', event => {
|
||||||
if (event.animationName.startsWith('fly')) {
|
if (event.animationName.startsWith('fly')) {
|
||||||
|
this.state = 'dropped'
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
quoteChar () {
|
|
||||||
this.previewCSV()
|
|
||||||
},
|
|
||||||
|
|
||||||
escapeChar () {
|
|
||||||
this.previewCSV()
|
|
||||||
},
|
|
||||||
|
|
||||||
header () {
|
|
||||||
this.previewCSV()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
cancelCsvImport () {
|
cancelCsvImport () {
|
||||||
if (!this.disableDialog) {
|
if (this.newDb) {
|
||||||
this.$modal.hide('parse')
|
this.newDb.shutDown()
|
||||||
if (this.newDb) {
|
this.newDb = null
|
||||||
this.newDb.shutDown()
|
|
||||||
this.newDb = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async finish () {
|
async finish () {
|
||||||
this.$store.commit('setDb', this.newDb)
|
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') {
|
if (this.$route.path !== '/editor') {
|
||||||
this.$router.push('/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) {
|
loadDb (file) {
|
||||||
this.newDb = database.getNewDatabase()
|
|
||||||
return Promise.all([this.newDb.loadDb(file), this.animationPromise])
|
return Promise.all([this.newDb.loadDb(file), this.animationPromise])
|
||||||
.then(([schema]) => {
|
.then(this.finish)
|
||||||
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
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async checkFile (file) {
|
async checkFile (file) {
|
||||||
this.state = 'drop'
|
this.state = 'dropping'
|
||||||
if (csvMimeTypes.includes(file.type)) {
|
this.newDb = database.getNewDatabase()
|
||||||
this.file = file
|
|
||||||
this.header = true
|
if (fIo.isDatabase(file)) {
|
||||||
this.quoteChar = '"'
|
|
||||||
this.escapeChar = '"'
|
|
||||||
this.delimiter = ''
|
|
||||||
return Promise.all([this.previewCSV(), this.animationPromise])
|
|
||||||
.then(() => {
|
|
||||||
this.$modal.show('parse')
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.loadDb(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 () {
|
browse () {
|
||||||
fu.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
|
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
|
||||||
.then(this.checkFile)
|
.then(this.checkFile)
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -431,6 +172,7 @@ export default {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
#img-container {
|
#img-container {
|
||||||
@@ -502,42 +244,16 @@ export default {
|
|||||||
#file-img.fly {
|
#file-img.fly {
|
||||||
animation: fly ease-in-out 1s 1 normal;
|
animation: fly ease-in-out 1s 1 normal;
|
||||||
transform-origin: center center;
|
transform-origin: center center;
|
||||||
top: 183px;
|
|
||||||
left: 225px;
|
|
||||||
transition: top 1s ease-in-out, left 1s ease-in-out;
|
|
||||||
}
|
}
|
||||||
@keyframes fly {
|
@keyframes fly {
|
||||||
100% { transform: rotate(360deg) scale(0.5); }
|
100% {
|
||||||
}
|
transform: rotate(360deg) scale(0.5);
|
||||||
/* Parse CSV dialog */
|
top: 183px;
|
||||||
.chars {
|
left: 225px;
|
||||||
display: flex;
|
}
|
||||||
align-items: flex-end;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.char-input {
|
|
||||||
margin-right: 44px;
|
|
||||||
}
|
|
||||||
.preview-table {
|
|
||||||
margin-top: 32px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.import-csv-errors {
|
#file-img.hidden {
|
||||||
height: 160px;
|
display: none;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
<template>
|
<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
|
<circle
|
||||||
class="loader-svg bg"
|
class="loader-svg bg"
|
||||||
cx="10"
|
:style="{ strokeWidth }"
|
||||||
cy="10"
|
:cx="size / 2"
|
||||||
r="8"
|
:cy="size / 2"
|
||||||
|
:r="radius"
|
||||||
/>
|
/>
|
||||||
<circle
|
<circle
|
||||||
class="loader-svg front"
|
class="loader-svg front"
|
||||||
:style="{ strokeDasharray: circleProgress }"
|
:style="{ strokeDasharray: circleProgress, strokeDashoffset: offset, strokeWidth }"
|
||||||
cx="10"
|
:cx="size / 2"
|
||||||
cy="10"
|
:cy="size / 2"
|
||||||
r="8"
|
:r="radius"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
@@ -19,15 +20,35 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'LoadingIndicator',
|
name: 'LoadingIndicator',
|
||||||
props: ['progress'],
|
props: {
|
||||||
|
progress: {
|
||||||
|
type: Number,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 20
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
circleProgress () {
|
circleProgress () {
|
||||||
const dash = (50.24 * this.progress) / 100
|
const circle = this.radius * 3.14 * 2
|
||||||
const space = 50.24 - dash
|
const dash = this.progress ? (circle * this.progress) / 100 : circle * 1 / 3
|
||||||
|
const space = circle - dash
|
||||||
return `${dash}px, ${space}px`
|
return `${dash}px, ${space}px`
|
||||||
},
|
},
|
||||||
animationClass () {
|
animationClass () {
|
||||||
return this.progress === undefined ? 'loading' : 'progress'
|
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;
|
position: absolute;
|
||||||
left: 0; right: 0; top: 0; bottom: 0;
|
left: 0; right: 0; top: 0; bottom: 0;
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke-width: 2px;
|
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
stroke: var(--color-accent);
|
stroke: var(--color-accent);
|
||||||
}
|
}
|
||||||
@@ -48,27 +68,30 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.loading .loader-svg.front {
|
.loading .loader-svg.front {
|
||||||
stroke-dasharray: 40.24px;
|
will-change: transform;
|
||||||
animation: fill-animation-loading 1s cubic-bezier(1,1,1,1) 0s infinite;
|
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 {
|
@keyframes fill-animation-loading {
|
||||||
0% {
|
0% {
|
||||||
stroke-dasharray: 10px 40.24px;
|
transform: rotate(0deg);
|
||||||
stroke-dashoffset: 0;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
stroke-dasharray: 25.12px;
|
|
||||||
stroke-dashoffset: 25.12px;
|
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
stroke-dasharray: 10px 40.24px;
|
transform: rotate(360deg);
|
||||||
stroke-dashoffset: 50.24px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress .loader-svg.front {
|
.progress .loader-svg.front {
|
||||||
stroke-dashoffset: 12.56;
|
|
||||||
transition: stroke-dasharray 0.2s;
|
transition: stroke-dasharray 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,14 +63,16 @@ export default {
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border: 1px solid var(--color-border-light);
|
border: 1px solid var(--color-border-light);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow-y: scroll;
|
overflow-y: auto;
|
||||||
|
color: var(--color-text-base);
|
||||||
}
|
}
|
||||||
.msg {
|
.msg {
|
||||||
padding: 16px 7px;
|
padding: 12px 7px;
|
||||||
border-bottom: 1px solid var(--color-border-light);
|
border-bottom: 1px solid var(--color-border-light);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg:last-child {
|
.msg:last-child {
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import splitter from '@/splitter'
|
import splitter from './splitter'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Splitpanes',
|
name: 'Splitpanes',
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
<div class="table-footer-count">
|
<div class="table-footer-count">
|
||||||
{{ dataSet.values.length}} {{dataSet.values.length === 1 ? 'row' : 'rows'}} retrieved
|
{{ dataSet.values.length}} {{dataSet.values.length === 1 ? 'row' : 'rows'}} retrieved
|
||||||
<span v-if="preview">for preview</span>
|
<span v-if="preview">for preview</span>
|
||||||
|
<span v-if="time">in {{ time }}</span>
|
||||||
</div>
|
</div>
|
||||||
<pager v-show="pageCount > 1" :page-count="pageCount" v-model="currentPage" />
|
<pager v-show="pageCount > 1" :page-count="pageCount" v-model="currentPage" />
|
||||||
</div>
|
</div>
|
||||||
@@ -48,12 +49,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Pager from '@/components/Pager'
|
import Pager from './Pager'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SqlTable',
|
name: 'SqlTable',
|
||||||
components: { Pager },
|
components: { Pager },
|
||||||
props: ['dataSet', 'height', 'preview'],
|
props: ['dataSet', 'time', 'height', 'preview'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
header: null,
|
header: null,
|
||||||
@@ -87,4 +87,7 @@ input.error {
|
|||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
.text-field-error:first-letter {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
61
src/components/svg/addTable.vue
Normal 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>
|
||||||
@@ -16,13 +16,13 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="icon-tooltip" :style="tooltipStyle">
|
<span class="icon-tooltip" :style="tooltipStyle">
|
||||||
Change database
|
Load another database or CSV
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import tooltipMixin from '@/mixins/tooltips'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'changeDbIcon',
|
name: 'changeDbIcon',
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import tooltipMixin from '@/mixins/tooltips'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ExportIcon',
|
name: 'ExportIcon',
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import tooltipMixin from '@/mixins/tooltips'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HintIcon',
|
name: 'HintIcon',
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import initSqlJs from 'sql.js/dist/sql-wasm.js'
|
import initSqlJs from 'sql.js/dist/sql-wasm.js'
|
||||||
import dbUtils from '@/db.utils'
|
import dbUtils from './_statements'
|
||||||
|
|
||||||
let SQL = null
|
let SQL = null
|
||||||
const sqlModuleReady = initSqlJs().then(sqlModule => { SQL = sqlModule })
|
const sqlModuleReady = initSqlJs().then(sqlModule => { SQL = sqlModule })
|
||||||
@@ -39,13 +39,15 @@ export default class Sql {
|
|||||||
return this.db.exec(sql, params)
|
return this.db.exec(sql, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
import (columns, values, progressCounterId, progressCallback, chunkSize = 1500) {
|
import (tabName, columns, values, progressCounterId, progressCallback, chunkSize = 1500) {
|
||||||
this.createDb()
|
if (this.db === null) {
|
||||||
this.db.exec(dbUtils.getCreateStatement(columns, values))
|
this.createDb()
|
||||||
|
}
|
||||||
|
this.db.exec(dbUtils.getCreateStatement(tabName, columns, values))
|
||||||
const chunks = dbUtils.generateChunks(values, chunkSize)
|
const chunks = dbUtils.generateChunks(values, chunkSize)
|
||||||
const chunksAmount = Math.ceil(values.length / chunkSize)
|
const chunksAmount = Math.ceil(values.length / chunkSize)
|
||||||
let count = 0
|
let count = 0
|
||||||
const insertStr = dbUtils.getInsertStmt(columns)
|
const insertStr = dbUtils.getInsertStmt(tabName, columns)
|
||||||
const insertStmt = this.db.prepare(insertStr)
|
const insertStmt = this.db.prepare(insertStr)
|
||||||
|
|
||||||
progressCallback({ progress: 0, id: progressCounterId })
|
progressCallback({ progress: 0, id: progressCounterId })
|
||||||
90
src/lib/database/_statements.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import registerPromiseWorker from 'promise-worker/register'
|
import registerPromiseWorker from 'promise-worker/register'
|
||||||
import Sql from '@/sql'
|
import Sql from './_sql'
|
||||||
|
|
||||||
const sqlReady = Sql.build()
|
const sqlReady = Sql.build()
|
||||||
|
|
||||||
@@ -8,10 +8,18 @@ function processMsg (sql) {
|
|||||||
switch (data && data.action) {
|
switch (data && data.action) {
|
||||||
case 'open':
|
case 'open':
|
||||||
return sql.open(data.buffer)
|
return sql.open(data.buffer)
|
||||||
|
case 'reopen':
|
||||||
|
return sql.open(sql.export())
|
||||||
case 'exec':
|
case 'exec':
|
||||||
return sql.exec(data.sql, data.params)
|
return sql.exec(data.sql, data.params)
|
||||||
case 'import':
|
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':
|
case 'export':
|
||||||
return sql.export()
|
return sql.export()
|
||||||
case 'close':
|
case 'close':
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import sqliteParser from 'sqlite-parser'
|
import stms from './_statements'
|
||||||
import fu from '@/file.utils'
|
import fu from '@/lib/utils/fileIo'
|
||||||
// We can import workers like so because of worker-loader:
|
// We can import workers like so because of worker-loader:
|
||||||
// https://webpack.js.org/loaders/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:
|
// Use promise-worker in order to turn worker into the promise based one:
|
||||||
// https://github.com/nolanlawson/promise-worker
|
// https://github.com/nolanlawson/promise-worker
|
||||||
@@ -20,6 +20,8 @@ export default {
|
|||||||
let progressCounterIds = 0
|
let progressCounterIds = 0
|
||||||
class Database {
|
class Database {
|
||||||
constructor (worker) {
|
constructor (worker) {
|
||||||
|
this.dbName = null
|
||||||
|
this.schema = null
|
||||||
this.worker = worker
|
this.worker = worker
|
||||||
this.pw = new PromiseWorker(worker)
|
this.pw = new PromiseWorker(worker)
|
||||||
|
|
||||||
@@ -50,33 +52,35 @@ class Database {
|
|||||||
delete this.importProgresses[id]
|
delete this.importProgresses[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
async createDb (name, data, progressCounterId) {
|
async addTableFromCsv (tabName, data, progressCounterId) {
|
||||||
const result = await this.pw.postMessage({
|
const result = await this.pw.postMessage({
|
||||||
action: 'import',
|
action: 'import',
|
||||||
columns: data.columns,
|
columns: data.columns,
|
||||||
values: data.values,
|
values: data.values,
|
||||||
progressCounterId
|
progressCounterId,
|
||||||
|
tabName
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
throw new Error(result.error)
|
throw new Error(result.error)
|
||||||
}
|
}
|
||||||
|
this.dbName = this.dbName || 'database'
|
||||||
return await this.getSchema(name)
|
this.refreshSchema()
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadDb (file) {
|
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 })
|
const res = await this.pw.postMessage({ action: 'open', buffer: fileContent })
|
||||||
|
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
throw new Error(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 = `
|
const getSchemaSql = `
|
||||||
SELECT name, sql
|
SELECT name, sql
|
||||||
FROM sqlite_master
|
FROM sqlite_master
|
||||||
@@ -85,21 +89,21 @@ class Database {
|
|||||||
const result = await this.execute(getSchemaSql)
|
const result = await this.execute(getSchemaSql)
|
||||||
// Parse DDL statements to get column names and types
|
// Parse DDL statements to get column names and types
|
||||||
const parsedSchema = []
|
const parsedSchema = []
|
||||||
result.values.forEach(item => {
|
if (result && result.values) {
|
||||||
parsedSchema.push({
|
result.values.forEach(item => {
|
||||||
name: item[0],
|
parsedSchema.push({
|
||||||
columns: getColumns(item[1])
|
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) {
|
async execute (commands) {
|
||||||
|
await this.pw.postMessage({ action: 'reopen' })
|
||||||
const results = await this.pw.postMessage({ action: 'exec', sql: commands })
|
const results = await this.pw.postMessage({ action: 'exec', sql: commands })
|
||||||
|
|
||||||
if (results.error) {
|
if (results.error) {
|
||||||
@@ -117,48 +121,27 @@ class Database {
|
|||||||
}
|
}
|
||||||
fu.exportToFile(data, fileName)
|
fu.exportToFile(data, fileName)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function getAst (sql) {
|
async validateTableName (name) {
|
||||||
// There is a bug is sqlite-parser
|
if (name.startsWith('sqlite_')) {
|
||||||
// It throws an error if tokenizer has an arguments:
|
throw new Error("Table name can't start with sqlite_")
|
||||||
// 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 })
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
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 _
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import fu from '@/file.utils'
|
import fu from '@/lib/utils/fileIo'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getStoredQueries () {
|
getStoredQueries () {
|
||||||
@@ -1,4 +1,15 @@
|
|||||||
export default {
|
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') {
|
exportToFile (str, fileName, type = 'octet/stream') {
|
||||||
// Create downloader
|
// Create downloader
|
||||||
const downloader = document.createElement('a')
|
const downloader = document.createElement('a')
|
||||||
15
src/lib/utils/time.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import App from './App.vue'
|
import App from '@/App.vue'
|
||||||
import router from './router'
|
import router from '@/router'
|
||||||
import store from './store'
|
import store from '@/store'
|
||||||
import { VuePlugin } from 'vuera'
|
import { VuePlugin } from 'vuera'
|
||||||
import VModal from 'vue-js-modal'
|
import VModal from 'vue-js-modal'
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ import '@/assets/styles/tooltips.css'
|
|||||||
import '@/assets/styles/messages.css'
|
import '@/assets/styles/messages.css'
|
||||||
|
|
||||||
if (!['localhost', '127.0.0.1'].includes(location.hostname)) {
|
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)
|
Vue.use(VuePlugin)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function invokeServiceWorkerUpdateFlow (registration) {
|
|||||||
|
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
window.addEventListener('load', async () => {
|
window.addEventListener('load', async () => {
|
||||||
const registration = await navigator.serviceWorker.register('/service-worker.js')
|
const registration = await navigator.serviceWorker.register('service-worker.js')
|
||||||
// ensure the case when the updatefound event was missed is also handled
|
// ensure the case when the updatefound event was missed is also handled
|
||||||
// by re-invoking the prompt when there's a waiting Service Worker
|
// by re-invoking the prompt when there's a waiting Service Worker
|
||||||
if (registration.waiting) {
|
if (registration.waiting) {
|
||||||
50
src/router.js
Normal 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
|
||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,112 +1,11 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Vuex from 'vuex'
|
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)
|
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({
|
export default new Vuex.Store({
|
||||||
state,
|
state,
|
||||||
mutations,
|
mutations,
|
||||||
|
|||||||
59
src/store/mutations.js
Normal 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
@@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
tabs: [],
|
||||||
|
currentTab: null,
|
||||||
|
currentTabId: null,
|
||||||
|
untitledLastIndex: 0,
|
||||||
|
predefinedQueries: [],
|
||||||
|
db: null
|
||||||
|
}
|
||||||
36
src/time.js
@@ -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$/, '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 can’t 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>
|
|
||||||
90
src/views/Main/AppDiagnosticInfo.vue
Normal 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>
|
||||||
@@ -5,11 +5,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="db">
|
<div id="db">
|
||||||
<div @click="schemaVisible = !schemaVisible" class="db-name">
|
<div @click="schemaVisible = !schemaVisible" class="db-name">
|
||||||
<tree-chevron :expanded="schemaVisible"/>
|
<tree-chevron v-show="schema.length > 0" :expanded="schemaVisible"/>
|
||||||
{{ dbName }}
|
{{ dbName }}
|
||||||
</div>
|
</div>
|
||||||
<db-uploader id="db-edit" type="small" />
|
<db-uploader id="db-edit" type="small" />
|
||||||
<export-icon tooltip="Export database" @click="exportToFile"/>
|
<export-icon tooltip="Export database" @click="exportToFile"/>
|
||||||
|
<add-table-icon @click="addCsv"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="schemaVisible" class="schema">
|
<div v-show="schemaVisible" class="schema">
|
||||||
<table-description
|
<table-description
|
||||||
@@ -19,15 +20,26 @@
|
|||||||
:columns="table.columns"
|
:columns="table.columns"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!--Parse csv dialog -->
|
||||||
|
<csv-import
|
||||||
|
ref="addCsv"
|
||||||
|
:file="file"
|
||||||
|
:db="$store.state.db"
|
||||||
|
dialog-name="addCsv"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import TableDescription from '@/components/TableDescription'
|
import fIo from '@/lib/utils/fileIo'
|
||||||
|
import TableDescription from './TableDescription'
|
||||||
import TextField from '@/components/TextField'
|
import TextField from '@/components/TextField'
|
||||||
import TreeChevron from '@/components/svg/treeChevron'
|
import TreeChevron from '@/components/svg/treeChevron'
|
||||||
import DbUploader from '@/components/DbUploader'
|
import DbUploader from '@/components/DbUploader'
|
||||||
import ExportIcon from '@/components/svg/export'
|
import ExportIcon from '@/components/svg/export'
|
||||||
|
import AddTableIcon from '@/components/svg/addTable'
|
||||||
|
import CsvImport from '@/components/CsvImport'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Schema',
|
name: 'Schema',
|
||||||
@@ -36,33 +48,44 @@ export default {
|
|||||||
TextField,
|
TextField,
|
||||||
TreeChevron,
|
TreeChevron,
|
||||||
DbUploader,
|
DbUploader,
|
||||||
ExportIcon
|
ExportIcon,
|
||||||
|
AddTableIcon,
|
||||||
|
CsvImport
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
schemaVisible: true,
|
schemaVisible: true,
|
||||||
filter: null
|
filter: null,
|
||||||
|
file: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
schema () {
|
schema () {
|
||||||
if (!this.$store.state.schema) {
|
if (!this.$store.state.db.schema) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
return !this.filter
|
return !this.filter
|
||||||
? this.$store.state.schema
|
? this.$store.state.db.schema
|
||||||
: this.$store.state.schema.filter(
|
: this.$store.state.db.schema.filter(
|
||||||
table => table.name.toUpperCase().indexOf(this.filter.toUpperCase()) !== -1
|
table => table.name.toUpperCase().indexOf(this.filter.toUpperCase()) !== -1
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
dbName () {
|
dbName () {
|
||||||
return this.$store.state.dbName
|
return this.$store.state.db.dbName
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
exportToFile () {
|
exportToFile () {
|
||||||
this.$store.state.db.export(`${this.dbName}.sqlite`)
|
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%;
|
width: 100%;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
box-sizing: border-box;
|
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;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
.schema, .db-name {
|
.schema, .db-name {
|
||||||
@@ -28,7 +28,7 @@ import plotly from 'plotly.js/dist/plotly'
|
|||||||
import 'react-chart-editor/lib/react-chart-editor.min.css'
|
import 'react-chart-editor/lib/react-chart-editor.min.css'
|
||||||
|
|
||||||
import PlotlyEditor from 'react-chart-editor'
|
import PlotlyEditor from 'react-chart-editor'
|
||||||
import chart from '@/chart'
|
import chartHelper from './chartHelper'
|
||||||
import dereference from 'react-chart-editor/lib/lib/dereference'
|
import dereference from 'react-chart-editor/lib/lib/dereference'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -49,10 +49,10 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
dataSources () {
|
dataSources () {
|
||||||
return chart.getDataSourcesFromSqlResult(this.sqlResult)
|
return chartHelper.getDataSourcesFromSqlResult(this.sqlResult)
|
||||||
},
|
},
|
||||||
dataSourceOptions () {
|
dataSourceOptions () {
|
||||||
return chart.getOptionsFromDataSources(this.dataSources)
|
return chartHelper.getOptionsFromDataSources(this.dataSources)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -71,7 +71,7 @@ export default {
|
|||||||
this.$emit('update')
|
this.$emit('update')
|
||||||
},
|
},
|
||||||
getChartStateForSave () {
|
getChartStateForSave () {
|
||||||
return chart.getChartStateForSave(this.state, this.dataSources)
|
return chartHelper.getChartStateForSave(this.state, this.dataSources)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@ import CM from 'codemirror'
|
|||||||
import 'codemirror/addon/hint/show-hint.js'
|
import 'codemirror/addon/hint/show-hint.js'
|
||||||
import 'codemirror/addon/hint/sql-hint.js'
|
import 'codemirror/addon/hint/sql-hint.js'
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
import { debounce } from 'debounce'
|
|
||||||
|
|
||||||
export function getHints (cm, options) {
|
export function getHints (cm, options) {
|
||||||
const token = cm.getTokenAt(cm.getCursor()).string.toUpperCase()
|
const token = cm.getTokenAt(cm.getCursor()).string.toUpperCase()
|
||||||
@@ -18,28 +17,34 @@ export function getHints (cm, options) {
|
|||||||
const hintOptions = {
|
const hintOptions = {
|
||||||
get tables () {
|
get tables () {
|
||||||
const tables = {}
|
const tables = {}
|
||||||
if (store.state.schema) {
|
if (store.state.db.schema) {
|
||||||
store.state.schema.forEach(table => {
|
store.state.db.schema.forEach(table => {
|
||||||
tables[table.name] = table.columns.map(column => column.name)
|
tables[table.name] = table.columns.map(column => column.name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return tables
|
return tables
|
||||||
},
|
},
|
||||||
|
get defaultTable () {
|
||||||
|
const schema = store.state.db.schema
|
||||||
|
return schema && schema.length === 1 ? schema[0].name : null
|
||||||
|
},
|
||||||
completeSingle: false,
|
completeSingle: false,
|
||||||
completeOnSingleClick: true,
|
completeOnSingleClick: true,
|
||||||
alignWithWord: false
|
alignWithWord: false
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export function showHintOnDemand (editor) {
|
||||||
show: debounce(function (editor) {
|
CM.showHint(editor, getHints, hintOptions)
|
||||||
// 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)
|
export default function showHint (editor) {
|
||||||
const tokenType = token.type
|
// Don't show autocomplete after a space or semicolon or in string literals
|
||||||
if (tokenType === 'string' || !ch || ch === ' ' || ch === ';') {
|
const token = editor.getTokenAt(editor.getCursor())
|
||||||
return
|
const ch = token.string.slice(-1)
|
||||||
}
|
const tokenType = token.type
|
||||||
|
if (tokenType === 'string' || !ch || ch === ' ' || ch === ';') {
|
||||||
CM.showHint(editor, getHints, hintOptions)
|
return
|
||||||
}, 400)
|
}
|
||||||
|
|
||||||
|
CM.showHint(editor, getHints, hintOptions)
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="codemirror-container">
|
<div class="codemirror-container">
|
||||||
<codemirror v-model="query" :options="cmOptions" @changes="onChange" />
|
<codemirror ref="cm" v-model="query" :options="cmOptions" @changes="onChange" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import hint from '@/hint'
|
import showHint, { showHintOnDemand } from './hint'
|
||||||
|
import time from '@/lib/utils/time'
|
||||||
import { codemirror } from 'vue-codemirror'
|
import { codemirror } from 'vue-codemirror'
|
||||||
import 'codemirror/lib/codemirror.css'
|
import 'codemirror/lib/codemirror.css'
|
||||||
import 'codemirror/mode/sql/sql.js'
|
import 'codemirror/mode/sql/sql.js'
|
||||||
@@ -27,8 +28,8 @@ export default {
|
|||||||
theme: 'neo',
|
theme: 'neo',
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
line: true,
|
line: true,
|
||||||
autofocus: true,
|
autoRefresh: true,
|
||||||
autoRefresh: true
|
extraKeys: { 'Ctrl-Space': showHintOnDemand }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -38,7 +39,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onChange: hint.show
|
onChange: time.debounce(showHint, 400),
|
||||||
|
focus () {
|
||||||
|
this.$refs.cm.codemirror.focus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
>
|
>
|
||||||
<template #left-pane>
|
<template #left-pane>
|
||||||
<div class="query-editor">
|
<div class="query-editor">
|
||||||
<sql-editor v-model="query" />
|
<sql-editor ref="sqlEditor" v-model="query" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #right-pane>
|
<template #right-pane>
|
||||||
@@ -21,7 +21,8 @@
|
|||||||
>
|
>
|
||||||
Run your query and get results here
|
Run your query and get results here
|
||||||
</div>
|
</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...
|
Fetching results...
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -30,10 +31,8 @@
|
|||||||
>
|
>
|
||||||
No rows retrieved according to your query
|
No rows retrieved according to your query
|
||||||
</div>
|
</div>
|
||||||
<div v-show="error" class="table-preview error">
|
<logs v-if="error" :messages="[error]"/>
|
||||||
{{ error }}
|
<sql-table v-if="result" :data-set="result" :time="time" :height="tableViewHeight" />
|
||||||
</div>
|
|
||||||
<sql-table v-if="result" :data-set="result" :height="tableViewHeight" />
|
|
||||||
</div>
|
</div>
|
||||||
<chart
|
<chart
|
||||||
:visible="view === 'chart'"
|
:visible="view === 'chart'"
|
||||||
@@ -50,10 +49,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import SqlTable from '@/components/SqlTable'
|
import SqlTable from '@/components/SqlTable'
|
||||||
import SqlEditor from '@/components/SqlEditor'
|
|
||||||
import Splitpanes from '@/components/Splitpanes'
|
import Splitpanes from '@/components/Splitpanes'
|
||||||
import ViewSwitcher from '@/components/ViewSwitcher'
|
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||||
import Chart from '@/components/Chart'
|
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 {
|
export default {
|
||||||
name: 'Tab',
|
name: 'Tab',
|
||||||
@@ -63,7 +65,9 @@ export default {
|
|||||||
SqlTable,
|
SqlTable,
|
||||||
Splitpanes,
|
Splitpanes,
|
||||||
ViewSwitcher,
|
ViewSwitcher,
|
||||||
Chart
|
Chart,
|
||||||
|
LoadingIndicator,
|
||||||
|
Logs
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -73,7 +77,8 @@ export default {
|
|||||||
tableViewHeight: 0,
|
tableViewHeight: 0,
|
||||||
isGettingResults: false,
|
isGettingResults: false,
|
||||||
error: null,
|
error: null,
|
||||||
resizeObserver: null
|
resizeObserver: null,
|
||||||
|
time: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -81,9 +86,6 @@ export default {
|
|||||||
return this.id === this.$store.state.currentTabId
|
return this.id === this.$store.state.currentTabId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
|
||||||
this.$store.commit('setCurrentTab', this)
|
|
||||||
},
|
|
||||||
mounted () {
|
mounted () {
|
||||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||||
this.resizeObserver.observe(this.$refs.bottomPane)
|
this.resizeObserver.observe(this.$refs.bottomPane)
|
||||||
@@ -93,9 +95,14 @@ export default {
|
|||||||
this.resizeObserver.unobserve(this.$refs.bottomPane)
|
this.resizeObserver.unobserve(this.$refs.bottomPane)
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
isActive () {
|
isActive: {
|
||||||
if (this.isActive) {
|
immediate: true,
|
||||||
this.$store.commit('setCurrentTab', this)
|
async handler () {
|
||||||
|
if (this.isActive) {
|
||||||
|
this.$store.commit('setCurrentTab', this)
|
||||||
|
await this.$nextTick()
|
||||||
|
this.$refs.sqlEditor.focus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
query () {
|
query () {
|
||||||
@@ -110,12 +117,16 @@ export default {
|
|||||||
this.error = null
|
this.error = null
|
||||||
const state = this.$store.state
|
const state = this.$store.state
|
||||||
try {
|
try {
|
||||||
|
const start = new Date()
|
||||||
this.result = await state.db.execute(this.query + ';')
|
this.result = await state.db.execute(this.query + ';')
|
||||||
const schema = await state.db.getSchema(state.dbName)
|
this.time = time.getPeriod(start, new Date())
|
||||||
this.$store.commit('saveSchema', schema)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.error = err
|
this.error = {
|
||||||
|
type: 'error',
|
||||||
|
message: err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
state.db.refreshSchema()
|
||||||
this.isGettingResults = false
|
this.isGettingResults = false
|
||||||
},
|
},
|
||||||
handleResize () {
|
handleResize () {
|
||||||
@@ -131,12 +142,12 @@ export default {
|
|||||||
calculateTableHeight () {
|
calculateTableHeight () {
|
||||||
const bottomPane = this.$refs.bottomPane
|
const bottomPane = this.$refs.bottomPane
|
||||||
// 88 - view swittcher height
|
// 88 - view swittcher height
|
||||||
// 42 - table footer width
|
// 34 - table footer width
|
||||||
// 30 - desirable space after the table
|
// 12 - desirable space after the table
|
||||||
// 5 - padding-bottom of rounded table container
|
// 5 - padding-bottom of rounded table container
|
||||||
// 40 - height of table header
|
// 35 - height of table header
|
||||||
const freeSpace = bottomPane.offsetHeight - 88 - 42 - 30 - 5 - 40
|
const freeSpace = bottomPane.offsetHeight - 88 - 34 - 12 - 5 - 35
|
||||||
this.tableViewHeight = freeSpace - (freeSpace % 40)
|
this.tableViewHeight = freeSpace - (freeSpace % 35)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,11 +194,32 @@ export default {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-preview.error {
|
.result-in-progress {
|
||||||
color: var(--color-text-error);
|
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 {
|
@keyframes show-loader {
|
||||||
text-transform: capitalize;
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
99% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Tab from '@/components/Tab'
|
import Tab from './Tab'
|
||||||
import CloseIcon from '@/components/svg/close'
|
import CloseIcon from '@/components/svg/close'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
62
src/views/Main/Editor/index.vue
Normal 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>
|
||||||
@@ -3,8 +3,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<router-link to="/editor">Editor</router-link>
|
<router-link to="/editor">Editor</router-link>
|
||||||
<router-link to="/my-queries">My queries</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>
|
<div id="nav-buttons">
|
||||||
<button
|
<button
|
||||||
id="run-btn"
|
id="run-btn"
|
||||||
v-if="currentQuery && $route.path === '/editor'"
|
v-if="currentQuery && $route.path === '/editor'"
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
</button>
|
</button>
|
||||||
|
<app-diagnostic-info />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--Save Query dialog -->
|
<!--Save Query dialog -->
|
||||||
@@ -62,13 +64,15 @@
|
|||||||
<script>
|
<script>
|
||||||
import TextField from '@/components/TextField'
|
import TextField from '@/components/TextField'
|
||||||
import CloseIcon from '@/components/svg/close'
|
import CloseIcon from '@/components/svg/close'
|
||||||
import storedQueries from '@/storedQueries'
|
import storedQueries from '@/lib/storedQueries'
|
||||||
|
import AppDiagnosticInfo from './AppDiagnosticInfo'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MainMenu',
|
name: 'MainMenu',
|
||||||
components: {
|
components: {
|
||||||
TextField,
|
TextField,
|
||||||
CloseIcon
|
CloseIcon,
|
||||||
|
AppDiagnosticInfo
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -96,7 +100,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
runDisabled () {
|
runDisabled () {
|
||||||
return this.currentQuery && (!this.$store.state.schema || !this.currentQuery.query)
|
return this.currentQuery && (!this.$store.state.db || !this.currentQuery.query)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
@@ -212,7 +216,7 @@ nav {
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
padding: 0 52px;
|
padding: 0 16px 0 52px;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
@@ -237,4 +241,8 @@ button {
|
|||||||
#save-note img {
|
#save-note img {
|
||||||
margin: -3px 6px 0 0;
|
margin: -3px 6px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#nav-buttons {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -141,16 +141,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import RenameIcon from '@/components/svg/rename'
|
import RenameIcon from './svg/rename'
|
||||||
import CopyIcon from '@/components/svg/copy'
|
import CopyIcon from './svg/copy'
|
||||||
import ExportIcon from '@/components/svg/export'
|
import ExportIcon from '@/components/svg/export'
|
||||||
import DeleteIcon from '@/components/svg/delete'
|
import DeleteIcon from './svg/delete'
|
||||||
import CloseIcon from '@/components/svg/close'
|
import CloseIcon from '@/components/svg/close'
|
||||||
import TextField from '@/components/TextField'
|
import TextField from '@/components/TextField'
|
||||||
import CheckBox from '@/components/CheckBox'
|
import CheckBox from '@/components/CheckBox'
|
||||||
import tooltipMixin from '@/mixins/tooltips'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
import storedQueries from '@/storedQueries'
|
import storedQueries from '@/lib/storedQueries'
|
||||||
import fu from '@/file.utils'
|
import fu from '@/lib/utils/fileIo'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MyQueries',
|
name: 'MyQueries',
|
||||||
@@ -448,6 +448,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rounded-bg {
|
.rounded-bg {
|
||||||
|
padding-top: 40px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: 1500px;
|
max-width: 1500px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import tooltipMixin from '@/mixins/tooltips'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CopyIcon',
|
name: 'CopyIcon',
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import tooltipMixin from '@/mixins/tooltips'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DeleteIcon',
|
name: 'DeleteIcon',
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import tooltipMixin from '@/mixins/tooltips'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RenameIcon',
|
name: 'RenameIcon',
|
||||||
@@ -8,11 +8,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MainMenu from '@/components/MainMenu'
|
import MainMenu from './MainMenu'
|
||||||
import '@/assets/styles/scrollbars.css'
|
import '@/assets/styles/scrollbars.css'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MainView',
|
name: 'Main',
|
||||||
components: { MainMenu }
|
components: { MainMenu }
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
<div id="note">
|
<div id="note">
|
||||||
Sqliteviz is fully client-side. Your database never leaves your computer.
|
Sqliteviz is fully client-side. Your database never leaves your computer.
|
||||||
</div>
|
</div>
|
||||||
<button id ="skip" class="secondary" @click="$router.push('/editor')">
|
<button id="skip" class="secondary" @click="$router.push('/editor')">
|
||||||
Skip database loading
|
Create empty database
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
import DbUploader from '@/components/DbUploader'
|
import DbUploader from '@/components/DbUploader'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Home',
|
name: 'Welcome',
|
||||||
components: { DbUploader }
|
components: { DbUploader }
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
706
tests/components/CsvImport/CsvImport.spec.js
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { mount, shallowMount } from '@vue/test-utils'
|
import { mount, shallowMount } from '@vue/test-utils'
|
||||||
import DelimiterSelector from '@/components/DelimiterSelector'
|
import DelimiterSelector from '@/components/CsvImport/DelimiterSelector'
|
||||||
|
|
||||||
describe('DelimiterSelector', async () => {
|
describe('DelimiterSelector', async () => {
|
||||||
it('shows the name of value', async () => {
|
it('shows the name of value', async () => {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import csv from '@/csv'
|
import csv from '@/components/CsvImport/csv'
|
||||||
import Papa from 'papaparse'
|
import Papa from 'papaparse'
|
||||||
|
|
||||||
describe('csv.js', () => {
|
describe('csv.js', () => {
|
||||||
@@ -15,7 +15,7 @@ describe('csv.js', () => {
|
|||||||
{ id: 2, name: 'bar' }
|
{ id: 2, name: 'bar' }
|
||||||
],
|
],
|
||||||
meta: {
|
meta: {
|
||||||
fields: ['id', 'name']
|
fields: ['id', 'name ']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
expect(csv.getResult(source)).to.eql({
|
expect(csv.getResult(source)).to.eql({
|
||||||
@@ -46,7 +46,7 @@ describe('csv.js', () => {
|
|||||||
|
|
||||||
it('parse resolves', async () => {
|
it('parse resolves', async () => {
|
||||||
sinon.stub(Papa, 'parse').callsFake((file, config) => {
|
sinon.stub(Papa, 'parse').callsFake((file, config) => {
|
||||||
config.complete({
|
config.chunk({
|
||||||
data: [
|
data: [
|
||||||
[1, 'foo'],
|
[1, 'foo'],
|
||||||
[2, 'bar']
|
[2, 'bar']
|
||||||
@@ -72,6 +72,7 @@ describe('csv.js', () => {
|
|||||||
truncated: true
|
truncated: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
config.complete()
|
||||||
})
|
})
|
||||||
const file = {}
|
const file = {}
|
||||||
const result = await csv.parse(file)
|
const result = await csv.parse(file)
|
||||||
@@ -112,4 +113,96 @@ describe('csv.js', () => {
|
|||||||
const file = {}
|
const file = {}
|
||||||
await expect(csv.parse(file)).to.be.rejectedWith(err)
|
await expect(csv.parse(file)).to.be.rejectedWith(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('concat chunks', async () => {
|
||||||
|
sinon.stub(Papa, 'parse').callsFake((file, config) => {
|
||||||
|
config.chunk({
|
||||||
|
data: [
|
||||||
|
[1, 'foo'],
|
||||||
|
[2, 'bar']
|
||||||
|
],
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: 'Quotes',
|
||||||
|
code: 'MissingQuotes',
|
||||||
|
message: 'Quote is missed',
|
||||||
|
row: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Delimiter',
|
||||||
|
code: 'UndetectableDelimiter',
|
||||||
|
message: 'Comma was used as a standart delimiter',
|
||||||
|
row: 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
delimiter: ',',
|
||||||
|
linebreak: '\n',
|
||||||
|
aborted: false,
|
||||||
|
truncated: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
config.chunk({
|
||||||
|
data: [
|
||||||
|
[3, 'baz'],
|
||||||
|
[4, 'boo']
|
||||||
|
],
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: 'Delimiter',
|
||||||
|
code: 'UndetectableDelimiter',
|
||||||
|
message: 'Comma was used as a standart delimiter',
|
||||||
|
row: 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
delimiter: ',',
|
||||||
|
linebreak: '\n',
|
||||||
|
aborted: false,
|
||||||
|
truncated: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
config.complete()
|
||||||
|
})
|
||||||
|
const file = {}
|
||||||
|
const result = await csv.parse(file)
|
||||||
|
|
||||||
|
expect(result).to.eql({
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[1, 'foo'],
|
||||||
|
[2, 'bar'],
|
||||||
|
[3, 'baz'],
|
||||||
|
[4, 'boo']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
delimiter: ',',
|
||||||
|
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.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'UndetectableDelimiter',
|
||||||
|
message: 'Comma was used as a standart delimiter',
|
||||||
|
row: 0,
|
||||||
|
type: 'info',
|
||||||
|
hint: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'UndetectableDelimiter',
|
||||||
|
message: 'Comma was used as a standart delimiter',
|
||||||
|
row: 2,
|
||||||
|
type: 'info',
|
||||||
|
hint: undefined
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -2,39 +2,41 @@ import { expect } from 'chai'
|
|||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
import { shallowMount, mount } from '@vue/test-utils'
|
import { shallowMount, mount } from '@vue/test-utils'
|
||||||
import DbUploader from '@/components/DbUploader.vue'
|
import DbUploader from '@/components/DbUploader'
|
||||||
import fu from '@/file.utils'
|
import fu from '@/lib/utils/fileIo'
|
||||||
import database from '@/database'
|
import database from '@/lib/database'
|
||||||
import csv from '@/csv'
|
|
||||||
|
|
||||||
describe('DbUploader.vue', () => {
|
describe('DbUploader.vue', () => {
|
||||||
let state = {}
|
let state = {}
|
||||||
let mutations = {}
|
let mutations = {}
|
||||||
let store = {}
|
let store = {}
|
||||||
|
let place
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// mock store state and mutations
|
// mock store state and mutations
|
||||||
state = {}
|
state = {}
|
||||||
mutations = {
|
mutations = {
|
||||||
saveSchema: sinon.stub(),
|
|
||||||
setDb: sinon.stub()
|
setDb: sinon.stub()
|
||||||
}
|
}
|
||||||
store = new Vuex.Store({ state, mutations })
|
store = new Vuex.Store({ state, mutations })
|
||||||
|
|
||||||
|
place = document.createElement('div')
|
||||||
|
document.body.appendChild(place)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
sinon.restore()
|
sinon.restore()
|
||||||
|
place.remove()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('loads db on click and redirects to /editor', async () => {
|
it('loads db on click and redirects to /editor', async () => {
|
||||||
// mock getting a file from user
|
// mock getting a file from user
|
||||||
const file = {}
|
const file = { name: 'test.db' }
|
||||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||||
|
|
||||||
// mock db loading
|
// mock db loading
|
||||||
const schema = {}
|
|
||||||
const db = {
|
const db = {
|
||||||
loadDb: sinon.stub().resolves(schema)
|
loadDb: sinon.stub().resolves()
|
||||||
}
|
}
|
||||||
sinon.stub(database, 'getNewDatabase').returns(db)
|
sinon.stub(database, 'getNewDatabase').returns(db)
|
||||||
|
|
||||||
@@ -44,22 +46,27 @@ describe('DbUploader.vue', () => {
|
|||||||
|
|
||||||
// mount the component
|
// mount the component
|
||||||
const wrapper = shallowMount(DbUploader, {
|
const wrapper = shallowMount(DbUploader, {
|
||||||
|
attachTo: place,
|
||||||
store,
|
store,
|
||||||
mocks: { $router, $route }
|
mocks: { $router, $route },
|
||||||
|
propsData: {
|
||||||
|
type: 'illustrated'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await wrapper.find('.drop-area').trigger('click')
|
await wrapper.find('.drop-area').trigger('click')
|
||||||
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
|
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
|
||||||
await db.loadDb.returnValues[0]
|
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)
|
expect($router.push.calledOnceWith('/editor')).to.equal(true)
|
||||||
|
wrapper.destroy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('loads db on drop and redirects to /editor', async () => {
|
it('loads db on drop and redirects to /editor', async () => {
|
||||||
// mock db loading
|
// mock db loading
|
||||||
const schema = {}
|
|
||||||
const db = {
|
const db = {
|
||||||
loadDb: sinon.stub().resolves(schema)
|
loadDb: sinon.stub().resolves()
|
||||||
}
|
}
|
||||||
sinon.stub(database, 'getNewDatabase').returns(db)
|
sinon.stub(database, 'getNewDatabase').returns(db)
|
||||||
|
|
||||||
@@ -69,12 +76,16 @@ describe('DbUploader.vue', () => {
|
|||||||
|
|
||||||
// mount the component
|
// mount the component
|
||||||
const wrapper = shallowMount(DbUploader, {
|
const wrapper = shallowMount(DbUploader, {
|
||||||
|
attachTo: place,
|
||||||
store,
|
store,
|
||||||
mocks: { $router, $route }
|
mocks: { $router, $route },
|
||||||
|
propsData: {
|
||||||
|
type: 'illustrated'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// mock a file dropped by a user
|
// mock a file dropped by a user
|
||||||
const file = {}
|
const file = { name: 'test.db' }
|
||||||
const dropData = { dataTransfer: new DataTransfer() }
|
const dropData = { dataTransfer: new DataTransfer() }
|
||||||
Object.defineProperty(dropData.dataTransfer, 'files', {
|
Object.defineProperty(dropData.dataTransfer, 'files', {
|
||||||
value: [file],
|
value: [file],
|
||||||
@@ -84,19 +95,20 @@ describe('DbUploader.vue', () => {
|
|||||||
await wrapper.find('.drop-area').trigger('drop', dropData)
|
await wrapper.find('.drop-area').trigger('drop', dropData)
|
||||||
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
|
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
|
||||||
await db.loadDb.returnValues[0]
|
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)
|
expect($router.push.calledOnceWith('/editor')).to.equal(true)
|
||||||
|
wrapper.destroy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("doesn't redirect if already on /editor", async () => {
|
it("doesn't redirect if already on /editor", async () => {
|
||||||
// mock getting a file from user
|
// mock getting a file from user
|
||||||
const file = {}
|
const file = { name: 'test.db' }
|
||||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||||
|
|
||||||
// mock db loading
|
// mock db loading
|
||||||
const schema = {}
|
|
||||||
const db = {
|
const db = {
|
||||||
loadDb: sinon.stub().resolves(schema)
|
loadDb: sinon.stub().resolves()
|
||||||
}
|
}
|
||||||
sinon.stub(database, 'getNewDatabase').returns(db)
|
sinon.stub(database, 'getNewDatabase').returns(db)
|
||||||
|
|
||||||
@@ -106,736 +118,82 @@ describe('DbUploader.vue', () => {
|
|||||||
|
|
||||||
// mount the component
|
// mount the component
|
||||||
const wrapper = shallowMount(DbUploader, {
|
const wrapper = shallowMount(DbUploader, {
|
||||||
|
attachTo: place,
|
||||||
store,
|
store,
|
||||||
mocks: { $router, $route }
|
mocks: { $router, $route },
|
||||||
|
propsData: {
|
||||||
|
type: 'illustrated'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await wrapper.find('.drop-area').trigger('click')
|
await wrapper.find('.drop-area').trigger('click')
|
||||||
await db.loadDb.returnValues[0]
|
await db.loadDb.returnValues[0]
|
||||||
|
await wrapper.vm.animationPromise
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
expect($router.push.called).to.equal(false)
|
expect($router.push.called).to.equal(false)
|
||||||
})
|
wrapper.destroy()
|
||||||
})
|
|
||||||
|
|
||||||
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()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows parse dialog if gets csv file', async () => {
|
it('shows parse dialog if gets csv file', async () => {
|
||||||
sinon.stub(csv, 'parse').resolves({
|
// mock getting a file from user
|
||||||
delimiter: '|',
|
const file = { name: 'test.csv' }
|
||||||
data: {
|
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||||
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
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.find('.drop-area').trigger('click')
|
// mock router
|
||||||
await csv.parse.returnValues[0]
|
const $router = { push: sinon.stub() }
|
||||||
await wrapper.vm.animationPromise
|
const $route = { path: '/editor' }
|
||||||
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)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('reparses when parameters changes', async () => {
|
// mount the component
|
||||||
const parse = sinon.stub(csv, 'parse')
|
const wrapper = mount(DbUploader, {
|
||||||
parse.onCall(0).resolves({
|
attachTo: place,
|
||||||
delimiter: '|',
|
store,
|
||||||
data: {
|
mocks: { $router, $route },
|
||||||
columns: ['col1', 'col2'],
|
propsData: {
|
||||||
values: [
|
type: 'illustrated'
|
||||||
[1, 'foo']
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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 wrapper.find('.drop-area').trigger('click')
|
||||||
await csv.parse.returnValues[0]
|
|
||||||
await wrapper.vm.animationPromise
|
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(CsvImport.reset.calledOnce).to.equal(true)
|
||||||
parse.onCall(1).resolves({
|
await wrapper.vm.animationPromise
|
||||||
delimiter: ',',
|
expect(CsvImport.previewCsv.calledOnce).to.equal(true)
|
||||||
data: {
|
await wrapper.vm.$nextTick()
|
||||||
columns: ['col1', 'col2'],
|
expect(CsvImport.open.calledOnce).to.equal(true)
|
||||||
values: [
|
wrapper.destroy()
|
||||||
[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 () => {
|
it('deletes temporary db if CSV import is canceled', async () => {
|
||||||
const parse = sinon.stub(csv, 'parse')
|
// mock getting a file from user
|
||||||
parse.onCall(0).resolves({
|
const file = { name: 'test.csv' }
|
||||||
delimiter: '|',
|
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
// mock router
|
||||||
values: [
|
const $router = { push: sinon.stub() }
|
||||||
[1, 'foo']
|
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')
|
const CsvImport = wrapper.vm.$refs.addCsv
|
||||||
await csv.parse.returnValues[0]
|
sinon.stub(CsvImport, 'reset')
|
||||||
await wrapper.vm.animationPromise
|
sinon.stub(CsvImport, 'previewCsv').resolves()
|
||||||
await wrapper.vm.$nextTick()
|
sinon.stub(CsvImport, 'open')
|
||||||
|
|
||||||
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: []
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.find('.drop-area').trigger('click')
|
await wrapper.find('.drop-area').trigger('click')
|
||||||
await csv.parse.returnValues[0]
|
|
||||||
await wrapper.vm.animationPromise
|
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
|
await CsvImport.$emit('cancel')
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
expect(wrapper.vm.newDb).to.equal(null)
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { shallowMount } from '@vue/test-utils'
|
import { shallowMount } from '@vue/test-utils'
|
||||||
import LoadingIndicator from '@/components/LoadingIndicator.vue'
|
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||||
|
|
||||||
describe('LoadingIndicator.vue', () => {
|
describe('LoadingIndicator.vue', () => {
|
||||||
it('Calculates animation class', async () => {
|
it('Calculates animation class', async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { shallowMount } from '@vue/test-utils'
|
import { shallowMount } from '@vue/test-utils'
|
||||||
import Logs from '@/components/Logs.vue'
|
import Logs from '@/components/Logs'
|
||||||
|
|
||||||
let place
|
let place
|
||||||
describe('Logs.vue', () => {
|
describe('Logs.vue', () => {
|
||||||
|
|||||||
@@ -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'))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { shallowMount } from '@vue/test-utils'
|
import { shallowMount } from '@vue/test-utils'
|
||||||
import Splitpanes from '@/components/Splitpanes.vue'
|
import Splitpanes from '@/components/Splitpanes'
|
||||||
|
|
||||||
describe('Splitpanes.vue', () => {
|
describe('Splitpanes.vue', () => {
|
||||||
it('renders correctly - vertical', () => {
|
it('renders correctly - vertical', () => {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import splitter from '@/splitter'
|
import splitter from '@/components/Splitpanes/splitter'
|
||||||
|
|
||||||
describe('splitter.js', () => {
|
describe('splitter.js', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import Pager from '@/components/Pager.vue'
|
import Pager from '@/components/SqlTable/Pager'
|
||||||
|
|
||||||
describe('Pager.vue', () => {
|
describe('Pager.vue', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -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);'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -2,14 +2,14 @@ import chai from 'chai'
|
|||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import chaiAsPromised from 'chai-as-promised'
|
import chaiAsPromised from 'chai-as-promised'
|
||||||
import initSqlJs from 'sql.js'
|
import initSqlJs from 'sql.js'
|
||||||
import Sql from '@/sql'
|
import Sql from '@/lib/database/_sql'
|
||||||
chai.use(chaiAsPromised)
|
chai.use(chaiAsPromised)
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
chai.should()
|
chai.should()
|
||||||
|
|
||||||
const getSQL = initSqlJs()
|
const getSQL = initSqlJs()
|
||||||
|
|
||||||
describe('sql.js', () => {
|
describe('_sql.js', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
sinon.restore()
|
sinon.restore()
|
||||||
})
|
})
|
||||||
@@ -74,8 +74,8 @@ describe('sql.js', () => {
|
|||||||
const progressCallback = sinon.stub()
|
const progressCallback = sinon.stub()
|
||||||
const progressCounterId = 1
|
const progressCounterId = 1
|
||||||
const sql = await Sql.build()
|
const sql = await Sql.build()
|
||||||
sql.import(data.columns, data.values, progressCounterId, progressCallback, 2)
|
sql.import('foo', data.columns, data.values, progressCounterId, progressCallback, 2)
|
||||||
const result = sql.exec('SELECT * from csv_import')
|
const result = sql.exec('SELECT * from foo')
|
||||||
expect(result).to.have.lengthOf(1)
|
expect(result).to.have.lengthOf(1)
|
||||||
expect(result[0].columns).to.eql(['id', 'name'])
|
expect(result[0].columns).to.eql(['id', 'name'])
|
||||||
expect(result[0].values).to.have.lengthOf(4)
|
expect(result[0].values).to.have.lengthOf(4)
|
||||||
@@ -135,7 +135,7 @@ describe('sql.js', () => {
|
|||||||
expect(sql.db.db).to.equal(null)
|
expect(sql.db.db).to.equal(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('overwrites', async () => {
|
it('adds', async () => {
|
||||||
const sql = await Sql.build()
|
const sql = await Sql.build()
|
||||||
sql.exec(`
|
sql.exec(`
|
||||||
CREATE TABLE test (
|
CREATE TABLE test (
|
||||||
@@ -160,12 +160,11 @@ describe('sql.js', () => {
|
|||||||
[4, 'Ron Weasley']
|
[4, 'Ron Weasley']
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
// rewrite the database by import
|
// import adds table
|
||||||
sql.import(data.columns, data.values, 1, sinon.stub(), 2)
|
sql.import('foo', data.columns, data.values, 1, sinon.stub(), 2)
|
||||||
result = sql.exec('SELECT * from csv_import')
|
result = sql.exec('SELECT * from foo')
|
||||||
expect(result[0].values).to.have.lengthOf(4)
|
expect(result[0].values).to.have.lengthOf(4)
|
||||||
|
result = sql.exec('SELECT * from test')
|
||||||
// test table oesn't exists anymore: the db was overwritten
|
expect(result[0].values).to.have.lengthOf(2)
|
||||||
expect(() => { sql.exec('SELECT * from test') }).to.throw('no such table: test')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
62
tests/lib/database/_statements.spec.js
Normal 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' }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -2,8 +2,8 @@ import chai from 'chai'
|
|||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import chaiAsPromised from 'chai-as-promised'
|
import chaiAsPromised from 'chai-as-promised'
|
||||||
import initSqlJs from 'sql.js'
|
import initSqlJs from 'sql.js'
|
||||||
import database from '@/database'
|
import database from '@/lib/database'
|
||||||
import fu from '@/file.utils'
|
import fu from '@/lib/utils/fileIo'
|
||||||
|
|
||||||
chai.use(chaiAsPromised)
|
chai.use(chaiAsPromised)
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
@@ -27,58 +27,39 @@ describe('database.js', () => {
|
|||||||
const tempDb = new SQL.Database()
|
const tempDb = new SQL.Database()
|
||||||
tempDb.run(`CREATE TABLE test (
|
tempDb.run(`CREATE TABLE test (
|
||||||
col1,
|
col1,
|
||||||
col2 integer,
|
col2 integer
|
||||||
col3 decimal(5,2),
|
|
||||||
col4 varchar(30)
|
|
||||||
)`)
|
)`)
|
||||||
|
|
||||||
const data = tempDb.export()
|
const data = tempDb.export()
|
||||||
const buffer = new Blob([data])
|
const buffer = new Blob([data])
|
||||||
buffer.name = 'foo.sqlite'
|
buffer.name = 'foo.sqlite'
|
||||||
|
|
||||||
const { schema, dbName } = await db.loadDb(buffer)
|
sinon.spy(db, 'refreshSchema')
|
||||||
expect(dbName).to.equal('foo')
|
|
||||||
|
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).to.have.lengthOf(1)
|
||||||
expect(schema[0].name).to.equal('test')
|
expect(schema[0].name).to.equal('test')
|
||||||
|
|
||||||
expect(schema[0].columns[0].name).to.equal('col1')
|
expect(schema[0].columns[0].name).to.equal('col1')
|
||||||
expect(schema[0].columns[0].type).to.equal('N/A')
|
expect(schema[0].columns[0].type).to.equal('N/A')
|
||||||
|
|
||||||
expect(schema[0].columns[1].name).to.equal('col2')
|
expect(schema[0].columns[1].name).to.equal('col2')
|
||||||
expect(schema[0].columns[1].type).to.equal('integer')
|
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 () => {
|
it('creates empty db with name database', async () => {
|
||||||
const SQL = await getSQL
|
sinon.spy(db, 'refreshSchema')
|
||||||
const tempDb = new SQL.Database()
|
|
||||||
tempDb.run(`
|
|
||||||
CREATE VIRTUAL TABLE test_virtual USING fts4(
|
|
||||||
col1, col2,
|
|
||||||
notindexed=col1, notindexed=col2,
|
|
||||||
tokenize=unicode61 "tokenchars=.+#")
|
|
||||||
`)
|
|
||||||
|
|
||||||
const data = tempDb.export()
|
await db.loadDb()
|
||||||
const buffer = new Blob([data])
|
await db.refreshSchema.returnValues[0]
|
||||||
buffer.name = 'foo.sqlite'
|
expect(db.dbName).to.equal('database')
|
||||||
|
|
||||||
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')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('loadDb throws errors', async () => {
|
it('loadDb throws errors', async () => {
|
||||||
const SQL = await getSQL
|
const buffer = new Blob([])
|
||||||
const tempDb = new SQL.Database()
|
|
||||||
tempDb.run('CREATE TABLE test (col1, col2)')
|
|
||||||
|
|
||||||
const data = tempDb.export()
|
|
||||||
const buffer = new Blob([data])
|
|
||||||
buffer.name = 'foo.sqlite'
|
buffer.name = 'foo.sqlite'
|
||||||
|
|
||||||
sinon.stub(db.pw, 'postMessage').resolves({ error: new Error('foo') })
|
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$/)
|
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 = {
|
const data = {
|
||||||
columns: ['id', 'name', 'faculty'],
|
columns: ['id', 'name', 'faculty'],
|
||||||
values: [
|
values: [
|
||||||
@@ -146,16 +127,19 @@ describe('database.js', () => {
|
|||||||
}
|
}
|
||||||
const progressHandler = sinon.spy()
|
const progressHandler = sinon.spy()
|
||||||
const progressCounterId = db.createProgressCounter(progressHandler)
|
const progressCounterId = db.createProgressCounter(progressHandler)
|
||||||
const { dbName, schema } = await db.createDb('foo', data, progressCounterId)
|
sinon.spy(db, 'refreshSchema')
|
||||||
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' })
|
|
||||||
|
|
||||||
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.columns).to.eql(data.columns)
|
||||||
expect(result.values).to.eql(data.values)
|
expect(result.values).to.eql(data.values)
|
||||||
|
|
||||||
@@ -164,7 +148,7 @@ describe('database.js', () => {
|
|||||||
expect(progressHandler.secondCall.calledWith(100)).to.equal(true)
|
expect(progressHandler.secondCall.calledWith(100)).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('createDb throws errors', async () => {
|
it('addTableFromCsv throws errors', async () => {
|
||||||
const data = {
|
const data = {
|
||||||
columns: ['id', 'name'],
|
columns: ['id', 'name'],
|
||||||
values: [
|
values: [
|
||||||
@@ -174,7 +158,7 @@ describe('database.js', () => {
|
|||||||
}
|
}
|
||||||
const progressHandler = sinon.stub()
|
const progressHandler = sinon.stub()
|
||||||
const progressCounterId = db.createProgressCounter(progressHandler)
|
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')
|
.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).to.have.lengthOf(1)
|
||||||
expect(result.values[0]).to.eql([1, 'Harry Potter'])
|
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_")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import storedQueries from '@/storedQueries.js'
|
import storedQueries from '@/lib/storedQueries'
|
||||||
import fu from '@/file.utils'
|
import fu from '@/lib/utils/fileIo'
|
||||||
|
|
||||||
describe('storedQueries.js', () => {
|
describe('storedQueries.js', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import fu from '@/file.utils'
|
import fIo from '@/lib/utils/fileIo'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
|
|
||||||
describe('file.utils.js', () => {
|
describe('fileIo.js', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
sinon.restore()
|
sinon.restore()
|
||||||
})
|
})
|
||||||
@@ -15,7 +15,7 @@ describe('file.utils.js', () => {
|
|||||||
sinon.spy(URL, 'revokeObjectURL')
|
sinon.spy(URL, 'revokeObjectURL')
|
||||||
sinon.spy(window, 'Blob')
|
sinon.spy(window, 'Blob')
|
||||||
|
|
||||||
fu.exportToFile('foo', 'foo.txt')
|
fIo.exportToFile('foo', 'foo.txt')
|
||||||
|
|
||||||
expect(document.createElement.calledOnceWith('a')).to.equal(true)
|
expect(document.createElement.calledOnceWith('a')).to.equal(true)
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ describe('file.utils.js', () => {
|
|||||||
sinon.spy(URL, 'revokeObjectURL')
|
sinon.spy(URL, 'revokeObjectURL')
|
||||||
sinon.spy(window, 'Blob')
|
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)
|
expect(document.createElement.calledOnceWith('a')).to.equal(true)
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ describe('file.utils.js', () => {
|
|||||||
|
|
||||||
setTimeout(() => { spyInput.dispatchEvent(new Event('change')) })
|
setTimeout(() => { spyInput.dispatchEvent(new Event('change')) })
|
||||||
|
|
||||||
const data = await fu.importFile()
|
const data = await fIo.importFile()
|
||||||
expect(data).to.equal('foo')
|
expect(data).to.equal('foo')
|
||||||
expect(document.createElement.calledOnceWith('input')).to.equal(true)
|
expect(document.createElement.calledOnceWith('input')).to.equal(true)
|
||||||
expect(spyInput.type).to.equal('file')
|
expect(spyInput.type).to.equal('file')
|
||||||
@@ -82,13 +82,13 @@ describe('file.utils.js', () => {
|
|||||||
it('readFile', () => {
|
it('readFile', () => {
|
||||||
sinon.spy(window, 'fetch')
|
sinon.spy(window, 'fetch')
|
||||||
|
|
||||||
fu.readFile('./foo.bar')
|
fIo.readFile('./foo.bar')
|
||||||
expect(window.fetch.calledOnceWith('./foo.bar')).to.equal(true)
|
expect(window.fetch.calledOnceWith('./foo.bar')).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('readAsArrayBuffer resolves', async () => {
|
it('readAsArrayBuffer resolves', async () => {
|
||||||
const blob = new Blob(['foo'])
|
const blob = new Blob(['foo'])
|
||||||
const buffer = await fu.readAsArrayBuffer(blob)
|
const buffer = await fIo.readAsArrayBuffer(blob)
|
||||||
|
|
||||||
const uint8Array = new Uint8Array(buffer)
|
const uint8Array = new Uint8Array(buffer)
|
||||||
const text = new TextDecoder().decode(uint8Array)
|
const text = new TextDecoder().decode(uint8Array)
|
||||||
@@ -103,6 +103,34 @@ describe('file.utils.js', () => {
|
|||||||
sinon.stub(window, 'FileReader').returns(r)
|
sinon.stub(window, 'FileReader').returns(r)
|
||||||
|
|
||||||
const blob = new Blob(['foo'])
|
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')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import time from '@/time'
|
import time from '@/lib/utils/time'
|
||||||
|
|
||||||
describe('time.js', () => {
|
describe('time.js', () => {
|
||||||
it('getPeriod', () => {
|
it('getPeriod', () => {
|
||||||
// 1.01.2021 13:00:00 000
|
// 1.01.2021 13:00:00 000
|
||||||
let start = new Date(2021, 0, 1, 13, 0, 0, 0)
|
let start = new Date(2021, 0, 1, 13, 0, 0, 0)
|
||||||
|
|
||||||
// 3.01.2021 22:15:20 500
|
// 1.01.2021 13:01:00 500
|
||||||
let end = new Date(2021, 0, 3, 22, 15, 20, 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
|
// 1.01.2021 13:00:00 000
|
||||||
start = new Date(2021, 0, 1, 13, 0, 0, 0)
|
start = new Date(2021, 0, 1, 13, 0, 0, 0)
|
||||||
|
|
||||||
// 1.01.2021 22:00:20 000
|
// 1.01.2021 13:00:20 500
|
||||||
end = new Date(2021, 0, 1, 22, 0, 20, 0)
|
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
|
// 1.01.2021 13:00:00 000
|
||||||
start = new Date(2021, 0, 1, 13, 0, 0, 0)
|
start = new Date(2021, 0, 1, 13, 0, 0, 0)
|
||||||
@@ -25,6 +25,6 @@ describe('time.js', () => {
|
|||||||
// 1.01.2021 13:00:00 45
|
// 1.01.2021 13:00:00 45
|
||||||
end = new Date(2021, 0, 1, 13, 0, 0, 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')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
67
tests/store/actions.spec.js
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import { mutations, actions } from '@/store'
|
import mutations from '@/store/mutations'
|
||||||
const {
|
const {
|
||||||
saveSchema,
|
|
||||||
updateTab,
|
updateTab,
|
||||||
deleteTab,
|
deleteTab,
|
||||||
setCurrentTabId,
|
setCurrentTabId,
|
||||||
@@ -11,8 +10,6 @@ const {
|
|||||||
setDb
|
setDb
|
||||||
} = mutations
|
} = mutations
|
||||||
|
|
||||||
const { addTab } = actions
|
|
||||||
|
|
||||||
describe('mutations', () => {
|
describe('mutations', () => {
|
||||||
it('setDb', () => {
|
it('setDb', () => {
|
||||||
const state = {
|
const state = {
|
||||||
@@ -26,25 +23,6 @@ describe('mutations', () => {
|
|||||||
expect(oldDb.shutDown.calledOnce).to.equal(true)
|
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)', () => {
|
it('updateTab (save)', () => {
|
||||||
const tab = {
|
const tab = {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -376,66 +354,3 @@ describe('mutations', () => {
|
|||||||
expect(state.predefinedQueries).to.eql(queries)
|
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { mount } from '@vue/test-utils'
|
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', () => {
|
it('tooltip is hidden in initial', () => {
|
||||||
const component = {
|
const component = {
|
||||||
template: '<div :style="tooltipStyle"></div>',
|
template: '<div :style="tooltipStyle"></div>',
|
||||||
178
tests/views/MainView/Editor/Schema/Schema.spec.js
Normal 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']]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { shallowMount } from '@vue/test-utils'
|
import { shallowMount } from '@vue/test-utils'
|
||||||
import TableDescription from '@/components/TableDescription.vue'
|
import TableDescription from '@/views/Main/Editor/Schema/TableDescription'
|
||||||
|
|
||||||
describe('TableDescription.vue', () => {
|
describe('TableDescription.vue', () => {
|
||||||
it('Initially the columns are hidden and table name is rendered', () => {
|
it('Initially the columns are hidden and table name is rendered', () => {
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import { mount, shallowMount } from '@vue/test-utils'
|
import { mount, shallowMount } from '@vue/test-utils'
|
||||||
import Chart from '@/components/Chart.vue'
|
import Chart from '@/views/Main/Editor/Tabs/Tab/Chart'
|
||||||
import chart from '@/chart.js'
|
import chartHelper from '@/views/Main/Editor/Tabs/Tab/Chart/chartHelper'
|
||||||
import * as dereference from 'react-chart-editor/lib/lib/dereference'
|
import * as dereference from 'react-chart-editor/lib/lib/dereference'
|
||||||
|
|
||||||
describe('Chart.vue', () => {
|
describe('Chart.vue', () => {
|
||||||
@@ -14,7 +14,7 @@ describe('Chart.vue', () => {
|
|||||||
// mount the component
|
// mount the component
|
||||||
const wrapper = shallowMount(Chart)
|
const wrapper = shallowMount(Chart)
|
||||||
const vm = wrapper.vm
|
const vm = wrapper.vm
|
||||||
const stub = sinon.stub(chart, 'getChartStateForSave').returns('result')
|
const stub = sinon.stub(chartHelper, 'getChartStateForSave').returns('result')
|
||||||
const chartData = vm.getChartStateForSave()
|
const chartData = vm.getChartStateForSave()
|
||||||
expect(stub.calledOnceWith(vm.state, vm.dataSources)).to.equal(true)
|
expect(stub.calledOnceWith(vm.state, vm.dataSources)).to.equal(true)
|
||||||
expect(chartData).to.equal('result')
|
expect(chartData).to.equal('result')
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
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'
|
import * as dereference from 'react-chart-editor/lib/lib/dereference'
|
||||||
|
|
||||||
describe('chart.js', () => {
|
describe('chartHelper.js', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
sinon.restore()
|
sinon.restore()
|
||||||
})
|
})
|
||||||
@@ -17,7 +17,7 @@ describe('chart.js', () => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const ds = chart.getDataSourcesFromSqlResult(sqlResult)
|
const ds = chartHelper.getDataSourcesFromSqlResult(sqlResult)
|
||||||
expect(ds).to.eql({
|
expect(ds).to.eql({
|
||||||
id: [1, 2],
|
id: [1, 2],
|
||||||
name: ['foo', 'bar']
|
name: ['foo', 'bar']
|
||||||
@@ -30,7 +30,7 @@ describe('chart.js', () => {
|
|||||||
name: ['foo', 'bar']
|
name: ['foo', 'bar']
|
||||||
}
|
}
|
||||||
|
|
||||||
const ds = chart.getOptionsFromDataSources(dataSources)
|
const ds = chartHelper.getOptionsFromDataSources(dataSources)
|
||||||
expect(ds).to.eql([
|
expect(ds).to.eql([
|
||||||
{ value: 'id', label: 'id' },
|
{ value: 'id', label: 'id' },
|
||||||
{ value: 'name', label: 'name' }
|
{ value: 'name', label: 'name' }
|
||||||
@@ -53,7 +53,7 @@ describe('chart.js', () => {
|
|||||||
sinon.stub(dereference, 'default')
|
sinon.stub(dereference, 'default')
|
||||||
sinon.spy(JSON, 'parse')
|
sinon.spy(JSON, 'parse')
|
||||||
|
|
||||||
const ds = chart.getChartStateForSave(state, dataSources)
|
const ds = chartHelper.getChartStateForSave(state, dataSources)
|
||||||
|
|
||||||
expect(dereference.default.calledOnce).to.equal(true)
|
expect(dereference.default.calledOnce).to.equal(true)
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import SqlEditor from '@/components/SqlEditor.vue'
|
import SqlEditor from '@/views/Main/Editor/Tabs/Tab/SqlEditor'
|
||||||
|
|
||||||
describe('SqlEditor.vue', () => {
|
describe('SqlEditor.vue', () => {
|
||||||
it('Emits input event when a query is changed', async () => {
|
it('Emits input event when a query is changed', async () => {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import { state } from '@/store'
|
import state from '@/store/state'
|
||||||
import hint, { getHints } from '@/hint'
|
import showHint, { getHints } from '@/views/Main/Editor/Tabs/Tab/SqlEditor/hint'
|
||||||
import CM from 'codemirror'
|
import CM from 'codemirror'
|
||||||
|
|
||||||
describe('hint.js', () => {
|
describe('hint.js', () => {
|
||||||
@@ -11,22 +11,24 @@ describe('hint.js', () => {
|
|||||||
|
|
||||||
it('Calculates table list for hint', () => {
|
it('Calculates table list for hint', () => {
|
||||||
// mock store state
|
// mock store state
|
||||||
const schema = [
|
const db = {
|
||||||
{
|
schema: [
|
||||||
name: 'foo',
|
{
|
||||||
columns: [
|
name: 'foo',
|
||||||
{ name: 'fooId', type: 'INTEGER' },
|
columns: [
|
||||||
{ name: 'name', type: 'NVARCHAR(20)' }
|
{ name: 'fooId', type: 'INTEGER' },
|
||||||
]
|
{ name: 'name', type: 'NVARCHAR(20)' }
|
||||||
},
|
]
|
||||||
{
|
},
|
||||||
name: 'bar',
|
{
|
||||||
columns: [
|
name: 'bar',
|
||||||
{ name: 'barId', type: 'INTEGER' }
|
columns: [
|
||||||
]
|
{ name: 'barId', type: 'INTEGER' }
|
||||||
}
|
]
|
||||||
]
|
}
|
||||||
sinon.stub(state, 'schema').value(schema)
|
]
|
||||||
|
}
|
||||||
|
sinon.stub(state, 'db').value(db)
|
||||||
|
|
||||||
// mock showHint and editor
|
// mock showHint and editor
|
||||||
sinon.stub(CM, 'showHint')
|
sinon.stub(CM, 'showHint')
|
||||||
@@ -40,15 +42,45 @@ describe('hint.js', () => {
|
|||||||
getCursor: sinon.stub()
|
getCursor: sinon.stub()
|
||||||
}
|
}
|
||||||
|
|
||||||
const clock = sinon.useFakeTimers()
|
showHint(editor)
|
||||||
hint.show(editor)
|
|
||||||
clock.tick(500)
|
|
||||||
|
|
||||||
expect(CM.showHint.called).to.equal(true)
|
expect(CM.showHint.called).to.equal(true)
|
||||||
expect(CM.showHint.firstCall.args[2].tables).to.eql({
|
expect(CM.showHint.firstCall.args[2].tables).to.eql({
|
||||||
foo: ['fooId', 'name'],
|
foo: ['fooId', 'name'],
|
||||||
bar: ['barId']
|
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 ';'", () => {
|
it("Doesn't show hint when in string or space, or ';'", () => {
|
||||||
@@ -64,10 +96,7 @@ describe('hint.js', () => {
|
|||||||
getCursor: sinon.stub()
|
getCursor: sinon.stub()
|
||||||
}
|
}
|
||||||
|
|
||||||
const clock = sinon.useFakeTimers()
|
showHint(editor)
|
||||||
hint.show(editor)
|
|
||||||
clock.tick(500)
|
|
||||||
|
|
||||||
expect(CM.showHint.called).to.equal(false)
|
expect(CM.showHint.called).to.equal(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -84,10 +113,7 @@ describe('hint.js', () => {
|
|||||||
getCursor: sinon.stub()
|
getCursor: sinon.stub()
|
||||||
}
|
}
|
||||||
|
|
||||||
const clock = sinon.useFakeTimers()
|
showHint(editor)
|
||||||
hint.show(editor)
|
|
||||||
clock.tick(500)
|
|
||||||
|
|
||||||
expect(CM.showHint.called).to.equal(false)
|
expect(CM.showHint.called).to.equal(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -104,10 +130,7 @@ describe('hint.js', () => {
|
|||||||
getCursor: sinon.stub()
|
getCursor: sinon.stub()
|
||||||
}
|
}
|
||||||
|
|
||||||
const clock = sinon.useFakeTimers()
|
showHint(editor)
|
||||||
hint.show(editor)
|
|
||||||
clock.tick(500)
|
|
||||||
|
|
||||||
expect(CM.showHint.called).to.equal(false)
|
expect(CM.showHint.called).to.equal(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -171,7 +194,7 @@ describe('hint.js', () => {
|
|||||||
|
|
||||||
it('tables is empty object when schema is null', () => {
|
it('tables is empty object when schema is null', () => {
|
||||||
// mock store state
|
// mock store state
|
||||||
sinon.stub(state, 'schema').value(null)
|
sinon.stub(state, 'db').value({ schema: null })
|
||||||
|
|
||||||
// mock showHint and editor
|
// mock showHint and editor
|
||||||
sinon.stub(CM, 'showHint')
|
sinon.stub(CM, 'showHint')
|
||||||
@@ -185,10 +208,7 @@ describe('hint.js', () => {
|
|||||||
getCursor: sinon.stub()
|
getCursor: sinon.stub()
|
||||||
}
|
}
|
||||||
|
|
||||||
const clock = sinon.useFakeTimers()
|
showHint(editor)
|
||||||
hint.show(editor)
|
|
||||||
clock.tick(500)
|
|
||||||
|
|
||||||
expect(CM.showHint.called).to.equal(true)
|
expect(CM.showHint.called).to.equal(true)
|
||||||
expect(CM.showHint.firstCall.args[2].tables).to.eql({})
|
expect(CM.showHint.firstCall.args[2].tables).to.eql({})
|
||||||
})
|
})
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import { mutations } from '@/store'
|
import mutations from '@/store/mutations'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
import Tab from '@/components/Tab.vue'
|
import Tab from '@/views/Main/Editor/Tabs/Tab'
|
||||||
|
|
||||||
describe('Tab.vue', () => {
|
describe('Tab.vue', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -115,6 +115,7 @@ describe('Tab.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
state.currentTabId = 1
|
state.currentTabId = 1
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
expect(mutations.setCurrentTab.calledOnceWith(state, wrapper.vm)).to.equal(true)
|
expect(mutations.setCurrentTab.calledOnceWith(state, wrapper.vm)).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -181,7 +182,8 @@ describe('Tab.vue', () => {
|
|||||||
const state = {
|
const state = {
|
||||||
currentTabId: 1,
|
currentTabId: 1,
|
||||||
db: {
|
db: {
|
||||||
execute: sinon.stub().rejects(new Error('There is no table foo'))
|
execute: sinon.stub().rejects(new Error('There is no table foo')),
|
||||||
|
refreshSchema: sinon.stub().resolves()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,9 +204,9 @@ describe('Tab.vue', () => {
|
|||||||
|
|
||||||
await wrapper.vm.execute()
|
await wrapper.vm.execute()
|
||||||
expect(wrapper.find('.table-view .result-before').isVisible()).to.equal(false)
|
expect(wrapper.find('.table-view .result-before').isVisible()).to.equal(false)
|
||||||
expect(wrapper.find('.table-view .result-in-progress').isVisible()).to.equal(false)
|
expect(wrapper.find('.table-view .result-in-progress').exists()).to.equal(false)
|
||||||
expect(wrapper.find('.table-preview.error').isVisible()).to.equal(true)
|
expect(wrapper.findComponent({ name: 'logs' }).isVisible()).to.equal(true)
|
||||||
expect(wrapper.find('.table-preview.error').text()).to.include('There is no table foo')
|
expect(wrapper.findComponent({ name: 'logs' }).text()).to.include('There is no table foo')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Passes result to sql-table component', async () => {
|
it('Passes result to sql-table component', async () => {
|
||||||
@@ -220,7 +222,7 @@ describe('Tab.vue', () => {
|
|||||||
currentTabId: 1,
|
currentTabId: 1,
|
||||||
db: {
|
db: {
|
||||||
execute: sinon.stub().resolves(result),
|
execute: sinon.stub().resolves(result),
|
||||||
getSchema: sinon.stub().resolves({ dbName: '', schema: [] })
|
refreshSchema: sinon.stub().resolves()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,8 +244,8 @@ describe('Tab.vue', () => {
|
|||||||
|
|
||||||
await wrapper.vm.execute()
|
await wrapper.vm.execute()
|
||||||
expect(wrapper.find('.table-view .result-before').isVisible()).to.equal(false)
|
expect(wrapper.find('.table-view .result-before').isVisible()).to.equal(false)
|
||||||
expect(wrapper.find('.table-view .result-in-progress').isVisible()).to.equal(false)
|
expect(wrapper.find('.table-view .result-in-progress').exists()).to.equal(false)
|
||||||
expect(wrapper.find('.table-preview.error').isVisible()).to.equal(false)
|
expect(wrapper.findComponent({ name: 'logs' }).exists()).to.equal(false)
|
||||||
expect(wrapper.findComponent({ name: 'SqlTable' }).vm.dataSet).to.eql(result)
|
expect(wrapper.findComponent({ name: 'SqlTable' }).vm.dataSet).to.eql(result)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -252,36 +254,17 @@ describe('Tab.vue', () => {
|
|||||||
columns: ['id', 'name'],
|
columns: ['id', 'name'],
|
||||||
values: []
|
values: []
|
||||||
}
|
}
|
||||||
const newSchema = {
|
|
||||||
dbName: 'fooDb',
|
|
||||||
schema: [
|
|
||||||
{
|
|
||||||
name: 'foo',
|
|
||||||
columns: [
|
|
||||||
{ name: 'id', type: 'INTEGER' },
|
|
||||||
{ name: 'title', type: 'NVARCHAR(30)' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'bar',
|
|
||||||
columns: [
|
|
||||||
{ name: 'a', type: 'N/A' },
|
|
||||||
{ name: 'b', type: 'N/A' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
// mock store state
|
// mock store state
|
||||||
const state = {
|
const state = {
|
||||||
currentTabId: 1,
|
currentTabId: 1,
|
||||||
dbName: 'fooDb',
|
dbName: 'fooDb',
|
||||||
db: {
|
db: {
|
||||||
execute: sinon.stub().resolves(result),
|
execute: sinon.stub().resolves(result),
|
||||||
getSchema: sinon.stub().resolves(newSchema)
|
refreshSchema: sinon.stub().resolves()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sinon.spy(mutations, 'saveSchema')
|
|
||||||
const store = new Vuex.Store({ state, mutations })
|
const store = new Vuex.Store({ state, mutations })
|
||||||
|
|
||||||
// mount the component
|
// mount the component
|
||||||
@@ -299,7 +282,6 @@ describe('Tab.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await wrapper.vm.execute()
|
await wrapper.vm.execute()
|
||||||
expect(state.db.getSchema.calledOnceWith('fooDb')).to.equal(true)
|
expect(state.db.refreshSchema.calledOnce).to.equal(true)
|
||||||
expect(mutations.saveSchema.calledOnceWith(state, newSchema)).to.equal(true)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||