├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── htdocs └── luci-static │ └── resources │ ├── netports.js │ ├── netports │ ├── buttons │ │ └── retweet.svg │ ├── icons │ │ ├── copper_disabled.svg │ │ ├── copper_down.svg │ │ ├── copper_up.svg │ │ ├── fixed_disabled.svg │ │ ├── fixed_down.svg │ │ ├── fixed_up.svg │ │ ├── gprs_disabled.svg │ │ ├── gprs_down.svg │ │ ├── gprs_up.svg │ │ ├── ppp_disabled.svg │ │ ├── ppp_down.svg │ │ ├── ppp_up.svg │ │ ├── sfp_disabled.svg │ │ ├── sfp_down.svg │ │ ├── sfp_up.svg │ │ ├── tunnel_disabled.svg │ │ ├── tunnel_down.svg │ │ ├── tunnel_up.svg │ │ ├── usb_rndis_disabled.svg │ │ ├── usb_rndis_down.svg │ │ ├── usb_rndis_up.svg │ │ ├── usb_stick_disabled.svg │ │ ├── usb_stick_down.svg │ │ ├── usb_stick_up.svg │ │ ├── wifi_disabled.svg │ │ ├── wifi_down.svg │ │ └── wifi_up.svg │ └── netports.css │ └── view │ └── status │ └── include │ └── 25_netports.js ├── i18n.sh ├── make-archive.cmd ├── po ├── ru │ └── netports.po └── templates │ └── netports.pot ├── root └── usr │ ├── libexec │ └── rpcd │ │ └── netports │ └── share │ └── rpcd │ └── acl.d │ └── netports.json ├── screenshots ├── h-mode-5-ports-extra.png ├── h-mode-5-ports.png └── v-mode-5-ports.png └── test-deploy.cmd /.gitignore: -------------------------------------------------------------------------------- 1 | *.po~ 2 | *.tar.gz 3 | *.zip 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 6 | 7 | All dates in this document are in `DD.MM.YYYY` format. 8 | 9 | ## [Unreleased] 10 | 11 | ### Fixed 12 | - Proper handle aliases when searching network name for bridges 13 | 14 | ### Added 15 | - Added 'sfp' port type with icon 16 | 17 | ## [Version 2.0.3] 18 | 19 | ### Changed 20 | - Added translation contexts for some strings. 21 | - Use prefixes to indicate binary multiples for Rx and Tx bytes 22 | from IEEE 1541-2002. 23 | 24 | ### Fixed 25 | - Do not show the table on insufficient permissions. 26 | - Synchronized colors of the firewall zones with colors from the 27 | network/firewall settings. 28 | - Fixed nbsp entity output for XML (luci-theme-openwrt). 29 | - Fixed markup for interfaces without MAC address (e.g. PPP connections). 30 | - Fixed IE11 compatibility. 31 | 32 | ## [Version 2.0.2] (20.01.2020) 33 | 34 | ### Fixed 35 | - Fixed handling of negative speed values in sysfs. 36 | 37 | ## [Version 2.0.1] (13.01.2020) 38 | 39 | ### Fixed 40 | - Fixed links to network interfaces page. 41 | 42 | ## [Version 2.0.0] (04.11.2019) 43 | 44 | ### Changed 45 | - Added support for client side view introduced in the latest changes 46 | in the master branch of the LuCI. 47 | 48 | ## [Version 1.1.1] (26.10.2019) 49 | 50 | ### Fixed 51 | - Fixed display of the icon for 'vpn' port type. 52 | - Fixed displaying of an empty MAC address. 53 | 54 | ## [Version 1.1.0] (06.10.2019) 55 | 56 | ### Added 57 | - This CHANGELOG file. 58 | - Handling cases when the firewall zones are assigned to both the network 59 | interface and the bridge of which the interface is a member. In this case, 60 | information about both firewall zones is displayed. 61 | - Added vertical table view mode. By default used new vertical mode. This may 62 | changed by the `global.default_h_mode` option in `/etc/config/luci_netports` 63 | configuration file. If used horizontal mode then view mode is automatically 64 | switched to vertical mode in case the number of interfaces is more than 6. 65 | - Added "Switch to vertical/horizontal mode" button. By default this button 66 | is disabled (not shown). Button may be enabled by the `global.hv_mode_switch_button` 67 | option in `/etc/config/luci_netports` configuration file. 68 | - Added example screenshots. 69 | - Added `usb_stick`, `usb_2g`, `usb_3g`, `usb_4g`, `tunnel`, `gprs`, `ppp` 70 | and `usb_wifi` new types and icons. 71 | - Added `auto` port type for automatically detect type by interface name. 72 | - Added link to the wireless interface configuration. 73 | - Added spinner for messages about waiting for data. 74 | - Added rpcd ubus script for data gathering 75 | 76 | ### Changed 77 | - Updated README.md file. 78 | - Renamed application section title from "Ports Status" to "Network Interfaces Ports Status" 79 | - Icons moved to `icons` subdirectory. 80 | - Place application on the "Overview" page after "Memory" section. 81 | This became possible after applying patch to LuCI from pull request 82 | [2364](https://github.com/openwrt/luci/pull/2364). 83 | - Moved part of the JavaScript code to LuCI resources directory. 84 | - Renamed "Network interface" parameter to "Interface" in the table. 85 | - Renamed configuration parameter `global.hide_additional_info` 86 | to `global.default_additional_info`. 87 | - Renamed old type `usb` to `usb_rndis`. Old type name `usb` is supported but deprecated. 88 | - Changed icons for disabled state. 89 | - Use luabitop for bitwise operations. 90 | - Use polling interval from LuCI configuration (luci.main.pollinterval). 91 | - Totally rework JavaScript code. 92 | 93 | ### Deprecated 94 | - Use type `usb_rndis` instead of `usb`. 95 | 96 | ### Fixed 97 | - Output dash if no assigned firewall zones. 98 | - Fix administrative down state detection. 99 | - Fix port link status icon display in IE. 100 | - Fix Russian translations for "Connected", "Disconnected" and "Disabled". 101 | - Properly handling of the operative interface state from 102 | `/sys/class/net//operstate` sysfs file. 103 | - Fix ports type icons flickering on updates. 104 | 105 | ## [Version 1.0.0] (07.12.2018) 106 | 107 | Initial release 108 | 109 | [Unreleased]: https://github.com/tano-systems/luci-app-netports/tree/master 110 | [Version 2.0.3]: https://github.com/tano-systems/luci-app-netports/releases/tag/v2.0.3 111 | [Version 2.0.2]: https://github.com/tano-systems/luci-app-netports/releases/tag/v2.0.2 112 | [Version 2.0.1]: https://github.com/tano-systems/luci-app-netports/releases/tag/v2.0.1 113 | [Version 2.0.0]: https://github.com/tano-systems/luci-app-netports/releases/tag/v2.0.0 114 | [Version 1.1.1]: https://github.com/tano-systems/luci-app-netports/releases/tag/v1.1.1 115 | [Version 1.1.0]: https://github.com/tano-systems/luci-app-netports/releases/tag/v1.1.0 116 | [Version 1.0.0]: https://github.com/tano-systems/luci-app-netports/releases/tag/v1.0.0 117 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Tano Systems 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Network ports status LuCI application 2 | 3 | ## Description 4 | This package allows you to monitor current state of the network interfaces specified in configuration file (`/etc/config/luci_netports`). 5 | 6 | ## Dependencies 7 | This LuCI application developed for LuCI master branch. Application requires luabitop package to be installed. 8 | 9 | Master branch of this repository requires index status page client side view implementation in the LuCI (see commit https://github.com/openwrt/luci/commit/c85af3d7618b55c499ce4bf58e3896068bd413ae). Support for older LuCI releases (e.g. for version 18.06.x) is left in the [v1.x](https://github.com/tano-systems/luci-app-tn-netports/tree/v1.x) branch of this repository. 10 | 11 | ## Supported languages 12 | - English 13 | - Russian 14 | 15 | ## UCI configuration 16 | UCI configuration for this application is stored in `/etc/config/luci_netports` file. 17 | 18 | This file contain one `global` section with global application settings and one or more `port` sections with network ports settings. 19 | 20 | ### Global settings 21 | 22 | The `global` section contains global application settings. Default `global` section configuration: 23 | ``` 24 | config global 'global' 25 | option default_additional_info 'false' 26 | option default_h_mode 'false' 27 | option hv_mode_switch_button 'false' 28 | ``` 29 | 30 | These options can be set in the `global` section: 31 | 32 | | Name | Type | Required | Default | Description | 33 | | ------------------------- | ------- | -------- | ------- | ---------------------------------------------------------------------------- | 34 | | `default_additional_info` | boolean | no | false | Display additional information in horizontal view mode by default. | 35 | | `default_h_mode` | boolean | no | false | Use horizontal view mode by default. | 36 | | `hv_mode_switch_button` | boolean | no | false | Show button for manual switching between horizontal and vertical view modes. | 37 | 38 | Horizontal display mode can be used maximum for 6 interfaces. If there are more interfaces, the vertical display mode will be automatically activated without the possibility of switching to the horizontal mode. 39 | 40 | ### Ports settings 41 | 42 | Sections of the type `port` declare network port settings for displaying in application. 43 | 44 | A minimal port declaration consists of the following lines: 45 | ``` 46 | config port 47 | option ifname 'eth0' 48 | ``` 49 | These options can be set in the `port` sections: 50 | 51 | | Name | Type | Required | Default | Description | 52 | | ------------------------- | ------- | -------- | ------- | ---------------------------------------------------------------------------- | 53 | | `ifname` | string | yes | (none) | Network interface name. | 54 | | `name` | string | no | value of `ifname` | Custom port name for displaying in application. | 55 | | `type` | string | no | `auto` | Custom port type. Available port types listed in the table below. | 56 | | `disable` | boolean | no | false | Do not display specified interface. | 57 | 58 | Available port types (`type` setting): 59 | 60 | | Type(s) | Disabled icon | Linkdown icon | Linkup icon | Description | 61 | | ----- |:------------:|:--------------:|:------------:| :---------- | 62 | | `auto` | - | - | - | Automatically detect type by interface name (see next table). | 63 | | `copper` | ![copper_disabled](htdocs/luci-static/resources/netports/icons/copper_disabled.svg?sanitize=true) | ![copper_down](htdocs/luci-static/resources/netports/icons/copper_down.svg?sanitize=true) | ![copper_up](htdocs/luci-static/resources/netports/icons/copper_up.svg?sanitize=true) | Wired (RJ45) connection. | 64 | | `sfp` | ![sfp_disabled](htdocs/luci-static/resources/netports/icons/sfp_disabled.svg?sanitize=true) | ![sfp_down](htdocs/luci-static/resources/netports/icons/sfp_down.svg?sanitize=true) | ![sfp_up](htdocs/luci-static/resources/netports/icons/sfp_up.svg?sanitize=true) | SFP connection. | 65 | | `fixed` | ![fixed_disabled](htdocs/luci-static/resources/netports/icons/fixed_disabled.svg?sanitize=true) | ![fixed_down](htdocs/luci-static/resources/netports/icons/fixed_down.svg?sanitize=true) | ![fixed_up](htdocs/luci-static/resources/netports/icons/fixed_up.svg?sanitize=true) | Intercircuit fixed link connection. | 66 | | `wifi`, `usb_wifi` | ![wifi_disabled](htdocs/luci-static/resources/netports/icons/wifi_disabled.svg?sanitize=true) | ![wifi_down](htdocs/luci-static/resources/netports/icons/wifi_down.svg?sanitize=true) | ![wifi_up](htdocs/luci-static/resources/netports/icons/wifi_up.svg?sanitize=true) | Wireless connection. | 67 | | `usb_rndis` | ![usb_rndis_disabled](htdocs/luci-static/resources/netports/icons/usb_rndis_disabled.svg?sanitize=true) | ![usb_rndis_down](htdocs/luci-static/resources/netports/icons/usb_rndis_down.svg?sanitize=true) | ![usb_rndis_up](htdocs/luci-static/resources/netports/icons/usb_rndis_up.svg?sanitize=true) | USB RNDIS connection. | 68 | | `usb_stick`, `usb_2g`, `usb_3g`, `usb_4g` | ![usb_stick_disabled](htdocs/luci-static/resources/netports/icons/usb_stick_disabled.svg?sanitize=true) | ![usb_stick_down](htdocs/luci-static/resources/netports/icons/usb_stick_down.svg?sanitize=true) | ![usb_stick_up](htdocs/luci-static/resources/netports/icons/usb_stick_up.svg?sanitize=true) | USB 2G/3G/4G modem connection. | 69 | | `gprs` | ![gprs_disabled](htdocs/luci-static/resources/netports/icons/gprs_disabled.svg?sanitize=true) | ![gprs_down](htdocs/luci-static/resources/netports/icons/gprs_down.svg?sanitize=true) | ![gprs_up](htdocs/luci-static/resources/netports/icons/gprs_up.svg?sanitize=true) | GPRS connection. | 70 | | `vpn`, `tunnel` | ![tunnel_disabled](htdocs/luci-static/resources/netports/icons/tunnel_disabled.svg?sanitize=true) | ![tunnel_down](htdocs/luci-static/resources/netports/icons/tunnel_down.svg?sanitize=true) | ![tunnel_up](htdocs/luci-static/resources/netports/icons/tunnel_up.svg?sanitize=true) | Tunnel connection. | 71 | | `ppp` | ![ppp_disabled](htdocs/luci-static/resources/netports/icons/ppp_disabled.svg?sanitize=true) | ![ppp_down](htdocs/luci-static/resources/netports/icons/ppp_down.svg?sanitize=true) | ![ppp_up](htdocs/luci-static/resources/netports/icons/ppp_up.svg?sanitize=true) | PPP connection. | 72 | 73 | Automatic network port type detection (`auto` type): 74 | 75 | | Interface name regular expression(s) | Example(s) | Detected type | 76 | | :------------- | :--- | :--- | 77 | | `^eth\d+`,
`^sw\d+p\d+`,
`^en[a-z]\d+[a-z]\d+` | `eth0`, `eth32`,
`sw1p4`,
`enp0s4`, `ens9f0` | `copper` | 78 | | `^wlan\d+`,
`^wl[a-z]\d+[a-z]\d+` | `wlan0`,
`wlp0s4`, `wls1f4` | `wifi` | 79 | | `^usb\d+` | `usb0` | `usb_rndis` | 80 | | `^wwan\d+`,
`^ww[a-z]\d+[a-z]\d+` | `wwan0`,
`wwp0s4`, `wws1f4` | `usb_stick` | 81 | | `^ppp\d+` | `ppp0` | `ppp` | 82 | | `^tun\d+`,
`^tap\d+`,
`^wg\d+` | `tun0`, `tap0`, `wg0` | `tunnel` | 83 | 84 | ## Screenshots 85 | 86 | ### Vertical mode 87 | 88 | ![Vertical view mode](screenshots/v-mode-5-ports.png?raw=true "Vertical view mode") 89 | 90 | ### Horizontal mode 91 | 92 | Without additional information: 93 | 94 | ![Horizontal view mode](screenshots/h-mode-5-ports.png?raw=true "Horizontal view mode") 95 | 96 | With additional information: 97 | 98 | ![Horizontal view mode (with extra information)](screenshots/h-mode-5-ports-extra.png?raw=true "Horizontal view mode (with extra information)") 99 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2020 Tano Systems LLC. All Rights Reserved. 3 | * Anton Kikin 4 | */ 5 | 6 | 'use strict'; 7 | 'require ui'; 8 | 'require uci'; 9 | 'require firewall'; 10 | 11 | const NetPortsMode = { 12 | H: 0, 13 | V: 1 14 | }; 15 | 16 | const NetPortsVersion = "2.0.3" 17 | 18 | const svgModeSwitch = 19 | '' + 20 | ''; 33 | 34 | const svgExpand = 35 | '' + 36 | ''; 39 | 40 | const svgCollapse = 41 | '' + 42 | ''; 45 | 46 | var NetPorts = L.Class.extend({ 47 | NetPorts: function(inputConfig) { 48 | var config = { 49 | targetElement: null, 50 | tblCellClasses: 'top left', 51 | mode: NetPortsMode.V, 52 | modeSwitchButton: false, 53 | autoSwitchHtoV: true, 54 | autoSwitchHtoVThreshold: 6, 55 | hModeFirstColWidth: 20, 56 | hModeExpanded: false, 57 | } 58 | 59 | var self = this; 60 | 61 | var fullUpdate = true; 62 | var targetElement = null; 63 | var tableElement = null; 64 | var mode = NetPortsMode.V; 65 | var currentData = null; 66 | 67 | var fmtNameAndMAC = function(portData) { 68 | var elements = [ E('strong', {}, portData.name) ]; 69 | 70 | if (portData.hwaddr) { 71 | elements.push(E('br', {})); 72 | elements.push(fmtMAC(portData)); 73 | } 74 | 75 | return elements; 76 | } 77 | 78 | var fmtMAC = function(portData) { 79 | return portData.hwaddr ? portData.hwaddr.toUpperCase() : '\u00a0'; /*   */ 80 | } 81 | 82 | var fmtStatus = function(portData) { 83 | var status = ''; 84 | var icon = ''; 85 | var phyup = 0; 86 | var adminup = 0; 87 | 88 | phyup = parseInt(portData.carrier); 89 | 90 | if (portData.adminstate === "up") 91 | adminup = 1; 92 | 93 | if (adminup) 94 | { 95 | if (phyup) 96 | icon = portData.type + '_up.svg'; 97 | else 98 | icon = portData.type + '_down.svg'; 99 | } 100 | else 101 | icon = portData.type + '_disabled.svg'; 102 | 103 | var status = E('div', { 104 | class: 'netports-linkstatus-icon-container netports-linkstatus-icon-container-' + 105 | ((config.mode === NetPortsMode.H) ? 'h' : 'v') 106 | }); 107 | 108 | status.appendChild(E('img', { 109 | class: "netports-linkstatus-icon", 110 | src: L.resource('netports/icons/' + icon) 111 | })); 112 | 113 | var statusText = E('div', { class: "netports-linkstatus-text" }); 114 | 115 | if (adminup) 116 | { 117 | if (phyup) 118 | { 119 | var speed = parseInt(portData.speed); 120 | 121 | if (speed > 0) 122 | statusText.innerHTML = speed + '\u00a0' + _('Mbit/s'); 123 | else 124 | statusText.innerHTML = _('Connected', 'Link status'); 125 | 126 | if (portData.duplex === "full") 127 | statusText.innerHTML += ',
' + _('full-duplex'); 128 | else if (portData.duplex === "half") 129 | statusText.innerHTML += ',
' + _('half-duplex'); 130 | } 131 | else 132 | { 133 | statusText.appendChild(E('span', { class: "netports-linkstatus-text-disconnected" }, _('Disconnected', 'Link status'))); 134 | statusText.appendChild(E('br', {})); 135 | statusText.appendChild(document.createTextNode('\u00a0')); /*   */ 136 | } 137 | } 138 | else 139 | { 140 | statusText.appendChild(E('span', { class: "netports-linkstatus-text-disabled" }, _('Disabled', 'Link status'))); 141 | statusText.appendChild(E('br', {})); 142 | statusText.appendChild(document.createTextNode('\u00a0')); /*   */ 143 | } 144 | 145 | return [ status, statusText ]; 146 | } 147 | 148 | var fmtNetIf = function(portData) { 149 | var v = portData.ifname 150 | if (portData.ntm && (portData.ntm.netname || portData.ntm.wifiname)) 151 | { 152 | if (portData.ntm.netname) 153 | { 154 | v += ' (' 155 | + portData.ntm.netname.toUpperCase() + ')'; 156 | } 157 | 158 | if (portData.ntm.wifiname) 159 | { 160 | v += "
"; 161 | v += '[' 162 | + portData.ntm.wifiname + ']'; 163 | } 164 | } 165 | return v; 166 | } 167 | 168 | var fmtBridgeIf = function(portData) { 169 | if (portData.bridge) 170 | { 171 | var v = portData.bridge.ifname; 172 | 173 | if (portData.ntm_bridge && portData.ntm_bridge.netname) 174 | v += ' (' 175 | + portData.ntm_bridge.netname.toUpperCase() + ')'; 176 | 177 | v += ',
' + _('port %d').format(portData.bridge.port); 178 | 179 | return v; 180 | } 181 | else 182 | return '–'; 183 | } 184 | 185 | var fmtFwZones = function(portData) { 186 | var z = ''; 187 | var ntm = []; 188 | var out_ifname = false; 189 | 190 | if (portData.ntm && portData.ntm.fwzone) 191 | ntm.push(portData.ntm); 192 | 193 | if (portData.ntm_bridge && portData.ntm_bridge.fwzone) 194 | ntm.push(portData.ntm_bridge); 195 | 196 | if (ntm.length == 0) 197 | return '–'; 198 | 199 | out_ifname = ntm.length > 1; 200 | 201 | ntm.forEach(function(n) { 202 | var ifname = ''; 203 | 204 | z += '
'; 205 | z += '
'; 206 | 207 | if (out_ifname) 208 | ifname = n.netname.toUpperCase() + ': '; 209 | 210 | z += n.fwzone 211 | ? '' 212 | + '' 213 | + ifname + n.fwzone + '' 214 | : '' + _('none') + ''; 215 | 216 | z += '
'; 217 | }) 218 | 219 | return z; 220 | } 221 | 222 | var fmtTx = function(portData) { 223 | if (portData.stats.tx_bytes) 224 | { 225 | return [ 226 | _('%1024.2mB').format(portData.stats.tx_bytes), 227 | E('br', {}), 228 | '(%d\u00a0%s)'.format(portData.stats.tx_packets, _('pkts.')) 229 | ]; 230 | } 231 | else 232 | return '–'; 233 | } 234 | 235 | var fmtRx = function(portData) { 236 | if (portData.stats.rx_bytes) 237 | { 238 | return [ 239 | _('%1024.2mB').format(portData.stats.rx_bytes), 240 | E('br', {}), 241 | '(%d\u00a0%s)'.format(portData.stats.rx_packets, _('pkts.')) 242 | ]; 243 | } 244 | else 245 | return '–'; 246 | } 247 | 248 | var dataTitles = [ 249 | { title: _('Name and MAC-address'), vModeMinWidth: "130px", fmtFunc: fmtNameAndMAC, hModeDisable: true }, 250 | { title: _('Link status'), vModeMinWidth: "165px", fmtFunc: fmtStatus }, 251 | { title: _('Interface'), fmtFunc: fmtNetIf, hModeExtra: true }, 252 | { title: _('Bridge member'), fmtFunc: fmtBridgeIf, hModeExtra: true }, 253 | { title: _('Firewall zones'), fmtFunc: fmtFwZones }, 254 | { title: _('RX'), fmtFunc: fmtRx, hModeExtra: true }, 255 | { title: _('TX'), fmtFunc: fmtTx, hModeExtra: true }, 256 | { title: _('MAC-address'), fmtFunc: fmtMAC, vModeDisable: true, hModeExtra: true }, 257 | ] 258 | 259 | 260 | var clear = function() { 261 | /* Clear all child elements for target */ 262 | while (targetElement.firstChild) { 263 | targetElement.removeChild(targetElement.firstChild); 264 | } 265 | } 266 | 267 | var clearTbl = function() { 268 | /* Clear all child elements for table */ 269 | while (tableElement.firstChild) { 270 | tableElement.removeChild(tableElement.firstChild); 271 | } 272 | } 273 | 274 | var btnModeSwitch = null; 275 | var btnExpand = null; 276 | var buttons = []; 277 | 278 | var setMode = function(mode) { 279 | if (config.mode == mode) 280 | return; 281 | 282 | if ((mode !== NetPortsMode.V) && 283 | (mode !== NetPortsMode.H)) 284 | return; 285 | 286 | config.mode = mode; 287 | fullUpdate = true; 288 | updateData(currentData); 289 | updateButtons(); 290 | } 291 | 292 | var setHModeExpanded = function(expanded) { 293 | if (config.hModeExpanded == expanded) 294 | return; 295 | 296 | config.hModeExpanded = expanded; 297 | 298 | var rows = tableElement.querySelectorAll('.tr.netports-extra'); 299 | 300 | rows.forEach(function(row) { 301 | row.style.display = 302 | (config.hModeExpanded) ? "table-row" : "none"; 303 | }); 304 | 305 | updateButtons(); 306 | } 307 | 308 | var createButtons = function() { 309 | btnExpand = 310 | E('button', { class: 'cbi-button', title: _('Toggle additional information') }, svgExpand); 311 | 312 | btnExpand.addEventListener('click', function() { 313 | if (config.hModeExpanded == true) 314 | setHModeExpanded(false); 315 | else 316 | setHModeExpanded(true); 317 | }); 318 | 319 | buttons.push(btnExpand); 320 | 321 | if (config.modeSwitchButton) { 322 | btnModeSwitch = 323 | E('button', { class: 'cbi-button', title: _('Toggle view mode') }, svgModeSwitch); 324 | 325 | btnModeSwitch.addEventListener('click', function() { 326 | if (config.mode == NetPortsMode.V) 327 | setMode(NetPortsMode.H); 328 | else 329 | setMode(NetPortsMode.V); 330 | }); 331 | 332 | buttons.push(btnModeSwitch); 333 | } 334 | } 335 | 336 | var updateButtons = function() { 337 | if (config.mode == NetPortsMode.H) { 338 | if (config.hModeExpanded) 339 | btnExpand.innerHTML = svgCollapse; 340 | else 341 | btnExpand.innerHTML = svgExpand; 342 | } 343 | 344 | btnExpand.style.display = 345 | (config.mode == NetPortsMode.H) ? "" : "none"; 346 | } 347 | 348 | var createBase = function() { 349 | clear(); 350 | createButtons(); 351 | updateButtons(); 352 | 353 | var title = E('div', { class: 'netports-title' }, [ 354 | E('div', { class: 'netports-copyright' }, 355 | E('a', { href: 'https://github.com/tano-systems/luci-app-tn-netports' }, 356 | 'luci-app-tn-netports ' + NetPortsVersion) 357 | ), 358 | E('div', { class: 'netports-buttons' }, buttons) 359 | ]); 360 | 361 | var table = E('div', { class: 'table netports-table' }, [ 362 | E('div', { class: 'tr table-titles' }, 363 | E('div', { class: 'th top center' }, '...') 364 | ), 365 | E('div', { class: 'tr placeholder' }, 366 | E('div', { class: 'td' }, 367 | E('em', { class: 'spinning' }, _('Collecting data...')) 368 | ) 369 | ) 370 | ]); 371 | 372 | var tableWrapper = E('div', { class: 'table-wrapper' }, table); 373 | 374 | targetElement.appendChild(title); 375 | targetElement.appendChild(tableWrapper); 376 | 377 | tableElement = table; 378 | } 379 | 380 | var init = function(inputConfig) { 381 | config.targetElement = inputConfig.target; 382 | config.mode = inputConfig.mode !== undefined ? Number(inputConfig.mode) : config.mode; 383 | config.modeSwitchButton = inputConfig.modeSwitchButton !== undefined ? 384 | inputConfig.modeSwitchButton : config.modeSwitchButton; 385 | config.hModeExpanded = inputConfig.hModeExpanded !== undefined 386 | ? inputConfig.hModeExpanded : config.hModeExpanded; 387 | config.hModeFirstColWidth = inputConfig.hModeFirstColWidth !== undefined 388 | ? inputConfig.hModeFirstColWidth : config.hModeFirstColWidth; 389 | config.autoSwitchHtoV = inputConfig.autoSwitchHtoV !== undefined 390 | ? inputConfig.autoSwitchHtoV : config.autoSwitchHtoV; 391 | config.autoSwitchHtoVThreshold = inputConfig.autoSwitchHtoVThreshold !== undefined 392 | ? inputConfig.autoSwitchHtoVThreshold : config.autoSwitchHtoVThreshold; 393 | 394 | targetElement = config.targetElement; 395 | 396 | createBase(); 397 | } 398 | 399 | var updateData = function(data) { 400 | if (!data || !data.data) { 401 | data = { data: [], count: 0 }; 402 | } 403 | 404 | if ((config.mode == NetPortsMode.H) && config.autoSwitchHtoV) { 405 | if (data.data.length > config.autoSwitchHtoVThreshold) { 406 | /* Auto switch to V */ 407 | if (config.mode !== NetPortsMode.V) { 408 | config.mode = NetPortsMode.V; 409 | fullUpdate = true; 410 | 411 | if (btnModeSwitch) { 412 | btnModeSwitch.setAttribute('disabled', true); 413 | btnModeSwitch.setAttribute('title', _('Too many ports for horizontal display mode')); 414 | } 415 | 416 | updateButtons(); 417 | } 418 | } 419 | else { 420 | if (btnModeSwitch) { 421 | btnModeSwitch.removeAttribute('disabled'); 422 | btnModeSwitch.setAttribute('title', _('Toggle view mode')); 423 | } 424 | } 425 | } 426 | 427 | if (fullUpdate || !currentData) { 428 | clearTbl(); 429 | if (config.mode == NetPortsMode.V) { 430 | updateTblHeader(data.data); 431 | } 432 | fullUpdate = false; 433 | } 434 | 435 | if (config.mode == NetPortsMode.H) { 436 | updateTblHeader(data.data); 437 | } 438 | 439 | updateTbl(data.data); 440 | currentData = data; 441 | } 442 | 443 | var updateTblHeader = function(data) { 444 | if (config.mode == NetPortsMode.V) { 445 | /* Vertical mode */ 446 | var titles = []; 447 | 448 | dataTitles.forEach(function(t) { 449 | if (t.vModeDisable) 450 | return; 451 | 452 | titles.push(E('div', { 453 | class: 'th ' + config.tblCellClasses, 454 | style: (t.vModeMinWidth ? 'min-width: ' + t.vModeMinWidth + ';' : '') 455 | }, t.title)) 456 | }); 457 | 458 | tableElement.appendChild(E('div', { class: 'tr table-titles' }, titles)); 459 | } 460 | else { 461 | /* Horizontal mode */ 462 | var row = []; 463 | var len = data.length; 464 | 465 | if (len == 0) { 466 | row.push(E('div', { class: 'th top center' }, '...')); 467 | } 468 | else { 469 | /* First column */ 470 | row.push(E('div', { 471 | class: 'th ' + config.tblCellClasses, 472 | style: 'width: ' + config.hModeFirstColWidth + '%;' 473 | })); 474 | 475 | /* Other columns */ 476 | var col_width = (100 - config.hModeFirstColWidth) / len; 477 | 478 | for (let p = 0; p < len; p++) { 479 | row.push(E('div', { 480 | class: 'th ' + config.tblCellClasses, 481 | style: 'width: ' + col_width + '%;' 482 | }, data[p].name)); 483 | } 484 | } 485 | 486 | var thead = tableElement.querySelectorAll('.tr.table-titles'); 487 | var trow = E('div', { class: 'tr table-titles' }, row); 488 | 489 | if (thead.length) { 490 | if (trow.innerHTML !== thead.innerHTML) 491 | tableElement.replaceChild(trow, thead[0]); 492 | } 493 | else { 494 | tableElement.appendChild(trow); 495 | } 496 | } 497 | } 498 | 499 | var updateTbl = function(data) { 500 | if (!Array.isArray(data)) 501 | return; 502 | 503 | var rows = tableElement.querySelectorAll('.tr'); 504 | var n = 0; 505 | 506 | if (config.mode == NetPortsMode.V) { 507 | data.forEach(function(port) { 508 | var tcells = [] 509 | 510 | dataTitles.forEach(function(t) { 511 | if (t.vModeDisable) 512 | return; 513 | 514 | tcells.push(E('div', { 515 | 'class': 'td ' + config.tblCellClasses }, t.fmtFunc(port))); 516 | }); 517 | 518 | var trow = E('div', { 'class': 'tr' }, tcells); 519 | trow.classList.add('cbi-rowstyle-%d'.format((n++ % 2) ? 2 : 1)); 520 | 521 | if (rows[n]) { 522 | /* 523 | * Update only changed cells. 524 | * This avoids flickering for port type icon. 525 | */ 526 | var cells_orig = rows[n].querySelectorAll('.td'); 527 | 528 | for (let cn = 0; cn < cells_orig.length; cn++) { 529 | if (cells_orig[cn].innerHTML !== tcells[cn].innerHTML) { 530 | rows[n].replaceChild(tcells[cn], cells_orig[cn]); 531 | } 532 | } 533 | } 534 | else 535 | tableElement.appendChild(trow); 536 | }); 537 | } 538 | else { 539 | if (data.length) { 540 | dataTitles.forEach(function(t) { 541 | if (t.hModeDisable) 542 | return; 543 | 544 | var tcells = [] 545 | 546 | tcells.push(E('div', { 'class': 'td ' + config.tblCellClasses }, t.title)) 547 | data.forEach(function(port) { 548 | tcells.push(E('div', { 'class': 'td ' + config.tblCellClasses }, t.fmtFunc(port))); 549 | }); 550 | 551 | var trow = E('div', { 'class': 'tr' }, tcells); 552 | trow.classList.add('cbi-rowstyle-%d'.format((n++ % 2) ? 2 : 1)); 553 | if (t.hModeExtra) 554 | { 555 | trow.classList.add('netports-extra'); 556 | trow.style.display = 557 | (config.hModeExpanded) ? "table-row" : "none"; 558 | } 559 | 560 | if (rows[n]) { 561 | /* 562 | * Update only changed cells. 563 | * This avoids flickering for port type icon. 564 | */ 565 | var cells_orig = rows[n].querySelectorAll('.td'); 566 | 567 | if (cells_orig.length != tcells.length) { 568 | tableElement.replaceChild(trow, rows[n]); 569 | } 570 | else { 571 | for (let cn = 0; cn < cells_orig.length; cn++) { 572 | if (cells_orig[cn].innerHTML !== tcells[cn].innerHTML) { 573 | rows[n].replaceChild(tcells[cn], cells_orig[cn]); 574 | } 575 | } 576 | } 577 | } 578 | else 579 | tableElement.appendChild(trow); 580 | }); 581 | } 582 | } 583 | 584 | while (rows[++n]) 585 | tableElement.removeChild(rows[n]); 586 | 587 | if (tableElement.firstElementChild === tableElement.lastElementChild) { 588 | var trow = tableElement.appendChild(E('div', { 'class': 'tr placeholder' })); 589 | var td = trow.appendChild(E('div', { 'class': 'td top center' }, _('No data to display'))); 590 | } 591 | } 592 | 593 | /* Public API */ 594 | this.init = init; 595 | this.updateData = updateData; 596 | 597 | /* Init */ 598 | init(inputConfig); 599 | } 600 | }); 601 | 602 | return NetPorts; 603 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/buttons/retweet.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/copper_disabled.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/copper_down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/copper_up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/fixed_disabled.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/fixed_down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/fixed_up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/gprs_disabled.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/gprs_down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/gprs_up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/ppp_disabled.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/ppp_down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/ppp_up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/sfp_disabled.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/sfp_down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/sfp_up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/tunnel_disabled.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/tunnel_down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/tunnel_up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/usb_rndis_disabled.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/usb_rndis_down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/usb_rndis_up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/usb_stick_disabled.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/usb_stick_down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/usb_stick_up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/wifi_disabled.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/wifi_down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/icons/wifi_up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/netports/netports.css: -------------------------------------------------------------------------------- 1 | .netports-table .td, 2 | .netports-table .th { 3 | line-height: 1.3em; 4 | } 5 | .netports-linkstatus-icon-container { 6 | width: 42px; 7 | } 8 | .netports-linkstatus-icon { 9 | display: inline-block; 10 | max-width: 100%; 11 | width: 100%; 12 | height: auto; 13 | } 14 | .netports-linkstatus-icon-container-v { 15 | float: left; 16 | margin-right: 8px; 17 | } 18 | .netports-linkstatus-text { 19 | white-space: nowrap; 20 | } 21 | .netports-linkstatus-text-disconnected, 22 | .netports-linkstatus-text-disabled { 23 | color: #bbbbbb; 24 | } 25 | .netports-ifacebox, 26 | .netports-ifacebox-head { 27 | width: auto !important; 28 | width: fit-content !important; 29 | width: -moz-fit-content !important; 30 | width: -webkit-fit-content !important; 31 | } 32 | .netports-ifacebox { 33 | display: block; 34 | padding: 0; 35 | margin: 0; 36 | min-width: auto; 37 | } 38 | .netports-ifacebox-head { 39 | padding-right: 8px; 40 | padding-left: 8px; 41 | text-align: center; 42 | min-width: 100px; 43 | } 44 | .netports-ifacebox-head a { 45 | color: inherit; 46 | } 47 | .netports-title { 48 | text-align: left; 49 | justify-content: space-between; 50 | display: flex; 51 | flex-flow: row-reverse; 52 | align-items: flex-end; 53 | } 54 | .netports-copyright { 55 | margin: 0; 56 | color: #ccc; 57 | font-size: 85%; 58 | white-space: nowrap; 59 | } 60 | .netports-buttons { 61 | margin: 0 6px 0 0; 62 | padding: 0; 63 | display: flex; 64 | flex-flow: row-reverse; 65 | } 66 | .netports-buttons button { 67 | margin-bottom: 8px; 68 | margin-right: 4px; 69 | height: 22px; 70 | padding: 0 10px; 71 | display: inline-flex; 72 | background: linear-gradient(#fff, #fff 25%, #e6e6e6) no-repeat; 73 | box-shadow: 0 1px 3px 0 grey; 74 | } 75 | .netports-buttons button:disabled { 76 | opacity: 0.6; 77 | } 78 | .netports-buttons button svg { 79 | width: 18px; 80 | height: 18px; 81 | vertical-align: middle; 82 | fill: #333; 83 | } 84 | .netports-buttons button:hover svg { 85 | fill: #008dd2; 86 | } 87 | .netports-buttons button:disabled svg { 88 | fill: #aaa; 89 | } 90 | .netports-copyright a { 91 | color: #ccc; 92 | font-weight: normal; 93 | text-decoration: none; 94 | } 95 | .netports-copyright a:hover { 96 | color: #aaa; 97 | text-decoration: underline; 98 | } 99 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/view/status/include/25_netports.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Tano Systems. All Rights Reserved. 3 | * Anton Kikin 4 | */ 5 | 6 | 'use strict'; 7 | 'require rpc'; 8 | 'require uci'; 9 | 'require netports'; 10 | 11 | var callSessionAccess = rpc.declare({ 12 | object: 'session', 13 | method: 'access', 14 | params: [ 'scope', 'object', 'function' ], 15 | expect: { 'access': false } 16 | }); 17 | 18 | var callNetPortsGetInfo = rpc.declare({ 19 | object: 'netports', 20 | method: 'getPortsInfo', 21 | expect: { '': {} } 22 | }); 23 | 24 | var netports_el = E('div', {}); 25 | var netports_object = null; 26 | 27 | return L.Class.extend({ 28 | __init__: function() { 29 | var head = document.getElementsByTagName('head')[0]; 30 | var css = E('link', { 'href': L.resource('netports/netports.css'), 'rel': 'stylesheet' }); 31 | head.appendChild(css); 32 | 33 | uci.load('luci_netports').then(function() { 34 | var np_default_additional_info = 35 | parseInt(uci.get('luci_netports', 'global', 'default_additional_info')) == 1; 36 | 37 | var np_default_h_mode = 38 | parseInt(uci.get('luci_netports', 'global', 'default_h_mode')) == 1; 39 | 40 | var np_hv_mode_switch_button = 41 | parseInt(uci.get('luci_netports', 'global', 'hv_mode_switch_button')) == 1; 42 | 43 | netports_object = new netports.NetPorts({ 44 | target: netports_el, 45 | mode: np_default_h_mode ? 0 : 1, 46 | modeSwitchButton: np_hv_mode_switch_button, 47 | autoSwitchHtoV: true, 48 | autoSwitchHtoVThreshold: 6, 49 | hModeFirstColWidth: 20, 50 | hModeExpanded: np_default_additional_info, 51 | }); 52 | }); 53 | }, 54 | 55 | title: _('Network Interfaces Ports Status'), 56 | 57 | load: function() { 58 | return Promise.all([ 59 | L.resolveDefault(callNetPortsGetInfo(), {}), 60 | callSessionAccess('access-group', 'luci-app-tn-netports', 'read'), 61 | ]); 62 | }, 63 | 64 | render: function(data) { 65 | var hasReadPermission = data[1]; 66 | 67 | if (!hasReadPermission) 68 | return E([]); 69 | 70 | if (netports_object) 71 | netports_object.updateData(data[0]); 72 | 73 | return E([ netports_el ]); 74 | } 75 | }); 76 | -------------------------------------------------------------------------------- /i18n.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p po/templates 4 | 5 | i18n-scan.pl . > po/templates/netports.pot 6 | i18n-update.pl po 7 | -------------------------------------------------------------------------------- /make-archive.cmd: -------------------------------------------------------------------------------- 1 | git archive --format=tar.gz --prefix=luci-app-netports/ --output=luci-app-netports.tar.gz HEAD 2 | -------------------------------------------------------------------------------- /po/ru/netports.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Content-Type: text/plain; charset=UTF-8\n" 4 | "Project-Id-Version: \n" 5 | "POT-Creation-Date: \n" 6 | "PO-Revision-Date: 2020-04-19 06:54+0300\n" 7 | "Last-Translator: Anton Kikin \n" 8 | "Language-Team: \n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "Language: ru\n" 12 | "X-Generator: Poedit 2.2\n" 13 | 14 | #: htdocs/luci-static/resources/netports.js:223 15 | #: htdocs/luci-static/resources/netports.js:236 16 | msgid "%1024.2mB" 17 | msgstr "" 18 | 19 | #: htdocs/luci-static/resources/netports.js:249 20 | msgid "Bridge member" 21 | msgstr "В составе моста" 22 | 23 | #: htdocs/luci-static/resources/netports.js:364 24 | msgid "Collecting data..." 25 | msgstr "Сбор данных..." 26 | 27 | #: htdocs/luci-static/resources/netports.js:121 28 | msgctxt "Link status" 29 | msgid "Connected" 30 | msgstr "Подключено" 31 | 32 | #: htdocs/luci-static/resources/netports.js:137 33 | msgctxt "Link status" 34 | msgid "Disabled" 35 | msgstr "Отключено" 36 | 37 | #: htdocs/luci-static/resources/netports.js:130 38 | msgctxt "Link status" 39 | msgid "Disconnected" 40 | msgstr "Не подключено" 41 | 42 | #: htdocs/luci-static/resources/netports.js:250 43 | msgid "Firewall zones" 44 | msgstr "Зоны межсетевого экрана" 45 | 46 | #: root/usr/share/rpcd/acl.d/netports.json:3 47 | msgid "Grant access to network ports status information" 48 | msgstr "Предоставить доступ к информации о состоянии сетевых портов" 49 | 50 | #: htdocs/luci-static/resources/netports.js:248 51 | msgid "Interface" 52 | msgstr "Интерфейс" 53 | 54 | #: htdocs/luci-static/resources/netports.js:247 55 | msgid "Link status" 56 | msgstr "Состояние подключения" 57 | 58 | #: htdocs/luci-static/resources/netports.js:253 59 | msgid "MAC-address" 60 | msgstr "MAC-адрес" 61 | 62 | #: htdocs/luci-static/resources/netports.js:119 63 | msgid "Mbit/s" 64 | msgstr "Мбит/с" 65 | 66 | #: htdocs/luci-static/resources/netports.js:246 67 | msgid "Name and MAC-address" 68 | msgstr "Имя и MAC-адрес" 69 | 70 | #: htdocs/luci-static/resources/view/status/include/25_netports.js:48 71 | msgid "Network Interfaces Ports Status" 72 | msgstr "Состояние портов сетевых интерфейсов" 73 | 74 | #: htdocs/luci-static/resources/netports.js:586 75 | msgid "No data to display" 76 | msgstr "Нет данных для отображения" 77 | 78 | #: htdocs/luci-static/resources/netports.js:251 79 | msgid "RX" 80 | msgstr "Получение (RX)" 81 | 82 | #: htdocs/luci-static/resources/netports.js:252 83 | msgid "TX" 84 | msgstr "Передача (TX)" 85 | 86 | #: htdocs/luci-static/resources/netports.js:307 87 | msgid "Toggle additional information" 88 | msgstr "Показать/скрыть дополнительную информацию" 89 | 90 | #: htdocs/luci-static/resources/netports.js:320 91 | #: htdocs/luci-static/resources/netports.js:419 92 | msgid "Toggle view mode" 93 | msgstr "Переключить режим отображения" 94 | 95 | #: htdocs/luci-static/resources/netports.js:410 96 | msgid "Too many ports for horizontal display mode" 97 | msgstr "Слишком много портов для горизонтального режима отображения" 98 | 99 | #: htdocs/luci-static/resources/netports.js:124 100 | msgid "full-duplex" 101 | msgstr "полный дуплекс" 102 | 103 | #: htdocs/luci-static/resources/netports.js:126 104 | msgid "half-duplex" 105 | msgstr "полу-дуплекс" 106 | 107 | #: htdocs/luci-static/resources/netports.js:211 108 | msgid "none" 109 | msgstr "нет" 110 | 111 | #: htdocs/luci-static/resources/netports.js:225 112 | #: htdocs/luci-static/resources/netports.js:238 113 | msgid "pkts." 114 | msgstr "пакетов" 115 | 116 | #: htdocs/luci-static/resources/netports.js:174 117 | msgid "port %d" 118 | msgstr "порт %d" 119 | 120 | #~ msgid "%.2mB" 121 | #~ msgstr "%.2mБ" 122 | -------------------------------------------------------------------------------- /po/templates/netports.pot: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "Content-Type: text/plain; charset=UTF-8" 3 | 4 | #: htdocs/luci-static/resources/netports.js:223 5 | #: htdocs/luci-static/resources/netports.js:236 6 | msgid "%1024.2mB" 7 | msgstr "" 8 | 9 | #: htdocs/luci-static/resources/netports.js:249 10 | msgid "Bridge member" 11 | msgstr "" 12 | 13 | #: htdocs/luci-static/resources/netports.js:364 14 | msgid "Collecting data..." 15 | msgstr "" 16 | 17 | #: htdocs/luci-static/resources/netports.js:121 18 | msgctxt "Link status" 19 | msgid "Connected" 20 | msgstr "" 21 | 22 | #: htdocs/luci-static/resources/netports.js:137 23 | msgctxt "Link status" 24 | msgid "Disabled" 25 | msgstr "" 26 | 27 | #: htdocs/luci-static/resources/netports.js:130 28 | msgctxt "Link status" 29 | msgid "Disconnected" 30 | msgstr "" 31 | 32 | #: htdocs/luci-static/resources/netports.js:250 33 | msgid "Firewall zones" 34 | msgstr "" 35 | 36 | #: root/usr/share/rpcd/acl.d/netports.json:3 37 | msgid "Grant access to network ports status information" 38 | msgstr "" 39 | 40 | #: htdocs/luci-static/resources/netports.js:248 41 | msgid "Interface" 42 | msgstr "" 43 | 44 | #: htdocs/luci-static/resources/netports.js:247 45 | msgid "Link status" 46 | msgstr "" 47 | 48 | #: htdocs/luci-static/resources/netports.js:253 49 | msgid "MAC-address" 50 | msgstr "" 51 | 52 | #: htdocs/luci-static/resources/netports.js:119 53 | msgid "Mbit/s" 54 | msgstr "" 55 | 56 | #: htdocs/luci-static/resources/netports.js:246 57 | msgid "Name and MAC-address" 58 | msgstr "" 59 | 60 | #: htdocs/luci-static/resources/view/status/include/25_netports.js:48 61 | msgid "Network Interfaces Ports Status" 62 | msgstr "" 63 | 64 | #: htdocs/luci-static/resources/netports.js:586 65 | msgid "No data to display" 66 | msgstr "" 67 | 68 | #: htdocs/luci-static/resources/netports.js:251 69 | msgid "RX" 70 | msgstr "" 71 | 72 | #: htdocs/luci-static/resources/netports.js:252 73 | msgid "TX" 74 | msgstr "" 75 | 76 | #: htdocs/luci-static/resources/netports.js:307 77 | msgid "Toggle additional information" 78 | msgstr "" 79 | 80 | #: htdocs/luci-static/resources/netports.js:320 81 | #: htdocs/luci-static/resources/netports.js:419 82 | msgid "Toggle view mode" 83 | msgstr "" 84 | 85 | #: htdocs/luci-static/resources/netports.js:410 86 | msgid "Too many ports for horizontal display mode" 87 | msgstr "" 88 | 89 | #: htdocs/luci-static/resources/netports.js:124 90 | msgid "full-duplex" 91 | msgstr "" 92 | 93 | #: htdocs/luci-static/resources/netports.js:126 94 | msgid "half-duplex" 95 | msgstr "" 96 | 97 | #: htdocs/luci-static/resources/netports.js:211 98 | msgid "none" 99 | msgstr "" 100 | 101 | #: htdocs/luci-static/resources/netports.js:225 102 | #: htdocs/luci-static/resources/netports.js:238 103 | msgid "pkts." 104 | msgstr "" 105 | 106 | #: htdocs/luci-static/resources/netports.js:174 107 | msgid "port %d" 108 | msgstr "" 109 | -------------------------------------------------------------------------------- /root/usr/libexec/rpcd/netports: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | -- 3 | -- Copyright (c) 2018-2019, Tano Systems. All Rights Reserved. 4 | -- Anton Kikin 5 | -- 6 | 7 | local json = require "luci.jsonc" 8 | local fs = require "nixio.fs" 9 | 10 | local sysfs_net_root = "/sys/class/net" 11 | 12 | local function sysfs_net_read(ifname, file) 13 | local v = nil 14 | if fs.access(sysfs_net_root .. "/%s/%s" % {ifname, file}) then 15 | v = fs.readfile(sysfs_net_root .. "/%s/%s" % {ifname, file}) 16 | if v then 17 | v = v:gsub("\n", "") 18 | end 19 | end 20 | return v or '' 21 | end 22 | 23 | local function sysfs_net_read_stats(ifname) 24 | local s = { 25 | tx_bytes = tonumber(sysfs_net_read(ifname, "statistics/tx_bytes")) or 0, 26 | tx_packets = tonumber(sysfs_net_read(ifname, "statistics/tx_packets")) or 0, 27 | rx_bytes = tonumber(sysfs_net_read(ifname, "statistics/rx_bytes")) or 0, 28 | rx_packets = tonumber(sysfs_net_read(ifname, "statistics/rx_packets")) or 0, 29 | } 30 | 31 | return s 32 | end 33 | 34 | local function sysfs_net_read_bridge(ifname) 35 | local b = { } 36 | 37 | -- Bridge port number 38 | b["port"] = tonumber(sysfs_net_read(ifname, "brport/port_no")) or 0 39 | 40 | if b["port"] == 0 then 41 | return nil 42 | end 43 | 44 | -- Get bridge system interface name 45 | local b_path = fs.readlink(sysfs_net_root .. "/%s/brport/bridge" % ifname) 46 | if b_path then 47 | b["ifname"] = fs.basename(b_path) 48 | end 49 | 50 | return b 51 | end 52 | 53 | local function type_autodetect(ifname) 54 | local matches = { 55 | copper = { "^eth%d+", "^en%l%d+%l%d+", "^sw%d+p%d+" }, 56 | usb_rndis = { "^usb%d+" }, 57 | usb_stick = { "^wwan%d+", "^ww%l%d+%l%d+" }, 58 | ppp = { "^ppp%d+" }, 59 | tunnel = { "^tun%d+", "^tap%d+", "^wg%d+" }, 60 | wifi = { "^wlan%d+", "^wl%l%d+%l%d+" } 61 | } 62 | 63 | local i, t, m 64 | 65 | for t, m in pairs(matches) do 66 | for i in pairs(m) do 67 | if ifname:match(m[i]) then 68 | return t 69 | end 70 | end 71 | end 72 | 73 | -- default type is 'copper' 74 | return "copper" 75 | end 76 | 77 | local function table_copy(t) 78 | if t == nil then return nil end 79 | local u = { } 80 | for k, v in pairs(t) do u[k] = v end 81 | return setmetatable(u, getmetatable(t)) 82 | end 83 | 84 | local methods = { 85 | getPortsInfo = { 86 | call = function(args) 87 | local util = require("luci.util") 88 | local uci = require("luci.model.uci").cursor() 89 | local ntm = require("luci.model.network").init() 90 | local fwm = require("luci.model.firewall").init() 91 | 92 | local bit = require("bit") 93 | 94 | local ports = { 95 | data = {}, 96 | count = 0 97 | } 98 | 99 | local netlist = {} 100 | local brlist = {} 101 | 102 | for _, net in ipairs(ntm:get_networks()) do 103 | local iface = net:get_interface() 104 | local wifiname = nil 105 | 106 | if iface ~= nil then 107 | if iface:type() == "wifi" then 108 | local wifinet = iface:get_wifinet() 109 | wifiname = wifinet:id() 110 | end 111 | 112 | local idx 113 | local dev 114 | local name = iface:name() 115 | local fwzone = fwm:get_zone_by_network(net:name()) 116 | local l = netlist 117 | 118 | if not net:is_alias() and iface:is_bridge() then 119 | l = brlist 120 | end 121 | 122 | l[name] = { } 123 | l[name]["netname"] = net:name() 124 | l[name]["wifiname"] = wifiname 125 | if fwzone then 126 | l[name]["fwzone"] = fwzone:name() 127 | l[name]["fwzone_sid"] = fwzone.sid 128 | end 129 | end 130 | end 131 | 132 | uci:foreach("luci_netports", "port", 133 | function(section) 134 | if section["disable"] and (section["disable"] == "true" 135 | or tonumber(section["disable"]) == 1) then 136 | -- Disabled in config 137 | return true 138 | end 139 | 140 | if not fs.access("/sys/class/net/%s/ifindex" % section["ifname"]) then 141 | -- Invalid or not existent interface name 142 | return true 143 | end 144 | 145 | local new_port = { } 146 | local ifname = section["ifname"] 147 | local type = section["type"] 148 | 149 | local knowntypes = { 150 | "auto", 151 | "copper", 152 | "fixed", 153 | "usb", -- deprecated 154 | "usb_stick", 155 | "usb_rndis", 156 | "usb_2g", 157 | "usb_3g", 158 | "usb_4g", 159 | "usb_wifi", 160 | "wifi", 161 | "vpn", 162 | "tunnel", 163 | "ppp", 164 | "gprs", 165 | "sfp" 166 | } 167 | 168 | if not util.contains(knowntypes, type) then 169 | type = "auto" 170 | end 171 | 172 | if type == "auto" then 173 | type = type_autodetect(ifname) 174 | elseif type == "usb" then 175 | type = "usb_rndis" 176 | elseif type == "usb_2g" or 177 | type == "usb_3g" or 178 | type == "usb_4g" then 179 | type = "usb_stick" 180 | elseif type == "usb_wifi" then 181 | type = "wifi" 182 | elseif type == "vpn" then 183 | type = "tunnel" 184 | end 185 | 186 | -- Port config parameters 187 | new_port["ifname"] = ifname 188 | new_port["type"] = type 189 | new_port["name"] = section["name"] 190 | 191 | if not new_port["name"] or new_port["name"] == "" then 192 | new_port["name"] = ifname 193 | end 194 | 195 | -- General port interface parameters 196 | new_port["hwaddr"] = sysfs_net_read(ifname, "address") 197 | new_port["carrier"] = tonumber(sysfs_net_read(ifname, "carrier")) or 0 198 | 199 | -- unknown, notpresent, down, lowerlayerdown, testing, dormant, up 200 | new_port["operstate"] = sysfs_net_read(ifname, "operstate") 201 | 202 | -- up or down 203 | local flags = sysfs_net_read(ifname, "flags") 204 | if bit.band(tonumber(flags), 1) == 1 then 205 | new_port["adminstate"] = "up" 206 | else 207 | new_port["adminstate"] = "down" 208 | end 209 | 210 | if new_port["carrier"] > 0 then 211 | -- full, half 212 | new_port["duplex"] = sysfs_net_read(ifname, "duplex") 213 | 214 | -- Value is an integer representing the link speed in Mbits/sec 215 | new_port["speed"] = tonumber(sysfs_net_read(ifname, "speed")) or 0 216 | if new_port["speed"] < 0 then 217 | new_port["speed"] = 0 218 | end 219 | end 220 | 221 | -- Port interface statistics 222 | new_port["stats"] = sysfs_net_read_stats(ifname) 223 | 224 | -- Bridge parameters 225 | new_port["bridge"] = sysfs_net_read_bridge(ifname) 226 | 227 | -- Parameters for/from netifd 228 | new_port["ntm"] = table_copy(netlist[ifname]) 229 | 230 | if new_port["bridge"] then 231 | new_port["ntm_bridge"] = table_copy(brlist[new_port["bridge"].ifname]) 232 | end 233 | 234 | new_port["id"] = section[".name"] 235 | ports.data[#ports.data + 1] = new_port 236 | ports.count = ports.count + 1 237 | end 238 | ) 239 | 240 | return ports 241 | end 242 | } 243 | } 244 | 245 | local function parseInput() 246 | local parse = json.new() 247 | local done, err 248 | 249 | while true do 250 | local chunk = io.read(4096) 251 | if not chunk then 252 | break 253 | elseif not done and not err then 254 | done, err = parse:parse(chunk) 255 | end 256 | end 257 | 258 | if not done then 259 | print(json.stringify({ error = err or "Incomplete input" })) 260 | os.exit(1) 261 | end 262 | 263 | return parse:get() 264 | end 265 | 266 | local function validateArgs(func, uargs) 267 | local method = methods[func] 268 | if not method then 269 | print(json.stringify({ error = "Method not found" })) 270 | os.exit(1) 271 | end 272 | 273 | if type(uargs) ~= "table" then 274 | print(json.stringify({ error = "Invalid arguments" })) 275 | os.exit(1) 276 | end 277 | 278 | uargs.ubus_rpc_session = nil 279 | 280 | local k, v 281 | local margs = method.args or {} 282 | for k, v in pairs(uargs) do 283 | if margs[k] == nil or 284 | (v ~= nil and type(v) ~= type(margs[k])) 285 | then 286 | print(json.stringify({ error = "Invalid arguments" })) 287 | os.exit(1) 288 | end 289 | end 290 | 291 | return method 292 | end 293 | 294 | if arg[1] == "list" then 295 | local _, method, rv = nil, nil, {} 296 | for _, method in pairs(methods) do rv[_] = method.args or {} end 297 | print((json.stringify(rv):gsub(":%[%]", ":{}"))) 298 | elseif arg[1] == "call" then 299 | local args = parseInput() 300 | local method = validateArgs(arg[2], args) 301 | local result, code = method.call(args) 302 | print((json.stringify(result):gsub("^%[%]$", "{}"))) 303 | os.exit(code or 0) 304 | end 305 | -------------------------------------------------------------------------------- /root/usr/share/rpcd/acl.d/netports.json: -------------------------------------------------------------------------------- 1 | { 2 | "luci-app-tn-netports": { 3 | "description": "Grant access to network ports status information", 4 | "read": { 5 | "ubus": { 6 | "netports": [ "getPortsInfo" ] 7 | }, 8 | "uci": [ "luci_netports" ] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /screenshots/h-mode-5-ports-extra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tano-systems/luci-app-tn-netports/b6b737745c560c50283e7ee9d2de937ba3306428/screenshots/h-mode-5-ports-extra.png -------------------------------------------------------------------------------- /screenshots/h-mode-5-ports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tano-systems/luci-app-tn-netports/b6b737745c560c50283e7ee9d2de937ba3306428/screenshots/h-mode-5-ports.png -------------------------------------------------------------------------------- /screenshots/v-mode-5-ports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tano-systems/luci-app-tn-netports/b6b737745c560c50283e7ee9d2de937ba3306428/screenshots/v-mode-5-ports.png -------------------------------------------------------------------------------- /test-deploy.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set LUCI_LUASRC_PATH=/usr/lib/lua/5.1/luci 4 | set LUCI_HTDOCS_PATH=/www 5 | set LUCI_ROOT_PATH=/ 6 | 7 | set HOST=%1 8 | set PASSWORD=%2 9 | set EXTRA_OPTIONS= 10 | 11 | if NOT [%PASSWORD%] == [] ( 12 | set EXTRA_OPTIONS=-pw "%PASSWORD%" 13 | ) 14 | 15 | if [%HOST%] == [] goto host_empty 16 | 17 | IF EXIST %~dp0/luasrc ( 18 | pscp -r %EXTRA_OPTIONS% %~dp0/luasrc/* %HOST%:%LUCI_LUASRC_PATH% 19 | ) 20 | 21 | IF EXIST %~dp0/htdocs ( 22 | pscp -r %EXTRA_OPTIONS% %~dp0/htdocs/* %HOST%:%LUCI_HTDOCS_PATH% 23 | ) 24 | 25 | IF EXIST %~dp0/root ( 26 | pscp -r %EXTRA_OPTIONS% %~dp0/root/* %HOST%:%LUCI_ROOT_PATH% 27 | ) 28 | 29 | rem Clear LuCI index cache 30 | plink %EXTRA_OPTIONS% %HOST% "/etc/init.d/uhttpd stop; rm -rf /tmp/luci-*; /etc/init.d/uhttpd start" 31 | 32 | echo Success 33 | goto done 34 | 35 | :host_empty 36 | echo Usage: %0 [user@]host [password] 37 | 38 | :done 39 | --------------------------------------------------------------------------------