From: Dominik Csapak <d.csapak@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager 09/11] ui: pbs: add datastore panel component
Date: Fri, 26 Sep 2025 09:20:29 +0200 [thread overview]
Message-ID: <20250926072749.560801-10-d.csapak@proxmox.com> (raw)
In-Reply-To: <20250926072749.560801-1-d.csapak@proxmox.com>
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 <d.csapak@proxmox.com>
---
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<DatastorePanel> for VNode {
+ fn from(val: DatastorePanel) -> Self {
+ VComp::new::<DatastorePanelComp>(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 {
+ Self {}
+ }
+
+ fn view(&self, ctx: &yew::Context<Self>) -> 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<DataStoreOverview> for VNode {
+ fn from(val: DataStoreOverview) -> Self {
+ VComp::new::<DataStoreOverviewComp>(Rc::new(val), None).into()
+ }
+}
+
+pub enum Msg {
+ ReloadRrd,
+ RrdResult(Result<Vec<PbsDatastoreDataPoint>, proxmox_client::Error>),
+ UpdateRrdTimeframe(RRDTimeframe),
+}
+
+pub struct DataStoreOverviewComp {
+ loaded: bool,
+ last_rrd_error: Option<proxmox_client::Error>,
+ _status_timeout: Option<Timeout>,
+ _async_pool: AsyncPool,
+
+ rrd_time_frame: RRDTimeframe,
+
+ time: Rc<Vec<i64>>,
+ disk: Rc<Series>,
+ disk_max: Rc<Series>,
+ disk_read: Rc<Series>,
+ disk_write: Rc<Series>,
+}
+
+impl DataStoreOverviewComp {
+ async fn reload_rrd(
+ remote: &str,
+ id: &str,
+ rrd_time_frame: RRDTimeframe,
+ ) -> Result<Vec<PbsDatastoreDataPoint>, 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>) -> 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<Self>, 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<Self>, 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<Self>) -> 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
next prev 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 ` Dominik Csapak [this message]
2025-09-26 7:20 ` [pdm-devel] [PATCH datacenter-manager 10/11] ui: pbs: add remote overview panel Dominik Csapak
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-10-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