feat: Sentinel mode support
- NewConnectionDialog: sentinel toggle + host/port/masterName/nodePassword form - connection.ts: use ioredis sentinels config when sentinelEnabled - Add sentinelEnabled/sentinelHost/sentinelPort/sentinelNodePassword to all types - i18n keys for all sentinel fields (zh-CN + en)
This commit is contained in:
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
- [x] **SSH 隧道** — 类型已定义,NewConnectionDialog 需添加 host/port/user/password/privateKey/passphrase 表单
|
- [x] **SSH 隧道** — 类型已定义,NewConnectionDialog 需添加 host/port/user/password/privateKey/passphrase 表单
|
||||||
- [x] **SSL/TLS 证书路径** — CA / Cert / Key 文件路径 + SNI servername,当前仅 TLS 复选框
|
- [x] **SSL/TLS 证书路径** — CA / Cert / Key 文件路径 + SNI servername,当前仅 TLS 复选框
|
||||||
- [ ] **Sentinel 模式** — masterName + nodePassword 表单
|
- [x] **Sentinel 模式** — masterName + nodePassword 表单
|
||||||
- [ ] **Cluster 开关** — UI 暴露 cluster 模式切换 + FAQ 说明
|
- [ ] **Cluster 开关** — UI 暴露 cluster 模式切换 + FAQ 说明
|
||||||
|
|
||||||
## P1 — 核心功能缺失
|
## P1 — 核心功能缺失
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ export interface ConnectionOptions {
|
|||||||
tlsCertPath?: string
|
tlsCertPath?: string
|
||||||
tlsKeyPath?: string
|
tlsKeyPath?: string
|
||||||
tlsRejectUnauthorized?: boolean
|
tlsRejectUnauthorized?: boolean
|
||||||
|
sentinelEnabled?: boolean
|
||||||
|
sentinelHost?: string
|
||||||
|
sentinelPort?: number
|
||||||
|
sentinelMasterName?: string
|
||||||
|
sentinelNodePassword?: string
|
||||||
sshEnabled?: boolean
|
sshEnabled?: boolean
|
||||||
sshHost?: string
|
sshHost?: string
|
||||||
sshPort?: number
|
sshPort?: number
|
||||||
@@ -163,6 +168,48 @@ export async function connect(id: string, opts: ConnectionOptions): Promise<void
|
|||||||
let tunnelServer: ReturnType<typeof createServer> | undefined
|
let tunnelServer: ReturnType<typeof createServer> | undefined
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (opts.sentinelEnabled && opts.sentinelMasterName) {
|
||||||
|
// Sentinel mode: connect via sentinel
|
||||||
|
const sentinelOptions: RedisOptions = {
|
||||||
|
sentinels: [{ host: opts.sentinelHost || opts.host, port: opts.sentinelPort || 26379 }],
|
||||||
|
name: opts.sentinelMasterName,
|
||||||
|
sentinelPassword: opts.sentinelNodePassword || undefined,
|
||||||
|
username: opts.username || undefined,
|
||||||
|
password: opts.password || undefined,
|
||||||
|
stringNumbers: true,
|
||||||
|
enableReadyCheck: true,
|
||||||
|
connectTimeout: 10000,
|
||||||
|
retryStrategy(times: number) {
|
||||||
|
if (times > 3) return null
|
||||||
|
return Math.min(times * 200, 1000)
|
||||||
|
},
|
||||||
|
maxRetriesPerRequest: 3,
|
||||||
|
}
|
||||||
|
if (opts.tls) {
|
||||||
|
sentinelOptions.tls = {
|
||||||
|
rejectUnauthorized: opts.tlsRejectUnauthorized || false,
|
||||||
|
checkServerIdentity: () => undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const redis = new Redis(sentinelOptions)
|
||||||
|
|
||||||
|
redis.once('ready', () => {
|
||||||
|
clients.set(id, { client: redis })
|
||||||
|
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) {
|
if (opts.sshEnabled) {
|
||||||
const tunnel = await createSshTunnel(opts)
|
const tunnel = await createSshTunnel(opts)
|
||||||
sshClient = tunnel.sshClient
|
sshClient = tunnel.sshClient
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ interface ConnectionConfig {
|
|||||||
sshPrivateKeyPath: string
|
sshPrivateKeyPath: string
|
||||||
sshPassphrase: string
|
sshPassphrase: string
|
||||||
cluster: boolean
|
cluster: boolean
|
||||||
|
sentinelEnabled: boolean
|
||||||
|
sentinelHost: string
|
||||||
|
sentinelPort: number
|
||||||
sentinelMasterName: string
|
sentinelMasterName: string
|
||||||
|
sentinelNodePassword: string
|
||||||
separator: string
|
separator: string
|
||||||
order: number
|
order: number
|
||||||
createdAt: number
|
createdAt: number
|
||||||
|
|||||||
Vendored
+4
@@ -19,7 +19,11 @@ export interface ConnectionConfig {
|
|||||||
sshPrivateKeyPath: string
|
sshPrivateKeyPath: string
|
||||||
sshPassphrase: string
|
sshPassphrase: string
|
||||||
cluster: boolean
|
cluster: boolean
|
||||||
|
sentinelEnabled: boolean
|
||||||
|
sentinelHost: string
|
||||||
|
sentinelPort: number
|
||||||
sentinelMasterName: string
|
sentinelMasterName: string
|
||||||
|
sentinelNodePassword: string
|
||||||
separator: string
|
separator: string
|
||||||
order: number
|
order: number
|
||||||
createdAt: number
|
createdAt: number
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ const form = reactive({
|
|||||||
tlsCertPath: '',
|
tlsCertPath: '',
|
||||||
tlsKeyPath: '',
|
tlsKeyPath: '',
|
||||||
tlsRejectUnauthorized: false,
|
tlsRejectUnauthorized: false,
|
||||||
|
sentinelEnabled: false,
|
||||||
|
sentinelHost: '',
|
||||||
|
sentinelPort: 26379,
|
||||||
|
sentinelMasterName: '',
|
||||||
|
sentinelNodePassword: '',
|
||||||
separator: ':',
|
separator: ':',
|
||||||
sshEnabled: false,
|
sshEnabled: false,
|
||||||
sshHost: '',
|
sshHost: '',
|
||||||
@@ -51,6 +56,12 @@ watch(() => props.visible, async (v) => {
|
|||||||
tlsCaPath: props.connection.tlsCaPath || '',
|
tlsCaPath: props.connection.tlsCaPath || '',
|
||||||
tlsCertPath: props.connection.tlsCertPath || '',
|
tlsCertPath: props.connection.tlsCertPath || '',
|
||||||
tlsKeyPath: props.connection.tlsKeyPath || '',
|
tlsKeyPath: props.connection.tlsKeyPath || '',
|
||||||
|
tlsRejectUnauthorized: props.connection.tlsRejectUnauthorized || false,
|
||||||
|
sentinelEnabled: props.connection.sentinelEnabled || false,
|
||||||
|
sentinelHost: props.connection.sentinelHost || '',
|
||||||
|
sentinelPort: props.connection.sentinelPort || 26379,
|
||||||
|
sentinelMasterName: props.connection.sentinelMasterName || '',
|
||||||
|
sentinelNodePassword: props.connection.sentinelNodePassword || '',
|
||||||
separator: props.connection.separator,
|
separator: props.connection.separator,
|
||||||
sshEnabled: props.connection.sshEnabled || false,
|
sshEnabled: props.connection.sshEnabled || false,
|
||||||
sshHost: props.connection.sshHost || '',
|
sshHost: props.connection.sshHost || '',
|
||||||
@@ -65,6 +76,9 @@ watch(() => props.visible, async (v) => {
|
|||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
name: '', host: '127.0.0.1', port: 6379,
|
name: '', host: '127.0.0.1', port: 6379,
|
||||||
auth: '', username: '', tls: false, tlsCaPath: '', tlsCertPath: '', tlsKeyPath: '',
|
auth: '', username: '', tls: false, tlsCaPath: '', tlsCertPath: '', tlsKeyPath: '',
|
||||||
|
tlsRejectUnauthorized: false,
|
||||||
|
sentinelEnabled: false, sentinelHost: '', sentinelPort: 26379,
|
||||||
|
sentinelMasterName: '', sentinelNodePassword: '',
|
||||||
separator: ':',
|
separator: ':',
|
||||||
sshEnabled: false, sshHost: '', sshPort: 22,
|
sshEnabled: false, sshHost: '', sshPort: 22,
|
||||||
sshUsername: '', sshPassword: '', sshPrivateKeyPath: '', sshPassphrase: '',
|
sshUsername: '', sshPassword: '', sshPrivateKeyPath: '', sshPassphrase: '',
|
||||||
@@ -128,6 +142,11 @@ async function handleTest() {
|
|||||||
tlsCertPath: form.tlsCertPath,
|
tlsCertPath: form.tlsCertPath,
|
||||||
tlsKeyPath: form.tlsKeyPath,
|
tlsKeyPath: form.tlsKeyPath,
|
||||||
tlsRejectUnauthorized: form.tlsRejectUnauthorized,
|
tlsRejectUnauthorized: form.tlsRejectUnauthorized,
|
||||||
|
sentinelEnabled: form.sentinelEnabled,
|
||||||
|
sentinelHost: form.sentinelHost.trim(),
|
||||||
|
sentinelPort: Number(form.sentinelPort) || 26379,
|
||||||
|
sentinelMasterName: form.sentinelMasterName.trim(),
|
||||||
|
sentinelNodePassword: form.sentinelNodePassword,
|
||||||
sshEnabled: form.sshEnabled,
|
sshEnabled: form.sshEnabled,
|
||||||
sshHost: form.sshHost.trim(),
|
sshHost: form.sshHost.trim(),
|
||||||
sshPort: Number(form.sshPort) || 22,
|
sshPort: Number(form.sshPort) || 22,
|
||||||
@@ -165,6 +184,11 @@ async function handleSave() {
|
|||||||
tlsCertPath: form.tlsCertPath,
|
tlsCertPath: form.tlsCertPath,
|
||||||
tlsKeyPath: form.tlsKeyPath,
|
tlsKeyPath: form.tlsKeyPath,
|
||||||
tlsRejectUnauthorized: form.tlsRejectUnauthorized,
|
tlsRejectUnauthorized: form.tlsRejectUnauthorized,
|
||||||
|
sentinelEnabled: form.sentinelEnabled,
|
||||||
|
sentinelHost: form.sentinelHost.trim(),
|
||||||
|
sentinelPort: Number(form.sentinelPort) || 26379,
|
||||||
|
sentinelMasterName: form.sentinelMasterName.trim(),
|
||||||
|
sentinelNodePassword: form.sentinelNodePassword,
|
||||||
sshEnabled: form.sshEnabled,
|
sshEnabled: form.sshEnabled,
|
||||||
sshHost: form.sshHost.trim(),
|
sshHost: form.sshHost.trim(),
|
||||||
sshPort: Number(form.sshPort) || 22,
|
sshPort: Number(form.sshPort) || 22,
|
||||||
@@ -249,6 +273,32 @@ async function handleSave() {
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Sentinel -->
|
||||||
|
<el-divider />
|
||||||
|
<el-form-item>
|
||||||
|
<el-checkbox v-model="form.sentinelEnabled">{{ t('connection.sentinel') }}</el-checkbox>
|
||||||
|
</el-form-item>
|
||||||
|
<template v-if="form.sentinelEnabled">
|
||||||
|
<el-row :gutter="12">
|
||||||
|
<el-col :span="16">
|
||||||
|
<el-form-item :label="t('connection.sentinelHost')">
|
||||||
|
<el-input v-model="form.sentinelHost" placeholder="192.168.1.1" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item :label="t('connection.sentinelPort')">
|
||||||
|
<el-input-number v-model="form.sentinelPort" :min="1" :max="65535" controls-position="right" style="width:100%" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item :label="t('connection.sentinelMasterName')">
|
||||||
|
<el-input v-model="form.sentinelMasterName" placeholder="mymaster" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="t('connection.sentinelNodePassword')">
|
||||||
|
<el-input v-model="form.sentinelNodePassword" type="password" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- SSH Tunnel -->
|
<!-- SSH Tunnel -->
|
||||||
<el-divider />
|
<el-divider />
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ export default {
|
|||||||
tlsCertPath: 'Client Certificate',
|
tlsCertPath: 'Client Certificate',
|
||||||
tlsKeyPath: 'Client Private Key',
|
tlsKeyPath: 'Client Private Key',
|
||||||
tlsRejectUnauthorized: 'Verify Server Certificate',
|
tlsRejectUnauthorized: 'Verify Server Certificate',
|
||||||
|
sentinel: 'Sentinel Mode',
|
||||||
|
sentinelHost: 'Sentinel Host',
|
||||||
|
sentinelPort: 'Sentinel Port',
|
||||||
|
sentinelMasterName: 'Master Name',
|
||||||
|
sentinelNodePassword: 'Sentinel Password',
|
||||||
ssh: 'SSH Tunnel',
|
ssh: 'SSH Tunnel',
|
||||||
sshHost: 'SSH Host',
|
sshHost: 'SSH Host',
|
||||||
sshPort: 'SSH Port',
|
sshPort: 'SSH Port',
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ export default {
|
|||||||
tlsCertPath: '客户端证书',
|
tlsCertPath: '客户端证书',
|
||||||
tlsKeyPath: '客户端私钥',
|
tlsKeyPath: '客户端私钥',
|
||||||
tlsRejectUnauthorized: '验证服务器证书',
|
tlsRejectUnauthorized: '验证服务器证书',
|
||||||
|
sentinel: 'Sentinel 哨兵模式',
|
||||||
|
sentinelHost: '哨兵主机',
|
||||||
|
sentinelPort: '哨兵端口',
|
||||||
|
sentinelMasterName: 'Master 名称',
|
||||||
|
sentinelNodePassword: '哨兵密码',
|
||||||
ssh: 'SSH 隧道',
|
ssh: 'SSH 隧道',
|
||||||
sshHost: 'SSH 主机',
|
sshHost: 'SSH 主机',
|
||||||
sshPort: 'SSH 端口',
|
sshPort: 'SSH 端口',
|
||||||
|
|||||||
@@ -22,7 +22,11 @@ export interface ConnectionConfig {
|
|||||||
sshPrivateKeyPath: string
|
sshPrivateKeyPath: string
|
||||||
sshPassphrase: string
|
sshPassphrase: string
|
||||||
cluster: boolean
|
cluster: boolean
|
||||||
|
sentinelEnabled: boolean
|
||||||
|
sentinelHost: string
|
||||||
|
sentinelPort: number
|
||||||
sentinelMasterName: string
|
sentinelMasterName: string
|
||||||
|
sentinelNodePassword: string
|
||||||
separator: string
|
separator: string
|
||||||
order: number
|
order: number
|
||||||
createdAt: number
|
createdAt: number
|
||||||
@@ -88,6 +92,11 @@ export const useConnectionStore = defineStore('connection', () => {
|
|||||||
tlsCertPath: conn.tlsCertPath,
|
tlsCertPath: conn.tlsCertPath,
|
||||||
tlsKeyPath: conn.tlsKeyPath,
|
tlsKeyPath: conn.tlsKeyPath,
|
||||||
tlsRejectUnauthorized: conn.tlsRejectUnauthorized,
|
tlsRejectUnauthorized: conn.tlsRejectUnauthorized,
|
||||||
|
sentinelEnabled: conn.sentinelEnabled,
|
||||||
|
sentinelHost: conn.sentinelHost,
|
||||||
|
sentinelPort: conn.sentinelPort,
|
||||||
|
sentinelMasterName: conn.sentinelMasterName,
|
||||||
|
sentinelNodePassword: conn.sentinelNodePassword,
|
||||||
sshEnabled: conn.sshEnabled,
|
sshEnabled: conn.sshEnabled,
|
||||||
sshHost: conn.sshHost,
|
sshHost: conn.sshHost,
|
||||||
sshPort: conn.sshPort,
|
sshPort: conn.sshPort,
|
||||||
@@ -156,7 +165,9 @@ export const useConnectionStore = defineStore('connection', () => {
|
|||||||
|
|
||||||
async function testConnection(opts: {
|
async function testConnection(opts: {
|
||||||
host: string; port: number; auth?: string; username?: string; tls?: boolean
|
host: string; port: number; auth?: string; username?: string; tls?: boolean
|
||||||
tlsCaPath?: string; tlsCertPath?: string; tlsKeyPath?: string
|
tlsCaPath?: string; tlsCertPath?: string; tlsKeyPath?: string; tlsRejectUnauthorized?: boolean
|
||||||
|
sentinelEnabled?: boolean; sentinelHost?: string; sentinelPort?: number
|
||||||
|
sentinelMasterName?: string; sentinelNodePassword?: string
|
||||||
sshEnabled?: boolean; sshHost?: string; sshPort?: number; sshUsername?: string
|
sshEnabled?: boolean; sshHost?: string; sshPort?: number; sshUsername?: string
|
||||||
sshPassword?: string; sshPrivateKeyPath?: string; sshPassphrase?: string
|
sshPassword?: string; sshPrivateKeyPath?: string; sshPassphrase?: string
|
||||||
}): Promise<{ success: boolean; error?: string }> {
|
}): Promise<{ success: boolean; error?: string }> {
|
||||||
@@ -175,6 +186,11 @@ export const useConnectionStore = defineStore('connection', () => {
|
|||||||
tlsCertPath: opts.tlsCertPath,
|
tlsCertPath: opts.tlsCertPath,
|
||||||
tlsKeyPath: opts.tlsKeyPath,
|
tlsKeyPath: opts.tlsKeyPath,
|
||||||
tlsRejectUnauthorized: (opts as any).tlsRejectUnauthorized,
|
tlsRejectUnauthorized: (opts as any).tlsRejectUnauthorized,
|
||||||
|
sentinelEnabled: opts.sentinelEnabled,
|
||||||
|
sentinelHost: opts.sentinelHost,
|
||||||
|
sentinelPort: opts.sentinelPort,
|
||||||
|
sentinelMasterName: opts.sentinelMasterName,
|
||||||
|
sentinelNodePassword: opts.sentinelNodePassword,
|
||||||
sshEnabled: opts.sshEnabled,
|
sshEnabled: opts.sshEnabled,
|
||||||
sshHost: opts.sshHost,
|
sshHost: opts.sshHost,
|
||||||
sshPort: opts.sshPort,
|
sshPort: opts.sshPort,
|
||||||
|
|||||||
Reference in New Issue
Block a user