status.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. /*
  2. * Copyright (c) 2020 Tano Systems. All Rights Reserved.
  3. * Author: Anton Kikin <a.kikin@tano-systems.com>
  4. * Copyright (c) 2023-2024. All Rights Reserved.
  5. * Paul Donald <newtwen+github@gmail.com>
  6. */
  7. 'use strict';
  8. 'require rpc';
  9. 'require form';
  10. 'require lldpd';
  11. 'require dom';
  12. 'require poll';
  13. var callLLDPStatus = rpc.declare({
  14. object: 'luci.lldpd',
  15. method: 'getStatus',
  16. expect: {}
  17. });
  18. var dataMap = {
  19. local: {
  20. localChassis: null,
  21. },
  22. remote: {
  23. neighbors: null,
  24. statistics: null,
  25. },
  26. };
  27. return L.view.extend({
  28. __init__: function() {
  29. this.super('__init__', arguments);
  30. this.rowsUnfolded = {};
  31. this.tableNeighbors = E('div', { 'class': 'table lldpd-table' }, [
  32. E('div', { 'class': 'tr table-titles' }, [
  33. E('div', { 'class': 'th left top' }, _('Local interface')),
  34. E('div', { 'class': 'th left top' }, _('Protocol')),
  35. E('div', { 'class': 'th left top' }, _('Discovered chassis')),
  36. E('div', { 'class': 'th left top' }, _('Discovered port')),
  37. ]),
  38. E('div', { 'class': 'tr center placeholder' }, [
  39. E('div', { 'class': 'td' }, E('em', { 'class': 'spinning' },
  40. _('Collecting data...'))),
  41. ])
  42. ]);
  43. this.tableStatistics = E('div', { 'class': 'table lldpd-table' }, [
  44. E('div', { 'class': 'tr table-titles' }, [
  45. E('div', { 'class': 'th left top' }, _('Local interface')),
  46. E('div', { 'class': 'th left top' }, _('Protocol')),
  47. E('div', { 'class': 'th left top' }, _('Administrative Status')),
  48. E('div', { 'class': 'th right top' }, _('Tx')),
  49. E('div', { 'class': 'th right top' }, _('Rx')),
  50. E('div', { 'class': 'th right top' }, _('Tx discarded')),
  51. E('div', { 'class': 'th right top' }, _('Rx unrecognized')),
  52. E('div', { 'class': 'th right top' }, _('Ageout count')),
  53. E('div', { 'class': 'th right top' }, _('Insert count')),
  54. E('div', { 'class': 'th right top' }, _('Delete count')),
  55. ]),
  56. E('div', { 'class': 'tr center placeholder' }, [
  57. E('div', { 'class': 'td' }, E('em', { 'class': 'spinning' },
  58. _('Collecting data...'))),
  59. ])
  60. ]);
  61. // Inject CSS
  62. var head = document.getElementsByTagName('head')[0];
  63. var css = E('link', { 'href':
  64. L.resource('lldpd/lldpd.css')
  65. + '?v=#PKG_VERSION', 'rel': 'stylesheet' });
  66. head.appendChild(css);
  67. },
  68. load: function() {
  69. return Promise.all([
  70. L.resolveDefault(callLLDPStatus(), {}),
  71. lldpd.init(),
  72. ]);
  73. },
  74. /** @private */
  75. renderParam: function(param, value) {
  76. if (typeof value === 'undefined')
  77. return '';
  78. return E('div', {}, [
  79. E('span', { 'class': 'lldpd-param' }, param),
  80. E('span', { 'class': 'lldpd-param-value' }, value)
  81. ]);
  82. },
  83. /** @private */
  84. renderAge: function(v) {
  85. if (typeof v === 'undefined')
  86. return "&#8211;";
  87. return E('nobr', {}, v);
  88. },
  89. /** @private */
  90. renderIdType: function(v) {
  91. if (typeof v === 'undefined')
  92. return "&#8211;";
  93. if (v == 'mac')
  94. return _('MAC address');
  95. else if (v == 'ifname')
  96. return _('Interface name');
  97. else if (v == 'local')
  98. return _('Local ID');
  99. else if (v == 'ip')
  100. return _('IP address');
  101. return v;
  102. },
  103. /** @private */
  104. renderProtocol: function(v) {
  105. if (typeof v === 'undefined' || v == 'unknown')
  106. return "&#8211;";
  107. if (v == 'LLDP')
  108. return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-lldp' }, v);
  109. else if ((v == 'CDPv1') || (v == 'CDPv2'))
  110. return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-cdp' }, v);
  111. else if (v == 'FDP')
  112. return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-fdp' }, v);
  113. else if (v == 'EDP')
  114. return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-edp' }, v);
  115. else if (v == 'SONMP')
  116. return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-sonmp' }, v);
  117. else
  118. return E('span', { 'class': 'lldpd-protocol-badge' }, v);
  119. },
  120. /** @private */
  121. renderAdminStatus: function(status) {
  122. if ((typeof status === 'undefined') || !Array.isArray(status))
  123. return '&#8211;';
  124. if (status[0].value === 'RX and TX')
  125. return _('Rx and Tx');
  126. else if (status[0].value === 'RX only')
  127. return _('Rx only');
  128. else if (status[0].value === 'TX only')
  129. return _('Tx only');
  130. else if (status[0].value === 'disabled')
  131. return _('Disabled');
  132. else
  133. return _('Unknown');
  134. },
  135. /** @private */
  136. renderNumber: function(v) {
  137. if (parseInt(v))
  138. return v;
  139. return '&#8211;';
  140. },
  141. /** @private */
  142. renderPort: function(port) {
  143. const portData = port?.port?.[0];
  144. const descrValue = portData?.descr?.[0]?.value;
  145. const idValue = portData?.id?.[0]?.value;
  146. if (portData) {
  147. if (descrValue && idValue && descrValue !== idValue) {
  148. return [
  149. E('strong', {}, descrValue),
  150. E('br', {}),
  151. idValue
  152. ];
  153. }
  154. return descrValue ?? idValue;
  155. }
  156. return '%s'.format(port.name);
  157. },
  158. /** @private */
  159. renderPortParamTableShort: function(port) {
  160. var items = [];
  161. items.push(this.renderParam(_('Name'), port.name));
  162. items.push(this.renderParam(_('Age'), this.renderAge(port.age)));
  163. return E('div', { 'class': 'lldpd-params' }, items);
  164. },
  165. /** @private */
  166. renderPortParamTable: function(port, only_id_and_ttl) {
  167. const items = [];
  168. if (!only_id_and_ttl) {
  169. items.push(this.renderParam(_('Name'), port?.name));
  170. items.push(this.renderParam(_('Age'), this.renderAge(port?.age)));
  171. }
  172. const portData = port?.port?.[0];
  173. if (portData) {
  174. const portId = portData?.id?.[0];
  175. if (portId) {
  176. items.push(this.renderParam(_('Port ID'), portId?.value));
  177. items.push(this.renderParam(_('Port ID type'), this.renderIdType(portId?.type)));
  178. }
  179. if (portData?.descr?.[0]?.value)
  180. items.push(this.renderParam(_('Port description'), portData.descr[0].value));
  181. const ttlValue = port?.ttl?.[0]?.ttl ?? portData?.ttl?.[0]?.value;
  182. if (ttlValue)
  183. items.push(this.renderParam(_('TTL'), ttlValue));
  184. if (portData?.mfs?.[0]?.value)
  185. items.push(this.renderParam(_('MFS'), portData.mfs[0].value));
  186. }
  187. return E('div', { 'class': 'lldpd-params' }, items);
  188. },
  189. /** @private */
  190. renderChassis: function(ch) {
  191. const nameValue = ch?.name?.[0]?.value;
  192. const descrValue = ch?.descr?.[0]?.value;
  193. const idValue = ch?.id?.[0]?.value;
  194. if (nameValue && descrValue) {
  195. return [
  196. E('strong', {}, nameValue),
  197. E('br', {}),
  198. descrValue
  199. ];
  200. }
  201. if (nameValue)
  202. return E('strong', {}, nameValue);
  203. if (descrValue)
  204. return descrValue;
  205. if (idValue)
  206. return idValue;
  207. return _('Unknown');
  208. },
  209. /** @private */
  210. renderChassisParamTable: function(ch) {
  211. const items = [];
  212. // Add name and description if available
  213. const nameValue = ch?.name?.[0]?.value;
  214. if (nameValue)
  215. items.push(this.renderParam(_('Name'), nameValue));
  216. const descrValue = ch?.descr?.[0]?.value;
  217. if (descrValue)
  218. items.push(this.renderParam(_('Description'), descrValue));
  219. // Add ID and ID type if available
  220. const idValue = ch?.id?.[0]?.value;
  221. const idType = ch?.id?.[0]?.type;
  222. if (idValue) {
  223. items.push(this.renderParam(_('ID'), idValue));
  224. items.push(this.renderParam(_('ID type'), this.renderIdType(idType)));
  225. }
  226. // Management addresses
  227. const mgmtIps = ch?.['mgmt-ip'];
  228. if (mgmtIps?.length > 0) {
  229. const ips = mgmtIps.map(ip => ip.value).join('<br />');
  230. items.push(this.renderParam(_('Management IP(s)'), ips));
  231. }
  232. // Capabilities
  233. const capabilities = ch?.capability;
  234. if (capabilities?.length > 0) {
  235. const caps = capabilities.map(cap =>
  236. `${cap.type} (${cap.enabled ? _('enabled') : _('disabled')})`
  237. ).join('<br />');
  238. items.push(this.renderParam(_('Capabilities'), caps));
  239. }
  240. return E('div', { 'class': 'lldpd-params' }, items);
  241. },
  242. /** @private */
  243. getFoldingImage: function(unfolded) {
  244. return L.resource('lldpd/details_' +
  245. (unfolded ? 'hide' : 'show') + '.svg');
  246. },
  247. /** @private */
  248. generateRowId: function(str) {
  249. return str.replace(/[^a-z0-9]/gi, '-');
  250. },
  251. /** @private */
  252. handleToggleFoldingRow: function(row, row_id) {
  253. var e_img = row.querySelector('img');
  254. var e_folded = row.querySelectorAll('.lldpd-folded');
  255. var e_unfolded = row.querySelectorAll('.lldpd-unfolded');
  256. if (e_folded.length != e_unfolded.length)
  257. return;
  258. var do_unfold = (e_folded[0].style.display !== 'none');
  259. this.rowsUnfolded[row_id] = do_unfold;
  260. for (var i = 0; i < e_folded.length; i++)
  261. {
  262. if (do_unfold)
  263. {
  264. e_folded[i].style.display = 'none';
  265. e_unfolded[i].style.display = 'block';
  266. }
  267. else
  268. {
  269. e_folded[i].style.display = 'block';
  270. e_unfolded[i].style.display = 'none';
  271. }
  272. }
  273. e_img.src = this.getFoldingImage(do_unfold);
  274. },
  275. /** @private */
  276. makeFoldingTableRow: function(row, unfolded) {
  277. //
  278. // row[0] - row id
  279. // row[1] - contents for first cell in row
  280. // row[2] - contents for second cell in row
  281. // ...
  282. // row[N] - contents for N-th cell in row
  283. //
  284. if (row.length < 2)
  285. return row;
  286. for (let i = 1; i < row.length; i++) {
  287. if (i == 1) {
  288. // Fold/unfold image appears only in first column
  289. var dImg = E('div', { 'style': 'padding: 0 8px 0 0;' }, [
  290. E('img', { 'width': '16px', 'src': this.getFoldingImage(unfolded) }),
  291. ]);
  292. }
  293. if (Array.isArray(row[i])) {
  294. // row[i][0] = folded contents
  295. // row[i][1] = unfolded contents
  296. // Folded cell data
  297. let dFolded = E('div', {
  298. 'class': 'lldpd-folded',
  299. 'style': unfolded ? 'display: none;' : 'display: block;'
  300. }, row[i][0]);
  301. // Unfolded cell data
  302. let dUnfolded = E('div', {
  303. 'class': 'lldpd-unfolded',
  304. 'style': unfolded ? 'display: block;' : 'display: none;'
  305. }, row[i][1]);
  306. if (i == 1) {
  307. row[i] = E('div', {
  308. 'style': 'display: flex; flex-wrap: nowrap;'
  309. }, [ dImg, dFolded, dUnfolded ]);
  310. }
  311. else {
  312. row[i] = E('div', {}, [ dFolded, dUnfolded ]);
  313. }
  314. }
  315. else {
  316. // row[i] = same content for folded and unfolded states
  317. if (i == 1) {
  318. row[i] = E('div', {
  319. 'style': 'display: flex; flex-wrap: nowrap;'
  320. }, [ dImg, E('div', row[i]) ]);
  321. }
  322. }
  323. }
  324. return row;
  325. },
  326. /** @private */
  327. makeNeighborsTableRow: function(obj) {
  328. obj.name = obj?.name ?? 'Unknown';
  329. let new_id = `${obj.name}-${obj.rid}`;
  330. const portData = obj?.port?.[0];
  331. const portIdValue = portData?.id?.[0]?.value;
  332. const portDescrValue = portData?.descr?.[0]?.value;
  333. if (portIdValue)
  334. new_id += `-${portIdValue}`;
  335. if (portDescrValue)
  336. new_id += `-${portDescrValue}`;
  337. const row_id = this.generateRowId(new_id);
  338. return this.makeFoldingTableRow([
  339. row_id,
  340. [
  341. '%s'.format(obj.name),
  342. this.renderPortParamTableShort(obj)
  343. ],
  344. this.renderProtocol(obj.via),
  345. [
  346. this.renderChassis(obj?.chassis?.[0]),
  347. this.renderChassisParamTable(obj?.chassis?.[0])
  348. ],
  349. [
  350. this.renderPort(obj),
  351. this.renderPortParamTable(obj, true)
  352. ]
  353. ], this.rowsUnfolded?.[row_id] || false);
  354. },
  355. /** @private */
  356. renderInterfaceProtocols: function(iface, neighbors) {
  357. const ifaceName = iface?.name;
  358. const interfaces = neighbors?.lldp?.[0]?.interface;
  359. // Check if required data is available
  360. if (!ifaceName || !interfaces)
  361. return "&#8211;";
  362. const protocols = interfaces
  363. .filter(n => n.name === ifaceName)
  364. .map(n => this.renderProtocol(n.via));
  365. return protocols.length > 0 ? E('span', {}, protocols) : "&#8211;";
  366. },
  367. /** @private */
  368. makeStatisticsTableRow: function(sobj, iobj, neighbors) {
  369. const row_id = this.generateRowId(iobj.name);
  370. return this.makeFoldingTableRow([
  371. row_id,
  372. [
  373. this.renderPort(iobj), // folded
  374. this.renderPortParamTable(iobj, false) // unfolded
  375. ],
  376. this.renderInterfaceProtocols(iobj, neighbors),
  377. this.renderAdminStatus(iobj?.status),
  378. this.renderNumber(sobj?.tx?.[0]?.tx),
  379. this.renderNumber(sobj?.rx?.[0]?.rx),
  380. this.renderNumber(sobj?.rx_discarded_cnt?.[0]?.rx_discarded_cnt),
  381. this.renderNumber(sobj?.rx_unrecognized_cnt?.[0]?.rx_unrecognized_cnt),
  382. this.renderNumber(sobj?.ageout_cnt?.[0]?.ageout_cnt),
  383. this.renderNumber(sobj?.insert_cnt?.[0]?.insert_cnt),
  384. this.renderNumber(sobj?.delete_cnt?.[0]?.delete_cnt)
  385. ], this.rowsUnfolded?.[row_id] || false);
  386. },
  387. /** @private */
  388. updateTable: function(table, data, placeholder) {
  389. var target = isElem(table) ? table : document.querySelector(table);
  390. if (!isElem(target))
  391. return;
  392. target.querySelectorAll(
  393. '.tr.table-titles, .cbi-section-table-titles').forEach(L.bind(function(thead) {
  394. var titles = [];
  395. thead.querySelectorAll('.th').forEach(function(th) {
  396. titles.push(th);
  397. });
  398. if (Array.isArray(data)) {
  399. var n = 0, rows = target.querySelectorAll('.tr');
  400. data.forEach(L.bind(function(row) {
  401. var id = row[0];
  402. var trow = E('div', { 'class': 'tr', 'click': L.bind(function(ev) {
  403. this.handleToggleFoldingRow(ev.currentTarget, id);
  404. // lldpd_folding_toggle(ev.currentTarget, id);
  405. }, this) });
  406. for (var i = 0; i < titles.length; i++) {
  407. var text = (titles[i].innerText || '').trim();
  408. var td = trow.appendChild(E('div', {
  409. 'class': titles[i].className,
  410. 'data-title': (text !== '') ? text : null
  411. }, row[i + 1] || ''));
  412. td.classList.remove('th');
  413. td.classList.add('td');
  414. }
  415. trow.classList.add('cbi-rowstyle-%d'.format((n++ % 2) ? 2 : 1));
  416. if (rows[n])
  417. target.replaceChild(trow, rows[n]);
  418. else
  419. target.appendChild(trow);
  420. }, this));
  421. while (rows[++n])
  422. target.removeChild(rows[n]);
  423. if (placeholder && target.firstElementChild === target.lastElementChild) {
  424. var trow = target.appendChild(
  425. E('div', { 'class': 'tr placeholder' }));
  426. var td = trow.appendChild(
  427. E('div', { 'class': 'center ' + titles[0].className }, placeholder));
  428. td.classList.remove('th');
  429. td.classList.add('td');
  430. }
  431. } else {
  432. thead.parentNode.style.display = 'none';
  433. thead.parentNode.querySelectorAll('.tr, .cbi-section-table-row').forEach(function(trow) {
  434. if (trow !== thead) {
  435. var n = 0;
  436. trow.querySelectorAll('.th, .td').forEach(function(td) {
  437. if (n < titles.length) {
  438. var text = (titles[n++].innerText || '').trim();
  439. if (text !== '')
  440. td.setAttribute('data-title', text);
  441. }
  442. });
  443. }
  444. });
  445. thead.parentNode.style.display = '';
  446. }
  447. }, this));
  448. },
  449. /** @private */
  450. startPolling: function() {
  451. poll.add(L.bind(function() {
  452. return callLLDPStatus().then(L.bind(function(data) {
  453. this.renderData(data);
  454. }, this));
  455. }, this));
  456. },
  457. /** @private */
  458. renderDataLocalChassis: function(data) {
  459. const chassis = data?.['local-chassis']?.[0]?.chassis?.[0]?.name;
  460. if (chassis)
  461. return this.renderChassisParamTable(data['local-chassis'][0].chassis[0]);
  462. else
  463. return E('div', { 'class': 'alert-message warning' }, _('No data to display'));
  464. },
  465. /** @private */
  466. renderDataNeighbors: function(neighbors) {
  467. const ifaces = neighbors?.lldp?.[0]?.interface;
  468. return ifaces ? ifaces.map(iface => this.makeNeighborsTableRow(iface)) : [];
  469. },
  470. /** @private */
  471. renderDataStatistics: function(statistics, interfaces, neighbors) {
  472. const sifaces = statistics?.lldp?.[0]?.interface;
  473. const ifaces = interfaces?.lldp?.[0]?.interface;
  474. if (sifaces && ifaces) {
  475. return sifaces.map((siface, i) => this.makeStatisticsTableRow(siface, ifaces[i], neighbors));
  476. }
  477. return [];
  478. },
  479. /** @private */
  480. renderData: function(data) {
  481. var r;
  482. r = this.renderDataLocalChassis(data.chassis);
  483. dom.content(document.getElementById('lldpd-local-chassis'), r);
  484. r = this.renderDataNeighbors(data.neighbors);
  485. this.updateTable(this.tableNeighbors, r,
  486. _('No data to display'));
  487. r = this.renderDataStatistics(data.statistics, data.interfaces, data.neighbors);
  488. this.updateTable(this.tableStatistics, r,
  489. _('No data to display'));
  490. },
  491. render: function(data) {
  492. var m, s, ss, o;
  493. m = new form.JSONMap(dataMap,
  494. _('LLDP Status'),
  495. _('This page allows you to see discovered LLDP neighbors, ' +
  496. 'local interfaces statistics and local chassis information.'));
  497. s = m.section(form.NamedSection, 'local', 'local',
  498. _('Local Chassis'));
  499. o = s.option(form.DummyValue, 'localChassis');
  500. o.render = function() {
  501. return E('div', { 'id': 'lldpd-local-chassis' }, [
  502. E('em', { 'class': 'spinning' }, _('Collecting data...'))
  503. ]);
  504. };
  505. s = m.section(form.NamedSection, 'remote', 'remote');
  506. s.tab('neighbors', _('Discovered Neighbors'));
  507. s.tab('statistics', _('Interface Statistics'));
  508. o = s.taboption('neighbors', form.DummyValue, 'neighbors');
  509. o.render = L.bind(function() {
  510. return E('div', { 'class': 'table-wrapper' }, [
  511. this.tableNeighbors
  512. ]);
  513. }, this);
  514. o = s.taboption('statistics', form.DummyValue, 'statistics');
  515. o.render = L.bind(function() {
  516. return E('div', { 'class': 'table-wrapper' }, [
  517. this.tableStatistics
  518. ]);
  519. }, this);
  520. return m.render().then(L.bind(function(rendered) {
  521. this.startPolling();
  522. return rendered;
  523. }, this));
  524. },
  525. handleSaveApply: null,
  526. handleSave: null,
  527. handleReset: null
  528. });