/** * javascript/pods.js * * Pods panel: one card per pod with its member containers nested inside, * backed by ajax/pods.php (which itself cross-references containers.php's * data server-side — see that file for why). */ (function () { 'use strict'; const P = window.Podman; let allPods = []; function portRowHtml() { return '' + '
' + '' + '' + '' + '' + '' + '
'; } function addRow(groupEl) { const div = document.createElement('div'); div.innerHTML = portRowHtml(); 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 openCreatePodModal() { const backdrop = document.createElement('div'); backdrop.className = 'podman-modal-backdrop'; backdrop.innerHTML = '' + ''; (document.querySelector('.podman-plugin') || document.body).appendChild(backdrop); const portsGroup = backdrop.querySelector('#cp-ports'); addRow(portsGroup); backdrop.querySelector('[data-add="port"]').addEventListener('click', function () { addRow(portsGroup); }); backdrop.querySelector('#cp-name').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 name = backdrop.querySelector('#cp-name').value.trim(); if (!name) { showError('"Name" is required.'); return; } if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(name)) { showError('"Name" can only contain letters, digits, ".", "_", "-" — no spaces. Try "' + name.replace(/[^a-zA-Z0-9_.-]+/g, '-') + '" instead.'); return; } const ports = readRows(portsGroup).filter(function (r) { return r.hostPort && r.containerPort; }); const submitBtn = backdrop.querySelector('[data-role="submit"]'); submitBtn.disabled = true; P.post('pods', 'create', { name: name, ports: ports }).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 memberRow(m) { return '' + '' + '' + P.escapeHtml(m.name) + '' + '' + P.escapeHtml(m.image) + '' + '' + P.escapeHtml(m.state) + '' + ''; } function podCard(pod) { const members = pod.members.length ? pod.members.map(memberRow).join('') : 'No member containers'; return '' + '
' + '
' + '' + P.escapeHtml(pod.status) + '' + '' + P.escapeHtml(pod.name) + '' + '' + pod.containersTotal + ' container(s)' + '' + '
' + '
' + '' + members + '
ContainerImageStatus
' + '
'; } function render() { const grid = P.el('pods-grid'); grid.innerHTML = allPods.length ? allPods.map(podCard).join('') : '
No pods yet — create one, or run a container with a "pod" set from the Create Container form.
'; } function load() { const container = P.el('podman-panel-pods'); if (!P.el('pods-grid')) { container.innerHTML = '' + '
' + '
' + 'Group containers sharing network/storage namespaces' + '' + '
' + '
' + '
'; P.el('pods-create-btn').addEventListener('click', openCreatePodModal); P.el('pods-grid').addEventListener('click', handleCardClick); } return P.get('pods', 'list').then(function (data) { allPods = data; render(); }).catch(function (err) { P.el('pods-grid').innerHTML = '
' + P.escapeHtml(err.message) + '
'; }); } function handleAction(name, action, extra) { return P.post('pods', action, Object.assign({ name: name }, extra)).then(load).catch(function (err) { alert('Action failed: ' + err.message); }); } function handleCardClick(e) { const btn = e.target.closest('button[data-action="menu"]'); if (!btn) return; const pod = allPods.find(function (p) { return p.name === btn.closest('.podman-pod-card').dataset.name; }); if (!pod) return; const items = []; if (pod.status === 'running') { items.push({ label: 'Stop', onClick: function () { handleAction(pod.name, 'stop', { timeout: 10 }); } }); items.push({ label: 'Restart', onClick: function () { handleAction(pod.name, 'restart', { timeout: 10 }); } }); } else { items.push({ label: 'Start', onClick: function () { handleAction(pod.name, 'start'); } }); } items.push('separator'); items.push({ label: 'Remove', danger: true, onClick: function () { if (!confirm('Remove pod "' + pod.name + '" and all its member containers?')) return; handleAction(pod.name, 'remove', { force: true }); }, }); P.openContextMenu(btn, items); } P.registerPanel('pods', { init: load, refresh: load }); })();