Merge pull request #1735 from sumpfralle/olsr-jsoninfo-parser-handle-empty-result
[oweals/luci.git] / modules / luci-base / htdocs / luci-static / resources / tools / widgets.js
1 'use strict';
2 'require ui';
3 'require form';
4 'require network';
5 'require firewall';
6
7 var CBIZoneSelect = form.ListValue.extend({
8         __name__: 'CBI.ZoneSelect',
9
10         load: function(section_id) {
11                 return Promise.all([ firewall.getZones(), network.getNetworks() ]).then(L.bind(function(zn) {
12                         this.zones = zn[0];
13                         this.networks = zn[1];
14
15                         return this.super('load', section_id);
16                 }, this));
17         },
18
19         filter: function(section_id, value) {
20                 return true;
21         },
22
23         lookupZone: function(name) {
24                 return this.zones.filter(function(zone) { return zone.getName() == name })[0];
25         },
26
27         lookupNetwork: function(name) {
28                 return this.networks.filter(function(network) { return network.getName() == name })[0];
29         },
30
31         renderWidget: function(section_id, option_index, cfgvalue) {
32                 var values = L.toArray((cfgvalue != null) ? cfgvalue : this.default),
33                     isOutputOnly = false,
34                     choices = {};
35
36                 if (this.option == 'dest') {
37                         for (var i = 0; i < this.section.children.length; i++) {
38                                 var opt = this.section.children[i];
39                                 if (opt.option == 'src') {
40                                         var val = opt.cfgvalue(section_id) || opt.default;
41                                         isOutputOnly = (val == null || val == '');
42                                         break;
43                                 }
44                         }
45
46                         this.title = isOutputOnly ? _('Output zone') :  _('Destination zone');
47                 }
48
49                 if (this.allowlocal) {
50                         choices[''] = E('span', {
51                                 'class': 'zonebadge',
52                                 'style': 'background-color:' + firewall.getColorForName(null)
53                         }, [
54                                 E('strong', _('Device')),
55                                 (this.allowany || this.allowlocal)
56                                         ? ' (%s)'.format(this.option != 'dest' ? _('output') : _('input')) : ''
57                         ]);
58                 }
59                 else if (!this.multiple && (this.rmempty || this.optional)) {
60                         choices[''] = E('span', {
61                                 'class': 'zonebadge',
62                                 'style': 'background-color:' + firewall.getColorForName(null)
63                         }, E('em', _('unspecified')));
64                 }
65
66                 if (this.allowany) {
67                         choices['*'] = E('span', {
68                                 'class': 'zonebadge',
69                                 'style': 'background-color:' + firewall.getColorForName(null)
70                         }, [
71                                 E('strong', _('Any zone')),
72                                 (this.allowany && this.allowlocal && !isOutputOnly) ? ' (%s)'.format(_('forward')) : ''
73                         ]);
74                 }
75
76                 for (var i = 0; i < this.zones.length; i++) {
77                         var zone = this.zones[i],
78                             name = zone.getName(),
79                             networks = zone.getNetworks(),
80                             ifaces = [];
81
82                         if (!this.filter(section_id, name))
83                                 continue;
84
85                         for (var j = 0; j < networks.length; j++) {
86                                 var network = this.lookupNetwork(networks[j]);
87
88                                 if (!network)
89                                         continue;
90
91                                 var span = E('span', {
92                                         'class': 'ifacebadge' + (network.getName() == this.network ? ' ifacebadge-active' : '')
93                                 }, network.getName() + ': ');
94
95                                 var devices = network.isBridge() ? network.getDevices() : L.toArray(network.getDevice());
96
97                                 for (var k = 0; k < devices.length; k++) {
98                                         span.appendChild(E('img', {
99                                                 'title': devices[k].getI18n(),
100                                                 'src': L.resource('icons/%s%s.png'.format(devices[k].getType(), devices[k].isUp() ? '' : '_disabled'))
101                                         }));
102                                 }
103
104                                 if (!devices.length)
105                                         span.appendChild(E('em', _('(empty)')));
106
107                                 ifaces.push(span);
108                         }
109
110                         if (!ifaces.length)
111                                 ifaces.push(E('em', _('(empty)')));
112
113                         choices[name] = E('span', {
114                                 'class': 'zonebadge',
115                                 'style': 'background-color:' + zone.getColor()
116                         }, [ E('strong', name) ].concat(ifaces));
117                 }
118
119                 var widget = new ui.Dropdown(values, choices, {
120                         id: this.cbid(section_id),
121                         sort: true,
122                         multiple: this.multiple,
123                         optional: this.optional || this.rmempty,
124                         select_placeholder: E('em', _('unspecified')),
125                         display_items: this.display_size || this.size || 3,
126                         dropdown_items: this.dropdown_size || this.size || 5,
127                         validate: L.bind(this.validate, this, section_id),
128                         create: !this.nocreate,
129                         create_markup: '' +
130                                 '<li data-value="{{value}}">' +
131                                         '<span class="zonebadge" style="background:repeating-linear-gradient(45deg,rgba(204,204,204,0.5),rgba(204,204,204,0.5) 5px,rgba(255,255,255,0.5) 5px,rgba(255,255,255,0.5) 10px)">' +
132                                                 '<strong>{{value}}:</strong> <em>('+_('create')+')</em>' +
133                                         '</span>' +
134                                 '</li>'
135                 });
136
137                 var elem = widget.render();
138
139                 if (this.option == 'src') {
140                         elem.addEventListener('cbi-dropdown-change', L.bind(function(ev) {
141                                 var opt = this.map.lookupOption('dest', section_id),
142                                     val = ev.detail.instance.getValue();
143
144                                 if (opt == null)
145                                         return;
146
147                                 var cbid = opt[0].cbid(section_id),
148                                     label = document.querySelector('label[for="widget.%s"]'.format(cbid)),
149                                     node = document.getElementById(cbid);
150
151                                 L.dom.content(label, val == '' ? _('Output zone') : _('Destination zone'));
152
153                                 if (val == '') {
154                                         if (L.dom.callClassMethod(node, 'getValue') == '')
155                                                 L.dom.callClassMethod(node, 'setValue', '*');
156
157                                         var emptyval = node.querySelector('[data-value=""]'),
158                                             anyval = node.querySelector('[data-value="*"]');
159
160                                         L.dom.content(anyval.querySelector('span'), E('strong', _('Any zone')));
161
162                                         if (emptyval != null)
163                                                 emptyval.parentNode.removeChild(emptyval);
164                                 }
165                                 else {
166                                         var anyval = node.querySelector('[data-value="*"]'),
167                                             emptyval = node.querySelector('[data-value=""]');
168
169                                         if (emptyval == null) {
170                                                 emptyval = anyval.cloneNode(true);
171                                                 emptyval.removeAttribute('display');
172                                                 emptyval.removeAttribute('selected');
173                                                 emptyval.setAttribute('data-value', '');
174                                         }
175
176                                         L.dom.content(emptyval.querySelector('span'), [
177                                                 E('strong', _('Device')), ' (%s)'.format(_('input'))
178                                         ]);
179
180                                         L.dom.content(anyval.querySelector('span'), [
181                                                 E('strong', _('Any zone')), ' (%s)'.format(_('forward'))
182                                         ]);
183
184                                         anyval.parentNode.insertBefore(emptyval, anyval);
185                                 }
186
187                         }, this));
188                 }
189                 else if (isOutputOnly) {
190                         var emptyval = elem.querySelector('[data-value=""]');
191                         emptyval.parentNode.removeChild(emptyval);
192                 }
193
194                 return elem;
195         },
196 });
197
198 var CBIZoneForwards = form.DummyValue.extend({
199         __name__: 'CBI.ZoneForwards',
200
201         load: function(section_id) {
202                 return Promise.all([
203                         firewall.getDefaults(),
204                         firewall.getZones(),
205                         network.getNetworks(),
206                         network.getDevices()
207                 ]).then(L.bind(function(dznd) {
208                         this.defaults = dznd[0];
209                         this.zones = dznd[1];
210                         this.networks = dznd[2];
211                         this.devices = dznd[3];
212
213                         return this.super('load', section_id);
214                 }, this));
215         },
216
217         renderZone: function(zone) {
218                 var name = zone.getName(),
219                     networks = zone.getNetworks(),
220                     devices = zone.getDevices(),
221                     subnets = zone.getSubnets(),
222                     ifaces = [];
223
224                 for (var j = 0; j < networks.length; j++) {
225                         var network = this.networks.filter(function(net) { return net.getName() == networks[j] })[0];
226
227                         if (!network)
228                                 continue;
229
230                         var span = E('span', {
231                                 'class': 'ifacebadge' + (network.getName() == this.network ? ' ifacebadge-active' : '')
232                         }, network.getName() + ': ');
233
234                         var subdevs = network.isBridge() ? network.getDevices() : L.toArray(network.getDevice());
235
236                         for (var k = 0; k < subdevs.length && subdevs[k]; k++) {
237                                 span.appendChild(E('img', {
238                                         'title': subdevs[k].getI18n(),
239                                         'src': L.resource('icons/%s%s.png'.format(subdevs[k].getType(), subdevs[k].isUp() ? '' : '_disabled'))
240                                 }));
241                         }
242
243                         if (!subdevs.length)
244                                 span.appendChild(E('em', _('(empty)')));
245
246                         ifaces.push(span);
247                 }
248
249                 for (var i = 0; i < devices.length; i++) {
250                         var device = this.devices.filter(function(dev) { return dev.getName() == devices[i] })[0],
251                             title = device ? device.getI18n() : _('Absent Interface'),
252                             type = device ? device.getType() : 'ethernet',
253                             up = device ? device.isUp() : false;
254
255                         ifaces.push(E('span', { 'class': 'ifacebadge' }, [
256                                 E('img', {
257                                         'title': title,
258                                         'src': L.resource('icons/%s%s.png'.format(type, up ? '' : '_disabled'))
259                                 }),
260                                 device ? device.getName() : devices[i]
261                         ]));
262                 }
263
264                 if (subnets.length > 0)
265                         ifaces.push(E('span', { 'class': 'ifacebadge' }, [ '{ %s }'.format(subnets.join('; ')) ]));
266
267                 if (!ifaces.length)
268                         ifaces.push(E('span', { 'class': 'ifacebadge' }, E('em', _('(empty)'))));
269
270                 return E('label', {
271                         'class': 'zonebadge cbi-tooltip-container',
272                         'style': 'background-color:' + zone.getColor()
273                 }, [
274                         E('strong', name),
275                         E('div', { 'class': 'cbi-tooltip' }, ifaces)
276                 ]);
277         },
278
279         renderWidget: function(section_id, option_index, cfgvalue) {
280                 var value = (cfgvalue != null) ? cfgvalue : this.default,
281                     zone = this.zones.filter(function(z) { return z.getName() == value })[0];
282
283                 if (!zone)
284                         return E([]);
285
286                 var forwards = zone.getForwardingsBy('src'),
287                     dzones = [];
288
289                 for (var i = 0; i < forwards.length; i++) {
290                         var dzone = forwards[i].getDestinationZone();
291
292                         if (!dzone)
293                                 continue;
294
295                         dzones.push(this.renderZone(dzone));
296                 }
297
298                 if (!dzones.length)
299                         dzones.push(E('label', { 'class': 'zonebadge zonebadge-empty' },
300                                 E('strong', this.defaults.getForward())));
301
302                 return E('div', { 'class': 'zone-forwards' }, [
303                         E('div', { 'class': 'zone-src' }, this.renderZone(zone)),
304                         E('span', '⇒'),
305                         E('div', { 'class': 'zone-dest' }, dzones)
306                 ]);
307         },
308 });
309
310 var CBINetworkSelect = form.ListValue.extend({
311         __name__: 'CBI.NetworkSelect',
312
313         load: function(section_id) {
314                 return network.getNetworks().then(L.bind(function(networks) {
315                         this.networks = networks;
316
317                         return this.super('load', section_id);
318                 }, this));
319         },
320
321         filter: function(section_id, value) {
322                 return true;
323         },
324
325         renderIfaceBadge: function(network) {
326                 var span = E('span', { 'class': 'ifacebadge' }, network.getName() + ': '),
327                     devices = network.isBridge() ? network.getDevices() : L.toArray(network.getDevice());
328
329                 for (var j = 0; j < devices.length && devices[j]; j++) {
330                         span.appendChild(E('img', {
331                                 'title': devices[j].getI18n(),
332                                 'src': L.resource('icons/%s%s.png'.format(devices[j].getType(), devices[j].isUp() ? '' : '_disabled'))
333                         }));
334                 }
335
336                 if (!devices.length) {
337                         span.appendChild(E('em', { 'class': 'hide-close' }, _('(no interfaces attached)')));
338                         span.appendChild(E('em', { 'class': 'hide-open' }, '-'));
339                 }
340
341                 return span;
342         },
343
344         renderWidget: function(section_id, option_index, cfgvalue) {
345                 var values = L.toArray((cfgvalue != null) ? cfgvalue : this.default),
346                     choices = {},
347                     checked = {};
348
349                 for (var i = 0; i < values.length; i++)
350                         checked[values[i]] = true;
351
352                 values = [];
353
354                 if (!this.multiple && (this.rmempty || this.optional))
355                         choices[''] = E('em', _('unspecified'));
356
357                 for (var i = 0; i < this.networks.length; i++) {
358                         var network = this.networks[i],
359                             name = network.getName();
360
361                         if (name == 'loopback' || name == this.exclude || !this.filter(section_id, name))
362                                 continue;
363
364                         if (this.novirtual && network.isVirtual())
365                                 continue;
366
367                         if (checked[name])
368                                 values.push(name);
369
370                         choices[name] = this.renderIfaceBadge(network);
371                 }
372
373                 var widget = new ui.Dropdown(this.multiple ? values : values[0], choices, {
374                         id: this.cbid(section_id),
375                         sort: true,
376                         multiple: this.multiple,
377                         optional: this.optional || this.rmempty,
378                         select_placeholder: E('em', _('unspecified')),
379                         display_items: this.display_size || this.size || 3,
380                         dropdown_items: this.dropdown_size || this.size || 5,
381                         validate: L.bind(this.validate, this, section_id),
382                         create: !this.nocreate,
383                         create_markup: '' +
384                                 '<li data-value="{{value}}">' +
385                                         '<span class="ifacebadge" style="background:repeating-linear-gradient(45deg,rgba(204,204,204,0.5),rgba(204,204,204,0.5) 5px,rgba(255,255,255,0.5) 5px,rgba(255,255,255,0.5) 10px)">' +
386                                                 '{{value}}: <em>('+_('create')+')</em>' +
387                                         '</span>' +
388                                 '</li>'
389                 });
390
391                 return widget.render();
392         },
393
394         textvalue: function(section_id) {
395                 var cfgvalue = this.cfgvalue(section_id),
396                     values = L.toArray((cfgvalue != null) ? cfgvalue : this.default),
397                     rv = E([]);
398
399                 for (var i = 0; i < (this.networks || []).length; i++) {
400                         var network = this.networks[i],
401                             name = network.getName();
402
403                         if (values.indexOf(name) == -1)
404                                 continue;
405
406                         if (rv.length)
407                                 L.dom.append(rv, ' ');
408
409                         L.dom.append(rv, this.renderIfaceBadge(network));
410                 }
411
412                 if (!rv.firstChild)
413                         rv.appendChild(E('em', _('unspecified')));
414
415                 return rv;
416         },
417 });
418
419 var CBIDeviceSelect = form.ListValue.extend({
420         __name__: 'CBI.DeviceSelect',
421
422         load: function(section_id) {
423                 return Promise.all([
424                         network.getDevices(),
425                         this.noaliases ? null : network.getNetworks()
426                 ]).then(L.bind(function(data) {
427                         this.devices = data[0];
428                         this.networks = data[1];
429
430                         return this.super('load', section_id);
431                 }, this));
432         },
433
434         filter: function(section_id, value) {
435                 return true;
436         },
437
438         renderWidget: function(section_id, option_index, cfgvalue) {
439                 var values = L.toArray((cfgvalue != null) ? cfgvalue : this.default),
440                     choices = {},
441                     checked = {},
442                     order = [];
443
444                 for (var i = 0; i < values.length; i++)
445                         checked[values[i]] = true;
446
447                 values = [];
448
449                 if (!this.multiple && (this.rmempty || this.optional))
450                         choices[''] = E('em', _('unspecified'));
451
452                 for (var i = 0; i < this.devices.length; i++) {
453                         var device = this.devices[i],
454                             name = device.getName(),
455                             type = device.getType();
456
457                         if (name == 'lo' || name == this.exclude || !this.filter(section_id, name))
458                                 continue;
459
460                         if (this.noaliases && type == 'alias')
461                                 continue;
462
463                         if (this.nobridges && type == 'bridge')
464                                 continue;
465
466                         if (this.noinactive && device.isUp() == false)
467                                 continue;
468
469                         var item = E([
470                                 E('img', {
471                                         'title': device.getI18n(),
472                                         'src': L.resource('icons/%s%s.png'.format(type, device.isUp() ? '' : '_disabled'))
473                                 }),
474                                 E('span', { 'class': 'hide-open' }, [ name ]),
475                                 E('span', { 'class': 'hide-close'}, [ device.getI18n() ])
476                         ]);
477
478                         var networks = device.getNetworks();
479
480                         if (networks.length > 0)
481                                 L.dom.append(item.lastChild, [ ' (', networks.map(function(n) { return n.getName() }).join(', '), ')' ]);
482
483                         if (checked[name])
484                                 values.push(name);
485
486                         choices[name] = item;
487                         order.push(name);
488                 }
489
490                 if (this.networks != null) {
491                         for (var i = 0; i < this.networks.length; i++) {
492                                 var net = this.networks[i],
493                                     device = network.instantiateDevice('@%s'.format(net.getName()), net),
494                                     name = device.getName();
495
496                                 if (name == '@loopback' || name == this.exclude || !this.filter(section_id, name))
497                                         continue;
498
499                                 if (this.noinactive && net.isUp() == false)
500                                         continue;
501
502                                 var item = E([
503                                         E('img', {
504                                                 'title': device.getI18n(),
505                                                 'src': L.resource('icons/alias%s.png'.format(net.isUp() ? '' : '_disabled'))
506                                         }),
507                                         E('span', { 'class': 'hide-open' }, [ name ]),
508                                         E('span', { 'class': 'hide-close'}, [ device.getI18n() ])
509                                 ]);
510
511                                 if (checked[name])
512                                         values.push(name);
513
514                                 choices[name] = item;
515                                 order.push(name);
516                         }
517                 }
518
519                 if (!this.nocreate) {
520                         var keys = Object.keys(checked).sort();
521
522                         for (var i = 0; i < keys.length; i++) {
523                                 if (choices.hasOwnProperty(keys[i]))
524                                         continue;
525
526                                 choices[keys[i]] = E([
527                                         E('img', {
528                                                 'title': _('Absent Interface'),
529                                                 'src': L.resource('icons/ethernet_disabled.png')
530                                         }),
531                                         E('span', { 'class': 'hide-open' }, [ keys[i] ]),
532                                         E('span', { 'class': 'hide-close'}, [ '%s: "%h"'.format(_('Absent Interface'), keys[i]) ])
533                                 ]);
534
535                                 values.push(keys[i]);
536                                 order.push(keys[i]);
537                         }
538                 }
539
540                 var widget = new ui.Dropdown(this.multiple ? values : values[0], choices, {
541                         id: this.cbid(section_id),
542                         sort: order,
543                         multiple: this.multiple,
544                         optional: this.optional || this.rmempty,
545                         select_placeholder: E('em', _('unspecified')),
546                         display_items: this.display_size || this.size || 3,
547                         dropdown_items: this.dropdown_size || this.size || 5,
548                         validate: L.bind(this.validate, this, section_id),
549                         create: !this.nocreate,
550                         create_markup: '' +
551                                 '<li data-value="{{value}}">' +
552                                         '<img title="'+_('Custom Interface')+': &quot;{{value}}&quot;" src="'+L.resource('icons/ethernet_disabled.png')+'" />' +
553                                         '<span class="hide-open">{{value}}</span>' +
554                                         '<span class="hide-close">'+_('Custom Interface')+': "{{value}}"</span>' +
555                                 '</li>'
556                 });
557
558                 return widget.render();
559         },
560 });
561
562
563 return L.Class.extend({
564         ZoneSelect: CBIZoneSelect,
565         ZoneForwards: CBIZoneForwards,
566         NetworkSelect: CBINetworkSelect,
567         DeviceSelect: CBIDeviceSelect,
568 });