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

19 Commits
0.6.0 ... 0.7.0

Author SHA1 Message Date
lana-k
e2db4e2634 chande button label 2020-12-21 11:23:59 +01:00
lana-k
6e29791892 minor changes 2020-12-20 22:30:40 +01:00
lana-k
068195589a wait for animation and file uploading 2020-12-20 14:29:53 +01:00
lana-k
6b72a937ea Move illustration to DbUpload component 2020-12-20 12:36:09 +01:00
lana-k
43e357de5b fix layout #19 2020-12-20 11:43:33 +01:00
lana-k
00c2fb86c5 add 'load db' illustration and animation #19 2020-12-19 23:28:45 +01:00
lana-k
085813ec50 Fix SQL editor issues #20 2020-12-17 19:23:51 +01:00
lana-k
0339aafe14 Save dialog remembers the name for another tab #22 2020-12-17 18:15:41 +01:00
lana-k
c2a155c561 License file is missing in the build #21 2020-12-16 22:38:05 +01:00
lana-k
5dbffe0c56 Pre-defined query file should be empty #24 2020-12-16 22:13:26 +01:00
lana-k
00d671fac0 Rename connection to loading #25 2020-12-16 22:02:53 +01:00
lana-k
18c8b30bef add coverage to gitignore 2020-12-16 19:15:52 +01:00
lana-k
0df9a7b982 remove unused component 2020-12-16 19:10:43 +01:00
lana-k
0c777b9f30 add proxy and test 2020-12-16 19:05:05 +01:00
lana-k
2aac74fba2 - 2020-12-15 14:45:45 +01:00
lana-k
6a1dcfc187 add karma packages 2020-12-15 14:43:17 +01:00
lana-k
cb49b7b6cb karma attempt 2020-12-15 12:32:05 +01:00
lana-k
a1a330986d small refactor 2020-11-25 22:04:11 +01:00
lana-k
3b43fcddb4 remove unused code 2020-11-25 21:41:38 +01:00
24 changed files with 25520 additions and 946 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
.DS_Store .DS_Store
node_modules node_modules
/dist /dist
/coverage
# local env files # local env files
.env.local .env.local

173
karma.conf.js Normal file
View File

@@ -0,0 +1,173 @@
// Karma configuration
"use strict";
const path = require("path");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
function resolve(dir) {
return path.join(__dirname, dir);
}
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: "",
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ["mocha", "sinon-chai"],
// list of files / patterns to load in the browser
files: [
"tests/unit/*.spec.js",
{ pattern: 'node_modules/sql.js/dist/sql-wasm.wasm',
watched: false,
included: false,
served: true,
nocache: false
},
{ pattern: 'node_modules/sql.js/dist/worker.sql-wasm.js',
watched: false,
included: false,
served: true,
nocache: false
}
],
// list of files / patterns to exclude
exclude: [],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
"tests/unit/*.spec.js": ["webpack"]
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ["spec", "coverage"],
coverageReporter: {
dir: "coverage",
reporters: [{ type: "lcov", subdir: "." }, { type: "text-summary" }]
},
// !!DONOT delete this reporter, or vue-cli-addon-ui-karma doesnot work
jsonResultReporter: {
outputFile: "report/karma-result.json",
isSynchronous: true
},
junitReporter: {
outputDir: "report", // results will be saved as $outputDir/$browserName.xml
outputFile: undefined, // if included, results will be saved as $outputDir/$browserName/$outputFile
suite: "", // suite will become the package name attribute in xml testsuite element
useBrowserName: true, // add browser name to report and classes names
nameFormatter: undefined, // function (browser, result) to customize the name attribute in xml testcase element
classNameFormatter: undefined, // function (browser, result) to customize the classname attribute in xml testcase element
properties: {} // key value pair of properties to add to the <properties> section of the report
},
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ["ChromiumHeadless"],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity,
client: {
captureConsole: true
},
browserConsoleLogOptions: {
terminal: true,
level: ""
},
webpack: {
mode: "development",
entry: "./src/main.js",
resolve: {
extensions: [".js", ".vue", ".json"],
alias: {
vue$: "vue/dist/vue.esm.js",
"@": resolve("src")
}
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: [
{
loader: "babel-loader"
},
{
loader: "istanbul-instrumenter-loader",
options: {
esModules: true
}
}
]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: "url-loader"
},
{
test: /\.vue$/,
loader: "vue-loader",
options: {
loaders: {
js: "babel-loader"
},
postLoaders: {
js: "istanbul-instrumenter-loader?esModules=true"
}
}
},
{
test: /\.css$/,
use: ["vue-style-loader", "css-loader"]
},
{
test: /\.scss$/,
use: ["vue-style-loader", "css-loader", "sass-loader"]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: "url-loader",
options: {
limit: 10000,
name: resolve("fonts/[name].[hash:7].[ext]")
}
}
]
},
plugins: [new VueLoaderPlugin()],
node: {
fs: 'empty'
}
},
proxies: {
"/js/": "/base/node_modules/sql.js/dist/"
}
});
};

