mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-06 10:08:52 +08:00
#115 record and row navigator
This commit is contained in:
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>
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="record-navigator">
|
||||
<icon-button
|
||||
:disabled="value === 0"
|
||||
tooltip="First row"
|
||||
tooltip-position="top-left"
|
||||
@click="$emit('input', 0)"
|
||||
>
|
||||
<edge-arrow-icon :disabled="false" />
|
||||
</icon-button>
|
||||
<icon-button
|
||||
:disabled="value === 0"
|
||||
tooltip="Previous row"
|
||||
tooltip-position="top-left"
|
||||
@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-last"
|
||||
@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="next-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-last {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
172
src/views/Main/Workspace/Tabs/Tab/RunResult/Record/index.vue
Normal file
172
src/views/Main/Workspace/Tabs/Tab/RunResult/Record/index.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div>
|
||||
<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>{{ col }}</th>
|
||||
<td
|
||||
:data-col="1"
|
||||
:data-row="index"
|
||||
:key="index"
|
||||
:aria-selected="false"
|
||||
@click="onCellClick"
|
||||
>
|
||||
<div class="cell-data">
|
||||
{{ dataSet.values[col][currentRowIndex] }}
|
||||
</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,
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
rowIndex: { type: Number, default: 0 }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
currentPage: 1,
|
||||
selectedCellElement: null,
|
||||
currentRowIndex: this.rowIndex
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
columns () {
|
||||
return this.dataSet.columns
|
||||
},
|
||||
rowCount () {
|
||||
return this.dataSet.values[this.columns[0]].length
|
||||
},
|
||||
pageCount () {
|
||||
return Math.ceil(this.rowCount / this.pageSize)
|
||||
},
|
||||
currentPageData () {
|
||||
const start = (this.currentPage - 1) * this.pageSize
|
||||
let end = start + this.pageSize
|
||||
if (end > this.rowCount - 1) {
|
||||
end = this.rowCount - 1
|
||||
}
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
count: end - start + 1
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onTableKeydown (e) {
|
||||
const keyCodeMap = {
|
||||
38: 'up',
|
||||
40: 'down'
|
||||
}
|
||||
|
||||
if (!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'))
|
||||
},
|
||||
selectCell (cell) {
|
||||
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
|
||||
}
|
||||
|
||||
this.$emit('updateSelectedCell', this.selectedCellElement)
|
||||
},
|
||||
moveFocusInTable (initialCell, direction) {
|
||||
const currentRowIndex = +initialCell.dataset.row
|
||||
const newRowIndex = direction === 'up'
|
||||
? currentRowIndex - 1
|
||||
: currentRowIndex + 1
|
||||
|
||||
const newCell = this.$refs.table
|
||||
.querySelector(`td[data-col="1"][data-row="${newRowIndex}"]`)
|
||||
if (newCell) {
|
||||
this.selectCell(newCell)
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
dataSet () {
|
||||
this.currentPage = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
</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);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
@@ -37,6 +37,15 @@
|
||||
<clipboard-icon/>
|
||||
</icon-button>
|
||||
|
||||
<icon-button
|
||||
:disabled="!result"
|
||||
tooltip="View record"
|
||||
tooltip-position="top-left"
|
||||
@click="toggleViewRecord"
|
||||
>
|
||||
<export-to-csv-icon/>
|
||||
</icon-button>
|
||||
|
||||
<icon-button
|
||||
:disabled="!result"
|
||||
tooltip="View value"
|
||||
@@ -78,7 +87,16 @@
|
||||
</div>
|
||||
<logs v-if="error" :messages="[error]"/>
|
||||
<sql-table
|
||||
v-if="result"
|
||||
v-if="result && !viewRecord"
|
||||
:data-set="result"
|
||||
:time="time"
|
||||
:pageSize="pageSize"
|
||||
class="straight"
|
||||
@updateSelectedCell="onUpdateSelectedCell"
|
||||
/>
|
||||
|
||||
<record
|
||||
v-if="result && viewRecord"
|
||||
:data-set="result"
|
||||
:time="time"
|
||||
:pageSize="pageSize"
|
||||
@@ -107,6 +125,7 @@ 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',
|
||||
@@ -125,7 +144,8 @@ export default {
|
||||
dataToCopy: null,
|
||||
viewValuePanelVisible: false,
|
||||
selectedCell: null,
|
||||
selectedCellValue: ''
|
||||
selectedCellValue: '',
|
||||
viewRecord: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
@@ -138,6 +158,7 @@ export default {
|
||||
ClipboardIcon,
|
||||
loadingDialog,
|
||||
ValueViewer,
|
||||
Record,
|
||||
Splitpanes,
|
||||
Teleport
|
||||
},
|
||||
@@ -229,6 +250,10 @@ export default {
|
||||
this.viewValuePanelVisible = !this.viewValuePanelVisible
|
||||
},
|
||||
|
||||
toggleViewRecord () {
|
||||
this.viewRecord = !this.viewRecord
|
||||
},
|
||||
|
||||
onUpdateSelectedCell (e) {
|
||||
this.selectedCell = e
|
||||
this.selectedCellValue = this.selectedCell?.innerText
|
||||
|
||||
Reference in New Issue
Block a user