mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-07 02:28:54 +08:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a9f4b3c0a | ||
|
|
77468d34ae | ||
|
|
a0577ec0ce | ||
|
|
e7d1398546 | ||
|
|
aa52048d51 | ||
|
|
33913f8f5c | ||
|
|
51eb7a543c | ||
|
|
a3fb38b23c | ||
|
|
3bb40b4eb7 | ||
|
|
6864bf84f8 | ||
|
|
9f1b3823f6 | ||
|
|
7574f529c3 | ||
|
|
653f8eff7b |
17
.github/workflows/config.grenrc.js
vendored
Normal file
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"]
|
||||
}
|
||||
}
|
||||
8
.github/workflows/main.yml
vendored
8
.github/workflows/main.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user