* [PATCH datacenter-manager] views: catch unknown widget types
@ 2026-04-27 11:13 Dominik Csapak
0 siblings, 0 replies; only message in thread
From: Dominik Csapak @ 2026-04-27 11:13 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>
---
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<RemoteType>,
},
+ #[serde(untagged)]
+ #[serde(rename_all = "kebab-case")]
+ 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] only message in thread
only message in thread, other threads:[~2026-04-27 11:14 UTC | newest]
Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-27 11:13 [PATCH datacenter-manager] views: catch unknown widget types Dominik Csapak
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox