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.
This commit is contained in:
@@ -6,7 +6,7 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { useCanvasStore } from '@/stores/canvasStore'
|
import { useCanvasStore } from '@/stores/canvasStore'
|
||||||
import { NODE_TYPE_LABELS, STATUS_COLORS, type ServiceInfo, type NodeData, type NodeProperty } from '@/types'
|
import { NODE_TYPE_LABELS, STATUS_COLORS, type ServiceInfo, type NodeData, type NodeProperty } from '@/types'
|
||||||
import { getServiceUrl } from '@/utils/serviceUrl'
|
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 { PROPERTY_ICONS, PROPERTY_ICON_NAMES, resolvePropertyIcon } from '@/utils/propertyIcons'
|
||||||
import type { Node } from '@xyflow/react'
|
import type { Node } from '@xyflow/react'
|
||||||
|
|
||||||
@@ -85,14 +85,8 @@ export function DetailPanel({ onEdit }: DetailPanelProps) {
|
|||||||
const { data } = node
|
const { data } = node
|
||||||
const services = data.services ?? []
|
const services = data.services ?? []
|
||||||
const statusColor = STATUS_COLORS[data.status]
|
const statusColor = STATUS_COLORS[data.status]
|
||||||
const host = data.ip ? primaryIp(data.ip) : data.hostname
|
const ipAddresses = data.ip ? splitIps(data.ip) : []
|
||||||
const ipAddresses = data.ip
|
const host = ipAddresses[0] ?? data.hostname
|
||||||
? data.ip
|
|
||||||
.split(/[\n,;]+/)
|
|
||||||
.map((ip) => primaryIp(ip.trim()))
|
|
||||||
.filter(Boolean)
|
|
||||||
.filter((ip, index, all) => all.indexOf(ip) === index)
|
|
||||||
: []
|
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (confirm(`Delete "${data.label}"?`)) {
|
if (confirm(`Delete "${data.label}"?`)) {
|
||||||
|
|||||||
@@ -446,6 +446,20 @@ describe('DetailPanel', () => {
|
|||||||
expect(screen.getByRole('link', { name: /192\.168\.1\.11/ })).toBeDefined()
|
expect(screen.getByRole('link', { name: /192\.168\.1\.11/ })).toBeDefined()
|
||||||
expect(screen.queryByText(',')).toBeNull()
|
expect(screen.queryByText(',')).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renders separate links for semicolon-separated IPs', () => {
|
||||||
|
setupStore({ ip: '192.168.1.10; 192.168.1.11' })
|
||||||
|
render(<DetailPanel onEdit={vi.fn()} />)
|
||||||
|
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(<DetailPanel onEdit={vi.fn()} />)
|
||||||
|
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', () => {
|
describe('ServiceBadge rendering', () => {
|
||||||
|
|||||||
@@ -55,6 +55,27 @@ describe('splitIps', () => {
|
|||||||
expect(splitIps('')).toEqual([])
|
expect(splitIps('')).toEqual([])
|
||||||
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', () => {
|
describe('primaryIp', () => {
|
||||||
|
|||||||
@@ -25,16 +25,17 @@ function maskSingle(ip: string): string {
|
|||||||
*/
|
*/
|
||||||
export function maskIp(ip: string): string {
|
export function maskIp(ip: string): string {
|
||||||
if (!ip) return ip
|
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.
|
* Split an IP string into trimmed values. Accepts comma, semicolon, or newline
|
||||||
* Empty string returns [].
|
* delimiters (or any combination). Duplicates removed, empty entries dropped.
|
||||||
*/
|
*/
|
||||||
export function splitIps(ip: string): string[] {
|
export function splitIps(ip: string): string[] {
|
||||||
if (!ip?.trim()) return []
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user