feat(ui): display business attributes on ExchangeDetail page

Show route-level attributes as Badge strips in the exchange header
card, and per-processor attributes above the message IN/OUT panels.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-26 18:33:16 +01:00
parent 2b1d49c032
commit a3706cf7c2
2 changed files with 58 additions and 1 deletions

View File

@@ -168,6 +168,27 @@
font-style: italic;
}
/* ==========================================================================
ATTRIBUTES STRIP
========================================================================== */
.attributesStrip {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
padding: 10px 14px;
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
margin-bottom: 16px;
}
.attributesLabel {
font-size: 11px;
color: var(--text-muted);
margin-right: 4px;
}
/* ==========================================================================
TIMELINE SECTION
========================================================================== */

View File

@@ -4,6 +4,7 @@ import {
Badge, StatusDot, MonoText, CodeBlock, InfoCallout,
ProcessorTimeline, Spinner, RouteFlow, useToast,
LogViewer, ButtonGroup, SectionHeader, useBreadcrumb,
Modal, Tabs, Button, Select, Input, Textarea,
} from '@cameleer/design-system'
import type { ProcessorStep, RouteNode, NodeBadge, LogEntry, ButtonGroupItem } from '@cameleer/design-system'
import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions'
@@ -11,7 +12,8 @@ import { useCorrelationChain } from '../../api/queries/correlation'
import { useDiagramLayout } from '../../api/queries/diagrams'
import { mapDiagramToRouteNodes, toFlowSegments } from '../../utils/diagram-mapping'
import { useTracingStore } from '../../stores/tracing-store'
import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands'
import { useApplicationConfig, useUpdateApplicationConfig, useReplayExchange } from '../../api/queries/commands'
import { useAgents } from '../../api/queries/agents'
import { useApplicationLogs } from '../../api/queries/logs'
import styles from './ExchangeDetail.module.css'
@@ -124,6 +126,17 @@ export default function ExchangeDetail() {
return result
}, [procList, tracedMap])
// Flatten processor tree into raw node objects (for attribute access)
const flatProcNodes = useMemo(() => {
const nodes: any[] = []
function walk(node: any) {
nodes.push(node)
if (node.children) node.children.forEach(walk)
}
procList.forEach(walk)
return nodes
}, [procList])
// Default selected processor: first failed, or 0
const defaultIndex = useMemo(() => {
if (!processors.length) return 0
@@ -359,6 +372,16 @@ export default function ExchangeDetail() {
</div>
</div>
{/* Route-level Attributes */}
{detail.attributes && Object.keys(detail.attributes).length > 0 && (
<div className={styles.attributesStrip}>
<span className={styles.attributesLabel}>Attributes</span>
{Object.entries(detail.attributes).map(([key, value]) => (
<Badge key={key} label={`${key}: ${value}`} color="auto" variant="filled" />
))}
</div>
)}
{/* Correlation Chain */}
{correlatedExchanges.length > 1 && (
<div className={styles.correlationChain}>
@@ -522,6 +545,19 @@ export default function ExchangeDetail() {
</div>
)}
{/* Processor Attributes */}
{selectedProc && (() => {
const procNode = flatProcNodes[activeIndex]
return procNode?.attributes && Object.keys(procNode.attributes).length > 0 ? (
<div className={styles.attributesStrip}>
<span className={styles.attributesLabel}>Processor Attributes</span>
{Object.entries(procNode.attributes).map(([key, value]) => (
<Badge key={key} label={`${key}: ${value}`} color="auto" variant="filled" />
))}
</div>
) : null
})()}
{/* Processor Detail Panel (split IN / OUT) */}
{selectedProc && snapshot && (
<div className={styles.detailSplit}>