mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 16:53:35 +08:00
✨ feat(redis): 支持按命名空间快速模糊筛选
- 为 Redis Key 目录增加完整路径快捷筛选入口 - 自动回填搜索框并切换到模糊匹配模式 - 补齐多语言文案、无障碍标签和交互回归测试 Fixes #692
This commit is contained in:
@@ -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(<RedisViewer connectionId="redis-1" redisDB={0} />);
|
||||
});
|
||||
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 = [
|
||||
{
|
||||
|
||||
@@ -1092,6 +1092,16 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ 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<HTMLElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -1164,26 +1174,51 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
|
||||
</span>
|
||||
<span style={{ fontSize: 12, color: workbenchTheme.textMuted, flexShrink: 0 }}>({treeNode.groupLeafCount ?? 0})</span>
|
||||
</Space>
|
||||
<Button
|
||||
size="small"
|
||||
style={{
|
||||
paddingInline: 10,
|
||||
height: 26,
|
||||
borderRadius: 999,
|
||||
flexShrink: 0,
|
||||
borderColor: workbenchTheme.accentBorder,
|
||||
background: workbenchTheme.accentSoft,
|
||||
color: workbenchTheme.accent,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
onMouseDown={stopTreeTitleEvent}
|
||||
onClick={(event) => {
|
||||
stopTreeTitleEvent(event);
|
||||
handleSelectGroupDescendants(treeNode);
|
||||
}}
|
||||
>
|
||||
{groupFullyChecked ? tr('redis_viewer.action.clear_group_selection') : tr('redis_viewer.action.select_group')}
|
||||
</Button>
|
||||
<Space size={6} style={{ flexShrink: 0 }}>
|
||||
<Tooltip title={tr('redis_viewer.action.filter_group')}>
|
||||
<Button
|
||||
size="small"
|
||||
className="redis-tree-group-filter-button"
|
||||
aria-label={tr('redis_viewer.action.filter_group')}
|
||||
icon={<SearchOutlined />}
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Button
|
||||
size="small"
|
||||
style={{
|
||||
paddingInline: 10,
|
||||
height: 26,
|
||||
borderRadius: 999,
|
||||
flexShrink: 0,
|
||||
borderColor: workbenchTheme.accentBorder,
|
||||
background: workbenchTheme.accentSoft,
|
||||
color: workbenchTheme.accent,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
onMouseDown={stopTreeTitleEvent}
|
||||
onClick={(event) => {
|
||||
stopTreeTitleEvent(event);
|
||||
handleSelectGroupDescendants(treeNode);
|
||||
}}
|
||||
>
|
||||
{groupFullyChecked ? tr('redis_viewer.action.clear_group_selection') : tr('redis_viewer.action.select_group')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -17,9 +17,11 @@ type RedisKeyTreeGroup = {
|
||||
leafCount: number;
|
||||
};
|
||||
|
||||
export type RedisTreeDataNode = DataNode & {
|
||||
export type RedisTreeDataNode = Omit<DataNode, 'children'> & {
|
||||
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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "さらに読み込む",
|
||||
|
||||
@@ -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": "Загрузить еще",
|
||||
|
||||
@@ -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": "加载更多",
|
||||
|
||||
@@ -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": "載入更多",
|
||||
|
||||
Reference in New Issue
Block a user