luci-base: widgets.js: CBIZoneSelect: improve initial rendering
[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([ firewall.getDefaults(), firewall.getZones(), network.getNetworks() ]).then(L.bind(function(dzn) {
203                         this.defaults = dzn[0];
204                         this.zones = dzn[1];
205                         this.networks = dzn[2];
206
207                         return this.super('load', section_id);
208                 }, this));
209         },
210
211         renderZone: function(zone) {
212                 var name = zone.getName(),
213                     networks = zone.getNetworks(),
214                     ifaces = [];
215
216                 for (var j = 0; j < networks.length; j++) {
217                         var network = this.networks.filter(function(net) { return net.getName() == networks[j] })[0];
218
219                         if (!network)
220                                 continue;
221
222                         var span = E('span', {
223                                 'class': 'ifacebadge' + (network.getName() == this.network ? ' ifacebadge-active' : '')
224                         }, network.getName() + ': ');
225
226                         var devices = network.isBridge() ? network.getDevices() : L.toArray(network.getDevice());
227
228                         for (var k = 0; k < devices.length && devices[k]; k++) {
229                                 span.appendChild(E('img', {
230                                         'title': devices[k].getI18n(),
231                                         'src': L.resource('icons/%s%s.png'.format(devices[k].getType(), devices[k].isUp() ? '' : '_disabled'))
232                                 }));
233                         }
234
235                         if (!devices.length)
236                                 span.appendChild(E('em', _('(empty)')));
237
238                         ifaces.push(span);
239                 }
240
241                 if (!ifaces.length)
242                         ifaces.push(E('span', { 'class': 'ifacebadge' }, E('em', _('(empty)'))));
243
244                 return E('label', {
245                         'class': 'zonebadge cbi-tooltip-container',
246                         'style': 'background-color:' + zone.getColor()
247                 }, [
248                         E('strong', name),
249                         E('div', { 'class': 'cbi-tooltip' }, ifaces)
250                 ]);
251         },
252
253         renderWidget: function(section_id, option_index, cfgvalue) {
254                 var value = (cfgvalue != null) ? cfgvalue : this.default,
255                     zone = this.zones.filter(function(z) { return z.getName() == value })[0];
256
257                 if (!zone)
258                         return E([]);
259
260                 var forwards = zone.getForwardingsBy('src'),
261                     dzones = [];
262
263                 for (var i = 0; i < forwards.length; i++) {
264                         var dzone = forwards[i].getDestinationZone();
265
266                         if (!dzone)
267                                 continue;
268
269                         dzones.push(this.renderZone(dzone));
270                 }
271
272                 if (!dzones.length)
273                         dzones.push(E('label', { 'class': 'zonebadge zonebadge-empty' },
274                                 E('strong', this.defaults.getForward())));
275
276                 return E('div', { 'class': 'zone-forwards' }, [
277                         E('div', { 'class': 'zone-src' }, this.renderZone(zone)),
278                         E('span', '⇒'),
279                         E('div', { 'class': 'zone-dest' }, dzones)
280                 ]);
281         },
282 });
283
284 var CBINetworkSelect = form.ListValue.extend({
285         __name__: 'CBI.NetworkSelect',
286
287         load: function(section_id) {
288                 return network.getNetworks().then(L.bind(function(networks) {
289                         this.networks = networks;
290
291                         return this.super('load', section_id);
292                 }, this));
293         },
294
295         filter: function(section_id, value) {
296                 return true;
297         },
298
299         renderIfaceBadge: function(network) {
300                 var span = E('span', { 'class': 'ifacebadge' }, network.getName() + ': '),
301                     devices = network.isBridge() ? network.getDevices() : L.toArray(network.getDevice());
302
303                 for (var j = 0; j < devices.length && devices[j]; j++) {
304                         span.appendChild(E('img', {
305                                 'title': devices[j].getI18n(),
306                                 'src': L.resource('icons/%s%s.png'.format(devices[j].getType(), devices[j].isUp() ? '' : '_disabled'))
307                         }));
308                 }
309
310                 if (!devices.length) {
311                         span.appendChild(E('em', { 'class': 'hide-close' }, _('(no interfaces attached)')));
312                         span.appendChild(E('em', { 'class': 'hide-open' }, '-'));
313                 }
314
315                 return span;
316         },
317
318         renderWidget: function(section_id, option_index, cfgvalue) {
319                 var values = L.toArray((cfgvalue != null) ? cfgvalue : this.default),
320                     choices = {},
321                     checked = {};
322
323                 for (var i = 0; i < values.length; i++)
324                         checked[values[i]] = true;
325
326                 values = [];
327
328                 if (!this.multiple && (this.rmempty || this.optional))
329                         choices[''] = E('em', _('unspecified'));
330
331                 for (var i = 0; i < this.networks.length; i++) {
332                         var network = this.networks[i],
333                             name = network.getName();
334
335                         if (name == 'loopback' || !this.filter(section_id, name))
336                                 continue;
337
338                         if (this.novirtual && network.isVirtual())
339                                 continue;
340
341                         if (checked[name])
342                                 values.push(name);
343
344                         choices[name] = this.renderIfaceBadge(network);
345                 }
346
347                 var widget = new ui.Dropdown(this.multiple ? values : values[0], choices, {
348                         id: this.cbid(section_id),
349                         sort: true,
350                         multiple: this.multiple,
351                         optional: this.optional || this.rmempty,
352                         select_placeholder: E('em', _('unspecified')),
353                         display_items: this.display_size || this.size || 3,
354                         dropdown_items: this.dropdown_size || this.size || 5,
355                         validate: L.bind(this.validate, this, section_id),
356                         create: !this.nocreate,
357                         create_markup: '' +
358                                 '<li data-value="{{value}}">' +
359                                         '<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)">' +
360                                                 '{{value}}: <em>('+_('create')+')</em>' +
361                                         '</span>' +
362                                 '</li>'
363                 });
364
365                 return widget.render();
366         },
367
368         textvalue: function(section_id) {
369                 var cfgvalue = this.cfgvalue(section_id),
370                     values = L.toArray((cfgvalue != null) ? cfgvalue : this.default),
371                     rv = E([]);
372
373                 for (var i = 0; i < (this.networks || []).length; i++) {
374                         var network = this.networks[i],
375                             name = network.getName();
376
377                         if (values.indexOf(name) == -1)
378                                 continue;
379
380                         if (rv.length)
381                                 L.dom.append(rv, ' ');
382
383                         L.dom.append(rv, this.renderIfaceBadge(network));
384                 }
385
386                 if (!rv.firstChild)
387                         rv.appendChild(E('em', _('unspecified')));
388
389                 return rv;
390         },
391 });
392
393
394 return L.Class.extend({
395         ZoneSelect: CBIZoneSelect,
396         ZoneForwards: CBIZoneForwards,
397         NetworkSelect: CBINetworkSelect
398 });