Add Redis cluster support and improve key loading

This commit is contained in:
t8y2 committed 2026-05-28 00:08:59 +08:00
1 parent 66ce9a368e
commit 797c0d8d21
20 files changed
+932 -201

No files matched your search

@@ -105,6 +105,7 @@ const defaultForm = (): Omit<ConnectionConfig, "id"> => ({
redis_sentinel_username: "",
redis_sentinel_password: "",
redis_sentinel_tls: false,
redis_cluster_nodes: "",
});
const form = ref(defaultForm());
@@ -362,6 +363,7 @@ watch(
redis_sentinel_username: config.redis_sentinel_username || "",
redis_sentinel_password: config.redis_sentinel_password || "",
redis_sentinel_tls: config.redis_sentinel_tls || false,
redis_cluster_nodes: config.redis_cluster_nodes || "",
};
selectedType.value = profile;
mongoUseUrl.value = !!config.connection_string;
@@ -753,15 +755,29 @@ function connectionConfigForSubmit(id: string): ConnectionConfig {
config.redis_sentinel_username = undefined;
config.redis_sentinel_password = undefined;
config.redis_sentinel_tls = undefined;
config.redis_cluster_nodes = 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() || "";
config.redis_cluster_nodes = undefined;
const firstNode = firstRedisSentinelEndpoint(config.redis_sentinel_nodes);
if (firstNode) {
config.host = firstNode.host;
config.port = firstNode.port;
}
} else if (config.redis_connection_mode === "cluster") {
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;
config.redis_cluster_nodes = normalizeRedisClusterNodes(config.redis_cluster_nodes || "");
const firstNode = firstRedisClusterEndpoint(config.redis_cluster_nodes);
if (firstNode) {
config.host = firstNode.host;
config.port = firstNode.port;
}
} else {
config.redis_connection_mode = "standalone";
config.redis_sentinel_master = undefined;
@@ -769,6 +785,7 @@ function connectionConfigForSubmit(id: string): ConnectionConfig {
config.redis_sentinel_username = undefined;
config.redis_sentinel_password = undefined;
config.redis_sentinel_tls = undefined;
config.redis_cluster_nodes = undefined;
}
if (config.db_type !== "mysql" && config.db_type !== "clickhouse") {
config.ca_cert_path = undefined;
@@ -875,6 +892,14 @@ function normalizePostgresSslMode(value: string): string {
}
function normalizeRedisSentinelNodes(value: string): string {
return normalizeRedisNodeList(value);
}
function normalizeRedisClusterNodes(value: string): string {
return normalizeRedisNodeList(value);
}
function normalizeRedisNodeList(value: string): string {
return value
.split(/[\n,;]+/)
.map((node) => node.trim())
@@ -883,13 +908,21 @@ function normalizeRedisSentinelNodes(value: string): string {
}
function firstRedisSentinelEndpoint(value?: string): { host: string; port: number } | null {
const first = normalizeRedisSentinelNodes(value || "")
const first = normalizeRedisNodeList(value || "")
.split("\n")
.find(Boolean);
if (!first) return null;
return parseRedisEndpoint(first, 26379);
}
function firstRedisClusterEndpoint(value?: string): { host: string; port: number } | null {
const first = normalizeRedisNodeList(value || "")
.split("\n")
.find(Boolean);
if (!first) return null;
return parseRedisEndpoint(first, 6379);
}
function parseRedisEndpoint(value: string, defaultPort: number): { host: string; port: number } {
const endpoint = value
.trim()
@@ -1616,7 +1649,7 @@ function openExternalUrl(url: string) {
<div class="col-span-3 flex gap-2">
<Button
size="sm"
:variant="form.redis_connection_mode === 'sentinel' ? 'outline' : 'default'"
:variant="form.redis_connection_mode === 'standalone' ? 'default' : 'outline'"
@click="form.redis_connection_mode = 'standalone'"
>
{{ t("connection.redisStandaloneMode") }}
@@ -1628,13 +1661,22 @@ function openExternalUrl(url: string) {
>
{{ t("connection.redisSentinelMode") }}
</Button>
<Button
size="sm"
:variant="form.redis_connection_mode === 'cluster' ? 'default' : 'outline'"
@click="form.redis_connection_mode = 'cluster'"
>
{{ t("connection.redisClusterMode") }}
</Button>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{
form.redis_connection_mode === "sentinel"
? t("connection.redisFirstSentinel")
: t("connection.host")
: form.redis_connection_mode === "cluster"
? t("connection.redisFirstClusterNode")
: t("connection.host")
}}</Label>
<Input v-model="form.host" class="col-span-2" />
<Input v-model.number="form.port" type="number" class="col-span-1" />
@@ -1669,6 +1711,17 @@ function openExternalUrl(url: string) {
</label>
</div>
</template>
<template v-else-if="form.redis_connection_mode === 'cluster'">
<div class="grid grid-cols-4 items-start gap-4">
<Label class="text-right mt-2">{{ t("connection.redisClusterNodes") }}</Label>
<textarea
v-model="form.redis_cluster_nodes"
class="col-span-3 flex min-h-[76px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
placeholder="redis-1:6379&#10;redis-2:6379"
spellcheck="false"
/>
</div>
</template>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.user") }}</Label>
<Input v-model="form.username" class="col-span-3" placeholder="default" />
@@ -196,6 +196,25 @@ async function streamValueSearch(requestId: number) {
}
}
async function fillInitialKeyBatch(requestId: number) {
const targetCount = Math.max(1, settingsStore.editorSettings.redisScanPageSize);
let rounds = 0;
while (
requestId === searchRequestId &&
searchMode.value === "key" &&
hasMore.value &&
flatKeys.value.length < targetCount
) {
const beforeCount = flatKeys.value.length;
const applied = await scanNextPage(requestId);
if (!applied) return;
rounds += 1;
if (flatKeys.value.length >= targetCount) return;
if (rounds >= 12 && flatKeys.value.length === beforeCount) return;
if (rounds >= 24) return;
}
}
async function loadKeys() {
const requestId = ++searchRequestId;
loading.value = true;
@@ -211,8 +230,12 @@ async function loadKeys() {
return;
}
const applied = await scanNextPage(requestId);
if (applied && searchMode.value === "value") {
await streamValueSearch(requestId);
if (applied) {
if (searchMode.value === "value") {
await streamValueSearch(requestId);
} else {
await fillInitialKeyBatch(requestId);
}
}
} finally {
if (requestId === searchRequestId) {
+3
View File
@@ -162,8 +162,11 @@ export default {
modeForm: "Form",
redisStandaloneMode: "Standalone",
redisSentinelMode: "Sentinel",
redisClusterMode: "Cluster",
redisFirstSentinel: "First Sentinel",
redisFirstClusterNode: "First Seed Node",
redisSentinelNodes: "Sentinel Nodes",
redisClusterNodes: "Cluster Seed Nodes",
redisSentinelMaster: "Master Name",
redisSentinelUser: "Sentinel User",
redisSentinelPassword: "Sentinel Password",
+3
View File
@@ -154,8 +154,11 @@ export default {
modeForm: "Formulario",
redisStandaloneMode: "Directa",
redisSentinelMode: "Sentinel",
redisClusterMode: "Cluster",
redisFirstSentinel: "Primer Sentinel",
redisFirstClusterNode: "Primer nodo semilla",
redisSentinelNodes: "Nodos Sentinel",
redisClusterNodes: "Nodos semilla del cluster",
redisSentinelMaster: "Nombre del master",
redisSentinelUser: "Usuario Sentinel",
redisSentinelPassword: "Contraseña Sentinel",
+3
View File
@@ -159,8 +159,11 @@ export default {
modeForm: "表单",
redisStandaloneMode: "直连",
redisSentinelMode: "哨兵",
redisClusterMode: "集群",
redisFirstSentinel: "首个哨兵",
redisFirstClusterNode: "首个种子节点",
redisSentinelNodes: "哨兵节点",
redisClusterNodes: "集群种子节点",
redisSentinelMaster: "Master 名称",
redisSentinelUser: "哨兵用户名",
redisSentinelPassword: "哨兵密码",
+2 -1
View File
@@ -87,12 +87,13 @@ export interface ConnectionConfig {
connection_string?: string;
jdbc_driver_class?: string;
jdbc_driver_paths?: string[];
redis_connection_mode?: "standalone" | "sentinel";
redis_connection_mode?: "standalone" | "sentinel" | "cluster";
redis_sentinel_master?: string;
redis_sentinel_nodes?: string;
redis_sentinel_username?: string;
redis_sentinel_password?: string;
redis_sentinel_tls?: boolean;
redis_cluster_nodes?: string;
one_time?: boolean;
}