From 4c3b95857a960f7c7b3c9b7fd6157f1b01a91ed7 Mon Sep 17 00:00:00 2001 From: Jason Staack Date: Sat, 21 Mar 2026 14:30:33 -0500 Subject: [PATCH] feat(ui): add Needs Attention panel and metrics strip to dashboard Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/fleet/FleetDashboard.tsx | 182 ++++++++++++++++-- 1 file changed, 169 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/fleet/FleetDashboard.tsx b/frontend/src/components/fleet/FleetDashboard.tsx index 7aae5b8..e8ffd0b 100644 --- a/frontend/src/components/fleet/FleetDashboard.tsx +++ b/frontend/src/components/fleet/FleetDashboard.tsx @@ -1,7 +1,7 @@ import { useState, useMemo } from 'react' import { useQuery } from '@tanstack/react-query' import { useAuth } from '@/lib/auth' -import { metricsApi, tenantsApi } from '@/lib/api' +import { metricsApi, tenantsApi, type FleetDevice } from '@/lib/api' import { useUIStore } from '@/lib/store' import { alertsApi } from '@/lib/alertsApi' import { useEventStreamContext } from '@/contexts/EventStreamContext' @@ -11,7 +11,6 @@ import { LoadingText } from '@/components/ui/skeleton' import { EmptyState } from '@/components/ui/empty-state' // ─── Dashboard Widgets ─────────────────────────────────────────────────────── -import { KpiCards } from '@/components/dashboard/KpiCards' import { HealthScore } from '@/components/dashboard/HealthScore' import { EventsTimeline } from '@/components/dashboard/EventsTimeline' import { BandwidthChart, type BandwidthDevice } from '@/components/dashboard/BandwidthChart' @@ -40,6 +39,113 @@ function DashboardLoading() { ) } +// ─── Needs Attention (inline component) ───────────────────────────────────── + +interface AttentionItem { + id: string + hostname: string + model: string | null + severity: 'error' | 'warning' + reason: string +} + +function NeedsAttention({ devices }: { devices: FleetDevice[] }) { + const items = useMemo(() => { + const result: AttentionItem[] = [] + + for (const d of devices) { + if (d.status === 'offline') { + result.push({ + id: `${d.id}-offline`, + hostname: d.hostname, + model: d.model, + severity: 'error', + reason: 'Offline', + }) + } else if (d.status === 'degraded') { + result.push({ + id: `${d.id}-degraded`, + hostname: d.hostname, + model: d.model, + severity: 'warning', + reason: 'Degraded', + }) + } + + if (d.last_cpu_load != null && d.last_cpu_load > 80) { + result.push({ + id: `${d.id}-cpu`, + hostname: d.hostname, + model: d.model, + severity: 'warning', + reason: `CPU ${d.last_cpu_load}%`, + }) + } + } + + // Sort: errors first, then warnings + result.sort((a, b) => { + if (a.severity === b.severity) return 0 + return a.severity === 'error' ? -1 : 1 + }) + + return result.slice(0, 10) + }, [devices]) + + const count = items.length + + return ( +
+ {/* Header */} +
+ + Needs Attention + + · + {count} +
+ {/* Rows */} + {count > 0 ? ( +
+ {items.map((item) => ( +
+
+ + {item.hostname} + + + {item.model} + +
+ + {item.reason} + +
+ ))} +
+ ) : ( +
+ No issues detected +
+ )} +
+ ) +} + // ─── Fleet Dashboard ───────────────────────────────────────────────────────── export function FleetDashboard() { @@ -101,6 +207,15 @@ export function FleetDashboard() { const onlinePercent = totalDevices > 0 ? (onlineDevices.length / totalDevices) * 100 : 0 + const degradedCount = useMemo( + () => fleetDevices?.filter((d) => d.status === 'degraded').length ?? 0, + [fleetDevices], + ) + const offlineCount = useMemo( + () => fleetDevices?.filter((d) => d.status === 'offline').length ?? 0, + [fleetDevices], + ) + // Alert counts const alerts = alertsData?.items ?? [] const criticalCount = alerts.filter((a) => a.severity === 'critical').length @@ -156,10 +271,10 @@ export function FleetDashboard() { return (
{/* ── Page Header ─────────────────────────────────────────────────── */} -
+
-

Dashboard

-

+

Overview

+

Fleet overview across{' '} {isSuperAdmin ? selectedTenantId && selectedTenantName @@ -196,7 +311,7 @@ export function FleetDashboard() { 'px-2.5 py-1 text-xs font-medium transition-colors', 'first:rounded-l-md last:rounded-r-md', refreshInterval === opt.value - ? 'bg-accent/15 text-accent' + ? 'bg-accent-soft text-accent' : 'text-text-muted hover:text-text-secondary hover:bg-elevated/50', )} > @@ -218,13 +333,54 @@ export function FleetDashboard() { /> ) : ( <> - {/* KPI Cards — full width, 4 columns */} - + {/* Metrics Strip — joined 4-column bar */} +

+
+
+ {totalDevices} +
+
+ Devices +
+
+
+
+ {onlineDevices.length} +
+
+ Online +
+
+
+
0 ? 'text-warning' : 'text-text-primary', + )} + > + {degradedCount} +
+
+ Degraded +
+
+
+
0 ? 'text-error' : 'text-text-primary', + )} + > + {offlineCount} +
+
+ Offline +
+
+
+ + {/* Needs Attention — full width */} + {/* Widget Grid — responsive 3 columns */}