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 8CDB81FF16B for ; Fri, 26 Sep 2025 09:27:31 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id F17F194BA; Fri, 26 Sep 2025 09:27:58 +0200 (CEST) From: Dominik Csapak To: pdm-devel@lists.proxmox.com Date: Fri, 26 Sep 2025 09:20:29 +0200 Message-ID: <20250926072749.560801-10-d.csapak@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20250926072749.560801-1-d.csapak@proxmox.com> References: <20250926072749.560801-1-d.csapak@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.027 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pdm-devel] [PATCH datacenter-manager 09/11] ui: pbs: add datastore panel component 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 is a tab panel with (for now) two tabs * Overview: like the node/remote overview but for the datastore * Content: uses the snapshot tree to show the content Since we don't have a datastore status api call yet, simply show some interesting things from the configuration. Not yet used in this patch. Signed-off-by: Dominik Csapak --- ui/src/pbs/datastore.rs | 73 +++++++++ ui/src/pbs/datastore/overview.rs | 260 +++++++++++++++++++++++++++++++ 2 files changed, 333 insertions(+) create mode 100644 ui/src/pbs/datastore.rs create mode 100644 ui/src/pbs/datastore/overview.rs diff --git a/ui/src/pbs/datastore.rs b/ui/src/pbs/datastore.rs new file mode 100644 index 00000000..3d4e6b6e --- /dev/null +++ b/ui/src/pbs/datastore.rs @@ -0,0 +1,73 @@ +use std::rc::Rc; + +use yew::{ + virtual_dom::{VComp, VNode}, + Component, Properties, +}; + +use pwt::{css::FlexFit, props::WidgetBuilder, tr, widget::TabBarItem}; + +use pbs_api_types::DataStoreConfig; + +use crate::pbs::SnapshotList; + +mod overview; +use overview::DataStoreOverview; + +#[derive(Properties, PartialEq)] +pub struct DatastorePanel { + remote: String, + config: DataStoreConfig, +} + +impl DatastorePanel { + pub fn new(remote: String, config: DataStoreConfig) -> Self { + yew::props!(Self { remote, config }) + } +} + +impl From for VNode { + fn from(val: DatastorePanel) -> Self { + VComp::new::(Rc::new(val), None).into() + } +} + +#[doc(hidden)] +struct DatastorePanelComp {} + +impl Component for DatastorePanelComp { + type Message = (); + type Properties = DatastorePanel; + + fn create(_ctx: &yew::Context) -> Self { + Self {} + } + + fn view(&self, ctx: &yew::Context) -> yew::Html { + let props = ctx.props(); + pwt::widget::TabPanel::new() + .class(FlexFit) + .title(tr!("Datastore {0}", props.config.name)) + .with_item_builder( + TabBarItem::new() + .label(tr!("Overview")) + .icon_class("fa fa-tachometer"), + { + let remote = props.remote.clone(); + let config = props.config.clone(); + move |_| DataStoreOverview::new(remote.clone(), config.clone()).into() + }, + ) + .with_item_builder( + TabBarItem::new() + .label(tr!("Content")) + .icon_class("fa fa-th"), + { + let remote = props.remote.clone(); + let name = props.config.name.clone(); + move |_| SnapshotList::new(remote.clone(), name.clone()).into() + }, + ) + .into() + } +} diff --git a/ui/src/pbs/datastore/overview.rs b/ui/src/pbs/datastore/overview.rs new file mode 100644 index 00000000..14e0add9 --- /dev/null +++ b/ui/src/pbs/datastore/overview.rs @@ -0,0 +1,260 @@ +use std::rc::Rc; + +use gloo_timers::callback::Timeout; +use pbs_api_types::DataStoreConfig; +use yew::{ + virtual_dom::{VComp, VNode}, + Properties, +}; + +use proxmox_yew_comp::{RRDGraph, RRDTimeframe, RRDTimeframeSelector, Series, StatusRow}; +use pwt::{ + css::{ColorScheme, FlexFit, JustifyContent}, + prelude::*, + props::WidgetBuilder, + widget::{Column, Container, Progress, Row}, + AsyncPool, +}; + +use pdm_api_types::rrddata::PbsDatastoreDataPoint; + +use crate::renderer::separator; + +#[derive(Clone, PartialEq, Properties)] +pub struct DataStoreOverview { + remote: String, + config: DataStoreConfig, + + #[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 Eq for DataStoreOverview {} + +impl DataStoreOverview { + pub fn new(remote: String, config: DataStoreConfig) -> Self { + yew::props!(Self { remote, config }) + } +} + +impl From for VNode { + fn from(val: DataStoreOverview) -> Self { + VComp::new::(Rc::new(val), None).into() + } +} + +pub enum Msg { + ReloadRrd, + RrdResult(Result, proxmox_client::Error>), + UpdateRrdTimeframe(RRDTimeframe), +} + +pub struct DataStoreOverviewComp { + loaded: bool, + last_rrd_error: Option, + _status_timeout: Option, + _async_pool: AsyncPool, + + rrd_time_frame: RRDTimeframe, + + time: Rc>, + disk: Rc, + disk_max: Rc, + disk_read: Rc, + disk_write: Rc, +} + +impl DataStoreOverviewComp { + async fn reload_rrd( + remote: &str, + id: &str, + rrd_time_frame: RRDTimeframe, + ) -> Result, proxmox_client::Error> { + let rrd = crate::pdm_client() + .pbs_datastore_rrddata(remote, id, rrd_time_frame.mode, rrd_time_frame.timeframe) + .await?; + Ok(rrd) + } +} + +impl yew::Component for DataStoreOverviewComp { + type Message = Msg; + + type Properties = DataStoreOverview; + + fn create(ctx: &yew::Context) -> Self { + ctx.link().send_message(Msg::ReloadRrd); + Self { + loaded: false, + _async_pool: AsyncPool::new(), + _status_timeout: None, + last_rrd_error: None, + + rrd_time_frame: RRDTimeframe::load(), + + time: Rc::new(Vec::new()), + disk: Rc::new(Series::new("", Vec::new())), + disk_max: Rc::new(Series::new("", Vec::new())), + disk_read: Rc::new(Series::new("", Vec::new())), + disk_write: Rc::new(Series::new("", Vec::new())), + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + let link = ctx.link().clone(); + let props = ctx.props(); + let remote = props.remote.clone(); + let id = props.config.name.clone(); + match msg { + Msg::ReloadRrd => { + let timeframe = self.rrd_time_frame; + self._async_pool.send_future(link, async move { + Msg::RrdResult(Self::reload_rrd(&remote, &id, timeframe).await) + }); + false + } + Msg::RrdResult(res) => { + match res { + Ok(rrd) => { + self.last_rrd_error = None; + + let mut disk = Vec::new(); + let mut disk_max = Vec::new(); + let mut disk_read = Vec::new(); + let mut disk_write = Vec::new(); + let mut time = Vec::new(); + for data in rrd { + disk.push(data.disk_used.unwrap_or(f64::NAN)); + disk_max.push(data.disk_total.unwrap_or(f64::NAN)); + disk_read.push(data.disk_read.unwrap_or(f64::NAN)); + disk_write.push(data.disk_write.unwrap_or(f64::NAN)); + time.push(data.time as i64); + } + + self.disk = Rc::new(Series::new(tr!("Usage"), disk)); + self.disk_max = Rc::new(Series::new(tr!("Total"), disk_max)); + self.disk_read = Rc::new(Series::new(tr!("Disk Read"), disk_read)); + self.disk_write = Rc::new(Series::new(tr!("Disk Write"), disk_write)); + self.time = Rc::new(time); + } + Err(err) => self.last_rrd_error = Some(err), + } + self._status_timeout = Some(Timeout::new(props.rrd_interval, move || { + link.send_message(Msg::ReloadRrd) + })); + self.loaded = true; + true + } + Msg::UpdateRrdTimeframe(rrd_time_frame) => { + self.rrd_time_frame = rrd_time_frame; + ctx.link().send_message(Msg::ReloadRrd); + false + } + } + } + + fn changed(&mut self, ctx: &Context, old_props: &Self::Properties) -> bool { + let props = ctx.props(); + + if props.remote != old_props.remote || props.config != old_props.config { + self.last_rrd_error = None; + + self.time = Rc::new(Vec::new()); + self.disk = Rc::new(Series::new("", Vec::new())); + self.disk_max = Rc::new(Series::new("", Vec::new())); + self.disk_read = Rc::new(Series::new("", Vec::new())); + self.disk_write = Rc::new(Series::new("", Vec::new())); + self._async_pool = AsyncPool::new(); + ctx.link().send_message(Msg::ReloadRrd); + true + } else { + false + } + } + + fn view(&self, ctx: &yew::Context) -> yew::Html { + let props = ctx.props(); + + // TODO get current status via API and show usage, etc. + + Container::new() + .class(FlexFit) + .class(ColorScheme::Neutral) + .with_child( + // FIXME: add some 'visible' or 'active' property to the progress + Progress::new() + .value((self.loaded).then_some(0.0)) + .style("opacity", (self.loaded).then_some("0")), + ) + .with_child( + Column::new() + .gap(2) + .padding(4) + .with_child( + StatusRow::new(tr!("Path")) + .icon_class("fa fa-fw fa-folder-o") + .status(&props.config.path), + ) + .with_optional_child(props.config.comment.as_deref().map(|comment| { + StatusRow::new(tr!("Comment")) + .icon_class("fa fa-fw fa-comment-o") + .status(comment) + })) + .with_optional_child(props.config.maintenance_mode.as_deref().map(|mode| { + StatusRow::new(tr!("Maintenance Mode")) + .icon_class("fa fa-fw fa-wrench") + .status(mode) + })), + ) + .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.clone()) + .title(tr!("Usage")) + .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.disk.clone())) + .serie1(Some(self.disk_max.clone())), + ) + .with_child( + RRDGraph::new(self.time.clone()) + .title(tr!("Disk I/O")) + .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.disk_read.clone())) + .serie1(Some(self.disk_write.clone())), + ), + ), + ) + .into() + } +} -- 2.47.3 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel