feat: Cluster mode support
- NewConnectionDialog: cluster checkbox - connection.ts: use Redis.Cluster() when cluster is enabled - i18n keys for cluster (zh-CN + en)
This commit is contained in:
+1
-1
@@ -9,7 +9,7 @@
|
||||
- [x] **SSH 隧道** — 类型已定义,NewConnectionDialog 需添加 host/port/user/password/privateKey/passphrase 表单
|
||||
- [x] **SSL/TLS 证书路径** — CA / Cert / Key 文件路径 + SNI servername,当前仅 TLS 复选框
|
||||
- [x] **Sentinel 模式** — masterName + nodePassword 表单
|
||||
- [ ] **Cluster 开关** — UI 暴露 cluster 模式切换 + FAQ 说明
|
||||
- [x] **Cluster 开关** — UI 暴露 cluster 模式切换 + FAQ 说明
|
||||
|
||||
## P1 — 核心功能缺失
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface ConnectionOptions {
|
||||
sentinelPort?: number
|
||||
sentinelMasterName?: string
|
||||
sentinelNodePassword?: string
|
||||
cluster?: boolean
|
||||
sshEnabled?: boolean
|
||||
sshHost?: string
|
||||
sshPort?: number
|
||||
@@ -210,6 +211,42 @@ export async function connect(id: string, opts: ConnectionOptions): Promise<void
|
||||
return
|
||||
}
|
||||
|
||||
if (opts.cluster) {
|
||||
// Cluster mode
|
||||
const clusterOptions: any = {
|
||||
redisOptions: {
|
||||
username: opts.username || undefined,
|
||||
password: opts.password || undefined,
|
||||
tls: opts.tls ? {
|
||||
rejectUnauthorized: opts.tlsRejectUnauthorized || false,
|
||||
checkServerIdentity: () => undefined,
|
||||
} : undefined,
|
||||
},
|
||||
clusterRetryStrategy(times: number) {
|
||||
if (times > 3) return null
|
||||
return Math.min(times * 200, 1000)
|
||||
},
|
||||
enableReadyCheck: true,
|
||||
}
|
||||
const redis = new Redis.Cluster([{ host: opts.host, port: opts.port }], clusterOptions)
|
||||
|
||||
redis.once('ready', () => {
|
||||
clients.set(id, { client: redis as any })
|
||||
resolve()
|
||||
})
|
||||
redis.once('error', (err: Error) => {
|
||||
redis.disconnect()
|
||||
reject(err)
|
||||
})
|
||||
setTimeout(() => {
|
||||
if (!clients.has(id)) {
|
||||
redis.disconnect()
|
||||
reject(new Error('连接超时,请检查主机和端口'))
|
||||
}
|
||||
}, 12000)
|
||||
return
|
||||
}
|
||||
|
||||
if (opts.sshEnabled) {
|
||||
const tunnel = await createSshTunnel(opts)
|
||||
sshClient = tunnel.sshClient
|
||||
|
||||
@@ -27,6 +27,7 @@ const form = reactive({
|
||||
sentinelPort: 26379,
|
||||
sentinelMasterName: '',
|
||||
sentinelNodePassword: '',
|
||||
cluster: false,
|
||||
separator: ':',
|
||||
sshEnabled: false,
|
||||
sshHost: '',
|
||||
@@ -62,6 +63,7 @@ watch(() => props.visible, async (v) => {
|
||||
sentinelPort: props.connection.sentinelPort || 26379,
|
||||
sentinelMasterName: props.connection.sentinelMasterName || '',
|
||||
sentinelNodePassword: props.connection.sentinelNodePassword || '',
|
||||
cluster: props.connection.cluster || false,
|
||||
separator: props.connection.separator,
|
||||
sshEnabled: props.connection.sshEnabled || false,
|
||||
sshHost: props.connection.sshHost || '',
|
||||
@@ -79,6 +81,7 @@ watch(() => props.visible, async (v) => {
|
||||
tlsRejectUnauthorized: false,
|
||||
sentinelEnabled: false, sentinelHost: '', sentinelPort: 26379,
|
||||
sentinelMasterName: '', sentinelNodePassword: '',
|
||||
cluster: false,
|
||||
separator: ':',
|
||||
sshEnabled: false, sshHost: '', sshPort: 22,
|
||||
sshUsername: '', sshPassword: '', sshPrivateKeyPath: '', sshPassphrase: '',
|
||||
@@ -147,6 +150,7 @@ async function handleTest() {
|
||||
sentinelPort: Number(form.sentinelPort) || 26379,
|
||||
sentinelMasterName: form.sentinelMasterName.trim(),
|
||||
sentinelNodePassword: form.sentinelNodePassword,
|
||||
cluster: form.cluster,
|
||||
sshEnabled: form.sshEnabled,
|
||||
sshHost: form.sshHost.trim(),
|
||||
sshPort: Number(form.sshPort) || 22,
|
||||
@@ -189,6 +193,7 @@ async function handleSave() {
|
||||
sentinelPort: Number(form.sentinelPort) || 26379,
|
||||
sentinelMasterName: form.sentinelMasterName.trim(),
|
||||
sentinelNodePassword: form.sentinelNodePassword,
|
||||
cluster: form.cluster,
|
||||
sshEnabled: form.sshEnabled,
|
||||
sshHost: form.sshHost.trim(),
|
||||
sshPort: Number(form.sshPort) || 22,
|
||||
@@ -196,8 +201,6 @@ async function handleSave() {
|
||||
sshPassword: form.sshPassword,
|
||||
sshPrivateKeyPath: form.sshPrivateKeyPath,
|
||||
sshPassphrase: form.sshPassphrase,
|
||||
cluster: false,
|
||||
sentinelMasterName: '',
|
||||
separator: form.separator || ':',
|
||||
order: props.connection?.order ?? connStore.connections.length,
|
||||
createdAt: props.connection?.createdAt ?? Date.now(),
|
||||
@@ -299,6 +302,12 @@ async function handleSave() {
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- Cluster -->
|
||||
<el-divider />
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="form.cluster">{{ t('connection.cluster') }}</el-checkbox>
|
||||
</el-form-item>
|
||||
|
||||
<!-- SSH Tunnel -->
|
||||
<el-divider />
|
||||
<el-form-item>
|
||||
|
||||
@@ -39,6 +39,7 @@ export default {
|
||||
sentinelPort: 'Sentinel Port',
|
||||
sentinelMasterName: 'Master Name',
|
||||
sentinelNodePassword: 'Sentinel Password',
|
||||
cluster: 'Cluster Mode',
|
||||
ssh: 'SSH Tunnel',
|
||||
sshHost: 'SSH Host',
|
||||
sshPort: 'SSH Port',
|
||||
|
||||
@@ -39,6 +39,7 @@ export default {
|
||||
sentinelPort: '哨兵端口',
|
||||
sentinelMasterName: 'Master 名称',
|
||||
sentinelNodePassword: '哨兵密码',
|
||||
cluster: 'Cluster 集群模式',
|
||||
ssh: 'SSH 隧道',
|
||||
sshHost: 'SSH 主机',
|
||||
sshPort: 'SSH 端口',
|
||||
|
||||
@@ -97,6 +97,7 @@ export const useConnectionStore = defineStore('connection', () => {
|
||||
sentinelPort: conn.sentinelPort,
|
||||
sentinelMasterName: conn.sentinelMasterName,
|
||||
sentinelNodePassword: conn.sentinelNodePassword,
|
||||
cluster: conn.cluster,
|
||||
sshEnabled: conn.sshEnabled,
|
||||
sshHost: conn.sshHost,
|
||||
sshPort: conn.sshPort,
|
||||
@@ -168,6 +169,7 @@ export const useConnectionStore = defineStore('connection', () => {
|
||||
tlsCaPath?: string; tlsCertPath?: string; tlsKeyPath?: string; tlsRejectUnauthorized?: boolean
|
||||
sentinelEnabled?: boolean; sentinelHost?: string; sentinelPort?: number
|
||||
sentinelMasterName?: string; sentinelNodePassword?: string
|
||||
cluster?: boolean
|
||||
sshEnabled?: boolean; sshHost?: string; sshPort?: number; sshUsername?: string
|
||||
sshPassword?: string; sshPrivateKeyPath?: string; sshPassphrase?: string
|
||||
}): Promise<{ success: boolean; error?: string }> {
|
||||
@@ -191,6 +193,7 @@ export const useConnectionStore = defineStore('connection', () => {
|
||||
sentinelPort: opts.sentinelPort,
|
||||
sentinelMasterName: opts.sentinelMasterName,
|
||||
sentinelNodePassword: opts.sentinelNodePassword,
|
||||
cluster: opts.cluster,
|
||||
sshEnabled: opts.sshEnabled,
|
||||
sshHost: opts.sshHost,
|
||||
sshPort: opts.sshPort,
|
||||
|
||||
Reference in New Issue
Block a user