Files
cameleer-server/ui/src/pages/Exchanges/ExchangeHeader.tsx
hsiegeln 4cdbcdaeea
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 32s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
fix: update frontend field names for identity rename (applicationId, instanceId)
The backend identity rename (applicationName → applicationId,
agentId → instanceId) was not reflected in the frontend. This caused
drilldown to fail (detail.applicationName was undefined, disabling
the diagram fetch) and various display issues.

Updated schema.d.ts, ExchangeHeader, ExecutionDiagram, Dashboard,
AgentHealth, AgentInstance, LayoutShell, LogTab, InfoTab, DetailPanel,
ExchangesPage, and tracing-store.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:22:16 +02:00

163 lines
7.5 KiB
TypeScript

import { useMemo } from 'react';
import { useNavigate } from 'react-router';
import { GitBranch, Server, RotateCcw } from 'lucide-react';
import { StatusDot, MonoText, Badge } from '@cameleer/design-system';
import { useCorrelationChain } from '../../api/queries/correlation';
import { useAgents } from '../../api/queries/agents';
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 { data: chainResult } = useCorrelationChain(detail.correlationId ?? null);
const chain = chainResult?.data;
const showChain = chain && chain.length > 1;
const attrs = Object.entries(detail.attributes ?? {});
// 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.id === detail.instanceId) : 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),
};
}, [agents, detail.instanceId]);
const roles = useAuthStore((s) => s.roles);
const canControl = roles.some(r => r === 'OPERATOR' || r === 'ADMIN');
return (
<div className={styles.header}>
{/* Exchange info — always shown */}
<div className={styles.info}>
<StatusDot variant={statusVariant(detail.status)} />
<Badge label={statusLabel(detail.status)} color={statusVariant(detail.status)} />
{attrs.length > 0 && (
<>
<span className={styles.separator} />
{attrs.map(([k, v]) => (
<Badge key={k} label={`${k}: ${v}`} color={attributeBadgeColor(String(v))} />
))}
</>
)}
<span className={styles.separator} />
<button className={styles.linkBtn} onClick={() => { onClearSelection?.(); navigate(`/exchanges/${detail.applicationId}`); }} title="Show all exchanges for this application">
<span className={styles.app}>{detail.applicationId}</span>
</button>
<button className={styles.linkBtn} onClick={() => { onClearSelection?.(); navigate(`/exchanges/${detail.applicationId}/${detail.routeId}`); }} title="Show all exchanges for this route">
<span className={styles.route}>{detail.routeId}</span>
<GitBranch size={12} className={styles.icon} />
</button>
{detail.instanceId && (
<>
<span className={styles.separator} />
<button className={styles.linkBtn} onClick={() => navigate(`/runtime/${detail.applicationId}`)} title="All agents for this application">
<span className={styles.app}>{detail.applicationId}</span>
</button>
<button className={styles.linkBtn} onClick={() => navigate(`/runtime/${detail.applicationId}/${detail.instanceId}`)} title="Agent details">
<MonoText size="xs">{detail.instanceId}</MonoText>
<Server size={12} className={agentState === 'live' ? styles.iconLive : agentState === 'stale' ? styles.iconStale : agentState === 'dead' ? styles.iconDead : styles.icon} />
</button>
</>
)}
<span className={styles.duration}>{formatDuration(detail.durationMs)}</span>
</div>
{/* Route control / replay — only if agent supports it AND user has operator+ role */}
{canControl && (hasRouteControl || hasReplay) && (
<RouteControlBar
application={detail.applicationId}
routeId={detail.routeId}
hasRouteControl={hasRouteControl}
hasReplay={hasReplay}
agentId={detail.instanceId}
exchangeId={detail.exchangeId}
inputHeaders={detail.inputHeaders}
inputBody={detail.inputBody}
/>
)}
{/* Correlation chain */}
<div className={styles.chain}>
<span className={styles.chainLabel}>Correlated</span>
{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 (
<span key={ce.executionId} className={styles.chainEntry}>
{i > 0 && <span className={styles.chainArrow}>&rarr;</span>}
<button
className={`${styles.chainNode} ${statusCls} ${isCurrent ? styles.chainNodeCurrent : ''}`}
onClick={() => {
if (!isCurrent && onCorrelatedSelect) {
onCorrelatedSelect(ce.executionId, ce.applicationId ?? detail.applicationId, ce.routeId);
}
}}
title={`${ce.executionId}\n${ce.routeId} \u2014 ${formatDuration(ce.durationMs)}${isReplay ? '\n(replay)' : ''}`}
>
<StatusDot variant={variant} />
{isReplay && <RotateCcw size={9} className={styles.replayIcon} />}
<span className={styles.chainRoute}>{ce.routeId}</span>
<span className={styles.chainDuration}>{formatDuration(ce.durationMs)}</span>
</button>
</span>
);
}) : (
<span className={styles.chainNone}>no correlated exchanges found</span>
)}
{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 ? <span className={styles.chainTotal}>{formatDuration(totalMs)}</span> : null;
})()}
</div>
</div>
);
}