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

31 Commits

Author SHA1 Message Date
lana-k
f9edeafd40 fix csv import with ISO dates #64 2021-07-01 19:07:59 +02:00
lana-k
a37ed93306 update version 2021-06-17 12:24:41 +02:00
lana-k
cf4b83f7d4 fix csv result when column names have spaces #59 2021-06-17 12:23:44 +02:00
lana-k
2abd42c9c3 run tests on pull request 2021-06-07 13:20:42 +02:00
lana-k
1251c542cb update react-chart-editor 2021-05-25 11:52:37 +02:00
lana-k
ac89259924 Always create empty db on start #46 2021-05-24 21:45:51 +02:00
lana-k
179ff8b1e1 fix table layout on My queries #32 2021-05-24 20:50:52 +02:00
lana-k
99a10225a3 CSV import as a table and db connection rework
- Add csv to existing db #32
- [RFE] Simplify working with temporary tables #53
2021-05-24 19:40:47 +02:00
lana-k
c96deb5766 Fix overflow for Firefox #46 2021-05-22 22:26:23 +02:00
lana-k
700970e1cc Add addTable icon #32 2021-05-22 22:25:19 +02:00
lana-k
e2be61e2cf Make error in text field start with uppercase #32 2021-05-22 22:24:20 +02:00
lana-k
9c2c8f3692 Make Logs smaller #32 2021-05-22 22:23:12 +02:00
lana-k
414a116f94 Lost focus in SQL query editor #54 2021-05-19 22:50:56 +02:00
lana-k
3e503f85a9 Stub app-diagnostic-info in tests 2021-05-19 21:52:42 +02:00
lana-k
88257bfcf6 fix CSV dialog height typo 2021-05-19 16:53:30 +02:00
lana-k
bdcc494138 Add scrolling to App info , CSV import dialogs #46 2021-05-19 16:43:42 +02:00
lana-k
d750541c80 App diagnostic dialog #46 2021-05-19 15:46:18 +02:00
lana-k
75f743ff9e remove title in release notes 2021-05-18 15:38:44 +02:00
lana-k
8a9f4b3c0a add Help link 2021-05-18 15:04:35 +02:00
lana-k
77468d34ae add release notes 2021-05-18 15:04:19 +02:00
lana-k
a0577ec0ce Fix gradient for Safari 2021-05-17 21:34:42 +02:00
lana-k
e7d1398546 Rewrite reg exp: make them work in Safari #52 2021-05-17 21:34:24 +02:00
lana-k
aa52048d51 Fix file type detection #48
file.type is empty on some Windows machines (Registry settings affects)
2021-05-17 21:32:09 +02:00
lana-k
33913f8f5c fix lint 2021-05-14 16:47:01 +02:00
lana-k
51eb7a543c Merge branch 'master' of github.com:lana-k/sqliteviz 2021-05-14 16:43:13 +02:00
lana-k
a3fb38b23c SQL query execution state in UI #3
- use LoadingIndicator
- use Logs
2021-05-14 16:42:58 +02:00
lana-k
3bb40b4eb7 Improve LoadingIndicator
- size parameters
- smooth animation (use ony transform)
2021-05-14 16:40:55 +02:00
lana-k
6864bf84f8 Update README.md 2021-05-06 21:36:20 +02:00
lana-k
9f1b3823f6 Update README.md 2021-05-06 20:46:36 +02:00
lana-k
7574f529c3 add hidden state for file in animation 2021-05-06 15:17:15 +02:00
lana-k
653f8eff7b minor changes in animation 2021-05-06 15:05:58 +02:00
49 changed files with 1885 additions and 1762 deletions

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

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

View File

@@ -25,11 +25,19 @@ jobs:
cd dist
zip -9 -r dist.zip . -x "js/*.map"
- name: Create Release Notes
run: |
npm install github-release-notes@0.16.0 -g
gren changelog --generate --config="/.github/workflows/config.grenrc.js"
env:
GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create release
uses: ncipollo/release-action@v1
with:
artifacts: "dist/dist.zip"
token: ${{ secrets.GITHUB_TOKEN }}
bodyFile: "CHANGELOG.md"
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@4.1.1

View File

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

View File

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

695
package-lock.json generated
View File

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

View File

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

View File

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

View File

