Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9edeafd40 | ||
|
|
a37ed93306 | ||
|
|
cf4b83f7d4 | ||
|
|
2abd42c9c3 | ||
|
|
1251c542cb | ||
|
|
ac89259924 | ||
|
|
179ff8b1e1 | ||
|
|
99a10225a3 | ||
|
|
c96deb5766 | ||
|
|
700970e1cc | ||
|
|
e2be61e2cf | ||
|
|
9c2c8f3692 | ||
|
|
414a116f94 | ||
|
|
3e503f85a9 | ||
|
|
88257bfcf6 | ||
|
|
bdcc494138 | ||
|
|
d750541c80 | ||
|
|
75f743ff9e | ||
|
|
8a9f4b3c0a | ||
|
|
77468d34ae | ||
|
|
a0577ec0ce | ||
|
|
e7d1398546 | ||
|
|
aa52048d51 | ||
|
|
33913f8f5c | ||
|
|
51eb7a543c | ||
|
|
a3fb38b23c | ||
|
|
3bb40b4eb7 | ||
|
|
6864bf84f8 | ||
|
|
9f1b3823f6 | ||
|
|
7574f529c3 | ||
|
|
653f8eff7b | ||
|
|
9b3dda6cff | ||
|
|
d94604ebfb | ||
|
|
16868ef430 | ||
|
|
b162c7043e | ||
|
|
8e856063b8 | ||
|
|
8684b4cef9 | ||
|
|
bcaebd4840 | ||
|
|
4619461af8 | ||
|
|
9fff1d699a | ||
|
|
5ab19c3fae | ||
|
|
cc483f4720 | ||
|
|
a07f2d3d99 | ||
|
|
b9844b8696 | ||
|
|
464bff3db8 | ||
|
|
00e434e142 | ||
|
|
5d6280abec | ||
|
|
7a39e905b9 | ||
|
|
297ea2c18a | ||
|
|
1f2327a724 | ||
|
|
6d512422cf | ||
|
|
8ce9a01372 | ||
|
|
acd56a85cb | ||
|
|
a45e218e3f | ||
|
|
a2bc495259 | ||
|
|
7f4b167dc2 | ||
|
|
5ded99e89f | ||
|
|
a991b02a20 | ||
|
|
9b6aa3d6c7 | ||
|
|
92022f9083 | ||
|
|
15636fed5f | ||
|
|
9ed53e0d25 | ||
|
|
35baaf2722 | ||
|
|
453098b410 | ||
|
|
a469de3674 | ||
|
|
24411ac18f | ||
|
|
a7ef152140 | ||
|
|
97c0c6191b |
17
.github/workflows/config.grenrc.js
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
module.exports = {
|
||||||
|
dataSource: 'milestones',
|
||||||
|
ignoreIssuesWith: [
|
||||||
|
'wontfix',
|
||||||
|
'duplicate'
|
||||||
|
],
|
||||||
|
milestoneMatch: 'v{{tag_name}}',
|
||||||
|
template: {
|
||||||
|
issue: '- {{name}} [{{text}}]({{url}})',
|
||||||
|
changelogTitle: "",
|
||||||
|
release: "{{body}}",
|
||||||
|
},
|
||||||
|
groupBy: {
|
||||||
|
'Enhancements': ["enhancement", "internal"],
|
||||||
|
'Bug fixes': ["bug"]
|
||||||
|
}
|
||||||
|
}
|
||||||
20
.github/workflows/main.yml
vendored
@@ -25,16 +25,26 @@ jobs:
|
|||||||
cd dist
|
cd dist
|
||||||
zip -9 -r dist.zip . -x "js/*.map"
|
zip -9 -r dist.zip . -x "js/*.map"
|
||||||
|
|
||||||
|
- name: Create Release Notes
|
||||||
|
run: |
|
||||||
|
npm install github-release-notes@0.16.0 -g
|
||||||
|
gren changelog --generate --config="/.github/workflows/config.grenrc.js"
|
||||||
|
env:
|
||||||
|
GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: ncipollo/release-action@v1
|
uses: ncipollo/release-action@v1
|
||||||
with:
|
with:
|
||||||
artifacts: "dist/dist.zip"
|
artifacts: "dist/dist.zip"
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
bodyFile: "CHANGELOG.md"
|
||||||
|
|
||||||
- name: Deploy 🚀
|
- name: Deploy 🚀
|
||||||
uses: JamesIves/github-pages-deploy-action@3.6.2
|
uses: JamesIves/github-pages-deploy-action@4.1.1
|
||||||
with:
|
with:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
BRANCH: build # The branch the action should deploy to.
|
branch: build # The branch the action should deploy to.
|
||||||
FOLDER: dist/ # The folder the action should deploy.
|
folder: dist/ # The folder the action should deploy.
|
||||||
CLEAN: false # Automatically remove deleted files from the deploy branch
|
clean: true # Automatically remove deleted files from the deploy branch
|
||||||
|
clean-exclude: .nojekyll
|
||||||
|
|
||||||
|
|||||||
12
.github/workflows/test.yml
vendored
@@ -4,6 +4,9 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'master'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
@@ -15,10 +18,11 @@ jobs:
|
|||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 10.x
|
node-version: 10.x
|
||||||
- name: Install chromium
|
- name: Install browsers
|
||||||
run:
|
run: |
|
||||||
sudo DEBIAN_FRONTEND=noninteractive apt-get update &&
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y chromium-browser
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y chromium-browser firefox
|
||||||
|
|
||||||
- name: Install the project
|
- name: Install the project
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|||||||
163
README.md
@@ -4,153 +4,23 @@
|
|||||||
|
|
||||||
# sqliteviz
|
# sqliteviz
|
||||||
|
|
||||||
Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases or CSV.
|
Sqliteviz is a single-page offline-first PWA for fully client-side visualisation of SQLite databases or CSV files.
|
||||||
|
|
||||||
## Get started
|
With sqliteviz you can:
|
||||||
|
- run SQL queries against a SQLite database and create [Plotly][11] charts based on the result sets
|
||||||
|
- import a CSV file into a SQLite database and visualize imported data
|
||||||
|
- manage queries and chart settings and run them against different databases
|
||||||
|
- import/export queries and chart settings to/from a JSON file
|
||||||
|
- export a modified SQLite database
|
||||||
|
- use it offline from your OS application menu like any other desktop app
|
||||||
|
|
||||||
The latest release of sqliteviz is running on [Github pages][6]. The simplest way to start is to use sqliteviz there.
|
https://user-images.githubusercontent.com/24638357/117355518-fa332680-aeb2-11eb-8a69-fbcea4f7aeb0.mp4
|
||||||
|
|
||||||
### Choose a databese or CSV file
|
## Quickstart
|
||||||
|
The latest release of sqliteviz is deployed on GitHub Pages at [lana-k.github.io/sqliteviz][6].
|
||||||
|
|
||||||
You can choose a database or CSV file right on the welcom page (fig. 1). The supported file extentions: `.csv`, `.db`,`.sqlite` and `.sqlite3`.
|
## Wiki
|
||||||
|
For user documentation, check out sqliteviz [Wiki][7].
|
||||||
<p align="center">
|
|
||||||
<img class="figure" src="src/assets/images/Screenshot_welcome.png" width="400"/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<sub>
|
|
||||||
Fig. 1: Welcome page
|
|
||||||
</sub>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
If you choose a CSV file it will be parsed. Then sqliteviz creates a new database with data from the CSV in `csv_import` table. You can change parsing settings in the dialog which is shown automatically if you choose a CSV file (fig. 2).
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img class="figure" src="src/assets/images/Screenshot_csv.png" width="650"/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<sub>
|
|
||||||
Fig. 2: Import CSV dialog
|
|
||||||
</sub>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
Choosing a database or CSV file is not a mandatory step. You can skip it and manipulate queries in sqliteviz without a database. Choose a database or CSV later in the left panel of the `Editor` when it's time to run a query (fig. 3).
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img class="figure" src="src/assets/images/Screenshot_editor.png" width="650"/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<sub>
|
|
||||||
Fig. 3: Editor (neither database nor CSV is chosen)
|
|
||||||
</sub>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
After chosing a database or CSV you can browse tables, columns and their types in the left panel of the `Editor` (fig. 4).
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img class="figure" src="src/assets/images/Screenshot_editor_with_db.png" width="650"/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<sub>
|
|
||||||
Fig. 4: Editor (database is chosen)
|
|
||||||
</sub>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
### Create a query
|
|
||||||
|
|
||||||
#### Open a new tab
|
|
||||||
|
|
||||||
Press `Create` button in the top toolbar or use `Ctrl+b`(`Cmd+b` for MacOS) keyboard shortcut to open a new tab for a query. The tab consists of two parts: a query text editor on the top and a result panel on the bottom.
|
|
||||||
|
|
||||||
In the query text editor part you can specify a `SELECT` statement for getting data.
|
|
||||||
|
|
||||||
The result panel has two modes: table view (fig. 4, fig. 5) and chart view (fig. 3). In the table view you can see the result of query running (fig. 5). In the chart view there is a chart editor component which allows to build a visialization from the result set.
|
|
||||||
|
|
||||||
#### Run a query
|
|
||||||
|
|
||||||
Press `Run` button in the top toolbar or use `Ctrl+r` or `Ctrl+Enter`(`Cmd+r` or `Cmd+Enter` for MacOS) keyboard shortcut to execute a query in the current opened tab.
|
|
||||||
|
|
||||||
> **Note:** Running is not available if neither a database nor CSV was chosen or a query for the current tab is not specified.
|
|
||||||
|
|
||||||
The query result will be displayed in the result panel in table mode (fig. 5).
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img class="figure" src="src/assets/images/Screenshot_result.png" width="650"/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<sub>
|
|
||||||
Fig. 5: Query results
|
|
||||||
</sub>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
#### Create a chart
|
|
||||||
|
|
||||||
After running a query you can switch result panel to the chart mode and create a chart with a `react-chart-editor` component. The same component with some additional features is used in Plotly Chart Studio. Explore its [documentation][7] to learn how to build charts with `react-chart-editor`.
|
|
||||||
|
|
||||||
### Save a query
|
|
||||||
|
|
||||||
Press `Save` button in the top toolbar or use `Ctrl+s`(`Cmd+s` for MacOS) keyboard shortcut to save a query in the current opened tab to local storage of your browser.
|
|
||||||
|
|
||||||
After that the query will be in the list on `My queries` page.
|
|
||||||
|
|
||||||
> **Note:** Only the text of the query and chart settings will be saved. The result of the query execution won't be saved.
|
|
||||||
|
|
||||||
## Working with saved queries
|
|
||||||
|
|
||||||
You can find all queries that you saved in `My queries` (fig. 6).
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img class="figure" src="src/assets/images/Screenshot_my_queries.png" width="600"/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<sub>
|
|
||||||
Fig. 6: My queries
|
|
||||||
</sub>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
To manipulate one query hover the cursor over the row with the query and choose the action:
|
|
||||||
|
|
||||||
* <img src="src/assets/images/rename.svg"/> - rename a query
|
|
||||||
* <img src="src/assets/images/copy.svg"/> - duplicate a query
|
|
||||||
* <img src="src/assets/images/file-export.svg"/> - export a query to json file
|
|
||||||
* <img src="src/assets/images/delete.svg"/> - delete a query
|
|
||||||
|
|
||||||
To edit the text of a query or its chart settings click on the respective row. You will be redirected to `Editor` where the chosen query will be opened in a tab.
|
|
||||||
|
|
||||||
> **Note:** After opening a query there will be no chart for it even if you specified it and saved. That is so because there is no data to build the chart. Run the query and all saved chart settings will be applied.
|
|
||||||
|
|
||||||
You can also delete or export to file a group of queries. Select queries with checkboxes and press `Delete`/`Export` button above the grid (fig. 7).
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img class="figure" src="src/assets/images/Screenshot_group.png" width="600"/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<sub>
|
|
||||||
Fig. 7: My queries: a group of queries is selected
|
|
||||||
</sub>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
> **Note:** Some operations are not available for predefined queries (see below).
|
|
||||||
|
|
||||||
## Import queries
|
|
||||||
|
|
||||||
Click `Import` button on `My queries` page to import queries from JSON file generated by export.
|
|
||||||
|
|
||||||
## Predefined queries
|
|
||||||
|
|
||||||
If you run sqliteviz on your own server you can specify predefined queries. These queries will be in `My queries` list for all users working with sqliteviz on your server.
|
|
||||||
|
|
||||||
To create a list of predefined queries choose queries in `My queries` list and export them to `queries.json`. Then place this file on the server in the same directory as `index.html`.
|
|
||||||
|
|
||||||
A user can't edit, rename or delete a predefined query. The rest operations are available.
|
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
It's a kind of middleground between [Plotly Falcon][1] and [Redash][2].
|
It's a kind of middleground between [Plotly Falcon][1] and [Redash][2].
|
||||||
@@ -163,8 +33,9 @@ It is built on top of [react-chart-editor][3], [sql.js][4] and [Vue-Codemirror][
|
|||||||
[3]: https://github.com/plotly/react-chart-editor
|
[3]: https://github.com/plotly/react-chart-editor
|
||||||
[4]: https://github.com/sql-js/sql.js
|
[4]: https://github.com/sql-js/sql.js
|
||||||
[5]: https://github.com/vuejs/vue
|
[5]: https://github.com/vuejs/vue
|
||||||
[6]: https://lana-k.github.io/sqliteviz
|
[6]: https://lana-k.github.io/sqliteviz/
|
||||||
[7]: https://plotly.com/chart-studio-help/tutorials/#basic
|
[7]: https://github.com/lana-k/sqliteviz/wiki
|
||||||
[8]: https://github.com/surmon-china/vue-codemirror#readme
|
[8]: https://github.com/surmon-china/vue-codemirror#readme
|
||||||
[9]: https://www.papaparse.com/
|
[9]: https://www.papaparse.com/
|
||||||
|
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries
|
||||||
|
[11]: https://github.com/plotly/plotly.js
|
||||||
|
|||||||
@@ -76,9 +76,17 @@ module.exports = function (config) {
|
|||||||
// enable / disable watching file and executing tests whenever any file changes
|
// enable / disable watching file and executing tests whenever any file changes
|
||||||
autoWatch: false,
|
autoWatch: false,
|
||||||
|
|
||||||
|
customLaunchers: {
|
||||||
|
FirefoxHeadlessTouch: {
|
||||||
|
base: 'FirefoxHeadless',
|
||||||
|
prefs: {
|
||||||
|
'dom.w3c_touch_events.enabled': 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
// start these browsers
|
// start these browsers
|
||||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||||
browsers: ['ChromiumHeadless'],
|
browsers: ['ChromiumHeadless', 'FirefoxHeadlessTouch'],
|
||||||
|
|
||||||
// Continuous Integration mode
|
// Continuous Integration mode
|
||||||
// if true, Karma captures browsers, runs the tests and exits
|
// if true, Karma captures browsers, runs the tests and exits
|
||||||
@@ -86,10 +94,13 @@ module.exports = function (config) {
|
|||||||
|
|
||||||
// Concurrency level
|
// Concurrency level
|
||||||
// how many browser should be started simultaneous
|
// how many browser should be started simultaneous
|
||||||
concurrency: Infinity,
|
concurrency: 2,
|
||||||
|
|
||||||
client: {
|
client: {
|
||||||
captureConsole: true
|
captureConsole: true,
|
||||||
|
mocha: {
|
||||||
|
timeout: 7000
|
||||||
|
}
|
||||||
},
|
},
|
||||||
browserConsoleLogOptions: {
|
browserConsoleLogOptions: {
|
||||||
terminal: true,
|
terminal: true,
|
||||||
@@ -130,7 +141,7 @@ module.exports = function (config) {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.worker\.js$/,
|
test: /worker\.js$/,
|
||||||
loader: 'worker-loader'
|
loader: 'worker-loader'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
4765
package-lock.json
generated
13
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sqliteviz",
|
"name": "sqliteviz",
|
||||||
"version": "1.0.0",
|
"version": "0.13.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -12,15 +12,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"codemirror": "^5.57.0",
|
"codemirror": "^5.57.0",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"debounce": "^1.2.0",
|
|
||||||
"nanoid": "^3.1.12",
|
"nanoid": "^3.1.12",
|
||||||
"papaparse": "^5.3.0",
|
"papaparse": "^5.3.1",
|
||||||
"plotly.js": "^1.57.1",
|
"plotly.js": "^1.58.4",
|
||||||
"promise-worker": "^2.0.1",
|
"promise-worker": "^2.0.1",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-chart-editor": "^0.42.0",
|
"react-chart-editor": "^0.45.0",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"sql.js": "^1.3.0",
|
"sql.js": "^1.5.0",
|
||||||
"sqlite-parser": "^1.0.1",
|
"sqlite-parser": "^1.0.1",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
"vue-codemirror": "^4.0.6",
|
"vue-codemirror": "^4.0.6",
|
||||||
@@ -48,9 +47,11 @@
|
|||||||
"eslint-plugin-standard": "^4.0.0",
|
"eslint-plugin-standard": "^4.0.0",
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
"karma": "^3.1.4",
|
"karma": "^3.1.4",
|
||||||
|
"karma-firefox-launcher": "^2.1.0",
|
||||||
"karma-webpack": "^4.0.2",
|
"karma-webpack": "^4.0.2",
|
||||||
"vue-cli-plugin-ui-karma": "^0.2.5",
|
"vue-cli-plugin-ui-karma": "^0.2.5",
|
||||||
"vue-template-compiler": "^2.6.11",
|
"vue-template-compiler": "^2.6.11",
|
||||||
|
"workbox-webpack-plugin": "^6.1.5",
|
||||||
"worker-loader": "^3.0.8"
|
"worker-loader": "^3.0.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/Logo192x192.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/Logo48x48.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/Logo512x512.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 798 B After Width: | Height: | Size: 774 B |
@@ -5,13 +5,94 @@
|
|||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.png">
|
<link rel="icon" href="<%= BASE_URL %>favicon.png">
|
||||||
|
<link rel="manifest" href="<%= BASE_URL %>manifest.webmanifest">
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
|
<style>
|
||||||
|
#sqliteviz-loading-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sqliteviz-loading-text {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: #506784;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sqliteviz-loading-wrapper svg {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sqliteviz-loading-wrapper circle {
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0; top: 0; bottom: 0;
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 5px;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke: #119DFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sqliteviz-loading-wrapper circle.bg {
|
||||||
|
stroke: #C8D4E3;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sqliteviz-loading-wrapper circle.front {
|
||||||
|
stroke-dasharray: 402px;
|
||||||
|
animation: sqliteviz-loading 2s linear 0s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sqliteviz-loading {
|
||||||
|
0% {
|
||||||
|
stroke-dasharray: 100px 402px;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
stroke-dasharray: 251px;
|
||||||
|
stroke-dashoffset: -251px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dasharray: 100px 402px;
|
||||||
|
stroke-dashoffset: -502px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
</noscript>
|
</noscript>
|
||||||
<div id="app"></div>
|
<div id="app">
|
||||||
|
<div id="sqliteviz-loading-wrapper">
|
||||||
|
<div id="sqliteviz-loading-text">LOADING</div>
|
||||||
|
<svg height="170" width="170" viewBox="0 0 170 170">
|
||||||
|
<circle
|
||||||
|
class="bg"
|
||||||
|
cx="85"
|
||||||
|
cy="85"
|
||||||
|
r="80"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
class="front"
|
||||||
|
cx="85"
|
||||||
|
cy="85"
|
||||||
|
r="80"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
30
public/manifest.webmanifest
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"background_color": "white",
|
||||||
|
"description": "Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases or CSV.",
|
||||||
|
"display": "fullscreen",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.png",
|
||||||
|
"sizes": "32x32",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "Logo48x48.png",
|
||||||
|
"sizes": "48x48",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "Logo192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "Logo512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "sqliteviz",
|
||||||
|
"short_name": "sqliteviz",
|
||||||
|
"start_url": "index.html"
|
||||||
|
}
|
||||||
@@ -60,4 +60,7 @@ button,
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
.CodeMirror-hints {
|
||||||
|
z-index: 999 !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 42 KiB |
@@ -1,5 +1,5 @@
|
|||||||
.rounded-bg {
|
.rounded-bg {
|
||||||
padding: 40px 5px 5px;
|
padding: 35px 5px 5px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
}
|
}
|
||||||
table {
|
table {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
margin-top: -40px;
|
margin-top: -35px;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
thead th, .fixed-header {
|
thead th, .fixed-header {
|
||||||
@@ -56,7 +56,7 @@ tbody td {
|
|||||||
border-right: 1px solid var(--color-border-light);
|
border-right: 1px solid var(--color-border-light);
|
||||||
}
|
}
|
||||||
td, th, .fixed-header {
|
td, th, .fixed-header {
|
||||||
padding: 12px 24px;
|
padding: 8px 24px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
line-height: 19px;;
|
line-height: 19px;;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 5;
|
|
||||||
height: 19px;
|
height: 19px;
|
||||||
border-radius: var(--border-radius-medium);
|
border-radius: var(--border-radius-medium);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ascii from '@/ascii'
|
import ascii from './ascii'
|
||||||
import DropDownChevron from '@/components/svg/dropDownChevron'
|
import DropDownChevron from '@/components/svg/dropDownChevron'
|
||||||
import ClearIcon from '@/components/svg/clear'
|
import ClearIcon from '@/components/svg/clear'
|
||||||
|
|
||||||
@@ -10,10 +10,17 @@ export default {
|
|||||||
getResult (source) {
|
getResult (source) {
|
||||||
const result = {}
|
const result = {}
|
||||||
if (source.meta.fields) {
|
if (source.meta.fields) {
|
||||||
result.columns = source.meta.fields
|
result.columns = source.meta.fields.map(col => col.trim())
|
||||||
result.values = source.data.map(row => {
|
result.values = source.data.map(row => {
|
||||||
const resultRow = []
|
const resultRow = []
|
||||||
result.columns.forEach(col => { resultRow.push(row[col]) })
|
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 {
|
||||||
381
src/components/CsvImport/index.vue
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
<template>
|
||||||
|
<modal
|
||||||
|
:name="dialogName"
|
||||||
|
classes="dialog"
|
||||||
|
height="auto"
|
||||||
|
width="80%"
|
||||||
|
scrollable
|
||||||
|
:clickToClose="false"
|
||||||
|
>
|
||||||
|
<div class="dialog-header">
|
||||||
|
CSV import
|
||||||
|
<close-icon @click="cancelCsvImport" :disabled="disableDialog"/>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-body">
|
||||||
|
<text-field
|
||||||
|
label="Table name"
|
||||||
|
v-model="tableName"
|
||||||
|
width="484px"
|
||||||
|
:disabled="disableDialog"
|
||||||
|
:error-msg="tableNameError"
|
||||||
|
id="csv-table-name"
|
||||||
|
/>
|
||||||
|
<div class="chars">
|
||||||
|
<delimiter-selector
|
||||||
|
v-model="delimiter"
|
||||||
|
width="210px"
|
||||||
|
class="char-input"
|
||||||
|
@input="previewCsv"
|
||||||
|
:disabled="disableDialog"
|
||||||
|
/>
|
||||||
|
<text-field
|
||||||
|
label="Quote char"
|
||||||
|
hint="The character used to quote fields."
|
||||||
|
v-model="quoteChar"
|
||||||
|
width="93px"
|
||||||
|
:disabled="disableDialog"
|
||||||
|
class="char-input"
|
||||||
|
id="quote-char"
|
||||||
|
/>
|
||||||
|
<text-field
|
||||||
|
label="Escape char"
|
||||||
|
hint='The character used to escape the quote character within a field (e.g. "column with ""quotes"" in text").'
|
||||||
|
max-hint-width="242px"
|
||||||
|
v-model="escapeChar"
|
||||||
|
width="93px"
|
||||||
|
:disabled="disableDialog"
|
||||||
|
class="char-input"
|
||||||
|
id="escape-char"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<check-box
|
||||||
|
@click="header = $event"
|
||||||
|
:init="true"
|
||||||
|
label="Use first row as column headers"
|
||||||
|
:disabled="disableDialog"
|
||||||
|
/>
|
||||||
|
<sql-table
|
||||||
|
v-if="previewData && (previewData.values.length > 0 || previewData.columns.length > 0)"
|
||||||
|
:data-set="previewData"
|
||||||
|
height="160"
|
||||||
|
class="preview-table"
|
||||||
|
:preview="true"
|
||||||
|
/>
|
||||||
|
<div v-else class="no-data">No data</div>
|
||||||
|
<logs
|
||||||
|
class="import-csv-errors"
|
||||||
|
:messages="importCsvMessages"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-buttons-container">
|
||||||
|
<button
|
||||||
|
class="secondary"
|
||||||
|
:disabled="disableDialog"
|
||||||
|
@click="cancelCsvImport"
|
||||||
|
id="csv-cancel"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-show="!importCsvCompleted"
|
||||||
|
class="primary"
|
||||||
|
:disabled="disableDialog"
|
||||||
|
@click="loadFromCsv(file)"
|
||||||
|
id="csv-import"
|
||||||
|
>
|
||||||
|
Import
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-show="importCsvCompleted"
|
||||||
|
class="primary"
|
||||||
|
:disabled="disableDialog"
|
||||||
|
@click="finish"
|
||||||
|
id="csv-finish"
|
||||||
|
>
|
||||||
|
Finish
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import csv from './csv'
|
||||||
|
import CloseIcon from '@/components/svg/close'
|
||||||
|
import TextField from '@/components/TextField'
|
||||||
|
import DelimiterSelector from './DelimiterSelector'
|
||||||
|
import CheckBox from '@/components/CheckBox'
|
||||||
|
import SqlTable from '@/components/SqlTable'
|
||||||
|
import Logs from '@/components/Logs'
|
||||||
|
import time from '@/lib/utils/time'
|
||||||
|
import fIo from '@/lib/utils/fileIo'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CsvImport',
|
||||||
|
components: {
|
||||||
|
CloseIcon,
|
||||||
|
TextField,
|
||||||
|
DelimiterSelector,
|
||||||
|
CheckBox,
|
||||||
|
SqlTable,
|
||||||
|
Logs
|
||||||
|
},
|
||||||
|
props: ['file', 'db', 'dialogName'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
disableDialog: false,
|
||||||
|
tableName: '',
|
||||||
|
delimiter: '',
|
||||||
|
quoteChar: '"',
|
||||||
|
escapeChar: '"',
|
||||||
|
header: true,
|
||||||
|
importCsvCompleted: false,
|
||||||
|
importCsvMessages: [],
|
||||||
|
previewData: null,
|
||||||
|
addedTable: null,
|
||||||
|
tableNameError: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
quoteChar () {
|
||||||
|
this.previewCsv()
|
||||||
|
},
|
||||||
|
|
||||||
|
escapeChar () {
|
||||||
|
this.previewCsv()
|
||||||
|
},
|
||||||
|
|
||||||
|
header () {
|
||||||
|
this.previewCsv()
|
||||||
|
},
|
||||||
|
tableName: time.debounce(function () {
|
||||||
|
this.tableNameError = ''
|
||||||
|
if (!this.tableName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.db.validateTableName(this.tableName)
|
||||||
|
.catch(err => {
|
||||||
|
this.tableNameError = err.message + '. Try another table name.'
|
||||||
|
})
|
||||||
|
}, 400)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
cancelCsvImport () {
|
||||||
|
if (!this.disableDialog) {
|
||||||
|
if (this.addedTable) {
|
||||||
|
this.db.execute(`DROP TABLE "${this.addedTable}"`)
|
||||||
|
this.db.refreshSchema()
|
||||||
|
}
|
||||||
|
this.$modal.hide(this.dialogName)
|
||||||
|
this.$emit('cancel')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset () {
|
||||||
|
this.header = true
|
||||||
|
this.quoteChar = '"'
|
||||||
|
this.escapeChar = '"'
|
||||||
|
this.delimiter = ''
|
||||||
|
this.tableName = ''
|
||||||
|
this.disableDialog = false
|
||||||
|
this.importCsvCompleted = false
|
||||||
|
this.importCsvMessages = []
|
||||||
|
this.previewData = null
|
||||||
|
this.addedTable = null
|
||||||
|
this.tableNameError = ''
|
||||||
|
},
|
||||||
|
open () {
|
||||||
|
this.tableName = this.db.sanitizeTableName(fIo.getFileName(this.file))
|
||||||
|
this.$modal.show(this.dialogName)
|
||||||
|
},
|
||||||
|
async previewCsv () {
|
||||||
|
this.importCsvCompleted = false
|
||||||
|
const config = {
|
||||||
|
preview: 3,
|
||||||
|
quoteChar: this.quoteChar || '"',
|
||||||
|
escapeChar: this.escapeChar,
|
||||||
|
header: this.header,
|
||||||
|
delimiter: this.delimiter
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const start = new Date()
|
||||||
|
const parseResult = await csv.parse(this.file, config)
|
||||||
|
const end = new Date()
|
||||||
|
this.previewData = parseResult.data
|
||||||
|
this.delimiter = parseResult.delimiter
|
||||||
|
|
||||||
|
// In parseResult.messages we can get parse errors
|
||||||
|
this.importCsvMessages = parseResult.messages || []
|
||||||
|
|
||||||
|
if (!parseResult.hasErrors) {
|
||||||
|
this.importCsvMessages.push({
|
||||||
|
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.importCsvMessages = [{
|
||||||
|
message: err,
|
||||||
|
type: 'error'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadFromCsv (file) {
|
||||||
|
if (!this.tableName) {
|
||||||
|
this.tableNameError = "Table name can't be empty"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.disableDialog = true
|
||||||
|
const config = {
|
||||||
|
quoteChar: this.quoteChar || '"',
|
||||||
|
escapeChar: this.escapeChar,
|
||||||
|
header: this.header,
|
||||||
|
delimiter: this.delimiter
|
||||||
|
}
|
||||||
|
const parseCsvMsg = {
|
||||||
|
message: 'Parsing CSV...',
|
||||||
|
type: 'info'
|
||||||
|
}
|
||||||
|
this.importCsvMessages.push(parseCsvMsg)
|
||||||
|
const parseCsvLoadingIndicator = setTimeout(() => { parseCsvMsg.type = 'loading' }, 1000)
|
||||||
|
|
||||||
|
const importMsg = {
|
||||||
|
message: 'Importing CSV into a SQLite database...',
|
||||||
|
type: 'info'
|
||||||
|
}
|
||||||
|
let importLoadingIndicator = null
|
||||||
|
|
||||||
|
const updateProgress = progress => {
|
||||||
|
this.$set(importMsg, 'progress', progress)
|
||||||
|
}
|
||||||
|
const progressCounterId = this.db.createProgressCounter(updateProgress)
|
||||||
|
|
||||||
|
try {
|
||||||
|
let start = new Date()
|
||||||
|
const parseResult = await csv.parse(this.file, config)
|
||||||
|
let end = new Date()
|
||||||
|
|
||||||
|
if (!parseResult.hasErrors) {
|
||||||
|
const rowCount = parseResult.data.values.length
|
||||||
|
let period = time.getPeriod(start, end)
|
||||||
|
parseCsvMsg.type = 'success'
|
||||||
|
|
||||||
|
if (parseResult.messages.length > 0) {
|
||||||
|
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
||||||
|
parseCsvMsg.message = `${rowCount} rows are parsed in ${period}.`
|
||||||
|
} else {
|
||||||
|
// Inform about csv parsing success
|
||||||
|
parseCsvMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading indicator for csv parsing is not needed anymore
|
||||||
|
clearTimeout(parseCsvLoadingIndicator)
|
||||||
|
|
||||||
|
// Add info about import start
|
||||||
|
this.importCsvMessages.push(importMsg)
|
||||||
|
|
||||||
|
// Show import progress after 1 second
|
||||||
|
importLoadingIndicator = setTimeout(() => {
|
||||||
|
importMsg.type = 'loading'
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
// Add table
|
||||||
|
start = new Date()
|
||||||
|
await this.db.addTableFromCsv(this.tableName, parseResult.data, progressCounterId)
|
||||||
|
end = new Date()
|
||||||
|
|
||||||
|
this.addedTable = this.tableName
|
||||||
|
// Inform about import success
|
||||||
|
period = time.getPeriod(start, end)
|
||||||
|
importMsg.message = `Importing CSV into a SQLite database is completed in ${period}.`
|
||||||
|
importMsg.type = 'success'
|
||||||
|
|
||||||
|
// Loading indicator for import is not needed anymore
|
||||||
|
clearTimeout(importLoadingIndicator)
|
||||||
|
|
||||||
|
this.importCsvCompleted = true
|
||||||
|
} else {
|
||||||
|
parseCsvMsg.message = 'Parsing ended with errors.'
|
||||||
|
parseCsvMsg.type = 'info'
|
||||||
|
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (parseCsvMsg.type === 'loading') {
|
||||||
|
parseCsvMsg.type = 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importMsg.type === 'loading') {
|
||||||
|
importMsg.type = 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.importCsvMessages.push({
|
||||||
|
message: err,
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(parseCsvLoadingIndicator)
|
||||||
|
clearTimeout(importLoadingIndicator)
|
||||||
|
this.db.deleteProgressCounter(progressCounterId)
|
||||||
|
this.disableDialog = false
|
||||||
|
},
|
||||||
|
async finish () {
|
||||||
|
this.$modal.hide(this.dialogName)
|
||||||
|
const stmt = [
|
||||||
|
'/*',
|
||||||
|
` * Your CSV file has been imported into ${this.addedTable} table.`,
|
||||||
|
' * You can run this SQL query to make all CSV records available for charting.',
|
||||||
|
' */',
|
||||||
|
`SELECT * FROM "${this.addedTable}"`
|
||||||
|
].join('\n')
|
||||||
|
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
||||||
|
this.$store.commit('setCurrentTabId', tabId)
|
||||||
|
this.importCsvCompleted = false
|
||||||
|
this.$emit('finish')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dialog-body {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chars {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin: 24px 0 20px;
|
||||||
|
}
|
||||||
|
.char-input {
|
||||||
|
margin-right: 44px;
|
||||||
|
}
|
||||||
|
.preview-table {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-csv-errors {
|
||||||
|
height: 136px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.no-data {
|
||||||
|
margin-top: 32px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid var(--color-border-light);
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 147px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-base);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* https://github.com/euvl/vue-js-modal/issues/623 */
|
||||||
|
>>> .vm--modal {
|
||||||
|
max-width: 1152px;
|
||||||
|
margin: auto;
|
||||||
|
left: 0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="db-uploader-container">
|
<div class="db-uploader-container" :style="{ width }">
|
||||||
<change-db-icon v-if="type === 'small'" @click.native="browse"/>
|
<change-db-icon v-if="type === 'small'" @click.native="browse"/>
|
||||||
<div v-if="['regular', 'illustrated'].includes(type)" class="drop-area-container">
|
<div v-if="type === 'illustrated'" class="drop-area-container">
|
||||||
<div
|
<div
|
||||||
class="drop-area"
|
class="drop-area"
|
||||||
@dragover.prevent="state = 'dragover'"
|
@dragover.prevent="state = 'dragover'"
|
||||||
@@ -26,7 +26,8 @@
|
|||||||
ref="fileImg"
|
ref="fileImg"
|
||||||
:class="{
|
:class="{
|
||||||
'swing': state === 'dragover',
|
'swing': state === 'dragover',
|
||||||
'fly': state === 'drop'
|
'fly': state === 'dropping',
|
||||||
|
'hidden': state === 'dropped'
|
||||||
}"
|
}"
|
||||||
:src="require('@/assets/images/file.png')"
|
:src="require('@/assets/images/file.png')"
|
||||||
/>
|
/>
|
||||||
@@ -41,103 +42,22 @@
|
|||||||
<div id="error" class="error"></div>
|
<div id="error" class="error"></div>
|
||||||
|
|
||||||
<!--Parse csv dialog -->
|
<!--Parse csv dialog -->
|
||||||
<modal name="parse" classes="dialog" height="auto" width="60%" :clickToClose="false">
|
<csv-import
|
||||||
<div class="dialog-header">
|
ref="addCsv"
|
||||||
Import CSV
|
:file="file"
|
||||||
<close-icon @click="cancelCsvImport" :disabled="disableDialog"/>
|
:db="newDb"
|
||||||
</div>
|
dialog-name="importFromCsv"
|
||||||
<div class="dialog-body">
|
@cancel="cancelCsvImport"
|
||||||
<div class="chars">
|
@finish="finish"
|
||||||
<delimiter-selector
|
/>
|
||||||
v-model="delimiter"
|
|
||||||
width="210px"
|
|
||||||
class="char-input"
|
|
||||||
@input="previewCSV"
|
|
||||||
:disabled="disableDialog"
|
|
||||||
/>
|
|
||||||
<text-field
|
|
||||||
label="Quote char"
|
|
||||||
hint="The character used to quote fields."
|
|
||||||
v-model="quoteChar"
|
|
||||||
width="93px"
|
|
||||||
:disabled="disableDialog"
|
|
||||||
class="char-input"
|
|
||||||
id="quote-char"
|
|
||||||
/>
|
|
||||||
<text-field
|
|
||||||
label="Escape char"
|
|
||||||
hint='The character used to escape the quote character within a field (e.g. "column with ""quotes"" in text").'
|
|
||||||
max-hint-width="242px"
|
|
||||||
v-model="escapeChar"
|
|
||||||
width="93px"
|
|
||||||
:disabled="disableDialog"
|
|
||||||
class="char-input"
|
|
||||||
id="escape-char"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<check-box
|
|
||||||
@click="header = $event"
|
|
||||||
:init="true"
|
|
||||||
label="Use first row as column headers"
|
|
||||||
:disabled="disableDialog"
|
|
||||||
/>
|
|
||||||
<sql-table
|
|
||||||
v-if="previewData"
|
|
||||||
:data-set="previewData"
|
|
||||||
height="160"
|
|
||||||
class="preview-table"
|
|
||||||
:preview="true"
|
|
||||||
/>
|
|
||||||
<div v-if="!previewData" class="no-data">No data</div>
|
|
||||||
<logs
|
|
||||||
class="import-csv-errors"
|
|
||||||
:messages="importCsvMessages"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="dialog-buttons-container">
|
|
||||||
<button
|
|
||||||
class="secondary"
|
|
||||||
:disabled="disableDialog"
|
|
||||||
@click="cancelCsvImport"
|
|
||||||
id="csv-cancel"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-show="!importCsvCompleted"
|
|
||||||
class="primary"
|
|
||||||
:disabled="disableDialog"
|
|
||||||
@click="loadFromCsv(file)"
|
|
||||||
id="csv-import"
|
|
||||||
>
|
|
||||||
Import
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-show="importCsvCompleted"
|
|
||||||
class="primary"
|
|
||||||
:disabled="disableDialog"
|
|
||||||
@click="finish"
|
|
||||||
id="csv-finish"
|
|
||||||
>
|
|
||||||
Finish
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</modal>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import fu from '@/file.utils'
|
import fIo from '@/lib/utils/fileIo'
|
||||||
import csv from '@/csv'
|
|
||||||
import CloseIcon from '@/components/svg/close'
|
|
||||||
import TextField from '@/components/TextField'
|
|
||||||
import DelimiterSelector from '@/components/DelimiterSelector'
|
|
||||||
import CheckBox from '@/components/CheckBox'
|
|
||||||
import SqlTable from '@/components/SqlTable'
|
|
||||||
import Logs from '@/components/Logs'
|
|
||||||
import ChangeDbIcon from '@/components/svg/changeDb'
|
import ChangeDbIcon from '@/components/svg/changeDb'
|
||||||
import time from '@/time'
|
import database from '@/lib/database'
|
||||||
import database from '@/database'
|
import CsvImport from '@/components/CsvImport'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DbUploader',
|
name: 'DbUploader',
|
||||||
@@ -145,35 +65,26 @@ export default {
|
|||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'regular',
|
default: 'small',
|
||||||
validator: (value) => {
|
validator: (value) => {
|
||||||
return ['regular', 'illustrated', 'small'].includes(value)
|
return ['illustrated', 'small'].includes(value)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'unset'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
ChangeDbIcon,
|
ChangeDbIcon,
|
||||||
TextField,
|
CsvImport
|
||||||
DelimiterSelector,
|
|
||||||
CloseIcon,
|
|
||||||
CheckBox,
|
|
||||||
SqlTable,
|
|
||||||
Logs
|
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
state: '',
|
state: '',
|
||||||
animationPromise: Promise.resolve(),
|
animationPromise: Promise.resolve(),
|
||||||
file: null,
|
file: null,
|
||||||
schema: null,
|
|
||||||
delimiter: '',
|
|
||||||
quoteChar: '"',
|
|
||||||
escapeChar: '"',
|
|
||||||
header: true,
|
|
||||||
previewData: null,
|
|
||||||
importCsvMessages: [],
|
|
||||||
disableDialog: false,
|
|
||||||
importCsvCompleted: false,
|
|
||||||
newDb: null
|
newDb: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -182,205 +93,50 @@ export default {
|
|||||||
this.animationPromise = new Promise((resolve) => {
|
this.animationPromise = new Promise((resolve) => {
|
||||||
this.$refs.fileImg.addEventListener('animationend', event => {
|
this.$refs.fileImg.addEventListener('animationend', event => {
|
||||||
if (event.animationName.startsWith('fly')) {
|
if (event.animationName.startsWith('fly')) {
|
||||||
|
this.state = 'dropped'
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
quoteChar () {
|
|
||||||
this.previewCSV()
|
|
||||||
},
|
|
||||||
|
|
||||||
escapeChar () {
|
|
||||||
this.previewCSV()
|
|
||||||
},
|
|
||||||
|
|
||||||
header () {
|
|
||||||
this.previewCSV()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
cancelCsvImport () {
|
cancelCsvImport () {
|
||||||
if (!this.disableDialog) {
|
if (this.newDb) {
|
||||||
this.$modal.hide('parse')
|
this.newDb.shutDown()
|
||||||
if (this.newDb) {
|
this.newDb = null
|
||||||
this.newDb.shutDown()
|
|
||||||
this.newDb = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async finish () {
|
async finish () {
|
||||||
this.$store.commit('setDb', this.newDb)
|
this.$store.commit('setDb', this.newDb)
|
||||||
this.$store.commit('saveSchema', this.schema)
|
|
||||||
if (this.importCsvCompleted) {
|
|
||||||
this.$modal.hide('parse')
|
|
||||||
const tabId = await this.$store.dispatch('addTab', { query: 'select * from csv_import' })
|
|
||||||
this.$store.commit('setCurrentTabId', tabId)
|
|
||||||
}
|
|
||||||
if (this.$route.path !== '/editor') {
|
if (this.$route.path !== '/editor') {
|
||||||
this.$router.push('/editor')
|
this.$router.push('/editor')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async previewCSV () {
|
|
||||||
this.importCsvCompleted = false
|
|
||||||
const config = {
|
|
||||||
preview: 3,
|
|
||||||
quoteChar: this.quoteChar || '"',
|
|
||||||
escapeChar: this.escapeChar,
|
|
||||||
header: this.header,
|
|
||||||
delimiter: this.delimiter
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const start = new Date()
|
|
||||||
const parseResult = await csv.parse(this.file, config)
|
|
||||||
const end = new Date()
|
|
||||||
this.previewData = parseResult.data
|
|
||||||
this.delimiter = parseResult.delimiter
|
|
||||||
|
|
||||||
// In parseResult.messages we can get parse errors
|
|
||||||
this.importCsvMessages = parseResult.messages || []
|
|
||||||
|
|
||||||
if (!parseResult.hasErrors) {
|
|
||||||
this.importCsvMessages.push({
|
|
||||||
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
|
|
||||||
type: 'success'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.importCsvMessages = [{
|
|
||||||
message: err,
|
|
||||||
type: 'error'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
loadDb (file) {
|
loadDb (file) {
|
||||||
this.newDb = database.getNewDatabase()
|
|
||||||
return Promise.all([this.newDb.loadDb(file), this.animationPromise])
|
return Promise.all([this.newDb.loadDb(file), this.animationPromise])
|
||||||
.then(([schema]) => {
|
.then(this.finish)
|
||||||
this.schema = schema
|
|
||||||
this.finish()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
async loadFromCsv (file) {
|
|
||||||
this.disableDialog = true
|
|
||||||
const config = {
|
|
||||||
quoteChar: this.quoteChar || '"',
|
|
||||||
escapeChar: this.escapeChar,
|
|
||||||
header: this.header,
|
|
||||||
delimiter: this.delimiter
|
|
||||||
}
|
|
||||||
const parseCsvMsg = {
|
|
||||||
message: 'Parsing CSV...',
|
|
||||||
type: 'info'
|
|
||||||
}
|
|
||||||
this.importCsvMessages.push(parseCsvMsg)
|
|
||||||
const parseCsvLoadingIndicator = setTimeout(() => { parseCsvMsg.type = 'loading' }, 1000)
|
|
||||||
|
|
||||||
const importMsg = {
|
|
||||||
message: 'Importing CSV into a SQLite database...',
|
|
||||||
type: 'info'
|
|
||||||
}
|
|
||||||
let importLoadingIndicator = null
|
|
||||||
|
|
||||||
const updateProgress = progress => {
|
|
||||||
this.$set(importMsg, 'progress', progress)
|
|
||||||
}
|
|
||||||
this.newDb = database.getNewDatabase()
|
|
||||||
const progressCounterId = this.newDb.createProgressCounter(updateProgress)
|
|
||||||
|
|
||||||
try {
|
|
||||||
let start = new Date()
|
|
||||||
const parseResult = await csv.parse(this.file, config)
|
|
||||||
let end = new Date()
|
|
||||||
|
|
||||||
if (!parseResult.hasErrors) {
|
|
||||||
const rowCount = parseResult.data.values.length
|
|
||||||
let period = time.getPeriod(start, end)
|
|
||||||
parseCsvMsg.type = 'success'
|
|
||||||
|
|
||||||
if (parseResult.messages.length > 0) {
|
|
||||||
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
|
||||||
parseCsvMsg.message = `${rowCount} rows are parsed in ${period}.`
|
|
||||||
} else {
|
|
||||||
// Inform about csv parsing success
|
|
||||||
parseCsvMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loading indicator for csv parsing is not needed anymore
|
|
||||||
clearTimeout(parseCsvLoadingIndicator)
|
|
||||||
|
|
||||||
// Add info about import start
|
|
||||||
this.importCsvMessages.push(importMsg)
|
|
||||||
|
|
||||||
// Show import progress after 1 second
|
|
||||||
importLoadingIndicator = setTimeout(() => {
|
|
||||||
importMsg.type = 'loading'
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
// Create db with csv table and get schema
|
|
||||||
start = new Date()
|
|
||||||
this.schema = await this.newDb.createDb(file.name, parseResult.data, progressCounterId)
|
|
||||||
end = new Date()
|
|
||||||
|
|
||||||
// Inform about import success
|
|
||||||
period = time.getPeriod(start, end)
|
|
||||||
importMsg.message = `Importing CSV into a SQLite database is completed in ${period}.`
|
|
||||||
importMsg.type = 'success'
|
|
||||||
|
|
||||||
// Loading indicator for import is not needed anymore
|
|
||||||
clearTimeout(importLoadingIndicator)
|
|
||||||
|
|
||||||
this.importCsvCompleted = true
|
|
||||||
} else {
|
|
||||||
parseCsvMsg.message = 'Parsing ended with errors.'
|
|
||||||
parseCsvMsg.type = 'info'
|
|
||||||
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (parseCsvMsg.type === 'loading') {
|
|
||||||
parseCsvMsg.type = 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (importMsg.type === 'loading') {
|
|
||||||
importMsg.type = 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
this.importCsvMessages.push({
|
|
||||||
message: err,
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(parseCsvLoadingIndicator)
|
|
||||||
clearTimeout(importLoadingIndicator)
|
|
||||||
this.newDb.deleteProgressCounter(progressCounterId)
|
|
||||||
this.disableDialog = false
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async checkFile (file) {
|
async checkFile (file) {
|
||||||
this.state = 'drop'
|
this.state = 'dropping'
|
||||||
if (file.type === 'text/csv') {
|
this.newDb = database.getNewDatabase()
|
||||||
this.file = file
|
|
||||||
this.header = true
|
if (fIo.isDatabase(file)) {
|
||||||
this.quoteChar = '"'
|
|
||||||
this.escapeChar = '"'
|
|
||||||
this.delimiter = ''
|
|
||||||
return Promise.all([this.previewCSV(), this.animationPromise])
|
|
||||||
.then(() => {
|
|
||||||
this.$modal.show('parse')
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.loadDb(file)
|
this.loadDb(file)
|
||||||
|
} else {
|
||||||
|
this.file = file
|
||||||
|
await this.$nextTick()
|
||||||
|
const csvImport = this.$refs.addCsv
|
||||||
|
csvImport.reset()
|
||||||
|
return Promise.all([csvImport.previewCsv(), this.animationPromise])
|
||||||
|
.then(csvImport.open)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
browse () {
|
browse () {
|
||||||
fu.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
|
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
|
||||||
.then(this.checkFile)
|
.then(this.checkFile)
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -416,6 +172,7 @@ export default {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
#img-container {
|
#img-container {
|
||||||
@@ -487,42 +244,16 @@ export default {
|
|||||||
#file-img.fly {
|
#file-img.fly {
|
||||||
animation: fly ease-in-out 1s 1 normal;
|
animation: fly ease-in-out 1s 1 normal;
|
||||||
transform-origin: center center;
|
transform-origin: center center;
|
||||||
top: 183px;
|
|
||||||
left: 225px;
|
|
||||||
transition: top 1s ease-in-out, left 1s ease-in-out;
|
|
||||||
}
|
}
|
||||||
@keyframes fly {
|
@keyframes fly {
|
||||||
100% { transform: rotate(360deg) scale(0.5); }
|
100% {
|
||||||
}
|
transform: rotate(360deg) scale(0.5);
|
||||||
/* Parse CSV dialog */
|
top: 183px;
|
||||||
.chars {
|
left: 225px;
|
||||||
display: flex;
|
}
|
||||||
align-items: flex-end;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.char-input {
|
|
||||||
margin-right: 44px;
|
|
||||||
}
|
|
||||||
.preview-table {
|
|
||||||
margin-top: 32px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.import-csv-errors {
|
#file-img.hidden {
|
||||||
height: 160px;
|
display: none;
|
||||||
margin-top: 32px;
|
|
||||||
}
|
|
||||||
.no-data {
|
|
||||||
margin-top: 32px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 5px;
|
|
||||||
position: relative;
|
|
||||||
border: 1px solid var(--color-border-light);
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: 160px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--color-text-base);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg :class="['svg-container', animationClass ]" height="20" width="20" viewBox="0 0 20 20">
|
<svg :class="animationClass" :height="size" :width="size" :viewBox="`0 0 ${size} ${size}`">
|
||||||
<circle
|
<circle
|
||||||
class="loader-svg bg"
|
class="loader-svg bg"
|
||||||
cx="10"
|
:style="{ strokeWidth }"
|
||||||
cy="10"
|
:cx="size / 2"
|
||||||
r="8"
|
:cy="size / 2"
|
||||||
|
:r="radius"
|
||||||
/>
|
/>
|
||||||
<circle
|
<circle
|
||||||
class="loader-svg front"
|
class="loader-svg front"
|
||||||
:style="{ strokeDasharray: circleProgress }"
|
:style="{ strokeDasharray: circleProgress, strokeDashoffset: offset, strokeWidth }"
|
||||||
cx="10"
|
:cx="size / 2"
|
||||||
cy="10"
|
:cy="size / 2"
|
||||||
r="8"
|
:r="radius"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
@@ -19,15 +20,35 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'LoadingIndicator',
|
name: 'LoadingIndicator',
|
||||||
props: ['progress'],
|
props: {
|
||||||
|
progress: {
|
||||||
|
type: Number,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 20
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
circleProgress () {
|
circleProgress () {
|
||||||
const dash = (50.24 * this.progress) / 100
|
const circle = this.radius * 3.14 * 2
|
||||||
const space = 50.24 - dash
|
const dash = this.progress ? (circle * this.progress) / 100 : circle * 1 / 3
|
||||||
return `${dash}, ${space}`
|
const space = circle - dash
|
||||||
|
return `${dash}px, ${space}px`
|
||||||
},
|
},
|
||||||
animationClass () {
|
animationClass () {
|
||||||
return this.progress === undefined ? 'loading' : 'progress'
|
return this.progress === undefined ? 'loading' : 'progress'
|
||||||
|
},
|
||||||
|
radius () {
|
||||||
|
return this.size / 2 - this.strokeWidth
|
||||||
|
},
|
||||||
|
offset () {
|
||||||
|
return this.radius * 3.14 / 2
|
||||||
|
},
|
||||||
|
strokeWidth () {
|
||||||
|
return this.size / 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,7 +59,6 @@ export default {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0; right: 0; top: 0; bottom: 0;
|
left: 0; right: 0; top: 0; bottom: 0;
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke-width: 2px;
|
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
stroke: var(--color-accent);
|
stroke: var(--color-accent);
|
||||||
}
|
}
|
||||||
@@ -48,27 +68,30 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.loading .loader-svg.front {
|
.loading .loader-svg.front {
|
||||||
stroke-dasharray: 40.24;
|
will-change: transform;
|
||||||
animation: fill-animation-loading 1s cubic-bezier(1,1,1,1) 0s infinite;
|
animation: fill-animation-loading 1s cubic-bezier(1,1,1,1) 0s infinite;
|
||||||
|
transform-origin: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
We can't change anything in loading animation except transform, opacity and filter. Because in
|
||||||
|
our case the Main Thread can be busy and animation will be frozen (e. g. getting a result set
|
||||||
|
from the web-worker after query execution).
|
||||||
|
But transform, opacity and filter trigger changes only in the Composite Layer stage in rendering
|
||||||
|
waterfall. Hence they can be processed only with Compositor Thread while the Main Thread
|
||||||
|
processes something else.
|
||||||
|
https://www.viget.com/articles/animation-performance-101-browser-under-the-hood/
|
||||||
|
*/
|
||||||
@keyframes fill-animation-loading {
|
@keyframes fill-animation-loading {
|
||||||
0% {
|
0% {
|
||||||
stroke-dasharray: 10 40.24;
|
transform: rotate(0deg);
|
||||||
stroke-dashoffset: 0;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
stroke-dasharray: 25.12;
|
|
||||||
stroke-dashoffset: 25.12;
|
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
stroke-dasharray: 10 40.24 ;
|
transform: rotate(360deg);
|
||||||
stroke-dashoffset: 50.24;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress .loader-svg.front {
|
.progress .loader-svg.front {
|
||||||
stroke-dashoffset: 12.56;
|
|
||||||
transition: stroke-dasharray 0.2s;
|
transition: stroke-dasharray 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,14 +63,16 @@ export default {
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border: 1px solid var(--color-border-light);
|
border: 1px solid var(--color-border-light);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow-y: scroll;
|
overflow-y: auto;
|
||||||
|
color: var(--color-text-base);
|
||||||
}
|
}
|
||||||
.msg {
|
.msg {
|
||||||
padding: 16px 7px;
|
padding: 12px 7px;
|
||||||
border-bottom: 1px solid var(--color-border-light);
|
border-bottom: 1px solid var(--color-border-light);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg:last-child {
|
.msg:last-child {
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import splitter from '@/splitter'
|
import splitter from './splitter'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Splitpanes',
|
name: 'Splitpanes',
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
<div class="table-footer-count">
|
<div class="table-footer-count">
|
||||||
{{ dataSet.values.length}} {{dataSet.values.length === 1 ? 'row' : 'rows'}} retrieved
|
{{ dataSet.values.length}} {{dataSet.values.length === 1 ? 'row' : 'rows'}} retrieved
|
||||||
<span v-if="preview">for preview</span>
|
<span v-if="preview">for preview</span>
|
||||||
|
<span v-if="time">in {{ time }}</span>
|
||||||
</div>
|
</div>
|
||||||
<pager v-show="pageCount > 1" :page-count="pageCount" v-model="currentPage" />
|
<pager v-show="pageCount > 1" :page-count="pageCount" v-model="currentPage" />
|
||||||
</div>
|
</div>
|
||||||
@@ -48,12 +49,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Pager from '@/components/Pager'
|
import Pager from './Pager'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SqlTable',
|
name: 'SqlTable',
|
||||||
components: { Pager },
|
components: { Pager },
|
||||||
props: ['dataSet', 'height', 'preview'],
|
props: ['dataSet', 'time', 'height', 'preview'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
header: null,
|
header: null,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div :class="['text-field-label', { error: errorMsg }, {'disabled': disabled}]">
|
<div v-if="label" :class="['text-field-label', { error: errorMsg }, {'disabled': disabled}]">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
<hint-icon class="hint" v-if="hint" :hint="hint" :max-width="maxHintWidth || '149px'"/>
|
<hint-icon class="hint" v-if="hint" :hint="hint" :max-width="maxHintWidth || '149px'"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
:style="{ width: width }"
|
:style="{ width: width }"
|
||||||
:value="value"
|
:value="value"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@input="$emit('input', $event.target.value)"
|
@input="$emit('input', $event.target.value)"
|
||||||
/>
|
/>
|
||||||
<div v-show="errorMsg" class="text-field-error">{{ errorMsg }}</div>
|
<div v-show="errorMsg" class="text-field-error">{{ errorMsg }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,4 +87,7 @@ input.error {
|
|||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
.text-field-error:first-letter {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
61
src/components/svg/addTable.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
<svg
|
||||||
|
class="icon"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
@click.stop="$emit('click')"
|
||||||
|
@mouseover="showTooltip"
|
||||||
|
@mouseout="hideTooltip"
|
||||||
|
>
|
||||||
|
<g clip-path="url(#clip0)">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="
|
||||||
|
M13.6573 1.5H2.59985C1.77485 1.5 1.09985 2.175 1.09985 3V13.6649C1.09985 14.4899
|
||||||
|
1.77485 15.1649 2.59985
|
||||||
|
15.1649H9.84V13.6649H8.87866V9.08244H13.6573V9.83777H15.1573V3C15.1573
|
||||||
|
2.17 14.4873 1.5 13.6573 1.5ZM13.6573
|
||||||
|
7.58244V3H8.87866V7.58244H13.6573ZM7.37866 3H2.59985V7.58244H7.37866V3ZM2.59985
|
||||||
|
9.08244V13.6649H7.37866V9.08244H2.59985ZM13.1702
|
||||||
|
10.8434H15.6702V13.1717H18.0001V15.6717H15.6702V18H13.1702V15.6717H10.8401V13.1717H13.1702V10.8434Z
|
||||||
|
"
|
||||||
|
fill="#A2B1C6"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0">
|
||||||
|
<rect width="18" height="18" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
<span class="icon-tooltip" :style="tooltipStyle">
|
||||||
|
Add new table from CSV
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AddTableIcon',
|
||||||
|
mixins: [tooltipMixin],
|
||||||
|
props: ['tooltip']
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.icon {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon:hover path {
|
||||||
|
fill: var(--color-accent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -16,13 +16,13 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="icon-tooltip" :style="tooltipStyle">
|
<span class="icon-tooltip" :style="tooltipStyle">
|
||||||
Change database
|
Load another database or CSV
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import tooltipMixin from '@/mixins/tooltips'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'changeDbIcon',
|
name: 'changeDbIcon',
|
||||||
|
|||||||
@@ -15,26 +15,28 @@
|
|||||||
d="M10.5 1.5H4.5C3.675 1.5 3 2.175 3 3V15C3 15.825 3.675 16.5 4.5 16.5H13.5C14.325 16.5 15 15.825 15 15V6L10.5 1.5ZM13.5 15H4.5V3H9.75V6.75H13.5V15ZM12 8.25V13.575L10.425 12L8.325 14.1L6.225 12L8.325 9.9L6.675 8.25H12Z"
|
d="M10.5 1.5H4.5C3.675 1.5 3 2.175 3 3V15C3 15.825 3.675 16.5 4.5 16.5H13.5C14.325 16.5 15 15.825 15 15V6L10.5 1.5ZM13.5 15H4.5V3H9.75V6.75H13.5V15ZM12 8.25V13.575L10.425 12L8.325 14.1L6.225 12L8.325 9.9L6.675 8.25H12Z"
|
||||||
fill="#A2B1C6"
|
fill="#A2B1C6"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="icon-tooltip" :style="tooltipStyle">
|
<span class="icon-tooltip" :style="tooltipStyle">
|
||||||
Export query to file
|
{{ tooltip }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import tooltipMixin from '@/mixins/tooltips'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ExportIcon',
|
name: 'ExportIcon',
|
||||||
mixins: [tooltipMixin]
|
mixins: [tooltipMixin],
|
||||||
|
props: ['tooltip']
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.icon {
|
.icon {
|
||||||
vertical-align: middle;
|
display: block;
|
||||||
margin: 0 12px;
|
margin: 0 12px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon:hover path {
|
.icon:hover path {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import tooltipMixin from '@/mixins/tooltips'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HintIcon',
|
name: 'HintIcon',
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
export default {
|
|
||||||
* generateChunks (arr, size) {
|
|
||||||
const count = Math.ceil(arr.length / size)
|
|
||||||
|
|
||||||
for (let i = 0; i <= count - 1; i++) {
|
|
||||||
const start = size * i
|
|
||||||
const end = start + size
|
|
||||||
yield arr.slice(start, end)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getInsertStmt (columns) {
|
|
||||||
const colList = `"${columns.join('", "')}"`
|
|
||||||
const params = columns.map(() => '?').join(', ')
|
|
||||||
return `INSERT INTO csv_import (${colList}) VALUES (${params});`
|
|
||||||
},
|
|
||||||
|
|
||||||
getCreateStatement (columns, values) {
|
|
||||||
let result = 'CREATE table csv_import('
|
|
||||||
columns.forEach((col, index) => {
|
|
||||||
// Get the first row of values to determine types
|
|
||||||
const value = values[0][index]
|
|
||||||
let type = ''
|
|
||||||
switch (typeof value) {
|
|
||||||
case 'number': {
|
|
||||||
type = 'REAL'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'boolean': {
|
|
||||||
type = 'INTEGER'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'string': {
|
|
||||||
type = 'TEXT'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default: type = 'TEXT'
|
|
||||||
}
|
|
||||||
result += `"${col}" ${type}, `
|
|
||||||
})
|
|
||||||
result = result.replace(/,\s$/, ');')
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import initSqlJs from 'sql.js/dist/sql-wasm.js'
|
import initSqlJs from 'sql.js/dist/sql-wasm.js'
|
||||||
import dbUtils from '@/db.utils'
|
import dbUtils from './_statements'
|
||||||
|
|
||||||
let SQL = null
|
let SQL = null
|
||||||
const sqlModuleReady = initSqlJs().then(sqlModule => { SQL = sqlModule })
|
const sqlModuleReady = initSqlJs().then(sqlModule => { SQL = sqlModule })
|
||||||
@@ -39,13 +39,15 @@ export default class Sql {
|
|||||||
return this.db.exec(sql, params)
|
return this.db.exec(sql, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
import (columns, values, progressCounterId, progressCallback, chunkSize = 1500) {
|
import (tabName, columns, values, progressCounterId, progressCallback, chunkSize = 1500) {
|
||||||
this.createDb()
|
if (this.db === null) {
|
||||||
this.db.exec(dbUtils.getCreateStatement(columns, values))
|
this.createDb()
|
||||||
|
}
|
||||||
|
this.db.exec(dbUtils.getCreateStatement(tabName, columns, values))
|
||||||
const chunks = dbUtils.generateChunks(values, chunkSize)
|
const chunks = dbUtils.generateChunks(values, chunkSize)
|
||||||
const chunksAmount = Math.ceil(values.length / chunkSize)
|
const chunksAmount = Math.ceil(values.length / chunkSize)
|
||||||
let count = 0
|
let count = 0
|
||||||
const insertStr = dbUtils.getInsertStmt(columns)
|
const insertStr = dbUtils.getInsertStmt(tabName, columns)
|
||||||
const insertStmt = this.db.prepare(insertStr)
|
const insertStmt = this.db.prepare(insertStr)
|
||||||
|
|
||||||
progressCallback({ progress: 0, id: progressCounterId })
|
progressCallback({ progress: 0, id: progressCounterId })
|
||||||
90
src/lib/database/_statements.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import sqliteParser from 'sqlite-parser'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
* generateChunks (arr, size) {
|
||||||
|
const count = Math.ceil(arr.length / size)
|
||||||
|
|
||||||
|
for (let i = 0; i <= count - 1; i++) {
|
||||||
|
const start = size * i
|
||||||
|
const end = start + size
|
||||||
|
yield arr.slice(start, end)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getInsertStmt (tabName, columns) {
|
||||||
|
const colList = `"${columns.join('", "')}"`
|
||||||
|
const params = columns.map(() => '?').join(', ')
|
||||||
|
return `INSERT INTO "${tabName}" (${colList}) VALUES (${params});`
|
||||||
|
},
|
||||||
|
|
||||||
|
getCreateStatement (tabName, columns, values) {
|
||||||
|
let result = `CREATE table "${tabName}"(`
|
||||||
|
columns.forEach((col, index) => {
|
||||||
|
// Get the first row of values to determine types
|
||||||
|
const value = values[0][index]
|
||||||
|
let type = ''
|
||||||
|
switch (typeof value) {
|
||||||
|
case 'number': {
|
||||||
|
type = 'REAL'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'boolean': {
|
||||||
|
type = 'INTEGER'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'string': {
|
||||||
|
type = 'TEXT'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: type = 'TEXT'
|
||||||
|
}
|
||||||
|
result += `"${col}" ${type}, `
|
||||||
|
})
|
||||||
|
result = result.replace(/,\s$/, ');')
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
getAst (sql) {
|
||||||
|
// There is a bug is sqlite-parser
|
||||||
|
// It throws an error if tokenizer has an arguments:
|
||||||
|
// https://github.com/codeschool/sqlite-parser/issues/59
|
||||||
|
const fixedSql = sql
|
||||||
|
.replace(/(tokenize=[^,]+)"tokenchars=.+?"/, '$1')
|
||||||
|
.replace(/(tokenize=[^,]+)"remove_diacritics=.+?"/, '$1')
|
||||||
|
.replace(/(tokenize=[^,]+)"separators=.+?"/, '$1')
|
||||||
|
.replace(/tokenize=.+?(,|\))/, 'tokenize=unicode61$1')
|
||||||
|
|
||||||
|
return sqliteParser(fixedSql)
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Return an array of columns with name and type. E.g.:
|
||||||
|
* [
|
||||||
|
* { name: 'id', type: 'INTEGER' },
|
||||||
|
* { name: 'title', type: 'NVARCHAR(30)' },
|
||||||
|
* ]
|
||||||
|
*/
|
||||||
|
getColumns (sql) {
|
||||||
|
const columns = []
|
||||||
|
const ast = this.getAst(sql)
|
||||||
|
|
||||||
|
const columnDefinition = ast.statement[0].format === 'table'
|
||||||
|
? ast.statement[0].definition
|
||||||
|
: ast.statement[0].result.args.expression // virtual table
|
||||||
|
|
||||||
|
columnDefinition.forEach(item => {
|
||||||
|
if (item.variant === 'column' && ['identifier', 'definition'].includes(item.type)) {
|
||||||
|
let type = item.datatype ? item.datatype.variant : 'N/A'
|
||||||
|
if (item.datatype && item.datatype.args) {
|
||||||
|
type = type + '(' + item.datatype.args.expression[0].value
|
||||||
|
if (item.datatype.args.expression.length === 2) {
|
||||||
|
type = type + ', ' + item.datatype.args.expression[1].value
|
||||||
|
}
|
||||||
|
type = type + ')'
|
||||||
|
}
|
||||||
|
columns.push({ name: item.name, type: type })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return columns
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import registerPromiseWorker from 'promise-worker/register'
|
import registerPromiseWorker from 'promise-worker/register'
|
||||||
import Sql from '@/sql'
|
import Sql from './_sql'
|
||||||
|
|
||||||
const sqlReady = Sql.build()
|
const sqlReady = Sql.build()
|
||||||
|
|
||||||
@@ -8,10 +8,18 @@ function processMsg (sql) {
|
|||||||
switch (data && data.action) {
|
switch (data && data.action) {
|
||||||
case 'open':
|
case 'open':
|
||||||
return sql.open(data.buffer)
|
return sql.open(data.buffer)
|
||||||
|
case 'reopen':
|
||||||
|
return sql.open(sql.export())
|
||||||
case 'exec':
|
case 'exec':
|
||||||
return sql.exec(data.sql, data.params)
|
return sql.exec(data.sql, data.params)
|
||||||
case 'import':
|
case 'import':
|
||||||
return sql.import(data.columns, data.values, data.progressCounterId, postMessage)
|
return sql.import(
|
||||||
|
data.tabName,
|
||||||
|
data.columns,
|
||||||
|
data.values,
|
||||||
|
data.progressCounterId,
|
||||||
|
postMessage
|
||||||
|
)
|
||||||
case 'export':
|
case 'export':
|
||||||
return sql.export()
|
return sql.export()
|
||||||
case 'close':
|
case 'close':
|
||||||
@@ -23,7 +31,7 @@ function processMsg (sql) {
|
|||||||
|
|
||||||
function onError (error) {
|
function onError (error) {
|
||||||
return {
|
return {
|
||||||
error
|
error: error.message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import sqliteParser from 'sqlite-parser'
|
import stms from './_statements'
|
||||||
import fu from '@/file.utils'
|
import fu from '@/lib/utils/fileIo'
|
||||||
// We can import workers like so because of worker-loader:
|
// We can import workers like so because of worker-loader:
|
||||||
// https://webpack.js.org/loaders/worker-loader/
|
// https://webpack.js.org/loaders/worker-loader/
|
||||||
import Worker from '@/db.worker.js'
|
import Worker from './_worker.js'
|
||||||
|
|
||||||
// Use promise-worker in order to turn worker into the promise based one:
|
// Use promise-worker in order to turn worker into the promise based one:
|
||||||
// https://github.com/nolanlawson/promise-worker
|
// https://github.com/nolanlawson/promise-worker
|
||||||
@@ -20,6 +20,8 @@ export default {
|
|||||||
let progressCounterIds = 0
|
let progressCounterIds = 0
|
||||||
class Database {
|
class Database {
|
||||||
constructor (worker) {
|
constructor (worker) {
|
||||||
|
this.dbName = null
|
||||||
|
this.schema = null
|
||||||
this.worker = worker
|
this.worker = worker
|
||||||
this.pw = new PromiseWorker(worker)
|
this.pw = new PromiseWorker(worker)
|
||||||
|
|
||||||
@@ -50,33 +52,35 @@ class Database {
|
|||||||
delete this.importProgresses[id]
|
delete this.importProgresses[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
async createDb (name, data, progressCounterId) {
|
async addTableFromCsv (tabName, data, progressCounterId) {
|
||||||
const result = await this.pw.postMessage({
|
const result = await this.pw.postMessage({
|
||||||
action: 'import',
|
action: 'import',
|
||||||
columns: data.columns,
|
columns: data.columns,
|
||||||
values: data.values,
|
values: data.values,
|
||||||
progressCounterId
|
progressCounterId,
|
||||||
|
tabName
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
throw result.error
|
throw new Error(result.error)
|
||||||
}
|
}
|
||||||
|
this.dbName = this.dbName || 'database'
|
||||||
return await this.getSchema(name)
|
this.refreshSchema()
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadDb (file) {
|
async loadDb (file) {
|
||||||
const fileContent = await fu.readAsArrayBuffer(file)
|
const fileContent = file ? await fu.readAsArrayBuffer(file) : null
|
||||||
const res = await this.pw.postMessage({ action: 'open', buffer: fileContent })
|
const res = await this.pw.postMessage({ action: 'open', buffer: fileContent })
|
||||||
|
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
throw res.error
|
throw new Error(res.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getSchema(file.name)
|
this.dbName = file ? fu.getFileName(file) : 'database'
|
||||||
|
this.refreshSchema()
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSchema (name) {
|
async refreshSchema () {
|
||||||
const getSchemaSql = `
|
const getSchemaSql = `
|
||||||
SELECT name, sql
|
SELECT name, sql
|
||||||
FROM sqlite_master
|
FROM sqlite_master
|
||||||
@@ -85,71 +89,59 @@ class Database {
|
|||||||
const result = await this.execute(getSchemaSql)
|
const result = await this.execute(getSchemaSql)
|
||||||
// Parse DDL statements to get column names and types
|
// Parse DDL statements to get column names and types
|
||||||
const parsedSchema = []
|
const parsedSchema = []
|
||||||
result.values.forEach(item => {
|
if (result && result.values) {
|
||||||
parsedSchema.push({
|
result.values.forEach(item => {
|
||||||
name: item[0],
|
parsedSchema.push({
|
||||||
columns: getColumns(item[1])
|
name: item[0],
|
||||||
|
columns: stms.getColumns(item[1])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
// Return db name and schema
|
|
||||||
return {
|
|
||||||
dbName: name,
|
|
||||||
schema: parsedSchema
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh schema
|
||||||
|
this.schema = parsedSchema
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute (commands) {
|
async execute (commands) {
|
||||||
|
await this.pw.postMessage({ action: 'reopen' })
|
||||||
const results = await this.pw.postMessage({ action: 'exec', sql: commands })
|
const results = await this.pw.postMessage({ action: 'exec', sql: commands })
|
||||||
|
|
||||||
if (results.error) {
|
if (results.error) {
|
||||||
throw results.error
|
throw new Error(results.error)
|
||||||
}
|
}
|
||||||
// if it was more than one select - take only the last one
|
// if it was more than one select - take only the last one
|
||||||
return results[results.length - 1]
|
return results[results.length - 1]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function getAst (sql) {
|
async export (fileName) {
|
||||||
// There is a bug is sqlite-parser
|
const data = await this.pw.postMessage({ action: 'export' })
|
||||||
// It throws an error if tokenizer has an arguments:
|
|
||||||
// https://github.com/codeschool/sqlite-parser/issues/59
|
|
||||||
const fixedSql = sql
|
|
||||||
.replace(/(?<=tokenize=.+)"tokenchars=.+"/, '')
|
|
||||||
.replace(/(?<=tokenize=.+)"remove_diacritics=.+"/, '')
|
|
||||||
.replace(/(?<=tokenize=.+)"separators=.+"/, '')
|
|
||||||
.replace(/tokenize=.+(?=(,|\)))/, 'tokenize=unicode61')
|
|
||||||
|
|
||||||
return sqliteParser(fixedSql)
|
if (data.error) {
|
||||||
}
|
throw new Error(data.error)
|
||||||
|
|
||||||
/*
|
|
||||||
* 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 })
|
|
||||||
}
|
}
|
||||||
})
|
fu.exportToFile(data, fileName)
|
||||||
return columns
|
}
|
||||||
|
|
||||||
|
async validateTableName (name) {
|
||||||
|
if (name.startsWith('sqlite_')) {
|
||||||
|
throw new Error("Table name can't start with sqlite_")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/[^\w]/.test(name)) {
|
||||||
|
throw new Error('Table name can contain only letters, digits and underscores')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^(\d)/.test(name)) {
|
||||||
|
throw new Error("Table name can't start with a digit")
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.execute(`BEGIN; CREATE TABLE "${name}"(id); ROLLBACK;`)
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitizeTableName (tabName) {
|
||||||
|
return tabName
|
||||||
|
.replace(/[^\w]/g, '_') // replace everything that is not letter, digit or _ with _
|
||||||
|
.replace(/^(\d)/, '_$1') // add _ at beginning if starts with digit
|
||||||
|
.replace(/_{2,}/g, '_') // replace multiple _ with one _
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import fu from '@/file.utils'
|
import fu from '@/lib/utils/fileIo'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getStoredQueries () {
|
getStoredQueries () {
|
||||||
@@ -1,4 +1,15 @@
|
|||||||
export default {
|
export default {
|
||||||
|
isDatabase (file) {
|
||||||
|
const dbTypes = ['application/vnd.sqlite3', 'application/x-sqlite3']
|
||||||
|
return file.type
|
||||||
|
? dbTypes.includes(file.type)
|
||||||
|
: /\.(db|sqlite(3)?)+$/.test(file.name)
|
||||||
|
},
|
||||||
|
|
||||||
|
getFileName (file) {
|
||||||
|
return file.name.replace(/\.[^.]+$/, '')
|
||||||
|
},
|
||||||
|
|
||||||
exportToFile (str, fileName, type = 'octet/stream') {
|
exportToFile (str, fileName, type = 'octet/stream') {
|
||||||
// Create downloader
|
// Create downloader
|
||||||
const downloader = document.createElement('a')
|
const downloader = document.createElement('a')
|
||||||
15
src/lib/utils/time.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export default {
|
||||||
|
getPeriod (start, end) {
|
||||||
|
const diff = end.getTime() - start.getTime()
|
||||||
|
const seconds = diff / 1000
|
||||||
|
return seconds.toFixed(3) + 's'
|
||||||
|
},
|
||||||
|
|
||||||
|
debounce (func, ms) {
|
||||||
|
let timeout
|
||||||
|
return function () {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(() => func.apply(this, arguments), ms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/main.js
@@ -1,7 +1,7 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import App from './App.vue'
|
import App from '@/App.vue'
|
||||||
import router from './router'
|
import router from '@/router'
|
||||||
import store from './store'
|
import store from '@/store'
|
||||||
import { VuePlugin } from 'vuera'
|
import { VuePlugin } from 'vuera'
|
||||||
import VModal from 'vue-js-modal'
|
import VModal from 'vue-js-modal'
|
||||||
|
|
||||||
@@ -12,6 +12,10 @@ import '@/assets/styles/dialogs.css'
|
|||||||
import '@/assets/styles/tooltips.css'
|
import '@/assets/styles/tooltips.css'
|
||||||
import '@/assets/styles/messages.css'
|
import '@/assets/styles/messages.css'
|
||||||
|
|
||||||
|
if (!['localhost', '127.0.0.1'].includes(location.hostname)) {
|
||||||
|
import('./registerServiceWorker') // eslint-disable-line no-unused-expressions
|
||||||
|
}
|
||||||
|
|
||||||
Vue.use(VuePlugin)
|
Vue.use(VuePlugin)
|
||||||
Vue.use(VModal)
|
Vue.use(VModal)
|
||||||
|
|
||||||
|
|||||||
44
src/registerServiceWorker.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
let refresh = false
|
||||||
|
|
||||||
|
function invokeServiceWorkerUpdateFlow (registration) {
|
||||||
|
const agree = confirm('New version of the app is available. Refresh now?')
|
||||||
|
if (agree) {
|
||||||
|
if (registration.waiting) {
|
||||||
|
// let waiting Service Worker know it should became active
|
||||||
|
refresh = true
|
||||||
|
registration.waiting.postMessage({ type: 'SKIP_WAITING' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', async () => {
|
||||||
|
const registration = await navigator.serviceWorker.register('service-worker.js')
|
||||||
|
// ensure the case when the updatefound event was missed is also handled
|
||||||
|
// by re-invoking the prompt when there's a waiting Service Worker
|
||||||
|
if (registration.waiting) {
|
||||||
|
invokeServiceWorkerUpdateFlow(registration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// detect Service Worker update available and wait for it to become installed
|
||||||
|
registration.addEventListener('updatefound', () => {
|
||||||
|
const newRegestration = registration.installing
|
||||||
|
if (newRegestration) {
|
||||||
|
// wait until the new Service worker is actually installed (ready to take over)
|
||||||
|
newRegestration.addEventListener('statechange', () => {
|
||||||
|
if (registration.waiting && navigator.serviceWorker.controller) {
|
||||||
|
invokeServiceWorkerUpdateFlow(registration)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// detect controller change and refresh the page
|
||||||
|
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||||
|
if (refresh) {
|
||||||
|
window.location.reload()
|
||||||
|
refresh = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
50
src/router.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import VueRouter from 'vue-router'
|
||||||
|
import Editor from '@/views/Main/Editor'
|
||||||
|
import MyQueries from '@/views/Main/MyQueries'
|
||||||
|
import Welcome from '@/views/Welcome'
|
||||||
|
import Main from '@/views/Main'
|
||||||
|
import store from '@/store'
|
||||||
|
import database from '@/lib/database'
|
||||||
|
|
||||||
|
Vue.use(VueRouter)
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Welcome',
|
||||||
|
component: Welcome
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Main',
|
||||||
|
component: Main,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/editor',
|
||||||
|
name: 'Editor',
|
||||||
|
component: Editor
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/my-queries',
|
||||||
|
name: 'MyQueries',
|
||||||
|
component: MyQueries
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = new VueRouter({
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach(async (to, from, next) => {
|
||||||
|
if (!store.state.db) {
|
||||||
|
const newDb = database.getNewDatabase()
|
||||||
|
await newDb.loadDb()
|
||||||
|
store.commit('setDb', newDb)
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import VueRouter from 'vue-router'
|
|
||||||
import Editor from '@/views/Editor'
|
|
||||||
import MyQueries from '@/views/MyQueries'
|
|
||||||
import Home from '@/views/Home'
|
|
||||||
import MainView from '@/views/MainView'
|
|
||||||
|
|
||||||
Vue.use(VueRouter)
|
|
||||||
|
|
||||||
const routes = [
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
name: 'Welcome',
|
|
||||||
component: Home
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
name: 'MainView',
|
|
||||||
component: MainView,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '/editor',
|
|
||||||
name: 'Editor',
|
|
||||||
component: Editor
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/my-queries',
|
|
||||||
name: 'MyQueries',
|
|
||||||
component: MyQueries
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const router = new VueRouter({
|
|
||||||
routes
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router
|
|
||||||
30
src/store/actions.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async addTab ({ state }, data) {
|
||||||
|
const tab = data ? JSON.parse(JSON.stringify(data)) : {}
|
||||||
|
// If no data then create a new blank one...
|
||||||
|
// No data.id means to create new tab, but not blank,
|
||||||
|
// e.g. with 'select * from csv_import' query after csv import
|
||||||
|
if (!data || !data.id) {
|
||||||
|
tab.id = nanoid()
|
||||||
|
tab.name = null
|
||||||
|
tab.tempName = state.untitledLastIndex
|
||||||
|
? `Untitled ${state.untitledLastIndex}`
|
||||||
|
: 'Untitled'
|
||||||
|
tab.isUnsaved = true
|
||||||
|
} else {
|
||||||
|
tab.isUnsaved = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// add new tab only if was not already opened
|
||||||
|
if (!state.tabs.some(openedTab => openedTab.id === tab.id)) {
|
||||||
|
state.tabs.push(tab)
|
||||||
|
if (!tab.name) {
|
||||||
|
state.untitledLastIndex += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tab.id
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,112 +1,11 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
import { nanoid } from 'nanoid'
|
import state from '@/store/state'
|
||||||
|
import mutations from '@/store/mutations'
|
||||||
|
import actions from '@/store/actions'
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
|
|
||||||
export const state = {
|
|
||||||
schema: null,
|
|
||||||
dbFile: null,
|
|
||||||
dbName: null,
|
|
||||||
tabs: [],
|
|
||||||
currentTab: null,
|
|
||||||
currentTabId: null,
|
|
||||||
untitledLastIndex: 0,
|
|
||||||
predefinedQueries: [],
|
|
||||||
db: null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mutations = {
|
|
||||||
setDb (state, db) {
|
|
||||||
if (state.db) {
|
|
||||||
state.db.shutDown()
|
|
||||||
}
|
|
||||||
state.db = db
|
|
||||||
},
|
|
||||||
saveSchema (state, { dbName, schema }) {
|
|
||||||
state.dbName = dbName
|
|
||||||
state.schema = schema
|
|
||||||
},
|
|
||||||
|
|
||||||
updateTab (state, { index, name, id, query, chart, isUnsaved }) {
|
|
||||||
const tab = state.tabs[index]
|
|
||||||
const oldId = tab.id
|
|
||||||
|
|
||||||
if (id && state.currentTabId === oldId) {
|
|
||||||
state.currentTabId = id
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id) { tab.id = id }
|
|
||||||
if (name) { tab.name = name }
|
|
||||||
if (query) { tab.query = query }
|
|
||||||
if (chart) { tab.chart = chart }
|
|
||||||
if (isUnsaved !== undefined) { tab.isUnsaved = isUnsaved }
|
|
||||||
if (!isUnsaved) {
|
|
||||||
// Saved query is not predefined
|
|
||||||
delete tab.isPredefined
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.set(state.tabs, index, tab)
|
|
||||||
},
|
|
||||||
deleteTab (state, index) {
|
|
||||||
// If closing tab is the current opened
|
|
||||||
if (state.tabs[index].id === state.currentTabId) {
|
|
||||||
if (index < state.tabs.length - 1) {
|
|
||||||
state.currentTabId = state.tabs[index + 1].id
|
|
||||||
} else if (index > 0) {
|
|
||||||
state.currentTabId = state.tabs[index - 1].id
|
|
||||||
} else {
|
|
||||||
state.currentTabId = null
|
|
||||||
state.currentTab = null
|
|
||||||
state.untitledLastIndex = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.tabs.splice(index, 1)
|
|
||||||
},
|
|
||||||
setCurrentTabId (state, id) {
|
|
||||||
state.currentTabId = id
|
|
||||||
},
|
|
||||||
setCurrentTab (state, tab) {
|
|
||||||
state.currentTab = tab
|
|
||||||
},
|
|
||||||
updatePredefinedQueries (state, queries) {
|
|
||||||
if (Array.isArray(queries)) {
|
|
||||||
state.predefinedQueries = queries
|
|
||||||
} else {
|
|
||||||
state.predefinedQueries = [queries]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const actions = {
|
|
||||||
async addTab ({ state }, data) {
|
|
||||||
const tab = data ? JSON.parse(JSON.stringify(data)) : {}
|
|
||||||
// If no data then create a new blank one...
|
|
||||||
// No data.id means to create new tab, but not blank,
|
|
||||||
// e.g. with 'select * from csv_import' query after csv import
|
|
||||||
if (!data || !data.id) {
|
|
||||||
tab.id = nanoid()
|
|
||||||
tab.name = null
|
|
||||||
tab.tempName = state.untitledLastIndex
|
|
||||||
? `Untitled ${state.untitledLastIndex}`
|
|
||||||
: 'Untitled'
|
|
||||||
tab.isUnsaved = true
|
|
||||||
} else {
|
|
||||||
tab.isUnsaved = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// add new tab only if was not already opened
|
|
||||||
if (!state.tabs.some(openedTab => openedTab.id === tab.id)) {
|
|
||||||
state.tabs.push(tab)
|
|
||||||
if (!tab.name) {
|
|
||||||
state.untitledLastIndex += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tab.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
state,
|
state,
|
||||||
mutations,
|
mutations,
|
||||||
|
|||||||
59
src/store/mutations.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
setDb (state, db) {
|
||||||
|
if (state.db) {
|
||||||
|
state.db.shutDown()
|
||||||
|
}
|
||||||
|
state.db = db
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTab (state, { index, name, id, query, chart, isUnsaved }) {
|
||||||
|
const tab = state.tabs[index]
|
||||||
|
const oldId = tab.id
|
||||||
|
|
||||||
|
if (id && state.currentTabId === oldId) {
|
||||||
|
state.currentTabId = id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id) { tab.id = id }
|
||||||
|
if (name) { tab.name = name }
|
||||||
|
if (query) { tab.query = query }
|
||||||
|
if (chart) { tab.chart = chart }
|
||||||
|
if (isUnsaved !== undefined) { tab.isUnsaved = isUnsaved }
|
||||||
|
if (!isUnsaved) {
|
||||||
|
// Saved query is not predefined
|
||||||
|
delete tab.isPredefined
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.set(state.tabs, index, tab)
|
||||||
|
},
|
||||||
|
deleteTab (state, index) {
|
||||||
|
// If closing tab is the current opened
|
||||||
|
if (state.tabs[index].id === state.currentTabId) {
|
||||||
|
if (index < state.tabs.length - 1) {
|
||||||
|
state.currentTabId = state.tabs[index + 1].id
|
||||||
|
} else if (index > 0) {
|
||||||
|
state.currentTabId = state.tabs[index - 1].id
|
||||||
|
} else {
|
||||||
|
state.currentTabId = null
|
||||||
|
state.currentTab = null
|
||||||
|
state.untitledLastIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.tabs.splice(index, 1)
|
||||||
|
},
|
||||||
|
setCurrentTabId (state, id) {
|
||||||
|
state.currentTabId = id
|
||||||
|
},
|
||||||
|
setCurrentTab (state, tab) {
|
||||||
|
state.currentTab = tab
|
||||||
|
},
|
||||||
|
updatePredefinedQueries (state, queries) {
|
||||||
|
if (Array.isArray(queries)) {
|
||||||
|
state.predefinedQueries = queries
|
||||||
|
} else {
|
||||||
|
state.predefinedQueries = [queries]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/store/state.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
tabs: [],
|
||||||
|
currentTab: null,
|
||||||
|
currentTabId: null,
|
||||||
|
untitledLastIndex: 0,
|
||||||
|
predefinedQueries: [],
|
||||||
|
db: null
|
||||||
|
}
|
||||||
36
src/time.js
@@ -1,36 +0,0 @@
|
|||||||
export default {
|
|
||||||
getPeriod (start, end) {
|
|
||||||
let diff = end.getTime() - start.getTime()
|
|
||||||
let result = ''
|
|
||||||
|
|
||||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
|
||||||
diff -= days * (1000 * 60 * 60 * 24)
|
|
||||||
if (days) {
|
|
||||||
result += days + ' d '
|
|
||||||
}
|
|
||||||
|
|
||||||
const hours = Math.floor(diff / (1000 * 60 * 60))
|
|
||||||
diff -= hours * (1000 * 60 * 60)
|
|
||||||
if (hours) {
|
|
||||||
result += hours + ' h '
|
|
||||||
}
|
|
||||||
|
|
||||||
const mins = Math.floor(diff / (1000 * 60))
|
|
||||||
diff -= mins * (1000 * 60)
|
|
||||||
if (mins) {
|
|
||||||
result += mins + ' m '
|
|
||||||
}
|
|
||||||
|
|
||||||
const seconds = Math.floor(diff / (1000))
|
|
||||||
diff -= seconds * (1000)
|
|
||||||
if (seconds) {
|
|
||||||
result += seconds + ' s '
|
|
||||||
}
|
|
||||||
|
|
||||||
if (diff) {
|
|
||||||
result += diff + ' ms '
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.replace(/\s$/, '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<splitpanes
|
|
||||||
class="schema-tabs-splitter"
|
|
||||||
:before="{ size: 20, max: 30 }"
|
|
||||||
:after="{ size: 80, max: 100 }"
|
|
||||||
>
|
|
||||||
<template #left-pane>
|
|
||||||
<schema v-if="$store.state.schema"/>
|
|
||||||
<div v-else id="empty-schema-container">
|
|
||||||
<div class="warning">
|
|
||||||
Database is not loaded. Queries can’t be run without database.
|
|
||||||
</div>
|
|
||||||
<db-uploader id="db-uploader"/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #right-pane>
|
|
||||||
<tabs />
|
|
||||||
</template>
|
|
||||||
</splitpanes>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Splitpanes from '@/components/Splitpanes'
|
|
||||||
import Schema from '@/components/Schema'
|
|
||||||
import Tabs from '@/components/Tabs'
|
|
||||||
import DbUploader from '@/components/DbUploader'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'Editor',
|
|
||||||
components: {
|
|
||||||
Schema,
|
|
||||||
Splitpanes,
|
|
||||||
Tabs,
|
|
||||||
DbUploader
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.schema-tabs-splitter {
|
|
||||||
height: 100%;
|
|
||||||
background-color: var(--color-white);
|
|
||||||
}
|
|
||||||
#empty-schema-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 200px;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#db-uploader {
|
|
||||||
flex-grow: 1;
|
|
||||||
padding: 24px;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning {
|
|
||||||
padding: 12px 24px;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
>>> .db-uploader-container {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
>>>.drop-area {
|
|
||||||
padding: 0 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
>>>.drop-area .text {
|
|
||||||
max-width: 200px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
90
src/views/Main/AppDiagnosticInfo.vue
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app-info-container">
|
||||||
|
<img
|
||||||
|
id="app-info-icon"
|
||||||
|
:src="require('@/assets/images/info.svg')"
|
||||||
|
@click="$modal.show('app-info')"
|
||||||
|
/>
|
||||||
|
<modal name="app-info" classes="dialog" height="auto" width="400px">
|
||||||
|
<div class="dialog-header">
|
||||||
|
App info
|
||||||
|
<close-icon @click="$modal.hide('app-info')"/>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-body">
|
||||||
|
<div v-for="(item, index) in info" :key="index" class="info-item">
|
||||||
|
{{item.name}}
|
||||||
|
<div class="divider"/>
|
||||||
|
<div class="options">
|
||||||
|
<div v-for="(opt, index) in item.info" :key="index">
|
||||||
|
{{opt}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CloseIcon from '@/components/svg/close'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AppDiagnosticInfo',
|
||||||
|
components: { CloseIcon },
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
info: [
|
||||||
|
{
|
||||||
|
name: 'sqliteviz version',
|
||||||
|
info: [require('../../../package.json').version]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async created () {
|
||||||
|
const state = this.$store.state
|
||||||
|
let result = await state.db.execute('select sqlite_version()')
|
||||||
|
this.info.push({
|
||||||
|
name: 'SQLite version',
|
||||||
|
info: result.values[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
result = await state.db.execute('PRAGMA compile_options')
|
||||||
|
this.info.push({
|
||||||
|
name: 'SQLite compile options',
|
||||||
|
info: result.values.map(row => row[0])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#app-info-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#app-info-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 32px;
|
||||||
|
}
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--color-border);
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
.options {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-left: 8px;
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 170px;
|
||||||
|
}
|
||||||
|
.info-item {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.info-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,10 +5,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="db">
|
<div id="db">
|
||||||
<div @click="schemaVisible = !schemaVisible" class="db-name">
|
<div @click="schemaVisible = !schemaVisible" class="db-name">
|
||||||
<tree-chevron :expanded="schemaVisible"/>
|
<tree-chevron v-show="schema.length > 0" :expanded="schemaVisible"/>
|
||||||
{{ dbName }}
|
{{ dbName }}
|
||||||
</div>
|
</div>
|
||||||
<db-uploader id="db-edit" type="small" />
|
<db-uploader id="db-edit" type="small" />
|
||||||
|
<export-icon tooltip="Export database" @click="exportToFile"/>
|
||||||
|
<add-table-icon @click="addCsv"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="schemaVisible" class="schema">
|
<div v-show="schemaVisible" class="schema">
|
||||||
<table-description
|
<table-description
|
||||||
@@ -18,14 +20,26 @@
|
|||||||
:columns="table.columns"
|
:columns="table.columns"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!--Parse csv dialog -->
|
||||||
|
<csv-import
|
||||||
|
ref="addCsv"
|
||||||
|
:file="file"
|
||||||
|
:db="$store.state.db"
|
||||||
|
dialog-name="addCsv"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import TableDescription from '@/components/TableDescription'
|
import fIo from '@/lib/utils/fileIo'
|
||||||
|
import TableDescription from './TableDescription'
|
||||||
import TextField from '@/components/TextField'
|
import TextField from '@/components/TextField'
|
||||||
import TreeChevron from '@/components/svg/treeChevron'
|
import TreeChevron from '@/components/svg/treeChevron'
|
||||||
import DbUploader from '@/components/DbUploader'
|
import DbUploader from '@/components/DbUploader'
|
||||||
|
import ExportIcon from '@/components/svg/export'
|
||||||
|
import AddTableIcon from '@/components/svg/addTable'
|
||||||
|
import CsvImport from '@/components/CsvImport'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Schema',
|
name: 'Schema',
|
||||||
@@ -33,28 +47,45 @@ export default {
|
|||||||
TableDescription,
|
TableDescription,
|
||||||
TextField,
|
TextField,
|
||||||
TreeChevron,
|
TreeChevron,
|
||||||
DbUploader
|
DbUploader,
|
||||||
|
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: {
|
||||||
|
exportToFile () {
|
||||||
|
this.$store.state.db.export(`${this.dbName}.sqlite`)
|
||||||
|
},
|
||||||
|
async addCsv () {
|
||||||
|
this.file = await fIo.getFileFromUser('.csv')
|
||||||
|
await this.$nextTick()
|
||||||
|
const csvImport = this.$refs.addCsv
|
||||||
|
csvImport.reset()
|
||||||
|
await csvImport.previewCsv()
|
||||||
|
csvImport.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +109,7 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-image: linear-gradient(white 73%, transparent);;
|
background-image: linear-gradient(white 73%, rgba(255, 255, 255, 0));
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
.schema, .db-name {
|
.schema, .db-name {
|
||||||
@@ -95,6 +126,11 @@ export default {
|
|||||||
|
|
||||||
.db-name {
|
.db-name {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin-right: 6px;
|
||||||
|
max-width: 150px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.db-name:hover .chevron-icon path,
|
.db-name:hover .chevron-icon path,
|
||||||
@@ -28,7 +28,7 @@ import plotly from 'plotly.js/dist/plotly'
|
|||||||
import 'react-chart-editor/lib/react-chart-editor.min.css'
|
import 'react-chart-editor/lib/react-chart-editor.min.css'
|
||||||
|
|
||||||
import PlotlyEditor from 'react-chart-editor'
|
import PlotlyEditor from 'react-chart-editor'
|
||||||
import chart from '@/chart'
|
import chartHelper from './chartHelper'
|
||||||
import dereference from 'react-chart-editor/lib/lib/dereference'
|
import dereference from 'react-chart-editor/lib/lib/dereference'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -49,10 +49,10 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
dataSources () {
|
dataSources () {
|
||||||
return chart.getDataSourcesFromSqlResult(this.sqlResult)
|
return chartHelper.getDataSourcesFromSqlResult(this.sqlResult)
|
||||||
},
|
},
|
||||||
dataSourceOptions () {
|
dataSourceOptions () {
|
||||||
return chart.getOptionsFromDataSources(this.dataSources)
|
return chartHelper.getOptionsFromDataSources(this.dataSources)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -71,7 +71,7 @@ export default {
|
|||||||
this.$emit('update')
|
this.$emit('update')
|
||||||
},
|
},
|
||||||
getChartStateForSave () {
|
getChartStateForSave () {
|
||||||
return chart.getChartStateForSave(this.state, this.dataSources)
|
return chartHelper.getChartStateForSave(this.state, this.dataSources)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@ import CM from 'codemirror'
|
|||||||
import 'codemirror/addon/hint/show-hint.js'
|
import 'codemirror/addon/hint/show-hint.js'
|
||||||
import 'codemirror/addon/hint/sql-hint.js'
|
import 'codemirror/addon/hint/sql-hint.js'
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
import { debounce } from 'debounce'
|
|
||||||
|
|
||||||
export function getHints (cm, options) {
|
export function getHints (cm, options) {
|
||||||
const token = cm.getTokenAt(cm.getCursor()).string.toUpperCase()
|
const token = cm.getTokenAt(cm.getCursor()).string.toUpperCase()
|
||||||
@@ -18,28 +17,34 @@ export function getHints (cm, options) {
|
|||||||
const hintOptions = {
|
const hintOptions = {
|
||||||
get tables () {
|
get tables () {
|
||||||
const tables = {}
|
const tables = {}
|
||||||
if (store.state.schema) {
|
if (store.state.db.schema) {
|
||||||
store.state.schema.forEach(table => {
|
store.state.db.schema.forEach(table => {
|
||||||
tables[table.name] = table.columns.map(column => column.name)
|
tables[table.name] = table.columns.map(column => column.name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return tables
|
return tables
|
||||||
},
|
},
|
||||||
|
get defaultTable () {
|
||||||
|
const schema = store.state.db.schema
|
||||||
|
return schema && schema.length === 1 ? schema[0].name : null
|
||||||
|
},
|
||||||
completeSingle: false,
|
completeSingle: false,
|
||||||
completeOnSingleClick: true,
|
completeOnSingleClick: true,
|
||||||
alignWithWord: false
|
alignWithWord: false
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export function showHintOnDemand (editor) {
|
||||||
show: debounce(function (editor) {
|
CM.showHint(editor, getHints, hintOptions)
|
||||||
// Don't show autocomplete after a space or semicolon or in string literals
|
}
|
||||||
const token = editor.getTokenAt(editor.getCursor())
|
|
||||||
const ch = token.string.slice(-1)
|
export default function showHint (editor) {
|
||||||
const tokenType = token.type
|
// Don't show autocomplete after a space or semicolon or in string literals
|
||||||
if (tokenType === 'string' || !ch || ch === ' ' || ch === ';') {
|
const token = editor.getTokenAt(editor.getCursor())
|
||||||
return
|
const ch = token.string.slice(-1)
|
||||||
}
|
const tokenType = token.type
|
||||||
|
if (tokenType === 'string' || !ch || ch === ' ' || ch === ';') {
|
||||||
CM.showHint(editor, getHints, hintOptions)
|
return
|
||||||
}, 400)
|
}
|
||||||
|
|
||||||
|
CM.showHint(editor, getHints, hintOptions)
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="codemirror-container">
|
<div class="codemirror-container">
|
||||||
<codemirror v-model="query" :options="cmOptions" @changes="onChange" />
|
<codemirror ref="cm" v-model="query" :options="cmOptions" @changes="onChange" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import hint from '@/hint'
|
import showHint, { showHintOnDemand } from './hint'
|
||||||
|
import time from '@/lib/utils/time'
|
||||||
import { codemirror } from 'vue-codemirror'
|
import { codemirror } from 'vue-codemirror'
|
||||||
import 'codemirror/lib/codemirror.css'
|
import 'codemirror/lib/codemirror.css'
|
||||||
import 'codemirror/mode/sql/sql.js'
|
import 'codemirror/mode/sql/sql.js'
|
||||||
@@ -27,8 +28,8 @@ export default {
|
|||||||
theme: 'neo',
|
theme: 'neo',
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
line: true,
|
line: true,
|
||||||
autofocus: true,
|
autoRefresh: true,
|
||||||
autoRefresh: true
|
extraKeys: { 'Ctrl-Space': showHintOnDemand }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -38,7 +39,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onChange: hint.show
|
onChange: time.debounce(showHint, 400),
|
||||||
|
focus () {
|
||||||
|
this.$refs.cm.codemirror.focus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
>
|
>
|
||||||
<template #left-pane>
|
<template #left-pane>
|
||||||
<div class="query-editor">
|
<div class="query-editor">
|
||||||
<sql-editor v-model="query" />
|
<sql-editor ref="sqlEditor" v-model="query" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #right-pane>
|
<template #right-pane>
|
||||||
@@ -21,7 +21,8 @@
|
|||||||
>
|
>
|
||||||
Run your query and get results here
|
Run your query and get results here
|
||||||
</div>
|
</div>
|
||||||
<div v-show="isGettingResults" class="table-preview result-in-progress">
|
<div v-if="isGettingResults" class="table-preview result-in-progress">
|
||||||
|
<loading-indicator :size="30"/>
|
||||||
Fetching results...
|
Fetching results...
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -30,10 +31,8 @@
|
|||||||
>
|
>
|
||||||
No rows retrieved according to your query
|
No rows retrieved according to your query
|
||||||
</div>
|
</div>
|
||||||
<div v-show="error" class="table-preview error">
|
<logs v-if="error" :messages="[error]"/>
|
||||||
{{ error }}
|
<sql-table v-if="result" :data-set="result" :time="time" :height="tableViewHeight" />
|
||||||
</div>
|
|
||||||
<sql-table v-if="result" :data-set="result" :height="tableViewHeight" />
|
|
||||||
</div>
|
</div>
|
||||||
<chart
|
<chart
|
||||||
:visible="view === 'chart'"
|
:visible="view === 'chart'"
|
||||||
@@ -50,10 +49,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import SqlTable from '@/components/SqlTable'
|
import SqlTable from '@/components/SqlTable'
|
||||||
import SqlEditor from '@/components/SqlEditor'
|
|
||||||
import Splitpanes from '@/components/Splitpanes'
|
import Splitpanes from '@/components/Splitpanes'
|
||||||
import ViewSwitcher from '@/components/ViewSwitcher'
|
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||||
import Chart from '@/components/Chart'
|
import SqlEditor from './SqlEditor'
|
||||||
|
import ViewSwitcher from './ViewSwitcher'
|
||||||
|
import Chart from './Chart'
|
||||||
|
import Logs from '@/components/Logs'
|
||||||
|
import time from '@/lib/utils/time'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Tab',
|
name: 'Tab',
|
||||||
@@ -63,7 +65,9 @@ export default {
|
|||||||
SqlTable,
|
SqlTable,
|
||||||
Splitpanes,
|
Splitpanes,
|
||||||
ViewSwitcher,
|
ViewSwitcher,
|
||||||
Chart
|
Chart,
|
||||||
|
LoadingIndicator,
|
||||||
|
Logs
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -73,7 +77,8 @@ export default {
|
|||||||
tableViewHeight: 0,
|
tableViewHeight: 0,
|
||||||
isGettingResults: false,
|
isGettingResults: false,
|
||||||
error: null,
|
error: null,
|
||||||
resizeObserver: null
|
resizeObserver: null,
|
||||||
|
time: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -81,9 +86,6 @@ export default {
|
|||||||
return this.id === this.$store.state.currentTabId
|
return this.id === this.$store.state.currentTabId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
|
||||||
this.$store.commit('setCurrentTab', this)
|
|
||||||
},
|
|
||||||
mounted () {
|
mounted () {
|
||||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||||
this.resizeObserver.observe(this.$refs.bottomPane)
|
this.resizeObserver.observe(this.$refs.bottomPane)
|
||||||
@@ -93,9 +95,14 @@ export default {
|
|||||||
this.resizeObserver.unobserve(this.$refs.bottomPane)
|
this.resizeObserver.unobserve(this.$refs.bottomPane)
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
isActive () {
|
isActive: {
|
||||||
if (this.isActive) {
|
immediate: true,
|
||||||
this.$store.commit('setCurrentTab', this)
|
async handler () {
|
||||||
|
if (this.isActive) {
|
||||||
|
this.$store.commit('setCurrentTab', this)
|
||||||
|
await this.$nextTick()
|
||||||
|
this.$refs.sqlEditor.focus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
query () {
|
query () {
|
||||||
@@ -108,11 +115,18 @@ export default {
|
|||||||
this.isGettingResults = true
|
this.isGettingResults = true
|
||||||
this.result = null
|
this.result = null
|
||||||
this.error = null
|
this.error = null
|
||||||
|
const state = this.$store.state
|
||||||
try {
|
try {
|
||||||
this.result = await this.$store.state.db.execute(this.query + ';')
|
const start = new Date()
|
||||||
|
this.result = await state.db.execute(this.query + ';')
|
||||||
|
this.time = time.getPeriod(start, new Date())
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.error = err
|
this.error = {
|
||||||
|
type: 'error',
|
||||||
|
message: err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
state.db.refreshSchema()
|
||||||
this.isGettingResults = false
|
this.isGettingResults = false
|
||||||
},
|
},
|
||||||
handleResize () {
|
handleResize () {
|
||||||
@@ -128,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,11 +194,32 @@ export default {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-preview.error {
|
.result-in-progress {
|
||||||
color: var(--color-text-error);
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
will-change: opacity;
|
||||||
|
/*
|
||||||
|
We need to show loader in 1 sec after starting query execution. We can't do that with
|
||||||
|
setTimeout because the main thread can be busy by getting a result set from the web worker.
|
||||||
|
But we can use CSS animation for opacity. Opacity triggers changes only in the Composite Layer
|
||||||
|
stage in rendering waterfall. Hence it can be processed only with Compositor Thread while
|
||||||
|
the Main Thread processes a result set.
|
||||||
|
https://www.viget.com/articles/animation-performance-101-browser-under-the-hood/
|
||||||
|
*/
|
||||||
|
animation: show-loader 1s linear 0s 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-preview.error::first-letter {
|
@keyframes show-loader {
|
||||||
text-transform: capitalize;
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
99% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Tab from '@/components/Tab'
|
import Tab from './Tab'
|
||||||
import CloseIcon from '@/components/svg/close'
|
import CloseIcon from '@/components/svg/close'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
62
src/views/Main/Editor/index.vue
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<splitpanes
|
||||||
|
class="schema-tabs-splitter"
|
||||||
|
:before="{ size: 20, max: 30 }"
|
||||||
|
:after="{ size: 80, max: 100 }"
|
||||||
|
>
|
||||||
|
<template #left-pane>
|
||||||
|
<schema/>
|
||||||
|
</template>
|
||||||
|
<template #right-pane>
|
||||||
|
<tabs />
|
||||||
|
</template>
|
||||||
|
</splitpanes>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Splitpanes from '@/components/Splitpanes'
|
||||||
|
import Schema from './Schema'
|
||||||
|
import Tabs from './Tabs'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Editor',
|
||||||
|
components: {
|
||||||
|
Schema,
|
||||||
|
Splitpanes,
|
||||||
|
Tabs
|
||||||
|
},
|
||||||
|
async beforeCreate () {
|
||||||
|
const schema = this.$store.state.db.schema
|
||||||
|
if (!schema || schema.length === 0) {
|
||||||
|
const stmt = [
|
||||||
|
'/*',
|
||||||
|
' * Your database is empty. In order to start building charts',
|
||||||
|
' * you should create a table and insert data into it.',
|
||||||
|
' */',
|
||||||
|
'CREATE TABLE house',
|
||||||
|
'(',
|
||||||
|
' name TEXT,',
|
||||||
|
' points INTEGER',
|
||||||
|
');',
|
||||||
|
'INSERT INTO house VALUES',
|
||||||
|
"('Gryffindor', 100),",
|
||||||
|
"('Hufflepuff', 90),",
|
||||||
|
"('Ravenclaw', 95),",
|
||||||
|
"('Slytherin', 80);"
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
||||||
|
this.$store.commit('setCurrentTabId', tabId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.schema-tabs-splitter {
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--color-white);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,8 +3,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<router-link to="/editor">Editor</router-link>
|
<router-link to="/editor">Editor</router-link>
|
||||||
<router-link to="/my-queries">My queries</router-link>
|
<router-link to="/my-queries">My queries</router-link>
|
||||||
|
<a href="https://github.com/lana-k/sqliteviz/wiki" target="_blank">Help</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div id="nav-buttons">
|
||||||
<button
|
<button
|
||||||
id="run-btn"
|
id="run-btn"
|
||||||
v-if="currentQuery && $route.path === '/editor'"
|
v-if="currentQuery && $route.path === '/editor'"
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
</button>
|
</button>
|
||||||
|
<app-diagnostic-info />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--Save Query dialog -->
|
<!--Save Query dialog -->
|
||||||
@@ -62,13 +64,15 @@
|
|||||||
<script>
|
<script>
|
||||||
import TextField from '@/components/TextField'
|
import TextField from '@/components/TextField'
|
||||||
import CloseIcon from '@/components/svg/close'
|
import CloseIcon from '@/components/svg/close'
|
||||||
import storedQueries from '@/storedQueries'
|
import storedQueries from '@/lib/storedQueries'
|
||||||
|
import AppDiagnosticInfo from './AppDiagnosticInfo'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MainMenu',
|
name: 'MainMenu',
|
||||||
components: {
|
components: {
|
||||||
TextField,
|
TextField,
|
||||||
CloseIcon
|
CloseIcon,
|
||||||
|
AppDiagnosticInfo
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -96,7 +100,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
runDisabled () {
|
runDisabled () {
|
||||||
return this.currentQuery && (!this.$store.state.schema || !this.currentQuery.query)
|
return this.currentQuery && (!this.$store.state.db || !this.currentQuery.query)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
@@ -212,7 +216,7 @@ nav {
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
padding: 0 52px;
|
padding: 0 16px 0 52px;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
@@ -237,4 +241,8 @@ button {
|
|||||||
#save-note img {
|
#save-note img {
|
||||||
margin: -3px 6px 0 0;
|
margin: -3px 6px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#nav-buttons {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -81,7 +81,10 @@
|
|||||||
<div class="icons-container">
|
<div class="icons-container">
|
||||||
<rename-icon v-if="!query.isPredefined" @click="showRenameDialog(query.id)" />
|
<rename-icon v-if="!query.isPredefined" @click="showRenameDialog(query.id)" />
|
||||||
<copy-icon @click="duplicateQuery(index)"/>
|
<copy-icon @click="duplicateQuery(index)"/>
|
||||||
<export-icon @click="exportToFile([query], `${query.name}.json`)"/>
|
<export-icon
|
||||||
|
@click="exportToFile([query], `${query.name}.json`)"
|
||||||
|
tooltip="Export query to file"
|
||||||
|
/>
|
||||||
<delete-icon
|
<delete-icon
|
||||||
v-if="!query.isPredefined"
|
v-if="!query.isPredefined"
|
||||||
@click="showDeleteDialog((new Set()).add(query.id))"
|
@click="showDeleteDialog((new Set()).add(query.id))"
|
||||||
@@ -138,16 +141,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import RenameIcon from '@/components/svg/rename'
|
import RenameIcon from './svg/rename'
|
||||||
import CopyIcon from '@/components/svg/copy'
|
import CopyIcon from './svg/copy'
|
||||||
import ExportIcon from '@/components/svg/export'
|
import ExportIcon from '@/components/svg/export'
|
||||||
import DeleteIcon from '@/components/svg/delete'
|
import DeleteIcon from './svg/delete'
|
||||||
import CloseIcon from '@/components/svg/close'
|
import CloseIcon from '@/components/svg/close'
|
||||||
import TextField from '@/components/TextField'
|
import TextField from '@/components/TextField'
|
||||||
import CheckBox from '@/components/CheckBox'
|
import CheckBox from '@/components/CheckBox'
|
||||||
import tooltipMixin from '@/mixins/tooltips'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
import storedQueries from '@/storedQueries'
|
import storedQueries from '@/lib/storedQueries'
|
||||||
import fu from '@/file.utils'
|
import fu from '@/lib/utils/fileIo'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MyQueries',
|
name: 'MyQueries',
|
||||||
@@ -445,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%;
|
||||||
@@ -520,7 +524,7 @@ tbody tr:hover td {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
tbody tr:hover .icons-container {
|
tbody tr:hover .icons-container {
|
||||||
display: block;
|
display: flex;
|
||||||
}
|
}
|
||||||
.dialog input {
|
.dialog input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import tooltipMixin from '@/mixins/tooltips'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CopyIcon',
|
name: 'CopyIcon',
|
||||||
@@ -33,7 +33,7 @@ export default {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.icon {
|
.icon {
|
||||||
vertical-align: middle;
|
display: block;
|
||||||
margin: 0 12px;
|
margin: 0 12px;
|
||||||
}
|
}
|
||||||
.icon:hover path {
|
.icon:hover path {
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import tooltipMixin from '@/mixins/tooltips'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DeleteIcon',
|
name: 'DeleteIcon',
|
||||||
@@ -33,7 +33,7 @@ export default {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.icon {
|
.icon {
|
||||||
vertical-align: middle;
|
display: block;
|
||||||
margin: 0 12px;
|
margin: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import tooltipMixin from '@/mixins/tooltips'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RenameIcon',
|
name: 'RenameIcon',
|
||||||
@@ -33,7 +33,7 @@ export default {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.icon {
|
.icon {
|
||||||
vertical-align: middle;
|
display: block;
|
||||||
margin: 0 12px;
|
margin: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8,11 +8,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MainMenu from '@/components/MainMenu'
|
import MainMenu from './MainMenu'
|
||||||
import '@/assets/styles/scrollbars.css'
|
import '@/assets/styles/scrollbars.css'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MainView',
|
name: 'Main',
|
||||||
components: { MainMenu }
|
components: { MainMenu }
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
<div id="note">
|
<div id="note">
|
||||||
Sqliteviz is fully client-side. Your database never leaves your computer.
|
Sqliteviz is fully client-side. Your database never leaves your computer.
|
||||||
</div>
|
</div>
|
||||||
<button id ="skip" class="secondary" @click="$router.push('/editor')">
|
<button id="skip" class="secondary" @click="$router.push('/editor')">
|
||||||
Skip database loading
|
Create empty database
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
import DbUploader from '@/components/DbUploader'
|
import DbUploader from '@/components/DbUploader'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Home',
|
name: 'Welcome',
|
||||||
components: { DbUploader }
|
components: { DbUploader }
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
706
tests/components/CsvImport/CsvImport.spec.js
Normal file
@@ -0,0 +1,706 @@
|
|||||||
|
import { expect } from 'chai'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import CsvImport from '@/components/CsvImport'
|
||||||
|
import csv from '@/components/CsvImport/csv'
|
||||||
|
|
||||||
|
describe('CsvImport.vue', () => {
|
||||||
|
let state = {}
|
||||||
|
let actions = {}
|
||||||
|
let mutations = {}
|
||||||
|
let store = {}
|
||||||
|
let clock
|
||||||
|
let wrapper
|
||||||
|
const newTabId = 1
|
||||||
|
const file = { name: 'my data.csv' }
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
clock = sinon.useFakeTimers()
|
||||||
|
|
||||||
|
// mock store state and mutations
|
||||||
|
state = {}
|
||||||
|
mutations = {
|
||||||
|
setDb: sinon.stub(),
|
||||||
|
setCurrentTabId: sinon.stub()
|
||||||
|
}
|
||||||
|
actions = {
|
||||||
|
addTab: sinon.stub().resolves(newTabId)
|
||||||
|
}
|
||||||
|
store = new Vuex.Store({ state, mutations, actions })
|
||||||
|
|
||||||
|
const db = {
|
||||||
|
sanitizeTableName: sinon.stub().returns('my_data'),
|
||||||
|
addTableFromCsv: sinon.stub().resolves(),
|
||||||
|
createProgressCounter: sinon.stub().returns(1),
|
||||||
|
deleteProgressCounter: sinon.stub(),
|
||||||
|
validateTableName: sinon.stub().resolves(),
|
||||||
|
execute: sinon.stub().resolves(),
|
||||||
|
refreshSchema: sinon.stub().resolves()
|
||||||
|
}
|
||||||
|
|
||||||
|
// mount the component
|
||||||
|
wrapper = mount(CsvImport, {
|
||||||
|
store,
|
||||||
|
propsData: {
|
||||||
|
file,
|
||||||
|
dialogName: 'addCsv',
|
||||||
|
db
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sinon.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('previews', async () => {
|
||||||
|
sinon.stub(csv, 'parse').resolves({
|
||||||
|
delimiter: '|',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[1, 'foo'],
|
||||||
|
[2, 'bar']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
messages: [{
|
||||||
|
code: 'UndetectableDelimiter',
|
||||||
|
message: 'Comma was used as a standart delimiter',
|
||||||
|
row: 0,
|
||||||
|
type: 'info',
|
||||||
|
hint: undefined
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
wrapper.vm.previewCsv()
|
||||||
|
await wrapper.vm.open()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(true)
|
||||||
|
expect(wrapper.find('#csv-table-name input').element.value).to.equal('my_data')
|
||||||
|
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.value).to.equal('|')
|
||||||
|
expect(wrapper.find('#quote-char input').element.value).to.equal('"')
|
||||||
|
expect(wrapper.find('#escape-char input').element.value).to.equal('"')
|
||||||
|
expect(wrapper.findComponent({ name: 'check-box' }).vm.checked).to.equal(true)
|
||||||
|
const rows = wrapper.findAll('tbody tr')
|
||||||
|
expect(rows).to.have.lengthOf(2)
|
||||||
|
expect(rows.at(0).findAll('td').at(0).text()).to.equal('1')
|
||||||
|
expect(rows.at(0).findAll('td').at(1).text()).to.equal('foo')
|
||||||
|
expect(rows.at(1).findAll('td').at(0).text()).to.equal('2')
|
||||||
|
expect(rows.at(1).findAll('td').at(1).text()).to.equal('bar')
|
||||||
|
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||||
|
.to.include('Information about row 0. Comma was used as a standart delimiter.')
|
||||||
|
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||||
|
.to.include('Preview parsing is completed in')
|
||||||
|
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||||
|
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reparses when parameters changes', async () => {
|
||||||
|
const parse = sinon.stub(csv, 'parse')
|
||||||
|
parse.onCall(0).resolves({
|
||||||
|
delimiter: '|',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[1, 'foo']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
wrapper.vm.previewCsv()
|
||||||
|
wrapper.vm.open()
|
||||||
|
await csv.parse.returnValues[0]
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
parse.onCall(1).resolves({
|
||||||
|
delimiter: ',',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[2, 'bar']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasErrors: false
|
||||||
|
})
|
||||||
|
await wrapper.find('.delimiter-selector-container input').setValue(',')
|
||||||
|
expect(parse.callCount).to.equal(2)
|
||||||
|
await csv.parse.returnValues[1]
|
||||||
|
|
||||||
|
let rows = wrapper.findAll('tbody tr')
|
||||||
|
expect(rows).to.have.lengthOf(1)
|
||||||
|
expect(rows.at(0).findAll('td').at(0).text()).to.equal('2')
|
||||||
|
expect(rows.at(0).findAll('td').at(1).text()).to.equal('bar')
|
||||||
|
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||||
|
.to.include('Preview parsing is completed in')
|
||||||
|
|
||||||
|
parse.onCall(2).resolves({
|
||||||
|
delimiter: ',',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[3, 'baz']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasErrors: true,
|
||||||
|
messages: [{
|
||||||
|
code: 'MissingQuotes',
|
||||||
|
message: 'Quote is missed',
|
||||||
|
row: 0,
|
||||||
|
type: 'error',
|
||||||
|
hint: 'Edit your CSV so that the field has a closing quote char.'
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.find('#quote-char input').setValue("'")
|
||||||
|
expect(parse.callCount).to.equal(3)
|
||||||
|
await csv.parse.returnValues[2]
|
||||||
|
rows = wrapper.findAll('tbody tr')
|
||||||
|
expect(rows).to.have.lengthOf(1)
|
||||||
|
expect(rows.at(0).findAll('td').at(0).text()).to.equal('3')
|
||||||
|
expect(rows.at(0).findAll('td').at(1).text()).to.equal('baz')
|
||||||
|
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||||
|
.to.contain('Error in row 0. Quote is missed. Edit your CSV so that the field has a closing quote char.')
|
||||||
|
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||||
|
.to.not.contain('Preview parsing is completed in')
|
||||||
|
|
||||||
|
parse.onCall(3).resolves({
|
||||||
|
delimiter: ',',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[4, 'qux']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasErrors: false
|
||||||
|
})
|
||||||
|
await wrapper.find('#escape-char input').setValue("'")
|
||||||
|
expect(parse.callCount).to.equal(4)
|
||||||
|
await csv.parse.returnValues[3]
|
||||||
|
rows = wrapper.findAll('tbody tr')
|
||||||
|
expect(rows).to.have.lengthOf(1)
|
||||||
|
expect(rows.at(0).findAll('td').at(0).text()).to.equal('4')
|
||||||
|
expect(rows.at(0).findAll('td').at(1).text()).to.equal('qux')
|
||||||
|
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||||
|
.to.contain('Preview parsing is completed in')
|
||||||
|
|
||||||
|
parse.onCall(4).resolves({
|
||||||
|
delimiter: ',',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[5, 'corge']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasErrors: false
|
||||||
|
})
|
||||||
|
await wrapper.findComponent({ name: 'check-box' }).trigger('click')
|
||||||
|
expect(parse.callCount).to.equal(5)
|
||||||
|
await csv.parse.returnValues[4]
|
||||||
|
rows = wrapper.findAll('tbody tr')
|
||||||
|
expect(rows).to.have.lengthOf(1)
|
||||||
|
expect(rows.at(0).findAll('td').at(0).text()).to.equal('5')
|
||||||
|
expect(rows.at(0).findAll('td').at(1).text()).to.equal('corge')
|
||||||
|
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||||
|
.to.include('Preview parsing is completed in')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has proper state before parsing is complete', async () => {
|
||||||
|
const parse = sinon.stub(csv, 'parse')
|
||||||
|
parse.onCall(0).resolves({
|
||||||
|
delimiter: '|',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[1, 'foo']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
wrapper.vm.previewCsv()
|
||||||
|
wrapper.vm.open()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
let resolveParsing
|
||||||
|
parse.onCall(1).returns(new Promise(resolve => {
|
||||||
|
resolveParsing = resolve
|
||||||
|
}))
|
||||||
|
|
||||||
|
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||||
|
await wrapper.find('#csv-import').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
// "Parsing CSV..." in the logs
|
||||||
|
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text())
|
||||||
|
.to.equal('Parsing CSV...')
|
||||||
|
|
||||||
|
// After 1 second - loading indicator is shown
|
||||||
|
await clock.tick(1000)
|
||||||
|
expect(
|
||||||
|
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||||
|
).to.equal(true)
|
||||||
|
|
||||||
|
// All the dialog controls are disabled
|
||||||
|
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||||
|
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||||
|
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||||
|
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||||
|
await resolveParsing()
|
||||||
|
await parse.returnValues[1]
|
||||||
|
|
||||||
|
// Loading indicator is not shown when parsing is compete
|
||||||
|
expect(
|
||||||
|
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||||
|
).to.equal(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parsing is completed successfully', async () => {
|
||||||
|
const parse = sinon.stub(csv, 'parse')
|
||||||
|
parse.onCall(0).resolves({
|
||||||
|
delimiter: '|',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[1, 'foo']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasErrors: false,
|
||||||
|
messages: []
|
||||||
|
})
|
||||||
|
|
||||||
|
parse.onCall(1).resolves({
|
||||||
|
delimiter: '|',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[1, 'foo'],
|
||||||
|
[2, 'bar']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasErrors: false,
|
||||||
|
messages: []
|
||||||
|
})
|
||||||
|
|
||||||
|
wrapper.vm.previewCsv()
|
||||||
|
wrapper.vm.open()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
let resolveImport
|
||||||
|
wrapper.vm.db.addTableFromCsv.onCall(0).returns(new Promise(resolve => {
|
||||||
|
resolveImport = resolve
|
||||||
|
}))
|
||||||
|
|
||||||
|
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||||
|
await wrapper.find('#csv-import').trigger('click')
|
||||||
|
await csv.parse.returnValues[1]
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
// Parsing success in the logs
|
||||||
|
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text())
|
||||||
|
.to.include('2 rows are parsed successfully in')
|
||||||
|
|
||||||
|
// All the dialog controls are disabled
|
||||||
|
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||||
|
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||||
|
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||||
|
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||||
|
await resolveImport()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parsing is completed with notes', async () => {
|
||||||
|
const parse = sinon.stub(csv, 'parse')
|
||||||
|
parse.onCall(0).resolves({
|
||||||
|
delimiter: '|',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[1, 'foo']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasErrors: false,
|
||||||
|
messages: []
|
||||||
|
})
|
||||||
|
|
||||||
|
parse.onCall(1).resolves({
|
||||||
|
delimiter: '|',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[1, 'foo'],
|
||||||
|
[2, 'bar']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasErrors: false,
|
||||||
|
messages: [{
|
||||||
|
code: 'UndetectableDelimiter',
|
||||||
|
message: 'Comma was used as a standart delimiter',
|
||||||
|
type: 'info',
|
||||||
|
hint: undefined
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
let resolveImport
|
||||||
|
wrapper.vm.db.addTableFromCsv.onCall(0).returns(new Promise(resolve => {
|
||||||
|
resolveImport = resolve
|
||||||
|
}))
|
||||||
|
|
||||||
|
wrapper.vm.previewCsv()
|
||||||
|
wrapper.vm.open()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||||
|
await wrapper.find('#csv-import').trigger('click')
|
||||||
|
await csv.parse.returnValues[1]
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
// Parsing success in the logs
|
||||||
|
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
||||||
|
expect(logs).to.have.lengthOf(4)
|
||||||
|
expect(logs.at(1).text()).to.include('2 rows are parsed in')
|
||||||
|
expect(logs.at(2).text()).to.equals('Comma was used as a standart delimiter.')
|
||||||
|
|
||||||
|
// All the dialog controls are disabled
|
||||||
|
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||||
|
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||||
|
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||||
|
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||||
|
await resolveImport()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parsing is completed with errors', async () => {
|
||||||
|
const parse = sinon.stub(csv, 'parse')
|
||||||
|
parse.onCall(0).resolves({
|
||||||
|
delimiter: '|',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[1, 'foo']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasErrors: false,
|
||||||
|
messages: []
|
||||||
|
})
|
||||||
|
|
||||||
|
parse.onCall(1).resolves({
|
||||||
|
delimiter: '|',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[1, 'foo'],
|
||||||
|
[2, 'bar']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasErrors: true,
|
||||||
|
messages: [{
|
||||||
|
code: 'Error',
|
||||||
|
message: 'Something is wrong',
|
||||||
|
type: 'error',
|
||||||
|
hint: undefined
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
wrapper.vm.previewCsv()
|
||||||
|
wrapper.vm.open()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||||
|
await wrapper.find('#csv-import').trigger('click')
|
||||||
|
await csv.parse.returnValues[1]
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
// Parsing success in the logs
|
||||||
|
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
||||||
|
expect(logs).to.have.lengthOf(3)
|
||||||
|
expect(logs.at(1).text()).to.include('Parsing ended with errors.')
|
||||||
|
expect(logs.at(2).text()).to.equals('Something is wrong.')
|
||||||
|
|
||||||
|
// All the dialog controls are enabled
|
||||||
|
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(false)
|
||||||
|
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
|
||||||
|
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
|
||||||
|
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
||||||
|
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
||||||
|
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
|
||||||
|
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
||||||
|
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||||
|
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has proper state before import is completed', async () => {
|
||||||
|
const parse = sinon.stub(csv, 'parse')
|
||||||
|
parse.onCall(0).resolves({
|
||||||
|
delimiter: '|',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[1, 'foo']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasErrors: false,
|
||||||
|
messages: []
|
||||||
|
})
|
||||||
|
|
||||||
|
parse.onCall(1).resolves({
|
||||||
|
delimiter: '|',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[1, 'foo'],
|
||||||
|
[2, 'bar']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasErrors: false,
|
||||||
|
messages: []
|
||||||
|
})
|
||||||
|
|
||||||
|
let resolveImport = sinon.stub()
|
||||||
|
wrapper.vm.db.addTableFromCsv = sinon.stub()
|
||||||
|
.resolves(new Promise(resolve => { resolveImport = resolve }))
|
||||||
|
|
||||||
|
wrapper.vm.previewCsv()
|
||||||
|
wrapper.vm.open()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||||
|
await wrapper.find('#csv-import').trigger('click')
|
||||||
|
await csv.parse.returnValues[1]
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
// Parsing success in the logs
|
||||||
|
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(2).text())
|
||||||
|
.to.equal('Importing CSV into a SQLite database...')
|
||||||
|
|
||||||
|
// After 1 second - loading indicator is shown
|
||||||
|
await clock.tick(1000)
|
||||||
|
expect(
|
||||||
|
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||||
|
).to.equal(true)
|
||||||
|
|
||||||
|
// All the dialog controls are disabled
|
||||||
|
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||||
|
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||||
|
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||||
|
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||||
|
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||||
|
expect(wrapper.vm.db.addTableFromCsv.getCall(0).args[0]).to.equal('foo') // table name
|
||||||
|
|
||||||
|
// After resolving - loading indicator is not shown
|
||||||
|
await resolveImport()
|
||||||
|
await wrapper.vm.db.addTableFromCsv.returnValues[0]
|
||||||
|
expect(
|
||||||
|
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||||
|
).to.equal(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('import success', async () => {
|
||||||
|
const parse = sinon.stub(csv, 'parse')
|
||||||
|
parse.onCall(0).resolves({
|
||||||
|
delimiter: '|',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[1, 'foo']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasErrors: false,
|
||||||
|
messages: []
|
||||||
|
})
|
||||||
|
// we need to separate calles because messages will mutate
|
||||||
|
parse.onCall(1).resolves({
|
||||||
|
delimiter: '|',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[1, 'foo'],
|
||||||
|
[2, 'bar']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasErrors: false,
|
||||||
|
messages: []
|
||||||
|
})
|
||||||
|
|
||||||
|
wrapper.vm.previewCsv()
|
||||||
|
wrapper.vm.open()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||||
|
await wrapper.find('#csv-import').trigger('click')
|
||||||
|
await csv.parse.returnValues[1]
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
// Import success in the logs
|
||||||
|
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
||||||
|
expect(logs).to.have.lengthOf(3)
|
||||||
|
expect(logs.at(2).text()).to.contain('Importing CSV into a SQLite database is completed in')
|
||||||
|
|
||||||
|
// All the dialog controls are enabled
|
||||||
|
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(false)
|
||||||
|
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
|
||||||
|
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
|
||||||
|
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
||||||
|
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
||||||
|
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
|
||||||
|
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
||||||
|
expect(wrapper.find('#csv-finish').isVisible()).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('import fails', async () => {
|
||||||
|
const parse = sinon.stub(csv, 'parse')
|
||||||
|
parse.onCall(0).resolves({
|
||||||
|
delimiter: '|',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[1, 'foo']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasErrors: false,
|
||||||
|
messages: []
|
||||||
|
})
|
||||||
|
// we need to separate calles because messages will mutate
|
||||||
|
parse.onCall(1).resolves({
|
||||||
|
delimiter: '|',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[1, 'foo'],
|
||||||
|
[2, 'bar']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasErrors: false,
|
||||||
|
messages: []
|
||||||
|
})
|
||||||
|
|
||||||
|
wrapper.vm.db.addTableFromCsv = sinon.stub().rejects(new Error('fail'))
|
||||||
|
|
||||||
|
wrapper.vm.previewCsv()
|
||||||
|
wrapper.vm.open()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||||
|
await wrapper.find('#csv-import').trigger('click')
|
||||||
|
await csv.parse.returnValues[1]
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
// Import success in the logs
|
||||||
|
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
||||||
|
expect(logs).to.have.lengthOf(4)
|
||||||
|
expect(logs.at(2).text()).to.contain('Importing CSV into a SQLite database...')
|
||||||
|
expect(logs.at(3).text()).to.equal('Error: fail.')
|
||||||
|
|
||||||
|
// All the dialog controls are enabled
|
||||||
|
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(false)
|
||||||
|
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
|
||||||
|
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
|
||||||
|
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
||||||
|
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
||||||
|
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
|
||||||
|
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
||||||
|
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('import finish', async () => {
|
||||||
|
sinon.stub(csv, 'parse').resolves({
|
||||||
|
delimiter: '|',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[1, 'foo']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasErrors: false,
|
||||||
|
messages: []
|
||||||
|
})
|
||||||
|
|
||||||
|
wrapper.vm.previewCsv()
|
||||||
|
wrapper.vm.open()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
await wrapper.find('#csv-import').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
await wrapper.find('#csv-finish').trigger('click')
|
||||||
|
|
||||||
|
expect(actions.addTab.calledOnce).to.equal(true)
|
||||||
|
await actions.addTab.returnValues[0]
|
||||||
|
expect(mutations.setCurrentTabId.calledOnceWith(state, newTabId)).to.equal(true)
|
||||||
|
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
|
||||||
|
expect(wrapper.emitted('finish')).to.have.lengthOf(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('import cancel', async () => {
|
||||||
|
sinon.stub(csv, 'parse').resolves({
|
||||||
|
delimiter: '|',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[1, 'foo']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasErrors: false,
|
||||||
|
messages: []
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.previewCsv()
|
||||||
|
await wrapper.vm.open()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
await wrapper.find('#csv-import').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
await wrapper.find('#csv-cancel').trigger('click')
|
||||||
|
|
||||||
|
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
|
||||||
|
expect(wrapper.vm.db.execute.calledOnceWith('DROP TABLE "my_data"')).to.equal(true)
|
||||||
|
expect(wrapper.vm.db.refreshSchema.calledOnce).to.equal(true)
|
||||||
|
expect(wrapper.emitted('cancel')).to.have.lengthOf(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('checks table name', async () => {
|
||||||
|
sinon.stub(csv, 'parse').resolves()
|
||||||
|
await wrapper.vm.previewCsv()
|
||||||
|
await wrapper.vm.open()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||||
|
await clock.tick(400)
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.find('#csv-table-name .text-field-error').text()).to.equal('')
|
||||||
|
|
||||||
|
wrapper.vm.db.validateTableName = sinon.stub().rejects(new Error('this is a bad table name'))
|
||||||
|
await wrapper.find('#csv-table-name input').setValue('bar')
|
||||||
|
await clock.tick(400)
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.find('#csv-table-name .text-field-error').text())
|
||||||
|
.to.equal('this is a bad table name. Try another table name.')
|
||||||
|
|
||||||
|
await wrapper.find('#csv-table-name input').setValue('')
|
||||||
|
await clock.tick(400)
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.find('#csv-table-name .text-field-error').text()).to.equal('')
|
||||||
|
|
||||||
|
await wrapper.find('#csv-import').trigger('click')
|
||||||
|
expect(wrapper.find('#csv-table-name .text-field-error').text())
|
||||||
|
.to.equal("Table name can't be empty")
|
||||||
|
expect(wrapper.vm.db.addTableFromCsv.called).to.equal(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { mount, shallowMount } from '@vue/test-utils'
|
import { mount, shallowMount } from '@vue/test-utils'
|
||||||
import DelimiterSelector from '@/components/DelimiterSelector'
|
import DelimiterSelector from '@/components/CsvImport/DelimiterSelector'
|
||||||
|
|
||||||
describe('DelimiterSelector', async () => {
|
describe('DelimiterSelector', async () => {
|
||||||
it('shows the name of value', async () => {
|
it('shows the name of value', async () => {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import csv from '@/csv'
|
import csv from '@/components/CsvImport/csv'
|
||||||
import Papa from 'papaparse'
|
import Papa from 'papaparse'
|
||||||
|
|
||||||
describe('csv.js', () => {
|
describe('csv.js', () => {
|
||||||
@@ -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']
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -2,38 +2,41 @@ import { expect } from 'chai'
|
|||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
import { shallowMount, mount } from '@vue/test-utils'
|
import { shallowMount, mount } from '@vue/test-utils'
|
||||||
import DbUploader from '@/components/DbUploader.vue'
|
import DbUploader from '@/components/DbUploader'
|
||||||
import fu from '@/file.utils'
|
import fu from '@/lib/utils/fileIo'
|
||||||
import database from '@/database'
|
import database from '@/lib/database'
|
||||||
import csv from '@/csv'
|
|
||||||
|
|
||||||
describe('DbUploader.vue', () => {
|
describe('DbUploader.vue', () => {
|
||||||
let state = {}
|
let state = {}
|
||||||
let mutations = {}
|
let mutations = {}
|
||||||
let store = {}
|
let store = {}
|
||||||
|
let place
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// mock store state and mutations
|
// mock store state and mutations
|
||||||
state = {}
|
state = {}
|
||||||
mutations = {
|
mutations = {
|
||||||
saveSchema: sinon.stub()
|
setDb: sinon.stub()
|
||||||
}
|
}
|
||||||
store = new Vuex.Store({ state, mutations })
|
store = new Vuex.Store({ state, mutations })
|
||||||
|
|
||||||
|
place = document.createElement('div')
|
||||||
|
document.body.appendChild(place)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
sinon.restore()
|
sinon.restore()
|
||||||
|
place.remove()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('loads db on click and redirects to /editor', async () => {
|
it('loads db on click and redirects to /editor', async () => {
|
||||||
// mock getting a file from user
|
// mock getting a file from user
|
||||||
const file = {}
|
const file = { name: 'test.db' }
|
||||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||||
|
|
||||||
// mock db loading
|
// mock db loading
|
||||||
const schema = {}
|
|
||||||
const db = {
|
const db = {
|
||||||
loadDb: sinon.stub().resolves(schema)
|
loadDb: sinon.stub().resolves()
|
||||||
}
|
}
|
||||||
sinon.stub(database, 'getNewDatabase').returns(db)
|
sinon.stub(database, 'getNewDatabase').returns(db)
|
||||||
|
|
||||||
@@ -43,22 +46,27 @@ describe('DbUploader.vue', () => {
|
|||||||
|
|
||||||
// mount the component
|
// mount the component
|
||||||
const wrapper = shallowMount(DbUploader, {
|
const wrapper = shallowMount(DbUploader, {
|
||||||
|
attachTo: place,
|
||||||
store,
|
store,
|
||||||
mocks: { $router, $route }
|
mocks: { $router, $route },
|
||||||
|
propsData: {
|
||||||
|
type: 'illustrated'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await wrapper.find('.drop-area').trigger('click')
|
await wrapper.find('.drop-area').trigger('click')
|
||||||
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
|
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
|
||||||
await db.loadDb.returnValues[0]
|
await db.loadDb.returnValues[0]
|
||||||
expect(mutations.saveSchema.calledOnceWith(state, schema)).to.equal(true)
|
await wrapper.vm.animationPromise
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
expect($router.push.calledOnceWith('/editor')).to.equal(true)
|
expect($router.push.calledOnceWith('/editor')).to.equal(true)
|
||||||
|
wrapper.destroy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('loads db on drop and redirects to /editor', async () => {
|
it('loads db on drop and redirects to /editor', async () => {
|
||||||
// mock db loading
|
// mock db loading
|
||||||
const schema = {}
|
|
||||||
const db = {
|
const db = {
|
||||||
loadDb: sinon.stub().resolves(schema)
|
loadDb: sinon.stub().resolves()
|
||||||
}
|
}
|
||||||
sinon.stub(database, 'getNewDatabase').returns(db)
|
sinon.stub(database, 'getNewDatabase').returns(db)
|
||||||
|
|
||||||
@@ -68,12 +76,16 @@ describe('DbUploader.vue', () => {
|
|||||||
|
|
||||||
// mount the component
|
// mount the component
|
||||||
const wrapper = shallowMount(DbUploader, {
|
const wrapper = shallowMount(DbUploader, {
|
||||||
|
attachTo: place,
|
||||||
store,
|
store,
|
||||||
mocks: { $router, $route }
|
mocks: { $router, $route },
|
||||||
|
propsData: {
|
||||||
|
type: 'illustrated'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// mock a file dropped by a user
|
// mock a file dropped by a user
|
||||||
const file = {}
|
const file = { name: 'test.db' }
|
||||||
const dropData = { dataTransfer: new DataTransfer() }
|
const dropData = { dataTransfer: new DataTransfer() }
|
||||||
Object.defineProperty(dropData.dataTransfer, 'files', {
|
Object.defineProperty(dropData.dataTransfer, 'files', {
|
||||||
value: [file],
|
value: [file],
|
||||||
@@ -83,19 +95,20 @@ describe('DbUploader.vue', () => {
|
|||||||
await wrapper.find('.drop-area').trigger('drop', dropData)
|
await wrapper.find('.drop-area').trigger('drop', dropData)
|
||||||
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
|
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
|
||||||
await db.loadDb.returnValues[0]
|
await db.loadDb.returnValues[0]
|
||||||
expect(mutations.saveSchema.calledOnceWith(state, schema)).to.equal(true)
|
await wrapper.vm.animationPromise
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
expect($router.push.calledOnceWith('/editor')).to.equal(true)
|
expect($router.push.calledOnceWith('/editor')).to.equal(true)
|
||||||
|
wrapper.destroy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("doesn't redirect if already on /editor", async () => {
|
it("doesn't redirect if already on /editor", async () => {
|
||||||
// mock getting a file from user
|
// mock getting a file from user
|
||||||
const file = {}
|
const file = { name: 'test.db' }
|
||||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||||
|
|
||||||
// mock db loading
|
// mock db loading
|
||||||
const schema = {}
|
|
||||||
const db = {
|
const db = {
|
||||||
loadDb: sinon.stub().resolves(schema)
|
loadDb: sinon.stub().resolves()
|
||||||
}
|
}
|
||||||
sinon.stub(database, 'getNewDatabase').returns(db)
|
sinon.stub(database, 'getNewDatabase').returns(db)
|
||||||
|
|
||||||
@@ -105,732 +118,82 @@ describe('DbUploader.vue', () => {
|
|||||||
|
|
||||||
// mount the component
|
// mount the component
|
||||||
const wrapper = shallowMount(DbUploader, {
|
const wrapper = shallowMount(DbUploader, {
|
||||||
|
attachTo: place,
|
||||||
store,
|
store,
|
||||||
mocks: { $router, $route }
|
mocks: { $router, $route },
|
||||||
|
propsData: {
|
||||||
|
type: 'illustrated'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await wrapper.find('.drop-area').trigger('click')
|
await wrapper.find('.drop-area').trigger('click')
|
||||||
await db.loadDb.returnValues[0]
|
await db.loadDb.returnValues[0]
|
||||||
|
await wrapper.vm.animationPromise
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
expect($router.push.called).to.equal(false)
|
expect($router.push.called).to.equal(false)
|
||||||
})
|
wrapper.destroy()
|
||||||
})
|
|
||||||
|
|
||||||
describe('DbUploader.vue import CSV', () => {
|
|
||||||
let state = {}
|
|
||||||
let mutations = {}
|
|
||||||
let actions = {}
|
|
||||||
const newTabId = 1
|
|
||||||
let store = {}
|
|
||||||
|
|
||||||
// mock router
|
|
||||||
const $router = { }
|
|
||||||
const $route = { path: '/' }
|
|
||||||
|
|
||||||
let clock
|
|
||||||
let wrapper
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// mock getting a file from user
|
|
||||||
sinon.stub(fu, 'getFileFromUser').resolves({ type: 'text/csv' })
|
|
||||||
|
|
||||||
clock = sinon.useFakeTimers()
|
|
||||||
|
|
||||||
// mock store state and mutations
|
|
||||||
state = {}
|
|
||||||
mutations = {
|
|
||||||
saveSchema: sinon.stub(),
|
|
||||||
setDb: sinon.stub(),
|
|
||||||
setCurrentTabId: sinon.stub()
|
|
||||||
}
|
|
||||||
actions = {
|
|
||||||
addTab: sinon.stub().resolves(newTabId)
|
|
||||||
}
|
|
||||||
store = new Vuex.Store({ state, mutations, actions })
|
|
||||||
|
|
||||||
$router.push = sinon.stub()
|
|
||||||
|
|
||||||
// mount the component
|
|
||||||
wrapper = mount(DbUploader, {
|
|
||||||
store,
|
|
||||||
mocks: { $router, $route }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
sinon.restore()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows parse dialog if gets csv file', async () => {
|
it('shows parse dialog if gets csv file', async () => {
|
||||||
sinon.stub(csv, 'parse').resolves({
|
// mock getting a file from user
|
||||||
delimiter: '|',
|
const file = { name: 'test.csv' }
|
||||||
data: {
|
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[1, 'foo'],
|
|
||||||
[2, 'bar']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
messages: [{
|
|
||||||
code: 'UndetectableDelimiter',
|
|
||||||
message: 'Comma was used as a standart delimiter',
|
|
||||||
row: 0,
|
|
||||||
type: 'info',
|
|
||||||
hint: undefined
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.find('.drop-area').trigger('click')
|
// mock router
|
||||||
await csv.parse.returnValues[0]
|
const $router = { push: sinon.stub() }
|
||||||
await wrapper.vm.animationPromise
|
const $route = { path: '/editor' }
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
expect(wrapper.find('[data-modal="parse"]').exists()).to.equal(true)
|
|
||||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.value).to.equal('|')
|
|
||||||
expect(wrapper.find('#quote-char input').element.value).to.equal('"')
|
|
||||||
expect(wrapper.find('#escape-char input').element.value).to.equal('"')
|
|
||||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.checked).to.equal(true)
|
|
||||||
const rows = wrapper.findAll('tbody tr')
|
|
||||||
expect(rows).to.have.lengthOf(2)
|
|
||||||
expect(rows.at(0).findAll('td').at(0).text()).to.equal('1')
|
|
||||||
expect(rows.at(0).findAll('td').at(1).text()).to.equal('foo')
|
|
||||||
expect(rows.at(1).findAll('td').at(0).text()).to.equal('2')
|
|
||||||
expect(rows.at(1).findAll('td').at(1).text()).to.equal('bar')
|
|
||||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
|
||||||
.to.include('Information about row 0. Comma was used as a standart delimiter.')
|
|
||||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
|
||||||
.to.include('Preview parsing is completed in')
|
|
||||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
|
||||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('reparses when parameters changes', async () => {
|
// mount the component
|
||||||
const parse = sinon.stub(csv, 'parse')
|
const wrapper = mount(DbUploader, {
|
||||||
parse.onCall(0).resolves({
|
attachTo: place,
|
||||||
delimiter: '|',
|
store,
|
||||||
data: {
|
mocks: { $router, $route },
|
||||||
columns: ['col1', 'col2'],
|
propsData: {
|
||||||
values: [
|
type: 'illustrated'
|
||||||
[1, 'foo']
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const CsvImport = wrapper.vm.$refs.addCsv
|
||||||
|
sinon.stub(CsvImport, 'reset')
|
||||||
|
sinon.stub(CsvImport, 'previewCsv').resolves()
|
||||||
|
sinon.stub(CsvImport, 'open')
|
||||||
|
|
||||||
await wrapper.find('.drop-area').trigger('click')
|
await wrapper.find('.drop-area').trigger('click')
|
||||||
await csv.parse.returnValues[0]
|
|
||||||
await wrapper.vm.animationPromise
|
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(CsvImport.reset.calledOnce).to.equal(true)
|
||||||
parse.onCall(1).resolves({
|
await wrapper.vm.animationPromise
|
||||||
delimiter: ',',
|
expect(CsvImport.previewCsv.calledOnce).to.equal(true)
|
||||||
data: {
|
await wrapper.vm.$nextTick()
|
||||||
columns: ['col1', 'col2'],
|
expect(CsvImport.open.calledOnce).to.equal(true)
|
||||||
values: [
|
wrapper.destroy()
|
||||||
[2, 'bar']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: false
|
|
||||||
})
|
|
||||||
await wrapper.find('.delimiter-selector-container input').setValue(',')
|
|
||||||
expect(parse.callCount).to.equal(2)
|
|
||||||
await csv.parse.returnValues[1]
|
|
||||||
|
|
||||||
let rows = wrapper.findAll('tbody tr')
|
|
||||||
expect(rows).to.have.lengthOf(1)
|
|
||||||
expect(rows.at(0).findAll('td').at(0).text()).to.equal('2')
|
|
||||||
expect(rows.at(0).findAll('td').at(1).text()).to.equal('bar')
|
|
||||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
|
||||||
.to.include('Preview parsing is completed in')
|
|
||||||
|
|
||||||
parse.onCall(2).resolves({
|
|
||||||
delimiter: ',',
|
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[3, 'baz']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: true,
|
|
||||||
messages: [{
|
|
||||||
code: 'MissingQuotes',
|
|
||||||
message: 'Quote is missed',
|
|
||||||
row: 0,
|
|
||||||
type: 'error',
|
|
||||||
hint: 'Edit your CSV so that the field has a closing quote char.'
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.find('#quote-char input').setValue("'")
|
|
||||||
expect(parse.callCount).to.equal(3)
|
|
||||||
await csv.parse.returnValues[2]
|
|
||||||
rows = wrapper.findAll('tbody tr')
|
|
||||||
expect(rows).to.have.lengthOf(1)
|
|
||||||
expect(rows.at(0).findAll('td').at(0).text()).to.equal('3')
|
|
||||||
expect(rows.at(0).findAll('td').at(1).text()).to.equal('baz')
|
|
||||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
|
||||||
.to.contain('Error in row 0. Quote is missed. Edit your CSV so that the field has a closing quote char.')
|
|
||||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
|
||||||
.to.not.contain('Preview parsing is completed in')
|
|
||||||
|
|
||||||
parse.onCall(3).resolves({
|
|
||||||
delimiter: ',',
|
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[4, 'qux']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: false
|
|
||||||
})
|
|
||||||
await wrapper.find('#escape-char input').setValue("'")
|
|
||||||
expect(parse.callCount).to.equal(4)
|
|
||||||
await csv.parse.returnValues[3]
|
|
||||||
rows = wrapper.findAll('tbody tr')
|
|
||||||
expect(rows).to.have.lengthOf(1)
|
|
||||||
expect(rows.at(0).findAll('td').at(0).text()).to.equal('4')
|
|
||||||
expect(rows.at(0).findAll('td').at(1).text()).to.equal('qux')
|
|
||||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
|
||||||
.to.contain('Preview parsing is completed in')
|
|
||||||
|
|
||||||
parse.onCall(4).resolves({
|
|
||||||
delimiter: ',',
|
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[5, 'corge']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: false
|
|
||||||
})
|
|
||||||
await wrapper.findComponent({ name: 'check-box' }).trigger('click')
|
|
||||||
expect(parse.callCount).to.equal(5)
|
|
||||||
await csv.parse.returnValues[4]
|
|
||||||
rows = wrapper.findAll('tbody tr')
|
|
||||||
expect(rows).to.have.lengthOf(1)
|
|
||||||
expect(rows.at(0).findAll('td').at(0).text()).to.equal('5')
|
|
||||||
expect(rows.at(0).findAll('td').at(1).text()).to.equal('corge')
|
|
||||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
|
||||||
.to.include('Preview parsing is completed in')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has proper state before parsing is complete', async () => {
|
it('deletes temporary db if CSV import is canceled', async () => {
|
||||||
const parse = sinon.stub(csv, 'parse')
|
// mock getting a file from user
|
||||||
parse.onCall(0).resolves({
|
const file = { name: 'test.csv' }
|
||||||
delimiter: '|',
|
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
// mock router
|
||||||
values: [
|
const $router = { push: sinon.stub() }
|
||||||
[1, 'foo']
|
const $route = { path: '/editor' }
|
||||||
]
|
|
||||||
|
// mount the component
|
||||||
|
const wrapper = mount(DbUploader, {
|
||||||
|
store,
|
||||||
|
mocks: { $router, $route },
|
||||||
|
propsData: {
|
||||||
|
type: 'illustrated'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await wrapper.find('.drop-area').trigger('click')
|
const CsvImport = wrapper.vm.$refs.addCsv
|
||||||
await csv.parse.returnValues[0]
|
sinon.stub(CsvImport, 'reset')
|
||||||
await wrapper.vm.animationPromise
|
sinon.stub(CsvImport, 'previewCsv').resolves()
|
||||||
await wrapper.vm.$nextTick()
|
sinon.stub(CsvImport, 'open')
|
||||||
|
|
||||||
let resolveParsing
|
|
||||||
parse.onCall(1).returns(new Promise(resolve => {
|
|
||||||
resolveParsing = resolve
|
|
||||||
}))
|
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
|
||||||
|
|
||||||
// "Parsing CSV..." in the logs
|
|
||||||
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text())
|
|
||||||
.to.equal('Parsing CSV...')
|
|
||||||
|
|
||||||
// After 1 second - loading indicator is shown
|
|
||||||
await clock.tick(1000)
|
|
||||||
expect(
|
|
||||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
|
||||||
).to.equal(true)
|
|
||||||
|
|
||||||
// All the dialog controls are disabled
|
|
||||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
|
||||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
|
||||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
|
||||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
|
||||||
await resolveParsing()
|
|
||||||
await parse.returnValues[1]
|
|
||||||
|
|
||||||
// Loading indicator is not shown when parsing is compete
|
|
||||||
expect(
|
|
||||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
|
||||||
).to.equal(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('parsing is completed successfully', async () => {
|
|
||||||
const parse = sinon.stub(csv, 'parse')
|
|
||||||
parse.onCall(0).resolves({
|
|
||||||
delimiter: '|',
|
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[1, 'foo']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: false,
|
|
||||||
messages: []
|
|
||||||
})
|
|
||||||
|
|
||||||
parse.onCall(1).resolves({
|
|
||||||
delimiter: '|',
|
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[1, 'foo'],
|
|
||||||
[2, 'bar']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: false,
|
|
||||||
messages: []
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.find('.drop-area').trigger('click')
|
await wrapper.find('.drop-area').trigger('click')
|
||||||
await csv.parse.returnValues[0]
|
|
||||||
await wrapper.vm.animationPromise
|
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
|
await CsvImport.$emit('cancel')
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
expect(wrapper.vm.newDb).to.equal(null)
|
||||||
await csv.parse.returnValues[1]
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
// Parsing success in the logs
|
|
||||||
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text())
|
|
||||||
.to.include('2 rows are parsed successfully in')
|
|
||||||
|
|
||||||
// All the dialog controls are disabled
|
|
||||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
|
||||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
|
||||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
|
||||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('parsing is completed with notes', async () => {
|
|
||||||
const parse = sinon.stub(csv, 'parse')
|
|
||||||
parse.onCall(0).resolves({
|
|
||||||
delimiter: '|',
|
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[1, 'foo']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: false,
|
|
||||||
messages: []
|
|
||||||
})
|
|
||||||
|
|
||||||
parse.onCall(1).resolves({
|
|
||||||
delimiter: '|',
|
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[1, 'foo'],
|
|
||||||
[2, 'bar']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: false,
|
|
||||||
messages: [{
|
|
||||||
code: 'UndetectableDelimiter',
|
|
||||||
message: 'Comma was used as a standart delimiter',
|
|
||||||
type: 'info',
|
|
||||||
hint: undefined
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.find('.drop-area').trigger('click')
|
|
||||||
await csv.parse.returnValues[0]
|
|
||||||
await wrapper.vm.animationPromise
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
|
||||||
await csv.parse.returnValues[1]
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
// Parsing success in the logs
|
|
||||||
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
|
||||||
expect(logs).to.have.lengthOf(4)
|
|
||||||
expect(logs.at(1).text()).to.include('2 rows are parsed in')
|
|
||||||
expect(logs.at(2).text()).to.equals('Comma was used as a standart delimiter.')
|
|
||||||
|
|
||||||
// All the dialog controls are disabled
|
|
||||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
|
||||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
|
||||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
|
||||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('parsing is completed with errors', async () => {
|
|
||||||
const parse = sinon.stub(csv, 'parse')
|
|
||||||
parse.onCall(0).resolves({
|
|
||||||
delimiter: '|',
|
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[1, 'foo']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: false,
|
|
||||||
messages: []
|
|
||||||
})
|
|
||||||
|
|
||||||
parse.onCall(1).resolves({
|
|
||||||
delimiter: '|',
|
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[1, 'foo'],
|
|
||||||
[2, 'bar']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: true,
|
|
||||||
messages: [{
|
|
||||||
code: 'Error',
|
|
||||||
message: 'Something is wrong',
|
|
||||||
type: 'error',
|
|
||||||
hint: undefined
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.find('.drop-area').trigger('click')
|
|
||||||
await csv.parse.returnValues[0]
|
|
||||||
await wrapper.vm.animationPromise
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
|
||||||
await csv.parse.returnValues[1]
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
// Parsing success in the logs
|
|
||||||
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
|
||||||
expect(logs).to.have.lengthOf(3)
|
|
||||||
expect(logs.at(1).text()).to.include('Parsing ended with errors.')
|
|
||||||
expect(logs.at(2).text()).to.equals('Something is wrong.')
|
|
||||||
|
|
||||||
// All the dialog controls are enabled
|
|
||||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(false)
|
|
||||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
|
|
||||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
|
|
||||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
|
||||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
|
||||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
|
|
||||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
|
||||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
|
||||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('has proper state before import is completed', async () => {
|
|
||||||
const parse = sinon.stub(csv, 'parse')
|
|
||||||
parse.onCall(0).resolves({
|
|
||||||
delimiter: '|',
|
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[1, 'foo']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: false,
|
|
||||||
messages: []
|
|
||||||
})
|
|
||||||
|
|
||||||
parse.onCall(1).resolves({
|
|
||||||
delimiter: '|',
|
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[1, 'foo'],
|
|
||||||
[2, 'bar']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: false,
|
|
||||||
messages: []
|
|
||||||
})
|
|
||||||
|
|
||||||
let resolveImport = sinon.stub()
|
|
||||||
const newDb = {
|
|
||||||
createDb: sinon.stub().resolves(new Promise(resolve => { resolveImport = resolve })),
|
|
||||||
createProgressCounter: sinon.stub().returns(1)
|
|
||||||
}
|
|
||||||
sinon.stub(database, 'getNewDatabase').returns(newDb)
|
|
||||||
|
|
||||||
await wrapper.find('.drop-area').trigger('click')
|
|
||||||
await csv.parse.returnValues[0]
|
|
||||||
await wrapper.vm.animationPromise
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
|
||||||
await csv.parse.returnValues[1]
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
// Parsing success in the logs
|
|
||||||
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(2).text())
|
|
||||||
.to.equal('Importing CSV into a SQLite database...')
|
|
||||||
|
|
||||||
// After 1 second - loading indicator is shown
|
|
||||||
await clock.tick(1000)
|
|
||||||
expect(
|
|
||||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
|
||||||
).to.equal(true)
|
|
||||||
|
|
||||||
// All the dialog controls are disabled
|
|
||||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
|
||||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
|
||||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
|
||||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
|
||||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
|
||||||
|
|
||||||
// After resolving - loading indicator is not shown
|
|
||||||
await resolveImport()
|
|
||||||
await newDb.createDb.returnValues[0]
|
|
||||||
expect(
|
|
||||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
|
||||||
).to.equal(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('import success', async () => {
|
|
||||||
const parse = sinon.stub(csv, 'parse')
|
|
||||||
parse.onCall(0).resolves({
|
|
||||||
delimiter: '|',
|
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[1, 'foo']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: false,
|
|
||||||
messages: []
|
|
||||||
})
|
|
||||||
|
|
||||||
parse.onCall(1).resolves({
|
|
||||||
delimiter: '|',
|
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[1, 'foo'],
|
|
||||||
[2, 'bar']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: false,
|
|
||||||
messages: []
|
|
||||||
})
|
|
||||||
|
|
||||||
const schema = {}
|
|
||||||
const newDb = {
|
|
||||||
createDb: sinon.stub().resolves(schema),
|
|
||||||
createProgressCounter: sinon.stub().returns(1),
|
|
||||||
deleteProgressCounter: sinon.stub()
|
|
||||||
}
|
|
||||||
sinon.stub(database, 'getNewDatabase').returns(newDb)
|
|
||||||
|
|
||||||
await wrapper.find('.drop-area').trigger('click')
|
|
||||||
await csv.parse.returnValues[0]
|
|
||||||
await wrapper.vm.animationPromise
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
|
||||||
await csv.parse.returnValues[1]
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
// Import success in the logs
|
|
||||||
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
|
||||||
expect(logs).to.have.lengthOf(3)
|
|
||||||
expect(logs.at(2).text()).to.contain('Importing CSV into a SQLite database is completed in')
|
|
||||||
|
|
||||||
// All the dialog controls are enabled
|
|
||||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(false)
|
|
||||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
|
|
||||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
|
|
||||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
|
||||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
|
||||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
|
|
||||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
|
||||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('import fails', async () => {
|
|
||||||
const parse = sinon.stub(csv, 'parse')
|
|
||||||
parse.onCall(0).resolves({
|
|
||||||
delimiter: '|',
|
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[1, 'foo']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: false,
|
|
||||||
messages: []
|
|
||||||
})
|
|
||||||
|
|
||||||
parse.onCall(1).resolves({
|
|
||||||
delimiter: '|',
|
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[1, 'foo'],
|
|
||||||
[2, 'bar']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: false,
|
|
||||||
messages: []
|
|
||||||
})
|
|
||||||
|
|
||||||
const newDb = {
|
|
||||||
createDb: sinon.stub().rejects(new Error('fail')),
|
|
||||||
createProgressCounter: sinon.stub().returns(1),
|
|
||||||
deleteProgressCounter: sinon.stub()
|
|
||||||
}
|
|
||||||
sinon.stub(database, 'getNewDatabase').returns(newDb)
|
|
||||||
|
|
||||||
await wrapper.find('.drop-area').trigger('click')
|
|
||||||
await csv.parse.returnValues[0]
|
|
||||||
await wrapper.vm.animationPromise
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
|
||||||
await csv.parse.returnValues[1]
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
// Import success in the logs
|
|
||||||
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
|
||||||
expect(logs).to.have.lengthOf(4)
|
|
||||||
expect(logs.at(2).text()).to.contain('Importing CSV into a SQLite database...')
|
|
||||||
expect(logs.at(3).text()).to.equal('Error: fail.')
|
|
||||||
|
|
||||||
// All the dialog controls are enabled
|
|
||||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.disabled).to.equal(false)
|
|
||||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
|
|
||||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
|
|
||||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
|
||||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
|
||||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
|
|
||||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
|
||||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('import final', async () => {
|
|
||||||
const parse = sinon.stub(csv, 'parse')
|
|
||||||
parse.onCall(0).resolves({
|
|
||||||
delimiter: '|',
|
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[1, 'foo']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: false,
|
|
||||||
messages: []
|
|
||||||
})
|
|
||||||
|
|
||||||
parse.onCall(1).resolves({
|
|
||||||
delimiter: '|',
|
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[1, 'foo'],
|
|
||||||
[2, 'bar']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: false,
|
|
||||||
messages: []
|
|
||||||
})
|
|
||||||
|
|
||||||
const schema = {}
|
|
||||||
const newDb = {
|
|
||||||
createDb: sinon.stub().resolves(schema),
|
|
||||||
createProgressCounter: sinon.stub().returns(1),
|
|
||||||
deleteProgressCounter: sinon.stub()
|
|
||||||
}
|
|
||||||
sinon.stub(database, 'getNewDatabase').returns(newDb)
|
|
||||||
|
|
||||||
await wrapper.find('.drop-area').trigger('click')
|
|
||||||
await csv.parse.returnValues[0]
|
|
||||||
await wrapper.vm.animationPromise
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
|
||||||
await csv.parse.returnValues[1]
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
await wrapper.find('#csv-finish').trigger('click')
|
|
||||||
|
|
||||||
expect(mutations.setDb.calledOnceWith(state, newDb)).to.equal(true)
|
|
||||||
expect(mutations.saveSchema.calledOnceWith(state, schema)).to.equal(true)
|
|
||||||
expect(actions.addTab.calledOnce).to.equal(true)
|
|
||||||
await actions.addTab.returnValues[0]
|
|
||||||
expect(mutations.setCurrentTabId.calledOnceWith(state, newTabId)).to.equal(true)
|
|
||||||
expect($router.push.calledOnceWith('/editor')).to.equal(true)
|
|
||||||
expect(wrapper.find('[data-modal="parse"]').exists()).to.equal(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('import cancel', async () => {
|
|
||||||
const parse = sinon.stub(csv, 'parse')
|
|
||||||
parse.onCall(0).resolves({
|
|
||||||
delimiter: '|',
|
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[1, 'foo']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: false,
|
|
||||||
messages: []
|
|
||||||
})
|
|
||||||
|
|
||||||
parse.onCall(1).resolves({
|
|
||||||
delimiter: '|',
|
|
||||||
data: {
|
|
||||||
columns: ['col1', 'col2'],
|
|
||||||
values: [
|
|
||||||
[1, 'foo'],
|
|
||||||
[2, 'bar']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasErrors: false,
|
|
||||||
messages: []
|
|
||||||
})
|
|
||||||
|
|
||||||
const schema = {}
|
|
||||||
const newDb = {
|
|
||||||
createDb: sinon.stub().resolves(schema),
|
|
||||||
createProgressCounter: sinon.stub().returns(1),
|
|
||||||
deleteProgressCounter: sinon.stub()
|
|
||||||
}
|
|
||||||
sinon.stub(database, 'getNewDatabase').returns(newDb)
|
|
||||||
|
|
||||||
await wrapper.find('.drop-area').trigger('click')
|
|
||||||
await csv.parse.returnValues[0]
|
|
||||||
await wrapper.vm.animationPromise
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
|
||||||
await csv.parse.returnValues[1]
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
await wrapper.find('#csv-cancel').trigger('click')
|
|
||||||
|
|
||||||
expect(mutations.setDb.called).to.equal(false)
|
|
||||||
expect(mutations.saveSchema.called).to.equal(false)
|
|
||||||
expect(actions.addTab.called).to.equal(false)
|
|
||||||
expect(mutations.setCurrentTabId.called).to.equal(false)
|
|
||||||
expect($router.push.called).to.equal(false)
|
|
||||||
expect(wrapper.find('[data-modal="parse"]').exists()).to.equal(false)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { shallowMount } from '@vue/test-utils'
|
import { shallowMount } from '@vue/test-utils'
|
||||||
import LoadingIndicator from '@/components/LoadingIndicator.vue'
|
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||||
|
|
||||||
describe('LoadingIndicator.vue', () => {
|
describe('LoadingIndicator.vue', () => {
|
||||||
it('Calculates animation class', async () => {
|
it('Calculates animation class', async () => {
|
||||||
@@ -19,6 +19,6 @@ describe('LoadingIndicator.vue', () => {
|
|||||||
})
|
})
|
||||||
// The lendth of circle in the component is 50.24. If progress is 50% then resulting arc
|
// The lendth of circle in the component is 50.24. If progress is 50% then resulting arc
|
||||||
// should be 25.12
|
// should be 25.12
|
||||||
expect(wrapper.find('.loader-svg.front').element.style.strokeDasharray).to.equal('25.12, 25.12')
|
expect(wrapper.find('.loader-svg.front').element.style.strokeDasharray).to.equal('25.12px, 25.12px')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { shallowMount } from '@vue/test-utils'
|
import { shallowMount } from '@vue/test-utils'
|
||||||
import Logs from '@/components/Logs.vue'
|
import Logs from '@/components/Logs'
|
||||||
|
|
||||||
let place
|
let place
|
||||||
describe('Logs.vue', () => {
|
describe('Logs.vue', () => {
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
import { expect } from 'chai'
|
|
||||||
import sinon from 'sinon'
|
|
||||||
import { mount, createLocalVue } from '@vue/test-utils'
|
|
||||||
import Vuex from 'vuex'
|
|
||||||
import Schema from '@/components/Schema.vue'
|
|
||||||
import TableDescription from '@/components/TableDescription.vue'
|
|
||||||
|
|
||||||
const localVue = createLocalVue()
|
|
||||||
localVue.use(Vuex)
|
|
||||||
|
|
||||||
describe('Schema.vue', () => {
|
|
||||||
afterEach(() => {
|
|
||||||
sinon.restore()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Renders DB name on initial', () => {
|
|
||||||
// mock store state
|
|
||||||
const state = {
|
|
||||||
dbName: 'fooDB'
|
|
||||||
}
|
|
||||||
const store = new Vuex.Store({ state })
|
|
||||||
|
|
||||||
// mout the component
|
|
||||||
const wrapper = mount(Schema, { store, localVue })
|
|
||||||
|
|
||||||
// check DB name and schema visibility
|
|
||||||
expect(wrapper.find('.db-name').text()).to.equal('fooDB')
|
|
||||||
expect(wrapper.find('.schema').isVisible()).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Schema visibility is toggled when click on DB name', async () => {
|
|
||||||
// mock store state
|
|
||||||
const state = {
|
|
||||||
dbName: 'fooDB'
|
|
||||||
}
|
|
||||||
const store = new Vuex.Store({ state })
|
|
||||||
|
|
||||||
// mout the component
|
|
||||||
const wrapper = mount(Schema, { store, localVue })
|
|
||||||
|
|
||||||
// click and check visibility
|
|
||||||
await wrapper.find('.db-name').trigger('click')
|
|
||||||
expect(wrapper.find('.schema').isVisible()).to.equal(false)
|
|
||||||
await wrapper.find('.db-name').trigger('click')
|
|
||||||
expect(wrapper.find('.schema').isVisible()).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Schema filter', async () => {
|
|
||||||
// mock store state
|
|
||||||
const state = {
|
|
||||||
dbName: 'fooDB',
|
|
||||||
schema: [
|
|
||||||
{
|
|
||||||
name: 'foo',
|
|
||||||
columns: [
|
|
||||||
{ name: 'id', type: 'INTEGER' },
|
|
||||||
{ name: 'title', type: 'NVARCHAR(24)' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'bar',
|
|
||||||
columns: [
|
|
||||||
{ name: 'id', type: 'INTEGER' },
|
|
||||||
{ name: 'price', type: 'INTEGER' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'foobar',
|
|
||||||
columns: [
|
|
||||||
{ name: 'id', type: 'INTEGER' },
|
|
||||||
{ name: 'price', type: 'INTEGER' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
const store = new Vuex.Store({ state })
|
|
||||||
|
|
||||||
// mount the component
|
|
||||||
const wrapper = mount(Schema, { store, localVue })
|
|
||||||
|
|
||||||
// apply filters and check the list of tables
|
|
||||||
await wrapper.find('#schema-filter input').setValue('foo')
|
|
||||||
let tables = wrapper.findAllComponents(TableDescription)
|
|
||||||
expect(tables).to.have.lengthOf(2)
|
|
||||||
expect(tables.at(0).vm.name).to.equal('foo')
|
|
||||||
expect(tables.at(1).vm.name).to.equal('foobar')
|
|
||||||
|
|
||||||
await wrapper.find('#schema-filter input').setValue('bar')
|
|
||||||
tables = wrapper.findAllComponents(TableDescription)
|
|
||||||
expect(tables).to.have.lengthOf(2)
|
|
||||||
expect(tables.at(0).vm.name).to.equal('bar')
|
|
||||||
expect(tables.at(1).vm.name).to.equal('foobar')
|
|
||||||
|
|
||||||
await wrapper.find('#schema-filter input').setValue('')
|
|
||||||
tables = wrapper.findAllComponents(TableDescription)
|
|
||||||
expect(tables).to.have.lengthOf(3)
|
|
||||||
expect(tables.at(0).vm.name).to.equal('foo')
|
|
||||||
expect(tables.at(1).vm.name).to.equal('bar')
|
|
||||||
expect(tables.at(2).vm.name).to.equal('foobar')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { shallowMount } from '@vue/test-utils'
|
import { shallowMount } from '@vue/test-utils'
|
||||||
import Splitpanes from '@/components/Splitpanes.vue'
|
import Splitpanes from '@/components/Splitpanes'
|
||||||
|
|
||||||
describe('Splitpanes.vue', () => {
|
describe('Splitpanes.vue', () => {
|
||||||
it('renders correctly - vertical', () => {
|
it('renders correctly - vertical', () => {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import splitter from '@/splitter'
|
import splitter from '@/components/Splitpanes/splitter'
|
||||||
|
|
||||||
describe('splitter.js', () => {
|
describe('splitter.js', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import Pager from '@/components/Pager.vue'
|
import Pager from '@/components/SqlTable/Pager'
|
||||||
|
|
||||||
describe('Pager.vue', () => {
|
describe('Pager.vue', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { expect } from 'chai'
|
|
||||||
import dbUtils from '@/db.utils'
|
|
||||||
|
|
||||||
describe('db.utils.js', () => {
|
|
||||||
it('generateChunks', () => {
|
|
||||||
const arr = ['1', '2', '3', '4', '5']
|
|
||||||
const size = 2
|
|
||||||
const chunks = dbUtils.generateChunks(arr, size)
|
|
||||||
const output = []
|
|
||||||
for (const chunk of chunks) {
|
|
||||||
output.push(chunk)
|
|
||||||
}
|
|
||||||
expect(output[0]).to.eql(['1', '2'])
|
|
||||||
expect(output[1]).to.eql(['3', '4'])
|
|
||||||
expect(output[2]).to.eql(['5'])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('getInsertStmt', () => {
|
|
||||||
const columns = ['id', 'name']
|
|
||||||
expect(dbUtils.getInsertStmt(columns))
|
|
||||||
.to.equal('INSERT INTO csv_import ("id", "name") VALUES (?, ?);')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('getCreateStatement', () => {
|
|
||||||
const columns = ['id', 'name', 'isAdmin', 'startDate']
|
|
||||||
const values = [
|
|
||||||
[1, 'foo', true, new Date()],
|
|
||||||
[2, 'bar', false, new Date()]
|
|
||||||
]
|
|
||||||
expect(dbUtils.getCreateStatement(columns, values)).to.equal(
|
|
||||||
'CREATE table csv_import("id" REAL, "name" TEXT, "isAdmin" INTEGER, "startDate" TEXT);'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -2,14 +2,14 @@ import chai from 'chai'
|
|||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import chaiAsPromised from 'chai-as-promised'
|
import chaiAsPromised from 'chai-as-promised'
|
||||||
import initSqlJs from 'sql.js'
|
import initSqlJs from 'sql.js'
|
||||||
import Sql from '@/sql'
|
import Sql from '@/lib/database/_sql'
|
||||||
chai.use(chaiAsPromised)
|
chai.use(chaiAsPromised)
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
chai.should()
|
chai.should()
|
||||||
|
|
||||||
const getSQL = initSqlJs()
|
const getSQL = initSqlJs()
|
||||||
|
|
||||||
describe('sql.js', () => {
|
describe('_sql.js', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
sinon.restore()
|
sinon.restore()
|
||||||
})
|
})
|
||||||
@@ -74,8 +74,8 @@ describe('sql.js', () => {
|
|||||||
const progressCallback = sinon.stub()
|
const progressCallback = sinon.stub()
|
||||||
const progressCounterId = 1
|
const progressCounterId = 1
|
||||||
const sql = await Sql.build()
|
const sql = await Sql.build()
|
||||||
sql.import(data.columns, data.values, progressCounterId, progressCallback, 2)
|
sql.import('foo', data.columns, data.values, progressCounterId, progressCallback, 2)
|
||||||
const result = sql.exec('SELECT * from csv_import')
|
const result = sql.exec('SELECT * from foo')
|
||||||
expect(result).to.have.lengthOf(1)
|
expect(result).to.have.lengthOf(1)
|
||||||
expect(result[0].columns).to.eql(['id', 'name'])
|
expect(result[0].columns).to.eql(['id', 'name'])
|
||||||
expect(result[0].values).to.have.lengthOf(4)
|
expect(result[0].values).to.have.lengthOf(4)
|
||||||
@@ -135,7 +135,7 @@ describe('sql.js', () => {
|
|||||||
expect(sql.db.db).to.equal(null)
|
expect(sql.db.db).to.equal(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('overwrites', async () => {
|
it('adds', async () => {
|
||||||
const sql = await Sql.build()
|
const sql = await Sql.build()
|
||||||
sql.exec(`
|
sql.exec(`
|
||||||
CREATE TABLE test (
|
CREATE TABLE test (
|
||||||
@@ -160,12 +160,11 @@ describe('sql.js', () => {
|
|||||||
[4, 'Ron Weasley']
|
[4, 'Ron Weasley']
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
// rewrite the database by import
|
// import adds table
|
||||||
sql.import(data.columns, data.values, 1, sinon.stub(), 2)
|
sql.import('foo', data.columns, data.values, 1, sinon.stub(), 2)
|
||||||
result = sql.exec('SELECT * from csv_import')
|
result = sql.exec('SELECT * from foo')
|
||||||
expect(result[0].values).to.have.lengthOf(4)
|
expect(result[0].values).to.have.lengthOf(4)
|
||||||
|
result = sql.exec('SELECT * from test')
|
||||||
// test table oesn't exists anymore: the db was overwritten
|
expect(result[0].values).to.have.lengthOf(2)
|
||||||
expect(() => { sql.exec('SELECT * from test') }).to.throw('no such table: test')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
62
tests/lib/database/_statements.spec.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { expect } from 'chai'
|
||||||
|
import stmts from '@/lib/database/_statements'
|
||||||
|
|
||||||
|
describe('_statements.js', () => {
|
||||||
|
it('generateChunks', () => {
|
||||||
|
const arr = ['1', '2', '3', '4', '5']
|
||||||
|
const size = 2
|
||||||
|
const chunks = stmts.generateChunks(arr, size)
|
||||||
|
const output = []
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
output.push(chunk)
|
||||||
|
}
|
||||||
|
expect(output[0]).to.eql(['1', '2'])
|
||||||
|
expect(output[1]).to.eql(['3', '4'])
|
||||||
|
expect(output[2]).to.eql(['5'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getInsertStmt', () => {
|
||||||
|
const columns = ['id', 'name']
|
||||||
|
expect(stmts.getInsertStmt('foo', columns))
|
||||||
|
.to.equal('INSERT INTO "foo" ("id", "name") VALUES (?, ?);')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getCreateStatement', () => {
|
||||||
|
const columns = ['id', 'name', 'isAdmin', 'startDate']
|
||||||
|
const values = [
|
||||||
|
[1, 'foo', true, new Date()],
|
||||||
|
[2, 'bar', false, new Date()]
|
||||||
|
]
|
||||||
|
expect(stmts.getCreateStatement('foo', columns, values)).to.equal(
|
||||||
|
'CREATE table "foo"("id" REAL, "name" TEXT, "isAdmin" INTEGER, "startDate" TEXT);'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getColumns', () => {
|
||||||
|
const sql = `CREATE TABLE test (
|
||||||
|
col1,
|
||||||
|
col2 integer,
|
||||||
|
col3 decimal(5,2),
|
||||||
|
col4 varchar(30)
|
||||||
|
)`
|
||||||
|
expect(stmts.getColumns(sql)).to.eql([
|
||||||
|
{ name: 'col1', type: 'N/A' },
|
||||||
|
{ name: 'col2', type: 'integer' },
|
||||||
|
{ name: 'col3', type: 'decimal(5, 2)' },
|
||||||
|
{ name: 'col4', type: 'varchar(30)' }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getColumns with virtual table', async () => {
|
||||||
|
const sql = `
|
||||||
|
CREATE VIRTUAL TABLE test_virtual USING fts4(
|
||||||
|
col1, col2,
|
||||||
|
notindexed=col1, notindexed=col2,
|
||||||
|
tokenize=unicode61 "tokenchars=.+#")
|
||||||
|
`
|
||||||
|
expect(stmts.getColumns(sql)).to.eql([
|
||||||
|
{ name: 'col1', type: 'N/A' },
|
||||||
|
{ name: 'col2', type: 'N/A' }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -2,7 +2,9 @@ import chai from 'chai'
|
|||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import chaiAsPromised from 'chai-as-promised'
|
import chaiAsPromised from 'chai-as-promised'
|
||||||
import initSqlJs from 'sql.js'
|
import initSqlJs from 'sql.js'
|
||||||
import database from '@/database.js'
|
import database from '@/lib/database'
|
||||||
|
import fu from '@/lib/utils/fileIo'
|
||||||
|
|
||||||
chai.use(chaiAsPromised)
|
chai.use(chaiAsPromised)
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
chai.should()
|
chai.should()
|
||||||
@@ -25,55 +27,40 @@ 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'
|
||||||
|
|
||||||
const { schema } = await db.loadDb(buffer)
|
sinon.spy(db, 'refreshSchema')
|
||||||
|
|
||||||
|
await db.loadDb(buffer)
|
||||||
|
await db.refreshSchema.returnValues[0]
|
||||||
|
const schema = db.schema
|
||||||
|
expect(db.dbName).to.equal('foo')
|
||||||
expect(schema).to.have.lengthOf(1)
|
expect(schema).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]
|
||||||
|
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()
|
buffer.name = 'foo.sqlite'
|
||||||
tempDb.run('CREATE TABLE test (col1, col2)')
|
|
||||||
|
|
||||||
const data = tempDb.export()
|
|
||||||
const buffer = new Blob([data])
|
|
||||||
|
|
||||||
sinon.stub(db.pw, 'postMessage').resolves({ error: new Error('foo') })
|
sinon.stub(db.pw, 'postMessage').resolves({ error: new Error('foo') })
|
||||||
|
|
||||||
@@ -97,6 +84,7 @@ describe('database.js', () => {
|
|||||||
|
|
||||||
const data = tempDb.export()
|
const data = tempDb.export()
|
||||||
const buffer = new Blob([data])
|
const buffer = new Blob([data])
|
||||||
|
buffer.name = 'foo.sqlite'
|
||||||
|
|
||||||
await db.loadDb(buffer)
|
await db.loadDb(buffer)
|
||||||
const result = await db.execute('SELECT * from test limit 1; SELECT * from test;')
|
const result = await db.execute('SELECT * from test limit 1; SELECT * from test;')
|
||||||
@@ -124,11 +112,12 @@ describe('database.js', () => {
|
|||||||
|
|
||||||
const data = tempDb.export()
|
const data = tempDb.export()
|
||||||
const buffer = new Blob([data])
|
const buffer = new Blob([data])
|
||||||
|
buffer.name = 'foo.sqlite'
|
||||||
await db.loadDb(buffer)
|
await db.loadDb(buffer)
|
||||||
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: [
|
||||||
@@ -138,16 +127,19 @@ describe('database.js', () => {
|
|||||||
}
|
}
|
||||||
const progressHandler = sinon.spy()
|
const progressHandler = sinon.spy()
|
||||||
const progressCounterId = db.createProgressCounter(progressHandler)
|
const progressCounterId = db.createProgressCounter(progressHandler)
|
||||||
const { dbName, schema } = await db.createDb('foo', data, progressCounterId)
|
sinon.spy(db, 'refreshSchema')
|
||||||
expect(dbName).to.equal('foo')
|
|
||||||
expect(schema).to.have.lengthOf(1)
|
|
||||||
expect(schema[0].name).to.equal('csv_import')
|
|
||||||
expect(schema[0].columns).to.have.lengthOf(3)
|
|
||||||
expect(schema[0].columns[0]).to.eql({ name: 'id', type: 'real' })
|
|
||||||
expect(schema[0].columns[1]).to.eql({ name: 'name', type: 'text' })
|
|
||||||
expect(schema[0].columns[2]).to.eql({ name: 'faculty', type: 'text' })
|
|
||||||
|
|
||||||
const result = await db.execute('SELECT * from csv_import')
|
await db.addTableFromCsv('foo', data, progressCounterId)
|
||||||
|
await db.refreshSchema.returnValues[0]
|
||||||
|
expect(db.dbName).to.equal('database')
|
||||||
|
expect(db.schema).to.have.lengthOf(1)
|
||||||
|
expect(db.schema[0].name).to.equal('foo')
|
||||||
|
expect(db.schema[0].columns).to.have.lengthOf(3)
|
||||||
|
expect(db.schema[0].columns[0]).to.eql({ name: 'id', type: 'real' })
|
||||||
|
expect(db.schema[0].columns[1]).to.eql({ name: 'name', type: 'text' })
|
||||||
|
expect(db.schema[0].columns[2]).to.eql({ name: 'faculty', type: 'text' })
|
||||||
|
|
||||||
|
const result = await db.execute('SELECT * from foo')
|
||||||
expect(result.columns).to.eql(data.columns)
|
expect(result.columns).to.eql(data.columns)
|
||||||
expect(result.values).to.eql(data.values)
|
expect(result.values).to.eql(data.values)
|
||||||
|
|
||||||
@@ -156,7 +148,7 @@ describe('database.js', () => {
|
|||||||
expect(progressHandler.secondCall.calledWith(100)).to.equal(true)
|
expect(progressHandler.secondCall.calledWith(100)).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('createDb throws errors', async () => {
|
it('addTableFromCsv throws errors', async () => {
|
||||||
const data = {
|
const data = {
|
||||||
columns: ['id', 'name'],
|
columns: ['id', 'name'],
|
||||||
values: [
|
values: [
|
||||||
@@ -166,7 +158,7 @@ describe('database.js', () => {
|
|||||||
}
|
}
|
||||||
const progressHandler = sinon.stub()
|
const progressHandler = sinon.stub()
|
||||||
const progressCounterId = db.createProgressCounter(progressHandler)
|
const progressCounterId = db.createProgressCounter(progressHandler)
|
||||||
await expect(db.createDb('foo', data, progressCounterId))
|
await expect(db.addTableFromCsv('foo', data, progressCounterId))
|
||||||
.to.be.rejectedWith('column index out of range')
|
.to.be.rejectedWith('column index out of range')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -204,4 +196,53 @@ describe('database.js', () => {
|
|||||||
db.deleteProgressCounter(firstId)
|
db.deleteProgressCounter(firstId)
|
||||||
expect(db.importProgresses[firstId]).to.equal(undefined)
|
expect(db.importProgresses[firstId]).to.equal(undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('exports db', async () => {
|
||||||
|
sinon.stub(fu, 'exportToFile').resolves()
|
||||||
|
|
||||||
|
// create db with table foo
|
||||||
|
const stmt = `
|
||||||
|
CREATE TABLE foo(id, name);
|
||||||
|
INSERT INTO foo VALUES (1, 'Harry Potter')
|
||||||
|
`
|
||||||
|
let result = await db.execute(stmt)
|
||||||
|
|
||||||
|
// export db to a file
|
||||||
|
await db.export('fooDb.sqlite')
|
||||||
|
expect(fu.exportToFile.called).to.equal(true)
|
||||||
|
|
||||||
|
// get data from export
|
||||||
|
const data = fu.exportToFile.getCall(0).args[0]
|
||||||
|
const file = new Blob([data])
|
||||||
|
file.name = 'fooDb.sqlite'
|
||||||
|
|
||||||
|
// loadDb from exported data
|
||||||
|
const anotherDb = database.getNewDatabase()
|
||||||
|
await anotherDb.loadDb(file)
|
||||||
|
|
||||||
|
// check that new db works and has the same table and data
|
||||||
|
result = await anotherDb.execute('SELECT * from foo')
|
||||||
|
expect(result.columns).to.eql(['id', 'name'])
|
||||||
|
expect(result.values).to.have.lengthOf(1)
|
||||||
|
expect(result.values[0]).to.eql([1, 'Harry Potter'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sanitizeTableName', () => {
|
||||||
|
let name = 'foo[]bar'
|
||||||
|
expect(db.sanitizeTableName(name)).to.equal('foo_bar')
|
||||||
|
|
||||||
|
name = '1 foo(01.05.2020)'
|
||||||
|
expect(db.sanitizeTableName(name)).to.equal('_1_foo_01_05_2020_')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('validateTableName', async () => {
|
||||||
|
await db.execute('CREATE TABLE foo(id)')
|
||||||
|
await expect(db.validateTableName('foo')).to.be.rejectedWith('table "foo" already exists')
|
||||||
|
await expect(db.validateTableName('1foo'))
|
||||||
|
.to.be.rejectedWith("Table name can't start with a digit")
|
||||||
|
await expect(db.validateTableName('foo(05.08.2020)'))
|
||||||
|
.to.be.rejectedWith('Table name can contain only letters, digits and underscores')
|
||||||
|
await expect(db.validateTableName('sqlite_foo'))
|
||||||
|
.to.be.rejectedWith("Table name can't start with sqlite_")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import storedQueries from '@/storedQueries.js'
|
import storedQueries from '@/lib/storedQueries'
|
||||||
import fu from '@/file.utils'
|
import fu from '@/lib/utils/fileIo'
|
||||||
|
|
||||||
describe('storedQueries.js', () => {
|
describe('storedQueries.js', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import fu from '@/file.utils'
|
import fIo from '@/lib/utils/fileIo'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
|
|
||||||
describe('file.utils.js', () => {
|
describe('fileIo.js', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
sinon.restore()
|
sinon.restore()
|
||||||
})
|
})
|
||||||
@@ -15,7 +15,7 @@ describe('file.utils.js', () => {
|
|||||||
sinon.spy(URL, 'revokeObjectURL')
|
sinon.spy(URL, 'revokeObjectURL')
|
||||||
sinon.spy(window, 'Blob')
|
sinon.spy(window, 'Blob')
|
||||||
|
|
||||||
fu.exportToFile('foo', 'foo.txt')
|
fIo.exportToFile('foo', 'foo.txt')
|
||||||
|
|
||||||
expect(document.createElement.calledOnceWith('a')).to.equal(true)
|
expect(document.createElement.calledOnceWith('a')).to.equal(true)
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ describe('file.utils.js', () => {
|
|||||||
sinon.spy(URL, 'revokeObjectURL')
|
sinon.spy(URL, 'revokeObjectURL')
|
||||||
sinon.spy(window, 'Blob')
|
sinon.spy(window, 'Blob')
|
||||||
|
|
||||||
fu.exportToFile('foo', 'foo.html', 'text/html')
|
fIo.exportToFile('foo', 'foo.html', 'text/html')
|
||||||
|
|
||||||
expect(document.createElement.calledOnceWith('a')).to.equal(true)
|
expect(document.createElement.calledOnceWith('a')).to.equal(true)
|
||||||
|
|
||||||
@@ -57,9 +57,9 @@ describe('file.utils.js', () => {
|
|||||||
expect(URL.revokeObjectURL.calledOnceWith(url)).to.equal(true)
|
expect(URL.revokeObjectURL.calledOnceWith(url)).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('importFile', () => {
|
it('importFile', async () => {
|
||||||
const spyInput = document.createElement('input')
|
const spyInput = document.createElement('input')
|
||||||
sinon.spy(spyInput, 'click')
|
sinon.stub(spyInput, 'click')
|
||||||
|
|
||||||
const blob = new Blob(['foo'])
|
const blob = new Blob(['foo'])
|
||||||
Object.defineProperty(spyInput, 'files', {
|
Object.defineProperty(spyInput, 'files', {
|
||||||
@@ -71,26 +71,24 @@ describe('file.utils.js', () => {
|
|||||||
|
|
||||||
setTimeout(() => { spyInput.dispatchEvent(new Event('change')) })
|
setTimeout(() => { spyInput.dispatchEvent(new Event('change')) })
|
||||||
|
|
||||||
return fu.importFile()
|
const data = await fIo.importFile()
|
||||||
.then((data) => {
|
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')
|
expect(spyInput.accept).to.equal('.json')
|
||||||
expect(spyInput.accept).to.equal('.json')
|
expect(spyInput.click.calledOnce).to.equal(true)
|
||||||
expect(spyInput.click.calledOnce).to.equal(true)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
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)
|
||||||
@@ -105,6 +103,34 @@ describe('file.utils.js', () => {
|
|||||||
sinon.stub(window, 'FileReader').returns(r)
|
sinon.stub(window, 'FileReader').returns(r)
|
||||||
|
|
||||||
const blob = new Blob(['foo'])
|
const blob = new Blob(['foo'])
|
||||||
await expect(fu.readAsArrayBuffer(blob)).to.be.rejectedWith('Problem parsing input file.')
|
await expect(fIo.readAsArrayBuffer(blob)).to.be.rejectedWith('Problem parsing input file.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('isDatabase', () => {
|
||||||
|
let file = { type: 'application/vnd.sqlite3' }
|
||||||
|
expect(fIo.isDatabase(file)).to.equal(true)
|
||||||
|
|
||||||
|
file = { type: 'application/x-sqlite3' }
|
||||||
|
expect(fIo.isDatabase(file)).to.equal(true)
|
||||||
|
|
||||||
|
file = { type: '', name: 'test.db' }
|
||||||
|
expect(fIo.isDatabase(file)).to.equal(true)
|
||||||
|
|
||||||
|
file = { type: '', name: 'test.sqlite' }
|
||||||
|
expect(fIo.isDatabase(file)).to.equal(true)
|
||||||
|
|
||||||
|
file = { type: '', name: 'test.sqlite3' }
|
||||||
|
expect(fIo.isDatabase(file)).to.equal(true)
|
||||||
|
|
||||||
|
file = { type: '', name: 'test.csv' }
|
||||||
|
expect(fIo.isDatabase(file)).to.equal(false)
|
||||||
|
|
||||||
|
file = { type: 'text', name: 'test.db' }
|
||||||
|
expect(fIo.isDatabase(file)).to.equal(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getFileName', () => {
|
||||||
|
expect(fIo.getFileName({ name: 'foo.csv' })).to.equal('foo')
|
||||||
|
expect(fIo.getFileName({ name: 'foo.bar.db' })).to.equal('foo.bar')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import time from '@/time'
|
import time from '@/lib/utils/time'
|
||||||
|
|
||||||
describe('time.js', () => {
|
describe('time.js', () => {
|
||||||
it('getPeriod', () => {
|
it('getPeriod', () => {
|
||||||
// 1.01.2021 13:00:00 000
|
// 1.01.2021 13:00:00 000
|
||||||
let start = new Date(2021, 0, 1, 13, 0, 0, 0)
|
let start = new Date(2021, 0, 1, 13, 0, 0, 0)
|
||||||
|
|
||||||
// 3.01.2021 22:15:20 500
|
// 1.01.2021 13:01:00 500
|
||||||
let end = new Date(2021, 0, 3, 22, 15, 20, 500)
|
let end = new Date(2021, 0, 1, 13, 1, 0, 500)
|
||||||
|
|
||||||
expect(time.getPeriod(start, end)).to.equal('2 d 9 h 15 m 20 s 500 ms')
|
expect(time.getPeriod(start, end)).to.equal('60.500s')
|
||||||
|
|
||||||
// 1.01.2021 13:00:00 000
|
// 1.01.2021 13:00:00 000
|
||||||
start = new Date(2021, 0, 1, 13, 0, 0, 0)
|
start = new Date(2021, 0, 1, 13, 0, 0, 0)
|
||||||
|
|
||||||
// 1.01.2021 22:00:20 000
|
// 1.01.2021 13:00:20 500
|
||||||
end = new Date(2021, 0, 1, 22, 0, 20, 0)
|
end = new Date(2021, 0, 1, 13, 0, 20, 500)
|
||||||
|
|
||||||
expect(time.getPeriod(start, end)).to.equal('9 h 20 s')
|
expect(time.getPeriod(start, end)).to.equal('20.500s')
|
||||||
|
|
||||||
// 1.01.2021 13:00:00 000
|
// 1.01.2021 13:00:00 000
|
||||||
start = new Date(2021, 0, 1, 13, 0, 0, 0)
|
start = new Date(2021, 0, 1, 13, 0, 0, 0)
|
||||||
@@ -25,6 +25,6 @@ describe('time.js', () => {
|
|||||||
// 1.01.2021 13:00:00 45
|
// 1.01.2021 13:00:00 45
|
||||||
end = new Date(2021, 0, 1, 13, 0, 0, 45)
|
end = new Date(2021, 0, 1, 13, 0, 0, 45)
|
||||||
|
|
||||||
expect(time.getPeriod(start, end)).to.equal('45 ms')
|
expect(time.getPeriod(start, end)).to.equal('0.045s')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
67
tests/store/actions.spec.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { expect } from 'chai'
|
||||||
|
import actions from '@/store/actions'
|
||||||
|
|
||||||
|
const { addTab } = actions
|
||||||
|
|
||||||
|
describe('actions', () => {
|
||||||
|
it('addTab adds new blank tab', async () => {
|
||||||
|
const state = {
|
||||||
|
tabs: [],
|
||||||
|
untitledLastIndex: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = await addTab({ state })
|
||||||
|
expect(state.tabs[0].id).to.eql(id)
|
||||||
|
expect(state.tabs[0].name).to.eql(null)
|
||||||
|
expect(state.tabs[0].tempName).to.eql('Untitled')
|
||||||
|
expect(state.tabs[0].isUnsaved).to.eql(true)
|
||||||
|
expect(state.untitledLastIndex).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('addTab adds tab from saved queries', async () => {
|
||||||
|
const state = {
|
||||||
|
tabs: [],
|
||||||
|
untitledLastIndex: 0
|
||||||
|
}
|
||||||
|
const tab = {
|
||||||
|
id: 1,
|
||||||
|
name: 'test',
|
||||||
|
tempName: null,
|
||||||
|
query: 'SELECT * from foo',
|
||||||
|
chart: {},
|
||||||
|
isUnsaved: false
|
||||||
|
}
|
||||||
|
await addTab({ state }, tab)
|
||||||
|
expect(state.tabs[0]).to.eql(tab)
|
||||||
|
expect(state.untitledLastIndex).to.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("addTab doesn't add anything when the query is already opened", async () => {
|
||||||
|
const tab1 = {
|
||||||
|
id: 1,
|
||||||
|
name: 'test',
|
||||||
|
tempName: null,
|
||||||
|
query: 'SELECT * from foo',
|
||||||
|
chart: {},
|
||||||
|
isUnsaved: false
|
||||||
|
}
|
||||||
|
|
||||||
|
const tab2 = {
|
||||||
|
id: 2,
|
||||||
|
name: 'bar',
|
||||||
|
tempName: null,
|
||||||
|
query: 'SELECT * from bar',
|
||||||
|
chart: {},
|
||||||
|
isUnsaved: false
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
tabs: [tab1, tab2],
|
||||||
|
untitledLastIndex: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
await addTab({ state }, tab1)
|
||||||
|
expect(state.tabs).to.have.lengthOf(2)
|
||||||
|
expect(state.untitledLastIndex).to.equal(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import { mutations, actions } from '@/store'
|
import mutations from '@/store/mutations'
|
||||||
const {
|
const {
|
||||||
saveSchema,
|
|
||||||
updateTab,
|
updateTab,
|
||||||
deleteTab,
|
deleteTab,
|
||||||
setCurrentTabId,
|
setCurrentTabId,
|
||||||
@@ -11,8 +10,6 @@ const {
|
|||||||
setDb
|
setDb
|
||||||
} = mutations
|
} = mutations
|
||||||
|
|
||||||
const { addTab } = actions
|
|
||||||
|
|
||||||
describe('mutations', () => {
|
describe('mutations', () => {
|
||||||
it('setDb', () => {
|
it('setDb', () => {
|
||||||
const state = {
|
const state = {
|
||||||
@@ -26,25 +23,6 @@ describe('mutations', () => {
|
|||||||
expect(oldDb.shutDown.calledOnce).to.equal(true)
|
expect(oldDb.shutDown.calledOnce).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('saveSchema', () => {
|
|
||||||
const state = {}
|
|
||||||
|
|
||||||
const schema = [
|
|
||||||
{
|
|
||||||
name: 'table1',
|
|
||||||
columns: [
|
|
||||||
{ name: 'id', type: 'INTEGER' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
saveSchema(state, {
|
|
||||||
dbName: 'test',
|
|
||||||
schema
|
|
||||||
})
|
|
||||||
expect(state.dbName).to.equal('test')
|
|
||||||
expect(state.schema).to.eql(schema)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('updateTab (save)', () => {
|
it('updateTab (save)', () => {
|
||||||
const tab = {
|
const tab = {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -376,66 +354,3 @@ describe('mutations', () => {
|
|||||||
expect(state.predefinedQueries).to.eql(queries)
|
expect(state.predefinedQueries).to.eql(queries)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('actions', () => {
|
|
||||||
it('addTab adds new blank tab', async () => {
|
|
||||||
const state = {
|
|
||||||
tabs: [],
|
|
||||||
untitledLastIndex: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = await addTab({ state })
|
|
||||||
expect(state.tabs[0].id).to.eql(id)
|
|
||||||
expect(state.tabs[0].name).to.eql(null)
|
|
||||||
expect(state.tabs[0].tempName).to.eql('Untitled')
|
|
||||||
expect(state.tabs[0].isUnsaved).to.eql(true)
|
|
||||||
expect(state.untitledLastIndex).to.equal(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('addTab adds tab from saved queries', async () => {
|
|
||||||
const state = {
|
|
||||||
tabs: [],
|
|
||||||
untitledLastIndex: 0
|
|
||||||
}
|
|
||||||
const tab = {
|
|
||||||
id: 1,
|
|
||||||
name: 'test',
|
|
||||||
tempName: null,
|
|
||||||
query: 'SELECT * from foo',
|
|
||||||
chart: {},
|
|
||||||
isUnsaved: false
|
|
||||||
}
|
|
||||||
await addTab({ state }, tab)
|
|
||||||
expect(state.tabs[0]).to.eql(tab)
|
|
||||||
expect(state.untitledLastIndex).to.equal(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("addTab doesn't add anything when the query is already opened", async () => {
|
|
||||||
const tab1 = {
|
|
||||||
id: 1,
|
|
||||||
name: 'test',
|
|
||||||
tempName: null,
|
|
||||||
query: 'SELECT * from foo',
|
|
||||||
chart: {},
|
|
||||||
isUnsaved: false
|
|
||||||
}
|
|
||||||
|
|
||||||
const tab2 = {
|
|
||||||
id: 2,
|
|
||||||
name: 'bar',
|
|
||||||
tempName: null,
|
|
||||||
query: 'SELECT * from bar',
|
|
||||||
chart: {},
|
|
||||||
isUnsaved: false
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
tabs: [tab1, tab2],
|
|
||||||
untitledLastIndex: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
await addTab({ state }, tab1)
|
|
||||||
expect(state.tabs).to.have.lengthOf(2)
|
|
||||||
expect(state.untitledLastIndex).to.equal(0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import tooltipMixin from '@/mixins/tooltips.js'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
|
|
||||||
describe('tooltips.js', () => {
|
describe('tooltipMixin.js', () => {
|
||||||
it('tooltip is hidden in initial', () => {
|
it('tooltip is hidden in initial', () => {
|
||||||
const component = {
|
const component = {
|
||||||
template: '<div :style="tooltipStyle"></div>',
|
template: '<div :style="tooltipStyle"></div>',
|
||||||
178
tests/views/MainView/Editor/Schema/Schema.spec.js
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import { expect } from 'chai'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
import { mount, createLocalVue } from '@vue/test-utils'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import Schema from '@/views/Main/Editor/Schema'
|
||||||
|
import TableDescription from '@/views/Main/Editor/Schema/TableDescription'
|
||||||
|
import database from '@/lib/database'
|
||||||
|
import fIo from '@/lib/utils/fileIo'
|
||||||
|
import csv from '@/components/CsvImport/csv'
|
||||||
|
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
localVue.use(Vuex)
|
||||||
|
|
||||||
|
describe('Schema.vue', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
sinon.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Renders DB name on initial', () => {
|
||||||
|
// mock store state
|
||||||
|
const state = {
|
||||||
|
db: {
|
||||||
|
dbName: 'fooDB'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const store = new Vuex.Store({ state })
|
||||||
|
|
||||||
|
// mout the component
|
||||||
|
const wrapper = mount(Schema, { store, localVue })
|
||||||
|
|
||||||
|
// check DB name and schema visibility
|
||||||
|
expect(wrapper.find('.db-name').text()).to.equal('fooDB')
|
||||||
|
expect(wrapper.find('.schema').isVisible()).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Schema visibility is toggled when click on DB name', async () => {
|
||||||
|
// mock store state
|
||||||
|
const state = {
|
||||||
|
db: {
|
||||||
|
dbName: 'fooDB'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const store = new Vuex.Store({ state })
|
||||||
|
|
||||||
|
// mout the component
|
||||||
|
const wrapper = mount(Schema, { store, localVue })
|
||||||
|
|
||||||
|
// click and check visibility
|
||||||
|
await wrapper.find('.db-name').trigger('click')
|
||||||
|
expect(wrapper.find('.schema').isVisible()).to.equal(false)
|
||||||
|
await wrapper.find('.db-name').trigger('click')
|
||||||
|
expect(wrapper.find('.schema').isVisible()).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Schema filter', async () => {
|
||||||
|
// mock store state
|
||||||
|
const state = {
|
||||||
|
db: {
|
||||||
|
dbName: 'fooDB',
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'INTEGER' },
|
||||||
|
{ name: 'title', type: 'NVARCHAR(24)' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bar',
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'INTEGER' },
|
||||||
|
{ name: 'price', type: 'INTEGER' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'foobar',
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'INTEGER' },
|
||||||
|
{ name: 'price', type: 'INTEGER' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const store = new Vuex.Store({ state })
|
||||||
|
|
||||||
|
// mount the component
|
||||||
|
const wrapper = mount(Schema, { store, localVue })
|
||||||
|
|
||||||
|
// apply filters and check the list of tables
|
||||||
|
await wrapper.find('#schema-filter input').setValue('foo')
|
||||||
|
let tables = wrapper.findAllComponents(TableDescription)
|
||||||
|
expect(tables).to.have.lengthOf(2)
|
||||||
|
expect(tables.at(0).vm.name).to.equal('foo')
|
||||||
|
expect(tables.at(1).vm.name).to.equal('foobar')
|
||||||
|
|
||||||
|
await wrapper.find('#schema-filter input').setValue('bar')
|
||||||
|
tables = wrapper.findAllComponents(TableDescription)
|
||||||
|
expect(tables).to.have.lengthOf(2)
|
||||||
|
expect(tables.at(0).vm.name).to.equal('bar')
|
||||||
|
expect(tables.at(1).vm.name).to.equal('foobar')
|
||||||
|
|
||||||
|
await wrapper.find('#schema-filter input').setValue('')
|
||||||
|
tables = wrapper.findAllComponents(TableDescription)
|
||||||
|
expect(tables).to.have.lengthOf(3)
|
||||||
|
expect(tables.at(0).vm.name).to.equal('foo')
|
||||||
|
expect(tables.at(1).vm.name).to.equal('bar')
|
||||||
|
expect(tables.at(2).vm.name).to.equal('foobar')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('exports db', async () => {
|
||||||
|
const state = {
|
||||||
|
db: {
|
||||||
|
dbName: 'fooDB',
|
||||||
|
export: sinon.stub().resolves()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const store = new Vuex.Store({ state })
|
||||||
|
const wrapper = mount(Schema, { store, localVue })
|
||||||
|
|
||||||
|
await wrapper.findComponent({ name: 'export-icon' }).find('svg').trigger('click')
|
||||||
|
expect(state.db.export.calledOnceWith('fooDB'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds table', async () => {
|
||||||
|
const file = { name: 'test.csv' }
|
||||||
|
sinon.stub(fIo, 'getFileFromUser').resolves(file)
|
||||||
|
|
||||||
|
sinon.stub(csv, 'parse').resolves({
|
||||||
|
delimiter: '|',
|
||||||
|
data: {
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [
|
||||||
|
[1, 'foo']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasErrors: false,
|
||||||
|
messages: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
db: database.getNewDatabase()
|
||||||
|
}
|
||||||
|
state.db.dbName = 'db'
|
||||||
|
state.db.execute('CREATE TABLE foo(id)')
|
||||||
|
state.db.refreshSchema()
|
||||||
|
sinon.spy(state.db, 'refreshSchema')
|
||||||
|
|
||||||
|
const store = new Vuex.Store({ state })
|
||||||
|
const wrapper = mount(Schema, { store, localVue })
|
||||||
|
sinon.spy(wrapper.vm.$refs.addCsv, 'previewCsv')
|
||||||
|
sinon.spy(wrapper.vm, 'addCsv')
|
||||||
|
sinon.spy(wrapper.vm.$refs.addCsv, 'loadFromCsv')
|
||||||
|
|
||||||
|
await wrapper.findComponent({ name: 'add-table-icon' }).find('svg').trigger('click')
|
||||||
|
await wrapper.vm.$refs.addCsv.previewCsv.returnValues[0]
|
||||||
|
await wrapper.vm.addCsv.returnValues[0]
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(true)
|
||||||
|
await wrapper.find('#csv-import').trigger('click')
|
||||||
|
await wrapper.vm.$refs.addCsv.loadFromCsv.returnValues[0]
|
||||||
|
await wrapper.find('#csv-finish').trigger('click')
|
||||||
|
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
|
||||||
|
await state.db.refreshSchema.returnValues[0]
|
||||||
|
|
||||||
|
expect(wrapper.vm.$store.state.db.schema).to.eql([
|
||||||
|
{ name: 'test', columns: [{ name: 'col1', type: 'real' }, { name: 'col2', type: 'text' }] },
|
||||||
|
{ name: 'foo', columns: [{ name: 'id', type: 'N/A' }] }
|
||||||
|
])
|
||||||
|
|
||||||
|
const res = await wrapper.vm.$store.state.db.execute('select * from test')
|
||||||
|
expect(res).to.eql({
|
||||||
|
columns: ['col1', 'col2'],
|
||||||
|
values: [[1, 'foo']]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { shallowMount } from '@vue/test-utils'
|
import { shallowMount } from '@vue/test-utils'
|
||||||
import TableDescription from '@/components/TableDescription.vue'
|
import TableDescription from '@/views/Main/Editor/Schema/TableDescription'
|
||||||
|
|
||||||
describe('TableDescription.vue', () => {
|
describe('TableDescription.vue', () => {
|
||||||
it('Initially the columns are hidden and table name is rendered', () => {
|
it('Initially the columns are hidden and table name is rendered', () => {
|
||||||
@@ -19,6 +19,7 @@ describe('TableDescription.vue', () => {
|
|||||||
|
|
||||||
it('Columns are visible and correct when click on table name', async () => {
|
it('Columns are visible and correct when click on table name', async () => {
|
||||||
const wrapper = shallowMount(TableDescription, {
|
const wrapper = shallowMount(TableDescription, {
|
||||||
|
stubs: ['router-link'],
|
||||||
propsData: {
|
propsData: {
|
||||||
name: 'Test table',
|
name: 'Test table',
|
||||||
columns: [
|
columns: [
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import { mount, shallowMount } from '@vue/test-utils'
|
import { mount, shallowMount } from '@vue/test-utils'
|
||||||
import Chart from '@/components/Chart.vue'
|
import Chart from '@/views/Main/Editor/Tabs/Tab/Chart'
|
||||||
import chart from '@/chart.js'
|
import chartHelper from '@/views/Main/Editor/Tabs/Tab/Chart/chartHelper'
|
||||||
import * as dereference from 'react-chart-editor/lib/lib/dereference'
|
import * as dereference from 'react-chart-editor/lib/lib/dereference'
|
||||||
|
|
||||||
describe('Chart.vue', () => {
|
describe('Chart.vue', () => {
|
||||||
@@ -14,7 +14,7 @@ describe('Chart.vue', () => {
|
|||||||
// mount the component
|
// mount the component
|
||||||
const wrapper = shallowMount(Chart)
|
const wrapper = shallowMount(Chart)
|
||||||
const vm = wrapper.vm
|
const vm = wrapper.vm
|
||||||
const stub = sinon.stub(chart, 'getChartStateForSave').returns('result')
|
const stub = sinon.stub(chartHelper, 'getChartStateForSave').returns('result')
|
||||||
const chartData = vm.getChartStateForSave()
|
const chartData = vm.getChartStateForSave()
|
||||||
expect(stub.calledOnceWith(vm.state, vm.dataSources)).to.equal(true)
|
expect(stub.calledOnceWith(vm.state, vm.dataSources)).to.equal(true)
|
||||||
expect(chartData).to.equal('result')
|
expect(chartData).to.equal('result')
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import * as chart from '@/chart'
|
import * as chartHelper from '@/views/Main/Editor/Tabs/Tab/Chart/chartHelper'
|
||||||
import * as dereference from 'react-chart-editor/lib/lib/dereference'
|
import * as dereference from 'react-chart-editor/lib/lib/dereference'
|
||||||
|
|
||||||
describe('chart.js', () => {
|
describe('chartHelper.js', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
sinon.restore()
|
sinon.restore()
|
||||||
})
|
})
|
||||||
@@ -17,7 +17,7 @@ describe('chart.js', () => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const ds = chart.getDataSourcesFromSqlResult(sqlResult)
|
const ds = chartHelper.getDataSourcesFromSqlResult(sqlResult)
|
||||||
expect(ds).to.eql({
|
expect(ds).to.eql({
|
||||||
id: [1, 2],
|
id: [1, 2],
|
||||||
name: ['foo', 'bar']
|
name: ['foo', 'bar']
|
||||||
@@ -30,7 +30,7 @@ describe('chart.js', () => {
|
|||||||
name: ['foo', 'bar']
|
name: ['foo', 'bar']
|
||||||
}
|
}
|
||||||
|
|
||||||
const ds = chart.getOptionsFromDataSources(dataSources)
|
const ds = chartHelper.getOptionsFromDataSources(dataSources)
|
||||||
expect(ds).to.eql([
|
expect(ds).to.eql([
|
||||||
{ value: 'id', label: 'id' },
|
{ value: 'id', label: 'id' },
|
||||||
{ value: 'name', label: 'name' }
|
{ value: 'name', label: 'name' }
|
||||||
@@ -53,7 +53,7 @@ describe('chart.js', () => {
|
|||||||
sinon.stub(dereference, 'default')
|
sinon.stub(dereference, 'default')
|
||||||
sinon.spy(JSON, 'parse')
|
sinon.spy(JSON, 'parse')
|
||||||
|
|
||||||
const ds = chart.getChartStateForSave(state, dataSources)
|
const ds = chartHelper.getChartStateForSave(state, dataSources)
|
||||||
|
|
||||||
expect(dereference.default.calledOnce).to.equal(true)
|
expect(dereference.default.calledOnce).to.equal(true)
|
||||||
|
|
||||||