From 151d93c12df0a58612f1d1611cbfdab5e981ac2f Mon Sep 17 00:00:00 2001 From: magges Date: Sun, 12 Jul 2026 12:02:05 +0000 Subject: [PATCH] Add container detail view (Overview/Environment/Labels/Mounts/Networks/Inspect tabs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking a container's name now opens a tabbed detail modal, fed entirely by the existing inspect action's raw libpod data — no new backend needed. Field names (Config.Env, Config.Labels, Mounts[].Source/Destination/RW, NetworkSettings.Networks{}.IPAddress/Gateway/MacAddress, HostConfig. RestartPolicy) verified against a real inspect response before building the tabs around them. Co-Authored-By: Claude Sonnet 5 --- webui/plugins/podman/javascript/containers.js | 138 +++++++++++++++++- webui/plugins/podman/styles/podman.css | 29 ++++ 2 files changed, 164 insertions(+), 3 deletions(-) diff --git a/webui/plugins/podman/javascript/containers.js b/webui/plugins/podman/javascript/containers.js index c37463d..180293c 100644 --- a/webui/plugins/podman/javascript/containers.js +++ b/webui/plugins/podman/javascript/containers.js @@ -23,7 +23,8 @@ return '' + '' + '' + P.escapeHtml(c.health || c.state) + '' + - '
' + iconLabel(c.name) + '' + P.escapeHtml(c.name) + '
' + + '' + '' + P.escapeHtml(c.image) + '' + '' + cpuMem + '' + '' + P.escapeHtml(c.ports.join(', ') || '—') + '' + @@ -77,6 +78,132 @@ }); } + // --- Detail view ----------------------------------------------------------- + // + // Fed entirely by the existing inspect action (raw libpod inspect JSON) — + // no new backend endpoint needed, just slicing that one payload into + // tabs. Field names below (Config.Env, Config.Labels, Mounts, + // NetworkSettings.Networks, HostConfig.RestartPolicy, ...) were checked + // live against a real inspect response, not assumed from docs. + + function kvTable(rows) { + if (rows.length === 0) return '
None.
'; + return '' + rows.map(function (r) { + return ''; + }).join('') + '
' + P.escapeHtml(r[0]) + '' + P.escapeHtml(r[1]) + '
'; + } + + function renderOverviewTab(d) { + const cfg = d.Config || {}; + const hostCfg = d.HostConfig || {}; + return kvTable([ + ['Name', (d.Name || '').replace(/^\//, '')], + ['ID', d.Id || ''], + ['Image', cfg.Image || d.Image || ''], + ['Created', d.Created || ''], + ['Command', (cfg.Cmd || []).join(' ') || (cfg.Entrypoint || []).join(' ') || '—'], + ['State', (d.State && d.State.Status) || '—'], + ['Restart policy', (hostCfg.RestartPolicy && hostCfg.RestartPolicy.Name) || '—'], + ['Restart count', String(d.RestartCount || 0)], + ['Privileged', hostCfg.Privileged ? 'yes' : 'no'], + ['Working dir', cfg.WorkingDir || '—'], + ]); + } + + function renderEnvTab(d) { + const env = (d.Config && d.Config.Env) || []; + return kvTable(env.map(function (line) { + const idx = line.indexOf('='); + return idx === -1 ? [line, ''] : [line.slice(0, idx), line.slice(idx + 1)]; + })); + } + + function renderLabelsTab(d) { + const labels = (d.Config && d.Config.Labels) || {}; + return kvTable(Object.keys(labels).map(function (k) { return [k, labels[k]]; })); + } + + function renderMountsTab(d) { + const mounts = d.Mounts || []; + if (mounts.length === 0) return '
No mounts.
'; + return '' + + '' + + mounts.map(function (m) { + const mode = m.RW ? 'rw' : 'ro'; + return ''; + }).join('') + '
TypeSource → Destination
' + P.escapeHtml(m.Type || '') + '' + + P.escapeHtml(m.Source || '') + ' → ' + P.escapeHtml(m.Destination || '') + + ' (' + mode + ')
'; + } + + function renderNetworksTab(d) { + const networks = (d.NetworkSettings && d.NetworkSettings.Networks) || {}; + const names = Object.keys(networks); + if (names.length === 0) return '
No networks (host or none mode).
'; + return names.map(function (name) { + const n = networks[name]; + return '
' + + P.escapeHtml(name) + '
' + kvTable([ + ['IP address', n.IPAddress || '—'], + ['Gateway', n.Gateway || '—'], + ['MAC address', n.MacAddress || '—'], + ['Aliases', (n.Aliases || []).join(', ') || '—'], + ]) + '
'; + }).join(''); + } + + function renderInspectTab(d) { + return '
' + P.escapeHtml(JSON.stringify(d, null, 2)) + '
'; + } + + const DETAIL_TABS = [ + { id: 'overview', label: 'Overview', render: renderOverviewTab }, + { id: 'env', label: 'Environment', render: renderEnvTab }, + { id: 'labels', label: 'Labels', render: renderLabelsTab }, + { id: 'mounts', label: 'Mounts', render: renderMountsTab }, + { id: 'networks', label: 'Networks', render: renderNetworksTab }, + { id: 'inspect', label: 'Inspect (JSON)', render: renderInspectTab }, + ]; + + function openDetailModal(c) { + const backdrop = document.createElement('div'); + backdrop.className = 'podman-modal-backdrop'; + backdrop.innerHTML = '' + + ''; + + (document.querySelector('.podman-plugin') || document.body).appendChild(backdrop); + backdrop.querySelector('[data-role="cancel"]').addEventListener('click', function () { backdrop.remove(); }); + backdrop.addEventListener('click', function (e) { if (e.target === backdrop) backdrop.remove(); }); + document.addEventListener('keydown', function onKey(e) { + if (e.key === 'Escape') { backdrop.remove(); document.removeEventListener('keydown', onKey); } + }); + + const body = backdrop.querySelector('.podman-detail-body'); + P.get('containers', 'inspect', { id: c.id }).then(function (data) { + function showTab(tabId) { + const tab = DETAIL_TABS.find(function (t) { return t.id === tabId; }); + body.innerHTML = tab.render(data); + } + backdrop.querySelectorAll('[data-tab]').forEach(function (btn) { + btn.addEventListener('click', function () { + backdrop.querySelectorAll('[data-tab]').forEach(function (b) { b.classList.remove('active'); }); + btn.classList.add('active'); + showTab(btn.dataset.tab); + }); + }); + showTab('overview'); + }).catch(function (err) { + body.innerHTML = '
' + P.escapeHtml(err.message) + '
'; + }); + } + function applyFilters() { return allContainers.filter(function (c) { if (filter === 'running' && c.state !== 'running') return false; @@ -328,9 +455,14 @@ if (!btn || btn.disabled) return; const row = btn.closest('tr'); const id = row.dataset.id; - if (btn.dataset.action === 'menu') { + if (btn.dataset.action === 'menu' || btn.dataset.action === 'details') { const c = allContainers.find(function (x) { return x.id === id; }); - if (c) openRowMenu(c, btn); + if (!c) return; + if (btn.dataset.action === 'menu') { + openRowMenu(c, btn); + } else { + openDetailModal(c); + } return; } handleAction(id, btn.dataset.action, btn); diff --git a/webui/plugins/podman/styles/podman.css b/webui/plugins/podman/styles/podman.css index 641999c..e02216a 100644 --- a/webui/plugins/podman/styles/podman.css +++ b/webui/plugins/podman/styles/podman.css @@ -145,6 +145,12 @@ .podman-plugin tbody tr:hover { background: var(--surface-2); } .podman-table-wrap { overflow-x: auto; } .podman-row-name { display: flex; align-items: center; gap: 10px; font-weight: 600; } +.podman-row-name-btn { + appearance: none; border: none; background: none; padding: 0; cursor: pointer; + color: var(--text); font-family: var(--font-ui); font-size: 13px; text-align: left; +} +.podman-row-name-btn:hover { color: var(--accent-strong); } +.podman-row-name-btn:hover .ico { border-color: var(--accent); } .podman-row-name .ico { width: 26px; height: 26px; border-radius: 6px; flex: none; background: var(--surface-3); display: grid; place-items: center; font-size: 12px; border: 1px solid var(--border); color: var(--text-dim); @@ -286,3 +292,26 @@ .podman-context-menu button.danger { color: var(--bad); } .podman-context-menu button[disabled] { opacity: .4; cursor: not-allowed; } .podman-context-menu-sep { height: 1px; background: var(--border); margin: 4px 2px; } + +/* Container detail modal — see containers.js openDetailModal(). */ +.podman-modal-xwide { max-width: 760px; } +.podman-detail-tabs { + display: flex; gap: 2px; padding: 0 20px; border-bottom: 1px solid var(--border); overflow-x: auto; +} +.podman-detail-tabs button { + appearance: none; background: none; border: none; border-bottom: 2px solid transparent; + color: var(--text-dim); padding: 10px 12px; font-size: 12.5px; font-weight: 600; cursor: pointer; + white-space: nowrap; font-family: var(--font-ui); +} +.podman-detail-tabs button:hover { color: var(--text); } +.podman-detail-tabs button.active { color: var(--accent-strong); border-bottom-color: var(--accent); } +.podman-detail-body { padding: 16px 20px; max-height: 50vh; overflow-y: auto; } +.podman-detail-table { width: 100%; border-collapse: collapse; font-size: 12.5px; } +.podman-detail-table td { padding: 6px 0; border-bottom: 1px solid var(--border); vertical-align: top; } +.podman-detail-table td:first-child { color: var(--text-faint); font-weight: 600; width: 160px; padding-right: 12px; } +.podman-detail-table td.mono { font-family: var(--font-mono); word-break: break-all; } +.podman-detail-json { + background: #0f1114; color: #c7ccd4; font-family: var(--font-mono); font-size: 11.8px; + padding: 14px 16px; border-radius: 8px; white-space: pre-wrap; word-break: break-all; line-height: 1.6; +} +.podman-detail-empty { color: var(--text-faint); font-size: 12.5px; padding: 8px 0; }