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);
|
color: var(--text-muted);
|
||||||
margin-bottom: 6px;
|
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 { useParams, useNavigate } from 'react-router';
|
||||||
import {
|
import {
|
||||||
Badge, StatusDot, MonoText, CodeBlock, InfoCallout,
|
Badge, StatusDot, MonoText, CodeBlock, InfoCallout,
|
||||||
ProcessorTimeline, Breadcrumb, Spinner,
|
ProcessorTimeline, Breadcrumb, Spinner,
|
||||||
} from '@cameleer/design-system';
|
} from '@cameleer/design-system';
|
||||||
import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions';
|
import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions';
|
||||||
|
import { useCorrelationChain } from '../../api/queries/correlation';
|
||||||
import styles from './ExchangeDetail.module.css';
|
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() {
|
export default function ExchangeDetail() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data: detail, isLoading } = useExecutionDetail(id ?? null);
|
const { data: detail, isLoading } = useExecutionDetail(id ?? null);
|
||||||
const [selectedProcessor, setSelectedProcessor] = useState<number | null>(null);
|
const [selectedProcessor, setSelectedProcessor] = useState<number | null>(null);
|
||||||
const { data: snapshot } = useProcessorSnapshot(id ?? null, selectedProcessor);
|
const { data: snapshot } = useProcessorSnapshot(id ?? null, selectedProcessor);
|
||||||
|
const { data: correlationData } = useCorrelationChain(detail?.correlationId ?? null);
|
||||||
|
|
||||||
const processors = useMemo(() => {
|
const processors = useMemo(() => {
|
||||||
if (!detail?.children) return [];
|
if (!detail?.children) return [];
|
||||||
@@ -58,6 +64,10 @@ export default function ExchangeDetail() {
|
|||||||
<div className={styles.headerStatLabel}>Duration</div>
|
<div className={styles.headerStatLabel}>Duration</div>
|
||||||
<div className={styles.headerStatValue}>{detail.durationMs}ms</div>
|
<div className={styles.headerStatValue}>{detail.durationMs}ms</div>
|
||||||
</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.headerStat}>
|
||||||
<div className={styles.headerStatLabel}>Route</div>
|
<div className={styles.headerStatLabel}>Route</div>
|
||||||
<div className={styles.headerStatValue}>{detail.routeId}</div>
|
<div className={styles.headerStatValue}>{detail.routeId}</div>
|
||||||
@@ -70,6 +80,33 @@ export default function ExchangeDetail() {
|
|||||||
</div>
|
</div>
|
||||||
</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 && (
|
{detail.errorMessage && (
|
||||||
<InfoCallout variant="error">
|
<InfoCallout variant="error">
|
||||||
{detail.errorMessage}
|
{detail.errorMessage}
|
||||||
|
|||||||
Reference in New Issue
Block a user