Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 notes\n\n",
|
||||
release: "{{body}}",
|
||||
},
|
||||
groupBy: {
|
||||
'Enhancements:': ["enhancement", "internal"],
|
||||
'Bug fixes:': ["bug"]
|
||||
}
|
||||
}
|
||||
20
.github/workflows/main.yml
vendored
@@ -25,16 +25,26 @@ jobs:
|
||||
cd dist
|
||||
zip -9 -r dist.zip . -x "js/*.map"
|
||||
|
||||
- name: Create Release Notes
|
||||
run: |
|
||||
npm install github-release-notes@0.16.0 -g
|
||||
gren changelog --generate --config="/.github/workflows/config.grenrc.js"
|
||||
env:
|
||||
GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "dist/dist.zip"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
bodyFile: "CHANGELOG.md"
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@3.6.2
|
||||
uses: JamesIves/github-pages-deploy-action@4.1.1
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BRANCH: build # The branch the action should deploy to.
|
||||
FOLDER: dist/ # The folder the action should deploy.
|
||||
CLEAN: false # Automatically remove deleted files from the deploy branch
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: build # The branch the action should deploy to.
|
||||
folder: dist/ # The folder the action should deploy.
|
||||
clean: true # Automatically remove deleted files from the deploy branch
|
||||
clean-exclude: .nojekyll
|
||||
|
||||
|
||||
9
.github/workflows/test.yml
vendored
@@ -15,10 +15,11 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
- name: Install chromium
|
||||
run:
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get update &&
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y chromium-browser
|
||||
- name: Install browsers
|
||||
run: |
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y chromium-browser firefox
|
||||
|
||||
- name: Install the project
|
||||
run: npm install
|
||||
|
||||
163
README.md
@@ -4,153 +4,23 @@
|
||||
|
||||
# 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`.
|
||||
|
||||
<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.
|
||||
## Wiki
|
||||
For user documentation, check out sqliteviz [Wiki][7].
|
||||
|
||||
## Motivation
|
||||
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
|
||||
[4]: https://github.com/sql-js/sql.js
|
||||
[5]: https://github.com/vuejs/vue
|
||||
[6]: https://lana-k.github.io/sqliteviz
|
||||
[7]: https://plotly.com/chart-studio-help/tutorials/#basic
|
||||
[6]: https://lana-k.github.io/sqliteviz/
|
||||
[7]: https://github.com/lana-k/sqliteviz/wiki
|
||||
[8]: https://github.com/surmon-china/vue-codemirror#readme
|
||||
[9]: https://www.papaparse.com/
|
||||
|
||||
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries
|
||||
[11]: https://github.com/plotly/plotly.js
|
||||
|
||||
@@ -76,9 +76,17 @@ module.exports = function (config) {
|
||||
// enable / disable watching file and executing tests whenever any file changes
|
||||
autoWatch: false,
|
||||
|
||||
customLaunchers: {
|
||||
FirefoxHeadlessTouch: {
|
||||
base: 'FirefoxHeadless',
|
||||
prefs: {
|
||||
'dom.w3c_touch_events.enabled': 1
|
||||
}
|
||||
}
|
||||
},
|
||||
// start these browsers
|
||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||
browsers: ['ChromiumHeadless'],
|
||||
browsers: ['ChromiumHeadless', 'FirefoxHeadlessTouch'],
|
||||
|
||||
// Continuous Integration mode
|
||||
// if true, Karma captures browsers, runs the tests and exits
|
||||
@@ -86,10 +94,13 @@ module.exports = function (config) {
|
||||
|
||||
// Concurrency level
|
||||
// how many browser should be started simultaneous
|
||||
concurrency: Infinity,
|
||||
concurrency: 2,
|
||||
|
||||
client: {
|
||||
captureConsole: true
|
||||
captureConsole: true,
|
||||
mocha: {
|
||||
timeout: 7000
|
||||
}
|
||||
},
|
||||
browserConsoleLogOptions: {
|
||||
terminal: true,
|
||||
@@ -130,7 +141,7 @@ module.exports = function (config) {
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.worker\.js$/,
|
||||
test: /worker\.js$/,
|
||||
loader: 'worker-loader'
|
||||
},
|
||||
{
|
||||
|
||||
4070
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sqliteviz",
|
||||
"version": "1.0.0",
|
||||
"version": "0.11.0",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -15,12 +15,12 @@
|
||||
"debounce": "^1.2.0",
|
||||
"nanoid": "^3.1.12",
|
||||
"papaparse": "^5.3.0",
|
||||
"plotly.js": "^1.57.1",
|
||||
"plotly.js": "^1.58.4",
|
||||
"promise-worker": "^2.0.1",
|
||||
"react": "^16.13.1",
|
||||
"react-chart-editor": "^0.42.0",
|
||||
"react-dom": "^16.13.1",
|
||||
"sql.js": "^1.3.0",
|
||||
"sql.js": "^1.5.0",
|
||||
"sqlite-parser": "^1.0.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue-codemirror": "^4.0.6",
|
||||
@@ -48,9 +48,11 @@
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"karma": "^3.1.4",
|
||||
"karma-firefox-launcher": "^2.1.0",
|
||||
"karma-webpack": "^4.0.2",
|
||||
"vue-cli-plugin-ui-karma": "^0.2.5",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"workbox-webpack-plugin": "^6.1.5",
|
||||
"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 name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.png">
|
||||
<link rel="manifest" href="<%= BASE_URL %>manifest.webmanifest">
|
||||
<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>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</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 -->
|
||||
</body>
|
||||
</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 {
|
||||
margin: 0;
|
||||
}
|
||||
.CodeMirror-hints {
|
||||
z-index: 999 !important;
|
||||
}
|
||||
</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 |
@@ -6,7 +6,6 @@
|
||||
padding: 0 6px;
|
||||
line-height: 19px;;
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
height: 19px;
|
||||
border-radius: var(--border-radius-medium);
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ascii from '@/ascii'
|
||||
import ascii from './ascii'
|
||||
import DropDownChevron from '@/components/svg/dropDownChevron'
|
||||
import ClearIcon from '@/components/svg/clear'
|
||||
|
||||
@@ -10,7 +10,7 @@ export default {
|
||||
getResult (source) {
|
||||
const result = {}
|
||||
if (source.meta.fields) {
|
||||
result.columns = source.meta.fields
|
||||
result.columns = source.meta.fields.map(col => col.trim())
|
||||
result.values = source.data.map(row => {
|
||||
const resultRow = []
|
||||
result.columns.forEach(col => { resultRow.push(row[col]) })
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="db-uploader-container">
|
||||
<div class="db-uploader-container" :style="{ width }">
|
||||
<change-db-icon v-if="type === 'small'" @click.native="browse"/>
|
||||
<div v-if="['regular', 'illustrated'].includes(type)" class="drop-area-container">
|
||||
<div v-if="type === 'illustrated'" class="drop-area-container">
|
||||
<div
|
||||
class="drop-area"
|
||||
@dragover.prevent="state = 'dragover'"
|
||||
@@ -26,7 +26,8 @@
|
||||
ref="fileImg"
|
||||
:class="{
|
||||
'swing': state === 'dragover',
|
||||
'fly': state === 'drop'
|
||||
'fly': state === 'dropping',
|
||||
'hidden': state === 'dropped'
|
||||
}"
|
||||
:src="require('@/assets/images/file.png')"
|
||||
/>
|
||||
@@ -127,17 +128,17 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import fu from '@/file.utils'
|
||||
import csv from '@/csv'
|
||||
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 DelimiterSelector from './DelimiterSelector'
|
||||
import CheckBox from '@/components/CheckBox'
|
||||
import SqlTable from '@/components/SqlTable'
|
||||
import Logs from '@/components/Logs'
|
||||
import ChangeDbIcon from '@/components/svg/changeDb'
|
||||
import time from '@/time'
|
||||
import database from '@/database'
|
||||
import time from '@/lib/utils/time'
|
||||
import database from '@/lib/database'
|
||||
|
||||
export default {
|
||||
name: 'DbUploader',
|
||||
@@ -145,10 +146,15 @@ export default {
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'regular',
|
||||
default: 'small',
|
||||
validator: (value) => {
|
||||
return ['regular', 'illustrated', 'small'].includes(value)
|
||||
return ['illustrated', 'small'].includes(value)
|
||||
}
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'unset'
|
||||
}
|
||||
},
|
||||
components: {
|
||||
@@ -182,6 +188,7 @@ export default {
|
||||
this.animationPromise = new Promise((resolve) => {
|
||||
this.$refs.fileImg.addEventListener('animationend', event => {
|
||||
if (event.animationName.startsWith('fly')) {
|
||||
this.state = 'dropped'
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
@@ -217,8 +224,16 @@ export default {
|
||||
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' })
|
||||
const stmt = [
|
||||
'/*',
|
||||
' * Your CSV file has been imported into csv_import table.',
|
||||
' * You can run this SQL query to make all CSV records available for charting.',
|
||||
' */',
|
||||
'SELECT * FROM csv_import'
|
||||
].join('\n')
|
||||
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
||||
this.$store.commit('setCurrentTabId', tabId)
|
||||
this.importCsvCompleted = false
|
||||
}
|
||||
if (this.$route.path !== '/editor') {
|
||||
this.$router.push('/editor')
|
||||
@@ -324,8 +339,9 @@ export default {
|
||||
}, 1000)
|
||||
|
||||
// Create db with csv table and get schema
|
||||
const name = file.name.replace(/\.[^.]+$/, '')
|
||||
start = new Date()
|
||||
this.schema = await this.newDb.createDb(file.name, parseResult.data, progressCounterId)
|
||||
this.schema = await this.newDb.importDb(name, parseResult.data, progressCounterId)
|
||||
end = new Date()
|
||||
|
||||
// Inform about import success
|
||||
@@ -364,8 +380,10 @@ export default {
|
||||
},
|
||||
|
||||
async checkFile (file) {
|
||||
this.state = 'drop'
|
||||
if (file.type === 'text/csv') {
|
||||
this.state = 'dropping'
|
||||
if (fIo.isDatabase(file)) {
|
||||
this.loadDb(file)
|
||||
} else {
|
||||
this.file = file
|
||||
this.header = true
|
||||
this.quoteChar = '"'
|
||||
@@ -375,12 +393,10 @@ export default {
|
||||
.then(() => {
|
||||
this.$modal.show('parse')
|
||||
})
|
||||
} else {
|
||||
this.loadDb(file)
|
||||
}
|
||||
},
|
||||
browse () {
|
||||
fu.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
|
||||
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
|
||||
.then(this.checkFile)
|
||||
},
|
||||
|
||||
@@ -416,6 +432,7 @@ export default {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#img-container {
|
||||
@@ -487,13 +504,19 @@ export default {
|
||||
#file-img.fly {
|
||||
animation: fly ease-in-out 1s 1 normal;
|
||||
transform-origin: center center;
|
||||
top: 183px;
|
||||
left: 225px;
|
||||
transition: top 1s ease-in-out, left 1s ease-in-out;
|
||||
}
|
||||
@keyframes fly {
|
||||
100% { transform: rotate(360deg) scale(0.5); }
|
||||
100% {
|
||||
transform: rotate(360deg) scale(0.5);
|
||||
top: 183px;
|
||||
left: 225px;
|
||||
}
|
||||
}
|
||||
|
||||
#file-img.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Parse CSV dialog */
|
||||
.chars {
|
||||
display: flex;
|
||||
@@ -1,17 +1,18 @@
|
||||
<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
|
||||
class="loader-svg bg"
|
||||
cx="10"
|
||||
cy="10"
|
||||
r="8"
|
||||
:style="{ strokeWidth }"
|
||||
:cx="size / 2"
|
||||
:cy="size / 2"
|
||||
:r="radius"
|
||||
/>
|
||||
<circle
|
||||
class="loader-svg front"
|
||||
:style="{ strokeDasharray: circleProgress }"
|
||||
cx="10"
|
||||
cy="10"
|
||||
r="8"
|
||||
:style="{ strokeDasharray: circleProgress, strokeDashoffset: offset, strokeWidth }"
|
||||
:cx="size / 2"
|
||||
:cy="size / 2"
|
||||
:r="radius"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -19,15 +20,35 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'LoadingIndicator',
|
||||
props: ['progress'],
|
||||
props: {
|
||||
progress: {
|
||||
type: Number,
|
||||
required: false
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 20
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
circleProgress () {
|
||||
const dash = (50.24 * this.progress) / 100
|
||||
const space = 50.24 - dash
|
||||
return `${dash}, ${space}`
|
||||
const circle = this.radius * 3.14 * 2
|
||||
const dash = this.progress ? (circle * this.progress) / 100 : circle * 1 / 3
|
||||
const space = circle - dash
|
||||
return `${dash}px, ${space}px`
|
||||
},
|
||||
animationClass () {
|
||||
return this.progress === undefined ? 'loading' : 'progress'
|
||||
},
|
||||
radius () {
|
||||
return this.size / 2 - this.strokeWidth
|
||||
},
|
||||
offset () {
|
||||
return this.radius * 3.14 / 2
|
||||
},
|
||||
strokeWidth () {
|
||||
return this.size / 10
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +59,6 @@ export default {
|
||||
position: absolute;
|
||||
left: 0; right: 0; top: 0; bottom: 0;
|
||||
fill: none;
|
||||
stroke-width: 2px;
|
||||
stroke-linecap: round;
|
||||
stroke: var(--color-accent);
|
||||
}
|
||||
@@ -48,27 +68,30 @@ export default {
|
||||
}
|
||||
|
||||
.loading .loader-svg.front {
|
||||
stroke-dasharray: 40.24;
|
||||
will-change: transform;
|
||||
animation: fill-animation-loading 1s cubic-bezier(1,1,1,1) 0s infinite;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
/*
|
||||
We can't change anything in loading animation except transform, opacity and filter. Because in
|
||||
our case the Main Thread can be busy and animation will be frozen (e. g. getting a result set
|
||||
from the web-worker after query execution).
|
||||
But transform, opacity and filter trigger changes only in the Composite Layer stage in rendering
|
||||
waterfall. Hence they can be processed only with Compositor Thread while the Main Thread
|
||||
processes something else.
|
||||
https://www.viget.com/articles/animation-performance-101-browser-under-the-hood/
|
||||
*/
|
||||
@keyframes fill-animation-loading {
|
||||
0% {
|
||||
stroke-dasharray: 10 40.24;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
50% {
|
||||
stroke-dasharray: 25.12;
|
||||
stroke-dashoffset: 25.12;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 10 40.24 ;
|
||||
stroke-dashoffset: 50.24;
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.progress .loader-svg.front {
|
||||
stroke-dashoffset: 12.56;
|
||||
transition: stroke-dasharray 0.2s;
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ export default {
|
||||
border: 1px solid var(--color-border-light);
|
||||
box-sizing: border-box;
|
||||
overflow-y: scroll;
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
.msg {
|
||||
padding: 16px 7px;
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import splitter from '@/splitter'
|
||||
import splitter from './splitter'
|
||||
|
||||
export default {
|
||||
name: 'Splitpanes',
|
||||
@@ -41,6 +41,7 @@
|
||||
<div class="table-footer-count">
|
||||
{{ dataSet.values.length}} {{dataSet.values.length === 1 ? 'row' : 'rows'}} retrieved
|
||||
<span v-if="preview">for preview</span>
|
||||
<span v-if="time">in {{ time }}</span>
|
||||
</div>
|
||||
<pager v-show="pageCount > 1" :page-count="pageCount" v-model="currentPage" />
|
||||
</div>
|
||||
@@ -48,12 +49,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Pager from '@/components/Pager'
|
||||
import Pager from './Pager'
|
||||
|
||||
export default {
|
||||
name: 'SqlTable',
|
||||
components: { Pager },
|
||||
props: ['dataSet', 'height', 'preview'],
|
||||
props: ['dataSet', 'time', 'height', 'preview'],
|
||||
data () {
|
||||
return {
|
||||
header: null,
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :class="['text-field-label', { error: errorMsg }, {'disabled': disabled}]">
|
||||
<div v-if="label" :class="['text-field-label', { error: errorMsg }, {'disabled': disabled}]">
|
||||
{{ label }}
|
||||
<hint-icon class="hint" v-if="hint" :hint="hint" :max-width="maxHintWidth || '149px'"/>
|
||||
</div>
|
||||
@@ -11,7 +11,7 @@
|
||||
:style="{ width: width }"
|
||||
:value="value"
|
||||
: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>
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
/>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Change database
|
||||
Load another database or CSV
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'changeDbIcon',
|
||||
|
||||
@@ -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"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Export query to file
|
||||
</span>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
{{ tooltip }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'ExportIcon',
|
||||
mixins: [tooltipMixin]
|
||||
mixins: [tooltipMixin],
|
||||
props: ['tooltip']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
display: block;
|
||||
margin: 0 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon:hover path {
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'HintIcon',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import initSqlJs from 'sql.js/dist/sql-wasm.js'
|
||||
import dbUtils from '@/db.utils'
|
||||
import dbUtils from './_statements'
|
||||
|
||||
let SQL = null
|
||||
const sqlModuleReady = initSqlJs().then(sqlModule => { SQL = sqlModule })
|
||||
@@ -1,5 +1,5 @@
|
||||
import registerPromiseWorker from 'promise-worker/register'
|
||||
import Sql from '@/sql'
|
||||
import Sql from './_sql'
|
||||
|
||||
const sqlReady = Sql.build()
|
||||
|
||||
@@ -23,7 +23,7 @@ function processMsg (sql) {
|
||||
|
||||
function onError (error) {
|
||||
return {
|
||||
error
|
||||
error: error.message
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import sqliteParser from 'sqlite-parser'
|
||||
import fu from '@/file.utils'
|
||||
import fu from '@/lib/utils/fileIo'
|
||||
// We can import workers like so because of worker-loader:
|
||||
// https://webpack.js.org/loaders/worker-loader/
|
||||
import Worker from '@/db.worker.js'
|
||||
import Worker from './_worker.js'
|
||||
|
||||
// Use promise-worker in order to turn worker into the promise based one:
|
||||
// https://github.com/nolanlawson/promise-worker
|
||||
@@ -50,7 +50,7 @@ class Database {
|
||||
delete this.importProgresses[id]
|
||||
}
|
||||
|
||||
async createDb (name, data, progressCounterId) {
|
||||
async importDb (name, data, progressCounterId) {
|
||||
const result = await this.pw.postMessage({
|
||||
action: 'import',
|
||||
columns: data.columns,
|
||||
@@ -59,21 +59,22 @@ class Database {
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
throw result.error
|
||||
throw new Error(result.error)
|
||||
}
|
||||
|
||||
return await this.getSchema(name)
|
||||
}
|
||||
|
||||
async loadDb (file) {
|
||||
const fileContent = await fu.readAsArrayBuffer(file)
|
||||
const fileContent = file ? await fu.readAsArrayBuffer(file) : null
|
||||
const res = await this.pw.postMessage({ action: 'open', buffer: fileContent })
|
||||
|
||||
if (res.error) {
|
||||
throw res.error
|
||||
throw new Error(res.error)
|
||||
}
|
||||
|
||||
return this.getSchema(file.name)
|
||||
const dbName = file ? file.name.replace(/\.[^.]+$/, '') : 'database'
|
||||
return this.getSchema(dbName)
|
||||
}
|
||||
|
||||
async getSchema (name) {
|
||||
@@ -85,12 +86,14 @@ class Database {
|
||||
const result = await this.execute(getSchemaSql)
|
||||
// Parse DDL statements to get column names and types
|
||||
const parsedSchema = []
|
||||
result.values.forEach(item => {
|
||||
parsedSchema.push({
|
||||
name: item[0],
|
||||
columns: getColumns(item[1])
|
||||
if (result && result.values) {
|
||||
result.values.forEach(item => {
|
||||
parsedSchema.push({
|
||||
name: item[0],
|
||||
columns: getColumns(item[1])
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Return db name and schema
|
||||
return {
|
||||
@@ -103,11 +106,20 @@ class Database {
|
||||
const results = await this.pw.postMessage({ action: 'exec', sql: commands })
|
||||
|
||||
if (results.error) {
|
||||
throw results.error
|
||||
throw new Error(results.error)
|
||||
}
|
||||
// if it was more than one select - take only the last one
|
||||
return results[results.length - 1]
|
||||
}
|
||||
|
||||
async export (fileName) {
|
||||
const data = await this.pw.postMessage({ action: 'export' })
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error)
|
||||
}
|
||||
fu.exportToFile(data, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
function getAst (sql) {
|
||||
@@ -115,10 +127,10 @@ function getAst (sql) {
|
||||
// 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')
|
||||
.replace(/(tokenize=[^,]+)"tokenchars=.+?"/, '$1')
|
||||
.replace(/(tokenize=[^,]+)"remove_diacritics=.+?"/, '$1')
|
||||
.replace(/(tokenize=[^,]+)"separators=.+?"/, '$1')
|
||||
.replace(/tokenize=.+?(,|\))/, 'tokenize=unicode61$1')
|
||||
|
||||
return sqliteParser(fixedSql)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { nanoid } from 'nanoid'
|
||||
import fu from '@/file.utils'
|
||||
import fu from '@/lib/utils/fileIo'
|
||||
|
||||
export default {
|
||||
getStoredQueries () {
|
||||
@@ -1,4 +1,11 @@
|
||||
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)
|
||||
},
|
||||
|
||||
exportToFile (str, fileName, type = 'octet/stream') {
|
||||
// Create downloader
|
||||
const downloader = document.createElement('a')
|
||||
7
src/lib/utils/time.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
getPeriod (start, end) {
|
||||
const diff = end.getTime() - start.getTime()
|
||||
const seconds = diff / 1000
|
||||
return seconds.toFixed(3) + 's'
|
||||
}
|
||||
}
|
||||
10
src/main.js
@@ -1,7 +1,7 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import App from '@/App.vue'
|
||||
import router from '@/router'
|
||||
import store from '@/store'
|
||||
import { VuePlugin } from 'vuera'
|
||||
import VModal from 'vue-js-modal'
|
||||
|
||||
@@ -12,6 +12,10 @@ import '@/assets/styles/dialogs.css'
|
||||
import '@/assets/styles/tooltips.css'
|
||||
import '@/assets/styles/messages.css'
|
||||
|
||||
if (!['localhost', '127.0.0.1'].includes(location.hostname)) {
|
||||
import('./registerServiceWorker') // eslint-disable-line no-unused-expressions
|
||||
}
|
||||
|
||||
Vue.use(VuePlugin)
|
||||
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
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
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'
|
||||
import Editor from '@/views/Main/Editor'
|
||||
import MyQueries from '@/views/Main/MyQueries'
|
||||
import Welcome from '@/views/Welcome'
|
||||
import Main from '@/views/Main'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
@@ -11,12 +11,12 @@ const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Welcome',
|
||||
component: Home
|
||||
component: Welcome
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'MainView',
|
||||
component: MainView,
|
||||
name: 'Main',
|
||||
component: Main,
|
||||
children: [
|
||||
{
|
||||
path: '/editor',
|
||||
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 Vuex from 'vuex'
|
||||
import { nanoid } from 'nanoid'
|
||||
import state from '@/store/state'
|
||||
import mutations from '@/store/mutations'
|
||||
import actions from '@/store/actions'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export const state = {
|
||||
schema: null,
|
||||
dbFile: null,
|
||||
dbName: null,
|
||||
tabs: [],
|
||||
currentTab: null,
|
||||
currentTabId: null,
|
||||
untitledLastIndex: 0,
|
||||
predefinedQueries: [],
|
||||
db: null
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
setDb (state, db) {
|
||||
if (state.db) {
|
||||
state.db.shutDown()
|
||||
}
|
||||
state.db = db
|
||||
},
|
||||
saveSchema (state, { dbName, schema }) {
|
||||
state.dbName = dbName
|
||||
state.schema = schema
|
||||
},
|
||||
|
||||
updateTab (state, { index, name, id, query, chart, isUnsaved }) {
|
||||
const tab = state.tabs[index]
|
||||
const oldId = tab.id
|
||||
|
||||
if (id && state.currentTabId === oldId) {
|
||||
state.currentTabId = id
|
||||
}
|
||||
|
||||
if (id) { tab.id = id }
|
||||
if (name) { tab.name = name }
|
||||
if (query) { tab.query = query }
|
||||
if (chart) { tab.chart = chart }
|
||||
if (isUnsaved !== undefined) { tab.isUnsaved = isUnsaved }
|
||||
if (!isUnsaved) {
|
||||
// Saved query is not predefined
|
||||
delete tab.isPredefined
|
||||
}
|
||||
|
||||
Vue.set(state.tabs, index, tab)
|
||||
},
|
||||
deleteTab (state, index) {
|
||||
// If closing tab is the current opened
|
||||
if (state.tabs[index].id === state.currentTabId) {
|
||||
if (index < state.tabs.length - 1) {
|
||||
state.currentTabId = state.tabs[index + 1].id
|
||||
} else if (index > 0) {
|
||||
state.currentTabId = state.tabs[index - 1].id
|
||||
} else {
|
||||
state.currentTabId = null
|
||||
state.currentTab = null
|
||||
state.untitledLastIndex = 0
|
||||
}
|
||||
}
|
||||
state.tabs.splice(index, 1)
|
||||
},
|
||||
setCurrentTabId (state, id) {
|
||||
state.currentTabId = id
|
||||
},
|
||||
setCurrentTab (state, tab) {
|
||||
state.currentTab = tab
|
||||
},
|
||||
updatePredefinedQueries (state, queries) {
|
||||
if (Array.isArray(queries)) {
|
||||
state.predefinedQueries = queries
|
||||
} else {
|
||||
state.predefinedQueries = [queries]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
async addTab ({ state }, data) {
|
||||
const tab = data ? JSON.parse(JSON.stringify(data)) : {}
|
||||
// If no data then create a new blank one...
|
||||
// No data.id means to create new tab, but not blank,
|
||||
// e.g. with 'select * from csv_import' query after csv import
|
||||
if (!data || !data.id) {
|
||||
tab.id = nanoid()
|
||||
tab.name = null
|
||||
tab.tempName = state.untitledLastIndex
|
||||
? `Untitled ${state.untitledLastIndex}`
|
||||
: 'Untitled'
|
||||
tab.isUnsaved = true
|
||||
} else {
|
||||
tab.isUnsaved = false
|
||||
}
|
||||
|
||||
// add new tab only if was not already opened
|
||||
if (!state.tabs.some(openedTab => openedTab.id === tab.id)) {
|
||||
state.tabs.push(tab)
|
||||
if (!tab.name) {
|
||||
state.untitledLastIndex += 1
|
||||
}
|
||||
}
|
||||
|
||||
return tab.id
|
||||
}
|
||||
}
|
||||
|
||||
export default new Vuex.Store({
|
||||
state,
|
||||
mutations,
|
||||
|
||||
63
src/store/mutations.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
export default {
|
||||
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]
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/store/state.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
schema: null,
|
||||
dbFile: null,
|
||||
dbName: null,
|
||||
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>
|
||||
@@ -5,10 +5,11 @@
|
||||
</div>
|
||||
<div id="db">
|
||||
<div @click="schemaVisible = !schemaVisible" class="db-name">
|
||||
<tree-chevron :expanded="schemaVisible"/>
|
||||
<tree-chevron v-show="schema.length > 0" :expanded="schemaVisible"/>
|
||||
{{ dbName }}
|
||||
</div>
|
||||
<db-uploader id="db-edit" type="small" />
|
||||
<db-uploader id="db-edit" type="small" />
|
||||
<export-icon tooltip="Export database" @click="exportToFile"/>
|
||||
</div>
|
||||
<div v-show="schemaVisible" class="schema">
|
||||
<table-description
|
||||
@@ -22,10 +23,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TableDescription from '@/components/TableDescription'
|
||||
import TableDescription from './TableDescription'
|
||||
import TextField from '@/components/TextField'
|
||||
import TreeChevron from '@/components/svg/treeChevron'
|
||||
import DbUploader from '@/components/DbUploader'
|
||||
import ExportIcon from '@/components/svg/export'
|
||||
|
||||
export default {
|
||||
name: 'Schema',
|
||||
@@ -33,7 +35,8 @@ export default {
|
||||
TableDescription,
|
||||
TextField,
|
||||
TreeChevron,
|
||||
DbUploader
|
||||
DbUploader,
|
||||
ExportIcon
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -56,6 +59,11 @@ export default {
|
||||
dbName () {
|
||||
return this.$store.state.dbName
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
exportToFile () {
|
||||
this.$store.state.db.export(`${this.dbName}.sqlite`)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -78,7 +86,7 @@ export default {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
box-sizing: border-box;
|
||||
background-image: linear-gradient(white 73%, transparent);;
|
||||
background-image: linear-gradient(white 73%, rgba(255, 255, 255, 0));
|
||||
z-index: 2;
|
||||
}
|
||||
.schema, .db-name {
|
||||
@@ -95,6 +103,11 @@ export default {
|
||||
|
||||
.db-name {
|
||||
cursor: pointer;
|
||||
margin-right: 6px;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.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 PlotlyEditor from 'react-chart-editor'
|
||||
import chart from '@/chart'
|
||||
import chartHelper from './chartHelper'
|
||||
import dereference from 'react-chart-editor/lib/lib/dereference'
|
||||
|
||||
export default {
|
||||
@@ -49,10 +49,10 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
dataSources () {
|
||||
return chart.getDataSourcesFromSqlResult(this.sqlResult)
|
||||
return chartHelper.getDataSourcesFromSqlResult(this.sqlResult)
|
||||
},
|
||||
dataSourceOptions () {
|
||||
return chart.getOptionsFromDataSources(this.dataSources)
|
||||
return chartHelper.getOptionsFromDataSources(this.dataSources)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -71,7 +71,7 @@ export default {
|
||||
this.$emit('update')
|
||||
},
|
||||
getChartStateForSave () {
|
||||
return chart.getChartStateForSave(this.state, this.dataSources)
|
||||
return chartHelper.getChartStateForSave(this.state, this.dataSources)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import CM from 'codemirror'
|
||||
import 'codemirror/addon/hint/show-hint.js'
|
||||
import 'codemirror/addon/hint/sql-hint.js'
|
||||
import store from '@/store'
|
||||
import { debounce } from 'debounce'
|
||||
|
||||
export function getHints (cm, options) {
|
||||
const token = cm.getTokenAt(cm.getCursor()).string.toUpperCase()
|
||||
@@ -25,21 +24,27 @@ const hintOptions = {
|
||||
}
|
||||
return tables
|
||||
},
|
||||
get defaultTable () {
|
||||
const schema = store.state.schema
|
||||
return schema && schema.length === 1 ? schema[0].name : null
|
||||
},
|
||||
completeSingle: false,
|
||||
completeOnSingleClick: true,
|
||||
alignWithWord: false
|
||||
}
|
||||
|
||||
export default {
|
||||
show: debounce(function (editor) {
|
||||
// Don't show autocomplete after a space or semicolon or in string literals
|
||||
const token = editor.getTokenAt(editor.getCursor())
|
||||
const ch = token.string.slice(-1)
|
||||
const tokenType = token.type
|
||||
if (tokenType === 'string' || !ch || ch === ' ' || ch === ';') {
|
||||
return
|
||||
}
|
||||
|
||||
CM.showHint(editor, getHints, hintOptions)
|
||||
}, 400)
|
||||
export function showHintOnDemand (editor) {
|
||||
CM.showHint(editor, getHints, hintOptions)
|
||||
}
|
||||
|
||||
export default function showHint (editor) {
|
||||
// Don't show autocomplete after a space or semicolon or in string literals
|
||||
const token = editor.getTokenAt(editor.getCursor())
|
||||
const ch = token.string.slice(-1)
|
||||
const tokenType = token.type
|
||||
if (tokenType === 'string' || !ch || ch === ' ' || ch === ';') {
|
||||
return
|
||||
}
|
||||
|
||||
CM.showHint(editor, getHints, hintOptions)
|
||||
}
|
||||
@@ -5,7 +5,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import hint from '@/hint'
|
||||
import showHint, { showHintOnDemand } from './hint'
|
||||
import { debounce } from 'debounce'
|
||||
import { codemirror } from 'vue-codemirror'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/mode/sql/sql.js'
|
||||
@@ -28,7 +29,8 @@ export default {
|
||||
lineNumbers: true,
|
||||
line: true,
|
||||
autofocus: true,
|
||||
autoRefresh: true
|
||||
autoRefresh: true,
|
||||
extraKeys: { 'Ctrl-Space': showHintOnDemand }
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -38,7 +40,7 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange: hint.show
|
||||
onChange: debounce(showHint, 400)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -21,7 +21,8 @@
|
||||
>
|
||||
Run your query and get results here
|
||||
</div>
|
||||
<div v-show="isGettingResults" class="table-preview result-in-progress">
|
||||
<div v-if="isGettingResults" class="table-preview result-in-progress">
|
||||
<loading-indicator :size="30"/>
|
||||
Fetching results...
|
||||
</div>
|
||||
<div
|
||||
@@ -30,10 +31,8 @@
|
||||
>
|
||||
No rows retrieved according to your query
|
||||
</div>
|
||||
<div v-show="error" class="table-preview error">
|
||||
{{ error }}
|
||||
</div>
|
||||
<sql-table v-if="result" :data-set="result" :height="tableViewHeight" />
|
||||
<logs v-if="error" :messages="[error]"/>
|
||||
<sql-table v-if="result" :data-set="result" :time="time" :height="tableViewHeight" />
|
||||
</div>
|
||||
<chart
|
||||
:visible="view === 'chart'"
|
||||
@@ -50,10 +49,13 @@
|
||||
|
||||
<script>
|
||||
import SqlTable from '@/components/SqlTable'
|
||||
import SqlEditor from '@/components/SqlEditor'
|
||||
import Splitpanes from '@/components/Splitpanes'
|
||||
import ViewSwitcher from '@/components/ViewSwitcher'
|
||||
import Chart from '@/components/Chart'
|
||||
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||
import SqlEditor from './SqlEditor'
|
||||
import ViewSwitcher from './ViewSwitcher'
|
||||
import Chart from './Chart'
|
||||
import Logs from '@/components/Logs'
|
||||
import time from '@/lib/utils/time'
|
||||
|
||||
export default {
|
||||
name: 'Tab',
|
||||
@@ -63,7 +65,9 @@ export default {
|
||||
SqlTable,
|
||||
Splitpanes,
|
||||
ViewSwitcher,
|
||||
Chart
|
||||
Chart,
|
||||
LoadingIndicator,
|
||||
Logs
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -73,7 +77,8 @@ export default {
|
||||
tableViewHeight: 0,
|
||||
isGettingResults: false,
|
||||
error: null,
|
||||
resizeObserver: null
|
||||
resizeObserver: null,
|
||||
time: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -108,10 +113,18 @@ export default {
|
||||
this.isGettingResults = true
|
||||
this.result = null
|
||||
this.error = null
|
||||
const state = this.$store.state
|
||||
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())
|
||||
const schema = await state.db.getSchema(state.dbName)
|
||||
this.$store.commit('saveSchema', schema)
|
||||
} catch (err) {
|
||||
this.error = err
|
||||
this.error = {
|
||||
type: 'error',
|
||||
message: err
|
||||
}
|
||||
}
|
||||
this.isGettingResults = false
|
||||
},
|
||||
@@ -180,11 +193,32 @@ export default {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.table-preview.error {
|
||||
color: var(--color-text-error);
|
||||
.result-in-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
will-change: opacity;
|
||||
/*
|
||||
We need to show loader in 1 sec after starting query execution. We can't do that with
|
||||
setTimeout because the main thread can be busy by getting a result set from the web worker.
|
||||
But we can use CSS animation for opacity. Opacity triggers changes only in the Composite Layer
|
||||
stage in rendering waterfall. Hence it can be processed only with Compositor Thread while
|
||||
the Main Thread processes a result set.
|
||||
https://www.viget.com/articles/animation-performance-101-browser-under-the-hood/
|
||||
*/
|
||||
animation: show-loader 1s linear 0s 1;
|
||||
}
|
||||
|
||||
.table-preview.error::first-letter {
|
||||
text-transform: capitalize;
|
||||
@keyframes show-loader {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
99% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -62,7 +62,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Tab from '@/components/Tab'
|
||||
import Tab from './Tab'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
|
||||
export default {
|
||||
68
src/views/Main/Editor/index.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<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'
|
||||
import database from '@/lib/database'
|
||||
import store from '@/store'
|
||||
|
||||
export default {
|
||||
name: 'Editor',
|
||||
components: {
|
||||
Schema,
|
||||
Splitpanes,
|
||||
Tabs
|
||||
},
|
||||
async beforeRouteEnter (to, from, next) {
|
||||
if (!store.state.schema) {
|
||||
const newDb = database.getNewDatabase()
|
||||
const newSchema = await newDb.loadDb()
|
||||
store.commit('setDb', newDb)
|
||||
store.commit('saveSchema', newSchema)
|
||||
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 store.dispatch('addTab', { query: stmt })
|
||||
store.commit('setCurrentTabId', tabId)
|
||||
}
|
||||
next()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.schema-tabs-splitter {
|
||||
height: 100%;
|
||||
background-color: var(--color-white);
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@
|
||||
<div>
|
||||
<router-link to="/editor">Editor</router-link>
|
||||
<router-link to="/my-queries">My queries</router-link>
|
||||
<a href="https://github.com/lana-k/sqliteviz/wiki" target="_blank">Help</a>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
@@ -62,7 +63,7 @@
|
||||
<script>
|
||||
import TextField from '@/components/TextField'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
import storedQueries from '@/storedQueries'
|
||||
import storedQueries from '@/lib/storedQueries'
|
||||
|
||||
export default {
|
||||
name: 'MainMenu',
|
||||
@@ -81,7 +81,10 @@
|
||||
<div class="icons-container">
|
||||
<rename-icon v-if="!query.isPredefined" @click="showRenameDialog(query.id)" />
|
||||
<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
|
||||
v-if="!query.isPredefined"
|
||||
@click="showDeleteDialog((new Set()).add(query.id))"
|
||||
@@ -138,16 +141,16 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RenameIcon from '@/components/svg/rename'
|
||||
import CopyIcon from '@/components/svg/copy'
|
||||
import RenameIcon from './svg/rename'
|
||||
import CopyIcon from './svg/copy'
|
||||
import ExportIcon from '@/components/svg/export'
|
||||
import DeleteIcon from '@/components/svg/delete'
|
||||
import DeleteIcon from './svg/delete'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
import TextField from '@/components/TextField'
|
||||
import CheckBox from '@/components/CheckBox'
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
import storedQueries from '@/storedQueries'
|
||||
import fu from '@/file.utils'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
import storedQueries from '@/lib/storedQueries'
|
||||
import fu from '@/lib/utils/fileIo'
|
||||
|
||||
export default {
|
||||
name: 'MyQueries',
|
||||
@@ -520,7 +523,7 @@ tbody tr:hover td {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
tbody tr:hover .icons-container {
|
||||
display: block;
|
||||
display: flex;
|
||||
}
|
||||
.dialog input {
|
||||
width: 100%;
|
||||
@@ -23,7 +23,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'CopyIcon',
|
||||
@@ -33,7 +33,7 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
display: block;
|
||||
margin: 0 12px;
|
||||
}
|
||||
.icon:hover path {
|
||||
@@ -23,7 +23,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'DeleteIcon',
|
||||
@@ -33,7 +33,7 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
display: block;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'RenameIcon',
|
||||
@@ -33,7 +33,7 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
display: block;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MainMenu from '@/components/MainMenu'
|
||||
import MainMenu from './MainMenu'
|
||||
import '@/assets/styles/scrollbars.css'
|
||||
|
||||
export default {
|
||||
name: 'MainView',
|
||||
name: 'Main',
|
||||
components: { MainMenu }
|
||||
}
|
||||
</script>
|
||||
@@ -4,8 +4,8 @@
|
||||
<div id="note">
|
||||
Sqliteviz is fully client-side. Your database never leaves your computer.
|
||||
</div>
|
||||
<button id ="skip" class="secondary" @click="$router.push('/editor')">
|
||||
Skip database loading
|
||||
<button id="skip" class="secondary" @click="$router.push('/editor')">
|
||||
Create empty database
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -14,7 +14,7 @@
|
||||
import DbUploader from '@/components/DbUploader'
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
name: 'Welcome',
|
||||
components: { DbUploader }
|
||||
}
|
||||
</script>
|
||||
@@ -2,32 +2,38 @@ import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import Vuex from 'vuex'
|
||||
import { shallowMount, mount } from '@vue/test-utils'
|
||||
import DbUploader from '@/components/DbUploader.vue'
|
||||
import fu from '@/file.utils'
|
||||
import database from '@/database'
|
||||
import csv from '@/csv'
|
||||
import DbUploader from '@/components/DbUploader'
|
||||
import fu from '@/lib/utils/fileIo'
|
||||
import database from '@/lib/database'
|
||||
import csv from '@/components/DbUploader/csv'
|
||||
|
||||
describe('DbUploader.vue', () => {
|
||||
let state = {}
|
||||
let mutations = {}
|
||||
let store = {}
|
||||
let place
|
||||
|
||||
beforeEach(() => {
|
||||
// mock store state and mutations
|
||||
state = {}
|
||||
mutations = {
|
||||
saveSchema: sinon.stub()
|
||||
saveSchema: sinon.stub(),
|
||||
setDb: sinon.stub()
|
||||
}
|
||||
store = new Vuex.Store({ state, mutations })
|
||||
|
||||
place = document.createElement('div')
|
||||
document.body.appendChild(place)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
place.remove()
|
||||
})
|
||||
|
||||
it('loads db on click and redirects to /editor', async () => {
|
||||
// mock getting a file from user
|
||||
const file = {}
|
||||
const file = { name: 'test.db' }
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock db loading
|
||||
@@ -43,15 +49,22 @@ describe('DbUploader.vue', () => {
|
||||
|
||||
// mount the component
|
||||
const wrapper = shallowMount(DbUploader, {
|
||||
attachTo: place,
|
||||
store,
|
||||
mocks: { $router, $route }
|
||||
mocks: { $router, $route },
|
||||
propsData: {
|
||||
type: 'illustrated'
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
|
||||
await db.loadDb.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(mutations.saveSchema.calledOnceWith(state, schema)).to.equal(true)
|
||||
expect($router.push.calledOnceWith('/editor')).to.equal(true)
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
it('loads db on drop and redirects to /editor', async () => {
|
||||
@@ -68,12 +81,16 @@ describe('DbUploader.vue', () => {
|
||||
|
||||
// mount the component
|
||||
const wrapper = shallowMount(DbUploader, {
|
||||
attachTo: place,
|
||||
store,
|
||||
mocks: { $router, $route }
|
||||
mocks: { $router, $route },
|
||||
propsData: {
|
||||
type: 'illustrated'
|
||||
}
|
||||
})
|
||||
|
||||
// mock a file dropped by a user
|
||||
const file = {}
|
||||
const file = { name: 'test.db' }
|
||||
const dropData = { dataTransfer: new DataTransfer() }
|
||||
Object.defineProperty(dropData.dataTransfer, 'files', {
|
||||
value: [file],
|
||||
@@ -83,13 +100,16 @@ describe('DbUploader.vue', () => {
|
||||
await wrapper.find('.drop-area').trigger('drop', dropData)
|
||||
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
|
||||
await db.loadDb.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(mutations.saveSchema.calledOnceWith(state, schema)).to.equal(true)
|
||||
expect($router.push.calledOnceWith('/editor')).to.equal(true)
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
it("doesn't redirect if already on /editor", async () => {
|
||||
// mock getting a file from user
|
||||
const file = {}
|
||||
const file = { name: 'test.db' }
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock db loading
|
||||
@@ -105,13 +125,20 @@ describe('DbUploader.vue', () => {
|
||||
|
||||
// mount the component
|
||||
const wrapper = shallowMount(DbUploader, {
|
||||
attachTo: place,
|
||||
store,
|
||||
mocks: { $router, $route }
|
||||
mocks: { $router, $route },
|
||||
propsData: {
|
||||
type: 'illustrated'
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await db.loadDb.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
expect($router.push.called).to.equal(false)
|
||||
wrapper.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -121,6 +148,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
let actions = {}
|
||||
const newTabId = 1
|
||||
let store = {}
|
||||
let place
|
||||
|
||||
// mock router
|
||||
const $router = { }
|
||||
@@ -131,7 +159,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
// mock getting a file from user
|
||||
sinon.stub(fu, 'getFileFromUser').resolves({ type: 'text/csv' })
|
||||
sinon.stub(fu, 'getFileFromUser').resolves({ type: 'text/csv', name: 'foo.csv' })
|
||||
|
||||
clock = sinon.useFakeTimers()
|
||||
|
||||
@@ -149,15 +177,24 @@ describe('DbUploader.vue import CSV', () => {
|
||||
|
||||
$router.push = sinon.stub()
|
||||
|
||||
place = document.createElement('div')
|
||||
document.body.appendChild(place)
|
||||
|
||||
// mount the component
|
||||
wrapper = mount(DbUploader, {
|
||||
attachTo: place,
|
||||
store,
|
||||
mocks: { $router, $route }
|
||||
mocks: { $router, $route },
|
||||
propsData: {
|
||||
type: 'illustrated'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
wrapper.destroy()
|
||||
place.remove()
|
||||
})
|
||||
|
||||
it('shows parse dialog if gets csv file', async () => {
|
||||
@@ -183,6 +220,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
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('"')
|
||||
@@ -218,6 +256,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: ',',
|
||||
@@ -327,6 +366,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
let resolveParsing
|
||||
parse.onCall(1).returns(new Promise(resolve => {
|
||||
@@ -394,6 +434,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
@@ -451,6 +492,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
@@ -510,6 +552,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
@@ -562,8 +605,9 @@ describe('DbUploader.vue import CSV', () => {
|
||||
|
||||
let resolveImport = sinon.stub()
|
||||
const newDb = {
|
||||
createDb: sinon.stub().resolves(new Promise(resolve => { resolveImport = resolve })),
|
||||
createProgressCounter: sinon.stub().returns(1)
|
||||
importDb: sinon.stub().resolves(new Promise(resolve => { resolveImport = resolve })),
|
||||
createProgressCounter: sinon.stub().returns(1),
|
||||
deleteProgressCounter: sinon.stub()
|
||||
}
|
||||
sinon.stub(database, 'getNewDatabase').returns(newDb)
|
||||
|
||||
@@ -571,6 +615,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
@@ -596,10 +641,11 @@ describe('DbUploader.vue import CSV', () => {
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
expect(newDb.importDb.getCall(0).args[0]).to.equal('foo') // file name
|
||||
|
||||
// After resolving - loading indicator is not shown
|
||||
await resolveImport()
|
||||
await newDb.createDb.returnValues[0]
|
||||
await newDb.importDb.returnValues[0]
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(false)
|
||||
@@ -618,7 +664,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
// we need to separate calles because messages will mutate
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
@@ -634,7 +680,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
|
||||
const schema = {}
|
||||
const newDb = {
|
||||
createDb: sinon.stub().resolves(schema),
|
||||
importDb: sinon.stub().resolves(schema),
|
||||
createProgressCounter: sinon.stub().returns(1),
|
||||
deleteProgressCounter: sinon.stub()
|
||||
}
|
||||
@@ -644,6 +690,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
@@ -678,7 +725,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
// we need to separate calles because messages will mutate
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
@@ -693,7 +740,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
})
|
||||
|
||||
const newDb = {
|
||||
createDb: sinon.stub().rejects(new Error('fail')),
|
||||
importDb: sinon.stub().rejects(new Error('fail')),
|
||||
createProgressCounter: sinon.stub().returns(1),
|
||||
deleteProgressCounter: sinon.stub()
|
||||
}
|
||||
@@ -703,6 +750,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
@@ -726,8 +774,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
})
|
||||
|
||||
it('import final', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
sinon.stub(csv, 'parse').resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
@@ -739,22 +786,9 @@ describe('DbUploader.vue import CSV', () => {
|
||||
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),
|
||||
importDb: sinon.stub().resolves(schema),
|
||||
createProgressCounter: sinon.stub().returns(1),
|
||||
deleteProgressCounter: sinon.stub()
|
||||
}
|
||||
@@ -764,6 +798,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
@@ -781,8 +816,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
})
|
||||
|
||||
it('import cancel', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
sinon.stub(csv, 'parse').resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
@@ -794,24 +828,12 @@ describe('DbUploader.vue import CSV', () => {
|
||||
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),
|
||||
importDb: sinon.stub().resolves(schema),
|
||||
createProgressCounter: sinon.stub().returns(1),
|
||||
deleteProgressCounter: sinon.stub()
|
||||
deleteProgressCounter: sinon.stub(),
|
||||
shutDown: sinon.stub()
|
||||
}
|
||||
sinon.stub(database, 'getNewDatabase').returns(newDb)
|
||||
|
||||
@@ -819,6 +841,7 @@ describe('DbUploader.vue import CSV', () => {
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
@@ -831,6 +854,53 @@ describe('DbUploader.vue import CSV', () => {
|
||||
expect(actions.addTab.called).to.equal(false)
|
||||
expect(mutations.setCurrentTabId.called).to.equal(false)
|
||||
expect($router.push.called).to.equal(false)
|
||||
expect(newDb.shutDown.calledOnce).to.equal(true)
|
||||
expect(wrapper.find('[data-modal="parse"]').exists()).to.equal(false)
|
||||
})
|
||||
|
||||
it("doesn't open new tab when load db after importing CSV", async () => {
|
||||
fu.getFileFromUser.onCall(0).resolves({ type: 'text/csv', name: 'foo.csv' })
|
||||
fu.getFileFromUser.onCall(1).resolves({ type: 'application/x-sqlite3', name: 'bar.sqlite3' })
|
||||
sinon.stub(csv, 'parse').resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: [
|
||||
[1, 'foo']
|
||||
]
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
const schema = {}
|
||||
const newDb = {
|
||||
importDb: sinon.stub().resolves(schema),
|
||||
createProgressCounter: sinon.stub().returns(1),
|
||||
deleteProgressCounter: sinon.stub(),
|
||||
loadDb: sinon.stub().resolves()
|
||||
}
|
||||
sinon.stub(database, 'getNewDatabase').returns(newDb)
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.animationPromise
|
||||
await wrapper.vm.$nextTick()
|
||||
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(actions.addTab.calledOnce).to.equal(true)
|
||||
await actions.addTab.returnValues[0]
|
||||
expect(mutations.setCurrentTabId.calledOnceWith(state, newTabId)).to.equal(true)
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await newDb.loadDb.returnValues[0]
|
||||
expect(actions.addTab.calledOnce).to.equal(true)
|
||||
expect(mutations.setCurrentTabId.calledOnce).to.equal(true)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import { mount, shallowMount } from '@vue/test-utils'
|
||||
import DelimiterSelector from '@/components/DelimiterSelector'
|
||||
import DelimiterSelector from '@/components/DbUploader/DelimiterSelector'
|
||||
|
||||
describe('DelimiterSelector', async () => {
|
||||
it('shows the name of value', async () => {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import csv from '@/csv'
|
||||
import csv from '@/components/DbUploader/csv'
|
||||
import Papa from 'papaparse'
|
||||
|
||||
describe('csv.js', () => {
|
||||
@@ -15,7 +15,7 @@ describe('csv.js', () => {
|
||||
{ id: 2, name: 'bar' }
|
||||
],
|
||||
meta: {
|
||||
fields: ['id', 'name']
|
||||
fields: ['id', 'name ']
|
||||
}
|
||||
}
|
||||
expect(csv.getResult(source)).to.eql({
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import LoadingIndicator from '@/components/LoadingIndicator.vue'
|
||||
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||
|
||||
describe('LoadingIndicator.vue', () => {
|
||||
it('Calculates animation class', async () => {
|
||||
@@ -19,6 +19,6 @@ describe('LoadingIndicator.vue', () => {
|
||||
})
|
||||
// The lendth of circle in the component is 50.24. If progress is 50% then resulting arc
|
||||
// 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 { shallowMount } from '@vue/test-utils'
|
||||
import Logs from '@/components/Logs.vue'
|
||||
import Logs from '@/components/Logs'
|
||||
|
||||
let place
|
||||
describe('Logs.vue', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import Splitpanes from '@/components/Splitpanes.vue'
|
||||
import Splitpanes from '@/components/Splitpanes'
|
||||
|
||||
describe('Splitpanes.vue', () => {
|
||||
it('renders correctly - vertical', () => {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import splitter from '@/splitter'
|
||||
import splitter from '@/components/Splitpanes/splitter'
|
||||
|
||||
describe('splitter.js', () => {
|
||||
afterEach(() => {
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Pager from '@/components/Pager.vue'
|
||||
import Pager from '@/components/SqlTable/Pager'
|
||||
|
||||
describe('Pager.vue', () => {
|
||||
afterEach(() => {
|
||||
@@ -2,14 +2,14 @@ import chai from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import initSqlJs from 'sql.js'
|
||||
import Sql from '@/sql'
|
||||
import Sql from '@/lib/database/_sql'
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
chai.should()
|
||||
|
||||
const getSQL = initSqlJs()
|
||||
|
||||
describe('sql.js', () => {
|
||||
describe('_sql.js', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from 'chai'
|
||||
import dbUtils from '@/db.utils'
|
||||
import dbUtils from '@/lib/database/_statements'
|
||||
|
||||
describe('db.utils.js', () => {
|
||||
describe('_statements.js', () => {
|
||||
it('generateChunks', () => {
|
||||
const arr = ['1', '2', '3', '4', '5']
|
||||
const size = 2
|
||||
@@ -2,7 +2,9 @@ import chai from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import initSqlJs from 'sql.js'
|
||||
import database from '@/database.js'
|
||||
import database from '@/lib/database'
|
||||
import fu from '@/lib/utils/fileIo'
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
chai.should()
|
||||
@@ -32,8 +34,10 @@ describe('database.js', () => {
|
||||
|
||||
const data = tempDb.export()
|
||||
const buffer = new Blob([data])
|
||||
buffer.name = 'foo.sqlite'
|
||||
|
||||
const { schema } = await db.loadDb(buffer)
|
||||
const { schema, dbName } = await db.loadDb(buffer)
|
||||
expect(dbName).to.equal('foo')
|
||||
expect(schema).to.have.lengthOf(1)
|
||||
expect(schema[0].name).to.equal('test')
|
||||
expect(schema[0].columns[0].name).to.equal('col1')
|
||||
@@ -58,6 +62,7 @@ describe('database.js', () => {
|
||||
|
||||
const data = tempDb.export()
|
||||
const buffer = new Blob([data])
|
||||
buffer.name = 'foo.sqlite'
|
||||
|
||||
const { schema } = await db.loadDb(buffer)
|
||||
expect(schema[0].name).to.equal('test_virtual')
|
||||
@@ -74,6 +79,7 @@ describe('database.js', () => {
|
||||
|
||||
const data = tempDb.export()
|
||||
const buffer = new Blob([data])
|
||||
buffer.name = 'foo.sqlite'
|
||||
|
||||
sinon.stub(db.pw, 'postMessage').resolves({ error: new Error('foo') })
|
||||
|
||||
@@ -97,6 +103,7 @@ describe('database.js', () => {
|
||||
|
||||
const data = tempDb.export()
|
||||
const buffer = new Blob([data])
|
||||
buffer.name = 'foo.sqlite'
|
||||
|
||||
await db.loadDb(buffer)
|
||||
const result = await db.execute('SELECT * from test limit 1; SELECT * from test;')
|
||||
@@ -124,6 +131,7 @@ describe('database.js', () => {
|
||||
|
||||
const data = tempDb.export()
|
||||
const buffer = new Blob([data])
|
||||
buffer.name = 'foo.sqlite'
|
||||
await db.loadDb(buffer)
|
||||
await expect(db.execute('SELECT * from foo')).to.be.rejectedWith(/^no such table: foo$/)
|
||||
})
|
||||
@@ -138,7 +146,7 @@ describe('database.js', () => {
|
||||
}
|
||||
const progressHandler = sinon.spy()
|
||||
const progressCounterId = db.createProgressCounter(progressHandler)
|
||||
const { dbName, schema } = await db.createDb('foo', data, progressCounterId)
|
||||
const { dbName, schema } = await db.importDb('foo', data, progressCounterId)
|
||||
expect(dbName).to.equal('foo')
|
||||
expect(schema).to.have.lengthOf(1)
|
||||
expect(schema[0].name).to.equal('csv_import')
|
||||
@@ -156,7 +164,7 @@ describe('database.js', () => {
|
||||
expect(progressHandler.secondCall.calledWith(100)).to.equal(true)
|
||||
})
|
||||
|
||||
it('createDb throws errors', async () => {
|
||||
it('importDb throws errors', async () => {
|
||||
const data = {
|
||||
columns: ['id', 'name'],
|
||||
values: [
|
||||
@@ -166,7 +174,7 @@ describe('database.js', () => {
|
||||
}
|
||||
const progressHandler = sinon.stub()
|
||||
const progressCounterId = db.createProgressCounter(progressHandler)
|
||||
await expect(db.createDb('foo', data, progressCounterId))
|
||||
await expect(db.importDb('foo', data, progressCounterId))
|
||||
.to.be.rejectedWith('column index out of range')
|
||||
})
|
||||
|
||||
@@ -204,4 +212,34 @@ describe('database.js', () => {
|
||||
db.deleteProgressCounter(firstId)
|
||||
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'])
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import storedQueries from '@/storedQueries.js'
|
||||
import fu from '@/file.utils'
|
||||
import storedQueries from '@/lib/storedQueries'
|
||||
import fu from '@/lib/utils/fileIo'
|
||||
|
||||
describe('storedQueries.js', () => {
|
||||
beforeEach(() => {
|
||||
@@ -1,8 +1,8 @@
|
||||
import { expect } from 'chai'
|
||||
import fu from '@/file.utils'
|
||||
import fu from '@/lib/utils/fileIo'
|
||||
import sinon from 'sinon'
|
||||
|
||||
describe('file.utils.js', () => {
|
||||
describe('fileIo.js', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
@@ -57,9 +57,9 @@ describe('file.utils.js', () => {
|
||||
expect(URL.revokeObjectURL.calledOnceWith(url)).to.equal(true)
|
||||
})
|
||||
|
||||
it('importFile', () => {
|
||||
it('importFile', async () => {
|
||||
const spyInput = document.createElement('input')
|
||||
sinon.spy(spyInput, 'click')
|
||||
sinon.stub(spyInput, 'click')
|
||||
|
||||
const blob = new Blob(['foo'])
|
||||
Object.defineProperty(spyInput, 'files', {
|
||||
@@ -71,14 +71,12 @@ describe('file.utils.js', () => {
|
||||
|
||||
setTimeout(() => { spyInput.dispatchEvent(new Event('change')) })
|
||||
|
||||
return fu.importFile()
|
||||
.then((data) => {
|
||||
expect(data).to.equal('foo')
|
||||
expect(document.createElement.calledOnceWith('input')).to.equal(true)
|
||||
expect(spyInput.type).to.equal('file')
|
||||
expect(spyInput.accept).to.equal('.json')
|
||||
expect(spyInput.click.calledOnce).to.equal(true)
|
||||
})
|
||||
const data = await fu.importFile()
|
||||
expect(data).to.equal('foo')
|
||||
expect(document.createElement.calledOnceWith('input')).to.equal(true)
|
||||
expect(spyInput.type).to.equal('file')
|
||||
expect(spyInput.accept).to.equal('.json')
|
||||
expect(spyInput.click.calledOnce).to.equal(true)
|
||||
})
|
||||
|
||||
it('readFile', () => {
|
||||
@@ -107,4 +105,27 @@ describe('file.utils.js', () => {
|
||||
const blob = new Blob(['foo'])
|
||||
await expect(fu.readAsArrayBuffer(blob)).to.be.rejectedWith('Problem parsing input file.')
|
||||
})
|
||||
|
||||
it('isDatabase', () => {
|
||||
let file = { type: 'application/vnd.sqlite3' }
|
||||
expect(fu.isDatabase(file)).to.equal(true)
|
||||
|
||||
file = { type: 'application/x-sqlite3' }
|
||||
expect(fu.isDatabase(file)).to.equal(true)
|
||||
|
||||
file = { type: '', name: 'test.db' }
|
||||
expect(fu.isDatabase(file)).to.equal(true)
|
||||
|
||||
file = { type: '', name: 'test.sqlite' }
|
||||
expect(fu.isDatabase(file)).to.equal(true)
|
||||
|
||||
file = { type: '', name: 'test.sqlite3' }
|
||||
expect(fu.isDatabase(file)).to.equal(true)
|
||||
|
||||
file = { type: '', name: 'test.csv' }
|
||||
expect(fu.isDatabase(file)).to.equal(false)
|
||||
|
||||
file = { type: 'text', name: 'test.db' }
|
||||
expect(fu.isDatabase(file)).to.equal(false)
|
||||
})
|
||||
})
|
||||
@@ -1,23 +1,23 @@
|
||||
import { expect } from 'chai'
|
||||
import time from '@/time'
|
||||
import time from '@/lib/utils/time'
|
||||
|
||||
describe('time.js', () => {
|
||||
it('getPeriod', () => {
|
||||
// 1.01.2021 13:00:00 000
|
||||
let start = new Date(2021, 0, 1, 13, 0, 0, 0)
|
||||
|
||||
// 3.01.2021 22:15:20 500
|
||||
let end = new Date(2021, 0, 3, 22, 15, 20, 500)
|
||||
// 1.01.2021 13:01:00 500
|
||||
let end = new Date(2021, 0, 1, 13, 1, 0, 500)
|
||||
|
||||
expect(time.getPeriod(start, end)).to.equal('2 d 9 h 15 m 20 s 500 ms')
|
||||
expect(time.getPeriod(start, end)).to.equal('60.500s')
|
||||
|
||||
// 1.01.2021 13:00:00 000
|
||||
start = new Date(2021, 0, 1, 13, 0, 0, 0)
|
||||
|
||||
// 1.01.2021 22:00:20 000
|
||||
end = new Date(2021, 0, 1, 22, 0, 20, 0)
|
||||
// 1.01.2021 13:00:20 500
|
||||
end = new Date(2021, 0, 1, 13, 0, 20, 500)
|
||||
|
||||
expect(time.getPeriod(start, end)).to.equal('9 h 20 s')
|
||||
expect(time.getPeriod(start, end)).to.equal('20.500s')
|
||||
|
||||
// 1.01.2021 13:00:00 000
|
||||
start = new Date(2021, 0, 1, 13, 0, 0, 0)
|
||||
@@ -25,6 +25,6 @@ describe('time.js', () => {
|
||||
// 1.01.2021 13:00:00 45
|
||||
end = new Date(2021, 0, 1, 13, 0, 0, 45)
|
||||
|
||||
expect(time.getPeriod(start, end)).to.equal('45 ms')
|
||||
expect(time.getPeriod(start, end)).to.equal('0.045s')
|
||||
})
|
||||
})
|
||||
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,6 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { mutations, actions } from '@/store'
|
||||
import mutations from '@/store/mutations'
|
||||
const {
|
||||
saveSchema,
|
||||
updateTab,
|
||||
@@ -11,8 +11,6 @@ const {
|
||||
setDb
|
||||
} = mutations
|
||||
|
||||
const { addTab } = actions
|
||||
|
||||
describe('mutations', () => {
|
||||
it('setDb', () => {
|
||||
const state = {
|
||||
@@ -376,66 +374,3 @@ describe('mutations', () => {
|
||||
expect(state.predefinedQueries).to.eql(queries)
|
||||
})
|
||||
})
|
||||
|
||||
describe('actions', () => {
|
||||
it('addTab adds new blank tab', async () => {
|
||||
const state = {
|
||||
tabs: [],
|
||||
untitledLastIndex: 0
|
||||
}
|
||||
|
||||
const id = await addTab({ state })
|
||||
expect(state.tabs[0].id).to.eql(id)
|
||||
expect(state.tabs[0].name).to.eql(null)
|
||||
expect(state.tabs[0].tempName).to.eql('Untitled')
|
||||
expect(state.tabs[0].isUnsaved).to.eql(true)
|
||||
expect(state.untitledLastIndex).to.equal(1)
|
||||
})
|
||||
|
||||
it('addTab adds tab from saved queries', async () => {
|
||||
const state = {
|
||||
tabs: [],
|
||||
untitledLastIndex: 0
|
||||
}
|
||||
const tab = {
|
||||
id: 1,
|
||||
name: 'test',
|
||||
tempName: null,
|
||||
query: 'SELECT * from foo',
|
||||
chart: {},
|
||||
isUnsaved: false
|
||||
}
|
||||
await addTab({ state }, tab)
|
||||
expect(state.tabs[0]).to.eql(tab)
|
||||
expect(state.untitledLastIndex).to.equal(0)
|
||||
})
|
||||
|
||||
it("addTab doesn't add anything when the query is already opened", async () => {
|
||||
const tab1 = {
|
||||
id: 1,
|
||||
name: 'test',
|
||||
tempName: null,
|
||||
query: 'SELECT * from foo',
|
||||
chart: {},
|
||||
isUnsaved: false
|
||||
}
|
||||
|
||||
const tab2 = {
|
||||
id: 2,
|
||||
name: 'bar',
|
||||
tempName: null,
|
||||
query: 'SELECT * from bar',
|
||||
chart: {},
|
||||
isUnsaved: false
|
||||
}
|
||||
|
||||
const state = {
|
||||
tabs: [tab1, tab2],
|
||||
untitledLastIndex: 0
|
||||
}
|
||||
|
||||
await addTab({ state }, tab1)
|
||||
expect(state.tabs).to.have.lengthOf(2)
|
||||
expect(state.untitledLastIndex).to.equal(0)
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,8 @@
|
||||
import { expect } from 'chai'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import tooltipMixin from '@/mixins/tooltips.js'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
describe('tooltips.js', () => {
|
||||
describe('tooltipMixin.js', () => {
|
||||
it('tooltip is hidden in initial', () => {
|
||||
const component = {
|
||||
template: '<div :style="tooltipStyle"></div>',
|
||||
@@ -2,8 +2,8 @@ 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'
|
||||
import Schema from '@/views/Main/Editor/Schema'
|
||||
import TableDescription from '@/views/Main/Editor/Schema/TableDescription'
|
||||
|
||||
const localVue = createLocalVue()
|
||||
localVue.use(Vuex)
|
||||
@@ -98,4 +98,18 @@ describe('Schema.vue', () => {
|
||||
expect(tables.at(1).vm.name).to.equal('bar')
|
||||
expect(tables.at(2).vm.name).to.equal('foobar')
|
||||
})
|
||||
|
||||
it('exports db', async () => {
|
||||
const state = {
|
||||
dbName: 'fooDB',
|
||||
db: {
|
||||
export: sinon.stub().resolves()
|
||||
}
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
const wrapper = mount(Schema, { store, localVue })
|
||||
|
||||
await wrapper.findComponent({ name: 'export-icon' }).trigger('click')
|
||||
expect(state.db.export.calledOnceWith('fooDB'))
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import TableDescription from '@/components/TableDescription.vue'
|
||||
import TableDescription from '@/views/Main/Editor/Schema/TableDescription'
|
||||
|
||||
describe('TableDescription.vue', () => {
|
||||
it('Initially the columns are hidden and table name is rendered', () => {
|
||||
@@ -19,6 +19,7 @@ describe('TableDescription.vue', () => {
|
||||
|
||||
it('Columns are visible and correct when click on table name', async () => {
|
||||
const wrapper = shallowMount(TableDescription, {
|
||||
stubs: ['router-link'],
|
||||
propsData: {
|
||||
name: 'Test table',
|
||||
columns: [
|
||||
@@ -1,8 +1,8 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { mount, shallowMount } from '@vue/test-utils'
|
||||
import Chart from '@/components/Chart.vue'
|
||||
import chart from '@/chart.js'
|
||||
import Chart from '@/views/Main/Editor/Tabs/Tab/Chart'
|
||||
import chartHelper from '@/views/Main/Editor/Tabs/Tab/Chart/chartHelper'
|
||||
import * as dereference from 'react-chart-editor/lib/lib/dereference'
|
||||
|
||||
describe('Chart.vue', () => {
|
||||
@@ -14,7 +14,7 @@ describe('Chart.vue', () => {
|
||||
// mount the component
|
||||
const wrapper = shallowMount(Chart)
|
||||
const vm = wrapper.vm
|
||||
const stub = sinon.stub(chart, 'getChartStateForSave').returns('result')
|
||||
const stub = sinon.stub(chartHelper, 'getChartStateForSave').returns('result')
|
||||
const chartData = vm.getChartStateForSave()
|
||||
expect(stub.calledOnceWith(vm.state, vm.dataSources)).to.equal(true)
|
||||
expect(chartData).to.equal('result')
|
||||
@@ -1,9 +1,9 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import * as chart from '@/chart'
|
||||
import * as chartHelper from '@/views/Main/Editor/Tabs/Tab/Chart/chartHelper'
|
||||
import * as dereference from 'react-chart-editor/lib/lib/dereference'
|
||||
|
||||
describe('chart.js', () => {
|
||||
describe('chartHelper.js', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
@@ -17,7 +17,7 @@ describe('chart.js', () => {
|
||||
]
|
||||
}
|
||||
|
||||
const ds = chart.getDataSourcesFromSqlResult(sqlResult)
|
||||
const ds = chartHelper.getDataSourcesFromSqlResult(sqlResult)
|
||||
expect(ds).to.eql({
|
||||
id: [1, 2],
|
||||
name: ['foo', 'bar']
|
||||
@@ -30,7 +30,7 @@ describe('chart.js', () => {
|
||||
name: ['foo', 'bar']
|
||||
}
|
||||
|
||||
const ds = chart.getOptionsFromDataSources(dataSources)
|
||||
const ds = chartHelper.getOptionsFromDataSources(dataSources)
|
||||
expect(ds).to.eql([
|
||||
{ value: 'id', label: 'id' },
|
||||
{ value: 'name', label: 'name' }
|
||||
@@ -53,7 +53,7 @@ describe('chart.js', () => {
|
||||
sinon.stub(dereference, 'default')
|
||||
sinon.spy(JSON, 'parse')
|
||||
|
||||
const ds = chart.getChartStateForSave(state, dataSources)
|
||||
const ds = chartHelper.getChartStateForSave(state, dataSources)
|
||||
|
||||
expect(dereference.default.calledOnce).to.equal(true)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { expect } from 'chai'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import SqlEditor from '@/components/SqlEditor.vue'
|
||||
import SqlEditor from '@/views/Main/Editor/Tabs/Tab/SqlEditor'
|
||||
|
||||
describe('SqlEditor.vue', () => {
|
||||
it('Emits input event when a query is changed', async () => {
|
||||
const wrapper = mount(SqlEditor)
|
||||
await wrapper.findComponent({ name: 'codemirror' }).vm.$emit('input', 'SELECT * FROM foo')
|
||||
expect(wrapper.emitted('input')[0]).to.eql(['SELECT * FROM foo'])
|
||||
// Take a pause to keep proper state in debounced '@/views/Main/Editor/Tabs/Tab/SqlEditor/hint'
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { state } from '@/store'
|
||||
import hint, { getHints } from '@/hint'
|
||||
import state from '@/store/state'
|
||||
import showHint, { getHints } from '@/views/Main/Editor/Tabs/Tab/SqlEditor/hint'
|
||||
import CM from 'codemirror'
|
||||
|
||||
describe('hint.js', () => {
|
||||
@@ -40,15 +40,43 @@ describe('hint.js', () => {
|
||||
getCursor: sinon.stub()
|
||||
}
|
||||
|
||||
const clock = sinon.useFakeTimers()
|
||||
hint.show(editor)
|
||||
clock.tick(500)
|
||||
showHint(editor)
|
||||
|
||||
expect(CM.showHint.called).to.equal(true)
|
||||
expect(CM.showHint.firstCall.args[2].tables).to.eql({
|
||||
foo: ['fooId', 'name'],
|
||||
bar: ['barId']
|
||||
})
|
||||
expect(CM.showHint.firstCall.args[2].defaultTable).to.equal(null)
|
||||
})
|
||||
|
||||
it('Add default table if there is only one table in schema', () => {
|
||||
// mock store state
|
||||
const schema = [
|
||||
{
|
||||
name: 'foo',
|
||||
columns: [
|
||||
{ name: 'fooId', type: 'INTEGER' },
|
||||
{ name: 'name', type: 'NVARCHAR(20)' }
|
||||
]
|
||||
}
|
||||
]
|
||||
sinon.stub(state, 'schema').value(schema)
|
||||
|
||||
// mock showHint and editor
|
||||
sinon.stub(CM, 'showHint')
|
||||
const editor = {
|
||||
getTokenAt () {
|
||||
return {
|
||||
string: 'SELECT',
|
||||
type: 'keyword'
|
||||
}
|
||||
},
|
||||
getCursor: sinon.stub()
|
||||
}
|
||||
|
||||
showHint(editor)
|
||||
expect(CM.showHint.firstCall.args[2].defaultTable).to.equal('foo')
|
||||
})
|
||||
|
||||
it("Doesn't show hint when in string or space, or ';'", () => {
|
||||
@@ -64,10 +92,7 @@ describe('hint.js', () => {
|
||||
getCursor: sinon.stub()
|
||||
}
|
||||
|
||||
const clock = sinon.useFakeTimers()
|
||||
hint.show(editor)
|
||||
clock.tick(500)
|
||||
|
||||
showHint(editor)
|
||||
expect(CM.showHint.called).to.equal(false)
|
||||
})
|
||||
|
||||
@@ -84,10 +109,7 @@ describe('hint.js', () => {
|
||||
getCursor: sinon.stub()
|
||||
}
|
||||
|
||||
const clock = sinon.useFakeTimers()
|
||||
hint.show(editor)
|
||||
clock.tick(500)
|
||||
|
||||
showHint(editor)
|
||||
expect(CM.showHint.called).to.equal(false)
|
||||
})
|
||||
|
||||
@@ -104,10 +126,7 @@ describe('hint.js', () => {
|
||||
getCursor: sinon.stub()
|
||||
}
|
||||
|
||||
const clock = sinon.useFakeTimers()
|
||||
hint.show(editor)
|
||||
clock.tick(500)
|
||||
|
||||
showHint(editor)
|
||||
expect(CM.showHint.called).to.equal(false)
|
||||
})
|
||||
|
||||
@@ -185,10 +204,7 @@ describe('hint.js', () => {
|
||||
getCursor: sinon.stub()
|
||||
}
|
||||
|
||||
const clock = sinon.useFakeTimers()
|
||||
hint.show(editor)
|
||||
clock.tick(500)
|
||||
|
||||
showHint(editor)
|
||||
expect(CM.showHint.called).to.equal(true)
|
||||
expect(CM.showHint.firstCall.args[2].tables).to.eql({})
|
||||
})
|
||||
@@ -1,11 +1,15 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { mutations } from '@/store'
|
||||
import mutations from '@/store/mutations'
|
||||
import Vuex from 'vuex'
|
||||
import Tab from '@/components/Tab.vue'
|
||||
import Tab from '@/views/Main/Editor/Tabs/Tab'
|
||||
|
||||
describe('Tab.vue', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('Renders passed query', () => {
|
||||
// mock store state
|
||||
const state = {
|
||||
@@ -142,7 +146,7 @@ describe('Tab.vue', () => {
|
||||
expect(state.tabs[0].isUnsaved).to.equal(true)
|
||||
})
|
||||
|
||||
it('Shows .result-in-progress message when executing query', (done) => {
|
||||
it('Shows .result-in-progress message when executing query', async () => {
|
||||
// mock store state
|
||||
const state = {
|
||||
currentTabId: 1,
|
||||
@@ -167,11 +171,9 @@ describe('Tab.vue', () => {
|
||||
})
|
||||
|
||||
wrapper.vm.execute()
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.find('.table-view .result-before').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('.table-view .result-in-progress').isVisible()).to.equal(true)
|
||||
})
|
||||
done()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('.table-view .result-before').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('.table-view .result-in-progress').isVisible()).to.equal(true)
|
||||
})
|
||||
|
||||
it('Shows error when executing query ends with error', async () => {
|
||||
@@ -179,7 +181,7 @@ describe('Tab.vue', () => {
|
||||
const state = {
|
||||
currentTabId: 1,
|
||||
db: {
|
||||
execute () { return Promise.reject(new Error('There is no table foo')) }
|
||||
execute: sinon.stub().rejects(new Error('There is no table foo'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,21 +202,12 @@ describe('Tab.vue', () => {
|
||||
|
||||
await wrapper.vm.execute()
|
||||
expect(wrapper.find('.table-view .result-before').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('.table-view .result-in-progress').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('.table-preview.error').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('.table-preview.error').text()).to.include('There is no table foo')
|
||||
expect(wrapper.find('.table-view .result-in-progress').exists()).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'logs' }).isVisible()).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text()).to.include('There is no table foo')
|
||||
})
|
||||
|
||||
it('Passes result to sql-table component', async () => {
|
||||
// mock store state
|
||||
const state = {
|
||||
currentTabId: 1,
|
||||
db: {
|
||||
execute () { return Promise.resolve(result) }
|
||||
}
|
||||
}
|
||||
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const result = {
|
||||
columns: ['id', 'name'],
|
||||
values: [
|
||||
@@ -222,6 +215,16 @@ describe('Tab.vue', () => {
|
||||
[2, 'bar']
|
||||
]
|
||||
}
|
||||
// mock store state
|
||||
const state = {
|
||||
currentTabId: 1,
|
||||
db: {
|
||||
execute: sinon.stub().resolves(result),
|
||||
getSchema: sinon.stub().resolves({ dbName: '', schema: [] })
|
||||
}
|
||||
}
|
||||
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
|
||||
// mount the component
|
||||
const wrapper = mount(Tab, {
|
||||
@@ -239,8 +242,64 @@ describe('Tab.vue', () => {
|
||||
|
||||
await wrapper.vm.execute()
|
||||
expect(wrapper.find('.table-view .result-before').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('.table-view .result-in-progress').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('.table-preview.error').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('.table-view .result-in-progress').exists()).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'logs' }).exists()).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'SqlTable' }).vm.dataSet).to.eql(result)
|
||||
})
|
||||
|
||||
it('Updates schema after query execution', async () => {
|
||||
const result = {
|
||||
columns: ['id', 'name'],
|
||||
values: []
|
||||
}
|
||||
const newSchema = {
|
||||
dbName: 'fooDb',
|
||||
schema: [
|
||||
{
|
||||
name: 'foo',
|
||||
columns: [
|
||||
{ name: 'id', type: 'INTEGER' },
|
||||
{ name: 'title', type: 'NVARCHAR(30)' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'bar',
|
||||
columns: [
|
||||
{ name: 'a', type: 'N/A' },
|
||||
{ name: 'b', type: 'N/A' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
// mock store state
|
||||
const state = {
|
||||
currentTabId: 1,
|
||||
dbName: 'fooDb',
|
||||
db: {
|
||||
execute: sinon.stub().resolves(result),
|
||||
getSchema: sinon.stub().resolves(newSchema)
|
||||
}
|
||||
}
|
||||
|
||||
sinon.spy(mutations, 'saveSchema')
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
|
||||
// mount the component
|
||||
const wrapper = mount(Tab, {
|
||||
store,
|
||||
stubs: ['chart'],
|
||||
propsData: {
|
||||
id: 1,
|
||||
initName: 'foo',
|
||||
initQuery: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
||||
initChart: [],
|
||||
tabIndex: 0,
|
||||
isPredefined: false
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.execute()
|
||||
expect(state.db.getSchema.calledOnceWith('fooDb')).to.equal(true)
|
||||
expect(mutations.saveSchema.calledOnceWith(state, newSchema)).to.equal(true)
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,15 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { shallowMount, mount, createWrapper } from '@vue/test-utils'
|
||||
import { mutations } from '@/store'
|
||||
import mutations from '@/store/mutations'
|
||||
import Vuex from 'vuex'
|
||||
import Tabs from '@/components/Tabs.vue'
|
||||
import Tabs from '@/views/Main/Editor/Tabs'
|
||||
|
||||
describe('Tabs.vue', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('Renders start guide when there is no opened tabs', () => {
|
||||
// mock store state
|
||||
const state = {
|
||||
@@ -14,7 +18,10 @@ describe('Tabs.vue', () => {
|
||||
const store = new Vuex.Store({ state })
|
||||
|
||||
// mount the component
|
||||
const wrapper = shallowMount(Tabs, { store })
|
||||
const wrapper = shallowMount(Tabs, {
|
||||
store,
|
||||
stubs: ['router-link']
|
||||
})
|
||||
|
||||
// check start-guide visibility
|
||||
expect(wrapper.find('#start-guide').isVisible()).to.equal(true)
|
||||
@@ -32,7 +39,10 @@ describe('Tabs.vue', () => {
|
||||
const store = new Vuex.Store({ state })
|
||||
|
||||
// mount the component
|
||||
const wrapper = shallowMount(Tabs, { store })
|
||||
const wrapper = shallowMount(Tabs, {
|
||||
store,
|
||||
stubs: ['router-link']
|
||||
})
|
||||
|
||||
// check start-guide visibility
|
||||
expect(wrapper.find('#start-guide').isVisible()).to.equal(false)
|
||||
@@ -64,7 +74,10 @@ describe('Tabs.vue', () => {
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
|
||||
// mount the component
|
||||
const wrapper = shallowMount(Tabs, { store })
|
||||
const wrapper = shallowMount(Tabs, {
|
||||
store,
|
||||
stubs: ['router-link']
|
||||
})
|
||||
|
||||
// click on the first tab
|
||||
const firstTab = wrapper.findAll('.tab').at(0)
|
||||
@@ -90,7 +103,10 @@ describe('Tabs.vue', () => {
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
|
||||
// mount the component
|
||||
const wrapper = mount(Tabs, { store, stubs: ['router-link'] })
|
||||
const wrapper = mount(Tabs, {
|
||||
store,
|
||||
stubs: ['router-link']
|
||||
})
|
||||
|
||||
// click on the close icon of the first tab
|
||||
const firstTabCloseIcon = wrapper.findAll('.tab').at(0).find('.close-icon')
|
||||
@@ -118,7 +134,10 @@ describe('Tabs.vue', () => {
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
|
||||
// mount the component
|
||||
const wrapper = mount(Tabs, { store, stubs: ['router-link'] })
|
||||
const wrapper = mount(Tabs, {
|
||||
store,
|
||||
stubs: ['router-link']
|
||||
})
|
||||
|
||||
// click on the close icon of the second tab
|
||||
const secondTabCloseIcon = wrapper.findAll('.tab').at(1).find('.close-icon')
|
||||
@@ -156,7 +175,10 @@ describe('Tabs.vue', () => {
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
|
||||
// mount the component
|
||||
const wrapper = mount(Tabs, { store, stubs: ['router-link'] })
|
||||
const wrapper = mount(Tabs, {
|
||||
store,
|
||||
stubs: ['router-link']
|
||||
})
|
||||
|
||||
// click on the close icon of the second tab
|
||||
const secondTabCloseIcon = wrapper.findAll('.tab').at(1).find('.close-icon')
|
||||
@@ -198,7 +220,10 @@ describe('Tabs.vue', () => {
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
|
||||
// mount the component
|
||||
const wrapper = mount(Tabs, { store, stubs: ['router-link'] })
|
||||
const wrapper = mount(Tabs, {
|
||||
store,
|
||||
stubs: ['router-link']
|
||||
})
|
||||
|
||||
// click on the close icon of the second tab
|
||||
const secondTabCloseIcon = wrapper.findAll('.tab').at(1).find('.close-icon')
|
||||
@@ -243,7 +268,10 @@ describe('Tabs.vue', () => {
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
|
||||
// mount the component
|
||||
const wrapper = shallowMount(Tabs, { store })
|
||||
const wrapper = shallowMount(Tabs, {
|
||||
store,
|
||||
stubs: ['router-link']
|
||||
})
|
||||
|
||||
const event = new Event('beforeunload')
|
||||
sinon.spy(event, 'preventDefault')
|
||||
@@ -264,7 +292,10 @@ describe('Tabs.vue', () => {
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
|
||||
// mount the component
|
||||
const wrapper = shallowMount(Tabs, { store })
|
||||
const wrapper = shallowMount(Tabs, {
|
||||
store,
|
||||
stubs: ['router-link']
|
||||
})
|
||||
|
||||
const event = new Event('beforeunload')
|
||||
sinon.spy(event, 'preventDefault')
|
||||
@@ -2,8 +2,8 @@ import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { mount, shallowMount, createWrapper } from '@vue/test-utils'
|
||||
import Vuex from 'vuex'
|
||||
import MainMenu from '@/components/MainMenu.vue'
|
||||
import storedQueries from '@/storedQueries.js'
|
||||
import MainMenu from '@/views/Main/MainMenu'
|
||||
import storedQueries from '@/lib/storedQueries'
|
||||
|
||||
let wrapper = null
|
||||
|
||||
@@ -2,10 +2,10 @@ import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { mount, shallowMount } from '@vue/test-utils'
|
||||
import Vuex from 'vuex'
|
||||
import MyQueries from '@/views/MyQueries.vue'
|
||||
import storedQueries from '@/storedQueries'
|
||||
import { mutations } from '@/store'
|
||||
import fu from '@/file.utils'
|
||||
import MyQueries from '@/views/Main/MyQueries'
|
||||
import storedQueries from '@/lib/storedQueries'
|
||||
import mutations from '@/store/mutations'
|
||||
import fu from '@/lib/utils/fileIo'
|
||||
|
||||
describe('MyQueries.vue', () => {
|
||||
afterEach(() => {
|
||||
@@ -1,4 +1,5 @@
|
||||
const CopyPlugin = require('copy-webpack-plugin')
|
||||
const WorkboxPlugin = require('workbox-webpack-plugin')
|
||||
|
||||
module.exports = {
|
||||
publicPath: '',
|
||||
@@ -9,7 +10,13 @@ module.exports = {
|
||||
// It is important that we do not change its name, and that it is in the same folder as the js
|
||||
{ from: 'node_modules/sql.js/dist/sql-wasm.wasm', to: 'js/' },
|
||||
{ from: 'LICENSE', to: './' }
|
||||
])
|
||||
]),
|
||||
new WorkboxPlugin.GenerateSW({
|
||||
exclude: [/\.map$/, 'LICENSE', 'queries.json'],
|
||||
clientsClaim: true,
|
||||
skipWaiting: false,
|
||||
maximumFileSizeToCacheInBytes: 40000000
|
||||
})
|
||||
]
|
||||
},
|
||||
chainWebpack: config => {
|
||||
@@ -24,11 +31,11 @@ module.exports = {
|
||||
|
||||
config.module
|
||||
.rule('worker')
|
||||
.test(/\.worker\.js$/)
|
||||
.test(/worker\.js$/)
|
||||
.use('worker-loader')
|
||||
.loader('worker-loader')
|
||||
.end()
|
||||
|
||||
config.module.rule('js').exclude.add(/\.worker\.js$/)
|
||||
config.module.rule('js').exclude.add(/worker\.js$/)
|
||||
}
|
||||
}
|
||||
|
||||