luci-app-dockerman: links for port bindings
authorPaul Donald <redacted>
Thu, 14 May 2026 12:08:50 +0000 (15:08 +0300)
committerPaul Donald <redacted>
Thu, 14 May 2026 13:59:09 +0000 (16:59 +0300)
- handling of hostIP in ports
- retain during container clone
- make link clickable
- provide router host if ip is general
- provide container host if specific

Signed-off-by: Serhii Ivanov <redacted>
applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js
applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js
applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js

index 80ce6863a4b5b68e65a5fc9b7caa21a2949416b3..66f412a68f1991f9802c84dc2b8af11de10299f0 100644 (file)
@@ -250,8 +250,12 @@ return dm2.dv.extend({
                if (!portBindings || typeof portBindings !== 'object') return [];
                const ports = [];
                for (const [containerPort, bindings] of Object.entries(portBindings)) {
-                       if (Array.isArray(bindings) && bindings.length > 0 && bindings[0]?.HostPort) {
-                               ports.push(`${bindings[0].HostPort}:${containerPort}`);
+                       if (Array.isArray(bindings)) {
+                               for (const b of bindings) {
+                                       if (!b?.HostPort) continue;
+                                       const ip = (b.HostIp && b.HostIp !== '0.0.0.0' && b.HostIp !== '::') ? b.HostIp + ':' : '';
+                                       ports.push(`${ip}${b.HostPort}:${containerPort}`);
+                               }
                        }
                }
                return ports;
index 726240eac1f6420faa48551d732861b522656fd5..c493683b8b85b86645b5ce4890f243b51cbd372f 100644 (file)
@@ -138,9 +138,12 @@ return dm2.dv.extend({
                                publish: (() => {
                                        const ports = [];
                                        for (const [containerPort, bindings] of Object.entries(hostConfig.PortBindings || {})) {
-                                               if (Array.isArray(bindings) && bindings.length > 0 && bindings[0]?.HostPort) {
-                                                       const hostPort = bindings[0].HostPort;
-                                                       ports.push(hostPort + ':' + containerPort);
+                                               if (Array.isArray(bindings) && bindings.length > 0) {
+                                                       for (const b of bindings) {
+                                                               if (!b?.HostPort) continue;
+                                                               const ip = (b.HostIp && b.HostIp !== '0.0.0.0' && b.HostIp !== '::') ? b.HostIp : '';
+                                                               ports.push((ip ? ip + ':' : '') + b.HostPort + ':' + containerPort);
+                                                       }
                                                }
                                        }
                                        return ports;
@@ -816,8 +819,32 @@ return dm2.dv.extend({
                                                        (Array.isArray(publish) ? publish : [publish])
                                                        .filter(p => p && typeof p === 'string' && p.trim().length > 0)
                                                        .map(p => {
-                                                               const m = p.match(/^(\d+):(\d+)\/(tcp|udp)$/);
-                                                               if (m) return [`${m[2]}/${m[3]}`, [{ HostPort: m[1] }]];
+                                                               // hostIp:hostPort:cPort/proto (e.g. 192.168.1.100:8080:80/tcp)
+                                                               const m = p.match(/^([^:]+):(\d+):(\d+)\/(tcp|udp)$/);
+                                                               if (m) {
+                                                                       const hostIp   = m[1];
+                                                                       const hostPort = m[2];
+                                                                       const cPort    = m[3];
+                                                                       const proto    = m[4];
+                                                                       return [`${cPort}/${proto}`, [{ HostIp: hostIp, HostPort: hostPort }]];
+                                                               }
+                                                               // [ipv6]:hostPort:cPort/proto (e.g. [::1]:8080:80/tcp)
+                                                               const m6 = p.match(/^\[([^\]]+)\]:(\d+):(\d+)\/(tcp|udp)$/);
+                                                               if (m6) {
+                                                                       const hostIp   = m6[1];
+                                                                       const hostPort = m6[2];
+                                                                       const cPort    = m6[3];
+                                                                       const proto    = m6[4];
+                                                                       return [`${cPort}/${proto}`, [{ HostIp: hostIp, HostPort: hostPort }]];
+                                                               }
+                                                               // hostPort:cPort/proto (e.g. 8080:80/tcp)
+                                                               const m2 = p.match(/^(\d+):(\d+)\/(tcp|udp)$/);
+                                                               if (m2) {
+                                                                       const hostPort = m2[1];
+                                                                       const cPort    = m2[2];
+                                                                       const proto    = m2[3];
+                                                                       return [`${cPort}/${proto}`, [{ HostPort: hostPort }]];
+                                                               }
                                                                return null;
                                                        }).filter(Boolean)
                                                ) : undefined,
index ec0499bb847e1b3824ffd95d456b3645ada7dc17..cc4980f6e41a002fa022cf709980e1fd03d7f521 100644 (file)
@@ -309,6 +309,56 @@ return dm2.dv.extend({
                }, E('div', btns));
        },
 
+       buildPortLinks(ports) {
+               // cont.Ports[] from GET /containers/json — flat array.
+               // Published ports have {IP, PrivatePort, PublicPort, Type}.
+               // Exposed-only ports omit IP and PublicPort: {PrivatePort, Type}.
+               if (!Array.isArray(ports) || ports.length === 0) return '';
+
+               const LOCAL_IPS = new Set(['0.0.0.0', '::']);
+
+               // Sort: published (has PublicPort) before exposed-only, then by PrivatePort
+               const sorted = [...ports].sort((a, b) => {
+                       const aHasPub = a.PublicPort ? 1 : 0;
+                       const bHasPub = b.PublicPort ? 1 : 0;
+                       if (aHasPub !== bHasPub) return bHasPub - aHasPub;
+                       return (a.PrivatePort || 0) - (b.PrivatePort || 0);
+               });
+
+               const lines = sorted.map(p => {
+                       const ip   = p.IP || '';
+                       const pub  = p.PublicPort || '';
+                       const priv = p.PrivatePort || '';
+                       const type = p.Type || '';
+
+                       const isIPv6    = ip.includes(':');
+                       const isLocal   = LOCAL_IPS.has(ip);
+                       const displayIp = isIPv6 ? `[${ip}]` : ip;
+
+                       let label;
+                       if (pub && ip)  label = `${displayIp}:${pub}->${priv}/${type}`;
+                       else if (pub)   label = `${pub}->${priv}/${type}`;
+                       else            label = `${priv}/${type}`;
+
+                       // Clickable link for published TCP ports only
+                       if (type === 'tcp' && pub) {
+                               const host = isLocal ? window.location.hostname : displayIp;
+                               return E('div', {}, [
+                                       E('a', {
+                                               href: `http://${host}:${pub}`,
+                                               target: '_blank',
+                                               rel: 'noopener noreferrer',
+                                               title: _('Open in browser'),
+                                       }, [label]),
+                               ]);
+                       }
+
+                       return E('div', {}, [label]);
+               });
+
+               return E('div', {}, lines);
+       },
+
        handleSave: null,
        handleSaveApply: null,
        handleReset: null,
@@ -343,16 +393,7 @@ return dm2.dv.extend({
                                _shortId: (cont?.Id || '').substring(0, 12),
                                Networks: this.parseNetworkLinksForContainer(network_list, cont?.NetworkSettings?.Networks || {}, true),
                                Created: this.buildTimeString(cont?.Created) || '',
-                               Ports: (Array.isArray(cont.Ports) && cont.Ports.length > 0)
-                                               ? cont.Ports.map(p => {
-                                                       // const ip = p.IP || '';
-                                                       const pub = p.PublicPort || '';
-                                                       const priv = p.PrivatePort || '';
-                                                       const type = p.Type || '';
-                                                       return `${pub ? pub + ':' : ''}${priv}/${type}`;
-                                                       // return `${ip ? ip + ':' : ''}${pub} -> ${priv} (${type})`;
-                                               }).join('<br/>')
-                                               : '',
+                               Ports: this.buildPortLinks(cont.Ports),
                        });
                }
 
git clone https://git.99rst.org/PROJECT