Files
MyGoNavi/frontend/src/components/RedisViewerKeyToolbar.tsx
Syngnat fce50b513c 🐛 fix(sql-editor): 修复 Oracle 事务结束并补充 Redis 拓扑提示
- SQL 编辑器:Oracle 托管事务优先使用 transaction provider 完成提交和回滚

- Redis:拆分 Key 浏览工具栏并展示 Cluster/Sentinel 拓扑上下文

- 测试:补充 Oracle 事务结束和 Redis 拓扑头部回归用例
2026-06-12 08:48:08 +08:00

153 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import { Button, Input, Popconfirm, Radio, Space, Tag } from 'antd';
import type { RadioChangeEvent } from 'antd';
import { DeleteOutlined, PlusOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
import type { SavedConnection } from '../types';
import { noAutoCapInputProps } from '../utils/inputAutoCap';
import type { RedisSearchMode } from '../utils/redisSearchPattern';
const { Search } = Input;
const normalizeText = (value: unknown): string => String(value || '').trim();
const normalizeRedisTopology = (connection?: SavedConnection): 'single' | 'cluster' | 'sentinel' => {
const topology = normalizeText(connection?.config?.topology).toLowerCase();
if (topology === 'sentinel') return 'sentinel';
if (topology === 'cluster') return 'cluster';
const extraHosts = Array.isArray(connection?.config?.hosts) ? connection.config.hosts.filter(Boolean) : [];
return extraHosts.length > 0 ? 'cluster' : 'single';
};
const buildRedisSeedAddresses = (connection?: SavedConnection): string[] => {
if (!connection) return [];
const config = connection.config || {};
const port = Number.isFinite(Number(config.port)) ? Number(config.port) : 6379;
const primary = normalizeText(config.host) ? `${normalizeText(config.host)}:${port}` : '';
const extraHosts = Array.isArray(config.hosts)
? config.hosts.map((host) => normalizeText(host)).filter(Boolean)
: [];
return [primary, ...extraHosts].filter(Boolean);
};
const getRedisTopologyLabel = (topology: 'single' | 'cluster' | 'sentinel'): string => {
if (topology === 'cluster') return 'Cluster';
if (topology === 'sentinel') return 'Sentinel';
return '单机';
};
type RedisViewerKeyToolbarProps = {
isV2Ui: boolean;
redisDB: number;
connection?: SavedConnection;
keyCount: number;
selectedKeyCount: number;
searchMode: RedisSearchMode;
searchInput: string;
mutedPillTagStyle: React.CSSProperties;
actionButtonStyle: React.CSSProperties;
primaryActionButtonStyle: React.CSSProperties;
dangerActionButtonStyle: React.CSSProperties;
textMutedColor: string;
textPrimaryColor: string;
onSearchModeChange: (event: RadioChangeEvent) => void;
onSearchInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
onSearch: (value: string) => void;
onRefresh: () => void;
onCreateKey: () => void;
onSelectAllLoadedKeys: () => void;
onClearAllSelectedKeys: () => void;
onDeleteSelectedKeys: () => void;
};
const RedisViewerKeyToolbar: React.FC<RedisViewerKeyToolbarProps> = ({
isV2Ui,
redisDB,
connection,
keyCount,
selectedKeyCount,
searchMode,
searchInput,
mutedPillTagStyle,
actionButtonStyle,
primaryActionButtonStyle,
dangerActionButtonStyle,
textMutedColor,
textPrimaryColor,
onSearchModeChange,
onSearchInputChange,
onSearch,
onRefresh,
onCreateKey,
onSelectAllLoadedKeys,
onClearAllSelectedKeys,
onDeleteSelectedKeys,
}) => {
const topology = normalizeRedisTopology(connection);
const seedAddresses = buildRedisSeedAddresses(connection);
const sentinelMaster = topology === 'sentinel'
? normalizeText(connection?.config?.redisSentinelMaster)
: '';
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12, marginBottom: 12 }}>
<div>
<div style={{ fontSize: 12, textTransform: 'uppercase', letterSpacing: '.08em', color: textMutedColor, fontWeight: 600 }}>Key Explorer</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginTop: 4 }}>
<div style={{ fontSize: 24, fontWeight: 700, color: textPrimaryColor }}>db{redisDB}</div>
<Tag style={mutedPillTagStyle}>{getRedisTopologyLabel(topology)}</Tag>
{topology !== 'single' && (
<Tag style={mutedPillTagStyle}>{seedAddresses.length || 1} </Tag>
)}
{sentinelMaster && (
<Tag style={mutedPillTagStyle}>master: {sentinelMaster}</Tag>
)}
</div>
</div>
<Tag style={mutedPillTagStyle}>{keyCount} Keys</Tag>
</div>
<Space.Compact style={{ width: '100%' }}>
<Radio.Group
value={searchMode}
onChange={onSearchModeChange}
buttonStyle="solid"
style={{ flexShrink: 0 }}
>
<Radio.Button value="fuzzy"></Radio.Button>
<Radio.Button value="exact"></Radio.Button>
</Radio.Group>
<Search
{...noAutoCapInputProps}
style={{ flex: 1 }}
placeholder={searchMode === 'exact' ? '输入完整 Key / 命名空间精确搜索' : '搜索 Key模糊匹配'}
value={searchInput}
onChange={onSearchInputChange}
onSearch={onSearch}
allowClear
enterButton={<SearchOutlined />}
/>
</Space.Compact>
<div className={isV2Ui ? 'gn-v2-redis-toolbar' : undefined} style={{ marginTop: 12, display: 'flex', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
<Space wrap size={8}>
<Button size="small" style={actionButtonStyle} icon={<ReloadOutlined />} onClick={onRefresh}></Button>
<Button size="small" style={actionButtonStyle} icon={<PlusOutlined />} onClick={onCreateKey}></Button>
<Button size="small" style={primaryActionButtonStyle} onClick={onSelectAllLoadedKeys} disabled={keyCount === 0}></Button>
<Button size="small" style={actionButtonStyle} onClick={onClearAllSelectedKeys} disabled={selectedKeyCount === 0}></Button>
</Space>
<Popconfirm
title={`确定删除选中的 ${selectedKeyCount} 个 Key`}
onConfirm={onDeleteSelectedKeys}
disabled={selectedKeyCount === 0}
>
<Button size="small" style={dangerActionButtonStyle} icon={<DeleteOutlined />} disabled={selectedKeyCount === 0}>
({selectedKeyCount})
</Button>
</Popconfirm>
</div>
</div>
);
};
export default RedisViewerKeyToolbar;