/** * 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) + '' + '
' + iconLabel(c.name) + '' + P.escapeHtml(c.name) + '
' + '' + 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); }, }); } 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') { const c = allContainers.find(function (x) { return x.id === id; }); if (c) openRowMenu(c, btn); return; } handleAction(id, btn.dataset.action, btn); }); return load(); } P.registerPanel('containers', { init: init, refresh: load }); })();