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

31 Commits

Author SHA1 Message Date
lana-k
8a9f4b3c0a add Help link 2021-05-18 15:04:35 +02:00
lana-k
77468d34ae add release notes 2021-05-18 15:04:19 +02:00
lana-k
a0577ec0ce Fix gradient for Safari 2021-05-17 21:34:42 +02:00
lana-k
e7d1398546 Rewrite reg exp: make them work in Safari #52 2021-05-17 21:34:24 +02:00
lana-k
aa52048d51 Fix file type detection #48
file.type is empty on some Windows machines (Registry settings affects)
2021-05-17 21:32:09 +02:00
lana-k
33913f8f5c fix lint 2021-05-14 16:47:01 +02:00
lana-k
51eb7a543c Merge branch 'master' of github.com:lana-k/sqliteviz 2021-05-14 16:43:13 +02:00
lana-k
a3fb38b23c SQL query execution state in UI #3
- use LoadingIndicator
- use Logs
2021-05-14 16:42:58 +02:00
lana-k
3bb40b4eb7 Improve LoadingIndicator
- size parameters
- smooth animation (use ony transform)
2021-05-14 16:40:55 +02:00
lana-k
6864bf84f8 Update README.md 2021-05-06 21:36:20 +02:00
lana-k
9f1b3823f6 Update README.md 2021-05-06 20:46:36 +02:00
lana-k
7574f529c3 add hidden state for file in animation 2021-05-06 15:17:15 +02:00
lana-k
653f8eff7b minor changes in animation 2021-05-06 15:05:58 +02:00
lana-k
9b3dda6cff delete .nojekyll from master 2021-05-05 23:43:51 +02:00
lana-k
d94604ebfb Merge branch 'master' of github.com:lana-k/sqliteviz 2021-05-05 23:31:17 +02:00
lana-k
16868ef430 add clean-exclude 2021-05-05 23:30:33 +02:00
lana-k
b162c7043e Create .nojekyll 2021-05-05 23:20:46 +02:00
lana-k
8e856063b8 allow files with underscores on GitHub pages 2021-05-05 23:12:47 +02:00
lana-k
8684b4cef9 remove console.log 2021-05-05 21:46:29 +02:00
lana-k
bcaebd4840 Create an empty database #44 2021-05-05 21:44:44 +02:00
lana-k
4619461af8 change period format 2021-05-05 15:08:54 +02:00
lana-k
9fff1d699a update sql.js #43 2021-05-05 15:08:12 +02:00
lana-k
5ab19c3fae show hint in codemirror on Ctrl+Space 2021-05-04 16:33:37 +02:00
lana-k
cc483f4720 change code structure 2021-05-04 14:13:58 +02:00
lana-k
a07f2d3d99 update plotly 2021-05-02 20:59:03 +02:00
lana-k
b9844b8696 refine pwa app icons 2021-05-02 20:46:27 +02:00
lana-k
464bff3db8 delete screenshots 2021-05-02 20:45:24 +02:00
lana-k
00e434e142 fix loading db after csv:
new tab is not opened now
2021-05-02 14:09:02 +02:00
lana-k
5d6280abec add default table in hint options 2021-05-02 14:04:46 +02:00
lana-k
7a39e905b9 trim csv column names 2021-04-30 20:49:37 +02:00
lana-k
297ea2c18a fix path to service worker 2021-04-30 19:42:26 +02:00
94 changed files with 785 additions and 586 deletions

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

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

View File

@@ -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

View File

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

View File

