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:
@@ -186,12 +186,25 @@
|
||||
@media (max-width: 1080px) { .podman-stat-grid { grid-template-columns: repeat(3, 1fr); } }
|
||||
@media (max-width: 620px) { .podman-stat-grid { grid-template-columns: repeat(2, 1fr); } }
|
||||
|
||||
.podman-stat { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; box-shadow: var(--shadow); }
|
||||
.podman-stat {
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px;
|
||||
box-shadow: var(--shadow); transition: transform .12s ease, box-shadow .12s ease;
|
||||
}
|
||||
.podman-stat:hover { transform: translateY(-1px); box-shadow: 0 2px 4px rgba(20, 22, 26, .08), 0 8px 20px rgba(20, 22, 26, .08); }
|
||||
.podman-stat .label { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-faint); font-weight: 700; }
|
||||
.podman-stat .value { font-size: 24px; font-weight: 700; margin-top: 6px; }
|
||||
.podman-stat .value small { font-size: 13px; color: var(--text-dim); font-weight: 600; }
|
||||
.podman-stat .bar { height: 5px; border-radius: 3px; background: var(--surface-3); margin-top: 10px; overflow: hidden; }
|
||||
.podman-stat .bar > span { display: block; height: 100%; background: var(--accent); border-radius: 3px; }
|
||||
.podman-stat .tone-good { color: var(--good); }
|
||||
.podman-stat .tone-warn { color: var(--warn); }
|
||||
.podman-stat .tone-bad { color: var(--bad); }
|
||||
.podman-usage-mini .track > span.warn { background: var(--warn); }
|
||||
.podman-usage-mini .track > span.bad { background: var(--bad); }
|
||||
|
||||
.podman-resource-rows { margin-top: 8px; }
|
||||
.podman-resource-row { display: flex; align-items: center; gap: 12px; padding: 7px 0; }
|
||||
.podman-resource-row + .podman-resource-row { border-top: 1px solid var(--border); }
|
||||
.podman-resource-row .k { font-size: 12.5px; color: var(--text-dim); font-weight: 600; min-width: 84px; flex: none; }
|
||||
.podman-resource-row .v { font-size: 12.5px; color: var(--text-faint); font-weight: 600; white-space: nowrap; flex: none; font-variant-numeric: tabular-nums; }
|
||||
|
||||
.podman-chip {
|
||||
display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border-radius: 100px;
|
||||
@@ -212,7 +225,46 @@
|
||||
.podman-plugin tbody tr:last-child td { border-bottom: none; }
|
||||
.podman-plugin tbody tr:hover { background: var(--surface-2); }
|
||||
.podman-table-wrap { overflow-x: auto; }
|
||||
.podman-row-name { display: flex; align-items: center; gap: 10px; font-weight: 600; }
|
||||
/*
|
||||
* table-layout: auto (the default) sizes every column from the widest
|
||||
* content across only the CURRENTLY VISIBLE rows — so expanding/
|
||||
* collapsing a folder (which adds/removes rows) changed what counted as
|
||||
* "widest" and shifted every column, including the header text, on
|
||||
* every toggle (found live). Fixed widths (set on the <th>s in
|
||||
* Podman.page) make column sizing depend only on those, never on row
|
||||
* content, so toggling a folder can no longer move anything.
|
||||
*/
|
||||
#containers-table { table-layout: fixed; }
|
||||
/* Only the plain-text columns truncate with an ellipsis (Image/Ports —
|
||||
both are just escaped text directly in the <td>, so ellipsis renders
|
||||
correctly) — NOT the Name column (its text sits inside a flex
|
||||
button+icon, where overflow:hidden would hard-clip instead of
|
||||
ellipsis-truncate), the folder-header row's colspan cell (its member
|
||||
chips need to wrap, see .podman-folder-members), or the actions
|
||||
column (icon buttons, not text, would otherwise get clipped if they
|
||||
ever didn't quite fit). */
|
||||
#containers-table tbody > tr:not(.podman-folder-row) > td:nth-child(3),
|
||||
#containers-table tbody > tr:not(.podman-folder-row) > td:nth-child(5) {
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
/* .podman-name-cell is a <div> INSIDE the <td>, not the <td> itself —
|
||||
display:flex directly on a table cell pulls it out of table-cell
|
||||
layout entirely, so with table-layout:fixed it stops respecting the
|
||||
column width the <th> assigns and throws off alignment with every
|
||||
other column (found live: the whole Name column visibly drifted).
|
||||
The div gives the same flex row (so a WebUI link, see
|
||||
.podman-webui-link, sits on the same line as the name button instead
|
||||
of wrapping below it — .podman-row-name-btn's own display:flex makes
|
||||
IT block-level by default, which would otherwise push everything
|
||||
after it in the cell onto its own line) without that side effect.
|
||||
min-width:0 lets the button actually shrink instead of forcing the
|
||||
cell wider than its fixed column width; .text (not the plain text
|
||||
node it used to be) is what actually ellipsis-truncates, since
|
||||
text-overflow only applies to the element whose own inline content
|
||||
overflows, not a nested flex child's. */
|
||||
.podman-name-cell { display: flex; align-items: center; gap: 6px; }
|
||||
.podman-row-name { display: flex; align-items: center; gap: 10px; font-weight: 600; min-width: 0; }
|
||||
.podman-row-name .text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.podman-row-name-btn {
|
||||
/* !important for the same reason as .podman-btn-ghost/-primary — Unraid's
|
||||
own site-wide button theme otherwise still shows its default border
|
||||
@@ -220,14 +272,16 @@
|
||||
click away from every table row still looked like a bordered button
|
||||
forever, not a plain label. */
|
||||
appearance: none; border: none !important; background: none !important; padding: 0; cursor: pointer;
|
||||
color: var(--text); font-family: var(--font-ui); font-size: 13px; text-align: left;
|
||||
color: var(--text); font-family: var(--font-ui); font-size: 13px; text-align: left; min-width: 0;
|
||||
}
|
||||
.podman-row-name-btn:hover { color: var(--accent-strong); }
|
||||
.podman-row-name-btn:hover .ico { border-color: var(--accent); }
|
||||
.podman-row-name .ico {
|
||||
width: 26px; height: 26px; border-radius: 6px; flex: none; background: var(--surface-3);
|
||||
display: grid; place-items: center; font-size: 12px; border: 1px solid var(--border); color: var(--text-dim);
|
||||
overflow: hidden;
|
||||
}
|
||||
.podman-row-name .ico img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.podman-row-sub { font-size: 11.5px; color: var(--text-faint); font-weight: 500; margin-top: 1px; }
|
||||
/*
|
||||
* The actions <td> itself stays a plain table-cell (default display) so
|
||||
@@ -265,7 +319,7 @@
|
||||
.podman-usage-mini { display: flex; align-items: center; gap: 8px; min-width: 110px; }
|
||||
.podman-usage-mini .track { flex: 1; height: 5px; border-radius: 3px; background: var(--surface-3); overflow: hidden; }
|
||||
.podman-usage-mini .track > span { display: block; height: 100%; background: var(--accent); }
|
||||
.podman-usage-mini .num { font-size: 11.5px; color: var(--text-dim); width: 34px; text-align: right; }
|
||||
.podman-usage-mini .num { font-size: 11.5px; color: var(--text-dim); width: 34px; text-align: right; font-variant-numeric: tabular-nums; }
|
||||
|
||||
.podman-toolbar { display: flex; align-items: center; gap: 10px; padding: 14px 18px; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
|
||||
/*
|
||||
@@ -459,6 +513,14 @@
|
||||
|
||||
.podman-badge-update { font-size: 10px; font-weight: 700; color: var(--accent-strong); background: color-mix(in srgb, var(--accent) 16%, transparent); padding: 2px 7px; border-radius: 100px; margin-left: 8px; }
|
||||
|
||||
.podman-webui-link {
|
||||
display: inline-flex; align-items: center; justify-content: center; height: 20px; padding: 0 7px;
|
||||
border-radius: 5px; color: var(--text-faint); text-decoration: none; font-size: 10px; font-weight: 700;
|
||||
letter-spacing: .03em; flex: none; border: 1px solid var(--border); background: var(--surface-3);
|
||||
vertical-align: middle;
|
||||
}
|
||||
.podman-webui-link:hover { color: var(--accent-strong); border-color: var(--accent); }
|
||||
|
||||
.podman-loading, .podman-error { padding: 32px 18px; text-align: center; color: var(--text-faint); font-size: 13px; }
|
||||
|
||||
/**
|
||||
@@ -581,3 +643,84 @@
|
||||
padding: 14px 16px; border-radius: 8px; white-space: pre-wrap; word-break: break-all; line-height: 1.6;
|
||||
}
|
||||
.podman-detail-empty { color: var(--text-faint); font-size: 12.5px; padding: 8px 0; }
|
||||
|
||||
/*
|
||||
* Toast notifications — replaces alert() for one-way feedback (saved,
|
||||
* removed, failed, ...) across every panel. Confirmations stay native
|
||||
* confirm() (a decision, not a notice); long-running command output
|
||||
* stays in the existing log-modal pattern (a toast has to stay short to
|
||||
* be readable while it's animating/auto-dismissing). z-index above the
|
||||
* modal backdrop (1000) so a toast fired from within a modal's own
|
||||
* action (e.g. "Save failed") is never hidden behind it.
|
||||
*/
|
||||
.podman-toast-container {
|
||||
position: fixed; right: 20px; bottom: 20px; z-index: 1100;
|
||||
display: flex; flex-direction: column-reverse; gap: 10px; pointer-events: none;
|
||||
max-width: min(380px, calc(100vw - 40px));
|
||||
}
|
||||
.podman-toast {
|
||||
pointer-events: auto; display: flex; align-items: flex-start; gap: 10px;
|
||||
background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--neutral);
|
||||
border-radius: 9px; box-shadow: var(--shadow); padding: 12px 14px; font-size: 13px; color: var(--text);
|
||||
animation: podman-toast-in .18s ease-out;
|
||||
}
|
||||
.podman-toast.leaving { animation: podman-toast-out .16s ease-in forwards; }
|
||||
.podman-toast-success { border-left-color: var(--good); }
|
||||
.podman-toast-warn { border-left-color: var(--warn); }
|
||||
.podman-toast-error { border-left-color: var(--bad); }
|
||||
.podman-toast .ico { flex: none; margin-top: 1px; font-size: 15px; line-height: 1; }
|
||||
.podman-toast-success .ico { color: var(--good); }
|
||||
.podman-toast-warn .ico { color: var(--warn); }
|
||||
.podman-toast-error .ico { color: var(--bad); }
|
||||
.podman-toast .msg { flex: 1; line-height: 1.45; word-break: break-word; }
|
||||
.podman-toast .close {
|
||||
appearance: none; background: none; border: none; color: var(--text-faint); cursor: pointer;
|
||||
font-size: 15px; line-height: 1; padding: 0; flex: none; font-family: var(--font-ui);
|
||||
}
|
||||
.podman-toast .close:hover { color: var(--text); }
|
||||
@keyframes podman-toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes podman-toast-out { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(8px); } }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.podman-toast, .podman-toast.leaving { animation: none; }
|
||||
}
|
||||
|
||||
/* Container folders — a group header row spanning the full table width,
|
||||
sitting between normal <tr>s in the same <tbody> (see containers.js's
|
||||
renderTable()) rather than a separate nested table, so column
|
||||
alignment with member rows stays free. */
|
||||
.podman-folder-row td { padding: 0; background: var(--surface-2); border-bottom: 1px solid var(--border); }
|
||||
.podman-folder-head { display: flex; align-items: center; gap: 14px; padding: 9px 16px; flex-wrap: wrap; }
|
||||
.podman-folder-toggle {
|
||||
appearance: none; background: none; border: none; display: flex; align-items: center; gap: 10px;
|
||||
cursor: pointer; font-family: var(--font-ui); color: var(--text); padding: 0; flex: none; text-align: left;
|
||||
}
|
||||
.podman-folder-toggle .chevron { color: var(--text-faint); font-size: 10px; width: 10px; flex: none; }
|
||||
.podman-folder-toggle .ico {
|
||||
width: 22px; height: 22px; border-radius: 6px; flex: none; background: var(--surface-3);
|
||||
display: grid; place-items: center; font-size: 10.5px; border: 1px solid var(--border); color: var(--text-dim);
|
||||
overflow: hidden;
|
||||
}
|
||||
.podman-folder-toggle .ico img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.podman-folder-toggle .name { font-weight: 700; font-size: 12.5px; }
|
||||
.podman-folder-toggle .count {
|
||||
font-size: 11px; color: var(--text-faint); font-weight: 700; background: var(--surface-3);
|
||||
padding: 1px 8px; border-radius: 100px; font-variant-numeric: tabular-nums; white-space: nowrap;
|
||||
}
|
||||
|
||||
/* The always-visible per-member preview inside a folder's header row —
|
||||
see containers.js's folderMemberChipHtml(). Each chip opens that
|
||||
container's detail modal directly, without needing to expand the
|
||||
folder first. */
|
||||
.podman-folder-members { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; flex: 1; min-width: 0; }
|
||||
.podman-folder-member {
|
||||
appearance: none; display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--border);
|
||||
background: var(--surface); border-radius: 100px; padding: 3px 10px 3px 4px; cursor: pointer;
|
||||
font-family: var(--font-ui); color: var(--text-dim); font-size: 11.5px;
|
||||
}
|
||||
.podman-folder-member:hover { border-color: var(--accent); color: var(--text); }
|
||||
.podman-folder-member .ico { width: 18px; height: 18px; border-radius: 5px; font-size: 9px; }
|
||||
.podman-folder-member .name { font-weight: 600; }
|
||||
.podman-folder-member .dot { width: 6px; height: 6px; border-radius: 50%; flex: none; }
|
||||
.podman-folder-member .dot.good { background: var(--good); }
|
||||
.podman-folder-member .dot.bad { background: var(--bad); }
|
||||
.podman-folder-member .state { color: var(--text-faint); }
|
||||
|
||||
Reference in New Issue
Block a user