From 32273b4d6e27a1c74e7cd604230440cf0ba8e769 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 26 May 2026 19:44:23 +0800 Subject: [PATCH] feat: support Redis Sentinel connections --- Cargo.lock | 1 + .../connection/ConnectionDialog.vue | 130 ++++++++++++++- apps/desktop/src/i18n/locales/en.ts | 9 ++ apps/desktop/src/i18n/locales/es.ts | 9 ++ apps/desktop/src/i18n/locales/zh-CN.ts | 9 ++ apps/desktop/src/types/database.ts | 6 + crates/dbx-core/Cargo.toml | 2 +- crates/dbx-core/src/connection.rs | 12 +- crates/dbx-core/src/connection_secrets.rs | 28 +++- crates/dbx-core/src/db/redis_driver.rs | 150 +++++++++++++++++- crates/dbx-core/src/models/connection.rs | 23 +++ crates/dbx-core/src/query.rs | 6 + crates/dbx-core/src/storage.rs | 3 + crates/dbx-core/src/transfer.rs | 6 + .../connectionDialogRedisSentinel.test.ts | 50 ++++++ packages/node-core/src/connections.ts | 22 +++ src-tauri/src/commands/connection.rs | 16 +- 17 files changed, 470 insertions(+), 12 deletions(-) create mode 100644 packages/app-tests/connectionDialogRedisSentinel.test.ts diff --git a/Cargo.lock b/Cargo.lock index 311926253b..3d3706fc81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6005,6 +6005,7 @@ dependencies = [ "num-bigint", "percent-encoding", "pin-project-lite", + "rand 0.9.4", "rustls 0.23.40", "rustls-native-certs 0.8.3", "ryu", diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index dab43c47ec..4d998ccaa3 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -97,6 +97,12 @@ const defaultForm = (): Omit => ({ connection_string: undefined, jdbc_driver_class: undefined, jdbc_driver_paths: [], + redis_connection_mode: "standalone", + redis_sentinel_master: "", + redis_sentinel_nodes: "", + redis_sentinel_username: "", + redis_sentinel_password: "", + redis_sentinel_tls: false, }); const form = ref(defaultForm()); @@ -348,6 +354,12 @@ watch( connection_string: config.connection_string, jdbc_driver_class: config.jdbc_driver_class, jdbc_driver_paths: config.jdbc_driver_paths || [], + redis_connection_mode: config.redis_connection_mode || "standalone", + redis_sentinel_master: config.redis_sentinel_master || "", + redis_sentinel_nodes: config.redis_sentinel_nodes || "", + redis_sentinel_username: config.redis_sentinel_username || "", + redis_sentinel_password: config.redis_sentinel_password || "", + redis_sentinel_tls: config.redis_sentinel_tls || false, }; selectedType.value = profile; mongoUseUrl.value = !!config.connection_string; @@ -676,6 +688,30 @@ function connectionConfigForSubmit(id: string): ConnectionConfig { } else { config.oracle_connection_type = config.oracle_connection_type || "service_name"; } + if (config.db_type !== "redis") { + config.redis_connection_mode = undefined; + config.redis_sentinel_master = undefined; + config.redis_sentinel_nodes = undefined; + config.redis_sentinel_username = undefined; + config.redis_sentinel_password = undefined; + config.redis_sentinel_tls = undefined; + } else if (config.redis_connection_mode === "sentinel") { + config.redis_sentinel_master = config.redis_sentinel_master?.trim() || ""; + config.redis_sentinel_nodes = normalizeRedisSentinelNodes(config.redis_sentinel_nodes || ""); + config.redis_sentinel_username = config.redis_sentinel_username?.trim() || ""; + const firstNode = firstRedisSentinelEndpoint(config.redis_sentinel_nodes); + if (firstNode) { + config.host = firstNode.host; + config.port = firstNode.port; + } + } else { + config.redis_connection_mode = "standalone"; + config.redis_sentinel_master = undefined; + config.redis_sentinel_nodes = undefined; + config.redis_sentinel_username = undefined; + config.redis_sentinel_password = undefined; + config.redis_sentinel_tls = undefined; + } if (config.db_type !== "clickhouse") { config.ca_cert_path = undefined; } else { @@ -694,6 +730,45 @@ function connectionConfigForSubmit(id: string): ConnectionConfig { return config; } +function normalizeRedisSentinelNodes(value: string): string { + return value + .split(/[\n,;]+/) + .map((node) => node.trim()) + .filter(Boolean) + .join("\n"); +} + +function firstRedisSentinelEndpoint(value?: string): { host: string; port: number } | null { + const first = normalizeRedisSentinelNodes(value || "") + .split("\n") + .find(Boolean); + if (!first) return null; + return parseRedisEndpoint(first, 26379); +} + +function parseRedisEndpoint(value: string, defaultPort: number): { host: string; port: number } { + const endpoint = value + .trim() + .replace(/^rediss?:\/\//, "") + .replace(/^.*@/, "") + .replace(/[/?#].*$/, ""); + if (endpoint.startsWith("[")) { + const end = endpoint.indexOf("]"); + if (end > 0) { + const host = endpoint.slice(1, end); + const portText = endpoint.slice(end + 1).replace(/^:/, ""); + const port = Number(portText); + return { host, port: Number.isFinite(port) && port > 0 ? port : defaultPort }; + } + } + const parts = endpoint.split(":"); + if (parts.length === 2) { + const port = Number(parts[1]); + return { host: parts[0], port: Number.isFinite(port) && port > 0 ? port : defaultPort }; + } + return { host: endpoint, port: defaultPort }; +} + function resetTestState() { testRunId += 1; isTesting.value = false; @@ -1344,10 +1419,63 @@ function openExternalUrl(url: string) {