feat(ui): integrate ContentTabs, ScopeTrail, and sidebar scope interception

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-28 14:01:52 +01:00
parent 66abb1fe3a
commit 4fe418cc89

View File

@@ -6,6 +6,9 @@ import { useAgents } from '../api/queries/agents';
import { useSearchExecutions } from '../api/queries/executions';
import { useAuthStore } from '../auth/auth-store';
import { useState, useMemo, useCallback, useEffect } from 'react';
import { ContentTabs } from './ContentTabs';
import { ScopeTrail } from './ScopeTrail';
import { useScope } from '../hooks/useScope';
function healthToColor(health: string): string {
switch (health) {
@@ -31,7 +34,7 @@ function buildSearchData(
title: app.appId,
badges: [{ label: (app.health || 'unknown').toUpperCase(), color: healthToColor(app.health) }],
meta: `${(app.routes || []).length} routes · ${(app.agents || []).length} agents (${liveAgents} live) · ${(app.exchangeCount ?? 0).toLocaleString()} exchanges`,
path: `/apps/${app.appId}`,
path: `/exchanges/${app.appId}`,
});
for (const route of (app.routes || [])) {
@@ -41,7 +44,7 @@ function buildSearchData(
title: route.routeId,
badges: [{ label: app.appId }],
meta: `${(route.exchangeCount ?? 0).toLocaleString()} exchanges`,
path: `/apps/${app.appId}/${route.routeId}`,
path: `/exchanges/${app.appId}/${route.routeId}`,
});
}
}
@@ -54,7 +57,7 @@ function buildSearchData(
title: agent.name,
badges: [{ label: (agent.state || 'unknown').toUpperCase(), color: healthToColor((agent.state || '').toLowerCase()) }],
meta: `${agent.application} · ${agent.version || ''}${agent.agentTps != null ? ` · ${agent.agentTps.toFixed(1)} msg/s` : ''}`,
path: `/agents/${agent.application}/${agent.id}`,
path: `/runtime/${agent.application}/${agent.id}`,
});
}
}
@@ -94,6 +97,7 @@ function LayoutContent() {
const { data: agents } = useAgents();
const { username, logout } = useAuthStore();
const { open: paletteOpen, setOpen: setPaletteOpen } = useCommandPalette();
const { scope, setTab } = useScope();
// Exchange full-text search via command palette
const [paletteQuery, setPaletteQuery] = useState('');
@@ -115,12 +119,7 @@ function LayoutContent() {
name: r.routeId,
exchangeCount: r.exchangeCount,
})),
agents: (app.agents || []).map((a: any) => ({
id: a.id,
name: a.name,
status: a.status as 'live' | 'stale' | 'dead',
tps: a.tps,
})),
agents: [],
}));
}, [catalog]);
@@ -136,7 +135,7 @@ function LayoutContent() {
title: e.executionId,
badges: [{ label: e.status, color: statusToColor(e.status) }],
meta: `${e.routeId} · ${e.applicationName ?? ''} · ${formatDuration(e.durationMs)}`,
path: `/exchanges/${e.executionId}`,
path: `/exchanges/${e.applicationName ?? ''}/${e.routeId}/${e.executionId}`,
serverFiltered: true,
matchContext: e.highlight ?? undefined,
}));
@@ -154,7 +153,7 @@ function LayoutContent() {
title: `${key} = "${value}"`,
badges: [{ label: e.status, color: statusToColor(e.status) }],
meta: `${e.executionId} · ${e.routeId} · ${e.applicationName ?? ''}`,
path: `/exchanges/${e.executionId}`,
path: `/exchanges/${e.applicationName ?? ''}/${e.routeId}/${e.executionId}`,
serverFiltered: true,
});
}
@@ -165,14 +164,11 @@ function LayoutContent() {
return [...catalogData, ...exchangeItems, ...attributeItems];
}, [catalogData, exchangeResults, debouncedQuery]);
const isAdminPage = location.pathname.startsWith('/admin');
const breadcrumb = useMemo(() => {
if (!isAdminPage) return [];
const LABELS: Record<string, string> = {
apps: 'Applications',
agents: 'Agents',
exchanges: 'Exchanges',
routes: 'Routes',
admin: 'Admin',
'api-docs': 'API Docs',
rbac: 'Users & Roles',
audit: 'Audit Log',
oidc: 'OIDC',
@@ -185,7 +181,7 @@ function LayoutContent() {
label: LABELS[part] ?? part,
...(i < parts.length - 1 ? { href: '/' + parts.slice(0, i + 1).join('/') } : {}),
}));
}, [location.pathname]);
}, [location.pathname, isAdminPage]);
const handleLogout = useCallback(() => {
logout();
@@ -200,19 +196,42 @@ function LayoutContent() {
}, [navigate, setPaletteOpen]);
const handlePaletteSubmit = useCallback((query: string) => {
// Navigate to dashboard with full-text search applied
const currentPath = location.pathname;
// Stay on the current app/route context if we're already there
const basePath = currentPath.startsWith('/apps/') ? currentPath.split('/').slice(0, 4).join('/') : '/apps';
navigate(`${basePath}?text=${encodeURIComponent(query)}`);
}, [navigate, location.pathname]);
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]);
// Intercept Sidebar's internal <Link> 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]);
return (
<AppShell
sidebar={
<Sidebar
apps={sidebarApps}
/>
<div onClick={handleSidebarClick}>
<Sidebar apps={sidebarApps} />
</div>
}
>
<TopBar
@@ -229,7 +248,17 @@ function LayoutContent() {
onQueryChange={setPaletteQuery}
data={searchData}
/>
<main style={{ flex: 1, overflow: 'auto', padding: '1.5rem' }}>
{!isAdminPage && (
<>
<ContentTabs active={scope.tab} onChange={setTab} />
<div style={{ padding: '0 1.5rem', paddingTop: '0.5rem' }}>
<ScopeTrail scope={scope} onNavigate={(path) => navigate(path)} />
</div>
</>
)}
<main style={{ flex: 1, overflow: 'auto', padding: isAdminPage ? '1.5rem' : '0.75rem 1.5rem' }}>
<Outlet />
</main>
</AppShell>