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:
@@ -5,6 +5,7 @@ import {
|
||||
ArrowUpRight,
|
||||
FileClock,
|
||||
Network,
|
||||
TrendingUp,
|
||||
Shield,
|
||||
Users
|
||||
} 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))
|
||||
.slice(0, 5);
|
||||
|
||||
const busiestDeviceTotal = topDevices.length
|
||||
? Math.max(...topDevices.map((device) => (device.rx_bytes ?? 0) + (device.tx_bytes ?? 0)))
|
||||
: 0;
|
||||
|
||||
const recentAudit = [...auditEvents]
|
||||
.sort((left, right) => new Date(right.created_at).getTime() - new Date(left.created_at).getTime())
|
||||
.slice(0, 6);
|
||||
@@ -112,6 +117,7 @@ export function DashboardPage() {
|
||||
totalRX,
|
||||
totalTX,
|
||||
topDevices,
|
||||
busiestDeviceTotal,
|
||||
recentAudit
|
||||
};
|
||||
}, [auditQuery.data, devicesQuery.data, gatewaysQuery.data, policiesQuery.data, servicesQuery.data, usersQuery.data]);
|
||||
@@ -192,6 +198,56 @@ export function DashboardPage() {
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<div className="dashboard-section-head">
|
||||
<div>
|
||||
@@ -217,7 +273,9 @@ export function DashboardPage() {
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="dashboard-bottom-grid">
|
||||
<Card>
|
||||
<div className="dashboard-section-head">
|
||||
<div>
|
||||
@@ -243,9 +301,7 @@ export function DashboardPage() {
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="dashboard-bottom-grid">
|
||||
<Card>
|
||||
<div className="dashboard-section-head">
|
||||
<div>
|
||||
|
||||
@@ -524,7 +524,7 @@ button {
|
||||
|
||||
.dashboard-main-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.8fr);
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
@@ -570,6 +570,68 @@ button {
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
@@ -717,7 +779,12 @@ button {
|
||||
.dashboard-bottom-grid,
|
||||
.dashboard-stats-grid,
|
||||
.dashboard-posture-grid,
|
||||
.dashboard-hero-metrics {
|
||||
.dashboard-hero-metrics,
|
||||
.dashboard-traffic-summary {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-traffic-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user