From 327ab333612c90f501d9ea766f5c00dcb0d34401 Mon Sep 17 00:00:00 2001 From: logan Date: Sun, 24 May 2026 23:16:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Redis=20Keys=20=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E6=A0=91=E7=BB=93=E6=9E=84=E6=B5=8F=E8=A7=88=20=E2=80=94=20?= =?UTF-8?q?=E6=8C=89=20:=20=E5=88=86=E9=9A=94=E7=AC=A6=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E5=B1=82=E7=BA=A7=E6=A0=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - buildRedisTree: 将扁平 key 列表按 ':' 分隔构建为树结构 - 文件夹节点可展开/折叠,显示子节点数量 - 叶子节点显示 key type 标签(string/hash/list/set/zset) - 点击叶子节点自动插入对应查询命令: - string → GET key - hash → HGETALL key - list → LRANGE key 0 -1 - set → SMEMBERS key - zset → ZRANGE key 0 -1 WITHSCORES - 右侧保留 Key 详情面板(类型/TTL/值预览) - DB 下拉改为从后端 databases 数据生成 - 树按文件夹优先+字母排序 --- frontend/src/pages/TicketCreatePage.tsx | 143 ++++++++++++++++++++---- 1 file changed, 124 insertions(+), 19 deletions(-) diff --git a/frontend/src/pages/TicketCreatePage.tsx b/frontend/src/pages/TicketCreatePage.tsx index f73f9bf..f41c8c3 100644 --- a/frontend/src/pages/TicketCreatePage.tsx +++ b/frontend/src/pages/TicketCreatePage.tsx @@ -645,6 +645,122 @@ export default function TicketCreatePage() { }; /* ---- Redis Tab ---- */ + /* ---- Redis Tree Node type ---- */ + interface RedisTreeNode { + name: string; + fullKey?: string; // only leaf nodes + type?: string; // key type: string/hash/list/set/zset + children?: RedisTreeNode[]; + } + + /** Build a tree from flat key list using ':' as delimiter */ + const buildRedisTree = useCallback((keys: RedisKeyItem[]): RedisTreeNode[] => { + const root: RedisTreeNode[] = []; + for (const k of keys) { + const parts = k.key.split(':'); + let nodes = root; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + let node = nodes.find(n => n.name === part); + if (!node) { + node = { name: part, children: [] }; + nodes.push(node); + } + if (i === parts.length - 1) { + // leaf — replace with leaf node + node.fullKey = k.key; + node.type = k.type; + delete node.children; + } else { + if (!node.children) node.children = []; + nodes = node.children; + } + } + } + // sort: folders first, then leaves, alphabetically + const sortNodes = (arr: RedisTreeNode[]) => { + arr.sort((a, b) => { + const aIsLeaf = !!a.fullKey; + const bIsLeaf = !!b.fullKey; + if (aIsLeaf !== bIsLeaf) return aIsLeaf ? 1 : -1; + return a.name.localeCompare(b.name); + }); + arr.forEach(n => { if (n.children) sortNodes(n.children); }); + }; + sortNodes(root); + return root; + }, []); + + /* Redis expanded folders state */ + const [redisExpanded, setRedisExpanded] = useState>(new Set()); + + const toggleRedisFolder = useCallback((path: string) => { + setRedisExpanded(prev => { + const next = new Set(prev); + if (next.has(path)) next.delete(path); else next.add(path); + return next; + }); + }, []); + + /** Get Redis command for a key based on its type */ + const redisCmdForKey = useCallback((key: string, type: string): string => { + switch (type) { + case 'string': return `GET ${key}`; + case 'hash': return `HGETALL ${key}`; + case 'list': return `LRANGE ${key} 0 -1`; + case 'set': return `SMEMBERS ${key}`; + case 'zset': return `ZRANGE ${key} 0 -1 WITHSCORES`; + default: return `GET ${key}`; + } + }, []); + + /** Recursive tree renderer */ + const renderRedisTreeNode = useCallback((node: RedisTreeNode, depth: number, parentPath: string): React.ReactNode => { + const path = parentPath ? `${parentPath}:${node.name}` : node.name; + const isLeaf = !!node.fullKey; + const isExpanded = redisExpanded.has(path); + + if (isLeaf) { + return ( + + ); + } + + // folder node + const childCount = node.children?.length || 0; + return ( +
+ + {isExpanded && node.children?.map(child => renderRedisTreeNode(child, depth + 1, path))} +
+ ); + }, [redisExpanded, selectedRedisKey, toggleRedisFolder, redisCmdForKey, handleSelectRedisKey]); + + const redisTree = useMemo(() => buildRedisTree(redisKeys), [redisKeys, buildRedisTree]); + const renderRedisTab = () => (
{/* Search Bar */} @@ -654,9 +770,10 @@ export default function TicketCreatePage() { onChange={(e) => { setRedisDb(Number(e.target.value)); fetchRedisKeys(); }} className="px-2 py-1.5 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded text-xs focus:outline-none focus:border-blue-500" > - {Array.from({ length: 16 }, (_, i) => ( - - ))} + {databases.length > 0 + ? databases.map(db => ) + : Array.from({ length: 16 }, (_, i) => ) + }
@@ -665,7 +782,7 @@ export default function TicketCreatePage() { onChange={(e) => setRedisPattern(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && fetchRedisKeys()} className="w-full pl-7 pr-3 py-1.5 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded text-xs focus:outline-none focus:border-blue-500" - placeholder="Key pattern (e.g. user:*)" + placeholder="Key pattern (e.g. matcher:*)" />
- ))} + {redisTree.map(node => renderRedisTreeNode(node, 0, ''))}
{/* Key Detail */} @@ -717,7 +822,7 @@ export default function TicketCreatePage() { ) : ( -
选择 Key 查看详情
+
点击 Key 查看详情
)}