This commit is contained in:
Mateusz Gruszczyński committed 2026-11-09 23:55:03 +01:00
1 parent 358a4518d9
commit 2789991452
13 files changed
+215 -6

No files matched your search

+2 -1
View File
@@ -9,7 +9,8 @@ Release history focused on **features, settings, default behavior, compatibility
- **Fit to screen (auto-arrange):** a new **Fit to screen** switch in the unlocked layout toolbar (account preference `dashboard_auto_fit`, **on by default**) re-arranges a custom Overview for the current screen width, so the layout designed on a wide screen stays tidy on laptops and tablets. Each 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 that start on one row get one height, a card whose side neighbour ends slightly lower is extended to that edge (only while the dashboard grows by at most two stat tiles), remaining holes are filled by widening a neighbouring card sideways or growing it into free space above or below. When a card is placed, a free strip beside it that is narrower than every card still to be placed is taken by that card at once, and at the end of the grid a chart or panel reaches the last row instead of leaving an empty area. The saved widths, heights and order are never changed; switch it off to see the layout exactly as saved. While a block is dragged the normal flow is shown so the drop placeholder stays accurate.
- **No layout jump on reload:** the Overview grid stays invisible until the account's layout from the server has been applied and the first live data has been rendered, then appears already arranged; meanwhile the panel's startup screen (logo, spinner, "Loading RustProxy…") is shown in its place. Nothing about the layout is stored in the browser (an older cached layout key is removed).
- **Overview layout with row heights:** in a custom (unlocked/saved) layout, card heights now snap to a shared row unit — half the height of the tallest compact stat card, so stat tiles always span two rows and line up — instead of every card in a row stretching to the tallest one. A tall panel (Process, RAM cache, Network…) can sit beside several rows of small cards or charts with aligned edges. Each card automatically gets the rows its content needs; in the layout editor, the new **↕ − / +** control next to the width makes a card taller (`A` marks automatic height, lowering it to the content height returns to automatic). Charts (also with chart scales enabled), chart stacks and tables grow into the extra height, and a card is stretched over the empty cells below it down to the next card or the end of the grid, so rows end on straight edges. **Fill gaps** packs blocks with their real heights. Small content changes (for example a growing table) cannot make the grid jump: a card grows at once but shrinks only after the smaller size is seen several times. Below a 700 px wide dashboard, cards stack by content height as before.
- **Preferences API:** additive optional `dashboard_widget_heights` (widget ID → 1–32 rows) in `/api/preferences` and dashboard presets, and `dashboard_auto_fit` (boolean, default `true`) in `/api/preferences`. Older documents and presets without it keep automatic heights.
- **Collapsible left menu:** a **Collapse menu** button at the bottom of the left menu turns it into a narrow rail (logo, section numbers or icons with the section name as a tooltip, status dot). On screens wider than 1300 px the choice is stored in the account preference `sidebar_collapsed` (off by default); between 851 and 1300 px the menu is always a rail and the button opens the full menu temporarily as an overlay that closes after choosing a section, clicking outside or pressing Esc. The mobile menu (850 px and below) is unchanged. The startup screen now stays until the account preferences are applied, so the panel never appears first with default settings.
- **Preferences API:** additive optional `dashboard_widget_heights` (widget ID → 1–32 rows) in `/api/preferences` and dashboard presets, `dashboard_auto_fit` (boolean, default `true`) and `sidebar_collapsed` (boolean, default `false`) in `/api/preferences`. Older documents and presets without it keep automatic heights.
- **Upgrade notes:** no configuration or database migration is required; existing saved layouts receive the new card hidden at the end. Legacy (never customized) layouts keep their grouped rows until the layout is edited.
## 0.36.8 — editor forms, toasts and CSP fixes
+2
View File
@@ -66,6 +66,8 @@
"configHistoryChangedFields": "Changed fields only",
"overview": "Overview",
"mainMenu": "Menu",
"sidebarCollapse": "Collapse menu",
"sidebarExpand": "Expand menu",
"routing": "Legacy routes",
"configuration": "Configuration",
"configStorageLabel": "CONFIGURATION SOURCE",
+2
View File
@@ -66,6 +66,8 @@
"configHistoryChangedFields": "Tylko zmienione pola",
"overview": "Przegląd",
"mainMenu": "Menu",
"sidebarCollapse": "Zwiń menu",
"sidebarExpand": "Rozwiń menu",
"routing": "Starsze trasy",
"configuration": "Konfiguracja",
"configStorageLabel": "ŹRÓDŁO KONFIGURACJI",
+5
View File
@@ -4115,6 +4115,11 @@
"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."
},
"sidebar_collapsed": {
"type": "boolean",
"default": false,
"description": "Account-scoped: collapse the left menu to a narrow rail on wide screens. Below 1300 px the menu is always shown as a rail and can be expanded temporarily."
},
"dashboard_hidden_widgets": {
"type": "array",
"maxItems": 78,
+4
View File
@@ -147,6 +147,9 @@ struct AdminPreferences {
/// 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,
/// Left menu collapsed to a narrow rail on wide screens (off by default).
#[serde(default)]
sidebar_collapsed: 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>>,
@@ -209,6 +212,7 @@ impl Default for AdminPreferences {
dashboard_widget_sizes: None,
dashboard_widget_heights: None,
dashboard_auto_fit: true,
sidebar_collapsed: false,
file_log_columns: default_file_log_columns(),
file_log_sort: default_file_log_sort(),
file_log_descending: default_file_log_descending(),
+10
View File
@@ -382,3 +382,13 @@ fn dashboard_auto_fit_defaults_on_for_new_and_legacy_documents() {
assert!(!off.dashboard_auto_fit);
assert!(validate_admin_preferences(&off).is_ok());
}
#[test]
fn sidebar_collapsed_defaults_off_for_new_and_legacy_documents() {
assert!(!AdminPreferences::default().sidebar_collapsed);
let legacy: AdminPreferences = serde_json::from_str(r#"{"theme":"dark"}"#).unwrap();
assert!(!legacy.sidebar_collapsed);
let on: AdminPreferences = serde_json::from_str(r#"{"sidebar_collapsed":true}"#).unwrap();
assert!(on.sidebar_collapsed);
assert!(validate_admin_preferences(&on).is_ok());
}
+1
View File
@@ -65,6 +65,7 @@ fn admin_app_javascript() -> &'static str {
include_str!("web/app/rate-sampling.js"),
include_str!("web/app/chart-scales.js"),
include_str!("web/app/overview-rows.js"),
include_str!("web/app/sidebar.js"),
"const ADMIN_DASHBOARD_WIDGET_REGISTRY = Object.freeze(",
include_str!("web/dashboard-widgets.json"),
");",
+6 -2
View File
@@ -53,6 +53,7 @@ function translate() {
document.querySelectorAll("[data-i18n-placeholder]").forEach(e => e.placeholder = t(e.dataset.i18nPlaceholder));
document.querySelectorAll("[data-i18n-aria]").forEach(e => e.setAttribute("aria-label", t(e.dataset.i18nAria)));
document.querySelectorAll("[data-i18n-title]").forEach(e => { e.title = t(e.dataset.i18nTitle); });
window.rustProxySidebar?.refresh?.();
window.rustProxyAclProfiles?.refreshTranslations?.();
$("language").value = languagePreference === "auto" ? "auto" : language; setPanel(currentPanel, false); syncAuthForm(); renderSession();
window.rustProxyDrafts?.refresh();
@@ -511,10 +512,13 @@ async function activate(info) {
for (const id of ["password", "password-confirm", "setup-token"]) $(id).value = "";
$("login").hidden = true; $("workspace").hidden = false; $("logout").hidden = false;
renderSession();
document.documentElement.classList.remove("app-loading");
document.documentElement.classList.remove("app-startup-failed");
rustProxyRefresh.start();
const preferences = await loadAdminPreferences();
// Keep the startup screen until the account preferences (menu, layout, density)
// are applied, so the panel never appears first with defaults and then changes.
let preferences;
try { preferences = await loadAdminPreferences(); }
finally { document.documentElement.classList.remove("app-loading"); }
if (!authenticated || epoch !== authEpoch) return false;
window.rustProxyLogs?.applyPreferences(preferences);
window.rustProxyFileLogs?.applyPreferences(preferences);
+1 -1
View File
@@ -168,7 +168,7 @@ function uiIconForTarget(target) {
if (manual && UI_ICON_SHAPES[manual]) return manual;
if (target.matches(".chart-style-choice")) return null;
if (target.matches(".overview-inline-grip, .overview-inline-narrow, .overview-inline-widen, .overview-inline-up, .overview-inline-down, .overview-inline-hide, .overview-layout-narrow, .overview-layout-widen, .overview-layout-up, .overview-layout-down, .overview-layout-hide")) return null;
if (target.matches("#tunnels-open, .vhost-backend-protocol-badge, .config-history-day, .draft-dock-toggle, .mobile-menu-toggle")) return null;
if (target.matches("#tunnels-open, .vhost-backend-protocol-badge, .config-history-day, .draft-dock-toggle, .mobile-menu-toggle, .sidebar-collapse")) return null;
if (UI_GLYPH_LABEL.test(String(target.textContent || "").trim())) return null;
if (UI_ID_ICONS[target.id]) return UI_ID_ICONS[target.id];
if (target.matches("#web-service-preset-apply, [data-cache-memory-preset], [data-cache-cold-preset], [data-transform-preset]")) return "wand";
+3 -2
View File
@@ -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, dashboard_auto_fit: true,
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, sidebar_collapsed: false,
});
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;
@@ -316,6 +316,7 @@ function applyAdminPreferences(value, cache = true) {
window.rustProxyStreams?.applyPreferences(normalized);
if (languagePacks.size) translate();
refreshAppearanceUi();
window.rustProxySidebar?.applyPreference?.(normalized.sidebar_collapsed === true);
window.rustProxyOverviewRows?.reveal?.();
return normalized;
}
@@ -357,7 +358,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_auto_fit = next.dashboard_auto_fit !== false;
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; next.sidebar_collapsed = next.sidebar_collapsed === true;
const saved = await api("/api/preferences", { method: "PUT", body: JSON.stringify(next) });
if (!authenticated || epoch !== authEpoch) return adminPreferences;
return applyAdminPreferences(saved);
+54
View File
@@ -0,0 +1,54 @@
// Collapsible left menu. On wide screens (above 1300 px) the account preference
// sidebar_collapsed shows the menu as a narrow rail (logo, section numbers or icons,
// status dot). Between 851 and 1300 px the menu is always a rail and the toggle
// opens it temporarily as an overlay (closed by choosing a section, clicking
// outside or Esc). At 850 px and below the existing mobile menu is used.
window.rustProxySidebar = (() => {
const root = document.documentElement;
const sidebar = document.querySelector(".sidebar");
const toggle = document.getElementById("sidebar-collapse-toggle");
if (!sidebar || !toggle) return { applyPreference() { }, refresh() { } };
const desktop = window.matchMedia("(min-width: 851px)");
const rail = window.matchMedia("(min-width: 851px) and (max-width: 1300px)");
const label = key => (typeof t === "function" ? t(key) : key);
let preferred = false, peek = false;
function render() {
if (!desktop.matches || !rail.matches) peek = false;
const collapsed = desktop.matches && (rail.matches ? !peek : preferred);
root.classList.toggle("sidebar-collapsed", collapsed);
root.classList.toggle("sidebar-peek", peek);
const key = collapsed ? "sidebarExpand" : "sidebarCollapse";
toggle.setAttribute("aria-expanded", String(!collapsed));
toggle.setAttribute("aria-label", label(key));
toggle.title = label(key);
toggle.querySelector(".sidebar-collapse-label").textContent = label(key);
toggle.querySelector(".sidebar-collapse-icon").textContent = collapsed ? "»" : "«";
for (const nav of sidebar.querySelectorAll(".nav")) {
if (collapsed) nav.title = nav.querySelector("b")?.textContent.replace(/\s+/g, " ").trim() || "";
else nav.removeAttribute("title");
}
}
function setPeek(open) {
if (peek === open) return;
peek = open;
render();
}
toggle.addEventListener("click", event => {
event.stopPropagation();
if (rail.matches) { setPeek(!peek); return; }
preferred = !preferred;
render();
window.rustProxyPreferences?.update?.({ sidebar_collapsed: preferred })?.catch?.(() => { });
});
sidebar.querySelectorAll(".nav, .brand").forEach(item => item.addEventListener("click", () => setPeek(false)));
document.addEventListener("click", event => { if (peek && !sidebar.contains(event.target)) setPeek(false); });
document.addEventListener("keydown", event => { if (event.key === "Escape" && peek) { setPeek(false); toggle.focus(); } });
desktop.addEventListener?.("change", render);
rail.addEventListener?.("change", render);
render();
return {
applyPreference(value) { preferred = value === true; render(); },
refresh: render,
};
})();
+122
View File
@@ -995,3 +995,125 @@ a.brand>.brand-mark[data-logo-effect="wobble"]>span {
animation: none !important;
}
}
/* Collapsible left menu (sidebar.js). Above 850 px the menu can be a narrow rail:
html.sidebar-collapsed (account preference on wide screens, always between 851
and 1300 px) and html.sidebar-peek (rail temporarily opened as an overlay). */
.sidebar-collapse {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
margin-top: 8px;
padding: 7px 10px;
border: 1px solid var(--line);
border-radius: var(--rp-radius-control, 0);
background: transparent;
color: var(--muted);
font: 11px var(--mono);
text-align: left;
cursor: pointer;
}
.sidebar-collapse:hover,
.sidebar-collapse:focus-visible {
color: var(--text);
background: var(--rp-nav-hover-bg);
}
.sidebar-collapse-icon {
width: 14px;
font-size: 14px;
line-height: 1;
text-align: center;
}
.sidebar-collapse-label {
font-weight: 450;
white-space: nowrap;
}
.sidebar-bottom[hidden]+.sidebar-collapse {
margin-top: auto;
}
@media (min-width: 851px) {
html.sidebar-collapsed .shell,
html.sidebar-peek .shell {
grid-template-columns: 58px minmax(0, 1fr);
}
html.sidebar-peek .shell>main {
grid-column: 2;
}
html.sidebar-collapsed .sidebar {
align-items: center;
padding: 14px 7px;
}
html.sidebar-collapsed .brand>span:not(.brand-mark),
html.sidebar-collapsed .edition,
html.sidebar-collapsed .sidebar .nav b,
html.sidebar-collapsed .sidebar-collapse-label,
html.sidebar-collapsed #app-health-label {
display: none;
}
html.sidebar-collapsed .brand-mark {
width: 40px;
height: 40px;
font-size: 24px;
}
html.sidebar-collapsed .brand-mark span {
font-size: 18px;
}
html.sidebar-collapsed .sidebar nav {
width: 100%;
margin-top: 18px;
}
html.sidebar-collapsed .sidebar .nav {
justify-content: center;
gap: 0;
padding: 9px 0;
}
html.sidebar-collapsed .sidebar .nav span {
opacity: .9;
}
html.sidebar-collapsed .sidebar .nav:has(>svg.ui-icon)>span {
display: none;
}
html.sidebar-collapsed .sidebar-bottom {
width: 100%;
}
html.sidebar-collapsed .app-health-button {
justify-content: center;
}
html.sidebar-collapsed .sidebar-collapse {
justify-content: center;
padding: 7px 0;
}
html.sidebar-peek .sidebar {
position: fixed;
top: 0;
left: 0;
z-index: 80;
width: 182px;
box-shadow: 10px 0 28px var(--shadow-strong, #0009);
}
}
@media (max-width: 850px) {
.sidebar-collapse {
display: none;
}
}
+3
View File
@@ -65,6 +65,9 @@
data-i18n="appHealthSidebarChecking">CHECKING</span>
</button>
</div>
<button class="sidebar-collapse" id="sidebar-collapse-toggle" type="button" aria-controls="main-navigation"
aria-expanded="true"><span class="sidebar-collapse-icon" aria-hidden="true">«</span><b
class="sidebar-collapse-label">Collapse menu</b></button>
</aside>
<main>
<header class="topbar"><span class="breadcrumb">CONTROL ROOM <span>/</span> <b