mirror of
https://github.com/openwrt/luci.git
synced 2026-04-15 19:01:56 +00:00
This is a complete rewrite of the original Lua dockerman in ECMAScript and ucode. Now with most of the Lua gone, we can rename LuCI to JUCI. JavaScript ucode Configuration Interface :) Docker manager basically saw no updates or bug fixes due to the Lua update embargo and transition to ECMAScript in the luci repo. But now that the app is rewritten, updates should come readily from the community. Expect a few bugs in this, although it has seen lots of testing - it's also seen lots of development in different directions. Networking scenarios might require some additions and fixes to the GUI. Swarm functionality is not implemented in this client and is left as an exercise to the community and those with time. All functionality found in the original Lua version is present in this one, except for container "upgrade". Some minor differences are introduced to improve layout and logic. There is no "remote endpoint" any longer since sockets are the main method of connecting to dockerd - and sockets accept remote connections. Docker manager and dockerd on the same host are a remote connection. Buuut, dockerd removes listening on any IP without --tls* options after v27. There is no encryption between docker manager and the API endpoint, or the container consoles when using the standard /var/run/docker.sock. See: https://github.com/openwrt/luci/issues/7310 TODO: handle image update ("Upgrade") for a container Signed-off-by: Paul Donald <newtwen+github@gmail.com>
237 lines
6.7 KiB
JavaScript
237 lines
6.7 KiB
JavaScript
'use strict';
|
|
'require form';
|
|
'require fs';
|
|
'require ui';
|
|
'require dockerman.common as dm2';
|
|
|
|
/*
|
|
Copyright 2026
|
|
Docker manager JS for Luci by Paul Donald <newtwen+github@gmail.com>
|
|
Based on Docker Lua by lisaac <https://github.com/lisaac/luci-app-dockerman>
|
|
LICENSE: GPLv2.0
|
|
*/
|
|
|
|
|
|
return dm2.dv.extend({
|
|
load() {
|
|
return Promise.all([
|
|
dm2.network_list(),
|
|
dm2.container_list({query: {all: true}}),
|
|
]);
|
|
},
|
|
|
|
render([networks, containers]) {
|
|
if (networks?.code !== 200) {
|
|
return E('div', {}, [ networks?.body?.message ]);
|
|
}
|
|
|
|
let network_list = this.getNetworksTable(networks.body, containers.body);
|
|
// let container_list = containers.body;
|
|
const view = this; // Capture the view context
|
|
|
|
|
|
let pollPending = null;
|
|
let netSec = null;
|
|
|
|
const refresh = () => {
|
|
if (pollPending) return pollPending;
|
|
pollPending = view.load().then(([networks2, containers2]) => {
|
|
network_list = view.getNetworksTable(networks2.body, containers2.body);
|
|
// container_list = containers2.body;
|
|
m.data = new m.data.constructor({network: network_list, prune: {}});
|
|
|
|
if (netSec) {
|
|
netSec.footer = [
|
|
`${_('Total')} ${network_list.length}`,
|
|
];
|
|
}
|
|
|
|
return m.render();
|
|
}).catch((err) => { console.warn(err) }).finally(() => { pollPending = null });
|
|
return pollPending;
|
|
};
|
|
|
|
|
|
let s, o;
|
|
const m = new form.JSONMap({network: network_list, prune: {}},
|
|
_('Docker - Networks'),
|
|
_('This page displays all docker networks that have been created on the connected docker host.'));
|
|
m.submit = false;
|
|
m.reset = false;
|
|
|
|
s = m.section(form.TableSection, 'prune', _('Networks overview'), null);
|
|
s.addremove = false;
|
|
s.anonymous = true;
|
|
|
|
const prune = s.option(form.Button, '_prune', null);
|
|
prune.inputtitle = `${dm2.ActionTypes['prune'].i18n} ${dm2.ActionTypes['prune'].e}`;
|
|
prune.inputstyle = 'negative';
|
|
prune.onclick = L.bind(function(section_id, ev) {
|
|
|
|
return this.super('handleXHRTransfer', [{
|
|
q_params: { },
|
|
commandCPath: '/networks/prune',
|
|
commandDPath: '/networks/prune',
|
|
commandTitle: dm2.ActionTypes['prune'].i18n,
|
|
onUpdate: (msg) => {
|
|
try {
|
|
if(msg.error)
|
|
ui.addTimeLimitedNotification(dm2.ActionTypes['prune'].i18n, msg.error, 7000, 'error');
|
|
|
|
const output = JSON.stringify(msg, null, 2) + '\n';
|
|
view.insertOutput(output);
|
|
} catch {
|
|
|
|
}
|
|
},
|
|
noFileUpload: true,
|
|
}]);
|
|
|
|
// return view.executeDockerAction(
|
|
// dm2.network_prune,
|
|
// { query: { filters: '' } },
|
|
// dm2.ActionTypes['prune'].i18n,
|
|
// {
|
|
// showOutput: true,
|
|
// successMessage: _('started/completed'),
|
|
// onSuccess: () => {
|
|
// setTimeout(() => window.location.href = `${this.dockerman_url}/networks`, 1000);
|
|
// }
|
|
// }
|
|
// );
|
|
}, this);
|
|
|
|
netSec = m.section(form.TableSection, 'network');
|
|
netSec.anonymous = true;
|
|
netSec.nodescriptions = true;
|
|
netSec.addremove = true;
|
|
netSec.sortable = true;
|
|
netSec.filterrow = true;
|
|
netSec.addbtntitle = `${dm2.ActionTypes['create'].i18n} ${dm2.ActionTypes['create'].e}`;
|
|
netSec.footer = [
|
|
`${_('Total')} ${network_list.length}`,
|
|
];
|
|
|
|
netSec.handleAdd = function(section_id, ev) {
|
|
window.location.href = `${view.dockerman_url}/network_new`;
|
|
};
|
|
|
|
netSec.handleRemove = function(section_id, force, ev) {
|
|
const network = network_list.find(net => net['.name'] === section_id);
|
|
if (!network?.Id) return false;
|
|
|
|
return view.executeDockerAction(
|
|
dm2.network_remove,
|
|
{ id: network.Id },
|
|
dm2.ActionTypes['remove'].i18n,
|
|
{
|
|
showOutput: true,
|
|
onSuccess: () => {
|
|
return refresh();
|
|
}
|
|
}
|
|
);
|
|
};
|
|
|
|
netSec.handleInspect = function(section_id, ev) {
|
|
const network = network_list.find(net => net['.name'] === section_id);
|
|
if (!network?.Id) return false;
|
|
|
|
return view.executeDockerAction(
|
|
dm2.network_inspect,
|
|
{ id: network.Id },
|
|
dm2.ActionTypes['inspect'].i18n,
|
|
{ showOutput: true, showSuccess: false }
|
|
);
|
|
};
|
|
|
|
netSec.renderRowActions = function (section_id) {
|
|
const network = network_list.find(net => net['.name'] === section_id);
|
|
const btns = [
|
|
E('button', {
|
|
'class': 'cbi-button view',
|
|
'title': dm2.ActionTypes['inspect'].i18n,
|
|
'click': ui.createHandlerFn(this, this.handleInspect, section_id),
|
|
}, [dm2.ActionTypes['inspect'].e]),
|
|
|
|
E('div', {
|
|
'style': 'width: 20px',
|
|
// Some safety margin for mis-clicks
|
|
}, [' ']),
|
|
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-negative remove',
|
|
'title': dm2.ActionTypes['remove'].i18n,
|
|
'click': ui.createHandlerFn(this, this.handleRemove, section_id, false),
|
|
'disabled': network?._disable_delete,
|
|
}, dm2.ActionTypes['remove'].e),
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-negative important remove',
|
|
'title': dm2.ActionTypes['force_remove'].i18n,
|
|
'click': ui.createHandlerFn(this, this.handleRemove, section_id, true),
|
|
'disabled': network?._disable_delete,
|
|
}, dm2.ActionTypes['force_remove'].e),
|
|
];
|
|
return E('td', { 'class': 'td middle cbi-section-actions' }, E('div', btns));
|
|
};
|
|
|
|
o = netSec.option(form.DummyValue, '_shortId', _('ID'));
|
|
|
|
o = netSec.option(form.DummyValue, 'Name', _('Name'));
|
|
|
|
o = netSec.option(form.DummyValue, 'Labels', _('Labels') + ' 🏷️');
|
|
o.cfgvalue = view.objectCfgValueTT;
|
|
|
|
o = netSec.option(form.DummyValue, '_container', _('Containers'));
|
|
|
|
o = netSec.option(form.DummyValue, 'Driver', _('Driver'));
|
|
|
|
o = netSec.option(form.DummyValue, '_interface', _('Parent Interface'));
|
|
|
|
o = netSec.option(form.DummyValue, '_subnet', _('Subnet'));
|
|
|
|
o = netSec.option(form.DummyValue, '_gateway', _('Gateway'));
|
|
|
|
this.insertOutputFrame(s, m);
|
|
|
|
return m.render();
|
|
},
|
|
|
|
handleSave: null,
|
|
handleSaveApply: null,
|
|
handleReset: null,
|
|
|
|
getNetworksTable(networks, containers) {
|
|
const data = [];
|
|
|
|
for (const [i, net] of (networks || []).entries()) {
|
|
const n = net.Name;
|
|
const _shortId = (net.Id || '').substring(0, 12);
|
|
const shortLink = E('a', {
|
|
'href': `${view.dockerman_url}/network/${net.Id}`,
|
|
'style': 'font-family: monospace;',
|
|
'title': _('Click to view this network'),
|
|
}, [_shortId]);
|
|
|
|
// Just push plain data objects without UCI metadata
|
|
const configs = Array.isArray(net?.IPAM?.Config) ? net.IPAM.Config : [];
|
|
data.push({
|
|
...net,
|
|
_gateway: configs.map(o => o.Gateway).filter(o => o).join(', ') || '',
|
|
_subnet: configs.map(o => o.Subnet).filter(o => o).join(', ') || '',
|
|
_disable_delete: ( n === 'bridge' || n === 'none' || n === 'host' ) ? true : null,
|
|
_shortId: shortLink,
|
|
_container: this.parseContainerLinksForNetwork(net, containers),
|
|
_interface: (net.Driver === 'bridge')
|
|
? net.Options?.['com.docker.network.bridge.name'] || ''
|
|
: (net.Driver === 'macvlan')
|
|
? net?.Options?.parent
|
|
: '',
|
|
});
|
|
}
|
|
|
|
return data;
|
|
},
|
|
|
|
});
|