@kyro-cms/admin 0.1.8 → 0.2.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/README.md +3 -0
- package/package.json +6 -3
- package/src/components/Admin.tsx +2 -1
- package/src/components/ApiKeysManager.tsx +45 -49
- package/src/components/AuditLogsPage.tsx +9 -4
- package/src/components/BrandingHub.tsx +24 -32
- package/src/components/CreateView.tsx +6 -13
- package/src/components/DetailView.tsx +8 -30
- package/src/components/DeveloperCenter.tsx +31 -27
- package/src/components/EnhancedListView.tsx +5 -6
- package/src/components/ListView.tsx +21 -19
- package/src/components/LoginPage.tsx +3 -17
- package/src/components/MediaGallery.tsx +103 -105
- package/src/components/UserManagement.tsx +28 -18
- package/src/components/WebhookManager.tsx +43 -56
- package/src/components/fields/RelationshipBlockField.tsx +4 -7
- package/src/components/fields/RelationshipField.tsx +4 -10
- package/src/components/fields/UploadField.tsx +37 -44
- package/src/components/ui/CommandPalette.tsx +1 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { apiGet, apiPost, apiDelete } from "@kyro-cms/utils/lib/api";
|
|
2
3
|
import {
|
|
3
4
|
Terminal,
|
|
4
5
|
Key,
|
|
@@ -41,11 +42,8 @@ export function DeveloperCenter({ collections }: { collections: any }) {
|
|
|
41
42
|
|
|
42
43
|
const loadKeys = async () => {
|
|
43
44
|
try {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
const data = await res.json();
|
|
47
|
-
setKeys(data);
|
|
48
|
-
}
|
|
45
|
+
const data = await apiGet("/api/keys");
|
|
46
|
+
setKeys(data);
|
|
49
47
|
} catch (e) {
|
|
50
48
|
console.error(e);
|
|
51
49
|
}
|
|
@@ -63,14 +61,8 @@ export function DeveloperCenter({ collections }: { collections: any }) {
|
|
|
63
61
|
const confirmGenerateKey = async () => {
|
|
64
62
|
if (!newKeyName.trim()) return;
|
|
65
63
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
headers: { "Content-Type": "application/json" },
|
|
69
|
-
body: JSON.stringify({ name: newKeyName }),
|
|
70
|
-
});
|
|
71
|
-
if (res.ok) {
|
|
72
|
-
loadKeys();
|
|
73
|
-
}
|
|
64
|
+
await apiPost("/api/keys", { name: newKeyName });
|
|
65
|
+
loadKeys();
|
|
74
66
|
} catch (e) {
|
|
75
67
|
console.error(e);
|
|
76
68
|
}
|
|
@@ -86,10 +78,8 @@ export function DeveloperCenter({ collections }: { collections: any }) {
|
|
|
86
78
|
const confirmRevokeKey = async () => {
|
|
87
79
|
if (!deleteKeyId) return;
|
|
88
80
|
try {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
loadKeys();
|
|
92
|
-
}
|
|
81
|
+
await apiDelete(`/api/keys/${deleteKeyId}`);
|
|
82
|
+
loadKeys();
|
|
93
83
|
} catch (e) {
|
|
94
84
|
console.error(e);
|
|
95
85
|
}
|
|
@@ -126,7 +116,8 @@ export function DeveloperCenter({ collections }: { collections: any }) {
|
|
|
126
116
|
</p>
|
|
127
117
|
</div>
|
|
128
118
|
<div className="flex items-center gap-3">
|
|
129
|
-
<button
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
130
121
|
onClick={handleGenerateKey}
|
|
131
122
|
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:shadow-[var(--kyro-primary)] active:scale-95 transition-all"
|
|
132
123
|
>
|
|
@@ -167,7 +158,8 @@ export function DeveloperCenter({ collections }: { collections: any }) {
|
|
|
167
158
|
: "••••••••••••••••••••••••••••••••"}
|
|
168
159
|
</code>
|
|
169
160
|
<div className="flex items-center gap-1 shrink-0">
|
|
170
|
-
<button
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
171
163
|
onClick={() =>
|
|
172
164
|
setShowKey(showKey === key.id ? null : key.id)
|
|
173
165
|
}
|
|
@@ -179,7 +171,10 @@ export function DeveloperCenter({ collections }: { collections: any }) {
|
|
|
179
171
|
<Eye className="w-3.5 h-3.5" />
|
|
180
172
|
)}
|
|
181
173
|
</button>
|
|
182
|
-
<button
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
className="p-1.5 hover:bg-[var(--kyro-surface)] rounded-md transition-all text-[var(--kyro-text-secondary)]"
|
|
177
|
+
>
|
|
183
178
|
<Copy className="w-3.5 h-3.5" />
|
|
184
179
|
</button>
|
|
185
180
|
</div>
|
|
@@ -198,7 +193,8 @@ export function DeveloperCenter({ collections }: { collections: any }) {
|
|
|
198
193
|
</div>
|
|
199
194
|
</div>
|
|
200
195
|
<div className="flex items-center gap-3">
|
|
201
|
-
<button
|
|
196
|
+
<button
|
|
197
|
+
type="button"
|
|
202
198
|
onClick={() => handleRevokeKey(key.id)}
|
|
203
199
|
className="p-3 bg-red-500/5 text-red-500 rounded-xl hover:bg-red-500/10 transition-all border border-transparent hover:border-red-500/20"
|
|
204
200
|
title="Revoke Key"
|
|
@@ -224,7 +220,10 @@ export function DeveloperCenter({ collections }: { collections: any }) {
|
|
|
224
220
|
All content is delivered via our optimized REST API. Use your
|
|
225
221
|
keys to authorize requests.
|
|
226
222
|
</p>
|
|
227
|
-
<button
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
className="w-full py-3 bg-white text-[var(--kyro-primary)] rounded-xl font-black text-xs uppercase tracking-widest hover:shadow-2xl transition-all flex items-center justify-center gap-2"
|
|
226
|
+
>
|
|
228
227
|
Review API Docs
|
|
229
228
|
<ExternalLink className="w-3.5 h-3.5" />
|
|
230
229
|
</button>
|
|
@@ -290,7 +289,8 @@ export function DeveloperCenter({ collections }: { collections: any }) {
|
|
|
290
289
|
className="w-full pl-20 pr-4 py-3 bg-[var(--kyro-bg-secondary)] border border-[var(--kyro-border)] rounded-2xl focus:outline-none focus:ring-2 focus:ring-[var(--kyro-primary)] transition-all font-mono text-xs font-bold"
|
|
291
290
|
/>
|
|
292
291
|
</div>
|
|
293
|
-
<button
|
|
292
|
+
<button
|
|
293
|
+
type="button"
|
|
294
294
|
onClick={handleRunTest}
|
|
295
295
|
disabled={exploring || !testEndpoint}
|
|
296
296
|
className="px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-2xl font-black text-xs uppercase tracking-widest shadow-xl disabled:opacity-50 disabled:cursor-not-allowed hover:shadow-[var(--kyro-primary)] transition-all flex items-center gap-2 shrink-0"
|
|
@@ -355,13 +355,15 @@ export function DeveloperCenter({ collections }: { collections: any }) {
|
|
|
355
355
|
/>
|
|
356
356
|
</ModalContent>
|
|
357
357
|
<ModalActions>
|
|
358
|
-
<button
|
|
358
|
+
<button
|
|
359
|
+
type="button"
|
|
359
360
|
onClick={() => setShowCreateModal(false)}
|
|
360
361
|
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)] hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
361
362
|
>
|
|
362
363
|
Cancel
|
|
363
364
|
</button>
|
|
364
|
-
<button
|
|
365
|
+
<button
|
|
366
|
+
type="button"
|
|
365
367
|
onClick={confirmGenerateKey}
|
|
366
368
|
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 transition-colors"
|
|
367
369
|
>
|
|
@@ -384,13 +386,15 @@ export function DeveloperCenter({ collections }: { collections: any }) {
|
|
|
384
386
|
</p>
|
|
385
387
|
</ModalContent>
|
|
386
388
|
<ModalActions>
|
|
387
|
-
<button
|
|
389
|
+
<button
|
|
390
|
+
type="button"
|
|
388
391
|
onClick={() => setShowDeleteModal(false)}
|
|
389
392
|
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)] hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
390
393
|
>
|
|
391
394
|
Cancel
|
|
392
395
|
</button>
|
|
393
|
-
<button
|
|
396
|
+
<button
|
|
397
|
+
type="button"
|
|
394
398
|
onClick={confirmRevokeKey}
|
|
395
399
|
className="px-4 py-2 rounded-lg font-medium text-sm bg-red-500 text-white hover:bg-red-600 transition-colors"
|
|
396
400
|
>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect, useMemo, useCallback } from "react";
|
|
2
2
|
import { Spinner } from "./ui/Spinner";
|
|
3
3
|
import { ConfirmModal } from "./ui/Modal";
|
|
4
|
+
import { apiGet, apiDelete, withCacheBust } from "@kyro-cms/utils/lib/api";
|
|
4
5
|
|
|
5
6
|
export interface FieldConfig {
|
|
6
7
|
name: string;
|
|
@@ -217,11 +218,9 @@ export function EnhancedListView({
|
|
|
217
218
|
params.append("filters", JSON.stringify(filters));
|
|
218
219
|
}
|
|
219
220
|
|
|
220
|
-
const
|
|
221
|
-
`/api/${collectionSlug}?${params}
|
|
221
|
+
const result = await apiGet(
|
|
222
|
+
withCacheBust(`/api/${collectionSlug}?${params}`),
|
|
222
223
|
);
|
|
223
|
-
if (!response.ok) throw new Error("Failed to load");
|
|
224
|
-
const result = await response.json();
|
|
225
224
|
setDocs(result.docs || []);
|
|
226
225
|
setTotalDocs(result.totalDocs || 0);
|
|
227
226
|
} catch (error) {
|
|
@@ -261,7 +260,7 @@ export function EnhancedListView({
|
|
|
261
260
|
try {
|
|
262
261
|
await Promise.all(
|
|
263
262
|
Array.from(selectedIds).map((id) =>
|
|
264
|
-
|
|
263
|
+
apiDelete(`/api/${collectionSlug}/${id}`),
|
|
265
264
|
),
|
|
266
265
|
);
|
|
267
266
|
setSelectedIds(new Set());
|
|
@@ -852,7 +851,7 @@ export function EnhancedListView({
|
|
|
852
851
|
const confirmDelete = async () => {
|
|
853
852
|
try {
|
|
854
853
|
for (const id of deleteConfirm.ids || []) {
|
|
855
|
-
await
|
|
854
|
+
await apiDelete(`/api/${collectionSlug}/${id}`);
|
|
856
855
|
}
|
|
857
856
|
fetchDocs();
|
|
858
857
|
} catch (error) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect } from "react";
|
|
2
|
+
import { apiGet, apiDelete } from "@kyro-cms/utils/lib/api";
|
|
2
3
|
import type { CollectionConfig, KyroConfig } from "@kyro-cms/core/client";
|
|
3
4
|
import { Spinner } from "./ui/Spinner";
|
|
4
5
|
import { ConfirmModal } from "./ui/Modal";
|
|
@@ -35,11 +36,9 @@ export function ListView({
|
|
|
35
36
|
const loadDocs = async () => {
|
|
36
37
|
try {
|
|
37
38
|
setLoading(true);
|
|
38
|
-
const
|
|
39
|
+
const result = await apiGet(
|
|
39
40
|
`/api/${collection.slug}?page=${page}&limit=${limit}`,
|
|
40
41
|
);
|
|
41
|
-
if (!response.ok) throw new Error("Failed to load");
|
|
42
|
-
const result = await response.json();
|
|
43
42
|
setDocs(result.docs || []);
|
|
44
43
|
setTotalPages(result.totalPages || 1);
|
|
45
44
|
} catch (error) {
|
|
@@ -62,12 +61,8 @@ export function ListView({
|
|
|
62
61
|
const confirmDelete = async () => {
|
|
63
62
|
if (!deleteId) return;
|
|
64
63
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
});
|
|
68
|
-
if (response.ok) {
|
|
69
|
-
setDocs((prev) => prev.filter((d) => d.id !== deleteId));
|
|
70
|
-
}
|
|
64
|
+
await apiDelete(`/api/${collection.slug}/${deleteId}`);
|
|
65
|
+
setDocs((prev) => prev.filter((d) => d.id !== deleteId));
|
|
71
66
|
} catch (error) {
|
|
72
67
|
console.error("Failed to delete:", error);
|
|
73
68
|
}
|
|
@@ -101,7 +96,8 @@ export function ListView({
|
|
|
101
96
|
className="w-full pl-11 pr-4 py-2.5 bg-[var(--kyro-bg-secondary)] border border-[var(--kyro-border)] rounded-xl focus:outline-none focus:ring-2 focus:ring-[var(--kyro-primary)] focus:border-[var(--kyro-primary)] transition-all text-sm font-medium"
|
|
102
97
|
/>
|
|
103
98
|
</div>
|
|
104
|
-
<button
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
105
101
|
className="flex items-center gap-2 px-6 py-2.5 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-black text-xs shadow-lg active:scale-95 transition-all"
|
|
106
102
|
onClick={onCreate}
|
|
107
103
|
>
|
|
@@ -134,7 +130,8 @@ export function ListView({
|
|
|
134
130
|
<p className="kyro-empty-text">
|
|
135
131
|
Get started by creating your first one.
|
|
136
132
|
</p>
|
|
137
|
-
<button
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
138
135
|
className="kyro-btn kyro-btn-primary kyro-btn-md"
|
|
139
136
|
style={{ marginTop: 16 }}
|
|
140
137
|
onClick={onCreate}
|
|
@@ -228,14 +225,16 @@ export function ListView({
|
|
|
228
225
|
onClick={(e) => e.stopPropagation()}
|
|
229
226
|
>
|
|
230
227
|
<div className="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
231
|
-
<button
|
|
228
|
+
<button
|
|
229
|
+
type="button"
|
|
232
230
|
className="p-2 text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-bg-secondary)] rounded-lg transition-all"
|
|
233
231
|
onClick={() => onEdit(doc.id)}
|
|
234
232
|
title="Edit"
|
|
235
233
|
>
|
|
236
234
|
<Settings className="w-4 h-4" />
|
|
237
235
|
</button>
|
|
238
|
-
<button
|
|
236
|
+
<button
|
|
237
|
+
type="button"
|
|
239
238
|
className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg transition-all"
|
|
240
239
|
onClick={() => handleDelete(doc.id)}
|
|
241
240
|
title="Delete"
|
|
@@ -270,7 +269,8 @@ export function ListView({
|
|
|
270
269
|
<p className="text-xs font-black uppercase tracking-widest">
|
|
271
270
|
Docs Selected
|
|
272
271
|
</p>
|
|
273
|
-
<button
|
|
272
|
+
<button
|
|
273
|
+
type="button"
|
|
274
274
|
onClick={() => setSelectedIds(new Set())}
|
|
275
275
|
className="text-[10px] font-bold text-[var(--kyro-primary)] hover:underline"
|
|
276
276
|
>
|
|
@@ -279,10 +279,14 @@ export function ListView({
|
|
|
279
279
|
</div>
|
|
280
280
|
</div>
|
|
281
281
|
<div className="flex items-center gap-3 pr-2">
|
|
282
|
-
<button
|
|
282
|
+
<button
|
|
283
|
+
type="button"
|
|
284
|
+
className="flex items-center gap-2 px-6 py-2.5 bg-green-500 text-white rounded-xl font-black text-[10px] uppercase tracking-widest shadow-lg hover:shadow-green-500/20 active:scale-95 transition-all"
|
|
285
|
+
>
|
|
283
286
|
Publish
|
|
284
287
|
</button>
|
|
285
|
-
<button
|
|
288
|
+
<button
|
|
289
|
+
type="button"
|
|
286
290
|
onClick={async () => {
|
|
287
291
|
if (
|
|
288
292
|
window.confirm(
|
|
@@ -290,9 +294,7 @@ export function ListView({
|
|
|
290
294
|
)
|
|
291
295
|
) {
|
|
292
296
|
for (const id of Array.from(selectedIds)) {
|
|
293
|
-
await
|
|
294
|
-
method: "DELETE",
|
|
295
|
-
});
|
|
297
|
+
await apiDelete(`/api/${collection.slug}/${id}`);
|
|
296
298
|
}
|
|
297
299
|
loadDocs();
|
|
298
300
|
setSelectedIds(new Set());
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect } from "react";
|
|
2
|
+
import { apiGet, apiPost } from "@kyro-cms/utils/lib/api";
|
|
2
3
|
import { ThemeProvider, type ThemeMode } from "./ThemeProvider";
|
|
3
4
|
import { Toast, ToastProvider } from "./ui/Toast";
|
|
4
5
|
|
|
@@ -30,11 +31,7 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
|
|
|
30
31
|
|
|
31
32
|
const checkIfFirstUser = async () => {
|
|
32
33
|
try {
|
|
33
|
-
|
|
34
|
-
if (res.status === 401 || res.status === 404) {
|
|
35
|
-
setIsFirstUser(true);
|
|
36
|
-
setMode("register");
|
|
37
|
-
}
|
|
34
|
+
await apiGet("/api/auth/users");
|
|
38
35
|
} catch {
|
|
39
36
|
setIsFirstUser(true);
|
|
40
37
|
setMode("register");
|
|
@@ -61,18 +58,7 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
|
|
|
61
58
|
body.confirmPassword = confirmPassword;
|
|
62
59
|
}
|
|
63
60
|
|
|
64
|
-
const
|
|
65
|
-
method: "POST",
|
|
66
|
-
headers: { "Content-Type": "application/json" },
|
|
67
|
-
body: JSON.stringify(body),
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const data = await res.json();
|
|
71
|
-
|
|
72
|
-
if (!res.ok) {
|
|
73
|
-
addToast("error", data.error || "Something went wrong");
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
61
|
+
const data = await apiPost(endpoint, body);
|
|
76
62
|
|
|
77
63
|
if (data.isFirstUser) {
|
|
78
64
|
setIsFirstUser(true);
|