public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Lukas Wagner <l.wagner@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH proxmox-datacenter-manager v2 4/5] ui: pve: move node overview to a new overview tab
Date: Wed,  3 Sep 2025 13:41:22 +0200	[thread overview]
Message-ID: <20250903114123.215787-7-l.wagner@proxmox.com> (raw)
In-Reply-To: <20250903114123.215787-1-l.wagner@proxmox.com>

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 <l.wagner@proxmox.com>
Tested-by: Stefan Hanreich <s.hanreich@proxmox.com>
Reviewed-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 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<VNode> for NodePanel {
     }
 }
 
-pub enum Msg {
-    ReloadRrd,
-    ReloadStatus,
-    LoadFinished(Result<Vec<NodeDataPoint>, proxmox_client::Error>),
-    StatusLoadFinished(Result<NodeStatus, proxmox_client::Error>),
-    UpdateRrdTimeframe(RRDTimeframe),
-}
-
-pub struct NodePanelComp {
-    time_data: Rc<Vec<i64>>,
-    cpu_data: Rc<Series>,
-    load_data: Rc<Series>,
-    mem_data: Rc<Series>,
-    mem_total_data: Rc<Series>,
-    status: Option<NodeStatus>,
-
-    rrd_time_frame: RRDTimeframe,
-
-    last_error: Option<proxmox_client::Error>,
-    last_status_error: Option<proxmox_client::Error>,
-
-    async_pool: AsyncPool,
-    _timeout: Option<gloo_timers::callback::Timeout>,
-    _status_timeout: Option<gloo_timers::callback::Timeout>,
-}
-
-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<NodeStatus, proxmox_client::Error> {
-        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>) -> 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<Self>, 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 {
+        Self
     }
 
     fn changed(&mut self, ctx: &Context<Self>, 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<Self>) -> 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<f32>) -> 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<VNode> for NodeOverviewPanel {
+    fn into(self) -> VNode {
+        VComp::new::<NodeOverviewPanelComp>(Rc::new(self), None).into()
+    }
+}
+
+pub enum Msg {
+    ReloadRrd,
+    ReloadStatus,
+    LoadFinished(Result<Vec<NodeDataPoint>, proxmox_client::Error>),
+    StatusLoadFinished(Result<NodeStatus, proxmox_client::Error>),
+    UpdateRrdTimeframe(RRDTimeframe),
+}
+
+pub struct NodeOverviewPanelComp {
+    time_data: Rc<Vec<i64>>,
+    cpu_data: Rc<Series>,
+    load_data: Rc<Series>,
+    mem_data: Rc<Series>,
+    mem_total_data: Rc<Series>,
+    status: Option<NodeStatus>,
+
+    rrd_time_frame: RRDTimeframe,
+
+    last_error: Option<proxmox_client::Error>,
+    last_status_error: Option<proxmox_client::Error>,
+
+    async_pool: AsyncPool,
+    _timeout: Option<gloo_timers::callback::Timeout>,
+    _status_timeout: Option<gloo_timers::callback::Timeout>,
+}
+
+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<NodeStatus, proxmox_client::Error> {
+        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>) -> 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<Self>, 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<Self>, 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<Self>) -> 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<f32>) -> 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


  parent reply	other threads:[~2025-09-03 11:41 UTC|newest]

Thread overview: 11+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-09-03 11:41 [pdm-devel] [PATCH proxmox{-yew-comp, -datacenter-manager} v2 0/7] PVE node update view Lukas Wagner
2025-09-03 11:41 ` [pdm-devel] [PATCH proxmox-yew-comp v2 1/2] apt view: allow to set task_base_url Lukas Wagner
2025-09-03 11:41 ` [pdm-devel] [PATCH proxmox-yew-comp v2 2/2] apt view: reload if base urls have changed Lukas Wagner
2025-09-03 11:41 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 1/5] update proxmox-api-types submodule Lukas Wagner
2025-09-03 11:41 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 2/5] server: add api for getting available updates/changelogs for remote nodes Lukas Wagner
2025-09-03 11:41 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 3/5] ui: pve: promote node.rs to dir-style module Lukas Wagner
2025-09-03 11:41 ` Lukas Wagner [this message]
2025-09-03 11:41 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 5/5] ui: pve: node: add update tab Lukas Wagner
2025-09-04  9:30 ` [pdm-devel] [PATCH proxmox{-yew-comp, -datacenter-manager} v2 0/7] PVE node update view Dominik Csapak
2025-09-04  9:57   ` Thomas Lamprecht
2025-09-04 12:01 ` [pdm-devel] applied: " Dominik Csapak

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20250903114123.215787-7-l.wagner@proxmox.com \
    --to=l.wagner@proxmox.com \
    --cc=pdm-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal