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
@@ -685,6 +685,8 @@ const groupTypes: Set<TreeNodeType> = new Set([
|
||||
"group-types",
|
||||
"group-partitions",
|
||||
"group-extensions",
|
||||
"group-tablespaces",
|
||||
"group-datafiles",
|
||||
]);
|
||||
|
||||
function isGroupLabel(node: TreeNode): boolean {
|
||||
@@ -807,6 +809,35 @@ async function toggle(requestId = beginNavigationRequest()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Xugu tablespace rows and their datafile groups are eagerly materialized
|
||||
// with the tablespace listing; expanding them is a pure local toggle.
|
||||
if (node.type === "tablespace" || node.type === "group-datafiles") {
|
||||
node.isExpanded = !node.isExpanded;
|
||||
emitNodeToggled(node, wasExpanded);
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === "group-tablespaces") {
|
||||
if (connectionStore.canUseLoadedTreeNodeToggle(node)) {
|
||||
node.isExpanded = !node.isExpanded;
|
||||
if (wasExpanded && shouldReleaseCollapsedTreeNodeChildren()) connectionStore.releaseCollapsedTreeNodeChildren(node.id);
|
||||
emitNodeToggled(node, wasExpanded);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await connectionStore.loadXuguTablespaces(node, { preserveCollapsedChildren: !shouldReleaseCollapsedTreeNodeChildren() });
|
||||
emitNodeToggled(node, wasExpanded);
|
||||
} catch (e: any) {
|
||||
if (!isCurrentNavigationRequest(requestId)) return;
|
||||
if (!wasExpanded) node.isExpanded = false;
|
||||
const errMsg = e?.message || String(e);
|
||||
if (errMsg.includes(CONNECTION_ATTEMPT_CANCELLED_MESSAGE)) return;
|
||||
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
openDriverStoreForInstallError(errMsg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isXuguTypeMemberContainer(node, connectionStore.getConfig(node.connectionId || "")?.db_type)) {
|
||||
await connectionStore.loadXuguTypeMembers(node, { preserveCollapsedChildren: !shouldReleaseCollapsedTreeNodeChildren() });
|
||||
emitNodeToggled(node, wasExpanded);
|
||||
|
||||
@@ -240,6 +240,10 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
|
||||
return { icon: node.isExpanded ? FolderOpen : FolderClosed, colorClass: "text-amber-500" };
|
||||
case "database":
|
||||
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 "linked-server-root":
|
||||
return { icon: Network, colorClass: "text-blue-500" };
|
||||
case "linked-server":
|
||||
@@ -395,6 +399,10 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
|
||||
return { icon: node.isExpanded ? FolderOpen : FolderClosed, colorClass: "text-green-400" };
|
||||
case "group-extensions":
|
||||
return { icon: Package, colorClass: "text-violet-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 "extension":
|
||||
return { icon: Package, colorClass: "text-violet-400" };
|
||||
case "load-more":
|
||||
@@ -432,6 +440,7 @@ function displayLabel(node: TreeNode): string {
|
||||
function treeNodeSecondaryValue(node: TreeNode): string | undefined {
|
||||
if (node.type === "type" && node.customTypeKind) return t(`customType.kinds.${node.customTypeKind}`);
|
||||
if (node.type === "type-member") return (node.meta as CustomTypeTreeMemberMeta | undefined)?.displayValue;
|
||||
if (node.type === "datafile") return node.xuguDatafilePath;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
+61
@@ -21,6 +21,9 @@ const connectionStore = {
|
||||
loadObjectGroupChildren: vi.fn(async (node: TreeNode) => {
|
||||
node.isExpanded = true;
|
||||
}),
|
||||
loadXuguTablespaces: vi.fn(async (node: TreeNode) => {
|
||||
node.isExpanded = true;
|
||||
}),
|
||||
};
|
||||
|
||||
const queryStore = {
|
||||
@@ -421,4 +424,62 @@ describe("SidebarTreeRuntimeHost expansion", () => {
|
||||
expect(packageFunctionGroup.isExpanded).toBe(true);
|
||||
expect(packageFunctionGroup.children).toHaveLength(401);
|
||||
});
|
||||
|
||||
it("toggles Xugu tablespace and datafile group nodes locally", async () => {
|
||||
const datafileGroup: TreeNode = {
|
||||
id: "xugu:SHOP_DEMO:tablespace:SYSTEM:datafiles",
|
||||
label: "tree.datafiles",
|
||||
type: "group-datafiles",
|
||||
parentType: "tablespace",
|
||||
parentName: "SYSTEM",
|
||||
connectionId: "xugu",
|
||||
database: "SHOP_DEMO",
|
||||
objectCount: 2,
|
||||
isExpanded: false,
|
||||
children: [
|
||||
{ id: "xugu:SHOP_DEMO:tablespace:SYSTEM:datafile:1", label: "system_01.dbf", type: "datafile", parentType: "tablespace", parentName: "SYSTEM", connectionId: "xugu", database: "SHOP_DEMO", objectName: "system_01.dbf" },
|
||||
{ id: "xugu:SHOP_DEMO:tablespace:SYSTEM:datafile:2", label: "system_02.dbf", type: "datafile", parentType: "tablespace", parentName: "SYSTEM", connectionId: "xugu", database: "SHOP_DEMO", objectName: "system_02.dbf" },
|
||||
],
|
||||
};
|
||||
const tablespaceNode: TreeNode = {
|
||||
id: "xugu:SHOP_DEMO:tablespace:SYSTEM",
|
||||
label: "SYSTEM",
|
||||
type: "tablespace",
|
||||
connectionId: "xugu",
|
||||
database: "SHOP_DEMO",
|
||||
objectName: "SYSTEM",
|
||||
isExpanded: false,
|
||||
children: [datafileGroup],
|
||||
};
|
||||
connectionStore.treeNodes = [tablespaceNode];
|
||||
connectionStore.getConfig.mockReturnValue({ db_type: "xugu" });
|
||||
connectionStore.canUseLoadedTreeNodeToggle.mockReturnValue(false);
|
||||
|
||||
const host = ref<InstanceType<typeof SidebarTreeRuntimeHost> | null>(null);
|
||||
const toggled = vi.fn();
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
setup: () => () => h(SidebarTreeRuntimeHost, { ref: host, node: tablespaceNode, depth: 0, onNodeToggled: toggled }),
|
||||
}),
|
||||
);
|
||||
mountedApps.push(app);
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
app.use(i18n);
|
||||
app.mount(container);
|
||||
|
||||
host.value?.toggleNode(tablespaceNode);
|
||||
await nextTick();
|
||||
|
||||
expect(tablespaceNode.isExpanded).toBe(true);
|
||||
expect(connectionStore.loadXuguTablespaces).not.toHaveBeenCalled();
|
||||
expect(toggled).toHaveBeenCalledWith(tablespaceNode, true);
|
||||
|
||||
host.value?.toggleNode(datafileGroup);
|
||||
await nextTick();
|
||||
|
||||
expect(datafileGroup.isExpanded).toBe(true);
|
||||
expect(connectionStore.loadXuguTablespaces).not.toHaveBeenCalled();
|
||||
expect(toggled).toHaveBeenCalledWith(datafileGroup, true);
|
||||
});
|
||||
});
|
||||
@@ -3369,6 +3369,8 @@ export default {
|
||||
damengUsers: "Users",
|
||||
damengRoles: "Roles",
|
||||
damengJobAdmin: "Agent Jobs",
|
||||
xuguTablespaces: "Storage Spaces",
|
||||
xuguDatafiles: "Files",
|
||||
linkedServers: "Linked Servers",
|
||||
defaultDatabase: "Default DB",
|
||||
columns: "Columns",
|
||||
|
||||
@@ -3277,6 +3277,8 @@ export default withEnglishFallback({
|
||||
damengUsers: "Usuarios",
|
||||
damengRoles: "Roles",
|
||||
damengJobAdmin: "Trabajos del agente",
|
||||
xuguTablespaces: "Espacios de almacenamiento",
|
||||
xuguDatafiles: "Archivos",
|
||||
linkedServers: "Servidores vinculados",
|
||||
defaultDatabase: "Base predeterminada",
|
||||
columns: "Columnas",
|
||||
|
||||
@@ -3275,6 +3275,8 @@ export default withEnglishFallback({
|
||||
damengUsers: "Utenti",
|
||||
damengRoles: "Ruoli",
|
||||
damengJobAdmin: "Job agente",
|
||||
xuguTablespaces: "Spazi di archiviazione",
|
||||
xuguDatafiles: "File",
|
||||
linkedServers: "Server Collegati",
|
||||
defaultDatabase: "DB Predefinito",
|
||||
columns: "Colonne",
|
||||
|
||||
@@ -3301,6 +3301,8 @@ export default withEnglishFallback({
|
||||
damengUsers: "ユーザー",
|
||||
damengRoles: "ロール",
|
||||
damengJobAdmin: "エージェントジョブ",
|
||||
xuguTablespaces: "ストレージ領域",
|
||||
xuguDatafiles: "ファイル",
|
||||
defaultDatabase: "デフォルトDB",
|
||||
columns: "列",
|
||||
attributes: "属性",
|
||||
|
||||
@@ -3154,6 +3154,8 @@ export default withEnglishFallback({
|
||||
damengUsers: "사용자",
|
||||
damengRoles: "역할",
|
||||
damengJobAdmin: "에이전트 작업",
|
||||
xuguTablespaces: "저장 공간",
|
||||
xuguDatafiles: "파일",
|
||||
linkedServers: "연결된 서버",
|
||||
defaultDatabase: "기본 DB",
|
||||
columns: "컬럼",
|
||||
|
||||
@@ -3277,6 +3277,8 @@ export default withEnglishFallback({
|
||||
damengUsers: "Usuários",
|
||||
damengRoles: "Funções",
|
||||
damengJobAdmin: "Jobs do agente",
|
||||
xuguTablespaces: "Espaços de armazenamento",
|
||||
xuguDatafiles: "Arquivos",
|
||||
linkedServers: "Servidores Vinculados",
|
||||
materializedViews: "Visualizações Materializadas",
|
||||
defaultDatabase: "Banco padrão",
|
||||
|
||||
@@ -3290,6 +3290,8 @@ export default withEnglishFallback({
|
||||
damengUsers: "用户",
|
||||
damengRoles: "角色",
|
||||
damengJobAdmin: "代理作业",
|
||||
xuguTablespaces: "存储空间",
|
||||
xuguDatafiles: "文件",
|
||||
linkedServers: "链接服务器",
|
||||
defaultDatabase: "默认库",
|
||||
columns: "字段",
|
||||
|
||||
@@ -3274,6 +3274,8 @@ export default withEnglishFallback({
|
||||
damengUsers: "使用者",
|
||||
damengRoles: "角色",
|
||||
damengJobAdmin: "代理作業",
|
||||
xuguTablespaces: "儲存空間",
|
||||
xuguDatafiles: "檔案",
|
||||
linkedServers: "連結伺服器",
|
||||
materializedViews: "具體化檢視",
|
||||
defaultDatabase: "預設庫",
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -164,6 +164,7 @@ import { invalidateObjectBrowserRowsCache } from "@/lib/table/objectBrowserRowsC
|
||||
import { MetadataTaskLimiter } from "@/lib/metadata/metadataTaskLimiter";
|
||||
import { buildCustomTypeTreeChildren } from "@/lib/sidebar/customTypeTree";
|
||||
import { TreeNodeLoadRegistry, type TreeNodeLoadHandle } from "@/lib/metadata/treeNodeLoadHandle";
|
||||
import { buildXuguTablespaceChildren } from "@/lib/sidebar/xuguTablespaces";
|
||||
import i18n from "@/i18n";
|
||||
import type { MqAdminConfig } from "@/types/mq";
|
||||
import { RABBITMQ_MQ_TENANT, resolveMqSystemKindFromConnection } from "@/lib/mq/mqConsoleDefaults";
|
||||
@@ -1364,7 +1365,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
// as metadata children, withConnectionUtilityNodes would keep the old copies
|
||||
// AND append fresh ones on every useCachedChildren pass, duplicating the
|
||||
// 用户/角色 menus once per refresh cycle.
|
||||
return node.type === "user-admin" || node.type === "dameng-users" || node.type === "dameng-roles" || node.type === "dameng-job-admin" || node.type === "saved-sql-root";
|
||||
return node.type === "user-admin" || node.type === "dameng-users" || node.type === "dameng-roles" || node.type === "dameng-job-admin" || node.type === "group-tablespaces" || node.type === "saved-sql-root";
|
||||
}
|
||||
|
||||
function connectionMetadataChildren(children: TreeNode[] | undefined): TreeNode[] {
|
||||
@@ -1653,13 +1654,33 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
};
|
||||
}
|
||||
|
||||
function buildXuguTablespacesNode(connectionId: string, existingConnectionNode?: TreeNode): TreeNode | undefined {
|
||||
const config = getConfig(connectionId);
|
||||
if (effectiveDatabaseTypeForConnection(config) !== "xugu") return undefined;
|
||||
const existing = existingConnectionNode?.children?.find((child) => child.type === "group-tablespaces");
|
||||
return {
|
||||
id: `${connectionId}:__xugu_tablespaces`,
|
||||
label: "tree.xuguTablespaces",
|
||||
type: "group-tablespaces",
|
||||
connectionId,
|
||||
// SYS_TABLESPACES/SYS_DATAFILES are scoped to the current database. The
|
||||
// optional value lets the agent switch to the configured database while
|
||||
// retaining compatibility with connections that have no default DB.
|
||||
database: config?.database || "",
|
||||
objectCount: existing?.objectCount,
|
||||
isExpanded: existing?.isExpanded ?? false,
|
||||
children: existing?.children ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
function withConnectionUtilityNodes(connectionId: string, children: TreeNode[], existingConnectionNode?: TreeNode): TreeNode[] {
|
||||
const nonUtilityChildren = connectionMetadataChildren(children);
|
||||
const userAdminNode = buildUserAdminNode(connectionId, existingConnectionNode);
|
||||
const damengUserNode = buildDamengUserNode(connectionId, existingConnectionNode);
|
||||
const damengRoleNode = buildDamengRoleNode(connectionId, existingConnectionNode);
|
||||
const damengJobAdminNode = buildDamengJobAdminNode(connectionId, existingConnectionNode);
|
||||
return [...nonUtilityChildren, userAdminNode, damengUserNode, damengRoleNode, damengJobAdminNode].filter(Boolean) as TreeNode[];
|
||||
const xuguTablespacesNode = buildXuguTablespacesNode(connectionId, existingConnectionNode);
|
||||
return [...nonUtilityChildren, userAdminNode, damengUserNode, damengRoleNode, damengJobAdminNode, xuguTablespacesNode].filter(Boolean) as TreeNode[];
|
||||
}
|
||||
|
||||
function withSavedSqlRoot(connectionId: string, children: TreeNode[], existingConnectionNode?: TreeNode): TreeNode[] {
|
||||
@@ -4137,6 +4158,50 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
{ flush: "post" },
|
||||
);
|
||||
|
||||
async function loadXuguTablespaces(node: TreeNode, options?: LoadTreeOptions) {
|
||||
if (node.type !== "group-tablespaces" || !node.connectionId) return;
|
||||
const connectionId = node.connectionId;
|
||||
const config = getConfig(connectionId);
|
||||
if (effectiveDatabaseTypeForConnection(config) !== "xugu") return;
|
||||
return runTreeMetadataLoad(
|
||||
{
|
||||
kind: "xugu-tablespaces",
|
||||
connectionId,
|
||||
database: node.database || undefined,
|
||||
driverProfile: metadataDriverProfile(config),
|
||||
},
|
||||
async () => {
|
||||
let load = beginTreeNodeLoad(node);
|
||||
try {
|
||||
await ensureConnected(connectionId);
|
||||
load = reclaimTreeNodeLoad(load, node);
|
||||
if (useCachedChildren(node, options, load)) return;
|
||||
const tablespaces = await withMetadataLoadTimeout(connectionId, api.listXuguTablespaces(connectionId, node.database || undefined), "Xugu tablespaces");
|
||||
const targetNode = treeNodeLoadTarget(load);
|
||||
if (!targetNode) return;
|
||||
const children = buildXuguTablespaceChildren(targetNode, tablespaces);
|
||||
setChildren(targetNode, children);
|
||||
targetNode.objectCount = children.length;
|
||||
targetNode.isExpanded = true;
|
||||
} catch (error) {
|
||||
// Storage metadata is an optional, read-only enhancement. A user
|
||||
// without SYS_* view access should see an empty group rather than a
|
||||
// connection-level RPC error that blocks the rest of the tree.
|
||||
console.debug("[DBX][xugu-tablespaces:unavailable]", { connectionId, error });
|
||||
const targetNode = treeNodeLoadTarget(load);
|
||||
if (targetNode) {
|
||||
setChildren(targetNode, []);
|
||||
targetNode.objectCount = 0;
|
||||
targetNode.isExpanded = true;
|
||||
}
|
||||
} finally {
|
||||
finishTreeNodeLoad(load);
|
||||
}
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
async function loadDatabases(connectionId: string, options?: LoadTreeOptions) {
|
||||
const configForScope = getConfig(connectionId);
|
||||
const searchFilter = activeTreeLoadSearchFilter(options);
|
||||
@@ -6549,6 +6614,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
node.isExpanded = true;
|
||||
} else if (node.type === "doris-catalog" && node.connectionId) {
|
||||
await loadDorisCatalogDatabases(node, options);
|
||||
} else if (node.type === "group-tablespaces" && node.connectionId) {
|
||||
await loadXuguTablespaces(node, options);
|
||||
} else if (node.type === "database" && node.connectionId && hasTreeNodeDatabaseContext(node)) {
|
||||
if (node.catalog && node.catalog !== "internal") {
|
||||
await loadDorisCatalogTables(node, options);
|
||||
@@ -8863,6 +8930,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
loadObjectGroupChildren,
|
||||
loadCustomTypeChildren,
|
||||
loadPackageMembers,
|
||||
loadXuguTablespaces,
|
||||
loadXuguTypeMembers,
|
||||
loadMoreObjectGroupChildren,
|
||||
loadAllObjectGroupChildren,
|
||||
|
||||
@@ -344,6 +344,29 @@ export interface DatabaseStorageInfo {
|
||||
size_bytes: number | null;
|
||||
}
|
||||
|
||||
export interface XuguDatafileInfo {
|
||||
node_id: string;
|
||||
space_id: number;
|
||||
path: string;
|
||||
file_no: number;
|
||||
max_size?: number | null;
|
||||
step_size?: number | null;
|
||||
curr_size?: number | null;
|
||||
reserved1?: string | null;
|
||||
}
|
||||
|
||||
export interface XuguTablespaceInfo {
|
||||
node_id: string;
|
||||
space_id: number;
|
||||
space_name: string;
|
||||
datafile_num: number;
|
||||
space_type: string;
|
||||
media_error?: string | null;
|
||||
total_chunk_num?: number | null;
|
||||
free_chunk_num?: number | null;
|
||||
datafiles: XuguDatafileInfo[];
|
||||
}
|
||||
|
||||
export interface SqlServerCompletionContext {
|
||||
default_schema: string;
|
||||
supports_session_database_switch: boolean;
|
||||
@@ -862,6 +885,8 @@ export type TreeNodeType =
|
||||
| "connection"
|
||||
| "connection-group"
|
||||
| "database"
|
||||
| "tablespace"
|
||||
| "datafile"
|
||||
| "doris-catalog"
|
||||
| "linked-server-root"
|
||||
| "linked-server"
|
||||
@@ -902,6 +927,8 @@ export type TreeNodeType =
|
||||
| "group-packages"
|
||||
| "group-partitions"
|
||||
| "group-extensions"
|
||||
| "group-tablespaces"
|
||||
| "group-datafiles"
|
||||
| "extension"
|
||||
| "object-browser"
|
||||
| "user-admin"
|
||||
@@ -1001,6 +1028,8 @@ export interface TreeNode {
|
||||
comment?: string | null;
|
||||
valid?: boolean | null;
|
||||
sizeBytes?: number | null;
|
||||
xuguTablespace?: XuguTablespaceInfo;
|
||||
xuguDatafilePath?: string;
|
||||
objectCount?: number;
|
||||
loadedKeyCount?: number;
|
||||
totalKeyCount?: number;
|
||||
|
||||
Reference in new issue
Block a user