From 2e6ee9dad263a5c24ee270f126f26475351f7201 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Sun, 19 Apr 2026 22:04:37 +0200 Subject: [PATCH] fix: handle multi-IP and add tests for clickable IP link Follow-up to #78: - Use primaryIp() so href targets the first IP when data.ip is comma-separated (e.g. "192.168.1.1, 2001:db8::1") - Add primaryIp() helper to maskIp.ts - Add 4 tests covering single IP link, absent IP, multi-IP href, multi-IP display text --- .../src/components/panels/DetailPanel.tsx | 3 +- .../panels/__tests__/DetailPanel.test.tsx | 30 +++++++++++++++++++ frontend/src/utils/maskIp.ts | 5 ++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/panels/DetailPanel.tsx b/frontend/src/components/panels/DetailPanel.tsx index 1e846c7..2deff7e 100644 --- a/frontend/src/components/panels/DetailPanel.tsx +++ b/frontend/src/components/panels/DetailPanel.tsx @@ -5,6 +5,7 @@ import { Input } from '@/components/ui/input' import { useCanvasStore } from '@/stores/canvasStore' import { NODE_TYPE_LABELS, STATUS_COLORS, type ServiceInfo, type NodeData, type NodeProperty } from '@/types' import { getServiceUrl } from '@/utils/serviceUrl' +import { primaryIp } from '@/utils/maskIp' import { PROPERTY_ICONS, PROPERTY_ICON_NAMES, resolvePropertyIcon } from '@/utils/propertyIcons' import type { Node } from '@xyflow/react' @@ -205,7 +206,7 @@ export function DetailPanel({ onEdit }: DetailPanelProps) { {data.ip && (
IP Address - + {data.ip}
diff --git a/frontend/src/components/panels/__tests__/DetailPanel.test.tsx b/frontend/src/components/panels/__tests__/DetailPanel.test.tsx index 3ecad14..07066bd 100644 --- a/frontend/src/components/panels/__tests__/DetailPanel.test.tsx +++ b/frontend/src/components/panels/__tests__/DetailPanel.test.tsx @@ -385,4 +385,34 @@ describe('DetailPanel', () => { expect(screen.getByText('nginx')).toBeDefined() }) }) + + describe('IP Address — clickable link', () => { + it('renders a link for a single IP', () => { + setupStore({ ip: '192.168.1.10' }) + render() + const link = screen.getByRole('link', { name: /192\.168\.1\.10/ }) + expect(link).toBeDefined() + expect(link.getAttribute('href')).toBe('http://192.168.1.10') + expect(link.getAttribute('target')).toBe('_blank') + }) + + it('renders no IP link when ip is absent', () => { + setupStore({ ip: undefined }) + render() + expect(screen.queryByText('IP Address')).toBeNull() + }) + + it('uses primary IP as href for comma-separated IPs', () => { + setupStore({ ip: '192.168.1.10, 192.168.1.11' }) + render() + const link = screen.getByRole('link', { name: /192\.168\.1\.10/ }) + expect(link.getAttribute('href')).toBe('http://192.168.1.10') + }) + + it('displays full comma-separated IP string as link text', () => { + setupStore({ ip: '192.168.1.10, 192.168.1.11' }) + render() + expect(screen.getByText(/192\.168\.1\.10, 192\.168\.1\.11/)).toBeDefined() + }) + }) }) diff --git a/frontend/src/utils/maskIp.ts b/frontend/src/utils/maskIp.ts index 7c0e8c9..31b8fe9 100644 --- a/frontend/src/utils/maskIp.ts +++ b/frontend/src/utils/maskIp.ts @@ -8,3 +8,8 @@ export function maskIp(ip: string): string { if (parts.length === 4) return `${parts[0]}.${parts[1]}.XX.XX` return ip } + +export function primaryIp(ip: string): string { + if (!ip?.trim()) return '' + return ip.split(',')[0].trim() +}