import React, { useState, useMemo } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { TrendingUp, TrendingDown, ArrowRight, ExternalLink, AlertTriangle } from 'lucide-react' import styles from './Dashboard.module.css' // Layout import { TopBar } from '../../design-system/layout/TopBar/TopBar' // Composites import { DataTable } from '../../design-system/composites/DataTable/DataTable' import type { Column } from '../../design-system/composites/DataTable/types' import { DetailPanel } from '../../design-system/composites/DetailPanel/DetailPanel' import { ShortcutsBar } from '../../design-system/composites/ShortcutsBar/ShortcutsBar' import { ProcessorTimeline } from '../../design-system/composites/ProcessorTimeline/ProcessorTimeline' import { RouteFlow } from '../../design-system/composites/RouteFlow/RouteFlow' import type { RouteNode } from '../../design-system/composites/RouteFlow/RouteFlow' import { KpiStrip } from '../../design-system/composites/KpiStrip/KpiStrip' import type { KpiItem } from '../../design-system/composites/KpiStrip/KpiStrip' // Primitives import { StatusDot } from '../../design-system/primitives/StatusDot/StatusDot' import { MonoText } from '../../design-system/primitives/MonoText/MonoText' import { Badge } from '../../design-system/primitives/Badge/Badge' // Global filters import { useGlobalFilters } from '../../design-system/providers/GlobalFilterProvider' // Mock data import { exchanges, type Exchange } from '../../mocks/exchanges' import { kpiMetrics, type KpiMetric } from '../../mocks/metrics' import { SIDEBAR_APPS, buildRouteToAppMap } from '../../mocks/sidebar' // Route → Application lookup const ROUTE_TO_APP = buildRouteToAppMap() // ─── KPI mapping ───────────────────────────────────────────────────────────── const ACCENT_TO_COLOR: Record = { amber: 'var(--amber)', success: 'var(--success)', error: 'var(--error)', running: 'var(--running)', warning: 'var(--warning)', } const TREND_ICONS: Record = { up: , down: , neutral: , } function sentimentToVariant(sentiment: KpiMetric['trendSentiment']): 'success' | 'error' | 'muted' { switch (sentiment) { case 'good': return 'success' case 'bad': return 'error' case 'neutral': return 'muted' } } const kpiItems: KpiItem[] = kpiMetrics.map((m) => ({ label: m.label, value: m.unit ? `${m.value} ${m.unit}` : m.value, trend: { label: <>{TREND_ICONS[m.trend]} {m.trendValue}, variant: sentimentToVariant(m.trendSentiment) }, subtitle: m.detail, sparkline: m.sparkline, borderColor: ACCENT_TO_COLOR[m.accent], })) // ─── 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 formatTimestamp(date: Date): string { 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: Exchange['status']): 'success' | 'error' | 'running' | 'warning' { switch (status) { case 'completed': return 'success' case 'failed': return 'error' case 'running': return 'running' case 'warning': return 'warning' } } function statusLabel(status: Exchange['status']): string { switch (status) { case 'completed': return 'OK' case 'failed': return 'ERR' case 'running': return 'RUN' case 'warning': return 'WARN' } } // ─── Table columns (base, without navigate action) ────────────────────────── const BASE_COLUMNS: Column[] = [ { key: 'status', header: 'Status', width: '80px', render: (_, row) => ( {statusLabel(row.status)} ), }, { key: 'route', header: 'Route', sortable: true, render: (_, row) => ( {row.route} ), }, { key: 'routeGroup', header: 'Application', sortable: true, render: (_, row) => ( {ROUTE_TO_APP.get(row.route) ?? row.routeGroup} ), }, { key: 'id', header: 'Exchange ID', sortable: true, render: (_, row) => ( {row.id} ), }, { key: 'timestamp', header: 'Started', sortable: true, render: (_, row) => ( {formatTimestamp(row.timestamp)} ), }, { key: 'durationMs', header: 'Duration', sortable: true, render: (_, row) => ( {formatDuration(row.durationMs)} ), }, { key: 'agent', header: 'Agent', render: (_, row) => ( {row.agent} ), }, ] function durationClass(ms: number, status: Exchange['status']): 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 } const SHORTCUTS = [ { keys: 'Ctrl+K', label: 'Search' }, { keys: '↑↓', label: 'Navigate rows' }, { keys: 'Enter', label: 'Open detail' }, { keys: 'Esc', label: 'Close panel' }, ] // ─── Dashboard component ────────────────────────────────────────────────────── export function Dashboard() { const { id: appId, routeId } = useParams<{ id: string; routeId: string }>() const navigate = useNavigate() const [selectedId, setSelectedId] = useState() const [panelOpen, setPanelOpen] = useState(false) const [selectedExchange, setSelectedExchange] = useState(null) // Build columns with inspect action as second column const COLUMNS: Column[] = useMemo(() => { const inspectCol: Column = { key: 'correlationId' as keyof Exchange, header: '', width: '36px', render: (_, row) => ( ), } const [statusCol, ...rest] = BASE_COLUMNS return [statusCol, inspectCol, ...rest] }, [navigate]) const { isInTimeRange, statusFilters } = useGlobalFilters() // Build set of route IDs belonging to the selected app (if any) const appRouteIds = useMemo(() => { if (!appId) return null const app = SIDEBAR_APPS.find((a) => a.id === appId) if (!app) return null return new Set(app.routes.map((r) => r.id)) }, [appId]) // Scope all data to the selected app (and optionally route) const scopedExchanges = useMemo(() => { if (routeId) return exchanges.filter((e) => e.route === routeId) if (!appRouteIds) return exchanges return exchanges.filter((e) => appRouteIds.has(e.route)) }, [appRouteIds, routeId]) // Filter exchanges (scoped + global filters) const filteredExchanges = useMemo(() => { let data = scopedExchanges // Time range filter data = data.filter((e) => isInTimeRange(e.timestamp)) // Status filter if (statusFilters.size > 0) { data = data.filter((e) => statusFilters.has(e.status)) } return data }, [scopedExchanges, isInTimeRange, statusFilters]) function handleRowClick(row: Exchange) { setSelectedId(row.id) setSelectedExchange(row) setPanelOpen(true) } function handleRowAccent(row: Exchange): 'error' | 'warning' | undefined { if (row.status === 'failed') return 'error' if (row.status === 'warning') return 'warning' return undefined } // Map processor types to RouteNode types function toRouteNodeType(procType: string): RouteNode['type'] { switch (procType) { case 'consumer': return 'from' case 'transform': return 'process' case 'enrich': return 'process' default: return procType as RouteNode['type'] } } // Build RouteFlow nodes from exchange processors const routeNodes: RouteNode[] = selectedExchange ? selectedExchange.processors.map((p) => ({ name: p.name, type: toRouteNodeType(p.type), durationMs: p.durationMs, status: p.status, })) : [] // Collect errors from processors const processorErrors = selectedExchange ? selectedExchange.processors.filter((p) => p.status === 'fail') : [] const hasExchangeError = selectedExchange?.errorMessage != null const totalErrors = processorErrors.length + (hasExchangeError && processorErrors.length === 0 ? 1 : 0) return ( <> {/* Top bar */} {/* Scrollable content */}
{/* Health strip */} {/* Exchanges table */}
Recent Exchanges
{filteredExchanges.length.toLocaleString()} of {scopedExchanges.length.toLocaleString()} exchanges
row.errorMessage ? (
{row.errorMessage}
Click to view full stack trace
) : null } />
{/* Shortcuts bar */} {/* Detail panel (portals itself) */} {selectedExchange && ( setPanelOpen(false)} title={`${selectedExchange.orderId} — ${selectedExchange.route}`} > {/* Link to full detail page */}
{/* Overview */}
Overview
Status {statusLabel(selectedExchange.status)}
Duration {formatDuration(selectedExchange.durationMs)}
Route {selectedExchange.route}
Customer {selectedExchange.customer}
Agent {selectedExchange.agent}
Correlation {selectedExchange.correlationId}
Timestamp {selectedExchange.timestamp.toISOString()}
{/* Errors */} {totalErrors > 0 && (
Errors {totalErrors > 1 && ( )}
{selectedExchange.errorClass ?? processorErrors[0]?.name}
{selectedExchange.errorMessage ?? `Failed at processor: ${processorErrors[0]?.name}`}
)} {/* Route Flow */}
Route Flow
{/* Processor Timeline */}
Processor Timeline {formatDuration(selectedExchange.durationMs)}
)} ) }