1
0
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:
lana-k
2021-05-04 14:13:58 +02:00
parent a07f2d3d99
commit cc483f4720
72 changed files with 297 additions and 311 deletions

View File

@@ -1,98 +0,0 @@
<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 chart from '@/chart'
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 chart.getDataSourcesFromSqlResult(this.sqlResult)
},
dataSourceOptions () {
return chart.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 chart.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>

View File

@@ -0,0 +1,130 @@
export default {
0: { name: 'null character' },
1: { name: 'start of header' },
2: { name: 'start of text' },
3: { name: 'end of text' },
4: { name: 'end of transmission' },
5: { name: 'enquiry' },
6: { name: 'acknowledge' },
7: { name: 'bell (ring)' },
8: { name: 'backspace' },
9: { name: 'horizontal tab' },
10: { name: 'line feed' },
11: { name: 'vertical tab' },
12: { name: 'form feed' },
13: { name: 'carriage return' },
14: { name: 'shift out' },
15: { name: 'shift in' },
16: { name: 'data link escape' },
17: { name: 'device control 1' },
18: { name: 'device control 2' },
19: { name: 'device control 3' },
20: { name: 'device control 4' },
21: { name: 'negative acknowledge' },
22: { name: 'synchronize' },
23: { name: 'end transmission block' },
24: { name: 'cancel' },
25: { name: 'end of medium' },
26: { name: 'substitute' },
27: { name: 'escape' },
28: { name: 'file separator' },
29: { name: 'group separator' },
30: { name: 'record separator' },
31: { name: 'unit separator' },
32: { name: 'space' },
33: { name: 'exclamation mark' },
34: { name: 'quotation mark' },
35: { name: 'number sign' },
36: { name: 'dollar sign' },
37: { name: 'percent sign' },
38: { name: 'ampersand' },
39: { name: 'apostrophe' },
40: { name: 'left parenthesis' },
41: { name: 'right parenthesis' },
42: { name: 'asterisk' },
43: { name: 'plus sign' },
44: { name: 'comma' },
45: { name: 'hyphen' },
46: { name: 'period' },
47: { name: 'slash' },
48: { name: 'digit 0' },
49: { name: 'digit 1' },
50: { name: 'digit 2' },
51: { name: 'digit 3' },
52: { name: 'digit 4' },
53: { name: 'digit 5' },
54: { name: 'digit 6' },
55: { name: 'digit 7' },
56: { name: 'digit 8' },
57: { name: 'digit 9' },
58: { name: 'colon' },
59: { name: 'semicolon' },
60: { name: 'less-than' },
61: { name: 'equals-to' },
62: { name: 'greater-than' },
63: { name: 'question mark' },
64: { name: 'at sign' },
65: { name: 'uppercase A' },
66: { name: 'uppercase B' },
67: { name: 'uppercase C' },
68: { name: 'uppercase D' },
69: { name: 'uppercase E' },
70: { name: 'uppercase F' },
71: { name: 'uppercase G' },
72: { name: 'uppercase H' },
73: { name: 'uppercase I' },
74: { name: 'uppercase J' },
75: { name: 'uppercase K' },
76: { name: 'uppercase L' },
77: { name: 'uppercase M' },
78: { name: 'uppercase N' },
79: { name: 'uppercase O' },
80: { name: 'uppercase P' },
81: { name: 'uppercase Q' },
82: { name: 'uppercase R' },
83: { name: 'uppercase S' },
84: { name: 'uppercase T' },
85: { name: 'uppercase U' },
86: { name: 'uppercase V' },
87: { name: 'uppercase W' },
88: { name: 'uppercase X' },
89: { name: 'uppercase Y' },
90: { name: 'uppercase Z' },
91: { name: 'left square bracket' },
92: { name: 'backslash' },
93: { name: 'right square bracket' },
94: { name: 'caret' },
95: { name: 'underscore' },
96: { name: 'grave accent' },
97: { name: 'lowercase a' },
98: { name: 'lowercase b' },
99: { name: 'lowercase c' },
100: { name: 'lowercase d' },
101: { name: 'lowercase e' },
102: { name: 'lowercase f' },
103: { name: 'lowercase g' },
104: { name: 'lowercase h' },
105: { name: 'lowercase i' },
106: { name: 'lowercase j' },
107: { name: 'lowercase k' },
108: { name: 'lowercase l' },
109: { name: 'lowercase m' },
110: { name: 'lowercase n' },
111: { name: 'lowercase o' },
112: { name: 'lowercase p' },
113: { name: 'lowercase q' },
114: { name: 'lowercase r' },
115: { name: 'lowercase s' },
116: { name: 'lowercase t' },
117: { name: 'lowercase u' },
118: { name: 'lowercase v' },
119: { name: 'lowercase w' },
120: { name: 'lowercase x' },
121: { name: 'lowercase y' },
122: { name: 'lowercase z' },
123: { name: 'left curly brace' },
124: { name: 'vertical bar' },
125: { name: 'right curly brace' },
126: { name: 'tilde' },
127: { name: 'delete (rubout)' }
}

View File

@@ -40,7 +40,7 @@
</template>
<script>
import ascii from '@/ascii'
import ascii from './ascii'
import DropDownChevron from '@/components/svg/dropDownChevron'
import ClearIcon from '@/components/svg/clear'

View File

@@ -0,0 +1,78 @@
import Papa from 'papaparse'
const hintsByCode = {
MissingQuotes: 'Edit your CSV so that the field has a closing quote char.',
TooFewFields: 'Add fields or try another delimiter.',
TooManyFields: 'Edit your CSV or try another delimiter.'
}
export default {
getResult (source) {
const result = {}
if (source.meta.fields) {
result.columns = source.meta.fields.map(col => col.trim())
result.values = source.data.map(row => {
const resultRow = []
result.columns.forEach(col => { resultRow.push(row[col]) })
return resultRow
})
} else {
result.values = source.data
result.columns = []
for (let i = 1; i <= source.data[0].length; i++) {
result.columns.push(`col${i}`)
}
}
return result
},
parse (file, config = {}) {
return new Promise((resolve, reject) => {
const defaultConfig = {
delimiter: '', // auto-detect
newline: '', // auto-detect
quoteChar: '"',
escapeChar: '"',
header: false,
transformHeader: undefined,
dynamicTyping: true,
preview: 0,
encoding: 'UTF-8',
worker: true,
comments: false,
step: undefined,
complete: results => {
const res = {
data: this.getResult(results),
delimiter: results.meta.delimiter,
hasErrors: false
}
res.messages = results.errors.map(msg => {
msg.type = msg.code === 'UndetectableDelimiter' ? 'info' : 'error'
if (msg.type === 'error') res.hasErrors = true
msg.hint = hintsByCode[msg.code]
return msg
})
resolve(res)
},
error: (error, file) => {
reject(error)
},
download: false,
downloadRequestHeaders: undefined,
downloadRequestBody: undefined,
skipEmptyLines: 'greedy',
chunk: undefined,
chunkSize: undefined,
fastMode: undefined,
beforeFirstChunk: undefined,
withCredentials: undefined,
transform: undefined,
delimitersToGuess: [',', '\t', '|', ';', Papa.RECORD_SEP, Papa.UNIT_SEP]
}
Papa.parse(file, { ...defaultConfig, ...config })
})
}
}

View File

@@ -127,17 +127,17 @@
</template>
<script>
import fu from '@/file.utils'
import csv from '@/csv'
import fu from '@/lib/utils/fileIo'
import csv from './csv'
import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField'
import DelimiterSelector from '@/components/DelimiterSelector'
import DelimiterSelector from './DelimiterSelector'
import CheckBox from '@/components/CheckBox'
import SqlTable from '@/components/SqlTable'
import Logs from '@/components/Logs'
import ChangeDbIcon from '@/components/svg/changeDb'
import time from '@/time'
import database from '@/database'
import time from '@/lib/utils/time'
import database from '@/lib/database'
const csvMimeTypes = [
'text/csv',

View File

@@ -1,240 +0,0 @@
<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 '@/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>

View File

@@ -1,117 +0,0 @@
<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 '@/components/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>

View File

@@ -65,7 +65,7 @@
</template>
<script>
import splitter from '@/splitter'
import splitter from './splitter'
export default {
name: 'Splitpanes',

View File

@@ -0,0 +1,36 @@
export default {
// Get the cursor position relative to the splitpane container.
getCurrentMouseDrag (event, container) {
const rect = container.getBoundingClientRect()
const { clientX, clientY } = ('ontouchstart' in window && event.touches)
? event.touches[0]
: event
return {
x: clientX - rect.left,
y: clientY - rect.top
}
},
// Returns the drag percentage of the splitter relative to the 2 panes it's inbetween.
getCurrentDragPercentage (event, container, isHorisontal) {
let drag = this.getCurrentMouseDrag(event, container)
drag = drag[isHorisontal ? 'y' : 'x']
const containerSize = container[isHorisontal ? 'clientHeight' : 'clientWidth']
return drag * 100 / containerSize
},
// Returns the new position in percents.
calculateOffset (event, { container, isHorisontal, paneBeforeMax, paneAfterMax }) {
const dragPercentage = this.getCurrentDragPercentage(event, container, isHorisontal)
const paneBeforeMaxReached = paneBeforeMax < 100 && (dragPercentage >= paneBeforeMax)
const paneAfterMaxReached = paneAfterMax < 100 && (dragPercentage <= 100 - paneAfterMax)
// Prevent dragging beyond pane max.
if (paneBeforeMaxReached || paneAfterMaxReached) {
return paneBeforeMaxReached ? paneBeforeMax : Math.max(100 - paneAfterMax, 0)
} else {
return Math.min(Math.max(dragPercentage, 0), paneBeforeMax)
}
}
}

View File

@@ -1,64 +0,0 @@
<template>
<div class="codemirror-container">
<codemirror v-model="query" :options="cmOptions" @changes="onChange" />
</div>
</template>
<script>
import hint from '@/hint'
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: hint.show
}
}
</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>

View File

@@ -48,7 +48,7 @@
</template>
<script>
import Pager from '@/components/Pager'
import Pager from './Pager'
export default {
name: 'SqlTable',

View File

@@ -1,193 +0,0 @@
<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 SqlEditor from '@/components/SqlEditor'
import Splitpanes from '@/components/Splitpanes'
import ViewSwitcher from '@/components/ViewSwitcher'
import Chart from '@/components/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>

View File

@@ -1,51 +0,0 @@
<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>

View File

@@ -1,195 +0,0 @@
<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 '@/components/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>

View File

@@ -1,67 +0,0 @@
<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>

View File

@@ -22,7 +22,7 @@
</template>
<script>
import tooltipMixin from '@/mixins/tooltips'
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'changeDbIcon',

View File

@@ -1,42 +0,0 @@
<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 '@/mixins/tooltips'
export default {
name: 'CopyIcon',
mixins: [tooltipMixin]
}
</script>
<style scoped>
.icon {
display: block;
margin: 0 12px;
}
.icon:hover path {
fill: var(--color-accent);
}
</style>

View File

@@ -1,43 +0,0 @@
<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 '@/mixins/tooltips'
export default {
name: 'DeleteIcon',
mixins: [tooltipMixin]
}
</script>
<style scoped>
.icon {
display: block;
margin: 0 12px;
}
.icon:hover path {
fill: var(--color-accent);
}
</style>

View File

@@ -23,7 +23,7 @@
</template>
<script>
import tooltipMixin from '@/mixins/tooltips'
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'ExportIcon',

View File

@@ -20,7 +20,7 @@
</template>
<script>
import tooltipMixin from '@/mixins/tooltips'
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'HintIcon',

View File

@@ -1,43 +0,0 @@
<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 '@/mixins/tooltips'
export default {
name: 'RenameIcon',
mixins: [tooltipMixin]
}
</script>
<style scoped>
.icon {
display: block;
margin: 0 12px;
}
.icon:hover path {
fill: var(--color-accent);
}
</style>