Rename Java packages from com.cameleer3 to com.cameleer, module directories from cameleer3-* to cameleer-*, and all references throughout workflows, Dockerfiles, docs, migrations, and pom.xml. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
573 lines
22 KiB
Markdown
573 lines
22 KiB
Markdown
# Composable Sidebar Migration Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Migrate the server UI from the old monolithic `<Sidebar apps={[...]}/>` to the new composable compound Sidebar API from `@cameleer/design-system` v0.1.23, adding admin accordion behavior and icon-rail collapse.
|
|
|
|
**Architecture:** Extract tree-building logic into a local `sidebar-utils.ts`, rewrite sidebar composition in `LayoutShell.tsx` using `Sidebar.Header/Section/Footer` compound components, add admin accordion behavior via route-based section state management, and simplify `AdminLayout.tsx` by removing its tab navigation (sidebar handles it now).
|
|
|
|
**Tech Stack:** React 19, `@cameleer/design-system` v0.1.23, react-router, lucide-react, CSS Modules
|
|
|
|
---
|
|
|
|
## File Map
|
|
|
|
| File | Action | Responsibility |
|
|
|------|--------|----------------|
|
|
| `ui/src/components/sidebar-utils.ts` | Create | Tree-building functions, SidebarApp type, formatCount, admin node builder |
|
|
| `ui/src/components/LayoutShell.tsx` | Modify | Rewrite sidebar composition with compound API, add accordion + collapse |
|
|
| `ui/src/pages/Admin/AdminLayout.tsx` | Modify | Remove tab navigation (sidebar handles it), keep content wrapper |
|
|
|
|
---
|
|
|
|
### Task 1: Create sidebar-utils.ts with Tree-Building Functions
|
|
|
|
**Files:**
|
|
- Create: `ui/src/components/sidebar-utils.ts`
|
|
|
|
- [ ] **Step 1: Create the utility file**
|
|
|
|
This file contains the `SidebarApp` type (moved from DS), tree-building functions, and the admin node builder. These were previously inside the DS's monolithic Sidebar component.
|
|
|
|
```typescript
|
|
import type { ReactNode } from 'react';
|
|
import type { SidebarTreeNode } from '@cameleer/design-system';
|
|
|
|
// ── Domain types (moved from DS) ──────────────────────────────────────────
|
|
|
|
export interface SidebarRoute {
|
|
id: string;
|
|
name: string;
|
|
exchangeCount: number;
|
|
}
|
|
|
|
export interface SidebarAgent {
|
|
id: string;
|
|
name: string;
|
|
status: 'live' | 'stale' | 'dead';
|
|
tps: number;
|
|
}
|
|
|
|
export interface SidebarApp {
|
|
id: string;
|
|
name: string;
|
|
health: 'live' | 'stale' | 'dead';
|
|
exchangeCount: number;
|
|
routes: SidebarRoute[];
|
|
agents: SidebarAgent[];
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
|
|
export function formatCount(n: number): string {
|
|
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
return String(n);
|
|
}
|
|
|
|
// ── Tree node builders ────────────────────────────────────────────────────
|
|
|
|
export function buildAppTreeNodes(
|
|
apps: SidebarApp[],
|
|
statusDot: (health: string) => ReactNode,
|
|
chevron: ReactNode,
|
|
): SidebarTreeNode[] {
|
|
return apps.map((app) => ({
|
|
id: app.id,
|
|
label: app.name,
|
|
icon: statusDot(app.health),
|
|
badge: formatCount(app.exchangeCount),
|
|
path: `/apps/${app.id}`,
|
|
starrable: true,
|
|
starKey: `app:${app.id}`,
|
|
children: app.routes.map((route) => ({
|
|
id: `${app.id}/${route.id}`,
|
|
label: route.name,
|
|
icon: chevron,
|
|
badge: formatCount(route.exchangeCount),
|
|
path: `/apps/${app.id}/${route.id}`,
|
|
})),
|
|
}));
|
|
}
|
|
|
|
export function buildAgentTreeNodes(
|
|
apps: SidebarApp[],
|
|
statusDot: (health: string) => ReactNode,
|
|
): SidebarTreeNode[] {
|
|
return apps
|
|
.filter((app) => app.agents.length > 0)
|
|
.map((app) => {
|
|
const liveCount = app.agents.filter((a) => a.status === 'live').length;
|
|
return {
|
|
id: `agents:${app.id}`,
|
|
label: app.name,
|
|
icon: statusDot(app.health),
|
|
badge: `${liveCount}/${app.agents.length} live`,
|
|
path: `/agents/${app.id}`,
|
|
starrable: true,
|
|
starKey: `agent:${app.id}`,
|
|
children: app.agents.map((agent) => ({
|
|
id: `agents:${app.id}/${agent.id}`,
|
|
label: agent.name,
|
|
icon: statusDot(agent.status),
|
|
badge: `${agent.tps.toFixed(1)}/s`,
|
|
path: `/agents/${app.id}/${agent.id}`,
|
|
})),
|
|
};
|
|
});
|
|
}
|
|
|
|
export function buildRouteTreeNodes(
|
|
apps: SidebarApp[],
|
|
statusDot: (health: string) => ReactNode,
|
|
chevron: ReactNode,
|
|
): SidebarTreeNode[] {
|
|
return apps
|
|
.filter((app) => app.routes.length > 0)
|
|
.map((app) => ({
|
|
id: `routes:${app.id}`,
|
|
label: app.name,
|
|
icon: statusDot(app.health),
|
|
badge: `${app.routes.length} route${app.routes.length !== 1 ? 's' : ''}`,
|
|
path: `/routes/${app.id}`,
|
|
starrable: true,
|
|
starKey: `routestat:${app.id}`,
|
|
children: app.routes.map((route) => ({
|
|
id: `routes:${app.id}/${route.id}`,
|
|
label: route.name,
|
|
icon: chevron,
|
|
badge: formatCount(route.exchangeCount),
|
|
path: `/routes/${app.id}/${route.id}`,
|
|
})),
|
|
}));
|
|
}
|
|
|
|
export function buildAdminTreeNodes(): SidebarTreeNode[] {
|
|
return [
|
|
{ id: 'admin:rbac', label: 'User Management', path: '/admin/rbac' },
|
|
{ id: 'admin:audit', label: 'Audit Log', path: '/admin/audit' },
|
|
{ id: 'admin:oidc', label: 'OIDC', path: '/admin/oidc' },
|
|
{ id: 'admin:appconfig', label: 'App Config', path: '/admin/appconfig' },
|
|
{ id: 'admin:database', label: 'Database', path: '/admin/database' },
|
|
{ id: 'admin:clickhouse', label: 'ClickHouse', path: '/admin/clickhouse' },
|
|
];
|
|
}
|
|
|
|
// ── localStorage-backed section collapse ──────────────────────────────────
|
|
|
|
export function readCollapsed(key: string, defaultValue: boolean): boolean {
|
|
try {
|
|
const raw = localStorage.getItem(key);
|
|
if (raw !== null) return raw === 'true';
|
|
} catch { /* ignore */ }
|
|
return defaultValue;
|
|
}
|
|
|
|
export function writeCollapsed(key: string, value: boolean): void {
|
|
try {
|
|
localStorage.setItem(key, String(value));
|
|
} catch { /* ignore */ }
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Verify it compiles**
|
|
|
|
Run: `cd C:/Users/Hendrik/Documents/projects/cameleer-server/ui && npx tsc --project tsconfig.app.json --noEmit`
|
|
Expected: No errors.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add ui/src/components/sidebar-utils.ts
|
|
git commit -m "feat(#112): extract sidebar tree builders and types from DS"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: Rewrite LayoutShell with Composable Sidebar
|
|
|
|
**Files:**
|
|
- Modify: `ui/src/components/LayoutShell.tsx`
|
|
|
|
This is the main migration task. The `LayoutContent` function gets a significant rewrite of its sidebar composition while preserving all existing TopBar, CommandPalette, ContentTabs, breadcrumb, and scope logic.
|
|
|
|
- [ ] **Step 1: Update imports**
|
|
|
|
Replace the old DS imports and add new ones. In `LayoutShell.tsx`, change the first 10 lines to:
|
|
|
|
```typescript
|
|
import { Outlet, useNavigate, useLocation } from 'react-router';
|
|
import { AppShell, Sidebar, SidebarTree, useStarred, TopBar, CommandPalette, CommandPaletteProvider, GlobalFilterProvider, ToastProvider, BreadcrumbProvider, useCommandPalette, useGlobalFilters, StatusDot } from '@cameleer/design-system';
|
|
import type { SidebarTreeNode, SearchResult } from '@cameleer/design-system';
|
|
import { Box, Cpu, GitBranch, Settings, FileText, ChevronRight } from 'lucide-react';
|
|
import { useRouteCatalog } from '../api/queries/catalog';
|
|
import { useAgents } from '../api/queries/agents';
|
|
import { useSearchExecutions, useAttributeKeys } from '../api/queries/executions';
|
|
import { useAuthStore } from '../auth/auth-store';
|
|
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
|
import { ContentTabs } from './ContentTabs';
|
|
import { useScope } from '../hooks/useScope';
|
|
import { buildAppTreeNodes, buildAgentTreeNodes, buildRouteTreeNodes, buildAdminTreeNodes, readCollapsed, writeCollapsed } from './sidebar-utils';
|
|
import type { SidebarApp } from './sidebar-utils';
|
|
```
|
|
|
|
- [ ] **Step 2: Remove the old `sidebarApps` builder and `healthToColor` function**
|
|
|
|
Delete the `healthToColor` function (lines 12-19) and the `sidebarApps` useMemo block (lines 128-154). These are replaced by tree-building functions that produce `SidebarTreeNode[]` directly.
|
|
|
|
- [ ] **Step 3: Add sidebar state management inside LayoutContent**
|
|
|
|
Add these state and memo declarations inside `LayoutContent`, after the existing hooks (after `const { scope, setTab } = useScope();` around line 112):
|
|
|
|
```typescript
|
|
// ── Sidebar state ──────────────────────────────────────────────────────
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
const [filterQuery, setFilterQuery] = useState('');
|
|
const { starredIds, isStarred, toggleStar } = useStarred();
|
|
|
|
// Section collapse states — persisted to localStorage
|
|
const [appsOpen, setAppsOpen] = useState(() => !readCollapsed('cameleer:sidebar:apps-collapsed', false));
|
|
const [agentsOpen, setAgentsOpen] = useState(() => !readCollapsed('cameleer:sidebar:agents-collapsed', false));
|
|
const [routesOpen, setRoutesOpen] = useState(() => !readCollapsed('cameleer:sidebar:routes-collapsed', true));
|
|
const [adminOpen, setAdminOpen] = useState(false);
|
|
|
|
const toggleApps = useCallback(() => {
|
|
setAppsOpen((v) => { writeCollapsed('cameleer:sidebar:apps-collapsed', v); return !v; });
|
|
}, []);
|
|
const toggleAgents = useCallback(() => {
|
|
setAgentsOpen((v) => { writeCollapsed('cameleer:sidebar:agents-collapsed', v); return !v; });
|
|
}, []);
|
|
const toggleRoutes = useCallback(() => {
|
|
setRoutesOpen((v) => { writeCollapsed('cameleer:sidebar:routes-collapsed', v); return !v; });
|
|
}, []);
|
|
const toggleAdmin = useCallback(() => setAdminOpen((v) => !v), []);
|
|
|
|
// Accordion: entering admin expands admin, collapses operational sections
|
|
const prevOpsState = useRef<{ apps: boolean; agents: boolean; routes: boolean } | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (isAdminPage) {
|
|
// Save current operational state, then collapse all, expand admin
|
|
prevOpsState.current = { apps: appsOpen, agents: agentsOpen, routes: routesOpen };
|
|
setAppsOpen(false);
|
|
setAgentsOpen(false);
|
|
setRoutesOpen(false);
|
|
setAdminOpen(true);
|
|
} else if (prevOpsState.current) {
|
|
// Restore operational state, collapse admin
|
|
setAppsOpen(prevOpsState.current.apps);
|
|
setAgentsOpen(prevOpsState.current.agents);
|
|
setRoutesOpen(prevOpsState.current.routes);
|
|
setAdminOpen(false);
|
|
prevOpsState.current = null;
|
|
}
|
|
}, [isAdminPage]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
// Build tree nodes from catalog data
|
|
const statusDot = useCallback((health: string) => <StatusDot status={health as any} />, []);
|
|
const chevronIcon = useMemo(() => <ChevronRight size={12} />, []);
|
|
|
|
const appNodes = useMemo(
|
|
() => buildAppTreeNodes(catalog ? [...catalog].sort((a: any, b: any) => a.appId.localeCompare(b.appId)).map((app: any) => ({
|
|
id: app.appId,
|
|
name: app.appId,
|
|
health: app.health as 'live' | 'stale' | 'dead',
|
|
exchangeCount: app.exchangeCount,
|
|
routes: [...(app.routes || [])].sort((a: any, b: any) => a.routeId.localeCompare(b.routeId)).map((r: any) => ({
|
|
id: r.routeId, name: r.routeId, exchangeCount: r.exchangeCount,
|
|
})),
|
|
agents: [...(app.agents || [])].sort((a: any, b: any) => a.name.localeCompare(b.name)).map((a: any) => ({
|
|
id: a.id, name: a.name, status: a.status as 'live' | 'stale' | 'dead', tps: a.tps ?? 0,
|
|
})),
|
|
})) : [], statusDot, chevronIcon),
|
|
[catalog, statusDot, chevronIcon],
|
|
);
|
|
|
|
const agentNodes = useMemo(
|
|
() => buildAgentTreeNodes(catalog ? [...catalog].sort((a: any, b: any) => a.appId.localeCompare(b.appId)).map((app: any) => ({
|
|
id: app.appId,
|
|
name: app.appId,
|
|
health: app.health as 'live' | 'stale' | 'dead',
|
|
exchangeCount: app.exchangeCount,
|
|
routes: [],
|
|
agents: [...(app.agents || [])].sort((a: any, b: any) => a.name.localeCompare(b.name)).map((a: any) => ({
|
|
id: a.id, name: a.name, status: a.status as 'live' | 'stale' | 'dead', tps: a.tps ?? 0,
|
|
})),
|
|
})) : [], statusDot),
|
|
[catalog, statusDot],
|
|
);
|
|
|
|
const routeNodes = useMemo(
|
|
() => buildRouteTreeNodes(catalog ? [...catalog].sort((a: any, b: any) => a.appId.localeCompare(b.appId)).map((app: any) => ({
|
|
id: app.appId,
|
|
name: app.appId,
|
|
health: app.health as 'live' | 'stale' | 'dead',
|
|
exchangeCount: app.exchangeCount,
|
|
routes: [...(app.routes || [])].sort((a: any, b: any) => a.routeId.localeCompare(b.routeId)).map((r: any) => ({
|
|
id: r.routeId, name: r.routeId, exchangeCount: r.exchangeCount,
|
|
})),
|
|
agents: [],
|
|
})) : [], statusDot, chevronIcon),
|
|
[catalog, statusDot, chevronIcon],
|
|
);
|
|
|
|
const adminNodes = useMemo(() => buildAdminTreeNodes(), []);
|
|
|
|
// Sidebar reveal from Cmd-K navigation
|
|
const sidebarRevealPath = (location.state as { sidebarReveal?: string } | null)?.sidebarReveal ?? null;
|
|
|
|
useEffect(() => {
|
|
if (!sidebarRevealPath) return;
|
|
if (sidebarRevealPath.startsWith('/apps') && !appsOpen) setAppsOpen(true);
|
|
if (sidebarRevealPath.startsWith('/agents') && !agentsOpen) setAgentsOpen(true);
|
|
if (sidebarRevealPath.startsWith('/routes') && !routesOpen) setRoutesOpen(true);
|
|
if (sidebarRevealPath.startsWith('/admin') && !adminOpen) setAdminOpen(true);
|
|
}, [sidebarRevealPath]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const effectiveSelectedPath = sidebarRevealPath ?? location.pathname;
|
|
```
|
|
|
|
- [ ] **Step 4: Replace the return block's sidebar composition**
|
|
|
|
Replace the `return` statement in `LayoutContent` (starting from `return (` to the closing `);`). The key change is replacing `<Sidebar apps={sidebarApps} onNavigate={handleSidebarNavigate} />` with the compound Sidebar:
|
|
|
|
```typescript
|
|
return (
|
|
<AppShell
|
|
sidebar={
|
|
<Sidebar
|
|
collapsed={sidebarCollapsed}
|
|
onCollapseToggle={() => setSidebarCollapsed((v) => !v)}
|
|
searchValue={filterQuery}
|
|
onSearchChange={setFilterQuery}
|
|
>
|
|
<Sidebar.Header
|
|
logo={<img src="/favicon.svg" alt="" style={{ width: 28, height: 24 }} />}
|
|
title="cameleer"
|
|
version="v3.2.1"
|
|
onClick={() => handleSidebarNavigate('/apps')}
|
|
/>
|
|
|
|
{isAdminPage && (
|
|
<Sidebar.Section
|
|
label="Admin"
|
|
icon={<Settings size={14} />}
|
|
open={adminOpen}
|
|
onToggle={toggleAdmin}
|
|
active={location.pathname.startsWith('/admin')}
|
|
>
|
|
<SidebarTree
|
|
nodes={adminNodes}
|
|
selectedPath={effectiveSelectedPath}
|
|
filterQuery={filterQuery}
|
|
onNavigate={handleSidebarNavigate}
|
|
/>
|
|
</Sidebar.Section>
|
|
)}
|
|
|
|
<Sidebar.Section
|
|
label="Applications"
|
|
icon={<Box size={14} />}
|
|
open={appsOpen}
|
|
onToggle={() => { toggleApps(); if (isAdminPage) handleSidebarNavigate('/apps'); }}
|
|
active={effectiveSelectedPath.startsWith('/apps') || effectiveSelectedPath.startsWith('/exchanges') || effectiveSelectedPath.startsWith('/dashboard')}
|
|
>
|
|
<SidebarTree
|
|
nodes={appNodes}
|
|
selectedPath={effectiveSelectedPath}
|
|
isStarred={isStarred}
|
|
onToggleStar={toggleStar}
|
|
filterQuery={filterQuery}
|
|
persistKey="cameleer:expanded:apps"
|
|
autoRevealPath={sidebarRevealPath}
|
|
onNavigate={handleSidebarNavigate}
|
|
/>
|
|
</Sidebar.Section>
|
|
|
|
<Sidebar.Section
|
|
label="Agents"
|
|
icon={<Cpu size={14} />}
|
|
open={agentsOpen}
|
|
onToggle={() => { toggleAgents(); if (isAdminPage) handleSidebarNavigate('/agents'); }}
|
|
active={effectiveSelectedPath.startsWith('/agents') || effectiveSelectedPath.startsWith('/runtime')}
|
|
>
|
|
<SidebarTree
|
|
nodes={agentNodes}
|
|
selectedPath={effectiveSelectedPath}
|
|
isStarred={isStarred}
|
|
onToggleStar={toggleStar}
|
|
filterQuery={filterQuery}
|
|
persistKey="cameleer:expanded:agents"
|
|
autoRevealPath={sidebarRevealPath}
|
|
onNavigate={handleSidebarNavigate}
|
|
/>
|
|
</Sidebar.Section>
|
|
|
|
<Sidebar.Section
|
|
label="Routes"
|
|
icon={<GitBranch size={14} />}
|
|
open={routesOpen}
|
|
onToggle={() => { toggleRoutes(); if (isAdminPage) handleSidebarNavigate('/routes'); }}
|
|
active={effectiveSelectedPath.startsWith('/routes')}
|
|
>
|
|
<SidebarTree
|
|
nodes={routeNodes}
|
|
selectedPath={effectiveSelectedPath}
|
|
isStarred={isStarred}
|
|
onToggleStar={toggleStar}
|
|
filterQuery={filterQuery}
|
|
persistKey="cameleer:expanded:routes"
|
|
autoRevealPath={sidebarRevealPath}
|
|
onNavigate={handleSidebarNavigate}
|
|
/>
|
|
</Sidebar.Section>
|
|
|
|
{!isAdminPage && (
|
|
<Sidebar.Section
|
|
label="Admin"
|
|
icon={<Settings size={14} />}
|
|
open={adminOpen}
|
|
onToggle={() => { toggleAdmin(); handleSidebarNavigate('/admin'); }}
|
|
active={false}
|
|
>
|
|
<SidebarTree
|
|
nodes={adminNodes}
|
|
selectedPath={effectiveSelectedPath}
|
|
filterQuery={filterQuery}
|
|
onNavigate={handleSidebarNavigate}
|
|
/>
|
|
</Sidebar.Section>
|
|
)}
|
|
|
|
<Sidebar.Footer>
|
|
<Sidebar.FooterLink
|
|
icon={<FileText size={14} />}
|
|
label="API Docs"
|
|
onClick={() => handleSidebarNavigate('/api-docs')}
|
|
active={location.pathname === '/api-docs'}
|
|
/>
|
|
</Sidebar.Footer>
|
|
</Sidebar>
|
|
}
|
|
>
|
|
<TopBar
|
|
breadcrumb={breadcrumb}
|
|
user={username ? { name: username } : undefined}
|
|
onLogout={handleLogout}
|
|
/>
|
|
<CommandPalette
|
|
open={paletteOpen}
|
|
onClose={() => setPaletteOpen(false)}
|
|
onOpen={() => setPaletteOpen(true)}
|
|
onSelect={handlePaletteSelect}
|
|
onSubmit={handlePaletteSubmit}
|
|
onQueryChange={setPaletteQuery}
|
|
data={searchData}
|
|
/>
|
|
|
|
{!isAdminPage && (
|
|
<ContentTabs active={scope.tab} onChange={setTab} scope={scope} />
|
|
)}
|
|
|
|
<main style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minHeight: 0, padding: isAdminPage ? '1.5rem' : 0 }}>
|
|
<Outlet />
|
|
</main>
|
|
</AppShell>
|
|
);
|
|
```
|
|
|
|
Note: The Admin section renders in two positions — at the top when `isAdminPage` (accordion mode), at the bottom when not (collapsed section). Only one renders at a time via the conditional.
|
|
|
|
- [ ] **Step 5: Verify it compiles**
|
|
|
|
Run: `cd C:/Users/Hendrik/Documents/projects/cameleer-server/ui && npx tsc --project tsconfig.app.json --noEmit`
|
|
Expected: No errors. If `StatusDot` is not exported from the DS, check the exact export name with `grep -r "StatusDot" ui/node_modules/@cameleer/design-system/dist/index.es.d.ts`.
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```bash
|
|
git add ui/src/components/LayoutShell.tsx
|
|
git commit -m "feat(#112): migrate to composable sidebar with accordion and collapse"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: Simplify AdminLayout
|
|
|
|
**Files:**
|
|
- Modify: `ui/src/pages/Admin/AdminLayout.tsx`
|
|
|
|
The sidebar now handles admin sub-page navigation, so `AdminLayout` no longer needs its own `<Tabs>`.
|
|
|
|
- [ ] **Step 1: Rewrite AdminLayout**
|
|
|
|
Replace the full contents of `AdminLayout.tsx`:
|
|
|
|
```typescript
|
|
import { Outlet } from 'react-router';
|
|
|
|
export default function AdminLayout() {
|
|
return (
|
|
<div style={{ padding: '20px 24px 40px' }}>
|
|
<Outlet />
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Verify it compiles**
|
|
|
|
Run: `cd C:/Users/Hendrik/Documents/projects/cameleer-server/ui && npx tsc --project tsconfig.app.json --noEmit`
|
|
Expected: No errors.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add ui/src/pages/Admin/AdminLayout.tsx
|
|
git commit -m "feat(#112): remove admin tabs, sidebar handles navigation"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: Visual Verification
|
|
|
|
- [ ] **Step 1: Verify operational mode**
|
|
|
|
Open `http://localhost:5173/exchanges` and verify:
|
|
1. Sidebar shows all 4 sections: Applications, Agents, Routes, Admin
|
|
2. Applications and Agents expanded by default, Routes and Admin collapsed
|
|
3. Sidebar search filters tree items
|
|
4. Clicking an app navigates to the exchanges page for that app
|
|
5. TopBar, ContentTabs, CommandPalette all work normally
|
|
6. Star/unstar items work
|
|
|
|
- [ ] **Step 2: Verify sidebar collapse**
|
|
|
|
Click the `<<` toggle in sidebar header:
|
|
1. Sidebar collapses to ~48px icon rail
|
|
2. Section icons visible (Box, Cpu, GitBranch, Settings)
|
|
3. Footer link icon visible (FileText for API Docs)
|
|
4. Click any section icon — sidebar expands and that section opens
|
|
|
|
- [ ] **Step 3: Verify admin accordion**
|
|
|
|
Navigate to `/admin/rbac` (click Admin section in sidebar or navigate directly):
|
|
1. Admin section appears at top of sidebar, expanded, showing 6 sub-pages
|
|
2. Applications, Agents, Routes sections are collapsed to single-line headers
|
|
3. Admin sub-page items show active highlighting for current page
|
|
4. No admin tabs visible in content area (just content with padding)
|
|
5. Clicking between admin sub-pages (e.g., Audit Log, OIDC) works via sidebar
|
|
|
|
- [ ] **Step 4: Verify leaving admin**
|
|
|
|
From an admin page, click "Applications" section header:
|
|
1. Navigates to `/exchanges` (or last operational tab)
|
|
2. Admin section collapses
|
|
3. Operational sections restore their previous open/closed states
|
|
|
|
- [ ] **Step 5: Final commit if any fixes were needed**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "fix(#112): sidebar migration adjustments from visual review"
|
|
```
|