diff --git a/ui/src/components/layout/AppSidebar.module.css b/ui/src/components/layout/AppSidebar.module.css index eba3e514..0261d18c 100644 --- a/ui/src/components/layout/AppSidebar.module.css +++ b/ui/src/components/layout/AppSidebar.module.css @@ -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; + } } diff --git a/ui/src/components/layout/AppSidebar.tsx b/ui/src/components/layout/AppSidebar.tsx index 1ebfa9f8..6426004a 100644 --- a/ui/src/components/layout/AppSidebar.tsx +++ b/ui/src/components/layout/AppSidebar.tsx @@ -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') && (
- - `${styles.bottomItem} ${isActive ? styles.bottomItemActive : ''}` - } - title="Admin" - > - - Admin - +
)} ); } + +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 ( + <> + + {open && !sidebarCollapsed && ( +
+ {ADMIN_LINKS.map((link) => ( + + `${styles.adminSubItem} ${isActive ? styles.adminSubItemActive : ''}` + } + > + {link.label} + + ))} +
+ )} + + ); +} diff --git a/ui/src/router.tsx b/ui/src/router.tsx index 406bbbcb..e808603f 100644 --- a/ui/src/router.tsx +++ b/ui/src/router.tsx @@ -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: }, { path: 'apps/:group', element: }, { path: 'apps/:group/routes/:routeId', element: }, + { path: 'admin', element: }, + { path: 'admin/database', element: }, + { path: 'admin/opensearch', element: }, + { path: 'admin/audit', element: }, { path: 'admin/oidc', element: }, { path: 'swagger', element: }, ],