"use client"; import { CheckIcon, CopyIcon, RotateCwIcon, WebhookIcon } from "lucide-react"; import { AnimatePresence, LayoutGroup, motion, useReducedMotion } from "motion/react"; import { type KeyboardEvent, type ReactNode, useEffect, useId, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; export type DeliveryStatus = number | "pending" | "timeout"; export interface WebhookDelivery { id: string; event: string; status: DeliveryStatus; latencyMs?: number; sentAt: string; attempt: number; request: { headers: Record; body: unknown }; response?: { headers?: Record; body?: string }; redeliveryOf?: string; } export interface WebhookInspectorProps { endpoint: string; deliveries: WebhookDelivery[]; onRedeliver?: (delivery: WebhookDelivery) => void; className?: string; } const ok = (s: DeliveryStatus) => typeof s === "number" && s >= 200 && s < 300; function statusStyle(s: DeliveryStatus) { if (s === "pending") return "bg-muted text-muted-foreground"; if (ok(s)) return "bg-success/10 text-success-foreground"; if (typeof s === "number" && s >= 400 && s < 500) return "bg-warning/12 text-warning-foreground"; return "bg-destructive/10 text-destructive-foreground"; } function statusLabel(s: DeliveryStatus) { return s === "pending" ? "…" : s === "timeout" ? "T/O" : String(s); } function ago(iso: string, now: number) { const s = Math.max(0, Math.round((now - new Date(iso).getTime()) / 1000)); if (s < 5) return "just now"; if (s < 60) return `${s}s ago`; if (s < 3600) return `${Math.floor(s / 60)}m ago`; if (s < 86400) return `${Math.floor(s / 3600)}h ago`; return `${Math.floor(s / 86400)}d ago`; } function Json({ value }: { value: unknown }) { const text = typeof value === "string" ? value : JSON.stringify(value, null, 2); const re = /("(?:\\.|[^"\\])*")(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d+)?(?:e[+-]?\d+)?/gi; const out: ReactNode[] = []; let last = 0; for (const m of text.matchAll(re)) { const at = m.index ?? 0; if (at > last) out.push({text.slice(last, at)}); if (m[1]) { out.push( {m[1]} , ); if (m[2]) out.push({m[2]}); } else { out.push( {m[0]} , ); } last = at + m[0].length; } if (last < text.length) out.push({text.slice(last)}); return <>{out}; } function Headers({ headers }: { headers: Record }) { return (
{Object.entries(headers).map(([k, v]) => (
{k}
{v}
))}
); } type Tab = "payload" | "headers" | "response"; export function WebhookInspector({ endpoint, deliveries, onRedeliver, className }: WebhookInspectorProps) { const reduce = useReducedMotion(); const uid = useId(); const [selectedId, setSelectedId] = useState(deliveries[0]?.id); const [tab, setTab] = useState("payload"); const [now, setNow] = useState(() => Date.now()); const [copied, setCopied] = useState(false); const known = useRef(new Set(deliveries.map((d) => d.id))); const itemRefs = useRef(new Map()); useEffect(() => { const t = setInterval(() => setNow(Date.now()), 5000); return () => clearInterval(t); }, []); useEffect(() => { if (!copied) return; const t = setTimeout(() => setCopied(false), 1400); return () => clearTimeout(t); }, [copied]); useEffect(() => { for (const d of deliveries) { if (!known.current.has(d.id)) { known.current.add(d.id); if (d.redeliveryOf) setSelectedId(d.id); } } setNow(Date.now()); }, [deliveries]); const selected = deliveries.find((d) => d.id === selectedId) ?? deliveries[0]; const total = deliveries.filter((d) => d.status !== "pending"); const rate = total.length ? Math.round((total.filter((d) => ok(d.status)).length / total.length) * 100) : 100; const pendingRedelivery = selected && deliveries.some((d) => d.redeliveryOf === selected.id && d.status === "pending"); const onListKey = (e: KeyboardEvent) => { const i = deliveries.findIndex((d) => d.id === selected?.id); const next = e.key === "ArrowDown" ? i + 1 : e.key === "ArrowUp" ? i - 1 : -1; if (next < 0 || next >= deliveries.length) return; e.preventDefault(); setSelectedId(deliveries[next].id); itemRefs.current.get(deliveries[next].id)?.focus(); }; const summary = (d: WebhookDelivery) => { if (d.status === "pending") return "Sending…"; if (d.status === "timeout") return `Timed out after ${((d.latencyMs ?? 30000) / 1000).toFixed(0)}s`; if (ok(d.status)) return `Delivered in ${d.latencyMs?.toLocaleString("en-US")} ms`; return `Endpoint returned ${d.status} after ${d.latencyMs?.toLocaleString("en-US")} ms`; }; const copyText = selected ? tab === "payload" ? JSON.stringify(selected.request.body, null, 2) : tab === "headers" ? Object.entries(selected.request.headers) .map(([k, v]) => `${k}: ${v}`) .join("\n") : (selected.response?.body ?? "") : ""; return (

Webhook deliveries

{endpoint}

Listening {rate}% delivered
{deliveries.map((d) => { const isSel = d.id === selected?.id; return ( setSelectedId(d.id)} ref={(el: HTMLButtonElement | null) => { if (el) itemRefs.current.set(d.id, el); else itemRefs.current.delete(d.id); }} role="option" tabIndex={isSel ? 0 : -1} transition={{ type: "spring", stiffness: 500, damping: 40 }} type="button" > {isSel && ( )} {d.status === "pending" ? ( ) : ( statusLabel(d.status) )} {d.event} {ago(d.sentAt, now)} {d.attempt > 1 && ` · attempt ${d.attempt}`} {d.status === "pending" ? "—" : d.status === "timeout" ? "30s" : `${d.latencyMs?.toLocaleString("en-US")}ms`} ); })}
{selected && (

{selected.event}

{summary(selected)}
{(["payload", "headers", "response"] as Tab[]).map((t) => ( ))}
{tab === "payload" && (
                  
                
)} {tab === "headers" && } {tab === "response" && (selected.status === "pending" ? (

Waiting for your endpoint to answer…

) : selected.status === "timeout" ? (

No response within 30 seconds

Respond with a 2xx quickly and do the work in a background job.

) : (
HTTP {selected.status} {selected.latencyMs?.toLocaleString("en-US")} ms
{selected.response?.headers && } {selected.response?.body && (
                        
                      
)}
))}
)}
); }