mirror of
https://github.com/openwrt/luci.git
synced 2025-12-21 17:04:35 +04:00
646 lines
17 KiB
JavaScript
646 lines
17 KiB
JavaScript
/*
|
|
* Copyright (c) 2020 Tano Systems. All Rights Reserved.
|
|
* Author: Anton Kikin <a.kikin@tano-systems.com>
|
|
* Copyright (c) 2023-2024. All Rights Reserved.
|
|
* Paul Donald <newtwen+github@gmail.com>
|
|
*/
|
|
|
|
'use strict';
|
|
'require rpc';
|
|
'require form';
|
|
'require lldpd';
|
|
'require dom';
|
|
'require poll';
|
|
|
|
const callLLDPStatus = rpc.declare({
|
|
object: 'luci.lldpd',
|
|
method: 'getStatus',
|
|
expect: {}
|
|
});
|
|
|
|
var dataMap = {
|
|
local: {
|
|
localChassis: null,
|
|
},
|
|
remote: {
|
|
neighbors: null,
|
|
statistics: null,
|
|
},
|
|
};
|
|
|
|
return L.view.extend({
|
|
__init__: function() {
|
|
this.super('__init__', arguments);
|
|
|
|
this.rowsUnfolded = {};
|
|
|
|
this.tableNeighbors = E('div', { 'class': 'table lldpd-table' }, [
|
|
E('div', { 'class': 'tr table-titles' }, [
|
|
E('div', { 'class': 'th left top' }, _('Local interface')),
|
|
E('div', { 'class': 'th left top' }, _('Protocol')),
|
|
E('div', { 'class': 'th left top' }, _('Discovered chassis')),
|
|
E('div', { 'class': 'th left top' }, _('Discovered port')),
|
|
]),
|
|
E('div', { 'class': 'tr center placeholder' }, [
|
|
E('div', { 'class': 'td' }, E('em', { 'class': 'spinning' },
|
|
_('Collecting data...'))),
|
|
])
|
|
]);
|
|
|
|
this.tableStatistics = E('div', { 'class': 'table lldpd-table' }, [
|
|
E('div', { 'class': 'tr table-titles' }, [
|
|
E('div', { 'class': 'th left top' }, _('Local interface')),
|
|
E('div', { 'class': 'th left top' }, _('Protocol')),
|
|
E('div', { 'class': 'th left top' }, _('Administrative Status')),
|
|
E('div', { 'class': 'th right top' }, _('Tx')),
|
|
E('div', { 'class': 'th right top' }, _('Rx')),
|
|
E('div', { 'class': 'th right top' }, _('Tx discarded')),
|
|
E('div', { 'class': 'th right top' }, _('Rx unrecognized')),
|
|
E('div', { 'class': 'th right top' }, _('Ageout count')),
|
|
E('div', { 'class': 'th right top' }, _('Insert count')),
|
|
E('div', { 'class': 'th right top' }, _('Delete count')),
|
|
]),
|
|
E('div', { 'class': 'tr center placeholder' }, [
|
|
E('div', { 'class': 'td' }, E('em', { 'class': 'spinning' },
|
|
_('Collecting data...'))),
|
|
])
|
|
]);
|
|
|
|
// Inject CSS
|
|
var head = document.getElementsByTagName('head')[0];
|
|
var css = E('link', { 'href':
|
|
L.resource('lldpd/lldpd.css')
|
|
+ '?v=#PKG_VERSION', 'rel': 'stylesheet' });
|
|
|
|
head.appendChild(css);
|
|
},
|
|
|
|
load: function() {
|
|
return Promise.all([
|
|
L.resolveDefault(callLLDPStatus(), {}),
|
|
lldpd.init(),
|
|
]);
|
|
},
|
|
|
|
/** @private */
|
|
renderParam: function(param, value) {
|
|
if (typeof value === 'undefined')
|
|
return '';
|
|
|
|
return E('div', {}, [
|
|
E('span', { 'class': 'lldpd-param' }, param),
|
|
E('span', { 'class': 'lldpd-param-value' }, value)
|
|
]);
|
|
},
|
|
|
|
/** @private */
|
|
renderAge: function(v) {
|
|
if (typeof v === 'undefined')
|
|
return "–";
|
|
|
|
return E('nobr', {}, v);
|
|
},
|
|
|
|
/** @private */
|
|
renderIdType: function(v) {
|
|
if (typeof v === 'undefined')
|
|
return "–";
|
|
|
|
if (v == 'mac')
|
|
return _('MAC address');
|
|
else if (v == 'ifname')
|
|
return _('Interface name');
|
|
else if (v == 'local')
|
|
return _('Local ID');
|
|
else if (v == 'ip')
|
|
return _('IP address');
|
|
|
|
return v;
|
|
},
|
|
|
|
/** @private */
|
|
renderProtocol: function(v) {
|
|
if (typeof v === 'undefined' || v == 'unknown')
|
|
return "–";
|
|
|
|
if (v == 'LLDP')
|
|
return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-lldp' }, v);
|
|
else if ((v == 'CDPv1') || (v == 'CDPv2'))
|
|
return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-cdp' }, v);
|
|
else if (v == 'FDP')
|
|
return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-fdp' }, v);
|
|
else if (v == 'EDP')
|
|
return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-edp' }, v);
|
|
else if (v == 'SONMP')
|
|
return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-sonmp' }, v);
|
|
else
|
|
return E('span', { 'class': 'lldpd-protocol-badge' }, v);
|
|
},
|
|
|
|
/** @private */
|
|
renderAdminStatus: function(status) {
|
|
if ((typeof status === 'undefined') || !Array.isArray(status))
|
|
return '–';
|
|
|
|
if (status[0].value === 'RX and TX')
|
|
return _('Rx and Tx');
|
|
else if (status[0].value === 'RX only')
|
|
return _('Rx only');
|
|
else if (status[0].value === 'TX only')
|
|
return _('Tx only');
|
|
else if (status[0].value === 'disabled')
|
|
return _('Disabled');
|
|
else
|
|
return _('Unknown');
|
|
},
|
|
|
|
/** @private */
|
|
renderNumber: function(v) {
|
|
if (parseInt(v))
|
|
return v;
|
|
|
|
return '–';
|
|
},
|
|
|
|
/** @private */
|
|
renderPort: function(port) {
|
|
const portData = port?.port?.[0];
|
|
const descrValue = portData?.descr?.[0]?.value;
|
|
const idValue = portData?.id?.[0]?.value;
|
|
|
|
if (portData) {
|
|
if (descrValue && idValue && descrValue !== idValue) {
|
|
return [
|
|
E('strong', {}, descrValue),
|
|
E('br', {}),
|
|
idValue
|
|
];
|
|
}
|
|
|
|
return descrValue ?? idValue;
|
|
}
|
|
|
|
return '%s'.format(port.name);
|
|
},
|
|
|
|
/** @private */
|
|
renderPortParamTableShort: function(port) {
|
|
var items = [];
|
|
|
|
items.push(this.renderParam(_('Name'), port.name));
|
|
items.push(this.renderParam(_('Age'), this.renderAge(port.age)));
|
|
|
|
return E('div', { 'class': 'lldpd-params' }, items);
|
|
},
|
|
|
|
/** @private */
|
|
renderPortParamTable: function(port, only_id_and_ttl) {
|
|
const items = [];
|
|
|
|
if (!only_id_and_ttl) {
|
|
items.push(this.renderParam(_('Name'), port?.name));
|
|
items.push(this.renderParam(_('Age'), this.renderAge(port?.age)));
|
|
}
|
|
|
|
const portData = port?.port?.[0];
|
|
|
|
if (portData) {
|
|
const portId = portData?.id?.[0];
|
|
if (portId) {
|
|
items.push(this.renderParam(_('Port ID'), portId?.value));
|
|
items.push(this.renderParam(_('Port ID type'), this.renderIdType(portId?.type)));
|
|
}
|
|
|
|
if (portData?.descr?.[0]?.value)
|
|
items.push(this.renderParam(_('Port description'), portData.descr[0].value));
|
|
|
|
const ttlValue = port?.ttl?.[0]?.ttl ?? portData?.ttl?.[0]?.value;
|
|
if (ttlValue)
|
|
items.push(this.renderParam(_('TTL'), ttlValue));
|
|
|
|
if (portData?.mfs?.[0]?.value)
|
|
items.push(this.renderParam(_('MFS'), portData.mfs[0].value));
|
|
|
|
}
|
|
|
|
return E('div', { 'class': 'lldpd-params' }, items);
|
|
},
|
|
|
|
/** @private */
|
|
renderChassis: function(ch) {
|
|
const nameValue = ch?.name?.[0]?.value;
|
|
const descrValue = ch?.descr?.[0]?.value;
|
|
const idValue = ch?.id?.[0]?.value;
|
|
|
|
if (nameValue && descrValue) {
|
|
return [
|
|
E('strong', {}, nameValue),
|
|
E('br', {}),
|
|
descrValue
|
|
];
|
|
}
|
|
|
|
if (nameValue)
|
|
return E('strong', {}, nameValue);
|
|
|
|
if (descrValue)
|
|
return descrValue;
|
|
|
|
if (idValue)
|
|
return idValue;
|
|
|
|
return _('Unknown');
|
|
},
|
|
|
|
/** @private */
|
|
renderChassisParamTable: function(ch) {
|
|
const items = [];
|
|
|
|
// Add name and description if available
|
|
const nameValue = ch?.name?.[0]?.value;
|
|
if (nameValue)
|
|
items.push(this.renderParam(_('Name'), nameValue));
|
|
|
|
const descrValue = ch?.descr?.[0]?.value;
|
|
if (descrValue)
|
|
items.push(this.renderParam(_('Description'), descrValue));
|
|
|
|
// Add ID and ID type if available
|
|
const idValue = ch?.id?.[0]?.value;
|
|
const idType = ch?.id?.[0]?.type;
|
|
if (idValue) {
|
|
items.push(this.renderParam(_('ID'), idValue));
|
|
items.push(this.renderParam(_('ID type'), this.renderIdType(idType)));
|
|
}
|
|
|
|
// Management addresses
|
|
const mgmtIps = ch?.['mgmt-ip'];
|
|
if (mgmtIps?.length > 0) {
|
|
const ips = mgmtIps.map(ip => ip.value).join('<br />');
|
|
items.push(this.renderParam(_('Management IP(s)'), ips));
|
|
}
|
|
|
|
// Capabilities
|
|
const capabilities = ch?.capability;
|
|
if (capabilities?.length > 0) {
|
|
const caps = capabilities.map(cap =>
|
|
`${cap.type} (${cap.enabled ? _('enabled') : _('disabled')})`
|
|
).join('<br />');
|
|
items.push(this.renderParam(_('Capabilities'), caps));
|
|
}
|
|
|
|
return E('div', { 'class': 'lldpd-params' }, items);
|
|
},
|
|
|
|
/** @private */
|
|
getFoldingImage: function(unfolded) {
|
|
return L.resource('lldpd/details_' +
|
|
(unfolded ? 'hide' : 'show') + '.svg');
|
|
},
|
|
|
|
/** @private */
|
|
generateRowId: function(str) {
|
|
return str.replace(/[^a-z0-9]/gi, '-');
|
|
},
|
|
|
|
/** @private */
|
|
handleToggleFoldingRow: function(row, row_id) {
|
|
var e_img = row.querySelector('img');
|
|
var e_folded = row.querySelectorAll('.lldpd-folded');
|
|
var e_unfolded = row.querySelectorAll('.lldpd-unfolded');
|
|
|
|
if (e_folded.length != e_unfolded.length)
|
|
return;
|
|
|
|
var do_unfold = (e_folded[0].style.display !== 'none');
|
|
this.rowsUnfolded[row_id] = do_unfold;
|
|
|
|
for (var i = 0; i < e_folded.length; i++)
|
|
{
|
|
if (do_unfold)
|
|
{
|
|
e_folded[i].style.display = 'none';
|
|
e_unfolded[i].style.display = 'block';
|
|
}
|
|
else
|
|
{
|
|
e_folded[i].style.display = 'block';
|
|
e_unfolded[i].style.display = 'none';
|
|
}
|
|
}
|
|
|
|
e_img.src = this.getFoldingImage(do_unfold);
|
|
},
|
|
|
|
/** @private */
|
|
makeFoldingTableRow: function(row, unfolded) {
|
|
//
|
|
// row[0] - row id
|
|
// row[1] - contents for first cell in row
|
|
// row[2] - contents for second cell in row
|
|
// ...
|
|
// row[N] - contents for N-th cell in row
|
|
//
|
|
if (row.length < 2)
|
|
return row;
|
|
|
|
for (let i = 1; i < row.length; i++) {
|
|
if (i == 1) {
|
|
// Fold/unfold image appears only in first column
|
|
var dImg = E('div', { 'style': 'padding: 0 8px 0 0;' }, [
|
|
E('img', { 'width': '16px', 'src': this.getFoldingImage(unfolded) }),
|
|
]);
|
|
}
|
|
|
|
if (Array.isArray(row[i])) {
|
|
// row[i][0] = folded contents
|
|
// row[i][1] = unfolded contents
|
|
|
|
// Folded cell data
|
|
let dFolded = E('div', {
|
|
'class': 'lldpd-folded',
|
|
'style': unfolded ? 'display: none;' : 'display: block;'
|
|
}, row[i][0]);
|
|
|
|
// Unfolded cell data
|
|
let dUnfolded = E('div', {
|
|
'class': 'lldpd-unfolded',
|
|
'style': unfolded ? 'display: block;' : 'display: none;'
|
|
}, row[i][1]);
|
|
|
|
if (i == 1) {
|
|
row[i] = E('div', {
|
|
'style': 'display: flex; flex-wrap: nowrap;'
|
|
}, [ dImg, dFolded, dUnfolded ]);
|
|
}
|
|
else {
|
|
row[i] = E('div', {}, [ dFolded, dUnfolded ]);
|
|
}
|
|
}
|
|
else {
|
|
// row[i] = same content for folded and unfolded states
|
|
|
|
if (i == 1) {
|
|
row[i] = E('div', {
|
|
'style': 'display: flex; flex-wrap: nowrap;'
|
|
}, [ dImg, E('div', row[i]) ]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return row;
|
|
},
|
|
|
|
/** @private */
|
|
makeNeighborsTableRow: function(obj) {
|
|
obj.name = obj?.name ?? 'Unknown';
|
|
|
|
let new_id = `${obj.name}-${obj.rid}`;
|
|
|
|
const portData = obj?.port?.[0];
|
|
const portIdValue = portData?.id?.[0]?.value;
|
|
const portDescrValue = portData?.descr?.[0]?.value;
|
|
|
|
if (portIdValue)
|
|
new_id += `-${portIdValue}`;
|
|
|
|
if (portDescrValue)
|
|
new_id += `-${portDescrValue}`;
|
|
|
|
const row_id = this.generateRowId(new_id);
|
|
|
|
return this.makeFoldingTableRow([
|
|
row_id,
|
|
[
|
|
'%s'.format(obj.name),
|
|
this.renderPortParamTableShort(obj)
|
|
],
|
|
this.renderProtocol(obj.via),
|
|
[
|
|
this.renderChassis(obj?.chassis?.[0]),
|
|
this.renderChassisParamTable(obj?.chassis?.[0])
|
|
],
|
|
[
|
|
this.renderPort(obj),
|
|
this.renderPortParamTable(obj, true)
|
|
]
|
|
], this.rowsUnfolded?.[row_id] || false);
|
|
},
|
|
|
|
/** @private */
|
|
renderInterfaceProtocols: function(iface, neighbors) {
|
|
const ifaceName = iface?.name;
|
|
const interfaces = neighbors?.lldp?.[0]?.interface;
|
|
|
|
// Check if required data is available
|
|
if (!ifaceName || !interfaces)
|
|
return "–";
|
|
|
|
const protocols = interfaces
|
|
.filter(n => n.name === ifaceName)
|
|
.map(n => this.renderProtocol(n.via));
|
|
|
|
return protocols.length > 0 ? E('span', {}, protocols) : "–";
|
|
},
|
|
|
|
/** @private */
|
|
makeStatisticsTableRow: function(sobj, iobj, neighbors) {
|
|
const row_id = this.generateRowId(iobj.name);
|
|
|
|
return this.makeFoldingTableRow([
|
|
row_id,
|
|
[
|
|
this.renderPort(iobj), // folded
|
|
this.renderPortParamTable(iobj, false) // unfolded
|
|
],
|
|
this.renderInterfaceProtocols(iobj, neighbors),
|
|
this.renderAdminStatus(iobj?.status),
|
|
this.renderNumber(sobj?.tx?.[0]?.tx),
|
|
this.renderNumber(sobj?.rx?.[0]?.rx),
|
|
this.renderNumber(sobj?.rx_discarded_cnt?.[0]?.rx_discarded_cnt),
|
|
this.renderNumber(sobj?.rx_unrecognized_cnt?.[0]?.rx_unrecognized_cnt),
|
|
this.renderNumber(sobj?.ageout_cnt?.[0]?.ageout_cnt),
|
|
this.renderNumber(sobj?.insert_cnt?.[0]?.insert_cnt),
|
|
this.renderNumber(sobj?.delete_cnt?.[0]?.delete_cnt)
|
|
], this.rowsUnfolded?.[row_id] || false);
|
|
},
|
|
|
|
/** @private */
|
|
updateTable: function(table, data, placeholder) {
|
|
var target = isElem(table) ? table : document.querySelector(table);
|
|
|
|
if (!isElem(target))
|
|
return;
|
|
|
|
target.querySelectorAll(
|
|
'.tr.table-titles, .cbi-section-table-titles').forEach(L.bind(function(thead) {
|
|
var titles = [];
|
|
|
|
thead.querySelectorAll('.th').forEach(function(th) {
|
|
titles.push(th);
|
|
});
|
|
|
|
if (Array.isArray(data)) {
|
|
var n = 0, rows = target.querySelectorAll('.tr');
|
|
|
|
data.forEach(L.bind(function(row) {
|
|
var id = row[0];
|
|
var trow = E('div', { 'class': 'tr', 'click': L.bind(function(ev) {
|
|
this.handleToggleFoldingRow(ev.currentTarget, id);
|
|
// lldpd_folding_toggle(ev.currentTarget, id);
|
|
}, this) });
|
|
|
|
for (var i = 0; i < titles.length; i++) {
|
|
var text = (titles[i].innerText || '').trim();
|
|
var td = trow.appendChild(E('div', {
|
|
'class': titles[i].className,
|
|
'data-title': (text !== '') ? text : null
|
|
}, row[i + 1] || ''));
|
|
|
|
td.classList.remove('th');
|
|
td.classList.add('td');
|
|
}
|
|
|
|
trow.classList.add('cbi-rowstyle-%d'.format((n++ % 2) ? 2 : 1));
|
|
|
|
if (rows[n])
|
|
target.replaceChild(trow, rows[n]);
|
|
else
|
|
target.appendChild(trow);
|
|
}, this));
|
|
|
|
while (rows[++n])
|
|
target.removeChild(rows[n]);
|
|
|
|
if (placeholder && target.firstElementChild === target.lastElementChild) {
|
|
var trow = target.appendChild(
|
|
E('div', { 'class': 'tr placeholder' }));
|
|
|
|
var td = trow.appendChild(
|
|
E('div', { 'class': 'center ' + titles[0].className }, placeholder));
|
|
|
|
td.classList.remove('th');
|
|
td.classList.add('td');
|
|
}
|
|
} else {
|
|
thead.parentNode.style.display = 'none';
|
|
|
|
thead.parentNode.querySelectorAll('.tr, .cbi-section-table-row').forEach(function(trow) {
|
|
if (trow !== thead) {
|
|
var n = 0;
|
|
trow.querySelectorAll('.th, .td').forEach(function(td) {
|
|
if (n < titles.length) {
|
|
var text = (titles[n++].innerText || '').trim();
|
|
if (text !== '')
|
|
td.setAttribute('data-title', text);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
thead.parentNode.style.display = '';
|
|
}
|
|
}, this));
|
|
},
|
|
|
|
/** @private */
|
|
startPolling: function() {
|
|
poll.add(L.bind(function() {
|
|
return callLLDPStatus().then(L.bind(function(data) {
|
|
this.renderData(data);
|
|
}, this));
|
|
}, this));
|
|
},
|
|
|
|
/** @private */
|
|
renderDataLocalChassis: function(data) {
|
|
const chassis = data?.['local-chassis']?.[0]?.chassis?.[0]?.name;
|
|
|
|
if (chassis)
|
|
return this.renderChassisParamTable(data['local-chassis'][0].chassis[0]);
|
|
else
|
|
return E('div', { 'class': 'alert-message warning' }, _('No data to display'));
|
|
},
|
|
|
|
/** @private */
|
|
renderDataNeighbors: function(neighbors) {
|
|
const ifaces = neighbors?.lldp?.[0]?.interface;
|
|
return ifaces ? ifaces.map(iface => this.makeNeighborsTableRow(iface)) : [];
|
|
},
|
|
|
|
/** @private */
|
|
renderDataStatistics: function(statistics, interfaces, neighbors) {
|
|
const sifaces = statistics?.lldp?.[0]?.interface;
|
|
const ifaces = interfaces?.lldp?.[0]?.interface;
|
|
|
|
if (sifaces && ifaces) {
|
|
return sifaces.map((siface, i) => this.makeStatisticsTableRow(siface, ifaces[i], neighbors));
|
|
}
|
|
|
|
return [];
|
|
},
|
|
|
|
/** @private */
|
|
renderData: function(data) {
|
|
var r;
|
|
|
|
r = this.renderDataLocalChassis(data.chassis);
|
|
dom.content(document.getElementById('lldpd-local-chassis'), r);
|
|
|
|
r = this.renderDataNeighbors(data.neighbors);
|
|
this.updateTable(this.tableNeighbors, r,
|
|
_('No data to display'));
|
|
|
|
r = this.renderDataStatistics(data.statistics, data.interfaces, data.neighbors);
|
|
this.updateTable(this.tableStatistics, r,
|
|
_('No data to display'));
|
|
},
|
|
|
|
render: function(data) {
|
|
var m, s, ss, o;
|
|
|
|
m = new form.JSONMap(dataMap,
|
|
_('LLDP Status'),
|
|
_('This page allows you to see discovered LLDP neighbors, ' +
|
|
'local interfaces statistics and local chassis information.'));
|
|
|
|
s = m.section(form.NamedSection, 'local', 'local',
|
|
_('Local Chassis'));
|
|
|
|
o = s.option(form.DummyValue, 'localChassis');
|
|
o.render = function() {
|
|
return E('div', { 'id': 'lldpd-local-chassis' }, [
|
|
E('em', { 'class': 'spinning' }, _('Collecting data...'))
|
|
]);
|
|
};
|
|
|
|
s = m.section(form.NamedSection, 'remote', 'remote');
|
|
|
|
s.tab('neighbors', _('Discovered Neighbors'));
|
|
s.tab('statistics', _('Interface Statistics'));
|
|
|
|
o = s.taboption('neighbors', form.DummyValue, 'neighbors');
|
|
o.render = L.bind(function() {
|
|
return E('div', { 'class': 'table-wrapper' }, [
|
|
this.tableNeighbors
|
|
]);
|
|
}, this);
|
|
|
|
o = s.taboption('statistics', form.DummyValue, 'statistics');
|
|
o.render = L.bind(function() {
|
|
return E('div', { 'class': 'table-wrapper' }, [
|
|
this.tableStatistics
|
|
]);
|
|
}, this);
|
|
|
|
return m.render().then(L.bind(function(rendered) {
|
|
this.startPolling();
|
|
return rendered;
|
|
}, this));
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|