feat(TrafficDashboard): add summary statistics and visual representation for traffic data across all systems
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m56s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m56s
This commit is contained in:
@@ -182,6 +182,44 @@ export default function TrafficDashboard() {
|
|||||||
fetchStats();
|
fetchStats();
|
||||||
}, [fetchStats]);
|
}, [fetchStats]);
|
||||||
|
|
||||||
|
// Суммарная статистика по всем отображаемым системам (без ошибок, с данными)
|
||||||
|
const summary = (() => {
|
||||||
|
const withData = (jumphosts || []).filter((jh) => !jh.error && Array.isArray(jh.interfaces) && jh.interfaces.length > 0);
|
||||||
|
let totalBytes = 0;
|
||||||
|
let totalRx = 0;
|
||||||
|
let totalTx = 0;
|
||||||
|
const byJumphost = [];
|
||||||
|
for (const jh of withData) {
|
||||||
|
let jhTotal = 0;
|
||||||
|
let jhRx = 0;
|
||||||
|
let jhTx = 0;
|
||||||
|
for (const i of jh.interfaces) {
|
||||||
|
jhTotal += i.totalBytes || 0;
|
||||||
|
jhRx += i.rxBytes || 0;
|
||||||
|
jhTx += i.txBytes || 0;
|
||||||
|
}
|
||||||
|
totalBytes += jhTotal;
|
||||||
|
totalRx += jhRx;
|
||||||
|
totalTx += jhTx;
|
||||||
|
if (jhTotal > 0) {
|
||||||
|
byJumphost.push({
|
||||||
|
name: jh.name || jh.host || 'Jumphost',
|
||||||
|
value: jhTotal,
|
||||||
|
fill: CHART_COLORS[byJumphost.length % CHART_COLORS.length],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
totalBytes,
|
||||||
|
totalRx,
|
||||||
|
totalTx,
|
||||||
|
systemsCount: withData.length,
|
||||||
|
byJumphost,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
const hasSummary = summary.systemsCount > 0 && summary.totalBytes > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -217,13 +255,92 @@ export default function TrafficDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!loading && jumphosts.length > 0 && (
|
{!loading && jumphosts.length > 0 && (
|
||||||
<div className="row row-cards g-3">
|
<>
|
||||||
{jumphosts.map((jh) => (
|
{hasSummary && (
|
||||||
<div key={jh.serverId || jh.host || jh.name} className="col-12 col-md-6 col-xl-4">
|
<div className="card mb-4">
|
||||||
<JumphostCard jumphost={jh} />
|
<div className="card-header">
|
||||||
|
<h3 className="card-title mb-0">Суммарно по всем системам</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="d-flex flex-wrap align-items-stretch gap-4">
|
||||||
|
<div className="position-relative flex-shrink-0" style={{ width: 220, height: 220 }}>
|
||||||
|
<ResponsiveContainer width={220} height={220}>
|
||||||
|
<PieChart margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>
|
||||||
|
<Pie
|
||||||
|
data={summary.byJumphost}
|
||||||
|
dataKey="value"
|
||||||
|
nameKey="name"
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={58}
|
||||||
|
outerRadius={98}
|
||||||
|
paddingAngle={0}
|
||||||
|
stroke="none"
|
||||||
|
>
|
||||||
|
{summary.byJumphost.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.fill} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip formatter={(value) => formatBytes(value)} />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
<div
|
||||||
|
className="position-absolute top-50 start-50 translate-middle text-center text-body"
|
||||||
|
style={{ pointerEvents: 'none', width: '55%' }}
|
||||||
|
>
|
||||||
|
<div className="fw-bold lh-1" style={{ fontSize: '1.35rem' }}>
|
||||||
|
{formatBytes(summary.totalBytes)}
|
||||||
|
</div>
|
||||||
|
<div className="text-muted small mt-1" style={{ fontSize: '0.7rem', letterSpacing: '0.02em' }}>
|
||||||
|
TRAFFIC
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex flex-wrap gap-4 align-items-center flex-grow-1">
|
||||||
|
<div className="traffic-summary-legend">
|
||||||
|
{summary.byJumphost.map((item) => (
|
||||||
|
<div key={item.name} className="d-flex align-items-center mb-2 small">
|
||||||
|
<span
|
||||||
|
className="flex-shrink-0 rounded"
|
||||||
|
style={{ width: 10, height: 10, backgroundColor: item.fill }}
|
||||||
|
/>
|
||||||
|
<span className="ms-2">
|
||||||
|
{item.name} - {formatBytes(item.value)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="d-flex flex-wrap gap-3 ms-md-auto">
|
||||||
|
<div className="px-3 py-2 rounded bg-blue-lt text-blue">
|
||||||
|
<span className="d-block small text-muted">Всего трафика</span>
|
||||||
|
<span className="fw-bold">{formatBytes(summary.totalBytes)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="px-3 py-2 rounded bg-green-lt text-green">
|
||||||
|
<span className="d-block small text-muted">RX</span>
|
||||||
|
<span className="fw-bold">{formatBytes(summary.totalRx)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="px-3 py-2 rounded bg-orange-lt text-orange">
|
||||||
|
<span className="d-block small text-muted">TX</span>
|
||||||
|
<span className="fw-bold">{formatBytes(summary.totalTx)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="px-3 py-2 rounded bg-azure-lt text-azure">
|
||||||
|
<span className="d-block small text-muted">Систем</span>
|
||||||
|
<span className="fw-bold">{summary.systemsCount}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
<div className="row row-cards g-3">
|
||||||
|
{jumphosts.map((jh) => (
|
||||||
|
<div key={jh.serverId || jh.host || jh.name} className="col-12 col-md-6 col-xl-4">
|
||||||
|
<JumphostCard jumphost={jh} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user