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;
|
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 ─── */
|
/* ─── Responsive ─── */
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
@@ -242,4 +280,8 @@
|
|||||||
.sidebar .bottomLabel {
|
.sidebar .bottomLabel {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar .adminSubMenu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo, useState } from 'react';
|
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 { useAgents } from '../../api/queries/agents';
|
||||||
import { useAuthStore } from '../../auth/auth-store';
|
import { useAuthStore } from '../../auth/auth-store';
|
||||||
import type { AgentInstance } from '../../api/types';
|
import type { AgentInstance } from '../../api/types';
|
||||||
@@ -112,18 +112,73 @@ export function AppSidebar({ collapsed }: AppSidebarProps) {
|
|||||||
{/* Bottom: Admin */}
|
{/* Bottom: Admin */}
|
||||||
{roles.includes('ADMIN') && (
|
{roles.includes('ADMIN') && (
|
||||||
<div className={styles.bottom}>
|
<div className={styles.bottom}>
|
||||||
<NavLink
|
<AdminSubMenu collapsed={collapsed} />
|
||||||
to="/admin/oidc"
|
|
||||||
className={({ isActive }) =>
|
|
||||||
`${styles.bottomItem} ${isActive ? styles.bottomItemActive : ''}`
|
|
||||||
}
|
|
||||||
title="Admin"
|
|
||||||
>
|
|
||||||
<span className={styles.bottomIcon}>⚙</span>
|
|
||||||
<span className={styles.bottomLabel}>Admin</span>
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</aside>
|
</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';
|
import { AppScopedView } from './pages/dashboard/AppScopedView';
|
||||||
|
|
||||||
const SwaggerPage = lazy(() => import('./pages/swagger/SwaggerPage').then(m => ({ default: m.SwaggerPage })));
|
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([
|
export const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@@ -30,6 +33,10 @@ export const router = createBrowserRouter([
|
|||||||
{ path: 'executions', element: <ExecutionExplorer /> },
|
{ path: 'executions', element: <ExecutionExplorer /> },
|
||||||
{ path: 'apps/:group', element: <AppScopedView /> },
|
{ path: 'apps/:group', element: <AppScopedView /> },
|
||||||
{ path: 'apps/:group/routes/:routeId', element: <RoutePage /> },
|
{ 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: 'admin/oidc', element: <OidcAdminPage /> },
|
||||||
{ path: 'swagger', element: <Suspense fallback={null}><SwaggerPage /></Suspense> },
|
{ path: 'swagger', element: <Suspense fallback={null}><SwaggerPage /></Suspense> },
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user