From 2a78f1535e6771f8e7e88d8aa606ee8f1655d2b3 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:20:35 +0100 Subject: [PATCH 1/5] refactor: use KpiStrip and StatusText in Dashboard page Replace StatCard loop with KpiStrip composite, map KpiMetric mock data to KpiItem interface. Remove unused .healthStrip CSS class. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/pages/Dashboard/Dashboard.module.css | 8 ---- src/pages/Dashboard/Dashboard.tsx | 52 ++++++++++++++++-------- 2 files changed, 36 insertions(+), 24 deletions(-) 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 */}
From 043f631eac921209d2c3265874d2acada699650a Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:21:51 +0100 Subject: [PATCH 2/5] refactor: use KpiStrip, StatusText, and Card title in Routes page Replace the custom KpiHeader function with KpiStrip composite, swap chart wrapper divs with Card title prop, and remove ~190 lines of now-redundant CSS. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/pages/Routes/Routes.module.css | 188 ----------------------------- src/pages/Routes/Routes.tsx | 179 ++++++++++----------------- 2 files changed, 61 insertions(+), 306 deletions(-) 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)
+ -
+
From eb62c80daf904c81c5337bc06f19a1a05419dc22 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:22:41 +0100 Subject: [PATCH 3/5] refactor: use LogViewer in AgentInstance page Replace custom log entry rendering (MonoText, Badge, per-entry HTML) with the LogViewer composite component. Map mock data to LogEntry interface, remove formatLogTime helper, and clean up unused CSS classes and imports (Card, CodeBlock, ProgressBar, sectionHeaderRow, sectionTitle, fdRow). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../AgentInstance/AgentInstance.module.css | 67 +------------------ src/pages/AgentInstance/AgentInstance.tsx | 50 +++++--------- 2 files changed, 16 insertions(+), 101 deletions(-) diff --git a/src/pages/AgentInstance/AgentInstance.module.css b/src/pages/AgentInstance/AgentInstance.module.css index 2028ac8..1048005 100644 --- a/src/pages/AgentInstance/AgentInstance.module.css +++ b/src/pages/AgentInstance/AgentInstance.module.css @@ -51,20 +51,6 @@ font-family: var(--font-mono); } -/* Section header — matches /agents */ -.sectionHeaderRow { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 12px; -} - -.sectionTitle { - font-size: 13px; - font-weight: 600; - color: var(--text-primary); -} - /* Charts 3x2 grid */ .chartsGrid { display: grid; @@ -125,12 +111,6 @@ font-weight: 500; } -.fdRow { - display: flex; - align-items: center; - gap: 8px; -} - /* Log + Timeline side by side */ .bottomRow { display: grid; @@ -155,52 +135,7 @@ border-bottom: 1px solid var(--border-subtle); } -.logEntries { - max-height: 360px; - overflow-y: auto; - font-size: 11px; -} - -.logEntry { - display: flex; - align-items: flex-start; - gap: 8px; - padding: 5px 16px; - border-bottom: 1px solid var(--border-subtle); - font-family: var(--font-mono); - transition: background 0.1s; -} - -.logEntry:hover { - background: var(--bg-hover); -} - -.logEntry:last-child { - border-bottom: none; -} - -.logTime { - flex-shrink: 0; - color: var(--text-muted); - min-width: 60px; -} - -.logLogger { - flex-shrink: 0; - color: var(--text-faint); - max-width: 220px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.logMsg { - color: var(--text-primary); - font-family: var(--font-mono); - font-size: 11px; - word-break: break-word; -} - +/* Empty state (shared) */ .logEmpty { padding: 24px; text-align: center; diff --git a/src/pages/AgentInstance/AgentInstance.tsx b/src/pages/AgentInstance/AgentInstance.tsx index 2c4f19b..bdcd538 100644 --- a/src/pages/AgentInstance/AgentInstance.tsx +++ b/src/pages/AgentInstance/AgentInstance.tsx @@ -12,16 +12,15 @@ import { LineChart } from '../../design-system/composites/LineChart/LineChart' import { AreaChart } from '../../design-system/composites/AreaChart/AreaChart' import { EventFeed } from '../../design-system/composites/EventFeed/EventFeed' import { Tabs } from '../../design-system/composites/Tabs/Tabs' +import { LogViewer } from '../../design-system/composites/LogViewer/LogViewer' +import type { LogEntry } from '../../design-system/composites/LogViewer/LogViewer' // 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' import { StatCard } from '../../design-system/primitives/StatCard/StatCard' -import { ProgressBar } from '../../design-system/primitives/ProgressBar/ProgressBar' import { SectionHeader } from '../../design-system/primitives/SectionHeader/SectionHeader' -import { Card } from '../../design-system/primitives/Card/Card' -import { CodeBlock } from '../../design-system/primitives/CodeBlock/CodeBlock' // Global filters import { useGlobalFilters } from '../../design-system/providers/GlobalFilterProvider' @@ -53,27 +52,23 @@ function buildMemoryHistory(currentPct: number) { // ── Mock log entries ───────────────────────────────────────────────────────── -function buildLogEntries(agentName: string) { +function buildLogEntries(agentName: string): LogEntry[] { const now = Date.now() const MIN = 60_000 return [ - { ts: new Date(now - 1 * MIN).toISOString(), level: 'INFO', logger: 'o.a.c.impl.DefaultCamelContext', msg: `Route order-validation started and consuming from: direct:validate` }, - { ts: new Date(now - 2 * MIN).toISOString(), level: 'INFO', logger: 'o.a.c.impl.DefaultCamelContext', msg: `Total 3 routes, of which 3 are started` }, - { ts: new Date(now - 5 * MIN).toISOString(), level: 'WARN', logger: 'o.a.c.processor.errorhandler', msg: `Failed delivery for exchangeId: ID-${agentName}-1710847200000-0-1. Exhausted after 3 attempts.` }, - { ts: new Date(now - 8 * MIN).toISOString(), level: 'INFO', logger: 'o.a.c.health.HealthCheckHelper', msg: `Health check [routes] is UP` }, - { ts: new Date(now - 12 * MIN).toISOString(), level: 'INFO', logger: 'o.a.c.health.HealthCheckHelper', msg: `Health check [consumers] is UP` }, - { ts: new Date(now - 15 * MIN).toISOString(), level: 'DEBUG', logger: 'o.a.c.component.kafka', msg: `KafkaConsumer[order-events] poll returned 42 records in 18ms` }, - { ts: new Date(now - 18 * MIN).toISOString(), level: 'INFO', logger: 'o.a.c.impl.engine.InternalRouteStartup', msg: `Route order-enrichment started and consuming from: kafka:order-events` }, - { ts: new Date(now - 25 * MIN).toISOString(), level: 'WARN', logger: 'o.a.c.component.http', msg: `HTTP endpoint https://payment-api.internal/verify returned 503 — will retry` }, - { ts: new Date(now - 30 * MIN).toISOString(), level: 'INFO', logger: 'o.a.c.impl.DefaultCamelContext', msg: `Apache Camel ${agentName} (CamelContext) is starting` }, - { ts: new Date(now - 32 * MIN).toISOString(), level: 'INFO', logger: 'org.springframework.boot', msg: `Started ${agentName} in 4.231 seconds (process running for 4.892)` }, + { timestamp: new Date(now - 1 * MIN).toISOString(), level: 'info', message: `[o.a.c.impl.DefaultCamelContext] Route order-validation started and consuming from: direct:validate` }, + { timestamp: new Date(now - 2 * MIN).toISOString(), level: 'info', message: `[o.a.c.impl.DefaultCamelContext] Total 3 routes, of which 3 are started` }, + { timestamp: new Date(now - 5 * MIN).toISOString(), level: 'warn', message: `[o.a.c.processor.errorhandler] Failed delivery for exchangeId: ID-${agentName}-1710847200000-0-1. Exhausted after 3 attempts.` }, + { timestamp: new Date(now - 8 * MIN).toISOString(), level: 'info', message: `[o.a.c.health.HealthCheckHelper] Health check [routes] is UP` }, + { timestamp: new Date(now - 12 * MIN).toISOString(), level: 'info', message: `[o.a.c.health.HealthCheckHelper] Health check [consumers] is UP` }, + { timestamp: new Date(now - 15 * MIN).toISOString(), level: 'debug', message: `[o.a.c.component.kafka] KafkaConsumer[order-events] poll returned 42 records in 18ms` }, + { timestamp: new Date(now - 18 * MIN).toISOString(), level: 'info', message: `[o.a.c.impl.engine.InternalRouteStartup] Route order-enrichment started and consuming from: kafka:order-events` }, + { timestamp: new Date(now - 25 * MIN).toISOString(), level: 'warn', message: `[o.a.c.component.http] HTTP endpoint https://payment-api.internal/verify returned 503 — will retry` }, + { timestamp: new Date(now - 30 * MIN).toISOString(), level: 'info', message: `[o.a.c.impl.DefaultCamelContext] Apache Camel ${agentName} (CamelContext) is starting` }, + { timestamp: new Date(now - 32 * MIN).toISOString(), level: 'info', message: `[org.springframework.boot] Started ${agentName} in 4.231 seconds (process running for 4.892)` }, ] } -function formatLogTime(iso: string): string { - return new Date(iso).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }) -} - // ── Mock JVM / process info ────────────────────────────────────────────────── function buildProcessInfo(agent: typeof agents[0]) { @@ -144,7 +139,7 @@ export function AgentInstance() { const logEntries = buildLogEntries(agent.name) const filteredLogs = logFilter === 'all' ? logEntries - : logEntries.filter((l) => 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 */} From bd4e22eafb32fb8db61708e1ebe32aa8c8cd4b34 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:22:56 +0100 Subject: [PATCH 4/5] refactor: use SplitPane and EntityList in Admin RBAC tabs Co-Authored-By: Claude Opus 4.6 (1M context) --- src/pages/Admin/UserManagement/GroupsTab.tsx | 361 +++++++------ src/pages/Admin/UserManagement/RolesTab.tsx | 245 +++++---- .../UserManagement/UserManagement.module.css | 78 --- src/pages/Admin/UserManagement/UsersTab.tsx | 477 +++++++++--------- 4 files changed, 519 insertions(+), 642 deletions(-) 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" + /> Date: Tue, 24 Mar 2026 15:29:57 +0100 Subject: [PATCH 5/5] docs: update CLAUDE.md import examples with new components Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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'