sitcod3 commited on
Commit
907df84
·
verified ·
1 Parent(s): 65031d2

Upload src/pages/Webhooks.tsx with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/pages/Webhooks.tsx +316 -0
src/pages/Webhooks.tsx ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import { apiClient } from "../lib/apiClient";
3
+ import {
4
+ Webhook,
5
+ Plus,
6
+ Play,
7
+ Trash2,
8
+ CheckCircle2,
9
+ AlertCircle,
10
+ Code,
11
+ ExternalLink,
12
+ Copy,
13
+ Terminal
14
+ } from "lucide-react";
15
+ import { useState } from "react";
16
+ import StatusBadge from "../components/StatusBadge";
17
+ import FormDrawer from "../components/FormDrawer";
18
+ import ConfirmModal from "../components/ConfirmModal";
19
+ import toast from "react-hot-toast";
20
+
21
+ const AVAILABLE_EVENTS = [
22
+ { id: "EMAIL_REPLIED", label: "Email Replied" },
23
+ { id: "EMAIL_OPENED", label: "Email Opened" },
24
+ { id: "EMAIL_CLICKED", label: "Link Clicked" },
25
+ { id: "EMAIL_BOUNCED", label: "Email Bounced" },
26
+ { id: "LEAD_CATEGORY_UPDATED", label: "Lead Category Updated" },
27
+ { id: "EMAIL_UNSUBSCRIBED", label: "Lead Unsubscribed" },
28
+ { id: "CAMPAIGN_COMPLETED", label: "Campaign Completed" },
29
+ ];
30
+
31
+ export default function Webhooks() {
32
+ const queryClient = useQueryClient();
33
+ const [isCreateOpen, setIsCreateOpen] = useState(false);
34
+ const [selectedForDelete, setSelectedForDelete] = useState<string | null>(null);
35
+ const [testExecutionResult, setTestExecutionResult] = useState<any | null>(null);
36
+
37
+ const { data: webhooks = [], isLoading, refetch } = useQuery({
38
+ queryKey: ["webhooks"],
39
+ queryFn: async () => {
40
+ const res = await apiClient.get("/webhooks");
41
+ return res.data;
42
+ },
43
+ });
44
+
45
+ const { data: campaigns = [] } = useQuery({
46
+ queryKey: ["campaigns"],
47
+ queryFn: async () => {
48
+ const res = await apiClient.get("/campaigns");
49
+ return res.data;
50
+ },
51
+ });
52
+
53
+ const testFireMutation = useMutation({
54
+ mutationFn: async (id: string) => {
55
+ const res = await apiClient.post(`/webhooks/${id}/test`);
56
+ return res.data;
57
+ },
58
+ onSuccess: (data) => {
59
+ toast.success("Webhook test payload dispatched!");
60
+ setTestExecutionResult(data);
61
+ },
62
+ onError: () => {
63
+ toast.error("Failed to fire test webhook");
64
+ },
65
+ });
66
+
67
+ const deleteMutation = useMutation({
68
+ mutationFn: async (id: string) => {
69
+ const res = await apiClient.delete(`/webhooks/${id}`);
70
+ return res.data;
71
+ },
72
+ onSuccess: () => {
73
+ toast.success("Webhook endpoint removed");
74
+ setSelectedForDelete(null);
75
+ refetch();
76
+ },
77
+ });
78
+
79
+ return (
80
+ <div className="p-6 max-w-7xl mx-auto space-y-6">
81
+ {/* Header */}
82
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
83
+ <div>
84
+ <h1 className="text-2xl font-bold tracking-tight text-zinc-100 flex items-center gap-2.5">
85
+ <Webhook className="w-6 h-6 text-purple-400" />
86
+ Event Webhooks & Integrations
87
+ </h1>
88
+ <p className="text-zinc-400 text-sm mt-1">
89
+ Stream real-time outreach events directly to your CRM, Slack, Zapier, or backend server.
90
+ </p>
91
+ </div>
92
+
93
+ <button
94
+ onClick={() => setIsCreateOpen(true)}
95
+ 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"
96
+ >
97
+ <Plus className="w-4 h-4" /> Add Webhook
98
+ </button>
99
+ </div>
100
+
101
+ {/* Webhooks Table */}
102
+ <div className="bg-zinc-900 border border-zinc-800 rounded-xl overflow-hidden shadow-sm">
103
+ <div className="overflow-x-auto">
104
+ <table className="w-full text-sm text-left">
105
+ <thead className="bg-zinc-950 border-b border-zinc-800 text-zinc-400 text-xs uppercase">
106
+ <tr>
107
+ <th className="px-6 py-3.5 font-semibold">Endpoint URL</th>
108
+ <th className="px-6 py-3.5 font-semibold">Campaign Scope</th>
109
+ <th className="px-6 py-3.5 font-semibold">Subscribed Events</th>
110
+ <th className="px-6 py-3.5 font-semibold">Status</th>
111
+ <th className="px-6 py-3.5 font-semibold text-right">Actions</th>
112
+ </tr>
113
+ </thead>
114
+ <tbody className="divide-y divide-zinc-800/80">
115
+ {webhooks.map((hook: any) => (
116
+ <tr key={hook.id} className="hover:bg-zinc-800/30 transition-colors">
117
+ <td className="px-6 py-4">
118
+ <div className="flex items-center gap-2">
119
+ <span className="font-mono text-xs text-zinc-200 font-medium">
120
+ {hook.target_url}
121
+ </span>
122
+ <button
123
+ onClick={() => {
124
+ navigator.clipboard.writeText(hook.target_url);
125
+ toast.success("Copied webhook URL");
126
+ }}
127
+ className="text-zinc-500 hover:text-zinc-300 p-1"
128
+ >
129
+ <Copy className="w-3.5 h-3.5" />
130
+ </button>
131
+ </div>
132
+ </td>
133
+ <td className="px-6 py-4 text-xs text-zinc-300">
134
+ {hook.campaign_id ? `Campaign: ${hook.campaign_id}` : "Global (All Campaigns)"}
135
+ </td>
136
+ <td className="px-6 py-4">
137
+ <div className="flex flex-wrap gap-1 max-w-xs">
138
+ {hook.event_types?.map((ev: string) => (
139
+ <span
140
+ key={ev}
141
+ className="px-2 py-0.5 bg-zinc-950 text-indigo-300 border border-indigo-500/20 rounded text-[10px] font-mono"
142
+ >
143
+ {ev}
144
+ </span>
145
+ ))}
146
+ </div>
147
+ </td>
148
+ <td className="px-6 py-4">
149
+ <StatusBadge status={hook.status || "ACTIVE"} size="sm" />
150
+ </td>
151
+ <td className="px-6 py-4 text-right">
152
+ <div className="flex items-center justify-end gap-2">
153
+ <button
154
+ onClick={() => testFireMutation.mutate(hook.id)}
155
+ disabled={testFireMutation.isPending}
156
+ 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"
157
+ >
158
+ <Play className="w-3 h-3 text-emerald-400" /> Test Fire
159
+ </button>
160
+ <button
161
+ onClick={() => setSelectedForDelete(hook.id)}
162
+ className="p-1.5 text-zinc-500 hover:text-rose-400 hover:bg-rose-500/10 rounded-md transition-colors"
163
+ >
164
+ <Trash2 className="w-3.5 h-3.5" />
165
+ </button>
166
+ </div>
167
+ </td>
168
+ </tr>
169
+ ))}
170
+
171
+ {webhooks.length === 0 && (
172
+ <tr>
173
+ <td colSpan={5} className="py-16 text-center text-xs text-zinc-500">
174
+ No webhooks configured yet. Create one to receive real-time JSON payloads.
175
+ </td>
176
+ </tr>
177
+ )}
178
+ </tbody>
179
+ </table>
180
+ </div>
181
+ </div>
182
+
183
+ {/* Test Fire Response Inspector */}
184
+ {testExecutionResult && (
185
+ <div className="bg-zinc-950 border border-zinc-800 rounded-xl p-5 shadow-lg space-y-3">
186
+ <div className="flex items-center justify-between">
187
+ <h3 className="text-xs font-semibold text-emerald-400 flex items-center gap-2 uppercase tracking-wider">
188
+ <Terminal className="w-4 h-4" /> Live Webhook Delivery Simulation (200 OK)
189
+ </h3>
190
+ <button
191
+ onClick={() => setTestExecutionResult(null)}
192
+ className="text-xs text-zinc-500 hover:text-zinc-300"
193
+ >
194
+ Close Inspector
195
+ </button>
196
+ </div>
197
+ <pre className="p-4 bg-zinc-900 border border-zinc-800 rounded-lg text-xs font-mono text-zinc-300 overflow-x-auto">
198
+ {JSON.stringify(testExecutionResult, null, 2)}
199
+ </pre>
200
+ </div>
201
+ )}
202
+
203
+ {/* Delete Confirmation */}
204
+ <ConfirmModal
205
+ isOpen={Boolean(selectedForDelete)}
206
+ onClose={() => setSelectedForDelete(null)}
207
+ onConfirm={() => selectedForDelete && deleteMutation.mutate(selectedForDelete)}
208
+ title="Delete Webhook"
209
+ description="Are you sure you want to delete this webhook endpoint? No further notifications will be dispatched to this URL."
210
+ />
211
+
212
+ {/* Create Webhook Drawer */}
213
+ <FormDrawer
214
+ isOpen={isCreateOpen}
215
+ onClose={() => setIsCreateOpen(false)}
216
+ title="Create Webhook Subscription"
217
+ subtitle="Specify the destination HTTPS endpoint and target event triggers."
218
+ >
219
+ <form
220
+ onSubmit={async (e) => {
221
+ e.preventDefault();
222
+ const form = e.target as HTMLFormElement;
223
+ const target_url = (form.elements.namedItem("target_url") as HTMLInputElement).value;
224
+ const campaign_id = (form.elements.namedItem("campaign_id") as HTMLSelectElement).value;
225
+ const checkedEvents = AVAILABLE_EVENTS.filter(
226
+ (ev) => (form.elements.namedItem(ev.id) as HTMLInputElement)?.checked
227
+ ).map((ev) => ev.id);
228
+
229
+ if (checkedEvents.length === 0) {
230
+ toast.error("Please select at least one event trigger");
231
+ return;
232
+ }
233
+
234
+ try {
235
+ await apiClient.post("/webhooks", {
236
+ target_url,
237
+ campaign_id: campaign_id === "ALL" ? null : campaign_id,
238
+ event_types: checkedEvents,
239
+ });
240
+ toast.success("Webhook configured successfully!");
241
+ setIsCreateOpen(false);
242
+ refetch();
243
+ } catch (err: any) {
244
+ toast.error("Failed to create webhook");
245
+ }
246
+ }}
247
+ className="space-y-4 text-xs"
248
+ >
249
+ <div>
250
+ <label className="block font-medium text-zinc-300 mb-1">Target Endpoint URL *</label>
251
+ <input
252
+ name="target_url"
253
+ type="url"
254
+ required
255
+ placeholder="https://api.yourdomain.com/webhooks/smartlead"
256
+ className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100 font-mono"
257
+ />
258
+ </div>
259
+
260
+ <div>
261
+ <label className="block font-medium text-zinc-300 mb-1">Campaign Scope</label>
262
+ <select
263
+ name="campaign_id"
264
+ defaultValue="ALL"
265
+ className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100"
266
+ >
267
+ <option value="ALL">All Campaigns (Global Scope)</option>
268
+ {campaigns.map((c: any) => (
269
+ <option key={c.id} value={c.id}>
270
+ {c.name}
271
+ </option>
272
+ ))}
273
+ </select>
274
+ </div>
275
+
276
+ <div className="space-y-2 pt-2 border-t border-zinc-800">
277
+ <label className="block font-medium text-zinc-300 mb-2">Subscribe to Events *</label>
278
+ <div className="grid grid-cols-1 gap-2">
279
+ {AVAILABLE_EVENTS.map((ev) => (
280
+ <label
281
+ key={ev.id}
282
+ 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"
283
+ >
284
+ <input
285
+ type="checkbox"
286
+ name={ev.id}
287
+ defaultChecked
288
+ className="w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 bg-zinc-950 border-zinc-700"
289
+ />
290
+ <span className="text-zinc-200 font-medium">{ev.label}</span>
291
+ <span className="text-[10px] text-zinc-500 font-mono ml-auto">{ev.id}</span>
292
+ </label>
293
+ ))}
294
+ </div>
295
+ </div>
296
+
297
+ <div className="pt-4 flex justify-end gap-2 border-t border-zinc-800">
298
+ <button
299
+ type="button"
300
+ onClick={() => setIsCreateOpen(false)}
301
+ className="px-3 py-2 bg-zinc-800 text-zinc-300 rounded-lg"
302
+ >
303
+ Cancel
304
+ </button>
305
+ <button
306
+ type="submit"
307
+ className="px-4 py-2 bg-indigo-600 text-white rounded-lg font-medium shadow"
308
+ >
309
+ Register Webhook
310
+ </button>
311
+ </div>
312
+ </form>
313
+ </FormDrawer>
314
+ </div>
315
+ );
316
+ }