feat(postgres): list user-defined types with details and DDL for PG-family databases

This commit is contained in:
amwps290 authored and GitHub committed 2026-08-11 15:00:24 +08:00
1 parent 0c6c1fada2
commit 154c460657
59 files changed
+6538 -150

No files matched your search

@@ -18,6 +18,7 @@ import {
Eraser,
Eye,
FileCode,
Info,
GripVertical,
KeyRound,
LayoutGrid,
@@ -55,6 +56,7 @@ import { SearchableSelect } from "@/components/ui/searchable-select";
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
import ProcedureExecutionDialog from "@/components/objects/ProcedureExecutionDialog.vue";
import CustomTypeInfoPanel from "@/components/objects/CustomTypeInfoPanel.vue";
import XlsxHeaderDialog from "@/components/export/XlsxHeaderDialog.vue";
import * as api from "@/lib/backend/api";
import type { ColumnInfo, ConnectionConfig, ForeignKeyInfo, IndexInfo, ObjectBrowserViewMode, ObjectBrowserViewport, ObjectInfo, ObjectSourceKind, ObjectStatistics, TableInfoTab, TreeNode, TriggerInfo } from "@/types/database";
@@ -118,6 +120,7 @@ import {
type ObjectBrowserSortKey,
} from "@/lib/table/objectBrowserRows";
import { isSourceOnlyObjectBrowserRow, resolveRowClickAction, shouldDeferSingleClick, type ObjectBrowserRowAction } from "@/lib/table/objectBrowserRowAction";
import { customTypeCapabilities, supportsTypeObjectSource } from "@/lib/database/databaseObjectCapabilities";
import { filterObjectBrowserTableColumns } from "@/lib/table/objectBrowserTableInfo";
import { createSidePanelRequestGuard } from "@/lib/table/sidePanelRequestGuard";
import { runBatchTableTruncate } from "@/lib/table/batchTableTruncate";
@@ -173,7 +176,7 @@ const sourceCanEdit = ref(true);
// --- Right-side panel state ---
// Unified panel: either "table-info" (for tables) or "source" (for views/procedures/etc.)
const sidePanelRow = ref<ObjectBrowserRow | null>(null);
const sidePanelMode = ref<"table-info" | "source">("source");
const sidePanelMode = ref<"table-info" | "source" | "type-info">("source");
// Table info panel state
const tableInfoTab = ref<TableInfoTab>("ddl");
const tableColumns = ref<ColumnInfo[]>([]);
@@ -196,6 +199,7 @@ let sidePanelResizeStartX = 0;
let sidePanelResizeStartWidth = 0;
const isResizingSidePanel = ref(false);
const sidePanelGuard = createSidePanelRequestGuard();
const sidePanelRef = ref<InstanceType<typeof CustomTypeInfoPanel> | null>(null);
const tableMetadataCapabilities = computed<TableMetadataCapabilities>(() => getTableMetadataCapabilities(effectiveDatabaseType.value));
const effectiveDatabaseType = computed(() => effectiveDatabaseTypeForConnection(props.connection) ?? props.connection.db_type);
const isVictoriaMetrics = computed(() => effectiveDatabaseType.value === "victoriametrics");
@@ -822,6 +826,9 @@ function executeRowAction(row: ObjectBrowserRow, action: ObjectBrowserRowAction)
case "table-info":
void openTableInfo(row);
break;
case "type-info":
void openTypeInfo(row);
break;
case "open-table":
emit("openTable", { tableName: row.name, schema: row.schema, catalog: props.catalog });
break;
@@ -833,7 +840,7 @@ function executeRowAction(row: ObjectBrowserRow, action: ObjectBrowserRowAction)
function onRowClick(row: ObjectBrowserRow, event: MouseEvent) {
const activation = settingsStore.editorSettings.sidebarActivation;
const { action, isDouble } = resolveRowClickAction(row, event.detail, activation);
const { action, isDouble } = resolveRowClickAction(row, event.detail, activation, effectiveDatabaseType.value);
// Double click: cancel any pending single-click and fire immediately
if (isDouble) {
if (singleClickTimer) {
@@ -1107,6 +1114,28 @@ function closeSidePanel() {
sidePanelGuard.bump();
}
async function openTypeInfo(row: ObjectBrowserRow) {
if (sidePanelRow.value?.id === row.id && sidePanelMode.value === "type-info") {
closeSidePanel();
return;
}
sidePanelRow.value = row;
sidePanelMode.value = "type-info";
sidePanelGuard.bump();
}
function openTypeDdl(row: ObjectBrowserRow) {
sidePanelRow.value = row;
sidePanelMode.value = "type-info";
sidePanelGuard.bump();
// DDL tab is selected by the panel once details load; switching the tab is
// deferred via a dedicated request so the panel can focus it.
nextTick(() => {
const panel = sidePanelRef.value;
panel?.selectTab("ddl");
});
}
const canOpenTableStructureEditor = computed(() => sidePanelRow.value?.type === "TABLE" && canOpenStructureEditor.value);
function openTableStructureEditor() {
@@ -2737,9 +2766,32 @@ function getPackageMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
];
}
function getTypeMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
const items: ContextMenuItem[] = [];
const capabilities = customTypeCapabilities(effectiveDatabaseType.value);
// Verified PG-family types open the read-only details panel; Xugu keeps its
// “view source” entry for TYPE/TYPE_BODY rows.
if (supportsTypeObjectSource(effectiveDatabaseType.value)) {
items.push({ label: t("contextMenu.viewSource"), action: () => openSource(item), icon: Code2 });
}
if (capabilities.details) {
items.push({ label: t("contextMenu.viewDetails"), action: () => void openTypeInfo(item), icon: Info });
if (capabilities.ddl) {
items.push({ label: t("contextMenu.viewDdl"), action: () => openTypeDdl(item), icon: FileCode });
}
}
// Only separate when an action precedes copy-name.
if (items.length > 0) {
items.push({ label: "", separator: true });
}
items.push({ label: t("contextMenu.copyName"), action: () => copyName(item), icon: Copy });
return items;
}
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 === "TYPE" || item.type === "TYPE_BODY") return getTypeMenuItems(item);
if (isSourceOnlyObjectBrowserRow(item)) return getPackageMenuItems(item);
return getProcFuncMenuItems(item);
}
@@ -3233,6 +3285,10 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
<Loader2 class="w-4 h-4 animate-spin text-muted-foreground" />
</div>
</template>
<!-- Type info mode (read-only user-defined type details) -->
<template v-else-if="sidePanelMode === 'type-info'">
<CustomTypeInfoPanel ref="sidePanelRef" :connection="props.connection" :database="props.database" :schema="sidePanelRow?.schema || selectedSchema || props.database" :name="sidePanelRow?.name || ''" :catalog="props.catalog" @close="closeSidePanel" />
</template>
<!-- Source mode (views, procedures, functions, sequences) -->
<template v-else>
<div class="flex h-8 shrink-0 items-center gap-2 border-b bg-muted/20 px-3">