refactor: consolidate breadcrumbs to single TopBar instance
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:
8
ui/package-lock.json
generated
8
ui/package-lock.json
generated
@@ -8,7 +8,7 @@
|
|||||||
"name": "ui",
|
"name": "ui",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cameleer/design-system": "^0.1.11",
|
"@cameleer/design-system": "^0.1.12",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"openapi-fetch": "^0.17.0",
|
"openapi-fetch": "^0.17.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
@@ -276,9 +276,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cameleer/design-system": {
|
"node_modules/@cameleer/design-system": {
|
||||||
"version": "0.1.11",
|
"version": "0.1.12",
|
||||||
"resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.11/design-system-0.1.11.tgz",
|
"resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.12/design-system-0.1.12.tgz",
|
||||||
"integrity": "sha512-u32cvvxOSwdDkL3WCiHjMZmdT+KxEcVWEYsg0zpm7CZmbwE98mlW1jZmIw7LRHLfdhb5jH6METRrsGsN6ke44g==",
|
"integrity": "sha512-7qXwa5UMzkN7OHCP+gVJYE53eeo/F2PYzt2XcmnsQpsoYcqlNdxmaf9Btl7wgkl1g5MbKGUxUlnlqAUkUbusAw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -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"
|
"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": {
|
"dependencies": {
|
||||||
"@cameleer/design-system": "^0.1.11",
|
"@cameleer/design-system": "^0.1.12",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"openapi-fetch": "^0.17.0",
|
"openapi-fetch": "^0.17.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Outlet, useNavigate, useLocation } from 'react-router';
|
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 type { SidebarApp, SearchResult } from '@cameleer/design-system';
|
||||||
import { useRouteCatalog } from '../api/queries/catalog';
|
import { useRouteCatalog } from '../api/queries/catalog';
|
||||||
import { useAgents } from '../api/queries/agents';
|
import { useAgents } from '../api/queries/agents';
|
||||||
@@ -143,10 +143,23 @@ function LayoutContent() {
|
|||||||
}, [catalogData, exchangeResults]);
|
}, [catalogData, exchangeResults]);
|
||||||
|
|
||||||
const breadcrumb = useMemo(() => {
|
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);
|
const parts = location.pathname.split('/').filter(Boolean);
|
||||||
return parts.map((part, i) => ({
|
return parts.map((part, i) => ({
|
||||||
label: part,
|
label: LABELS[part] ?? part,
|
||||||
href: '/' + parts.slice(0, i + 1).join('/'),
|
...(i < parts.length - 1 ? { href: '/' + parts.slice(0, i + 1).join('/') } : {}),
|
||||||
}));
|
}));
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
@@ -195,7 +208,9 @@ export function LayoutShell() {
|
|||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<CommandPaletteProvider>
|
<CommandPaletteProvider>
|
||||||
<GlobalFilterProvider>
|
<GlobalFilterProvider>
|
||||||
<LayoutContent />
|
<BreadcrumbProvider>
|
||||||
|
<LayoutContent />
|
||||||
|
</BreadcrumbProvider>
|
||||||
</GlobalFilterProvider>
|
</GlobalFilterProvider>
|
||||||
</CommandPaletteProvider>
|
</CommandPaletteProvider>
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
|
|||||||
@@ -31,35 +31,6 @@
|
|||||||
.routesWarning { color: var(--warning); }
|
.routesWarning { color: var(--warning); }
|
||||||
.routesError { color: var(--error); }
|
.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 */
|
/* Section header */
|
||||||
.sectionTitle {
|
.sectionTitle {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { useParams, Link } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import {
|
import {
|
||||||
StatCard, StatusDot, Badge, MonoText, ProgressBar,
|
StatCard, StatusDot, Badge, MonoText, ProgressBar,
|
||||||
GroupCard, DataTable, LineChart, EventFeed, DetailPanel,
|
GroupCard, DataTable, LineChart, EventFeed, DetailPanel,
|
||||||
@@ -463,15 +463,7 @@ export default function AgentHealth() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scope trail + badges */}
|
<div style={{ marginBottom: 12 }}>
|
||||||
<div className={styles.scopeTrail}>
|
|
||||||
{appId && (
|
|
||||||
<>
|
|
||||||
<Link to="/agents" className={styles.scopeLink}>All Agents</Link>
|
|
||||||
<span className={styles.scopeSep}>▸</span>
|
|
||||||
<span className={styles.scopeCurrent}>{appId}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Badge
|
<Badge
|
||||||
label={`${liveCount}/${totalInstances} live`}
|
label={`${liveCount}/${totalInstances} live`}
|
||||||
color={deadCount > 0 ? 'error' : staleCount > 0 ? 'warning' : 'success'}
|
color={deadCount > 0 ? 'error' : staleCount > 0 ? 'warning' : 'success'}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { useState, useMemo, useCallback, useEffect } from 'react'
|
|||||||
import { useParams, useNavigate } from 'react-router'
|
import { useParams, useNavigate } from 'react-router'
|
||||||
import {
|
import {
|
||||||
Badge, StatusDot, MonoText, CodeBlock, InfoCallout,
|
Badge, StatusDot, MonoText, CodeBlock, InfoCallout,
|
||||||
ProcessorTimeline, Breadcrumb, Spinner, RouteFlow, useToast,
|
ProcessorTimeline, Spinner, RouteFlow, useToast,
|
||||||
LogViewer, ButtonGroup, SectionHeader,
|
LogViewer, ButtonGroup, SectionHeader, useBreadcrumb,
|
||||||
} from '@cameleer/design-system'
|
} from '@cameleer/design-system'
|
||||||
import type { ProcessorStep, RouteNode, NodeBadge, LogEntry, ButtonGroupItem } from '@cameleer/design-system'
|
import type { ProcessorStep, RouteNode, NodeBadge, LogEntry, ButtonGroupItem } from '@cameleer/design-system'
|
||||||
import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions'
|
import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions'
|
||||||
@@ -256,6 +256,15 @@ export default function ExchangeDetail() {
|
|||||||
return correlationData.data
|
return correlationData.data
|
||||||
}, [correlationData])
|
}, [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)
|
// Exchange logs from OpenSearch (filtered by exchangeId via MDC)
|
||||||
const { data: rawLogs } = useApplicationLogs(
|
const { data: rawLogs } = useApplicationLogs(
|
||||||
detail?.applicationName,
|
detail?.applicationName,
|
||||||
@@ -288,11 +297,6 @@ export default function ExchangeDetail() {
|
|||||||
if (!detail) {
|
if (!detail) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<Breadcrumb items={[
|
|
||||||
{ label: 'Applications', href: '/apps' },
|
|
||||||
{ label: 'Exchanges' },
|
|
||||||
{ label: id ?? 'Unknown' },
|
|
||||||
]} />
|
|
||||||
<InfoCallout variant="warning">Exchange "{id}" not found.</InfoCallout>
|
<InfoCallout variant="warning">Exchange "{id}" not found.</InfoCallout>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -304,14 +308,6 @@ export default function ExchangeDetail() {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.content}>
|
<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 */}
|
{/* Exchange header card */}
|
||||||
<div className={styles.exchangeHeader}>
|
<div className={styles.exchangeHeader}>
|
||||||
<div className={styles.headerRow}>
|
<div className={styles.headerRow}>
|
||||||
|
|||||||
Reference in New Issue
Block a user