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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user