import { gitWebhookCreate, gitWebhookDelete, gitWebhookList, gitWebhookUpdate } from "@/client"; import type { WebhookEvent, WebhookResponse } from "@/client"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Edit2, Loader2, Plus, Trash2, Webhook, } from "lucide-react"; import { useState } from "react"; import { useParams } from "react-router-dom"; import { toast } from "sonner"; import {getApiErrorMessage} from '@/lib/api-error'; const WEBHOOK_EVENTS: Array<{ key: keyof WebhookEvent; label: string; desc: string }> = [ { key: "push", label: "Push", desc: "Push to a repository branch or tag" }, { key: "tag_push", label: "Tag", desc: "Create or delete a repository tag" }, { key: "pull_request", label: "Pull Request", desc: "Open, close, or merge a pull request" }, { key: "release", label: "Release", desc: "Publish or update a release" }, ]; function WebhookForm({ webhook, onSave, onCancel, isPending, }: { webhook?: WebhookResponse; onSave: (data: { url: string; secret: string; events: WebhookEvent }) => void; onCancel: () => void; isPending: boolean; }) { const [url, setUrl] = useState(webhook?.url ?? ""); const [secret, setSecret] = useState(webhook?.secret ?? ""); const [events, setEvents] = useState(webhook?.events ?? { push: true, tag_push: false, pull_request: false, issue_comment: false, release: false, }); const toggleEvent = (key: keyof WebhookEvent) => { setEvents((prev) => ({ ...prev, [key]: !prev[key] })); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!url.trim()) return; onSave({ url: url.trim(), secret: secret.trim(), events }); }; return (
setUrl(e.target.value)} placeholder="https://example.com/webhook" required disabled={isPending} />
setSecret(e.target.value)} placeholder="Optional secret for HMAC signature" disabled={isPending} />

Optional. If set, payloads will be signed with HMAC-SHA256.

{WEBHOOK_EVENTS.map((e) => (

{e.label}

{e.desc}

toggleEvent(e.key)} disabled={isPending} />
))}
); } function WebhookCard({ webhook, onEdit, onDelete, isDeleting }: { webhook: WebhookResponse; onEdit: () => void; onDelete: () => void; isDeleting: boolean }) { const activeEvents = WEBHOOK_EVENTS.filter((e) => webhook.events[e.key]); return (
{webhook.url}
{activeEvents.length > 0 && (
{activeEvents.map((e) => ( {e.label} ))}
)}

{webhook.touch_count} deliveries ยท created {new Date(webhook.created_at).toLocaleDateString()}

); } export function RepoSettingsWebhooks() { const queryClient = useQueryClient(); const { namespace, repoName } = useParams<{ namespace: string; repoName: string }>(); const ns = namespace!; const rn = repoName!; const [showForm, setShowForm] = useState(false); const [editingId, setEditingId] = useState(null); const [deleteId, setDeleteId] = useState(null); const { data, isLoading } = useQuery({ queryKey: ["repo-webhooks", ns, rn], queryFn: async () => { const resp = await gitWebhookList({ path: { namespace: ns, repo: rn } }); return resp.data?.data ?? null; }, enabled: !!ns && !!rn, staleTime: 30 * 1000, }); const webhooks: WebhookResponse[] = data?.webhooks ?? []; const createMutation = useMutation({ mutationFn: async (params: { url: string; secret: string; events: WebhookEvent }) => { await gitWebhookCreate({ path: { namespace: ns, repo: rn }, body: { url: params.url, secret: params.secret || null, content_type: "json", events: params.events, active: true }, }); }, onSuccess: () => { toast.success("Webhook created"); queryClient.invalidateQueries({ queryKey: ["repo-webhooks", ns, rn] }); resetForm(); }, onError: (err: unknown) => { toast.error(getApiErrorMessage(err, "Failed to create webhook")); }, }); const updateMutation = useMutation({ mutationFn: async (params: { url: string; secret: string; events: WebhookEvent }) => { if (!editingId) return; await gitWebhookUpdate({ path: { namespace: ns, repo: rn, webhook_id: editingId }, body: { url: params.url, secret: params.secret || null, events: params.events }, }); }, onSuccess: () => { toast.success("Webhook updated"); queryClient.invalidateQueries({ queryKey: ["repo-webhooks", ns, rn] }); resetForm(); }, onError: (err: unknown) => { toast.error(getApiErrorMessage(err, "Failed to update webhook")); }, }); const deleteMutation = useMutation({ mutationFn: async (webhookId: number) => { await gitWebhookDelete({ path: { namespace: ns, repo: rn, webhook_id: webhookId } }); }, onSuccess: () => { toast.success("Webhook deleted"); queryClient.invalidateQueries({ queryKey: ["repo-webhooks", ns, rn] }); setDeleteId(null); }, onError: (err: unknown) => { toast.error(getApiErrorMessage(err, "Failed to delete webhook")); }, }); const resetForm = () => { setShowForm(false); setEditingId(null); }; const handleEdit = (wh: WebhookResponse) => { setEditingId(wh.id); setShowForm(true); }; const pendingMutation = createMutation.isPending || updateMutation.isPending; const editingWebhook = editingId ? webhooks.find((w) => w.id === editingId) : undefined; return (

Webhooks

Configure webhooks to receive notifications about repository events.

{!showForm && }
{showForm && { editingId ? updateMutation.mutate(data) : createMutation.mutate(data); }} onCancel={resetForm} isPending={pendingMutation} />} {isLoading ? (
) : webhooks.length === 0 && !showForm ? (

No webhooks configured

Add a webhook to receive HTTP POST notifications.

) : ( webhooks.map((wh) => handleEdit(wh)} onDelete={() => setDeleteId(wh.id)} isDeleting={deleteMutation.isPending && deleteId === wh.id} />) )}
{deleteId && (

Delete webhook?

This will permanently delete the webhook and stop all future deliveries.

)}
); }