@@ -141,7 +141,7 @@ module.exports = function (config) {
]
},
{
test: /\.worker\.js$/,
test: /worker\.js$/,
loader: 'worker-loader'
},
{

44
package-lock.json generated
View File

@@ -14,12 +14,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",
@@ -10057,9 +10057,9 @@
}
},
"node_modules/gl-plot3d": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-2.4.6.tgz",
"integrity": "sha512-CkrNvDKu0p74Di2g2Oc9kU+s1Oe+wi4cIfHzXABp8DvfoRl0/bayqJ9q8EcRAqMeQQxQZYGvJkk4hlBwI758Jw==",
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-2.4.7.tgz",
"integrity": "sha512-mLDVWrl4Dj0O0druWyHUK5l7cBQrRIJRn2oROEgrRuOgbbrLAzsREKefwMO0bA0YqkiZMFMnV5VvPA9j57X5Xg==",
"dependencies": {
"3d-view": "^2.0.0",
"a-big-triangle": "^1.0.3",
@@ -15945,9 +15945,9 @@
}
},
"node_modules/plotly.js": {
"version": "1.57.1",
"resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-1.57.1.tgz",
"integrity": "sha512-23GlzClmOGT1lE86Ys0DLuxBM/fgRNzJqH9y7ZylO4VPwstPAlQd12DklXsuqOgCNSxnnWUaP+J7BaUOFplsUg==",
"version": "1.58.4",
"resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-1.58.4.tgz",
"integrity": "sha512-hdt/aEvkPjS1HJ7tJKcPqsqi9ErEZPhUFs4d2ANTLeBim+AmVcHzS1rtwr7ZrVCINgliW/+92u81omJoy+lbUw==",
"dependencies": {
"@plotly/d3-sankey": "0.7.2",
"@plotly/d3-sankey-circular": "0.33.1",
@@ -15979,7 +15979,7 @@
"gl-mat4": "^1.2.0",
"gl-mesh3d": "^2.3.1",
"gl-plot2d": "^1.4.5",
"gl-plot3d": "^2.4.6",
"gl-plot3d": "^2.4.7",
"gl-pointcloud2d": "^1.0.3",
"gl-scatter3d": "^1.2.3",
"gl-select-box": "^1.0.4",
@@ -19633,9 +19633,9 @@
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"node_modules/sql.js": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.3.0.tgz",
"integrity": "sha512-bxrJ/9rqJ2SA6hpHnSodRjKBugZHewRvNTITTt74W1VZWmzODjdS68yQW0/J9oC0NWKylHEtV1ptkoTyOYO4Tw=="
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.5.0.tgz",
"integrity": "sha512-Qqr6HgX/hCDpLFWdN0BNoNpYQ2c1tOl1c3HGI0cshjaFSAWszKICuLZ9CyFUvRFPpEGW8RzHzwuXWWvXVGTKBg=="
},
"node_modules/sqlite-parser": {
"version": "1.0.1",
@@ -32186,9 +32186,9 @@
}
},
"gl-plot3d": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-2.4.6.tgz",
"integrity": "sha512-CkrNvDKu0p74Di2g2Oc9kU+s1Oe+wi4cIfHzXABp8DvfoRl0/bayqJ9q8EcRAqMeQQxQZYGvJkk4hlBwI758Jw==",
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-2.4.7.tgz",
"integrity": "sha512-mLDVWrl4Dj0O0druWyHUK5l7cBQrRIJRn2oROEgrRuOgbbrLAzsREKefwMO0bA0YqkiZMFMnV5VvPA9j57X5Xg==",
"requires": {
"3d-view": "^2.0.0",
"a-big-triangle": "^1.0.3",
@@ -37188,9 +37188,9 @@
}
},
"plotly.js": {
"version": "1.57.1",
"resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-1.57.1.tgz",
"integrity": "sha512-23GlzClmOGT1lE86Ys0DLuxBM/fgRNzJqH9y7ZylO4VPwstPAlQd12DklXsuqOgCNSxnnWUaP+J7BaUOFplsUg==",
"version": "1.58.4",
"resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-1.58.4.tgz",
"integrity": "sha512-hdt/aEvkPjS1HJ7tJKcPqsqi9ErEZPhUFs4d2ANTLeBim+AmVcHzS1rtwr7ZrVCINgliW/+92u81omJoy+lbUw==",
"requires": {
"@plotly/d3-sankey": "0.7.2",
"@plotly/d3-sankey-circular": "0.33.1",
@@ -37222,7 +37222,7 @@
"gl-mat4": "^1.2.0",
"gl-mesh3d": "^2.3.1",
"gl-plot2d": "^1.4.5",
"gl-plot3d": "^2.4.6",
"gl-plot3d": "^2.4.7",
"gl-pointcloud2d": "^1.0.3",
"gl-scatter3d": "^1.2.3",
"gl-select-box": "^1.0.4",
@@ -40491,9 +40491,9 @@
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"sql.js": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.3.0.tgz",
"integrity": "sha512-bxrJ/9rqJ2SA6hpHnSodRjKBugZHewRvNTITTt74W1VZWmzODjdS68yQW0/J9oC0NWKylHEtV1ptkoTyOYO4Tw=="
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.5.0.tgz",
"integrity": "sha512-Qqr6HgX/hCDpLFWdN0BNoNpYQ2c1tOl1c3HGI0cshjaFSAWszKICuLZ9CyFUvRFPpEGW8RzHzwuXWWvXVGTKBg=="
},
"sqlite-parser": {
"version": "1.0.1",

View File

@@ -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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/Logo48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 798 B

After

Width:  |  Height:  |  Size: 774 B

View File

@@ -3,6 +3,16 @@
"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",

View File

@@ -60,4 +60,7 @@ button,
body {
margin: 0;
}
.CodeMirror-hints {
z-index: 999 !important;
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -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'

View File

@@ -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]) })

