@kyro-cms/admin 0.1.6 → 0.1.7
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/README.md +149 -51
- package/package.json +53 -6
- package/src/collections/auth/index.ts +2 -2
- package/src/collections/portfolio/index.ts +343 -0
- package/src/components/ActionBar.tsx +153 -16
- package/src/components/Admin.tsx +136 -27
- package/src/components/ApiExplorer.tsx +325 -0
- package/src/components/ApiKeysManager.tsx +563 -0
- package/src/components/AuditLogsPage.tsx +664 -0
- package/src/components/AutoForm.tsx +1417 -661
- package/src/components/BrandingHub.tsx +267 -0
- package/src/components/BulkActionsBar.tsx +3 -3
- package/src/components/CreateView.tsx +3 -3
- package/src/components/Dashboard.tsx +393 -0
- package/src/components/DetailView.tsx +199 -57
- package/src/components/DeveloperCenter.tsx +403 -0
- package/src/components/EnhancedListView.tsx +786 -0
- package/src/components/GraphQLExplorer.tsx +675 -0
- package/src/components/GraphQLPlayground.tsx +627 -0
- package/src/components/ListView.tsx +191 -53
- package/src/components/MediaGallery.tsx +1569 -0
- package/src/components/Modal.tsx +149 -0
- package/src/components/RestPlayground.tsx +951 -0
- package/src/components/Sidebar.astro +237 -0
- package/src/components/UserManagement.tsx +204 -0
- package/src/components/VersionHistoryPanel.tsx +3 -3
- package/src/components/WebhookManager.tsx +608 -0
- package/src/components/blocks/AccordionBlock.tsx +97 -0
- package/src/components/blocks/ArrayBlock.tsx +75 -0
- package/src/components/blocks/BlockEditModal.MARKER +12 -0
- package/src/components/blocks/BlockEditModal.tsx +774 -0
- package/src/components/blocks/ButtonBlock.tsx +165 -0
- package/src/components/blocks/ChildBlocksTree.tsx +551 -0
- package/src/components/blocks/CodeBlock.tsx +66 -0
- package/src/components/blocks/ColumnsBlock.tsx +151 -0
- package/src/components/blocks/DividerBlock.tsx +43 -0
- package/src/components/blocks/FileBlock.tsx +64 -0
- package/src/components/blocks/HeadingBlock.tsx +81 -0
- package/src/components/blocks/HeroBlock.tsx +157 -0
- package/src/components/blocks/ImageBlock.tsx +83 -0
- package/src/components/blocks/LinkBlock.tsx +71 -0
- package/src/components/blocks/ListBlock.tsx +39 -0
- package/src/components/blocks/ParagraphBlock.tsx +61 -0
- package/src/components/blocks/RelationshipBlock.tsx +279 -0
- package/src/components/blocks/VStackBlock.tsx +75 -0
- package/src/components/blocks/VideoBlock.tsx +45 -0
- package/src/components/blocks/index.ts +10 -0
- package/src/components/fields/BlocksField.tsx +323 -0
- package/src/components/fields/CheckboxField.tsx +15 -9
- package/src/components/fields/CodeField.tsx +234 -0
- package/src/components/fields/DateField.tsx +38 -11
- package/src/components/fields/EditorClient.tsx +271 -0
- package/src/components/fields/FileField.tsx +390 -0
- package/src/components/fields/HybridContentField.tsx +109 -0
- package/src/components/fields/ImageField.tsx +429 -0
- package/src/components/fields/JSONField.tsx +361 -0
- package/src/components/fields/MarkdownField.tsx +282 -0
- package/src/components/fields/NumberField.tsx +42 -12
- package/src/components/fields/PortableTextField.tsx +143 -0
- package/src/components/fields/PortableTextRenderer.tsx +68 -0
- package/src/components/fields/RelationshipField.tsx +231 -59
- package/src/components/fields/SelectField.tsx +25 -15
- package/src/components/fields/TextField.tsx +45 -14
- package/src/components/fields/extensions/blockComponents.tsx +237 -0
- package/src/components/fields/extensions/blocksStore.ts +273 -0
- package/src/components/fields/index.ts +13 -0
- package/src/components/index.ts +1 -2
- package/src/components/layout/Header.tsx +2 -2
- package/src/components/layout/Layout.tsx +2 -2
- package/src/components/ui/Badge.tsx +9 -4
- package/src/components/ui/BlockDrawer.tsx +79 -0
- package/src/components/ui/Button.tsx +1 -1
- package/src/components/ui/CommandPalette.tsx +362 -0
- package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
- package/src/components/ui/Dropdown.tsx +1 -1
- package/src/components/ui/Modal.tsx +37 -12
- package/src/components/ui/PromptModal.tsx +94 -0
- package/src/components/ui/SlidePanel.tsx +43 -16
- package/src/components/ui/Toast.tsx +80 -14
- package/src/env.d.ts +16 -0
- package/src/env.ts +20 -0
- package/src/index.ts +0 -1
- package/src/layouts/AdminLayout.astro +164 -170
- package/src/layouts/AuthLayout.astro +23 -6
- package/src/lib/MediaService.ts +541 -0
- package/src/lib/auth/sqlite-adapter.ts +319 -0
- package/src/lib/config.ts +22 -6
- package/src/lib/dataStore.ts +132 -74
- package/src/lib/db/adapter.ts +54 -0
- package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
- package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
- package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
- package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
- package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
- package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
- package/src/lib/db/index.ts +449 -0
- package/src/lib/db/mongodb-adapter.ts +207 -0
- package/src/lib/db/mongodb-auth-adapter.ts +305 -0
- package/src/lib/db/schema/mysql-auth.ts +113 -0
- package/src/lib/db/schema/mysql-content.ts +20 -0
- package/src/lib/db/schema/postgres-auth.ts +116 -0
- package/src/lib/db/schema/postgres-content.ts +35 -0
- package/src/lib/db/schema/postgres-media.ts +52 -0
- package/src/lib/db/schema/postgres-settings.ts +11 -0
- package/src/lib/db/schema/sqlite-auth.ts +112 -0
- package/src/lib/db/schema/sqlite-content.ts +20 -0
- package/src/lib/graphql/index.ts +1 -0
- package/src/lib/graphql/schema.ts +443 -0
- package/src/lib/rate-limit.ts +267 -0
- package/src/lib/storage.ts +374 -0
- package/src/lib/store.ts +85 -0
- package/src/middleware.ts +70 -11
- package/src/pages/[collection]/[id].astro +178 -122
- package/src/pages/[collection]/index.astro +24 -156
- package/src/pages/admin/api-explorer.astro +98 -0
- package/src/pages/admin/graphql-explorer.astro +40 -0
- package/src/pages/admin/graphql.astro +97 -0
- package/src/pages/admin/index.astro +200 -139
- package/src/pages/admin/keys.astro +8 -0
- package/src/pages/admin/rest-playground.astro +44 -0
- package/src/pages/admin/webhooks.astro +8 -0
- package/src/pages/api/[collection]/[id]/publish.ts +44 -0
- package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
- package/src/pages/api/[collection]/[id]/versions.ts +36 -0
- package/src/pages/api/[collection]/[id].ts +102 -159
- package/src/pages/api/[collection]/index.ts +151 -230
- package/src/pages/api/auth/[id].ts +48 -69
- package/src/pages/api/auth/audit-logs.ts +20 -43
- package/src/pages/api/auth/login.ts +159 -45
- package/src/pages/api/auth/logout.ts +42 -24
- package/src/pages/api/auth/refresh.ts +119 -0
- package/src/pages/api/auth/register.ts +110 -40
- package/src/pages/api/auth/users.ts +22 -97
- package/src/pages/api/collections.ts +59 -0
- package/src/pages/api/globals/[slug]/test.ts +172 -0
- package/src/pages/api/globals/[slug].ts +42 -0
- package/src/pages/api/graphql.ts +90 -0
- package/src/pages/api/health.ts +417 -40
- package/src/pages/api/keys/[id].ts +26 -0
- package/src/pages/api/keys/index.ts +75 -0
- package/src/pages/api/media/[id].ts +309 -0
- package/src/pages/api/media/folders.ts +609 -0
- package/src/pages/api/media/index.ts +146 -0
- package/src/pages/api/media/resize.ts +267 -0
- package/src/pages/api/search.ts +82 -0
- package/src/pages/api/slug-availability.ts +70 -0
- package/src/pages/api/storage-config.ts +20 -0
- package/src/pages/api/storage-status.ts +206 -0
- package/src/pages/api/upload.ts +334 -0
- package/src/pages/api/webhooks/index.ts +71 -0
- package/src/pages/audit/index.astro +2 -104
- package/src/pages/login.astro +11 -11
- package/src/pages/media.astro +10 -0
- package/src/pages/preview/[collection]/[id].astro +178 -0
- package/src/pages/register.astro +13 -13
- package/src/pages/roles/index.astro +21 -21
- package/src/pages/settings/[slug].astro +162 -0
- package/src/pages/settings/index.astro +9 -0
- package/src/pages/users/[id].astro +29 -21
- package/src/pages/users/index.astro +22 -17
- package/src/pages/users/new.astro +18 -17
- package/src/styles/main.css +553 -128
- package/src/components/layout/Sidebar.tsx +0 -497
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Webhook,
|
|
4
|
+
Plus,
|
|
5
|
+
Trash2,
|
|
6
|
+
Play,
|
|
7
|
+
Pause,
|
|
8
|
+
Send,
|
|
9
|
+
AlertTriangle,
|
|
10
|
+
CheckCircle2,
|
|
11
|
+
Clock,
|
|
12
|
+
RefreshCw,
|
|
13
|
+
Info,
|
|
14
|
+
ExternalLink,
|
|
15
|
+
Zap,
|
|
16
|
+
Shield,
|
|
17
|
+
} from "lucide-react";
|
|
18
|
+
import { ConfirmModal, Modal, ModalContent, ModalActions } from "./ui/Modal";
|
|
19
|
+
|
|
20
|
+
interface WebhookItem {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
url: string;
|
|
24
|
+
events: string[];
|
|
25
|
+
secret?: string;
|
|
26
|
+
status: "active" | "paused";
|
|
27
|
+
lastTriggered?: string;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function WebhookManager() {
|
|
32
|
+
const [webhooks, setWebhooks] = useState<WebhookItem[]>([]);
|
|
33
|
+
const [loading, setLoading] = useState(false);
|
|
34
|
+
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
35
|
+
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
|
36
|
+
const [showTestModal, setShowTestModal] = useState(false);
|
|
37
|
+
const [showHelpModal, setShowHelpModal] = useState(false);
|
|
38
|
+
const [testResult, setTestResult] = useState<{
|
|
39
|
+
success: boolean;
|
|
40
|
+
message: string;
|
|
41
|
+
} | null>(null);
|
|
42
|
+
const [deleteId, setDeleteId] = useState<string | null>(null);
|
|
43
|
+
const [testId, setTestId] = useState<string | null>(null);
|
|
44
|
+
const [formData, setFormData] = useState({
|
|
45
|
+
name: "",
|
|
46
|
+
url: "",
|
|
47
|
+
events: [] as string[],
|
|
48
|
+
secret: "",
|
|
49
|
+
});
|
|
50
|
+
const [createError, setCreateError] = useState("");
|
|
51
|
+
|
|
52
|
+
const loadWebhooks = async () => {
|
|
53
|
+
setLoading(true);
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch("/api/webhooks");
|
|
56
|
+
if (res.ok) {
|
|
57
|
+
const data = await res.json();
|
|
58
|
+
setWebhooks(data);
|
|
59
|
+
}
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error(e);
|
|
62
|
+
} finally {
|
|
63
|
+
setLoading(false);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
loadWebhooks();
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
const handleCreate = async () => {
|
|
72
|
+
if (!formData.name.trim() || !formData.url.trim()) {
|
|
73
|
+
setCreateError("Name and URL are required");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const res = await fetch("/api/webhooks", {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: { "Content-Type": "application/json" },
|
|
81
|
+
body: JSON.stringify(formData),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (res.ok) {
|
|
85
|
+
setShowCreateModal(false);
|
|
86
|
+
setFormData({ name: "", url: "", events: [], secret: "" });
|
|
87
|
+
loadWebhooks();
|
|
88
|
+
} else {
|
|
89
|
+
const error = await res.json();
|
|
90
|
+
setCreateError(error.error || "Failed to create webhook");
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.error(e);
|
|
94
|
+
setCreateError("Failed to create webhook");
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const handleDelete = async (id: string) => {
|
|
99
|
+
setDeleteId(id);
|
|
100
|
+
setShowDeleteModal(true);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const confirmDelete = async () => {
|
|
104
|
+
if (!deleteId) return;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const res = await fetch(`/api/webhooks/${deleteId}`, {
|
|
108
|
+
method: "DELETE",
|
|
109
|
+
});
|
|
110
|
+
if (res.ok) {
|
|
111
|
+
loadWebhooks();
|
|
112
|
+
}
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.error(e);
|
|
115
|
+
}
|
|
116
|
+
setShowDeleteModal(false);
|
|
117
|
+
setDeleteId(null);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const handleTest = async (id: string) => {
|
|
121
|
+
setTestId(id);
|
|
122
|
+
setTestResult(null);
|
|
123
|
+
setShowTestModal(true);
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const res = await fetch(`/api/webhooks/${id}/test`, { method: "POST" });
|
|
127
|
+
const data = await res.json();
|
|
128
|
+
setTestResult({
|
|
129
|
+
success: res.ok,
|
|
130
|
+
message:
|
|
131
|
+
data.message ||
|
|
132
|
+
(res.ok
|
|
133
|
+
? "Webhook triggered successfully"
|
|
134
|
+
: "Failed to trigger webhook"),
|
|
135
|
+
});
|
|
136
|
+
} catch (e) {
|
|
137
|
+
setTestResult({ success: false, message: "Error testing webhook" });
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const toggleStatus = async (id: string, currentStatus: string) => {
|
|
142
|
+
try {
|
|
143
|
+
const res = await fetch(`/api/webhooks/${id}`, {
|
|
144
|
+
method: "PATCH",
|
|
145
|
+
headers: { "Content-Type": "application/json" },
|
|
146
|
+
body: JSON.stringify({
|
|
147
|
+
status: currentStatus === "active" ? "paused" : "active",
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
150
|
+
if (res.ok) {
|
|
151
|
+
loadWebhooks();
|
|
152
|
+
}
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.error(e);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const eventOptions = [
|
|
159
|
+
{
|
|
160
|
+
label: "Create",
|
|
161
|
+
value: "create",
|
|
162
|
+
description: "When a new document is created",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
label: "Update",
|
|
166
|
+
value: "update",
|
|
167
|
+
description: "When a document is updated",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
label: "Delete",
|
|
171
|
+
value: "delete",
|
|
172
|
+
description: "When a document is deleted",
|
|
173
|
+
},
|
|
174
|
+
{ label: "Auth", value: "auth", description: "User login/logout events" },
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 px-8 pb-32">
|
|
179
|
+
{/* Header */}
|
|
180
|
+
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6 pt-4">
|
|
181
|
+
<div>
|
|
182
|
+
<div className="flex items-center gap-3">
|
|
183
|
+
<h1 className="text-4xl font-black tracking-tighter text-[var(--kyro-text-primary)]">
|
|
184
|
+
Web<span className="text-[var(--kyro-primary)]">hooks</span>
|
|
185
|
+
</h1>
|
|
186
|
+
<button type="button"
|
|
187
|
+
onClick={() => setShowHelpModal(true)}
|
|
188
|
+
className="p-2 rounded-lg text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)] transition-all"
|
|
189
|
+
title="Learn how webhooks work"
|
|
190
|
+
>
|
|
191
|
+
<Info className="w-5 h-5" />
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
194
|
+
<p className="text-[var(--kyro-text-secondary)] mt-1 font-medium opacity-70">
|
|
195
|
+
Receive real-time notifications when events occur in your CMS.
|
|
196
|
+
</p>
|
|
197
|
+
</div>
|
|
198
|
+
<button type="button"
|
|
199
|
+
onClick={() => {
|
|
200
|
+
setFormData({ name: "", url: "", events: [], secret: "" });
|
|
201
|
+
setCreateError("");
|
|
202
|
+
setShowCreateModal(true);
|
|
203
|
+
}}
|
|
204
|
+
className="flex items-center gap-2 px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-full font-black text-sm shadow-xl hover:opacity-90 active:scale-95 transition-all"
|
|
205
|
+
>
|
|
206
|
+
<Plus className="w-4 h-4" />
|
|
207
|
+
Add Webhook
|
|
208
|
+
</button>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
{/* Info Banner */}
|
|
212
|
+
<div className="surface-tile p-6 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)]">
|
|
213
|
+
<div className="flex items-start gap-4">
|
|
214
|
+
<div className="p-3 bg-[var(--kyro-primary)]/10 rounded-xl">
|
|
215
|
+
<Zap className="w-6 h-6 text-[var(--kyro-primary)]" />
|
|
216
|
+
</div>
|
|
217
|
+
<div className="flex-1">
|
|
218
|
+
<h3 className="text-lg font-black mb-1">What are webhooks?</h3>
|
|
219
|
+
<p className="text-sm text-[var(--kyro-text-secondary)] opacity-70 mb-3">
|
|
220
|
+
Webhooks allow your application to receive real-time HTTP
|
|
221
|
+
notifications when events happen in your CMS. Instead of polling
|
|
222
|
+
the API, your endpoint gets notified immediately.
|
|
223
|
+
</p>
|
|
224
|
+
<div className="grid grid-cols-2 gap-3 text-xs">
|
|
225
|
+
{eventOptions.map((opt) => (
|
|
226
|
+
<div key={opt.value} className="flex items-center gap-2">
|
|
227
|
+
<span className="w-2 h-2 rounded-full bg-[var(--kyro-primary)]" />
|
|
228
|
+
<span className="font-medium">{opt.label}</span>
|
|
229
|
+
</div>
|
|
230
|
+
))}
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
{/* Webhooks List */}
|
|
237
|
+
<section className="space-y-6">
|
|
238
|
+
<div className="flex items-center gap-2 px-2">
|
|
239
|
+
<Webhook className="w-4 h-4 text-[var(--kyro-primary)] opacity-40" />
|
|
240
|
+
<span className="text-[10px] font-black uppercase tracking-widest opacity-40">
|
|
241
|
+
Configured Webhooks
|
|
242
|
+
</span>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
{loading ? (
|
|
246
|
+
<div className="surface-tile p-8 text-center">Loading...</div>
|
|
247
|
+
) : webhooks.length === 0 ? (
|
|
248
|
+
<div className="surface-tile p-12 text-center border-2 border-dashed border-[var(--kyro-border)]">
|
|
249
|
+
<div className="w-16 h-16 mx-auto mb-6 bg-[var(--kyro-surface-accent)] rounded-2xl flex items-center justify-center">
|
|
250
|
+
<Webhook className="w-8 h-8 text-[var(--kyro-text-secondary)] opacity-20" />
|
|
251
|
+
</div>
|
|
252
|
+
<h3 className="text-lg font-black mb-2">No Webhooks Configured</h3>
|
|
253
|
+
<p className="text-sm text-[var(--kyro-text-secondary)] opacity-60 mb-6">
|
|
254
|
+
Add your first webhook to start receiving event notifications.
|
|
255
|
+
</p>
|
|
256
|
+
<button type="button"
|
|
257
|
+
onClick={() => setShowCreateModal(true)}
|
|
258
|
+
className="px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-full font-black text-sm hover:opacity-90 transition-all"
|
|
259
|
+
>
|
|
260
|
+
<Plus className="w-4 h-4 inline mr-2" />
|
|
261
|
+
Create Your First Webhook
|
|
262
|
+
</button>
|
|
263
|
+
</div>
|
|
264
|
+
) : (
|
|
265
|
+
<div className="grid gap-4">
|
|
266
|
+
{webhooks.map((webhook) => (
|
|
267
|
+
<div
|
|
268
|
+
key={webhook.id}
|
|
269
|
+
className="surface-tile p-6 group hover:border-[var(--kyro-primary)] transition-all"
|
|
270
|
+
>
|
|
271
|
+
<div className="flex items-start justify-between gap-6">
|
|
272
|
+
<div className="flex-1">
|
|
273
|
+
<div className="flex items-center gap-3 mb-3">
|
|
274
|
+
<h3 className="text-lg font-black">{webhook.name}</h3>
|
|
275
|
+
<span
|
|
276
|
+
className={`px-2 py-0.5 rounded-full text-[8px] font-black uppercase ${
|
|
277
|
+
webhook.status === "active"
|
|
278
|
+
? "bg-green-500/10 text-green-500"
|
|
279
|
+
: "bg-amber-500/10 text-amber-500"
|
|
280
|
+
}`}
|
|
281
|
+
>
|
|
282
|
+
{webhook.status}
|
|
283
|
+
</span>
|
|
284
|
+
</div>
|
|
285
|
+
<p className="text-sm text-[var(--kyro-text-secondary)] opacity-50 font-mono mb-4 break-all">
|
|
286
|
+
{webhook.url}
|
|
287
|
+
</p>
|
|
288
|
+
<div className="flex items-center gap-2 flex-wrap mb-4">
|
|
289
|
+
{webhook.events?.map((event) => (
|
|
290
|
+
<span
|
|
291
|
+
key={event}
|
|
292
|
+
className="px-3 py-1 bg-[var(--kyro-surface-accent)] rounded-lg text-[10px] font-black uppercase opacity-60"
|
|
293
|
+
>
|
|
294
|
+
{event}
|
|
295
|
+
</span>
|
|
296
|
+
))}
|
|
297
|
+
</div>
|
|
298
|
+
<div className="flex items-center gap-6 text-[10px] font-black uppercase opacity-40">
|
|
299
|
+
<div className="flex items-center gap-2">
|
|
300
|
+
<RefreshCw className="w-3 h-3" />
|
|
301
|
+
{webhook.lastTriggered
|
|
302
|
+
? `Last triggered: ${new Date(webhook.lastTriggered).toLocaleDateString()}`
|
|
303
|
+
: "Never triggered"}
|
|
304
|
+
</div>
|
|
305
|
+
<div className="flex items-center gap-2">
|
|
306
|
+
<Clock className="w-3 h-3" />
|
|
307
|
+
Created:{" "}
|
|
308
|
+
{new Date(webhook.createdAt).toLocaleDateString()}
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
<div className="flex items-center gap-2 flex-shrink-0">
|
|
313
|
+
<button type="button"
|
|
314
|
+
onClick={() => handleTest(webhook.id)}
|
|
315
|
+
className="p-3 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-xl hover:bg-[var(--kyro-surface)] flex items-center gap-2"
|
|
316
|
+
title="Send test request"
|
|
317
|
+
>
|
|
318
|
+
<Send className="w-4 h-4" />
|
|
319
|
+
<span className="text-xs font-medium hidden lg:inline">
|
|
320
|
+
Test
|
|
321
|
+
</span>
|
|
322
|
+
</button>
|
|
323
|
+
<button type="button"
|
|
324
|
+
onClick={() => toggleStatus(webhook.id, webhook.status)}
|
|
325
|
+
className={`p-3 rounded-xl flex items-center gap-2 ${
|
|
326
|
+
webhook.status === "active"
|
|
327
|
+
? "text-amber-500 bg-amber-500/10 hover:bg-amber-500/20"
|
|
328
|
+
: "text-green-500 bg-green-500/10 hover:bg-green-500/20"
|
|
329
|
+
}`}
|
|
330
|
+
title={
|
|
331
|
+
webhook.status === "active"
|
|
332
|
+
? "Pause webhook"
|
|
333
|
+
: "Activate webhook"
|
|
334
|
+
}
|
|
335
|
+
>
|
|
336
|
+
{webhook.status === "active" ? (
|
|
337
|
+
<>
|
|
338
|
+
<Pause className="w-4 h-4" />
|
|
339
|
+
<span className="text-xs font-medium hidden lg:inline">
|
|
340
|
+
Pause
|
|
341
|
+
</span>
|
|
342
|
+
</>
|
|
343
|
+
) : (
|
|
344
|
+
<>
|
|
345
|
+
<Play className="w-4 h-4" />
|
|
346
|
+
<span className="text-xs font-medium hidden lg:inline">
|
|
347
|
+
Activate
|
|
348
|
+
</span>
|
|
349
|
+
</>
|
|
350
|
+
)}
|
|
351
|
+
</button>
|
|
352
|
+
<button type="button"
|
|
353
|
+
onClick={() => handleDelete(webhook.id)}
|
|
354
|
+
className="p-3 text-red-500 bg-red-500/10 rounded-xl hover:bg-red-500/20"
|
|
355
|
+
title="Delete webhook"
|
|
356
|
+
>
|
|
357
|
+
<Trash2 className="w-4 h-4" />
|
|
358
|
+
</button>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
))}
|
|
363
|
+
</div>
|
|
364
|
+
)}
|
|
365
|
+
</section>
|
|
366
|
+
|
|
367
|
+
{/* Create Modal */}
|
|
368
|
+
<Modal
|
|
369
|
+
open={showCreateModal}
|
|
370
|
+
onClose={() => setShowCreateModal(false)}
|
|
371
|
+
title="Add New Webhook"
|
|
372
|
+
>
|
|
373
|
+
<ModalContent>
|
|
374
|
+
<div className="space-y-5">
|
|
375
|
+
<div>
|
|
376
|
+
<label className="block text-sm font-medium mb-2">
|
|
377
|
+
Name <span className="text-red-500">*</span>
|
|
378
|
+
</label>
|
|
379
|
+
<input
|
|
380
|
+
type="text"
|
|
381
|
+
value={formData.name}
|
|
382
|
+
onChange={(e) =>
|
|
383
|
+
setFormData({ ...formData, name: e.target.value })
|
|
384
|
+
}
|
|
385
|
+
placeholder="e.g., Slack Notifications, Email Parser"
|
|
386
|
+
className="w-full px-4 py-3 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-xl focus:outline-none focus:border-[var(--kyro-primary)]"
|
|
387
|
+
/>
|
|
388
|
+
</div>
|
|
389
|
+
<div>
|
|
390
|
+
<label className="block text-sm font-medium mb-2">
|
|
391
|
+
Endpoint URL <span className="text-red-500">*</span>
|
|
392
|
+
</label>
|
|
393
|
+
<input
|
|
394
|
+
type="url"
|
|
395
|
+
value={formData.url}
|
|
396
|
+
onChange={(e) =>
|
|
397
|
+
setFormData({ ...formData, url: e.target.value })
|
|
398
|
+
}
|
|
399
|
+
placeholder="https://your-server.com/webhook"
|
|
400
|
+
className="w-full px-4 py-3 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-xl focus:outline-none focus:border-[var(--kyro-primary)]"
|
|
401
|
+
/>
|
|
402
|
+
<p className="text-xs text-[var(--kyro-text-secondary)] mt-1 opacity-60">
|
|
403
|
+
The URL that will receive the POST request when events occur.
|
|
404
|
+
</p>
|
|
405
|
+
</div>
|
|
406
|
+
<div>
|
|
407
|
+
<label className="block text-sm font-medium mb-2">
|
|
408
|
+
Events to trigger on
|
|
409
|
+
</label>
|
|
410
|
+
<div className="grid grid-cols-2 gap-3">
|
|
411
|
+
{eventOptions.map((opt) => (
|
|
412
|
+
<button type="button"
|
|
413
|
+
key={opt.value}
|
|
414
|
+
onClick={() => {
|
|
415
|
+
const events = formData.events.includes(opt.value)
|
|
416
|
+
? formData.events.filter((e) => e !== opt.value)
|
|
417
|
+
: [...formData.events, opt.value];
|
|
418
|
+
setFormData({ ...formData, events });
|
|
419
|
+
}}
|
|
420
|
+
className={`p-3 rounded-xl text-left transition-all ${
|
|
421
|
+
formData.events.includes(opt.value)
|
|
422
|
+
? "bg-[var(--kyro-primary)]/10 border border-[var(--kyro-primary)] text-[var(--kyro-primary)]"
|
|
423
|
+
: "bg-[var(--kyro-surface-accent)] border border-transparent"
|
|
424
|
+
}`}
|
|
425
|
+
>
|
|
426
|
+
<div className="font-medium text-sm">{opt.label}</div>
|
|
427
|
+
<div className="text-[10px] opacity-60">
|
|
428
|
+
{opt.description}
|
|
429
|
+
</div>
|
|
430
|
+
</button>
|
|
431
|
+
))}
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
<div>
|
|
435
|
+
<label className="block text-sm font-medium mb-2">
|
|
436
|
+
Signing Secret (optional)
|
|
437
|
+
</label>
|
|
438
|
+
<input
|
|
439
|
+
type="text"
|
|
440
|
+
value={formData.secret}
|
|
441
|
+
onChange={(e) =>
|
|
442
|
+
setFormData({ ...formData, secret: e.target.value })
|
|
443
|
+
}
|
|
444
|
+
placeholder="Your webhook signing secret"
|
|
445
|
+
className="w-full px-4 py-3 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-xl focus:outline-none focus:border-[var(--kyro-primary)]"
|
|
446
|
+
/>
|
|
447
|
+
<p className="text-xs text-[var(--kyro-text-secondary)] mt-1 opacity-60">
|
|
448
|
+
Used to verify that requests came from Kyro CMS.
|
|
449
|
+
</p>
|
|
450
|
+
</div>
|
|
451
|
+
{createError && (
|
|
452
|
+
<p className="text-sm text-red-500">{createError}</p>
|
|
453
|
+
)}
|
|
454
|
+
</div>
|
|
455
|
+
</ModalContent>
|
|
456
|
+
<ModalActions>
|
|
457
|
+
<button type="button"
|
|
458
|
+
onClick={() => setShowCreateModal(false)}
|
|
459
|
+
className="px-4 py-2 rounded-lg font-medium text-sm border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
|
|
460
|
+
>
|
|
461
|
+
Cancel
|
|
462
|
+
</button>
|
|
463
|
+
<button type="button"
|
|
464
|
+
onClick={handleCreate}
|
|
465
|
+
className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90"
|
|
466
|
+
>
|
|
467
|
+
Create Webhook
|
|
468
|
+
</button>
|
|
469
|
+
</ModalActions>
|
|
470
|
+
</Modal>
|
|
471
|
+
|
|
472
|
+
{/* Delete Modal */}
|
|
473
|
+
<Modal
|
|
474
|
+
open={showDeleteModal}
|
|
475
|
+
onClose={() => setShowDeleteModal(false)}
|
|
476
|
+
title="Delete Webhook"
|
|
477
|
+
variant="danger"
|
|
478
|
+
>
|
|
479
|
+
<ModalContent>
|
|
480
|
+
<p className="text-sm text-[var(--kyro-text-secondary)] mb-4">
|
|
481
|
+
Are you sure you want to delete this webhook? This action cannot be
|
|
482
|
+
undone.
|
|
483
|
+
</p>
|
|
484
|
+
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-xl">
|
|
485
|
+
<p className="text-sm font-medium text-red-500">
|
|
486
|
+
This endpoint will stop receiving notifications immediately.
|
|
487
|
+
</p>
|
|
488
|
+
</div>
|
|
489
|
+
</ModalContent>
|
|
490
|
+
<ModalActions>
|
|
491
|
+
<button type="button"
|
|
492
|
+
onClick={() => setShowDeleteModal(false)}
|
|
493
|
+
className="px-4 py-2 rounded-lg font-medium text-sm border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
|
|
494
|
+
>
|
|
495
|
+
Keep Webhook
|
|
496
|
+
</button>
|
|
497
|
+
<button type="button"
|
|
498
|
+
onClick={confirmDelete}
|
|
499
|
+
className="px-4 py-2 rounded-lg font-medium text-sm bg-red-500 text-white hover:bg-red-600"
|
|
500
|
+
>
|
|
501
|
+
Delete Permanently
|
|
502
|
+
</button>
|
|
503
|
+
</ModalActions>
|
|
504
|
+
</Modal>
|
|
505
|
+
|
|
506
|
+
{/* Test Webhook Modal */}
|
|
507
|
+
<Modal
|
|
508
|
+
open={showTestModal}
|
|
509
|
+
onClose={() => setShowTestModal(false)}
|
|
510
|
+
title="Test Webhook"
|
|
511
|
+
>
|
|
512
|
+
<ModalContent>
|
|
513
|
+
{testResult ? (
|
|
514
|
+
<div
|
|
515
|
+
className={`flex items-center gap-3 ${testResult.success ? "text-green-500" : "text-red-500"}`}
|
|
516
|
+
>
|
|
517
|
+
{testResult.success ? (
|
|
518
|
+
<CheckCircle2 className="w-6 h-6" />
|
|
519
|
+
) : (
|
|
520
|
+
<AlertTriangle className="w-6 h-6" />
|
|
521
|
+
)}
|
|
522
|
+
<div>
|
|
523
|
+
<p className="font-bold">
|
|
524
|
+
{testResult.success ? "Success!" : "Failed"}
|
|
525
|
+
</p>
|
|
526
|
+
<p className="text-sm text-[var(--kyro-text-secondary)]">
|
|
527
|
+
{testResult.message}
|
|
528
|
+
</p>
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
) : (
|
|
532
|
+
<div className="flex items-center gap-3">
|
|
533
|
+
<RefreshCw className="w-5 h-5 animate-spin" />
|
|
534
|
+
<p>Sending test request...</p>
|
|
535
|
+
</div>
|
|
536
|
+
)}
|
|
537
|
+
</ModalContent>
|
|
538
|
+
<ModalActions>
|
|
539
|
+
<button type="button"
|
|
540
|
+
onClick={() => setShowTestModal(false)}
|
|
541
|
+
className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90"
|
|
542
|
+
>
|
|
543
|
+
Close
|
|
544
|
+
</button>
|
|
545
|
+
</ModalActions>
|
|
546
|
+
</Modal>
|
|
547
|
+
|
|
548
|
+
{/* Help Modal */}
|
|
549
|
+
<Modal
|
|
550
|
+
open={showHelpModal}
|
|
551
|
+
onClose={() => setShowHelpModal(false)}
|
|
552
|
+
title="How Webhooks Work"
|
|
553
|
+
>
|
|
554
|
+
<ModalContent>
|
|
555
|
+
<div className="space-y-6">
|
|
556
|
+
<div>
|
|
557
|
+
<h4 className="font-bold mb-2">What is a webhook?</h4>
|
|
558
|
+
<p className="text-sm text-[var(--kyro-text-secondary)]">
|
|
559
|
+
A webhook is an HTTP POST request sent to your endpoint when an
|
|
560
|
+
event occurs in your CMS. Unlike API polling (continuously
|
|
561
|
+
checking for changes), webhooks give you instant notifications.
|
|
562
|
+
</p>
|
|
563
|
+
</div>
|
|
564
|
+
<div>
|
|
565
|
+
<h4 className="font-bold mb-2">Request format</h4>
|
|
566
|
+
<p className="text-sm text-[var(--kyro-text-secondary)] mb-3">
|
|
567
|
+
When an event triggers, Kyro sends a POST request with:
|
|
568
|
+
</p>
|
|
569
|
+
<div className="bg-[var(--kyro-bg)] rounded-lg p-4 font-mono text-sm space-y-2">
|
|
570
|
+
<div className="text-[var(--kyro-text-secondary)]">{"{"}</div>
|
|
571
|
+
<div className="pl-4">"event": "create",</div>
|
|
572
|
+
<div className="pl-4">"collection": "posts",</div>
|
|
573
|
+
<div className="pl-4">"documentId": "xxx",</div>
|
|
574
|
+
<div className="pl-4">"timestamp": "2024-..."</div>
|
|
575
|
+
<div className="text-[var(--kyro-text-secondary)]">{"}"}</div>
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
<div>
|
|
579
|
+
<h4 className="font-bold mb-2">Verifying requests</h4>
|
|
580
|
+
<p className="text-sm text-[var(--kyro-text-secondary)]">
|
|
581
|
+
If you provide a signing secret, the request will include an
|
|
582
|
+
X-Kyro-Signature header that you can use to verify the request
|
|
583
|
+
came from Kyro CMS.
|
|
584
|
+
</p>
|
|
585
|
+
</div>
|
|
586
|
+
<div className="bg-amber-500/10 border border-amber-500/20 rounded-xl p-4">
|
|
587
|
+
<div className="flex items-start gap-2">
|
|
588
|
+
<Shield className="w-5 h-5 text-amber-500 flex-shrink-0" />
|
|
589
|
+
<p className="text-sm text-[var(--kyro-text-secondary)]">
|
|
590
|
+
Always verify webhook signatures in production to ensure
|
|
591
|
+
requests are authentic.
|
|
592
|
+
</p>
|
|
593
|
+
</div>
|
|
594
|
+
</div>
|
|
595
|
+
</div>
|
|
596
|
+
</ModalContent>
|
|
597
|
+
<ModalActions>
|
|
598
|
+
<button type="button"
|
|
599
|
+
onClick={() => setShowHelpModal(false)}
|
|
600
|
+
className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90"
|
|
601
|
+
>
|
|
602
|
+
Got it
|
|
603
|
+
</button>
|
|
604
|
+
</ModalActions>
|
|
605
|
+
</Modal>
|
|
606
|
+
</div>
|
|
607
|
+
);
|
|
608
|
+
}
|