feat: add tunnel throughput distribution card with traffic summary stats and device bandwidth visualization

Add traffic overview card to dashboard main grid showing total received, sent, and combined tunnel throughput. Display top 5 devices with horizontal bar chart visualization proportional to busiest device total.

Calculate busiestDeviceTotal from topDevices max combined rx/tx bytes. Add dashboard-traffic-summary grid with three stat cards, dashboard-traffic-list with device rows showing name
This commit is contained in:
2026-03-24 18:06:38 +01:00
parent 80660d1c12
commit 6e68b13c06
2 changed files with 127 additions and 4 deletions

View File

@@ -5,6 +5,7 @@ import {
ArrowUpRight, ArrowUpRight,
FileClock, FileClock,
Network, Network,
TrendingUp,
Shield, Shield,
Users Users
} from "lucide-react"; } from "lucide-react";
@@ -94,6 +95,10 @@ export function DashboardPage() {
.sort((left, right) => (right.rx_bytes + right.tx_bytes) - (left.rx_bytes + left.tx_bytes)) .sort((left, right) => (right.rx_bytes + right.tx_bytes) - (left.rx_bytes + left.tx_bytes))
.slice(0, 5); .slice(0, 5);
const busiestDeviceTotal = topDevices.length
? Math.max(...topDevices.map((device) => (device.rx_bytes ?? 0) + (device.tx_bytes ?? 0)))
: 0;
const recentAudit = [...auditEvents] const recentAudit = [...auditEvents]
.sort((left, right) => new Date(right.created_at).getTime() - new Date(left.created_at).getTime()) .sort((left, right) => new Date(right.created_at).getTime() - new Date(left.created_at).getTime())
.slice(0, 6); .slice(0, 6);
@@ -112,6 +117,7 @@ export function DashboardPage() {
totalRX, totalRX,
totalTX, totalTX,
topDevices, topDevices,
busiestDeviceTotal,
recentAudit recentAudit
}; };
}, [auditQuery.data, devicesQuery.data, gatewaysQuery.data, policiesQuery.data, servicesQuery.data, usersQuery.data]); }, [auditQuery.data, devicesQuery.data, gatewaysQuery.data, policiesQuery.data, servicesQuery.data, usersQuery.data]);
@@ -192,6 +198,56 @@ export function DashboardPage() {
</div> </div>
<div className="dashboard-main-grid"> <div className="dashboard-main-grid">
<Card>
<div className="dashboard-section-head">
<div>
<p className="eyebrow">Traffic overview</p>
<h4>Tunnel throughput distribution</h4>
</div>
<div className="dashboard-inline-icon">
<TrendingUp size={16} strokeWidth={2} />
</div>
</div>
<div className="dashboard-traffic-summary">
<div className="dashboard-traffic-stat">
<span className="dashboard-posture-label">Received</span>
<strong>{formatBytes(summary.totalRX)}</strong>
</div>
<div className="dashboard-traffic-stat">
<span className="dashboard-posture-label">Sent</span>
<strong>{formatBytes(summary.totalTX)}</strong>
</div>
<div className="dashboard-traffic-stat">
<span className="dashboard-posture-label">Combined</span>
<strong>{formatBytes(summary.totalRX + summary.totalTX)}</strong>
</div>
</div>
<div className="dashboard-traffic-list">
{isLoading ? <p className="dashboard-empty">Loading traffic overview...</p> : null}
{!isLoading && summary.topDevices.length === 0 ? <p className="dashboard-empty">No tunnel traffic has been recorded yet.</p> : null}
{summary.topDevices.map((device) => {
const total = (device.rx_bytes ?? 0) + (device.tx_bytes ?? 0);
const width = summary.busiestDeviceTotal > 0 ? `${Math.max(8, Math.round((total / summary.busiestDeviceTotal) * 100))}%` : "0%";
return (
<div className="dashboard-traffic-row" key={device.id}>
<div className="dashboard-traffic-copy">
<strong>{device.name}</strong>
<span>{device.assigned_ip ?? "No VPN IP"} · {formatBytes(total)}</span>
</div>
<div className="dashboard-traffic-bar">
<span className="dashboard-traffic-bar-fill" style={{ width }} />
</div>
<div className="dashboard-traffic-split">
<small>RX {formatBytes(device.rx_bytes ?? 0)}</small>
<small>TX {formatBytes(device.tx_bytes ?? 0)}</small>
</div>
</div>
);
})}
</div>
</Card>
<Card> <Card>
<div className="dashboard-section-head"> <div className="dashboard-section-head">
<div> <div>
@@ -217,7 +273,9 @@ export function DashboardPage() {
))} ))}
</div> </div>
</Card> </Card>
</div>
<div className="dashboard-bottom-grid">
<Card> <Card>
<div className="dashboard-section-head"> <div className="dashboard-section-head">
<div> <div>
@@ -243,9 +301,7 @@ export function DashboardPage() {
))} ))}
</div> </div>
</Card> </Card>
</div>
<div className="dashboard-bottom-grid">
<Card> <Card>
<div className="dashboard-section-head"> <div className="dashboard-section-head">
<div> <div>

View File

@@ -524,7 +524,7 @@ button {
.dashboard-main-grid { .dashboard-main-grid {
display: grid; display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.8fr); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px; gap: 18px;
} }
@@ -570,6 +570,68 @@ button {
color: var(--muted); color: var(--muted);
} }
.dashboard-traffic-summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
margin-bottom: 18px;
}
.dashboard-traffic-stat {
display: grid;
gap: 6px;
padding: 16px;
border-radius: 16px;
border: 1px solid var(--line);
background: rgba(8, 14, 30, 0.58);
}
.dashboard-traffic-stat strong {
font-size: 1.2rem;
}
.dashboard-traffic-list {
display: grid;
gap: 14px;
}
.dashboard-traffic-row {
display: grid;
grid-template-columns: minmax(0, 220px) minmax(0, 1fr) auto;
align-items: center;
gap: 16px;
padding: 14px 16px;
border-radius: 16px;
border: 1px solid var(--line);
background: rgba(8, 14, 30, 0.58);
}
.dashboard-traffic-copy,
.dashboard-traffic-split {
display: grid;
gap: 4px;
}
.dashboard-traffic-copy span,
.dashboard-traffic-split small {
color: var(--muted);
}
.dashboard-traffic-bar {
position: relative;
height: 10px;
border-radius: 999px;
overflow: hidden;
background: rgba(255, 255, 255, 0.06);
}
.dashboard-traffic-bar-fill {
position: absolute;
inset: 0 auto 0 0;
border-radius: inherit;
background: linear-gradient(90deg, var(--accent) 0%, var(--accent-strong) 60%, var(--accent-violet) 100%);
}
.dashboard-posture-grid { .dashboard-posture-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -717,7 +779,12 @@ button {
.dashboard-bottom-grid, .dashboard-bottom-grid,
.dashboard-stats-grid, .dashboard-stats-grid,
.dashboard-posture-grid, .dashboard-posture-grid,
.dashboard-hero-metrics { .dashboard-hero-metrics,
.dashboard-traffic-summary {
grid-template-columns: 1fr;
}
.dashboard-traffic-row {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }