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})
-
+
+
+ }
+ style={{
+ width: 26,
+ height: 26,
+ padding: 0,
+ borderRadius: 999,
+ borderColor: workbenchTheme.actionSecondaryBorder,
+ background: workbenchTheme.actionSecondaryBg,
+ color: workbenchTheme.accent,
+ boxShadow: 'none',
+ }}
+ onMouseDown={stopTreeTitleEvent}
+ onClick={(event) => {
+ stopTreeTitleEvent(event);
+ handleFilterByGroup(treeNode);
+ }}
+ />
+
+
+
);
}
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": "載入更多",