feat(sidebar): 支持表元数据显示配置

- 新增侧栏表元数据字段配置并兼容旧版表备注开关
- 设置中心新增侧栏表元数据入口并移除树上重复显示开关
- 批量补齐表大小、创建时间、修改时间等元数据加载与渲染
- 统一表元数据格式化逻辑并修复 V2 左树横向滚动裁切
- 补充设置中心、store、侧栏元数据相关回归测试与多语言文案
This commit is contained in:
Syngnat
2026-07-02 12:54:43 +08:00
parent 92eb4266da
commit 68d4081fcf
23 changed files with 1097 additions and 251 deletions

View File

@@ -1306,6 +1306,34 @@ describe('Sidebar locate toolbar', () => {
expect(wideWidth).toBeUndefined();
});
it('includes table metadata suffixes when estimating v2 tree horizontal scroll width', () => {
setCurrentLanguage('zh-CN');
const tableNode = [{
title: 'orders',
key: 'table-orders',
type: 'table',
dataRef: {
tableComment: '订单归档明细按月分区',
rowCount: 2_450_000,
tableSize: 157_286_400,
createdAt: '2026-07-01 08:30:00',
updatedAt: '2026-07-02 09:45:00',
},
}];
const viewportWidth = 360;
const titleOnlyWidth = estimateV2TreeHorizontalScrollWidth(tableNode as any, viewportWidth, []);
const metadataWidth = estimateV2TreeHorizontalScrollWidth(
tableNode as any,
viewportWidth,
['comment', 'rows', 'size', 'createdAt', 'updatedAt'],
);
expect(titleOnlyWidth).toBeUndefined();
expect(metadataWidth).toBeGreaterThan(viewportWidth);
expect(metadataWidth).toBeGreaterThan(titleOnlyWidth ?? viewportWidth);
});
it('does not repeat the active connection as an object-tree root in v2', () => {
mocks.state.connections = [{
id: 'conn-local',
@@ -2926,7 +2954,6 @@ describe('Sidebar locate toolbar', () => {
dbName="mkefu_ai_dev"
count={15}
currentSort="frequency"
showTableComments
/>,
);
@@ -2938,8 +2965,6 @@ 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'));
@@ -2955,7 +2980,6 @@ 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);
@@ -3020,6 +3044,10 @@ describe('Sidebar locate toolbar', () => {
dbName: 'main',
tableName: 'users',
tableComment: '用户表',
rowCount: 7,
tableSize: 4096,
createdAt: '2026-07-02 10:11:12',
updatedAt: '2026-07-03 11:12:13',
},
};
const baseOptions = {
@@ -3036,18 +3064,23 @@ describe('Sidebar locate toolbar', () => {
const hiddenSuffixMarkup = renderToStaticMarkup(renderSidebarV2TreeTitle({
...baseOptions,
showSidebarTableComment: false,
sidebarTableMetadataFields: ['rows'],
}));
expect(hiddenSuffixMarkup).not.toContain('gn-v2-tree-table-comment');
const visibleSuffixMarkup = renderToStaticMarkup(renderSidebarV2TreeTitle({
...baseOptions,
showSidebarTableComment: true,
sidebarTableMetadataFields: ['comment', 'rows', 'size', 'createdAt', 'updatedAt'],
}));
expect(visibleSuffixMarkup).toContain('gn-v2-tree-table-comment');
expect(visibleSuffixMarkup).toContain('用户表');
expect(visibleSuffixMarkup).toContain(t('sidebar.v2_table_group_menu.metadata_value.rows', { count: '7' }));
expect(visibleSuffixMarkup).toContain('4 KB');
expect(visibleSuffixMarkup).toContain(t('sidebar.v2_table_group_menu.metadata_value.created_at', { time: '2026-07-02 10:11' }));
expect(visibleSuffixMarkup).toContain(t('sidebar.v2_table_group_menu.metadata_value.updated_at', { time: '2026-07-03 11:12' }));
const treeTitleSource = readSourceFile('./sidebar/SidebarTreeTitle.tsx');
const sidebarHelpersSource = readSourceFile('./sidebar/sidebarHelpers.ts');
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}');
@@ -3059,6 +3092,10 @@ describe('Sidebar locate toolbar', () => {
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(sidebarHelpersSource).toContain("t('sidebar.v2_table_group_menu.metadata_value.rows'");
expect(treeTitleSource).toContain("t('sidebar.v2_table_group_menu.display_table_size')");
expect(treeTitleSource).toContain("t('sidebar.v2_table_group_menu.display_create_time')");
expect(treeTitleSource).toContain("t('sidebar.v2_table_group_menu.display_update_time')");
expect(treeTitleSource).toContain('mouseEnterDelay={1.2}');
const css = readV2ThemeCss();
@@ -3076,13 +3113,24 @@ describe('Sidebar locate toolbar', () => {
const oracleSql = buildSidebarTableStatusSQL({ config: { type: 'oracle' } } as any, 'APP');
expect(mysqlSql).toContain('TABLE_COMMENT AS table_comment');
expect(mysqlSql).toContain('AS table_size');
expect(mysqlSql).toContain('CREATE_TIME AS create_time');
expect(pgSql).toContain("obj_description(c.oid, 'pg_class') AS table_comment");
expect(pgSql).toContain('pg_total_relation_size(c.oid) AS table_size');
expect(sqlServerSql).toContain('ep.value AS table_comment');
expect(sqlServerSql).toContain('t.create_date AS create_time');
expect(oracleSql).toContain('comments AS table_comment');
expect(oracleSql).toContain('o.last_ddl_time AS update_time');
const loaderSource = readSourceFile('./sidebar/useSidebarTreeLoaders.tsx');
expect(loaderSource).toContain('tableCommentMap');
expect(loaderSource).toContain('tableComment: entry.tableComment');
expect(loaderSource).toContain('tableMetadataMap');
expect(loaderSource).toContain('resolvedMetadata?.tableComment');
expect(loaderSource).toContain('tableSize: resolvedMetadata?.tableSize');
expect(loaderSource).toContain('createdAt: resolvedMetadata?.createdAt');
expect(loaderSource).toContain('updatedAt: resolvedMetadata?.updatedAt');
expect(loaderSource).toContain('tableSize: entry.tableSize');
expect(loaderSource).toContain('createdAt: entry.createdAt');
expect(loaderSource).toContain('updatedAt: entry.updatedAt');
});
it('listens for table overview pin changes to refresh the matching sidebar database node', () => {

View File

@@ -142,6 +142,7 @@ import {
import { useExportProgressDialog } from './ExportProgressModal';
import { getShortcutPlatform, resolveShortcutDisplay } from '../utils/shortcuts';
import { buildExternalSQLRootNode, type ExternalSQLTreeNode } from '../utils/externalSqlTree';
import { resolveSidebarTableMetadataFields } from '../utils/sidebarTableMetadata';
import { t } from '../i18n';
import MessagePublishModal from './MessagePublishModal';
import {
@@ -481,7 +482,13 @@ const Sidebar: React.FC<{
const darkMode = theme === 'dark';
const resolvedAppearance = resolveAppearanceValues(appearance);
const opacity = normalizeOpacityForPlatform(resolvedAppearance.opacity);
const showSidebarTableComment = queryOptions?.showSidebarTableComment === true;
const sidebarTableMetadataFields = useMemo(
() => resolveSidebarTableMetadataFields(
queryOptions?.sidebarTableMetadataFields,
queryOptions?.showSidebarTableComment === true,
),
[queryOptions?.showSidebarTableComment, queryOptions?.sidebarTableMetadataFields],
);
const { exportProgressModal, runExportWithProgress } = useExportProgressDialog();
const disableLocalBackdropFilter = isMacLikePlatform();
const autoFetchVisible = useAutoFetchVisibility();
@@ -2102,8 +2109,6 @@ const Sidebar: React.FC<{
moveConnectionToTag,
setSidebarTablePinned,
setTableSortPreference,
setQueryOptions,
showSidebarTableComment,
replaceTreeNodeChildren,
loadDatabases,
loadTables,
@@ -2166,6 +2171,7 @@ const Sidebar: React.FC<{
v2CommandSearchValue,
setV2CommandActiveIndex,
v2ExplorerFilter,
sidebarTableMetadataFields,
treeData,
treeViewportWidth,
treeHeight,
@@ -2231,7 +2237,6 @@ const Sidebar: React.FC<{
v2TreeMetrics,
tableSortPreference,
pinnedSidebarTables,
showSidebarTableComment,
getConnectionNodeForAction,
buildRuntimeConfig,
extractObjectName,
@@ -2256,7 +2261,7 @@ const Sidebar: React.FC<{
hoverTitle,
statusBadge,
getV2TreeMetaText,
showSidebarTableComment,
sidebarTableMetadataFields,
toggleSidebarTablePinned,
snapshotTreeSelectionBeforeDrag,
restoreTreeSelectionAfterDrag,

View File

@@ -20,6 +20,7 @@ import {
CheckSquareOutlined,
CloudOutlined,
ClearOutlined,
ClockCircleOutlined,
ColumnWidthOutlined,
DashboardOutlined,
EyeInvisibleOutlined,
@@ -34,6 +35,7 @@ import {
} from '@ant-design/icons';
import { getCurrentLanguage, t } from '../i18n';
import { getPrimaryShortcutDisplayLabel, type ShortcutPlatform } from '../utils/shortcuts';
import { formatSidebarTableSize } from './sidebar/sidebarHelpers';
export type V2TableContextMenuActionKey =
| 'pin-table'
@@ -88,11 +90,8 @@ export const formatV2TableContextMenuRows = (count?: number): string => {
};
export const formatV2TableContextMenuSize = (bytes?: number): string => {
if (bytes === undefined || bytes === null || !Number.isFinite(bytes) || bytes < 0) return '—';
if (bytes < 1024) return `${Math.round(bytes)} B`;
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
const formatted = bytes === undefined ? '' : formatSidebarTableSize(bytes);
return formatted || '—';
};
const resolveV2TableContextMenuMeta = (stats?: V2TableContextMenuStats): string => {
@@ -264,7 +263,6 @@ export const V2TableContextMenuView: React.FC<{
export type V2TableGroupContextMenuActionKey =
| 'new-table'
| 'toggle-table-comments'
| 'sort-by-name'
| 'sort-by-frequency';
@@ -274,7 +272,6 @@ export const V2TableGroupContextMenuView: React.FC<{
dbName?: string;
count?: number;
currentSort?: 'name' | 'frequency';
showTableComments?: boolean;
onAction?: (action: V2TableGroupContextMenuActionKey) => void;
}> = ({
title,
@@ -282,7 +279,6 @@ export const V2TableGroupContextMenuView: React.FC<{
dbName,
count,
currentSort = 'name',
showTableComments = false,
onAction,
}) => {
const sortLabel = currentSort === 'frequency'
@@ -313,17 +309,6 @@ 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

@@ -3,17 +3,28 @@ 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 {
type SidebarTableMetadataField,
type SidebarTableMetadataSnapshot,
} from '../../utils/sidebarTableMetadata';
import { sanitizeRedisDbAlias } from '../../utils/redisDbAlias';
import { resolveConnectionHostSummary } from '../../utils/tabDisplay';
import { resolveSidebarObjectDragText } from '../sidebarCoreUtils';
import { resolveV2ObjectGroupTitle } from './sidebarHelpers';
import {
buildSidebarTableMetadataDisplayItems,
buildSidebarTableMetadataSnapshot,
formatSidebarRowCount,
formatSidebarTableSize,
formatSidebarTableTimestamp,
resolveV2ObjectGroupTitle,
} from './sidebarHelpers';
type SidebarV2TreeTitleOptions = {
node: any;
hoverTitle: string;
statusBadge: React.ReactNode;
getV2TreeMetaText: (node: any) => string;
showSidebarTableComment: boolean;
sidebarTableMetadataFields: SidebarTableMetadataField[];
toggleSidebarTablePinned: (node: any) => void;
snapshotTreeSelectionBeforeDrag: () => void;
restoreTreeSelectionAfterDrag: () => void;
@@ -42,7 +53,7 @@ const clearSidebarTableNativeHoverTitle = (event: React.SyntheticEvent<HTMLEleme
const renderSidebarTableHoverInfo = (
node: any,
displayTitle: string,
tableComment: string,
metadata: SidebarTableMetadataSnapshot,
): React.ReactNode => {
const dataRef = node?.dataRef || {};
const tableName = String(dataRef.tableName || displayTitle || node?.title || '').trim();
@@ -57,7 +68,11 @@ const renderSidebarTableHoverInfo = (
[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],
[t('table_designer.action.table_comment'), metadata.tableComment || ''],
[t('sidebar.v2_table_group_menu.display_table_rows'), metadata.rowCount !== undefined ? formatSidebarRowCount(metadata.rowCount) : ''],
[t('sidebar.v2_table_group_menu.display_table_size'), metadata.tableSize !== undefined ? formatSidebarTableSize(metadata.tableSize) : ''],
[t('sidebar.v2_table_group_menu.display_create_time'), metadata.createdAt ? formatSidebarTableTimestamp(metadata.createdAt) : ''],
[t('sidebar.v2_table_group_menu.display_update_time'), metadata.updatedAt ? formatSidebarTableTimestamp(metadata.updatedAt) : ''],
].filter(([, value]) => Boolean(value));
return (
@@ -100,7 +115,7 @@ export const renderSidebarV2TreeTitle = ({
hoverTitle,
statusBadge,
getV2TreeMetaText,
showSidebarTableComment,
sidebarTableMetadataFields,
toggleSidebarTablePinned,
snapshotTreeSelectionBeforeDrag,
restoreTreeSelectionAfterDrag,
@@ -130,15 +145,17 @@ export const renderSidebarV2TreeTitle = ({
}
return rawTitle;
})();
const tableComment = node.type === 'table'
? String(node?.dataRef?.tableComment || '').trim()
: '';
const tableCommentSuffix = showSidebarTableComment && tableComment ? tableComment : '';
const tableMetadata = node.type === 'table'
? buildSidebarTableMetadataSnapshot(node?.dataRef)
: null;
const tableMetadataItems = tableMetadata
? buildSidebarTableMetadataDisplayItems(sidebarTableMetadataFields, tableMetadata)
: [];
const effectiveHoverTitle = hoverTitle;
const tableHoverInfo = node.type === 'table'
? renderSidebarTableHoverInfo(node, displayTitle, tableComment)
? renderSidebarTableHoverInfo(node, displayTitle, tableMetadata ?? {})
: null;
const metaText = getV2TreeMetaText(node);
const metaText = node.type === 'table' ? '' : getV2TreeMetaText(node);
const redisDbAlias = node.type === 'redis-db'
? sanitizeRedisDbAlias(node?.dataRef?.redisDbAlias)
: '';
@@ -248,9 +265,9 @@ export const renderSidebarV2TreeTitle = ({
</>
) : displayTitle}
</span>
{tableCommentSuffix && (
<span className="gn-v2-tree-table-comment">{tableCommentSuffix}</span>
)}
{tableMetadataItems.map((item) => (
<span key={item.key} className={item.className}>{item.text}</span>
))}
{metaText && <span className="gn-v2-tree-count">{metaText}</span>}
</span>
);

View File

@@ -9,6 +9,10 @@
// - 共享常量和类型集中管理,便于跨文件复用
import { t } from '../../i18n';
import type {
SidebarTableMetadataField,
SidebarTableMetadataSnapshot,
} from '../../utils/sidebarTableMetadata';
// === 共享常量 ===
@@ -36,6 +40,122 @@ export const formatSidebarRowCount = (count: number): string => {
return String(Math.round(count));
};
/**
* formatSidebarTableSize 把字节数格式化为适合侧栏展示的短文本。
*/
export const formatSidebarTableSize = (bytes: number): string => {
if (!Number.isFinite(bytes) || bytes < 0) return '';
if (bytes < 1024) return `${Math.round(bytes)} B`;
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
};
const padSidebarTimestampPart = (value: number): string => String(value).padStart(2, '0');
/**
* formatSidebarTableTimestamp 把数据库返回的时间值统一压缩成 `YYYY-MM-DD HH:mm`。
* 若值无法被 Date 可靠解析,则尽量保留原始文本的可读部分。
*/
export const formatSidebarTableTimestamp = (value: unknown): string => {
const text = String(value ?? '').trim();
if (!text) return '';
const normalizedText = text.replace('T', ' ').replace(/\.\d+$/, '');
const parsed = new Date(text);
if (Number.isNaN(parsed.getTime())) {
return normalizedText.length > 16 ? normalizedText.slice(0, 16) : normalizedText;
}
return [
parsed.getFullYear(),
'-',
padSidebarTimestampPart(parsed.getMonth() + 1),
'-',
padSidebarTimestampPart(parsed.getDate()),
' ',
padSidebarTimestampPart(parsed.getHours()),
':',
padSidebarTimestampPart(parsed.getMinutes()),
].join('');
};
export interface SidebarTableMetadataDisplayItem {
key: SidebarTableMetadataField;
text: string;
className: string;
}
export const buildSidebarTableMetadataSnapshot = (
value: Partial<SidebarTableMetadataSnapshot> | null | undefined,
): SidebarTableMetadataSnapshot => {
const rowCount = Number(value?.rowCount);
const tableSize = Number(value?.tableSize);
const tableComment = String(value?.tableComment || '').trim();
const createdAt = String(value?.createdAt || '').trim();
const updatedAt = String(value?.updatedAt || '').trim();
return {
...(tableComment ? { tableComment } : {}),
...(Number.isFinite(rowCount) && rowCount >= 0 ? { rowCount } : {}),
...(Number.isFinite(tableSize) && tableSize >= 0 ? { tableSize } : {}),
...(createdAt ? { createdAt } : {}),
...(updatedAt ? { updatedAt } : {}),
};
};
export const buildSidebarTableMetadataDisplayItems = (
metadataFields: SidebarTableMetadataField[],
snapshot: SidebarTableMetadataSnapshot,
): SidebarTableMetadataDisplayItem[] => {
const items: SidebarTableMetadataDisplayItem[] = [];
if (metadataFields.includes('comment') && snapshot.tableComment) {
items.push({
key: 'comment',
text: snapshot.tableComment,
className: 'gn-v2-tree-table-comment',
});
}
if (metadataFields.includes('rows') && snapshot.rowCount !== undefined) {
const count = formatSidebarRowCount(snapshot.rowCount);
if (count) {
items.push({
key: 'rows',
text: t('sidebar.v2_table_group_menu.metadata_value.rows', { count }),
className: 'gn-v2-tree-count',
});
}
}
if (metadataFields.includes('size') && snapshot.tableSize !== undefined) {
const size = formatSidebarTableSize(snapshot.tableSize);
if (size) {
items.push({
key: 'size',
text: t('sidebar.v2_table_group_menu.metadata_value.size', { size }),
className: 'gn-v2-tree-count',
});
}
}
if (metadataFields.includes('createdAt') && snapshot.createdAt) {
const time = formatSidebarTableTimestamp(snapshot.createdAt);
if (time) {
items.push({
key: 'createdAt',
text: t('sidebar.v2_table_group_menu.metadata_value.created_at', { time }),
className: 'gn-v2-tree-count',
});
}
}
if (metadataFields.includes('updatedAt') && snapshot.updatedAt) {
const time = formatSidebarTableTimestamp(snapshot.updatedAt);
if (time) {
items.push({
key: 'updatedAt',
text: t('sidebar.v2_table_group_menu.metadata_value.updated_at', { time }),
className: 'gn-v2-tree-count',
});
}
}
return items;
};
/**
* hasSidebarLazyChildren 判断树节点的 children 是否已加载(用于按需展开)。
*/

View File

@@ -262,7 +262,9 @@ const buildSidebarTableStatusSQL = (
case "mysql":
case "starrocks":
return [
"SELECT TABLE_NAME AS table_name, TABLE_COMMENT AS table_comment, TABLE_ROWS AS table_rows",
"SELECT TABLE_NAME AS table_name, TABLE_COMMENT AS table_comment, TABLE_ROWS AS table_rows,",
"COALESCE(DATA_LENGTH, 0) + COALESCE(INDEX_LENGTH, 0) AS table_size,",
"CREATE_TIME AS create_time, UPDATE_TIME AS update_time",
"FROM information_schema.tables",
`WHERE table_schema = '${safeDbName}'`,
"AND table_type = 'BASE TABLE'",
@@ -275,7 +277,8 @@ const buildSidebarTableStatusSQL = (
case "opengauss":
case "gaussdb":
return [
"SELECT n.nspname || '.' || c.relname AS table_name, obj_description(c.oid, 'pg_class') AS table_comment, 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,",
"pg_total_relation_size(c.oid) AS table_size, NULL::text AS create_time, NULL::text AS update_time",
"FROM pg_class c",
"JOIN pg_namespace n ON n.oid = c.relnamespace",
"WHERE c.relkind = 'r'",
@@ -286,19 +289,22 @@ const buildSidebarTableStatusSQL = (
case "sqlserver": {
const safeDb = quoteSqlServerIdentifier(dbName);
return [
"SELECT s.name + '.' + t.name AS table_name, ep.value AS table_comment, 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,",
"SUM(a.total_pages) * 8 * 1024 AS table_size, t.create_date AS create_time, t.modify_date AS update_time",
`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)`,
`LEFT JOIN ${safeDb}.sys.allocation_units a ON p.partition_id = a.container_id`,
"WHERE t.type = 'U'",
"GROUP BY s.name, t.name, ep.value",
"GROUP BY s.name, t.name, ep.value, t.create_date, t.modify_date",
"ORDER BY s.name, t.name",
].join("\n");
}
case "clickhouse":
return [
"SELECT name AS table_name, comment AS table_comment, total_rows AS table_rows",
"SELECT name AS table_name, comment AS table_comment, total_rows AS table_rows,",
"total_bytes AS table_size, NULL AS create_time, metadata_modification_time AS update_time",
"FROM system.tables",
`WHERE database = '${safeDbName}'`,
"AND engine NOT IN ('View', 'MaterializedView')",
@@ -308,10 +314,15 @@ const buildSidebarTableStatusSQL = (
case "dm": {
const owner = escapeSQLLiteral(dbName).toUpperCase();
return [
"SELECT owner AS schema_name, 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",
"SELECT c.owner AS schema_name, c.table_name, c.comments AS table_comment, t.num_rows AS table_rows,",
"COALESCE(SUM(s.bytes), 0) AS table_size, o.created AS create_time, o.last_ddl_time AS update_time",
"FROM all_tab_comments c",
"JOIN all_tables t ON t.owner = c.owner AND t.table_name = c.table_name",
"LEFT JOIN all_objects o ON o.owner = t.owner AND o.object_name = t.table_name AND o.object_type = 'TABLE'",
"LEFT JOIN all_segments s ON s.owner = t.owner AND s.segment_name = t.table_name AND s.segment_type = 'TABLE'",
`WHERE c.owner = '${owner}'`,
"GROUP BY c.owner, c.table_name, c.comments, t.num_rows, o.created, o.last_ddl_time",
"ORDER BY c.table_name",
].join("\n");
}
default:

View File

@@ -21,6 +21,7 @@ import { useStore } from '../../store';
import type { SavedConnection } from '../../types';
import { getCurrentLanguage, t } from '../../i18n';
import { resolveShortcutDisplay } from '../../utils/shortcuts';
import type { SidebarTableMetadataField } from '../../utils/sidebarTableMetadata';
import { resolveConnectionHostSummary, resolveConnectionHostTokens } from '../../utils/tabDisplay';
import { resolveConnectionAccentColor, resolveConnectionIconType } from '../../utils/connectionVisual';
import { getDbIcon } from '../DatabaseIcons';
@@ -71,6 +72,7 @@ type SidebarSearchModelArgs = {
v2CommandSearchValue: string;
setV2CommandActiveIndex: Dispatch<SetStateAction<number>>;
v2ExplorerFilter: V2ExplorerFilter;
sidebarTableMetadataFields: SidebarTableMetadataField[];
treeData: TreeNode[];
treeViewportWidth: number;
treeHeight: number;
@@ -109,6 +111,7 @@ export const useSidebarSearchModel = ({
v2CommandSearchValue,
setV2CommandActiveIndex,
v2ExplorerFilter,
sidebarTableMetadataFields,
treeData,
treeViewportWidth,
treeHeight,
@@ -608,8 +611,12 @@ export const useSidebarSearchModel = ({
return filterV2ExplorerTreeByKind(activeConnectionTreeData, v2ExplorerFilter);
}, [activeConnectionTreeData, displayTreeData, v2ExplorerFilter]);
const v2TreeHorizontalScrollWidth = useMemo(
() => estimateV2TreeHorizontalScrollWidth(v2VisibleTreeData, treeViewportWidth),
[treeViewportWidth, v2VisibleTreeData],
() => estimateV2TreeHorizontalScrollWidth(
v2VisibleTreeData,
treeViewportWidth,
sidebarTableMetadataFields,
),
[sidebarTableMetadataFields, treeViewportWidth, v2VisibleTreeData],
);
const effectiveTreeHeight = treeHeight;
const v2TreeMetrics = useMemo(() => {

View File

@@ -51,6 +51,7 @@ import {
type SidebarTreeNode as TreeNode,
} from '../sidebarV2Utils';
import { DBGetDatabases, DBGetTables, DBQuery, GetDriverStatusList, JVMProbeCapabilities } from '../../../wailsjs/go/app/App';
import type { SidebarTableMetadataSnapshot } from '../../utils/sidebarTableMetadata';
type DriverStatusSnapshot = {
type: string;
@@ -495,9 +496,7 @@ export const useSidebarTreeLoaders = ({
const tableStatsResult = tableStatusSql
? 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 tableSchemaMap = new Map<string, string>();
const tableMetadataMap = new Map<string, SidebarTableMetadataSnapshot & { schemaName?: string }>();
const buildTableMetadataKeys = (rawTableName: string, rawSchemaName = ''): string[] => {
const tableName = String(rawTableName || '').trim();
if (!tableName) return [];
@@ -510,11 +509,33 @@ export const useSidebarTreeLoaders = ({
if (qualifiedName) keys.add(qualifiedName.toLowerCase());
return Array.from(keys);
};
const putTableComment = (rawTableName: string, rawComment: string, rawSchemaName = '') => {
const comment = String(rawComment || '').trim();
if (!comment) return;
const readNumericMetadataValue = (row: Record<string, any>, keys: string[]): number | undefined => {
const rawValue = getCaseInsensitiveValue(row, keys);
if (rawValue === undefined || rawValue === null || rawValue === '') return undefined;
const numericValue = Number(String(rawValue).replace(/,/g, ''));
return Number.isFinite(numericValue) ? numericValue : undefined;
};
const normalizeMetadataTimestamp = (rawValue: unknown): string | undefined => {
if (rawValue === undefined || rawValue === null) return undefined;
const normalized = String(rawValue).trim();
return normalized ? normalized : undefined;
};
const mergeTableMetadata = (
rawTableName: string,
patch: SidebarTableMetadataSnapshot & { schemaName?: string },
rawSchemaName = '',
) => {
buildTableMetadataKeys(rawTableName, rawSchemaName).forEach((metadataKey) => {
tableCommentMap.set(metadataKey, comment);
const current = tableMetadataMap.get(metadataKey) || {};
tableMetadataMap.set(metadataKey, {
...current,
...(patch.schemaName ? { schemaName: patch.schemaName } : {}),
...(patch.tableComment ? { tableComment: patch.tableComment } : {}),
...(patch.rowCount !== undefined ? { rowCount: patch.rowCount } : {}),
...(patch.tableSize !== undefined ? { tableSize: patch.tableSize } : {}),
...(patch.createdAt ? { createdAt: patch.createdAt } : {}),
...(patch.updatedAt ? { updatedAt: patch.updatedAt } : {}),
});
});
};
if (tableStatsResult?.success && Array.isArray(tableStatsResult.data)) {
@@ -526,12 +547,7 @@ export const useSidebarTreeLoaders = ({
).trim();
if (!rawTableName) return;
const rawSchemaName = getCaseInsensitiveValue(row, ['schema_name', 'SCHEMA_NAME', 'owner', 'OWNER']);
buildTableMetadataKeys(rawTableName, rawSchemaName).forEach((metadataKey) => {
if (rawSchemaName) {
tableSchemaMap.set(metadataKey, rawSchemaName);
}
});
putTableComment(rawTableName, getCaseInsensitiveValue(row, [
const tableComment = String(getCaseInsensitiveValue(row, [
'table_comment',
'TABLE_COMMENT',
'comment',
@@ -539,21 +555,54 @@ export const useSidebarTreeLoaders = ({
'comments',
'COMMENTS',
'MS_Description',
]), rawSchemaName);
]) || '').trim();
const rowCount = parseMetadataRowCount(row);
if (rowCount === undefined) return;
buildTableMetadataKeys(rawTableName, rawSchemaName).forEach((metadataKey) => {
tableRowCountMap.set(metadataKey, rowCount);
});
const tableSize = readNumericMetadataValue(row, [
'table_size',
'TABLE_SIZE',
'data_length',
'DATA_LENGTH',
'total_bytes',
'TOTAL_BYTES',
]);
const createdAt = normalizeMetadataTimestamp(getCaseInsensitiveValue(row, [
'create_time',
'CREATE_TIME',
'created_at',
'CREATED_AT',
'create_date',
'CREATE_DATE',
]));
const updatedAt = normalizeMetadataTimestamp(getCaseInsensitiveValue(row, [
'update_time',
'UPDATE_TIME',
'updated_at',
'UPDATED_AT',
'modify_date',
'MODIFY_DATE',
'last_ddl_time',
'LAST_DDL_TIME',
]));
mergeTableMetadata(rawTableName, {
schemaName: rawSchemaName ? String(rawSchemaName).trim() : undefined,
...(tableComment ? { tableComment } : {}),
...(rowCount !== undefined ? { rowCount } : {}),
...(tableSize !== undefined ? { tableSize } : {}),
...(createdAt ? { createdAt } : {}),
...(updatedAt ? { updatedAt } : {}),
}, rawSchemaName);
});
}
const tableEntries = tableRows.map((row: any) => {
const tableName = Object.values(row)[0] as string;
const parsed = splitQualifiedName(tableName);
const metadataKeys = buildTableMetadataKeys(tableName);
const resolvedMetadata = metadataKeys
.map((metadataKey) => tableMetadataMap.get(metadataKey))
.find((value): value is SidebarTableMetadataSnapshot & { schemaName?: string } => !!value);
const rowSchemaName = getCaseInsensitiveValue(row, ['schema_name', 'SCHEMA_NAME', 'owner', 'OWNER']);
const mappedSchemaName = rowSchemaName
|| metadataKeys.map((metadataKey) => tableSchemaMap.get(metadataKey)).find((value): value is string => !!value)
|| resolvedMetadata?.schemaName
|| parsed.schemaName;
const rowComment = getCaseInsensitiveValue(row, [
'table_comment',
@@ -567,13 +616,12 @@ export const useSidebarTreeLoaders = ({
tableName,
schemaName: mappedSchemaName,
displayName: getSidebarTableDisplayName(conn, tableName),
rowCount: metadataKeys
.map((metadataKey) => tableRowCountMap.get(metadataKey))
.find((value) => value !== undefined),
rowCount: resolvedMetadata?.rowCount,
tableSize: resolvedMetadata?.tableSize,
createdAt: resolvedMetadata?.createdAt,
updatedAt: resolvedMetadata?.updatedAt,
tableComment: rowComment
|| metadataKeys
.map((metadataKey) => tableCommentMap.get(metadataKey))
.find((value) => !!value)
|| resolvedMetadata?.tableComment
|| '',
};
});
@@ -740,7 +788,16 @@ 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; tableComment?: string }): TreeNode => {
const buildTableNode = (entry: {
tableName: string;
schemaName: string;
displayName: string;
rowCount?: number;
tableSize?: number;
createdAt?: string;
updatedAt?: string;
tableComment?: string;
}): TreeNode => {
const isPinned = isV2Ui && isSidebarTablePinned(
currentPinnedSidebarTables,
conn.id,
@@ -758,6 +815,9 @@ export const useSidebarTreeLoaders = ({
tableName: entry.tableName,
schemaName: entry.schemaName,
rowCount: entry.rowCount,
tableSize: entry.tableSize,
createdAt: entry.createdAt,
updatedAt: entry.updatedAt,
tableComment: entry.tableComment,
...(isPinned ? { pinnedSidebarTable: true } : {}),
},

View File

@@ -5,7 +5,6 @@ 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';
@@ -56,8 +55,6 @@ 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>;
@@ -121,8 +118,6 @@ export const useSidebarV2ActionHandlers = ({
moveConnectionToTag,
setSidebarTablePinned,
setTableSortPreference,
setQueryOptions,
showSidebarTableComment,
replaceTreeNodeChildren,
loadDatabases,
loadTables,
@@ -267,9 +262,6 @@ 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,7 +55,6 @@ 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;
@@ -83,7 +82,6 @@ export const useSidebarV2ContextMenu = ({
v2TreeMetrics,
tableSortPreference,
pinnedSidebarTables,
showSidebarTableComment,
getConnectionNodeForAction,
buildRuntimeConfig,
extractObjectName,
@@ -313,7 +311,6 @@ 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

@@ -7,8 +7,13 @@ import {
resolveSidebarRootOrderTokens,
} from '../store';
import type { ConnectionTag, SavedConnection } from '../types';
import type { SidebarTableMetadataField } from '../utils/sidebarTableMetadata';
import { t } from '../i18n';
import { t as catalogTranslate } from '../i18n/catalog';
import {
buildSidebarTableMetadataDisplayItems,
buildSidebarTableMetadataSnapshot,
} from './sidebar/sidebarHelpers';
type SidebarV2Translate = (key: string) => string;
@@ -327,12 +332,15 @@ const V2_TREE_HORIZONTAL_SCROLL_MAX_WIDTH = 2600;
const V2_TREE_HORIZONTAL_SCROLL_BASE_WIDTH = 88;
const V2_TREE_HORIZONTAL_SCROLL_INDENT_WIDTH = 24;
const V2_TREE_HORIZONTAL_SCROLL_AVG_CHAR_WIDTH = 8;
const V2_TREE_HORIZONTAL_SCROLL_ITEM_GAP_WIDTH = 5;
const V2_TREE_HORIZONTAL_SCROLL_COMMENT_MAX_CHARS = 32;
const V2_TREE_HORIZONTAL_SCROLL_VIEWPORT_BUFFER = 48;
export const V2_TREE_HORIZONTAL_SCROLL_BOTTOM_RESERVE = 32;
export const estimateV2TreeHorizontalScrollWidth = (
nodes: SidebarTreeNode[],
viewportWidth: number,
sidebarTableMetadataFields: SidebarTableMetadataField[] = [],
): number | undefined => {
const safeViewportWidth = Math.max(0, Math.ceil(viewportWidth || 0));
let estimatedContentWidth = safeViewportWidth;
@@ -340,12 +348,30 @@ export const estimateV2TreeHorizontalScrollWidth = (
const visit = (items: SidebarTreeNode[], depth: number) => {
items.forEach((node) => {
const title = String(node?.title || '');
const metaText = node?.dataRef?.groupKey === 'tables' && Array.isArray(node.children)
? String(node.children.length)
: '';
const tableMetadataItems = node?.type === 'table'
? buildSidebarTableMetadataDisplayItems(
sidebarTableMetadataFields,
buildSidebarTableMetadataSnapshot(node?.dataRef),
)
: [];
const metaText = tableMetadataItems.length > 0
? tableMetadataItems
.map((item) => item.key === 'comment'
? item.text.slice(0, V2_TREE_HORIZONTAL_SCROLL_COMMENT_MAX_CHARS)
: item.text)
.join('')
: node?.dataRef?.groupKey === 'tables' && Array.isArray(node.children)
? String(node.children.length)
: '';
const metaItemCount = tableMetadataItems.length > 0
? tableMetadataItems.length
: metaText
? 1
: 0;
const nodeWidth = V2_TREE_HORIZONTAL_SCROLL_BASE_WIDTH
+ (depth * V2_TREE_HORIZONTAL_SCROLL_INDENT_WIDTH)
+ ((title.length + metaText.length) * V2_TREE_HORIZONTAL_SCROLL_AVG_CHAR_WIDTH);
+ ((title.length + metaText.length) * V2_TREE_HORIZONTAL_SCROLL_AVG_CHAR_WIDTH)
+ (metaItemCount * V2_TREE_HORIZONTAL_SCROLL_ITEM_GAP_WIDTH);
estimatedContentWidth = Math.max(estimatedContentWidth, nodeWidth);
if (node.children?.length) {
visit(node.children, depth + 1);