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:
2026-07-05 21:48:33 +08:00
parent 15869943e8
commit d131a2732d
10 changed files with 270 additions and 38 deletions
+60 -37
View File
@@ -5,6 +5,21 @@ import store, { ConnectionConfig } from './store'
import { encrypt, decrypt } from './credential'
import * as redis from './redis'
// Helper: wrap handler with command logging
function withLog(cmd: string, fn: (...args: any[]) => any) {
return async (...args: any[]) => {
const start = Date.now()
try {
const result = await fn(...args)
redis.log('current', cmd, Date.now() - start)
return result
} catch (err) {
redis.log('current', cmd, Date.now() - start)
throw err
}
}
}
export function registerIpcHandlers(): void {
// Window
ipcMain.on('window:minimize', () => BrowserWindow.getFocusedWindow()?.minimize())
@@ -88,15 +103,15 @@ export function registerIpcHandlers(): void {
})
// Redis - 服务器
ipcMain.handle('redis:execute', async (_e, id: string, command: string, args: string[]) => {
ipcMain.handle('redis:execute', withLog('EXEC', async (_e, id: string, command: string, args: string[]) => {
return redis.execute(id, command, args)
})
}))
ipcMain.handle('redis:getInfo', async (_e, id: string) => {
return redis.getServerInfo(id)
})
ipcMain.handle('redis:selectDb', async (_e, id: string, db: number) => {
ipcMain.handle('redis:selectDb', withLog('SELECT', async (_e, id: string, db: number) => {
await redis.selectDb(id, db)
})
}))
ipcMain.handle('redis:dbSize', async (_e, id: string) => {
return redis.dbSize(id)
})
@@ -117,21 +132,21 @@ export function registerIpcHandlers(): void {
ipcMain.handle('redis:getKeyTTL', async (_e, id: string, key: string) => {
return redis.getKeyTTL(id, key)
})
ipcMain.handle('redis:setTTL', async (_e, id: string, key: string, ttl: number) => {
ipcMain.handle('redis:setTTL', withLog('EXPIRE', async (_e, id: string, key: string, ttl: number) => {
await redis.setKeyTTL(id, key, ttl)
})
ipcMain.handle('redis:deleteKey', async (_e, id: string, key: string) => {
}))
ipcMain.handle('redis:deleteKey', withLog('DEL', async (_e, id: string, key: string) => {
await redis.deleteKey(id, key)
})
ipcMain.handle('redis:renameKey', async (_e, id: string, oldKey: string, newKey: string) => {
}))
ipcMain.handle('redis:renameKey', withLog('RENAME', async (_e, id: string, oldKey: string, newKey: string) => {
await redis.renameKey(id, oldKey, newKey)
})
}))
ipcMain.handle('redis:existsKey', async (_e, id: string, key: string) => {
return redis.existsKey(id, key)
})
ipcMain.handle('redis:batchDelete', async (_e, id: string, keys: string[]) => {
ipcMain.handle('redis:batchDelete', withLog('DEL batch', async (_e, id: string, keys: string[]) => {
return redis.batchDelete(id, keys)
})
}))
ipcMain.handle('redis:memoryUsage', async (_e, id: string, key: string) => {
return redis.memoryUsage(id, key)
})
@@ -140,20 +155,20 @@ export function registerIpcHandlers(): void {
ipcMain.handle('redis:getString', async (_e, id: string, key: string) => {
return redis.getStringValue(id, key)
})
ipcMain.handle('redis:setString', async (_e, id: string, key: string, value: string) => {
ipcMain.handle('redis:setString', withLog('SET', async (_e, id: string, key: string, value: string) => {
await redis.setStringValue(id, key, value)
})
}))
// Redis - Hash
ipcMain.handle('redis:hashScan', async (_e, id: string, key: string, cursor: string, count: number) => {
return redis.getHashFields(id, key, cursor, count)
})
ipcMain.handle('redis:hashSet', async (_e, id: string, key: string, field: string, value: string) => {
ipcMain.handle('redis:hashSet', withLog('HSET', async (_e, id: string, key: string, field: string, value: string) => {
await redis.setHashField(id, key, field, value)
})
ipcMain.handle('redis:hashDel', async (_e, id: string, key: string, field: string) => {
}))
ipcMain.handle('redis:hashDel', withLog('HDEL', async (_e, id: string, key: string, field: string) => {
await redis.deleteHashField(id, key, field)
})
}))
// Redis - List
ipcMain.handle('redis:listRange', async (_e, id: string, key: string, start: number, stop: number) => {
@@ -162,15 +177,15 @@ export function registerIpcHandlers(): void {
ipcMain.handle('redis:listLength', async (_e, id: string, key: string) => {
return redis.listLength(id, key)
})
ipcMain.handle('redis:listPush', async (_e, id: string, key: string, values: string[]) => {
ipcMain.handle('redis:listPush', withLog('RPUSH', async (_e, id: string, key: string, values: string[]) => {
return redis.listPush(id, key, ...values)
})
ipcMain.handle('redis:listSet', async (_e, id: string, key: string, index: number, value: string) => {
}))
ipcMain.handle('redis:listSet', withLog('LSET', async (_e, id: string, key: string, index: number, value: string) => {
await redis.listSet(id, key, index, value)
})
ipcMain.handle('redis:listRemove', async (_e, id: string, key: string, count: number, value: string) => {
}))
ipcMain.handle('redis:listRemove', withLog('LREM', async (_e, id: string, key: string, count: number, value: string) => {
return redis.listRemove(id, key, count, value)
})
}))
// Redis - Set
ipcMain.handle('redis:setMembers', async (_e, id: string, key: string) => {
@@ -179,12 +194,12 @@ export function registerIpcHandlers(): void {
ipcMain.handle('redis:setSize', async (_e, id: string, key: string) => {
return redis.setSize(id, key)
})
ipcMain.handle('redis:setAdd', async (_e, id: string, key: string, members: string[]) => {
ipcMain.handle('redis:setAdd', withLog('SADD', async (_e, id: string, key: string, members: string[]) => {
return redis.setAdd(id, key, ...members)
})
ipcMain.handle('redis:setRemove', async (_e, id: string, key: string, members: string[]) => {
}))
ipcMain.handle('redis:setRemove', withLog('SREM', async (_e, id: string, key: string, members: string[]) => {
return redis.setRemove(id, key, ...members)
})
}))
// Redis - Zset
ipcMain.handle('redis:zsetRange', async (_e, id: string, key: string, start: number, stop: number, withScores: boolean) => {
@@ -193,12 +208,12 @@ export function registerIpcHandlers(): void {
ipcMain.handle('redis:zsetSize', async (_e, id: string, key: string) => {
return redis.zsetSize(id, key)
})
ipcMain.handle('redis:zsetAdd', async (_e, id: string, key: string, score: number, member: string) => {
ipcMain.handle('redis:zsetAdd', withLog('ZADD', async (_e, id: string, key: string, score: number, member: string) => {
return redis.zsetAdd(id, key, score, member)
})
ipcMain.handle('redis:zsetRemove', async (_e, id: string, key: string, members: string[]) => {
}))
ipcMain.handle('redis:zsetRemove', withLog('ZREM', async (_e, id: string, key: string, members: string[]) => {
return redis.zsetRemove(id, key, ...members)
})
}))
// Redis - Stream
ipcMain.handle('redis:streamInfo', async (_e, id: string, key: string) => {
@@ -207,13 +222,21 @@ export function registerIpcHandlers(): void {
ipcMain.handle('redis:streamRange', async (_e, id: string, key: string, start: string, end: string, count: number) => {
return redis.streamRange(id, key, start, end, count)
})
ipcMain.handle('redis:streamAdd', async (_e, id: string, key: string, streamId: string, fieldValues: string[]) => {
ipcMain.handle('redis:streamAdd', withLog('XADD', async (_e, id: string, key: string, streamId: string, fieldValues: string[]) => {
return redis.streamAdd(id, key, streamId, fieldValues)
})
ipcMain.handle('redis:streamTrim', async (_e, id: string, key: string, maxLen: number) => {
}))
ipcMain.handle('redis:streamTrim', withLog('XTRIM', async (_e, id: string, key: string, maxLen: number) => {
return redis.streamTrim(id, key, maxLen)
})
ipcMain.handle('redis:streamDel', async (_e, id: string, key: string, ids: string[]) => {
}))
ipcMain.handle('redis:streamDel', withLog('XDEL', async (_e, id: string, key: string, ids: string[]) => {
return redis.streamDel(id, key, ...ids)
}))
// Command Log
ipcMain.handle('redis:getCommandLog', () => {
return redis.getLog()
})
ipcMain.handle('redis:clearCommandLog', () => {
redis.clearLog()
})
}
+49
View File
@@ -0,0 +1,49 @@
// electron/main/redis/commandLogger.ts
// 命令日志记录器
export interface CommandEntry {
id: number
time: string
connection: string
command: string
duration: number
isWrite: boolean
}
const MAX_ENTRIES = 5000
let nextId = 1
const entries: CommandEntry[] = []
const WRITE_COMMANDS = new Set([
'SET', 'DEL', 'HSET', 'HDEL', 'RPUSH', 'LPUSH', 'LSET', 'LREM',
'SADD', 'SREM', 'ZADD', 'ZREM', 'XADD', 'XDEL', 'XTRIM',
'RENAME', 'EXPIRE', 'PERSIST', 'FLUSHDB', 'SELECT',
'SETEX', 'INCR', 'DECR', 'INCRBY', 'DECRBY', 'APPEND',
'HMSET', 'HINCRBY', 'HINCRBYFLOAT',
'LPOP', 'RPOP', 'BLPOP', 'BRPOP',
'ZINCRBY', 'ZREMRANGEBYRANK', 'ZREMRANGEBYSCORE',
'UNLINK', 'MOVE', 'RENAMENX',
])
export function log(connection: string, command: string, duration: number): void {
const isWrite = WRITE_COMMANDS.has(command.split(' ')[0].toUpperCase())
entries.push({
id: nextId++,
time: new Date().toISOString(),
connection,
command,
duration,
isWrite,
})
if (entries.length > MAX_ENTRIES) {
entries.splice(0, entries.length - MAX_ENTRIES)
}
}
export function getLog(): CommandEntry[] {
return entries
}
export function clearLog(): void {
entries.length = 0
}
+1
View File
@@ -9,3 +9,4 @@ export * from './set'
export * from './zset'
export * from './stream'
export * from './server'
export * from './commandLogger'