* [PATCH datacenter-manager v3 1/9] lib: api types: add new ResourceView type and move accessors there
2026-08-21 12:30 [PATCH datacenter-manager v3 0/9] refactor and partially fix #7371 Dominik Csapak
@ 2026-08-21 12:30 ` Dominik Csapak
2026-08-21 12:30 ` [PATCH datacenter-manager v3 2/9] lib: api types: resource: add 'node' helper to ResourceView Dominik Csapak
` (7 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Dominik Csapak @ 2026-08-21 12:30 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 an AsResourceView trait to make converting more
ergonomic.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
lib/pdm-api-types/src/resource.rs | 273 +++++++++++++++++++++---------
1 file changed, 196 insertions(+), 77 deletions(-)
diff --git a/lib/pdm-api-types/src/resource.rs b/lib/pdm-api-types/src/resource.rs
index 303ac3cb..05cf297c 100644
--- a/lib/pdm-api-types/src/resource.rs
+++ b/lib/pdm-api-types/src/resource.rs
@@ -45,102 +45,36 @@ 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()
}
+ /// Returns the resource type, e.g. Node, Qemu, etc.
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()
}
+ /// Returns the status of the resource, e.g. 'online' or 'offline'. The exact
+ /// status values depends on the 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()
}
+ /// Returns additional properties of the resource. Only a PBS datastore
+ /// returns values.
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 +163,191 @@ pub enum PveResource {
Network(PveNetworkResource),
}
+/// A borrowed view of a resource that can come from a [Resource] or a [PveResource] This is useful
+/// for having a single implementation of field access regardless of 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(&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(),
+ }
+ }
+
+ /// Returns the resource type, e.g. Node, Qemu, etc.
+ 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,
+ }
+ }
+
+ /// Returns the status of the resource, e.g. 'online' or 'offline'. The exact
+ /// status values depends on the resource type.
+ 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"
+ }
+ }
+ }
+ }
+
+ /// Returns additional properties of the resource. Only a PBS datastore
+ /// returns values.
+ 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),
+ }
+ }
+}
+
+/// A trait to that allows any struct which contains resources (e.g. PveStorageResource,
+/// PveQemuResource, etc.) to be represented with a lifetime, so a reference. This makes it
+/// possible to use a reference to such a resource in a generic manner.
+pub trait AsResourceView {
+ fn as_resource_view(&self) -> ResourceView<'_>;
+}
+
+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] 10+ messages in thread* [PATCH datacenter-manager v3 2/9] lib: api types: resource: add 'node' helper to ResourceView
2026-08-21 12:30 [PATCH datacenter-manager v3 0/9] refactor and partially fix #7371 Dominik Csapak
2026-08-21 12:30 ` [PATCH datacenter-manager v3 1/9] lib: api types: add new ResourceView type and move accessors there Dominik Csapak
@ 2026-08-21 12:30 ` Dominik Csapak
2026-08-21 12:30 ` [PATCH datacenter-manager v3 3/9] lib: api types: add guest specific getter " Dominik Csapak
` (6 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Dominik Csapak @ 2026-08-21 12:30 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 05cf297c..564e2fc2 100644
--- a/lib/pdm-api-types/src/resource.rs
+++ b/lib/pdm-api-types/src/resource.rs
@@ -234,6 +234,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)
+ }
+
/// Returns the resource type, e.g. Node, Qemu, etc.
pub fn resource_type(&self) -> ResourceType {
match self {
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] 10+ messages in thread* [PATCH datacenter-manager v3 3/9] lib: api types: add guest specific getter to ResourceView
2026-08-21 12:30 [PATCH datacenter-manager v3 0/9] refactor and partially fix #7371 Dominik Csapak
2026-08-21 12:30 ` [PATCH datacenter-manager v3 1/9] lib: api types: add new ResourceView type and move accessors there Dominik Csapak
2026-08-21 12:30 ` [PATCH datacenter-manager v3 2/9] lib: api types: resource: add 'node' helper to ResourceView Dominik Csapak
@ 2026-08-21 12:30 ` Dominik Csapak
2026-08-21 12:30 ` [PATCH datacenter-manager v3 4/9] ui: pve: factor out the pve-manager version extraction Dominik Csapak
` (5 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Dominik Csapak @ 2026-08-21 12:30 UTC (permalink / raw)
To: pdm-devel
This is useful for the UI to get the VMID and guest type independent of
the exact resource 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 564e2fc2..de2a93b4 100644
--- a/lib/pdm-api-types/src/resource.rs
+++ b/lib/pdm-api-types/src/resource.rs
@@ -247,6 +247,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,
+ }
+ }
+
/// Returns the resource type, e.g. Node, Qemu, etc.
pub fn resource_type(&self) -> ResourceType {
match self {
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] 10+ messages in thread* [PATCH datacenter-manager v3 4/9] ui: pve: factor out the pve-manager version extraction
2026-08-21 12:30 [PATCH datacenter-manager v3 0/9] refactor and partially fix #7371 Dominik Csapak
` (2 preceding siblings ...)
2026-08-21 12:30 ` [PATCH datacenter-manager v3 3/9] lib: api types: add guest specific getter " Dominik Csapak
@ 2026-08-21 12:30 ` Dominik Csapak
2026-08-21 12:30 ` [PATCH datacenter-manager v3 5/9] ui: renderer: use ResourceView for rendering Dominik Csapak
` (4 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Dominik Csapak @ 2026-08-21 12:30 UTC (permalink / raw)
To: pdm-devel
This was done three times in the same way. Simply refactor it into
its 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] 10+ messages in thread* [PATCH datacenter-manager v3 5/9] ui: renderer: use ResourceView for rendering
2026-08-21 12:30 [PATCH datacenter-manager v3 0/9] refactor and partially fix #7371 Dominik Csapak
` (3 preceding siblings ...)
2026-08-21 12:30 ` [PATCH datacenter-manager v3 4/9] ui: pve: factor out the pve-manager version extraction Dominik Csapak
@ 2026-08-21 12:30 ` Dominik Csapak
2026-08-21 12:30 ` [PATCH datacenter-manager v3 6/9] ui: pve: tree: reuse `PveResource` for `PveTreeNode` Dominik Csapak
` (3 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Dominik Csapak @ 2026-08-21 12:30 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] 10+ messages in thread* [PATCH datacenter-manager v3 6/9] ui: pve: tree: reuse `PveResource` for `PveTreeNode`
2026-08-21 12:30 [PATCH datacenter-manager v3 0/9] refactor and partially fix #7371 Dominik Csapak
` (4 preceding siblings ...)
2026-08-21 12:30 ` [PATCH datacenter-manager v3 5/9] ui: renderer: use ResourceView for rendering Dominik Csapak
@ 2026-08-21 12:30 ` Dominik Csapak
2026-08-21 12:30 ` [PATCH datacenter-manager v3 7/9] ui: pve: show ha maintenance mode for nodes Dominik Csapak
` (2 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Dominik Csapak @ 2026-08-21 12:30 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 ResourceTree) 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 | 293 +++++++++++++++++++++------------------------
2 files changed, 158 insertions(+), 175 deletions(-)
diff --git a/ui/src/pve/mod.rs b/ui/src/pve/mod.rs
index 1c32d26f..340fc731 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()
- }
+ // the tree does not show the networks currently
+ 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..4ea5716a 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.name()),
+ },
}
}
}
@@ -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,29 @@ 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(a), PveResource::Network(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 +327,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 +408,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,
+ // don't show the networks
+ PveTreeNode::Resource(PveResource::Network(_)) => false,
});
}
}
@@ -582,7 +562,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 +573,7 @@ fn create_empty_node(node_id: String) -> PveTreeNode {
uptime: Default::default(),
status: Default::default(),
level: Default::default(),
- })
+ }))
}
fn columns(
@@ -620,15 +600,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 +611,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 +624,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] 10+ messages in thread* [PATCH datacenter-manager v3 7/9] ui: pve: show ha maintenance mode for nodes
2026-08-21 12:30 [PATCH datacenter-manager v3 0/9] refactor and partially fix #7371 Dominik Csapak
` (5 preceding siblings ...)
2026-08-21 12:30 ` [PATCH datacenter-manager v3 6/9] ui: pve: tree: reuse `PveResource` for `PveTreeNode` Dominik Csapak
@ 2026-08-21 12:30 ` Dominik Csapak
2026-08-21 12:30 ` [PATCH datacenter-manager v3 8/9] ui: pve: node selector: show maintenance badge with node name Dominik Csapak
2026-08-21 12:30 ` [PATCH datacenter-manager v3 9/9] ui: pve: node: show ha maintenance badge Dominik Csapak
8 siblings, 0 replies; 10+ messages in thread
From: Dominik Csapak @ 2026-08-21 12:30 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 hastate 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 | 26 ++++++++++++++++++++++++++
server/src/api/resources.rs | 1 +
ui/pwt-assets | 2 +-
ui/src/pve/tree.rs | 11 +++++++++--
ui/src/renderer.rs | 19 +++++++++++++++++--
ui/src/widget/resource_tree.rs | 3 ++-
6 files changed, 56 insertions(+), 6 deletions(-)
diff --git a/lib/pdm-api-types/src/resource.rs b/lib/pdm-api-types/src/resource.rs
index de2a93b4..29d70a3e 100644
--- a/lib/pdm-api-types/src/resource.rs
+++ b/lib/pdm-api-types/src/resource.rs
@@ -477,6 +477,29 @@ pub struct PveLxcResource {
pub vmid: u32,
}
+#[api]
+#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
+#[serde(rename_all = "kebab-case")]
+/// HA State of a PVE node.
+pub enum PveNodeHaState {
+ /// The node is online and member of quorate partition
+ Online,
+ /// Node is a member of a quorate partition but in maintenance mode
+ Maintenance,
+ /// Not member of quorate partition, but possibly still running
+ Unknown,
+ /// Node needs to be fenced
+ Fence,
+ /// Node vanished from cluster member list
+ Gone,
+ /// Other (unknown) HA state
+ #[serde(untagged)]
+ Other(String),
+}
+
+serde_plain::derive_display_from_serialize!(PveNodeHaState);
+serde_plain::derive_fromstr_from_deserialize!(PveNodeHaState);
+
#[api]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
@@ -502,6 +525,9 @@ pub struct PveNodeResource {
pub status: String,
/// Subscription level
pub level: String,
+ /// HA State
+ #[serde(default)]
+ pub ha_state: Option<PveNodeHaState>,
}
#[api]
diff --git a/server/src/api/resources.rs b/server/src/api/resources.rs
index 09d2b88d..69a8df8e 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(),
+ ha_state: resource.hastate.and_then(|state| state.parse().ok()),
}),
_ => 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 4ea5716a..4a674759 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_info, render_resource_name, render_status_icon, render_tree_column,
+ },
widget::MigrateWindow,
};
@@ -573,6 +575,7 @@ fn create_empty_node(node_id: String) -> PveTreeNode {
uptime: Default::default(),
status: Default::default(),
level: Default::default(),
+ ha_state: Default::default(),
}))
}
@@ -591,6 +594,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"),
@@ -601,11 +605,14 @@ fn columns(
tr!("Datacenter"),
),
PveTreeNode::Resource(r) => {
+ extra = render_resource_extra_info(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..38719e1d 100644
--- a/ui/src/renderer.rs
+++ b/ui/src/renderer.rs
@@ -2,11 +2,11 @@ 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;
-use pdm_api_types::resource::{AsResourceView, ResourceView};
+use pdm_api_types::resource::{AsResourceView, PveNodeHaState, ResourceView};
use crate::pve;
@@ -22,6 +22,21 @@ pub fn render_resource_name(resource: impl AsResourceView, vmid_first: bool) ->
}
}
+pub fn render_resource_extra_info(resource: impl AsResourceView) -> Option<Html> {
+ match resource.as_resource_view() {
+ ResourceView::PveNode(node) => match node.ha_state.as_ref()? {
+ PveNodeHaState::Maintenance => Some(
+ Badge::new(tr!("HA Maintenance"))
+ .color_scheme(pwt::css::ColorScheme::PrimaryContainer)
+ .icon("wrench")
+ .into(),
+ ),
+ _ => None,
+ },
+ _ => 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..0681c24d 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_info, 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_info(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] 10+ messages in thread* [PATCH datacenter-manager v3 8/9] ui: pve: node selector: show maintenance badge with node name
2026-08-21 12:30 [PATCH datacenter-manager v3 0/9] refactor and partially fix #7371 Dominik Csapak
` (6 preceding siblings ...)
2026-08-21 12:30 ` [PATCH datacenter-manager v3 7/9] ui: pve: show ha maintenance mode for nodes Dominik Csapak
@ 2026-08-21 12:30 ` Dominik Csapak
2026-08-21 12:30 ` [PATCH datacenter-manager v3 9/9] ui: pve: node: show ha maintenance badge Dominik Csapak
8 siblings, 0 replies; 10+ messages in thread
From: Dominik Csapak @ 2026-08-21 12:30 UTC (permalink / raw)
To: pdm-devel
So when selecting a migration target for example, 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 | 91 ++++++++++++++++++------------
2 files changed, 62 insertions(+), 35 deletions(-)
diff --git a/lib/pdm-api-types/src/resource.rs b/lib/pdm-api-types/src/resource.rs
index 29d70a3e..48af8bd5 100644
--- a/lib/pdm-api-types/src/resource.rs
+++ b/lib/pdm-api-types/src/resource.rs
@@ -355,6 +355,12 @@ pub trait AsResourceView {
fn as_resource_view(&self) -> ResourceView<'_>;
}
+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..d471780e 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_info;
#[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,55 @@ 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_info(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| {
+ if entry.status != "online" {
+ return html! {};
+ }
+ rrd_value_renderer::render_cpu_usage(&entry.cpu).into()
})
- .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.
+ .sorter(|a: &PveNodeResource, b: &PveNodeResource| {
+ let a = if a.status == "online" {
a.cpu
- .unwrap_or(f64::NEG_INFINITY)
- .total_cmp(&b.cpu.unwrap_or(f64::NEG_INFINITY))
- },
- )
+ } else {
+ f64::NEG_INFINITY
+ };
+ let b = if b.status == "online" {
+ b.cpu
+ } else {
+ f64::NEG_INFINITY
+ };
+ a.total_cmp(&b)
+ })
.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| {
+ // don't show values when the node isn't online or when it's non sensible
+ if entry.maxmem == 0 || entry.status != "online" {
+ return html! {};
+ }
+ 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] 10+ messages in thread* [PATCH datacenter-manager v3 9/9] ui: pve: node: show ha maintenance badge
2026-08-21 12:30 [PATCH datacenter-manager v3 0/9] refactor and partially fix #7371 Dominik Csapak
` (7 preceding siblings ...)
2026-08-21 12:30 ` [PATCH datacenter-manager v3 8/9] ui: pve: node selector: show maintenance badge with node name Dominik Csapak
@ 2026-08-21 12:30 ` Dominik Csapak
8 siblings, 0 replies; 10+ messages in thread
From: Dominik Csapak @ 2026-08-21 12:30 UTC (permalink / raw)
To: pdm-devel
in the title of the node panel, so it's visible on all tabs, including
the shell, where admins might do the maintenance.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/pve/mod.rs | 11 ++++++++++-
ui/src/pve/node/mod.rs | 15 +++++++++++----
2 files changed, 21 insertions(+), 5 deletions(-)
diff --git a/ui/src/pve/mod.rs b/ui/src/pve/mod.rs
index 340fc731..3513a814 100644
--- a/ui/src/pve/mod.rs
+++ b/ui/src/pve/mod.rs
@@ -216,7 +216,16 @@ impl LoadableComponent for PveRemoteComp {
PveTreeNode::Root => remote::PveRemotePanel::new(remote.clone()).into(),
PveTreeNode::Resource(r) => match r {
PveResource::Node(node) => {
- node::PveNodePanel::new(remote.clone(), node.node.clone())
+ let info = self
+ .resources
+ .iter()
+ .find_map(|r: &PveResource| match r {
+ PveResource::Node(info) if node.id == info.id => Some(info),
+ _ => None,
+ })
+ .unwrap_or(node)
+ .clone();
+ node::PveNodePanel::new(remote.clone(), node.node.clone(), info)
.pve_manager_version(self.get_pve_manager_version(&node.node))
.into()
}
diff --git a/ui/src/pve/node/mod.rs b/ui/src/pve/node/mod.rs
index 056f15cd..55e28a81 100644
--- a/ui/src/pve/node/mod.rs
+++ b/ui/src/pve/node/mod.rs
@@ -13,13 +13,15 @@ use pwt::{
};
use pwt_macros::builder;
+use pdm_api_types::resource::{PveNodeResource, ResourceView};
+
mod overview;
use overview::PveNodeOverviewPanel;
-use crate::get_deep_url;
+use crate::{get_deep_url, renderer::render_resource_extra_info};
-#[derive(Clone, Debug, Eq, PartialEq, Properties)]
+#[derive(Clone, Debug, PartialEq, Properties)]
#[builder]
pub struct PveNodePanel {
/// The remote to show
@@ -28,6 +30,8 @@ pub struct PveNodePanel {
/// The node to show
pub node: String,
+ info: PveNodeResource,
+
#[prop_or_default]
#[builder]
/// The nodes pve-manager version, used to feature gate some entries.
@@ -35,8 +39,8 @@ pub struct PveNodePanel {
}
impl PveNodePanel {
- pub fn new(remote: String, node: String) -> Self {
- yew::props!(Self { remote, node })
+ pub fn new(remote: String, node: String, info: PveNodeResource) -> Self {
+ yew::props!(Self { remote, node, info })
}
}
@@ -64,6 +68,9 @@ impl yew::Component for PveNodePanelComp {
.class(AlignItems::Baseline)
.with_child(Fa::new("building"))
.with_child(tr! {"Node '{0}'", props.node})
+ .with_optional_child(render_resource_extra_info(ResourceView::PveNode(
+ &props.info,
+ )))
.into();
TabPanel::new()
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread