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 64C711FF15C for ; Fri, 31 Oct 2025 13:48:06 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 6EED6117B3; Fri, 31 Oct 2025 13:48:41 +0100 (CET) From: Dominik Csapak To: pdm-devel@lists.proxmox.com Date: Fri, 31 Oct 2025 13:44:03 +0100 Message-ID: <20251031124822.2739685-21-d.csapak@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20251031124822.2739685-1-d.csapak@proxmox.com> References: <20251031124822.2739685-1-d.csapak@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.021 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 PROLO_LEO1 0.1 Meta Catches all Leo drug variations so far 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 20/21] ui: dashboard: use SharedState for create_*_panel 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 avoids some unnecessary clones of the data. Signed-off-by: Dominik Csapak --- ui/src/dashboard/guest_panel.rs | 29 ++++++++++++----- ui/src/dashboard/node_status_panel.rs | 17 ++++++++-- ui/src/dashboard/pbs_datastores_panel.rs | 13 ++++++-- ui/src/dashboard/remote_panel.rs | 18 ++++++++--- ui/src/dashboard/sdn_zone_panel.rs | 14 ++++++--- ui/src/dashboard/tasks.rs | 13 +++++--- ui/src/dashboard/top_entities.rs | 24 +++++++++----- ui/src/dashboard/view.rs | 40 +++++------------------- 8 files changed, 102 insertions(+), 66 deletions(-) diff --git a/ui/src/dashboard/guest_panel.rs b/ui/src/dashboard/guest_panel.rs index be7a7a2e..03b86837 100644 --- a/ui/src/dashboard/guest_panel.rs +++ b/ui/src/dashboard/guest_panel.rs @@ -1,20 +1,24 @@ use std::rc::Rc; +use anyhow::Error; +use yew::{ + virtual_dom::{VComp, VNode}, + Properties, +}; + use pdm_api_types::resource::{GuestStatusCount, ResourceType, ResourcesStatus}; use pdm_search::{Search, SearchTerm}; use proxmox_yew_comp::GuestState; use pwt::{ css::{self, TextAlign}, prelude::*, - widget::{Container, Fa, List, ListTile, Panel}, -}; -use yew::{ - virtual_dom::{VComp, VNode}, - Properties, + state::SharedState, + widget::{error_message, Container, Fa, List, ListTile, Panel}, }; use crate::{ dashboard::create_title_with_icon, pve::GuestType, search_provider::get_search_provider, + LoadResult, }; use super::loading_column; @@ -211,14 +215,23 @@ fn create_guest_search_term( /// Creates a new guest panel. Setting `guest_type` to `None` means we /// create one for all guests, regardless of type. -pub fn create_guest_panel(guest_type: Option, status: Option) -> Panel { +pub fn create_guest_panel( + guest_type: Option, + status: SharedState>, +) -> Panel { let (icon, title) = match guest_type { Some(GuestType::Qemu) => ("desktop", tr!("Virtual Machines")), Some(GuestType::Lxc) => ("cubes", tr!("Linux Container")), None => ("desktop", tr!("Guests")), }; + let status = status.read(); Panel::new() .title(create_title_with_icon(icon, title)) - .border(true) - .with_child(GuestPanel::new(guest_type, status)) + .with_child(GuestPanel::new(guest_type, status.data.clone())) + .with_optional_child( + status + .error + .as_ref() + .map(|err| error_message(&err.to_string())), + ) } diff --git a/ui/src/dashboard/node_status_panel.rs b/ui/src/dashboard/node_status_panel.rs index 0873635d..60648ef0 100644 --- a/ui/src/dashboard/node_status_panel.rs +++ b/ui/src/dashboard/node_status_panel.rs @@ -1,18 +1,21 @@ use std::rc::Rc; +use anyhow::Error; use yew::virtual_dom::{VComp, VNode}; use pdm_search::{Search, SearchTerm}; use proxmox_yew_comp::Status; use pwt::css::{AlignItems, FlexFit, JustifyContent}; use pwt::prelude::*; -use pwt::widget::{Column, Fa, Panel}; +use pwt::state::SharedState; +use pwt::widget::{error_message, Column, Fa, Panel}; use pdm_api_types::resource::NodeStatusCount; use pdm_api_types::{remotes::RemoteType, resource::ResourcesStatus}; use crate::dashboard::create_title_with_icon; use crate::search_provider::get_search_provider; +use crate::LoadResult; use super::loading_column; @@ -150,7 +153,7 @@ fn map_status( /// Passing `None` to `remote_type` means creating a panel for all nodes, regardless of remote type. pub fn create_node_panel( remote_type: Option, - status: Option, + status: SharedState>, ) -> Panel { let (icon, title) = match remote_type { Some(RemoteType::Pve) => ("building", tr!("Virtual Environment Nodes")), @@ -158,7 +161,9 @@ pub fn create_node_panel( None => ("building", tr!("Nodes")), }; - let (nodes_status, failed_remotes) = match status { + let status = status.read(); + + let (nodes_status, failed_remotes) = match &status.data { Some(status) => { let nodes_status = match remote_type { Some(RemoteType::Pve) => Some(status.pve_nodes.clone()), @@ -190,4 +195,10 @@ pub fn create_node_panel( nodes_status, failed_remotes, )) + .with_optional_child( + status + .error + .as_ref() + .map(|err| error_message(&err.to_string())), + ) } diff --git a/ui/src/dashboard/pbs_datastores_panel.rs b/ui/src/dashboard/pbs_datastores_panel.rs index 6542ac00..b28647fa 100644 --- a/ui/src/dashboard/pbs_datastores_panel.rs +++ b/ui/src/dashboard/pbs_datastores_panel.rs @@ -1,5 +1,6 @@ use std::rc::Rc; +use anyhow::Error; use yew::virtual_dom::{VComp, VNode}; use pdm_api_types::resource::{PbsDatastoreStatusCount, ResourceType, ResourcesStatus}; @@ -7,10 +8,12 @@ use pdm_search::{Search, SearchTerm}; use proxmox_yew_comp::Status; use pwt::css::{self, TextAlign}; use pwt::prelude::*; +use pwt::state::SharedState; use pwt::widget::{Container, Fa, List, ListTile, Panel}; use crate::dashboard::create_title_with_icon; use crate::search_provider::get_search_provider; +use crate::LoadResult; use super::loading_column; @@ -155,8 +158,14 @@ fn create_pbs_datastores_status_search_term(search_term: Option<(&str, &str)>) - Search::with_terms(terms) } -pub fn create_pbs_datastores_panel(status: Option) -> Panel { - let pbs_datastores = status.map(|status| status.pbs_datastores.clone()); +pub fn create_pbs_datastores_panel( + status: SharedState>, +) -> Panel { + let pbs_datastores = status + .read() + .data + .as_ref() + .map(|status| status.pbs_datastores.clone()); Panel::new() .title(create_title_with_icon( diff --git a/ui/src/dashboard/remote_panel.rs b/ui/src/dashboard/remote_panel.rs index 747f9b8d..a5682d7a 100644 --- a/ui/src/dashboard/remote_panel.rs +++ b/ui/src/dashboard/remote_panel.rs @@ -1,5 +1,6 @@ use std::rc::Rc; +use anyhow::Error; use yew::html::IntoEventCallback; use yew::virtual_dom::{VComp, VNode}; @@ -8,11 +9,13 @@ use proxmox_yew_comp::Status; use pwt::css; use pwt::prelude::*; use pwt::props::{ContainerBuilder, WidgetBuilder}; +use pwt::state::SharedState; use pwt::widget::menu::{Menu, MenuButton, MenuEvent, MenuItem}; -use pwt::widget::{Column, Container, Fa, Panel}; +use pwt::widget::{error_message, Column, Container, Fa, Panel}; use pdm_api_types::resource::ResourcesStatus; +use crate::LoadResult; use crate::{dashboard::create_title_with_icon, search_provider::get_search_provider}; #[derive(Properties, PartialEq)] @@ -116,14 +119,14 @@ fn create_search_term(failure: bool) -> Search { } pub fn create_remote_panel( - status: Option, + status: SharedState>, on_pve_wizard: Option>, on_pbs_wizard: Option>, ) -> Panel { + let status = status.read(); let mut panel = Panel::new() .title(create_title_with_icon("server", tr!("Remotes"))) - .border(true) - .with_child(RemotePanel::new(status)); + .with_child(RemotePanel::new(status.data.clone())); if on_pve_wizard.is_some() || on_pbs_wizard.is_some() { let mut menu = Menu::new(); @@ -143,5 +146,10 @@ pub fn create_remote_panel( } panel.add_tool(MenuButton::new(tr!("Add")).show_arrow(true).menu(menu)); } - panel + panel.with_optional_child( + status + .error + .as_ref() + .map(|err| error_message(&err.to_string())), + ) } diff --git a/ui/src/dashboard/sdn_zone_panel.rs b/ui/src/dashboard/sdn_zone_panel.rs index 611aadd1..8112550c 100644 --- a/ui/src/dashboard/sdn_zone_panel.rs +++ b/ui/src/dashboard/sdn_zone_panel.rs @@ -1,10 +1,13 @@ use std::rc::Rc; +use anyhow::Error; + use pdm_api_types::resource::{ResourceType, ResourcesStatus, SdnStatus, SdnZoneCount}; use pdm_search::{Search, SearchTerm}; use pwt::{ css::{self, FontColor, TextAlign}, prelude::*, + state::SharedState, widget::{Container, Fa, List, ListTile, Panel}, }; use yew::{ @@ -12,7 +15,7 @@ use yew::{ Properties, }; -use crate::{dashboard::create_title_with_icon, search_provider::get_search_provider}; +use crate::{dashboard::create_title_with_icon, search_provider::get_search_provider, LoadResult}; use super::loading_column; @@ -156,11 +159,14 @@ fn create_sdn_zone_search_term(status: Option) -> Search { Search::with_terms(terms) } -pub fn create_sdn_panel(status: Option) -> Panel { - let sdn_zones_status = status.map(|status| status.sdn_zones); +pub fn create_sdn_panel(status: SharedState>) -> Panel { + let sdn_zones_status = status + .read() + .data + .as_ref() + .map(|status| status.sdn_zones.clone()); Panel::new() .title(create_title_with_icon("sdn", tr!("SDN Zones"))) - .border(true) .with_child(SdnZonePanel::new(sdn_zones_status)) } diff --git a/ui/src/dashboard/tasks.rs b/ui/src/dashboard/tasks.rs index 9989d4a9..3903865c 100644 --- a/ui/src/dashboard/tasks.rs +++ b/ui/src/dashboard/tasks.rs @@ -4,6 +4,7 @@ use std::rc::Rc; use anyhow::Error; use js_sys::Date; +use pwt::state::SharedState; use yew::html::Scope; use yew::virtual_dom::Key; @@ -24,6 +25,7 @@ use crate::dashboard::create_title_with_icon; use crate::dashboard::loading_column; use crate::dashboard::refresh_config_edit::DEFAULT_TASK_SUMMARY_HOURS; use crate::tasks::TaskWorkerType; +use crate::LoadResult; use super::filtered_tasks::FilteredTasks; @@ -307,8 +309,7 @@ impl Component for ProxmoxTaskSummary { } pub fn create_task_summary_panel( - statistics: Option, - error: Option<&Error>, + statistics: SharedState>, remotes: Option, hours: u32, since: i64, @@ -317,15 +318,17 @@ pub fn create_task_summary_panel( Some(_count) => tr!("Task Summary Sorted by Failed Tasks (Last {0}h)", hours), None => tr!("Task Summary by Category (Last {0}h)", hours), }; - let loading = error.is_none() && statistics.is_none(); + let loading = !statistics.read().has_data(); + let guard = statistics.read(); + let data = guard.data.clone(); + let error = guard.error.as_ref(); Panel::new() - .border(true) .title(create_title_with_icon("list", title)) .with_child( Container::new() .class(css::FlexFit) .padding(2) - .with_optional_child(statistics.map(|data| TaskSummary::new(data, since, remotes))) + .with_optional_child(data.map(|data| TaskSummary::new(data, since, remotes))) .with_optional_child((loading).then_some(loading_column())) .with_optional_child(error.map(|err| error_message(&err.to_string()))), ) diff --git a/ui/src/dashboard/top_entities.rs b/ui/src/dashboard/top_entities.rs index dfe38692..e94c1b8c 100644 --- a/ui/src/dashboard/top_entities.rs +++ b/ui/src/dashboard/top_entities.rs @@ -1,5 +1,6 @@ use std::rc::Rc; +use pwt::state::SharedState; use web_sys::HtmlElement; use yew::virtual_dom::{VComp, VNode}; @@ -18,6 +19,7 @@ use pwt::{ use pdm_client::types::{Resource, TopEntity}; +use crate::LoadResult; use crate::{ dashboard::{create_title_with_icon, loading_column, types::LeaderboardType}, get_deep_url, get_resource_node, navigate_to, @@ -327,37 +329,45 @@ fn graph_from_data(data: &Vec>, threshold: f64) -> Container { } pub fn create_top_entities_panel( - entities: Option>, - error: Option<&proxmox_client::Error>, + top_entities: SharedState< + LoadResult, + >, leaderboard_type: LeaderboardType, ) -> Panel { - let (icon, title, metrics_title, threshold) = match leaderboard_type { + let top_entities = top_entities.read(); + let (entities, icon, title, metrics_title, threshold) = match leaderboard_type { LeaderboardType::GuestCpu => ( + top_entities.data.as_ref().map(|e| e.guest_cpu.clone()), "desktop", tr!("Guests With the Highest CPU Usage"), tr!("CPU usage"), 0.85, ), LeaderboardType::NodeCpu => ( + top_entities.data.as_ref().map(|e| e.node_cpu.clone()), "building", tr!("Nodes With the Highest CPU Usage"), tr!("CPU usage"), 0.85, ), LeaderboardType::NodeMemory => ( + top_entities.data.as_ref().map(|e| e.node_memory.clone()), "building", tr!("Nodes With the Highest Memory Usage"), tr!("Memory usage"), 0.95, ), }; - let loading = entities.is_none() && error.is_none(); Panel::new() - .border(true) .title(create_title_with_icon(icon, title)) .with_optional_child( entities.map(|entities| TopEntities::new(entities, metrics_title, threshold)), ) - .with_optional_child(loading.then_some(loading_column())) - .with_optional_child(error.map(|err| error_message(&err.to_string()))) + .with_optional_child((!top_entities.has_data()).then_some(loading_column())) + .with_optional_child( + top_entities + .error + .as_ref() + .map(|err| error_message(&err.to_string())), + ) } diff --git a/ui/src/dashboard/view.rs b/ui/src/dashboard/view.rs index bcb277ed..fb46064c 100644 --- a/ui/src/dashboard/view.rs +++ b/ui/src/dashboard/view.rs @@ -103,37 +103,19 @@ fn render_widget( statistics: SharedState>, refresh_config: RefreshConfig, ) -> Html { - let status = status.read(); - let top_entities = top_entities.read(); - let statistics = statistics.read(); - let mut widget = match &item.r#type { - WidgetType::Nodes { remote_type } => create_node_panel(*remote_type, status.data.clone()), - WidgetType::Guests { guest_type } => create_guest_panel(*guest_type, status.data.clone()), + WidgetType::Nodes { remote_type } => create_node_panel(*remote_type, status), + WidgetType::Guests { guest_type } => create_guest_panel(*guest_type, status), WidgetType::Remotes { show_wizard } => create_remote_panel( - status.data.clone(), - show_wizard.then_some(link.callback(|_| Msg::CreateWizard(Some(RemoteType::Pve)))), + status, show_wizard.then_some(link.callback(|_| Msg::CreateWizard(Some(RemoteType::Pve)))), + show_wizard.then_some(link.callback(|_| Msg::CreateWizard(Some(RemoteType::Pbs)))), ), - WidgetType::PbsDatastores => create_pbs_datastores_panel(status.data.clone()), + WidgetType::PbsDatastores => create_pbs_datastores_panel(status), WidgetType::Subscription => create_subscription_panel(subscriptions), - WidgetType::Sdn => create_sdn_panel(status.data.clone()), + WidgetType::Sdn => create_sdn_panel(status), WidgetType::Leaderboard { leaderboard_type } => { - let entities = match leaderboard_type { - LeaderboardType::GuestCpu => top_entities - .data - .as_ref() - .map(|entities| entities.guest_cpu.clone()), - LeaderboardType::NodeCpu => top_entities - .data - .as_ref() - .map(|entities| entities.node_cpu.clone()), - LeaderboardType::NodeMemory => top_entities - .data - .as_ref() - .map(|entities| entities.node_memory.clone()), - }; - create_top_entities_panel(entities, top_entities.error.as_ref(), *leaderboard_type) + create_top_entities_panel(top_entities, *leaderboard_type) } WidgetType::TaskSummary { grouping } => { let remotes = match grouping { @@ -141,13 +123,7 @@ fn render_widget( TaskSummaryGrouping::Remote => Some(5), }; let (hours, since) = get_task_options(refresh_config.task_last_hours); - create_task_summary_panel( - statistics.data.clone(), - statistics.error.as_ref(), - remotes, - hours, - since, - ) + create_task_summary_panel(statistics, remotes, hours, since) } }; -- 2.47.3 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel