mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-06 20:14:10 +08:00
🍕 Sync 2026-03-20 22:17:54
This commit is contained in:
1 parent
de1c41debb
commit
628685b0e3
91 files changed
+82161
-19938
No files matched your search
@@ -0,0 +1,280 @@
|
||||
'use strict';
|
||||
'require form';
|
||||
'require fs';
|
||||
'require uci';
|
||||
'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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns a Set of image IDs in use by containers
|
||||
* @param {Array} containers - Array of container objects
|
||||
* @returns {Set<string>} Set of image IDs
|
||||
*/
|
||||
function getImagesInUseByContainers(containers) {
|
||||
const inUse = new Set();
|
||||
for (const c of containers || []) {
|
||||
if (c.ImageID) inUse.add(c.ImageID);
|
||||
else if (c.Image) inUse.add(c.Image);
|
||||
}
|
||||
return inUse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Set of network IDs in use by containers
|
||||
* @param {Array} containers - Array of container objects
|
||||
* @returns {Set<string>} Set of network IDs
|
||||
*/
|
||||
function getNetworksInUseByContainers(containers) {
|
||||
const inUse = new Set();
|
||||
for (const c of containers || []) {
|
||||
const networks = c.NetworkSettings?.Networks;
|
||||
if (networks && typeof networks === 'object') {
|
||||
for (const netName in networks) {
|
||||
const net = networks[netName];
|
||||
if (net.NetworkID) inUse.add(net.NetworkID);
|
||||
else if (netName) inUse.add(netName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return inUse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Set of volume mountpoints in use by containers
|
||||
* @param {Array} containers - Array of container objects
|
||||
* @returns {Set<string>} Set of volume names or mountpoints
|
||||
*/
|
||||
function getVolumesInUseByContainers(containers) {
|
||||
const inUse = new Set();
|
||||
for (const c of containers || []) {
|
||||
const mounts = c.Mounts;
|
||||
if (Array.isArray(mounts)) {
|
||||
for (const m of mounts) {
|
||||
if (m.Type === 'volume' && m.Name) inUse.add(m.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return inUse;
|
||||
}
|
||||
|
||||
|
||||
return dm2.dv.extend({
|
||||
load() {
|
||||
// const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
return Promise.all([
|
||||
dm2.docker_version(),
|
||||
dm2.docker_info(),
|
||||
// dm2.docker_df(), // takes > 20 seconds on large docker environments
|
||||
dm2.container_list().then(r => r.body || []),
|
||||
dm2.image_list().then(r => r.body || []),
|
||||
dm2.network_list().then(r => r.body || []),
|
||||
dm2.volume_list().then(r => r.body || []),
|
||||
dm2.callMountPoints(),
|
||||
]);
|
||||
},
|
||||
|
||||
handleAction(name, action, ev) {
|
||||
return dm2.callRcInit(name, action).then(function(ret) {
|
||||
if (ret)
|
||||
throw _('Command failed');
|
||||
|
||||
return true;
|
||||
}).catch(function(e) {
|
||||
L.ui.addTimeLimitedNotification(null, E('p', _('Failed to execute "/etc/init.d/%s %s" action: %s').format(name, action, e)), 5000, 'warning');
|
||||
});
|
||||
},
|
||||
|
||||
render([version_response,
|
||||
info_response,
|
||||
// df_response,
|
||||
container_list,
|
||||
image_list,
|
||||
network_list,
|
||||
volume_list,
|
||||
mounts,
|
||||
]) {
|
||||
const version_headers = [];
|
||||
const version_body = [];
|
||||
const info_body = [];
|
||||
// const df_body = [];
|
||||
const docker_ep = uci.get('dockerd', 'globals', 'hosts');
|
||||
let isLocal = false;
|
||||
if (!docker_ep || docker_ep.length === 0 || docker_ep.map(e => e.includes('.sock')).filter(Boolean).length == 1)
|
||||
isLocal = true;
|
||||
|
||||
if (info_response?.code !== 200) {
|
||||
return E('div', {}, [ info_response?.body?.message ]);
|
||||
}
|
||||
|
||||
this.parseHeaders(version_response.headers, version_headers);
|
||||
this.parseBody(version_response.body, version_body);
|
||||
this.parseBody(info_response.body, info_body);
|
||||
// this.parseBody(df_response.body, df_body);
|
||||
const view = this;
|
||||
const info = info_response.body;
|
||||
|
||||
this.concount = info?.Containers || 0;
|
||||
this.conactivecount = info?.ContainersRunning || 0;
|
||||
|
||||
/* Because the df function that reconciles Volumes, Networks and Containers
|
||||
is slow on large and busy dockerd endpoints, we do it here manually. It's fast. */
|
||||
this.imgcount = image_list.length;
|
||||
this.imgactivecount = getImagesInUseByContainers(container_list)?.size || 0;
|
||||
|
||||
this.netcount = network_list.length;
|
||||
this.netactivecount = getNetworksInUseByContainers(container_list)?.size || 0;
|
||||
|
||||
this.volcount = volume_list?.Volumes?.length;
|
||||
this.volactivecount = getVolumesInUseByContainers(container_list)?.size || 0;
|
||||
|
||||
this.freespace = isLocal ? mounts.find(m => m.mount === info?.DockerRootDir)?.avail || 0 : 0;
|
||||
if (isLocal && this.freespace !== 0)
|
||||
this.freespace = '(' + '%1024.2m'.format(this.freespace) + ' ' + _('Available') + ')';
|
||||
|
||||
const mainContainer = E('div', { 'class': 'cbi-map' });
|
||||
|
||||
// Add heading and description first
|
||||
mainContainer.appendChild(E('h2', { 'class': 'section-title' }, [_('Docker - Overview')]));
|
||||
mainContainer.appendChild(E('div', { 'class': 'cbi-map-descr' }, [
|
||||
_('An overview with the relevant data is displayed here with which the LuCI docker client is connected.'),
|
||||
E('br'),
|
||||
E('a', { href: 'https://github.com/openwrt/luci/blob/master/applications/luci-app-dockerman/README.md' }, ['README'])
|
||||
]));
|
||||
|
||||
if (isLocal)
|
||||
mainContainer.appendChild(E('div', { 'class': 'cbi-section' }, [
|
||||
E('div', { 'style': 'display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: 10px;' }, [
|
||||
E('button', { 'class': 'btn cbi-button-action neutral', 'click': () => this.handleAction('dockerd', 'restart') }, _('Restart', 'daemon restart action')),
|
||||
E('button', { 'class': 'btn cbi-button-action negative', 'click': () => this.handleAction('dockerd', 'stop') }, _('Stop', 'daemon stop action')),
|
||||
])
|
||||
]));
|
||||
|
||||
// Create the info table
|
||||
const summaryTable = new L.ui.Table(
|
||||
[_('Info'), ''],
|
||||
{ id: 'containers-table', style: 'width: 100%; table-layout: auto;' },
|
||||
[]
|
||||
);
|
||||
|
||||
summaryTable.update([
|
||||
[ _('Docker Version'), version_response.body.Version ],
|
||||
[ _('Api Version'), version_response.body.ApiVersion ],
|
||||
[ _('CPUs'), info_response.body.NCPU ],
|
||||
[ _('Total Memory'), '%1024.2m'.format(info_response.body.MemTotal) ],
|
||||
[ _('Docker Root Dir'), `${info_response.body.DockerRootDir} ${ (isLocal && this.freespace) ? this.freespace : '' }` ],
|
||||
[ _('Index Server Address'), info_response.body.IndexServerAddress ],
|
||||
[ _('Registry Mirrors'), info_response.body.RegistryConfig?.Mirrors || '-' ],
|
||||
]);
|
||||
|
||||
// Wrap the table in a cbi-section
|
||||
mainContainer.appendChild(E('div', { 'class': 'cbi-section' }, [
|
||||
summaryTable.render()
|
||||
]));
|
||||
|
||||
|
||||
// Create a container div with grid layout for the status badges
|
||||
let statusContainer = E('div', { style: 'display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px; margin-bottom: 20px;' }, [
|
||||
this.overviewBadge(`${this.dockerman_url}/containers`,
|
||||
E('img', {
|
||||
src: L.resource('dockerman/containers.svg'),
|
||||
style: 'width: 80px; height: 80px;'
|
||||
}, []),
|
||||
_('Containers'),
|
||||
_('Total: '),
|
||||
view.concount,
|
||||
_('Running: '),
|
||||
view.conactivecount),
|
||||
this.overviewBadge(`${this.dockerman_url}/images`,
|
||||
E('img', {
|
||||
src: L.resource('dockerman/images.svg'),
|
||||
style: 'width: 80px; height: 80px;'
|
||||
}, []),
|
||||
_('Images'),
|
||||
_('Total: '),
|
||||
view.imgcount,
|
||||
view.imgactivecount ? _('In Use: ') : '',
|
||||
view.imgactivecount ? view.imgactivecount : ''),
|
||||
this.overviewBadge(`${this.dockerman_url}/networks`,
|
||||
E('img', {
|
||||
src: L.resource('dockerman/networks.svg'),
|
||||
style: 'width: 80px; height: 80px;'
|
||||
}, []),
|
||||
_('Networks'),
|
||||
_('Total: '),
|
||||
view.netcount,
|
||||
view.netactivecount ? _('In Use: ') : '',
|
||||
view.netactivecount ? view.netactivecount : ''),
|
||||
this.overviewBadge(`${this.dockerman_url}/volumes`,
|
||||
E('img', {
|
||||
src: L.resource('dockerman/volumes.svg'),
|
||||
style: 'width: 80px; height: 80px;'
|
||||
}, []),
|
||||
_('Volumes'),
|
||||
_('Total: '),
|
||||
view.volcount,
|
||||
view.volactivecount ? _('In Use: ') : '',
|
||||
view.volactivecount ? view.volactivecount : ''),
|
||||
]);
|
||||
|
||||
// Add badges section
|
||||
mainContainer.appendChild(statusContainer);
|
||||
|
||||
const m = new form.JSONMap({
|
||||
// df: df_body,
|
||||
vb: version_body,
|
||||
ib: info_body
|
||||
});
|
||||
m.readonly = true;
|
||||
m.tabbed = false;
|
||||
|
||||
let s;
|
||||
|
||||
// Add Version and Environment tables
|
||||
s = m.section(form.TableSection, 'vb', _('Version'));
|
||||
s.anonymous = true;
|
||||
|
||||
s.option(form.DummyValue, 'entry', _('Name'));
|
||||
s.option(form.DummyValue, 'value', _('Value'));
|
||||
|
||||
s = m.section(form.TableSection, 'ib', _('Environment'));
|
||||
s.anonymous = true;
|
||||
s.filterrow = true;
|
||||
|
||||
s.option(form.DummyValue, 'entry', _('Entry'));
|
||||
s.option(form.DummyValue, 'value', _('Value'));
|
||||
|
||||
// Render the form sections and append them
|
||||
return m.render()
|
||||
.then(fe => {
|
||||
mainContainer.appendChild(fe);
|
||||
return mainContainer;
|
||||
});
|
||||
},
|
||||
|
||||
overviewBadge(url, resource_div, caption, total_caption, total_count, active_caption, active_count) {
|
||||
return E('a', { href: url, style: 'text-decoration: none; cursor: pointer;', title: _('Go to relevant configuration page') }, [
|
||||
E('div', { style: 'border: 1px solid #ddd; border-radius: 5px; padding: 15px; min-height: 120px; display: flex; align-items: center;' }, [
|
||||
E('div', { style: 'flex: 0 0 auto; margin-right: 15px;' }, [
|
||||
resource_div,
|
||||
]),
|
||||
E('div', { style: 'flex: 1;' }, [
|
||||
E('div', { style: 'font-size: 20px; font-weight: bold; color: #333; margin-bottom: 8px;' }, caption),
|
||||
E('div', { style: 'font-size: 16px; margin: 4px 0;' }, [
|
||||
E('span', { style: 'color: #666; margin-right: 10px;' }, [total_caption, E('strong', { style: 'color: #0066cc;' }, total_count)])
|
||||
]),
|
||||
E('div', { style: 'font-size: 16px; margin: 4px 0;' }, [
|
||||
E('span', { style: 'color: #666;' }, [active_caption, E('strong', { style: 'color: #28a745;' }, active_count)])
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user