@kyro-cms/admin 0.11.6 → 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.
- package/dist/index.cjs +18 -18
- package/dist/index.js +18 -18
- package/package.json +1 -1
- package/src/components/AutoForm.tsx +54 -22
- package/src/components/GraphQLPlayground.tsx +259 -12
- package/src/components/WebhookManager.tsx +338 -188
- package/src/components/autoform/AutoFormHeader.tsx +19 -8
- package/src/components/autoform/ErrorBoundary.tsx +93 -0
- package/src/components/blocks/HeadingSubheadingBlock.tsx +2 -2
- package/src/components/blocks/HeroBlock.tsx +2 -2
- package/src/components/fields/BlocksField.tsx +1 -1
- package/src/components/fields/HeadingSubheadingField.tsx +2 -2
- package/src/components/fields/HeroField.tsx +4 -4
- package/src/components/fields/RelationshipField.tsx +2 -2
- package/src/components/fields/extensions/blocksStore.ts +1 -1
- package/src/hooks/useAutoFormState.ts +51 -1
- package/src/integration.ts +4 -4
- package/src/pages/settings/[slug].astro +1 -1
|
@@ -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()
|
|
69
|
-
setCreateError("Name
|
|
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
|
-
|
|
76
|
-
toast.success(`Webhook
|
|
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("
|
|
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" ? "
|
|
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="
|
|
235
|
+
description="Get notified when your content changes."
|
|
137
236
|
icon={Webhook}
|
|
138
237
|
actions={[
|
|
139
238
|
{
|
|
140
|
-
label: "
|
|
239
|
+
label: "New webhook",
|
|
141
240
|
onClick: () => {
|
|
142
|
-
|
|
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
|
|
163
|
-
<div className="relative overflow-hidden rounded-
|
|
164
|
-
<div className="
|
|
165
|
-
<
|
|
166
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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)]"><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
|
|
275
|
+
<h2 className="text-sm font-medium text-[var(--kyro-text-secondary)]">Endpoints</h2>
|
|
197
276
|
</div>
|
|
198
|
-
<div className="text-[
|
|
199
|
-
{webhooks.length}
|
|
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
|
|
205
|
-
|
|
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-
|
|
209
|
-
<div className="w-
|
|
210
|
-
<Webhook className="w-
|
|
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-
|
|
213
|
-
<p className="text-sm text-[var(--kyro-text-secondary)] opacity-60 mb-
|
|
214
|
-
|
|
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={() =>
|
|
219
|
-
|
|
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-
|
|
222
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
235
|
-
<div className="p-2
|
|
236
|
-
<Webhook className="w-
|
|
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-
|
|
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-
|
|
242
|
-
<span className={`text-[10px] font-
|
|
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-
|
|
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-[
|
|
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-
|
|
257
|
-
<span className="text-[
|
|
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,
|
|
260
|
-
<Badge key={event} variant="outline" className="text-[
|
|
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 >
|
|
265
|
-
<span className="text-[
|
|
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-
|
|
270
|
-
<span className="text-[
|
|
271
|
-
<div className="text-[10px]
|
|
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
|
|
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-
|
|
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="
|
|
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-
|
|
289
|
-
<span className="text-
|
|
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-
|
|
298
|
-
title={webhook.status === "active" ? "Pause
|
|
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-
|
|
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-
|
|
307
|
-
title="Delete
|
|
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-
|
|
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="
|
|
324
|
-
size="lg"
|
|
406
|
+
title="New webhook"
|
|
325
407
|
>
|
|
326
408
|
<ModalContent>
|
|
327
|
-
<div className="space-y-
|
|
328
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
369
|
-
<
|
|
370
|
-
|
|
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-
|
|
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
|
-
: "
|
|
544
|
+
: "border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/20"
|
|
384
545
|
}`}
|
|
385
546
|
>
|
|
386
|
-
<div className={`w-
|
|
387
|
-
{formData.events.includes(opt.value) && <CheckCircle2 className="w-
|
|
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-
|
|
391
|
-
<div className="text-[10px] opacity-
|
|
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-
|
|
401
|
-
<AlertTriangle className="w-
|
|
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={() =>
|
|
411
|
-
className="px-
|
|
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
|
-
|
|
574
|
+
Back
|
|
414
575
|
</button>
|
|
415
576
|
<button
|
|
416
577
|
type="button"
|
|
417
578
|
onClick={handleCreate}
|
|
418
|
-
className="kyro-btn kyro-btn-primary px-
|
|
579
|
+
className="kyro-btn kyro-btn-primary px-4 py-2 rounded-lg text-sm font-medium"
|
|
419
580
|
>
|
|
420
|
-
Create
|
|
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="
|
|
590
|
+
title="Test webhook"
|
|
430
591
|
>
|
|
431
592
|
<ModalContent>
|
|
432
|
-
<div className="p-
|
|
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-
|
|
435
|
-
<div className={`w-
|
|
436
|
-
{testResult.success ? <CheckCircle2 className="w-
|
|
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-
|
|
440
|
-
<p className="text-
|
|
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-
|
|
447
|
-
<RefreshCw className="w-
|
|
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-
|
|
450
|
-
<p className="text-
|
|
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-
|
|
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
|
-
|
|
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="
|
|
472
|
-
size="lg"
|
|
632
|
+
title="How webhooks work"
|
|
473
633
|
>
|
|
474
634
|
<ModalContent>
|
|
475
|
-
<div className="space-y-
|
|
476
|
-
<div className="grid grid-cols-1 gap-
|
|
477
|
-
<div className="p-
|
|
478
|
-
<h4 className="font-
|
|
479
|
-
<ExternalLink className="w-
|
|
480
|
-
Request
|
|
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-
|
|
483
|
-
When an event triggers, Kyro sends a
|
|
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-
|
|
488
|
-
<h4 className="font-
|
|
489
|
-
<Shield className="w-
|
|
490
|
-
|
|
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-
|
|
493
|
-
If a secret is
|
|
494
|
-
|
|
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-
|
|
500
|
-
<h4 className="text-xs font-
|
|
501
|
-
<div className="
|
|
502
|
-
<div className="
|
|
503
|
-
|
|
504
|
-
<div className="
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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-
|
|
677
|
+
className="kyro-btn kyro-btn-primary w-full py-2.5 rounded-lg text-sm font-medium"
|
|
528
678
|
>
|
|
529
|
-
|
|
679
|
+
Got it
|
|
530
680
|
</button>
|
|
531
681
|
</ModalActions>
|
|
532
682
|
</Modal>
|