import { useState, useMemo, useCallback, useEffect } from 'react' import { useParams, useNavigate, useSearchParams } from 'react-router' import { AlertTriangle, X, Search, Footprints, RotateCcw } from 'lucide-react' import { DataTable, StatusDot, MonoText, Badge, useGlobalFilters, } from '@cameleer/design-system' import type { Column } from '@cameleer/design-system' import { useSearchExecutions, } from '../../api/queries/executions' import { useEnvironmentStore } from '../../api/environment-store' import type { ExecutionSummary } from '../../api/types' import { attributeBadgeColor } from '../../utils/attribute-color' import { formatDuration, statusLabel } from '../../utils/format-utils' import styles from './Dashboard.module.css' // Row type extends ExecutionSummary with an `id` field for DataTable interface Row extends ExecutionSummary { id: string } // ─── Helpers ───────────────────────────────────────────────────────────────── function formatTimestamp(iso: string): string { const date = new Date(iso) const y = date.getFullYear() const mo = String(date.getMonth() + 1).padStart(2, '0') const d = String(date.getDate()).padStart(2, '0') const h = String(date.getHours()).padStart(2, '0') const mi = String(date.getMinutes()).padStart(2, '0') const s = String(date.getSeconds()).padStart(2, '0') return `${y}-${mo}-${d} ${h}:${mi}:${s}` } function statusToVariant(status: string): 'success' | 'error' | 'running' | 'warning' { switch (status) { case 'COMPLETED': return 'success' case 'FAILED': return 'error' case 'RUNNING': return 'running' default: return 'warning' } } function durationClass(ms: number, status: string): string { if (status === 'FAILED') return styles.durBreach if (ms < 100) return styles.durFast if (ms < 200) return styles.durNormal if (ms < 300) return styles.durSlow return styles.durBreach } // ─── Table columns (base, without inspect action) ──────────────────────────── function buildBaseColumns(): Column[] { return [ { key: 'status', header: 'Status', width: '80px', render: (_: unknown, row: Row) => ( {statusLabel(row.status)} {row.hasTraceData && } {row.isReplay && } ), }, { key: 'routeId', header: 'Route', sortable: true, render: (_: unknown, row: Row) => ( {row.routeId} ), }, { key: 'applicationId', header: 'Application', sortable: true, render: (_: unknown, row: Row) => ( {row.applicationId ?? ''} ), }, { key: 'attributes', header: 'Attributes', render: (_, row) => { const attrs = row.attributes; if (!attrs || Object.keys(attrs).length === 0) return ; const entries = Object.entries(attrs); const shown = entries.slice(0, 2); const overflow = entries.length - 2; return (
{shown.map(([k, v]) => ( ))} {overflow > 0 && +{overflow}}
); }, }, { key: 'executionId', header: 'Exchange ID', sortable: true, render: (_: unknown, row: Row) => ( {row.executionId} ), }, { key: 'startTime', header: 'Started', sortable: true, render: (_: unknown, row: Row) => ( {formatTimestamp(row.startTime)} ), }, { key: 'durationMs', header: 'Duration', sortable: true, render: (_: unknown, row: Row) => ( {formatDuration(row.durationMs)} ), }, { key: 'instanceId', header: 'Agent', render: (_: unknown, row: Row) => ( {row.instanceId} ), }, ] } // ─── Dashboard component ───────────────────────────────────────────────────── export interface SelectedExchange { executionId: string; applicationId: string; routeId: string; } interface DashboardProps { onExchangeSelect?: (exchange: SelectedExchange) => void; activeExchangeId?: string; } export default function Dashboard({ onExchangeSelect, activeExchangeId }: DashboardProps = {}) { const { appId, routeId } = useParams<{ appId: string; routeId: string }>() const navigate = useNavigate() const [searchParams, setSearchParams] = useSearchParams() const textFilter = searchParams.get('text') || undefined const [selectedId, setSelectedId] = useState(activeExchangeId) const [sortField, setSortField] = useState('startTime') const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc') // Sync selection from parent (survives remount when split view toggles) useEffect(() => { if (activeExchangeId !== undefined) setSelectedId(activeExchangeId); }, [activeExchangeId]); const selectedEnv = useEnvironmentStore((s) => s.environment); const { timeRange, statusFilters } = useGlobalFilters() const timeFrom = timeRange.start.toISOString() const timeTo = timeRange.end.toISOString() const handleSortChange = useCallback((key: string, dir: 'asc' | 'desc') => { setSortField(key) setSortDir(dir) }, []) // ─── API hooks ─────────────────────────────────────────────────────────── // Convert design-system status filters (lowercase) to API status param (uppercase) const statusParam = statusFilters.size > 0 ? [...statusFilters].map(s => s.toUpperCase()).join(',') : undefined const { data: searchResult } = useSearchExecutions( { timeFrom, timeTo, routeId: routeId || undefined, applicationId: appId || undefined, environment: selectedEnv, status: statusParam, text: textFilter, sortField, sortDir, offset: 0, limit: textFilter ? 200 : 50, }, !textFilter, ) // ─── Rows ──────────────────────────────────────────────────────────────── const rows: Row[] = useMemo( () => (searchResult?.data || []).map((e: ExecutionSummary) => ({ ...e, id: e.executionId })), [searchResult], ) // ─── Table columns ────────────────────────────────────────────────────── const columns: Column[] = useMemo(() => buildBaseColumns(), []) // ─── Row click → navigate to diagram view ──────────────────────────────── function handleRowClick(row: Row) { setSelectedId(row.id) if (onExchangeSelect) { onExchangeSelect({ executionId: row.executionId, applicationId: row.applicationId ?? '', routeId: row.routeId, }) } } function handleRowAccent(row: Row): 'error' | 'warning' | undefined { if (row.status === 'FAILED') return 'error' return undefined } return (
{textFilter ? ( <> Search: “{textFilter}” ) : 'Recent Exchanges'}
{rows.length.toLocaleString()} of {(searchResult?.total ?? 0).toLocaleString()} exchanges {!textFilter && }
row.errorMessage ? (
{row.errorMessage}
Click to view full stack trace
) : null } />
) }