From 60ea0fa11b93cfc79ddf2cd6aab06f041579c2eb Mon Sep 17 00:00:00 2001 From: findthelorax Date: Mon, 4 May 2026 20:37:43 -0400 Subject: [PATCH 1/4] made multiple ips all clickable separately --- .../src/components/panels/DetailPanel.tsx | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/panels/DetailPanel.tsx b/frontend/src/components/panels/DetailPanel.tsx index cddd301..16ec7ce 100644 --- a/frontend/src/components/panels/DetailPanel.tsx +++ b/frontend/src/components/panels/DetailPanel.tsx @@ -85,7 +85,14 @@ export function DetailPanel({ onEdit }: DetailPanelProps) { const { data } = node const services = data.services ?? [] const statusColor = STATUS_COLORS[data.status] - const host = data.ip ?? data.hostname + const host = data.ip ? primaryIp(data.ip) : data.hostname + const ipAddresses = data.ip + ? data.ip + .split(/[\n,;]+/) + .map((ip) => primaryIp(ip.trim())) + .filter(Boolean) + .filter((ip, index, all) => all.indexOf(ip) === index) + : [] const handleDelete = () => { if (confirm(`Delete "${data.label}"?`)) { @@ -223,12 +230,26 @@ export function DetailPanel({ onEdit }: DetailPanelProps) { )} - {data.ip && ( -
- IP Address - - {data.ip} - + {ipAddresses.length > 0 && ( +
+ {ipAddresses.length > 1 ? 'IP Addresses' : 'IP Address'} +
+ {ipAddresses.map((ip, index) => ( + + + {ip} + + + {index < ipAddresses.length - 1 && ,} + + ))} +
)} {data.mac && } From 8d937b2a0d46591a0c3aa15b7f7f94001fca1e96 Mon Sep 17 00:00:00 2001 From: findthelorax Date: Fri, 8 May 2026 19:49:16 -0400 Subject: [PATCH 2/4] removed comma and allow multiple in a row --- frontend/src/components/panels/DetailPanel.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/panels/DetailPanel.tsx b/frontend/src/components/panels/DetailPanel.tsx index 16ec7ce..79b7d5d 100644 --- a/frontend/src/components/panels/DetailPanel.tsx +++ b/frontend/src/components/panels/DetailPanel.tsx @@ -233,20 +233,19 @@ export function DetailPanel({ onEdit }: DetailPanelProps) { {ipAddresses.length > 0 && (
{ipAddresses.length > 1 ? 'IP Addresses' : 'IP Address'} -
+
{ipAddresses.map((ip, index) => ( - + {ip} - {index < ipAddresses.length - 1 && ,} ))}
From 9cd93ef294311e0b7b8167a4f5e1421648eec307 Mon Sep 17 00:00:00 2001 From: findthelorax Date: Fri, 8 May 2026 19:58:17 -0400 Subject: [PATCH 3/4] updated tests for multiple ips --- frontend/package-lock.json | 63 +++++++++++++++++++ .../panels/__tests__/DetailPanel.test.tsx | 4 +- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5b0e902..5e526c8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2520,6 +2520,9 @@ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2534,6 +2537,9 @@ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2548,6 +2554,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2562,6 +2571,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2576,6 +2588,9 @@ "loong64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2590,6 +2605,9 @@ "loong64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2604,6 +2622,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2618,6 +2639,9 @@ "ppc64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2632,6 +2656,9 @@ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2646,6 +2673,9 @@ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2660,6 +2690,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2674,6 +2707,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2688,6 +2724,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2936,6 +2975,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2953,6 +2995,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2970,6 +3015,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2987,6 +3035,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -6884,6 +6935,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6905,6 +6959,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6926,6 +6983,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6947,6 +7007,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/frontend/src/components/panels/__tests__/DetailPanel.test.tsx b/frontend/src/components/panels/__tests__/DetailPanel.test.tsx index 1c12e31..11a522c 100644 --- a/frontend/src/components/panels/__tests__/DetailPanel.test.tsx +++ b/frontend/src/components/panels/__tests__/DetailPanel.test.tsx @@ -442,7 +442,9 @@ describe('DetailPanel', () => { 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() + expect(screen.getByRole('link', { name: /192\.168\.1\.10/ })).toBeDefined() + expect(screen.getByRole('link', { name: /192\.168\.1\.11/ })).toBeDefined() + expect(screen.queryByText(',')).toBeNull() }) }) From ea451885af7f0964ccc3658671ecc76acb1b44ef Mon Sep 17 00:00:00 2001 From: Pouzor Date: Fri, 15 May 2026 13:36:15 +0200 Subject: [PATCH 4/4] refactor(ip-parsing): centralize multi-delim splitting in splitIps Extend splitIps to accept comma/semicolon/newline delimiters with dedupe, and reuse it in DetailPanel for both the IP list and the service-URL host. Fixes ServiceBadge links pointing at concatenated string when IPs were entered with ';' or newline separators. Adds tests for new delimiters. --- .../src/components/panels/DetailPanel.tsx | 12 +++-------- .../panels/__tests__/DetailPanel.test.tsx | 14 +++++++++++++ frontend/src/utils/__tests__/maskIp.test.ts | 21 +++++++++++++++++++ frontend/src/utils/maskIp.ts | 9 ++++---- 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/panels/DetailPanel.tsx b/frontend/src/components/panels/DetailPanel.tsx index 79b7d5d..d8896bf 100644 --- a/frontend/src/components/panels/DetailPanel.tsx +++ b/frontend/src/components/panels/DetailPanel.tsx @@ -6,7 +6,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 { splitIps } from '@/utils/maskIp' import { PROPERTY_ICONS, PROPERTY_ICON_NAMES, resolvePropertyIcon } from '@/utils/propertyIcons' import type { Node } from '@xyflow/react' @@ -85,14 +85,8 @@ export function DetailPanel({ onEdit }: DetailPanelProps) { const { data } = node const services = data.services ?? [] const statusColor = STATUS_COLORS[data.status] - const host = data.ip ? primaryIp(data.ip) : data.hostname - const ipAddresses = data.ip - ? data.ip - .split(/[\n,;]+/) - .map((ip) => primaryIp(ip.trim())) - .filter(Boolean) - .filter((ip, index, all) => all.indexOf(ip) === index) - : [] + const ipAddresses = data.ip ? splitIps(data.ip) : [] + const host = ipAddresses[0] ?? data.hostname const handleDelete = () => { if (confirm(`Delete "${data.label}"?`)) { diff --git a/frontend/src/components/panels/__tests__/DetailPanel.test.tsx b/frontend/src/components/panels/__tests__/DetailPanel.test.tsx index 11a522c..bbbd38a 100644 --- a/frontend/src/components/panels/__tests__/DetailPanel.test.tsx +++ b/frontend/src/components/panels/__tests__/DetailPanel.test.tsx @@ -446,6 +446,20 @@ describe('DetailPanel', () => { expect(screen.getByRole('link', { name: /192\.168\.1\.11/ })).toBeDefined() expect(screen.queryByText(',')).toBeNull() }) + + it('renders separate links for semicolon-separated IPs', () => { + setupStore({ ip: '192.168.1.10; 192.168.1.11' }) + render() + expect(screen.getByRole('link', { name: /192\.168\.1\.10/ }).getAttribute('href')).toBe('http://192.168.1.10') + expect(screen.getByRole('link', { name: /192\.168\.1\.11/ }).getAttribute('href')).toBe('http://192.168.1.11') + }) + + it('renders separate links for newline-separated IPs', () => { + setupStore({ ip: '192.168.1.10\n192.168.1.11' }) + render() + expect(screen.getByRole('link', { name: /192\.168\.1\.10/ }).getAttribute('href')).toBe('http://192.168.1.10') + expect(screen.getByRole('link', { name: /192\.168\.1\.11/ }).getAttribute('href')).toBe('http://192.168.1.11') + }) }) describe('ServiceBadge rendering', () => { diff --git a/frontend/src/utils/__tests__/maskIp.test.ts b/frontend/src/utils/__tests__/maskIp.test.ts index d5f0a33..1669bcf 100644 --- a/frontend/src/utils/__tests__/maskIp.test.ts +++ b/frontend/src/utils/__tests__/maskIp.test.ts @@ -55,6 +55,27 @@ describe('splitIps', () => { expect(splitIps('')).toEqual([]) expect(splitIps(' ')).toEqual([]) }) + + it('splits on semicolons', () => { + expect(splitIps('10.0.0.1; 10.0.0.2')).toEqual(['10.0.0.1', '10.0.0.2']) + }) + + it('splits on newlines', () => { + expect(splitIps('10.0.0.1\n10.0.0.2')).toEqual(['10.0.0.1', '10.0.0.2']) + }) + + it('splits on mixed delimiters', () => { + expect(splitIps('10.0.0.1,10.0.0.2; 10.0.0.3\n10.0.0.4')).toEqual([ + '10.0.0.1', + '10.0.0.2', + '10.0.0.3', + '10.0.0.4', + ]) + }) + + it('deduplicates repeated IPs', () => { + expect(splitIps('10.0.0.1, 10.0.0.1; 10.0.0.2')).toEqual(['10.0.0.1', '10.0.0.2']) + }) }) describe('primaryIp', () => { diff --git a/frontend/src/utils/maskIp.ts b/frontend/src/utils/maskIp.ts index 8dfa9f8..ca3d5fe 100644 --- a/frontend/src/utils/maskIp.ts +++ b/frontend/src/utils/maskIp.ts @@ -25,16 +25,17 @@ function maskSingle(ip: string): string { */ export function maskIp(ip: string): string { if (!ip) return ip - return ip.split(',').map(maskSingle).join(', ') + return splitIps(ip).map(maskSingle).join(', ') } /** - * Split a comma-separated IP string into an array of trimmed values. - * Empty string returns []. + * Split an IP string into trimmed values. Accepts comma, semicolon, or newline + * delimiters (or any combination). Duplicates removed, empty entries dropped. */ export function splitIps(ip: string): string[] { if (!ip?.trim()) return [] - return ip.split(',').map((s) => s.trim()).filter(Boolean) + const parts = ip.split(/[\n,;]+/).map((s) => s.trim()).filter(Boolean) + return parts.filter((v, i) => parts.indexOf(v) === i) } /**