From 1e4ece74a7324c8be97320ca9fb6e99cfbaede85 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 24 Jul 2026 14:46:24 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(sidebar):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=AA=84=E5=AE=BD=E5=BA=A6=E8=BF=9E=E6=8E=A5=E5=A4=B4=E9=83=A8?= =?UTF-8?q?=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除连接头部状态徽章,释放连接名与库名显示空间 - 窄宽度时收起新建查询文字并保持操作按钮可用 - 连接名或库名截断时悬浮同时展示完整信息 - 补充响应式布局、主题对比度与悬浮提示回归测试 --- .../Sidebar.locate-toolbar.test.tsx | 27 ++++-- frontend/src/components/Sidebar.tsx | 17 ++-- .../sidebar/useSidebarV2ContextMenu.tsx | 32 +------ frontend/src/v2-theme.contrast.test.ts | 4 +- frontend/src/v2-theme.css | 86 +++++++++++-------- 5 files changed, 85 insertions(+), 81 deletions(-) diff --git a/frontend/src/components/Sidebar.locate-toolbar.test.tsx b/frontend/src/components/Sidebar.locate-toolbar.test.tsx index 06922a5e..ab391668 100644 --- a/frontend/src/components/Sidebar.locate-toolbar.test.tsx +++ b/frontend/src/components/Sidebar.locate-toolbar.test.tsx @@ -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();"); - 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('本地'); - expect(markup).toContain('app_db'); + expect(markup).toMatch(/本地<\/strong>/); + expect(markup).toMatch(/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('localhost'); 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(`${t('sidebar.active_connection.no_host_selected')}`); - expect(markup).toContain(`${t('sidebar.active_connection.no_database_selected')}`); + expect(markup).toMatch(new RegExp(`${t('sidebar.active_connection.no_host_selected')}`)); + expect(markup).toMatch(new RegExp(`${t('sidebar.active_connection.no_database_selected')}`)); + 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('本地'); }); @@ -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('
'); + expect(source).toContain('{activeConnectionDisplayName}'); + expect(source).toContain('{activeDatabaseDisplayName || v2NoDatabaseSelectedLabel}'); + expect(source.match(//g)).toHaveLength(2); + expect(source).not.toContain(''); + expect(source).not.toContain(''); 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}'); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 52c92d18..e5c2f495 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -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 = ( +
+ {activeConnectionDisplayName} + {activeDatabaseDisplayName || v2NoDatabaseSelectedLabel} +
+ ); const v2CommandSearchLabel = t('sidebar.command_search.label'); const v2CommandSearchPlaceholder = t('sidebar.command_search.placeholder'); @@ -3046,10 +3050,13 @@ const Sidebar: React.FC<{ {isV2Ui && (
-
- {activeConnectionDisplayName} - {activeDatabaseDisplayName || v2NoDatabaseSelectedLabel} + + {activeConnectionDisplayName} + + + {activeDatabaseDisplayName || v2NoDatabaseSelectedLabel} +
diff --git a/frontend/src/components/sidebar/useSidebarV2ContextMenu.tsx b/frontend/src/components/sidebar/useSidebarV2ContextMenu.tsx index 7a762c03..c35c4928 100644 --- a/frontend/src/components/sidebar/useSidebarV2ContextMenu.tsx +++ b/frontend/src/components/sidebar/useSidebarV2ContextMenu.tsx @@ -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; 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(null); const [v2TableContextMenuStats, setV2TableContextMenuStats] = useState>({}); - const connectionStatusMap = useMemo(() => { - const statusMap = new Map(); - 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, diff --git a/frontend/src/v2-theme.contrast.test.ts b/frontend/src/v2-theme.contrast.test.ts index 3312f1cd..db3acc3a 100644 --- a/frontend/src/v2-theme.contrast.test.ts +++ b/frontend/src/v2-theme.contrast.test.ts @@ -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) { diff --git a/frontend/src/v2-theme.css b/frontend/src/v2-theme.css index 11fd1920..a279b689 100644 --- a/frontend/src/v2-theme.css +++ b/frontend/src/v2-theme.css @@ -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;