Add container pause/resume/kill/rename + reusable context-menu component

First increment of the big WebUI feature-parity spec (see task list) —
row-level actions were about to run out of icon-button space, so this adds
a small anchored dropdown menu (app.js openContextMenu) for secondary
per-container actions instead of cramming more buttons into every row.

All four new actions verified live against the real podman API before
wiring up the UI (same discipline as the CSRF/pull/compose bugs found
earlier — field names and response shapes checked against the running
socket, not assumed from docs).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 11:56:46 +00:00
co-authored by Claude Sonnet 5
parent 5b47b4cc0a
commit 993e187f59
5 changed files with 179 additions and 5 deletions
+64
View File
@@ -229,6 +229,69 @@ window.Podman = (function () {
});
}
/**
* Small anchored dropdown menu — used for secondary per-row actions
* (pause/kill/rename/...) that would otherwise clutter a table row with
* one icon button each. Only one menu is ever open at a time.
*
* @param {HTMLElement} anchorEl button the menu opens from/closes on
* @param {Array<{label:string, danger?:boolean, disabled?:boolean, onClick?:Function}|'separator'>} items
*/
let openMenuCloser = null;
function openContextMenu(anchorEl, items) {
if (openMenuCloser) {
openMenuCloser();
return;
}
const menu = document.createElement('div');
menu.className = 'podman-context-menu';
menu.innerHTML = items.map(function (item) {
if (item === 'separator') return '<div class="podman-context-menu-sep"></div>';
return '<button type="button" class="' + (item.danger ? 'danger' : '') + '"' +
(item.disabled ? ' disabled' : '') + '>' + escapeHtml(item.label) + '</button>';
}).join('');
const rect = anchorEl.getBoundingClientRect();
menu.style.top = (rect.bottom + window.scrollY + 4) + 'px';
menu.style.left = (rect.right + window.scrollX - 180) + 'px';
(document.querySelector('.podman-plugin') || document.body).appendChild(menu);
let buttonIndex = 0;
items.forEach(function (item) {
if (item === 'separator') return;
const btn = menu.children[buttonIndex];
buttonIndex++;
if (item.disabled) return;
btn.addEventListener('click', function (e) {
e.stopPropagation();
close();
if (item.onClick) item.onClick();
});
});
function close() {
menu.remove();
document.removeEventListener('click', onOutsideClick);
document.removeEventListener('keydown', onKey);
openMenuCloser = null;
}
function onOutsideClick(e) {
if (!menu.contains(e.target)) close();
}
function onKey(e) {
if (e.key === 'Escape') close();
}
openMenuCloser = close;
// Deferred so the click that opened the menu doesn't immediately
// trigger onOutsideClick via event bubbling.
setTimeout(function () {
document.addEventListener('click', onOutsideClick);
document.addEventListener('keydown', onKey);
}, 0);
}
// --- Panel router ----------------------------------------------------------
const panelModules = {};
@@ -295,6 +358,7 @@ window.Podman = (function () {
loadingRow: loadingRow,
errorRow: errorRow,
openFormModal: openFormModal,
openContextMenu: openContextMenu,
registerPanel: registerPanel,
activatePanel: activatePanel,
};