10 var callNetworkRrdnsLookup
= rpc
.declare({
11 object
: 'network.rrdns',
13 params
: [ 'addrs', 'timeout', 'limit' ],
17 var chartRegistry
= {},
19 trafficData
= { columns
: [], data
: [] },
34 loadHosts: function() {
35 return L
.resolveDefault(network
.getHostHints()).then(function(res
) {
37 var hints
= res
.getMACHints();
39 for (var i
= 0; i
< hints
.length
; i
++) {
40 hostInfo
[hints
[i
][0]] = {
41 name
: res
.getHostnameByMACAddr(hints
[i
][0]),
42 ipv6
: res
.getIP6AddrByMACAddr(hints
[i
][0]),
43 ipv4
: res
.getIPAddrByMACAddr(hints
[i
][0])
51 var url
= 'https://raw.githubusercontent.com/jow-/oui-database/master/oui.json';
53 return L
.resolveDefault(request
.get(url
, { cache
: true })).then(function(res
) {
54 res
= res
? res
.json() : [];
56 if (Array
.isArray(res
))
61 loadPeriods: function() {
62 return L
.resolveDefault(fs
.exec_direct('/usr/libexec/nlbwmon-action', [ 'periods' ], 'json')).then(function(res
) {
63 if (L
.isObject(res
) && Array
.isArray(res
.periods
))
64 trafficPeriods
= res
.periods
;
68 loadData: function(period
) {
69 var args
= [ 'download', '-g', 'family,mac,ip,layer7', '-o', '-rx_bytes,-tx_bytes' ];
72 args
.push('-t', period
);
74 return fs
.exec_direct('/usr/libexec/nlbwmon-action', args
, 'json').then(L
.bind(function(res
) {
75 if (!L
.isObject(res
) || !Array
.isArray(res
.columns
) || !Array
.isArray(res
.data
))
76 throw new Error(_('Malformed data received'));
80 var addrs
= this.query(null, [ 'ip' ], null),
83 for (var i
= 0; i
< addrs
.length
; i
++)
84 if (ipAddrs
.indexOf(addrs
[i
].ip
) < 0)
85 ipAddrs
.push(addrs
[i
].ip
);
88 return L
.resolveDefault(callNetworkRrdnsLookup(ipAddrs
, 1000, 1000), {}).then(function(res
) {
91 }, this)).catch(function(err
) {
92 ui
.addNotification(null, _('Unable to fetch traffic statistic data: %s').format(err
.message
));
99 if (!isNaN(elem
.offsetLeft
) && !isNaN(elem
.offsetTop
)) {
100 val
[0] += elem
.offsetLeft
;
101 val
[1] += elem
.offsetTop
;
104 while ((elem
= elem
.offsetParent
) != null);
108 kpi: function(id
, val1
, val2
, val3
) {
109 var e
= L
.dom
.elem(id
) ? id
: document
.getElementById(id
);
111 if (val1
&& val2
&& val3
)
112 e
.innerHTML
= _('%s, %s and %s').format(val1
, val2
, val3
);
113 else if (val1
&& val2
)
114 e
.innerHTML
= _('%s and %s').format(val1
, val2
);
118 e
.parentNode
.style
.display
= val1
? 'list-item' : '';
121 pie: function(id
, data
) {
122 var total
= data
.reduce(function(n
, d
) { return n
+ d
.value
}, 0);
124 data
.sort(function(a
, b
) { return b
.value
- a
.value
});
130 label
: [ _('no traffic') ]
133 for (var i
= 0; i
< data
.length
; i
++) {
134 if (!data
[i
].color
) {
135 var hue
= 120 / (data
.length
-1) * i
;
136 data
[i
].color
= 'hsl(%u, 80%%, 50%%)'.format(hue
);
137 data
[i
].label
.push(hue
);
141 var node
= L
.dom
.elem(id
) ? id
: document
.getElementById(id
),
142 key
= L
.dom
.elem(id
) ? id
.id
: id
,
143 ctx
= node
.getContext('2d');
145 if (chartRegistry
.hasOwnProperty(key
))
146 chartRegistry
[key
].destroy();
148 chartRegistry
[key
] = new Chart(ctx
).Doughnut(data
, {
149 segmentStrokeWidth
: 1,
150 percentageInnerCutout
: 30
153 return chartRegistry
[key
];
157 var m
, l
= 0, r
= ouiData
.length
/ 3 - 1;
158 var mac1
= parseInt(mac
.replace(/[^a-fA-F0-9]/g, ''), 16);
161 m
= l
+ Math
.floor((r
- l
) / 2);
163 var mask
= (0xffffffffffff -
164 (Math
.pow(2, 48 - ouiData
[m
* 3 + 1]) - 1));
166 var mac1_hi
= ((mac1
/ 0x10000) & (mask
/ 0x10000)) >>> 0;
167 var mac1_lo
= ((mac1
& 0xffff) & (mask
& 0xffff)) >>> 0;
169 var mac2
= parseInt(ouiData
[m
* 3], 16);
170 var mac2_hi
= (mac2
/ 0x10000) >>> 0;
171 var mac2_lo
= (mac2
& 0xffff) >>> 0;
173 if (mac1_hi
=== mac2_hi
&& mac1_lo
=== mac2_lo
)
174 return ouiData
[m
* 3 + 2];
176 if (mac2_hi
> mac1_hi
||
177 (mac2_hi
=== mac1_hi
&& mac2_lo
> mac1_lo
))
186 query: function(filter
, group
, order
) {
187 var keys
= [], columns
= {}, records
= {}, result
= [];
189 if (typeof(group
) !== 'function' && typeof(group
) !== 'object')
192 for (var i
= 0; i
< trafficData
.columns
.length
; i
++)
193 columns
[trafficData
.columns
[i
]] = i
;
195 for (var i
= 0; i
< trafficData
.data
.length
; i
++) {
196 var record
= trafficData
.data
[i
];
198 if (typeof(filter
) === 'function' && filter(columns
, record
) !== true)
203 if (typeof(group
) === 'function') {
204 key
= group(columns
, record
);
209 for (var j
= 0; j
< group
.length
; j
++)
210 if (columns
.hasOwnProperty(group
[j
]))
211 key
.push(record
[columns
[group
[j
]]]);
216 if (!records
.hasOwnProperty(key
)) {
219 for (var col
in columns
)
220 rec
[col
] = record
[columns
[col
]];
226 records
[key
].conns
+= record
[columns
.conns
];
227 records
[key
].rx_bytes
+= record
[columns
.rx_bytes
];
228 records
[key
].rx_pkts
+= record
[columns
.rx_pkts
];
229 records
[key
].tx_bytes
+= record
[columns
.tx_bytes
];
230 records
[key
].tx_pkts
+= record
[columns
.tx_pkts
];
234 if (typeof(order
) === 'function')
240 renderPeriods: function() {
241 if (!trafficPeriods
.length
)
247 for (var e
, i
= trafficPeriods
.length
- 1; e
= trafficPeriods
[i
]; i
--) {
248 var ymd1
= e
.split(/-/);
249 var d1
= new Date(+ymd1
[0], +ymd1
[1] - 1, +ymd1
[2]);
253 ymd2
= trafficPeriods
[i
- 1].split(/-/);
254 d2
= new Date(+ymd2
[0], +ymd2
[1] - 1, +ymd2
[2]);
255 d2
.setDate(d2
.getDate() - 1);
264 choices
[pd
] = '%04d-%02d-%02d - %04d-%02d-%02d'.format(
265 d1
.getFullYear(), d1
.getMonth() + 1, d1
.getDate(),
266 d2
.getFullYear(), d2
.getMonth() + 1, d2
.getDate()
270 var dropdown
= new ui
.Dropdown('-', choices
, { sort
: keys
, optional
: false }).render();
272 dropdown
.addEventListener('cbi-dropdown-change', ui
.createHandlerFn(this, function(ev
) {
275 var period
= ev
.detail
.value
.value
!= '-' ? ev
.detail
.value
.value
: null;
277 return this.loadData(period
).then(L
.bind(function() {
278 this.renderHostData();
279 this.renderLayer7Data();
280 this.renderIPv6Data();
285 E('p', [ _('Select accounting period:'), ' ', dropdown
]),
290 formatHostname: function(dns
) {
291 if (dns
=== undefined || dns
=== null || dns
=== '')
294 dns
= dns
.split('.')[0];
297 return '<span title="%q">%h…</span>'.format(dns
, dns
.substr(0, 12));
299 return '%h'.format(dns
);
302 renderHostData: function() {
303 var trafData
= [], connData
= [];
304 var rx_total
= 0, tx_total
= 0, conn_total
= 0;
306 var hostData
= this.query(
308 return (r
[c
.rx_bytes
] > 0 || r
[c
.tx_bytes
] > 0);
312 // return (r[c.mac] !== '00:00:00:00:00:00') ? r[c.mac] : r[c.ip];
315 return ((r2
.rx_bytes
+ r2
.tx_bytes
) - (r1
.rx_bytes
+ r1
.tx_bytes
));
321 for (var i
= 0; i
< hostData
.length
; i
++) {
322 var rec
= hostData
[i
],
323 mac
= rec
.mac
.toUpperCase(),
324 key
= (mac
!== '00:00:00:00:00:00') ? mac
: rec
.ip
,
325 dns
= hostInfo
[mac
] ? hostInfo
[mac
].name
: null;
327 var cell
= E('div', this.formatHostname(dns
));
332 'href': '#' + rec
.mac
,
334 'data-tooltip': _('Source IP')
335 }, (mac
!== '00:00:00:00:00:00') ? mac
: _('other')),
337 'href': '#' + rec
.mac
,
338 'data-col': 'layer7',
339 'data-tooltip': _('Protocol')
340 }, '%1000.2m'.format(rec
.conns
)),
341 '%1024.2mB'.format(rec
.rx_bytes
),
342 '%1000.2mP'.format(rec
.rx_pkts
),
343 '%1024.2mB'.format(rec
.tx_bytes
),
344 '%1000.2mP'.format(rec
.tx_pkts
)
348 value
: rec
.rx_bytes
+ rec
.tx_bytes
,
349 label
: ["%s: %%.2mB".format(key
), cell
]
354 label
: ["%s: %%.2m".format(key
), cell
]
357 rx_total
+= rec
.rx_bytes
;
358 tx_total
+= rec
.tx_bytes
;
359 conn_total
+= rec
.conns
;
362 cbi_update_table('#host-data', rows
, E('em', [
363 _('No data recorded yet.'), ' ',
366 'click': ui
.createHandlerFn(this, 'handleCommit')
367 }, _('Force reload…'))
370 this.pie('traf-pie', trafData
);
371 this.pie('conn-pie', connData
);
373 this.kpi('rx-total', '%1024.2mB'.format(rx_total
));
374 this.kpi('tx-total', '%1024.2mB'.format(tx_total
));
375 this.kpi('conn-total', '%1000m'.format(conn_total
));
376 this.kpi('host-total', '%u'.format(hostData
.length
));
379 renderLayer7Data: function() {
380 var rxData
= [], txData
= [];
381 var topConn
= [[0],[0],[0]], topRx
= [[0],[0],[0]], topTx
= [[0],[0],[0]];
383 var layer7Data
= this.query(
386 return ((r2
.rx_bytes
+ r2
.tx_bytes
) - (r1
.rx_bytes
+ r1
.tx_bytes
));
392 for (var i
= 0, c
= 0; i
< layer7Data
.length
; i
++) {
393 var rec
= layer7Data
[i
],
394 cell
= E('div', rec
.layer7
|| _('other'));
398 '%1000m'.format(rec
.conns
),
399 '%1024.2mB'.format(rec
.rx_bytes
),
400 '%1000.2mP'.format(rec
.rx_pkts
),
401 '%1024.2mB'.format(rec
.tx_bytes
),
402 '%1000.2mP'.format(rec
.tx_pkts
)
407 label
: ["%s: %%.2mB".format(rec
.layer7
|| _('other')), cell
]
412 label
: ["%s: %%.2mB".format(rec
.layer7
|| _('other')), cell
]
416 topRx
.push([rec
.rx_bytes
, rec
.layer7
]);
417 topTx
.push([rec
.tx_bytes
, rec
.layer7
]);
418 topConn
.push([rec
.conns
, rec
.layer7
]);
422 cbi_update_table('#layer7-data', rows
, E('em', [
423 _('No data recorded yet.'), ' ',
424 E('a', { 'href': L
.url('admin/nlbw/commit') }, _('Force reload…'))
427 this.pie('layer7-rx-pie', rxData
);
428 this.pie('layer7-tx-pie', txData
);
430 topRx
.sort(function(a
, b
) { return b
[0] - a
[0] });
431 topTx
.sort(function(a
, b
) { return b
[0] - a
[0] });
432 topConn
.sort(function(a
, b
) { return b
[0] - a
[0] });
434 this.kpi('layer7-total', layer7Data
.length
);
435 this.kpi('layer7-most-rx', topRx
[0][1], topRx
[1][1], topRx
[2][1]);
436 this.kpi('layer7-most-tx', topTx
[0][1], topTx
[1][1], topTx
[2][1]);
437 this.kpi('layer7-most-conn', topConn
[0][1], topConn
[1][1], topConn
[2][1]);
440 renderIPv6Data: function() {
452 var ipv6Data
= this.query(
453 null, ['family', 'mac'],
455 return ((r2
.rx_bytes
+ r2
.tx_bytes
) - (r1
.rx_bytes
+ r1
.tx_bytes
));
459 for (var i
= 0, c
= 0; i
< ipv6Data
.length
; i
++) {
460 var rec
= ipv6Data
[i
],
461 mac
= rec
.mac
.toUpperCase(),
463 fam
= families
[mac
] || 0,
464 recs
= records
[mac
] || {};
466 if (rec
.family
== 4) {
467 rx4_total
+= rec
.rx_bytes
;
468 tx4_total
+= rec
.tx_bytes
;
472 rx6_total
+= rec
.rx_bytes
;
473 tx6_total
+= rec
.tx_bytes
;
477 recs
[rec
.family
] = rec
;
483 for (var mac
in families
) {
484 switch (families
[mac
])
502 for (var mac
in records
) {
503 if (mac
=== '00:00:00:00:00:00')
506 var dns
= hostInfo
[mac
] ? hostInfo
[mac
].name
: null,
507 rec4
= records
[mac
][4],
508 rec6
= records
[mac
][6];
511 this.formatHostname(dns
),
513 [ E('span', _('IPv4')),
514 E('span', _('IPv6')) ],
515 [ E('span', rec4
? '%1024.2mB'.format(rec4
.rx_bytes
) : '-'),
516 E('span', rec6
? '%1024.2mB'.format(rec6
.rx_bytes
) : '-') ],
517 [ E('span', rec4
? '%1000.2mP'.format(rec4
.rx_pkts
) : '-'),
518 E('span', rec6
? '%1000.2mP'.format(rec6
.rx_pkts
) : '-') ],
519 [ E('span', rec4
? '%1024.2mB'.format(rec4
.tx_bytes
) : '-'),
520 E('span', rec6
? '%1024.2mB'.format(rec6
.tx_bytes
) : '-') ],
521 [ E('span', rec4
? '%1000.2mP'.format(rec4
.tx_pkts
) : '-'),
522 E('span', rec6
? '%1000.2mP'.format(rec6
.tx_pkts
) : '-') ]
526 cbi_update_table('#ipv6-data', rows
, E('em', [
527 _('No data recorded yet.'), ' ',
528 E('a', { 'href': L
.url('admin/nlbw/commit') }, _('Force reload…'))
531 var shareData
= [], hostsData
= [];
533 if (rx4_total
> 0 || tx4_total
> 0)
535 value
: rx4_total
+ tx4_total
,
536 label
: ["IPv4: %.2mB"],
537 color
: 'hsl(140, 100%, 50%)'
540 if (rx6_total
> 0 || tx6_total
> 0)
542 value
: rx6_total
+ tx6_total
,
543 label
: ["IPv6: %.2mB"],
544 color
: 'hsl(180, 100%, 50%)'
550 label
: [_('%d IPv4-only hosts')],
551 color
: 'hsl(140, 100%, 50%)'
557 label
: [_('%d IPv6-only hosts')],
558 color
: 'hsl(180, 100%, 50%)'
564 label
: [_('%d dual-stack hosts')],
565 color
: 'hsl(50, 100%, 50%)'
568 this.pie('ipv6-share-pie', shareData
);
569 this.pie('ipv6-hosts-pie', hostsData
);
571 this.kpi('ipv6-hosts', '%.2f%%'.format(100 / (ds_total
+ v4_total
+ v6_total
) * (ds_total
+ v6_total
)));
572 this.kpi('ipv6-share', '%.2f%%'.format(100 / (rx4_total
+ rx6_total
+ tx4_total
+ tx6_total
) * (rx6_total
+ tx6_total
)));
573 this.kpi('ipv6-rx', '%1024.2mB'.format(rx6_total
));
574 this.kpi('ipv6-tx', '%1024.2mB'.format(tx6_total
));
577 renderHostDetail: function(node
, tooltip
) {
578 var key
= node
.getAttribute('href').substr(1),
579 col
= node
.getAttribute('data-col'),
580 label
= node
.getAttribute('data-tooltip');
582 var detailData
= this.query(
584 return ((r
[c
.mac
] === key
|| r
[c
.ip
] === key
) &&
585 (r
[c
.rx_bytes
] > 0 || r
[c
.tx_bytes
] > 0));
589 return ((r2
.rx_bytes
+ r2
.tx_bytes
) - (r1
.rx_bytes
+ r1
.tx_bytes
));
593 var rxData
= [], txData
= [];
595 dom
.content(tooltip
, [
596 E('div', { 'class': 'head' }, [
597 E('div', { 'class': 'pie' }, [
598 E('label', _('Download')),
599 E('canvas', { 'id': 'bubble-pie1', 'width': 100, 'height': 100 })
601 E('div', { 'class': 'pie' }, [
602 E('label', _('Upload')),
603 E('canvas', { 'id': 'bubble-pie2', 'width': 100, 'height': 100 })
605 E('div', { 'class': 'kpi' }, [
607 E('li', _('Hostname: <big id="bubble-hostname">example.org</big>')),
608 E('li', _('Vendor: <big id="bubble-vendor">Example Corp.</big>'))
612 E('div', { 'class': 'table' }, [
613 E('div', { 'class': 'tr table-titles' }, [
614 E('div', { 'class': 'th' }, label
|| col
),
615 E('div', { 'class': 'th' }, _('Conn.')),
616 E('div', { 'class': 'th' }, _('Down. (Bytes)')),
617 E('div', { 'class': 'th' }, _('Down. (Pkts.)')),
618 E('div', { 'class': 'th' }, _('Up. (Bytes)')),
619 E('div', { 'class': 'th' }, _('Up. (Pkts.)')),
626 for (var i
= 0; i
< detailData
.length
; i
++) {
627 var rec
= detailData
[i
],
628 cell
= E('div', rec
[col
] || _('other'));
632 '%1000.2m'.format(rec
.conns
),
633 '%1024.2mB'.format(rec
.rx_bytes
),
634 '%1000.2mP'.format(rec
.rx_pkts
),
635 '%1024.2mB'.format(rec
.tx_bytes
),
636 '%1000.2mP'.format(rec
.tx_pkts
)
640 label
: ['%s: %%1024.2mB'.format(rec
[col
] || _('other')), cell
],
645 label
: ['%s: %%1024.2mB'.format(rec
[col
] || _('other')), cell
],
650 cbi_update_table(tooltip
.lastElementChild
, rows
);
652 this.pie(tooltip
.querySelector('#bubble-pie1'), rxData
);
653 this.pie(tooltip
.querySelector('#bubble-pie2'), txData
);
655 var mac
= key
.toUpperCase();
656 var name
= hostInfo
.hasOwnProperty(mac
) ? hostInfo
[mac
].name
: null;
659 for (var i
= 0; i
< detailData
.length
; i
++)
660 if ((name
= hostNames
[detailData
[i
].ip
]) !== undefined)
663 if (mac
!== '00:00:00:00:00:00') {
664 this.kpi(tooltip
.querySelector('#bubble-hostname'), name
);
665 this.kpi(tooltip
.querySelector('#bubble-vendor'), this.oui(mac
));
668 this.kpi(tooltip
.querySelector('#bubble-hostname'));
669 this.kpi(tooltip
.querySelector('#bubble-vendor'));
672 var rect
= node
.getBoundingClientRect(), x
, y
;
674 if ('ontouchstart' in window
|| window
.innerWidth
<= 992) {
675 var vpHeight
= Math
.max(document
.documentElement
.clientHeight
, window
.innerHeight
|| 0),
676 scrollFrom
= window
.pageYOffset
,
677 scrollTo
= scrollFrom
+ rect
.top
- vpHeight
* 0.5,
680 tooltip
.style
.top
= (rect
.top
+ rect
.height
+ window
.pageYOffset
) + 'px';
681 tooltip
.style
.left
= 0;
683 var scrollStep = function(timestamp
) {
687 var duration
= Math
.max(timestamp
- start
, 1);
688 if (duration
< 100) {
689 document
.body
.scrollTop
= scrollFrom
+ (scrollTo
- scrollFrom
) * (duration
/ 100);
690 window
.requestAnimationFrame(scrollStep
);
693 document
.body
.scrollTop
= scrollTo
;
697 window
.requestAnimationFrame(scrollStep
);
700 x
= rect
.left
+ rect
.width
+ window
.pageXOffset
,
701 y
= rect
.top
+ window
.pageYOffset
;
703 if ((y
+ tooltip
.offsetHeight
) > (window
.innerHeight
+ window
.pageYOffset
))
704 y
-= ((y
+ tooltip
.offsetHeight
) - (window
.innerHeight
+ window
.pageYOffset
));
706 tooltip
.style
.top
= y
+ 'px';
707 tooltip
.style
.left
= x
+ 'px';
713 setupCharts: function() {
714 Chart
.defaults
.global
.customTooltips
= L
.bind(function(tooltip
) {
715 var tooltipEl
= document
.getElementById('chartjs-tooltip');
718 tooltipEl
= document
.createElement('div');
719 tooltipEl
.setAttribute('id', 'chartjs-tooltip');
720 document
.body
.appendChild(tooltipEl
);
725 tooltipEl
.row
.style
.backgroundColor
= '';
727 tooltipEl
.style
.opacity
= 0;
731 var pos
= this.off(tooltip
.chart
.canvas
);
733 tooltipEl
.className
= tooltip
.yAlign
;
734 tooltipEl
.innerHTML
= tooltip
.text
[0];
736 tooltipEl
.style
.opacity
= 1;
737 tooltipEl
.style
.left
= pos
[0] + tooltip
.x
+ 'px';
738 tooltipEl
.style
.top
= pos
[1] + tooltip
.y
- tooltip
.caretHeight
- tooltip
.caretPadding
+ 'px';
740 var row
= findParent(tooltip
.text
[1], '.tr'),
741 hue
= tooltip
.text
[2];
743 if (row
&& !isNaN(hue
)) {
744 row
.style
.backgroundColor
= 'hsl(%u, 100%%, 80%%)'.format(hue
);
749 Chart
.defaults
.global
.tooltipFontSize
= 10;
750 Chart
.defaults
.global
.tooltipTemplate = function(tip
) {
751 tip
.label
[0] = tip
.label
[0].format(tip
.value
);
755 this.renderHostData();
756 this.renderLayer7Data();
757 this.renderIPv6Data();
760 handleDownload: function(type
, group
, order
) {
761 var args
= [ 'download', '-f', type
];
764 args
.push('-g', group
);
767 args
.push('-o', order
);
769 return fs
.exec_direct('/usr/libexec/nlbwmon-action', args
, 'blob').then(function(blob
) {
770 var data
= blob
.slice(0, blob
.size
, (type
== 'csv') ? 'text/csv' : 'application/json'),
771 name
= 'nlbwmon-data.%s'.format(type
),
772 url
= window
.URL
.createObjectURL(data
),
773 link
= E('a', { 'style': 'display:none', 'href': url
, 'download': name
});
775 document
.body
.appendChild(link
);
777 document
.body
.removeChild(link
);
778 window
.URL
.revokeObjectURL(url
);
779 }).catch(function(err
) {
780 ui
.addNotification(null, E('p', [ _('Failed to download traffic data: %s').format(err
.message
) ]));
784 handleCommit: function() {
785 return fs
.exec('/usr/libexec/nlbwmon-action', [ 'commit' ]).then(function(res
) {
787 throw new Error(res
.stderr
|| res
.stdout
);
789 window
.location
.reload(true);
790 }).catch(function(err
) {
791 ui
.addNotification(null, E('p', [ _('Failed to commit database: %s').format(err
.message
) ]));
796 document
.addEventListener('tooltip-open', L
.bind(function(ev
) {
797 this.renderHostDetail(ev
.detail
.target
, ev
.target
);
800 if ('ontouchstart' in window
) {
801 document
.addEventListener('touchstart', function(ev
) {
802 var tooltip
= document
.querySelector('.cbi-tooltip');
803 if (tooltip
=== ev
.target
|| tooltip
.contains(ev
.target
))
811 E('link', { 'rel': 'stylesheet', 'href': L
.resource('view/nlbw.css') }),
813 'type': 'text/javascript',
814 'src': L
.resource('nlbw.chart.min.js'),
815 'load': L
.bind(this.setupCharts
, this)
818 E('h2', [ _('Netlink Bandwidth Monitor') ]),
819 this.renderPeriods(),
822 E('div', { 'class': 'cbi-section', 'data-tab': 'traffic', 'data-tab-title': _('Traffic Distribution') }, [
823 E('div', { 'class': 'head' }, [
824 E('div', { 'class': 'pie' }, [
825 E('label', [ _('Traffic / Host') ]),
826 E('canvas', { 'id': 'traf-pie', 'width': 200, 'height': 200 })
829 E('div', { 'class': 'pie' }, [
830 E('label', [ _('Connections / Host') ]),
831 E('canvas', { 'id': 'conn-pie', 'width': 200, 'height': 200 })
834 E('div', { 'class': 'kpi' }, [
836 E('li', _('<big id="host-total">0</big> hosts')),
837 E('li', _('<big id="rx-total">0</big> download')),
838 E('li', _('<big id="tx-total">0</big> upload')),
839 E('li', _('<big id="conn-total">0</big> connections'))
844 E('div', { 'class': 'table', 'id': 'host-data' }, [
845 E('div', { 'class': 'tr table-titles' }, [
846 E('div', { 'class': 'th left hostname' }, [ _('Host') ]),
847 E('div', { 'class': 'th right' }, [ _('MAC') ]),
848 E('div', { 'class': 'th right' }, [ _('Connections') ]),
849 E('div', { 'class': 'th right' }, [ _('Download (Bytes)') ]),
850 E('div', { 'class': 'th right' }, [ _('Download (Packets)') ]),
851 E('div', { 'class': 'th right' }, [ _('Upload (Bytes)') ]),
852 E('div', { 'class': 'th right' }, [ _('Upload (Packets)') ]),
854 E('div', { 'class': 'tr placeholder' }, [
855 E('div', { 'class': 'td' }, [
856 E('em', { 'class': 'spinning' }, [ _('Collecting data...') ])
862 E('div', { 'class': 'cbi-section', 'data-tab': 'layer7', 'data-tab-title': _('Application Protocols') }, [
863 E('div', { 'class': 'head' }, [
864 E('div', { 'class': 'pie' }, [
865 E('label', [ _('Download / Application') ]),
866 E('canvas', { 'id': 'layer7-rx-pie', 'width': 200, 'height': 200 })
869 E('div', { 'class': 'pie' }, [
870 E('label', [ _('Upload / Application') ]),
871 E('canvas', { 'id': 'layer7-tx-pie', 'width': 200, 'height': 200 })
874 E('div', { 'class': 'kpi' }, [
876 E('li', _('<big id="layer7-total">0</big> different application protocols')),
877 E('li', _('<big id="layer7-most-rx">0</big> cause the most download')),
878 E('li', _('<big id="layer7-most-tx">0</big> cause the most upload')),
879 E('li', _('<big id="layer7-most-conn">0</big> cause the most connections'))
884 E('div', { 'class': 'table', 'id': 'layer7-data' }, [
885 E('div', { 'class': 'tr table-titles' }, [
886 E('div', { 'class': 'th left' }, [ _('Application') ]),
887 E('div', { 'class': 'th right' }, [ _('Connections') ]),
888 E('div', { 'class': 'th right' }, [ _('Download (Bytes)') ]),
889 E('div', { 'class': 'th right' }, [ _('Download (Packets)') ]),
890 E('div', { 'class': 'th right' }, [ _('Upload (Bytes)') ]),
891 E('div', { 'class': 'th right' }, [ _('Upload (Packets)') ]),
893 E('div', { 'class': 'tr placeholder' }, [
894 E('div', { 'class': 'td' }, [
895 E('em', { 'class': 'spinning' }, [ _('Collecting data...') ])
901 E('div', { 'class': 'cbi-section', 'data-tab': 'ipv6', 'data-tab-title': _('IPv6') }, [
902 E('div', { 'class': 'head' }, [
903 E('div', { 'class': 'pie' }, [
904 E('label', [ _('IPv4 vs. IPv6') ]),
905 E('canvas', { 'id': 'ipv6-share-pie', 'width': 200, 'height': 200 })
908 E('div', { 'class': 'pie' }, [
909 E('label', [ _('Dualstack enabled hosts') ]),
910 E('canvas', { 'id': 'ipv6-hosts-pie', 'width': 200, 'height': 200 })
913 E('div', { 'class': 'kpi' }, [
915 E('li', _('<big id="ipv6-hosts">0%</big> IPv6 support rate among hosts')),
916 E('li', _('<big id="ipv6-share">0%</big> of the total traffic is IPv6')),
917 E('li', _('<big id="ipv6-rx">0B</big> total IPv6 download')),
918 E('li', _('<big id="ipv6-tx">0B</big> total IPv6 upload'))
923 E('div', { 'class': 'table', 'id': 'ipv6-data' }, [
924 E('div', { 'class': 'tr table-titles' }, [
925 E('div', { 'class': 'th left' }, [ _('Host') ]),
926 E('div', { 'class': 'th right' }, [ _('MAC') ]),
927 E('div', { 'class': 'th double right hide-xs' }, [ _('Family') ]),
928 E('div', { 'class': 'th double right' }, [ _('Download (Bytes)') ]),
929 E('div', { 'class': 'th double right' }, [ _('Download (Packets)') ]),
930 E('div', { 'class': 'th double right' }, [ _('Upload (Bytes)') ]),
931 E('div', { 'class': 'th double right' }, [ _('Upload (Packets)') ]),
933 E('div', { 'class': 'tr placeholder' }, [
934 E('div', { 'class': 'td' }, [
935 E('em', { 'class': 'spinning' }, [ _('Collecting data...') ])
941 E('div', { 'class': 'cbi-section', 'data-tab': 'export', 'data-tab-title': _('Export') }, [
946 'click': ui
.createHandlerFn(this, 'handleDownload', 'csv', 'mac', '-rx,-tx')
947 }, [ _('CSV, grouped by MAC') ])
952 'click': ui
.createHandlerFn(this, 'handleDownload', 'csv', 'ip', '-rx,-tx')
953 }, [ _('CSV, grouped by IP') ])
958 'click': ui
.createHandlerFn(this, 'handleDownload', 'csv', 'layer7', '-rx,-tx')
959 }, [ _('CSV, grouped by protocol') ])
964 'click': ui
.createHandlerFn(this, 'handleDownload', 'json', null, null)
965 }, [ _('JSON dump') ])
972 ui
.tabs
.initTabGroup(node
.lastElementChild
.childNodes
);
978 handleSaveApply
: null,