/**
* 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 '' +
'
' +
' ' +
'→ ' +
' ' +
'TCP UDP ' +
'× ' +
'
';
}
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 = '' +
'' +
'
New Pod ' +
'
' +
'
' +
'Cancel ' +
'Create ' +
'
';
(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) ' +
'⋮ ' +
'
' +
'
Container Image Status ' +
'' + members + '
' +
'
';
}
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 ' +
'+ New Pod ' +
'
' +
'
' +
'
';
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 });
})();