import { memo, useState, useEffect } from "react"; import { Handle, Position, type NodeProps } from "@xyflow/react"; import { useNodeStats } from "../hooks/useStatsStore"; import { Database, Zap, Globe, Server, Container, Shield, Cog, Timer, Radar, MonitorDot, KeyRound, FileCode, Boxes, Gem, Coffee, Bug, Rabbit, Mail, BarChart3, AlertTriangle, type LucideIcon, } from "lucide-react"; interface ServiceNodeData { label: string; image: string; state: string; ports: { host: number; container: number }[]; project: string; flash?: string; id?: string; activeHandles?: string[]; highlighted?: boolean; [key: string]: unknown; } const stateStyles: Record = { running: { ring: "ring-emerald-500/50", dot: "bg-emerald-500", bg: "bg-emerald-500/10", border: "border-slate-700/80" }, exited: { ring: "ring-red-500/50", dot: "bg-red-500", bg: "bg-red-500/10", border: "border-red-500/60" }, paused: { ring: "ring-amber-500/50", dot: "bg-amber-500", bg: "bg-amber-500/10", border: "border-amber-500/60" }, restarting: { ring: "ring-amber-500/50", dot: "bg-amber-500", bg: "bg-amber-500/10", border: "border-amber-500/60" }, dead: { ring: "ring-red-500/50", dot: "bg-red-500", bg: "bg-red-500/10", border: "border-red-500/60" }, crashed: { ring: "ring-orange-500/50", dot: "bg-orange-500", bg: "bg-orange-500/10", border: "border-orange-500/60" }, processing: { ring: "ring-yellow-500/50", dot: "bg-yellow-500 animate-pulse", bg: "bg-yellow-500/10", border: "border-yellow-500/60" }, }; // Map image/name patterns to Lucide icons and colors const iconMap: { pattern: string; icon: LucideIcon; color: string }[] = [ { pattern: "postgres", icon: Database, color: "#336791" }, { pattern: "mysql", icon: Database, color: "#4479A1" }, { pattern: "mariadb", icon: Database, color: "#003545" }, { pattern: "mongo", icon: Database, color: "#47A248" }, { pattern: "redis", icon: Zap, color: "#DC382D" }, { pattern: "memcached", icon: Zap, color: "#3B9C60" }, { pattern: "nginx", icon: Globe, color: "#009639" }, { pattern: "traefik", icon: Globe, color: "#24A1C1" }, { pattern: "haproxy", icon: Globe, color: "#2E86C1" }, { pattern: "caddy", icon: Globe, color: "#1F88E5" }, { pattern: "node", icon: MonitorDot, color: "#339933" }, { pattern: "python", icon: FileCode, color: "#3776AB" }, { pattern: "golang", icon: Boxes, color: "#00ADD8" }, { pattern: "ruby", icon: Gem, color: "#CC342D" }, { pattern: "java", icon: Coffee, color: "#ED8B00" }, { pattern: "rabbitmq", icon: Rabbit, color: "#FF6600" }, { pattern: "kafka", icon: Mail, color: "#231F20" }, { pattern: "grafana", icon: BarChart3, color: "#F46800" }, { pattern: "prometheus", icon: BarChart3, color: "#E6522C" }, { pattern: "certbot", icon: Shield, color: "#003A70" }, ]; // Name-based patterns (for custom-built images) const nameIconMap: { pattern: string; icon: LucideIcon; color: string }[] = [ { pattern: "collector", icon: Radar, color: "#f59e0b" }, { pattern: "celery", icon: Cog, color: "#97C95F" }, { pattern: "worker", icon: Cog, color: "#97C95F" }, { pattern: "beat", icon: Timer, color: "#97C95F" }, { pattern: "auth", icon: KeyRound, color: "#8b5cf6" }, { pattern: "backend", icon: Server, color: "#3b82f6" }, { pattern: "frontend", icon: MonitorDot, color: "#06b6d4" }, { pattern: "api", icon: Server, color: "#3b82f6" }, ]; function guessIcon(image: string, name: string): { Icon: LucideIcon; color: string } { const lowerImage = image.toLowerCase(); const lowerName = name.toLowerCase(); // Check image first for (const { pattern, icon, color } of iconMap) { if (lowerImage.includes(pattern)) return { Icon: icon, color }; } // Then check name for (const { pattern, icon, color } of nameIconMap) { if (lowerName.includes(pattern)) return { Icon: icon, color }; } return { Icon: Container, color: "#64748b" }; } function ProcessingTimer({ startedAt }: { startedAt: number }) { const [elapsed, setElapsed] = useState(Math.floor((Date.now() - startedAt) / 1000)); useEffect(() => { const interval = setInterval(() => { setElapsed(Math.floor((Date.now() - startedAt) / 1000)); }, 1000); return () => clearInterval(interval); }, [startedAt]); return {elapsed}s; } export const ServiceNode = memo(function ServiceNode({ data, id }: NodeProps) { const d = data as unknown as ServiceNodeData; const nodeStats = useNodeStats(id); const s = stateStyles[d.state] || stateStyles.exited; const { Icon, color: iconColor } = guessIcon(d.image, d.label); const flashClass = d.flash || ""; const activeHandles = new Set(d.activeHandles || []); const highlighted = d.highlighted; const hdot = (id: string) => { if (activeHandles.has(id)) { return highlighted ? "!bg-cyan-400 !w-2 !h-2 !border-0 !opacity-100" : "!bg-slate-500 !w-1.5 !h-1.5 !border-0 !opacity-80"; } return "!bg-transparent !w-1.5 !h-1.5 !border-0 !opacity-0"; }; // 3 slots per side at 25%, 50%, 75% const offsets = ["25%", "50%", "75%"]; return (
`${p.host}:${p.container}`).join(", ") || "none"}`} className={`relative rounded-xl border ${s.border} ${s.bg} backdrop-blur-sm shadow-lg shadow-black/30 p-4 min-w-[220px] ring-2 ${s.ring} transition-[opacity,box-shadow] duration-300 ${flashClass}`} > {/* Top handles — left offset, transform centered horizontally */} {offsets.map((o, i) => ( ))} {offsets.map((o, i) => ( ))} {/* Left handles — top offset, transform centered vertically */} {offsets.map((o, i) => ( ))} {offsets.map((o, i) => ( ))} {/* Right handles — top offset, transform centered */} {offsets.map((o, i) => ( ))} {offsets.map((o, i) => ( ))} {/* Top section: icon left, name + image right */}
{d.label} {d.state === "processing" ? (
{(d as any)._processingStartedAt && }
) : d.state === "crashed" ? ( ) : (
)}
{d.image.startsWith("sha256:") ? `Sin Tag (${d.image.slice(7, 19)})` : d.image}
{/* Ports */} {d.ports?.length > 0 && (
{d.ports.map((p) => ( :{p.host} ))}
)} {/* Stats */} {nodeStats && (
CPU {nodeStats.cpu.toFixed(1)}% MEM {nodeStats.mem_mb.toFixed(0)}MB
)} {/* Bottom handles — left offset, transform centered */} {offsets.map((o, i) => ( ))} {offsets.map((o, i) => ( ))}
); });