/** * javascript/containers.js * * Containers panel: table of all containers with lifecycle actions * (start/stop/restart/remove), backed entirely by ajax/containers.php. */ (function () { 'use strict'; const P = window.Podman; let allContainers = []; let filter = 'all'; let searchTerm = ''; function iconLabel(name) { return P.escapeHtml(name.slice(0, 2).toUpperCase()); } function rowHtml(c) { const cpuMem = c.state === 'running' ? 'running' : ''; return '' + '' + '' + P.escapeHtml(c.health || c.state) + '' + '' + '' + P.escapeHtml(c.image) + '' + '' + cpuMem + '' + '' + P.escapeHtml(c.ports.join(', ') || '—') + '' + '' + P.formatDuration(c.uptimeSeconds) + '' + '' + actionButtons(c) + '' + ''; } function actionButtons(c) { if (c.state === 'running') { return '' + '' + '' + ''; } if (c.state === 'paused') { return '' + '' + ''; } return '' + '' + ''; } function openRowMenu(c, anchorBtn) { const items = []; if (c.state === 'running') { items.push({ label: 'Pause', onClick: function () { handleAction(c.id, 'pause'); } }); items.push({ label: 'Kill', danger: true, onClick: function () { handleAction(c.id, 'kill'); } }); } items.push({ label: 'Rename', onClick: function () { openRenameModal(c); } }); items.push('separator'); items.push({ label: 'Remove', danger: true, disabled: c.state === 'running', onClick: function () { handleAction(c.id, 'remove'); }, }); P.openContextMenu(anchorBtn, items); } function openRenameModal(c) { P.openFormModal({ title: 'Rename Container', submitLabel: 'Rename', fields: [{ name: 'name', label: 'New name', required: true, placeholder: c.name }], onSubmit: function (values) { return P.post('containers', 'rename', { id: c.id, name: values.name }).then(load); }, }); } // --- 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; if (filter === 'stopped' && c.state === 'running') return false; if (searchTerm && c.name.toLowerCase().indexOf(searchTerm) === -1 && c.image.toLowerCase().indexOf(searchTerm) === -1) return false; return true; }); } function renderTable() { const tbody = P.el('containers-tbody'); const visible = applyFilters(); tbody.innerHTML = visible.length ? visible.map(rowHtml).join('') : 'No containers match.'; } function renderCounts() { const running = allContainers.filter(function (c) { return c.state === 'running'; }).length; P.el('containers-count-all').textContent = 'All ' + allContainers.length; P.el('containers-count-running').textContent = 'Running ' + running; P.el('containers-count-stopped').textContent = 'Stopped ' + (allContainers.length - running); } function load() { const tbody = P.el('containers-tbody'); tbody.innerHTML = P.loadingRow(7); return P.get('containers', 'list').then(function (data) { allContainers = data; renderCounts(); renderTable(); }).catch(function (err) { tbody.innerHTML = P.errorRow(7, err.message); }); } // --- Create Container ----------------------------------------------------- // // Purpose-built modal (not app.js's generic openFormModal, which only // supports flat text fields) — port/volume/env rows are dynamic // add/remove groups, and network needs a ' + '' + '' + '' + '' + ''; } function volumeRowHtml() { return '' + '
' + '' + '' + '' + '' + '' + '
'; } function envRowHtml() { return '' + '
' + '' + '=' + '' + '' + '
'; } function addRow(groupEl, rowHtmlFn) { const div = document.createElement('div'); div.innerHTML = rowHtmlFn(); const row = div.firstElementChild; row.querySelector('[data-remove-row]').addEventListener('click', function () { row.remove(); }); groupEl.appendChild(row); } function readRows(groupEl) { return Array.from(groupEl.children).map(function (row) { const values = {}; row.querySelectorAll('[data-field]').forEach(function (input) { values[input.dataset.field] = input.value.trim(); }); return values; }); } function openCreateContainerModal() { const backdrop = document.createElement('div'); backdrop.className = 'podman-modal-backdrop'; backdrop.innerHTML = '' + ''; (document.querySelector('.podman-plugin') || document.body).appendChild(backdrop); const portsGroup = backdrop.querySelector('#cc-ports'); const volumesGroup = backdrop.querySelector('#cc-volumes'); const envGroup = backdrop.querySelector('#cc-env'); addRow(portsGroup, portRowHtml); addRow(volumesGroup, volumeRowHtml); addRow(envGroup, envRowHtml); backdrop.querySelector('[data-add="port"]').addEventListener('click', function () { addRow(portsGroup, portRowHtml); }); backdrop.querySelector('[data-add="volume"]').addEventListener('click', function () { addRow(volumesGroup, volumeRowHtml); }); backdrop.querySelector('[data-add="env"]').addEventListener('click', function () { addRow(envGroup, envRowHtml); }); // Populate the network dropdown with any existing custom (non-default) // podman networks, in addition to the built-in bridge/host/none modes // — best-effort: if the list call fails, the three built-ins still work. P.get('networks', 'list').then(function (networks) { const select = backdrop.querySelector('#cc-network'); networks.filter(function (n) { return !n.isDefault; }).forEach(function (n) { const opt = document.createElement('option'); opt.value = n.name; opt.textContent = n.name; select.appendChild(opt); }); }).catch(function () { /* built-in modes still usable */ }); backdrop.querySelector('#cc-image').focus(); function close() { backdrop.remove(); } function showError(message) { let box = backdrop.querySelector('.podman-modal-error'); if (!box) { box = document.createElement('div'); box.className = 'podman-modal-error'; backdrop.querySelector('.podman-modal-body').appendChild(box); } box.textContent = message; } function submit() { const image = backdrop.querySelector('#cc-image').value.trim(); if (!image) { showError('"Image" is required.'); return; } const ports = readRows(portsGroup).filter(function (r) { return r.hostPort && r.containerPort; }); const volumes = readRows(volumesGroup).filter(function (r) { return r.source && r.containerPath; }); const env = readRows(envGroup).filter(function (r) { return r.key; }); const submitBtn = backdrop.querySelector('[data-role="submit"]'); submitBtn.disabled = true; P.post('containers', 'create', { image: image, name: backdrop.querySelector('#cc-name').value.trim(), networkMode: backdrop.querySelector('#cc-network').value, ports: ports, volumes: volumes, env: env, restartPolicy: backdrop.querySelector('#cc-restart').value, privileged: backdrop.querySelector('#cc-privileged').checked, startAfterCreate: backdrop.querySelector('#cc-start').checked, }).then(function () { close(); return load(); }).catch(function (err) { submitBtn.disabled = false; showError(err.message); }); } backdrop.querySelector('[data-role="cancel"]').addEventListener('click', close); backdrop.querySelector('[data-role="submit"]').addEventListener('click', submit); backdrop.querySelector('form').addEventListener('submit', function (e) { e.preventDefault(); submit(); }); backdrop.addEventListener('click', function (e) { if (e.target === backdrop) close(); }); document.addEventListener('keydown', function onKey(e) { if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); } }); } function handleAction(id, action, btn) { const doIt = function (extra) { if (btn) btn.disabled = true; return P.post('containers', action, Object.assign({ id: id }, extra)).then(load).catch(function (err) { alert('Action failed: ' + err.message); if (btn) btn.disabled = false; }); }; if (action === 'remove') { if (!confirm('Remove this container? This does not remove its volumes.')) return; doIt({ force: true }); } else if (action === 'kill') { if (!confirm('Send SIGKILL to this container? Unlike Stop, this does not let it shut down gracefully.')) return; doIt({}); } else { doIt({}); } } function init() { P.el('containers-create-btn').addEventListener('click', openCreateContainerModal); P.el('containers-search').addEventListener('input', function (e) { searchTerm = e.target.value.trim().toLowerCase(); renderTable(); }); document.querySelectorAll('#containers-filterset button').forEach(function (btn) { btn.addEventListener('click', function () { document.querySelectorAll('#containers-filterset button').forEach(function (b) { b.classList.remove('active'); }); btn.classList.add('active'); filter = btn.dataset.filter; renderTable(); }); }); P.el('containers-tbody').addEventListener('click', function (e) { const btn = e.target.closest('button[data-action]'); if (!btn || btn.disabled) return; const row = btn.closest('tr'); const id = row.dataset.id; if (btn.dataset.action === 'menu' || btn.dataset.action === 'details') { const c = allContainers.find(function (x) { return x.id === id; }); if (!c) return; if (btn.dataset.action === 'menu') { openRowMenu(c, btn); } else { openDetailModal(c); } return; } handleAction(id, btn.dataset.action, btn); }); return load(); } P.registerPanel('containers', { init: init, refresh: load }); })();