From: Daniel Vijge Date: Thu, 26 Oct 2023 20:58:03 +0000 (+0200) Subject: luci-app-dawn: Implement in JavaScript X-Git-Url: http://git.lede-project.org./?a=commitdiff_plain;h=ea8c0aa2a1a1ff9f29ffd452049a7b749298ed17;p=project%2Fluci.git luci-app-dawn: Implement in JavaScript This commit re-implements luci-app-dawn in JavaScript, removing the older lua implementation. Besides a 1-to-1 port, there are some changes/improvements: * In both the network overview and the hearing map, replace MAC addresses by host name if known. * In the hearing map, the table is sortable. If the same client is connected to multiple access points/frequencies the MAC/host name is listed twice, whereas in the lua implementation the second MAC address was empty to show it was referring to the same client. This means the table can be sorted on any column, and the information remains correct. * The view in the network overview is a bit different. This table is not sortable, because LuCi doesn't seem to like a table inside a table for sorting. * Align the column names between the network overview and the hearing table. * Add tooltips for abbreviations in column names. Signed-off-by: Daniel Vijge --- diff --git a/applications/luci-app-dawn/Makefile b/applications/luci-app-dawn/Makefile index a216f28187..10877fad15 100644 --- a/applications/luci-app-dawn/Makefile +++ b/applications/luci-app-dawn/Makefile @@ -1,7 +1,7 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=LuCI support for DAWN -LUCI_DEPENDS:=+luci-base +dawn +luci-compat +luci-lib-json +LUCI_DEPENDS:=+luci-base +dawn include ../../luci.mk diff --git a/applications/luci-app-dawn/htdocs/luci-static/resources/dawn/dawn-common.js b/applications/luci-app-dawn/htdocs/luci-static/resources/dawn/dawn-common.js new file mode 100644 index 0000000000..5d002d9b7f --- /dev/null +++ b/applications/luci-app-dawn/htdocs/luci-static/resources/dawn/dawn-common.js @@ -0,0 +1,69 @@ +'use strict'; +'require baseclass'; +'require rpc'; + +let callDawnGetNetwork, callDawnGetHearingMap, callHostHints; + +callDawnGetNetwork = rpc.declare({ + object: 'dawn', + method: 'get_network', + expect: { } +}); + +callDawnGetHearingMap = rpc.declare({ + object: 'dawn', + method: 'get_hearing_map', + expect: { } +}); + +callHostHints = rpc.declare({ + object: 'luci-rpc', + method: 'getHostHints', + expect: { } +}); + +function getAvailableText(available) { + return ( available ? _('Available') : _('Not available') ); +} + +function getChannelFromFrequency(freq) { + if (freq <= 2400) { + return 0; + } + else if (freq == 2484) { + return 14; + } + else if (freq < 2484) { + return (freq - 2407) / 5; + } + else if (freq >= 4910 && freq <= 4980) { + return (freq - 4000) / 5; + } + else if (freq <= 45000) { + return (freq - 5000) / 5; + } + else if (freq >= 58320 && freq <= 64800) { + return (freq - 56160) / 2160; + } + else { + return 0; + } +} + +function getFormattedNumber(num, decimals, divider = 1) { + return (num/divider).toFixed(decimals); +} + +function getHostnameFromMAC(hosthints, mac) { + return ( hosthints[mac] && hosthints[mac].name ? hosthints[mac].name : mac); +} + +return L.Class.extend({ + callDawnGetNetwork: callDawnGetNetwork, + callDawnGetHearingMap: callDawnGetHearingMap, + callHostHints: callHostHints, + getAvailableText: getAvailableText, + getChannelFromFrequency: getChannelFromFrequency, + getFormattedNumber: getFormattedNumber, + getHostnameFromMAC: getHostnameFromMAC +}); diff --git a/applications/luci-app-dawn/htdocs/luci-static/resources/view/dawn/hearing_map.js b/applications/luci-app-dawn/htdocs/luci-static/resources/view/dawn/hearing_map.js new file mode 100644 index 0000000000..ea2aa67998 --- /dev/null +++ b/applications/luci-app-dawn/htdocs/luci-static/resources/view/dawn/hearing_map.js @@ -0,0 +1,78 @@ +'use strict'; +'require uci'; +'require view'; +'require dawn.dawn-common as dawn'; + +return view.extend({ + handleSaveApply: null, + handleSave: null, + handleReset: null, + + load: function() { + return Promise.all([ + dawn.callDawnGetHearingMap(), + dawn.callHostHints() + ]); + }, + + render: function(data) { + + const dawnHearingMapData = data[0]; + const hostHintsData = data[1]; + + const body = E([ + E('h2', _('Hearing Map')) + ]); + + for (let network in dawnHearingMapData) { + + body.appendChild( + E('h3', 'SSID: ' + network) + ); + + let hearing_map_table = E('table', { 'class': 'table cbi-section-table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, _('Client')), + E('th', { 'class': 'th' }, _('Access Point')), + E('th', { 'class': 'th' }, _('Frequency')), + E('th', { 'class': 'th' }, E('span', { 'data-tooltip': _('High Throughput') }, [ _('HT') ])), + E('th', { 'class': 'th' }, E('span', { 'data-tooltip': _('Very High Throughput') }, [ _('VHT') ])), + E('th', { 'class': 'th' }, _('Signal')), + E('th', { 'class': 'th' }, E('span', { 'data-tooltip': _('Received Channel Power Indication') }, [ _('RCPI') ])), + E('th', { 'class': 'th' }, E('span', { 'data-tooltip': _('Received Signal to Noise Indicator') }, [ _('RSNI') ])), + E('th', { 'class': 'th' }, _('Channel Utilization')), + E('th', { 'class': 'th' }, _('Stations Connected')), + E('th', { 'class': 'th' }, _('Score')) + ]) + ]); + + let clients = Object.entries(dawnHearingMapData[network]).map(function(client) { + + return Object.entries(client[1]).map(function(ap) { + + if (ap[1].freq != 0) { + return [ + dawn.getHostnameFromMAC(hostHintsData, client[0]), + dawn.getHostnameFromMAC(hostHintsData, ap[0]), + dawn.getFormattedNumber(ap[1].freq, 3, 1000) + ' GHz (' + _('Channel') + ': ' + dawn.getChannelFromFrequency(ap[1].freq) + ')', + dawn.getAvailableText(ap[1].ht_capabilities && ap[1].ht_support), + dawn.getAvailableText(ap[1].vht_capabilities && ap[1].vht_support), + ap[1].signal, + ap[1].rcpi, + ap[1].rsni, + dawn.getFormattedNumber(ap[1].channel_utilization, 2, 2.55) + '%', + ap[1].num_sta, + ap[1].score + ] + } + }).flat() + + }); + + cbi_update_table(hearing_map_table, clients, E('em', _('No clients connected.'))); + + body.appendChild(hearing_map_table); + } + return body; + } +}); diff --git a/applications/luci-app-dawn/htdocs/luci-static/resources/view/dawn/network_overview.js b/applications/luci-app-dawn/htdocs/luci-static/resources/view/dawn/network_overview.js new file mode 100644 index 0000000000..71133991ab --- /dev/null +++ b/applications/luci-app-dawn/htdocs/luci-static/resources/view/dawn/network_overview.js @@ -0,0 +1,93 @@ +'use strict'; +'require uci'; +'require view'; +'require dawn.dawn-common as dawn'; + +return view.extend({ + handleSaveApply: null, + handleSave: null, + handleReset: null, + + load: function() { + return Promise.all([ + dawn.callDawnGetNetwork(), + dawn.callHostHints() + ]); + }, + + render: function(data) { + + const dawnNetworkData = data[0]; + const hostHintsData = data[1]; + + const body = E([ + E('h2', _('Network Overview')) + ]); + + let client_table = {}; + + for (let network in dawnNetworkData) { + + body.appendChild( + E('h3', 'SSID: ' + network) + ); + + let ap_table = E('table', { 'class': 'table cbi-section-table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th left cbi-section-actions' }, _('Access Point')), + E('th', { 'class': 'th left cbi-section-actions' }, _('Interface')), + E('th', { 'class': 'th left cbi-section-actions' }, _('MAC')), + E('th', { 'class': 'th left cbi-section-actions' }, _('Utilization')), + E('th', { 'class': 'th left cbi-section-actions' }, _('Frequency')), + E('th', { 'class': 'th left cbi-section-actions' }, _('Stations Connected')), + E('th', { 'class': 'th left cbi-section-actions' }, E('span', { 'data-tooltip': _('High Throughput') }, [ _('HT') ])), + E('th', { 'class': 'th left cbi-section-actions' }, E('span', { 'data-tooltip': _('Very High Throughput') }, [ _('VHT') ])), + E('th', { 'class': 'th center cbi-section-actions' }, _('Clients')), + ]) + ]); + + let aps = Object.entries(dawnNetworkData[network]).map(function(ap) { + client_table[ap[0]] = E('table', { 'class': 'table cbi-section-table', 'style': 'display: table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, _('Client')), + E('th', { 'class': 'th' }, E('span', { 'data-tooltip': _('High Throughput') }, [ _('HT') ])), + E('th', { 'class': 'th' }, E('span', { 'data-tooltip': _('Very High Throughput') }, [ _('VHT') ])), + E('th', { 'class': 'th' }, _('Signal')) + ]) + ]); + + let clients = []; + let clientData = Object.entries(ap[1]); + for (let i = 0; i < clientData.length; i++) { + if (typeof clientData[i][1] === 'object') { + clients.push([ + dawn.getHostnameFromMAC(hostHintsData ,clientData[i][0]), + dawn.getAvailableText(clientData[i][1].ht), + dawn.getAvailableText(clientData[i][1].vht), + clientData[i][1].signal + ]); + } + } + + cbi_update_table(client_table[ap[0]], clients, E('em', _('No clients connected.'))); + + return [ + ap[1].hostname, + ap[1].iface, + ap[0], + dawn.getFormattedNumber(ap[1].channel_utilization, 2, 2.55) + '%', + dawn.getFormattedNumber(ap[1].freq, 3, 1000) + ' GHz (' + _('Channel') + ': ' + dawn.getChannelFromFrequency(ap[1].freq) + ')', + ap[1].num_sta, + dawn.getAvailableText(ap[1].ht_support), + dawn.getAvailableText(ap[1].vht_support), + ap[1].num_sta > 0 ? client_table[ap[0]] : E('em', { 'style': 'display: inline' }, _('No clients connected.')) + ] + }); + + cbi_update_table(ap_table, aps, E('em', _('No access points available.'))); + + body.appendChild(ap_table); + } + return body; + } +}); diff --git a/applications/luci-app-dawn/luasrc/controller/dawn.lua b/applications/luci-app-dawn/luasrc/controller/dawn.lua deleted file mode 100644 index 1ae903f3a4..0000000000 --- a/applications/luci-app-dawn/luasrc/controller/dawn.lua +++ /dev/null @@ -1,10 +0,0 @@ -module("luci.controller.dawn", package.seeall) - -function index() - local e = entry({ "admin", "dawn" }, firstchild(), "DAWN", 60) - e.dependent = false - e.acl_depends = { "luci-app-dawn" } - - entry({ "admin", "dawn", "view_network" }, cbi("dawn/dawn_network"), "View Network Overview", 1) - entry({ "admin", "dawn", "view_hearing_map" }, cbi("dawn/dawn_hearing_map"), "View Hearing Map", 2) -end diff --git a/applications/luci-app-dawn/luasrc/model/cbi/dawn/dawn_hearing_map.lua b/applications/luci-app-dawn/luasrc/model/cbi/dawn/dawn_hearing_map.lua deleted file mode 100644 index 3ba5329855..0000000000 --- a/applications/luci-app-dawn/luasrc/model/cbi/dawn/dawn_hearing_map.lua +++ /dev/null @@ -1,70 +0,0 @@ -m = Map("dawn", "Hearing Map", translate("Hearing Map")) -m.pageaction = false - -s = m:section(NamedSection, "__hearingmap__") - -function s.render(self, sid) - local tpl = require "luci.template" - tpl.render_string([[ - <% - local utl = require "luci.util" - local xml = require "luci.xml" - local status = require "luci.tools.ieee80211" - local stat = utl.ubus("dawn", "get_hearing_map", { }) - local name, macs - - for name, macs in pairs(stat) do - %> -
-

