public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Dominik Csapak <d.csapak@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager 10/11] ui: pbs: add remote overview panel
Date: Fri, 26 Sep 2025 09:20:30 +0200	[thread overview]
Message-ID: <20250926072749.560801-11-d.csapak@proxmox.com> (raw)
In-Reply-To: <20250926072749.560801-1-d.csapak@proxmox.com>

similar to the PVE node overview.

Not yet used in this patch.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 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<VNode> for RemoteOverviewPanel {
+    fn into(self) -> VNode {
+        VComp::new::<RemoteOverviewPanelComp>(Rc::new(self), None).into()
+    }
+}
+
+pub enum Msg {
+    ReloadRrd,
+    ReloadStatus,
+    LoadFinished(Result<Vec<PbsNodeDataPoint>, proxmox_client::Error>),
+    StatusLoadFinished(Result<NodeStatus, proxmox_client::Error>),
+    UpdateRrdTimeframe(RRDTimeframe),
+}
+
+pub struct RemoteOverviewPanelComp {
+    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 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<NodeStatus, proxmox_client::Error> {
+        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>) -> 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 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<Self>, 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<Self>) -> 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


  parent reply	other threads:[~2025-09-26  7:27 UTC|newest]

Thread overview: 13+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-09-26  7:20 [pdm-devel] [PATCH datacenter-manager 00/11] improve PBS view Dominik Csapak
2025-09-26  7:20 ` [pdm-devel] [PATCH datacenter-manager 01/11] server: api: return list of pve/pbs remotes Dominik Csapak
2025-09-26  7:20 ` [pdm-devel] [PATCH datacenter-manager 02/11] server: api: pbs: add status api call Dominik Csapak
2025-09-26  7:20 ` [pdm-devel] [PATCH datacenter-manager 03/11] server: pbs-client: use `json` formatter for the snapshot list streaming api Dominik Csapak
2025-09-26  7:20 ` [pdm-devel] [PATCH datacenter-manager 04/11] pdm-client: add pbs node status helper Dominik Csapak
2025-09-26  7:20 ` [pdm-devel] [PATCH datacenter-manager 05/11] ui: pve: tree: refactor rendering the tree column Dominik Csapak
2025-09-26  7:20 ` [pdm-devel] [PATCH datacenter-manager 06/11] ui: add helper to compare two strings with `localeCompare` Dominik Csapak
2025-09-26  7:20 ` [pdm-devel] [PATCH datacenter-manager 07/11] ui: pbs: add datastore tree Dominik Csapak
2025-09-26  7:20 ` [pdm-devel] [PATCH datacenter-manager 08/11] ui: pbs: convert snapshot list to a snapshot tree Dominik Csapak
2025-09-26  7:20 ` [pdm-devel] [PATCH datacenter-manager 09/11] ui: pbs: add datastore panel component Dominik Csapak
2025-09-26  7:20 ` Dominik Csapak [this message]
2025-09-26  7:20 ` [pdm-devel] [PATCH datacenter-manager 11/11] ui: pbs: use new pbs panels/overview Dominik Csapak
2025-09-26 13:45 ` [pdm-devel] applied: [PATCH datacenter-manager 00/11] improve PBS view Thomas Lamprecht

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=20250926072749.560801-11-d.csapak@proxmox.com \
    --to=d.csapak@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