From 8519748512739e3898387968c32aaaacf4db0c0e Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 12 Jun 2026 17:22:09 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(redis):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=B6=85=E8=BF=8716=E4=B8=AA=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端改为通过 CONFIG GET databases 动态获取 Redis 数据库数量 - 放宽单机和 Sentinel 模式的 RedisDB 索引限制,支持 db16 及以上 - 前端连接配置和持久化不再裁剪高编号 Redis 数据库 - 连接测试成功后按服务端返回的真实数据库列表展示可选 DB - 增加 Redis db31 展示、切换、保存和 URI 解析回归测试 Refs #558 --- frontend/src/components/ConnectionModal.tsx | 132 ++++++++++++++---- .../ConnectionModalRedisSections.tsx | 2 +- frontend/src/store.test.ts | 24 ++++ frontend/src/store.ts | 10 +- frontend/src/types.ts | 4 +- frontend/src/utils/redisConnectionUri.test.ts | 2 +- frontend/src/utils/redisConnectionUri.ts | 5 +- internal/app/db_context.go | 2 +- internal/app/db_context_test.go | 13 ++ internal/redis/redis.go | 2 +- internal/redis/redis_impl.go | 120 ++++++++++------ internal/redis/redis_impl_test.go | 131 ++++++++++++++++- 12 files changed, 369 insertions(+), 78 deletions(-) diff --git a/frontend/src/components/ConnectionModal.tsx b/frontend/src/components/ConnectionModal.tsx index ab6ed7f..27c60e2 100644 --- a/frontend/src/components/ConnectionModal.tsx +++ b/frontend/src/components/ConnectionModal.tsx @@ -117,6 +117,7 @@ import { MongoDiscoverMembers, TestConnection, RedisConnect, + RedisGetDatabases, SelectDatabaseFile, SelectCertificateFile, SelectSSHKeyFile, @@ -141,6 +142,7 @@ const MAX_URI_HOSTS = 32; const MAX_TIMEOUT_SECONDS = 3600; const CONNECTION_MODAL_WIDTH = 960; const CONNECTION_MODAL_BODY_HEIGHT = 620; +const REDIS_DEFAULT_DATABASE_COUNT = 16; const STEP1_SIDEBAR_DIVIDER_DARK = "rgba(255, 255, 255, 0.16)"; const STEP1_SIDEBAR_DIVIDER_LIGHT = "rgba(0, 0, 0, 0.08)"; const CLICKHOUSE_PROTOCOL_OPTIONS: Array<{ @@ -158,6 +160,62 @@ const OCEANBASE_PROTOCOL_OPTIONS: Array<{ { value: "mysql", label: "MySQL" }, { value: "oracle", label: "Oracle" }, ]; + +const normalizeRedisDatabaseIndex = (value: unknown): number | null => { + const parsed = Number(value); + if (!Number.isFinite(parsed) || parsed < 0) return null; + return Math.trunc(parsed); +}; + +const buildRedisDatabaseList = (...values: unknown[]): number[] => { + const indexes = new Set(); + for (let i = 0; i < REDIS_DEFAULT_DATABASE_COUNT; i += 1) { + indexes.add(i); + } + const collect = (value: unknown) => { + if (Array.isArray(value)) { + value.forEach(collect); + return; + } + const index = normalizeRedisDatabaseIndex(value); + if (index !== null) { + indexes.add(index); + } + }; + values.forEach(collect); + return Array.from(indexes).sort((a, b) => a - b); +}; + +const extractRedisDatabaseList = (value: unknown): number[] => { + if (!Array.isArray(value)) return []; + const indexes = new Set(); + value.forEach((row: any) => { + const index = normalizeRedisDatabaseIndex(row?.index ?? row?.Index); + if (index !== null) { + indexes.add(index); + } + }); + const result = Array.from(indexes).sort((a, b) => a - b); + return result.length > 0 ? result : buildRedisDatabaseList(); +}; + +const normalizeRedisDatabaseSelection = ( + value: unknown, + supportedDbs: number[], +): number[] | undefined => { + if (!Array.isArray(value)) return undefined; + const supported = new Set(supportedDbs); + const selected = Array.from( + new Set( + value + .map(normalizeRedisDatabaseIndex) + .filter((index): index is number => index !== null) + .filter((index) => supported.size === 0 || supported.has(index)), + ), + ).sort((a, b) => a - b); + return selected.length > 0 ? selected : undefined; +}; + const normalizeClickHouseProtocolValue = ( value: unknown, ): ClickHouseProtocolChoice => { @@ -290,7 +348,7 @@ const ConnectionModal: React.FC<{ } | null>(null); const [testErrorLogOpen, setTestErrorLogOpen] = useState(false); const [dbList, setDbList] = useState([]); - const [redisDbList, setRedisDbList] = useState([]); // Redis databases 0-15 + const [redisDbList, setRedisDbList] = useState([]); const [mongoMembers, setMongoMembers] = useState([]); const [discoveringMembers, setDiscoveringMembers] = useState(false); const [uriFeedback, setUriFeedback] = useState<{ @@ -728,19 +786,17 @@ const ConnectionModal: React.FC<{ ) { form.setFieldValue("port", 6379); } - const supportedDbs = Array.from({ length: 16 }, (_, i) => i); + const supportedDbs = buildRedisDatabaseList( + form.getFieldValue("redisDB"), + form.getFieldValue("includeRedisDatabases"), + ); setRedisDbList(supportedDbs); - const selectedDbsRaw = form.getFieldValue("includeRedisDatabases"); - const selectedDbs = Array.isArray(selectedDbsRaw) - ? selectedDbsRaw.map((entry: any) => Number(entry)) - : []; - const validDbs = selectedDbs - .filter((entry: number) => Number.isFinite(entry)) - .map((entry: number) => Math.trunc(entry)) - .filter((entry: number) => supportedDbs.includes(entry)); form.setFieldValue( "includeRedisDatabases", - validDbs.length > 0 ? validDbs : undefined, + normalizeRedisDatabaseSelection( + form.getFieldValue("includeRedisDatabases"), + supportedDbs, + ), ); } if (fieldName === "proxyType") { @@ -2472,7 +2528,12 @@ const ConnectionModal: React.FC<{ } // 如果是 Redis 编辑模式,设置已保存的 Redis 数据库列表 if (configType === "redis") { - setRedisDbList(Array.from({ length: 16 }, (_, i) => i)); + setRedisDbList( + buildRedisDatabaseList( + config.redisDB, + initialValues.includeRedisDatabases, + ), + ); } } else { // Create mode: Start at step 1 @@ -2878,7 +2939,32 @@ const ConnectionModal: React.FC<{ void message.destroy("connection-test-failure"); setTestResult({ type: "success", message: res.message }); if (isRedisType) { - setRedisDbList(Array.from({ length: 16 }, (_, i) => i)); + const dbRes = await withClientTimeout( + RedisGetDatabases(config as any), + rpcTimeoutMs, + `连接成功但拉取 Redis 数据库列表超时(>${timeoutSeconds} 秒)`, + ); + if (dbRes.success) { + const supportedDbs = extractRedisDatabaseList(dbRes.data); + setRedisDbList(supportedDbs); + form.setFieldValue( + "includeRedisDatabases", + normalizeRedisDatabaseSelection( + form.getFieldValue("includeRedisDatabases"), + supportedDbs, + ), + ); + } else { + setRedisDbList( + buildRedisDatabaseList( + config.redisDB, + form.getFieldValue("includeRedisDatabases"), + ), + ); + message.warning( + `连接成功,但获取 Redis 数据库列表失败:${normalizeConnectionSecretErrorMessage(dbRes.message, "未知错误")}`, + ); + } } else if (!isJVMType) { // Other databases: fetch database list const dbRes = await withClientTimeout( @@ -3419,7 +3505,7 @@ const ConnectionModal: React.FC<{ connectionParams: normalizedConnectionParams, timeout: Number(mergedValues.timeout || 30), redisDB: Number.isFinite(Number(mergedValues.redisDB)) - ? Math.max(0, Math.min(15, Math.trunc(Number(mergedValues.redisDB)))) + ? Math.max(0, Math.trunc(Number(mergedValues.redisDB))) : 0, redisSentinelMaster: redisSentinelMaster, redisSentinelUser: redisSentinelUser, @@ -5957,19 +6043,17 @@ const ConnectionModal: React.FC<{ ) { form.setFieldValue("port", 6379); } - const supportedDbs = Array.from({ length: 16 }, (_, i) => i); + const supportedDbs = buildRedisDatabaseList( + form.getFieldValue("redisDB"), + form.getFieldValue("includeRedisDatabases"), + ); setRedisDbList(supportedDbs); - const selectedDbsRaw = form.getFieldValue("includeRedisDatabases"); - const selectedDbs = Array.isArray(selectedDbsRaw) - ? selectedDbsRaw.map((entry: any) => Number(entry)) - : []; - const validDbs = selectedDbs - .filter((entry: number) => Number.isFinite(entry)) - .map((entry: number) => Math.trunc(entry)) - .filter((entry: number) => supportedDbs.includes(entry)); form.setFieldValue( "includeRedisDatabases", - validDbs.length > 0 ? validDbs : undefined, + normalizeRedisDatabaseSelection( + form.getFieldValue("includeRedisDatabases"), + supportedDbs, + ), ); } if ( diff --git a/frontend/src/components/ConnectionModalRedisSections.tsx b/frontend/src/components/ConnectionModalRedisSections.tsx index a60c2df..33a794e 100644 --- a/frontend/src/components/ConnectionModalRedisSections.tsx +++ b/frontend/src/components/ConnectionModalRedisSections.tsx @@ -224,7 +224,7 @@ const ConnectionModalRedisSections: React.FC >