);
}
```
- [ ] **Step 3: Verify build**
Run: `cd ui && npx tsc --noEmit`
Fix any type errors. Common issues:
- `useDiagramByRoute` might need different params — check `ui/src/api/queries/diagrams.ts`
- `ExecutionSummary` import path — check `ui/src/api/types.ts`
- [ ] **Step 4: Commit**
```bash
git add ui/src/pages/Exchanges/
git commit -m "feat(ui): add ExchangesPage with full-width and 3-column modes"
```
---
## Task 7: Create RuntimePage and DashboardPage wrappers
Thin wrappers that render the existing page components based on URL params.
**Files:**
- Create: `ui/src/pages/RuntimeTab/RuntimePage.tsx`
- Create: `ui/src/pages/DashboardTab/DashboardPage.tsx`
- [ ] **Step 1: Create RuntimePage**
```typescript
// ui/src/pages/RuntimeTab/RuntimePage.tsx
import { useParams } from 'react-router';
import { lazy, Suspense } from 'react';
import { Spinner } from '@cameleer/design-system';
const AgentHealth = lazy(() => import('../AgentHealth/AgentHealth'));
const AgentInstance = lazy(() => import('../AgentInstance/AgentInstance'));
const Fallback =
;
export default function RuntimePage() {
const { instanceId } = useParams<{ appId?: string; instanceId?: string }>();
// If instanceId is present, show agent instance detail; otherwise show agent health overview
if (instanceId) {
return ;
}
return ;
}
```
- [ ] **Step 2: Create DashboardPage**
```typescript
// ui/src/pages/DashboardTab/DashboardPage.tsx
import { useParams } from 'react-router';
import { lazy, Suspense } from 'react';
import { Spinner } from '@cameleer/design-system';
const RoutesMetrics = lazy(() => import('../Routes/RoutesMetrics'));
const RouteDetail = lazy(() => import('../Routes/RouteDetail'));
const Fallback =
;
export default function DashboardPage() {
const { routeId } = useParams<{ appId?: string; routeId?: string }>();
// If routeId is present, show route detail; otherwise show routes metrics overview
if (routeId) {
return ;
}
return ;
}
```
- [ ] **Step 3: Verify build**
Run: `cd ui && npx tsc --noEmit`
- [ ] **Step 4: Commit**
```bash
git add ui/src/pages/RuntimeTab/ ui/src/pages/DashboardTab/
git commit -m "feat(ui): add RuntimePage and DashboardPage tab wrappers"
```
---
## Task 8: Update router with new URL structure
Replace the old page-based routes with the new tab-based structure.
**Files:**
- Modify: `ui/src/router.tsx`
- [ ] **Step 1: Rewrite router.tsx**
Replace the content of `ui/src/router.tsx` with:
```typescript
// ui/src/router.tsx
import { createBrowserRouter, Navigate, useParams } from 'react-router';
import { ProtectedRoute } from './auth/ProtectedRoute';
import { LoginPage } from './auth/LoginPage';
import { OidcCallback } from './auth/OidcCallback';
import { LayoutShell } from './components/LayoutShell';
import { lazy, Suspense } from 'react';
import { Spinner } from '@cameleer/design-system';
const ExchangesPage = lazy(() => import('./pages/Exchanges/ExchangesPage'));
const DashboardPage = lazy(() => import('./pages/DashboardTab/DashboardPage'));
const RuntimePage = lazy(() => import('./pages/RuntimeTab/RuntimePage'));
const AdminLayout = lazy(() => import('./pages/Admin/AdminLayout'));
const RbacPage = lazy(() => import('./pages/Admin/RbacPage'));
const AuditLogPage = lazy(() => import('./pages/Admin/AuditLogPage'));
const OidcConfigPage = lazy(() => import('./pages/Admin/OidcConfigPage'));
const DatabaseAdminPage = lazy(() => import('./pages/Admin/DatabaseAdminPage'));
const OpenSearchAdminPage = lazy(() => import('./pages/Admin/OpenSearchAdminPage'));
const AppConfigPage = lazy(() => import('./pages/Admin/AppConfigPage'));
const SwaggerPage = lazy(() => import('./pages/Swagger/SwaggerPage'));
function SuspenseWrapper({ children }: { children: React.ReactNode }) {
return (
}>
{children}
);
}
export const router = createBrowserRouter([
{ path: '/login', element: },
{ path: '/oidc/callback', element: },
{
element: ,
children: [
{
element: ,
children: [
// Default redirect
{ index: true, element: },
// Exchanges tab
{ path: 'exchanges', element: },
{ path: 'exchanges/:appId', element: },
{ path: 'exchanges/:appId/:routeId', element: },
{ path: 'exchanges/:appId/:routeId/:exchangeId', element: },
// Dashboard tab
{ path: 'dashboard', element: },
{ path: 'dashboard/:appId', element: },
{ path: 'dashboard/:appId/:routeId', element: },
// Runtime tab
{ path: 'runtime', element: },
{ path: 'runtime/:appId', element: },
{ path: 'runtime/:appId/:instanceId', element: },
// Legacy redirects (sidebar uses /apps/... and /agents/... paths)
{ path: 'apps', element: },
{ path: 'apps/:appId', element: },
{ path: 'apps/:appId/:routeId', element: },
{ path: 'agents', element: },
{ path: 'agents/:appId', element: },
{ path: 'agents/:appId/:instanceId', element: },
// Old exchange detail redirect
{ path: 'exchanges-old/:id', element: },
// Admin (unchanged)
{
path: 'admin',
element: ,
children: [
{ index: true, element: },
{ path: 'rbac', element: },
{ path: 'audit', element: },
{ path: 'oidc', element: },
{ path: 'appconfig', element: },
{ path: 'database', element: },
{ path: 'opensearch', element: },
],
},
{ path: 'api-docs', element: },
],
},
],
},
]);
// Legacy redirect components — translate old sidebar paths to current tab
// (useParams is already imported at the top of this file)
function LegacyAppRedirect() {
const { appId, routeId } = useParams<{ appId: string; routeId?: string }>();
const path = routeId ? `/exchanges/${appId}/${routeId}` : `/exchanges/${appId}`;
return ;
}
function LegacyAgentRedirect() {
const { appId, instanceId } = useParams<{ appId: string; instanceId?: string }>();
const path = instanceId ? `/runtime/${appId}/${instanceId}` : `/runtime/${appId}`;
return ;
}
```
The `useParams` import from `react-router` is already at the top alongside the other router imports. The legacy redirects ensure the Sidebar's hardcoded `/apps/...` and `/agents/...` Links still work (they redirect to the Exchanges tab by default).
- [ ] **Step 2: Verify build**
Run: `cd ui && npx tsc --noEmit`
- [ ] **Step 3: Commit**
```bash
git add ui/src/router.tsx
git commit -m "feat(ui): restructure router for tab-based navigation with legacy redirects"
```
---
## Task 9: Update LayoutShell
Wire ContentTabs, ScopeTrail, sidebar interception, and remove agents from sidebar data.
**Files:**
- Modify: `ui/src/components/LayoutShell.tsx`
- [ ] **Step 1: Update imports**
Add these imports to the top of `LayoutShell.tsx`:
```typescript
import { ContentTabs } from './ContentTabs';
import { ScopeTrail } from './ScopeTrail';
import { useScope } from '../hooks/useScope';
```
- [ ] **Step 2: Remove agents from sidebar data**
In the `sidebarApps` useMemo (around line 106), change the agents mapping to always return an empty array:
Replace:
```typescript
agents: (app.agents || []).map((a: any) => ({
id: a.id,
name: a.name,
status: a.status as 'live' | 'stale' | 'dead',
tps: a.tps,
})),
```
With:
```typescript
agents: [],
```
- [ ] **Step 3: Add scope + sidebar interception to LayoutContent**
At the top of the `LayoutContent` function (after existing hooks around line 92), add:
```typescript
const { scope, setTab, clearScope } = useScope();
```
Add the sidebar click interceptor function (before the return statement):
```typescript
// Intercept Sidebar's internal navigation to re-route through current tab
const handleSidebarClick = useCallback((e: React.MouseEvent) => {
const anchor = (e.target as HTMLElement).closest('a[href]');
if (!anchor) return;
const href = anchor.getAttribute('href') || '';
// Intercept /apps/:appId and /apps/:appId/:routeId links
const appMatch = href.match(/^\/apps\/([^/]+)(?:\/(.+))?$/);
if (appMatch) {
e.preventDefault();
const [, sAppId, sRouteId] = appMatch;
navigate(sRouteId ? `/${scope.tab}/${sAppId}/${sRouteId}` : `/${scope.tab}/${sAppId}`);
return;
}
// Intercept /agents/* links — redirect to runtime tab
const agentMatch = href.match(/^\/agents\/([^/]+)(?:\/(.+))?$/);
if (agentMatch) {
e.preventDefault();
const [, sAppId, sInstanceId] = agentMatch;
navigate(sInstanceId ? `/runtime/${sAppId}/${sInstanceId}` : `/runtime/${sAppId}`);
}
}, [navigate, scope.tab]);
```
- [ ] **Step 4: Replace breadcrumbs with ScopeTrail**
Replace the existing `breadcrumb` useMemo block (lines 168-188) with:
```typescript
// Breadcrumb is now the ScopeTrail — built from scope, not URL path
// Keep the old breadcrumb generation for admin pages only
const isAdminPage = location.pathname.startsWith('/admin');
const breadcrumb = useMemo(() => {
if (!isAdminPage) return []; // ScopeTrail handles non-admin breadcrumbs
const LABELS: Record = {
admin: 'Admin',
rbac: 'Users & Roles',
audit: 'Audit Log',
oidc: 'OIDC',
database: 'Database',
opensearch: 'OpenSearch',
appconfig: 'App Config',
};
const parts = location.pathname.split('/').filter(Boolean);
return parts.map((part, i) => ({
label: LABELS[part] ?? part,
...(i < parts.length - 1 ? { href: '/' + parts.slice(0, i + 1).join('/') } : {}),
}));
}, [location.pathname, isAdminPage]);
```
- [ ] **Step 5: Update CommandPalette submit handler**
Replace the `handlePaletteSubmit` callback (around line 202) so it uses the Exchanges tab path:
```typescript
const handlePaletteSubmit = useCallback((query: string) => {
// Full-text search: navigate to Exchanges tab with text param
const baseParts = [`/exchanges`];
if (scope.appId) baseParts.push(scope.appId);
if (scope.routeId) baseParts.push(scope.routeId);
navigate(`${baseParts.join('/')}?text=${encodeURIComponent(query)}`);
}, [navigate, scope.appId, scope.routeId]);
```
- [ ] **Step 6: Update the search result paths in buildSearchData**
In `buildSearchData` function, update the `path` values for apps and routes:
Replace:
```typescript
path: `/apps/${app.appId}`,
```
With:
```typescript
path: `/exchanges/${app.appId}`,
```
Replace:
```typescript
path: `/apps/${app.appId}/${route.routeId}`,
```
With:
```typescript
path: `/exchanges/${app.appId}/${route.routeId}`,
```
Replace:
```typescript
path: `/agents/${agent.application}/${agent.id}`,
```
With:
```typescript
path: `/runtime/${agent.application}/${agent.id}`,
```
- [ ] **Step 7: Update exchange search result paths**
In the `searchData` useMemo (around line 132), update the exchange path:
Replace:
```typescript
path: `/exchanges/${e.executionId}`,
```
With a path that includes the app and route for proper 3-column view:
```typescript
path: `/exchanges/${e.applicationName ?? ''}/${e.routeId}/${e.executionId}`,
```
Do the same for `attributeItems` path.
- [ ] **Step 8: Update the JSX return**
Replace the return block of `LayoutContent` with:
```typescript
return (
}
>
setPaletteOpen(false)}
onOpen={() => setPaletteOpen(true)}
onSelect={handlePaletteSelect}
onSubmit={handlePaletteSubmit}
onQueryChange={setPaletteQuery}
data={searchData}
/>
{/* Content tabs + scope trail — only for main content, not admin */}
{!isAdminPage && (
<>
navigate(path)} />
>
)}
);
```
- [ ] **Step 9: Verify build**
Run: `cd ui && npx tsc --noEmit`
Fix any type errors. Then run the dev server:
Run: `cd ui && npm run dev`
Visually verify:
- Tab bar appears below TopBar with Exchanges | Dashboard | Runtime
- Scope trail shows "All Applications" by default
- Clicking an app in sidebar scopes to that app and stays on current tab
- Clicking a route transitions to 3-column layout (Exchanges tab)
- Clicking Dashboard or Runtime tabs shows the correct content
- Admin pages still work without tabs/scope trail
- [ ] **Step 10: Commit**
```bash
git add ui/src/components/LayoutShell.tsx
git commit -m "feat(ui): integrate ContentTabs, ScopeTrail, and sidebar scope interception"
```
---
## Task 10: Final cleanup and verification
**Files:**
- Modify: `ui/src/pages/Dashboard/Dashboard.tsx` (update inspect link path)
- [ ] **Step 1: Update Dashboard inspect link**
In `Dashboard.tsx`, the inspect button navigates to `/exchanges/:id` (the old exchange detail page). Update it to stay in the current scope. Find the inspect column render (around line 347):
Replace:
```typescript
navigate(`/exchanges/${row.executionId}`)
```
With:
```typescript
navigate(`/exchanges/${row.applicationName}/${row.routeId}/${row.executionId}`)
```
This navigates to the 3-column view with the exchange pre-selected.
- [ ] **Step 2: Update Dashboard "Open full details" link**
In the detail panel section (around line 465):
Replace:
```typescript
onClick={() => navigate(`/exchanges/${detail.executionId}`)}
```
With:
```typescript
onClick={() => navigate(`/exchanges/${detail.applicationName}/${detail.routeId}/${detail.executionId}`)}
```
- [ ] **Step 3: Verify all navigation flows**
Run the dev server and verify:
```bash
cd ui && npm run dev
```
Verification checklist:
1. `/exchanges` — Shows full-width exchange table with KPI strip
2. Click app in sidebar — URL updates to `/exchanges/:appId`, table filters
3. Click route in sidebar — URL updates to `/exchanges/:appId/:routeId`, 3-column layout appears
4. Click exchange in left list — Right panel shows exchange header + diagram + details
5. Click "Dashboard" tab — URL changes to `/dashboard/:appId/:routeId` (scope preserved)
6. Dashboard tab shows RoutesMetrics (no route) or RouteDetail (with route)
7. Click "Runtime" tab — URL changes to `/runtime`, shows AgentHealth
8. Click agent in Runtime content — Shows AgentInstance detail
9. Scope trail segments are clickable and navigate correctly
10. Cmd+K search works, results navigate to correct new URLs
11. Admin pages (gear icon or /admin) still work with breadcrumbs, no tabs
12. Sidebar shows apps with routes only, no agents section
- [ ] **Step 4: Verify production build**
Run: `cd ui && npm run build`
Expected: Clean build with no errors
- [ ] **Step 5: Commit**
```bash
git add -A
git commit -m "feat(ui): complete navigation redesign - tab-based layout with scope filtering
Redesigns navigation from page-based routing to scope-based model:
- Three content tabs: Exchanges, Dashboard, Runtime
- Sidebar simplified to app/route hierarchy (scope filter)
- Scope trail replaces breadcrumbs
- Exchanges tab: full-width table or 3-column layout with diagram
- Legacy URL redirects for backward compatibility"
```
---
## Known Limitations and Future Work
1. **Sidebar design system update needed:** The click interception via event delegation is a workaround. A proper `onNavigate` prop should be added to the design system's `Sidebar` component.
2. **Dashboard tab content:** The analytics/dashboard content is deferred per spec. Currently wraps existing RoutesMetrics and RouteDetail pages.
3. **ExchangeDetail page:** The old full-page ExchangeDetail at `/exchanges/:id` is replaced by the inline 3-column view. The ExchangeDetail component file is not deleted — it may be useful for reference. Clean up when confident the new view covers all use cases.
4. **Exchange header:** Uses inline styles for brevity. Extract to CSS module if it grows.
5. **KPI hero per tab:** Currently only the Exchanges tab has a KPI strip (from Dashboard). The Dashboard and Runtime tabs will get their own KPI strips in future iterations.
6. **Application log and replay:** These features from ExchangeDetail are accessible through the ExecutionDiagram's detail panel tabs but not directly in the exchange header. A future iteration could add log/replay buttons.