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-30 23:26:55 +02:00
import { GitBranch , Server , RotateCcw } 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-30 21:42:06 +02:00
import { useAuthStore } from '../../auth/auth-store' ;
2026-03-28 13:55:13 +01:00
import type { ExecutionDetail } from '../../components/ExecutionDiagram/types' ;
2026-03-28 15:37:49 +01:00
import { attributeBadgeColor } from '../../utils/attribute-color' ;
2026-03-30 21:42:06 +02:00
import { RouteControlBar } from './RouteControlBar' ;
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 15:42:45 +01:00
onClearSelection ? : ( ) = > 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:42:45 +01:00
export function ExchangeHeader ( { detail , onCorrelatedSelect , onClearSelection } : 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-30 21:42:06 +02:00
// Look up agent state for icon coloring + route control capability
2026-03-28 15:16:54 +01:00
const { data : agents } = useAgents ( undefined , detail . applicationName ) ;
2026-03-30 21:42:06 +02:00
const { agentState , hasRouteControl , hasReplay } = useMemo ( ( ) = > {
if ( ! agents ) return { agentState : undefined , hasRouteControl : false , hasReplay : false } ;
const agentList = agents as any [ ] ;
const agent = detail . agentId ? agentList . find ( ( a : any ) = > a . id === detail . agentId ) : undefined ;
return {
agentState : agent?.state?.toLowerCase ( ) as 'live' | 'stale' | 'dead' | undefined ,
hasRouteControl : agentList.some ( ( a : any ) = > a . capabilities ? . routeControl === true ) ,
hasReplay : agentList.some ( ( a : any ) = > a . capabilities ? . replay === true ) ,
} ;
2026-03-28 15:16:54 +01:00
} , [ agents , detail . agentId ] ) ;
2026-03-30 21:42:06 +02:00
const roles = useAuthStore ( ( s ) = > s . roles ) ;
const canControl = roles . some ( r = > r === 'OPERATOR' || r === 'ADMIN' ) ;
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 ] ) = > (
2026-03-28 15:37:49 +01:00
< Badge key = { k } label = { ` ${ k } : ${ v } ` } color = { attributeBadgeColor ( String ( v ) ) } / >
2026-03-28 15:07:23 +01:00
) ) }
< / >
) }
< span className = { styles . separator } / >
2026-03-28 15:42:45 +01:00
< button className = { styles . linkBtn } onClick = { ( ) = > { onClearSelection ? . ( ) ; navigate ( ` /exchanges/ ${ detail . applicationName } ` ) ; } } title = "Show all exchanges for this application" >
2026-03-28 15:14:48 +01:00
< span className = { styles . app } > { detail . applicationName } < / span >
< / button >
2026-03-28 15:42:45 +01:00
< button className = { styles . linkBtn } onClick = { ( ) = > { onClearSelection ? . ( ) ; navigate ( ` /exchanges/ ${ detail . applicationName } / ${ detail . routeId } ` ) ; } } title = "Show all exchanges for this route" >
2026-03-28 15:14:48 +01:00
< 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-30 21:42:06 +02:00
{ /* Route control / replay — only if agent supports it AND user has operator+ role */ }
{ canControl && ( hasRouteControl || hasReplay ) && (
< RouteControlBar
application = { detail . applicationName }
routeId = { detail . routeId }
hasRouteControl = { hasRouteControl }
hasReplay = { hasReplay }
agentId = { detail . agentId }
exchangeId = { detail . exchangeId }
inputHeaders = { detail . inputHeaders }
inputBody = { detail . inputBody }
/ >
) }
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 ) ;
2026-03-30 23:26:55 +02:00
const isReplay = ce . attributes ? . _replay != null ;
2026-03-28 14:49:45 +01:00
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
}
} }
2026-03-30 23:26:55 +02:00
title = { ` ${ ce . executionId } \ n ${ ce . routeId } \ u2014 ${ formatDuration ( ce . durationMs ) } ${ isReplay ? '\n(replay)' : '' } ` }
2026-03-28 15:00:45 +01:00
>
< StatusDot variant = { variant } / >
2026-03-30 23:26:55 +02:00
{ isReplay && < RotateCcw size = { 9 } className = { styles . replayIcon } / > }
2026-03-28 15:00:45 +01:00
< 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 >
) ;
}