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>
)}
</>
);
}

View File

@@ -10,6 +10,9 @@ import { RoutePage } from './pages/routes/RoutePage';
import { AppScopedView } from './pages/dashboard/AppScopedView';
const SwaggerPage = lazy(() => import('./pages/swagger/SwaggerPage').then(m => ({ default: m.SwaggerPage })));
const DatabaseAdminPage = lazy(() => import('./pages/admin/DatabaseAdminPage').then(m => ({ default: m.DatabaseAdminPage })));
const OpenSearchAdminPage = lazy(() => import('./pages/admin/OpenSearchAdminPage').then(m => ({ default: m.OpenSearchAdminPage })));
const AuditLogPage = lazy(() => import('./pages/admin/AuditLogPage').then(m => ({ default: m.AuditLogPage })));
export const router = createBrowserRouter([
{
@@ -30,6 +33,10 @@ export const router = createBrowserRouter([
{ path: 'executions', element: <ExecutionExplorer /> },
{ path: 'apps/:group', element: <AppScopedView /> },
{ path: 'apps/:group/routes/:routeId', element: <RoutePage /> },
{ path: 'admin', element: <Navigate to="/admin/database" replace /> },
{ path: 'admin/database', element: <Suspense fallback={null}><DatabaseAdminPage /></Suspense> },
{ path: 'admin/opensearch', element: <Suspense fallback={null}><OpenSearchAdminPage /></Suspense> },
{ path: 'admin/audit', element: <Suspense fallback={null}><AuditLogPage /></Suspense> },
{ path: 'admin/oidc', element: <OidcAdminPage /> },
{ path: 'swagger', element: <Suspense fallback={null}><SwaggerPage /></Suspense> },
],