fix: expose exchange body in API, fix RouteFlow index mapping
Add inputBody/outputBody/inputHeaders/outputHeaders to ExecutionDetail DTO so exchange-level bodies are returned by the detail endpoint. Show "Exchange Input" and "Exchange Output" panels on the detail page when the data is available. Fix RouteFlow node click selecting the wrong processor snapshot by building a flowToTreeIndex mapping that correctly translates flow display index → diagram node index → processorId → processor tree index. Previously the diagram node index was used directly as the processor tree index, which broke when the two orderings differed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -171,7 +171,7 @@ export default function ExchangeDetail() {
|
||||
return ids
|
||||
}, [procList])
|
||||
|
||||
// ProcessorId lookup: flow node index → processorId (diagram order)
|
||||
// ProcessorId lookup: diagram node index → processorId
|
||||
const flowProcessorIds: string[] = useMemo(() => {
|
||||
if (!diagram?.nodes) return processorIds
|
||||
const flatProcs: Array<{ diagramNodeId?: string; processorId?: string }> = []
|
||||
@@ -188,6 +188,15 @@ export default function ExchangeDetail() {
|
||||
return diagram.nodes.map(node => lookup.get(node.id ?? '') ?? node.id ?? '')
|
||||
}, [diagram, procList, processorIds])
|
||||
|
||||
// Map flow display index → processor tree index (for snapshot API)
|
||||
const flowToTreeIndex = useMemo(() =>
|
||||
flowIndexMap.map(diagramIdx => {
|
||||
const pid = flowProcessorIds[diagramIdx]
|
||||
return pid ? processorIds.indexOf(pid) : -1
|
||||
}),
|
||||
[flowIndexMap, flowProcessorIds, processorIds],
|
||||
)
|
||||
|
||||
// ── Tracing toggle ──────────────────────────────────────────────────────
|
||||
const { toast } = useToast()
|
||||
const tracingStore = useTracingStore()
|
||||
@@ -395,8 +404,11 @@ export default function ExchangeDetail() {
|
||||
routeFlows.length > 0 ? (
|
||||
<RouteFlow
|
||||
flows={routeFlows}
|
||||
onNodeClick={(_node, index) => setSelectedProcessorIndex(flowIndexMap[index] ?? index)}
|
||||
selectedIndex={flowIndexMap.indexOf(activeIndex)}
|
||||
onNodeClick={(_node, index) => {
|
||||
const treeIdx = flowToTreeIndex[index]
|
||||
if (treeIdx >= 0) setSelectedProcessorIndex(treeIdx)
|
||||
}}
|
||||
selectedIndex={flowToTreeIndex.indexOf(activeIndex)}
|
||||
getActions={(_node, index) => {
|
||||
const origIdx = flowIndexMap[index] ?? index
|
||||
const pid = flowProcessorIds[origIdx]
|
||||
@@ -415,6 +427,66 @@ export default function ExchangeDetail() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Exchange-level body (start/end of route) */}
|
||||
{detail && (detail.inputBody || detail.outputBody) && (
|
||||
<div className={styles.detailSplit}>
|
||||
<div className={styles.detailPanel}>
|
||||
<div className={styles.panelHeader}>
|
||||
<span className={styles.panelTitle}>
|
||||
<span className={styles.arrowIn}>→</span> Exchange Input
|
||||
</span>
|
||||
<span className={styles.panelTag}>at route entry</span>
|
||||
</div>
|
||||
<div className={styles.panelBody}>
|
||||
{detail.inputHeaders && (
|
||||
<div className={styles.headersSection}>
|
||||
<div className={styles.sectionLabel}>Headers</div>
|
||||
<div className={styles.headerList}>
|
||||
{Object.entries(parseHeaders(detail.inputHeaders)).map(([key, value]) => (
|
||||
<div key={key} className={styles.headerKvRow}>
|
||||
<span className={styles.headerKey}>{key}</span>
|
||||
<span className={styles.headerValue}>{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.bodySection}>
|
||||
<div className={styles.sectionLabel}>Body</div>
|
||||
<CodeBlock content={detail.inputBody ?? 'null'} language="json" copyable />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.detailPanel}>
|
||||
<div className={styles.panelHeader}>
|
||||
<span className={styles.panelTitle}>
|
||||
<span className={styles.arrowOut}>←</span> Exchange Output
|
||||
</span>
|
||||
<span className={styles.panelTag}>at route exit</span>
|
||||
</div>
|
||||
<div className={styles.panelBody}>
|
||||
{detail.outputHeaders && (
|
||||
<div className={styles.headersSection}>
|
||||
<div className={styles.sectionLabel}>Headers</div>
|
||||
<div className={styles.headerList}>
|
||||
{Object.entries(parseHeaders(detail.outputHeaders)).map(([key, value]) => (
|
||||
<div key={key} className={styles.headerKvRow}>
|
||||
<span className={styles.headerKey}>{key}</span>
|
||||
<span className={styles.headerValue}>{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.bodySection}>
|
||||
<div className={styles.sectionLabel}>Body</div>
|
||||
<CodeBlock content={detail.outputBody ?? 'null'} language="json" copyable />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Processor Detail Panel (split IN / OUT) */}
|
||||
{selectedProc && snapshot && (
|
||||
<div className={styles.detailSplit}>
|
||||
|
||||
Reference in New Issue
Block a user