25903
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "sqliteviz", "name": "sqliteviz",
"version": "0.6.0", "version": "0.7.0",
"license" : "Apache-2.0", "license": "Apache-2.0",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
@@ -32,7 +32,6 @@
"@vue/cli-plugin-babel": "^4.4.0", "@vue/cli-plugin-babel": "^4.4.0",
"@vue/cli-plugin-eslint": "^4.4.0", "@vue/cli-plugin-eslint": "^4.4.0",
"@vue/cli-plugin-router": "^4.4.0", "@vue/cli-plugin-router": "^4.4.0",
"@vue/cli-plugin-unit-mocha": "^4.4.0",
"@vue/cli-plugin-vuex": "^4.4.0", "@vue/cli-plugin-vuex": "^4.4.0",
"@vue/cli-service": "^4.4.0", "@vue/cli-service": "^4.4.0",
"@vue/eslint-config-standard": "^5.1.2", "@vue/eslint-config-standard": "^5.1.2",
@@ -45,6 +44,9 @@
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0", "eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^6.2.2", "eslint-plugin-vue": "^6.2.2",
"karma": "^3.1.4",
"karma-webpack": "^4.0.2",
"vue-cli-plugin-ui-karma": "^0.2.5",
"vue-template-compiler": "^2.6.11" "vue-template-compiler": "^2.6.11"
} }
} }

View File

@@ -1,47 +0,0 @@
{
"query": "select * from invoices",
"chart": {
"data": [
{
"type": "scatter",
"mode": "lines",
"x": null,
"xsrc": "InvoiceId",
"meta": {
"columnNames": {
"x": "InvoiceId",
"y": "Total"
}
},
"y": null,
"ysrc": "Total"
}
],
"layout": {
"xaxis": {
"range": [
1,
412
],
"autorange": true,
"type": "linear"
},
"yaxis": {
"range": [
-0.39166666666666683,
27.241666666666667
],
"autorange": true,
"type": "linear"
},
"autosize": true,
"mapbox": {
"style": "open-street-map"
}
},
"frames": []
},
"name": "Invoices",
"id": "ieZfcITwDUTADwOmQlYyL",
"createdAt": "2020-11-03T14:17:49.524Z"
}

View File

@@ -5,7 +5,7 @@
</template> </template>
<style> <style>
#app, * { #app {
font-family: Open-Sans, Helvetica, Arial, sans-serif; font-family: Open-Sans, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;

