luci2.ui: add icon() helper function
[project/luci2/ui.git] / luci2 / htdocs / luci2 / ui.js
1 (function() {
2 var ui_class = {
3 saveScrollTop: function()
4 {
5 this._scroll_top = $(document).scrollTop();
6 },
7
8 restoreScrollTop: function()
9 {
10 if (typeof(this._scroll_top) == 'undefined')
11 return;
12
13 $(document).scrollTop(this._scroll_top);
14
15 delete this._scroll_top;
16 },
17
18 loading: function(enable)
19 {
20 var win = $(window);
21 var body = $('body');
22
23 var state = this._loading || (this._loading = {
24 modal: $('<div />')
25 .css('z-index', 2000)
26 .addClass('modal fade')
27 .append($('<div />')
28 .addClass('modal-dialog')
29 .append($('<div />')
30 .addClass('modal-content luci2-modal-loader')
31 .append($('<div />')
32 .addClass('modal-body')
33 .text(L.tr('Loading data…')))))
34 .appendTo(body)
35 .modal({
36 backdrop: 'static',
37 keyboard: false
38 })
39 });
40
41 state.modal.modal(enable ? 'show' : 'hide');
42 },
43
44 dialog: function(title, content, options)
45 {
46 var win = $(window);
47 var body = $('body');
48 var self = this;
49
50 var state = this._dialog || (this._dialog = {
51 dialog: $('<div />')
52 .addClass('modal fade')
53 .append($('<div />')
54 .addClass('modal-dialog')
55 .append($('<div />')
56 .addClass('modal-content')
57 .append($('<div />')
58 .addClass('modal-header')
59 .append('<h4 />')
60 .addClass('modal-title'))
61 .append($('<div />')
62 .addClass('modal-body'))
63 .append($('<div />')
64 .addClass('modal-footer')
65 .append(self.button(L.tr('Close'), 'primary')
66 .click(function() {
67 $(this).parents('div.modal').modal('hide');
68 })))))
69 .appendTo(body)
70 });
71
72 if (typeof(options) != 'object')
73 options = { };
74
75 if (title === false)
76 {
77 state.dialog.modal('hide');
78
79 return state.dialog;
80 }
81
82 var cnt = state.dialog.children().children().children('div.modal-body');
83 var ftr = state.dialog.children().children().children('div.modal-footer');
84
85 ftr.empty().show();
86
87 if (options.style == 'confirm')
88 {
89 ftr.append(L.ui.button(L.tr('Ok'), 'primary')
90 .click(options.confirm || function() { L.ui.dialog(false) }));
91
92 ftr.append(L.ui.button(L.tr('Cancel'), 'default')
93 .click(options.cancel || function() { L.ui.dialog(false) }));
94 }
95 else if (options.style == 'close')
96 {
97 ftr.append(L.ui.button(L.tr('Close'), 'primary')
98 .click(options.close || function() { L.ui.dialog(false) }));
99 }
100 else if (options.style == 'wait')
101 {
102 ftr.append(L.ui.button(L.tr('Close'), 'primary')
103 .attr('disabled', true));
104 }
105
106 if (options.wide)
107 {
108 state.dialog.addClass('wide');
109 }
110 else
111 {
112 state.dialog.removeClass('wide');
113 }
114
115 state.dialog.find('h4:first').text(title);
116 state.dialog.modal('show');
117
118 cnt.empty().append(content);
119
120 return state.dialog;
121 },
122
123 upload: function(title, content, options)
124 {
125 var state = L.ui._upload || (L.ui._upload = {
126 form: $('<form />')
127 .attr('method', 'post')
128 .attr('action', '/cgi-bin/luci-upload')
129 .attr('enctype', 'multipart/form-data')
130 .attr('target', 'cbi-fileupload-frame')
131 .append($('<p />'))
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'))
142 .append($('<div />')
143 .css('width', '100%')
144 .addClass('progress progress-striped active')
145 .append($('<div />')
146 .addClass('progress-bar')
147 .css('width', '100%')))
148 .append($('<iframe />')
149 .addClass('pull-right')
150 .attr('name', 'cbi-fileupload-frame')
151 .css('width', '1px')
152 .css('height', '1px')
153 .css('visibility', 'hidden')),
154
155 finish_cb: function(ev) {
156 $(this).off('load');
157
158 var body = (this.contentDocument || this.contentWindow.document).body;
159 if (body.firstChild.tagName.toLowerCase() == 'pre')
160 body = body.firstChild;
161
162 var json;
163 try {
164 json = $.parseJSON(body.innerHTML);
165 } catch(e) {
166 json = {
167 message: L.tr('Invalid server response received'),
168 error: [ -1, L.tr('Invalid data') ]
169 };
170 };
171
172 if (json.error)
173 {
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' });
179 }
180 else if (typeof(state.success_cb) == 'function')
181 {
182 state.success_cb(json);
183 }
184 },
185
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');
190
191 if (!f.val())
192 return;
193
194 state.form.find('iframe').on('load', state.finish_cb);
195 state.form.submit();
196
197 f.hide();
198 b.show();
199 p.text(L.tr('File upload in progress …'));
200
201 state.form.parent().parent().find('button').prop('disabled', true);
202 }
203 });
204
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')));
208
209 state.form.find('[name=sessionid]').val(L.globals.sid);
210 state.form.find('[name=filename]').val(options.filename);
211
212 state.success_cb = options.success;
213
214 L.ui.dialog(title || L.tr('File upload'), state.form, {
215 style: 'confirm',
216 confirm: state.confirm_cb
217 });
218 },
219
220 reconnect: function()
221 {
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;
225 var images = $();
226 var interval, timeout;
227
228 L.ui.dialog(
229 L.tr('Waiting for device'), [
230 $('<p />').text(L.tr('Please stand by while the device is reconfiguring …')),
231 $('<div />')
232 .css('width', '100%')
233 .addClass('progressbar')
234 .addClass('intermediate')
235 .append($('<div />')
236 .css('width', '100%'))
237 ], { style: 'wait' }
238 );
239
240 for (var i = 0; i < protocols.length; i++)
241 images = images.add($('<img />').attr('url', protocols[i] + '://' + address + ':' + ports[i]));
242
243 //L.network.getNetworkStatus(function(s) {
244 // for (var i = 0; i < protocols.length; i++)
245 // {
246 // for (var j = 0; j < s.length; j++)
247 // {
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]));
250 //
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]));
253 // }
254 // }
255 //}).then(function() {
256 images.on('load', function() {
257 var url = this.getAttribute('url');
258 L.session.isAlive().then(function(access) {
259 if (access)
260 {
261 window.clearTimeout(timeout);
262 window.clearInterval(interval);
263 L.ui.dialog(false);
264 images = null;
265 }
266 else
267 {
268 location.href = url;
269 }
270 });
271 });
272
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());
276 });
277 }, 5000);
278
279 timeout = window.setTimeout(function() {
280 window.clearInterval(interval);
281 images.off('load');
282
283 L.ui.dialog(
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.'),
286 { style: 'close' }
287 );
288 }, 180000);
289 //});
290 },
291
292 login: function(invalid)
293 {
294 var state = L.ui._login || (L.ui._login = {
295 form: $('<form />')
296 .attr('target', '')
297 .attr('method', 'post')
298 .append($('<p />')
299 .addClass('alert alert-danger')
300 .text(L.tr('Wrong username or password given!')))
301 .append($('<p />')
302 .append($('<label />')
303 .text(L.tr('Username'))
304 .append($('<br />'))
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)
312 state.confirm_cb();
313 }))))
314 .append($('<p />')
315 .append($('<label />')
316 .text(L.tr('Password'))
317 .append($('<br />'))
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)
324 state.confirm_cb();
325 }))))
326 .append($('<p />')
327 .text(L.tr('Enter your username and password above, then click "%s" to proceed.').format(L.tr('Ok')))),
328
329 response_cb: function(response) {
330 if (!response.ubus_rpc_session)
331 {
332 L.ui.login(true);
333 }
334 else
335 {
336 L.globals.sid = response.ubus_rpc_session;
337 L.setHash('id', L.globals.sid);
338 L.session.startHeartbeat();
339 L.ui.dialog(false);
340 state.deferred.resolve();
341 }
342 },
343
344 confirm_cb: function() {
345 var u = state.form.find('[name=username]').val();
346 var p = state.form.find('[name=password]').val();
347
348 if (!u)
349 return;
350
351 L.ui.dialog(
352 L.tr('Logging in'), [
353 $('<p />').text(L.tr('Log in in progress …')),
354 $('<div />')
355 .css('width', '100%')
356 .addClass('progressbar')
357 .addClass('intermediate')
358 .append($('<div />')
359 .css('width', '100%'))
360 ], { style: 'wait' }
361 );
362
363 L.globals.sid = '00000000000000000000000000000000';
364 L.session.login(u, p).then(state.response_cb);
365 }
366 });
367
368 if (!state.deferred || state.deferred.state() != 'pending')
369 state.deferred = $.Deferred();
370
371 /* try to find sid from hash */
372 var sid = L.getHash('id');
373 if (sid && sid.match(/^[a-f0-9]{32}$/))
374 {
375 L.globals.sid = sid;
376 L.session.isAlive().then(function(access) {
377 if (access)
378 {
379 L.session.startHeartbeat();
380 state.deferred.resolve();
381 }
382 else
383 {
384 L.setHash('id', undefined);
385 L.ui.login();
386 }
387 });
388
389 return state.deferred;
390 }
391
392 if (invalid)
393 state.form.find('.alert').show();
394 else
395 state.form.find('.alert').hide();
396
397 L.ui.dialog(L.tr('Authorization Required'), state.form, {
398 style: 'confirm',
399 confirm: state.confirm_cb
400 });
401
402 state.form.find('[name=password]').focus();
403
404 return state.deferred;
405 },
406
407 cryptPassword: L.rpc.declare({
408 object: 'luci2.ui',
409 method: 'crypt',
410 params: [ 'data' ],
411 expect: { crypt: '' }
412 }),
413
414
415 mergeACLScope: function(acl_scope, scope)
416 {
417 if ($.isArray(scope))
418 {
419 for (var i = 0; i < scope.length; i++)
420 acl_scope[scope[i]] = true;
421 }
422 else if ($.isPlainObject(scope))
423 {
424 for (var object_name in scope)
425 {
426 if (!$.isArray(scope[object_name]))
427 continue;
428
429 var acl_object = acl_scope[object_name] || (acl_scope[object_name] = { });
430
431 for (var i = 0; i < scope[object_name].length; i++)
432 acl_object[scope[object_name][i]] = true;
433 }
434 }
435 },
436
437 mergeACLPermission: function(acl_perm, perm)
438 {
439 if ($.isPlainObject(perm))
440 {
441 for (var scope_name in perm)
442 {
443 var acl_scope = acl_perm[scope_name] || (acl_perm[scope_name] = { });
444 L.ui.mergeACLScope(acl_scope, perm[scope_name]);
445 }
446 }
447 },
448
449 mergeACLGroup: function(acl_group, group)
450 {
451 if ($.isPlainObject(group))
452 {
453 if (!acl_group.description)
454 acl_group.description = group.description;
455
456 if (group.read)
457 {
458 var acl_perm = acl_group.read || (acl_group.read = { });
459 L.ui.mergeACLPermission(acl_perm, group.read);
460 }
461
462 if (group.write)
463 {
464 var acl_perm = acl_group.write || (acl_group.write = { });
465 L.ui.mergeACLPermission(acl_perm, group.write);
466 }
467 }
468 },
469
470 callACLsCallback: function(trees)
471 {
472 var acl_tree = { };
473
474 for (var i = 0; i < trees.length; i++)
475 {
476 if (!$.isPlainObject(trees[i]))
477 continue;
478
479 for (var group_name in trees[i])
480 {
481 var acl_group = acl_tree[group_name] || (acl_tree[group_name] = { });
482 L.ui.mergeACLGroup(acl_group, trees[i][group_name]);
483 }
484 }
485
486 return acl_tree;
487 },
488
489 callACLs: L.rpc.declare({
490 object: 'luci2.ui',
491 method: 'acls',
492 expect: { acls: [ ] }
493 }),
494
495 getAvailableACLs: function()
496 {
497 return this.callACLs().then(this.callACLsCallback);
498 },
499
500 renderChangeIndicator: function()
501 {
502 return $('<ul />')
503 .addClass('nav navbar-nav navbar-right')
504 .append($('<li />')
505 .append($('<a />')
506 .attr('id', 'changes')
507 .attr('href', '#')
508 .append($('<span />')
509 .addClass('label label-info'))));
510 },
511
512 callMenuCallback: function(entries)
513 {
514 L.globals.mainMenu = new L.ui.menu();
515 L.globals.mainMenu.entries(entries);
516
517 $('#mainmenu')
518 .empty()
519 .append(L.globals.mainMenu.render(0, 1))
520 .append(L.ui.renderChangeIndicator());
521 },
522
523 callMenu: L.rpc.declare({
524 object: 'luci2.ui',
525 method: 'menu',
526 expect: { menu: { } }
527 }),
528
529 renderMainMenu: function()
530 {
531 return this.callMenu().then(this.callMenuCallback);
532 },
533
534 renderViewMenu: function()
535 {
536 $('#viewmenu')
537 .empty()
538 .append(L.globals.mainMenu.render(2, 900));
539 },
540
541 renderView: function()
542 {
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 = { });
547 var args = [ ];
548
549 for (var i = 1; i < arguments.length; i++)
550 args.push(arguments[i]);
551
552 if (L.globals.currentView)
553 L.globals.currentView.finish();
554
555 L.ui.renderViewMenu();
556 L.setHash('view', node.view);
557
558 if (views[cname] instanceof L.ui.view)
559 {
560 L.globals.currentView = views[cname];
561 return views[cname].render.apply(views[cname], args);
562 }
563
564 var url = L.globals.resource + '/view/' + name + '.js';
565
566 return $.ajax(url, {
567 method: 'GET',
568 cache: true,
569 dataType: 'text'
570 }).then(function(data) {
571 try {
572 var viewConstructorSource = (
573 '(function(L, $) { ' +
574 'return %s' +
575 '})(L, $);\n\n' +
576 '//@ sourceURL=%s'
577 ).format(data, url);
578
579 var viewConstructor = eval(viewConstructorSource);
580
581 views[cname] = new viewConstructor({
582 name: name,
583 acls: node.write || { }
584 });
585
586 L.globals.currentView = views[cname];
587 return views[cname].render.apply(views[cname], args);
588 }
589 catch(e) {
590 alert('Unable to instantiate view "%s": %s'.format(url, e));
591 };
592
593 return $.Deferred().resolve();
594 });
595 },
596
597 changeView: function()
598 {
599 var name = L.getHash('view');
600 var node = L.globals.defaultNode;
601
602 if (name && L.globals.mainMenu)
603 node = L.globals.mainMenu.getNode(name);
604
605 if (node)
606 {
607 L.ui.loading(true);
608 L.ui.renderView(node).then(function() {
609 $('#mainmenu.in').collapse('hide');
610 L.ui.loading(false);
611 });
612 }
613 },
614
615 updateHostname: function()
616 {
617 return L.system.getBoardInfo().then(function(info) {
618 if (info.hostname)
619 $('#hostname').text(info.hostname);
620 });
621 },
622
623 updateChanges: function()
624 {
625 return L.uci.changes().then(function(changes) {
626 var n = 0;
627 var html = '';
628
629 for (var config in changes)
630 {
631 var log = [ ];
632
633 for (var i = 0; i < changes[config].length; i++)
634 {
635 var c = changes[config][i];
636
637 switch (c[0])
638 {
639 case 'order':
640 log.push('uci reorder %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2]));
641 break;
642
643 case 'remove':
644 if (c.length < 3)
645 log.push('uci delete %s.<del>%s</del>'.format(config, c[1]));
646 else
647 log.push('uci delete %s.%s.<del>%s</del>'.format(config, c[1], c[2]));
648 break;
649
650 case 'rename':
651 if (c.length < 4)
652 log.push('uci rename %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3]));
653 else
654 log.push('uci rename %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
655 break;
656
657 case 'add':
658 log.push('uci add %s <ins>%s</ins> (= <ins><strong>%s</strong></ins>)'.format(config, c[2], c[1]));
659 break;
660
661 case 'list-add':
662 log.push('uci add_list %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
663 break;
664
665 case 'list-del':
666 log.push('uci del_list %s.%s.<del>%s=<strong>%s</strong></del>'.format(config, c[1], c[2], c[3], c[4]));
667 break;
668
669 case 'set':
670 if (c.length < 4)
671 log.push('uci set %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2]));
672 else
673 log.push('uci set %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
674 break;
675 }
676 }
677
678 html += '<code>/etc/config/%s</code><pre class="uci-changes">%s</pre>'.format(config, log.join('\n'));
679 n += changes[config].length;
680 }
681
682 if (n > 0)
683 $('#changes')
684 .click(function(ev) {
685 L.ui.dialog(L.tr('Staged configuration changes'), html, {
686 style: 'confirm',
687 confirm: function() {
688 L.uci.apply().then(
689 function(code) { alert('Success with code ' + code); },
690 function(code) { alert('Error with code ' + code); }
691 );
692 }
693 });
694 ev.preventDefault();
695 })
696 .children('span')
697 .show()
698 .text(L.trcp('Pending configuration changes', '1 change', '%d changes', n).format(n));
699 else
700 $('#changes').children('span').hide();
701 });
702 },
703
704 load: function()
705 {
706 var self = this;
707
708 self.loading(true);
709
710 $.when(
711 L.session.updateACLs(),
712 self.updateHostname(),
713 self.updateChanges(),
714 self.renderMainMenu(),
715 L.network.load()
716 ).then(function() {
717 self.renderView(L.globals.defaultNode).then(function() {
718 self.loading(false);
719 });
720
721 $(window).on('hashchange', function() {
722 self.changeView();
723 });
724 });
725 },
726
727 button: function(label, style, title)
728 {
729 style = style || 'default';
730
731 return $('<button />')
732 .attr('type', 'button')
733 .attr('title', title ? title : '')
734 .addClass('btn btn-' + style)
735 .text(label);
736 },
737
738 icon: function(src, alt, title)
739 {
740 if (!src.match(/\.[a-z]+$/))
741 src += '.png';
742
743 if (!src.match(/^\//))
744 src = L.globals.resource + '/icons/' + src;
745
746 var icon = $('<img />')
747 .attr('src', src);
748
749 if (typeof(alt) !== 'undefined')
750 icon.attr('alt', alt);
751
752 if (typeof(title) !== 'undefined')
753 icon.attr('title', title);
754
755 return icon;
756 }
757 };
758
759 ui_class.AbstractWidget = Class.extend({
760 i18n: function(text) {
761 return text;
762 },
763
764 label: function() {
765 var key = arguments[0];
766 var args = [ ];
767
768 for (var i = 1; i < arguments.length; i++)
769 args.push(arguments[i]);
770
771 switch (typeof(this.options[key]))
772 {
773 case 'undefined':
774 return '';
775
776 case 'function':
777 return this.options[key].apply(this, args);
778
779 default:
780 return ''.format.apply('' + this.options[key], args);
781 }
782 },
783
784 toString: function() {
785 return $('<div />').append(this.render()).html();
786 },
787
788 insertInto: function(id) {
789 return $(id).empty().append(this.render());
790 },
791
792 appendTo: function(id) {
793 return $(id).append(this.render());
794 },
795
796 on: function(evname, evfunc)
797 {
798 var evnames = L.toArray(evname);
799
800 if (!this.events)
801 this.events = { };
802
803 for (var i = 0; i < evnames.length; i++)
804 this.events[evnames[i]] = evfunc;
805
806 return this;
807 },
808
809 trigger: function(evname, evdata)
810 {
811 if (this.events)
812 {
813 var evnames = L.toArray(evname);
814
815 for (var i = 0; i < evnames.length; i++)
816 if (this.events[evnames[i]])
817 this.events[evnames[i]].call(this, evdata);
818 }
819
820 return this;
821 }
822 });
823
824 ui_class.view = ui_class.AbstractWidget.extend({
825 _fetch_template: function()
826 {
827 return $.ajax(L.globals.resource + '/template/' + this.options.name + '.htm', {
828 method: 'GET',
829 cache: true,
830 dataType: 'text',
831 success: function(data) {
832 data = data.replace(/<%([#:=])?(.+?)%>/g, function(match, p1, p2) {
833 p2 = p2.replace(/^\s+/, '').replace(/\s+$/, '');
834 switch (p1)
835 {
836 case '#':
837 return '';
838
839 case ':':
840 return L.tr(p2);
841
842 case '=':
843 return L.globals[p2] || '';
844
845 default:
846 return '(?' + match + ')';
847 }
848 });
849
850 $('#maincontent').append(data);
851 }
852 });
853 },
854
855 execute: function()
856 {
857 throw "Not implemented";
858 },
859
860 render: function()
861 {
862 var container = $('#maincontent');
863
864 container.empty();
865
866 if (this.title)
867 container.append($('<h2 />').append(this.title));
868
869 if (this.description)
870 container.append($('<p />').append(this.description));
871
872 var self = this;
873 var args = [ ];
874
875 for (var i = 0; i < arguments.length; i++)
876 args.push(arguments[i]);
877
878 return this._fetch_template().then(function() {
879 return L.deferrable(self.execute.apply(self, args));
880 });
881 },
882
883 repeat: function(func, interval)
884 {
885 var self = this;
886
887 if (!self._timeouts)
888 self._timeouts = [ ];
889
890 var index = self._timeouts.length;
891
892 if (typeof(interval) != 'number')
893 interval = 5000;
894
895 var setTimer, runTimer;
896
897 setTimer = function() {
898 if (self._timeouts)
899 self._timeouts[index] = window.setTimeout(runTimer, interval);
900 };
901
902 runTimer = function() {
903 L.deferrable(func.call(self)).then(setTimer, setTimer);
904 };
905
906 runTimer();
907 },
908
909 finish: function()
910 {
911 if ($.isArray(this._timeouts))
912 {
913 for (var i = 0; i < this._timeouts.length; i++)
914 window.clearTimeout(this._timeouts[i]);
915
916 delete this._timeouts;
917 }
918 }
919 });
920
921 ui_class.menu = ui_class.AbstractWidget.extend({
922 init: function() {
923 this._nodes = { };
924 },
925
926 entries: function(entries)
927 {
928 for (var entry in entries)
929 {
930 var path = entry.split(/\//);
931 var node = this._nodes;
932
933 for (i = 0; i < path.length; i++)
934 {
935 if (!node.childs)
936 node.childs = { };
937
938 if (!node.childs[path[i]])
939 node.childs[path[i]] = { };
940
941 node = node.childs[path[i]];
942 }
943
944 $.extend(node, entries[entry]);
945 }
946 },
947
948 sortNodesCallback: function(a, b)
949 {
950 var x = a.index || 0;
951 var y = b.index || 0;
952 return (x - y);
953 },
954
955 firstChildView: function(node)
956 {
957 if (node.view)
958 return node;
959
960 var nodes = [ ];
961 for (var child in (node.childs || { }))
962 nodes.push(node.childs[child]);
963
964 nodes.sort(this.sortNodesCallback);
965
966 for (var i = 0; i < nodes.length; i++)
967 {
968 var child = this.firstChildView(nodes[i]);
969 if (child)
970 {
971 for (var key in child)
972 if (!node.hasOwnProperty(key) && child.hasOwnProperty(key))
973 node[key] = child[key];
974
975 return node;
976 }
977 }
978
979 return undefined;
980 },
981
982 handleClick: function(ev)
983 {
984 L.setHash('view', ev.data);
985
986 ev.preventDefault();
987 this.blur();
988 },
989
990 renderNodes: function(childs, level, min, max)
991 {
992 var nodes = [ ];
993 for (var node in childs)
994 {
995 var child = this.firstChildView(childs[node]);
996 if (child)
997 nodes.push(childs[node]);
998 }
999
1000 nodes.sort(this.sortNodesCallback);
1001
1002 var list = $('<ul />');
1003
1004 if (level == 0)
1005 list.addClass('nav').addClass('navbar-nav');
1006 else if (level == 1)
1007 list.addClass('dropdown-menu').addClass('navbar-inverse');
1008
1009 for (var i = 0; i < nodes.length; i++)
1010 {
1011 if (!L.globals.defaultNode)
1012 {
1013 var v = L.getHash('view');
1014 if (!v || v == nodes[i].view)
1015 L.globals.defaultNode = nodes[i];
1016 }
1017
1018 var item = $('<li />')
1019 .append($('<a />')
1020 .attr('href', '#')
1021 .text(L.tr(nodes[i].title)))
1022 .appendTo(list);
1023
1024 if (nodes[i].childs && level < max)
1025 {
1026 item.addClass('dropdown');
1027
1028 item.find('a')
1029 .addClass('dropdown-toggle')
1030 .attr('data-toggle', 'dropdown')
1031 .append('<b class="caret"></b>');
1032
1033 item.append(this.renderNodes(nodes[i].childs, level + 1));
1034 }
1035 else
1036 {
1037 item.find('a').click(nodes[i].view, this.handleClick);
1038 }
1039 }
1040
1041 return list.get(0);
1042 },
1043
1044 render: function(min, max)
1045 {
1046 var top = min ? this.getNode(L.globals.defaultNode.view, min) : this._nodes;
1047 return this.renderNodes(top.childs, 0, min, max);
1048 },
1049
1050 getNode: function(path, max)
1051 {
1052 var p = path.split(/\//);
1053 var n = this._nodes;
1054
1055 if (typeof(max) == 'undefined')
1056 max = p.length;
1057
1058 for (var i = 0; i < max; i++)
1059 {
1060 if (!n.childs[p[i]])
1061 return undefined;
1062
1063 n = n.childs[p[i]];
1064 }
1065
1066 return n;
1067 }
1068 });
1069
1070 ui_class.table = ui_class.AbstractWidget.extend({
1071 init: function()
1072 {
1073 this._rows = [ ];
1074 },
1075
1076 row: function(values)
1077 {
1078 if ($.isArray(values))
1079 {
1080 this._rows.push(values);
1081 }
1082 else if ($.isPlainObject(values))
1083 {
1084 var v = [ ];
1085 for (var i = 0; i < this.options.columns.length; i++)
1086 {
1087 var col = this.options.columns[i];
1088
1089 if (typeof col.key == 'string')
1090 v.push(values[col.key]);
1091 else
1092 v.push(null);
1093 }
1094 this._rows.push(v);
1095 }
1096 },
1097
1098 rows: function(rows)
1099 {
1100 for (var i = 0; i < rows.length; i++)
1101 this.row(rows[i]);
1102 },
1103
1104 render: function(id)
1105 {
1106 var fieldset = document.createElement('fieldset');
1107 fieldset.className = 'cbi-section';
1108
1109 if (this.options.caption)
1110 {
1111 var legend = document.createElement('legend');
1112 $(legend).append(this.options.caption);
1113 fieldset.appendChild(legend);
1114 }
1115
1116 var table = document.createElement('table');
1117 table.className = 'table table-condensed table-hover';
1118
1119 var has_caption = false;
1120 var has_description = false;
1121
1122 for (var i = 0; i < this.options.columns.length; i++)
1123 if (this.options.columns[i].caption)
1124 {
1125 has_caption = true;
1126 break;
1127 }
1128 else if (this.options.columns[i].description)
1129 {
1130 has_description = true;
1131 break;
1132 }
1133
1134 if (has_caption)
1135 {
1136 var tr = table.insertRow(-1);
1137 tr.className = 'cbi-section-table-titles';
1138
1139 for (var i = 0; i < this.options.columns.length; i++)
1140 {
1141 var col = this.options.columns[i];
1142 var th = document.createElement('th');
1143 th.className = 'cbi-section-table-cell';
1144
1145 tr.appendChild(th);
1146
1147 if (col.width)
1148 th.style.width = col.width;
1149
1150 if (col.align)
1151 th.style.textAlign = col.align;
1152
1153 if (col.caption)
1154 $(th).append(col.caption);
1155 }
1156 }
1157
1158 if (has_description)
1159 {
1160 var tr = table.insertRow(-1);
1161 tr.className = 'cbi-section-table-descr';
1162
1163 for (var i = 0; i < this.options.columns.length; i++)
1164 {
1165 var col = this.options.columns[i];
1166 var th = document.createElement('th');
1167 th.className = 'cbi-section-table-cell';
1168
1169 tr.appendChild(th);
1170
1171 if (col.width)
1172 th.style.width = col.width;
1173
1174 if (col.align)
1175 th.style.textAlign = col.align;
1176
1177 if (col.description)
1178 $(th).append(col.description);
1179 }
1180 }
1181
1182 if (this._rows.length == 0)
1183 {
1184 if (this.options.placeholder)
1185 {
1186 var tr = table.insertRow(-1);
1187 var td = tr.insertCell(-1);
1188 td.className = 'cbi-section-table-cell';
1189
1190 td.colSpan = this.options.columns.length;
1191 $(td).append(this.options.placeholder);
1192 }
1193 }
1194 else
1195 {
1196 for (var i = 0; i < this._rows.length; i++)
1197 {
1198 var tr = table.insertRow(-1);
1199
1200 for (var j = 0; j < this.options.columns.length; j++)
1201 {
1202 var col = this.options.columns[j];
1203 var td = tr.insertCell(-1);
1204
1205 var val = this._rows[i][j];
1206
1207 if (typeof(val) == 'undefined')
1208 val = col.placeholder;
1209
1210 if (typeof(val) == 'undefined')
1211 val = '';
1212
1213 if (col.width)
1214 td.style.width = col.width;
1215
1216 if (col.align)
1217 td.style.textAlign = col.align;
1218
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));
1223 else
1224 $(td).append(val);
1225 }
1226 }
1227 }
1228
1229 this._rows = [ ];
1230 fieldset.appendChild(table);
1231
1232 return fieldset;
1233 }
1234 });
1235
1236 ui_class.progress = ui_class.AbstractWidget.extend({
1237 render: function()
1238 {
1239 var vn = parseInt(this.options.value) || 0;
1240 var mn = parseInt(this.options.max) || 100;
1241 var pc = Math.floor((100 / mn) * vn);
1242
1243 var text;
1244
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);
1249 else
1250 text = '%.2f%%'.format(pc);
1251
1252 return $('<div />')
1253 .addClass('progress')
1254 .append($('<div />')
1255 .addClass('progress-bar')
1256 .addClass('progress-bar-info')
1257 .css('width', pc + '%'))
1258 .append($('<small />')
1259 .text(text));
1260 }
1261 });
1262
1263 ui_class.devicebadge = ui_class.AbstractWidget.extend({
1264 render: function()
1265 {
1266 var l2dev = this.options.l2_device || this.options.device;
1267 var l3dev = this.options.l3_device;
1268 var dev = l3dev || l2dev || '?';
1269
1270 var span = document.createElement('span');
1271 span.className = 'badge';
1272
1273 if (typeof(this.options.signal) == 'number' ||
1274 typeof(this.options.noise) == 'number')
1275 {
1276 var r = 'none';
1277 if (typeof(this.options.signal) != 'undefined' &&
1278 typeof(this.options.noise) != 'undefined')
1279 {
1280 var q = (-1 * (this.options.noise - this.options.signal)) / 5;
1281 if (q < 1)
1282 r = '0';
1283 else if (q < 2)
1284 r = '0-25';
1285 else if (q < 3)
1286 r = '25-50';
1287 else if (q < 4)
1288 r = '50-75';
1289 else
1290 r = '75-100';
1291 }
1292
1293 span.appendChild(document.createElement('img'));
1294 span.lastChild.src = L.globals.resource + '/icons/signal-' + r + '.png';
1295
1296 if (r == 'none')
1297 span.title = L.tr('No signal');
1298 else
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')
1302 );
1303 }
1304 else
1305 {
1306 var type = 'ethernet';
1307 var desc = L.tr('Ethernet device');
1308
1309 if (l3dev != l2dev)
1310 {
1311 type = 'tunnel';
1312 desc = L.tr('Tunnel interface');
1313 }
1314 else if (dev.indexOf('br-') == 0)
1315 {
1316 type = 'bridge';
1317 desc = L.tr('Bridge');
1318 }
1319 else if (dev.indexOf('.') > 0)
1320 {
1321 type = 'vlan';
1322 desc = L.tr('VLAN interface');
1323 }
1324 else if (dev.indexOf('wlan') == 0 ||
1325 dev.indexOf('ath') == 0 ||
1326 dev.indexOf('wl') == 0)
1327 {
1328 type = 'wifi';
1329 desc = L.tr('Wireless Network');
1330 }
1331
1332 span.appendChild(document.createElement('img'));
1333 span.lastChild.src = L.globals.resource + '/icons/' + type + (this.options.up ? '' : '_disabled') + '.png';
1334 span.title = desc;
1335 }
1336
1337 $(span).append(' ');
1338 $(span).append(dev);
1339
1340 return span;
1341 }
1342 });
1343
1344 return Class.extend(ui_class);
1345 })();