all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [RFC PATCH datacenter-manager 1/2] lib: pdm-client: add method for /cluster/options
@ 2026-04-17 14:10 Dominik Csapak
  2026-04-17 14:10 ` [RFC PATCH datacenter-manager 2/2] ui: pve: load and use tag style overrides for resource tree Dominik Csapak
  0 siblings, 1 reply; 2+ messages in thread
From: Dominik Csapak @ 2026-04-17 14:10 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 lib/pdm-client/src/lib.rs | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/lib/pdm-client/src/lib.rs b/lib/pdm-client/src/lib.rs
index 1565869c..954fae4d 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -567,6 +567,11 @@ impl<T: HttpApiClient> PdmClient<T> {
         Ok(self.0.get(&query).await?.expect_json()?.data)
     }
 
+    pub async fn pve_cluster_options(&self, remote: &str) -> Result<Value, Error> {
+        let url = format!("/api2/extjs/pve/remotes/{remote}/options");
+        Ok(self.0.get(&url).await?.expect_json()?.data)
+    }
+
     pub async fn pve_cluster_updates(&self, remote: &str) -> Result<RemoteUpdateSummary, Error> {
         let url = format!("/api2/extjs/pve/remotes/{remote}/updates");
         Ok(self.0.get(&url).await?.expect_json()?.data)
-- 
2.47.3





^ permalink raw reply	[flat|nested] 2+ messages in thread

* [RFC PATCH datacenter-manager 2/2] ui: pve: load and use tag style overrides for resource tree
  2026-04-17 14:10 [RFC PATCH datacenter-manager 1/2] lib: pdm-client: add method for /cluster/options Dominik Csapak
@ 2026-04-17 14:10 ` Dominik Csapak
  0 siblings, 0 replies; 2+ messages in thread
From: Dominik Csapak @ 2026-04-17 14:10 UTC (permalink / raw)
  To: pdm-devel

This loads the tag style overrides (currently only the color), from
/cluster/options once per remote when the remotes panel is opened,
and uses the color-map to change the appearance of the tags.

Since the /cluster/options return value is just a `Value` currently,
introduce our own `TagStyle` and `DataCenterOptions` struct for parsing.
(This can be dropped when we update the return schema and types in
pve-manager and redo the return type in the pdm api)

These will be converted into a `TagStyleOverride` struct that contains
the necessary infos and interfaces and can be cloned cheaply (as it uses
an Rc for the internal HashMap) so that we can easily move this around
in the code, e.g. into closures and callbacks.

The only downside to this is, that the global tree (e.g. in the search
bar or a view) does not have these overrides and the tags can be
different.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
Not sure if we want to use the per remote color/style overrides in PDM
at all, so I only included the color for now and sent as RFC. If we want
to include the style, sorting, etc. it should be easy to include.

If we want this, I'd also go and fix the return types in pve-manager,
and update pve-api-types of course. Until then we could leave this in as
a stopgap.

 ui/src/pve/mod.rs              | 75 +++++++++++++++++++++++++++++++++-
 ui/src/pve/tree.rs             | 19 +++++++--
 ui/src/pve/utils.rs            | 22 +++++++---
 ui/src/widget/resource_tree.rs | 15 ++++---
 4 files changed, 116 insertions(+), 15 deletions(-)

diff --git a/ui/src/pve/mod.rs b/ui/src/pve/mod.rs
index 34109931..426c5ee1 100644
--- a/ui/src/pve/mod.rs
+++ b/ui/src/pve/mod.rs
@@ -1,7 +1,8 @@
-use std::{fmt::Display, rc::Rc};
+use std::{collections::HashMap, fmt::Display, rc::Rc};
 
 use gloo_utils::window;
 use serde::{Deserialize, Serialize};
