* [pdm-devel] [PATCH datacenter-manager 1/2] ui: dashboard: show finished loading data immediately
@ 2025-09-09 7:37 Dominik Csapak
2025-09-09 7:37 ` [pdm-devel] [PATCH datacenter-manager 2/2] ui: dashboard: render last refresh date/time instead of a seconds counter Dominik Csapak
0 siblings, 1 reply; 2+ messages in thread
From: Dominik Csapak @ 2025-09-09 7:37 UTC (permalink / raw)
To: pdm-devel
instead of waiting for all data to have come in, already show loaded
data as soon as it's available
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/dashboard/mod.rs | 220 ++++++++++++++++++++++------------------
1 file changed, 121 insertions(+), 99 deletions(-)
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 0659dc0..8d68dc2 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -25,7 +25,7 @@ use pwt::{
};
use pdm_api_types::{
- resource::{GuestStatusCount, NodeStatusCount, ResourcesStatus},
+ resource::{NodeStatusCount, ResourcesStatus},
TaskStatistics,
};
use pdm_client::types::TopEntity;
@@ -103,11 +103,12 @@ pub struct DashboardConfig {
task_last_hours: Option<u32>,
}
-pub type LoadingResult = (
- Result<ResourcesStatus, Error>,
- Result<pdm_client::types::TopEntities, proxmox_client::Error>,
- Result<TaskStatistics, Error>,
-);
+pub enum LoadingResult {
+ Resources(Result<ResourcesStatus, Error>),
+ TopEntities(Result<pdm_client::types::TopEntities, proxmox_client::Error>),
+ TaskStatistics(Result<TaskStatistics, Error>),
+ All,
+}
pub enum Msg {
LoadingFinished(LoadingResult),
@@ -126,7 +127,7 @@ struct StatisticsOptions {
}
pub struct PdmDashboard {
- status: ResourcesStatus,
+ status: Option<ResourcesStatus>,
last_error: Option<Error>,
top_entities: Option<pdm_client::types::TopEntities>,
last_top_entities_error: Option<proxmox_client::Error>,
@@ -152,46 +153,47 @@ impl PdmDashboard {
.into()
}
- fn create_node_panel(
- &self,
- ctx: &yew::Context<Self>,
- icon: &str,
- title: String,
- status: &NodeStatusCount,
- ) -> Panel {
+ fn create_node_panel(&self, ctx: &yew::Context<Self>, icon: &str, title: String) -> Panel {
let mut search_terms = vec![SearchTerm::new("node").category(Some("type"))];
- let (status_icon, text): (Fa, String) = match status {
- NodeStatusCount {
- online,
- offline,
- unknown,
- } if *offline > 0 => {
- search_terms.push(SearchTerm::new("offline").category(Some("status")));
- (
- Status::Error.into(),
- tr!(
- "{0} of {1} nodes are offline",
+ let (status_icon, text): (Fa, String) = match &self.status {
+ Some(status) => {
+ match status.pve_nodes {
+ NodeStatusCount {
+ online,
offline,
- online + offline + unknown,
+ unknown,
+ } if offline > 0 => {
+ search_terms.push(SearchTerm::new("offline").category(Some("status")));
+ (
+ Status::Error.into(),
+ tr!(
+ "{0} of {1} nodes are offline",
+ offline,
+ online + offline + unknown,
+ ),
+ )
+ }
+ NodeStatusCount { unknown, .. } if unknown > 0 => {
+ search_terms.push(SearchTerm::new("unknown").category(Some("status")));
+ (
+ Status::Warning.into(),
+ tr!("{0} nodes have an unknown status", unknown),
+ )
+ }
+ // FIXME, get more detailed status about the failed remotes (name, type, error)?
+ NodeStatusCount { online, .. } if status.failed_remotes > 0 => (
+ Status::Unknown.into(),
+ tr!("{0} of an unknown number of nodes online", online),
),
- )
- }
- NodeStatusCount { unknown, .. } if *unknown > 0 => {
- search_terms.push(SearchTerm::new("unknown").category(Some("status")));
- (
- Status::Warning.into(),
- tr!("{0} nodes have an unknown status", unknown),
- )
- }
- // FIXME, get more detailed status about the failed remotes (name, type, error)?
- NodeStatusCount { online, .. } if self.status.failed_remotes > 0 => (
- Status::Unknown.into(),
- tr!("{0} of an unknown number of nodes online", online),
- ),
- NodeStatusCount { online, .. } => {
- (Status::Success.into(), tr!("{0} nodes online", online))
+ NodeStatusCount { online, .. } => {
+ (Status::Success.into(), tr!("{0} nodes online", online))
+ }
+ }
}
+ None => (Status::Unknown.into(), String::new()),
};
+
+ let loading = self.status.is_none();
let search = Search::with_terms(search_terms);
Panel::new()
.flex(1.0)
@@ -217,29 +219,34 @@ impl PdmDashboard {
.class(AlignItems::Center)
.class(JustifyContent::Center)
.gap(2)
- .with_child(if self.loading {
+ .with_child(if loading {
html! {<i class={"pwt-loading-icon"} />}
} else {
status_icon.large_4x().into()
})
- .with_optional_child((!self.loading).then_some(text)),
+ .with_optional_child((!loading).then_some(text)),
)
}
- fn create_guest_panel(&self, guest_type: GuestType, status: &GuestStatusCount) -> Panel {
- let (icon, title) = match guest_type {
- GuestType::Qemu => ("desktop", tr!("Virtual Machines")),
- GuestType::Lxc => ("cubes", tr!("Linux Container")),
+ fn create_guest_panel(&self, guest_type: GuestType) -> Panel {
+ let (icon, title, status) = match guest_type {
+ GuestType::Qemu => (
+ "desktop",
+ tr!("Virtual Machines"),
+ self.status.as_ref().map(|s| s.qemu.clone()),
+ ),
+ GuestType::Lxc => (
+ "cubes",
+ tr!("Linux Container"),
+ self.status.as_ref().map(|s| s.lxc.clone()),
+ ),
};
Panel::new()
.flex(1.0)
.width(300)
.title(self.create_title_with_icon(icon, title))
.border(true)
- .with_child(GuestPanel::new(
- guest_type,
- (!self.loading).then_some(status.clone()),
- ))
+ .with_child(GuestPanel::new(guest_type, status))
}
fn create_task_summary_panel(
@@ -323,8 +330,21 @@ impl PdmDashboard {
self.async_pool.spawn(async move {
let client = crate::pdm_client();
- let top_entities_future = client.get_top_entities();
- let status_future = http_get("/resources/status", Some(json!({"max-age": max_age})));
+ let top_entities_future = {
+ let link = link.clone();
+ async move {
+ let res = client.get_top_entities().await;
+ link.send_message(Msg::LoadingFinished(LoadingResult::TopEntities(res)));
+ }
+ };
+ let status_future = {
+ let link = link.clone();
+ async move {
+ let res: Result<ResourcesStatus, _> =
+ http_get("/resources/status", Some(json!({"max-age": max_age}))).await;
+ link.send_message(Msg::LoadingFinished(LoadingResult::Resources(res)));
+ }
+ };
let params = Some(json!({
"since": since,
@@ -332,16 +352,16 @@ impl PdmDashboard {
}));
// TODO replace with pdm client call
- let statistics_future = http_get("/remote-tasks/statistics", params);
-
- let (top_entities_res, status_res, statistics_res) =
- join!(top_entities_future, status_future, statistics_future);
-
- link.send_message(Msg::LoadingFinished((
- status_res,
- top_entities_res,
- statistics_res,
- )));
+ let statistics_future = {
+ let link = link.clone();
+ async move {
+ let res: Result<TaskStatistics, _> =
+ http_get("/remote-tasks/statistics", params).await;
+ link.send_message(Msg::LoadingFinished(LoadingResult::TaskStatistics(res)));
+ }
+ };
+ join!(top_entities_future, status_future, statistics_future);
+ link.send_message(Msg::LoadingFinished(LoadingResult::All));
});
}
@@ -367,7 +387,7 @@ impl Component for PdmDashboard {
.expect("No Remote list context provided");
let mut this = Self {
- status: ResourcesStatus::default(),
+ status: None,
last_error: None,
top_entities: None,
last_top_entities_error: None,
@@ -393,36 +413,41 @@ impl Component for PdmDashboard {
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
- Msg::LoadingFinished((resources_status, top_entities, task_statistics)) => {
- match resources_status {
- Ok(status) => {
- self.last_error = None;
- self.status = status;
- }
- Err(err) => self.last_error = Some(err),
- }
- match top_entities {
- Ok(data) => {
- self.last_top_entities_error = None;
- self.top_entities = Some(data);
- }
- Err(err) => self.last_top_entities_error = Some(err),
- }
- match task_statistics {
- Ok(statistics) => {
- self.statistics.error = None;
- self.statistics.data = Some(statistics);
+ Msg::LoadingFinished(res) => {
+ match res {
+ LoadingResult::Resources(resources_status) => match resources_status {
+ Ok(status) => {
+ self.last_error = None;
+ self.status = Some(status);
+ }
+ Err(err) => self.last_error = Some(err),
+ },
+ LoadingResult::TopEntities(top_entities) => match top_entities {
+ Ok(data) => {
+ self.last_top_entities_error = None;
+ self.top_entities = Some(data);
+ }
+ Err(err) => self.last_top_entities_error = Some(err),
+ },
+
+ LoadingResult::TaskStatistics(task_statistics) => match task_statistics {
+ Ok(statistics) => {
+ self.statistics.error = None;
+ self.statistics.data = Some(statistics);
+ }
+ Err(err) => self.statistics.error = Some(err),
+ },
+ LoadingResult::All => {
+ self.loading = false;
+ if !self.loaded_once {
+ self.loaded_once = true;
+ // immediately trigger a "normal" reload after the first load with the
+ // configured or default max-age to ensure users sees more current data.
+ ctx.link().send_message(Msg::Reload);
+ }
+ self.load_finished_time = Some(Date::now() / 1000.0);
}
- Err(err) => self.statistics.error = Some(err),
- }
- self.loading = false;
- if !self.loaded_once {
- self.loaded_once = true;
- // immediately trigger a "normal" reload after the first load with the
- // configured or default max-age to ensure users sees more current data.
- ctx.link().send_message(Msg::Reload);
}
- self.load_finished_time = Some(Date::now() / 1000.0);
true
}
Msg::RemoteListChanged(remote_list) => {
@@ -504,18 +529,15 @@ impl Component for PdmDashboard {
.icon_class("fa fa-plus-circle")
.on_activate(ctx.link().callback(|_| Msg::CreateWizard(true))),
)
- .with_child(RemotePanel::new(
- (!self.loading).then_some(self.status.clone()),
- )),
+ .with_child(RemotePanel::new(self.status.clone())),
)
.with_child(self.create_node_panel(
ctx,
"building",
tr!("Virtual Environment Nodes"),
- &self.status.pve_nodes,
))
- .with_child(self.create_guest_panel(GuestType::Qemu, &self.status.qemu))
- .with_child(self.create_guest_panel(GuestType::Lxc, &self.status.lxc))
+ .with_child(self.create_guest_panel(GuestType::Qemu))
+ .with_child(self.create_guest_panel(GuestType::Lxc))
// FIXME: add PBS support
//.with_child(self.create_node_panel(
// "building-o",
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 2+ messages in thread
* [pdm-devel] [PATCH datacenter-manager 2/2] ui: dashboard: render last refresh date/time instead of a seconds counter
2025-09-09 7:37 [pdm-devel] [PATCH datacenter-manager 1/2] ui: dashboard: show finished loading data immediately Dominik Csapak
@ 2025-09-09 7:37 ` Dominik Csapak
0 siblings, 0 replies; 2+ messages in thread
From: Dominik Csapak @ 2025-09-09 7:37 UTC (permalink / raw)
To: pdm-devel
this is less jarring, since it does not change the content of the page
as often. Since we now only have to do things when we actually reload,
we can use the actual configured interval for the internal one, and
don't have to check every second if that elapsed.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/dashboard/mod.rs | 4 +--
ui/src/dashboard/status_row.rs | 47 +++++++++++++---------------------
2 files changed, 20 insertions(+), 31 deletions(-)
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 8d68dc2..11b4e2d 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -71,7 +71,7 @@ pub const DEFAULT_MAX_AGE_S: u64 = 30;
/// The default refresh interval, we poll more frequently than the default max-age to quicker show
/// any new data that was gathered either by the backend polling tasks or by a manual update
/// triggered by another user.
-pub const DEFAULT_REFRESH_INTERVAL_S: u64 = 10;
+pub const DEFAULT_REFRESH_INTERVAL_S: u32 = 10;
/// The default hours to show for task summaries. Use 2 days to ensure that all tasks from yesterday
/// are included independent from the time a user checks the dashboard on the current day.
@@ -96,7 +96,7 @@ impl Default for Dashboard {
#[serde(rename_all = "kebab-case")]
pub struct DashboardConfig {
#[serde(skip_serializing_if = "Option::is_none")]
- refresh_interval: Option<u64>,
+ refresh_interval: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
max_age: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
diff --git a/ui/src/dashboard/status_row.rs b/ui/src/dashboard/status_row.rs
index 85048fe..5e37741 100644
--- a/ui/src/dashboard/status_row.rs
+++ b/ui/src/dashboard/status_row.rs
@@ -1,5 +1,4 @@
use gloo_timers::callback::Interval;
-use js_sys::Date;
use yew::{Component, Properties};
use pwt::prelude::*;
@@ -9,13 +8,13 @@ use pwt::{
};
use pwt_macros::widget;
-use proxmox_yew_comp::utils::format_duration_human;
+use proxmox_yew_comp::utils::render_epoch;
#[widget(comp=PdmDashboardStatusRow)]
#[derive(Properties, PartialEq, Clone)]
pub struct DashboardStatusRow {
last_refresh: Option<f64>,
- reload_interval_s: u64,
+ reload_interval_s: u32,
on_reload: Callback<bool>,
@@ -25,7 +24,7 @@ pub struct DashboardStatusRow {
impl DashboardStatusRow {
pub fn new(
last_refresh: Option<f64>,
- reload_interval_s: u64,
+ reload_interval_s: u32,
on_reload: impl Into<Callback<bool>>,
on_settings_click: impl Into<Callback<()>>,
) -> Self {
@@ -41,24 +40,24 @@ impl DashboardStatusRow {
pub enum Msg {
/// The bool denotes if the reload comes from the click or the timer.
Reload(bool),
- CheckReload,
}
#[doc(hidden)]
pub struct PdmDashboardStatusRow {
- _interval: Option<Interval>,
+ _interval: Interval,
}
impl PdmDashboardStatusRow {
- fn update_interval(&mut self, ctx: &yew::Context<Self>) {
+ fn create_interval(ctx: &yew::Context<Self>) -> Interval {
let link = ctx.link().clone();
- let _interval = ctx.props().last_refresh.map(|_| {
- Interval::new(1000, move || {
- link.send_message(Msg::CheckReload);
- })
- });
+ let _interval = Interval::new(
+ ctx.props().reload_interval_s.saturating_mul(1000),
+ move || {
+ link.send_message(Msg::Reload(false));
+ },
+ );
- self._interval = _interval;
+ _interval
}
}
@@ -67,9 +66,9 @@ impl Component for PdmDashboardStatusRow {
type Properties = DashboardStatusRow;
fn create(ctx: &yew::Context<Self>) -> Self {
- let mut this = Self { _interval: None };
- this.update_interval(ctx);
- this
+ Self {
+ _interval: Self::create_interval(ctx),
+ }
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
@@ -79,21 +78,11 @@ impl Component for PdmDashboardStatusRow {
props.on_reload.emit(clicked);
true
}
- Msg::CheckReload => match ctx.props().last_refresh {
- Some(last_refresh) => {
- let duration = Date::now() / 1000.0 - last_refresh;
- if duration >= props.reload_interval_s as f64 {
- ctx.link().send_message(Msg::Reload(false));
- }
- true
- }
- None => false,
- },
}
}
fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
- self.update_interval(ctx);
+ self._interval = Self::create_interval(ctx);
true
}
@@ -119,8 +108,8 @@ impl Component for PdmDashboardStatusRow {
)
.with_child(Container::new().with_child(match ctx.props().last_refresh {
Some(last_refresh) => {
- let duration = Date::now() / 1000.0 - last_refresh;
- tr!("Last refreshed: {0} ago", format_duration_human(duration))
+ let date = render_epoch(last_refresh as i64);
+ tr!("Last refresh: {0}", date)
}
None => tr!("Now refreshing"),
}))
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2025-09-09 7:38 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-09-09 7:37 [pdm-devel] [PATCH datacenter-manager 1/2] ui: dashboard: show finished loading data immediately Dominik Csapak
2025-09-09 7:37 ` [pdm-devel] [PATCH datacenter-manager 2/2] ui: dashboard: render last refresh date/time instead of a seconds counter Dominik Csapak
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.