8a01b1db49babd53c18e5f155769644141faa9c2
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 ymd1
= e
.split(/-/);
230 var d1
= new Date(+ymd1
[0], +ymd1
[1] - 1, +ymd1
[2]);
234 ymd2
= trafficPeriods
[i
- 1].split(/-/);
235 d2
= new Date(+ymd2
[0], +ymd2
[1] - 1, +ymd2
[2]);
236 d2
.setDate(d2
.getDate() - 1);
244 var opt
= document
.createElement('option');
245 opt
.setAttribute('data-duration', (d2
.getTime() - d1
.getTime()) / 1000);
247 opt
.text
= '%04d-%02d-%02d - %04d-%02d-%02d'.format(
248 d1
.getFullYear(), d1
.getMonth() + 1, d1
.getDate(),
249 d2
.getFullYear(), d2
.getMonth() + 1, d2
.getDate());
251 sel
.appendChild(opt
);
254 sel
.selectedIndex
= sel
.childNodes
.length
- 1;
255 sel
.style
.display
= '';
257 sel
.onchange = function(ev
) {
259 fetchData(sel
.options
[sel
.selectedIndex
].value
);
263 function renderHostDetail(tooltip
)
265 var key
= this.getAttribute('href').substr(1),
266 col
= this.getAttribute('data-col'),
267 label
= this.getAttribute('data-tooltip');
269 var detailData
= query(
271 return ((r
[c
.mac
] === key
|| r
[c
.ip
] === key
) &&
272 (r
[c
.rx_bytes
] > 0 || r
[c
.tx_bytes
] > 0));
276 return ((r2
.rx_bytes
+ r2
.tx_bytes
) - (r1
.rx_bytes
+ r1
.tx_bytes
));
280 var rxData
= [], txData
= [];
282 L
.dom
.content(tooltip
, [
283 E('div', { 'class': 'head' }, [
284 E('div', { 'class': 'pie' }, [
285 E('label', _('Download')),
286 E('canvas', { 'id': 'bubble-pie1', 'width': 100, 'height': 100 })
288 E('div', { 'class': 'pie' }, [
289 E('label', _('Upload')),
290 E('canvas', { 'id': 'bubble-pie2', 'width': 100, 'height': 100 })
292 E('div', { 'class': 'kpi' }, [
294 E('li', _('Hostname: <big id="bubble-hostname">example.org</big>')),
295 E('li', _('Vendor: <big id="bubble-vendor">Example Corp.</big>'))
299 E('div', { 'class': 'table' }, [
300 E('div', { 'class': 'tr table-titles' }, [
301 E('div', { 'class': 'th' }, label
|| col
),
302 E('div', { 'class': 'th' }, _('Conn.')),
303 E('div', { 'class': 'th' }, _('Down. (Bytes)')),
304 E('div', { 'class': 'th' }, _('Down. (Pkts.)')),
305 E('div', { 'class': 'th' }, _('Up. (Bytes)')),
306 E('div', { 'class': 'th' }, _('Up. (Pkts.)')),
313 for (var i
= 0; i
< detailData
.length
; i
++) {
314 var rec
= detailData
[i
],
315 cell
= E('div', rec
[col
] || _('other'));
319 '%1000.2m'.format(rec
.conns
),
320 '%1024.2mB'.format(rec
.rx_bytes
),
321 '%1000.2mP'.format(rec
.rx_pkts
),
322 '%1024.2mB'.format(rec
.tx_bytes
),
323 '%1000.2mP'.format(rec
.tx_pkts
)
327 label
: ['%s: %%1024.2mB'.format(rec
[col
] || _('other')), cell
],
332 label
: ['%s: %%1024.2mB'.format(rec
[col
] || _('other')), cell
],
337 cbi_update_table(tooltip
.lastElementChild
, rows
);
339 pie(tooltip
.querySelector('#bubble-pie1'), rxData
);
340 pie(tooltip
.querySelector('#bubble-pie2'), txData
);
342 var mac
= key
.toUpperCase();
343 var name
= hostInfo
.hasOwnProperty(mac
) ? hostInfo
[mac
].name
: null;
346 for (var i
= 0; i
< detailData
.length
; i
++)
347 if ((name
= hostNames
[detailData
[i
].ip
]) !== undefined)
350 if (mac
!== '00:00:00:00:00:00') {
351 kpi(tooltip
.querySelector('#bubble-hostname'), name
);
352 kpi(tooltip
.querySelector('#bubble-vendor'), oui(mac
));
355 kpi(tooltip
.querySelector('#bubble-hostname'));
356 kpi(tooltip
.querySelector('#bubble-vendor'));
359 var rect
= this.getBoundingClientRect(), x
, y
;
361 if ('ontouchstart' in window
|| window
.innerWidth
<= 992) {
362 var vpHeight
= Math
.max(document
.documentElement
.clientHeight
, window
.innerHeight
|| 0),
363 scrollFrom
= window
.pageYOffset
,
364 scrollTo
= scrollFrom
+ rect
.top
- vpHeight
* 0.5,
367 tooltip
.style
.top
= (rect
.top
+ rect
.height
+ window
.pageYOffset
) + 'px';
368 tooltip
.style
.left
= 0;
370 var scrollStep = function(timestamp
) {
374 var duration
= Math
.max(timestamp
- start
, 1);
375 if (duration
< 100) {
376 document
.body
.scrollTop
= scrollFrom
+ (scrollTo
- scrollFrom
) * (duration
/ 100);
377 window
.requestAnimationFrame(scrollStep
);
380 document
.body
.scrollTop
= scrollTo
;
384 window
.requestAnimationFrame(scrollStep
);
387 x
= rect
.left
+ rect
.width
+ window
.pageXOffset
,
388 y
= rect
.top
+ window
.pageYOffset
;
390 if ((y
+ tooltip
.offsetHeight
) > (window
.innerHeight
+ window
.pageYOffset
))
391 y
-= ((y
+ tooltip
.offsetHeight
) - (window
.innerHeight
+ window
.pageYOffset
));
393 tooltip
.style
.top
= y
+ 'px';
394 tooltip
.style
.left
= x
+ 'px';
400 function formatHostname(dns
)
402 if (dns
=== undefined || dns
=== null || dns
=== '')
405 dns
= dns
.split('.')[0];
408 return '<span title="%q">%h…</span>'.format(dns
, dns
.substr(0, 12));
410 return '%h'.format(dns
);
413 function renderHostData()
415 var trafData
= [], connData
= [];
416 var rx_total
= 0, tx_total
= 0, conn_total
= 0;
418 var hostData
= query(
420 return (r
[c
.rx_bytes
] > 0 || r
[c
.tx_bytes
] > 0);
424 // return (r[c.mac] !== '00:00:00:00:00:00') ? r[c.mac] : r[c.ip];
427 return ((r2
.rx_bytes
+ r2
.tx_bytes
) - (r1
.rx_bytes
+ r1
.tx_bytes
));
433 for (var i
= 0; i
< hostData
.length
; i
++) {
434 var rec
= hostData
[i
],
435 mac
= rec
.mac
.toUpperCase(),
436 key
= (mac
!== '00:00:00:00:00:00') ? mac
: rec
.ip
,
437 dns
= hostInfo
[mac
] ? hostInfo
[mac
].name
: null;
439 var cell
= E('div', formatHostname(dns
));
444 'href': '#' + rec
.mac
,
446 'data-tooltip': _('Source IP')
447 }, (mac
!== '00:00:00:00:00:00') ? mac
: _('other')),
449 'href': '#' + rec
.mac
,
450 'data-col': 'layer7',
451 'data-tooltip': _('Protocol')
452 }, '%1000.2m'.format(rec
.conns
)),
453 '%1024.2mB'.format(rec
.rx_bytes
),
454 '%1000.2mP'.format(rec
.rx_pkts
),
455 '%1024.2mB'.format(rec
.tx_bytes
),
456 '%1000.2mP'.format(rec
.tx_pkts
)
460 value
: rec
.rx_bytes
+ rec
.tx_bytes
,
461 label
: ["%s: %%.2mB".format(key
), cell
]
466 label
: ["%s: %%.2m".format(key
), cell
]
469 rx_total
+= rec
.rx_bytes
;
470 tx_total
+= rec
.tx_bytes
;
471 conn_total
+= rec
.conns
;
474 cbi_update_table('#host-data', rows
, E('em', [
475 _('No data recorded yet.'), ' ',
476 E('a', { 'href': L
.url('admin/nlbw/commit') }, _('Force reload…'))
479 pie('traf-pie', trafData
);
480 pie('conn-pie', connData
);
482 kpi('rx-total', '%1024.2mB'.format(rx_total
));
483 kpi('tx-total', '%1024.2mB'.format(tx_total
));
484 kpi('conn-total', '%1000m'.format(conn_total
));
485 kpi('host-total', '%u'.format(hostData
.length
));
488 function renderLayer7Data()
490 var rxData
= [], txData
= [];
491 var topConn
= [[0],[0],[0]], topRx
= [[0],[0],[0]], topTx
= [[0],[0],[0]];
493 var layer7Data
= query(
496 return ((r2
.rx_bytes
+ r2
.tx_bytes
) - (r1
.rx_bytes
+ r1
.tx_bytes
));
502 for (var i
= 0, c
= 0; i
< layer7Data
.length
; i
++) {
503 var rec
= layer7Data
[i
],
504 cell
= E('div', rec
.layer7
|| _('other'));
508 '%1000m'.format(rec
.conns
),
509 '%1024.2mB'.format(rec
.rx_bytes
),
510 '%1000.2mP'.format(rec
.rx_pkts
),
511 '%1024.2mB'.format(rec
.tx_bytes
),
512 '%1000.2mP'.format(rec
.tx_pkts
)
517 label
: ["%s: %%.2mB".format(rec
.layer7
|| _('other')), cell
]
522 label
: ["%s: %%.2mB".format(rec
.layer7
|| _('other')), cell
]
526 topRx
.push([rec
.rx_bytes
, rec
.layer7
]);
527 topTx
.push([rec
.tx_bytes
, rec
.layer7
]);
528 topConn
.push([rec
.conns
, rec
.layer7
]);
532 cbi_update_table('#layer7-data', rows
, E('em', [
533 _('No data recorded yet.'), ' ',
534 E('a', { 'href': L
.url('admin/nlbw/commit') }, _('Force reload…'))
537 pie('layer7-rx-pie', rxData
);
538 pie('layer7-tx-pie', txData
);
540 topRx
.sort(function(a
, b
) { return b
[0] - a
[0] });
541 topTx
.sort(function(a
, b
) { return b
[0] - a
[0] });
542 topConn
.sort(function(a
, b
) { return b
[0] - a
[0] });
544 kpi('layer7-total', layer7Data
.length
);
545 kpi('layer7-most-rx', topRx
[0][1], topRx
[1][1], topRx
[2][1]);
546 kpi('layer7-most-tx', topTx
[0][1], topTx
[1][1], topTx
[2][1]);
547 kpi('layer7-most-conn', topConn
[0][1], topConn
[1][1], topConn
[2][1]);
550 function renderIPv6Data()
564 null, ['family', 'mac'],
566 return ((r2
.rx_bytes
+ r2
.tx_bytes
) - (r1
.rx_bytes
+ r1
.tx_bytes
));
570 for (var i
= 0, c
= 0; i
< ipv6Data
.length
; i
++) {
571 var rec
= ipv6Data
[i
],
572 mac
= rec
.mac
.toUpperCase(),
574 fam
= families
[mac
] || 0,
575 recs
= records
[mac
] || {};
577 if (rec
.family
== 4) {
578 rx4_total
+= rec
.rx_bytes
;
579 tx4_total
+= rec
.tx_bytes
;
583 rx6_total
+= rec
.rx_bytes
;
584 tx6_total
+= rec
.tx_bytes
;
588 recs
[rec
.family
] = rec
;
594 for (var mac
in families
) {
595 switch (families
[mac
])
613 for (var mac
in records
) {
614 if (mac
=== '00:00:00:00:00:00')
617 var dns
= hostInfo
[mac
] ? hostInfo
[mac
].name
: null,
618 rec4
= records
[mac
][4],
619 rec6
= records
[mac
][6];
624 [ E('span', _('IPv4')),
625 E('span', _('IPv6')) ],
626 [ E('span', rec4
? '%1024.2mB'.format(rec4
.rx_bytes
) : '-'),
627 E('span', rec6
? '%1024.2mB'.format(rec6
.rx_bytes
) : '-') ],
628 [ E('span', rec4
? '%1000.2mP'.format(rec4
.rx_pkts
) : '-'),
629 E('span', rec6
? '%1000.2mP'.format(rec6
.rx_pkts
) : '-') ],
630 [ E('span', rec4
? '%1024.2mB'.format(rec4
.tx_bytes
) : '-'),
631 E('span', rec6
? '%1024.2mB'.format(rec6
.tx_bytes
) : '-') ],
632 [ E('span', rec4
? '%1000.2mP'.format(rec4
.tx_pkts
) : '-'),
633 E('span', rec6
? '%1000.2mP'.format(rec6
.tx_pkts
) : '-') ]
637 cbi_update_table('#ipv6-data', rows
, E('em', [
638 _('No data recorded yet.'), ' ',
639 E('a', { 'href': L
.url('admin/nlbw/commit') }, _('Force reload…'))
642 var shareData
= [], hostsData
= [];
644 if (rx4_total
> 0 || tx4_total
> 0)
646 value
: rx4_total
+ tx4_total
,
647 label
: ["IPv4: %.2mB"],
648 color
: 'hsl(140, 100%, 50%)'
651 if (rx6_total
> 0 || tx6_total
> 0)
653 value
: rx6_total
+ tx6_total
,
654 label
: ["IPv6: %.2mB"],
655 color
: 'hsl(180, 100%, 50%)'
661 label
: [_('%d IPv4-only hosts')],
662 color
: 'hsl(140, 100%, 50%)'
668 label
: [_('%d IPv6-only hosts')],
669 color
: 'hsl(180, 100%, 50%)'
675 label
: [_('%d dual-stack hosts')],
676 color
: 'hsl(50, 100%, 50%)'
679 pie('ipv6-share-pie', shareData
);
680 pie('ipv6-hosts-pie', hostsData
);
682 kpi('ipv6-hosts', '%.2f%%'.format(100 / (ds_total
+ v4_total
+ v6_total
) * (ds_total
+ v6_total
)));
683 kpi('ipv6-share', '%.2f%%'.format(100 / (rx4_total
+ rx6_total
+ tx4_total
+ tx6_total
) * (rx6_total
+ tx6_total
)));
684 kpi('ipv6-rx', '%1024.2mB'.format(rx6_total
));
685 kpi('ipv6-tx', '%1024.2mB'.format(tx6_total
));