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 766661FF16B for ; Fri, 26 Sep 2025 09:27:30 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 23C8D9531; Fri, 26 Sep 2025 09:27:58 +0200 (CEST) From: Dominik Csapak To: pdm-devel@lists.proxmox.com Date: Fri, 26 Sep 2025 09:20:30 +0200 Message-ID: <20250926072749.560801-11-d.csapak@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20250926072749.560801-1-d.csapak@proxmox.com> References: <20250926072749.560801-1-d.csapak@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.027 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 datacenter-manager 10/11] ui: pbs: add remote overview panel 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" similar to the PVE node overview. Not yet used in this patch. Signed-off-by: Dominik Csapak --- ui/src/pbs/remote.rs | 291 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 ui/src/pbs/remote.rs diff --git a/ui/src/pbs/remote.rs b/ui/src/pbs/remote.rs new file mode 100644 index 00000000..7cf7c7e2 --- /dev/null +++ b/ui/src/pbs/remote.rs @@ -0,0 +1,291 @@ +use std::rc::Rc; + +use yew::{ + virtual_dom::{VComp, VNode}, + Context, +}; + +use proxmox_yew_comp::{node_info, RRDGraph, RRDTimeframe, RRDTimeframeSelector, Series}; +use pwt::{ + css::{AlignItems, ColorScheme, FlexFit, JustifyContent}, + prelude::*, + props::{ContainerBuilder, WidgetBuilder}, + widget::{error_message, Column, Container, Fa, Panel, Progress, Row}, + AsyncPool, +}; + +use pbs_api_types::NodeStatus; +use pdm_api_types::rrddata::PbsNodeDataPoint; + +use crate::renderer::separator; + +#[derive(Clone, Debug, Eq, PartialEq, Properties)] +pub struct RemoteOverviewPanel { + /// The remote to show + pub remote: String, + + #[prop_or(60_000)] + /// The interval for refreshing the rrd data + pub rrd_interval: u32, + + #[prop_or(10_000)] + /// The interval for refreshing the status data + pub status_interval: u32, +} + +impl RemoteOverviewPanel { + pub fn new(remote: String) -> Self { + yew::props!(Self { remote }) + } +} + +impl Into for RemoteOverviewPanel { + fn into(self) -> VNode { + VComp::new::(Rc::new(self), None).into() + } +} + +pub enum Msg { + ReloadRrd, + ReloadStatus, + LoadFinished(Result, proxmox_client::Error>), + StatusLoadFinished(Result), + UpdateRrdTimeframe(RRDTimeframe), +} + +pub struct RemoteOverviewPanelComp { + time_data: Rc>, + cpu_data: Rc, + load_data: Rc, + mem_data: Rc, + mem_total_data: Rc, + status: Option, + + rrd_time_frame: RRDTimeframe, + + last_error: Option, + last_status_error: Option, + + async_pool: AsyncPool, + _timeout: Option, + _status_timeout: Option, +} + +impl RemoteOverviewPanelComp { + async fn reload_rrd(remote: &str, rrd_time_frame: RRDTimeframe) -> Msg { + let res = crate::pdm_client() + .pbs_node_rrddata(remote, rrd_time_frame.mode, rrd_time_frame.timeframe) + .await; + + Msg::LoadFinished(res) + } + + async fn reload_status(remote: &str) -> Result { + let status = crate::pdm_client().pbs_node_status(remote).await?; + Ok(status) + } +} + +impl yew::Component for RemoteOverviewPanelComp { + type Message = Msg; + type Properties = RemoteOverviewPanel; + + fn create(ctx: &yew::Context) -> Self { + ctx.link().send_message(Msg::ReloadRrd); + ctx.link().send_message(Msg::ReloadStatus); + Self { + time_data: Rc::new(Vec::new()), + cpu_data: Rc::new(Series::new("", Vec::new())), + load_data: Rc::new(Series::new("", Vec::new())), + mem_data: Rc::new(Series::new("", Vec::new())), + mem_total_data: Rc::new(Series::new("", Vec::new())), + rrd_time_frame: RRDTimeframe::load(), + status: None, + last_error: None, + last_status_error: None, + async_pool: AsyncPool::new(), + _timeout: None, + _status_timeout: None, + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + match msg { + Msg::ReloadRrd => { + self._timeout = None; + let props = ctx.props(); + let remote = props.remote.clone(); + let timeframe = self.rrd_time_frame; + self.async_pool.send_future(ctx.link().clone(), async move { + Self::reload_rrd(&remote, timeframe).await + }); + } + Msg::ReloadStatus => { + self._status_timeout = None; + let props = ctx.props(); + let remote = props.remote.clone(); + self.async_pool.send_future(ctx.link().clone(), async move { + let res = Self::reload_status(&remote).await; + Msg::StatusLoadFinished(res) + }); + } + Msg::LoadFinished(res) => match res { + Ok(data_points) => { + self.last_error = None; + let mut cpu_vec = Vec::with_capacity(data_points.len()); + let mut load_vec = Vec::with_capacity(data_points.len()); + let mut mem_vec = Vec::with_capacity(data_points.len()); + let mut mem_total_vec = Vec::with_capacity(data_points.len()); + let mut time_vec = Vec::with_capacity(data_points.len()); + for data in data_points { + cpu_vec.push(data.cpu_current.unwrap_or(f64::NAN)); + load_vec.push(data.cpu_avg1.unwrap_or(f64::NAN)); + mem_vec.push(data.mem_used.unwrap_or(f64::NAN)); + mem_total_vec.push(data.mem_total.unwrap_or(f64::NAN)); + time_vec.push(data.time as i64); + } + + self.cpu_data = Rc::new(Series::new(tr!("CPU"), cpu_vec)); + self.load_data = Rc::new(Series::new(tr!("Server Load"), load_vec)); + self.mem_data = Rc::new(Series::new(tr!("Used Memory"), mem_vec)); + self.mem_total_data = Rc::new(Series::new(tr!("Total Memory"), mem_total_vec)); + self.time_data = Rc::new(time_vec); + + let link = ctx.link().clone(); + self._timeout = Some(gloo_timers::callback::Timeout::new( + ctx.props().rrd_interval, + move || link.send_message(Msg::ReloadRrd), + )) + } + Err(err) => self.last_error = Some(err), + }, + Msg::StatusLoadFinished(res) => { + match res { + Ok(status) => { + self.last_status_error = None; + self.status = Some(status); + } + Err(err) => { + self.last_status_error = Some(err); + } + } + let link = ctx.link().clone(); + self._status_timeout = Some(gloo_timers::callback::Timeout::new( + ctx.props().status_interval, + move || link.send_message(Msg::ReloadStatus), + )) + } + Msg::UpdateRrdTimeframe(rrd_time_frame) => { + self.rrd_time_frame = rrd_time_frame; + ctx.link().send_message(Msg::ReloadRrd); + return false; + } + } + true + } + + fn changed(&mut self, ctx: &Context, old_props: &Self::Properties) -> bool { + let props = ctx.props(); + + if props.remote != old_props.remote { + self.status = None; + self.last_error = None; + self.time_data = Rc::new(Vec::new()); + self.cpu_data = Rc::new(Series::new("", Vec::new())); + self.load_data = Rc::new(Series::new("", Vec::new())); + self.mem_data = Rc::new(Series::new("", Vec::new())); + self.mem_total_data = Rc::new(Series::new("", Vec::new())); + self.async_pool = AsyncPool::new(); + ctx.link() + .send_message_batch(vec![Msg::ReloadRrd, Msg::ReloadStatus]); + true + } else { + false + } + } + + fn view(&self, ctx: &yew::Context) -> yew::Html { + let status_comp = node_info(self.status.as_ref().map(|s| s.into())); + + let loading = self.status.is_none() && self.last_status_error.is_none(); + let title: Html = Row::new() + .gap(2) + .class(AlignItems::Baseline) + .with_child(Fa::new("tachometer")) + .with_child(tr! {"Overview"}) + .into(); + Panel::new() + .title(title) + .class(FlexFit) + .class(ColorScheme::Neutral) + .with_child( + // FIXME: add some 'visible' or 'active' property to the progress + Progress::new() + .value((!loading).then_some(0.0)) + .style("opacity", (!loading).then_some("0")), + ) + .with_child(status_comp) + .with_optional_child( + self.last_status_error + .as_ref() + .map(|err| error_message(&err.to_string())), + ) + .with_child(separator().padding_x(4)) + .with_child( + Row::new() + .padding_x(4) + .padding_y(1) + .class(JustifyContent::FlexEnd) + .with_child( + RRDTimeframeSelector::new() + .on_change(ctx.link().callback(Msg::UpdateRrdTimeframe)), + ), + ) + .with_child( + Container::new().class(FlexFit).with_child( + Column::new() + .padding(4) + .gap(4) + .with_child( + RRDGraph::new(self.time_data.clone()) + .title(tr!("CPU Usage")) + .render_value(|v: &f64| { + if v.is_finite() { + format!("{:.2}%", v * 100.0) + } else { + v.to_string() + } + }) + .serie0(Some(self.cpu_data.clone())), + ) + .with_child( + RRDGraph::new(self.time_data.clone()) + .title(tr!("Server load")) + .render_value(|v: &f64| { + if v.is_finite() { + format!("{:.2}", v) + } else { + v.to_string() + } + }) + .serie0(Some(self.load_data.clone())), + ) + .with_child( + RRDGraph::new(self.time_data.clone()) + .title(tr!("Memory Usage")) + .binary(true) + .render_value(|v: &f64| { + if v.is_finite() { + proxmox_human_byte::HumanByte::from(*v as u64).to_string() + } else { + v.to_string() + } + }) + .serie0(Some(self.mem_data.clone())) + .serie1(Some(self.mem_total_data.clone())), + ), + ), + ) + .into() + } +} -- 2.47.3 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel