import { useCallback, useEffect, useMemo, useState } from "react"; import type { ToastType } from "../hooks/useToast"; export interface AddNodeInput { name: string; type: "local" | "remote"; url?: string; apiKey?: string; maxConcurrent: number; } interface AddNodeModalProps { isOpen: boolean; onClose: () => void; onSubmit: (input: AddNodeInput) => Promise; addToast: (message: string, type?: ToastType) => void; } interface FormErrors { name?: string; url?: string; maxConcurrent?: string; } const MAX_CONCURRENT_MIN = 1; const MAX_CONCURRENT_MAX = 10; function validateInput(input: AddNodeInput): FormErrors { const errors: FormErrors = {}; if (!input.name.trim()) { errors.name = "Name is required"; } if (input.type === "remote" && !input.url?.trim()) { errors.url = "URL is required for remote nodes"; } if (!Number.isFinite(input.maxConcurrent) || input.maxConcurrent < MAX_CONCURRENT_MIN || input.maxConcurrent > MAX_CONCURRENT_MAX) { errors.maxConcurrent = `Concurrency must be between ${MAX_CONCURRENT_MIN} and ${MAX_CONCURRENT_MAX}`; } return errors; } export function AddNodeModal({ isOpen, onClose, onSubmit, addToast }: AddNodeModalProps) { const [name, setName] = useState(""); const [type, setType] = useState<"local" | "remote">("local"); const [url, setUrl] = useState(""); const [apiKey, setApiKey] = useState(""); const [maxConcurrent, setMaxConcurrent] = useState(2); const [errors, setErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = useCallback(() => { setName(""); setType("local"); setUrl(""); setApiKey(""); setMaxConcurrent(2); setErrors({}); setIsSubmitting(false); }, []); const closeModal = useCallback(() => { if (isSubmitting) return; resetForm(); onClose(); }, [isSubmitting, onClose, resetForm]); useEffect(() => { if (!isOpen) { resetForm(); return; } const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { event.preventDefault(); closeModal(); } }; document.addEventListener("keydown", handleKeyDown); return () => { document.removeEventListener("keydown", handleKeyDown); }; }, [closeModal, isOpen, resetForm]); const input = useMemo(() => ({ name: name.trim(), type, url: type === "remote" ? url.trim() || undefined : undefined, apiKey: type === "remote" ? apiKey || undefined : undefined, maxConcurrent, }), [apiKey, maxConcurrent, name, type, url]); const handleSubmit = useCallback(async () => { if (isSubmitting) return; const validationErrors = validateInput(input); setErrors(validationErrors); if (Object.keys(validationErrors).length > 0) { return; } setIsSubmitting(true); try { await onSubmit(input); addToast(`Node "${input.name}" registered`, "success"); closeModal(); } catch (error) { const message = error instanceof Error ? error.message : "Failed to register node"; addToast(message, "error"); } finally { setIsSubmitting(false); } }, [addToast, closeModal, input, isSubmitting, onSubmit]); if (!isOpen) return null; return (
event.stopPropagation()} role="dialog" aria-modal="true" aria-label="Add Node">

Add Node

Register a node to distribute task execution across machines.

); }