From 5c546f68af96764e6472a770a466ceb6571091c3 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 30 Apr 2026 13:01:10 +0800 Subject: [PATCH] feat(redis): implement pagination for Redis key scanning and enhance UI for loading more keys --- src-tauri/src/commands/redis_cmd.rs | 7 +-- src-tauri/src/db/redis_driver.rs | 57 +++++++++++++----------- src/components/redis/RedisKeyBrowser.vue | 42 +++++++++++++++-- src/components/sidebar/TreeItem.vue | 20 +++++++-- src/i18n/locales/en.ts | 3 ++ src/i18n/locales/zh-CN.ts | 3 ++ src/lib/tauri.ts | 9 +++- src/stores/connectionStore.ts | 34 +++++++++++++- src/types/database.ts | 2 + 9 files changed, 137 insertions(+), 40 deletions(-) diff --git a/src-tauri/src/commands/redis_cmd.rs b/src-tauri/src/commands/redis_cmd.rs index 1936cfeed..721210831 100644 --- a/src-tauri/src/commands/redis_cmd.rs +++ b/src-tauri/src/commands/redis_cmd.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use tauri::State; use crate::commands::connection::{AppState, PoolKind}; -use crate::db::redis_driver::{self, RedisKeyInfo, RedisValue}; +use crate::db::redis_driver::{self, RedisScanResult, RedisValue}; #[tauri::command] pub async fn redis_list_databases( @@ -25,16 +25,17 @@ pub async fn redis_scan_keys( state: State<'_, Arc>, connection_id: String, db: u32, + cursor: u64, pattern: String, count: usize, -) -> Result, String> { +) -> Result { let connections = state.connections.lock().await; let pool = connections.get(&connection_id).ok_or("Connection not found")?; match pool { PoolKind::Redis(con) => { let mut con = con.lock().await; redis_driver::select_db(&mut con, db).await?; - redis_driver::scan_keys(&mut con, &pattern, count).await + redis_driver::scan_keys_page(&mut con, cursor, &pattern, count).await } _ => Err("Not a Redis connection".to_string()), } diff --git a/src-tauri/src/db/redis_driver.rs b/src-tauri/src/db/redis_driver.rs index e9cf6b76c..a25fd4e7b 100644 --- a/src-tauri/src/db/redis_driver.rs +++ b/src-tauri/src/db/redis_driver.rs @@ -8,6 +8,12 @@ pub struct RedisKeyInfo { pub ttl: i64, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RedisScanResult { + pub cursor: u64, + pub keys: Vec, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RedisValue { pub key: String, @@ -18,13 +24,20 @@ pub struct RedisValue { pub async fn connect(url: &str) -> Result { let client = redis::Client::open(url).map_err(|e| format!("Redis connection failed: {e}"))?; - tokio::time::timeout( + let mut con = tokio::time::timeout( std::time::Duration::from_secs(10), client.get_multiplexed_async_connection(), ) .await .map_err(|_| "Redis connection timed out (10s)".to_string())? - .map_err(|e| format!("Redis connection failed: {e}")) + .map_err(|e| format!("Redis connection failed: {e}"))?; + + redis::cmd("PING") + .query_async::(&mut con) + .await + .map_err(|e| format!("Redis authentication failed or command rejected: {e}"))?; + + Ok(con) } pub async fn list_databases(con: &mut redis::aio::MultiplexedConnection) -> Result, String> { @@ -58,33 +71,24 @@ pub async fn select_db(con: &mut redis::aio::MultiplexedConnection, db: u32) -> .map_err(|e| e.to_string()) } -pub async fn scan_keys( +pub async fn scan_keys_page( con: &mut redis::aio::MultiplexedConnection, + cursor: u64, pattern: &str, count: usize, -) -> Result, String> { - let mut cursor: u64 = 0; - let mut all_keys: Vec = Vec::new(); - loop { - let (new_cursor, keys): (u64, Vec) = redis::cmd("SCAN") - .arg(cursor) - .arg("MATCH") - .arg(pattern) - .arg("COUNT") - .arg(100) - .query_async(con) - .await - .map_err(|e| e.to_string())?; - all_keys.extend(keys); - cursor = new_cursor; - if cursor == 0 || all_keys.len() >= count { - break; - } - } - all_keys.truncate(count); +) -> Result { + let (next_cursor, keys): (u64, Vec) = redis::cmd("SCAN") + .arg(cursor) + .arg("MATCH") + .arg(pattern) + .arg("COUNT") + .arg(count) + .query_async(con) + .await + .map_err(|e| e.to_string())?; let mut result = Vec::new(); - for key in &all_keys { + for key in &keys { let key_type: String = redis::cmd("TYPE") .arg(key.as_str()) .query_async(con) @@ -99,7 +103,10 @@ pub async fn scan_keys( ttl, }); } - Ok(result) + Ok(RedisScanResult { + cursor: next_cursor, + keys: result, + }) } pub async fn get_value( diff --git a/src/components/redis/RedisKeyBrowser.vue b/src/components/redis/RedisKeyBrowser.vue index 03fd8efaf..7534a5b77 100644 --- a/src/components/redis/RedisKeyBrowser.vue +++ b/src/components/redis/RedisKeyBrowser.vue @@ -1,7 +1,7 @@ @@ -269,7 +280,8 @@ function showMore() { :style="{ paddingLeft: `${(depth + 1) * 16 + 8}px` }" @click="showMore" > - {{ t('sidebar.showMore', { count: Math.min(CHILDREN_PAGE_SIZE, remainingCount) }) }} + + {{ node.hasMore && remainingCount <= 0 ? t('sidebar.loadMore') : t('sidebar.showMore', { count: Math.min(CHILDREN_PAGE_SIZE, remainingCount) }) }} diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 3d5b2ac5f..ea56850fd 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -14,6 +14,7 @@ export default { import: "Import Connections", export: "Export Connections", showMore: "Show {count} more...", + loadMore: "Load more...", }, connection: { title: "New Connection", @@ -155,6 +156,8 @@ export default { noKeys: "No keys found", pattern: "pattern (e.g. user:*)", keys: "{count} keys", + loadingKeys: "Loading keys...", + loadMoreKeys: "Load more keys", items: "{count} items", fields: "{count} fields", members: "{count} members", diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index ec696ec28..5f67e29c5 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -14,6 +14,7 @@ export default { import: "导入连接", export: "导出连接", showMore: "加载更多 ({count})...", + loadMore: "继续加载...", }, connection: { title: "新建连接", @@ -153,6 +154,8 @@ export default { noKeys: "未找到 key", pattern: "匹配模式 (如 user:*)", keys: "{count} 个 key", + loadingKeys: "正在加载 key...", + loadMoreKeys: "继续加载 key", items: "{count} 个元素", fields: "{count} 个字段", members: "{count} 个成员", diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index 6555fc9c6..c54181534 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -115,12 +115,17 @@ export interface RedisValue { value: any; } +export interface RedisScanResult { + cursor: number; + keys: RedisKeyInfo[]; +} + export async function redisListDatabases(connectionId: string): Promise { return invoke("redis_list_databases", { connectionId }); } -export async function redisScanKeys(connectionId: string, db: number, pattern: string, count: number): Promise { - return invoke("redis_scan_keys", { connectionId, db, pattern, count }); +export async function redisScanKeys(connectionId: string, db: number, cursor: number, pattern: string, count: number): Promise { + return invoke("redis_scan_keys", { connectionId, db, cursor, pattern, count }); } export async function redisGetValue(connectionId: string, key: string): Promise { diff --git a/src/stores/connectionStore.ts b/src/stores/connectionStore.ts index 3c6a757f6..7e27d6119 100644 --- a/src/stores/connectionStore.ts +++ b/src/stores/connectionStore.ts @@ -145,8 +145,10 @@ export const useConnectionStore = defineStore("connection", () => { node.isLoading = true; try { - const keys = await api.redisScanKeys(connectionId, db, "*", 500); - node.children = keys.map((k) => ({ + const result = await api.redisScanKeys(connectionId, db, 0, "*", 200); + node.scanCursor = result.cursor; + node.hasMore = result.cursor !== 0; + node.children = result.keys.map((k) => ({ id: `${nodeId}:${k.key}`, label: `${k.key} [${k.key_type}]${k.ttl > 0 ? ` TTL:${k.ttl}` : ""}`, type: "redis-key" as const, @@ -160,6 +162,33 @@ export const useConnectionStore = defineStore("connection", () => { } } + async function loadMoreRedisKeys(connectionId: string, db: number, nodeId: string) { + const node = findNode(treeNodes.value, nodeId); + if (!node || node.isLoading || !node.hasMore) return; + + node.isLoading = true; + try { + const result = await api.redisScanKeys(connectionId, db, node.scanCursor ?? 0, "*", 200); + const existingIds = new Set((node.children ?? []).map((child) => child.id)); + const moreChildren = result.keys + .filter((k) => !existingIds.has(`${nodeId}:${k.key}`)) + .map((k) => ({ + id: `${nodeId}:${k.key}`, + label: `${k.key} [${k.key_type}]${k.ttl > 0 ? ` TTL:${k.ttl}` : ""}`, + type: "redis-key" as const, + connectionId, + database: String(db), + isExpanded: false, + })); + + node.scanCursor = result.cursor; + node.hasMore = result.cursor !== 0; + node.children = [...(node.children ?? []), ...moreChildren]; + } finally { + node.isLoading = false; + } + } + async function loadMongoDatabases(connectionId: string) { const node = findNode(treeNodes.value, connectionId); if (!node) return; @@ -455,6 +484,7 @@ export const useConnectionStore = defineStore("connection", () => { loadDatabases, loadRedisDatabases, loadRedisKeys, + loadMoreRedisKeys, loadMongoDatabases, loadMongoCollections, loadSchemas, diff --git a/src/types/database.ts b/src/types/database.ts index 504d818d7..7666ab928 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -78,6 +78,8 @@ export interface TreeNode { children?: TreeNode[]; isLoading?: boolean; isExpanded?: boolean; + scanCursor?: number; + hasMore?: boolean; connectionId?: string; database?: string; schema?: string;