luci-app-dawn: Implement in JavaScript
authorDaniel Vijge <danielvijge@gmail.com>
Thu, 26 Oct 2023 20:58:03 +0000 (22:58 +0200)
committerNick Hainke <vincent@systemli.org>
Mon, 30 Oct 2023 23:28:03 +0000 (00:28 +0100)
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 <danielvijge@gmail.com>
applications/luci-app-dawn/Makefile
applications/luci-app-dawn/htdocs/luci-static/resources/dawn/dawn-common.js [new file with mode: 0644]
applications/luci-app-dawn/htdocs/luci-static/resources/view/dawn/hearing_map.js [new file with mode: 0644]
applications/luci-app-dawn/htdocs/luci-static/resources/view/dawn/network_overview.js [new file with mode: 0644]
applications/luci-app-dawn/luasrc/controller/dawn.lua [deleted file]
applications/luci-app-dawn/luasrc/model/cbi/dawn/dawn_hearing_map.lua [deleted file]
applications/luci-app-dawn/luasrc/model/cbi/dawn/dawn_network.lua [deleted file]
applications/luci-app-dawn/luasrc/tools/ieee80211.lua [deleted file]
applications/luci-app-dawn/root/usr/share/luci/menu.d/luci-app-dawn.json [new file with mode: 0644]
applications/luci-app-dawn/root/usr/share/rpcd/acl.d/luci-app-dawn.json

index a216f28187aa7871f934f5792c7b929ad4c2a162..10877fad15fc125e5216d7ac1d8f03490f1bc3d7 100644 (file)
@@ -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 (file)
index 0000000..5d002d9
--- /dev/null
@@ -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 (file)
index 0000000..ea2aa67
--- /dev/null
@@ -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 (file)
index 0000000..7113399
--- /dev/null
@@ -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 (file)
index 1ae903f..0000000
+++ /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 (file)
index 3ba5329..0000000
+++ /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
-               %>
-                       <div class="cbi-section-node">
-                               <h3>SSID: <%= xml.pcdata(name) %></h3>
-                               <table class="table" id="dawn_hearing_map">
-                                       <tr class="tr table-titles">
-                                               <th class="th">Client MAC</th>
-                                               <th class="th">AP MAC</th>
-                                               <th class="th">Frequency</th>
-                                               <th class="th">HT Sup</th>
-                                               <th class="th">VHT Sup</th>
-                                               <th class="th">Signal</th>
-                                               <th class="th">RCPI</th>
-                                               <th class="th">RSNI</th>
-                                               <th class="th">Channel Utilization</th>
-                                               <th class="th">Station connect to AP</th>
-                                               <th class="th">Score</th>
-                                       </tr>
-                                       <%
-                                               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
-                                       %>
-                                               <tr class="tr">
-                                                       <td class="td"><%= (count_loop == 0) and mac or "" %></td>
-                                                       <td class="td"><%= mac2 %></td>
-                                                       <td class="td"><%= "%.3f" %( data2.freq / 1000 ) %> GHz Channel: <%= "%d" %( status.frequency_to_channel(data2.freq) ) %></td>
-                                                       <td class="td"><%= (data2.ht_capabilities == true and data2.ht_support == true) and "True" or "False" %></td>
-                                                       <td class="td"><%= (data2.vht_capabilities == true and data2.vht_support == true) and "True" or "False" %></td>
-                                                       <td class="td"><%= "%d" % data2.signal %></td>
-                                                       <td class="td"><%= "%d" % data2.rcpi %></td>
-                                                       <td class="td"><%= "%d" % data2.rsni %></td>
-                                                       <td class="td"><%= "%.2f" % (data2.channel_utilization / 2.55) %> %</td>
-                                                       <td class="td"><%= "%d" % data2.num_sta %></td>
-                                                       <td class="td"><%= "%d" % data2.score %></td>
-                                               </tr>
-                                       <%
-                                                                       count_loop = count_loop + 1
-                                                               end
-                                                       end
-                                               end
-                                       %>
-                               </table>
-                       </div>
-               <%
-                       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 (file)
index f9e04b9..0000000
+++ /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
-               %>
-                       <div class="cbi-section-node">
-                               <h3>SSID: <%= xml.pcdata(name) %></h3>
-                               <table class="table" id="network_overview_main">
-                                       <tr class="tr table-titles">
-                                               <th class="th">AP</th>
-                                               <th class="th">Clients</th>
-                                       </tr>
-                                       <%
-                                               local mac, data
-                                               for mac, data in pairs(macs) do
-                                       %>
-                                               <tr class="tr">
-                                                       <td class="td" style="vertical-align: top;">
-                                                               <table class="table" id="ap-<%= mac %>">
-                                                                       <tr class="tr table-titles">
-                                                                               <th class="th">Hostname</th>
-                                                                               <th class="th">Interface</th>
-                                                                               <th class="th">MAC</th>
-                                                                               <th class="th">Utilization</th>
-                                                                               <th class="th">Frequency</th>
-                                                                               <th class="th">Stations</th>
-                                                                               <th class="th">HT Sup</th>
-                                                                               <th class="th">VHT Sup</th>
-                                                                       </tr>
-                                                                       <tr class="tr">
-                                                                               <td class="td"><%= xml.pcdata(data.hostname) %></td>
-                                                                               <td class="td"><%= xml.pcdata(data.iface) %></td>
-                                                                               <td class="td"><%= mac %></td>
-                                                                               <td class="td"><%= "%.2f" %(data.channel_utilization / 2.55) %> %</td>
-                                                                               <td class="td"><%= "%.3f" %( data.freq / 1000 ) %> GHz (Channel: <%= "%d" %( status.frequency_to_channel(data.freq) ) %>)</td>
-                                                                               <td class="td"><%= "%d" % data.num_sta %></td>
-                                                                               <td class="td"><%= (data.ht_support == true) and "available" or "not available" %></td>
-                                                                               <td class="td"><%= (data.vht_support == true) and "available" or "not available" %></td>
-                                                                       </tr>
-                                                               </table>
-                                                       </td>
-                                                       <td class="td" style="vertical-align: top;">
-                                                               <table class="table" id="clients-<%= mac %>">
-                                                                       <tr class="tr table-titles">
-                                                                               <th class="th">MAC</th>
-                                                                               <th class="th">HT</th>
-                                                                               <th class="th">VHT</th>
-                                                                               <th class="th">Signal</th>
-                                                                       </tr>
-                                                                       <%
-                                                                               local mac2, data2
-                                                                               for clientmac, clientvals in pairs(data) do
-                                                                                       if (type(clientvals) == "table") then
-                                                                       %>
-                                                                               <tr class="tr">
-                                                                                       <td class="td"><%= clientmac %></td>
-                                                                                       <td class="td"><%= (clientvals.ht == true) and "available" or "not available" %></td>
-                                                                                       <td class="td"><%= (clientvals.vht == true) and "available" or "not available" %></td>
-                                                                                       <td class="td"><%= "%d" % clientvals.signal %></td>
-                                                                               </tr>
-                                                                       <%
-                                                                                       end
-                                                                               end
-                                                                       %>
-                                                               </table>
-                                                       </td>
-                                               </tr>
-                                       <%
-                                               end
-                                       %>
-                               </table>
-                       </div>
-               <%
-                       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 (file)
index 44b0464..0000000
+++ /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 (file)
index 0000000..96e88db
--- /dev/null
@@ -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"
+               }
+       }
+}
index 4fece64a869e9ecaa67d2f2d48d8863570412f83..5968c406a011c748ffa3bc35519b449da619d474 100644 (file)
@@ -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" ]