1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-07 10:38:54 +08:00

13 Commits
0.3.0 ... 0.4.0

Author SHA1 Message Date
lana-k
4c685b6d7c move to dist 2020-11-10 21:37:32 +01:00
lana-k
a37edc501b create an archive 2020-11-10 21:33:35 +01:00
lana-k
46a9d1613b minor changes 2020-11-10 20:12:06 +01:00
lana-k
b08265fab9 minor changes 2020-11-10 20:05:40 +01:00
lana-k
f1b5f5e3c7 rename github action 2020-11-10 18:58:12 +01:00
lana-k
e08fdbecbc fix Create release action 2020-11-10 18:53:42 +01:00
lana-k
a5ba1cb821 add release.yml 2020-11-06 16:58:29 +01:00
lana-k
7da1cb36fa Add tooltips #9 2020-11-06 12:20:44 +01:00
lana-k
57c8fe5bc8 Add table filter in schema sidebar #7 2020-11-05 17:29:38 +01:00
lana-k
e7d3da8869 fade out in schema 2020-11-05 13:48:02 +01:00
lana-k
5dc80751c4 add padding to schema 2020-11-04 22:31:38 +01:00
lana-k
3e52dcac2c move run button to the menu 2020-11-04 22:18:04 +01:00
lana-k
1037185a6a add predefined queries 2020-11-04 19:13:27 +01:00
21 changed files with 624 additions and 180 deletions

View File

@@ -1,4 +1,4 @@
name: Deploy to GitHub Pages
name: Deploy to GitHub Pages and create release
on:
workflow_dispatch:
push:
@@ -7,10 +7,10 @@ on:
jobs:
deploy:
name: Deploy to GitHub Pages
name: Deploy to GitHub Pages and create release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
@@ -20,6 +20,17 @@ jobs:
npm install
npm run build
- name: Create archive
run: |
cd dist
zip -9 -r dist.zip . -x "js/*.map"
- name: Create release
uses: ncipollo/release-action@v1
with:
artifacts: "dist/dist.zip"
token: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@3.6.2
with:

47
public/queries.json Normal file
View File

@@ -0,0 +1,47 @@
{
"query": "select * from invoices",
"chart": {
"data": [
{
"type": "scatter",
"mode": "lines",
"x": null,
"xsrc": "InvoiceId",
"meta": {
"columnNames": {
"x": "InvoiceId",
"y": "Total"
}
},
"y": null,
"ysrc": "Total"
}
],
"layout": {
"xaxis": {
"range": [
1,
412
],
"autorange": true,
"type": "linear"
},
"yaxis": {
"range": [
-0.39166666666666683,
27.241666666666667
],
"autorange": true,
"type": "linear"
},
"autosize": true,
"mapbox": {
"style": "open-street-map"
}
},
"frames": []
},
"name": "Invoices",
"id": "ieZfcITwDUTADwOmQlYyL",
"createdAt": "2020-11-03T14:17:49.524Z"
}

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 9H13V7H11V9ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM12 2C10.6868 2 9.38642 2.25866 8.17317 2.7612C6.95991 3.26375 5.85752 4.00035 4.92893 4.92893C3.05357 6.8043 2 9.34784 2 12C2 14.6522 3.05357 17.1957 4.92893 19.0711C5.85752 19.9997 6.95991 20.7362 8.17317 21.2388C9.38642 21.7413 10.6868 22 12 22C14.6522 22 17.1957 20.9464 19.0711 19.0711C20.9464 17.1957 22 14.6522 22 12C22 10.6868 21.7413 9.38642 21.2388 8.17317C20.7362 6.95991 19.9997 5.85752 19.0711 4.92893C18.1425 4.00035 17.0401 3.26375 15.8268 2.7612C14.6136 2.25866 13.3132 2 12 2V2ZM11 17H13V11H11V17Z" fill="#A2B1C6"/>
</svg>

After

Width:  |  Height:  |  Size: 754 B

View File

@@ -0,0 +1,15 @@
.icon-tooltip {
visibility: hidden;
background-color: rgba(80, 103, 132, 0.85);
color: #fff;
text-align: center;
font-size: 11px;
padding: 0 6px;
line-height: 19px;;
position: fixed;
z-index: 5;
height: 19px;
border-radius: var(--border-radius-medium);
white-space: nowrap;
z-index: 999;
}

View File

