feat: Command Log dialog
- New commandLogger.ts: in-memory log (max 5000 entries) - withLog() wrapper in ipc-handlers.ts for all write commands - CommandLog.vue: table with time/command/duration, write-only filter - IPC: redis:getCommandLog, redis:clearCommandLog - Ctrl+G shortcut from Sidebar - i18n keys (zh-CN + en)
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
// src/components/CommandLog.vue
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from '@renderer/i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface CommandEntry {
|
||||
id: number
|
||||
time: string
|
||||
connection: string
|
||||
command: string
|
||||
duration: number
|
||||
isWrite: boolean
|
||||
}
|
||||
|
||||
const entries = ref<CommandEntry[]>([])
|
||||
const onlyWrite = ref(false)
|
||||
const visible = defineModel<boolean>('visible', { default: false })
|
||||
|
||||
const filteredEntries = () => {
|
||||
if (onlyWrite.value) {
|
||||
return entries.value.filter(e => e.isWrite)
|
||||
}
|
||||
return entries.value
|
||||
}
|
||||
|
||||
function formatTime(iso: string): string {
|
||||
const d = new Date(iso)
|
||||
return d.toLocaleTimeString()
|
||||
}
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
if (ms < 1) return '<1ms'
|
||||
if (ms < 1000) return ms + 'ms'
|
||||
return (ms / 1000).toFixed(2) + 's'
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
entries.value = await window.electronAPI.redis.getCommandLog()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function clear() {
|
||||
try {
|
||||
await window.electronAPI.redis.clearCommandLog()
|
||||
entries.value = []
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ refresh })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
:title="t('commandLog.title')"
|
||||
width="700px"
|
||||
top="5vh"
|
||||
@update:model-value="visible = $event"
|
||||
@opened="refresh"
|
||||
>
|
||||
<div class="cmdlog-toolbar">
|
||||
<el-checkbox v-model="onlyWrite" size="small">{{ t('commandLog.onlyWrite') }}</el-checkbox>
|
||||
<div class="cmdlog-actions">
|
||||
<el-button size="small" @click="refresh">{{ t('common.refresh') }}</el-button>
|
||||
<el-button size="small" type="danger" @click="clear">{{ t('commandLog.clear') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cmdlog-table-wrap">
|
||||
<el-table
|
||||
:data="filteredEntries()"
|
||||
size="small"
|
||||
stripe
|
||||
height="420"
|
||||
highlight-current-row
|
||||
>
|
||||
<el-table-column prop="id" label="#" width="50" />
|
||||
<el-table-column :label="t('commandLog.time')" width="90">
|
||||
<template #default="{ row }">{{ formatTime(row.time) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="command" :label="t('commandLog.command')" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column :label="t('commandLog.duration')" width="80" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ color: row.duration > 100 ? 'var(--warning)' : 'var(--success)' }">
|
||||
{{ formatDuration(row.duration) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-if="entries.length === 0" class="cmdlog-empty">
|
||||
{{ t('commandLog.empty') }}
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cmdlog-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.cmdlog-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cmdlog-table-wrap {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.cmdlog-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 150px;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@
|
||||
import { ref } from 'vue'
|
||||
import ConnectionList from './ConnectionList.vue'
|
||||
import NewConnectionDialog from './NewConnectionDialog.vue'
|
||||
import CommandLog from './CommandLog.vue'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import { useKeyStore } from '@renderer/stores/key'
|
||||
import { useI18n } from '@renderer/i18n'
|
||||
@@ -16,6 +17,7 @@ const { t } = useI18n()
|
||||
const dialogVisible = ref(false)
|
||||
const editingConnection = ref<ConnectionConfig | null>(null)
|
||||
const collapsed = ref(false)
|
||||
const commandLogVisible = ref(false)
|
||||
|
||||
function handleEdit(conn: ConnectionConfig | null) {
|
||||
editingConnection.value = conn
|
||||
@@ -53,6 +55,7 @@ function refreshAll() {
|
||||
useKeyboard({
|
||||
'ctrl+n': handleNew,
|
||||
'ctrl+r': refreshAll,
|
||||
'ctrl+g': () => { commandLogVisible.value = true },
|
||||
'f5': refreshAll,
|
||||
'delete': deleteSelectedKey,
|
||||
'escape': deselectKey,
|
||||
@@ -73,6 +76,7 @@ useKeyboard({
|
||||
v-model:visible="dialogVisible"
|
||||
:connection="editingConnection"
|
||||
/>
|
||||
<CommandLog v-model:visible="commandLogVisible" />
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -156,6 +156,16 @@ export default {
|
||||
sortDesc: 'Descending',
|
||||
scanned: '{n} keys scanned',
|
||||
},
|
||||
commandLog: {
|
||||
title: 'Command Log',
|
||||
time: 'Time',
|
||||
command: 'Command',
|
||||
duration: 'Duration',
|
||||
connection: 'Connection',
|
||||
onlyWrite: 'Write Only',
|
||||
clear: 'Clear',
|
||||
empty: 'No commands recorded',
|
||||
},
|
||||
status: {
|
||||
title: 'Status',
|
||||
database: 'Database',
|
||||
|
||||
@@ -156,6 +156,16 @@ export default {
|
||||
sortDesc: '降序',
|
||||
scanned: '已扫描 {n} 个',
|
||||
},
|
||||
commandLog: {
|
||||
title: '命令日志',
|
||||
time: '时间',
|
||||
command: '命令',
|
||||
duration: '耗时',
|
||||
connection: '连接',
|
||||
onlyWrite: '仅写操作',
|
||||
clear: '清空',
|
||||
empty: '暂无命令记录',
|
||||
},
|
||||
status: {
|
||||
title: '状态',
|
||||
database: '数据库',
|
||||
|
||||
Reference in New Issue
Block a user