@kyro-cms/admin 0.12.3 → 0.12.5
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 +64 -26
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +64 -26
- package/package.json +8 -2
- package/src/components/ActionBar.tsx +16 -16
- package/src/components/ApiHealth.tsx +522 -0
- package/src/components/ApiKeysManager.tsx +6 -4
- package/src/components/AuditLogsPage.tsx +4 -2
- package/src/components/AutoForm.tsx +10 -7
- package/src/components/BrandingHub.tsx +3 -1
- package/src/components/Dashboard.tsx +44 -37
- package/src/components/DashboardMetrics.tsx +168 -58
- package/src/components/DetailView.tsx +125 -50
- package/src/components/DeveloperCenter.tsx +5 -3
- package/src/components/GraphQLPlayground.tsx +10 -8
- package/src/components/ListView.tsx +66 -45
- package/src/components/LoginPage.tsx +21 -19
- package/src/components/MarketplaceManager.tsx +3 -1
- package/src/components/MediaGallery.tsx +12 -9
- package/src/components/PluginsManager.tsx +6 -4
- package/src/components/RestPlayground.tsx +8 -6
- package/src/components/SessionsManager.tsx +3 -1
- package/src/components/Sidebar.astro +180 -92
- package/src/components/UserManagement.tsx +8 -6
- package/src/components/UserMenu.tsx +19 -17
- package/src/components/VersionHistoryPanel.tsx +6 -4
- package/src/components/WebhookManager.tsx +143 -15
- package/src/components/autoform/AutoFormApiView.tsx +3 -1
- package/src/components/autoform/AutoFormEditView.tsx +5 -3
- package/src/components/autoform/AutoFormHeader.tsx +6 -6
- package/src/components/blocks/AccordionBlock.tsx +5 -3
- package/src/components/blocks/ArrayBlock.tsx +5 -3
- package/src/components/blocks/BlockWrapper.tsx +5 -3
- package/src/components/blocks/CodeBlock.tsx +4 -2
- package/src/components/blocks/FileBlock.tsx +5 -3
- package/src/components/blocks/HeroBlock.tsx +5 -3
- package/src/components/blocks/ImageBlock.tsx +6 -4
- package/src/components/blocks/ListBlock.tsx +5 -3
- package/src/components/blocks/ParagraphBlock.tsx +3 -1
- package/src/components/blocks/RelationshipBlock.tsx +5 -3
- package/src/components/blocks/RichTextBlock.tsx +5 -3
- package/src/components/blocks/VideoBlock.tsx +5 -3
- package/src/components/fields/AccordionField.tsx +10 -51
- package/src/components/fields/ArrayField.tsx +9 -7
- package/src/components/fields/ArrayLayout.tsx +3 -1
- package/src/components/fields/BlocksField.tsx +24 -75
- package/src/components/fields/ButtonField.tsx +3 -1
- package/src/components/fields/CardField.tsx +6 -4
- package/src/components/fields/GroupLayout.tsx +2 -49
- package/src/components/fields/HeadingField.tsx +3 -1
- package/src/components/fields/HeadingSubheadingField.tsx +4 -2
- package/src/components/fields/HeroField.tsx +8 -6
- package/src/components/fields/JSONField.tsx +9 -10
- package/src/components/fields/LinkField.tsx +4 -2
- package/src/components/fields/ListField.tsx +3 -1
- package/src/components/fields/MarkdownField.tsx +3 -17
- package/src/components/fields/RelationshipBlockField.tsx +7 -5
- package/src/components/fields/RelationshipField.tsx +3 -1
- package/src/components/fields/RichTextField.tsx +19 -16
- package/src/components/fields/SecretField.tsx +9 -69
- package/src/components/fields/SelectField.tsx +11 -6
- package/src/components/fields/TabsLayout.tsx +2 -48
- package/src/components/fields/TextField.tsx +9 -7
- package/src/components/fields/UploadField.tsx +3 -1
- package/src/components/fields/VideoField.tsx +6 -4
- package/src/components/ui/BlockDrawer.tsx +3 -1
- package/src/components/ui/CommandPalette.tsx +9 -7
- package/src/components/ui/CommandPaletteWrapper.tsx +3 -1
- package/src/components/ui/IconPickerModal.tsx +4 -2
- package/src/components/ui/PageHeader.tsx +48 -20
- package/src/components/users/UserDetail.tsx +3 -1
- package/src/components/users/UserForm.tsx +5 -3
- package/src/fields/examples/sample-field-2.tsx +3 -1
- package/src/fields/examples/sample-field.tsx +3 -1
- package/src/hooks/useAutoFormState.ts +19 -11
- package/src/integration.ts +21 -0
- package/src/layouts/AdminLayout.astro +252 -189
- package/src/layouts/AuthLayout.astro +52 -31
- package/src/lib/api.ts +7 -7
- package/src/lib/autoform-store.ts +4 -0
- package/src/lib/deep-equal.ts +3 -3
- package/src/lib/i18n.ts +44 -0
- package/src/lib/vite-shim-plugin.ts +3 -0
- package/src/locales/de.json +163 -0
- package/src/locales/en.json +163 -0
- package/src/locales/es.json +163 -0
- package/src/locales/fr.json +163 -0
- package/src/locales/pt.json +163 -0
- package/src/pages/403.astro +2 -1
- package/src/pages/[collection]/[id].astro +23 -10
- package/src/pages/[collection]/index.astro +48 -34
- package/src/pages/audit/index.astro +2 -1
- package/src/pages/graphql-explorer.astro +2 -1
- package/src/pages/graphql.astro +2 -1
- package/src/pages/health.astro +9 -0
- package/src/pages/index.astro +6 -5
- package/src/pages/keys.astro +2 -1
- package/src/pages/marketplace.astro +2 -1
- package/src/pages/media.astro +2 -1
- package/src/pages/plugins.astro +2 -1
- package/src/pages/rest-playground.astro +2 -1
- package/src/pages/roles/index.astro +16 -52
- package/src/pages/sessions.astro +2 -1
- package/src/pages/settings/[slug].astro +45 -30
- package/src/pages/users/[id].astro +1 -0
- package/src/pages/users/index.astro +2 -1
- package/src/pages/users/new.astro +2 -1
- package/src/pages/webhooks.astro +2 -1
- package/src/pages/index_ALT.astro +0 -3
|
@@ -4,6 +4,7 @@ import { Shield, Monitor, Trash2, Clock, AlertTriangle, Info, LogOut, Globe, Act
|
|
|
4
4
|
import { PageHeader } from "./ui/PageHeader";
|
|
5
5
|
import { toast } from "../lib/stores";
|
|
6
6
|
import { Badge } from "./ui/Badge";
|
|
7
|
+
import { useTranslation } from "react-i18next";
|
|
7
8
|
|
|
8
9
|
interface Session {
|
|
9
10
|
id: string;
|
|
@@ -33,6 +34,7 @@ function timeAgo(ts: number): string {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export function SessionsManager() {
|
|
37
|
+
const { t } = useTranslation();
|
|
36
38
|
const [sessions, setSessions] = useState<Session[]>([]);
|
|
37
39
|
const [loading, setLoading] = useState(true);
|
|
38
40
|
const [error, setError] = useState("");
|
|
@@ -82,7 +84,7 @@ export function SessionsManager() {
|
|
|
82
84
|
return (
|
|
83
85
|
<div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-32">
|
|
84
86
|
<PageHeader
|
|
85
|
-
title="Active Sessions"
|
|
87
|
+
title={t("tooltips.activeSessions", { defaultValue: "Active Sessions" })}
|
|
86
88
|
description="Monitor and manage your cryptographic access across all devices."
|
|
87
89
|
icon={Monitor}
|
|
88
90
|
actions={otherCount > 0 ? [
|
|
@@ -21,6 +21,9 @@ const darkLogo = brandSettings?.identity?.darkLogo;
|
|
|
21
21
|
const logoWidth = siteLogo?.width || darkLogo?.width;
|
|
22
22
|
const logoHeight = siteLogo?.height || darkLogo?.height;
|
|
23
23
|
const logoAlt = siteLogo?.altText || darkLogo?.altText || siteName;
|
|
24
|
+
const showSiteName = brandSettings?.identity?.showSiteName ?? true;
|
|
25
|
+
|
|
26
|
+
import i18next from "../lib/i18n";
|
|
24
27
|
|
|
25
28
|
interface Props {
|
|
26
29
|
title: string;
|
|
@@ -29,38 +32,59 @@ interface Props {
|
|
|
29
32
|
const { title } = Astro.props;
|
|
30
33
|
const currentPath = Astro.url.pathname;
|
|
31
34
|
|
|
32
|
-
const
|
|
35
|
+
const groupedCollections = new Map<string, NavItem[]>();
|
|
36
|
+
const defaultCollections: NavItem[] = [];
|
|
37
|
+
|
|
38
|
+
nonAuthCollections
|
|
33
39
|
.filter((col) => col.slug !== "media") //removed here cos it's been added manually in the navSections.
|
|
34
|
-
.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
.forEach((col) => {
|
|
41
|
+
const item: NavItem = {
|
|
42
|
+
href: `${adminPath}/${col.slug}`,
|
|
43
|
+
label: col.label || col.slug,
|
|
44
|
+
icon: (col.admin?.icon as string) || "collection",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (col.admin?.group) {
|
|
48
|
+
const groupName = col.admin.group as string;
|
|
49
|
+
if (!groupedCollections.has(groupName)) {
|
|
50
|
+
groupedCollections.set(groupName, []);
|
|
51
|
+
}
|
|
52
|
+
groupedCollections.get(groupName)!.push(item);
|
|
53
|
+
} else {
|
|
54
|
+
defaultCollections.push(item);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const dynamicSections = Array.from(groupedCollections.entries()).map(([group, items]) => ({
|
|
59
|
+
label: group,
|
|
60
|
+
items: items,
|
|
61
|
+
}));
|
|
39
62
|
|
|
40
63
|
const navSections: { label: string; items: NavItem[] }[] = [
|
|
41
64
|
{
|
|
42
|
-
label: "
|
|
65
|
+
label: "Dashboard",
|
|
43
66
|
items: [
|
|
44
|
-
{ href: adminPath, label: "
|
|
67
|
+
{ href: adminPath, label: "nav.dashboard", icon: "home" },
|
|
45
68
|
{
|
|
46
69
|
href: adminPath + "/media",
|
|
47
|
-
label: "
|
|
70
|
+
label: "nav.media",
|
|
48
71
|
icon: "media",
|
|
49
72
|
},
|
|
50
73
|
],
|
|
51
74
|
},
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
...dynamicSections,
|
|
76
|
+
...(defaultCollections.length > 0 ? [{
|
|
77
|
+
label: "Content",
|
|
78
|
+
items: defaultCollections,
|
|
79
|
+
}] : []),
|
|
56
80
|
{
|
|
57
81
|
label: "Settings",
|
|
58
82
|
items: [
|
|
59
|
-
{ href: adminPath + "/users", label: "
|
|
60
|
-
{ href: adminPath + "/plugins", label: "
|
|
83
|
+
{ href: adminPath + "/users", label: "nav.users", icon: "users" },
|
|
84
|
+
{ href: adminPath + "/plugins", label: "nav.plugins", icon: "plugins" },
|
|
61
85
|
{
|
|
62
86
|
href: adminPath + "/settings",
|
|
63
|
-
label: "
|
|
87
|
+
label: "nav.settings",
|
|
64
88
|
icon: "settings",
|
|
65
89
|
},
|
|
66
90
|
],
|
|
@@ -101,36 +125,47 @@ function isActive(item: NavItem): boolean {
|
|
|
101
125
|
<div class="px-6 md:px-8 py-6 md:py-8 flex items-center justify-between gap-3">
|
|
102
126
|
{
|
|
103
127
|
siteLogo || darkLogo ? (
|
|
104
|
-
<div class="flex items-center">
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
<div class="flex items-center gap-3">
|
|
129
|
+
<div class="flex items-center">
|
|
130
|
+
{siteLogo && (
|
|
131
|
+
<img
|
|
132
|
+
src={siteLogo.url}
|
|
133
|
+
alt={logoAlt}
|
|
134
|
+
style={{
|
|
135
|
+
width: logoWidth ? `${logoWidth}px` : "auto",
|
|
136
|
+
height: logoHeight ? `${logoHeight}px` : "32px",
|
|
137
|
+
objectFit: "contain",
|
|
138
|
+
}}
|
|
139
|
+
class={`rounded-lg ${darkLogo ? "block dark:hidden" : ""}`}
|
|
140
|
+
/>
|
|
141
|
+
)}
|
|
142
|
+
{darkLogo && (
|
|
143
|
+
<img
|
|
144
|
+
src={darkLogo.url}
|
|
145
|
+
alt={logoAlt}
|
|
146
|
+
style={{
|
|
147
|
+
width: logoWidth ? `${logoWidth}px` : "auto",
|
|
148
|
+
height: logoHeight ? `${logoHeight}px` : "32px",
|
|
149
|
+
objectFit: "contain",
|
|
150
|
+
}}
|
|
151
|
+
class={`rounded-lg ${siteLogo ? "hidden dark:block" : ""}`}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
{showSiteName && (
|
|
156
|
+
<span class="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)]">
|
|
157
|
+
{siteName}
|
|
158
|
+
</span>
|
|
128
159
|
)}
|
|
129
160
|
</div>
|
|
130
161
|
) : (
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
162
|
+
<div class="flex items-center gap-3">
|
|
163
|
+
<img src="/logo-white.svg" alt="Kyro CMS" class="w-7 h-7 hidden dark:block" />
|
|
164
|
+
<img src="/logo.svg" alt="Kyro CMS" class="w-7 h-7 block dark:hidden" />
|
|
165
|
+
<span class="text-2xl font-black tracking-tighter text-[var(--kyro-text-primary)]">
|
|
166
|
+
{siteName}
|
|
167
|
+
</span>
|
|
168
|
+
</div>
|
|
134
169
|
)
|
|
135
170
|
}
|
|
136
171
|
<button id="mobile-close-btn" class="md:hidden p-2 -mr-2 rounded-lg text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] transition-colors">
|
|
@@ -142,43 +177,54 @@ function isActive(item: NavItem): boolean {
|
|
|
142
177
|
<div class="space-y-4">
|
|
143
178
|
{
|
|
144
179
|
navSections.map((section) => (
|
|
145
|
-
<div class="space-y-2" data-section={section.label}>
|
|
180
|
+
<div class="space-y-2 group/section" data-section={section.label}>
|
|
146
181
|
<div class="pt-4 pb-2">
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
182
|
+
<button type="button" class="w-full flex items-center justify-between px-6 focus:outline-none sidebar-section-toggle transition-opacity hover:opacity-80">
|
|
183
|
+
<p class="text-[10px] font-medium text-[var(--kyro-text-secondary)] tracking-[0.2em] opacity-40 uppercase">
|
|
184
|
+
{i18next.t(section.label, { defaultValue: section.label.split('.').pop()?.toUpperCase() || section.label }) || (section.label.split('.').pop()?.toUpperCase() || section.label)}
|
|
185
|
+
</p>
|
|
186
|
+
<Icons.ChevronDown className="w-3.5 h-3.5 text-[var(--kyro-text-secondary)] opacity-40 transition-transform duration-200 group-data-[collapsed=true]/section:-rotate-90" />
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
<div class="section-items space-y-1">
|
|
190
|
+
{section.items.map((item) => {
|
|
191
|
+
const slug = item.href.split("/").pop();
|
|
192
|
+
const type = item.href.includes("/settings")
|
|
193
|
+
? "global"
|
|
194
|
+
: "collection";
|
|
195
|
+
return (
|
|
196
|
+
<a
|
|
197
|
+
href={item.href}
|
|
198
|
+
data-nav-item
|
|
199
|
+
data-slug={slug}
|
|
200
|
+
data-type={type}
|
|
201
|
+
class={`flex items-center gap-4 px-6 py-2 rounded-2xl transition-all font-medium text-[13px] ${
|
|
202
|
+
item.icon === "collection"
|
|
203
|
+
? currentPath === item.href ||
|
|
204
|
+
currentPath.startsWith(item.href + "/")
|
|
205
|
+
? "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-primary)] active-nav-item"
|
|
206
|
+
: "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"
|
|
207
|
+
: isActive(item)
|
|
208
|
+
? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-lg active-nav-item"
|
|
209
|
+
: "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"
|
|
210
|
+
}`}
|
|
211
|
+
>
|
|
212
|
+
{(() => {
|
|
213
|
+
const iconName = iconAliases[item.icon] || item.icon;
|
|
214
|
+
const Icon = (Icons as any)[iconName] || Icons.Dot;
|
|
215
|
+
return <Icon className="w-4 h-4" strokeWidth={2.5} />;
|
|
216
|
+
})()}
|
|
217
|
+
<span>
|
|
218
|
+
{(() => {
|
|
219
|
+
const fallback = item.label.split('.').pop() || item.label;
|
|
220
|
+
const capitalized = fallback.charAt(0).toUpperCase() + fallback.slice(1);
|
|
221
|
+
return i18next.t(item.label, { defaultValue: capitalized }) || capitalized;
|
|
222
|
+
})()}
|
|
223
|
+
</span>
|
|
224
|
+
</a>
|
|
225
|
+
);
|
|
226
|
+
})}
|
|
150
227
|
</div>
|
|
151
|
-
{section.items.map((item) => {
|
|
152
|
-
const slug = item.href.split("/").pop();
|
|
153
|
-
const type = item.href.includes("/settings")
|
|
154
|
-
? "global"
|
|
155
|
-
: "collection";
|
|
156
|
-
return (
|
|
157
|
-
<a
|
|
158
|
-
href={item.href}
|
|
159
|
-
data-nav-item
|
|
160
|
-
data-slug={slug}
|
|
161
|
-
data-type={type}
|
|
162
|
-
class={`flex items-center gap-4 px-6 py-2 rounded-2xl transition-all font-medium text-[13px] ${
|
|
163
|
-
item.icon === "collection"
|
|
164
|
-
? currentPath === item.href ||
|
|
165
|
-
currentPath.startsWith(item.href + "/")
|
|
166
|
-
? "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-primary)]"
|
|
167
|
-
: "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"
|
|
168
|
-
: isActive(item)
|
|
169
|
-
? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-lg"
|
|
170
|
-
: "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"
|
|
171
|
-
}`}
|
|
172
|
-
>
|
|
173
|
-
{(() => {
|
|
174
|
-
const iconName = iconAliases[item.icon] || item.icon;
|
|
175
|
-
const Icon = (Icons as any)[iconName] || Icons.Dot;
|
|
176
|
-
return <Icon className="w-4 h-4" strokeWidth={2.5} />;
|
|
177
|
-
})()}
|
|
178
|
-
<span>{item.label}</span>
|
|
179
|
-
</a>
|
|
180
|
-
);
|
|
181
|
-
})}
|
|
182
228
|
</div>
|
|
183
229
|
))
|
|
184
230
|
}
|
|
@@ -186,42 +232,84 @@ function isActive(item: NavItem): boolean {
|
|
|
186
232
|
</nav>
|
|
187
233
|
|
|
188
234
|
<script>
|
|
235
|
+
function initSidebar() {
|
|
236
|
+
const sections = document.querySelectorAll("[data-section]");
|
|
237
|
+
sections.forEach((section) => {
|
|
238
|
+
const label = section.getAttribute("data-section");
|
|
239
|
+
const toggleBtn = section.querySelector('.sidebar-section-toggle');
|
|
240
|
+
const itemsContainer = section.querySelector('.section-items');
|
|
241
|
+
|
|
242
|
+
const isCollapsed = localStorage.getItem(`kyro-sidebar-collapsed-${label}`) === 'true';
|
|
243
|
+
if (isCollapsed) {
|
|
244
|
+
section.setAttribute('data-collapsed', 'true');
|
|
245
|
+
if (itemsContainer) itemsContainer.classList.add('hidden');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const activeItem = section.querySelector('.active-nav-item');
|
|
249
|
+
if (activeItem && isCollapsed) {
|
|
250
|
+
section.removeAttribute('data-collapsed');
|
|
251
|
+
if (itemsContainer) itemsContainer.classList.remove('hidden');
|
|
252
|
+
localStorage.setItem(`kyro-sidebar-collapsed-${label}`, 'false');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Remove old listeners to prevent duplicates
|
|
256
|
+
const newToggleBtn = toggleBtn?.cloneNode(true);
|
|
257
|
+
if (toggleBtn && newToggleBtn && toggleBtn.parentNode) {
|
|
258
|
+
toggleBtn.parentNode.replaceChild(newToggleBtn, toggleBtn);
|
|
259
|
+
newToggleBtn.addEventListener('click', () => {
|
|
260
|
+
const currentlyCollapsed = section.getAttribute('data-collapsed') === 'true';
|
|
261
|
+
if (currentlyCollapsed) {
|
|
262
|
+
section.removeAttribute('data-collapsed');
|
|
263
|
+
if (itemsContainer) itemsContainer.classList.remove('hidden');
|
|
264
|
+
localStorage.setItem(`kyro-sidebar-collapsed-${label}`, 'false');
|
|
265
|
+
} else {
|
|
266
|
+
section.setAttribute('data-collapsed', 'true');
|
|
267
|
+
if (itemsContainer) itemsContainer.classList.add('hidden');
|
|
268
|
+
localStorage.setItem(`kyro-sidebar-collapsed-${label}`, 'true');
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Run on load and view transitions
|
|
276
|
+
initSidebar();
|
|
277
|
+
document.addEventListener('astro:page-load', initSidebar);
|
|
278
|
+
|
|
189
279
|
// Handle frontend RBAC pruning in the sidebar
|
|
190
|
-
window.addEventListener("kyro:auth-ready", (event
|
|
280
|
+
window.addEventListener("kyro:auth-ready", (event) => {
|
|
281
|
+
// @ts-ignore
|
|
191
282
|
const { permissions } = event.detail;
|
|
192
283
|
if (!permissions) return;
|
|
193
284
|
|
|
194
285
|
const navItems = document.querySelectorAll("[data-nav-item]");
|
|
195
|
-
navItems.forEach((item
|
|
286
|
+
navItems.forEach((item) => {
|
|
196
287
|
const slug = item.getAttribute("data-slug");
|
|
197
288
|
const type = item.getAttribute("data-type");
|
|
198
289
|
|
|
199
290
|
let hasAccess = true;
|
|
200
|
-
|
|
201
291
|
if (type === "collection" && permissions.collections) {
|
|
202
292
|
const colPerms = permissions.collections[slug];
|
|
203
|
-
if (colPerms && colPerms.read === false)
|
|
204
|
-
hasAccess = false;
|
|
205
|
-
}
|
|
293
|
+
if (colPerms && colPerms.read === false) hasAccess = false;
|
|
206
294
|
} else if (type === "global" && permissions.globals) {
|
|
207
295
|
const globalPerms = permissions.globals[slug];
|
|
208
|
-
if (globalPerms && globalPerms.read === false)
|
|
209
|
-
hasAccess = false;
|
|
210
|
-
}
|
|
296
|
+
if (globalPerms && globalPerms.read === false) hasAccess = false;
|
|
211
297
|
}
|
|
212
298
|
|
|
213
299
|
if (!hasAccess) {
|
|
300
|
+
// @ts-ignore
|
|
214
301
|
item.style.display = "none";
|
|
215
302
|
}
|
|
216
303
|
});
|
|
217
304
|
|
|
218
|
-
// Hide empty sections
|
|
305
|
+
// Hide empty sections after RBAC
|
|
219
306
|
const sections = document.querySelectorAll("[data-section]");
|
|
220
|
-
sections.forEach((section
|
|
307
|
+
sections.forEach((section) => {
|
|
221
308
|
const visibleItems = section.querySelectorAll(
|
|
222
|
-
'[data-nav-item]:not([style*="display: none"])'
|
|
309
|
+
'[data-nav-item]:not([style*="display: none"])'
|
|
223
310
|
);
|
|
224
311
|
if (visibleItems.length === 0) {
|
|
312
|
+
// @ts-ignore
|
|
225
313
|
section.style.display = "none";
|
|
226
314
|
}
|
|
227
315
|
});
|
|
@@ -238,14 +326,14 @@ function isActive(item: NavItem): boolean {
|
|
|
238
326
|
<button
|
|
239
327
|
id="theme-light-btn"
|
|
240
328
|
class="p-2 rounded-lg transition-all active:scale-95"
|
|
241
|
-
title="Light Mode"
|
|
329
|
+
title={i18next.t("sidebar.lightMode", { defaultValue: "Light Mode" })}
|
|
242
330
|
>
|
|
243
331
|
<Icons.Sun className="w-4 h-4" strokeWidth={2.5} />
|
|
244
332
|
</button>
|
|
245
333
|
<button
|
|
246
334
|
id="theme-dark-btn"
|
|
247
335
|
class="p-2 rounded-lg transition-all active:scale-95"
|
|
248
|
-
title="Dark Mode"
|
|
336
|
+
title={i18next.t("sidebar.darkMode", { defaultValue: "Dark Mode" })}
|
|
249
337
|
>
|
|
250
338
|
<Icons.Moon className="w-4 h-4" strokeWidth={2.5} />
|
|
251
339
|
</button>
|
|
@@ -15,6 +15,7 @@ import { useUIStore, toast } from "../lib/stores";
|
|
|
15
15
|
import { Modal, ModalContent, ModalActions } from "./ui/Modal";
|
|
16
16
|
import { Badge } from "./ui/Badge";
|
|
17
17
|
import { PageHeader } from "./ui/PageHeader";
|
|
18
|
+
import { useTranslation } from "react-i18next";
|
|
18
19
|
|
|
19
20
|
interface User {
|
|
20
21
|
id: string;
|
|
@@ -29,6 +30,7 @@ interface User {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export function UserManagement() {
|
|
33
|
+
const { t } = useTranslation();
|
|
32
34
|
const [users, setUsers] = useState<User[]>([]);
|
|
33
35
|
const [loading, setLoading] = useState(true);
|
|
34
36
|
const [searchQuery, setSearchQuery] = useState("");
|
|
@@ -135,7 +137,7 @@ export function UserManagement() {
|
|
|
135
137
|
<div className="w-full space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-700 px-4 md:px-8 pb-12">
|
|
136
138
|
{/* Header */}
|
|
137
139
|
<PageHeader
|
|
138
|
-
title="Identity & Access"
|
|
140
|
+
title={t("tooltips.identityAccess", { defaultValue: "Identity & Access" })}
|
|
139
141
|
description="Manage the core administrative team and security permissions."
|
|
140
142
|
icon={Users}
|
|
141
143
|
action={{
|
|
@@ -155,7 +157,7 @@ export function UserManagement() {
|
|
|
155
157
|
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-secondary)] opacity-40 group-focus-within:opacity-100 transition-opacity" />
|
|
156
158
|
<input
|
|
157
159
|
type="text"
|
|
158
|
-
placeholder="Search by identity or email..."
|
|
160
|
+
placeholder={t("fields.searchByIdentityOr", { defaultValue: "Search by identity or email..." })}
|
|
159
161
|
value={searchQuery}
|
|
160
162
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
161
163
|
className="w-full pl-10 pr-4 py-2.5 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-xl focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] transition-all text-xs font-bold"
|
|
@@ -241,7 +243,7 @@ export function UserManagement() {
|
|
|
241
243
|
<button
|
|
242
244
|
onClick={() => handleDelete(user)}
|
|
243
245
|
className="p-1.5 rounded-lg border border-red-500/20 bg-red-500/10 text-red-500 hover:bg-red-500/20 transition-all"
|
|
244
|
-
title="Delete User"
|
|
246
|
+
title={t("tooltips.deleteUser", { defaultValue: "Delete User" })}
|
|
245
247
|
>
|
|
246
248
|
<Trash2 className="w-3.5 h-3.5" />
|
|
247
249
|
</button>
|
|
@@ -258,7 +260,7 @@ export function UserManagement() {
|
|
|
258
260
|
<Modal
|
|
259
261
|
open={showCreateModal}
|
|
260
262
|
onClose={() => setShowCreateModal(false)}
|
|
261
|
-
title="Create User"
|
|
263
|
+
title={t("tooltips.createUser", { defaultValue: "Create User" })}
|
|
262
264
|
size="lg"
|
|
263
265
|
>
|
|
264
266
|
<ModalContent>
|
|
@@ -269,7 +271,7 @@ export function UserManagement() {
|
|
|
269
271
|
type="text"
|
|
270
272
|
value={createForm.name}
|
|
271
273
|
onChange={(e) => setCreateForm({ ...createForm, name: e.target.value })}
|
|
272
|
-
placeholder="John Doe"
|
|
274
|
+
placeholder={t("fields.johnDoe", { defaultValue: "John Doe" })}
|
|
273
275
|
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)]"
|
|
274
276
|
/>
|
|
275
277
|
</div>
|
|
@@ -290,7 +292,7 @@ export function UserManagement() {
|
|
|
290
292
|
type="password"
|
|
291
293
|
value={createForm.password}
|
|
292
294
|
onChange={(e) => setCreateForm({ ...createForm, password: e.target.value })}
|
|
293
|
-
placeholder="Minimum 12 characters"
|
|
295
|
+
placeholder={t("fields.minimum12Characters", { defaultValue: "Minimum 12 characters" })}
|
|
294
296
|
required
|
|
295
297
|
minLength={12}
|
|
296
298
|
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)]"
|
|
@@ -3,12 +3,14 @@ import { Dropdown, DropdownItem, DropdownSeparator } from "./ui/Dropdown";
|
|
|
3
3
|
import { User, Shield, Key, Webhook, Clock, FileText, ExternalLink, HelpCircle, LogOut, Terminal, Zap } from "./ui/icons";
|
|
4
4
|
import { useAuthStore } from "../lib/stores";
|
|
5
5
|
import { apiGet } from "../lib/api";
|
|
6
|
+
import { useTranslation } from "react-i18next";
|
|
6
7
|
|
|
7
8
|
interface UserMenuProps {
|
|
8
9
|
adminPath: string;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function UserMenu({ adminPath }: UserMenuProps) {
|
|
13
|
+
const { t } = useTranslation();
|
|
12
14
|
const currentUser = useAuthStore((s) => s.user);
|
|
13
15
|
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
|
|
14
16
|
const [apiAccess, setApiAccess] = useState<{ graphqlEnabled?: boolean } | null>(null);
|
|
@@ -41,7 +43,7 @@ export function UserMenu({ adminPath }: UserMenuProps) {
|
|
|
41
43
|
trigger={
|
|
42
44
|
<div
|
|
43
45
|
className="flex justify-center p-.5 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface)] rounded-xl transition-all shadow-sm active:scale-95"
|
|
44
|
-
title="Account"
|
|
46
|
+
title={t("userMenu.account", { defaultValue: "Account" })}
|
|
45
47
|
>
|
|
46
48
|
{avatarUrl ? (
|
|
47
49
|
<img src={avatarUrl} alt="" className="w-8 h-8 rounded-full object-cover" />
|
|
@@ -53,7 +55,7 @@ export function UserMenu({ adminPath }: UserMenuProps) {
|
|
|
53
55
|
>
|
|
54
56
|
<div className="px-4 py-2 mb-1">
|
|
55
57
|
<p className="text-[10px] font-medium tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40">
|
|
56
|
-
Account
|
|
58
|
+
{t("userMenu.account", { defaultValue: "Account" })}
|
|
57
59
|
</p>
|
|
58
60
|
</div>
|
|
59
61
|
|
|
@@ -68,21 +70,21 @@ export function UserMenu({ adminPath }: UserMenuProps) {
|
|
|
68
70
|
}
|
|
69
71
|
}}
|
|
70
72
|
>
|
|
71
|
-
Profile Settings
|
|
73
|
+
{t("userMenu.profileSettings", { defaultValue: "Profile Settings" })}
|
|
72
74
|
</DropdownItem>
|
|
73
75
|
|
|
74
76
|
<DropdownItem
|
|
75
77
|
icon={<Shield className="w-4 h-4" />}
|
|
76
78
|
onClick={() => window.location.href = `${adminPath}/roles`}
|
|
77
79
|
>
|
|
78
|
-
Permissions
|
|
80
|
+
{t("userMenu.permissions", { defaultValue: "Permissions" })}
|
|
79
81
|
</DropdownItem>
|
|
80
82
|
|
|
81
83
|
<DropdownSeparator />
|
|
82
84
|
|
|
83
85
|
<div className="px-4 py-2 mb-1">
|
|
84
86
|
<p className="text-[10px] font-medium tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40">
|
|
85
|
-
Developer
|
|
87
|
+
{t("userMenu.developer", { defaultValue: "Developer" })}
|
|
86
88
|
</p>
|
|
87
89
|
</div>
|
|
88
90
|
|
|
@@ -90,33 +92,33 @@ export function UserMenu({ adminPath }: UserMenuProps) {
|
|
|
90
92
|
icon={<Key className="w-4 h-4" />}
|
|
91
93
|
onClick={() => (window.location.href = `${adminPath}/keys`)}
|
|
92
94
|
>
|
|
93
|
-
API Keys
|
|
95
|
+
{t("userMenu.apiKeys", { defaultValue: "API Keys" })}
|
|
94
96
|
</DropdownItem>
|
|
95
97
|
|
|
96
98
|
<DropdownItem
|
|
97
99
|
icon={<Webhook className="w-4 h-4" />}
|
|
98
100
|
onClick={() => (window.location.href = `${adminPath}/webhooks`)}
|
|
99
101
|
>
|
|
100
|
-
Web Hooks
|
|
102
|
+
{t("userMenu.webHooks", { defaultValue: "Web Hooks" })}
|
|
101
103
|
</DropdownItem>
|
|
102
104
|
<DropdownItem
|
|
103
105
|
icon={<Clock className="w-4 h-4" />}
|
|
104
106
|
onClick={() => (window.location.href = `${adminPath}/sessions`)}
|
|
105
107
|
>
|
|
106
|
-
Sessions
|
|
108
|
+
{t("userMenu.sessions", { defaultValue: "Sessions" })}
|
|
107
109
|
</DropdownItem>
|
|
108
110
|
<DropdownItem
|
|
109
111
|
icon={<FileText className="w-4 h-4" />}
|
|
110
112
|
onClick={() => (window.location.href = `${adminPath}/audit`)}
|
|
111
113
|
>
|
|
112
|
-
Audit Logs
|
|
114
|
+
{t("userMenu.auditLogs", { defaultValue: "Audit Logs" })}
|
|
113
115
|
</DropdownItem>
|
|
114
116
|
|
|
115
117
|
<DropdownItem
|
|
116
118
|
icon={<Terminal className="w-4 h-4" />}
|
|
117
119
|
onClick={() => (window.location.href = `${adminPath}/rest-playground`)}
|
|
118
120
|
>
|
|
119
|
-
API Explorer
|
|
121
|
+
{t("userMenu.apiExplorer", { defaultValue: "API Explorer" })}
|
|
120
122
|
</DropdownItem>
|
|
121
123
|
|
|
122
124
|
{(apiAccess === null || apiAccess?.graphqlEnabled) && (
|
|
@@ -124,7 +126,7 @@ export function UserMenu({ adminPath }: UserMenuProps) {
|
|
|
124
126
|
icon={<Zap className="w-4 h-4" />}
|
|
125
127
|
onClick={() => (window.location.href = `${adminPath}/graphql`)}
|
|
126
128
|
>
|
|
127
|
-
GraphQL Playground
|
|
129
|
+
{t("userMenu.graphqlPlayground", { defaultValue: "GraphQL Playground" })}
|
|
128
130
|
</DropdownItem>
|
|
129
131
|
)}
|
|
130
132
|
|
|
@@ -132,22 +134,22 @@ export function UserMenu({ adminPath }: UserMenuProps) {
|
|
|
132
134
|
|
|
133
135
|
<div className="px-4 py-2 mb-1">
|
|
134
136
|
<p className="text-[10px] font-medium tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40">
|
|
135
|
-
Resources
|
|
137
|
+
{t("userMenu.resources", { defaultValue: "Resources" })}
|
|
136
138
|
</p>
|
|
137
139
|
</div>
|
|
138
140
|
|
|
139
141
|
<DropdownItem
|
|
140
142
|
icon={<ExternalLink className="w-4 h-4" />}
|
|
141
|
-
onClick={() => window.open("https://
|
|
143
|
+
onClick={() => window.open("https://kyro-cms.com/getting-started.html", "_blank")}
|
|
142
144
|
>
|
|
143
|
-
Documentation
|
|
145
|
+
{t("userMenu.documentation", { defaultValue: "Documentation" })}
|
|
144
146
|
</DropdownItem>
|
|
145
147
|
|
|
146
148
|
<DropdownItem
|
|
147
149
|
icon={<HelpCircle className="w-4 h-4" />}
|
|
148
|
-
onClick={() => window.open("https://github.com/danielDozie/kyro-cms/issues", "_blank")}
|
|
150
|
+
onClick={() => window.open("https://github.com/danielDozie/kyro-cms/issues/new", "_blank")}
|
|
149
151
|
>
|
|
150
|
-
Get Support
|
|
152
|
+
{t("userMenu.getSupport", { defaultValue: "Get Support" })}
|
|
151
153
|
</DropdownItem>
|
|
152
154
|
|
|
153
155
|
<DropdownSeparator />
|
|
@@ -157,7 +159,7 @@ export function UserMenu({ adminPath }: UserMenuProps) {
|
|
|
157
159
|
danger
|
|
158
160
|
onClick={() => document.getElementById("logout-btn")?.click()}
|
|
159
161
|
>
|
|
160
|
-
Sign Out
|
|
162
|
+
{t("actions.signOut", { defaultValue: "Sign Out" })}
|
|
161
163
|
</DropdownItem>
|
|
162
164
|
</Dropdown>
|
|
163
165
|
);
|
|
@@ -2,6 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { SlidePanel } from "./ui/SlidePanel";
|
|
3
3
|
import { Spinner } from "./ui/Spinner";
|
|
4
4
|
import { History, Eye, GitCompare, Undo2, CheckCircle2, Clock, User } from "lucide-react";
|
|
5
|
+
import { useTranslation } from "react-i18next";
|
|
5
6
|
|
|
6
7
|
interface Version {
|
|
7
8
|
id: string;
|
|
@@ -38,6 +39,7 @@ export function VersionHistoryPanel({
|
|
|
38
39
|
onCompare,
|
|
39
40
|
loading = false,
|
|
40
41
|
}: VersionHistoryPanelProps) {
|
|
42
|
+
const { t } = useTranslation();
|
|
41
43
|
const formatDate = (dateValue: any) => {
|
|
42
44
|
if (!dateValue) return "Unknown date";
|
|
43
45
|
const date = new Date(dateValue);
|
|
@@ -82,7 +84,7 @@ export function VersionHistoryPanel({
|
|
|
82
84
|
<SlidePanel
|
|
83
85
|
open={open}
|
|
84
86
|
onClose={onClose}
|
|
85
|
-
title="Version History"
|
|
87
|
+
title={t("tooltips.versionHistory", { defaultValue: "Version History" })}
|
|
86
88
|
width="md"
|
|
87
89
|
>
|
|
88
90
|
{loading ? (
|
|
@@ -159,7 +161,7 @@ export function VersionHistoryPanel({
|
|
|
159
161
|
<button type="button"
|
|
160
162
|
onClick={() => onPreview(version)}
|
|
161
163
|
className="p-2 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-primary)]/10 rounded-md transition-colors"
|
|
162
|
-
title="Preview this version"
|
|
164
|
+
title={t("tooltips.previewThisVersion", { defaultValue: "Preview this version" })}
|
|
163
165
|
>
|
|
164
166
|
<Eye className="w-4 h-4" />
|
|
165
167
|
</button>
|
|
@@ -172,7 +174,7 @@ export function VersionHistoryPanel({
|
|
|
172
174
|
)
|
|
173
175
|
}
|
|
174
176
|
className="p-2 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-primary)]/10 rounded-md transition-colors"
|
|
175
|
-
title="Compare with current"
|
|
177
|
+
title={t("tooltips.compareWithCurrent", { defaultValue: "Compare with current" })}
|
|
176
178
|
>
|
|
177
179
|
<GitCompare className="w-4 h-4" />
|
|
178
180
|
</button>
|
|
@@ -181,7 +183,7 @@ export function VersionHistoryPanel({
|
|
|
181
183
|
<button type="button"
|
|
182
184
|
onClick={() => onRestore(version)}
|
|
183
185
|
className="p-2 text-amber-600 hover:bg-amber-500/10 rounded-md transition-colors"
|
|
184
|
-
title="Restore this version"
|
|
186
|
+
title={t("tooltips.restoreThisVersion", { defaultValue: "Restore this version" })}
|
|
185
187
|
>
|
|
186
188
|
<Undo2 className="w-4 h-4" />
|
|
187
189
|
</button>
|