View File

@@ -1,7 +1,7 @@
<template>
<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,26 +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'
const csvMimeTypes = [
'text/csv',
'text/x-csv',
'application/x-csv',
'application/csv',
'text/x-comma-separated-values',
'text/comma-separated-values'
]
import time from '@/lib/utils/time'
import database from '@/lib/database'
export default {
name: 'DbUploader',
@@ -154,9 +146,9 @@ 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: {
@@ -196,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()
}
})
@@ -231,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')
@@ -340,7 +341,7 @@ export default {
// Create db with csv table and get schema
const name = file.name.replace(/\.[^.]+$/, '')
start = new Date()
this.schema = await this.newDb.createDb(name, parseResult.data, progressCounterId)
this.schema = await this.newDb.importDb(name, parseResult.data, progressCounterId)
end = new Date()
// Inform about import success
@@ -379,8 +380,10 @@ export default {
},
async checkFile (file) {
this.state = 'drop'
if (csvMimeTypes.includes(file.type)) {
this.state = 'dropping'
if (fIo.isDatabase(file)) {
this.loadDb(file)
} else {
this.file = file
this.header = true
this.quoteChar = '"'
@@ -390,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)
},
@@ -431,6 +432,7 @@ export default {
align-items: center;
justify-content: center;
height: 100%;
cursor: pointer;
}
#img-container {
@@ -502,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;

View File

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

View File

@@ -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;

View File

@@ -65,7 +65,7 @@
</template>
<script>
import splitter from '@/splitter'
import splitter from './splitter'
export default {
name: 'Splitpanes',

View File

@@ -41,6 +41,7 @@
<div class="table-footer-count">
{{ dataSet.values.length}} {{dataSet.values.length === 1 ? 'row' : 'rows'}} retrieved
<span v-if="preview">for preview</span>
<span v-if="time">in {{ time }}</span>
</div>
<pager v-show="pageCount > 1" :page-count="pageCount" v-model="currentPage" />
</div>
@@ -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,

View File

@@ -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',

View File

@@ -23,7 +23,7 @@
</template>
<script>
import tooltipMixin from '@/mixins/tooltips'
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'ExportIcon',

View File

@@ -20,7 +20,7 @@
</template>
<script>
import tooltipMixin from '@/mixins/tooltips'
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'HintIcon',

View File

@@ -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 })

View File

@@ -1,5 +1,5 @@
import registerPromiseWorker from 'promise-worker/register'
import Sql from '@/sql'
import Sql from './_sql'
const sqlReady = Sql.build()

View File

@@ -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,
@@ -66,14 +66,15 @@ class Database {
}
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 new Error(res.error)
}
return this.getSchema(file.name.replace(/\.[^.]+$/, ''))
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 {
@@ -124,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)
}

View File

