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

14 Commits
0.2.0 ... 0.3.0

Author SHA1 Message Date
lana-k
fec8fb5ac0 update version 2020-10-25 22:30:33 +01:00
lana-k
621a41844e add table messages 2020-10-25 22:28:48 +01:00
lana-k
0a3a94444e add a message about no data for chart 2020-10-25 21:31:18 +01:00
lana-k
37aa2d35d5 minor changes 2020-10-25 17:33:53 +01:00
lana-k
5f91180a8c small style fixes 2020-10-25 16:49:06 +01:00
lana-k
b8c5a2bfd7 codemirror wrapped as a vue component 2020-10-25 16:41:23 +01:00
lana-k
880c15762b chart is a separated component now 2020-10-24 18:21:48 +02:00
lana-k
df54c9086b save plotly settings 2020-10-24 16:59:44 +02:00
lana-k
fdd50b2f86 not greedy splitter 2020-10-22 22:47:15 +02:00
lana-k
b39a6bdb86 disable Run button without schema 2020-10-20 19:14:37 +02:00
lana-k
5a8b2584ff add dataBase lib 2020-10-20 19:05:38 +02:00
lana-k
aae47eff86 fix typo 2020-10-20 17:21:24 +02:00
lana-k
65db2556c0 add start guid 2020-10-20 17:20:29 +02:00
lana-k
d132127143 add skip db button 2020-10-20 15:37:41 +02:00
17 changed files with 1472 additions and 631 deletions

1202
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "sqliteviz", "name": "sqliteviz",
"version": "0.0.1", "version": "0.3.0",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
@@ -12,9 +12,9 @@
"codemirror": "^5.57.0", "codemirror": "^5.57.0",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"nanoid": "^3.1.12", "nanoid": "^3.1.12",
"plotly.js": "^1.54.6", "plotly.js": "^1.57.1",
"react": "^16.13.1", "react": "^16.13.1",
"react-chart-editor": "^0.41.7", "react-chart-editor": "^0.42.0",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"sql.js": "^1.3.0", "sql.js": "^1.3.0",
"sqlite-parser": "^1.0.1", "sqlite-parser": "^1.0.1",

View File

@@ -11,6 +11,7 @@
--color-blue-dark: #0D76BF; --color-blue-dark: #0D76BF;
--color-blue-dark-2: #2A3F5F; --color-blue-dark-2: #2A3F5F;
--color-red: #EF553B; --color-red: #EF553B;
--color-yellow: #FBEFCB;
@@ -18,6 +19,7 @@
--color-bg-light-2: var(--color-gray-light-2); --color-bg-light-2: var(--color-gray-light-2);
--color-bg-light-3: var(--color-gray-light-5); --color-bg-light-3: var(--color-gray-light-5);
--color-bg-dark: var(--color-gray-dark); --color-bg-dark: var(--color-gray-dark);
--color-bg-warning: var(--color-yellow);
--color-accent: var(--color-blue-medium); --color-accent: var(--color-blue-medium);
--color-accent-shade: var(--color-blue-dark); --color-accent-shade: var(--color-blue-dark);
--color-border-light: var(--color-gray-light-2); --color-border-light: var(--color-gray-light-2);

131
src/components/Chart.vue Normal file
View File

