mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-22 00:29:40 +08:00
🐛 fix(connection): 修复连接颜色重启丢失并同步标签页展示
- 恢复连接清洗流程中的图标类型与颜色字段 - 标签页增加连接色标识,便于区分多连接会话 - 抽取连接视觉解析并补充回归测试 Refs #334
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<SortableTabLabelProps> = ({
|
||||
displayTitle,
|
||||
menuItems,
|
||||
accentColor,
|
||||
}) => {
|
||||
const labelStyle = accentColor
|
||||
? ({ '--connection-accent': accentColor } as React.CSSProperties)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
|
||||
<span
|
||||
className="tab-dnd-label"
|
||||
className={`tab-dnd-label${accentColor ? ' has-connection-accent' : ''}`}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
title={displayTitle}
|
||||
style={labelStyle}
|
||||
>
|
||||
{displayTitle}
|
||||
{accentColor ? <span className="tab-connection-accent" aria-hidden="true" /> : null}
|
||||
<span className="tab-title-text">{displayTitle}</span>
|
||||
</span>
|
||||
</Dropdown>
|
||||
);
|
||||
@@ -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 = () => {
|
||||
<SortableTabLabel
|
||||
displayTitle={displayTitle}
|
||||
menuItems={menuItems}
|
||||
accentColor={accentColor}
|
||||
/>
|
||||
),
|
||||
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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
46
frontend/src/utils/connectionVisual.test.ts
Normal file
46
frontend/src/utils/connectionVisual.test.ts
Normal file
@@ -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');
|
||||
});
|
||||
});
|
||||
40
frontend/src/utils/connectionVisual.ts
Normal file
40
frontend/src/utils/connectionVisual.ts
Normal file
@@ -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<SavedConnection, 'iconType' | 'config'> | 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<SavedConnection, 'iconColor' | 'iconType' | 'config'> | null,
|
||||
): string => {
|
||||
const iconColor = normalizeConnectionIconColor(connection?.iconColor);
|
||||
if (iconColor) {
|
||||
return iconColor;
|
||||
}
|
||||
return getDbDefaultColor(resolveConnectionIconType(connection));
|
||||
};
|
||||
Reference in New Issue
Block a user