🎈 Sync 2026-05-22 14:28:32

This commit is contained in:
github-actions[bot] committed 2026-05-22 14:28:32 +08:00
1 parent 49605899bb
commit 3174ebefc8
70 files changed
+7296 -4358

No files matched your search

@@ -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;
@@ -306,8 +310,9 @@ return dm2.dv.extend({
Name: name,
NetworkID: netid,
DNSNames: net?.DNSNames || '',
IPv4Address: net?.IPAMConfig?.IPv4Address || '',
IPv4Address: net?.IPAMConfig?.IPv4Address || net?.IPAddress || '',
IPv6Address: net?.IPAMConfig?.IPv6Address || '',
Aliases: net?.Aliases || '',
});
}
@@ -315,6 +320,7 @@ return dm2.dv.extend({
},
render([this_container, images, networks, cpus_mem, ps_top, stats_data, changes_data]) {
this.networks = networks;
const view = this;
const containerName = this_container.Name?.substring(1) || this_container.Id || '';
const containerIdShort = (this_container.Id || '').substring(0, 12);
@@ -359,7 +365,7 @@ return dm2.dv.extend({
}
// Stop button
if (containerStatus === 'running' || containerStatus === 'paused') {
if (containerStatus === 'running' || containerStatus === 'paused' || containerStatus === 'restarting') {
const stopBtn = E('button', {
'class': 'cbi-button cbi-button-reset',
'click': (ev) => this.executeAction(ev, 'stop', this_container.Id)
@@ -368,7 +374,7 @@ return dm2.dv.extend({
}
// Kill button
if (containerStatus === 'running') {
if (containerStatus === 'running' || containerStatus === 'restarting') {
const killBtn = E('button', {
'class': 'cbi-button',
'style': 'background-color: #dc3545;',
@@ -581,6 +587,8 @@ return dm2.dv.extend({
o = ss.option(form.DummyValue, 'DNSNames', _('DNS Names'));
o = ss.option(form.DummyValue, 'Aliases', _('Aliases'));
ss.handleAdd = function(ev) {
ev.preventDefault();
view.executeNetworkAction('connect', null, null, this_container);
@@ -1849,7 +1857,7 @@ return dm2.dv.extend({
dm2.network_disconnect,
{
id: networkID,
body: { Container: view.containerId, Force: false }
body: { Container: this_container.Id, Force: false }
},
_('Disconnect network'),
{
@@ -1883,7 +1891,7 @@ return dm2.dv.extend({
const ip4Input = E('input', {
'type': 'text',
'id': 'network-ip',
'id': 'network-ip4',
'class': 'cbi-input-text',
'placeholder': 'e.g., 172.18.0.5',
'style': 'width:100%; margin-top:5px;'
@@ -1891,18 +1899,29 @@ return dm2.dv.extend({
const ip6Input = E('input', {
'type': 'text',
'id': 'network-ip',
'id': 'network-ip6',
'class': 'cbi-input-text',
'placeholder': 'e.g., 2001:db8:1::1',
'style': 'width:100%; margin-top:5px;'
});
const aliasesInput = E('input', {
'type': 'text',
'id': 'network-aliases',
'class': 'cbi-input-text',
'placeholder': 'e.g., database,db (comma-separated)',
'style': 'width:100%; margin-top:5px;'
});
const modalBody = E('div', { 'class': 'cbi-section' }, [
E('p', {}, _('Select network to connect:')),
networkSelect,
E('label', { 'style': 'display:block; margin-top:10px;' }, _('IP Address (optional):')),
E('label', { 'style': 'display:block; margin-top:10px;' }, _('IPv4 Address (optional):')),
ip4Input,
E('label', { 'style': 'display:block; margin-top:10px;' }, _('IPv6 Address (optional):')),
ip6Input,
E('label', { 'style': 'display:block; margin-top:10px;' }, _('Aliases (optional):')),
aliasesInput,
]);
ui.showModal(_('Connect Network'), [
@@ -1918,7 +1937,9 @@ return dm2.dv.extend({
'click': () => {
const selectedNetwork = networkSelect.value;
const ip4Address = ip4Input.value || '';
// const ip6Address = ip6Input.value || '';
const ip6Address = ip6Input.value || '';
const aliasesRaw = aliasesInput.value || '';
const aliases = aliasesRaw.split(',').map(a => a.trim()).filter(Boolean);
if (!selectedNetwork) {
view.showNotification(_('Error'), [_('No network selected')], 5000, 'error');
@@ -1927,8 +1948,14 @@ return dm2.dv.extend({
ui.hideModal();
const body = { Container: view.containerId };
body.EndpointConfig = { IPAMConfig: { IPv4Address: ip4Address } }; //, IPv6Address: ip6Address || null
const body = { Container: this_container.Id };
body.EndpointConfig = {
IPAMConfig: {
IPv4Address: ip4Address || null,
IPv6Address: ip6Address || null
},
Aliases: aliases.length > 0 ? aliases : null
};
view.executeDockerAction(
dm2.network_connect,
@@ -84,6 +84,10 @@ return dm2.dv.extend({
privileged: hostConfig.Privileged ? 1 : 0,
restart_policy: hostConfig.RestartPolicy?.Name || 'unless-stopped',
network: firstName || '',
network_aliases: (() => {
if (!firstName || builtInNetworks.has(firstName)) return '';
return (firstNet?.Aliases || []).join(', ');
})(),
ipv4: (() => {
if (!firstName || builtInNetworks.has(firstName)) return '';
return firstNet?.IPAddress || '';
@@ -137,14 +141,19 @@ return dm2.dv.extend({
if (Array.isArray(bindings) && bindings.length > 0) {
for (const b of bindings) {
if (!b?.HostPort) continue;
const ip = b.HostIp || '';
const ip = (b.HostIp && b.HostIp !== '0.0.0.0' && b.HostIp !== '::') ? b.HostIp : '';
ports.push((ip ? ip + ':' : '') + b.HostPort + ':' + containerPort);
}
}
}
return ports;
})(),
command: c.Config?.Cmd ? c.Config?.Cmd.join(' ') : '',
command: c.Config?.Cmd ? c.Config?.Cmd.map(arg => {
if (arg.includes(' ') || arg.includes('"') || arg.includes("'")) {
return '"' + arg.replace(/"/g, '\\"') + '"';
}
return arg;
}).join(' ') : '',
hostname: c.Config?.Hostname || '',
publish_all: hostConfig.PublishAllPorts ? 1 : 0,
device: (hostConfig.Devices || []).map(d => d.PathOnHost + ':' + d.PathInContainer + (d.CgroupPermissions ? ':' + d.CgroupPermissions : '')),
@@ -184,6 +193,7 @@ return dm2.dv.extend({
}
return list;
})(),
log_driver: hostConfig.LogConfig?.Type || '',
};
}
@@ -269,6 +279,11 @@ return dm2.dv.extend({
o.datatype = 'ip6ll';
o.validate = not_with_a_docker_net;
o = s.option(form.Value, 'network_aliases', _('Network Aliases'));
o.rmempty = true;
o.placeholder = 'database,db (CSV)';
o.validate = not_with_a_docker_net;
o = s.option(form.DynamicList, 'link', _('Links with other containers'));
o.rmempty = true;
o.placeholder='container_name:alias';
@@ -733,6 +748,14 @@ return dm2.dv.extend({
o.placeholder='max-size=1m';
o.depends('advanced', 1);
o = s.option(form.ListValue, 'log_driver', _('Log driver'));
o.rmempty = true;
o.value('', _('Default (daemon)'));
o.value('local', _('local'));
o.value('json-file', _('json-file'));
o.value('syslog', _('syslog'));
o.value('none', _('none'));
o.depends('advanced', 1);
this.map = m;
@@ -757,8 +780,10 @@ return dm2.dv.extend({
.then(() => {
const get = (opt) => map.data.get('json', 'container', opt);
const name = get('name');
const log_driver = get('log_driver');
// const pull = toBool(get('pull'));
const network = get('network');
const network_aliases = get('network_aliases');
const publish = get('publish');
const command = get('command');
// const publish_all = toBool(get('publish_all'));
@@ -774,7 +799,7 @@ return dm2.dv.extend({
Tty: toBool(get('tty')),
OpenStdin: toBool(get('interactive')),
Env: get('env'),
Cmd: command ? command.split(' ') : null,
Cmd: command ? (command.match(/(?:[^\s"]+|"[^"]*")+/g) || []).map(arg => arg.replace(/^"|"$/g, '')) : null,
Image: get('image'),
HostConfig: {
CpuShares: toInt(get('cpu_shares')),
@@ -795,24 +820,45 @@ return dm2.dv.extend({
CgroupPermissions: parts[2] || 'rwm'
};
}) : undefined,
LogConfig: log_opt ? {
LogConfig: log_driver ? {
Type: log_driver,
Config: listToKv(log_opt)
} : (log_opt ? {
Type: 'json-file',
Config: listToKv(log_opt)
} : undefined,
} : undefined),
NetworkMode: network,
PortBindings: publish ? Object.fromEntries(
(Array.isArray(publish) ? publish : [publish])
.filter(p => p && typeof p === 'string' && p.trim().length > 0)
.map(p => {
// formats: hostPort:cPort/proto or hostIp:hostPort:cPort/proto
const m = p.match(/^(?:([^:]+):(\d+)|(\d+)):(\d+)\/(tcp|udp)$/);
if (!m) return null;
const hostIp = m[1] || '';
const hostPort = m[2] || m[3];
const cPort = m[4];
const proto = m[5];
const binding = hostIp ? { HostIp: hostIp, HostPort: hostPort } : { HostPort: hostPort };
return [`${cPort}/${proto}`, [binding]];
// 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,
Mounts: undefined,
@@ -832,7 +878,10 @@ return dm2.dv.extend({
Sysctls: sysctl ? listToKv(sysctl) : undefined,
},
NetworkingConfig: {
EndpointsConfig: { [network]: { IPAMConfig: { IPv4Address: get('ipv4') || null, IPv6Address: get('ipv6') || null } } },
EndpointsConfig: { [network]: {
IPAMConfig: { IPv4Address: get('ipv4') || null, IPv6Address: get('ipv6') || null },
Aliases: network_aliases ? network_aliases.split(',').map(a => a.trim()).filter(Boolean) : null
} },
}
};
@@ -177,6 +177,7 @@ return dm2.dv.extend({
buildContainerActions(cont, idx) {
const view = this;
const isRunning = cont?.State === 'running';
const isRestarting = cont?.State === 'restarting';
const isPaused = cont?.State === 'paused';
const btns = [
E('button', {
@@ -252,7 +253,7 @@ return dm2.dv.extend({
dm2.Types['container'].sub['stop'].i18n,
{showOutput: true, showSuccess: false}
),
'disabled' : !(isRunning || isPaused) ? true : null
'disabled' : !(isRunning || isPaused || isRestarting) ? true : null
}, [dm2.Types['container'].sub['stop'].e]),
E('button', {
@@ -264,7 +265,7 @@ return dm2.dv.extend({
dm2.Types['container'].sub['kill'].i18n,
{showOutput: true, showSuccess: false}
),
'disabled' : !(isRunning || isPaused) ? true : null
'disabled' : !(isRunning || isPaused || isRestarting) ? true : null
}, [dm2.Types['container'].sub['kill'].e]),
E('button', {
@@ -32,13 +32,22 @@ application/json-seq: ␊ = \n | ^J | 0xa, ␞ = ␞ | ^^ | 0x1e
return dm2.dv.extend({
load() {
const now = Math.floor(Date.now() / 1000);
this.js_api = false;
return Promise.resolve([]);
return Promise.all([
dm2.docker_events({ query: { since: `0`, until: `${now}` } }),
dm2.js_api_ready.then(([ok, ]) => this.js_api = ok),
]);
},
render() {
this.outputText = '';
const event_list = [];
render([events, js_api_available]) {
if (events?.code !== 200) {
return E('div', {}, [ events?.body?.message ]);
}
this.outputText = events?.body ? JSON.stringify(events?.body, null, 2) + '\n' : '';
const event_list = events?.body || [];
const view = this;
const mainContainer = E('div', { 'class': 'cbi-map' }, [
@@ -89,7 +98,7 @@ return dm2.dv.extend({
E('input', {
'id': 'event-from-date',
'type': 'datetime-local',
'value': new Date(Date.now() - (60 * 60 * 1000)).toISOString().slice(0, 16),
'value': '1970-01-01T00:00',
'step': 60,
'style': 'width: 180px;',
'change': () => { view.renderEventsTable(event_list); }
@@ -141,16 +150,6 @@ return dm2.dv.extend({
}
}, _('Now'))
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, '\u00a0'),
E('div', { 'class': 'cbi-value-field' }, [
E('button', {
'type': 'button',
'class': 'cbi-button cbi-button-action',
'click': () => { view.renderEventsTable(event_list); }
}, _('Load Events'))
])
])
])
]);
@@ -159,18 +158,10 @@ return dm2.dv.extend({
this.tableSection = E('div', { 'class': 'cbi-section', 'id': 'events-section' });
mainContainer.appendChild(this.tableSection);
this.tableSection.appendChild(E('em', {}, _('Loading recent events…')));
this.renderEventsTable(event_list);
mainContainer.appendChild(this.insertOutputFrame(E('div', {}), null));
dm2.js_api_ready.then(([ok, ]) => {
view.js_api = ok;
}).catch(() => {
view.js_api = false;
});
window.setTimeout(() => view.renderEventsTable(event_list), 0);
return mainContainer;
},
@@ -208,7 +208,7 @@ return dm2.dv.extend({
const n = net.Name;
const _shortId = (net.Id || '').substring(0, 12);
const shortLink = E('a', {
'href': `${view.dockerman_url}/network/${net.Id}`,
'href': `${this.dockerman_url}/network/${net.Id}`,
'style': 'font-family: monospace;',
'title': _('Click to view this network'),
}, [_shortId]);
@@ -63,154 +63,6 @@ function getVolumesInUseByContainers(containers) {
return inUse;
}
function formatScalar(value) {
if (value == null || value === '')
return '-';
if (Array.isArray(value))
return value.length ? value.join(', ') : '-';
if (typeof value === 'boolean')
return value ? 'true' : 'false';
return String(value);
}
function formatKeyValueObject(obj) {
if (!obj || typeof obj !== 'object')
return formatScalar(obj);
const pairs = [];
for (const [key, value] of Object.entries(obj)) {
if (value == null || value === '' || value === false)
continue;
pairs.push(`${key}: ${formatScalar(value)}`);
}
return pairs.length ? pairs.join('; ') : '-';
}
function formatPlugins(plugins) {
if (!plugins || typeof plugins !== 'object')
return '-';
const parts = [];
for (const [type, entries] of Object.entries(plugins)) {
if (!entries)
continue;
if (Array.isArray(entries))
parts.push(`${type}: ${entries.length ? entries.join(', ') : '-'}`);
else
parts.push(`${type}: ${formatScalar(entries)}`);
}
return parts.length ? parts.join('; ') : '-';
}
function formatRegistryConfig(registryConfig) {
if (!registryConfig || typeof registryConfig !== 'object')
return '-';
const parts = [];
if (Array.isArray(registryConfig.Mirrors) && registryConfig.Mirrors.length)
parts.push(`Mirrors: ${registryConfig.Mirrors.join(', ')}`);
if (Array.isArray(registryConfig.InsecureRegistryCIDRs) && registryConfig.InsecureRegistryCIDRs.length)
parts.push(`Insecure CIDRs: ${registryConfig.InsecureRegistryCIDRs.join(', ')}`);
return parts.length ? parts.join('; ') : '-';
}
function formatRuntimes(runtimes) {
if (!runtimes || typeof runtimes !== 'object')
return '-';
const parts = [];
for (const [name, runtime] of Object.entries(runtimes)) {
const path = runtime?.path ? ` (${runtime.path})` : '';
parts.push(`${name}${path}`);
}
return parts.length ? parts.join(', ') : '-';
}
function formatSwarm(swarm) {
if (!swarm || typeof swarm !== 'object')
return '-';
const parts = [];
if (swarm.LocalNodeState)
parts.push(`State: ${swarm.LocalNodeState}`);
if (swarm.NodeID)
parts.push(`NodeID: ${swarm.NodeID}`);
if (swarm.NodeAddr)
parts.push(`NodeAddr: ${swarm.NodeAddr}`);
if (swarm.ControlAvailable != null)
parts.push(`Control: ${swarm.ControlAvailable ? 'yes' : 'no'}`);
return parts.length ? parts.join('; ') : '-';
}
function formatContainerd(containerd) {
if (!containerd || typeof containerd !== 'object')
return '-';
const parts = [];
if (containerd.Address)
parts.push(`Address: ${containerd.Address}`);
if (containerd.Namespaces && typeof containerd.Namespaces === 'object')
parts.push(`Namespaces: ${formatKeyValueObject(containerd.Namespaces)}`);
return parts.length ? parts.join('; ') : '-';
}
function formatInfoBody(body) {
const rows = [];
for (const [key, value] of Object.entries(body || {})) {
if (!value)
continue;
let formatted = null;
switch (key) {
case 'Plugins':
formatted = formatPlugins(value);
break;
case 'RegistryConfig':
formatted = formatRegistryConfig(value);
break;
case 'Runtimes':
formatted = formatRuntimes(value);
break;
case 'Swarm':
formatted = formatSwarm(value);
break;
case 'Containerd':
formatted = formatContainerd(value);
break;
case 'Warnings':
case 'Labels':
case 'SecurityOptions':
case 'CDISpecDirs':
formatted = formatScalar(value);
break;
case 'DriverStatus':
formatted = Array.isArray(value) ? value.map(v => Array.isArray(v) ? `${v[0]}: ${v[1]}` : formatScalar(v)).join('; ') : formatScalar(value);
break;
default:
if (typeof value === 'object')
formatted = formatKeyValueObject(value);
else
formatted = formatScalar(value);
}
rows.push({ entry: key, value: formatted });
}
return rows;
}
return dm2.dv.extend({
load() {
@@ -263,7 +115,7 @@ return dm2.dv.extend({
this.parseHeaders(version_response.headers, version_headers);
this.parseBody(version_response.body, version_body);
info_body.push(...formatInfoBody(info_response.body));
this.parseBody(info_response.body, info_body);
// this.parseBody(df_response.body, df_body);
const view = this;
const info = info_response.body;