fix: 修复连接树偶发只剩用户与权限
This commit is contained in:
1 parent
95be4fb195
commit
cb8989153a
3 files changed
+169
-4
No files matched your search
@@ -597,7 +597,30 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
return pinnedTreeNodeIds.value.has(id);
|
return pinnedTreeNodeIds.value.has(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isConnectionUtilityNode(node: TreeNode): boolean {
|
||||||
|
return node.type === "user-admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectionMetadataChildren(children: TreeNode[] | undefined): TreeNode[] {
|
||||||
|
return (children || []).filter((child) => !isConnectionUtilityNode(child));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasConnectionMetadataChildren(children: TreeNode[] | undefined): boolean {
|
||||||
|
return connectionMetadataChildren(children).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function preserveExistingConnectionMetadataChildren(parent: TreeNode, children: TreeNode[]): TreeNode[] {
|
||||||
|
if (parent.type !== "connection" || hasConnectionMetadataChildren(children)) return children;
|
||||||
|
|
||||||
|
const existingMetadataChildren = connectionMetadataChildren(parent.children);
|
||||||
|
const nextUtilityChildren = children.filter(isConnectionUtilityNode);
|
||||||
|
if (existingMetadataChildren.length === 0 || nextUtilityChildren.length === 0) return children;
|
||||||
|
|
||||||
|
return [...existingMetadataChildren, ...nextUtilityChildren];
|
||||||
|
}
|
||||||
|
|
||||||
function setChildren(parent: TreeNode, children: TreeNode[]) {
|
function setChildren(parent: TreeNode, children: TreeNode[]) {
|
||||||
|
children = preserveExistingConnectionMetadataChildren(parent, children);
|
||||||
if (parent.children && parent.children.length > 0) {
|
if (parent.children && parent.children.length > 0) {
|
||||||
const oldMap = new Map(parent.children.map((c) => [c.id, c] as const));
|
const oldMap = new Map(parent.children.map((c) => [c.id, c] as const));
|
||||||
children = children.map((child) => {
|
children = children.map((child) => {
|
||||||
@@ -637,7 +660,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function withConnectionUtilityNodes(connectionId: string, children: TreeNode[], existingConnectionNode?: TreeNode): TreeNode[] {
|
function withConnectionUtilityNodes(connectionId: string, children: TreeNode[], existingConnectionNode?: TreeNode): TreeNode[] {
|
||||||
const nonUtilityChildren = children.filter((child) => child.type !== "user-admin");
|
const nonUtilityChildren = connectionMetadataChildren(children);
|
||||||
const userAdminNode = buildUserAdminNode(connectionId, existingConnectionNode);
|
const userAdminNode = buildUserAdminNode(connectionId, existingConnectionNode);
|
||||||
return [...nonUtilityChildren, userAdminNode].filter(Boolean) as TreeNode[];
|
return [...nonUtilityChildren, userAdminNode].filter(Boolean) as TreeNode[];
|
||||||
}
|
}
|
||||||
@@ -865,6 +888,9 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
const config = node.connectionId ? getConfig(node.connectionId) : undefined;
|
const config = node.connectionId ? getConfig(node.connectionId) : undefined;
|
||||||
const cachedChildren = normalizeCataloglessDatabaseNodes(expandCachedObjectBrowserNodes(decoded.children));
|
const cachedChildren = normalizeCataloglessDatabaseNodes(expandCachedObjectBrowserNodes(decoded.children));
|
||||||
const childrenWithLinkedServers = node.type === "connection" && node.connectionId ? ensureSqlServerLinkedRootNode(node.connectionId, cachedChildren, config) : cachedChildren;
|
const childrenWithLinkedServers = node.type === "connection" && node.connectionId ? ensureSqlServerLinkedRootNode(node.connectionId, cachedChildren, config) : cachedChildren;
|
||||||
|
if (node.type === "connection" && !hasConnectionMetadataChildren(childrenWithLinkedServers)) {
|
||||||
|
return { hit: false, isStale: false };
|
||||||
|
}
|
||||||
const normalizedChildren = sortSidebarTreeChildrenForParent(node, childrenWithLinkedServers, config?.db_type);
|
const normalizedChildren = sortSidebarTreeChildrenForParent(node, childrenWithLinkedServers, config?.db_type);
|
||||||
setChildren(node, node.type === "connection" && node.connectionId ? withSavedSqlRoot(node.connectionId, normalizedChildren, node) : normalizedChildren);
|
setChildren(node, node.type === "connection" && node.connectionId ? withSavedSqlRoot(node.connectionId, normalizedChildren, node) : normalizedChildren);
|
||||||
node.isExpanded = true;
|
node.isExpanded = true;
|
||||||
@@ -875,9 +901,19 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
await api.saveSchemaCache(cacheKey, encodeSchemaTreeCache(children)).catch(() => undefined);
|
await api.saveSchemaCache(cacheKey, encodeSchemaTreeCache(children)).catch(() => undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function savePersistedConnectionTreeChildren(cacheKey: string, children: TreeNode[]) {
|
||||||
|
const metadataChildren = connectionMetadataChildren(children);
|
||||||
|
if (metadataChildren.length === 0) return;
|
||||||
|
await savePersistedTreeChildren(cacheKey, metadataChildren);
|
||||||
|
}
|
||||||
|
|
||||||
function useCachedChildren(node: TreeNode, options?: LoadTreeOptions): boolean {
|
function useCachedChildren(node: TreeNode, options?: LoadTreeOptions): boolean {
|
||||||
if (options?.force || !loadedTreeNodeChildrenIds.value.has(node.id)) return false;
|
if (options?.force || !loadedTreeNodeChildrenIds.value.has(node.id)) return false;
|
||||||
if (node.type === "connection" && node.connectionId) {
|
if (node.type === "connection" && node.connectionId) {
|
||||||
|
if (!hasConnectionMetadataChildren(node.children)) {
|
||||||
|
clearLoadedChildrenCache(node.id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const normalizedChildren = sortSidebarTreeChildrenForParent(node, withSavedSqlRoot(node.connectionId, node.children || [], node), getConfig(node.connectionId)?.db_type);
|
const normalizedChildren = sortSidebarTreeChildrenForParent(node, withSavedSqlRoot(node.connectionId, node.children || [], node), getConfig(node.connectionId)?.db_type);
|
||||||
setChildren(node, normalizedChildren);
|
setChildren(node, normalizedChildren);
|
||||||
}
|
}
|
||||||
@@ -1389,7 +1425,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
const children = withSavedSqlRoot(connectionId, buildDuckDbConnectionTreeNodes(connectionId, databases, schemas), node);
|
const children = withSavedSqlRoot(connectionId, buildDuckDbConnectionTreeNodes(connectionId, databases, schemas), node);
|
||||||
if (isSidebarSearchQueryChanged(options)) return;
|
if (isSidebarSearchQueryChanged(options)) return;
|
||||||
setChildren(node, children);
|
setChildren(node, children);
|
||||||
await savePersistedTreeChildren(cacheKey, children);
|
await savePersistedConnectionTreeChildren(cacheKey, node.children || children);
|
||||||
} else if (config && connectionUsesVisibleSchemaFilter(config)) {
|
} else if (config && connectionUsesVisibleSchemaFilter(config)) {
|
||||||
const schemaFilterConfig = config;
|
const schemaFilterConfig = config;
|
||||||
const effectiveDb = schemaFilterConfig.database || "";
|
const effectiveDb = schemaFilterConfig.database || "";
|
||||||
@@ -1415,7 +1451,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
}));
|
}));
|
||||||
if (isSidebarSearchQueryChanged(options)) return;
|
if (isSidebarSearchQueryChanged(options)) return;
|
||||||
setChildren(node, withSavedSqlRoot(connectionId, schemaNodes, node));
|
setChildren(node, withSavedSqlRoot(connectionId, schemaNodes, node));
|
||||||
await savePersistedTreeChildren(cacheKey, schemaNodes);
|
await savePersistedConnectionTreeChildren(cacheKey, node.children || schemaNodes);
|
||||||
} else {
|
} else {
|
||||||
const cacheKey = schemaCacheKey(connectionId, "databases");
|
const cacheKey = schemaCacheKey(connectionId, "databases");
|
||||||
if (!options?.force) {
|
if (!options?.force) {
|
||||||
@@ -1458,7 +1494,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
const children = withSavedSqlRoot(connectionId, databaseNodes, node);
|
const children = withSavedSqlRoot(connectionId, databaseNodes, node);
|
||||||
if (isSidebarSearchQueryChanged(options)) return;
|
if (isSidebarSearchQueryChanged(options)) return;
|
||||||
setChildren(node, children);
|
setChildren(node, children);
|
||||||
await savePersistedTreeChildren(cacheKey, children);
|
await savePersistedConnectionTreeChildren(cacheKey, node.children || children);
|
||||||
}
|
}
|
||||||
node.isExpanded = true;
|
node.isExpanded = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
@@ -34,6 +34,10 @@ function conn(id: string): ConnectionConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function jsonResponse(value: unknown): Response {
|
||||||
|
return new Response(JSON.stringify(value), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||||
|
}
|
||||||
|
|
||||||
test("successful disconnect clears the connection error", async () => {
|
test("successful disconnect clears the connection error", async () => {
|
||||||
const restoreStorage = installMemoryStorage();
|
const restoreStorage = installMemoryStorage();
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
@@ -368,6 +372,131 @@ test("hanging database metadata load times out and clears loading state", async
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("utility-only connection cache is ignored and replaced with live metadata children", async () => {
|
||||||
|
const restoreStorage = installMemoryStorage();
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
const utilityOnlyCache = {
|
||||||
|
version: 2,
|
||||||
|
cachedAt: new Date().toISOString(),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "conn-1:__user_admin",
|
||||||
|
label: "tree.userAdmin",
|
||||||
|
type: "user-admin",
|
||||||
|
connectionId: "conn-1",
|
||||||
|
database: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let listDatabasesCalls = 0;
|
||||||
|
let savedPayload: any = null;
|
||||||
|
|
||||||
|
globalThis.fetch = (async (input, init) => {
|
||||||
|
const url = String(input);
|
||||||
|
if (url.startsWith("/api/schema/cache?")) {
|
||||||
|
return jsonResponse(utilityOnlyCache);
|
||||||
|
}
|
||||||
|
if (url.startsWith("/api/schema/databases?")) {
|
||||||
|
listDatabasesCalls++;
|
||||||
|
return jsonResponse([{ name: "app" }]);
|
||||||
|
}
|
||||||
|
if (url === "/api/schema/cache" && init?.method === "POST") {
|
||||||
|
savedPayload = JSON.parse(String(init.body ?? "{}")).payload;
|
||||||
|
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 });
|
||||||
|
store.treeNodes.push({
|
||||||
|
id: "conn-1",
|
||||||
|
label: "conn-1",
|
||||||
|
type: "connection",
|
||||||
|
connectionId: "conn-1",
|
||||||
|
children: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.loadDatabases("conn-1");
|
||||||
|
|
||||||
|
assert.equal(listDatabasesCalls, 1);
|
||||||
|
assert.deepEqual(
|
||||||
|
store.treeNodes[0].children?.map((child) => child.type),
|
||||||
|
["database", "user-admin"],
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
savedPayload?.children?.map((child: any) => child.type),
|
||||||
|
["database"],
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
restoreStorage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("empty root metadata does not replace existing databases with only user admin", async () => {
|
||||||
|
const restoreStorage = installMemoryStorage();
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
let savedPayload: any = null;
|
||||||
|
|
||||||
|
globalThis.fetch = (async (input, init) => {
|
||||||
|
const url = String(input);
|
||||||
|
if (url.startsWith("/api/schema/cache?")) {
|
||||||
|
return jsonResponse(null);
|
||||||
|
}
|
||||||
|
if (url.startsWith("/api/schema/databases?")) {
|
||||||
|
return jsonResponse([]);
|
||||||
|
}
|
||||||
|
if (url === "/api/schema/cache" && init?.method === "POST") {
|
||||||
|
savedPayload = JSON.parse(String(init.body ?? "{}")).payload;
|
||||||
|
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 });
|
||||||
|
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),
|
||||||
|
["database", "user-admin"],
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
store.treeNodes[0].children?.map((child) => child.id),
|
||||||
|
["conn-1:app", "conn-1:__user_admin"],
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
savedPayload?.children?.map((child: any) => child.id),
|
||||||
|
["conn-1:app"],
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
restoreStorage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test.each([
|
test.each([
|
||||||
["redis", "loadRedisDatabases", "/api/redis/list-databases", "Redis databases"],
|
["redis", "loadRedisDatabases", "/api/redis/list-databases", "Redis databases"],
|
||||||
["mq", "loadMqTenants", "/api/mq/tenants/list", "message queue tenants"],
|
["mq", "loadMqTenants", "/api/mq/tenants/list", "message queue tenants"],
|
||||||
|
|||||||
Reference in new issue
Block a user