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