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