mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-02 12:52:25 +08:00
♻️ refactor(connection): 拆分 Redis 连接配置区
- 抽离 Redis 单机、集群、哨兵配置区到独立组件 - 保留 Redis 密码、Sentinel 密钥和 DB 范围设置行为 - 同步更新连接弹窗源码级回归检查
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const source = readFileSync(new URL('./ConnectionModal.tsx', import.meta.url), 'utf8');
|
||||
const connectionModalSource = readFileSync(new URL('./ConnectionModal.tsx', import.meta.url), 'utf8');
|
||||
const redisSectionsSource = readFileSync(new URL('./ConnectionModalRedisSections.tsx', import.meta.url), 'utf8');
|
||||
const source = `${connectionModalSource}\n${redisSectionsSource}`;
|
||||
|
||||
describe('ConnectionModal edit password behavior', () => {
|
||||
it('keeps the prefilled primary password masked by default', () => {
|
||||
|
||||
@@ -97,6 +97,7 @@ import {
|
||||
TestJVMConnection,
|
||||
} from "../../wailsjs/go/app/App";
|
||||
import { ConnectionConfig, MongoMemberInfo, SavedConnection } from "../types";
|
||||
import ConnectionModalRedisSections from "./ConnectionModalRedisSections";
|
||||
|
||||
const { Text } = Typography;
|
||||
type EditableJVMMode = (typeof JVM_EDITABLE_MODES)[number];
|
||||
@@ -5637,182 +5638,19 @@ const ConnectionModal: React.FC<{
|
||||
),
|
||||
})}
|
||||
|
||||
{isRedis &&
|
||||
renderConfigSectionCard({
|
||||
sectionKey: "connectionMode",
|
||||
icon: <ClusterOutlined />,
|
||||
children: (
|
||||
<>
|
||||
{renderChoiceCards({
|
||||
fieldName: "redisTopology",
|
||||
value: String(redisTopology),
|
||||
options: [
|
||||
{
|
||||
value: "single",
|
||||
label: "单机模式",
|
||||
description: "只连接一个 Redis 节点。",
|
||||
},
|
||||
{
|
||||
value: "cluster",
|
||||
label: "集群模式",
|
||||
description: "Redis Cluster,配置多个种子节点。",
|
||||
},
|
||||
{
|
||||
value: "sentinel",
|
||||
label: "哨兵模式",
|
||||
description: "通过 Sentinel 发现主节点,适合主从高可用。",
|
||||
},
|
||||
],
|
||||
})}
|
||||
{(redisTopology === "cluster" ||
|
||||
redisTopology === "sentinel") && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="redisHosts"
|
||||
label={
|
||||
redisTopology === "sentinel"
|
||||
? "Sentinel 附加节点地址"
|
||||
: "集群附加节点地址"
|
||||
}
|
||||
help={
|
||||
redisTopology === "sentinel"
|
||||
? "上方主机地址作为第一个 Sentinel;这里填写其他 Sentinel 节点,格式:host:port"
|
||||
: "主节点使用上方主机地址;这里填写其他种子节点,格式:host:port"
|
||||
}
|
||||
style={{ marginTop: 16, marginBottom: 0 }}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder={
|
||||
redisTopology === "sentinel"
|
||||
? "例如:10.10.0.12:26379、10.10.0.13:26379"
|
||||
: "例如:10.10.0.12:6379、10.10.0.13:6379"
|
||||
}
|
||||
tokenSeparators={[",", ";", " "]}
|
||||
/>
|
||||
</Form.Item>
|
||||
{redisTopology === "sentinel" && (
|
||||
<Form.Item
|
||||
name="redisSentinelMaster"
|
||||
label="Sentinel master 名称"
|
||||
help="填写 Sentinel 配置中的 monitor 名称,例如 mymaster。"
|
||||
rules={[
|
||||
createUriAwareRequiredRule(
|
||||
"请输入 Sentinel master 名称",
|
||||
),
|
||||
]}
|
||||
style={{ marginTop: 16, marginBottom: 0 }}
|
||||
>
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
placeholder="例如:mymaster"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
})}
|
||||
|
||||
{isRedis &&
|
||||
renderConfigSectionCard({
|
||||
sectionKey: "credentials",
|
||||
icon: <SafetyCertificateOutlined />,
|
||||
children: (
|
||||
<>
|
||||
<Form.Item name="password" label="密码 (可选)">
|
||||
<Input.Password
|
||||
{...noAutoCapInputProps}
|
||||
visibilityToggle={{
|
||||
visible: primaryPasswordVisible,
|
||||
onVisibleChange: setPrimaryPasswordVisible,
|
||||
}}
|
||||
placeholder={getStoredSecretPlaceholder({
|
||||
hasStoredSecret: initialValues?.hasPrimaryPassword,
|
||||
emptyPlaceholder:
|
||||
"Redis 密码(如果设置了 requirepass)",
|
||||
retainedLabel: "已保存 Redis 密码",
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
{redisTopology === "sentinel" && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns:
|
||||
"repeat(2, minmax(0, 1fr))",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="redisSentinelUser"
|
||||
label="Sentinel 用户名(可选)"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
placeholder="留空表示 Sentinel 不使用 ACL 用户名"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="redisSentinelPassword"
|
||||
label="Sentinel 密码(可选)"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Input.Password
|
||||
{...noAutoCapInputProps}
|
||||
placeholder={getStoredSecretPlaceholder({
|
||||
hasStoredSecret:
|
||||
initialValues?.hasRedisSentinelPassword,
|
||||
emptyPlaceholder:
|
||||
"Sentinel 自身认证密码,留空则不发送",
|
||||
retainedLabel: "已保存 Sentinel 密码",
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
{renderStoredSecretControls({
|
||||
fieldName: "redisSentinelPassword",
|
||||
clearKey: "redisSentinelPassword",
|
||||
hasStoredSecret:
|
||||
initialValues?.hasRedisSentinelPassword,
|
||||
clearLabel: "清除已保存 Sentinel 密码",
|
||||
description:
|
||||
"当前已保存 Sentinel 密码。留空表示继续沿用,输入新值表示替换。",
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
})}
|
||||
|
||||
{isRedis &&
|
||||
renderConfigSectionCard({
|
||||
sectionKey: "databaseScope",
|
||||
icon: <DatabaseOutlined />,
|
||||
children: (
|
||||
<Form.Item
|
||||
name="includeRedisDatabases"
|
||||
label="显示数据库 (留空显示全部)"
|
||||
help="连接测试成功后可选择"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="选择显示的数据库 (0-15)"
|
||||
allowClear
|
||||
>
|
||||
{redisDbList.map((db) => (
|
||||
<Select.Option key={db} value={db}>
|
||||
db{db}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
),
|
||||
})}
|
||||
{isRedis && (
|
||||
<ConnectionModalRedisSections
|
||||
redisTopology={String(redisTopology)}
|
||||
redisDbList={redisDbList}
|
||||
initialValues={initialValues}
|
||||
primaryPasswordVisible={primaryPasswordVisible}
|
||||
setPrimaryPasswordVisible={setPrimaryPasswordVisible}
|
||||
renderChoiceCards={renderChoiceCards}
|
||||
renderConfigSectionCard={renderConfigSectionCard}
|
||||
renderStoredSecretControls={renderStoredSecretControls}
|
||||
createUriAwareRequiredRule={createUriAwareRequiredRule}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isFileDb &&
|
||||
!isRedis &&
|
||||
|
||||
242
frontend/src/components/ConnectionModalRedisSections.tsx
Normal file
242
frontend/src/components/ConnectionModalRedisSections.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
import React from "react";
|
||||
import { Form, Input, Select } from "antd";
|
||||
import {
|
||||
ClusterOutlined,
|
||||
DatabaseOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
import type { SavedConnection } from "../types";
|
||||
import {
|
||||
getStoredSecretPlaceholder,
|
||||
type ConnectionConfigSectionKey,
|
||||
} from "../utils/connectionModalPresentation";
|
||||
import { noAutoCapInputProps } from "../utils/inputAutoCap";
|
||||
|
||||
type ChoiceCardOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
type RenderChoiceCards = (params: {
|
||||
fieldName: string;
|
||||
value: string;
|
||||
options: ChoiceCardOption[];
|
||||
minWidth?: number;
|
||||
onSelect?: (value: string) => void;
|
||||
}) => React.ReactNode;
|
||||
|
||||
type RenderConfigSectionCard = (params: {
|
||||
sectionKey: ConnectionConfigSectionKey;
|
||||
icon: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
badge?: React.ReactNode;
|
||||
}) => React.ReactNode;
|
||||
|
||||
type RenderStoredSecretControls = (params: {
|
||||
fieldName: string;
|
||||
clearKey: "redisSentinelPassword";
|
||||
hasStoredSecret?: boolean;
|
||||
clearLabel: string;
|
||||
description: string;
|
||||
}) => React.ReactNode;
|
||||
|
||||
interface ConnectionModalRedisSectionsProps {
|
||||
redisTopology: string;
|
||||
redisDbList: number[];
|
||||
initialValues?: SavedConnection | null;
|
||||
primaryPasswordVisible: boolean;
|
||||
setPrimaryPasswordVisible: (visible: boolean) => void;
|
||||
renderChoiceCards: RenderChoiceCards;
|
||||
renderConfigSectionCard: RenderConfigSectionCard;
|
||||
renderStoredSecretControls: RenderStoredSecretControls;
|
||||
createUriAwareRequiredRule: (
|
||||
messageText: string,
|
||||
validateValue?: (value: unknown) => boolean,
|
||||
) => any;
|
||||
}
|
||||
|
||||
const ConnectionModalRedisSections: React.FC<ConnectionModalRedisSectionsProps> = ({
|
||||
redisTopology,
|
||||
redisDbList,
|
||||
initialValues,
|
||||
primaryPasswordVisible,
|
||||
setPrimaryPasswordVisible,
|
||||
renderChoiceCards,
|
||||
renderConfigSectionCard,
|
||||
renderStoredSecretControls,
|
||||
createUriAwareRequiredRule,
|
||||
}) => (
|
||||
<>
|
||||
{renderConfigSectionCard({
|
||||
sectionKey: "connectionMode",
|
||||
icon: <ClusterOutlined />,
|
||||
children: (
|
||||
<>
|
||||
{renderChoiceCards({
|
||||
fieldName: "redisTopology",
|
||||
value: String(redisTopology),
|
||||
options: [
|
||||
{
|
||||
value: "single",
|
||||
label: "单机模式",
|
||||
description: "只连接一个 Redis 节点。",
|
||||
},
|
||||
{
|
||||
value: "cluster",
|
||||
label: "集群模式",
|
||||
description: "Redis Cluster,配置多个种子节点。",
|
||||
},
|
||||
{
|
||||
value: "sentinel",
|
||||
label: "哨兵模式",
|
||||
description: "通过 Sentinel 发现主节点,适合主从高可用。",
|
||||
},
|
||||
],
|
||||
})}
|
||||
{(redisTopology === "cluster" || redisTopology === "sentinel") && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="redisHosts"
|
||||
label={
|
||||
redisTopology === "sentinel"
|
||||
? "Sentinel 附加节点地址"
|
||||
: "集群附加节点地址"
|
||||
}
|
||||
help={
|
||||
redisTopology === "sentinel"
|
||||
? "上方主机地址作为第一个 Sentinel;这里填写其他 Sentinel 节点,格式:host:port"
|
||||
: "主节点使用上方主机地址;这里填写其他种子节点,格式:host:port"
|
||||
}
|
||||
style={{ marginTop: 16, marginBottom: 0 }}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder={
|
||||
redisTopology === "sentinel"
|
||||
? "例如:10.10.0.12:26379、10.10.0.13:26379"
|
||||
: "例如:10.10.0.12:6379、10.10.0.13:6379"
|
||||
}
|
||||
tokenSeparators={[",", ";", " "]}
|
||||
/>
|
||||
</Form.Item>
|
||||
{redisTopology === "sentinel" && (
|
||||
<Form.Item
|
||||
name="redisSentinelMaster"
|
||||
label="Sentinel master 名称"
|
||||
help="填写 Sentinel 配置中的 monitor 名称,例如 mymaster。"
|
||||
rules={[
|
||||
createUriAwareRequiredRule(
|
||||
"请输入 Sentinel master 名称",
|
||||
),
|
||||
]}
|
||||
style={{ marginTop: 16, marginBottom: 0 }}
|
||||
>
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
placeholder="例如:mymaster"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
})}
|
||||
|
||||
{renderConfigSectionCard({
|
||||
sectionKey: "credentials",
|
||||
icon: <SafetyCertificateOutlined />,
|
||||
children: (
|
||||
<>
|
||||
<Form.Item name="password" label="密码 (可选)">
|
||||
<Input.Password
|
||||
{...noAutoCapInputProps}
|
||||
visibilityToggle={{
|
||||
visible: primaryPasswordVisible,
|
||||
onVisibleChange: setPrimaryPasswordVisible,
|
||||
}}
|
||||
placeholder={getStoredSecretPlaceholder({
|
||||
hasStoredSecret: initialValues?.hasPrimaryPassword,
|
||||
emptyPlaceholder: "Redis 密码(如果设置了 requirepass)",
|
||||
retainedLabel: "已保存 Redis 密码",
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
{redisTopology === "sentinel" && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="redisSentinelUser"
|
||||
label="Sentinel 用户名(可选)"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
placeholder="留空表示 Sentinel 不使用 ACL 用户名"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="redisSentinelPassword"
|
||||
label="Sentinel 密码(可选)"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Input.Password
|
||||
{...noAutoCapInputProps}
|
||||
placeholder={getStoredSecretPlaceholder({
|
||||
hasStoredSecret: initialValues?.hasRedisSentinelPassword,
|
||||
emptyPlaceholder: "Sentinel 自身认证密码,留空则不发送",
|
||||
retainedLabel: "已保存 Sentinel 密码",
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
{renderStoredSecretControls({
|
||||
fieldName: "redisSentinelPassword",
|
||||
clearKey: "redisSentinelPassword",
|
||||
hasStoredSecret: initialValues?.hasRedisSentinelPassword,
|
||||
clearLabel: "清除已保存 Sentinel 密码",
|
||||
description:
|
||||
"当前已保存 Sentinel 密码。留空表示继续沿用,输入新值表示替换。",
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
})}
|
||||
|
||||
{renderConfigSectionCard({
|
||||
sectionKey: "databaseScope",
|
||||
icon: <DatabaseOutlined />,
|
||||
children: (
|
||||
<Form.Item
|
||||
name="includeRedisDatabases"
|
||||
label="显示数据库 (留空显示全部)"
|
||||
help="连接测试成功后可选择"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="选择显示的数据库 (0-15)"
|
||||
allowClear
|
||||
>
|
||||
{redisDbList.map((db) => (
|
||||
<Select.Option key={db} value={db}>
|
||||
db{db}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
),
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
export default ConnectionModalRedisSections;
|
||||
Reference in New Issue
Block a user