@@ -0,0 +1,131 @@
<template>
<div class="chart-container">
<div class="chart-worning" v-show="!sqlResult && visible">
There is no data to build a chart. Run your sql query and make sure the result is not empty.
</div>
<PlotlyEditor
v-show="visible"
:data="state.data"
:layout="state.layout"
:frames="state.frames"
:config="{ editable: true, displaylogo: false }"
:dataSources="dataSources"
:dataSourceOptions="dataSourceOptions"
:plotly="plotly"
@onUpdate="update"
:useResizeHandler="true"
:debug="true"
:advancedTraceTypeSelector="true"
class="chart"
:style="{ height: !sqlResult ? 'calc(100% - 40px)' : '100%' }"
/>
</div>
</template>
<script>
import plotly from 'plotly.js/dist/plotly'
import 'react-chart-editor/lib/react-chart-editor.min.css'
import PlotlyEditor from 'react-chart-editor'
import dereference from 'react-chart-editor/lib/lib/dereference'
export default {
name: 'Chart',
props: ['sqlResult', 'initChart', 'visible'],
components: {
PlotlyEditor
},
data () {
return {
plotly: plotly,
state: this.initChart || {
data: [],
layout: {},
frames: []
}
}
},
computed: {
dataSources () {
if (!this.sqlResult) {
return {}
}
const dataSorces = {}
const matrix = this.sqlResult.values
const [row] = matrix
const transposedMatrix = row.map((value, column) => matrix.map(row => row[column]))
this.sqlResult.columns.forEach((column, index) => {
dataSorces[column] = transposedMatrix[index]
})
return dataSorces
},
dataSourceOptions () {
return Object.keys(this.dataSources).map(name => ({
value: name,
label: name
}))
}
},
watch: {
dataSources () {
// we need to update state.data in order to update the graph
// https://github.com/plotly/react-chart-editor/issues/948
dereference(this.state.data, this.dataSources)
}
},
methods: {
update (data, layout, frames) {
this.state = { data, layout, frames }
this.$emit('update')
},
getChartSatateForSave () {
// we don't need to save the data, only settings
// so we modify state.data using dereference
const stateCopy = JSON.parse(JSON.stringify(this.state))
const emptySources = {}
for (const key in this.dataSources) {
emptySources[key] = []
}
dereference(stateCopy.data, emptySources)
return stateCopy
}
}
}
</script>
<style scoped>
.chart-container {
height: calc(100% - 89px);
}
.chart-worning {
background-color: var(--color-bg-warning);
height: 40px;
line-height: 40px;
color: var(--color-text-base);
font-size: 13px;
padding: 0 24px;
}
.chart {
border-top: 1px solid var(--color-border);
}
>>> .editor_controls .sidebar__item:before {
width: 0;
}
>>> .sidebar {
width: 120px;
min-width: 120px;
max-width: 120px;
}
>>> .editor_controls__wrapper>.panel,
>>> .editor_controls .panel__empty {
width: 315px;
}
>>> .editor_controls .sidebar__group__title {
padding-left: 10px;
}
>>> .editor_controls .sidebar__item {
padding-left: 32px;
}
</style>

View File