BIN
src/assets/images/body.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
src/assets/images/file.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -5,6 +5,7 @@ button {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
font-family: Open-Sans, Helvetica, Arial, sans-serif;
} }
button:focus { button:focus {

View File

@@ -1,7 +1,12 @@
<template> <template>
<div class="db-upload-container"> <div class="db-upload-container">
<label for="assetsFieldHandle"> <label for="assetsFieldHandle">
<div class="drop-area" @dragover="dragover" @dragleave="dragleave" @drop="drop"> <div
class="drop-area"
@dragover.prevent="state = 'dragover'"
@dragleave.prevent="state=''"
@drop.prevent="drop"
>
<input <input
type="file" type="file"
id="assetsFieldHandle" id="assetsFieldHandle"
@@ -10,10 +15,34 @@
accept=".db,.sqlite,.sqlite3" accept=".db,.sqlite,.sqlite3"
/> />
<div class="text"> <div class="text">
Drop the database file to upload here or click to choose a file from your computer. Drop the database file here or click to choose a file from your computer.
</div> </div>
</div> </div>
</label> </label>
<div v-if="illustrated" id="img-container">
<img id="drop-file-top-img" :src="require('@/assets/images/dropFileTop.png')" />
<img
id="left-arm-img"
:class="{'swing': state === 'dragover'}"
:src="require('@/assets/images/leftArm.png')"
/>
<img
id="file-img"
ref="fileImg"
:class="{
'swing': state === 'dragover',
'fly': state === 'drop'
}"
:src="require('@/assets/images/file.png')"
/>
<img id="drop-file-bottom-img" :src="require('@/assets/images/dropFileBottom.png')" />
<img id="body-img" :src="require('@/assets/images/body.png')" />
<img
id="right-arm-img"
:class="{'swing': state === 'dragover'}"
:src="require('@/assets/images/rightArm.png')"
/>
</div>
<div id="error" class="error"></div> <div id="error" class="error"></div>
</div> </div>
</template> </template>
@@ -21,30 +50,44 @@
<script> <script>
export default { export default {
name: 'DbUpload', name: 'DbUpload',
props: {
illustrated: {
type: Boolean,
required: false,
default: false
}
},
data () {
return {
state: '',
animationPromise: Promise.resolve()
}
},
mounted () {
if (this.illustrated) {
this.animationPromise = new Promise((resolve) => {
this.$refs.fileImg.addEventListener('animationend', event => {
if (event.animationName.startsWith('fly')) {
resolve()
}
})
})
}
},
methods: { methods: {
loadDb () { loadDb () {
this.$db.loadDb(this.$refs.file.files[0]) this.state = 'drop'
}, return Promise.all([this.$db.loadDb(this.$refs.file.files[0]), this.animationPromise])
dragover (event) { .then(([schema]) => {
event.preventDefault() this.$store.commit('saveSchema', schema)
// TODO: Add some visual stuff to show the user can drop its files if (this.$route.path !== '/editor') {
if (!event.currentTarget.classList.contains('bg-green-300')) { this.$router.push('/editor')
event.currentTarget.classList.remove('bg-gray-100') }
event.currentTarget.classList.add('bg-green-300') })
}
},
dragleave (event) {
// Clean up
event.currentTarget.classList.add('bg-gray-100')
event.currentTarget.classList.remove('bg-green-300')
}, },
drop (event) { drop (event) {
event.preventDefault()
this.$refs.file.files = event.dataTransfer.files this.$refs.file.files = event.dataTransfer.files
this.loadDb() this.loadDb()
// Clean up
event.currentTarget.classList.add('bg-gray-100')
event.currentTarget.classList.remove('bg-green-300')
} }
} }
} }
@@ -77,4 +120,80 @@ label {
input { input {
display: none; display: none;
} }
#img-container {
position: absolute;
top: calc(50% - 120px);
left: 50%;
transform: translate(-50%, -50%);
width: 450px;
height: 338px;
pointer-events: none;
}
#drop-file-top-img {
width: 450px;
height: 171px;
position: absolute;
top: 0;
left: 0;
}
#drop-file-bottom-img {
width: 450px;
height: 167px;
position: absolute;
bottom: 0;
left: 0;
}
#body-img {
width: 74px;
position: absolute;
top: 94.05px;
left: 46px;
}
#right-arm-img {
width: 106px;
position: absolute;
top: 110.05px;
left: 78px;
}
#left-arm-img {
width: 114px;
position: absolute;
top: 69.05px;
left: 69px;
}
#file-img {
width: 125px;
position: absolute;
top: 15.66px;
left: 152px;
}
.swing {
animation: swing ease-in-out 0.6s infinite alternate;
}
#left-arm-img.swing {
transform-origin: 9px 83px;
}
#right-arm-img.swing {
transform-origin: 0 56px;
}
#file-img.swing {
transform-origin: -74px 139px;
}
@keyframes swing {
0% { transform: rotate(0deg); }
100% { transform: rotate(-7deg); }
}
#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); }
}
</style> </style>

