kenzok8-package/luci-app-wrtbwmon/htdocs/luci-static/resources/view/wrtbwmon/details.js
2023-10-30 16:22:53 +08:00

640 lines
19 KiB
JavaScript

'use strict';
'require dom';
'require fs';
'require poll';
'require rpc';
'require ui';
'require validation';
'require view';
var cachedData = [];
var luciConfig = '/etc/luci-wrtbwmon.conf';
var hostNameFile = '/etc/wrtbwmon.user';
var columns = {
thClient: _('Clients'),
thMAC: _('MAC'),
thDownload: _('Download'),
thUpload: _('Upload'),
thTotalDown: _('Total Down'),
thTotalUp: _('Total Up'),
thTotal: _('Total'),
thFirstSeen: _('First Seen'),
thLastSeen: _('Last Seen')
};
var callLuciDHCPLeases = rpc.declare({
object: 'luci-rpc',
method: 'getDHCPLeases',
expect: { '': {} }
});
var callLuciDSLStatus = rpc.declare({
object: 'luci-rpc',
method: 'getDSLStatus',
expect: { '': {} }
});
var callGetDatabaseRaw = rpc.declare({
object: 'luci.wrtbwmon',
method: 'get_db_raw',
params: [ 'protocol' ]
});
var callGetDatabasePath = rpc.declare({
object: 'luci.wrtbwmon',
method: 'get_db_path',
params: [ 'protocol' ]
});
var callRemoveDatabase = rpc.declare({
object: 'luci.wrtbwmon',
method: 'remove_db',
params: [ 'protocol' ]
});
function $(tid) {
return document.getElementById(tid);
}
function clickToResetDatabase(settings, table, updated, updating, ev) {
if (confirm(_('This will delete the database file. Are you sure?'))) {
return callRemoveDatabase(settings.protocol)
.then(function() {
updateData(settings, table, updated, updating, true);
});
}
}
function clickToSaveConfig(keylist, cstrs) {
var data = {};
for (var i = 0; i < keylist.length; i++) {
data[keylist[i]] = cstrs[keylist[i]].getValue();
}
ui.showModal(_('Configuration'), [
E('p', { 'class': 'spinning' }, _('Saving configuration data...'))
]);
return fs.write(luciConfig, JSON.stringify(data, undefined, '\t') + '\n')
.catch(function(err) {
ui.addNotification(null, E('p', {}, [ _('Unable to save %s: %s').format(luciConfig, err) ]));
})
.then(ui.hideModal)
.then(function() { document.location.reload(); });
}
function clickToSelectInterval(settings, updating, ev) {
if (ev.target.value > 0) {
settings.interval = parseInt(ev.target.value);
if (!poll.active()) poll.start();
}
else {
poll.stop();
setUpdateMessage(updating, -1);
}
}
function clickToSelectProtocol(settings, table, updated, updating, ev) {
settings.protocol = ev.target.value;
updateData(settings, table, updated, updating, true);
}
function createOption(args, val) {
var cstr = args[0], title = args[1], desc = args.slice(-1), widget, frame;
widget = args.length == 4 ? new cstr(val, args[2]) : new cstr(val, args[2], args[3]);
frame = E('div', {'class': 'cbi-value'}, [
E('label', {'class': 'cbi-value-title'}, title),
E('div', {'class': 'cbi-value-field'}, E('div', {}, widget.render()))
]);
if (desc && desc != '')
dom.append(frame.lastChild, E('div', { 'class': 'cbi-value-description' }, desc));
return [widget, frame];
}
function displayTable(tb, settings) {
var elm, elmID, col, sortedBy, flag, IPVer;
elm = tb.querySelector('.th.sorted');
elmID = elm ? elm.id : 'thTotal';
sortedBy = elm && elm.classList.contains('ascent') ? 'asc' : 'desc';
col = Object.keys(columns).indexOf(elmID);
IPVer = col == 0 ? settings.protocol : null;
flag = sortedBy == 'desc' ? 1 : -1;
cachedData[0].sort(sortTable.bind(this, col, IPVer, flag));
//console.time('show');
updateTable(tb, cachedData, '<em>%s</em>'.format(_('Collecting data...')), settings);
//console.timeEnd('show');
progressbar('downstream', cachedData[1][0], settings.downstream, settings.useBits, settings.useMultiple);
progressbar('upstream', cachedData[1][1], settings.upstream, settings.useBits, settings.useMultiple);
}
function formatSize(size, useBits, useMultiple) {
// String.format automatically adds the i for KiB if the multiple is 1024
return String.format('%%%s.2m%s'.format(useMultiple, (useBits ? 'bit' : 'B')), useBits ? size * 8 : size);
}
function formatSpeed(speed, useBits, useMultiple) {
return formatSize(speed, useBits, useMultiple) + '/s';
}
function formatDate(d) {
var Y = d.getFullYear(), M = d.getMonth() + 1, D = d.getDate(),
hh = d.getHours(), mm = d.getMinutes(), ss = d.getSeconds();
return '%04d/%02d/%02d %02d:%02d:%02d'.format(Y, M, D, hh, mm, ss);
}
function getDSLBandwidth() {
return callLuciDSLStatus().then(function(res) {
return {
upstream : res.max_data_rate_up || null,
downstream : res.max_data_rate_down || null
};
});
}
function handleConfig(ev) {
ui.showModal(_('Configuration'), [
E('p', { 'class': 'spinning' }, _('Loading configuration data...'))
]);
parseDefaultSettings(luciConfig)
.then(function(settings) {
var arglist, keylist = Object.keys(settings), res, cstrs = {}, node = [], body;
arglist = [
[ui.Select, _('Default Protocol'), {'ipv4': _('ipv4'), 'ipv6': _('ipv6')}, {}, ''],
[ui.Select, _('Default Refresh Interval'), {'-1': _('Disabled'), '2': _('2 seconds'), '5': _('5 seconds'), '10': _('10 seconds'), '30': _('30 seconds')}, {sort: ['-1', '2', '5', '10', '30']}, ''],
[ui.Dropdown, _('Default Columns'), columns, {multiple: true, sort: false, custom_placeholder: '', dropdown_items: 3}, ''],
[ui.Checkbox, _('Show Zeros'), {value_enabled: true, value_disabled: false}, ''],
[ui.Checkbox, _('Transfer Speed in Bits'), {value_enabled: true, value_disabled: false}, ''],
[ui.Select, _('Multiple of Unit'), {'1000': _('SI - 1000'), '1024': _('IEC - 1024')}, {}, ''],
[ui.Checkbox, _('Use DSL Bandwidth'), {value_enabled: true, value_disabled: false}, ''],
[ui.Textfield, _('Upstream Bandwidth'), {datatype: 'ufloat'}, 'Mbps'],
[ui.Textfield, _('Downstream Bandwidth'), {datatype: 'ufloat'}, 'Mbps'],
[ui.DynamicList, _('Hide MAC Addresses'), '', {datatype: 'macaddr'}, '']
]; // [constructor, label(, all_choices), options, description]
for (var i = 0; i < keylist.length; i++) {
res = createOption(arglist[i], settings[keylist[i]]);
cstrs[keylist[i]] = res[0];
node.push(res[1]);
}
body = [
E('p', {}, _('Configure the default values for luci-app-wrtbwmon.')),
E('div', {}, node),
E('div', { 'class': 'right' }, [
E('div', {
'class': 'btn cbi-button-neutral',
'click': ui.hideModal
}, _('Cancel')),
' ',
E('div', {
'class': 'btn cbi-button-positive',
'click': clickToSaveConfig.bind(this, keylist, cstrs),
'disabled': (L.hasViewPermission ? !L.hasViewPermission() : null) || null
}, _('Save'))
])
];
ui.showModal(_('Configuration'), body);
})
}
function loadCss(path) {
var head = document.head || document.getElementsByTagName('head')[0];
var link = E('link', {
'rel': 'stylesheet',
'href': path,
'type': 'text/css'
});
head.appendChild(link);
}
function parseDatabase(raw, hosts, showZero, hideMACs) {
var values = [],
totals = [0, 0, 0, 0, 0],
rows = raw.trim().split(/\r?\n|\r/g),
rowIndex = [1, 0, 3, 4, 5, 6, 7, 8, 9, 0];
rows.shift();
for (var i = 0; i < rows.length; i++) {
var row = rows[i].split(',');
if ((!showZero && row[7] == 0) || hideMACs.indexOf(row[0]) >= 0) continue;
for (var j = 0; j < totals.length; j++) {
totals[j] += parseInt(row[3 + j]);
}
var newRow = rowIndex.map(function(i) { return row[i] });
if (newRow[1].toLowerCase() in hosts) {
newRow[9] = hosts[newRow[1].toLowerCase()];
}
values.push(newRow);
}
return [values, totals];
}
function parseDefaultSettings(file) {
var defaultColumns = ['thClient', 'thDownload', 'thUpload', 'thTotalDown', 'thTotalUp', 'thTotal'],
keylist = ['protocol', 'interval', 'showColumns', 'showZero', 'useBits', 'useMultiple', 'useDSL', 'upstream', 'downstream', 'hideMACs'],
valuelist = ['ipv4', '5', defaultColumns, true, false, '1000', false, '100', '100', []];
return fs.read_direct(file, 'json').then(function(oldSettings) {
var settings = {};
for (var i = 0; i < keylist.length; i++) {
if (!(keylist[i] in oldSettings))
settings[keylist[i]] = valuelist[i];
else
settings[keylist[i]] = oldSettings[keylist[i]];
}
if (settings.useDSL) {
return getDSLBandwidth().then(function(dsl) {
for (var s in dsl)
settings[s] = dsl[s];
return settings;
});
}
else {
return settings;
}
})
.catch(function() { return {} });
}
function progressbar(query, v, m, useBits, useMultiple) {
// v = B/s, m = Mb/s
var pg = $(query),
vn = (v * 8) || 0,
mn = (m || 100) * Math.pow(1000, 2),
fv = formatSpeed(v, useBits, useMultiple),
pc = '%.2f'.format((100 / mn) * vn),
wt = Math.floor(pc > 100 ? 100 : pc),
bgc = (pc >= 95 ? 'red' : (pc >= 80 ? 'darkorange' : (pc >= 60 ? 'yellow' : 'lime')));
if (pg) {
pg.firstElementChild.style.width = wt + '%';
pg.firstElementChild.style.background = bgc;
pg.setAttribute('title', '%s (%f%%)'.format(fv, pc));
}
}
function setupThisDOM(settings, table) {
document.addEventListener('poll-stop', function() {
$('selectInterval').value = -1;
});
document.addEventListener('poll-start', function() {
$('selectInterval').value = settings.interval;
});
table.querySelectorAll('.th').forEach(function(e) {
if (e) {
e.addEventListener('click', function (ev) {
setSortedColumn(ev.target);
displayTable(table, settings);
});
if (settings.showColumns.indexOf(e.id) >= 0)
e.classList.remove('hide');
else
e.classList.add('hide');
}
});
}
function renameFile(str, tag) {
var n = str.lastIndexOf('/'), fn = n > -1 ? str.slice(n + 1) : str, dir = n > -1 ? str.slice(0, n + 1) : '';
var n = fn.lastIndexOf('.'), bn = n > -1 ? fn.slice(0, n) : fn;
var n = fn.lastIndexOf('.'), en = n > -1 ? fn.slice(n + 1) : '';
return dir + bn + '.' + tag + (en ? '.' + en : '');
}
function resolveCustomizedHostName() {
return fs.stat(hostNameFile).then(function() {
return fs.read_direct(hostNameFile).then(function(raw) {
var arr = raw.trim().split(/\r?\n/), hosts = {}, row;
for (var i = 0; i < arr.length; i++) {
row = arr[i].split(',');
if (row.length == 2 && row[0])
hosts[row[0].toLowerCase()] = row[1];
}
return hosts;
})
})
.catch(function() { return []; });
}
function resolveHostNameByMACAddr() {
return Promise.all([
resolveCustomizedHostName(),
callLuciDHCPLeases()
]).then(function(res) {
var hosts = res[0];
for (var key in res[1]) {
var leases = Array.isArray(res[1][key]) ? res[1][key] : [];
for (var i = 0; i < leases.length; i++) {
if(leases[i].macaddr) {
var macaddr = leases[i].macaddr.toLowerCase();
if (!(macaddr in hosts) && Boolean(leases[i].hostname))
hosts[macaddr] = leases[i].hostname;
}
}
}
return hosts;
});
}
function setSortedColumn(sorting) {
var sorted = document.querySelector('.th.sorted') || $('thTotal');
if (sorting.isSameNode(sorted)) {
sorting.classList.toggle('ascent');
}
else {
sorting.classList.add('sorted');
sorted.classList.remove('sorted', 'ascent');
}
}
function setUpdateMessage(e, sec) {
e.innerHTML = sec < 0 ? '' : _('Updating again in %s second(s).').format('<b>' + sec + '</b>');
}
function sortTable(col, IPVer, flag, x, y) {
var byCol = x[col] == y[col] ? 1 : col;
var a = x[byCol], b = y[byCol];
if (!IPVer || byCol != 0) {
if (!(a.match(/\D/g) || b.match(/\D/g)))
a = parseInt(a), b = parseInt(b);
}
else {
IPVer == 'ipv4'
? (a = validation.parseIPv4(a) || [0, 0, 0, 0], b = validation.parseIPv4(b) || [0, 0, 0, 0])
: (a = validation.parseIPv6(a) || [0, 0, 0, 0, 0, 0, 0, 0], b = validation.parseIPv6(b) || [0, 0, 0, 0, 0, 0, 0, 0]);
}
if (Array.isArray(a) && Array.isArray(b)) {
for (var i = 0; i < a.length; i++) {
if (a[i] != b[i]) {
return (b[i] - a[i]) * flag;
}
}
return 0;
}
return a == b ? 0 : (a < b ? 1 : -1) * flag;
}
function updateData(settings, table, updated, updating, once) {
var tick = poll.tick,
interval = settings.interval,
sec = (interval - tick % interval) % interval;
if (!sec || once) {
callGetDatabasePath()
.then(function(res) {
var params = settings.protocol == 'ipv4' ? '-4' : '-6';
return fs.exec_direct('/usr/sbin/wrtbwmon', [params, '-f', res.file_4])
})
.then(function() {
return Promise.all([
callGetDatabaseRaw(settings.protocol),
resolveHostNameByMACAddr()
]);
})
.then(function(res) {
//console.time('start');
cachedData = parseDatabase(res[0].data || '', res[1], settings.showZero, settings.hideMACs);
displayTable(table, settings);
updated.textContent = _('Last updated at %s.').format(formatDate(new Date(document.lastModified)));
//console.timeEnd('start');
});
}
setUpdateMessage(updating, sec);
if (!sec)
setTimeout(setUpdateMessage.bind(this, updating, interval), 100);
}
function updateTable(tb, values, placeholder, settings) {
var fragment = document.createDocumentFragment(), nodeLen = tb.childElementCount - 2;
var formData = values[0], tbTitle = tb.firstElementChild, newNode, childTD;
// Update the table data.
for (var i = 0; i < formData.length; i++) {
if (i < nodeLen) {
newNode = tbTitle.nextElementSibling;
}
else {
if (nodeLen > 0) {
newNode = fragment.firstChild.cloneNode(true);
}
else {
newNode = document.createElement('tr');
childTD = document.createElement('td');
for (var j = 0; j < tbTitle.children.length; j++) {
childTD.className = 'td' + (settings.showColumns.indexOf(tbTitle.children[j].id) >= 0 ? '' : ' hide');
childTD.setAttribute('data-title', tbTitle.children[j].textContent);
newNode.appendChild(childTD.cloneNode(true));
}
}
newNode.className = 'tr cbi-rowstyle-%d'.format(i % 2 ? 2 : 1);
}
childTD = newNode.firstElementChild;
childTD.title = formData[i].slice(-1);
for (var j = 0; j < tbTitle.childElementCount; j++, childTD = childTD.nextElementSibling) {
switch (j) {
case 2:
case 3:
childTD.textContent = formatSpeed(formData[i][j], settings.useBits, settings.useMultiple);
break;
case 4:
case 5:
case 6:
childTD.textContent = formatSize(formData[i][j], settings.useBits, settings.useMultiple);
break;
case 7:
case 8:
childTD.textContent = formatDate(new Date(formData[i][j] * 1000));
break;
default:
childTD.textContent = formData[i][j];
}
}
fragment.appendChild(newNode);
}
// Remove the table data which has been deleted from the database.
while (tb.childElementCount > 1) {
tb.removeChild(tbTitle.nextElementSibling);
}
//Append the totals or placeholder row.
if (formData.length == 0) {
newNode = document.createElement('tr');
newNode.className = 'tr placeholder';
childTD = document.createElement('td');
childTD.className = 'td';
childTD.innerHTML = placeholder;
newNode.appendChild(childTD);
}
else{
newNode = fragment.firstChild.cloneNode(true);
newNode.className = 'tr table-totals';
newNode.children[0].textContent = _('TOTAL') + (settings.showColumns.indexOf('thMAC') >= 0 ? '' : ': ' + formData.length);
newNode.children[1].textContent = formData.length + ' ' + _('Clients');
for (var j = 0; j < tbTitle.childElementCount; j++) {
switch(j) {
case 0:
case 1:
newNode.children[j].removeAttribute('title');
newNode.children[j].style.fontWeight = 'bold';
break;
case 2:
case 3:
newNode.children[j].textContent = formatSpeed(values[1][j - 2], settings.useBits, settings.useMultiple);
break;
case 4:
case 5:
case 6:
newNode.children[j].textContent = formatSize(values[1][j - 2], settings.useBits, settings.useMultiple);
break;
default:
newNode.children[j].textContent = '';
newNode.children[j].removeAttribute('data-title');
}
}
}
fragment.appendChild(newNode);
tb.appendChild(fragment);
}
function initOption(options, selected) {
var res = [], attr = {};
for (var idx in options) {
attr.value = idx;
attr.selected = idx == selected ? '' : null;
res.push(E('option', attr, options[idx]));
}
return res;
}
return view.extend({
load: function() {
return Promise.all([
parseDefaultSettings(luciConfig),
loadCss(L.resource('view/wrtbwmon/wrtbwmon.css'))
]);
},
render: function(data) {
var settings = data[0],
labelUpdated = E('label'),
labelUpdating = E('label'),
table = E('table', { 'class': 'table', 'id': 'traffic' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th', 'id': 'thClient' }, _('Clients')),
E('th', { 'class': 'th hide', 'id': 'thMAC' }, _('MAC')),
E('th', { 'class': 'th', 'id': 'thDownload' }, _('Download')),
E('th', { 'class': 'th', 'id': 'thUpload' }, _('Upload')),
E('th', { 'class': 'th', 'id': 'thTotalDown' }, _('Total Down')),
E('th', { 'class': 'th', 'id': 'thTotalUp' }, _('Total Up')),
E('th', { 'class': 'th sorted', 'id': 'thTotal' }, _('Total')),
E('th', { 'class': 'th hide', 'id': 'thFirstSeen' }, _('First Seen')),
E('th', { 'class': 'th hide', 'id': 'thLastSeen' }, _('Last Seen'))
]),
E('tr', {'class': 'tr placeholder'}, [
E('td', { 'class': 'td' }, E('em', {}, _('Collecting data...')))
])
]);
poll.add(updateData.bind(this, settings, table, labelUpdated, labelUpdating, false), 1);
setupThisDOM(settings, table);
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('Usage - Details')),
E('div', { 'class': 'cbi-section' }, [
E('div', { 'id': 'control_panel' }, [
E('div', {}, [
E('label', {}, _('Protocol:')),
E('select', {
'id': 'selectProtocol',
'change': clickToSelectProtocol.bind(this, settings, table, labelUpdated, labelUpdating)
}, initOption({
'ipv4': 'ipv4',
'ipv6': 'ipv6'
}, settings.protocol))
]),
E('div', {}, [
E('button', {
'class': 'btn cbi-button cbi-button-reset important',
'id': 'resetDatabase',
'click': clickToResetDatabase.bind(this, settings, table, labelUpdated, labelUpdating)
}, _('Reset Database')),
' ',
E('button', {
'class': 'btn cbi-button cbi-button-neutral',
'click': handleConfig
}, _('Configure Options'))
])
]),
E('div', {}, [
E('div', {}, [ labelUpdated, labelUpdating ]),
E('div', {}, [
E('label', { 'for': 'selectInterval' }, _('Auto update every:')),
E('select', {
'id': 'selectInterval',
'change': clickToSelectInterval.bind(this, settings, labelUpdating)
}, initOption({
'-1': _('Disabled'),
'2': _('2 seconds'),
'5': _('5 seconds'),
'10': _('10 seconds'),
'30': _('30 seconds')
}, settings.interval))
])
]),
E('div', { 'id': 'progressbar_panel' }, [
E('div', {}, [
E('label', {}, _('Downstream:')),
E('div', {
'id': 'downstream',
'class': 'cbi-progressbar',
'title': '-'
}, E('div')
)
]),
E('div', {}, [
E('label', {}, _('Upstream:')),
E('div', {
'id': 'upstream',
'class': 'cbi-progressbar',
'title': '-'
}, E('div')
)
]),
]),
table
])
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});