feat(xugu): expose tablespaces and datafiles in tree
This commit is contained in:
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);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
|
||||
@@ -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,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;
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user