Rework Dashboard, add toasts, container folders/icons/WebUI links, detail tabs
Dashboard: - Plain-count stat tiles (Running/Stopped/Pods/Images/Volumes/Networks) separated from a single "Resource Usage" card (CPU/Memory/Swap/Storage meter rows) instead of forcing both into one tile grid, which produced awkward spanning-tile/dead-cell layouts. - Fixed CPU usage never changing (libpod's own cpuUtilization is computed once and never resampled) by computing it from /proc/stat deltas instead. - Fixed memory usage reading far too high by using /proc/meminfo's MemAvailable instead of libpod's raw (non-reclaimable-aware) memFree. - Added an Autostart Queue table reusing podman-autostart.sh's own failure-counter files. - Dashboard and Containers now auto-refresh every ~2s (paused when the tab is hidden or a modal is open). Toasts: - Real success/warn/error/info toast notifications replacing every alert() used for one-way feedback, across every panel. Container detail modal: - 5 new tabs: Resources, Logs, Console, Events, Healthcheck. Containers panel: - Folders to group containers (name + icon), stored in the plugin's own folders.json — a folder's header always shows an icon+name+status chip per member, matching Unraid's own Docker page folders. "Move to Folder" becomes "Remove from Folder" once a container is already grouped. - Containers can carry an icon URL and a WebUI URL (small button next to the name), both stored as container labels and auto-filled from templates where applicable. - Settings: an "Add container" control for the Autostart order table. Fixes: - Context menus now measure their own rendered size and flip above the anchor when there isn't room below, instead of running off-screen. - Containers table now uses table-layout:fixed with explicit column widths — auto layout was shifting every column (and the header) on every folder expand/collapse, and briefly again when a flex wrapper was mistakenly placed directly on a <td>. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -126,6 +126,60 @@ window.Podman = (function () {
|
||||
return '<tr><td colspan="' + colspan + '" class="podman-error">' + escapeHtml(message) + '</td></tr>';
|
||||
}
|
||||
|
||||
// --- Toast notifications ----------------------------------------------------
|
||||
//
|
||||
// Replaces alert() for one-way feedback ("Saved.", "Removed 3 image(s)",
|
||||
// "Save failed: ..."). Confirmations stay native confirm() — a toast is
|
||||
// for telling the user something happened, not for asking them a
|
||||
// yes/no question. Command output that can run to hundreds of lines
|
||||
// (e.g. `podman compose up`) stays in the existing openLogModal()
|
||||
// pattern instead — a toast has to stay short and auto-dismiss, which
|
||||
// doesn't fit a scrolling log.
|
||||
|
||||
const TOAST_ICON = { success: '✓', warn: '!', error: '✕', info: 'ℹ' };
|
||||
const TOAST_DURATION_MS = { success: 4000, warn: 5000, error: 7000, info: 4000 };
|
||||
let toastContainer = null;
|
||||
|
||||
function toastRoot() {
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.className = 'podman-toast-container';
|
||||
(document.querySelector('.podman-plugin') || document.body).appendChild(toastContainer);
|
||||
}
|
||||
return toastContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
* @param {('success'|'warn'|'error'|'info')} [type='info']
|
||||
*/
|
||||
function toast(message, type) {
|
||||
const kind = TOAST_ICON[type] ? type : 'info';
|
||||
const root = toastRoot();
|
||||
|
||||
const node = document.createElement('div');
|
||||
node.className = 'podman-toast podman-toast-' + kind;
|
||||
node.innerHTML =
|
||||
'<span class="ico">' + TOAST_ICON[kind] + '</span>' +
|
||||
'<span class="msg"></span>' +
|
||||
'<button type="button" class="close" aria-label="Dismiss">✕</button>';
|
||||
node.querySelector('.msg').textContent = message;
|
||||
root.appendChild(node);
|
||||
|
||||
let dismissed = false;
|
||||
function dismiss() {
|
||||
if (dismissed) return;
|
||||
dismissed = true;
|
||||
node.classList.add('leaving');
|
||||
// Not animationend — that never fires when prefers-reduced-motion
|
||||
// disables the animation (podman.css sets animation:none for it),
|
||||
// which would leave the toast stuck on screen forever.
|
||||
setTimeout(function () { node.remove(); }, 160);
|
||||
}
|
||||
node.querySelector('.close').addEventListener('click', dismiss);
|
||||
setTimeout(dismiss, TOAST_DURATION_MS[kind]);
|
||||
}
|
||||
|
||||
// --- Modal form dialog -----------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -299,12 +353,32 @@ window.Podman = (function () {
|
||||
(item.disabled ? ' disabled' : '') + '>' + escapeHtml(item.label) + '</button>';
|
||||
}).join('');
|
||||
|
||||
(document.querySelector('.podman-plugin') || document.body).appendChild(menu);
|
||||
|
||||
// Viewport-relative (see the "position: fixed" comment on
|
||||
// .podman-context-menu in podman.css) — no scrollY/scrollX added.
|
||||
// Measured AFTER appending (not assumed) since the menu's height
|
||||
// varies with its item count — a long menu (e.g. a container's row
|
||||
// menu with Details/Pause/Kill/Rename/Edit/Remove) opened from a row
|
||||
// near the bottom of the viewport used to run off-screen with no way
|
||||
// to reach its last few items. Flips above the anchor instead when
|
||||
// there isn't enough room below, and clamps horizontally the same way.
|
||||
const rect = anchorEl.getBoundingClientRect();
|
||||
menu.style.top = (rect.bottom + 4) + 'px';
|
||||
menu.style.left = (rect.right - 180) + 'px';
|
||||
(document.querySelector('.podman-plugin') || document.body).appendChild(menu);
|
||||
const menuRect = menu.getBoundingClientRect();
|
||||
const margin = 8;
|
||||
|
||||
let top = rect.bottom + 4;
|
||||
if (top + menuRect.height > window.innerHeight - margin) {
|
||||
top = rect.top - menuRect.height - 4;
|
||||
}
|
||||
top = Math.max(margin, top);
|
||||
|
||||
let left = rect.right - menuRect.width;
|
||||
left = Math.min(left, window.innerWidth - menuRect.width - margin);
|
||||
left = Math.max(margin, left);
|
||||
|
||||
menu.style.top = top + 'px';
|
||||
menu.style.left = left + 'px';
|
||||
|
||||
// menu.children includes the separator <div>s too, so indexing into it
|
||||
// directly (by a counter that only advances for real items) drifts by
|
||||
@@ -399,6 +473,29 @@ window.Podman = (function () {
|
||||
// (Dashboard, by default — see Podman.page).
|
||||
const initial = document.querySelector('.podman-subnav button.active');
|
||||
activatePanel(initial ? initial.dataset.panel : 'dashboard');
|
||||
|
||||
setInterval(autoRefreshTick, AUTO_REFRESH_INTERVAL_MS);
|
||||
}
|
||||
|
||||
// Only panels that opt in via `autoRefresh: true` (Dashboard, Containers)
|
||||
// get polled — most panels (Settings, Compose, Terminal, ...) have
|
||||
// in-progress forms or connections an unexpected refresh would disrupt.
|
||||
// Paused while the tab is hidden (nothing to look at) and while any
|
||||
// modal is open (a full-panel re-render mid-edit would be jarring),
|
||||
// rather than fighting those cases with more state.
|
||||
const AUTO_REFRESH_INTERVAL_MS = 2000;
|
||||
|
||||
function autoRefreshTick() {
|
||||
if (document.hidden) return;
|
||||
if (document.querySelector('.podman-modal-backdrop')) return;
|
||||
|
||||
const activeBtn = document.querySelector('.podman-subnav button.active');
|
||||
if (!activeBtn) return;
|
||||
const name = activeBtn.dataset.panel;
|
||||
const module = panelModules[name];
|
||||
if (module && module.autoRefresh && initialized[name] && typeof module.refresh === 'function') {
|
||||
module.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', boot);
|
||||
@@ -414,6 +511,7 @@ window.Podman = (function () {
|
||||
stateChipClass: stateChipClass,
|
||||
loadingRow: loadingRow,
|
||||
errorRow: errorRow,
|
||||
toast: toast,
|
||||
openFormModal: openFormModal,
|
||||
openLogModal: openLogModal,
|
||||
openContextMenu: openContextMenu,
|
||||
|
||||
Reference in New Issue
Block a user