feat(15-03): add signal history charts with expandable rows in station and link tables

- Create SignalHistoryChart with recharts LineChart, green/yellow/red reference bands, and 24h/7d/30d range selector
- Add expandable rows to WirelessStationTable (click station to see signal history)
- Add expandable rows to WirelessLinksTable CPE rows (click link to see signal history)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-19 07:22:49 -05:00
parent ef82a0d294
commit 3bddd6f654
6 changed files with 271 additions and 76 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useMemo } from 'react'
import React, { useState, useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Wifi } from 'lucide-react'
import { wirelessApi, type LinkResponse } from '@/lib/api'
@@ -14,6 +14,7 @@ import {
SelectValue,
} from '@/components/ui/select'
import { signalColor } from './signal-color'
import { SignalHistoryChart } from './SignalHistoryChart'
function timeAgo(dateStr: string): string {
const diff = Date.now() - new Date(dateStr).getTime()
@@ -180,6 +181,8 @@ function APGroup({
apDeviceId: string
links: LinkResponse[]
}) {
const [expandedLinkId, setExpandedLinkId] = useState<string | null>(null)
return (
<>
{/* AP header row */}
@@ -198,34 +201,47 @@ function APGroup({
</tr>
{/* CPE rows */}
{links.map((link) => (
<tr
key={link.id}
className="border-b border-border/50 hover:bg-elevated/50 transition-colors"
>
<td className="px-2 py-1.5 pl-6">
<DeviceLink tenantId={tenantId} deviceId={link.cpe_device_id}>
{link.cpe_hostname ?? link.client_mac}
</DeviceLink>
</td>
<td className={cn('px-2 py-1.5 text-right font-medium', signalColor(link.signal_strength))}>
{link.signal_strength != null ? `${link.signal_strength} dBm` : '--'}
</td>
<td className="px-2 py-1.5 text-right text-text-secondary">
{link.tx_ccq != null ? `${link.tx_ccq}%` : '--'}
</td>
<td className="px-2 py-1.5 text-right text-text-secondary">
{link.tx_rate ?? '--'}
</td>
<td className="px-2 py-1.5 text-right text-text-secondary">
{link.rx_rate ?? '--'}
</td>
<td className="px-2 py-1.5 text-center">
<StateBadge state={link.state} />
</td>
<td className="px-2 py-1.5 text-right text-text-muted text-xs">
{timeAgo(link.last_seen)}
</td>
</tr>
<React.Fragment key={link.id}>
<tr
className="border-b border-border/50 hover:bg-elevated/50 transition-colors cursor-pointer"
onClick={() => setExpandedLinkId(expandedLinkId === link.id ? null : link.id)}
>
<td className="px-2 py-1.5 pl-6">
<DeviceLink tenantId={tenantId} deviceId={link.cpe_device_id}>
{link.cpe_hostname ?? link.client_mac}
</DeviceLink>
</td>
<td className={cn('px-2 py-1.5 text-right font-medium', signalColor(link.signal_strength))}>
{link.signal_strength != null ? `${link.signal_strength} dBm` : '--'}
</td>
<td className="px-2 py-1.5 text-right text-text-secondary">
{link.tx_ccq != null ? `${link.tx_ccq}%` : '--'}
</td>
<td className="px-2 py-1.5 text-right text-text-secondary">
{link.tx_rate ?? '--'}
</td>
<td className="px-2 py-1.5 text-right text-text-secondary">
{link.rx_rate ?? '--'}
</td>
<td className="px-2 py-1.5 text-center">
<StateBadge state={link.state} />
</td>
<td className="px-2 py-1.5 text-right text-text-muted text-xs">
{timeAgo(link.last_seen)}
</td>
</tr>
{expandedLinkId === link.id && (
<tr>
<td colSpan={7} className="px-3 py-3 bg-elevated/20">
<SignalHistoryChart
tenantId={tenantId}
deviceId={link.ap_device_id}
macAddress={link.client_mac}
/>
</td>
</tr>
)}
</React.Fragment>
))}
</>
)