fix(dashboard): heights fit content to 10px, not a whole 70px row; text columns read from the left (#5214)
View mode splits each saved row into seven, so a widget no longer carries up to 69px of empty space below its content (Overview, SNMP). Edit mode and the saved layout keep 70px rows. The Busiest Containers host column aligns left like its header, its note fits one line, and the Hosts column reads Disk. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<table class="w-table">
|
<table class="w-table">
|
||||||
<colgroup><col><col class="w-wide-only" style="width:7.5rem"><col style="width:5.5rem"><col style="width:6.5rem"></colgroup>
|
<colgroup><col><col class="w-wide-only" style="width:7.5rem"><col style="width:5.5rem"><col style="width:6.5rem"></colgroup>
|
||||||
<thead><tr><th>Container</th><th class="w-wide-only">Host</th><th>CPU</th><th>Memory</th></tr></thead>
|
<thead><tr><th>Container</th><th class="w-wide-only w-text">Host</th><th>CPU</th><th>Memory</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for r in rows %}
|
{% for r in rows %}
|
||||||
{% set u = r.usage %}
|
{% set u = r.usage %}
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<td class="w-name" title="{{ r.c.name }}">
|
<td class="w-name" title="{{ r.c.name }}">
|
||||||
{%- if session.user_id %}<a href="/plugins/docker/container/{{ r.c.host_id }}/{{ r.c.name }}">{{ r.label }}</a>{% else %}{{ r.label }}{% endif -%}
|
{%- if session.user_id %}<a href="/plugins/docker/container/{{ r.c.host_id }}/{{ r.c.name }}">{{ r.label }}</a>{% else %}{{ r.label }}{% endif -%}
|
||||||
</td>
|
</td>
|
||||||
<td class="w-wide-only" title="{{ r.host_name }}">{{ r.host_name }}</td>
|
<td class="w-wide-only w-text" title="{{ r.host_name }}">{{ r.host_name }}</td>
|
||||||
{% if u.cores is none %}<td>—</td>{% else %}
|
{% if u.cores is none %}<td>—</td>{% else %}
|
||||||
<td class="w-meter tone-{{ status_class(u.cpu_share, 'cpu') if u.cpus else 'dim' }}" style="--pct:{{ [[u.cpu_share or 0, 0]|max, 100]|min }}%"
|
<td class="w-meter tone-{{ status_class(u.cpu_share, 'cpu') if u.cpus else 'dim' }}" style="--pct:{{ [[u.cpu_share or 0, 0]|max, 100]|min }}%"
|
||||||
title="{% if u.cpus %}{{ '%.1f'|format(u.cpu_share) }}% of {{ r.host_name }}'s {{ u.cpus }} cores{% else %}The host's core count isn't known{% endif %}">
|
title="{% if u.cpus %}{{ '%.1f'|format(u.cpu_share) }}% of {{ r.host_name }}'s {{ u.cpus }} cores{% else %}The host's core count isn't known{% endif %}">
|
||||||
@@ -37,5 +37,5 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="w-note">CPU over the last {{ minutes }} minutes. Shading: share of the host's cores, and of the memory limit (or the host's memory when no limit is set).</div>
|
<div class="w-note" title="Shading: share of the host's cores, and of the memory limit (or the host's memory when no limit is set).">CPU over the last {{ minutes }} min; shading is the share of the host.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -7,9 +7,14 @@
|
|||||||
side by side; none is stretched. Edit mode always shows the saved 12-column
|
side by side; none is stretched. Edit mode always shows the saved 12-column
|
||||||
layout, so what is saved never depends on the screen it was edited on.
|
layout, so what is saved never depends on the screen it was edited on.
|
||||||
|
|
||||||
|
Outside edit mode each saved row is split into ROW_SPLIT finer rows, so a
|
||||||
|
widget's height fits its content to within 10px rather than to a whole
|
||||||
|
70px row (#5214). viewed() gives the saved place in those rows.
|
||||||
|
|
||||||
Used by the dashboard (Gridstack) and the share view (CSS grid). */
|
Used by the dashboard (Gridstack) and the share view (CSS grid). */
|
||||||
(function () {
|
(function () {
|
||||||
var COL_PX = 110, MIN_COLS = 12, MAX_COLS = 24, NARROW_PX = 700, MIN_ROWS = 2;
|
var COL_PX = 110, MIN_COLS = 12, MAX_COLS = 24, NARROW_PX = 700;
|
||||||
|
var ROW_SPLIT = 7, MIN_ROWS = ROW_SPLIT; // view rows: one saved row is the least
|
||||||
|
|
||||||
function columnsFor(width) {
|
function columnsFor(width) {
|
||||||
if (width < NARROW_PX) return 1;
|
if (width < NARROW_PX) return 1;
|
||||||
@@ -74,9 +79,9 @@
|
|||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Grid rows for that height: rowPx is one row plus the gap that follows it,
|
/* View rows for that height: rowPx is one view row (with its share of the
|
||||||
gapPx the space a cell loses to its margins. Capped at the saved height,
|
gap), gapPx the space a cell loses to its margins. Capped at maxRows, the
|
||||||
where the body scrolls instead. */
|
saved height in view rows, where the body scrolls instead. */
|
||||||
function fitRows(box, rowPx, gapPx, maxRows) {
|
function fitRows(box, rowPx, gapPx, maxRows) {
|
||||||
var rows = Math.ceil((naturalHeight(box) + gapPx) / rowPx);
|
var rows = Math.ceil((naturalHeight(box) + gapPx) / rowPx);
|
||||||
return Math.max(MIN_ROWS, Math.min(maxRows, rows));
|
return Math.max(MIN_ROWS, Math.min(maxRows, rows));
|
||||||
@@ -86,7 +91,13 @@
|
|||||||
return { x: +el.dataset.x, y: +el.dataset.y, w: +el.dataset.w, h: +el.dataset.h };
|
return { x: +el.dataset.x, y: +el.dataset.y, w: +el.dataset.w, h: +el.dataset.h };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function viewed(el) {
|
||||||
|
var s = saved(el);
|
||||||
|
return { x: s.x, y: s.y * ROW_SPLIT, w: s.w, h: s.h * ROW_SPLIT };
|
||||||
|
}
|
||||||
|
|
||||||
window.StewardDashLayout = {
|
window.StewardDashLayout = {
|
||||||
MIN_COLS: MIN_COLS, columnsFor: columnsFor, pack: pack, fitRows: fitRows, saved: saved,
|
MIN_COLS: MIN_COLS, ROW_SPLIT: ROW_SPLIT, columnsFor: columnsFor, pack: pack,
|
||||||
|
fitRows: fitRows, saved: saved, viewed: viewed,
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -390,6 +390,7 @@ body.dash-editing .widget-drawer { transform:translateY(0); }
|
|||||||
.w-table th { text-align:right; font-weight:500; font-size:var(--fs-size-tiny); color:var(--text-dim); text-transform:uppercase; letter-spacing:var(--fs-tracking-tiny); padding:0 0 0.25rem 0.5rem; white-space:nowrap; }
|
.w-table th { text-align:right; font-weight:500; font-size:var(--fs-size-tiny); color:var(--text-dim); text-transform:uppercase; letter-spacing:var(--fs-tracking-tiny); padding:0 0 0.25rem 0.5rem; white-space:nowrap; }
|
||||||
.w-table td { text-align:right; padding:0.2rem 0 0.2rem 0.5rem; border-top:1px solid var(--border); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
.w-table td { text-align:right; padding:0.2rem 0 0.2rem 0.5rem; border-top:1px solid var(--border); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||||
.w-table th:first-child, .w-table td:first-child { text-align:left; padding-left:0; }
|
.w-table th:first-child, .w-table td:first-child { text-align:left; padding-left:0; }
|
||||||
|
.w-table .w-text { text-align:left; } /* a text column after the name: reads from the left, like its header */
|
||||||
.w-table td.w-name { overflow:hidden; text-overflow:ellipsis; }
|
.w-table td.w-name { overflow:hidden; text-overflow:ellipsis; }
|
||||||
.w-table td.w-name a { color:inherit; text-decoration:none; }
|
.w-table td.w-name a { color:inherit; text-decoration:none; }
|
||||||
.w-table td.w-name a:hover { text-decoration:underline; }
|
.w-table td.w-name a:hover { text-decoration:underline; }
|
||||||
|
|||||||
@@ -68,8 +68,9 @@
|
|||||||
|
|
||||||
var L = window.StewardDashLayout;
|
var L = window.StewardDashLayout;
|
||||||
var CELL_PX = 70, MARGIN_PX = 8;
|
var CELL_PX = 70, MARGIN_PX = 8;
|
||||||
|
var VIEW_PX = CELL_PX / L.ROW_SPLIT; // view mode: finer rows, so heights fit closely
|
||||||
var grid = GridStack.init({
|
var grid = GridStack.init({
|
||||||
column: 12, cellHeight: CELL_PX, margin: MARGIN_PX, float: false,
|
column: 12, cellHeight: VIEW_PX, margin: MARGIN_PX, float: false,
|
||||||
staticGrid: true, // view mode: positioned but inert
|
staticGrid: true, // view mode: positioned but inert
|
||||||
handle: '.panel-drag',
|
handle: '.panel-drag',
|
||||||
resizable: { handles: 'e, se, s' },
|
resizable: { handles: 'e, se, s' },
|
||||||
@@ -84,7 +85,7 @@
|
|||||||
var cols = L.columnsFor(gridEl.clientWidth);
|
var cols = L.columnsFor(gridEl.clientWidth);
|
||||||
var els = items();
|
var els = items();
|
||||||
var pos = L.pack(els.map(function (el) {
|
var pos = L.pack(els.map(function (el) {
|
||||||
var s = L.saved(el);
|
var s = L.viewed(el);
|
||||||
s.id = el.getAttribute('gs-id');
|
s.id = el.getAttribute('gs-id');
|
||||||
s.h = el._fitH || s.h;
|
s.h = el._fitH || s.h;
|
||||||
return s;
|
return s;
|
||||||
@@ -100,7 +101,7 @@
|
|||||||
function fit(el) {
|
function fit(el) {
|
||||||
var box = el.querySelector('.grid-stack-item-content');
|
var box = el.querySelector('.grid-stack-item-content');
|
||||||
if (!box) return;
|
if (!box) return;
|
||||||
var rows = L.fitRows(box, CELL_PX, 2 * MARGIN_PX, L.saved(el).h);
|
var rows = L.fitRows(box, VIEW_PX, 2 * MARGIN_PX, L.viewed(el).h);
|
||||||
if (rows !== el._fitH) { el._fitH = rows; relayoutSoon(); }
|
if (rows !== el._fitH) { el._fitH = rows; relayoutSoon(); }
|
||||||
}
|
}
|
||||||
document.body.addEventListener('htmx:afterSettle', function (e) {
|
document.body.addEventListener('htmx:afterSettle', function (e) {
|
||||||
@@ -135,6 +136,7 @@
|
|||||||
// Edit mode shows the saved 12-column layout at saved heights, so a layout
|
// Edit mode shows the saved 12-column layout at saved heights, so a layout
|
||||||
// edited on a wide screen is the same one a narrow screen starts from.
|
// edited on a wide screen is the same one a narrow screen starts from.
|
||||||
function showSaved() {
|
function showSaved() {
|
||||||
|
grid.cellHeight(CELL_PX);
|
||||||
grid.batchUpdate();
|
grid.batchUpdate();
|
||||||
if (grid.getColumn() !== L.MIN_COLS) grid.column(L.MIN_COLS, 'none');
|
if (grid.getColumn() !== L.MIN_COLS) grid.column(L.MIN_COLS, 'none');
|
||||||
items().forEach(function (el) { grid.update(el, L.saved(el)); });
|
items().forEach(function (el) { grid.update(el, L.saved(el)); });
|
||||||
@@ -148,7 +150,7 @@
|
|||||||
document.body.classList.toggle('dash-editing', on);
|
document.body.classList.toggle('dash-editing', on);
|
||||||
var btn = document.getElementById('dash-edit-toggle');
|
var btn = document.getElementById('dash-edit-toggle');
|
||||||
if (btn) btn.textContent = on ? 'Done' : 'Edit';
|
if (btn) btn.textContent = on ? 'Done' : 'Edit';
|
||||||
if (!on) { items().forEach(fit); relayout(); }
|
if (!on) { grid.cellHeight(VIEW_PX); items().forEach(fit); relayout(); }
|
||||||
}
|
}
|
||||||
var toggle = document.getElementById('dash-edit-toggle');
|
var toggle = document.getElementById('dash-edit-toggle');
|
||||||
if (toggle) toggle.addEventListener('click', function () { setEditing(!editing); });
|
if (toggle) toggle.addEventListener('click', function () { setEditing(!editing); });
|
||||||
|
|||||||
@@ -19,10 +19,12 @@
|
|||||||
.share-header { background: var(--bg-card); padding: 0 1.5rem; display: flex; align-items: center; justify-content: space-between; gap: 1rem; border-bottom: 1px solid var(--border-mid); min-height: 48px; }
|
.share-header { background: var(--bg-card); padding: 0 1.5rem; display: flex; align-items: center; justify-content: space-between; gap: 1rem; border-bottom: 1px solid var(--border-mid); min-height: 48px; }
|
||||||
.share-name { font-family: var(--fs-font-display); font-weight: 500; font-size: 1.15rem; color: var(--gold); overflow-wrap: anywhere; }
|
.share-name { font-family: var(--fs-font-display); font-weight: 500; font-size: 1.15rem; color: var(--gold); overflow-wrap: anywhere; }
|
||||||
.share-main { max-width: 2400px; }
|
.share-main { max-width: 2400px; }
|
||||||
/* The same layout as the signed-in dashboard: rows 70px, gaps 1rem, and
|
/* The same layout as the signed-in dashboard: a saved row is 70px plus a
|
||||||
static/dash-layout.js sets the columns and each cell's place and height. */
|
1rem gap, split into ROW_SPLIT view rows (the gap is each cell's bottom
|
||||||
.dash-grid { display: grid; grid-template-columns: repeat(12, 1fr); grid-auto-rows: 70px; gap: 1rem; }
|
margin), and static/dash-layout.js sets the columns and each cell's
|
||||||
.dash-grid > .dash-cell { margin-bottom: 0; min-width: 0; overflow: hidden; display: flex; flex-direction: column; }
|
place and height. */
|
||||||
|
.dash-grid { display: grid; grid-template-columns: repeat(12, 1fr); grid-auto-rows: calc((70px + 1rem) / 7); column-gap: 1rem; row-gap: 0; }
|
||||||
|
.dash-grid > .dash-cell { margin-bottom: 1rem; min-width: 0; overflow: hidden; display: flex; flex-direction: column; }
|
||||||
.dash-cell .dash-cell-body { flex: 1; min-height: 0; overflow: auto; }
|
.dash-cell .dash-cell-body { flex: 1; min-height: 0; overflow: auto; }
|
||||||
@media (max-width: 820px) {
|
@media (max-width: 820px) {
|
||||||
.share-main { padding: 1rem; }
|
.share-main { padding: 1rem; }
|
||||||
@@ -69,12 +71,12 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
var L = window.StewardDashLayout, grid = document.querySelector('.dash-grid');
|
var L = window.StewardDashLayout, grid = document.querySelector('.dash-grid');
|
||||||
if (!L || !grid) return;
|
if (!L || !grid) return;
|
||||||
var ROW_PX = 70, GAP_PX = 16;
|
var GAP_PX = 16, VIEW_PX = (70 + GAP_PX) / L.ROW_SPLIT;
|
||||||
function cells() { return Array.prototype.slice.call(grid.querySelectorAll('.dash-cell')); }
|
function cells() { return Array.prototype.slice.call(grid.querySelectorAll('.dash-cell')); }
|
||||||
function relayout() {
|
function relayout() {
|
||||||
var cols = L.columnsFor(grid.clientWidth);
|
var cols = L.columnsFor(grid.clientWidth);
|
||||||
var pos = L.pack(cells().map(function (el) {
|
var pos = L.pack(cells().map(function (el) {
|
||||||
var s = L.saved(el); s.id = el.dataset.id; s.h = el._fitH || s.h; return s;
|
var s = L.viewed(el); s.id = el.dataset.id; s.h = el._fitH || s.h; return s;
|
||||||
}), cols);
|
}), cols);
|
||||||
grid.style.gridTemplateColumns = 'repeat(' + cols + ', 1fr)';
|
grid.style.gridTemplateColumns = 'repeat(' + cols + ', 1fr)';
|
||||||
cells().forEach(function (el) {
|
cells().forEach(function (el) {
|
||||||
@@ -85,7 +87,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
}
|
}
|
||||||
var pending = null;
|
var pending = null;
|
||||||
function fit(el) {
|
function fit(el) {
|
||||||
var rows = L.fitRows(el, ROW_PX + GAP_PX, GAP_PX, L.saved(el).h);
|
var rows = L.fitRows(el, VIEW_PX, GAP_PX, L.viewed(el).h);
|
||||||
if (rows !== el._fitH) { el._fitH = rows; clearTimeout(pending); pending = setTimeout(relayout, 60); }
|
if (rows !== el._fitH) { el._fitH = rows; clearTimeout(pending); pending = setTimeout(relayout, 60); }
|
||||||
}
|
}
|
||||||
document.body.addEventListener('htmx:afterSettle', function (e) {
|
document.body.addEventListener('htmx:afterSettle', function (e) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<table class="w-table">
|
<table class="w-table">
|
||||||
<colgroup><col><col style="width:3.6rem"><col style="width:3.6rem"><col style="width:3.6rem"><col style="width:6.5rem"></colgroup>
|
<colgroup><col><col style="width:3.6rem"><col style="width:3.6rem"><col style="width:3.6rem"><col style="width:6.5rem"></colgroup>
|
||||||
<thead><tr><th>Host</th><th>CPU</th><th>Mem</th><th>Disk /</th><th>Load</th></tr></thead>
|
<thead><tr><th>Host</th><th>CPU</th><th>Mem</th><th title="Root filesystem">Disk</th><th>Load</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for r in rows %}
|
{% for r in rows %}
|
||||||
{% set a = r.agent %}{% set fresh = a.get('fresh') %}
|
{% set a = r.agent %}{% set fresh = a.get('fresh') %}
|
||||||
|
|||||||
Reference in New Issue
Block a user