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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}>⚙</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}>⚙</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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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> },
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user