import { useMemo } from 'react'; import { useNavigate } from 'react-router'; import { GitBranch, Server, RotateCcw, FileText } from 'lucide-react'; import { StatusDot, MonoText, Badge, useGlobalFilters } from '@cameleer/design-system'; import { useCorrelationChain } from '../../api/queries/correlation'; import { useAgents } from '../../api/queries/agents'; import { useRouteCatalog } from '../../api/queries/catalog'; import { useAuthStore } from '../../auth/auth-store'; import type { ExecutionDetail } from '../../components/ExecutionDiagram/types'; import { attributeBadgeColor } from '../../utils/attribute-color'; import { RouteControlBar } from './RouteControlBar'; import styles from './ExchangeHeader.module.css'; interface ExchangeHeaderProps { detail: ExecutionDetail; onCorrelatedSelect?: (executionId: string, applicationId: string, routeId: string) => void; onClearSelection?: () => void; } 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'; } } function statusLabel(s: string): string { switch (s) { case 'COMPLETED': return 'OK'; case 'FAILED': return 'ERR'; case 'RUNNING': return 'RUN'; default: return s; } } 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`; } export function ExchangeHeader({ detail, onCorrelatedSelect, onClearSelection }: ExchangeHeaderProps) { const navigate = useNavigate(); const { timeRange } = useGlobalFilters(); const { data: chainResult } = useCorrelationChain(detail.correlationId ?? null); const chain = chainResult?.data; const showChain = chain && chain.length > 1; const attrs = Object.entries(detail.attributes ?? {}); // Look up route state from catalog const { data: catalog } = useRouteCatalog(timeRange.start.toISOString(), timeRange.end.toISOString()); const routeState = useMemo(() => { if (!catalog) return undefined; for (const app of catalog as any[]) { if (app.appId !== detail.applicationId) continue; for (const route of app.routes || []) { if (route.routeId === detail.routeId) { return (route.routeState ?? 'started') as 'started' | 'stopped' | 'suspended'; } } } return undefined; }, [catalog, detail.applicationId, detail.routeId]); // Look up agent state for icon coloring + route control capability const { data: agents } = useAgents(undefined, detail.applicationId); const { agentState, hasRouteControl, hasReplay } = useMemo(() => { if (!agents) return { agentState: undefined, hasRouteControl: false, hasReplay: false }; const agentList = agents as any[]; const agent = detail.instanceId ? agentList.find((a: any) => a.instanceId === detail.instanceId) : undefined; return { agentState: agent?.status?.toLowerCase() as 'live' | 'stale' | 'dead' | undefined, hasRouteControl: agentList.some((a: any) => a.capabilities?.routeControl === true), hasReplay: agentList.some((a: any) => a.capabilities?.replay === true), }; }, [agents, detail.instanceId]); const roles = useAuthStore((s) => s.roles); const canControl = roles.some(r => r === 'OPERATOR' || r === 'ADMIN'); return (
{/* Exchange info — always shown */}
{attrs.length > 0 && ( <> {attrs.map(([k, v]) => ( ))} )} {detail.instanceId && ( <> )} {formatDuration(detail.durationMs)}
{/* Route control / replay — only if agent supports it AND user has operator+ role */} {canControl && (hasRouteControl || hasReplay) && ( )} {/* Correlation chain */}
Correlated {showChain ? chain.map((ce: any, i: number) => { const isCurrent = ce.executionId === detail.executionId; const variant = statusVariant(ce.status); const isReplay = !!ce.isReplay; const statusCls = variant === 'success' ? styles.chainNodeSuccess : variant === 'error' ? styles.chainNodeError : variant === 'running' ? styles.chainNodeRunning : styles.chainNodeWarning; return ( {i > 0 && } ); }) : ( no correlated exchanges found )} {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; return totalMs > 0 ? {formatDuration(totalMs)} : null; })()}
); }