import { createElement, useEffect, useMemo } from 'react' import { NodeResizer, useUpdateNodeInternals, useViewport, type NodeProps, type Node } from '@xyflow/react' import { Cpu, MemoryStick, HardDrive, ExternalLink, type LucideIcon } from 'lucide-react' import type { NodeData } from '@/types' import { resolveNodeColors } from '@/utils/nodeColors' import { resolveNodeIcon, isBrandIconKey } from '@/utils/nodeIcons' import { NodeIcon } from '@/components/ui/NodeIcon' import { resolvePropertyIcon } from '@/utils/propertyIcons' import { useThemeStore } from '@/stores/themeStore' import { THEMES } from '@/utils/themes' import { useCanvasStore, serviceStatusKey } from '@/stores/canvasStore' import { maskIp, primaryIp, splitIps } from '@/utils/maskIp' import { sideHandleCount } from '@/utils/handleUtils' import { SideHandles } from './SideHandles' import { getServiceUrl } from '@/utils/serviceUrl' interface BaseNodeProps extends NodeProps> { icon: LucideIcon } function formatStorage(gb: number): string { if (gb >= 1024) return `${(gb / 1024).toFixed(1).replace(/\.0$/, '')} TB` return `${gb} GB` } export function BaseNode({ id, data, selected, icon: typeIcon, width, height }: BaseNodeProps) { const updateNodeInternals = useUpdateNodeInternals() useEffect(() => { updateNodeInternals(id) }, [data.top_handles, data.bottom_handles, data.left_handles, data.right_handles, id, updateNodeInternals]) const { zoom } = useViewport() const borderWidth = useMemo(() => Math.max(1, 1 / zoom), [zoom]) const activeTheme = useThemeStore((s) => s.activeTheme) const hideIp = useCanvasStore((s) => s.hideIp) const serviceStatuses = useCanvasStore((s) => s.serviceStatuses) const theme = THEMES[activeTheme] const resolvedIcon = resolveNodeIcon(typeIcon, data.custom_icon) const colors = resolveNodeColors(data, activeTheme) const statusColor = theme.colors.statusColors[data.status] const isOnline = data.status === 'online' const services = data.services ?? [] const showServices = data.custom_colors?.show_services === true const serviceHost = data.ip ? primaryIp(data.ip) : data.hostname // Properties: prefer new system; fall back to legacy hardware fields for unmigrated nodes const visibleProperties = data.properties?.filter((p) => p.visible) ?? null const showLegacyHardware = !data.properties && data.show_hardware && (data.cpu_count != null || data.cpu_model || data.ram_gb != null || data.disk_gb != null) // Resolved per-side connection-point counts (missing field → side default). const topCount = sideHandleCount(data, 'top') const bottomCount = sideHandleCount(data, 'bottom') const leftCount = sideHandleCount(data, 'left') const rightCount = sideHandleCount(data, 'right') return (
{/* Status dot — absolute to avoid affecting node auto-width */}
{/* Main row */}
{/* Icon */}
{isBrandIconKey(data.custom_icon) ? : createElement(resolvedIcon, { size: 15 })}
{/* Label + IP */}
{data.label}
{data.ip && splitIps(data.ip).map((ip) => (
{hideIp ? maskIp(ip) : ip}
))}
{/* Properties section (new system) */} {visibleProperties && visibleProperties.length > 0 && ( <>
{visibleProperties.map((prop) => { const Icon = resolvePropertyIcon(prop.icon) return (
{Icon && } {prop.key} · {prop.value}
) })}
)} {showServices && services.length > 0 && ( <>
{services.map((svc, idx) => { const url = getServiceUrl(svc, serviceHost) const svcOffline = serviceStatuses[serviceStatusKey(id, svc.port, svc.protocol)] === 'offline' const row = (
{/* LEFT: service name */} {svc.service_name} {/* RIGHT: path + port */}
{svc.path && ( {svc.path} )} {svc.port}
) if (!url) return
{row}
return ( e.stopPropagation()} > {row} ) })}
)} {/* Legacy hardware section — fallback for nodes not yet migrated */} {showLegacyHardware && ( <>
{(data.cpu_model || data.cpu_count != null) && (
{data.cpu_model && ( {data.cpu_model} )} {data.cpu_count != null && ( {data.cpu_model ? `· ${data.cpu_count}c` : `${data.cpu_count} cores`} )}
)} {(data.ram_gb != null || data.disk_gb != null) && (
{data.ram_gb != null && ( {formatStorage(data.ram_gb)} )} {data.disk_gb != null && ( {formatStorage(data.disk_gb)} )}
)}
)}
) }