feat(schema): show table properties in object browser
This commit is contained in:
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>
|
||||
|
||||
Reference in new issue
Block a user