diff --git a/frontend/src/components/RedisViewer.interaction.test.tsx b/frontend/src/components/RedisViewer.interaction.test.tsx index 5a7d4e17..db2d73eb 100644 --- a/frontend/src/components/RedisViewer.interaction.test.tsx +++ b/frontend/src/components/RedisViewer.interaction.test.tsx @@ -296,6 +296,58 @@ describe('RedisViewer tree interactions', () => { renderer!.unmount(); }); + it('filters keys by the full namespace path from a group action', async () => { + let renderer: ReactTestRenderer; + let groupTitleRenderer: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + await flushEffects(); + + const searchModeGroup = renderer!.root.findAll( + node => node.props.buttonStyle === 'solid' && typeof node.props.onChange === 'function', + )[0]; + await act(async () => { + searchModeGroup.props.onChange({ target: { value: 'exact' } }); + }); + await flushEffects(); + + const appGroup = antdState.treeProps.treeData.find((node: any) => node.key === 'group:app'); + const userGroup = appGroup.children.find((node: any) => node.key === 'group:app:user'); + await act(async () => { + groupTitleRenderer = create(antdState.treeProps.titleRender(userGroup)); + }); + + const filterButton = groupTitleRenderer!.root.findByProps({ 'aria-label': 'Filter by namespace' }); + const event = { + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + }; + await act(async () => { + filterButton.props.onClick(event); + }); + await flushEffects(); + + const searchInput = renderer!.root.findAllByType('input').find(node => typeof node.props.onSearch === 'function'); + const updatedSearchModeGroup = renderer!.root.findAll( + node => node.props.buttonStyle === 'solid' && typeof node.props.onChange === 'function', + )[0]; + + expect(event.preventDefault).toHaveBeenCalled(); + expect(event.stopPropagation).toHaveBeenCalled(); + expect(searchInput?.props.value).toBe('app:user'); + expect(updatedSearchModeGroup.props.value).toBe('fuzzy'); + expect(redisBackend.RedisScanKeys).toHaveBeenLastCalledWith( + expect.any(Object), + '*[aA][pP][pP]:[uU][sS][eE][rR]*', + '0', + 600, + ); + + groupTitleRenderer!.unmount(); + renderer!.unmount(); + }); + it('shows Redis Cluster topology context in the key explorer header', async () => { storeState.connections = [ { diff --git a/frontend/src/components/RedisViewer.tsx b/frontend/src/components/RedisViewer.tsx index 9ab0aa2e..428650e8 100644 --- a/frontend/src/components/RedisViewer.tsx +++ b/frontend/src/components/RedisViewer.tsx @@ -1092,6 +1092,16 @@ const RedisViewer: React.FC = ({ connectionId, redisDB }) => { }); }, [isLargeKeyspace]); + const handleFilterByGroup = useCallback((treeNode: RedisTreeDataNode) => { + const groupPath = treeNode.groupPath?.trim(); + if (!groupPath) { + return; + } + + setSearchMode('fuzzy'); + executeSearch(groupPath, 'fuzzy'); + }, [executeSearch]); + const stopTreeTitleEvent = (event: React.SyntheticEvent) => { event.preventDefault(); event.stopPropagation(); @@ -1164,26 +1174,51 @@ const RedisViewer: React.FC = ({ connectionId, redisDB }) => { ({treeNode.groupLeafCount ?? 0}) - + + + + ); } diff --git a/frontend/src/components/redisViewerTree.test.ts b/frontend/src/components/redisViewerTree.test.ts index 7a558cba..b0ef084e 100644 --- a/frontend/src/components/redisViewerTree.test.ts +++ b/frontend/src/components/redisViewerTree.test.ts @@ -24,6 +24,8 @@ describe('redisViewerTree helpers', () => { expect(appGroup).toBeTruthy(); expect(userGroup).toBeTruthy(); + expect(appGroup?.groupPath).toBe('app'); + expect(userGroup?.groupPath).toBe('app:user'); expect(appGroup?.descendantRawKeys).toEqual(['app:order:1', 'app:user:1', 'app:user:2']); const selectedAfterGroupCheck = applyTreeNodeCheck([], appGroup!, true); diff --git a/frontend/src/components/redisViewerTree.ts b/frontend/src/components/redisViewerTree.ts index e212ce93..c8eef32c 100644 --- a/frontend/src/components/redisViewerTree.ts +++ b/frontend/src/components/redisViewerTree.ts @@ -17,9 +17,11 @@ type RedisKeyTreeGroup = { leafCount: number; }; -export type RedisTreeDataNode = DataNode & { +export type RedisTreeDataNode = Omit & { nodeType: 'group' | 'leaf'; + children?: RedisTreeDataNode[]; groupName?: string; + groupPath?: string; groupLeafCount?: number; leafLabel?: string; rawKey?: string; @@ -134,6 +136,7 @@ export const buildRedisKeyTree = ( title: child.name, nodeType: 'group', groupName: child.name, + groupPath: child.path, groupLeafCount: child.leafCount, descendantRawKeys, children, diff --git a/shared/i18n/de-DE.json b/shared/i18n/de-DE.json index 5dc4e208..7d1b73ce 100644 --- a/shared/i18n/de-DE.json +++ b/shared/i18n/de-DE.json @@ -6868,6 +6868,7 @@ "redis_viewer.action.edit": "Bearbeiten", "redis_viewer.action.export_all": "Alle exportieren", "redis_viewer.action.export_selected": "Auswahl exportieren", + "redis_viewer.action.filter_group": "Nach diesem Namensraum filtern", "redis_viewer.action.import": "Importieren", "redis_viewer.action.load_all": "Alle laden", "redis_viewer.action.load_more": "Mehr laden", diff --git a/shared/i18n/en-US.json b/shared/i18n/en-US.json index c829b052..3923d4e7 100644 --- a/shared/i18n/en-US.json +++ b/shared/i18n/en-US.json @@ -6868,6 +6868,7 @@ "redis_viewer.action.edit": "Edit", "redis_viewer.action.export_all": "Export all", "redis_viewer.action.export_selected": "Export selected", + "redis_viewer.action.filter_group": "Filter by namespace", "redis_viewer.action.import": "Import", "redis_viewer.action.load_all": "Load all", "redis_viewer.action.load_more": "Load more", diff --git a/shared/i18n/ja-JP.json b/shared/i18n/ja-JP.json index 8ff53842..6757de25 100644 --- a/shared/i18n/ja-JP.json +++ b/shared/i18n/ja-JP.json @@ -6868,6 +6868,7 @@ "redis_viewer.action.edit": "編集", "redis_viewer.action.export_all": "すべてエクスポート", "redis_viewer.action.export_selected": "選択した Key をエクスポート", + "redis_viewer.action.filter_group": "この名前空間で絞り込む", "redis_viewer.action.import": "インポート", "redis_viewer.action.load_all": "すべて読み込む", "redis_viewer.action.load_more": "さらに読み込む", diff --git a/shared/i18n/ru-RU.json b/shared/i18n/ru-RU.json index 51d393ca..6c5b64e0 100644 --- a/shared/i18n/ru-RU.json +++ b/shared/i18n/ru-RU.json @@ -6868,6 +6868,7 @@ "redis_viewer.action.edit": "Редактировать", "redis_viewer.action.export_all": "Экспортировать все", "redis_viewer.action.export_selected": "Экспортировать выбранное", + "redis_viewer.action.filter_group": "Фильтровать по этому пространству имён", "redis_viewer.action.import": "Импортировать", "redis_viewer.action.load_all": "Загрузить все", "redis_viewer.action.load_more": "Загрузить еще", diff --git a/shared/i18n/zh-CN.json b/shared/i18n/zh-CN.json index 58a3b9c1..085fc476 100644 --- a/shared/i18n/zh-CN.json +++ b/shared/i18n/zh-CN.json @@ -6868,6 +6868,7 @@ "redis_viewer.action.edit": "编辑", "redis_viewer.action.export_all": "导出全部", "redis_viewer.action.export_selected": "导出已选", + "redis_viewer.action.filter_group": "按此目录筛选", "redis_viewer.action.import": "导入", "redis_viewer.action.load_all": "加载全部", "redis_viewer.action.load_more": "加载更多", diff --git a/shared/i18n/zh-TW.json b/shared/i18n/zh-TW.json index dc0ce6b6..932cec31 100644 --- a/shared/i18n/zh-TW.json +++ b/shared/i18n/zh-TW.json @@ -6868,6 +6868,7 @@ "redis_viewer.action.edit": "編輯", "redis_viewer.action.export_all": "匯出全部", "redis_viewer.action.export_selected": "匯出已選", + "redis_viewer.action.filter_group": "依此目錄篩選", "redis_viewer.action.import": "匯入", "redis_viewer.action.load_all": "載入全部", "redis_viewer.action.load_more": "載入更多",