@@ -13,7 +13,14 @@ export default {
result.columns = source.meta.fields.map(col => col.trim())
result.values = source.data.map(row => {
const resultRow = []
result.columns.forEach(col => { resultRow.push(row[col]) })
source.meta.fields.forEach(col => {
let value = row[col]
if (value instanceof Date) {
value = value.toISOString()
}
resultRow.push(value)
})
return resultRow
})
} else {

View File

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

View File

@@ -0,0 +1,259 @@
<template>
<div class="db-uploader-container" :style="{ width }">
<change-db-icon v-if="type === 'small'" @click.native="browse"/>
<div v-if="type === 'illustrated'" class="drop-area-container">
<div
class="drop-area"
@dragover.prevent="state = 'dragover'"
@dragleave.prevent="state=''"
@drop.prevent="drop"
@click="browse"
>
<div class="text">
Drop the database or CSV file here or click to choose a file from your computer.
</div>
</div>
</div>
<div v-if="type === 'illustrated'" id="img-container">
<img id="drop-file-top-img" :src="require('@/assets/images/top.svg')" />
<img
id="left-arm-img"
:class="{'swing': state === 'dragover'}"
:src="require('@/assets/images/leftArm.svg')"
/>
<img
id="file-img"
ref="fileImg"
:class="{
'swing': state === 'dragover',
'fly': state === 'dropping',
'hidden': state === 'dropped'
}"
:src="require('@/assets/images/file.png')"
/>
<img id="drop-file-bottom-img" :src="require('@/assets/images/bottom.svg')" />
<img id="body-img" :src="require('@/assets/images/body.svg')" />
<img
id="right-arm-img"
:class="{'swing': state === 'dragover'}"
:src="require('@/assets/images/rightArm.svg')"
/>
</div>
<div id="error" class="error"></div>
<!--Parse csv dialog -->
<csv-import
ref="addCsv"
:file="file"
:db="newDb"
dialog-name="importFromCsv"
@cancel="cancelCsvImport"
@finish="finish"
/>
</div>
</template>
<script>
import fIo from '@/lib/utils/fileIo'
import ChangeDbIcon from '@/components/svg/changeDb'
import database from '@/lib/database'
import CsvImport from '@/components/CsvImport'
export default {
name: 'DbUploader',
props: {
type: {
type: String,
required: false,
default: 'small',
validator: (value) => {
return ['illustrated', 'small'].includes(value)
}
},
width: {
type: String,
required: false,
default: 'unset'
}
},
components: {
ChangeDbIcon,
CsvImport
},
data () {
return {
state: '',
animationPromise: Promise.resolve(),
file: null,
newDb: null
}
},
mounted () {
if (this.type === 'illustrated') {
this.animationPromise = new Promise((resolve) => {
this.$refs.fileImg.addEventListener('animationend', event => {
if (event.animationName.startsWith('fly')) {
this.state = 'dropped'
resolve()
}
})
})
}
},
methods: {
cancelCsvImport () {
if (this.newDb) {
this.newDb.shutDown()
this.newDb = null
}
},
async finish () {
this.$store.commit('setDb', this.newDb)
if (this.$route.path !== '/editor') {
this.$router.push('/editor')
}
},
loadDb (file) {
return Promise.all([this.newDb.loadDb(file), this.animationPromise])
.then(this.finish)
},
async checkFile (file) {
this.state = 'dropping'
this.newDb = database.getNewDatabase()
if (fIo.isDatabase(file)) {
this.loadDb(file)
} else {
this.file = file
await this.$nextTick()
const csvImport = this.$refs.addCsv
csvImport.reset()
return Promise.all([csvImport.previewCsv(), this.animationPromise])
.then(csvImport.open)
}
},
browse () {
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
.then(this.checkFile)
},
drop (event) {
this.checkFile(event.dataTransfer.files[0])
}
}
}
</script>
<style scoped>
.db-uploader-container {
position: relative;
}
.drop-area-container {
display: inline-block;
border: 1px dashed var(--color-border);
padding: 8px;
border-radius: var(--border-radius-big);
height: 100%;
width: 100%;
box-sizing: border-box;
}
.drop-area {
background-color: var(--color-bg-light-3);
border-radius: var(--border-radius-big);
color: var(--color-text-base);
font-size: 13px;
text-align: center;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
cursor: pointer;
}
#img-container {
position: absolute;
top: 54px;
left: 50%;
transform: translate(-50%, 0);
width: 450px;
height: 338px;
pointer-events: none;
}
#drop-file-top-img {
width: 450px;
height: 175px;
position: absolute;
top: 0;
left: 0;
}
#drop-file-bottom-img {
width: 450px;
height: 167px;
position: absolute;
bottom: 0;
left: 0;
}
#body-img {
width: 74px;
position: absolute;
top: 94.05px;
left: 46px;
}
#right-arm-img {
width: 106px;
position: absolute;
top: 110.05px;
left: 78px;
}
#left-arm-img {
width: 114px;
position: absolute;
top: 69.05px;
left: 69px;
}
#file-img {
width: 125px;
position: absolute;
top: 15.66px;
left: 152px;
}
.swing {
animation: swing ease-in-out 0.6s infinite alternate;
}
#left-arm-img.swing {
transform-origin: 9px 83px;
}
#right-arm-img.swing {
transform-origin: 0 56px;
}
#file-img.swing {
transform-origin: -74px 139px;
}
@keyframes swing {
0% { transform: rotate(0deg); }
100% { transform: rotate(-7deg); }
}
#file-img.fly {
animation: fly ease-in-out 1s 1 normal;
transform-origin: center center;
}
@keyframes fly {
100% {
transform: rotate(360deg) scale(0.5);
top: 183px;
left: 225px;
}
}
#file-img.hidden {
display: none;
}
</style>

View File

