1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-07 02:28:54 +08:00

18 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
45 changed files with 1740 additions and 1730 deletions

View File

@@ -7,11 +7,11 @@ module.exports = {
milestoneMatch: 'v{{tag_name}}', milestoneMatch: 'v{{tag_name}}',
template: { template: {
issue: '- {{name}} [{{text}}]({{url}})', issue: '- {{name}} [{{text}}]({{url}})',
changelogTitle: "## Release notes\n\n", changelogTitle: "",
release: "{{body}}", release: "{{body}}",
}, },
groupBy: { groupBy: {
'Enhancements:': ["enhancement", "internal"], 'Enhancements': ["enhancement", "internal"],
'Bug fixes:': ["bug"] 'Bug fixes': ["bug"]
} }
} }

View File

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

695
package-lock.json generated
View File

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

View File

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

View File

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

View File

@@ -13,7 +13,14 @@ export default {
result.columns = source.meta.fields.map(col => col.trim()) result.columns = source.meta.fields.map(col => col.trim())
result.values = source.data.map(row => { result.values = source.data.map(row => {
const resultRow = [] const resultRow = []
result.columns.forEach(col => { resultRow.push(row[col]) }) source.meta.fields.forEach(col => {
let value = row[col]
if (value instanceof Date) {
value = value.toISOString()
}
resultRow.push(value)
})
return resultRow return resultRow
}) })
} else { } 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,551 +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 === '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 -->
<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 fIo 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'
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')) {
this.state = 'dropped'
resolve()
}
})
})
}
},
watch: {
quoteChar () {
this.previewCSV()
},
escapeChar () {
this.previewCSV()
},
header () {
this.previewCSV()
}
},
methods: {
cancelCsvImport () {
if (!this.disableDialog) {
this.$modal.hide('parse')
if (this.newDb) {
this.newDb.shutDown()
this.newDb = null
}
}
},
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 = 'dropping'
if (fIo.isDatabase(file)) {
this.loadDb(file)
} else {
this.file = file
this.header = true
this.quoteChar = '"'
this.escapeChar = '"'
this.delimiter = ''
return Promise.all([this.previewCSV(), this.animationPromise])
.then(() => {
this.$modal.show('parse')
})
}
},
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;
}
/* 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

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

View File

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

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) return this.db.exec(sql, params)
} }
import (columns, values, progressCounterId, progressCallback, chunkSize = 1500) { import (tabName, columns, values, progressCounterId, progressCallback, chunkSize = 1500) {
this.createDb() if (this.db === null) {
this.db.exec(dbUtils.getCreateStatement(columns, values)) this.createDb()
}
this.db.exec(dbUtils.getCreateStatement(tabName, columns, values))
const chunks = dbUtils.generateChunks(values, chunkSize) const chunks = dbUtils.generateChunks(values, chunkSize)
const chunksAmount = Math.ceil(values.length / chunkSize) const chunksAmount = Math.ceil(values.length / chunkSize)
let count = 0 let count = 0
const insertStr = dbUtils.getInsertStmt(columns) const insertStr = dbUtils.getInsertStmt(tabName, columns)
const insertStmt = this.db.prepare(insertStr) const insertStmt = this.db.prepare(insertStr)
progressCallback({ progress: 0, id: progressCounterId }) progressCallback({ progress: 0, id: progressCounterId })

View File

@@ -1,3 +1,5 @@
import sqliteParser from 'sqlite-parser'
export default { export default {
* generateChunks (arr, size) { * generateChunks (arr, size) {
const count = Math.ceil(arr.length / size) const count = Math.ceil(arr.length / size)
@@ -9,14 +11,14 @@ export default {
} }
}, },
getInsertStmt (columns) { getInsertStmt (tabName, columns) {
const colList = `"${columns.join('", "')}"` const colList = `"${columns.join('", "')}"`
const params = columns.map(() => '?').join(', ') const params = columns.map(() => '?').join(', ')
return `INSERT INTO csv_import (${colList}) VALUES (${params});` return `INSERT INTO "${tabName}" (${colList}) VALUES (${params});`
}, },
getCreateStatement (columns, values) { getCreateStatement (tabName, columns, values) {
let result = 'CREATE table csv_import(' let result = `CREATE table "${tabName}"(`
columns.forEach((col, index) => { columns.forEach((col, index) => {
// Get the first row of values to determine types // Get the first row of values to determine types
const value = values[0][index] const value = values[0][index]
@@ -40,5 +42,49 @@ export default {
}) })
result = result.replace(/,\s$/, ');') result = result.replace(/,\s$/, ');')
return result 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) { switch (data && data.action) {
case 'open': case 'open':
return sql.open(data.buffer) return sql.open(data.buffer)
case 'reopen':
return sql.open(sql.export())
case 'exec': case 'exec':
return sql.exec(data.sql, data.params) return sql.exec(data.sql, data.params)
case 'import': case 'import':
return sql.import(data.columns, data.values, data.progressCounterId, postMessage) return sql.import(
data.tabName,
data.columns,
data.values,
data.progressCounterId,
postMessage
)
case 'export': case 'export':
return sql.export() return sql.export()
case 'close': case 'close':

View File

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

View File

@@ -6,6 +6,10 @@ export default {
: /\.(db|sqlite(3)?)+$/.test(file.name) : /\.(db|sqlite(3)?)+$/.test(file.name)
}, },
getFileName (file) {
return file.name.replace(/\.[^.]+$/, '')
},
exportToFile (str, fileName, type = 'octet/stream') { exportToFile (str, fileName, type = 'octet/stream') {
// Create downloader // Create downloader
const downloader = document.createElement('a') const downloader = document.createElement('a')

View File

@@ -3,5 +3,13 @@ export default {
const diff = end.getTime() - start.getTime() const diff = end.getTime() - start.getTime()
const seconds = diff / 1000 const seconds = diff / 1000
return seconds.toFixed(3) + 's' 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 MyQueries from '@/views/Main/MyQueries'
import Welcome from '@/views/Welcome' import Welcome from '@/views/Welcome'
import Main from '@/views/Main' import Main from '@/views/Main'
import store from '@/store'
import database from '@/lib/database'
Vue.use(VueRouter) Vue.use(VueRouter)
@@ -36,4 +38,13 @@ const router = new VueRouter({
routes 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 export default router

View File

@@ -7,10 +7,6 @@ export default {
} }
state.db = db state.db = db
}, },
saveSchema (state, { dbName, schema }) {
state.dbName = dbName
state.schema = schema
},
updateTab (state, { index, name, id, query, chart, isUnsaved }) { updateTab (state, { index, name, id, query, chart, isUnsaved }) {
const tab = state.tabs[index] const tab = state.tabs[index]

View File

@@ -1,7 +1,4 @@
export default { export default {
schema: null,
dbFile: null,
dbName: null,
tabs: [], tabs: [],
currentTab: null, currentTab: null,
currentTabId: 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> </div>
<db-uploader id="db-edit" type="small" /> <db-uploader id="db-edit" type="small" />
<export-icon tooltip="Export database" @click="exportToFile"/> <export-icon tooltip="Export database" @click="exportToFile"/>
<add-table-icon @click="addCsv"/>
</div> </div>
<div v-show="schemaVisible" class="schema"> <div v-show="schemaVisible" class="schema">
<table-description <table-description
@@ -19,15 +20,26 @@
:columns="table.columns" :columns="table.columns"
/> />
</div> </div>
<!--Parse csv dialog -->
<csv-import
ref="addCsv"
:file="file"
:db="$store.state.db"
dialog-name="addCsv"
/>
</div> </div>
</template> </template>
<script> <script>
import fIo from '@/lib/utils/fileIo'
import TableDescription from './TableDescription' import TableDescription from './TableDescription'
import TextField from '@/components/TextField' import TextField from '@/components/TextField'
import TreeChevron from '@/components/svg/treeChevron' import TreeChevron from '@/components/svg/treeChevron'
import DbUploader from '@/components/DbUploader' import DbUploader from '@/components/DbUploader'
import ExportIcon from '@/components/svg/export' import ExportIcon from '@/components/svg/export'
import AddTableIcon from '@/components/svg/addTable'
import CsvImport from '@/components/CsvImport'
export default { export default {
name: 'Schema', name: 'Schema',
@@ -36,33 +48,44 @@ export default {
TextField, TextField,
TreeChevron, TreeChevron,
DbUploader, DbUploader,
ExportIcon ExportIcon,
AddTableIcon,
CsvImport
}, },
data () { data () {
return { return {
schemaVisible: true, schemaVisible: true,
filter: null filter: null,
file: null
} }
}, },
computed: { computed: {
schema () { schema () {
if (!this.$store.state.schema) { if (!this.$store.state.db.schema) {
return [] return []
} }
return !this.filter return !this.filter
? this.$store.state.schema ? this.$store.state.db.schema
: this.$store.state.schema.filter( : this.$store.state.db.schema.filter(
table => table.name.toUpperCase().indexOf(this.filter.toUpperCase()) !== -1 table => table.name.toUpperCase().indexOf(this.filter.toUpperCase()) !== -1
) )
}, },
dbName () { dbName () {
return this.$store.state.dbName return this.$store.state.db.dbName
} }
}, },
methods: { methods: {
exportToFile () { exportToFile () {
this.$store.state.db.export(`${this.dbName}.sqlite`) this.$store.state.db.export(`${this.dbName}.sqlite`)
},
async addCsv () {
this.file = await fIo.getFileFromUser('.csv')
await this.$nextTick()
const csvImport = this.$refs.addCsv
csvImport.reset()
await csvImport.previewCsv()
csvImport.open()
} }
} }
} }

View File

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

View File

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

View File

@@ -8,7 +8,7 @@
> >
<template #left-pane> <template #left-pane>
<div class="query-editor"> <div class="query-editor">
<sql-editor v-model="query" /> <sql-editor ref="sqlEditor" v-model="query" />
</div> </div>
</template> </template>
<template #right-pane> <template #right-pane>
@@ -86,9 +86,6 @@ export default {
return this.id === this.$store.state.currentTabId return this.id === this.$store.state.currentTabId
} }
}, },
created () {
this.$store.commit('setCurrentTab', this)
},
mounted () { mounted () {
this.resizeObserver = new ResizeObserver(this.handleResize) this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.bottomPane) this.resizeObserver.observe(this.$refs.bottomPane)
@@ -98,9 +95,14 @@ export default {
this.resizeObserver.unobserve(this.$refs.bottomPane) this.resizeObserver.unobserve(this.$refs.bottomPane)
}, },
watch: { watch: {
isActive () { isActive: {
if (this.isActive) { immediate: true,
this.$store.commit('setCurrentTab', this) async handler () {
if (this.isActive) {
this.$store.commit('setCurrentTab', this)
await this.$nextTick()
this.$refs.sqlEditor.focus()
}
} }
}, },
query () { query () {
@@ -118,14 +120,13 @@ export default {
const start = new Date() const start = new Date()
this.result = await state.db.execute(this.query + ';') this.result = await state.db.execute(this.query + ';')
this.time = time.getPeriod(start, new Date()) this.time = time.getPeriod(start, new Date())
const schema = await state.db.getSchema(state.dbName)
this.$store.commit('saveSchema', schema)
} catch (err) { } catch (err) {
this.error = { this.error = {
type: 'error', type: 'error',
message: err message: err
} }
} }
state.db.refreshSchema()
this.isGettingResults = false this.isGettingResults = false
}, },
handleResize () { handleResize () {
@@ -141,12 +142,12 @@ export default {
calculateTableHeight () { calculateTableHeight () {
const bottomPane = this.$refs.bottomPane const bottomPane = this.$refs.bottomPane
// 88 - view swittcher height // 88 - view swittcher height
// 42 - table footer width // 34 - table footer width
// 30 - desirable space after the table // 12 - desirable space after the table
// 5 - padding-bottom of rounded table container // 5 - padding-bottom of rounded table container
// 40 - height of table header // 35 - height of table header
const freeSpace = bottomPane.offsetHeight - 88 - 42 - 30 - 5 - 40 const freeSpace = bottomPane.offsetHeight - 88 - 34 - 12 - 5 - 35
this.tableViewHeight = freeSpace - (freeSpace % 40) this.tableViewHeight = freeSpace - (freeSpace % 35)
} }
} }
} }

View File

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

View File

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

View File

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

View File

@@ -1,172 +1,26 @@
import { expect } from 'chai' import { expect } from 'chai'
import sinon from 'sinon' import sinon from 'sinon'
import Vuex from 'vuex' import Vuex from 'vuex'
import { shallowMount, mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import DbUploader from '@/components/DbUploader' import CsvImport from '@/components/CsvImport'
import fu from '@/lib/utils/fileIo' import csv from '@/components/CsvImport/csv'
import database from '@/lib/database'
import csv from '@/components/DbUploader/csv'
describe('DbUploader.vue', () => { describe('CsvImport.vue', () => {
let state = {} 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 = { name: 'test.db' }
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 = { 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(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 = { name: 'test.db' }
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 = {} let actions = {}
const newTabId = 1 let mutations = {}
let store = {} let store = {}
let place
// mock router
const $router = { }
const $route = { path: '/' }
let clock let clock
let wrapper let wrapper
const newTabId = 1
const file = { name: 'my data.csv' }
beforeEach(() => { beforeEach(() => {
// mock getting a file from user
sinon.stub(fu, 'getFileFromUser').resolves({ type: 'text/csv', name: 'foo.csv' })
clock = sinon.useFakeTimers() clock = sinon.useFakeTimers()
// mock store state and mutations // mock store state and mutations
state = {} state = {}
mutations = { mutations = {
saveSchema: sinon.stub(),
setDb: sinon.stub(), setDb: sinon.stub(),
setCurrentTabId: sinon.stub() setCurrentTabId: sinon.stub()
} }
@@ -175,29 +29,32 @@ describe('DbUploader.vue import CSV', () => {
} }
store = new Vuex.Store({ state, mutations, actions }) store = new Vuex.Store({ state, mutations, actions })
$router.push = sinon.stub() const db = {
sanitizeTableName: sinon.stub().returns('my_data'),
place = document.createElement('div') addTableFromCsv: sinon.stub().resolves(),
document.body.appendChild(place) createProgressCounter: sinon.stub().returns(1),
deleteProgressCounter: sinon.stub(),
validateTableName: sinon.stub().resolves(),
execute: sinon.stub().resolves(),
refreshSchema: sinon.stub().resolves()
}
// mount the component // mount the component
wrapper = mount(DbUploader, { wrapper = mount(CsvImport, {
attachTo: place,
store, store,
mocks: { $router, $route },
propsData: { propsData: {
type: 'illustrated' file,
dialogName: 'addCsv',
db
} }
}) })
}) })
afterEach(() => { afterEach(() => {
sinon.restore() sinon.restore()
wrapper.destroy()
place.remove()
}) })
it('shows parse dialog if gets csv file', async () => { it('previews', async () => {
sinon.stub(csv, 'parse').resolves({ sinon.stub(csv, 'parse').resolves({
delimiter: '|', delimiter: '|',
data: { data: {
@@ -216,12 +73,11 @@ describe('DbUploader.vue import CSV', () => {
}] }]
}) })
await wrapper.find('.drop-area').trigger('click') wrapper.vm.previewCsv()
await csv.parse.returnValues[0] await wrapper.vm.open()
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick() expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(true)
expect(wrapper.find('[data-modal="parse"]').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.findComponent({ name: 'delimiter-selector' }).vm.value).to.equal('|')
expect(wrapper.find('#quote-char input').element.value).to.equal('"') expect(wrapper.find('#quote-char input').element.value).to.equal('"')
expect(wrapper.find('#escape-char input').element.value).to.equal('"') expect(wrapper.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 csv.parse.returnValues[0]
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
parse.onCall(1).resolves({ parse.onCall(1).resolves({
@@ -362,17 +217,18 @@ describe('DbUploader.vue import CSV', () => {
} }
}) })
await wrapper.find('.drop-area').trigger('click') wrapper.vm.previewCsv()
await csv.parse.returnValues[0] wrapper.vm.open()
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
let resolveParsing let resolveParsing
parse.onCall(1).returns(new Promise(resolve => { parse.onCall(1).returns(new Promise(resolve => {
resolveParsing = resolve resolveParsing = resolve
})) }))
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click') await wrapper.find('#csv-import').trigger('click')
await wrapper.vm.$nextTick()
// "Parsing CSV..." in the logs // "Parsing CSV..." in the logs
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text()) expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text())
@@ -430,12 +286,16 @@ describe('DbUploader.vue import CSV', () => {
messages: [] messages: []
}) })
await wrapper.find('.drop-area').trigger('click') wrapper.vm.previewCsv()
await csv.parse.returnValues[0] wrapper.vm.open()
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick() 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 wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1] await csv.parse.returnValues[1]
await wrapper.vm.$nextTick() 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.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false) expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true) expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
await resolveImport()
}) })
it('parsing is completed with notes', async () => { it('parsing is completed with notes', async () => {
@@ -488,12 +349,16 @@ describe('DbUploader.vue import CSV', () => {
}] }]
}) })
await wrapper.find('.drop-area').trigger('click') let resolveImport
await csv.parse.returnValues[0] wrapper.vm.db.addTableFromCsv.onCall(0).returns(new Promise(resolve => {
await wrapper.vm.animationPromise resolveImport = resolve
await wrapper.vm.$nextTick() }))
wrapper.vm.previewCsv()
wrapper.vm.open()
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click') await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1] await csv.parse.returnValues[1]
await wrapper.vm.$nextTick() 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.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false) expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true) expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
await resolveImport()
}) })
it('parsing is completed with errors', async () => { it('parsing is completed with errors', async () => {
@@ -548,12 +414,11 @@ describe('DbUploader.vue import CSV', () => {
}] }]
}) })
await wrapper.find('.drop-area').trigger('click') wrapper.vm.previewCsv()
await csv.parse.returnValues[0] wrapper.vm.open()
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click') await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1] await csv.parse.returnValues[1]
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
@@ -604,19 +469,14 @@ describe('DbUploader.vue import CSV', () => {
}) })
let resolveImport = sinon.stub() let resolveImport = sinon.stub()
const newDb = { wrapper.vm.db.addTableFromCsv = sinon.stub()
importDb: sinon.stub().resolves(new Promise(resolve => { resolveImport = resolve })), .resolves(new Promise(resolve => { resolveImport = resolve }))
createProgressCounter: sinon.stub().returns(1),
deleteProgressCounter: sinon.stub()
}
sinon.stub(database, 'getNewDatabase').returns(newDb)
await wrapper.find('.drop-area').trigger('click') wrapper.vm.previewCsv()
await csv.parse.returnValues[0] wrapper.vm.open()
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click') await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1] await csv.parse.returnValues[1]
await wrapper.vm.$nextTick() 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.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false) expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true) 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 // After resolving - loading indicator is not shown
await resolveImport() await resolveImport()
await newDb.importDb.returnValues[0] await wrapper.vm.db.addTableFromCsv.returnValues[0]
expect( expect(
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists() wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
).to.equal(false) ).to.equal(false)
@@ -678,20 +538,11 @@ describe('DbUploader.vue import CSV', () => {
messages: [] messages: []
}) })
const schema = {} wrapper.vm.previewCsv()
const newDb = { wrapper.vm.open()
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()
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click') await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1] await csv.parse.returnValues[1]
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
@@ -739,19 +590,13 @@ describe('DbUploader.vue import CSV', () => {
messages: [] messages: []
}) })
const newDb = { wrapper.vm.db.addTableFromCsv = sinon.stub().rejects(new Error('fail'))
importDb: sinon.stub().rejects(new Error('fail')),
createProgressCounter: sinon.stub().returns(1),
deleteProgressCounter: sinon.stub()
}
sinon.stub(database, 'getNewDatabase').returns(newDb)
await wrapper.find('.drop-area').trigger('click') wrapper.vm.previewCsv()
await csv.parse.returnValues[0] wrapper.vm.open()
await wrapper.vm.animationPromise
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click') await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1] await csv.parse.returnValues[1]
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
@@ -773,7 +618,7 @@ describe('DbUploader.vue import CSV', () => {
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false) expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
}) })
it('import final', async () => { it('import finish', async () => {
sinon.stub(csv, 'parse').resolves({ sinon.stub(csv, 'parse').resolves({
delimiter: '|', delimiter: '|',
data: { data: {
@@ -786,33 +631,20 @@ describe('DbUploader.vue import CSV', () => {
messages: [] messages: []
}) })
const schema = {} wrapper.vm.previewCsv()
const newDb = { wrapper.vm.open()
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()
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
await wrapper.find('#csv-import').trigger('click') await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
await wrapper.find('#csv-finish').trigger('click') 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) expect(actions.addTab.calledOnce).to.equal(true)
await actions.addTab.returnValues[0] await actions.addTab.returnValues[0]
expect(mutations.setCurrentTabId.calledOnceWith(state, newTabId)).to.equal(true) expect(mutations.setCurrentTabId.calledOnceWith(state, newTabId)).to.equal(true)
expect($router.push.calledOnceWith('/editor')).to.equal(true) expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
expect(wrapper.find('[data-modal="parse"]').exists()).to.equal(false) expect(wrapper.emitted('finish')).to.have.lengthOf(1)
}) })
it('import cancel', async () => { it('import cancel', async () => {
@@ -828,79 +660,47 @@ describe('DbUploader.vue import CSV', () => {
messages: [] messages: []
}) })
const schema = {} await wrapper.vm.previewCsv()
const newDb = { await wrapper.vm.open()
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.$nextTick() await wrapper.vm.$nextTick()
await wrapper.find('#csv-import').trigger('click') await wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
await wrapper.find('#csv-cancel').trigger('click') await wrapper.find('#csv-cancel').trigger('click')
expect(mutations.setDb.called).to.equal(false) expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
expect(mutations.saveSchema.called).to.equal(false) expect(wrapper.vm.db.execute.calledOnceWith('DROP TABLE "my_data"')).to.equal(true)
expect(actions.addTab.called).to.equal(false) expect(wrapper.vm.db.refreshSchema.calledOnce).to.equal(true)
expect(mutations.setCurrentTabId.called).to.equal(false) expect(wrapper.emitted('cancel')).to.have.lengthOf(1)
expect($router.push.called).to.equal(false)
expect(newDb.shutDown.calledOnce).to.equal(true)
expect(wrapper.find('[data-modal="parse"]').exists()).to.equal(false)
}) })
it("doesn't open new tab when load db after importing CSV", async () => { it('checks table name', async () => {
fu.getFileFromUser.onCall(0).resolves({ type: 'text/csv', name: 'foo.csv' }) sinon.stub(csv, 'parse').resolves()
fu.getFileFromUser.onCall(1).resolves({ type: 'application/x-sqlite3', name: 'bar.sqlite3' }) await wrapper.vm.previewCsv()
sinon.stub(csv, 'parse').resolves({ await wrapper.vm.open()
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
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
await wrapper.find('#csv-table-name input').setValue('foo')
await clock.tick(400)
await wrapper.vm.$nextTick() 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 wrapper.find('#csv-import').trigger('click')
await csv.parse.returnValues[1] expect(wrapper.find('#csv-table-name .text-field-error').text())
await wrapper.vm.$nextTick() .to.equal("Table name can't be empty")
expect(wrapper.vm.db.addTableFromCsv.called).to.equal(false)
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)
}) })
}) })

View File

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

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai' import { expect } from 'chai'
import sinon from 'sinon' import sinon from 'sinon'
import csv from '@/components/DbUploader/csv' import csv from '@/components/CsvImport/csv'
import Papa from 'papaparse' import Papa from 'papaparse'
describe('csv.js', () => { describe('csv.js', () => {
@@ -11,18 +11,18 @@ describe('csv.js', () => {
it('getResult with fields', () => { it('getResult with fields', () => {
const source = { const source = {
data: [ data: [
{ id: 1, name: 'foo' }, { id: 1, 'name ': 'foo', date: new Date('2021-06-30T14:10:24.717Z') },
{ id: 2, name: 'bar' } { id: 2, 'name ': 'bar', date: new Date('2021-07-30T14:10:15.717Z') }
], ],
meta: { meta: {
fields: ['id', 'name '] fields: ['id', 'name ', 'date']
} }
} }
expect(csv.getResult(source)).to.eql({ expect(csv.getResult(source)).to.eql({
columns: ['id', 'name'], columns: ['id', 'name', 'date'],
values: [ values: [
[1, 'foo'], [1, 'foo', '2021-06-30T14:10:24.717Z'],
[2, 'bar'] [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 progressCallback = sinon.stub()
const progressCounterId = 1 const progressCounterId = 1
const sql = await Sql.build() const sql = await Sql.build()
sql.import(data.columns, data.values, progressCounterId, progressCallback, 2) sql.import('foo', data.columns, data.values, progressCounterId, progressCallback, 2)
const result = sql.exec('SELECT * from csv_import') const result = sql.exec('SELECT * from foo')
expect(result).to.have.lengthOf(1) expect(result).to.have.lengthOf(1)
expect(result[0].columns).to.eql(['id', 'name']) expect(result[0].columns).to.eql(['id', 'name'])
expect(result[0].values).to.have.lengthOf(4) expect(result[0].values).to.have.lengthOf(4)
@@ -135,7 +135,7 @@ describe('_sql.js', () => {
expect(sql.db.db).to.equal(null) expect(sql.db.db).to.equal(null)
}) })
it('overwrites', async () => { it('adds', async () => {
const sql = await Sql.build() const sql = await Sql.build()
sql.exec(` sql.exec(`
CREATE TABLE test ( CREATE TABLE test (
@@ -160,12 +160,11 @@ describe('_sql.js', () => {
[4, 'Ron Weasley'] [4, 'Ron Weasley']
] ]
} }
// rewrite the database by import // import adds table
sql.import(data.columns, data.values, 1, sinon.stub(), 2) sql.import('foo', data.columns, data.values, 1, sinon.stub(), 2)
result = sql.exec('SELECT * from csv_import') result = sql.exec('SELECT * from foo')
expect(result[0].values).to.have.lengthOf(4) expect(result[0].values).to.have.lengthOf(4)
result = sql.exec('SELECT * from test')
// test table oesn't exists anymore: the db was overwritten expect(result[0].values).to.have.lengthOf(2)
expect(() => { sql.exec('SELECT * from test') }).to.throw('no such table: test')
}) })
}) })

View File

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

View File

@@ -1,5 +1,5 @@
import { expect } from 'chai' import { expect } from 'chai'
import fu from '@/lib/utils/fileIo' import fIo from '@/lib/utils/fileIo'
import sinon from 'sinon' import sinon from 'sinon'
describe('fileIo.js', () => { describe('fileIo.js', () => {
@@ -15,7 +15,7 @@ describe('fileIo.js', () => {
sinon.spy(URL, 'revokeObjectURL') sinon.spy(URL, 'revokeObjectURL')
sinon.spy(window, 'Blob') sinon.spy(window, 'Blob')
fu.exportToFile('foo', 'foo.txt') fIo.exportToFile('foo', 'foo.txt')
expect(document.createElement.calledOnceWith('a')).to.equal(true) expect(document.createElement.calledOnceWith('a')).to.equal(true)
@@ -40,7 +40,7 @@ describe('fileIo.js', () => {
sinon.spy(URL, 'revokeObjectURL') sinon.spy(URL, 'revokeObjectURL')
sinon.spy(window, 'Blob') sinon.spy(window, 'Blob')
fu.exportToFile('foo', 'foo.html', 'text/html') fIo.exportToFile('foo', 'foo.html', 'text/html')
expect(document.createElement.calledOnceWith('a')).to.equal(true) expect(document.createElement.calledOnceWith('a')).to.equal(true)
@@ -71,7 +71,7 @@ describe('fileIo.js', () => {
setTimeout(() => { spyInput.dispatchEvent(new Event('change')) }) setTimeout(() => { spyInput.dispatchEvent(new Event('change')) })
const data = await fu.importFile() const data = await fIo.importFile()
expect(data).to.equal('foo') expect(data).to.equal('foo')
expect(document.createElement.calledOnceWith('input')).to.equal(true) expect(document.createElement.calledOnceWith('input')).to.equal(true)
expect(spyInput.type).to.equal('file') expect(spyInput.type).to.equal('file')
@@ -82,13 +82,13 @@ describe('fileIo.js', () => {
it('readFile', () => { it('readFile', () => {
sinon.spy(window, 'fetch') sinon.spy(window, 'fetch')
fu.readFile('./foo.bar') fIo.readFile('./foo.bar')
expect(window.fetch.calledOnceWith('./foo.bar')).to.equal(true) expect(window.fetch.calledOnceWith('./foo.bar')).to.equal(true)
}) })
it('readAsArrayBuffer resolves', async () => { it('readAsArrayBuffer resolves', async () => {
const blob = new Blob(['foo']) const blob = new Blob(['foo'])
const buffer = await fu.readAsArrayBuffer(blob) const buffer = await fIo.readAsArrayBuffer(blob)
const uint8Array = new Uint8Array(buffer) const uint8Array = new Uint8Array(buffer)
const text = new TextDecoder().decode(uint8Array) const text = new TextDecoder().decode(uint8Array)
@@ -103,29 +103,34 @@ describe('fileIo.js', () => {
sinon.stub(window, 'FileReader').returns(r) sinon.stub(window, 'FileReader').returns(r)
const blob = new Blob(['foo']) const blob = new Blob(['foo'])
await expect(fu.readAsArrayBuffer(blob)).to.be.rejectedWith('Problem parsing input file.') await expect(fIo.readAsArrayBuffer(blob)).to.be.rejectedWith('Problem parsing input file.')
}) })
it('isDatabase', () => { it('isDatabase', () => {
let file = { type: 'application/vnd.sqlite3' } let file = { type: 'application/vnd.sqlite3' }
expect(fu.isDatabase(file)).to.equal(true) expect(fIo.isDatabase(file)).to.equal(true)
file = { type: 'application/x-sqlite3' } file = { type: 'application/x-sqlite3' }
expect(fu.isDatabase(file)).to.equal(true) expect(fIo.isDatabase(file)).to.equal(true)
file = { type: '', name: 'test.db' } file = { type: '', name: 'test.db' }
expect(fu.isDatabase(file)).to.equal(true) expect(fIo.isDatabase(file)).to.equal(true)
file = { type: '', name: 'test.sqlite' } file = { type: '', name: 'test.sqlite' }
expect(fu.isDatabase(file)).to.equal(true) expect(fIo.isDatabase(file)).to.equal(true)
file = { type: '', name: 'test.sqlite3' } file = { type: '', name: 'test.sqlite3' }
expect(fu.isDatabase(file)).to.equal(true) expect(fIo.isDatabase(file)).to.equal(true)
file = { type: '', name: 'test.csv' } file = { type: '', name: 'test.csv' }
expect(fu.isDatabase(file)).to.equal(false) expect(fIo.isDatabase(file)).to.equal(false)
file = { type: 'text', name: 'test.db' } file = { type: 'text', name: 'test.db' }
expect(fu.isDatabase(file)).to.equal(false) 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 sinon from 'sinon'
import mutations from '@/store/mutations' import mutations from '@/store/mutations'
const { const {
saveSchema,
updateTab, updateTab,
deleteTab, deleteTab,
setCurrentTabId, setCurrentTabId,
@@ -24,25 +23,6 @@ describe('mutations', () => {
expect(oldDb.shutDown.calledOnce).to.equal(true) expect(oldDb.shutDown.calledOnce).to.equal(true)
}) })
it('saveSchema', () => {
const state = {}
const schema = [
{
name: 'table1',
columns: [
{ name: 'id', type: 'INTEGER' }
]
}
]
saveSchema(state, {
dbName: 'test',
schema
})
expect(state.dbName).to.equal('test')
expect(state.schema).to.eql(schema)
})
it('updateTab (save)', () => { it('updateTab (save)', () => {
const tab = { const tab = {
id: 1, id: 1,

View File

@@ -4,6 +4,9 @@ import { mount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex' import Vuex from 'vuex'
import Schema from '@/views/Main/Editor/Schema' import Schema from '@/views/Main/Editor/Schema'
import TableDescription from '@/views/Main/Editor/Schema/TableDescription' 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() const localVue = createLocalVue()
localVue.use(Vuex) localVue.use(Vuex)
@@ -16,7 +19,9 @@ describe('Schema.vue', () => {
it('Renders DB name on initial', () => { it('Renders DB name on initial', () => {
// mock store state // mock store state
const state = { const state = {
dbName: 'fooDB' db: {
dbName: 'fooDB'
}
} }
const store = new Vuex.Store({ state }) const store = new Vuex.Store({ state })
@@ -31,7 +36,9 @@ describe('Schema.vue', () => {
it('Schema visibility is toggled when click on DB name', async () => { it('Schema visibility is toggled when click on DB name', async () => {
// mock store state // mock store state
const state = { const state = {
dbName: 'fooDB' db: {
dbName: 'fooDB'
}
} }
const store = new Vuex.Store({ state }) const store = new Vuex.Store({ state })
@@ -48,30 +55,32 @@ describe('Schema.vue', () => {
it('Schema filter', async () => { it('Schema filter', async () => {
// mock store state // mock store state
const state = { const state = {
dbName: 'fooDB', db: {
schema: [ dbName: 'fooDB',
{ schema: [
name: 'foo', {
columns: [ name: 'foo',
{ name: 'id', type: 'INTEGER' }, columns: [
{ name: 'title', type: 'NVARCHAR(24)' } { name: 'id', type: 'INTEGER' },
] { name: 'title', type: 'NVARCHAR(24)' }
}, ]
{ },
name: 'bar', {
columns: [ name: 'bar',
{ name: 'id', type: 'INTEGER' }, columns: [
{ name: 'price', type: 'INTEGER' } { name: 'id', type: 'INTEGER' },
] { name: 'price', type: 'INTEGER' }
}, ]
{ },
name: 'foobar', {
columns: [ name: 'foobar',
{ name: 'id', type: 'INTEGER' }, columns: [
{ name: 'price', type: 'INTEGER' } { name: 'id', type: 'INTEGER' },
] { name: 'price', type: 'INTEGER' }
} ]
] }
]
}
} }
const store = new Vuex.Store({ state }) const store = new Vuex.Store({ state })
@@ -101,15 +110,69 @@ describe('Schema.vue', () => {
it('exports db', async () => { it('exports db', async () => {
const state = { const state = {
dbName: 'fooDB',
db: { db: {
dbName: 'fooDB',
export: sinon.stub().resolves() export: sinon.stub().resolves()
} }
} }
const store = new Vuex.Store({ state }) const store = new Vuex.Store({ state })
const wrapper = mount(Schema, { store, localVue }) 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')) 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) const wrapper = mount(SqlEditor)
await wrapper.findComponent({ name: 'codemirror' }).vm.$emit('input', 'SELECT * FROM foo') await wrapper.findComponent({ name: 'codemirror' }).vm.$emit('input', 'SELECT * FROM foo')
expect(wrapper.emitted('input')[0]).to.eql(['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', () => { it('Calculates table list for hint', () => {
// mock store state // mock store state
const schema = [ const db = {
{ schema: [
name: 'foo', {
columns: [ name: 'foo',
{ name: 'fooId', type: 'INTEGER' }, columns: [
{ name: 'name', type: 'NVARCHAR(20)' } { name: 'fooId', type: 'INTEGER' },
] { name: 'name', type: 'NVARCHAR(20)' }
}, ]
{ },
name: 'bar', {
columns: [ name: 'bar',
{ name: 'barId', type: 'INTEGER' } columns: [
] { name: 'barId', type: 'INTEGER' }
} ]
] }
sinon.stub(state, 'schema').value(schema) ]
}
sinon.stub(state, 'db').value(db)
// mock showHint and editor // mock showHint and editor
sinon.stub(CM, 'showHint') sinon.stub(CM, 'showHint')
@@ -52,16 +54,18 @@ describe('hint.js', () => {
it('Add default table if there is only one table in schema', () => { it('Add default table if there is only one table in schema', () => {
// mock store state // mock store state
const schema = [ const db = {
{ schema: [
name: 'foo', {
columns: [ name: 'foo',
{ name: 'fooId', type: 'INTEGER' }, columns: [
{ name: 'name', type: 'NVARCHAR(20)' } { name: 'fooId', type: 'INTEGER' },
] { name: 'name', type: 'NVARCHAR(20)' }
} ]
] }
sinon.stub(state, 'schema').value(schema) ]
}
sinon.stub(state, 'db').value(db)
// mock showHint and editor // mock showHint and editor
sinon.stub(CM, 'showHint') sinon.stub(CM, 'showHint')
@@ -190,7 +194,7 @@ describe('hint.js', () => {
it('tables is empty object when schema is null', () => { it('tables is empty object when schema is null', () => {
// mock store state // mock store state
sinon.stub(state, 'schema').value(null) sinon.stub(state, 'db').value({ schema: null })
// mock showHint and editor // mock showHint and editor
sinon.stub(CM, 'showHint') sinon.stub(CM, 'showHint')

View File

@@ -115,6 +115,7 @@ describe('Tab.vue', () => {
}) })
state.currentTabId = 1 state.currentTabId = 1
await wrapper.vm.$nextTick()
expect(mutations.setCurrentTab.calledOnceWith(state, wrapper.vm)).to.equal(true) expect(mutations.setCurrentTab.calledOnceWith(state, wrapper.vm)).to.equal(true)
}) })
@@ -181,7 +182,8 @@ describe('Tab.vue', () => {
const state = { const state = {
currentTabId: 1, currentTabId: 1,
db: { db: {
execute: sinon.stub().rejects(new Error('There is no table foo')) execute: sinon.stub().rejects(new Error('There is no table foo')),
refreshSchema: sinon.stub().resolves()
} }
} }
@@ -220,7 +222,7 @@ describe('Tab.vue', () => {
currentTabId: 1, currentTabId: 1,
db: { db: {
execute: sinon.stub().resolves(result), execute: sinon.stub().resolves(result),
getSchema: sinon.stub().resolves({ dbName: '', schema: [] }) refreshSchema: sinon.stub().resolves()
} }
} }
@@ -252,36 +254,17 @@ describe('Tab.vue', () => {
columns: ['id', 'name'], columns: ['id', 'name'],
values: [] values: []
} }
const newSchema = {
dbName: 'fooDb',
schema: [
{
name: 'foo',
columns: [
{ name: 'id', type: 'INTEGER' },
{ name: 'title', type: 'NVARCHAR(30)' }
]
},
{
name: 'bar',
columns: [
{ name: 'a', type: 'N/A' },
{ name: 'b', type: 'N/A' }
]
}
]
}
// mock store state // mock store state
const state = { const state = {
currentTabId: 1, currentTabId: 1,
dbName: 'fooDb', dbName: 'fooDb',
db: { db: {
execute: sinon.stub().resolves(result), execute: sinon.stub().resolves(result),
getSchema: sinon.stub().resolves(newSchema) refreshSchema: sinon.stub().resolves()
} }
} }
sinon.spy(mutations, 'saveSchema')
const store = new Vuex.Store({ state, mutations }) const store = new Vuex.Store({ state, mutations })
// mount the component // mount the component
@@ -299,7 +282,6 @@ describe('Tab.vue', () => {
}) })
await wrapper.vm.execute() await wrapper.vm.execute()
expect(state.db.getSchema.calledOnceWith('fooDb')).to.equal(true) expect(state.db.refreshSchema.calledOnce).to.equal(true)
expect(mutations.saveSchema.calledOnceWith(state, newSchema)).to.equal(true)
}) })
}) })

View File

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