import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; import { Skeleton } from '@/components/ui/skeleton'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { toast } from '@/components/ui/toast-store'; import { apiFetch } from '@/lib/api'; import { ProGate } from '@/components/ProGate'; import { CapabilityGate } from '@/components/CapabilityGate'; import { TierBadge } from '@/components/TierBadge'; import { RefreshCw, CheckCircle, XCircle, Webhook, Copy, Trash2, Plus, ChevronDown, ChevronRight, History, } from 'lucide-react'; interface WebhookItem { id: number; name: string; stack_name: string; action: string; secret: string; enabled: boolean; created_at: number; updated_at: number; } interface WebhookExecution { id: number; webhook_id: number; action: string; status: 'success' | 'failure'; trigger_source: string | null; duration_ms: number | null; error: string | null; executed_at: number; } export function WebhooksSection({ isPro }: { isPro: boolean }) { const [webhooks, setWebhooks] = useState([]); const [loading, setLoading] = useState(true); const [creating, setCreating] = useState(false); const [showForm, setShowForm] = useState(false); const [newSecret, setNewSecret] = useState<{ id: number; secret: string } | null>(null); const [expandedHistory, setExpandedHistory] = useState(null); const [history, setHistory] = useState>({}); const [loadingHistory, setLoadingHistory] = useState(null); // Form state const [formName, setFormName] = useState(''); const [formStack, setFormStack] = useState(''); const [formAction, setFormAction] = useState('deploy'); const [stacks, setStacks] = useState([]); const fetchWebhooks = async () => { try { const res = await apiFetch('/webhooks', { localOnly: true }); if (res.ok) setWebhooks(await res.json()); } catch { /* ignore */ } finally { setLoading(false); } }; const fetchStacks = async () => { try { const res = await apiFetch('/stacks'); if (res.ok) setStacks(await res.json()); } catch { /* ignore */ } }; useEffect(() => { fetchWebhooks(); fetchStacks(); }, []); const handleCreate = async () => { if (!formName || !formStack || !formAction) { toast.error('All fields are required.'); return; } setCreating(true); try { const res = await apiFetch('/webhooks', { method: 'POST', localOnly: true, body: JSON.stringify({ name: formName, stack_name: formStack, action: formAction }), }); if (res.ok) { const data = await res.json(); setNewSecret({ id: data.id, secret: data.secret }); setShowForm(false); setFormName(''); setFormStack(''); setFormAction('deploy'); fetchWebhooks(); toast.success('Webhook created.'); } else { const err = await res.json().catch(() => ({})); toast.error(err?.error || err?.message || 'Failed to create webhook.'); } } catch (e: unknown) { toast.error((e as Error)?.message || 'Network error.'); } finally { setCreating(false); } }; const handleDelete = async (id: number) => { try { const res = await apiFetch(`/webhooks/${id}`, { method: 'DELETE', localOnly: true }); if (res.ok) { toast.success('Webhook deleted.'); fetchWebhooks(); } else { const err = await res.json().catch(() => ({})); toast.error(err?.error || 'Failed to delete.'); } } catch { toast.error('Network error.'); } }; const handleToggle = async (id: number, enabled: boolean) => { try { const res = await apiFetch(`/webhooks/${id}`, { method: 'PUT', localOnly: true, body: JSON.stringify({ enabled }), }); if (res.ok) fetchWebhooks(); } catch { /* ignore */ } }; const fetchHistory = async (webhookId: number) => { if (expandedHistory === webhookId) { setExpandedHistory(null); return; } setExpandedHistory(webhookId); setLoadingHistory(webhookId); try { const res = await apiFetch(`/webhooks/${webhookId}/history`, { localOnly: true }); if (res.ok) { const data = await res.json(); setHistory(prev => ({ ...prev, [webhookId]: data })); } } catch { /* ignore */ } finally { setLoadingHistory(null); } }; const copyToClipboard = (text: string, label: string) => { navigator.clipboard.writeText(text); toast.success(`${label} copied to clipboard.`); }; if (!isPro) { return (

Webhooks

Trigger stack actions from CI/CD pipelines via HTTP.

); } return (

Webhooks

Trigger stack actions from CI/CD pipelines via HTTP.

{/* Create Form */} {showForm && (
setFormName(e.target.value)} />
)} {/* Secret reveal (shown once after creation) */} {newSecret && (
Webhook created - copy your secret now

This secret will not be shown again. Store it securely.

{newSecret.secret}
)} {/* Loading state */} {loading && (
)} {/* Empty state */} {!loading && webhooks.length === 0 && !showForm && (

No webhooks configured yet.

Create one to trigger stack actions from CI/CD.

)} {/* Webhook list */} {!loading && webhooks.map(wh => { const triggerUrl = `${window.location.origin}/api/webhooks/${wh.id}/trigger`; const isExpanded = expandedHistory === wh.id; return (
{wh.name} {wh.action} {wh.stack_name}
handleToggle(wh.id!, c)} />
{/* Trigger URL */}
{triggerUrl}
{/* Secret (masked) */}
Secret: {wh.secret}
{/* History toggle */}
{/* Execution history */} {isExpanded && (
{loadingHistory === wh.id ? ( ) : (history[wh.id!] ?? []).length === 0 ? (

No executions yet.

) : (
{(history[wh.id!] ?? []).map(ex => (
{ex.status === 'success' ? : } {ex.action} {new Date(ex.executed_at).toLocaleString()} {ex.duration_ms !== null && ( {(ex.duration_ms / 1000).toFixed(1)}s )} {ex.error && ( {ex.error} )}
))}
)}
)}
); })}
); }