mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-07 02:28:54 +08:00
change code structure
This commit is contained in:
51
src/views/Main/Editor/Schema/TableDescription.vue
Normal file
51
src/views/Main/Editor/Schema/TableDescription.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<div @click="colVisible = !colVisible" class="table-name">
|
||||
<tree-chevron :expanded="colVisible"/>
|
||||
{{ name }}
|
||||
</div>
|
||||
<div v-show="colVisible" class="columns">
|
||||
<div v-for="(col, index) in columns" :key="index" class="column">
|
||||
{{ col.name }}
|
||||
<span class="column-type">{{ col.type }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TreeChevron from '@/components/svg/treeChevron'
|
||||
|
||||
export default {
|
||||
name: 'TableDescription',
|
||||
components: { TreeChevron },
|
||||
props: ['name', 'columns'],
|
||||
data () {
|
||||
return {
|
||||
colVisible: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.table-name, .column {
|
||||
margin-top: 11px;
|
||||
}
|
||||
|
||||
.table-name:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.columns {
|
||||
margin-left: 24px;
|
||||
}
|
||||
.column-type {
|
||||
display: inline-block;
|
||||
background-color: var(--color-gray-light-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-small);
|
||||
padding: 2px 6px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
117
src/views/Main/Editor/Schema/index.vue
Normal file
117
src/views/Main/Editor/Schema/index.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div id="schema-container">
|
||||
<div id="schema-filter">
|
||||
<text-field placeholder="Search table" width="100%" v-model="filter"/>
|
||||
</div>
|
||||
<div id="db">
|
||||
<div @click="schemaVisible = !schemaVisible" class="db-name">
|
||||
<tree-chevron :expanded="schemaVisible"/>
|
||||
{{ dbName }}
|
||||
</div>
|
||||
<db-uploader id="db-edit" type="small" />
|
||||
<export-icon tooltip="Export database" @click="exportToFile"/>
|
||||
</div>
|
||||
<div v-show="schemaVisible" class="schema">
|
||||
<table-description
|
||||
v-for="table in schema"
|
||||
:key="table.name"
|
||||
:name="table.name"
|
||||
:columns="table.columns"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TableDescription from './TableDescription'
|
||||
import TextField from '@/components/TextField'
|
||||
import TreeChevron from '@/components/svg/treeChevron'
|
||||
import DbUploader from '@/components/DbUploader'
|
||||
import ExportIcon from '@/components/svg/export'
|
||||
|
||||
export default {
|
||||
name: 'Schema',
|
||||
components: {
|
||||
TableDescription,
|
||||
TextField,
|
||||
TreeChevron,
|
||||
DbUploader,
|
||||
ExportIcon
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
schemaVisible: true,
|
||||
filter: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
schema () {
|
||||
if (!this.$store.state.schema) {
|
||||
return []
|
||||
}
|
||||
|
||||
return !this.filter
|
||||
? this.$store.state.schema
|
||||
: this.$store.state.schema.filter(
|
||||
table => table.name.toUpperCase().indexOf(this.filter.toUpperCase()) !== -1
|
||||
)
|
||||
},
|
||||
dbName () {
|
||||
return this.$store.state.dbName
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
exportToFile () {
|
||||
this.$store.state.db.export(`${this.dbName}.sqlite`)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#schema-container {
|
||||
position: relative;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.schema {
|
||||
margin-left: 12px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
#schema-filter {
|
||||
padding: 32px 12px;
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
box-sizing: border-box;
|
||||
background-image: linear-gradient(white 73%, transparent);;
|
||||
z-index: 2;
|
||||
}
|
||||
.schema, .db-name {
|
||||
color: var(--color-text-base);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#db {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: -5px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.db-name {
|
||||
cursor: pointer;
|
||||
margin-right: 6px;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.db-name:hover .chevron-icon path,
|
||||
>>> .table-name:hover .chevron-icon path {
|
||||
fill: var(--color-gray-dark);
|
||||
}
|
||||
</style>
|
||||
40
src/views/Main/Editor/Tabs/Tab/Chart/chartHelper.js
Normal file
40
src/views/Main/Editor/Tabs/Tab/Chart/chartHelper.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import dereference from 'react-chart-editor/lib/lib/dereference'
|
||||
|
||||
export function getDataSourcesFromSqlResult (sqlResult) {
|
||||
if (!sqlResult) {
|
||||
return {}
|
||||
}
|
||||
const dataSorces = {}
|
||||
const matrix = sqlResult.values
|
||||
const [row] = matrix
|
||||
const transposedMatrix = row.map((value, column) => matrix.map(row => row[column]))
|
||||
sqlResult.columns.forEach((column, index) => {
|
||||
dataSorces[column] = transposedMatrix[index]
|
||||
})
|
||||
return dataSorces
|
||||
}
|
||||
|
||||
export function getOptionsFromDataSources (dataSources) {
|
||||
return Object.keys(dataSources).map(name => ({
|
||||
value: name,
|
||||
label: name
|
||||
}))
|
||||
}
|
||||
|
||||
export function getChartStateForSave (state, dataSources) {
|
||||
// we don't need to save the data, only settings
|
||||
// so we modify state.data using dereference
|
||||
const stateCopy = JSON.parse(JSON.stringify(state))
|
||||
const emptySources = {}
|
||||
for (const key in dataSources) {
|
||||
emptySources[key] = []
|
||||
}
|
||||
dereference(stateCopy.data, emptySources)
|
||||
return stateCopy
|
||||
}
|
||||
|
||||
export default {
|
||||
getDataSourcesFromSqlResult,
|
||||
getOptionsFromDataSources,
|
||||
getChartStateForSave
|
||||
}
|
||||
98
src/views/Main/Editor/Tabs/Tab/Chart/index.vue
Normal file
98
src/views/Main/Editor/Tabs/Tab/Chart/index.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div v-show="visible" class="chart-container">
|
||||
<div class="warning chart-warning" 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
|
||||
:data="state.data"
|
||||
:layout="state.layout"
|
||||
:frames="state.frames"
|
||||
:config="{ editable: true, displaylogo: false }"
|
||||
:dataSources="dataSources"
|
||||
:dataSourceOptions="dataSourceOptions"
|
||||
:plotly="plotly"
|
||||
@onUpdate="update"
|
||||
@onRender="go"
|
||||
:useResizeHandler="true"
|
||||
:debug="true"
|
||||
:advancedTraceTypeSelector="true"
|
||||
class="chart"
|
||||
ref="plotlyEditor"
|
||||
: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 chartHelper from './chartHelper'
|
||||
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 () {
|
||||
return chartHelper.getDataSourcesFromSqlResult(this.sqlResult)
|
||||
},
|
||||
dataSourceOptions () {
|
||||
return chartHelper.getOptionsFromDataSources(this.dataSources)
|
||||
}
|
||||
},
|
||||
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: {
|
||||
go (data, layout, frames) {
|
||||
// TODO: check changes and enable Save button if needed
|
||||
},
|
||||
update (data, layout, frames) {
|
||||
this.state = { data, layout, frames }
|
||||
this.$emit('update')
|
||||
},
|
||||
getChartStateForSave () {
|
||||
return chartHelper.getChartStateForSave(this.state, this.dataSources)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-container {
|
||||
height: calc(100% - 89px);
|
||||
}
|
||||
|
||||
.chart-warning {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.chart {
|
||||
border-top: 1px solid var(--color-border);
|
||||
min-height: 242px;
|
||||
}
|
||||
|
||||
>>> .editor_controls .sidebar__item:before {
|
||||
width: 0;
|
||||
}
|
||||
</style>
|
||||
45
src/views/Main/Editor/Tabs/Tab/SqlEditor/hint.js
Normal file
45
src/views/Main/Editor/Tabs/Tab/SqlEditor/hint.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import CM from 'codemirror'
|
||||
import 'codemirror/addon/hint/show-hint.js'
|
||||
import 'codemirror/addon/hint/sql-hint.js'
|
||||
import store from '@/store'
|
||||
|
||||
export function getHints (cm, options) {
|
||||
const token = cm.getTokenAt(cm.getCursor()).string.toUpperCase()
|
||||
const result = CM.hint.sql(cm, options)
|
||||
// Don't show the hint if there is only one option
|
||||
// and the token is already completed with this option
|
||||
if (result.list.length === 1 && result.list[0].text.toUpperCase() === token) {
|
||||
result.list = []
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const hintOptions = {
|
||||
get tables () {
|
||||
const tables = {}
|
||||
if (store.state.schema) {
|
||||
store.state.schema.forEach(table => {
|
||||
tables[table.name] = table.columns.map(column => column.name)
|
||||
})
|
||||
}
|
||||
return tables
|
||||
},
|
||||
get defaultTable () {
|
||||
const schema = store.state.schema
|
||||
return schema && schema.length === 1 ? schema[0].name : null
|
||||
},
|
||||
completeSingle: false,
|
||||
completeOnSingleClick: true,
|
||||
alignWithWord: false
|
||||
}
|
||||
|
||||
export default function showHint (editor) {
|
||||
// Don't show autocomplete after a space or semicolon or in string literals
|
||||
const token = editor.getTokenAt(editor.getCursor())
|
||||
const ch = token.string.slice(-1)
|
||||
const tokenType = token.type
|
||||
if (tokenType === 'string' || !ch || ch === ' ' || ch === ';') {
|
||||
return
|
||||
}
|
||||
CM.showHint(editor, getHints, hintOptions)
|
||||
}
|
||||
65
src/views/Main/Editor/Tabs/Tab/SqlEditor/index.vue
Normal file
65
src/views/Main/Editor/Tabs/Tab/SqlEditor/index.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="codemirror-container">
|
||||
<codemirror v-model="query" :options="cmOptions" @changes="onChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import showHint from './hint'
|
||||
import { debounce } from 'debounce'
|
||||
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.css'
|
||||
import 'codemirror/addon/display/autorefresh.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,
|
||||
autofocus: true,
|
||||
autoRefresh: true
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
query () {
|
||||
this.$emit('input', this.query)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange: debounce(showHint, 400)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.codemirror-container {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
>>> .vue-codemirror {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
>>> .CodeMirror {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
>>> .CodeMirror-cursor {
|
||||
width: 1px;
|
||||
background: var(--color-text-base);
|
||||
}
|
||||
</style>
|
||||
67
src/views/Main/Editor/Tabs/Tab/ViewSwitcher.vue
Normal file
67
src/views/Main/Editor/Tabs/Tab/ViewSwitcher.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="view-switcher">
|
||||
<div
|
||||
:class="['table-mode', {'active-mode': view === 'table'}]"
|
||||
@click="$emit('update:view','table')"
|
||||
>
|
||||
Table
|
||||
</div>
|
||||
<div
|
||||
:class="['chart-mode', {'active-mode': view === 'chart'}]"
|
||||
@click="$emit('update:view','chart')"
|
||||
>
|
||||
Chart
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ViewSwitcher',
|
||||
props: ['view']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.view-switcher {
|
||||
height: 28px;
|
||||
display: flex;
|
||||
padding: 30px;
|
||||
justify-content: center;
|
||||
}
|
||||
.view-switcher div {
|
||||
height: 100%;
|
||||
width: 136px;
|
||||
box-sizing: border-box;
|
||||
line-height: 28px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
background: var(--color-white);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-base);
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
}
|
||||
.view-switcher div:hover {
|
||||
background-color: var(--color-bg-light);
|
||||
color: var(--color-text-active);
|
||||
}
|
||||
.view-switcher div.active-mode {
|
||||
background: var(--color-accent);
|
||||
border: 1px solid var(--color-accent-shade);
|
||||
color: var(--color-text-light);
|
||||
text-shadow: var(--shadow);
|
||||
z-index: 1;
|
||||
font-weight: 600;
|
||||
}
|
||||
.view-switcher div.active-mode:hover {
|
||||
background: var(--color-accent-shade);
|
||||
}
|
||||
.table-mode {
|
||||
border-radius: var(--border-radius-medium) 0 0 var(--border-radius-medium);
|
||||
}
|
||||
.chart-mode {
|
||||
margin-left: -1px;
|
||||
border-radius: 0 var(--border-radius-medium) var(--border-radius-medium) 0;
|
||||
}
|
||||
</style>
|
||||
193
src/views/Main/Editor/Tabs/Tab/index.vue
Normal file
193
src/views/Main/Editor/Tabs/Tab/index.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="tab-content-container" v-show="isActive">
|
||||
<splitpanes
|
||||
class="query-results-splitter"
|
||||
horizontal
|
||||
:before="{ size: 50, max: 100 }"
|
||||
:after="{ size: 50, max: 100 }"
|
||||
>
|
||||
<template #left-pane>
|
||||
<div class="query-editor">
|
||||
<sql-editor v-model="query" />
|
||||
</div>
|
||||
</template>
|
||||
<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 result-before"
|
||||
>
|
||||
Run your query and get results here
|
||||
</div>
|
||||
<div v-show="isGettingResults" class="table-preview result-in-progress">
|
||||
Fetching results...
|
||||
</div>
|
||||
<div
|
||||
v-show="result === undefined && !isGettingResults && !error"
|
||||
class="table-preview result-empty"
|
||||
>
|
||||
No rows retrieved according to your query
|
||||
</div>
|
||||
<div v-show="error" class="table-preview error">
|
||||
{{ error }}
|
||||
</div>
|
||||
<sql-table v-if="result" :data-set="result" :height="tableViewHeight" />
|
||||
</div>
|
||||
<chart
|
||||
:visible="view === 'chart'"
|
||||
:sql-result="result"
|
||||
:init-chart="initChart"
|
||||
ref="chart"
|
||||
@update="$store.commit('updateTab', { index: tabIndex, isUnsaved: true })"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</splitpanes>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SqlTable from '@/components/SqlTable'
|
||||
import Splitpanes from '@/components/Splitpanes'
|
||||
import SqlEditor from './SqlEditor'
|
||||
import ViewSwitcher from './ViewSwitcher'
|
||||
import Chart from './Chart'
|
||||
|
||||
export default {
|
||||
name: 'Tab',
|
||||
props: ['id', 'initName', 'initQuery', 'initChart', 'tabIndex', 'isPredefined'],
|
||||
components: {
|
||||
SqlEditor,
|
||||
SqlTable,
|
||||
Splitpanes,
|
||||
ViewSwitcher,
|
||||
Chart
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
query: this.initQuery,
|
||||
result: null,
|
||||
view: 'table',
|
||||
tableViewHeight: 0,
|
||||
isGettingResults: false,
|
||||
error: null,
|
||||
resizeObserver: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isActive () {
|
||||
return this.id === this.$store.state.currentTabId
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.commit('setCurrentTab', this)
|
||||
},
|
||||
mounted () {
|
||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||
this.resizeObserver.observe(this.$refs.bottomPane)
|
||||
this.calculateTableHeight()
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.resizeObserver.unobserve(this.$refs.bottomPane)
|
||||
},
|
||||
watch: {
|
||||
isActive () {
|
||||
if (this.isActive) {
|
||||
this.$store.commit('setCurrentTab', this)
|
||||
}
|
||||
},
|
||||
query () {
|
||||
this.$store.commit('updateTab', { index: this.tabIndex, isUnsaved: true })
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// Run a command in the database
|
||||
async execute () {
|
||||
this.isGettingResults = true
|
||||
this.result = null
|
||||
this.error = null
|
||||
const state = this.$store.state
|
||||
try {
|
||||
this.result = await state.db.execute(this.query + ';')
|
||||
const schema = await state.db.getSchema(state.dbName)
|
||||
this.$store.commit('saveSchema', schema)
|
||||
} catch (err) {
|
||||
this.error = err
|
||||
}
|
||||
this.isGettingResults = false
|
||||
},
|
||||
handleResize () {
|
||||
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 () {
|
||||
const bottomPane = this.$refs.bottomPane
|
||||
// 88 - view swittcher height
|
||||
// 42 - table footer width
|
||||
// 30 - desirable space after the table
|
||||
// 5 - padding-bottom of rounded table container
|
||||
// 40 - height of table header
|
||||
const freeSpace = bottomPane.offsetHeight - 88 - 42 - 30 - 5 - 40
|
||||
this.tableViewHeight = freeSpace - (freeSpace % 40)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tab-content-container {
|
||||
background-color: var(--color-white);
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
#bottomPane {
|
||||
height: 100%;
|
||||
background-color: var(--color-bg-light);
|
||||
}
|
||||
|
||||
.query-results-splitter {
|
||||
height: calc(100vh - 104px);
|
||||
background-color: var(--color-bg-light);
|
||||
}
|
||||
|
||||
.query-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 190px;
|
||||
}
|
||||
|
||||
.table-view {
|
||||
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>
|
||||
195
src/views/Main/Editor/Tabs/index.vue
Normal file
195
src/views/Main/Editor/Tabs/index.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div id="tabs">
|
||||
<div id="tabs-header" v-if="tabs.length > 0">
|
||||
<div
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
@click="selectTab(tab.id)"
|
||||
:class="[{'tab-selected': (tab.id === selectedIndex)}, 'tab']"
|
||||
>
|
||||
<div class="tab-name">
|
||||
<span v-show="tab.isUnsaved" class="star">*</span>
|
||||
<span v-if="tab.name">{{ tab.name }}</span>
|
||||
<span v-else class="tab-untitled">{{ tab.tempName }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<close-icon class="close-icon" :size="10" @click="beforeCloseTab(index)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<tab
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="tab.id"
|
||||
:id="tab.id"
|
||||
:init-name="tab.name"
|
||||
:init-query="tab.query"
|
||||
:init-chart="tab.chart"
|
||||
:is-predefined="tab.isPredefined"
|
||||
:tab-index="index"
|
||||
/>
|
||||
<div v-show="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>
|
||||
|
||||
<!--Close tab warning dialog -->
|
||||
<modal name="close-warn" classes="dialog" height="auto">
|
||||
<div class="dialog-header">
|
||||
Close tab {{
|
||||
closingTabIndex !== null
|
||||
? (tabs[closingTabIndex].name || `[${tabs[closingTabIndex].tempName}]`)
|
||||
: ''
|
||||
}}
|
||||
<close-icon @click="$modal.hide('close-warn')"/>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
You have unsaved changes. Save changes in {{
|
||||
closingTabIndex !== null
|
||||
? (tabs[closingTabIndex].name || `[${tabs[closingTabIndex].tempName}]`)
|
||||
: ''
|
||||
}} before closing?
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button class="secondary" @click="closeTab(closingTabIndex)">
|
||||
Close without saving
|
||||
</button>
|
||||
<button class="secondary" @click="$modal.hide('close-warn')">Cancel</button>
|
||||
<button class="primary" @click="saveAndClose(closingTabIndex)">Save and close</button>
|
||||
</div>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Tab from './Tab'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Tab,
|
||||
CloseIcon
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
closingTabIndex: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tabs () {
|
||||
return this.$store.state.tabs
|
||||
},
|
||||
selectedIndex () {
|
||||
return this.$store.state.currentTabId
|
||||
}
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('beforeunload', this.leavingSqliteviz)
|
||||
},
|
||||
methods: {
|
||||
leavingSqliteviz (event) {
|
||||
if (this.tabs.some(tab => tab.isUnsaved)) {
|
||||
event.preventDefault()
|
||||
event.returnValue = ''
|
||||
}
|
||||
},
|
||||
selectTab (id) {
|
||||
this.$store.commit('setCurrentTabId', id)
|
||||
},
|
||||
beforeCloseTab (index) {
|
||||
this.closingTabIndex = index
|
||||
if (this.tabs[index].isUnsaved) {
|
||||
this.$modal.show('close-warn')
|
||||
} else {
|
||||
this.closeTab(index)
|
||||
}
|
||||
},
|
||||
closeTab (index) {
|
||||
this.$modal.hide('close-warn')
|
||||
this.closingTabIndex = null
|
||||
this.$store.commit('deleteTab', index)
|
||||
},
|
||||
saveAndClose (index) {
|
||||
this.$root.$on('querySaved', () => {
|
||||
this.closeTab(index)
|
||||
this.$root.$off('querySaved')
|
||||
})
|
||||
this.selectTab(this.tabs[index].id)
|
||||
this.$modal.hide('close-warn')
|
||||
this.$nextTick(() => {
|
||||
this.$root.$emit('saveQuery')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#tabs {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
background-color: var(--color-bg-light);
|
||||
}
|
||||
#tabs-header {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
#tabs-header .tab {
|
||||
height: 36px;
|
||||
background-color: var(--color-bg-light);
|
||||
border-right: 1px solid var(--color-border-light);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
line-height: 36px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-base);
|
||||
padding: 0 12px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
max-width: 200px;
|
||||
display: flex;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
#tabs-header .tab-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
#tabs-header div:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#tabs-header .tab-selected {
|
||||
color: var(--color-text-active);
|
||||
font-weight: 600;
|
||||
border-bottom: none;
|
||||
background-color: var(--color-white);
|
||||
}
|
||||
#tabs-header .tab-selected:hover {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
#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;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
74
src/views/Main/Editor/index.vue
Normal file
74
src/views/Main/Editor/index.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div>
|
||||
<splitpanes
|
||||
class="schema-tabs-splitter"
|
||||
:before="{ size: 20, max: 30 }"
|
||||
:after="{ size: 80, max: 100 }"
|
||||
>
|
||||
<template #left-pane>
|
||||
<schema v-if="$store.state.schema"/>
|
||||
<div v-else id="empty-schema-container">
|
||||
<div class="warning">
|
||||
Database is not loaded. Queries can’t be run without database.
|
||||
</div>
|
||||
<db-uploader id="db-uploader" width="100%"/>
|
||||
</div>
|
||||
</template>
|
||||
<template #right-pane>
|
||||
<tabs />
|
||||
</template>
|
||||
</splitpanes>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Splitpanes from '@/components/Splitpanes'
|
||||
import Schema from './Schema'
|
||||
import Tabs from './Tabs'
|
||||
import DbUploader from '@/components/DbUploader'
|
||||
|
||||
export default {
|
||||
name: 'Editor',
|
||||
components: {
|
||||
Schema,
|
||||
Splitpanes,
|
||||
Tabs,
|
||||
DbUploader
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.schema-tabs-splitter {
|
||||
height: 100%;
|
||||
background-color: var(--color-white);
|
||||
}
|
||||
#empty-schema-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 200px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#db-uploader {
|
||||
flex-grow: 1;
|
||||
padding: 24px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.warning {
|
||||
padding: 12px 24px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
>>>.drop-area {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
>>>.drop-area .text {
|
||||
max-width: 200px;
|
||||
}
|
||||
</style>
|
||||
240
src/views/Main/MainMenu.vue
Normal file
240
src/views/Main/MainMenu.vue
Normal file
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<nav>
|
||||
<div>
|
||||
<router-link to="/editor">Editor</router-link>
|
||||
<router-link to="/my-queries">My queries</router-link>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
id="run-btn"
|
||||
v-if="currentQuery && $route.path === '/editor'"
|
||||
class="primary"
|
||||
:disabled="runDisabled"
|
||||
@click="currentQuery.execute"
|
||||
>
|
||||
Run
|
||||
</button>
|
||||
<button
|
||||
id="save-btn"
|
||||
v-show="currentQuery && $route.path === '/editor'"
|
||||
class="primary"
|
||||
:disabled="!isUnsaved"
|
||||
@click="checkQueryBeforeSave"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
id="create-btn"
|
||||
class="primary"
|
||||
@click="createNewQuery"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!--Save Query dialog -->
|
||||
<modal name="save" classes="dialog" height="auto">
|
||||
<div class="dialog-header">
|
||||
Save query
|
||||
<close-icon @click="cancelSave"/>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<div v-show="isPredefined" id="save-note">
|
||||
<img :src="require('@/assets/images/info.svg')">
|
||||
Note: Predefined queries can't be edited.
|
||||
That's why your modifications will be saved as a new query. Enter the name for it.
|
||||
</div>
|
||||
<text-field
|
||||
label="Query name"
|
||||
:error-msg="errorMsg"
|
||||
v-model="name"
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button class="secondary" @click="cancelSave">Cancel</button>
|
||||
<button class="primary" @click="saveQuery">Save</button>
|
||||
</div>
|
||||
</modal>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TextField from '@/components/TextField'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
import storedQueries from '@/lib/storedQueries'
|
||||
|
||||
export default {
|
||||
name: 'MainMenu',
|
||||
components: {
|
||||
TextField,
|
||||
CloseIcon
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
name: '',
|
||||
errorMsg: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentQuery () {
|
||||
return this.$store.state.currentTab
|
||||
},
|
||||
isUnsaved () {
|
||||
if (!this.currentQuery) {
|
||||
return false
|
||||
}
|
||||
const tabIndex = this.currentQuery.tabIndex
|
||||
const tab = this.$store.state.tabs[tabIndex]
|
||||
return tab && tab.isUnsaved
|
||||
},
|
||||
isPredefined () {
|
||||
if (this.currentQuery) {
|
||||
return this.currentQuery.isPredefined
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
runDisabled () {
|
||||
return this.currentQuery && (!this.$store.state.schema || !this.currentQuery.query)
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$root.$on('createNewQuery', this.createNewQuery)
|
||||
this.$root.$on('saveQuery', this.checkQueryBeforeSave)
|
||||
document.addEventListener('keydown', this._keyListener)
|
||||
},
|
||||
beforeDestroy () {
|
||||
document.removeEventListener('keydown', this._keyListener)
|
||||
},
|
||||
methods: {
|
||||
createNewQuery () {
|
||||
this.$store.dispatch('addTab').then(id => {
|
||||
this.$store.commit('setCurrentTabId', id)
|
||||
if (this.$route.path !== '/editor') {
|
||||
this.$router.push('/editor')
|
||||
}
|
||||
})
|
||||
},
|
||||
cancelSave () {
|
||||
this.$modal.hide('save')
|
||||
this.$root.$off('querySaved')
|
||||
},
|
||||
checkQueryBeforeSave () {
|
||||
this.errorMsg = null
|
||||
this.name = ''
|
||||
|
||||
if (storedQueries.isTabNeedName(this.currentQuery)) {
|
||||
this.$modal.show('save')
|
||||
} else {
|
||||
this.saveQuery()
|
||||
}
|
||||
},
|
||||
saveQuery () {
|
||||
const isNeedName = storedQueries.isTabNeedName(this.currentQuery)
|
||||
if (isNeedName && !this.name) {
|
||||
this.errorMsg = 'Query name can\'t be empty'
|
||||
return
|
||||
}
|
||||
const dataSet = this.currentQuery.result
|
||||
const tabView = this.currentQuery.view
|
||||
|
||||
// Save query
|
||||
const value = storedQueries.save(this.currentQuery, this.name)
|
||||
|
||||
// Update tab in store
|
||||
this.$store.commit('updateTab', {
|
||||
index: this.currentQuery.tabIndex,
|
||||
name: value.name,
|
||||
id: value.id,
|
||||
query: value.query,
|
||||
chart: value.chart,
|
||||
isUnsaved: false
|
||||
})
|
||||
|
||||
// Restore data:
|
||||
// e.g. if we save predefined query the tab will be created again
|
||||
// (because of new id) and
|
||||
// it will be without sql result and has default view - table.
|
||||
// That's why we need to restore data and view
|
||||
this.$nextTick(() => {
|
||||
this.currentQuery.result = dataSet
|
||||
this.currentQuery.view = tabView
|
||||
})
|
||||
|
||||
// Hide dialog
|
||||
this.$modal.hide('save')
|
||||
|
||||
// Signal about saving
|
||||
this.$root.$emit('querySaved')
|
||||
},
|
||||
_keyListener (e) {
|
||||
if (this.$route.path === '/editor') {
|
||||
// Run query Ctrl+R or Ctrl+Enter
|
||||
if ((e.key === 'r' || e.key === 'Enter') && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
if (!this.runDisabled) {
|
||||
this.currentQuery.execute()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Save query Ctrl+S
|
||||
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
if (this.isUnsaved) {
|
||||
this.checkQueryBeforeSave()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
// New (blank) query Ctrl+B
|
||||
if (e.key === 'b' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
this.createNewQuery()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
nav {
|
||||
height: 68px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--color-bg-light);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
box-shadow: var(--shadow-1);
|
||||
box-sizing: border-box;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
padding: 0 52px;
|
||||
z-index: 999;
|
||||
}
|
||||
a {
|
||||
font-size: 18px;
|
||||
color: var(--color-text-base);
|
||||
text-transform: none;
|
||||
text-decoration: none;
|
||||
margin-right: 28px;
|
||||
}
|
||||
a.router-link-active {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
button {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
#save-note {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
#save-note img {
|
||||
margin: -3px 6px 0 0;
|
||||
}
|
||||
</style>
|
||||
564
src/views/Main/MyQueries/index.vue
Normal file
564
src/views/Main/MyQueries/index.vue
Normal file
@@ -0,0 +1,564 @@
|
||||
<template>
|
||||
<div>
|
||||
<div id="start-guide" v-if="showedQueries.length === 0">
|
||||
You don't have saved queries so far.
|
||||
<span class="link" @click="$root.$emit('createNewQuery')">Create</span>
|
||||
the one from scratch or
|
||||
<span @click="importQueries" class="link">import</span> from a file.
|
||||
</div>
|
||||
<div id="my-queries-content" ref="my-queries-content" v-show="showedQueries.length > 0">
|
||||
<div id="my-queries-toolbar">
|
||||
<div id="toolbar-buttons">
|
||||
<button id="toolbar-btns-import" class="toolbar" @click="importQueries">
|
||||
Import
|
||||
</button>
|
||||
<button
|
||||
id="toolbar-btns-export"
|
||||
class="toolbar"
|
||||
v-show="selectedQueriesCount > 0"
|
||||
@click="exportSelectedQueries()"
|
||||
>
|
||||
Export
|
||||
</button>
|
||||
<button
|
||||
id="toolbar-btns-delete"
|
||||
class="toolbar"
|
||||
v-show="selectedNotPredefinedCount > 0"
|
||||
@click="showDeleteDialog(selectedQueriesIds)"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<div id="toolbar-search">
|
||||
<text-field placeholder="Search query by name" width="300px" v-model="filter"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-bg">
|
||||
<div class="header-container">
|
||||
<div>
|
||||
<div class="fixed-header" ref="name-th">
|
||||
<check-box ref="mainCheckBox" theme="light" @click="toggleSelectAll"/>
|
||||
<div class="name-th">Name</div>
|
||||
</div>
|
||||
<div class="fixed-header">
|
||||
Created at
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container" :style="{ 'max-height': `${maxTableHeight}px` }">
|
||||
<table ref="table">
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(query, index) in showedQueries"
|
||||
:key="query.id"
|
||||
@click="openQuery(index)"
|
||||
>
|
||||
<td ref="name-td">
|
||||
<div class="cell-data">
|
||||
<check-box
|
||||
ref="rowCheckBox"
|
||||
:init="selectAll || selectedQueriesIds.has(query.id)"
|
||||
@click="toggleRow($event, query.id)"
|
||||
/>
|
||||
<div class="name">{{ query.name }}</div>
|
||||
<div
|
||||
v-if="query.isPredefined"
|
||||
class="badge"
|
||||
@mouseover="showTooltip"
|
||||
@mouseout="hideTooltip"
|
||||
>
|
||||
Predefined
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Predefined queries come from the server.
|
||||
These queries can’t be deleted or renamed.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="second-column">
|
||||
<div class="date-container">{{ query.createdAt | date }}</div>
|
||||
<div class="icons-container">
|
||||
<rename-icon v-if="!query.isPredefined" @click="showRenameDialog(query.id)" />
|
||||
<copy-icon @click="duplicateQuery(index)"/>
|
||||
<export-icon
|
||||
@click="exportToFile([query], `${query.name}.json`)"
|
||||
tooltip="Export query to file"
|
||||
/>
|
||||
<delete-icon
|
||||
v-if="!query.isPredefined"
|
||||
@click="showDeleteDialog((new Set()).add(query.id))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--Rename Query dialog -->
|
||||
<modal name="rename" classes="dialog" height="auto">
|
||||
<div class="dialog-header">
|
||||
Rename query
|
||||
<close-icon @click="$modal.hide('rename')"/>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<text-field
|
||||
label="New query name"
|
||||
:error-msg="errorMsg"
|
||||
v-model="newName"
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button class="secondary" @click="$modal.hide('rename')">Cancel</button>
|
||||
<button class="primary" @click="renameQuery">Rename</button>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
<!--Delete Query dialog -->
|
||||
<modal name="delete" classes="dialog" height="auto">
|
||||
<div class="dialog-header">
|
||||
Delete {{ deleteGroup ? 'queries' : 'query' }}
|
||||
<close-icon @click="$modal.hide('delete')"/>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
{{ deleteDialogMsg }}
|
||||
<div v-show="selectedQueriesCount > selectedNotPredefinedCount" id="note">
|
||||
<img :src="require('@/assets/images/info.svg')">
|
||||
Note: Predefined queries you've selected won't be deleted
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button class="secondary" @click="$modal.hide('delete')">Cancel</button>
|
||||
<button class="primary" @click="deleteQuery">Delete</button>
|
||||
</div>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RenameIcon from './svg/rename'
|
||||
import CopyIcon from './svg/copy'
|
||||
import ExportIcon from '@/components/svg/export'
|
||||
import DeleteIcon from './svg/delete'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
import TextField from '@/components/TextField'
|
||||
import CheckBox from '@/components/CheckBox'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
import storedQueries from '@/lib/storedQueries'
|
||||
import fu from '@/lib/utils/fileIo'
|
||||
|
||||
export default {
|
||||
name: 'MyQueries',
|
||||
components: {
|
||||
RenameIcon,
|
||||
CopyIcon,
|
||||
ExportIcon,
|
||||
DeleteIcon,
|
||||
CloseIcon,
|
||||
TextField,
|
||||
CheckBox
|
||||
},
|
||||
mixins: [tooltipMixin],
|
||||
data () {
|
||||
return {
|
||||
queries: [],
|
||||
filter: null,
|
||||
newName: null,
|
||||
processedQueryId: null,
|
||||
errorMsg: null,
|
||||
selectedQueriesIds: new Set(),
|
||||
selectedQueriesCount: 0,
|
||||
selectedNotPredefinedCount: 0,
|
||||
selectAll: false,
|
||||
deleteGroup: false,
|
||||
resizeObserver: null,
|
||||
maxTableHeight: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
predefinedQueries () {
|
||||
return this.$store.state.predefinedQueries.map(query => {
|
||||
query.isPredefined = true
|
||||
return query
|
||||
})
|
||||
},
|
||||
predefinedQueriesIds () {
|
||||
return new Set(this.predefinedQueries.map(query => query.id))
|
||||
},
|
||||
showedQueries () {
|
||||
let showedQueries = this.allQueries
|
||||
if (this.filter) {
|
||||
showedQueries = showedQueries.filter(
|
||||
query => query.name.toUpperCase().indexOf(this.filter.toUpperCase()) >= 0
|
||||
)
|
||||
}
|
||||
return showedQueries
|
||||
},
|
||||
allQueries () {
|
||||
return this.predefinedQueries.concat(this.queries)
|
||||
},
|
||||
processedQueryIndex () {
|
||||
return this.queries.findIndex(query => query.id === this.processedQueryId)
|
||||
},
|
||||
deleteDialogMsg () {
|
||||
if (!this.deleteGroup && (
|
||||
this.processedQueryIndex === null ||
|
||||
this.processedQueryIndex < 0 ||
|
||||
this.processedQueryIndex > this.queries.length
|
||||
)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const deleteItem = this.deleteGroup
|
||||
? `${this.selectedNotPredefinedCount} ${this.selectedNotPredefinedCount > 1
|
||||
? 'queries'
|
||||
: 'query'}`
|
||||
: `"${this.queries[this.processedQueryIndex].name}"`
|
||||
|
||||
return `Are you sure you want to delete ${deleteItem}?`
|
||||
}
|
||||
},
|
||||
created () {
|
||||
storedQueries.readPredefinedQueries()
|
||||
.then(queries => {
|
||||
this.$store.commit('updatePredefinedQueries', queries)
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
this.queries = storedQueries.getStoredQueries()
|
||||
})
|
||||
},
|
||||
mounted () {
|
||||
this.resizeObserver = new ResizeObserver(this.calcMaxTableHeight)
|
||||
this.resizeObserver.observe(this.$refs['my-queries-content'])
|
||||
|
||||
this.tableResizeObserver = new ResizeObserver(this.calcNameWidth)
|
||||
this.tableResizeObserver.observe(this.$refs.table)
|
||||
this.calcNameWidth()
|
||||
this.calcMaxTableHeight()
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.resizeObserver.unobserve(this.$refs['my-queries-content'])
|
||||
this.tableResizeObserver.unobserve(this.$refs.table)
|
||||
},
|
||||
filters: {
|
||||
date (value) {
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
const dateOptions = { year: 'numeric', month: 'long', day: 'numeric' }
|
||||
const timeOptions = {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}
|
||||
return new Date(value).toLocaleDateString('en-GB', dateOptions) + ' ' +
|
||||
new Date(value).toLocaleTimeString('en-GB', timeOptions)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
calcNameWidth () {
|
||||
const nameWidth = this.$refs['name-td']
|
||||
? this.$refs['name-td'][0].getBoundingClientRect().width
|
||||
: 0
|
||||
this.$refs['name-th'].style = `width: ${nameWidth}px`
|
||||
},
|
||||
calcMaxTableHeight () {
|
||||
const freeSpace = this.$refs['my-queries-content'].offsetHeight - 200
|
||||
this.maxTableHeight = freeSpace - (freeSpace % 40) + 1
|
||||
},
|
||||
openQuery (index) {
|
||||
const tab = this.showedQueries[index]
|
||||
this.$store.dispatch('addTab', tab).then(id => {
|
||||
this.$store.commit('setCurrentTabId', id)
|
||||
this.$router.push('/editor')
|
||||
})
|
||||
},
|
||||
showRenameDialog (id) {
|
||||
this.errorMsg = null
|
||||
this.processedQueryId = id
|
||||
this.newName = this.queries[this.processedQueryIndex].name
|
||||
this.$modal.show('rename')
|
||||
},
|
||||
renameQuery () {
|
||||
if (!this.newName) {
|
||||
this.errorMsg = 'Query name can\'t be empty'
|
||||
return
|
||||
}
|
||||
const processedQuery = this.queries[this.processedQueryIndex]
|
||||
processedQuery.name = this.newName
|
||||
this.$set(this.queries, this.processedQueryIndex, processedQuery)
|
||||
|
||||
// update queries in local storage
|
||||
storedQueries.updateStorage(this.queries)
|
||||
|
||||
// update tab, if renamed query is opened
|
||||
const tabIndex = this.findTabIndex(processedQuery.id)
|
||||
if (tabIndex >= 0) {
|
||||
this.$store.commit('updateTab', {
|
||||
index: tabIndex,
|
||||
name: this.newName,
|
||||
id: processedQuery.id
|
||||
})
|
||||
}
|
||||
// hide dialog
|
||||
this.$modal.hide('rename')
|
||||
},
|
||||
duplicateQuery (index) {
|
||||
const newQuery = storedQueries.duplicateQuery(this.showedQueries[index])
|
||||
if (this.selectAll) {
|
||||
this.selectedQueriesIds.add(newQuery.id)
|
||||
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||
}
|
||||
this.queries.push(newQuery)
|
||||
storedQueries.updateStorage(this.queries)
|
||||
},
|
||||
showDeleteDialog (idsSet) {
|
||||
this.deleteGroup = idsSet.size > 1
|
||||
if (!this.deleteGroup) {
|
||||
this.processedQueryId = idsSet.values().next().value
|
||||
}
|
||||
this.$modal.show('delete')
|
||||
},
|
||||
deleteQuery () {
|
||||
this.$modal.hide('delete')
|
||||
if (!this.deleteGroup) {
|
||||
this.queries.splice(this.processedQueryIndex, 1)
|
||||
|
||||
// Close deleted query tab if it was opened
|
||||
const tabIndex = this.findTabIndex(this.processedQueryId)
|
||||
if (tabIndex >= 0) {
|
||||
this.$store.commit('deleteTab', tabIndex)
|
||||
}
|
||||
|
||||
// Clear checkboxes
|
||||
if (this.selectedQueriesIds.has(this.processedQueryId)) {
|
||||
this.selectedQueriesIds.delete(this.processedQueryId)
|
||||
}
|
||||
} else {
|
||||
this.queries = this.selectAll
|
||||
? []
|
||||
: this.queries.filter(query => !this.selectedQueriesIds.has(query.id))
|
||||
|
||||
// Close deleted queries if it was opened
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear checkboxes
|
||||
this.selectedQueriesIds.clear()
|
||||
}
|
||||
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||
storedQueries.updateStorage(this.queries)
|
||||
},
|
||||
findTabIndex (id) {
|
||||
return this.$store.state.tabs.findIndex(tab => tab.id === id)
|
||||
},
|
||||
exportToFile (queryList, fileName) {
|
||||
const jsonStr = storedQueries.serialiseQueries(queryList)
|
||||
fu.exportToFile(jsonStr, fileName)
|
||||
},
|
||||
exportSelectedQueries () {
|
||||
const queryList = this.selectAll
|
||||
? this.allQueries
|
||||
: this.allQueries.filter(query => this.selectedQueriesIds.has(query.id))
|
||||
|
||||
this.exportToFile(queryList, 'My sqliteviz queries.json')
|
||||
},
|
||||
importQueries () {
|
||||
storedQueries.importQueries()
|
||||
.then(importedQueries => {
|
||||
if (this.selectAll) {
|
||||
importedQueries.forEach(query => {
|
||||
this.selectedQueriesIds.add(query.id)
|
||||
})
|
||||
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||
}
|
||||
|
||||
this.queries = this.queries.concat(importedQueries)
|
||||
storedQueries.updateStorage(this.queries)
|
||||
})
|
||||
},
|
||||
toggleSelectAll (checked) {
|
||||
this.selectAll = checked
|
||||
this.$refs.rowCheckBox.forEach(item => { item.checked = checked })
|
||||
|
||||
this.selectedQueriesIds = checked
|
||||
? new Set(this.allQueries.map(query => query.id))
|
||||
: new Set()
|
||||
|
||||
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||
this.selectedNotPredefinedCount = checked ? this.queries.length : 0
|
||||
},
|
||||
toggleRow (checked, id) {
|
||||
const isPredefined = this.predefinedQueriesIds.has(id)
|
||||
if (checked) {
|
||||
this.selectedQueriesIds.add(id)
|
||||
if (!isPredefined) {
|
||||
this.selectedNotPredefinedCount += 1
|
||||
}
|
||||
} else {
|
||||
if (this.selectedQueriesIds.size === this.allQueries.length) {
|
||||
this.$refs.mainCheckBox.checked = false
|
||||
this.selectAll = false
|
||||
}
|
||||
this.selectedQueriesIds.delete(id)
|
||||
if (!isPredefined) {
|
||||
this.selectedNotPredefinedCount -= 1
|
||||
}
|
||||
}
|
||||
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#start-guide {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: var(--color-text-base);
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#my-queries-content {
|
||||
padding: 52px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#my-queries-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 18px;
|
||||
margin: 0 auto 8px;
|
||||
max-width: 1500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rounded-bg {
|
||||
margin: 0 auto;
|
||||
max-width: 1500px;
|
||||
width: 100%;
|
||||
}
|
||||
.fixed-header {
|
||||
padding: 11px 24px;
|
||||
}
|
||||
.fixed-header:first-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 12px;
|
||||
}
|
||||
.fixed-header:first-child .name-th {
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
table {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
tbody tr td {
|
||||
min-width: 0;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
tbody tr td:first-child {
|
||||
width: 70%;
|
||||
max-width: 0;
|
||||
padding: 0 12px;
|
||||
}
|
||||
tbody tr td:last-child {
|
||||
width: 30%;
|
||||
max-width: 0;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
tbody .cell-data {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
tbody .cell-data div.name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
tbody tr:hover td {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
tbody tr:hover td {
|
||||
color: var(--color-text-active);
|
||||
}
|
||||
|
||||
.second-column {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.icons-container {
|
||||
display: none;
|
||||
margin-right: -12px;
|
||||
}
|
||||
.date-container {
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
tbody tr:hover .icons-container {
|
||||
display: flex;
|
||||
}
|
||||
.dialog input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button.toolbar {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: none;
|
||||
background-color: var(--color-gray-light-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-small);
|
||||
padding: 2px 6px;
|
||||
font-size: 11px;
|
||||
line-height: normal;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
tbody tr:hover .badge {
|
||||
display: block;
|
||||
}
|
||||
#note {
|
||||
margin-top: 24px;
|
||||
}
|
||||
#note img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.icon-tooltip {
|
||||
display: block;
|
||||
width: 149px;
|
||||
white-space: normal;
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
padding: 6px;
|
||||
}
|
||||
</style>
|
||||
42
src/views/Main/MyQueries/svg/copy.vue
Normal file
42
src/views/Main/MyQueries/svg/copy.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<span>
|
||||
<svg
|
||||
class="icon"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@click.stop="$emit('click')"
|
||||
@mouseover="showTooltip"
|
||||
@mouseout="hideTooltip"
|
||||
>
|
||||
<path
|
||||
d="M14.25 15.75H6V5.25H14.25V15.75ZM14.25 3.75H6C5.60218 3.75 5.22064 3.90804 4.93934 4.18934C4.65804 4.47064 4.5 4.85218 4.5 5.25V15.75C4.5 16.1478 4.65804 16.5294 4.93934 16.8107C5.22064 17.092 5.60218 17.25 6 17.25H14.25C14.6478 17.25 15.0294 17.092 15.3107 16.8107C15.592 16.5294 15.75 16.1478 15.75 15.75V5.25C15.75 4.85218 15.592 4.47064 15.3107 4.18934C15.0294 3.90804 14.6478 3.75 14.25 3.75ZM12 0.75H3C2.60218 0.75 2.22064 0.908035 1.93934 1.18934C1.65804 1.47064 1.5 1.85218 1.5 2.25V12.75H3V2.25H12V0.75Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Duplicate query
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'CopyIcon',
|
||||
mixins: [tooltipMixin]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
display: block;
|
||||
margin: 0 12px;
|
||||
}
|
||||
.icon:hover path {
|
||||
fill: var(--color-accent);
|
||||
}
|
||||
</style>
|
||||
43
src/views/Main/MyQueries/svg/delete.vue
Normal file
43
src/views/Main/MyQueries/svg/delete.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<span>
|
||||
<svg
|
||||
class="icon"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@click.stop="$emit('click')"
|
||||
@mouseover="showTooltip"
|
||||
@mouseout="hideTooltip"
|
||||
>
|
||||
<path
|
||||
d="M6.75 2.25V3H3V4.5H3.75V14.25C3.75 14.6478 3.90804 15.0294 4.18934 15.3107C4.47064 15.592 4.85218 15.75 5.25 15.75H12.75C13.1478 15.75 13.5294 15.592 13.8107 15.3107C14.092 15.0294 14.25 14.6478 14.25 14.25V4.5H15V3H11.25V2.25H6.75ZM5.25 4.5H12.75V14.25H5.25V4.5ZM6.75 6V12.75H8.25V6H6.75ZM9.75 6V12.75H11.25V6H9.75Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Delete query
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'DeleteIcon',
|
||||
mixins: [tooltipMixin]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
display: block;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.icon:hover path {
|
||||
fill: var(--color-accent);
|
||||
}
|
||||
</style>
|
||||
43
src/views/Main/MyQueries/svg/rename.vue
Normal file
43
src/views/Main/MyQueries/svg/rename.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<span>
|
||||
<svg
|
||||
class="icon"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@click.stop="$emit('click')"
|
||||
@mouseover="showTooltip"
|
||||
@mouseout="hideTooltip"
|
||||
>
|
||||
<path
|
||||
d="M10.545 6.75L11.25 7.455L4.44 14.25H3.75V13.56L10.545 6.75ZM13.245 2.25C13.0575 2.25 12.8625 2.325 12.72 2.4675L11.3475 3.84L14.16 6.6525L15.5325 5.28C15.825 4.9875 15.825 4.5 15.5325 4.2225L13.7775 2.4675C13.6275 2.3175 13.44 2.25 13.245 2.25ZM10.545 4.6425L2.25 12.9375V15.75H5.0625L13.3575 7.455L10.545 4.6425Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Rename query
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'RenameIcon',
|
||||
mixins: [tooltipMixin]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
display: block;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.icon:hover path {
|
||||
fill: var(--color-accent);
|
||||
}
|
||||
</style>
|
||||
25
src/views/Main/index.vue
Normal file
25
src/views/Main/index.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div>
|
||||
<main-menu />
|
||||
<keep-alive include="Editor">
|
||||
<router-view id="main-view" />
|
||||
</keep-alive>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MainMenu from './MainMenu'
|
||||
import '@/assets/styles/scrollbars.css'
|
||||
|
||||
export default {
|
||||
name: 'Main',
|
||||
components: { MainMenu }
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
#main-view {
|
||||
margin-top: 68px;
|
||||
height: calc(100vh - 68px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user