2026-03-28 15:16:54 +01:00
import { useMemo } from 'react' ;
2026-03-28 15:00:45 +01:00
import { useNavigate } from 'react-router' ;
2026-03-28 15:12:37 +01:00
import { GitBranch , Server } from 'lucide-react' ;
2026-03-28 13:55:13 +01:00
import { StatusDot , MonoText , Badge } from '@cameleer/design-system' ;
import { useCorrelationChain } from '../../api/queries/correlation' ;
2026-03-28 15:16:54 +01:00
import { useAgents } from '../../api/queries/agents' ;
2026-03-28 13:55:13 +01:00
import type { ExecutionDetail } from '../../components/ExecutionDiagram/types' ;
2026-03-28 14:49:45 +01:00
import styles from './ExchangeHeader.module.css' ;
2026-03-28 13:55:13 +01:00
interface ExchangeHeaderProps {
detail : ExecutionDetail ;
2026-03-28 15:26:01 +01:00
onCorrelatedSelect ? : ( executionId : string , applicationName : string , routeId : string ) = > void ;
2026-03-28 13:55:13 +01:00
}
type StatusVariant = 'success' | 'error' | 'running' | 'warning' ;
function statusVariant ( s : string ) : StatusVariant {
switch ( s ) {
case 'COMPLETED' : return 'success' ;
case 'FAILED' : return 'error' ;
case 'RUNNING' : return 'running' ;
default : return 'warning' ;
}
}
2026-03-28 15:00:45 +01:00
function statusLabel ( s : string ) : string {
switch ( s ) {
case 'COMPLETED' : return 'OK' ;
case 'FAILED' : return 'ERR' ;
case 'RUNNING' : return 'RUN' ;
default : return s ;
}
}
2026-03-28 13:55:13 +01:00
function formatDuration ( ms : number ) : string {
if ( ms >= 60 _000 ) return ` ${ ( ms / 1000 ) . toFixed ( 0 ) } s ` ;
if ( ms >= 1000 ) return ` ${ ( ms / 1000 ) . toFixed ( 2 ) } s ` ;
return ` ${ ms } ms ` ;
}
2026-03-28 15:26:01 +01:00
export function ExchangeHeader ( { detail , onCorrelatedSelect } : ExchangeHeaderProps ) {
2026-03-28 15:00:45 +01:00
const navigate = useNavigate ( ) ;
2026-03-28 13:55:13 +01:00
const { data : chainResult } = useCorrelationChain ( detail . correlationId ? ? null ) ;
const chain = chainResult ? . data ;
const showChain = chain && chain . length > 1 ;
2026-03-28 15:07:23 +01:00
const attrs = Object . entries ( detail . attributes ? ? { } ) ;
2026-03-28 13:55:13 +01:00
2026-03-28 15:16:54 +01:00
// Look up agent state for icon coloring
const { data : agents } = useAgents ( undefined , detail . applicationName ) ;
const agentState = useMemo ( ( ) = > {
if ( ! agents || ! detail . agentId ) return undefined ;
const agent = ( agents as any [ ] ) . find ( ( a : any ) = > a . id === detail . agentId ) ;
return agent ? . state ? . toLowerCase ( ) as 'live' | 'stale' | 'dead' | undefined ;
} , [ agents , detail . agentId ] ) ;
2026-03-28 13:55:13 +01:00
return (
2026-03-28 14:49:45 +01:00
< div className = { styles . header } >
2026-03-28 15:00:45 +01:00
{ /* Exchange info — always shown */ }
< div className = { styles . info } >
< StatusDot variant = { statusVariant ( detail . status ) } / >
< Badge label = { statusLabel ( detail . status ) } color = { statusVariant ( detail . status ) } / >
2026-03-28 15:07:23 +01:00
{ attrs . length > 0 && (
< >
< span className = { styles . separator } / >
{ attrs . map ( ( [ k , v ] ) = > (
< Badge key = { k } label = { ` ${ k } : ${ v } ` } color = "auto" / >
) ) }
< / >
) }
< span className = { styles . separator } / >
2026-03-28 15:14:48 +01:00
< button className = { styles . linkBtn } onClick = { ( ) = > navigate ( ` /exchanges/ ${ detail . applicationName } ` ) } title = "Show all exchanges for this application" >
< span className = { styles . app } > { detail . applicationName } < / span >
< / button >
< button className = { styles . linkBtn } onClick = { ( ) = > navigate ( ` /exchanges/ ${ detail . applicationName } / ${ detail . routeId } ` ) } title = "Show all exchanges for this route" >
< span className = { styles . route } > { detail . routeId } < / span >
< GitBranch size = { 12 } className = { styles . icon } / >
< / button >
2026-03-28 15:07:58 +01:00
{ detail . agentId && (
< >
< span className = { styles . separator } / >
2026-03-28 15:16:54 +01:00
< button className = { styles . linkBtn } onClick = { ( ) = > navigate ( ` /runtime/ ${ detail . applicationName } ` ) } title = "All agents for this application" >
2026-03-28 15:14:48 +01:00
< span className = { styles . app } > { detail . applicationName } < / span >
2026-03-28 15:16:54 +01:00
< / button >
< button className = { styles . linkBtn } onClick = { ( ) = > navigate ( ` /runtime/ ${ detail . applicationName } / ${ detail . agentId } ` ) } title = "Agent details" >
2026-03-28 15:14:48 +01:00
< MonoText size = "xs" > { detail . agentId } < / MonoText >
2026-03-28 15:16:54 +01:00
< Server size = { 12 } className = { agentState === 'live' ? styles.iconLive : agentState === 'stale' ? styles.iconStale : agentState === 'dead' ? styles.iconDead : styles.icon } / >
2026-03-28 15:14:48 +01:00
< / button >
2026-03-28 15:07:58 +01:00
< / >
) }
2026-03-28 15:09:00 +01:00
< span className = { styles . duration } > { formatDuration ( detail . durationMs ) } < / span >
2026-03-28 15:00:45 +01:00
< / div >
2026-03-28 15:24:20 +01:00
{ /* Correlation chain */ }
< div className = { styles . chain } >
< span className = { styles . chainLabel } > Correlated < / span >
{ showChain ? chain . map ( ( ce : any , i : number ) = > {
2026-03-28 14:49:45 +01:00
const isCurrent = ce . executionId === detail . executionId ;
const variant = statusVariant ( ce . status ) ;
const statusCls =
variant === 'success' ? styles . chainNodeSuccess
: variant === 'error' ? styles . chainNodeError
: variant === 'running' ? styles . chainNodeRunning
: styles . chainNodeWarning ;
return (
2026-03-28 15:00:45 +01:00
< span key = { ce . executionId } className = { styles . chainEntry } >
{ i > 0 && < span className = { styles . chainArrow } > & rarr ; < / span > }
< button
className = { ` ${ styles . chainNode } ${ statusCls } ${ isCurrent ? styles . chainNodeCurrent : '' } ` }
onClick = { ( ) = > {
2026-03-28 15:26:01 +01:00
if ( ! isCurrent && onCorrelatedSelect ) {
onCorrelatedSelect ( ce . executionId , ce . applicationName ? ? detail . applicationName , ce . routeId ) ;
2026-03-28 15:00:45 +01:00
}
} }
title = { ` ${ ce . executionId } \ n ${ ce . routeId } \ u2014 ${ formatDuration ( ce . durationMs ) } ` }
>
< StatusDot variant = { variant } / >
< span className = { styles . chainRoute } > { ce . routeId } < / span >
< span className = { styles . chainDuration } > { formatDuration ( ce . durationMs ) } < / span >
< / button >
< / span >
2026-03-28 14:49:45 +01:00
) ;
2026-03-28 15:24:20 +01:00
} ) : (
< span className = { styles . chainNone } > no correlated exchanges found < / span >
) }
2026-03-28 15:29:35 +01:00
{ showChain && ( ( ) = > {
const starts = chain . map ( ( ce : any ) = > new Date ( ce . startTime ) . getTime ( ) ) . filter ( ( t : number ) = > ! isNaN ( t ) ) ;
const ends = chain . map ( ( ce : any ) = > new Date ( ce . endTime ? ? ce . startTime ) . getTime ( ) + ( ce . durationMs ? ? 0 ) ) . filter ( ( t : number ) = > ! isNaN ( t ) ) ;
const totalMs = starts . length > 0 && ends . length > 0 ? Math . max ( . . . ends ) - Math . min ( . . . starts ) : 0 ;
2026-03-28 15:30:22 +01:00
return totalMs > 0 ? < span className = { styles . chainTotal } > { formatDuration ( totalMs ) } < / span > : null ;
2026-03-28 15:29:35 +01:00
} ) ( ) }
2026-03-28 15:24:20 +01:00
< / div >
2026-03-28 13:55:13 +01:00
< / div >
) ;
}