luci-app-lldpd: Create from Tano Systems
[project/luci.git] / applications / luci-app-lldpd / htdocs / luci-static / resources / view / lldpd / status.js
1 /*
2 * Copyright (c) 2020 Tano Systems. All Rights Reserved.
3 * Author: Anton Kikin <a.kikin@tano-systems.com>
4 */
5
6 'use strict';
7 'require rpc';
8 'require form';
9 'require lldpd';
10 'require dom';
11 'require poll';
12
13 var callLLDPStatus = rpc.declare({
14 object: 'lldpd',
15 method: 'getStatus',
16 expect: {}
17 });
18
19 var dataMap = {
20 local: {
21 localChassis: null,
22 },
23 remote: {
24 neightbors: null,
25 statistics: null,
26 },
27 };
28
29 return L.view.extend({
30 __init__: function() {
31 this.super('__init__', arguments);
32
33 this.rowsUnfolded = {};
34
35 this.tableNeighbors = E('div', { 'class': 'table lldpd-table' }, [
36 E('div', { 'class': 'tr table-titles' }, [
37 E('div', { 'class': 'th left top' }, _('Local interface')),
38 E('div', { 'class': 'th left top' }, _('Protocol')),
39 E('div', { 'class': 'th left top' }, _('Discovered chassis')),
40 E('div', { 'class': 'th left top' }, _('Discovered port')),
41 ]),
42 E('div', { 'class': 'tr center placeholder' }, [
43 E('div', { 'class': 'td' }, E('em', { 'class': 'spinning' },
44 _('Collecting data...'))),
45 ])
46 ]);
47
48 this.tableStatistics = E('div', { 'class': 'table lldpd-table' }, [
49 E('div', { 'class': 'tr table-titles' }, [
50 E('div', { 'class': 'th left top' }, _('Local interface')),
51 E('div', { 'class': 'th left top' }, _('Protocol')),
52 E('div', { 'class': 'th left top' }, _('Administrative Status')),
53 E('div', { 'class': 'th right top' }, _('Tx')),
54 E('div', { 'class': 'th right top' }, _('Rx')),
55 E('div', { 'class': 'th right top' }, _('Tx discarded')),
56 E('div', { 'class': 'th right top' }, _('Rx unrecognized')),
57 E('div', { 'class': 'th right top' }, _('Ageout count')),
58 E('div', { 'class': 'th right top' }, _('Insert count')),
59 E('div', { 'class': 'th right top' }, _('Delete count')),
60 ]),
61 E('div', { 'class': 'tr center placeholder' }, [
62 E('div', { 'class': 'td' }, E('em', { 'class': 'spinning' },
63 _('Collecting data...'))),
64 ])
65 ]);
66
67 // Inject CSS
68 var head = document.getElementsByTagName('head')[0];
69 var css = E('link', { 'href':
70 L.resource('lldpd/lldpd.css')
71 + '?v=#PKG_VERSION', 'rel': 'stylesheet' });
72
73 head.appendChild(css);
74 },
75
76 load: function() {
77 return Promise.all([
78 L.resolveDefault(callLLDPStatus(), {}),
79 lldpd.init(),
80 ]);
81 },
82
83 /** @private */
84 renderParam: function(param, value) {
85 if (typeof value === 'undefined')
86 return '';
87
88 return E('div', {}, [
89 E('span', { 'class': 'lldpd-param' }, param),
90 E('span', { 'class': 'lldpd-param-value' }, value)
91 ]);
92 },
93
94 /** @private */
95 renderAge: function(v) {
96 if (typeof v === 'undefined')
97 return "&#8211;";
98
99 return E('nobr', {}, v);
100 },
101
102 /** @private */
103 renderIdType: function(v) {
104 if (typeof v === 'undefined')
105 return "&#8211;";
106
107 if (v == 'mac')
108 return _('MAC address');
109 else if (v == 'ifname')
110 return _('Interface name');
111 else if (v == 'local')
112 return _('Local ID');
113 else if (v == 'ip')
114 return _('IP address');
115
116 return v;
117 },
118
119 /** @private */
120 renderProtocol: function(v) {
121 if (typeof v === 'undefined' || v == 'unknown')
122 return "&#8211;";
123
124 if (v == 'LLDP')
125 return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-lldp' }, v);
126 else if ((v == 'CDPv1') || (v == 'CDPv2'))
127 return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-cdp' }, v);
128 else if (v == 'FDP')
129 return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-fdp' }, v);
130 else if (v == 'EDP')
131 return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-edp' }, v);
132 else if (v == 'SONMP')
133 return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-sonmp' }, v);
134 else
135 return E('span', { 'class': 'lldpd-protocol-badge' }, v);
136 },
137
138 /** @private */
139 renderAdminStatus: function(status) {
140 if ((typeof status === 'undefined') || !Array.isArray(status))
141 return '&#8211;';
142
143 if (status[0].value === 'RX and TX')
144 return _('Rx and Tx');
145 else if (status[0].value === 'RX only')
146 return _('Rx only');
147 else if (status[0].value === 'TX only')
148 return _('Tx only');
149 else if (status[0].value === 'disabled')
150 return _('Disabled');
151 else
152 return _('Unknown');
153 },
154
155 /** @private */
156 renderNumber: function(v) {
157 if (parseInt(v))
158 return v;
159
160 return '&#8211;';
161 },
162
163 /** @private */
164 renderPort: function(port) {
165 if (typeof port.port !== 'undefined')
166 {
167 if (typeof port.port[0].descr !== 'undefined' &&
168 typeof port.port[0].id[0].value !== 'undefined' &&
169 port.port[0].descr[0].value !== port.port[0].id[0].value)
170 {
171 return [
172 E('strong', {}, port.port[0].descr[0].value),
173 E('br', {}),
174 port.port[0].id[0].value
175 ];
176 }
177 else
178 {
179 if (typeof port.port[0].descr !== 'undefined')
180 return port.port[0].descr[0].value;
181 else
182 return port.port[0].id[0].value;
183 }
184 }
185 else
186 {
187 return '%s'.format(port.name);
188 }
189 },
190
191 /** @private */
192 renderPortParamTableShort: function(port) {
193 var items = [];
194
195 items.push(this.renderParam(_('Name'), port.name));
196 items.push(this.renderParam(_('Age'), this.renderAge(port.age)));
197
198 return E('div', { 'class': 'lldpd-params' }, items);
199 },
200
201 /** @private */
202 renderPortParamTable: function(port, only_id_and_ttl) {
203 var items = [];
204
205 if (!only_id_and_ttl) {
206 items.push(this.renderParam(_('Name'), port.name));
207 items.push(this.renderParam(_('Age'), this.renderAge(port.age)));
208 }
209
210 if (typeof port.port !== 'undefined')
211 {
212 if (typeof port.port[0].id !== 'undefined')
213 {
214 items.push(this.renderParam(_('Port ID'),
215 port.port[0].id[0].value));
216
217 items.push(this.renderParam(_('Port ID type'),
218 this.renderIdType(port.port[0].id[0].type)));
219 }
220
221 if (typeof port.port[0].descr !== 'undefined')
222 items.push(this.renderParam(_('Port description'),
223 port.port[0].descr[0].value));
224
225 if (typeof port.ttl !== 'undefined')
226 items.push(this.renderParam(_('TTL'), port.ttl[0].ttl));
227 else if (port.port[0].ttl !== 'undefined')
228 items.push(this.renderParam(_('TTL'), port.port[0].ttl[0].value));
229
230 if (typeof port.port[0].mfs !== 'undefined')
231 items.push(this.renderParam(_('MFS'), port.port[0].mfs[0].value));
232 }
233
234 return E('div', { 'class': 'lldpd-params' }, items);
235 },
236
237 /** @private */
238 renderChassis: function(ch) {
239 if (typeof ch.name !== 'undefined' &&
240 typeof ch.descr !== 'undefined' &&
241 typeof ch.name[0].value !== 'undefined' &&
242 typeof ch.descr[0].value !== 'undefined')
243 {
244 return [
245 E('strong', {}, ch.name[0].value),
246 E('br', {}),
247 ch.descr[0].value
248 ];
249 }
250 else if (typeof ch.name !== 'undefined' &&
251 typeof ch.name[0].value !== 'undefined')
252 return E('strong', {}, ch.name[0].value);
253 else if (typeof ch.descr !== 'undefined' &&
254 typeof ch.descr[0].value !== 'undefined')
255 return ch.descr[0].value;
256 else if (typeof ch.id !== 'undefined' &&
257 typeof ch.id[0].value !== 'undefined')
258 return ch.id[0].value;
259 else
260 return _('Unknown');
261 },
262
263 /** @private */
264 renderChassisParamTable: function(ch) {
265 var items = [];
266
267 if (typeof ch.name !== 'undefined')
268 items.push(this.renderParam(_('Name'), ch.name[0].value));
269
270 if (typeof ch.descr !== 'undefined')
271 items.push(this.renderParam(_('Description'), ch.descr[0].value));
272
273 if (typeof ch.id !== 'undefined') {
274 items.push(this.renderParam(_('ID'), ch.id[0].value));
275 items.push(this.renderParam(_('ID type'),
276 this.renderIdType(ch.id[0].type)));
277 }
278
279 // Management addresses
280 if (typeof ch['mgmt-ip'] !== 'undefined') {
281 var ips = '';
282
283 if (ch['mgmt-ip'].length > 0) {
284 // Array of addresses
285 for (var ip = 0; ip < ch["mgmt-ip"].length; ip++)
286 ips += ch['mgmt-ip'][ip].value + '<br />';
287 }
288 else {
289 // One address
290 ips += ch['mgmt-ip'][0].value;
291 }
292
293 items.push(this.renderParam(_('Management IP(s)'), ips));
294 }
295
296 if (typeof ch.capability !== 'undefined') {
297 var caps = '';
298
299 if (ch.capability.length > 0)
300 {
301 // Array of capabilities
302 for (var cap = 0; cap < ch.capability.length; cap++) {
303 caps += ch.capability[cap].type;
304 caps += ' (' + (ch.capability[cap].enabled
305 ? _('enabled') : _('disabled')) + ')';
306 caps += '<br />';
307 }
308 }
309 else
310 {
311 // One capability
312 caps += ch.capability[0].type;
313 caps += ' (' + (ch.capability[0].enabled
314 ? _('enabled') : _('disabled')) + ')';
315 }
316
317 items.push(this.renderParam(_('Capabilities'), caps));
318 }
319
320 return E('div', { 'class': 'lldpd-params' }, items);
321 },
322
323 /** @private */
324 getFoldingImage: function(unfolded) {
325 return L.resource('lldpd/details_' +
326 (unfolded ? 'hide' : 'show') + '.svg');
327 },
328
329 /** @private */
330 generateRowId: function(str) {
331 return str.replace(/[^a-z0-9]/gi, '-');
332 },
333
334 /** @private */
335 handleToggleFoldingRow: function(row, row_id) {
336 var e_img = row.querySelector('img');
337 var e_folded = row.querySelectorAll('.lldpd-folded');
338 var e_unfolded = row.querySelectorAll('.lldpd-unfolded');
339
340 if (e_folded.length != e_unfolded.length)
341 return;
342
343 var do_unfold = (e_folded[0].style.display !== 'none');
344 this.rowsUnfolded[row_id] = do_unfold;
345
346 for (var i = 0; i < e_folded.length; i++)
347 {
348 if (do_unfold)
349 {
350 e_folded[i].style.display = 'none';
351 e_unfolded[i].style.display = 'block';
352 }
353 else
354 {
355 e_folded[i].style.display = 'block';
356 e_unfolded[i].style.display = 'none';
357 }
358 }
359
360 e_img.src = this.getFoldingImage(do_unfold);
361 },
362
363 /** @private */
364 makeFoldingTableRow: function(row, unfolded) {
365 //
366 // row[0] - row id
367 // row[1] - contents for first cell in row
368 // row[2] - contents for second cell in row
369 // ...
370 // row[N] - contents for N-th cell in row
371 //
372 if (row.length < 2)
373 return row;
374
375 for (let i = 1; i < row.length; i++) {
376 if (i == 1) {
377 // Fold/unfold image appears only in first column
378 var dImg = E('div', { 'style': 'padding: 0 8px 0 0;' }, [
379 E('img', { 'width': '16px', 'src': this.getFoldingImage(unfolded) }),
380 ]);
381 }
382
383 if (Array.isArray(row[i])) {
384 // row[i][0] = folded contents
385 // row[i][1] = unfolded contents
386
387 // Folded cell data
388 let dFolded = E('div', {
389 'class': 'lldpd-folded',
390 'style': unfolded ? 'display: none;' : 'display: block;'
391 }, row[i][0]);
392
393 // Unfolded cell data
394 let dUnfolded = E('div', {
395 'class': 'lldpd-unfolded',
396 'style': unfolded ? 'display: block;' : 'display: none;'
397 }, row[i][1]);
398
399 if (i == 1) {
400 row[i] = E('div', {
401 'style': 'display: flex; flex-wrap: nowrap;'
402 }, [ dImg, dFolded, dUnfolded ]);
403 }
404 else {
405 row[i] = E('div', {}, [ dFolded, dUnfolded ]);
406 }
407 }
408 else {
409 // row[i] = same content for folded and unfolded states
410
411 if (i == 1) {
412 row[i] = E('div', {
413 'style': 'display: flex; flex-wrap: nowrap;'
414 }, [ dImg, E('div', row[i]) ]);
415 }
416 }
417 }
418
419 return row;
420 },
421
422 /** @private */
423 makeNeighborsTableRow: function(obj) {
424 if (typeof obj === 'undefined')
425 obj.name = 'Unknown';
426
427 var new_id = obj.name + '-' + obj.rid;
428
429 if (typeof obj.port !== 'undefined') {
430 if (typeof obj.port[0].id !== 'undefined')
431 new_id += "-" + obj.port[0].id[0].value;
432
433 if (typeof obj.port[0].descr !== 'undefined')
434 new_id += "-" + obj.port[0].descr[0].value;
435 }
436
437 var row_id = this.generateRowId(new_id);
438
439 return this.makeFoldingTableRow([
440 row_id,
441 [
442 '%s'.format(obj.name),
443 this.renderPortParamTableShort(obj)
444 ],
445 this.renderProtocol(obj.via),
446 [
447 this.renderChassis(obj.chassis[0]),
448 this.renderChassisParamTable(obj.chassis[0])
449 ],
450 [
451 this.renderPort(obj),
452 this.renderPortParamTable(obj, true)
453 ]
454 ], this.rowsUnfolded[row_id] || false);
455 },
456
457 /** @private */
458 renderInterfaceProtocols: function(iface, neighbors) {
459 if ((typeof iface === 'undefined') ||
460 (typeof neighbors == 'undefined') ||
461 (typeof neighbors.lldp[0] === 'undefined') ||
462 (typeof neighbors.lldp[0].interface === 'undefined'))
463 return "&#8211;";
464
465 var name = iface.name;
466 var protocols = [];
467
468 /* Search protocols for interface <name> */
469 neighbors.lldp[0].interface.forEach(function(n) {
470 if (n.name !== name)
471 return;
472
473 protocols.push(this.renderProtocol(n.via));
474 }.bind(this));
475
476 if (protocols.length > 0)
477 return E('span', {}, protocols);
478 else
479 return "&#8211;";
480 },
481
482 /** @private */
483 makeStatisticsTableRow: function(sobj, iobj, neighbors) {
484 var row_id = this.generateRowId(iobj.name);
485
486 return this.makeFoldingTableRow([
487 row_id,
488 [
489 this.renderPort(iobj), // folded
490 this.renderPortParamTable(iobj, false) // unfolded
491 ],
492 this.renderInterfaceProtocols(iobj, neighbors),
493 this.renderAdminStatus(iobj.status),
494 this.renderNumber(sobj.tx[0].tx),
495 this.renderNumber(sobj.rx[0].rx),
496 this.renderNumber(sobj.rx_discarded_cnt[0].rx_discarded_cnt),
497 this.renderNumber(sobj.rx_unrecognized_cnt[0].rx_unrecognized_cnt),
498 this.renderNumber(sobj.ageout_cnt[0].ageout_cnt),
499 this.renderNumber(sobj.insert_cnt[0].insert_cnt),
500 this.renderNumber(sobj.delete_cnt[0].delete_cnt)
501 ], this.rowsUnfolded[row_id] || false);
502 },
503
504 /** @private */
505 updateTable: function(table, data, placeholder) {
506 var target = isElem(table) ? table : document.querySelector(table);
507
508 if (!isElem(target))
509 return;
510
511 target.querySelectorAll(
512 '.tr.table-titles, .cbi-section-table-titles').forEach(L.bind(function(thead) {
513 var titles = [];
514
515 thead.querySelectorAll('.th').forEach(function(th) {
516 titles.push(th);
517 });
518
519 if (Array.isArray(data)) {
520 var n = 0, rows = target.querySelectorAll('.tr');
521
522 data.forEach(L.bind(function(row) {
523 var id = row[0];
524 var trow = E('div', { 'class': 'tr', 'click': L.bind(function(ev) {
525 this.handleToggleFoldingRow(ev.currentTarget, id);
526 // lldpd_folding_toggle(ev.currentTarget, id);
527 }, this) });
528
529 for (var i = 0; i < titles.length; i++) {
530 var text = (titles[i].innerText || '').trim();
531 var td = trow.appendChild(E('div', {
532 'class': titles[i].className,
533 'data-title': (text !== '') ? text : null
534 }, row[i + 1] || ''));
535
536 td.classList.remove('th');
537 td.classList.add('td');
538 }
539
540 trow.classList.add('cbi-rowstyle-%d'.format((n++ % 2) ? 2 : 1));
541
542 if (rows[n])
543 target.replaceChild(trow, rows[n]);
544 else
545 target.appendChild(trow);
546 }, this));
547
548 while (rows[++n])
549 target.removeChild(rows[n]);
550
551 if (placeholder && target.firstElementChild === target.lastElementChild) {
552 var trow = target.appendChild(
553 E('div', { 'class': 'tr placeholder' }));
554
555 var td = trow.appendChild(
556 E('div', { 'class': 'center ' + titles[0].className }, placeholder));
557
558 td.classList.remove('th');
559 td.classList.add('td');
560 }
561 } else {
562 thead.parentNode.style.display = 'none';
563
564 thead.parentNode.querySelectorAll('.tr, .cbi-section-table-row').forEach(function(trow) {
565 if (trow !== thead) {
566 var n = 0;
567 trow.querySelectorAll('.th, .td').forEach(function(td) {
568 if (n < titles.length) {
569 var text = (titles[n++].innerText || '').trim();
570 if (text !== '')
571 td.setAttribute('data-title', text);
572 }
573 });
574 }
575 });
576
577 thead.parentNode.style.display = '';
578 }
579 }, this));
580 },
581
582 /** @private */
583 startPolling: function() {
584 poll.add(L.bind(function() {
585 return callLLDPStatus().then(L.bind(function(data) {
586 this.renderData(data);
587 }, this));
588 }, this));
589 },
590
591 /** @private */
592 renderDataLocalChassis: function(data) {
593 if (data &&
594 typeof data !== 'undefined' &&
595 typeof data['local-chassis'] !== 'undefined' &&
596 typeof data['local-chassis'][0].chassis[0].name !== 'undefined') {
597 return this.renderChassisParamTable(data['local-chassis'][0].chassis[0]);
598 }
599 else {
600 return E('div', { 'class': 'alert-message warning' },
601 _('No data to display'));
602 }
603 },
604
605 /** @private */
606 renderDataNeighbors: function(neighbors) {
607 var rows = [];
608
609 if (neighbors &&
610 typeof neighbors !== 'undefined' &&
611 typeof neighbors.lldp !== 'undefined')
612 {
613 var ifaces = neighbors.lldp[0].interface;
614
615 // Fill table rows
616 if (typeof ifaces !== 'undefined') {
617 for (i = 0; i < ifaces.length; i++)
618 rows.push(this.makeNeighborsTableRow(ifaces[i]));
619 }
620 }
621
622 return rows;
623 },
624
625 /** @private */
626 renderDataStatistics: function(statistics, interfaces, neighbors) {
627 var rows = [];
628
629 if (statistics &&
630 interfaces &&
631 typeof statistics !== 'undefined' &&
632 typeof interfaces !== 'undefined' &&
633 typeof statistics.lldp !== 'undefined' &&
634 typeof interfaces.lldp !== 'undefined')
635 {
636 var sifaces = statistics.lldp[0].interface;
637 var ifaces = interfaces.lldp[0].interface;
638
639 if ((typeof sifaces !== 'undefined') &&
640 (typeof ifaces !== 'undefined')) {
641 for (var i = 0; i < sifaces.length; i++)
642 rows.push(this.makeStatisticsTableRow(sifaces[i], ifaces[i], neighbors));
643 }
644 }
645
646 return rows;
647 },
648
649 /** @private */
650 renderData: function(data) {
651 var r;
652
653 r = this.renderDataLocalChassis(data.chassis);
654 dom.content(document.getElementById('lldpd-local-chassis'), r);
655
656 r = this.renderDataNeighbors(data.neighbors);
657 this.updateTable(this.tableNeighbors, r,
658 _('No data to display'));
659
660 r = this.renderDataStatistics(data.statistics, data.interfaces, data.neighbors);
661 this.updateTable(this.tableStatistics, r,
662 _('No data to display'));
663 },
664
665 render: function(data) {
666 var m, s, ss, o;
667
668 m = new form.JSONMap(dataMap,
669 _('LLDP Status'),
670 _('This page allows you to see discovered LLDP neighbors, ' +
671 'local interfaces statistics and local chassis information.'));
672
673 s = m.section(form.NamedSection, 'local', 'local',
674 _('Local Chassis'));
675
676 o = s.option(form.DummyValue, 'localChassis');
677 o.render = function() {
678 return E('div', { 'id': 'lldpd-local-chassis' }, [
679 E('em', { 'class': 'spinning' }, _('Collecting data...'))
680 ]);
681 };
682
683 s = m.section(form.NamedSection, 'remote', 'remote');
684
685 s.tab('neighbors', _('Discovered Neighbors'));
686 s.tab('statistics', _('Interface Statistics'));
687
688 o = s.taboption('neighbors', form.DummyValue, 'neighbors');
689 o.render = L.bind(function() {
690 return E('div', { 'class': 'table-wrapper' }, [
691 this.tableNeighbors
692 ]);
693 }, this);
694
695 o = s.taboption('statistics', form.DummyValue, 'statistics');
696 o.render = L.bind(function() {
697 return E('div', { 'class': 'table-wrapper' }, [
698 this.tableStatistics
699 ]);
700 }, this);
701
702 return m.render().then(L.bind(function(rendered) {
703 this.startPolling();
704 return rendered;
705 }, this));
706 },
707
708 handleSaveApply: null,
709 handleSave: null,
710 handleReset: null
711 });