1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2026-03-24 23:16:18 +08:00
This commit is contained in:
lana-k
2025-03-20 22:04:15 +01:00
parent 5e2b34a856
commit 0c1b91ab2f
146 changed files with 3317 additions and 2438 deletions

View File

@@ -1,17 +1,15 @@
<template>
<div>
<logs
id="logs"
:messages="messages"
/>
<button
v-if="hasErrors"
id="open-workspace-btn"
class="secondary"
@click="$router.push('/workspace?hide_schema=1')">
Open workspace
</button>
</div>
<div>
<logs id="logs" :messages="messages" />
<button
v-if="hasErrors"
id="open-workspace-btn"
class="secondary"
@click="$router.push('/workspace?hide_schema=1')"
>
Open workspace
</button>
</div>
</template>
<script>
@@ -25,7 +23,7 @@ export default {
components: {
Logs
},
data () {
data() {
return {
newDb: null,
messages: [],
@@ -34,11 +32,11 @@ export default {
}
},
computed: {
hasErrors () {
hasErrors() {
return this.dataMsg.type === 'error' || this.inquiryMsg.type === 'error'
}
},
async created () {
async created() {
const {
data_url: dataUrl,
data_format: dataFormat,
@@ -65,7 +63,7 @@ export default {
}
},
methods: {
async loadData (dataUrl, dataFormat) {
async loadData(dataUrl, dataFormat) {
this.newDb = database.getNewDatabase()
if (dataUrl) {
this.dataMsg = {
@@ -95,7 +93,7 @@ export default {
}
this.$store.commit('setDb', this.newDb)
},
async getSqliteDb (dataUrl) {
async getSqliteDb(dataUrl) {
try {
const filename = new URL(dataUrl).pathname.split('/').pop()
const res = await fu.readFile(dataUrl)
@@ -114,7 +112,7 @@ export default {
this.dataMsg.type = 'error'
}
},
async loadInquiries (inquiryUrl, inquiryIds = []) {
async loadInquiries(inquiryUrl, inquiryIds = []) {
if (!inquiryUrl) {
return []
}
@@ -148,7 +146,7 @@ export default {
// Loading indicator is not needed anymore
clearTimeout(loadingInquiriesIndicator)
},
async openInquiries (inquiries, maximize) {
async openInquiries(inquiries, maximize) {
let tabToOpen = null
const layout = maximize ? this.getLayout(maximize) : undefined
for (const inquiry of inquiries) {
@@ -167,7 +165,7 @@ export default {
this.$store.state.currentTab.execute()
},
getLayout (panelToMaximize) {
getLayout(panelToMaximize) {
if (panelToMaximize === 'dataView') {
return {
sqlEditor: 'hidden',
@@ -190,7 +188,6 @@ export default {
#logs {
margin: 8px auto;
max-width: 800px;
}
#open-workspace-btn {

View File

@@ -5,22 +5,18 @@
src="~@/assets/images/info.svg"
@click="$modal.show('app-info')"
/>
<modal
modal-id="app-info"
class="dialog"
content-class="app-info-modal"
>
<modal modal-id="app-info" class="dialog" content-class="app-info-modal">
<div class="dialog-header">
App info
<close-icon @click="$modal.hide('app-info')"/>
<close-icon @click="$modal.hide('app-info')" />
</div>
<div class="dialog-body">
<div v-for="(item, index) in info" :key="index" class="info-item">
{{item.name}}
<div class="divider"/>
{{ item.name }}
<div class="divider" />
<div class="options">
<div v-for="(opt, index) in item.info" :key="index">
{{opt}}
{{ opt }}
</div>
</div>
</div>
@@ -36,7 +32,7 @@ import { version } from '../../../package.json'
export default {
name: 'AppDiagnosticInfo',
components: { CloseIcon },
data () {
data() {
return {
info: [
{
@@ -47,7 +43,7 @@ export default {
}
},
async created () {
async created() {
const state = this.$store.state
let result = (await state.db.execute('select sqlite_version()')).values
this.info.push({
@@ -94,7 +90,7 @@ export default {
}
.info-item {
margin-bottom: 32px;
font-size: 14px;
font-size: 14px;
}
.info-item:last-child {
margin-bottom: 0;

View File

@@ -10,13 +10,21 @@
id="loading-predefined-status"
v-if="$store.state.loadingPredefinedInquiries"
>
<loading-indicator/>
<loading-indicator />
Loading predefined inquiries...
</div>
<div id="my-inquiries-content" ref="my-inquiries-content" v-show="allInquiries.length > 0">
<div
id="my-inquiries-content"
ref="my-inquiries-content"
v-show="allInquiries.length > 0"
>
<div id="my-inquiries-toolbar">
<div id="toolbar-buttons">
<button id="toolbar-btns-import" class="toolbar" @click="importInquiries">
<button
id="toolbar-btns-import"
class="toolbar"
@click="importInquiries"
>
Import
</button>
<button
@@ -37,7 +45,11 @@
</button>
</div>
<div id="toolbar-search">
<text-field placeholder="Search inquiry by name" width="300px" v-model="filter"/>
<text-field
placeholder="Search inquiry by name"
width="300px"
v-model="filter"
/>
</div>
</div>
@@ -46,27 +58,32 @@
</div>
<div v-show="showedInquiries.length > 0" 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 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>
<div class="table-container" :style="{ 'max-height': `${maxTableHeight}px` }">
<table ref="table" class="sqliteviz-table">
<tbody>
<tr
v-for="(inquiry, index) in showedInquiries"
:key="inquiry.id"
@click="openInquiry(index)"
>
<td ref="name-td">
<div class="cell-data">
<div
class="table-container"
:style="{ 'max-height': `${maxTableHeight}px` }"
>
<table ref="table" class="sqliteviz-table">
<tbody>
<tr
v-for="(inquiry, index) in showedInquiries"
:key="inquiry.id"
@click="openInquiry(index)"
>
<td ref="name-td">
<div class="cell-data">
<check-box
ref="rowCheckBox"
:init="selectAll || selectedInquiriesIds.has(inquiry.id)"
@@ -81,82 +98,89 @@
@mouseleave="hideTooltip"
>
Predefined
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
Predefined inquiries come from the server.
These inquiries cant be deleted or renamed.
<span
class="icon-tooltip"
:style="tooltipStyle"
ref="tooltip"
>
Predefined inquiries come from the server. These
inquiries cant be deleted or renamed.
</span>
</div>
</div>
</td>
<td>
<div class="second-column">
<div class="date-container">
{{ createdAtFormatted(inquiry.createdAt) }}
</div>
<div class="icons-container">
<rename-icon
v-if="!inquiry.isPredefined"
@click="showRenameDialog(inquiry.id)"
/>
<copy-icon @click="duplicateInquiry(index)"/>
<export-icon
@click="exportToFile([inquiry], `${inquiry.name}.json`)"
tooltip="Export inquiry to file"
tooltip-position="top-left"
/>
<delete-icon
v-if="!inquiry.isPredefined"
@click="showDeleteDialog((new Set()).add(inquiry.id))"
/>
</td>
<td>
<div class="second-column">
<div class="date-container">
{{ createdAtFormatted(inquiry.createdAt) }}
</div>
<div class="icons-container">
<rename-icon
v-if="!inquiry.isPredefined"
@click="showRenameDialog(inquiry.id)"
/>
<copy-icon @click="duplicateInquiry(index)" />
<export-icon
@click="exportToFile([inquiry], `${inquiry.name}.json`)"
tooltip="Export inquiry to file"
tooltip-position="top-left"
/>
<delete-icon
v-if="!inquiry.isPredefined"
@click="showDeleteDialog(new Set().add(inquiry.id))"
/>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!--Rename Inquiry dialog -->
<modal modal-id="rename" class="dialog" content-style="width: 560px;">
<div class="dialog-header">
Rename inquiry
<close-icon @click="$modal.hide('rename')" />
</div>
<div class="dialog-body">
<text-field
label="New inquiry 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="renameInquiry">Rename</button>
</div>
</modal>
<!--Delete Inquiry dialog -->
<modal modal-id="delete" class="dialog" content-style="width: 480px;">
<div class="dialog-header">
Delete {{ deleteGroup ? 'inquiries' : 'inquiry' }}
<close-icon @click="$modal.hide('delete')" />
</div>
<div class="dialog-body">
{{ deleteDialogMsg }}
<div
v-show="selectedInquiriesCount > selectedNotPredefinedCount"
id="note"
>
<img src="~@/assets/images/info.svg" />
Note: Predefined inquiries 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="deleteInquiry">Delete</button>
</div>
</modal>
</div>
<!--Rename Inquiry dialog -->
<modal modal-id="rename" class="dialog" content-style="width: 560px;">
<div class="dialog-header">
Rename inquiry
<close-icon @click="$modal.hide('rename')"/>
</div>
<div class="dialog-body">
<text-field
label="New inquiry 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="renameInquiry">Rename</button>
</div>
</modal>
<!--Delete Inquiry dialog -->
<modal modal-id="delete" class="dialog" content-style="width: 480px;">
<div class="dialog-header">
Delete {{ deleteGroup ? 'inquiries' : 'inquiry' }}
<close-icon @click="$modal.hide('delete')"/>
</div>
<div class="dialog-body">
{{ deleteDialogMsg }}
<div v-show="selectedInquiriesCount > selectedNotPredefinedCount" id="note">
<img src="~@/assets/images/info.svg">
Note: Predefined inquiries 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="deleteInquiry">Delete</button>
</div>
</modal>
</div>
</template>
<script>
@@ -185,7 +209,7 @@ export default {
LoadingIndicator
},
mixins: [tooltipMixin],
data () {
data() {
return {
filter: null,
newName: null,
@@ -201,47 +225,51 @@ export default {
}
},
computed: {
inquiries () {
inquiries() {
return this.$store.state.inquiries
},
predefinedInquiries () {
predefinedInquiries() {
return this.$store.state.predefinedInquiries.map(inquiry => {
inquiry.isPredefined = true
return inquiry
})
},
predefinedInquiriesIds () {
predefinedInquiriesIds() {
return new Set(this.predefinedInquiries.map(inquiry => inquiry.id))
},
showedInquiries () {
showedInquiries() {
let showedInquiries = this.allInquiries
if (this.filter) {
showedInquiries = showedInquiries.filter(
inquiry => inquiry.name.toUpperCase().indexOf(this.filter.toUpperCase()) >= 0
inquiry =>
inquiry.name.toUpperCase().indexOf(this.filter.toUpperCase()) >= 0
)
}
return showedInquiries
},
allInquiries () {
allInquiries() {
return this.predefinedInquiries.concat(this.inquiries)
},
processedInquiryIndex () {
return this.inquiries.findIndex(inquiry => inquiry.id === this.processedInquiryId)
processedInquiryIndex() {
return this.inquiries.findIndex(
inquiry => inquiry.id === this.processedInquiryId
)
},
deleteDialogMsg () {
if (!this.deleteGroup && (
this.processedInquiryIndex === null ||
deleteDialogMsg() {
if (
!this.deleteGroup &&
(this.processedInquiryIndex === null ||
this.processedInquiryIndex < 0 ||
this.processedInquiryIndex > this.inquiries.length
)) {
this.processedInquiryIndex > this.inquiries.length)
) {
return ''
}
const deleteItem = this.deleteGroup
? `${this.selectedNotPredefinedCount} ${this.selectedNotPredefinedCount > 1
? 'inquiries'
: 'inquiry'}`
? `${this.selectedNotPredefinedCount} ${
this.selectedNotPredefinedCount > 1 ? 'inquiries' : 'inquiry'
}`
: `"${this.inquiries[this.processedInquiryIndex].name}"`
return `Are you sure you want to delete ${deleteItem}?`
@@ -249,14 +277,16 @@ export default {
},
watch: {
showedInquiries: {
handler () {
this.selectedInquiriesIds = new Set(this.showedInquiries
.filter(inquiry => this.selectedInquiriesIds.has(inquiry.id))
.map(inquiry => inquiry.id)
handler() {
this.selectedInquiriesIds = new Set(
this.showedInquiries
.filter(inquiry => this.selectedInquiriesIds.has(inquiry.id))
.map(inquiry => inquiry.id)
)
this.selectedInquiriesCount = this.selectedInquiriesIds.size
this.selectedNotPredefinedCount = ([...this.selectedInquiriesIds]
.filter(id => !this.predefinedInquiriesIds.has(id))).length
this.selectedNotPredefinedCount = [...this.selectedInquiriesIds].filter(
id => !this.predefinedInquiriesIds.has(id)
).length
if (this.selectedInquiriesIds.size < this.showedInquiries.length) {
if (this.$refs.mainCheckBox) {
@@ -268,9 +298,11 @@ export default {
deep: true
}
},
async created () {
const loadingPredefinedInquiries = this.$store.state.loadingPredefinedInquiries
const predefinedInquiriesLoaded = this.$store.state.predefinedInquiriesLoaded
async created() {
const loadingPredefinedInquiries =
this.$store.state.loadingPredefinedInquiries
const predefinedInquiriesLoaded =
this.$store.state.predefinedInquiriesLoaded
if (!predefinedInquiriesLoaded && !loadingPredefinedInquiries) {
try {
this.$store.commit('setLoadingPredefinedInquiries', true)
@@ -283,7 +315,7 @@ export default {
this.$store.commit('setLoadingPredefinedInquiries', false)
}
},
mounted () {
mounted() {
this.resizeObserver = new ResizeObserver(this.calcMaxTableHeight)
this.resizeObserver.observe(this.$refs['my-inquiries-content'])
@@ -292,15 +324,15 @@ export default {
this.calcNameWidth()
this.calcMaxTableHeight()
},
beforeUnmount () {
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs['my-inquiries-content'])
this.tableResizeObserver.unobserve(this.$refs.table)
},
methods: {
emitCreateTabEvent () {
emitCreateTabEvent() {
eventBus.$emit('createNewInquiry')
},
createdAtFormatted (value) {
createdAtFormatted(value) {
if (!value) {
return ''
}
@@ -310,20 +342,24 @@ export default {
hour: '2-digit',
minute: '2-digit'
}
return new Date(value).toLocaleDateString('en-GB', dateOptions) + ' ' +
new Date(value).toLocaleTimeString('en-GB', timeOptions)
return (
new Date(value).toLocaleDateString('en-GB', dateOptions) +
' ' +
new Date(value).toLocaleTimeString('en-GB', timeOptions)
)
},
calcNameWidth () {
const nameWidth = this.$refs['name-td'] && this.$refs['name-td'][0]
? this.$refs['name-td'][0].getBoundingClientRect().width
: 0
calcNameWidth() {
const nameWidth =
this.$refs['name-td'] && this.$refs['name-td'][0]
? this.$refs['name-td'][0].getBoundingClientRect().width
: 0
this.$refs['name-th'].style = `width: ${nameWidth}px`
},
calcMaxTableHeight () {
calcMaxTableHeight() {
const freeSpace = this.$refs['my-inquiries-content'].offsetHeight - 200
this.maxTableHeight = freeSpace - (freeSpace % 40) + 1
},
openInquiry (index) {
openInquiry(index) {
const tab = this.showedInquiries[index]
setTimeout(() => {
this.$store.dispatch('addTab', tab).then(id => {
@@ -332,13 +368,13 @@ export default {
})
})
},
showRenameDialog (id) {
showRenameDialog(id) {
this.errorMsg = null
this.processedInquiryId = id
this.newName = this.inquiries[this.processedInquiryIndex].name
this.$modal.show('rename')
},
renameInquiry () {
renameInquiry() {
if (!this.newName) {
this.errorMsg = "Inquiry name can't be empty"
return
@@ -351,21 +387,26 @@ export default {
// hide dialog
this.$modal.hide('rename')
},
duplicateInquiry (index) {
const newInquiry = storedInquiries.duplicateInquiry(this.showedInquiries[index])
duplicateInquiry(index) {
const newInquiry = storedInquiries.duplicateInquiry(
this.showedInquiries[index]
)
this.$store.dispatch('addInquiry', newInquiry)
},
showDeleteDialog (idsSet) {
showDeleteDialog(idsSet) {
this.deleteGroup = idsSet.size > 1
if (!this.deleteGroup) {
this.processedInquiryId = idsSet.values().next().value
}
this.$modal.show('delete')
},
deleteInquiry () {
deleteInquiry() {
this.$modal.hide('delete')
if (!this.deleteGroup) {
this.$store.dispatch('deleteInquiries', new Set().add(this.processedInquiryId))
this.$store.dispatch(
'deleteInquiries',
new Set().add(this.processedInquiryId)
)
// Clear checkbox
if (this.selectedInquiriesIds.has(this.processedInquiryId)) {
@@ -379,27 +420,31 @@ export default {
}
this.selectedInquiriesCount = this.selectedInquiriesIds.size
},
exportToFile (inquiryList, fileName) {
exportToFile(inquiryList, fileName) {
storedInquiries.export(inquiryList, fileName)
},
exportSelectedInquiries () {
const inquiryList = this.allInquiries.filter(
inquiry => this.selectedInquiriesIds.has(inquiry.id)
exportSelectedInquiries() {
const inquiryList = this.allInquiries.filter(inquiry =>
this.selectedInquiriesIds.has(inquiry.id)
)
this.exportToFile(inquiryList, 'My sqliteviz inquiries.json')
},
importInquiries () {
storedInquiries.importInquiries()
.then(importedInquiries => {
this.$store.commit('setInquiries', this.inquiries.concat(importedInquiries))
})
importInquiries() {
storedInquiries.importInquiries().then(importedInquiries => {
this.$store.commit(
'setInquiries',
this.inquiries.concat(importedInquiries)
)
})
},
toggleSelectAll (checked) {
toggleSelectAll(checked) {
this.selectAll = checked
this.$refs.rowCheckBox.forEach(item => { item.checked = checked })
this.$refs.rowCheckBox.forEach(item => {
item.checked = checked
})
this.selectedInquiriesIds = checked
? new Set(this.showedInquiries.map(inquiry => inquiry.id))
@@ -407,12 +452,13 @@ export default {
this.selectedInquiriesCount = this.selectedInquiriesIds.size
this.selectedNotPredefinedCount = checked
? ([...this.selectedInquiriesIds].filter(id => !this.predefinedInquiriesIds.has(id)))
.length
? [...this.selectedInquiriesIds].filter(
id => !this.predefinedInquiriesIds.has(id)
).length
: 0
},
toggleRow (checked, id) {
toggleRow(checked, id) {
const isPredefined = this.predefinedInquiriesIds.has(id)
if (checked) {
this.selectedInquiriesIds.add(id)

View File

@@ -34,7 +34,7 @@ export default {
emits: ['click'],
mixins: [tooltipMixin],
methods: {
onClick () {
onClick() {
this.hideTooltip()
this.$emit('click')
}

View File

@@ -32,7 +32,7 @@ export default {
emits: ['click'],
mixins: [tooltipMixin],
methods: {
onClick () {
onClick() {
this.hideTooltip()
this.$emit('click')
}

View File

@@ -32,7 +32,7 @@ export default {
emits: ['click'],
mixins: [tooltipMixin],
methods: {
onClick () {
onClick() {
this.hideTooltip()
this.$emit('click')
}

View File

@@ -2,7 +2,7 @@
<nav>
<div id="nav-links">
<a href="https://sqliteviz.com">
<img src="~@/assets/images/logo_simple.svg">
<img src="~@/assets/images/logo_simple.svg" />
</a>
<router-link to="/workspace">Workspace</router-link>
<router-link to="/inquiries">Inquiries</router-link>
@@ -18,11 +18,7 @@
>
Save
</button>
<button
id="create-btn"
class="primary"
@click="createNewInquiry"
>
<button id="create-btn" class="primary" @click="createNewInquiry">
Create
</button>
<app-diagnostic-info />
@@ -32,13 +28,13 @@
<modal modal-id="save" class="dialog" content-style="width: 560px;">
<div class="dialog-header">
Save inquiry
<close-icon @click="cancelSave"/>
<close-icon @click="cancelSave" />
</div>
<div class="dialog-body">
<div v-show="isPredefined" id="save-note">
<img src="~@/assets/images/info.svg">
Note: Predefined inquiries can't be edited.
That's why your modifications will be saved as a new inquiry. Enter the name for it.
<img src="~@/assets/images/info.svg" />
Note: Predefined inquiries can't be edited. That's why your
modifications will be saved as a new inquiry. Enter the name for it.
</div>
<text-field
label="Inquiry name"
@@ -70,36 +66,39 @@ export default {
CloseIcon,
AppDiagnosticInfo
},
data () {
data() {
return {
name: '',
errorMsg: null
}
},
computed: {
currentInquiry () {
currentInquiry() {
return this.$store.state.currentTab
},
isSaved () {
isSaved() {
return this.currentInquiry && this.currentInquiry.isSaved
},
isPredefined () {
isPredefined() {
return this.currentInquiry && this.currentInquiry.isPredefined
},
runDisabled () {
return this.currentInquiry && (!this.$store.state.db || !this.currentInquiry.query)
runDisabled() {
return (
this.currentInquiry &&
(!this.$store.state.db || !this.currentInquiry.query)
)
}
},
created () {
created() {
eventBus.$on('createNewInquiry', this.createNewInquiry)
eventBus.$on('saveInquiry', this.checkInquiryBeforeSave)
document.addEventListener('keydown', this._keyListener)
},
beforeUnmount () {
beforeUnmount() {
document.removeEventListener('keydown', this._keyListener)
},
methods: {
createNewInquiry () {
createNewInquiry() {
this.$store.dispatch('addTab').then(id => {
this.$store.commit('setCurrentTabId', id)
if (this.$route.path !== '/workspace') {
@@ -109,11 +108,11 @@ export default {
events.send('inquiry.create', null, { auto: false })
},
cancelSave () {
cancelSave() {
this.$modal.hide('save')
eventBus.$off('inquirySaved')
},
checkInquiryBeforeSave () {
checkInquiryBeforeSave() {
this.errorMsg = null
this.name = ''
@@ -123,10 +122,10 @@ export default {
this.saveInquiry()
}
},
async saveInquiry () {
async saveInquiry() {
const isNeedName = storedInquiries.isTabNeedName(this.currentInquiry)
if (isNeedName && !this.name) {
this.errorMsg = 'Inquiry name can\'t be empty'
this.errorMsg = "Inquiry name can't be empty"
return
}
const dataSet = this.currentInquiry.result
@@ -168,7 +167,7 @@ export default {
eventBus.$emit('inquirySaved')
events.send('inquiry.save')
},
_keyListener (e) {
_keyListener(e) {
if (this.$route.path === '/workspace') {
// Run query Ctrl+R or Ctrl+Enter
if ((e.key === 'r' || e.key === 'Enter') && (e.ctrlKey || e.metaKey)) {

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div @click="colVisible = !colVisible" class="table-name">
<tree-chevron :expanded="colVisible"/>
<tree-chevron :expanded="colVisible" />
{{ name }}
</div>
<div v-show="colVisible" class="columns">
@@ -20,7 +20,7 @@ export default {
name: 'TableDescription',
components: { TreeChevron },
props: ['name', 'columns'],
data () {
data() {
return {
colVisible: false
}
@@ -29,7 +29,8 @@ export default {
</script>
<style scoped>
.table-name, .column {
.table-name,
.column {
margin-top: 11px;
}

View File

@@ -1,16 +1,16 @@
<template>
<div id="schema-container">
<div id="schema-filter">
<text-field placeholder="Search table" width="100%" v-model="filter"/>
<text-field placeholder="Search table" width="100%" v-model="filter" />
</div>
<div id="db">
<div @click="schemaVisible = !schemaVisible" class="db-name">
<tree-chevron v-show="schema.length > 0" :expanded="schemaVisible"/>
<tree-chevron v-show="schema.length > 0" :expanded="schemaVisible" />
{{ dbName }}
</div>
<db-uploader id="db-edit" type="small" />
<export-icon tooltip="Export database" @click="exportToFile"/>
<add-table-icon @click="addCsvJson"/>
<export-icon tooltip="Export database" @click="exportToFile" />
<add-table-icon @click="addCsvJson" />
</div>
<div v-show="schemaVisible" class="schema">
<table-description
@@ -53,7 +53,7 @@ export default {
AddTableIcon,
CsvJsonImport
},
data () {
data() {
return {
schemaVisible: true,
filter: null,
@@ -61,7 +61,7 @@ export default {
}
},
computed: {
schema () {
schema() {
if (!this.$store.state.db.schema) {
return []
}
@@ -69,18 +69,19 @@ export default {
return !this.filter
? this.$store.state.db.schema
: this.$store.state.db.schema.filter(
table => table.name.toUpperCase().indexOf(this.filter.toUpperCase()) !== -1
)
table =>
table.name.toUpperCase().indexOf(this.filter.toUpperCase()) !== -1
)
},
dbName () {
dbName() {
return this.$store.state.db.dbName
}
},
methods: {
exportToFile () {
exportToFile() {
this.$store.state.db.export(`${this.dbName}.sqlite`)
},
async addCsvJson () {
async addCsvJson() {
this.file = await fIo.getFileFromUser('.csv,.json,.ndjson')
await this.$nextTick()
const csvJsonImportModal = this.$refs.addCsvJson
@@ -119,7 +120,8 @@ export default {
background-image: linear-gradient(white 73%, rgba(255, 255, 255, 0));
z-index: 2;
}
.schema, .db-name {
.schema,
.db-name {
color: var(--color-text-base);
font-size: 13px;
white-space: nowrap;

View File

@@ -1,15 +1,23 @@
<template>
<div v-show="visible" class="chart-container" ref="chartContainer">
<div class="warning chart-warning" v-show="!dataSources && visible">
There is no data to build a chart. Run your SQL query and make sure the result is not empty.
There is no data to build a chart. Run your SQL query and make sure the
result is not empty.
</div>
<div class="chart" :style="{ height: !dataSources ? 'calc(100% - 40px)' : '100%' }">
<div
class="chart"
:style="{ height: !dataSources ? 'calc(100% - 40px)' : '100%' }"
>
<PlotlyEditor
v-show="visible"
v-show="visible"
:data="state.data"
:layout="state.layout"
:frames="state.frames"
:config="{ editable: true, displaylogo: false, modeBarButtonsToRemove: ['toImage'] }"
:config="{
editable: true,
displaylogo: false,
modeBarButtonsToRemove: ['toImage']
}"
:dataSources="dataSources"
:dataSourceOptions="dataSourceOptions"
:plotly="plotly"
@@ -21,7 +29,7 @@
@render="onRender"
/>
</div>
</div>
</div>
</template>
<script>
@@ -38,15 +46,17 @@ import events from '@/lib/utils/events'
export default {
name: 'Chart',
props: [
'dataSources', 'initOptions',
'importToPngEnabled', 'importToSvgEnabled',
'dataSources',
'initOptions',
'importToPngEnabled',
'importToSvgEnabled',
'forPivot'
],
emits: ['update:importToSvgEnabled', 'update', 'loadingImageCompleted'],
components: {
PlotlyEditor: applyPureReactInVue(ReactPlotlyEditor)
},
data () {
data() {
return {
plotly,
state: this.initOptions || {
@@ -60,20 +70,23 @@ export default {
}
},
computed: {
dataSourceOptions () {
dataSourceOptions() {
return chartHelper.getOptionsFromDataSources(this.dataSources)
}
},
created () {
created() {
// https://github.com/plotly/plotly.js/issues/4555
plotly.setPlotConfig({
notifyOnLogging: 1
})
this.$watch(
() => this.state && this.state.data && this.state.data
.map(trace => `${trace.type}${trace.mode ? '-' + trace.mode : ''}`)
.join(','),
(value) => {
() =>
this.state &&
this.state.data &&
this.state.data
.map(trace => `${trace.type}${trace.mode ? '-' + trace.mode : ''}`)
.join(','),
value => {
events.send('viz_plotly.render', null, {
type: value,
pivot: !!this.forPivot
@@ -83,21 +96,21 @@ export default {
)
this.$emit('update:importToSvgEnabled', true)
},
mounted () {
mounted() {
this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.chartContainer)
},
activated () {
activated() {
this.useResizeHandler = true
},
deactivated () {
deactivated() {
this.useResizeHandler = false
},
beforeUnmount () {
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.chartContainer)
},
watch: {
dataSources () {
dataSources() {
// we need to update state.data in order to update the graph
// https://github.com/plotly/react-chart-editor/issues/948
if (this.dataSources) {
@@ -106,41 +119,44 @@ export default {
}
},
methods: {
async handleResize () {
async handleResize() {
this.visible = false
await this.$nextTick()
this.visible = true
},
onRender (data, layout, frames) {
onRender(data, layout, frames) {
// TODO: check changes and enable Save button if needed
},
update (data, layout, frames) {
update(data, layout, frames) {
this.state = { data, layout, frames }
this.$emit('update')
},
getOptionsForSave () {
getOptionsForSave() {
return chartHelper.getOptionsForSave(this.state, this.dataSources)
},
async saveAsPng () {
async saveAsPng() {
const url = await this.prepareCopy()
this.$emit('loadingImageCompleted')
fIo.downloadFromUrl(url, 'chart')
},
async saveAsSvg () {
async saveAsSvg() {
const url = await this.prepareCopy('svg')
fIo.downloadFromUrl(url, 'chart')
},
saveAsHtml () {
saveAsHtml() {
fIo.exportToFile(
chartHelper.getHtml(this.state),
'chart.html',
'text/html'
)
},
async prepareCopy (type = 'png') {
return await chartHelper.getImageDataUrl(this.$refs.plotlyEditor.$el, type)
async prepareCopy(type = 'png') {
return await chartHelper.getImageDataUrl(
this.$refs.plotlyEditor.$el,
type
)
}
}
}

View File

@@ -1,11 +1,11 @@
<template>
<div :class="['pivot-sort-btn', direction] " @click="changeSorting">
{{ modelValue.includes('key') ? 'key' : 'value' }}
<sort-icon
class="sort-icon"
:horizontal="direction === 'col'"
:asc="modelValue.includes('a_to_z')"
/>
<div :class="['pivot-sort-btn', direction]" @click="changeSorting">
{{ modelValue.includes('key') ? 'key' : 'value' }}
<sort-icon
class="sort-icon"
:horizontal="direction === 'col'"
:asc="modelValue.includes('a_to_z')"
/>
</div>
</template>
@@ -20,7 +20,7 @@ export default {
SortIcon
},
methods: {
changeSorting () {
changeSorting() {
if (this.modelValue === 'key_a_to_z') {
this.$emit('update:modelValue', 'value_a_to_z')
} else if (this.modelValue === 'value_a_to_z') {

View File

@@ -1,6 +1,6 @@
<template>
<div class="pivot-ui">
<div :class="{collapsed}">
<div :class="{ collapsed }">
<div class="row">
<label>Columns</label>
<multiselect
@@ -139,7 +139,12 @@
import $ from 'jquery'
import Multiselect from 'vue-multiselect'
import PivotSortBtn from './PivotSortBtn'
import { renderers, aggregators, zeroValAggregators, twoValAggregators } from '../pivotHelper'
import {
renderers,
aggregators,
zeroValAggregators,
twoValAggregators
} from '../pivotHelper'
export default {
name: 'pivotUi',
@@ -149,23 +154,35 @@ export default {
Multiselect,
PivotSortBtn
},
data () {
const aggregatorName = (this.modelValue && this.modelValue.aggregatorName) || 'Count'
const rendererName = (this.modelValue && this.modelValue.rendererName) || 'Table'
data() {
const aggregatorName =
(this.modelValue && this.modelValue.aggregatorName) || 'Count'
const rendererName =
(this.modelValue && this.modelValue.rendererName) || 'Table'
return {
collapsed: false,
renderer: { name: rendererName, fun: $.pivotUtilities.renderers[rendererName] },
aggregator: { name: aggregatorName, fun: $.pivotUtilities.aggregators[aggregatorName] },
renderer: {
name: rendererName,
fun: $.pivotUtilities.renderers[rendererName]
},
aggregator: {
name: aggregatorName,
fun: $.pivotUtilities.aggregators[aggregatorName]
},
rows: (this.modelValue && this.modelValue.rows) || [],
cols: (this.modelValue && this.modelValue.cols) || [],
val1: (this.modelValue && this.modelValue.vals && this.modelValue.vals[0]) || '',
val2: (this.modelValue && this.modelValue.vals && this.modelValue.vals[1]) || '',
val1:
(this.modelValue && this.modelValue.vals && this.modelValue.vals[0]) ||
'',
val2:
(this.modelValue && this.modelValue.vals && this.modelValue.vals[1]) ||
'',
colOrder: (this.modelValue && this.modelValue.colOrder) || 'key_a_to_z',
rowOrder: (this.modelValue && this.modelValue.rowOrder) || 'key_a_to_z'
}
},
computed: {
valCount () {
valCount() {
if (zeroValAggregators.includes(this.aggregator.name)) {
return 0
}
@@ -176,47 +193,47 @@ export default {
return 1
},
renderers () {
renderers() {
return renderers
},
aggregators () {
aggregators() {
return aggregators
},
rowsToSelect () {
rowsToSelect() {
return this.keyNames.filter(key => !this.cols.includes(key))
},
colsToSelect () {
colsToSelect() {
return this.keyNames.filter(key => !this.rows.includes(key))
}
},
watch: {
renderer () {
renderer() {
this.returnValue()
},
aggregator () {
aggregator() {
this.returnValue()
},
rows () {
rows() {
this.returnValue()
},
cols () {
cols() {
this.returnValue()
},
val1 () {
val1() {
this.returnValue()
},
val2 () {
val2() {
this.returnValue()
},
colOrder () {
colOrder() {
this.returnValue()
},
rowOrder () {
rowOrder() {
this.returnValue()
}
},
methods: {
returnValue () {
returnValue() {
const vals = []
for (let i = 1; i <= this.valCount; i++) {
vals.push(this[`val${i}`])
@@ -281,7 +298,6 @@ export default {
white-space: nowrap;
margin: auto;
cursor: pointer;
}
.switcher:hover {

View File

@@ -1,27 +1,28 @@
<template>
<div class="pivot-container">
<div class="warning pivot-warning" v-show="!dataSources">
There is no data to build a pivot. Run your SQL query and make sure the result is not empty.
</div>
<pivot-ui
:key-names="columns"
v-model="pivotOptions"
@update="$emit('update')"
/>
<div ref="pivotOutput" class="pivot-output"/>
<div
ref="customChartOutput"
v-show="viewCustomChart"
class="custom-chart-output"
>
<chart
ref="customChart"
v-bind="customChartComponentProps"
<div class="pivot-container">
<div class="warning pivot-warning" v-show="!dataSources">
There is no data to build a pivot. Run your SQL query and make sure the
result is not empty.
</div>
<pivot-ui
:key-names="columns"
v-model="pivotOptions"
@update="$emit('update')"
@loadingImageCompleted="$emit('loadingImageCompleted')"
/>
<div ref="pivotOutput" class="pivot-output" />
<div
ref="customChartOutput"
v-show="viewCustomChart"
class="custom-chart-output"
>
<chart
ref="customChart"
v-bind="customChartComponentProps"
@update="$emit('update')"
@loadingImageCompleted="$emit('loadingImageCompleted')"
/>
</div>
</div>
</div>
</template>
<script>
@@ -37,7 +38,12 @@ import events from '@/lib/utils/events'
export default {
name: 'pivot',
props: ['dataSources', 'initOptions', 'importToPngEnabled', 'importToSvgEnabled'],
props: [
'dataSources',
'initOptions',
'importToPngEnabled',
'importToSvgEnabled'
],
emits: [
'loadingImageCompleted',
'update',
@@ -48,7 +54,7 @@ export default {
PivotUi,
Chart
},
data () {
data() {
return {
resizeObserver: null,
pivotOptions: !this.initOptions
@@ -83,25 +89,25 @@ export default {
}
},
computed: {
columns () {
columns() {
return Object.keys(this.dataSources || {})
},
viewStandartChart () {
viewStandartChart() {
return this.pivotOptions.rendererName in $.pivotUtilities.plotly_renderers
},
viewCustomChart () {
viewCustomChart() {
return this.pivotOptions.rendererName === 'Custom chart'
}
},
watch: {
dataSources () {
dataSources() {
this.show()
},
'pivotOptions.rendererName': {
immediate: true,
handler () {
handler() {
this.$emit(
'update:importToPngEnabled',
this.pivotOptions.rendererName !== 'TSV Export'
@@ -115,22 +121,22 @@ export default {
})
}
},
pivotOptions () {
pivotOptions() {
this.show()
}
},
mounted () {
mounted() {
this.show()
// We need to detect resizing because plotly doesn't resize when resize its container
// but it resize on window.resize (we will trigger it manualy in order to make plotly resize)
this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.customChartOutput)
},
beforeUnmount () {
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.customChartOutput)
},
methods: {
handleResize () {
handleResize() {
// hack: plotly changes size only on window.resize event,
// so, we trigger it when container resizes (e.g. when move splitter)
if (this.viewStandartChart) {
@@ -138,7 +144,7 @@ export default {
}
},
show () {
show() {
const options = { ...this.pivotOptions }
if (this.viewStandartChart) {
options.rendererOptions = {
@@ -163,7 +169,9 @@ export default {
$(this.$refs.pivotOutput).pivot(
function (callback) {
const rowCount = !this.dataSources ? 0 : this.dataSources[this.columns[0]].length
const rowCount = !this.dataSources
? 0
: this.dataSources[this.columns[0]].length
for (let i = 1; i <= rowCount; i++) {
const row = {}
this.columns.forEach(col => {
@@ -181,7 +189,7 @@ export default {
}
},
getOptionsForSave () {
getOptionsForSave() {
const options = { ...this.pivotOptions }
if (this.viewCustomChart) {
const chartComponent = this.$refs.customChart
@@ -192,20 +200,22 @@ export default {
return options
},
async saveAsPng () {
async saveAsPng() {
if (this.viewCustomChart) {
this.$refs.customChart.saveAsPng()
} else {
const source = this.viewStandartChart
? await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
: (await pivotHelper.getPivotCanvas(this.$refs.pivotOutput)).toDataURL('image/png')
: (
await pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
).toDataURL('image/png')
this.$emit('loadingImageCompleted')
fIo.downloadFromUrl(source, 'pivot')
}
},
async prepareCopy () {
async prepareCopy() {
if (this.viewCustomChart) {
return await this.$refs.customChart.prepareCopy()
}
@@ -215,16 +225,19 @@ export default {
return await pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
},
async saveAsSvg () {
async saveAsSvg() {
if (this.viewCustomChart) {
this.$refs.customChart.saveAsSvg()
} else if (this.viewStandartChart) {
const url = await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'svg')
const url = await chartHelper.getImageDataUrl(
this.$refs.pivotOutput,
'svg'
)
fIo.downloadFromUrl(url, 'pivot')
}
},
saveAsHtml () {
saveAsHtml() {
if (this.viewCustomChart) {
this.$refs.customChart.saveAsHtml()
return

View File

@@ -17,7 +17,7 @@ export const twoValAggregators = [
'80% Lower Bound'
]
export function _getDataSources (pivotData) {
export function _getDataSources(pivotData) {
const rowKeys = pivotData.getRowKeys()
const colKeys = pivotData.getColKeys()
@@ -49,7 +49,7 @@ export function _getDataSources (pivotData) {
return Object.assign(dataSources, dataSourcesByCols, dataSourcesByRows)
}
function customChartRenderer (data, options) {
function customChartRenderer(data, options) {
const propsRef = options.getCustomComponentsProps()
propsRef.dataSources = _getDataSources(data)
return null
@@ -69,19 +69,21 @@ export const renderers = Object.keys($.pivotUtilities.renderers).map(key => {
}
})
export const aggregators = Object.keys($.pivotUtilities.aggregators).map(key => {
return {
name: key,
fun: $.pivotUtilities.aggregators[key]
export const aggregators = Object.keys($.pivotUtilities.aggregators).map(
key => {
return {
name: key,
fun: $.pivotUtilities.aggregators[key]
}
}
})
)
export async function getPivotCanvas (pivotOutput) {
export async function getPivotCanvas(pivotOutput) {
const tableElement = pivotOutput.querySelector('.pvtTable')
return await html2canvas(tableElement, { logging: false })
}
export function getPivotHtml (pivotOutput) {
export function getPivotHtml(pivotOutput) {
return `
<style>
table.pvtTable {

View File

@@ -31,7 +31,7 @@
<pivot-icon />
</icon-button>
<div class="side-tool-bar-divider"/>
<div class="side-tool-bar-divider" />
<icon-button
:disabled="!importToPngEnabled || loadingImage"
@@ -67,20 +67,20 @@
tooltip-position="top-left"
@click="prepareCopy"
>
<clipboard-icon/>
<clipboard-icon />
</icon-button>
</side-tool-bar>
<loading-dialog
loadingMsg="Rendering the visualisation..."
successMsg="Image is ready"
actionBtnName="Copy"
name="prepareCopy"
title="Copy to clipboard"
:loading="preparingCopy"
@action="copyToClipboard"
@cancel="cancelCopy"
/>
<loading-dialog
loadingMsg="Rendering the visualisation..."
successMsg="Image is ready"
actionBtnName="Copy"
name="prepareCopy"
title="Copy to clipboard"
:loading="preparingCopy"
@action="copyToClipboard"
@cancel="cancelCopy"
/>
</div>
</template>
@@ -117,7 +117,7 @@ export default {
ClipboardIcon,
loadingDialog
},
data () {
data() {
return {
mode: this.initMode || 'chart',
importToPngEnabled: true,
@@ -129,18 +129,18 @@ export default {
}
},
computed: {
plotlyInPivot () {
plotlyInPivot() {
return this.mode === 'pivot' && this.$refs.viewComponent.viewCustomChart
}
},
watch: {
mode () {
mode() {
this.$emit('update')
this.importToPngEnabled = true
}
},
methods: {
async saveAsPng () {
async saveAsPng() {
this.loadingImage = true
/*
setTimeout does its thing by putting its callback on the callback queue.
@@ -160,10 +160,10 @@ export default {
this.$refs.viewComponent.saveAsPng()
this.exportSignal('png')
},
getOptionsForSave () {
getOptionsForSave() {
return this.$refs.viewComponent.getOptionsForSave()
},
async prepareCopy () {
async prepareCopy() {
if ('ClipboardItem' in window) {
this.preparingCopy = true
this.$modal.show('prepareCopy')
@@ -172,7 +172,7 @@ export default {
await time.sleep(0)
this.dataToCopy = await this.$refs.viewComponent.prepareCopy()
const t1 = performance.now()
if ((t1 - t0) < 950) {
if (t1 - t0 < 950) {
this.$modal.hide('prepareCopy')
this.copyToClipboard()
} else {
@@ -181,30 +181,30 @@ export default {
} else {
alert(
"Your browser doesn't support copying images into the clipboard. " +
'If you use Firefox you can enable it ' +
'by setting dom.events.asyncClipboard.clipboardItem to true.'
'If you use Firefox you can enable it ' +
'by setting dom.events.asyncClipboard.clipboardItem to true.'
)
}
},
async copyToClipboard () {
async copyToClipboard() {
cIo.copyImage(this.dataToCopy)
this.$modal.hide('prepareCopy')
this.exportSignal('clipboard')
},
cancelCopy () {
cancelCopy() {
this.dataToCopy = null
this.$modal.hide('prepareCopy')
},
saveAsSvg () {
saveAsSvg() {
this.$refs.viewComponent.saveAsSvg()
this.exportSignal('svg')
},
saveAsHtml () {
saveAsHtml() {
this.$refs.viewComponent.saveAsHtml()
this.exportSignal('html')
},
exportSignal (to) {
exportSignal(to) {
const eventLabels = { type: to }
if (this.mode === 'chart' || this.plotlyInPivot) {

View File

@@ -1,42 +1,42 @@
<template>
<div class="record-navigator">
<icon-button
:disabled="modelValue === 0"
tooltip="First row"
tooltip-position="top-left"
class="first"
@click="$emit('update:modelValue', 0)"
>
<edge-arrow-icon :disabled="false" />
</icon-button>
<icon-button
:disabled="modelValue === 0"
tooltip="Previous row"
tooltip-position="top-left"
class="prev"
@click="$emit('update:modelValue', modelValue - 1)"
>
<arrow-icon :disabled="false" />
</icon-button>
<icon-button
:disabled="modelValue === total - 1"
tooltip="Next row"
tooltip-position="top-left"
class="next"
@click="$emit('update:modelValue', modelValue + 1)"
>
<arrow-icon :disabled="false" />
</icon-button>
<icon-button
:disabled="modelValue === total - 1"
tooltip="Last row"
tooltip-position="top-left"
class="last"
@click="$emit('update:modelValue', total - 1)"
>
<edge-arrow-icon :disabled="false" />
</icon-button>
</div>
<div class="record-navigator">
<icon-button
:disabled="modelValue === 0"
tooltip="First row"
tooltip-position="top-left"
class="first"
@click="$emit('update:modelValue', 0)"
>
<edge-arrow-icon :disabled="false" />
</icon-button>
<icon-button
:disabled="modelValue === 0"
tooltip="Previous row"
tooltip-position="top-left"
class="prev"
@click="$emit('update:modelValue', modelValue - 1)"
>
<arrow-icon :disabled="false" />
</icon-button>
<icon-button
:disabled="modelValue === total - 1"
tooltip="Next row"
tooltip-position="top-left"
class="next"
@click="$emit('update:modelValue', modelValue + 1)"
>
<arrow-icon :disabled="false" />
</icon-button>
<icon-button
:disabled="modelValue === total - 1"
tooltip="Last row"
tooltip-position="top-left"
class="last"
@click="$emit('update:modelValue', total - 1)"
>
<edge-arrow-icon :disabled="false" />
</icon-button>
</div>
</template>
<script>
@@ -59,7 +59,7 @@ export default {
<style scoped>
.record-navigator {
display: flex;
display: flex;
}
.record-navigator .next,

View File

@@ -9,11 +9,9 @@
>
<thead>
<tr>
<th/>
<th />
<th>
<div class="cell-data">
Row #{{ currentRowIndex + 1 }}
</div>
<div class="cell-data">Row #{{ currentRowIndex + 1 }}</div>
</th>
</tr>
</thead>
@@ -39,11 +37,11 @@
</div>
<div class="table-footer">
<div class="table-footer-count">
{{ rowCount }} {{rowCount === 1 ? 'row' : 'rows'}} retrieved
{{ rowCount }} {{ rowCount === 1 ? 'row' : 'rows' }} retrieved
<span v-if="time">in {{ time }}</span>
</div>
<row-navigator v-model="currentRowIndex" :total="rowCount"/>
<row-navigator v-model="currentRowIndex" :total="rowCount" />
</div>
</div>
</template>
@@ -61,31 +59,32 @@ export default {
selectedColumnIndex: Number
},
emits: ['updateSelectedCell'],
data () {
data() {
return {
selectedCellElement: null,
currentRowIndex: this.rowIndex
}
},
computed: {
columns () {
columns() {
return this.dataSet.columns
},
rowCount () {
rowCount() {
return this.dataSet.values[this.columns[0]].length
}
},
mounted () {
mounted() {
const col = this.selectedColumnIndex
const row = this.currentRowIndex
const cell = this.$refs.table
.querySelector(`td[data-col="${col}"][data-row="${row}"]`)
const cell = this.$refs.table.querySelector(
`td[data-col="${col}"][data-row="${row}"]`
)
if (cell) {
this.selectCell(cell)
}
},
watch: {
async currentRowIndex () {
async currentRowIndex() {
await nextTick()
if (this.selectedCellElement) {
const previouslySelected = this.selectedCellElement
@@ -95,16 +94,16 @@ export default {
}
},
methods: {
isBlob (value) {
isBlob(value) {
return value && ArrayBuffer.isView(value)
},
isNull (value) {
isNull(value) {
return value === null
},
getCellValue (col) {
getCellValue(col) {
return this.dataSet.values[col][this.currentRowIndex]
},
getCellText (col) {
getCellText(col) {
const value = this.getCellValue(col)
if (this.isNull(value)) {
return 'NULL'
@@ -114,7 +113,7 @@ export default {
}
return value
},
onTableKeydown (e) {
onTableKeydown(e) {
const keyCodeMap = {
38: 'up',
40: 'down'
@@ -130,10 +129,10 @@ export default {
this.moveFocusInTable(this.selectedCellElement, keyCodeMap[e.keyCode])
},
onCellClick (e) {
onCellClick(e) {
this.selectCell(e.target.closest('td'), false)
},
selectCell (cell, scrollTo = true) {
selectCell(cell, scrollTo = true) {
if (!cell) {
if (this.selectedCellElement) {
this.selectedCellElement.ariaSelected = 'false'
@@ -152,19 +151,21 @@ export default {
if (this.selectedCellElement && scrollTo) {
this.selectedCellElement.scrollIntoView()
this.selectedCellElement.closest('.table-container').scrollTo({ left: 0 })
this.selectedCellElement
.closest('.table-container')
.scrollTo({ left: 0 })
}
this.$emit('updateSelectedCell', this.selectedCellElement)
},
moveFocusInTable (initialCell, direction) {
moveFocusInTable(initialCell, direction) {
const currentColIndex = +initialCell.dataset.col
const newColIndex = direction === 'up'
? currentColIndex - 1
: currentColIndex + 1
const newColIndex =
direction === 'up' ? currentColIndex - 1 : currentColIndex + 1
const newCell = this.$refs.table
.querySelector(`td[data-col="${newColIndex}"][data-row="${this.currentRowIndex}"]`)
const newCell = this.$refs.table.querySelector(
`td[data-col="${newColIndex}"][data-row="${this.currentRowIndex}"]`
)
if (newCell) {
this.selectCell(newCell)
}
@@ -180,7 +181,7 @@ table.sqliteviz-table:focus {
.sqliteviz-table tbody td:hover {
background-color: var(--color-bg-light-3);
}
.sqliteviz-table tbody td[aria-selected="true"] {
.sqliteviz-table tbody td[aria-selected='true'] {
box-shadow: inset 0 0 0 1px var(--color-accent);
}

View File

@@ -12,13 +12,7 @@
{{ format.text }}
</button>
<button
type="button"
class="copy"
@click="copyToClipboard"
>
Copy
</button>
<button type="button" class="copy" @click="copyToClipboard">Copy</button>
</div>
<div class="value-body">
<codemirror
@@ -30,7 +24,8 @@
<pre
v-if="currentFormat === 'text'"
:class="['text-value', { 'meta-value': isNull || isBlob }]"
>{{ cellText }}</pre>
>{{ cellText }}</pre
>
<logs
v-if="messages && messages.length > 0"
:messages="messages"
@@ -60,7 +55,7 @@ export default {
props: {
cellValue: [String, Number, Uint8Array]
},
data () {
data() {
return {
formats: [
{ text: 'Text', value: 'text' },
@@ -82,13 +77,13 @@ export default {
}
},
computed: {
isBlob () {
isBlob() {
return this.cellValue && ArrayBuffer.isView(this.cellValue)
},
isNull () {
isNull() {
return this.cellValue === null
},
cellText () {
cellText() {
const value = this.cellValue
if (this.isNull) {
return 'NULL'
@@ -100,14 +95,14 @@ export default {
}
},
watch: {
currentFormat () {
currentFormat() {
this.messages = []
this.formattedJson = ''
if (this.currentFormat === 'json') {
this.formatJson(this.cellValue)
}
},
cellValue () {
cellValue() {
this.messages = []
if (this.currentFormat === 'json') {
this.formatJson(this.cellValue)
@@ -115,25 +110,24 @@ export default {
}
},
methods: {
formatJson (jsonStr) {
formatJson(jsonStr) {
try {
this.formattedJson = JSON.stringify(
JSON.parse(jsonStr), null, 4
)
this.formattedJson = JSON.stringify(JSON.parse(jsonStr), null, 4)
} catch (e) {
console.error(e)
this.formattedJson = ''
this.messages = [{
type: 'error',
message: 'Can\'t parse JSON.'
}]
this.messages = [
{
type: 'error',
message: "Can't parse JSON."
}
]
}
},
copyToClipboard () {
cIo.copyText(this.currentFormat === 'json'
? this.formattedJson
: this.cellValue,
'The value is copied to clipboard.'
copyToClipboard() {
cIo.copyText(
this.currentFormat === 'json' ? this.formattedJson : this.cellValue,
'The value is copied to clipboard.'
)
}
}
@@ -188,7 +182,7 @@ export default {
background-color: var(--color-bg-light);
}
.value-viewer-toolbar button[aria-selected="true"] {
.value-viewer-toolbar button[aria-selected='true'] {
color: var(--color-accent);
}

View File

@@ -1,23 +1,33 @@
<template>
<div class="run-result-panel" ref="runResultPanel">
<component
:is="viewValuePanelVisible ? 'splitpanes':'div'"
<component
:is="viewValuePanelVisible ? 'splitpanes' : 'div'"
:before="{ size: 50, max: 100 }"
:after="{ size: 50, max: 100 }"
:default="{ before: 50, after: 50 }"
class="run-result-panel-content"
>
<template #left-pane>
<div :id="'run-result-left-pane-'+tab.id" class="result-set-container"/>
<template #left-pane>
<div
:id="'run-result-left-pane-' + tab.id"
class="result-set-container"
/>
</template>
<div :id="'run-result-result-set-'+tab.id" class="result-set-container"/>
<div
:id="'run-result-result-set-' + tab.id"
class="result-set-container"
/>
<template #right-pane v-if="viewValuePanelVisible">
<div class="value-viewer-container">
<value-viewer
v-show="selectedCell"
:cellValue="selectedCell
? result.values[result.columns[selectedCell.dataset.col]][selectedCell.dataset.row]
: ''"
:cellValue="
selectedCell
? result.values[result.columns[selectedCell.dataset.col]][
selectedCell.dataset.row
]
: ''
"
/>
<div v-show="!selectedCell" class="table-preview">
No cell selected to view
@@ -33,7 +43,7 @@
tooltip-position="top-left"
@click="exportToCsv"
>
<export-to-csv-icon/>
<export-to-csv-icon />
</icon-button>
<icon-button
@@ -43,7 +53,7 @@
tooltip-position="top-left"
@click="prepareCopy"
>
<clipboard-icon/>
<clipboard-icon />
</icon-button>
<icon-button
@@ -54,7 +64,7 @@
:active="viewRecord"
@click="toggleViewRecord"
>
<row-icon/>
<row-icon />
</icon-button>
<icon-button
@@ -65,7 +75,7 @@
:active="viewValuePanelVisible"
@click="toggleViewValuePanel"
>
<view-cell-value-icon/>
<view-cell-value-icon />
</icon-button>
</side-tool-bar>
@@ -80,50 +90,46 @@
@cancel="cancelCopy"
/>
<teleport
defer
:to="resultSetTeleportTarget"
:disabled="!enableTeleport"
>
<teleport defer :to="resultSetTeleportTarget" :disabled="!enableTeleport">
<div>
<div
v-show="result === null && !isGettingResults && !error"
class="table-preview result-before"
>
Run your query and get results here
</div>
<div v-if="isGettingResults" class="table-preview result-in-progress">
<loading-indicator :size="30"/>
Fetching results...
</div>
<div
v-show="result === undefined && !isGettingResults && !error"
class="table-preview result-empty"
>
No rows retrieved according to your query
</div>
<logs v-if="error" :messages="[error]"/>
<sql-table
v-if="result && !viewRecord"
:data-set="result"
:time="time"
:pageSize="pageSize"
:page="defaultPage"
:selected-cell-coordinates="defaultSelectedCell"
class="straight"
@updateSelectedCell="onUpdateSelectedCell"
/>
<div
v-show="result === null && !isGettingResults && !error"
class="table-preview result-before"
>
Run your query and get results here
</div>
<div v-if="isGettingResults" class="table-preview result-in-progress">
<loading-indicator :size="30" />
Fetching results...
</div>
<div
v-show="result === undefined && !isGettingResults && !error"
class="table-preview result-empty"
>
No rows retrieved according to your query
</div>
<logs v-if="error" :messages="[error]" />
<sql-table
v-if="result && !viewRecord"
:data-set="result"
:time="time"
:pageSize="pageSize"
:page="defaultPage"
:selected-cell-coordinates="defaultSelectedCell"
class="straight"
@updateSelectedCell="onUpdateSelectedCell"
/>
<record
ref="recordView"
v-if="result && viewRecord"
:data-set="result"
:time="time"
:selected-column-index="selectedCell ? +selectedCell.dataset.col : 0"
:rowIndex="selectedCell ? +selectedCell.dataset.row : 0"
@updateSelectedCell="onUpdateSelectedCell"
/>
</div>
<record
ref="recordView"
v-if="result && viewRecord"
:data-set="result"
:time="time"
:selected-column-index="selectedCell ? +selectedCell.dataset.col : 0"
:rowIndex="selectedCell ? +selectedCell.dataset.row : 0"
@updateSelectedCell="onUpdateSelectedCell"
/>
</div>
</teleport>
</div>
</template>
@@ -158,7 +164,7 @@ export default {
time: [String, Number]
},
emits: ['switchTo'],
data () {
data() {
return {
resizeObserver: null,
pageSize: 20,
@@ -188,44 +194,45 @@ export default {
Splitpanes
},
computed: {
resultSetTeleportTarget () {
resultSetTeleportTarget() {
if (!this.enableTeleport) {
return undefined
}
const base = `#${this.viewValuePanelVisible
? 'run-result-left-pane'
: 'run-result-result-set'
const base = `#${
this.viewValuePanelVisible
? 'run-result-left-pane'
: 'run-result-result-set'
}`
const tabIdPostfix = `-${this.tab.id}`
return base + tabIdPostfix
}
},
activated () {
activated() {
this.enableTeleport = true
},
deactivated () {
deactivated() {
this.enableTeleport = false
},
mounted () {
mounted() {
this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.runResultPanel)
this.calculatePageSize()
},
beforeUnmount () {
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.runResultPanel)
},
watch: {
result () {
result() {
this.defaultSelectedCell = null
this.selectedCell = null
}
},
methods: {
handleResize () {
handleResize() {
this.calculatePageSize()
},
calculatePageSize () {
calculatePageSize() {
const runResultPanel = this.$refs.runResultPanel
// 27 - table footer hight
// 5 - padding-bottom of rounded table container
@@ -234,9 +241,10 @@ export default {
this.pageSize = Math.max(Math.floor(freeSpace / 35), 20)
},
exportToCsv () {
exportToCsv() {
if (this.result && this.result.values) {
events.send('resultset.export',
events.send(
'resultset.export',
this.result.values[this.result.columns[0]].length,
{ to: 'csv' }
)
@@ -245,9 +253,10 @@ export default {
fIo.exportToFile(csv.serialize(this.result), 'result_set.csv', 'text/csv')
},
async prepareCopy () {
async prepareCopy() {
if (this.result && this.result.values) {
events.send('resultset.export',
events.send(
'resultset.export',
this.result.values[this.result.columns[0]].length,
{ to: 'clipboard' }
)
@@ -261,7 +270,7 @@ export default {
await time.sleep(0)
this.dataToCopy = csv.serialize(this.result)
const t1 = performance.now()
if ((t1 - t0) < 950) {
if (t1 - t0 < 950) {
this.$modal.hide('prepareCSVCopy')
this.copyToClipboard()
} else {
@@ -270,27 +279,27 @@ export default {
} else {
alert(
"Your browser doesn't support copying into the clipboard. " +
'If you use Firefox you can enable it ' +
'by setting dom.events.asyncClipboard.clipboardItem to true.'
'If you use Firefox you can enable it ' +
'by setting dom.events.asyncClipboard.clipboardItem to true.'
)
}
},
copyToClipboard () {
copyToClipboard() {
cIo.copyText(this.dataToCopy, 'CSV copied to clipboard successfully')
this.$modal.hide('prepareCSVCopy')
},
cancelCopy () {
cancelCopy() {
this.dataToCopy = null
this.$modal.hide('prepareCSVCopy')
},
toggleViewValuePanel () {
toggleViewValuePanel() {
this.viewValuePanelVisible = !this.viewValuePanelVisible
},
toggleViewRecord () {
toggleViewRecord() {
if (this.viewRecord) {
this.defaultSelectedCell = {
row: this.$refs.recordView.currentRowIndex,
@@ -304,7 +313,7 @@ export default {
this.viewRecord = !this.viewRecord
},
onUpdateSelectedCell (e) {
onUpdateSelectedCell(e) {
this.selectedCell = e
}
}

View File

@@ -17,7 +17,7 @@
tooltip-position="top-left"
@click="$emit('switchTo', 'table')"
>
<table-icon/>
<table-icon />
</icon-button>
<icon-button
@@ -30,9 +30,9 @@
<data-view-icon />
</icon-button>
<div class="side-tool-bar-divider" v-if="$slots.default"/>
<div class="side-tool-bar-divider" v-if="$slots.default" />
<slot/>
<slot />
</div>
</template>

View File

@@ -3,17 +3,19 @@ import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/hint/sql-hint.js'
import store from '@/store'
function _getHintText (hint) {
function _getHintText(hint) {
return typeof hint === 'string' ? hint : hint.text
}
export function getHints (cm, options) {
export function getHints(cm, options) {
const result = CM.hint.sql(cm, options)
// Don't show the hint if there is only one option
// and the replacingText is already equals to this option
const replacedText = cm.getRange(result.from, result.to).toUpperCase()
if (result.list.length === 1 &&
_getHintText(result.list[0]).toUpperCase() === replacedText) {
if (
result.list.length === 1 &&
_getHintText(result.list[0]).toUpperCase() === replacedText
) {
result.list = []
}
@@ -21,7 +23,7 @@ export function getHints (cm, options) {
}
const hintOptions = {
get tables () {
get tables() {
const tables = {}
if (store.state.db.schema) {
store.state.db.schema.forEach(table => {
@@ -30,7 +32,7 @@ const hintOptions = {
}
return tables
},
get defaultTable () {
get defaultTable() {
const schema = store.state.db.schema
return schema && schema.length === 1 ? schema[0].name : null
},
@@ -39,11 +41,11 @@ const hintOptions = {
alignWithWord: false
}
export function showHintOnDemand (editor) {
export function showHintOnDemand(editor) {
CM.showHint(editor, getHints, hintOptions)
}
export default function showHint (editor) {
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)

View File

@@ -18,7 +18,7 @@
tooltip-position="top-left"
@click="$emit('run')"
>
<run-icon :disabled="runDisabled"/>
<run-icon :disabled="runDisabled" />
</icon-button>
</side-tool-bar>
</div>
@@ -47,7 +47,7 @@ export default {
IconButton,
RunIcon
},
data () {
data() {
return {
query: this.modelValue,
cmOptions: {
@@ -63,18 +63,18 @@ export default {
}
},
computed: {
runDisabled () {
return (!this.$store.state.db || !this.query || this.isGettingResults)
runDisabled() {
return !this.$store.state.db || !this.query || this.isGettingResults
}
},
watch: {
query () {
query() {
this.$emit('update:modelValue', this.query)
}
},
methods: {
onChange: time.debounce((value, editor) => showHint(editor), 400),
focus () {
focus() {
this.$refs.cm.cminstance?.focus()
}
}

View File

@@ -11,15 +11,15 @@
<div :id="'above-' + tab.id" class="above" />
</template>
<template #right-pane>
<div :id="'bottom-'+ tab.id" ref="bottomPane" class="bottomPane" />
<div :id="'bottom-' + tab.id" ref="bottomPane" class="bottomPane" />
</template>
</splitpanes>
<div :id="'hidden-'+ tab.id" class="hidden-part" />
<div :id="'hidden-' + tab.id" class="hidden-part" />
<teleport
defer
:to="enableTeleport ? `#${tab.layout.sqlEditor}-${tab.id}`: undefined"
:to="enableTeleport ? `#${tab.layout.sqlEditor}-${tab.id}` : undefined"
:disabled="!enableTeleport"
>
<sql-editor
@@ -33,7 +33,7 @@
<teleport
defer
:to="enableTeleport ? `#${tab.layout.table}-${tab.id}`: undefined"
:to="enableTeleport ? `#${tab.layout.table}-${tab.id}` : undefined"
:disabled="!enableTeleport"
>
<run-result
@@ -84,51 +84,53 @@ export default {
RunResult,
Splitpanes
},
data () {
data() {
return {
topPaneSize: this.tab.maximize
? this.tab.layout[this.tab.maximize] === 'above' ? 100 : 0
? this.tab.layout[this.tab.maximize] === 'above'
? 100
: 0
: 50,
enableTeleport: this.$store.state.isWorkspaceVisible
}
},
computed: {
isActive () {
isActive() {
return this.tab.id === this.$store.state.currentTabId
}
},
watch: {
isActive: {
immediate: true,
async handler () {
async handler() {
if (this.isActive) {
await nextTick()
this.$refs.sqlEditor?.focus()
}
}
},
'tab.query' () {
'tab.query'() {
this.$store.commit('updateTab', {
tab: this.tab,
newValues: { isSaved: false }
})
}
},
async activated () {
async activated() {
this.enableTeleport = true
if (this.isActive) {
await nextTick()
this.$refs.sqlEditor.focus()
}
},
deactivated () {
deactivated() {
this.enableTeleport = false
},
async mounted () {
async mounted() {
this.tab.dataView = this.$refs.dataView
},
methods: {
onSwitchView (from, to) {
onSwitchView(from, to) {
const fromPosition = this.tab.layout[from]
this.tab.layout[from] = this.tab.layout[to]
this.tab.layout[to] = fromPosition
@@ -136,7 +138,7 @@ export default {
events.send('inquiry.panel', null, { panel: to })
},
onDataViewUpdate () {
onDataViewUpdate() {
this.$store.commit('updateTab', {
tab: this.tab,
newValues: { isSaved: false }

View File

@@ -1,11 +1,11 @@
<template>
<div id="tabs">
<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 === selectedTabId)}, 'tab']"
:class="[{ 'tab-selected': tab.id === selectedTabId }, 'tab']"
>
<div class="tab-name">
<span v-show="!tab.isSaved" class="star">*</span>
@@ -13,15 +13,15 @@
<span v-else class="tab-untitled">{{ tab.tempName }}</span>
</div>
<div>
<close-icon class="close-icon" :size="10" @click="beforeCloseTab(tab)"/>
<close-icon
class="close-icon"
:size="10"
@click="beforeCloseTab(tab)"
/>
</div>
</div>
</div>
<tab
v-for="tab in tabs"
:key="tab.id"
:tab="tab"
/>
<tab v-for="tab in tabs" :key="tab.id" :tab="tab" />
<div v-show="tabs.length === 0" id="start-guide">
<span class="link" @click="emitCreateTabEvent">Create</span>
new inquiry from scratch or open one from
@@ -31,26 +31,33 @@
<!--Close tab warning dialog -->
<modal modal-id="close-warn" class="dialog" content-style="width: 560px;">
<div class="dialog-header">
Close tab {{
Close tab
{{
closingTab !== null
? (closingTab.name || `[${closingTab.tempName}]`)
: ''
? closingTab.name || `[${closingTab.tempName}]`
: ''
}}
<close-icon @click="$modal.hide('close-warn')"/>
<close-icon @click="$modal.hide('close-warn')" />
</div>
<div class="dialog-body">
You have unsaved changes. Save changes in {{
You have unsaved changes. Save changes in
{{
closingTab !== null
? (closingTab.name || `[${closingTab.tempName}]`)
: ''
}} before closing?
? closingTab.name || `[${closingTab.tempName}]`
: ''
}}
before closing?
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="closeTab(closingTab)">
Close without saving
</button>
<button class="secondary" @click="$modal.hide('close-warn')">Don't close</button>
<button class="primary" @click="saveAndClose(closingTab)">Save and close</button>
<button class="secondary" @click="$modal.hide('close-warn')">
Don't close
</button>
<button class="primary" @click="saveAndClose(closingTab)">
Save and close
</button>
</div>
</modal>
</div>
@@ -67,36 +74,36 @@ export default {
Tab,
CloseIcon
},
data () {
data() {
return {
closingTab: null
}
},
computed: {
tabs () {
tabs() {
return this.$store.state.tabs
},
selectedTabId () {
selectedTabId() {
return this.$store.state.currentTabId
}
},
created () {
created() {
window.addEventListener('beforeunload', this.leavingSqliteviz)
},
methods: {
emitCreateTabEvent () {
emitCreateTabEvent() {
eventBus.$emit('createNewInquiry')
},
leavingSqliteviz (event) {
leavingSqliteviz(event) {
if (this.tabs.some(tab => !tab.isSaved)) {
event.preventDefault()
event.returnValue = ''
}
},
selectTab (id) {
selectTab(id) {
this.$store.commit('setCurrentTabId', id)
},
beforeCloseTab (tab) {
beforeCloseTab(tab) {
this.closingTab = tab
if (!tab.isSaved) {
this.$modal.show('close-warn')
@@ -104,11 +111,11 @@ export default {
this.closeTab(tab)
}
},
closeTab (tab) {
closeTab(tab) {
this.$modal.hide('close-warn')
this.$store.commit('deleteTab', tab)
},
saveAndClose (tab) {
saveAndClose(tab) {
eventBus.$on('inquirySaved', () => {
this.closeTab(tab)
eventBus.$off('inquirySaved')

View File

@@ -7,7 +7,7 @@
:default="{ before: 20, after: 80 }"
>
<template #left-pane>
<schema/>
<schema />
</template>
<template #right-pane>
<tabs />
@@ -29,14 +29,17 @@ export default {
Splitpanes,
Tabs
},
data () {
data() {
return {
schemaWidth: this.$route.query.hide_schema === '1' ? 0 : 20
}
},
async beforeCreate () {
async beforeCreate() {
const schema = this.$store.state.db.schema
if ((!schema || schema.length === 0) && this.$store.state.tabs.length === 0) {
if (
(!schema || schema.length === 0) &&
this.$store.state.tabs.length === 0
) {
const stmt = [
'/*',
' * Your database is empty. In order to start building charts',
@@ -60,10 +63,10 @@ export default {
events.send('inquiry.create', null, { auto: true })
}
},
activated () {
activated() {
this.$store.commit('setIsWorkspaceVisible', true)
},
deactivated () {
deactivated() {
this.$store.commit('setIsWorkspaceVisible', false)
}
}

View File

@@ -1,11 +1,11 @@
<template>
<div>
<main-menu />
<router-view id="main-view" v-slot="{ Component }">
<keep-alive include="Workspace,Inquiries">
<component :is="Component"/>
</keep-alive>
</router-view>
<router-view id="main-view" v-slot="{ Component }">
<keep-alive include="Workspace,Inquiries">
<component :is="Component" />
</keep-alive>
</router-view>
</div>
</template>