import { useState, useMemo } from 'react' import { useParams } from 'react-router-dom' import styles from './Dashboard.module.css' // Layout import { AppShell } from '../../design-system/layout/AppShell/AppShell' import { Sidebar } from '../../design-system/layout/Sidebar/Sidebar' import { TopBar } from '../../design-system/layout/TopBar/TopBar' // Composites import { FilterBar } from '../../design-system/composites/FilterBar/FilterBar' import type { ActiveFilter } from '../../design-system/composites/FilterBar/FilterBar' 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 { CommandPalette } from '../../design-system/composites/CommandPalette/CommandPalette' import type { SearchResult } from '../../design-system/composites/CommandPalette/types' import { ShortcutsBar } from '../../design-system/composites/ShortcutsBar/ShortcutsBar' import { ProcessorTimeline } from '../../design-system/composites/ProcessorTimeline/ProcessorTimeline' // Primitives import { StatCard } from '../../design-system/primitives/StatCard/StatCard' import { StatusDot } from '../../design-system/primitives/StatusDot/StatusDot' import { MonoText } from '../../design-system/primitives/MonoText/MonoText' import { Badge } from '../../design-system/primitives/Badge/Badge' // Mock data import { exchanges, type Exchange } from '../../mocks/exchanges' import { routes } from '../../mocks/routes' import { agents } from '../../mocks/agents' import { kpiMetrics } from '../../mocks/metrics' import { SIDEBAR_APPS } from '../../mocks/sidebar' // ─── 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 { return date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) } 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 ──────────────────────────────────────────────────────────── const COLUMNS: Column[] = [ { key: 'status', header: 'Status', width: '80px', render: (_, row) => ( {statusLabel(row.status)} ), }, { key: 'route', header: 'Route', sortable: true, render: (_, row) => (
{row.route}
{row.routeGroup}
), }, { key: 'orderId', header: 'Order ID', sortable: true, render: (_, row) => ( {row.orderId} ), }, { key: 'customer', header: 'Customer', render: (_, row) => ( {row.customer} ), }, { 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 } // ─── Build CommandPalette search data ──────────────────────────────────────── function buildSearchData( exs: Exchange[], rts: typeof routes, ags: typeof agents, ): SearchResult[] { const results: SearchResult[] = [] for (const exec of exs) { results.push({ id: exec.id, category: 'exchange', title: `${exec.orderId} — ${exec.route}`, badges: [{ label: statusLabel(exec.status), color: statusToVariant(exec.status) }], meta: `${exec.correlationId} · ${formatDuration(exec.durationMs)} · ${exec.customer}`, timestamp: formatTimestamp(exec.timestamp), }) } for (const route of rts) { results.push({ id: route.id, category: 'route', title: route.name, badges: [{ label: route.group }], meta: `${route.exchangeCount.toLocaleString()} exchanges · ${route.successRate}% success`, }) } for (const agent of ags) { results.push({ id: agent.id, category: 'agent', title: agent.name, badges: [{ label: agent.status }], meta: `${agent.service} ${agent.version} · ${agent.tps} · ${agent.lastSeen}`, }) } return results } function buildStatusFilters(exs: Exchange[]) { return [ { label: 'All', value: 'all', count: exs.length }, { label: 'OK', value: 'completed', count: exs.filter((e) => e.status === 'completed').length, color: 'success' as const }, { label: 'Warn', value: 'warning', count: exs.filter((e) => e.status === 'warning').length }, { label: 'Error', value: 'failed', count: exs.filter((e) => e.status === 'failed').length, color: 'error' as const }, { label: 'Running', value: 'running', count: exs.filter((e) => e.status === 'running').length, color: 'running' as const }, ] } 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 } = useParams<{ id: string }>() const [activeFilters, setActiveFilters] = useState([]) const [search, setSearch] = useState('') const [selectedId, setSelectedId] = useState() const [panelOpen, setPanelOpen] = useState(false) const [selectedExchange, setSelectedExchange] = useState(null) const [paletteOpen, setPaletteOpen] = useState(false) // 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]) const selectedApp = appId ? SIDEBAR_APPS.find((a) => a.id === appId) : null // Scope all data to the selected app const scopedExchanges = useMemo(() => { if (!appRouteIds) return exchanges return exchanges.filter((e) => appRouteIds.has(e.route)) }, [appRouteIds]) const scopedRoutes = useMemo(() => { if (!appRouteIds) return routes return routes.filter((r) => appRouteIds.has(r.id)) }, [appRouteIds]) const scopedAgents = useMemo(() => { if (!selectedApp) return agents const agentIds = new Set(selectedApp.agents.map((a) => a.id)) return agents.filter((a) => agentIds.has(a.id)) }, [selectedApp]) // Filter exchanges (scoped + user filters) const filteredExchanges = useMemo(() => { let data = scopedExchanges const statusFilter = activeFilters.find((f) => ['completed', 'failed', 'running', 'warning', 'all'].includes(f.value), ) if (statusFilter && statusFilter.value !== 'all') { data = data.filter((e) => e.status === statusFilter.value) } if (search.trim()) { const q = search.toLowerCase() data = data.filter( (e) => e.orderId.toLowerCase().includes(q) || e.route.toLowerCase().includes(q) || e.customer.toLowerCase().includes(q) || e.correlationId.toLowerCase().includes(q) || (e.errorMessage?.toLowerCase().includes(q) ?? false), ) } return data }, [activeFilters, search, scopedExchanges]) const searchData = useMemo( () => buildSearchData(scopedExchanges, scopedRoutes, scopedAgents), [scopedExchanges, scopedRoutes, scopedAgents], ) 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 } // Build detail panel tabs for selected exchange const detailTabs = selectedExchange ? [ { label: 'Overview', value: 'overview', content: (
Order ID {selectedExchange.orderId}
Route {selectedExchange.route}
Status {statusLabel(selectedExchange.status)}
Duration {formatDuration(selectedExchange.durationMs)}
Customer {selectedExchange.customer}
Agent {selectedExchange.agent}
Correlation ID {selectedExchange.correlationId}
Timestamp {selectedExchange.timestamp.toISOString()}
{selectedExchange.errorMessage && (
{selectedExchange.errorClass}
{selectedExchange.errorMessage}
)}
), }, { label: 'Processors', value: 'processors', content: (
), }, { label: 'Exchange', value: 'exchange', content: (
Exchange snapshot not available in mock mode.
), }, { label: 'Error', value: 'error', content: (
{selectedExchange.errorMessage ? ( <>
{selectedExchange.errorClass}
{selectedExchange.errorMessage}
) : (
No error for this exchange.
)}
), }, ] : [] return ( } detail={ selectedExchange ? ( setPanelOpen(false)} title={`${selectedExchange.orderId} — ${selectedExchange.route}`} tabs={detailTabs} /> ) : undefined } > {/* Top bar */} setPaletteOpen(true)} /> {/* Scrollable content */}
{/* Health strip */}
{kpiMetrics.map((kpi, i) => ( ))}
{/* Filter bar */} {/* Exchanges table */}
Recent Exchanges
{filteredExchanges.length.toLocaleString()} of {scopedExchanges.length.toLocaleString()} exchanges
row.errorMessage ? (
{row.errorMessage}
Click to view full stack trace
) : null } />
{/* Command palette */} setPaletteOpen(false)} onSelect={() => setPaletteOpen(false)} data={searchData} onOpen={() => setPaletteOpen(true)} /> {/* Shortcuts bar */}
) }