* [PATCH datacenter-manager 1/8] lib: add new ResourceView type and move accessors there
2026-08-14 13:43 [PATCH datacenter-manager 0/8] refactor and partially fix #7371 Dominik Csapak
@ 2026-08-14 13:43 ` Dominik Csapak
2026-08-14 13:43 ` [PATCH datacenter-manager 2/8] lib: api types: resource: add 'node' helper to ResourceView Dominik Csapak
` (6 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Dominik Csapak @ 2026-08-14 13:43 UTC (permalink / raw)
To: pdm-devel
This is intended as a view type for Resource and PveResource, which will
always share some enum type. This way, we can reuse all accessors
cheaply for PveResource values without cloning or consuming.
This makes it easier to share code for both of them, for instance in the
UI where we want to render properties consistently for resources.
Introduces also a AsResourceView trait to make converting more
ergonomic.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
lib/pdm-api-types/src/resource.rs | 261 +++++++++++++++++++++---------
1 file changed, 184 insertions(+), 77 deletions(-)
diff --git a/lib/pdm-api-types/src/resource.rs b/lib/pdm-api-types/src/resource.rs
index 303ac3cb..a8c4849d 100644
--- a/lib/pdm-api-types/src/resource.rs
+++ b/lib/pdm-api-types/src/resource.rs
@@ -45,102 +45,31 @@ impl Resource {
/// Returns the local ID, not a globally unique one, e.g.
/// `qemu/<vmid>`
pub fn id(&self) -> String {
- match self {
- Resource::PveStorage(r) => format!("storage/{}/{}", r.node, r.storage),
- Resource::PveQemu(r) => format!("qemu/{}", r.vmid),
- Resource::PveLxc(r) => format!("lxc/{}", r.vmid),
- Resource::PveNode(r) => format!("node/{}", r.node),
- Resource::PveNetwork(r) => {
- if let PveNetworkResource::Zone(z) = r {
- if z.legacy {
- return format!("sdn/{}/{}", r.node(), r.name());
- }
- }
-
- format!("network/{}/{}/{}", r.node(), r.network_type(), r.name())
- }
- Resource::PbsNode(r) => format!("node/{}", r.name),
- Resource::PbsDatastore(r) => r.name.clone(),
- }
+ self.as_resource_view().id()
}
/// Returns the PDM global ID for the resource, e.g.
/// `remote/<remote-id>/guest/<vmid>`
pub fn global_id(&self) -> &str {
- match self {
- Resource::PveStorage(r) => r.id.as_str(),
- Resource::PveQemu(r) => r.id.as_str(),
- Resource::PveLxc(r) => r.id.as_str(),
- Resource::PveNode(r) => r.id.as_str(),
- Resource::PveNetwork(r) => r.id(),
- Resource::PbsNode(r) => r.id.as_str(),
- Resource::PbsDatastore(r) => r.id.as_str(),
- }
+ self.as_resource_view().global_id()
}
/// Returns the "name" of the resource, e.g. the guest name for VMs/Containers or
/// the hostname for nodes
pub fn name(&self) -> &str {
- match self {
- Resource::PveStorage(r) => r.storage.as_str(),
- Resource::PveQemu(r) => r.name.as_str(),
- Resource::PveLxc(r) => r.name.as_str(),
- Resource::PveNode(r) => r.node.as_str(),
- Resource::PveNetwork(r) => r.name(),
- Resource::PbsNode(r) => r.name.as_str(),
- Resource::PbsDatastore(r) => r.name.as_str(),
- }
+ self.as_resource_view().name()
}
pub fn resource_type(&self) -> ResourceType {
- match self {
- Resource::PveStorage(_) => ResourceType::PveStorage,
- Resource::PveQemu(_) => ResourceType::PveQemu,
- Resource::PveLxc(_) => ResourceType::PveLxc,
- Resource::PveNetwork(_) => ResourceType::PveNetwork,
- Resource::PveNode(_) | Resource::PbsNode(_) => ResourceType::Node,
- Resource::PbsDatastore(_) => ResourceType::PbsDatastore,
- }
+ self.as_resource_view().resource_type()
}
pub fn status(&self) -> &str {
- match self {
- Resource::PveStorage(r) => r.status.as_str(),
- Resource::PveQemu(r) => r.status.as_str(),
- Resource::PveLxc(r) => r.status.as_str(),
- Resource::PveNode(r) => r.status.as_str(),
- Resource::PveNetwork(r) => r.status(),
- Resource::PbsNode(r) => {
- if r.uptime > 0 {
- "online"
- } else {
- "offline"
- }
- }
- Resource::PbsDatastore(r) => {
- if r.maintenance.is_none() {
- "online"
- } else {
- "under-maintenance"
- }
- }
- }
+ self.as_resource_view().status()
}
pub fn properties(&self) -> String {
- let mut properties = Vec::new();
- if let Resource::PbsDatastore(r) = self {
- if let Some(backend_type) = &r.backend_type {
- properties.push(backend_type.to_string());
- }
- if r.backing_device.is_some() {
- properties.push("removable".to_string());
- }
- if r.usage > PBS_DATASTORE_HIGH_USAGE_THRESHOLD {
- properties.push("high-usage".to_string());
- }
- }
- properties.join(",")
+ self.as_resource_view().properties()
}
}
@@ -229,6 +158,184 @@ pub enum PveResource {
Network(PveNetworkResource),
}
+/// A borrowed view of a resource than can come from a [Resource] or a [PveResource]
+/// This is useful for having a single implementation of field access regardless which
+/// of the types is used.
+#[derive(Clone, Debug, PartialEq)]
+pub enum ResourceView<'a> {
+ /// Storage resource in PVE.
+ PveStorage(&'a PveStorageResource),
+ /// QEMU guest resource in PVE.
+ PveQemu(&'a PveQemuResource),
+ /// LXC guest resource in PVE.
+ PveLxc(&'a PveLxcResource),
+ /// A PVE node.
+ PveNode(&'a PveNodeResource),
+ /// Network in PVE.
+ PveNetwork(&'a PveNetworkResource),
+ /// A PBS node.
+ PbsNode(&'a PbsNodeResource),
+ /// Datastore on a PBS node.
+ PbsDatastore(&'a PbsDatastoreResource),
+}
+
+impl<'a> ResourceView<'a> {
+ /// Returns the local ID, not a globally unique one, e.g.
+ /// `qemu/<vmid>`
+ pub fn id(&'a self) -> String {
+ match self {
+ ResourceView::PveStorage(r) => format!("storage/{}/{}", r.node, r.storage),
+ ResourceView::PveQemu(r) => format!("qemu/{}", r.vmid),
+ ResourceView::PveLxc(r) => format!("lxc/{}", r.vmid),
+ ResourceView::PveNode(r) => format!("node/{}", r.node),
+ ResourceView::PveNetwork(r) => {
+ if let PveNetworkResource::Zone(z) = r {
+ if z.legacy {
+ return format!("sdn/{}/{}", r.node(), r.name());
+ }
+ }
+
+ format!("network/{}/{}/{}", r.node(), r.network_type(), r.name())
+ }
+ ResourceView::PbsNode(r) => format!("node/{}", r.name),
+ ResourceView::PbsDatastore(r) => r.name.clone(),
+ }
+ }
+
+ /// Returns the PDM global ID for the resource, e.g.
+ /// `remote/<remote-id>/guest/<vmid>`
+ pub fn global_id(&self) -> &'a str {
+ match self {
+ ResourceView::PveStorage(r) => r.id.as_str(),
+ ResourceView::PveQemu(r) => r.id.as_str(),
+ ResourceView::PveLxc(r) => r.id.as_str(),
+ ResourceView::PveNode(r) => r.id.as_str(),
+ ResourceView::PveNetwork(r) => r.id(),
+ ResourceView::PbsNode(r) => r.id.as_str(),
+ ResourceView::PbsDatastore(r) => r.id.as_str(),
+ }
+ }
+
+ /// Returns the "name" of the resource, e.g. the guest name for VMs/Containers or
+ /// the hostname for nodes
+ pub fn name(&self) -> &'a str {
+ match self {
+ ResourceView::PveStorage(r) => r.storage.as_str(),
+ ResourceView::PveQemu(r) => r.name.as_str(),
+ ResourceView::PveLxc(r) => r.name.as_str(),
+ ResourceView::PveNode(r) => r.node.as_str(),
+ ResourceView::PveNetwork(r) => r.name(),
+ ResourceView::PbsNode(r) => r.name.as_str(),
+ ResourceView::PbsDatastore(r) => r.name.as_str(),
+ }
+ }
+
+ pub fn resource_type(&self) -> ResourceType {
+ match self {
+ ResourceView::PveStorage(_) => ResourceType::PveStorage,
+ ResourceView::PveQemu(_) => ResourceType::PveQemu,
+ ResourceView::PveLxc(_) => ResourceType::PveLxc,
+ ResourceView::PveNetwork(_) => ResourceType::PveNetwork,
+ ResourceView::PveNode(_) | ResourceView::PbsNode(_) => ResourceType::Node,
+ ResourceView::PbsDatastore(_) => ResourceType::PbsDatastore,
+ }
+ }
+
+ pub fn status(&self) -> &'a str {
+ match self {
+ ResourceView::PveStorage(r) => r.status.as_str(),
+ ResourceView::PveQemu(r) => r.status.as_str(),
+ ResourceView::PveLxc(r) => r.status.as_str(),
+ ResourceView::PveNode(r) => r.status.as_str(),
+ ResourceView::PveNetwork(r) => r.status(),
+ ResourceView::PbsNode(r) => {
+ if r.uptime > 0 {
+ "online"
+ } else {
+ "offline"
+ }
+ }
+ ResourceView::PbsDatastore(r) => {
+ if r.maintenance.is_none() {
+ "online"
+ } else {
+ "under-maintenance"
+ }
+ }
+ }
+ }
+
+ pub fn properties(&self) -> String {
+ let mut properties = Vec::new();
+ if let ResourceView::PbsDatastore(r) = self {
+ if let Some(backend_type) = &r.backend_type {
+ properties.push(backend_type.to_string());
+ }
+ if r.backing_device.is_some() {
+ properties.push("removable".to_string());
+ }
+ if r.usage > PBS_DATASTORE_HIGH_USAGE_THRESHOLD {
+ properties.push("high-usage".to_string());
+ }
+ }
+ properties.join(",")
+ }
+}
+
+impl<'a> From<&'a Resource> for ResourceView<'a> {
+ fn from(value: &'a Resource) -> Self {
+ match value {
+ Resource::PveStorage(r) => ResourceView::PveStorage(r),
+ Resource::PveQemu(r) => ResourceView::PveQemu(r),
+ Resource::PveLxc(r) => ResourceView::PveLxc(r),
+ Resource::PveNode(r) => ResourceView::PveNode(r),
+ Resource::PveNetwork(r) => ResourceView::PveNetwork(r),
+ Resource::PbsNode(r) => ResourceView::PbsNode(r),
+ Resource::PbsDatastore(r) => ResourceView::PbsDatastore(r),
+ }
+ }
+}
+
+impl<'a> From<&'a PveResource> for ResourceView<'a> {
+ fn from(value: &'a PveResource) -> Self {
+ match value {
+ PveResource::Storage(r) => ResourceView::PveStorage(r),
+ PveResource::Qemu(r) => ResourceView::PveQemu(r),
+ PveResource::Lxc(r) => ResourceView::PveLxc(r),
+ PveResource::Node(r) => ResourceView::PveNode(r),
+ PveResource::Network(r) => ResourceView::PveNetwork(r),
+ }
+ }
+}
+
+pub trait AsResourceView {
+ fn as_resource_view<'a>(&'a self) -> ResourceView<'a>;
+}
+
+impl AsResourceView for Resource {
+ fn as_resource_view<'a>(&'a self) -> ResourceView<'a> {
+ ResourceView::from(self)
+ }
+}
+
+impl AsResourceView for &Resource {
+ fn as_resource_view<'a>(&'a self) -> ResourceView<'a> {
+ ResourceView::from(*self)
+ }
+}
+
+impl AsResourceView for PveResource {
+ fn as_resource_view<'a>(&'a self) -> ResourceView<'a> {
+ ResourceView::from(self)
+ }
+}
+
+impl AsResourceView for &PveResource {
+ fn as_resource_view<'a>(&'a self) -> ResourceView<'a> {
+ ResourceView::from(*self)
+ }
+}
+
#[api(
properties: {
tags: {
--
2.47.3
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH datacenter-manager 2/8] lib: api types: resource: add 'node' helper to ResourceView
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 ` Dominik Csapak
2026-08-14 13:43 ` [PATCH datacenter-manager 3/8] lib: api-types: add 'vmid' getter " Dominik Csapak
` (5 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Dominik Csapak @ 2026-08-14 13:43 UTC (permalink / raw)
To: pdm-devel
this is often needed independent of the underlying variant
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
lib/pdm-api-types/src/resource.rs | 13 +++++++++++++
ui/src/dashboard/top_entities.rs | 5 +++--
ui/src/guests.rs | 6 +++---
ui/src/lib.rs | 13 -------------
ui/src/widget/resource_tree.rs | 8 ++++----
5 files changed, 23 insertions(+), 22 deletions(-)
diff --git a/lib/pdm-api-types/src/resource.rs b/lib/pdm-api-types/src/resource.rs
index a8c4849d..92d05cbc 100644
--- a/lib/pdm-api-types/src/resource.rs
+++ b/lib/pdm-api-types/src/resource.rs
@@ -230,6 +230,19 @@ impl<'a> ResourceView<'a> {
}
}
+ /// Returns the cluster node the resource is located on. Returns `None` if it's a PBS resource
+ pub fn node(&self) -> Option<&'a str> {
+ let node = match self {
+ ResourceView::PveStorage(r) => &r.node,
+ ResourceView::PveQemu(r) => &r.node,
+ ResourceView::PveLxc(r) => &r.node,
+ ResourceView::PveNode(r) => &r.node,
+ ResourceView::PveNetwork(r) => r.node(),
+ ResourceView::PbsNode(_) | ResourceView::PbsDatastore(_) => return None,
+ };
+ Some(node)
+ }
+
pub fn resource_type(&self) -> ResourceType {
match self {
ResourceView::PveStorage(_) => ResourceType::PveStorage,
diff --git a/ui/src/dashboard/top_entities.rs b/ui/src/dashboard/top_entities.rs
index 634be1d6..cdc3b465 100644
--- a/ui/src/dashboard/top_entities.rs
+++ b/ui/src/dashboard/top_entities.rs
@@ -18,13 +18,14 @@ use pwt::{
widget::{ActionIcon, Column, Container, Panel, Row, error_message},
};
+use pdm_api_types::resource::AsResourceView;
use pdm_api_types::views::LeaderboardType;
use pdm_client::types::{Resource, TopEntity};
use crate::LoadResult;
use crate::{
dashboard::{create_title_with_icon, loading_column},
- get_deep_url, get_resource_node, navigate_to,
+ get_deep_url, navigate_to,
renderer::{render_resource_icon, render_resource_name},
};
@@ -138,7 +139,7 @@ impl Component for TopEntitiesComp {
let rrd = &entity.rrd_data;
let remote = &entity.remote;
- let node = get_resource_node(resource).map(|n| n.to_string());
+ let node = resource.as_resource_view().node().map(|n| n.to_string());
let tooltip_anchor = if let Some(info) = self.tooltip_info.as_ref() {
if info.id == resource.global_id() {
diff --git a/ui/src/guests.rs b/ui/src/guests.rs
index ef670a00..b40b1866 100644
--- a/ui/src/guests.rs
+++ b/ui/src/guests.rs
@@ -39,14 +39,14 @@ use pwt::widget::{
};
use pdm_api_types::RemoteUpid;
-use pdm_api_types::resource::{RemoteResources, Resource};
+use pdm_api_types::resource::{AsResourceView, RemoteResources, Resource};
use pdm_search::SearchTerm;
use crate::pve::utils::{guest_is_live, guest_status_label, render_guest_tags};
use crate::pve::{GuestInfo, GuestType};
use crate::renderer::{empty_state, render_resource_name, render_status_icon, render_tree_column};
use crate::{
- get_deep_url, get_resource_node,
+ get_deep_url,
widget::{MigrateWindow, SnapshotWindow},
};
@@ -161,7 +161,7 @@ impl GuestEntry {
}
fn node(&self) -> &str {
- get_resource_node(&self.resource).unwrap_or("")
+ self.resource.as_resource_view().node().unwrap_or("")
}
fn guest_info(&self) -> GuestInfo {
diff --git a/ui/src/lib.rs b/ui/src/lib.rs
index fedbfee5..6e89f044 100644
--- a/ui/src/lib.rs
+++ b/ui/src/lib.rs
@@ -5,7 +5,6 @@ use pdm_api_types::remote_updates::RemoteUpdateSummary;
use pdm_api_types::remotes::RemoteType;
use pdm_api_types::resource::{PveLxcResource, PveQemuResource};
use pdm_api_types::subscription::PdmSubscriptionInfo;
-use pdm_client::types::Resource;
use proxmox_deb_version::Version;
use pwt::props::ContainerBuilder;
use pwt::tr;
@@ -221,18 +220,6 @@ pub(crate) fn navigate_to<C: yew::Component>(
}
}
-pub(crate) fn get_resource_node(resource: &Resource) -> Option<&str> {
- match resource {
- Resource::PveStorage(storage) => Some(&storage.node),
- Resource::PveQemu(qemu) => Some(&qemu.node),
- Resource::PveLxc(lxc) => Some(&lxc.node),
- Resource::PveNode(node) => Some(&node.node),
- Resource::PveNetwork(network) => Some(network.node()),
- Resource::PbsNode(_) => None,
- Resource::PbsDatastore(_) => None,
- }
-}
-
/// Wrapper to 'locale compare' to strings
///
/// Note: The first parameter must be a [`String`], since it needs to be converted to a [`js_sys::JsString`].
diff --git a/ui/src/widget/resource_tree.rs b/ui/src/widget/resource_tree.rs
index 3ba10a7b..337d8e23 100644
--- a/ui/src/widget/resource_tree.rs
+++ b/ui/src/widget/resource_tree.rs
@@ -28,12 +28,12 @@ use pwt_macros::{builder, widget};
use proxmox_yew_comp::{Status, http_get};
-use pdm_api_types::resource::{RemoteResources, Resource};
+use pdm_api_types::resource::{AsResourceView, RemoteResources, Resource};
use crate::{
RemoteList,
dashboard::view::ViewContext,
- get_deep_url, get_resource_node,
+ get_deep_url,
pve::utils::render_guest_tags,
renderer::{render_resource_name, render_status_icon},
};
@@ -420,7 +420,7 @@ fn columns(
match item {
PdmTreeEntry::Root => "",
PdmTreeEntry::Resource(_, resource) => {
- get_resource_node(resource).unwrap_or("")
+ resource.as_resource_view().node().unwrap_or("")
}
PdmTreeEntry::Remote(_, _) => "",
}
@@ -436,7 +436,7 @@ fn columns(
let (remote, id, node) = match item {
PdmTreeEntry::Root => return html! {},
PdmTreeEntry::Resource(remote_id, resource) => {
- (remote_id, resource.id(), get_resource_node(resource))
+ (remote_id, resource.id(), resource.as_resource_view().node())
}
PdmTreeEntry::Remote(remote_id, _) => (remote_id, String::new(), None),
};
--
2.47.3
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH datacenter-manager 3/8] lib: api-types: add 'vmid' getter to ResourceView
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 ` Dominik Csapak
2026-08-14 13:44 ` [PATCH datacenter-manager 4/8] ui: pve: factor out the pve-manager version extraction Dominik Csapak
` (4 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Dominik Csapak @ 2026-08-14 13:43 UTC (permalink / raw)
To: pdm-devel
This is useful for the UI to get the VMID independent of the guest type.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
lib/pdm-api-types/src/resource.rs | 18 ++++++++++++++++++
ui/src/lib.rs | 10 +++++-----
2 files changed, 23 insertions(+), 5 deletions(-)
diff --git a/lib/pdm-api-types/src/resource.rs b/lib/pdm-api-types/src/resource.rs
index 92d05cbc..18177dab 100644
--- a/lib/pdm-api-types/src/resource.rs
+++ b/lib/pdm-api-types/src/resource.rs
@@ -243,6 +243,24 @@ impl<'a> ResourceView<'a> {
Some(node)
}
+ /// Returns the VMID if the resource has one. Returns `None` for non guest resources.
+ pub fn vmid(&self) -> Option<u32> {
+ match self {
+ ResourceView::PveQemu(qemu) => Some(qemu.vmid),
+ ResourceView::PveLxc(lxc) => Some(lxc.vmid),
+ _ => None,
+ }
+ }
+
+ /// Returns the guest type if the resource has one. Returns `None` for non guest resources.
+ pub fn guest_type(&self) -> Option<GuestType> {
+ match self {
+ ResourceView::PveQemu(_) => Some(GuestType::Qemu),
+ ResourceView::PveLxc(_) => Some(GuestType::Lxc),
+ _ => None,
+ }
+ }
+
pub fn resource_type(&self) -> ResourceType {
match self {
ResourceView::PveStorage(_) => ResourceType::PveStorage,
diff --git a/ui/src/lib.rs b/ui/src/lib.rs
index 6e89f044..d42ef42f 100644
--- a/ui/src/lib.rs
+++ b/ui/src/lib.rs
@@ -3,7 +3,7 @@ use gloo_utils::window;
use js_sys::{Array, JsString, Object};
use pdm_api_types::remote_updates::RemoteUpdateSummary;
use pdm_api_types::remotes::RemoteType;
-use pdm_api_types::resource::{PveLxcResource, PveQemuResource};
+use pdm_api_types::resource::AsResourceView;
use pdm_api_types::subscription::PdmSubscriptionInfo;
use proxmox_deb_version::Version;
use pwt::props::ContainerBuilder;
@@ -190,11 +190,11 @@ pub(crate) fn navigate_to<C: yew::Component>(
if let Some(nav) = link.navigator() {
let (prefix, id) = resource
.and_then(|resource| {
+ if let Some(vmid) = resource.as_resource_view().vmid() {
+ return Some((true, format!("guest+{vmid}")));
+ }
+
Some(match resource {
- pdm_client::types::Resource::PveQemu(PveQemuResource { vmid, .. })
- | pdm_client::types::Resource::PveLxc(PveLxcResource { vmid, .. }) => {
- (true, format!("guest+{vmid}"))
- }
pdm_client::types::Resource::PveNode(node) => {
(true, format!("node+{}", node.node))
}
--
2.47.3
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH datacenter-manager 4/8] ui: pve: factor out the pve-manager version extraction
2026-08-14 13:43 [PATCH datacenter-manager 0/8] refactor and partially fix #7371 Dominik Csapak
` (2 preceding siblings ...)
2026-08-14 13:43 ` [PATCH datacenter-manager 3/8] lib: api-types: add 'vmid' getter " Dominik Csapak
@ 2026-08-14 13:44 ` Dominik Csapak
2026-08-14 13:44 ` [PATCH datacenter-manager 5/8] ui: renderer: use ResourceView for rendering Dominik Csapak
` (3 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Dominik Csapak @ 2026-08-14 13:44 UTC (permalink / raw)
To: pdm-devel
This was done three times in the same way. Simply refactor it into
it's own method.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/pve/mod.rs | 32 +++++++++++++-------------------
1 file changed, 13 insertions(+), 19 deletions(-)
diff --git a/ui/src/pve/mod.rs b/ui/src/pve/mod.rs
index f069ec75..1c32d26f 100644
--- a/ui/src/pve/mod.rs
+++ b/ui/src/pve/mod.rs
@@ -25,6 +25,7 @@ use pdm_api_types::{
remote_updates::RemoteUpdateSummary,
resource::{PveResource, ResourceType},
};
+use proxmox_deb_version::Version;
use crate::remotes::RemoteCertCheck;
use crate::{LoadResult, extract_package_version, get_deep_url, get_remote};
@@ -158,6 +159,13 @@ pub struct PveRemoteComp {
updates: LoadResult<RemoteUpdateSummary, Error>,
}
+impl PveRemoteComp {
+ fn get_pve_manager_version(&self, node: &str) -> Option<Version> {
+ let updates = self.updates.data.as_ref()?;
+ extract_package_version(updates, node, "pve-manager")
+ }
+}
+
pwt::impl_deref_mut_property!(PveRemoteComp, state, LoadableComponentState<ViewState>);
impl LoadableComponent for PveRemoteComp {
@@ -206,31 +214,17 @@ impl LoadableComponent for PveRemoteComp {
let content: Html = match &self.view {
PveTreeNode::Root => remote::PveRemotePanel::new(remote.clone()).into(),
- PveTreeNode::Node(node) => {
- let pve_manager = match &self.updates.data {
- Some(updates) => extract_package_version(updates, &node.node, "pve-manager"),
- None => None,
- };
- node::PveNodePanel::new(remote.clone(), node.node.clone())
- .pve_manager_version(pve_manager)
- .into()
- }
+ PveTreeNode::Node(node) => node::PveNodePanel::new(remote.clone(), node.node.clone())
+ .pve_manager_version(self.get_pve_manager_version(&node.node))
+ .into(),
PveTreeNode::Qemu(qemu) => {
- let pve_manager = match &self.updates.data {
- Some(updates) => extract_package_version(updates, &qemu.node, "pve-manager"),
- None => None,
- };
qemu::QemuPanel::new(remote.clone(), qemu.node.clone(), qemu.clone())
- .pve_manager_version(pve_manager)
+ .pve_manager_version(self.get_pve_manager_version(&qemu.node))
.into()
}
PveTreeNode::Lxc(lxc) => {
- let pve_manager = match &self.updates.data {
- Some(updates) => extract_package_version(updates, &lxc.node, "pve-manager"),
- None => None,
- };
lxc::LxcPanel::new(remote.clone(), lxc.node.clone(), lxc.clone())
- .pve_manager_version(pve_manager)
+ .pve_manager_version(self.get_pve_manager_version(&lxc.node))
.into()
}
PveTreeNode::Storage(storage) => {
--
2.47.3
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH datacenter-manager 5/8] ui: renderer: use ResourceView for rendering
2026-08-14 13:43 [PATCH datacenter-manager 0/8] refactor and partially fix #7371 Dominik Csapak
` (3 preceding siblings ...)
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 ` Dominik Csapak
2026-08-14 13:44 ` [PATCH datacenter-manager 6/8] ui: pve: tree: reuse `PveResource` for `PveTreeNode` Dominik Csapak
` (2 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Dominik Csapak @ 2026-08-14 13:44 UTC (permalink / raw)
To: pdm-devel
A `Resource` can be converted to the `ResourceView` and the data
returned is identical, so there is no need to change any current caller,
but it's now possible to call it with a `PveResource` too.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/renderer.rs | 52 +++++++++++++++++++++++-----------------------
1 file changed, 26 insertions(+), 26 deletions(-)
diff --git a/ui/src/renderer.rs b/ui/src/renderer.rs
index be3a94b4..0eecc889 100644
--- a/ui/src/renderer.rs
+++ b/ui/src/renderer.rs
@@ -6,43 +6,43 @@ use pwt::widget::{Column, Container, Fa, Row};
use proxmox_human_byte::HumanByte;
-use pdm_client::types::Resource;
+use pdm_api_types::resource::{AsResourceView, ResourceView};
use crate::pve;
-pub fn render_resource_name(resource: &Resource, vmid_first: bool) -> String {
- match resource {
- Resource::PveStorage(storage) => storage.storage.clone(),
- Resource::PveQemu(qemu) => pve::utils::render_qemu_name(qemu, vmid_first),
- Resource::PveLxc(lxc) => pve::utils::render_lxc_name(lxc, vmid_first),
- Resource::PveNode(node) => node.node.clone(),
- Resource::PveNetwork(network) => network.name().to_string(),
- Resource::PbsNode(node) => node.name.clone(),
- Resource::PbsDatastore(store) => store.name.clone(),
+pub fn render_resource_name(resource: impl AsResourceView, vmid_first: bool) -> String {
+ match resource.as_resource_view() {
+ ResourceView::PveStorage(storage) => storage.storage.clone(),
+ ResourceView::PveQemu(qemu) => pve::utils::render_qemu_name(qemu, vmid_first),
+ ResourceView::PveLxc(lxc) => pve::utils::render_lxc_name(lxc, vmid_first),
+ ResourceView::PveNode(node) => node.node.clone(),
+ ResourceView::PveNetwork(network) => network.name().to_string(),
+ ResourceView::PbsNode(node) => node.name.clone(),
+ ResourceView::PbsDatastore(store) => store.name.clone(),
}
}
-pub fn render_resource_icon(resource: &Resource) -> Fa {
- let class = match resource {
- Resource::PveStorage(_) => "database",
- Resource::PveQemu(_) => "desktop",
- Resource::PveLxc(_) => "cube",
- Resource::PveNode(_) => "building",
- Resource::PveNetwork(_) => "fa-sdn",
- Resource::PbsNode(_) => "building-o",
- Resource::PbsDatastore(_) => "floppy-o",
+pub fn render_resource_icon(resource: impl AsResourceView) -> Fa {
+ let class = match resource.as_resource_view() {
+ ResourceView::PveStorage(_) => "database",
+ ResourceView::PveQemu(_) => "desktop",
+ ResourceView::PveLxc(_) => "cube",
+ ResourceView::PveNode(_) => "building",
+ ResourceView::PveNetwork(_) => "fa-sdn",
+ ResourceView::PbsNode(_) => "building-o",
+ ResourceView::PbsDatastore(_) => "floppy-o",
};
Fa::new(class)
}
-pub fn render_status_icon(resource: &Resource) -> Container {
- match resource {
- Resource::PveStorage(store) => pve::utils::render_storage_status_icon(store),
- Resource::PveQemu(qemu) => pve::utils::render_qemu_status_icon(qemu),
- Resource::PveLxc(lxc) => pve::utils::render_lxc_status_icon(lxc),
- Resource::PveNode(node) => pve::utils::render_node_status_icon(node),
- Resource::PveNetwork(network) => pve::utils::render_sdn_status_icon(network),
+pub fn render_status_icon(resource: impl AsResourceView) -> Container {
+ match resource.as_resource_view() {
+ ResourceView::PveStorage(store) => pve::utils::render_storage_status_icon(store),
+ ResourceView::PveQemu(qemu) => pve::utils::render_qemu_status_icon(qemu),
+ ResourceView::PveLxc(lxc) => pve::utils::render_lxc_status_icon(lxc),
+ ResourceView::PveNode(node) => pve::utils::render_node_status_icon(node),
+ ResourceView::PveNetwork(network) => pve::utils::render_sdn_status_icon(network),
// FIXME: implement remaining types
_ => Container::new().with_child(render_resource_icon(resource)),
}
--
2.47.3
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH datacenter-manager 6/8] ui: pve: tree: reuse `PveResource` for `PveTreeNode`
2026-08-14 13:43 [PATCH datacenter-manager 0/8] refactor and partially fix #7371 Dominik Csapak
` (4 preceding siblings ...)
2026-08-14 13:44 ` [PATCH datacenter-manager 5/8] ui: renderer: use ResourceView for rendering Dominik Csapak
@ 2026-08-14 13:44 ` 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 ` [PATCH datacenter-manager 8/8] ui: pve: node selector: show maintenance badge with node name Dominik Csapak
7 siblings, 0 replies; 9+ messages in thread
From: Dominik Csapak @ 2026-08-14 13:44 UTC (permalink / raw)
To: pdm-devel
This has some advantages: We can't forget types that are represented in
the backend (as we already did with Network for example). And with the
new `ResourceView` type, we can reuse methods that were made for
`Resource`. The inner types are the same anyway, so this does not change
what data we can show.
While changing the PveTreeNode, refactor some code that can profit from
the newly abstracted ResourceView to make it more consistent with other
parts of PDM (like the ResrouceTree) and to save some lines.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/pve/mod.rs | 40 ++++---
ui/src/pve/tree.rs | 292 +++++++++++++++++++++------------------------
2 files changed, 157 insertions(+), 175 deletions(-)
diff --git a/ui/src/pve/mod.rs b/ui/src/pve/mod.rs
index 1c32d26f..ae06179b 100644
--- a/ui/src/pve/mod.rs
+++ b/ui/src/pve/mod.rs
@@ -214,23 +214,31 @@ impl LoadableComponent for PveRemoteComp {
let content: Html = match &self.view {
PveTreeNode::Root => remote::PveRemotePanel::new(remote.clone()).into(),
- PveTreeNode::Node(node) => node::PveNodePanel::new(remote.clone(), node.node.clone())
- .pve_manager_version(self.get_pve_manager_version(&node.node))
+ PveTreeNode::Resource(r) => match r {
+ PveResource::Node(node) => {
+ node::PveNodePanel::new(remote.clone(), node.node.clone())
+ .pve_manager_version(self.get_pve_manager_version(&node.node))
+ .into()
+ }
+ PveResource::Qemu(qemu) => {
+ qemu::QemuPanel::new(remote.clone(), qemu.node.clone(), qemu.clone())
+ .pve_manager_version(self.get_pve_manager_version(&qemu.node))
+ .into()
+ }
+ PveResource::Lxc(lxc) => {
+ lxc::LxcPanel::new(remote.clone(), lxc.node.clone(), lxc.clone())
+ .pve_manager_version(self.get_pve_manager_version(&lxc.node))
+ .into()
+ }
+ PveResource::Storage(storage) => storage::StoragePanel::new(
+ remote.clone(),
+ storage.node.clone(),
+ storage.clone(),
+ )
.into(),
- PveTreeNode::Qemu(qemu) => {
- qemu::QemuPanel::new(remote.clone(), qemu.node.clone(), qemu.clone())
- .pve_manager_version(self.get_pve_manager_version(&qemu.node))
- .into()
- }
- PveTreeNode::Lxc(lxc) => {
- lxc::LxcPanel::new(remote.clone(), lxc.node.clone(), lxc.clone())
- .pve_manager_version(self.get_pve_manager_version(&lxc.node))
- .into()
- }
- PveTreeNode::Storage(storage) => {
- storage::StoragePanel::new(remote.clone(), storage.node.clone(), storage.clone())
- .into()
- }
+ // don't show anything for network for now
+ PveResource::Network(_) => Container::new().into(),
+ },
};
let content = NavigationContainer::new().with_child(content);
diff --git a/ui/src/pve/tree.rs b/ui/src/pve/tree.rs
index 0042331c..f68c728e 100644
--- a/ui/src/pve/tree.rs
+++ b/ui/src/pve/tree.rs
@@ -24,33 +24,31 @@ use pwt::{prelude::*, widget::Button};
use pdm_api_types::{
RemoteUpid,
- resource::{PveLxcResource, PveNodeResource, PveQemuResource, PveResource, PveStorageResource},
+ resource::{AsResourceView, PveNodeResource, PveResource},
};
-use crate::{get_deep_url, renderer::render_tree_column, widget::MigrateWindow};
+use crate::{
+ get_deep_url,
+ renderer::{render_resource_name, render_status_icon, render_tree_column},
+ widget::MigrateWindow,
+};
use super::{
GuestInfo, GuestType,
- utils::{self, render_guest_tags, render_lxc_name, render_qemu_name},
+ utils::{self, render_guest_tags},
};
#[derive(Clone, PartialEq)]
pub enum PveTreeNode {
Root,
- Node(PveNodeResource),
- Lxc(PveLxcResource),
- Qemu(PveQemuResource),
- Storage(PveStorageResource),
+ Resource(PveResource),
}
impl ExtractPrimaryKey for PveTreeNode {
fn extract_key(&self) -> Key {
Key::from(match self {
PveTreeNode::Root => "__root__",
- PveTreeNode::Node(node) => node.id.as_str(),
- PveTreeNode::Lxc(lxc) => lxc.id.as_str(),
- PveTreeNode::Qemu(qemu) => qemu.id.as_str(),
- PveTreeNode::Storage(storage) => storage.id.as_str(),
+ PveTreeNode::Resource(resource) => resource.as_resource_view().global_id(),
})
}
}
@@ -59,12 +57,15 @@ impl PveTreeNode {
fn get_path(&self) -> String {
match self {
PveTreeNode::Root => "datacenter".to_string(),
- PveTreeNode::Node(node) => format!("node+{}", node.node),
- PveTreeNode::Lxc(lxc) => format!("guest+{}", lxc.vmid),
- PveTreeNode::Qemu(qemu) => format!("guest+{}", qemu.vmid),
- PveTreeNode::Storage(storage) => {
- format!("storage+{}+{}", storage.node, storage.storage)
- }
+ PveTreeNode::Resource(resource) => match resource {
+ PveResource::Node(node) => format!("node+{}", node.node),
+ PveResource::Lxc(lxc) => format!("guest+{}", lxc.vmid),
+ PveResource::Qemu(qemu) => format!("guest+{}", qemu.vmid),
+ PveResource::Storage(storage) => {
+ format!("storage+{}+{}", storage.node, storage.storage)
+ }
+ PveResource::Network(network) => format!("network+{}", network.id()),
+ },
}
}
}
@@ -163,39 +164,16 @@ impl PveTreeComp {
let key = Key::from(node_info.id.as_str());
if let Some(mut node) = root.find_node_by_key_mut(&key) {
- *node.record_mut() = PveTreeNode::Node(node_info.clone());
+ *node.record_mut() = PveTreeNode::Resource(entry.clone());
} else {
- root.append(PveTreeNode::Node(node_info.clone()));
- }
- }
- PveResource::Qemu(qemu_info) => {
- let node_id = format!("remote/{}/node/{}", remote, qemu_info.node);
- let key = Key::from(node_id.as_str());
- let mut node = match root.find_node_by_key_mut(&key) {
- Some(node) => node,
- None => root.append(create_empty_node(node_id)),
- };
-
- if !self.loaded {
- node.set_expanded(true);
+ root.append(PveTreeNode::Resource(entry.clone()));
}
- node.append(PveTreeNode::Qemu(qemu_info.clone()));
}
- PveResource::Lxc(lxc_info) => {
- let node_id = format!("remote/{}/node/{}", remote, lxc_info.node);
- let key = Key::from(node_id.as_str());
- let mut node = match root.find_node_by_key_mut(&key) {
- Some(node) => node,
- None => root.append(create_empty_node(node_id)),
- };
+ PveResource::Qemu(_) | PveResource::Lxc(_) | PveResource::Storage(_) => {
+ // PVE resources always have a node
+ let node = entry.as_resource_view().node().unwrap();
+ let node_id = format!("remote/{remote}/node/{node}");
- if !self.loaded {
- node.set_expanded(true);
- }
- node.append(PveTreeNode::Lxc(lxc_info.clone()));
- }
- PveResource::Storage(storage) => {
- let node_id = format!("remote/{}/node/{}", remote, storage.node);
let key = Key::from(node_id.as_str());
let mut node = match root.find_node_by_key_mut(&key) {
Some(node) => node,
@@ -205,7 +183,7 @@ impl PveTreeComp {
if !self.loaded {
node.set_expanded(true);
}
- node.append(PveTreeNode::Storage(storage.clone()));
+ node.append(PveTreeNode::Resource(entry.clone()));
}
PveResource::Network(_) => {}
}
@@ -227,24 +205,28 @@ impl PveTreeComp {
(PveTreeNode::Root, PveTreeNode::Root) => std::cmp::Ordering::Equal,
(PveTreeNode::Root, _) => std::cmp::Ordering::Less,
(_, PveTreeNode::Root) => std::cmp::Ordering::Greater,
- (PveTreeNode::Node(a), PveTreeNode::Node(b)) => a.node.cmp(&b.node),
- (PveTreeNode::Node(_), _) => std::cmp::Ordering::Less,
- (_, PveTreeNode::Node(_)) => std::cmp::Ordering::Greater,
- (PveTreeNode::Lxc(a), PveTreeNode::Lxc(b)) => {
- cmp_guests(a.template, b.template, a.vmid, b.vmid)
- }
- (PveTreeNode::Lxc(_), PveTreeNode::Qemu(_)) => std::cmp::Ordering::Less,
- (PveTreeNode::Qemu(_), PveTreeNode::Lxc(_)) => std::cmp::Ordering::Greater,
- (PveTreeNode::Qemu(a), PveTreeNode::Qemu(b)) => {
- cmp_guests(a.template, b.template, a.vmid, b.vmid)
- }
- (PveTreeNode::Lxc(_) | PveTreeNode::Qemu(_), PveTreeNode::Storage(_)) => {
- std::cmp::Ordering::Less
- }
- (PveTreeNode::Storage(_), PveTreeNode::Lxc(_) | PveTreeNode::Qemu(_)) => {
- std::cmp::Ordering::Greater
- }
- (PveTreeNode::Storage(a), PveTreeNode::Storage(b)) => a.id.cmp(&b.id),
+ (PveTreeNode::Resource(a), PveTreeNode::Resource(b)) => match (a, b) {
+ (PveResource::Node(a), PveResource::Node(b)) => a.node.cmp(&b.node),
+ (PveResource::Node(_), _) => std::cmp::Ordering::Less,
+ (_, PveResource::Node(_)) => std::cmp::Ordering::Greater,
+ (PveResource::Lxc(a), PveResource::Lxc(b)) => {
+ cmp_guests(a.template, b.template, a.vmid, b.vmid)
+ }
+ (PveResource::Lxc(_), PveResource::Qemu(_)) => std::cmp::Ordering::Less,
+ (PveResource::Qemu(_), PveResource::Lxc(_)) => std::cmp::Ordering::Greater,
+ (PveResource::Qemu(a), PveResource::Qemu(b)) => {
+ cmp_guests(a.template, b.template, a.vmid, b.vmid)
+ }
+ (PveResource::Lxc(_) | PveResource::Qemu(_), PveResource::Storage(_)) => {
+ std::cmp::Ordering::Less
+ }
+ (PveResource::Storage(_), PveResource::Lxc(_) | PveResource::Qemu(_)) => {
+ std::cmp::Ordering::Greater
+ }
+ (PveResource::Storage(a), PveResource::Storage(b)) => a.id.cmp(&b.id),
+ (PveResource::Network(_), _) => std::cmp::Ordering::Greater,
+ (_, PveResource::Network(_)) => std::cmp::Ordering::Less,
+ },
});
let first_id = root
.children()
@@ -344,54 +326,49 @@ impl LoadableComponent for PveTreeComp {
let record = node.record().clone();
let link = ctx.link().clone();
- match record {
- PveTreeNode::Lxc(r) => ctx.link().spawn(async move {
- let res = match action {
- Action::Start => {
- crate::pdm_client()
- .pve_lxc_start(&remote, Some(&r.node), r.vmid)
- .await
- }
- Action::Shutdown => {
- crate::pdm_client()
- .pve_lxc_shutdown(&remote, Some(&r.node), r.vmid)
- .await
- }
- // LXC resume is not exposed yet; the UI never offers it.
- Action::Resume => return,
- };
+ let PveTreeNode::Resource(resource) = record else {
+ return false;
+ };
+ let resource = resource.as_resource_view();
- match res {
- Ok(upid) => link.show_task_progress(upid.to_string()),
- Err(err) => link.show_error(tr!("Error"), err.to_string(), true),
- }
- }),
- PveTreeNode::Qemu(r) => ctx.link().spawn(async move {
- let res = match action {
- Action::Start => {
- crate::pdm_client()
- .pve_qemu_start(&remote, Some(&r.node), r.vmid)
- .await
- }
- Action::Shutdown => {
- crate::pdm_client()
- .pve_qemu_shutdown(&remote, Some(&r.node), r.vmid)
- .await
- }
- Action::Resume => {
- crate::pdm_client()
- .pve_qemu_resume(&remote, Some(&r.node), r.vmid)
- .await
- }
- };
+ let Some(guest_node) = resource.node().map(|n| n.to_string()) else {
+ return false;
+ };
+ let Some(vmid) = resource.vmid() else {
+ return false;
+ };
+ let Some(guest_type) = resource.guest_type() else {
+ return false;
+ };
- match res {
- Ok(upid) => link.show_task_progress(upid.to_string()),
- Err(err) => link.show_error(tr!("Error"), err.to_string(), true),
+ ctx.link().spawn(async move {
+ let client = crate::pdm_client();
+ let node = Some(guest_node.as_str());
+ let res = match (guest_type.into(), action) {
+ (GuestType::Qemu, Action::Start) => {
+ client.pve_qemu_start(&remote, node, vmid).await
}
- }),
- _ => {}
- }
+ (GuestType::Qemu, Action::Shutdown) => {
+ client.pve_qemu_shutdown(&remote, node, vmid).await
+ }
+ (GuestType::Qemu, Action::Resume) => {
+ client.pve_qemu_resume(&remote, node, vmid).await
+ }
+ (GuestType::Lxc, Action::Start) => {
+ client.pve_lxc_start(&remote, node, vmid).await
+ }
+ (GuestType::Lxc, Action::Shutdown) => {
+ client.pve_lxc_shutdown(&remote, node, vmid).await
+ }
+ // LXC resume is not exposed yet, the UI never offers it.
+ (GuestType::Lxc, Action::Resume) => return,
+ };
+
+ match res {
+ Ok(upid) => link.show_task_progress(upid.to_string()),
+ Err(err) => link.show_error(tr!("Error"), err.to_string(), true),
+ }
+ });
}
Msg::KeySelected(key) => {
let key = key.unwrap_or_else(|| Key::from("__root__"));
@@ -430,26 +407,28 @@ impl LoadableComponent for PveTreeComp {
} else {
let text = self.filter.to_lowercase();
self.store.set_filter(move |node: &PveTreeNode| match node {
- PveTreeNode::Lxc(r) => {
+ PveTreeNode::Resource(PveResource::Lxc(r)) => {
r.vmid.to_string().to_lowercase().contains(&text)
|| r.name.to_lowercase().contains(&text)
|| "lxc".contains(&text)
|| r.tags.iter().any(|tag| tag.contains(&text))
}
- PveTreeNode::Qemu(r) => {
+ PveTreeNode::Resource(PveResource::Qemu(r)) => {
r.vmid.to_string().to_lowercase().contains(&text)
|| r.name.to_lowercase().contains(&text)
|| "qemu".contains(&text)
|| r.tags.iter().any(|tag| tag.contains(&text))
}
- PveTreeNode::Storage(r) => {
+ PveTreeNode::Resource(PveResource::Storage(r)) => {
r.storage.to_string().to_lowercase().contains(&text)
|| "storage".contains(&text)
}
// always show tree root node to ensure tree does not look odd.
// For now also always show all nodes (should we filter those without any
// matches for the node or for it's sub elements?
- PveTreeNode::Root | PveTreeNode::Node(_) => true,
+ PveTreeNode::Root | PveTreeNode::Resource(PveResource::Node(_)) => true,
+ // hide the networks for now
+ PveTreeNode::Resource(PveResource::Network(_)) => false,
});
}
}
@@ -582,7 +561,7 @@ impl LoadableComponent for PveTreeComp {
}
fn create_empty_node(node_id: String) -> PveTreeNode {
- PveTreeNode::Node(PveNodeResource {
+ PveTreeNode::Resource(PveResource::Node(PveNodeResource {
cgroup_mode: Default::default(),
cpu: Default::default(),
maxcpu: Default::default(),
@@ -593,7 +572,7 @@ fn create_empty_node(node_id: String) -> PveTreeNode {
uptime: Default::default(),
status: Default::default(),
level: Default::default(),
- })
+ }))
}
fn columns(
@@ -620,15 +599,8 @@ fn columns(
Container::new().with_child(Fa::new("server")),
tr!("Datacenter"),
),
- PveTreeNode::Node(r) => (utils::render_node_status_icon(r), r.node.to_string()),
- PveTreeNode::Qemu(r) => {
- (utils::render_qemu_status_icon(r), render_qemu_name(r, true))
- }
- PveTreeNode::Lxc(r) => {
- (utils::render_lxc_status_icon(r), render_lxc_name(r, true))
- }
- PveTreeNode::Storage(r) => {
- (utils::render_storage_status_icon(r), r.storage.clone())
+ PveTreeNode::Resource(r) => {
+ (render_status_icon(r), render_resource_name(r, true))
}
};
@@ -638,8 +610,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::Resource(PveResource::Lxc(lxc)) => {
+ render_guest_tags(&lxc.tags[..]).into()
+ }
+ PveTreeNode::Resource(PveResource::Qemu(qemu)) => {
+ render_guest_tags(&qemu.tags[..]).into()
+ }
_ => html! {},
})
.into(),
@@ -647,39 +623,37 @@ fn columns(
.width("180px")
.render(move |entry: &PveTreeNode| {
let (id, local_id, guest_info, node) = match entry {
- PveTreeNode::Lxc(r) => {
- let guest_info = GuestInfo::new(GuestType::Lxc, r.vmid);
- let local_id = guest_info.local_id();
- (
- r.id.as_str(),
- local_id,
- Some((guest_info, r.status.as_str(), r.template)),
- Some(r.node.clone()),
- )
- }
- PveTreeNode::Qemu(r) => {
- let guest_info = GuestInfo::new(GuestType::Qemu, r.vmid);
- let local_id = guest_info.local_id();
- (
- r.id.as_str(),
- local_id,
- Some((guest_info, r.status.as_str(), r.template)),
- Some(r.node.clone()),
- )
- }
PveTreeNode::Root => ("root", "root".to_string(), None, None),
- PveTreeNode::Node(r) => (
- r.id.as_str(),
- format!("node/{}", r.node),
- None,
- Some(r.node.clone()),
- ),
- PveTreeNode::Storage(r) => (
- r.id.as_str(),
- format!("storage/{}/{}", r.node, r.storage),
- None,
- Some(r.node.clone()),
- ),
+ PveTreeNode::Resource(r) => match r {
+ PveResource::Lxc(r) => {
+ let guest_info = GuestInfo::new(GuestType::Lxc, r.vmid);
+ let local_id = guest_info.local_id();
+ (
+ r.id.as_str(),
+ local_id,
+ Some((guest_info, r.status.as_str(), r.template)),
+ Some(r.node.clone()),
+ )
+ }
+ PveResource::Qemu(r) => {
+ let guest_info = GuestInfo::new(GuestType::Qemu, r.vmid);
+ let local_id = guest_info.local_id();
+ (
+ r.id.as_str(),
+ local_id,
+ Some((guest_info, r.status.as_str(), r.template)),
+ Some(r.node.clone()),
+ )
+ }
+ PveResource::Node(_)
+ | PveResource::Storage(_)
+ | PveResource::Network(_) => (
+ r.as_resource_view().global_id(),
+ r.as_resource_view().id(),
+ None,
+ r.as_resource_view().node().map(|n| n.to_string()),
+ ),
+ },
};
Row::new()
--
2.47.3
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH datacenter-manager 7/8] ui: pve: show ha maintenance mode for nodes
2026-08-14 13:43 [PATCH datacenter-manager 0/8] refactor and partially fix #7371 Dominik Csapak
` (5 preceding siblings ...)
2026-08-14 13:44 ` [PATCH datacenter-manager 6/8] ui: pve: tree: reuse `PveResource` for `PveTreeNode` Dominik Csapak
@ 2026-08-14 13:44 ` Dominik Csapak
2026-08-14 13:44 ` [PATCH datacenter-manager 8/8] ui: pve: node selector: show maintenance badge with node name Dominik Csapak
7 siblings, 0 replies; 9+ messages in thread
From: Dominik Csapak @ 2026-08-14 13:44 UTC (permalink / raw)
To: pdm-devel
By rendering a small `Badge` after the nodename in the pve tree and the
resource tree, when the node is in ha maintenance mode.
I opted for showing the ha state here differently than in PVE, because
seeing the online/offline state separately from the hastat can make
sense (so there is no ambiguity if the node is online or offline).
To do this we have to add the hastate to the `PveNodeResource` and wire
that through from the /cluster/resources call.
We also have to update the pwt-assets submodule to get the necessary CSS
classes for the badge.
This partially fixes #7371.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
lib/pdm-api-types/src/resource.rs | 2 ++
server/src/api/resources.rs | 1 +
ui/pwt-assets | 2 +-
ui/src/pve/tree.rs | 11 +++++++++--
ui/src/renderer.rs | 14 +++++++++++++-
ui/src/widget/resource_tree.rs | 3 ++-
6 files changed, 28 insertions(+), 5 deletions(-)
diff --git a/lib/pdm-api-types/src/resource.rs b/lib/pdm-api-types/src/resource.rs
index 18177dab..0db56dc4 100644
--- a/lib/pdm-api-types/src/resource.rs
+++ b/lib/pdm-api-types/src/resource.rs
@@ -490,6 +490,8 @@ pub struct PveNodeResource {
pub status: String,
/// Subscription level
pub level: String,
+ /// HA State
+ pub hastate: String,
}
#[api]
diff --git a/server/src/api/resources.rs b/server/src/api/resources.rs
index 09d2b88d..2e4b3aba 100644
--- a/server/src/api/resources.rs
+++ b/server/src/api/resources.rs
@@ -1189,6 +1189,7 @@ pub(super) fn map_pve_node(remote: &str, resource: ClusterResource) -> Option<Pv
uptime: resource.uptime.unwrap_or_default() as u64,
status: resource.status.unwrap_or_default(),
level: resource.level.unwrap_or_default(),
+ hastate: resource.hastate.unwrap_or_default(),
}),
_ => None,
}
diff --git a/ui/pwt-assets b/ui/pwt-assets
index cd2819ed..5b3a866f 160000
--- a/ui/pwt-assets
+++ b/ui/pwt-assets
@@ -1 +1 @@
-Subproject commit cd2819edb53c4b01a9df7f0a2f1f43736ff677fa
+Subproject commit 5b3a866ff9e2a0945bf5d84176d0d23ae83ef782
diff --git a/ui/src/pve/tree.rs b/ui/src/pve/tree.rs
index f68c728e..75eaa362 100644
--- a/ui/src/pve/tree.rs
+++ b/ui/src/pve/tree.rs
@@ -29,7 +29,9 @@ use pdm_api_types::{
use crate::{
get_deep_url,
- renderer::{render_resource_name, render_status_icon, render_tree_column},
+ renderer::{
+ render_resource_extra, render_resource_name, render_status_icon, render_tree_column,
+ },
widget::MigrateWindow,
};
@@ -572,6 +574,7 @@ fn create_empty_node(node_id: String) -> PveTreeNode {
uptime: Default::default(),
status: Default::default(),
level: Default::default(),
+ hastate: Default::default(),
}))
}
@@ -590,6 +593,7 @@ fn columns(
.flex(1)
.tree_column(store)
.render(move |entry: &PveTreeNode| {
+ let mut extra = None;
let (icon, text) = match entry {
PveTreeNode::Root if loading => (
Container::from_tag("i").class("pwt-loading-icon"),
@@ -600,11 +604,14 @@ fn columns(
tr!("Datacenter"),
),
PveTreeNode::Resource(r) => {
+ extra = render_resource_extra(r);
(render_status_icon(r), render_resource_name(r, true))
}
};
- render_tree_column(icon.into(), text).into()
+ render_tree_column(icon.into(), text)
+ .with_optional_child(extra)
+ .into()
})
.into(),
DataTableColumn::new(tr!("Tags"))
diff --git a/ui/src/renderer.rs b/ui/src/renderer.rs
index 0eecc889..b116fab8 100644
--- a/ui/src/renderer.rs
+++ b/ui/src/renderer.rs
@@ -2,7 +2,7 @@ use proxmox_yew_comp::MeterLabel;
use pwt::css::{AlignItems, FlexFit, FontStyle, JustifyContent};
use pwt::prelude::*;
use pwt::props::ContainerBuilder;
-use pwt::widget::{Column, Container, Fa, Row};
+use pwt::widget::{Badge, Column, Container, Fa, Row};
use proxmox_human_byte::HumanByte;
@@ -22,6 +22,18 @@ pub fn render_resource_name(resource: impl AsResourceView, vmid_first: bool) ->
}
}
+pub fn render_resource_extra(resource: impl AsResourceView) -> Option<Html> {
+ match resource.as_resource_view() {
+ ResourceView::PveNode(node) => (node.hastate == "maintenance").then_some(
+ Badge::new(tr!("HA Maintenance"))
+ .color_scheme(pwt::css::ColorScheme::PrimaryContainer)
+ .icon("wrench")
+ .into(),
+ ),
+ _ => None,
+ }
+}
+
pub fn render_resource_icon(resource: impl AsResourceView) -> Fa {
let class = match resource.as_resource_view() {
ResourceView::PveStorage(_) => "database",
diff --git a/ui/src/widget/resource_tree.rs b/ui/src/widget/resource_tree.rs
index 337d8e23..bf2ffee3 100644
--- a/ui/src/widget/resource_tree.rs
+++ b/ui/src/widget/resource_tree.rs
@@ -35,7 +35,7 @@ use crate::{
dashboard::view::ViewContext,
get_deep_url,
pve::utils::render_guest_tags,
- renderer::{render_resource_name, render_status_icon},
+ renderer::{render_resource_extra, render_resource_name, render_status_icon},
};
const REFRESH_TIME_S: u32 = 60;
@@ -381,6 +381,7 @@ fn columns(
Row::new()
.gap(1)
.with_child(render_resource_name(resource, true))
+ .with_optional_child(render_resource_extra(resource))
.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[..],
--
2.47.3
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH datacenter-manager 8/8] ui: pve: node selector: show maintenance badge with node name
2026-08-14 13:43 [PATCH datacenter-manager 0/8] refactor and partially fix #7371 Dominik Csapak
` (6 preceding siblings ...)
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
7 siblings, 0 replies; 9+ messages in thread
From: Dominik Csapak @ 2026-08-14 13:44 UTC (permalink / raw)
To: pdm-devel
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
^ permalink raw reply related [flat|nested] 9+ messages in thread