+use serde_json::Value;
 use yew::{
     prelude::Html,
     virtual_dom::{VComp, VNode},
@@ -141,6 +142,7 @@ pub enum Msg {
         Result<Vec<PveResource>, Error>,
         Result<RemoteUpdateSummary, Error>,
     ),
+    OptionsLoadFinished(Result<Value, Error>),
 }
 
 pub struct PveRemoteComp {
@@ -149,10 +151,66 @@ pub struct PveRemoteComp {
     resources: Rc<Vec<PveResource>>,
     last_error: Option<String>,
     updates: LoadResult<RemoteUpdateSummary, Error>,
+    tag_style_override: TagStyleOverride,
 }
 
 pwt::impl_deref_mut_property!(PveRemoteComp, state, LoadableComponentState<()>);
 
+#[derive(Clone, PartialEq, Default)]
+/// Holds overrides for the guest tag display
+///
+/// This can be cloned cheaply, so it can be easily passed to e.g. renderer closures.
+pub struct TagStyleOverride {
+    color_map: Rc<HashMap<String, (String, String)>>,
+}
+
+impl TagStyleOverride {
+    /// Returns the background and foreground color of a tag as CSS hex color strings.
+    pub fn get(&self, name: &str) -> Option<&(String, String)> {
+        self.color_map.get(name)
+    }
+
+    /// Parses the tag style override from a [serde_json::Value]
+    pub fn from_value(value: Value) -> Self {
+        let mut map = HashMap::new();
+        match serde_json::from_value::<DataCenterOptions>(value) {
+            Ok(DataCenterOptions {
+                tag_style: Some(tag_style),
+            }) => {
+                for tag_override in tag_style.color_map.split(';') {
+                    let mut list = tag_override.splitn(3, ':');
+                    if let (Some(name), Some(bg), Some(fg)) =
+                        (list.next(), list.next(), list.next())
+                    {
+                        // store with css color syntax directly
+                        map.insert(name.to_string(), (format!("#{bg}"), format!("#{fg}")));
+                    }
+                }
+            }
+            Ok(_) => {} // no override options
+            Err(err) => log::warn!("error parsing tag style options: {err}"),
+        }
+
+        Self {
+            color_map: Rc::new(map),
+        }
+    }
+}
+
+// types for Deserialize
+#[derive(Deserialize)]
+#[serde(rename_all = "kebab-case")]
+struct TagStyle {
+    #[serde(default)]
+    color_map: String,
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "kebab-case")]
+struct DataCenterOptions {
+    tag_style: Option<TagStyle>,
+}
+
 impl LoadableComponent for PveRemoteComp {
     type Message = Msg;
     type Properties = PveRemote;
@@ -160,12 +218,22 @@ impl LoadableComponent for PveRemoteComp {
 
     fn create(ctx: &LoadableComponentContext<PveRemoteComp>) -> Self {
         ctx.link().repeated_load(5000);
+
+        // load datacenter options only once
+        let remote = ctx.props().remote.clone();
+        ctx.link().send_future(async move {
+            let client = crate::pdm_client();
+            let options = client.pve_cluster_options(&remote).await;
+            Msg::OptionsLoadFinished(options)
+        });
+
         Self {
             state: LoadableComponentState::new(),
             view: PveTreeNode::Root,
             resources: Rc::new(Vec::new()),
             last_error: None,
             updates: LoadResult::new(),
+            tag_style_override: TagStyleOverride::default(),
         }
     }
 
@@ -188,6 +256,10 @@ impl LoadableComponent for PveRemoteComp {
                 };
                 self.updates.update(updates);
             }
+            Msg::OptionsLoadFinished(options) => match options {
+                Ok(options) => self.tag_style_override = TagStyleOverride::from_value(options),
+                Err(err) => log::warn!("error loading datacenter options: {err}"),
+            },
         }
         true
     }
