From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 511561FF191 for ; Tue, 21 Oct 2025 13:11:38 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 5B2A01BB9A; Tue, 21 Oct 2025 13:11:54 +0200 (CEST) From: Christian Ebner To: pdm-devel@lists.proxmox.com Date: Tue, 21 Oct 2025 13:11:29 +0200 Message-ID: <20251021111129.294349-20-c.ebner@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20251021111129.294349-1-c.ebner@proxmox.com> References: <20251021111129.294349-1-c.ebner@proxmox.com> MIME-Version: 1.0 X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1761045097180 X-SPAM-LEVEL: Spam detection results: 1 AWL -2.833 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 KAM_SOMETLD_ARE_BAD_TLD 5 .bar, .beauty, .buzz, .cam, .casa, .cfd, .club, .date, .guru, .link, .live, .monster, .online, .press, .pw, .quest, .rest, .sbs, .shop, .stream, .top, .trade, .wiki, .work, .xyz TLD abuse PDS_OTHER_BAD_TLD 0.75 Untrustworthy TLDs 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 v3 19/19] ui: dashboard: add panel for PBS datastore statistics 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" Show a dashboard with the number of datastores in their respective storage state. Signed-off-by: Christian Ebner --- Changes since version 2: - not present in previous version ui/src/dashboard/mod.rs | 76 +++------- ui/src/dashboard/pbs_datastores_panel.rs | 175 +++++++++++++++++++++++ 2 files changed, 193 insertions(+), 58 deletions(-) create mode 100644 ui/src/dashboard/pbs_datastores_panel.rs diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs index d3da03d..07d5cd9 100644 --- a/ui/src/dashboard/mod.rs +++ b/ui/src/dashboard/mod.rs @@ -54,6 +54,9 @@ use status_row::DashboardStatusRow; mod filtered_tasks; +mod pbs_datastores_panel; +use pbs_datastores_panel::PbsDatastoresPanel; + mod tasks; use tasks::TaskSummary; @@ -282,6 +285,20 @@ impl PdmDashboard { ) } + fn create_pbs_datastores_panel(&self) -> Panel { + let pbs_datastores = self + .status + .as_ref() + .map(|status| status.pbs_datastores.clone()); + + Panel::new() + .flex(1.0) + .width(300) + .title(self.create_title_with_icon("database", tr!("Backup Server Datastores"))) + .border(true) + .with_child(PbsDatastoresPanel::new(pbs_datastores)) + } + fn reload(&mut self, ctx: &yew::Context) { let max_age = if self.loaded_once { self.config.max_age.unwrap_or(DEFAULT_MAX_AGE_S) @@ -520,64 +537,7 @@ impl Component for PdmDashboard { tr!("Backup Server Nodes"), RemoteType::Pbs, )) - // FIXME: add further PBS support - //.with_child( - // Panel::new() - // .flex(1.0) - // .width(300) - // .title(self.create_title_with_icon( - // "floppy-o", - // tr!("Backup Server Datastores"), - // )) - // .border(true) - // .with_child(if self.loading { - // Column::new() - // .padding(4) - // .class(FlexFit) - // .class(JustifyContent::Center) - // .class(AlignItems::Center) - // .with_child(html! {}) - // } else { - // Column::new() - // .padding(4) - // .class(FlexFit) - // .class(JustifyContent::Center) - // .gap(2) - // // FIXME: show more detailed status (usage?) - // .with_child( - // Row::new() - // .gap(2) - // .with_child( - // StorageState::Available.to_fa_icon().fixed_width(), - // ) - // .with_child(tr!("available")) - // .with_flex_spacer() - // .with_child( - // Container::from_tag("span").with_child( - // self.status.pbs_datastores.available, - // ), - // ), - // ) - // .with_optional_child( - // (self.status.pbs_datastores.unknown > 0).then_some( - // Row::new() - // .gap(2) - // .with_child( - // StorageState::Unknown - // .to_fa_icon() - // .fixed_width(), - // ) - // .with_child(tr!("unknown")) - // .with_flex_spacer() - // .with_child( - // Container::from_tag("span").with_child( - // self.status.pbs_datastores.unknown, - // ), - // ), - // ), - // ) - // }), - //) + .with_child(self.create_pbs_datastores_panel()) .with_child(SubscriptionInfo::new()), ) .with_child( diff --git a/ui/src/dashboard/pbs_datastores_panel.rs b/ui/src/dashboard/pbs_datastores_panel.rs new file mode 100644 index 0000000..e028476 --- /dev/null +++ b/ui/src/dashboard/pbs_datastores_panel.rs @@ -0,0 +1,175 @@ +use std::rc::Rc; + +use pdm_api_types::resource::{PbsDatastoreStatusCount, ResourceType}; +use pdm_search::{Search, SearchTerm}; +use proxmox_yew_comp::Status; +use pwt::{ + css::{self, TextAlign}, + prelude::*, + widget::{Container, Fa, List, ListTile}, +}; +use yew::{ + virtual_dom::{VComp, VNode}, + Properties, +}; + +use crate::search_provider::get_search_provider; + +use super::loading_column; + +#[derive(PartialEq, Clone, Properties)] +pub struct PbsDatastoresPanel { + status: Option, +} + +impl PbsDatastoresPanel { + /// Create new datastore status panel with given storage status counts + pub fn new(status: Option) -> Self { + yew::props!(Self { status }) + } +} + +impl From for VNode { + fn from(value: PbsDatastoresPanel) -> Self { + let comp = VComp::new::(Rc::new(value), None); + VNode::from(comp) + } +} + +#[derive(PartialEq, Clone)] +pub enum StatusRow { + Online(u64), + InMaintenance(u64), + Removable(u64), + S3Backend(u64), + HighUsage(u64), + Unknown(u64), + All(u64), +} + +pub struct PbsDatastoresPanelComponent {} + +impl yew::Component for PbsDatastoresPanelComponent { + type Message = Search; + type Properties = PbsDatastoresPanel; + + fn create(_ctx: &yew::Context) -> Self { + Self {} + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + if let Some(provider) = get_search_provider(ctx) { + provider.search(msg); + } + + false + } + + fn view(&self, ctx: &yew::Context) -> yew::Html { + let props = ctx.props(); + + let Some(status) = &props.status else { + return loading_column().into(); + }; + + let data = vec![ + StatusRow::Online(status.online), + StatusRow::InMaintenance(status.in_maintenance.unwrap_or_default()), + StatusRow::Removable(status.removable.unwrap_or_default()), + StatusRow::S3Backend(status.s3_backend.unwrap_or_default()), + StatusRow::HighUsage(status.high_usage.unwrap_or_default()), + StatusRow::Unknown(status.unknown.unwrap_or_default()), + StatusRow::All(status.online + status.in_maintenance.unwrap_or_default()), + ]; + + let tiles: Vec<_> = data + .into_iter() + .filter_map(|row| create_list_tile(ctx.link(), row)) + .collect(); + + let list = List::new(tiles.len() as u64, move |idx: u64| { + tiles[idx as usize].clone() + }) + .padding(4) + .class(css::Flex::Fill) + .grid_template_columns("auto auto 1fr auto"); + + list.into() + } +} + +fn create_list_tile( + link: &html::Scope, + status_row: StatusRow, +) -> Option { + let (icon, count, name, search_term) = match status_row { + StatusRow::Online(count) => ( + Fa::from(Status::Success), + count, + "Online", + Some(("online", "status")), + ), + StatusRow::HighUsage(count) => ( + Fa::from(Status::Warning), + count, + "High usage", + Some(("high-usage", "property")), + ), + StatusRow::InMaintenance(count) => ( + Fa::new("wrench"), + count, + "In Maintenance", + Some(("in-maintenance", "status")), + ), + StatusRow::Removable(count) => ( + Fa::new("plug"), + count, + "Removable", + Some(("removable", "property")), + ), + StatusRow::S3Backend(count) => ( + Fa::new("cloud-upload"), + count, + "S3", + Some(("s3", "property")), + ), + StatusRow::Unknown(count) => ( + Fa::from(Status::Unknown), + count, + "Unknown", + Some(("unknown", "property")), + ), + StatusRow::All(count) => (Fa::new("database"), count, "All", None), + }; + + Some( + ListTile::new() + .tabindex(0) + .interactive(true) + .with_child(icon) + .with_child(Container::new().padding_x(2).with_child(name)) + .with_child( + Container::new() + .class(TextAlign::Right) + .padding_end(2) + .with_child(count), + ) + .with_child(Fa::new("search")) + .onclick(link.callback(move |_| create_pbs_datastores_status_search_term(search_term))) + .onkeydown(link.batch_callback( + move |event: KeyboardEvent| match event.key().as_str() { + "Enter" | " " => Some(create_pbs_datastores_status_search_term(search_term)), + _ => None, + }, + )), + ) +} + +fn create_pbs_datastores_status_search_term(search_term: Option<(&str, &str)>) -> Search { + let resource_type: ResourceType = ResourceType::PbsDatastore; + let mut terms = vec![SearchTerm::new(resource_type.as_str()).category(Some("type"))]; + if let Some((search_term, category)) = search_term { + terms.push(SearchTerm::new(search_term).category(Some(category))); + } + Search::with_terms(terms) +} -- 2.47.3 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel