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

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

View File

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

@@ -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,7 +128,7 @@
</template>
<script>
import fu from '@/lib/utils/fileIo'
import fIo from '@/lib/utils/fileIo'
import csv from './csv'
import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField'
@@ -139,15 +140,6 @@ import ChangeDbIcon from '@/components/svg/changeDb'
import time from '@/lib/utils/time'
import database from '@/lib/database'
const csvMimeTypes = [
'text/csv',
'text/x-csv',
'application/x-csv',
'application/csv',
'text/x-comma-separated-values',
'text/comma-separated-values'
]
export default {
name: 'DbUploader',
props: {
@@ -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()
}
})
@@ -387,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 = '"'
@@ -398,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)
},
@@ -511,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

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

View File

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

View File

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

@@ -21,7 +21,8 @@
>
Run your query and get results here
</div>
<div v-show="isGettingResults" class="table-preview result-in-progress">
<div v-if="isGettingResults" class="table-preview result-in-progress">
<loading-indicator :size="30"/>
Fetching results...
</div>
<div
@@ -30,10 +31,8 @@
>
No rows retrieved according to your query
</div>
<div v-show="error" class="table-preview error">
{{ error }}
</div>
<sql-table v-if="result" :data-set="result" :height="tableViewHeight" />
<logs v-if="error" :messages="[error]"/>
<sql-table v-if="result" :data-set="result" :time="time" :height="tableViewHeight" />
</div>
<chart
:visible="view === 'chart'"
@@ -51,9 +50,12 @@
<script>
import SqlTable from '@/components/SqlTable'
import Splitpanes from '@/components/Splitpanes'
import LoadingIndicator from '@/components/LoadingIndicator'
import SqlEditor from './SqlEditor'
import ViewSwitcher from './ViewSwitcher'
import Chart from './Chart'
import Logs from '@/components/Logs'
import time from '@/lib/utils/time'
export default {
name: 'Tab',
@@ -63,7 +65,9 @@ export default {
SqlTable,
Splitpanes,
ViewSwitcher,
Chart
Chart,
LoadingIndicator,
Logs
},
data () {
return {
@@ -73,7 +77,8 @@ export default {
tableViewHeight: 0,
isGettingResults: false,
error: null,
resizeObserver: null
resizeObserver: null,
time: 0
}
},
computed: {
@@ -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

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

View File

@@ -33,7 +33,7 @@ describe('DbUploader.vue', () => {
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
@@ -90,7 +90,7 @@ describe('DbUploader.vue', () => {
})
// 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],
@@ -109,7 +109,7 @@ describe('DbUploader.vue', () => {
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

View File

@@ -105,4 +105,27 @@ describe('fileIo.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

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