feat: restructure admin sidebar with collapsible sub-navigation and new routes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-17 16:09:23 +01:00
parent 4d5a4842b9
commit 9fbda7715c
3 changed files with 115 additions and 11 deletions

View File

@@ -209,6 +209,44 @@
text-align: center;
}
/* ─── Admin Sub-Menu ─── */
.adminChevron {
margin-left: 6px;
font-size: 8px;
color: var(--text-muted);
}
.adminSubMenu {
display: flex;
flex-direction: column;
}
.adminSubItem {
display: block;
padding: 6px 16px 6px 42px;
font-size: 12px;
color: var(--text-muted);
text-decoration: none;
transition: all 0.1s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.adminSubItem:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.adminSubItemActive {
color: var(--amber);
background: var(--amber-glow);
}
.sidebarCollapsed .adminSubMenu {
display: none;
}
/* ─── Responsive ─── */
@media (max-width: 1024px) {
.sidebar {
@@ -242,4 +280,8 @@
.sidebar .bottomLabel {
display: none;
}
.sidebar .adminSubMenu {
display: none;
}
}

View File

@@ -1,5 +1,5 @@
import { useMemo, useState } from 'react';
import { NavLink, useParams } from 'react-router';
import { NavLink, useParams, useLocation } from 'react-router';
import { useAgents } from '../../api/queries/agents';
import { useAuthStore } from '../../auth/auth-store';
import type { AgentInstance } from '../../api/types';
@@ -112,18 +112,73 @@ export function AppSidebar({ collapsed }: AppSidebarProps) {
{/* Bottom: Admin */}
{roles.includes('ADMIN') && (
<div className={styles.bottom}>
<NavLink
to="/admin/oidc"
className={({ isActive }) =>
`${styles.bottomItem} ${isActive ? styles.bottomItemActive : ''}`
}
title="Admin"
>
<span className={styles.bottomIcon}>&#9881;</span>
<span className={styles.bottomLabel}>Admin</span>
</NavLink>
<AdminSubMenu collapsed={collapsed} />
</div>
)}
</aside>
);
}
const ADMIN_LINKS = [
{ to: '/admin/database', label: 'Database' },
{ to: '/admin/opensearch', label: 'OpenSearch' },
{ to: '/admin/audit', label: 'Audit Log' },
{ to: '/admin/oidc', label: 'OIDC' },
];
function AdminSubMenu({ collapsed: sidebarCollapsed }: { collapsed: boolean }) {
const location = useLocation();
const isAdminActive = location.pathname.startsWith('/admin');
const [open, setOpen] = useState(() => {
try {
return localStorage.getItem('cameleer-admin-sidebar-open') === 'true';
} catch {
return false;
}
});
function toggle() {
const next = !open;
setOpen(next);
try {
localStorage.setItem('cameleer-admin-sidebar-open', String(next));
} catch { /* ignore */ }
}
return (
<>
<button
type="button"
className={`${styles.bottomItem} ${isAdminActive ? styles.bottomItemActive : ''}`}
onClick={toggle}
title="Admin"
>
<span className={styles.bottomIcon}>&#9881;</span>
<span className={styles.bottomLabel}>
Admin
{!sidebarCollapsed && (
<span className={styles.adminChevron}>
{open ? '\u25BC' : '\u25B6'}
</span>
)}
</span>
</button>
{open && !sidebarCollapsed && (
<div className={styles.adminSubMenu}>
{ADMIN_LINKS.map((link) => (
<NavLink
key={link.to}
to={link.to}
className={({ isActive }) =>
`${styles.adminSubItem} ${isActive ? styles.adminSubItemActive : ''}`
}
>
{link.label}
</NavLink>
))}
</div>
)}
</>
);
}