From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id C9C501FF16F for ; Tue, 2 Sep 2025 17:14:54 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 274D01855C; Tue, 2 Sep 2025 17:15:09 +0200 (CEST) From: Lukas Wagner To: pdm-devel@lists.proxmox.com Date: Tue, 2 Sep 2025 17:14:26 +0200 Message-ID: <20250902151427.425017-10-l.wagner@proxmox.com> X-Mailer: git-send-email 2.47.2 In-Reply-To: <20250902151427.425017-1-l.wagner@proxmox.com> References: <20250902151427.425017-1-l.wagner@proxmox.com> MIME-Version: 1.0 X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1756826057365 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 proxmox-datacenter-manager 3/4] ui: pve: move node overview to a new overview tab 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" This allows us add other tabs later. No functional changes for the overview component, just moving code around and adapting as needed to make it work in a tab panel. Signed-off-by: Lukas Wagner --- ui/src/pve/node/mod.rs | 320 ++------------------------------ ui/src/pve/node/overview.rs | 358 ++++++++++++++++++++++++++++++++++++ 2 files changed, 376 insertions(+), 302 deletions(-) create mode 100644 ui/src/pve/node/overview.rs diff --git a/ui/src/pve/node/mod.rs b/ui/src/pve/node/mod.rs index ff24f7ec..0190611b 100644 --- a/ui/src/pve/node/mod.rs +++ b/ui/src/pve/node/mod.rs @@ -5,20 +5,16 @@ use yew::{ Context, }; -use proxmox_human_byte::HumanByte; -use proxmox_yew_comp::{RRDGraph, RRDTimeframe, RRDTimeframeSelector, Series}; use pwt::{ - css::{AlignItems, ColorScheme, FlexFit, JustifyContent}, + css::{AlignItems, ColorScheme}, prelude::*, props::{ContainerBuilder, WidgetBuilder}, - widget::{error_message, Column, Container, Fa, Panel, Progress, Row}, - AsyncPool, + widget::{Fa, Row, TabBarItem, TabPanel}, }; -use pdm_api_types::rrddata::NodeDataPoint; -use pdm_client::types::NodeStatus; +mod overview; -use crate::renderer::separator; +use overview::NodeOverviewPanel; #[derive(Clone, Debug, Eq, PartialEq, Properties)] pub struct NodePanel { @@ -27,14 +23,6 @@ pub struct NodePanel { /// The node to show pub node: 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 NodePanel { @@ -49,160 +37,20 @@ impl Into for NodePanel { } } -pub enum Msg { - ReloadRrd, - ReloadStatus, - LoadFinished(Result, proxmox_client::Error>), - StatusLoadFinished(Result), - UpdateRrdTimeframe(RRDTimeframe), -} - -pub struct NodePanelComp { - 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 NodePanelComp { - async fn reload_rrd(remote: &str, node: &str, rrd_time_frame: RRDTimeframe) -> Msg { - let res = crate::pdm_client() - .pve_node_rrddata(remote, node, rrd_time_frame.mode, rrd_time_frame.timeframe) - .await; - - Msg::LoadFinished(res) - } - - async fn reload_status(remote: &str, node: &str) -> Result { - let status = crate::pdm_client().pve_node_status(remote, node).await?; - Ok(status) - } -} +pub struct NodePanelComp; impl yew::Component for NodePanelComp { - type Message = Msg; + type Message = (); type Properties = NodePanel; - 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 node = props.node.clone(); - let timeframe = self.rrd_time_frame; - self.async_pool.send_future(ctx.link().clone(), async move { - Self::reload_rrd(&remote, &node, timeframe).await - }); - } - Msg::ReloadStatus => { - self._status_timeout = None; - let props = ctx.props(); - let remote = props.remote.clone(); - let node = props.node.clone(); - self.async_pool.send_future(ctx.link().clone(), async move { - let res = Self::reload_status(&remote, &node).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 create(_ctx: &yew::Context) -> Self { + Self } fn changed(&mut self, ctx: &Context, old_props: &Self::Properties) -> bool { let props = ctx.props(); if props.remote != old_props.remote || props.node != old_props.node { - self.status = None; - self.last_status_error = 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 @@ -210,7 +58,8 @@ impl yew::Component for NodePanelComp { } fn view(&self, ctx: &yew::Context) -> yew::Html { - let props = ctx.props(); + let props = ctx.props().clone(); + let title: Html = Row::new() .gap(2) .class(AlignItems::Baseline) @@ -218,150 +67,17 @@ impl yew::Component for NodePanelComp { .with_child(tr! {"Node '{0}'", props.node}) .into(); - let mut status_comp = Column::new().gap(2).padding(4); - let status = self.status.as_ref(); - let cpu = status.map(|s| s.cpu).unwrap_or_default(); - let maxcpu = status.map(|s| s.cpuinfo.cpus).unwrap_or_default(); - let load = status.map(|s| s.loadavg.join(", ")).unwrap_or_default(); - - let memory = status.map(|s| s.memory.used as u64).unwrap_or_default(); - let maxmem = status.map(|s| s.memory.total as u64).unwrap_or(1); - - let root = status.map(|s| s.rootfs.used as u64).unwrap_or_default(); - let maxroot = status.map(|s| s.rootfs.total as u64).unwrap_or(1); - - let memory_used = memory as f64 / maxmem as f64; - let root_used = root as f64 / maxroot as f64; - - status_comp = status_comp - .with_child(make_row( - tr!("CPU usage"), - Fa::new("cpu"), - tr!("{0}% of {1} CPU(s)", format!("{:.2}", cpu * 100.0), maxcpu), - Some(cpu as f32), - )) - .with_child(make_row( - tr!("Load average"), - Fa::new("line-chart"), - load, - None, - )) - .with_child(make_row( - tr!("Memory usage"), - Fa::new("memory"), - tr!( - "{0}% ({1} of {2})", - format!("{:.2}", memory_used * 100.0), - HumanByte::from(memory), - HumanByte::from(maxmem), - ), - Some(memory_used as f32), - )) - .with_child(make_row( - tr!("Root filesystem usage"), - Fa::new("database"), - tr!( - "{0}% ({1} of {2})", - format!("{:.2}", root_used * 100.0), - HumanByte::from(root), - HumanByte::from(maxroot), - ), - Some(root_used as f32), - )) - .with_child(Container::new().padding(1)) // spacer - .with_child( - Row::new() - .with_child(tr!("Version")) - .with_flex_spacer() - .with_optional_child(status.map(|s| s.pveversion.as_str())), - ) - .with_child( - Row::new() - .with_child(tr!("CPU Model")) - .with_flex_spacer() - .with_child(tr!( - "{0} ({1} sockets)", - status.map(|s| s.cpuinfo.model.as_str()).unwrap_or_default(), - status.map(|s| s.cpuinfo.sockets).unwrap_or_default() - )), - ); - - if let Some(err) = &self.last_status_error { - status_comp.add_child(error_message(&err.to_string())); - } - - let loading = self.status.is_none() && self.last_status_error.is_none(); - Panel::new() - .class(FlexFit) + TabPanel::new() + .class(pwt::css::FlexFit) .title(title) .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_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_total_data.clone())) - .serie1(Some(self.mem_data.clone())), - ), - ), + .with_item_builder( + TabBarItem::new() + .key("status_view") + .label(tr!("Overview")) + .icon_class("fa fa-tachometer"), + move |_| NodeOverviewPanel::new(props.remote.clone(), props.node.clone()).into(), ) .into() } } - -fn make_row(title: String, icon: Fa, text: String, meter_value: Option) -> Column { - crate::renderer::status_row(title, icon, text, meter_value, false) -} diff --git a/ui/src/pve/node/overview.rs b/ui/src/pve/node/overview.rs new file mode 100644 index 00000000..9ebc3e1d --- /dev/null +++ b/ui/src/pve/node/overview.rs @@ -0,0 +1,358 @@ +use std::rc::Rc; + +use yew::{ + virtual_dom::{VComp, VNode}, + Context, +}; + +use proxmox_human_byte::HumanByte; +use proxmox_yew_comp::{RRDGraph, RRDTimeframe, RRDTimeframeSelector, Series}; +use pwt::{ + css::{ColorScheme, FlexFit, JustifyContent}, + prelude::*, + props::{ContainerBuilder, WidgetBuilder}, + widget::{error_message, Column, Container, Fa, Progress, Row}, + AsyncPool, +}; + +use pdm_api_types::rrddata::NodeDataPoint; +use pdm_client::types::NodeStatus; + +use crate::renderer::separator; + +#[derive(Clone, Debug, Eq, PartialEq, Properties)] +pub struct NodeOverviewPanel { + /// The remote to show + pub remote: String, + + /// The node to show + pub node: 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 NodeOverviewPanel { + pub fn new(remote: String, node: String) -> Self { + yew::props!(Self { remote, node }) + } +} + +impl Into for NodeOverviewPanel { + 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 NodeOverviewPanelComp { + 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 NodeOverviewPanelComp { + async fn reload_rrd(remote: &str, node: &str, rrd_time_frame: RRDTimeframe) -> Msg { + let res = crate::pdm_client() + .pve_node_rrddata(remote, node, rrd_time_frame.mode, rrd_time_frame.timeframe) + .await; + + Msg::LoadFinished(res) + } + + async fn reload_status(remote: &str, node: &str) -> Result { + let status = crate::pdm_client().pve_node_status(remote, node).await?; + Ok(status) + } +} + +impl yew::Component for NodeOverviewPanelComp { + type Message = Msg; + type Properties = NodeOverviewPanel; + + 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 node = props.node.clone(); + let timeframe = self.rrd_time_frame; + self.async_pool.send_future(ctx.link().clone(), async move { + Self::reload_rrd(&remote, &node, timeframe).await + }); + } + Msg::ReloadStatus => { + self._status_timeout = None; + let props = ctx.props(); + let remote = props.remote.clone(); + let node = props.node.clone(); + self.async_pool.send_future(ctx.link().clone(), async move { + let res = Self::reload_status(&remote, &node).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 || props.node != old_props.node { + self.status = None; + self.last_status_error = 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 mut status_comp = Column::new().gap(2).padding(4); + let status = self.status.as_ref(); + let cpu = status.map(|s| s.cpu).unwrap_or_default(); + let maxcpu = status.map(|s| s.cpuinfo.cpus).unwrap_or_default(); + let load = status.map(|s| s.loadavg.join(", ")).unwrap_or_default(); + + let memory = status.map(|s| s.memory.used as u64).unwrap_or_default(); + let maxmem = status.map(|s| s.memory.total as u64).unwrap_or(1); + + let root = status.map(|s| s.rootfs.used as u64).unwrap_or_default(); + let maxroot = status.map(|s| s.rootfs.total as u64).unwrap_or(1); + + let memory_used = memory as f64 / maxmem as f64; + let root_used = root as f64 / maxroot as f64; + + status_comp = status_comp + .with_child(make_row( + tr!("CPU usage"), + Fa::new("cpu"), + tr!("{0}% of {1} CPU(s)", format!("{:.2}", cpu * 100.0), maxcpu), + Some(cpu as f32), + )) + .with_child(make_row( + tr!("Load average"), + Fa::new("line-chart"), + load, + None, + )) + .with_child(make_row( + tr!("Memory usage"), + Fa::new("memory"), + tr!( + "{0}% ({1} of {2})", + format!("{:.2}", memory_used * 100.0), + HumanByte::from(memory), + HumanByte::from(maxmem), + ), + Some(memory_used as f32), + )) + .with_child(make_row( + tr!("Root filesystem usage"), + Fa::new("database"), + tr!( + "{0}% ({1} of {2})", + format!("{:.2}", root_used * 100.0), + HumanByte::from(root), + HumanByte::from(maxroot), + ), + Some(root_used as f32), + )) + .with_child(Container::new().padding(1)) // spacer + .with_child( + Row::new() + .with_child(tr!("Version")) + .with_flex_spacer() + .with_optional_child(status.map(|s| s.pveversion.as_str())), + ) + .with_child( + Row::new() + .with_child(tr!("CPU Model")) + .with_flex_spacer() + .with_child(tr!( + "{0} ({1} sockets)", + status.map(|s| s.cpuinfo.model.as_str()).unwrap_or_default(), + status.map(|s| s.cpuinfo.sockets).unwrap_or_default() + )), + ); + + if let Some(err) = &self.last_status_error { + status_comp.add_child(error_message(&err.to_string())); + } + + let loading = self.status.is_none() && self.last_status_error.is_none(); + Container::new() + .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_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_total_data.clone())) + .serie1(Some(self.mem_data.clone())), + ), + ), + ) + .into() + } +} + +fn make_row(title: String, icon: Fa, text: String, meter_value: Option) -> Column { + crate::renderer::status_row(title, icon, text, meter_value, false) +} -- 2.47.2 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel