@kyro-cms/admin 0.11.5 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,6 +24,24 @@ import { Badge } from "./ui/Badge";
24
24
  import { PageHeader } from "./ui/PageHeader";
25
25
 
26
26
 
27
+ interface WebhookConfigField {
28
+ name: string;
29
+ label: string;
30
+ required: boolean;
31
+ placeholder: string;
32
+ }
33
+
34
+ interface WebhookActionDef {
35
+ label: string;
36
+ description: string;
37
+ configFields: WebhookConfigField[];
38
+ envVars: string[];
39
+ }
40
+
41
+ interface WebhookActionsResponse {
42
+ actions: Record<string, WebhookActionDef>;
43
+ }
44
+
27
45
  interface WebhookItem {
28
46
  id: string;
29
47
  name: string;
@@ -31,10 +49,19 @@ interface WebhookItem {
31
49
  events: string[];
32
50
  secret?: string;
33
51
  status: "active" | "paused";
52
+ action?: string;
53
+ config?: Record<string, string>;
34
54
  lastTriggered?: string;
35
55
  createdAt: string;
36
56
  }
37
57
 
58
+ const ACTION_ICONS: Record<string, string> = {
59
+ generic: "🔗",
60
+ "github-dispatch": "⚙️",
61
+ "dokploy-deploy": "🚀",
62
+ "coolify-deploy": "🎯",
63
+ };
64
+
38
65
  export function WebhookManager() {
39
66
  const {
40
67
  items: webhooks,
@@ -56,24 +83,91 @@ export function WebhookManager() {
56
83
  message: string;
57
84
  } | null>(null);
58
85
  const [testId, setTestId] = useState<string | null>(null);
86
+ const [actions, setActions] = useState<Record<string, WebhookActionDef>>({});
87
+ const [loadingActions, setLoadingActions] = useState(true);
88
+ const [step, setStep] = useState<"action" | "config">("action");
89
+ const [selectedAction, setSelectedAction] = useState<string>("generic");
59
90
  const [formData, setFormData] = useState({
60
91
  name: "",
61
92
  url: "",
62
93
  events: [] as string[],
63
94
  secret: "",
95
+ action: "generic" as string,
96
+ config: {} as Record<string, string>,
64
97
  });
65
98
  const [createError, setCreateError] = useState("");
66
99
 
100
+ useEffect(() => {
101
+ loadActions();
102
+ }, []);
103
+
104
+ const loadActions = async () => {
105
+ try {
106
+ const data = await apiGet<WebhookActionsResponse>("/api/webhooks/actions");
107
+ setActions(data.actions);
108
+ } catch (e) {
109
+ console.error("Failed to load webhook actions:", e);
110
+ } finally {
111
+ setLoadingActions(false);
112
+ }
113
+ };
114
+
115
+ const resetCreateModal = () => {
116
+ setStep("action");
117
+ setSelectedAction("generic");
118
+ setFormData({
119
+ name: "",
120
+ url: "",
121
+ events: ["collection.create", "collection.update", "collection.delete"],
122
+ secret: "",
123
+ action: "generic",
124
+ config: {},
125
+ });
126
+ setCreateError("");
127
+ };
128
+
129
+ const handleActionSelect = (actionKey: string) => {
130
+ setSelectedAction(actionKey);
131
+ setFormData((prev) => ({
132
+ ...prev,
133
+ action: actionKey,
134
+ config: {},
135
+ }));
136
+ setStep("config");
137
+ };
138
+
139
+ const handleConfigChange = (fieldName: string, value: string) => {
140
+ setFormData((prev) => ({
141
+ ...prev,
142
+ config: { ...prev.config, [fieldName]: value },
143
+ }));
144
+ };
145
+
67
146
  const handleCreate = async () => {
68
- if (!formData.name.trim() || !formData.url.trim()) {
69
- setCreateError("Name and URL are required");
147
+ if (!formData.name.trim()) {
148
+ setCreateError("Name is required");
149
+ return;
150
+ }
151
+
152
+ if (selectedAction === "generic" && !formData.url.trim()) {
153
+ setCreateError("URL is required for custom webhooks");
70
154
  return;
71
155
  }
72
156
 
157
+ const actionDef = actions[selectedAction];
158
+ if (actionDef) {
159
+ for (const field of actionDef.configFields) {
160
+ if (field.required && !formData.config[field.name]?.trim()) {
161
+ setCreateError(`${field.label} is required`);
162
+ return;
163
+ }
164
+ }
165
+ }
166
+
73
167
  try {
74
168
  await create(formData);
75
- setFormData({ name: "", url: "", events: [], secret: "" });
76
- toast.success(`Webhook established: ${formData.name}`);
169
+ resetCreateModal();
170
+ toast.success(`Webhook created: ${formData.name}`);
77
171
  } catch (e) {
78
172
  setCreateError("Failed to create webhook");
79
173
  toast.error("Failed to create webhook");
@@ -91,7 +185,7 @@ export function WebhookManager() {
91
185
  success: true,
92
186
  message: data.message || "Webhook triggered successfully",
93
187
  });
94
- toast.success("Dispatch verified: Remote endpoint responded");
188
+ toast.success("Test delivered successfully");
95
189
  } catch (e) {
96
190
  setTestResult({ success: false, message: "Failed to trigger webhook" });
97
191
  }
@@ -103,7 +197,7 @@ export function WebhookManager() {
103
197
  await update(id, {
104
198
  status: newStatus,
105
199
  });
106
- toast.success(newStatus === "active" ? "Signals resumed" : "Dispatcher paused");
200
+ toast.success(newStatus === "active" ? "Webhook activated" : "Webhook paused");
107
201
  } catch (e) {
108
202
  console.error(e);
109
203
  toast.error("Failed to toggle webhook status");
@@ -129,23 +223,22 @@ export function WebhookManager() {
129
223
  { label: "Auth", value: "auth", description: "User login/logout events" },
130
224
  ];
131
225
 
226
+ const getActionLabel = (action?: string) => {
227
+ const actionDef = actions[action || "generic"];
228
+ return actionDef?.label || "Custom URL";
229
+ };
230
+
132
231
  return (
133
232
  <div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-32">
134
233
  <PageHeader
135
234
  title="Webhooks"
136
- description="Receive real-time notifications when your content changes."
235
+ description="Get notified when your content changes."
137
236
  icon={Webhook}
138
237
  actions={[
139
238
  {
140
- label: "Create Webhook",
239
+ label: "New webhook",
141
240
  onClick: () => {
142
- setFormData({
143
- name: "",
144
- url: "",
145
- events: ["collection.create", "collection.update", "collection.delete"],
146
- secret: "",
147
- });
148
- setCreateError("");
241
+ resetCreateModal();
149
242
  setShowCreateModal(true);
150
243
  },
151
244
  icon: Plus,
@@ -159,32 +252,18 @@ export function WebhookManager() {
159
252
  ]}
160
253
  />
161
254
 
162
- {/* Info Banner - Modernized */}
163
- <div className="relative overflow-hidden rounded-[2rem] border border-[var(--kyro-border)] bg-gradient-to-br from-[var(--kyro-surface)] to-[var(--kyro-surface-accent)] p-8">
164
- <div className="absolute top-0 right-0 p-8 opacity-[0.03] pointer-events-none">
165
- <Zap className="w-64 h-64 rotate-12" />
166
- </div>
167
- <div className="flex flex-col md:flex-row items-center gap-8 relative z-10">
168
- <div className="p-5 bg-gradient-to-br from-[var(--kyro-primary)] to-[var(--kyro-primary)]/50 rounded-[1.5rem] shadow-xl shadow-[var(--kyro-primary)]/20">
169
- <Zap className="w-8 h-8 text-white" />
255
+ {/* Info Banner */}
256
+ <div className="relative overflow-hidden rounded-2xl border border-[var(--kyro-border)] bg-[var(--kyro-surface)] p-6">
257
+ <div className="flex flex-col md:flex-row items-center gap-6 relative z-10">
258
+ <div className="p-3 bg-[var(--kyro-primary)]/10 rounded-xl">
259
+ <Zap className="w-6 h-6 text-[var(--kyro-primary)]" />
170
260
  </div>
171
261
  <div className="flex-1 text-center md:text-left">
172
- <h3 className="text-xl font-bold mb-2">Real-time Event Synchronization</h3>
173
262
  <p className="text-sm text-[var(--kyro-text-secondary)] opacity-70 max-w-2xl leading-relaxed">
174
- Webhooks allow your application to receive instant HTTP notifications when events happen in your CMS.
175
- Eliminate polling and build responsive, event-driven architectures with ease.
263
+ Receive instant HTTP notifications when events happen in your CMS.
264
+ Choose a platform preset or point to any custom endpoint.
176
265
  </p>
177
266
  </div>
178
- <div className="flex gap-4">
179
- <div className="flex flex-col items-center px-4 py-2 bg-[var(--kyro-surface)]/50 border border-[var(--kyro-border)] rounded-2xl">
180
- <span className="text-[10px] font-bold opacity-40 uppercase tracking-tighter">Collections</span>
181
- <span className="text-sm font-bold text-[var(--kyro-primary)]">Triggered</span>
182
- </div>
183
- <div className="flex flex-col items-center px-4 py-2 bg-[var(--kyro-surface)]/50 border border-[var(--kyro-border)] rounded-2xl">
184
- <span className="text-[10px] font-bold opacity-40 uppercase tracking-tighter">Latency</span>
185
- <span className="text-sm font-bold text-[var(--kyro-primary)]">&lt;200ms</span>
186
- </div>
187
- </div>
188
267
  </div>
189
268
  </div>
190
269
 
@@ -193,33 +272,36 @@ export function WebhookManager() {
193
272
  <div className="flex items-center justify-between px-2">
194
273
  <div className="flex items-center gap-2">
195
274
  <div className="w-1 h-4 bg-[var(--kyro-primary)] rounded-full" />
196
- <h2 className="text-sm font-medium tracking-[0.2em] opacity-40">ACTIVE ENDPOINTS</h2>
275
+ <h2 className="text-sm font-medium text-[var(--kyro-text-secondary)]">Endpoints</h2>
197
276
  </div>
198
- <div className="text-[10px] font-bold opacity-40 uppercase">
199
- {webhooks.length} Hook{webhooks.length !== 1 && "s"}
277
+ <div className="text-xs text-[var(--kyro-text-secondary)] opacity-50">
278
+ {webhooks.length} hook{webhooks.length !== 1 && "s"}
200
279
  </div>
201
280
  </div>
202
281
 
203
282
  {loading ? (
204
- <div className="flex items-center justify-center p-20 surface-tile rounded-3xl opacity-50 italic">
205
- Connecting to dispatcher...
283
+ <div className="flex items-center justify-center p-20 opacity-50 text-sm text-[var(--kyro-text-secondary)]">
284
+ Loading webhooks...
206
285
  </div>
207
286
  ) : webhooks.length === 0 ? (
208
- <div className="p-16 text-center rounded-[3rem] border-2 border-dashed border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)]/30">
209
- <div className="w-20 h-20 mx-auto mb-6 bg-gradient-to-tr from-[var(--kyro-surface)] to-[var(--kyro-surface-accent)] rounded-3xl flex items-center justify-center shadow-xl border border-[var(--kyro-border)]">
210
- <Webhook className="w-10 h-10 text-[var(--kyro-primary)]" />
287
+ <div className="p-16 text-center rounded-2xl border border-dashed border-[var(--kyro-border)] bg-[var(--kyro-surface)]/50">
288
+ <div className="w-16 h-16 mx-auto mb-5 bg-[var(--kyro-primary)]/10 rounded-2xl flex items-center justify-center">
289
+ <Webhook className="w-8 h-8 text-[var(--kyro-primary)]" />
211
290
  </div>
212
- <h3 className="text-2xl font-bold mb-3">No Webhooks Found</h3>
213
- <p className="text-sm text-[var(--kyro-text-secondary)] opacity-60 mb-8 max-w-sm mx-auto">
214
- Your CMS is currently silent. Create a webhook to start broadcasting events to your external services.
291
+ <h3 className="text-lg font-bold mb-2">No webhooks yet</h3>
292
+ <p className="text-sm text-[var(--kyro-text-secondary)] opacity-60 mb-6 max-w-sm mx-auto">
293
+ Create a webhook to get notified when your content changes.
215
294
  </p>
216
295
  <button
217
296
  type="button"
218
- onClick={() => setShowCreateModal(true)}
219
- className="kyro-btn kyro-btn-primary inline-flex items-center gap-3 px-8 py-4 rounded-2xl font-bold hover:scale-[1.05] transition-all shadow-xl shadow-[var(--kyro-primary)]/10"
297
+ onClick={() => {
298
+ resetCreateModal();
299
+ setShowCreateModal(true);
300
+ }}
301
+ className="kyro-btn kyro-btn-primary inline-flex items-center gap-2 px-6 py-3 rounded-xl text-sm font-medium"
220
302
  >
221
- <Plus className="w-5 h-5" />
222
- Configure Webhook
303
+ <Plus className="w-4 h-4" />
304
+ Create webhook
223
305
  </button>
224
306
  </div>
225
307
  ) : (
@@ -227,86 +309,87 @@ export function WebhookManager() {
227
309
  {webhooks.map((webhook) => (
228
310
  <div
229
311
  key={webhook.id}
230
- className="group relative overflow-hidden bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-3xl p-6 hover:border-[var(--kyro-primary)]/50 transition-all duration-300"
312
+ className="group relative overflow-hidden bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-2xl p-5 hover:border-[var(--kyro-primary)]/30 transition-all duration-200"
231
313
  >
232
- <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6 relative z-10">
314
+ <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-5 relative z-10">
233
315
  <div className="flex-1">
234
- <div className="flex items-center gap-3 mb-4">
235
- <div className="p-2.5 bg-[var(--kyro-surface-accent)] rounded-xl group-hover:bg-[var(--kyro-primary)]/10 transition-colors">
236
- <Webhook className="w-5 h-5 text-[var(--kyro-text-secondary)] group-hover:text-[var(--kyro-primary)] transition-colors" />
316
+ <div className="flex items-center gap-3 mb-3">
317
+ <div className="p-2 bg-[var(--kyro-surface-accent)] rounded-lg">
318
+ <Webhook className="w-4 h-4 text-[var(--kyro-text-secondary)]" />
237
319
  </div>
238
320
  <div>
239
- <h3 className="text-lg font-bold group-hover:text-[var(--kyro-primary)] transition-colors">{webhook.name}</h3>
321
+ <h3 className="text-sm font-semibold">{webhook.name}</h3>
240
322
  <div className="flex items-center gap-2 mt-0.5">
241
- <span className={`w-2 h-2 rounded-full ${webhook.status === "active" ? "bg-green-500 animate-pulse" : "bg-amber-500"}`} />
242
- <span className={`text-[10px] font-bold uppercase tracking-wider ${webhook.status === "active" ? "text-green-500" : "text-amber-500"}`}>
323
+ <span className={`w-1.5 h-1.5 rounded-full ${webhook.status === "active" ? "bg-green-500" : "bg-amber-500"}`} />
324
+ <span className={`text-[10px] font-medium ${webhook.status === "active" ? "text-green-500" : "text-amber-500"}`}>
243
325
  {webhook.status}
244
326
  </span>
327
+ <Badge variant="outline" className="text-[9px] px-1.5 opacity-50">
328
+ {getActionLabel(webhook.action)}
329
+ </Badge>
245
330
  </div>
246
331
  </div>
247
332
  </div>
248
333
 
249
- <div className="grid grid-cols-1 sm:grid-cols-3 gap-6 pt-2">
334
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-4 pt-1">
250
335
  <div className="space-y-1">
251
- <span className="text-[9px] font-bold uppercase tracking-widest opacity-30">Destination</span>
252
- <div className="font-mono text-xs opacity-60 truncate max-w-[200px]" title={webhook.url}>
253
- {webhook.url}
336
+ <span className="text-[10px] text-[var(--kyro-text-secondary)] opacity-50">Destination</span>
337
+ <div className="font-mono text-xs text-[var(--kyro-text-secondary)] opacity-60 truncate max-w-[200px]" title={webhook.url}>
338
+ {webhook.url || `${getActionLabel(webhook.action)} webhook`}
254
339
  </div>
255
340
  </div>
256
- <div className="space-y-1 sm:border-l border-t sm:border-t-0 border-[var(--kyro-border)] pt-4 sm:pt-0 sm:pl-6 mt-4 sm:mt-0">
257
- <span className="text-[9px] font-bold uppercase tracking-widest opacity-30">Events</span>
341
+ <div className="space-y-1 sm:border-l border-t sm:border-t-0 border-[var(--kyro-border)] pt-3 sm:pt-0 sm:pl-4">
342
+ <span className="text-[10px] text-[var(--kyro-text-secondary)] opacity-50">Events</span>
258
343
  <div className="flex flex-wrap gap-1">
259
- {webhook.events.slice(0, 2).map((event) => (
260
- <Badge key={event} variant="outline" className="text-[8px] font-bold px-1.5 opacity-60">
344
+ {webhook.events.slice(0, 3).map((event) => (
345
+ <Badge key={event} variant="outline" className="text-[9px] px-1.5 opacity-50">
261
346
  {event}
262
347
  </Badge>
263
348
  ))}
264
- {webhook.events.length > 2 && (
265
- <span className="text-[8px] font-bold opacity-30">+{webhook.events.length - 2} more</span>
349
+ {webhook.events.length > 3 && (
350
+ <span className="text-[9px] text-[var(--kyro-text-secondary)] opacity-30">+{webhook.events.length - 3}</span>
266
351
  )}
267
352
  </div>
268
353
  </div>
269
- <div className="space-y-1 sm:border-l border-t sm:border-t-0 border-[var(--kyro-border)] pt-4 sm:pt-0 sm:pl-6 mt-4 sm:mt-0">
270
- <span className="text-[9px] font-bold uppercase tracking-widest opacity-30">Activity</span>
271
- <div className="text-[10px] font-bold opacity-60 flex items-center gap-1.5">
354
+ <div className="space-y-1 sm:border-l border-t sm:border-t-0 border-[var(--kyro-border)] pt-3 sm:pt-0 sm:pl-4">
355
+ <span className="text-[10px] text-[var(--kyro-text-secondary)] opacity-50">Activity</span>
356
+ <div className="text-[10px] text-[var(--kyro-text-secondary)] opacity-60 flex items-center gap-1.5">
272
357
  <Clock className="w-3 h-3" />
273
358
  {webhook.lastTriggered
274
- ? `Last triggered: ${new Date(webhook.lastTriggered).toLocaleDateString()}`
359
+ ? `Last triggered ${new Date(webhook.lastTriggered).toLocaleDateString()}`
275
360
  : "Never triggered"}
276
361
  </div>
277
362
  </div>
278
363
  </div>
279
364
  </div>
280
365
 
281
- <div className="flex items-center gap-2 lg:bg-[var(--kyro-surface-accent)]/50 lg:p-2 lg:rounded-2xl lg:opacity-0 group-hover:opacity-100 transition-all duration-300 lg:translate-x-4 group-hover:translate-x-0">
366
+ <div className="flex items-center gap-1.5 lg:opacity-0 group-hover:opacity-100 transition-opacity duration-200">
282
367
  <button
283
368
  type="button"
284
369
  onClick={() => handleTest(webhook.id)}
285
- className="flex-1 lg:flex-none p-3 bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-xl hover:border-[var(--kyro-primary)] transition-all flex items-center justify-center gap-2"
370
+ className="p-2 rounded-lg border border-[var(--kyro-border)] hover:border-[var(--kyro-primary)] transition-colors flex items-center gap-1.5"
286
371
  title="Send test request"
287
372
  >
288
- <Send className="w-4 h-4" />
289
- <span className="text-xs font-bold">Test</span>
373
+ <Send className="w-3.5 h-3.5" />
374
+ <span className="text-[11px] font-medium">Test</span>
290
375
  </button>
291
376
 
292
- <div className="w-px h-6 bg-[var(--kyro-border)] mx-1 hidden lg:block" />
293
-
294
377
  <button
295
378
  type="button"
296
379
  onClick={() => toggleStatus(webhook.id, webhook.status)}
297
- className={`p-3 bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-xl hover:border-[var(--kyro-primary)] transition-all group/toggle ${webhook.status === "active" ? "text-amber-500/50 hover:text-amber-500" : "text-green-500/50 hover:text-green-500"}`}
298
- title={webhook.status === "active" ? "Pause webhook" : "Activate webhook"}
380
+ className={`p-2 rounded-lg border border-[var(--kyro-border)] hover:border-[var(--kyro-primary)] transition-colors ${webhook.status === "active" ? "text-amber-500/60 hover:text-amber-500" : "text-green-500/60 hover:text-green-500"}`}
381
+ title={webhook.status === "active" ? "Pause" : "Activate"}
299
382
  >
300
- {webhook.status === "active" ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
383
+ {webhook.status === "active" ? <Pause className="w-3.5 h-3.5" /> : <Play className="w-3.5 h-3.5" />}
301
384
  </button>
302
385
 
303
386
  <button
304
387
  type="button"
305
388
  onClick={() => remove(webhook.id, "Webhook")}
306
- className="p-3 bg-red-500/5 border border-red-500/10 rounded-xl hover:bg-red-500/10 hover:border-red-500/30 transition-all group/delete"
307
- title="Delete webhook"
389
+ className="p-2 rounded-lg border border-[var(--kyro-border)] hover:border-red-500/30 hover:bg-red-500/5 transition-colors"
390
+ title="Delete"
308
391
  >
309
- <Trash2 className="w-4 h-4 text-red-500/50 group-hover/delete:text-red-500" />
392
+ <Trash2 className="w-3.5 h-3.5 text-red-500/50 hover:text-red-500" />
310
393
  </button>
311
394
  </div>
312
395
  </div>
@@ -316,58 +399,136 @@ export function WebhookManager() {
316
399
  )}
317
400
  </section>
318
401
 
319
- {/* Create Modal */}
402
+ {/* Create Modal - Step 1: Select Action */}
320
403
  <Modal
321
- open={showCreateModal}
404
+ open={showCreateModal && step === "action"}
322
405
  onClose={() => setShowCreateModal(false)}
323
- title="Register Webhook"
324
- size="lg"
406
+ title="New webhook"
325
407
  >
326
408
  <ModalContent>
327
- <div className="space-y-6">
328
- <div className="flex flex-col gap-6">
409
+ <div className="space-y-4">
410
+ {loadingActions ? (
411
+ <div className="flex items-center justify-center p-12 opacity-50">
412
+ <RefreshCw className="w-5 h-5 animate-spin text-[var(--kyro-primary)]" />
413
+ </div>
414
+ ) : (
415
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
416
+ {Object.entries(actions).map(([key, action]) => (
417
+ <button
418
+ type="button"
419
+ key={key}
420
+ onClick={() => handleActionSelect(key)}
421
+ className="flex flex-col items-start p-4 rounded-xl border border-[var(--kyro-border)] bg-[var(--kyro-surface)] hover:border-[var(--kyro-primary)]/40 transition-all text-left group"
422
+ >
423
+ <div className="text-lg mb-2">{ACTION_ICONS[key] || "🔗"}</div>
424
+ <h3 className="text-sm font-medium mb-0.5 group-hover:text-[var(--kyro-primary)] transition-colors">
425
+ {action.label}
426
+ </h3>
427
+ <p className="text-[11px] text-[var(--kyro-text-secondary)] opacity-50 leading-relaxed">
428
+ {action.description}
429
+ </p>
430
+ {action.envVars.length > 0 && (
431
+ <div className="mt-2 flex flex-wrap gap-1">
432
+ {action.envVars.map((envVar) => (
433
+ <span key={envVar} className="text-[9px] font-mono bg-[var(--kyro-surface-accent)] px-1.5 py-0.5 rounded opacity-40">
434
+ {envVar}
435
+ </span>
436
+ ))}
437
+ </div>
438
+ )}
439
+ </button>
440
+ ))}
441
+ </div>
442
+ )}
443
+ </div>
444
+ </ModalContent>
445
+ </Modal>
446
+
447
+ {/* Create Modal - Step 2: Configure */}
448
+ <Modal
449
+ open={showCreateModal && step === "config"}
450
+ onClose={() => setShowCreateModal(false)}
451
+ title={actions[selectedAction]?.label || "Webhook"}
452
+ >
453
+ <ModalContent>
454
+ <div className="space-y-5">
455
+ <div className="flex flex-col gap-5">
329
456
  <div className="space-y-4">
330
457
  <div>
331
- <label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)] uppercase tracking-wider">Webhook Name</label>
458
+ <label className="block text-xs font-medium mb-1.5 text-[var(--kyro-text-secondary)]">Name</label>
332
459
  <input
333
460
  type="text"
334
461
  value={formData.name}
335
- onChange={(e) =>
336
- setFormData({ ...formData, name: e.target.value })
337
- }
338
- placeholder="e.g., Slack Notifications"
339
- className="w-full px-4 py-3 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-xl text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
340
- />
341
- </div>
342
- <div>
343
- <label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)] uppercase tracking-wider">Payload URL</label>
344
- <input
345
- type="url"
346
- value={formData.url}
347
- onChange={(e) =>
348
- setFormData({ ...formData, url: e.target.value })
349
- }
350
- placeholder="https://your-server.com/webhook"
351
- className="w-full px-4 py-3 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-xl text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
352
- />
353
- </div>
354
- <div>
355
- <label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)] uppercase tracking-wider">Signing Secret</label>
356
- <input
357
- type="text"
358
- value={formData.secret}
359
- onChange={(e) =>
360
- setFormData({ ...formData, secret: e.target.value })
361
- }
362
- placeholder="Optional secret for verification"
363
- className="w-full px-4 py-3 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-xl text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
462
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
463
+ placeholder="e.g., Deploy on content update"
464
+ className="w-full px-3 py-2.5 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-lg text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
364
465
  />
365
466
  </div>
467
+
468
+ {selectedAction === "generic" && (
469
+ <div>
470
+ <label className="block text-xs font-medium mb-1.5 text-[var(--kyro-text-secondary)]">URL</label>
471
+ <input
472
+ type="url"
473
+ value={formData.url}
474
+ onChange={(e) => setFormData({ ...formData, url: e.target.value })}
475
+ placeholder="https://your-server.com/webhook"
476
+ className="w-full px-3 py-2.5 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-lg text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
477
+ />
478
+ </div>
479
+ )}
480
+
481
+ {selectedAction !== "generic" && actions[selectedAction]?.configFields.map((field) => (
482
+ <div key={field.name}>
483
+ <label className="block text-xs font-medium mb-1.5 text-[var(--kyro-text-secondary)]">
484
+ {field.label} {field.required && <span className="text-red-500">*</span>}
485
+ </label>
486
+ <input
487
+ type="text"
488
+ value={formData.config[field.name] || ""}
489
+ onChange={(e) => handleConfigChange(field.name, e.target.value)}
490
+ placeholder={field.placeholder}
491
+ className="w-full px-3 py-2.5 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-lg text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
492
+ />
493
+ </div>
494
+ ))}
495
+
496
+ {selectedAction === "generic" && (
497
+ <div>
498
+ <label className="block text-xs font-medium mb-1.5 text-[var(--kyro-text-secondary)]">Signing secret</label>
499
+ <input
500
+ type="text"
501
+ value={formData.secret}
502
+ onChange={(e) => setFormData({ ...formData, secret: e.target.value })}
503
+ placeholder="Optional"
504
+ className="w-full px-3 py-2.5 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-lg text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
505
+ />
506
+ </div>
507
+ )}
366
508
  </div>
367
509
 
368
- <div className="space-y-4">
369
- <label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)] uppercase tracking-wider">Subscribed Events</label>
370
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
510
+ {selectedAction !== "generic" && actions[selectedAction]?.envVars.length > 0 && (
511
+ <div className="p-3 rounded-lg bg-amber-500/5 border border-amber-500/15">
512
+ <div className="flex items-center gap-1.5 mb-1.5">
513
+ <AlertTriangle className="w-3.5 h-3.5 text-amber-500" />
514
+ <span className="text-[11px] font-medium text-amber-600">Environment variables required</span>
515
+ </div>
516
+ <p className="text-[11px] text-amber-600/60 mb-2">
517
+ Add these to your <code className="font-mono bg-amber-500/10 px-1 py-0.5 rounded text-[10px]">.env</code> file:
518
+ </p>
519
+ <div className="flex flex-wrap gap-1.5">
520
+ {actions[selectedAction].envVars.map((envVar) => (
521
+ <code key={envVar} className="text-[10px] font-mono bg-amber-500/10 text-amber-600 px-1.5 py-0.5 rounded">
522
+ {envVar}=...
523
+ </code>
524
+ ))}
525
+ </div>
526
+ </div>
527
+ )}
528
+
529
+ <div className="space-y-3">
530
+ <label className="block text-xs font-medium text-[var(--kyro-text-secondary)]">Events</label>
531
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
371
532
  {eventOptions.map((opt) => (
372
533
  <button
373
534
  type="button"
@@ -378,17 +539,17 @@ export function WebhookManager() {
378
539
  : [...formData.events, opt.value];
379
540
  setFormData({ ...formData, events });
380
541
  }}
381
- className={`flex items-center gap-3 p-3 rounded-lg border transition-all text-left ${formData.events.includes(opt.value)
542
+ className={`flex items-center gap-2.5 p-2.5 rounded-lg border transition-all text-left text-sm ${formData.events.includes(opt.value)
382
543
  ? "bg-[var(--kyro-primary)]/5 border-[var(--kyro-primary)]/30 text-[var(--kyro-primary)]"
383
- : "bg-[var(--kyro-surface-accent)]/50 border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/30"
544
+ : "border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/20"
384
545
  }`}
385
546
  >
386
- <div className={`w-4 h-4 rounded flex items-center justify-center border transition-all ${formData.events.includes(opt.value) ? "bg-[var(--kyro-primary)] border-[var(--kyro-primary)]" : "border-[var(--kyro-border)]"}`}>
387
- {formData.events.includes(opt.value) && <CheckCircle2 className="w-3 h-3 text-white" />}
547
+ <div className={`w-3.5 h-3.5 rounded flex items-center justify-center border transition-all ${formData.events.includes(opt.value) ? "bg-[var(--kyro-primary)] border-[var(--kyro-primary)]" : "border-[var(--kyro-border)]"}`}>
548
+ {formData.events.includes(opt.value) && <CheckCircle2 className="w-2.5 h-2.5 text-white" />}
388
549
  </div>
389
550
  <div>
390
- <div className="text-xs font-bold">{opt.label}</div>
391
- <div className="text-[10px] opacity-50">{opt.description}</div>
551
+ <div className="text-xs font-medium">{opt.label}</div>
552
+ <div className="text-[10px] opacity-40">{opt.description}</div>
392
553
  </div>
393
554
  </button>
394
555
  ))}
@@ -397,8 +558,8 @@ export function WebhookManager() {
397
558
  </div>
398
559
 
399
560
  {createError && (
400
- <div className="p-3 bg-red-500/10 border border-red-500/20 rounded-xl flex items-center gap-2 text-red-500 text-xs font-bold">
401
- <AlertTriangle className="w-4 h-4" />
561
+ <div className="p-2.5 bg-red-500/10 border border-red-500/20 rounded-lg flex items-center gap-2 text-red-500 text-xs">
562
+ <AlertTriangle className="w-3.5 h-3.5" />
402
563
  {createError}
403
564
  </div>
404
565
  )}
@@ -407,17 +568,17 @@ export function WebhookManager() {
407
568
  <ModalActions>
408
569
  <button
409
570
  type="button"
410
- onClick={() => setShowCreateModal(false)}
411
- className="px-6 py-2.5 rounded-xl font-bold text-sm border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] transition-colors"
571
+ onClick={() => setStep("action")}
572
+ className="px-4 py-2 rounded-lg text-sm font-medium border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] transition-colors"
412
573
  >
413
- Cancel
574
+ Back
414
575
  </button>
415
576
  <button
416
577
  type="button"
417
578
  onClick={handleCreate}
418
- className="kyro-btn kyro-btn-primary px-6 py-2.5 rounded-xl font-bold text-sm hover:opacity-90 transition-all shadow-lg shadow-[var(--kyro-primary)]/10"
579
+ className="kyro-btn kyro-btn-primary px-4 py-2 rounded-lg text-sm font-medium"
419
580
  >
420
- Create Webhook
581
+ Create webhook
421
582
  </button>
422
583
  </ModalActions>
423
584
  </Modal>
@@ -426,28 +587,28 @@ export function WebhookManager() {
426
587
  <Modal
427
588
  open={showTestModal}
428
589
  onClose={() => setShowTestModal(false)}
429
- title="Webhook Test Dispatcher"
590
+ title="Test webhook"
430
591
  >
431
592
  <ModalContent>
432
- <div className="p-8 rounded-[2rem] bg-[var(--kyro-surface-accent)]/30 border border-[var(--kyro-border)] text-center">
593
+ <div className="p-6 rounded-xl bg-[var(--kyro-surface)] border border-[var(--kyro-border)] text-center">
433
594
  {testResult ? (
434
- <div className="space-y-6">
435
- <div className={`w-16 h-16 mx-auto rounded-2xl flex items-center justify-center shadow-xl ${testResult.success ? "bg-green-500/10 text-green-500" : "bg-red-500/10 text-red-500"}`}>
436
- {testResult.success ? <CheckCircle2 className="w-8 h-8" /> : <AlertTriangle className="w-8 h-8" />}
595
+ <div className="space-y-4">
596
+ <div className={`w-12 h-12 mx-auto rounded-xl flex items-center justify-center ${testResult.success ? "bg-green-500/10 text-green-500" : "bg-red-500/10 text-red-500"}`}>
597
+ {testResult.success ? <CheckCircle2 className="w-6 h-6" /> : <AlertTriangle className="w-6 h-6" />}
437
598
  </div>
438
599
  <div>
439
- <h4 className="text-xl font-bold mb-2">{testResult.success ? "Dispatch Successful" : "Dispatch Failed"}</h4>
440
- <p className="text-sm text-[var(--kyro-text-secondary)] opacity-70">
600
+ <h4 className="text-sm font-semibold mb-1">{testResult.success ? "Delivered" : "Failed"}</h4>
601
+ <p className="text-xs text-[var(--kyro-text-secondary)] opacity-60">
441
602
  {testResult.message}
442
603
  </p>
443
604
  </div>
444
605
  </div>
445
606
  ) : (
446
- <div className="space-y-6 py-4">
447
- <RefreshCw className="w-12 h-12 text-[var(--kyro-primary)] animate-spin mx-auto opacity-50" />
607
+ <div className="space-y-4 py-4">
608
+ <RefreshCw className="w-8 h-8 text-[var(--kyro-primary)] animate-spin mx-auto opacity-40" />
448
609
  <div>
449
- <h4 className="text-xl font-bold mb-1">Synthesizing Payload</h4>
450
- <p className="text-sm text-[var(--kyro-text-secondary)] opacity-50 italic">Dispatching to remote endpoint...</p>
610
+ <h4 className="text-sm font-semibold mb-1">Sending test payload</h4>
611
+ <p className="text-xs text-[var(--kyro-text-secondary)] opacity-50">Dispatching...</p>
451
612
  </div>
452
613
  </div>
453
614
  )}
@@ -457,9 +618,9 @@ export function WebhookManager() {
457
618
  <button
458
619
  type="button"
459
620
  onClick={() => setShowTestModal(false)}
460
- className="w-full py-3 rounded-xl font-bold text-sm bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] hover:bg-[var(--kyro-surface)] transition-all"
621
+ className="w-full py-2.5 rounded-lg text-sm font-medium bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] hover:bg-[var(--kyro-surface)] transition-colors"
461
622
  >
462
- Acknowledge
623
+ Close
463
624
  </button>
464
625
  </ModalActions>
465
626
  </Modal>
@@ -468,53 +629,42 @@ export function WebhookManager() {
468
629
  <Modal
469
630
  open={showHelpModal}
470
631
  onClose={() => setShowHelpModal(false)}
471
- title="Webhook Integration Guide"
472
- size="lg"
632
+ title="How webhooks work"
473
633
  >
474
634
  <ModalContent>
475
- <div className="space-y-8">
476
- <div className="grid grid-cols-1 gap-6">
477
- <div className="p-6 rounded-3xl bg-[var(--kyro-surface-accent)]/50 border border-[var(--kyro-border)]">
478
- <h4 className="font-bold mb-3 flex items-center gap-2">
479
- <ExternalLink className="w-4 h-4 text-[var(--kyro-primary)]" />
480
- Request Context
635
+ <div className="space-y-6">
636
+ <div className="grid grid-cols-1 gap-4">
637
+ <div className="p-4 rounded-xl bg-[var(--kyro-surface)] border border-[var(--kyro-border)]">
638
+ <h4 className="text-sm font-medium mb-1.5 flex items-center gap-2">
639
+ <ExternalLink className="w-3.5 h-3.5 text-[var(--kyro-primary)]" />
640
+ Request format
481
641
  </h4>
482
- <p className="text-sm text-[var(--kyro-text-secondary)] leading-relaxed">
483
- When an event triggers, Kyro sends a standard <span className="font-bold text-[var(--kyro-text-primary)]">POST</span> request
642
+ <p className="text-xs text-[var(--kyro-text-secondary)] leading-relaxed opacity-70">
643
+ When an event triggers, Kyro sends a <span className="font-medium">POST</span> request
484
644
  to your endpoint with a JSON payload containing document metadata and operation details.
485
645
  </p>
486
646
  </div>
487
- <div className="p-6 rounded-3xl bg-[var(--kyro-surface-accent)]/50 border border-[var(--kyro-border)]">
488
- <h4 className="font-bold mb-3 flex items-center gap-2">
489
- <Shield className="w-4 h-4 text-[var(--kyro-primary)]" />
490
- Security Verification
647
+ <div className="p-4 rounded-xl bg-[var(--kyro-surface)] border border-[var(--kyro-border)]">
648
+ <h4 className="text-sm font-medium mb-1.5 flex items-center gap-2">
649
+ <Shield className="w-3.5 h-3.5 text-[var(--kyro-primary)]" />
650
+ Signature verification
491
651
  </h4>
492
- <p className="text-sm text-[var(--kyro-text-secondary)] leading-relaxed">
493
- If a secret is provided, each request includes an <span className="font-mono text-[var(--kyro-primary)]">X-Kyro-Signature</span> header.
494
- Always verify this signature in production environments.
652
+ <p className="text-xs text-[var(--kyro-text-secondary)] leading-relaxed opacity-70">
653
+ If a secret is set, each request includes an <span className="font-mono text-[10px] bg-[var(--kyro-surface-accent)] px-1 py-0.5 rounded">X-Webhook-Signature</span> header
654
+ with an HMAC-SHA256 signature. Verify it in production.
495
655
  </p>
496
656
  </div>
497
657
  </div>
498
658
 
499
- <div className="space-y-3">
500
- <h4 className="text-xs font-bold uppercase tracking-widest opacity-40 px-1">Payload Architecture</h4>
501
- <div className="relative group">
502
- <div className="absolute -inset-1 bg-gradient-to-r from-[var(--kyro-primary)]/20 to-transparent rounded-2xl blur opacity-25 group-hover:opacity-50 transition-all"></div>
503
- <div className="relative bg-[var(--kyro-bg)] rounded-2xl border border-[var(--kyro-border)] p-6 font-mono text-sm overflow-hidden">
504
- <div className="flex items-center gap-2 mb-4 border-b border-[var(--kyro-border)] pb-4 opacity-30">
505
- <div className="w-2 h-2 rounded-full bg-red-500/50" />
506
- <div className="w-2 h-2 rounded-full bg-amber-500/50" />
507
- <div className="w-2 h-2 rounded-full bg-green-500/50" />
508
- <span className="text-[10px] ml-2 tracking-tighter">payload.json</span>
509
- </div>
510
- <div className="space-y-1">
511
- <div className="text-[var(--kyro-text-secondary)]">{"{"}</div>
512
- <div className="pl-4"><span className="text-[var(--kyro-primary)]">"event"</span>: <span className="text-green-500">"collection.create"</span>,</div>
513
- <div className="pl-4"><span className="text-[var(--kyro-primary)]">"collection"</span>: <span className="text-green-500">"products"</span>,</div>
514
- <div className="pl-4"><span className="text-[var(--kyro-primary)]">"id"</span>: <span className="text-green-500">"prod_8273"</span>,</div>
515
- <div className="pl-4"><span className="text-[var(--kyro-primary)]">"timestamp"</span>: <span className="text-green-500">"2026-05-14T02:53:22Z"</span></div>
516
- <div className="text-[var(--kyro-text-secondary)]">{"}"}</div>
517
- </div>
659
+ <div className="space-y-2">
660
+ <h4 className="text-xs font-medium text-[var(--kyro-text-secondary)] opacity-50">Example payload</h4>
661
+ <div className="bg-[var(--kyro-bg)] rounded-xl border border-[var(--kyro-border)] p-4 font-mono text-xs overflow-hidden">
662
+ <div className="space-y-0.5 text-[var(--kyro-text-secondary)]">
663
+ <div>{"{"}</div>
664
+ <div className="pl-3"><span className="text-[var(--kyro-primary)]">"event"</span>: <span className="text-green-500">"collection.create"</span>,</div>
665
+ <div className="pl-3"><span className="text-[var(--kyro-primary)]">"collection"</span>: <span className="text-green-500">"posts"</span>,</div>
666
+ <div className="pl-3"><span className="text-[var(--kyro-primary)]">"timestamp"</span>: <span className="text-green-500">"2026-06-28T12:00:00Z"</span></div>
667
+ <div>{"}"}</div>
518
668
  </div>
519
669
  </div>
520
670
  </div>
@@ -524,9 +674,9 @@ export function WebhookManager() {
524
674
  <button
525
675
  type="button"
526
676
  onClick={() => setShowHelpModal(false)}
527
- className="kyro-btn kyro-btn-primary w-full py-3 rounded-xl font-bold text-sm hover:opacity-90 transition-all shadow-lg shadow-[var(--kyro-primary)]/20"
677
+ className="kyro-btn kyro-btn-primary w-full py-2.5 rounded-lg text-sm font-medium"
528
678
  >
529
- I Understand
679
+ Got it
530
680
  </button>
531
681
  </ModalActions>
532
682
  </Modal>