@@ -281,6 +353,7 @@ impl LoadableComponent for PveRemoteComp {
                                 remote.to_string(),
                                 self.resources.clone(),
                                 self.loading(),
+                                self.tag_style_override.clone(),
                                 link.callback(Msg::SelectedView),
                                 {
                                     let link = link.clone();
diff --git a/ui/src/pve/tree.rs b/ui/src/pve/tree.rs
index c3e3e8b2..7e09fb6b 100644
--- a/ui/src/pve/tree.rs
+++ b/ui/src/pve/tree.rs
@@ -27,7 +27,9 @@ use pdm_api_types::{
     RemoteUpid,
 };
 
-use crate::{get_deep_url, renderer::render_tree_column, widget::MigrateWindow};
+use crate::{
+    get_deep_url, pve::TagStyleOverride, renderer::render_tree_column, widget::MigrateWindow,
+};
 
 use super::{
     utils::{self, render_guest_tags, render_lxc_name, render_qemu_name},
@@ -77,6 +79,8 @@ pub struct PveTree {
 
     loading: bool,
 
+    tag_style_override: TagStyleOverride,
+
     on_select: Callback<PveTreeNode>,
 
     on_reload_click: Callback<()>,
@@ -87,6 +91,7 @@ impl PveTree {
         remote: String,
         resources: Rc<Vec<PveResource>>,
         loading: bool,
+        tag_style_override: TagStyleOverride,
         on_select: impl Into<Callback<PveTreeNode>>,
         on_reload_click: impl Into<Callback<()>>,
     ) -> Self {
@@ -94,6 +99,7 @@ impl PveTree {
             remote,
             resources,
             loading,
+            tag_style_override,
             on_select: on_select.into(),
             on_reload_click: on_reload_click.into(),
         })
@@ -312,6 +318,7 @@ impl LoadableComponent for PveTreeComp {
                 store.clone(),
                 ctx.props().remote.clone(),
                 ctx.props().loading,
+                ctx.props().tag_style_override.clone(),
             ),
             loaded: false,
             store,
@@ -464,6 +471,7 @@ impl LoadableComponent for PveTreeComp {
             self.store.clone(),
             props.remote.clone(),
             props.loading,
+            props.tag_style_override.clone(),
         );
 
         true
@@ -589,6 +597,7 @@ fn columns(
     store: TreeStore<PveTreeNode>,
     remote: String,
     loading: bool,
+    tag_style_override: TagStyleOverride,
 ) -> Rc<Vec<DataTableHeader<PveTreeNode>>> {
     let loading = match store.read().root() {
         Some(root) => loading && root.children_count() == 0,
@@ -626,8 +635,12 @@ fn columns(
         DataTableColumn::new(tr!("Tags"))
             .flex(1)
             .render(move |entry: &PveTreeNode| match entry {
-                PveTreeNode::Lxc(lxc) => render_guest_tags(&lxc.tags[..]).into(),
-                PveTreeNode::Qemu(qemu) => render_guest_tags(&qemu.tags[..]).into(),
+                PveTreeNode::Lxc(lxc) => {
+                    render_guest_tags(&lxc.tags[..], &tag_style_override).into()
+                }
+                PveTreeNode::Qemu(qemu) => {
+                    render_guest_tags(&qemu.tags[..], &tag_style_override).into()
+                }
                 _ => html! {},
             })
             .into(),
diff --git a/ui/src/pve/utils.rs b/ui/src/pve/utils.rs
index a52689ec..89a00cee 100644
--- a/ui/src/pve/utils.rs
+++ b/ui/src/pve/utils.rs
@@ -16,6 +16,8 @@ use pwt::{
     widget::{Container, Fa, Row},
 };
 
+use crate::pve::TagStyleOverride;
+
 /// Renders the display name for Virtual Machines, e.g. used for resource trees
 pub fn render_qemu_name(qemu: &PveQemuResource, vmid_first: bool) -> String {
     render_guest_name(&qemu.name, qemu.vmid, vmid_first)
@@ -121,21 +123,31 @@ pub fn render_storage_status_icon(node: &PveStorageResource) -> Container {
 }
 
 /// Returns a [`pwt::widget::Row`] with an element for each tag
-pub fn render_guest_tags(tags: &[String]) -> Row {
+pub fn render_guest_tags(tags: &[String], tag_style_override: &TagStyleOverride) -> Row {
     let mut row = Row::new().class("pve-tags").gap(2);
 
     for tag in tags {
         if tag.is_empty() {
             continue;
         }
-        let color = pdm_ui_shared::colors::text_to_rgb(tag).unwrap();
-        let foreground = pdm_ui_shared::colors::get_best_contrast_color(&color);
+
+        let (color, foreground) = match tag_style_override.get(tag) {
+            Some(tag_override) => tag_override.clone(),
+            None => {
+                let color = pdm_ui_shared::colors::text_to_rgb(tag).unwrap();
+                let foreground = pdm_ui_shared::colors::get_best_contrast_color(&color);
+                (
+                    color.as_css_rgb().to_string(),
+                    foreground.as_css_rgb().to_string(),
+                )
+            }
+        };
 
         row.add_child(
             Container::new()
                 .class("pve-tag")
-                .style("background-color", color.as_css_rgb().to_string())
-                .style("color", foreground.as_css_rgb().to_string())
+                .style("background-color", color)
+                .style("color", foreground)
                 .with_child(tag),
         );
     }
diff --git a/ui/src/widget/resource_tree.rs b/ui/src/widget/resource_tree.rs
index ba178b89..dc67fe94 100644
--- a/ui/src/widget/resource_tree.rs
+++ b/ui/src/widget/resource_tree.rs
@@ -33,7 +33,7 @@ use pdm_api_types::resource::{RemoteResources, Resource};
 use crate::{
     dashboard::view::ViewContext,
     get_deep_url, get_resource_node,
-    pve::utils::render_guest_tags,
+    pve::{utils::render_guest_tags, TagStyleOverride},
     renderer::{render_resource_name, render_status_icon},
     RemoteList,
 };
@@ -381,11 +381,14 @@ fn columns(
                         Row::new()
                             .gap(1)
                             .with_child(render_resource_name(resource, true))
-                            .with_child(render_guest_tags(match resource {
-                                Resource::PveQemu(pve_qemu_resource) => &pve_qemu_resource.tags[..],
-                                Resource::PveLxc(pve_lxc_resource) => &pve_lxc_resource.tags[..],
-                                _ => &[],
-                            }))
+                            .with_child(render_guest_tags(
+                                match resource {
+                                    Resource::PveQemu(qemu) => &qemu.tags[..],
+                                    Resource::PveLxc(lxc) => &lxc.tags[..],
+                                    _ => &[],
+                                },
+                                &TagStyleOverride::default(),
+                            ))
                             .into(),
                         None,
                     ),
-- 
2.47.3





^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-04-17 14:13 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-04-17 14:10 [RFC PATCH datacenter-manager 1/2] lib: pdm-client: add method for /cluster/options Dominik Csapak
2026-04-17 14:10 ` [RFC PATCH datacenter-manager 2/2] ui: pve: load and use tag style overrides for resource tree Dominik Csapak

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal