feat(phase6): test connection, keyboard shortcuts, import/export
- NewConnectionDialog: test connection button before save - useKeyboard composable: Ctrl+N (new), Ctrl+R (refresh keys) - ConnectionList: import/export JSON buttons - ConnectionStore: testConnection, exportConnections, importConnections - i18n support in Sidebar (keyboard shortcuts)
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// src/renderer/src/components/ConnectionList.vue
|
// src/renderer/src/components/ConnectionList.vue
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { ElMessageBox } from 'element-plus'
|
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||||
import ConnectionCard from './ConnectionCard.vue'
|
import ConnectionCard from './ConnectionCard.vue'
|
||||||
import { useConnectionStore } from '@renderer/stores/connection'
|
import { useConnectionStore } from '@renderer/stores/connection'
|
||||||
import type { ConnectionConfig } from '@renderer/stores/connection'
|
import type { ConnectionConfig } from '@renderer/stores/connection'
|
||||||
@@ -35,6 +35,38 @@ function handleEdit(conn: ConnectionConfig) {
|
|||||||
function handleNew() {
|
function handleNew() {
|
||||||
emit('edit', null)
|
emit('edit', null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleExport() {
|
||||||
|
try {
|
||||||
|
const data = await connStore.exportConnections()
|
||||||
|
const blob = new Blob([data], { type: 'application/json' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `jredis-connections-${new Date().toISOString().slice(0, 10)}.json`
|
||||||
|
a.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
ElMessage.success('Connections exported')
|
||||||
|
} catch (err: any) {
|
||||||
|
ElMessage.error(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleImport() {
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI.dialog.openFile({
|
||||||
|
title: 'Import Connections',
|
||||||
|
filters: [{ name: 'JSON', extensions: ['json'] }],
|
||||||
|
})
|
||||||
|
if (result) {
|
||||||
|
const count = await connStore.importConnections(result.content)
|
||||||
|
ElMessage.success(`Imported ${count} connection(s)`)
|
||||||
|
await connStore.loadConnections()
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
ElMessage.error(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -60,9 +92,19 @@ function handleNew() {
|
|||||||
<p v-else>No connections yet</p>
|
<p v-else>No connections yet</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="list-add-btn" @click="handleNew">
|
<div class="list-footer">
|
||||||
+ New Connection
|
<button class="list-add-btn" @click="handleNew">
|
||||||
</button>
|
+ New Connection
|
||||||
|
</button>
|
||||||
|
<div class="list-actions">
|
||||||
|
<el-button size="small" text @click="handleImport">
|
||||||
|
<el-icon><Upload /></el-icon> Import
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" text @click="handleExport">
|
||||||
|
<el-icon><Download /></el-icon> Export
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -113,8 +155,13 @@ function handleNew() {
|
|||||||
padding: 40px 0;
|
padding: 40px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-footer {
|
||||||
|
padding: 14px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
.list-add-btn {
|
.list-add-btn {
|
||||||
margin: 14px;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border: 1px dashed var(--border-color);
|
border: 1px dashed var(--border-color);
|
||||||
@@ -123,6 +170,7 @@ function handleNew() {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-add-btn:hover {
|
.list-add-btn:hover {
|
||||||
@@ -130,4 +178,10 @@ function handleNew() {
|
|||||||
color: var(--accent-light);
|
color: var(--accent-light);
|
||||||
background: var(--bg-card-hover);
|
background: var(--bg-card-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const form = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
|
const testing = ref(false)
|
||||||
const isEdit = ref(false)
|
const isEdit = ref(false)
|
||||||
|
|
||||||
watch(() => props.visible, (v) => {
|
watch(() => props.visible, (v) => {
|
||||||
@@ -49,6 +50,30 @@ function close() {
|
|||||||
emit('update:visible', false)
|
emit('update:visible', false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleTest() {
|
||||||
|
if (!form.host.trim()) {
|
||||||
|
ElMessage.warning('Host is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
testing.value = true
|
||||||
|
try {
|
||||||
|
const result = await connStore.testConnection({
|
||||||
|
host: form.host.trim(),
|
||||||
|
port: Number(form.port) || 6379,
|
||||||
|
auth: form.auth || undefined,
|
||||||
|
username: form.username || undefined,
|
||||||
|
tls: form.tls,
|
||||||
|
})
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('Connection successful')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.error || 'Connection failed')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
testing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
if (!form.name.trim() || !form.host.trim()) {
|
if (!form.name.trim() || !form.host.trim()) {
|
||||||
ElMessage.warning('Name and host are required')
|
ElMessage.warning('Name and host are required')
|
||||||
@@ -128,10 +153,28 @@ async function handleSave() {
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="close">Cancel</el-button>
|
<div class="dialog-footer">
|
||||||
<el-button type="primary" :loading="saving" @click="handleSave">
|
<el-button @click="handleTest" :loading="testing">Test Connection</el-button>
|
||||||
{{ isEdit ? 'Save' : 'Create' }}
|
<div class="footer-right">
|
||||||
</el-button>
|
<el-button @click="close">Cancel</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="handleSave">
|
||||||
|
{{ isEdit ? 'Save' : 'Create' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import { ref } from 'vue'
|
|||||||
import ConnectionList from './ConnectionList.vue'
|
import ConnectionList from './ConnectionList.vue'
|
||||||
import NewConnectionDialog from './NewConnectionDialog.vue'
|
import NewConnectionDialog from './NewConnectionDialog.vue'
|
||||||
import { useConnectionStore } from '@renderer/stores/connection'
|
import { useConnectionStore } from '@renderer/stores/connection'
|
||||||
|
import { useKeyStore } from '@renderer/stores/key'
|
||||||
import type { ConnectionConfig } from '@renderer/stores/connection'
|
import type { ConnectionConfig } from '@renderer/stores/connection'
|
||||||
|
import { useKeyboard } from '@renderer/composables/useKeyboard'
|
||||||
|
|
||||||
const connStore = useConnectionStore()
|
const connStore = useConnectionStore()
|
||||||
|
const keyStore = useKeyStore()
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const editingConnection = ref<ConnectionConfig | null>(null)
|
const editingConnection = ref<ConnectionConfig | null>(null)
|
||||||
|
|
||||||
@@ -14,6 +17,20 @@ function handleEdit(conn: ConnectionConfig | null) {
|
|||||||
editingConnection.value = conn
|
editingConnection.value = conn
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleNew() {
|
||||||
|
editingConnection.value = null
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
useKeyboard({
|
||||||
|
'ctrl+n': handleNew,
|
||||||
|
'ctrl+r': () => {
|
||||||
|
if (connStore.activeId) {
|
||||||
|
keyStore.scanKeys(connStore.activeId, '0', true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// src/renderer/src/composables/useKeyboard.ts
|
||||||
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
interface ShortcutMap {
|
||||||
|
[key: string]: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useKeyboard(shortcuts: ShortcutMap) {
|
||||||
|
function handler(e: KeyboardEvent) {
|
||||||
|
const key: string[] = []
|
||||||
|
if (e.ctrlKey || e.metaKey) key.push('ctrl')
|
||||||
|
if (e.shiftKey) key.push('shift')
|
||||||
|
if (e.altKey) key.push('alt')
|
||||||
|
key.push(e.key.toLowerCase())
|
||||||
|
const combo = key.join('+')
|
||||||
|
|
||||||
|
if (shortcuts[combo]) {
|
||||||
|
e.preventDefault()
|
||||||
|
shortcuts[combo]()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => window.addEventListener('keydown', handler))
|
||||||
|
onUnmounted(() => window.removeEventListener('keydown', handler))
|
||||||
|
}
|
||||||
@@ -95,10 +95,51 @@ export const useConnectionStore = defineStore('connection', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function testConnection(opts: {
|
||||||
|
host: string; port: number; auth?: string; username?: string; tls?: boolean
|
||||||
|
}): Promise<{ success: boolean; error?: string }> {
|
||||||
|
const testId = `test_${Date.now()}`
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI.redis.connect(testId, opts)
|
||||||
|
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 {
|
return {
|
||||||
connections, activeId, connecting, error, serverInfo,
|
connections, activeId, connecting, error, serverInfo,
|
||||||
activeConnection, isConnected,
|
activeConnection, isConnected,
|
||||||
loadConnections, saveConnection, deleteConnection, reorderConnections,
|
loadConnections, saveConnection, deleteConnection, reorderConnections,
|
||||||
connect, disconnect,
|
connect, disconnect, testConnection, exportConnections, importConnections,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user