feat(grid): add table info overview tab to the info drawer
This commit is contained in:
1 parent
0f8ad280c1
commit
2c4395d412
17 files changed
+200
-6
No files matched your search
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user