refactor: consolidate breadcrumbs to single TopBar instance
All checks were successful
CI / build (push) Successful in 1m1s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 1m11s
CI / deploy (push) Successful in 35s
CI / deploy-feature (push) Has been skipped

Remove duplicate in-page breadcrumbs (ExchangeDetail, AgentHealth scope
trail) and improve the global TopBar breadcrumb with semantic labels and
a context-based override for pages with richer navigation data.

- Add BreadcrumbProvider from design system v0.1.12
- LayoutShell: label map prettifies URL segments (apps→Applications, etc.)
- ExchangeDetail: uses useBreadcrumb() to set semantic trail via context
- AgentHealth: remove scope trail, keep live-count badge standalone

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-26 11:40:37 +01:00
parent bde0459416
commit 479b67cd2d
6 changed files with 37 additions and 63 deletions

8
ui/package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "ui",
"version": "0.0.0",
"dependencies": {
"@cameleer/design-system": "^0.1.11",
"@cameleer/design-system": "^0.1.12",
"@tanstack/react-query": "^5.90.21",
"openapi-fetch": "^0.17.0",
"react": "^19.2.4",
@@ -276,9 +276,9 @@
}
},
"node_modules/@cameleer/design-system": {
"version": "0.1.11",
"resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.11/design-system-0.1.11.tgz",
"integrity": "sha512-u32cvvxOSwdDkL3WCiHjMZmdT+KxEcVWEYsg0zpm7CZmbwE98mlW1jZmIw7LRHLfdhb5jH6METRrsGsN6ke44g==",
"version": "0.1.12",
"resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.12/design-system-0.1.12.tgz",
"integrity": "sha512-7qXwa5UMzkN7OHCP+gVJYE53eeo/F2PYzt2XcmnsQpsoYcqlNdxmaf9Btl7wgkl1g5MbKGUxUlnlqAUkUbusAw==",
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",

View File

@@ -14,7 +14,7 @@
"generate-api:live": "curl -s http://localhost:8081/api/v1/api-docs -o src/api/openapi.json && openapi-typescript src/api/openapi.json -o src/api/schema.d.ts"
},
"dependencies": {
"@cameleer/design-system": "^0.1.11",
"@cameleer/design-system": "^0.1.12",
"@tanstack/react-query": "^5.90.21",
"openapi-fetch": "^0.17.0",
"react": "^19.2.4",

View File

@@ -1,5 +1,5 @@
import { Outlet, useNavigate, useLocation } from 'react-router';
import { AppShell, Sidebar, TopBar, CommandPalette, CommandPaletteProvider, GlobalFilterProvider, ToastProvider, useCommandPalette } from '@cameleer/design-system';
import { AppShell, Sidebar, TopBar, CommandPalette, CommandPaletteProvider, GlobalFilterProvider, ToastProvider, BreadcrumbProvider, useCommandPalette } from '@cameleer/design-system';
import type { SidebarApp, SearchResult } from '@cameleer/design-system';
import { useRouteCatalog } from '../api/queries/catalog';
import { useAgents } from '../api/queries/agents';
@@ -143,10 +143,23 @@ function LayoutContent() {
}, [catalogData, exchangeResults]);
const breadcrumb = useMemo(() => {
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',
database: 'Database',
opensearch: 'OpenSearch',
};
const parts = location.pathname.split('/').filter(Boolean);
return parts.map((part, i) => ({
label: part,
href: '/' + parts.slice(0, i + 1).join('/'),
label: LABELS[part] ?? part,
...(i < parts.length - 1 ? { href: '/' + parts.slice(0, i + 1).join('/') } : {}),
}));
}, [location.pathname]);
@@ -195,7 +208,9 @@ export function LayoutShell() {
<ToastProvider>
<CommandPaletteProvider>
<GlobalFilterProvider>
<LayoutContent />
<BreadcrumbProvider>
<LayoutContent />
</BreadcrumbProvider>
</GlobalFilterProvider>
</CommandPaletteProvider>
</ToastProvider>

View File

@@ -31,35 +31,6 @@
.routesWarning { color: var(--warning); }
.routesError { color: var(--error); }
/* Scope breadcrumb trail */
.scopeTrail {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 12px;
font-size: 12px;
}
.scopeLink {
color: var(--amber);
text-decoration: none;
font-weight: 500;
}
.scopeLink:hover {
text-decoration: underline;
}
.scopeSep {
color: var(--text-muted);
font-size: 10px;
}
.scopeCurrent {
color: var(--text-primary);
font-weight: 600;
font-family: var(--font-mono);
}
/* Section header */
.sectionTitle {

View File

@@ -1,5 +1,5 @@
import { useState, useMemo } from 'react';
import { useParams, Link } from 'react-router';
import { useParams } from 'react-router';
import {
StatCard, StatusDot, Badge, MonoText, ProgressBar,
GroupCard, DataTable, LineChart, EventFeed, DetailPanel,
@@ -463,15 +463,7 @@ export default function AgentHealth() {
/>
</div>
{/* Scope trail + badges */}
<div className={styles.scopeTrail}>
{appId && (
<>
<Link to="/agents" className={styles.scopeLink}>All Agents</Link>
<span className={styles.scopeSep}>&#9656;</span>
<span className={styles.scopeCurrent}>{appId}</span>
</>
)}
<div style={{ marginBottom: 12 }}>
<Badge
label={`${liveCount}/${totalInstances} live`}
color={deadCount > 0 ? 'error' : staleCount > 0 ? 'warning' : 'success'}

View File

@@ -2,8 +2,8 @@ import { useState, useMemo, useCallback, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router'
import {
Badge, StatusDot, MonoText, CodeBlock, InfoCallout,
ProcessorTimeline, Breadcrumb, Spinner, RouteFlow, useToast,
LogViewer, ButtonGroup, SectionHeader,
ProcessorTimeline, Spinner, RouteFlow, useToast,
LogViewer, ButtonGroup, SectionHeader, useBreadcrumb,
} from '@cameleer/design-system'
import type { ProcessorStep, RouteNode, NodeBadge, LogEntry, ButtonGroupItem } from '@cameleer/design-system'
import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions'
@@ -256,6 +256,15 @@ export default function ExchangeDetail() {
return correlationData.data
}, [correlationData])
// Set semantic breadcrumb in TopBar when detail is loaded
const breadcrumbItems = useMemo(() => detail ? [
{ label: 'Applications', href: '/apps' },
{ label: detail.applicationName || 'App', href: `/apps/${detail.applicationName}` },
{ label: detail.routeId, href: `/apps/${detail.applicationName}/${detail.routeId}` },
{ label: detail.executionId?.slice(0, 12) || '' },
] : null, [detail?.applicationName, detail?.routeId, detail?.executionId])
useBreadcrumb(breadcrumbItems)
// Exchange logs from OpenSearch (filtered by exchangeId via MDC)
const { data: rawLogs } = useApplicationLogs(
detail?.applicationName,
@@ -288,11 +297,6 @@ export default function ExchangeDetail() {
if (!detail) {
return (
<div className={styles.content}>
<Breadcrumb items={[
{ label: 'Applications', href: '/apps' },
{ label: 'Exchanges' },
{ label: id ?? 'Unknown' },
]} />
<InfoCallout variant="warning">Exchange &quot;{id}&quot; not found.</InfoCallout>
</div>
)
@@ -304,14 +308,6 @@ export default function ExchangeDetail() {
return (
<div className={styles.content}>
{/* Breadcrumb */}
<Breadcrumb items={[
{ label: 'Applications', href: '/apps' },
{ label: detail.applicationName || 'App', href: `/apps/${detail.applicationName}` },
{ label: detail.routeId, href: `/apps/${detail.applicationName}/${detail.routeId}` },
{ label: detail.executionId?.slice(0, 12) || '' },
]} />
{/* Exchange header card */}
<div className={styles.exchangeHeader}>
<div className={styles.headerRow}>