import { useCallback, useEffect, useMemo, useState } from "react"; import { Box, Plus, Server, Wifi, WifiOff, Globe, RefreshCw, X } from "lucide-react"; import "./NodesView.css"; import { useNodes } from "../hooks/useNodes"; import { useProjects } from "../hooks/useProjects"; import { useNodeSettingsSync, computeSyncState } from "../hooks/useNodeSettingsSync"; import { useMeshState } from "../hooks/useMeshState"; import type { ManagedDockerNodeInfo, NodeInfo, NodeUpdateInput } from "../api"; import { NodeCard } from "./NodeCard"; import { MeshTopology } from "./MeshTopology"; import { AddNodeModal, type AddNodeInput } from "./AddNodeModal"; import { DockerNodeOnboardingModal } from "./DockerNodeOnboardingModal"; import { NodeDetailModal } from "./NodeDetailModal"; import { useManagedDockerNodes } from "../hooks/useManagedDockerNodes"; import type { ManagedDockerNodeInput } from "@fusion/core"; import type { ToastType } from "../hooks/useToast"; interface NodesViewProps { addToast: (message: string, type?: ToastType) => void; onClose: () => void; } export function NodesView({ addToast, onClose }: NodesViewProps) { const { nodes, loading, error, refresh, register, update, unregister, healthCheck, patchDockerConfig, fetchDockerDiff, discoverRemoteProjects, } = useNodes(); const { projects, refresh: refreshProjects } = useProjects(); const { meshState, loading: meshLoading, error: meshError } = useMeshState(); const { syncStatusMap, pushSettings, pullSettings, syncAuth, trackNode, getAuthSyncState, getAuthProviders } = useNodeSettingsSync(); const { dockerNodes, loading: dockerLoading, refresh: refreshDocker, getContainerStatus, getLogs, create: createDockerNode, } = useManagedDockerNodes(); const [addModalOpen, setAddModalOpen] = useState(false); const [dockerOnboardingOpen, setDockerOnboardingOpen] = useState(false); const [selectedNode, setSelectedNode] = useState(null); // Track remote nodes for sync status polling useEffect(() => { const remoteNodes = nodes.filter((node) => node.type === "remote"); for (const node of remoteNodes) { trackNode(node.id); } }, [nodes, trackNode]); 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; const synced = nodes.filter( (node) => node.type === "remote" && syncStatusMap[node.id] && computeSyncState(syncStatusMap[node.id]).syncState === "synced" ).length; const docker = dockerNodes.length; return { total, online, offline, remote, synced, docker }; }, [dockerNodes.length, nodes, syncStatusMap]); const handleRegister = useCallback(async (input: AddNodeInput) => { await register(input); await refreshProjects(); }, [refreshProjects, register]); const handleCreateDockerNode = useCallback(async (input: ManagedDockerNodeInput) => { try { await createDockerNode(input); addToast(`Docker node "${input.name}" created`, "success"); setDockerOnboardingOpen(false); } catch (err) { const message = err instanceof Error ? err.message : "Failed to create Docker node"; addToast(message, "error"); throw err; } }, [addToast, createDockerNode]); const dockerNodeMap = useMemo(() => { const map = new Map(); for (const dockerNode of dockerNodes) { if (dockerNode.nodeId) { map.set(dockerNode.nodeId, dockerNode); } } return map; }, [dockerNodes]); const handleRefresh = useCallback(async () => { try { await Promise.all([refresh(), refreshDocker()]); } catch { addToast("Failed to refresh nodes", "error"); } }, [addToast, refresh, refreshDocker]); 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}
Synced {stats.synced}
Docker {stats.docker}
{(error || meshError) &&
{error ?? meshError}
} {/* Mesh Topology Visualization */} {!meshLoading && meshState.length > 0 && (

Mesh Topology

)} {loading ? (
{Array.from({ length: 4 }).map((_, index) => (
))}
) : nodes.length === 0 ? (

No nodes are registered yet.

) : (
{nodes.map((node) => { const nodeSyncStatus = node.type === "remote" && syncStatusMap[node.id] ? computeSyncState(syncStatusMap[node.id]) : undefined; return ( { void handleHealthCheck(id); }} onEdit={(selected) => setSelectedNode(selected)} onRemove={(id) => { void handleUnregister(id); }} isLoading={loading} syncStatus={nodeSyncStatus} authSyncState={node.type === "remote" ? getAuthSyncState(node.id) : undefined} authSyncProviders={node.type === "remote" ? getAuthProviders(node.id) : undefined} managedDockerNode={dockerNodeMap.get(node.id)} /> ); })}
)} setAddModalOpen(false)} onSubmit={handleRegister} onDiscoverRemoteProjects={discoverRemoteProjects} addToast={addToast} projects={projects} /> setDockerOnboardingOpen(false)} onSubmit={handleCreateDockerNode} addToast={addToast} /> setSelectedNode(null)} node={selectedNode} projects={projects} onUpdate={handleUpdate} onHealthCheck={handleHealthCheck} addToast={addToast} syncStatus={selectedNode?.type === "remote" && selectedNode && syncStatusMap[selectedNode.id] ? computeSyncState(syncStatusMap[selectedNode.id]) : undefined} onPushSettings={pushSettings} onPullSettings={pullSettings} onSyncAuth={syncAuth} managedDockerNode={selectedNode ? dockerNodeMap.get(selectedNode.id) : undefined} onFetchContainerStatus={getContainerStatus} onFetchLogs={getLogs} onUpdateDockerConfig={patchDockerConfig} onFetchDockerConfigDiff={fetchDockerDiff} />
); }