From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 8ABF11FF0EA for ; Fri, 14 Aug 2026 15:46:01 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id AA03F21679; Fri, 14 Aug 2026 15:45:53 +0200 (CEST) From: Dominik Csapak To: pdm-devel@lists.proxmox.com Subject: [PATCH datacenter-manager 8/8] ui: pve: node selector: show maintenance badge with node name Date: Fri, 14 Aug 2026 15:44:04 +0200 Message-ID: <20260814134548.3446943-9-d.csapak@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260814134548.3446943-1-d.csapak@proxmox.com> References: <20260814134548.3446943-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.182 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) POISEN_SPAM_PILL 0.1 Meta: its spam POISEN_SPAM_PILL_1 0.1 random spam to be learned in bayes POISEN_SPAM_PILL_3 0.1 random spam to be learned in bayes RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust RDNS_NONE 1.274 Delivered to internal network by a host with no rDNS 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: 4XUX6NA746WPNVSVOD5LBWI33KTNZMJ2 X-Message-ID-Hash: 4XUX6NA746WPNVSVOD5LBWI33KTNZMJ2 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: So when e.g. selecting a target for a migration, it's obvious that the node is in ha maintenance mode. To do this we have to change the node selector from the /nodes api call to the /cluster/resources one (since only that includes the hastate), and we have to implement `AsResourceView` for the ResourceView itself. We do this by cloning (should be cheap, ResourceView is just an enum with references, so no real data is contained only pointers.) This partially fixes #7371. Signed-off-by: Dominik Csapak --- lib/pdm-api-types/src/resource.rs | 6 +++ ui/src/widget/pve_node_selector.rs | 78 ++++++++++++++++-------------- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/lib/pdm-api-types/src/resource.rs b/lib/pdm-api-types/src/resource.rs index 0db56dc4..df59bb4f 100644 --- a/lib/pdm-api-types/src/resource.rs +++ b/lib/pdm-api-types/src/resource.rs @@ -343,6 +343,12 @@ pub trait AsResourceView { fn as_resource_view<'a>(&'a self) -> ResourceView<'a>; } +impl<'a> AsResourceView for ResourceView<'a> { + fn as_resource_view<'b>(&'b self) -> ResourceView<'a> { + self.clone() + } +} + impl AsResourceView for Resource { fn as_resource_view<'a>(&'a self) -> ResourceView<'a> { ResourceView::from(self) diff --git a/ui/src/widget/pve_node_selector.rs b/ui/src/widget/pve_node_selector.rs index e0ca7b61..cebfadd4 100644 --- a/ui/src/widget/pve_node_selector.rs +++ b/ui/src/widget/pve_node_selector.rs @@ -16,13 +16,17 @@ use pwt::{ tr, widget::{ Fa, GridPicker, Row, + container::div, data_table::{DataTable, DataTableColumn, DataTableHeader}, form::{Selector, SelectorRenderArgs}, }, }; use pwt_macros::{builder, widget}; -use pdm_client::types::ClusterNodeIndexResponse; +use pdm_api_types::resource::{PveNodeResource, ResourceView}; +use pdm_client::types::ClusterResourceKind; + +use crate::renderer::render_resource_extra; #[widget(comp=PveNodeSelectorComp, @input)] #[derive(Clone, Properties, PartialEq)] @@ -76,27 +80,35 @@ impl PveNodeSelector { } pub enum Msg { - UpdateNodeList(Result, Error>), + UpdateNodeList(Result, Error>), } pub struct PveNodeSelectorComp { _async_pool: AsyncPool, - store: Store, + store: Store, /// Unfiltered node list as fetched from the remote, kept so a prop change to `excluded_nodes` /// can re-filter without round-tripping the remote again. - raw_nodes: Vec, + raw_nodes: Vec, last_err: Option, } impl PveNodeSelectorComp { - async fn get_node_list(remote: AttrValue) -> Result, Error> { - let mut nodes = crate::pdm_client().pve_list_nodes(&remote).await?; + async fn get_node_list(remote: AttrValue) -> Result, Error> { + let mut nodes: Vec = crate::pdm_client() + .pve_cluster_resources(&remote, Some(ClusterResourceKind::Node)) + .await? + .into_iter() + .filter_map(|r| match r { + pdm_api_types::resource::PveResource::Node(node) => Some(node), + _ => None, + }) + .collect(); nodes.sort_by(|a, b| a.node.cmp(&b.node)); Ok(nodes) } fn apply_filter(&mut self, excluded: &[String], source_node: Option<&str>) { - let filtered: Vec = self + let filtered: Vec = self .raw_nodes .iter() .filter(|n| { @@ -122,9 +134,7 @@ impl Component for PveNodeSelectorComp { _async_pool, last_err: None, raw_nodes: Vec::new(), - store: Store::with_extract_key(|node: &ClusterNodeIndexResponse| { - Key::from(node.node.as_str()) - }), + store: Store::with_extract_key(|node: &PveNodeResource| Key::from(node.node.as_str())), } } @@ -175,7 +185,7 @@ impl Component for PveNodeSelectorComp { } }; Selector::new(self.store.clone(), { - move |args: &SelectorRenderArgs>| { + move |args: &SelectorRenderArgs>| { if let Some(err) = &err { return Row::new() .with_child(Fa::from(Status::Error)) @@ -203,44 +213,40 @@ impl Component for PveNodeSelectorComp { } } -fn columns(show_memory: bool) -> Rc>> { +fn columns(show_memory: bool) -> Rc>> { let mut columns = vec![ DataTableColumn::new(tr!("Node")) - .get_property(|entry: &ClusterNodeIndexResponse| &entry.node) + .width("250px") + .render(|entry: &PveNodeResource| { + Row::new() + .gap(1) + .with_child(div(&entry.node)) + .with_optional_child(render_resource_extra(ResourceView::PveNode(entry))) + .into() + }) + .sorter(|a: &PveNodeResource, b: &PveNodeResource| a.node.cmp(&b.node)) .sort_order(true) .into(), ]; if show_memory { columns.push( DataTableColumn::new(tr!("CPU Usage")) - .render(|entry: &ClusterNodeIndexResponse| match entry.cpu { - Some(cpu) => html! { rrd_value_renderer::render_cpu_usage(&cpu) }, - None => html! {}, + .render(|entry: &PveNodeResource| { + rrd_value_renderer::render_cpu_usage(&entry.cpu).into() + }) + .sorter(|a: &PveNodeResource, b: &PveNodeResource| { + // total_cmp tolerates NaN; preserve the "no data sorts low" intuition by + // mapping None to negative infinity so unprobed nodes stay at the bottom. + a.cpu.total_cmp(&b.cpu) }) - .sorter( - |a: &ClusterNodeIndexResponse, b: &ClusterNodeIndexResponse| { - // total_cmp tolerates NaN; preserve the "no data sorts low" intuition by - // mapping None to negative infinity so unprobed nodes stay at the bottom. - a.cpu - .unwrap_or(f64::NEG_INFINITY) - .total_cmp(&b.cpu.unwrap_or(f64::NEG_INFINITY)) - }, - ) .into(), ); columns.push( DataTableColumn::new(tr!("Memory Usage")) - .render( - |entry: &ClusterNodeIndexResponse| match (entry.mem, entry.maxmem) { - (Some(mem), Some(maxmem)) => { - html! {format!("{:.2}%", 100.0 * mem as f64 / maxmem as f64)} - } - _ => html! {}, - }, - ) - .sorter( - |a: &ClusterNodeIndexResponse, b: &ClusterNodeIndexResponse| a.mem.cmp(&b.mem), - ) + .render(|entry: &PveNodeResource| { + html! {format!("{:.2}%", 100.0 * entry.mem as f64 / entry.maxmem as f64)} + }) + .sorter(|a: &PveNodeResource, b: &PveNodeResource| a.mem.cmp(&b.mem)) .into(), ); } -- 2.47.3