mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-06 18:18:53 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fec8fb5ac0 | ||
|
|
621a41844e | ||
|
|
0a3a94444e | ||
|
|
37aa2d35d5 | ||
|
|
5f91180a8c | ||
|
|
b8c5a2bfd7 | ||
|
|
880c15762b | ||
|
|
df54c9086b | ||
|
|
fdd50b2f86 | ||
|
|
b39a6bdb86 | ||
|
|
5a8b2584ff | ||
|
|
aae47eff86 | ||
|
|
65db2556c0 | ||
|
|
d132127143 |
1202
package-lock.json
generated
1202
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||||
|
|||||||
@@ -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
131
src/components/Chart.vue
Normal 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>
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
82
src/components/SqlEditor.vue
Normal file
82
src/components/SqlEditor.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
46
src/dataBase.js
Normal 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 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user