feat(phase2): connection management
- RedisService in main process (ioredis wrapper, execute, scan, hash ops) - electron-store for connection/settings persistence - safeStorage credential encrypt/decrypt - IPC handlers for redis, storage, credential channels - Preload API typed with all new channels - useConnectionStore (Pinia) with connect/disconnect/save/delete - ConnectionCard with 3D hover effect + status indicator - ConnectionList with search filter - NewConnectionDialog (create/edit connection form) - Sidebar component - StatusBar with live connection status + pulse animation - App.vue updated with Sidebar layout
This commit is contained in:
@@ -1,19 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
// src/renderer/src/App.vue
|
||||
import { onMounted } from 'vue'
|
||||
import TitleBar from './components/TitleBar.vue'
|
||||
import Sidebar from './components/Sidebar.vue'
|
||||
import StatusBar from './components/StatusBar.vue'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
|
||||
const connStore = useConnectionStore()
|
||||
|
||||
onMounted(() => {
|
||||
connStore.loadConnections()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-shell">
|
||||
<TitleBar />
|
||||
<main class="app-main">
|
||||
<div class="placeholder-content">
|
||||
<div class="placeholder-icon">R</div>
|
||||
<h2>JRedisDesktop</h2>
|
||||
<p>A modern Redis desktop manager</p>
|
||||
</div>
|
||||
</main>
|
||||
<div class="app-body">
|
||||
<Sidebar />
|
||||
<main class="app-main">
|
||||
<div v-if="!connStore.isConnected" class="placeholder-content">
|
||||
<div class="placeholder-icon">R</div>
|
||||
<h2>JRedisDesktop</h2>
|
||||
<p>A modern Redis desktop manager</p>
|
||||
<p class="placeholder-hint">Select or create a connection to get started</p>
|
||||
</div>
|
||||
<div v-else class="connected-content">
|
||||
<div class="info-card">
|
||||
<h3>{{ connStore.activeConnection?.name }}</h3>
|
||||
<pre class="info-text">{{ connStore.serverInfo }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<StatusBar />
|
||||
</div>
|
||||
</template>
|
||||
@@ -26,13 +45,19 @@ import StatusBar from './components/StatusBar.vue'
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-primary);
|
||||
overflow: hidden;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.placeholder-content {
|
||||
@@ -65,4 +90,40 @@ import StatusBar from './components/StatusBar.vue'
|
||||
.placeholder-content p {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.placeholder-hint {
|
||||
margin-top: 8px;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.connected-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 20px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.info-card h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-light);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
white-space: pre-wrap;
|
||||
font-family: 'Cascadia Code', 'Fira Code', monospace;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<script setup lang="ts">
|
||||
// src/renderer/src/components/ConnectionCard.vue
|
||||
import { computed } from 'vue'
|
||||
import type { ConnectionConfig } from '@renderer/stores/connection'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
|
||||
const props = defineProps<{ connection: ConnectionConfig }>()
|
||||
const emit = defineEmits<{ edit: [conn: ConnectionConfig]; delete: [id: string] }>()
|
||||
const connStore = useConnectionStore()
|
||||
|
||||
const isActive = computed(() => connStore.activeId === props.connection.id)
|
||||
const statusColor = computed(() => {
|
||||
if (isActive.value) return 'var(--success)'
|
||||
return 'var(--text-muted)'
|
||||
})
|
||||
|
||||
function handleClick() {
|
||||
if (isActive.value) {
|
||||
connStore.disconnect()
|
||||
} else {
|
||||
connStore.connect(props.connection)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="connection-card"
|
||||
:class="{ active: isActive, connecting: connStore.connecting && connStore.activeId === connection.id }"
|
||||
@click="handleClick"
|
||||
:style="{ '--status-color': statusColor }"
|
||||
>
|
||||
<div class="card-status" />
|
||||
<div class="card-body">
|
||||
<div class="card-name">{{ connection.name }}</div>
|
||||
<div class="card-host">{{ connection.host }}:{{ connection.port }}</div>
|
||||
</div>
|
||||
<div class="card-actions" @click.stop>
|
||||
<button class="card-btn" title="Edit" @click="emit('edit', connection)">✎</button>
|
||||
<button class="card-btn card-btn-danger" title="Delete" @click="emit('delete', connection.id)">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.connection-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.connection-card:hover {
|
||||
background: var(--bg-card-hover);
|
||||
transform: perspective(600px) rotateY(-1deg);
|
||||
}
|
||||
|
||||
.connection-card.active {
|
||||
background: var(--bg-card-active);
|
||||
border-color: var(--border-accent);
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
|
||||
.connection-card.connecting {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card-status {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--status-color);
|
||||
box-shadow: 0 0 8px var(--status-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.card-host {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.connection-card:hover .card-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-btn:hover {
|
||||
background: var(--bg-card-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-btn-danger:hover {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: var(--danger);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,133 @@
|
||||
<script setup lang="ts">
|
||||
// src/renderer/src/components/ConnectionList.vue
|
||||
import { ref, computed } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import ConnectionCard from './ConnectionCard.vue'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import type { ConnectionConfig } from '@renderer/stores/connection'
|
||||
|
||||
const connStore = useConnectionStore()
|
||||
const searchQuery = ref('')
|
||||
const emit = defineEmits<{ edit: [conn: ConnectionConfig | null] }>()
|
||||
|
||||
const filteredConnections = computed(() => {
|
||||
const q = searchQuery.value.toLowerCase()
|
||||
return connStore.connections
|
||||
.filter(c => c.name.toLowerCase().includes(q) || c.host.includes(q))
|
||||
.sort((a, b) => a.order - b.order)
|
||||
})
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
try {
|
||||
await ElMessageBox.confirm('Delete this connection?', 'Confirm', {
|
||||
type: 'warning',
|
||||
confirmButtonText: 'Delete',
|
||||
cancelButtonText: 'Cancel',
|
||||
})
|
||||
await connStore.deleteConnection(id)
|
||||
} catch { /* cancelled */ }
|
||||
}
|
||||
|
||||
function handleEdit(conn: ConnectionConfig) {
|
||||
emit('edit', conn)
|
||||
}
|
||||
|
||||
function handleNew() {
|
||||
emit('edit', null)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="connection-list">
|
||||
<div class="list-search">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
class="search-input"
|
||||
type="text"
|
||||
placeholder="Search connections..."
|
||||
/>
|
||||
</div>
|
||||
<div class="list-items">
|
||||
<ConnectionCard
|
||||
v-for="conn in filteredConnections"
|
||||
:key="conn.id"
|
||||
:connection="conn"
|
||||
@edit="handleEdit"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
<div v-if="filteredConnections.length === 0" class="list-empty">
|
||||
<p v-if="searchQuery">No connections match</p>
|
||||
<p v-else>No connections yet</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="list-add-btn" @click="handleNew">
|
||||
+ New Connection
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.connection-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.list-search {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: var(--border-accent);
|
||||
}
|
||||
|
||||
.list-items {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.list-empty {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.list-add-btn {
|
||||
margin: 14px;
|
||||
padding: 10px;
|
||||
background: var(--bg-card);
|
||||
border: 1px dashed var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.list-add-btn:hover {
|
||||
border-color: var(--border-accent);
|
||||
color: var(--accent-light);
|
||||
background: var(--bg-card-hover);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
// src/renderer/src/components/NewConnectionDialog.vue
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import type { ConnectionConfig } from '@renderer/stores/connection'
|
||||
|
||||
const props = defineProps<{ visible: boolean; connection: ConnectionConfig | null }>()
|
||||
const emit = defineEmits<{ 'update:visible': [v: boolean] }>()
|
||||
const connStore = useConnectionStore()
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
host: '127.0.0.1',
|
||||
port: 6379,
|
||||
auth: '',
|
||||
username: '',
|
||||
tls: false,
|
||||
separator: ':',
|
||||
})
|
||||
|
||||
const saving = ref(false)
|
||||
const isEdit = ref(false)
|
||||
|
||||
watch(() => props.visible, (v) => {
|
||||
if (v) {
|
||||
if (props.connection) {
|
||||
isEdit.value = true
|
||||
Object.assign(form, {
|
||||
name: props.connection.name,
|
||||
host: props.connection.host,
|
||||
port: props.connection.port,
|
||||
auth: '',
|
||||
username: props.connection.username,
|
||||
tls: props.connection.tls,
|
||||
separator: props.connection.separator,
|
||||
})
|
||||
} else {
|
||||
isEdit.value = false
|
||||
Object.assign(form, {
|
||||
name: '', host: '127.0.0.1', port: 6379,
|
||||
auth: '', username: '', tls: false, separator: ':',
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function close() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.name.trim() || !form.host.trim()) {
|
||||
ElMessage.warning('Name and host are required')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const conn: ConnectionConfig = {
|
||||
id: props.connection?.id ?? `conn_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
name: form.name.trim(),
|
||||
host: form.host.trim(),
|
||||
port: Number(form.port) || 6379,
|
||||
auth: form.auth,
|
||||
username: form.username.trim(),
|
||||
tls: form.tls,
|
||||
tlsCaPath: '',
|
||||
tlsCertPath: '',
|
||||
tlsKeyPath: '',
|
||||
sshEnabled: false,
|
||||
sshHost: '',
|
||||
sshPort: 22,
|
||||
sshUsername: '',
|
||||
sshPassword: '',
|
||||
sshPrivateKeyPath: '',
|
||||
cluster: false,
|
||||
sentinelMasterName: '',
|
||||
separator: form.separator || ':',
|
||||
order: props.connection?.order ?? connStore.connections.length,
|
||||
createdAt: props.connection?.createdAt ?? Date.now(),
|
||||
}
|
||||
await connStore.saveConnection(conn)
|
||||
ElMessage.success(isEdit.value ? 'Connection updated' : 'Connection created')
|
||||
close()
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || 'Failed to save connection')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
:title="isEdit ? 'Edit Connection' : 'New Connection'"
|
||||
width="480px"
|
||||
:close-on-click-modal="false"
|
||||
@update:model-value="close"
|
||||
>
|
||||
<el-form label-position="top" size="small">
|
||||
<el-form-item label="Name" required>
|
||||
<el-input v-model="form.name" placeholder="My Redis" />
|
||||
</el-form-item>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="16">
|
||||
<el-form-item label="Host" required>
|
||||
<el-input v-model="form.host" placeholder="127.0.0.1" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="Port">
|
||||
<el-input-number v-model="form.port" :min="1" :max="65535" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="Password">
|
||||
<el-input v-model="form.auth" type="password" show-password placeholder="Redis password" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Username (Redis 6+ ACL)">
|
||||
<el-input v-model="form.username" placeholder="default" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Key Separator">
|
||||
<el-input v-model="form.separator" placeholder=":" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="form.tls">Enable TLS/SSL</el-checkbox>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="close">Cancel</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleSave">
|
||||
{{ isEdit ? 'Save' : 'Create' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
// src/renderer/src/components/Sidebar.vue
|
||||
import { ref } from 'vue'
|
||||
import ConnectionList from './ConnectionList.vue'
|
||||
import NewConnectionDialog from './NewConnectionDialog.vue'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import type { ConnectionConfig } from '@renderer/stores/connection'
|
||||
|
||||
const connStore = useConnectionStore()
|
||||
const dialogVisible = ref(false)
|
||||
const editingConnection = ref<ConnectionConfig | null>(null)
|
||||
|
||||
function handleEdit(conn: ConnectionConfig | null) {
|
||||
editingConnection.value = conn
|
||||
dialogVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="sidebar">
|
||||
<ConnectionList @edit="handleEdit" />
|
||||
<NewConnectionDialog
|
||||
v-model:visible="dialogVisible"
|
||||
:connection="editingConnection"
|
||||
/>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
// src/renderer/src/components/StatusBar.vue
|
||||
import { computed } from 'vue'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
|
||||
const connStore = useConnectionStore()
|
||||
|
||||
const statusText = computed(() => {
|
||||
if (connStore.connecting) return 'Connecting...'
|
||||
if (connStore.error) return `Error: ${connStore.error}`
|
||||
if (connStore.activeConnection) {
|
||||
return `${connStore.activeConnection.host}:${connStore.activeConnection.port}`
|
||||
}
|
||||
return 'No connection'
|
||||
})
|
||||
|
||||
const statusClass = computed(() => {
|
||||
if (connStore.connecting) return 'status-connecting'
|
||||
if (connStore.error) return 'status-error'
|
||||
if (connStore.isConnected) return 'status-connected'
|
||||
return 'status-disconnected'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="statusbar">
|
||||
<span class="statusbar-left">No connection</span>
|
||||
<span class="statusbar-left">
|
||||
<span class="status-dot" :class="statusClass" />
|
||||
{{ statusText }}
|
||||
</span>
|
||||
<span class="statusbar-right">JRedisDesktop v2.0</span>
|
||||
</footer>
|
||||
</template>
|
||||
@@ -23,4 +46,38 @@
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.statusbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
.status-dot.status-connected {
|
||||
background: var(--success);
|
||||
box-shadow: 0 0 6px var(--success);
|
||||
}
|
||||
|
||||
.status-dot.status-connecting {
|
||||
background: var(--warning);
|
||||
box-shadow: 0 0 6px var(--warning);
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
.status-dot.status-error {
|
||||
background: var(--danger);
|
||||
box-shadow: 0 0 6px var(--danger);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// 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 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)
|
||||
} else {
|
||||
error.value = result.error || 'Connection failed'
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message
|
||||
} finally {
|
||||
connecting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
if (activeId.value) {
|
||||
await window.electronAPI.redis.disconnect(activeId.value)
|
||||
activeId.value = null
|
||||
serverInfo.value = null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
connections, activeId, connecting, error, serverInfo,
|
||||
activeConnection, isConnected,
|
||||
loadConnections, saveConnection, deleteConnection, reorderConnections,
|
||||
connect, disconnect,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user