feat(ui): add Needs Attention panel and metrics strip to dashboard

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-21 14:30:33 -05:00
parent 86cff80c97
commit 4c3b95857a

View File

@@ -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<AttentionItem[]>(() => {
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 (
<div className="bg-panel border border-border-default rounded-sm mb-3.5">
{/* Header */}
<div className="px-3 py-2 border-b border-border-default bg-elevated">
<span className="text-[7px] font-medium text-text-muted uppercase tracking-[1.5px]">
Needs Attention
</span>
<span className="text-[7px] text-[hsl(var(--text-label))]"> · </span>
<span className="text-[7px] text-text-secondary font-mono">{count}</span>
</div>
{/* Rows */}
{count > 0 ? (
<div className="divide-y divide-border-subtle">
{items.map((item) => (
<div
key={item.id}
className="flex items-center justify-between px-3 py-1.5 border-l-2"
style={{
borderLeftColor:
item.severity === 'error'
? 'hsl(var(--error))'
: 'hsl(var(--warning))',
}}
>
<div className="flex items-center gap-2 min-w-0">
<span className="text-xs text-text-primary font-medium truncate">
{item.hostname}
</span>
<span className="text-[10px] text-text-secondary">
{item.model}
</span>
</div>
<span
className={cn(
'text-[10px] font-mono font-medium flex-shrink-0',
item.severity === 'error' ? 'text-error' : 'text-warning',
)}
>
{item.reason}
</span>
</div>
))}
</div>
) : (
<div className="py-5 text-center">
<span className="text-[9px] text-text-muted">No issues detected</span>
</div>
)}
</div>
)
}
// ─── 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 (
<div className="space-y-6" data-testid="dashboard">
{/* ── Page Header ─────────────────────────────────────────────────── */}
<div className="flex items-center justify-between gap-4">
<div className="flex items-center justify-between gap-4 pb-2.5 mb-3.5 border-b border-border-default">
<div>
<h1 className="text-xl font-semibold">Dashboard</h1>
<p className="text-sm text-text-muted mt-0.5">
<h1 className="text-sm font-semibold text-text-primary">Overview</h1>
<p className="text-[9px] text-text-muted mt-0.5">
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 */}
<KpiCards
totalDevices={totalDevices}
onlinePercent={onlinePercent}
activeAlerts={totalAlerts}
totalBandwidthBps={totalBandwidthBps}
/>
{/* Metrics Strip — joined 4-column bar */}
<div className="flex gap-px mb-3.5 bg-border-default rounded-sm overflow-hidden">
<div className="flex-1 bg-panel px-3 py-2">
<div className="text-lg font-medium font-mono text-text-primary">
{totalDevices}
</div>
<div className="text-[7px] text-text-muted uppercase tracking-[1.5px] font-medium mt-0.5">
Devices
</div>
</div>
<div className="flex-1 bg-panel px-3 py-2">
<div className="text-lg font-medium font-mono text-success">
{onlineDevices.length}
</div>
<div className="text-[7px] text-text-muted uppercase tracking-[1.5px] font-medium mt-0.5">
Online
</div>
</div>
<div className="flex-1 bg-panel px-3 py-2">
<div
className={cn(
'text-lg font-medium font-mono',
degradedCount > 0 ? 'text-warning' : 'text-text-primary',
)}
>
{degradedCount}
</div>
<div className="text-[7px] text-text-muted uppercase tracking-[1.5px] font-medium mt-0.5">
Degraded
</div>
</div>
<div className="flex-1 bg-panel px-3 py-2">
<div
className={cn(
'text-lg font-medium font-mono',
offlineCount > 0 ? 'text-error' : 'text-text-primary',
)}
>
{offlineCount}
</div>
<div className="text-[7px] text-text-muted uppercase tracking-[1.5px] font-medium mt-0.5">
Offline
</div>
</div>
</div>
{/* Needs Attention — full width */}
<NeedsAttention devices={fleetDevices ?? []} />
{/* Widget Grid — responsive 3 columns */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">