mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 04:50:12 +00:00
fix(sidebar): refresh tree after dropping the last visible database
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user