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:
20
ui/src/api/queries/correlation.ts
Normal file
20
ui/src/api/queries/correlation.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user