diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 2b0b6fdc99..684d18acf4 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -309,6 +309,7 @@ function defaultSshTunnel(): SshTunnelConfig { use_ssh_agent: false, ssh_agent_sock_path: "", auth_method: "password", + allow_exec_channel_proxy: false, }; } @@ -342,6 +343,7 @@ function normalizeSshTunnel(hop: Partial): SshTunnelConfig { use_ssh_agent: !!hop.use_ssh_agent, ssh_agent_sock_path: hop.ssh_agent_sock_path || "", auth_method: hop.auth_method || inferSshAuthMethod(hop), + allow_exec_channel_proxy: !!hop.allow_exec_channel_proxy, profile_id: hop.profile_id || undefined, }; } @@ -6978,6 +6980,13 @@ function openExternalUrl(url: string) { {{ t("connection.sshExposeLan") }} +
+ + +
diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue index 42ca4dfc08..bba6d265b0 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -528,10 +528,10 @@ async function toggle() { node.isExpanded = false; if (!connectionStore.sidebarSearchQuery) connectionStore.releaseCollapsedTreeNodeChildren(node.id); connectionStore.cancelTreeNodeLoad(node.id); - emit("node-toggled", node, true); + emitNodeToggled(node, true, false); } else { node.isExpanded = true; - emit("node-toggled", node, false); + emitNodeToggled(node, false); } return; } @@ -541,13 +541,13 @@ async function toggle() { if (node.type === "connection-group") { node.isExpanded = !node.isExpanded; connectionStore.toggleConnectionGroupCollapsed(node.id); - emit("node-toggled", node, wasExpanded); + emitNodeToggled(node, wasExpanded); return; } if (node.type === "group-partitions") { node.isExpanded = !node.isExpanded; - emit("node-toggled", node, wasExpanded); + emitNodeToggled(node, wasExpanded); return; } @@ -558,27 +558,27 @@ async function toggle() { if (databaseObjectGroup && connectionStore.canUseLoadedTreeNodeToggle(node)) { node.isExpanded = !node.isExpanded; if (wasExpanded && !connectionStore.sidebarSearchQuery) connectionStore.releaseCollapsedTreeNodeChildren(node.id); - emit("node-toggled", node, wasExpanded); + emitNodeToggled(node, wasExpanded); return; } if (node.type === "group-extensions" && connectionStore.canUseLoadedTreeNodeToggle(node)) { node.isExpanded = !node.isExpanded; if (wasExpanded && !connectionStore.sidebarSearchQuery) connectionStore.releaseCollapsedTreeNodeChildren(node.id); - emit("node-toggled", node, wasExpanded); + emitNodeToggled(node, wasExpanded); return; } if (node.isExpanded) { node.isExpanded = false; if (!connectionStore.sidebarSearchQuery) connectionStore.releaseCollapsedTreeNodeChildren(node.id); - emit("node-toggled", node, wasExpanded); + emitNodeToggled(node, wasExpanded, false); return; } try { if (await loadSidebarObjectGroup(node, connectionStore)) { - emit("node-toggled", node, wasExpanded); + emitNodeToggled(node, wasExpanded); return; } @@ -715,7 +715,7 @@ async function toggle() { } else if (node.type === "group-extensions" && node.connectionId && hasTreeNodeDatabaseContext(node)) { await connectionStore.refreshTreeNode(node); } - emit("node-toggled", node, wasExpanded); + emitNodeToggled(node, wasExpanded); } catch (e: any) { if (!wasExpanded) node.isExpanded = false; const errMsg = e?.message || String(e); @@ -4673,6 +4673,15 @@ function activateRuntimeNode(node: TreeNode) { activeNode.value = node; } +// Async loaders can rebuild a connection node while awaiting the backend. +// Publish the live tree node so a stale rendered row cannot reset expansion. +function emitNodeToggled(node: TreeNode, wasExpanded: boolean, expandedOverride?: boolean) { + const liveNode = findSidebarActionTarget(connectionStore.treeNodes, createSidebarActionTarget(node)) ?? node; + if (expandedOverride !== undefined) liveNode.isExpanded = expandedOverride; + activeNode.value = liveNode; + emit("node-toggled", liveNode, wasExpanded); +} + function activateActionTarget(target: SidebarActionTarget) { activeNode.value = findSidebarActionTarget(connectionStore.treeNodes, target) ?? target; } diff --git a/apps/desktop/src/components/sidebar/__tests__/crossDatabaseTablePaste.spec.ts b/apps/desktop/src/components/sidebar/__tests__/crossDatabaseTablePaste.spec.ts index cd010cc11c..76528d1c28 100644 --- a/apps/desktop/src/components/sidebar/__tests__/crossDatabaseTablePaste.spec.ts +++ b/apps/desktop/src/components/sidebar/__tests__/crossDatabaseTablePaste.spec.ts @@ -4,6 +4,14 @@ import { describe, expect, it } from "vitest"; const runtimeSource = readFileSync(new URL("../SidebarTreeRuntimeHost.vue", import.meta.url), "utf8"); describe("cross-database table paste", () => { + it("publishes the live node after async tree loads", () => { + expect(runtimeSource).toContain("function emitNodeToggled(node: TreeNode, wasExpanded: boolean, expandedOverride?: boolean)"); + expect(runtimeSource).toContain("findSidebarActionTarget(connectionStore.treeNodes, createSidebarActionTarget(node)) ?? node"); + expect(runtimeSource).toContain("emitNodeToggled(node, wasExpanded, false)"); + expect(runtimeSource).toMatch(/await connectionStore\.loadMongoDatabases\(node\.connectionId\);[\s\S]*?emitNodeToggled\(node, wasExpanded\)/); + expect(runtimeSource).toContain("connectionStore.cancelTreeNodeLoad(node.id)"); + }); + it("routes a table clipboard from another context to data transfer", () => { expect(runtimeSource).toContain("function canTransferTreeClipboardToCurrentNode()"); expect(runtimeSource).toContain("function openTransferFromTreeClipboard()"); diff --git a/apps/desktop/src/components/ssh/SshHostKeyPromptDialog.vue b/apps/desktop/src/components/ssh/SshHostKeyPromptDialog.vue index 79970d0539..27d9716f6a 100644 --- a/apps/desktop/src/components/ssh/SshHostKeyPromptDialog.vue +++ b/apps/desktop/src/components/ssh/SshHostKeyPromptDialog.vue @@ -17,6 +17,7 @@ interface SshPromptRequest { key_type?: string | null; fingerprint?: string | null; prompt?: string | null; + echo?: boolean; } interface SshHostKeyNotice { @@ -35,6 +36,7 @@ const { toast } = useToast(); // Using a queue keyed by request id means every prompt is shown and answered. const queue = ref([]); const current = computed(() => queue.value[0] ?? null); +const isSecretPrompt = computed(() => current.value?.kind === "SecretInput"); const visible = ref(false); const remember = ref(true); @@ -235,17 +237,19 @@ function submitSecret() {