3 saveScrollTop: function()
5 this._scroll_top = $(document).scrollTop();
8 restoreScrollTop: function()
10 if (typeof(this._scroll_top) == 'undefined')
13 $(document).scrollTop(this._scroll_top);
15 delete this._scroll_top;
18 loading: function(enable)
23 var state = this._loading || (this._loading = {
26 .addClass('modal fade')
28 .addClass('modal-dialog')
30 .addClass('modal-content luci2-modal-loader')
32 .addClass('modal-body')
33 .text(L.tr('Loading data…')))))
41 state.modal.modal(enable ? 'show' : 'hide');
44 dialog: function(title, content, options)
50 var state = this._dialog || (this._dialog = {
52 .addClass('modal fade')
54 .addClass('modal-dialog')
56 .addClass('modal-content')
58 .addClass('modal-header')
60 .addClass('modal-title'))
62 .addClass('modal-body'))
64 .addClass('modal-footer')
65 .append(self.button(L.tr('Close'), 'primary')
67 $(this).parents('div.modal').modal('hide');
72 if (typeof(options) != 'object')
77 state.dialog.modal('hide');
82 var cnt = state.dialog.children().children().children('div.modal-body');
83 var ftr = state.dialog.children().children().children('div.modal-footer');
87 if (options.style == 'confirm')
89 ftr.append(L.ui.button(L.tr('Ok'), 'primary')
90 .click(options.confirm || function() { L.ui.dialog(false) }));
92 ftr.append(L.ui.button(L.tr('Cancel'), 'default')
93 .click(options.cancel || function() { L.ui.dialog(false) }));
95 else if (options.style == 'close')
97 ftr.append(L.ui.button(L.tr('Close'), 'primary')
98 .click(options.close || function() { L.ui.dialog(false) }));
100 else if (options.style == 'wait')
102 ftr.append(L.ui.button(L.tr('Close'), 'primary')
103 .attr('disabled', true));
108 state.dialog.addClass('wide');
112 state.dialog.removeClass('wide');
115 state.dialog.find('h4:first').text(title);
116 state.dialog.modal('show');
118 cnt.empty().append(content);
123 upload: function(title, content, options)
125 var state = L.ui._upload || (L.ui._upload = {
127 .attr('method', 'post')
128 .attr('action', '/cgi-bin/luci-upload')
129 .attr('enctype', 'multipart/form-data')
130 .attr('target', 'cbi-fileupload-frame')
132 .append($('<input />')
133 .attr('type', 'hidden')
134 .attr('name', 'sessionid'))
135 .append($('<input />')
136 .attr('type', 'hidden')
137 .attr('name', 'filename'))
138 .append($('<input />')
139 .attr('type', 'file')
140 .attr('name', 'filedata')
141 .addClass('cbi-input-file'))
143 .css('width', '100%')
144 .addClass('progress progress-striped active')
146 .addClass('progress-bar')
147 .css('width', '100%')))
148 .append($('<iframe />')
149 .addClass('pull-right')
150 .attr('name', 'cbi-fileupload-frame')
152 .css('height', '1px')
153 .css('visibility', 'hidden')),
155 finish_cb: function(ev) {
158 var body = (this.contentDocument || this.contentWindow.document).body;
159 if (body.firstChild.tagName.toLowerCase() == 'pre')
160 body = body.firstChild;
164 json = $.parseJSON(body.innerHTML);
167 message: L.tr('Invalid server response received'),
168 error: [ -1, L.tr('Invalid data') ]
174 L.ui.dialog(L.tr('File upload'), [
175 $('<p />').text(L.tr('The file upload failed with the server response below:')),
176 $('<pre />').addClass('alert-message').text(json.message || json.error[1]),
177 $('<p />').text(L.tr('In case of network problems try uploading the file again.'))
178 ], { style: 'close' });
180 else if (typeof(state.success_cb) == 'function')
182 state.success_cb(json);
186 confirm_cb: function() {
187 var f = state.form.find('.cbi-input-file');
188 var b = state.form.find('.progress');
189 var p = state.form.find('p');
194 state.form.find('iframe').on('load', state.finish_cb);
199 p.text(L.tr('File upload in progress …'));
201 state.form.parent().parent().find('button').prop('disabled', true);
205 state.form.find('.progress').hide();
206 state.form.find('.cbi-input-file').val('').show();
207 state.form.find('p').text(content || L.tr('Select the file to upload and press "%s" to proceed.').format(L.tr('Ok')));
209 state.form.find('[name=sessionid]').val(L.globals.sid);
210 state.form.find('[name=filename]').val(options.filename);
212 state.success_cb = options.success;
214 L.ui.dialog(title || L.tr('File upload'), state.form, {
216 confirm: state.confirm_cb
220 reconnect: function()
222 var protocols = (location.protocol == 'https:') ? [ 'http', 'https' ] : [ 'http' ];
223 var ports = (location.protocol == 'https:') ? [ 80, location.port || 443 ] : [ location.port || 80 ];
224 var address = location.hostname.match(/^[A-Fa-f0-9]*:[A-Fa-f0-9:]+$/) ? '[' + location.hostname + ']' : location.hostname;
226 var interval, timeout;
229 L.tr('Waiting for device'), [
230 $('<p />').text(L.tr('Please stand by while the device is reconfiguring …')),
232 .css('width', '100%')
233 .addClass('progressbar')
234 .addClass('intermediate')
236 .css('width', '100%'))
240 for (var i = 0; i < protocols.length; i++)
241 images = images.add($('<img />').attr('url', protocols[i] + '://' + address + ':' + ports[i]));
243 //L.network.getNetworkStatus(function(s) {
244 // for (var i = 0; i < protocols.length; i++)
246 // for (var j = 0; j < s.length; j++)
248 // for (var k = 0; k < s[j]['ipv4-address'].length; k++)
249 // images = images.add($('<img />').attr('url', protocols[i] + '://' + s[j]['ipv4-address'][k].address + ':' + ports[i]));
251 // for (var l = 0; l < s[j]['ipv6-address'].length; l++)
252 // images = images.add($('<img />').attr('url', protocols[i] + '://[' + s[j]['ipv6-address'][l].address + ']:' + ports[i]));
255 //}).then(function() {
256 images.on('load', function() {
257 var url = this.getAttribute('url');
258 L.session.isAlive().then(function(access) {
261 window.clearTimeout(timeout);
262 window.clearInterval(interval);
273 interval = window.setInterval(function() {
274 images.each(function() {
275 this.setAttribute('src', this.getAttribute('url') + L.globals.resource + '/icons/loading.gif?r=' + Math.random());
279 timeout = window.setTimeout(function() {
280 window.clearInterval(interval);
284 L.tr('Device not responding'),
285 L.tr('The device was not responding within 180 seconds, you might need to manually reconnect your computer or use SSH to regain access.'),
292 login: function(invalid)
294 var state = L.ui._login || (L.ui._login = {
297 .attr('method', 'post')
299 .addClass('alert alert-danger')
300 .text(L.tr('Wrong username or password given!')))
302 .append($('<label />')
303 .text(L.tr('Username'))
305 .append($('<input />')
306 .attr('type', 'text')
307 .attr('name', 'username')
308 .attr('value', 'root')
309 .addClass('form-control')
310 .keypress(function(ev) {
311 if (ev.which == 10 || ev.which == 13)
315 .append($('<label />')
316 .text(L.tr('Password'))
318 .append($('<input />')
319 .attr('type', 'password')
320 .attr('name', 'password')
321 .addClass('form-control')
322 .keypress(function(ev) {
323 if (ev.which == 10 || ev.which == 13)
327 .text(L.tr('Enter your username and password above, then click "%s" to proceed.').format(L.tr('Ok')))),
329 response_cb: function(response) {
330 if (!response.ubus_rpc_session)
336 L.globals.sid = response.ubus_rpc_session;
337 L.setHash('id', L.globals.sid);
338 L.session.startHeartbeat();
340 state.deferred.resolve();
344 confirm_cb: function() {
345 var u = state.form.find('[name=username]').val();
346 var p = state.form.find('[name=password]').val();
352 L.tr('Logging in'), [
353 $('<p />').text(L.tr('Log in in progress …')),
355 .css('width', '100%')
356 .addClass('progressbar')
357 .addClass('intermediate')
359 .css('width', '100%'))
363 L.globals.sid = '00000000000000000000000000000000';
364 L.session.login(u, p).then(state.response_cb);
368 if (!state.deferred || state.deferred.state() != 'pending')
369 state.deferred = $.Deferred();
371 /* try to find sid from hash */
372 var sid = L.getHash('id');
373 if (sid && sid.match(/^[a-f0-9]{32}$/))
376 L.session.isAlive().then(function(access) {
379 L.session.startHeartbeat();
380 state.deferred.resolve();
384 L.setHash('id', undefined);
389 return state.deferred;
393 state.form.find('.alert').show();
395 state.form.find('.alert').hide();
397 L.ui.dialog(L.tr('Authorization Required'), state.form, {
399 confirm: state.confirm_cb
402 state.form.find('[name=password]').focus();
404 return state.deferred;
407 cryptPassword: L.rpc.declare({
411 expect: { crypt: '' }
415 mergeACLScope: function(acl_scope, scope)
417 if ($.isArray(scope))
419 for (var i = 0; i < scope.length; i++)
420 acl_scope[scope[i]] = true;
422 else if ($.isPlainObject(scope))
424 for (var object_name in scope)
426 if (!$.isArray(scope[object_name]))
429 var acl_object = acl_scope[object_name] || (acl_scope[object_name] = { });
431 for (var i = 0; i < scope[object_name].length; i++)
432 acl_object[scope[object_name][i]] = true;
437 mergeACLPermission: function(acl_perm, perm)
439 if ($.isPlainObject(perm))
441 for (var scope_name in perm)
443 var acl_scope = acl_perm[scope_name] || (acl_perm[scope_name] = { });
444 L.ui.mergeACLScope(acl_scope, perm[scope_name]);
449 mergeACLGroup: function(acl_group, group)
451 if ($.isPlainObject(group))
453 if (!acl_group.description)
454 acl_group.description = group.description;
458 var acl_perm = acl_group.read || (acl_group.read = { });
459 L.ui.mergeACLPermission(acl_perm, group.read);
464 var acl_perm = acl_group.write || (acl_group.write = { });
465 L.ui.mergeACLPermission(acl_perm, group.write);
470 callACLsCallback: function(trees)
474 for (var i = 0; i < trees.length; i++)
476 if (!$.isPlainObject(trees[i]))
479 for (var group_name in trees[i])
481 var acl_group = acl_tree[group_name] || (acl_tree[group_name] = { });
482 L.ui.mergeACLGroup(acl_group, trees[i][group_name]);
489 callACLs: L.rpc.declare({
492 expect: { acls: [ ] }
495 getAvailableACLs: function()
497 return this.callACLs().then(this.callACLsCallback);
500 renderChangeIndicator: function()
503 .addClass('nav navbar-nav navbar-right')
506 .attr('id', 'changes')
508 .append($('<span />')
509 .addClass('label label-info'))));
512 callMenuCallback: function(entries)
514 L.globals.mainMenu = new L.ui.menu();
515 L.globals.mainMenu.entries(entries);
519 .append(L.globals.mainMenu.render(0, 1))
520 .append(L.ui.renderChangeIndicator());
523 callMenu: L.rpc.declare({
526 expect: { menu: { } }
529 renderMainMenu: function()
531 return this.callMenu().then(this.callMenuCallback);
534 renderViewMenu: function()
538 .append(L.globals.mainMenu.render(2, 900));
541 renderView: function()
543 var node = arguments[0];
544 var name = node.view.split(/\//).join('.');
545 var cname = L.toClassName(name);
546 var views = L.views || (L.views = { });
549 for (var i = 1; i < arguments.length; i++)
550 args.push(arguments[i]);
552 if (L.globals.currentView)
553 L.globals.currentView.finish();
555 L.ui.renderViewMenu();
556 L.setHash('view', node.view);
558 if (views[cname] instanceof L.ui.view)
560 L.globals.currentView = views[cname];
561 return views[cname].render.apply(views[cname], args);
564 var url = L.globals.resource + '/view/' + name + '.js';
570 }).then(function(data) {
572 var viewConstructorSource = (
573 '(function(L, $) { ' +
579 var viewConstructor = eval(viewConstructorSource);
581 views[cname] = new viewConstructor({
583 acls: node.write || { }
586 L.globals.currentView = views[cname];
587 return views[cname].render.apply(views[cname], args);
590 alert('Unable to instantiate view "%s": %s'.format(url, e));
593 return $.Deferred().resolve();
597 changeView: function()
599 var name = L.getHash('view');
600 var node = L.globals.defaultNode;
602 if (name && L.globals.mainMenu)
603 node = L.globals.mainMenu.getNode(name);
608 L.ui.renderView(node).then(function() {
609 $('#mainmenu.in').collapse('hide');
615 updateHostname: function()
617 return L.system.getBoardInfo().then(function(info) {
619 $('#hostname').text(info.hostname);
623 updateChanges: function()
625 return L.uci.changes().then(function(changes) {
629 for (var config in changes)
633 for (var i = 0; i < changes[config].length; i++)
635 var c = changes[config][i];
640 log.push('uci reorder %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2]));
645 log.push('uci delete %s.<del>%s</del>'.format(config, c[1]));
647 log.push('uci delete %s.%s.<del>%s</del>'.format(config, c[1], c[2]));
652 log.push('uci rename %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3]));
654 log.push('uci rename %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
658 log.push('uci add %s <ins>%s</ins> (= <ins><strong>%s</strong></ins>)'.format(config, c[2], c[1]));
662 log.push('uci add_list %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
666 log.push('uci del_list %s.%s.<del>%s=<strong>%s</strong></del>'.format(config, c[1], c[2], c[3], c[4]));
671 log.push('uci set %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2]));
673 log.push('uci set %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
678 html += '<code>/etc/config/%s</code><pre class="uci-changes">%s</pre>'.format(config, log.join('\n'));
679 n += changes[config].length;
684 .click(function(ev) {
685 L.ui.dialog(L.tr('Staged configuration changes'), html, {
687 confirm: function() {
689 function(code) { alert('Success with code ' + code); },
690 function(code) { alert('Error with code ' + code); }
698 .text(L.trcp('Pending configuration changes', '1 change', '%d changes', n).format(n));
700 $('#changes').children('span').hide();
711 L.session.updateACLs(),
712 self.updateHostname(),
713 self.updateChanges(),
714 self.renderMainMenu(),
717 self.renderView(L.globals.defaultNode).then(function() {
721 $(window).on('hashchange', function() {
727 button: function(label, style, title)
729 style = style || 'default';
731 return $('<button />')
732 .attr('type', 'button')
733 .attr('title', title ? title : '')
734 .addClass('btn btn-' + style)
738 icon: function(src, alt, title)
740 if (!src.match(/\.[a-z]+$/))
743 if (!src.match(/^\//))
744 src = L.globals.resource + '/icons/' + src;
746 var icon = $('<img />')
749 if (typeof(alt) !== 'undefined')
750 icon.attr('alt', alt);
752 if (typeof(title) !== 'undefined')
753 icon.attr('title', title);
759 ui_class.AbstractWidget = Class.extend({
760 i18n: function(text) {
765 var key = arguments[0];
768 for (var i = 1; i < arguments.length; i++)
769 args.push(arguments[i]);
771 switch (typeof(this.options[key]))
777 return this.options[key].apply(this, args);
780 return ''.format.apply('' + this.options[key], args);
784 toString: function() {
785 return $('<div />').append(this.render()).html();
788 insertInto: function(id) {
789 return $(id).empty().append(this.render());
792 appendTo: function(id) {
793 return $(id).append(this.render());
796 on: function(evname, evfunc)
798 var evnames = L.toArray(evname);
803 for (var i = 0; i < evnames.length; i++)
804 this.events[evnames[i]] = evfunc;
809 trigger: function(evname, evdata)
813 var evnames = L.toArray(evname);
815 for (var i = 0; i < evnames.length; i++)
816 if (this.events[evnames[i]])
817 this.events[evnames[i]].call(this, evdata);
824 ui_class.view = ui_class.AbstractWidget.extend({
825 _fetch_template: function()
827 return $.ajax(L.globals.resource + '/template/' + this.options.name + '.htm', {
831 success: function(data) {
832 data = data.replace(/<%([#:=])?(.+?)%>/g, function(match, p1, p2) {
833 p2 = p2.replace(/^\s+/, '').replace(/\s+$/, '');
843 return L.globals[p2] || '';
846 return '(?' + match + ')';
850 $('#maincontent').append(data);
857 throw "Not implemented";
862 var container = $('#maincontent');
867 container.append($('<h2 />').append(this.title));
869 if (this.description)
870 container.append($('<p />').append(this.description));
875 for (var i = 0; i < arguments.length; i++)
876 args.push(arguments[i]);
878 return this._fetch_template().then(function() {
879 return L.deferrable(self.execute.apply(self, args));
883 repeat: function(func, interval)
888 self._timeouts = [ ];
890 var index = self._timeouts.length;
892 if (typeof(interval) != 'number')
895 var setTimer, runTimer;
897 setTimer = function() {
899 self._timeouts[index] = window.setTimeout(runTimer, interval);
902 runTimer = function() {
903 L.deferrable(func.call(self)).then(setTimer, setTimer);
911 if ($.isArray(this._timeouts))
913 for (var i = 0; i < this._timeouts.length; i++)
914 window.clearTimeout(this._timeouts[i]);
916 delete this._timeouts;
921 ui_class.menu = ui_class.AbstractWidget.extend({
926 entries: function(entries)
928 for (var entry in entries)
930 var path = entry.split(/\//);
931 var node = this._nodes;
933 for (i = 0; i < path.length; i++)
938 if (!node.childs[path[i]])
939 node.childs[path[i]] = { };
941 node = node.childs[path[i]];
944 $.extend(node, entries[entry]);
948 sortNodesCallback: function(a, b)
950 var x = a.index || 0;
951 var y = b.index || 0;
955 firstChildView: function(node)
961 for (var child in (node.childs || { }))
962 nodes.push(node.childs[child]);
964 nodes.sort(this.sortNodesCallback);
966 for (var i = 0; i < nodes.length; i++)
968 var child = this.firstChildView(nodes[i]);
971 for (var key in child)
972 if (!node.hasOwnProperty(key) && child.hasOwnProperty(key))
973 node[key] = child[key];
982 handleClick: function(ev)
984 L.setHash('view', ev.data);
990 renderNodes: function(childs, level, min, max)
993 for (var node in childs)
995 var child = this.firstChildView(childs[node]);
997 nodes.push(childs[node]);
1000 nodes.sort(this.sortNodesCallback);
1002 var list = $('<ul />');
1005 list.addClass('nav').addClass('navbar-nav');
1006 else if (level == 1)
1007 list.addClass('dropdown-menu').addClass('navbar-inverse');
1009 for (var i = 0; i < nodes.length; i++)
1011 if (!L.globals.defaultNode)
1013 var v = L.getHash('view');
1014 if (!v || v == nodes[i].view)
1015 L.globals.defaultNode = nodes[i];
1018 var item = $('<li />')
1021 .text(L.tr(nodes[i].title)))
1024 if (nodes[i].childs && level < max)
1026 item.addClass('dropdown');
1029 .addClass('dropdown-toggle')
1030 .attr('data-toggle', 'dropdown')
1031 .append('<b class="caret"></b>');
1033 item.append(this.renderNodes(nodes[i].childs, level + 1));
1037 item.find('a').click(nodes[i].view, this.handleClick);
1044 render: function(min, max)
1046 var top = min ? this.getNode(L.globals.defaultNode.view, min) : this._nodes;
1047 return this.renderNodes(top.childs, 0, min, max);
1050 getNode: function(path, max)
1052 var p = path.split(/\//);
1053 var n = this._nodes;
1055 if (typeof(max) == 'undefined')
1058 for (var i = 0; i < max; i++)
1060 if (!n.childs[p[i]])
1070 ui_class.table = ui_class.AbstractWidget.extend({
1076 row: function(values)
1078 if ($.isArray(values))
1080 this._rows.push(values);
1082 else if ($.isPlainObject(values))
1085 for (var i = 0; i < this.options.columns.length; i++)
1087 var col = this.options.columns[i];
1089 if (typeof col.key == 'string')
1090 v.push(values[col.key]);
1098 rows: function(rows)
1100 for (var i = 0; i < rows.length; i++)
1104 render: function(id)
1106 var fieldset = document.createElement('fieldset');
1107 fieldset.className = 'cbi-section';
1109 if (this.options.caption)
1111 var legend = document.createElement('legend');
1112 $(legend).append(this.options.caption);
1113 fieldset.appendChild(legend);
1116 var table = document.createElement('table');
1117 table.className = 'table table-condensed table-hover';
1119 var has_caption = false;
1120 var has_description = false;
1122 for (var i = 0; i < this.options.columns.length; i++)
1123 if (this.options.columns[i].caption)
1128 else if (this.options.columns[i].description)
1130 has_description = true;
1136 var tr = table.insertRow(-1);
1137 tr.className = 'cbi-section-table-titles';
1139 for (var i = 0; i < this.options.columns.length; i++)
1141 var col = this.options.columns[i];
1142 var th = document.createElement('th');
1143 th.className = 'cbi-section-table-cell';
1148 th.style.width = col.width;
1151 th.style.textAlign = col.align;
1154 $(th).append(col.caption);
1158 if (has_description)
1160 var tr = table.insertRow(-1);
1161 tr.className = 'cbi-section-table-descr';
1163 for (var i = 0; i < this.options.columns.length; i++)
1165 var col = this.options.columns[i];
1166 var th = document.createElement('th');
1167 th.className = 'cbi-section-table-cell';
1172 th.style.width = col.width;
1175 th.style.textAlign = col.align;
1177 if (col.description)
1178 $(th).append(col.description);
1182 if (this._rows.length == 0)
1184 if (this.options.placeholder)
1186 var tr = table.insertRow(-1);
1187 var td = tr.insertCell(-1);
1188 td.className = 'cbi-section-table-cell';
1190 td.colSpan = this.options.columns.length;
1191 $(td).append(this.options.placeholder);
1196 for (var i = 0; i < this._rows.length; i++)
1198 var tr = table.insertRow(-1);
1200 for (var j = 0; j < this.options.columns.length; j++)
1202 var col = this.options.columns[j];
1203 var td = tr.insertCell(-1);
1205 var val = this._rows[i][j];
1207 if (typeof(val) == 'undefined')
1208 val = col.placeholder;
1210 if (typeof(val) == 'undefined')
1214 td.style.width = col.width;
1217 td.style.textAlign = col.align;
1219 if (typeof col.format == 'string')
1220 $(td).append(col.format.format(val));
1221 else if (typeof col.format == 'function')
1222 $(td).append(col.format(val, i));
1230 fieldset.appendChild(table);
1236 ui_class.progress = ui_class.AbstractWidget.extend({
1239 var vn = parseInt(this.options.value) || 0;
1240 var mn = parseInt(this.options.max) || 100;
1241 var pc = Math.floor((100 / mn) * vn);
1245 if (typeof(this.options.format) == 'string')
1246 text = this.options.format.format(this.options.value, this.options.max, pc);
1247 else if (typeof(this.options.format) == 'function')
1248 text = this.options.format(pc);
1250 text = '%.2f%%'.format(pc);
1253 .addClass('progress')
1254 .append($('<div />')
1255 .addClass('progress-bar')
1256 .addClass('progress-bar-info')
1257 .css('width', pc + '%'))
1258 .append($('<small />')
1263 ui_class.devicebadge = ui_class.AbstractWidget.extend({
1266 var l2dev = this.options.l2_device || this.options.device;
1267 var l3dev = this.options.l3_device;
1268 var dev = l3dev || l2dev || '?';
1270 var span = document.createElement('span');
1271 span.className = 'badge';
1273 if (typeof(this.options.signal) == 'number' ||
1274 typeof(this.options.noise) == 'number')
1277 if (typeof(this.options.signal) != 'undefined' &&
1278 typeof(this.options.noise) != 'undefined')
1280 var q = (-1 * (this.options.noise - this.options.signal)) / 5;
1293 span.appendChild(document.createElement('img'));
1294 span.lastChild.src = L.globals.resource + '/icons/signal-' + r + '.png';
1297 span.title = L.tr('No signal');
1299 span.title = '%s: %d %s / %s: %d %s'.format(
1300 L.tr('Signal'), this.options.signal, L.tr('dBm'),
1301 L.tr('Noise'), this.options.noise, L.tr('dBm')
1306 var type = 'ethernet';
1307 var desc = L.tr('Ethernet device');
1312 desc = L.tr('Tunnel interface');
1314 else if (dev.indexOf('br-') == 0)
1317 desc = L.tr('Bridge');
1319 else if (dev.indexOf('.') > 0)
1322 desc = L.tr('VLAN interface');
1324 else if (dev.indexOf('wlan') == 0 ||
1325 dev.indexOf('ath') == 0 ||
1326 dev.indexOf('wl') == 0)
1329 desc = L.tr('Wireless Network');
1332 span.appendChild(document.createElement('img'));
1333 span.lastChild.src = L.globals.resource + '/icons/' + type + (this.options.up ? '' : '_disabled') + '.png';
1337 $(span).append(' ');
1338 $(span).append(dev);
1344 return Class.extend(ui_class);