// 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([]) const activeId = ref(null) const connectedIds = ref([]) const connecting = ref(false) const error = ref(null) const serverInfoMap = ref>({}) const healthMap = ref>({}) const currentDb = ref(0) const healthTimers = new Map>() 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 { 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 { 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 { const data = JSON.stringify(connections.value, null, 2) return data } async function importConnections(jsonStr: string): Promise { 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, } })