feat(xugu): expose tablespaces and datafiles in tree

This commit is contained in:
Elias authored and GitHub committed 2026-09-01 00:34:12 +08:00
1 parent afe3b3f3c2
commit 351a8b699b
32 files changed
+811 -4

No files matched your search

@@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import { buildXuguTablespaceChildren, xuguDatafileDisplayName } from "@/lib/sidebar/xuguTablespaces";
describe("Xugu tablespace tree", () => {
it("uses the physical file basename while retaining the full path", () => {
expect(xuguDatafileDisplayName("/data/DATA1.DBF", 1)).toBe("DATA1.DBF");
expect(xuguDatafileDisplayName("", 2)).toBe("file-2");
});
it("builds tablespace -> files -> datafile nodes with stable ids", () => {
const parent = { id: "conn:storage", connectionId: "conn", database: "APP", children: [] };
const [node] = buildXuguTablespaceChildren(parent, [
{
node_id: "1",
space_id: 7,
space_name: "DATA1",
datafile_num: 1,
space_type: "PERMANENT",
datafiles: [{ node_id: "1", space_id: 7, path: "/data/DATA1.DBF", file_no: 1, curr_size: 1024 }],
},
]);
expect(node.id).toBe("conn:storage:tablespace:7");
expect(node.children?.[0].type).toBe("group-datafiles");
expect(node.children?.[0].children?.[0]).toMatchObject({
id: "conn:storage:tablespace:7:files:1:/data/DATA1.DBF",
type: "datafile",
label: "DATA1.DBF",
xuguDatafilePath: "/data/DATA1.DBF",
});
});
it("preserves expanded state on refresh", () => {
const first = buildXuguTablespaceChildren({ id: "conn:storage", connectionId: "conn", database: "APP", children: [] }, [{ node_id: "1", space_id: 7, space_name: "DATA1", datafile_num: 0, space_type: "PERMANENT", datafiles: [] }]);
first[0].isExpanded = true;
first[0].children![0].isExpanded = true;
const second = buildXuguTablespaceChildren({ id: "conn:storage", connectionId: "conn", database: "APP", children: first }, [{ node_id: "1", space_id: 7, space_name: "DATA1", datafile_num: 0, space_type: "PERMANENT", datafiles: [] }]);
expect(second[0].isExpanded).toBe(true);
expect(second[0].children?.[0].isExpanded).toBe(true);
});
});
+1
View File
@@ -151,6 +151,7 @@ export const syncSavedSqlDirectory = forward("syncSavedSqlDirectory");
export const listDatabases = forward("listDatabases");
export const listDatabaseMetadata = forward("listDatabaseMetadata");
export const listDatabaseStorage = forward("listDatabaseStorage");
export const listXuguTablespaces = forward("listXuguTablespaces");
export const getSqlServerCompletionContext = forward("getSqlServerCompletionContext");
export const listDorisCatalogs = forward("listDorisCatalogs");
export const listDorisCatalogDatabases = forward("listDorisCatalogDatabases");
+5
View File
@@ -4,6 +4,7 @@ import type {
DatabaseConnectionInfo,
DatabaseInfo,
DatabaseStorageInfo,
XuguTablespaceInfo,
SqlServerCompletionContext,
SchemaInfo,
LinkedServerInfo,
@@ -796,6 +797,10 @@ export async function listDatabaseStorage(connectionId: string, databases: strin
});
}
export async function listXuguTablespaces(connectionId: string, database?: string): Promise<XuguTablespaceInfo[]> {
return get(`/api/schema/xugu/tablespaces?${qs({ connection_id: connectionId, database })}`);
}
export async function getSqlServerCompletionContext(connectionId: string, database: string): Promise<SqlServerCompletionContext> {
return get(`/api/schema/sqlserver/completion-context?${qs({ connection_id: connectionId, database })}`);
}
+5
View File
@@ -5,6 +5,7 @@ import { normalizeRustMongoCommand, type MongoCommand } from "@/lib/mongo/mongoS
import { ExternalSqlFileTooLargeError } from "@/lib/sql/sqlFileOpen";
import { appendDebugLog, isDebugLoggingEnabled } from "@/lib/backend/debugLog";
import { decodeMeilisearchDocumentPage, decodeMeilisearchSearchResult, type MeilisearchDocumentPage, type MeilisearchDocumentPageWire, type MeilisearchSearchResult, type MeilisearchSearchWireResult } from "@/lib/backend/meilisearchTransport";
import type { XuguTablespaceInfo } from "@/types/database";
import type { CreatedKey, EnqueuedTaskSummary, KeyCreateInput, KeyListItem, KeyPage, KeyUpdateInput, MeilisearchSystemOverview, MeilisearchTask, TaskListInput, TaskPage, TaskSelector } from "@/types/meilisearchManagement";
/** Normalize Tauri rejections once at the public backend boundary. */
@@ -1160,6 +1161,10 @@ export async function listDatabaseStorage(connectionId: string, databases: strin
return invoke("list_database_storage", { connectionId, databases });
}
export async function listXuguTablespaces(connectionId: string, database?: string): Promise<XuguTablespaceInfo[]> {
return invoke("list_xugu_tablespaces", { connectionId, database });
}
export async function getSqlServerCompletionContext(connectionId: string, database: string): Promise<SqlServerCompletionContext> {
return invoke("get_sqlserver_completion_context", { connectionId, database });
}
@@ -39,6 +39,7 @@ const leafTypes: Set<TreeNodeType> = new Set([
"table-search-control",
"load-more",
"extension",
"datafile",
]);
const fullWidthLabelTypes: Set<TreeNodeType> = new Set(["table", "view", "materialized_view", "mongo-collection", "mongo-bucket", "dynamodb-table", "vector-collection", "elasticsearch-index"]);
@@ -13,7 +13,7 @@ export function treeNodeLoadedChildrenContentPresent(node: TreeNode, sidebarObje
// Grouped mode always materializes object-group placeholders; empty means a stale shell.
return sidebarObjectDisplay === "simple";
}
if (objectTypesForGroupNode(node.type) || TABLE_STRUCTURE_GROUP_TYPES.has(node.type) || node.type === "group-extensions") {
if (objectTypesForGroupNode(node.type) || TABLE_STRUCTURE_GROUP_TYPES.has(node.type) || node.type === "group-extensions" || node.type === "group-tablespaces" || node.type === "group-datafiles") {
return true;
}
return childCount > 0;
@@ -22,6 +22,8 @@ const treeGroupNodeTypes = new Set<TreeNodeType>([
"group-types",
"group-partitions",
"group-extensions",
"group-tablespaces",
"group-datafiles",
"type-attributes",
"type-methods",
]);
@@ -15,6 +15,10 @@ export function getTreeNodeIconInfo(node: TreeNode): TreeNodeIconInfo | null {
case "database":
case "mongo-db":
return { icon: Database, colorClass: "text-yellow-500" };
case "tablespace":
return { icon: Database, colorClass: "text-orange-500" };
case "datafile":
return { icon: FileCode, colorClass: "text-slate-500" };
case "vector-database":
return { icon: Database, colorClass: "text-cyan-500" };
case "linked-server-root":
@@ -138,6 +142,10 @@ export function getTreeNodeIconInfo(node: TreeNode): TreeNodeIconInfo | null {
return { icon: Clock, colorClass: "text-orange-400" };
case "group-packages":
return { icon: Package, colorClass: "text-cyan-500" };
case "group-tablespaces":
return { icon: Database, colorClass: "text-orange-500" };
case "group-datafiles":
return { icon: node.isExpanded ? FolderOpen : FolderClosed, colorClass: "text-slate-500" };
case "group-partitions":
return { icon: node.isExpanded ? FolderOpen : FolderClosed, colorClass: "text-green-400" };
case "group-extensions":
@@ -0,0 +1,50 @@
import type { TreeNode, XuguTablespaceInfo } from "@/types/database";
export function xuguDatafileDisplayName(path: string, fileNo: number): string {
const normalized = path.trim().replaceAll("\\", "/");
const parts = normalized.split("/").filter(Boolean);
const basename = parts.length > 0 ? parts[parts.length - 1] : undefined;
return basename || path.trim() || `file-${fileNo}`;
}
export function buildXuguTablespaceChildren(parent: Pick<TreeNode, "id" | "connectionId" | "database" | "children">, tablespaces: readonly XuguTablespaceInfo[]): TreeNode[] {
const existing = new Map((parent.children ?? []).map((child) => [child.id, child] as const));
return tablespaces.map((tablespace) => {
const tablespaceId = `${parent.id}:tablespace:${tablespace.space_id}`;
const previousTablespace = existing.get(tablespaceId);
const filesGroupId = `${tablespaceId}:files`;
const previousFilesGroup = previousTablespace?.children?.find((child) => child.id === filesGroupId);
const fileNodes = tablespace.datafiles.map((file) => ({
id: `${filesGroupId}:${file.file_no}:${file.path}`,
label: xuguDatafileDisplayName(file.path, file.file_no),
type: "datafile" as const,
connectionId: parent.connectionId,
database: parent.database,
objectName: file.path,
xuguDatafilePath: file.path,
comment: file.path,
}));
const filesGroup: TreeNode = {
id: filesGroupId,
label: "tree.xuguDatafiles",
type: "group-datafiles",
connectionId: parent.connectionId,
database: parent.database,
objectCount: fileNodes.length,
isExpanded: previousFilesGroup?.isExpanded ?? false,
children: fileNodes,
};
return {
id: tablespaceId,
label: tablespace.space_name,
type: "tablespace" as const,
connectionId: parent.connectionId,
database: parent.database,
objectName: tablespace.space_name,
xuguTablespace: tablespace,
objectCount: tablespace.datafiles.length,
isExpanded: previousTablespace?.isExpanded ?? false,
children: [filesGroup],
} satisfies TreeNode;
});
}