Download src/pages/Webhooks.tsx from sitcod3/modul-s: direct link, hf CLI and curl.
- Browser
- Download file 12.9 kB
-
https://huggingface.co/spaces/sitcod3/modul-s/resolve/main/src/pages/Webhooks.tsx
- Command line
-
hf download hf://spaces/sitcod3/modul-s/src/pages/Webhooks.tsx
-
curl -L -o Webhooks.tsx https://huggingface.co/spaces/sitcod3/modul-s/resolve/main/src/pages/Webhooks.tsx
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> | |
| ); | |
| } | |