mirror of
https://github.com/lana-k/sqliteviz.git
synced 2026-03-24 23:16:18 +08:00
format
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 can’t be deleted or renamed.
|
||||
<span
|
||||
class="icon-tooltip"
|
||||
:style="tooltipStyle"
|
||||
ref="tooltip"
|
||||
>
|
||||
Predefined inquiries come from the server. These
|
||||
inquiries can’t 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)
|
||||
|
||||
@@ -34,7 +34,7 @@ export default {
|
||||
emits: ['click'],
|
||||
mixins: [tooltipMixin],
|
||||
methods: {
|
||||
onClick () {
|
||||
onClick() {
|
||||
this.hideTooltip()
|
||||
this.$emit('click')
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export default {
|
||||
emits: ['click'],
|
||||
mixins: [tooltipMixin],
|
||||
methods: {
|
||||
onClick () {
|
||||
onClick() {
|
||||
this.hideTooltip()
|
||||
this.$emit('click')
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export default {
|
||||
emits: ['click'],
|
||||
mixins: [tooltipMixin],
|
||||
methods: {
|
||||
onClick () {
|
||||
onClick() {
|
||||
this.hideTooltip()
|
||||
this.$emit('click')
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user