luci-mod-status: persist sorting of Wi-Fi associated stations table
[project/luci.git] / modules / luci-mod-status / htdocs / luci-static / resources / view / status / include / 60_wifi.js
1 'use strict';
2 'require baseclass';
3 'require dom';
4 'require network';
5 'require uci';
6 'require fs';
7 'require rpc';
8
9 return baseclass.extend({
10 title: _('Wireless'),
11
12 WPSTranslateTbl: {
13 Disabled: _('Disabled'),
14 Active: _('Active'),
15 'Timed-out': _('Timed-out'),
16 Overlap: _('Overlap'),
17 Unknown: _('Unknown')
18 },
19
20 callSessionAccess: rpc.declare({
21 object: 'session',
22 method: 'access',
23 params: [ 'scope', 'object', 'function' ],
24 expect: { 'access': false }
25 }),
26
27 wifirate: function(rt) {
28 var s = '%.1f\xa0%s, %d\xa0%s'.format(rt.rate / 1000, _('Mbit/s'), rt.mhz, _('MHz')),
29 ht = rt.ht, vht = rt.vht,
30 mhz = rt.mhz, nss = rt.nss,
31 mcs = rt.mcs, sgi = rt.short_gi,
32 he = rt.he, he_gi = rt.he_gi,
33 he_dcm = rt.he_dcm;
34
35 if (ht || vht) {
36 if (vht) s += ', VHT-MCS\xa0%d'.format(mcs);
37 if (nss) s += ', VHT-NSS\xa0%d'.format(nss);
38 if (ht) s += ', MCS\xa0%s'.format(mcs);
39 if (sgi) s += ', ' + _('Short GI').replace(/ /g, '\xa0');
40 }
41
42 if (he) {
43 s += ', HE-MCS\xa0%d'.format(mcs);
44 if (nss) s += ', HE-NSS\xa0%d'.format(nss);
45 if (he_gi) s += ', HE-GI\xa0%d'.format(he_gi);
46 if (he_dcm) s += ', HE-DCM\xa0%d'.format(he_dcm);
47 }
48
49 return s;
50 },
51
52 handleDelClient: function(wifinet, mac, ev, cmd) {
53 var exec = cmd || 'disconnect';
54
55 dom.parent(ev.currentTarget, '.tr').style.opacity = 0.5;
56 ev.currentTarget.classList.add('spinning');
57 ev.currentTarget.disabled = true;
58 ev.currentTarget.blur();
59
60 /* Disconnect client before adding to maclist */
61 wifinet.disconnectClient(mac, true, 5, 60000);
62
63 if (exec == 'addlist') {
64 wifinet.maclist.push(mac);
65
66 uci.set('wireless', wifinet.sid, 'maclist', wifinet.maclist);
67
68 return uci.save()
69 .then(L.bind(L.ui.changes.init, L.ui.changes))
70 .then(L.bind(L.ui.changes.displayChanges, L.ui.changes));
71 }
72 },
73
74 handleGetWPSStatus: function(wifinet) {
75 return rpc.declare({
76 object: 'hostapd.%s'.format(wifinet),
77 method: 'wps_status',
78 })()
79 },
80
81 handleCallWPS: function(wifinet, ev) {
82 ev.currentTarget.classList.add('spinning');
83 ev.currentTarget.disabled = true;
84 ev.currentTarget.blur();
85
86 return rpc.declare({
87 object: 'hostapd.%s'.format(wifinet),
88 method: 'wps_start',
89 })();
90 },
91
92 handleCancelWPS: function(wifinet, ev) {
93 ev.currentTarget.classList.add('spinning');
94 ev.currentTarget.disabled = true;
95 ev.currentTarget.blur();
96
97 return rpc.declare({
98 object: 'hostapd.%s'.format(wifinet),
99 method: 'wps_cancel',
100 })();
101 },
102
103 renderbox: function(radio, networks) {
104 var chan = null,
105 freq = null,
106 rate = null,
107 badges = [];
108
109 for (var i = 0; i < networks.length; i++) {
110 var net = networks[i],
111 is_assoc = (net.getBSSID() != '00:00:00:00:00:00' && net.getChannel() && !net.isDisabled()),
112 quality = net.getSignalPercent();
113
114 var icon;
115 if (net.isDisabled())
116 icon = L.resource('icons/signal-none.png');
117 else if (quality <= 0)
118 icon = L.resource('icons/signal-0.png');
119 else if (quality < 25)
120 icon = L.resource('icons/signal-0-25.png');
121 else if (quality < 50)
122 icon = L.resource('icons/signal-25-50.png');
123 else if (quality < 75)
124 icon = L.resource('icons/signal-50-75.png');
125 else
126 icon = L.resource('icons/signal-75-100.png');
127
128 var WPS_button = null;
129
130 if (net.isWPSEnabled) {
131 if (net.wps_status == 'Active') {
132 WPS_button = E('button', {
133 'class' : 'cbi-button cbi-button-remove',
134 'click': L.bind(this.handleCancelWPS, this, net.getIfname()),
135 }, [ _('Stop WPS') ])
136 } else {
137 WPS_button = E('button', {
138 'class' : 'cbi-button cbi-button-apply',
139 'click': L.bind(this.handleCallWPS, this, net.getIfname()),
140 }, [ _('Start WPS') ])
141 }
142 }
143
144 var badge = renderBadge(
145 icon,
146 '%s: %d dBm / %s: %d%%'.format(_('Signal'), net.getSignal(), _('Quality'), quality),
147 _('SSID'), net.getActiveSSID() || '?',
148 _('Mode'), net.getActiveMode(),
149 _('BSSID'), is_assoc ? (net.getActiveBSSID() || '-') : null,
150 _('Encryption'), is_assoc ? net.getActiveEncryption() : null,
151 _('Associations'), is_assoc ? (net.assoclist.length || '-') : null,
152 null, is_assoc ? null : E('em', net.isDisabled() ? _('Wireless is disabled') : _('Wireless is not associated')),
153 _('WPS status'), this.WPSTranslateTbl[net.wps_status],
154 '', WPS_button
155 );
156
157 badges.push(badge);
158
159 chan = (chan != null) ? chan : net.getChannel();
160 freq = (freq != null) ? freq : net.getFrequency();
161 rate = (rate != null) ? rate : net.getBitRate();
162 }
163
164 return E('div', { class: 'ifacebox' }, [
165 E('div', { class: 'ifacebox-head center ' + (radio.isUp() ? 'active' : '') },
166 E('strong', radio.getName())),
167 E('div', { class: 'ifacebox-body left' }, [
168 L.itemlist(E('span'), [
169 _('Type'), radio.getI18n().replace(/^Generic | Wireless Controller .+$/g, ''),
170 _('Channel'), chan ? '%d (%.3f %s)'.format(chan, freq, _('GHz')) : '-',
171 _('Bitrate'), rate ? '%d %s'.format(rate, _('Mbit/s')) : '-',
172 ]),
173 E('div', {}, badges)
174 ])
175 ]);
176 },
177
178 isWPSEnabled: {},
179
180 load: function() {
181 return Promise.all([
182 network.getWifiDevices(),
183 network.getWifiNetworks(),
184 network.getHostHints(),
185 this.callSessionAccess('access-group', 'luci-mod-status-index-wifi', 'read'),
186 this.callSessionAccess('access-group', 'luci-mod-status-index-wifi', 'write'),
187 uci.load('wireless')
188 ]).then(L.bind(function(data) {
189 var tasks = [],
190 radios_networks_hints = data[1],
191 hasWPS = L.hasSystemFeature('hostapd', 'wps');
192
193 for (var i = 0; i < radios_networks_hints.length; i++) {
194 tasks.push(L.resolveDefault(radios_networks_hints[i].getAssocList(), []).then(L.bind(function(net, list) {
195 net.assoclist = list.sort(function(a, b) { return a.mac > b.mac });
196 }, this, radios_networks_hints[i])));
197
198 if (hasWPS && uci.get('wireless', radios_networks_hints[i].sid, 'wps_pushbutton') == '1') {
199 radios_networks_hints[i].isWPSEnabled = true;
200 tasks.push(L.resolveDefault(this.handleGetWPSStatus(radios_networks_hints[i].getIfname()), null)
201 .then(L.bind(function(net, data) {
202 net.wps_status = data ? data.pbc_status : _('No Data');
203 }, this, radios_networks_hints[i])));
204 }
205 }
206
207 return Promise.all(tasks).then(function() {
208 return data;
209 });
210 }, this));
211 },
212
213 render: function(data) {
214 var seen = {},
215 radios = data[0],
216 networks = data[1],
217 hosthints = data[2],
218 hasReadPermission = data[3],
219 hasWritePermission = data[4];
220
221 var table = E('div', { 'class': 'network-status-table' });
222
223 for (var i = 0; i < radios.sort(function(a, b) { a.getName() > b.getName() }).length; i++)
224 table.appendChild(this.renderbox(radios[i],
225 networks.filter(function(net) { return net.getWifiDeviceName() == radios[i].getName() })));
226
227 if (!table.lastElementChild)
228 return null;
229
230 var assoclist = E('table', { 'class': 'table assoclist', 'id': 'wifi_assoclist_table' }, [
231 E('tr', { 'class': 'tr table-titles' }, [
232 E('th', { 'class': 'th nowrap' }, _('Network')),
233 E('th', { 'class': 'th hide-xs' }, _('MAC address')),
234 E('th', { 'class': 'th' }, _('Host')),
235 E('th', { 'class': 'th' }, '%s / %s'.format(_('Signal'), _('Noise'))),
236 E('th', { 'class': 'th' }, '%s / %s'.format(_('RX Rate'), _('TX Rate')))
237 ])
238 ]);
239
240 var rows = [];
241
242 for (var i = 0; i < networks.length; i++) {
243 var macfilter = uci.get('wireless', networks[i].sid, 'macfilter'),
244 maclist = {};
245
246 if (macfilter != null && macfilter != 'disable') {
247 networks[i].maclist = L.toArray(uci.get('wireless', networks[i].sid, 'maclist'));
248 for (var j = 0; j < networks[i].maclist.length; j++) {
249 var mac = networks[i].maclist[j].toUpperCase();
250 maclist[mac] = true;
251 }
252 }
253
254 for (var k = 0; k < networks[i].assoclist.length; k++) {
255 var bss = networks[i].assoclist[k],
256 name = hosthints.getHostnameByMACAddr(bss.mac),
257 ipv4 = hosthints.getIPAddrByMACAddr(bss.mac),
258 ipv6 = hosthints.getIP6AddrByMACAddr(bss.mac);
259
260 var icon;
261 var q = Math.min((bss.signal + 110) / 70 * 100, 100);
262 if (q == 0)
263 icon = L.resource('icons/signal-0.png');
264 else if (q < 25)
265 icon = L.resource('icons/signal-0-25.png');
266 else if (q < 50)
267 icon = L.resource('icons/signal-25-50.png');
268 else if (q < 75)
269 icon = L.resource('icons/signal-50-75.png');
270 else
271 icon = L.resource('icons/signal-75-100.png');
272
273 var sig_title, sig_value;
274
275 if (bss.noise) {
276 sig_value = '%d/%d\xa0%s'.format(bss.signal, bss.noise, _('dBm'));
277 sig_title = '%s: %d %s / %s: %d %s / %s %d'.format(
278 _('Signal'), bss.signal, _('dBm'),
279 _('Noise'), bss.noise, _('dBm'),
280 _('SNR'), bss.signal - bss.noise);
281 }
282 else {
283 sig_value = '%d\xa0%s'.format(bss.signal, _('dBm'));
284 sig_title = '%s: %d %s'.format(_('Signal'), bss.signal, _('dBm'));
285 }
286
287 var hint;
288
289 if (name && ipv4 && ipv6)
290 hint = '%s <span class="hide-xs">(%s, %s)</span>'.format(name, ipv4, ipv6);
291 else if (name && (ipv4 || ipv6))
292 hint = '%s <span class="hide-xs">(%s)</span>'.format(name, ipv4 || ipv6);
293 else
294 hint = name || ipv4 || ipv6 || '?';
295
296 var row = [
297 E('span', {
298 'class': 'ifacebadge',
299 'title': networks[i].getI18n(),
300 'data-ifname': networks[i].getIfname(),
301 'data-ssid': networks[i].getActiveSSID()
302 }, [
303 E('img', { 'src': L.resource('icons/wifi.png') }),
304 E('span', {}, [
305 ' ', networks[i].getShortName(),
306 E('small', {}, [ ' (', networks[i].getIfname(), ')' ])
307 ])
308 ]),
309 bss.mac,
310 hint,
311 E('span', {
312 'class': 'ifacebadge',
313 'title': sig_title,
314 'data-signal': bss.signal,
315 'data-noise': bss.noise
316 }, [
317 E('img', { 'src': icon }),
318 E('span', {}, [
319 ' ', sig_value
320 ])
321 ]),
322 E('span', {}, [
323 E('span', this.wifirate(bss.rx)),
324 E('br'),
325 E('span', this.wifirate(bss.tx))
326 ])
327 ];
328
329 if (networks[i].isClientDisconnectSupported() && hasWritePermission) {
330 if (assoclist.firstElementChild.childNodes.length < 6)
331 assoclist.firstElementChild.appendChild(E('th', { 'class': 'th cbi-section-actions' }));
332
333 if (macfilter != null && macfilter != 'disable' && !maclist[bss.mac]) {
334 row.push(new L.ui.ComboButton('button', {
335 'addlist': macfilter == 'allow' ? _('Add to Whitelist') : _('Add to Blacklist'),
336 'disconnect': _('Disconnect')
337 }, {
338 'click': L.bind(this.handleDelClient, this, networks[i], bss.mac),
339 'sort': [ 'disconnect', 'addlist' ],
340 'classes': {
341 'addlist': 'btn cbi-button cbi-button-remove',
342 'disconnect': 'btn cbi-button cbi-button-remove'
343 }
344 }).render()
345 )
346 }
347 else {
348 row.push(E('button', {
349 'class': 'cbi-button cbi-button-remove',
350 'click': L.bind(this.handleDelClient, this, networks[i], bss.mac)
351 }, [ _('Disconnect') ]));
352 }
353 }
354 else {
355 row.push('-');
356 }
357
358 rows.push(row);
359 }
360 }
361
362 cbi_update_table(assoclist, rows, E('em', _('No information available')));
363
364 return E([
365 table,
366 hasReadPermission ? E('h3', _('Associated Stations')) : E([]),
367 hasReadPermission ? assoclist : E([])
368 ]);
369 }
370 });