diff options
Diffstat (limited to 'applications/luci-app-nlbwmon/htdocs')
4 files changed, 1230 insertions, 686 deletions
diff --git a/applications/luci-app-nlbwmon/htdocs/luci-static/resources/view/nlbw.js b/applications/luci-app-nlbwmon/htdocs/luci-static/resources/view/nlbw.js deleted file mode 100644 index 8a01b1db4..000000000 --- a/applications/luci-app-nlbwmon/htdocs/luci-static/resources/view/nlbw.js +++ /dev/null @@ -1,686 +0,0 @@ -var chartRegistry = {}, - trafficPeriods = [], - trafficData = { columns: [], data: [] }, - hostNames = {}, - ouiData = []; - - -function off(elem) -{ - var val = [0, 0]; - do { - if (!isNaN(elem.offsetLeft) && !isNaN(elem.offsetTop)) { - val[0] += elem.offsetLeft; - val[1] += elem.offsetTop; - } - } - while ((elem = elem.offsetParent) != null); - return val; -} - -Chart.defaults.global.customTooltips = function(tooltip) { - var tooltipEl = document.getElementById('chartjs-tooltip'); - - if (!tooltipEl) { - tooltipEl = document.createElement('div'); - tooltipEl.setAttribute('id', 'chartjs-tooltip'); - document.body.appendChild(tooltipEl); - } - - if (!tooltip) { - if (tooltipEl.row) - tooltipEl.row.style.backgroundColor = ''; - - tooltipEl.style.opacity = 0; - return; - } - - var pos = off(tooltip.chart.canvas); - - tooltipEl.className = tooltip.yAlign; - tooltipEl.innerHTML = tooltip.text[0]; - - tooltipEl.style.opacity = 1; - tooltipEl.style.left = pos[0] + tooltip.x + 'px'; - tooltipEl.style.top = pos[1] + tooltip.y - tooltip.caretHeight - tooltip.caretPadding + 'px'; - - console.debug(tooltip.text); - - var row = findParent(tooltip.text[1], '.tr'), - hue = tooltip.text[2]; - - if (row && !isNaN(hue)) { - row.style.backgroundColor = 'hsl(%u, 100%%, 80%%)'.format(hue); - tooltipEl.row = row; - } -}; - -Chart.defaults.global.tooltipFontSize = 10; -Chart.defaults.global.tooltipTemplate = function(tip) { - tip.label[0] = tip.label[0].format(tip.value); - return tip.label; -}; - -function kpi(id, val1, val2, val3) -{ - var e = L.dom.elem(id) ? id : document.getElementById(id); - - if (val1 && val2 && val3) - e.innerHTML = _('%s, %s and %s').format(val1, val2, val3); - else if (val1 && val2) - e.innerHTML = _('%s and %s').format(val1, val2); - else if (val1) - e.innerHTML = val1; - - e.parentNode.style.display = val1 ? 'list-item' : ''; -} - -function pie(id, data) -{ - var total = data.reduce(function(n, d) { return n + d.value }, 0); - - data.sort(function(a, b) { return b.value - a.value }); - - if (total === 0) - data = [{ - value: 1, - color: '#cccccc', - label: [ _('no traffic') ] - }]; - - for (var i = 0; i < data.length; i++) { - if (!data[i].color) { - var hue = 120 / (data.length-1) * i; - data[i].color = 'hsl(%u, 80%%, 50%%)'.format(hue); - data[i].label.push(hue); - } - } - - var node = L.dom.elem(id) ? id : document.getElementById(id), - key = L.dom.elem(id) ? id.id : id, - ctx = node.getContext('2d'); - - if (chartRegistry.hasOwnProperty(key)) - chartRegistry[key].destroy(); - - chartRegistry[key] = new Chart(ctx).Doughnut(data, { - segmentStrokeWidth: 1, - percentageInnerCutout: 30 - }); - - return chartRegistry[key]; -} - -function query(filter, group, order) -{ - var keys = [], columns = {}, records = {}, result = []; - - if (typeof(group) !== 'function' && typeof(group) !== 'object') - group = ['mac']; - - for (var i = 0; i < trafficData.columns.length; i++) - columns[trafficData.columns[i]] = i; - - for (var i = 0; i < trafficData.data.length; i++) { - var record = trafficData.data[i]; - - if (typeof(filter) === 'function' && filter(columns, record) !== true) - continue; - - var key; - - if (typeof(group) === 'function') { - key = group(columns, record); - } - else { - key = []; - - for (var j = 0; j < group.length; j++) - if (columns.hasOwnProperty(group[j])) - key.push(record[columns[group[j]]]); - - key = key.join(','); - } - - if (!records.hasOwnProperty(key)) { - var rec = {}; - - for (var col in columns) - rec[col] = record[columns[col]]; - - records[key] = rec; - result.push(rec); - } - else { - records[key].conns += record[columns.conns]; - records[key].rx_bytes += record[columns.rx_bytes]; - records[key].rx_pkts += record[columns.rx_pkts]; - records[key].tx_bytes += record[columns.tx_bytes]; - records[key].tx_pkts += record[columns.tx_pkts]; - } - } - - if (typeof(order) === 'function') - result.sort(order); - - return result; -} - -function oui(mac) { - var m, l = 0, r = ouiData.length / 3 - 1; - var mac1 = parseInt(mac.replace(/[^a-fA-F0-9]/g, ''), 16); - - while (l <= r) { - m = l + Math.floor((r - l) / 2); - - var mask = (0xffffffffffff - - (Math.pow(2, 48 - ouiData[m * 3 + 1]) - 1)); - - var mac1_hi = ((mac1 / 0x10000) & (mask / 0x10000)) >>> 0; - var mac1_lo = ((mac1 & 0xffff) & (mask & 0xffff)) >>> 0; - - var mac2 = parseInt(ouiData[m * 3], 16); - var mac2_hi = (mac2 / 0x10000) >>> 0; - var mac2_lo = (mac2 & 0xffff) >>> 0; - - if (mac1_hi === mac2_hi && mac1_lo === mac2_lo) - return ouiData[m * 3 + 2]; - - if (mac2_hi > mac1_hi || - (mac2_hi === mac1_hi && mac2_lo > mac1_lo)) - r = m - 1; - else - l = m + 1; - } - - return null; -} - - -function fetchData(period) -{ - XHR.get(L.url('admin/nlbw/data'), { period: period, group_by: 'family,mac,ip,layer7', order_by: '-rx_bytes,-tx_bytes' }, function(xhr, res) { - if (res !== null && typeof(res) === 'object' && typeof(res.columns) === 'object' && typeof(res.data) === 'object') - trafficData = res; - - var addrs = query(null, ['ip'], null); - var ipAddrs = []; - - for (var i = 0; i < addrs.length; i++) - if (ipAddrs.indexOf(addrs[i].ip) < 0) - ipAddrs.push(addrs[i].ip); - - renderHostData(); - renderLayer7Data(); - renderIPv6Data(); - - XHR.get(L.url('admin/nlbw/ptr', ipAddrs.join('/')), null, function(xhr, res) { - if (res !== null && typeof(res) === 'object') - hostNames = res; - }); - }); -} - -function renderPeriods() -{ - var sel = document.getElementById('nlbw.period'); - - for (var e, i = trafficPeriods.length - 1; e = trafficPeriods[i]; i--) { - var ymd1 = e.split(/-/); - var d1 = new Date(+ymd1[0], +ymd1[1] - 1, +ymd1[2]); - var ymd2, d2, pd; - - if (i) { - ymd2 = trafficPeriods[i - 1].split(/-/); - d2 = new Date(+ymd2[0], +ymd2[1] - 1, +ymd2[2]); - d2.setDate(d2.getDate() - 1); - pd = e; - } - else { - d2 = new Date(); - pd = ''; - } - - var opt = document.createElement('option'); - opt.setAttribute('data-duration', (d2.getTime() - d1.getTime()) / 1000); - opt.value = pd; - opt.text = '%04d-%02d-%02d - %04d-%02d-%02d'.format( - d1.getFullYear(), d1.getMonth() + 1, d1.getDate(), - d2.getFullYear(), d2.getMonth() + 1, d2.getDate()); - - sel.appendChild(opt); - } - - sel.selectedIndex = sel.childNodes.length - 1; - sel.style.display = ''; - - sel.onchange = function(ev) { - L.hideTooltip(ev); - fetchData(sel.options[sel.selectedIndex].value); - } -} - -function renderHostDetail(tooltip) -{ - var key = this.getAttribute('href').substr(1), - col = this.getAttribute('data-col'), - label = this.getAttribute('data-tooltip'); - - var detailData = query( - function(c, r) { - return ((r[c.mac] === key || r[c.ip] === key) && - (r[c.rx_bytes] > 0 || r[c.tx_bytes] > 0)); - }, - [col], - function(r1, r2) { - return ((r2.rx_bytes + r2.tx_bytes) - (r1.rx_bytes + r1.tx_bytes)); - } - ); - - var rxData = [], txData = []; - - L.dom.content(tooltip, [ - E('div', { 'class': 'head' }, [ - E('div', { 'class': 'pie' }, [ - E('label', _('Download')), - E('canvas', { 'id': 'bubble-pie1', 'width': 100, 'height': 100 }) - ]), - E('div', { 'class': 'pie' }, [ - E('label', _('Upload')), - E('canvas', { 'id': 'bubble-pie2', 'width': 100, 'height': 100 }) - ]), - E('div', { 'class': 'kpi' }, [ - E('ul', [ - E('li', _('Hostname: <big id="bubble-hostname">example.org</big>')), - E('li', _('Vendor: <big id="bubble-vendor">Example Corp.</big>')) - ]) - ]) - ]), - E('div', { 'class': 'table' }, [ - E('div', { 'class': 'tr table-titles' }, [ - E('div', { 'class': 'th' }, label || col), - E('div', { 'class': 'th' }, _('Conn.')), - E('div', { 'class': 'th' }, _('Down. (Bytes)')), - E('div', { 'class': 'th' }, _('Down. (Pkts.)')), - E('div', { 'class': 'th' }, _('Up. (Bytes)')), - E('div', { 'class': 'th' }, _('Up. (Pkts.)')), - ]) - ]) - ]); - - var rows = []; - - for (var i = 0; i < detailData.length; i++) { - var rec = detailData[i], - cell = E('div', rec[col] || _('other')); - - rows.push([ - cell, - '%1000.2m'.format(rec.conns), - '%1024.2mB'.format(rec.rx_bytes), - '%1000.2mP'.format(rec.rx_pkts), - '%1024.2mB'.format(rec.tx_bytes), - '%1000.2mP'.format(rec.tx_pkts) - ]); - - rxData.push({ - label: ['%s: %%1024.2mB'.format(rec[col] || _('other')), cell], - value: rec.rx_bytes - }); - - txData.push({ - label: ['%s: %%1024.2mB'.format(rec[col] || _('other')), cell], - value: rec.tx_bytes - }); - } - - cbi_update_table(tooltip.lastElementChild, rows); - - pie(tooltip.querySelector('#bubble-pie1'), rxData); - pie(tooltip.querySelector('#bubble-pie2'), txData); - - var mac = key.toUpperCase(); - var name = hostInfo.hasOwnProperty(mac) ? hostInfo[mac].name : null; - - if (!name) - for (var i = 0; i < detailData.length; i++) - if ((name = hostNames[detailData[i].ip]) !== undefined) - break; - - if (mac !== '00:00:00:00:00:00') { - kpi(tooltip.querySelector('#bubble-hostname'), name); - kpi(tooltip.querySelector('#bubble-vendor'), oui(mac)); - } - else { - kpi(tooltip.querySelector('#bubble-hostname')); - kpi(tooltip.querySelector('#bubble-vendor')); - } - - var rect = this.getBoundingClientRect(), x, y; - - if ('ontouchstart' in window || window.innerWidth <= 992) { - var vpHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0), - scrollFrom = window.pageYOffset, - scrollTo = scrollFrom + rect.top - vpHeight * 0.5, - start = null; - - tooltip.style.top = (rect.top + rect.height + window.pageYOffset) + 'px'; - tooltip.style.left = 0; - - var scrollStep = function(timestamp) { - if (!start) - start = timestamp; - - var duration = Math.max(timestamp - start, 1); - if (duration < 100) { - document.body.scrollTop = scrollFrom + (scrollTo - scrollFrom) * (duration / 100); - window.requestAnimationFrame(scrollStep); - } - else { - document.body.scrollTop = scrollTo; - } - }; - - window.requestAnimationFrame(scrollStep); - } - else { - x = rect.left + rect.width + window.pageXOffset, - y = rect.top + window.pageYOffset; - - if ((y + tooltip.offsetHeight) > (window.innerHeight + window.pageYOffset)) - y -= ((y + tooltip.offsetHeight) - (window.innerHeight + window.pageYOffset)); - - tooltip.style.top = y + 'px'; - tooltip.style.left = x + 'px'; - } - - return false; -} - -function formatHostname(dns) -{ - if (dns === undefined || dns === null || dns === '') - return '-'; - - dns = dns.split('.')[0]; - - if (dns.length > 12) - return '<span title="%q">%h…</span>'.format(dns, dns.substr(0, 12)); - - return '%h'.format(dns); -} - -function renderHostData() -{ - var trafData = [], connData = []; - var rx_total = 0, tx_total = 0, conn_total = 0; - - var hostData = query( - function(c, r) { - return (r[c.rx_bytes] > 0 || r[c.tx_bytes] > 0); - }, - ['mac'], - //function(c, r) { - // return (r[c.mac] !== '00:00:00:00:00:00') ? r[c.mac] : r[c.ip]; - //}, - function(r1, r2) { - return ((r2.rx_bytes + r2.tx_bytes) - (r1.rx_bytes + r1.tx_bytes)); - } - ); - - var rows = []; - - for (var i = 0; i < hostData.length; i++) { - var rec = hostData[i], - mac = rec.mac.toUpperCase(), - key = (mac !== '00:00:00:00:00:00') ? mac : rec.ip, - dns = hostInfo[mac] ? hostInfo[mac].name : null; - - var cell = E('div', formatHostname(dns)); - - rows.push([ - cell, - E('a', { - 'href': '#' + rec.mac, - 'data-col': 'ip', - 'data-tooltip': _('Source IP') - }, (mac !== '00:00:00:00:00:00') ? mac : _('other')), - E('a', { - 'href': '#' + rec.mac, - 'data-col': 'layer7', - 'data-tooltip': _('Protocol') - }, '%1000.2m'.format(rec.conns)), - '%1024.2mB'.format(rec.rx_bytes), - '%1000.2mP'.format(rec.rx_pkts), - '%1024.2mB'.format(rec.tx_bytes), - '%1000.2mP'.format(rec.tx_pkts) - ]); - - trafData.push({ - value: rec.rx_bytes + rec.tx_bytes, - label: ["%s: %%.2mB".format(key), cell] - }); - - connData.push({ - value: rec.conns, - label: ["%s: %%.2m".format(key), cell] - }); - - rx_total += rec.rx_bytes; - tx_total += rec.tx_bytes; - conn_total += rec.conns; - } - - cbi_update_table('#host-data', rows, E('em', [ - _('No data recorded yet.'), ' ', - E('a', { 'href': L.url('admin/nlbw/commit') }, _('Force reload…')) - ])); - - pie('traf-pie', trafData); - pie('conn-pie', connData); - - kpi('rx-total', '%1024.2mB'.format(rx_total)); - kpi('tx-total', '%1024.2mB'.format(tx_total)); - kpi('conn-total', '%1000m'.format(conn_total)); - kpi('host-total', '%u'.format(hostData.length)); -} - -function renderLayer7Data() -{ - var rxData = [], txData = []; - var topConn = [[0],[0],[0]], topRx = [[0],[0],[0]], topTx = [[0],[0],[0]]; - - var layer7Data = query( - null, ['layer7'], - function(r1, r2) { - return ((r2.rx_bytes + r2.tx_bytes) - (r1.rx_bytes + r1.tx_bytes)); - } - ); - - var rows = []; - - for (var i = 0, c = 0; i < layer7Data.length; i++) { - var rec = layer7Data[i], - cell = E('div', rec.layer7 || _('other')); - - rows.push([ - cell, - '%1000m'.format(rec.conns), - '%1024.2mB'.format(rec.rx_bytes), - '%1000.2mP'.format(rec.rx_pkts), - '%1024.2mB'.format(rec.tx_bytes), - '%1000.2mP'.format(rec.tx_pkts) - ]); - - rxData.push({ - value: rec.rx_bytes, - label: ["%s: %%.2mB".format(rec.layer7 || _('other')), cell] - }); - - txData.push({ - value: rec.tx_bytes, - label: ["%s: %%.2mB".format(rec.layer7 || _('other')), cell] - }); - - if (rec.layer7) { - topRx.push([rec.rx_bytes, rec.layer7]); - topTx.push([rec.tx_bytes, rec.layer7]); - topConn.push([rec.conns, rec.layer7]); - } - } - - cbi_update_table('#layer7-data', rows, E('em', [ - _('No data recorded yet.'), ' ', - E('a', { 'href': L.url('admin/nlbw/commit') }, _('Force reload…')) - ])); - - pie('layer7-rx-pie', rxData); - pie('layer7-tx-pie', txData); - - topRx.sort(function(a, b) { return b[0] - a[0] }); - topTx.sort(function(a, b) { return b[0] - a[0] }); - topConn.sort(function(a, b) { return b[0] - a[0] }); - - kpi('layer7-total', layer7Data.length); - kpi('layer7-most-rx', topRx[0][1], topRx[1][1], topRx[2][1]); - kpi('layer7-most-tx', topTx[0][1], topTx[1][1], topTx[2][1]); - kpi('layer7-most-conn', topConn[0][1], topConn[1][1], topConn[2][1]); -} - -function renderIPv6Data() -{ - var col = { }, - rx4_total = 0, - tx4_total = 0, - rx6_total = 0, - tx6_total = 0, - v4_total = 0, - v6_total = 0, - ds_total = 0, - families = { }, - records = { }; - - ipv6Data = query( - null, ['family', 'mac'], - function(r1, r2) { - return ((r2.rx_bytes + r2.tx_bytes) - (r1.rx_bytes + r1.tx_bytes)); - } - ); - - for (var i = 0, c = 0; i < ipv6Data.length; i++) { - var rec = ipv6Data[i], - mac = rec.mac.toUpperCase(), - ip = rec.ip, - fam = families[mac] || 0, - recs = records[mac] || {}; - - if (rec.family == 4) { - rx4_total += rec.rx_bytes; - tx4_total += rec.tx_bytes; - fam |= 1; - } - else { - rx6_total += rec.rx_bytes; - tx6_total += rec.tx_bytes; - fam |= 2; - } - - recs[rec.family] = rec; - records[mac] = recs; - - families[mac] = fam; - } - - for (var mac in families) { - switch (families[mac]) - { - case 3: - ds_total++; - break; - - case 2: - v6_total++; - break; - - case 1: - v4_total++; - break; - } - } - - var rows = []; - - for (var mac in records) { - if (mac === '00:00:00:00:00:00') - continue; - - var dns = hostInfo[mac] ? hostInfo[mac].name : null, - rec4 = records[mac][4], - rec6 = records[mac][6]; - - rows.push([ - formatHostname(dns), - mac, - [ E('span', _('IPv4')), - E('span', _('IPv6')) ], - [ E('span', rec4 ? '%1024.2mB'.format(rec4.rx_bytes) : '-'), - E('span', rec6 ? '%1024.2mB'.format(rec6.rx_bytes) : '-') ], - [ E('span', rec4 ? '%1000.2mP'.format(rec4.rx_pkts) : '-'), - E('span', rec6 ? '%1000.2mP'.format(rec6.rx_pkts) : '-') ], - [ E('span', rec4 ? '%1024.2mB'.format(rec4.tx_bytes) : '-'), - E('span', rec6 ? '%1024.2mB'.format(rec6.tx_bytes) : '-') ], - [ E('span', rec4 ? '%1000.2mP'.format(rec4.tx_pkts) : '-'), - E('span', rec6 ? '%1000.2mP'.format(rec6.tx_pkts) : '-') ] - ]); - } - - cbi_update_table('#ipv6-data', rows, E('em', [ - _('No data recorded yet.'), ' ', - E('a', { 'href': L.url('admin/nlbw/commit') }, _('Force reload…')) - ])); - - var shareData = [], hostsData = []; - - if (rx4_total > 0 || tx4_total > 0) - shareData.push({ - value: rx4_total + tx4_total, - label: ["IPv4: %.2mB"], - color: 'hsl(140, 100%, 50%)' - }); - - if (rx6_total > 0 || tx6_total > 0) - shareData.push({ - value: rx6_total + tx6_total, - label: ["IPv6: %.2mB"], - color: 'hsl(180, 100%, 50%)' - }); - - if (v4_total > 0) - hostsData.push({ - value: v4_total, - label: [_('%d IPv4-only hosts')], - color: 'hsl(140, 100%, 50%)' - }); - - if (v6_total > 0) - hostsData.push({ - value: v6_total, - label: [_('%d IPv6-only hosts')], - color: 'hsl(180, 100%, 50%)' - }); - - if (ds_total > 0) - hostsData.push({ - value: ds_total, - label: [_('%d dual-stack hosts')], - color: 'hsl(50, 100%, 50%)' - }); - - pie('ipv6-share-pie', shareData); - pie('ipv6-hosts-pie', hostsData); - - kpi('ipv6-hosts', '%.2f%%'.format(100 / (ds_total + v4_total + v6_total) * (ds_total + v6_total))); - kpi('ipv6-share', '%.2f%%'.format(100 / (rx4_total + rx6_total + tx4_total + tx6_total) * (rx6_total + tx6_total))); - kpi('ipv6-rx', '%1024.2mB'.format(rx6_total)); - kpi('ipv6-tx', '%1024.2mB'.format(tx6_total)); -} diff --git a/applications/luci-app-nlbwmon/htdocs/luci-static/resources/view/nlbw/backup.js b/applications/luci-app-nlbwmon/htdocs/luci-static/resources/view/nlbw/backup.js new file mode 100644 index 000000000..c5fcfe5cf --- /dev/null +++ b/applications/luci-app-nlbwmon/htdocs/luci-static/resources/view/nlbw/backup.js @@ -0,0 +1,70 @@ +'use strict'; +'require view'; +'require ui'; +'require fs'; + +return view.extend({ + load: function() { + return fs.trimmed('/proc/sys/kernel/hostname'); + }, + + handleArchiveUpload: function(ev) { + return ui.uploadFile('/tmp/nlbw-restore.tar.gz').then(function() { + return fs.exec('/usr/libexec/nlbwmon-action', [ 'restore' ]).then(function(res) { + if (res.code != 0) + throw new Error(res.stderr || res.stdout); + + var json = JSON.parse(res.stdout || '{}'), + list = (L.isObject(json) && Array.isArray(json.restored)) ? json.restored : []; + + ui.showModal(_('Restore complete'), [ + E('p', [ _('The following database files have been restored:') ]), + E('ul', list.map(function(file) { return E('li', [ file ]) })), + E('div', { 'class': 'right' }, [ + E('button', { 'click': ui.hideModal }, [ _('Dismiss') ]) + ]) + ]); + }).catch(function(err) { + ui.addNotification(null, E('p', [ _('Failed to restore backup archive: %s').format(err.message) ])); + }); + }); + }, + + handleArchiveDownload: function(hostname, ev) { + return fs.exec_direct('/usr/libexec/nlbwmon-action', [ 'backup' ], 'blob').then(function(blob) { + var url = window.URL.createObjectURL(blob), + date = new Date(), + name = 'nlbwmon-backup-%s-%04d-%02d-%02d.tar.gz'.format(hostname, date.getFullYear(), date.getMonth() + 1, date.getDate()), + link = E('a', { 'style': 'display:none', 'href': url, 'download': name }); + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + }).catch(function(err) { + ui.addNotification(null, E('p', [ _('Failed to download backup archive: %s').format(err.message) ])); + }); + }, + + render: function(hostname) { + return E([], [ + E('h2', [ _('Netlink Bandwidth Monitor - Backup / Restore') ]), + E('h5', [ _('Restore Database Backup') ]), + E('p', [ + E('button', { + 'click': ui.createHandlerFn(this, 'handleArchiveUpload') + }, [ _('Restore') ]) + ]), + E('h5', [ _('Download Database Backup') ]), + E('p', [ + E('button', { + 'click': ui.createHandlerFn(this, 'handleArchiveDownload', hostname) + }, [ _('Generate Backup') ]) + ]) + ]); + }, + + handleSave: null, + handleSaveApply: null, + handleReset: null +}); diff --git a/applications/luci-app-nlbwmon/htdocs/luci-static/resources/view/nlbw/config.js b/applications/luci-app-nlbwmon/htdocs/luci-static/resources/view/nlbw/config.js new file mode 100644 index 000000000..cb13eecce --- /dev/null +++ b/applications/luci-app-nlbwmon/htdocs/luci-static/resources/view/nlbw/config.js @@ -0,0 +1,180 @@ +'use strict'; +'require view'; +'require form'; +'require uci'; +'require fs'; +'require validation'; +'require tools.widgets as widgets'; + +function writePeriod(section_id, value) { + var interval = this.map.lookupOption('_interval', section_id)[0], + period = this.map.lookupOption('_period', section_id)[0], + date = this.map.lookupOption('_date', section_id)[0], + days = this.map.lookupOption('_days', section_id)[0]; + + if (period.formvalue(section_id) == 'relative') { + uci.set('nlbwmon', section_id, 'database_interval', interval.formvalue(section_id)); + } + else { + uci.set('nlbwmon', section_id, 'database_interval', '%s/%s'.format( + date.formvalue(section_id), + days.formvalue(section_id) + )); + } +} + +function writeNetworks(section_id, value) { + var oldval = L.toArray(uci.get('nlbwmon', section_id, 'local_network')), + subnets = this.map.lookupOption('_subnets', section_id)[0], + ifaces = this.map.lookupOption('_ifaces', section_id)[0]; + + var newval = [].concat( + L.toArray(subnets.formvalue(section_id)), + L.toArray(ifaces.formvalue(section_id)) + ); + + if (oldval.length != newval.length || oldval.join(' ') != newval.join(' ')) + uci.set('nlbwmon', section_id, 'local_network', newval); +} + +function writeProtocols(section_id, value) { + return fs.write('/usr/share/nlbwmon/protocols', (value || '').trim().replace(/\r\n/g, '\n') + '\n'); +} + +return view.extend({ + load: function() { + return uci.load('nlbwmon'); + }, + + render: function() { + var m, s, o; + + m = new form.Map('nlbwmon', _('Netlink Bandwidth Monitor - Configuration'), + _('The Netlink Bandwidth Monitor (nlbwmon) is a lightweight, efficient traffic accounting program keeping track of bandwidth usage per host and protocol.')); + + s = m.section(form.TypedSection, 'nlbwmon'); + s.anonymous = true; + s.addremove = false; + + s.tab('general', _('General Settings')); + s.tab('advanced', _('Advanced Settings')); + s.tab('protocol', _('Protocol Mapping'), _('Protocol mappings to distinguish traffic types per host, one mapping per line. The first value specifies the IP protocol, the second value the port number and the third column is the name of the mapped protocol.')); + + o = s.taboption('general', form.ListValue, '_period', _('Accounting period'), + _('Choose "Day of month" to restart the accounting period monthly on a specific date, e.g. every 3rd. Choose "Fixed interval" to restart the accounting period exactly every N days, beginning at a given date.')); + o.cfgvalue = function(section_id) { + var value = uci.get('nlbwmon', section_id, 'database_interval'), + m = /^[0-9]{4}-[0-9]{2}-[0-9]{2}\/[0-9]+$/.test(value); + + return m ? 'absolute' : 'relative'; + }; + o.write = writePeriod; + o.value('relative', _('Day of month')); + o.value('absolute', _('Fixed interval')); + + o = s.taboption('general', form.DummyValue, '_warning', _('Warning')); + o.default = _('Changing the accounting interval type will invalidate existing databases!<br /><strong><a href="%s">Download backup</a></strong>.').format(L.url('admin/nlbw/backup')); + o.rawhtml = true; + if (/^[0-9]{4}-[0-9]{2}-[0-9]{2}\/[0-9]+$/.test(uci.get_first('nlbwmon', 'nlbwmon', 'database_interval'))) + o.depends('_period', 'relative'); + else + o.depends('_period', 'absolute'); + + o = s.taboption('general', form.Value, '_interval', _('Due date'), + _('Day of month to restart the accounting period. Use negative values to count towards the end of month, e.g. "-5" to specify the 27th of July or the 24th of February.')); + o.rmempty = false; + o.cfgvalue = function(section_id) { + var value = +uci.get('nlbwmon', section_id, 'database_interval'); + return !isNaN(value) ? value.toString() : null; + }; + o.write = writePeriod; + o.depends('_period', 'relative'); + o.value('1', _('1 - Restart every 1st of month')); + o.value('-1', _('-1 - Restart every last day of month')); + o.value('-7', _('-7 - Restart a week before end of month')); + + o = s.taboption('general', form.Value, '_date', _('Start date'), + _('Start date of the first accounting period, e.g. begin of ISP contract.')); + o.rmempty = false; + o.cfgvalue = function(section_id) { + var value = uci.get('nlbwmon', section_id, 'database_interval'), + m = /^([0-9]{4}-[0-9]{2}-[0-9]{2})\/[0-9]+$/.exec(value); + + return m ? m[1] : null; + }; + o.write = writePeriod; + o.depends('_period', 'absolute'); + + o = s.taboption('general', form.Value, '_days', _('Interval'), + _('Length of accounting interval in days.')); + o.rmempty = false; + o.cfgvalue = function(section_id) { + var value = uci.get('nlbwmon', section_id, 'database_interval'), + m = /^[0-9]{4}-[0-9]{2}-[0-9]{2}\/([0-9]+)$/.exec(value); + + return m ? m[1] : null; + }; + o.write = writePeriod; + o.depends('_period', 'absolute'); + + o = s.taboption('general', widgets.NetworkSelect, '_ifaces', _('Local interfaces'), + _('Only conntrack streams from or to any of these networks are counted.')); + o.nocreate = true; + o.multiple = true; + o.cfgvalue = function(section_id) { + return L.toArray(uci.get('nlbwmon', section_id, 'local_network')); + }; + o.write = writeNetworks; + + o = s.taboption('general', form.DynamicList, '_subnets', _('Local subnets'), + _('Only conntrack streams from or to any of these subnets are counted.')); + o.cfgvalue = function(section_id) { + return L.toArray(uci.get('nlbwmon', section_id, 'local_network')).filter(function(addr) { + var m = /^([0-9a-fA-F:.]+)(?:\/[0-9a-fA-F:.]+)?$/.exec(addr); + return m && (validation.parseIPv4(m[1]) || validation.parseIPv6(m[1])); + }); + }; + o.write = writeNetworks; + o.datatype = 'ipaddr'; + + + o = s.taboption('advanced', form.Value, 'database_limit', _('Maximum entries'), + _('The maximum amount of entries that should be put into the database, setting the limit to 0 will allow databases to grow indefinitely.')); + + o = s.taboption('advanced', form.Flag, 'database_prealloc', _('Preallocate database'), + _('Whether to preallocate the maximum possible database size in memory. This is mainly useful for memory constrained systems which might not be able to satisfy memory allocation after longer uptime periods.')); + o.depends({ 'database_limit': '0', '!reverse': 'true' }); + + o = s.taboption('advanced', form.Flag, 'database_compress', _('Compress database'), + _('Whether to gzip compress archive databases. Compressing the database files makes accessing old data slightly slower but helps to reduce storage requirements.')); + + o = s.taboption('advanced', form.Value, 'database_generations', _('Stored periods'), + _('Maximum number of accounting periods to keep, use zero to keep databases forever.')); + + o = s.taboption('advanced', form.Value, 'commit_interval', _('Commit interval'), + _('Interval at which the temporary in-memory database is committed to the persistent database directory.')); + o.value('24h', _('24h - least flash wear at the expense of data loss risk')); + o.value('12h', _('12h - compromise between risk of data loss and flash wear')); + o.value('10m', _('10m - frequent commits at the expense of flash wear')); + o.value('60s', _('60s - commit minutely, useful for non-flash storage')); + + o = s.taboption('advanced', form.Value, 'refresh_interval', _('Refresh interval'), + _('Interval at which traffic counters of still established connections are refreshed from netlink information.')); + o.value('30s', _('30s - refresh twice per minute for reasonably current stats')); + o.value('5m', _('5m - rarely refresh to avoid frequently clearing conntrack counters')); + + o = s.taboption('advanced', form.Value, 'database_directory', _('Database directory'), + _('Database storage directory. One file per accounting period will be placed into this directory.')); + + + o = s.taboption('protocol', form.TextValue, '_protocols'); + o.rows = 50; + o.load = function(section_id) { + return fs.trimmed('/usr/share/nlbwmon/protocols'); + }; + o.write = writeProtocols; + o.remove = writeProtocols; + + return m.render(); + } +}); diff --git a/applications/luci-app-nlbwmon/htdocs/luci-static/resources/view/nlbw/display.js b/applications/luci-app-nlbwmon/htdocs/luci-static/resources/view/nlbw/display.js new file mode 100644 index 000000000..1bf668782 --- /dev/null +++ b/applications/luci-app-nlbwmon/htdocs/luci-static/resources/view/nlbw/display.js @@ -0,0 +1,980 @@ +'use strict'; +'require view'; +'require network'; +'require request'; +'require fs'; +'require ui'; +'require rpc'; +'require dom'; + +var callNetworkRrdnsLookup = rpc.declare({ + object: 'network.rrdns', + method: 'lookup', + params: [ 'addrs', 'timeout', 'limit' ], + expect: { '': {} } +}); + +var chartRegistry = {}, + trafficPeriods = [], + trafficData = { columns: [], data: [] }, + hostNames = {}, + hostInfo = {}, + ouiData = []; + +return view.extend({ + load: function() { + return Promise.all([ + this.loadHosts(), + this.loadPeriods(), + this.loadData(), + this.loadOUI() + ]); + }, + + loadHosts: function() { + return L.resolveDefault(network.getHostHints()).then(function(res) { + if (res) { + var hints = res.getMACHints(); + + for (var i = 0; i < hints.length; i++) { + hostInfo[hints[i][0]] = { + name: res.getHostnameByMACAddr(hints[i][0]), + ipv6: res.getIP6AddrByMACAddr(hints[i][0]), + ipv4: res.getIPAddrByMACAddr(hints[i][0]) + }; + } + } + }); + }, + + loadOUI: function() { + var url = 'https://raw.githubusercontent.com/jow-/oui-database/master/oui.json'; + + return L.resolveDefault(request.get(url, { cache: true }), []).then(function(res) { + res = res.json(); + + if (Array.isArray(res)) + ouiData = res; + }); + }, + + loadPeriods: function() { + return L.resolveDefault(fs.exec_direct('/usr/libexec/nlbwmon-action', [ 'periods' ], 'json')).then(function(res) { + if (L.isObject(res) && Array.isArray(res.periods)) + trafficPeriods = res.periods; + }); + }, + + loadData: function(period) { + var args = [ 'download', '-g', 'family,mac,ip,layer7', '-o', '-rx_bytes,-tx_bytes' ]; + + if (period) + args.push('-t', period); + + return fs.exec_direct('/usr/libexec/nlbwmon-action', args, 'json').then(L.bind(function(res) { + if (!L.isObject(res) || !Array.isArray(res.columns) || !Array.isArray(res.data)) + throw new Error(_('Malformed data received')); + + trafficData = res; + + var addrs = this.query(null, [ 'ip' ], null), + ipAddrs = []; + + for (var i = 0; i < addrs.length; i++) + if (ipAddrs.indexOf(addrs[i].ip) < 0) + ipAddrs.push(addrs[i].ip); + + if (ipAddrs.length) + return L.resolveDefault(callNetworkRrdnsLookup(ipAddrs, 1000, 1000), {}).then(function(res) { + hostNames = res; + }); + }, this)).catch(function(err) { + ui.addNotification(null, _('Unable to fetch traffic statistic data: %s').format(err.message)); + }); + }, + + off: function(elem) { + var val = [0, 0]; + do { + if (!isNaN(elem.offsetLeft) && !isNaN(elem.offsetTop)) { + val[0] += elem.offsetLeft; + val[1] += elem.offsetTop; + } + } + while ((elem = elem.offsetParent) != null); + return val; + }, + + kpi: function(id, val1, val2, val3) { + var e = L.dom.elem(id) ? id : document.getElementById(id); + + if (val1 && val2 && val3) + e.innerHTML = _('%s, %s and %s').format(val1, val2, val3); + else if (val1 && val2) + e.innerHTML = _('%s and %s').format(val1, val2); + else if (val1) + e.innerHTML = val1; + + e.parentNode.style.display = val1 ? 'list-item' : ''; + }, + + pie: function(id, data) { + var total = data.reduce(function(n, d) { return n + d.value }, 0); + + data.sort(function(a, b) { return b.value - a.value }); + + if (total === 0) + data = [{ + value: 1, + color: '#cccccc', + label: [ _('no traffic') ] + }]; + + for (var i = 0; i < data.length; i++) { + if (!data[i].color) { + var hue = 120 / (data.length-1) * i; + data[i].color = 'hsl(%u, 80%%, 50%%)'.format(hue); + data[i].label.push(hue); + } + } + + var node = L.dom.elem(id) ? id : document.getElementById(id), + key = L.dom.elem(id) ? id.id : id, + ctx = node.getContext('2d'); + + if (chartRegistry.hasOwnProperty(key)) + chartRegistry[key].destroy(); + + chartRegistry[key] = new Chart(ctx).Doughnut(data, { + segmentStrokeWidth: 1, + percentageInnerCutout: 30 + }); + + return chartRegistry[key]; + }, + + oui: function(mac) { + var m, l = 0, r = ouiData.length / 3 - 1; + var mac1 = parseInt(mac.replace(/[^a-fA-F0-9]/g, ''), 16); + + while (l <= r) { + m = l + Math.floor((r - l) / 2); + + var mask = (0xffffffffffff - + (Math.pow(2, 48 - ouiData[m * 3 + 1]) - 1)); + + var mac1_hi = ((mac1 / 0x10000) & (mask / 0x10000)) >>> 0; + var mac1_lo = ((mac1 & 0xffff) & (mask & 0xffff)) >>> 0; + + var mac2 = parseInt(ouiData[m * 3], 16); + var mac2_hi = (mac2 / 0x10000) >>> 0; + var mac2_lo = (mac2 & 0xffff) >>> 0; + + if (mac1_hi === mac2_hi && mac1_lo === mac2_lo) + return ouiData[m * 3 + 2]; + + if (mac2_hi > mac1_hi || + (mac2_hi === mac1_hi && mac2_lo > mac1_lo)) + r = m - 1; + else + l = m + 1; + } + + return null; + }, + + query: function(filter, group, order) { + var keys = [], columns = {}, records = {}, result = []; + + if (typeof(group) !== 'function' && typeof(group) !== 'object') + group = ['mac']; + + for (var i = 0; i < trafficData.columns.length; i++) + columns[trafficData.columns[i]] = i; + + for (var i = 0; i < trafficData.data.length; i++) { + var record = trafficData.data[i]; + + if (typeof(filter) === 'function' && filter(columns, record) !== true) + continue; + + var key; + + if (typeof(group) === 'function') { + key = group(columns, record); + } + else { + key = []; + + for (var j = 0; j < group.length; j++) + if (columns.hasOwnProperty(group[j])) + key.push(record[columns[group[j]]]); + + key = key.join(','); + } + + if (!records.hasOwnProperty(key)) { + var rec = {}; + + for (var col in columns) + rec[col] = record[columns[col]]; + + records[key] = rec; + result.push(rec); + } + else { + records[key].conns += record[columns.conns]; + records[key].rx_bytes += record[columns.rx_bytes]; + records[key].rx_pkts += record[columns.rx_pkts]; + records[key].tx_bytes += record[columns.tx_bytes]; + records[key].tx_pkts += record[columns.tx_pkts]; + } + } + + if (typeof(order) === 'function') + result.sort(order); + + return result; + }, + + renderPeriods: function() { + if (!trafficPeriods.length) + return E([]); + + var choices = {}, + keys = []; + + for (var e, i = trafficPeriods.length - 1; e = trafficPeriods[i]; i--) { + var ymd1 = e.split(/-/); + var d1 = new Date(+ymd1[0], +ymd1[1] - 1, +ymd1[2]); + var ymd2, d2, pd; + + if (i) { + ymd2 = trafficPeriods[i - 1].split(/-/); + d2 = new Date(+ymd2[0], +ymd2[1] - 1, +ymd2[2]); + d2.setDate(d2.getDate() - 1); + pd = e; + } + else { + d2 = new Date(); + pd = '-'; + } + + keys.push(pd); + choices[pd] = '%04d-%02d-%02d - %04d-%02d-%02d'.format( + d1.getFullYear(), d1.getMonth() + 1, d1.getDate(), + d2.getFullYear(), d2.getMonth() + 1, d2.getDate() + ); + } + + var dropdown = new ui.Dropdown('-', choices, { sort: keys, optional: false }).render(); + + dropdown.addEventListener('cbi-dropdown-change', ui.createHandlerFn(this, function(ev) { + ui.hideTooltip(ev); + + var period = ev.detail.value.value != '-' ? ev.detail.value.value : null; + + return this.loadData(period).then(L.bind(function() { + this.renderHostData(); + this.renderLayer7Data(); + this.renderIPv6Data(); + }, this)); + })); + + return E([], [ + E('p', [ _('Select accounting period:'), ' ', dropdown ]), + E('hr') + ]); + }, + + formatHostname: function(dns) { + if (dns === undefined || dns === null || dns === '') + return '-'; + + dns = dns.split('.')[0]; + + if (dns.length > 12) + return '<span title="%q">%h…</span>'.format(dns, dns.substr(0, 12)); + + return '%h'.format(dns); + }, + + renderHostData: function() { + var trafData = [], connData = []; + var rx_total = 0, tx_total = 0, conn_total = 0; + + var hostData = this.query( + function(c, r) { + return (r[c.rx_bytes] > 0 || r[c.tx_bytes] > 0); + }, + ['mac'], + //function(c, r) { + // return (r[c.mac] !== '00:00:00:00:00:00') ? r[c.mac] : r[c.ip]; + //}, + function(r1, r2) { + return ((r2.rx_bytes + r2.tx_bytes) - (r1.rx_bytes + r1.tx_bytes)); + } + ); + + var rows = []; + + for (var i = 0; i < hostData.length; i++) { + var rec = hostData[i], + mac = rec.mac.toUpperCase(), + key = (mac !== '00:00:00:00:00:00') ? mac : rec.ip, + dns = hostInfo[mac] ? hostInfo[mac].name : null; + + var cell = E('div', this.formatHostname(dns)); + + rows.push([ + cell, + E('a', { + 'href': '#' + rec.mac, + 'data-col': 'ip', + 'data-tooltip': _('Source IP') + }, (mac !== '00:00:00:00:00:00') ? mac : _('other')), + E('a', { + 'href': '#' + rec.mac, + 'data-col': 'layer7', + 'data-tooltip': _('Protocol') + }, '%1000.2m'.format(rec.conns)), + '%1024.2mB'.format(rec.rx_bytes), + '%1000.2mP'.format(rec.rx_pkts), + '%1024.2mB'.format(rec.tx_bytes), + '%1000.2mP'.format(rec.tx_pkts) + ]); + + trafData.push({ + value: rec.rx_bytes + rec.tx_bytes, + label: ["%s: %%.2mB".format(key), cell] + }); + + connData.push({ + value: rec.conns, + label: ["%s: %%.2m".format(key), cell] + }); + + rx_total += rec.rx_bytes; + tx_total += rec.tx_bytes; + conn_total += rec.conns; + } + + cbi_update_table('#host-data', rows, E('em', [ + _('No data recorded yet.'), ' ', + E('a', { + 'href': '#', + 'click': ui.createHandlerFn(this, 'handleCommit') + }, _('Force reload…')) + ])); + + this.pie('traf-pie', trafData); + this.pie('conn-pie', connData); + + this.kpi('rx-total', '%1024.2mB'.format(rx_total)); + this.kpi('tx-total', '%1024.2mB'.format(tx_total)); + this.kpi('conn-total', '%1000m'.format(conn_total)); + this.kpi('host-total', '%u'.format(hostData.length)); + }, + + renderLayer7Data: function() { + var rxData = [], txData = []; + var topConn = [[0],[0],[0]], topRx = [[0],[0],[0]], topTx = [[0],[0],[0]]; + + var layer7Data = this.query( + null, ['layer7'], + function(r1, r2) { + return ((r2.rx_bytes + r2.tx_bytes) - (r1.rx_bytes + r1.tx_bytes)); + } + ); + + var rows = []; + + for (var i = 0, c = 0; i < layer7Data.length; i++) { + var rec = layer7Data[i], + cell = E('div', rec.layer7 || _('other')); + + rows.push([ + cell, + '%1000m'.format(rec.conns), + '%1024.2mB'.format(rec.rx_bytes), + '%1000.2mP'.format(rec.rx_pkts), + '%1024.2mB'.format(rec.tx_bytes), + '%1000.2mP'.format(rec.tx_pkts) + ]); + + rxData.push({ + value: rec.rx_bytes, + label: ["%s: %%.2mB".format(rec.layer7 || _('other')), cell] + }); + + txData.push({ + value: rec.tx_bytes, + label: ["%s: %%.2mB".format(rec.layer7 || _('other')), cell] + }); + + if (rec.layer7) { + topRx.push([rec.rx_bytes, rec.layer7]); + topTx.push([rec.tx_bytes, rec.layer7]); + topConn.push([rec.conns, rec.layer7]); + } + } + + cbi_update_table('#layer7-data', rows, E('em', [ + _('No data recorded yet.'), ' ', + E('a', { 'href': L.url('admin/nlbw/commit') }, _('Force reload…')) + ])); + + this.pie('layer7-rx-pie', rxData); + this.pie('layer7-tx-pie', txData); + + topRx.sort(function(a, b) { return b[0] - a[0] }); + topTx.sort(function(a, b) { return b[0] - a[0] }); + topConn.sort(function(a, b) { return b[0] - a[0] }); + + this.kpi('layer7-total', layer7Data.length); + this.kpi('layer7-most-rx', topRx[0][1], topRx[1][1], topRx[2][1]); + this.kpi('layer7-most-tx', topTx[0][1], topTx[1][1], topTx[2][1]); + this.kpi('layer7-most-conn', topConn[0][1], topConn[1][1], topConn[2][1]); + }, + + renderIPv6Data: function() { + var col = { }, + rx4_total = 0, + tx4_total = 0, + rx6_total = 0, + tx6_total = 0, + v4_total = 0, + v6_total = 0, + ds_total = 0, + families = { }, + records = { }; + + var ipv6Data = this.query( + null, ['family', 'mac'], + function(r1, r2) { + return ((r2.rx_bytes + r2.tx_bytes) - (r1.rx_bytes + r1.tx_bytes)); + } + ); + + for (var i = 0, c = 0; i < ipv6Data.length; i++) { + var rec = ipv6Data[i], + mac = rec.mac.toUpperCase(), + ip = rec.ip, + fam = families[mac] || 0, + recs = records[mac] || {}; + + if (rec.family == 4) { + rx4_total += rec.rx_bytes; + tx4_total += rec.tx_bytes; + fam |= 1; + } + else { + rx6_total += rec.rx_bytes; + tx6_total += rec.tx_bytes; + fam |= 2; + } + + recs[rec.family] = rec; + records[mac] = recs; + + families[mac] = fam; + } + + for (var mac in families) { + switch (families[mac]) + { + case 3: + ds_total++; + break; + + case 2: + v6_total++; + break; + + case 1: + v4_total++; + break; + } + } + + var rows = []; + + for (var mac in records) { + if (mac === '00:00:00:00:00:00') + continue; + + var dns = hostInfo[mac] ? hostInfo[mac].name : null, + rec4 = records[mac][4], + rec6 = records[mac][6]; + + rows.push([ + this.formatHostname(dns), + mac, + [ E('span', _('IPv4')), + E('span', _('IPv6')) ], + [ E('span', rec4 ? '%1024.2mB'.format(rec4.rx_bytes) : '-'), + E('span', rec6 ? '%1024.2mB'.format(rec6.rx_bytes) : '-') ], + [ E('span', rec4 ? '%1000.2mP'.format(rec4.rx_pkts) : '-'), + E('span', rec6 ? '%1000.2mP'.format(rec6.rx_pkts) : '-') ], + [ E('span', rec4 ? '%1024.2mB'.format(rec4.tx_bytes) : '-'), + E('span', rec6 ? '%1024.2mB'.format(rec6.tx_bytes) : '-') ], + [ E('span', rec4 ? '%1000.2mP'.format(rec4.tx_pkts) : '-'), + E('span', rec6 ? '%1000.2mP'.format(rec6.tx_pkts) : '-') ] + ]); + } + + cbi_update_table('#ipv6-data', rows, E('em', [ + _('No data recorded yet.'), ' ', + E('a', { 'href': L.url('admin/nlbw/commit') }, _('Force reload…')) + ])); + + var shareData = [], hostsData = []; + + if (rx4_total > 0 || tx4_total > 0) + shareData.push({ + value: rx4_total + tx4_total, + label: ["IPv4: %.2mB"], + color: 'hsl(140, 100%, 50%)' + }); + + if (rx6_total > 0 || tx6_total > 0) + shareData.push({ + value: rx6_total + tx6_total, + label: ["IPv6: %.2mB"], + color: 'hsl(180, 100%, 50%)' + }); + + if (v4_total > 0) + hostsData.push({ + value: v4_total, + label: [_('%d IPv4-only hosts')], + color: 'hsl(140, 100%, 50%)' + }); + + if (v6_total > 0) + hostsData.push({ + value: v6_total, + label: [_('%d IPv6-only hosts')], + color: 'hsl(180, 100%, 50%)' + }); + + if (ds_total > 0) + hostsData.push({ + value: ds_total, + label: [_('%d dual-stack hosts')], + color: 'hsl(50, 100%, 50%)' + }); + + this.pie('ipv6-share-pie', shareData); + this.pie('ipv6-hosts-pie', hostsData); + + this.kpi('ipv6-hosts', '%.2f%%'.format(100 / (ds_total + v4_total + v6_total) * (ds_total + v6_total))); + this.kpi('ipv6-share', '%.2f%%'.format(100 / (rx4_total + rx6_total + tx4_total + tx6_total) * (rx6_total + tx6_total))); + this.kpi('ipv6-rx', '%1024.2mB'.format(rx6_total)); + this.kpi('ipv6-tx', '%1024.2mB'.format(tx6_total)); + }, + + renderHostDetail: function(node, tooltip) { + var key = node.getAttribute('href').substr(1), + col = node.getAttribute('data-col'), + label = node.getAttribute('data-tooltip'); + + var detailData = this.query( + function(c, r) { + return ((r[c.mac] === key || r[c.ip] === key) && + (r[c.rx_bytes] > 0 || r[c.tx_bytes] > 0)); + }, + [col], + function(r1, r2) { + return ((r2.rx_bytes + r2.tx_bytes) - (r1.rx_bytes + r1.tx_bytes)); + } + ); + + var rxData = [], txData = []; + + dom.content(tooltip, [ + E('div', { 'class': 'head' }, [ + E('div', { 'class': 'pie' }, [ + E('label', _('Download')), + E('canvas', { 'id': 'bubble-pie1', 'width': 100, 'height': 100 }) + ]), + E('div', { 'class': 'pie' }, [ + E('label', _('Upload')), + E('canvas', { 'id': 'bubble-pie2', 'width': 100, 'height': 100 }) + ]), + E('div', { 'class': 'kpi' }, [ + E('ul', [ + E('li', _('Hostname: <big id="bubble-hostname">example.org</big>')), + E('li', _('Vendor: <big id="bubble-vendor">Example Corp.</big>')) + ]) + ]) + ]), + E('div', { 'class': 'table' }, [ + E('div', { 'class': 'tr table-titles' }, [ + E('div', { 'class': 'th' }, label || col), + E('div', { 'class': 'th' }, _('Conn.')), + E('div', { 'class': 'th' }, _('Down. (Bytes)')), + E('div', { 'class': 'th' }, _('Down. (Pkts.)')), + E('div', { 'class': 'th' }, _('Up. (Bytes)')), + E('div', { 'class': 'th' }, _('Up. (Pkts.)')), + ]) + ]) + ]); + + var rows = []; + + for (var i = 0; i < detailData.length; i++) { + var rec = detailData[i], + cell = E('div', rec[col] || _('other')); + + rows.push([ + cell, + '%1000.2m'.format(rec.conns), + '%1024.2mB'.format(rec.rx_bytes), + '%1000.2mP'.format(rec.rx_pkts), + '%1024.2mB'.format(rec.tx_bytes), + '%1000.2mP'.format(rec.tx_pkts) + ]); + + rxData.push({ + label: ['%s: %%1024.2mB'.format(rec[col] || _('other')), cell], + value: rec.rx_bytes + }); + + txData.push({ + label: ['%s: %%1024.2mB'.format(rec[col] || _('other')), cell], + value: rec.tx_bytes + }); + } + + cbi_update_table(tooltip.lastElementChild, rows); + + this.pie(tooltip.querySelector('#bubble-pie1'), rxData); + this.pie(tooltip.querySelector('#bubble-pie2'), txData); + + var mac = key.toUpperCase(); + var name = hostInfo.hasOwnProperty(mac) ? hostInfo[mac].name : null; + + if (!name) + for (var i = 0; i < detailData.length; i++) + if ((name = hostNames[detailData[i].ip]) !== undefined) + break; + + if (mac !== '00:00:00:00:00:00') { + this.kpi(tooltip.querySelector('#bubble-hostname'), name); + this.kpi(tooltip.querySelector('#bubble-vendor'), this.oui(mac)); + } + else { + this.kpi(tooltip.querySelector('#bubble-hostname')); + this.kpi(tooltip.querySelector('#bubble-vendor')); + } + + var rect = node.getBoundingClientRect(), x, y; + + if ('ontouchstart' in window || window.innerWidth <= 992) { + var vpHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0), + scrollFrom = window.pageYOffset, + scrollTo = scrollFrom + rect.top - vpHeight * 0.5, + start = null; + + tooltip.style.top = (rect.top + rect.height + window.pageYOffset) + 'px'; + tooltip.style.left = 0; + + var scrollStep = function(timestamp) { + if (!start) + start = timestamp; + + var duration = Math.max(timestamp - start, 1); + if (duration < 100) { + document.body.scrollTop = scrollFrom + (scrollTo - scrollFrom) * (duration / 100); + window.requestAnimationFrame(scrollStep); + } + else { + document.body.scrollTop = scrollTo; + } + }; + + window.requestAnimationFrame(scrollStep); + } + else { + x = rect.left + rect.width + window.pageXOffset, + y = rect.top + window.pageYOffset; + + if ((y + tooltip.offsetHeight) > (window.innerHeight + window.pageYOffset)) + y -= ((y + tooltip.offsetHeight) - (window.innerHeight + window.pageYOffset)); + + tooltip.style.top = y + 'px'; + tooltip.style.left = x + 'px'; + } + + return false; + }, + + setupCharts: function() { + Chart.defaults.global.customTooltips = L.bind(function(tooltip) { + var tooltipEl = document.getElementById('chartjs-tooltip'); + + if (!tooltipEl) { + tooltipEl = document.createElement('div'); + tooltipEl.setAttribute('id', 'chartjs-tooltip'); + document.body.appendChild(tooltipEl); + } + + if (!tooltip) { + if (tooltipEl.row) + tooltipEl.row.style.backgroundColor = ''; + + tooltipEl.style.opacity = 0; + return; + } + + var pos = this.off(tooltip.chart.canvas); + + tooltipEl.className = tooltip.yAlign; + tooltipEl.innerHTML = tooltip.text[0]; + + tooltipEl.style.opacity = 1; + tooltipEl.style.left = pos[0] + tooltip.x + 'px'; + tooltipEl.style.top = pos[1] + tooltip.y - tooltip.caretHeight - tooltip.caretPadding + 'px'; + + var row = findParent(tooltip.text[1], '.tr'), + hue = tooltip.text[2]; + + if (row && !isNaN(hue)) { + row.style.backgroundColor = 'hsl(%u, 100%%, 80%%)'.format(hue); + tooltipEl.row = row; + } + }, this); + + Chart.defaults.global.tooltipFontSize = 10; + Chart.defaults.global.tooltipTemplate = function(tip) { + tip.label[0] = tip.label[0].format(tip.value); + return tip.label; + }; + + this.renderHostData(); + this.renderLayer7Data(); + this.renderIPv6Data(); + }, + + handleDownload: function(type, group, order) { + var args = [ 'download', '-f', type ]; + + if (group) + args.push('-g', group); + + if (order) + args.push('-o', order); + + return fs.exec_direct('/usr/libexec/nlbwmon-action', args, 'blob').then(function(blob) { + var data = blob.slice(0, blob.size, (type == 'csv') ? 'text/csv' : 'application/json'), + name = 'nlbwmon-data.%s'.format(type), + url = window.URL.createObjectURL(data), + link = E('a', { 'style': 'display:none', 'href': url, 'download': name }); + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + }).catch(function(err) { + ui.addNotification(null, E('p', [ _('Failed to download traffic data: %s').format(err.message) ])); + }); + }, + + handleCommit: function() { + return fs.exec('/usr/libexec/nlbwmon-action', [ 'commit' ]).then(function(res) { + if (res.code != 0) + throw new Error(res.stderr || res.stdout); + + window.location.reload(true); + }).catch(function(err) { + ui.addNotification(null, E('p', [ _('Failed to commit database: %s').format(err.message) ])); + }); + }, + + render: function() { + document.addEventListener('tooltip-open', L.bind(function(ev) { + this.renderHostDetail(ev.detail.target, ev.target); + }, this)); + + if ('ontouchstart' in window) { + document.addEventListener('touchstart', function(ev) { + var tooltip = document.querySelector('.cbi-tooltip'); + if (tooltip === ev.target || tooltip.contains(ev.target)) + return; + + ui.hideTooltip(ev); + }); + } + + var node = E([], [ + E('link', { 'rel': 'stylesheet', 'href': L.resource('view/nlbw.css') }), + E('script', { + 'type': 'text/javascript', + 'src': L.resource('nlbw.chart.min.js'), + 'load': L.bind(this.setupCharts, this) + }), + + E('h2', [ _('Netlink Bandwidth Monitor') ]), + this.renderPeriods(), + + E('div', [ + E('div', { 'class': 'cbi-section', 'data-tab': 'traffic', 'data-tab-title': _('Traffic Distribution') }, [ + E('div', { 'class': 'head' }, [ + E('div', { 'class': 'pie' }, [ + E('label', [ _('Traffic / Host') ]), + E('canvas', { 'id': 'traf-pie', 'width': 200, 'height': 200 }) + ]), + + E('div', { 'class': 'pie' }, [ + E('label', [ _('Connections / Host') ]), + E('canvas', { 'id': 'conn-pie', 'width': 200, 'height': 200 }) + ]), + + E('div', { 'class': 'kpi' }, [ + E('ul', [ + E('li', _('<big id="host-total">0</big> hosts')), + E('li', _('<big id="rx-total">0</big> download')), + E('li', _('<big id="tx-total">0</big> upload')), + E('li', _('<big id="conn-total">0</big> connections')) + ]) + ]) + ]), + + E('div', { 'class': 'table', 'id': 'host-data' }, [ + E('div', { 'class': 'tr table-titles' }, [ + E('div', { 'class': 'th left hostname' }, [ _('Host') ]), + E('div', { 'class': 'th right' }, [ _('MAC') ]), + E('div', { 'class': 'th right' }, [ _('Connections') ]), + E('div', { 'class': 'th right' }, [ _('Download (Bytes)') ]), + E('div', { 'class': 'th right' }, [ _('Download (Packets)') ]), + E('div', { 'class': 'th right' }, [ _('Upload (Bytes)') ]), + E('div', { 'class': 'th right' }, [ _('Upload (Packets)') ]), + ]), + E('div', { 'class': 'tr placeholder' }, [ + E('div', { 'class': 'td' }, [ + E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]) + ]) + ]) + ]) + ]), + + E('div', { 'class': 'cbi-section', 'data-tab': 'layer7', 'data-tab-title': _('Application Protocols') }, [ + E('div', { 'class': 'head' }, [ + E('div', { 'class': 'pie' }, [ + E('label', [ _('Download / Application') ]), + E('canvas', { 'id': 'layer7-rx-pie', 'width': 200, 'height': 200 }) + ]), + + E('div', { 'class': 'pie' }, [ + E('label', [ _('Upload / Application') ]), + E('canvas', { 'id': 'layer7-tx-pie', 'width': 200, 'height': 200 }) + ]), + + E('div', { 'class': 'kpi' }, [ + E('ul', [ + E('li', _('<big id="layer7-total">0</big> different application protocols')), + E('li', _('<big id="layer7-most-rx">0</big> cause the most download')), + E('li', _('<big id="layer7-most-tx">0</big> cause the most upload')), + E('li', _('<big id="layer7-most-conn">0</big> cause the most connections')) + ]) + ]) + ]), + + E('div', { 'class': 'table', 'id': 'layer7-data' }, [ + E('div', { 'class': 'tr table-titles' }, [ + E('div', { 'class': 'th left' }, [ _('Application') ]), + E('div', { 'class': 'th right' }, [ _('Connections') ]), + E('div', { 'class': 'th right' }, [ _('Download (Bytes)') ]), + E('div', { 'class': 'th right' }, [ _('Download (Packets)') ]), + E('div', { 'class': 'th right' }, [ _('Upload (Bytes)') ]), + E('div', { 'class': 'th right' }, [ _('Upload (Packets)') ]), + ]), + E('div', { 'class': 'tr placeholder' }, [ + E('div', { 'class': 'td' }, [ + E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]) + ]) + ]) + ]) + ]), + + E('div', { 'class': 'cbi-section', 'data-tab': 'ipv6', 'data-tab-title': _('IPv6') }, [ + E('div', { 'class': 'head' }, [ + E('div', { 'class': 'pie' }, [ + E('label', [ _('IPv4 vs. IPv6') ]), + E('canvas', { 'id': 'ipv6-share-pie', 'width': 200, 'height': 200 }) + ]), + + E('div', { 'class': 'pie' }, [ + E('label', [ _('Dualstack enabled hosts') ]), + E('canvas', { 'id': 'ipv6-hosts-pie', 'width': 200, 'height': 200 }) + ]), + + E('div', { 'class': 'kpi' }, [ + E('ul', [ + E('li', _('<big id="ipv6-hosts">0%</big> IPv6 support rate among hosts')), + E('li', _('<big id="ipv6-share">0%</big> of the total traffic is IPv6')), + E('li', _('<big id="ipv6-rx">0B</big> total IPv6 download')), + E('li', _('<big id="ipv6-tx">0B</big> total IPv6 upload')) + ]) + ]) + ]), + + E('div', { 'class': 'table', 'id': 'ipv6-data' }, [ + E('div', { 'class': 'tr table-titles' }, [ + E('div', { 'class': 'th left' }, [ _('Host') ]), + E('div', { 'class': 'th right' }, [ _('MAC') ]), + E('div', { 'class': 'th double right hide-xs' }, [ _('Family') ]), + E('div', { 'class': 'th double right' }, [ _('Download (Bytes)') ]), + E('div', { 'class': 'th double right' }, [ _('Download (Packets)') ]), + E('div', { 'class': 'th double right' }, [ _('Upload (Bytes)') ]), + E('div', { 'class': 'th double right' }, [ _('Upload (Packets)') ]), + ]), + E('div', { 'class': 'tr placeholder' }, [ + E('div', { 'class': 'td' }, [ + E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]) + ]) + ]) + ]) + ]), + + E('div', { 'class': 'cbi-section', 'data-tab': 'export', 'data-tab-title': _('Export') }, [ + E('ul', [ + E('li', [ + E('a', { + 'href': '#', + 'click': ui.createHandlerFn(this, 'handleDownload', 'csv', 'mac', '-rx,-tx') + }, [ _('CSV, grouped by MAC') ]) + ]), + E('li', [ + E('a', { + 'href': '#', + 'click': ui.createHandlerFn(this, 'handleDownload', 'csv', 'ip', '-rx,-tx') + }, [ _('CSV, grouped by IP') ]) + ]), + E('li', [ + E('a', { + 'href': '#', + 'click': ui.createHandlerFn(this, 'handleDownload', 'csv', 'layer7', '-rx,-tx') + }, [ _('CSV, grouped by protocol') ]) + ]), + E('li', [ + E('a', { + 'href': '#', + 'click': ui.createHandlerFn(this, 'handleDownload', 'json', null, null) + }, [ _('JSON dump') ]) + ]) + ]) + ]) + ]) + ]); + + ui.tabs.initTabGroup(node.lastElementChild.childNodes); + + return node; + }, + + handleSave: null, + handleSaveApply: null, + handleReset: null +}); |