* [PATCH datacenter-manager v2] views: catch unknown widget types
@ 2026-05-04 10:12 Dominik Csapak
2026-05-04 12:37 ` applied: " Lukas Wagner
0 siblings, 1 reply; 2+ messages in thread
From: Dominik Csapak @ 2026-05-04 10:12 UTC (permalink / raw)
To: pdm-devel
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 <d.csapak@proxmox.com>
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Tested-by: Lukas Wagner <l.wagner@proxmox.com>
---
lib/pdm-api-types/Cargo.toml | 1 +
lib/pdm-api-types/src/views.rs | 32 +++++++++++++++++++++++++++++++-
server/src/api/config/views.rs | 20 +++++++++++++++-----
ui/src/dashboard/view.rs | 25 ++++++++++++++++++++++++-
4 files changed, 71 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..3e215d06 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,15 @@ pub enum WidgetType {
#[serde(skip_serializing_if = "Option::is_none")]
remote_type: Option<RemoteType>,
},
+ #[serde(untagged)]
+ #[serde(rename_all = "kebab-case")]
+ /// Catches all widgets for unknown types.
+ /// This can happen for example if the frontend is not the same version as the backend.
+ UnknownWidget {
+ widget_type: String,
+ #[serde(flatten)]
+ extra: HashMap<String, serde_json::Value>,
+ },
}
#[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<ConfigDigest>) -> Result<(), Er
let id = view.id.clone();
if !view.layout.is_empty() {
- if let Err(err) = serde_json::from_str::<ViewTemplate>(&view.layout) {
- param_bail!("layout", "layout is not valid: '{}'", err)
+ match serde_json::from_str::<ViewTemplate>(&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::<ViewTemplate>(&layout) {
- param_bail!("layout", "layout is not valid: '{}'", err)
+ match serde_json::from_str::<ViewTemplate>(&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<T: yew::Component>(ctx: &yew::Context<T>, 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
^ permalink raw reply related [flat|nested] 2+ messages in thread* applied: [PATCH datacenter-manager v2] views: catch unknown widget types
2026-05-04 10:12 [PATCH datacenter-manager v2] views: catch unknown widget types Dominik Csapak
@ 2026-05-04 12:37 ` Lukas Wagner
0 siblings, 0 replies; 2+ messages in thread
From: Lukas Wagner @ 2026-05-04 12:37 UTC (permalink / raw)
To: pdm-devel, Dominik Csapak
On Mon, 04 May 2026 12:12:28 +0200, Dominik Csapak wrote:
> 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.
>
> [...]
Applied, thanks!
[1/1] views: catch unknown widget types
commit: e19ebafd2843d49d15c53179184d4fc4e9d57ec6
Best regards,
--
Lukas Wagner <l.wagner@proxmox.com>
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-05-04 12:39 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-04 10:12 [PATCH datacenter-manager v2] views: catch unknown widget types Dominik Csapak
2026-05-04 12:37 ` applied: " Lukas Wagner
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox