fix(sidebar): refresh tree after dropping the last visible database

This commit is contained in:
Melody Ruch
2026-08-26 13:52:16 +08:00
committed by GitHub
parent ca0d4356c0
commit 4c28492f2c
3 changed files with 80 additions and 7 deletions
@@ -3714,7 +3714,14 @@ async function confirmDropDatabase() {
const executed = await executeTreeNodeSqlWithProductionGuard(node, sql, { database: "" });
if (!executed) return;
toast(t("contextMenu.dropDatabaseSuccess", { name: node.label }), 3000);
await connectionStore.loadDatabases(connectionId, { force: true });
try {
await connectionStore.loadDatabases(connectionId, { force: true });
} catch {
// The database was already dropped successfully above; if this refresh
// itself fails (e.g. a network blip), evict the node directly instead
// of leaving the deleted database showing in the tree.
connectionStore.removeTreeNode(node.id);
}
showDropDatabaseConfirm.value = false;
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
+15 -6
View File
@@ -1365,8 +1365,16 @@ export const useConnectionStore = defineStore("connection", () => {
return connectionMetadataChildren(children).length > 0;
}
function preserveExistingConnectionMetadataChildren(parent: TreeNode, children: TreeNode[]): TreeNode[] {
// `trustEmptyMetadataChildren` distinguishes two very different reasons this
// reload's metadata children can come back empty: the backend's raw fetch
// returned nothing at all (still ambiguous/possibly transient — keep the
// stale-preserve protection below), vs. the raw fetch had data but our own
// visible-databases/visible-schemas filter deterministically reduced it to
// zero (e.g. the only visible database was just dropped) — that emptiness
// is legitimate and must not be overridden by stale cached children.
function preserveExistingConnectionMetadataChildren(parent: TreeNode, children: TreeNode[], trustEmptyMetadataChildren = false): TreeNode[] {
if (parent.type !== "connection" || hasConnectionMetadataChildren(children)) return children;
if (trustEmptyMetadataChildren) return children;
const existingMetadataChildren = connectionMetadataChildren(parent.children);
const nextUtilityChildren = children.filter(isConnectionUtilityNode);
@@ -1467,9 +1475,9 @@ export const useConnectionStore = defineStore("connection", () => {
return deduped;
}
function setChildren(parent: TreeNode, children: TreeNode[]) {
function setChildren(parent: TreeNode, children: TreeNode[], options?: { trustEmptyConnectionChildren?: boolean }) {
// Compare markers against the resolved child list (after connection preserve), not the raw loader payload.
children = preserveExistingConnectionMetadataChildren(parent, children);
children = preserveExistingConnectionMetadataChildren(parent, children, options?.trustEmptyConnectionChildren === true);
children = decorateDatabaseSavedSqlTreeNodes(children, savedSqlFilesByDatabase, parent.children);
if (parent.type === "database") {
children = withDatabaseSavedSqlRoot(parent, children, savedSqlFilesByDatabase);
@@ -4130,7 +4138,7 @@ export const useConnectionStore = defineStore("connection", () => {
const targetNode = treeNodeLoadTarget(load);
if (!targetNode) return;
recordPrimaryVisibleObjectNames(connectionId, databaseNames);
setChildren(targetNode, children);
setChildren(targetNode, children, { trustEmptyConnectionChildren: databaseNames.length > 0 && visibleNames.length === 0 });
await savePersistedConnectionTreeChildren(cacheKey, targetNode.children || children);
} else if (config && connectionUsesVisibleSchemaFilter(config)) {
const schemaFilterConfig = config;
@@ -4161,7 +4169,7 @@ export const useConnectionStore = defineStore("connection", () => {
const targetNode = treeNodeLoadTarget(load);
if (!targetNode) return;
recordPrimaryVisibleObjectNames(connectionId, schemas);
setChildren(targetNode, withSavedSqlRoot(connectionId, schemaNodes, targetNode));
setChildren(targetNode, withSavedSqlRoot(connectionId, schemaNodes, targetNode), { trustEmptyConnectionChildren: schemas.length > 0 && visibleSchemas.length === 0 });
await savePersistedConnectionTreeChildren(cacheKey, targetNode.children || schemaNodes);
} else {
// Doris / StarRocks multi-catalog: when external catalogs exist,
@@ -4252,7 +4260,7 @@ export const useConnectionStore = defineStore("connection", () => {
connectionId,
databases.map((database) => database.name),
);
setChildren(targetNode, children);
setChildren(targetNode, children, { trustEmptyConnectionChildren: databases.length > 0 && visibleNames.length === 0 });
await savePersistedConnectionTreeChildren(cacheKey, targetNode.children || children);
}
}
@@ -4683,6 +4691,7 @@ export const useConnectionStore = defineStore("connection", () => {
})),
targetNode,
),
{ trustEmptyConnectionChildren: dbs.length > 0 && visibleDbs.length === 0 },
);
targetNode.isExpanded = true;
} catch (e) {
+57
View File
@@ -498,6 +498,63 @@ test("empty root metadata does not replace existing databases with only user adm
}
});
test("dropping the last visible database removes the stale node instead of preserving it", async () => {
const restoreStorage = installMemoryStorage();
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (input, init) => {
const url = String(input);
if (url.startsWith("/api/schema/cache?")) {
return jsonResponse(null);
}
if (url.startsWith("/api/schema/databases?")) {
// The backend still reports its own system databases; only the
// previously-visible user database ("app") is gone after the drop.
return jsonResponse([{ name: "information_schema" }, { name: "mysql" }, { name: "performance_schema" }, { name: "sys" }]);
}
if (url === "/api/schema/cache" && init?.method === "POST") {
return jsonResponse(null);
}
return new Response("unexpected request", { status: 500 });
}) as typeof fetch;
try {
setActivePinia(createPinia());
const store = useConnectionStore();
store.addEphemeralConnection({ ...conn("conn-1"), db_type: "mysql", port: 3306, visible_databases: ["app"] });
store.treeNodes.push({
id: "conn-1",
label: "conn-1",
type: "connection",
connectionId: "conn-1",
children: [
{
id: "conn-1:app",
label: "app",
type: "database",
connectionId: "conn-1",
database: "app",
children: [],
},
],
});
await store.loadDatabases("conn-1");
assert.deepEqual(
store.treeNodes[0].children?.map((child) => child.type),
["user-admin"],
);
assert.equal(
store.treeNodes[0].children?.some((child) => child.id === "conn-1:app"),
false,
);
} finally {
globalThis.fetch = originalFetch;
restoreStorage();
}
});
test.each([
["redis", "loadRedisDatabases", "/api/redis/list-databases", "Redis databases"],
["mq", "loadMqTenants", "/api/mq/tenants/list", "message queue tenants"],