feat(xugu): expose programmable object metadata

This commit is contained in:
Elias authored and GitHub committed 2026-07-17 17:53:44 +08:00
1 parent d3887c9936
commit 08cd8903f4
36 files changed
+431 -62

No files matched your search

@@ -93,13 +93,13 @@ import {
type ObjectBrowserSortDirection,
type ObjectBrowserSortKey,
} from "@/lib/table/objectBrowserRows";
import { resolveRowClickAction, shouldDeferSingleClick, type ObjectBrowserRowAction } from "@/lib/table/objectBrowserRowAction";
import { isSourceOnlyObjectBrowserRow, resolveRowClickAction, shouldDeferSingleClick, type ObjectBrowserRowAction } from "@/lib/table/objectBrowserRowAction";
import { filterObjectBrowserTableColumns } from "@/lib/table/objectBrowserTableInfo";
import { createSidePanelRequestGuard } from "@/lib/table/sidePanelRequestGuard";
import { runBatchTableTruncate } from "@/lib/table/batchTableTruncate";
import { tableColumnDefaultDisplayValue } from "@/lib/table/tableColumnDefaultPresentation";
type ObjectFilter = "all" | "tables" | "views" | "materializedViews" | "procedures" | "functions" | "sequences" | "packages";
type ObjectFilter = "all" | "tables" | "views" | "materializedViews" | "procedures" | "functions" | "triggers" | "sequences" | "packages" | "types";
type ObjectBrowserColumnKey = "select" | "name" | "type" | "estimatedRows" | "totalBytes" | "created_at" | "updated_at" | "comment";
const props = defineProps<{
@@ -236,8 +236,10 @@ const viewCount = computed(() => rows.value.filter((row) => row.type === "VIEW")
const materializedViewCount = computed(() => rows.value.filter((row) => row.type === "MATERIALIZED_VIEW").length);
const procedureCount = computed(() => rows.value.filter((row) => row.type === "PROCEDURE").length);
const functionCount = computed(() => rows.value.filter((row) => row.type === "FUNCTION").length);
const triggerCount = computed(() => rows.value.filter((row) => row.type === "TRIGGER").length);
const sequenceCount = computed(() => rows.value.filter((row) => row.type === "SEQUENCE").length);
const packageCount = computed(() => rows.value.filter((row) => row.type === "PACKAGE" || row.type === "PACKAGE_BODY").length);
const typeCount = computed(() => rows.value.filter((row) => row.type === "TYPE" || row.type === "TYPE_BODY").length);
const canOpenStructureEditor = computed(() => supportsTableStructureEditing(tableStructureDatabaseType.value));
const canOpenDiagram = computed(() => !!props.database && supportsSchemaDiagram(effectiveDatabaseType.value));
const canOpenTableImport = computed(() => !!props.database && supportsTableImport(effectiveDatabaseType.value));
@@ -253,8 +255,10 @@ const objectFilters = computed<ObjectFilter[]>(() =>
["materializedViews", materializedViewCount.value],
["procedures", procedureCount.value],
["functions", functionCount.value],
["triggers", triggerCount.value],
["sequences", sequenceCount.value],
["packages", packageCount.value],
["types", typeCount.value],
] as Array<[ObjectFilter, number]>
)
.filter(([filter, count]) => filter === "all" || count > 0)
@@ -526,8 +530,10 @@ function iconFor(row: ObjectBrowserRow) {
if (row.type === "VIEW" || row.type === "MATERIALIZED_VIEW") return Eye;
if (row.type === "PROCEDURE") return ScrollText;
if (row.type === "FUNCTION") return Braces;
if (row.type === "TRIGGER") return RotateCcw;
if (row.type === "SEQUENCE") return ListTree;
if (row.type === "PACKAGE" || row.type === "PACKAGE_BODY") return Package;
if (row.type === "TYPE" || row.type === "TYPE_BODY") return Braces;
return Table2;
}
@@ -536,9 +542,12 @@ function typeLabel(type: ObjectBrowserRow["type"]) {
if (type === "VIEW") return t("objects.view");
if (type === "PROCEDURE") return t("objects.procedure");
if (type === "FUNCTION") return t("objects.function");
if (type === "TRIGGER") return t("objects.trigger");
if (type === "SEQUENCE") return t("objects.sequence");
if (type === "PACKAGE") return t("objects.package");
if (type === "PACKAGE_BODY") return t("objects.packageBody");
if (type === "TYPE") return t("objects.typeDefinition");
if (type === "TYPE_BODY") return t("objects.typeBody");
return t("objects.table");
}
@@ -635,8 +644,10 @@ function rowMatchesObjectFilter(row: ObjectBrowserRow) {
if (objectFilter.value === "materializedViews") return row.type === "MATERIALIZED_VIEW";
if (objectFilter.value === "procedures") return row.type === "PROCEDURE";
if (objectFilter.value === "functions") return row.type === "FUNCTION";
if (objectFilter.value === "triggers") return row.type === "TRIGGER";
if (objectFilter.value === "sequences") return row.type === "SEQUENCE";
if (objectFilter.value === "packages") return row.type === "PACKAGE" || row.type === "PACKAGE_BODY";
if (objectFilter.value === "types") return row.type === "TYPE" || row.type === "TYPE_BODY";
return true;
}
@@ -673,8 +684,10 @@ function iconClass(type: ObjectBrowserRow["type"]) {
if (type === "VIEW" || type === "MATERIALIZED_VIEW") return "text-purple-500";
if (type === "PROCEDURE") return "text-blue-500";
if (type === "FUNCTION") return "text-amber-500";
if (type === "TRIGGER") return "text-rose-500";
if (type === "SEQUENCE") return "text-emerald-500";
if (type === "PACKAGE" || type === "PACKAGE_BODY") return "text-cyan-500";
if (type === "TYPE" || type === "TYPE_BODY") return "text-violet-500";
return "text-green-500";
}
@@ -682,8 +695,10 @@ function iconBgClass(type: ObjectBrowserRow["type"]) {
if (type === "VIEW" || type === "MATERIALIZED_VIEW") return "object-browser-icon-bg object-browser-icon-bg-view";
if (type === "PROCEDURE") return "object-browser-icon-bg object-browser-icon-bg-procedure";
if (type === "FUNCTION") return "object-browser-icon-bg object-browser-icon-bg-function";
if (type === "TRIGGER") return "object-browser-icon-bg object-browser-icon-bg-procedure";
if (type === "SEQUENCE") return "object-browser-icon-bg object-browser-icon-bg-sequence";
if (type === "PACKAGE" || type === "PACKAGE_BODY") return "object-browser-icon-bg object-browser-icon-bg-package";
if (type === "TYPE" || type === "TYPE_BODY") return "object-browser-icon-bg object-browser-icon-bg-function";
return "object-browser-icon-bg object-browser-icon-bg-table";
}
@@ -1013,14 +1028,16 @@ async function openSource(row: ObjectBrowserRow) {
try {
const result = await api.getObjectSource(connectionId, database, schema, row.name, row.type as ObjectSourceKind);
if (sidePanelGuard.isStale(epoch)) return;
sourceCanEdit.value = result.editable !== false && row.type !== "SEQUENCE";
const editable = await api.buildEditableObjectSource({
databaseType: effectiveDatabaseType.value,
objectType: row.type as ObjectSourceKind,
schema,
name: row.name,
source: result.source,
});
sourceCanEdit.value = result.editable !== false && !["SEQUENCE", "TRIGGER", "TYPE", "TYPE_BODY"].includes(row.type);
const editable = sourceCanEdit.value
? await api.buildEditableObjectSource({
databaseType: effectiveDatabaseType.value,
objectType: row.type as ObjectSourceKind,
schema,
name: row.name,
source: result.source,
})
: result.source;
if (sidePanelGuard.isStale(epoch)) return;
// Viewing database source must preserve its original whitespace and comments;
// formatting remains an explicit editor action instead of altering it on open.
@@ -2129,8 +2146,10 @@ function filterCount(filter: ObjectFilter) {
if (filter === "materializedViews") return materializedViewCount.value;
if (filter === "procedures") return procedureCount.value;
if (filter === "functions") return functionCount.value;
if (filter === "triggers") return triggerCount.value;
if (filter === "sequences") return sequenceCount.value;
if (filter === "packages") return packageCount.value;
if (filter === "types") return typeCount.value;
return rows.value.length;
}
@@ -2146,11 +2165,15 @@ function filterLabel(filter: ObjectFilter) {
? "objects.procedures"
: filter === "functions"
? "objects.functions"
: filter === "sequences"
? "objects.sequences"
: filter === "packages"
? "objects.packages"
: "objects.all";
: filter === "triggers"
? "tree.triggers"
: filter === "sequences"
? "objects.sequences"
: filter === "packages"
? "objects.packages"
: filter === "types"
? "tree.types"
: "objects.all";
return `${t(key)} ${filterCount(filter)}`;
}
@@ -2334,8 +2357,7 @@ function getPackageMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
if (item.type === "TABLE") return getTableMenuItems(item);
if (item.type === "VIEW" || item.type === "MATERIALIZED_VIEW") return getViewMenuItems(item);
if (item.type === "SEQUENCE") return getPackageMenuItems(item);
if (item.type === "PACKAGE" || item.type === "PACKAGE_BODY") return getPackageMenuItems(item);
if (isSourceOnlyObjectBrowserRow(item)) return getPackageMenuItems(item);
return getProcFuncMenuItems(item);
}
</script>
@@ -154,9 +154,9 @@ watch(deferredSearchQuery, (newQuery, oldQuery) => {
.catch(() => {});
});
const searchableObjectGroupTypes = new Set<TreeNodeType>(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-sequences", "group-packages"]);
const searchableObjectGroupTypes = new Set<TreeNodeType>(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-sequences", "group-packages", "group-types"]);
const simpleObjectParentTypes = new Set<TreeNodeType>(["database", "schema", "linked-server-schema"]);
const simpleObjectChildTypes = new Set<TreeNodeType>(["table", "view", "materialized_view", "procedure", "function", "sequence", "package", "package-body", "load-more"]);
const simpleObjectChildTypes = new Set<TreeNodeType>(["table", "view", "materialized_view", "procedure", "function", "trigger", "sequence", "package", "package-body", "type", "type-body", "load-more"]);
function isSimpleObjectSearchParent(node: TreeNode): boolean {
return settingsStore.editorSettings.sidebarObjectDisplay === "simple" && simpleObjectParentTypes.has(node.type) && node.isExpanded === true && (!!node.children?.some((child) => simpleObjectChildTypes.has(child.type)) || !!store.sidebarTableSearchQueries[node.id]?.trim());
@@ -445,7 +445,22 @@ function hasNodeDatabaseContext(node: TreeNode): node is TreeNode & { connection
return !!node.connectionId && hasTreeNodeDatabaseContext(node);
}
const groupTypes: Set<TreeNodeType> = new Set(["group-columns", "group-indexes", "group-fkeys", "group-triggers", "group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-sequences", "group-packages", "group-partitions", "group-extensions"]);
const groupTypes: Set<TreeNodeType> = new Set([
"group-columns",
"group-indexes",
"group-fkeys",
"group-triggers",
"group-tables",
"group-views",
"group-materialized-views",
"group-procedures",
"group-functions",
"group-sequences",
"group-packages",
"group-types",
"group-partitions",
"group-extensions",
]);
function isGroupLabel(node: TreeNode): boolean {
return groupTypes.has(node.type);
@@ -642,7 +657,7 @@ function runRowClickAction(clickDetail: number) {
scheduleOpenData(node);
} else if (isDocumentBrowserTreeNode(node.type)) {
openMongoTreeData(node);
} else if (node.type === "procedure" || node.type === "function" || node.type === "sequence" || node.type === "package" || node.type === "package-body") {
} else if (node.type === "procedure" || node.type === "function" || node.type === "trigger" || node.type === "sequence" || node.type === "package" || node.type === "package-body" || node.type === "type" || node.type === "type-body") {
openObjectSourceDialog(false);
} else if (action === "toggle") {
toggle();
@@ -2833,7 +2848,7 @@ const canOpenFieldLineage = computed(() => {
const hasTypeMenu = computed(() => {
const t = activeNode.value.type;
return t === "connection" || t === "database" || t === "schema" || t === "table" || t === "view" || t === "column" || t === "procedure" || t === "function" || t === "package" || t === "package-body" || isGroupLabel(activeNode.value);
return t === "connection" || t === "database" || t === "schema" || t === "table" || t === "view" || t === "column" || t === "procedure" || t === "function" || t === "trigger" || t === "package" || t === "package-body" || t === "type" || t === "type-body" || isGroupLabel(activeNode.value);
});
const isSelected = computed(() => connectionStore.selectedTreeNodeId === activeNode.value.id);
@@ -3871,7 +3886,7 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
return true;
}
if (node.type === "index" || node.type === "fkey" || node.type === "trigger") {
if (node.type === "index" || node.type === "fkey" || (node.type === "trigger" && !!node.tableName)) {
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
if (node.type === "index" && canOpenStructureEditor.value) {
items.push({ label: "", separator: true });
@@ -3931,7 +3946,7 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
return true;
}
if (node.type === "package" || node.type === "package-body") {
if (node.type === "trigger" || node.type === "package" || node.type === "package-body" || node.type === "type" || node.type === "type-body") {
items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 });
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
@@ -261,6 +261,10 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
return { icon: Package, colorClass: "text-cyan-500" };
case "package-body":
return { icon: FileCode, colorClass: "text-cyan-400" };
case "type":
return { icon: Braces, colorClass: "text-violet-500" };
case "type-body":
return { icon: FileCode, colorClass: "text-violet-400" };
case "group-tables":
return { icon: Table, colorClass: "text-green-500" };
case "group-views":
@@ -275,6 +279,8 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
return { icon: ListTree, colorClass: "text-emerald-500" };
case "group-packages":
return { icon: Package, colorClass: "text-cyan-500" };
case "group-types":
return { icon: Braces, colorClass: "text-violet-500" };
case "group-partitions":
return { icon: node.isExpanded ? FolderOpen : FolderClosed, colorClass: "text-green-400" };
case "group-extensions":
@@ -288,7 +294,22 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
}
}
const groupTypes: Set<TreeNodeType> = new Set(["group-columns", "group-indexes", "group-fkeys", "group-triggers", "group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-sequences", "group-packages", "group-partitions", "group-extensions"]);
const groupTypes: Set<TreeNodeType> = new Set([
"group-columns",
"group-indexes",
"group-fkeys",
"group-triggers",
"group-tables",
"group-views",
"group-materialized-views",
"group-procedures",
"group-functions",
"group-sequences",
"group-packages",
"group-types",
"group-partitions",
"group-extensions",
]);
function isGroupLabel(node: TreeNode): boolean {
return groupTypes.has(node.type);
@@ -304,10 +325,11 @@ function displayLabel(node: TreeNode): string {
}
function visibleLabel(node: TreeNode): string {
const withValidity = (label: string) => (node.valid === false ? `${label} · INVALID` : label);
if (node.type === "table" || node.type === "view" || node.type === "materialized_view" || node.type === "mongo-collection" || node.type === "vector-collection" || node.type === "elasticsearch-index") {
return sidebarDisplayTableName(node.label, settingsStore.editorSettings.sidebarHiddenTablePrefixes);
return withValidity(sidebarDisplayTableName(node.label, settingsStore.editorSettings.sidebarHiddenTablePrefixes));
}
return displayLabel(node);
return withValidity(displayLabel(node));
}
type DetailTooltipRow = {