luci-mod-network: Add IPSelect widget which eases selection of interface IPs
authorPaul Donald <[email protected]>
Sat, 25 Oct 2025 23:47:57 +0000 (01:47 +0200)
committerPaul Donald <[email protected]>
Tue, 28 Oct 2025 20:01:29 +0000 (21:01 +0100)
This widget is modeled after CBINetworkSelect, which is similar in nature.
It presents a dropdown box of all device IPs with an accompanying badge of the
device.

Signed-off-by: Paul Donald <[email protected]>
modules/luci-base/htdocs/luci-static/resources/tools/widgets.js

index 98fc6502f56e706d02d8d486070f0146a5876804..f43c37130a76c1b4f00a60436546ecdcfbd85586 100644 (file)
@@ -338,6 +338,110 @@ var CBIZoneForwards = form.DummyValue.extend({
        },
 });
 
+const CBIIPSelect = form.ListValue.extend({
+       __name__: 'CBI.IPSelect',
+
+       load(section_id) {
+               return network.getDevices().then(L.bind(function(devices) {
+                       this.devices = devices;
+                       return this.super('load', section_id);
+               }, this));
+       },
+
+       filter(section_id, value) {
+               return true;
+       },
+
+       renderIfaceBadge(device, ip) {
+               return E('div', {}, [
+                       ip,
+                       ' ',
+                       E('span', { 'class': 'ifacebadge', }, [ device.getName(),
+                               E('img', {
+                                       'title': device.getI18n(),
+                                       'src': L.resource('icons/%s%s.svg'.format(device.getType(), device.isUp() ? '' : '_disabled'))
+                               })
+                       ]),
+               ]);
+       },
+
+       renderWidget(section_id, option_index, cfgvalue) {
+               let values = L.toArray((cfgvalue != null) ? cfgvalue : this.default);
+               const choices = {};
+               const checked = {};
+
+               for (const val of values)
+                       checked[val] = true;
+
+               values = [];
+
+               if (!this.multiple && (this.rmempty || this.optional))
+                       choices[''] = E('em', _('unspecified'));
+
+
+               for (const device of (this.devices || [])) {
+                       const name = device.getName();
+
+                       if (name == this.exclude || !this.filter(section_id, name))
+                               continue;
+
+                       if (name == 'loopback' && !this.loopback)
+                               continue;
+
+                       if (this.novirtual && device.isVirtual())
+                               continue;
+
+                       for (const ip of [...device.getIPAddrs(), ...device.getIP6Addrs()]) {
+                               const iponly = ip.split('/')?.[0]
+                               if (checked[iponly])
+                                       values.push(iponly);
+                               choices[iponly] = this.renderIfaceBadge(device, iponly);
+                       }
+               }
+
+               const widget = new ui.Dropdown(this.multiple ? values : values[0], choices, {
+                       id: this.cbid(section_id),
+                       sort: true,
+                       multiple: this.multiple,
+                       optional: this.optional || this.rmempty,
+                       disabled: (this.readonly != null) ? this.readonly : this.map.readonly,
+                       select_placeholder: E('em', _('unspecified')),
+                       display_items: this.display_size || this.size || 2,
+                       dropdown_items: this.dropdown_size || this.size || 5,
+                       datatype: this.multiple ? 'list(ipaddr)' : 'ipaddr',
+                       validate: L.bind(this.validate, this, section_id),
+                       create: false,
+               });
+
+               return widget.render();
+       },
+
+       textvalue(section_id) {
+               const cfgvalue = this.cfgvalue(section_id);
+               const values = L.toArray((cfgvalue != null) ? cfgvalue : this.default);
+               const rv = E([]);
+
+               for (const device of (this.devices || [])) {
+                       for (const ip of [...device.getIPAddrs(), ...device.getIP6Addrs()]) {
+                               const iponly = ip.split('/')[0];
+                               if (values.indexOf(iponly) === -1)
+                                       continue;
+
+                               if (rv.childNodes.length)
+                                       rv.appendChild(document.createTextNode(' '));
+
+                               rv.appendChild(this.renderIfaceBadge(device, iponly));
+                       }
+               }
+
+               if (!rv.firstChild)
+                       rv.appendChild(E('em', _('unspecified')));
+
+               return rv;
+       },
+});
+
+
 var CBINetworkSelect = form.ListValue.extend({
        __name__: 'CBI.NetworkSelect',
 
@@ -652,6 +756,7 @@ var CBIGroupSelect = form.ListValue.extend({
 return L.Class.extend({
        ZoneSelect: CBIZoneSelect,
        ZoneForwards: CBIZoneForwards,
+       IPSelect: CBIIPSelect,
        NetworkSelect: CBINetworkSelect,
        DeviceSelect: CBIDeviceSelect,
        UserSelect: CBIUserSelect,