diff --git a/ui/src/api/queries/correlation.ts b/ui/src/api/queries/correlation.ts new file mode 100644 index 00000000..66210bdb --- /dev/null +++ b/ui/src/api/queries/correlation.ts @@ -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, + }); +} diff --git a/ui/src/pages/ExchangeDetail/ExchangeDetail.module.css b/ui/src/pages/ExchangeDetail/ExchangeDetail.module.css index 9c38c628..552cc066 100644 --- a/ui/src/pages/ExchangeDetail/ExchangeDetail.module.css +++ b/ui/src/pages/ExchangeDetail/ExchangeDetail.module.css @@ -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; } diff --git a/ui/src/pages/ExchangeDetail/ExchangeDetail.tsx b/ui/src/pages/ExchangeDetail/ExchangeDetail.tsx index 422db4d1..d4579cfe 100644 --- a/ui/src/pages/ExchangeDetail/ExchangeDetail.tsx +++ b/ui/src/pages/ExchangeDetail/ExchangeDetail.tsx @@ -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(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() {
Duration
{detail.durationMs}ms
+
+
Processors
+
{countProcessors(detail.processors || detail.children || [])}
+
Route
{detail.routeId}
@@ -70,6 +80,33 @@ export default function ExchangeDetail() {
+ {correlationData?.data && correlationData.data.length > 1 && ( +
+
+ Correlation Chain +
+
+ {correlationData.data.map((exec, i) => ( + + {i > 0 && } + { e.preventDefault(); navigate(`/exchanges/${exec.executionId}`); }} + > + + {exec.routeId} + {exec.durationMs}ms + + + ))} + {correlationData.total > 20 && ( + +{correlationData.total - 20} more + )} +
+
+ )} + {detail.errorMessage && ( {detail.errorMessage}