refactor: restructure project layout
- Move main process to electron/main/ - Move preload to electron/preload/ - Add shared types in electron/shared/ - Split redis-service into modular redis/ directory - Flatten renderer to src/ (remove src/renderer/ nesting) - Update electron.vite.config.ts paths - Update tsconfig paths - Output to electron-dist/
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
// src/renderer/src/stores/app.ts
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export type ThemeMode = 'system' | 'dark' | 'light'
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
const theme = ref<ThemeMode>((localStorage.getItem('theme') as ThemeMode) || 'system')
|
||||
const isDark = ref(true)
|
||||
const fontSize = ref<number>(Number(localStorage.getItem('fontSize')) || 13)
|
||||
const scanCount = ref<number>(Number(localStorage.getItem('scanCount')) || 100)
|
||||
|
||||
function applyTheme(mode: ThemeMode, osDark?: boolean) {
|
||||
theme.value = mode
|
||||
const dark = mode === 'system' ? (osDark ?? true) : mode === 'dark'
|
||||
isDark.value = dark
|
||||
document.documentElement.classList.toggle('light', !dark)
|
||||
}
|
||||
|
||||
if (window.electronAPI) {
|
||||
window.electronAPI.theme.onOsUpdated((osDark: boolean) => {
|
||||
if (theme.value === 'system') {
|
||||
applyTheme('system', osDark)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
window.electronAPI?.theme.get().then((osDark: boolean) => {
|
||||
applyTheme(theme.value, osDark)
|
||||
})
|
||||
|
||||
function setTheme(mode: ThemeMode) {
|
||||
applyTheme(mode)
|
||||
localStorage.setItem('theme', mode)
|
||||
window.electronAPI?.theme.set(mode)
|
||||
}
|
||||
|
||||
function setFontSize(size: number) {
|
||||
fontSize.value = size
|
||||
localStorage.setItem('fontSize', String(size))
|
||||
}
|
||||
|
||||
function setScanCount(count: number) {
|
||||
scanCount.value = count
|
||||
localStorage.setItem('scanCount', String(count))
|
||||
}
|
||||
|
||||
return { theme, isDark, fontSize, scanCount, setTheme, setFontSize, setScanCount }
|
||||
})
|
||||
@@ -0,0 +1,182 @@
|
||||
// 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
|
||||
sshEnabled: boolean
|
||||
sshHost: string
|
||||
sshPort: number
|
||||
sshUsername: string
|
||||
sshPassword: string
|
||||
sshPrivateKeyPath: string
|
||||
cluster: boolean
|
||||
sentinelMasterName: 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 connecting = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const serverInfo = ref<string | null>(null)
|
||||
const healthOk = ref(true)
|
||||
let healthTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const activeConnection = computed(() =>
|
||||
connections.value.find((c: ConnectionConfig) => c.id === activeId.value) ?? null
|
||||
)
|
||||
const isConnected = computed(() => activeId.value !== null)
|
||||
|
||||
async function loadConnections() {
|
||||
connections.value = await window.electronAPI.storage.getConnections()
|
||||
}
|
||||
|
||||
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 (activeId.value === id) await disconnect()
|
||||
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) {
|
||||
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,
|
||||
})
|
||||
if (result.success) {
|
||||
activeId.value = conn.id
|
||||
serverInfo.value = 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
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
stopHealthCheck()
|
||||
if (activeId.value) {
|
||||
await window.electronAPI.redis.disconnect(activeId.value)
|
||||
activeId.value = null
|
||||
serverInfo.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function startHealthCheck(connId: string) {
|
||||
stopHealthCheck()
|
||||
healthOk.value = true
|
||||
healthTimer = setInterval(async () => {
|
||||
try {
|
||||
const ok = await window.electronAPI.redis.isConnected(connId)
|
||||
healthOk.value = ok
|
||||
if (!ok && activeId.value === connId) {
|
||||
// Connection lost
|
||||
error.value = 'Connection lost'
|
||||
}
|
||||
} catch {
|
||||
healthOk.value = false
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
function stopHealthCheck() {
|
||||
if (healthTimer) {
|
||||
clearInterval(healthTimer)
|
||||
healthTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
async function testConnection(opts: {
|
||||
host: string; port: number; auth?: string; username?: string; tls?: boolean
|
||||
}): 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,
|
||||
})
|
||||
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, connecting, error, serverInfo, healthOk,
|
||||
activeConnection, isConnected,
|
||||
loadConnections, saveConnection, deleteConnection, reorderConnections,
|
||||
connect, disconnect, testConnection, exportConnections, importConnections,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,186 @@
|
||||
// src/renderer/src/stores/key.ts
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export interface KeyItem {
|
||||
name: string
|
||||
type: string
|
||||
ttl: number
|
||||
level: number
|
||||
children?: KeyItem[]
|
||||
isLeaf: boolean
|
||||
}
|
||||
|
||||
interface ScanResult {
|
||||
cursor: string
|
||||
keys: string[]
|
||||
}
|
||||
|
||||
export const useKeyStore = defineStore('key', () => {
|
||||
const keys = ref<KeyItem[]>([])
|
||||
const treeKeys = ref<KeyItem[]>([])
|
||||
const selectedKey = ref<string | null>(null)
|
||||
const selectedType = ref<string | null>(null)
|
||||
const selectedTTL = ref<number>(-1)
|
||||
const keyData = ref<any>(null)
|
||||
const loading = ref(false)
|
||||
const scanCursor = ref('0')
|
||||
const hasMore = ref(false)
|
||||
const pattern = ref('*')
|
||||
const useTree = ref(false)
|
||||
const currentDb = ref(0)
|
||||
|
||||
function buildTree(flatKeys: string[], separator: string): KeyItem[] {
|
||||
const root: KeyItem[] = []
|
||||
const map = new Map<string, KeyItem>()
|
||||
|
||||
for (const key of flatKeys) {
|
||||
const parts = key.split(separator)
|
||||
let currentLevel = root
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i]
|
||||
const isLast = i === parts.length - 1
|
||||
const path = parts.slice(0, i + 1).join(separator)
|
||||
|
||||
if (map.has(path)) {
|
||||
const existing = map.get(path)!
|
||||
if (isLast) {
|
||||
existing.name = key
|
||||
existing.isLeaf = true
|
||||
existing.type = ''
|
||||
existing.ttl = -1
|
||||
}
|
||||
currentLevel = existing.children || []
|
||||
} else {
|
||||
const node: KeyItem = {
|
||||
name: isLast ? key : part,
|
||||
type: isLast ? '' : 'branch',
|
||||
ttl: -1,
|
||||
level: i,
|
||||
children: isLast ? undefined : [],
|
||||
isLeaf: isLast,
|
||||
}
|
||||
map.set(path, node)
|
||||
currentLevel.push(node)
|
||||
if (!isLast) currentLevel = node.children!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
async function scanKeys(connId: string, cursor: string = '0', reset: boolean = false) {
|
||||
if (reset) {
|
||||
scanCursor.value = '0'
|
||||
keys.value = []
|
||||
hasMore.value = false
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const result: ScanResult = await window.electronAPI.redis.scanKeys(
|
||||
connId, cursor, pattern.value, 100
|
||||
)
|
||||
const newKeys = result.keys.map(k => ({
|
||||
name: k,
|
||||
type: '',
|
||||
ttl: -1,
|
||||
level: 0,
|
||||
isLeaf: true,
|
||||
}))
|
||||
|
||||
if (reset) {
|
||||
keys.value = newKeys
|
||||
} else {
|
||||
keys.value.push(...newKeys)
|
||||
}
|
||||
|
||||
scanCursor.value = result.cursor
|
||||
hasMore.value = result.cursor !== '0'
|
||||
|
||||
if (useTree.value) {
|
||||
const conn = (await window.electronAPI.storage.getConnections())
|
||||
.find((c: any) => c.id === connId)
|
||||
const sep = conn?.separator || ':'
|
||||
treeKeys.value = buildTree(keys.value.map(k => k.name), sep)
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadKeyTypeAndTTL(connId: string, key: string) {
|
||||
const [type, ttl] = await Promise.all([
|
||||
window.electronAPI.redis.getKeyType(connId, key),
|
||||
window.electronAPI.redis.getKeyTTL(connId, key),
|
||||
])
|
||||
selectedType.value = type
|
||||
selectedTTL.value = ttl
|
||||
}
|
||||
|
||||
async function loadKeyData(connId: string, key: string) {
|
||||
loading.value = true
|
||||
try {
|
||||
await loadKeyTypeAndTTL(connId, key)
|
||||
selectedKey.value = key
|
||||
|
||||
switch (selectedType.value) {
|
||||
case 'string':
|
||||
keyData.value = await window.electronAPI.redis.getString(connId, key)
|
||||
break
|
||||
case 'hash': {
|
||||
const result = await window.electronAPI.redis.hashScan(connId, key, '0', 100)
|
||||
keyData.value = result.fields
|
||||
break
|
||||
}
|
||||
case 'list': {
|
||||
const len = await window.electronAPI.redis.listLength(connId, key)
|
||||
keyData.value = { length: len }
|
||||
break
|
||||
}
|
||||
case 'set': {
|
||||
const size = await window.electronAPI.redis.setSize(connId, key)
|
||||
keyData.value = { size }
|
||||
break
|
||||
}
|
||||
case 'zset': {
|
||||
const size = await window.electronAPI.redis.zsetSize(connId, key)
|
||||
keyData.value = { size }
|
||||
break
|
||||
}
|
||||
case 'stream': {
|
||||
try {
|
||||
const info = await window.electronAPI.redis.streamInfo(connId, key)
|
||||
keyData.value = { length: info.length }
|
||||
} catch {
|
||||
keyData.value = { length: 0 }
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
keyData.value = null
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function selectDb(connId: string, db: number) {
|
||||
await window.electronAPI.redis.selectDb(connId, db)
|
||||
currentDb.value = db
|
||||
keys.value = []
|
||||
treeKeys.value = []
|
||||
selectedKey.value = null
|
||||
keyData.value = null
|
||||
scanCursor.value = '0'
|
||||
hasMore.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
keys, treeKeys, selectedKey, selectedType, selectedTTL, keyData,
|
||||
loading, scanCursor, hasMore, pattern, useTree, currentDb,
|
||||
scanKeys, loadKeyTypeAndTTL, loadKeyData, selectDb,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user