import { useState, useMemo } from 'react'; import { useParams, useNavigate } from 'react-router'; import { StatCard, StatusDot, Badge, MonoText, DataTable, DetailPanel, ProcessorTimeline, RouteFlow, Alert, Collapsible, CodeBlock, ShortcutsBar, } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; import { useSearchExecutions, useExecutionStats, useStatsTimeseries, useExecutionDetail } from '../../api/queries/executions'; import { useDiagramByRoute } from '../../api/queries/diagrams'; import { useGlobalFilters } from '@cameleer/design-system'; import type { ExecutionSummary } from '../../api/types'; import { mapDiagramToRouteNodes } from '../../utils/diagram-mapping'; import styles from './Dashboard.module.css'; interface Row extends ExecutionSummary { id: string } function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms`; return `${(ms / 1000).toFixed(1)}s`; } export default function Dashboard() { const { appId, routeId } = useParams(); const navigate = useNavigate(); const { timeRange } = useGlobalFilters(); const timeFrom = timeRange.start.toISOString(); const timeTo = timeRange.end.toISOString(); const [selectedId, setSelectedId] = useState(null); const timeWindowSeconds = (timeRange.end.getTime() - timeRange.start.getTime()) / 1000; const { data: stats } = useExecutionStats(timeFrom, timeTo, routeId, appId); const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, routeId, appId); const { data: searchResult } = useSearchExecutions({ timeFrom, timeTo, routeId: routeId || undefined, group: appId || undefined, offset: 0, limit: 50, }, true); const { data: detail } = useExecutionDetail(selectedId); const rows: Row[] = useMemo(() => (searchResult?.data || []).map((e: ExecutionSummary) => ({ ...e, id: e.executionId })), [searchResult], ); const selectedRow = rows.find(r => r.id === selectedId); const { data: diagram } = useDiagramByRoute(detail?.groupName ?? selectedRow?.groupName, detail?.routeId); const totalCount = stats?.totalCount ?? 0; const failedCount = stats?.failedCount ?? 0; const successRate = totalCount > 0 ? ((totalCount - failedCount) / totalCount * 100) : 100; const throughput = timeWindowSeconds > 0 ? totalCount / timeWindowSeconds : 0; const sparkExchanges = useMemo(() => (timeseries?.buckets || []).map((b: any) => b.totalCount as number), [timeseries]); const sparkErrors = useMemo(() => (timeseries?.buckets || []).map((b: any) => b.failedCount as number), [timeseries]); const sparkLatency = useMemo(() => (timeseries?.buckets || []).map((b: any) => b.p99DurationMs as number), [timeseries]); const sparkThroughput = useMemo(() => (timeseries?.buckets || []).map((b: any) => { const bucketSeconds = timeWindowSeconds / Math.max((timeseries?.buckets || []).length, 1); return bucketSeconds > 0 ? (b.totalCount as number) / bucketSeconds : 0; }), [timeseries, timeWindowSeconds]); const prevTotal = stats?.prevTotalCount ?? 0; const prevFailed = stats?.prevFailedCount ?? 0; const exchangeTrend = prevTotal > 0 ? ((totalCount - prevTotal) / prevTotal * 100) : 0; const prevSuccessRate = prevTotal > 0 ? ((prevTotal - prevFailed) / prevTotal * 100) : 100; const successRateDelta = successRate - prevSuccessRate; const errorDelta = failedCount - prevFailed; const columns: Column[] = [ { key: 'status', header: 'Status', width: '80px', render: (v, row) => ( {v === 'COMPLETED' ? 'OK' : v === 'FAILED' ? 'ERR' : 'RUN'} ), }, { key: '_inspect' as any, header: '', width: '36px', render: (_v, row) => ( { e.stopPropagation(); e.preventDefault(); navigate(`/exchanges/${row.executionId}`); }} className={styles.inspectLink} title="Open full details" >↗ ), }, { key: 'routeId', header: 'Route', sortable: true, render: (v) => {String(v)} }, { key: 'groupName', header: 'Application', sortable: true, render: (v) => {String(v ?? '')} }, { key: 'executionId', header: 'Exchange ID', sortable: true, render: (v) => {String(v)} }, { key: 'startTime', header: 'Started', sortable: true, render: (v) => {new Date(v as string).toISOString().replace('T', ' ').slice(0, 19)} }, { key: 'durationMs', header: 'Duration', sortable: true, render: (v) => {formatDuration(v as number)}, }, { key: 'agentId', header: 'Agent', render: (v) => v ? : null, }, ]; const procList = detail ? (detail.processors?.length ? detail.processors : (detail.children ?? [])) : []; return (
0 ? 'up' : exchangeTrend < 0 ? 'down' : 'neutral'} trendValue={exchangeTrend > 0 ? `+${exchangeTrend.toFixed(0)}%` : `${exchangeTrend.toFixed(0)}%`} sparkline={sparkExchanges} accent="amber" /> = 0 ? 'up' : 'down'} trendValue={`${successRateDelta >= 0 ? '+' : ''}${successRateDelta.toFixed(1)}%`} accent="success" /> 0 ? 'up' : errorDelta < 0 ? 'down' : 'neutral'} trendValue={errorDelta > 0 ? `+${errorDelta}` : `${errorDelta}`} sparkline={sparkErrors} accent="error" />
Recent Exchanges
{rows.length} of {searchResult?.total ?? 0} exchanges
{ setSelectedId(row.id); }} selectedId={selectedId ?? undefined} sortable pageSize={25} />
{selectedId && detail && ( setSelectedId(null)} title={`${detail.routeId} — ${selectedId.slice(0, 12)}`} className={styles.detailPanelOverride} > {/* Open full details link */}
{/* Overview */}
Overview
Status {detail.status}
Duration {formatDuration(detail.durationMs)}
Route {detail.routeId}
Agent {detail.agentId ?? '—'}
Correlation {detail.correlationId ?? '—'}
Timestamp {detail.startTime ? new Date(detail.startTime).toISOString().replace('T', ' ').slice(0, 19) : '—'}
{/* Errors */} {detail.errorMessage && (
Errors
{detail.errorMessage.split(':')[0]}
{detail.errorMessage.includes(':') ? detail.errorMessage.substring(detail.errorMessage.indexOf(':') + 1).trim() : ''}
{detail.errorStackTrace && ( )}
)} {/* Route Flow */}
Route Flow
{diagram ? ( {}} /> ) :
No diagram available
}
{/* Processor Timeline */}
Processor Timeline {formatDuration(detail.durationMs)}
{procList.length ? ( ) :
No processor data
}
)}
); } function flattenProcessors(nodes: any[]): any[] { 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); } nodes.forEach(walk); return result; }