@@ -1,5 +1,5 @@
import { nanoid } from 'nanoid'
import fu from '@/file.utils'
import fu from '@/lib/utils/fileIo'
export default {
getStoredQueries () {

View File

@@ -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
View 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'
}
}

View File

@@ -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'
@@ -13,7 +13,7 @@ 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
import('./registerServiceWorker') // eslint-disable-line no-unused-expressions
}
Vue.use(VuePlugin)

View File

@@ -13,7 +13,7 @@ function invokeServiceWorkerUpdateFlow (registration) {
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
const registration = await navigator.serviceWorker.register('/service-worker.js')
const registration = await navigator.serviceWorker.register('service-worker.js')
// ensure the case when the updatefound event was missed is also handled
// by re-invoking the prompt when there's a waiting Service Worker
if (registration.waiting) {

View File

@@ -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
View 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
}
}

View File

@@ -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
View 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
View File

@@ -0,0 +1,11 @@
export default {
schema: null,
dbFile: null,
dbName: null,
tabs: [],
currentTab: null,
currentTabId: null,
untitledLastIndex: 0,
predefinedQueries: [],
db: null
}

View File

@@ -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$/, '')
}
}

View File

@@ -1,74 +0,0 @@
<template>
<div>
<splitpanes
class="schema-tabs-splitter"
:before="{ size: 20, max: 30 }"
:after="{ size: 80, max: 100 }"
>
<template #left-pane>
<schema v-if="$store.state.schema"/>
<div v-else id="empty-schema-container">
<div class="warning">
Database is not loaded. Queries cant be run without database.
</div>
<db-uploader id="db-uploader" width="100%"/>
</div>
</template>
<template #right-pane>
<tabs />
</template>
</splitpanes>
</div>
</template>
<script>
import Splitpanes from '@/components/Splitpanes'
import Schema from '@/components/Schema'
import Tabs from '@/components/Tabs'
import DbUploader from '@/components/DbUploader'
export default {
name: 'Editor',
components: {
Schema,
Splitpanes,
Tabs,
DbUploader
}
}
</script>
<style scoped>
.schema-tabs-splitter {
height: 100%;
background-color: var(--color-white);
}
#empty-schema-container {
display: flex;
flex-direction: column;
align-items: center;
min-width: 200px;
height: 100%;
}
#db-uploader {
flex-grow: 1;
padding: 24px;
width: 100%;
box-sizing: border-box;
}
.warning {
padding: 12px 24px;
width: 100%;
box-sizing: border-box;
}
>>>.drop-area {
padding: 0 15px;
}
>>>.drop-area .text {
max-width: 200px;
}
</style>

View File

@@ -5,7 +5,7 @@
</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" />
@@ -23,7 +23,7 @@
</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'
@@ -86,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 {

View File

@@ -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)
}
}
}

View File

@@ -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)
}

View File

@@ -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>

View File

