1fb9a4947c0c8b4413ded5b4769a69c4893692a4
[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 })();