PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / assets / dist / js / Webhooks-Cb89OgkB.js

Webhooks-Cb89OgkB.js in Yatra – Travel Booking & Tour Operator Software 3.0.16, at assets/dist/js/Webhooks-Cb89OgkB.js

2,547 lines 125.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { r as reactExports, u as useQuery, j as jsxRuntimeExports, x as Webhook, as as Clock, V as AlertTriangle, C as Crown, aE as CheckCircle2, X as ExternalLink, v as useQueryClient, w as useMutation, ax as Plus, bF as Pencil, bk as Send, O as RefreshCw, bD as ShieldCheck, aO as Trash2, b9 as XCircle, Z as Zap, bl as Filter, a6 as React, aR as Eye, R as RotateCcw, E as Loader2, y as ChevronDown, aB as Check, aH as Copy } from "./react-vendor-CithbthY.js";
2 import { a as apiClient, _ as __, u as useToast, s as sprintf, I as Input } from "./index-Dec_weLB.js";
3 import { P as PageHeader, C as Card, d as CardContent, f as CardHeader, g as CardTitle, h as CardDescription, B as Button, Y as Table, e as Badge, k as ConfirmationDialog, S as Select, _ as Pagination, Z as Tooltip, s as Label, $ as Switch, M as Modal, A as Alert } from "../../admin/dist/js/app.js";
4 import { M as ModulePageSkeleton } from "./module-skeleton-CpoAoviJ.js";
5 const webhooksApi = {
6 getMeta: () => apiClient.get("/webhooks/meta"),
7 listEvents: () => apiClient.get("/webhooks/events"),
8 /** Returns the latest REAL payload for this event — either explicitly
9 * captured via {@link startListen} or pulled from a prior delivery.
10 * `payload` is null if no sample exists yet (UI prompts to Listen). */
11 getEventSample: (key) => apiClient.get(
12 `/webhooks/events/${encodeURIComponent(key)}/sample`
13 ),
14 /** Real-time capture flow (Pabbly/Zapier-style "listen for sample"):
15 * arm a capture, the next firing of the event records its real
16 * payload, the UI polls until it shows up. Zero guesswork. */
17 startListen: (key) => apiClient.post(
18 `/webhooks/events/${encodeURIComponent(key)}/listen`,
19 {}
20 ),
21 getListenStatus: (key) => apiClient.get(
22 `/webhooks/events/${encodeURIComponent(key)}/listen`
23 ),
24 /** Cancel an active capture. Pass forget=true to also discard the
25 * previously-captured sample (gives a clean slate). */
26 stopListen: (key, forget = false) => apiClient.delete(
27 `/webhooks/events/${encodeURIComponent(key)}/listen${forget ? "?forget=1" : ""}`
28 ),
29 listEndpoints: () => apiClient.get("/webhooks/endpoints"),
30 getEndpoint: (id) => apiClient.get(`/webhooks/endpoints/${id}`),
31 /** Returns the plaintext signing secret ONCE — show in a copy dialog
32 * unless the operator provided a custom_secret (in which case they
33 * already know it; the server still echoes it back for confirmation). */
34 createEndpoint: (payload) => apiClient.post("/webhooks/endpoints", payload),
35 updateEndpoint: (id, payload) => apiClient.put(`/webhooks/endpoints/${id}`, payload),
36 deleteEndpoint: (id) => apiClient.delete(`/webhooks/endpoints/${id}`),
37 /** Generates a fresh secret — shown ONCE. Invalidates the previous one. */
38 regenerateSecret: (id) => apiClient.post(
39 `/webhooks/endpoints/${id}/regenerate-secret`,
40 {}
41 ),
42 /** Queues a synthetic `webhook.ping` event — operator inspects the result
43 * in the Deliveries tab afterwards. */
44 pingEndpoint: (id) => apiClient.post(`/webhooks/endpoints/${id}/ping`, {}),
45 listDeliveries: (params = {}) => apiClient.get("/webhooks/deliveries", { params }),
46 /** Returns the FULL payload — the "listen mode" view. */
47 getDelivery: (id) => apiClient.get(`/webhooks/deliveries/${id}`),
48 /** Re-queues a delivery for a fresh attempt. Attempt counter preserved. */
49 replayDelivery: (id) => apiClient.post(`/webhooks/deliveries/${id}/replay`, {}),
50 /**
51 * Bulk replay. Accepts either explicit ids[] (cap 200) or a filter
52 * descriptor (cap 500). Rows whose endpoint is inactive or deleted
53 * are skipped and counted separately so the UI can show a partial-
54 * success summary.
55 */
56 bulkReplayDeliveries: (input) => apiClient.post("/webhooks/deliveries/bulk-replay", input),
57 /**
58 * Aggregated buried-deliveries snapshot. Pairs with bulkReplay above
59 * — operators triage from this summary, then bulk-replay either by
60 * explicit ids picked from `recent[]` or by filter (e.g. "every
61 * permanent_failure for endpoint 7 from the last 24h").
62 */
63 getDeadLetterSummary: () => apiClient.get("/webhooks/deliveries/dead-letter"),
64 /* ----------------------------- mTLS ------------------------------ */
65 /** Per-endpoint client-cert state. Returns `configured: false` when
66 * nothing's been uploaded. Never returns the key/cert PEM — only
67 * fingerprint + expiry hint. */
68 getMtlsHint: (id) => apiClient.get(`/webhooks/endpoints/${id}/mtls`),
69 /** Upload a PEM cert + private key (+ optional passphrase). The
70 * server validates the pair matches with openssl_x509_check_private_key
71 * before persisting, so a mismatched-pair mistake is caught at save
72 * time rather than at delivery time. */
73 setMtls: (id, payload) => apiClient.post(`/webhooks/endpoints/${id}/mtls`, payload),
74 clearMtls: (id) => apiClient.delete(`/webhooks/endpoints/${id}/mtls`)
75 };
76 function objectToRows(obj) {
77 return Object.entries(obj || {}).map(([k, v]) => ({ key: k, value: v }));
78 }
79 function rowsToObject(rows) {
80 const out = {};
81 for (const r of rows) {
82 const k = r.key.trim();
83 const v = r.value;
84 if (k === "") continue;
85 out[k] = v;
86 }
87 return out;
88 }
89 const KeyValueEditor = ({ rows, onChange, keyPlaceholder, valuePlaceholder, idPrefix }) => {
90 const setRow = (idx, patch) => {
91 const next = rows.slice();
92 next[idx] = { ...next[idx], ...patch };
93 onChange(next);
94 };
95 const removeRow = (idx) => {
96 onChange(rows.filter((_, i) => i !== idx));
97 };
98 const addRow = () => onChange([...rows, { key: "", value: "" }]);
99 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
100 rows.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: __("None configured.", "yatra") }) : rows.map((row, idx) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 items-start", children: [
101 /* @__PURE__ */ jsxRuntimeExports.jsx(
102 Input,
103 {
104 id: `${idPrefix}-key-${idx}`,
105 "aria-label": keyPlaceholder,
106 value: row.key,
107 onChange: (e) => setRow(idx, { key: e.target.value }),
108 placeholder: keyPlaceholder,
109 className: "font-mono text-sm flex-1"
110 }
111 ),
112 /* @__PURE__ */ jsxRuntimeExports.jsx(
113 Input,
114 {
115 id: `${idPrefix}-value-${idx}`,
116 "aria-label": valuePlaceholder,
117 value: row.value,
118 onChange: (e) => setRow(idx, { value: e.target.value }),
119 placeholder: valuePlaceholder,
120 className: "font-mono text-sm flex-[2]"
121 }
122 ),
123 /* @__PURE__ */ jsxRuntimeExports.jsx(
124 Button,
125 {
126 type: "button",
127 variant: "outline",
128 size: "sm",
129 onClick: () => removeRow(idx),
130 "aria-label": __("Remove row", "yatra"),
131 className: "flex-shrink-0",
132 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "h-3.5 w-3.5" })
133 }
134 )
135 ] }, idx)),
136 /* @__PURE__ */ jsxRuntimeExports.jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addRow, children: [
137 /* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { className: "mr-1.5 h-3.5 w-3.5" }),
138 __("Add row", "yatra")
139 ] })
140 ] });
141 };
142 function getInitialTab() {
143 if (typeof window === "undefined") return "endpoints";
144 const tab = new URLSearchParams(window.location.search).get("tab");
145 if (tab === "deliveries" || tab === "logs") return "deliveries";
146 if (tab === "buried" || tab === "dead-letter" || tab === "dlq")
147 return "buried";
148 return "endpoints";
149 }
150 const Webhooks = () => {
151 const [activeTab, setActiveTab] = reactExports.useState(() => getInitialTab());
152 const switchTab = (next) => {
153 setActiveTab(next);
154 if (typeof window !== "undefined") {
155 const url = new URL(window.location.href);
156 url.searchParams.set("tab", next);
157 window.history.replaceState({}, "", url.toString());
158 }
159 };
160 const { data: meta, isLoading: metaLoading } = useQuery({
161 queryKey: ["webhooks-meta"],
162 queryFn: () => webhooksApi.getMeta()
163 });
164 if (metaLoading) {
165 return /* @__PURE__ */ jsxRuntimeExports.jsx(ModulePageSkeleton, { variant: "tabs" });
166 }
167 if (!meta) return null;
168 if (!meta.is_agency_active) {
169 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [
170 /* @__PURE__ */ jsxRuntimeExports.jsx(
171 PageHeader,
172 {
173 description: __(
174 "Push every Yatra event to your CRM, accounting tool, Zapier, Make, Slack, or any HTTPS endpoint.",
175 "yatra"
176 )
177 }
178 ),
179 /* @__PURE__ */ jsxRuntimeExports.jsx(UpgradeCard, { meta })
180 ] });
181 }
182 if (!meta.is_module_enabled) {
183 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [
184 /* @__PURE__ */ jsxRuntimeExports.jsx(
185 PageHeader,
186 {
187 description: __(
188 "Push every Yatra event to your CRM, accounting tool, Zapier, Make, Slack, or any HTTPS endpoint.",
189 "yatra"
190 )
191 }
192 ),
193 /* @__PURE__ */ jsxRuntimeExports.jsx(ModulePrompt, {})
194 ] });
195 }
196 const tabs = [
197 { key: "endpoints", label: __("Endpoints", "yatra"), icon: Webhook },
198 { key: "deliveries", label: __("Deliveries", "yatra"), icon: Clock },
199 // "Buried" surfaces every delivery that exhausted retries +
200 // landed in permanent_failure. Bulk-replay lives here so operators
201 // triage from the summary instead of paging through deliveries.
202 { key: "buried", label: __("Buried", "yatra"), icon: AlertTriangle }
203 ];
204 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [
205 /* @__PURE__ */ jsxRuntimeExports.jsx(
206 PageHeader,
207 {
208 description: __(
209 "HMAC-signed outbound events. HTTPS endpoints only, encrypted secrets, automatic retry with exponential backoff, full delivery log with payload inspection + replay.",
210 "yatra"
211 )
212 }
213 ),
214 /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
215 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-b border-gray-200 dark:border-gray-700", children: /* @__PURE__ */ jsxRuntimeExports.jsx("nav", { className: "flex flex-wrap gap-1 px-4", children: tabs.map((tab) => {
216 const Icon = tab.icon;
217 const active = activeTab === tab.key;
218 return /* @__PURE__ */ jsxRuntimeExports.jsxs(
219 "button",
220 {
221 type: "button",
222 onClick: () => switchTab(tab.key),
223 className: `flex items-center gap-2 px-4 py-3 border-b-2 font-medium text-sm transition-colors ${active ? "border-blue-500 text-blue-600 dark:text-blue-400" : "border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600"}`,
224 children: [
225 /* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { className: "w-4 h-4" }),
226 tab.label
227 ]
228 },
229 tab.key
230 );
231 }) }) }),
232 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "p-6", children: [
233 activeTab === "endpoints" && /* @__PURE__ */ jsxRuntimeExports.jsx(EndpointsTab, {}),
234 activeTab === "deliveries" && /* @__PURE__ */ jsxRuntimeExports.jsx(DeliveriesTab, {}),
235 activeTab === "buried" && /* @__PURE__ */ jsxRuntimeExports.jsx(BuriedTab, {})
236 ] })
237 ] })
238 ] });
239 };
240 const UpgradeCard = ({ meta }) => /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { className: "max-w-3xl", children: [
241 /* @__PURE__ */ jsxRuntimeExports.jsx(CardHeader, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-3", children: [
242 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "rounded-md bg-purple-100 p-2 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Crown, { className: "h-5 w-5" }) }),
243 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1", children: [
244 /* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { children: __("Webhooks", "yatra") }),
245 /* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { className: "mt-1", children: __(
246 "Available on the Scale plan. The integration backbone for agencies wiring Yatra into a custom tech stack — sync bookings to CRMs, post revenue events to accounting tools, trigger Zapier workflows, ping Slack on VIP bookings.",
247 "yatra"
248 ) })
249 ] })
250 ] }) }),
251 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-4", children: [
252 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [
253 [
254 __("Encrypted per-endpoint secrets", "yatra"),
255 __("Libsodium-backed at-rest encryption.", "yatra")
256 ],
257 [
258 __("HMAC-SHA256 signed payloads", "yatra"),
259 __("Stripe-style signature header.", "yatra")
260 ],
261 [
262 __("Automatic retry with backoff", "yatra"),
263 __("5 attempts with jittered exponential delay.", "yatra")
264 ],
265 [
266 __("Full delivery log + replay", "yatra"),
267 __("Every payload preserved for 90 days.", "yatra")
268 ]
269 ].map(([title, sub]) => /* @__PURE__ */ jsxRuntimeExports.jsx(
270 "div",
271 {
272 className: "rounded-md border border-gray-200 dark:border-gray-700 p-3",
273 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-2", children: [
274 /* @__PURE__ */ jsxRuntimeExports.jsx(CheckCircle2, { className: "h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" }),
275 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
276 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-medium text-gray-900 dark:text-white", children: title }),
277 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-gray-500 dark:text-gray-400 mt-0.5", children: sub })
278 ] })
279 ] })
280 },
281 title
282 )) }),
283 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 pt-2", children: [
284 /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("a", { href: meta.upgrade_url, target: "_blank", rel: "noopener noreferrer", children: [
285 __("Upgrade to Scale", "yatra"),
286 /* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { className: "ml-1.5 h-4 w-4" })
287 ] }) }),
288 meta.docs_url && /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "outline", asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: meta.docs_url, target: "_blank", rel: "noopener noreferrer", children: __("Read the docs", "yatra") }) })
289 ] })
290 ] })
291 ] });
292 const ModulePrompt = () => /* @__PURE__ */ jsxRuntimeExports.jsx(Card, { className: "max-w-3xl", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
293 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardTitle, { className: "flex items-center gap-2", children: [
294 /* @__PURE__ */ jsxRuntimeExports.jsx(Webhook, { className: "h-5 w-5 text-blue-500" }),
295 __("Enable the Webhooks module", "yatra")
296 ] }),
297 /* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __(
298 'Toggle "Webhooks" on under Yatra → Modules to start configuring endpoints.',
299 "yatra"
300 ) })
301 ] }) });
302 const EndpointsTab = () => {
303 const queryClient = useQueryClient();
304 const { showToast } = useToast();
305 const [editing, setEditing] = reactExports.useState(null);
306 const [pendingDelete, setPendingDelete] = reactExports.useState(
307 null
308 );
309 const [revealSecret, setRevealSecret] = reactExports.useState(null);
310 const [pingInspectId, setPingInspectId] = reactExports.useState(null);
311 const [mtlsEndpoint, setMtlsEndpoint] = reactExports.useState(
312 null
313 );
314 const { data: endpointsData, isLoading } = useQuery({
315 queryKey: ["webhooks-endpoints"],
316 queryFn: () => webhooksApi.listEndpoints()
317 });
318 const { data: eventsData } = useQuery({
319 queryKey: ["webhooks-events"],
320 queryFn: () => webhooksApi.listEvents()
321 });
322 const deleteEndpoint = useMutation({
323 mutationFn: (id) => webhooksApi.deleteEndpoint(id),
324 onSuccess: () => {
325 queryClient.invalidateQueries({ queryKey: ["webhooks-endpoints"] });
326 showToast(__("Endpoint deleted.", "yatra"), "success");
327 setPendingDelete(null);
328 },
329 onError: (e) => {
330 showToast(extractError(e), "error");
331 setPendingDelete(null);
332 }
333 });
334 const pingEndpoint = useMutation({
335 mutationFn: (id) => webhooksApi.pingEndpoint(id),
336 onSuccess: (res) => {
337 queryClient.invalidateQueries({ queryKey: ["webhooks-deliveries"] });
338 setPingInspectId(res.row_id);
339 },
340 onError: (e) => showToast(extractError(e), "error")
341 });
342 const regenerateSecret = useMutation({
343 mutationFn: (id) => webhooksApi.regenerateSecret(id),
344 onSuccess: (res, id) => {
345 queryClient.invalidateQueries({ queryKey: ["webhooks-endpoints"] });
346 const endpoint = endpointsData == null ? void 0 : endpointsData.data.find((e) => e.id === id);
347 setRevealSecret({
348 secret: res.secret,
349 endpointName: (endpoint == null ? void 0 : endpoint.name) || __("this endpoint", "yatra")
350 });
351 },
352 onError: (e) => showToast(extractError(e), "error")
353 });
354 if (editing !== null) {
355 return /* @__PURE__ */ jsxRuntimeExports.jsx(
356 EndpointEditForm,
357 {
358 existing: editing === "new" ? null : editing,
359 events: (eventsData == null ? void 0 : eventsData.data) ?? [],
360 onClose: () => setEditing(null),
361 onCreated: (secret, name) => {
362 setEditing(null);
363 setRevealSecret({ secret, endpointName: name });
364 }
365 }
366 );
367 }
368 const endpoints = (endpointsData == null ? void 0 : endpointsData.data) ?? [];
369 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
370 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between gap-3 flex-wrap", children: [
371 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "max-w-2xl", children: [
372 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-lg font-semibold text-gray-900 dark:text-white", children: __("HTTP endpoints", "yatra") }),
373 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: __(
374 "Each endpoint receives a signed POST when one of its subscribed events fires. The signing secret is shown once on create — store it in your receiver's config.",
375 "yatra"
376 ) })
377 ] }),
378 /* @__PURE__ */ jsxRuntimeExports.jsxs(Button, { onClick: () => setEditing("new"), children: [
379 /* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { className: "mr-1.5 h-4 w-4" }),
380 __("Add endpoint", "yatra")
381 ] })
382 ] }),
383 /* @__PURE__ */ jsxRuntimeExports.jsx(Card, { className: "overflow-visible", children: /* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { className: "p-0 overflow-visible", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
384 Table,
385 {
386 data: endpoints,
387 columns: [
388 {
389 key: "name",
390 label: __("Endpoint", "yatra"),
391 render: (ep) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [
392 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Webhook, { className: "w-5 h-5 text-blue-600 dark:text-blue-400" }) }),
393 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0", children: [
394 /* @__PURE__ */ jsxRuntimeExports.jsx(
395 "button",
396 {
397 type: "button",
398 onClick: () => setEditing(ep),
399 className: "font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline transition-colors cursor-pointer text-left",
400 children: ep.name
401 }
402 ),
403 /* @__PURE__ */ jsxRuntimeExports.jsx(
404 "div",
405 {
406 className: "text-xs font-mono text-gray-500 dark:text-gray-400 mt-0.5 truncate max-w-md",
407 title: ep.url,
408 children: ep.url
409 }
410 ),
411 ep.consecutive_failures >= 5 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-1 inline-flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400", children: [
412 /* @__PURE__ */ jsxRuntimeExports.jsx(AlertTriangle, { className: "h-3 w-3" }),
413 ep.consecutive_failures,
414 " ",
415 __("consecutive failures", "yatra")
416 ] })
417 ] })
418 ] })
419 },
420 {
421 key: "status",
422 label: __("Status", "yatra"),
423 render: (ep) => ep.is_active ? /* @__PURE__ */ jsxRuntimeExports.jsxs(Badge, { className: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400", children: [
424 /* @__PURE__ */ jsxRuntimeExports.jsx(CheckCircle2, { className: "mr-1 h-3 w-3" }),
425 __("Active", "yatra")
426 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(Badge, { className: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300", children: [
427 /* @__PURE__ */ jsxRuntimeExports.jsx(XCircle, { className: "mr-1 h-3 w-3" }),
428 __("Inactive", "yatra")
429 ] })
430 },
431 {
432 key: "event",
433 label: __("Event", "yatra"),
434 render: (ep) => /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
435 ep.event ? /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "inline-flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium bg-indigo-50 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800", children: [
436 /* @__PURE__ */ jsxRuntimeExports.jsx(Zap, { className: "h-3 w-3" }),
437 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "font-mono", children: ep.event })
438 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-gray-400", children: "—" }),
439 ep.selected_fields && ep.selected_fields.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-1 inline-flex items-center gap-1 text-[10px] text-gray-500 dark:text-gray-400", children: [
440 /* @__PURE__ */ jsxRuntimeExports.jsx(Filter, { className: "h-3 w-3" }),
441 ep.selected_fields.length,
442 " ",
443 __("fields", "yatra")
444 ] }),
445 !ep.log_deliveries && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-1 inline-flex items-center gap-1 text-[10px] text-gray-500 dark:text-gray-400", children: __("Logging off", "yatra") })
446 ] })
447 },
448 {
449 key: "health",
450 label: __("Health", "yatra"),
451 render: (ep) => /* @__PURE__ */ jsxRuntimeExports.jsx(HealthCell, { health: ep.health })
452 },
453 {
454 key: "last_delivered_at",
455 label: __("Last delivery", "yatra"),
456 render: (ep) => ep.last_delivered_at ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap", children: [
457 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: new Date(ep.last_delivered_at).toLocaleString() }),
458 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-0.5", children: ep.last_status || "—" })
459 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-gray-400", children: "—" })
460 }
461 ],
462 actions: [
463 {
464 key: "edit",
465 label: __("Edit", "yatra"),
466 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Pencil, { className: "w-4 h-4" }),
467 onClick: (ep) => setEditing(ep)
468 },
469 {
470 key: "ping",
471 label: __("Send test ping", "yatra"),
472 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Send, { className: "w-4 h-4" }),
473 onClick: (ep) => pingEndpoint.mutate(ep.id),
474 condition: (ep) => ep.is_active
475 },
476 {
477 key: "regenerate",
478 label: __("Regenerate signing secret", "yatra"),
479 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "w-4 h-4" }),
480 onClick: (ep) => regenerateSecret.mutate(ep.id)
481 },
482 {
483 key: "mtls",
484 label: __("Client certificate (mTLS)", "yatra"),
485 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(ShieldCheck, { className: "w-4 h-4" }),
486 onClick: (ep) => setMtlsEndpoint(ep)
487 },
488 {
489 key: "delete",
490 label: __("Delete endpoint", "yatra"),
491 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "w-4 h-4" }),
492 onClick: (ep) => setPendingDelete(ep),
493 variant: "destructive"
494 }
495 ],
496 isLoading,
497 emptyText: __("No endpoints configured yet", "yatra"),
498 emptyDescription: __(
499 "Add your first endpoint to start receiving events. Common targets: Zapier catch hooks, Make scenarios, internal CRM ingest URLs, Slack webhook URLs.",
500 "yatra"
501 ),
502 onCreateClick: () => setEditing("new")
503 }
504 ) }) }),
505 /* @__PURE__ */ jsxRuntimeExports.jsx(
506 ConfirmationDialog,
507 {
508 isOpen: pendingDelete !== null,
509 onClose: () => {
510 if (!deleteEndpoint.isPending) setPendingDelete(null);
511 },
512 onConfirm: () => {
513 if (pendingDelete) deleteEndpoint.mutate(pendingDelete.id);
514 },
515 title: __("Delete endpoint?", "yatra"),
516 description: pendingDelete ? __(
517 'Delete "{name}"? Its delivery log will also be removed. This cannot be undone.',
518 "yatra"
519 ).replace("{name}", pendingDelete.name) : "",
520 confirmText: __("Delete endpoint", "yatra"),
521 cancelText: __("Cancel", "yatra"),
522 variant: "danger",
523 isLoading: deleteEndpoint.isPending
524 }
525 ),
526 /* @__PURE__ */ jsxRuntimeExports.jsx(
527 SecretRevealDialog,
528 {
529 secret: (revealSecret == null ? void 0 : revealSecret.secret) ?? null,
530 endpointName: (revealSecret == null ? void 0 : revealSecret.endpointName) ?? "",
531 onClose: () => setRevealSecret(null)
532 }
533 ),
534 pingInspectId !== null && /* @__PURE__ */ jsxRuntimeExports.jsx(
535 DeliveryInspectDialog,
536 {
537 id: pingInspectId,
538 onClose: () => setPingInspectId(null)
539 }
540 ),
541 /* @__PURE__ */ jsxRuntimeExports.jsx(
542 MtlsDialog,
543 {
544 endpoint: mtlsEndpoint,
545 onClose: () => setMtlsEndpoint(null)
546 }
547 )
548 ] });
549 };
550 const ENVELOPE_KEYS = ["id", "type", "api_version", "occurred_at"];
551 function applyFieldFilter(payload, selected) {
552 if (payload === null) return null;
553 if (selected.length === 0 || selected.includes("*")) return payload;
554 const paths = Array.from(
555 new Set(
556 selected.map((p) => p.trim()).filter((p) => p !== "").map((p) => p.startsWith("data.") ? p.slice(5) : p).filter((p) => p !== "")
557 )
558 );
559 const out = {};
560 for (const k of ENVELOPE_KEYS) {
561 if (k in payload) out[k] = payload[k];
562 }
563 const data = payload.data;
564 if (data === null || typeof data !== "object" || Array.isArray(data)) {
565 out.data = data;
566 return out;
567 }
568 const filteredData = {};
569 for (const path of paths) {
570 extractInto(
571 data,
572 path.split("."),
573 0,
574 filteredData
575 );
576 }
577 out.data = filteredData;
578 return out;
579 }
580 function extractInto(tree, parts, depth, out) {
581 if (depth >= parts.length) return;
582 const key = parts[depth];
583 if (!(key in tree)) return;
584 const isLeaf = depth === parts.length - 1;
585 if (isLeaf) {
586 out[key] = tree[key];
587 return;
588 }
589 const child = tree[key];
590 if (typeof child !== "object" || child === null || Array.isArray(child)) {
591 return;
592 }
593 let nested = out[key];
594 if (typeof nested !== "object" || nested === null || Array.isArray(nested)) {
595 nested = {};
596 out[key] = nested;
597 }
598 extractInto(
599 child,
600 parts,
601 depth + 1,
602 nested
603 );
604 }
605 const HealthCell = ({ health }) => {
606 if (!health || health.total === 0) {
607 return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-gray-400", children: __("No data", "yatra") });
608 }
609 const rate = health.success_rate ?? 0;
610 const cls = rate >= 95 ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300" : rate >= 80 ? "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300" : "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300";
611 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
612 /* @__PURE__ */ jsxRuntimeExports.jsxs(
613 "span",
614 {
615 className: `inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium ${cls}`,
616 title: `${health.delivered} delivered / ${health.failed} failed (last ${health.total})`,
617 children: [
618 rate,
619 "%"
620 ]
621 }
622 ),
623 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-0.5 text-[10px] text-gray-500 dark:text-gray-400 whitespace-nowrap", children: [
624 health.delivered,
625 "/",
626 health.total
627 ] })
628 ] });
629 };
630 const FieldSelectorCard = ({
631 eventKey,
632 eventName,
633 paths,
634 isLoading,
635 sendAll,
636 onChangeSendAll,
637 selectedFields,
638 onToggle,
639 onSelectAll,
640 onClear,
641 samplePayload,
642 sampleSource,
643 sampleCapturedAt
644 }) => {
645 var _a, _b, _c, _d;
646 const queryClient = useQueryClient();
647 const { showToast } = useToast();
648 const [showRawPayload, setShowRawPayload] = reactExports.useState(false);
649 const [payloadView, setPayloadView] = reactExports.useState("output");
650 const listenQuery = useQuery({
651 queryKey: ["webhooks-listen", eventKey],
652 queryFn: () => webhooksApi.getListenStatus(eventKey),
653 enabled: eventKey !== "",
654 refetchInterval: (q) => {
655 var _a2;
656 return ((_a2 = q.state.data) == null ? void 0 : _a2.armed) ? 3e3 : false;
657 }
658 });
659 const armed = ((_a = listenQuery.data) == null ? void 0 : _a.armed) ?? false;
660 const expiresAt = ((_b = listenQuery.data) == null ? void 0 : _b.expires_at) ?? null;
661 const armMutation = useMutation({
662 mutationFn: () => webhooksApi.startListen(eventKey),
663 onSuccess: () => {
664 queryClient.invalidateQueries({
665 queryKey: ["webhooks-listen", eventKey]
666 });
667 },
668 onError: (e) => showToast(extractError(e), "error")
669 });
670 const disarmMutation = useMutation({
671 mutationFn: (forget) => webhooksApi.stopListen(eventKey, forget),
672 onSuccess: () => {
673 queryClient.invalidateQueries({
674 queryKey: ["webhooks-listen", eventKey]
675 });
676 queryClient.invalidateQueries({
677 queryKey: ["webhooks-event-sample", eventKey]
678 });
679 },
680 onError: (e) => showToast(extractError(e), "error")
681 });
682 React.useEffect(() => {
683 var _a2;
684 if ((_a2 = listenQuery.data) == null ? void 0 : _a2.captured) {
685 queryClient.invalidateQueries({
686 queryKey: ["webhooks-event-sample", eventKey]
687 });
688 }
689 }, [(_d = (_c = listenQuery.data) == null ? void 0 : _c.captured) == null ? void 0 : _d.captured_at, eventKey, queryClient]);
690 const formatSample = (val) => {
691 if (val === null || val === void 0) return "null";
692 if (typeof val === "string") return val === "" ? '""' : val;
693 if (typeof val === "number" || typeof val === "boolean") return String(val);
694 if (Array.isArray(val) || typeof val === "object")
695 return JSON.stringify(val);
696 return String(val);
697 };
698 const [now, setNow] = reactExports.useState(() => Math.floor(Date.now() / 1e3));
699 React.useEffect(() => {
700 if (!armed) return;
701 const t = window.setInterval(
702 () => setNow(Math.floor(Date.now() / 1e3)),
703 1e3
704 );
705 return () => window.clearInterval(t);
706 }, [armed]);
707 const remaining = armed && expiresAt ? Math.max(0, expiresAt - now) : 0;
708 const mm = Math.floor(remaining / 60).toString().padStart(2, "0");
709 const ss = (remaining % 60).toString().padStart(2, "0");
710 const sourceLabel = {
711 captured: __("Captured live", "yatra"),
712 delivery_log: __("From last delivery", "yatra")
713 };
714 const hasSample = paths.length > 0 && samplePayload !== null;
715 return /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
716 /* @__PURE__ */ jsxRuntimeExports.jsx(CardHeader, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between gap-3 flex-wrap", children: [
717 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
718 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardTitle, { className: "flex items-center gap-2", children: [
719 /* @__PURE__ */ jsxRuntimeExports.jsx(Filter, { className: "h-4 w-4 text-blue-500" }),
720 __("Payload fields to send", "yatra")
721 ] }),
722 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardDescription, { className: "space-y-1", children: [
723 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "block", children: __(
724 "Pick which fields from this event's data block should be forwarded. The envelope (id, type, api_version, occurred_at) is always sent so receivers can route the event.",
725 "yatra"
726 ) }),
727 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "block text-xs text-gray-500 dark:text-gray-400", children: __(
728 "Fields ship in the JSON body of the POST under their original keys — nested structure is preserved (e.g. customer.email arrives as data.customer.email, not flattened).",
729 "yatra"
730 ) })
731 ] })
732 ] }),
733 hasSample && /* @__PURE__ */ jsxRuntimeExports.jsxs(
734 Button,
735 {
736 type: "button",
737 variant: "outline",
738 size: "sm",
739 onClick: () => setShowRawPayload(!showRawPayload),
740 children: [
741 /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "mr-1.5 h-3.5 w-3.5" }),
742 showRawPayload ? __("Hide payload preview", "yatra") : __("Preview payload", "yatra")
743 ]
744 }
745 )
746 ] }) }),
747 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-4", children: [
748 /* @__PURE__ */ jsxRuntimeExports.jsx(
749 Alert,
750 {
751 variant: armed ? "info" : hasSample ? "success" : "warning",
752 title: armed ? `${__("Listening for", "yatra")} ${eventName}…` : hasSample ? sampleSource ? sourceLabel[sampleSource] : __("Sample ready", "yatra") : __("No sample captured yet", "yatra"),
753 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between gap-3 flex-wrap", children: [
754 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "min-w-0 flex-1 space-y-1", children: armed ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
755 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: __(
756 "Trigger this event in Yatra (create a booking, submit an enquiry, etc.). The real payload will appear here automatically.",
757 "yatra"
758 ) }),
759 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "font-mono text-xs", children: [
760 __("Expires in", "yatra"),
761 " ",
762 mm,
763 ":",
764 ss
765 ] })
766 ] }) : hasSample ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
767 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: __(
768 "Pick from the real fields below — these are the exact paths your receiver will see.",
769 "yatra"
770 ) }),
771 sampleCapturedAt && /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs opacity-80", children: [
772 __("Captured", "yatra"),
773 " ",
774 new Date(sampleCapturedAt * 1e3).toLocaleString()
775 ] })
776 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: __(
777 `Click "Listen for sample" then perform the action that triggers this event in Yatra. We'll show you the real payload — no guesswork.`,
778 "yatra"
779 ) }) }),
780 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2 flex-shrink-0", children: [
781 armed ? /* @__PURE__ */ jsxRuntimeExports.jsxs(
782 Button,
783 {
784 type: "button",
785 variant: "outline",
786 size: "sm",
787 onClick: () => disarmMutation.mutate(false),
788 disabled: disarmMutation.isPending,
789 children: [
790 /* @__PURE__ */ jsxRuntimeExports.jsx(XCircle, { className: "mr-1.5 h-3.5 w-3.5" }),
791 __("Stop listening", "yatra")
792 ]
793 }
794 ) : /* @__PURE__ */ jsxRuntimeExports.jsxs(
795 Button,
796 {
797 type: "button",
798 size: "sm",
799 onClick: () => armMutation.mutate(),
800 disabled: armMutation.isPending,
801 children: [
802 armMutation.isPending ? /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-1.5 h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "mr-1.5 h-3.5 w-3.5" }),
803 hasSample ? __("Re-capture sample", "yatra") : __("Listen for sample", "yatra")
804 ]
805 }
806 ),
807 hasSample && !armed && /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip, { content: __("Discard the captured sample", "yatra"), children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
808 Button,
809 {
810 type: "button",
811 variant: "outline",
812 size: "sm",
813 onClick: () => disarmMutation.mutate(true),
814 disabled: disarmMutation.isPending,
815 children: [
816 /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "mr-1.5 h-3.5 w-3.5" }),
817 __("Clear", "yatra")
818 ]
819 }
820 ) })
821 ] })
822 ] })
823 }
824 ),
825 /* @__PURE__ */ jsxRuntimeExports.jsxs(
826 "label",
827 {
828 htmlFor: `webhook-send-all-${eventKey}`,
829 className: `flex items-start gap-3 p-3 rounded-md border cursor-pointer transition-colors ${sendAll ? "border-blue-500 bg-blue-50 dark:border-blue-400 dark:bg-blue-900/20" : "border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800/50"}`,
830 children: [
831 /* @__PURE__ */ jsxRuntimeExports.jsx(
832 "input",
833 {
834 id: `webhook-send-all-${eventKey}`,
835 type: "radio",
836 name: "webhook-field-mode",
837 checked: sendAll,
838 onChange: () => onChangeSendAll(true),
839 className: "h-4 w-4 mt-0.5 flex-shrink-0"
840 }
841 ),
842 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0", children: [
843 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-medium text-gray-900 dark:text-white", children: __("Send the full payload", "yatra") }),
844 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-gray-500 dark:text-gray-400 mt-0.5", children: __(
845 "POSTs the entire envelope + data block — every field under its original key. Recommended when your receiver picks what it needs.",
846 "yatra"
847 ) })
848 ] })
849 ]
850 }
851 ),
852 /* @__PURE__ */ jsxRuntimeExports.jsxs(
853 "label",
854 {
855 htmlFor: `webhook-select-${eventKey}`,
856 className: `flex items-start gap-3 p-3 rounded-md border cursor-pointer transition-colors ${!sendAll ? "border-blue-500 bg-blue-50 dark:border-blue-400 dark:bg-blue-900/20" : "border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800/50"} ${!hasSample ? "opacity-50 pointer-events-none" : ""}`,
857 children: [
858 /* @__PURE__ */ jsxRuntimeExports.jsx(
859 "input",
860 {
861 id: `webhook-select-${eventKey}`,
862 type: "radio",
863 name: "webhook-field-mode",
864 checked: !sendAll,
865 onChange: () => onChangeSendAll(false),
866 disabled: !hasSample,
867 className: "h-4 w-4 mt-0.5 flex-shrink-0"
868 }
869 ),
870 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0", children: [
871 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-sm font-medium text-gray-900 dark:text-white", children: [
872 __("Send only selected fields", "yatra"),
873 !hasSample && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ml-2 text-xs font-normal text-gray-500 dark:text-gray-400", children: [
874 "(",
875 __("capture a sample first", "yatra"),
876 ")"
877 ] })
878 ] }),
879 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-gray-500 dark:text-gray-400 mt-0.5", children: __(
880 "Only the dot-paths you tick will ship in data{…}. Nested structure is preserved (picking customer.email keeps it nested, not flattened). Useful to strip PII or fields a downstream system bills you for.",
881 "yatra"
882 ) })
883 ] })
884 ]
885 }
886 ),
887 !sendAll && hasSample && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2 pt-2 border-t border-gray-200 dark:border-gray-700", children: [
888 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-gray-500 dark:text-gray-400", children: __(
889 "Each row is a dot-path inside data{}. The value shown is from the real captured sample — your receiver will get that same key, at that same nesting.",
890 "yatra"
891 ) }),
892 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-2 flex-wrap", children: [
893 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-xs text-gray-500 dark:text-gray-400", children: [
894 selectedFields.length,
895 " / ",
896 paths.length,
897 " ",
898 __("fields selected", "yatra")
899 ] }),
900 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-1", children: [
901 /* @__PURE__ */ jsxRuntimeExports.jsx(
902 Button,
903 {
904 type: "button",
905 variant: "outline",
906 size: "sm",
907 onClick: onSelectAll,
908 disabled: paths.length === 0,
909 children: __("Select all", "yatra")
910 }
911 ),
912 /* @__PURE__ */ jsxRuntimeExports.jsx(
913 Button,
914 {
915 type: "button",
916 variant: "outline",
917 size: "sm",
918 onClick: onClear,
919 disabled: selectedFields.length === 0,
920 children: __("Clear", "yatra")
921 }
922 )
923 ] })
924 ] }),
925 isLoading ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-2 max-h-96 overflow-y-auto pr-1 animate-pulse", children: Array.from({ length: 8 }).map((_, i) => /* @__PURE__ */ jsxRuntimeExports.jsx(
926 "div",
927 {
928 className: "h-9 rounded bg-gray-200 dark:bg-gray-700"
929 },
930 i
931 )) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-2 max-h-96 overflow-y-auto pr-1", children: paths.map((row) => {
932 const checked = selectedFields.includes(row.path);
933 return /* @__PURE__ */ jsxRuntimeExports.jsxs(
934 "label",
935 {
936 htmlFor: `field-${row.path}`,
937 className: `flex items-start gap-2 p-2.5 rounded-md border cursor-pointer transition-colors ${checked ? "border-blue-500 bg-blue-50/60 dark:border-blue-400 dark:bg-blue-900/20" : "border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800/50"}`,
938 children: [
939 /* @__PURE__ */ jsxRuntimeExports.jsx(
940 "input",
941 {
942 id: `field-${row.path}`,
943 type: "checkbox",
944 checked,
945 onChange: () => onToggle(row.path),
946 className: "h-4 w-4 mt-0.5 rounded border-gray-300 flex-shrink-0"
947 }
948 ),
949 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0 flex-1", children: [
950 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "text-xs font-mono text-gray-900 dark:text-white break-all", children: row.path }),
951 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[11px] text-gray-500 dark:text-gray-400 mt-0.5 truncate", children: formatSample(row.sample) })
952 ] })
953 ]
954 },
955 row.path
956 );
957 }) })
958 ] }),
959 showRawPayload && samplePayload && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "pt-3 border-t border-gray-200 dark:border-gray-700 space-y-2", children: [
960 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-2 flex-wrap", children: [
961 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "inline-flex rounded-md border border-gray-200 dark:border-gray-700 overflow-hidden", children: [
962 /* @__PURE__ */ jsxRuntimeExports.jsx(
963 "button",
964 {
965 type: "button",
966 onClick: () => setPayloadView("output"),
967 className: `px-3 py-1.5 text-xs font-medium transition-colors ${payloadView === "output" ? "bg-blue-600 text-white" : "bg-white text-gray-700 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"}`,
968 children: __("Output (what your receiver gets)", "yatra")
969 }
970 ),
971 /* @__PURE__ */ jsxRuntimeExports.jsx(
972 "button",
973 {
974 type: "button",
975 onClick: () => setPayloadView("raw"),
976 className: `px-3 py-1.5 text-xs font-medium border-l border-gray-200 dark:border-gray-700 transition-colors ${payloadView === "raw" ? "bg-blue-600 text-white" : "bg-white text-gray-700 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"}`,
977 children: __("Input (full event data)", "yatra")
978 }
979 )
980 ] }),
981 /* @__PURE__ */ jsxRuntimeExports.jsx(
982 CopyButton,
983 {
984 text: JSON.stringify(
985 payloadView === "output" ? applyFieldFilter(
986 samplePayload,
987 sendAll ? [] : selectedFields
988 ) : samplePayload,
989 null,
990 2
991 )
992 }
993 )
994 ] }),
995 /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "text-xs font-mono bg-gray-900 text-gray-100 dark:bg-black rounded-md p-3 max-h-80 overflow-auto whitespace-pre-wrap", children: JSON.stringify(
996 payloadView === "output" ? applyFieldFilter(
997 samplePayload,
998 sendAll ? [] : selectedFields
999 ) : samplePayload,
1000 null,
1001 2
1002 ) }),
1003 payloadView === "output" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "rounded-md border border-blue-200 dark:border-blue-800 bg-blue-50/60 dark:bg-blue-900/20 p-2.5", children: /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-blue-800 dark:text-blue-300", children: sendAll || selectedFields.length === 0 ? __(
1004 "Send full payload mode — every field above ships in the POST body, exactly as shown.",
1005 "yatra"
1006 ) : __(
1007 "These are the exact JSON bytes that will be POSTed to your receiver. Keys stay nested under their original paths.",
1008 "yatra"
1009 ) }) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-gray-500 dark:text-gray-400", children: sampleSource === "captured" ? __("Captured live from a real event firing.", "yatra") : __(
1010 "Pulled from the most recent successful delivery for this event.",
1011 "yatra"
1012 ) })
1013 ] })
1014 ] })
1015 ] });
1016 };
1017 const EndpointEditForm = ({ existing, events, onClose, onCreated }) => {
1018 var _a, _b, _c, _d, _e;
1019 const queryClient = useQueryClient();
1020 const { showToast } = useToast();
1021 const isCreate = existing === null;
1022 const [name, setName] = reactExports.useState((existing == null ? void 0 : existing.name) ?? "");
1023 const [url, setUrl] = reactExports.useState((existing == null ? void 0 : existing.url) ?? "");
1024 const [description, setDescription] = reactExports.useState((existing == null ? void 0 : existing.description) ?? "");
1025 const [eventKey, setEventKey] = reactExports.useState((existing == null ? void 0 : existing.event) ?? "");
1026 const [showEventDropdown, setShowEventDropdown] = reactExports.useState(false);
1027 const [httpMethod, setHttpMethod] = reactExports.useState(
1028 (existing == null ? void 0 : existing.http_method) ?? "POST"
1029 );
1030 const [isActive, setIsActive] = reactExports.useState((existing == null ? void 0 : existing.is_active) ?? true);
1031 const [logDeliveries, setLogDeliveries] = reactExports.useState(
1032 (existing == null ? void 0 : existing.log_deliveries) ?? true
1033 );
1034 const [selectedFields, setSelectedFields] = reactExports.useState(
1035 (existing == null ? void 0 : existing.selected_fields) ?? []
1036 );
1037 const [sendAllData, setSendAllData] = reactExports.useState(
1038 (((_a = existing == null ? void 0 : existing.selected_fields) == null ? void 0 : _a.length) ?? 0) === 0
1039 );
1040 const [headerRows, setHeaderRows] = reactExports.useState(
1041 () => objectToRows((existing == null ? void 0 : existing.headers) ?? {})
1042 );
1043 const [extraRows, setExtraRows] = reactExports.useState(
1044 () => objectToRows((existing == null ? void 0 : existing.additional_payload_fields) ?? {})
1045 );
1046 const [useCustomSecret, setUseCustomSecret] = reactExports.useState(false);
1047 const [customSecret, setCustomSecret] = reactExports.useState("");
1048 const buildPayload = () => {
1049 const payload = {
1050 name,
1051 url,
1052 description,
1053 event: eventKey,
1054 http_method: httpMethod,
1055 headers: rowsToObject(headerRows),
1056 additional_payload_fields: rowsToObject(extraRows),
1057 // Empty list = "send everything"; the server keeps null in DB.
1058 selected_fields: sendAllData ? [] : selectedFields,
1059 is_active: isActive,
1060 log_deliveries: logDeliveries
1061 };
1062 if (useCustomSecret && customSecret.trim() !== "") {
1063 payload.custom_secret = customSecret.trim();
1064 }
1065 return payload;
1066 };
1067 const createMutation = useMutation({
1068 mutationFn: () => webhooksApi.createEndpoint(buildPayload()),
1069 onSuccess: (res) => {
1070 queryClient.invalidateQueries({ queryKey: ["webhooks-endpoints"] });
1071 onCreated(res.secret, res.data.name);
1072 },
1073 onError: (e) => showToast(extractError(e), "error")
1074 });
1075 const updateMutation = useMutation({
1076 mutationFn: () => webhooksApi.updateEndpoint(existing.id, buildPayload()),
1077 onSuccess: () => {
1078 queryClient.invalidateQueries({ queryKey: ["webhooks-endpoints"] });
1079 showToast(__("Endpoint updated.", "yatra"), "success");
1080 onClose();
1081 },
1082 onError: (e) => showToast(extractError(e), "error")
1083 });
1084 const selectedEvent = reactExports.useMemo(
1085 () => events.find((e) => e.key === eventKey) ?? null,
1086 [events, eventKey]
1087 );
1088 const sampleQuery = useQuery({
1089 queryKey: ["webhooks-event-sample", eventKey],
1090 queryFn: () => webhooksApi.getEventSample(eventKey),
1091 enabled: eventKey !== "",
1092 staleTime: 3e4
1093 });
1094 const samplePaths = ((_b = sampleQuery.data) == null ? void 0 : _b.paths) ?? [];
1095 const samplePayload = ((_c = sampleQuery.data) == null ? void 0 : _c.payload) ?? null;
1096 const sampleSource = ((_d = sampleQuery.data) == null ? void 0 : _d.source) ?? null;
1097 const sampleCapturedAt = ((_e = sampleQuery.data) == null ? void 0 : _e.captured_at) ?? null;
1098 const toggleField = (path) => {
1099 setSelectedFields(
1100 (prev) => prev.includes(path) ? prev.filter((p) => p !== path) : [...prev, path]
1101 );
1102 };
1103 const selectAllFields = () => setSelectedFields(samplePaths.map((p) => p.path));
1104 const clearAllFields = () => setSelectedFields([]);
1105 const customSecretValid = !useCustomSecret || customSecret.trim().length >= 24;
1106 const fieldSelectionValid = sendAllData || selectedFields.length > 0;
1107 const canSave = name.trim() !== "" && url.trim() !== "" && eventKey !== "" && customSecretValid && fieldSelectionValid && !createMutation.isPending && !updateMutation.isPending;
1108 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
1109 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-3 flex-wrap", children: [
1110 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-lg font-semibold text-gray-900 dark:text-white", children: isCreate ? __("Add endpoint", "yatra") : __("Edit endpoint", "yatra") }),
1111 /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "outline", onClick: onClose, children: __("Cancel", "yatra") })
1112 ] }),
1113 /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
1114 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
1115 /* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { children: __("Destination", "yatra") }),
1116 /* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __(
1117 "HTTPS-only. We sign every request with HMAC-SHA256 — your receiver verifies the X-Yatra-Signature header against your stored secret.",
1118 "yatra"
1119 ) })
1120 ] }),
1121 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-4", children: [
1122 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1123 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "webhook-name", children: __("Name", "yatra") }),
1124 /* @__PURE__ */ jsxRuntimeExports.jsx(
1125 Input,
1126 {
1127 id: "webhook-name",
1128 value: name,
1129 onChange: (e) => setName(e.target.value),
1130 placeholder: __("Zapier — booking sync", "yatra"),
1131 className: "mt-1"
1132 }
1133 )
1134 ] }),
1135 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1136 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "webhook-url", children: __("URL", "yatra") }),
1137 /* @__PURE__ */ jsxRuntimeExports.jsx(
1138 Input,
1139 {
1140 id: "webhook-url",
1141 type: "url",
1142 value: url,
1143 onChange: (e) => setUrl(e.target.value),
1144 placeholder: "https://hooks.zapier.com/hooks/catch/12345/abcdef/",
1145 className: "mt-1 font-mono text-sm"
1146 }
1147 ),
1148 url.trim().startsWith("http://") ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-1 flex items-start gap-1.5 text-xs text-amber-700 dark:text-amber-400", children: [
1149 /* @__PURE__ */ jsxRuntimeExports.jsx(AlertTriangle, { className: "h-3.5 w-3.5 mt-0.5 flex-shrink-0" }),
1150 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: __(
1151 "Insecure URL — http:// is only accepted in WP_DEBUG mode for local development. Production receivers must use https://.",
1152 "yatra"
1153 ) })
1154 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: __("Must use HTTPS in production.", "yatra") })
1155 ] }),
1156 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1157 /* @__PURE__ */ jsxRuntimeExports.jsxs(Label, { htmlFor: "webhook-description", children: [
1158 __("Description", "yatra"),
1159 " ",
1160 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs font-normal text-gray-500", children: [
1161 "(",
1162 __("optional", "yatra"),
1163 ")"
1164 ] })
1165 ] }),
1166 /* @__PURE__ */ jsxRuntimeExports.jsx(
1167 Input,
1168 {
1169 id: "webhook-description",
1170 value: description,
1171 onChange: (e) => setDescription(e.target.value),
1172 placeholder: __("Internal CRM ingest — sales team", "yatra"),
1173 className: "mt-1"
1174 }
1175 )
1176 ] }),
1177 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1178 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "webhook-method", children: __("HTTP method", "yatra") }),
1179 /* @__PURE__ */ jsxRuntimeExports.jsxs(
1180 Select,
1181 {
1182 id: "webhook-method",
1183 value: httpMethod,
1184 onChange: (e) => setHttpMethod(e.target.value),
1185 className: "mt-1",
1186 children: [
1187 /* @__PURE__ */ jsxRuntimeExports.jsxs("option", { value: "POST", children: [
1188 "POST ",
1189 __("(default — recommended)", "yatra")
1190 ] }),
1191 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "PUT", children: "PUT" }),
1192 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "PATCH", children: "PATCH" }),
1193 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "DELETE", children: "DELETE" }),
1194 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "GET", children: "GET" })
1195 ]
1196 }
1197 ),
1198 httpMethod === "GET" ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-1 flex items-start gap-1.5 text-xs text-amber-700 dark:text-amber-400", children: [
1199 /* @__PURE__ */ jsxRuntimeExports.jsx(AlertTriangle, { className: "h-3.5 w-3.5 mt-0.5 flex-shrink-0" }),
1200 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: __(
1201 "GET deliveries send the payload as a ?payload=<json> query parameter (URL-encoded). The HMAC signature is then computed over an empty body — receivers should reconstruct the signed string as `timestamp + '.' + ''`. Only use GET when the receiver explicitly requires it.",
1202 "yatra"
1203 ) })
1204 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: __(
1205 "POST is the universal webhook convention. PUT / PATCH / DELETE are for receivers that explicitly require them (upsert, partial update, object-removal mirrors).",
1206 "yatra"
1207 ) })
1208 ] }),
1209 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-3 rounded-md border border-gray-200 dark:border-gray-700 p-3", children: [
1210 /* @__PURE__ */ jsxRuntimeExports.jsxs(
1211 Label,
1212 {
1213 htmlFor: "webhook-is-active",
1214 className: "font-normal cursor-pointer",
1215 children: [
1216 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "block text-sm font-medium text-gray-900 dark:text-white", children: __("Active", "yatra") }),
1217 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "block text-xs text-gray-500 dark:text-gray-400 mt-0.5", children: __(
1218 "Start delivering events as soon as this is saved.",
1219 "yatra"
1220 ) })
1221 ]
1222 }
1223 ),
1224 /* @__PURE__ */ jsxRuntimeExports.jsx(
1225 Switch,
1226 {
1227 checked: isActive,
1228 onCheckedChange: setIsActive,
1229 className: "flex-shrink-0"
1230 }
1231 )
1232 ] }),
1233 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-3 rounded-md border border-gray-200 dark:border-gray-700 p-3", children: [
1234 /* @__PURE__ */ jsxRuntimeExports.jsxs(
1235 Label,
1236 {
1237 htmlFor: "webhook-log-deliveries",
1238 className: "font-normal cursor-pointer",
1239 children: [
1240 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "block text-sm font-medium text-gray-900 dark:text-white", children: __("Log delivery payloads", "yatra") }),
1241 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "block text-xs text-gray-500 dark:text-gray-400 mt-0.5", children: logDeliveries ? __(
1242 "Stores the full POST body + receiver response on every attempt (90-day retention). Recommended for debugging.",
1243 "yatra"
1244 ) : __(
1245 "Only metadata (status, duration, attempts) is stored. Saves disk on high-volume endpoints, but the inspector won't show payload or response bodies.",
1246 "yatra"
1247 ) })
1248 ]
1249 }
1250 ),
1251 /* @__PURE__ */ jsxRuntimeExports.jsx(
1252 Switch,
1253 {
1254 checked: logDeliveries,
1255 onCheckedChange: setLogDeliveries,
1256 className: "flex-shrink-0"
1257 }
1258 )
1259 ] })
1260 ] })
1261 ] }),
1262 /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
1263 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
1264 /* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { children: __("Trigger event", "yatra") }),
1265 /* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __(
1266 "Pick the single event that triggers this endpoint. The field selector below will then show you the exact payload shape so you can choose which data to forward.",
1267 "yatra"
1268 ) })
1269 ] }),
1270 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { children: [
1271 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [
1272 /* @__PURE__ */ jsxRuntimeExports.jsxs(
1273 "button",
1274 {
1275 type: "button",
1276 onClick: () => setShowEventDropdown(!showEventDropdown),
1277 className: "w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-left flex items-center justify-between hover:border-gray-400 dark:hover:border-gray-500 transition-colors",
1278 children: [
1279 selectedEvent ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [
1280 /* @__PURE__ */ jsxRuntimeExports.jsx(Zap, { className: "w-4 h-4 text-blue-500 flex-shrink-0" }),
1281 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-medium text-gray-900 dark:text-white truncate", children: selectedEvent.name }),
1282 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "text-xs px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 rounded font-mono", children: selectedEvent.key })
1283 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
1284 /* @__PURE__ */ jsxRuntimeExports.jsx(Zap, { className: "w-4 h-4 text-gray-400" }),
1285 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: __("Choose an event…", "yatra") })
1286 ] }),
1287 /* @__PURE__ */ jsxRuntimeExports.jsx(
1288 ChevronDown,
1289 {
1290 className: `w-4 h-4 text-gray-400 transition-transform ${showEventDropdown ? "rotate-180" : ""}`
1291 }
1292 )
1293 ]
1294 }
1295 ),
1296 showEventDropdown && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg max-h-80 overflow-y-auto", children: [
1297 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-2 border-b border-gray-200 dark:border-gray-700", children: /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 px-2", children: __(
1298 "Select the Yatra event that should fire this webhook",
1299 "yatra"
1300 ) }) }),
1301 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "py-1", children: events.map((event) => /* @__PURE__ */ jsxRuntimeExports.jsx(
1302 "button",
1303 {
1304 type: "button",
1305 onClick: () => {
1306 setEventKey(event.key);
1307 setShowEventDropdown(false);
1308 if (event.key !== eventKey) {
1309 setSelectedFields([]);
1310 setSendAllData(true);
1311 }
1312 },
1313 className: `w-full px-3 py-3 text-left hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors border-b border-gray-100 dark:border-gray-700 last:border-b-0 ${eventKey === event.key ? "bg-indigo-50 dark:bg-indigo-900/20" : ""}`,
1314 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-2", children: [
1315 /* @__PURE__ */ jsxRuntimeExports.jsx(
1316 Zap,
1317 {
1318 className: `w-4 h-4 mt-0.5 flex-shrink-0 ${eventKey === event.key ? "text-blue-500" : "text-gray-400"}`
1319 }
1320 ),
1321 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [
1322 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
1323 /* @__PURE__ */ jsxRuntimeExports.jsx(
1324 "span",
1325 {
1326 className: `text-sm font-medium ${eventKey === event.key ? "text-blue-800 dark:text-blue-400" : "text-gray-900 dark:text-white"}`,
1327 children: event.name
1328 }
1329 ),
1330 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "text-xs px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 rounded", children: event.key })
1331 ] }),
1332 event.description && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-0.5 line-clamp-2", children: event.description })
1333 ] }),
1334 eventKey === event.key && /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "w-4 h-4 text-blue-500 flex-shrink-0" })
1335 ] })
1336 },
1337 event.key
1338 )) })
1339 ] }),
1340 showEventDropdown && /* @__PURE__ */ jsxRuntimeExports.jsx(
1341 "div",
1342 {
1343 className: "fixed inset-0 z-40",
1344 onClick: () => setShowEventDropdown(false)
1345 }
1346 )
1347 ] }),
1348 (selectedEvent == null ? void 0 : selectedEvent.description) && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-3 p-3 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg border border-indigo-200 dark:border-indigo-800", children: /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-indigo-800 dark:text-indigo-300", children: selectedEvent.description }) })
1349 ] })
1350 ] }),
1351 eventKey !== "" && /* @__PURE__ */ jsxRuntimeExports.jsx(
1352 FieldSelectorCard,
1353 {
1354 eventKey,
1355 eventName: (selectedEvent == null ? void 0 : selectedEvent.name) ?? eventKey,
1356 paths: samplePaths,
1357 isLoading: sampleQuery.isLoading,
1358 sendAll: sendAllData,
1359 onChangeSendAll: (next) => {
1360 setSendAllData(next);
1361 if (next) setSelectedFields([]);
1362 },
1363 selectedFields,
1364 onToggle: toggleField,
1365 onSelectAll: selectAllFields,
1366 onClear: clearAllFields,
1367 samplePayload,
1368 sampleSource,
1369 sampleCapturedAt
1370 }
1371 ),
1372 /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
1373 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
1374 /* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { children: __("Custom HTTP headers", "yatra") }),
1375 /* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __(
1376 "Sent alongside Yatra's signed headers on every request. Useful for receivers that require Bearer / Basic auth on TOP of the HMAC signature, or for tagging requests for routing. Reserved headers (X-Yatra-*, Content-Length, Host, Cookie) are blocked.",
1377 "yatra"
1378 ) })
1379 ] }),
1380 /* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1381 KeyValueEditor,
1382 {
1383 rows: headerRows,
1384 onChange: setHeaderRows,
1385 keyPlaceholder: __("Header name (e.g. Authorization)", "yatra"),
1386 valuePlaceholder: __("Header value (e.g. Bearer xyz)", "yatra"),
1387 idPrefix: "webhook-header"
1388 }
1389 ) })
1390 ] }),
1391 /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
1392 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
1393 /* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { children: __("Additional payload fields", "yatra") }),
1394 /* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __(
1395 `Static fields merged into every payload's "data" block. Use this to tag events with operator-specific metadata (tenant_id, environment, region, source_app) without writing a server-side filter. Operator fields never overwrite Yatra's canonical entity fields.`,
1396 "yatra"
1397 ) })
1398 ] }),
1399 /* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1400 KeyValueEditor,
1401 {
1402 rows: extraRows,
1403 onChange: setExtraRows,
1404 keyPlaceholder: __("Field key (e.g. tenant_id)", "yatra"),
1405 valuePlaceholder: __("Static value (e.g. acme-prod)", "yatra"),
1406 idPrefix: "webhook-extra"
1407 }
1408 ) })
1409 ] }),
1410 /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
1411 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
1412 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardTitle, { className: "flex items-center gap-2", children: [
1413 /* @__PURE__ */ jsxRuntimeExports.jsx(ShieldCheck, { className: "h-4 w-4 text-green-500" }),
1414 __("Signing secret", "yatra")
1415 ] }),
1416 /* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: isCreate ? __(
1417 "By default Yatra generates a strong random 32-byte signing secret. If your receiver already has a secret configured (e.g. you're migrating from another system), paste it below — minimum 24 characters.",
1418 "yatra"
1419 ) : __(
1420 "Leave unchecked to keep the existing secret. Check to rotate to a new one — receivers using the old secret will start failing signature verification until you update them.",
1421 "yatra"
1422 ) })
1423 ] }),
1424 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-3", children: [
1425 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-3 rounded-md border border-gray-200 dark:border-gray-700 p-3", children: [
1426 /* @__PURE__ */ jsxRuntimeExports.jsxs(
1427 Label,
1428 {
1429 htmlFor: "webhook-use-custom-secret",
1430 className: "font-normal cursor-pointer",
1431 children: [
1432 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "block text-sm font-medium text-gray-900 dark:text-white", children: isCreate ? __("Use my own signing secret", "yatra") : __("Rotate to a new signing secret", "yatra") }),
1433 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "block text-xs text-gray-500 dark:text-gray-400 mt-0.5", children: isCreate ? __(
1434 "Provide a 24+ character secret instead of letting Yatra auto-generate one.",
1435 "yatra"
1436 ) : __(
1437 "Replaces the current secret. Receivers using the old one will start failing signature verification.",
1438 "yatra"
1439 ) })
1440 ]
1441 }
1442 ),
1443 /* @__PURE__ */ jsxRuntimeExports.jsx(
1444 Switch,
1445 {
1446 checked: useCustomSecret,
1447 onCheckedChange: (next) => {
1448 setUseCustomSecret(next);
1449 if (!next) setCustomSecret("");
1450 },
1451 className: "flex-shrink-0"
1452 }
1453 )
1454 ] }),
1455 useCustomSecret && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1456 /* @__PURE__ */ jsxRuntimeExports.jsx(
1457 Input,
1458 {
1459 id: "webhook-custom-secret",
1460 type: "password",
1461 value: customSecret,
1462 onChange: (e) => setCustomSecret(e.target.value),
1463 placeholder: __(
1464 "Paste your signing secret (min 24 chars)",
1465 "yatra"
1466 ),
1467 className: "font-mono text-sm",
1468 "aria-describedby": "webhook-custom-secret-help"
1469 }
1470 ),
1471 /* @__PURE__ */ jsxRuntimeExports.jsx(
1472 "p",
1473 {
1474 id: "webhook-custom-secret-help",
1475 className: "mt-1 text-xs text-gray-500 dark:text-gray-400",
1476 children: customSecret.length > 0 && customSecret.length < 24 ? /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-amber-600 dark:text-amber-400", children: [
1477 __("Too short — need at least 24 characters.", "yatra"),
1478 " (",
1479 customSecret.length,
1480 "/24)"
1481 ] }) : __(
1482 "Stored encrypted at rest. Treat it like a password.",
1483 "yatra"
1484 )
1485 }
1486 )
1487 ] })
1488 ] })
1489 ] }),
1490 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2", children: [
1491 /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "outline", onClick: onClose, children: __("Cancel", "yatra") }),
1492 /* @__PURE__ */ jsxRuntimeExports.jsx(
1493 Button,
1494 {
1495 disabled: !canSave,
1496 onClick: () => isCreate ? createMutation.mutate() : updateMutation.mutate(),
1497 children: createMutation.isPending || updateMutation.isPending ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1498 /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }),
1499 __("Saving…", "yatra")
1500 ] }) : isCreate ? __("Create endpoint", "yatra") : __("Save changes", "yatra")
1501 }
1502 )
1503 ] })
1504 ] });
1505 };
1506 const SecretRevealDialog = ({ secret, endpointName, onClose }) => {
1507 const [copied, setCopied] = reactExports.useState(false);
1508 const [snippetLang, setSnippetLang] = reactExports.useState(
1509 "php"
1510 );
1511 const doCopy = async () => {
1512 if (!secret) return;
1513 try {
1514 await navigator.clipboard.writeText(secret);
1515 setCopied(true);
1516 window.setTimeout(() => setCopied(false), 2e3);
1517 } catch (_e) {
1518 }
1519 };
1520 if (!secret) return null;
1521 const snippets = {
1522 php: `// PHP receiver verification
1523 $signature = $_SERVER['HTTP_X_YATRA_SIGNATURE'] ?? '';
1524 $body = file_get_contents('php://input');
1525 $parts = [];
1526 foreach (explode(',', $signature) as $kv) {
1527 [$k, $v] = array_pad(explode('=', $kv, 2), 2, '');
1528 $parts[$k] = $v;
1529 }
1530 $ts = (int) ($parts['t'] ?? 0);
1531 $sig = $parts['v1'] ?? '';
1532 // Reject replays > 5 min old
1533 if (abs(time() - $ts) > 300) { http_response_code(400); exit; }
1534 $expected = hash_hmac('sha256', $ts . '.' . $body, '${secret}');
1535 if (!hash_equals($expected, $sig)) { http_response_code(401); exit; }
1536 // Signature valid — process json_decode($body, true)`,
1537 node: `// Node.js (Express) receiver verification
1538 import crypto from 'crypto';
1539 app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => {
1540 const sig = req.header('x-yatra-signature') || '';
1541 const parts = Object.fromEntries(
1542 sig.split(',').map(kv => kv.split('=', 2))
1543 );
1544 const ts = parseInt(parts.t || '0', 10);
1545 // Reject replays > 5 min old
1546 if (Math.abs(Date.now() / 1000 - ts) > 300) return res.status(400).end();
1547 const expected = crypto
1548 .createHmac('sha256', '${secret}')
1549 .update(\`\${ts}.\${req.body}\`)
1550 .digest('hex');
1551 if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ''))) {
1552 return res.status(401).end();
1553 }
1554 const event = JSON.parse(req.body);
1555 // …handle event
1556 });`,
1557 python: `# Python (Flask) receiver verification
1558 import hmac, hashlib, time
1559 from flask import request, abort
1560
1561 @app.route('/webhook', methods=['POST'])
1562 def webhook():
1563 sig = request.headers.get('X-Yatra-Signature', '')
1564 parts = dict(kv.split('=', 1) for kv in sig.split(',') if '=' in kv)
1565 ts = int(parts.get('t', '0'))
1566 if abs(time.time() - ts) > 300:
1567 abort(400) # replay protection
1568 expected = hmac.new(
1569 b'${secret}',
1570 f"{ts}.{request.data.decode()}".encode(),
1571 hashlib.sha256
1572 ).hexdigest()
1573 if not hmac.compare_digest(expected, parts.get('v1', '')):
1574 abort(401)
1575 event = request.get_json()
1576 # …handle event`
1577 };
1578 return /* @__PURE__ */ jsxRuntimeExports.jsx(
1579 Modal,
1580 {
1581 isOpen: true,
1582 onClose,
1583 title: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
1584 /* @__PURE__ */ jsxRuntimeExports.jsx(ShieldCheck, { className: "w-5 h-5 text-green-500" }),
1585 __("Signing secret", "yatra")
1586 ] }),
1587 size: "lg",
1588 hideFooter: false,
1589 footer: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end gap-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { onClick: onClose, children: __("I've saved it", "yatra") }) }),
1590 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
1591 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-sm text-gray-700 dark:text-gray-200", children: [
1592 __("Below is the signing secret for ", "yatra"),
1593 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: endpointName }),
1594 ".",
1595 " ",
1596 __(
1597 "It will not be shown again — copy it now and store it in your receiver's configuration. If you lose it, you'll need to regenerate (which invalidates the old one).",
1598 "yatra"
1599 )
1600 ] }),
1601 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
1602 /* @__PURE__ */ jsxRuntimeExports.jsx(
1603 Input,
1604 {
1605 value: secret,
1606 readOnly: true,
1607 onFocus: (e) => e.currentTarget.select(),
1608 className: "font-mono text-sm",
1609 "aria-label": __("Signing secret value", "yatra")
1610 }
1611 ),
1612 /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { onClick: doCopy, variant: "outline", children: copied ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1613 /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "mr-1.5 h-4 w-4" }),
1614 __("Copied", "yatra")
1615 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1616 /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { className: "mr-1.5 h-4 w-4" }),
1617 __("Copy", "yatra")
1618 ] }) })
1619 ] }),
1620 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1621 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-1 gap-2 flex-wrap", children: [
1622 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { className: "text-sm font-semibold", children: __("Verify the signature on your receiver", "yatra") }),
1623 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-1", children: [
1624 ["php", "node", "python"].map((lang) => /* @__PURE__ */ jsxRuntimeExports.jsx(
1625 "button",
1626 {
1627 type: "button",
1628 onClick: () => setSnippetLang(lang),
1629 className: `px-2 py-1 rounded text-xs font-medium transition-colors ${snippetLang === lang ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"}`,
1630 children: lang.toUpperCase()
1631 },
1632 lang
1633 )),
1634 /* @__PURE__ */ jsxRuntimeExports.jsx(CopyButton, { text: snippets[snippetLang] })
1635 ] })
1636 ] }),
1637 /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "bg-gray-900 text-gray-100 dark:bg-black rounded-md p-3 text-[11px] font-mono overflow-auto max-h-64 whitespace-pre-wrap", children: snippets[snippetLang] })
1638 ] }),
1639 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-md p-3", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 text-xs text-amber-700 dark:text-amber-400", children: [
1640 /* @__PURE__ */ jsxRuntimeExports.jsx(AlertTriangle, { className: "h-4 w-4 flex-shrink-0 mt-0.5" }),
1641 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: __(
1642 "Treat this secret like a password. Anyone with it can forge requests that appear to come from Yatra.",
1643 "yatra"
1644 ) })
1645 ] }) })
1646 ] })
1647 }
1648 );
1649 };
1650 const DeliveriesTab = () => {
1651 const [page, setPage] = reactExports.useState(1);
1652 const [statusFilter, setStatusFilter] = reactExports.useState("");
1653 const [endpointFilter, setEndpointFilter] = reactExports.useState(0);
1654 const [eventFilter, setEventFilter] = reactExports.useState("");
1655 const [inspectId, setInspectId] = reactExports.useState(null);
1656 const perPage = 25;
1657 React.useEffect(() => {
1658 setPage(1);
1659 }, [statusFilter, endpointFilter, eventFilter]);
1660 const { data, isLoading } = useQuery({
1661 queryKey: [
1662 "webhooks-deliveries",
1663 page,
1664 perPage,
1665 statusFilter,
1666 endpointFilter,
1667 eventFilter
1668 ],
1669 queryFn: () => webhooksApi.listDeliveries({
1670 page,
1671 per_page: perPage,
1672 ...statusFilter ? { status: statusFilter } : {},
1673 ...endpointFilter ? { endpoint_id: endpointFilter } : {},
1674 ...eventFilter ? { event_key: eventFilter } : {}
1675 }),
1676 refetchInterval: 1e4,
1677 placeholderData: (prev) => prev
1678 });
1679 const { data: endpointsData } = useQuery({
1680 queryKey: ["webhooks-endpoints"],
1681 queryFn: () => webhooksApi.listEndpoints()
1682 });
1683 const { data: eventsData } = useQuery({
1684 queryKey: ["webhooks-events"],
1685 queryFn: () => webhooksApi.listEvents()
1686 });
1687 const queryClient = useQueryClient();
1688 const { showToast } = useToast();
1689 const replayMutation = useMutation({
1690 mutationFn: (id) => webhooksApi.replayDelivery(id),
1691 onSuccess: () => {
1692 queryClient.invalidateQueries({ queryKey: ["webhooks-deliveries"] });
1693 showToast(__("Delivery re-queued.", "yatra"), "success");
1694 },
1695 onError: (e) => showToast(extractError(e), "error")
1696 });
1697 const rows = (data == null ? void 0 : data.data) ?? [];
1698 const total = (data == null ? void 0 : data.total) ?? 0;
1699 const totalPages = Math.max(1, Math.ceil(total / perPage));
1700 const hasFilters = !!(statusFilter || endpointFilter || eventFilter);
1701 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
1702 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1703 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2", children: [
1704 /* @__PURE__ */ jsxRuntimeExports.jsx(Clock, { className: "w-5 h-5 text-blue-500" }),
1705 __("Delivery log", "yatra")
1706 ] }),
1707 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: __(
1708 "Every outbound POST attempt with status, response, and the full payload that was sent. Click any row to inspect the JSON your receiver got.",
1709 "yatra"
1710 ) })
1711 ] }),
1712 /* @__PURE__ */ jsxRuntimeExports.jsx(Card, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { className: "p-3", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col lg:flex-row lg:items-center gap-2", children: [
1713 /* @__PURE__ */ jsxRuntimeExports.jsxs(
1714 Select,
1715 {
1716 value: String(endpointFilter),
1717 onChange: (e) => setEndpointFilter(Number(e.target.value)),
1718 "aria-label": __("Filter by endpoint", "yatra"),
1719 className: "w-full lg:w-64 lg:flex-none",
1720 children: [
1721 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "0", children: __("All endpoints", "yatra") }),
1722 ((endpointsData == null ? void 0 : endpointsData.data) ?? []).map((ep) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: ep.id, children: ep.name }, ep.id))
1723 ]
1724 }
1725 ),
1726 /* @__PURE__ */ jsxRuntimeExports.jsxs(
1727 Select,
1728 {
1729 value: eventFilter,
1730 onChange: (e) => setEventFilter(e.target.value),
1731 "aria-label": __("Filter by event", "yatra"),
1732 className: "w-full lg:w-56 lg:flex-none",
1733 children: [
1734 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: __("All events", "yatra") }),
1735 ((eventsData == null ? void 0 : eventsData.data) ?? []).map((ev) => /* @__PURE__ */ jsxRuntimeExports.jsxs("option", { value: ev.key, children: [
1736 ev.name,
1737 " (",
1738 ev.key,
1739 ")"
1740 ] }, ev.key))
1741 ]
1742 }
1743 ),
1744 /* @__PURE__ */ jsxRuntimeExports.jsxs(
1745 Select,
1746 {
1747 value: statusFilter,
1748 onChange: (e) => setStatusFilter(e.target.value),
1749 "aria-label": __("Filter by status", "yatra"),
1750 className: "w-full lg:w-48 lg:flex-none",
1751 children: [
1752 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: __("All statuses", "yatra") }),
1753 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "queued", children: __("Queued", "yatra") }),
1754 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "delivering", children: __("Delivering", "yatra") }),
1755 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "delivered", children: __("Delivered", "yatra") }),
1756 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "failed", children: __("Failed (retrying)", "yatra") }),
1757 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "permanent_failure", children: __("Permanent failure", "yatra") })
1758 ]
1759 }
1760 ),
1761 hasFilters && /* @__PURE__ */ jsxRuntimeExports.jsx(
1762 Button,
1763 {
1764 variant: "outline",
1765 onClick: () => {
1766 setStatusFilter("");
1767 setEndpointFilter(0);
1768 setEventFilter("");
1769 },
1770 className: "w-full lg:w-auto",
1771 children: __("Reset", "yatra")
1772 }
1773 )
1774 ] }) }) }),
1775 /* @__PURE__ */ jsxRuntimeExports.jsx(Card, { className: "overflow-visible", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "p-0 overflow-visible", children: [
1776 /* @__PURE__ */ jsxRuntimeExports.jsx(
1777 Table,
1778 {
1779 data: rows,
1780 columns: [
1781 {
1782 key: "event_key",
1783 label: __("Event", "yatra"),
1784 render: (d) => {
1785 const ep = endpointsData == null ? void 0 : endpointsData.data.find(
1786 (e) => e.id === d.endpoint_id
1787 );
1788 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0", children: [
1789 /* @__PURE__ */ jsxRuntimeExports.jsx(
1790 "button",
1791 {
1792 type: "button",
1793 onClick: () => setInspectId(d.id),
1794 className: "font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline transition-colors cursor-pointer text-left",
1795 children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-xs", children: d.event_key })
1796 }
1797 ),
1798 /* @__PURE__ */ jsxRuntimeExports.jsxs(
1799 "div",
1800 {
1801 className: "text-xs text-gray-500 dark:text-gray-400 mt-0.5 truncate max-w-md",
1802 title: (ep == null ? void 0 : ep.url) || "",
1803 children: [
1804 "→ ",
1805 (ep == null ? void 0 : ep.name) ?? `#${d.endpoint_id}`
1806 ]
1807 }
1808 )
1809 ] });
1810 }
1811 },
1812 {
1813 key: "status",
1814 label: __("Status", "yatra"),
1815 render: (d) => /* @__PURE__ */ jsxRuntimeExports.jsx(StatusBadge, { status: d.status, httpStatus: d.http_status })
1816 },
1817 {
1818 key: "attempts",
1819 label: __("Attempts", "yatra"),
1820 render: (d) => /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-gray-600 dark:text-gray-300", children: d.attempts })
1821 },
1822 {
1823 key: "duration_ms",
1824 label: __("Duration", "yatra"),
1825 render: (d) => d.duration_ms !== null ? /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap", children: [
1826 d.duration_ms,
1827 " ms"
1828 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-gray-400", children: "—" })
1829 },
1830 {
1831 key: "created_at",
1832 label: __("Created", "yatra"),
1833 render: (d) => /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap", children: d.created_at ? new Date(d.created_at).toLocaleString() : "—" })
1834 }
1835 ],
1836 actions: [
1837 {
1838 key: "inspect",
1839 label: __("Inspect payload + response", "yatra"),
1840 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "w-4 h-4" }),
1841 onClick: (d) => setInspectId(d.id)
1842 },
1843 {
1844 key: "replay",
1845 label: __("Replay delivery", "yatra"),
1846 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { className: "w-4 h-4" }),
1847 onClick: (d) => replayMutation.mutate(d.id),
1848 condition: (d) => d.status === "failed" || d.status === "permanent_failure"
1849 }
1850 ],
1851 isLoading,
1852 emptyText: total === 0 && !hasFilters ? __("No deliveries yet", "yatra") : __("No matches", "yatra"),
1853 emptyDescription: total === 0 && !hasFilters ? __(
1854 "Deliveries appear here once an event fires. Use the Ping button on an endpoint to fire a test event.",
1855 "yatra"
1856 ) : __("Try adjusting your filters to see more results.", "yatra")
1857 }
1858 ),
1859 rows.length > 0 && totalPages > 1 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-t border-gray-200 px-4 py-3 dark:border-gray-700", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1860 Pagination,
1861 {
1862 currentPage: page,
1863 totalPages,
1864 totalItems: total,
1865 itemsPerPage: perPage,
1866 onPageChange: setPage,
1867 itemName: __("deliveries", "yatra")
1868 }
1869 ) })
1870 ] }) }),
1871 /* @__PURE__ */ jsxRuntimeExports.jsx(
1872 DeliveryInspectDialog,
1873 {
1874 id: inspectId,
1875 onClose: () => setInspectId(null)
1876 }
1877 )
1878 ] });
1879 };
1880 const MtlsDialog = ({ endpoint, onClose }) => {
1881 const queryClient = useQueryClient();
1882 const { showToast } = useToast();
1883 const [cert, setCert] = reactExports.useState("");
1884 const [keyPem, setKeyPem] = reactExports.useState("");
1885 const [passphrase, setPassphrase] = reactExports.useState("");
1886 const [mode, setMode] = reactExports.useState("view");
1887 const enabled = endpoint !== null;
1888 const { data, isLoading } = useQuery({
1889 queryKey: ["webhook-mtls", endpoint == null ? void 0 : endpoint.id],
1890 queryFn: () => webhooksApi.getMtlsHint(endpoint.id),
1891 enabled
1892 });
1893 const hint = data == null ? void 0 : data.data;
1894 React.useEffect(() => {
1895 if (endpoint) {
1896 setCert("");
1897 setKeyPem("");
1898 setPassphrase("");
1899 setMode((hint == null ? void 0 : hint.configured) ? "view" : "edit");
1900 }
1901 }, [endpoint == null ? void 0 : endpoint.id, hint == null ? void 0 : hint.configured]);
1902 const save = useMutation({
1903 mutationFn: () => webhooksApi.setMtls(endpoint.id, { cert, key: keyPem, passphrase }),
1904 onSuccess: (r) => {
1905 showToast(r.message, "success");
1906 setCert("");
1907 setKeyPem("");
1908 setPassphrase("");
1909 setMode("view");
1910 void queryClient.invalidateQueries({
1911 queryKey: ["webhook-mtls", endpoint.id]
1912 });
1913 },
1914 onError: (e) => showToast(extractError(e), "error")
1915 });
1916 const clear = useMutation({
1917 mutationFn: () => webhooksApi.clearMtls(endpoint.id),
1918 onSuccess: (r) => {
1919 showToast(r.message, "success");
1920 void queryClient.invalidateQueries({
1921 queryKey: ["webhook-mtls", endpoint.id]
1922 });
1923 onClose();
1924 },
1925 onError: (e) => showToast(extractError(e), "error")
1926 });
1927 if (!endpoint) return null;
1928 return /* @__PURE__ */ jsxRuntimeExports.jsx(
1929 Modal,
1930 {
1931 isOpen: true,
1932 onClose,
1933 title: __("Client certificate (mTLS)", "yatra"),
1934 size: "xl",
1935 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
1936 /* @__PURE__ */ jsxRuntimeExports.jsx(Alert, { variant: "info", children: __(
1937 "Optional. Some receivers require Yatra to authenticate at the TLS handshake with a client certificate, in addition to the HMAC signature on the body. Paste your PEM-encoded cert and private key below. The pair is encrypted at rest and used only at delivery time.",
1938 "yatra"
1939 ) }),
1940 isLoading ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-center py-6", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-6 w-6 animate-spin text-gray-400" }) }) : mode === "view" && (hint == null ? void 0 : hint.configured) ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-md border border-gray-200 dark:border-gray-700 p-4 space-y-3", children: [
1941 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-sm", children: [
1942 /* @__PURE__ */ jsxRuntimeExports.jsx(CheckCircle2, { className: "h-4 w-4 text-green-500" }),
1943 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-gray-900 dark:text-white", children: __("Client certificate configured", "yatra") })
1944 ] }),
1945 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1 text-xs", children: [
1946 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
1947 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-gray-500 w-24 flex-shrink-0", children: __("Fingerprint", "yatra") }),
1948 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "font-mono break-all text-gray-700 dark:text-gray-300", children: hint.fingerprint || __("(unavailable)", "yatra") })
1949 ] }),
1950 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
1951 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-gray-500 w-24 flex-shrink-0", children: __("Expires", "yatra") }),
1952 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-gray-700 dark:text-gray-300", children: hint.expires_at ? new Date(hint.expires_at).toLocaleString() : __("(unknown)", "yatra") })
1953 ] })
1954 ] }),
1955 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 pt-1", children: [
1956 /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "outline", onClick: () => setMode("edit"), children: __("Replace certificate", "yatra") }),
1957 /* @__PURE__ */ jsxRuntimeExports.jsxs(
1958 Button,
1959 {
1960 variant: "destructive",
1961 onClick: () => clear.mutate(),
1962 disabled: clear.isPending,
1963 children: [
1964 clear.isPending ? /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "h-4 w-4 mr-1" }),
1965 __("Remove certificate", "yatra")
1966 ]
1967 }
1968 )
1969 ] })
1970 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3", children: [
1971 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1972 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "mtls-cert", children: __("Certificate (PEM)", "yatra") }),
1973 /* @__PURE__ */ jsxRuntimeExports.jsx(
1974 "textarea",
1975 {
1976 id: "mtls-cert",
1977 className: "mt-1 w-full font-mono text-xs px-3 py-2 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 min-h-[140px]",
1978 value: cert,
1979 onChange: (e) => setCert(e.target.value),
1980 placeholder: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
1981 }
1982 )
1983 ] }),
1984 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1985 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "mtls-key", children: __("Private key (PEM)", "yatra") }),
1986 /* @__PURE__ */ jsxRuntimeExports.jsx(
1987 "textarea",
1988 {
1989 id: "mtls-key",
1990 className: "mt-1 w-full font-mono text-xs px-3 py-2 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 min-h-[140px]",
1991 value: keyPem,
1992 onChange: (e) => setKeyPem(e.target.value),
1993 placeholder: "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
1994 }
1995 )
1996 ] }),
1997 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1998 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "mtls-pass", children: __("Key passphrase (optional)", "yatra") }),
1999 /* @__PURE__ */ jsxRuntimeExports.jsx(
2000 Input,
2001 {
2002 id: "mtls-pass",
2003 type: "password",
2004 value: passphrase,
2005 onChange: (e) => setPassphrase(e.target.value),
2006 placeholder: __(
2007 "Leave blank if the key is unencrypted",
2008 "yatra"
2009 )
2010 }
2011 )
2012 ] }),
2013 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2 pt-2", children: [
2014 (hint == null ? void 0 : hint.configured) && /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "outline", onClick: () => setMode("view"), children: __("Cancel", "yatra") }),
2015 /* @__PURE__ */ jsxRuntimeExports.jsxs(
2016 Button,
2017 {
2018 onClick: () => save.mutate(),
2019 disabled: !cert || !keyPem || save.isPending,
2020 children: [
2021 save.isPending && /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" }),
2022 __("Save certificate", "yatra")
2023 ]
2024 }
2025 )
2026 ] }),
2027 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-gray-500", children: __(
2028 "The certificate and key are validated together before storage — if they don't match, the save is rejected. Stored encrypted with libsodium / OpenSSL AES-256-GCM.",
2029 "yatra"
2030 ) })
2031 ] })
2032 ] })
2033 }
2034 );
2035 };
2036 const BuriedTab = () => {
2037 const queryClient = useQueryClient();
2038 const { showToast } = useToast();
2039 const [selectedIds, setSelectedIds] = reactExports.useState(/* @__PURE__ */ new Set());
2040 const [inspectId, setInspectId] = reactExports.useState(null);
2041 const { data, isLoading, refetch, isFetching } = useQuery({
2042 queryKey: ["webhook-dead-letter"],
2043 queryFn: () => webhooksApi.getDeadLetterSummary(),
2044 refetchOnWindowFocus: false
2045 });
2046 const { data: endpoints } = useQuery({
2047 queryKey: ["webhook-endpoints"],
2048 queryFn: () => webhooksApi.listEndpoints()
2049 });
2050 const endpointNameById = reactExports.useMemo(() => {
2051 var _a;
2052 const map = /* @__PURE__ */ new Map();
2053 (_a = endpoints == null ? void 0 : endpoints.data) == null ? void 0 : _a.forEach((e) => map.set(e.id, e.name));
2054 return map;
2055 }, [endpoints]);
2056 const bulkReplay = useMutation({
2057 mutationFn: (input) => webhooksApi.bulkReplayDeliveries(input),
2058 onSuccess: (r) => {
2059 showToast(r.message, "success");
2060 setSelectedIds(/* @__PURE__ */ new Set());
2061 void queryClient.invalidateQueries({ queryKey: ["webhook-dead-letter"] });
2062 void queryClient.invalidateQueries({ queryKey: ["webhook-deliveries"] });
2063 },
2064 onError: (e) => showToast(extractError(e), "error")
2065 });
2066 if (isLoading) {
2067 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-center py-16", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-8 w-8 animate-spin text-gray-400" }) });
2068 }
2069 const summary = data == null ? void 0 : data.data;
2070 const total = (summary == null ? void 0 : summary.total) ?? 0;
2071 if (total === 0) {
2072 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-center py-16", children: [
2073 /* @__PURE__ */ jsxRuntimeExports.jsx(CheckCircle2, { className: "w-12 h-12 text-green-500 mx-auto" }),
2074 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "mt-4 text-base font-semibold text-gray-900 dark:text-white", children: __("Nothing buried", "yatra") }),
2075 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mt-1 text-sm text-gray-500 dark:text-gray-400 max-w-md mx-auto", children: __(
2076 "Every delivery either succeeded or is still mid-retry. Permanent failures will show up here so you can replay or investigate.",
2077 "yatra"
2078 ) }),
2079 /* @__PURE__ */ jsxRuntimeExports.jsxs(Button, { variant: "outline", className: "mt-4", onClick: () => refetch(), children: [
2080 /* @__PURE__ */ jsxRuntimeExports.jsx(
2081 RefreshCw,
2082 {
2083 className: `h-4 w-4 mr-1 ${isFetching ? "animate-spin" : ""}`
2084 }
2085 ),
2086 __("Refresh", "yatra")
2087 ] })
2088 ] });
2089 }
2090 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [
2091 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-3 rounded-md border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-950/30 p-4", children: [
2092 /* @__PURE__ */ jsxRuntimeExports.jsx(AlertTriangle, { className: "w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" }),
2093 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [
2094 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-sm font-semibold text-red-900 dark:text-red-100", children: sprintf(
2095 /* translators: %d: number of buried deliveries */
2096 __("%d delivery(ies) reached permanent failure", "yatra"),
2097 total
2098 ) }),
2099 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-red-800 dark:text-red-200/90 mt-0.5", children: __(
2100 "These exhausted automatic retries. Group by endpoint or error pattern below and bulk-replay once the downstream issue is fixed.",
2101 "yatra"
2102 ) })
2103 ] }),
2104 /* @__PURE__ */ jsxRuntimeExports.jsxs(
2105 Button,
2106 {
2107 variant: "outline",
2108 onClick: () => refetch(),
2109 disabled: isFetching,
2110 children: [
2111 /* @__PURE__ */ jsxRuntimeExports.jsx(
2112 RefreshCw,
2113 {
2114 className: `h-4 w-4 mr-1 ${isFetching ? "animate-spin" : ""}`
2115 }
2116 ),
2117 __("Refresh", "yatra")
2118 ]
2119 }
2120 )
2121 ] }),
2122 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-3 gap-4", children: [
2123 /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
2124 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
2125 /* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { className: "text-base", children: __("By endpoint", "yatra") }),
2126 /* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __("Top receivers accumulating failures.", "yatra") })
2127 ] }),
2128 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-2", children: [
2129 summary.by_endpoint.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-gray-500", children: __("None.", "yatra") }),
2130 summary.by_endpoint.map((row) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
2131 "div",
2132 {
2133 className: "flex items-center justify-between gap-2 rounded border border-gray-200 dark:border-gray-700 px-3 py-2",
2134 children: [
2135 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0 flex-1", children: [
2136 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-medium text-gray-900 dark:text-white truncate", children: endpointNameById.get(row.endpoint_id) ?? `Endpoint #${row.endpoint_id}` }),
2137 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-gray-500", children: sprintf(__("%d buried", "yatra"), row.count) })
2138 ] }),
2139 /* @__PURE__ */ jsxRuntimeExports.jsxs(
2140 Button,
2141 {
2142 size: "sm",
2143 variant: "outline",
2144 onClick: () => bulkReplay.mutate({
2145 filter: {
2146 endpoint_id: row.endpoint_id,
2147 status: "permanent_failure"
2148 }
2149 }),
2150 disabled: bulkReplay.isPending,
2151 children: [
2152 /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { className: "h-3.5 w-3.5 mr-1" }),
2153 __("Replay all", "yatra")
2154 ]
2155 }
2156 )
2157 ]
2158 },
2159 row.endpoint_id
2160 ))
2161 ] })
2162 ] }),
2163 /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
2164 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
2165 /* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { className: "text-base", children: __("By event", "yatra") }),
2166 /* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __("Event types failing most.", "yatra") })
2167 ] }),
2168 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-2", children: [
2169 summary.by_event.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-gray-500", children: __("None.", "yatra") }),
2170 summary.by_event.map((row) => /* @__PURE__ */ jsxRuntimeExports.jsx(
2171 "div",
2172 {
2173 className: "flex items-center justify-between gap-2 rounded border border-gray-200 dark:border-gray-700 px-3 py-2",
2174 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0 flex-1", children: [
2175 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-mono text-gray-900 dark:text-white truncate", children: row.event_key || __("(unknown)", "yatra") }),
2176 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-gray-500", children: sprintf(__("%d buried", "yatra"), row.count) })
2177 ] })
2178 },
2179 row.event_key
2180 ))
2181 ] })
2182 ] }),
2183 /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
2184 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
2185 /* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { className: "text-base", children: __("By error", "yatra") }),
2186 /* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __("Grouped by error-message prefix.", "yatra") })
2187 ] }),
2188 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-2", children: [
2189 summary.by_error.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-gray-500", children: __("None.", "yatra") }),
2190 summary.by_error.map((row, idx) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
2191 "div",
2192 {
2193 className: "rounded border border-gray-200 dark:border-gray-700 px-3 py-2",
2194 children: [
2195 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-gray-700 dark:text-gray-300 break-words", children: row.fingerprint }),
2196 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-gray-500 mt-0.5", children: sprintf(__("%d buried", "yatra"), row.count) })
2197 ]
2198 },
2199 `${row.fingerprint}-${idx}`
2200 ))
2201 ] })
2202 ] })
2203 ] }),
2204 /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
2205 /* @__PURE__ */ jsxRuntimeExports.jsx(CardHeader, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
2206 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
2207 /* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { className: "text-base", children: __("Recent buried", "yatra") }),
2208 /* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: sprintf(
2209 /* translators: %d: number of recent rows shown */
2210 __("Last %d deliveries in permanent failure.", "yatra"),
2211 summary.recent.length
2212 ) })
2213 ] }),
2214 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
2215 selectedIds.size > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-gray-500", children: sprintf(
2216 /* translators: %d: number selected */
2217 __("%d selected", "yatra"),
2218 selectedIds.size
2219 ) }),
2220 /* @__PURE__ */ jsxRuntimeExports.jsxs(
2221 Button,
2222 {
2223 onClick: () => bulkReplay.mutate({ ids: Array.from(selectedIds) }),
2224 disabled: selectedIds.size === 0 || bulkReplay.isPending,
2225 children: [
2226 /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { className: "h-4 w-4 mr-1" }),
2227 __("Replay selected", "yatra")
2228 ]
2229 }
2230 )
2231 ] })
2232 ] }) }),
2233 /* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { className: "p-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "min-w-full text-sm", children: [
2234 /* @__PURE__ */ jsxRuntimeExports.jsx("thead", { className: "bg-gray-50 dark:bg-gray-800/40 border-b border-gray-200 dark:border-gray-700", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [
2235 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-left w-10", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2236 "input",
2237 {
2238 type: "checkbox",
2239 checked: selectedIds.size > 0 && selectedIds.size === summary.recent.length,
2240 onChange: (e) => {
2241 if (e.target.checked) {
2242 setSelectedIds(
2243 new Set(summary.recent.map((r) => r.id))
2244 );
2245 } else {
2246 setSelectedIds(/* @__PURE__ */ new Set());
2247 }
2248 }
2249 }
2250 ) }),
2251 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-left font-medium text-gray-700 dark:text-gray-300", children: __("Event", "yatra") }),
2252 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-left font-medium text-gray-700 dark:text-gray-300", children: __("Endpoint", "yatra") }),
2253 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-left font-medium text-gray-700 dark:text-gray-300", children: __("Attempts", "yatra") }),
2254 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-left font-medium text-gray-700 dark:text-gray-300", children: __("Error", "yatra") }),
2255 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-left font-medium text-gray-700 dark:text-gray-300", children: __("When", "yatra") }),
2256 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 w-32 text-right font-medium text-gray-700 dark:text-gray-300", children: __("Actions", "yatra") })
2257 ] }) }),
2258 /* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { className: "divide-y divide-gray-200 dark:divide-gray-700", children: summary.recent.map((row) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
2259 "tr",
2260 {
2261 className: "hover:bg-gray-50 dark:hover:bg-gray-800/40",
2262 children: [
2263 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2264 "input",
2265 {
2266 type: "checkbox",
2267 checked: selectedIds.has(row.id),
2268 onChange: (e) => {
2269 const next = new Set(selectedIds);
2270 if (e.target.checked) next.add(row.id);
2271 else next.delete(row.id);
2272 setSelectedIds(next);
2273 }
2274 }
2275 ) }),
2276 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 font-mono text-xs text-gray-900 dark:text-white", children: row.event_key }),
2277 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-gray-700 dark:text-gray-300", children: endpointNameById.get(row.endpoint_id) ?? `#${row.endpoint_id}` }),
2278 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-gray-700 dark:text-gray-300", children: row.attempts }),
2279 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-xs text-gray-600 dark:text-gray-400 max-w-md truncate", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip, { content: row.error_message ?? "", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: row.error_message ?? __("—", "yatra") }) }) }),
2280 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-xs text-gray-500 whitespace-nowrap", children: row.created_at }),
2281 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-right", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "inline-flex gap-1", children: [
2282 /* @__PURE__ */ jsxRuntimeExports.jsx(
2283 Button,
2284 {
2285 size: "sm",
2286 variant: "outline",
2287 onClick: () => setInspectId(row.id),
2288 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "h-3.5 w-3.5" })
2289 }
2290 ),
2291 /* @__PURE__ */ jsxRuntimeExports.jsx(
2292 Button,
2293 {
2294 size: "sm",
2295 variant: "outline",
2296 onClick: () => bulkReplay.mutate({ ids: [row.id] }),
2297 disabled: bulkReplay.isPending,
2298 children: /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { className: "h-3.5 w-3.5" })
2299 }
2300 )
2301 ] }) })
2302 ]
2303 },
2304 row.id
2305 )) })
2306 ] }) }) })
2307 ] }),
2308 /* @__PURE__ */ jsxRuntimeExports.jsx(
2309 DeliveryInspectDialog,
2310 {
2311 id: inspectId,
2312 onClose: () => setInspectId(null)
2313 }
2314 )
2315 ] });
2316 };
2317 const StatusBadge = ({
2318 status,
2319 httpStatus
2320 }) => {
2321 const cfg = {
2322 queued: {
2323 cls: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
2324 label: __("Queued", "yatra")
2325 },
2326 delivering: {
2327 cls: "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400",
2328 label: __("Delivering", "yatra")
2329 },
2330 delivered: {
2331 cls: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400",
2332 label: __("Delivered", "yatra")
2333 },
2334 failed: {
2335 cls: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400",
2336 label: __("Failed", "yatra")
2337 },
2338 permanent_failure: {
2339 cls: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400",
2340 label: __("Failed (final)", "yatra")
2341 }
2342 };
2343 const x = cfg[status] ?? {
2344 cls: "bg-gray-100 text-gray-700",
2345 label: status
2346 };
2347 return /* @__PURE__ */ jsxRuntimeExports.jsxs(Badge, { className: x.cls, children: [
2348 x.label,
2349 httpStatus !== null ? ` (${httpStatus})` : ""
2350 ] });
2351 };
2352 const DeliveryInspectDialog = ({ id, onClose }) => {
2353 const queryClient = useQueryClient();
2354 const { showToast } = useToast();
2355 const { data, isLoading } = useQuery({
2356 queryKey: ["webhooks-delivery", id],
2357 queryFn: () => webhooksApi.getDelivery(id),
2358 enabled: id !== null,
2359 // Auto-refetch while the row hasn't reached a terminal status.
2360 // Ensures the inspector reflects "queued → delivering → delivered"
2361 // live, which matters most right after a Ping click.
2362 refetchInterval: (q) => {
2363 var _a, _b;
2364 const status = (_b = (_a = q.state.data) == null ? void 0 : _a.data) == null ? void 0 : _b.status;
2365 const terminal = status === "delivered" || status === "permanent_failure";
2366 return terminal ? false : 2e3;
2367 }
2368 });
2369 const replay = useMutation({
2370 mutationFn: (deliveryId) => webhooksApi.replayDelivery(deliveryId),
2371 onSuccess: () => {
2372 queryClient.invalidateQueries({ queryKey: ["webhooks-deliveries"] });
2373 queryClient.invalidateQueries({ queryKey: ["webhooks-delivery", id] });
2374 showToast(__("Delivery re-queued.", "yatra"), "success");
2375 },
2376 onError: (e) => showToast(extractError(e), "error")
2377 });
2378 if (id === null) return null;
2379 const delivery = data == null ? void 0 : data.data;
2380 const payloadJson = delivery ? JSON.stringify(delivery.payload, null, 2) : "";
2381 return /* @__PURE__ */ jsxRuntimeExports.jsx(
2382 Modal,
2383 {
2384 isOpen: true,
2385 onClose,
2386 title: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
2387 /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "w-5 h-5 text-blue-500" }),
2388 __("Delivery inspector", "yatra")
2389 ] }),
2390 size: "xl",
2391 hideFooter: false,
2392 footer: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2", children: [
2393 /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "outline", onClick: onClose, children: __("Close", "yatra") }),
2394 delivery && (delivery.status === "failed" || delivery.status === "permanent_failure") && /* @__PURE__ */ jsxRuntimeExports.jsx(
2395 Button,
2396 {
2397 onClick: () => replay.mutate(delivery.id),
2398 disabled: replay.isPending,
2399 children: replay.isPending ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
2400 /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }),
2401 __("Replaying…", "yatra")
2402 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
2403 /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { className: "mr-1.5 h-4 w-4" }),
2404 __("Replay delivery", "yatra")
2405 ] })
2406 }
2407 )
2408 ] }),
2409 children: isLoading || !delivery ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-center py-12", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-8 w-8 animate-spin text-gray-400" }) }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
2410 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-3 text-sm", children: [
2411 /* @__PURE__ */ jsxRuntimeExports.jsx(
2412 Field,
2413 {
2414 label: __("Event", "yatra"),
2415 value: delivery.event_key,
2416 mono: true
2417 }
2418 ),
2419 /* @__PURE__ */ jsxRuntimeExports.jsx(
2420 Field,
2421 {
2422 label: __("Delivery ID", "yatra"),
2423 value: delivery.delivery_id,
2424 mono: true
2425 }
2426 ),
2427 /* @__PURE__ */ jsxRuntimeExports.jsx(
2428 Field,
2429 {
2430 label: __("Status", "yatra"),
2431 raw: /* @__PURE__ */ jsxRuntimeExports.jsx(
2432 StatusBadge,
2433 {
2434 status: delivery.status,
2435 httpStatus: delivery.http_status
2436 }
2437 )
2438 }
2439 ),
2440 /* @__PURE__ */ jsxRuntimeExports.jsx(
2441 Field,
2442 {
2443 label: __("Attempts", "yatra"),
2444 value: String(delivery.attempts)
2445 }
2446 ),
2447 /* @__PURE__ */ jsxRuntimeExports.jsx(
2448 Field,
2449 {
2450 label: __("Created", "yatra"),
2451 value: new Date(delivery.created_at).toLocaleString()
2452 }
2453 ),
2454 /* @__PURE__ */ jsxRuntimeExports.jsx(
2455 Field,
2456 {
2457 label: __("Delivered", "yatra"),
2458 value: delivery.delivered_at ? new Date(delivery.delivered_at).toLocaleString() : "—"
2459 }
2460 ),
2461 delivery.duration_ms !== null && /* @__PURE__ */ jsxRuntimeExports.jsx(
2462 Field,
2463 {
2464 label: __("Duration", "yatra"),
2465 value: `${delivery.duration_ms} ms`
2466 }
2467 ),
2468 delivery.next_attempt_at && /* @__PURE__ */ jsxRuntimeExports.jsx(
2469 Field,
2470 {
2471 label: __("Next attempt", "yatra"),
2472 value: new Date(delivery.next_attempt_at).toLocaleString()
2473 }
2474 )
2475 ] }),
2476 delivery.error_message && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700 rounded-md p-3 text-sm text-red-700 dark:text-red-400", children: [
2477 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { className: "block mb-1", children: __("Error", "yatra") }),
2478 delivery.error_message
2479 ] }),
2480 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
2481 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-1", children: [
2482 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { className: "text-sm font-semibold", children: __("Payload sent", "yatra") }),
2483 /* @__PURE__ */ jsxRuntimeExports.jsx(CopyButton, { text: payloadJson })
2484 ] }),
2485 /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "bg-gray-900 text-gray-100 dark:bg-black rounded-md p-4 text-xs font-mono overflow-auto max-h-96", children: payloadJson })
2486 ] }),
2487 delivery.response_body && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
2488 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { className: "text-sm font-semibold mb-1 block", children: __("Response body", "yatra") }),
2489 /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md p-4 text-xs font-mono overflow-auto max-h-48", children: delivery.response_body })
2490 ] }),
2491 delivery.response_headers && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
2492 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { className: "text-sm font-semibold mb-1 block", children: __("Response headers", "yatra") }),
2493 /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md p-3 text-xs font-mono overflow-auto max-h-32", children: delivery.response_headers })
2494 ] })
2495 ] })
2496 }
2497 );
2498 };
2499 const Field = ({ label, value, mono, raw }) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
2500 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-gray-500 dark:text-gray-400", children: label }),
2501 /* @__PURE__ */ jsxRuntimeExports.jsx(
2502 "div",
2503 {
2504 className: `text-sm text-gray-900 dark:text-white ${mono ? "font-mono" : ""}`,
2505 children: raw ?? value ?? "—"
2506 }
2507 )
2508 ] });
2509 const CopyButton = ({ text }) => {
2510 const [copied, setCopied] = reactExports.useState(false);
2511 return /* @__PURE__ */ jsxRuntimeExports.jsx(
2512 Button,
2513 {
2514 size: "sm",
2515 variant: "outline",
2516 onClick: async () => {
2517 try {
2518 await navigator.clipboard.writeText(text);
2519 setCopied(true);
2520 window.setTimeout(() => setCopied(false), 1800);
2521 } catch (_e) {
2522 }
2523 },
2524 children: copied ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
2525 /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "mr-1 h-3 w-3" }),
2526 __("Copied", "yatra")
2527 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
2528 /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { className: "mr-1 h-3 w-3" }),
2529 __("Copy", "yatra")
2530 ] })
2531 }
2532 );
2533 };
2534 function extractError(e) {
2535 var _a;
2536 if (!e) return "Request failed.";
2537 const data = ((_a = e == null ? void 0 : e.response) == null ? void 0 : _a.data) ?? (e == null ? void 0 : e.data) ?? null;
2538 if (data && typeof data === "object" && typeof data.message === "string") {
2539 return data.message;
2540 }
2541 return (e == null ? void 0 : e.message) || "Request failed.";
2542 }
2543 export {
2544 Webhooks as default
2545 };
2546 //# sourceMappingURL=Webhooks-Cb89OgkB.js.map
2547