diff --git a/frontend/src/components/panels/DetailPanel.tsx b/frontend/src/components/panels/DetailPanel.tsx
index 1b99028..30b7742 100644
--- a/frontend/src/components/panels/DetailPanel.tsx
+++ b/frontend/src/components/panels/DetailPanel.tsx
@@ -63,69 +63,66 @@ export function DetailPanel({ onEdit }: DetailPanelProps) {
node={node}
nodes={nodes}
onUngroup={() => { ungroup(node.id) }}
- // Layout: service name always takes precedence, path truncates first or hides if needed, port/protocol always shown
- return (
-
-
-
-
- {svc.service_name}
-
- {/* Path: only show if name is not too long, truncate path first */}
- {pathLabel && (
-
- {pathLabel}
-
- )}
-
- {/* Port/protocol always shown, never truncated, always right-aligned */}
- {hasPort && (
-
{portLabel}/{svc.protocol}
- )}
-
- {url ? : }
-
-
-
- {url && (
-
- )}
-
- );
+ onToggleBorder={() => {
+ snapshotHistory()
+ updateNode(node.id, {
+ custom_colors: {
+ ...node.data.custom_colors,
+ show_border: !(node.data.custom_colors?.show_border !== false),
+ },
+ })
+ }}
+ onClose={() => setSelectedNode(null)}
+ onSelectChild={(id) => setSelectedNode(id)}
+ />
+ )
+ }
+
+ // Normal single-node panel
+ const addingService = addingForNode === node.id
+ const editingIndex = editingFor?.nodeId === node.id ? editingFor.index : null
+ const { data } = node
+ const services = data.services ?? []
+ const statusColor = STATUS_COLORS[data.status]
+ const host = data.ip ?? data.hostname
+
+ const handleDelete = () => {
+ if (confirm(`Delete "${data.label}"?`)) {
+ snapshotHistory()
+ deleteNode(node.id)
+ }
+ }
+
+ const handleAddService = () => {
+ const trimmedPort = newSvc.port.trim()
+ const port = trimmedPort === '' ? undefined : parseInt(trimmedPort, 10)
+ if (!newSvc.service_name.trim()) return
+ if (trimmedPort !== '' && (port == null || Number.isNaN(port) || port < 1 || port > 65535)) return
+ snapshotHistory()
+ const path = newSvc.path.trim()
+ const svc: ServiceInfo = {
+ ...(port != null ? { port } : {}),
+ protocol: newSvc.protocol,
+ service_name: newSvc.service_name.trim(),
+ ...(path ? { path } : {}),
+ }
+ updateNode(node.id, { services: [...services, svc] })
+ setNewSvc(EMPTY_FORM)
+ setAddingForNode(null)
+ }
+
+ const handleRemoveService = (index: number) => {
+ snapshotHistory()
+ const updated = services.filter((_, i) => i !== index)
+ updateNode(node.id, { services: updated })
+ if (editingIndex === index) setEditingFor(null)
+ }
+
+ const handleStartEdit = (index: number) => {
+ const svc = services[index]
+ if (!svc) return
+ setEditSvc({ port: svc.port != null ? String(svc.port) : '', protocol: svc.protocol, service_name: svc.service_name, path: svc.path ?? '' })
+ setEditingFor({ nodeId: node.id, index })
setAddingForNode(null)
}
@@ -667,7 +664,15 @@ function ServiceBadge({ svc, host, onEdit, onRemove }: { svc: ServiceInfo; host?
const hasPort = svc.port != null;
const portLabel = hasPort ? String(svc.port) : '';
const pathLabel = svc.path?.trim() ? svc.path.trim() : '';
- // Layout: service name takes all available space, path/port are always right-aligned next to buttons, path is hidden if name must truncate
+
+ // Calculate available width for path: if name is long, path gets less or no space
+ // We'll use a ref to measure the name width, but for minimal change, estimate by string length
+ const maxTotalWidth = 220; // px, total badge width minus paddings/buttons
+ const nameCharWidth = 7.2; // px per char (approx for font-size 12px)
+ const nameWidth = Math.min(svc.service_name.length * nameCharWidth, maxTotalWidth - 60); // reserve 60px for port/buttons
+ const pathMaxWidth = Math.max(0, maxTotalWidth - nameWidth - 60); // 60px for port/buttons
+ const showPath = pathLabel && pathMaxWidth > 30; // hide path if not enough space
+
return (
- {pathLabel && (
+ {showPath && (