Files

261 lines
11 KiB
HTML

<%
local http = require "luci.http"
local uci = require "luci.model.uci".cursor()
local jsonc = require "luci.jsonc"
local nsu = require "luci.netstat"
local have_vnstat = nsu.vnstat_installed()
local ifaces, db = nsu.db_interfaces()
local saved_mode = uci:get("netstats", "@config[0]", "mode") or "daily"
-- Requested interface, then the Settings choice / default-route device, as
-- long as vnStat has a database entry for it; else the first one it has.
local cur_iface = nsu.resolve_device(http.formvalue("iface"), db)
or nsu.wan_device(db)
or ifaces[1] or ""
local view_mode = http.formvalue("mode") or saved_mode
if view_mode ~= "daily" and view_mode ~= "monthly" then view_mode = "daily" end
local title = view_mode == "monthly" and translate("Monthly") or translate("Daily")
-- Raw vnStat JSON, parsed in the browser by netstat/vnstat-parse.js.
local raw = ""
if cur_iface ~= "" then
raw = nsu.vnstat_json_raw(view_mode == "monthly" and "m" or "d",
view_mode == "monthly" and 12 or 30, cur_iface) or ""
end
local raw_js = (jsonc.stringify(raw) or '""'):gsub("</", "<\\/")
%>
<link rel="stylesheet" href="/luci-static/resources/netstat/netstat-usage.css" />
<div class="netstat-dashboard">
<p class="netstat-intro">
<%:Usage is displayed using the Interface and View Mode selected in the Settings tab.%>
</p>
<% if have_vnstat and cur_iface ~= "" then %>
<div id="netstat-usage-data" style="display:none">
<div class="netstat-summary">
<div class="netstat-summary-item">
<span class="netstat-summary-dot rx"></span>
<span class="netstat-summary-label"><%:Download%>:</span>
<strong id="netstat-total-rx">-</strong>
</div>
<div class="netstat-summary-item">
<span class="netstat-summary-dot tx"></span>
<span class="netstat-summary-label"><%:Upload%>:</span>
<strong id="netstat-total-tx">-</strong>
</div>
<div class="netstat-summary-item netstat-summary-total">
<span class="netstat-summary-label"><%:Total%>:</span>
<strong id="netstat-total-all">-</strong>
</div>
</div>
<div class="netstat-chart-wrap">
<div class="netstat-chart-header">
<span class="netstat-chart-title"><%=title%> <%:Usage%></span>
<span class="netstat-chart-badge"><%=cur_iface%></span>
</div>
<div class="netstat-chart-box"><canvas id="trafficChart"></canvas></div>
</div>
</div>
<div id="netstat-usage-empty" class="netstat-empty" style="display:none">
<span class="netstat-empty-icon">&#128202;</span>
<%:No data available for%> <strong><%=cur_iface%></strong>
<div class="netstat-hint">
<%:vnStat may still be collecting its first data. It saves to its database every few minutes, try again shortly.%>
</div>
</div>
<script src="/luci-static/resources/netstat/chart.js"></script>
<script src="/luci-static/resources/netstat/chartjs-plugin-datalabels"></script>
<script src="/luci-static/resources/netstat/vnstat-parse.js"></script>
<script>
(function () {
// Chart text follows the theme's own text colour, so it is readable
// on light and dark themes alike; grid lines are translucent grey.
var dashboard = document.querySelector('.netstat-dashboard');
function themeTextColor() {
return getComputedStyle(dashboard).color || '#888';
}
var textColor = themeTextColor();
var gridColor = 'rgba(128,128,128,0.2)';
var usage = window.netstatVnstat.series(<%=raw_js%>, '<%=cur_iface%>', '<%=view_mode%>');
if (!usage.entries) {
document.getElementById('netstat-usage-empty').style.display = '';
return;
}
document.getElementById('netstat-usage-data').style.display = '';
var labels = usage.labels;
var rxData = usage.rx;
var txData = usage.tx;
// Height follows the number of rows; width follows the container
// (Chart.js responsive mode), so the chart never overflows when the
// theme's sidebar appears or the window is resized.
var BAR_H = 48;
var canvas = document.getElementById('trafficChart');
canvas.parentElement.style.height =
Math.min(Math.max(labels.length * BAR_H, 280), 700) + 'px';
var ctx = canvas.getContext('2d');
function rxGradient(c) {
var area = c.chart.chartArea;
if (!area) return '#4ade80';
var g = c.chart.ctx.createLinearGradient(area.left, 0, area.right, 0);
g.addColorStop(0, '#86efac');
g.addColorStop(1, '#15803d');
return g;
}
function fmtBytes(v) {
if (v >= 1099511627776) return (v/1099511627776).toFixed(2) + ' TB';
if (v >= 1073741824) return (v/1073741824).toFixed(2) + ' GB';
if (v >= 1048576) return (v/1048576).toFixed(2) + ' MB';
if (v >= 1024) return (v/1024).toFixed(2) + ' KB';
return v + ' B';
}
document.getElementById('netstat-total-rx').textContent = fmtBytes(usage.totalRx);
document.getElementById('netstat-total-tx').textContent = fmtBytes(usage.totalTx);
document.getElementById('netstat-total-all').textContent = fmtBytes(usage.totalRx + usage.totalTx);
var maxVal = Math.max.apply(null, rxData.concat(txData).concat([1]));
var chart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [
{
label: '<%:Download (RX)%>',
data: rxData,
backgroundColor: rxGradient,
borderRadius: 4,
borderSkipped: false,
datalabels: {
color: '#fff',
anchor: 'center',
align: 'center',
font: { size: 11, weight: '600' },
formatter: fmtBytes,
display: function(ctx) {
return maxVal > 0 && ctx.dataset.data[ctx.dataIndex] / maxVal > 0.08;
}
}
},
{
label: '<%:Upload (TX)%>',
data: txData,
backgroundColor: '#f59e0b',
borderRadius: 4,
borderSkipped: false,
datalabels: {
color: textColor,
anchor: 'end',
align: 'right',
font: { size: 11, weight: '600' },
formatter: fmtBytes,
display: function(ctx) {
return maxVal > 0 && ctx.dataset.data[ctx.dataIndex] / maxVal > 0.02;
}
}
}
]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
animation: { duration: 400 },
plugins: {
legend: {
position: 'top',
labels: {
color: textColor,
usePointStyle: true,
pointStyle: 'circle',
padding: 16
}
},
tooltip: {
callbacks: {
label: function(ctx) { return ' ' + fmtBytes(ctx.raw); }
}
}
},
scales: {
x: {
grid: { color: gridColor },
ticks: {
color: textColor,
callback: fmtBytes,
maxTicksLimit: 6
}
},
y: {
grid: { display: false },
ticks: { color: textColor, autoSkip: false }
}
}
},
plugins: [ChartDataLabels]
});
// Recolour when the theme switches between light and dark without a
// page reload (themes toggle a class or inline style on <html>/<body>).
function recolour() {
var c = themeTextColor();
if (c === textColor) return;
textColor = c;
chart.options.plugins.legend.labels.color = c;
chart.options.scales.x.ticks.color = c;
chart.options.scales.y.ticks.color = c;
chart.data.datasets[1].datalabels.color = c;
chart.update('none');
}
// Themes may animate the colour change (e.g. "transition: all 0.2s"),
// so check again once it has settled and whenever a colour
// transition finishes.
function recolourSoon() {
setTimeout(recolour, 50);
setTimeout(recolour, 500);
}
document.addEventListener('transitionend', function (ev) {
if (ev.propertyName === 'color') recolour();
});
if (window.MutationObserver) {
var mo = new MutationObserver(recolourSoon);
[document.documentElement, document.body].forEach(function (el) {
mo.observe(el, { attributes: true, attributeFilter: ['class', 'style', 'data-theme', 'data-darkmode'] });
});
var darkStyle = document.getElementById('dark-theme-style');
if (darkStyle) mo.observe(darkStyle, { childList: true, characterData: true, subtree: true });
}
})();
</script>
<% elseif not have_vnstat then %>
<div class="netstat-empty">
<span class="netstat-empty-icon">&#128225;</span>
<%:vnStat is not installed. Install the vnstat2 package to record traffic history.%>
</div>
<% else %>
<div class="netstat-empty">
<span class="netstat-empty-icon">&#128225;</span>
<%:vnStat is not tracking any interface yet.%>
<div class="netstat-hint">
<%:Interfaces are added automatically when they come up. You can also run "/usr/libexec/netstat-vnstat-track" on the router.%>
</div>
</div>
<% end %>
</div>