feat(tabs): show table comments in tab tooltips

Closes #8342
This commit is contained in:
t8y2 committed 2026-09-07 14:10:16 +08:00
1 parent c0c97ef0e4
commit 5f81ed2e11
21 files changed
+75 -11

No files matched your search

+2 -1
View File
@@ -3579,7 +3579,7 @@ onUnmounted(() => {
@view-table-ddl="(_tabId: string, target: SqlObjectNavigationTarget) => onViewTableDdl(target)"
@open-object-source="(_tabId: string, target: SqlObjectNavigationTarget, initialEditing: boolean) => onOpenObjectSource(target, initialEditing)"
@open-object-table="
(tabId: string, target: { tableName: string; schema?: string; tableType?: string; catalog?: string }) => {
(tabId: string, target: { tableName: string; schema?: string; tableType?: string; catalog?: string; comment?: string | null }) => {
const tab = queryStore.tabs.find((candidate) => candidate.id === tabId) ?? activeTab;
if (!tab) return;
openObjectBrowserTableTarget({
@@ -3589,6 +3589,7 @@ onUnmounted(() => {
catalog: target.catalog,
tableName: target.tableName,
tableType: target.tableType,
comment: target.comment,
});
}
"
@@ -1300,7 +1300,7 @@ watch([() => props.specialPageTabs?.settingsActive, () => props.specialPageTabs?
</TooltipTrigger>
<TooltipContent :side="tabTooltipSide" class="text-xs grid grid-cols-[auto_1fr] gap-x-2">
<template v-for="line in tabTooltipLines(entry.tab, t)" :key="line.label">
<span class="text-muted-foreground">{{ line.label }}</span>
<span class="font-medium opacity-70">{{ line.label }}</span>
<span>{{ line.value }}</span>
</template>
</TooltipContent>
@@ -85,7 +85,7 @@ export interface ContentAreaSurfaceEmits {
viewTableDdl: [tabId: string, target: SqlObjectNavigationTarget];
editTableStructure: [tabId: string, target: SqlObjectNavigationTarget];
openObjectSource: [tabId: string, target: SqlObjectNavigationTarget, initialEditing: boolean];
openObjectTable: [tabId: string, target: { tableName: string; schema?: string; tableType?: string; catalog?: string }];
openObjectTable: [tabId: string, target: { tableName: string; schema?: string; tableType?: string; catalog?: string; comment?: string | null }];
objectSchemaChange: [tabId: string, schema: string | undefined];
objectBrowserViewportChange: [tabId: string, viewport: ObjectBrowserViewport];
objectBrowserSearchChange: [tabId: string, query: string];
@@ -182,7 +182,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
openTable: [target: { tableName: string; schema?: string; tableType?: string; catalog?: string }];
openTable: [target: { tableName: string; schema?: string; tableType?: string; catalog?: string; comment?: string | null }];
schemaChange: [schema: string | undefined];
viewportChange: [viewport: ObjectBrowserViewport];
searchChange: [query: string];
@@ -966,7 +966,7 @@ function executeRowAction(row: ObjectBrowserRow, action: ObjectBrowserRowAction)
void openTypeInfo(row);
break;
case "open-table":
emit("openTable", { tableName: row.name, schema: row.schema, tableType: objectBrowserOpenTableType(row), catalog: props.catalog });
emit("openTable", { tableName: row.name, schema: row.schema, tableType: objectBrowserOpenTableType(row), catalog: props.catalog, comment: row.comment });
break;
case "open-source":
void (row.type === "EVENT" ? openEventEditor(row) : openSource(row));
@@ -1736,7 +1736,7 @@ function objectBrowserOpenTableType(row: ObjectBrowserRow): string {
}
function openViewData(row: ObjectBrowserRow) {
emit("openTable", { tableName: row.name, schema: row.schema, tableType: objectBrowserOpenTableType(row), catalog: props.catalog });
emit("openTable", { tableName: row.name, schema: row.schema, tableType: objectBrowserOpenTableType(row), catalog: props.catalog, comment: row.comment });
}
function openStructureEditor(row: ObjectBrowserRow) {
@@ -202,16 +202,24 @@ describe("useSidebarDataOpenRuntime", () => {
it("reuses a sidebar tab for the same table in same-table mode", async () => {
mocks.openDataTabsNextToActive = true;
await useSidebarDataOpenRuntime().openData(tableNode);
await useSidebarDataOpenRuntime().openData(tableNode);
await useSidebarDataOpenRuntime().openData({ ...tableNode, comment: "Old comment" });
await useSidebarDataOpenRuntime().openData({ ...tableNode, comment: "Updated comment" });
expect(mocks.tabs).toHaveLength(1);
expect(mocks.tabs[0]?.tableComment).toBe("Updated comment");
});
it("copies the existing sidebar table comment without another metadata request", async () => {
await useSidebarDataOpenRuntime().openData({ ...tableNode, comment: "Application users" });
expect(mocks.tabs[0]?.tableComment).toBe("Application users");
expect(mocks.loadTableMetadata).toHaveBeenCalledTimes(1);
});
it("keeps different sidebar tables independent in same-table mode", async () => {
const ordersNode = { ...tableNode, id: "table-orders", label: "orders" };
await useSidebarDataOpenRuntime().openData(tableNode);
await useSidebarDataOpenRuntime().openData({ ...tableNode, comment: "User table" });
await useSidebarDataOpenRuntime().openData(ordersNode);
expect(mocks.tabs).toHaveLength(2);
@@ -270,6 +278,7 @@ describe("useSidebarDataOpenRuntime", () => {
expect(mocks.tabs).toHaveLength(1);
expect(mocks.tabs[0]?.title).toBe("orders");
expect(mocks.tabs[0]?.tableComment).toBeUndefined();
expect(mocks.tabs[0]?.tableMeta?.tableName).toBe("orders");
expect(mocks.tabs[0]?.resultLocalSortOriginalLargeValueCells).toBeUndefined();
});
@@ -22,6 +22,7 @@ export type NavigationTarget = {
schema?: string;
tableName: string;
tableType?: string;
comment?: string | null;
columnName?: string;
whereInput?: string;
};
@@ -72,7 +73,10 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
}
const tabId = queryStore.createTab(target.connectionId, target.database, tabTitle, "data", tableSchema, undefined, undefined, { forceNew: true });
const targetTab = queryStore.tabs.find((tab) => tab.id === tabId);
if (targetTab) targetTab.tableInfoTab = options.tableInfoTab;
if (targetTab) {
targetTab.tableInfoTab = options.tableInfoTab;
targetTab.tableComment = target.comment;
}
// Stamp the new table identity synchronously so SQL rebuilds (refresh,
// filters, row count) never read a stale tableMeta from a reused tab or
// fall back to parsing the schema-qualified tab title (issue #3613).
@@ -339,6 +343,7 @@ export function useNavigationTargets(dialogs: { showFieldLineageDialog: { value:
schema: target.schema,
catalog: target.catalog,
tableType: target.tableType,
comment: target.comment,
},
undefined,
"default",
@@ -173,6 +173,7 @@ export function useSidebarDataOpenRuntime() {
};
if (existingSameTableTab && (existingSameTableTab.isExecuting || canActivateExistingDataTableTab(existingSameTableTab, { activateExecuting: false }))) {
if (node.comment !== undefined) existingSameTableTab.tableComment = node.comment;
queryStore.switchTab(existingSameTableTab.id);
logPhase("existing-tab-activated", { table: node.label });
// 代次失配视同冷缓存(即使位于 30s TTL 窗口内也要重建):disconnect /
@@ -192,12 +193,16 @@ export function useSidebarDataOpenRuntime() {
if (existingDataTabCandidate) {
queryStore.switchTab(existingDataTabCandidate.tab.id);
resetReusedDataTabState(existingDataTabCandidate.tab);
existingDataTabCandidate.tab.tableComment = node.comment;
return existingDataTabCandidate.tab.id;
}
return queryStore.createTab(node.connectionId, node.database, node.label, "data", tableSchema, undefined, node.catalog, {
const createdTabId = queryStore.createTab(node.connectionId, node.database, node.label, "data", tableSchema, undefined, node.catalog, {
forceNew: true,
insertAfterActive: settingsStore.editorSettings.openDataTabsNextToActive,
});
const createdTab = queryStore.tabs.find((tab) => tab.id === createdTabId);
if (createdTab) createdTab.tableComment = node.comment;
return createdTabId;
})();
openDataLog("info", "tab-created", { traceId, tabId, elapsed: elapsed() });
logPhase("tab-created", { tabId });
+1
View File
@@ -1456,6 +1456,7 @@ export default {
tooltipGroup: "Group:",
tooltipDatabase: "Database:",
tooltipTable: "Table:",
tooltipTableComment: "Table Comment:",
tooltipCollection: "Collection:",
tooltipSchema: "Schema:",
resultN: "Result {n}",
+1
View File
@@ -1433,6 +1433,7 @@ export default withEnglishFallback({
tooltipGroup: "Grupo:",
tooltipDatabase: "Base de datos:",
tooltipTable: "Tabla:",
tooltipTableComment: "Comentario de tabla:",
tooltipCollection: "Colección:",
tooltipSchema: "Esquema:",
resultN: "Resultado {n}",
+1
View File
@@ -1431,6 +1431,7 @@ export default withEnglishFallback({
tooltipGroup: "Gruppo:",
tooltipDatabase: "Database:",
tooltipTable: "Tabella:",
tooltipTableComment: "Commento tabella:",
tooltipCollection: "Collezione:",
tooltipSchema: "Schema:",
resultN: "Risultato {n}",
+1
View File
@@ -1450,6 +1450,7 @@ export default withEnglishFallback({
tooltipGroup: "グループ:",
tooltipDatabase: "データベース:",
tooltipTable: "テーブル:",
tooltipTableComment: "テーブルコメント:",
tooltipCollection: "コレクション:",
tooltipSchema: "スキーマ:",
resultN: "結果 {n}",
+1
View File
@@ -1337,6 +1337,7 @@ export default withEnglishFallback({
tooltipGroup: "그룹:",
tooltipDatabase: "데이터베이스:",
tooltipTable: "테이블:",
tooltipTableComment: "테이블 주석:",
tooltipCollection: "컬렉션:",
tooltipSchema: "스키마:",
resultN: "결과 {n}",
+1
View File
@@ -1433,6 +1433,7 @@ export default withEnglishFallback({
tooltipGroup: "Grupo:",
tooltipDatabase: "Banco de dados:",
tooltipTable: "Tabela:",
tooltipTableComment: "Comentário da tabela:",
tooltipCollection: "Coleção:",
tooltipSchema: "Schema:",
resultN: "Resultado {n}",
+1
View File
@@ -1456,6 +1456,7 @@ export default withEnglishFallback({
tooltipGroup: "Grup:",
tooltipDatabase: "Veritabanı:",
tooltipTable: "Tablo:",
tooltipTableComment: "Tablo açıklaması:",
tooltipCollection: "Koleksiyon:",
tooltipSchema: "Şema:",
resultN: "Sonuç {n}",
+1
View File
@@ -1380,6 +1380,7 @@ export default withEnglishFallback({
tooltipGroup: "分组:",
tooltipDatabase: "数据库:",
tooltipTable: "表:",
tooltipTableComment: "表注释:",
tooltipCollection: "集合:",
tooltipSchema: "Schema:",
resultN: "结果 {n}",
+1
View File
@@ -1432,6 +1432,7 @@ export default withEnglishFallback({
tooltipGroup: "群組:",
tooltipDatabase: "資料庫:",
tooltipTable: "資料表:",
tooltipTableComment: "資料表註解:",
tooltipCollection: "集合:",
tooltipSchema: "Schema:",
resultN: "結果 {n}",
@@ -27,6 +27,8 @@ const translations: Record<string, string> = {
"tabs.tooltipConnection": "Connection:",
"tabs.tooltipGroup": "Group:",
"tabs.tooltipDatabase": "Database:",
"tabs.tooltipTable": "Table:",
"tabs.tooltipTableComment": "Table Comment:",
"connectionGroup.ungroupedLabel": "Ungrouped",
"editor.noDatabase": "No database",
};
@@ -209,6 +211,22 @@ describe("tab group presentation", () => {
expect(connectionGroupDisplayName("conn-1", translate)).toBe("Ungrouped");
});
it("shows a bounded table comment only when it is non-empty", () => {
const lines = tabTooltipLines(
queryTab({
mode: "data",
tableComment: ` ${"表".repeat(55)}\narchive `,
tableMeta: { schema: "public", tableName: "users", columns: [], primaryKeys: [] },
}),
translate,
);
const comment = lines.find((line) => line.label === "Table Comment:")?.value;
expect(Array.from(comment || "")).toHaveLength(50);
expect(comment?.endsWith("…")).toBe(true);
expect(tabTooltipLines(queryTab({ mode: "data", tableComment: " ", tableMeta: { schema: "public", tableName: "users", columns: [], primaryKeys: [] } }), translate).some((line) => line.label === "Table Comment:")).toBe(false);
});
});
describe("query result source ranges", () => {
@@ -53,6 +53,7 @@ export interface SavedOpenTab {
objectBrowser?: QueryTab["objectBrowser"];
objectSource?: QueryTab["objectSource"];
sourceView?: boolean;
tableComment?: QueryTab["tableComment"];
tableMeta?: QueryTab["tableMeta"];
mongoEditTarget?: QueryTab["mongoEditTarget"];
resultEvicted?: boolean;
@@ -148,6 +149,7 @@ export function serializeOpenTabs(tabs: QueryTab[]): SavedOpenTab[] {
objectBrowser: tab.objectBrowser,
objectSource: tab.objectSource,
...(tab.sourceView ? { sourceView: true } : {}),
...(tab.tableComment !== undefined ? { tableComment: tab.tableComment } : {}),
tableMeta: tab.tableMeta,
...(tab.mongoEditTarget !== undefined ? { mongoEditTarget: tab.mongoEditTarget } : {}),
...(tab.mode !== "data" && tab.resultEvicted ? { resultEvicted: true } : {}),
@@ -11,6 +11,14 @@ import type { BatchSqlExecution, ConnectionConfig, DatabaseType, QueryResult, Qu
type Translate = (key: string, params?: Record<string, unknown>) => string;
export type OutputView = "result" | "summary" | "explain" | "chart";
const TABLE_COMMENT_TOOLTIP_MAX_LENGTH = 50;
function tableCommentTooltipValue(comment: string | null | undefined): string | undefined {
const normalized = comment?.trim().replace(/\s+/g, " ");
if (!normalized) return undefined;
const characters = Array.from(normalized);
return characters.length <= TABLE_COMMENT_TOOLTIP_MAX_LENGTH ? normalized : `${characters.slice(0, TABLE_COMMENT_TOOLTIP_MAX_LENGTH - 1).join("")}…`;
}
export function connectionDisplayName(connectionId: string): string {
const connectionStore = useConnectionStore();
@@ -170,6 +178,10 @@ export function tabTooltipLines(tab: QueryTab, t: Translate): { label: string; v
}
if (tab.mode === "data" && tab.tableMeta?.tableName) {
lines.push({ label: t("tabs.tooltipTable"), value: tab.tableMeta.tableName });
const comment = tableCommentTooltipValue(tab.tableComment);
if (comment) {
lines.push({ label: t("tabs.tooltipTableComment"), value: comment });
}
}
if (tab.mode === "mongo" && tab.sql) {
lines.push({ label: t("tabs.tooltipCollection"), value: tab.sql });
+1
View File
@@ -1304,6 +1304,7 @@ export interface QueryTab {
objectType: ObjectSourceKind;
signature?: string;
};
tableComment?: string | null;
tableMeta?: {
schema?: string;
tableName: string;