feat: replace UI with design system example pages wired to real API
Migrate all page components from the @cameleer/design-system v0.0.3 example UI, replacing mock data with real backend API hooks. This brings richer visuals (KpiStrip, GroupCard, RouteFlow, ProcessorTimeline, DateRangePicker, expandable rows) while preserving all existing API integration, auth, and routing infrastructure. Pages migrated: Dashboard, RoutesMetrics, RouteDetail, ExchangeDetail, AgentHealth, AgentInstance, OidcConfig, AuditLog, RBAC (Users/Groups/Roles). Also enhanced LayoutShell CommandPalette with real search data from catalog. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,21 @@
|
||||
/* Scrollable content area */
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px 24px 40px;
|
||||
min-width: 0;
|
||||
background: var(--bg-body);
|
||||
}
|
||||
|
||||
.loadingContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 4rem;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
EXCHANGE HEADER CARD
|
||||
========================================================================== */
|
||||
.exchangeHeader {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
@@ -38,14 +56,14 @@
|
||||
}
|
||||
|
||||
.routeLink {
|
||||
color: var(--accent, #c6820e);
|
||||
color: var(--amber);
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.routeLink:hover {
|
||||
color: var(--amber-deep, #a36b0b);
|
||||
color: var(--amber-deep);
|
||||
}
|
||||
|
||||
.headerDivider {
|
||||
@@ -78,7 +96,9 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Correlation Chain */
|
||||
/* ==========================================================================
|
||||
CORRELATION CHAIN
|
||||
========================================================================== */
|
||||
.correlationChain {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -104,7 +124,7 @@
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-subtle);
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
@@ -120,20 +140,37 @@
|
||||
}
|
||||
|
||||
.chainNodeCurrent {
|
||||
background: var(--amber-bg, rgba(198, 130, 14, 0.08));
|
||||
border-color: var(--accent, #c6820e);
|
||||
color: var(--accent, #c6820e);
|
||||
background: var(--amber-bg);
|
||||
border-color: var(--amber-light);
|
||||
color: var(--amber-deep);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chainNodeSuccess { border-left: 3px solid var(--success); }
|
||||
.chainNodeError { border-left: 3px solid var(--error); }
|
||||
.chainNodeRunning { border-left: 3px solid var(--running); }
|
||||
.chainNodeWarning { border-left: 3px solid var(--warning); }
|
||||
.chainNodeSuccess {
|
||||
border-left: 3px solid var(--success);
|
||||
}
|
||||
|
||||
.chainMore { color: var(--text-muted); font-size: 11px; font-style: italic; }
|
||||
.chainNodeError {
|
||||
border-left: 3px solid var(--error);
|
||||
}
|
||||
|
||||
/* Timeline Section */
|
||||
.chainNodeRunning {
|
||||
border-left: 3px solid var(--running);
|
||||
}
|
||||
|
||||
.chainNodeWarning {
|
||||
border-left: 3px solid var(--warning);
|
||||
}
|
||||
|
||||
.chainMore {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
TIMELINE SECTION
|
||||
========================================================================== */
|
||||
.timelineSection {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
@@ -174,7 +211,7 @@
|
||||
display: inline-flex;
|
||||
gap: 0;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -194,20 +231,22 @@
|
||||
}
|
||||
|
||||
.toggleBtnActive {
|
||||
background: var(--accent, #c6820e);
|
||||
background: var(--amber);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.toggleBtnActive:hover {
|
||||
background: var(--amber-deep, #a36b0b);
|
||||
background: var(--amber-deep);
|
||||
}
|
||||
|
||||
.timelineBody {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
/* Detail Split (IN / OUT panels) */
|
||||
/* ==========================================================================
|
||||
DETAIL SPLIT (IN / OUT panels)
|
||||
========================================================================== */
|
||||
.detailSplit {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -224,7 +263,7 @@
|
||||
}
|
||||
|
||||
.detailPanelError {
|
||||
border-color: var(--error-border, rgba(220, 38, 38, 0.3));
|
||||
border-color: var(--error-border);
|
||||
}
|
||||
|
||||
.panelHeader {
|
||||
@@ -238,8 +277,8 @@
|
||||
}
|
||||
|
||||
.detailPanelError .panelHeader {
|
||||
background: var(--error-bg, rgba(220, 38, 38, 0.06));
|
||||
border-bottom-color: var(--error-border, rgba(220, 38, 38, 0.3));
|
||||
background: var(--error-bg);
|
||||
border-bottom-color: var(--error-border);
|
||||
}
|
||||
|
||||
.panelTitle {
|
||||
@@ -350,14 +389,33 @@
|
||||
}
|
||||
|
||||
/* Error panel styles */
|
||||
.errorBadgeRow {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.errorHttpBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
background: var(--error-bg);
|
||||
color: var(--error);
|
||||
border: 1px solid var(--error-border);
|
||||
}
|
||||
|
||||
.errorMessageBox {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
background: var(--error-bg, rgba(220, 38, 38, 0.06));
|
||||
background: var(--error-bg);
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
border: 1px solid var(--error-border, rgba(220, 38, 38, 0.3));
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--error-border);
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
@@ -382,3 +440,11 @@
|
||||
font-family: var(--font-mono);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Snapshot loading */
|
||||
.snapshotLoading {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@@ -1,112 +1,187 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router';
|
||||
import { useState, useMemo } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router'
|
||||
import {
|
||||
Badge, StatusDot, MonoText, CodeBlock, InfoCallout,
|
||||
ProcessorTimeline, Breadcrumb, Spinner, RouteFlow,
|
||||
} from '@cameleer/design-system';
|
||||
import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions';
|
||||
import { useCorrelationChain } from '../../api/queries/correlation';
|
||||
import { useDiagramLayout } from '../../api/queries/diagrams';
|
||||
import { mapDiagramToRouteNodes } from '../../utils/diagram-mapping';
|
||||
import styles from './ExchangeDetail.module.css';
|
||||
} from '@cameleer/design-system'
|
||||
import type { ProcessorStep, RouteNode } from '@cameleer/design-system'
|
||||
import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions'
|
||||
import { useCorrelationChain } from '../../api/queries/correlation'
|
||||
import { useDiagramLayout } from '../../api/queries/diagrams'
|
||||
import { mapDiagramToRouteNodes } from '../../utils/diagram-mapping'
|
||||
import styles from './ExchangeDetail.module.css'
|
||||
|
||||
function countProcessors(nodes: any[]): number {
|
||||
return nodes.reduce((sum, n) => sum + 1 + countProcessors(n.children || []), 0);
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
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`
|
||||
}
|
||||
|
||||
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`;
|
||||
function backendStatusToVariant(status: string): 'success' | 'error' | 'running' | 'warning' {
|
||||
switch (status.toUpperCase()) {
|
||||
case 'COMPLETED': return 'success'
|
||||
case 'FAILED': return 'error'
|
||||
case 'RUNNING': return 'running'
|
||||
default: return 'warning'
|
||||
}
|
||||
}
|
||||
|
||||
function backendStatusToLabel(status: string): string {
|
||||
return status.toUpperCase()
|
||||
}
|
||||
|
||||
function procStatusToStep(status: string): 'ok' | 'slow' | 'fail' {
|
||||
const s = status.toUpperCase()
|
||||
if (s === 'FAILED') return 'fail'
|
||||
if (s === 'RUNNING') return 'slow'
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
function parseHeaders(raw: string | undefined | null): Record<string, string> {
|
||||
if (!raw) return {};
|
||||
if (!raw) return {}
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
const parsed = JSON.parse(raw)
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
const result: Record<string, string> = {};
|
||||
const result: Record<string, string> = {}
|
||||
for (const [k, v] of Object.entries(parsed)) {
|
||||
result[k] = typeof v === 'string' ? v : JSON.stringify(v);
|
||||
result[k] = typeof v === 'string' ? v : JSON.stringify(v)
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
return {};
|
||||
return {}
|
||||
}
|
||||
|
||||
function countProcessors(nodes: Array<{ children?: any[] }>): number {
|
||||
return nodes.reduce((sum, n) => sum + 1 + countProcessors(n.children || []), 0)
|
||||
}
|
||||
|
||||
// ── ExchangeDetail ───────────────────────────────────────────────────────────
|
||||
export default function ExchangeDetail() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { data: detail, isLoading } = useExecutionDetail(id ?? null);
|
||||
const [timelineView, setTimelineView] = useState<'gantt' | 'flow'>('gantt');
|
||||
const { data: correlationData } = useCorrelationChain(detail?.correlationId ?? null);
|
||||
const { data: diagram } = useDiagramLayout(detail?.diagramContentHash ?? null);
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const procList = detail ? (detail.processors?.length ? detail.processors : (detail.children ?? [])) : [];
|
||||
const { data: detail, isLoading } = useExecutionDetail(id ?? null)
|
||||
const { data: correlationData } = useCorrelationChain(detail?.correlationId ?? null)
|
||||
const { data: diagram } = useDiagramLayout(detail?.diagramContentHash ?? null)
|
||||
|
||||
// Auto-select first failed processor, or 0
|
||||
const defaultIndex = useMemo(() => {
|
||||
if (!procList.length) return 0;
|
||||
const failIdx = procList.findIndex((p: any) =>
|
||||
(p.status || '').toUpperCase() === 'FAILED' || p.status === 'fail'
|
||||
);
|
||||
return failIdx >= 0 ? failIdx : 0;
|
||||
}, [procList]);
|
||||
const [timelineView, setTimelineView] = useState<'gantt' | 'flow'>('gantt')
|
||||
|
||||
const [selectedProcessorIndex, setSelectedProcessorIndex] = useState<number | null>(null);
|
||||
const activeIndex = selectedProcessorIndex ?? defaultIndex;
|
||||
const procList = detail
|
||||
? (detail.processors?.length ? detail.processors : (detail.children ?? []))
|
||||
: []
|
||||
|
||||
const { data: snapshot } = useProcessorSnapshot(id ?? null, procList.length > 0 ? activeIndex : null);
|
||||
|
||||
const processors = useMemo(() => {
|
||||
if (!procList.length) return [];
|
||||
const result: any[] = [];
|
||||
let offset = 0;
|
||||
// Flatten processor tree into ProcessorStep[]
|
||||
const processors: ProcessorStep[] = useMemo(() => {
|
||||
if (!procList.length) return []
|
||||
const result: ProcessorStep[] = []
|
||||
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',
|
||||
status: procStatusToStep(node.status ?? ''),
|
||||
startMs: offset,
|
||||
});
|
||||
offset += node.durationMs ?? 0;
|
||||
if (node.children) node.children.forEach(walk);
|
||||
})
|
||||
offset += node.durationMs ?? 0
|
||||
if (node.children) node.children.forEach(walk)
|
||||
}
|
||||
procList.forEach(walk);
|
||||
return result;
|
||||
}, [procList]);
|
||||
procList.forEach(walk)
|
||||
return result
|
||||
}, [procList])
|
||||
|
||||
const selectedProc = processors[activeIndex];
|
||||
const isSelectedFailed = selectedProc?.status === 'fail';
|
||||
// Default selected processor: first failed, or 0
|
||||
const defaultIndex = useMemo(() => {
|
||||
if (!processors.length) return 0
|
||||
const failIdx = processors.findIndex((p) => p.status === 'fail')
|
||||
return failIdx >= 0 ? failIdx : 0
|
||||
}, [processors])
|
||||
|
||||
// Parse snapshot headers
|
||||
const inputHeaders = parseHeaders(snapshot?.inputHeaders);
|
||||
const outputHeaders = parseHeaders(snapshot?.outputHeaders);
|
||||
const inputBody = snapshot?.inputBody ?? null;
|
||||
const outputBody = snapshot?.outputBody ?? null;
|
||||
const [selectedProcessorIndex, setSelectedProcessorIndex] = useState<number | null>(null)
|
||||
const activeIndex = selectedProcessorIndex ?? defaultIndex
|
||||
|
||||
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>;
|
||||
const { data: snapshot } = useProcessorSnapshot(
|
||||
id ?? null,
|
||||
procList.length > 0 ? activeIndex : null,
|
||||
)
|
||||
|
||||
const selectedProc = processors[activeIndex]
|
||||
const isSelectedFailed = selectedProc?.status === 'fail'
|
||||
|
||||
// Parse snapshot data
|
||||
const inputHeaders = parseHeaders(snapshot?.inputHeaders)
|
||||
const outputHeaders = parseHeaders(snapshot?.outputHeaders)
|
||||
const inputBody = snapshot?.inputBody ?? null
|
||||
const outputBody = snapshot?.outputBody ?? null
|
||||
|
||||
// Build RouteFlow nodes from diagram + execution data
|
||||
const routeNodes: RouteNode[] = useMemo(() => {
|
||||
if (diagram?.nodes) {
|
||||
return mapDiagramToRouteNodes(diagram.nodes, procList)
|
||||
}
|
||||
// Fallback: build from processor list
|
||||
return processors.map((p) => ({
|
||||
name: p.name,
|
||||
type: 'process' as RouteNode['type'],
|
||||
durationMs: p.durationMs,
|
||||
status: p.status,
|
||||
}))
|
||||
}, [diagram, processors, procList])
|
||||
|
||||
// Correlation chain
|
||||
const correlatedExchanges = useMemo(() => {
|
||||
if (!correlationData?.data || correlationData.data.length <= 1) return []
|
||||
return correlationData.data
|
||||
}, [correlationData])
|
||||
|
||||
// ── Loading state ────────────────────────────────────────────────────────
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={styles.loadingContainer}>
|
||||
<Spinner size="lg" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Not found state ──────────────────────────────────────────────────────
|
||||
if (!detail) {
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<Breadcrumb items={[
|
||||
{ label: 'Applications', href: '/apps' },
|
||||
{ label: 'Exchanges' },
|
||||
{ label: id ?? 'Unknown' },
|
||||
]} />
|
||||
<InfoCallout variant="warning">Exchange "{id}" not found.</InfoCallout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const statusVariant = backendStatusToVariant(detail.status)
|
||||
const statusLabel = backendStatusToLabel(detail.status)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.content}>
|
||||
|
||||
{/* Breadcrumb */}
|
||||
<Breadcrumb items={[
|
||||
{ label: 'Dashboard', href: '/apps' },
|
||||
{ label: 'Applications', href: '/apps' },
|
||||
{ label: detail.applicationName || 'App', href: `/apps/${detail.applicationName}` },
|
||||
{ label: id?.slice(0, 12) || '' },
|
||||
{ label: detail.routeId, href: `/apps/${detail.applicationName}/${detail.routeId}` },
|
||||
{ label: detail.executionId?.slice(0, 12) || '' },
|
||||
]} />
|
||||
|
||||
{/* Exchange header card */}
|
||||
<div className={styles.exchangeHeader}>
|
||||
<div className={styles.headerRow}>
|
||||
<div className={styles.headerLeft}>
|
||||
<StatusDot variant={detail.status === 'COMPLETED' ? 'success' : detail.status === 'FAILED' ? 'error' : 'running'} />
|
||||
<StatusDot variant={statusVariant} />
|
||||
<div>
|
||||
<div className={styles.exchangeId}>
|
||||
<MonoText size="md">{id}</MonoText>
|
||||
<Badge label={detail.status} color={detail.status === 'COMPLETED' ? 'success' : 'error'} variant="filled" />
|
||||
<MonoText size="md">{detail.executionId}</MonoText>
|
||||
<Badge label={statusLabel} color={statusVariant} variant="filled" />
|
||||
</div>
|
||||
<div className={styles.exchangeRoute}>
|
||||
Route: <span className={styles.routeLink} onClick={() => navigate(`/apps/${detail.applicationName}/${detail.routeId}`)}>{detail.routeId}</span>
|
||||
@@ -116,6 +191,12 @@ export default function ExchangeDetail() {
|
||||
App: <MonoText size="xs">{detail.applicationName}</MonoText>
|
||||
</>
|
||||
)}
|
||||
{detail.correlationId && (
|
||||
<>
|
||||
<span className={styles.headerDivider}>·</span>
|
||||
Correlation: <MonoText size="xs">{detail.correlationId}</MonoText>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,7 +212,9 @@ export default function ExchangeDetail() {
|
||||
<div className={styles.headerStat}>
|
||||
<div className={styles.headerStatLabel}>Started</div>
|
||||
<div className={styles.headerStatValue}>
|
||||
{detail.startTime ? new Date(detail.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '—'}
|
||||
{detail.startTime
|
||||
? new Date(detail.startTime).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
||||
: '\u2014'}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.headerStat}>
|
||||
@@ -142,43 +225,39 @@ export default function ExchangeDetail() {
|
||||
</div>
|
||||
|
||||
{/* Correlation Chain */}
|
||||
{correlationData?.data && correlationData.data.length > 1 && (
|
||||
{correlatedExchanges.length > 1 && (
|
||||
<div className={styles.correlationChain}>
|
||||
<span className={styles.chainLabel}>Correlated Exchanges</span>
|
||||
{correlationData.data.map((exec: any) => {
|
||||
const isCurrent = exec.executionId === id;
|
||||
const variant = exec.status === 'COMPLETED' ? 'success' : exec.status === 'FAILED' ? 'error' : 'running';
|
||||
{correlatedExchanges.map((ce) => {
|
||||
const isCurrent = ce.executionId === id
|
||||
const variant = backendStatusToVariant(ce.status)
|
||||
const statusCls =
|
||||
variant === 'success' ? styles.chainNodeSuccess
|
||||
: variant === 'error' ? styles.chainNodeError
|
||||
: styles.chainNodeRunning;
|
||||
: variant === 'running' ? styles.chainNodeRunning
|
||||
: styles.chainNodeWarning
|
||||
return (
|
||||
<button
|
||||
key={exec.executionId}
|
||||
key={ce.executionId}
|
||||
className={`${styles.chainNode} ${statusCls} ${isCurrent ? styles.chainNodeCurrent : ''}`}
|
||||
onClick={() => { if (!isCurrent) navigate(`/exchanges/${exec.executionId}`); }}
|
||||
title={`${exec.executionId} — ${exec.routeId}`}
|
||||
onClick={() => {
|
||||
if (!isCurrent) navigate(`/exchanges/${ce.executionId}`)
|
||||
}}
|
||||
title={`${ce.executionId} \u2014 ${ce.routeId}`}
|
||||
>
|
||||
<StatusDot variant={variant as any} />
|
||||
<span>{exec.routeId}</span>
|
||||
<StatusDot variant={variant} />
|
||||
<span>{ce.routeId}</span>
|
||||
</button>
|
||||
);
|
||||
)
|
||||
})}
|
||||
{correlationData.total > 20 && (
|
||||
{correlationData && correlationData.total > 20 && (
|
||||
<span className={styles.chainMore}>+{correlationData.total - 20} more</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Error callout */}
|
||||
{detail.errorMessage && (
|
||||
<InfoCallout variant="error">
|
||||
{detail.errorMessage}
|
||||
</InfoCallout>
|
||||
)}
|
||||
|
||||
{/* Processor Timeline / Flow Section */}
|
||||
{/* Processor Timeline Section */}
|
||||
<div className={styles.timelineSection}>
|
||||
<div className={styles.timelineHeader}>
|
||||
<span className={styles.timelineTitle}>
|
||||
@@ -206,17 +285,17 @@ export default function ExchangeDetail() {
|
||||
<ProcessorTimeline
|
||||
processors={processors}
|
||||
totalMs={detail.durationMs}
|
||||
onProcessorClick={(_p, i) => setSelectedProcessorIndex(i)}
|
||||
onProcessorClick={(_proc, index) => setSelectedProcessorIndex(index)}
|
||||
selectedIndex={activeIndex}
|
||||
/>
|
||||
) : (
|
||||
<InfoCallout>No processor data available</InfoCallout>
|
||||
)
|
||||
) : (
|
||||
diagram ? (
|
||||
routeNodes.length > 0 ? (
|
||||
<RouteFlow
|
||||
nodes={mapDiagramToRouteNodes(diagram.nodes || [], procList)}
|
||||
onNodeClick={(_node, i) => setSelectedProcessorIndex(i)}
|
||||
nodes={routeNodes}
|
||||
onNodeClick={(_node, index) => setSelectedProcessorIndex(index)}
|
||||
selectedIndex={activeIndex}
|
||||
/>
|
||||
) : (
|
||||
@@ -226,7 +305,7 @@ export default function ExchangeDetail() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Processor Detail: Message IN / Message OUT or Error */}
|
||||
{/* Processor Detail Panel (split IN / OUT) */}
|
||||
{selectedProc && snapshot && (
|
||||
<div className={styles.detailSplit}>
|
||||
{/* Message IN */}
|
||||
@@ -255,7 +334,7 @@ export default function ExchangeDetail() {
|
||||
)}
|
||||
<div className={styles.bodySection}>
|
||||
<div className={styles.sectionLabel}>Body</div>
|
||||
<CodeBlock content={inputBody ?? 'null'} />
|
||||
<CodeBlock content={inputBody ?? 'null'} language="json" copyable />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -309,7 +388,7 @@ export default function ExchangeDetail() {
|
||||
)}
|
||||
<div className={styles.bodySection}>
|
||||
<div className={styles.sectionLabel}>Body</div>
|
||||
<CodeBlock content={outputBody ?? 'null'} />
|
||||
<CodeBlock content={outputBody ?? 'null'} language="json" copyable />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -317,12 +396,13 @@ export default function ExchangeDetail() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No snapshot loaded yet - show prompt */}
|
||||
{/* Snapshot loading indicator */}
|
||||
{selectedProc && !snapshot && procList.length > 0 && (
|
||||
<div style={{ color: 'var(--text-muted)', fontSize: 12, textAlign: 'center', padding: 20 }}>
|
||||
<div className={styles.snapshotLoading}>
|
||||
Loading exchange snapshot...
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user