From: Dominik Csapak <d.csapak@proxmox.com>
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 [thread overview]
Message-ID: <20260814134548.3446943-9-d.csapak@proxmox.com> (raw)
In-Reply-To: <20260814134548.3446943-1-d.csapak@proxmox.com>
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 <d.csapak@proxmox.com>
---
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<Vec<ClusterNodeIndexResponse>, Error>),
+ UpdateNodeList(Result<Vec<PveNodeResource>, Error>),
}
pub struct PveNodeSelectorComp {
_async_pool: AsyncPool,
- store: Store<ClusterNodeIndexResponse>,
+ store: Store<PveNodeResource>,
/// 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<ClusterNodeIndexResponse>,
+ raw_nodes: Vec<PveNodeResource>,
last_err: Option<AttrValue>,
}
impl PveNodeSelectorComp {
- async fn get_node_list(remote: AttrValue) -> Result<Vec<ClusterNodeIndexResponse>, Error> {
- let mut nodes = crate::pdm_client().pve_list_nodes(&remote).await?;
+ async fn get_node_list(remote: AttrValue) -> Result<Vec<PveNodeResource>, Error> {
+ let mut nodes: Vec<PveNodeResource> = 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<ClusterNodeIndexResponse> = self
+ let filtered: Vec<PveNodeResource> = 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<Store<ClusterNodeIndexResponse>>| {
+ move |args: &SelectorRenderArgs<Store<PveNodeResource>>| {
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<Vec<DataTableHeader<ClusterNodeIndexResponse>>> {
+fn columns(show_memory: bool) -> Rc<Vec<DataTableHeader<PveNodeResource>>> {
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
prev parent reply other threads:[~2026-08-14 13:46 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-14 13:43 [PATCH datacenter-manager 0/8] refactor and partially fix #7371 Dominik Csapak
2026-08-14 13:43 ` [PATCH datacenter-manager 1/8] lib: add new ResourceView type and move accessors there Dominik Csapak
2026-08-14 13:43 ` [PATCH datacenter-manager 2/8] lib: api types: resource: add 'node' helper to ResourceView Dominik Csapak
2026-08-14 13:43 ` [PATCH datacenter-manager 3/8] lib: api-types: add 'vmid' getter " Dominik Csapak
2026-08-14 13:44 ` [PATCH datacenter-manager 4/8] ui: pve: factor out the pve-manager version extraction Dominik Csapak
2026-08-14 13:44 ` [PATCH datacenter-manager 5/8] ui: renderer: use ResourceView for rendering Dominik Csapak
2026-08-14 13:44 ` [PATCH datacenter-manager 6/8] ui: pve: tree: reuse `PveResource` for `PveTreeNode` Dominik Csapak
2026-08-14 13:44 ` [PATCH datacenter-manager 7/8] ui: pve: show ha maintenance mode for nodes Dominik Csapak
2026-08-14 13:44 ` Dominik Csapak [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260814134548.3446943-9-d.csapak@proxmox.com \
--to=d.csapak@proxmox.com \
--cc=pdm-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
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.