'use strict'; 'require form'; 'require fs'; 'require uci'; 'require dockerman.common as dm2'; /* Copyright 2026 Docker manager JS for Luci by Paul Donald Based on Docker Lua by lisaac LICENSE: GPLv2.0 */ /** * Returns a Set of image IDs in use by containers * @param {Array} containers - Array of container objects * @returns {Set} 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} 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} 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)]) ]) ]) ]) ]) } });