feat(sidebar): 优化表备注悬浮信息展示

- 读取不同数据源表备注并写入左侧表节点
- 支持表分组菜单切换备注显示
- 表节点悬浮复用 Tab 信息卡并移除原生双提示

Fixes #569
This commit is contained in:
Syngnat
2026-06-27 17:43:11 +08:00
parent 038ecc8b70
commit e456925c23
17 changed files with 430 additions and 51 deletions

View File

@@ -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('<V2TableGroupContextMenuView');
expect(tableGroupCallSource).toContain('showTableComments={showSidebarTableComment}');
expect(tableGroupCallSource).not.toContain('title=');
['? ? tables', '表 · tables'].forEach((rawSnippet) => {
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();

View File

@@ -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,

View File

@@ -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: <TableOutlined />, title: t('sidebar.menu.create_table'), kbd: primaryShortcut('N', shortcutPlatform), featured: true },
])}
<div className="gn-v2-context-menu-section-title">{t('sidebar.v2_table_group_menu.display_section')}</div>
{renderItems([
{
action: 'toggle-table-comments',
icon: showTableComments ? <CheckSquareOutlined /> : <FileTextOutlined />,
title: t('sidebar.v2_table_group_menu.show_table_comments'),
kbd: showTableComments ? t('data_grid.context_menu.current_marker') : undefined,
selected: showTableComments,
},
])}
<div className="gn-v2-context-menu-section-title">{t('data_grid.context_menu.sort_section')}</div>
{renderItems([
{ action: 'sort-by-name', icon: currentSort === 'name' ? <CheckSquareOutlined /> : <ReloadOutlined />, title: t('sidebar.menu.sort_by_name'), kbd: currentSort === 'name' ? t('data_grid.context_menu.current_marker') : undefined, selected: currentSort === 'name' },

View File

@@ -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<HTMLElement>) => {
event.stopPropagation();
};
const clearSidebarTableNativeHoverTitleElement = (element: HTMLElement | null) => {
element?.closest(SIDEBAR_TREE_NODE_CONTENT_SELECTOR)?.removeAttribute('title');
};
const clearSidebarTableNativeHoverTitleRef: React.RefCallback<HTMLSpanElement> = (element) => {
clearSidebarTableNativeHoverTitleElement(element);
};
const clearSidebarTableNativeHoverTitle = (event: React.SyntheticEvent<HTMLElement>) => {
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 (
<div
className="gn-v2-tab-hover-card"
data-tab-hover-info="true"
data-sidebar-table-hover-info="true"
onPointerDown={stopSidebarTableHoverPropagation}
onPointerMove={stopSidebarTableHoverPropagation}
onPointerUp={stopSidebarTableHoverPropagation}
onPointerDownCapture={stopSidebarTableHoverPropagation}
onPointerUpCapture={stopSidebarTableHoverPropagation}
onMouseDown={stopSidebarTableHoverPropagation}
onMouseMove={stopSidebarTableHoverPropagation}
onMouseUp={stopSidebarTableHoverPropagation}
onClick={stopSidebarTableHoverPropagation}
onClickCapture={stopSidebarTableHoverPropagation}
onTouchStart={stopSidebarTableHoverPropagation}
onTouchMove={stopSidebarTableHoverPropagation}
onTouchEnd={stopSidebarTableHoverPropagation}
>
<div className="gn-v2-tab-hover-head">
<span>{t('tab_manager.kind_badge.table')}</span>
<strong>{tableName || displayTitle}</strong>
</div>
<div className="gn-v2-tab-hover-rows">
{rows.map(([label, value], index) => (
<div className="gn-v2-tab-hover-row" key={`${String(label)}-${index}`}>
<span>{label}</span>
<strong>{value}</strong>
</div>
))}
</div>
</div>
);
};
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 (
<span
className={`${titleClassName} is-connection`}
title={hoverTitle}
title={effectiveHoverTitle}
data-node-type={node.type}
data-sidebar-node-key={String(node.key || '')}
data-sidebar-node-type={String(node.type || '')}
@@ -119,49 +205,71 @@ export const renderSidebarV2TreeTitle = ({
</span>
);
}
const titleNode = (
<span
ref={tableHoverInfo ? clearSidebarTableNativeHoverTitleRef : undefined}
className={titleClassName}
title={tableHoverInfo ? undefined : effectiveHoverTitle}
draggable={!!dragText}
data-node-type={node.type}
data-group-key={groupKey || undefined}
data-sidebar-node-key={String(node.key || '')}
data-sidebar-node-type={String(node.type || '')}
onPointerOverCapture={tableHoverInfo ? clearSidebarTableNativeHoverTitle : undefined}
onMouseOverCapture={tableHoverInfo ? clearSidebarTableNativeHoverTitle : undefined}
onDragStart={dragText ? (event) => {
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}
<span className="gn-v2-tree-label">
{redisDbAlias ? (
<>
<span className="gn-v2-redis-db-name">{redisDbBaseTitle}</span>
<span className="gn-v2-redis-db-alias">{redisDbAlias}</span>
</>
) : displayTitle}
</span>
{tableCommentSuffix && (
<span className="gn-v2-tree-table-comment">{tableCommentSuffix}</span>
)}
{metaText && <span className="gn-v2-tree-count">{metaText}</span>}
</span>
);
const wrappedTitleNode = tableHoverInfo ? (
<Tooltip
title={tableHoverInfo}
placement="right"
mouseEnterDelay={1.2}
destroyOnHidden
rootClassName="gn-v2-tab-hover-tooltip gn-v2-sidebar-table-hover-tooltip"
>
{titleNode}
</Tooltip>
) : titleNode;
return (
<>
<span
className={titleClassName}
title={hoverTitle}
draggable={!!dragText}
data-node-type={node.type}
data-group-key={groupKey || undefined}
data-sidebar-node-key={String(node.key || '')}
data-sidebar-node-type={String(node.type || '')}
onDragStart={dragText ? (event) => {
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}
<span className="gn-v2-tree-label">
{redisDbAlias ? (
<>
<span className="gn-v2-redis-db-name">{redisDbBaseTitle}</span>
<span className="gn-v2-redis-db-alias">{redisDbAlias}</span>
</>
) : displayTitle}
</span>
{metaText && <span className="gn-v2-tree-count">{metaText}</span>}
</span>
{wrappedTitleNode}
{tablePinAction}
</>
);

View File

@@ -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");

View File

@@ -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) {

View File

@@ -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<string, number>();
const tableCommentMap = new Map<string, string>();
const putTableComment = (rawTableName: string, rawComment: string) => {
const tableName = String(rawTableName || '').trim();
const comment = String(rawComment || '').trim();
if (!tableName || !comment) return;
const keys = new Set<string>([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<string, any>) => {
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,

View File

@@ -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<QueryOptions>) => void;
showSidebarTableComment: boolean;
replaceTreeNodeChildren: (key: React.Key, children: TreeNode[] | undefined) => void;
loadDatabases: (node: any) => Promise<void>;
loadTables: (node: any) => Promise<void>;
@@ -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;

View File

@@ -55,6 +55,7 @@ type SidebarV2ContextMenuOptions = {
};
tableSortPreference: Record<string, any>;
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);

View File

@@ -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<AppState>()(
queryOptions: {
maxRows: 5000,
showColumnComment: true,
showSidebarTableComment: false,
showColumnType: true,
showQueryResultsPanel: false,
},

View File

@@ -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);

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 でクエリを生成",

View File

@@ -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",

View File

@@ -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 生成查询",

View File

@@ -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 產生查詢",