import { useCallback, useEffect, useMemo, useState } from "react"; import { Plus, Server, Wifi, WifiOff, Globe, RefreshCw } from "lucide-react"; import { useNodes } from "../hooks/useNodes"; import { useProjects } from "../hooks/useProjects"; import type { NodeInfo, NodeUpdateInput } from "../api"; import { NodeCard } from "./NodeCard"; import { AddNodeModal, type AddNodeInput } from "./AddNodeModal"; import { NodeDetailModal } from "./NodeDetailModal"; import type { ToastType } from "../hooks/useToast"; interface NodesViewProps { addToast: (message: string, type?: ToastType) => void; } export function NodesView({ addToast }: NodesViewProps) { const { nodes, loading, error, refresh, register, update, unregister, healthCheck } = useNodes(); const { projects } = useProjects(); const [addModalOpen, setAddModalOpen] = useState(false); const [selectedNode, setSelectedNode] = useState(null); useEffect(() => { if (!selectedNode) return; const latest = nodes.find((node) => node.id === selectedNode.id) ?? null; setSelectedNode(latest); }, [nodes, selectedNode]); const stats = useMemo(() => { const total = nodes.length; const online = nodes.filter((node) => node.status === "online").length; const offline = nodes.filter((node) => node.status === "offline" || node.status === "error").length; const remote = nodes.filter((node) => node.type === "remote").length; return { total, online, offline, remote }; }, [nodes]); const handleRegister = useCallback(async (input: AddNodeInput) => { await register(input); }, [register]); const handleRefresh = useCallback(async () => { try { await refresh(); } catch { addToast("Failed to refresh nodes", "error"); } }, [addToast, refresh]); const handleHealthCheck = useCallback(async (id: string) => { try { await healthCheck(id); addToast("Node health check complete", "success"); } catch (err) { const message = err instanceof Error ? err.message : "Health check failed"; addToast(message, "error"); } }, [addToast, healthCheck]); const handleUnregister = useCallback(async (id: string) => { try { await unregister(id); addToast("Node removed", "success"); if (selectedNode?.id === id) { setSelectedNode(null); } } catch (err) { const message = err instanceof Error ? err.message : "Failed to remove node"; addToast(message, "error"); } }, [addToast, selectedNode?.id, unregister]); const handleUpdate = useCallback(async (id: string, updates: NodeUpdateInput) => { await update(id, updates); }, [update]); return (

Nodes

{nodes.length} registered
Total {stats.total}
Online {stats.online}
Offline {stats.offline}
Remote {stats.remote}
{error &&
{error}
} {loading ? (
{Array.from({ length: 4 }).map((_, index) => (
))}
) : nodes.length === 0 ? (

No nodes are registered yet.

) : (
{nodes.map((node) => ( { void handleHealthCheck(id); }} onEdit={(selected) => setSelectedNode(selected)} onRemove={(id) => { void handleUnregister(id); }} isLoading={loading} /> ))}
)} setAddModalOpen(false)} onSubmit={handleRegister} addToast={addToast} /> setSelectedNode(null)} node={selectedNode} projects={projects} onUpdate={handleUpdate} onHealthCheck={handleHealthCheck} addToast={addToast} />
); }