feat: add correlation chain and processor count to Exchange Detail

Adds a recursive processor count stat to the exchange header, and a
Correlation Chain section that visualises related executions sharing
the same correlationId, with the current exchange highlighted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-23 18:19:50 +01:00
parent 63d8078688
commit 651cf9de6e
3 changed files with 98 additions and 1 deletions

View File

@@ -0,0 +1,20 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '../client';
export function useCorrelationChain(correlationId: string | null) {
return useQuery({
queryKey: ['correlation-chain', correlationId],
queryFn: async () => {
const { data } = await api.POST('/search/executions', {
body: {
correlationId: correlationId!,
limit: 20,
sortField: 'startTime',
sortDir: 'asc',
},
});
return data;
},
enabled: !!correlationId,
});
}

View File

@@ -141,3 +141,43 @@
color: var(--text-muted);
margin-bottom: 6px;
}
.correlationChain { margin-bottom: 16px; }
.chainRow {
display: flex;
align-items: center;
gap: 8px;
overflow-x: auto;
padding: 12px;
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
}
.chainArrow { color: var(--text-muted); font-size: 16px; flex-shrink: 0; }
.chainCard {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: var(--bg-raised);
border: 1px solid var(--border-subtle);
border-radius: 6px;
font-size: 12px;
text-decoration: none;
color: var(--text-primary);
flex-shrink: 0;
cursor: pointer;
}
.chainCard:hover { background: var(--bg-hover); }
.chainCardActive { border-color: var(--accent); background: var(--bg-hover); }
.chainRoute { font-weight: 600; }
.chainDuration { color: var(--text-muted); font-family: var(--font-mono); font-size: 11px; }
.chainMore { color: var(--text-muted); font-size: 11px; font-style: italic; }

View File

@@ -1,18 +1,24 @@
import { useState, useMemo } from 'react';
import React, { useState, useMemo } from 'react';
import { useParams, useNavigate } from 'react-router';
import {
Badge, StatusDot, MonoText, CodeBlock, InfoCallout,
ProcessorTimeline, Breadcrumb, Spinner,
} from '@cameleer/design-system';
import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions';
import { useCorrelationChain } from '../../api/queries/correlation';
import styles from './ExchangeDetail.module.css';
function countProcessors(nodes: any[]): number {
return nodes.reduce((sum, n) => sum + 1 + countProcessors(n.children || []), 0);
}
export default function ExchangeDetail() {
const { id } = useParams();
const navigate = useNavigate();
const { data: detail, isLoading } = useExecutionDetail(id ?? null);
const [selectedProcessor, setSelectedProcessor] = useState<number | null>(null);
const { data: snapshot } = useProcessorSnapshot(id ?? null, selectedProcessor);
const { data: correlationData } = useCorrelationChain(detail?.correlationId ?? null);
const processors = useMemo(() => {
if (!detail?.children) return [];
@@ -58,6 +64,10 @@ export default function ExchangeDetail() {
<div className={styles.headerStatLabel}>Duration</div>
<div className={styles.headerStatValue}>{detail.durationMs}ms</div>
</div>
<div className={styles.headerStat}>
<div className={styles.headerStatLabel}>Processors</div>
<div className={styles.headerStatValue}>{countProcessors(detail.processors || detail.children || [])}</div>
</div>
<div className={styles.headerStat}>
<div className={styles.headerStatLabel}>Route</div>
<div className={styles.headerStatValue}>{detail.routeId}</div>
@@ -70,6 +80,33 @@ export default function ExchangeDetail() {
</div>
</div>
{correlationData?.data && correlationData.data.length > 1 && (
<div className={styles.correlationChain}>
<div className={styles.panelHeader}>
<span className={styles.panelTitle}>Correlation Chain</span>
</div>
<div className={styles.chainRow}>
{correlationData.data.map((exec, i) => (
<React.Fragment key={exec.executionId}>
{i > 0 && <span className={styles.chainArrow}></span>}
<a
href={`/exchanges/${exec.executionId}`}
className={`${styles.chainCard} ${exec.executionId === id ? styles.chainCardActive : ''}`}
onClick={(e) => { e.preventDefault(); navigate(`/exchanges/${exec.executionId}`); }}
>
<StatusDot variant={exec.status === 'COMPLETED' ? 'success' : exec.status === 'FAILED' ? 'error' : 'warning'} />
<span className={styles.chainRoute}>{exec.routeId}</span>
<span className={styles.chainDuration}>{exec.durationMs}ms</span>
</a>
</React.Fragment>
))}
{correlationData.total > 20 && (
<span className={styles.chainMore}>+{correlationData.total - 20} more</span>
)}
</div>
</div>
)}
{detail.errorMessage && (
<InfoCallout variant="error">
{detail.errorMessage}