Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a07f2d3d99 | ||
|
|
b9844b8696 | ||
|
|
464bff3db8 | ||
|
|
00e434e142 | ||
|
|
5d6280abec | ||
|
|
7a39e905b9 | ||
|
|
297ea2c18a | ||
|
|
1f2327a724 |
23
README.md
@@ -4,20 +4,18 @@
|
||||
|
||||
# sqliteviz
|
||||
|
||||
Sqliteviz is a single-page PWA for fully client-side visualisation of SQLite databases or CSV files.
|
||||
Sqliteviz is a single-page offline-first PWA for fully client-side visualisation of SQLite databases or CSV files.
|
||||
|
||||
This application allows to:
|
||||
- run SQL queries in SQLite database and create all kinds of charts based on result set
|
||||
- import CSV file into SQLite database and visualize imported data
|
||||
- save queries and chart settings
|
||||
With sqliteviz you can:
|
||||
- run SQL queries against a SQLite database and create [Plotly][11] charts based on the result sets
|
||||
- import a CSV file into a SQLite database and visualize imported data
|
||||
- manage queries and chart settings and run them against different databases
|
||||
- import/export queries and chart settings to/from a JSON file
|
||||
- manipulate saved queries (rename, duplicate, delete)
|
||||
- set predefined queries available for all users of sqliteviz on your server (read more about predefind queries on [Wiki][10])
|
||||
- export modified SQLite database
|
||||
- use it offline
|
||||
- export a modified SQLite database
|
||||
- use it offline from your OS application menu like any other desktop app
|
||||
|
||||
## Get started
|
||||
The latest release of sqliteviz is running on [Github pages][6]. The simplest way to start is to use sqliteviz there.
|
||||
## Quickstart
|
||||
The latest release of sqliteviz is deployed on GitHub Pages at [lana-k.github.io/sqliteviz][6].
|
||||
|
||||
## Wiki
|
||||
For user documentation, check out sqliteviz [Wiki][7].
|
||||
@@ -33,8 +31,9 @@ It is built on top of [react-chart-editor][3], [sql.js][4] and [Vue-Codemirror][
|
||||
[3]: https://github.com/plotly/react-chart-editor
|
||||
[4]: https://github.com/sql-js/sql.js
|
||||
[5]: https://github.com/vuejs/vue
|
||||
[6]: https://lana-k.github.io/sqliteviz
|
||||
[6]: https://lana-k.github.io/sqliteviz/
|
||||
[7]: https://github.com/lana-k/sqliteviz/wiki
|
||||
[8]: https://github.com/surmon-china/vue-codemirror#readme
|
||||
[9]: https://www.papaparse.com/
|
||||
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries
|
||||
[11]: https://github.com/plotly/plotly.js
|
||||
30
package-lock.json
generated
@@ -14,7 +14,7 @@
|
||||
"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",
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"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",
|
||||
|
||||
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 11 KiB |
BIN
public/Logo48x48.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 798 B After Width: | Height: | Size: 774 B |
@@ -8,7 +8,7 @@
|
||||
<link rel="manifest" href="<%= BASE_URL %>manifest.webmanifest">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<style>
|
||||
#loading-wrapper {
|
||||
#sqliteviz-loading-wrapper {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -17,7 +17,7 @@
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#loading-text {
|
||||
#sqliteviz-loading-text {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@@ -28,7 +28,7 @@
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.svg-container {
|
||||
#sqliteviz-loading-wrapper svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
@@ -36,7 +36,7 @@
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.loader-svg {
|
||||
#sqliteviz-loading-wrapper circle {
|
||||
position: absolute;
|
||||
left: 0; right: 0; top: 0; bottom: 0;
|
||||
fill: none;
|
||||
@@ -45,16 +45,16 @@
|
||||
stroke: #119DFF;
|
||||
}
|
||||
|
||||
.loader-svg.bg {
|
||||
#sqliteviz-loading-wrapper circle.bg {
|
||||
stroke: #C8D4E3;
|
||||
}
|
||||
|
||||
.loader-svg.front {
|
||||
#sqliteviz-loading-wrapper circle.front {
|
||||
stroke-dasharray: 402px;
|
||||
animation: loading 2s linear 0s infinite;
|
||||
animation: sqliteviz-loading 2s linear 0s infinite;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
@keyframes sqliteviz-loading {
|
||||
0% {
|
||||
stroke-dasharray: 100px 402px;
|
||||
stroke-dashoffset: 0;
|
||||
@@ -75,17 +75,17 @@
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app">
|
||||
<div id="loading-wrapper">
|
||||
<div id="loading-text">LOADING</div>
|
||||
<svg class="svg-container" height="170" width="170" viewBox="0 0 170 170">
|
||||
<div id="sqliteviz-loading-wrapper">
|
||||
<div id="sqliteviz-loading-text">LOADING</div>
|
||||
<svg height="170" width="170" viewBox="0 0 170 170">
|
||||
<circle
|
||||
class="loader-svg bg"
|
||||
class="bg"
|
||||
cx="85"
|
||||
cy="85"
|
||||
r="80"
|
||||
/>
|
||||
<circle
|
||||
class="loader-svg front"
|
||||
class="front"
|
||||
cx="85"
|
||||
cy="85"
|
||||
r="80"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 42 KiB |
@@ -233,6 +233,7 @@ export default {
|
||||
this.$modal.hide('parse')
|
||||
const tabId = await this.$store.dispatch('addTab', { query: 'select * from csv_import' })
|
||||
this.$store.commit('setCurrentTabId', tabId)
|
||||
this.importCsvCompleted = false
|
||||
}
|
||||
if (this.$route.path !== '/editor') {
|
||||
this.$router.push('/editor')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<svg :class="['svg-container', animationClass ]" height="20" width="20" viewBox="0 0 20 20">
|
||||
<svg :class="animationClass" height="20" width="20" viewBox="0 0 20 20">
|
||||
<circle
|
||||
class="loader-svg bg"
|
||||
cx="10"
|
||||
|
||||
@@ -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]) })
|
||||
|
||||
@@ -25,6 +25,10 @@ const hintOptions = {
|
||||
}
|
||||
return tables
|
||||
},
|
||||
get defaultTable () {
|
||||
const schema = store.state.schema
|
||||
return schema.length === 1 ? schema[0].name : null
|
||||
},
|
||||
completeSingle: false,
|
||||
completeOnSingleClick: true,
|
||||
alignWithWord: false
|
||||
|
||||
@@ -838,4 +838,49 @@ 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 = {
|
||||
createDb: 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.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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('csv.js', () => {
|
||||
{ id: 2, name: 'bar' }
|
||||
],
|
||||
meta: {
|
||||
fields: ['id', 'name']
|
||||
fields: ['id', 'name ']
|
||||
}
|
||||
}
|
||||
expect(csv.getResult(source)).to.eql({
|
||||
|
||||
@@ -49,6 +49,39 @@ describe('hint.js', () => {
|
||||
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()
|
||||
}
|
||||
|
||||
const clock = sinon.useFakeTimers()
|
||||
hint.show(editor)
|
||||
clock.tick(500)
|
||||
|
||||
expect(CM.showHint.firstCall.args[2].defaultTable).to.equal('foo')
|
||||
})
|
||||
|
||||
it("Doesn't show hint when in string or space, or ';'", () => {
|
||||
|
||||