SSID: <%= xml.pcdata(name) %>

- - - - - - - - - - - - - - - <% - local mac, data - for mac, data in pairs(macs) do - - local mac2, data2 - local count_loop = 0 - for mac2, data2 in pairs(data) do - if data2.freq ~= 0 then --prevent empty entry crashes - %> - - - - - - - - - - - - - - <% - count_loop = count_loop + 1 - end - end - end - %> -
Client MACAP MACFrequencyHT SupVHT SupSignalRCPIRSNIChannel UtilizationStation connect to APScore
<%= (count_loop == 0) and mac or "" %><%= mac2 %><%= "%.3f" %( data2.freq / 1000 ) %> GHz Channel: <%= "%d" %( status.frequency_to_channel(data2.freq) ) %><%= (data2.ht_capabilities == true and data2.ht_support == true) and "True" or "False" %><%= (data2.vht_capabilities == true and data2.vht_support == true) and "True" or "False" %><%= "%d" % data2.signal %><%= "%d" % data2.rcpi %><%= "%d" % data2.rsni %><%= "%.2f" % (data2.channel_utilization / 2.55) %> %<%= "%d" % data2.num_sta %><%= "%d" % data2.score %>
-
- <% - end - %> - ]]) -end - -return m diff --git a/applications/luci-app-dawn/luasrc/model/cbi/dawn/dawn_network.lua b/applications/luci-app-dawn/luasrc/model/cbi/dawn/dawn_network.lua deleted file mode 100644 index f9e04b90cb..0000000000 --- a/applications/luci-app-dawn/luasrc/model/cbi/dawn/dawn_network.lua +++ /dev/null @@ -1,94 +0,0 @@ -m = Map("dawn", "Network Overview", translate("Network Overview")) -m.pageaction = false - -s = m:section(NamedSection, "__networkoverview__") - -function s.render(self, sid) - local tpl = require "luci.template" - local json = require "luci.json" - local utl = require "luci.util" - tpl.render_string([[ - <% - local status = require "luci.tools.ieee80211" - local utl = require "luci.util" - local sys = require "luci.sys" - local xml = require "luci.xml" - local hosts = sys.net.host_hints() - local stat = utl.ubus("dawn", "get_network", { }) - local name, macs - for name, macs in pairs(stat) do - %> -
-

SSID: <%= xml.pcdata(name) %>

- - - - - - <% - local mac, data - for mac, data in pairs(macs) do - %> - - - - - <% - end - %> -
APClients
- - - - - - - - - - - - - - - - - - - - - -
HostnameInterfaceMACUtilizationFrequencyStationsHT SupVHT Sup
<%= xml.pcdata(data.hostname) %><%= xml.pcdata(data.iface) %><%= mac %><%= "%.2f" %(data.channel_utilization / 2.55) %> %<%= "%.3f" %( data.freq / 1000 ) %> GHz (Channel: <%= "%d" %( status.frequency_to_channel(data.freq) ) %>)<%= "%d" % data.num_sta %><%= (data.ht_support == true) and "available" or "not available" %><%= (data.vht_support == true) and "available" or "not available" %>
-
- - - - - - - - <% - local mac2, data2 - for clientmac, clientvals in pairs(data) do - if (type(clientvals) == "table") then - %> - - - - - - - <% - end - end - %> -
MACHTVHTSignal
<%= clientmac %><%= (clientvals.ht == true) and "available" or "not available" %><%= (clientvals.vht == true) and "available" or "not available" %><%= "%d" % clientvals.signal %>
-
-
- <% - end - %> - ]]) -end - -return m diff --git a/applications/luci-app-dawn/luasrc/tools/ieee80211.lua b/applications/luci-app-dawn/luasrc/tools/ieee80211.lua deleted file mode 100644 index 44b0464427..0000000000 --- a/applications/luci-app-dawn/luasrc/tools/ieee80211.lua +++ /dev/null @@ -1,20 +0,0 @@ -module("luci.tools.ieee80211", package.seeall) - -function frequency_to_channel(freq) - if (freq <= 2400) then - return 0; - elseif (freq == 2484) then - return 14; - elseif (freq < 2484) then - return (freq - 2407) / 5; - elseif (freq >= 4910 and freq <= 4980) then - return (freq - 4000) / 5; - elseif (freq <= 45000) then - return (freq - 5000) / 5; - elseif (freq >= 58320 and freq <= 64800) then - return (freq - 56160) / 2160; - else - return 0; - end -end - diff --git a/applications/luci-app-dawn/root/usr/share/luci/menu.d/luci-app-dawn.json b/applications/luci-app-dawn/root/usr/share/luci/menu.d/luci-app-dawn.json new file mode 100644 index 0000000000..96e88db39e --- /dev/null +++ b/applications/luci-app-dawn/root/usr/share/luci/menu.d/luci-app-dawn.json @@ -0,0 +1,30 @@ +{ + "admin/dawn/": { + "title": "DAWN", + "order": 60, + "action": { + "type": "firstchild" + }, + "depends": { + "acl": [ "luci-app-dawn" ] + } + }, + + "admin/dawn/network_overview": { + "title": "Network Overview", + "order": 1, + "action": { + "type": "view", + "path": "dawn/network_overview" + } + }, + + "admin/dawn/hearing_map": { + "title": "Hearing Map", + "order": 2, + "action": { + "type": "view", + "path": "dawn/hearing_map" + } + } +} diff --git a/applications/luci-app-dawn/root/usr/share/rpcd/acl.d/luci-app-dawn.json b/applications/luci-app-dawn/root/usr/share/rpcd/acl.d/luci-app-dawn.json index 4fece64a86..5968c406a0 100644 --- a/applications/luci-app-dawn/root/usr/share/rpcd/acl.d/luci-app-dawn.json +++ b/applications/luci-app-dawn/root/usr/share/rpcd/acl.d/luci-app-dawn.json @@ -2,7 +2,11 @@ "luci-app-dawn": { "description": "Grant UCI access for luci-app-dawn", "read": { - "uci": [ "dawn" ] + "uci": [ "dawn" ], + "ubus": { + "dawn": [ "get_network", "get_hearing_map" ], + "luci-rpc": [ "getHostHints" ] + } }, "write": { "uci": [ "dawn" ]