autocore: switch to Jo's ethernet port status view implementation

Signed-off-by: Tianling Shen <cnsztl@immortalwrt.org>
This commit is contained in:
Jo-Philipp Wich 2023-03-29 03:34:59 +08:00 committed by Tianling Shen
parent 62b1521045
commit dc204bb443
No known key found for this signature in database
GPG Key ID: 6850B6345C862176
3 changed files with 377 additions and 86 deletions

View File

@ -1,6 +1,11 @@
'use strict';
'require baseclass';
'require fs';
'require ui';
'require uci';
'require rpc';
'require network';
'require firewall';
var callLuciETHInfo = rpc.declare({
object: 'luci',
@ -8,53 +13,337 @@ var callLuciETHInfo = rpc.declare({
expect: { '': {} }
});
return L.Class.extend({
function isString(v)
{
return typeof(v) === 'string' && v !== '';
}
function resolveVLANChain(ifname, bridges, mapping)
{
while (!mapping[ifname]) {
var m = ifname.match(/^(.+)\.([^.]+)$/);
if (!m)
break;
if (bridges[m[1]]) {
if (bridges[m[1]].vlan_filtering)
mapping[ifname] = bridges[m[1]].vlans[m[2]];
else
mapping[ifname] = bridges[m[1]].ports;
}
else if (/^[0-9]{1,4}$/.test(m[2]) && m[2] <= 4095) {
mapping[ifname] = [ m[1] ];
}
else {
break;
}
ifname = m[1];
}
}
function buildVLANMappings(mapping)
{
var bridge_vlans = uci.sections('network', 'bridge-vlan'),
vlan_devices = uci.sections('network', 'device'),
interfaces = uci.sections('network', 'interface'),
bridges = {};
/* find bridge VLANs */
for (var i = 0, s; (s = bridge_vlans[i]) != null; i++) {
if (!isString(s.device) || !/^[0-9]{1,4}$/.test(s.vlan) || +s.vlan > 4095)
continue;
var aliases = L.toArray(s.alias),
ports = L.toArray(s.ports),
br = bridges[s.device] = (bridges[s.device] || { ports: [], vlans: {}, vlan_filtering: true });
br.vlans[s.vlan] = [];
for (var j = 0; j < ports.length; j++) {
var port = ports[j].replace(/:[ut*]+$/, '');
if (br.ports.indexOf(port) === -1)
br.ports.push(port);
br.vlans[s.vlan].push(port);
}
for (var j = 0; j < aliases.length; j++)
if (aliases[j] != s.vlan)
br.vlans[aliases[j]] = br.vlans[s.vlan];
}
/* find bridges, VLAN devices */
for (var i = 0, s; (s = vlan_devices[i]) != null; i++) {
if (s.type == 'bridge') {
if (!isString(s.name))
continue;
var ports = L.toArray(s.ports),
br = bridges[s.name] || (bridges[s.name] = { ports: [], vlans: {}, vlan_filtering: false });
if (s.vlan_filtering == '0')
br.vlan_filtering = false;
else if (s.vlan_filtering == '1')
br.vlan_filtering = true;
for (var j = 0; j < ports.length; j++)
if (br.ports.indexOf(ports[j]) === -1)
br.ports.push(ports[j]);
mapping[s.name] = br.ports;
}
else if (s.type == '8021q' || s.type == '8021ad') {
if (!isString(s.name) || !isString(s.vid) || !isString(s.ifname))
continue;
/* parent device is a bridge */
if (bridges[s.ifname]) {
/* parent bridge is VLAN enabled, device refers to VLAN ports */
if (bridges[s.ifname].vlan_filtering)
mapping[s.name] = bridges[s.ifname].vlans[s.vid];
/* parent bridge is not VLAN enabled, device refers to all bridge ports */
else
mapping[s.name] = bridges[s.ifname].ports;
}
/* parent is a simple netdev */
else {
mapping[s.name] = [ s.ifname ];
}
resolveVLANChain(s.ifname, bridges, mapping);
}
}
/* resolve VLAN tagged interfaces in bridge ports */
for (var brname in bridges) {
for (var i = 0; i < bridges[brname].ports.length; i++)
resolveVLANChain(bridges[brname].ports[i], bridges, mapping);
for (var vid in bridges[brname].vlans)
for (var i = 0; i < bridges[brname].vlans[vid].length; i++)
resolveVLANChain(bridges[brname].vlans[vid][i], bridges, mapping);
}
/* find implicit VLAN devices */
for (var i = 0, s; (s = interfaces[i]) != null; i++) {
if (!isString(s.device))
continue;
resolveVLANChain(s.device, bridges, mapping);
}
}
function resolveVLANPorts(ifname, mapping)
{
var ports = [];
if (mapping[ifname])
for (var i = 0; i < mapping[ifname].length; i++)
ports.push.apply(ports, resolveVLANPorts(mapping[ifname][i], mapping));
else
ports.push(ifname);
return ports.sort(L.naturalCompare);
}
function buildInterfaceMapping(zones, networks) {
var vlanmap = {},
portmap = {},
netmap = {};
buildVLANMappings(vlanmap);
for (var i = 0; i < networks.length; i++) {
var l3dev = networks[i].getDevice();
if (!l3dev)
continue;
var ports = resolveVLANPorts(l3dev.getName(), vlanmap);
for (var j = 0; j < ports.length; j++) {
portmap[ports[j]] = portmap[ports[j]] || { networks: [], zones: [] };
portmap[ports[j]].networks.push(networks[i]);
}
netmap[networks[i].getName()] = networks[i];
}
for (var i = 0; i < zones.length; i++) {
var networknames = zones[i].getNetworks();
for (var j = 0; j < networknames.length; j++) {
if (!netmap[networknames[j]])
continue;
var l3dev = netmap[networknames[j]].getDevice();
if (!l3dev)
continue;
var ports = resolveVLANPorts(l3dev.getName(), vlanmap);
for (var k = 0; k < ports.length; k++) {
portmap[ports[k]] = portmap[ports[k]] || { networks: [], zones: [] };
if (portmap[ports[k]].zones.indexOf(zones[i]) === -1)
portmap[ports[k]].zones.push(zones[i]);
}
}
}
return portmap;
}
function formatSpeed(speed, duplex) {
if (speed && duplex) {
var d = (duplex == 'half') ? '\u202f(H)' : '',
e = E('span', { 'title': _('Speed: %d Mbit/s, Duplex: %s').format(speed, duplex) });
switch (speed) {
case 10: e.innerText = '10\u202fM' + d; break;
case 100: e.innerText = '100\u202fM' + d; break;
case 1000: e.innerText = '1\u202fGbE' + d; break;
case 2500: e.innerText = '2.5\u202fGbE'; break;
case 5000: e.innerText = '5\u202fGbE'; break;
case 10000: e.innerText = '10\u202fGbE'; break;
case 25000: e.innerText = '25\u202fGbE'; break;
case 40000: e.innerText = '40\u202fGbE'; break;
default: e.innerText = '%d\u202fMbE%s'.format(speed, d);
}
return e;
}
return _('no link');
}
function formatStats(portdev) {
var stats = portdev._devstate('stats');
return ui.itemlist(E('span'), [
_('Received bytes'), '%1024mB'.format(stats.rx_bytes),
_('Received packets'), '%1000mPkts.'.format(stats.rx_packets),
_('Received multicast'), '%1000mPkts.'.format(stats.multicast),
_('Receive errors'), '%1000mPkts.'.format(stats.rx_errors),
_('Receive dropped'), '%1000mPkts.'.format(stats.rx_dropped),
_('Transmitted bytes'), '%1024mB'.format(stats.tx_bytes),
_('Transmitted packets'), '%1000mPkts.'.format(stats.tx_packets),
_('Transmit errors'), '%1000mPkts.'.format(stats.tx_errors),
_('Transmit dropped'), '%1000mPkts.'.format(stats.tx_dropped),
_('Collisions seen'), stats.collisions
]);
}
function renderNetworkBadge(network, zonename) {
var l3dev = network.getDevice();
var span = E('span', { 'class': 'ifacebadge', 'style': 'margin:.125em 0' }, [
E('span', {
'class': 'zonebadge',
'title': zonename ? _('Part of zone %q').format(zonename) : _('No zone assigned'),
'style': firewall.getZoneColorStyle(zonename)
}, '\u202f'),
'\u202f', network.getName(), ': '
]);
if (l3dev)
span.appendChild(E('img', {
'title': l3dev.getI18n(),
'src': L.resource('icons/%s%s.png'.format(l3dev.getType(), l3dev.isUp() ? '' : '_disabled'))
}));
else
span.appendChild(E('em', _('(no interfaces attached)')));
return span;
}
function renderNetworksTooltip(pmap) {
var res = [ null ],
zmap = {};
for (var i = 0; pmap && i < pmap.zones.length; i++) {
var networknames = pmap.zones[i].getNetworks();
for (var k = 0; k < networknames.length; k++)
zmap[networknames[k]] = pmap.zones[i].getName();
}
for (var i = 0; pmap && i < pmap.networks.length; i++)
res.push(E('br'), renderNetworkBadge(pmap.networks[i], zmap[pmap.networks[i].getName()]));
if (res.length > 1)
res[0] = N_((res.length - 1) / 2, 'Part of network:', 'Part of networks:');
else
res[0] = _('Port is not part of any network');
return E([], res);
}
return baseclass.extend({
title: _('Ethernet Information'),
load: function() {
return Promise.all([
L.resolveDefault(callLuciETHInfo(), {})
L.resolveDefault(callLuciETHInfo(), {}),
firewall.getZones(),
network.getNetworks(),
uci.load('network')
]);
},
render: function(data) {
var ethinfo = Array.isArray(data[0].ethinfo) ? data[0].ethinfo : [];
var known_ports = [],
port_map = buildInterfaceMapping(data[1], data[2]);
var table = E('table', { 'class': 'table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, _('Ethernet Name')),
E('th', { 'class': 'th' }, _('Link Status')),
E('th', { 'class': 'th' }, _('Speed')),
E('th', { 'class': 'th' }, _('Duplex'))
])
]);
if (Array.isArray(data[0].ethinfo))
data[0].ethinfo.forEach((k) => known_ports.push({
device: k,
netdev: network.instantiateDevice(k)
}));
cbi_update_table(table, ethinfo.map(function(info) {
var exp1;
var exp2;
known_ports.sort(function(a, b) {
return L.naturalCompare(a.device, b.device);
});
if (info.status == "yes")
exp1 = _('Link Up');
else if (info.status == "no")
exp1 = _('Link Down');
return E('div', { 'style': 'display:grid;grid-template-columns:repeat(auto-fit, minmax(70px, 1fr));margin-bottom:1em;justify-items:center' }, known_ports.map(function(port) {
var speed = port.netdev.getSpeed(),
duplex = port.netdev.getDuplex(),
pmap = port_map[port.netdev.getName()],
pzones = (pmap && pmap.zones.length) ? pmap.zones.sort(function(a, b) { return L.naturalCompare(a.getName(), b.getName()) }) : [ null ];
if (info.duplex == "Full")
exp2 = _('Full Duplex');
else if (info.duplex == "Half")
exp2 = _('Half Duplex');
else
exp2 = _('-');
return [
info.name,
exp1,
info.speed,
exp2
];
return E('div', { 'class': 'ifacebox', 'style': 'margin:.25em;width:100px' }, [
E('div', { 'class': 'ifacebox-head', 'style': 'font-weight:bold' }, [ port.netdev.getName() ]),
E('div', { 'class': 'ifacebox-body' }, [
E('img', { 'src': L.resource('icons/port_%s.png').format((speed && duplex) ? 'up' : 'down') }),
E('br'),
formatSpeed(speed, duplex)
]),
E('div', { 'class': 'ifacebox-head cbi-tooltip-container', 'style': 'display:flex' }, [
E([], pzones.map(function(zone) {
return E('div', {
'class': 'zonebadge',
'style': 'cursor:help;flex:1;height:3px;' + firewall.getZoneColorStyle(zone)
});
})),
E('span', { 'class': 'cbi-tooltip left' }, [ renderNetworksTooltip(pmap) ])
]),
E('div', { 'class': 'ifacebox-body' }, [
E('div', { 'class': 'cbi-tooltip-container', 'style': 'text-align:left;font-size:80%' }, [
'\u25b2\u202f%1024.1mB'.format(port.netdev.getTXBytes()),
E('br'),
'\u25bc\u202f%1024.1mB'.format(port.netdev.getRXBytes()),
E('span', { 'class': 'cbi-tooltip' }, formatStats(port.netdev))
]),
])
]);
}));
return E([
table
]);
}
});

View File

@ -12,40 +12,14 @@ if (!stat) {
exit(1);
}
let eth_info = [];
for (let ifname in stat) {
if (!ifname || stat[ifname]?.devtype !== 'ethernet' || !length(stat[ifname]['link-advertising']))
continue;
let status, speed, duplex;
if (stat[ifname]?.carrier)
status = 'yes';
else
status = 'no';
if (!stat[ifname]?.carrier || !stat[ifname]?.speed || substr(stat[ifname]?.speed, 0, 1) === '-')
speed = '-';
else
speed = substr(stat[ifname]?.speed, 0, -1) + 'Mb/s';
if (speed == '-')
duplex = '-';
else if (substr(stat[ifname]?.speed, -1) === 'F')
duplex = 'Full';
else
duplex = 'Half';
push(eth_info, {
name: ifname,
status: status,
speed: speed,
duplex: duplex,
});
}
sort(eth_info, (a, b) => {
return a?.name > b?.name;
let device_list = [];
map(keys(stat), (ifname) => {
if (ifname && stat[ifname]?.devtype === 'ethernet' && length(stat[ifname]['link-advertising']))
push(device_list, ifname);
});
print(eth_info);
sort(device_list, (a, b) => {
return a > b;
});
print(device_list);

View File

@ -69,32 +69,60 @@ msgid ""
"option does not comply with IEEE 802.11n-2009!"
msgstr "强制启用 40MHz 频宽并忽略辅助信道重叠。此选项可能不兼容某些无线硬件导致无法启用!"
msgid "(no interfaces attached)"
msgstr "(未连接到接口)"
msgid "Collisions seen"
msgstr "检测到冲突"
msgid "Ethernet Information"
msgstr "网口信息"
msgid "Ethernet Name"
msgstr "网口名"
msgid "No zone assigned"
msgstr "未分配区域"
msgid "Link Status"
msgstr "连接状态"
msgid "Part of network:"
msgid_plural "Part of networks:"
msgstr[0] "属于以下网络:"
msgid "Speed"
msgstr "速率"
msgid "Part of zone %q"
msgstr "属于区域 %q"
msgid "Duplex"
msgstr "双工模式"
msgid "Port is not part of any network"
msgstr "端口不属于任何网络"
msgid "Link Up"
msgstr "已连接"
msgid "Receive dropped"
msgstr "接收丢弃"
msgid "Link Down"
msgstr "已断开"
msgid "Receive errors"
msgstr "接收错误"
msgid "Full Duplex"
msgstr "全双工"
msgid "Received bytes"
msgstr "接收字节"
msgid "Half Duplex"
msgstr "半双工"
msgid "Received multicast"
msgstr "接收组播"
msgid "Received packets"
msgstr "接收数据包"
msgid "Speed: %d Mbit/s, Duplex: %s"
msgstr "速率:%d Mbit/s双工模式%s"
msgid "Transmit dropped"
msgstr "传输丢弃"
msgid "Transmit errors"
msgstr "传输错误"
msgid "Transmitted bytes"
msgstr "传输字节"
msgid "Transmitted packets"
msgstr "传输数据包"
msgid "no link"
msgstr "未连接"
msgid "Allow AP mode to disconnect STAs based on low ACK condition"
msgstr "允许 AP 模式基于低 ACK 条件判断剔除弱信号的客户端"