Replace old design tokens and hardcoded colors across 29 files: - bg-primary/text-primary-foreground -> bg-accent/text-white - text-muted-foreground -> text-text-muted - text-destructive/bg-destructive -> text-error/bg-error - bg-muted -> bg-elevated (background usage) - Hardcoded green/red/yellow/emerald/amber/slate -> semantic tokens - Remove shadow-md/lg from cards, tooltips, topology nodes - rounded-xl -> rounded-lg on cards/panels - focus:ring-1 focus:ring-ring -> focus:border-accent on inputs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
128 lines
4.2 KiB
TypeScript
128 lines
4.2 KiB
TypeScript
import {
|
|
ResponsiveContainer,
|
|
AreaChart,
|
|
Area,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
} from 'recharts'
|
|
import type { InterfaceMetricPoint } from '@/lib/api'
|
|
|
|
interface TrafficChartProps {
|
|
data: InterfaceMetricPoint[]
|
|
interfaceName: string
|
|
}
|
|
|
|
function formatBps(bps: number): string {
|
|
if (bps >= 1_000_000_000) return `${(bps / 1_000_000_000).toFixed(1)} Gbps`
|
|
if (bps >= 1_000_000) return `${(bps / 1_000_000).toFixed(1)} Mbps`
|
|
if (bps >= 1_000) return `${(bps / 1_000).toFixed(1)} Kbps`
|
|
return `${bps} bps`
|
|
}
|
|
|
|
function formatBucket(bucket: string, useDate: boolean): string {
|
|
const d = new Date(bucket)
|
|
if (useDate) {
|
|
const mm = String(d.getMonth() + 1).padStart(2, '0')
|
|
const dd = String(d.getDate()).padStart(2, '0')
|
|
return `${mm}/${dd}`
|
|
}
|
|
const hh = String(d.getHours()).padStart(2, '0')
|
|
const min = String(d.getMinutes()).padStart(2, '0')
|
|
return `${hh}:${min}`
|
|
}
|
|
|
|
function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: Array<{ value?: number; dataKey?: string; name?: string; color?: string }>; label?: string }) {
|
|
if (!active || !payload?.length) return null
|
|
return (
|
|
<div className="rounded border border-border bg-surface px-2 py-1.5 text-xs text-text-primary">
|
|
<div className="mb-1 text-text-muted">{label}</div>
|
|
{payload.map((entry) => (
|
|
<div key={entry.dataKey} className="flex items-center gap-2">
|
|
<span style={{ color: entry.color }}>■</span>
|
|
<span>{entry.name === 'avg_rx_bps' ? 'RX' : 'TX'}</span>
|
|
<span className="ml-auto pl-4">{formatBps(entry.value ?? 0)}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function TrafficChart({ data, interfaceName }: TrafficChartProps) {
|
|
// Determine if we should show dates vs times based on data span
|
|
const useDate =
|
|
data.length >= 2
|
|
? new Date(data[data.length - 1].bucket).getTime() - new Date(data[0].bucket).getTime() >
|
|
2 * 24 * 60 * 60 * 1000
|
|
: false
|
|
|
|
const chartData = data.map((point) => ({
|
|
bucket: formatBucket(point.bucket, useDate),
|
|
avg_rx_bps: point.avg_rx_bps ?? 0,
|
|
avg_tx_bps: point.avg_tx_bps ?? 0,
|
|
}))
|
|
|
|
return (
|
|
<div>
|
|
<div className="mb-1 text-xs text-text-muted">{interfaceName}</div>
|
|
<ResponsiveContainer width="100%" height={200}>
|
|
<AreaChart data={chartData} margin={{ top: 4, right: 8, left: 0, bottom: 0 }}>
|
|
<defs>
|
|
<linearGradient id={`rx-grad-${interfaceName}`} x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stopColor="#38BDF8" stopOpacity={0.3} />
|
|
<stop offset="100%" stopColor="#38BDF8" stopOpacity={0} />
|
|
</linearGradient>
|
|
<linearGradient id={`tx-grad-${interfaceName}`} x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stopColor="#4ADE80" stopOpacity={0.3} />
|
|
<stop offset="100%" stopColor="#4ADE80" stopOpacity={0} />
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
|
<XAxis
|
|
dataKey="bucket"
|
|
tick={{ fontSize: 10, fill: '#94a3b8' }}
|
|
axisLine={false}
|
|
tickLine={false}
|
|
interval="preserveStartEnd"
|
|
/>
|
|
<YAxis
|
|
tickFormatter={formatBps}
|
|
tick={{ fontSize: 10, fill: '#94a3b8' }}
|
|
axisLine={false}
|
|
tickLine={false}
|
|
width={60}
|
|
/>
|
|
<Tooltip content={<CustomTooltip />} />
|
|
<Area
|
|
type="monotone"
|
|
dataKey="avg_rx_bps"
|
|
name="avg_rx_bps"
|
|
stroke="#38BDF8"
|
|
strokeWidth={1.5}
|
|
fill={`url(#rx-grad-${interfaceName})`}
|
|
/>
|
|
<Area
|
|
type="monotone"
|
|
dataKey="avg_tx_bps"
|
|
name="avg_tx_bps"
|
|
stroke="#4ADE80"
|
|
strokeWidth={1.5}
|
|
fill={`url(#tx-grad-${interfaceName})`}
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
<div className="mt-1 flex gap-4 text-xs text-text-muted">
|
|
<span>
|
|
<span className="mr-1 inline-block h-2 w-2 rounded-sm bg-chart-1" />
|
|
RX
|
|
</span>
|
|
<span>
|
|
<span className="mr-1 inline-block h-2 w-2 rounded-sm bg-chart-2" />
|
|
TX
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|