From 55829bce86b870788af809bcde7a71da85b8d64b Mon Sep 17 00:00:00 2001 From: Syngnat Date: Sun, 26 Apr 2026 19:33:12 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(connection):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E8=BF=9E=E6=8E=A5=E9=A2=9C=E8=89=B2=E9=87=8D=E5=90=AF?= =?UTF-8?q?=E4=B8=A2=E5=A4=B1=E5=B9=B6=E5=90=8C=E6=AD=A5=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E9=A1=B5=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 恢复连接清洗流程中的图标类型与颜色字段 - 标签页增加连接色标识,便于区分多连接会话 - 抽取连接视觉解析并补充回归测试 Refs #334 --- frontend/src/components/Sidebar.tsx | 5 ++- frontend/src/components/TabManager.tsx | 33 ++++++++++++++- frontend/src/store.test.ts | 23 +++++++++++ frontend/src/store.ts | 14 +++++++ frontend/src/utils/connectionVisual.test.ts | 46 +++++++++++++++++++++ frontend/src/utils/connectionVisual.ts | 40 ++++++++++++++++++ 6 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 frontend/src/utils/connectionVisual.test.ts create mode 100644 frontend/src/utils/connectionVisual.ts diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 7c13fb2..81edd57 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -48,6 +48,7 @@ import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig'; import { noAutoCapInputProps } from '../utils/inputAutoCap'; import { normalizeSidebarViewName, resolveSidebarRuntimeDatabase } from '../utils/sidebarMetadata'; import { resolveConnectionHostTokens } from '../utils/tabDisplay'; +import { resolveConnectionAccentColor, resolveConnectionIconType } from '../utils/connectionVisual'; import { buildJVMTabTitle } from '../utils/jvmRuntimePresentation'; import { buildJVMDiagnosticActionDescriptor, buildJVMMonitoringActionDescriptors } from '../utils/jvmSidebarActions'; import { buildTableSelectQuery } from '../utils/objectQueryTemplates'; @@ -358,10 +359,12 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> const buildConnectionNode = (conn: SavedConnection): TreeNode => { const existing = prevMap.get(conn.id); + const iconType = resolveConnectionIconType(conn); + const iconColor = resolveConnectionAccentColor(conn); return { title: conn.name, key: conn.id, - icon: getDbIcon(conn.iconType || conn.config.type, conn.iconColor, 22), + icon: getDbIcon(iconType, iconColor, 22), type: 'connection', dataRef: conn, isLeaf: false, diff --git a/frontend/src/components/TabManager.tsx b/frontend/src/components/TabManager.tsx index 7f480cb..d17f453 100644 --- a/frontend/src/components/TabManager.tsx +++ b/frontend/src/components/TabManager.tsx @@ -23,24 +23,33 @@ import JVMDiagnosticConsole from './JVMDiagnosticConsole'; import JVMMonitoringDashboard from './JVMMonitoringDashboard'; import type { TabData } from '../types'; import { buildTabDisplayTitle } from '../utils/tabDisplay'; +import { resolveConnectionAccentColor } from '../utils/connectionVisual'; type SortableTabLabelProps = { displayTitle: string; menuItems: MenuProps['items']; + accentColor?: string; }; const SortableTabLabel: React.FC = ({ displayTitle, menuItems, + accentColor, }) => { + const labelStyle = accentColor + ? ({ '--connection-accent': accentColor } as React.CSSProperties) + : undefined; + return ( e.preventDefault()} title={displayTitle} + style={labelStyle} > - {displayTitle} + {accentColor ? ); @@ -188,6 +197,7 @@ const TabManager: React.FC = () => { const items = useMemo(() => tabs.map((tab, index) => { const connection = connections.find((conn) => conn.id === tab.connectionId); const displayTitle = buildTabDisplayTitle(tab, connection); + const accentColor = connection ? resolveConnectionAccentColor(connection) : undefined; const tabIsActive = tab.id === activeTabId; let content; if (tab.type === 'query') { @@ -253,6 +263,7 @@ const TabManager: React.FC = () => { ), key: tab.id, @@ -317,8 +328,26 @@ const TabManager: React.FC = () => { -webkit-user-select: none; display: inline-flex; align-items: center; + gap: 7px; max-width: 100%; } + .main-tabs .tab-dnd-label.has-connection-accent { + position: relative; + } + .main-tabs .tab-connection-accent { + width: 9px; + height: 9px; + border-radius: 999px; + background: var(--connection-accent); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--connection-accent) 22%, transparent); + flex: 0 0 auto; + } + .main-tabs .tab-title-text { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } .main-tabs .tab-dnd-node.is-dragging, .main-tabs .tab-dnd-node.is-dragging .tab-dnd-label { cursor: grabbing !important; diff --git a/frontend/src/store.test.ts b/frontend/src/store.test.ts index 248270e..727b105 100644 --- a/frontend/src/store.test.ts +++ b/frontend/src/store.test.ts @@ -164,6 +164,29 @@ describe('store appearance persistence', () => { }); }); + it('preserves connection icon metadata when replacing saved connections', async () => { + const { useStore } = await importStore(); + + useStore.getState().replaceConnections([ + { + id: 'visual-1', + name: 'Visual Orders', + iconType: 'postgres', + iconColor: '#2f855a', + config: { + id: 'visual-1', + type: 'mysql', + host: 'db.local', + port: 3306, + user: 'root', + }, + }, + ]); + + expect(useStore.getState().connections[0]?.iconType).toBe('postgres'); + expect(useStore.getState().connections[0]?.iconColor).toBe('#2f855a'); + }); + it('keeps legacy global proxy password during hydration until explicit cleanup', async () => { storage.setItem('lite-db-storage', JSON.stringify({ state: { diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 58285f7..cd5a22e 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -243,6 +243,18 @@ const sanitizeAddressList = (value: unknown): string[] => { return all.slice(0, MAX_HOST_ENTRIES); }; +const sanitizeConnectionIconType = (value: unknown): string | undefined => { + const iconType = toTrimmedString(value).toLowerCase(); + return iconType || undefined; +}; + +const sanitizeConnectionIconColor = (value: unknown): string | undefined => { + const color = toTrimmedString(value); + return /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(color) + ? color + : undefined; +}; + const normalizeConnectionType = (value: unknown): string => { const type = toTrimmedString(value).toLowerCase(); if (type === "doris") { @@ -574,6 +586,8 @@ const sanitizeSavedConnection = ( includeDatabases.length > 0 ? includeDatabases : undefined, includeRedisDatabases: includeRedisDatabases.length > 0 ? includeRedisDatabases : undefined, + iconType: sanitizeConnectionIconType(raw.iconType), + iconColor: sanitizeConnectionIconColor(raw.iconColor), }; }; diff --git a/frontend/src/utils/connectionVisual.test.ts b/frontend/src/utils/connectionVisual.test.ts new file mode 100644 index 0000000..f2ba285 --- /dev/null +++ b/frontend/src/utils/connectionVisual.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; + +import type { SavedConnection } from '../types'; +import { + resolveConnectionAccentColor, + resolveConnectionIconType, +} from './connectionVisual'; + +const baseConnection: SavedConnection = { + id: 'conn-1', + name: 'Orders', + config: { + id: 'conn-1', + type: 'mysql', + host: 'db.local', + port: 3306, + user: 'root', + }, +}; + +describe('connectionVisual', () => { + it('uses custom icon metadata as the connection visual identity', () => { + const connection: SavedConnection = { + ...baseConnection, + iconType: 'postgres', + iconColor: '#2f855a', + }; + + expect(resolveConnectionIconType(connection)).toBe('postgres'); + expect(resolveConnectionAccentColor(connection)).toBe('#2f855a'); + }); + + it('falls back to the data source default color when custom color is blank', () => { + expect(resolveConnectionIconType(baseConnection)).toBe('mysql'); + expect(resolveConnectionAccentColor(baseConnection)).toBe('#00758F'); + }); + + it('ignores invalid custom colors instead of rendering unsafe CSS values', () => { + const connection: SavedConnection = { + ...baseConnection, + iconColor: 'url(javascript:alert(1))', + }; + + expect(resolveConnectionAccentColor(connection)).toBe('#00758F'); + }); +}); diff --git a/frontend/src/utils/connectionVisual.ts b/frontend/src/utils/connectionVisual.ts new file mode 100644 index 0000000..1841a84 --- /dev/null +++ b/frontend/src/utils/connectionVisual.ts @@ -0,0 +1,40 @@ +import type { SavedConnection } from '../types'; +import { getDbDefaultColor } from '../components/DatabaseIcons'; + +const HEX_COLOR_PATTERN = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i; + +const toTrimmedString = (value: unknown): string => { + if (typeof value === 'string') { + return value.trim(); + } + if (typeof value === 'number' || typeof value === 'boolean') { + return String(value).trim(); + } + return ''; +}; + +export const normalizeConnectionIconColor = (value: unknown): string => { + const color = toTrimmedString(value); + return HEX_COLOR_PATTERN.test(color) ? color : ''; +}; + +export const resolveConnectionIconType = ( + connection?: Pick | null, +): string => { + const iconType = toTrimmedString(connection?.iconType).toLowerCase(); + if (iconType) { + return iconType; + } + const configType = toTrimmedString(connection?.config?.type).toLowerCase(); + return configType || 'custom'; +}; + +export const resolveConnectionAccentColor = ( + connection?: Pick | null, +): string => { + const iconColor = normalizeConnectionIconColor(connection?.iconColor); + if (iconColor) { + return iconColor; + } + return getDbDefaultColor(resolveConnectionIconType(connection)); +};