View File

@@ -101,6 +101,7 @@ export default {
const isFromScratch = !this.currentQuery.initName const isFromScratch = !this.currentQuery.initName
if (isFromScratch || this.isPredefined) { if (isFromScratch || this.isPredefined) {
this.name = ''
this.$modal.show('save') this.$modal.show('save')
} else { } else {
this.saveQuery() this.saveQuery()

View File

@@ -76,6 +76,9 @@ export default {
methods: { methods: {
changeDb () { changeDb () {
this.$db.loadDb(this.$refs.dbfile.files[0]) this.$db.loadDb(this.$refs.dbfile.files[0])
.then((schema) => {
this.$store.commit('saveSchema', schema)
})
} }
} }
} }

View File

@@ -98,4 +98,8 @@ export default {
height: 100%; height: 100%;
max-height: 100%; max-height: 100%;
} }
>>> .CodeMirror-cursor {
width: 1px;
background: var(--color-text-base);
}
</style> </style>

View File

@@ -1,35 +1,36 @@
import store from '@/store' import store from '@/store'
import router from '@/router'
const worker = new Worker('js/worker.sql-wasm.js') const worker = new Worker('js/worker.sql-wasm.js')
export default { export default {
loadDb (file) { loadDb (file) {
const dbName = file.name return new Promise((resolve, reject) => {
store.commit('saveDbName', dbName) const dbName = file.name
const f = file store.commit('saveDbName', dbName)
const r = new FileReader() const f = file
r.onload = () => { const r = new FileReader()
worker.onmessage = () => { r.onload = () => {
const getSchemaSql = ` // on 'action: open' completed
SELECT name, sql worker.onmessage = () => {
FROM sqlite_master const getSchemaSql = `
WHERE type='table' AND name NOT LIKE 'sqlite_%';` SELECT name, sql
worker.onmessage = event => { FROM sqlite_master
store.commit('saveSchema', event.data.results[0].values) WHERE type='table' AND name NOT LIKE 'sqlite_%';`
if (router.currentRoute.path !== '/editor') {
router.push('/editor') // on 'action: exec' completed
worker.onmessage = event => {
resolve(event.data.results[0].values)
} }
worker.postMessage({ action: 'exec', sql: getSchemaSql })
}
try {
worker.postMessage({ action: 'open', buffer: r.result }, [r.result])
} catch (exception) {
worker.postMessage({ action: 'open', buffer: r.result })
} }
worker.postMessage({ action: 'exec', sql: getSchemaSql })
} }
store.commit('saveDbFile', r.result) r.readAsArrayBuffer(f)
try { })
worker.postMessage({ action: 'open', buffer: r.result }, [r.result])
} catch (exception) {
worker.postMessage({ action: 'open', buffer: r.result })
}
}
r.readAsArrayBuffer(f)
}, },
execute (commands) { execute (commands) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@@ -9,7 +9,7 @@
<schema v-if="$store.state.schema"/> <schema v-if="$store.state.schema"/>
<div v-else id="empty-schema-container"> <div v-else id="empty-schema-container">
<div class="warning"> <div class="warning">
Database is not uploaded. Queries cant be run without database. Database is not loaded. Queries cant be run without database.
</div> </div>
<db-upload id="db-uploader"/> <db-upload id="db-uploader"/>
</div> </div>

View File

@@ -1,9 +1,11 @@
<template> <template>
<div id="dbloader-container"> <div id="dbloader-container">
<h1>Sqliteviz</h1> <db-upload illustrated />
<db-upload /> <div id="note">
Sqliteviz is fully client-side. Your database never leaves your computer.
</div>
<button id ="skip" class="secondary" @click="$router.push('/editor')"> <button id ="skip" class="secondary" @click="$router.push('/editor')">
Skip database connection for now Skip database loading
</button> </button>
</div> </div>
</template> </template>
@@ -13,33 +15,7 @@ import dbUpload from '@/components/DbUpload'
export default { export default {
name: 'Home', name: 'Home',
components: { dbUpload }, components: { dbUpload }
methods: {
loadDb () {
this.$db.loadDb(this.$refs.file.files[0])
},
dragover (event) {
event.preventDefault()
// TODO: Add some visual stuff to show the user can drop its files
if (!event.currentTarget.classList.contains('bg-green-300')) {
event.currentTarget.classList.remove('bg-gray-100')
event.currentTarget.classList.add('bg-green-300')
}
},
dragleave (event) {
// Clean up
event.currentTarget.classList.add('bg-gray-100')
event.currentTarget.classList.remove('bg-green-300')
},
drop (event) {
event.preventDefault()
this.$refs.file.files = event.dataTransfer.files
this.loadDb()
// Clean up
event.currentTarget.classList.add('bg-gray-100')
event.currentTarget.classList.remove('bg-green-300')
}
}
} }
</script> </script>
@@ -53,18 +29,26 @@ export default {
justify-content: center; justify-content: center;
} }
h1 { #note {
color: var(--color-accent); margin-top: 27px;
font-size: 13px;
color: var(--color-text-base);
} }
#skip { #skip {
position: absolute; margin-top: 83px;
bottom: 50px;
} }
>>>.drop-area { >>>.drop-area {
width: 628px; width: 706px;
height: 490px; height: 482px;
padding: 0 150px; padding: 0 150px;
position: relative;
}
>>>.drop-area .text {
position: absolute;
bottom: 42px;
max-width: 300px;
} }
</style> </style>

