feat(sidebar): 同步标签上下文并补充对象树统计信息

- 切换和关闭标签时同步 activeContext,避免新建查询误用 host 或数据库
- 侧边栏表节点展示行数统计,数据库节点展示表数量
- 旧版 sidebar 工具栏改为稳定五列布局,v1 不再混入 v2 置顶分组
- 补充 sidebar 与 store 回归测试
This commit is contained in:
Syngnat
2026-05-31 13:34:21 +08:00
parent e5fb03bbcd
commit 255e484dcf
5 changed files with 473 additions and 84 deletions

View File

@@ -4,6 +4,7 @@ import { renderToStaticMarkup } from 'react-dom/server';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import Sidebar, {
buildSidebarTableChildrenForUi,
buildV2SidebarTableSectionedChildren,
buildV2RailConnectionGroups,
filterV2ExplorerTreeByKind,
@@ -357,6 +358,20 @@ describe('Sidebar locate toolbar', () => {
expect(locateActionIndex).toBeGreaterThan(externalSqlActionIndex);
});
it('keeps the legacy sidebar toolbar on a stable five-column grid layout', () => {
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
const markup = renderToStaticMarkup(<Sidebar />);
expect(markup).toContain('data-sidebar-legacy-toolbar="true"');
expect(markup).toContain('data-sidebar-legacy-toolbar-item="true"');
expect(source).toContain("const legacyToolbarStyle: React.CSSProperties = {");
expect(source).toContain("gridTemplateColumns: 'repeat(5, minmax(0, 1fr))'");
expect(source).toContain("justifyItems: 'center'");
expect(source).toContain("const legacyToolbarItemStyle: React.CSSProperties = {");
expect(source).toContain("const legacyToolbarDisabledWrapStyle: React.CSSProperties = {");
expect(source).not.toContain("justifyContent: 'space-between', borderTop: `1px solid ${darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}`, borderBottom: `1px solid ${darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}`, background: darkMode ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.015)' }}>");
});
it('renders the v2 sidebar rail, command search hint, filter tabs and log footer', () => {
const markup = renderToStaticMarkup(<Sidebar uiVersion="v2" sqlLogCount={2341} onCreateConnection={mocks.noop} />);
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
@@ -382,7 +397,8 @@ describe('Sidebar locate toolbar', () => {
expect(markup).toContain('gn-v2-sidebar-log-footer');
expect(markup).toContain('SQL 执行日志');
expect(markup).toContain('2,341');
expect(markup).toContain('gn-v2-rail-action-group');
expect(markup).not.toContain('gn-v2-rail-action-group');
expect(source).toContain('className="gn-v2-rail-primary-actions"');
expect(markup).toContain('data-sidebar-create-group-action="true"');
expect(markup).toContain('data-sidebar-batch-table-action="true"');
expect(markup).toContain('data-sidebar-batch-database-action="true"');
@@ -443,12 +459,12 @@ describe('Sidebar locate toolbar', () => {
expect(css).toMatch(/\.ant-tree \{[^}]*font-size: var\(--gn-sidebar-tree-font-size, var\(--gn-font-size-sm, 12px\)\);/s);
expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree \{[^}]*font-size: var\(--gn-sidebar-tree-font-size, var\(--gn-font-size-sm, 12px\)\);/s);
expect(css).toMatch(/\.gn-v2-tree-title \{[^}]*font-size: var\(--gn-sidebar-tree-font-size, var\(--gn-font-size-sm, 12px\)\);/s);
expect(css).toMatch(/\.gn-v2-tree-title\.is-mono \.gn-v2-tree-label \{[^}]*font-size: clamp\(9px, calc\(var\(--gn-sidebar-tree-font-size, var\(--gn-font-size-sm, 12px\)\) - 1px\), 17px\);/s);
expect(css).toMatch(/\.gn-v2-tree-count \{[^}]*font-size: clamp\(9px, calc\(var\(--gn-sidebar-tree-font-size, var\(--gn-font-size-sm, 12px\)\) - 2px\), 16px\);/s);
expect(css).toMatch(/\.gn-v2-connection-rail \{[^}]*width: calc\(54px \* var\(--gn-ui-scale, 1\)\);[^}]*flex: 0 0 calc\(54px \* var\(--gn-ui-scale, 1\)\);/s);
expect(css).toMatch(/\.gn-v2-rail-item,[^}]*\.gn-v2-rail-tool \{[^}]*width: calc\(64px \* var\(--gn-ui-scale, 1\)\);[^}]*height: calc\(38px \* var\(--gn-ui-scale, 1\)\);[^}]*font-size: var\(--gn-font-size-sm, 12px\);/s);
expect(css).toMatch(/\.gn-v2-tree-title\.is-mono \.gn-v2-tree-label \{[^}]*font-size: inherit;[^}]*font-weight: 400 !important;/s);
expect(css).toMatch(/\.gn-v2-tree-count \{[^}]*font-size: clamp\(10px, calc\(var\(--gn-sidebar-tree-font-size, var\(--gn-font-size-sm, 12px\)\) - 1px\), 16px\);/s);
expect(css).toMatch(/\.gn-v2-connection-rail \{[^}]*width: calc\(38px \* var\(--gn-ui-scale, 1\)\);[^}]*flex: 0 0 calc\(38px \* var\(--gn-ui-scale, 1\)\);/s);
expect(css).toMatch(/\.gn-v2-rail-item,\s*body\[data-ui-version="v2"\] \.gn-v2-rail-tool \{[^}]*width: calc\(36px \* var\(--gn-ui-scale, 1\)\);[^}]*height: calc\(38px \* var\(--gn-ui-scale, 1\)\);[^}]*font-size: var\(--gn-font-size-sm, 12px\);/s);
expect(css).toMatch(/\.gn-v2-rail-tool \{[^}]*height: calc\(32px \* var\(--gn-ui-scale, 1\)\);/s);
expect(css).toMatch(/\.gn-v2-rail-tool \{[^}]*width: calc\(34px \* var\(--gn-ui-scale, 1\)\);/s);
expect(css).toMatch(/\.gn-v2-rail-tool \{[^}]*width: calc\(24px \* var\(--gn-ui-scale, 1\)\);/s);
expect(css).toMatch(/\.gn-v2-active-connection-trigger \{[^}]*height: 34px;[^}]*border: 0;[^}]*background: transparent;/s);
expect(css).not.toContain('.gn-v2-active-connection-trigger:hover');
});
@@ -921,6 +937,24 @@ describe('Sidebar locate toolbar', () => {
});
});
it('keeps legacy sidebar table groups flat and ignores v2 pin sections', () => {
const tableNodes = [
{ title: 'orders', key: 'orders', type: 'table' as const, dataRef: { pinnedSidebarTable: true } },
{ title: 'users', key: 'users', type: 'table' as const, dataRef: { pinnedSidebarTable: false } },
];
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
expect(buildSidebarTableChildrenForUi('conn-main-tables', tableNodes, false)).toBe(tableNodes);
expect(buildSidebarTableChildrenForUi('conn-main-tables', tableNodes, true).map((node) => node.title)).toEqual([
'置顶',
'orders',
'全部',
'users',
]);
expect(source).toContain('pinnedSidebarTables: isV2Ui ? currentPinnedSidebarTables : []');
expect(source).toContain('buildSidebarTableChildrenForUi(groupNodeKey, children, isV2Ui)');
});
it('keeps v2 table sections out of regular table lists when nothing is pinned', () => {
const tableNodes = [
{ title: 'users', key: 'users', type: 'table' as const, dataRef: { pinnedSidebarTable: false } },

View File

@@ -151,6 +151,7 @@ type SidebarTableEntryForSort = {
tableName: string;
schemaName?: string;
displayName: string;
rowCount?: number;
};
export const isSidebarTablePinned = (
@@ -227,6 +228,22 @@ export const buildV2SidebarTableSectionedChildren = (
];
};
export const buildSidebarTableChildrenForUi = (
parentKey: string,
tableNodes: TreeNode[],
isV2Ui: boolean,
): TreeNode[] => {
if (!isV2Ui) return tableNodes;
return buildV2SidebarTableSectionedChildren(parentKey, tableNodes);
};
export const formatSidebarRowCount = (count: number): string => {
if (!Number.isFinite(count) || count < 0) return '';
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`;
return String(Math.round(count));
};
type BatchTableExportMode = 'schema' | 'backup' | 'dataOnly';
type BatchObjectType = 'table' | 'view';
type BatchObjectFilterType = 'all' | BatchObjectType;
@@ -1543,6 +1560,80 @@ const Sidebar: React.FC<{
return '';
};
const parseMetadataRowCount = (row: Record<string, any>): number | undefined => {
const rawValue = getCaseInsensitiveRawValue(row, ['Rows', 'table_rows', 'TABLE_ROWS', 'num_rows', 'reltuples', 'total_rows']);
if (rawValue === undefined || rawValue === null || rawValue === '') {
return undefined;
}
const parsed = Number(String(rawValue).replace(/,/g, ''));
if (!Number.isFinite(parsed) || parsed < 0) {
return undefined;
}
return Math.round(parsed);
};
const buildSidebarTableStatusSQL = (conn: SavedConnection, dbName: string): string => {
const dialect = getMetadataDialect(conn);
const safeDbName = escapeSQLLiteral(dbName);
switch (dialect) {
case 'mysql':
case 'starrocks':
return [
'SELECT TABLE_NAME AS table_name, TABLE_ROWS AS table_rows',
'FROM information_schema.tables',
`WHERE table_schema = '${safeDbName}'`,
"AND table_type = 'BASE TABLE'",
'ORDER BY table_name',
].join('\n');
case 'postgres':
case 'kingbase':
case 'vastbase':
case 'highgo':
case 'opengauss':
return [
"SELECT n.nspname || '.' || c.relname AS table_name, c.reltuples::bigint AS table_rows",
'FROM pg_class c',
'JOIN pg_namespace n ON n.oid = c.relnamespace',
"WHERE c.relkind = 'r'",
"AND n.nspname NOT IN ('information_schema', 'pg_catalog')",
"AND n.nspname NOT LIKE 'pg\\_%' ESCAPE '\\'",
'ORDER BY n.nspname, c.relname',
].join('\n');
case 'sqlserver': {
const safeDb = quoteSqlServerIdentifier(dbName);
return [
'SELECT s.name + \'.\' + t.name AS table_name, 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.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',
'ORDER BY s.name, t.name',
].join('\n');
}
case 'clickhouse':
return [
'SELECT name AS table_name, total_rows AS table_rows',
'FROM system.tables',
`WHERE database = '${safeDbName}'`,
"AND engine NOT IN ('View', 'MaterializedView')",
'ORDER BY name',
].join('\n');
case 'oracle':
case 'dm': {
const owner = escapeSQLLiteral(dbName).toUpperCase();
return [
'SELECT table_name, num_rows AS table_rows',
'FROM all_tables',
`WHERE owner = '${owner}'`,
'ORDER BY table_name',
].join('\n');
}
default:
return '';
}
};
const buildQualifiedName = (schemaName: string, objectName: string): string => {
const schema = String(schemaName || '').trim();
const name = String(objectName || '').trim();
@@ -2310,6 +2401,24 @@ const Sidebar: React.FC<{
setConnectionStates(prev => ({ ...prev, [key as string]: 'success' }));
const tableRows: any[] = Array.isArray(res.data) ? res.data : [];
const tableStatusSql = buildSidebarTableStatusSQL(conn as SavedConnection, conn.dbName);
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>();
if (tableStatsResult?.success && Array.isArray(tableStatsResult.data)) {
tableStatsResult.data.forEach((row: Record<string, any>) => {
const rawTableName = String(
getCaseInsensitiveValue(row, ['table_name', 'TABLE_NAME', 'Name', 'name'])
|| getMySQLShowTablesName(row)
|| ''
).trim();
if (!rawTableName) return;
const rowCount = parseMetadataRowCount(row);
if (rowCount === undefined) return;
tableRowCountMap.set(rawTableName.toLowerCase(), rowCount);
});
}
const tableEntries = tableRows.map((row: any) => {
const tableName = Object.values(row)[0] as string;
const parsed = splitQualifiedName(tableName);
@@ -2317,6 +2426,7 @@ const Sidebar: React.FC<{
tableName,
schemaName: parsed.schemaName,
displayName: getSidebarTableDisplayName(conn, tableName),
rowCount: tableRowCountMap.get(String(tableName || '').trim().toLowerCase()),
};
});
@@ -2454,7 +2564,7 @@ const Sidebar: React.FC<{
dbName: conn.dbName,
sortBy,
tableAccessCount: currentTableAccessCount,
pinnedSidebarTables: currentPinnedSidebarTables,
pinnedSidebarTables: isV2Ui ? currentPinnedSidebarTables : [],
});
// Sort views by name (case-insensitive)
@@ -2470,14 +2580,26 @@ const Sidebar: React.FC<{
eventEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
const buildTableNode = (entry: { tableName: string; schemaName: string; displayName: string }): TreeNode => {
const isPinned = isSidebarTablePinned(currentPinnedSidebarTables, conn.id, conn.dbName, entry.tableName, entry.schemaName);
const buildTableNode = (entry: { tableName: string; schemaName: string; displayName: string; rowCount?: number }): TreeNode => {
const isPinned = isV2Ui && isSidebarTablePinned(
currentPinnedSidebarTables,
conn.id,
conn.dbName,
entry.tableName,
entry.schemaName,
);
return {
title: entry.displayName,
key: `${conn.id}-${conn.dbName}-${entry.tableName}`,
icon: <TableOutlined />,
type: 'table',
dataRef: { ...conn, tableName: entry.tableName, schemaName: entry.schemaName, pinnedSidebarTable: isPinned },
dataRef: {
...conn,
tableName: entry.tableName,
schemaName: entry.schemaName,
rowCount: entry.rowCount,
...(isPinned ? { pinnedSidebarTable: true } : {}),
},
isLeaf: false,
};
};
@@ -2537,7 +2659,7 @@ const Sidebar: React.FC<{
): TreeNode => {
const groupNodeKey = `${parentKey}-${groupKey}`;
const groupedChildren = groupKey === 'tables'
? buildV2SidebarTableSectionedChildren(groupNodeKey, children)
? buildSidebarTableChildrenForUi(groupNodeKey, children, isV2Ui)
: children;
return {
title: groupTitle,
@@ -5483,7 +5605,7 @@ const Sidebar: React.FC<{
return filterV2ExplorerTreeByKind(activeConnectionTreeData, v2ExplorerFilter);
}, [activeConnectionTreeData, displayTreeData, v2ExplorerFilter]);
const v2TreeMetrics = useMemo(() => {
const databaseObjectCounts = new Map<React.Key, number>();
const databaseTableCounts = new Map<React.Key, number>();
const objectGroupCounts = new Map<React.Key, number>();
let activeObjectCount = 0;
@@ -5491,7 +5613,21 @@ const Sidebar: React.FC<{
const childCount = (node.children || []).reduce((total, child) => total + visitAndCount(child), 0);
const totalCount = (isV2SidebarObjectNode(node) ? 1 : 0) + childCount;
if (node.type === 'database') {
databaseObjectCounts.set(node.key, childCount);
const tableCount = (node.children || []).reduce((total, child) => {
if (child.type === 'object-group' && child?.dataRef?.groupKey === 'tables') {
return total + (Array.isArray(child.children) ? child.children.filter((item) => item.type === 'table').length : 0);
}
if (child?.dataRef?.groupKey === 'schema' && Array.isArray(child.children)) {
return total + child.children.reduce((schemaTotal, schemaChild) => {
if (schemaChild.type === 'object-group' && schemaChild?.dataRef?.groupKey === 'tables') {
return schemaTotal + (Array.isArray(schemaChild.children) ? schemaChild.children.filter((item) => item.type === 'table').length : 0);
}
return schemaTotal;
}, 0);
}
return total;
}, 0);
databaseTableCounts.set(node.key, tableCount);
} else if (node.type === 'object-group') {
objectGroupCounts.set(node.key, childCount);
}
@@ -5502,11 +5638,34 @@ const Sidebar: React.FC<{
return {
activeObjectCount,
databaseObjectCounts,
databaseTableCounts,
objectGroupCounts,
};
}, [v2VisibleTreeData]);
const activeConnectionObjectCount = v2TreeMetrics.activeObjectCount;
const legacyToolbarButtonColor = darkMode ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,0.65)';
const legacyToolbarStyle: React.CSSProperties = {
padding: '6px 16px',
display: 'grid',
gridTemplateColumns: 'repeat(5, minmax(0, 1fr))',
gap: 8,
alignItems: 'center',
justifyItems: 'center',
borderTop: `1px solid ${darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}`,
borderBottom: `1px solid ${darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}`,
background: darkMode ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.015)',
};
const legacyToolbarItemStyle: React.CSSProperties = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minWidth: 0,
};
const legacyToolbarDisabledWrapStyle: React.CSSProperties = {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
};
const connectionStatusMap = useMemo(() => {
const statusMap = new Map<string, 'live' | 'error' | 'idle'>();
@@ -5656,7 +5815,7 @@ const Sidebar: React.FC<{
return count > 0 ? count.toLocaleString() : '';
}
if (node.type === 'database') {
const count = v2TreeMetrics.databaseObjectCounts.get(node.key) || 0;
const count = v2TreeMetrics.databaseTableCounts.get(node.key) || 0;
return count > 0 ? count.toLocaleString() : '';
}
if (node.type === 'object-group') {
@@ -5668,9 +5827,8 @@ const Sidebar: React.FC<{
return match?.[1] || '';
}
if (node.type === 'table') {
const key = `${node?.dataRef?.id}-${node?.dataRef?.dbName}-${node?.dataRef?.tableName}`;
const count = tableAccessCount[key] || 0;
return count > 0 ? count.toLocaleString() : '';
const rowCount = Number(node?.dataRef?.rowCount);
return Number.isFinite(rowCount) && rowCount >= 0 ? formatSidebarRowCount(rowCount) : '';
}
return '';
};
@@ -7766,65 +7924,75 @@ const Sidebar: React.FC<{
{/* Toolbar */}
{!isV2Ui && (
<div style={{ padding: '6px 16px', display: 'flex', gap: 8, justifyContent: 'space-between', borderTop: `1px solid ${darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}`, borderBottom: `1px solid ${darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}`, background: darkMode ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.015)' }}>
<Tooltip title="新建组">
<Button
size="small"
type="text"
icon={<FolderOpenOutlined />}
aria-label="新建组"
data-sidebar-create-group-action="true"
onClick={() => { setRenameViewTarget(null); createTagForm.resetFields(); setIsCreateTagModalOpen(true); }}
style={{ color: darkMode ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,0.65)' }}
/>
</Tooltip>
<Tooltip title="批量操作表">
<Button
size="small"
type="text"
icon={<TableOutlined />}
aria-label="批量操作表"
data-sidebar-batch-table-action="true"
onClick={() => openBatchOperationModal()}
style={{ color: darkMode ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,0.65)' }}
/>
</Tooltip>
<Tooltip title="批量操作库">
<Button
size="small"
type="text"
icon={<DatabaseOutlined />}
aria-label="批量操作库"
data-sidebar-batch-database-action="true"
onClick={() => openBatchDatabaseModal()}
style={{ color: darkMode ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,0.65)' }}
/>
</Tooltip>
<Tooltip title="运行外部SQL文件">
<Button
size="small"
type="text"
icon={<FileAddOutlined />}
aria-label="运行外部 SQL 文件"
data-sidebar-open-external-sql-file-action="true"
onClick={handleOpenSQLFileFromToolbar}
style={{ color: darkMode ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,0.65)' }}
/>
</Tooltip>
<Tooltip title={canLocateActiveTab ? '定位当前打开表' : '当前标签页没有可定位的表'}>
<span>
<div data-sidebar-legacy-toolbar="true" style={legacyToolbarStyle}>
<div data-sidebar-legacy-toolbar-item="true" style={legacyToolbarItemStyle}>
<Tooltip title="新建组">
<Button
size="small"
type="text"
icon={<AimOutlined />}
aria-label="定位当前打开表"
data-sidebar-locate-current-tab-action="true"
disabled={!canLocateActiveTab}
onClick={handleLocateActiveTabInSidebar}
style={{ color: darkMode ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,0.65)' }}
icon={<FolderOpenOutlined />}
aria-label="新建组"
data-sidebar-create-group-action="true"
onClick={() => { setRenameViewTarget(null); createTagForm.resetFields(); setIsCreateTagModalOpen(true); }}
style={{ color: legacyToolbarButtonColor }}
/>
</span>
</Tooltip>
</Tooltip>
</div>
<div data-sidebar-legacy-toolbar-item="true" style={legacyToolbarItemStyle}>
<Tooltip title="批量操作表">
<Button
size="small"
type="text"
icon={<TableOutlined />}
aria-label="批量操作表"
data-sidebar-batch-table-action="true"
onClick={() => openBatchOperationModal()}
style={{ color: legacyToolbarButtonColor }}
/>
</Tooltip>
</div>
<div data-sidebar-legacy-toolbar-item="true" style={legacyToolbarItemStyle}>
<Tooltip title="批量操作库">
<Button
size="small"
type="text"
icon={<DatabaseOutlined />}
aria-label="批量操作库"
data-sidebar-batch-database-action="true"
onClick={() => openBatchDatabaseModal()}
style={{ color: legacyToolbarButtonColor }}
/>
</Tooltip>
</div>
<div data-sidebar-legacy-toolbar-item="true" style={legacyToolbarItemStyle}>
<Tooltip title="运行外部SQL文件">
<Button
size="small"
type="text"
icon={<FileAddOutlined />}
aria-label="运行外部 SQL 文件"
data-sidebar-open-external-sql-file-action="true"
onClick={handleOpenSQLFileFromToolbar}
style={{ color: legacyToolbarButtonColor }}
/>
</Tooltip>
</div>
<div data-sidebar-legacy-toolbar-item="true" style={legacyToolbarItemStyle}>
<Tooltip title={canLocateActiveTab ? '定位当前打开表' : '当前标签页没有可定位的表'}>
<span style={legacyToolbarDisabledWrapStyle}>
<Button
size="small"
type="text"
icon={<AimOutlined />}
aria-label="定位当前打开表"
data-sidebar-locate-current-tab-action="true"
disabled={!canLocateActiveTab}
onClick={handleLocateActiveTabInSidebar}
style={{ color: legacyToolbarButtonColor }}
/>
</span>
</Tooltip>
</div>
</div>
)}

View File

@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest';
import { formatSidebarRowCount } from './Sidebar';
describe('Sidebar v2 metadata', () => {
it('formats table row counts for sidebar labels', () => {
expect(formatSidebarRowCount(-1)).toBe('');
expect(formatSidebarRowCount(0)).toBe('0');
expect(formatSidebarRowCount(27)).toBe('27');
expect(formatSidebarRowCount(1532)).toBe('1.5K');
expect(formatSidebarRowCount(2_450_000)).toBe('2.5M');
});
});