mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-07 02:28:54 +08:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2464d839f | ||
|
|
316e603c3c | ||
|
|
88466eca5e | ||
|
|
5123e39a60 | ||
|
|
4c8401f32f | ||
|
|
d949629ee4 | ||
|
|
7a18e415c8 | ||
|
|
878689b3f7 | ||
|
|
42f040975d | ||
|
|
78e9ca2120 | ||
|
|
96af391f20 | ||
|
|
f58b62eb0c | ||
|
|
b17040d3ef | ||
|
|
bc6154b9ad | ||
|
|
3aea8c951b | ||
|
|
1e982a1196 | ||
|
|
6ecbde7fd3 | ||
|
|
5ee881432a | ||
|
|
735e4ec7f6 | ||
|
|
07d31dbfe9 | ||
|
|
ac1f7de62c | ||
|
|
96877de532 | ||
|
|
b60fc28e47 | ||
|
|
bec3d9c737 | ||
|
|
8aac7af481 | ||
|
|
6982204e68 | ||
|
|
41e0ae7332 | ||
|
|
ebb5af4f10 | ||
|
|
ae26358b25 | ||
|
|
d9ee702b8e | ||
|
|
446045fa55 | ||
|
|
1a9d1b308b | ||
|
|
014ecf145e | ||
|
|
0044d82b6f | ||
|
|
998e8d66f7 |
31
package-lock.json
generated
31
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sqliteviz",
|
"name": "sqliteviz",
|
||||||
"version": "0.22.0",
|
"version": "0.23.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sqliteviz",
|
"name": "sqliteviz",
|
||||||
"version": "0.22.0",
|
"version": "0.23.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"codemirror": "^5.57.0",
|
"codemirror": "^5.57.0",
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"html2canvas": "^1.1.4",
|
"html2canvas": "^1.1.4",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
"nanoid": "^3.1.12",
|
"nanoid": "^3.1.12",
|
||||||
"papaparse": "^5.3.1",
|
"papaparse": "^5.4.1",
|
||||||
"pivottable": "^2.23.0",
|
"pivottable": "^2.23.0",
|
||||||
"plotly.js": "^1.58.4",
|
"plotly.js": "^1.58.4",
|
||||||
"promise-worker": "^2.0.1",
|
"promise-worker": "^2.0.1",
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
"eslint-plugin-promise": "^4.2.1",
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
"eslint-plugin-standard": "^4.0.0",
|
"eslint-plugin-standard": "^4.0.0",
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
|
"flush-promises": "^1.0.2",
|
||||||
"karma": "^3.1.4",
|
"karma": "^3.1.4",
|
||||||
"karma-firefox-launcher": "^2.1.0",
|
"karma-firefox-launcher": "^2.1.0",
|
||||||
"karma-webpack": "^4.0.2",
|
"karma-webpack": "^4.0.2",
|
||||||
@@ -10090,6 +10091,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/flip-pixels/-/flip-pixels-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/flip-pixels/-/flip-pixels-1.0.2.tgz",
|
||||||
"integrity": "sha512-oXbJGbjDnfJRWPC7Va38EFhd+A8JWE5/hCiKcK8qjCdbLj9DTpsq6MEudwpRTH+V4qq+Jw7d3pUgQdSr3x3mTA=="
|
"integrity": "sha512-oXbJGbjDnfJRWPC7Va38EFhd+A8JWE5/hCiKcK8qjCdbLj9DTpsq6MEudwpRTH+V4qq+Jw7d3pUgQdSr3x3mTA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/flush-promises": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/flush-promises/-/flush-promises-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-G0sYfLQERwKz4+4iOZYQEZVpOt9zQrlItIxQAAYAWpfby3gbHrx0osCHz5RLl/XoXevXk0xoN4hDFky/VV9TrA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/flush-write-stream": {
|
"node_modules/flush-write-stream": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
|
||||||
@@ -16374,9 +16381,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/papaparse": {
|
"node_modules/papaparse": {
|
||||||
"version": "5.3.2",
|
"version": "5.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz",
|
||||||
"integrity": "sha512-6dNZu0Ki+gyV0eBsFKJhYr+MdQYAzFUGlBMNj3GNrmHxmz1lfRa24CjFObPXtjcetlOv5Ad299MhIK0znp3afw=="
|
"integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw=="
|
||||||
},
|
},
|
||||||
"node_modules/parallel-transform": {
|
"node_modules/parallel-transform": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
@@ -33658,6 +33665,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/flip-pixels/-/flip-pixels-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/flip-pixels/-/flip-pixels-1.0.2.tgz",
|
||||||
"integrity": "sha512-oXbJGbjDnfJRWPC7Va38EFhd+A8JWE5/hCiKcK8qjCdbLj9DTpsq6MEudwpRTH+V4qq+Jw7d3pUgQdSr3x3mTA=="
|
"integrity": "sha512-oXbJGbjDnfJRWPC7Va38EFhd+A8JWE5/hCiKcK8qjCdbLj9DTpsq6MEudwpRTH+V4qq+Jw7d3pUgQdSr3x3mTA=="
|
||||||
},
|
},
|
||||||
|
"flush-promises": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/flush-promises/-/flush-promises-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-G0sYfLQERwKz4+4iOZYQEZVpOt9zQrlItIxQAAYAWpfby3gbHrx0osCHz5RLl/XoXevXk0xoN4hDFky/VV9TrA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"flush-write-stream": {
|
"flush-write-stream": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
|
||||||
@@ -38842,9 +38855,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"papaparse": {
|
"papaparse": {
|
||||||
"version": "5.3.2",
|
"version": "5.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz",
|
||||||
"integrity": "sha512-6dNZu0Ki+gyV0eBsFKJhYr+MdQYAzFUGlBMNj3GNrmHxmz1lfRa24CjFObPXtjcetlOv5Ad299MhIK0znp3afw=="
|
"integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw=="
|
||||||
},
|
},
|
||||||
"parallel-transform": {
|
"parallel-transform": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sqliteviz",
|
"name": "sqliteviz",
|
||||||
"version": "0.22.0",
|
"version": "0.24.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"html2canvas": "^1.1.4",
|
"html2canvas": "^1.1.4",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
"nanoid": "^3.1.12",
|
"nanoid": "^3.1.12",
|
||||||
"papaparse": "^5.3.1",
|
"papaparse": "^5.4.1",
|
||||||
"pivottable": "^2.23.0",
|
"pivottable": "^2.23.0",
|
||||||
"plotly.js": "^1.58.4",
|
"plotly.js": "^1.58.4",
|
||||||
"promise-worker": "^2.0.1",
|
"promise-worker": "^2.0.1",
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
"eslint-plugin-promise": "^4.2.1",
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
"eslint-plugin-standard": "^4.0.0",
|
"eslint-plugin-standard": "^4.0.0",
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
|
"flush-promises": "^1.0.2",
|
||||||
"karma": "^3.1.4",
|
"karma": "^3.1.4",
|
||||||
"karma-firefox-launcher": "^2.1.0",
|
"karma-firefox-launcher": "^2.1.0",
|
||||||
"karma-webpack": "^4.0.2",
|
"karma-webpack": "^4.0.2",
|
||||||
|
|||||||
@@ -107,3 +107,9 @@ table.sqliteviz-table {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sqliteviz-table tbody td[data-isNull="true"],
|
||||||
|
.sqliteviz-table tbody td[data-isBlob="true"] {
|
||||||
|
color: var(--color-text-light-2);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<button
|
||||||
:class="['icon-btn', { active }, { disabled }]"
|
:class="['icon-btn', { active }]"
|
||||||
|
:disabled="disabled"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
@mouseenter="showTooltip($event, tooltipPosition)"
|
@mouseenter="showTooltip($event, tooltipPosition)"
|
||||||
@mouseleave="hideTooltip"
|
@mouseleave="hideTooltip"
|
||||||
@@ -12,7 +13,7 @@
|
|||||||
<span v-if="tooltip" class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
<span v-if="tooltip" class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
||||||
{{ tooltip }}
|
{{ tooltip }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -38,11 +39,12 @@ export default {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 26px;
|
width: 26px;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
.icon-btn:hover {
|
.icon-btn:hover {
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
@@ -56,12 +58,12 @@ export default {
|
|||||||
fill: var(--color-accent);
|
fill: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled.icon-btn .icon >>> path,
|
.icon-btn:disabled .icon >>> path,
|
||||||
.disabled.icon-btn .icon >>> circle {
|
.icon-btn:disabled .icon >>> circle {
|
||||||
fill: var(--color-border);
|
fill: var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled.icon-btn {
|
.icon-btn:disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,14 +75,23 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
horizontal: { type: Boolean, default: false },
|
horizontal: { type: Boolean, default: false },
|
||||||
before: { type: Object },
|
before: { type: Object },
|
||||||
after: { type: Object }
|
after: { type: Object },
|
||||||
|
default: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {
|
||||||
|
before: 50,
|
||||||
|
after: 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
container: null,
|
container: null,
|
||||||
paneBefore: this.before,
|
paneBefore: this.before,
|
||||||
paneAfter: this.after,
|
paneAfter: this.after,
|
||||||
beforeMinimising: {
|
beforeMinimising: !this.after.size || !this.before.size ? this.default : {
|
||||||
before: this.before.size,
|
before: this.before.size,
|
||||||
after: this.after.size
|
after: this.after.size
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,7 +18,12 @@
|
|||||||
ref="table-container"
|
ref="table-container"
|
||||||
@scroll="onScrollTable"
|
@scroll="onScrollTable"
|
||||||
>
|
>
|
||||||
<table ref="table" class="sqliteviz-table">
|
<table
|
||||||
|
ref="table"
|
||||||
|
class="sqliteviz-table"
|
||||||
|
tabindex="0"
|
||||||
|
@keydown="onTableKeydown"
|
||||||
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th v-for="(th, index) in columns" :key="index" ref="th">
|
<th v-for="(th, index) in columns" :key="index" ref="th">
|
||||||
@@ -28,9 +33,18 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="rowIndex in currentPageData.count" :key="rowIndex">
|
<tr v-for="rowIndex in currentPageData.count" :key="rowIndex">
|
||||||
<td v-for="(col, colIndex) in columns" :key="colIndex">
|
<td
|
||||||
|
v-for="(col, colIndex) in columns"
|
||||||
|
:data-col="colIndex"
|
||||||
|
:data-row="pageSize * (currentPage - 1) + rowIndex - 1"
|
||||||
|
:data-isNull="isNull(getCellValue(col, rowIndex))"
|
||||||
|
:data-isBlob="isBlob(getCellValue(col, rowIndex))"
|
||||||
|
:key="colIndex"
|
||||||
|
:aria-selected="false"
|
||||||
|
@click="onCellClick"
|
||||||
|
>
|
||||||
<div class="cell-data" :style="cellStyle">
|
<div class="cell-data" :style="cellStyle">
|
||||||
{{ dataSet.values[col][rowIndex - 1 + currentPageData.start] }}
|
{{ getCellText(col, rowIndex) }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -44,7 +58,11 @@
|
|||||||
<span v-if="preview">for preview</span>
|
<span v-if="preview">for preview</span>
|
||||||
<span v-if="time">in {{ time }}</span>
|
<span v-if="time">in {{ time }}</span>
|
||||||
</div>
|
</div>
|
||||||
<pager v-show="pageCount > 1" :page-count="pageCount" v-model="currentPage" />
|
<pager
|
||||||
|
v-show="pageCount > 1"
|
||||||
|
:page-count="pageCount"
|
||||||
|
v-model="currentPage"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -62,14 +80,20 @@ export default {
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 20
|
default: 20
|
||||||
},
|
},
|
||||||
preview: Boolean
|
page: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
preview: Boolean,
|
||||||
|
selectedCellCoordinates: Object
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
header: null,
|
header: null,
|
||||||
tableWidth: null,
|
tableWidth: null,
|
||||||
currentPage: 1,
|
currentPage: this.page,
|
||||||
resizeObserver: null
|
resizeObserver: null,
|
||||||
|
selectedCellElement: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -99,7 +123,40 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted () {
|
||||||
|
this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth)
|
||||||
|
this.resizeObserver.observe(this.$refs.table)
|
||||||
|
this.calculateHeadersWidth()
|
||||||
|
|
||||||
|
if (this.selectedCellCoordinates) {
|
||||||
|
const { row, col } = this.selectedCellCoordinates
|
||||||
|
const cell = this.$refs.table
|
||||||
|
.querySelector(`td[data-col="${col}"][data-row="${row}"]`)
|
||||||
|
if (cell) {
|
||||||
|
this.selectCell(cell)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
isBlob (value) {
|
||||||
|
return value && ArrayBuffer.isView(value)
|
||||||
|
},
|
||||||
|
isNull (value) {
|
||||||
|
return value === null
|
||||||
|
},
|
||||||
|
getCellValue (col, rowIndex) {
|
||||||
|
return this.dataSet.values[col][rowIndex - 1 + this.currentPageData.start]
|
||||||
|
},
|
||||||
|
getCellText (col, rowIndex) {
|
||||||
|
const value = this.getCellValue(col, rowIndex)
|
||||||
|
if (this.isNull(value)) {
|
||||||
|
return 'NULL'
|
||||||
|
}
|
||||||
|
if (this.isBlob(value)) {
|
||||||
|
return 'BLOB'
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
},
|
||||||
calculateHeadersWidth () {
|
calculateHeadersWidth () {
|
||||||
this.tableWidth = this.$refs['table-container'].offsetWidth
|
this.tableWidth = this.$refs['table-container'].offsetWidth
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -110,18 +167,95 @@ export default {
|
|||||||
},
|
},
|
||||||
onScrollTable () {
|
onScrollTable () {
|
||||||
this.$refs['header-container'].scrollLeft = this.$refs['table-container'].scrollLeft
|
this.$refs['header-container'].scrollLeft = this.$refs['table-container'].scrollLeft
|
||||||
}
|
|
||||||
},
|
},
|
||||||
mounted () {
|
onTableKeydown (e) {
|
||||||
this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth)
|
const keyCodeMap = {
|
||||||
this.resizeObserver.observe(this.$refs.table)
|
37: 'left',
|
||||||
this.calculateHeadersWidth()
|
39: 'right',
|
||||||
|
38: 'up',
|
||||||
|
40: 'down'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.selectedCellElement ||
|
||||||
|
!Object.keys(keyCodeMap).includes(e.keyCode.toString())
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.moveFocusInTable(this.selectedCellElement, keyCodeMap[e.keyCode])
|
||||||
|
},
|
||||||
|
onCellClick (e) {
|
||||||
|
this.selectCell(e.target.closest('td'), false)
|
||||||
|
},
|
||||||
|
selectCell (cell, scrollTo = true) {
|
||||||
|
if (!cell) {
|
||||||
|
if (this.selectedCellElement) {
|
||||||
|
this.selectedCellElement.ariaSelected = 'false'
|
||||||
|
}
|
||||||
|
this.selectedCellElement = cell
|
||||||
|
} else if (!cell.ariaSelected || cell.ariaSelected === 'false') {
|
||||||
|
if (this.selectedCellElement) {
|
||||||
|
this.selectedCellElement.ariaSelected = 'false'
|
||||||
|
}
|
||||||
|
cell.ariaSelected = 'true'
|
||||||
|
this.selectedCellElement = cell
|
||||||
|
} else {
|
||||||
|
cell.ariaSelected = 'false'
|
||||||
|
this.selectedCellElement = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedCellElement && scrollTo) {
|
||||||
|
this.selectedCellElement.scrollIntoView()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('updateSelectedCell', this.selectedCellElement)
|
||||||
|
},
|
||||||
|
moveFocusInTable (initialCell, direction) {
|
||||||
|
const currentRowIndex = +initialCell.dataset.row
|
||||||
|
const currentColIndex = +initialCell.dataset.col
|
||||||
|
let newRowIndex, newColIndex
|
||||||
|
|
||||||
|
if (direction === 'right') {
|
||||||
|
if (currentColIndex === this.columns.length - 1) {
|
||||||
|
newRowIndex = currentRowIndex + 1
|
||||||
|
newColIndex = 0
|
||||||
|
} else {
|
||||||
|
newRowIndex = currentRowIndex
|
||||||
|
newColIndex = currentColIndex + 1
|
||||||
|
}
|
||||||
|
} else if (direction === 'left') {
|
||||||
|
if (currentColIndex === 0) {
|
||||||
|
newRowIndex = currentRowIndex - 1
|
||||||
|
newColIndex = this.columns.length - 1
|
||||||
|
} else {
|
||||||
|
newRowIndex = currentRowIndex
|
||||||
|
newColIndex = currentColIndex - 1
|
||||||
|
}
|
||||||
|
} else if (direction === 'up') {
|
||||||
|
newRowIndex = currentRowIndex - 1
|
||||||
|
newColIndex = currentColIndex
|
||||||
|
} else if (direction === 'down') {
|
||||||
|
newRowIndex = currentRowIndex + 1
|
||||||
|
newColIndex = currentColIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCell = this.$refs.table
|
||||||
|
.querySelector(`td[data-col="${newColIndex}"][data-row="${newRowIndex}"]`)
|
||||||
|
if (newCell) {
|
||||||
|
this.selectCell(newCell)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
this.resizeObserver.unobserve(this.$refs.table)
|
this.resizeObserver.unobserve(this.$refs.table)
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
currentPageData: 'calculateHeadersWidth',
|
currentPageData () {
|
||||||
|
this.calculateHeadersWidth()
|
||||||
|
this.selectCell(null)
|
||||||
|
},
|
||||||
dataSet () {
|
dataSet () {
|
||||||
this.currentPage = 1
|
this.currentPage = 1
|
||||||
}
|
}
|
||||||
@@ -130,4 +264,13 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
table.sqliteviz-table:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.sqliteviz-table tbody td:hover {
|
||||||
|
background-color: var(--color-bg-light-3);
|
||||||
|
}
|
||||||
|
.sqliteviz-table tbody td[aria-selected="true"] {
|
||||||
|
box-shadow:inset 0 0 0 1px var(--color-accent);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
20
src/components/svg/arrow.vue
Normal file
20
src/components/svg/arrow.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="28"
|
||||||
|
height="27"
|
||||||
|
viewBox="0 0 28 27"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M17.9475 8.33625L12.7838 13.5L17.9475 18.6638L16.35 20.25L9.60001
|
||||||
|
13.5L16.35 6.75L17.9475 8.33625Z"
|
||||||
|
fill="#506784"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
}
|
||||||
|
</script>
|
||||||
26
src/components/svg/edgeArrow.vue
Normal file
26
src/components/svg/edgeArrow.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="27"
|
||||||
|
height="27"
|
||||||
|
viewBox="0 0 27 27"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M17.3474 8.33625L12.1837 13.5L17.3474 18.6638L15.7499 20.25L8.99991
|
||||||
|
13.5L15.7499 6.75L17.3474 8.33625Z"
|
||||||
|
fill="#506784"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M7.19995 19.8L7.19995 7.20001H9.19995V19.8H7.19995Z"
|
||||||
|
fill="#506784"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
}
|
||||||
|
</script>
|
||||||
53
src/components/svg/row.vue
Normal file
53
src/components/svg/row.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="19"
|
||||||
|
height="19"
|
||||||
|
viewBox="0 0 19 19"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<g clip-path="url(#clip0_2130_5292)">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M1.85303 11.3794L1.85303 7.80371L5.86304 7.80371L5.86304
|
||||||
|
11.3794L1.85303 11.3794ZM7.36304 11.3794L7.36304 7.80371L11.3428
|
||||||
|
7.80371L11.3428 11.3794L7.36304 11.3794ZM12.8428 11.3794L16.853
|
||||||
|
11.3794L16.853 7.80371L12.8428 7.80371L12.8428 11.3794ZM15.353
|
||||||
|
6.30371L16.853 6.30371C17.6815 6.30371 18.353 6.97528 18.353
|
||||||
|
7.80371L18.353 11.3794C18.353 12.2078 17.6815 12.8794 16.853
|
||||||
|
12.8794L15.353 12.8794L15.353 14.3111C15.353 15.0153 14.7603 15.5916
|
||||||
|
14.0358 15.5916L4.67027 15.5916C3.94579 15.5916 3.35303 15.0153 3.35303
|
||||||
|
14.3111L3.35303 12.8794L1.85303 12.8794C1.0246 12.8794 0.353027 12.2078
|
||||||
|
0.353027 11.3794L0.353027 7.80371C0.353027 6.97528 1.0246 6.30371
|
||||||
|
1.85303 6.30371L3.35303 6.30371L3.35303 4.87201C3.35303 4.16349 3.94139
|
||||||
|
3.59155 4.67027 3.59155L14.0358 3.59155C14.7604 3.59155 15.353 4.16117
|
||||||
|
15.353 4.87201L15.353 6.30371ZM14.0315 6.30371L14.0315 4.87086L11.887
|
||||||
|
4.87086L11.887 6.30371L12.8428 6.30371L14.0315 6.30371ZM10.387
|
||||||
|
6.30371L10.387 4.87086L8.26685 4.87086L8.26685 6.30371L10.387
|
||||||
|
6.30371ZM6.76685 6.30371L6.76685 4.87086L4.67027 4.87086L4.67027
|
||||||
|
6.30371L6.76685 6.30371ZM4.67027 12.8794L4.67027 14.3121L6.76685
|
||||||
|
14.3121L6.76685 12.8794L4.67027 12.8794ZM8.26685 12.8794L8.26685
|
||||||
|
14.3121L10.387 14.3121L10.387 12.8794L8.26685 12.8794ZM11.887
|
||||||
|
12.8794L11.887 14.3121L14.0315 14.3121L14.0315 12.8794L11.887 12.8794Z"
|
||||||
|
fill="#A2B1C6"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2130_5292">
|
||||||
|
<rect
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
fill="white"
|
||||||
|
transform="translate(0.353027 18.5916) rotate(-90)"
|
||||||
|
/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RowIcon'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
50
src/components/svg/viewCellValue.vue
Normal file
50
src/components/svg/viewCellValue.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="19"
|
||||||
|
height="19"
|
||||||
|
viewBox="0 0 19 19"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<g clip-path="url(#clip0_2131_6054)">
|
||||||
|
<path
|
||||||
|
d="M3.53784 11.5846L3.53784 3.14734L11.9751 3.14734V7.676C12.4655 7.51991
|
||||||
|
12.9771 7.47439 13.4751 7.53264V3.14734C13.4751 2.31891 12.8035 1.64734
|
||||||
|
11.9751 1.64734L3.53784 1.64734C2.70941 1.64734 2.03784 2.31891 2.03784
|
||||||
|
3.14734L2.03784 11.5846C2.03784 12.413 2.70942 13.0846 3.53784
|
||||||
|
13.0846H10.0831C9.771 12.6184 9.58279 12.1055 9.51083
|
||||||
|
11.5846H3.53784Z"
|
||||||
|
fill="#A2B1C6"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M14.7887 9.9291C15.4307 10.8837 15.1773 12.1779 14.2228
|
||||||
|
12.8199C13.2682 13.4618 11.974 13.2084 11.332 12.2539C10.69 11.2993
|
||||||
|
10.9434 10.0051 11.898 9.3631C12.8525 8.72113 14.1468 8.97454 14.7887
|
||||||
|
9.9291ZM14.4606 14.3901L16.6181 17.5982C16.8492 17.9419 17.3153 18.0331
|
||||||
|
17.659 17.802C18.0027 17.5708 18.0939 17.1048 17.8628 16.7611L15.6884
|
||||||
|
13.5279C16.7949 12.3365 16.9801 10.4996 16.0334 9.092C14.9292 7.45002
|
||||||
|
12.7029 7.01412 11.0609 8.1184C9.41891 9.22268 8.98302 11.449 10.0873
|
||||||
|
13.0909C11.062 14.5403 12.9109 15.05 14.4606 14.3901Z"
|
||||||
|
fill="#A2B1C6"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2131_6054">
|
||||||
|
<rect
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
fill="white"
|
||||||
|
transform="translate(0.5 18.5916) rotate(-90)"
|
||||||
|
/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ViewCellValueIcon'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -73,7 +73,9 @@ export default {
|
|||||||
comments: false,
|
comments: false,
|
||||||
step: undefined,
|
step: undefined,
|
||||||
complete: results => {
|
complete: results => {
|
||||||
const res = {
|
let res
|
||||||
|
try {
|
||||||
|
res = {
|
||||||
data: this.getResult(results),
|
data: this.getResult(results),
|
||||||
delimiter: results.meta.delimiter,
|
delimiter: results.meta.delimiter,
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
@@ -85,9 +87,12 @@ export default {
|
|||||||
msg.hint = hintsByCode[msg.code]
|
msg.hint = hintsByCode[msg.code]
|
||||||
return msg
|
return msg
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
resolve(res)
|
resolve(res)
|
||||||
},
|
},
|
||||||
error: (error, file) => {
|
error: error => {
|
||||||
reject(error)
|
reject(error)
|
||||||
},
|
},
|
||||||
download: false,
|
download: false,
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ function _getDataSourcesFromSqlResult (sqlResult) {
|
|||||||
if (!sqlResult) {
|
if (!sqlResult) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
const dataSorces = {}
|
const dataSources = {}
|
||||||
sqlResult.columns.forEach((column, index) => {
|
sqlResult.columns.forEach((column, index) => {
|
||||||
dataSorces[column] = sqlResult.values.map(row => row[index])
|
dataSources[column] = sqlResult.values.map(row => row[index])
|
||||||
})
|
})
|
||||||
return dataSorces
|
return dataSources
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Sql {
|
export default class Sql {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.dbName = file ? fu.getFileName(file) : 'database'
|
this.dbName = file ? fu.getFileName(file) : 'database'
|
||||||
this.refreshSchema()
|
await this.refreshSchema()
|
||||||
|
|
||||||
events.send('database.import', file ? file.size : 0, {
|
events.send('database.import', file ? file.size : 0, {
|
||||||
from: file ? 'sqlite' : 'none',
|
from: file ? 'sqlite' : 'none',
|
||||||
|
|||||||
@@ -33,17 +33,16 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
isTabNeedName (inquiryTab) {
|
isTabNeedName (inquiryTab) {
|
||||||
const isFromScratch = !inquiryTab.initName
|
return inquiryTab.isPredefined || !inquiryTab.name
|
||||||
return inquiryTab.isPredefined || isFromScratch
|
|
||||||
},
|
},
|
||||||
|
|
||||||
save (inquiryTab, newName) {
|
save (inquiryTab, newName) {
|
||||||
const value = {
|
const value = {
|
||||||
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
|
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
|
||||||
query: inquiryTab.query,
|
query: inquiryTab.query,
|
||||||
viewType: inquiryTab.$refs.dataView.mode,
|
viewType: inquiryTab.dataView.mode,
|
||||||
viewOptions: inquiryTab.$refs.dataView.getOptionsForSave(),
|
viewOptions: inquiryTab.dataView.getOptionsForSave(),
|
||||||
name: newName || inquiryTab.initName
|
name: newName || inquiryTab.name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get inquiries from local storage
|
// Get inquiries from local storage
|
||||||
|
|||||||
59
src/lib/tab.js
Normal file
59
src/lib/tab.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
import time from '@/lib/utils/time'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
|
export default class Tab {
|
||||||
|
constructor (state, inquiry = {}) {
|
||||||
|
this.id = inquiry.id || nanoid()
|
||||||
|
this.name = inquiry.id ? inquiry.name : null
|
||||||
|
this.tempName = inquiry.name || (state.untitledLastIndex
|
||||||
|
? `Untitled ${state.untitledLastIndex}`
|
||||||
|
: 'Untitled')
|
||||||
|
this.query = inquiry.query
|
||||||
|
this.viewOptions = inquiry.viewOptions || undefined
|
||||||
|
this.isPredefined = inquiry.isPredefined
|
||||||
|
this.viewType = inquiry.viewType || 'chart'
|
||||||
|
this.result = null
|
||||||
|
this.isGettingResults = false
|
||||||
|
this.error = null
|
||||||
|
this.time = 0
|
||||||
|
this.layout = inquiry.layout || {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
}
|
||||||
|
this.maximize = inquiry.maximize
|
||||||
|
|
||||||
|
this.isSaved = !!inquiry.id
|
||||||
|
this.state = state
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute () {
|
||||||
|
this.isGettingResults = true
|
||||||
|
this.result = null
|
||||||
|
this.error = null
|
||||||
|
const db = this.state.db
|
||||||
|
try {
|
||||||
|
const start = new Date()
|
||||||
|
this.result = await db.execute(this.query + ';')
|
||||||
|
this.time = time.getPeriod(start, new Date())
|
||||||
|
|
||||||
|
if (this.result && this.result.values) {
|
||||||
|
events.send('resultset.create',
|
||||||
|
this.result.values[this.result.columns[0]].length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
events.send('query.run', parseFloat(this.time), { status: 'success' })
|
||||||
|
} catch (err) {
|
||||||
|
this.error = {
|
||||||
|
type: 'error',
|
||||||
|
message: err
|
||||||
|
}
|
||||||
|
|
||||||
|
events.send('query.run', 0, { status: 'error' })
|
||||||
|
}
|
||||||
|
db.refreshSchema()
|
||||||
|
this.isGettingResults = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,11 @@ import Lib from 'plotly.js/src/lib'
|
|||||||
import dataUrlToBlob from 'dataurl-to-blob'
|
import dataUrlToBlob from 'dataurl-to-blob'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async copyCsv (str) {
|
async copyText (str, notifyMessage) {
|
||||||
await navigator.clipboard.writeText(str)
|
await navigator.clipboard.writeText(str)
|
||||||
Lib.notifier('CSV copied to clipboard successfully', 'long')
|
if (notifyMessage) {
|
||||||
|
Lib.notifier(notifyMessage, 'long')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async copyImage (source) {
|
async copyImage (source) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Workspace from '@/views/Main/Workspace'
|
|||||||
import Inquiries from '@/views/Main/Inquiries'
|
import Inquiries from '@/views/Main/Inquiries'
|
||||||
import Welcome from '@/views/Welcome'
|
import Welcome from '@/views/Welcome'
|
||||||
import Main from '@/views/Main'
|
import Main from '@/views/Main'
|
||||||
|
import LoadView from '@/views/LoadView'
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
import database from '@/lib/database'
|
import database from '@/lib/database'
|
||||||
|
|
||||||
@@ -31,6 +32,11 @@ const routes = [
|
|||||||
component: Inquiries
|
component: Inquiries
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/load',
|
||||||
|
name: 'Load',
|
||||||
|
component: LoadView
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -39,7 +45,7 @@ const router = new VueRouter({
|
|||||||
})
|
})
|
||||||
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
if (!store.state.db) {
|
if (!store.state.db && to.name !== 'Load') {
|
||||||
const newDb = database.getNewDatabase()
|
const newDb = database.getNewDatabase()
|
||||||
await newDb.loadDb()
|
await newDb.loadDb()
|
||||||
store.commit('setDb', newDb)
|
store.commit('setDb', newDb)
|
||||||
|
|||||||
@@ -1,32 +1,17 @@
|
|||||||
import { nanoid } from 'nanoid'
|
import Tab from '@/lib/tab'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async addTab ({ state }, data) {
|
async addTab ({ state }, inquiry = {}) {
|
||||||
const tab = data ? JSON.parse(JSON.stringify(data)) : {}
|
// add new tab only if it was not already opened
|
||||||
// If no data then create a new blank one...
|
if (!state.tabs.some(openedTab => openedTab.id === inquiry.id)) {
|
||||||
// No data.id means to create new tab, but not blank,
|
const tab = new Tab(state, JSON.parse(JSON.stringify(inquiry)))
|
||||||
// e.g. with 'select * from csv_import' inquiry after csv import
|
|
||||||
if (!data || !data.id) {
|
|
||||||
tab.id = nanoid()
|
|
||||||
tab.name = null
|
|
||||||
tab.tempName = state.untitledLastIndex
|
|
||||||
? `Untitled ${state.untitledLastIndex}`
|
|
||||||
: 'Untitled'
|
|
||||||
tab.viewType = 'chart'
|
|
||||||
tab.viewOptions = undefined
|
|
||||||
tab.isSaved = false
|
|
||||||
} else {
|
|
||||||
tab.isSaved = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// add new tab only if was not already opened
|
|
||||||
if (!state.tabs.some(openedTab => openedTab.id === tab.id)) {
|
|
||||||
state.tabs.push(tab)
|
state.tabs.push(tab)
|
||||||
if (!tab.name) {
|
if (!tab.name) {
|
||||||
state.untitledLastIndex += 1
|
state.untitledLastIndex += 1
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return tab.id
|
return tab.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return inquiry.id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
setDb (state, db) {
|
setDb (state, db) {
|
||||||
if (state.db) {
|
if (state.db) {
|
||||||
@@ -8,8 +6,8 @@ export default {
|
|||||||
state.db = db
|
state.db = db
|
||||||
},
|
},
|
||||||
|
|
||||||
updateTab (state, { index, name, id, query, viewType, viewOptions, isSaved }) {
|
updateTab (state, { tab, newValues }) {
|
||||||
const tab = state.tabs[index]
|
const { name, id, query, viewType, viewOptions, isSaved } = newValues
|
||||||
const oldId = tab.id
|
const oldId = tab.id
|
||||||
|
|
||||||
if (id && state.currentTabId === oldId) {
|
if (id && state.currentTabId === oldId) {
|
||||||
@@ -26,30 +24,33 @@ export default {
|
|||||||
// Saved inquiry is not predefined
|
// Saved inquiry is not predefined
|
||||||
delete tab.isPredefined
|
delete tab.isPredefined
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.set(state.tabs, index, tab)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteTab (state, index) {
|
deleteTab (state, tab) {
|
||||||
|
const index = state.tabs.indexOf(tab)
|
||||||
// If closing tab is the current opened
|
// If closing tab is the current opened
|
||||||
if (state.tabs[index].id === state.currentTabId) {
|
if (tab.id === state.currentTabId) {
|
||||||
if (index < state.tabs.length - 1) {
|
if (index < state.tabs.length - 1) {
|
||||||
state.currentTabId = state.tabs[index + 1].id
|
state.currentTabId = state.tabs[index + 1].id
|
||||||
} else if (index > 0) {
|
} else if (index > 0) {
|
||||||
state.currentTabId = state.tabs[index - 1].id
|
state.currentTabId = state.tabs[index - 1].id
|
||||||
} else {
|
} else {
|
||||||
state.currentTabId = null
|
state.currentTabId = null
|
||||||
state.currentTab = null
|
|
||||||
state.untitledLastIndex = 0
|
state.untitledLastIndex = 0
|
||||||
}
|
}
|
||||||
|
state.currentTab = state.currentTabId
|
||||||
|
? state.tabs.find(tab => tab.id === state.currentTabId)
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
state.tabs.splice(index, 1)
|
state.tabs.splice(index, 1)
|
||||||
},
|
},
|
||||||
setCurrentTabId (state, id) {
|
setCurrentTabId (state, id) {
|
||||||
|
try {
|
||||||
state.currentTabId = id
|
state.currentTabId = id
|
||||||
},
|
state.currentTab = state.tabs.find(tab => tab.id === id)
|
||||||
setCurrentTab (state, tab) {
|
} catch (e) {
|
||||||
state.currentTab = tab
|
console.error('Can\'t open a tab id:' + id)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
updatePredefinedInquiries (state, inquiries) {
|
updatePredefinedInquiries (state, inquiries) {
|
||||||
state.predefinedInquiries = Array.isArray(inquiries) ? inquiries : [inquiries]
|
state.predefinedInquiries = Array.isArray(inquiries) ? inquiries : [inquiries]
|
||||||
|
|||||||
200
src/views/LoadView.vue
Normal file
200
src/views/LoadView.vue
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import fu from '@/lib/utils/fileIo'
|
||||||
|
import database from '@/lib/database'
|
||||||
|
import Logs from '@/components/Logs'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LoadView',
|
||||||
|
components: {
|
||||||
|
Logs
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
newDb: null,
|
||||||
|
messages: [],
|
||||||
|
dataMsg: {},
|
||||||
|
inquiryMsg: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasErrors () {
|
||||||
|
return this.dataMsg.type === 'error' || this.inquiryMsg.type === 'error'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created () {
|
||||||
|
const {
|
||||||
|
data_url: dataUrl,
|
||||||
|
data_format: dataFormat,
|
||||||
|
inquiry_url: inquiryUrl,
|
||||||
|
inquiry_id: inquiryIds,
|
||||||
|
maximize
|
||||||
|
} = this.$route.query
|
||||||
|
|
||||||
|
events.send('share.load', null, {
|
||||||
|
has_data_url: !!dataUrl,
|
||||||
|
data_format: dataFormat,
|
||||||
|
has_inquiry_url: !!inquiryUrl,
|
||||||
|
inquiry_id_count: (inquiryIds || []).length,
|
||||||
|
maximize
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.loadData(dataUrl, dataFormat)
|
||||||
|
const inquiries = await this.loadInquiries(inquiryUrl, inquiryIds)
|
||||||
|
if (inquiries && inquiries.length > 0) {
|
||||||
|
await this.openInquiries(inquiries, maximize)
|
||||||
|
}
|
||||||
|
if (!this.hasErrors) {
|
||||||
|
this.$router.push('/workspace?hide_schema=1')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadData (dataUrl, dataFormat) {
|
||||||
|
this.newDb = database.getNewDatabase()
|
||||||
|
if (dataUrl) {
|
||||||
|
this.dataMsg = {
|
||||||
|
message: 'Preparing data...',
|
||||||
|
type: 'info'
|
||||||
|
}
|
||||||
|
this.messages.push(this.dataMsg)
|
||||||
|
|
||||||
|
// Show loading indicator after 1 second
|
||||||
|
const loadingDataIndicator = setTimeout(() => {
|
||||||
|
if (this.dataMsg.type === 'info') {
|
||||||
|
this.dataMsg.type = 'loading'
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
if (dataFormat === 'sqlite') {
|
||||||
|
await this.getSqliteDb(dataUrl)
|
||||||
|
} else {
|
||||||
|
this.dataMsg.message = 'Unknown data format'
|
||||||
|
this.dataMsg.type = 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading indicator is not needed anymore
|
||||||
|
clearTimeout(loadingDataIndicator)
|
||||||
|
} else {
|
||||||
|
await this.newDb.loadDb()
|
||||||
|
}
|
||||||
|
this.$store.commit('setDb', this.newDb)
|
||||||
|
},
|
||||||
|
async getSqliteDb (dataUrl) {
|
||||||
|
try {
|
||||||
|
const filename = new URL(dataUrl).pathname.split('/').pop()
|
||||||
|
const res = await fu.readFile(dataUrl)
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('Fetching DB failed')
|
||||||
|
}
|
||||||
|
const file = await res.blob()
|
||||||
|
file.name = filename
|
||||||
|
|
||||||
|
await this.newDb.loadDb(file)
|
||||||
|
this.dataMsg.message = 'Data is ready'
|
||||||
|
this.dataMsg.type = 'success'
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
this.dataMsg.message = error
|
||||||
|
this.dataMsg.type = 'error'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadInquiries (inquiryUrl, inquiryIds = []) {
|
||||||
|
if (!inquiryUrl) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
// Show loading indicator after 1 second
|
||||||
|
const loadingInquiriesIndicator = setTimeout(() => {
|
||||||
|
if (this.inquiryMsg.type === 'info') {
|
||||||
|
this.inquiryMsg.type = 'loading'
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
try {
|
||||||
|
this.inquiryMsg = {
|
||||||
|
message: 'Preparing inquiries...',
|
||||||
|
type: 'info'
|
||||||
|
}
|
||||||
|
this.messages.push(this.inquiryMsg)
|
||||||
|
|
||||||
|
const res = await fu.readFile(inquiryUrl)
|
||||||
|
const file = await res.json()
|
||||||
|
|
||||||
|
this.inquiryMsg.message = 'Inquiries are ready'
|
||||||
|
this.inquiryMsg.type = 'success'
|
||||||
|
|
||||||
|
return inquiryIds.length > 0
|
||||||
|
? file.inquiries.filter(inquiry => inquiryIds.includes(inquiry.id))
|
||||||
|
: file.inquiries
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
this.inquiryMsg.message = error
|
||||||
|
this.inquiryMsg.type = 'error'
|
||||||
|
}
|
||||||
|
// Loading indicator is not needed anymore
|
||||||
|
clearTimeout(loadingInquiriesIndicator)
|
||||||
|
},
|
||||||
|
async openInquiries (inquiries, maximize) {
|
||||||
|
let tabToOpen = null
|
||||||
|
const layout = maximize ? this.getLayout(maximize) : undefined
|
||||||
|
for (const inquiry of inquiries) {
|
||||||
|
const tabId = await this.$store.dispatch('addTab', {
|
||||||
|
...inquiry,
|
||||||
|
id: undefined,
|
||||||
|
layout,
|
||||||
|
maximize
|
||||||
|
})
|
||||||
|
if (!tabToOpen) {
|
||||||
|
tabToOpen = tabId
|
||||||
|
this.$store.commit('setCurrentTabId', tabToOpen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$store.state.currentTab.execute()
|
||||||
|
},
|
||||||
|
|
||||||
|
getLayout (panelToMaximize) {
|
||||||
|
if (panelToMaximize === 'dataView') {
|
||||||
|
return {
|
||||||
|
sqlEditor: 'hidden',
|
||||||
|
table: 'above',
|
||||||
|
dataView: 'bottom'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#logs {
|
||||||
|
margin: 8px auto;
|
||||||
|
max-width: 800px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#open-workspace-btn {
|
||||||
|
margin: 16px auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -338,12 +338,14 @@ export default {
|
|||||||
storedInquiries.updateStorage(this.inquiries)
|
storedInquiries.updateStorage(this.inquiries)
|
||||||
|
|
||||||
// update tab, if renamed inquiry is opened
|
// update tab, if renamed inquiry is opened
|
||||||
const tabIndex = this.findTabIndex(processedInquiry.id)
|
const tab = this.$store.state.tabs
|
||||||
if (tabIndex >= 0) {
|
.find(tab => tab.id === processedInquiry.id)
|
||||||
|
if (tab) {
|
||||||
this.$store.commit('updateTab', {
|
this.$store.commit('updateTab', {
|
||||||
index: tabIndex,
|
tab,
|
||||||
name: this.newName,
|
newValues: {
|
||||||
id: processedInquiry.id
|
name: this.newName
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// hide dialog
|
// hide dialog
|
||||||
@@ -367,9 +369,10 @@ export default {
|
|||||||
this.inquiries.splice(this.processedInquiryIndex, 1)
|
this.inquiries.splice(this.processedInquiryIndex, 1)
|
||||||
|
|
||||||
// Close deleted inquiry tab if it was opened
|
// Close deleted inquiry tab if it was opened
|
||||||
const tabIndex = this.findTabIndex(this.processedInquiryId)
|
const tab = this.$store.state.tabs
|
||||||
if (tabIndex >= 0) {
|
.find(tab => tab.id === this.processedInquiryId)
|
||||||
this.$store.commit('deleteTab', tabIndex)
|
if (tab) {
|
||||||
|
this.$store.commit('deleteTab', tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear checkbox
|
// Clear checkbox
|
||||||
@@ -383,10 +386,12 @@ export default {
|
|||||||
|
|
||||||
// Close deleted inquiries if it was opened
|
// Close deleted inquiries if it was opened
|
||||||
const tabs = this.$store.state.tabs
|
const tabs = this.$store.state.tabs
|
||||||
for (let i = tabs.length - 1; i >= 0; i--) {
|
let i = tabs.length - 1
|
||||||
|
while (i > -1) {
|
||||||
if (this.selectedInquiriesIds.has(tabs[i].id)) {
|
if (this.selectedInquiriesIds.has(tabs[i].id)) {
|
||||||
this.$store.commit('deleteTab', i)
|
this.$store.commit('deleteTab', tabs[i])
|
||||||
}
|
}
|
||||||
|
i--
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear checkboxes
|
// Clear checkboxes
|
||||||
@@ -395,9 +400,6 @@ export default {
|
|||||||
this.selectedInquiriesCount = this.selectedInquiriesIds.size
|
this.selectedInquiriesCount = this.selectedInquiriesIds.size
|
||||||
storedInquiries.updateStorage(this.inquiries)
|
storedInquiries.updateStorage(this.inquiries)
|
||||||
},
|
},
|
||||||
findTabIndex (id) {
|
|
||||||
return this.$store.state.tabs.findIndex(tab => tab.id === id)
|
|
||||||
},
|
|
||||||
exportToFile (inquiryList, fileName) {
|
exportToFile (inquiryList, fileName) {
|
||||||
storedInquiries.export(inquiryList, fileName)
|
storedInquiries.export(inquiryList, fileName)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -80,19 +80,10 @@ export default {
|
|||||||
return this.$store.state.currentTab
|
return this.$store.state.currentTab
|
||||||
},
|
},
|
||||||
isSaved () {
|
isSaved () {
|
||||||
if (!this.currentInquiry) {
|
return this.currentInquiry && this.currentInquiry.isSaved
|
||||||
return false
|
|
||||||
}
|
|
||||||
const tabIndex = this.currentInquiry.tabIndex
|
|
||||||
const tab = this.$store.state.tabs[tabIndex]
|
|
||||||
return tab && tab.isSaved
|
|
||||||
},
|
},
|
||||||
isPredefined () {
|
isPredefined () {
|
||||||
if (this.currentInquiry) {
|
return this.currentInquiry && this.currentInquiry.isPredefined
|
||||||
return this.currentInquiry.isPredefined
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
runDisabled () {
|
runDisabled () {
|
||||||
return this.currentInquiry && (!this.$store.state.db || !this.currentInquiry.query)
|
return this.currentInquiry && (!this.$store.state.db || !this.currentInquiry.query)
|
||||||
@@ -145,13 +136,15 @@ export default {
|
|||||||
|
|
||||||
// Update tab in store
|
// Update tab in store
|
||||||
this.$store.commit('updateTab', {
|
this.$store.commit('updateTab', {
|
||||||
index: this.currentInquiry.tabIndex,
|
tab: this.currentInquiry,
|
||||||
|
newValues: {
|
||||||
name: value.name,
|
name: value.name,
|
||||||
id: value.id,
|
id: value.id,
|
||||||
query: value.query,
|
query: value.query,
|
||||||
viewType: value.viewType,
|
viewType: value.viewType,
|
||||||
viewOptions: value.viewOptions,
|
viewOptions: value.viewOptions,
|
||||||
isSaved: true
|
isSaved: true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Restore data:
|
// Restore data:
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export default {
|
|||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
this.$emit('update:importToSvgEnabled', true)
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||||
|
|||||||
@@ -41,7 +41,6 @@
|
|||||||
>
|
>
|
||||||
<png-icon />
|
<png-icon />
|
||||||
</icon-button>
|
</icon-button>
|
||||||
|
|
||||||
<icon-button
|
<icon-button
|
||||||
:disabled="!importToSvgEnabled"
|
:disabled="!importToSvgEnabled"
|
||||||
tooltip="Save as SVG"
|
tooltip="Save as SVG"
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<div class="record-navigator">
|
||||||
|
<icon-button
|
||||||
|
:disabled="value === 0"
|
||||||
|
tooltip="First row"
|
||||||
|
tooltip-position="top-left"
|
||||||
|
class="first"
|
||||||
|
@click="$emit('input', 0)"
|
||||||
|
>
|
||||||
|
<edge-arrow-icon :disabled="false" />
|
||||||
|
</icon-button>
|
||||||
|
<icon-button
|
||||||
|
:disabled="value === 0"
|
||||||
|
tooltip="Previous row"
|
||||||
|
tooltip-position="top-left"
|
||||||
|
class="prev"
|
||||||
|
@click="$emit('input', value - 1)"
|
||||||
|
>
|
||||||
|
<arrow-icon :disabled="false" />
|
||||||
|
</icon-button>
|
||||||
|
<icon-button
|
||||||
|
:disabled="value === total - 1"
|
||||||
|
tooltip="Next row"
|
||||||
|
tooltip-position="top-left"
|
||||||
|
class="next"
|
||||||
|
@click="$emit('input', value + 1)"
|
||||||
|
>
|
||||||
|
<arrow-icon :disabled="false" />
|
||||||
|
</icon-button>
|
||||||
|
<icon-button
|
||||||
|
:disabled="value === total - 1"
|
||||||
|
tooltip="Last row"
|
||||||
|
tooltip-position="top-left"
|
||||||
|
class="last"
|
||||||
|
@click="$emit('input', total - 1)"
|
||||||
|
>
|
||||||
|
<edge-arrow-icon :disabled="false" />
|
||||||
|
</icon-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import IconButton from '@/components/IconButton'
|
||||||
|
import ArrowIcon from '@/components/svg/arrow'
|
||||||
|
import EdgeArrowIcon from '@/components/svg/edgeArrow'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
IconButton,
|
||||||
|
ArrowIcon,
|
||||||
|
EdgeArrowIcon
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: Number,
|
||||||
|
total: Number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.record-navigator {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-navigator .next,
|
||||||
|
.record-navigator .last {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
221
src/views/Main/Workspace/Tabs/Tab/RunResult/Record/index.vue
Normal file
221
src/views/Main/Workspace/Tabs/Tab/RunResult/Record/index.vue
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
<template>
|
||||||
|
<div class="record-view">
|
||||||
|
<div class="table-container">
|
||||||
|
<table
|
||||||
|
ref="table"
|
||||||
|
class="sqliteviz-table"
|
||||||
|
tabindex="0"
|
||||||
|
@keydown="onTableKeydown"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th/>
|
||||||
|
<th>
|
||||||
|
<div class="cell-data">
|
||||||
|
Row #{{ currentRowIndex + 1 }}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(col, index) in columns" :key="index">
|
||||||
|
<th class="column-cell">{{ col }}</th>
|
||||||
|
<td
|
||||||
|
:data-col="index"
|
||||||
|
:data-row="currentRowIndex"
|
||||||
|
:data-isNull="isNull(getCellValue(col))"
|
||||||
|
:data-isBlob="isBlob(getCellValue(col))"
|
||||||
|
:key="index"
|
||||||
|
:aria-selected="false"
|
||||||
|
@click="onCellClick"
|
||||||
|
>
|
||||||
|
<div class="cell-data">
|
||||||
|
{{ getCellText(col) }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="table-footer">
|
||||||
|
<div class="table-footer-count">
|
||||||
|
{{ rowCount }} {{rowCount === 1 ? 'row' : 'rows'}} retrieved
|
||||||
|
<span v-if="time">in {{ time }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<row-navigator v-model="currentRowIndex" :total="rowCount"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import RowNavigator from './RowNavigator.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { RowNavigator },
|
||||||
|
props: {
|
||||||
|
dataSet: Object,
|
||||||
|
time: String,
|
||||||
|
rowIndex: { type: Number, default: 0 },
|
||||||
|
selectedColumnIndex: Number
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
selectedCellElement: null,
|
||||||
|
currentRowIndex: this.rowIndex
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
columns () {
|
||||||
|
return this.dataSet.columns
|
||||||
|
},
|
||||||
|
rowCount () {
|
||||||
|
return this.dataSet.values[this.columns[0]].length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
const col = this.selectedColumnIndex
|
||||||
|
const row = this.currentRowIndex
|
||||||
|
const cell = this.$refs.table
|
||||||
|
.querySelector(`td[data-col="${col}"][data-row="${row}"]`)
|
||||||
|
if (cell) {
|
||||||
|
this.selectCell(cell)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
async currentRowIndex () {
|
||||||
|
await this.$nextTick()
|
||||||
|
if (this.selectedCellElement) {
|
||||||
|
const previouslySelected = this.selectedCellElement
|
||||||
|
this.selectCell(null)
|
||||||
|
this.selectCell(previouslySelected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isBlob (value) {
|
||||||
|
return value && ArrayBuffer.isView(value)
|
||||||
|
},
|
||||||
|
isNull (value) {
|
||||||
|
return value === null
|
||||||
|
},
|
||||||
|
getCellValue (col) {
|
||||||
|
return this.dataSet.values[col][this.currentRowIndex]
|
||||||
|
},
|
||||||
|
getCellText (col) {
|
||||||
|
const value = this.getCellValue(col)
|
||||||
|
if (this.isNull(value)) {
|
||||||
|
return 'NULL'
|
||||||
|
}
|
||||||
|
if (this.isBlob(value)) {
|
||||||
|
return 'BLOB'
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
onTableKeydown (e) {
|
||||||
|
const keyCodeMap = {
|
||||||
|
38: 'up',
|
||||||
|
40: 'down'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.selectedCellElement ||
|
||||||
|
!Object.keys(keyCodeMap).includes(e.keyCode.toString())
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.moveFocusInTable(this.selectedCellElement, keyCodeMap[e.keyCode])
|
||||||
|
},
|
||||||
|
onCellClick (e) {
|
||||||
|
this.selectCell(e.target.closest('td'), false)
|
||||||
|
},
|
||||||
|
selectCell (cell, scrollTo = true) {
|
||||||
|
if (!cell) {
|
||||||
|
if (this.selectedCellElement) {
|
||||||
|
this.selectedCellElement.ariaSelected = 'false'
|
||||||
|
}
|
||||||
|
this.selectedCellElement = cell
|
||||||
|
} else if (!cell.ariaSelected || cell.ariaSelected === 'false') {
|
||||||
|
if (this.selectedCellElement) {
|
||||||
|
this.selectedCellElement.ariaSelected = 'false'
|
||||||
|
}
|
||||||
|
cell.ariaSelected = 'true'
|
||||||
|
this.selectedCellElement = cell
|
||||||
|
} else {
|
||||||
|
cell.ariaSelected = 'false'
|
||||||
|
this.selectedCellElement = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedCellElement && scrollTo) {
|
||||||
|
this.selectedCellElement.scrollIntoView()
|
||||||
|
this.selectedCellElement.closest('.table-container').scrollTo({ left: 0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('updateSelectedCell', this.selectedCellElement)
|
||||||
|
},
|
||||||
|
moveFocusInTable (initialCell, direction) {
|
||||||
|
const currentColIndex = +initialCell.dataset.col
|
||||||
|
const newColIndex = direction === 'up'
|
||||||
|
? currentColIndex - 1
|
||||||
|
: currentColIndex + 1
|
||||||
|
|
||||||
|
const newCell = this.$refs.table
|
||||||
|
.querySelector(`td[data-col="${newColIndex}"][data-row="${this.currentRowIndex}"]`)
|
||||||
|
if (newCell) {
|
||||||
|
this.selectCell(newCell)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
table.sqliteviz-table:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.sqliteviz-table tbody td:hover {
|
||||||
|
background-color: var(--color-bg-light-3);
|
||||||
|
}
|
||||||
|
.sqliteviz-table tbody td[aria-selected="true"] {
|
||||||
|
box-shadow: inset 0 0 0 1px var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
table.sqliteviz-table {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.sqliteviz-table thead tr th {
|
||||||
|
border-bottom: 1px solid var(--color-border-light);
|
||||||
|
}
|
||||||
|
.sqliteviz-table tbody tr th {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: var(--color-bg-dark);
|
||||||
|
color: var(--color-text-light);
|
||||||
|
border-bottom: 1px solid var(--color-border-light);
|
||||||
|
border-right: 1px solid var(--color-border-light);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-footer {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.record-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-cell {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
207
src/views/Main/Workspace/Tabs/Tab/RunResult/ValueViewer.vue
Normal file
207
src/views/Main/Workspace/Tabs/Tab/RunResult/ValueViewer.vue
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
<template>
|
||||||
|
<div class="value-viewer">
|
||||||
|
<div class="value-viewer-toolbar">
|
||||||
|
<button
|
||||||
|
v-for="format in formats"
|
||||||
|
:key="format.value"
|
||||||
|
type="button"
|
||||||
|
:aria-selected="currentFormat === format.value"
|
||||||
|
:class="format.value"
|
||||||
|
@click="currentFormat = format.value"
|
||||||
|
>
|
||||||
|
{{ format.text }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="copy"
|
||||||
|
@click="copyToClipboard"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="value-body">
|
||||||
|
<codemirror
|
||||||
|
v-if="currentFormat === 'json' && formattedJson"
|
||||||
|
:value="formattedJson"
|
||||||
|
:options="cmOptions"
|
||||||
|
class="json-value"
|
||||||
|
/>
|
||||||
|
<pre
|
||||||
|
v-if="currentFormat === 'text'"
|
||||||
|
:class="['text-value', { 'meta-value': isNull || isBlob }]"
|
||||||
|
>{{ cellText }}</pre>
|
||||||
|
<logs
|
||||||
|
v-if="messages && messages.length > 0"
|
||||||
|
:messages="messages"
|
||||||
|
class="messages"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { codemirror } from 'vue-codemirror'
|
||||||
|
import 'codemirror/lib/codemirror.css'
|
||||||
|
import 'codemirror/mode/javascript/javascript.js'
|
||||||
|
import 'codemirror/addon/fold/foldcode.js'
|
||||||
|
import 'codemirror/addon/fold/foldgutter.js'
|
||||||
|
import 'codemirror/addon/fold/foldgutter.css'
|
||||||
|
import 'codemirror/addon/fold/brace-fold.js'
|
||||||
|
import 'codemirror/theme/neo.css'
|
||||||
|
import cIo from '@/lib/utils/clipboardIo'
|
||||||
|
import Logs from '@/components/Logs'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
codemirror,
|
||||||
|
Logs
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
cellValue: [String, Number, Uint8Array]
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
formats: [
|
||||||
|
{ text: 'Text', value: 'text' },
|
||||||
|
{ text: 'JSON', value: 'json' }
|
||||||
|
],
|
||||||
|
currentFormat: 'text',
|
||||||
|
cmOptions: {
|
||||||
|
tabSize: 4,
|
||||||
|
mode: { name: 'javascript', json: true },
|
||||||
|
theme: 'neo',
|
||||||
|
lineNumbers: true,
|
||||||
|
line: true,
|
||||||
|
foldGutter: true,
|
||||||
|
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
formattedJson: '',
|
||||||
|
messages: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isBlob () {
|
||||||
|
return this.cellValue && ArrayBuffer.isView(this.cellValue)
|
||||||
|
},
|
||||||
|
isNull () {
|
||||||
|
return this.cellValue === null
|
||||||
|
},
|
||||||
|
cellText () {
|
||||||
|
const value = this.cellValue
|
||||||
|
if (this.isNull) {
|
||||||
|
return 'NULL'
|
||||||
|
}
|
||||||
|
if (this.isBlob) {
|
||||||
|
return 'BLOB'
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
currentFormat () {
|
||||||
|
this.messages = []
|
||||||
|
this.formattedJson = ''
|
||||||
|
if (this.currentFormat === 'json') {
|
||||||
|
this.formatJson(this.cellValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cellValue () {
|
||||||
|
this.messages = []
|
||||||
|
if (this.currentFormat === 'json') {
|
||||||
|
this.formatJson(this.cellValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatJson (jsonStr) {
|
||||||
|
try {
|
||||||
|
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.'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
copyToClipboard () {
|
||||||
|
cIo.copyText(this.currentFormat === 'json'
|
||||||
|
? this.formattedJson
|
||||||
|
: this.cellValue,
|
||||||
|
'The value is copied to clipboard.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.value-viewer {
|
||||||
|
background-color: var(--color-white);
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.value-viewer-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
.value-body {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.text-value {
|
||||||
|
padding: 0 8px;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-value {
|
||||||
|
margin-top: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-value.meta-value {
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--color-text-light-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages {
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-viewer-toolbar button {
|
||||||
|
font-size: 10px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text-base);
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-viewer-toolbar button:hover {
|
||||||
|
background-color: var(--color-bg-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-viewer-toolbar button[aria-selected="true"] {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
>>> .vue-codemirror {
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
>>> .CodeMirror {
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
>>> .CodeMirror-cursor {
|
||||||
|
width: 1px;
|
||||||
|
background: var(--color-text-base);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,31 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="run-result-panel" ref="runResultPanel">
|
<div class="run-result-panel" ref="runResultPanel">
|
||||||
<div class="run-result-panel-content">
|
<component
|
||||||
<div
|
:is="viewValuePanelVisible ? 'splitpanes':'div'"
|
||||||
v-show="result === null && !isGettingResults && !error"
|
:before="{ size: 50, max: 100 }"
|
||||||
class="table-preview result-before"
|
:after="{ size: 50, max: 100 }"
|
||||||
|
:default="{ before: 50, after: 50 }"
|
||||||
|
class="run-result-panel-content"
|
||||||
>
|
>
|
||||||
Run your query and get results here
|
<template #left-pane>
|
||||||
</div>
|
<div :id="'run-result-left-pane-'+tab.id" class="result-set-container"/>
|
||||||
<div v-if="isGettingResults" class="table-preview result-in-progress">
|
</template>
|
||||||
<loading-indicator :size="30"/>
|
<div :id="'run-result-result-set-'+tab.id" class="result-set-container"/>
|
||||||
Fetching results...
|
<template #right-pane v-if="viewValuePanelVisible">
|
||||||
</div>
|
<div class="value-viewer-container">
|
||||||
<div
|
<value-viewer
|
||||||
v-show="result === undefined && !isGettingResults && !error"
|
v-show="selectedCell"
|
||||||
class="table-preview result-empty"
|
:cellValue="selectedCell
|
||||||
>
|
? result.values[result.columns[selectedCell.dataset.col]][selectedCell.dataset.row]
|
||||||
No rows retrieved according to your query
|
: ''"
|
||||||
</div>
|
|
||||||
<logs v-if="error" :messages="[error]"/>
|
|
||||||
<sql-table
|
|
||||||
v-if="result"
|
|
||||||
:data-set="result"
|
|
||||||
:time="time"
|
|
||||||
:pageSize="pageSize"
|
|
||||||
class="straight"
|
|
||||||
/>
|
/>
|
||||||
|
<div v-show="!selectedCell" class="table-preview">
|
||||||
|
No cell selected to view
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</component>
|
||||||
|
|
||||||
<side-tool-bar @switchTo="$emit('switchTo', $event)" panel="table">
|
<side-tool-bar @switchTo="$emit('switchTo', $event)" panel="table">
|
||||||
<icon-button
|
<icon-button
|
||||||
:disabled="!result"
|
:disabled="!result"
|
||||||
@@ -44,6 +44,26 @@
|
|||||||
>
|
>
|
||||||
<clipboard-icon/>
|
<clipboard-icon/>
|
||||||
</icon-button>
|
</icon-button>
|
||||||
|
|
||||||
|
<icon-button
|
||||||
|
:disabled="!result"
|
||||||
|
tooltip="View record"
|
||||||
|
tooltip-position="top-left"
|
||||||
|
:active="viewRecord"
|
||||||
|
@click="toggleViewRecord"
|
||||||
|
>
|
||||||
|
<row-icon/>
|
||||||
|
</icon-button>
|
||||||
|
|
||||||
|
<icon-button
|
||||||
|
:disabled="!result"
|
||||||
|
tooltip="View value"
|
||||||
|
tooltip-position="top-left"
|
||||||
|
:active="viewValuePanelVisible"
|
||||||
|
@click="toggleViewValuePanel"
|
||||||
|
>
|
||||||
|
<view-cell-value-icon/>
|
||||||
|
</icon-button>
|
||||||
</side-tool-bar>
|
</side-tool-bar>
|
||||||
|
|
||||||
<loading-dialog
|
<loading-dialog
|
||||||
@@ -56,6 +76,48 @@
|
|||||||
@action="copyToClipboard"
|
@action="copyToClipboard"
|
||||||
@cancel="cancelCopy"
|
@cancel="cancelCopy"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<teleport :to="resultSetTeleportTarget">
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -63,9 +125,12 @@
|
|||||||
import Logs from '@/components/Logs'
|
import Logs from '@/components/Logs'
|
||||||
import SqlTable from '@/components/SqlTable'
|
import SqlTable from '@/components/SqlTable'
|
||||||
import LoadingIndicator from '@/components/LoadingIndicator'
|
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||||
import SideToolBar from './SideToolBar'
|
import SideToolBar from '../SideToolBar'
|
||||||
|
import Splitpanes from '@/components/Splitpanes'
|
||||||
import ExportToCsvIcon from '@/components/svg/exportToCsv'
|
import ExportToCsvIcon from '@/components/svg/exportToCsv'
|
||||||
import ClipboardIcon from '@/components/svg/clipboard'
|
import ClipboardIcon from '@/components/svg/clipboard'
|
||||||
|
import ViewCellValueIcon from '@/components/svg/viewCellValue'
|
||||||
|
import RowIcon from '@/components/svg/row'
|
||||||
import IconButton from '@/components/IconButton'
|
import IconButton from '@/components/IconButton'
|
||||||
import csv from '@/lib/csv'
|
import csv from '@/lib/csv'
|
||||||
import fIo from '@/lib/utils/fileIo'
|
import fIo from '@/lib/utils/fileIo'
|
||||||
@@ -73,16 +138,30 @@ import cIo from '@/lib/utils/clipboardIo'
|
|||||||
import time from '@/lib/utils/time'
|
import time from '@/lib/utils/time'
|
||||||
import loadingDialog from '@/components/LoadingDialog'
|
import loadingDialog from '@/components/LoadingDialog'
|
||||||
import events from '@/lib/utils/events'
|
import events from '@/lib/utils/events'
|
||||||
|
import Teleport from 'vue2-teleport'
|
||||||
|
import ValueViewer from './ValueViewer'
|
||||||
|
import Record from './Record/index.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RunResult',
|
name: 'RunResult',
|
||||||
props: ['result', 'isGettingResults', 'error', 'time'],
|
props: {
|
||||||
|
tab: Object,
|
||||||
|
result: Object,
|
||||||
|
isGettingResults: Boolean,
|
||||||
|
error: Object,
|
||||||
|
time: [String, Number]
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
resizeObserver: null,
|
resizeObserver: null,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
preparingCopy: false,
|
preparingCopy: false,
|
||||||
dataToCopy: null
|
dataToCopy: null,
|
||||||
|
viewValuePanelVisible: false,
|
||||||
|
selectedCell: null,
|
||||||
|
viewRecord: false,
|
||||||
|
defaultPage: 1,
|
||||||
|
defaultSelectedCell: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
@@ -93,7 +172,23 @@ export default {
|
|||||||
ExportToCsvIcon,
|
ExportToCsvIcon,
|
||||||
IconButton,
|
IconButton,
|
||||||
ClipboardIcon,
|
ClipboardIcon,
|
||||||
loadingDialog
|
ViewCellValueIcon,
|
||||||
|
RowIcon,
|
||||||
|
loadingDialog,
|
||||||
|
ValueViewer,
|
||||||
|
Record,
|
||||||
|
Splitpanes,
|
||||||
|
Teleport
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
resultSetTeleportTarget () {
|
||||||
|
const base = `#${this.viewValuePanelVisible
|
||||||
|
? 'run-result-left-pane'
|
||||||
|
: 'run-result-result-set'
|
||||||
|
}`
|
||||||
|
const tabIdPostfix = `-${this.tab.id}`
|
||||||
|
return base + tabIdPostfix
|
||||||
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||||
@@ -103,6 +198,12 @@ export default {
|
|||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
this.resizeObserver.unobserve(this.$refs.runResultPanel)
|
this.resizeObserver.unobserve(this.$refs.runResultPanel)
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
result () {
|
||||||
|
this.defaultSelectedCell = null
|
||||||
|
this.selectedCell = null
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleResize () {
|
handleResize () {
|
||||||
this.calculatePageSize()
|
this.calculatePageSize()
|
||||||
@@ -160,13 +261,35 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
copyToClipboard () {
|
copyToClipboard () {
|
||||||
cIo.copyCsv(this.dataToCopy)
|
cIo.copyText(this.dataToCopy, 'CSV copied to clipboard successfully')
|
||||||
this.$modal.hide('prepareCSVCopy')
|
this.$modal.hide('prepareCSVCopy')
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelCopy () {
|
cancelCopy () {
|
||||||
this.dataToCopy = null
|
this.dataToCopy = null
|
||||||
this.$modal.hide('prepareCSVCopy')
|
this.$modal.hide('prepareCSVCopy')
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleViewValuePanel () {
|
||||||
|
this.viewValuePanelVisible = !this.viewValuePanelVisible
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleViewRecord () {
|
||||||
|
if (this.viewRecord) {
|
||||||
|
this.defaultSelectedCell = {
|
||||||
|
row: this.$refs.recordView.currentRowIndex,
|
||||||
|
col: this.selectedCell ? +this.selectedCell.dataset.col : 0
|
||||||
|
}
|
||||||
|
this.defaultPage = Math.ceil(
|
||||||
|
(this.$refs.recordView.currentRowIndex + 1) / this.pageSize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.viewRecord = !this.viewRecord
|
||||||
|
},
|
||||||
|
|
||||||
|
onUpdateSelectedCell (e) {
|
||||||
|
this.selectedCell = e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,12 +303,24 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.run-result-panel-content {
|
.run-result-panel-content {
|
||||||
position: relative;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 0;
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-set-container,
|
||||||
|
.result-set-container > div {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
.value-viewer-container {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--color-white);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.table-preview {
|
.table-preview {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -194,6 +329,7 @@ export default {
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-in-progress {
|
.result-in-progress {
|
||||||
@@ -3,44 +3,46 @@
|
|||||||
<splitpanes
|
<splitpanes
|
||||||
class="query-results-splitter"
|
class="query-results-splitter"
|
||||||
horizontal
|
horizontal
|
||||||
:before="{ size: 50, max: 100 }"
|
:before="{ size: topPaneSize, max: 100 }"
|
||||||
:after="{ size: 50, max: 100 }"
|
:after="{ size: 100 - topPaneSize, max: 100 }"
|
||||||
|
:default="{ before: 50, after: 50 }"
|
||||||
>
|
>
|
||||||
<template #left-pane>
|
<template #left-pane>
|
||||||
<div :id="'above-' + tabIndex" class="above" />
|
<div :id="'above-' + tab.id" class="above" />
|
||||||
</template>
|
</template>
|
||||||
<template #right-pane>
|
<template #right-pane>
|
||||||
<div :id="'bottom-'+ tabIndex" ref="bottomPane" class="bottomPane" />
|
<div :id="'bottom-'+ tab.id" ref="bottomPane" class="bottomPane" />
|
||||||
</template>
|
</template>
|
||||||
</splitpanes>
|
</splitpanes>
|
||||||
|
|
||||||
<div :id="'hidden-'+ tabIndex" class="hidden-part" />
|
<div :id="'hidden-'+ tab.id" class="hidden-part" />
|
||||||
|
|
||||||
<teleport :to="`#${layout.sqlEditor}-${tabIndex}`">
|
<teleport :to="`#${tab.layout.sqlEditor}-${tab.id}`">
|
||||||
<sql-editor
|
<sql-editor
|
||||||
ref="sqlEditor"
|
ref="sqlEditor"
|
||||||
v-model="query"
|
v-model="tab.query"
|
||||||
:is-getting-results="isGettingResults"
|
:is-getting-results="tab.isGettingResults"
|
||||||
@switchTo="onSwitchView('sqlEditor', $event)"
|
@switchTo="onSwitchView('sqlEditor', $event)"
|
||||||
@run="execute"
|
@run="tab.execute()"
|
||||||
/>
|
/>
|
||||||
</teleport>
|
</teleport>
|
||||||
|
|
||||||
<teleport :to="`#${layout.table}-${tabIndex}`">
|
<teleport :to="`#${tab.layout.table}-${tab.id}`">
|
||||||
<run-result
|
<run-result
|
||||||
:result="result"
|
:tab="tab"
|
||||||
:is-getting-results="isGettingResults"
|
:result="tab.result"
|
||||||
:error="error"
|
:is-getting-results="tab.isGettingResults"
|
||||||
:time="time"
|
:error="tab.error"
|
||||||
|
:time="tab.time"
|
||||||
@switchTo="onSwitchView('table', $event)"
|
@switchTo="onSwitchView('table', $event)"
|
||||||
/>
|
/>
|
||||||
</teleport>
|
</teleport>
|
||||||
|
|
||||||
<teleport :to="`#${layout.dataView}-${tabIndex}`">
|
<teleport :to="`#${tab.layout.dataView}-${tab.id}`">
|
||||||
<data-view
|
<data-view
|
||||||
:data-source="(result && result.values) || null"
|
:data-source="(tab.result && tab.result.values) || null"
|
||||||
:init-options="initViewOptions"
|
:init-options="tab.viewOptions"
|
||||||
:init-mode="initViewType"
|
:init-mode="tab.viewType"
|
||||||
ref="dataView"
|
ref="dataView"
|
||||||
@switchTo="onSwitchView('dataView', $event)"
|
@switchTo="onSwitchView('dataView', $event)"
|
||||||
@update="onDataViewUpdate"
|
@update="onDataViewUpdate"
|
||||||
@@ -54,15 +56,15 @@ import Splitpanes from '@/components/Splitpanes'
|
|||||||
import SqlEditor from './SqlEditor'
|
import SqlEditor from './SqlEditor'
|
||||||
import DataView from './DataView'
|
import DataView from './DataView'
|
||||||
import RunResult from './RunResult'
|
import RunResult from './RunResult'
|
||||||
import time from '@/lib/utils/time'
|
|
||||||
import Teleport from 'vue2-teleport'
|
import Teleport from 'vue2-teleport'
|
||||||
import events from '@/lib/utils/events'
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Tab',
|
name: 'Tab',
|
||||||
props: [
|
props: {
|
||||||
'id', 'initName', 'initQuery', 'initViewOptions', 'tabIndex', 'isPredefined', 'initViewType'
|
tab: Object
|
||||||
],
|
},
|
||||||
components: {
|
components: {
|
||||||
SqlEditor,
|
SqlEditor,
|
||||||
DataView,
|
DataView,
|
||||||
@@ -72,21 +74,14 @@ export default {
|
|||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
query: this.initQuery,
|
topPaneSize: this.tab.maximize
|
||||||
result: null,
|
? this.tab.layout[this.tab.maximize] === 'above' ? 100 : 0
|
||||||
isGettingResults: false,
|
: 50
|
||||||
error: null,
|
|
||||||
time: 0,
|
|
||||||
layout: {
|
|
||||||
sqlEditor: 'above',
|
|
||||||
table: 'bottom',
|
|
||||||
dataView: 'hidden'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isActive () {
|
isActive () {
|
||||||
return this.id === this.$store.state.currentTabId
|
return this.tab.id === this.$store.state.currentTabId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -94,54 +89,34 @@ export default {
|
|||||||
immediate: true,
|
immediate: true,
|
||||||
async handler () {
|
async handler () {
|
||||||
if (this.isActive) {
|
if (this.isActive) {
|
||||||
this.$store.commit('setCurrentTab', this)
|
|
||||||
await this.$nextTick()
|
await this.$nextTick()
|
||||||
this.$refs.sqlEditor.focus()
|
this.$refs.sqlEditor.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
query () {
|
'tab.query' () {
|
||||||
this.$store.commit('updateTab', { index: this.tabIndex, isSaved: false })
|
this.$store.commit('updateTab', {
|
||||||
|
tab: this.tab,
|
||||||
|
newValues: { isSaved: false }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted () {
|
||||||
|
this.tab.dataView = this.$refs.dataView
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onSwitchView (from, to) {
|
onSwitchView (from, to) {
|
||||||
const fromPosition = this.layout[from]
|
const fromPosition = this.tab.layout[from]
|
||||||
this.layout[from] = this.layout[to]
|
this.tab.layout[from] = this.tab.layout[to]
|
||||||
this.layout[to] = fromPosition
|
this.tab.layout[to] = fromPosition
|
||||||
|
|
||||||
events.send('inquiry.panel', null, { panel: to })
|
events.send('inquiry.panel', null, { panel: to })
|
||||||
},
|
},
|
||||||
onDataViewUpdate () {
|
onDataViewUpdate () {
|
||||||
this.$store.commit('updateTab', { index: this.tabIndex, isSaved: false })
|
this.$store.commit('updateTab', {
|
||||||
},
|
tab: this.tab,
|
||||||
async execute () {
|
newValues: { isSaved: false }
|
||||||
this.isGettingResults = true
|
})
|
||||||
this.result = null
|
|
||||||
this.error = null
|
|
||||||
const state = this.$store.state
|
|
||||||
try {
|
|
||||||
const start = new Date()
|
|
||||||
this.result = await state.db.execute(this.query + ';')
|
|
||||||
this.time = time.getPeriod(start, new Date())
|
|
||||||
|
|
||||||
if (this.result && this.result.values) {
|
|
||||||
events.send('resultset.create',
|
|
||||||
this.result.values[this.result.columns[0]].length
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
events.send('query.run', parseFloat(this.time), { status: 'success' })
|
|
||||||
} catch (err) {
|
|
||||||
this.error = {
|
|
||||||
type: 'error',
|
|
||||||
message: err
|
|
||||||
}
|
|
||||||
|
|
||||||
events.send('query.run', 0, { status: 'error' })
|
|
||||||
}
|
|
||||||
state.db.refreshSchema()
|
|
||||||
this.isGettingResults = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
v-for="(tab, index) in tabs"
|
v-for="(tab, index) in tabs"
|
||||||
:key="index"
|
:key="index"
|
||||||
@click="selectTab(tab.id)"
|
@click="selectTab(tab.id)"
|
||||||
:class="[{'tab-selected': (tab.id === selectedIndex)}, 'tab']"
|
:class="[{'tab-selected': (tab.id === selectedTabId)}, 'tab']"
|
||||||
>
|
>
|
||||||
<div class="tab-name">
|
<div class="tab-name">
|
||||||
<span v-show="!tab.isSaved" class="star">*</span>
|
<span v-show="!tab.isSaved" class="star">*</span>
|
||||||
@@ -13,20 +13,14 @@
|
|||||||
<span v-else class="tab-untitled">{{ tab.tempName }}</span>
|
<span v-else class="tab-untitled">{{ tab.tempName }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<close-icon class="close-icon" :size="10" @click="beforeCloseTab(index)"/>
|
<close-icon class="close-icon" :size="10" @click="beforeCloseTab(tab)"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<tab
|
<tab
|
||||||
v-for="(tab, index) in tabs"
|
v-for="tab in tabs"
|
||||||
:key="tab.id"
|
:key="tab.id"
|
||||||
:id="tab.id"
|
:tab="tab"
|
||||||
:init-name="tab.name"
|
|
||||||
:init-query="tab.query"
|
|
||||||
:init-view-options="tab.viewOptions"
|
|
||||||
:init-view-type="tab.viewType"
|
|
||||||
:is-predefined="tab.isPredefined"
|
|
||||||
:tab-index="index"
|
|
||||||
/>
|
/>
|
||||||
<div v-show="tabs.length === 0" id="start-guide">
|
<div v-show="tabs.length === 0" id="start-guide">
|
||||||
<span class="link" @click="$root.$emit('createNewInquiry')">Create</span>
|
<span class="link" @click="$root.$emit('createNewInquiry')">Create</span>
|
||||||
@@ -38,25 +32,25 @@
|
|||||||
<modal name="close-warn" classes="dialog" height="auto">
|
<modal name="close-warn" classes="dialog" height="auto">
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
Close tab {{
|
Close tab {{
|
||||||
closingTabIndex !== null
|
closingTab !== null
|
||||||
? (tabs[closingTabIndex].name || `[${tabs[closingTabIndex].tempName}]`)
|
? (closingTab.name || `[${closingTab.tempName}]`)
|
||||||
: ''
|
: ''
|
||||||
}}
|
}}
|
||||||
<close-icon @click="$modal.hide('close-warn')"/>
|
<close-icon @click="$modal.hide('close-warn')"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
You have unsaved changes. Save changes in {{
|
You have unsaved changes. Save changes in {{
|
||||||
closingTabIndex !== null
|
closingTab !== null
|
||||||
? (tabs[closingTabIndex].name || `[${tabs[closingTabIndex].tempName}]`)
|
? (closingTab.name || `[${closingTab.tempName}]`)
|
||||||
: ''
|
: ''
|
||||||
}} before closing?
|
}} before closing?
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-buttons-container">
|
<div class="dialog-buttons-container">
|
||||||
<button class="secondary" @click="closeTab(closingTabIndex)">
|
<button class="secondary" @click="closeTab(closingTab)">
|
||||||
Close without saving
|
Close without saving
|
||||||
</button>
|
</button>
|
||||||
<button class="secondary" @click="$modal.hide('close-warn')">Cancel</button>
|
<button class="secondary" @click="$modal.hide('close-warn')">Cancel</button>
|
||||||
<button class="primary" @click="saveAndClose(closingTabIndex)">Save and close</button>
|
<button class="primary" @click="saveAndClose(closingTab)">Save and close</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,14 +67,14 @@ export default {
|
|||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
closingTabIndex: null
|
closingTab: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
tabs () {
|
tabs () {
|
||||||
return this.$store.state.tabs
|
return this.$store.state.tabs
|
||||||
},
|
},
|
||||||
selectedIndex () {
|
selectedTabId () {
|
||||||
return this.$store.state.currentTabId
|
return this.$store.state.currentTabId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -97,25 +91,24 @@ export default {
|
|||||||
selectTab (id) {
|
selectTab (id) {
|
||||||
this.$store.commit('setCurrentTabId', id)
|
this.$store.commit('setCurrentTabId', id)
|
||||||
},
|
},
|
||||||
beforeCloseTab (index) {
|
beforeCloseTab (tab) {
|
||||||
this.closingTabIndex = index
|
this.closingTab = tab
|
||||||
if (!this.tabs[index].isSaved) {
|
if (!tab.isSaved) {
|
||||||
this.$modal.show('close-warn')
|
this.$modal.show('close-warn')
|
||||||
} else {
|
} else {
|
||||||
this.closeTab(index)
|
this.closeTab(tab)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
closeTab (index) {
|
closeTab (tab) {
|
||||||
this.$modal.hide('close-warn')
|
this.$modal.hide('close-warn')
|
||||||
this.closingTabIndex = null
|
this.$store.commit('deleteTab', tab)
|
||||||
this.$store.commit('deleteTab', index)
|
|
||||||
},
|
},
|
||||||
saveAndClose (index) {
|
saveAndClose (tab) {
|
||||||
this.$root.$on('inquirySaved', () => {
|
this.$root.$on('inquirySaved', () => {
|
||||||
this.closeTab(index)
|
this.closeTab(tab)
|
||||||
this.$root.$off('inquirySaved')
|
this.$root.$off('inquirySaved')
|
||||||
})
|
})
|
||||||
this.selectTab(this.tabs[index].id)
|
this.selectTab(tab.id)
|
||||||
this.$modal.hide('close-warn')
|
this.$modal.hide('close-warn')
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$root.$emit('saveInquiry')
|
this.$root.$emit('saveInquiry')
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<splitpanes
|
<splitpanes
|
||||||
class="schema-tabs-splitter"
|
class="schema-tabs-splitter"
|
||||||
:before="{ size: 20, max: 30 }"
|
:before="{ size: schemaWidth, max: 30 }"
|
||||||
:after="{ size: 80, max: 100 }"
|
:after="{ size: 100 - schemaWidth, max: 100 }"
|
||||||
|
:default="{ before: 20, after: 80 }"
|
||||||
>
|
>
|
||||||
<template #left-pane>
|
<template #left-pane>
|
||||||
<schema/>
|
<schema/>
|
||||||
@@ -28,9 +29,14 @@ export default {
|
|||||||
Splitpanes,
|
Splitpanes,
|
||||||
Tabs
|
Tabs
|
||||||
},
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
schemaWidth: this.$route.query.hide_schema === '1' ? 0 : 20
|
||||||
|
}
|
||||||
|
},
|
||||||
async beforeCreate () {
|
async beforeCreate () {
|
||||||
const schema = this.$store.state.db.schema
|
const schema = this.$store.state.db.schema
|
||||||
if (!schema || schema.length === 0) {
|
if ((!schema || schema.length === 0) && this.$store.state.tabs.length === 0) {
|
||||||
const stmt = [
|
const stmt = [
|
||||||
'/*',
|
'/*',
|
||||||
' * Your database is empty. In order to start building charts',
|
' * Your database is empty. In order to start building charts',
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ describe('Splitpanes.vue', () => {
|
|||||||
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.height).to.equal('40%')
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.height).to.equal('40%')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('toggles correctly', async () => {
|
it('toggles correctly - no maximized initially', async () => {
|
||||||
// mount the component
|
// mount the component
|
||||||
const wrapper = shallowMount(Splitpanes, {
|
const wrapper = shallowMount(Splitpanes, {
|
||||||
slots: {
|
slots: {
|
||||||
@@ -70,6 +70,64 @@ describe('Splitpanes.vue', () => {
|
|||||||
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('40%')
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('40%')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('toggles correctly - with maximized initially', async () => {
|
||||||
|
// mount the component
|
||||||
|
let wrapper = shallowMount(Splitpanes, {
|
||||||
|
slots: {
|
||||||
|
leftPane: '<div />',
|
||||||
|
rightPane: '<div />'
|
||||||
|
},
|
||||||
|
propsData: {
|
||||||
|
before: { size: 0, max: 100 },
|
||||||
|
after: { size: 100, max: 100 },
|
||||||
|
default: { before: 20, after: 80 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.find('.toggle-btn').trigger('click')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('20%')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('80%')
|
||||||
|
|
||||||
|
await wrapper.findAll('.toggle-btn').at(0).trigger('click')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('0%')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('100%')
|
||||||
|
|
||||||
|
await wrapper.find('.toggle-btn').trigger('click')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('20%')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('80%')
|
||||||
|
|
||||||
|
await wrapper.findAll('.toggle-btn').at(1).trigger('click')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('100%')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('0%')
|
||||||
|
|
||||||
|
wrapper = shallowMount(Splitpanes, {
|
||||||
|
slots: {
|
||||||
|
leftPane: '<div />',
|
||||||
|
rightPane: '<div />'
|
||||||
|
},
|
||||||
|
propsData: {
|
||||||
|
before: { size: 100, max: 100 },
|
||||||
|
after: { size: 0, max: 100 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.find('.toggle-btn').trigger('click')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('50%')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('50%')
|
||||||
|
|
||||||
|
await wrapper.findAll('.toggle-btn').at(0).trigger('click')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('0%')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('100%')
|
||||||
|
|
||||||
|
await wrapper.find('.toggle-btn').trigger('click')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('50%')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('50%')
|
||||||
|
|
||||||
|
await wrapper.findAll('.toggle-btn').at(1).trigger('click')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('100%')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('0%')
|
||||||
|
})
|
||||||
|
|
||||||
it('drag - vertical', async () => {
|
it('drag - vertical', async () => {
|
||||||
const root = document.createElement('div')
|
const root = document.createElement('div')
|
||||||
const place = document.createElement('div')
|
const place = document.createElement('div')
|
||||||
|
|||||||
@@ -116,6 +116,33 @@ describe('csv.js', () => {
|
|||||||
await expect(csv.parse(file)).to.be.rejectedWith(err)
|
await expect(csv.parse(file)).to.be.rejectedWith(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('parse rejects when getResult failed', async () => {
|
||||||
|
let err
|
||||||
|
try {
|
||||||
|
new Date('invalid date').toISOString()
|
||||||
|
} catch (e) {
|
||||||
|
err = e // get error message, it's different depending on browser
|
||||||
|
}
|
||||||
|
sinon.stub(Papa, 'parse').callsFake((file, config) => {
|
||||||
|
config.complete({
|
||||||
|
data: [
|
||||||
|
[1, new Date('invalid date')],
|
||||||
|
[2, new Date('2023-05-05T15:30:00Z')]
|
||||||
|
],
|
||||||
|
errors: [],
|
||||||
|
meta: {
|
||||||
|
delimiter: ',',
|
||||||
|
linebreak: '\n',
|
||||||
|
aborted: false,
|
||||||
|
truncated: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const file = {}
|
||||||
|
await expect(csv.parse(file)).to.be.rejectedWith(err.message)
|
||||||
|
})
|
||||||
|
|
||||||
it('prepareForExport', () => {
|
it('prepareForExport', () => {
|
||||||
const resultSet = {
|
const resultSet = {
|
||||||
columns: ['id', 'name'],
|
columns: ['id', 'name'],
|
||||||
|
|||||||
@@ -87,14 +87,14 @@ describe('storedInquiries.js', () => {
|
|||||||
|
|
||||||
it('isTabNeedName returns false when the inquiry has a name and is not predefined', () => {
|
it('isTabNeedName returns false when the inquiry has a name and is not predefined', () => {
|
||||||
const tab = {
|
const tab = {
|
||||||
initName: 'foo'
|
name: 'foo'
|
||||||
}
|
}
|
||||||
expect(storedInquiries.isTabNeedName(tab)).to.equal(false)
|
expect(storedInquiries.isTabNeedName(tab)).to.equal(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('isTabNeedName returns true when the inquiry has no name and is not predefined', () => {
|
it('isTabNeedName returns true when the inquiry has no name and is not predefined', () => {
|
||||||
const tab = {
|
const tab = {
|
||||||
initName: null,
|
name: null,
|
||||||
tempName: 'Untitled'
|
tempName: 'Untitled'
|
||||||
}
|
}
|
||||||
expect(storedInquiries.isTabNeedName(tab)).to.equal(true)
|
expect(storedInquiries.isTabNeedName(tab)).to.equal(true)
|
||||||
@@ -102,7 +102,7 @@ describe('storedInquiries.js', () => {
|
|||||||
|
|
||||||
it('isTabNeedName returns true when the inquiry is predefined', () => {
|
it('isTabNeedName returns true when the inquiry is predefined', () => {
|
||||||
const tab = {
|
const tab = {
|
||||||
initName: 'foo',
|
name: 'foo',
|
||||||
isPredefined: true
|
isPredefined: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,14 +351,13 @@ describe('storedInquiries.js', () => {
|
|||||||
query: 'select * from foo',
|
query: 'select * from foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: [],
|
||||||
initName: null,
|
name: null,
|
||||||
$refs: {
|
|
||||||
dataView: {
|
dataView: {
|
||||||
getOptionsForSave () {
|
getOptionsForSave () {
|
||||||
return ['chart']
|
return ['chart']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const value = storedInquiries.save(tab, 'foo')
|
const value = storedInquiries.save(tab, 'foo')
|
||||||
expect(value.id).to.equal(tab.id)
|
expect(value.id).to.equal(tab.id)
|
||||||
@@ -376,19 +375,18 @@ describe('storedInquiries.js', () => {
|
|||||||
query: 'select * from foo',
|
query: 'select * from foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: [],
|
||||||
initName: null,
|
name: null,
|
||||||
$refs: {
|
|
||||||
dataView: {
|
dataView: {
|
||||||
getOptionsForSave () {
|
getOptionsForSave () {
|
||||||
return ['chart']
|
return ['chart']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const first = storedInquiries.save(tab, 'foo')
|
const first = storedInquiries.save(tab, 'foo')
|
||||||
|
|
||||||
tab.initName = 'foo'
|
tab.name = 'foo'
|
||||||
tab.query = 'select * from foo'
|
tab.query = 'select * from foo'
|
||||||
storedInquiries.save(tab)
|
storedInquiries.save(tab)
|
||||||
const inquiries = storedInquiries.getStoredInquiries()
|
const inquiries = storedInquiries.getStoredInquiries()
|
||||||
@@ -409,13 +407,11 @@ describe('storedInquiries.js', () => {
|
|||||||
query: 'select * from foo',
|
query: 'select * from foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: [],
|
||||||
initName: 'foo predefined',
|
name: 'foo predefined',
|
||||||
$refs: {
|
|
||||||
dataView: {
|
dataView: {
|
||||||
getOptionsForSave () {
|
getOptionsForSave () {
|
||||||
return ['chart']
|
return ['chart']
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
isPredefined: true
|
isPredefined: true
|
||||||
}
|
}
|
||||||
|
|||||||
189
tests/lib/tab.spec.js
Normal file
189
tests/lib/tab.spec.js
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import { expect } from 'chai'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
import Tab from '@/lib/tab.js'
|
||||||
|
|
||||||
|
describe('tab.js', () => {
|
||||||
|
it('Creates a tab for new inquiry', () => {
|
||||||
|
const state = {
|
||||||
|
untitledLastIndex: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTab = new Tab(state)
|
||||||
|
expect(newTab).to.include({
|
||||||
|
name: null,
|
||||||
|
tempName: 'Untitled 5',
|
||||||
|
query: undefined,
|
||||||
|
viewOptions: undefined,
|
||||||
|
isPredefined: undefined,
|
||||||
|
viewType: 'chart',
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: false,
|
||||||
|
state: state
|
||||||
|
})
|
||||||
|
expect(newTab.layout).to.include({
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
})
|
||||||
|
expect(newTab.id).to.have.lengthOf(21)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Creates a tab for existing inquiry', () => {
|
||||||
|
const state = {
|
||||||
|
untitledLastIndex: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
const inquiry = {
|
||||||
|
id: 'qwerty',
|
||||||
|
query: 'SELECT * from foo',
|
||||||
|
viewType: 'pivot',
|
||||||
|
viewOptions: 'this is view options object',
|
||||||
|
name: 'Foo inquiry',
|
||||||
|
createdAt: '2022-12-05T18:30:30'
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTab = new Tab(state, inquiry)
|
||||||
|
expect(newTab).to.include({
|
||||||
|
id: 'qwerty',
|
||||||
|
name: 'Foo inquiry',
|
||||||
|
tempName: 'Foo inquiry',
|
||||||
|
query: 'SELECT * from foo',
|
||||||
|
viewOptions: 'this is view options object',
|
||||||
|
isPredefined: undefined,
|
||||||
|
viewType: 'pivot',
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true,
|
||||||
|
state: state
|
||||||
|
})
|
||||||
|
expect(newTab.layout).to.include({
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Set isGettingResults true when execute', async () => {
|
||||||
|
let resolveQuering
|
||||||
|
// mock store state
|
||||||
|
const state = {
|
||||||
|
currentTabId: 1,
|
||||||
|
dbName: 'fooDb',
|
||||||
|
db: {
|
||||||
|
execute: sinon.stub().returns(new Promise(resolve => {
|
||||||
|
resolveQuering = resolve
|
||||||
|
})),
|
||||||
|
refreshSchema: sinon.stub().resolves()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTab = new Tab(state, {
|
||||||
|
id: 'qwerty',
|
||||||
|
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
||||||
|
viewType: 'cart',
|
||||||
|
viewOptions: 'this is view options object',
|
||||||
|
name: 'Foo inquiry',
|
||||||
|
createdAt: '2022-12-05T18:30:30'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(newTab.isGettingResults).to.equal(false)
|
||||||
|
newTab.execute()
|
||||||
|
expect(newTab.isGettingResults).to.equal(true)
|
||||||
|
resolveQuering()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Updates result with query execution result', async () => {
|
||||||
|
const result = {
|
||||||
|
columns: ['id', 'name'],
|
||||||
|
values: {
|
||||||
|
id: [1, 2],
|
||||||
|
name: ['Harry', 'Drako']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mock store state
|
||||||
|
const state = {
|
||||||
|
currentTabId: 1,
|
||||||
|
dbName: 'fooDb',
|
||||||
|
db: {
|
||||||
|
execute: sinon.stub().resolves(result),
|
||||||
|
refreshSchema: sinon.stub().resolves()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTab = new Tab(state, {
|
||||||
|
id: 'qwerty',
|
||||||
|
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
||||||
|
viewType: 'cart',
|
||||||
|
viewOptions: 'this is view options object',
|
||||||
|
name: 'Foo inquiry',
|
||||||
|
createdAt: '2022-12-05T18:30:30'
|
||||||
|
})
|
||||||
|
|
||||||
|
await newTab.execute()
|
||||||
|
expect(newTab.isGettingResults).to.equal(false)
|
||||||
|
expect(newTab.result).to.eql(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Updates error with query execution error', async () => {
|
||||||
|
// mock store state
|
||||||
|
const state = {
|
||||||
|
currentTabId: 1,
|
||||||
|
dbName: 'fooDb',
|
||||||
|
db: {
|
||||||
|
execute: sinon.stub().rejects(new Error('No such table')),
|
||||||
|
refreshSchema: sinon.stub().resolves()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTab = new Tab(state, {
|
||||||
|
id: 'qwerty',
|
||||||
|
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
||||||
|
viewType: 'cart',
|
||||||
|
viewOptions: 'this is view options object',
|
||||||
|
name: 'Foo inquiry',
|
||||||
|
createdAt: '2022-12-05T18:30:30'
|
||||||
|
})
|
||||||
|
|
||||||
|
await newTab.execute()
|
||||||
|
expect(newTab.error.type).to.eql('error')
|
||||||
|
expect(newTab.error.message.toString()).to.equal('Error: No such table')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Updates schema after query execution', async () => {
|
||||||
|
const result = {
|
||||||
|
columns: ['id', 'name'],
|
||||||
|
values: {
|
||||||
|
id: [],
|
||||||
|
name: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mock store state
|
||||||
|
const state = {
|
||||||
|
currentTabId: 1,
|
||||||
|
dbName: 'fooDb',
|
||||||
|
db: {
|
||||||
|
execute: sinon.stub().resolves(result),
|
||||||
|
refreshSchema: sinon.stub().resolves()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTab = new Tab(state, {
|
||||||
|
id: 'qwerty',
|
||||||
|
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
||||||
|
viewType: 'cart',
|
||||||
|
viewOptions: 'this is view options object',
|
||||||
|
name: 'Foo inquiry',
|
||||||
|
createdAt: '2022-12-05T18:30:30'
|
||||||
|
})
|
||||||
|
|
||||||
|
await newTab.execute()
|
||||||
|
expect(state.db.refreshSchema.calledOnce).to.equal(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -7,9 +7,9 @@ describe('clipboardIo.js', async () => {
|
|||||||
sinon.restore()
|
sinon.restore()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('copyCsv', async () => {
|
it('copyText', async () => {
|
||||||
sinon.stub(navigator.clipboard, 'writeText').resolves(true)
|
sinon.stub(navigator.clipboard, 'writeText').resolves(true)
|
||||||
await cIo.copyCsv('id\tname\r\n1\t2')
|
await cIo.copyText('id\tname\r\n1\t2')
|
||||||
expect(navigator.clipboard.writeText.calledOnceWith('id\tname\r\n1\t2'))
|
expect(navigator.clipboard.writeText.calledOnceWith('id\tname\r\n1\t2'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ describe('actions', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let id = await addTab({ state })
|
let id = await addTab({ state })
|
||||||
expect(state.tabs[0]).to.eql({
|
expect(state.tabs[0]).to.include({
|
||||||
id: id,
|
id: id,
|
||||||
name: null,
|
name: null,
|
||||||
tempName: 'Untitled',
|
tempName: 'Untitled',
|
||||||
@@ -22,7 +22,7 @@ describe('actions', () => {
|
|||||||
expect(state.untitledLastIndex).to.equal(1)
|
expect(state.untitledLastIndex).to.equal(1)
|
||||||
|
|
||||||
id = await addTab({ state })
|
id = await addTab({ state })
|
||||||
expect(state.tabs[1]).to.eql({
|
expect(state.tabs[1]).to.include({
|
||||||
id: id,
|
id: id,
|
||||||
name: null,
|
name: null,
|
||||||
tempName: 'Untitled 1',
|
tempName: 'Untitled 1',
|
||||||
@@ -41,14 +41,13 @@ describe('actions', () => {
|
|||||||
const tab = {
|
const tab = {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'test',
|
name: 'test',
|
||||||
tempName: null,
|
|
||||||
query: 'SELECT * from foo',
|
query: 'SELECT * from foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: {},
|
viewOptions: 'an object with view options',
|
||||||
isSaved: true
|
isSaved: true
|
||||||
}
|
}
|
||||||
await addTab({ state }, tab)
|
await addTab({ state }, tab)
|
||||||
expect(state.tabs[0]).to.eql(tab)
|
expect(state.tabs[0]).to.include(tab)
|
||||||
expect(state.untitledLastIndex).to.equal(0)
|
expect(state.untitledLastIndex).to.equal(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ const {
|
|||||||
updateTab,
|
updateTab,
|
||||||
deleteTab,
|
deleteTab,
|
||||||
setCurrentTabId,
|
setCurrentTabId,
|
||||||
setCurrentTab,
|
|
||||||
updatePredefinedInquiries,
|
updatePredefinedInquiries,
|
||||||
setDb,
|
setDb,
|
||||||
setLoadingPredefinedInquiries,
|
setLoadingPredefinedInquiries,
|
||||||
@@ -37,8 +36,7 @@ describe('mutations', () => {
|
|||||||
isPredefined: false
|
isPredefined: false
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTab = {
|
const newValues = {
|
||||||
index: 0,
|
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'new test',
|
name: 'new test',
|
||||||
query: 'SELECT * from bar',
|
query: 'SELECT * from bar',
|
||||||
@@ -51,7 +49,7 @@ describe('mutations', () => {
|
|||||||
tabs: [tab]
|
tabs: [tab]
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTab(state, newTab)
|
updateTab(state, { tab, newValues })
|
||||||
expect(state.tabs[0]).to.eql({
|
expect(state.tabs[0]).to.eql({
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'new test',
|
name: 'new test',
|
||||||
@@ -75,8 +73,7 @@ describe('mutations', () => {
|
|||||||
isPredefined: true
|
isPredefined: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTab = {
|
const newValues = {
|
||||||
index: 0,
|
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'new test',
|
name: 'new test',
|
||||||
query: 'SELECT * from bar',
|
query: 'SELECT * from bar',
|
||||||
@@ -90,7 +87,7 @@ describe('mutations', () => {
|
|||||||
currentTabId: 1
|
currentTabId: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTab(state, newTab)
|
updateTab(state, { tab, newValues })
|
||||||
expect(state.tabs).to.have.lengthOf(1)
|
expect(state.tabs).to.have.lengthOf(1)
|
||||||
expect(state.currentTabId).to.equal(2)
|
expect(state.currentTabId).to.equal(2)
|
||||||
expect(state.tabs[0].id).to.equal(2)
|
expect(state.tabs[0].id).to.equal(2)
|
||||||
@@ -111,8 +108,7 @@ describe('mutations', () => {
|
|||||||
isSaved: false
|
isSaved: false
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTab = {
|
const newValues = {
|
||||||
index: 0,
|
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'new test'
|
name: 'new test'
|
||||||
}
|
}
|
||||||
@@ -121,7 +117,7 @@ describe('mutations', () => {
|
|||||||
tabs: [tab]
|
tabs: [tab]
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTab(state, newTab)
|
updateTab(state, { tab, newValues })
|
||||||
expect(state.tabs).to.have.lengthOf(1)
|
expect(state.tabs).to.have.lengthOf(1)
|
||||||
expect(state.tabs[0].id).to.equal(1)
|
expect(state.tabs[0].id).to.equal(1)
|
||||||
expect(state.tabs[0].name).to.equal('new test')
|
expect(state.tabs[0].name).to.equal('new test')
|
||||||
@@ -141,8 +137,7 @@ describe('mutations', () => {
|
|||||||
isPredefined: true
|
isPredefined: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTab = {
|
const newValues = {
|
||||||
index: 0,
|
|
||||||
isSaved: false
|
isSaved: false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +145,7 @@ describe('mutations', () => {
|
|||||||
tabs: [tab]
|
tabs: [tab]
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTab(state, newTab)
|
updateTab(state, { tab, newValues })
|
||||||
expect(state.tabs).to.have.lengthOf(1)
|
expect(state.tabs).to.have.lengthOf(1)
|
||||||
expect(state.tabs[0].id).to.equal(1)
|
expect(state.tabs[0].id).to.equal(1)
|
||||||
expect(state.tabs[0].name).to.equal('test')
|
expect(state.tabs[0].name).to.equal('test')
|
||||||
@@ -181,13 +176,15 @@ describe('mutations', () => {
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [tab1, tab2],
|
tabs: [tab1, tab2],
|
||||||
currentTabId: 1
|
currentTabId: 1,
|
||||||
|
currentTab: tab1
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTab(state, 0)
|
deleteTab(state, tab1)
|
||||||
expect(state.tabs).to.have.lengthOf(1)
|
expect(state.tabs).to.have.lengthOf(1)
|
||||||
expect(state.tabs[0].id).to.equal(2)
|
expect(state.tabs[0].id).to.equal(2)
|
||||||
expect(state.currentTabId).to.equal(2)
|
expect(state.currentTabId).to.equal(2)
|
||||||
|
expect(state.currentTab).to.eql(tab2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('deleteTab - opened, last', () => {
|
it('deleteTab - opened, last', () => {
|
||||||
@@ -213,13 +210,15 @@ describe('mutations', () => {
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [tab1, tab2],
|
tabs: [tab1, tab2],
|
||||||
currentTabId: 2
|
currentTabId: 2,
|
||||||
|
currentTab: tab2
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTab(state, 1)
|
deleteTab(state, tab2)
|
||||||
expect(state.tabs).to.have.lengthOf(1)
|
expect(state.tabs).to.have.lengthOf(1)
|
||||||
expect(state.tabs[0].id).to.equal(1)
|
expect(state.tabs[0].id).to.equal(1)
|
||||||
expect(state.currentTabId).to.equal(1)
|
expect(state.currentTabId).to.equal(1)
|
||||||
|
expect(state.currentTab).to.eql(tab1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('deleteTab - opened, in the middle', () => {
|
it('deleteTab - opened, in the middle', () => {
|
||||||
@@ -255,14 +254,16 @@ describe('mutations', () => {
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [tab1, tab2, tab3],
|
tabs: [tab1, tab2, tab3],
|
||||||
currentTabId: 2
|
currentTabId: 2,
|
||||||
|
currentTab: tab2
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTab(state, 1)
|
deleteTab(state, tab2)
|
||||||
expect(state.tabs).to.have.lengthOf(2)
|
expect(state.tabs).to.have.lengthOf(2)
|
||||||
expect(state.tabs[0].id).to.equal(1)
|
expect(state.tabs[0].id).to.equal(1)
|
||||||
expect(state.tabs[1].id).to.equal(3)
|
expect(state.tabs[1].id).to.equal(3)
|
||||||
expect(state.currentTabId).to.equal(3)
|
expect(state.currentTabId).to.equal(3)
|
||||||
|
expect(state.currentTab).to.eql(tab3)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('deleteTab - opened, single', () => {
|
it('deleteTab - opened, single', () => {
|
||||||
@@ -278,48 +279,19 @@ describe('mutations', () => {
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [tab1],
|
tabs: [tab1],
|
||||||
currentTabId: 1
|
currentTabId: 1,
|
||||||
|
currentTab: tab1
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTab(state, 0)
|
deleteTab(state, tab1)
|
||||||
expect(state.tabs).to.have.lengthOf(0)
|
expect(state.tabs).to.have.lengthOf(0)
|
||||||
expect(state.currentTabId).to.equal(null)
|
expect(state.currentTabId).to.equal(null)
|
||||||
})
|
expect(state.currentTab).to.equal(null)
|
||||||
|
|
||||||
it('deleteTab - not opened', () => {
|
|
||||||
const tab1 = {
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
tempName: null,
|
|
||||||
query: 'SELECT * from foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: {},
|
|
||||||
isSaved: true
|
|
||||||
}
|
|
||||||
|
|
||||||
const tab2 = {
|
|
||||||
id: 2,
|
|
||||||
name: 'bar',
|
|
||||||
tempName: null,
|
|
||||||
query: 'SELECT * from bar',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: {},
|
|
||||||
isSaved: true
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
tabs: [tab1, tab2],
|
|
||||||
currentTabId: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteTab(state, 1)
|
|
||||||
expect(state.tabs).to.have.lengthOf(1)
|
|
||||||
expect(state.tabs[0].id).to.equal(1)
|
|
||||||
expect(state.currentTabId).to.equal(1)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('setCurrentTabId', () => {
|
it('setCurrentTabId', () => {
|
||||||
const state = {
|
const state = {
|
||||||
|
tabs: [{ id: 1 }, { id: 2 }],
|
||||||
currentTabId: 1
|
currentTabId: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,15 +299,6 @@ describe('mutations', () => {
|
|||||||
expect(state.currentTabId).to.equal(2)
|
expect(state.currentTabId).to.equal(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('setCurrentTab', () => {
|
|
||||||
const state = {
|
|
||||||
currentTab: { id: 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentTab(state, { id: 2 })
|
|
||||||
expect(state.currentTab).to.eql({ id: 2 })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('updatePredefinedInquiries - single', () => {
|
it('updatePredefinedInquiries - single', () => {
|
||||||
const inquiry = {
|
const inquiry = {
|
||||||
id: 1,
|
id: 1,
|
||||||
|
|||||||
147
tests/views/LoadView.spec.js
Normal file
147
tests/views/LoadView.spec.js
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { expect } from 'chai'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import LoadView from '@/views/LoadView'
|
||||||
|
import fu from '@/lib/utils/fileIo'
|
||||||
|
import database from '@/lib/database'
|
||||||
|
import realMutations from '@/store/mutations'
|
||||||
|
import realActions from '@/store/actions'
|
||||||
|
import flushPromises from 'flush-promises'
|
||||||
|
import Tab from '@/lib/tab'
|
||||||
|
|
||||||
|
describe('LoadView.vue', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
sinon.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Loads db and inquiries and redirects to workspace if no errors', async () => {
|
||||||
|
const state = {
|
||||||
|
tabs: []
|
||||||
|
}
|
||||||
|
const mutations = {
|
||||||
|
setCurrentTabId: sinon.stub().callsFake(realMutations.setCurrentTabId),
|
||||||
|
setDb: sinon.stub().callsFake(realMutations.setDb)
|
||||||
|
}
|
||||||
|
const actions = {
|
||||||
|
addTab: sinon.stub().callsFake(realActions.addTab)
|
||||||
|
}
|
||||||
|
const store = new Vuex.Store({ state, mutations, actions })
|
||||||
|
const $route = {
|
||||||
|
path: '/workspace',
|
||||||
|
query: {
|
||||||
|
data_url: 'https://my-url/test.db',
|
||||||
|
data_format: 'sqlite',
|
||||||
|
inquiry_url: 'https://my-url/test_inquiries.json',
|
||||||
|
inquiry_id: [1],
|
||||||
|
maximize: 'dataView'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const $router = { push: sinon.stub() }
|
||||||
|
|
||||||
|
const readFile = sinon.stub(fu, 'readFile')
|
||||||
|
const dataRes = new Response()
|
||||||
|
dataRes.blob = sinon.stub().resolves({})
|
||||||
|
readFile.onCall(0).returns(Promise.resolve(dataRes))
|
||||||
|
|
||||||
|
const inquiriesRes = new Response()
|
||||||
|
inquiriesRes.json = sinon.stub().resolves({
|
||||||
|
version: 2,
|
||||||
|
inquiries: [{ id: 1, name: 'foo' }, { id: 2, name: 'bar' }]
|
||||||
|
})
|
||||||
|
readFile.onCall(1).returns(Promise.resolve(inquiriesRes))
|
||||||
|
const db = {
|
||||||
|
loadDb: sinon.stub().resolves()
|
||||||
|
}
|
||||||
|
sinon.stub(database, 'getNewDatabase').returns(db)
|
||||||
|
Tab.prototype.execute = sinon.stub()
|
||||||
|
|
||||||
|
const wrapper = mount(LoadView, {
|
||||||
|
store,
|
||||||
|
mocks: { $route, $router },
|
||||||
|
stubs: ['router-link']
|
||||||
|
})
|
||||||
|
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
// DB file is read
|
||||||
|
expect(fu.readFile.firstCall.args[0]).to.equal('https://my-url/test.db')
|
||||||
|
|
||||||
|
// Db is loaded
|
||||||
|
expect(db.loadDb.firstCall.args[0]).to.equal(await dataRes.blob.returnValues[0])
|
||||||
|
|
||||||
|
// Inquiries file is read
|
||||||
|
expect(fu.readFile.secondCall.args[0])
|
||||||
|
.to.equal('https://my-url/test_inquiries.json')
|
||||||
|
|
||||||
|
// Tab for inquiry is created
|
||||||
|
expect(actions.addTab.calledOnce).to.equal(true)
|
||||||
|
expect(actions.addTab.firstCall.args[1]).eql({
|
||||||
|
id: undefined,
|
||||||
|
name: 'foo',
|
||||||
|
layout: {
|
||||||
|
dataView: 'bottom',
|
||||||
|
sqlEditor: 'hidden',
|
||||||
|
table: 'above'
|
||||||
|
},
|
||||||
|
maximize: 'dataView'
|
||||||
|
})
|
||||||
|
const executedTab = Tab.prototype.execute.firstCall.thisValue
|
||||||
|
expect(executedTab.tempName).to.equal('foo')
|
||||||
|
expect(wrapper.find('#open-workspace-btn').exists()).to.equal(false)
|
||||||
|
expect($router.push.called).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Doesn\'t redirect and show the button if there is an error', async () => {
|
||||||
|
const state = {
|
||||||
|
tabs: []
|
||||||
|
}
|
||||||
|
const mutations = {
|
||||||
|
setCurrentTabId: sinon.stub().callsFake(realMutations.setCurrentTabId),
|
||||||
|
setDb: sinon.stub().callsFake(realMutations.setDb)
|
||||||
|
}
|
||||||
|
const actions = {
|
||||||
|
addTab: sinon.stub().callsFake(realActions.addTab)
|
||||||
|
}
|
||||||
|
const store = new Vuex.Store({ state, mutations, actions })
|
||||||
|
const $route = {
|
||||||
|
path: '/workspace',
|
||||||
|
query: {
|
||||||
|
data_url: 'https://my-url/test.db',
|
||||||
|
data_format: 'sqlite',
|
||||||
|
inquiry_url: 'https://my-url/test_inquiries.json',
|
||||||
|
inquiry_id: [1],
|
||||||
|
maximize: 'dataView'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const $router = { push: sinon.stub() }
|
||||||
|
|
||||||
|
const readFile = sinon.stub(fu, 'readFile')
|
||||||
|
const dataRes = new Response()
|
||||||
|
dataRes.blob = sinon.stub().rejects(new Error('Something is wrong'))
|
||||||
|
readFile.onCall(0).returns(Promise.resolve(dataRes))
|
||||||
|
|
||||||
|
const inquiriesRes = new Response()
|
||||||
|
inquiriesRes.json = sinon.stub().resolves({
|
||||||
|
version: 2,
|
||||||
|
inquiries: [{ id: 1 }]
|
||||||
|
})
|
||||||
|
readFile.onCall(1).returns(Promise.resolve(inquiriesRes))
|
||||||
|
sinon.stub(database, 'getNewDatabase').returns({
|
||||||
|
loadDb: sinon.stub().resolves()
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapper = mount(LoadView, {
|
||||||
|
store,
|
||||||
|
mocks: { $route, $router },
|
||||||
|
stubs: ['router-link']
|
||||||
|
})
|
||||||
|
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.find('#open-workspace-btn').exists()).to.equal(true)
|
||||||
|
expect($router.push.called).to.equal(false)
|
||||||
|
expect(wrapper.find('#logs').text()).to.include('Something is wrong')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -19,7 +19,9 @@ describe('Inquiries.vue', () => {
|
|||||||
predefinedInquiries: []
|
predefinedInquiries: []
|
||||||
}
|
}
|
||||||
const mutations = {
|
const mutations = {
|
||||||
updatePredefinedInquiries: sinon.stub()
|
setPredefinedInquiriesLoaded: sinon.stub(),
|
||||||
|
updatePredefinedInquiries: sinon.stub(),
|
||||||
|
setLoadingPredefinedInquiries: sinon.stub()
|
||||||
}
|
}
|
||||||
const store = new Vuex.Store({ state, mutations })
|
const store = new Vuex.Store({ state, mutations })
|
||||||
const wrapper = shallowMount(Inquiries, { store })
|
const wrapper = shallowMount(Inquiries, { store })
|
||||||
@@ -327,6 +329,7 @@ describe('Inquiries.vue', () => {
|
|||||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([inquiryInStorage])
|
sinon.stub(storedInquiries, 'getStoredInquiries').returns([inquiryInStorage])
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
|
tabs: [],
|
||||||
predefinedInquiries: []
|
predefinedInquiries: []
|
||||||
}
|
}
|
||||||
const actions = { addTab: sinon.stub().resolves(1) }
|
const actions = { addTab: sinon.stub().resolves(1) }
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ describe('MainMenu.vue', () => {
|
|||||||
it('Save is not visible if there is no tabs', () => {
|
it('Save is not visible if there is no tabs', () => {
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: null,
|
currentTab: null,
|
||||||
tabs: [{}],
|
tabs: [],
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const store = new Vuex.Store({ state })
|
const store = new Vuex.Store({ state })
|
||||||
@@ -62,13 +62,15 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Save is disabled if current tab.isSaved is true', async () => {
|
it('Save is disabled if current tab.isSaved is true', async () => {
|
||||||
const state = {
|
const tab = {
|
||||||
currentTab: {
|
id: 1,
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
tabIndex: 0
|
isSaved: false
|
||||||
},
|
}
|
||||||
tabs: [{ isSaved: false }],
|
const state = {
|
||||||
|
currentTab: tab,
|
||||||
|
tabs: [tab],
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const store = new Vuex.Store({ state })
|
const store = new Vuex.Store({ state })
|
||||||
@@ -83,17 +85,19 @@ describe('MainMenu.vue', () => {
|
|||||||
expect(wrapper.find('#save-btn').element.disabled).to.equal(false)
|
expect(wrapper.find('#save-btn').element.disabled).to.equal(false)
|
||||||
|
|
||||||
await vm.$set(state.tabs[0], 'isSaved', true)
|
await vm.$set(state.tabs[0], 'isSaved', true)
|
||||||
|
await vm.$nextTick()
|
||||||
expect(wrapper.find('#save-btn').element.disabled).to.equal(true)
|
expect(wrapper.find('#save-btn').element.disabled).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Creates a tab', async () => {
|
it('Creates a tab', async () => {
|
||||||
const state = {
|
const tab = {
|
||||||
currentTab: {
|
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
tabIndex: 0
|
isSaved: false
|
||||||
},
|
}
|
||||||
tabs: [{ isSaved: false }],
|
const state = {
|
||||||
|
currentTab: tab,
|
||||||
|
tabs: [tab],
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const newInquiryId = 1
|
const newInquiryId = 1
|
||||||
@@ -121,13 +125,14 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Creates a tab and redirects to workspace', async () => {
|
it('Creates a tab and redirects to workspace', async () => {
|
||||||
const state = {
|
const tab = {
|
||||||
currentTab: {
|
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
tabIndex: 0
|
isSaved: false
|
||||||
},
|
}
|
||||||
tabs: [{ isSaved: false }],
|
const state = {
|
||||||
|
currentTab: tab,
|
||||||
|
tabs: [tab],
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const newInquiryId = 1
|
const newInquiryId = 1
|
||||||
@@ -156,13 +161,14 @@ describe('MainMenu.vue', () => {
|
|||||||
|
|
||||||
it('Ctrl R calls currentTab.execute if running is enabled and route.path is "/workspace"',
|
it('Ctrl R calls currentTab.execute if running is enabled and route.path is "/workspace"',
|
||||||
async () => {
|
async () => {
|
||||||
const state = {
|
const tab = {
|
||||||
currentTab: {
|
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
tabIndex: 0
|
isSaved: false
|
||||||
},
|
}
|
||||||
tabs: [{ isSaved: false }],
|
const state = {
|
||||||
|
currentTab: tab,
|
||||||
|
tabs: [tab],
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const store = new Vuex.Store({ state })
|
const store = new Vuex.Store({ state })
|
||||||
@@ -201,13 +207,14 @@ describe('MainMenu.vue', () => {
|
|||||||
|
|
||||||
it('Ctrl Enter calls currentTab.execute if running is enabled and route.path is "/workspace"',
|
it('Ctrl Enter calls currentTab.execute if running is enabled and route.path is "/workspace"',
|
||||||
async () => {
|
async () => {
|
||||||
const state = {
|
const tab = {
|
||||||
currentTab: {
|
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
tabIndex: 0
|
isSaved: false
|
||||||
},
|
}
|
||||||
tabs: [{ isSaved: false }],
|
const state = {
|
||||||
|
currentTab: tab,
|
||||||
|
tabs: [tab],
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const store = new Vuex.Store({ state })
|
const store = new Vuex.Store({ state })
|
||||||
@@ -245,13 +252,14 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Ctrl B calls createNewInquiry', async () => {
|
it('Ctrl B calls createNewInquiry', async () => {
|
||||||
const state = {
|
const tab = {
|
||||||
currentTab: {
|
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
tabIndex: 0
|
isSaved: false
|
||||||
},
|
}
|
||||||
tabs: [{ isSaved: false }],
|
const state = {
|
||||||
|
currentTab: tab,
|
||||||
|
tabs: [tab],
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const store = new Vuex.Store({ state })
|
const store = new Vuex.Store({ state })
|
||||||
@@ -280,13 +288,14 @@ describe('MainMenu.vue', () => {
|
|||||||
|
|
||||||
it('Ctrl S calls checkInquiryBeforeSave if the tab is unsaved and route path is /workspace',
|
it('Ctrl S calls checkInquiryBeforeSave if the tab is unsaved and route path is /workspace',
|
||||||
async () => {
|
async () => {
|
||||||
const state = {
|
const tab = {
|
||||||
currentTab: {
|
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
tabIndex: 0
|
isSaved: false
|
||||||
},
|
}
|
||||||
tabs: [{ isSaved: false }],
|
const state = {
|
||||||
|
currentTab: tab,
|
||||||
|
tabs: [tab],
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const store = new Vuex.Store({ state })
|
const store = new Vuex.Store({ state })
|
||||||
@@ -325,13 +334,16 @@ describe('MainMenu.vue', () => {
|
|||||||
|
|
||||||
it('Saves the inquiry when no need the new name',
|
it('Saves the inquiry when no need the new name',
|
||||||
async () => {
|
async () => {
|
||||||
const state = {
|
const tab = {
|
||||||
currentTab: {
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
tabIndex: 0
|
isSaved: false
|
||||||
},
|
}
|
||||||
tabs: [{ id: 1, name: 'foo', isSaved: false }],
|
const state = {
|
||||||
|
currentTab: tab,
|
||||||
|
tabs: [tab],
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const mutations = {
|
const mutations = {
|
||||||
@@ -364,13 +376,15 @@ describe('MainMenu.vue', () => {
|
|||||||
|
|
||||||
// check that the tab was updated
|
// check that the tab was updated
|
||||||
expect(mutations.updateTab.calledOnceWith(state, sinon.match({
|
expect(mutations.updateTab.calledOnceWith(state, sinon.match({
|
||||||
index: 0,
|
tab,
|
||||||
|
newValues: {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
id: 1,
|
id: 1,
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: [],
|
||||||
isSaved: true
|
isSaved: true
|
||||||
|
}
|
||||||
}))).to.equal(true)
|
}))).to.equal(true)
|
||||||
|
|
||||||
// check that 'inquirySaved' event was triggered on $root
|
// check that 'inquirySaved' event was triggered on $root
|
||||||
@@ -378,13 +392,17 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Shows en error when the new name is needed but not specifyied', async () => {
|
it('Shows en error when the new name is needed but not specifyied', async () => {
|
||||||
const state = {
|
const tab = {
|
||||||
currentTab: {
|
id: 1,
|
||||||
|
name: null,
|
||||||
|
tempName: 'Untitled',
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
tabIndex: 0
|
isSaved: false
|
||||||
},
|
}
|
||||||
tabs: [{ id: 1, name: null, tempName: 'Untitled', isSaved: false }],
|
const state = {
|
||||||
|
currentTab: tab,
|
||||||
|
tabs: [tab],
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const mutations = {
|
const mutations = {
|
||||||
@@ -424,13 +442,17 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Saves the inquiry with a new name', async () => {
|
it('Saves the inquiry with a new name', async () => {
|
||||||
const state = {
|
const tab = {
|
||||||
currentTab: {
|
id: 1,
|
||||||
|
name: null,
|
||||||
|
tempName: 'Untitled',
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
tabIndex: 0
|
isSaved: false
|
||||||
},
|
}
|
||||||
tabs: [{ id: 1, name: null, tempName: 'Untitled', isSaved: false }],
|
const state = {
|
||||||
|
currentTab: tab,
|
||||||
|
tabs: [tab],
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const mutations = {
|
const mutations = {
|
||||||
@@ -475,13 +497,15 @@ describe('MainMenu.vue', () => {
|
|||||||
|
|
||||||
// check that the tab was updated
|
// check that the tab was updated
|
||||||
expect(mutations.updateTab.calledOnceWith(state, sinon.match({
|
expect(mutations.updateTab.calledOnceWith(state, sinon.match({
|
||||||
index: 0,
|
tab: tab,
|
||||||
|
newValues: {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
id: 1,
|
id: 1,
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: [],
|
||||||
isSaved: true
|
isSaved: true
|
||||||
|
}
|
||||||
}))).to.equal(true)
|
}))).to.equal(true)
|
||||||
|
|
||||||
// check that 'inquirySaved' event was triggered on $root
|
// check that 'inquirySaved' event was triggered on $root
|
||||||
@@ -489,11 +513,11 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Saves a predefined inquiry with a new name', async () => {
|
it('Saves a predefined inquiry with a new name', async () => {
|
||||||
const state = {
|
const tab = {
|
||||||
currentTab: {
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
tabIndex: 0,
|
|
||||||
isPredefined: true,
|
isPredefined: true,
|
||||||
result: {
|
result: {
|
||||||
columns: ['id', 'name'],
|
columns: ['id', 'name'],
|
||||||
@@ -503,9 +527,12 @@ describe('MainMenu.vue', () => {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: []
|
viewOptions: [],
|
||||||
},
|
isSaved: false
|
||||||
tabs: [{ id: 1, name: 'foo', isSaved: false, isPredefined: true }],
|
}
|
||||||
|
const state = {
|
||||||
|
currentTab: tab,
|
||||||
|
tabs: [tab],
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const mutations = {
|
const mutations = {
|
||||||
@@ -553,13 +580,15 @@ describe('MainMenu.vue', () => {
|
|||||||
|
|
||||||
// check that the tab was updated
|
// check that the tab was updated
|
||||||
expect(mutations.updateTab.calledOnceWith(state, sinon.match({
|
expect(mutations.updateTab.calledOnceWith(state, sinon.match({
|
||||||
index: 0,
|
tab,
|
||||||
|
newValues: {
|
||||||
name: 'bar',
|
name: 'bar',
|
||||||
id: 2,
|
id: 2,
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: [],
|
||||||
isSaved: true
|
isSaved: true
|
||||||
|
}
|
||||||
}))).to.equal(true)
|
}))).to.equal(true)
|
||||||
|
|
||||||
// check that 'inquirySaved' event was triggered on $root
|
// check that 'inquirySaved' event was triggered on $root
|
||||||
@@ -580,13 +609,17 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Cancel saving', async () => {
|
it('Cancel saving', async () => {
|
||||||
const state = {
|
const tab = {
|
||||||
currentTab: {
|
id: 1,
|
||||||
|
name: null,
|
||||||
|
tempName: 'Untitled',
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
tabIndex: 0
|
isSaved: false
|
||||||
},
|
}
|
||||||
tabs: [{ id: 1, name: null, tempName: 'Untitled', isSaved: false }],
|
const state = {
|
||||||
|
currentTab: tab,
|
||||||
|
tabs: [tab],
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const mutations = {
|
const mutations = {
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ describe('DataView.vue', () => {
|
|||||||
const pivot = wrapper.findComponent({ name: 'pivot' }).vm
|
const pivot = wrapper.findComponent({ name: 'pivot' }).vm
|
||||||
sinon.spy(pivot, 'saveAsSvg')
|
sinon.spy(pivot, 'saveAsSvg')
|
||||||
|
|
||||||
|
// Switch to Custom Chart renderer
|
||||||
|
pivot.pivotOptions.rendererName = 'Custom chart'
|
||||||
|
await pivot.$nextTick()
|
||||||
|
|
||||||
// Export to svg
|
// Export to svg
|
||||||
await svgBtn.trigger('click')
|
await svgBtn.trigger('click')
|
||||||
expect(pivot.saveAsSvg.calledOnce).to.equal(true)
|
expect(pivot.saveAsSvg.calledOnce).to.equal(true)
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
import { expect } from 'chai'
|
|
||||||
import { mount, createWrapper } from '@vue/test-utils'
|
|
||||||
import RunResult from '@/views/Main/Workspace/Tabs/Tab/RunResult'
|
|
||||||
import csv from '@/lib/csv'
|
|
||||||
import sinon from 'sinon'
|
|
||||||
|
|
||||||
describe('RunResult.vue', () => {
|
|
||||||
afterEach(() => {
|
|
||||||
sinon.restore()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows alert when ClipboardItem is not supported', async () => {
|
|
||||||
const ClipboardItem = window.ClipboardItem
|
|
||||||
delete window.ClipboardItem
|
|
||||||
sinon.spy(window, 'alert')
|
|
||||||
const wrapper = mount(RunResult, {
|
|
||||||
propsData: {
|
|
||||||
result: {
|
|
||||||
columns: ['id', 'name'],
|
|
||||||
values: {
|
|
||||||
id: [1],
|
|
||||||
name: ['foo']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const copyBtn = createWrapper(wrapper.findComponent({ name: 'clipboardIcon' }).vm.$parent)
|
|
||||||
await copyBtn.trigger('click')
|
|
||||||
|
|
||||||
expect(
|
|
||||||
window.alert.calledOnceWith(
|
|
||||||
"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.'
|
|
||||||
)
|
|
||||||
).to.equal(true)
|
|
||||||
|
|
||||||
window.ClipboardItem = ClipboardItem
|
|
||||||
})
|
|
||||||
|
|
||||||
it('copy to clipboard more than 1 sec', async () => {
|
|
||||||
sinon.stub(window.navigator.clipboard, 'writeText').resolves()
|
|
||||||
const clock = sinon.useFakeTimers()
|
|
||||||
const wrapper = mount(RunResult, {
|
|
||||||
propsData: {
|
|
||||||
result: {
|
|
||||||
columns: ['id', 'name'],
|
|
||||||
values: {
|
|
||||||
id: [1],
|
|
||||||
name: ['foo']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
sinon.stub(csv, 'serialize').callsFake(() => {
|
|
||||||
clock.tick(5000)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Click copy to clipboard
|
|
||||||
const copyBtn = createWrapper(wrapper.findComponent({ name: 'clipboardIcon' }).vm.$parent)
|
|
||||||
await copyBtn.trigger('click')
|
|
||||||
|
|
||||||
// The dialog is shown...
|
|
||||||
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(true)
|
|
||||||
|
|
||||||
// ... with Building message...
|
|
||||||
expect(wrapper.find('.dialog-body').text()).to.equal('Building CSV...')
|
|
||||||
|
|
||||||
// Switch to microtasks (let serialize run)
|
|
||||||
clock.tick(0)
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
// The dialog is shown...
|
|
||||||
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(true)
|
|
||||||
|
|
||||||
// ... with Ready message...
|
|
||||||
expect(wrapper.find('.dialog-body').text()).to.equal('CSV is ready')
|
|
||||||
|
|
||||||
// Click copy
|
|
||||||
await wrapper.find('.dialog-buttons-container button.primary').trigger('click')
|
|
||||||
|
|
||||||
// The dialog is not shown...
|
|
||||||
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('copy to clipboard less than 1 sec', async () => {
|
|
||||||
sinon.stub(window.navigator.clipboard, 'writeText').resolves()
|
|
||||||
const clock = sinon.useFakeTimers()
|
|
||||||
const wrapper = mount(RunResult, {
|
|
||||||
propsData: {
|
|
||||||
result: {
|
|
||||||
columns: ['id', 'name'],
|
|
||||||
values: {
|
|
||||||
id: [1],
|
|
||||||
name: ['foo']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
sinon.spy(wrapper.vm, 'copyToClipboard')
|
|
||||||
sinon.stub(csv, 'serialize').callsFake(() => {
|
|
||||||
clock.tick(500)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Click copy to clipboard
|
|
||||||
const copyBtn = createWrapper(wrapper.findComponent({ name: 'clipboardIcon' }).vm.$parent)
|
|
||||||
await copyBtn.trigger('click')
|
|
||||||
|
|
||||||
// Switch to microtasks (let serialize run)
|
|
||||||
clock.tick(0)
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
// The dialog is not shown...
|
|
||||||
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(false)
|
|
||||||
// copyToClipboard is called
|
|
||||||
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cancel long copy', async () => {
|
|
||||||
sinon.stub(window.navigator.clipboard, 'writeText').resolves()
|
|
||||||
const clock = sinon.useFakeTimers()
|
|
||||||
const wrapper = mount(RunResult, {
|
|
||||||
propsData: {
|
|
||||||
result: {
|
|
||||||
columns: ['id', 'name'],
|
|
||||||
values: {
|
|
||||||
id: [1],
|
|
||||||
name: ['foo']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
sinon.spy(wrapper.vm, 'copyToClipboard')
|
|
||||||
sinon.stub(csv, 'serialize').callsFake(() => {
|
|
||||||
clock.tick(5000)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Click copy to clipboard
|
|
||||||
const copyBtn = createWrapper(wrapper.findComponent({ name: 'clipboardIcon' }).vm.$parent)
|
|
||||||
await copyBtn.trigger('click')
|
|
||||||
|
|
||||||
// Switch to microtasks (let serialize run)
|
|
||||||
clock.tick(0)
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
// Click cancel
|
|
||||||
await wrapper.find('.dialog-buttons-container button.secondary').trigger('click')
|
|
||||||
|
|
||||||
// The dialog is not shown...
|
|
||||||
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(false)
|
|
||||||
// copyToClipboard is not called
|
|
||||||
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
116
tests/views/Main/Workspace/Tabs/Tab/RunResult/Record.spec.js
Normal file
116
tests/views/Main/Workspace/Tabs/Tab/RunResult/Record.spec.js
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { expect } from 'chai'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import Record from '@/views/Main/Workspace/Tabs/Tab/RunResult/Record'
|
||||||
|
|
||||||
|
describe('Record.vue', () => {
|
||||||
|
it('shows record with selected cell', async () => {
|
||||||
|
const wrapper = mount(Record, {
|
||||||
|
propsData: {
|
||||||
|
dataSet: {
|
||||||
|
columns: ['id', 'name'],
|
||||||
|
values: {
|
||||||
|
id: [1, 2],
|
||||||
|
name: ['foo', 'bar']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rowIndex: 1,
|
||||||
|
selectedColumnIndex: 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const rows = wrapper.findAll('tbody tr')
|
||||||
|
expect(rows).to.have.lengthOf(2)
|
||||||
|
expect(rows.at(0).findAll('th').at(0).text()).to.equals('id')
|
||||||
|
expect(rows.at(0).findAll('td').at(0).text()).to.equals('2')
|
||||||
|
expect(rows.at(1).findAll('th').at(0).text()).to.equals('name')
|
||||||
|
expect(rows.at(1).findAll('td').at(0).text()).to.equals('bar')
|
||||||
|
|
||||||
|
const selectedCell = wrapper
|
||||||
|
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||||
|
expect(selectedCell.text()).to.equals('bar')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('switches to the next or previous row', async () => {
|
||||||
|
const wrapper = mount(Record, {
|
||||||
|
propsData: {
|
||||||
|
dataSet: {
|
||||||
|
columns: ['id', 'name'],
|
||||||
|
values: {
|
||||||
|
id: [1, 2, 3],
|
||||||
|
name: ['foo', 'bar', 'baz']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rowIndex: 0,
|
||||||
|
selectedColumnIndex: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let rows = wrapper.findAll('tbody tr')
|
||||||
|
expect(rows).to.have.lengthOf(2)
|
||||||
|
expect(rows.at(0).findAll('td').at(0).text()).to.equals('1')
|
||||||
|
expect(rows.at(1).findAll('td').at(0).text()).to.equals('foo')
|
||||||
|
let selectedCell = wrapper
|
||||||
|
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||||
|
expect(selectedCell.text()).to.equals('1')
|
||||||
|
|
||||||
|
await wrapper.find('.next').trigger('click')
|
||||||
|
|
||||||
|
rows = wrapper.findAll('tbody tr')
|
||||||
|
expect(rows.at(0).findAll('td').at(0).text()).to.equals('2')
|
||||||
|
expect(rows.at(1).findAll('td').at(0).text()).to.equals('bar')
|
||||||
|
selectedCell = wrapper
|
||||||
|
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||||
|
expect(selectedCell.text()).to.equals('2')
|
||||||
|
|
||||||
|
await wrapper.find('.prev').trigger('click')
|
||||||
|
|
||||||
|
rows = wrapper.findAll('tbody tr')
|
||||||
|
expect(rows.at(0).findAll('td').at(0).text()).to.equals('1')
|
||||||
|
expect(rows.at(1).findAll('td').at(0).text()).to.equals('foo')
|
||||||
|
selectedCell = wrapper
|
||||||
|
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||||
|
expect(selectedCell.text()).to.equals('1')
|
||||||
|
|
||||||
|
await wrapper.find('.last').trigger('click')
|
||||||
|
|
||||||
|
rows = wrapper.findAll('tbody tr')
|
||||||
|
expect(rows.at(0).findAll('td').at(0).text()).to.equals('3')
|
||||||
|
expect(rows.at(1).findAll('td').at(0).text()).to.equals('baz')
|
||||||
|
selectedCell = wrapper
|
||||||
|
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||||
|
expect(selectedCell.text()).to.equals('3')
|
||||||
|
|
||||||
|
await wrapper.find('.first').trigger('click')
|
||||||
|
|
||||||
|
rows = wrapper.findAll('tbody tr')
|
||||||
|
expect(rows.at(0).findAll('td').at(0).text()).to.equals('1')
|
||||||
|
expect(rows.at(1).findAll('td').at(0).text()).to.equals('foo')
|
||||||
|
selectedCell = wrapper
|
||||||
|
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||||
|
expect(selectedCell.text()).to.equals('1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes selection when click on selected cell', async () => {
|
||||||
|
const wrapper = mount(Record, {
|
||||||
|
propsData: {
|
||||||
|
dataSet: {
|
||||||
|
columns: ['id', 'name'],
|
||||||
|
values: {
|
||||||
|
id: [1, 2],
|
||||||
|
name: ['foo', 'bar']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rowIndex: 1,
|
||||||
|
selectedColumnIndex: 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedCell = wrapper
|
||||||
|
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||||
|
await selectedCell.trigger('click')
|
||||||
|
|
||||||
|
const selectedCellAfterClick = wrapper
|
||||||
|
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||||
|
expect(selectedCellAfterClick.exists()).to.equals(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
348
tests/views/Main/Workspace/Tabs/Tab/RunResult/RunResult.spec.js
Normal file
348
tests/views/Main/Workspace/Tabs/Tab/RunResult/RunResult.spec.js
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
import { expect } from 'chai'
|
||||||
|
import { mount, createWrapper } from '@vue/test-utils'
|
||||||
|
import RunResult from '@/views/Main/Workspace/Tabs/Tab/RunResult'
|
||||||
|
import csv from '@/lib/csv'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
|
||||||
|
describe('RunResult.vue', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
sinon.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows alert when ClipboardItem is not supported', async () => {
|
||||||
|
const ClipboardItem = window.ClipboardItem
|
||||||
|
delete window.ClipboardItem
|
||||||
|
sinon.spy(window, 'alert')
|
||||||
|
const wrapper = mount(RunResult, {
|
||||||
|
propsData: {
|
||||||
|
tab: { id: 1 },
|
||||||
|
result: {
|
||||||
|
columns: ['id', 'name'],
|
||||||
|
values: {
|
||||||
|
id: [1],
|
||||||
|
name: ['foo']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const copyBtn = createWrapper(wrapper.findComponent({ name: 'clipboardIcon' }).vm.$parent)
|
||||||
|
await copyBtn.trigger('click')
|
||||||
|
|
||||||
|
expect(
|
||||||
|
window.alert.calledOnceWith(
|
||||||
|
"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.'
|
||||||
|
)
|
||||||
|
).to.equal(true)
|
||||||
|
|
||||||
|
window.ClipboardItem = ClipboardItem
|
||||||
|
})
|
||||||
|
|
||||||
|
it('copy to clipboard more than 1 sec', async () => {
|
||||||
|
sinon.stub(window.navigator.clipboard, 'writeText').resolves()
|
||||||
|
const clock = sinon.useFakeTimers()
|
||||||
|
const wrapper = mount(RunResult, {
|
||||||
|
propsData: {
|
||||||
|
tab: { id: 1 },
|
||||||
|
result: {
|
||||||
|
columns: ['id', 'name'],
|
||||||
|
values: {
|
||||||
|
id: [1],
|
||||||
|
name: ['foo']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
sinon.stub(csv, 'serialize').callsFake(() => {
|
||||||
|
clock.tick(5000)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Click copy to clipboard
|
||||||
|
const copyBtn = createWrapper(wrapper.findComponent({ name: 'clipboardIcon' }).vm.$parent)
|
||||||
|
await copyBtn.trigger('click')
|
||||||
|
|
||||||
|
// The dialog is shown...
|
||||||
|
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(true)
|
||||||
|
|
||||||
|
// ... with Building message...
|
||||||
|
expect(wrapper.find('.dialog-body').text()).to.equal('Building CSV...')
|
||||||
|
|
||||||
|
// Switch to microtasks (let serialize run)
|
||||||
|
clock.tick(0)
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
// The dialog is shown...
|
||||||
|
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(true)
|
||||||
|
|
||||||
|
// ... with Ready message...
|
||||||
|
expect(wrapper.find('.dialog-body').text()).to.equal('CSV is ready')
|
||||||
|
|
||||||
|
// Click copy
|
||||||
|
await wrapper.find('.dialog-buttons-container button.primary').trigger('click')
|
||||||
|
|
||||||
|
// The dialog is not shown...
|
||||||
|
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('copy to clipboard less than 1 sec', async () => {
|
||||||
|
sinon.stub(window.navigator.clipboard, 'writeText').resolves()
|
||||||
|
const clock = sinon.useFakeTimers()
|
||||||
|
const wrapper = mount(RunResult, {
|
||||||
|
propsData: {
|
||||||
|
tab: { id: 1 },
|
||||||
|
result: {
|
||||||
|
columns: ['id', 'name'],
|
||||||
|
values: {
|
||||||
|
id: [1],
|
||||||
|
name: ['foo']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
sinon.spy(wrapper.vm, 'copyToClipboard')
|
||||||
|
sinon.stub(csv, 'serialize').callsFake(() => {
|
||||||
|
clock.tick(500)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Click copy to clipboard
|
||||||
|
const copyBtn = createWrapper(wrapper.findComponent({ name: 'clipboardIcon' }).vm.$parent)
|
||||||
|
await copyBtn.trigger('click')
|
||||||
|
|
||||||
|
// Switch to microtasks (let serialize run)
|
||||||
|
clock.tick(0)
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
// The dialog is not shown...
|
||||||
|
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(false)
|
||||||
|
// copyToClipboard is called
|
||||||
|
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cancel long copy', async () => {
|
||||||
|
sinon.stub(window.navigator.clipboard, 'writeText').resolves()
|
||||||
|
const clock = sinon.useFakeTimers()
|
||||||
|
const wrapper = mount(RunResult, {
|
||||||
|
propsData: {
|
||||||
|
tab: { id: 1 },
|
||||||
|
result: {
|
||||||
|
columns: ['id', 'name'],
|
||||||
|
values: {
|
||||||
|
id: [1],
|
||||||
|
name: ['foo']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
sinon.spy(wrapper.vm, 'copyToClipboard')
|
||||||
|
sinon.stub(csv, 'serialize').callsFake(() => {
|
||||||
|
clock.tick(5000)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Click copy to clipboard
|
||||||
|
const copyBtn = createWrapper(wrapper.findComponent({ name: 'clipboardIcon' }).vm.$parent)
|
||||||
|
await copyBtn.trigger('click')
|
||||||
|
|
||||||
|
// Switch to microtasks (let serialize run)
|
||||||
|
clock.tick(0)
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
// Click cancel
|
||||||
|
await wrapper.find('.dialog-buttons-container button.secondary').trigger('click')
|
||||||
|
|
||||||
|
// The dialog is not shown...
|
||||||
|
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(false)
|
||||||
|
// copyToClipboard is not called
|
||||||
|
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows value of selected cell - result set', async () => {
|
||||||
|
const wrapper = mount(RunResult, {
|
||||||
|
propsData: {
|
||||||
|
tab: { id: 1 },
|
||||||
|
result: {
|
||||||
|
columns: ['id', 'name'],
|
||||||
|
values: {
|
||||||
|
id: [1, 2],
|
||||||
|
name: ['foo', 'bar']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Open cell value panel
|
||||||
|
const viewValueBtn = createWrapper(
|
||||||
|
wrapper.findComponent({ name: 'viewCellValueIcon' }).vm.$parent
|
||||||
|
)
|
||||||
|
await viewValueBtn.trigger('click')
|
||||||
|
|
||||||
|
/*
|
||||||
|
Result set:
|
||||||
|
|1 | foo
|
||||||
|
+--+-----
|
||||||
|
|2 | bar
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Click on '1' cell
|
||||||
|
const rows = wrapper.findAll('table tbody tr')
|
||||||
|
await rows.at(0).findAll('td').at(0).trigger('click')
|
||||||
|
|
||||||
|
expect(wrapper.find('.value-body').text()).to.equals('1')
|
||||||
|
|
||||||
|
// Go to 'foo' with right arrow key
|
||||||
|
await wrapper.find('table').trigger('keydown.right')
|
||||||
|
expect(wrapper.find('.value-body').text()).to.equals('foo')
|
||||||
|
|
||||||
|
// Go to 'bar' with down arrow key
|
||||||
|
await wrapper.find('table').trigger('keydown.down')
|
||||||
|
expect(wrapper.find('.value-body').text()).to.equals('bar')
|
||||||
|
|
||||||
|
// Go to '2' with left arrow key
|
||||||
|
await wrapper.find('table').trigger('keydown.left')
|
||||||
|
expect(wrapper.find('.value-body').text()).to.equals('2')
|
||||||
|
|
||||||
|
// Go to '1' with up arrow key
|
||||||
|
await wrapper.find('table').trigger('keydown.up')
|
||||||
|
expect(wrapper.find('.value-body').text()).to.equals('1')
|
||||||
|
|
||||||
|
// Click on 'bar' cell
|
||||||
|
await rows.at(1).findAll('td').at(1).trigger('click')
|
||||||
|
|
||||||
|
expect(wrapper.find('.value-body').text()).to.equals('bar')
|
||||||
|
|
||||||
|
// Click on 'bar' cell again
|
||||||
|
await rows.at(1).findAll('td').at(1).trigger('click')
|
||||||
|
|
||||||
|
expect(wrapper.find('.value-viewer-container .table-preview').text())
|
||||||
|
.to.equals('No cell selected to view')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows value of selected cell - record view', async () => {
|
||||||
|
const wrapper = mount(RunResult, {
|
||||||
|
propsData: {
|
||||||
|
tab: { id: 1 },
|
||||||
|
result: {
|
||||||
|
columns: ['id', 'name'],
|
||||||
|
values: {
|
||||||
|
id: [1, 2],
|
||||||
|
name: ['foo', 'bar']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Open cell value panel
|
||||||
|
const viewValueBtn = createWrapper(
|
||||||
|
wrapper.findComponent({ name: 'viewCellValueIcon' }).vm.$parent
|
||||||
|
)
|
||||||
|
await viewValueBtn.trigger('click')
|
||||||
|
|
||||||
|
// Go to record view
|
||||||
|
const vierRecordBtn = createWrapper(
|
||||||
|
wrapper.findComponent({ name: 'rowIcon' }).vm.$parent
|
||||||
|
)
|
||||||
|
await vierRecordBtn.trigger('click')
|
||||||
|
|
||||||
|
/*
|
||||||
|
Record 1:
|
||||||
|
|id | 1
|
||||||
|
+-----+-----
|
||||||
|
|name | foo
|
||||||
|
|
||||||
|
Record 2:
|
||||||
|
|id | 2
|
||||||
|
+-----+-----
|
||||||
|
|name | bar
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Click '1' is selected by default
|
||||||
|
expect(wrapper.find('.value-body').text()).to.equals('1')
|
||||||
|
|
||||||
|
// Go to 'foo' with down arrow key
|
||||||
|
await wrapper.find('table').trigger('keydown.down')
|
||||||
|
expect(wrapper.find('.value-body').text()).to.equals('foo')
|
||||||
|
|
||||||
|
// Go to next record
|
||||||
|
await wrapper.find('.icon-btn.next').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.find('.value-body').text()).to.equals('bar')
|
||||||
|
|
||||||
|
// Go to '2' with up arrow key
|
||||||
|
await wrapper.find('table').trigger('keydown.up')
|
||||||
|
expect(wrapper.find('.value-body').text()).to.equals('2')
|
||||||
|
|
||||||
|
// Go to prev record
|
||||||
|
await wrapper.find('.icon-btn.prev').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.find('.value-body').text()).to.equals('1')
|
||||||
|
|
||||||
|
// Click on 'foo' cell
|
||||||
|
const rows = wrapper.findAll('table tbody tr')
|
||||||
|
await rows.at(1).find('td').trigger('click')
|
||||||
|
expect(wrapper.find('.value-body').text()).to.equals('foo')
|
||||||
|
|
||||||
|
// Click on 'foo' cell again
|
||||||
|
await rows.at(1).find('td').trigger('click')
|
||||||
|
expect(wrapper.find('.value-viewer-container .table-preview').text())
|
||||||
|
.to.equals('No cell selected to view')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps selected cell when switch between record and regular view', async () => {
|
||||||
|
const wrapper = mount(RunResult, {
|
||||||
|
propsData: {
|
||||||
|
tab: { id: 1 },
|
||||||
|
result: {
|
||||||
|
columns: ['id', 'name'],
|
||||||
|
values: {
|
||||||
|
id: [...Array(30)].map((x, i) => i),
|
||||||
|
name: [...Array(30)].map((x, i) => `name-${i}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Open cell value panel
|
||||||
|
const viewValueBtn = createWrapper(
|
||||||
|
wrapper.findComponent({ name: 'viewCellValueIcon' }).vm.$parent
|
||||||
|
)
|
||||||
|
await viewValueBtn.trigger('click')
|
||||||
|
|
||||||
|
// Click on 'name-1' cell
|
||||||
|
const rows = wrapper.findAll('table tbody tr')
|
||||||
|
await rows.at(1).findAll('td').at(1).trigger('click')
|
||||||
|
|
||||||
|
expect(wrapper.find('.value-body').text()).to.equals('name-1')
|
||||||
|
|
||||||
|
// Go to record view
|
||||||
|
const vierRecordBtn = createWrapper(
|
||||||
|
wrapper.findComponent({ name: 'rowIcon' }).vm.$parent
|
||||||
|
)
|
||||||
|
await vierRecordBtn.trigger('click')
|
||||||
|
|
||||||
|
// 'name-1' is selected
|
||||||
|
expect(wrapper.find('.value-body').text()).to.equals('name-1')
|
||||||
|
let selectedCell = wrapper
|
||||||
|
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||||
|
expect(selectedCell.text()).to.equals('name-1')
|
||||||
|
|
||||||
|
// Go to last record
|
||||||
|
await wrapper.find('.icon-btn.last').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.find('.value-body').text()).to.equals('name-29')
|
||||||
|
|
||||||
|
// Go to '29' with up arrow key
|
||||||
|
await wrapper.find('table').trigger('keydown.up')
|
||||||
|
expect(wrapper.find('.value-body').text()).to.equals('29')
|
||||||
|
|
||||||
|
// Go to regular view
|
||||||
|
await vierRecordBtn.trigger('click')
|
||||||
|
|
||||||
|
// '29' is selected
|
||||||
|
expect(wrapper.find('.value-body').text()).to.equals('29')
|
||||||
|
selectedCell = wrapper
|
||||||
|
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||||
|
expect(selectedCell.text()).to.equals('29')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { expect } from 'chai'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import ValueViewer from '@/views/Main/Workspace/Tabs/Tab/RunResult/ValueViewer'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
|
||||||
|
describe('ValueViewer.vue', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
sinon.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows value in text mode', async () => {
|
||||||
|
const wrapper = mount(ValueViewer, {
|
||||||
|
propsData: {
|
||||||
|
cellValue: 'foo'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.find('.value-body').text()).to.equals('foo')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error in json mode if the value is not json', async () => {
|
||||||
|
const wrapper = mount(ValueViewer, {
|
||||||
|
propsData: {
|
||||||
|
cellValue: 'foo'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await wrapper.find('button.json').trigger('click')
|
||||||
|
expect(wrapper.find('.value-body').text()).to.equals('Can\'t parse JSON.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('copy to clipboard', async () => {
|
||||||
|
sinon.stub(window.navigator.clipboard, 'writeText').resolves()
|
||||||
|
const wrapper = mount(ValueViewer, {
|
||||||
|
propsData: {
|
||||||
|
cellValue: 'foo'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.find('button.copy').trigger('click')
|
||||||
|
|
||||||
|
expect(window.navigator.clipboard.writeText.calledOnceWith('foo'))
|
||||||
|
.to.equal(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -31,13 +31,23 @@ describe('Tab.vue', () => {
|
|||||||
store,
|
store,
|
||||||
stubs: ['chart'],
|
stubs: ['chart'],
|
||||||
propsData: {
|
propsData: {
|
||||||
|
tab: {
|
||||||
id: 1,
|
id: 1,
|
||||||
initName: 'foo',
|
name: 'foo',
|
||||||
initQuery: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
initViewType: 'chart',
|
viewType: 'chart',
|
||||||
initViewOptions: [],
|
viewOptions: {},
|
||||||
tabIndex: 0,
|
layout: {
|
||||||
isPredefined: false
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -60,7 +70,23 @@ describe('Tab.vue', () => {
|
|||||||
store,
|
store,
|
||||||
stubs: ['chart'],
|
stubs: ['chart'],
|
||||||
propsData: {
|
propsData: {
|
||||||
id: 1
|
tab: {
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(wrapper.find('.tab-content-container').isVisible()).to.equal(false)
|
expect(wrapper.find('.tab-content-container').isVisible()).to.equal(false)
|
||||||
@@ -79,40 +105,51 @@ describe('Tab.vue', () => {
|
|||||||
store,
|
store,
|
||||||
stubs: ['chart'],
|
stubs: ['chart'],
|
||||||
propsData: {
|
propsData: {
|
||||||
id: 1
|
tab: {
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(wrapper.find('.tab-content-container').isVisible()).to.equal(false)
|
expect(wrapper.find('.tab-content-container').isVisible()).to.equal(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Calls setCurrentTab when becomes active', async () => {
|
|
||||||
// mock store state
|
|
||||||
const state = {
|
|
||||||
currentTabId: 0
|
|
||||||
}
|
|
||||||
sinon.spy(mutations, 'setCurrentTab')
|
|
||||||
const store = new Vuex.Store({ state, mutations })
|
|
||||||
|
|
||||||
// mount the component
|
|
||||||
const wrapper = mount(Tab, {
|
|
||||||
store,
|
|
||||||
stubs: ['chart'],
|
|
||||||
propsData: {
|
|
||||||
id: 1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
state.currentTabId = 1
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
expect(mutations.setCurrentTab.calledOnceWith(state, wrapper.vm)).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Update tab state when a query is changed', async () => {
|
it('Update tab state when a query is changed', async () => {
|
||||||
// mock store state
|
// mock store state
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [
|
tabs: [
|
||||||
{ id: 1, name: 'foo', query: 'SELECT * FROM foo', chart: [], isSaved: true }
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
],
|
],
|
||||||
currentTabId: 1
|
currentTabId: 1
|
||||||
}
|
}
|
||||||
@@ -124,13 +161,7 @@ describe('Tab.vue', () => {
|
|||||||
store,
|
store,
|
||||||
stubs: ['chart'],
|
stubs: ['chart'],
|
||||||
propsData: {
|
propsData: {
|
||||||
id: 1,
|
tab: state.tabs[0]
|
||||||
initName: 'foo',
|
|
||||||
initQuery: 'SELECT * FROM foo',
|
|
||||||
initViewOptions: [],
|
|
||||||
initViewType: 'chart',
|
|
||||||
tabIndex: 0,
|
|
||||||
isPredefined: false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await wrapper.findComponent({ name: 'SqlEditor' }).vm.$emit('input', ' limit 100')
|
await wrapper.findComponent({ name: 'SqlEditor' }).vm.$emit('input', ' limit 100')
|
||||||
@@ -141,7 +172,24 @@ describe('Tab.vue', () => {
|
|||||||
// mock store state
|
// mock store state
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [
|
tabs: [
|
||||||
{ id: 1, name: 'foo', query: 'SELECT * FROM foo', chart: [], isSaved: true }
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
],
|
],
|
||||||
currentTabId: 1
|
currentTabId: 1
|
||||||
}
|
}
|
||||||
@@ -153,13 +201,7 @@ describe('Tab.vue', () => {
|
|||||||
store,
|
store,
|
||||||
stubs: ['chart'],
|
stubs: ['chart'],
|
||||||
propsData: {
|
propsData: {
|
||||||
id: 1,
|
tab: state.tabs[0]
|
||||||
initName: 'foo',
|
|
||||||
initQuery: 'SELECT * FROM foo',
|
|
||||||
initViewOptions: [],
|
|
||||||
initViewType: 'chart',
|
|
||||||
tabIndex: 0,
|
|
||||||
isPredefined: false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await wrapper.findComponent({ name: 'DataView' }).vm.$emit('update')
|
await wrapper.findComponent({ name: 'DataView' }).vm.$emit('update')
|
||||||
@@ -169,29 +211,38 @@ describe('Tab.vue', () => {
|
|||||||
it('Shows .result-in-progress message when executing query', async () => {
|
it('Shows .result-in-progress message when executing query', async () => {
|
||||||
// mock store state
|
// mock store state
|
||||||
const state = {
|
const state = {
|
||||||
currentTabId: 1,
|
currentTabId: 1
|
||||||
db: {
|
|
||||||
execute () { return new Promise(() => {}) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = new Vuex.Store({ state, mutations })
|
const store = new Vuex.Store({ state, mutations })
|
||||||
// mount the component
|
// mount the component
|
||||||
|
const tab = {
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
const wrapper = mount(Tab, {
|
const wrapper = mount(Tab, {
|
||||||
store,
|
store,
|
||||||
stubs: ['chart'],
|
stubs: ['chart'],
|
||||||
propsData: {
|
propsData: {
|
||||||
id: 1,
|
tab
|
||||||
initName: 'foo',
|
|
||||||
initQuery: 'SELECT * FROM foo',
|
|
||||||
initViewOptions: [],
|
|
||||||
initViewType: 'chart',
|
|
||||||
tabIndex: 0,
|
|
||||||
isPredefined: false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
wrapper.vm.execute()
|
tab.isGettingResults = true
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
expect(wrapper.find('.run-result-panel .result-in-progress').isVisible()).to.equal(true)
|
expect(wrapper.find('.run-result-panel .result-in-progress').isVisible()).to.equal(true)
|
||||||
})
|
})
|
||||||
@@ -199,30 +250,42 @@ describe('Tab.vue', () => {
|
|||||||
it('Shows error when executing query ends with error', async () => {
|
it('Shows error when executing query ends with error', async () => {
|
||||||
// mock store state
|
// mock store state
|
||||||
const state = {
|
const state = {
|
||||||
currentTabId: 1,
|
currentTabId: 1
|
||||||
db: {
|
|
||||||
execute: sinon.stub().rejects(new Error('There is no table foo')),
|
|
||||||
refreshSchema: sinon.stub().resolves()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = new Vuex.Store({ state, mutations })
|
const store = new Vuex.Store({ state, mutations })
|
||||||
|
const tab = {
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
// mount the component
|
// mount the component
|
||||||
const wrapper = mount(Tab, {
|
const wrapper = mount(Tab, {
|
||||||
store,
|
store,
|
||||||
stubs: ['chart'],
|
stubs: ['chart'],
|
||||||
propsData: {
|
propsData: {
|
||||||
id: 1,
|
tab
|
||||||
initName: 'foo',
|
|
||||||
initQuery: 'SELECT * FROM foo',
|
|
||||||
initViewOptions: [],
|
|
||||||
initViewType: 'chart',
|
|
||||||
tabIndex: 0,
|
|
||||||
isPredefined: false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await wrapper.vm.execute()
|
tab.error = {
|
||||||
|
type: 'error',
|
||||||
|
message: 'There is no table foo'
|
||||||
|
}
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
expect(wrapper.find('.run-result-panel .result-before').isVisible()).to.equal(false)
|
expect(wrapper.find('.run-result-panel .result-before').isVisible()).to.equal(false)
|
||||||
expect(wrapper.find('.run-result-panel .result-in-progress').exists()).to.equal(false)
|
expect(wrapper.find('.run-result-panel .result-in-progress').exists()).to.equal(false)
|
||||||
expect(wrapper.findComponent({ name: 'logs' }).isVisible()).to.equal(true)
|
expect(wrapper.findComponent({ name: 'logs' }).isVisible()).to.equal(true)
|
||||||
@@ -239,11 +302,26 @@ describe('Tab.vue', () => {
|
|||||||
}
|
}
|
||||||
// mock store state
|
// mock store state
|
||||||
const state = {
|
const state = {
|
||||||
currentTabId: 1,
|
currentTabId: 1
|
||||||
db: {
|
|
||||||
execute: sinon.stub().resolves(result),
|
|
||||||
refreshSchema: sinon.stub().resolves()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tab = {
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = new Vuex.Store({ state, mutations })
|
const store = new Vuex.Store({ state, mutations })
|
||||||
@@ -253,83 +331,50 @@ describe('Tab.vue', () => {
|
|||||||
store,
|
store,
|
||||||
stubs: ['chart'],
|
stubs: ['chart'],
|
||||||
propsData: {
|
propsData: {
|
||||||
id: 1,
|
tab
|
||||||
initName: 'foo',
|
|
||||||
initQuery: 'SELECT * FROM foo',
|
|
||||||
initViewOptions: [],
|
|
||||||
initViewType: 'chart',
|
|
||||||
tabIndex: 0,
|
|
||||||
isPredefined: false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await wrapper.vm.execute()
|
tab.result = result
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
expect(wrapper.find('.run-result-panel .result-before').isVisible()).to.equal(false)
|
expect(wrapper.find('.run-result-panel .result-before').isVisible()).to.equal(false)
|
||||||
expect(wrapper.find('.run-result-panel .result-in-progress').exists()).to.equal(false)
|
expect(wrapper.find('.run-result-panel .result-in-progress').exists()).to.equal(false)
|
||||||
expect(wrapper.findComponent({ name: 'logs' }).exists()).to.equal(false)
|
expect(wrapper.findComponent({ name: 'logs' }).exists()).to.equal(false)
|
||||||
expect(wrapper.findComponent({ name: 'SqlTable' }).vm.dataSet).to.eql(result)
|
expect(wrapper.findComponent({ name: 'SqlTable' }).vm.dataSet).to.eql(result)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Updates schema after query execution', async () => {
|
|
||||||
const result = {
|
|
||||||
columns: ['id', 'name'],
|
|
||||||
values: {
|
|
||||||
id: [],
|
|
||||||
name: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// mock store state
|
|
||||||
const state = {
|
|
||||||
currentTabId: 1,
|
|
||||||
dbName: 'fooDb',
|
|
||||||
db: {
|
|
||||||
execute: sinon.stub().resolves(result),
|
|
||||||
refreshSchema: sinon.stub().resolves()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = new Vuex.Store({ state, mutations })
|
|
||||||
|
|
||||||
// mount the component
|
|
||||||
const wrapper = mount(Tab, {
|
|
||||||
store,
|
|
||||||
stubs: ['chart'],
|
|
||||||
propsData: {
|
|
||||||
id: 1,
|
|
||||||
initName: 'foo',
|
|
||||||
initQuery: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
|
||||||
initViewOptions: [],
|
|
||||||
initViewType: 'chart',
|
|
||||||
tabIndex: 0,
|
|
||||||
isPredefined: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.vm.execute()
|
|
||||||
expect(state.db.refreshSchema.calledOnce).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Switches views', async () => {
|
it('Switches views', async () => {
|
||||||
const state = {
|
const state = {
|
||||||
currentTabId: 1,
|
currentTabId: 1
|
||||||
db: {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = new Vuex.Store({ state, mutations })
|
const store = new Vuex.Store({ state, mutations })
|
||||||
|
|
||||||
|
const tab = {
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
|
|
||||||
const wrapper = mount(Tab, {
|
const wrapper = mount(Tab, {
|
||||||
attachTo: place,
|
attachTo: place,
|
||||||
store,
|
store,
|
||||||
stubs: ['chart'],
|
stubs: ['chart'],
|
||||||
propsData: {
|
propsData: {
|
||||||
id: 1,
|
tab
|
||||||
initName: 'foo',
|
|
||||||
initQuery: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
|
||||||
initViewOptions: [],
|
|
||||||
initViewType: 'chart',
|
|
||||||
tabIndex: 0,
|
|
||||||
isPredefined: false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -361,4 +406,119 @@ describe('Tab.vue', () => {
|
|||||||
expect(wrapper.find('.above .sql-editor-panel').exists()).to.equal(true)
|
expect(wrapper.find('.above .sql-editor-panel').exists()).to.equal(true)
|
||||||
expect(wrapper.find('.bottomPane .run-result-panel').exists()).to.equal(true)
|
expect(wrapper.find('.bottomPane .run-result-panel').exists()).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Maximize top panel if maximized panel is above', () => {
|
||||||
|
const state = {
|
||||||
|
currentTabId: 1
|
||||||
|
}
|
||||||
|
const store = new Vuex.Store({ state, mutations })
|
||||||
|
const tab = {
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
maximize: 'sqlEditor',
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = mount(Tab, {
|
||||||
|
attachTo: place,
|
||||||
|
store,
|
||||||
|
stubs: ['chart'],
|
||||||
|
propsData: {
|
||||||
|
tab
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.find('.above').element.parentElement.style.height)
|
||||||
|
.to.equal('100%')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Maximize bottom panel if maximized panel is below', () => {
|
||||||
|
const state = {
|
||||||
|
currentTabId: 1
|
||||||
|
}
|
||||||
|
const store = new Vuex.Store({ state, mutations })
|
||||||
|
const tab = {
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
maximize: 'table',
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = mount(Tab, {
|
||||||
|
attachTo: place,
|
||||||
|
store,
|
||||||
|
stubs: ['chart'],
|
||||||
|
propsData: {
|
||||||
|
tab
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.find('.bottomPane').element.parentElement.style.height)
|
||||||
|
.to.equal('100%')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Panel size is 50 is nothing to maximize', () => {
|
||||||
|
const state = {
|
||||||
|
currentTabId: 1
|
||||||
|
}
|
||||||
|
const store = new Vuex.Store({ state, mutations })
|
||||||
|
const tab = {
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = mount(Tab, {
|
||||||
|
attachTo: place,
|
||||||
|
store,
|
||||||
|
stubs: ['chart'],
|
||||||
|
propsData: {
|
||||||
|
tab
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.find('.above').element.parentElement.style.height)
|
||||||
|
.to.equal('50%')
|
||||||
|
expect(wrapper.find('.bottomPane').element.parentElement.style.height)
|
||||||
|
.to.equal('50%')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -94,8 +94,33 @@ describe('Tabs.vue', () => {
|
|||||||
// mock store state
|
// mock store state
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [
|
tabs: [
|
||||||
{ id: 1, name: 'foo', query: 'select * from foo', chart: [], isSaved: true },
|
{
|
||||||
{ id: 2, name: null, tempName: 'Untitled', query: '', chart: [], isSaved: false }
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'select * from foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isSaved: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: null,
|
||||||
|
tempName: 'Untitled',
|
||||||
|
query: '',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
],
|
],
|
||||||
currentTabId: 2
|
currentTabId: 2
|
||||||
}
|
}
|
||||||
@@ -125,8 +150,33 @@ describe('Tabs.vue', () => {
|
|||||||
// mock store state
|
// mock store state
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [
|
tabs: [
|
||||||
{ id: 1, name: 'foo', query: 'select * from foo', chart: [], isSaved: true },
|
{
|
||||||
{ id: 2, name: null, tempName: 'Untitled', query: '', chart: [], isSaved: false }
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'select * from foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isSaved: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: null,
|
||||||
|
tempName: 'Untitled',
|
||||||
|
query: '',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
],
|
],
|
||||||
currentTabId: 2
|
currentTabId: 2
|
||||||
}
|
}
|
||||||
@@ -166,8 +216,33 @@ describe('Tabs.vue', () => {
|
|||||||
// mock store state
|
// mock store state
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [
|
tabs: [
|
||||||
{ id: 1, name: 'foo', query: 'select * from foo', chart: [], isSaved: true },
|
{
|
||||||
{ id: 2, name: null, tempName: 'Untitled', query: '', chart: [], isSaved: false }
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'select * from foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isSaved: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: null,
|
||||||
|
tempName: 'Untitled',
|
||||||
|
query: '',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
],
|
],
|
||||||
currentTabId: 2
|
currentTabId: 2
|
||||||
}
|
}
|
||||||
@@ -211,8 +286,33 @@ describe('Tabs.vue', () => {
|
|||||||
// mock store state
|
// mock store state
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [
|
tabs: [
|
||||||
{ id: 1, name: 'foo', query: 'select * from foo', chart: [], isSaved: true },
|
{
|
||||||
{ id: 2, name: null, tempName: 'Untitled', query: '', chart: [], isSaved: false }
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'select * from foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isSaved: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: null,
|
||||||
|
tempName: 'Untitled',
|
||||||
|
query: '',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
],
|
],
|
||||||
currentTabId: 2
|
currentTabId: 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ describe('Workspace.vue', () => {
|
|||||||
tabs: []
|
tabs: []
|
||||||
}
|
}
|
||||||
const store = new Vuex.Store({ state, actions, mutations })
|
const store = new Vuex.Store({ state, actions, mutations })
|
||||||
|
const $route = { path: '/workspace', query: {} }
|
||||||
mount(Workspace, {
|
mount(Workspace, {
|
||||||
store,
|
store,
|
||||||
stubs: ['router-link']
|
stubs: ['router-link'],
|
||||||
|
mocks: { $route }
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(state.tabs[0].query).to.include('Your database is empty.')
|
expect(state.tabs[0].query).to.include('Your database is empty.')
|
||||||
@@ -24,4 +26,20 @@ describe('Workspace.vue', () => {
|
|||||||
expect(state.tabs[0].viewOptions).to.equal(undefined)
|
expect(state.tabs[0].viewOptions).to.equal(undefined)
|
||||||
expect(state.tabs[0].isSaved).to.equal(false)
|
expect(state.tabs[0].isSaved).to.equal(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Collapse schema if hide_schema is 1', () => {
|
||||||
|
const state = {
|
||||||
|
db: {},
|
||||||
|
tabs: []
|
||||||
|
}
|
||||||
|
const store = new Vuex.Store({ state, actions, mutations })
|
||||||
|
const $route = { path: '/workspace', query: { hide_schema: '1' } }
|
||||||
|
const vm = mount(Workspace, {
|
||||||
|
store,
|
||||||
|
stubs: ['router-link'],
|
||||||
|
mocks: { $route }
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(vm.find('#schema-container').element.offsetWidth).to.equal(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user