mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-06 18:18:53 +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/>
|
<clipboard-icon/>
|
||||||
</icon-button>
|
</icon-button>
|
||||||
|
|
||||||
|
<icon-button
|
||||||
|
:disabled="!result"
|
||||||
|
tooltip="View record"
|
||||||
|
tooltip-position="top-left"
|
||||||
|
@click="toggleViewRecord"
|
||||||
|
>
|
||||||
|
<export-to-csv-icon/>
|
||||||
|
</icon-button>
|
||||||
|
|
||||||
<icon-button
|
<icon-button
|
||||||
:disabled="!result"
|
:disabled="!result"
|
||||||
tooltip="View value"
|
tooltip="View value"
|
||||||
@@ -78,7 +87,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<logs v-if="error" :messages="[error]"/>
|
<logs v-if="error" :messages="[error]"/>
|
||||||
<sql-table
|
<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"
|
:data-set="result"
|
||||||
:time="time"
|
:time="time"
|
||||||
:pageSize="pageSize"
|
:pageSize="pageSize"
|
||||||
@@ -107,6 +125,7 @@ import loadingDialog from '@/components/LoadingDialog'
|
|||||||
import events from '@/lib/utils/events'
|
import events from '@/lib/utils/events'
|
||||||
import Teleport from 'vue2-teleport'
|
import Teleport from 'vue2-teleport'
|
||||||
import ValueViewer from './ValueViewer'
|
import ValueViewer from './ValueViewer'
|
||||||
|
import Record from './Record/index.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RunResult',
|
name: 'RunResult',
|
||||||
@@ -125,7 +144,8 @@ export default {
|
|||||||
dataToCopy: null,
|
dataToCopy: null,
|
||||||
viewValuePanelVisible: false,
|
viewValuePanelVisible: false,
|
||||||
selectedCell: null,
|
selectedCell: null,
|
||||||
selectedCellValue: ''
|
selectedCellValue: '',
|
||||||
|
viewRecord: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
@@ -138,6 +158,7 @@ export default {
|
|||||||
ClipboardIcon,
|
ClipboardIcon,
|
||||||
loadingDialog,
|
loadingDialog,
|
||||||
ValueViewer,
|
ValueViewer,
|
||||||
|
Record,
|
||||||
Splitpanes,
|
Splitpanes,
|
||||||
Teleport
|
Teleport
|
||||||
},
|
},
|
||||||
@@ -229,6 +250,10 @@ export default {
|
|||||||
this.viewValuePanelVisible = !this.viewValuePanelVisible
|
this.viewValuePanelVisible = !this.viewValuePanelVisible
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleViewRecord () {
|
||||||
|
this.viewRecord = !this.viewRecord
|
||||||
|
},
|
||||||
|
|
||||||
onUpdateSelectedCell (e) {
|
onUpdateSelectedCell (e) {
|
||||||
this.selectedCell = e
|
this.selectedCell = e
|
||||||
this.selectedCellValue = this.selectedCell?.innerText
|
this.selectedCellValue = this.selectedCell?.innerText
|
||||||
|
|||||||
Reference in New Issue
Block a user