@@ -8,7 +8,7 @@
<button <button
v-if="$store.state.tabs.length > 0" v-if="$store.state.tabs.length > 0"
class="primary" class="primary"
:disabled="!$store.state.currentTab.isUnsaved" :disabled="$store.state.currentTab && !$store.state.currentTab.isUnsaved"
@click="saveQuery" @click="saveQuery"
> >
Save Save
@@ -23,6 +23,9 @@ import { nanoid } from 'nanoid'
export default { export default {
name: 'MainMenu', name: 'MainMenu',
created () {
this.$root.$on('createNewQuery', this.createNewQuery)
},
methods: { methods: {
createNewQuery () { createNewQuery () {
const tab = { const tab = {
@@ -42,8 +45,8 @@ export default {
const isFromScratch = !this.$store.state.currentTab.initName const isFromScratch = !this.$store.state.currentTab.initName
const value = { const value = {
id: currentQuery.id, id: currentQuery.id,
query: currentQuery.query query: currentQuery.query,
// TODO: save plotly settings chart: currentQuery.getChartSatateForSave()
} }
if (isFromScratch) { if (isFromScratch) {

View File

@@ -57,8 +57,7 @@ export default {
components: { TableDescription, TextField }, components: { TableDescription, TextField },
data () { data () {
return { return {
schemaVisible: true, schemaVisible: true
worker: this.$store.state.worker
} }
}, },
computed: { computed: {
@@ -71,29 +70,7 @@ export default {
}, },
methods: { methods: {
changeDb () { changeDb () {
const dbName = this.$refs.dbfile.value.substr(this.$refs.dbfile.value.lastIndexOf('\\') + 1) this.$db.loadDb(this.$refs.dbfile.files[0])
this.$store.commit('saveDbName', dbName)
const f = this.$refs.dbfile.files[0]
const r = new FileReader()
r.onload = () => {
this.worker.onmessage = () => {
const getSchemaSql = `
SELECT name, sql
FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%';`
this.worker.onmessage = event => {
this.$store.commit('saveSchema', event.data.results[0].values)
}
this.worker.postMessage({ action: 'exec', sql: getSchemaSql })
}
this.$store.commit('saveDbFile', r.result)
try {
this.worker.postMessage({ action: 'open', buffer: r.result }, [r.result])
} catch (exception) {
this.worker.postMessage({ action: 'open', buffer: r.result })
}
}
r.readAsArrayBuffer(f)
} }
} }
} }

View File

@@ -1,8 +1,13 @@
<template> <template>
<div <div
ref="container" ref="container"
:class="['splitpanes', `splitpanes--${horizontal ? 'horizontal' : 'vertical'}`, { 'splitpanes--dragging': touch.dragging }]" :class="[
'splitpanes',
`splitpanes--${horizontal ? 'horizontal' : 'vertical'}`,
{ 'splitpanes--dragging': touch.dragging }
]"
> >
<div class="movable-splitter" ref="movableSplitter" :style="movableSplitterStyle" />
<div <div
class="splitpanes__pane" class="splitpanes__pane"
ref="left" ref="left"
@@ -12,13 +17,21 @@
> >
<slot name="left-pane" /> <slot name="left-pane" />
</div> </div>
<!-- Splitter start-->
<splitter <div
class="splitpanes__splitter"
@mousedown="onMouseDown" @mousedown="onMouseDown"
@toggle="toggleFirstPane" @touchstart="onMouseDown"
:expanded="paneBefore.size !== 0" >
/> <div class="toggle-btn" @click="toggleFirstPane">
<img
class="direction-icon"
:src="require('@/assets/images/chevron.svg')"
:style="directionIconStyle"
>
</div>
</div>
<!-- splitter end -->
<div <div
class="splitpanes__pane" class="splitpanes__pane"
ref="right" ref="right"
@@ -30,33 +43,58 @@
</template> </template>
<script> <script>
import Splitter from '@/components/splitter'
export default { export default {
name: 'splitpanes', name: 'Splitpanes',
components: { Splitter },
props: { props: {
horizontal: { type: Boolean, default: false }, horizontal: { type: Boolean, default: false },
before: { type: Object }, before: { type: Object },
after: { type: Object } after: { type: Object }
}, },
data: () => ({ data () {
container: null, return {
paneBefore: null, container: null,
paneAfter: null, paneBefore: this.before,
beforeMinimising: 20, paneAfter: this.after,
touch: { beforeMinimising: this.before.size,
mouseDown: false, touch: {
dragging: false mouseDown: false,
dragging: false
},
movableSplitter: {
top: 0,
left: 0,
visibility: 'hidden'
}
} }
}), },
computed: { computed: {
styles () { styles () {
return [ return [
{ [this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%` }, { [this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%` },
{ [this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%` } { [this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%` }
] ]
},
movableSplitterStyle () {
const style = { ...this.movableSplitter }
style.top += '%'
style.left += '%'
return style
},
expanded () {
return this.paneBefore.size !== 0
},
directionIconStyle () {
const translation = 'translate(-50%, -50%)'
if (this.horizontal) {
return {
transform: `${translation} ${this.expanded ? 'rotate(-90deg)' : 'rotate(90deg)'}`
}
} else {
return {
transform: `${translation} ${this.expanded ? 'rotate(180deg)' : ''}`
}
}
} }
}, },
@@ -92,12 +130,28 @@ export default {
// Prevent scrolling while touch dragging (only works with an active event, eg. passive: false). // Prevent scrolling while touch dragging (only works with an active event, eg. passive: false).
event.preventDefault() event.preventDefault()
this.touch.dragging = true this.touch.dragging = true
this.calculatePanesSize(this.getCurrentMouseDrag(event)) this.$set(this.movableSplitter, 'visibility', 'visible')
this.moveSplitter(event)
} }
}, },
onMouseUp () { onMouseUp () {
this.touch.mouseDown = false this.touch.mouseDown = false
if (this.touch.dragging) {
const dragPercentage = this.horizontal
? this.movableSplitter.top
: this.movableSplitter.left
this.paneBefore.size = dragPercentage
this.paneAfter.size = 100 - dragPercentage
this.movableSplitter = {
top: 0,
left: 0,
visibility: 'hidden'
}
}
// Keep dragging flag until click event is finished (click happens immediately after mouseup) // Keep dragging flag until click event is finished (click happens immediately after mouseup)
// in order to prevent emitting `splitter-click` event if splitter was dragged. // in order to prevent emitting `splitter-click` event if splitter was dragged.
setTimeout(() => { setTimeout(() => {
@@ -109,8 +163,9 @@ export default {
// Get the cursor position relative to the splitpane container. // Get the cursor position relative to the splitpane container.
getCurrentMouseDrag (event) { getCurrentMouseDrag (event) {
const rect = this.container.getBoundingClientRect() const rect = this.container.getBoundingClientRect()
const { clientX, clientY } = ('ontouchstart' in window && event.touches) ? event.touches[0] : event const { clientX, clientY } = ('ontouchstart' in window && event.touches)
? event.touches[0]
: event
return { return {
x: clientX - rect.left, x: clientX - rect.left,
y: clientY - rect.top y: clientY - rect.top
@@ -126,27 +181,26 @@ export default {
return drag * 100 / containerSize return drag * 100 / containerSize
}, },
calculatePanesSize (drag) { moveSplitter (event) {
const dragPercentage = this.getCurrentDragPercentage(drag) const dragPercentage = this.getCurrentDragPercentage(this.getCurrentMouseDrag(event))
// If not pushing other panes, panes to resize are right before and right after splitter.
const paneBefore = this.paneBefore const paneBefore = this.paneBefore
const paneAfter = this.paneAfter const paneAfter = this.paneAfter
const paneBeforeMaxReached = paneBefore.max < 100 && (dragPercentage >= paneBefore.max) const paneBeforeMaxReached = paneBefore.max < 100 && (dragPercentage >= paneBefore.max)
const paneAfterMaxReached = paneAfter.max < 100 && (dragPercentage <= 100 - paneAfter.max) const paneAfterMaxReached = paneAfter.max < 100 && (dragPercentage <= 100 - paneAfter.max)
const dir = this.horizontal ? 'top' : 'left'
// Prevent dragging beyond pane max. // Prevent dragging beyond pane max.
if (paneBeforeMaxReached || paneAfterMaxReached) { if (paneBeforeMaxReached || paneAfterMaxReached) {
if (paneBeforeMaxReached) { if (paneBeforeMaxReached) {
paneBefore.size = paneBefore.max this.$set(this.movableSplitter, dir, paneBefore.max)
paneAfter.size = Math.max(100 - paneBefore.max, 0)
} else { } else {
paneBefore.size = Math.max(100 - paneAfter.max, 0) this.$set(this.movableSplitter, dir, Math.max(100 - paneAfter.max, 0))
paneAfter.size = paneAfter.max
} }
return } else {
this.$set(this.movableSplitter, dir, Math.min(Math.max(dragPercentage, 0), paneBefore.max))
} }
paneBefore.size = Math.min(Math.max(dragPercentage, 0), paneBefore.max)
paneAfter.size = Math.min(Math.max(100 - dragPercentage, 0), paneAfter.max)
}, },
toggleFirstPane () { toggleFirstPane () {
if (this.paneBefore.size > 0) { if (this.paneBefore.size > 0) {
@@ -160,10 +214,6 @@ export default {
}, },
mounted () { mounted () {
this.container = this.$refs.container this.container = this.$refs.container
},
created () {
this.paneBefore = this.before
this.paneAfter = this.after
} }
} }
</script> </script>
@@ -172,6 +222,7 @@ export default {
.splitpanes { .splitpanes {
display: flex; display: flex;
height: 100%; height: 100%;
position: relative;
} }
.splitpanes--vertical {flex-direction: row;} .splitpanes--vertical {flex-direction: row;}
@@ -183,4 +234,68 @@ export default {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
} }
/* Splitter */
.splitpanes--vertical > .splitpanes__splitter,
.splitpanes--vertical.splitpanes--dragging {
cursor: col-resize;
}
.splitpanes--horizontal > .splitpanes__splitter,
.splitpanes--horizontal.splitpanes--dragging {
cursor: row-resize;
}
.splitpanes__splitter {
touch-action: none;
background-color: var(--color-bg-light-2);
box-sizing: border-box;
position: relative;
flex-shrink: 0;
z-index: 1;
}
.movable-splitter {
position: absolute;
background-color:rgba(162, 177, 198, 0.5);
}
.splitpanes--vertical > .splitpanes__splitter,
.splitpanes--vertical .movable-splitter {
width: 3px;
z-index: 5;
height: 100%
}
.splitpanes--horizontal > .splitpanes__splitter,
.splitpanes--horizontal .movable-splitter {
height: 3px;
width: 100%;
}
.splitpanes__splitter .toggle-btn {
background-color: var(--color-bg-light-2);
border-radius: var(--border-radius-small);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.splitpanes__splitter .toggle-btn:hover {
cursor: pointer;
}
.splitpanes--vertical .toggle-btn {
height: 68px;
width: 15px;
}
.splitpanes--horizontal .toggle-btn {
width: 68px;
height: 15px;
}
.splitpanes__splitter .toggle-btn .direction-icon {
position: absolute;
top: 50%;
left: 50%;
}
</style> </style>

View File

@@ -0,0 +1,82 @@
<template>
<div class="codemirror-container">
<codemirror v-model="query" :options="cmOptions" @changes="onCmChange" />
</div>
</template>
<script>
import CM from 'codemirror'
import { codemirror } from 'vue-codemirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/mode/sql/sql.js'
import 'codemirror/theme/neo.css'
import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/hint/show-hint.css'
import 'codemirror/addon/hint/sql-hint.js'
export default {
name: 'SqlEditor',
props: ['value'],
components: {
codemirror
},
data () {
return {
query: this.value,
cmOptions: {
// codemirror options
tabSize: 4,
mode: 'text/x-mysql',
theme: 'neo',
lineNumbers: true,
line: true
},
result: null
}
},
watch: {
query () {
this.$emit('input', this.query)
}
},
methods: {
onCmChange (editor) {
// Don't show autocomplete after a space or semicolon
const ch = editor.getTokenAt(editor.getCursor()).string.slice(-1)
if (!ch || ch === ' ' || ch === ';') {
return
}
const hintOptions = {
// tables: this.state.tables,
completeSingle: false,
completeOnSingleClick: true
}
// editor.hint.sql is defined when importing codemirror/addon/hint/sql-hint
// (this is mentioned in codemirror addon documentation)
// Reference the hint function imported here when including other hint addons
// or supply your own
CM.showHint(editor, CM.hint.sql, hintOptions)
}
}
}
</script>
<style scoped>
.codemirror-container {
flex-grow: 1;
min-height: 0;
}
>>> .vue-codemirror {
height: 100%;
max-height: 100%;
}
>>> .CodeMirror {
border: 1px solid var(--color-border);
border-radius: var(--border-radius-big);
height: 100%;
max-height: 100%;
}
</style>

View File

@@ -17,7 +17,7 @@
class="table-container" class="table-container"
ref="table-container" ref="table-container"
@scroll="onScrollTable" @scroll="onScrollTable"
:style="{height: `${height}px`}" :style="{maxHeight: `${height}px`}"
> >
<table ref="table"> <table ref="table">
<thead> <thead>

View File

@@ -6,122 +6,89 @@
:before="{ size: 50, max: 50 }" :before="{ size: 50, max: 50 }"
:after="{ size: 50, max: 100 }" :after="{ size: 50, max: 100 }"
> >
<div slot="left-pane" class="query-editor"> <template #left-pane>
<div class="codemirror-container"> <div class="query-editor">
<codemirror v-model="query" :options="cmOptions" @changes="onCmChange" ref="codemirror" /> <sql-editor v-model="query" />
<div class="run-btn-container">
<button
class="primary run-btn"
@click="execute"
:disabled="!$store.state.schema || !query"
>
Run
</button>
</div>
</div> </div>
<div class="run-btn-container"> </template>
<button class="primary run-btn" @click="execEditorContents">Run</button> <template #right-pane>
<div id="bottomPane" ref="bottomPane">
<view-switcher :view.sync="view" />
<div v-show="view === 'table'" class="table-view">
<div v-show="result === null && !isGettingResults && !error" class="table-preview">
Run your query and get results here
</div>
<div v-show="isGettingResults" class="table-preview">
Fetching results...
</div>
<div v-show="result === undefined && !isGettingResults && !error" class="table-preview">
No rows retrieved according to your query
</div>
<div v-show="error" class="table-preview error">
{{ error }}
</div>
<sql-table v-if="result" :data="result" :height="tableViewHeight" />
</div>
<chart
:visible="view === 'chart'"
:sql-result="result"
:init-chart="initChart"
ref="chart"
@update="isUnsaved = true"
/>
</div> </div>
</div> </template>
<div slot="right-pane" id="bottomPane" ref="bottomPane">
<view-switcher :view.sync="view" />
<div v-show="view === 'table'" class="table-view">
<!-- <div id="error" class="error"></div>
<pre ref="output" id="output">Results will be displayed here</pre> -->
<sql-table v-if="result" :data="result" :height="tableViewHeight" />
</div>
<PlotlyEditor
v-show="view === 'chart'"
:data="state.data"
:layout="state.layout"
:frames="state.frames"
:config="{ editable: true }"
:dataSources="dataSources"
:dataSourceOptions="dataSourceOptions"
:plotly="plotly"
@onUpdate="update"
:useResizeHandler="true"
:debug="true"
:advancedTraceTypeSelector="true"
/>
</div>
</splitpanes> </splitpanes>
</div> </div>
</template> </template>
<script> <script>
import SqlTable from '@/components/SqlTable' import SqlTable from '@/components/SqlTable'
import Splitpanes from '@/components/splitpanes' import SqlEditor from '@/components/SqlEditor'
import Splitpanes from '@/components/Splitpanes'
import ViewSwitcher from '@/components/ViewSwitcher' import ViewSwitcher from '@/components/ViewSwitcher'
import Chart from '@/components/Chart'
import plotly from 'plotly.js/dist/plotly'
import 'react-chart-editor/lib/react-chart-editor.min.css'
import CM from 'codemirror'
import { codemirror } from 'vue-codemirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/mode/sql/sql.js'
import 'codemirror/theme/neo.css'
import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/hint/show-hint.css'
import 'codemirror/addon/hint/sql-hint.js'
import PlotlyEditor from 'react-chart-editor'
export default { export default {
name: 'TabContent', name: 'TabContent',
props: ['id', 'initName', 'initQuery', 'initPlotly', 'tabIndex'], props: ['id', 'initName', 'initQuery', 'initChart', 'tabIndex'],
components: { components: {
codemirror, SqlEditor,
SqlTable, SqlTable,
Splitpanes, Splitpanes,
ViewSwitcher, ViewSwitcher,
PlotlyEditor Chart
}, },
data () { data () {
return { return {
plotly: plotly, query: this.initQuery,
state: {
data: [],
layout: {},
frames: []
},
query: 'select * from albums',
cmOptions: {
// codemirror options
tabSize: 4,
mode: 'text/x-mysql',
theme: 'neo',
lineNumbers: true,
line: true
},
result: null, result: null,
view: 'table', view: 'table',
tableViewHeight: 0, tableViewHeight: 0,
worker: this.$store.state.worker, isUnsaved: !this.initName,
isUnsaved: !this.name isGettingResults: false,
error: null
} }
}, },
computed: { computed: {
isActive () { isActive () {
return this.id === this.$store.state.currentTabId return this.id === this.$store.state.currentTabId
},
dataSources () {
if (!this.result) {
return {}
}
const dataSorces = {}
const matrix = this.result.values
const [row] = matrix
const transposedMatrix = row.map((value, column) => matrix.map(row => row[column]))
this.result.columns.forEach((column, index) => {
dataSorces[column] = transposedMatrix[index]
})
return dataSorces
},
dataSourceOptions () {
return Object.keys(this.dataSources).map(name => ({
value: name,
label: name
}))
} }
}, },
created () { created () {
this.$store.commit('setCurrentTab', this) this.$store.commit('setCurrentTab', this)
}, },
mounted () { mounted () {
new ResizeObserver(this.calculateTableHeight).observe(this.$refs.bottomPane) new ResizeObserver(this.handleResize).observe(this.$refs.bottomPane)
this.calculateTableHeight() this.calculateTableHeight()
}, },
watch: { watch: {
@@ -138,47 +105,32 @@ export default {
} }
}, },
methods: { methods: {
update (data, layout, frames) {
this.state = { data, layout, frames }
this.isUnsaved = true
console.log(this.state)
},
onCmChange (editor) {
// Don't show autocomplete after a space or semicolon
const ch = editor.getTokenAt(editor.getCursor()).string.slice(-1)
if (!ch || ch === ' ' || ch === ';') {
return
}
const hintOptions = {
// tables: this.state.tables,
completeSingle: false,
completeOnSingleClick: true
}
// editor.hint.sql is defined when importing codemirror/addon/hint/sql-hint
// (this is mentioned in codemirror addon documentation)
// Reference the hint function imported here when including other hint addons
// or supply your own
CM.showHint(editor, CM.hint.sql, hintOptions)
},
// Run a command in the database // Run a command in the database
execute (commands) { execute () {
this.worker.onmessage = (event) => { // this.$refs.output.textContent = 'Fetching results...' */
// if it was more than one select - take only the first one this.isGettingResults = true
this.result = event.data.results[0] this.result = null
if (!this.result) { this.error = null
console.log(event.data.error) this.$db.execute(this.query + ';')
// return .then(result => {
} this.result = result
})
// this.$refs.output.innerHTML = '' .catch(err => {
} this.error = err
this.worker.postMessage({ action: 'exec', sql: commands }) })
// this.$refs.output.textContent = 'Fetching results...' .finally(() => {
this.isGettingResults = false
})
}, },
execEditorContents () { handleResize () {
this.execute(this.query + ';') if (this.view === 'chart') {
// hack react-chart editor: hidden and show in order to make the graph resize
this.view = 'not chart'
this.$nextTick(() => {
this.view = 'chart'
})
}
this.calculateTableHeight()
}, },
calculateTableHeight () { calculateTableHeight () {
const bottomPane = this.$refs.bottomPane const bottomPane = this.$refs.bottomPane
@@ -189,6 +141,9 @@ export default {
// 40 - height of table header // 40 - height of table header
const freeSpace = bottomPane.offsetHeight - 88 - 42 - 30 - 5 - 40 const freeSpace = bottomPane.offsetHeight - 88 - 42 - 30 - 5 - 40
this.tableViewHeight = freeSpace - (freeSpace % 40) this.tableViewHeight = freeSpace - (freeSpace % 40)
},
getChartSatateForSave () {
return this.$refs.chart.getChartSatateForSave()
} }
} }
} }
@@ -222,29 +177,33 @@ export default {
height: 100%; height: 100%;
max-height: 100%; max-height: 100%;
box-sizing: border-box; box-sizing: border-box;
min-height: 150px; min-height: 190px;
}
.codemirror-container {
flex-grow: 1;
min-height: 0;
} }
.run-btn-container { .run-btn-container {
text-align: right; text-align: right;
} }
>>> .vue-codemirror {
height: 100%;
max-height: 100%;
}
>>> .CodeMirror {
border: 1px solid var(--color-border);
border-radius: var(--border-radius-big);
height: 100%;
max-height: 100%;
}
.table-view { .table-view {
margin: 0 52px; margin: 0 52px;
height: calc(100% - 88px);
position: relative;
}
.table-preview {
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--color-text-base);
font-size: 13px;
}
.table-preview.error {
color: var(--color-text-error);
}
.table-preview.error::first-letter {
text-transform: capitalize;
} }
</style> </style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div> <div id="tabs-container">
<div id="tabs__header"> <div id="tabs__header" v-if="tabs.length > 0">
<div <div
v-for="(tab, index) in tabs" v-for="(tab, index) in tabs"
:key="tab.id" :key="tab.id"
@@ -34,8 +34,15 @@
:key="tab.id" :key="tab.id"
:id="tab.id" :id="tab.id"
:init-name="tab.name" :init-name="tab.name"
:init-query="tab.query"
:init-chart="tab.chart"
:tab-index="index" :tab-index="index"
/> />
<div v-if="tabs.length === 0" id="start-guide">
<span class="link" @click="$root.$emit('createNewQuery')">Create</span>
a new query from scratch or open the one from
<router-link class="link" to="/my-queries">My queries</router-link>
</div>
</div> </div>
</template> </template>
@@ -70,6 +77,10 @@ export default {
</script> </script>
<style> <style>
#tabs-container {
position: relative;
height: 100%;
}
#tabs__header { #tabs__header {
display: flex; display: flex;
margin: 0; margin: 0;
@@ -129,4 +140,18 @@ export default {
fill: var(--color-text-base); fill: var(--color-text-base);
cursor: pointer; cursor: pointer;
} }
#start-guide {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--color-text-base);
font-size: 14px;
text-align: center;
}
.link {
color: var(--color-accent);
text-decoration: none;
cursor: pointer;
}
</style> </style>

View File

@@ -1,85 +0,0 @@
<template>
<div
class="splitpanes__splitter"
@mousedown="$emit('mousedown')"
@touchstart="$emit('mousedown')"
>
<div class="toggle-btn" @click="$emit('toggle')">
<img
class="direction-icon"
:src="require('@/assets/images/chevron.svg')"
:style="directionIconStyle"
>
</div>
</div>
</template>
<script>
export default {
name: 'splitter',
props: ['expanded'],
computed: {
directionIconStyle () {
const translation = 'translate(-50%, -50%)'
if (this.$parent.horizontal) {
return {
transform: `${translation} ${this.expanded ? 'rotate(-90deg)' : 'rotate(90deg)'}`
}
} else {
return {
transform: `${translation} ${this.expanded ? 'rotate(180deg)' : ''}`
}
}
}
}
}
</script>
<style>
.splitpanes--vertical > .splitpanes__splitter {min-width: 1px;cursor: col-resize;}
.splitpanes--horizontal > .splitpanes__splitter {min-height: 1px; cursor: row-resize;}
.splitpanes__splitter {
touch-action: none;
background-color: var(--color-bg-light-2);
box-sizing: border-box;
position: relative;
flex-shrink: 0;
z-index: 1;
}
.splitpanes--vertical > .splitpanes__splitter {
width: 3px;
z-index: 3;
}
.splitpanes--horizontal > .splitpanes__splitter {
height: 3px;
width: 100%;
}
.splitpanes__splitter .toggle-btn {
background-color: var(--color-bg-light-2);
border-radius: var(--border-radius-small);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.splitpanes__splitter .toggle-btn:hover {
cursor: pointer;
}
.splitpanes--vertical .toggle-btn {
height: 68px;
width: 15px;
}
.splitpanes--horizontal .toggle-btn {
width: 68px;
height: 15px;
}
.splitpanes__splitter .toggle-btn .direction-icon {
position: absolute;
top: 50%;
left: 50%;
}
</style>

46
src/dataBase.js Normal file
View File

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

View File

@@ -4,6 +4,7 @@ import router from './router'
import store from './store' import store from './store'
import { VuePlugin } from 'vuera' import { VuePlugin } from 'vuera'
import VModal from 'vue-js-modal' import VModal from 'vue-js-modal'
import db from '@/dataBase'
import '@/assets/styles/variables.css' import '@/assets/styles/variables.css'
import '@/assets/styles/buttons.css' import '@/assets/styles/buttons.css'
@@ -14,6 +15,7 @@ Vue.use(VuePlugin)
Vue.use(VModal) Vue.use(VModal)
Vue.config.productionTip = false Vue.config.productionTip = false
Vue.prototype.$db = db
new Vue({ new Vue({
router, router,

View File

@@ -8,7 +8,6 @@ export default new Vuex.Store({
schema: null, schema: null,
dbFile: null, dbFile: null,
dbName: null, dbName: null,
worker: new Worker('js/worker.sql-wasm.js'),
tabs: [], tabs: [],
currentTab: null, currentTab: null,
currentTabId: null, currentTabId: null,

View File

@@ -16,47 +16,22 @@
</div> </div>
</label> </label>
<div id="error" class="error"></div> <div id="error" class="error"></div>
<button id ="skip" class="secondary" @click="$router.push('/editor')">
Skip database connection for now
</button>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'DbUpload', name: 'DbUpload',
data () {
return {
worker: this.$store.state.worker
}
},
methods: { methods: {
loadDb () { loadDb () {
const dbName = this.$refs.file.value.substr(this.$refs.file.value.lastIndexOf('\\') + 1) this.$db.loadDb(this.$refs.file.files[0])
this.$store.commit('saveDbName', dbName)
const f = this.$refs.file.files[0]
const r = new FileReader()
r.onload = () => {
this.worker.onmessage = () => {
const getSchemaSql = `
SELECT name, sql
FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%';`
this.worker.onmessage = event => {
this.$store.commit('saveSchema', event.data.results[0].values)
this.$router.push('/editor')
}
this.worker.postMessage({ action: 'exec', sql: getSchemaSql })
}
this.$store.commit('saveDbFile', r.result)
try {
this.worker.postMessage({ action: 'open', buffer: r.result }, [r.result])
} catch (exception) {
this.worker.postMessage({ action: 'open', buffer: r.result })
}
}
r.readAsArrayBuffer(f)
}, },
dragover (event) { dragover (event) {
event.preventDefault() event.preventDefault()
// TODO: Add some visual fluff to show the user can drop its files // TODO: Add some visual stuff to show the user can drop its files
if (!event.currentTarget.classList.contains('bg-green-300')) { if (!event.currentTarget.classList.contains('bg-green-300')) {
event.currentTarget.classList.remove('bg-gray-100') event.currentTarget.classList.remove('bg-gray-100')
event.currentTarget.classList.add('bg-green-300') event.currentTarget.classList.add('bg-green-300')
@@ -98,17 +73,21 @@ label {
border-radius: var(--border-radius-big); border-radius: var(--border-radius-big);
} }
#drop-area { #drop-area {
width: 231px; width: 628px;
height: 153px; height: 490px;
background-color: var(--color-bg-light-3); background-color: var(--color-bg-light-3);
border-radius: var(--border-radius-big); border-radius: var(--border-radius-big);
color: var(--color-text-base); color: var(--color-text-base);
font-size: 13px; font-size: 13px;
padding: 44px 15px; padding: 200px 144px;
text-align: center; text-align: center;
box-sizing: border-box; box-sizing: border-box;
} }
input { input {
display: none; display: none;
} }
#skip {
position: absolute;
bottom: 50px;
}
</style> </style>

View File

@@ -5,18 +5,18 @@
:before="{ size: 20, max: 30 }" :before="{ size: 20, max: 30 }"
:after="{ size: 80, max: 100 }" :after="{ size: 80, max: 100 }"
> >
<div slot="left-pane"> <template #left-pane>
<schema /> <schema />
</div> </template>
<div slot="right-pane"> <template #right-pane>
<tabs /> <tabs />
</div> </template>
</splitpanes> </splitpanes>
</div> </div>
</template> </template>
<script> <script>
import Splitpanes from '@/components/splitpanes' import Splitpanes from '@/components/Splitpanes'
import Schema from '@/components/Schema' import Schema from '@/components/Schema'
import Tabs from '@/components/Tabs' import Tabs from '@/components/Tabs'