@@ -1,10 +1,9 @@
<template>
<div class="chart-container">
<div v-show="visible" class="chart-container">
<div class="chart-worning" v-show="!sqlResult && visible">
There is no data to build a chart. Run your sql query and make sure the result is not empty.
</div>
<PlotlyEditor
v-show="visible"
:data="state.data"
:layout="state.layout"
:frames="state.frames"
@@ -106,6 +105,7 @@ export default {
}
.chart {
border-top: 1px solid var(--color-border);
min-height: 242px;
}
>>> .editor_controls .sidebar__item:before {

View File

@@ -35,11 +35,6 @@ export default {
checked: this.init
}
},
watch: {
checked () {
this.$emit('change', this.checked)
}
},
methods: {
onClick () {
this.checked = !this.checked

View File

@@ -8,21 +8,78 @@
<button
v-if="$store.state.tabs.length > 0"
class="primary"
:disabled="$store.state.currentTab && !$store.state.currentTab.isUnsaved"
@click="saveQuery"
:disabled="currentQuery && !$store.state.schema || !currentQuery.query"
@click="currentQuery.execute"
>
Run
</button>
<button
v-if="$store.state.tabs.length > 0"
class="primary"
:disabled="currentQuery && !currentQuery.isUnsaved"
@click="checkQueryBeforeSave"
>
Save
</button>
<button class="primary" @click="createNewQuery">Create</button>
</div>
<!--Save Query dialog -->
<modal name="save" classes="dialog" height="auto">
<div class="dialog-header">
Save query
<close-icon @click="$modal.hide('save')"/>
</div>
<div class="dialog-body">
<div v-show="isPredefined" id="save-note">
<img :src="require('@/assets/images/info.svg')">
Note: Predefined queries can't be edited.
That's why your modifications will be saved as a new query. Enter the name for it.
</div>
<text-field
label="Query name"
:error-msg="errorMsg"
v-model="name"
width="100%"
/>
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="$modal.hide('save')">Cancel</button>
<button class="primary" @click="saveQuery">Save</button>
</div>
</modal>
</nav>
</template>
<script>
import { nanoid } from 'nanoid'
import TextField from '@/components/TextField'
import CloseIcon from '@/components/svg/close'
export default {
name: 'MainMenu',
components: {
TextField,
CloseIcon
},
data () {
return {
name: '',
errorMsg: null
}
},
computed: {
currentQuery () {
return this.$store.state.currentTab
},
isPredefined () {
if (this.currentQuery) {
return this.currentQuery.isPredefined
} else {
return false
}
}
},
created () {
this.$root.$on('createNewQuery', this.createNewQuery)
},
@@ -38,38 +95,69 @@ export default {
}
this.$store.commit('addTab', tab)
this.$store.commit('setCurrentTabId', tab.id)
this.$store.commit('updateUntitledLastIndex')
},
checkQueryBeforeSave () {
this.errorMsg = null
const isFromScratch = !this.currentQuery.initName
if (isFromScratch || this.isPredefined) {
this.$modal.show('save')
} else {
this.saveQuery()
}
},
saveQuery () {
const currentQuery = this.$store.state.currentTab
const isFromScratch = !this.$store.state.currentTab.initName
const isFromScratch = !this.currentQuery.initName
if ((isFromScratch || this.isPredefined) && !this.name) {
this.errorMsg = 'Query name can\'t be empty'
return
}
const dataSet = this.currentQuery.result
const tabView = this.currentQuery.view
// Prepare query
const value = {
id: currentQuery.id,
query: currentQuery.query,
chart: currentQuery.getChartSatateForSave()
}
if (isFromScratch) {
value.name = prompt('query name')
// TODO: create dialog
this.$store.commit('updateTabName', { index: currentQuery.tabIndex, newName: value.name })
value.createdAt = new Date()
} else {
value.name = currentQuery.initName
id: this.isPredefined ? nanoid() : this.currentQuery.id,
query: this.currentQuery.query,
chart: this.currentQuery.getChartSatateForSave(),
name: (!this.isPredefined && this.currentQuery.initName) || this.name,
createdAt: new Date()
}
// Save query
let myQueries = JSON.parse(localStorage.getItem('myQueries'))
if (!myQueries) {
myQueries = [value]
} else if (isFromScratch) {
} else if (isFromScratch || this.isPredefined) {
myQueries.push(value)
} else {
const queryIndex = myQueries.findIndex(query => query.id === currentQuery.id)
const queryIndex = myQueries.findIndex(query => query.id === this.currentQuery.id)
value.createdAt = myQueries[queryIndex].createdAt
myQueries[queryIndex] = value
}
localStorage.setItem('myQueries', JSON.stringify(myQueries))
currentQuery.isUnsaved = false
// Update tab
this.$store.commit('updateTab', {
index: this.currentQuery.tabIndex,
name: value.name,
id: value.id,
query: value.query,
chart: value.chart,
isUnsaved: false
})
// Restore data:
// e.g. if we save predefined query the tab will be created again
// (because of new id) and
// it will be without sql result and has default view - table.
// That's why we need to restore data and view
this.$nextTick(() => {
this.currentQuery.result = dataSet
this.currentQuery.view = tabView
})
// Hide dialog
this.$modal.hide('save')
}
}
}
@@ -89,6 +177,7 @@ nav {
left: 0;
width: 100vw;
padding: 0 52px;
z-index: 999;
}
a {
font-size: 18px;
@@ -103,4 +192,13 @@ a.router-link-active {
button {
margin-left: 16px;
}
#save-note {
margin-bottom: 24px;
display: flex;
align-items: flex-start;
}
#save-note img {
margin: -3px 6px 0 0;
}
</style>

View File

