public inbox for pdm-devel@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; 5+ 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 related	[flat|nested] 5+ 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
  2026-08-31  9:26   ` Dominik Csapak
  2026-08-31 12:18   ` Lukas Wagner
  0 siblings, 2 replies; 5+ 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 related	[flat|nested] 5+ messages in thread

* Re: [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 2/2] ui: pve: load and use tag style overrides for resource tree Dominik Csapak
@ 2026-08-31  9:26   ` Dominik Csapak
  2026-08-31 12:18   ` Lukas Wagner
  1 sibling, 0 replies; 5+ messages in thread
From: Dominik Csapak @ 2026-08-31  9:26 UTC (permalink / raw)
  To: pdm-devel

ping, any feedback on this?

i was not sure how to handle custom colors in pdm, but
I guess users would expect it to honor the set colors in
pve...

alternatively, we can do the whole override dance in PDM itself




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

* Re: [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 2/2] ui: pve: load and use tag style overrides for resource tree Dominik Csapak
  2026-08-31  9:26   ` Dominik Csapak
@ 2026-08-31 12:18   ` Lukas Wagner
  2026-08-31 12:41     ` Dominik Csapak
  1 sibling, 1 reply; 5+ messages in thread
From: Lukas Wagner @ 2026-08-31 12:18 UTC (permalink / raw)
  To: Dominik Csapak, pdm-devel

On Fri Apr 17, 2026 at 4:10 PM CEST, Dominik Csapak wrote:
> 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.

I think having a consistent style for tags would be quite important; to
me, the style really is part of the 'identity' of the tag.

In my opinion, either the entire PDM UI should properly respects the
overrides that were configured in PVE, or not at all. Having two visual
styles for the same tag is really not great, I think.

That raises the overall question about tag 'identity'. Should PDM
consider two tags with the same name (e.g. 'debian') from two remotes as
the same? I would argue strongly argue in favor of that, and I'd also
say that they should have the same style then.

So, as a conclusion, I believe that tag style overrides should not be
per-remote, but 'global' to PDM, applying to all remotes. To make this
nicer for users, we could

  - allow importing existing overrides from PVE remotes (with the option
    to decide what happens if any imported style would override an
    existing definition)
  - allow applying configured overrides to remotes


What do you think?


>
> 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.
>

Btw, this patch does not apply any more.

Thanks!




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

* Re: [RFC PATCH datacenter-manager 2/2] ui: pve: load and use tag style overrides for resource tree
  2026-08-31 12:18   ` Lukas Wagner
@ 2026-08-31 12:41     ` Dominik Csapak
  0 siblings, 0 replies; 5+ messages in thread
From: Dominik Csapak @ 2026-08-31 12:41 UTC (permalink / raw)
  To: Lukas Wagner, pdm-devel



On 8/31/26 2:17 PM, Lukas Wagner wrote:
> On Fri Apr 17, 2026 at 4:10 PM CEST, Dominik Csapak wrote:
>> 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.
> 
> I think having a consistent style for tags would be quite important; to
> me, the style really is part of the 'identity' of the tag.
> 
> In my opinion, either the entire PDM UI should properly respects the
> overrides that were configured in PVE, or not at all. Having two visual
> styles for the same tag is really not great, I think.

I agree, i wasn't really happy with that split anyway...

> 
> That raises the overall question about tag 'identity'. Should PDM
> consider two tags with the same name (e.g. 'debian') from two remotes as
> the same? I would argue strongly argue in favor of that, and I'd also
> say that they should have the same style then.

mhmm this is a tricky question, e.g. for a user that is 'owner' of all
remotes this makes totally sense, but consider e.g. the "MSP" case,
where a company has added the remotes of all their customers.
there each customer might have different meaning for tags, and
overlap 'by accident' would lump them together in pdm.

not saying we couldn't do that anyway. but maybe having some sort
of tag mapping (at least as an option) could maybe be necessary
before simply treating tags from all remotes the same

> 
> So, as a conclusion, I believe that tag style overrides should not be
> per-remote, but 'global' to PDM, applying to all remotes. To make this
> nicer for users, we could
> 
>    - allow importing existing overrides from PVE remotes (with the option
>      to decide what happens if any imported style would override an
>      existing definition)

seems good to me (not sure about the ux yet, but makes sense)

>    - allow applying configured overrides to remotes

what do you mean? copying the pdm local applied overrides to some remote?

> 
> 
> What do you think?
> 
> 
>>
>> 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.
>>
> 
> Btw, this patch does not apply any more.
> 
> Thanks!


yes, no worries, i wanted to avoid rebasing work before getting any
feedback at all ;)




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

end of thread, other threads:[~2026-08-31 12:41 UTC | newest]

Thread overview: 5+ 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
2026-08-31  9:26   ` Dominik Csapak
2026-08-31 12:18   ` Lukas Wagner
2026-08-31 12:41     ` Dominik Csapak

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal