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