1 var chartRegistry
= {},
3 trafficData
= { columns
: [], data
: [] },
12 if (!isNaN(elem
.offsetLeft
) && !isNaN(elem
.offsetTop
)) {
13 val
[0] += elem
.offsetLeft
;
14 val
[1] += elem
.offsetTop
;
17 while ((elem
= elem
.offsetParent
) != null);
21 Chart
.defaults
.global
.customTooltips = function(tooltip
) {
22 var tooltipEl
= document
.getElementById('chartjs-tooltip');
25 tooltipEl
= document
.createElement('div');
26 tooltipEl
.setAttribute('id', 'chartjs-tooltip');
27 document
.body
.appendChild(tooltipEl
);
32 tooltipEl
.row
.style
.backgroundColor
= '';
34 tooltipEl
.style
.opacity
= 0;
38 var pos
= off(tooltip
.chart
.canvas
);
40 tooltipEl
.className
= tooltip
.yAlign
;
41 tooltipEl
.innerHTML
= tooltip
.text
[0];
43 tooltipEl
.style
.opacity
= 1;
44 tooltipEl
.style
.left
= pos
[0] + tooltip
.x
+ 'px';
45 tooltipEl
.style
.top
= pos
[1] + tooltip
.y
- tooltip
.caretHeight
- tooltip
.caretPadding
+ 'px';
47 console
.debug(tooltip
.text
);
49 var row
= findParent(tooltip
.text
[1], '.tr'),
50 hue
= tooltip
.text
[2];
52 if (row
&& !isNaN(hue
)) {
53 row
.style
.backgroundColor
= 'hsl(%u, 100%%, 80%%)'.format(hue
);
58 Chart
.defaults
.global
.tooltipFontSize
= 10;
59 Chart
.defaults
.global
.tooltipTemplate = function(tip
) {
60 tip
.label
[0] = tip
.label
[0].format(tip
.value
);
64 function kpi(id
, val1
, val2
, val3
)
66 var e
= L
.dom
.elem(id
) ? id
: document
.getElementById(id
);
68 if (val1
&& val2
&& val3
)
69 e
.innerHTML
= _('%s, %s and %s').format(val1
, val2
, val3
);
70 else if (val1
&& val2
)
71 e
.innerHTML
= _('%s and %s').format(val1
, val2
);
75 e
.parentNode
.style
.display
= val1
? 'list-item' : '';
78 function pie(id
, data
)
80 var total
= data
.reduce(function(n
, d
) { return n
+ d
.value
}, 0);
82 data
.sort(function(a
, b
) { return b
.value
- a
.value
});
88 label
: [ _('no traffic') ]
91 for (var i
= 0; i
< data
.length
; i
++) {
93 var hue
= 120 / (data
.length
-1) * i
;
94 data
[i
].color
= 'hsl(%u, 80%%, 50%%)'.format(hue
);
95 data
[i
].label
.push(hue
);
99 var node
= L
.dom
.elem(id
) ? id
: document
.getElementById(id
),
100 key
= L
.dom
.elem(id
) ? id
.id
: id
,
101 ctx
= node
.getContext('2d');
103 if (chartRegistry
.hasOwnProperty(key
))
104 chartRegistry
[key
].destroy();
106 chartRegistry
[key
] = new Chart(ctx
).Doughnut(data
, {
107 segmentStrokeWidth
: 1,
108 percentageInnerCutout
: 30
111 return chartRegistry
[key
];
114 function query(filter
, group
, order
)
116 var keys
= [], columns
= {}, records
= {}, result
= [];
118 if (typeof(group
) !== 'function' && typeof(group
) !== 'object')
121 for (var i
= 0; i
< trafficData
.columns
.length
; i
++)
122 columns
[trafficData
.columns
[i
]] = i
;
124 for (var i
= 0; i
< trafficData
.data
.length
; i
++) {
125 var record
= trafficData
.data
[i
];
127 if (typeof(filter
) === 'function' && filter(columns
, record
) !== true)
132 if (typeof(group
) === 'function') {
133 key
= group(columns
, record
);
138 for (var j
= 0; j
< group
.length
; j
++)
139 if (columns
.hasOwnProperty(group
[j
]))
140 key
.push(record
[columns
[group
[j
]]]);
145 if (!records
.hasOwnProperty(key
)) {
148 for (var col
in columns
)
149 rec
[col
] = record
[columns
[col
]];
155 records
[key
].conns
+= record
[columns
.conns
];
156 records
[key
].rx_bytes
+= record
[columns
.rx_bytes
];
157 records
[key
].rx_pkts
+= record
[columns
.rx_pkts
];
158 records
[key
].tx_bytes
+= record
[columns
.tx_bytes
];
159 records
[key
].tx_pkts
+= record
[columns
.tx_pkts
];
163 if (typeof(order
) === 'function')
170 var m
, l
= 0, r
= ouiData
.length
/ 3 - 1;
171 var mac1
= parseInt(mac
.replace(/[^a-fA-F0-9]/g, ''), 16);
174 m
= l
+ Math
.floor((r
- l
) / 2);
176 var mask
= (0xffffffffffff -
177 (Math
.pow(2, 48 - ouiData
[m
* 3 + 1]) - 1));
179 var mac1_hi
= ((mac1
/ 0x10000) & (mask
/ 0x10000)) >>> 0;
180 var mac1_lo
= ((mac1
& 0xffff) & (mask
& 0xffff)) >>> 0;
182 var mac2
= parseInt(ouiData
[m
* 3], 16);
183 var mac2_hi
= (mac2
/ 0x10000) >>> 0;
184 var mac2_lo
= (mac2
& 0xffff) >>> 0;
186 if (mac1_hi
=== mac2_hi
&& mac1_lo
=== mac2_lo
)
187 return ouiData
[m
* 3 + 2];
189 if (mac2_hi
> mac1_hi
||
190 (mac2_hi
=== mac1_hi
&& mac2_lo
> mac1_lo
))
200 function fetchData(period
)
202 XHR
.get(L
.url('admin/nlbw/data'), { period
: period
, group_by
: 'family,mac,ip,layer7', order_by
: '-rx_bytes,-tx_bytes' }, function(xhr
, res
) {
203 if (res
!== null && typeof(res
) === 'object' && typeof(res
.columns
) === 'object' && typeof(res
.data
) === 'object')
206 var addrs
= query(null, ['ip'], null);
209 for (var i
= 0; i
< addrs
.length
; i
++)
210 if (ipAddrs
.indexOf(addrs
[i
].ip
) < 0)
211 ipAddrs
.push(addrs
[i
].ip
);
217 XHR
.get(L
.url('admin/nlbw/ptr', ipAddrs
.join('/')), null, function(xhr
, res
) {
218 if (res
!== null && typeof(res
) === 'object')
224 function renderPeriods()
226 var sel
= document
.getElementById('nlbw.period');
228 for (var e
, i
= trafficPeriods
.length
- 1; e
= trafficPeriods
[i
]; i
--) {
229 var d1
= new Date(e
);
233 d2
= new Date(trafficPeriods
[i
- 1]);
234 d2
.setDate(d2
.getDate() - 1);
235 pd
= '%04d-%02d-%02d'.format(d1
.getFullYear(), d1
.getMonth() + 1, d1
.getDate());
242 var opt
= document
.createElement('option');
243 opt
.setAttribute('data-duration', (d2
.getTime() - d1
.getTime()) / 1000);
245 opt
.text
= '%04d-%02d-%02d - %04d-%02d-%02d'.format(
246 d1
.getFullYear(), d1
.getMonth() + 1, d1
.getDate(),
247 d2
.getFullYear(), d2
.getMonth() + 1, d2
.getDate());
249 sel
.appendChild(opt
);
252 sel
.selectedIndex
= sel
.childNodes
.length
- 1;
253 sel
.style
.display
= '';
255 sel
.onchange = function(ev
) {
257 fetchData(sel
.options
[sel
.selectedIndex
].value
);
261 function renderHostDetail(tooltip
)
263 var key
= this.getAttribute('href').substr(1),
264 col
= this.getAttribute('data-col'),
265 label
= this.getAttribute('data-tooltip');
267 var detailData
= query(
269 return ((r
[c
.mac
] === key
|| r
[c
.ip
] === key
) &&
270 (r
[c
.rx_bytes
] > 0 || r
[c
.tx_bytes
] > 0));
274 return ((r2
.rx_bytes
+ r2
.tx_bytes
) - (r1
.rx_bytes
+ r1
.tx_bytes
));
278 var rxData
= [], txData
= [];
280 L
.dom
.content(tooltip
, [
281 E('div', { 'class': 'head' }, [
282 E('div', { 'class': 'pie' }, [
283 E('label', _('Download')),
284 E('canvas', { 'id': 'bubble-pie1', 'width': 100, 'height': 100 })
286 E('div', { 'class': 'pie' }, [
287 E('label', _('Upload')),
288 E('canvas', { 'id': 'bubble-pie2', 'width': 100, 'height': 100 })
290 E('div', { 'class': 'kpi' }, [
292 E('li', _('Hostname: <big id="bubble-hostname">example.org</big>')),
293 E('li', _('Vendor: <big id="bubble-vendor">Example Corp.</big>'))
297 E('div', { 'class': 'table' }, [
298 E('div', { 'class': 'tr table-titles' }, [
299 E('div', { 'class': 'th' }, label
|| col
),
300 E('div', { 'class': 'th' }, _('Conn.')),
301 E('div', { 'class': 'th' }, _('Down. (Bytes)')),
302 E('div', { 'class': 'th' }, _('Down. (Pkts.)')),
303 E('div', { 'class': 'th' }, _('Up. (Bytes)')),
304 E('div', { 'class': 'th' }, _('Up. (Pkts.)')),
311 for (var i
= 0; i
< detailData
.length
; i
++) {
312 var rec
= detailData
[i
],
313 cell
= E('div', rec
[col
] || _('other'));
317 '%1000.2m'.format(rec
.conns
),
318 '%1024.2mB'.format(rec
.rx_bytes
),
319 '%1000.2mP'.format(rec
.rx_pkts
),
320 '%1024.2mB'.format(rec
.tx_bytes
),
321 '%1000.2mP'.format(rec
.tx_pkts
)
325 label
: ['%s: %%1024.2mB'.format(rec
[col
] || _('other')), cell
],
330 label
: ['%s: %%1024.2mB'.format(rec
[col
] || _('other')), cell
],
335 cbi_update_table(tooltip
.lastElementChild
, rows
);
337 pie(tooltip
.querySelector('#bubble-pie1'), rxData
);
338 pie(tooltip
.querySelector('#bubble-pie2'), txData
);
340 var mac
= key
.toUpperCase();
341 var name
= hostInfo
.hasOwnProperty(mac
) ? hostInfo
[mac
].name
: null;
344 for (var i
= 0; i
< detailData
.length
; i
++)
345 if ((name
= hostNames
[detailData
[i
].ip
]) !== undefined)
348 if (mac
!== '00:00:00:00:00:00') {
349 kpi(tooltip
.querySelector('#bubble-hostname'), name
);
350 kpi(tooltip
.querySelector('#bubble-vendor'), oui(mac
));
353 kpi(tooltip
.querySelector('#bubble-hostname'));
354 kpi(tooltip
.querySelector('#bubble-vendor'));
357 var rect
= this.getBoundingClientRect(), x
, y
;
359 if ('ontouchstart' in window
|| window
.innerWidth
<= 992) {
360 var vpHeight
= Math
.max(document
.documentElement
.clientHeight
, window
.innerHeight
|| 0),
361 scrollFrom
= window
.pageYOffset
,
362 scrollTo
= scrollFrom
+ rect
.top
- vpHeight
* 0.5,
365 tooltip
.style
.top
= (rect
.top
+ rect
.height
+ window
.pageYOffset
) + 'px';
366 tooltip
.style
.left
= 0;
368 var scrollStep = function(timestamp
) {
372 var duration
= Math
.max(timestamp
- start
, 1);
373 if (duration
< 100) {
374 document
.body
.scrollTop
= scrollFrom
+ (scrollTo
- scrollFrom
) * (duration
/ 100);
375 window
.requestAnimationFrame(scrollStep
);
378 document
.body
.scrollTop
= scrollTo
;
382 window
.requestAnimationFrame(scrollStep
);
385 x
= rect
.left
+ rect
.width
+ window
.pageXOffset
,
386 y
= rect
.top
+ window
.pageYOffset
;
388 if ((y
+ tooltip
.offsetHeight
) > (window
.innerHeight
+ window
.pageYOffset
))
389 y
-= ((y
+ tooltip
.offsetHeight
) - (window
.innerHeight
+ window
.pageYOffset
));
391 tooltip
.style
.top
= y
+ 'px';
392 tooltip
.style
.left
= x
+ 'px';
398 function formatHostname(dns
)
400 if (dns
=== undefined || dns
=== null || dns
=== '')
403 dns
= dns
.split('.')[0];
406 return '<span title="%q">%h…</span>'.format(dns
, dns
.substr(0, 12));
408 return '%h'.format(dns
);
411 function renderHostData()
413 var trafData
= [], connData
= [];
414 var rx_total
= 0, tx_total
= 0, conn_total
= 0;
416 var hostData
= query(
418 return (r
[c
.rx_bytes
] > 0 || r
[c
.tx_bytes
] > 0);
422 // return (r[c.mac] !== '00:00:00:00:00:00') ? r[c.mac] : r[c.ip];
425 return ((r2
.rx_bytes
+ r2
.tx_bytes
) - (r1
.rx_bytes
+ r1
.tx_bytes
));
431 for (var i
= 0; i
< hostData
.length
; i
++) {
432 var rec
= hostData
[i
],
433 mac
= rec
.mac
.toUpperCase(),
434 key
= (mac
!== '00:00:00:00:00:00') ? mac
: rec
.ip
,
435 dns
= hostInfo
[mac
] ? hostInfo
[mac
].name
: null;
437 var cell
= E('div', formatHostname(dns
));
442 'href': '#' + rec
.mac
,
444 'data-tooltip': _('Source IP')
445 }, (mac
!== '00:00:00:00:00:00') ? mac
: _('other')),
447 'href': '#' + rec
.mac
,
448 'data-col': 'layer7',
449 'data-tooltip': _('Protocol')
450 }, '%1000.2m'.format(rec
.conns
)),
451 '%1024.2mB'.format(rec
.rx_bytes
),
452 '%1000.2mP'.format(rec
.rx_pkts
),
453 '%1024.2mB'.format(rec
.tx_bytes
),
454 '%1000.2mP'.format(rec
.tx_pkts
)
458 value
: rec
.rx_bytes
+ rec
.tx_bytes
,
459 label
: ["%s: %%.2mB".format(key
), cell
]
464 label
: ["%s: %%.2m".format(key
), cell
]
467 rx_total
+= rec
.rx_bytes
;
468 tx_total
+= rec
.tx_bytes
;
469 conn_total
+= rec
.conns
;
472 cbi_update_table('#host-data', rows
, E('em', [
473 _('No data recorded yet.'), ' ',
474 E('a', { 'href': L
.url('admin/nlbw/commit') }, _('Force reload…'))
477 pie('traf-pie', trafData
);
478 pie('conn-pie', connData
);
480 kpi('rx-total', '%1024.2mB'.format(rx_total
));
481 kpi('tx-total', '%1024.2mB'.format(tx_total
));
482 kpi('conn-total', '%1000m'.format(conn_total
));
483 kpi('host-total', '%u'.format(hostData
.length
));
486 function renderLayer7Data()
488 var rxData
= [], txData
= [];
489 var topConn
= [[0],[0],[0]], topRx
= [[0],[0],[0]], topTx
= [[0],[0],[0]];
491 var layer7Data
= query(
494 return ((r2
.rx_bytes
+ r2
.tx_bytes
) - (r1
.rx_bytes
+ r1
.tx_bytes
));
500 for (var i
= 0, c
= 0; i
< layer7Data
.length
; i
++) {
501 var rec
= layer7Data
[i
],
502 cell
= E('div', rec
.layer7
|| _('other'));
506 '%1000m'.format(rec
.conns
),
507 '%1024.2mB'.format(rec
.rx_bytes
),
508 '%1000.2mP'.format(rec
.rx_pkts
),
509 '%1024.2mB'.format(rec
.tx_bytes
),
510 '%1000.2mP'.format(rec
.tx_pkts
)
515 label
: ["%s: %%.2mB".format(rec
.layer7
|| _('other')), cell
]
520 label
: ["%s: %%.2mB".format(rec
.layer7
|| _('other')), cell
]
524 topRx
.push([rec
.rx_bytes
, rec
.layer7
]);
525 topTx
.push([rec
.tx_bytes
, rec
.layer7
]);
526 topConn
.push([rec
.conns
, rec
.layer7
]);
530 cbi_update_table('#layer7-data', rows
, E('em', [
531 _('No data recorded yet.'), ' ',
532 E('a', { 'href': L
.url('admin/nlbw/commit') }, _('Force reload…'))
535 pie('layer7-rx-pie', rxData
);
536 pie('layer7-tx-pie', txData
);
538 topRx
.sort(function(a
, b
) { return b
[0] - a
[0] });
539 topTx
.sort(function(a
, b
) { return b
[0] - a
[0] });
540 topConn
.sort(function(a
, b
) { return b
[0] - a
[0] });
542 kpi('layer7-total', layer7Data
.length
);
543 kpi('layer7-most-rx', topRx
[0][1], topRx
[1][1], topRx
[2][1]);
544 kpi('layer7-most-tx', topTx
[0][1], topTx
[1][1], topTx
[2][1]);
545 kpi('layer7-most-conn', topConn
[0][1], topConn
[1][1], topConn
[2][1]);
548 function renderIPv6Data()
562 null, ['family', 'mac'],
564 return ((r2
.rx_bytes
+ r2
.tx_bytes
) - (r1
.rx_bytes
+ r1
.tx_bytes
));
568 for (var i
= 0, c
= 0; i
< ipv6Data
.length
; i
++) {
569 var rec
= ipv6Data
[i
],
570 mac
= rec
.mac
.toUpperCase(),
572 fam
= families
[mac
] || 0,
573 recs
= records
[mac
] || {};
575 if (rec
.family
== 4) {
576 rx4_total
+= rec
.rx_bytes
;
577 tx4_total
+= rec
.tx_bytes
;
581 rx6_total
+= rec
.rx_bytes
;
582 tx6_total
+= rec
.tx_bytes
;
586 recs
[rec
.family
] = rec
;
592 for (var mac
in families
) {
593 switch (families
[mac
])
611 for (var mac
in records
) {
612 if (mac
=== '00:00:00:00:00:00')
615 var dns
= hostInfo
[mac
] ? hostInfo
[mac
].name
: null,
616 rec4
= records
[mac
][4],
617 rec6
= records
[mac
][6];
622 [ E('span', _('IPv4')),
623 E('span', _('IPv6')) ],
624 [ E('span', rec4
? '%1024.2mB'.format(rec4
.rx_bytes
) : '-'),
625 E('span', rec6
? '%1024.2mB'.format(rec6
.rx_bytes
) : '-') ],
626 [ E('span', rec4
? '%1000.2mP'.format(rec4
.rx_pkts
) : '-'),
627 E('span', rec6
? '%1000.2mP'.format(rec6
.rx_pkts
) : '-') ],
628 [ E('span', rec4
? '%1024.2mB'.format(rec4
.tx_bytes
) : '-'),
629 E('span', rec6
? '%1024.2mB'.format(rec6
.tx_bytes
) : '-') ],
630 [ E('span', rec4
? '%1000.2mP'.format(rec4
.tx_pkts
) : '-'),
631 E('span', rec6
? '%1000.2mP'.format(rec6
.tx_pkts
) : '-') ]
635 cbi_update_table('#ipv6-data', rows
, E('em', [
636 _('No data recorded yet.'), ' ',
637 E('a', { 'href': L
.url('admin/nlbw/commit') }, _('Force reload…'))
640 var shareData
= [], hostsData
= [];
642 if (rx4_total
> 0 || tx4_total
> 0)
644 value
: rx4_total
+ tx4_total
,
645 label
: ["IPv4: %.2mB"],
646 color
: 'hsl(140, 100%, 50%)'
649 if (rx6_total
> 0 || tx6_total
> 0)
651 value
: rx6_total
+ tx6_total
,
652 label
: ["IPv6: %.2mB"],
653 color
: 'hsl(180, 100%, 50%)'
659 label
: [_('%d IPv4-only hosts')],
660 color
: 'hsl(140, 100%, 50%)'
666 label
: [_('%d IPv6-only hosts')],
667 color
: 'hsl(180, 100%, 50%)'
673 label
: [_('%d dual-stack hosts')],
674 color
: 'hsl(50, 100%, 50%)'
677 pie('ipv6-share-pie', shareData
);
678 pie('ipv6-hosts-pie', hostsData
);
680 kpi('ipv6-hosts', '%.2f%%'.format(100 / (ds_total
+ v4_total
+ v6_total
) * (ds_total
+ v6_total
)));
681 kpi('ipv6-share', '%.2f%%'.format(100 / (rx4_total
+ rx6_total
+ tx4_total
+ tx6_total
) * (rx6_total
+ tx6_total
)));
682 kpi('ipv6-rx', '%1024.2mB'.format(rx6_total
));
683 kpi('ipv6-tx', '%1024.2mB'.format(tx6_total
));