5ab1ddd0bb630243f8204a517af9e69b795474fd
[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
739 ui_class.AbstractWidget = Class.extend({
740 i18n: function(text) {
741 return text;
742 },
743
744 label: function() {
745 var key = arguments[0];
746 var args = [ ];
747
748 for (var i = 1; i < arguments.length; i++)
749 args.push(arguments[i]);
750
751 switch (typeof(this.options[key]))
752 {
753 case 'undefined':
754 return '';
755
756 case 'function':
757 return this.options[key].apply(this, args);
758
759 default:
760 return ''.format.apply('' + this.options[key], args);
761 }
762 },
763
764 toString: function() {
765 return $('<div />').append(this.render()).html();
766 },
767
768 insertInto: function(id) {
769 return $(id).empty().append(this.render());
770 },
771
772 appendTo: function(id) {
773 return $(id).append(this.render());
774 },
775
776 on: function(evname, evfunc)
777 {
778 var evnames = L.toArray(evname);
779
780 if (!this.events)
781 this.events = { };
782
783 for (var i = 0; i < evnames.length; i++)
784 this.events[evnames[i]] = evfunc;
785
786 return this;
787 },
788
789 trigger: function(evname, evdata)
790 {
791 if (this.events)
792 {
793 var evnames = L.toArray(evname);
794
795 for (var i = 0; i < evnames.length; i++)
796 if (this.events[evnames[i]])
797 this.events[evnames[i]].call(this, evdata);
798 }
799
800 return this;
801 }
802 });
803
804 ui_class.view = ui_class.AbstractWidget.extend({
805 _fetch_template: function()
806 {
807 return $.ajax(L.globals.resource + '/template/' + this.options.name + '.htm', {
808 method: 'GET',
809 cache: true,
810 dataType: 'text',
811 success: function(data) {
812 data = data.replace(/<%([#:=])?(.+?)%>/g, function(match, p1, p2) {
813 p2 = p2.replace(/^\s+/, '').replace(/\s+$/, '');
814 switch (p1)
815 {
816 case '#':
817 return '';
818
819 case ':':
820 return L.tr(p2);
821
822 case '=':
823 return L.globals[p2] || '';
824
825 default:
826 return '(?' + match + ')';
827 }
828 });
829
830 $('#maincontent').append(data);
831 }
832 });
833 },
834
835 execute: function()
836 {
837 throw "Not implemented";
838 },
839
840 render: function()
841 {
842 var container = $('#maincontent');
843
844 container.empty();
845
846 if (this.title)
847 container.append($('<h2 />').append(this.title));
848
849 if (this.description)
850 container.append($('<p />').append(this.description));
851
852 var self = this;
853 var args = [ ];
854
855 for (var i = 0; i < arguments.length; i++)
856 args.push(arguments[i]);
857
858 return this._fetch_template().then(function() {
859 return L.deferrable(self.execute.apply(self, args));
860 });
861 },
862
863 repeat: function(func, interval)
864 {
865 var self = this;
866
867 if (!self._timeouts)
868 self._timeouts = [ ];
869
870 var index = self._timeouts.length;
871
872 if (typeof(interval) != 'number')
873 interval = 5000;
874
875 var setTimer, runTimer;
876
877 setTimer = function() {
878 if (self._timeouts)
879 self._timeouts[index] = window.setTimeout(runTimer, interval);
880 };
881
882 runTimer = function() {
883 L.deferrable(func.call(self)).then(setTimer, setTimer);
884 };
885
886 runTimer();
887 },
888
889 finish: function()
890 {
891 if ($.isArray(this._timeouts))
892 {
893 for (var i = 0; i < this._timeouts.length; i++)
894 window.clearTimeout(this._timeouts[i]);
895
896 delete this._timeouts;
897 }
898 }
899 });
900
901 ui_class.menu = ui_class.AbstractWidget.extend({
902 init: function() {
903 this._nodes = { };
904 },
905
906 entries: function(entries)
907 {
908 for (var entry in entries)
909 {
910 var path = entry.split(/\//);
911 var node = this._nodes;
912
913 for (i = 0; i < path.length; i++)
914 {
915 if (!node.childs)
916 node.childs = { };
917
918 if (!node.childs[path[i]])
919 node.childs[path[i]] = { };
920
921 node = node.childs[path[i]];
922 }
923
924 $.extend(node, entries[entry]);
925 }
926 },
927
928 sortNodesCallback: function(a, b)
929 {
930 var x = a.index || 0;
931 var y = b.index || 0;
932 return (x - y);
933 },
934
935 firstChildView: function(node)
936 {
937 if (node.view)
938 return node;
939
940 var nodes = [ ];
941 for (var child in (node.childs || { }))
942 nodes.push(node.childs[child]);
943
944 nodes.sort(this.sortNodesCallback);
945
946 for (var i = 0; i < nodes.length; i++)
947 {
948 var child = this.firstChildView(nodes[i]);
949 if (child)
950 {
951 for (var key in child)
952 if (!node.hasOwnProperty(key) && child.hasOwnProperty(key))
953 node[key] = child[key];
954
955 return node;
956 }
957 }
958
959 return undefined;
960 },
961
962 handleClick: function(ev)
963 {
964 L.setHash('view', ev.data);
965
966 ev.preventDefault();
967 this.blur();
968 },
969
970 renderNodes: function(childs, level, min, max)
971 {
972 var nodes = [ ];
973 for (var node in childs)
974 {
975 var child = this.firstChildView(childs[node]);
976 if (child)
977 nodes.push(childs[node]);
978 }
979
980 nodes.sort(this.sortNodesCallback);
981
982 var list = $('<ul />');
983
984 if (level == 0)
985 list.addClass('nav').addClass('navbar-nav');
986 else if (level == 1)
987 list.addClass('dropdown-menu').addClass('navbar-inverse');
988
989 for (var i = 0; i < nodes.length; i++)
990 {
991 if (!L.globals.defaultNode)
992 {
993 var v = L.getHash('view');
994 if (!v || v == nodes[i].view)
995 L.globals.defaultNode = nodes[i];
996 }
997
998 var item = $('<li />')
999 .append($('<a />')
1000 .attr('href', '#')
1001 .text(L.tr(nodes[i].title)))
1002 .appendTo(list);
1003
1004 if (nodes[i].childs && level < max)
1005 {
1006 item.addClass('dropdown');
1007
1008 item.find('a')
1009 .addClass('dropdown-toggle')
1010 .attr('data-toggle', 'dropdown')
1011 .append('<b class="caret"></b>');
1012
1013 item.append(this.renderNodes(nodes[i].childs, level + 1));
1014 }
1015 else
1016 {
1017 item.find('a').click(nodes[i].view, this.handleClick);
1018 }
1019 }
1020
1021 return list.get(0);
1022 },
1023
1024 render: function(min, max)
1025 {
1026 var top = min ? this.getNode(L.globals.defaultNode.view, min) : this._nodes;
1027 return this.renderNodes(top.childs, 0, min, max);
1028 },
1029
1030 getNode: function(path, max)
1031 {
1032 var p = path.split(/\//);
1033 var n = this._nodes;
1034
1035 if (typeof(max) == 'undefined')
1036 max = p.length;
1037
1038 for (var i = 0; i < max; i++)
1039 {
1040 if (!n.childs[p[i]])
1041 return undefined;
1042
1043 n = n.childs[p[i]];
1044 }
1045
1046 return n;
1047 }
1048 });
1049
1050 ui_class.table = ui_class.AbstractWidget.extend({
1051 init: function()
1052 {
1053 this._rows = [ ];
1054 },
1055
1056 row: function(values)
1057 {
1058 if ($.isArray(values))
1059 {
1060 this._rows.push(values);
1061 }
1062 else if ($.isPlainObject(values))
1063 {
1064 var v = [ ];
1065 for (var i = 0; i < this.options.columns.length; i++)
1066 {
1067 var col = this.options.columns[i];
1068
1069 if (typeof col.key == 'string')
1070 v.push(values[col.key]);
1071 else
1072 v.push(null);
1073 }
1074 this._rows.push(v);
1075 }
1076 },
1077
1078 rows: function(rows)
1079 {
1080 for (var i = 0; i < rows.length; i++)
1081 this.row(rows[i]);
1082 },
1083
1084 render: function(id)
1085 {
1086 var fieldset = document.createElement('fieldset');
1087 fieldset.className = 'cbi-section';
1088
1089 if (this.options.caption)
1090 {
1091 var legend = document.createElement('legend');
1092 $(legend).append(this.options.caption);
1093 fieldset.appendChild(legend);
1094 }
1095
1096 var table = document.createElement('table');
1097 table.className = 'table table-condensed table-hover';
1098
1099 var has_caption = false;
1100 var has_description = false;
1101
1102 for (var i = 0; i < this.options.columns.length; i++)
1103 if (this.options.columns[i].caption)
1104 {
1105 has_caption = true;
1106 break;
1107 }
1108 else if (this.options.columns[i].description)
1109 {
1110 has_description = true;
1111 break;
1112 }
1113
1114 if (has_caption)
1115 {
1116 var tr = table.insertRow(-1);
1117 tr.className = 'cbi-section-table-titles';
1118
1119 for (var i = 0; i < this.options.columns.length; i++)
1120 {
1121 var col = this.options.columns[i];
1122 var th = document.createElement('th');
1123 th.className = 'cbi-section-table-cell';
1124
1125 tr.appendChild(th);
1126
1127 if (col.width)
1128 th.style.width = col.width;
1129
1130 if (col.align)
1131 th.style.textAlign = col.align;
1132
1133 if (col.caption)
1134 $(th).append(col.caption);
1135 }
1136 }
1137
1138 if (has_description)
1139 {
1140 var tr = table.insertRow(-1);
1141 tr.className = 'cbi-section-table-descr';
1142
1143 for (var i = 0; i < this.options.columns.length; i++)
1144 {
1145 var col = this.options.columns[i];
1146 var th = document.createElement('th');
1147 th.className = 'cbi-section-table-cell';
1148
1149 tr.appendChild(th);
1150
1151 if (col.width)
1152 th.style.width = col.width;
1153
1154 if (col.align)
1155 th.style.textAlign = col.align;
1156
1157 if (col.description)
1158 $(th).append(col.description);
1159 }
1160 }
1161
1162 if (this._rows.length == 0)
1163 {
1164 if (this.options.placeholder)
1165 {
1166 var tr = table.insertRow(-1);
1167 var td = tr.insertCell(-1);
1168 td.className = 'cbi-section-table-cell';
1169
1170 td.colSpan = this.options.columns.length;
1171 $(td).append(this.options.placeholder);
1172 }
1173 }
1174 else
1175 {
1176 for (var i = 0; i < this._rows.length; i++)
1177 {
1178 var tr = table.insertRow(-1);
1179
1180 for (var j = 0; j < this.options.columns.length; j++)
1181 {
1182 var col = this.options.columns[j];
1183 var td = tr.insertCell(-1);
1184
1185 var val = this._rows[i][j];
1186
1187 if (typeof(val) == 'undefined')
1188 val = col.placeholder;
1189
1190 if (typeof(val) == 'undefined')
1191 val = '';
1192
1193 if (col.width)
1194 td.style.width = col.width;
1195
1196 if (col.align)
1197 td.style.textAlign = col.align;
1198
1199 if (typeof col.format == 'string')
1200 $(td).append(col.format.format(val));
1201 else if (typeof col.format == 'function')
1202 $(td).append(col.format(val, i));
1203 else
1204 $(td).append(val);
1205 }
1206 }
1207 }
1208
1209 this._rows = [ ];
1210 fieldset.appendChild(table);
1211
1212 return fieldset;
1213 }
1214 });
1215
1216 ui_class.progress = ui_class.AbstractWidget.extend({
1217 render: function()
1218 {
1219 var vn = parseInt(this.options.value) || 0;
1220 var mn = parseInt(this.options.max) || 100;
1221 var pc = Math.floor((100 / mn) * vn);
1222
1223 var text;
1224
1225 if (typeof(this.options.format) == 'string')
1226 text = this.options.format.format(this.options.value, this.options.max, pc);
1227 else if (typeof(this.options.format) == 'function')
1228 text = this.options.format(pc);
1229 else
1230 text = '%.2f%%'.format(pc);
1231
1232 return $('<div />')
1233 .addClass('progress')
1234 .append($('<div />')
1235 .addClass('progress-bar')
1236 .addClass('progress-bar-info')
1237 .css('width', pc + '%'))
1238 .append($('<small />')
1239 .text(text));
1240 }
1241 });
1242
1243 ui_class.devicebadge = ui_class.AbstractWidget.extend({
1244 render: function()
1245 {
1246 var l2dev = this.options.l2_device || this.options.device;
1247 var l3dev = this.options.l3_device;
1248 var dev = l3dev || l2dev || '?';
1249
1250 var span = document.createElement('span');
1251 span.className = 'badge';
1252
1253 if (typeof(this.options.signal) == 'number' ||
1254 typeof(this.options.noise) == 'number')
1255 {
1256 var r = 'none';
1257 if (typeof(this.options.signal) != 'undefined' &&
1258 typeof(this.options.noise) != 'undefined')
1259 {
1260 var q = (-1 * (this.options.noise - this.options.signal)) / 5;
1261 if (q < 1)
1262 r = '0';
1263 else if (q < 2)
1264 r = '0-25';
1265 else if (q < 3)
1266 r = '25-50';
1267 else if (q < 4)
1268 r = '50-75';
1269 else
1270 r = '75-100';
1271 }
1272
1273 span.appendChild(document.createElement('img'));
1274 span.lastChild.src = L.globals.resource + '/icons/signal-' + r + '.png';
1275
1276 if (r == 'none')
1277 span.title = L.tr('No signal');
1278 else
1279 span.title = '%s: %d %s / %s: %d %s'.format(
1280 L.tr('Signal'), this.options.signal, L.tr('dBm'),
1281 L.tr('Noise'), this.options.noise, L.tr('dBm')
1282 );
1283 }
1284 else
1285 {
1286 var type = 'ethernet';
1287 var desc = L.tr('Ethernet device');
1288
1289 if (l3dev != l2dev)
1290 {
1291 type = 'tunnel';
1292 desc = L.tr('Tunnel interface');
1293 }
1294 else if (dev.indexOf('br-') == 0)
1295 {
1296 type = 'bridge';
1297 desc = L.tr('Bridge');
1298 }
1299 else if (dev.indexOf('.') > 0)
1300 {
1301 type = 'vlan';
1302 desc = L.tr('VLAN interface');
1303 }
1304 else if (dev.indexOf('wlan') == 0 ||
1305 dev.indexOf('ath') == 0 ||
1306 dev.indexOf('wl') == 0)
1307 {
1308 type = 'wifi';
1309 desc = L.tr('Wireless Network');
1310 }
1311
1312 span.appendChild(document.createElement('img'));
1313 span.lastChild.src = L.globals.resource + '/icons/' + type + (this.options.up ? '' : '_disabled') + '.png';
1314 span.title = desc;
1315 }
1316
1317 $(span).append(' ');
1318 $(span).append(dev);
1319
1320 return span;
1321 }
1322 });
1323
1324 return Class.extend(ui_class);
1325 })();