mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-07 02:28:54 +08:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fec8fb5ac0 | ||
|
|
621a41844e | ||
|
|
0a3a94444e | ||
|
|
37aa2d35d5 | ||
|
|
5f91180a8c | ||
|
|
b8c5a2bfd7 | ||
|
|
880c15762b | ||
|
|
df54c9086b | ||
|
|
fdd50b2f86 | ||
|
|
b39a6bdb86 | ||
|
|
5a8b2584ff | ||
|
|
aae47eff86 | ||
|
|
65db2556c0 | ||
|
|
d132127143 | ||
|
|
3e5e4b29c1 | ||
|
|
71c70e0232 | ||
|
|
8f49c0509f | ||
|
|
5e29a051b2 | ||
|
|
8b76258260 | ||
|
|
518270e1f5 |
1202
package-lock.json
generated
1202
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"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",
|
||||||
"build": "vue-cli-service build",
|
"build": "NODE_OPTIONS=--max_old_space_size=4096 vue-cli-service build",
|
||||||
"test:unit": "vue-cli-service test:unit",
|
"test:unit": "vue-cli-service test:unit",
|
||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
@@ -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",
|
||||||
|
|||||||
17
src/assets/images/checkbox_checked.svg
Normal file
17
src/assets/images/checkbox_checked.svg
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="0.5" y="0.5" width="17" height="17" rx="2.5" fill="#119DFF" stroke="#0D76BF"/>
|
||||||
|
<g filter="url(#filter0_d)">
|
||||||
|
<path d="M15.75 5.25L6.75 14.25L2.625 10.125L3.6825 9.0675L6.75 12.1275L14.6925 4.1925L15.75 5.25Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d" x="0.625" y="3.1925" width="17.125" height="14.0575" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||||
|
<feOffset dy="1"/>
|
||||||
|
<feGaussianBlur stdDeviation="1"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.164706 0 0 0 0 0.247059 0 0 0 0 0.372549 0 0 0 0.7 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 981 B |
17
src/assets/images/checkbox_checked_light.svg
Normal file
17
src/assets/images/checkbox_checked_light.svg
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="0.5" y="0.5" width="17" height="17" rx="2.5" fill="#F3F6FA" stroke="#C8D4E3"/>
|
||||||
|
<g filter="url(#filter0_d)">
|
||||||
|
<path d="M15.75 5.24988L6.75 14.2499L2.625 10.1249L3.6825 9.06738L6.75 12.1274L14.6925 4.19238L15.75 5.24988Z" fill="#119DFF"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d" x="0.625" y="3.19238" width="17.125" height="14.0575" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||||
|
<feOffset dy="1"/>
|
||||||
|
<feGaussianBlur stdDeviation="1"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.164706 0 0 0 0 0.247059 0 0 0 0 0.372549 0 0 0 0.45 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 996 B |
@@ -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>
|
||||||
72
src/components/CheckBox.vue
Normal file
72
src/components/CheckBox.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="checkbox-container" @click.stop="onClick">
|
||||||
|
<div v-show="!checked" class="unchecked" />
|
||||||
|
<img
|
||||||
|
v-show="checked && theme === 'accent'"
|
||||||
|
:src="require('@/assets/images/checkbox_checked.svg')"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-show="checked && theme === 'light'"
|
||||||
|
:src="require('@/assets/images/checkbox_checked_light.svg')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'checkBox',
|
||||||
|
props: {
|
||||||
|
theme: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'accent',
|
||||||
|
validator: (value) => {
|
||||||
|
return ['accent', 'light'].includes(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
init: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
checked: this.init
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
checked () {
|
||||||
|
this.$emit('change', this.checked)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onClick () {
|
||||||
|
this.checked = !this.checked
|
||||||
|
this.$emit('click', this.checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.checkbox-container {
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.unchecked {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius-medium);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.unchecked:hover {
|
||||||
|
background-color: var(--color-bg-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</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'
|
||||||
|
|
||||||
|
|||||||
@@ -15,18 +15,31 @@
|
|||||||
Import
|
Import
|
||||||
</label>
|
</label>
|
||||||
</button>
|
</button>
|
||||||
<button class="toolbar" v-show="selectedQueries.length > 0">Export</button>
|
<button
|
||||||
<button class="toolbar" v-show="selectedQueries.length > 0">Delete</button>
|
class="toolbar"
|
||||||
|
v-show="selectedQueriesCount > 0"
|
||||||
|
@click="exportQuery(selectedQueriesIds)"
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="toolbar"
|
||||||
|
v-show="selectedQueriesCount > 0"
|
||||||
|
@click="showDeleteDialog(selectedQueriesIds)"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="toolbar-search">
|
<div id="toolbar-search">
|
||||||
<text-field placeholder="Search query by name" width="300px"/>
|
<text-field placeholder="Search query by name" width="300px" v-model="filter"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-bg">
|
<div class="rounded-bg">
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div>
|
<div>
|
||||||
<div class="fixed-header" ref="name-th">
|
<div class="fixed-header" ref="name-th">
|
||||||
Name
|
<check-box ref="mainCheckBox" theme="light" @click="toggleSelectAll"/>
|
||||||
|
<div class="name-th">Name</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fixed-header">
|
<div class="fixed-header">
|
||||||
Created at
|
Created at
|
||||||
@@ -39,18 +52,25 @@
|
|||||||
>
|
>
|
||||||
<table ref="table">
|
<table ref="table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(query, index) in queries" :key="query.id" @click="openQuery(index)">
|
<tr v-for="(query, index) in showedQueries" :key="query.id" @click="openQuery(index)">
|
||||||
<td ref="name-td">
|
<td ref="name-td">
|
||||||
{{ query.name }}
|
<div class="cell-data">
|
||||||
|
<check-box
|
||||||
|
ref="rowCheckBox"
|
||||||
|
:init="selectAll || selectedQueriesIds.has(query.id)"
|
||||||
|
@change="toggleRow($event, query.id)"
|
||||||
|
/>
|
||||||
|
<div class="name">{{ query.name }}</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="second-column">
|
<div class="second-column">
|
||||||
<div class="date-container">{{ query.createdAt | date }}</div>
|
<div class="date-container">{{ query.createdAt | date }}</div>
|
||||||
<div class="icons-container">
|
<div class="icons-container">
|
||||||
<rename-icon @click="showRenameDialog(index)" />
|
<rename-icon @click="showRenameDialog(query.id)" />
|
||||||
<copy-icon @click="duplicateQuery(index)"/>
|
<copy-icon @click="duplicateQuery(index)"/>
|
||||||
<export-icon @click="exportQuery(index)"/>
|
<export-icon @click="exportQuery(index)"/>
|
||||||
<delete-icon @click="showDeleteDialog(index)"/>
|
<delete-icon @click="showDeleteDialog(query.id)"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -84,19 +104,24 @@
|
|||||||
<!--Delete Query dialog -->
|
<!--Delete Query dialog -->
|
||||||
<modal name="delete" classes="dialog" height="auto">
|
<modal name="delete" classes="dialog" height="auto">
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
Delete query
|
Delete {{ deleteGroup ? 'queries' : 'query' }}
|
||||||
<close-icon @click="$modal.hide('delete')"/>
|
<close-icon @click="$modal.hide('delete')"/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
currentQueryIndex !== null
|
deleteGroup || (
|
||||||
&& currentQueryIndex >= 0
|
currentQueryIndex !== null
|
||||||
&& currentQueryIndex < queries.length
|
&& currentQueryIndex >= 0
|
||||||
|
&& currentQueryIndex < queries.length
|
||||||
|
)
|
||||||
"
|
"
|
||||||
class="dialog-body"
|
class="dialog-body"
|
||||||
>
|
>
|
||||||
Are you sure you want to delete
|
Are you sure you want to delete
|
||||||
"{{ queries[currentQueryIndex].name }}"?
|
{{ deleteGroup
|
||||||
|
? `${selectedQueriesCount} ${selectedQueriesCount > 1 ? 'queries' : 'query'}`
|
||||||
|
: `"${queries[currentQueryIndex].name}"`
|
||||||
|
}}?
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-buttons-container">
|
<div class="dialog-buttons-container">
|
||||||
<button class="secondary" @click="$modal.hide('delete')">Cancel</button>
|
<button class="secondary" @click="$modal.hide('delete')">Cancel</button>
|
||||||
@@ -114,6 +139,7 @@ import ExportIcon from '@/components/svg/export'
|
|||||||
import DeleteIcon from '@/components/svg/delete'
|
import DeleteIcon from '@/components/svg/delete'
|
||||||
import CloseIcon from '@/components/svg/close'
|
import CloseIcon from '@/components/svg/close'
|
||||||
import TextField from '@/components/TextField'
|
import TextField from '@/components/TextField'
|
||||||
|
import CheckBox from '@/components/CheckBox'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -124,15 +150,32 @@ export default {
|
|||||||
ExportIcon,
|
ExportIcon,
|
||||||
DeleteIcon,
|
DeleteIcon,
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
TextField
|
TextField,
|
||||||
|
CheckBox
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
queries: [],
|
queries: [],
|
||||||
|
filter: null,
|
||||||
newName: null,
|
newName: null,
|
||||||
currentQueryIndex: null,
|
currentQueryId: null,
|
||||||
errorMsg: null,
|
errorMsg: null,
|
||||||
selectedQueries: []
|
selectedQueriesIds: new Set(),
|
||||||
|
selectedQueriesCount: 0,
|
||||||
|
selectAll: false,
|
||||||
|
deleteGroup: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
showedQueries () {
|
||||||
|
if (!this.filter) {
|
||||||
|
return this.queries
|
||||||
|
} else {
|
||||||
|
return this.queries.filter(query => query.name.toUpperCase().indexOf(this.filter.toUpperCase()) >= 0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
currentQueryIndex () {
|
||||||
|
return this.queries.findIndex(query => query.id === this.currentQueryId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
@@ -162,15 +205,15 @@ export default {
|
|||||||
this.$refs['name-th'].style = `width: ${this.$refs['name-td'][0].offsetWidth}px`
|
this.$refs['name-th'].style = `width: ${this.$refs['name-td'][0].offsetWidth}px`
|
||||||
},
|
},
|
||||||
openQuery (index) {
|
openQuery (index) {
|
||||||
const tab = this.queries[index]
|
const tab = this.showedQueries[index]
|
||||||
tab.isUnsaved = false
|
tab.isUnsaved = false
|
||||||
this.$store.commit('addTab', tab)
|
this.$store.commit('addTab', tab)
|
||||||
this.$store.commit('setCurrentTabId', tab.id)
|
this.$store.commit('setCurrentTabId', tab.id)
|
||||||
this.$router.push('/editor')
|
this.$router.push('/editor')
|
||||||
},
|
},
|
||||||
showRenameDialog (index) {
|
showRenameDialog (id) {
|
||||||
this.errorMsg = null
|
this.errorMsg = null
|
||||||
this.currentQueryIndex = index
|
this.currentQueryId = id
|
||||||
this.newName = this.queries[this.currentQueryIndex].name
|
this.newName = this.queries[this.currentQueryIndex].name
|
||||||
this.$modal.show('rename')
|
this.$modal.show('rename')
|
||||||
},
|
},
|
||||||
@@ -190,41 +233,82 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
duplicateQuery (index) {
|
duplicateQuery (index) {
|
||||||
const newQuery = JSON.parse(JSON.stringify(this.queries[index]))
|
const newQuery = JSON.parse(JSON.stringify(this.showedQueries[index]))
|
||||||
newQuery.name = newQuery.name + ' Copy'
|
newQuery.name = newQuery.name + ' Copy'
|
||||||
newQuery.id = nanoid()
|
newQuery.id = nanoid()
|
||||||
newQuery.createdAt = new Date()
|
newQuery.createdAt = new Date()
|
||||||
this.queries.push(newQuery)
|
this.queries.push(newQuery)
|
||||||
|
if (this.selectAll) {
|
||||||
|
this.selectedQueriesIds.add(newQuery.id)
|
||||||
|
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||||
|
}
|
||||||
this.saveQueriesInLocalStorage()
|
this.saveQueriesInLocalStorage()
|
||||||
},
|
},
|
||||||
showDeleteDialog (index) {
|
showDeleteDialog (id) {
|
||||||
this.currentQueryIndex = index
|
this.deleteGroup = typeof id !== 'string'
|
||||||
|
if (!this.deleteGroup) {
|
||||||
|
this.currentQueryId = id
|
||||||
|
}
|
||||||
this.$modal.show('delete')
|
this.$modal.show('delete')
|
||||||
},
|
},
|
||||||
deleteQuery () {
|
deleteQuery () {
|
||||||
this.$modal.hide('delete')
|
this.$modal.hide('delete')
|
||||||
const id = this.queries[this.currentQueryIndex].id
|
if (!this.deleteGroup) {
|
||||||
this.queries.splice(this.currentQueryIndex, 1)
|
this.queries.splice(this.currentQueryIndex, 1)
|
||||||
this.saveQueriesInLocalStorage()
|
const tabIndex = this.findTabIndex(this.currentQueryId)
|
||||||
const tabIndex = this.findTabIndex(id)
|
if (tabIndex >= 0) {
|
||||||
if (tabIndex >= 0) {
|
this.$store.commit('deleteTab', tabIndex)
|
||||||
this.$store.commit('deleteTab', tabIndex)
|
}
|
||||||
|
if (this.selectedQueriesIds.has(this.currentQueryId)) {
|
||||||
|
this.selectedQueriesIds.delete(this.currentQueryId)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.queries = this.selectAll
|
||||||
|
? []
|
||||||
|
: this.queries.filter(query => !this.selectedQueriesIds.has(query.id))
|
||||||
|
const tabs = this.$store.state.tabs
|
||||||
|
for (let i = tabs.length - 1; i >= 0; i--) {
|
||||||
|
if (this.selectedQueriesIds.has(tabs[i].id)) {
|
||||||
|
this.$store.commit('deleteTab', i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.selectedQueriesIds.clear()
|
||||||
}
|
}
|
||||||
|
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||||
|
this.saveQueriesInLocalStorage()
|
||||||
},
|
},
|
||||||
findTabIndex (id) {
|
findTabIndex (id) {
|
||||||
return this.$store.state.tabs.findIndex(tab => tab.id === id)
|
return this.$store.state.tabs.findIndex(tab => tab.id === id)
|
||||||
},
|
},
|
||||||
exportQuery (index) {
|
exportQuery (index) {
|
||||||
this.currentQueryIndex = index
|
let data
|
||||||
|
let name
|
||||||
|
|
||||||
|
// single operation
|
||||||
|
if (typeof index === 'number') {
|
||||||
|
console.log('single')
|
||||||
|
data = JSON.parse(JSON.stringify(this.showedQueries[index]))
|
||||||
|
name = data.name
|
||||||
|
delete data.id
|
||||||
|
delete data.createdAt
|
||||||
|
} else {
|
||||||
|
// group operation
|
||||||
|
data = this.selectAll
|
||||||
|
? JSON.parse(JSON.stringify(this.queries))
|
||||||
|
: this.queries.filter(query => this.selectedQueriesIds.has(query.id))
|
||||||
|
name = 'My sqliteviz queries'
|
||||||
|
data.forEach(query => {
|
||||||
|
delete query.id
|
||||||
|
delete query.createdAt
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// export data to file
|
||||||
const downloader = this.$refs.downloader
|
const downloader = this.$refs.downloader
|
||||||
const currentQuery = JSON.parse(JSON.stringify(this.queries[this.currentQueryIndex]))
|
const json = JSON.stringify(data, null, 4)
|
||||||
delete currentQuery.id
|
|
||||||
delete currentQuery.createdAt
|
|
||||||
const json = JSON.stringify(currentQuery)
|
|
||||||
const blob = new Blob([json], { type: 'octet/stream' })
|
const blob = new Blob([json], { type: 'octet/stream' })
|
||||||
const url = window.URL.createObjectURL(blob)
|
const url = window.URL.createObjectURL(blob)
|
||||||
downloader.href = url
|
downloader.href = url
|
||||||
downloader.download = `${currentQuery.name}.json`
|
downloader.download = `${name}.json`
|
||||||
downloader.click()
|
downloader.click()
|
||||||
window.URL.revokeObjectURL(url)
|
window.URL.revokeObjectURL(url)
|
||||||
},
|
},
|
||||||
@@ -241,6 +325,10 @@ export default {
|
|||||||
importedQueries.forEach(query => {
|
importedQueries.forEach(query => {
|
||||||
query.id = nanoid()
|
query.id = nanoid()
|
||||||
query.createdAt = new Date()
|
query.createdAt = new Date()
|
||||||
|
if (this.selectAll) {
|
||||||
|
this.selectedQueriesIds.add(query.id)
|
||||||
|
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.queries = this.queries.concat(importedQueries)
|
this.queries = this.queries.concat(importedQueries)
|
||||||
@@ -251,6 +339,24 @@ export default {
|
|||||||
},
|
},
|
||||||
saveQueriesInLocalStorage () {
|
saveQueriesInLocalStorage () {
|
||||||
localStorage.setItem('myQueries', JSON.stringify(this.queries))
|
localStorage.setItem('myQueries', JSON.stringify(this.queries))
|
||||||
|
},
|
||||||
|
toggleSelectAll (checked) {
|
||||||
|
this.selectAll = checked
|
||||||
|
this.$refs.rowCheckBox.forEach(item => { item.checked = checked })
|
||||||
|
this.selectedQueriesIds = checked ? new Set(this.queries.map(query => query.id)) : new Set()
|
||||||
|
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||||
|
},
|
||||||
|
toggleRow (checked, id) {
|
||||||
|
if (checked) {
|
||||||
|
this.selectedQueriesIds.add(id)
|
||||||
|
} else {
|
||||||
|
if (this.selectedQueriesIds.size === this.queries.length) {
|
||||||
|
this.$refs.mainCheckBox.checked = false
|
||||||
|
this.selectAll = false
|
||||||
|
}
|
||||||
|
this.selectedQueriesIds.delete(id)
|
||||||
|
}
|
||||||
|
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,24 +381,43 @@ export default {
|
|||||||
max-width: 1500px;
|
max-width: 1500px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.fixed-header:first-child {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
.fixed-header:first-child .name-th {
|
||||||
|
margin-left: 24px;
|
||||||
|
}
|
||||||
table {
|
table {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody tr td {
|
tbody tr td {
|
||||||
overflow: hidden;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
text-overflow: ellipsis;
|
|
||||||
padding: 0 24px;
|
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody tr td:first-child {
|
tbody tr td:first-child {
|
||||||
width: 70%;
|
width: 70%;
|
||||||
max-width: 0;
|
max-width: 0;
|
||||||
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
tbody tr td:last-child {
|
tbody tr td:last-child {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
max-width: 0;
|
max-width: 0;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody .cell-data {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
tbody .cell-data div.name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-left: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody tr:hover td {
|
tbody tr:hover td {
|
||||||
|
|||||||
Reference in New Issue
Block a user