From 9dbd4902e563038e8802b03dbc0ebda856c94158 Mon Sep 17 00:00:00 2001 From: ManjusriBuddha <55905747+ManjusriBuddha@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:28:13 +0800 Subject: [PATCH] fix(sidebar): search databases across connections --- .../src/components/sidebar/ConnectionTree.vue | 6 ++ .../sidebar/SidebarTreeRuntimeHost.vue | 4 +- .../connectionStore.metadataLoading.spec.ts | 89 +++++++++++++++++++ apps/desktop/src/stores/connectionStore.ts | 27 +++++- 4 files changed, 123 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue index 79cf600cc..dfa56c402 100644 --- a/apps/desktop/src/components/sidebar/ConnectionTree.vue +++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue @@ -163,6 +163,12 @@ function isSimpleObjectSearchParent(node: TreeNode): boolean { } function collectExpandedObjectSearchTargets(node: TreeNode, tasks: Promise[], refreshedNodeIds?: Set) { + if (refreshedNodeIds && node.type === "connection" && node.connectionId) { + if (store.connectedIds.has(node.connectionId)) { + tasks.push(store.loadConnectedConnectionRootForSidebarSearch(node.connectionId)); + } + if (node.connectionId !== store.activeConnectionId) return; + } if (refreshedNodeIds && isSimpleObjectSearchParent(node)) { refreshedNodeIds.add(node.id); tasks.push(store.refreshTreeNode(node)); diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue index 5c5294b4e..08d82c392 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -479,14 +479,14 @@ async function toggle() { const databaseObjectGroup = node.type === "group-tables" || node.type === "group-views" || node.type === "group-materialized-views" || node.type === "group-procedures" || node.type === "group-functions" || node.type === "group-sequences" || node.type === "group-packages"; if (databaseObjectGroup && connectionStore.isTreeNodeChildrenLoaded(node.id)) { node.isExpanded = !node.isExpanded; - if (wasExpanded) connectionStore.releaseCollapsedTreeNodeChildren(node.id); + if (wasExpanded && !connectionStore.sidebarSearchQuery) connectionStore.releaseCollapsedTreeNodeChildren(node.id); emit("node-toggled", node, wasExpanded); return; } if (node.isExpanded) { node.isExpanded = false; - connectionStore.releaseCollapsedTreeNodeChildren(node.id); + if (!connectionStore.sidebarSearchQuery) connectionStore.releaseCollapsedTreeNodeChildren(node.id); emit("node-toggled", node, wasExpanded); return; } diff --git a/apps/desktop/src/stores/__tests__/connectionStore.metadataLoading.spec.ts b/apps/desktop/src/stores/__tests__/connectionStore.metadataLoading.spec.ts index abff9d224..064bce28f 100644 --- a/apps/desktop/src/stores/__tests__/connectionStore.metadataLoading.spec.ts +++ b/apps/desktop/src/stores/__tests__/connectionStore.metadataLoading.spec.ts @@ -58,6 +58,95 @@ describe("connectionStore metadata loading", () => { setActivePinia(createPinia()); }); + it("loads missing database roots only for connected sidebar search targets", async () => { + const checkConnectionHealth = vi.fn().mockResolvedValue(undefined); + const listDatabases = vi.fn().mockResolvedValue([{ name: "dajia", comment: null }]); + + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + checkConnectionHealth, + listDatabases, + loadSchemaCache: vi.fn().mockResolvedValue(null), + saveSchemaCache: vi.fn().mockResolvedValue(undefined), + saveConnections: vi.fn().mockResolvedValue(undefined), + saveSidebarLayout: vi.fn().mockResolvedValue(undefined), + })); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const { filterSidebarTree } = await import("@/lib/sidebar/sidebarSearchTree"); + const store = useConnectionStore(); + const active = { ...mysqlConnection(), id: "mysql-active", name: "localhost" }; + const connected = { ...mysqlConnection(), id: "mysql-connected", name: "PLM-PRO" }; + const disconnected = { ...mysqlConnection(), id: "mysql-disconnected", name: "offline" }; + const nodes: TreeNode[] = [ + { + id: active.id, + label: active.name, + type: "connection", + connectionId: active.id, + isExpanded: true, + children: [{ id: `${active.id}:dajia`, label: "dajia", type: "database", connectionId: active.id, database: "dajia", isExpanded: false }], + }, + { id: connected.id, label: connected.name, type: "connection", connectionId: connected.id, isExpanded: false, children: [] }, + { id: disconnected.id, label: disconnected.name, type: "connection", connectionId: disconnected.id, isExpanded: false, children: [] }, + ]; + store.connections = [active, connected, disconnected]; + store.connectedIds = new Set([active.id, connected.id]); + store.activeConnectionId = active.id; + store.treeNodes = nodes; + + await Promise.all(nodes.map((node) => store.loadConnectedConnectionRootForSidebarSearch(node.connectionId!))); + + expect(listDatabases).toHaveBeenCalledTimes(1); + expect(listDatabases).toHaveBeenCalledWith(connected.id); + expect(checkConnectionHealth).not.toHaveBeenCalled(); + expect(store.activeConnectionId).toBe(active.id); + expect(nodes.map((node) => node.isExpanded)).toEqual([true, false, false]); + expect(filterSidebarTree(nodes, "dajia", new Set()).map((node) => node.id)).toEqual([active.id, connected.id]); + }); + + it("does not collapse a connection whose normal root load is already in flight", async () => { + let resolveDatabases!: (databases: Array<{ name: string; comment: null }>) => void; + let markListStarted!: () => void; + const listStarted = new Promise((resolve) => { + markListStarted = resolve; + }); + const listDatabases = vi.fn( + () => + new Promise>((resolve) => { + resolveDatabases = resolve; + markListStarted(); + }), + ); + + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + checkConnectionHealth: vi.fn().mockResolvedValue(undefined), + listDatabases, + loadSchemaCache: vi.fn().mockResolvedValue(null), + saveSchemaCache: vi.fn().mockResolvedValue(undefined), + saveConnections: vi.fn().mockResolvedValue(undefined), + saveSidebarLayout: vi.fn().mockResolvedValue(undefined), + })); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + const connection = mysqlConnection(); + const node: TreeNode = { id: connection.id, label: connection.name, type: "connection", connectionId: connection.id, isExpanded: false, children: [] }; + store.connections = [connection]; + store.connectedIds.add(connection.id); + store.treeNodes = [node]; + + const normalLoad = store.loadDatabases(connection.id); + const searchLoad = store.loadConnectedConnectionRootForSidebarSearch(connection.id); + await listStarted; + resolveDatabases([{ name: "dajia", comment: null }]); + await Promise.all([normalLoad, searchLoad]); + + expect(listDatabases).toHaveBeenCalledTimes(1); + expect(node.isExpanded).toBe(true); + }); + it("renders simple-mode table children without waiting for supplemental objects", async () => { const tables: TableInfo[] = [{ name: "users", table_type: "TABLE", comment: null }]; const listTables = vi.fn().mockResolvedValue(tables); diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index d547e2279..93fe5cf0d 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -205,6 +205,7 @@ export type TreeClipboard = interface LoadTreeOptions { force?: boolean; + connectedOnly?: boolean; expectedSidebarSearchQuery?: string; searchFilter?: string; sidebarTableSearchParentId?: string; @@ -2227,7 +2228,11 @@ export const useConnectionStore = defineStore("connection", () => { if (!node) return; node.isLoading = true; try { - await ensureConnected(connectionId); + if (options?.connectedOnly) { + if (!connectedIds.value.has(connectionId)) return; + } else { + await ensureConnected(connectionId); + } if (useCachedChildren(node, options)) return; const config = getConfig(connectionId); @@ -2368,6 +2373,25 @@ export const useConnectionStore = defineStore("connection", () => { ); } + async function loadConnectedConnectionRootForSidebarSearch(connectionId: string) { + if (!connectedIds.value.has(connectionId)) return; + const config = getConfig(connectionId); + if (!config || ["redis", "etcd", "zookeeper", "mongodb", "elasticsearch", "milvus", "qdrant", "weaviate", "chromadb", "mq", "nacos"].includes(config.db_type)) return; + const node = findNode(treeNodes.value, connectionId); + if (!node || node.type !== "connection" || node.isLoading || hasConnectionMetadataChildren(node.children)) return; + const scope = { kind: "connection-databases" as const, connectionId, driverProfile: metadataDriverProfile(config) }; + if (metadataLoadCoordinator.has(scope)) return; + + const wasExpanded = !!node.isExpanded; + node.isLoading = true; + try { + await loadDatabases(connectionId, { connectedOnly: true }); + } finally { + node.isExpanded = wasExpanded; + node.isLoading = false; + } + } + async function loadRedisDatabases(connectionId: string) { const node = findNode(treeNodes.value, connectionId); if (!node) return; @@ -5369,6 +5393,7 @@ export const useConnectionStore = defineStore("connection", () => { disconnect, closeDatabaseConnection, ensureConnected, + loadConnectedConnectionRootForSidebarSearch, isTreeNodeChildrenLoaded, releaseCollapsedTreeNodeChildren, setBeforeConnectHandler,