@@ -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: {
@@ -110,11 +115,16 @@ export default {
this.error = null
const state = this.$store.state
try {
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
},
@@ -183,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>

View File

@@ -62,7 +62,7 @@
</template>
<script>
import Tab from '@/components/Tab'
import Tab from './Tab'
import CloseIcon from '@/components/svg/close'
export default {

View 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>

View File

@@ -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',

View File

@@ -141,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',

View File

@@ -23,7 +23,7 @@
</template>
<script>
import tooltipMixin from '@/mixins/tooltips'
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'CopyIcon',

View File

@@ -23,7 +23,7 @@
</template>
<script>
import tooltipMixin from '@/mixins/tooltips'
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'DeleteIcon',

View File

@@ -23,7 +23,7 @@
</template>
<script>
import tooltipMixin from '@/mixins/tooltips'
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'RenameIcon',

View File

@@ -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>

View File

@@ -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>

View File

@@ -2,15 +2,16 @@ 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
@@ -20,15 +21,19 @@ describe('DbUploader.vue', () => {
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
@@ -44,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 () => {
@@ -69,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],
@@ -84,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
@@ -106,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()
})
})
@@ -122,6 +148,7 @@ describe('DbUploader.vue import CSV', () => {
let actions = {}
const newTabId = 1
let store = {}
let place
// mock router
const $router = { }
@@ -150,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 () => {
@@ -184,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('"')
@@ -219,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: ',',
@@ -328,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 => {
@@ -395,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]
@@ -452,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]
@@ -511,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]
@@ -563,7 +605,7 @@ describe('DbUploader.vue import CSV', () => {
let resolveImport = sinon.stub()
const newDb = {
createDb: sinon.stub().resolves(new Promise(resolve => { resolveImport = resolve })),
importDb: sinon.stub().resolves(new Promise(resolve => { resolveImport = resolve })),
createProgressCounter: sinon.stub().returns(1),
deleteProgressCounter: sinon.stub()
}
@@ -573,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]
@@ -598,11 +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.createDb.getCall(0).args[0]).to.equal('foo') // file name
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)
@@ -621,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: {
@@ -637,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()
}
@@ -647,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]
@@ -681,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: {
@@ -696,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()
}
@@ -706,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]
@@ -729,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'],
@@ -742,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()
}
@@ -767,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]
@@ -784,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'],
@@ -797,22 +828,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(),
shutDown: sinon.stub()
@@ -823,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]
@@ -838,4 +857,50 @@ describe('DbUploader.vue import CSV', () => {
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)
})
})

View File

@@ -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 () => {

View File

@@ -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({

View File

@@ -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 () => {

View File

@@ -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', () => {

View File

@@ -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', () => {

View File

@@ -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(() => {

View File

@@ -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(() => {

View File

@@ -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()
})

View File

@@ -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

View File

@@ -2,8 +2,8 @@ import chai from 'chai'
import sinon from 'sinon'
import chaiAsPromised from 'chai-as-promised'
import initSqlJs from 'sql.js'
import database from '@/database'
import fu from '@/file.utils'
import database from '@/lib/database'
import fu from '@/lib/utils/fileIo'
chai.use(chaiAsPromised)
const expect = chai.expect
@@ -146,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')
@@ -164,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: [
@@ -174,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')
})

View File

@@ -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(() => {

View File

@@ -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()
})
@@ -105,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)
})
})

View File

@@ -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')
})
})

View 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)
})
})

View File

@@ -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)
})
})

View File

@@ -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>',

View File

@@ -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)

View File

@@ -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', () => {

View File

@@ -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')

View File

@@ -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)

View File

@@ -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'
})
})

View File

@@ -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({})
})

View File

@@ -1,9 +1,9 @@
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(() => {
@@ -202,9 +202,9 @@ describe('Tab.vue', () => {
await wrapper.vm.execute()
expect(wrapper.find('.table-view .result-before').isVisible()).to.equal(false)
expect(wrapper.find('.table-view .result-in-progress').isVisible()).to.equal(false)
expect(wrapper.find('.table-preview.error').isVisible()).to.equal(true)
expect(wrapper.find('.table-preview.error').text()).to.include('There is no table foo')
expect(wrapper.find('.table-view .result-in-progress').exists()).to.equal(false)
expect(wrapper.findComponent({ name: 'logs' }).isVisible()).to.equal(true)
expect(wrapper.findComponent({ name: 'logs' }).text()).to.include('There is no table foo')
})
it('Passes result to sql-table component', async () => {
@@ -242,8 +242,8 @@ describe('Tab.vue', () => {
await wrapper.vm.execute()
expect(wrapper.find('.table-view .result-before').isVisible()).to.equal(false)
expect(wrapper.find('.table-view .result-in-progress').isVisible()).to.equal(false)
expect(wrapper.find('.table-preview.error').isVisible()).to.equal(false)
expect(wrapper.find('.table-view .result-in-progress').exists()).to.equal(false)
expect(wrapper.findComponent({ name: 'logs' }).exists()).to.equal(false)
expect(wrapper.findComponent({ name: 'SqlTable' }).vm.dataSet).to.eql(result)
})

View File

@@ -1,9 +1,9 @@
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(() => {

View File

@@ -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

View File

@@ -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(() => {

View File

@@ -31,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$/)
}
}