import { useCallback, useEffect, useLayoutEffect, useState } from "react"; import { AlertTriangle, Check, Copy, Plus, RotateCw, Trash2, Webhook, X, } from "lucide-react"; import { Badge } from "@nous-research/ui/ui/components/badge"; import { Button } from "@nous-research/ui/ui/components/button"; import { Select, SelectOption } from "@nous-research/ui/ui/components/select"; import { Spinner } from "@nous-research/ui/ui/components/spinner"; import { H2 } from "@nous-research/ui/ui/components/typography/h2"; import { api } from "@/lib/api"; import type { WebhookRoute, WebhooksResponse } from "@/lib/api"; import { copyTextToClipboard } from "@/lib/clipboard"; import { DeleteConfirmDialog } from "@/components/DeleteConfirmDialog"; import { useToast } from "@nous-research/ui/hooks/use-toast"; import { useConfirmDelete } from "@nous-research/ui/hooks/use-confirm-delete"; import { useModalBehavior } from "@/hooks/useModalBehavior"; import { Toast } from "@nous-research/ui/ui/components/toast"; import { Card, CardContent } from "@nous-research/ui/ui/components/card"; import { Input } from "@nous-research/ui/ui/components/input"; import { Label } from "@nous-research/ui/ui/components/label"; import { usePageHeader } from "@/contexts/usePageHeader"; import { cn, themedBody } from "@/lib/utils"; interface CreatedWebhook { url: string; secret: string; } function CopyButton({ value }: { value: string }) { const [copied, setCopied] = useState(false); const handleCopy = useCallback(() => { void copyTextToClipboard(value).then((copied) => { if (!copied) return; setCopied(true); window.setTimeout(() => setCopied(false), 1500); }); }, [value]); return ( ); } export default function WebhooksPage() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [enabling, setEnabling] = useState(false); const [restartNeeded, setRestartNeeded] = useState(false); const [restartMessage, setRestartMessage] = useState(null); const [restartError, setRestartError] = useState(null); const [restarting, setRestarting] = useState(false); const { toast, showToast } = useToast(); const { setEnd } = usePageHeader(); // New subscription modal state const [createModalOpen, setCreateModalOpen] = useState(false); const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [events, setEvents] = useState(""); const [deliver, setDeliver] = useState("log"); const [deliverOnly, setDeliverOnly] = useState(false); const [prompt, setPrompt] = useState(""); const [creating, setCreating] = useState(false); const [created, setCreated] = useState(null); const closeCreateModal = useCallback(() => { setCreateModalOpen(false); setCreated(null); }, []); const createModalRef = useModalBehavior({ open: createModalOpen, onClose: closeCreateModal, }); const enabled = data?.enabled ?? false; const subscriptions = data?.subscriptions ?? []; const loadWebhooks = useCallback(() => { return api .getWebhooks() .then(setData) .catch(() => showToast("Failed to load webhooks", "error")) .finally(() => setLoading(false)); }, [showToast]); useEffect(() => { loadWebhooks(); }, [loadWebhooks]); const watchRestartOutcome = useCallback(async () => { for (let i = 0; i < 20; i++) { await new Promise((resolve) => setTimeout(resolve, 1500)); try { const st = await api.getActionStatus("gateway-restart", 5); if (st.running) continue; if (st.exit_code !== 0 && st.exit_code !== null) { setRestartMessage(null); setRestartNeeded(true); setRestartError(`Gateway restart failed with exit ${st.exit_code}.`); showToast( `Gateway restart failed (exit ${st.exit_code}) — restart manually`, "error", ); } else { setRestartMessage(null); setRestartNeeded(false); setRestartError(null); } return; } catch { // The dashboard may briefly lose its connection while the gateway restarts. } } setRestartMessage(null); }, [showToast]); const handleRestart = useCallback(async () => { setRestarting(true); try { await api.restartGateway(); setRestartNeeded(false); setRestartError(null); setRestartMessage("Gateway restarting…"); showToast("Gateway restarting…", "success"); setTimeout(() => void loadWebhooks(), 4000); void watchRestartOutcome(); } catch (e) { setRestartNeeded(true); setRestartError(String(e)); showToast(`Failed to restart: ${e}`, "error"); } finally { setRestarting(false); } }, [loadWebhooks, showToast, watchRestartOutcome]); const handleEnableWebhooks = useCallback(async () => { setEnabling(true); setRestartNeeded(false); setRestartError(null); try { const result = await api.enableWebhooks(); await loadWebhooks(); if (result.restart_started) { setRestartMessage("Webhooks enabled; gateway restarting…"); showToast("Webhooks enabled; gateway restarting…", "success"); setTimeout(() => void loadWebhooks(), 4000); void watchRestartOutcome(); } else { const detail = result.restart_error ? `: ${result.restart_error}` : "."; setRestartMessage(null); setRestartNeeded(true); setRestartError(`Gateway restart failed${detail}`); showToast(`Webhooks enabled; gateway restart failed${detail}`, "error"); } } catch (e) { showToast(`Failed to enable webhooks: ${e}`, "error"); } finally { setEnabling(false); } }, [loadWebhooks, showToast, watchRestartOutcome]); const resetForm = useCallback(() => { setName(""); setDescription(""); setEvents(""); setDeliver("log"); setDeliverOnly(false); setPrompt(""); }, []); const handleCreate = async () => { if (!name.trim()) { showToast("Name required", "error"); return; } setCreating(true); try { const eventsList = events .split(",") .map((e) => e.trim()) .filter(Boolean); const res = await api.createWebhook({ name: name.trim(), description: description.trim() || undefined, events: eventsList.length ? eventsList : undefined, deliver, deliver_only: deliverOnly, prompt: prompt.trim() || undefined, }); showToast("Created ✓", "success"); setCreated({ url: res.url, secret: res.secret }); resetForm(); loadWebhooks(); } catch (e) { showToast(`Failed to create: ${e}`, "error"); } finally { setCreating(false); } }; const [togglingName, setTogglingName] = useState(null); const handleToggleEnabled = useCallback( async (subName: string, nextEnabled: boolean) => { setTogglingName(subName); try { await api.setWebhookEnabled(subName, nextEnabled); showToast( nextEnabled ? `Enabled: "${subName}"` : `Disabled: "${subName}"`, "success", ); loadWebhooks(); } catch (e) { showToast(`Error: ${e}`, "error"); } finally { setTogglingName(null); } }, [loadWebhooks, showToast], ); const webhookDelete = useConfirmDelete({ onDelete: useCallback( async (name: string) => { try { await api.deleteWebhook(name); showToast(`Deleted: "${name}"`, "success"); loadWebhooks(); } catch (e) { showToast(`Error: ${e}`, "error"); throw e; } }, [loadWebhooks, showToast], ), }); // Put "New subscription" button in page header useLayoutEffect(() => { setEnd( , ); return () => { setEnd(null); }; }, [setEnd, enabled, enabling, loading]); if (loading) { return (
); } const pendingName = webhookDelete.pendingId ?? ""; return (
{/* Create subscription modal */} {createModalOpen && (
e.target === e.currentTarget && closeCreateModal()} role="dialog" aria-modal="true" aria-labelledby="create-webhook-title" >

New subscription

{created ? (

Subscription created. Copy the secret now — it is only shown once.

{created.url}
{created.secret}
) : (
setName(e.target.value)} />
setDescription(e.target.value)} />
setEvents(e.target.value)} />