prioritize service name, truncate path or hide when necessary

This commit is contained in:
findthelorax
2026-04-22 00:24:24 -04:00
committed by Pouzor
parent 9979ae9dc8
commit 0b7691c083
+71 -66
View File
@@ -63,69 +63,66 @@ export function DetailPanel({ onEdit }: DetailPanelProps) {
node={node} node={node}
nodes={nodes} nodes={nodes}
onUngroup={() => { ungroup(node.id) }} onUngroup={() => { ungroup(node.id) }}
// Layout: service name always takes precedence, path truncates first or hides if needed, port/protocol always shown onToggleBorder={() => {
return ( snapshotHistory()
<div updateNode(node.id, {
className="group flex items-center border rounded-md text-xs transition-colors px-2 py-1.5 min-w-0" custom_colors: {
style={{ background: '#21262d', borderColor: '#30363d', position: 'relative' }} ...node.data.custom_colors,
> show_border: !(node.data.custom_colors?.show_border !== false),
<span className="shrink-0 w-1.5 h-1.5 rounded-full mr-1" style={{ backgroundColor: color }} /> },
<div className="flex items-center min-w-0 flex-grow" style={{ minWidth: 0 }}> })
<span }}
className="font-medium truncate min-w-0" onClose={() => setSelectedNode(null)}
style={{ color, maxWidth: '100%', marginRight: 8 }} onSelectChild={(id) => setSelectedNode(id)}
title={svc.service_name} />
tabIndex={0} )
aria-label={svc.service_name} }
>
{svc.service_name} // Normal single-node panel
</span> const addingService = addingForNode === node.id
{/* Path: only show if name is not too long, truncate path first */} const editingIndex = editingFor?.nodeId === node.id ? editingFor.index : null
{pathLabel && ( const { data } = node
<span const services = data.services ?? []
className="truncate text-[#8b949e] text-right" const statusColor = STATUS_COLORS[data.status]
style={{ minWidth: 0, maxWidth: 80, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block', marginLeft: 8 }} const host = data.ip ?? data.hostname
title={pathLabel}
tabIndex={0} const handleDelete = () => {
aria-label={pathLabel} if (confirm(`Delete "${data.label}"?`)) {
> snapshotHistory()
{pathLabel} deleteNode(node.id)
</span> }
)} }
</div>
{/* Port/protocol always shown, never truncated, always right-aligned */} const handleAddService = () => {
{hasPort && ( const trimmedPort = newSvc.port.trim()
<span className="font-mono text-[#8b949e] ml-2" style={{ whiteSpace: 'nowrap', flexShrink: 0 }}>{portLabel}/{svc.protocol}</span> const port = trimmedPort === '' ? undefined : parseInt(trimmedPort, 10)
)} if (!newSvc.service_name.trim()) return
<span className="inline-flex w-2.5 h-2.5 items-center justify-center shrink-0 ml-2" aria-hidden="true"> if (trimmedPort !== '' && (port == null || Number.isNaN(port) || port < 1 || port > 65535)) return
{url ? <ExternalLink size={10} className="text-muted-foreground" /> : <span style={{ width: 10, display: 'inline-block' }} />} snapshotHistory()
</span> const path = newSvc.path.trim()
<button const svc: ServiceInfo = {
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onEdit(); }} ...(port != null ? { port } : {}),
className="opacity-100 transition-opacity text-[#8b949e] hover:text-[#00d4ff] ml-0.5" protocol: newSvc.protocol,
title="Edit service" service_name: newSvc.service_name.trim(),
> ...(path ? { path } : {}),
<Pencil size={10} /> }
</button> updateNode(node.id, { services: [...services, svc] })
<button setNewSvc(EMPTY_FORM)
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onRemove(); }} setAddingForNode(null)
className="opacity-100 transition-opacity text-[#8b949e] hover:text-[#f85149] ml-0.5" }
title="Remove service"
> const handleRemoveService = (index: number) => {
<X size={10} /> snapshotHistory()
</button> const updated = services.filter((_, i) => i !== index)
{url && ( updateNode(node.id, { services: updated })
<a if (editingIndex === index) setEditingFor(null)
href={url} }
target="_blank"
rel="noopener noreferrer" const handleStartEdit = (index: number) => {
style={{ position: 'absolute', left: 0, top: 0, bottom: 0, right: 80, zIndex: 1, opacity: 0 }} const svc = services[index]
tabIndex={-1} if (!svc) return
aria-hidden="true" setEditSvc({ port: svc.port != null ? String(svc.port) : '', protocol: svc.protocol, service_name: svc.service_name, path: svc.path ?? '' })
/> setEditingFor({ nodeId: node.id, index })
)}
</div>
);
setAddingForNode(null) setAddingForNode(null)
} }
@@ -667,7 +664,15 @@ function ServiceBadge({ svc, host, onEdit, onRemove }: { svc: ServiceInfo; host?
const hasPort = svc.port != null; const hasPort = svc.port != null;
const portLabel = hasPort ? String(svc.port) : ''; const portLabel = hasPort ? String(svc.port) : '';
const pathLabel = svc.path?.trim() ? svc.path.trim() : ''; 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 ( return (
<div <div
className="group flex items-center border rounded-md text-xs transition-colors px-2 py-1.5 min-w-0" className="group flex items-center border rounded-md text-xs transition-colors px-2 py-1.5 min-w-0"
@@ -684,10 +689,10 @@ function ServiceBadge({ svc, host, onEdit, onRemove }: { svc: ServiceInfo; host?
{svc.service_name} {svc.service_name}
</span> </span>
<div className="flex items-center gap-1 shrink-0" style={{ maxWidth: 180, minWidth: 0 }}> <div className="flex items-center gap-1 shrink-0" style={{ maxWidth: 180, minWidth: 0 }}>
{pathLabel && ( {showPath && (
<span <span
className="truncate text-[#8b949e] text-right" className="truncate text-[#8b949e] text-right"
style={{ minWidth: 0, maxWidth: 80, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }} style={{ minWidth: 0, maxWidth: pathMaxWidth, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}
title={pathLabel} title={pathLabel}
tabIndex={0} tabIndex={0}
aria-label={pathLabel} aria-label={pathLabel}