import { useState, useMemo } from 'react'; import { useParams } from 'react-router'; import { StatCard, StatusDot, Badge, MonoText, Sparkline, DataTable, DetailPanel, ProcessorTimeline, RouteFlow, Alert, Collapsible, CodeBlock, } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; import { useSearchExecutions, useExecutionStats, useStatsTimeseries, useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions'; import { useGlobalFilters } from '@cameleer/design-system'; import type { ExecutionSummary } from '../../api/types'; import styles from './Dashboard.module.css'; interface Row extends ExecutionSummary { id: string } export default function Dashboard() { const { appId, routeId } = useParams(); const { timeRange } = useGlobalFilters(); const timeFrom = timeRange.start.toISOString(); const timeTo = timeRange.end.toISOString(); const [selectedId, setSelectedId] = useState(null); const [detailTab, setDetailTab] = useState('overview'); const [processorIdx, setProcessorIdx] = 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 { data: snapshot } = useProcessorSnapshot(selectedId, processorIdx); const rows: Row[] = useMemo(() => (searchResult?.data || []).map((e: ExecutionSummary) => ({ ...e, id: e.executionId })), [searchResult], ); const sparklineData = useMemo(() => (timeseries?.buckets || []).map((b: any) => b.totalCount as number), [timeseries], ); const columns: Column[] = [ { key: 'status', header: 'Status', width: '80px', render: (v) => , }, { key: 'routeId', header: 'Route', render: (v) => {String(v)} }, { key: 'groupName', header: 'App', render: (v) => }, { key: 'executionId', header: 'Exchange ID', render: (v) => {String(v).slice(0, 12)} }, { key: 'startTime', header: 'Started', sortable: true, render: (v) => new Date(v as string).toLocaleTimeString() }, { key: 'durationMs', header: 'Duration', sortable: true, render: (v) => `${v}ms`, }, ]; const detailTabs = detail ? [ { label: 'Overview', value: 'overview', content: ( <>
Details
Exchange ID {detail.executionId}
Status
Route {detail.routeId}
Duration {detail.durationMs}ms
{detail.errorMessage && (
Errors
{detail.errorMessage.split(':')[0]}
{detail.errorMessage.includes(':') ? detail.errorMessage.substring(detail.errorMessage.indexOf(':') + 1).trim() : ''}
{detail.errorStackTrace && ( )}
)} ), }, { label: 'Processors', value: 'processors', content: (() => { const procList = detail.processors?.length ? detail.processors : (detail.children ?? []); return procList.length ? ( setProcessorIdx(i)} selectedIndex={processorIdx ?? undefined} /> ) :
No processor data
; })(), }, ] : []; return (
0 ? `${((stats?.totalCount ?? 0) / timeWindowSeconds).toFixed(2)} ex/s` : '0.00 ex/s'} sparkline={sparklineData} /> 0 ? `${((stats?.failedCount ?? 0) / stats!.totalCount * 100).toFixed(1)}%` : '0.0%'} accent="error" />
Recent Exchanges
{rows.length} results
{ setSelectedId(row.id); setProcessorIdx(null); }} selectedId={selectedId ?? undefined} sortable pageSize={25} />
setSelectedId(null)} title={selectedId ? `Exchange ${selectedId.slice(0, 12)}...` : ''} tabs={detailTabs} />
); } 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; }