From e456925c23fcc95704ca8843f621eaf36408d8b5 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Sat, 27 Jun 2026 17:43:11 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(sidebar):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=A1=A8=E5=A4=87=E6=B3=A8=E6=82=AC=E6=B5=AE=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 读取不同数据源表备注并写入左侧表节点 - 支持表分组菜单切换备注显示 - 表节点悬浮复用 Tab 信息卡并移除原生双提示 Fixes #569 --- .../Sidebar.locate-toolbar.test.tsx | 83 ++++++++ frontend/src/components/Sidebar.tsx | 7 + .../src/components/V2TableContextMenu.tsx | 14 ++ .../components/sidebar/SidebarTreeTitle.tsx | 192 ++++++++++++++---- .../sidebar/sidebarMetadataLoaders.ts | 15 +- .../sidebar/useSidebarTitleRender.tsx | 4 + .../sidebar/useSidebarTreeLoaders.tsx | 34 +++- .../sidebar/useSidebarV2ActionHandlers.tsx | 8 + .../sidebar/useSidebarV2ContextMenu.tsx | 3 + frontend/src/store.ts | 9 +- frontend/src/v2-theme.css | 94 +++++++++ shared/i18n/de-DE.json | 3 + shared/i18n/en-US.json | 3 + shared/i18n/ja-JP.json | 3 + shared/i18n/ru-RU.json | 3 + shared/i18n/zh-CN.json | 3 + shared/i18n/zh-TW.json | 3 + 17 files changed, 430 insertions(+), 51 deletions(-) diff --git a/frontend/src/components/Sidebar.locate-toolbar.test.tsx b/frontend/src/components/Sidebar.locate-toolbar.test.tsx index 90d987a..2c2ddd1 100644 --- a/frontend/src/components/Sidebar.locate-toolbar.test.tsx +++ b/frontend/src/components/Sidebar.locate-toolbar.test.tsx @@ -52,6 +52,8 @@ import { buildSidebarRootTagToken, buildSidebarTablePinKey, } from '../store'; +import { renderSidebarV2TreeTitle } from './sidebar/SidebarTreeTitle'; +import { buildSidebarTableStatusSQL } from './sidebar/sidebarMetadataLoaders'; import { DEFAULT_SHORTCUT_OPTIONS, cloneShortcutOptions, @@ -200,6 +202,8 @@ vi.mock('../store', () => ({ recordTableAccess: mocks.noop, setTableSortPreference: mocks.noop, setSidebarTablePinned: mocks.noop, + queryOptions: { showSidebarTableComment: false }, + setQueryOptions: mocks.noop, addSqlLog: mocks.noop, sqlLogs: [], shortcutOptions: mocks.state.shortcutOptions ?? cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS), @@ -2820,6 +2824,7 @@ describe('Sidebar locate toolbar', () => { dbName="mkefu_ai_dev" count={15} currentSort="frequency" + showTableComments />, ); @@ -2831,6 +2836,8 @@ describe('Sidebar locate toolbar', () => { sort: t('sidebar.v2_table_group_menu.sort_frequency'), })); expect(markup).toContain(t('sidebar.menu.create_table')); + expect(markup).toContain(t('sidebar.v2_table_group_menu.display_section')); + expect(markup).toContain(t('sidebar.v2_table_group_menu.show_table_comments')); expect(markup).toContain(t('data_grid.context_menu.sort_section')); expect(markup).toContain(t('sidebar.menu.sort_by_name')); expect(markup).toContain(t('sidebar.menu.sort_by_frequency')); @@ -2846,6 +2853,7 @@ describe('Sidebar locate toolbar', () => { expect(end).toBeGreaterThan(start); const tableGroupCallSource = sidebarSource.slice(start, end); expect(tableGroupCallSource).toContain(' { expect(tableGroupCallSource).not.toContain(rawSnippet); @@ -2900,6 +2908,81 @@ describe('Sidebar locate toolbar', () => { }); }); + it('renders sidebar table comments as an opt-in suffix while using the tab-style table hover card', () => { + const baseNode = { + type: 'table', + title: 'users', + key: 'conn-main-users', + dataRef: { + id: 'conn', + dbName: 'main', + tableName: 'users', + tableComment: '用户表', + }, + }; + const baseOptions = { + node: baseNode, + hoverTitle: 'users', + statusBadge: null, + getV2TreeMetaText: () => '', + toggleSidebarTablePinned: vi.fn(), + snapshotTreeSelectionBeforeDrag: vi.fn(), + restoreTreeSelectionAfterDrag: vi.fn(), + treeDragSelectSuppressUntilRef: { current: 0 }, + setIsTreeDragging: vi.fn(), + }; + + const hiddenSuffixMarkup = renderToStaticMarkup(renderSidebarV2TreeTitle({ + ...baseOptions, + showSidebarTableComment: false, + })); + expect(hiddenSuffixMarkup).not.toContain('gn-v2-tree-table-comment'); + + const visibleSuffixMarkup = renderToStaticMarkup(renderSidebarV2TreeTitle({ + ...baseOptions, + showSidebarTableComment: true, + })); + expect(visibleSuffixMarkup).toContain('gn-v2-tree-table-comment'); + expect(visibleSuffixMarkup).toContain('用户表'); + + const treeTitleSource = readSourceFile('./sidebar/SidebarTreeTitle.tsx'); + expect(treeTitleSource).toContain('data-sidebar-table-hover-info="true"'); + expect(treeTitleSource).toContain('rootClassName="gn-v2-tab-hover-tooltip gn-v2-sidebar-table-hover-tooltip"'); + expect(treeTitleSource).toContain('title={tableHoverInfo ? undefined : effectiveHoverTitle}'); + expect(treeTitleSource).toContain("const SIDEBAR_TREE_NODE_CONTENT_SELECTOR = '.ant-tree-node-content-wrapper';"); + expect(treeTitleSource).toContain("removeAttribute('title')"); + expect(treeTitleSource).toContain('ref={tableHoverInfo ? clearSidebarTableNativeHoverTitleRef : undefined}'); + expect(treeTitleSource).toContain('onPointerOverCapture={tableHoverInfo ? clearSidebarTableNativeHoverTitle : undefined}'); + expect(treeTitleSource).toContain("resolveConnectionHostSummary(dataRef.config)"); + expect(treeTitleSource).toContain("t('tab_manager.kind_badge.table')"); + expect(treeTitleSource).toContain("t('tab_manager.hover.kind.table')"); + expect(treeTitleSource).toContain("t('table_designer.action.table_comment')"); + expect(treeTitleSource).toContain('mouseEnterDelay={1.2}'); + + const css = readV2ThemeCss(); + expect(css).toMatch(/\.gn-v2-tree-table-comment \{[^}]*max-width: 24em;[^}]*text-overflow: ellipsis;/s); + expect(css).toMatch(/\.gn-v2-tab-hover-tooltip \.ant-tooltip-inner \{[^}]*min-width: 260px;[^}]*padding: 0;/s); + expect(css).toMatch(/\.gn-v2-tab-hover-card \{[^}]*cursor: text;[^}]*user-select: text;/s); + expect(css).toContain('--gn-v2-tab-hover-grid-columns: 56px minmax(0, 1fr);'); + expect(css).toMatch(/\.gn-v2-tab-hover-row \{[^}]*grid-template-columns: var\(--gn-v2-tab-hover-grid-columns\);/s); + }); + + it('loads table comments through the sidebar table status metadata query', () => { + const mysqlSql = buildSidebarTableStatusSQL({ config: { type: 'mysql' } } as any, 'app'); + const pgSql = buildSidebarTableStatusSQL({ config: { type: 'postgres' } } as any, 'app'); + const sqlServerSql = buildSidebarTableStatusSQL({ config: { type: 'sqlserver' } } as any, 'app'); + const oracleSql = buildSidebarTableStatusSQL({ config: { type: 'oracle' } } as any, 'APP'); + + expect(mysqlSql).toContain('TABLE_COMMENT AS table_comment'); + expect(pgSql).toContain("obj_description(c.oid, 'pg_class') AS table_comment"); + expect(sqlServerSql).toContain('ep.value AS table_comment'); + expect(oracleSql).toContain('comments AS table_comment'); + + const loaderSource = readSourceFile('./sidebar/useSidebarTreeLoaders.tsx'); + expect(loaderSource).toContain('tableCommentMap'); + expect(loaderSource).toContain('tableComment: entry.tableComment'); + }); + it('listens for table overview pin changes to refresh the matching sidebar database node', () => { const source = readSidebarSource(); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 2591a8f..2af531a 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -418,6 +418,8 @@ const Sidebar: React.FC<{ const recordTableAccess = useStore(state => state.recordTableAccess); const setTableSortPreference = useStore(state => state.setTableSortPreference); const setSidebarTablePinned = useStore(state => state.setSidebarTablePinned); + const queryOptions = useStore(state => state.queryOptions); + const setQueryOptions = useStore(state => state.setQueryOptions); const addSqlLog = useStore(state => state.addSqlLog); const sqlLogs = useStore(state => state.sqlLogs) || []; const shortcutOptions = useStore(state => state.shortcutOptions); @@ -429,6 +431,7 @@ const Sidebar: React.FC<{ const darkMode = theme === 'dark'; const resolvedAppearance = resolveAppearanceValues(appearance); const opacity = normalizeOpacityForPlatform(resolvedAppearance.opacity); + const showSidebarTableComment = queryOptions?.showSidebarTableComment === true; const { exportProgressModal, runExportWithProgress } = useExportProgressDialog(); const disableLocalBackdropFilter = isMacLikePlatform(); const autoFetchVisible = useAutoFetchVisibility(); @@ -2035,6 +2038,8 @@ const Sidebar: React.FC<{ moveConnectionToTag, setSidebarTablePinned, setTableSortPreference, + setQueryOptions, + showSidebarTableComment, replaceTreeNodeChildren, loadDatabases, loadTables, @@ -2162,6 +2167,7 @@ const Sidebar: React.FC<{ v2TreeMetrics, tableSortPreference, pinnedSidebarTables, + showSidebarTableComment, getConnectionNodeForAction, buildRuntimeConfig, extractObjectName, @@ -2186,6 +2192,7 @@ const Sidebar: React.FC<{ hoverTitle, statusBadge, getV2TreeMetaText, + showSidebarTableComment, toggleSidebarTablePinned, snapshotTreeSelectionBeforeDrag, restoreTreeSelectionAfterDrag, diff --git a/frontend/src/components/V2TableContextMenu.tsx b/frontend/src/components/V2TableContextMenu.tsx index 66f6eb3..d5b71ee 100644 --- a/frontend/src/components/V2TableContextMenu.tsx +++ b/frontend/src/components/V2TableContextMenu.tsx @@ -264,6 +264,7 @@ export const V2TableContextMenuView: React.FC<{ export type V2TableGroupContextMenuActionKey = | 'new-table' + | 'toggle-table-comments' | 'sort-by-name' | 'sort-by-frequency'; @@ -273,6 +274,7 @@ export const V2TableGroupContextMenuView: React.FC<{ dbName?: string; count?: number; currentSort?: 'name' | 'frequency'; + showTableComments?: boolean; onAction?: (action: V2TableGroupContextMenuActionKey) => void; }> = ({ title, @@ -280,6 +282,7 @@ export const V2TableGroupContextMenuView: React.FC<{ dbName, count, currentSort = 'name', + showTableComments = false, onAction, }) => { const sortLabel = currentSort === 'frequency' @@ -310,6 +313,17 @@ export const V2TableGroupContextMenuView: React.FC<{ { action: 'new-table', icon: , title: t('sidebar.menu.create_table'), kbd: primaryShortcut('N', shortcutPlatform), featured: true }, ])} +
{t('sidebar.v2_table_group_menu.display_section')}
+ {renderItems([ + { + action: 'toggle-table-comments', + icon: showTableComments ? : , + title: t('sidebar.v2_table_group_menu.show_table_comments'), + kbd: showTableComments ? t('data_grid.context_menu.current_marker') : undefined, + selected: showTableComments, + }, + ])} +
{t('data_grid.context_menu.sort_section')}
{renderItems([ { action: 'sort-by-name', icon: currentSort === 'name' ? : , title: t('sidebar.menu.sort_by_name'), kbd: currentSort === 'name' ? t('data_grid.context_menu.current_marker') : undefined, selected: currentSort === 'name' }, diff --git a/frontend/src/components/sidebar/SidebarTreeTitle.tsx b/frontend/src/components/sidebar/SidebarTreeTitle.tsx index 516d4f3..061118c 100644 --- a/frontend/src/components/sidebar/SidebarTreeTitle.tsx +++ b/frontend/src/components/sidebar/SidebarTreeTitle.tsx @@ -1,8 +1,10 @@ import React from 'react'; +import { Tooltip } from 'antd'; import { StarFilled, StarOutlined } from '@ant-design/icons'; import { t } from '../../i18n'; import { SIDEBAR_SQL_EDITOR_DRAG_MIME, encodeSidebarSqlEditorDragPayload } from '../../utils/sidebarSqlDrag'; import { sanitizeRedisDbAlias } from '../../utils/redisDbAlias'; +import { resolveConnectionHostSummary } from '../../utils/tabDisplay'; import { resolveSidebarObjectDragText } from '../sidebarCoreUtils'; import { resolveV2ObjectGroupTitle } from './sidebarHelpers'; @@ -11,6 +13,7 @@ type SidebarV2TreeTitleOptions = { hoverTitle: string; statusBadge: React.ReactNode; getV2TreeMetaText: (node: any) => string; + showSidebarTableComment: boolean; toggleSidebarTablePinned: (node: any) => void; snapshotTreeSelectionBeforeDrag: () => void; restoreTreeSelectionAfterDrag: () => void; @@ -18,11 +21,86 @@ type SidebarV2TreeTitleOptions = { setIsTreeDragging: (dragging: boolean) => void; }; +const SIDEBAR_TREE_NODE_CONTENT_SELECTOR = '.ant-tree-node-content-wrapper'; + +const stopSidebarTableHoverPropagation = (event: React.SyntheticEvent) => { + event.stopPropagation(); +}; + +const clearSidebarTableNativeHoverTitleElement = (element: HTMLElement | null) => { + element?.closest(SIDEBAR_TREE_NODE_CONTENT_SELECTOR)?.removeAttribute('title'); +}; + +const clearSidebarTableNativeHoverTitleRef: React.RefCallback = (element) => { + clearSidebarTableNativeHoverTitleElement(element); +}; + +const clearSidebarTableNativeHoverTitle = (event: React.SyntheticEvent) => { + clearSidebarTableNativeHoverTitleElement(event.currentTarget); +}; + +const renderSidebarTableHoverInfo = ( + node: any, + displayTitle: string, + tableComment: string, +): React.ReactNode => { + const dataRef = node?.dataRef || {}; + const tableName = String(dataRef.tableName || displayTitle || node?.title || '').trim(); + const schemaName = String(dataRef.schemaName || '').trim(); + const dbName = String(dataRef.dbName || dataRef?.config?.database || '').trim(); + const connectionLabel = String(dataRef.name || '').trim(); + const hostSummary = resolveConnectionHostSummary(dataRef.config); + const rows = [ + [t('tab_manager.hover.label.type'), t('tab_manager.hover.kind.table')], + [t('tab_manager.hover.label.connection'), connectionLabel || t('tab_manager.hover.fallback.unbound_connection')], + ['Host', hostSummary || t('tab_manager.hover.fallback.host_not_configured')], + [t('tab_manager.hover.label.database'), dbName || t('tab_manager.hover.fallback.database_not_specified')], + ['Schema', schemaName], + [t('tab_manager.hover.label.object'), tableName], + [t('table_designer.action.table_comment'), tableComment], + ].filter(([, value]) => Boolean(value)); + + return ( +
+
+ {t('tab_manager.kind_badge.table')} + {tableName || displayTitle} +
+
+ {rows.map(([label, value], index) => ( +
+ {label} + {value} +
+ ))} +
+
+ ); +}; + export const renderSidebarV2TreeTitle = ({ node, hoverTitle, statusBadge, getV2TreeMetaText, + showSidebarTableComment, toggleSidebarTablePinned, snapshotTreeSelectionBeforeDrag, restoreTreeSelectionAfterDrag, @@ -52,6 +130,14 @@ export const renderSidebarV2TreeTitle = ({ } return rawTitle; })(); + const tableComment = node.type === 'table' + ? String(node?.dataRef?.tableComment || '').trim() + : ''; + const tableCommentSuffix = showSidebarTableComment && tableComment ? tableComment : ''; + const effectiveHoverTitle = hoverTitle; + const tableHoverInfo = node.type === 'table' + ? renderSidebarTableHoverInfo(node, displayTitle, tableComment) + : null; const metaText = getV2TreeMetaText(node); const redisDbAlias = node.type === 'redis-db' ? sanitizeRedisDbAlias(node?.dataRef?.redisDbAlias) @@ -107,7 +193,7 @@ export const renderSidebarV2TreeTitle = ({ return ( ); } + const titleNode = ( + { + snapshotTreeSelectionBeforeDrag(); + treeDragSelectSuppressUntilRef.current = Date.now() + 600; + setIsTreeDragging(true); + event.stopPropagation(); + event.dataTransfer.effectAllowed = 'copy'; + event.dataTransfer.setData('text/plain', dragText); + event.dataTransfer.setData( + SIDEBAR_SQL_EDITOR_DRAG_MIME, + encodeSidebarSqlEditorDragPayload({ + text: dragText, + nodeType: node.type, + connectionId: String(node?.dataRef?.id || ''), + dbName: String(node?.dataRef?.dbName || ''), + }), + ); + } : undefined} + onDragEnd={dragText ? () => { + restoreTreeSelectionAfterDrag(); + setIsTreeDragging(false); + } : undefined} + > + {statusBadge} + + {redisDbAlias ? ( + <> + {redisDbBaseTitle} + {redisDbAlias} + + ) : displayTitle} + + {tableCommentSuffix && ( + {tableCommentSuffix} + )} + {metaText && {metaText}} + + ); + + const wrappedTitleNode = tableHoverInfo ? ( + + {titleNode} + + ) : titleNode; + return ( <> - { - snapshotTreeSelectionBeforeDrag(); - treeDragSelectSuppressUntilRef.current = Date.now() + 600; - setIsTreeDragging(true); - event.stopPropagation(); - event.dataTransfer.effectAllowed = 'copy'; - event.dataTransfer.setData('text/plain', dragText); - event.dataTransfer.setData( - SIDEBAR_SQL_EDITOR_DRAG_MIME, - encodeSidebarSqlEditorDragPayload({ - text: dragText, - nodeType: node.type, - connectionId: String(node?.dataRef?.id || ''), - dbName: String(node?.dataRef?.dbName || ''), - }), - ); - } : undefined} - onDragEnd={dragText ? () => { - restoreTreeSelectionAfterDrag(); - setIsTreeDragging(false); - } : undefined} - > - {statusBadge} - - {redisDbAlias ? ( - <> - {redisDbBaseTitle} - {redisDbAlias} - - ) : displayTitle} - - {metaText && {metaText}} - + {wrappedTitleNode} {tablePinAction} ); diff --git a/frontend/src/components/sidebar/sidebarMetadataLoaders.ts b/frontend/src/components/sidebar/sidebarMetadataLoaders.ts index a503fe4..67d4056 100644 --- a/frontend/src/components/sidebar/sidebarMetadataLoaders.ts +++ b/frontend/src/components/sidebar/sidebarMetadataLoaders.ts @@ -262,7 +262,7 @@ const buildSidebarTableStatusSQL = ( case "mysql": case "starrocks": return [ - "SELECT TABLE_NAME AS table_name, TABLE_ROWS AS table_rows", + "SELECT TABLE_NAME AS table_name, TABLE_COMMENT AS table_comment, TABLE_ROWS AS table_rows", "FROM information_schema.tables", `WHERE table_schema = '${safeDbName}'`, "AND table_type = 'BASE TABLE'", @@ -275,7 +275,7 @@ const buildSidebarTableStatusSQL = ( case "opengauss": case "gaussdb": return [ - "SELECT n.nspname || '.' || c.relname AS table_name, c.reltuples::bigint AS table_rows", + "SELECT n.nspname || '.' || c.relname AS table_name, obj_description(c.oid, 'pg_class') AS table_comment, c.reltuples::bigint AS table_rows", "FROM pg_class c", "JOIN pg_namespace n ON n.oid = c.relnamespace", "WHERE c.relkind = 'r'", @@ -286,18 +286,19 @@ const buildSidebarTableStatusSQL = ( case "sqlserver": { const safeDb = quoteSqlServerIdentifier(dbName); return [ - "SELECT s.name + '.' + t.name AS table_name, SUM(p.rows) AS table_rows", + "SELECT s.name + '.' + t.name AS table_name, ep.value AS table_comment, SUM(p.rows) AS table_rows", `FROM ${safeDb}.sys.tables t`, `JOIN ${safeDb}.sys.schemas s ON t.schema_id = s.schema_id`, + `LEFT JOIN ${safeDb}.sys.extended_properties ep ON ep.major_id = t.object_id AND ep.minor_id = 0 AND ep.name = 'MS_Description'`, `LEFT JOIN ${safeDb}.sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)`, "WHERE t.type = 'U'", - "GROUP BY s.name, t.name", + "GROUP BY s.name, t.name, ep.value", "ORDER BY s.name, t.name", ].join("\n"); } case "clickhouse": return [ - "SELECT name AS table_name, total_rows AS table_rows", + "SELECT name AS table_name, comment AS table_comment, total_rows AS table_rows", "FROM system.tables", `WHERE database = '${safeDbName}'`, "AND engine NOT IN ('View', 'MaterializedView')", @@ -307,8 +308,8 @@ const buildSidebarTableStatusSQL = ( case "dm": { const owner = escapeSQLLiteral(dbName).toUpperCase(); return [ - "SELECT table_name, num_rows AS table_rows", - "FROM all_tables", + "SELECT table_name, comments AS table_comment, num_rows AS table_rows", + "FROM all_tab_comments JOIN all_tables USING (table_name, owner)", `WHERE owner = '${owner}'`, "ORDER BY table_name", ].join("\n"); diff --git a/frontend/src/components/sidebar/useSidebarTitleRender.tsx b/frontend/src/components/sidebar/useSidebarTitleRender.tsx index 750e6d0..ec7394f 100644 --- a/frontend/src/components/sidebar/useSidebarTitleRender.tsx +++ b/frontend/src/components/sidebar/useSidebarTitleRender.tsx @@ -62,6 +62,10 @@ export const useSidebarTitleRender = ({ hoverTitle = rawTableName; } } + const tableComment = node.type === 'table' ? String(node?.dataRef?.tableComment || '').trim() : ''; + if (tableComment) { + hoverTitle = `${hoverTitle}\n${t('sidebar.v2_table_group_menu.table_comment_tooltip', { comment: tableComment })}`; + } } else if (node.type === 'object-group') { const objectGroupTitle = resolveV2ObjectGroupTitle(node); if (objectGroupTitle) { diff --git a/frontend/src/components/sidebar/useSidebarTreeLoaders.tsx b/frontend/src/components/sidebar/useSidebarTreeLoaders.tsx index fb59876..0cef983 100644 --- a/frontend/src/components/sidebar/useSidebarTreeLoaders.tsx +++ b/frontend/src/components/sidebar/useSidebarTreeLoaders.tsx @@ -486,6 +486,16 @@ export const useSidebarTreeLoaders = ({ ? await DBQuery(buildRpcConnectionConfig(config) as any, conn.dbName, tableStatusSql).catch(() => ({ success: false, data: [] as any[] })) : { success: false, data: [] as any[] }; const tableRowCountMap = new Map(); + const tableCommentMap = new Map(); + const putTableComment = (rawTableName: string, rawComment: string) => { + const tableName = String(rawTableName || '').trim(); + const comment = String(rawComment || '').trim(); + if (!tableName || !comment) return; + const keys = new Set([tableName.toLowerCase()]); + const parsed = splitQualifiedName(tableName); + if (parsed.objectName) keys.add(parsed.objectName.toLowerCase()); + keys.forEach((metadataKey) => tableCommentMap.set(metadataKey, comment)); + }; if (tableStatsResult?.success && Array.isArray(tableStatsResult.data)) { tableStatsResult.data.forEach((row: Record) => { const rawTableName = String( @@ -494,6 +504,15 @@ export const useSidebarTreeLoaders = ({ || '' ).trim(); if (!rawTableName) return; + putTableComment(rawTableName, getCaseInsensitiveValue(row, [ + 'table_comment', + 'TABLE_COMMENT', + 'comment', + 'Comment', + 'comments', + 'COMMENTS', + 'MS_Description', + ])); const rowCount = parseMetadataRowCount(row); if (rowCount === undefined) return; tableRowCountMap.set(rawTableName.toLowerCase(), rowCount); @@ -502,11 +521,23 @@ export const useSidebarTreeLoaders = ({ const tableEntries = tableRows.map((row: any) => { const tableName = Object.values(row)[0] as string; const parsed = splitQualifiedName(tableName); + const rowComment = getCaseInsensitiveValue(row, [ + 'table_comment', + 'TABLE_COMMENT', + 'comment', + 'Comment', + 'comments', + 'COMMENTS', + ]); return { tableName, schemaName: parsed.schemaName, displayName: getSidebarTableDisplayName(conn, tableName), rowCount: tableRowCountMap.get(String(tableName || '').trim().toLowerCase()), + tableComment: rowComment + || tableCommentMap.get(String(tableName || '').trim().toLowerCase()) + || tableCommentMap.get(String(parsed.objectName || '').trim().toLowerCase()) + || '', }; }); @@ -660,7 +691,7 @@ export const useSidebarTreeLoaders = ({ eventEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase())); - const buildTableNode = (entry: { tableName: string; schemaName: string; displayName: string; rowCount?: number }): TreeNode => { + const buildTableNode = (entry: { tableName: string; schemaName: string; displayName: string; rowCount?: number; tableComment?: string }): TreeNode => { const isPinned = isV2Ui && isSidebarTablePinned( currentPinnedSidebarTables, conn.id, @@ -678,6 +709,7 @@ export const useSidebarTreeLoaders = ({ tableName: entry.tableName, schemaName: entry.schemaName, rowCount: entry.rowCount, + tableComment: entry.tableComment, ...(isPinned ? { pinnedSidebarTable: true } : {}), }, isLeaf: false, diff --git a/frontend/src/components/sidebar/useSidebarV2ActionHandlers.tsx b/frontend/src/components/sidebar/useSidebarV2ActionHandlers.tsx index 5dfbc02..9e7b6df 100644 --- a/frontend/src/components/sidebar/useSidebarV2ActionHandlers.tsx +++ b/frontend/src/components/sidebar/useSidebarV2ActionHandlers.tsx @@ -5,6 +5,7 @@ import type { FormInstance } from 'antd/es/form'; import Modal from '../common/ResizableDraggableModal'; import { t } from '../../i18n'; import type { SavedConnection } from '../../types'; +import type { QueryOptions } from '../../store'; import { buildRpcConnectionConfig } from '../../utils/connectionRpcConfig'; import { resolveConnectionAccentColor, resolveConnectionIconType } from '../../utils/connectionVisual'; import { buildTableSelectQuery } from '../../utils/objectQueryTemplates'; @@ -55,6 +56,8 @@ type UseSidebarV2ActionHandlersArgs = { moveConnectionToTag: (connectionId: string, tagId: string | null) => void; setSidebarTablePinned: (connectionId: string, dbName: string, tableName: string, schemaName: string, pinned: boolean) => void; setTableSortPreference: (connectionId: string, dbName: string, sortBy: 'name' | 'frequency') => void; + setQueryOptions: (options: Partial) => void; + showSidebarTableComment: boolean; replaceTreeNodeChildren: (key: React.Key, children: TreeNode[] | undefined) => void; loadDatabases: (node: any) => Promise; loadTables: (node: any) => Promise; @@ -118,6 +121,8 @@ export const useSidebarV2ActionHandlers = ({ moveConnectionToTag, setSidebarTablePinned, setTableSortPreference, + setQueryOptions, + showSidebarTableComment, replaceTreeNodeChildren, loadDatabases, loadTables, @@ -262,6 +267,9 @@ export const useSidebarV2ActionHandlers = ({ case 'new-table': openNewTableDesign(node); return; + case 'toggle-table-comments': + setQueryOptions({ showSidebarTableComment: !showSidebarTableComment }); + return; case 'sort-by-name': handleTableGroupSortAction(node, 'name'); return; diff --git a/frontend/src/components/sidebar/useSidebarV2ContextMenu.tsx b/frontend/src/components/sidebar/useSidebarV2ContextMenu.tsx index 170a6cc..4097483 100644 --- a/frontend/src/components/sidebar/useSidebarV2ContextMenu.tsx +++ b/frontend/src/components/sidebar/useSidebarV2ContextMenu.tsx @@ -55,6 +55,7 @@ type SidebarV2ContextMenuOptions = { }; tableSortPreference: Record; pinnedSidebarTables: any[]; + showSidebarTableComment: boolean; getConnectionNodeForAction: (conn: SavedConnection) => TreeNode; buildRuntimeConfig: (conn: any, overrideDatabase?: string, clearDatabase?: boolean) => any; extractObjectName: (fullName: string) => string; @@ -82,6 +83,7 @@ export const useSidebarV2ContextMenu = ({ v2TreeMetrics, tableSortPreference, pinnedSidebarTables, + showSidebarTableComment, getConnectionNodeForAction, buildRuntimeConfig, extractObjectName, @@ -311,6 +313,7 @@ export const useSidebarV2ContextMenu = ({ dbName={String(groupData.dbName || '')} count={Array.isArray(node.children) ? node.children.length : 0} currentSort={currentSort} + showTableComments={showSidebarTableComment} onAction={(action) => { setContextMenu(null); handleV2TableGroupContextMenuAction(node, action); diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 1164ba0..dcd60f3 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -1231,6 +1231,7 @@ export interface SqlLog { export interface QueryOptions { maxRows: number; showColumnComment: boolean; + showSidebarTableComment?: boolean; showColumnType: boolean; showQueryResultsPanel: boolean; } @@ -1922,16 +1923,21 @@ const sanitizeQueryOptions = (value: unknown): QueryOptions => { const maxRows = Number(raw.maxRows); const showColumnComment = typeof raw.showColumnComment === "boolean" ? raw.showColumnComment : true; + const showSidebarTableComment = + typeof raw.showSidebarTableComment === "boolean" + ? raw.showSidebarTableComment + : false; const showColumnType = typeof raw.showColumnType === "boolean" ? raw.showColumnType : true; const showQueryResultsPanel = typeof raw.showQueryResultsPanel === "boolean" ? raw.showQueryResultsPanel : false; if (!Number.isFinite(maxRows) || maxRows <= 0) { - return { maxRows: 5000, showColumnComment, showColumnType, showQueryResultsPanel }; + return { maxRows: 5000, showColumnComment, showSidebarTableComment, showColumnType, showQueryResultsPanel }; } return { maxRows: Math.min(50000, Math.trunc(maxRows)), showColumnComment, + showSidebarTableComment, showColumnType, showQueryResultsPanel, }; @@ -2361,6 +2367,7 @@ export const useStore = create()( queryOptions: { maxRows: 5000, showColumnComment: true, + showSidebarTableComment: false, showColumnType: true, showQueryResultsPanel: false, }, diff --git a/frontend/src/v2-theme.css b/frontend/src/v2-theme.css index d91522f..cfec1be 100644 --- a/frontend/src/v2-theme.css +++ b/frontend/src/v2-theme.css @@ -2847,6 +2847,100 @@ body[data-ui-version="v2"] .gn-v2-tree-title.is-redis-db .gn-v2-tree-label { gap: 6px; } +body[data-ui-version="v2"] .gn-v2-tree-table-comment { + max-width: 24em; + flex: 0 1 auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--gn-fg-5); + font-family: var(--gn-font-sans); + font-size: clamp(10px, calc(var(--gn-sidebar-tree-font-size, var(--gn-font-size-sm, 12px)) - 1px), 16px); + font-weight: 400 !important; + opacity: 0.78; +} + +body[data-ui-version="v2"] .gn-v2-tab-hover-tooltip .ant-tooltip-inner { + min-width: 260px; + padding: 0; +} + +body[data-ui-version="v2"] .gn-v2-tab-hover-tooltip { + pointer-events: auto; +} + +body[data-ui-version="v2"] .gn-v2-tab-hover-card { + --gn-v2-tab-hover-grid-columns: 56px minmax(0, 1fr); + display: flex; + flex-direction: column; + gap: 8px; + padding: 10px; + color: var(--gn-fg-2); + cursor: text; + user-select: text; + -webkit-user-select: text; +} + +body[data-ui-version="v2"] .gn-v2-tab-hover-card * { + user-select: text; + -webkit-user-select: text; +} + +body[data-ui-version="v2"] .gn-v2-tab-hover-head { + display: grid; + grid-template-columns: var(--gn-v2-tab-hover-grid-columns); + align-items: start; + gap: 8px; + min-width: 0; +} + +body[data-ui-version="v2"] .gn-v2-tab-hover-head > span { + justify-self: start; + padding: 2px 6px; + border-radius: 5px; + background: var(--gn-bg-active); + color: var(--gn-accent-2); + font-family: var(--gn-font-mono); + font-size: 10px; + font-weight: 700; + line-height: 14px; +} + +body[data-ui-version="v2"] .gn-v2-tab-hover-head > strong { + min-width: 0; + overflow-wrap: anywhere; + color: var(--gn-fg-1); + font-size: var(--gn-font-size-sm, 12px); + font-weight: 700; + line-height: 18px; + white-space: normal; +} + +body[data-ui-version="v2"] .gn-v2-tab-hover-rows { + display: grid; + gap: 5px; +} + +body[data-ui-version="v2"] .gn-v2-tab-hover-row { + display: grid; + grid-template-columns: var(--gn-v2-tab-hover-grid-columns); + align-items: start; + gap: 8px; + font-size: var(--gn-font-size-sm, 12px); + line-height: 18px; +} + +body[data-ui-version="v2"] .gn-v2-tab-hover-row > span { + color: var(--gn-fg-5); +} + +body[data-ui-version="v2"] .gn-v2-tab-hover-row > strong { + min-width: 0; + overflow-wrap: anywhere; + color: var(--gn-fg-2); + font-weight: 600; +} + body[data-ui-version="v2"] .gn-v2-redis-db-alias { color: var(--gn-fg-5); font-family: var(--gn-font-sans); diff --git a/shared/i18n/de-DE.json b/shared/i18n/de-DE.json index 5c8f717..399b3e5 100644 --- a/shared/i18n/de-DE.json +++ b/shared/i18n/de-DE.json @@ -7074,9 +7074,12 @@ "sidebar.v2_schema_menu.export_current_schema_sql": "Tabellenstrukturen des aktuellen Schemas exportieren · SQL", "sidebar.v2_schema_menu.meta": "{{database}} · Schema-Aktionen", "sidebar.v2_table_group_menu.current_database": "Aktuelle Datenbank", + "sidebar.v2_table_group_menu.display_section": "Anzeige", "sidebar.v2_table_group_menu.meta": "{{database}} · {{count}} Tabellen · nach {{sort}} sortiert", + "sidebar.v2_table_group_menu.show_table_comments": "Tabellenkommentare anzeigen", "sidebar.v2_table_group_menu.sort_frequency": "Nutzungshäufigkeit", "sidebar.v2_table_group_menu.sort_name": "Name", + "sidebar.v2_table_group_menu.table_comment_tooltip": "Kommentar: {{comment}}", "sidebar.v2_table_group_menu.title": "Tabellen", "sidebar.v2_table_menu.ai_explain_table": "Mit AI diese Tabelle erklären", "sidebar.v2_table_menu.ai_generate_query": "Mit AI eine Abfrage erzeugen", diff --git a/shared/i18n/en-US.json b/shared/i18n/en-US.json index 4a00591..6be5ec2 100644 --- a/shared/i18n/en-US.json +++ b/shared/i18n/en-US.json @@ -7074,9 +7074,12 @@ "sidebar.v2_schema_menu.export_current_schema_sql": "Export current schema table structures · SQL", "sidebar.v2_schema_menu.meta": "{{database}} · Schema actions", "sidebar.v2_table_group_menu.current_database": "Current database", + "sidebar.v2_table_group_menu.display_section": "Display", "sidebar.v2_table_group_menu.meta": "{{database}} · {{count}} tables · sorted by {{sort}}", + "sidebar.v2_table_group_menu.show_table_comments": "Show table comments", "sidebar.v2_table_group_menu.sort_frequency": "usage frequency", "sidebar.v2_table_group_menu.sort_name": "name", + "sidebar.v2_table_group_menu.table_comment_tooltip": "Comment: {{comment}}", "sidebar.v2_table_group_menu.title": "Tables", "sidebar.v2_table_menu.ai_explain_table": "Use AI to explain this table", "sidebar.v2_table_menu.ai_generate_query": "Use AI to generate a query", diff --git a/shared/i18n/ja-JP.json b/shared/i18n/ja-JP.json index 59d7a26..c383a8d 100644 --- a/shared/i18n/ja-JP.json +++ b/shared/i18n/ja-JP.json @@ -7074,9 +7074,12 @@ "sidebar.v2_schema_menu.export_current_schema_sql": "現在のスキーマのテーブル構造をエクスポート · SQL", "sidebar.v2_schema_menu.meta": "{{database}} · スキーマ操作", "sidebar.v2_table_group_menu.current_database": "現在のデータベース", + "sidebar.v2_table_group_menu.display_section": "表示", "sidebar.v2_table_group_menu.meta": "{{database}} · {{count}} テーブル · {{sort}}順で並べ替え中", + "sidebar.v2_table_group_menu.show_table_comments": "テーブルコメントを表示", "sidebar.v2_table_group_menu.sort_frequency": "使用頻度", "sidebar.v2_table_group_menu.sort_name": "名前", + "sidebar.v2_table_group_menu.table_comment_tooltip": "コメント: {{comment}}", "sidebar.v2_table_group_menu.title": "テーブル", "sidebar.v2_table_menu.ai_explain_table": "AI でこのテーブルを説明", "sidebar.v2_table_menu.ai_generate_query": "AI でクエリを生成", diff --git a/shared/i18n/ru-RU.json b/shared/i18n/ru-RU.json index c2244ab..597d1de 100644 --- a/shared/i18n/ru-RU.json +++ b/shared/i18n/ru-RU.json @@ -7074,9 +7074,12 @@ "sidebar.v2_schema_menu.export_current_schema_sql": "Экспортировать структуры таблиц текущей схемы · SQL", "sidebar.v2_schema_menu.meta": "{{database}} · Действия со схемой", "sidebar.v2_table_group_menu.current_database": "Текущая база данных", + "sidebar.v2_table_group_menu.display_section": "Отображение", "sidebar.v2_table_group_menu.meta": "{{database}} · {{count}} таблиц · сортировка по {{sort}}", + "sidebar.v2_table_group_menu.show_table_comments": "Показывать комментарии таблиц", "sidebar.v2_table_group_menu.sort_frequency": "частоте использования", "sidebar.v2_table_group_menu.sort_name": "имени", + "sidebar.v2_table_group_menu.table_comment_tooltip": "Комментарий: {{comment}}", "sidebar.v2_table_group_menu.title": "Таблицы", "sidebar.v2_table_menu.ai_explain_table": "Объяснить эту таблицу с помощью AI", "sidebar.v2_table_menu.ai_generate_query": "Сгенерировать запрос с помощью AI", diff --git a/shared/i18n/zh-CN.json b/shared/i18n/zh-CN.json index 92f25d1..11071f6 100644 --- a/shared/i18n/zh-CN.json +++ b/shared/i18n/zh-CN.json @@ -7074,9 +7074,12 @@ "sidebar.v2_schema_menu.export_current_schema_sql": "导出当前模式表结构 · SQL", "sidebar.v2_schema_menu.meta": "{{database}} · 模式操作", "sidebar.v2_table_group_menu.current_database": "当前数据库", + "sidebar.v2_table_group_menu.display_section": "显示", "sidebar.v2_table_group_menu.meta": "{{database}} · {{count}} 张表 · 当前按{{sort}}排序", + "sidebar.v2_table_group_menu.show_table_comments": "显示表备注", "sidebar.v2_table_group_menu.sort_frequency": "使用频率", "sidebar.v2_table_group_menu.sort_name": "名称", + "sidebar.v2_table_group_menu.table_comment_tooltip": "备注:{{comment}}", "sidebar.v2_table_group_menu.title": "表", "sidebar.v2_table_menu.ai_explain_table": "用 AI 解释这张表", "sidebar.v2_table_menu.ai_generate_query": "用 AI 生成查询", diff --git a/shared/i18n/zh-TW.json b/shared/i18n/zh-TW.json index cd6ad13..4c1f59a 100644 --- a/shared/i18n/zh-TW.json +++ b/shared/i18n/zh-TW.json @@ -7074,9 +7074,12 @@ "sidebar.v2_schema_menu.export_current_schema_sql": "匯出目前模式的資料表結構 · SQL", "sidebar.v2_schema_menu.meta": "{{database}} · 模式操作", "sidebar.v2_table_group_menu.current_database": "目前資料庫", + "sidebar.v2_table_group_menu.display_section": "顯示", "sidebar.v2_table_group_menu.meta": "{{database}} · {{count}} 張資料表 · 目前依{{sort}}排序", + "sidebar.v2_table_group_menu.show_table_comments": "顯示資料表備註", "sidebar.v2_table_group_menu.sort_frequency": "使用頻率", "sidebar.v2_table_group_menu.sort_name": "名稱", + "sidebar.v2_table_group_menu.table_comment_tooltip": "備註:{{comment}}", "sidebar.v2_table_group_menu.title": "資料表", "sidebar.v2_table_menu.ai_explain_table": "用 AI 解釋這張資料表", "sidebar.v2_table_menu.ai_generate_query": "用 AI 產生查詢",