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