2026-03-18 10:06:41 +01:00
|
|
|
import { useState, useMemo } from 'react'
|
2026-03-18 19:26:27 +01:00
|
|
|
import { useParams } from 'react-router-dom'
|
2026-03-18 10:06:41 +01:00
|
|
|
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
|
2026-03-18 15:54:27 +01:00
|
|
|
import { exchanges, type Exchange } from '../../mocks/exchanges'
|
2026-03-18 10:06:41 +01:00
|
|
|
import { routes } from '../../mocks/routes'
|
|
|
|
|
import { agents } from '../../mocks/agents'
|
|
|
|
|
import { kpiMetrics } from '../../mocks/metrics'
|
2026-03-18 17:50:41 +01:00
|
|
|
import { SIDEBAR_APPS } from '../../mocks/sidebar'
|
2026-03-18 10:06:41 +01:00
|
|
|
|
|
|
|
|
// ─── 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' })
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 15:54:27 +01:00
|
|
|
function statusToVariant(status: Exchange['status']): 'success' | 'error' | 'running' | 'warning' {
|
2026-03-18 10:06:41 +01:00
|
|
|
switch (status) {
|
|
|
|
|
case 'completed': return 'success'
|
|
|
|
|
case 'failed': return 'error'
|
|
|
|
|
case 'running': return 'running'
|
|
|
|
|
case 'warning': return 'warning'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 15:54:27 +01:00
|
|
|
function statusLabel(status: Exchange['status']): string {
|
2026-03-18 10:06:41 +01:00
|
|
|
switch (status) {
|
|
|
|
|
case 'completed': return 'OK'
|
|
|
|
|
case 'failed': return 'ERR'
|
|
|
|
|
case 'running': return 'RUN'
|
|
|
|
|
case 'warning': return 'WARN'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Table columns ────────────────────────────────────────────────────────────
|
2026-03-18 15:54:27 +01:00
|
|
|
const COLUMNS: Column<Exchange>[] = [
|
2026-03-18 10:06:41 +01:00
|
|
|
{
|
|
|
|
|
key: 'status',
|
|
|
|
|
header: 'Status',
|
|
|
|
|
width: '80px',
|
|
|
|
|
render: (_, row) => (
|
|
|
|
|
<span className={styles.statusCell}>
|
|
|
|
|
<StatusDot variant={statusToVariant(row.status)} />
|
|
|
|
|
<MonoText size="xs">{statusLabel(row.status)}</MonoText>
|
|
|
|
|
</span>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'route',
|
|
|
|
|
header: 'Route',
|
|
|
|
|
sortable: true,
|
|
|
|
|
render: (_, row) => (
|
|
|
|
|
<div>
|
|
|
|
|
<div className={styles.routeName}>{row.route}</div>
|
|
|
|
|
<div className={styles.routeGroup}>{row.routeGroup}</div>
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'orderId',
|
|
|
|
|
header: 'Order ID',
|
|
|
|
|
sortable: true,
|
|
|
|
|
render: (_, row) => (
|
|
|
|
|
<MonoText size="sm">{row.orderId}</MonoText>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'customer',
|
|
|
|
|
header: 'Customer',
|
|
|
|
|
render: (_, row) => (
|
|
|
|
|
<MonoText size="xs" className={styles.customerText}>{row.customer}</MonoText>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'timestamp',
|
|
|
|
|
header: 'Started',
|
|
|
|
|
sortable: true,
|
|
|
|
|
render: (_, row) => (
|
|
|
|
|
<MonoText size="xs">{formatTimestamp(row.timestamp)}</MonoText>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'durationMs',
|
|
|
|
|
header: 'Duration',
|
|
|
|
|
sortable: true,
|
|
|
|
|
render: (_, row) => (
|
|
|
|
|
<MonoText size="sm" className={durationClass(row.durationMs, row.status)}>
|
|
|
|
|
{formatDuration(row.durationMs)}
|
|
|
|
|
</MonoText>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'agent',
|
|
|
|
|
header: 'Agent',
|
|
|
|
|
render: (_, row) => (
|
|
|
|
|
<span className={styles.agentBadge}>
|
|
|
|
|
<span className={styles.agentDot} />
|
|
|
|
|
{row.agent}
|
|
|
|
|
</span>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
2026-03-18 15:54:27 +01:00
|
|
|
function durationClass(ms: number, status: Exchange['status']): string {
|
2026-03-18 10:06:41 +01:00
|
|
|
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 ────────────────────────────────────────
|
2026-03-18 19:26:27 +01:00
|
|
|
function buildSearchData(
|
|
|
|
|
exs: Exchange[],
|
|
|
|
|
rts: typeof routes,
|
|
|
|
|
ags: typeof agents,
|
|
|
|
|
): SearchResult[] {
|
2026-03-18 10:06:41 +01:00
|
|
|
const results: SearchResult[] = []
|
|
|
|
|
|
2026-03-18 19:26:27 +01:00
|
|
|
for (const exec of exs) {
|
2026-03-18 10:06:41 +01:00
|
|
|
results.push({
|
|
|
|
|
id: exec.id,
|
2026-03-18 15:54:27 +01:00
|
|
|
category: 'exchange',
|
2026-03-18 10:06:41 +01:00
|
|
|
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),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 19:26:27 +01:00
|
|
|
for (const route of rts) {
|
2026-03-18 10:06:41 +01:00
|
|
|
results.push({
|
|
|
|
|
id: route.id,
|
|
|
|
|
category: 'route',
|
|
|
|
|
title: route.name,
|
|
|
|
|
badges: [{ label: route.group }],
|
2026-03-18 15:54:27 +01:00
|
|
|
meta: `${route.exchangeCount.toLocaleString()} exchanges · ${route.successRate}% success`,
|
2026-03-18 10:06:41 +01:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 19:26:27 +01:00
|
|
|
for (const agent of ags) {
|
2026-03-18 10:06:41 +01:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 19:26:27 +01:00
|
|
|
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 },
|
|
|
|
|
]
|
|
|
|
|
}
|
2026-03-18 10:06:41 +01:00
|
|
|
|
|
|
|
|
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() {
|
2026-03-18 19:26:27 +01:00
|
|
|
const { id: appId } = useParams<{ id: string }>()
|
2026-03-18 10:06:41 +01:00
|
|
|
const [activeFilters, setActiveFilters] = useState<ActiveFilter[]>([])
|
|
|
|
|
const [search, setSearch] = useState('')
|
|
|
|
|
const [selectedId, setSelectedId] = useState<string | undefined>()
|
|
|
|
|
const [panelOpen, setPanelOpen] = useState(false)
|
2026-03-18 15:54:27 +01:00
|
|
|
const [selectedExchange, setSelectedExchange] = useState<Exchange | null>(null)
|
2026-03-18 10:06:41 +01:00
|
|
|
const [paletteOpen, setPaletteOpen] = useState(false)
|
|
|
|
|
|
2026-03-18 19:26:27 +01:00
|
|
|
// 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)
|
2026-03-18 15:54:27 +01:00
|
|
|
const filteredExchanges = useMemo(() => {
|
2026-03-18 19:26:27 +01:00
|
|
|
let data = scopedExchanges
|
2026-03-18 10:06:41 +01:00
|
|
|
|
|
|
|
|
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
|
2026-03-18 19:26:27 +01:00
|
|
|
}, [activeFilters, search, scopedExchanges])
|
|
|
|
|
|
|
|
|
|
const searchData = useMemo(
|
|
|
|
|
() => buildSearchData(scopedExchanges, scopedRoutes, scopedAgents),
|
|
|
|
|
[scopedExchanges, scopedRoutes, scopedAgents],
|
|
|
|
|
)
|
2026-03-18 10:06:41 +01:00
|
|
|
|
2026-03-18 15:54:27 +01:00
|
|
|
function handleRowClick(row: Exchange) {
|
2026-03-18 10:06:41 +01:00
|
|
|
setSelectedId(row.id)
|
2026-03-18 15:54:27 +01:00
|
|
|
setSelectedExchange(row)
|
2026-03-18 10:06:41 +01:00
|
|
|
setPanelOpen(true)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 15:54:27 +01:00
|
|
|
function handleRowAccent(row: Exchange): 'error' | 'warning' | undefined {
|
2026-03-18 10:06:41 +01:00
|
|
|
if (row.status === 'failed') return 'error'
|
|
|
|
|
if (row.status === 'warning') return 'warning'
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 15:54:27 +01:00
|
|
|
// Build detail panel tabs for selected exchange
|
|
|
|
|
const detailTabs = selectedExchange
|
2026-03-18 10:06:41 +01:00
|
|
|
? [
|
|
|
|
|
{
|
|
|
|
|
label: 'Overview',
|
|
|
|
|
value: 'overview',
|
|
|
|
|
content: (
|
|
|
|
|
<div className={styles.overviewTab}>
|
|
|
|
|
<div className={styles.overviewRow}>
|
|
|
|
|
<span className={styles.overviewLabel}>Order ID</span>
|
2026-03-18 15:54:27 +01:00
|
|
|
<MonoText size="sm">{selectedExchange.orderId}</MonoText>
|
2026-03-18 10:06:41 +01:00
|
|
|
</div>
|
|
|
|
|
<div className={styles.overviewRow}>
|
|
|
|
|
<span className={styles.overviewLabel}>Route</span>
|
2026-03-18 15:54:27 +01:00
|
|
|
<span>{selectedExchange.route}</span>
|
2026-03-18 10:06:41 +01:00
|
|
|
</div>
|
|
|
|
|
<div className={styles.overviewRow}>
|
|
|
|
|
<span className={styles.overviewLabel}>Status</span>
|
|
|
|
|
<span className={styles.statusCell}>
|
2026-03-18 15:54:27 +01:00
|
|
|
<StatusDot variant={statusToVariant(selectedExchange.status)} />
|
|
|
|
|
<span>{statusLabel(selectedExchange.status)}</span>
|
2026-03-18 10:06:41 +01:00
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className={styles.overviewRow}>
|
|
|
|
|
<span className={styles.overviewLabel}>Duration</span>
|
2026-03-18 15:54:27 +01:00
|
|
|
<MonoText size="sm">{formatDuration(selectedExchange.durationMs)}</MonoText>
|
2026-03-18 10:06:41 +01:00
|
|
|
</div>
|
|
|
|
|
<div className={styles.overviewRow}>
|
|
|
|
|
<span className={styles.overviewLabel}>Customer</span>
|
2026-03-18 15:54:27 +01:00
|
|
|
<MonoText size="sm">{selectedExchange.customer}</MonoText>
|
2026-03-18 10:06:41 +01:00
|
|
|
</div>
|
|
|
|
|
<div className={styles.overviewRow}>
|
|
|
|
|
<span className={styles.overviewLabel}>Agent</span>
|
2026-03-18 15:54:27 +01:00
|
|
|
<MonoText size="sm">{selectedExchange.agent}</MonoText>
|
2026-03-18 10:06:41 +01:00
|
|
|
</div>
|
|
|
|
|
<div className={styles.overviewRow}>
|
|
|
|
|
<span className={styles.overviewLabel}>Correlation ID</span>
|
2026-03-18 15:54:27 +01:00
|
|
|
<MonoText size="xs">{selectedExchange.correlationId}</MonoText>
|
2026-03-18 10:06:41 +01:00
|
|
|
</div>
|
|
|
|
|
<div className={styles.overviewRow}>
|
|
|
|
|
<span className={styles.overviewLabel}>Timestamp</span>
|
2026-03-18 15:54:27 +01:00
|
|
|
<MonoText size="xs">{selectedExchange.timestamp.toISOString()}</MonoText>
|
2026-03-18 10:06:41 +01:00
|
|
|
</div>
|
2026-03-18 15:54:27 +01:00
|
|
|
{selectedExchange.errorMessage && (
|
2026-03-18 10:06:41 +01:00
|
|
|
<div className={styles.errorBlock}>
|
2026-03-18 15:54:27 +01:00
|
|
|
<div className={styles.errorClass}>{selectedExchange.errorClass}</div>
|
|
|
|
|
<div className={styles.errorMessage}>{selectedExchange.errorMessage}</div>
|
2026-03-18 10:06:41 +01:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Processors',
|
|
|
|
|
value: 'processors',
|
|
|
|
|
content: (
|
|
|
|
|
<div className={styles.processorsTab}>
|
|
|
|
|
<ProcessorTimeline
|
2026-03-18 15:54:27 +01:00
|
|
|
processors={selectedExchange.processors}
|
|
|
|
|
totalMs={selectedExchange.durationMs}
|
2026-03-18 10:06:41 +01:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Exchange',
|
|
|
|
|
value: 'exchange',
|
|
|
|
|
content: (
|
|
|
|
|
<div className={styles.exchangeTab}>
|
|
|
|
|
<div className={styles.emptyTabMsg}>Exchange snapshot not available in mock mode.</div>
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Error',
|
|
|
|
|
value: 'error',
|
|
|
|
|
content: (
|
|
|
|
|
<div className={styles.errorTab}>
|
2026-03-18 15:54:27 +01:00
|
|
|
{selectedExchange.errorMessage ? (
|
2026-03-18 10:06:41 +01:00
|
|
|
<>
|
2026-03-18 15:54:27 +01:00
|
|
|
<div className={styles.errorClass}>{selectedExchange.errorClass}</div>
|
|
|
|
|
<pre className={styles.errorPre}>{selectedExchange.errorMessage}</pre>
|
2026-03-18 10:06:41 +01:00
|
|
|
</>
|
|
|
|
|
) : (
|
2026-03-18 15:54:27 +01:00
|
|
|
<div className={styles.emptyTabMsg}>No error for this exchange.</div>
|
2026-03-18 10:06:41 +01:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
: []
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<AppShell
|
|
|
|
|
sidebar={
|
2026-03-18 17:50:41 +01:00
|
|
|
<Sidebar apps={SIDEBAR_APPS} />
|
2026-03-18 10:06:41 +01:00
|
|
|
}
|
|
|
|
|
detail={
|
2026-03-18 15:54:27 +01:00
|
|
|
selectedExchange ? (
|
2026-03-18 10:06:41 +01:00
|
|
|
<DetailPanel
|
|
|
|
|
open={panelOpen}
|
|
|
|
|
onClose={() => setPanelOpen(false)}
|
2026-03-18 15:54:27 +01:00
|
|
|
title={`${selectedExchange.orderId} — ${selectedExchange.route}`}
|
2026-03-18 10:06:41 +01:00
|
|
|
tabs={detailTabs}
|
|
|
|
|
/>
|
|
|
|
|
) : undefined
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{/* Top bar */}
|
|
|
|
|
<TopBar
|
2026-03-18 19:26:27 +01:00
|
|
|
breadcrumb={appId
|
|
|
|
|
? [{ label: 'Applications', href: '/apps' }, { label: appId }]
|
|
|
|
|
: [{ label: 'Applications' }]
|
|
|
|
|
}
|
2026-03-18 10:06:41 +01:00
|
|
|
environment="PRODUCTION"
|
|
|
|
|
user={{ name: 'hendrik' }}
|
|
|
|
|
onSearchClick={() => setPaletteOpen(true)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Scrollable content */}
|
|
|
|
|
<div className={styles.content}>
|
|
|
|
|
|
|
|
|
|
{/* Health strip */}
|
|
|
|
|
<div className={styles.healthStrip}>
|
|
|
|
|
{kpiMetrics.map((kpi, i) => (
|
|
|
|
|
<StatCard
|
|
|
|
|
key={i}
|
|
|
|
|
label={kpi.label}
|
|
|
|
|
value={kpi.value}
|
|
|
|
|
detail={kpi.detail}
|
|
|
|
|
trend={kpi.trend}
|
|
|
|
|
trendValue={kpi.trendValue}
|
|
|
|
|
accent={kpi.accent}
|
|
|
|
|
sparkline={kpi.sparkline}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Filter bar */}
|
|
|
|
|
<FilterBar
|
2026-03-18 19:26:27 +01:00
|
|
|
filters={buildStatusFilters(scopedExchanges)}
|
2026-03-18 10:06:41 +01:00
|
|
|
activeFilters={activeFilters}
|
|
|
|
|
onFilterChange={setActiveFilters}
|
|
|
|
|
searchPlaceholder="Search by Order ID, correlation ID, error message..."
|
|
|
|
|
searchValue={search}
|
|
|
|
|
onSearchChange={setSearch}
|
|
|
|
|
className={styles.filterBar}
|
|
|
|
|
/>
|
|
|
|
|
|
2026-03-18 15:54:27 +01:00
|
|
|
{/* Exchanges table */}
|
2026-03-18 10:06:41 +01:00
|
|
|
<div className={styles.tableSection}>
|
|
|
|
|
<div className={styles.tableHeader}>
|
2026-03-18 15:54:27 +01:00
|
|
|
<span className={styles.tableTitle}>Recent Exchanges</span>
|
2026-03-18 10:06:41 +01:00
|
|
|
<div className={styles.tableRight}>
|
|
|
|
|
<span className={styles.tableMeta}>
|
2026-03-18 19:26:27 +01:00
|
|
|
{filteredExchanges.length.toLocaleString()} of {scopedExchanges.length.toLocaleString()} exchanges
|
2026-03-18 10:06:41 +01:00
|
|
|
</span>
|
|
|
|
|
<Badge label="LIVE" color="success" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<DataTable
|
|
|
|
|
columns={COLUMNS}
|
2026-03-18 15:54:27 +01:00
|
|
|
data={filteredExchanges}
|
2026-03-18 10:06:41 +01:00
|
|
|
onRowClick={handleRowClick}
|
|
|
|
|
selectedId={selectedId}
|
|
|
|
|
sortable
|
|
|
|
|
rowAccent={handleRowAccent}
|
|
|
|
|
expandedContent={(row) =>
|
|
|
|
|
row.errorMessage ? (
|
|
|
|
|
<div className={styles.inlineError}>
|
|
|
|
|
<span className={styles.inlineErrorIcon}>⚠</span>
|
|
|
|
|
<div>
|
|
|
|
|
<div className={styles.inlineErrorText}>{row.errorMessage}</div>
|
|
|
|
|
<div className={styles.inlineErrorHint}>Click to view full stack trace</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : null
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Command palette */}
|
|
|
|
|
<CommandPalette
|
|
|
|
|
open={paletteOpen}
|
|
|
|
|
onClose={() => setPaletteOpen(false)}
|
|
|
|
|
onSelect={() => setPaletteOpen(false)}
|
2026-03-18 19:26:27 +01:00
|
|
|
data={searchData}
|
2026-03-18 10:06:41 +01:00
|
|
|
onOpen={() => setPaletteOpen(true)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Shortcuts bar */}
|
|
|
|
|
<ShortcutsBar shortcuts={SHORTCUTS} />
|
|
|
|
|
</AppShell>
|
|
|
|
|
)
|
|
|
|
|
}
|