From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 9C6941FF17A for ; Tue, 28 Oct 2025 17:44:14 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id A38AC20300; Tue, 28 Oct 2025 17:44:43 +0100 (CET) From: Shannon Sterz To: pdm-devel@lists.proxmox.com Date: Tue, 28 Oct 2025 17:44:32 +0100 Message-ID: <20251028164435.576642-4-s.sterz@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20251028164435.576642-1-s.sterz@proxmox.com> References: <20251028164435.576642-1-s.sterz@proxmox.com> MIME-Version: 1.0 X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1761669866317 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.058 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pdm-devel] [PATCH yew-comp 2/2] node status panel: add a panel that show the current status of a node X-BeenThere: pdm-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Datacenter Manager development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" it also allows shutting down or reloading the node. Signed-off-by: Shannon Sterz --- Cargo.toml | 1 + src/lib.rs | 3 + src/node_status_panel.rs | 244 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 248 insertions(+) create mode 100644 src/node_status_panel.rs diff --git a/Cargo.toml b/Cargo.toml index 235aaea..12421b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,7 @@ proxmox-apt-api-types = { version = "2.0", optional = true } proxmox-access-control = "1.1" proxmox-dns-api = { version = "1", optional = true } proxmox-network-api = { version = "1", optional = true } +proxmox-node-status = { version = "1", features = [] } pve-api-types = "8" pbs-api-types = "1" diff --git a/src/lib.rs b/src/lib.rs index 3a9e32b..e097b05 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,6 +91,9 @@ pub use loadable_component::{ mod node_info; pub use node_info::{node_info, NodeStatus}; +mod node_status_panel; +pub use node_status_panel::NodeStatusPanel; + mod notes_view; pub use notes_view::{NotesView, NotesWithDigest, ProxmoxNotesView}; diff --git a/src/node_status_panel.rs b/src/node_status_panel.rs new file mode 100644 index 0000000..da21feb --- /dev/null +++ b/src/node_status_panel.rs @@ -0,0 +1,244 @@ +use std::future::Future; +use std::rc::Rc; + +use anyhow::Error; +use html::IntoPropValue; +use pwt::css::{AlignItems, ColorScheme}; +use pwt::widget::form::DisplayField; +use yew::virtual_dom::{VComp, VNode}; + +use pwt::prelude::*; +use pwt::widget::{error_message, Fa, Panel, Row, Tooltip}; +use pwt::widget::{Button, Dialog}; +use pwt_macros::builder; + +use proxmox_node_status::{NodePowerCommand, NodeStatus}; + +use crate::utils::copy_text_to_clipboard; +use crate::{ + http_get, http_post, node_info, ConfirmButton, LoadableComponent, LoadableComponentContext, + LoadableComponentMaster, +}; + +#[derive(Properties, Clone, PartialEq)] +#[builder] +pub struct NodeStatusPanel { + /// URL path to load the node's status from. + #[builder(IntoPropValue, into_prop_value)] + #[prop_or_default] + status_base_url: Option, +} + +impl NodeStatusPanel { + pub fn new() -> Self { + yew::props!(Self {}) + } +} + +impl Default for NodeStatusPanel { + fn default() -> Self { + Self::new() + } +} + +enum Msg { + Error(Error), + Loaded(Rc), + RebootOrShutdown(NodePowerCommand), + Reload, +} + +#[derive(PartialEq)] +enum ViewState { + FingerprintDialog, +} + +struct ProxmoxNodeStatusPanel { + node_status: Option>, + error: Option, +} + +impl ProxmoxNodeStatusPanel { + fn change_power_state(&self, ctx: &LoadableComponentContext, command: NodePowerCommand) { + let Some(url) = ctx.props().status_base_url.clone() else { + return; + }; + let link = ctx.link().clone(); + + ctx.link().spawn(async move { + let data = Some(serde_json::json!({ + "command": command, + })); + + match http_post(url.as_str(), data).await { + Ok(()) => link.send_message(Msg::Reload), + Err(err) => link.send_message(Msg::Error(err)), + } + }); + } + + fn fingerprint_dialog( + &self, + ctx: &LoadableComponentContext, + fingerprint: &str, + ) -> Dialog { + let link = ctx.link(); + let link_button = ctx.link(); + let fingerprint = fingerprint.to_owned(); + + Dialog::new(tr!("Fingerprint")) + .resizable(true) + .min_width(500) + .on_close(move |_| link.change_view(None)) + .with_child( + Row::new() + .gap(2) + .margin_start(2) + .margin_end(2) + .with_child( + DisplayField::new() + .class(pwt::css::FlexFit) + .value(fingerprint.clone()) + .border(true), + ) + .with_child( + Tooltip::new( + Button::new_icon("fa fa-clipboard") + .class(ColorScheme::Primary) + .on_activate(move |_| copy_text_to_clipboard(&fingerprint)), + ) + .tip(tr!("Copy token secret to clipboard.")), + ), + ) + .with_child( + Row::new() + .padding(2) + .with_flex_spacer() + .with_child( + Button::new(tr!("OK")).on_activate(move |_| link_button.change_view(None)), + ) + .with_flex_spacer(), + ) + } +} + +impl LoadableComponent for ProxmoxNodeStatusPanel { + type Message = Msg; + type ViewState = ViewState; + type Properties = NodeStatusPanel; + + fn create(ctx: &crate::LoadableComponentContext) -> Self { + ctx.link().repeated_load(5000); + + Self { + node_status: None, + error: None, + } + } + + fn load( + &self, + ctx: &crate::LoadableComponentContext, + ) -> std::pin::Pin>>> { + let url = ctx.props().status_base_url.clone(); + let link = ctx.link().clone(); + + Box::pin(async move { + if let Some(url) = url { + match http_get(url.as_str(), None).await { + Ok(res) => link.send_message(Msg::Loaded(Rc::new(res))), + Err(err) => link.send_message(Msg::Error(err)), + } + } + Ok(()) + }) + } + + fn update(&mut self, ctx: &crate::LoadableComponentContext, msg: Self::Message) -> bool { + match msg { + Msg::Error(err) => { + self.error = Some(err); + true + } + Msg::Loaded(status) => { + self.node_status = Some(status); + self.error = None; + true + } + Msg::RebootOrShutdown(command) => { + self.change_power_state(ctx, command); + false + } + Msg::Reload => true, + } + } + + fn dialog_view( + &self, + ctx: &LoadableComponentContext, + view_state: &Self::ViewState, + ) -> Option { + if view_state == &ViewState::FingerprintDialog { + if let Some(ref node_status) = self.node_status { + return Some( + self.fingerprint_dialog(&ctx, &node_status.info.fingerprint) + .into(), + ); + } + } + None + } + + fn main_view(&self, ctx: &crate::LoadableComponentContext) -> Html { + let status = self + .node_status + .as_ref() + .map(|r| crate::NodeStatus::Common(r)); + + Panel::new() + .title( + Row::new() + .class(AlignItems::Center) + .gap(2) + .with_child(Fa::new("book")) + .with_child(tr!("Node Status")) + .into_html(), + ) + .with_tool( + ConfirmButton::new(tr!("Reboot")) + .confirm_message(tr!("Are you sure you want to reboot the node?")) + .on_activate( + ctx.link() + .callback(|_| Msg::RebootOrShutdown(NodePowerCommand::Reboot)), + ) + .icon_class("fa fa-undo"), + ) + .with_tool( + ConfirmButton::new(tr!("Shutdown")) + .confirm_message(tr!("Are you sure you want to shut down the node?")) + .on_activate( + ctx.link() + .callback(|_| Msg::RebootOrShutdown(NodePowerCommand::Shutdown)), + ) + .icon_class("fa fa-power-off"), + ) + .with_tool( + Button::new(tr!("Show Fingerprint")) + .icon_class("fa fa-hashtag") + .class(ColorScheme::Primary) + .on_activate( + ctx.link() + .change_view_callback(|_| ViewState::FingerprintDialog), + ), + ) + .with_child(node_info(status)) + .with_optional_child(self.error.as_ref().map(|e| error_message(&e.to_string()))) + .into() + } +} + +impl From for VNode { + fn from(value: NodeStatusPanel) -> Self { + VComp::new::>(Rc::new(value), None).into() + } +} -- 2.47.3 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel