Files
JRedisDesktop/src/stores/connection.ts
T
Jokul 5505434325 feat: P3 batch 1 - context menus, shortcuts, connection management
- Key list right-click context menu (copy/delete/export/memory/copy value)
- Shift+Click range selection + Select All checkbox
- Keyboard shortcuts: Ctrl+, (settings), Ctrl+S (save), Ctrl+L (clear CLI), Ctrl+/ (hotkeys help)
- CLI auto-sync: SELECT updates DB, write commands refresh key list
- Connection color marking (6 predefined colors)
- Duplicate connection button
- Connection drag-and-drop reorder (native HTML5)
- i18n keys for all features (zh-CN + en)
2026-07-07 22:34:48 +08:00

283 lines
9.0 KiB
TypeScript

// src/renderer/src/stores/connection.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export interface ConnectionConfig {
id: string
name: string
host: string
port: number
auth: string
username: string
tls: boolean
tlsCaPath: string
tlsCertPath: string
tlsKeyPath: string
tlsRejectUnauthorized: boolean
sshEnabled: boolean
sshHost: string
sshPort: number
sshUsername: string
sshPassword: string
sshPrivateKeyPath: string
sshPassphrase: string
cluster: boolean
sentinelEnabled: boolean
sentinelHost: string
sentinelPort: number
sentinelMasterName: string
sentinelNodePassword: string
separator: string
order: number
createdAt: number
color?: string
}
export const useConnectionStore = defineStore('connection', () => {
const connections = ref<ConnectionConfig[]>([])
const activeId = ref<string | null>(null)
const connectedIds = ref<string[]>([])
const connecting = ref(false)
const error = ref<string | null>(null)
const serverInfoMap = ref<Record<string, string>>({})
const healthMap = ref<Record<string, boolean>>({})
const currentDb = ref<number>(0)
const healthTimers = new Map<string, ReturnType<typeof setInterval>>()
const activeConnection = computed(() =>
connections.value.find((c: ConnectionConfig) => c.id === activeId.value) ?? null
)
const isConnected = computed(() => activeId.value !== null && connectedIds.value.includes(activeId.value))
const serverInfo = computed(() => activeId.value ? serverInfoMap.value[activeId.value] ?? null : null)
const healthOk = computed(() => activeId.value ? healthMap.value[activeId.value] ?? false : false)
function isConnectedById(id: string): boolean {
return connectedIds.value.includes(id)
}
async function loadConnections() {
connections.value = await window.electronAPI.storage.getConnections()
}
async function decryptPassword(auth: string): Promise<string> {
if (auth.startsWith('enc:')) {
return await window.electronAPI.credential.decrypt(auth.slice(4))
}
return auth
}
async function saveConnection(conn: ConnectionConfig) {
if (conn.auth && !conn.auth.startsWith('enc:')) {
conn.auth = 'enc:' + await window.electronAPI.credential.encrypt(conn.auth)
}
connections.value = await window.electronAPI.storage.saveConnection(conn)
}
async function deleteConnection(id: string) {
if (connectedIds.value.includes(id)) await disconnectById(id)
connections.value = await window.electronAPI.storage.deleteConnection(id)
}
async function reorderConnections(ids: string[]) {
connections.value = await window.electronAPI.storage.reorderConnections(ids)
}
async function connect(conn: ConnectionConfig) {
// If already connected, just switch
if (connectedIds.value.includes(conn.id)) {
activeId.value = conn.id
return
}
connecting.value = true
error.value = null
try {
let auth = conn.auth
if (auth.startsWith('enc:')) {
auth = await window.electronAPI.credential.decrypt(auth.slice(4))
}
const result = await window.electronAPI.redis.connect(conn.id, {
host: conn.host,
port: conn.port,
password: auth || undefined,
username: conn.username || undefined,
tls: conn.tls,
tlsCaPath: conn.tlsCaPath,
tlsCertPath: conn.tlsCertPath,
tlsKeyPath: conn.tlsKeyPath,
tlsRejectUnauthorized: conn.tlsRejectUnauthorized,
sentinelEnabled: conn.sentinelEnabled,
sentinelHost: conn.sentinelHost,
sentinelPort: conn.sentinelPort,
sentinelMasterName: conn.sentinelMasterName,
sentinelNodePassword: conn.sentinelNodePassword,
cluster: conn.cluster,
sshEnabled: conn.sshEnabled,
sshHost: conn.sshHost,
sshPort: conn.sshPort,
sshUsername: conn.sshUsername,
sshPassword: conn.sshPassword,
sshPrivateKeyPath: conn.sshPrivateKeyPath,
sshPassphrase: conn.sshPassphrase,
})
if (result.success) {
connectedIds.value.push(conn.id)
activeId.value = conn.id
serverInfoMap.value[conn.id] = await window.electronAPI.redis.getInfo(conn.id)
startHealthCheck(conn.id)
} else {
error.value = result.error || 'Connection failed'
}
} catch (err: any) {
error.value = err.message
} finally {
connecting.value = false
}
}
function switchTo(id: string) {
if (connectedIds.value.includes(id)) {
activeId.value = id
}
}
async function disconnectById(id: string) {
stopHealthCheck(id)
await window.electronAPI.redis.disconnect(id)
connectedIds.value = connectedIds.value.filter(i => i !== id)
delete serverInfoMap.value[id]
delete healthMap.value[id]
if (activeId.value === id) {
activeId.value = connectedIds.value[0] ?? null
}
}
async function disconnect() {
if (activeId.value) {
await disconnectById(activeId.value)
}
}
async function getInfo(): Promise<string> {
if (!activeId.value) return ''
try {
const info = await window.electronAPI.redis.getInfo(activeId.value)
serverInfoMap.value[activeId.value] = info
return info
} catch {
return ''
}
}
function startHealthCheck(connId: string) {
stopHealthCheck(connId)
healthMap.value[connId] = true
const timer = setInterval(async () => {
try {
const ok = await window.electronAPI.redis.isConnected(connId)
healthMap.value[connId] = ok
if (!ok && activeId.value === connId) {
error.value = 'Connection lost'
}
} catch {
healthMap.value[connId] = false
}
}, 5000)
healthTimers.set(connId, timer)
}
function stopHealthCheck(connId?: string) {
if (connId) {
const timer = healthTimers.get(connId)
if (timer) {
clearInterval(timer)
healthTimers.delete(connId)
}
} else {
for (const [, timer] of healthTimers) {
clearInterval(timer)
}
healthTimers.clear()
}
}
async function testConnection(opts: {
host: string; port: number; auth?: string; username?: string; tls?: boolean
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 }> {
if (!window.electronAPI) {
return { success: false, error: 'Electron API not ready' }
}
const testId = `test_${Date.now()}`
try {
const result = await window.electronAPI.redis.connect(testId, {
host: opts.host,
port: opts.port,
password: opts.auth || undefined,
username: opts.username || undefined,
tls: opts.tls,
tlsCaPath: opts.tlsCaPath,
tlsCertPath: opts.tlsCertPath,
tlsKeyPath: opts.tlsKeyPath,
tlsRejectUnauthorized: (opts as any).tlsRejectUnauthorized,
sentinelEnabled: opts.sentinelEnabled,
sentinelHost: opts.sentinelHost,
sentinelPort: opts.sentinelPort,
sentinelMasterName: opts.sentinelMasterName,
sentinelNodePassword: opts.sentinelNodePassword,
cluster: opts.cluster,
sshEnabled: opts.sshEnabled,
sshHost: opts.sshHost,
sshPort: opts.sshPort,
sshUsername: opts.sshUsername,
sshPassword: opts.sshPassword,
sshPrivateKeyPath: opts.sshPrivateKeyPath,
sshPassphrase: opts.sshPassphrase,
})
if (result.success) {
await window.electronAPI.redis.disconnect(testId)
return { success: true }
}
return { success: false, error: result.error }
} catch (err: any) {
return { success: false, error: err.message }
}
}
async function exportConnections(): Promise<string> {
const data = JSON.stringify(connections.value, null, 2)
return data
}
async function importConnections(jsonStr: string): Promise<number> {
try {
const imported = JSON.parse(jsonStr) as ConnectionConfig[]
if (!Array.isArray(imported)) throw new Error('Invalid format')
let count = 0
for (const conn of imported) {
if (conn.id && conn.name && conn.host) {
const exists = connections.value.find((c: ConnectionConfig) => c.id === conn.id)
if (!exists) {
await saveConnection(conn)
count++
}
}
}
return count
} catch (err: any) {
throw new Error('Import failed: ' + err.message)
}
}
return {
connections, activeId, connectedIds, connecting, error, serverInfo, healthOk, currentDb,
activeConnection, isConnected,
loadConnections, decryptPassword, getInfo, saveConnection, deleteConnection, reorderConnections,
connect, disconnect, disconnectById, switchTo, isConnectedById, testConnection, exportConnections, importConnections,
}
})