feat(schema): show table properties in object browser

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-23 14:43:33 +08:00
1 parent 12ed7a49b3
commit 13d2db6191
22 files changed
+284 -12

No files matched your search

@@ -78,6 +78,7 @@ import { isSchemaAware, supportsTableVacuum, supportsTransfer } from "@/lib/data
import { supportsAiAssistantContext, supportsSchemaDiagram, supportsTableImport, supportsTableStructureEditing, supportsTableTruncate } from "@/lib/database/databaseFeatureSupport";
import { codeMirrorSqlDialect, connectionObjectTreeNodeSchema, connectionTableSqlSchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, objectListSchemaForConnection, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { getTableMetadataCapabilities, type TableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilities";
import { findTableStatistics } from "@/lib/dataGrid/tableInfoOverview";
import { constraintsForConstraintsTab } from "@/lib/table/constraintPresentation";
import { buildTableSelectSql } from "@/lib/table/tableSelectSql";
import { PARTITION_TREE_INDENT_PX } from "@/lib/table/pgPartitionPresentation";
@@ -248,7 +249,11 @@ const eventEditorKey = computed(() =>
}),
);
// Table info panel state
const tableInfoTab = ref<TableInfoTab>("ddl");
const tableInfoTab = ref<TableInfoTab>("info");
const tableOverviewStats = ref<ObjectStatistics | null>(null);
const tableOverviewComment = ref<string | null>(null);
const tableOverviewLoading = ref(false);
const tableOverviewLoaded = ref(false);
const tableColumns = ref<ColumnInfo[]>([]);
const tableColumnsLoading = ref(false);
const tableColumnsLoaded = ref(false);
@@ -281,6 +286,7 @@ const tableConstraintsForTab = computed(() => constraintsForConstraintsTab(table
const tableInfoSearchQuery = ref("");
const tableInfoDdlPreRef = ref<HTMLPreElement | null>(null);
const activeTableInfoLoading = computed(() => {
if (tableInfoTab.value === "info") return tableOverviewLoading.value;
if (tableInfoTab.value === "ddl") return tableDdlLoading.value;
if (tableInfoTab.value === "columns") return tableColumnsLoading.value;
if (tableInfoTab.value === "indexes") return tableIndexesLoading.value;
@@ -1063,6 +1069,7 @@ type TableInfoTabItem = { id: TableInfoTab; label: string; icon: Component; coun
const tableInfoTabs = computed<TableInfoTabItem[]>(() => {
const tabs: TableInfoTabItem[] = [];
tabs.push({ id: "info", label: t("grid.tableInfoOverview"), icon: Info });
if (tableMetadataCapabilities.value.ddl) {
tabs.push({ id: "ddl", label: "DDL", icon: Code2 });
}
@@ -1092,6 +1099,31 @@ const tableInfoTabListStyle = computed(() => ({
}));
const filteredTableColumns = computed(() => filterObjectBrowserTableColumns(tableColumns.value, tableInfoSearchQuery.value));
const tableOverviewRows = computed(() => {
const stats = tableOverviewStats.value;
const rows = [
{ label: t("common.table"), value: sidePanelRow.value?.name ?? "" },
{ label: t("common.schema"), value: sidePanelRow.value?.schema || selectedSchema.value || props.database },
{ label: t("common.database"), value: props.database },
{ label: t("structureEditor.comment"), value: tableOverviewComment.value ?? "" },
{ label: t("grid.tableInfoEstimatedRows"), value: formatObjectBrowserCount(stats?.estimated_rows) },
{ label: t("grid.tableInfoTotalSize"), value: formatObjectBrowserBytes(stats?.total_bytes) },
{ label: t("grid.tableInfoDataLength"), value: formatObjectBrowserBytes(stats?.data_length) },
{ label: t("grid.tableInfoEngine"), value: stats?.engine ?? "" },
{ label: t("grid.tableInfoCreatedAt"), value: stats?.created_at ?? "" },
{ label: t("grid.tableInfoUpdatedAt"), value: stats?.updated_at ?? "" },
{ label: t("grid.tableInfoCollation"), value: stats?.collation ?? "" },
{ label: t("grid.tableInfoRowFormat"), value: stats?.row_format ?? "" },
{ label: t("grid.tableInfoAvgRowLength"), value: formatObjectBrowserBytes(stats?.avg_row_length) },
{ label: t("grid.tableInfoMaxDataLength"), value: formatObjectBrowserBytes(stats?.max_data_length) },
{ label: t("grid.tableInfoCheckTime"), value: stats?.check_time ?? "" },
{ label: t("grid.tableInfoIndexLength"), value: formatObjectBrowserBytes(stats?.index_length) },
{ label: t("grid.tableInfoAutoIncrement"), value: stats?.auto_increment ?? "" },
{ label: t("grid.tableInfoDataFree"), value: formatObjectBrowserBytes(stats?.data_free) },
];
const query = tableInfoSearchQuery.value.trim().toLowerCase();
return rows.filter((row) => row.value && (!query || row.label.toLowerCase().includes(query) || row.value.toLowerCase().includes(query)));
});
const filteredTableIndexes = computed(() => {
if (!tableInfoSearchQuery.value) return tableIndexes.value;
@@ -1140,6 +1172,9 @@ async function openTableInfo(row: ObjectBrowserRow, initialTab?: TableInfoTab) {
sidePanelGuard.bump();
// Reset state
tableColumns.value = [];
tableOverviewStats.value = null;
tableOverviewComment.value = null;
tableOverviewLoaded.value = false;
rawTableDdlContent.value = "";
tableIndexes.value = [];
tableForeignKeys.value = [];
@@ -1173,7 +1208,8 @@ async function selectTableInfoTab(tab: TableInfoTab) {
if (!nextTab) return;
tableInfoTab.value = nextTab;
tableInfoSearchQuery.value = "";
if (nextTab === "ddl") await fetchTableDdl();
if (nextTab === "info") await fetchTableOverview();
else if (nextTab === "ddl") await fetchTableDdl();
else if (nextTab === "columns") await fetchTableColumns();
else if (nextTab === "indexes") await fetchTableIndexes();
else if (nextTab === "foreignKeys") await fetchTableForeignKeys();
@@ -1182,6 +1218,23 @@ async function selectTableInfoTab(tab: TableInfoTab) {
else if (nextTab === "partitions") await fetchTablePartitions();
}
async function fetchTableOverview(force = false) {
const row = sidePanelRow.value;
if (!row || (!force && tableOverviewLoaded.value)) return;
const epoch = sidePanelGuard.capture();
const schema = row.schema || selectedSchema.value || props.database;
tableOverviewLoading.value = true;
try {
const [statistics, comment] = await Promise.all([api.listObjectStatistics(props.connection.id, props.database, schema).catch(() => [] as ObjectStatistics[]), api.getTableComment(props.connection.id, props.database, schema, row.name, props.catalog).catch(() => null)]);
if (sidePanelGuard.isStale(epoch)) return;
tableOverviewStats.value = findTableStatistics(statistics, row.name, schema) ?? null;
tableOverviewComment.value = comment;
tableOverviewLoaded.value = true;
} finally {
if (sidePanelGuard.isFresh(epoch)) tableOverviewLoading.value = false;
}
}
function tableMetadataRequest(row: ObjectBrowserRow): ObjectDdlRequest {
return {
connectionId: props.connection.id,
@@ -1392,7 +1445,12 @@ async function refreshActiveTableInfo() {
if (sidePanelMode.value !== "table-info" || !sidePanelRow.value) return;
sidePanelGuard.bump();
if (tableInfoTab.value === "ddl") {
if (tableInfoTab.value === "info") {
tableOverviewStats.value = null;
tableOverviewComment.value = null;
tableOverviewLoaded.value = false;
await fetchTableOverview(true);
} else if (tableInfoTab.value === "ddl") {
rawTableDdlContent.value = "";
tableDdlLoaded.value = false;
await fetchTableDdl(true);
@@ -3893,7 +3951,17 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
<div v-if="tableInfoTab === 'ddl' && effectiveDatabaseType === 'oceanbase-oracle'" class="border-b px-3 py-2">
<DdlStorageToggle :database-type="effectiveDatabaseType" :disabled="tableDdlLoading" />
</div>
<div v-if="tableInfoTab === 'columns'" class="flex-1 min-h-0 overflow-auto">
<div v-if="tableInfoTab === 'info'" class="flex-1 min-h-0 overflow-auto">
<div v-if="tableOverviewLoading" class="h-full flex items-center justify-center"><Loader2 class="w-4 h-4 animate-spin text-muted-foreground" /></div>
<div v-else-if="tableInfoSearchQuery && tableOverviewRows.length === 0" class="p-6 text-center text-xs text-muted-foreground">{{ t("grid.tableInfoNoResults") }}</div>
<div v-else class="divide-y">
<div v-for="row in tableOverviewRows" :key="row.label" class="flex items-baseline gap-3 px-3 py-2 text-xs">
<span class="w-24 shrink-0 text-muted-foreground">{{ row.label }}</span>
<span class="min-w-0 flex-1 select-text break-words font-mono text-[11px]" :title="row.value">{{ row.value }}</span>
</div>
</div>
</div>
<div v-else-if="tableInfoTab === 'columns'" class="flex-1 min-h-0 overflow-auto">
<div v-if="tableColumnsLoading" class="h-full flex items-center justify-center">
<Loader2 class="w-4 h-4 animate-spin text-muted-foreground" />
</div>