From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id D275E1FF14F for ; Mon, 27 Apr 2026 13:14:47 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id ABED219162; Mon, 27 Apr 2026 13:14:47 +0200 (CEST) From: Dominik Csapak To: pdm-devel@lists.proxmox.com Subject: [PATCH datacenter-manager] views: catch unknown widget types Date: Mon, 27 Apr 2026 13:13:10 +0200 Message-ID: <20260427111402.2322116-1-d.csapak@proxmox.com> X-Mailer: git-send-email 2.47.3 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.051 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: XGVSGNK2YTKRDNFHIO5BPXBUUKXSGW3U X-Message-ID-Hash: XGVSGNK2YTKRDNFHIO5BPXBUUKXSGW3U X-MailFrom: d.csapak@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: If there is a mismatch between backend and frontend, (e.g. after an update or during development) deserialize unknown widget types in a way that we can still show the remaining widgets. Use serde's untagged + flatten feature so we can save all extra parameters and send it back to the backend. In the api, we reject all view updates that contain unknown widgets, since the frontend should never send any types the backend does not know. Signed-off-by: Dominik Csapak --- lib/pdm-api-types/Cargo.toml | 1 + lib/pdm-api-types/src/views.rs | 30 +++++++++++++++++++++++++++++- server/src/api/config/views.rs | 20 +++++++++++++++----- ui/src/dashboard/view.rs | 25 ++++++++++++++++++++++++- 4 files changed, 69 insertions(+), 7 deletions(-) diff --git a/lib/pdm-api-types/Cargo.toml b/lib/pdm-api-types/Cargo.toml index 7aa7b64e..0c84d301 100644 --- a/lib/pdm-api-types/Cargo.toml +++ b/lib/pdm-api-types/Cargo.toml @@ -12,6 +12,7 @@ http.workspace = true regex.workspace = true serde.workspace = true serde_plain.workspace = true +serde_json.workspace = true proxmox-acme-api.workspace = true proxmox-access-control = { workspace = true, features = ["acl"] } diff --git a/lib/pdm-api-types/src/views.rs b/lib/pdm-api-types/src/views.rs index c1885828..89281bb3 100644 --- a/lib/pdm-api-types/src/views.rs +++ b/lib/pdm-api-types/src/views.rs @@ -1,4 +1,7 @@ -use std::{fmt::Debug, fmt::Display, str::FromStr, sync::OnceLock}; +use std::collections::HashMap; +use std::fmt::{Debug, Display}; +use std::str::FromStr; +use std::sync::OnceLock; use anyhow::{bail, Error}; use const_format::concatcp; @@ -255,6 +258,24 @@ pub enum ViewLayout { }, } +impl ViewLayout { + /// Tests if this layout has unknown widget types + pub fn has_unknown_widgets(&self) -> bool { + match self { + ViewLayout::Rows { rows } => { + for row in rows { + for widget in row { + if let WidgetType::UnknownWidget { .. } = widget.r#type { + return true; + } + } + } + } + } + false + } +} + #[derive(Serialize, Deserialize, PartialEq, Clone)] #[serde(rename_all = "kebab-case")] pub struct RowWidget { @@ -312,6 +333,13 @@ pub enum WidgetType { #[serde(skip_serializing_if = "Option::is_none")] remote_type: Option, }, + #[serde(untagged)] + #[serde(rename_all = "kebab-case")] + UnknownWidget { + widget_type: String, + #[serde(flatten)] + extra: HashMap, + }, } #[derive(Serialize, Deserialize, PartialEq, Clone, Copy)] diff --git a/server/src/api/config/views.rs b/server/src/api/config/views.rs index 6f187eb3..bdae0fc1 100644 --- a/server/src/api/config/views.rs +++ b/server/src/api/config/views.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Error}; +use anyhow::{format_err, Context, Error}; use serde::{Deserialize, Serialize}; use proxmox_access_control::CachedUserInfo; @@ -97,8 +97,13 @@ pub fn add_view(view: ViewConfig, digest: Option) -> Result<(), Er let id = view.id.clone(); if !view.layout.is_empty() { - if let Err(err) = serde_json::from_str::(&view.layout) { - param_bail!("layout", "layout is not valid: '{}'", err) + match serde_json::from_str::(&view.layout) { + Ok(ViewTemplate { layout, .. }) => { + if layout.has_unknown_widgets() { + param_bail!("layout", format_err!("layout has unknown widgets")); + } + } + Err(err) => param_bail!("layout", "layout is not valid: '{}'", err), } } @@ -200,8 +205,13 @@ pub fn update_view( if let Some(layout) = view.layout { if !layout.is_empty() { - if let Err(err) = serde_json::from_str::(&layout) { - param_bail!("layout", "layout is not valid: '{}'", err) + match serde_json::from_str::(&layout) { + Ok(ViewTemplate { layout, .. }) => { + if layout.has_unknown_widgets() { + param_bail!("layout", format_err!("layout has unknown widgets")); + } + } + Err(err) => param_bail!("layout", "layout is not valid: '{}'", err), } } conf.layout = layout; diff --git a/ui/src/dashboard/view.rs b/ui/src/dashboard/view.rs index 81810664..bdf92bf6 100644 --- a/ui/src/dashboard/view.rs +++ b/ui/src/dashboard/view.rs @@ -7,12 +7,14 @@ use serde_json::{json, Value}; use yew::virtual_dom::{VComp, VNode}; use proxmox_yew_comp::percent_encoding::percent_encode_component; -use proxmox_yew_comp::{http_get, http_put}; +use proxmox_yew_comp::{http_get, http_put, Status}; use pwt::css; use pwt::prelude::*; use pwt::props::StorageLocation; use pwt::state::{PersistentState, SharedState}; +use pwt::widget::container::span; use pwt::widget::{error_message, form::FormContext, Column, Container, Progress}; +use pwt::widget::{Fa, Panel, Row}; use pwt::AsyncPool; use crate::dashboard::refresh_config_edit::{ @@ -171,6 +173,7 @@ fn render_widget( resource, remote_type, } => create_gauge_panel(*resource, *remote_type, status), + WidgetType::UnknownWidget { widget_type, .. } => create_unknown_widget_panel(widget_type), }; if let Some(title) = &item.title { @@ -284,6 +287,7 @@ fn required_api_calls(layout: &ViewLayout) -> (bool, bool, bool) { WidgetType::ResourceTree => { // each list must do it itself } + WidgetType::UnknownWidget { .. } => {} } } } @@ -674,3 +678,22 @@ pub fn add_current_view_to_search(ctx: &yew::Context, sear } } } + +fn create_unknown_widget_panel(widget_type: &str) -> Panel { + Panel::new() + .title(tr!("Unknown Widget")) + .border(true) + .with_child( + Column::new() + .class(css::FlexFit) + .class(css::JustifyContent::Center) + .class(css::AlignItems::Center) + .with_child( + Row::new() + .gap(1) + .class(css::AlignItems::Center) + .with_child(Fa::from(Status::Warning).large_2x()) + .with_child(span(tr!("Unknown Widget of type '{0}'", widget_type))), + ), + ) +} -- 2.47.3