v0.36.9
This commit is contained in:
1 parent
fe945f51a5
commit
88728bdd5a
13 files changed
+320
-17
No files matched your search
@@ -4110,6 +4110,11 @@
|
||||
},
|
||||
"description": "Optional per-card heights in Overview row units (1-32; a compact stat card spans 2 rows). Cards without an entry size to their content; a height smaller than the content needs is raised automatically. Keys must be registered Overview widget IDs."
|
||||
},
|
||||
"dashboard_auto_fit": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Re-flow the custom Overview layout for the current screen width: cards keep a readable minimum width, holes at the end of a row are filled by widening the neighbouring cards and small steps at the bottom are closed. The saved widths and order are not changed."
|
||||
},
|
||||
"dashboard_hidden_widgets": {
|
||||
"type": "array",
|
||||
"maxItems": 78,
|
||||
|
||||
@@ -58,6 +58,9 @@ fn reconcile_dashboard_widgets(prefs: &mut AdminPreferences) {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_admin_dashboard_auto_fit() -> bool {
|
||||
true
|
||||
}
|
||||
fn default_file_log_sort() -> String {
|
||||
"time".to_owned()
|
||||
}
|
||||
@@ -141,6 +144,9 @@ struct AdminPreferences {
|
||||
dashboard_hidden_widgets: Vec<String>,
|
||||
#[serde(default)]
|
||||
dashboard_widget_sizes: Option<std::collections::BTreeMap<String, u8>>,
|
||||
/// Re-flow the custom Overview layout for the current screen width (on by default).
|
||||
#[serde(default = "default_admin_dashboard_auto_fit")]
|
||||
dashboard_auto_fit: bool,
|
||||
/// Optional per-card heights in Overview row units (1..=32, a compact stat card is 2); missing cards size to content.
|
||||
#[serde(default)]
|
||||
dashboard_widget_heights: Option<std::collections::BTreeMap<String, u8>>,
|
||||
@@ -202,6 +208,7 @@ impl Default for AdminPreferences {
|
||||
dashboard_hidden_widgets: default_hidden_dashboard_widgets(),
|
||||
dashboard_widget_sizes: None,
|
||||
dashboard_widget_heights: None,
|
||||
dashboard_auto_fit: true,
|
||||
file_log_columns: default_file_log_columns(),
|
||||
file_log_sort: default_file_log_sort(),
|
||||
file_log_descending: default_file_log_descending(),
|
||||
|
||||
@@ -372,3 +372,13 @@ fn dashboard_height_preferences_are_optional_bounded_and_reconciled() {
|
||||
assert_eq!(heights.get("requests"), Some(&2));
|
||||
assert!(validate_admin_preferences(&prefs).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dashboard_auto_fit_defaults_on_for_new_and_legacy_documents() {
|
||||
assert!(AdminPreferences::default().dashboard_auto_fit);
|
||||
let legacy: AdminPreferences = serde_json::from_str(r#"{"theme":"dark"}"#).unwrap();
|
||||
assert!(legacy.dashboard_auto_fit);
|
||||
let off: AdminPreferences = serde_json::from_str(r#"{"dashboard_auto_fit":false}"#).unwrap();
|
||||
assert!(!off.dashboard_auto_fit);
|
||||
assert!(validate_admin_preferences(&off).is_ok());
|
||||
}
|
||||
+1
-1
@@ -64,6 +64,7 @@ fn admin_app_javascript() -> &'static str {
|
||||
include_str!("web/app/refresh.js"),
|
||||
include_str!("web/app/rate-sampling.js"),
|
||||
include_str!("web/app/chart-scales.js"),
|
||||
include_str!("web/app/overview-rows.js"),
|
||||
"const ADMIN_DASHBOARD_WIDGET_REGISTRY = Object.freeze(",
|
||||
include_str!("web/dashboard-widgets.json"),
|
||||
");",
|
||||
@@ -76,7 +77,6 @@ fn admin_app_javascript() -> &'static str {
|
||||
include_str!("web/app/overview-wide-metrics.js"),
|
||||
include_str!("web/app/web-overview.js"),
|
||||
include_str!("web/app/overview-masonry.js"),
|
||||
include_str!("web/app/overview-rows.js"),
|
||||
include_str!("web/app/config-history.js"),
|
||||
include_str!("web/app/certificate-inspection.js"),
|
||||
include_str!("web/app/memory-breakdown.js"),
|
||||
|
||||
@@ -10,6 +10,7 @@ window.rustProxyInlineLayout = (() => {
|
||||
const hiddenCount = $("overview-inline-hidden-count");
|
||||
const modalButton = $("overview-layout-open");
|
||||
const compactButton = $("overview-inline-compact");
|
||||
const autoFitToggle = $("overview-inline-autofit");
|
||||
let draft = null;
|
||||
let original = null;
|
||||
let opening = false;
|
||||
@@ -29,7 +30,8 @@ window.rustProxyInlineLayout = (() => {
|
||||
const dirty = () => draft && original && (draft.order.some((id, i) => id !== original.order[i])
|
||||
|| [...draft.hidden].some(id => !original.hidden.has(id)) || [...original.hidden].some(id => !draft.hidden.has(id))
|
||||
|| JSON.stringify(draft.sizes) !== JSON.stringify(original.sizes)
|
||||
|| JSON.stringify(normalizeDashboardWidgetHeights(draft.heights)) !== JSON.stringify(normalizeDashboardWidgetHeights(original.heights)));
|
||||
|| JSON.stringify(normalizeDashboardWidgetHeights(draft.heights)) !== JSON.stringify(normalizeDashboardWidgetHeights(original.heights))
|
||||
|| draft.autoFit !== original.autoFit);
|
||||
|
||||
function renderHidden() {
|
||||
if (!hiddenList || !draft) return;
|
||||
@@ -67,7 +69,13 @@ window.rustProxyInlineLayout = (() => {
|
||||
const span = dashboardWidgetWidth(node.dataset.dashboardWidget, draft.sizes);
|
||||
const at = widths.indexOf(span);
|
||||
const value = node.querySelector(".overview-inline-width-value");
|
||||
if (value) value.textContent = `${span}/12`;
|
||||
if (value) {
|
||||
value.textContent = `${span}/12`;
|
||||
// Auto-fit may show the card wider on this screen than its saved width.
|
||||
const fitted = window.rustProxyOverviewRows?.fitFor?.(node.dataset.dashboardWidget);
|
||||
value.classList.toggle("is-fitted", !!fitted && draft.autoFit !== false);
|
||||
value.title = fitted && draft.autoFit !== false ? t("overviewLayoutAutoFitWidened").replace("{span}", String(fitted)) : t("overviewLayoutWidth");
|
||||
}
|
||||
const narrow = node.querySelector(".overview-inline-narrow");
|
||||
const widen = node.querySelector(".overview-inline-widen");
|
||||
if (narrow) narrow.disabled = at <= 0;
|
||||
@@ -180,6 +188,8 @@ window.rustProxyInlineLayout = (() => {
|
||||
hint.hidden = false;
|
||||
hint.textContent = t("overviewLayoutInlineHint");
|
||||
if (compactButton) compactButton.title = t("overviewLayoutInlineCompactHint");
|
||||
const autoFitLabel = $("overview-inline-autofit-label");
|
||||
if (autoFitLabel) autoFitLabel.title = t("overviewLayoutAutoFitHint");
|
||||
if (hiddenShelf) { hiddenShelf.hidden = false; hiddenShelf.open = false; }
|
||||
if (modalButton) modalButton.disabled = true;
|
||||
for (const node of nodes()) toolbar(node);
|
||||
@@ -198,7 +208,10 @@ window.rustProxyInlineLayout = (() => {
|
||||
if (hiddenShelf) { hiddenShelf.open = false; hiddenShelf.hidden = true; }
|
||||
if (hiddenList) hiddenList.replaceChildren();
|
||||
if (modalButton) modalButton.disabled = false;
|
||||
if (!preserveSaved && original) applyDashboardLayout(original.order, [...original.hidden], original.sizes, original.heights);
|
||||
if (!preserveSaved && original) {
|
||||
window.rustProxyOverviewRows?.setAutoFit?.(original.autoFit);
|
||||
applyDashboardLayout(original.order, [...original.hidden], original.sizes, original.heights);
|
||||
}
|
||||
draft = original = null;
|
||||
// Recompute from scratch so the saved view is exactly what was arranged.
|
||||
window.rustProxyOverviewRows?.flush?.();
|
||||
@@ -214,8 +227,10 @@ window.rustProxyInlineLayout = (() => {
|
||||
const hidden = new Set(normalizeHiddenDashboardWidgets(saved.dashboard_hidden_widgets, saved.dashboard_widgets));
|
||||
const sizes = normalizeDashboardWidgetSizes(saved.dashboard_widget_sizes);
|
||||
const heights = normalizeDashboardWidgetHeights(saved.dashboard_widget_heights);
|
||||
original = { order: [...order], hidden: new Set(hidden), sizes: sizes === null ? null : { ...sizes }, heights: heights ? { ...heights } : null };
|
||||
draft = { order: [...order], hidden: new Set(hidden), sizes: sizes === null ? null : { ...sizes }, heights: heights ? { ...heights } : null };
|
||||
const autoFit = saved.dashboard_auto_fit !== false;
|
||||
original = { order: [...order], hidden: new Set(hidden), sizes: sizes === null ? null : { ...sizes }, heights: heights ? { ...heights } : null, autoFit };
|
||||
draft = { order: [...order], hidden: new Set(hidden), sizes: sizes === null ? null : { ...sizes }, heights: heights ? { ...heights } : null, autoFit };
|
||||
if (autoFitToggle) autoFitToggle.checked = autoFit;
|
||||
showEditing();
|
||||
} catch (error) { if (error?.status !== 401) toast(error.message, true); }
|
||||
finally { opening = false; unlock.disabled = false; }
|
||||
@@ -230,6 +245,7 @@ window.rustProxyInlineLayout = (() => {
|
||||
dashboard_widgets: [...draft.order], dashboard_hidden_widgets: [...draft.hidden],
|
||||
dashboard_widget_sizes: draft.sizes === null ? null : { ...draft.sizes },
|
||||
dashboard_widget_heights: normalizeDashboardWidgetHeights(draft.heights),
|
||||
dashboard_auto_fit: draft.autoFit !== false,
|
||||
});
|
||||
finish(true);
|
||||
toast(t("overviewLayoutEditorSaved"));
|
||||
@@ -417,6 +433,12 @@ window.rustProxyInlineLayout = (() => {
|
||||
});
|
||||
}
|
||||
compactButton?.addEventListener("click", compact);
|
||||
autoFitToggle?.addEventListener("change", () => {
|
||||
if (!draft) return;
|
||||
draft.autoFit = autoFitToggle.checked;
|
||||
window.rustProxyOverviewRows?.setAutoFit?.(draft.autoFit);
|
||||
paint();
|
||||
});
|
||||
unlock?.addEventListener("click", () => { if (draft) void save(); else void begin(); });
|
||||
saveButton?.addEventListener("click", () => { void save(); });
|
||||
cancelButton?.addEventListener("click", cancel);
|
||||
@@ -426,6 +448,7 @@ window.rustProxyInlineLayout = (() => {
|
||||
order: [...draft.order], hidden: [...draft.hidden],
|
||||
sizes: draft.sizes === null ? null : { ...draft.sizes },
|
||||
heights: draft.heights ? { ...draft.heights } : null,
|
||||
autoFit: draft.autoFit !== false,
|
||||
} : null,
|
||||
cancel,
|
||||
};
|
||||
|
||||
@@ -5,14 +5,24 @@
|
||||
// (dashboard_widget_heights) can only make it taller, and charts stretch into
|
||||
// the extra space. Below the narrow breakpoint the grid falls back to content
|
||||
// height (CSS sets --overview-row-units: off).
|
||||
// With auto-fit on (the default) the layout is packed for the current screen:
|
||||
// every card keeps its saved width (but at least a readable minimum for its type),
|
||||
// cards are placed in saved order into the first free spot from the top, stat
|
||||
// tiles of one row get one height, remaining holes are filled by widening a
|
||||
// neighbouring card sideways, and small steps at the bottom are closed. The
|
||||
// saved widths, heights and order are never changed.
|
||||
window.rustProxyOverviewRows = (() => {
|
||||
const grid = document.getElementById("dashboard-overview-grid");
|
||||
// A compact stat card spans ROW_DIVISIONS rows; finer rows keep taller cards
|
||||
// close to their natural height instead of rounding up by a whole stat card.
|
||||
const MAX_ROWS = 32, ROW_DIVISIONS = 2, FALLBACK_STAT = 84, MIN_INTERVAL_MS = 1500, SHRINK_CONFIRMATIONS = 3;
|
||||
const noop = { schedule() { }, flush() { }, setHeights() { }, rowsFor() { return null; } };
|
||||
// Auto-fit (dashboard_auto_fit): the smallest readable width of a card by its
|
||||
// registry display type, in CSS pixels. Cards narrower than this on the current
|
||||
// screen get more columns; the saved widths are not changed.
|
||||
const MIN_CARD_WIDTH = { value: 150, value_chart: 210, chart: 300, panel: 340, table: 640 };
|
||||
const noop = { schedule() { }, flush() { }, setHeights() { }, setAutoFit() { }, reveal() { }, dataReady() { }, rowsFor() { return null; }, fitFor() { return null; } };
|
||||
if (!grid) return noop;
|
||||
let heights = null, frame = 0, lastRun = 0, lastWidth = 0;
|
||||
let heights = null, frame = 0, lastRun = 0, lastWidth = 0, autoFit = true;
|
||||
const state = new Map();
|
||||
const shrinkVotes = new Map();
|
||||
const cards = () => [...grid.children].filter(node => node.dataset.dashboardWidget);
|
||||
@@ -25,8 +35,41 @@ window.rustProxyOverviewRows = (() => {
|
||||
grid.classList.remove("has-row-units");
|
||||
grid.style.removeProperty("--overview-row-unit");
|
||||
for (const node of cards()) { node.style.removeProperty("--overview-rows"); delete node.dataset.dashboardRows; }
|
||||
clearFit();
|
||||
state.clear(); shrinkVotes.clear();
|
||||
}
|
||||
function clearFit() {
|
||||
grid.classList.remove("has-auto-placement");
|
||||
for (const node of cards()) {
|
||||
for (const name of ["--overview-col", "--overview-cols", "--overview-row"]) node.style.removeProperty(name);
|
||||
}
|
||||
for (const node of cards()) {
|
||||
if (node.dataset.dashboardFit === undefined) continue;
|
||||
node.style.removeProperty("--overview-widget-placement");
|
||||
delete node.dataset.dashboardFit;
|
||||
}
|
||||
}
|
||||
function setSpan(node, span, columns) {
|
||||
node.style.setProperty("--overview-widget-placement", span >= columns ? "1/-1" : `auto/span ${span}`);
|
||||
node.dataset.dashboardFit = String(Math.min(span, columns));
|
||||
}
|
||||
const spanOf = card => card.last - card.first + 1;
|
||||
function widgetType(node) {
|
||||
return typeof dashboardWidgetType === "function" ? dashboardWidgetType(node.dataset.dashboardWidget) : "value";
|
||||
}
|
||||
// Widen cards that would be narrower than their readable minimum on this screen.
|
||||
function widenToMinimum(items, geometry) {
|
||||
const columns = geometry.starts.length;
|
||||
const column = geometry.starts.length > 1 ? geometry.starts[1] - geometry.starts[0] : 0;
|
||||
if (!column) return;
|
||||
const columnGap = column - geometry.tracks[0];
|
||||
for (const node of items) {
|
||||
const card = place(node, geometry);
|
||||
const minimum = MIN_CARD_WIDTH[widgetType(node)] ?? MIN_CARD_WIDTH.value;
|
||||
const needed = Math.min(columns, Math.ceil((minimum + columnGap) / column - 0.02));
|
||||
if (needed > spanOf(card)) setSpan(node, needed, columns);
|
||||
}
|
||||
}
|
||||
function apply() {
|
||||
frame = 0;
|
||||
if (!grid.getClientRects().length || grid.classList.contains("is-overview-sorting")) return;
|
||||
@@ -35,6 +78,7 @@ window.rustProxyOverviewRows = (() => {
|
||||
lastWidth = grid.clientWidth;
|
||||
const items = cards().filter(node => !node.hidden && node.getClientRects().length);
|
||||
if (!items.length) return;
|
||||
clearFit();
|
||||
// Measure as the saved (locked) dashboard: editing styles move a few
|
||||
// controls inside cards, which must not change the layout being edited.
|
||||
const editing = grid.classList.contains("is-unlocked");
|
||||
@@ -43,6 +87,10 @@ window.rustProxyOverviewRows = (() => {
|
||||
// Bounding boxes include CSS zoom (Settings → Interface scale); grid tracks,
|
||||
// gaps and the row unit are in CSS pixels, so divide measurements by it.
|
||||
const zoom = zoomOf();
|
||||
if (autoFit) {
|
||||
const preliminary = gridGeometry(1, zoom);
|
||||
if (preliminary) widenToMinimum(items, preliminary);
|
||||
}
|
||||
const natural = items.map(node => node.getBoundingClientRect().height / zoom);
|
||||
grid.classList.remove("is-measuring-rows");
|
||||
if (editing) grid.classList.add("is-unlocked");
|
||||
@@ -76,9 +124,132 @@ window.rustProxyOverviewRows = (() => {
|
||||
grid.classList.add("has-row-units");
|
||||
const geometry = gridGeometry(unit + gap, zoom);
|
||||
if (!geometry) return;
|
||||
if (autoFit) { packCards(items, geometry, isStat); return; }
|
||||
equalizeStatRows(items.filter(isStat), geometry);
|
||||
alignBottom(items, geometry);
|
||||
}
|
||||
// Auto-fit packing on a model of the grid (columns × rows), then explicit
|
||||
// placement through CSS variables. Pure arithmetic: no layout passes per card.
|
||||
function packCards(items, geometry, isStat) {
|
||||
const columns = geometry.starts.length;
|
||||
const cardsModel = items.map(node => {
|
||||
const flow = place(node, geometry);
|
||||
return { node, stat: isStat(node), w: Math.min(columns, spanOf(flow)), design: spanOf(flow), h: rowsOf(node), r: 0, c: 0 };
|
||||
});
|
||||
let occupancy = [];
|
||||
const free = (r, c, w, h) => {
|
||||
for (let y = r; y < r + h; y++) {
|
||||
const row = occupancy[y];
|
||||
if (!row) continue;
|
||||
for (let x = c; x < c + w; x++) if (row[x]) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const mark = (r, c, w, h, value = 1) => {
|
||||
for (let y = r; y < r + h; y++) {
|
||||
const row = occupancy[y] || (occupancy[y] = new Uint8Array(columns));
|
||||
for (let x = c; x < c + w; x++) row[x] = value;
|
||||
}
|
||||
};
|
||||
const pack = () => {
|
||||
occupancy = [];
|
||||
for (const card of cardsModel) {
|
||||
let placed = false;
|
||||
for (let r = 0; !placed; r++) {
|
||||
for (let c = 0; c + card.w <= columns; c++) {
|
||||
if (free(r, c, card.w, card.h)) { card.r = r; card.c = c; mark(r, c, card.w, card.h); placed = true; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// Stat tiles starting on the same row share the tallest height; re-pack until
|
||||
// stable (raising a row can move later cards).
|
||||
const settle = () => {
|
||||
for (let pass = 0; pass < 6; pass++) {
|
||||
pack();
|
||||
const tallest = new Map();
|
||||
for (const card of cardsModel) if (card.stat) tallest.set(card.r, Math.max(tallest.get(card.r) || 0, card.h));
|
||||
let changed = false;
|
||||
for (const card of cardsModel) {
|
||||
const target = card.stat ? tallest.get(card.r) : card.h;
|
||||
if (target !== card.h) { card.h = target; changed = true; }
|
||||
}
|
||||
if (!changed) break;
|
||||
}
|
||||
return Math.max(...cardsModel.map(card => card.r + card.h));
|
||||
};
|
||||
let total = settle();
|
||||
// A card whose side neighbour ends slightly lower (up to one stat tile) is
|
||||
// extended to that edge, so the cards below start on one line instead of a
|
||||
// staggered notch. Each extension is kept only while the whole dashboard
|
||||
// grows by at most two stat tiles in total, so it can never cascade.
|
||||
const snap = ROW_DIVISIONS, budget = total + ROW_DIVISIONS * 2;
|
||||
for (let attempt = 0; attempt < cardsModel.length; attempt++) {
|
||||
const candidates = [];
|
||||
for (const card of cardsModel) {
|
||||
const end = card.r + card.h;
|
||||
let best = end;
|
||||
for (const other of cardsModel) {
|
||||
const touching = other.c === card.c + card.w || other.c + other.w === card.c;
|
||||
const otherEnd = other.r + other.h;
|
||||
if (touching && other.r < end && otherEnd > end && otherEnd - end <= snap) best = Math.max(best, otherEnd);
|
||||
}
|
||||
if (best !== end && !card.snapTried) candidates.push([card, best - card.r]);
|
||||
}
|
||||
if (!candidates.length) break;
|
||||
const [card, height] = candidates[0];
|
||||
card.snapTried = true;
|
||||
const saved = cardsModel.map(item => item.h), previous = card.h;
|
||||
card.h = height;
|
||||
const next = settle();
|
||||
if (next > budget) { cardsModel.forEach((item, index) => { item.h = saved[index]; }); card.h = previous; total = settle(); }
|
||||
else total = next;
|
||||
}
|
||||
// Holes beside a card (over its whole height) are taken by widening it,
|
||||
// first to the right, then to the left.
|
||||
const order = [...cardsModel].sort((a, b) => a.r - b.r || a.c - b.c);
|
||||
const columnFree = (x, r, h) => {
|
||||
for (let y = r; y < r + h; y++) if (occupancy[y]?.[x]) return false;
|
||||
return true;
|
||||
};
|
||||
for (const card of order) {
|
||||
while (card.c + card.w < columns && columnFree(card.c + card.w, card.r, card.h)) { mark(card.r, card.c + card.w, 1, card.h); card.w++; }
|
||||
while (card.c > 0 && columnFree(card.c - 1, card.r, card.h)) { card.c--; card.w++; mark(card.r, card.c, 1, card.h); }
|
||||
}
|
||||
// Free cells directly above a card (left by a taller neighbour that started
|
||||
// earlier) are taken by growing it upwards, within the same limit.
|
||||
const rowFree = (y, c, w) => {
|
||||
if (y < 0) return false;
|
||||
for (let x = c; x < c + w; x++) if (occupancy[y]?.[x]) return false;
|
||||
return true;
|
||||
};
|
||||
for (const card of order) {
|
||||
let up = 0;
|
||||
while (up < Math.max(ROW_DIVISIONS * 2, card.h) && rowFree(card.r - up - 1, card.c, card.w)) up++;
|
||||
if (up) { mark(card.r - up, card.c, card.w, up); card.r -= up; card.h += up; }
|
||||
}
|
||||
// Steps below a card (up to two stat tiles or its own height) are closed by
|
||||
// stretching it down to the next card or the end of the grid; charts and
|
||||
// stacks inside grow into the extra height.
|
||||
const bottom = Math.max(...cardsModel.map(card => card.r + card.h));
|
||||
for (const card of order) {
|
||||
const end = card.r + card.h;
|
||||
let next = bottom;
|
||||
for (const other of cardsModel) {
|
||||
if (other !== card && other.c < card.c + card.w && other.c + other.w > card.c && other.r >= end) next = Math.min(next, other.r);
|
||||
}
|
||||
const extra = next - end;
|
||||
if (extra > 0 && extra <= Math.max(ROW_DIVISIONS * 2, card.h)) { mark(end, card.c, card.w, extra); card.h += extra; }
|
||||
}
|
||||
for (const card of cardsModel) {
|
||||
card.node.style.setProperty("--overview-col", String(card.c + 1));
|
||||
card.node.style.setProperty("--overview-cols", String(card.w));
|
||||
card.node.style.setProperty("--overview-row", String(card.r + 1));
|
||||
setRows(card.node, card.h);
|
||||
if (card.w !== card.design) card.node.dataset.dashboardFit = String(card.w);
|
||||
}
|
||||
grid.classList.add("has-auto-placement");
|
||||
}
|
||||
function setRows(node, rows) {
|
||||
if (node.dataset.dashboardRows === String(rows)) return;
|
||||
node.style.setProperty("--overview-rows", String(rows));
|
||||
@@ -99,7 +270,7 @@ window.rustProxyOverviewRows = (() => {
|
||||
const starts = [];
|
||||
tracks.reduce((x, width) => { starts.push(x); return x + width + columnGap; }, 0);
|
||||
return {
|
||||
starts, step, zoom,
|
||||
starts, step, zoom, tracks,
|
||||
left: (parseFloat(style.borderLeftWidth) || 0) + (parseFloat(style.paddingLeft) || 0),
|
||||
top: (parseFloat(style.borderTopWidth) || 0) + (parseFloat(style.paddingTop) || 0),
|
||||
};
|
||||
@@ -167,6 +338,26 @@ window.rustProxyOverviewRows = (() => {
|
||||
heights = value && typeof value === "object" ? { ...value } : null;
|
||||
schedule(true);
|
||||
}
|
||||
// The grid is shown (see is-layout-pending) once the saved layout has been
|
||||
// applied (reveal) and the first live data has been rendered (dataReady), and
|
||||
// the layout is computed from scratch at that moment, so nothing moves after.
|
||||
let layoutReady = false, contentReady = false;
|
||||
function show() {
|
||||
if (!layoutReady || !contentReady || !grid.classList.contains("is-layout-pending")) return;
|
||||
flush();
|
||||
grid.classList.remove("is-layout-pending");
|
||||
}
|
||||
function reveal() { layoutReady = true; show(); }
|
||||
function dataReady() { contentReady = true; show(); }
|
||||
// Never leave the dashboard hidden if preferences cannot be loaded.
|
||||
setTimeout(() => grid.classList.contains("is-layout-pending") && grid.classList.remove("is-layout-pending"), 4000);
|
||||
function setAutoFit(value) {
|
||||
const next = value !== false;
|
||||
if (next === autoFit) return;
|
||||
autoFit = next;
|
||||
grid.dataset.overviewAutoFit = String(autoFit);
|
||||
schedule(true);
|
||||
}
|
||||
if (typeof ResizeObserver === "function") {
|
||||
new ResizeObserver(() => {
|
||||
if (grid.clientWidth && grid.clientWidth !== lastWidth) { shrinkVotes.clear(); schedule(true); }
|
||||
@@ -177,6 +368,13 @@ window.rustProxyOverviewRows = (() => {
|
||||
schedule,
|
||||
flush,
|
||||
setHeights,
|
||||
setAutoFit,
|
||||
reveal,
|
||||
dataReady,
|
||||
fitFor(id) {
|
||||
const node = cards().find(item => item.dataset.dashboardWidget === id);
|
||||
return node?.dataset.dashboardFit ? Number(node.dataset.dashboardFit) : null;
|
||||
},
|
||||
rowsFor(id) {
|
||||
return grid.classList.contains("has-row-units") ? state.get(id) || null : null;
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ const ADMIN_DEFAULT_HIDDEN_WIDGETS = Object.freeze([...new Set([
|
||||
const ADMIN_APPEARANCE_DEFAULTS = Object.freeze({
|
||||
theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", chart_style: "default",
|
||||
chart_axes: false, interface_icons: false, logo_animation: false,
|
||||
button_style: "normal", corner_style: "default", table_density: "normal", dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_DEFAULT_HIDDEN_WIDGETS], dashboard_widget_sizes: null, dashboard_widget_heights: null,
|
||||
button_style: "normal", corner_style: "default", table_density: "normal", dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_DEFAULT_HIDDEN_WIDGETS], dashboard_widget_sizes: null, dashboard_widget_heights: null, dashboard_auto_fit: true,
|
||||
});
|
||||
const CHART_STYLE_KEYS = Object.freeze({ default: "chartStyleDefault", soft: "chartStyleSoft", technical: "chartStyleTechnical", high_contrast: "chartStyleHighContrast", neon: "chartStyleNeon", monochrome: "chartStyleMonochrome", warm: "chartStyleWarm", cool: "chartStyleCool", sunset: "chartStyleSunset", aurora: "chartStyleAurora", ocean: "chartStyleOcean", forest: "chartStyleForest", pastel: "chartStylePastel", terminal: "chartStyleTerminal", blueprint: "chartStyleBlueprint", colorblind: "chartStyleColorblind" });
|
||||
let adminPreferences = null, adminPreferencesLoading = null, adminPreferencesWrite = Promise.resolve(), adminPreferencesUnavailable = false;
|
||||
@@ -295,6 +295,8 @@ function applyAdminPreferences(value, cache = true) {
|
||||
const savedHidden = normalizeHiddenDashboardWidgets(value.dashboard_hidden_widgets, value.dashboard_widgets);
|
||||
const savedSizes = normalizeDashboardWidgetSizes(value.dashboard_widget_sizes);
|
||||
const savedHeights = normalizeDashboardWidgetHeights(value.dashboard_widget_heights);
|
||||
const savedAutoFit = value.dashboard_auto_fit !== false;
|
||||
window.rustProxyOverviewRows?.setAutoFit?.(editing ? editing.autoFit !== false : savedAutoFit);
|
||||
if (editing) applyDashboardLayout(editing.order, editing.hidden, editing.sizes, editing.heights);
|
||||
else applyDashboardLayout(savedOrder, savedHidden, savedSizes, savedHeights);
|
||||
const normalized = {
|
||||
@@ -302,7 +304,7 @@ function applyAdminPreferences(value, cache = true) {
|
||||
refresh: normalizeRefreshPreferences(value.refresh),
|
||||
theme: applyAdminTheme(value.theme, cache), language: applyAdminLanguage(value.language, cache, false), ui_scale: applyAdminUiScale(value.ui_scale, cache),
|
||||
time_format: applyAdminTimeFormat(value.time_format), chart_style: applyAdminChartStyle(value.chart_style), chart_axes: applyAdminChartAxes(value.chart_axes), interface_icons: applyAdminInterfaceIcons(value.interface_icons), logo_animation: applyAdminLogoAnimation(value.logo_animation), button_style: applyAdminButtonStyle(value.button_style), corner_style: applyAdminCornerStyle(value.corner_style, cache), table_density: applyAdminTableDensity(value.table_density),
|
||||
dashboard_density: applyAdminDashboardDensity(value.dashboard_density), dashboard_widgets: savedOrder, dashboard_hidden_widgets: savedHidden, dashboard_widget_sizes: savedSizes, dashboard_widget_heights: savedHeights,
|
||||
dashboard_density: applyAdminDashboardDensity(value.dashboard_density), dashboard_widgets: savedOrder, dashboard_hidden_widgets: savedHidden, dashboard_widget_sizes: savedSizes, dashboard_widget_heights: savedHeights, dashboard_auto_fit: savedAutoFit,
|
||||
};
|
||||
adminPreferences = normalized;
|
||||
window.rustProxyRefresh?.update(normalized.refresh);
|
||||
@@ -313,11 +315,16 @@ function applyAdminPreferences(value, cache = true) {
|
||||
window.rustProxyNotFound?.applyPreferences(normalized);
|
||||
window.rustProxyStreams?.applyPreferences(normalized);
|
||||
if (languagePacks.size) translate();
|
||||
refreshAppearanceUi(); return normalized;
|
||||
refreshAppearanceUi();
|
||||
window.rustProxyOverviewRows?.reveal?.();
|
||||
return normalized;
|
||||
}
|
||||
try { applyAdminTheme(localStorage.getItem("rp-theme") || "dark", false); } catch { applyAdminTheme("dark", false); }
|
||||
try { applyAdminUiScale(localStorage.getItem("rp-ui-scale") || 100, false); } catch { applyAdminUiScale(100, false); }
|
||||
try { applyAdminCornerStyle(localStorage.getItem("rp-corner-style") || "default", false); } catch { applyAdminCornerStyle("default", false); }
|
||||
// No Overview layout is kept in the browser: the grid stays invisible until the
|
||||
// account's layout from the server is applied (rustProxyOverviewRows.reveal).
|
||||
try { localStorage.removeItem("rp-dashboard-layout"); } catch { }
|
||||
|
||||
async function loadAdminPreferences(force = false) {
|
||||
if (!authenticated) return adminPreferences;
|
||||
@@ -350,7 +357,7 @@ function updateAdminPreferences(patch) {
|
||||
next.refresh = normalizeRefreshPreferences(next.refresh);
|
||||
next.theme = normalizeAdminTheme(next.theme); next.language = normalizeAdminLanguage(next.language); next.ui_scale = normalizeAdminUiScale(next.ui_scale);
|
||||
next.time_format = normalizeAdminTimeFormat(next.time_format); next.chart_style = normalizeAdminChartStyle(next.chart_style); next.chart_axes = next.chart_axes === true; next.interface_icons = next.interface_icons === true; next.logo_animation = next.logo_animation === true; next.button_style = normalizeAdminButtonStyle(next.button_style); next.corner_style = normalizeAdminCornerStyle(next.corner_style); next.table_density = normalizeAdminDensity(next.table_density);
|
||||
next.dashboard_density = normalizeAdminDensity(next.dashboard_density, "compact"); next.dashboard_widgets = normalizeDashboardWidgets(next.dashboard_widgets); next.dashboard_hidden_widgets = normalizeHiddenDashboardWidgets(next.dashboard_hidden_widgets); next.dashboard_widget_sizes = normalizeDashboardWidgetSizes(next.dashboard_widget_sizes); next.dashboard_widget_heights = normalizeDashboardWidgetHeights(next.dashboard_widget_heights);
|
||||
next.dashboard_density = normalizeAdminDensity(next.dashboard_density, "compact"); next.dashboard_widgets = normalizeDashboardWidgets(next.dashboard_widgets); next.dashboard_hidden_widgets = normalizeHiddenDashboardWidgets(next.dashboard_hidden_widgets); next.dashboard_widget_sizes = normalizeDashboardWidgetSizes(next.dashboard_widget_sizes); next.dashboard_widget_heights = normalizeDashboardWidgetHeights(next.dashboard_widget_heights); next.dashboard_auto_fit = next.dashboard_auto_fit !== false;
|
||||
const saved = await api("/api/preferences", { method: "PUT", body: JSON.stringify(next) });
|
||||
if (!authenticated || epoch !== authEpoch) return adminPreferences;
|
||||
return applyAdminPreferences(saved);
|
||||
@@ -481,9 +488,9 @@ async function resetAppearancePatch(patch, control, messageKey = "appearanceSect
|
||||
document.querySelectorAll("[data-appearance-reset]").forEach(control => control.addEventListener("click", () => {
|
||||
const patches = {
|
||||
interface: { theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", interface_icons: false, logo_animation: false, button_style: "normal", corner_style: "default" }, charts: { chart_style: "default", chart_axes: false }, tables: { table_density: "normal" },
|
||||
dashboard: { dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets], dashboard_widget_sizes: null, dashboard_widget_heights: null },
|
||||
dashboard: { dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets], dashboard_widget_sizes: null, dashboard_widget_heights: null, dashboard_auto_fit: true },
|
||||
};
|
||||
void resetAppearancePatch(patches[control.dataset.appearanceReset] || {}, control);
|
||||
}));
|
||||
$("appearance-reset")?.addEventListener("click", () => void resetAppearancePatch({ ...ADMIN_APPEARANCE_DEFAULTS, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets], dashboard_widget_sizes: null, dashboard_widget_heights: null }, $("appearance-reset"), "appearanceResetDone"));
|
||||
$("appearance-reset")?.addEventListener("click", () => void resetAppearancePatch({ ...ADMIN_APPEARANCE_DEFAULTS, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets], dashboard_widget_sizes: null, dashboard_widget_heights: null, dashboard_auto_fit: true }, $("appearance-reset"), "appearanceResetDone"));
|
||||
refreshAppearanceUi();
|
||||
@@ -667,6 +667,7 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
||||
renderProcessOverview(d, status, live, systemSample);
|
||||
window.rustProxyMemoryBreakdown?.render(status, appendSample);
|
||||
window.rustProxyOverviewRows?.schedule();
|
||||
window.rustProxyOverviewRows?.dataReady?.();
|
||||
updateCacheMemorySizing();
|
||||
const networkRateSupported = live.network_rx_bytes_per_second != null && live.network_tx_bytes_per_second != null;
|
||||
if (systemSample) setOptionalMetric("system-network", d.network_rx_bytes_per_second != null && d.network_tx_bytes_per_second != null, `${fmtBytes(d.network_rx_bytes_per_second)} / ${fmtBytes(d.network_tx_bytes_per_second)}`);
|
||||
|
||||
@@ -1442,6 +1442,32 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* "Fit to screen" switch in the unlocked layout toolbar. */
|
||||
.overview-inline-autofit {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 28px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--rp-radius-control, 0);
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.overview-inline-autofit input {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Width shown wider on this screen by Fit to screen. */
|
||||
#panel-overview .overview-inline-width-value.is-fitted {
|
||||
color: var(--accent);
|
||||
text-decoration: underline dotted;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
/* Height value: readable between its − / + buttons. Automatic height (fits the
|
||||
content) is slightly dimmed, a fixed height is shown in full colour. */
|
||||
#panel-overview .dashboard-overview-grid.is-unlocked.has-row-units .overview-inline-width-value.overview-inline-height-value {
|
||||
@@ -2246,6 +2272,19 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
/* Auto-fit: overview-rows.js packs the cards and places them explicitly. While a
|
||||
block is being dragged the normal flow is used, so the drop placeholder works. */
|
||||
#panel-overview #dashboard-overview-grid.has-row-units.has-auto-placement:not(.is-overview-sorting)>[data-dashboard-widget]:not([hidden]) {
|
||||
grid-column: var(--overview-col, auto)/span var(--overview-cols, 1);
|
||||
grid-row: var(--overview-row, auto)/span var(--overview-rows, 1);
|
||||
}
|
||||
|
||||
/* Until the account's layout is applied the grid stays invisible, so the page
|
||||
never shows the factory arrangement first and then jumps (no browser storage). */
|
||||
#panel-overview .dashboard-overview-grid.is-layout-pending {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid.is-measuring-rows>[data-dashboard-widget] {
|
||||
align-self: start !important;
|
||||
}
|
||||
|
||||
@@ -147,6 +147,11 @@
|
||||
<button class="quiet" id="overview-inline-compact" type="button"
|
||||
data-i18n="overviewLayoutInlineCompact"
|
||||
title="Move blocks into empty grid space once">Fill gaps</button>
|
||||
<label class="overview-inline-autofit" id="overview-inline-autofit-label"
|
||||
title="Re-flow this layout for the current screen width">
|
||||
<input id="overview-inline-autofit" type="checkbox" checked>
|
||||
<span data-i18n="overviewLayoutAutoFit">Fit to screen</span>
|
||||
</label>
|
||||
<button class="quiet" id="overview-inline-cancel" type="button"
|
||||
data-i18n="overviewLayoutInlineCancel">Cancel changes</button>
|
||||
</div>
|
||||
@@ -164,7 +169,7 @@
|
||||
id="overview-inline-hidden-count"></span></summary>
|
||||
<div class="overview-inline-hidden-list" id="overview-inline-hidden-list"></div>
|
||||
</details>
|
||||
<div class="stats-grid dashboard-overview-grid" id="dashboard-overview-grid">
|
||||
<div class="stats-grid dashboard-overview-grid is-layout-pending" id="dashboard-overview-grid">
|
||||
<!-- RP_METRICS_MAIN --><!-- RP_CHARTS_MAIN --><!-- RP_METRICS_WEB --><!-- RP_CHARTS_WEB -->
|
||||
<article class="card" data-dashboard-widget="cache_diagnostics">
|
||||
<div class="mini-heading">
|
||||
|
||||
Reference in new issue
Block a user