import { createElement, useEffect, useMemo } from 'react' import { Handle, Position, NodeResizer, useUpdateNodeInternals, useViewport, type NodeProps, type Node } from '@xyflow/react' import { Cpu, MemoryStick, HardDrive, type LucideIcon } from 'lucide-react' import type { NodeData } from '@/types' import { resolveNodeColors } from '@/utils/nodeColors' import { resolveNodeIcon } from '@/utils/nodeIcons' import { resolvePropertyIcon } from '@/utils/propertyIcons' import { useThemeStore } from '@/stores/themeStore' import { THEMES } from '@/utils/themes' import { useCanvasStore } from '@/stores/canvasStore' import { maskIp, splitIps } from '@/utils/maskIp' import { BOTTOM_HANDLE_IDS, BOTTOM_HANDLE_POSITIONS } from '@/utils/handleUtils' 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.bottom_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 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' // 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) return (
{/* Status dot — absolute to avoid affecting node auto-width */}
{/* Main row */}
{/* 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}
) })}
)} {/* 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)} )}
)}
)} {(BOTTOM_HANDLE_POSITIONS[data.bottom_handles ?? 1] ?? BOTTOM_HANDLE_POSITIONS[1]).map((leftPct, idx) => { const sourceId = BOTTOM_HANDLE_IDS[idx] const targetId = idx === 0 ? 'bottom-t' : `bottom-${idx + 1}-t` return ( ) })}
) }