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;
(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,
}, 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,
_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),
});
}