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 B7C691FF17C for ; Wed, 3 Sep 2025 11:10:39 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id A93162EDF6; Wed, 3 Sep 2025 11:10:54 +0200 (CEST) Message-ID: <8b20cc07-8c04-47e6-bc95-3547a40e07a7@proxmox.com> Date: Wed, 3 Sep 2025 11:10:17 +0200 MIME-Version: 1.0 User-Agent: Mozilla Thunderbird To: Proxmox Datacenter Manager development discussion , Lukas Wagner References: <20250902151427.425017-1-l.wagner@proxmox.com> <20250902151427.425017-10-l.wagner@proxmox.com> Content-Language: en-US From: Stefan Hanreich In-Reply-To: <20250902151427.425017-10-l.wagner@proxmox.com> X-SPAM-LEVEL: Spam detection results: 0 AWL 0.708 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 RCVD_IN_MSPIKE_H2 0.001 Average reputation (+2) RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_RPBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_SAFE_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [mod.rs, rootfs.total, memory.total, overview.rs] Subject: Re: [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" one small nit inline (pre-existing) I noticed while looking over the changes On 9/2/25 5:14 PM, Lukas Wagner wrote: > 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())), pre-existing but this could profit from a Default implementation > + 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) > +} _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel