mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-07 02:28:54 +08:00
Compare commits
26 Commits
41e0ae7332
...
0.24.1
| 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 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sqliteviz",
|
||||
"version": "0.23.1",
|
||||
"version": "0.24.1",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -107,3 +107,9 @@ table.sqliteviz-table {
|
||||
font-size: 11px;
|
||||
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>
|
||||
<div
|
||||
:class="['icon-btn', { active }, { disabled }]"
|
||||
<button
|
||||
:class="['icon-btn', { active }]"
|
||||
:disabled="disabled"
|
||||
@click="onClick"
|
||||
@mouseenter="showTooltip($event, tooltipPosition)"
|
||||
@mouseleave="hideTooltip"
|
||||
@@ -12,7 +13,7 @@
|
||||
<span v-if="tooltip" class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
||||
{{ tooltip }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -38,11 +39,12 @@ export default {
|
||||
box-sizing: border-box;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
.icon-btn:hover {
|
||||
border: 1px solid var(--color-border);
|
||||
@@ -56,12 +58,12 @@ export default {
|
||||
fill: var(--color-accent);
|
||||
}
|
||||
|
||||
.disabled.icon-btn .icon >>> path,
|
||||
.disabled.icon-btn .icon >>> circle {
|
||||
.icon-btn:disabled .icon >>> path,
|
||||
.icon-btn:disabled .icon >>> circle {
|
||||
fill: var(--color-border);
|
||||
}
|
||||
|
||||
.disabled.icon-btn {
|
||||
.icon-btn:disabled {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,12 @@
|
||||
ref="table-container"
|
||||
@scroll="onScrollTable"
|
||||
>
|
||||
<table ref="table" class="sqliteviz-table">
|
||||
<table
|
||||
ref="table"
|
||||
class="sqliteviz-table"
|
||||
tabindex="0"
|
||||
@keydown="onTableKeydown"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="(th, index) in columns" :key="index" ref="th">
|
||||
@@ -28,9 +33,18 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<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">
|
||||
{{ dataSet.values[col][rowIndex - 1 + currentPageData.start] }}
|
||||
{{ getCellText(col, rowIndex) }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -44,7 +58,11 @@
|
||||
<span v-if="preview">for preview</span>
|
||||
<span v-if="time">in {{ time }}</span>
|
||||
</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>
|
||||
</template>
|
||||
@@ -62,14 +80,20 @@ export default {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
preview: Boolean
|
||||
page: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
preview: Boolean,
|
||||
selectedCellCoordinates: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
header: null,
|
||||
tableWidth: null,
|
||||
currentPage: 1,
|
||||
resizeObserver: null
|
||||
currentPage: this.page,
|
||||
resizeObserver: null,
|
||||
selectedCellElement: null
|
||||
}
|
||||
},
|
||||
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: {
|
||||
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 () {
|
||||
this.tableWidth = this.$refs['table-container'].offsetWidth
|
||||
this.$nextTick(() => {
|
||||
@@ -110,18 +167,95 @@ export default {
|
||||
},
|
||||
onScrollTable () {
|
||||
this.$refs['header-container'].scrollLeft = this.$refs['table-container'].scrollLeft
|
||||
},
|
||||
onTableKeydown (e) {
|
||||
const keyCodeMap = {
|
||||
37: 'left',
|
||||
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)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth)
|
||||
this.resizeObserver.observe(this.$refs.table)
|
||||
this.calculateHeadersWidth()
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.resizeObserver.unobserve(this.$refs.table)
|
||||
},
|
||||
watch: {
|
||||
currentPageData: 'calculateHeadersWidth',
|
||||
currentPageData () {
|
||||
this.calculateHeadersWidth()
|
||||
this.selectCell(null)
|
||||
},
|
||||
dataSet () {
|
||||
this.currentPage = 1
|
||||
}
|
||||
@@ -130,4 +264,13 @@ export default {
|
||||
</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);
|
||||
}
|
||||
</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>
|
||||
@@ -8,11 +8,11 @@ function _getDataSourcesFromSqlResult (sqlResult) {
|
||||
if (!sqlResult) {
|
||||
return {}
|
||||
}
|
||||
const dataSorces = {}
|
||||
const dataSources = {}
|
||||
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 {
|
||||
|
||||
@@ -2,9 +2,11 @@ import Lib from 'plotly.js/src/lib'
|
||||
import dataUrlToBlob from 'dataurl-to-blob'
|
||||
|
||||
export default {
|
||||
async copyCsv (str) {
|
||||
async copyText (str, notifyMessage) {
|
||||
await navigator.clipboard.writeText(str)
|
||||
Lib.notifier('CSV copied to clipboard successfully', 'long')
|
||||
if (notifyMessage) {
|
||||
Lib.notifier(notifyMessage, 'long')
|
||||
}
|
||||
},
|
||||
|
||||
async copyImage (source) {
|
||||
|
||||
@@ -36,9 +36,11 @@ export default {
|
||||
state.currentTabId = state.tabs[index - 1].id
|
||||
} else {
|
||||
state.currentTabId = null
|
||||
state.currentTab = null
|
||||
state.untitledLastIndex = 0
|
||||
}
|
||||
state.currentTab = state.currentTabId
|
||||
? state.tabs.find(tab => tab.id === state.currentTabId)
|
||||
: null
|
||||
}
|
||||
state.tabs.splice(index, 1)
|
||||
},
|
||||
|
||||
@@ -77,6 +77,7 @@ export default {
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
this.$emit('update:importToSvgEnabled', true)
|
||||
},
|
||||
mounted () {
|
||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
>
|
||||
<png-icon />
|
||||
</icon-button>
|
||||
|
||||
<icon-button
|
||||
:disabled="!importToSvgEnabled"
|
||||
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>
|
||||
<div class="run-result-panel" ref="runResultPanel">
|
||||
<div class="run-result-panel-content">
|
||||
<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"
|
||||
:data-set="result"
|
||||
:time="time"
|
||||
:pageSize="pageSize"
|
||||
class="straight"
|
||||
/>
|
||||
</div>
|
||||
<component
|
||||
:is="viewValuePanelVisible ? 'splitpanes':'div'"
|
||||
:before="{ size: 50, max: 100 }"
|
||||
:after="{ size: 50, max: 100 }"
|
||||
:default="{ before: 50, after: 50 }"
|
||||
class="run-result-panel-content"
|
||||
>
|
||||
<template #left-pane>
|
||||
<div :id="'run-result-left-pane-'+tab.id" class="result-set-container"/>
|
||||
</template>
|
||||
<div :id="'run-result-result-set-'+tab.id" class="result-set-container"/>
|
||||
<template #right-pane v-if="viewValuePanelVisible">
|
||||
<div class="value-viewer-container">
|
||||
<value-viewer
|
||||
v-show="selectedCell"
|
||||
:cellValue="selectedCell
|
||||
? result.values[result.columns[selectedCell.dataset.col]][selectedCell.dataset.row]
|
||||
: ''"
|
||||
/>
|
||||
<div v-show="!selectedCell" class="table-preview">
|
||||
No cell selected to view
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</component>
|
||||
|
||||
<side-tool-bar @switchTo="$emit('switchTo', $event)" panel="table">
|
||||
<icon-button
|
||||
:disabled="!result"
|
||||
@@ -44,6 +44,26 @@
|
||||
>
|
||||
<clipboard-icon/>
|
||||
</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>
|
||||
|
||||
<loading-dialog
|
||||
@@ -56,6 +76,48 @@
|
||||
@action="copyToClipboard"
|
||||
@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>
|
||||
</template>
|
||||
|
||||
@@ -63,9 +125,12 @@
|
||||
import Logs from '@/components/Logs'
|
||||
import SqlTable from '@/components/SqlTable'
|
||||
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 ClipboardIcon from '@/components/svg/clipboard'
|
||||
import ViewCellValueIcon from '@/components/svg/viewCellValue'
|
||||
import RowIcon from '@/components/svg/row'
|
||||
import IconButton from '@/components/IconButton'
|
||||
import csv from '@/lib/csv'
|
||||
import fIo from '@/lib/utils/fileIo'
|
||||
@@ -73,16 +138,30 @@ import cIo from '@/lib/utils/clipboardIo'
|
||||
import time from '@/lib/utils/time'
|
||||
import loadingDialog from '@/components/LoadingDialog'
|
||||
import events from '@/lib/utils/events'
|
||||
import Teleport from 'vue2-teleport'
|
||||
import ValueViewer from './ValueViewer'
|
||||
import Record from './Record/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'RunResult',
|
||||
props: ['result', 'isGettingResults', 'error', 'time'],
|
||||
props: {
|
||||
tab: Object,
|
||||
result: Object,
|
||||
isGettingResults: Boolean,
|
||||
error: Object,
|
||||
time: [String, Number]
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
resizeObserver: null,
|
||||
pageSize: 20,
|
||||
preparingCopy: false,
|
||||
dataToCopy: null
|
||||
dataToCopy: null,
|
||||
viewValuePanelVisible: false,
|
||||
selectedCell: null,
|
||||
viewRecord: false,
|
||||
defaultPage: 1,
|
||||
defaultSelectedCell: null
|
||||
}
|
||||
},
|
||||
components: {
|
||||
@@ -93,7 +172,23 @@ export default {
|
||||
ExportToCsvIcon,
|
||||
IconButton,
|
||||
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 () {
|
||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||
@@ -103,6 +198,12 @@ export default {
|
||||
beforeDestroy () {
|
||||
this.resizeObserver.unobserve(this.$refs.runResultPanel)
|
||||
},
|
||||
watch: {
|
||||
result () {
|
||||
this.defaultSelectedCell = null
|
||||
this.selectedCell = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleResize () {
|
||||
this.calculatePageSize()
|
||||
@@ -160,13 +261,35 @@ export default {
|
||||
},
|
||||
|
||||
copyToClipboard () {
|
||||
cIo.copyCsv(this.dataToCopy)
|
||||
cIo.copyText(this.dataToCopy, 'CSV copied to clipboard successfully')
|
||||
this.$modal.hide('prepareCSVCopy')
|
||||
},
|
||||
|
||||
cancelCopy () {
|
||||
this.dataToCopy = null
|
||||
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 {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.result-set-container,
|
||||
.result-set-container > div {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.value-viewer-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--color-white);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table-preview {
|
||||
position: absolute;
|
||||
@@ -194,6 +329,7 @@ export default {
|
||||
transform: translate(-50%, -50%);
|
||||
color: var(--color-text-base);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.result-in-progress {
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
<teleport :to="`#${tab.layout.table}-${tab.id}`">
|
||||
<run-result
|
||||
:tab="tab"
|
||||
:result="tab.result"
|
||||
:is-getting-results="tab.isGettingResults"
|
||||
:error="tab.error"
|
||||
|
||||
@@ -7,9 +7,9 @@ describe('clipboardIo.js', async () => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('copyCsv', async () => {
|
||||
it('copyText', async () => {
|
||||
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'))
|
||||
})
|
||||
|
||||
|
||||
@@ -176,13 +176,15 @@ describe('mutations', () => {
|
||||
|
||||
const state = {
|
||||
tabs: [tab1, tab2],
|
||||
currentTabId: 1
|
||||
currentTabId: 1,
|
||||
currentTab: tab1
|
||||
}
|
||||
|
||||
deleteTab(state, tab1)
|
||||
expect(state.tabs).to.have.lengthOf(1)
|
||||
expect(state.tabs[0].id).to.equal(2)
|
||||
expect(state.currentTabId).to.equal(2)
|
||||
expect(state.currentTab).to.eql(tab2)
|
||||
})
|
||||
|
||||
it('deleteTab - opened, last', () => {
|
||||
@@ -208,13 +210,15 @@ describe('mutations', () => {
|
||||
|
||||
const state = {
|
||||
tabs: [tab1, tab2],
|
||||
currentTabId: 2
|
||||
currentTabId: 2,
|
||||
currentTab: tab2
|
||||
}
|
||||
|
||||
deleteTab(state, tab2)
|
||||
expect(state.tabs).to.have.lengthOf(1)
|
||||
expect(state.tabs[0].id).to.equal(1)
|
||||
expect(state.currentTabId).to.equal(1)
|
||||
expect(state.currentTab).to.eql(tab1)
|
||||
})
|
||||
|
||||
it('deleteTab - opened, in the middle', () => {
|
||||
@@ -250,7 +254,8 @@ describe('mutations', () => {
|
||||
|
||||
const state = {
|
||||
tabs: [tab1, tab2, tab3],
|
||||
currentTabId: 2
|
||||
currentTabId: 2,
|
||||
currentTab: tab2
|
||||
}
|
||||
|
||||
deleteTab(state, tab2)
|
||||
@@ -258,6 +263,7 @@ describe('mutations', () => {
|
||||
expect(state.tabs[0].id).to.equal(1)
|
||||
expect(state.tabs[1].id).to.equal(3)
|
||||
expect(state.currentTabId).to.equal(3)
|
||||
expect(state.currentTab).to.eql(tab3)
|
||||
})
|
||||
|
||||
it('deleteTab - opened, single', () => {
|
||||
@@ -273,12 +279,14 @@ describe('mutations', () => {
|
||||
|
||||
const state = {
|
||||
tabs: [tab1],
|
||||
currentTabId: 1
|
||||
currentTabId: 1,
|
||||
currentTab: tab1
|
||||
}
|
||||
|
||||
deleteTab(state, tab1)
|
||||
expect(state.tabs).to.have.lengthOf(0)
|
||||
expect(state.currentTabId).to.equal(null)
|
||||
expect(state.currentTab).to.equal(null)
|
||||
})
|
||||
|
||||
it('setCurrentTabId', () => {
|
||||
|
||||
@@ -54,6 +54,10 @@ describe('DataView.vue', () => {
|
||||
const pivot = wrapper.findComponent({ name: 'pivot' }).vm
|
||||
sinon.spy(pivot, 'saveAsSvg')
|
||||
|
||||
// Switch to Custom Chart renderer
|
||||
pivot.pivotOptions.rendererName = 'Custom chart'
|
||||
await pivot.$nextTick()
|
||||
|
||||
// Export to svg
|
||||
await svgBtn.trigger('click')
|
||||
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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user