From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 7D6461FF0AA for ; Fri, 21 Aug 2026 14:32:16 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id B4BE32162B; Fri, 21 Aug 2026 14:32:07 +0200 (CEST) From: Dominik Csapak To: pdm-devel@lists.proxmox.com Subject: [PATCH datacenter-manager v3 7/9] ui: pve: show ha maintenance mode for nodes Date: Fri, 21 Aug 2026 14:30:19 +0200 Message-ID: <20260821123201.3035643-8-d.csapak@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260821123201.3035643-1-d.csapak@proxmox.com> References: <20260821123201.3035643-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.652 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) POISEN_SPAM_PILL 0.1 Meta: its spam POISEN_SPAM_PILL_1 0.1 random spam to be learned in bayes POISEN_SPAM_PILL_3 0.1 random spam to be learned in bayes RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: UPA6YZJOJFPIQ7EWXUNGGBXNISO4J4JW X-Message-ID-Hash: UPA6YZJOJFPIQ7EWXUNGGBXNISO4J4JW X-MailFrom: d.csapak@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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 --- 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, } #[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 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 { + 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