import { useCallback, useEffect, useMemo, useState } from "react"; import { Activity, Pencil, Save, X } from "lucide-react"; import type { NodeInfo, NodeUpdateInput, ProjectInfo } from "../api"; import type { ToastType } from "../hooks/useToast"; import { getProjectsForNode } from "../utils/nodeProjectAssignment"; interface NodeDetailModalProps { isOpen: boolean; onClose: () => void; node: NodeInfo | null; projects: ProjectInfo[]; onUpdate: (id: string, updates: NodeUpdateInput) => Promise; onHealthCheck: (id: string) => Promise; addToast: (message: string, type?: ToastType) => void; } function formatTimestamp(value?: string): string { if (!value) return "—"; const date = new Date(value); if (Number.isNaN(date.getTime())) return "—"; return date.toLocaleString(); } export function NodeDetailModal({ isOpen, onClose, node, projects, onUpdate, onHealthCheck, addToast, }: NodeDetailModalProps) { const [editMode, setEditMode] = useState(false); const [name, setName] = useState(""); const [url, setUrl] = useState(""); const [apiKey, setApiKey] = useState(""); const [maxConcurrent, setMaxConcurrent] = useState(2); const [isSaving, setIsSaving] = useState(false); useEffect(() => { if (!node || !isOpen) { setEditMode(false); return; } setName(node.name); setUrl(node.url ?? ""); setApiKey(node.apiKey ?? ""); setMaxConcurrent(node.maxConcurrent); setEditMode(false); }, [isOpen, node]); useEffect(() => { if (!isOpen) return; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { event.preventDefault(); onClose(); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [isOpen, onClose]); const assignedProjects = useMemo(() => { if (!node) return []; return getProjectsForNode(projects, node); }, [node, projects]); const handleHealthCheck = useCallback(async () => { if (!node) return; try { await onHealthCheck(node.id); addToast(`Health check completed for ${node.name}`, "success"); } catch (error) { const message = error instanceof Error ? error.message : "Health check failed"; addToast(message, "error"); } }, [addToast, node, onHealthCheck]); const handleSave = useCallback(async () => { if (!node || isSaving) return; const trimmedName = name.trim(); if (!trimmedName) { addToast("Name is required", "error"); return; } if (node.type === "remote" && !url.trim()) { addToast("URL is required for remote nodes", "error"); return; } if (!Number.isFinite(maxConcurrent) || maxConcurrent < 1) { addToast("Concurrency must be at least 1", "error"); return; } setIsSaving(true); try { await onUpdate(node.id, { name: trimmedName, url: node.type === "remote" ? url.trim() || undefined : undefined, apiKey: node.type === "remote" ? apiKey || undefined : undefined, maxConcurrent, }); addToast(`Updated ${trimmedName}`, "success"); setEditMode(false); } catch (error) { const message = error instanceof Error ? error.message : "Failed to update node"; addToast(message, "error"); } finally { setIsSaving(false); } }, [addToast, apiKey, isSaving, maxConcurrent, name, node, onUpdate, url]); const handleCancelEdit = useCallback(() => { if (!node) return; setName(node.name); setUrl(node.url ?? ""); setApiKey(node.apiKey ?? ""); setMaxConcurrent(node.maxConcurrent); setEditMode(false); }, [node]); if (!isOpen || !node) return null; return (
event.stopPropagation()} role="dialog" aria-modal="true" aria-label={`Node details for ${node.name}`} >

Node Details

Overview

{!editMode && ( )}
Type {node.type === "local" ? "Local" : "Remote"}
Status {node.status}
{node.type === "remote" && ( <> )}
Created {formatTimestamp(node.createdAt)}
Updated {formatTimestamp(node.updatedAt)}
{editMode && (
)}

{node.type === "local" ? "Projects" : "Assigned Projects"} ({assignedProjects.length})

{assignedProjects.length === 0 ? (

{node.type === "local" ? "No projects are running on this node." : "No projects are assigned to this node."}

) : (
    {assignedProjects.map((project) => (
  • {project.name} {project.id}
  • ))}
)}

Health

Status: {node.status} Last check: {formatTimestamp(node.updatedAt)}
); }