Files
cameleer-server/ui/src/pages/ExchangeDetail/ExchangeDetail.tsx

178 lines
7.2 KiB
TypeScript
Raw Normal View History

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<number | null>(null);
const { data: snapshot } = useProcessorSnapshot(id ?? null, selectedProcessor);
const { data: correlationData } = useCorrelationChain(detail?.correlationId ?? null);
const processors = useMemo(() => {
if (!detail?.children) return [];
const result: any[] = [];
let offset = 0;
function walk(node: any) {
result.push({
name: node.processorId || node.processorType,
type: node.processorType,
durationMs: node.durationMs ?? 0,
status: node.status === 'COMPLETED' ? 'ok' : node.status === 'FAILED' ? 'fail' : 'ok',
startMs: offset,
});
offset += node.durationMs ?? 0;
if (node.children) node.children.forEach(walk);
}
detail.children.forEach(walk);
return result;
}, [detail]);
if (isLoading) return <div style={{ display: 'flex', justifyContent: 'center', padding: '4rem' }}><Spinner size="lg" /></div>;
if (!detail) return <InfoCallout variant="warning">Exchange not found</InfoCallout>;
return (
<div>
<Breadcrumb items={[
{ label: 'Dashboard', href: '/apps' },
{ label: detail.groupName || 'App', href: `/apps/${detail.groupName}` },
{ label: id?.slice(0, 12) || '' },
]} />
<div className={styles.exchangeHeader}>
<div className={styles.headerRow}>
<div className={styles.headerLeft}>
<StatusDot variant={detail.status === 'COMPLETED' ? 'success' : detail.status === 'FAILED' ? 'error' : 'running'} />
<div>
<Badge label={detail.status} color={detail.status === 'COMPLETED' ? 'success' : 'error'} />
<MonoText>{id}</MonoText>
</div>
</div>
<div className={styles.headerRight}>
<div className={styles.headerStat}>
<div className={styles.headerStatLabel}>Duration</div>
<div className={styles.headerStatValue}>{detail.durationMs}ms</div>
</div>
<div className={styles.headerStat}>
<div className={styles.headerStatLabel}>Processors</div>
<div className={styles.headerStatValue}>{countProcessors(detail.processors || detail.children || [])}</div>
</div>
<div className={styles.headerStat}>
<div className={styles.headerStatLabel}>Route</div>
<div className={styles.headerStatValue}>{detail.routeId}</div>
</div>
<div className={styles.headerStat}>
<div className={styles.headerStatLabel}>Application</div>
<div className={styles.headerStatValue}>{detail.groupName || 'unknown'}</div>
</div>
</div>
</div>
</div>
{correlationData?.data && correlationData.data.length > 1 && (
<div className={styles.correlationChain}>
<div className={styles.panelHeader}>
<span className={styles.panelTitle}>Correlation Chain</span>
</div>
<div className={styles.chainRow}>
{correlationData.data.map((exec, i) => (
<React.Fragment key={exec.executionId}>
{i > 0 && <span className={styles.chainArrow}></span>}
<a
href={`/exchanges/${exec.executionId}`}
className={`${styles.chainCard} ${exec.executionId === id ? styles.chainCardActive : ''}`}
onClick={(e) => { e.preventDefault(); navigate(`/exchanges/${exec.executionId}`); }}
>
<StatusDot variant={exec.status === 'COMPLETED' ? 'success' : exec.status === 'FAILED' ? 'error' : 'warning'} />
<span className={styles.chainRoute}>{exec.routeId}</span>
<span className={styles.chainDuration}>{exec.durationMs}ms</span>
</a>
</React.Fragment>
))}
{correlationData.total > 20 && (
<span className={styles.chainMore}>+{correlationData.total - 20} more</span>
)}
</div>
</div>
)}
{detail.errorMessage && (
<InfoCallout variant="error">
{detail.errorMessage}
</InfoCallout>
)}
<div className={styles.timelineSection}>
<div className={styles.timelineHeader}>
<span className={styles.timelineTitle}>Processor Timeline</span>
</div>
<div className={styles.timelineBody}>
{processors.length > 0 ? (
<ProcessorTimeline
processors={processors}
totalMs={detail.durationMs}
onProcessorClick={(_p, i) => setSelectedProcessor(i)}
selectedIndex={selectedProcessor ?? undefined}
/>
) : (
<InfoCallout>No processor data available</InfoCallout>
)}
</div>
</div>
{snapshot && (
<>
<div className={styles.sectionLabel}>Exchange Snapshot</div>
<div className={styles.detailSplit}>
<div className={styles.detailPanel}>
<div className={styles.panelHeader}>
<span className={styles.panelTitle}>Input Body</span>
</div>
<div className={styles.panelBody}>
<CodeBlock content={String(snapshot.inputBody ?? 'null')} />
</div>
</div>
<div className={styles.detailPanel}>
<div className={styles.panelHeader}>
<span className={styles.panelTitle}>Output Body</span>
</div>
<div className={styles.panelBody}>
<CodeBlock content={String(snapshot.outputBody ?? 'null')} />
</div>
</div>
</div>
<div className={styles.detailSplit}>
<div className={styles.detailPanel}>
<div className={styles.panelHeader}>
<span className={styles.panelTitle}>Input Headers</span>
</div>
<div className={styles.panelBody}>
<CodeBlock content={JSON.stringify(snapshot.inputHeaders ?? {}, null, 2)} />
</div>
</div>
<div className={styles.detailPanel}>
<div className={styles.panelHeader}>
<span className={styles.panelTitle}>Output Headers</span>
</div>
<div className={styles.panelBody}>
<CodeBlock content={JSON.stringify(snapshot.outputHeaders ?? {}, null, 2)} />
</div>
</div>
</div>
</>
)}
</div>
);
}