@@ -1,10 +1,12 @@
<template>
<div>
<text-field placeholder="Search table" width="100%"/>
<div id="schema-container">
<div id="schema-filter">
<text-field placeholder="Search table" width="100%" v-model="filter"/>
</div>
<div id="db">
<div @click="schemaVisible = !schemaVisible" class="db-name">
<svg
:style="{transform: schemaVisible ? 'rotate(90deg)' : 'rotate(0)'}"
:style="{ transform: schemaVisible ? 'rotate(90deg)' : 'rotate(0)' }"
class="chevron-icon"
width="9"
height="9"
@@ -22,25 +24,14 @@
<div class="db-edit">
<input type="file" id="actual-btn" ref="dbfile" hidden @change="changeDb"/>
<label for="actual-btn">
<svg
class="db-edit-icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M3 10.5V12.75C3 14.25 5.2875 15.54 8.25 15.75V13.5825L8.3475 13.5C5.34 13.32 3 12.045 3 10.5ZM9 9.75C5.685 9.75 3 8.4075 3 6.75V9C3 10.6575 5.685 12 9 12C9.2925 12 9.5775 12 9.87 12L12.75 9.09C11.55 9.54 10.2825 9.75 9 9.75ZM9 2.25C5.685 2.25 3 3.5925 3 5.25C3 6.9075 5.685 8.25 9 8.25C12.315 8.25 15 6.9075 15 5.25C15 3.5925 12.315 2.25 9 2.25ZM15.75 8.3475C15.6375 8.3475 15.5325 8.3925 15.4575 8.475L14.7075 9.225L16.245 10.725L16.995 9.975C17.1525 9.825 17.16 9.57 16.995 9.3975L16.065 8.475C15.99 8.3925 15.885 8.3475 15.78 8.3475H15.75ZM14.28 9.66L9.75 14.205V15.75H11.295L15.84 11.1975L14.28 9.66Z"
fill="#A2B1C6"/>
</svg>
<change-db-icon />
</label>
<span class="db-edit-tooltip">Change database</span>
</div>
</div>
<div v-show="schemaVisible" class="schema">
<div v-if="schemaVisible" class="schema">
<table-description
v-for="(table, index) in schema"
:key="index"
v-for="table in schema"
:key="table[0]"
:name="table[0]"
:sql="table[1]"
/>
@@ -51,18 +42,32 @@
<script>
import TableDescription from '@/components/TableDescription'
import TextField from '@/components/TextField'
import ChangeDbIcon from '@/components/svg/changeDb'
export default {
name: 'Schema',
components: { TableDescription, TextField },
components: {
TableDescription,
TextField,
ChangeDbIcon
},
data () {
return {
schemaVisible: true
schemaVisible: true,
filter: null
}
},
computed: {
schema () {
return this.$store.state.schema
if (!this.$store.state.schema) {
return []
}
return !this.filter
? this.$store.state.schema
: this.$store.state.schema.filter(
table => table[0].toUpperCase().indexOf(this.filter.toUpperCase()) !== -1
)
},
dbName () {
return this.$store.state.dbName
@@ -77,8 +82,25 @@ export default {
</script>
<style scoped>
#schema-container {
position: relative;
padding-bottom: 24px;
}
.schema {
margin-left: 12px;
padding: 0 12px;
}
#schema-filter {
padding: 32px 12px;
position: sticky;
position: -webkit-sticky;
top: 0;
width: 100%;
height: 100px;
box-sizing: border-box;
background-image: linear-gradient(white 73%, transparent);;
z-index: 2;
}
.schema, .db-name {
color: var(--color-text-base);
@@ -88,6 +110,8 @@ export default {
#db {
display: flex;
align-items: center;
margin-top: -5px;
padding: 0 12px;
}
.db-name {
@@ -101,36 +125,4 @@ export default {
>>> .table-name:hover .chevron-icon path {
fill: #506784;
}
.db-edit {
position: relative;
}
.db-edit-icon {
display: block;
}
.db-edit-icon:hover path{
fill: var(--color-accent);
cursor: pointer;
}
.db-edit-tooltip {
visibility: hidden;
background-color: rgba(80, 103, 132, 0.75);
color: #fff;
text-align: center;
font-size: 11px;
padding: 0 6px;
line-height: 19px;;
position: absolute;
z-index: 5;
height: 19px;
left: 24px;
top: -12px;
border-radius: var(--border-radius-medium);
white-space: nowrap;
}
.db-edit:hover .db-edit-tooltip {
visibility: visible;
}
</style>

View File

@@ -22,7 +22,7 @@
<table ref="table">
<thead>
<tr>
<th v-for="(th,index) in data.columns" :key="index" ref="th">
<th v-for="(th,index) in dataSet.columns" :key="index" ref="th">
<div class="cell-data" :style="cellStyle">{{ th }}</div>
</th>
</tr>
@@ -39,7 +39,7 @@
</div>
<div class="table-footer">
<div class="table-footer-count">
{{ data.values.length}} {{data.values.length === 1 ? 'row' : 'rows'}} retrieved
{{ dataSet.values.length}} {{dataSet.values.length === 1 ? 'row' : 'rows'}} retrieved
</div>
<pager v-show="pageCount > 1" :page-count="pageCount" v-model="currentPage" />
</div>
@@ -52,17 +52,18 @@ import Pager from '@/components/Pager'
export default {
name: 'SqlTable',
components: { Pager },
props: ['data', 'height'],
props: ['dataSet', 'height'],
data () {
return {
header: null,
tableWidth: null,
currentPage: 1
currentPage: 1,
resizeObserver: null
}
},
computed: {
cellStyle () {
const eq = this.tableWidth / this.data.columns.length
const eq = this.tableWidth / this.dataSet.columns.length
return { maxWidth: `${Math.max(eq, 100)}px` }
},
@@ -70,11 +71,11 @@ export default {
return Math.max(Math.floor(this.height / 40), 20)
},
pageCount () {
return Math.ceil(this.data.values.length / this.pageSize)
return Math.ceil(this.dataSet.values.length / this.pageSize)
},
currentPageData () {
const start = (this.currentPage - 1) * this.pageSize
return this.data.values.slice(start, start + this.pageSize)
return this.dataSet.values.slice(start, start + this.pageSize)
}
},
methods: {
@@ -94,12 +95,16 @@ export default {
}
},
mounted () {
new ResizeObserver(this.calculateHeadersWidth).observe(this.$refs.table)
this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth)
this.resizeObserver.observe(this.$refs.table)
this.calculateHeadersWidth()
},
beforeDestroy () {
this.resizeObserver.unobserve(this.$refs.table)
},
watch: {
currentPageData: 'calculateHeadersWidth',
data () {
dataSet () {
this.currentPage = 1
}
}

View File

@@ -3,21 +3,12 @@
<splitpanes
class="query-results-splitter"
horizontal
:before="{ size: 50, max: 50 }"
:before="{ size: 50, max: 70 }"
:after="{ size: 50, max: 100 }"
>
<template #left-pane>
<div class="query-editor">
<sql-editor v-model="query" />
<div class="run-btn-container">
<button
class="primary run-btn"
@click="execute"
:disabled="!$store.state.schema || !query"
>
Run
</button>
</div>
</div>
</template>
<template #right-pane>
@@ -36,7 +27,7 @@
<div v-show="error" class="table-preview error">
{{ error }}
</div>
<sql-table v-if="result" :data="result" :height="tableViewHeight" />
<sql-table v-if="result" :data-set="result" :height="tableViewHeight" />
</div>
<chart
:visible="view === 'chart'"
@@ -59,8 +50,8 @@ import ViewSwitcher from '@/components/ViewSwitcher'
import Chart from '@/components/Chart'
export default {
name: 'TabContent',
props: ['id', 'initName', 'initQuery', 'initChart', 'tabIndex'],
name: 'Tab',
props: ['id', 'initName', 'initQuery', 'initChart', 'tabIndex', 'isPredefined'],
components: {
SqlEditor,
SqlTable,
@@ -76,7 +67,8 @@ export default {
tableViewHeight: 0,
isUnsaved: !this.initName,
isGettingResults: false,
error: null
error: null,
resizeObserver: null
}
},
computed: {
@@ -88,9 +80,13 @@ export default {
this.$store.commit('setCurrentTab', this)
},
mounted () {
new ResizeObserver(this.handleResize).observe(this.$refs.bottomPane)
this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.bottomPane)
this.calculateTableHeight()
},
beforeDestroy () {
this.resizeObserver.unobserve(this.$refs.bottomPane)
},
watch: {
isActive () {
if (this.isActive) {
@@ -166,12 +162,8 @@ export default {
background-color: var(--color-bg-light);
}
.run-btn {
margin-top: 24px;
}
.query-editor {
padding: 52px 52px 24px;
padding: 52px;
display: flex;
flex-direction: column;
height: 100%;
@@ -180,10 +172,6 @@ export default {
min-height: 190px;
}
.run-btn-container {
text-align: right;
}
.table-view {
margin: 0 52px;
height: calc(100% - 88px);

View File

@@ -3,7 +3,7 @@
<div id="tabs__header" v-if="tabs.length > 0">
<div
v-for="(tab, index) in tabs"
:key="tab.id"
:key="index"
@click="selectTab(tab.id)"
:class="[{'tab__selected': (tab.id === selectedIndex)}, 'tab']"
>
@@ -29,13 +29,14 @@
</div>
</div>
</div>
<tab-content
<tab
v-for="(tab, index) in tabs"
:key="tab.id"
:id="tab.id"
:init-name="tab.name"
:init-query="tab.query"
:init-chart="tab.chart"
:is-predefined="tab.isPredefined"
:tab-index="index"
/>
<div v-if="tabs.length === 0" id="start-guide">
@@ -47,11 +48,11 @@
</template>
<script>
import TabContent from '@/components/TabContent'
import Tab from '@/components/Tab'
export default {
components: {
TabContent
Tab
},
data () {
return {
@@ -153,5 +154,6 @@ export default {
color: var(--color-accent);
text-decoration: none;
cursor: pointer;
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<div>
<svg
class="db-edit-icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@mouseover="showTooltip"
@mouseout="$set(tooltipStyle, 'visibility', 'hidden')"
>
<path
d="M3 10.5V12.75C3 14.25 5.2875 15.54 8.25 15.75V13.5825L8.3475 13.5C5.34 13.32 3 12.045 3 10.5ZM9 9.75C5.685 9.75 3 8.4075 3 6.75V9C3 10.6575 5.685 12 9 12C9.2925 12 9.5775 12 9.87 12L12.75 9.09C11.55 9.54 10.2825 9.75 9 9.75ZM9 2.25C5.685 2.25 3 3.5925 3 5.25C3 6.9075 5.685 8.25 9 8.25C12.315 8.25 15 6.9075 15 5.25C15 3.5925 12.315 2.25 9 2.25ZM15.75 8.3475C15.6375 8.3475 15.5325 8.3925 15.4575 8.475L14.7075 9.225L16.245 10.725L16.995 9.975C17.1525 9.825 17.16 9.57 16.995 9.3975L16.065 8.475C15.99 8.3925 15.885 8.3475 15.78 8.3475H15.75ZM14.28 9.66L9.75 14.205V15.75H11.295L15.84 11.1975L14.28 9.66Z"
fill="#A2B1C6"
/>
</svg>
<span class="icon-tooltip" :style="tooltipStyle">
Change database
</span>
</div>
</template>
<script>
import tooltipMixin from '@/mixins/tooltips'
export default {
name: 'changeDbIcon',
mixins: [tooltipMixin]
}
</script>
<style scoped>
.db-edit-icon {
display: block;
cursor: pointer;
}
.db-edit-icon:hover path {
fill: var(--color-accent);
}
</style>

View File

@@ -1,12 +1,33 @@
<template>
<svg @click.stop="$emit('click')" class="icon" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.25 15.75H6V5.25H14.25V15.75ZM14.25 3.75H6C5.60218 3.75 5.22064 3.90804 4.93934 4.18934C4.65804 4.47064 4.5 4.85218 4.5 5.25V15.75C4.5 16.1478 4.65804 16.5294 4.93934 16.8107C5.22064 17.092 5.60218 17.25 6 17.25H14.25C14.6478 17.25 15.0294 17.092 15.3107 16.8107C15.592 16.5294 15.75 16.1478 15.75 15.75V5.25C15.75 4.85218 15.592 4.47064 15.3107 4.18934C15.0294 3.90804 14.6478 3.75 14.25 3.75ZM12 0.75H3C2.60218 0.75 2.22064 0.908035 1.93934 1.18934C1.65804 1.47064 1.5 1.85218 1.5 2.25V12.75H3V2.25H12V0.75Z" fill="#A2B1C6"/>
</svg>
<span>
<svg
class="icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@click.stop="$emit('click')"
@mouseover="showTooltip"
@mouseout="$set(tooltipStyle, 'visibility', 'hidden')"
>
<path
d="M14.25 15.75H6V5.25H14.25V15.75ZM14.25 3.75H6C5.60218 3.75 5.22064 3.90804 4.93934 4.18934C4.65804 4.47064 4.5 4.85218 4.5 5.25V15.75C4.5 16.1478 4.65804 16.5294 4.93934 16.8107C5.22064 17.092 5.60218 17.25 6 17.25H14.25C14.6478 17.25 15.0294 17.092 15.3107 16.8107C15.592 16.5294 15.75 16.1478 15.75 15.75V5.25C15.75 4.85218 15.592 4.47064 15.3107 4.18934C15.0294 3.90804 14.6478 3.75 14.25 3.75ZM12 0.75H3C2.60218 0.75 2.22064 0.908035 1.93934 1.18934C1.65804 1.47064 1.5 1.85218 1.5 2.25V12.75H3V2.25H12V0.75Z"
fill="#A2B1C6"
/>
</svg>
<span class="icon-tooltip" :style="tooltipStyle">
Duplicate query
</span>
</span>
</template>
<script>
import tooltipMixin from '@/mixins/tooltips'
export default {
name: 'CopyIcon'
name: 'CopyIcon',
mixins: [tooltipMixin]
}
</script>

View File

@@ -1,19 +1,40 @@
<template>
<svg @click.stop="$emit('click')" class="icon" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.75 2.25V3H3V4.5H3.75V14.25C3.75 14.6478 3.90804 15.0294 4.18934 15.3107C4.47064 15.592 4.85218 15.75 5.25 15.75H12.75C13.1478 15.75 13.5294 15.592 13.8107 15.3107C14.092 15.0294 14.25 14.6478 14.25 14.25V4.5H15V3H11.25V2.25H6.75ZM5.25 4.5H12.75V14.25H5.25V4.5ZM6.75 6V12.75H8.25V6H6.75ZM9.75 6V12.75H11.25V6H9.75Z" fill="#A2B1C6"/>
</svg>
<span>
<svg
class="icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@click.stop="$emit('click')"
@mouseover="showTooltip"
@mouseout="$set(tooltipStyle, 'visibility', 'hidden')"
>
<path
d="M6.75 2.25V3H3V4.5H3.75V14.25C3.75 14.6478 3.90804 15.0294 4.18934 15.3107C4.47064 15.592 4.85218 15.75 5.25 15.75H12.75C13.1478 15.75 13.5294 15.592 13.8107 15.3107C14.092 15.0294 14.25 14.6478 14.25 14.25V4.5H15V3H11.25V2.25H6.75ZM5.25 4.5H12.75V14.25H5.25V4.5ZM6.75 6V12.75H8.25V6H6.75ZM9.75 6V12.75H11.25V6H9.75Z"
fill="#A2B1C6"
/>
</svg>
<span class="icon-tooltip" :style="tooltipStyle">
Delete query
</span>
</span>
</template>
<script>
import tooltipMixin from '@/mixins/tooltips'
export default {
name: 'DeleteIcon'
name: 'DeleteIcon',
mixins: [tooltipMixin]
}
</script>
<style scoped>
.icon {
vertical-align: middle;
margin: 0 0 0 12px;
margin: 0 12px;
}
.icon:hover path {

View File

@@ -1,12 +1,33 @@
<template>
<svg @click.stop="$emit('click')" class="icon" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 1.5H4.5C3.675 1.5 3 2.175 3 3V15C3 15.825 3.675 16.5 4.5 16.5H13.5C14.325 16.5 15 15.825 15 15V6L10.5 1.5ZM13.5 15H4.5V3H9.75V6.75H13.5V15ZM12 8.25V13.575L10.425 12L8.325 14.1L6.225 12L8.325 9.9L6.675 8.25H12Z" fill="#A2B1C6"/>
</svg>
<span>
<svg
class="icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@click.stop="$emit('click')"
@mouseover="showTooltip"
@mouseout="$set(tooltipStyle, 'visibility', 'hidden')"
>
<path
d="M10.5 1.5H4.5C3.675 1.5 3 2.175 3 3V15C3 15.825 3.675 16.5 4.5 16.5H13.5C14.325 16.5 15 15.825 15 15V6L10.5 1.5ZM13.5 15H4.5V3H9.75V6.75H13.5V15ZM12 8.25V13.575L10.425 12L8.325 14.1L6.225 12L8.325 9.9L6.675 8.25H12Z"
fill="#A2B1C6"
/>
</svg>
<span class="icon-tooltip" :style="tooltipStyle">
Export query to file
</span>
</span>
</template>
<script>
import tooltipMixin from '@/mixins/tooltips'
export default {
name: 'ExportIcon'
name: 'ExportIcon',
mixins: [tooltipMixin]
}
</script>

View File

@@ -1,19 +1,40 @@
<template>
<svg @click.stop="$emit('click')" class="icon" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.545 6.75L11.25 7.455L4.44 14.25H3.75V13.56L10.545 6.75ZM13.245 2.25C13.0575 2.25 12.8625 2.325 12.72 2.4675L11.3475 3.84L14.16 6.6525L15.5325 5.28C15.825 4.9875 15.825 4.5 15.5325 4.2225L13.7775 2.4675C13.6275 2.3175 13.44 2.25 13.245 2.25ZM10.545 4.6425L2.25 12.9375V15.75H5.0625L13.3575 7.455L10.545 4.6425Z" fill="#A2B1C6"/>
</svg>
<span>
<svg
class="icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@click.stop="$emit('click')"
@mouseover="showTooltip"
@mouseout="$set(tooltipStyle, 'visibility', 'hidden')"
>
<path
d="M10.545 6.75L11.25 7.455L4.44 14.25H3.75V13.56L10.545 6.75ZM13.245 2.25C13.0575 2.25 12.8625 2.325 12.72 2.4675L11.3475 3.84L14.16 6.6525L15.5325 5.28C15.825 4.9875 15.825 4.5 15.5325 4.2225L13.7775 2.4675C13.6275 2.3175 13.44 2.25 13.245 2.25ZM10.545 4.6425L2.25 12.9375V15.75H5.0625L13.3575 7.455L10.545 4.6425Z"
fill="#A2B1C6"
/>
</svg>
<span class="icon-tooltip" :style="tooltipStyle">
Rename query
</span>
</span>
</template>
<script>
import tooltipMixin from '@/mixins/tooltips'
export default {
name: 'RenameIcon'
name: 'RenameIcon',
mixins: [tooltipMixin]
}
</script>
<style scoped>
.icon {
vertical-align: middle;
margin: 0 12px 0 6px;
margin: 0 12px;
}
.icon:hover path {

View File

@@ -10,6 +10,7 @@ import '@/assets/styles/variables.css'
import '@/assets/styles/buttons.css'
import '@/assets/styles/tables.css'
import '@/assets/styles/dialogs.css'
import '@/assets/styles/tooltips.css'
Vue.use(VuePlugin)
Vue.use(VModal)

17
src/mixins/tooltips.js Normal file
View File

@@ -0,0 +1,17 @@
export default {
data () {
return {
tooltipStyle: {}
}
},
methods: {
showTooltip (e) {
this.tooltipStyle = {
visibility: 'visible',
position: 'fixed',
top: e.clientY - 12 + 'px',
left: e.clientX + 12 + 'px'
}
}
}
}

View File

@@ -11,7 +11,8 @@ export default new Vuex.Store({
tabs: [],
currentTab: null,
currentTabId: null,
untitledLastIndex: 0
untitledLastIndex: 0,
predefinedQueries: []
},
mutations: {
saveSchema (state, schema) {
@@ -25,14 +26,29 @@ export default new Vuex.Store({
},
addTab (state, tab) {
state.tabs.push(tab)
if (!tab.name) {
state.untitledLastIndex += 1
}
},
updateTabName (state, { index, newName }) {
updateTab (state, { index, name, id, query, chart, isUnsaved }) {
const tab = state.tabs[index]
tab.name = newName
const oldId = tab.id
if (state.currentTabId === oldId) {
state.currentTabId = id
}
tab.id = id
if (name) { tab.name = name }
if (query) { tab.query = query }
if (chart) { tab.chart = chart }
if (isUnsaved !== undefined) { tab.isUnsaved = isUnsaved }
delete tab.isPredefined
Vue.set(state.tabs, index, tab)
},
updateTabState (state, { index, newValue }) {
console.log(index, newValue)
const tab = state.tabs[index]
tab.isUnsaved = newValue
Vue.set(state.tabs, index, tab)
@@ -45,6 +61,7 @@ export default new Vuex.Store({
state.currentTabId = state.tabs[index - 1].id
} else {
state.currentTabId = null
state.currentTab = null
state.untitledLastIndex = 0
}
state.tabs.splice(index, 1)
@@ -55,12 +72,14 @@ export default new Vuex.Store({
setCurrentTab (state, tab) {
state.currentTab = tab
},
updateUntitledLastIndex (state) {
state.untitledLastIndex += 1
updatePredefinedQueries (state, queries) {
if (Array.isArray(queries)) {
state.predefinedQueries = queries
} else {
state.predefinedQueries = [queries]
}
}
},
actions: {
},
modules: {
}
})

View File

@@ -13,7 +13,35 @@ import '@/assets/styles/scrollbars.css'
export default {
name: 'MainView',
components: { MainMenu }
components: { MainMenu },
created () {
this.readPredefinedQueries()
.then(queries => {
this.$store.commit('updatePredefinedQueries', queries)
})
.catch(console.error)
},
methods: {
readPredefinedQueries () {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('GET', './queries.json')
xhr.onload = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText))
} else {
reject(xhr.statusText)
}
}
}
xhr.onerror = () => {
reject(xhr.statusText)
}
xhr.send()
})
}
}
}
</script>
<style scoped>

View File

@@ -24,7 +24,7 @@
</button>
<button
class="toolbar"
v-show="selectedQueriesCount > 0"
v-show="selectedNotPredefinedCount > 0"
@click="showDeleteDialog(selectedQueriesIds)"
>
Delete
@@ -52,25 +52,41 @@
>
<table ref="table">
<tbody>
<tr v-for="(query, index) in showedQueries" :key="query.id" @click="openQuery(index)">
<tr
v-for="(query, index) in showedQueries"
:key="query.id"
:class="{ 'predefined': query.isPredefined }"
@click="openQuery(index)"
>
<td ref="name-td">
<div class="cell-data">
<check-box
ref="rowCheckBox"
:init="selectAll || selectedQueriesIds.has(query.id)"
@change="toggleRow($event, query.id)"
@click="toggleRow($event, query.id)"
/>
<div class="name">{{ query.name }}</div>
<div
class="badge"
@mouseover="showTooltip"
@mouseout="$set(tooltipStyle, 'visibility', 'hidden')"
>
Predefined
<span class="icon-tooltip" :style="tooltipStyle">
Predefined queries come from the server.
These queries cant be deleted or renamed.
</span>
</div>
</div>
</td>
<td>
<div class="second-column">
<div class="date-container">{{ query.createdAt | date }}</div>
<div class="icons-container">
<rename-icon @click="showRenameDialog(query.id)" />
<rename-icon v-if="!query.isPredefined" @click="showRenameDialog(query.id)" />
<copy-icon @click="duplicateQuery(index)"/>
<export-icon @click="exportQuery(index)"/>
<delete-icon @click="showDeleteDialog(query.id)"/>
<delete-icon v-if="!query.isPredefined" @click="showDeleteDialog(query.id)"/>
</div>
</div>
</td>
@@ -119,9 +135,13 @@
>
Are you sure you want to delete
{{ deleteGroup
? `${selectedQueriesCount} ${selectedQueriesCount > 1 ? 'queries' : 'query'}`
? `${selectedNotPredefinedCount} ${selectedNotPredefinedCount > 1 ? 'queries' : 'query'}`
: `"${queries[currentQueryIndex].name}"`
}}?
<div v-show="selectedQueriesCount > selectedNotPredefinedCount" id="note">
<img :src="require('@/assets/images/info.svg')">
Note: Predefined queries you've selected won't be deleted
</div>
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="$modal.hide('delete')">Cancel</button>
@@ -140,6 +160,7 @@ import DeleteIcon from '@/components/svg/delete'
import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField'
import CheckBox from '@/components/CheckBox'
import tooltipMixin from '@/mixins/tooltips'
import { nanoid } from 'nanoid'
export default {
@@ -153,6 +174,7 @@ export default {
TextField,
CheckBox
},
mixins: [tooltipMixin],
data () {
return {
queries: [],
@@ -162,29 +184,49 @@ export default {
errorMsg: null,
selectedQueriesIds: new Set(),
selectedQueriesCount: 0,
selectedNotPredefinedCount: 0,
selectAll: false,
deleteGroup: false
deleteGroup: false,
resizeObserver: null
}
},
computed: {
predefinedQueries () {
return this.$store.state.predefinedQueries.map(query => {
query.isPredefined = true
return query
})
},
predefinedQueriesIds () {
return new Set(this.predefinedQueries.map(query => query.id))
},
showedQueries () {
if (!this.filter) {
return this.queries
} else {
return this.queries.filter(query => query.name.toUpperCase().indexOf(this.filter.toUpperCase()) >= 0)
let showedQueries = this.allQueries
if (this.filter) {
showedQueries = showedQueries.filter(
query => query.name.toUpperCase().indexOf(this.filter.toUpperCase()) >= 0
)
}
return showedQueries
},
allQueries () {
return this.predefinedQueries.concat(this.queries)
},
currentQueryIndex () {
return this.queries.findIndex(query => query.id === this.currentQueryId)
}
},
created () {
this.queries = JSON.parse(localStorage.getItem('myQueries'))
this.queries = JSON.parse(localStorage.getItem('myQueries')) || []
},
mounted () {
new ResizeObserver(this.calcNameWidth).observe(this.$refs.table)
this.resizeObserver = new ResizeObserver(this.calcNameWidth)
this.resizeObserver.observe(this.$refs.table)
this.calcNameWidth()
},
beforeDestroy () {
this.resizeObserver.unobserve(this.$refs.table)
},
filters: {
date (value) {
if (!value) {
@@ -205,7 +247,7 @@ export default {
this.$refs['name-th'].style = `width: ${this.$refs['name-td'][0].offsetWidth}px`
},
openQuery (index) {
const tab = this.showedQueries[index]
const tab = JSON.parse(JSON.stringify(this.showedQueries[index]))
tab.isUnsaved = false
this.$store.commit('addTab', tab)
this.$store.commit('setCurrentTabId', tab.id)
@@ -225,23 +267,33 @@ export default {
const currentQuery = this.queries[this.currentQueryIndex]
currentQuery.name = this.newName
this.$set(this.queries, this.currentQueryIndex, currentQuery)
this.$modal.hide('rename')
// update queries in local storage
this.saveQueriesInLocalStorage()
// update tab, if renamed query is opened
const tabIndex = this.findTabIndex(currentQuery.id)
if (tabIndex >= 0) {
this.$store.commit('updateTabName', { index: tabIndex, newName: this.newName })
this.$store.commit('updateTab', {
index: tabIndex,
name: this.newName,
id: currentQuery.id
})
}
// hide dialog
this.$modal.hide('rename')
},
duplicateQuery (index) {
const newQuery = JSON.parse(JSON.stringify(this.showedQueries[index]))
newQuery.name = newQuery.name + ' Copy'
newQuery.id = nanoid()
newQuery.createdAt = new Date()
this.queries.push(newQuery)
delete newQuery.isPredefined
if (this.selectAll) {
this.selectedQueriesIds.add(newQuery.id)
this.selectedQueriesCount = this.selectedQueriesIds.size
}
this.queries.push(newQuery)
this.saveQueriesInLocalStorage()
},
showDeleteDialog (id) {
@@ -286,22 +338,18 @@ export default {
// single operation
if (typeof index === 'number') {
console.log('single')
data = JSON.parse(JSON.stringify(this.showedQueries[index]))
name = data.name
delete data.id
delete data.createdAt
delete data.isPredefined
} else {
// group operation
data = this.selectAll
? JSON.parse(JSON.stringify(this.queries))
: this.queries.filter(query => this.selectedQueriesIds.has(query.id))
? JSON.parse(JSON.stringify(this.allQueries))
: this.allQueries.filter(query => this.selectedQueriesIds.has(query.id))
name = 'My sqliteviz queries'
data.forEach(query => {
delete query.id
delete query.createdAt
})
data.forEach(query => delete query.isPredefined)
}
// export data to file
const downloader = this.$refs.downloader
const json = JSON.stringify(data, null, 4)
@@ -323,14 +371,19 @@ export default {
}
importedQueries.forEach(query => {
query.id = nanoid()
query.createdAt = new Date()
if (this.selectAll) {
this.selectedQueriesIds.add(query.id)
this.selectedQueriesCount = this.selectedQueriesIds.size
const allQueriesIds = this.allQueries.map(query => query.id)
if (new Set(allQueriesIds).has(query.id)) {
query.id = nanoid()
}
})
if (this.selectAll) {
importedQueries.forEach(query => {
this.selectedQueriesIds.add(query.id)
})
this.selectedQueriesCount = this.selectedQueriesIds.size
}
this.queries = this.queries.concat(importedQueries)
this.saveQueriesInLocalStorage()
this.$refs.importFile.value = null
@@ -343,18 +396,30 @@ export default {
toggleSelectAll (checked) {
this.selectAll = checked
this.$refs.rowCheckBox.forEach(item => { item.checked = checked })
this.selectedQueriesIds = checked ? new Set(this.queries.map(query => query.id)) : new Set()
this.selectedQueriesIds = checked
? new Set(this.allQueries.map(query => query.id))
: new Set()
this.selectedQueriesCount = this.selectedQueriesIds.size
this.selectedNotPredefinedCount = checked ? this.queries.length : 0
},
toggleRow (checked, id) {
const isPredefined = this.predefinedQueriesIds.has(id)
if (checked) {
this.selectedQueriesIds.add(id)
if (!isPredefined) {
this.selectedNotPredefinedCount += 1
}
} else {
if (this.selectedQueriesIds.size === this.queries.length) {
if (this.selectedQueriesIds.size === this.allQueries.length) {
this.$refs.mainCheckBox.checked = false
this.selectAll = false
}
this.selectedQueriesIds.delete(id)
if (!isPredefined) {
this.selectedNotPredefinedCount -= 1
}
}
this.selectedQueriesCount = this.selectedQueriesIds.size
}
@@ -413,6 +478,7 @@ tbody .cell-data {
display: flex;
align-items: center;
max-width: 100%;
width: 100%;
}
tbody .cell-data div.name {
overflow: hidden;
@@ -438,6 +504,7 @@ tbody tr:hover td {
.icons-container {
display: none;
margin-right: -12px;
}
.date-container {
flex-shrink: 1;
@@ -457,11 +524,42 @@ a, #import-file {
button.toolbar {
margin-right: 16px;
}
button label {
display: block;
line-height: 36px;
}
button label:hover {
cursor: pointer;
}
.badge {
display: none;
background-color: var(--color-gray-light-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-small);
padding: 2px 6px;
font-size: 11px;
line-height: normal;
margin-left: 12px;
}
tbody tr.predefined:hover .badge {
display: block;
}
#note {
margin-top: 24px;
}
#note img {
vertical-align: middle;
}
.icon-tooltip {
display: block;
width: 149px;
white-space: normal;
height: auto;
line-height: normal;
padding: 6px;
}
</style>