feat(grid): add table info overview tab to the info drawer

This commit is contained in:
Allen authored and GitHub committed 2026-09-21 16:58:01 +08:00
1 parent 0f8ad280c1
commit 2c4395d412
17 files changed
+200 -6

No files matched your search

+60 -3
View File
@@ -91,10 +91,11 @@ import TemporalCellEditor from "@/components/grid/TemporalCellEditor.vue";
import EnumCellEditor from "@/components/grid/EnumCellEditor.vue";
import DataGridReadonlyTextSelection from "@/components/grid/DataGridReadonlyTextSelection.vue";
import GridSnapshotDialog from "@/components/grid/GridSnapshotDialog.vue";
import type { QueryResult, ColumnInfo, ConstraintInfo, DatabaseType, ForeignKeyInfo, IndexInfo, PgTablePartitioning, TriggerInfo, TableInfoTab, QueryResultSourceColumnRef, QueryPageJumpProgress } from "@/types/database";
import type { QueryResult, ColumnInfo, ConstraintInfo, DatabaseType, ForeignKeyInfo, IndexInfo, ObjectStatistics, PgTablePartitioning, TriggerInfo, TableInfoTab, QueryResultSourceColumnRef, QueryPageJumpProgress } from "@/types/database";
import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
import { shouldNavigateFromTableInfoColumnClick } from "@/lib/table/tableInfoColumnNavigation";
import { tableInfoTabForDrawerToggle } from "@/lib/table/tableInfoTabPreference";
import { findTableStatistics } from "@/lib/dataGrid/tableInfoOverview";
import * as api from "@/lib/backend/api";
import { formatElapsedSeconds } from "@/lib/common/elapsedTime";
import type { SqlInsertMode } from "@/lib/export/sqlInsertMode";
@@ -10346,6 +10347,13 @@ const tableOwner = ref<string | null>(null);
const tableOwnerLoading = ref(false);
const tableOwnerError = ref("");
const tableOwnerRequestGeneration = ref(0);
// Overview tab state: row/size statistics plus the table comment, loaded lazily
// when the tab is first selected.
const tableOverviewStats = ref<ObjectStatistics | null>(null);
const tableOverviewComment = ref<string | null>(null);
const tableOverviewLoading = ref(false);
const tableOverviewLoaded = ref(false);
const tableOverviewRequestGeneration = ref(0);
const canShowTableOwner = computed(() => resolvedDatabaseType.value === "postgres" && !!props.connectionId && !!props.database && !!props.tableMeta?.schema && !!props.tableMeta?.tableName);
function scrollDdlSearchMatchIntoView(match: HTMLElement) {
@@ -10455,6 +10463,7 @@ const partitionStatusResolved = ref(false);
const constraintsForTab = computed(() => constraintsForConstraintsTab(constraints.value, tableMetadataCapabilities.value.foreignKeys));
const searchQuery = ref("");
const activeTableInfoLoading = computed(() => {
if (activeTableInfoTab.value === "info") return tableOverviewLoading.value;
if (activeTableInfoTab.value === "ddl") return ddlLoading.value;
if (activeTableInfoTab.value === "columns") return tableInfoColumnsLoading.value;
if (activeTableInfoTab.value === "indexes") return indexesLoading.value;
@@ -10635,9 +10644,41 @@ const metadataLoaders = useDataGridTableMetadataLoaders({
});
const { fetchDdl, fetchTableInfoColumns, fetchTableOwner, currentIndexTableIdentity, fetchIndexes, refreshMongoIndexMetadataAfterMutation, currentForeignKeyTableIdentity, fetchForeignKeys: fetchForeignKeysMetadata, fetchTriggers, fetchConstraints, fetchPartitions } = metadataLoaders;
async function fetchTableOverview(force = false) {
const connectionId = props.connectionId;
const database = props.database;
const schema = props.tableMeta?.schema;
const tableName = props.tableMeta?.tableName;
if (!connectionId || !database || !tableName) return;
if (!force && tableOverviewLoaded.value) return;
const generation = ++tableOverviewRequestGeneration.value;
tableOverviewLoading.value = true;
try {
// Both lookups are best-effort: drivers without statistics support simply
// leave the corresponding rows hidden in the overview tab.
const [stats, comment] = await Promise.all([
api.listObjectStatistics(connectionId, database, schema ?? "").catch((error) => {
console.debug("table overview statistics unavailable", error);
return [] as ObjectStatistics[];
}),
api.getTableComment(connectionId, database, schema ?? "", tableName, props.tableMeta?.catalog).catch((error) => {
console.debug("table overview comment unavailable", error);
return null;
}),
]);
if (generation !== tableOverviewRequestGeneration.value) return;
tableOverviewStats.value = findTableStatistics(stats, tableName, schema) ?? null;
tableOverviewComment.value = comment || null;
tableOverviewLoaded.value = true;
} finally {
if (generation === tableOverviewRequestGeneration.value) tableOverviewLoading.value = false;
}
}
tableMetadataLoaderFetchForeignKeys = fetchForeignKeysMetadata;
const tableInfoTabs = computed(() => {
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 });
}
@@ -10721,7 +10762,8 @@ async function selectTableInfoTab(tab: TableInfoTab) {
if (!nextTab) return;
activeTableInfoTab.value = nextTab;
if (tabSupported) settingsStore.updateEditorSettings({ tableInfoActiveTab: tab });
if (nextTab === "ddl") await fetchDdl();
if (nextTab === "info") await fetchTableOverview();
else if (nextTab === "ddl") await fetchDdl();
else if (nextTab === "indexes") await fetchIndexes();
else if (nextTab === "foreignKeys") await fetchForeignKeys();
else if (nextTab === "constraints") await fetchConstraints();
@@ -10751,7 +10793,10 @@ async function refreshActiveTableInfo() {
if (!showTableInfo.value || !props.tableMeta) return;
if (canShowTableOwner.value) void fetchTableOwner(true);
if (activeTableInfoTab.value === "ddl") await fetchDdl(true);
if (activeTableInfoTab.value === "info") {
tableOverviewLoaded.value = false;
await fetchTableOverview();
} else if (activeTableInfoTab.value === "ddl") await fetchDdl(true);
else if (activeTableInfoTab.value === "columns") await fetchTableInfoColumns(true);
else if (activeTableInfoTab.value === "indexes") {
indexesLoaded.value = false;
@@ -10781,6 +10826,11 @@ watch(
tableOwnerLoading.value = false;
tableOwnerError.value = "";
tableOwnerRequestGeneration.value += 1;
tableOverviewStats.value = null;
tableOverviewComment.value = null;
tableOverviewLoading.value = false;
tableOverviewLoaded.value = false;
tableOverviewRequestGeneration.value += 1;
rawDdlContent.value = "";
indexes.value = [];
indexesLoaded.value = false;
@@ -13525,6 +13575,13 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
v-if="activeTableInfoTab !== 'ddl'"
:active-tab="activeTableInfoTab"
:search-query="searchQuery"
:table-name="props.tableMeta?.tableName ?? ''"
:table-schema="props.tableMeta?.schema ?? ''"
:database="props.database ?? ''"
:table-owner="tableOwner"
:overview-stats="tableOverviewStats"
:overview-comment="tableOverviewComment"
:overview-loading="tableOverviewLoading"
:columns="filteredColumns"
:columns-loading="tableInfoColumnsLoading"
:indexes="filteredIndexes"
@@ -1,14 +1,23 @@
<script setup lang="ts">
import { computed } from "vue";
import { KeyRound, Loader2, Trash2 } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import TablePartitionsPanel from "@/components/structure/TablePartitionsPanel.vue";
import { tableColumnDefaultDisplayValue } from "@/lib/table/tableColumnDefaultPresentation";
import type { ColumnInfo, ConstraintInfo, ForeignKeyInfo, IndexInfo, PgTablePartitioning, TableInfoTab, TriggerInfo } from "@/types/database";
import { formatObjectBrowserBytes, formatObjectBrowserCount } from "@/lib/table/objectBrowserRows";
import type { ColumnInfo, ConstraintInfo, ForeignKeyInfo, IndexInfo, ObjectStatistics, PgTablePartitioning, TableInfoTab, TriggerInfo } from "@/types/database";
interface DataGridTableInfoPanelsProps {
activeTab: TableInfoTab;
searchQuery: string;
tableName: string;
tableSchema: string;
database: string;
tableOwner: string | null;
overviewStats: ObjectStatistics | null;
overviewComment: string | null;
overviewLoading: boolean;
columns: ColumnInfo[];
columnsLoading: boolean;
indexes: IndexInfo[];
@@ -40,6 +49,20 @@ const emit = defineEmits<{
}>();
const { t } = useI18n();
const overviewRows = computed(() => {
const rows: { label: string; value: string; mono?: boolean }[] = [
{ label: t("common.table"), value: props.tableName },
{ label: t("common.schema"), value: props.tableSchema },
{ label: t("common.database"), value: props.database },
{ label: t("grid.tableOwner"), value: props.tableOwner ?? "", mono: true },
{ label: t("structureEditor.comment"), value: props.overviewComment ?? "" },
{ label: t("grid.tableInfoEstimatedRows"), value: formatObjectBrowserCount(props.overviewStats?.estimated_rows), mono: true },
{ label: t("grid.tableInfoTotalSize"), value: formatObjectBrowserBytes(props.overviewStats?.total_bytes), mono: true },
];
const query = props.searchQuery.trim().toLowerCase();
return rows.filter((row) => row.value && (!query || row.label.toLowerCase().includes(query) || row.value.toLowerCase().includes(query)));
});
</script>
<template>
@@ -218,6 +241,20 @@ const { t } = useI18n();
</div>
</div>
</div>
<div v-else-if="props.activeTab === 'info'" class="table-info-scroller flex-1 min-h-0 overflow-auto">
<div v-if="props.overviewLoading" class="h-full flex items-center justify-center">
<Loader2 class="w-4 h-4 animate-spin text-muted-foreground" />
</div>
<div v-else-if="props.searchQuery && overviewRows.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 overviewRows" :key="row.label" class="flex items-baseline gap-3 px-3 py-2 text-xs">
<span class="w-20 shrink-0 text-muted-foreground">{{ row.label }}</span>
<span class="min-w-0 flex-1 select-text break-words" :class="row.mono ? 'font-mono text-[11px]' : ''" :title="row.value">{{ row.value }}</span>
</div>
</div>
</div>
<TablePartitionsPanel v-else-if="props.activeTab === 'partitions'" :partitioning="props.partitioning" :loading="props.partitionsLoading" :error="props.partitionsError" :search-query="props.searchQuery" />
</template>