feat(sidebar): 优化窄宽度连接头部展示

- 移除连接头部状态徽章,释放连接名与库名显示空间
- 窄宽度时收起新建查询文字并保持操作按钮可用
- 连接名或库名截断时悬浮同时展示完整信息
- 补充响应式布局、主题对比度与悬浮提示回归测试
This commit is contained in:
Syngnat
2026-07-24 14:46:24 +08:00
parent 3bb0866633
commit 1e4ece74a7
5 changed files with 85 additions and 81 deletions

View File

@@ -1325,6 +1325,10 @@ describe('Sidebar locate toolbar', () => {
expect(css).toMatch(/\.gn-v2-rail-tool \{[^}]*width: calc\(24px \* var\(--gn-v2-rail-scale\)\);/s);
expect(css).toMatch(/\.gn-v2-active-connection-trigger \{[^}]*height: 34px;[^}]*border: 0;[^}]*background: transparent;/s);
expect(css).toMatch(/\.gn-v2-active-connection-query-action \{[^}]*max-width: 96px;[^}]*font-size: 12px;/s);
expect(css).toMatch(/\.gn-v2-active-connection-tooltip \{[^}]*display: flex;[^}]*flex-direction: column;[^}]*gap: 2px;[^}]*max-width: min\(320px, calc\(100vw - 24px\)\);[^}]*overflow-wrap: anywhere;/s);
expect(css).toMatch(/\.gn-v2-object-explorer \{[^}]*container-type: inline-size;[^}]*container-name: gn-v2-object-explorer;/s);
expect(css).toMatch(/@container gn-v2-object-explorer \(max-width: 440px\) \{[\s\S]*\.gn-v2-active-connection-query-action \{[^}]*width: 28px;[^}]*max-width: 28px;[^}]*padding: 0;/s);
expect(css).toMatch(/@container gn-v2-object-explorer \(max-width: 440px\) \{[\s\S]*\.gn-v2-active-connection-query-action > span:not\(\.anticon\):not\(\.ant-btn-icon\) \{[^}]*display: none;/s);
expect(css).not.toContain('.gn-v2-active-connection-trigger:hover');
});
@@ -1384,7 +1388,6 @@ describe('Sidebar locate toolbar', () => {
const source = readSidebarSource();
const treeLoaderSource = readSourceFile('./sidebar/useSidebarTreeLoaders.tsx');
const titleRenderSource = readSourceFile('./sidebar/useSidebarTitleRender.tsx');
const v2ContextMenuSource = readSourceFile('./sidebar/useSidebarV2ContextMenu.tsx');
expect(source).toContain("export type SidebarConnectionState = 'loading' | 'success' | 'error';");
expect(treeLoaderSource).toContain("setConnectionStates(prev => ({ ...prev, [conn.id]: 'loading' }));");
@@ -1398,8 +1401,6 @@ describe('Sidebar locate toolbar', () => {
expect(treeLoaderSource).toContain("setConnectionStates(prev => ({ ...prev, [key as string]: 'success' }));");
expect(titleRenderSource).toContain("let status: 'loading' | 'success' | 'error' | 'default' = 'default';");
expect(titleRenderSource).toContain("if (connectionStates[node.key] === 'loading') status = 'loading';");
expect(v2ContextMenuSource).toContain("const statusMap = new Map<string, 'loading' | 'live' | 'error' | 'idle'>();");
expect(v2ContextMenuSource).toContain("value === 'loading' ? 'loading'");
expect(css).toMatch(/\.gn-v2-tree-status\.is-loading::before \{[^}]*border: 2px solid rgba\(37, 99, 235, 0\.24\);[^}]*animation: gn-v2-tree-status-spin 0\.8s linear infinite;/s);
expect(css).toMatch(/@keyframes gn-v2-tree-status-spin \{[^}]*to \{ transform: rotate\(360deg\); \}/s);
});
@@ -1555,8 +1556,11 @@ describe('Sidebar locate toolbar', () => {
expect(markup).toContain('gn-v2-connection-rail');
expect(markup).toContain('gn-v2-active-connection-copy');
expect(markup).toContain('<strong>本地</strong>');
expect(markup).toContain('<span>app_db</span>');
expect(markup).toMatch(/<strong aria-describedby="[^"]+">本地<\/strong>/);
expect(markup).toMatch(/<span aria-describedby="[^"]+">app_db<\/span>/);
expect(markup).not.toContain('title="本地"');
expect(markup).not.toContain('title="app_db"');
expect(markup).not.toContain('gn-v2-live-dot');
expect(markup).not.toContain('<span>localhost</span>');
expect(markup).not.toContain('gn-v2-db-icon-label');
});
@@ -1586,8 +1590,10 @@ describe('Sidebar locate toolbar', () => {
const markup = renderSidebarMarkup({ uiVersion: 'v2' });
expect(markup).toContain(`<strong>${t('sidebar.active_connection.no_host_selected')}</strong>`);
expect(markup).toContain(`<span>${t('sidebar.active_connection.no_database_selected')}</span>`);
expect(markup).toMatch(new RegExp(`<strong aria-describedby="[^"]+">${t('sidebar.active_connection.no_host_selected')}</strong>`));
expect(markup).toMatch(new RegExp(`<span aria-describedby="[^"]+">${t('sidebar.active_connection.no_database_selected')}</span>`));
expect(markup).not.toContain(`title="${t('sidebar.active_connection.no_host_selected')}"`);
expect(markup).not.toContain(`title="${t('sidebar.active_connection.no_database_selected')}"`);
expect(markup).not.toContain('<strong>本地</strong>');
});
@@ -2803,6 +2809,13 @@ describe('Sidebar locate toolbar', () => {
expect(source).toContain("title: String(node.title || dataRef.dbName || t('database.unnamed'))");
expect(source).toContain("meta: conn?.name || dataRef.id || t('database.label')");
expect(source).toContain("const activeConnectionDisplayName = String(activeConnection?.name || '').trim() || t('sidebar.active_connection.no_host_selected');");
expect(source).toContain('const v2ActiveConnectionTooltipContent = (');
expect(source).toContain('<div className="gn-v2-active-connection-tooltip">');
expect(source).toContain('<strong>{activeConnectionDisplayName}</strong>');
expect(source).toContain('<span>{activeDatabaseDisplayName || v2NoDatabaseSelectedLabel}</span>');
expect(source.match(/<Tooltip title=\{v2ActiveConnectionTooltipContent\} placement="bottomLeft" mouseEnterDelay=\{0\.35\}>/g)).toHaveLength(2);
expect(source).not.toContain('<strong title={activeConnectionDisplayName}>');
expect(source).not.toContain('<span title={activeDatabaseDisplayName || v2NoDatabaseSelectedLabel}>');
expect(utilsSource).toContain("name: item.tag.name || t('connection.sidebar.group.untitled'),");
expect(source).toContain('groupName={group.name}');
expect(source).toContain('count={group.connections.length}');

View File

@@ -2537,7 +2537,6 @@ const Sidebar: React.FC<{
contextMenu,
setContextMenu,
contextMenuPortalRef,
buildRailConnectionStatus,
openV2ConnectionContextMenu,
getV2TreeMetaText,
renderV2SidebarContextMenuContent,
@@ -2545,7 +2544,6 @@ const Sidebar: React.FC<{
refreshV2TableContextMenuStats,
} = useSidebarV2ContextMenu({
connections,
connectionStates,
connectionTags,
activeShortcutPlatform,
flattenConnectionNodes,
@@ -2923,6 +2921,12 @@ const Sidebar: React.FC<{
const v2ActiveConnectionHeaderLabel = t('sidebar.active_connection.current_host_database');
const v2NoDatabaseSelectedLabel = t('sidebar.active_connection.no_database_selected');
const v2ConnectionActionsLabel = t('sidebar.active_connection.actions');
const v2ActiveConnectionTooltipContent = (
<div className="gn-v2-active-connection-tooltip">
<strong>{activeConnectionDisplayName}</strong>
<span>{activeDatabaseDisplayName || v2NoDatabaseSelectedLabel}</span>
</div>
);
const v2CommandSearchLabel = t('sidebar.command_search.label');
const v2CommandSearchPlaceholder = t('sidebar.command_search.placeholder');
@@ -3046,10 +3050,13 @@ const Sidebar: React.FC<{
{isV2Ui && (
<div className="gn-v2-active-connection-header" data-object-count={activeConnectionObjectCount}>
<div className="gn-v2-active-connection-trigger" aria-label={v2ActiveConnectionHeaderLabel}>
<span className={`gn-v2-live-dot is-${activeConnection ? buildRailConnectionStatus(activeConnection.id) : 'idle'}`} />
<div className="gn-v2-active-connection-copy">
<strong>{activeConnectionDisplayName}</strong>
<span>{activeDatabaseDisplayName || v2NoDatabaseSelectedLabel}</span>
<Tooltip title={v2ActiveConnectionTooltipContent} placement="bottomLeft" mouseEnterDelay={0.35}>
<strong>{activeConnectionDisplayName}</strong>
</Tooltip>
<Tooltip title={v2ActiveConnectionTooltipContent} placement="bottomLeft" mouseEnterDelay={0.35}>
<span>{activeDatabaseDisplayName || v2NoDatabaseSelectedLabel}</span>
</Tooltip>
</div>
</div>
<div className="gn-v2-active-connection-actions">

View File

@@ -22,7 +22,7 @@ import { getDataSourceCapabilities } from '../../utils/dataSourceCapabilities';
import { resolveConnectionHostSummary } from '../../utils/tabDisplay';
import { resolveConnectionIconType } from '../../utils/connectionVisual';
import { formatSidebarRowCount } from './sidebarHelpers';
import { isSidebarTablePinned, type SidebarConnectionState, type SidebarTreeNode as TreeNode, type V2RailConnectionGroup } from '../sidebarV2Utils';
import { isSidebarTablePinned, type SidebarTreeNode as TreeNode, type V2RailConnectionGroup } from '../sidebarV2Utils';
import { getTableDataDangerActionMeta, supportsTableTruncateAction } from '../tableDataDangerActions';
import {
SIDEBAR_CONTEXT_MENU_FALLBACK_HEIGHT,
@@ -45,7 +45,6 @@ export type SidebarContextMenuState = {
type SidebarV2ContextMenuOptions = {
connections: SavedConnection[];
connectionStates: Record<string, SidebarConnectionState>;
connectionTags: Array<{ id: string; name: string; connectionIds: string[] }>;
activeShortcutPlatform: any;
flattenConnectionNodes: (nodes: TreeNode[]) => TreeNode[];
@@ -76,7 +75,6 @@ type SidebarV2ContextMenuOptions = {
export const useSidebarV2ContextMenu = ({
connections,
connectionStates,
connectionTags,
activeShortcutPlatform,
flattenConnectionNodes,
@@ -105,33 +103,6 @@ export const useSidebarV2ContextMenu = ({
const contextMenuPortalRef = useRef<HTMLDivElement | null>(null);
const [v2TableContextMenuStats, setV2TableContextMenuStats] = useState<Record<string, V2TableContextMenuStats>>({});
const connectionStatusMap = useMemo(() => {
const statusMap = new Map<string, 'loading' | 'live' | 'error' | 'idle'>();
const sortedConnectionIds = connections
.map((conn) => conn.id)
.sort((a, b) => b.length - a.length);
connections.forEach((conn) => {
statusMap.set(conn.id, 'idle');
});
Object.entries(connectionStates).forEach(([key, value]) => {
const ownState = statusMap.get(key);
if (ownState !== undefined) {
statusMap.set(key, value === 'loading' ? 'loading' : value === 'success' ? 'live' : 'error');
return;
}
if (value !== 'success') return;
const ownerId = sortedConnectionIds.find((id) => key.startsWith(`${id}-`));
if (ownerId && statusMap.get(ownerId) === 'idle') {
statusMap.set(ownerId, 'live');
}
});
return statusMap;
}, [connectionStates, connections]);
const buildRailConnectionStatus = useCallback((connectionId: string): 'loading' | 'live' | 'error' | 'idle' => {
return connectionStatusMap.get(connectionId) || 'idle';
}, [connectionStatusMap]);
const openV2ConnectionContextMenu = (
event: React.MouseEvent,
connOrNode: SavedConnection | TreeNode,
@@ -529,7 +500,6 @@ export const useSidebarV2ContextMenu = ({
contextMenu,
setContextMenu,
contextMenuPortalRef,
buildRailConnectionStatus,
openV2ConnectionContextMenu,
getV2TreeMetaText,
renderV2SidebarContextMenuContent,

View File

@@ -37,15 +37,13 @@ describe('v2 theme connection identity contrast', () => {
const dark = readRuleBlock('body[data-ui-version="v2"][data-theme="dark"]');
const host = readRuleBlock('body[data-ui-version="v2"] .gn-v2-tab-label-part-host');
const database = readRuleBlock('body[data-ui-version="v2"] .gn-v2-tab-label-part-database');
const liveDot = readRuleBlock('body[data-ui-version="v2"] .gn-v2-live-dot');
const treeDot = readRuleBlock('body[data-ui-version="v2"] .gn-v2-tree-status::before');
expect(host).toContain('color: var(--gn-info);');
expect(database).toContain('color: var(--gn-accent);');
expect(liveDot).toContain('width: 10px;');
expect(liveDot).toContain('height: 10px;');
expect(treeDot).toContain('width: 9px;');
expect(treeDot).toContain('height: 9px;');
expect(source).not.toContain('.gn-v2-live-dot');
expect(source).toContain('background: var(--gn-status-connected);');
for (const [mode, block] of [['light', light], ['dark', dark]] as const) {

View File

@@ -2144,6 +2144,8 @@ body[data-ui-version="v2"] .gn-v2-object-explorer {
flex: 1 1 calc(100% - calc(38px * var(--gn-v2-rail-scale)));
background: var(--gn-bg-panel-2);
overflow: hidden;
container-type: inline-size;
container-name: gn-v2-object-explorer;
}
body[data-ui-version="v2"] .gn-v2-active-connection-header {
@@ -2152,7 +2154,7 @@ body[data-ui-version="v2"] .gn-v2-active-connection-header {
border-bottom: 0.5px solid var(--gn-br-1);
display: flex;
align-items: center;
gap: 8px;
gap: 6px;
}
body[data-ui-version="v2"] .gn-v2-active-connection-trigger {
@@ -2161,8 +2163,7 @@ body[data-ui-version="v2"] .gn-v2-active-connection-trigger {
height: 34px;
display: flex;
align-items: center;
gap: 8px;
padding: 0 9px;
padding: 0 4px;
border: 0;
border-radius: 0;
background: transparent;
@@ -2170,37 +2171,6 @@ body[data-ui-version="v2"] .gn-v2-active-connection-trigger {
text-align: left;
}
body[data-ui-version="v2"] .gn-v2-live-dot {
width: 10px;
height: 10px;
box-sizing: border-box;
border: 1.5px solid var(--gn-fg-4);
border-radius: 50%;
flex-shrink: 0;
background: transparent;
}
body[data-ui-version="v2"] .gn-v2-live-dot.is-live {
border: 0;
background: var(--gn-status-connected);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--gn-status-connected) 22%, transparent);
}
body[data-ui-version="v2"] .gn-v2-live-dot.is-loading {
width: 10px;
height: 10px;
border: 2px solid rgba(37, 99, 235, 0.24);
border-top-color: #2563eb;
background: transparent;
animation: gn-v2-tree-status-spin 0.8s linear infinite;
}
body[data-ui-version="v2"] .gn-v2-live-dot.is-error {
border: 0;
background: var(--gn-danger);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--gn-danger) 20%, transparent);
}
body[data-ui-version="v2"] .gn-v2-active-connection-copy {
min-width: 0;
flex: 1 1 auto;
@@ -2232,6 +2202,29 @@ body[data-ui-version="v2"] .gn-v2-active-connection-copy span {
line-height: 1.2;
}
body[data-ui-version="v2"] .gn-v2-active-connection-tooltip {
display: flex;
flex-direction: column;
gap: 2px;
max-width: min(320px, calc(100vw - 24px));
overflow-wrap: anywhere;
font-family: var(--gn-font-sans);
letter-spacing: 0;
}
body[data-ui-version="v2"] .gn-v2-active-connection-tooltip strong {
color: var(--gn-fg-1);
font-size: 12px;
font-weight: 600;
line-height: 1.35;
}
body[data-ui-version="v2"] .gn-v2-active-connection-tooltip span {
color: var(--gn-fg-3);
font-size: 11px;
line-height: 1.35;
}
body[data-ui-version="v2"] .gn-v2-active-connection-actions {
display: inline-flex;
align-items: center;
@@ -2246,13 +2239,36 @@ body[data-ui-version="v2"] .gn-v2-active-connection-query-action {
font-weight: 500;
}
body[data-ui-version="v2"] .gn-v2-active-connection-query-action > span:not(.anticon) {
body[data-ui-version="v2"] .gn-v2-active-connection-query-action > span:not(.anticon):not(.ant-btn-icon) {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@container gn-v2-object-explorer (max-width: 440px) {
body[data-ui-version="v2"] .gn-v2-active-connection-header {
padding-right: 8px;
padding-left: 10px;
gap: 4px;
}
body[data-ui-version="v2"] .gn-v2-active-connection-actions {
gap: 2px;
}
body[data-ui-version="v2"] .gn-v2-active-connection-query-action {
width: 28px;
min-width: 28px;
max-width: 28px;
padding: 0;
}
body[data-ui-version="v2"] .gn-v2-active-connection-query-action > span:not(.anticon):not(.ant-btn-icon) {
display: none;
}
}
body[data-ui-version="v2"] .gn-v2-explorer-search {
padding: 10px 10px 6px !important;
border-bottom: none !important;