diff --git a/ui/src/components/LayoutShell.tsx b/ui/src/components/LayoutShell.tsx index 673bafb3..97abd2d9 100644 --- a/ui/src/components/LayoutShell.tsx +++ b/ui/src/components/LayoutShell.tsx @@ -26,6 +26,7 @@ import { useScope } from '../hooks/useScope'; import { buildAppTreeNodes, buildAgentTreeNodes, + buildRouteTreeNodes, buildAdminTreeNodes, readCollapsed, writeCollapsed, @@ -145,6 +146,7 @@ function makeChevron() { const SK_APPS = 'sidebar:section:apps'; const SK_AGENTS = 'sidebar:section:agents'; +const SK_ROUTES = 'sidebar:section:routes'; const SK_ADMIN = 'sidebar:section:admin'; const SK_COLLAPSED = 'sidebar:collapsed'; @@ -181,10 +183,11 @@ function LayoutContent() { // --- Section open states ------------------------------------------ const [appsOpen, setAppsOpen] = useState(() => readCollapsed(SK_APPS, true)); const [agentsOpen, setAgentsOpen] = useState(() => readCollapsed(SK_AGENTS, false)); + const [routesOpen, setRoutesOpen] = useState(() => readCollapsed(SK_ROUTES, false)); const [adminOpen, setAdminOpen] = useState(() => readCollapsed(SK_ADMIN, false)); // Ref to remember operational section states when switching to admin - const opsStateRef = useRef({ apps: appsOpen, agents: agentsOpen }); + const opsStateRef = useRef({ apps: appsOpen, agents: agentsOpen, routes: routesOpen }); const isAdminPage = location.pathname.startsWith('/admin'); @@ -193,9 +196,10 @@ function LayoutContent() { useEffect(() => { if (isAdminPage && !prevAdminRef.current) { // Entering admin — save operational states and collapse them - opsStateRef.current = { apps: appsOpen, agents: agentsOpen }; + opsStateRef.current = { apps: appsOpen, agents: agentsOpen, routes: routesOpen }; setAppsOpen(false); setAgentsOpen(false); + setRoutesOpen(false); setAdminOpen(true); writeCollapsed(SK_APPS, false); writeCollapsed(SK_AGENTS, false); @@ -204,9 +208,11 @@ function LayoutContent() { // Leaving admin — restore operational states setAppsOpen(opsStateRef.current.apps); setAgentsOpen(opsStateRef.current.agents); + setRoutesOpen(opsStateRef.current.routes); setAdminOpen(false); writeCollapsed(SK_APPS, opsStateRef.current.apps); writeCollapsed(SK_AGENTS, opsStateRef.current.agents); + writeCollapsed(SK_ROUTES, opsStateRef.current.routes); writeCollapsed(SK_ADMIN, false); } prevAdminRef.current = isAdminPage; @@ -235,6 +241,17 @@ function LayoutContent() { }); }, [isAdminPage, navigate]); + const toggleRoutes = useCallback(() => { + if (isAdminPage) { + navigate('/exchanges'); + return; + } + setRoutesOpen((prev) => { + writeCollapsed(SK_ROUTES, !prev); + return !prev; + }); + }, [isAdminPage, navigate]); + const toggleAdmin = useCallback(() => { setAdminOpen((prev) => { writeCollapsed(SK_ADMIN, !prev); @@ -282,6 +299,11 @@ function LayoutContent() { [sidebarApps], ); + const routeTreeNodes: SidebarTreeNode[] = useMemo( + () => buildRouteTreeNodes(sidebarApps, makeStatusDot, makeChevron), + [sidebarApps], + ); + const adminTreeNodes: SidebarTreeNode[] = useMemo( () => buildAdminTreeNodes(), [], @@ -511,6 +533,24 @@ function LayoutContent() { /> + + + + {/* When NOT on admin pages, show Admin section at bottom */} {!isAdminPage && ( )} -
+