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 && (
)}
-
+