fix(sidebar): search databases across connections

This commit is contained in:
ManjusriBuddha 2026-07-17 11:28:13 +08:00 committed by GitHub
parent cfd5f31473
commit 9dbd4902e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 123 additions and 3 deletions

View File

@ -163,6 +163,12 @@ function isSimpleObjectSearchParent(node: TreeNode): boolean {
}
function collectExpandedObjectSearchTargets(node: TreeNode, tasks: Promise<void>[], refreshedNodeIds?: Set<string>) {
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));

View File

@ -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;
}

View File

@ -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<void>((resolve) => {
markListStarted = resolve;
});
const listDatabases = vi.fn(
() =>
new Promise<Array<{ name: string; comment: null }>>((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);

View File

@ -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,