5505434325
- 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)
283 lines
9.0 KiB
TypeScript
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,
|
|
}
|
|
})
|