fix(connection): improve save-and-connect flow and feedback (#23)
* fix(connection): avoid duplicate saves on failed connect * fix(connection): show save-and-connect progress
This commit is contained in:
parent
da154c0dc2
commit
de3e934d8b
20
src/App.vue
20
src/App.vue
|
|
@ -75,6 +75,18 @@ watch(showConnectionDialog, (v) => {
|
|||
if (!v) connectionStore.stopEditing();
|
||||
});
|
||||
|
||||
function onConnectionConnectStarted(name: string) {
|
||||
toast(t("connection.connecting", { name }), 30000);
|
||||
}
|
||||
|
||||
function onConnectionConnectSucceeded(name: string) {
|
||||
toast(t("connection.connectSuccess", { name }), 2000);
|
||||
}
|
||||
|
||||
function onConnectionConnectFailed(message: string) {
|
||||
toast(t("connection.connectFailed", { message }), 5000);
|
||||
}
|
||||
|
||||
const activeTab = computed(() =>
|
||||
queryStore.tabs.find((t) => t.id === queryStore.activeTabId)
|
||||
);
|
||||
|
|
@ -953,7 +965,13 @@ async function setupFileDrop() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<ConnectionDialog v-model:open="showConnectionDialog" :edit-config="editConfig" />
|
||||
<ConnectionDialog
|
||||
v-model:open="showConnectionDialog"
|
||||
:edit-config="editConfig"
|
||||
@connect-started="onConnectionConnectStarted"
|
||||
@connect-succeeded="onConnectionConnectSucceeded"
|
||||
@connect-failed="onConnectionConnectFailed"
|
||||
/>
|
||||
<DangerConfirmDialog v-model:open="showDangerDialog" :sql="dangerSql" @confirm="onDangerConfirm" />
|
||||
<Dialog v-model:open="showUpdateDialog">
|
||||
<DialogContent class="sm:max-w-[520px]">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { nextTick, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle,
|
||||
|
|
@ -23,8 +23,15 @@ const props = defineProps<{
|
|||
editConfig?: ConnectionConfig;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
connectStarted: [name: string];
|
||||
connectSucceeded: [name: string];
|
||||
connectFailed: [message: string];
|
||||
}>();
|
||||
|
||||
const store = useConnectionStore();
|
||||
const isTesting = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const testResult = ref<{ ok: boolean; message: string } | null>(null);
|
||||
const editingId = ref<string | null>(null);
|
||||
|
||||
|
|
@ -228,6 +235,9 @@ watch(open, (value) => {
|
|||
});
|
||||
|
||||
async function save() {
|
||||
if (isSaving.value) return;
|
||||
isSaving.value = true;
|
||||
testResult.value = null;
|
||||
try {
|
||||
if (editingId.value) {
|
||||
const updated: ConnectionConfig = { ...form.value, id: editingId.value };
|
||||
|
|
@ -236,11 +246,23 @@ async function save() {
|
|||
} else {
|
||||
const config: ConnectionConfig = { ...form.value, id: crypto.randomUUID() };
|
||||
store.addConnection(config);
|
||||
await store.connect(config);
|
||||
open.value = false;
|
||||
await nextTick();
|
||||
emit("connectStarted", config.name);
|
||||
void store.connect(config)
|
||||
.then(() => {
|
||||
emit("connectSucceeded", config.name);
|
||||
})
|
||||
.catch((e: any) => {
|
||||
emit("connectFailed", String(e?.message || e));
|
||||
});
|
||||
return;
|
||||
}
|
||||
open.value = false;
|
||||
} catch (e) {
|
||||
console.error("[save] error:", e);
|
||||
} catch (e: any) {
|
||||
testResult.value = { ok: false, message: String(e?.message || e) };
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -419,11 +441,11 @@ watch([() => editingId.value, () => open.value], () => {
|
|||
<span v-if="testResult" :class="testResult.ok ? 'text-green-500' : 'text-red-500'" class="text-sm mr-auto">
|
||||
{{ testResult.ok ? t('connection.testSuccess') : testResult.message }}
|
||||
</span>
|
||||
<Button variant="outline" :disabled="isTesting" @click="testConnection">
|
||||
<Button variant="outline" :disabled="isTesting || isSaving" @click="testConnection">
|
||||
{{ isTesting ? t('connection.testing') : t('connection.test') }}
|
||||
</Button>
|
||||
<Button @click="save" :disabled="!form.name || !form.host">
|
||||
{{ editingId ? t('connection.save') : t('connection.saveAndConnect') }}
|
||||
<Button @click="save" :disabled="isSaving || !form.name || !form.host">
|
||||
{{ isSaving ? t('common.loading') : (editingId ? t('connection.save') : t('connection.saveAndConnect')) }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ export default {
|
|||
save: "Save",
|
||||
editTitle: "Edit Connection",
|
||||
testSuccess: "Connection successful",
|
||||
connecting: "Connecting to {name}...",
|
||||
connectSuccess: "Connected to {name}",
|
||||
connectFailed: "Connection failed: {message}",
|
||||
sshTunnel: "SSH Tunnel",
|
||||
sshEnable: "Connect via SSH tunnel",
|
||||
sshHost: "SSH Host",
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ export default {
|
|||
save: "保存",
|
||||
editTitle: "编辑连接",
|
||||
testSuccess: "连接成功",
|
||||
connecting: "正在连接 {name}...",
|
||||
connectSuccess: "已连接 {name}",
|
||||
connectFailed: "连接失败:{message}",
|
||||
sshTunnel: "SSH 隧道",
|
||||
sshEnable: "通过 SSH 隧道连接",
|
||||
sshHost: "SSH 主机",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,23 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
};
|
||||
}
|
||||
|
||||
function upsertConnectionNode(config: ConnectionConfig) {
|
||||
const node: TreeNode = {
|
||||
id: config.id,
|
||||
label: config.name,
|
||||
type: "connection",
|
||||
connectionId: config.id,
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
};
|
||||
const existing = treeNodes.value.findIndex((n) => n.id === config.id);
|
||||
if (existing >= 0) {
|
||||
treeNodes.value[existing] = { ...treeNodes.value[existing], ...node };
|
||||
} else {
|
||||
treeNodes.value.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
function loadPinnedTreeNodeIds(): Set<string> {
|
||||
try {
|
||||
if (typeof localStorage === "undefined") return new Set();
|
||||
|
|
@ -105,7 +122,14 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
}
|
||||
|
||||
function addConnection(config: ConnectionConfig) {
|
||||
connections.value.push(normalizeConnection(config));
|
||||
const normalized = normalizeConnection(config);
|
||||
const existing = connections.value.findIndex((c) => c.id === normalized.id);
|
||||
if (existing >= 0) {
|
||||
connections.value[existing] = normalized;
|
||||
} else {
|
||||
connections.value.push(normalized);
|
||||
}
|
||||
upsertConnectionNode(normalized);
|
||||
persistConnections();
|
||||
}
|
||||
|
||||
|
|
@ -134,25 +158,32 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
|
||||
async function connect(config: ConnectionConfig) {
|
||||
config = normalizeConnection(config);
|
||||
const id = await api.connectDb(config);
|
||||
activeConnectionId.value = id;
|
||||
connectedIds.value.add(id);
|
||||
const pendingNode = findNode(treeNodes.value, config.id);
|
||||
if (pendingNode) pendingNode.isLoading = true;
|
||||
try {
|
||||
const id = await api.connectDb(config);
|
||||
activeConnectionId.value = id;
|
||||
connectedIds.value.add(id);
|
||||
|
||||
const node: TreeNode = {
|
||||
id,
|
||||
label: config.name,
|
||||
type: "connection",
|
||||
connectionId: id,
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
};
|
||||
const existing = treeNodes.value.findIndex((n) => n.id === id);
|
||||
if (existing >= 0) {
|
||||
treeNodes.value[existing] = node;
|
||||
} else {
|
||||
treeNodes.value.push(node);
|
||||
const node: TreeNode = {
|
||||
id,
|
||||
label: config.name,
|
||||
type: "connection",
|
||||
connectionId: id,
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
};
|
||||
const existing = treeNodes.value.findIndex((n) => n.id === id);
|
||||
if (existing >= 0) {
|
||||
treeNodes.value[existing] = node;
|
||||
} else {
|
||||
treeNodes.value.push(node);
|
||||
}
|
||||
return id;
|
||||
} finally {
|
||||
const node = findNode(treeNodes.value, config.id);
|
||||
if (node) node.isLoading = false;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
async function disconnect(connectionId: string) {
|
||||
|
|
@ -475,14 +506,6 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
config.id = crypto.randomUUID();
|
||||
const normalized = normalizeConnection(config);
|
||||
addConnection(normalized);
|
||||
treeNodes.value.push({
|
||||
id: normalized.id,
|
||||
label: normalized.name,
|
||||
type: "connection" as const,
|
||||
connectionId: normalized.id,
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue