From: Dominik Csapak <d.csapak@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager v3 20/21] ui: dashboard: use SharedState for create_*_panel
Date: Fri, 31 Oct 2025 13:44:03 +0100 [thread overview]
Message-ID: <20251031124822.2739685-21-d.csapak@proxmox.com> (raw)
In-Reply-To: <20251031124822.2739685-1-d.csapak@proxmox.com>
this avoids some unnecessary clones of the data.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
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<GuestType>, status: Option<ResourcesStatus>) -> Panel {
+pub fn create_guest_panel(
+ guest_type: Option<GuestType>,
+ status: SharedState<LoadResult<ResourcesStatus, Error>>,
+) -> 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<RemoteType>,
- status: Option<ResourcesStatus>,
+ status: SharedState<LoadResult<ResourcesStatus, Error>>,
) -> 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<ResourcesStatus>) -> Panel {
- let pbs_datastores = status.map(|status| status.pbs_datastores.clone());
+pub fn create_pbs_datastores_panel(
+ status: SharedState<LoadResult<ResourcesStatus, Error>>,
+) -> 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<ResourcesStatus>,
+ status: SharedState<LoadResult<ResourcesStatus, Error>>,
on_pve_wizard: Option<impl IntoEventCallback<MenuEvent>>,
on_pbs_wizard: Option<impl IntoEventCallback<MenuEvent>>,
) -> 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<SdnStatus>) -> Search {
Search::with_terms(terms)
}
-pub fn create_sdn_panel(status: Option<ResourcesStatus>) -> Panel {
- let sdn_zones_status = status.map(|status| status.sdn_zones);
+pub fn create_sdn_panel(status: SharedState<LoadResult<ResourcesStatus, Error>>) -> 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<TaskStatistics>,
- error: Option<&Error>,
+ statistics: SharedState<LoadResult<TaskStatistics, Error>>,
remotes: Option<u32>,
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<Option<f64>>, threshold: f64) -> Container {
}
pub fn create_top_entities_panel(
- entities: Option<Vec<TopEntity>>,
- error: Option<&proxmox_client::Error>,
+ top_entities: SharedState<
+ LoadResult<pdm_api_types::resource::TopEntities, proxmox_client::Error>,
+ >,
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<LoadResult<TaskStatistics, Error>>,
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
next prev parent reply other threads:[~2025-10-31 12:48 UTC|newest]
Thread overview: 22+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-10-31 12:43 [pdm-devel] [PATCH datacenter-manager v3 00/21] prepare ui for customizable views Dominik Csapak
2025-10-31 12:43 ` [pdm-devel] [PATCH datacenter-manager v3 01/21] ui: dashboard: refactor guest panel creation to its own module Dominik Csapak
2025-10-31 12:43 ` [pdm-devel] [PATCH datacenter-manager v3 02/21] ui: dashboard: refactor creating the node panel into " Dominik Csapak
2025-10-31 12:43 ` [pdm-devel] [PATCH datacenter-manager v3 03/21] ui: dashboard: node panel: make remote type optional Dominik Csapak
2025-10-31 12:43 ` [pdm-devel] [PATCH datacenter-manager v3 04/21] ui: dashboard: refactor remote panel creation into its own module Dominik Csapak
2025-10-31 12:43 ` [pdm-devel] [PATCH datacenter-manager v3 05/21] ui: dashboard: remote panel: make wizard menu optional Dominik Csapak
2025-10-31 12:43 ` [pdm-devel] [PATCH datacenter-manager v3 06/21] ui: dashboard: refactor sdn panel creation into its own module Dominik Csapak
2025-10-31 12:43 ` [pdm-devel] [PATCH datacenter-manager v3 07/21] ui: dashboard: refactor task summary panel creation to " Dominik Csapak
2025-10-31 12:43 ` [pdm-devel] [PATCH datacenter-manager v3 08/21] ui: dashboard: task summary: disable virtual scrolling Dominik Csapak
2025-10-31 12:43 ` [pdm-devel] [PATCH datacenter-manager v3 09/21] ui: dashboard: refactor subscription panel creation to its own module Dominik Csapak
2025-10-31 12:43 ` [pdm-devel] [PATCH datacenter-manager v3 10/21] ui: dashboard: refactor top entities " Dominik Csapak
2025-10-31 12:43 ` [pdm-devel] [PATCH datacenter-manager v3 11/21] ui: dashboard: refactor DashboardConfig editing/constants to their module Dominik Csapak
2025-10-31 12:43 ` [pdm-devel] [PATCH datacenter-manager v3 12/21] ui: dashboard: factor out task parameter calculation Dominik Csapak
2025-10-31 12:43 ` [pdm-devel] [PATCH datacenter-manager v3 13/21] ui: dashboard: pbs datastores panel: refactor creation into own module Dominik Csapak
2025-10-31 12:43 ` [pdm-devel] [PATCH datacenter-manager v3 14/21] ui: dashboard: remove unused remote list Dominik Csapak
2025-10-31 12:43 ` [pdm-devel] [PATCH datacenter-manager v3 15/21] ui: dashboard: status row: make loading less jarring Dominik Csapak
2025-10-31 12:43 ` [pdm-devel] [PATCH datacenter-manager v3 16/21] ui: introduce `LoadResult` helper type Dominik Csapak
2025-10-31 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 17/21] ui: dashboard: implement 'View' Dominik Csapak
2025-10-31 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 18/21] ui: dashboard: use 'View' instead of the Dashboard Dominik Csapak
2025-10-31 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 19/21] ui: dashboard: subscription info: move subscription loading to view Dominik Csapak
2025-10-31 12:44 ` Dominik Csapak [this message]
2025-10-31 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 21/21] ui: dashboard: enable editing view Dominik Csapak
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=20251031124822.2739685-21-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