modul-s / src /pages /Webhooks.tsx
sitcod3's picture
Upload src/pages/Webhooks.tsx with huggingface_hub
907df84 verified
Raw History Blame Contribute Delete
12.9 kB
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { apiClient } from "../lib/apiClient";
import {
Webhook,
Plus,
Play,
Trash2,
CheckCircle2,
AlertCircle,
Code,
ExternalLink,
Copy,
Terminal
} from "lucide-react";
import { useState } from "react";
import StatusBadge from "../components/StatusBadge";
import FormDrawer from "../components/FormDrawer";
import ConfirmModal from "../components/ConfirmModal";
import toast from "react-hot-toast";
const AVAILABLE_EVENTS = [
{ id: "EMAIL_REPLIED", label: "Email Replied" },
{ id: "EMAIL_OPENED", label: "Email Opened" },
{ id: "EMAIL_CLICKED", label: "Link Clicked" },
{ id: "EMAIL_BOUNCED", label: "Email Bounced" },
{ id: "LEAD_CATEGORY_UPDATED", label: "Lead Category Updated" },
{ id: "EMAIL_UNSUBSCRIBED", label: "Lead Unsubscribed" },
{ id: "CAMPAIGN_COMPLETED", label: "Campaign Completed" },
];
export default function Webhooks() {
const queryClient = useQueryClient();
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [selectedForDelete, setSelectedForDelete] = useState<string | null>(null);
const [testExecutionResult, setTestExecutionResult] = useState<any | null>(null);
const { data: webhooks = [], isLoading, refetch } = useQuery({
queryKey: ["webhooks"],
queryFn: async () => {
const res = await apiClient.get("/webhooks");
return res.data;
},
});
const { data: campaigns = [] } = useQuery({
queryKey: ["campaigns"],
queryFn: async () => {
const res = await apiClient.get("/campaigns");
return res.data;
},
});
const testFireMutation = useMutation({
mutationFn: async (id: string) => {
const res = await apiClient.post(`/webhooks/${id}/test`);
return res.data;
},
onSuccess: (data) => {
toast.success("Webhook test payload dispatched!");
setTestExecutionResult(data);
},
onError: () => {
toast.error("Failed to fire test webhook");
},
});
const deleteMutation = useMutation({
mutationFn: async (id: string) => {
const res = await apiClient.delete(`/webhooks/${id}`);
return res.data;
},
onSuccess: () => {
toast.success("Webhook endpoint removed");
setSelectedForDelete(null);
refetch();
},
});
return (
<div className="p-6 max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold tracking-tight text-zinc-100 flex items-center gap-2.5">
<Webhook className="w-6 h-6 text-purple-400" />
Event Webhooks & Integrations
</h1>
<p className="text-zinc-400 text-sm mt-1">
Stream real-time outreach events directly to your CRM, Slack, Zapier, or backend server.
</p>
</div>
<button
onClick={() => setIsCreateOpen(true)}
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition-all shadow-md shadow-indigo-600/20 flex items-center gap-2"
>
<Plus className="w-4 h-4" /> Add Webhook
</button>
</div>
{/* Webhooks Table */}
<div className="bg-zinc-900 border border-zinc-800 rounded-xl overflow-hidden shadow-sm">
<div className="overflow-x-auto">
<table className="w-full text-sm text-left">
<thead className="bg-zinc-950 border-b border-zinc-800 text-zinc-400 text-xs uppercase">
<tr>
<th className="px-6 py-3.5 font-semibold">Endpoint URL</th>
<th className="px-6 py-3.5 font-semibold">Campaign Scope</th>
<th className="px-6 py-3.5 font-semibold">Subscribed Events</th>
<th className="px-6 py-3.5 font-semibold">Status</th>
<th className="px-6 py-3.5 font-semibold text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-800/80">
{webhooks.map((hook: any) => (
<tr key={hook.id} className="hover:bg-zinc-800/30 transition-colors">
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-zinc-200 font-medium">
{hook.target_url}
</span>
<button
onClick={() => {
navigator.clipboard.writeText(hook.target_url);
toast.success("Copied webhook URL");
}}
className="text-zinc-500 hover:text-zinc-300 p-1"
>
<Copy className="w-3.5 h-3.5" />
</button>
</div>
</td>
<td className="px-6 py-4 text-xs text-zinc-300">
{hook.campaign_id ? `Campaign: ${hook.campaign_id}` : "Global (All Campaigns)"}
</td>
<td className="px-6 py-4">
<div className="flex flex-wrap gap-1 max-w-xs">
{hook.event_types?.map((ev: string) => (
<span
key={ev}
className="px-2 py-0.5 bg-zinc-950 text-indigo-300 border border-indigo-500/20 rounded text-[10px] font-mono"
>
{ev}
</span>
))}
</div>
</td>
<td className="px-6 py-4">
<StatusBadge status={hook.status || "ACTIVE"} size="sm" />
</td>
<td className="px-6 py-4 text-right">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => testFireMutation.mutate(hook.id)}
disabled={testFireMutation.isPending}
className="px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-200 text-xs font-medium rounded-md transition-colors flex items-center gap-1"
>
<Play className="w-3 h-3 text-emerald-400" /> Test Fire
</button>
<button
onClick={() => setSelectedForDelete(hook.id)}
className="p-1.5 text-zinc-500 hover:text-rose-400 hover:bg-rose-500/10 rounded-md transition-colors"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</td>
</tr>
))}
{webhooks.length === 0 && (
<tr>
<td colSpan={5} className="py-16 text-center text-xs text-zinc-500">
No webhooks configured yet. Create one to receive real-time JSON payloads.
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{/* Test Fire Response Inspector */}
{testExecutionResult && (
<div className="bg-zinc-950 border border-zinc-800 rounded-xl p-5 shadow-lg space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-xs font-semibold text-emerald-400 flex items-center gap-2 uppercase tracking-wider">
<Terminal className="w-4 h-4" /> Live Webhook Delivery Simulation (200 OK)
</h3>
<button
onClick={() => setTestExecutionResult(null)}
className="text-xs text-zinc-500 hover:text-zinc-300"
>
Close Inspector
</button>
</div>
<pre className="p-4 bg-zinc-900 border border-zinc-800 rounded-lg text-xs font-mono text-zinc-300 overflow-x-auto">
{JSON.stringify(testExecutionResult, null, 2)}
</pre>
</div>
)}
{/* Delete Confirmation */}
<ConfirmModal
isOpen={Boolean(selectedForDelete)}
onClose={() => setSelectedForDelete(null)}
onConfirm={() => selectedForDelete && deleteMutation.mutate(selectedForDelete)}
title="Delete Webhook"
description="Are you sure you want to delete this webhook endpoint? No further notifications will be dispatched to this URL."
/>
{/* Create Webhook Drawer */}
<FormDrawer
isOpen={isCreateOpen}
onClose={() => setIsCreateOpen(false)}
title="Create Webhook Subscription"
subtitle="Specify the destination HTTPS endpoint and target event triggers."
>
<form
onSubmit={async (e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const target_url = (form.elements.namedItem("target_url") as HTMLInputElement).value;
const campaign_id = (form.elements.namedItem("campaign_id") as HTMLSelectElement).value;
const checkedEvents = AVAILABLE_EVENTS.filter(
(ev) => (form.elements.namedItem(ev.id) as HTMLInputElement)?.checked
).map((ev) => ev.id);
if (checkedEvents.length === 0) {
toast.error("Please select at least one event trigger");
return;
}
try {
await apiClient.post("/webhooks", {
target_url,
campaign_id: campaign_id === "ALL" ? null : campaign_id,
event_types: checkedEvents,
});
toast.success("Webhook configured successfully!");
setIsCreateOpen(false);
refetch();
} catch (err: any) {
toast.error("Failed to create webhook");
}
}}
className="space-y-4 text-xs"
>
<div>
<label className="block font-medium text-zinc-300 mb-1">Target Endpoint URL *</label>
<input
name="target_url"
type="url"
required
placeholder="https://api.yourdomain.com/webhooks/smartlead"
className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100 font-mono"
/>
</div>
<div>
<label className="block font-medium text-zinc-300 mb-1">Campaign Scope</label>
<select
name="campaign_id"
defaultValue="ALL"
className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100"
>
<option value="ALL">All Campaigns (Global Scope)</option>
{campaigns.map((c: any) => (
<option key={c.id} value={c.id}>
{c.name}
</option>
))}
</select>
</div>
<div className="space-y-2 pt-2 border-t border-zinc-800">
<label className="block font-medium text-zinc-300 mb-2">Subscribe to Events *</label>
<div className="grid grid-cols-1 gap-2">
{AVAILABLE_EVENTS.map((ev) => (
<label
key={ev.id}
className="flex items-center gap-2 p-2.5 bg-zinc-900 border border-zinc-800 rounded-lg cursor-pointer hover:bg-zinc-800/60"
>
<input
type="checkbox"
name={ev.id}
defaultChecked
className="w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 bg-zinc-950 border-zinc-700"
/>
<span className="text-zinc-200 font-medium">{ev.label}</span>
<span className="text-[10px] text-zinc-500 font-mono ml-auto">{ev.id}</span>
</label>
))}
</div>
</div>
<div className="pt-4 flex justify-end gap-2 border-t border-zinc-800">
<button
type="button"
onClick={() => setIsCreateOpen(false)}
className="px-3 py-2 bg-zinc-800 text-zinc-300 rounded-lg"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-indigo-600 text-white rounded-lg font-medium shadow"
>
Register Webhook
</button>
</div>
</form>
</FormDrawer>
</div>
);
}