@@ -1,552 +0,0 @@
<template>
<div class="db-uploader-container" :style="{ width }">
<change-db-icon v-if="type === 'small'" @click.native="browse"/>
<div v-if="type === 'illustrated'" class="drop-area-container">
<div
class="drop-area"
@dragover.prevent="state = 'dragover'"
@dragleave.prevent="state=''"
@drop.prevent="drop"
@click="browse"
>
<div class="text">
Drop the database or CSV file here or click to choose a file from your computer.
</div>
</div>
</div>
<div v-if="type === 'illustrated'" id="img-container">
<img id="drop-file-top-img" :src="require('@/assets/images/top.svg')" />
<img
id="left-arm-img"
:class="{'swing': state === 'dragover'}"
:src="require('@/assets/images/leftArm.svg')"
/>
<img
id="file-img"
ref="fileImg"
:class="{
'swing': state === 'dragover',
'fly': state === 'drop'
}"
:src="require('@/assets/images/file.png')"
/>
<img id="drop-file-bottom-img" :src="require('@/assets/images/bottom.svg')" />
<img id="body-img" :src="require('@/assets/images/body.svg')" />
<img
id="right-arm-img"
:class="{'swing': state === 'dragover'}"
:src="require('@/assets/images/rightArm.svg')"
/>
</div>
<div id="error" class="error"></div>
<!--Parse csv dialog -->
<modal name="parse" classes="dialog" height="auto" width="60%" :clickToClose="false">
<div class="dialog-header">
Import CSV
<close-icon @click="cancelCsvImport" :disabled="disableDialog"/>
</div>
<div class="dialog-body">
<div class="chars">
<delimiter-selector
v-model="delimiter"
width="210px"
class="char-input"
@input="previewCSV"
:disabled="disableDialog"
/>
<text-field
label="Quote char"
hint="The character used to quote fields."
v-model="quoteChar"
width="93px"
:disabled="disableDialog"
class="char-input"
id="quote-char"
/>
<text-field
label="Escape char"
hint='The character used to escape the quote character within a field (e.g. "column with ""quotes"" in text").'
max-hint-width="242px"
v-model="escapeChar"
width="93px"
:disabled="disableDialog"
class="char-input"
id="escape-char"
/>
</div>
<check-box
@click="header = $event"
:init="true"
label="Use first row as column headers"
:disabled="disableDialog"
/>
<sql-table
v-if="previewData"
:data-set="previewData"
height="160"
class="preview-table"
:preview="true"
/>
<div v-if="!previewData" class="no-data">No data</div>
<logs
class="import-csv-errors"
:messages="importCsvMessages"
/>
</div>
<div class="dialog-buttons-container">
<button
class="secondary"
:disabled="disableDialog"
@click="cancelCsvImport"
id="csv-cancel"
>
Cancel
</button>
<button
v-show="!importCsvCompleted"
class="primary"
:disabled="disableDialog"
@click="loadFromCsv(file)"
id="csv-import"
>
Import
</button>
<button
v-show="importCsvCompleted"
class="primary"
:disabled="disableDialog"
@click="finish"
id="csv-finish"
>
Finish
</button>
</div>
</modal>
</div>
</template>
<script>
import fu from '@/lib/utils/fileIo'
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 ChangeDbIcon from '@/components/svg/changeDb'
import time from '@/lib/utils/time'
import database from '@/lib/database'
const csvMimeTypes = [
'text/csv',
'text/x-csv',
'application/x-csv',
'application/csv',
'text/x-comma-separated-values',
'text/comma-separated-values'
]
export default {
name: 'DbUploader',
props: {
type: {
type: String,
required: false,
default: 'small',
validator: (value) => {
return ['illustrated', 'small'].includes(value)
}
},
width: {
type: String,
required: false,
default: 'unset'
}
},
components: {
ChangeDbIcon,
TextField,
DelimiterSelector,
CloseIcon,
CheckBox,
SqlTable,
Logs
},
data () {
return {
state: '',
animationPromise: Promise.resolve(),
file: null,
schema: null,
delimiter: '',
quoteChar: '"',
escapeChar: '"',
header: true,
previewData: null,
importCsvMessages: [],
disableDialog: false,
importCsvCompleted: false,
newDb: null
}
},
mounted () {
if (this.type === 'illustrated') {
this.animationPromise = new Promise((resolve) => {
this.$refs.fileImg.addEventListener('animationend', event => {
if (event.animationName.startsWith('fly')) {
resolve()
}
})
})
}
},
watch: {
quoteChar () {
this.previewCSV()
},
escapeChar () {
this.previewCSV()
},
header () {
this.previewCSV()
}
},
methods: {
cancelCsvImport () {
if (!this.disableDialog) {
this.$modal.hide('parse')
if (this.newDb) {
this.newDb.shutDown()
this.newDb = null
}
}
},
async finish () {
this.$store.commit('setDb', this.newDb)
this.$store.commit('saveSchema', this.schema)
if (this.importCsvCompleted) {
this.$modal.hide('parse')
const stmt = [
'/*',
' * Your CSV file has been imported into csv_import table.',
' * You can run this SQL query to make all CSV records available for charting.',
' */',
'SELECT * FROM csv_import'
].join('\n')
const tabId = await this.$store.dispatch('addTab', { query: stmt })
this.$store.commit('setCurrentTabId', tabId)
this.importCsvCompleted = false
}
if (this.$route.path !== '/editor') {
this.$router.push('/editor')
}
},
async previewCSV () {
this.importCsvCompleted = false
const config = {
preview: 3,
quoteChar: this.quoteChar || '"',
escapeChar: this.escapeChar,
header: this.header,
delimiter: this.delimiter
}
try {
const start = new Date()
const parseResult = await csv.parse(this.file, config)
const end = new Date()
this.previewData = parseResult.data
this.delimiter = parseResult.delimiter
// In parseResult.messages we can get parse errors
this.importCsvMessages = parseResult.messages || []
if (!parseResult.hasErrors) {
this.importCsvMessages.push({
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
type: 'success'
})
}
} catch (err) {
this.importCsvMessages = [{
message: err,
type: 'error'
}]
}
},
loadDb (file) {
this.newDb = database.getNewDatabase()
return Promise.all([this.newDb.loadDb(file), this.animationPromise])
.then(([schema]) => {
this.schema = schema
this.finish()
})
},
async loadFromCsv (file) {
this.disableDialog = true
const config = {
quoteChar: this.quoteChar || '"',
escapeChar: this.escapeChar,
header: this.header,
delimiter: this.delimiter
}
const parseCsvMsg = {
message: 'Parsing CSV...',
type: 'info'
}
this.importCsvMessages.push(parseCsvMsg)
const parseCsvLoadingIndicator = setTimeout(() => { parseCsvMsg.type = 'loading' }, 1000)
const importMsg = {
message: 'Importing CSV into a SQLite database...',
type: 'info'
}
let importLoadingIndicator = null
const updateProgress = progress => {
this.$set(importMsg, 'progress', progress)
}
this.newDb = database.getNewDatabase()
const progressCounterId = this.newDb.createProgressCounter(updateProgress)
try {
let start = new Date()
const parseResult = await csv.parse(this.file, config)
let end = new Date()
if (!parseResult.hasErrors) {
const rowCount = parseResult.data.values.length
let period = time.getPeriod(start, end)
parseCsvMsg.type = 'success'
if (parseResult.messages.length > 0) {
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
parseCsvMsg.message = `${rowCount} rows are parsed in ${period}.`
} else {
// Inform about csv parsing success
parseCsvMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
}
// Loading indicator for csv parsing is not needed anymore
clearTimeout(parseCsvLoadingIndicator)
// Add info about import start
this.importCsvMessages.push(importMsg)
// Show import progress after 1 second
importLoadingIndicator = setTimeout(() => {
importMsg.type = 'loading'
}, 1000)
// Create db with csv table and get schema
const name = file.name.replace(/\.[^.]+$/, '')
start = new Date()
this.schema = await this.newDb.importDb(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) {
this.state = 'drop'
if (csvMimeTypes.includes(file.type)) {
this.file = file
this.header = true
this.quoteChar = '"'
this.escapeChar = '"'
this.delimiter = ''
return Promise.all([this.previewCSV(), this.animationPromise])
.then(() => {
this.$modal.show('parse')
})
} else {
this.loadDb(file)
}
},
browse () {
fu.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
.then(this.checkFile)
},
drop (event) {
this.checkFile(event.dataTransfer.files[0])
}
}
}
</script>
<style scoped>
.db-uploader-container {
position: relative;
}
.drop-area-container {
display: inline-block;
border: 1px dashed var(--color-border);
padding: 8px;
border-radius: var(--border-radius-big);
height: 100%;
width: 100%;
box-sizing: border-box;
}
.drop-area {
background-color: var(--color-bg-light-3);
border-radius: var(--border-radius-big);
color: var(--color-text-base);
font-size: 13px;
text-align: center;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
cursor: pointer;
}
#img-container {
position: absolute;
top: 54px;
left: 50%;
transform: translate(-50%, 0);
width: 450px;
height: 338px;
pointer-events: none;
}
#drop-file-top-img {
width: 450px;
height: 175px;
position: absolute;
top: 0;
left: 0;
}
#drop-file-bottom-img {
width: 450px;
height: 167px;
position: absolute;
bottom: 0;
left: 0;
}
#body-img {
width: 74px;
position: absolute;
top: 94.05px;
left: 46px;
}
#right-arm-img {
width: 106px;
position: absolute;
top: 110.05px;
left: 78px;
}
#left-arm-img {
width: 114px;
position: absolute;
top: 69.05px;
left: 69px;
}
#file-img {
width: 125px;
position: absolute;
top: 15.66px;
left: 152px;
}
.swing {
animation: swing ease-in-out 0.6s infinite alternate;
}
#left-arm-img.swing {
transform-origin: 9px 83px;
}
#right-arm-img.swing {
transform-origin: 0 56px;
}
#file-img.swing {
transform-origin: -74px 139px;
}
@keyframes swing {
0% { transform: rotate(0deg); }
100% { transform: rotate(-7deg); }
}
#file-img.fly {
animation: fly ease-in-out 1s 1 normal;
transform-origin: center center;
top: 183px;
left: 225px;
transition: top 1s ease-in-out, left 1s ease-in-out;
}
@keyframes fly {
100% { transform: rotate(360deg) scale(0.5); }
}
/* Parse CSV dialog */
.chars {
display: flex;
align-items: flex-end;
margin-bottom: 20px;
}
.char-input {
margin-right: 44px;
}
.preview-table {
margin-top: 32px;
}
.import-csv-errors {
height: 160px;
margin-top: 32px;
}
.no-data {
margin-top: 32px;
background-color: white;
border-radius: 5px;
position: relative;
border: 1px solid var(--color-border-light);
box-sizing: border-box;
height: 160px;
font-size: 13px;
color: var(--color-text-base);
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -39,13 +39,15 @@ export default class Sql {
return this.db.exec(sql, params)
}
import (columns, values, progressCounterId, progressCallback, chunkSize = 1500) {
this.createDb()
this.db.exec(dbUtils.getCreateStatement(columns, values))
import (tabName, columns, values, progressCounterId, progressCallback, chunkSize = 1500) {
if (this.db === null) {
this.createDb()
}
this.db.exec(dbUtils.getCreateStatement(tabName, columns, values))
const chunks = dbUtils.generateChunks(values, chunkSize)
const chunksAmount = Math.ceil(values.length / chunkSize)
let count = 0
const insertStr = dbUtils.getInsertStmt(columns)
const insertStr = dbUtils.getInsertStmt(tabName, columns)
const insertStmt = this.db.prepare(insertStr)
progressCallback({ progress: 0, id: progressCounterId })

View File

@@ -1,3 +1,5 @@
import sqliteParser from 'sqlite-parser'
export default {
* generateChunks (arr, size) {
const count = Math.ceil(arr.length / size)
@@ -9,14 +11,14 @@ export default {
}
},
getInsertStmt (columns) {
getInsertStmt (tabName, columns) {
const colList = `"${columns.join('", "')}"`
const params = columns.map(() => '?').join(', ')
return `INSERT INTO csv_import (${colList}) VALUES (${params});`
return `INSERT INTO "${tabName}" (${colList}) VALUES (${params});`
},
getCreateStatement (columns, values) {
let result = 'CREATE table csv_import('
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]
@@ -40,5 +42,49 @@ export default {
})
result = result.replace(/,\s$/, ');')
return result
},
getAst (sql) {
// There is a bug is sqlite-parser
// It throws an error if tokenizer has an arguments:
// https://github.com/codeschool/sqlite-parser/issues/59
const fixedSql = sql
.replace(/(tokenize=[^,]+)"tokenchars=.+?"/, '$1')
.replace(/(tokenize=[^,]+)"remove_diacritics=.+?"/, '$1')
.replace(/(tokenize=[^,]+)"separators=.+?"/, '$1')
.replace(/tokenize=.+?(,|\))/, 'tokenize=unicode61$1')
return sqliteParser(fixedSql)
},
/*
* Return an array of columns with name and type. E.g.:
* [
* { name: 'id', type: 'INTEGER' },
* { name: 'title', type: 'NVARCHAR(30)' },
* ]
*/
getColumns (sql) {
const columns = []
const ast = this.getAst(sql)
const columnDefinition = ast.statement[0].format === 'table'
? ast.statement[0].definition
: ast.statement[0].result.args.expression // virtual table
columnDefinition.forEach(item => {
if (item.variant === 'column' && ['identifier', 'definition'].includes(item.type)) {
let type = item.datatype ? item.datatype.variant : 'N/A'
if (item.datatype && item.datatype.args) {
type = type + '(' + item.datatype.args.expression[0].value
if (item.datatype.args.expression.length === 2) {
type = type + ', ' + item.datatype.args.expression[1].value
}
type = type + ')'
}
columns.push({ name: item.name, type: type })
}
})
return columns
}
}

View File

@@ -8,10 +8,18 @@ function processMsg (sql) {
switch (data && data.action) {
case 'open':
return sql.open(data.buffer)
case 'reopen':
return sql.open(sql.export())
case 'exec':
return sql.exec(data.sql, data.params)
case 'import':
return sql.import(data.columns, data.values, data.progressCounterId, postMessage)
return sql.import(
data.tabName,
data.columns,
data.values,
data.progressCounterId,
postMessage
)
case 'export':
return sql.export()
case 'close':

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,8 @@ 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)
@@ -36,4 +38,13 @@ const router = new VueRouter({
routes
})
router.beforeEach(async (to, from, next) => {
if (!store.state.db) {
const newDb = database.getNewDatabase()
await newDb.loadDb()
store.commit('setDb', newDb)
}
next()
})
export default router

View File

@@ -7,10 +7,6 @@ export default {
}
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]

View File

@@ -1,7 +1,4 @@
export default {
schema: null,
dbFile: null,
dbName: null,
tabs: [],
currentTab: null,
currentTabId: null,

View File

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

View File

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

View File

@@ -17,15 +17,15 @@ export function getHints (cm, options) {
const hintOptions = {
get tables () {
const tables = {}
if (store.state.schema) {
store.state.schema.forEach(table => {
if (store.state.db.schema) {
store.state.db.schema.forEach(table => {
tables[table.name] = table.columns.map(column => column.name)
})
}
return tables
},
get defaultTable () {
const schema = store.state.schema
const schema = store.state.db.schema
return schema && schema.length === 1 ? schema[0].name : null
},
completeSingle: false,

View File

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

View File

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

View File

@@ -19,8 +19,6 @@
import Splitpanes from '@/components/Splitpanes'
import Schema from './Schema'
import Tabs from './Tabs'
import database from '@/lib/database'
import store from '@/store'
export default {
name: 'Editor',
@@ -29,12 +27,9 @@ export default {
Splitpanes,
Tabs
},
async beforeRouteEnter (to, from, next) {
if (!store.state.schema) {
const newDb = database.getNewDatabase()
const newSchema = await newDb.loadDb()
store.commit('setDb', newDb)
store.commit('saveSchema', newSchema)
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',
@@ -52,10 +47,9 @@ export default {
"('Slytherin', 80);"
].join('\n')
const tabId = await store.dispatch('addTab', { query: stmt })
store.commit('setCurrentTabId', tabId)
const tabId = await this.$store.dispatch('addTab', { query: stmt })
this.$store.commit('setCurrentTabId', tabId)
}
next()
}
}
</script>

View File

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

View File

@@ -448,6 +448,7 @@ export default {
}
.rounded-bg {
padding-top: 40px;
margin: 0 auto;
max-width: 1500px;
width: 100%;

View File

@@ -1,172 +1,26 @@
import { expect } from 'chai'
import sinon from 'sinon'
import Vuex from 'vuex'
import { shallowMount, mount } from '@vue/test-utils'
import DbUploader from '@/components/DbUploader'
import fu from '@/lib/utils/fileIo'
import database from '@/lib/database'
import csv from '@/components/DbUploader/csv'
import { mount } from '@vue/test-utils'
import CsvImport from '@/components/CsvImport'
import csv from '@/components/CsvImport/csv'
describe('DbUploader.vue', () => {
describe('CsvImport.vue', () => {
let state = {}
let mutations = {}
let store = {}
let place
beforeEach(() => {
// mock store state and mutations
state = {}
mutations = {
saveSchema: sinon.stub(),
setDb: sinon.stub()
}
store = new Vuex.Store({ state, mutations })
place = document.createElement('div')
document.body.appendChild(place)
})
afterEach(() => {
sinon.restore()
place.remove()
})
it('loads db on click and redirects to /editor', async () => {
// mock getting a file from user
const file = {}
sinon.stub(fu, 'getFileFromUser').resolves(file)
// mock db loading
const schema = {}
const db = {
loadDb: sinon.stub().resolves(schema)
}
sinon.stub(database, 'getNewDatabase').returns(db)
// mock router
const $router = { push: sinon.stub() }
const $route = { path: '/' }
// mount the component
const wrapper = shallowMount(DbUploader, {
attachTo: place,
store,
mocks: { $router, $route },
propsData: {
type: 'illustrated'
}
})
await wrapper.find('.drop-area').trigger('click')
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
await db.loadDb.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
expect(mutations.saveSchema.calledOnceWith(state, schema)).to.equal(true)
expect($router.push.calledOnceWith('/editor')).to.equal(true)
wrapper.destroy()
})
it('loads db on drop and redirects to /editor', async () => {
// mock db loading
const schema = {}
const db = {
loadDb: sinon.stub().resolves(schema)
}
sinon.stub(database, 'getNewDatabase').returns(db)
// mock router
const $router = { push: sinon.stub() }
const $route = { path: '/' }
// mount the component
const wrapper = shallowMount(DbUploader, {
attachTo: place,
store,
mocks: { $router, $route },
propsData: {
type: 'illustrated'
}
})
// mock a file dropped by a user
const file = {}
const dropData = { dataTransfer: new DataTransfer() }
Object.defineProperty(dropData.dataTransfer, 'files', {
value: [file],
writable: false
})
await wrapper.find('.drop-area').trigger('drop', dropData)
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
await db.loadDb.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
expect(mutations.saveSchema.calledOnceWith(state, schema)).to.equal(true)
expect($router.push.calledOnceWith('/editor')).to.equal(true)
wrapper.destroy()
})
it("doesn't redirect if already on /editor", async () => {
// mock getting a file from user
const file = {}
sinon.stub(fu, 'getFileFromUser').resolves(file)
// mock db loading
const schema = {}
const db = {
loadDb: sinon.stub().resolves(schema)
}
sinon.stub(database, 'getNewDatabase').returns(db)
// mock router
const $router = { push: sinon.stub() }
const $route = { path: '/editor' }
// mount the component
const wrapper = shallowMount(DbUploader, {
attachTo: place,
store,
mocks: { $router, $route },
propsData: {
type: 'illustrated'
}
})
await wrapper.find('.drop-area').trigger('click')
await db.loadDb.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
expect($router.push.called).to.equal(false)
wrapper.destroy()
})
})
describe('DbUploader.vue import CSV', () => {
let state = {}
let mutations = {}
let actions = {}
const newTabId = 1
let mutations = {}
let store = {}
let place
// mock router
const $router = { }
const $route = { path: '/' }
let clock
let wrapper
const newTabId = 1
const file = { name: 'my data.csv' }
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()
}
@@ -175,29 +29,32 @@ describe('DbUploader.vue import CSV', () => {
}
store = new Vuex.Store({ state, mutations, actions })
$router.push = sinon.stub()
place = document.createElement('div')
document.body.appendChild(place)
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(DbUploader, {
attachTo: place,
wrapper = mount(CsvImport, {
store,
mocks: { $router, $route },
propsData: {
type: 'illustrated'
file,
dialogName: 'addCsv',
db
}
})
})
afterEach(() => {
sinon.restore()
wrapper.destroy()
place.remove()
})
it('shows parse dialog if gets csv file', async () => {
it('previews', async () => {
sinon.stub(csv, 'parse').resolves({
delimiter: '|',
data: {
@@ -216,12 +73,11 @@ describe('DbUploader.vue import CSV', () => {
}]
})
await wrapper.find('.drop-area').trigger('click')
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
wrapper.vm.previewCsv()
await wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick()
expect(wrapper.find('[data-modal="parse"]').exists()).to.equal(true)
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('"')
@@ -252,10 +108,9 @@ describe('DbUploader.vue import CSV', () => {
}
})
await wrapper.find('.drop-area').trigger('click')
wrapper.vm.previewCsv()
wrapper.vm.open()
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick()
parse.onCall(1).resolves({
@@ -362,17 +217,18 @@ describe('DbUploader.vue import CSV', () => {
}
})
await wrapper.find('.drop-area').trigger('click')
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
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())
@@ -430,12 +286,16 @@ describe('DbUploader.vue import CSV', () => {
messages: []
})
await wrapper.find('.drop-area').trigger('click')
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
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()
@@ -454,6 +314,7 @@ describe('DbUploader.vue import CSV', () => {
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 () => {
@@ -488,12 +349,16 @@ describe('DbUploader.vue import CSV', () => {
}]
})
await wrapper.find('.drop-area').trigger('click')
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
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()
@@ -514,6 +379,7 @@ describe('DbUploader.vue import CSV', () => {
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 () => {
@@ -548,12 +414,11 @@ describe('DbUploader.vue import CSV', () => {
}]
})
await wrapper.find('.drop-area').trigger('click')
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
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()
@@ -604,19 +469,14 @@ describe('DbUploader.vue import CSV', () => {
})
let resolveImport = sinon.stub()
const newDb = {
importDb: sinon.stub().resolves(new Promise(resolve => { resolveImport = resolve })),
createProgressCounter: sinon.stub().returns(1),
deleteProgressCounter: sinon.stub()
}
sinon.stub(database, 'getNewDatabase').returns(newDb)
wrapper.vm.db.addTableFromCsv = sinon.stub()
.resolves(new Promise(resolve => { resolveImport = resolve }))
await wrapper.find('.drop-area').trigger('click')
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
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()
@@ -641,11 +501,11 @@ describe('DbUploader.vue import CSV', () => {
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.importDb.getCall(0).args[0]).to.equal('foo') // file name
expect(wrapper.vm.db.addTableFromCsv.getCall(0).args[0]).to.equal('foo') // table name
// After resolving - loading indicator is not shown
await resolveImport()
await newDb.importDb.returnValues[0]
await wrapper.vm.db.addTableFromCsv.returnValues[0]
expect(
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
).to.equal(false)
@@ -678,20 +538,11 @@ describe('DbUploader.vue import CSV', () => {
messages: []
})
const schema = {}
const newDb = {
importDb: 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()
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()
@@ -739,19 +590,13 @@ describe('DbUploader.vue import CSV', () => {
messages: []
})
const newDb = {
importDb: sinon.stub().rejects(new Error('fail')),
createProgressCounter: sinon.stub().returns(1),
deleteProgressCounter: sinon.stub()
}
sinon.stub(database, 'getNewDatabase').returns(newDb)
wrapper.vm.db.addTableFromCsv = sinon.stub().rejects(new Error('fail'))
await wrapper.find('.drop-area').trigger('click')
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
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()
@@ -773,7 +618,7 @@ describe('DbUploader.vue import CSV', () => {
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
})
it('import final', async () => {
it('import finish', async () => {
sinon.stub(csv, 'parse').resolves({
delimiter: '|',
data: {
@@ -786,33 +631,20 @@ describe('DbUploader.vue import CSV', () => {
messages: []
})
const schema = {}
const newDb = {
importDb: 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()
wrapper.vm.previewCsv()
wrapper.vm.open()
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)
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
expect(wrapper.emitted('finish')).to.have.lengthOf(1)
})
it('import cancel', async () => {
@@ -828,79 +660,47 @@ describe('DbUploader.vue import CSV', () => {
messages: []
})
const schema = {}
const newDb = {
importDb: 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.vm.previewCsv()
await wrapper.vm.open()
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)
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("doesn't open new tab when load db after importing CSV", async () => {
fu.getFileFromUser.onCall(0).resolves({ type: 'text/csv', name: 'foo.csv' })
fu.getFileFromUser.onCall(1).resolves({ type: 'application/x-sqlite3', name: 'bar.sqlite3' })
sinon.stub(csv, 'parse').resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
const schema = {}
const newDb = {
importDb: sinon.stub().resolves(schema),
createProgressCounter: sinon.stub().returns(1),
deleteProgressCounter: sinon.stub(),
loadDb: sinon.stub().resolves()
}
sinon.stub(database, 'getNewDatabase').returns(newDb)
await wrapper.find('.drop-area').trigger('click')
await csv.parse.returnValues[0]
await wrapper.vm.animationPromise
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')
await csv.parse.returnValues[1]
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)
await wrapper.find('.drop-area').trigger('click')
await newDb.loadDb.returnValues[0]
expect(actions.addTab.calledOnce).to.equal(true)
expect(mutations.setCurrentTabId.calledOnce).to.equal(true)
expect(wrapper.find('#csv-table-name .text-field-error').text())
.to.equal("Table name can't be empty")
expect(wrapper.vm.db.addTableFromCsv.called).to.equal(false)
})
})

View File

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

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import sinon from 'sinon'
import csv from '@/components/DbUploader/csv'
import csv from '@/components/CsvImport/csv'
import Papa from 'papaparse'
describe('csv.js', () => {
@@ -11,18 +11,18 @@ describe('csv.js', () => {
it('getResult with fields', () => {
const source = {
data: [
{ id: 1, name: 'foo' },
{ id: 2, name: 'bar' }
{ id: 1, 'name ': 'foo', date: new Date('2021-06-30T14:10:24.717Z') },
{ id: 2, 'name ': 'bar', date: new Date('2021-07-30T14:10:15.717Z') }
],
meta: {
fields: ['id', 'name ']
fields: ['id', 'name ', 'date']
}
}
expect(csv.getResult(source)).to.eql({
columns: ['id', 'name'],
columns: ['id', 'name', 'date'],
values: [
[1, 'foo'],
[2, 'bar']
[1, 'foo', '2021-06-30T14:10:24.717Z'],
[2, 'bar', '2021-07-30T14:10:15.717Z']
]
})
})

View File

@@ -0,0 +1,199 @@
import { expect } from 'chai'
import sinon from 'sinon'
import Vuex from 'vuex'
import { shallowMount, mount } from '@vue/test-utils'
import DbUploader from '@/components/DbUploader'
import fu from '@/lib/utils/fileIo'
import database from '@/lib/database'
describe('DbUploader.vue', () => {
let state = {}
let mutations = {}
let store = {}
let place
beforeEach(() => {
// mock store state and mutations
state = {}
mutations = {
setDb: sinon.stub()
}
store = new Vuex.Store({ state, mutations })
place = document.createElement('div')
document.body.appendChild(place)
})
afterEach(() => {
sinon.restore()
place.remove()
})
it('loads db on click and redirects to /editor', async () => {
// mock getting a file from user
const file = { name: 'test.db' }
sinon.stub(fu, 'getFileFromUser').resolves(file)
// mock db loading
const db = {
loadDb: sinon.stub().resolves()
}
sinon.stub(database, 'getNewDatabase').returns(db)
// mock router
const $router = { push: sinon.stub() }
const $route = { path: '/' }
// mount the component
const wrapper = shallowMount(DbUploader, {
attachTo: place,
store,
mocks: { $router, $route },
propsData: {
type: 'illustrated'
}
})
await wrapper.find('.drop-area').trigger('click')
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
await db.loadDb.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
expect($router.push.calledOnceWith('/editor')).to.equal(true)
wrapper.destroy()
})
it('loads db on drop and redirects to /editor', async () => {
// mock db loading
const db = {
loadDb: sinon.stub().resolves()
}
sinon.stub(database, 'getNewDatabase').returns(db)
// mock router
const $router = { push: sinon.stub() }
const $route = { path: '/' }
// mount the component
const wrapper = shallowMount(DbUploader, {
attachTo: place,
store,
mocks: { $router, $route },
propsData: {
type: 'illustrated'
}
})
// mock a file dropped by a user
const file = { name: 'test.db' }
const dropData = { dataTransfer: new DataTransfer() }
Object.defineProperty(dropData.dataTransfer, 'files', {
value: [file],
writable: false
})
await wrapper.find('.drop-area').trigger('drop', dropData)
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
await db.loadDb.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
expect($router.push.calledOnceWith('/editor')).to.equal(true)
wrapper.destroy()
})
it("doesn't redirect if already on /editor", async () => {
// mock getting a file from user
const file = { name: 'test.db' }
sinon.stub(fu, 'getFileFromUser').resolves(file)
// mock db loading
const db = {
loadDb: sinon.stub().resolves()
}
sinon.stub(database, 'getNewDatabase').returns(db)
// mock router
const $router = { push: sinon.stub() }
const $route = { path: '/editor' }
// mount the component
const wrapper = shallowMount(DbUploader, {
attachTo: place,
store,
mocks: { $router, $route },
propsData: {
type: 'illustrated'
}
})
await wrapper.find('.drop-area').trigger('click')
await db.loadDb.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
expect($router.push.called).to.equal(false)
wrapper.destroy()
})
it('shows parse dialog if gets csv file', async () => {
// mock getting a file from user
const file = { name: 'test.csv' }
sinon.stub(fu, 'getFileFromUser').resolves(file)
// mock router
const $router = { push: sinon.stub() }
const $route = { path: '/editor' }
// mount the component
const wrapper = mount(DbUploader, {
attachTo: place,
store,
mocks: { $router, $route },
propsData: {
type: 'illustrated'
}
})
const CsvImport = wrapper.vm.$refs.addCsv
sinon.stub(CsvImport, 'reset')
sinon.stub(CsvImport, 'previewCsv').resolves()
sinon.stub(CsvImport, 'open')
await wrapper.find('.drop-area').trigger('click')
await wrapper.vm.$nextTick()
expect(CsvImport.reset.calledOnce).to.equal(true)
await wrapper.vm.animationPromise
expect(CsvImport.previewCsv.calledOnce).to.equal(true)
await wrapper.vm.$nextTick()
expect(CsvImport.open.calledOnce).to.equal(true)
wrapper.destroy()
})
it('deletes temporary db if CSV import is canceled', async () => {
// mock getting a file from user
const file = { name: 'test.csv' }
sinon.stub(fu, 'getFileFromUser').resolves(file)
// mock router
const $router = { push: sinon.stub() }
const $route = { path: '/editor' }
// mount the component
const wrapper = mount(DbUploader, {
store,
mocks: { $router, $route },
propsData: {
type: 'illustrated'
}
})
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.vm.$nextTick()
await CsvImport.$emit('cancel')
expect(wrapper.vm.newDb).to.equal(null)
})
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ import { expect } from 'chai'
import sinon from 'sinon'
import mutations from '@/store/mutations'
const {
saveSchema,
updateTab,
deleteTab,
setCurrentTabId,
@@ -24,25 +23,6 @@ describe('mutations', () => {
expect(oldDb.shutDown.calledOnce).to.equal(true)
})
it('saveSchema', () => {
const state = {}
const schema = [
{
name: 'table1',
columns: [
{ name: 'id', type: 'INTEGER' }
]
}
]
saveSchema(state, {
dbName: 'test',
schema
})
expect(state.dbName).to.equal('test')
expect(state.schema).to.eql(schema)
})
it('updateTab (save)', () => {
const tab = {
id: 1,

View File

@@ -4,6 +4,9 @@ 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)
@@ -16,7 +19,9 @@ describe('Schema.vue', () => {
it('Renders DB name on initial', () => {
// mock store state
const state = {
dbName: 'fooDB'
db: {
dbName: 'fooDB'
}
}
const store = new Vuex.Store({ state })
@@ -31,7 +36,9 @@ describe('Schema.vue', () => {
it('Schema visibility is toggled when click on DB name', async () => {
// mock store state
const state = {
dbName: 'fooDB'
db: {
dbName: 'fooDB'
}
}
const store = new Vuex.Store({ state })
@@ -48,30 +55,32 @@ describe('Schema.vue', () => {
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' }
]
}
]
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 })
@@ -101,15 +110,69 @@ describe('Schema.vue', () => {
it('exports db', async () => {
const state = {
dbName: 'fooDB',
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' }).trigger('click')
await wrapper.findComponent({ name: 'export-icon' }).find('svg').trigger('click')
expect(state.db.export.calledOnceWith('fooDB'))
})
it('adds table', async () => {
const file = { name: 'test.csv' }
sinon.stub(fIo, 'getFileFromUser').resolves(file)
sinon.stub(csv, 'parse').resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
const state = {
db: database.getNewDatabase()
}
state.db.dbName = 'db'
state.db.execute('CREATE TABLE foo(id)')
state.db.refreshSchema()
sinon.spy(state.db, 'refreshSchema')
const store = new Vuex.Store({ state })
const wrapper = mount(Schema, { store, localVue })
sinon.spy(wrapper.vm.$refs.addCsv, 'previewCsv')
sinon.spy(wrapper.vm, 'addCsv')
sinon.spy(wrapper.vm.$refs.addCsv, 'loadFromCsv')
await wrapper.findComponent({ name: 'add-table-icon' }).find('svg').trigger('click')
await wrapper.vm.$refs.addCsv.previewCsv.returnValues[0]
await wrapper.vm.addCsv.returnValues[0]
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick()
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(true)
await wrapper.find('#csv-import').trigger('click')
await wrapper.vm.$refs.addCsv.loadFromCsv.returnValues[0]
await wrapper.find('#csv-finish').trigger('click')
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
await state.db.refreshSchema.returnValues[0]
expect(wrapper.vm.$store.state.db.schema).to.eql([
{ name: 'test', columns: [{ name: 'col1', type: 'real' }, { name: 'col2', type: 'text' }] },
{ name: 'foo', columns: [{ name: 'id', type: 'N/A' }] }
])
const res = await wrapper.vm.$store.state.db.execute('select * from test')
expect(res).to.eql({
columns: ['col1', 'col2'],
values: [[1, 'foo']]
})
})
})

View File

@@ -7,6 +7,5 @@ describe('SqlEditor.vue', () => {
const wrapper = mount(SqlEditor)
await wrapper.findComponent({ name: 'codemirror' }).vm.$emit('input', 'SELECT * FROM foo')
expect(wrapper.emitted('input')[0]).to.eql(['SELECT * FROM foo'])
// Take a pause to keep proper state in debounced '@/views/Main/Editor/Tabs/Tab/SqlEditor/hint'
})
})

View File

@@ -11,22 +11,24 @@ describe('hint.js', () => {
it('Calculates table list for hint', () => {
// mock store state
const schema = [
{
name: 'foo',
columns: [
{ name: 'fooId', type: 'INTEGER' },
{ name: 'name', type: 'NVARCHAR(20)' }
]
},
{
name: 'bar',
columns: [
{ name: 'barId', type: 'INTEGER' }
]
}
]
sinon.stub(state, 'schema').value(schema)
const db = {
schema: [
{
name: 'foo',
columns: [
{ name: 'fooId', type: 'INTEGER' },
{ name: 'name', type: 'NVARCHAR(20)' }
]
},
{
name: 'bar',
columns: [
{ name: 'barId', type: 'INTEGER' }
]
}
]
}
sinon.stub(state, 'db').value(db)
// mock showHint and editor
sinon.stub(CM, 'showHint')
@@ -52,16 +54,18 @@ describe('hint.js', () => {
it('Add default table if there is only one table in schema', () => {
// mock store state
const schema = [
{
name: 'foo',
columns: [
{ name: 'fooId', type: 'INTEGER' },
{ name: 'name', type: 'NVARCHAR(20)' }
]
}
]
sinon.stub(state, 'schema').value(schema)
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')
@@ -190,7 +194,7 @@ describe('hint.js', () => {
it('tables is empty object when schema is null', () => {
// mock store state
sinon.stub(state, 'schema').value(null)
sinon.stub(state, 'db').value({ schema: null })
// mock showHint and editor
sinon.stub(CM, 'showHint')

View File

@@ -115,6 +115,7 @@ describe('Tab.vue', () => {
})
state.currentTabId = 1
await wrapper.vm.$nextTick()
expect(mutations.setCurrentTab.calledOnceWith(state, wrapper.vm)).to.equal(true)
})
@@ -181,7 +182,8 @@ describe('Tab.vue', () => {
const state = {
currentTabId: 1,
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()
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-preview.error').isVisible()).to.equal(true)
expect(wrapper.find('.table-preview.error').text()).to.include('There is no table foo')
expect(wrapper.find('.table-view .result-in-progress').exists()).to.equal(false)
expect(wrapper.findComponent({ name: 'logs' }).isVisible()).to.equal(true)
expect(wrapper.findComponent({ name: 'logs' }).text()).to.include('There is no table foo')
})
it('Passes result to sql-table component', async () => {
@@ -220,7 +222,7 @@ describe('Tab.vue', () => {
currentTabId: 1,
db: {
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()
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-preview.error').isVisible()).to.equal(false)
expect(wrapper.find('.table-view .result-in-progress').exists()).to.equal(false)
expect(wrapper.findComponent({ name: 'logs' }).exists()).to.equal(false)
expect(wrapper.findComponent({ name: 'SqlTable' }).vm.dataSet).to.eql(result)
})
@@ -252,36 +254,17 @@ describe('Tab.vue', () => {
columns: ['id', 'name'],
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
const state = {
currentTabId: 1,
dbName: 'fooDb',
db: {
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 })
// mount the component
@@ -299,7 +282,6 @@ describe('Tab.vue', () => {
})
await wrapper.vm.execute()
expect(state.db.getSchema.calledOnceWith('fooDb')).to.equal(true)
expect(mutations.saveSchema.calledOnceWith(state, newSchema)).to.equal(true)
expect(state.db.refreshSchema.calledOnce).to.equal(true)
})
})

View File

@@ -20,7 +20,7 @@ describe('MainMenu.vue', () => {
const state = {
currentTab: { query: '', execute: sinon.stub() },
tabs: [{}],
schema: []
db: {}
}
const store = new Vuex.Store({ state })
const $route = { path: '/editor' }
@@ -49,7 +49,7 @@ describe('MainMenu.vue', () => {
const state = {
currentTab: null,
tabs: [{}],
schema: []
db: {}
}
const store = new Vuex.Store({ state })
const $route = { path: '/editor' }
@@ -65,11 +65,11 @@ describe('MainMenu.vue', () => {
expect(wrapper.find('#create-btn').isVisible()).to.equal(true)
})
it('Run is disabled if there is no schema or no query', async () => {
it('Run is disabled if there is no db or no query', async () => {
const state = {
currentTab: { query: 'SELECT * FROM foo', execute: sinon.stub() },
tabs: [{}],
schema: null
db: null
}
const store = new Vuex.Store({ state })
const $route = { path: '/editor' }
@@ -82,7 +82,7 @@ describe('MainMenu.vue', () => {
const vm = wrapper.vm
expect(wrapper.find('#run-btn').element.disabled).to.equal(true)
await vm.$set(state, 'schema', [])
await vm.$set(state, 'db', {})
expect(wrapper.find('#run-btn').element.disabled).to.equal(false)
await vm.$set(state.currentTab, 'query', '')
@@ -97,7 +97,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ isUnsaved: true }],
schema: null
db: {}
}
const store = new Vuex.Store({ state })
const $route = { path: '/editor' }
@@ -122,7 +122,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ isUnsaved: true }],
schema: null
db: {}
}
const newQueryId = 1
const actions = {
@@ -156,7 +156,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ isUnsaved: true }],
schema: null
db: {}
}
const newQueryId = 1
const actions = {
@@ -191,7 +191,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ isUnsaved: true }],
schema: []
db: {}
}
const store = new Vuex.Store({ state })
const $route = { path: '/editor' }
@@ -212,14 +212,14 @@ describe('MainMenu.vue', () => {
expect(state.currentTab.execute.calledTwice).to.equal(true)
// Running is disabled and route path is editor
await wrapper.vm.$set(state, 'schema', null)
await wrapper.vm.$set(state, 'db', null)
document.dispatchEvent(ctrlR)
expect(state.currentTab.execute.calledTwice).to.equal(true)
document.dispatchEvent(metaR)
expect(state.currentTab.execute.calledTwice).to.equal(true)
// Running is enabled and route path is not editor
await wrapper.vm.$set(state, 'schema', [])
await wrapper.vm.$set(state, 'db', {})
await wrapper.vm.$set($route, 'path', '/my-queries')
document.dispatchEvent(ctrlR)
expect(state.currentTab.execute.calledTwice).to.equal(true)
@@ -236,7 +236,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ isUnsaved: true }],
schema: []
db: {}
}
const store = new Vuex.Store({ state })
const $route = { path: '/editor' }
@@ -257,14 +257,14 @@ describe('MainMenu.vue', () => {
expect(state.currentTab.execute.calledTwice).to.equal(true)
// Running is disabled and route path is editor
await wrapper.vm.$set(state, 'schema', null)
await wrapper.vm.$set(state, 'db', null)
document.dispatchEvent(ctrlEnter)
expect(state.currentTab.execute.calledTwice).to.equal(true)
document.dispatchEvent(metaEnter)
expect(state.currentTab.execute.calledTwice).to.equal(true)
// Running is enabled and route path is not editor
await wrapper.vm.$set(state, 'schema', [])
await wrapper.vm.$set(state, 'db', {})
await wrapper.vm.$set($route, 'path', '/my-queries')
document.dispatchEvent(ctrlEnter)
expect(state.currentTab.execute.calledTwice).to.equal(true)
@@ -280,7 +280,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ isUnsaved: true }],
schema: []
db: {}
}
const store = new Vuex.Store({ state })
const $route = { path: '/editor' }
@@ -315,7 +315,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ isUnsaved: true }],
schema: []
db: {}
}
const store = new Vuex.Store({ state })
const $route = { path: '/editor' }
@@ -360,7 +360,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ id: 1, name: 'foo', isUnsaved: true }],
schema: []
db: {}
}
const mutations = {
updateTab: sinon.stub()
@@ -378,7 +378,7 @@ describe('MainMenu.vue', () => {
wrapper = mount(MainMenu, {
store,
mocks: { $route },
stubs: ['router-link']
stubs: ['router-link', 'app-diagnostic-info']
})
await wrapper.find('#save-btn').trigger('click')
@@ -411,7 +411,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ id: 1, name: null, tempName: 'Untitled', isUnsaved: true }],
schema: []
db: {}
}
const mutations = {
updateTab: sinon.stub()
@@ -429,7 +429,7 @@ describe('MainMenu.vue', () => {
wrapper = mount(MainMenu, {
store,
mocks: { $route },
stubs: ['router-link']
stubs: ['router-link', 'app-diagnostic-info']
})
await wrapper.find('#save-btn').trigger('click')
@@ -456,7 +456,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ id: 1, name: null, tempName: 'Untitled', isUnsaved: true }],
schema: []
db: {}
}
const mutations = {
updateTab: sinon.stub()
@@ -474,7 +474,7 @@ describe('MainMenu.vue', () => {
wrapper = mount(MainMenu, {
store,
mocks: { $route },
stubs: ['router-link']
stubs: ['router-link', 'app-diagnostic-info']
})
await wrapper.find('#save-btn').trigger('click')
@@ -528,7 +528,7 @@ describe('MainMenu.vue', () => {
view: 'chart'
},
tabs: [{ id: 1, name: 'foo', isUnsaved: true, isPredefined: true }],
schema: []
db: {}
}
const mutations = {
updateTab: sinon.stub()
@@ -546,7 +546,7 @@ describe('MainMenu.vue', () => {
wrapper = mount(MainMenu, {
store,
mocks: { $route },
stubs: ['router-link']
stubs: ['router-link', 'app-diagnostic-info']
})
await wrapper.find('#save-btn').trigger('click')
@@ -607,7 +607,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ id: 1, name: null, tempName: 'Untitled', isUnsaved: true }],
schema: []
db: {}
}
const mutations = {
updateTab: sinon.stub()
@@ -625,7 +625,7 @@ describe('MainMenu.vue', () => {
wrapper = mount(MainMenu, {
store,
mocks: { $route },
stubs: ['router-link']
stubs: ['router-link', 'app-diagnostic-info']
})
await wrapper.find('#save-btn').trigger('click')