diff --git a/CLAUDE.md b/CLAUDE.md index 7384bd7..06a1394 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,8 +37,8 @@ Always read `COMPONENT_GUIDE.md` before building any UI feature. It contains dec ### Import Paths ```tsx import { Button, Input } from '../design-system/primitives' -import { Modal, DataTable } from '../design-system/composites' -import type { Column } from '../design-system/composites' +import { Modal, DataTable, KpiStrip, SplitPane, EntityList, LogViewer } from '../design-system/composites' +import type { Column, KpiItem, LogEntry } from '../design-system/composites' import { AppShell } from '../design-system/layout/AppShell' import { ThemeProvider } from '../design-system/providers/ThemeProvider' ``` @@ -91,10 +91,10 @@ import { Button, AppShell, ThemeProvider } from '@cameleer/design-system' ```tsx // All components from single entry -import { Button, Input, Modal, DataTable, AppShell } from '@cameleer/design-system' +import { Button, Input, Modal, DataTable, KpiStrip, SplitPane, EntityList, LogViewer, StatusText, AppShell } from '@cameleer/design-system' // Types -import type { Column, DataTableProps, SearchResult } from '@cameleer/design-system' +import type { Column, DataTableProps, SearchResult, KpiItem, LogEntry } from '@cameleer/design-system' // Providers import { ThemeProvider, useTheme } from '@cameleer/design-system' diff --git a/src/pages/Admin/UserManagement/GroupsTab.tsx b/src/pages/Admin/UserManagement/GroupsTab.tsx index 4714ea9..480375c 100644 --- a/src/pages/Admin/UserManagement/GroupsTab.tsx +++ b/src/pages/Admin/UserManagement/GroupsTab.tsx @@ -11,8 +11,10 @@ import { InlineEdit } from '../../../design-system/primitives/InlineEdit/InlineE import { MultiSelect } from '../../../design-system/composites/MultiSelect/MultiSelect' import { ConfirmDialog } from '../../../design-system/composites/ConfirmDialog/ConfirmDialog' import { AlertDialog } from '../../../design-system/composites/AlertDialog/AlertDialog' +import { SplitPane } from '../../../design-system/composites/SplitPane/SplitPane' +import { EntityList } from '../../../design-system/composites/EntityList/EntityList' import { useToast } from '../../../design-system/composites/Toast/Toast' -import { MOCK_GROUPS, MOCK_USERS, MOCK_ROLES, getChildGroups, type MockGroup } from './rbacMocks' +import { MOCK_GROUPS, MOCK_USERS, MOCK_ROLES, type MockGroup } from './rbacMocks' import styles from './UserManagement.module.css' export function GroupsTab() { @@ -83,207 +85,190 @@ export function GroupsTab() { return ( <> -
-
-
- setSearch(e.target.value)} - onClear={() => setSearch('')} - className={styles.listHeaderSearch} - /> - -
- - {creating && ( -
- setNewName(e.target.value)} /> - {duplicateGroupName && Group name already exists} - setNewName(e.target.value)} /> + {duplicateGroupName && Group name already exists} + setSearch(e.target.value)} - onClear={() => setSearch('')} - className={styles.listHeaderSearch} - /> - -
- - {creating && ( -
- setNewName(e.target.value)} /> - {duplicateRoleName && Role name already exists} - setNewDesc(e.target.value)} /> -
- - -
-
- )} - -
- {filtered.map((role) => ( -
setSelectedId(role.id)} - role="option" - tabIndex={0} - aria-selected={selectedId === role.id} - onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setSelectedId(role.id) } }} - > - -
-
- {role.name} - {role.system && } -
-
- {role.description} · {getAssignmentCount(role)} assignments -
-
- {MOCK_GROUPS.filter((g) => g.directRoles.includes(role.name)) - .map((g) => )} - {MOCK_USERS.filter((u) => u.directRoles.includes(role.name)) - .map((u) => )} -
+ + {creating && ( +
+ setNewName(e.target.value)} /> + {duplicateRoleName && Role name already exists} + setNewDesc(e.target.value)} /> +
+ +
- ))} - {filtered.length === 0 && ( -
No roles match your search
)} -
-
-
- {selected ? ( - <> -
- -
-
{selected.name}
- {selected.description && ( -
{selected.description}
- )} -
- {!selected.system && ( - - )} -
- -
- ID - {selected.id} - Scope - {selected.scope} - {selected.system && ( - <> - Type - System role (read-only) - - )} -
- - Assigned to groups -
- {assignedGroups.map((g) => )} - {assignedGroups.length === 0 && (none)} -
- - Assigned to users (direct) -
- {directUsers.map((u) => )} - {directUsers.length === 0 && (none)} -
- - Effective principals -
- {effectivePrincipals.map((u) => { - const isDirect = u.directRoles.includes(selected.name) - return ( - - ) - })} - {effectivePrincipals.length === 0 && (none)} -
- {effectivePrincipals.some((u) => !u.directRoles.includes(selected.name)) && ( - - Dashed entries inherit this role through group membership - + ( + <> + +
+
+ {role.name} + {role.system && } +
+
+ {role.description} · {getAssignmentCount(role)} assignments +
+
+ {MOCK_GROUPS.filter((g) => g.directRoles.includes(role.name)) + .map((g) => )} + {MOCK_USERS.filter((u) => u.directRoles.includes(role.name)) + .map((u) => )} +
+
+ )} - - ) : ( -
Select a role to view details
- )} -
-
+ getItemId={(role) => role.id} + selectedId={selectedId ?? undefined} + onSelect={setSelectedId} + searchPlaceholder="Search roles..." + onSearch={setSearch} + addLabel="+ Add role" + onAdd={() => setCreating(true)} + emptyMessage="No roles match your search" + /> + + } + detail={selected ? ( + <> +
+ +
+
{selected.name}
+ {selected.description && ( +
{selected.description}
+ )} +
+ {!selected.system && ( + + )} +
+ +
+ ID + {selected.id} + Scope + {selected.scope} + {selected.system && ( + <> + Type + System role (read-only) + + )} +
+ + Assigned to groups +
+ {assignedGroups.map((g) => )} + {assignedGroups.length === 0 && (none)} +
+ + Assigned to users (direct) +
+ {directUsers.map((u) => )} + {directUsers.length === 0 && (none)} +
+ + Effective principals +
+ {effectivePrincipals.map((u) => { + const isDirect = u.directRoles.includes(selected.name) + return ( + + ) + })} + {effectivePrincipals.length === 0 && (none)} +
+ {effectivePrincipals.some((u) => !u.directRoles.includes(selected.name)) && ( + + Dashed entries inherit this role through group membership + + )} + + ) : null} + emptyMessage="Select a role to view details" + /> -
-
-
- setSearch(e.target.value)} - onClear={() => setSearch('')} - className={styles.listHeaderSearch} - /> - -
- - {creating && ( -
- setNewProvider(v as 'local' | 'oidc')} orientation="horizontal"> - - - -
- setNewUsername(e.target.value)} /> - setNewDisplay(e.target.value)} /> -
- {duplicateUsername && Username already exists} - setNewEmail(e.target.value)} /> - {newProvider === 'local' && ( - setNewPassword(e.target.value)} /> - )} - {newProvider === 'oidc' && ( - - OIDC users authenticate via the configured identity provider. Pre-register to assign roles/groups before their first login. - - )} -
- - -
-
- )} - -
- {filtered.map((user) => ( -
{ setSelectedId(user.id); setResettingPassword(false) }} - role="option" - tabIndex={0} - aria-selected={selectedId === user.id} - onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setSelectedId(user.id); setResettingPassword(false) } }} - > - -
-
- {user.displayName} - {user.provider !== 'local' && ( - - )} -
-
- {user.email} · {getUserGroupPath(user)} -
-
- {user.directRoles.map((r) => )} - {user.directGroups.map((gId) => { - const g = MOCK_GROUPS.find((gr) => gr.id === gId) - return g ? : null - })} -
+ + {creating && ( +
+ setNewProvider(v as 'local' | 'oidc')} orientation="horizontal"> + + + +
+ setNewUsername(e.target.value)} /> + setNewDisplay(e.target.value)} /> +
+ {duplicateUsername && Username already exists} + setNewEmail(e.target.value)} /> + {newProvider === 'local' && ( + setNewPassword(e.target.value)} /> + )} + {newProvider === 'oidc' && ( + + OIDC users authenticate via the configured identity provider. Pre-register to assign roles/groups before their first login. + + )} +
+ +
- ))} - {filtered.length === 0 && ( -
No users match your search
)} -
-
-
- {selected ? ( - <> -
- -
-
- updateUser(selected.id, { displayName: v })} - /> -
-
{selected.email}
-
- -
- - Status -
- -
- -
- ID - {selected.id} - Created - {new Date(selected.createdAt).toLocaleDateString()} - Provider - {selected.provider} -
- - Security -
- {selected.provider === 'local' ? ( - <> -
- Password - •••••••• - {!resettingPassword && ( - + ( + <> + +
+
+ {user.displayName} + {user.provider !== 'local' && ( + )}
- {resettingPassword && ( -
- setNewPw(e.target.value)} - className={styles.resetInput} - /> - - -
- )} - - ) : ( - <> -
- Authentication - OIDC ({selected.provider}) +
+ {user.email} · {getUserGroupPath(user)}
- - Password managed by the identity provider. - - - )} -
- - Group membership (direct only) -
- {selected.directGroups.map((gId) => { - const g = MOCK_GROUPS.find((gr) => gr.id === gId) - return g ? ( - { - const group = MOCK_GROUPS.find((gr) => gr.id === gId) - if (group && group.directRoles.length > 0) { - setRemoveGroupTarget(gId) - } else { - updateUser(selected.id, { directGroups: selected.directGroups.filter((id) => id !== gId) }) - toast({ title: 'Group removed', variant: 'success' }) - } - }} - /> - ) : null - })} - {selected.directGroups.length === 0 && ( - (no groups) - )} - { - updateUser(selected.id, { directGroups: [...selected.directGroups, ...ids] }) - toast({ title: `${ids.length} group(s) added`, variant: 'success' }) - }} - placeholder="+ Add" - /> -
- - Effective roles (direct + inherited) -
- {effectiveRoles.map(({ role, source }) => - source === 'direct' ? ( - { - updateUser(selected.id, { directRoles: selected.directRoles.filter((r) => r !== role) }) - toast({ title: 'Role removed', description: role, variant: 'success' }) - }} - /> - ) : ( - - ) - )} - {effectiveRoles.length === 0 && ( - (no roles) - )} - { - updateUser(selected.id, { directRoles: [...selected.directRoles, ...roles] }) - toast({ title: `${roles.length} role(s) added`, variant: 'success' }) - }} - placeholder="+ Add" - /> -
- {effectiveRoles.some((r) => r.source !== 'direct') && ( - - Roles with ↑ are inherited through group membership - +
+ {user.directRoles.map((r) => )} + {user.directGroups.map((gId) => { + const g = MOCK_GROUPS.find((gr) => gr.id === gId) + return g ? : null + })} +
+
+ )} - - ) : ( -
Select a user to view details
- )} -
-
+ getItemId={(user) => user.id} + selectedId={selectedId ?? undefined} + onSelect={(id) => { setSelectedId(id); setResettingPassword(false) }} + searchPlaceholder="Search users..." + onSearch={setSearch} + addLabel="+ Add user" + onAdd={() => setCreating(true)} + emptyMessage="No users match your search" + /> + + } + detail={selected ? ( + <> +
+ +
+
+ updateUser(selected.id, { displayName: v })} + /> +
+
{selected.email}
+
+ +
+ + Status +
+ +
+ +
+ ID + {selected.id} + Created + {new Date(selected.createdAt).toLocaleDateString()} + Provider + {selected.provider} +
+ + Security +
+ {selected.provider === 'local' ? ( + <> +
+ Password + •••••••• + {!resettingPassword && ( + + )} +
+ {resettingPassword && ( +
+ setNewPw(e.target.value)} + className={styles.resetInput} + /> + + +
+ )} + + ) : ( + <> +
+ Authentication + OIDC ({selected.provider}) +
+ + Password managed by the identity provider. + + + )} +
+ + Group membership (direct only) +
+ {selected.directGroups.map((gId) => { + const g = MOCK_GROUPS.find((gr) => gr.id === gId) + return g ? ( + { + const group = MOCK_GROUPS.find((gr) => gr.id === gId) + if (group && group.directRoles.length > 0) { + setRemoveGroupTarget(gId) + } else { + updateUser(selected.id, { directGroups: selected.directGroups.filter((id) => id !== gId) }) + toast({ title: 'Group removed', variant: 'success' }) + } + }} + /> + ) : null + })} + {selected.directGroups.length === 0 && ( + (no groups) + )} + { + updateUser(selected.id, { directGroups: [...selected.directGroups, ...ids] }) + toast({ title: `${ids.length} group(s) added`, variant: 'success' }) + }} + placeholder="+ Add" + /> +
+ + Effective roles (direct + inherited) +
+ {effectiveRoles.map(({ role, source }) => + source === 'direct' ? ( + { + updateUser(selected.id, { directRoles: selected.directRoles.filter((r) => r !== role) }) + toast({ title: 'Role removed', description: role, variant: 'success' }) + }} + /> + ) : ( + + ) + )} + {effectiveRoles.length === 0 && ( + (no roles) + )} + { + updateUser(selected.id, { directRoles: [...selected.directRoles, ...roles] }) + toast({ title: `${roles.length} role(s) added`, variant: 'success' }) + }} + placeholder="+ Add" + /> +
+ {effectiveRoles.some((r) => r.source !== 'direct') && ( + + Roles with ↑ are inherited through group membership + + )} + + ) : null} + emptyMessage="Select a user to view details" + /> l.level === logFilter.toUpperCase()) + : logEntries.filter((l) => l.level === logFilter) const cpuData = buildTimeSeries(agent.cpuUsagePct, 15) const memSeries = buildMemoryHistory(agent.memoryUsagePct) @@ -289,22 +284,7 @@ export function AgentInstance() { Application Log
-
- {filteredLogs.map((entry, i) => ( -
- {formatLogTime(entry.ts)} - - {entry.logger} - {entry.msg} -
- ))} - {filteredLogs.length === 0 && ( -
No log entries match the selected filter.
- )} -
+
{/* Timeline */} diff --git a/src/pages/Dashboard/Dashboard.module.css b/src/pages/Dashboard/Dashboard.module.css index 29e7112..72fb682 100644 --- a/src/pages/Dashboard/Dashboard.module.css +++ b/src/pages/Dashboard/Dashboard.module.css @@ -7,14 +7,6 @@ background: var(--bg-body); } -/* Health strip */ -.healthStrip { - display: grid; - grid-template-columns: repeat(5, 1fr); - gap: 10px; - margin-bottom: 16px; -} - /* Filter bar spacing */ .filterBar { margin-bottom: 16px; diff --git a/src/pages/Dashboard/Dashboard.tsx b/src/pages/Dashboard/Dashboard.tsx index 59477c7..97ea2e4 100644 --- a/src/pages/Dashboard/Dashboard.tsx +++ b/src/pages/Dashboard/Dashboard.tsx @@ -15,9 +15,10 @@ import { ShortcutsBar } from '../../design-system/composites/ShortcutsBar/Shortc 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 { 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' @@ -27,12 +28,44 @@ import { useGlobalFilters } from '../../design-system/providers/GlobalFilterProv // Mock data import { exchanges, type Exchange } from '../../mocks/exchanges' -import { kpiMetrics } from '../../mocks/metrics' +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: '\u2191', + down: '\u2193', + neutral: '\u2192', +} + +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` @@ -370,20 +403,7 @@ export function Dashboard() {
{/* Health strip */} -
- {kpiMetrics.map((kpi, i) => ( - - ))} -
+ {/* Exchanges table */}
diff --git a/src/pages/Routes/Routes.module.css b/src/pages/Routes/Routes.module.css index db6ca45..ff20a4d 100644 --- a/src/pages/Routes/Routes.module.css +++ b/src/pages/Routes/Routes.module.css @@ -35,176 +35,6 @@ font-family: var(--font-mono); } -/* KPI strip */ -.kpiStrip { - display: grid; - grid-template-columns: repeat(5, 1fr); - gap: 12px; - margin-bottom: 20px; -} - -/* KPI card */ -.kpiCard { - background: var(--bg-surface); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-lg); - padding: 16px 18px 12px; - box-shadow: var(--shadow-card); - position: relative; - overflow: hidden; - transition: box-shadow 0.15s; -} - -.kpiCard:hover { - box-shadow: var(--shadow-md); -} - -.kpiCard::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 3px; -} - -.kpiCardAmber::before { background: linear-gradient(90deg, var(--amber), transparent); } -.kpiCardGreen::before { background: linear-gradient(90deg, var(--success), transparent); } -.kpiCardError::before { background: linear-gradient(90deg, var(--error), transparent); } -.kpiCardTeal::before { background: linear-gradient(90deg, var(--running), transparent); } -.kpiCardWarn::before { background: linear-gradient(90deg, var(--warning), transparent); } - -.kpiLabel { - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.6px; - color: var(--text-muted); - margin-bottom: 6px; -} - -.kpiValueRow { - display: flex; - align-items: baseline; - gap: 6px; - margin-bottom: 4px; -} - -.kpiValue { - font-family: var(--font-mono); - font-size: 26px; - font-weight: 600; - line-height: 1.2; -} - -.kpiValueAmber { color: var(--amber); } -.kpiValueGreen { color: var(--success); } -.kpiValueError { color: var(--error); } -.kpiValueTeal { color: var(--running); } -.kpiValueWarn { color: var(--warning); } - -.kpiUnit { - font-size: 12px; - color: var(--text-muted); -} - -.kpiTrend { - font-family: var(--font-mono); - font-size: 11px; - display: inline-flex; - align-items: center; - gap: 2px; - margin-left: auto; -} - -.trendUpGood { color: var(--success); } -.trendUpBad { color: var(--error); } -.trendDownGood { color: var(--success); } -.trendDownBad { color: var(--error); } -.trendFlat { color: var(--text-muted); } - -.kpiDetail { - font-size: 11px; - color: var(--text-muted); - margin-top: 2px; -} - -.kpiDetailStrong { - color: var(--text-secondary); - font-weight: 600; -} - -.kpiSparkline { - margin-top: 8px; - height: 32px; -} - -/* Latency percentiles card */ -.latencyValues { - display: flex; - gap: 12px; - margin-bottom: 4px; -} - -.latencyItem { - flex: 1; - display: flex; - flex-direction: column; - gap: 2px; -} - -.latencyLabel { - font-size: 9px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - color: var(--text-muted); -} - -.latencyVal { - font-family: var(--font-mono); - font-size: 18px; - font-weight: 600; - line-height: 1.2; -} - -.latValGreen { color: var(--success); } -.latValAmber { color: var(--amber); } -.latValRed { color: var(--error); } - -.latencyTrend { - font-family: var(--font-mono); - font-size: 9px; -} - -/* Active routes donut */ -.donutWrap { - display: flex; - align-items: center; - gap: 10px; - margin-top: 4px; -} - -.donutLabel { - font-family: var(--font-mono); - font-size: 10px; - font-weight: 600; - color: var(--text-secondary); -} - -.donutLegend { - display: flex; - flex-direction: column; - gap: 2px; - font-size: 10px; - color: var(--text-muted); -} - -.donutLegendActive { - color: var(--running); - font-weight: 600; -} - /* Route performance table */ .tableSection { background: var(--bg-surface); @@ -273,24 +103,6 @@ gap: 16px; } -.chartCard { - background: var(--bg-surface); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-card); - padding: 16px; - overflow: hidden; -} - -.chartTitle { - font-size: 12px; - font-weight: 600; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: 12px; -} - .chart { width: 100%; } diff --git a/src/pages/Routes/Routes.tsx b/src/pages/Routes/Routes.tsx index aa08d27..b49165b 100644 --- a/src/pages/Routes/Routes.tsx +++ b/src/pages/Routes/Routes.tsx @@ -15,11 +15,14 @@ import { DataTable } from '../../design-system/composites/DataTable/DataTable' import type { Column } from '../../design-system/composites/DataTable/types' import { RouteFlow } from '../../design-system/composites/RouteFlow/RouteFlow' import type { RouteNode } from '../../design-system/composites/RouteFlow/RouteFlow' +import { KpiStrip } from '../../design-system/composites' +import type { KpiItem } from '../../design-system/composites' // Primitives import { Sparkline } from '../../design-system/primitives/Sparkline/Sparkline' import { MonoText } from '../../design-system/primitives/MonoText/MonoText' import { Badge } from '../../design-system/primitives/Badge/Badge' +import { Card } from '../../design-system/primitives' // Mock data import { @@ -34,8 +37,8 @@ import { SIDEBAR_APPS, buildRouteToAppMap } from '../../mocks/sidebar' const ROUTE_TO_APP = buildRouteToAppMap() -// ─── KPI Header Strip (matches mock-v3-metrics-dashboard) ──────────────────── -function KpiHeader({ scopedMetrics }: { scopedMetrics: RouteMetricRow[] }) { +// ─── Build KPI items from scoped route metrics ────────────────────────────── +function buildKpiItems(scopedMetrics: RouteMetricRow[]): KpiItem[] { const totalExchanges = scopedMetrics.reduce((sum, r) => sum + r.exchangeCount, 0) const totalErrors = scopedMetrics.reduce((sum, r) => sum + r.errorCount, 0) const errorRate = totalExchanges > 0 ? ((totalErrors / totalExchanges) * 100) : 0 @@ -45,113 +48,57 @@ function KpiHeader({ scopedMetrics }: { scopedMetrics: RouteMetricRow[] }) { const p99Latency = scopedMetrics.length > 0 ? Math.max(...scopedMetrics.map((r) => r.p99DurationMs)) : 0 - const avgSuccessRate = scopedMetrics.length > 0 - ? Number((scopedMetrics.reduce((sum, r) => sum + r.successRate, 0) / scopedMetrics.length).toFixed(1)) - : 0 const throughputPerSec = totalExchanges > 0 ? (totalExchanges / 360).toFixed(1) : '0' const activeRoutes = scopedMetrics.length const totalRoutes = routeMetrics.length - return ( -
- {/* Card 1: Total Throughput */} -
-
Total Throughput
-
- {totalExchanges.toLocaleString()} - exchanges - ▲ +8% -
-
- {throughputPerSec} msg/s · Capacity 39% -
-
- -
-
+ const p50 = Math.round(avgLatency * 0.5) + const p95 = Math.round(avgLatency * 1.4) + const slaStatus = p99Latency > 300 ? 'BREACH' : 'OK' - {/* Card 2: System Error Rate */} -
-
System Error Rate
-
- {errorRate.toFixed(2)}% - - {errorRate < 1 ? '\u25BC -0.1%' : '\u25B2 +0.4%'} - -
-
- {totalErrors} errors / {totalExchanges.toLocaleString()} total (6h) -
-
- -
-
- - {/* Card 3: Latency Percentiles */} -
300 ? styles.kpiCardWarn : styles.kpiCardGreen}`}> -
Latency Percentiles
-
-
- P50 - {Math.round(avgLatency * 0.5)}ms - ▼3 -
-
- P95 - 150 ? styles.latValAmber : styles.latValGreen}`}>{Math.round(avgLatency * 1.4)}ms - ▲12 -
-
- P99 - 300 ? styles.latValRed : styles.latValAmber}`}>{p99Latency}ms - ▲28 -
-
-
- SLA: <300ms P99 · {p99Latency > 300 - ? BREACH - : OK} -
-
- - {/* Card 4: Active Routes */} -
-
Active Routes
-
- {activeRoutes} - of {totalRoutes} - ↔ stable -
-
- - - - -
- {activeRoutes} active - {totalRoutes - activeRoutes} stopped -
-
-
- - {/* Card 5: In-Flight Exchanges */} -
-
In-Flight Exchanges
-
- 23 - -
-
- High-water: 67 (2h ago) -
-
- -
-
-
- ) + return [ + { + label: 'Total Throughput', + value: totalExchanges.toLocaleString(), + trend: { label: '\u25B2 +8%', variant: 'success' as const }, + subtitle: `${throughputPerSec} msg/s \u00B7 Capacity 39%`, + sparkline: [44, 46, 45, 47, 48, 46, 47, 48, 46, 47, 48, 47, 46, 47], + borderColor: 'var(--amber)', + }, + { + label: 'System Error Rate', + value: `${errorRate.toFixed(2)}%`, + trend: { + label: errorRate < 1 ? '\u25BC -0.1%' : '\u25B2 +0.4%', + variant: errorRate < 1 ? 'success' as const : 'error' as const, + }, + subtitle: `${totalErrors} errors / ${totalExchanges.toLocaleString()} total (6h)`, + sparkline: [1.2, 1.8, 1.5, 2.1, 2.4, 2.2, 2.5, 2.6, 2.7, 2.8, 2.7, 2.9, 2.8, errorRate], + borderColor: errorRate < 1 ? 'var(--success)' : 'var(--error)', + }, + { + label: 'Latency Percentiles', + value: `${p99Latency}ms`, + trend: { label: '\u25B2 +28', variant: p99Latency > 300 ? 'error' as const : 'warning' as const }, + subtitle: `P50 ${p50}ms \u00B7 P95 ${p95}ms \u00B7 SLA <300ms P99: ${slaStatus}`, + borderColor: p99Latency > 300 ? 'var(--warning)' : 'var(--success)', + }, + { + label: 'Active Routes', + value: `${activeRoutes} / ${totalRoutes}`, + trend: { label: '\u2194 stable', variant: 'muted' as const }, + subtitle: `${activeRoutes} active \u00B7 ${totalRoutes - activeRoutes} stopped`, + borderColor: 'var(--running)', + }, + { + label: 'In-Flight Exchanges', + value: '23', + trend: { label: '\u2194', variant: 'muted' as const }, + subtitle: 'High-water: 67 (2h ago)', + sparkline: [16, 14, 18, 12, 10, 15, 8, 6, 4, 3, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 18, 16, 18, 20, 18, 23], + borderColor: 'var(--amber)', + }, + ] } // ─── Route metric row with id field (required by DataTable) ────────────────── @@ -475,7 +422,7 @@ export function Routes() { Auto-refresh: 30s
- + {/* Processor Performance table */}
@@ -520,7 +467,7 @@ export function Routes() {
{/* KPI header cards */} - + {/* Per-route performance table */}
@@ -544,8 +491,7 @@ export function Routes() { {/* 2x2 chart grid */}
-
-
Throughput (msg/s)
+ -
+ -
-
Latency (ms)
+ -
+ -
-
Errors by Route
+ -
+ -
-
Message Volume (msg/min)
+ -
+