View File

@@ -29,7 +29,7 @@ export default {
xhr.onload = () => { xhr.onload = () => {
if (xhr.readyState === 4) { if (xhr.readyState === 4) {
if (xhr.status === 200) { if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText)) resolve(JSON.parse(xhr.responseText || '[]'))
} else { } else {
reject(xhr.statusText) reject(xhr.statusText)
} }

View File

@@ -0,0 +1,39 @@
/* import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message'
const wrapper = shallowMount(HelloWorld, {
propsData: { msg }
})
expect(wrapper.text()).to.include(msg)
})
})
*/
import { expect } from 'chai'
import initSqlJs from 'sql.js'
import db from '@/dataBase.js'
describe('dataBase.js', () => {
it('creates schema', () => {
const config = {
locateFile: filename => `js/sql-wasm.wasm`
}
return initSqlJs(config)
.then(SQL => {
const dataBase = new SQL.Database()
dataBase.run('CREATE TABLE test (col1, col2);')
const data = dataBase.export()
const buffer = new Blob([data])
return db.loadDb(buffer)
})
.then(schema => {
expect(schema.length).to.equal(1)
expect(schema[0][0]).to.equal('test')
expect(schema[0][1]).to.equal('CREATE TABLE test (col1, col2)')
})
})
})

View File

@@ -1,13 +0,0 @@
import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message'
const wrapper = shallowMount(HelloWorld, {
propsData: { msg }
})
expect(wrapper.text()).to.include(msg)
})
})

View File

@@ -8,7 +8,8 @@ module.exports = {
// This wasm file will be fetched dynamically when we initialize sql.js // This wasm file will be fetched dynamically when we initialize sql.js
// It is important that we do not change its name, and that it is in the same folder as the js // It is important that we do not change its name, and that it is in the same folder as the js
{ from: 'node_modules/sql.js/dist/sql-wasm.wasm', to: 'js/' }, { from: 'node_modules/sql.js/dist/sql-wasm.wasm', to: 'js/' },
{ from: 'node_modules/sql.js/dist/worker.sql-wasm.js', to: 'js/' } { from: 'node_modules/sql.js/dist/worker.sql-wasm.js', to: 'js/' },
{ from: 'LICENSE', to: './' }
]) ])
] ]
} }