fix: expose exchange body in API, fix RouteFlow index mapping
Some checks failed
CI / build (push) Failing after 25s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped

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:
hsiegeln
2026-03-25 22:02:26 +01:00
parent 9b63443842
commit b3c5e87230
3 changed files with 87 additions and 5 deletions

View File

@@ -25,7 +25,9 @@ public class DetailService {
exec.durationMs() != null ? exec.durationMs() : 0L,
exec.correlationId(), exec.exchangeId(),
exec.errorMessage(), exec.errorStacktrace(),
exec.diagramContentHash(), roots
exec.diagramContentHash(), roots,
exec.inputBody(), exec.outputBody(),
exec.inputHeaders(), exec.outputHeaders()
);
});
}

View File

@@ -22,6 +22,10 @@ import java.util.List;
* @param errorStackTrace error stack trace (empty string if no error)
* @param diagramContentHash content hash linking to the active route diagram version
* @param processors nested processor execution tree (root nodes)
* @param inputBody exchange input body at route entry (null if not captured)
* @param outputBody exchange output body at route exit (null if not captured)
* @param inputHeaders exchange input headers at route entry (null if not captured)
* @param outputHeaders exchange output headers at route exit (null if not captured)
*/
public record ExecutionDetail(
String executionId,
@@ -37,6 +41,10 @@ public record ExecutionDetail(
String errorMessage,
String errorStackTrace,
String diagramContentHash,
List<ProcessorNode> processors
List<ProcessorNode> processors,
String inputBody,
String outputBody,
String inputHeaders,
String outputHeaders
) {
}

View File

@@ -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}>&rarr;</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}>&larr;</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}>