v0.36.9
This commit is contained in:
1 parent
358a4518d9
commit
2789991452
13 files changed
+215
-6
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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"),
|
||||
");",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
})();
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user