This commit is contained in:
Mateusz Gruszczyński committed 2026-10-15 23:29:15 +02:00
1 parent 162e986d37
commit f462ca7194
26 files changed
+980 -710

No files matched your search

+103
View File
@@ -0,0 +1,103 @@
const DASHBOARD_PRESET_MAX: usize = 20;
const DASHBOARD_PRESET_NAME_MAX: usize = 64;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct DashboardPreset {
name: String,
dashboard_density: String,
dashboard_widgets: Vec<String>,
dashboard_hidden_widgets: Vec<String>,
#[serde(default)]
dashboard_widget_sizes: Option<std::collections::BTreeMap<String, u8>>,
}
fn reconcile_dashboard_layout(
order: &mut Vec<String>,
hidden: &mut Vec<String>,
sizes: &mut Option<std::collections::BTreeMap<String, u8>>,
) {
let mut seen = std::collections::HashSet::new();
order.retain(|id| {
DASHBOARD_WIDGETS.iter().any(|widget| widget.id == *id) && seen.insert(id.clone())
});
let mut seen_hidden = std::collections::HashSet::new();
hidden.retain(|id| {
DASHBOARD_WIDGETS.iter().any(|widget| widget.id == *id) && seen_hidden.insert(id.clone())
});
if let Some(sizes) = sizes.as_mut() {
sizes.retain(|id, span| {
DASHBOARD_WIDGETS
.iter()
.any(|widget| widget.id == *id && valid_dashboard_span(&widget.display_type, *span))
});
}
for widget in DASHBOARD_WIDGETS.iter() {
if !seen.contains(&widget.id) {
order.push(widget.id.clone());
if widget.default_hidden && seen_hidden.insert(widget.id.clone()) {
hidden.push(widget.id.clone());
}
}
}
}
fn reconcile_dashboard_presets(presets: &mut [DashboardPreset]) {
for preset in presets {
reconcile_dashboard_layout(
&mut preset.dashboard_widgets,
&mut preset.dashboard_hidden_widgets,
&mut preset.dashboard_widget_sizes,
);
}
}
fn valid_dashboard_preset_name(name: &str) -> bool {
!name.is_empty()
&& name.chars().count() <= DASHBOARD_PRESET_NAME_MAX
&& name.trim() == name
&& !name.chars().any(char::is_control)
}
fn valid_dashboard_preset(preset: &DashboardPreset) -> bool {
if !valid_dashboard_preset_name(&preset.name)
|| !matches!(preset.dashboard_density.as_str(), "normal" | "compact" | "ultra")
|| preset.dashboard_widgets.is_empty()
|| preset.dashboard_widgets.len() > DASHBOARD_WIDGETS.len()
|| preset.dashboard_hidden_widgets.len() > DASHBOARD_WIDGETS.len()
|| !valid_dashboard_sizes(&preset.dashboard_widget_sizes)
{
return false;
}
let mut order = std::collections::HashSet::new();
if preset.dashboard_widgets.iter().any(|id| {
!DASHBOARD_WIDGETS.iter().any(|widget| widget.id == *id) || !order.insert(id.as_str())
}) {
return false;
}
let mut hidden = std::collections::HashSet::new();
!preset.dashboard_hidden_widgets.iter().any(|id| {
!order.contains(id.as_str()) || !hidden.insert(id.as_str())
})
}
fn validate_dashboard_presets(presets: &[DashboardPreset]) -> Result<(), ApiError> {
if presets.len() > DASHBOARD_PRESET_MAX {
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"too many dashboard presets".into(),
));
}
let mut names = std::collections::HashSet::new();
for preset in presets {
if !valid_dashboard_preset(preset)
|| !names.insert(preset.name.to_lowercase())
{
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid dashboard preset".into(),
));
}
}
Ok(())
}