* [pdm-devel] [PATCH datacenter-manager 01/15] ui: dashboard: refactor guest panel creation to its own module
2025-10-21 14:03 [pdm-devel] [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
@ 2025-10-21 14:03 ` Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 02/15] ui: dashboard: refactor creating the node panel into " Dominik Csapak
` (14 subsequent siblings)
15 siblings, 0 replies; 17+ messages in thread
From: Dominik Csapak @ 2025-10-21 14:03 UTC (permalink / raw)
To: pdm-devel
so we can more easily reuse it. For this, also make the 'create_title_with_icon'
a freestanding function that is public so we can reuse it outside the
dashboard struct.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/dashboard/guest_panel.rs | 75 +++++++++++++++++++++++++--------
ui/src/dashboard/mod.rs | 66 ++++++++++++-----------------
2 files changed, 84 insertions(+), 57 deletions(-)
diff --git a/ui/src/dashboard/guest_panel.rs b/ui/src/dashboard/guest_panel.rs
index 814ecfa5..3197feb4 100644
--- a/ui/src/dashboard/guest_panel.rs
+++ b/ui/src/dashboard/guest_panel.rs
@@ -1,30 +1,32 @@
use std::rc::Rc;
-use pdm_api_types::resource::{GuestStatusCount, ResourceType};
+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},
+ widget::{Container, Fa, List, ListTile, Panel},
};
use yew::{
virtual_dom::{VComp, VNode},
Properties,
};
-use crate::{pve::GuestType, search_provider::get_search_provider};
+use crate::{
+ dashboard::create_title_with_icon, pve::GuestType, search_provider::get_search_provider,
+};
use super::loading_column;
#[derive(PartialEq, Clone, Properties)]
pub struct GuestPanel {
- guest_type: GuestType,
- status: Option<GuestStatusCount>,
+ guest_type: Option<GuestType>,
+ status: Option<ResourcesStatus>,
}
impl GuestPanel {
- pub fn new(guest_type: GuestType, status: Option<GuestStatusCount>) -> Self {
+ pub fn new(guest_type: Option<GuestType>, status: Option<ResourcesStatus>) -> Self {
yew::props!(Self { guest_type, status })
}
}
@@ -63,7 +65,16 @@ impl yew::Component for PdmGuestPanel {
let props = ctx.props();
let guest_type = props.guest_type;
let status = match &props.status {
- Some(status) => status,
+ Some(status) => match guest_type {
+ Some(GuestType::Qemu) => status.qemu.clone(),
+ Some(GuestType::Lxc) => status.lxc.clone(),
+ None => GuestStatusCount {
+ running: status.qemu.running + status.lxc.running,
+ stopped: status.qemu.stopped + status.lxc.stopped,
+ template: status.qemu.template + status.lxc.template,
+ unknown: status.qemu.unknown + status.lxc.unknown,
+ },
+ },
None => return loading_column().into(),
};
@@ -93,7 +104,7 @@ impl yew::Component for PdmGuestPanel {
fn create_list_tile(
link: &html::Scope<PdmGuestPanel>,
- guest_type: GuestType,
+ guest_type: Option<GuestType>,
status_row: StatusRow,
) -> Option<ListTile> {
let (icon, text, count, status, template) = match status_row {
@@ -129,7 +140,13 @@ fn create_list_tile(
None,
),
},
- StatusRow::All(count) => (Fa::from(guest_type), tr!("All"), count, None, None),
+ StatusRow::All(count) => (
+ Fa::from(guest_type.unwrap_or(GuestType::Qemu)),
+ tr!("All"),
+ count,
+ None,
+ None,
+ ),
};
Some(
@@ -158,19 +175,29 @@ fn create_list_tile(
}
fn create_guest_search_term(
- guest_type: GuestType,
+ guest_type: Option<GuestType>,
status: Option<&'static str>,
template: Option<bool>,
) -> Search {
- let resource_type: ResourceType = guest_type.into();
- if status.is_none() && template.is_none() {
- return Search::with_terms(vec![
- SearchTerm::new(resource_type.as_str()).category(Some("type"))
- ]);
+ let mut terms = Vec::new();
+ match guest_type {
+ Some(guest_type) => {
+ let resource_type: ResourceType = guest_type.into();
+ terms.push(SearchTerm::new(resource_type.as_str()).category(Some("type")));
+ }
+ None => {
+ terms.push(
+ SearchTerm::new(ResourceType::PveQemu.as_str())
+ .category(Some("type"))
+ .optional(true),
+ );
+ terms.push(
+ SearchTerm::new(ResourceType::PveLxc.as_str())
+ .category(Some("type"))
+ .optional(true),
+ );
+ }
}
-
- let mut terms = vec![SearchTerm::new(resource_type.as_str()).category(Some("type"))];
-
if let Some(template) = template {
terms.push(SearchTerm::new(template.to_string()).category(Some("template")));
}
@@ -179,3 +206,15 @@ fn create_guest_search_term(
}
Search::with_terms(terms)
}
+
+pub fn create_guest_panel(guest_type: Option<GuestType>, status: Option<ResourcesStatus>) -> 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")),
+ };
+ Panel::new()
+ .title(create_title_with_icon(icon, title))
+ .border(true)
+ .with_child(GuestPanel::new(guest_type, status))
+}
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index f54c509c..602c8a3b 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -46,7 +46,7 @@ mod remote_panel;
use remote_panel::RemotePanel;
mod guest_panel;
-use guest_panel::GuestPanel;
+pub use guest_panel::create_guest_panel;
mod sdn_zone_panel;
use sdn_zone_panel::SdnZonePanel;
@@ -149,15 +149,6 @@ pub struct PdmDashboard {
}
impl PdmDashboard {
- fn create_title_with_icon(&self, icon: &str, title: String) -> Html {
- Row::new()
- .class(AlignItems::Center)
- .gap(2)
- .with_child(Fa::new(icon))
- .with_child(title)
- .into()
- }
-
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 &self.status {
@@ -203,7 +194,7 @@ impl PdmDashboard {
Panel::new()
.flex(1.0)
.width(300)
- .title(self.create_title_with_icon(icon, title))
+ .title(create_title_with_icon(icon, title))
.border(true)
.with_child(
Column::new()
@@ -233,34 +224,13 @@ impl PdmDashboard {
)
}
- 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, status))
- }
-
fn create_sdn_panel(&self) -> Panel {
let sdn_zones_status = self.status.as_ref().map(|status| status.sdn_zones.clone());
Panel::new()
.flex(1.0)
.width(200)
- .title(self.create_title_with_icon("sdn", tr!("SDN Zones")))
+ .title(create_title_with_icon("sdn", tr!("SDN Zones")))
.border(true)
.with_child(SdnZonePanel::new(
(!self.loading).then_some(sdn_zones_status).flatten(),
@@ -281,7 +251,7 @@ impl PdmDashboard {
.flex(1.0)
.width(500)
.border(true)
- .title(self.create_title_with_icon("list", title))
+ .title(create_title_with_icon("list", title))
.with_child(
Container::new()
.class(FlexFit)
@@ -318,7 +288,7 @@ impl PdmDashboard {
.width(500)
.min_width(400)
.border(true)
- .title(self.create_title_with_icon(icon, title))
+ .title(create_title_with_icon(icon, title))
.with_optional_child(
entities
.map(|entities| TopEntities::new(entities.clone(), metrics_title, threshold)),
@@ -537,7 +507,7 @@ impl Component for PdmDashboard {
.padding_top(0)
.with_child(
Panel::new()
- .title(self.create_title_with_icon("server", tr!("Remotes")))
+ .title(create_title_with_icon("server", tr!("Remotes")))
.flex(1.0)
//.border(true)
.width(300)
@@ -568,8 +538,16 @@ impl Component for PdmDashboard {
"building",
tr!("Virtual Environment Nodes"),
))
- .with_child(self.create_guest_panel(GuestType::Qemu))
- .with_child(self.create_guest_panel(GuestType::Lxc))
+ .with_child(
+ create_guest_panel(Some(GuestType::Qemu), self.status.clone())
+ .flex(1.0)
+ .width(300),
+ )
+ .with_child(
+ create_guest_panel(Some(GuestType::Lxc), self.status.clone())
+ .flex(1.0)
+ .width(300),
+ )
// FIXME: add PBS support
//.with_child(self.create_node_panel(
// "building-o",
@@ -580,7 +558,7 @@ impl Component for PdmDashboard {
// Panel::new()
// .flex(1.0)
// .width(300)
- // .title(self.create_title_with_icon(
+ // .title(create_title_with_icon(
// "floppy-o",
// tr!("Backup Server Datastores"),
// ))
@@ -777,3 +755,13 @@ fn loading_column() -> Column {
.class(AlignItems::Center)
.with_child(html! {<i class={"pwt-loading-icon"} />})
}
+
+/// Create a consistent title component for the given title and icon
+pub fn create_title_with_icon(icon: &str, title: String) -> Html {
+ Row::new()
+ .class(AlignItems::Center)
+ .gap(2)
+ .with_child(Fa::new(icon))
+ .with_child(title)
+ .into()
+}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pdm-devel] [PATCH datacenter-manager 02/15] ui: dashboard: refactor creating the node panel into its own module
2025-10-21 14:03 [pdm-devel] [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 01/15] ui: dashboard: refactor guest panel creation to its own module Dominik Csapak
@ 2025-10-21 14:03 ` Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 03/15] ui: dashboard: refactor remote panel creation " Dominik Csapak
` (13 subsequent siblings)
15 siblings, 0 replies; 17+ messages in thread
From: Dominik Csapak @ 2025-10-21 14:03 UTC (permalink / raw)
To: pdm-devel
so we can easily reuse it outside the Dashboard struct. Since this
shifts the search logic there, it can be removed from the Dashboard
struct.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/dashboard/mod.rs | 106 +++--------------------
ui/src/dashboard/node_panel.rs | 150 +++++++++++++++++++++++++++++++++
2 files changed, 161 insertions(+), 95 deletions(-)
create mode 100644 ui/src/dashboard/node_panel.rs
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 602c8a3b..2e170443 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -10,7 +10,7 @@ use yew::{
Component,
};
-use proxmox_yew_comp::{http_get, EditWindow, Status};
+use proxmox_yew_comp::{http_get, EditWindow};
use pwt::{
css::{AlignItems, FlexDirection, FlexFit, FlexWrap, JustifyContent},
prelude::*,
@@ -25,16 +25,11 @@ use pwt::{
AsyncPool,
};
-use pdm_api_types::{
- remotes::RemoteType,
- resource::{NodeStatusCount, ResourcesStatus},
- TaskStatistics,
-};
+use pdm_api_types::{remotes::RemoteType, resource::ResourcesStatus, TaskStatistics};
use pdm_client::types::TopEntity;
-use pdm_search::{Search, SearchTerm};
use proxmox_client::ApiResponseData;
-use crate::{pve::GuestType, remotes::AddWizard, search_provider::get_search_provider, RemoteList};
+use crate::{pve::GuestType, remotes::AddWizard, RemoteList};
mod top_entities;
pub use top_entities::TopEntities;
@@ -42,6 +37,9 @@ pub use top_entities::TopEntities;
mod subscription_info;
pub use subscription_info::SubscriptionInfo;
+mod node_panel;
+pub use node_panel::create_node_panel;
+
mod remote_panel;
use remote_panel::RemotePanel;
@@ -123,7 +121,6 @@ pub enum Msg {
ForceReload,
UpdateConfig(DashboardConfig),
ConfigWindow(bool),
- Search(Search),
}
struct StatisticsOptions {
@@ -149,81 +146,6 @@ pub struct PdmDashboard {
}
impl PdmDashboard {
- 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 &self.status {
- Some(status) => {
- match status.pve_nodes {
- 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",
- 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 { 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)
- .width(300)
- .title(create_title_with_icon(icon, title))
- .border(true)
- .with_child(
- Column::new()
- .padding(4)
- .class("pwt-pointer")
- .onclick(ctx.link().callback({
- let search = search.clone();
- move |_| Msg::Search(search.clone())
- }))
- .onkeydown(ctx.link().batch_callback({
- let search = search.clone();
- move |event: KeyboardEvent| match event.key().as_str() {
- "Enter" | " " => Some(Msg::Search(search.clone())),
- _ => None,
- }
- }))
- .class(FlexFit)
- .class(AlignItems::Center)
- .class(JustifyContent::Center)
- .gap(2)
- .with_child(if loading {
- html! {<i class={"pwt-loading-icon"} />}
- } else {
- status_icon.large_4x().into()
- })
- .with_optional_child((!loading).then_some(text)),
- )
- }
-
fn create_sdn_panel(&self) -> Panel {
let sdn_zones_status = self.status.as_ref().map(|status| status.sdn_zones.clone());
@@ -471,12 +393,6 @@ impl Component for PdmDashboard {
self.show_config_window = false;
true
}
- Msg::Search(search_term) => {
- if let Some(provider) = get_search_provider(ctx) {
- provider.search(search_term.into());
- }
- false
- }
}
}
@@ -533,11 +449,11 @@ impl Component for PdmDashboard {
)
.with_child(RemotePanel::new(self.status.clone())),
)
- .with_child(self.create_node_panel(
- ctx,
- "building",
- tr!("Virtual Environment Nodes"),
- ))
+ .with_child(
+ create_node_panel(Some(RemoteType::Pve), self.status.clone())
+ .flex(1.0)
+ .width(300),
+ )
.with_child(
create_guest_panel(Some(GuestType::Qemu), self.status.clone())
.flex(1.0)
diff --git a/ui/src/dashboard/node_panel.rs b/ui/src/dashboard/node_panel.rs
new file mode 100644
index 00000000..76ff424e
--- /dev/null
+++ b/ui/src/dashboard/node_panel.rs
@@ -0,0 +1,150 @@
+use std::rc::Rc;
+
+use yew::virtual_dom::{VComp, VNode};
+
+use proxmox_yew_comp::Status;
+use pwt::widget::{Column, Fa, Panel};
+use pwt::{css, prelude::*};
+
+use pdm_api_types::remotes::RemoteType;
+use pdm_api_types::resource::{NodeStatusCount, ResourcesStatus};
+use pdm_search::{Search, SearchTerm};
+
+use crate::dashboard::create_title_with_icon;
+use crate::search_provider::get_search_provider;
+
+#[derive(Properties, PartialEq)]
+pub struct NodePanel {
+ remote_type: Option<RemoteType>,
+ status: Option<ResourcesStatus>,
+}
+
+impl NodePanel {
+ pub fn new(remote_type: Option<RemoteType>, status: Option<ResourcesStatus>) -> Self {
+ Self {
+ remote_type,
+ status,
+ }
+ }
+}
+
+impl From<NodePanel> for VNode {
+ fn from(value: NodePanel) -> Self {
+ let comp = VComp::new::<PdmNodePanel>(Rc::new(value), None);
+ VNode::from(comp)
+ }
+}
+
+pub struct PdmNodePanel {}
+
+impl Component for PdmNodePanel {
+ type Message = Search;
+ type Properties = NodePanel;
+
+ fn create(_ctx: &Context<Self>) -> Self {
+ Self {}
+ }
+
+ fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
+ if let Some(provider) = get_search_provider(ctx) {
+ provider.search(msg);
+ }
+ false
+ }
+
+ fn view(&self, ctx: &Context<Self>) -> Html {
+ let props = ctx.props();
+ let remote_type = &props.remote_type;
+ let status = &props.status;
+ let mut search_terms = vec![SearchTerm::new("node").category(Some("type"))];
+ let (status_icon, text): (Fa, String) = match &status {
+ Some(status) => {
+ let count = match remote_type {
+ Some(RemoteType::Pve) => status.pve_nodes.clone(),
+ Some(RemoteType::Pbs) => status.pbs_nodes.clone(),
+ None => NodeStatusCount {
+ online: status.pve_nodes.online + status.pbs_nodes.online,
+ offline: status.pve_nodes.offline + status.pbs_nodes.offline,
+ unknown: status.pve_nodes.unknown + status.pbs_nodes.offline,
+ },
+ };
+ match count {
+ 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",
+ 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 { online, .. } => {
+ (Status::Success.into(), tr!("{0} nodes online", online))
+ }
+ }
+ }
+ None => (Status::Unknown.into(), String::new()),
+ };
+
+ let loading = status.is_none();
+ let search = Search::with_terms(search_terms);
+ Column::new()
+ .padding(4)
+ .class("pwt-pointer")
+ .onclick(ctx.link().callback({
+ let search = search.clone();
+ move |_| search.clone()
+ }))
+ .onkeydown(ctx.link().batch_callback({
+ let search = search.clone();
+ move |event: KeyboardEvent| match event.key().as_str() {
+ "Enter" | " " => Some(search.clone()),
+ _ => None,
+ }
+ }))
+ .class(css::FlexFit)
+ .class(css::AlignItems::Center)
+ .class(css::JustifyContent::Center)
+ .gap(2)
+ .with_child(if loading {
+ html! {<i class={"pwt-loading-icon"} />}
+ } else {
+ status_icon.large_4x().into()
+ })
+ .with_optional_child((!loading).then_some(text))
+ .into()
+ }
+}
+
+pub fn create_node_panel(
+ remote_type: Option<RemoteType>,
+ status: Option<ResourcesStatus>,
+) -> Panel {
+ let (icon, title) = match remote_type {
+ Some(RemoteType::Pve) => ("building", tr!("Virtual Environment Nodes")),
+ Some(RemoteType::Pbs) => ("building-o", tr!("Backup Server Nodes")),
+ None => ("building", tr!("Nodes")),
+ };
+ Panel::new()
+ .title(create_title_with_icon(icon, title))
+ .border(true)
+ .with_child(NodePanel::new(remote_type, status))
+}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pdm-devel] [PATCH datacenter-manager 03/15] ui: dashboard: refactor remote panel creation into its own module
2025-10-21 14:03 [pdm-devel] [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 01/15] ui: dashboard: refactor guest panel creation to its own module Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 02/15] ui: dashboard: refactor creating the node panel into " Dominik Csapak
@ 2025-10-21 14:03 ` Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 04/15] ui: dashboard: remote panel: make wizard menu optional Dominik Csapak
` (12 subsequent siblings)
15 siblings, 0 replies; 17+ messages in thread
From: Dominik Csapak @ 2025-10-21 14:03 UTC (permalink / raw)
To: pdm-devel
so we can more easily reuse them outside the dashboard struct.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/dashboard/mod.rs | 39 ++++++++-------------------
ui/src/dashboard/remote_panel.rs | 46 ++++++++++++++++++++++++--------
2 files changed, 46 insertions(+), 39 deletions(-)
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 2e170443..2fe4d7fa 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -19,7 +19,6 @@ use pwt::{
widget::{
error_message,
form::{DisplayField, FormContext, Number},
- menu::{Menu, MenuButton, MenuItem},
Column, Container, Fa, InputPanel, Panel, Row,
},
AsyncPool,
@@ -41,7 +40,7 @@ mod node_panel;
pub use node_panel::create_node_panel;
mod remote_panel;
-use remote_panel::RemotePanel;
+pub use remote_panel::create_remote_panel;
mod guest_panel;
pub use guest_panel::create_guest_panel;
@@ -422,32 +421,16 @@ impl Component for PdmDashboard {
.class(FlexWrap::Wrap)
.padding_top(0)
.with_child(
- Panel::new()
- .title(create_title_with_icon("server", tr!("Remotes")))
- .flex(1.0)
- //.border(true)
- .width(300)
- .min_height(175)
- .with_tool(
- MenuButton::new(tr!("Add")).show_arrow(true).menu(
- Menu::new()
- .with_item(
- MenuItem::new("Proxmox VE")
- .icon_class("fa fa-building")
- .on_select(ctx.link().callback(|_| {
- Msg::CreateWizard(Some(RemoteType::Pve))
- })),
- )
- .with_item(
- MenuItem::new("Proxmox Backup Server")
- .icon_class("fa fa-floppy-o")
- .on_select(ctx.link().callback(|_| {
- Msg::CreateWizard(Some(RemoteType::Pbs))
- })),
- ),
- ),
- )
- .with_child(RemotePanel::new(self.status.clone())),
+ create_remote_panel(
+ self.status.clone(),
+ ctx.link()
+ .callback(|_| Msg::CreateWizard(Some(RemoteType::Pve))),
+ ctx.link()
+ .callback(|_| Msg::CreateWizard(Some(RemoteType::Pbs))),
+ )
+ .flex(1.0)
+ .width(300)
+ .min_height(175),
)
.with_child(
create_node_panel(Some(RemoteType::Pve), self.status.clone())
diff --git a/ui/src/dashboard/remote_panel.rs b/ui/src/dashboard/remote_panel.rs
index 75f772fb..27eebac2 100644
--- a/ui/src/dashboard/remote_panel.rs
+++ b/ui/src/dashboard/remote_panel.rs
@@ -1,21 +1,19 @@
use std::rc::Rc;
+use yew::html::IntoEventCallback;
+use yew::virtual_dom::{VComp, VNode};
+
use pdm_search::{Search, SearchTerm};
use proxmox_yew_comp::Status;
-use pwt::{
- css,
- prelude::*,
- props::{ContainerBuilder, WidgetBuilder},
- widget::{Column, Container, Fa},
-};
-use yew::{
- virtual_dom::{VComp, VNode},
- Component, Properties,
-};
+use pwt::css;
+use pwt::prelude::*;
+use pwt::props::{ContainerBuilder, WidgetBuilder};
+use pwt::widget::menu::{Menu, MenuButton, MenuEvent, MenuItem};
+use pwt::widget::{Column, Container, Fa, Panel};
use pdm_api_types::resource::ResourcesStatus;
-use crate::search_provider::get_search_provider;
+use crate::{dashboard::create_title_with_icon, search_provider::get_search_provider};
#[derive(Properties, PartialEq)]
/// A panel for showing the overall remotes status
@@ -116,3 +114,29 @@ fn create_search_term(failure: bool) -> Search {
Search::with_terms(vec![SearchTerm::new("remote").category(Some("type"))])
}
}
+
+pub fn create_remote_panel(
+ status: Option<ResourcesStatus>,
+ on_pve_wizard: impl IntoEventCallback<MenuEvent>,
+ on_pbs_wizard: impl IntoEventCallback<MenuEvent>,
+) -> Panel {
+ Panel::new()
+ .title(create_title_with_icon("server", tr!("Remotes")))
+ .border(true)
+ .with_tool(
+ MenuButton::new(tr!("Add")).show_arrow(true).menu(
+ Menu::new()
+ .with_item(
+ MenuItem::new("Proxmox VE")
+ .icon_class("fa fa-building")
+ .on_select(on_pve_wizard),
+ )
+ .with_item(
+ MenuItem::new("Proxmox Backup Server")
+ .icon_class("fa fa-floppy-o")
+ .on_select(on_pbs_wizard),
+ ),
+ ),
+ )
+ .with_child(RemotePanel::new(status))
+}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pdm-devel] [PATCH datacenter-manager 04/15] ui: dashboard: remote panel: make wizard menu optional
2025-10-21 14:03 [pdm-devel] [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
` (2 preceding siblings ...)
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 03/15] ui: dashboard: refactor remote panel creation " Dominik Csapak
@ 2025-10-21 14:03 ` Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 05/15] ui: dashboard: refactor sdn panel creation into its own module Dominik Csapak
` (11 subsequent siblings)
15 siblings, 0 replies; 17+ messages in thread
From: Dominik Csapak @ 2025-10-21 14:03 UTC (permalink / raw)
To: pdm-devel
if the pve/pbs wizard callbacks are None, don't show the menu
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/dashboard/mod.rs | 12 ++++++---
ui/src/dashboard/remote_panel.rs | 43 ++++++++++++++++++--------------
2 files changed, 32 insertions(+), 23 deletions(-)
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 2fe4d7fa..0441440a 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -423,10 +423,14 @@ impl Component for PdmDashboard {
.with_child(
create_remote_panel(
self.status.clone(),
- ctx.link()
- .callback(|_| Msg::CreateWizard(Some(RemoteType::Pve))),
- ctx.link()
- .callback(|_| Msg::CreateWizard(Some(RemoteType::Pbs))),
+ Some(
+ ctx.link()
+ .callback(|_| Msg::CreateWizard(Some(RemoteType::Pve))),
+ ),
+ Some(
+ ctx.link()
+ .callback(|_| Msg::CreateWizard(Some(RemoteType::Pbs))),
+ ),
)
.flex(1.0)
.width(300)
diff --git a/ui/src/dashboard/remote_panel.rs b/ui/src/dashboard/remote_panel.rs
index 27eebac2..747f9b8d 100644
--- a/ui/src/dashboard/remote_panel.rs
+++ b/ui/src/dashboard/remote_panel.rs
@@ -117,26 +117,31 @@ fn create_search_term(failure: bool) -> Search {
pub fn create_remote_panel(
status: Option<ResourcesStatus>,
- on_pve_wizard: impl IntoEventCallback<MenuEvent>,
- on_pbs_wizard: impl IntoEventCallback<MenuEvent>,
+ on_pve_wizard: Option<impl IntoEventCallback<MenuEvent>>,
+ on_pbs_wizard: Option<impl IntoEventCallback<MenuEvent>>,
) -> Panel {
- Panel::new()
+ let mut panel = Panel::new()
.title(create_title_with_icon("server", tr!("Remotes")))
.border(true)
- .with_tool(
- MenuButton::new(tr!("Add")).show_arrow(true).menu(
- Menu::new()
- .with_item(
- MenuItem::new("Proxmox VE")
- .icon_class("fa fa-building")
- .on_select(on_pve_wizard),
- )
- .with_item(
- MenuItem::new("Proxmox Backup Server")
- .icon_class("fa fa-floppy-o")
- .on_select(on_pbs_wizard),
- ),
- ),
- )
- .with_child(RemotePanel::new(status))
+ .with_child(RemotePanel::new(status));
+
+ if on_pve_wizard.is_some() || on_pbs_wizard.is_some() {
+ let mut menu = Menu::new();
+ if let Some(on_pve_wizard) = on_pve_wizard {
+ menu.add_item(
+ MenuItem::new("Proxmox VE")
+ .icon_class("fa fa-building")
+ .on_select(on_pve_wizard),
+ );
+ }
+ if let Some(on_pbs_wizard) = on_pbs_wizard {
+ menu.add_item(
+ MenuItem::new("Proxmox Backup Server")
+ .icon_class("fa fa-floppy-o")
+ .on_select(on_pbs_wizard),
+ );
+ }
+ panel.add_tool(MenuButton::new(tr!("Add")).show_arrow(true).menu(menu));
+ }
+ panel
}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pdm-devel] [PATCH datacenter-manager 05/15] ui: dashboard: refactor sdn panel creation into its own module
2025-10-21 14:03 [pdm-devel] [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
` (3 preceding siblings ...)
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 04/15] ui: dashboard: remote panel: make wizard menu optional Dominik Csapak
@ 2025-10-21 14:03 ` Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 06/15] ui: dashboard: refactor task summary panel creation to " Dominik Csapak
` (10 subsequent siblings)
15 siblings, 0 replies; 17+ messages in thread
From: Dominik Csapak @ 2025-10-21 14:03 UTC (permalink / raw)
To: pdm-devel
so we can more easily reuse that outside the dashboard struct.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/dashboard/mod.rs | 17 ++---------------
ui/src/dashboard/sdn_zone_panel.rs | 15 ++++++++++++---
2 files changed, 14 insertions(+), 18 deletions(-)
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 0441440a..fa0fe2fc 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -46,7 +46,7 @@ mod guest_panel;
pub use guest_panel::create_guest_panel;
mod sdn_zone_panel;
-use sdn_zone_panel::SdnZonePanel;
+use sdn_zone_panel::create_sdn_panel;
mod status_row;
use status_row::DashboardStatusRow;
@@ -145,19 +145,6 @@ pub struct PdmDashboard {
}
impl PdmDashboard {
- fn create_sdn_panel(&self) -> Panel {
- let sdn_zones_status = self.status.as_ref().map(|status| status.sdn_zones.clone());
-
- Panel::new()
- .flex(1.0)
- .width(200)
- .title(create_title_with_icon("sdn", tr!("SDN Zones")))
- .border(true)
- .with_child(SdnZonePanel::new(
- (!self.loading).then_some(sdn_zones_status).flatten(),
- ))
- }
-
fn create_task_summary_panel(
&self,
statistics: &StatisticsOptions,
@@ -556,7 +543,7 @@ impl Component for PdmDashboard {
.class(FlexWrap::Wrap)
.with_child(self.create_task_summary_panel(&self.statistics, None))
.with_child(self.create_task_summary_panel(&self.statistics, Some(5)))
- .with_child(self.create_sdn_panel()),
+ .with_child(create_sdn_panel(self.status.clone()).flex(1.0).width(200)),
);
Panel::new()
diff --git a/ui/src/dashboard/sdn_zone_panel.rs b/ui/src/dashboard/sdn_zone_panel.rs
index 0e26fa9c..611aadd1 100644
--- a/ui/src/dashboard/sdn_zone_panel.rs
+++ b/ui/src/dashboard/sdn_zone_panel.rs
@@ -1,18 +1,18 @@
use std::rc::Rc;
-use pdm_api_types::resource::{ResourceType, SdnStatus, SdnZoneCount};
+use pdm_api_types::resource::{ResourceType, ResourcesStatus, SdnStatus, SdnZoneCount};
use pdm_search::{Search, SearchTerm};
use pwt::{
css::{self, FontColor, TextAlign},
prelude::*,
- widget::{Container, Fa, List, ListTile},
+ widget::{Container, Fa, List, ListTile, Panel},
};
use yew::{
virtual_dom::{VComp, VNode},
Properties,
};
-use crate::search_provider::get_search_provider;
+use crate::{dashboard::create_title_with_icon, search_provider::get_search_provider};
use super::loading_column;
@@ -155,3 +155,12 @@ 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);
+
+ Panel::new()
+ .title(create_title_with_icon("sdn", tr!("SDN Zones")))
+ .border(true)
+ .with_child(SdnZonePanel::new(sdn_zones_status))
+}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pdm-devel] [PATCH datacenter-manager 06/15] ui: dashboard: refactor task summary panel creation to its own module
2025-10-21 14:03 [pdm-devel] [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
` (4 preceding siblings ...)
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 05/15] ui: dashboard: refactor sdn panel creation into its own module Dominik Csapak
@ 2025-10-21 14:03 ` Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 07/15] ui: dashboard: refactor subscription " Dominik Csapak
` (9 subsequent siblings)
15 siblings, 0 replies; 17+ messages in thread
From: Dominik Csapak @ 2025-10-21 14:03 UTC (permalink / raw)
To: pdm-devel
so we can easily reuse it outside the dashboard struct.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/dashboard/mod.rs | 65 +++++++++++++++------------------------
ui/src/dashboard/tasks.rs | 30 ++++++++++++++++++
2 files changed, 54 insertions(+), 41 deletions(-)
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index fa0fe2fc..c0b20b36 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -54,7 +54,7 @@ use status_row::DashboardStatusRow;
mod filtered_tasks;
mod tasks;
-use tasks::TaskSummary;
+use tasks::create_task_summary_panel;
/// The initial 'max-age' parameter in seconds. The backend polls every 15 minutes, so to increase
/// the chance of showing some data quickly use that as max age at the very first load.
@@ -145,44 +145,6 @@ pub struct PdmDashboard {
}
impl PdmDashboard {
- fn create_task_summary_panel(
- &self,
- statistics: &StatisticsOptions,
- remotes: Option<u32>,
- ) -> Panel {
- let (hours, since) = Self::get_task_options(&self.config);
- let title = match remotes {
- Some(_count) => tr!("Task Summary Sorted by Failed Tasks (Last {0}h)", hours),
- None => tr!("Task Summary by Category (Last {0}h)", hours),
- };
- Panel::new()
- .flex(1.0)
- .width(500)
- .border(true)
- .title(create_title_with_icon("list", title))
- .with_child(
- Container::new()
- .class(FlexFit)
- .padding(2)
- .with_optional_child(
- statistics
- .data
- .clone()
- .map(|data| TaskSummary::new(data, since, remotes)),
- )
- .with_optional_child(
- (statistics.error.is_none() && statistics.data.is_none())
- .then_some(loading_column()),
- )
- .with_optional_child(
- statistics
- .error
- .as_ref()
- .map(|err| error_message(&err.to_string())),
- ),
- )
- }
-
fn create_top_entities_panel(
&self,
icon: &str,
@@ -383,6 +345,7 @@ impl Component for PdmDashboard {
}
fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
+ let (hours, since) = Self::get_task_options(&self.config);
let content = Column::new()
.class(FlexFit)
.with_child(
@@ -541,8 +504,28 @@ impl Component for PdmDashboard {
.style("padding-top", "0")
.class(pwt::css::Flex::Fill)
.class(FlexWrap::Wrap)
- .with_child(self.create_task_summary_panel(&self.statistics, None))
- .with_child(self.create_task_summary_panel(&self.statistics, Some(5)))
+ .with_child(
+ create_task_summary_panel(
+ self.statistics.data.clone(),
+ self.statistics.error.as_ref(),
+ None,
+ hours,
+ since,
+ )
+ .flex(1.0)
+ .width(500),
+ )
+ .with_child(
+ create_task_summary_panel(
+ self.statistics.data.clone(),
+ self.statistics.error.as_ref(),
+ Some(5),
+ hours,
+ since,
+ )
+ .flex(1.0)
+ .width(500),
+ )
.with_child(create_sdn_panel(self.status.clone()).flex(1.0).width(200)),
);
diff --git a/ui/src/dashboard/tasks.rs b/ui/src/dashboard/tasks.rs
index d32cf378..cdb0d9c6 100644
--- a/ui/src/dashboard/tasks.rs
+++ b/ui/src/dashboard/tasks.rs
@@ -2,6 +2,7 @@ use std::collections::BTreeMap;
use std::collections::HashMap;
use std::rc::Rc;
+use anyhow::Error;
use yew::html::Scope;
use yew::virtual_dom::Key;
@@ -11,11 +12,15 @@ use pwt::prelude::*;
use pwt::props::ExtractPrimaryKey;
use pwt::state::Store;
use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader};
+use pwt::widget::error_message;
+use pwt::widget::Panel;
use pwt::widget::{ActionIcon, Container, Tooltip};
use pwt_macros::{builder, widget};
use pdm_api_types::TaskStatistics;
+use crate::dashboard::create_title_with_icon;
+use crate::dashboard::loading_column;
use crate::tasks::TaskWorkerType;
use super::filtered_tasks::FilteredTasks;
@@ -295,3 +300,28 @@ impl Component for ProxmoxTaskSummary {
.into()
}
}
+
+pub fn create_task_summary_panel(
+ statistics: Option<TaskStatistics>,
+ error: Option<&Error>,
+ remotes: Option<u32>,
+ hours: u32,
+ since: i64,
+) -> Panel {
+ let title = match remotes {
+ 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();
+ 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((loading).then_some(loading_column()))
+ .with_optional_child(error.map(|err| error_message(&err.to_string()))),
+ )
+}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pdm-devel] [PATCH datacenter-manager 07/15] ui: dashboard: refactor subscription panel creation to its own module
2025-10-21 14:03 [pdm-devel] [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
` (5 preceding siblings ...)
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 06/15] ui: dashboard: refactor task summary panel creation to " Dominik Csapak
@ 2025-10-21 14:03 ` Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 08/15] ui: dashboard: refactor top entities " Dominik Csapak
` (8 subsequent siblings)
15 siblings, 0 replies; 17+ messages in thread
From: Dominik Csapak @ 2025-10-21 14:03 UTC (permalink / raw)
To: pdm-devel
so we can more easily reuse it outside the dashboard struct.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/dashboard/mod.rs | 9 ++++-
ui/src/dashboard/subscription_info.rs | 54 +++++++++++++--------------
2 files changed, 34 insertions(+), 29 deletions(-)
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index c0b20b36..19a09a8c 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -34,7 +34,7 @@ mod top_entities;
pub use top_entities::TopEntities;
mod subscription_info;
-pub use subscription_info::SubscriptionInfo;
+pub use subscription_info::create_subscription_panel;
mod node_panel;
pub use node_panel::create_node_panel;
@@ -464,7 +464,12 @@ impl Component for PdmDashboard {
// )
// }),
//)
- .with_child(SubscriptionInfo::new()),
+ .with_child(
+ create_subscription_panel()
+ .flex(1.0)
+ .width(500)
+ .min_height(150),
+ ),
)
.with_child(
Container::new()
diff --git a/ui/src/dashboard/subscription_info.rs b/ui/src/dashboard/subscription_info.rs
index 08658d10..2a4d94b7 100644
--- a/ui/src/dashboard/subscription_info.rs
+++ b/ui/src/dashboard/subscription_info.rs
@@ -11,9 +11,7 @@ use proxmox_yew_comp::{http_get, Status};
use pwt::{
css::{AlignItems, FlexFit, JustifyContent, TextAlign},
prelude::tr,
- props::{
- ContainerBuilder, CssBorderBuilder, CssPaddingBuilder, WidgetBuilder, WidgetStyleBuilder,
- },
+ props::{ContainerBuilder, CssBorderBuilder, CssPaddingBuilder, WidgetBuilder},
widget::{Column, Container, Fa, Panel, Row},
AsyncPool,
};
@@ -133,31 +131,19 @@ impl Component for PdmSubscriptionInfo {
}
fn view(&self, _ctx: &yew::Context<Self>) -> yew::Html {
- let title: Html = Row::new()
+ Column::new()
+ .class(FlexFit)
+ .class(JustifyContent::Center)
.class(AlignItems::Center)
- .gap(2)
- .with_child(Fa::new("ticket"))
- .with_child(tr!("Subscription Status"))
- .into();
-
- Panel::new()
- .flex(1.0)
- .width(500)
- .min_height(150)
- .title(title)
- .border(true)
- .with_child(
- Column::new()
- .class(FlexFit)
- .class(JustifyContent::Center)
- .class(AlignItems::Center)
- .with_optional_child(
- self.loading
- .then_some(html! {<i class={"pwt-loading-icon"} />}),
- )
- .with_optional_child(
- (!self.loading).then_some(render_subscription_status(&self.status)),
- ),
+ .with_optional_child(
+ self.loading.then_some(
+ Container::new()
+ .padding(4)
+ .with_child(Container::from_tag("i").class("pwt-loading-icon")),
+ ),
+ )
+ .with_optional_child(
+ (!self.loading).then_some(render_subscription_status(&self.status)),
)
.into()
}
@@ -169,3 +155,17 @@ impl From<SubscriptionInfo> for VNode {
VNode::from(comp)
}
}
+
+pub fn create_subscription_panel() -> Panel {
+ let title: Html = Row::new()
+ .class(AlignItems::Center)
+ .gap(2)
+ .with_child(Fa::new("ticket"))
+ .with_child(tr!("Subscription Status"))
+ .into();
+
+ Panel::new()
+ .title(title)
+ .border(true)
+ .with_child(SubscriptionInfo::new())
+}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pdm-devel] [PATCH datacenter-manager 08/15] ui: dashboard: refactor top entities panel creation to its own module
2025-10-21 14:03 [pdm-devel] [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
` (6 preceding siblings ...)
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 07/15] ui: dashboard: refactor subscription " Dominik Csapak
@ 2025-10-21 14:03 ` Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 09/15] ui: dashboard: refactor DashboardConfig editing/constants to their module Dominik Csapak
` (7 subsequent siblings)
15 siblings, 0 replies; 17+ messages in thread
From: Dominik Csapak @ 2025-10-21 14:03 UTC (permalink / raw)
To: pdm-devel
so we can more easily reuse it outside the dashboard struct. Introduce
the 'LeaderboardType' enum to differentiate between the various types.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/dashboard/mod.rs | 83 +++++++++++++-------------------
ui/src/dashboard/top_entities.rs | 45 +++++++++++++++--
ui/src/dashboard/types.rs | 9 ++++
3 files changed, 82 insertions(+), 55 deletions(-)
create mode 100644 ui/src/dashboard/types.rs
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 19a09a8c..bb6b4049 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -17,7 +17,6 @@ use pwt::{
props::StorageLocation,
state::PersistentState,
widget::{
- error_message,
form::{DisplayField, FormContext, Number},
Column, Container, Fa, InputPanel, Panel, Row,
},
@@ -25,13 +24,12 @@ use pwt::{
};
use pdm_api_types::{remotes::RemoteType, resource::ResourcesStatus, TaskStatistics};
-use pdm_client::types::TopEntity;
use proxmox_client::ApiResponseData;
use crate::{pve::GuestType, remotes::AddWizard, RemoteList};
mod top_entities;
-pub use top_entities::TopEntities;
+pub use top_entities::create_top_entities_panel;
mod subscription_info;
pub use subscription_info::create_subscription_panel;
@@ -56,6 +54,8 @@ mod filtered_tasks;
mod tasks;
use tasks::create_task_summary_panel;
+pub mod types;
+
/// The initial 'max-age' parameter in seconds. The backend polls every 15 minutes, so to increase
/// the chance of showing some data quickly use that as max age at the very first load.
pub const INITIAL_MAX_AGE_S: u64 = 900;
@@ -145,32 +145,6 @@ pub struct PdmDashboard {
}
impl PdmDashboard {
- fn create_top_entities_panel(
- &self,
- icon: &str,
- title: String,
- metrics_title: String,
- entities: Option<&Vec<TopEntity>>,
- threshold: f64,
- ) -> Panel {
- Panel::new()
- .flex(1.0)
- .width(500)
- .min_width(400)
- .border(true)
- .title(create_title_with_icon(icon, title))
- .with_optional_child(
- entities
- .map(|entities| TopEntities::new(entities.clone(), metrics_title, threshold)),
- )
- .with_optional_child(self.top_entities.is_none().then_some(loading_column()))
- .with_optional_child(
- self.last_top_entities_error
- .as_ref()
- .map(|err| error_message(&err.to_string())),
- )
- }
-
fn reload(&mut self, ctx: &yew::Context<Self>) {
let max_age = if self.loaded_once {
self.config.max_age.unwrap_or(DEFAULT_MAX_AGE_S)
@@ -479,27 +453,36 @@ impl Component for PdmDashboard {
.padding_top(0)
.class(FlexWrap::Wrap)
//.min_height(175)
- .with_child(self.create_top_entities_panel(
- "desktop",
- tr!("Guests With the Highest CPU Usage"),
- tr!("CPU usage"),
- self.top_entities.as_ref().map(|e| &e.guest_cpu),
- 0.85,
- ))
- .with_child(self.create_top_entities_panel(
- "building",
- tr!("Nodes With the Highest CPU Usage"),
- tr!("CPU usage"),
- self.top_entities.as_ref().map(|e| &e.node_cpu),
- 0.85,
- ))
- .with_child(self.create_top_entities_panel(
- "building",
- tr!("Nodes With the Highest Memory Usage"),
- tr!("Memory usage"),
- self.top_entities.as_ref().map(|e| &e.node_memory),
- 0.95,
- )),
+ .with_child(
+ create_top_entities_panel(
+ self.top_entities.as_ref().map(|e| e.guest_cpu.clone()),
+ self.last_top_entities_error.as_ref(),
+ types::LeaderboardType::GuestCpu,
+ )
+ .flex(1.0)
+ .width(500)
+ .min_width(400),
+ )
+ .with_child(
+ create_top_entities_panel(
+ self.top_entities.as_ref().map(|e| e.node_cpu.clone()),
+ self.last_top_entities_error.as_ref(),
+ types::LeaderboardType::NodeCpu,
+ )
+ .flex(1.0)
+ .width(500)
+ .min_width(400),
+ )
+ .with_child(
+ create_top_entities_panel(
+ self.top_entities.as_ref().map(|e| e.node_memory.clone()),
+ self.last_top_entities_error.as_ref(),
+ types::LeaderboardType::NodeCpu,
+ )
+ .flex(1.0)
+ .width(500)
+ .min_width(400),
+ ),
)
.with_child(
Container::new()
diff --git a/ui/src/dashboard/top_entities.rs b/ui/src/dashboard/top_entities.rs
index c93ee252..dfe38692 100644
--- a/ui/src/dashboard/top_entities.rs
+++ b/ui/src/dashboard/top_entities.rs
@@ -1,12 +1,10 @@
use std::rc::Rc;
use web_sys::HtmlElement;
-use yew::{
- virtual_dom::{VComp, VNode},
- Component, NodeRef, PointerEvent, Properties, TargetCast,
-};
+use yew::virtual_dom::{VComp, VNode};
use proxmox_yew_comp::utils::render_epoch;
+use pwt::prelude::*;
use pwt::{
css::{AlignItems, Display, FlexFit, JustifyContent},
dom::align::{align_to, AlignOptions},
@@ -15,12 +13,13 @@ use pwt::{
WidgetStyleBuilder,
},
tr,
- widget::{ActionIcon, Column, Container, Row},
+ widget::{error_message, ActionIcon, Column, Container, Panel, Row},
};
use pdm_client::types::{Resource, TopEntity};
use crate::{
+ dashboard::{create_title_with_icon, loading_column, types::LeaderboardType},
get_deep_url, get_resource_node, navigate_to,
renderer::{render_resource_icon, render_resource_name},
};
@@ -326,3 +325,39 @@ 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>,
+ leaderboard_type: LeaderboardType,
+) -> Panel {
+ let (icon, title, metrics_title, threshold) = match leaderboard_type {
+ LeaderboardType::GuestCpu => (
+ "desktop",
+ tr!("Guests With the Highest CPU Usage"),
+ tr!("CPU usage"),
+ 0.85,
+ ),
+ LeaderboardType::NodeCpu => (
+ "building",
+ tr!("Nodes With the Highest CPU Usage"),
+ tr!("CPU usage"),
+ 0.85,
+ ),
+ LeaderboardType::NodeMemory => (
+ "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())))
+}
diff --git a/ui/src/dashboard/types.rs b/ui/src/dashboard/types.rs
new file mode 100644
index 00000000..152d4f57
--- /dev/null
+++ b/ui/src/dashboard/types.rs
@@ -0,0 +1,9 @@
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, PartialEq, Clone, Copy)]
+#[serde(rename_all = "kebab-case")]
+pub enum LeaderboardType {
+ GuestCpu,
+ NodeCpu,
+ NodeMemory,
+}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pdm-devel] [PATCH datacenter-manager 09/15] ui: dashboard: refactor DashboardConfig editing/constants to their module
2025-10-21 14:03 [pdm-devel] [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
` (7 preceding siblings ...)
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 08/15] ui: dashboard: refactor top entities " Dominik Csapak
@ 2025-10-21 14:03 ` Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 10/15] ui: dashboard: factor out task parameter calculation Dominik Csapak
` (6 subsequent siblings)
15 siblings, 0 replies; 17+ messages in thread
From: Dominik Csapak @ 2025-10-21 14:03 UTC (permalink / raw)
To: pdm-devel
makes it easier to use outside the dashboard struct. Since it's not
really a 'dashboard configuration' but rather 'refresh configuration',
rename it as such.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/dashboard/mod.rs | 129 +++++-------------------
ui/src/dashboard/refresh_config_edit.rs | 107 ++++++++++++++++++++
2 files changed, 130 insertions(+), 106 deletions(-)
create mode 100644 ui/src/dashboard/refresh_config_edit.rs
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index bb6b4049..fc2e150a 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -1,30 +1,25 @@
-use std::{collections::HashMap, rc::Rc};
+use std::rc::Rc;
use anyhow::Error;
use futures::join;
use js_sys::Date;
-use serde::{Deserialize, Serialize};
use serde_json::json;
use yew::{
virtual_dom::{VComp, VNode},
Component,
};
-use proxmox_yew_comp::{http_get, EditWindow};
+use proxmox_yew_comp::http_get;
use pwt::{
css::{AlignItems, FlexDirection, FlexFit, FlexWrap, JustifyContent},
prelude::*,
props::StorageLocation,
state::PersistentState,
- widget::{
- form::{DisplayField, FormContext, Number},
- Column, Container, Fa, InputPanel, Panel, Row,
- },
+ widget::{form::FormContext, Column, Container, Fa, Panel, Row},
AsyncPool,
};
use pdm_api_types::{remotes::RemoteType, resource::ResourcesStatus, TaskStatistics};
-use proxmox_client::ApiResponseData;
use crate::{pve::GuestType, remotes::AddWizard, RemoteList};
@@ -56,28 +51,15 @@ use tasks::create_task_summary_panel;
pub mod types;
-/// The initial 'max-age' parameter in seconds. The backend polls every 15 minutes, so to increase
-/// the chance of showing some data quickly use that as max age at the very first load.
-pub const INITIAL_MAX_AGE_S: u64 = 900;
-/// The 'max-age' parameter in seconds for when user forces a reload. Do not use 0 as the data will
-/// never be realtime anyway, with 5s we get very current data while avoiding that one or more
-/// "fidgety" users put unbounded load onto the remotes.
-pub const FORCE_RELOAD_MAX_AGE_S: u64 = 3;
-
-/// The default 'max-age' parameter in seconds. The backend polls every 15 minutes, but if a user
-/// has the dashboard active for a longer time it's beneficial to refresh a bit more often, forcing
-/// new data twice a minute is a good compromise.
-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: 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.
-pub const DEFAULT_TASK_SUMMARY_HOURS: u32 = 48;
+mod refresh_config_edit;
+pub use refresh_config_edit::{
+ create_refresh_config_edit_window, refresh_config_id, RefreshConfig,
+};
+use refresh_config_edit::{
+ DEFAULT_MAX_AGE_S, DEFAULT_REFRESH_INTERVAL_S, DEFAULT_TASK_SUMMARY_HOURS,
+ FORCE_RELOAD_MAX_AGE_S, INITIAL_MAX_AGE_S,
+};
#[derive(Properties, PartialEq)]
pub struct Dashboard {}
@@ -94,17 +76,6 @@ impl Default for Dashboard {
}
}
-#[derive(Serialize, Deserialize, Default, Debug)]
-#[serde(rename_all = "kebab-case")]
-pub struct DashboardConfig {
- #[serde(skip_serializing_if = "Option::is_none")]
- refresh_interval: Option<u32>,
- #[serde(skip_serializing_if = "Option::is_none")]
- max_age: Option<u64>,
- #[serde(skip_serializing_if = "Option::is_none")]
- task_last_hours: Option<u32>,
-}
-
pub enum LoadingResult {
Resources(Result<ResourcesStatus, Error>),
TopEntities(Result<pdm_client::types::TopEntities, proxmox_client::Error>),
@@ -118,7 +89,7 @@ pub enum Msg {
CreateWizard(Option<RemoteType>),
Reload,
ForceReload,
- UpdateConfig(DashboardConfig),
+ UpdateConfig(RefreshConfig),
ConfigWindow(bool),
}
@@ -141,7 +112,7 @@ pub struct PdmDashboard {
show_config_window: bool,
_context_listener: ContextHandle<RemoteList>,
async_pool: AsyncPool,
- config: PersistentState<DashboardConfig>,
+ config: PersistentState<RefreshConfig>,
}
impl PdmDashboard {
@@ -197,7 +168,7 @@ impl PdmDashboard {
});
}
- fn get_task_options(config: &PersistentState<DashboardConfig>) -> (u32, i64) {
+ fn get_task_options(config: &PersistentState<RefreshConfig>) -> (u32, i64) {
let hours = config.task_last_hours.unwrap_or(DEFAULT_TASK_SUMMARY_HOURS);
let since = (Date::now() / 1000.0) as i64 - (hours * 60 * 60) as i64;
(hours, since)
@@ -209,8 +180,8 @@ impl Component for PdmDashboard {
type Properties = Dashboard;
fn create(ctx: &yew::Context<Self>) -> Self {
- let config: PersistentState<DashboardConfig> =
- PersistentState::new(StorageLocation::local("dashboard-config"));
+ let config: PersistentState<RefreshConfig> =
+ PersistentState::new(StorageLocation::local(refresh_config_id("dashboard")));
let async_pool = AsyncPool::new();
let (remote_list, _context_listener) = ctx
@@ -520,75 +491,21 @@ impl Component for PdmDashboard {
Panel::new()
.class(FlexFit)
.with_child(content)
- .with_optional_child(
- self.show_wizard.map(|remote_type| {
- AddWizard::new(remote_type)
- .on_close(ctx.link().callback(|_| Msg::CreateWizard(None)))
- .on_submit(move |ctx| {
- crate::remotes::create_remote(ctx, remote_type)
- })
- }),
- )
+ .with_optional_child(self.show_wizard.map(|remote_type| {
+ AddWizard::new(remote_type)
+ .on_close(ctx.link().callback(|_| Msg::CreateWizard(None)))
+ .on_submit(move |ctx| crate::remotes::create_remote(ctx, remote_type))
+ }))
.with_optional_child(
self.show_config_window.then_some(
- EditWindow::new(tr!("Dashboard Configuration"))
- .submit_text(tr!("Save"))
- .loader({
- || {
- let data: PersistentState<DashboardConfig> = PersistentState::new(
- StorageLocation::local("dashboard-config"),
- );
-
- async move {
- let data = serde_json::to_value(data.into_inner())?;
- Ok(ApiResponseData {
- attribs: HashMap::new(),
- data,
- })
- }
- }
- })
- .renderer(|_ctx: &FormContext| {
- InputPanel::new()
- .width(600)
- .padding(2)
- .with_field(
- tr!("Refresh Interval (seconds)"),
- Number::new()
- .name("refresh-interval")
- .min(5u64)
- .step(5)
- .placeholder(DEFAULT_REFRESH_INTERVAL_S.to_string()),
- )
- .with_field(
- tr!("Max Age (seconds)"),
- Number::new()
- .name("max-age")
- .min(0u64)
- .step(5)
- .placeholder(DEFAULT_MAX_AGE_S.to_string()),
- )
- .with_field(
- "",
- DisplayField::new()
- .key("max-age-explanation")
- .value(tr!("If a response from a remote is older than 'Max Age', it will be updated on the next refresh.")))
- .with_field(
- tr!("Task Summary Time Range (last hours)"),
- Number::new()
- .name("task-last-hours")
- .min(0u64)
- .placeholder(DEFAULT_TASK_SUMMARY_HOURS.to_string()),
- )
- .into()
- })
+ create_refresh_config_edit_window("dashboard")
.on_close(ctx.link().callback(|_| Msg::ConfigWindow(false)))
.on_submit({
let link = ctx.link().clone();
move |ctx: FormContext| {
let link = link.clone();
async move {
- let data: DashboardConfig =
+ let data: RefreshConfig =
serde_json::from_value(ctx.get_submit_data())?;
link.send_message(Msg::UpdateConfig(data));
Ok(())
diff --git a/ui/src/dashboard/refresh_config_edit.rs b/ui/src/dashboard/refresh_config_edit.rs
new file mode 100644
index 00000000..9bd0c884
--- /dev/null
+++ b/ui/src/dashboard/refresh_config_edit.rs
@@ -0,0 +1,107 @@
+use std::collections::HashMap;
+
+use serde::{Deserialize, Serialize};
+
+use pwt::prelude::*;
+use pwt::props::StorageLocation;
+use pwt::state::PersistentState;
+use pwt::widget::form::{DisplayField, FormContext, Number};
+use pwt::widget::InputPanel;
+
+use proxmox_client::ApiResponseData;
+use proxmox_yew_comp::EditWindow;
+
+/// The initial 'max-age' parameter in seconds. The backend polls every 15 minutes, so to increase
+/// the chance of showing some data quickly use that as max age at the very first load.
+pub const INITIAL_MAX_AGE_S: u64 = 900;
+
+/// The 'max-age' parameter in seconds for when user forces a reload. Do not use 0 as the data will
+/// never be realtime anyway, with 5s we get very current data while avoiding that one or more
+/// "fidgety" users put unbounded load onto the remotes.
+pub const FORCE_RELOAD_MAX_AGE_S: u64 = 3;
+
+/// The default 'max-age' parameter in seconds. The backend polls every 15 minutes, but if a user
+/// has the dashboard active for a longer time it's beneficial to refresh a bit more often, forcing
+/// new data twice a minute is a good compromise.
+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: 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.
+pub const DEFAULT_TASK_SUMMARY_HOURS: u32 = 48;
+
+#[derive(Serialize, Deserialize, Default, Debug)]
+#[serde(rename_all = "kebab-case")]
+pub struct RefreshConfig {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub refresh_interval: Option<u32>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub max_age: Option<u64>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub task_last_hours: Option<u32>,
+}
+
+/// Get a consistent id for use in a local storage
+pub fn refresh_config_id(id: &str) -> String {
+ format!("view-{id}-config")
+}
+
+pub fn create_refresh_config_edit_window(id: &str) -> EditWindow {
+ let id = refresh_config_id(id);
+ EditWindow::new(tr!("Refresh Configuration"))
+ .submit_text(tr!("Save"))
+ .loader({
+ move || {
+ let id = id.clone();
+ let data: PersistentState<RefreshConfig> = PersistentState::new(
+ StorageLocation::local(id),
+ );
+
+ async move {
+ let data = serde_json::to_value(data.into_inner())?;
+ Ok(ApiResponseData {
+ attribs: HashMap::new(),
+ data,
+ })
+ }
+ }
+ })
+ .renderer(|_ctx: &FormContext| {
+ InputPanel::new()
+ .width(600)
+ .padding(2)
+ .with_field(
+ tr!("Refresh Interval (seconds)"),
+ Number::new()
+ .name("refresh-interval")
+ .min(5u64)
+ .step(5)
+ .placeholder(DEFAULT_REFRESH_INTERVAL_S.to_string()),
+ )
+ .with_field(
+ tr!("Max Age (seconds)"),
+ Number::new()
+ .name("max-age")
+ .min(0u64)
+ .step(5)
+ .placeholder(DEFAULT_MAX_AGE_S.to_string()),
+ )
+ .with_field(
+ "",
+ DisplayField::new()
+ .key("max-age-explanation")
+ .value(tr!("If a response from a remote is older than 'Max Age', it will be updated on the next refresh.")))
+ .with_field(
+ tr!("Task Summary Time Range (last hours)"),
+ Number::new()
+ .name("task-last-hours")
+ .min(0u64)
+ .placeholder(DEFAULT_TASK_SUMMARY_HOURS.to_string()),
+ )
+ .into()
+ })
+}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pdm-devel] [PATCH datacenter-manager 10/15] ui: dashboard: factor out task parameter calculation
2025-10-21 14:03 [pdm-devel] [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
` (8 preceding siblings ...)
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 09/15] ui: dashboard: refactor DashboardConfig editing/constants to their module Dominik Csapak
@ 2025-10-21 14:03 ` Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 11/15] ui: dashboard: remove unused remote list Dominik Csapak
` (5 subsequent siblings)
15 siblings, 0 replies; 17+ messages in thread
From: Dominik Csapak @ 2025-10-21 14:03 UTC (permalink / raw)
To: pdm-devel
into the dashboard::tasks module.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/dashboard/mod.rs | 19 ++++++-------------
ui/src/dashboard/tasks.rs | 8 ++++++++
2 files changed, 14 insertions(+), 13 deletions(-)
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index fc2e150a..06283500 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -47,7 +47,7 @@ use status_row::DashboardStatusRow;
mod filtered_tasks;
mod tasks;
-use tasks::create_task_summary_panel;
+use tasks::{create_task_summary_panel, get_task_options};
pub mod types;
@@ -57,8 +57,7 @@ pub use refresh_config_edit::{
create_refresh_config_edit_window, refresh_config_id, RefreshConfig,
};
use refresh_config_edit::{
- DEFAULT_MAX_AGE_S, DEFAULT_REFRESH_INTERVAL_S, DEFAULT_TASK_SUMMARY_HOURS,
- FORCE_RELOAD_MAX_AGE_S, INITIAL_MAX_AGE_S,
+ DEFAULT_MAX_AGE_S, DEFAULT_REFRESH_INTERVAL_S, FORCE_RELOAD_MAX_AGE_S, INITIAL_MAX_AGE_S,
};
#[derive(Properties, PartialEq)]
@@ -127,7 +126,7 @@ impl PdmDashboard {
fn do_reload(&mut self, ctx: &yew::Context<Self>, max_age: u64) {
let link = ctx.link().clone();
- let (_, since) = Self::get_task_options(&self.config);
+ let (_, since) = get_task_options(self.config.task_last_hours);
self.load_finished_time = None;
self.async_pool.spawn(async move {
@@ -167,12 +166,6 @@ impl PdmDashboard {
link.send_message(Msg::LoadingFinished(LoadingResult::All));
});
}
-
- fn get_task_options(config: &PersistentState<RefreshConfig>) -> (u32, i64) {
- let hours = config.task_last_hours.unwrap_or(DEFAULT_TASK_SUMMARY_HOURS);
- let since = (Date::now() / 1000.0) as i64 - (hours * 60 * 60) as i64;
- (hours, since)
- }
}
impl Component for PdmDashboard {
@@ -275,9 +268,9 @@ impl Component for PdmDashboard {
true
}
Msg::UpdateConfig(dashboard_config) => {
- let (old_hours, _) = Self::get_task_options(&self.config);
+ let (old_hours, _) = get_task_options(self.config.task_last_hours);
self.config.update(dashboard_config);
- let (new_hours, _) = Self::get_task_options(&self.config);
+ let (new_hours, _) = get_task_options(self.config.task_last_hours);
if old_hours != new_hours {
self.reload(ctx);
@@ -290,7 +283,7 @@ impl Component for PdmDashboard {
}
fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
- let (hours, since) = Self::get_task_options(&self.config);
+ let (hours, since) = get_task_options(self.config.task_last_hours);
let content = Column::new()
.class(FlexFit)
.with_child(
diff --git a/ui/src/dashboard/tasks.rs b/ui/src/dashboard/tasks.rs
index cdb0d9c6..9a009c4d 100644
--- a/ui/src/dashboard/tasks.rs
+++ b/ui/src/dashboard/tasks.rs
@@ -3,6 +3,7 @@ use std::collections::HashMap;
use std::rc::Rc;
use anyhow::Error;
+use js_sys::Date;
use yew::html::Scope;
use yew::virtual_dom::Key;
@@ -21,6 +22,7 @@ use pdm_api_types::TaskStatistics;
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 super::filtered_tasks::FilteredTasks;
@@ -325,3 +327,9 @@ pub fn create_task_summary_panel(
.with_optional_child(error.map(|err| error_message(&err.to_string()))),
)
}
+
+pub fn get_task_options(last_hours: Option<u32>) -> (u32, i64) {
+ let hours = last_hours.unwrap_or(DEFAULT_TASK_SUMMARY_HOURS);
+ let since = (Date::now() / 1000.0) as i64 - (hours * 60 * 60) as i64;
+ (hours, since)
+}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pdm-devel] [PATCH datacenter-manager 11/15] ui: dashboard: remove unused remote list
2025-10-21 14:03 [pdm-devel] [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
` (9 preceding siblings ...)
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 10/15] ui: dashboard: factor out task parameter calculation Dominik Csapak
@ 2025-10-21 14:03 ` Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 12/15] ui: dashboard: status row: make loading less jarring Dominik Csapak
` (4 subsequent siblings)
15 siblings, 0 replies; 17+ messages in thread
From: Dominik Csapak @ 2025-10-21 14:03 UTC (permalink / raw)
To: pdm-devel
this was introduced in commit
9f2256c7 (ui: dashboard: make better use of the status api call)
but never actually used, so simply remove them and the corresponding
_context_listener.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/dashboard/mod.rs | 17 +----------------
1 file changed, 1 insertion(+), 16 deletions(-)
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 06283500..e4115c6d 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -21,7 +21,7 @@ use pwt::{
use pdm_api_types::{remotes::RemoteType, resource::ResourcesStatus, TaskStatistics};
-use crate::{pve::GuestType, remotes::AddWizard, RemoteList};
+use crate::{pve::GuestType, remotes::AddWizard};
mod top_entities;
pub use top_entities::create_top_entities_panel;
@@ -84,7 +84,6 @@ pub enum LoadingResult {
pub enum Msg {
LoadingFinished(LoadingResult),
- RemoteListChanged(RemoteList),
CreateWizard(Option<RemoteType>),
Reload,
ForceReload,
@@ -106,10 +105,8 @@ pub struct PdmDashboard {
loaded_once: bool,
loading: bool,
load_finished_time: Option<f64>,
- remote_list: RemoteList,
show_wizard: Option<RemoteType>,
show_config_window: bool,
- _context_listener: ContextHandle<RemoteList>,
async_pool: AsyncPool,
config: PersistentState<RefreshConfig>,
}
@@ -177,11 +174,6 @@ impl Component for PdmDashboard {
PersistentState::new(StorageLocation::local(refresh_config_id("dashboard")));
let async_pool = AsyncPool::new();
- let (remote_list, _context_listener) = ctx
- .link()
- .context(ctx.link().callback(Msg::RemoteListChanged))
- .expect("No Remote list context provided");
-
let mut this = Self {
status: None,
last_error: None,
@@ -194,10 +186,8 @@ impl Component for PdmDashboard {
loaded_once: false,
loading: true,
load_finished_time: None,
- remote_list,
show_wizard: None,
show_config_window: false,
- _context_listener,
async_pool,
config,
};
@@ -246,11 +236,6 @@ impl Component for PdmDashboard {
}
true
}
- Msg::RemoteListChanged(remote_list) => {
- let changed = self.remote_list != remote_list;
- self.remote_list = remote_list;
- changed
- }
Msg::CreateWizard(remote_type) => {
self.show_wizard = remote_type;
true
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pdm-devel] [PATCH datacenter-manager 12/15] ui: dashboard: status row: make loading less jarring
2025-10-21 14:03 [pdm-devel] [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
` (10 preceding siblings ...)
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 11/15] ui: dashboard: remove unused remote list Dominik Csapak
@ 2025-10-21 14:03 ` Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 13/15] ui: introduce `LoadResult` helper type Dominik Csapak
` (3 subsequent siblings)
15 siblings, 0 replies; 17+ messages in thread
From: Dominik Csapak @ 2025-10-21 14:03 UTC (permalink / raw)
To: pdm-devel
by tracking the loading state not only via the last_refresh property,
but also from a reload message to a changing last_refresh value.
This means it now always shows the last refresh time if there was one
and only rotates the icon in that case.
This is much less jarring that a split second change of the text from
a date to 'now refreshing' back to a finished date.
While at it, remove the unused 'loading' property of the PdmDashboard
struct.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/dashboard/mod.rs | 11 ++---------
ui/src/dashboard/status_row.rs | 11 +++++++++--
2 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index e4115c6d..a394ea81 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -102,8 +102,6 @@ pub struct PdmDashboard {
top_entities: Option<pdm_client::types::TopEntities>,
last_top_entities_error: Option<proxmox_client::Error>,
statistics: StatisticsOptions,
- loaded_once: bool,
- loading: bool,
load_finished_time: Option<f64>,
show_wizard: Option<RemoteType>,
show_config_window: bool,
@@ -113,7 +111,7 @@ pub struct PdmDashboard {
impl PdmDashboard {
fn reload(&mut self, ctx: &yew::Context<Self>) {
- let max_age = if self.loaded_once {
+ let max_age = if self.load_finished_time.is_some() {
self.config.max_age.unwrap_or(DEFAULT_MAX_AGE_S)
} else {
INITIAL_MAX_AGE_S
@@ -125,7 +123,6 @@ impl PdmDashboard {
let link = ctx.link().clone();
let (_, since) = get_task_options(self.config.task_last_hours);
- self.load_finished_time = None;
self.async_pool.spawn(async move {
let client = crate::pdm_client();
@@ -183,8 +180,6 @@ impl Component for PdmDashboard {
data: None,
error: None,
},
- loaded_once: false,
- loading: true,
load_finished_time: None,
show_wizard: None,
show_config_window: false,
@@ -224,9 +219,7 @@ impl Component for PdmDashboard {
Err(err) => self.statistics.error = Some(err),
},
LoadingResult::All => {
- self.loading = false;
- if !self.loaded_once {
- self.loaded_once = true;
+ if self.load_finished_time.is_none() {
// 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);
diff --git a/ui/src/dashboard/status_row.rs b/ui/src/dashboard/status_row.rs
index 5e377411..0855b123 100644
--- a/ui/src/dashboard/status_row.rs
+++ b/ui/src/dashboard/status_row.rs
@@ -45,6 +45,7 @@ pub enum Msg {
#[doc(hidden)]
pub struct PdmDashboardStatusRow {
_interval: Interval,
+ loading: bool,
}
impl PdmDashboardStatusRow {
@@ -68,6 +69,7 @@ impl Component for PdmDashboardStatusRow {
fn create(ctx: &yew::Context<Self>) -> Self {
Self {
_interval: Self::create_interval(ctx),
+ loading: false,
}
}
@@ -76,19 +78,24 @@ impl Component for PdmDashboardStatusRow {
match msg {
Msg::Reload(clicked) => {
props.on_reload.emit(clicked);
+ self.loading = true;
true
}
}
}
- fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
+ fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
self._interval = Self::create_interval(ctx);
+ let new_refresh = ctx.props().last_refresh;
+ if new_refresh.is_some() && old_props.last_refresh != new_refresh {
+ self.loading = false;
+ }
true
}
fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
let props = ctx.props();
- let is_loading = props.last_refresh.is_none();
+ let is_loading = props.last_refresh.is_none() || self.loading;
let on_settings_click = props.on_settings_click.clone();
Row::new()
.gap(1)
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pdm-devel] [PATCH datacenter-manager 13/15] ui: introduce `LoadResult` helper type
2025-10-21 14:03 [pdm-devel] [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
` (11 preceding siblings ...)
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 12/15] ui: dashboard: status row: make loading less jarring Dominik Csapak
@ 2025-10-21 14:03 ` Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 14/15] ui: dashboard: implement 'View' Dominik Csapak
` (2 subsequent siblings)
15 siblings, 0 replies; 17+ messages in thread
From: Dominik Csapak @ 2025-10-21 14:03 UTC (permalink / raw)
To: pdm-devel
this factors out some common pattern when loading data, such as saving
the last valid data even when an error occurs, and a check if anything
has been set yet.
This saves a few lines when we use it vs duplicating that pattern
everywhere.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/lib.rs | 3 +++
ui/src/load_result.rs | 42 +++++++++++++++++++++++++++++++++++++
ui/src/pbs/remote.rs | 30 +++++++++-----------------
ui/src/pve/lxc.rs | 28 +++++++------------------
ui/src/pve/node/overview.rs | 29 +++++++++----------------
ui/src/pve/qemu.rs | 28 +++++++------------------
ui/src/pve/storage.rs | 29 +++++++------------------
7 files changed, 89 insertions(+), 100 deletions(-)
create mode 100644 ui/src/load_result.rs
diff --git a/ui/src/lib.rs b/ui/src/lib.rs
index a2b79b05..de76e1c0 100644
--- a/ui/src/lib.rs
+++ b/ui/src/lib.rs
@@ -39,6 +39,9 @@ pub mod sdn;
pub mod renderer;
+mod load_result;
+pub use load_result::LoadResult;
+
mod tasks;
pub use tasks::register_pve_tasks;
diff --git a/ui/src/load_result.rs b/ui/src/load_result.rs
new file mode 100644
index 00000000..4f3e6d5a
--- /dev/null
+++ b/ui/src/load_result.rs
@@ -0,0 +1,42 @@
+/// Helper wrapper to factor out some common api loading behavior
+pub struct LoadResult<T, E> {
+ pub data: Option<T>,
+ pub error: Option<E>,
+}
+
+impl<T, E> LoadResult<T, E> {
+ /// Creates a new empty result that contains no data or error.
+ pub fn new() -> Self {
+ Self {
+ data: None,
+ error: None,
+ }
+ }
+
+ /// Update the current value with the given result
+ ///
+ /// On `Ok`, the previous error will be deleted.
+ /// On `Err`, the previous valid date is kept.
+ pub fn update(&mut self, result: Result<T, E>) {
+ match result {
+ Ok(data) => {
+ self.error = None;
+ self.data = Some(data);
+ }
+ Err(err) => {
+ self.error = Some(err);
+ }
+ }
+ }
+
+ /// If any of data or err has any value
+ pub fn has_data(&self) -> bool {
+ self.data.is_some() || self.error.is_some()
+ }
+}
+
+impl<T, E> Default for LoadResult<T, E> {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/ui/src/pbs/remote.rs b/ui/src/pbs/remote.rs
index 7cf7c7e2..67c9172f 100644
--- a/ui/src/pbs/remote.rs
+++ b/ui/src/pbs/remote.rs
@@ -17,7 +17,7 @@ use pwt::{
use pbs_api_types::NodeStatus;
use pdm_api_types::rrddata::PbsNodeDataPoint;
-use crate::renderer::separator;
+use crate::{renderer::separator, LoadResult};
#[derive(Clone, Debug, Eq, PartialEq, Properties)]
pub struct RemoteOverviewPanel {
@@ -59,12 +59,11 @@ pub struct RemoteOverviewPanelComp {
load_data: Rc<Series>,
mem_data: Rc<Series>,
mem_total_data: Rc<Series>,
- status: Option<NodeStatus>,
+ status: LoadResult<NodeStatus, proxmox_client::Error>,
rrd_time_frame: RRDTimeframe,
last_error: Option<proxmox_client::Error>,
- last_status_error: Option<proxmox_client::Error>,
async_pool: AsyncPool,
_timeout: Option<gloo_timers::callback::Timeout>,
@@ -100,9 +99,8 @@ impl yew::Component for RemoteOverviewPanelComp {
mem_data: Rc::new(Series::new("", Vec::new())),
mem_total_data: Rc::new(Series::new("", Vec::new())),
rrd_time_frame: RRDTimeframe::load(),
- status: None,
+ status: LoadResult::new(),
last_error: None,
- last_status_error: None,
async_pool: AsyncPool::new(),
_timeout: None,
_status_timeout: None,
@@ -160,15 +158,7 @@ impl yew::Component for RemoteOverviewPanelComp {
Err(err) => self.last_error = Some(err),
},
Msg::StatusLoadFinished(res) => {
- match res {
- Ok(status) => {
- self.last_status_error = None;
- self.status = Some(status);
- }
- Err(err) => {
- self.last_status_error = Some(err);
- }
- }
+ self.status.update(res);
let link = ctx.link().clone();
self._status_timeout = Some(gloo_timers::callback::Timeout::new(
ctx.props().status_interval,
@@ -188,7 +178,7 @@ impl yew::Component for RemoteOverviewPanelComp {
let props = ctx.props();
if props.remote != old_props.remote {
- self.status = None;
+ self.status = LoadResult::new();
self.last_error = None;
self.time_data = Rc::new(Vec::new());
self.cpu_data = Rc::new(Series::new("", Vec::new()));
@@ -205,9 +195,8 @@ impl yew::Component for RemoteOverviewPanelComp {
}
fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
- let status_comp = node_info(self.status.as_ref().map(|s| s.into()));
+ let status_comp = node_info(self.status.data.as_ref().map(|s| s.into()));
- let loading = self.status.is_none() && self.last_status_error.is_none();
let title: Html = Row::new()
.gap(2)
.class(AlignItems::Baseline)
@@ -221,12 +210,13 @@ impl yew::Component for RemoteOverviewPanelComp {
.with_child(
// FIXME: add some 'visible' or 'active' property to the progress
Progress::new()
- .value((!loading).then_some(0.0))
- .style("opacity", (!loading).then_some("0")),
+ .value(self.status.has_data().then_some(0.0))
+ .style("opacity", self.status.has_data().then_some("0")),
)
.with_child(status_comp)
.with_optional_child(
- self.last_status_error
+ self.status
+ .error
.as_ref()
.map(|err| error_message(&err.to_string())),
)
diff --git a/ui/src/pve/lxc.rs b/ui/src/pve/lxc.rs
index 08380b66..4028284f 100644
--- a/ui/src/pve/lxc.rs
+++ b/ui/src/pve/lxc.rs
@@ -24,6 +24,7 @@ use pdm_client::types::{IsRunning, LxcStatus};
use crate::{
pve::utils::render_lxc_name,
renderer::{separator, status_row},
+ LoadResult,
};
#[derive(Clone, Debug, Properties)]
@@ -76,8 +77,7 @@ pub enum Msg {
}
pub struct LxcanelComp {
- status: Option<LxcStatus>,
- last_status_error: Option<proxmox_client::Error>,
+ status: LoadResult<LxcStatus, proxmox_client::Error>,
last_rrd_error: Option<proxmox_client::Error>,
_status_timeout: Option<Timeout>,
_rrd_timeout: Option<Timeout>,
@@ -124,12 +124,11 @@ impl yew::Component for LxcanelComp {
ctx.link()
.send_message_batch(vec![Msg::ReloadStatus, Msg::ReloadRrd]);
Self {
- status: None,
+ status: LoadResult::new(),
_status_timeout: None,
_rrd_timeout: None,
_async_pool: AsyncPool::new(),
last_rrd_error: None,
- last_status_error: None,
rrd_time_frame: RRDTimeframe::load(),
@@ -164,15 +163,7 @@ impl yew::Component for LxcanelComp {
false
}
Msg::StatusResult(res) => {
- match res {
- Ok(status) => {
- self.last_status_error = None;
- self.status = Some(status);
- }
- Err(err) => {
- self.last_status_error = Some(err);
- }
- }
+ self.status.update(res);
self._status_timeout = Some(Timeout::new(props.status_interval, move || {
link.send_message(Msg::ReloadStatus)
@@ -231,8 +222,7 @@ impl yew::Component for LxcanelComp {
let props = ctx.props();
if props.remote != old_props.remote || props.info != old_props.info {
- self.status = None;
- self.last_status_error = None;
+ self.status = LoadResult::new();
self.last_rrd_error = None;
self.time = Rc::new(Vec::new());
@@ -262,7 +252,7 @@ impl yew::Component for LxcanelComp {
.into();
let mut status_comp = Column::new().gap(2).padding(4);
- let status = match &self.status {
+ let status = match &self.status.data {
Some(status) => status,
None => &LxcStatus {
cpu: Some(props.info.cpu),
@@ -345,8 +335,6 @@ impl yew::Component for LxcanelComp {
HumanByte::from(status.maxdisk.unwrap_or_default() as u64).to_string(),
));
- let loading = self.status.is_none() && self.last_status_error.is_none();
-
Panel::new()
.class(FlexFit)
.title(title)
@@ -354,8 +342,8 @@ impl yew::Component for LxcanelComp {
.with_child(
// FIXME: add some 'visible' or 'active' property to the progress
Progress::new()
- .value((!loading).then_some(0.0))
- .style("opacity", (!loading).then_some("0")),
+ .value(self.status.has_data().then_some(0.0))
+ .style("opacity", self.status.has_data().then_some("0")),
)
.with_child(status_comp)
.with_child(separator().padding_x(4))
diff --git a/ui/src/pve/node/overview.rs b/ui/src/pve/node/overview.rs
index 1a98c004..c2f2958f 100644
--- a/ui/src/pve/node/overview.rs
+++ b/ui/src/pve/node/overview.rs
@@ -17,7 +17,7 @@ use pwt::{
use pdm_api_types::rrddata::NodeDataPoint;
use pdm_client::types::NodeStatus;
-use crate::renderer::separator;
+use crate::{renderer::separator, LoadResult};
#[derive(Clone, Debug, Eq, PartialEq, Properties)]
pub struct NodeOverviewPanel {
@@ -62,12 +62,11 @@ pub struct NodeOverviewPanelComp {
load_data: Rc<Series>,
mem_data: Rc<Series>,
mem_total_data: Rc<Series>,
- status: Option<NodeStatus>,
+ status: LoadResult<NodeStatus, proxmox_client::Error>,
rrd_time_frame: RRDTimeframe,
last_error: Option<proxmox_client::Error>,
- last_status_error: Option<proxmox_client::Error>,
async_pool: AsyncPool,
_timeout: Option<gloo_timers::callback::Timeout>,
@@ -103,9 +102,8 @@ impl yew::Component for NodeOverviewPanelComp {
mem_data: Rc::new(Series::new("", Vec::new())),
mem_total_data: Rc::new(Series::new("", Vec::new())),
rrd_time_frame: RRDTimeframe::load(),
- status: None,
+ status: LoadResult::new(),
last_error: None,
- last_status_error: None,
async_pool: AsyncPool::new(),
_timeout: None,
_status_timeout: None,
@@ -165,13 +163,7 @@ impl yew::Component for NodeOverviewPanelComp {
Err(err) => self.last_error = Some(err),
},
Msg::StatusLoadFinished(res) => {
- match res {
- Ok(status) => {
- self.last_status_error = None;
- self.status = Some(status);
- }
- Err(err) => self.last_status_error = Some(err),
- }
+ self.status.update(res);
let link = ctx.link().clone();
self._status_timeout = Some(gloo_timers::callback::Timeout::new(
ctx.props().status_interval,
@@ -191,8 +183,7 @@ impl yew::Component for NodeOverviewPanelComp {
let props = ctx.props();
if props.remote != old_props.remote || props.node != old_props.node {
- self.status = None;
- self.last_status_error = None;
+ self.status = LoadResult::new();
self.last_error = None;
self.time_data = Rc::new(Vec::new());
self.cpu_data = Rc::new(Series::new("", Vec::new()));
@@ -209,20 +200,20 @@ impl yew::Component for NodeOverviewPanelComp {
}
fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
- let status_comp = node_info(self.status.as_ref().map(|s| s.into()));
- let loading = self.status.is_none() && self.last_status_error.is_none();
+ let status_comp = node_info(self.status.data.as_ref().map(|s| s.into()));
Container::new()
.class(FlexFit)
.class(ColorScheme::Neutral)
.with_child(
// FIXME: add some 'visible' or 'active' property to the progress
Progress::new()
- .value((!loading).then_some(0.0))
- .style("opacity", (!loading).then_some("0")),
+ .value(self.status.has_data().then_some(0.0))
+ .style("opacity", self.status.has_data().then_some("0")),
)
.with_child(status_comp)
.with_optional_child(
- self.last_status_error
+ self.status
+ .error
.as_ref()
.map(|err| error_message(&err.to_string())),
)
diff --git a/ui/src/pve/qemu.rs b/ui/src/pve/qemu.rs
index 10266f39..3a18ca35 100644
--- a/ui/src/pve/qemu.rs
+++ b/ui/src/pve/qemu.rs
@@ -24,6 +24,7 @@ use pdm_client::types::{IsRunning, QemuStatus};
use crate::{
pve::utils::render_qemu_name,
renderer::{separator, status_row},
+ LoadResult,
};
#[derive(Clone, Debug, Properties)]
@@ -76,8 +77,7 @@ pub enum Msg {
}
pub struct QemuPanelComp {
- status: Option<QemuStatus>,
- last_status_error: Option<proxmox_client::Error>,
+ status: LoadResult<QemuStatus, proxmox_client::Error>,
last_rrd_error: Option<proxmox_client::Error>,
_status_timeout: Option<Timeout>,
_rrd_timeout: Option<Timeout>,
@@ -124,12 +124,11 @@ impl yew::Component for QemuPanelComp {
ctx.link()
.send_message_batch(vec![Msg::ReloadStatus, Msg::ReloadRrd]);
Self {
- status: None,
+ status: LoadResult::new(),
_status_timeout: None,
_rrd_timeout: None,
_async_pool: AsyncPool::new(),
last_rrd_error: None,
- last_status_error: None,
rrd_time_frame: RRDTimeframe::load(),
@@ -164,16 +163,7 @@ impl yew::Component for QemuPanelComp {
false
}
Msg::StatusResult(res) => {
- match res {
- Ok(status) => {
- self.last_status_error = None;
- self.status = Some(status);
- }
- Err(err) => {
- self.last_status_error = Some(err);
- }
- }
-
+ self.status.update(res);
self._status_timeout = Some(Timeout::new(props.status_interval, move || {
link.send_message(Msg::ReloadStatus)
}));
@@ -233,8 +223,7 @@ impl yew::Component for QemuPanelComp {
let props = ctx.props();
if props.remote != old_props.remote || props.info != old_props.info {
- self.status = None;
- self.last_status_error = None;
+ self.status = LoadResult::new();
self.last_rrd_error = None;
self.time = Rc::new(Vec::new());
@@ -265,7 +254,7 @@ impl yew::Component for QemuPanelComp {
let mut status_comp = Column::new().gap(2).padding(4);
- let status = match &self.status {
+ let status = match &self.status.data {
Some(status) => status,
None => &QemuStatus {
agent: None,
@@ -359,7 +348,6 @@ impl yew::Component for QemuPanelComp {
HumanByte::from(status.maxdisk.unwrap_or_default() as u64).to_string(),
));
- let loading = self.status.is_none() && self.last_status_error.is_none();
Panel::new()
.class(FlexFit)
.title(title)
@@ -367,8 +355,8 @@ impl yew::Component for QemuPanelComp {
.with_child(
// FIXME: add some 'visible' or 'active' property to the progress
Progress::new()
- .value((!loading).then_some(0.0))
- .style("opacity", (!loading).then_some("0")),
+ .value(self.status.has_data().then_some(0.0))
+ .style("opacity", self.status.has_data().then_some("0")),
)
.with_child(status_comp)
.with_child(separator().padding_x(4))
diff --git a/ui/src/pve/storage.rs b/ui/src/pve/storage.rs
index 93a2fa29..9730d751 100644
--- a/ui/src/pve/storage.rs
+++ b/ui/src/pve/storage.rs
@@ -22,6 +22,7 @@ use pdm_client::types::PveStorageStatus;
use crate::{
pve::utils::{render_content_type, render_storage_type},
renderer::{separator, status_row_right_icon},
+ LoadResult,
};
#[derive(Clone, Debug, Properties)]
@@ -74,8 +75,7 @@ pub enum Msg {
}
pub struct StoragePanelComp {
- status: Option<PveStorageStatus>,
- last_status_error: Option<proxmox_client::Error>,
+ status: LoadResult<PveStorageStatus, proxmox_client::Error>,
last_rrd_error: Option<proxmox_client::Error>,
/// internal guard for the periodic timeout callback to update status
@@ -131,12 +131,11 @@ impl yew::Component for StoragePanelComp {
ctx.link()
.send_message_batch(vec![Msg::ReloadStatus, Msg::ReloadRrd]);
Self {
- status: None,
+ status: LoadResult::new(),
status_update_timeout_guard: None,
rrd_update_timeout_guard: None,
_async_pool: AsyncPool::new(),
last_rrd_error: None,
- last_status_error: None,
rrd_time_frame: RRDTimeframe::load(),
@@ -167,16 +166,7 @@ impl yew::Component for StoragePanelComp {
false
}
Msg::StatusResult(res) => {
- match res {
- Ok(status) => {
- self.last_status_error = None;
- self.status = Some(status);
- }
- Err(err) => {
- self.last_status_error = Some(err);
- }
- }
-
+ self.status.update(res);
self.status_update_timeout_guard =
Some(Timeout::new(props.status_interval, move || {
link.send_message(Msg::ReloadStatus)
@@ -220,8 +210,7 @@ impl yew::Component for StoragePanelComp {
let props = ctx.props();
if props.remote != old_props.remote || props.info != old_props.info {
- self.status = None;
- self.last_status_error = None;
+ self.status = LoadResult::new();
self.last_rrd_error = None;
self.time = Rc::new(Vec::new());
@@ -246,7 +235,7 @@ impl yew::Component for StoragePanelComp {
.into();
let mut status_comp = Column::new().gap(2).padding(4);
- let status = match &self.status {
+ let status = match &self.status.data {
Some(status) => status,
None => &PveStorageStatus {
active: None,
@@ -305,8 +294,6 @@ impl yew::Component for StoragePanelComp {
.value(disk_usage as f32),
);
- let loading = self.status.is_none() && self.last_status_error.is_none();
-
Panel::new()
.class(FlexFit)
.title(title)
@@ -314,8 +301,8 @@ impl yew::Component for StoragePanelComp {
.with_child(
// FIXME: add some 'visible' or 'active' property to the progress
Progress::new()
- .value((!loading).then_some(0.0))
- .style("opacity", (!loading).then_some("0")),
+ .value(self.status.has_data().then_some(0.0))
+ .style("opacity", self.status.has_data().then_some("0")),
)
.with_child(status_comp)
.with_child(separator().padding_x(4))
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pdm-devel] [PATCH datacenter-manager 14/15] ui: dashboard: implement 'View'
2025-10-21 14:03 [pdm-devel] [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
` (12 preceding siblings ...)
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 13/15] ui: introduce `LoadResult` helper type Dominik Csapak
@ 2025-10-21 14:03 ` Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 15/15] ui: dashboard: use 'View' instead of the Dashboard Dominik Csapak
2025-10-23 8:33 ` [pdm-devel] superseded: [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
15 siblings, 0 replies; 17+ messages in thread
From: Dominik Csapak @ 2025-10-21 14:03 UTC (permalink / raw)
To: pdm-devel
this is a more generalized version of our `Dashboard`, which can be
configured with a `ViewTemplate` that is completely serializable, so
we're able to dynamically configure it (e.g. in the future over the api)
An example of such a configuration could be, e.g.:
{
"layout": {
"layout-type": "rows",
"rows": [
[
{
"widget-type": "remotes",
"show-wizard": true
}
],
[
{
"widget-type": "leaderboard",
"leaderboard-type": "guest-cpu"
}
]
]
}
}
Implemented are all widget our Dashboard holds, but this is easily
extendable.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/dashboard/mod.rs | 1 +
ui/src/dashboard/types.rs | 69 ++++++
ui/src/dashboard/view.rs | 456 ++++++++++++++++++++++++++++++++++++++
ui/src/pve/mod.rs | 4 +-
4 files changed, 529 insertions(+), 1 deletion(-)
create mode 100644 ui/src/dashboard/view.rs
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index a394ea81..8a0bdad0 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -51,6 +51,7 @@ use tasks::{create_task_summary_panel, get_task_options};
pub mod types;
+pub mod view;
mod refresh_config_edit;
pub use refresh_config_edit::{
diff --git a/ui/src/dashboard/types.rs b/ui/src/dashboard/types.rs
index 152d4f57..8899246e 100644
--- a/ui/src/dashboard/types.rs
+++ b/ui/src/dashboard/types.rs
@@ -1,5 +1,67 @@
use serde::{Deserialize, Serialize};
+use pdm_api_types::remotes::RemoteType;
+
+use crate::pve::GuestType;
+
+#[derive(Serialize, Deserialize, PartialEq, Clone)]
+#[serde(rename_all = "kebab-case")]
+pub struct ViewTemplate {
+ #[serde(skip_serializing_if = "String::is_empty")]
+ pub description: String,
+ pub layout: ViewLayout,
+}
+
+#[derive(Serialize, Deserialize, PartialEq, Clone)]
+#[serde(rename_all = "kebab-case")]
+#[serde(tag = "layout-type")]
+pub enum ViewLayout {
+ Rows {
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ rows: Vec<Vec<RowWidget>>,
+ },
+}
+
+#[derive(Serialize, Deserialize, PartialEq, Clone)]
+#[serde(rename_all = "kebab-case")]
+pub struct RowWidget {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub flex: Option<f32>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub title: Option<String>,
+ #[serde(flatten)]
+ pub r#type: WidgetType,
+}
+
+#[derive(Serialize, Deserialize, PartialEq, Clone)]
+#[serde(rename_all = "kebab-case")]
+#[serde(tag = "widget-type")]
+pub enum WidgetType {
+ #[serde(rename_all = "kebab-case")]
+ Nodes {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ remote_type: Option<RemoteType>,
+ },
+ #[serde(rename_all = "kebab-case")]
+ Guests {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ guest_type: Option<GuestType>,
+ },
+ #[serde(rename_all = "kebab-case")]
+ Remotes {
+ show_wizard: bool,
+ },
+ Subscription,
+ Sdn,
+ #[serde(rename_all = "kebab-case")]
+ Leaderboard {
+ leaderboard_type: LeaderboardType,
+ },
+ TaskSummary {
+ grouping: TaskSummaryGrouping,
+ },
+}
+
#[derive(Serialize, Deserialize, PartialEq, Clone, Copy)]
#[serde(rename_all = "kebab-case")]
pub enum LeaderboardType {
@@ -7,3 +69,10 @@ pub enum LeaderboardType {
NodeCpu,
NodeMemory,
}
+
+#[derive(Serialize, Deserialize, PartialEq, Clone, Copy)]
+#[serde(rename_all = "kebab-case")]
+pub enum TaskSummaryGrouping {
+ Category,
+ Remote,
+}
diff --git a/ui/src/dashboard/view.rs b/ui/src/dashboard/view.rs
new file mode 100644
index 00000000..7159d4e8
--- /dev/null
+++ b/ui/src/dashboard/view.rs
@@ -0,0 +1,456 @@
+use std::rc::Rc;
+
+use anyhow::Error;
+use futures::join;
+use js_sys::Date;
+use pwt::widget::form::FormContext;
+use serde_json::json;
+use yew::virtual_dom::{VComp, VNode};
+
+use proxmox_yew_comp::http_get;
+use pwt::css;
+use pwt::prelude::*;
+use pwt::props::StorageLocation;
+use pwt::state::PersistentState;
+use pwt::widget::{error_message, Column, Container, Panel, Progress, Row};
+use pwt::AsyncPool;
+
+use crate::dashboard::refresh_config_edit::{
+ refresh_config_id, RefreshConfig, DEFAULT_MAX_AGE_S, DEFAULT_REFRESH_INTERVAL_S,
+ FORCE_RELOAD_MAX_AGE_S, INITIAL_MAX_AGE_S,
+};
+use crate::dashboard::tasks::get_task_options;
+use crate::dashboard::types::{
+ LeaderboardType, TaskSummaryGrouping, ViewLayout, ViewTemplate, WidgetType,
+};
+use crate::dashboard::{
+ create_guest_panel, create_node_panel, create_refresh_config_edit_window, create_remote_panel,
+ create_sdn_panel, create_subscription_panel, create_task_summary_panel,
+ create_top_entities_panel, DashboardStatusRow,
+};
+use crate::remotes::AddWizard;
+use crate::{pdm_client, LoadResult};
+
+use pdm_api_types::remotes::RemoteType;
+use pdm_api_types::resource::ResourcesStatus;
+use pdm_api_types::TaskStatistics;
+use pdm_client::types::TopEntities;
+
+#[derive(Properties, PartialEq)]
+pub struct View {
+ view: AttrValue,
+}
+
+impl From<View> for VNode {
+ fn from(val: View) -> Self {
+ let comp = VComp::new::<ViewComp>(Rc::new(val), None);
+ VNode::from(comp)
+ }
+}
+
+impl View {
+ pub fn new(view: impl Into<AttrValue>) -> Self {
+ Self { view: view.into() }
+ }
+}
+
+pub enum LoadingResult {
+ Resources(Result<ResourcesStatus, Error>),
+ TopEntities(Result<pdm_client::types::TopEntities, proxmox_client::Error>),
+ TaskStatistics(Result<TaskStatistics, Error>),
+ All,
+}
+
+pub enum Msg {
+ ViewTemplateLoaded(Result<ViewTemplate, Error>),
+ LoadingResult(LoadingResult),
+ CreateWizard(Option<RemoteType>),
+ Reload(bool), // force
+ ConfigWindow(bool), // show
+ UpdateConfig(RefreshConfig),
+}
+
+struct ViewComp {
+ template: LoadResult<ViewTemplate, Error>,
+
+ // various api call results
+ status: LoadResult<ResourcesStatus, Error>,
+ top_entities: LoadResult<TopEntities, proxmox_client::Error>,
+ statistics: LoadResult<TaskStatistics, Error>,
+
+ refresh_config: PersistentState<RefreshConfig>,
+
+ async_pool: AsyncPool,
+ loading: bool,
+ load_finished_time: Option<f64>,
+ show_config_window: bool,
+ show_create_wizard: Option<RemoteType>,
+}
+
+impl ViewComp {
+ fn create_widget(&self, ctx: &yew::Context<Self>, widget: &WidgetType) -> Panel {
+ match widget {
+ WidgetType::Nodes { remote_type } => {
+ create_node_panel(*remote_type, self.status.data.clone())
+ }
+ WidgetType::Guests { guest_type } => {
+ create_guest_panel(*guest_type, self.status.data.clone())
+ }
+ WidgetType::Remotes { show_wizard } => create_remote_panel(
+ self.status.data.clone(),
+ show_wizard.then_some(
+ ctx.link()
+ .callback(|_| Msg::CreateWizard(Some(RemoteType::Pve))),
+ ),
+ show_wizard.then_some(
+ ctx.link()
+ .callback(|_| Msg::CreateWizard(Some(RemoteType::Pve))),
+ ),
+ ),
+ WidgetType::Subscription => create_subscription_panel(),
+ WidgetType::Sdn => create_sdn_panel(self.status.data.clone()),
+ WidgetType::Leaderboard { leaderboard_type } => {
+ let entities = match leaderboard_type {
+ LeaderboardType::GuestCpu => self
+ .top_entities
+ .data
+ .as_ref()
+ .map(|entities| entities.guest_cpu.clone()),
+ LeaderboardType::NodeCpu => self
+ .top_entities
+ .data
+ .as_ref()
+ .map(|entities| entities.node_cpu.clone()),
+ LeaderboardType::NodeMemory => self
+ .top_entities
+ .data
+ .as_ref()
+ .map(|entities| entities.node_memory.clone()),
+ };
+ create_top_entities_panel(
+ entities,
+ self.top_entities.error.as_ref(),
+ *leaderboard_type,
+ )
+ }
+ WidgetType::TaskSummary { grouping } => {
+ let remotes = match grouping {
+ TaskSummaryGrouping::Category => None,
+ TaskSummaryGrouping::Remote => Some(5),
+ };
+ let (hours, since) = get_task_options(self.refresh_config.task_last_hours);
+ create_task_summary_panel(
+ self.statistics.data.clone(),
+ self.statistics.error.as_ref(),
+ remotes,
+ hours,
+ since,
+ )
+ }
+ }
+ }
+
+ fn reload(&mut self, ctx: &yew::Context<Self>) {
+ let max_age = if self.load_finished_time.is_some() {
+ self.refresh_config.max_age.unwrap_or(DEFAULT_MAX_AGE_S)
+ } else {
+ INITIAL_MAX_AGE_S
+ };
+ self.do_reload(ctx, max_age)
+ }
+
+ fn do_reload(&mut self, ctx: &yew::Context<Self>, max_age: u64) {
+ if let Some(data) = self.template.data.as_ref() {
+ let link = ctx.link().clone();
+ let (_, since) = get_task_options(self.refresh_config.task_last_hours);
+ let (status, top_entities, tasks) = required_api_calls(&data.layout);
+
+ self.loading = true;
+ self.async_pool.spawn(async move {
+ let status_future = async {
+ if status {
+ let res =
+ http_get("/resources/status", Some(json!({"max-age": max_age}))).await;
+ link.send_message(Msg::LoadingResult(LoadingResult::Resources(res)));
+ }
+ };
+
+ let entities_future = async {
+ if top_entities {
+ let client: pdm_client::PdmClient<Rc<proxmox_yew_comp::HttpClientWasm>> =
+ pdm_client();
+ let res = client.get_top_entities().await;
+ link.send_message(Msg::LoadingResult(LoadingResult::TopEntities(res)));
+ }
+ };
+
+ let tasks_future = async {
+ if tasks {
+ let params = Some(json!({
+ "since": since,
+ "limit": 0,
+ }));
+ let res = http_get("/remote-tasks/statistics", params).await;
+ link.send_message(Msg::LoadingResult(LoadingResult::TaskStatistics(res)));
+ }
+ };
+
+ join!(status_future, entities_future, tasks_future);
+ link.send_message(Msg::LoadingResult(LoadingResult::All));
+ });
+ } else {
+ ctx.link()
+ .send_message(Msg::LoadingResult(LoadingResult::All));
+ }
+ }
+}
+
+// returns which api calls are required: status, top_entities, task statistics
+fn required_api_calls(layout: &ViewLayout) -> (bool, bool, bool) {
+ let mut status = false;
+ let mut top_entities = false;
+ let mut task_statistics = false;
+ match layout {
+ ViewLayout::Rows { rows } => {
+ for row in rows {
+ for item in row {
+ match item.r#type {
+ WidgetType::Nodes { .. }
+ | WidgetType::Guests { .. }
+ | WidgetType::Remotes { .. }
+ | WidgetType::Sdn => {
+ status = true;
+ }
+ WidgetType::Subscription => {
+ // panel does it itself, it's always required anyway
+ }
+ WidgetType::Leaderboard { .. } => top_entities = true,
+ WidgetType::TaskSummary { .. } => task_statistics = true,
+ }
+ }
+ }
+ }
+ }
+
+ (status, top_entities, task_statistics)
+}
+
+fn has_sub_panel(layout: Option<&ViewTemplate>) -> bool {
+ match layout.map(|template| &template.layout) {
+ Some(ViewLayout::Rows { rows }) => {
+ for row in rows {
+ for item in row {
+ if item.r#type == WidgetType::Subscription {
+ return true;
+ }
+ }
+ }
+ }
+ None => {}
+ }
+
+ false
+}
+
+impl Component for ViewComp {
+ type Message = Msg;
+ type Properties = View;
+
+ fn create(ctx: &yew::Context<Self>) -> Self {
+ let refresh_config: PersistentState<RefreshConfig> = PersistentState::new(
+ StorageLocation::local(refresh_config_id(ctx.props().view.as_str())),
+ );
+
+ let async_pool = AsyncPool::new();
+ async_pool.send_future(ctx.link().clone(), async move {
+ Msg::ViewTemplateLoaded(load_template().await)
+ });
+
+ Self {
+ template: LoadResult::new(),
+ async_pool,
+
+ status: LoadResult::new(),
+ top_entities: LoadResult::new(),
+ statistics: LoadResult::new(),
+
+ refresh_config,
+ load_finished_time: None,
+ loading: true,
+ show_config_window: false,
+ show_create_wizard: None,
+ }
+ }
+
+ fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
+ match msg {
+ Msg::ViewTemplateLoaded(view_template) => {
+ self.template.update(view_template);
+ self.reload(ctx);
+ }
+ Msg::LoadingResult(loading_result) => match loading_result {
+ LoadingResult::Resources(status) => self.status.update(status),
+ LoadingResult::TopEntities(top_entities) => self.top_entities.update(top_entities),
+ LoadingResult::TaskStatistics(task_statistics) => {
+ self.statistics.update(task_statistics)
+ }
+ LoadingResult::All => {
+ self.loading = false;
+ if self.load_finished_time.is_none() {
+ // 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(false));
+ }
+ self.load_finished_time = Some(Date::now() / 1000.0);
+ }
+ },
+ Msg::CreateWizard(remote_type) => {
+ self.show_create_wizard = remote_type;
+ }
+ Msg::Reload(force) => {
+ if force {
+ self.do_reload(ctx, FORCE_RELOAD_MAX_AGE_S);
+ } else {
+ self.reload(ctx);
+ }
+ }
+
+ Msg::ConfigWindow(show) => {
+ self.show_config_window = show;
+ }
+ Msg::UpdateConfig(dashboard_config) => {
+ let (old_hours, _) = get_task_options(self.refresh_config.task_last_hours);
+ self.refresh_config.update(dashboard_config);
+ let (new_hours, _) = get_task_options(self.refresh_config.task_last_hours);
+
+ if old_hours != new_hours {
+ self.reload(ctx);
+ }
+
+ self.show_config_window = false;
+ }
+ }
+ true
+ }
+
+ fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
+ self.async_pool = AsyncPool::new();
+ self.load_finished_time = None;
+ self.async_pool.send_future(ctx.link().clone(), async move {
+ Msg::ViewTemplateLoaded(load_template().await)
+ });
+ true
+ }
+
+ fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
+ if !self.template.has_data() {
+ return Progress::new().into();
+ }
+ let mut view = Column::new().class(css::FlexFit).with_child(
+ Container::new()
+ .class("pwt-content-spacer-padding")
+ .class("pwt-content-spacer-colors")
+ .class("pwt-default-colors")
+ .with_child(DashboardStatusRow::new(
+ self.load_finished_time,
+ self.refresh_config
+ .refresh_interval
+ .unwrap_or(DEFAULT_REFRESH_INTERVAL_S),
+ ctx.link().callback(Msg::Reload),
+ ctx.link().callback(|_| Msg::ConfigWindow(true)),
+ )),
+ );
+ if !has_sub_panel(self.template.data.as_ref()) {
+ view.add_child(
+ Row::new()
+ .class("pwt-content-spacer")
+ .with_child(create_subscription_panel()),
+ );
+ }
+ match self.template.data.as_ref().map(|template| &template.layout) {
+ Some(ViewLayout::Rows { rows }) => {
+ for items in rows {
+ let mut row = Row::new()
+ .gap(4)
+ .padding_top(0)
+ .class("pwt-content-spacer")
+ .class(css::FlexDirection::Row)
+ .class(css::FlexWrap::Wrap);
+ let flex_sum: f32 = items.iter().map(|item| item.flex.unwrap_or(1.0)).sum();
+ let gaps_ratio = items.len().saturating_sub(1) as f32 / items.len() as f32;
+ for item in items {
+ let flex = item.flex.unwrap_or(1.0);
+ let flex_ratio = 100.0 * flex / flex_sum;
+ // we have to subtract the gaps too
+ let flex_style = format!(
+ "{} {} calc({}% - calc({} * var(--pwt-spacer-4)))",
+ flex, flex, flex_ratio, gaps_ratio
+ );
+ let mut widget = self
+ .create_widget(ctx, &item.r#type)
+ .style("flex", flex_style);
+ if let Some(title) = item.title.clone() {
+ widget.set_title(title);
+ }
+ row.add_child(widget);
+ }
+ view.add_child(row);
+ }
+ }
+ None => {}
+ }
+ // fill remaining space
+ view.add_child(
+ Container::new()
+ .class(css::Flex::Fill)
+ .class("pwt-content-spacer"),
+ );
+ view.add_optional_child(
+ self.template
+ .error
+ .as_ref()
+ .map(|err| error_message(&err.to_string())),
+ );
+ view.add_optional_child(
+ self.show_config_window.then_some(
+ create_refresh_config_edit_window(&ctx.props().view)
+ .on_close(ctx.link().callback(|_| Msg::ConfigWindow(false)))
+ .on_submit({
+ let link = ctx.link().clone();
+ move |ctx: FormContext| {
+ let link = link.clone();
+ async move {
+ let data: RefreshConfig =
+ serde_json::from_value(ctx.get_submit_data())?;
+ link.send_message(Msg::UpdateConfig(data));
+ Ok(())
+ }
+ }
+ }),
+ ),
+ );
+ view.add_optional_child(self.show_create_wizard.map(|remote_type| {
+ AddWizard::new(remote_type)
+ .on_close(ctx.link().callback(|_| Msg::CreateWizard(None)))
+ .on_submit(move |ctx| crate::remotes::create_remote(ctx, remote_type))
+ }));
+ view.into()
+ }
+}
+
+async fn load_template() -> Result<ViewTemplate, Error> {
+ // FIXME: load template from api
+
+ let view_str = "
+ {
+ \"description\": \"some description\",
+ \"layout\": {
+ \"layout-type\": \"rows\",
+ \"rows\": []
+ }
+ }
+ ";
+
+ let template: ViewTemplate = serde_json::from_str(view_str)?;
+ Ok(template)
+}
diff --git a/ui/src/pve/mod.rs b/ui/src/pve/mod.rs
index 058424a9..256744ea 100644
--- a/ui/src/pve/mod.rs
+++ b/ui/src/pve/mod.rs
@@ -3,6 +3,7 @@ use std::{fmt::Display, rc::Rc};
use gloo_utils::window;
use proxmox_client::Error;
use proxmox_yew_comp::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster};
+use serde::{Deserialize, Serialize};
use yew::{
prelude::Html,
virtual_dom::{VComp, VNode},
@@ -67,7 +68,8 @@ impl std::fmt::Display for Action {
}
}
-#[derive(PartialEq, Clone, Copy)]
+#[derive(PartialEq, Clone, Copy, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
pub enum GuestType {
Qemu,
Lxc,
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pdm-devel] [PATCH datacenter-manager 15/15] ui: dashboard: use 'View' instead of the Dashboard
2025-10-21 14:03 [pdm-devel] [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
` (13 preceding siblings ...)
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 14/15] ui: dashboard: implement 'View' Dominik Csapak
@ 2025-10-21 14:03 ` Dominik Csapak
2025-10-23 8:33 ` [pdm-devel] superseded: [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
15 siblings, 0 replies; 17+ messages in thread
From: Dominik Csapak @ 2025-10-21 14:03 UTC (permalink / raw)
To: pdm-devel
this uses our new `View` with a (currently) static configuration to
replicate our Dashboard. Since all functionality of that is available
in the View, the `Dashboard` struct can be removed.
This should be functionally the same as before.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/dashboard/mod.rs | 486 +--------------------------------------
ui/src/dashboard/view.rs | 61 ++++-
ui/src/lib.rs | 2 +-
ui/src/main_menu.rs | 5 +-
4 files changed, 76 insertions(+), 478 deletions(-)
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 8a0bdad0..8e979417 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -1,27 +1,6 @@
-use std::rc::Rc;
-
-use anyhow::Error;
-use futures::join;
-use js_sys::Date;
-use serde_json::json;
-use yew::{
- virtual_dom::{VComp, VNode},
- Component,
-};
-
-use proxmox_yew_comp::http_get;
-use pwt::{
- css::{AlignItems, FlexDirection, FlexFit, FlexWrap, JustifyContent},
- prelude::*,
- props::StorageLocation,
- state::PersistentState,
- widget::{form::FormContext, Column, Container, Fa, Panel, Row},
- AsyncPool,
-};
-
-use pdm_api_types::{remotes::RemoteType, resource::ResourcesStatus, TaskStatistics};
-
-use crate::{pve::GuestType, remotes::AddWizard};
+use pwt::css;
+use pwt::prelude::*;
+use pwt::widget::{Column, Fa, Row};
mod top_entities;
pub use top_entities::create_top_entities_panel;
@@ -39,477 +18,36 @@ mod guest_panel;
pub use guest_panel::create_guest_panel;
mod sdn_zone_panel;
-use sdn_zone_panel::create_sdn_panel;
+pub use sdn_zone_panel::create_sdn_panel;
mod status_row;
-use status_row::DashboardStatusRow;
+pub use status_row::DashboardStatusRow;
mod filtered_tasks;
mod tasks;
-use tasks::{create_task_summary_panel, get_task_options};
+pub use tasks::create_task_summary_panel;
pub mod types;
pub mod view;
mod refresh_config_edit;
-pub use refresh_config_edit::{
- create_refresh_config_edit_window, refresh_config_id, RefreshConfig,
-};
-use refresh_config_edit::{
- DEFAULT_MAX_AGE_S, DEFAULT_REFRESH_INTERVAL_S, FORCE_RELOAD_MAX_AGE_S, INITIAL_MAX_AGE_S,
-};
-
-#[derive(Properties, PartialEq)]
-pub struct Dashboard {}
-
-impl Dashboard {
- pub fn new() -> Self {
- yew::props!(Self {})
- }
-}
-
-impl Default for Dashboard {
- fn default() -> Self {
- Self::new()
- }
-}
-
-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),
- CreateWizard(Option<RemoteType>),
- Reload,
- ForceReload,
- UpdateConfig(RefreshConfig),
- ConfigWindow(bool),
-}
-
-struct StatisticsOptions {
- data: Option<TaskStatistics>,
- error: Option<Error>,
-}
-
-pub struct PdmDashboard {
- status: Option<ResourcesStatus>,
- last_error: Option<Error>,
- top_entities: Option<pdm_client::types::TopEntities>,
- last_top_entities_error: Option<proxmox_client::Error>,
- statistics: StatisticsOptions,
- load_finished_time: Option<f64>,
- show_wizard: Option<RemoteType>,
- show_config_window: bool,
- async_pool: AsyncPool,
- config: PersistentState<RefreshConfig>,
-}
-
-impl PdmDashboard {
- fn reload(&mut self, ctx: &yew::Context<Self>) {
- let max_age = if self.load_finished_time.is_some() {
- self.config.max_age.unwrap_or(DEFAULT_MAX_AGE_S)
- } else {
- INITIAL_MAX_AGE_S
- };
- self.do_reload(ctx, max_age)
- }
-
- fn do_reload(&mut self, ctx: &yew::Context<Self>, max_age: u64) {
- let link = ctx.link().clone();
- let (_, since) = get_task_options(self.config.task_last_hours);
-
- self.async_pool.spawn(async move {
- let client = crate::pdm_client();
-
- 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,
- "limit": 0,
- }));
-
- // TODO replace with pdm client call
- 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));
- });
- }
-}
-
-impl Component for PdmDashboard {
- type Message = Msg;
- type Properties = Dashboard;
-
- fn create(ctx: &yew::Context<Self>) -> Self {
- let config: PersistentState<RefreshConfig> =
- PersistentState::new(StorageLocation::local(refresh_config_id("dashboard")));
- let async_pool = AsyncPool::new();
-
- let mut this = Self {
- status: None,
- last_error: None,
- top_entities: None,
- last_top_entities_error: None,
- statistics: StatisticsOptions {
- data: None,
- error: None,
- },
- load_finished_time: None,
- show_wizard: None,
- show_config_window: false,
- async_pool,
- config,
- };
-
- this.reload(ctx);
-
- this
- }
-
- fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
- match msg {
- 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 => {
- if self.load_finished_time.is_none() {
- // 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::CreateWizard(remote_type) => {
- self.show_wizard = remote_type;
- true
- }
- Msg::Reload => {
- self.reload(ctx);
- true
- }
- Msg::ForceReload => {
- self.do_reload(ctx, FORCE_RELOAD_MAX_AGE_S);
- true
- }
- Msg::ConfigWindow(show) => {
- self.show_config_window = show;
- true
- }
- Msg::UpdateConfig(dashboard_config) => {
- let (old_hours, _) = get_task_options(self.config.task_last_hours);
- self.config.update(dashboard_config);
- let (new_hours, _) = get_task_options(self.config.task_last_hours);
-
- if old_hours != new_hours {
- self.reload(ctx);
- }
-
- self.show_config_window = false;
- true
- }
- }
- }
-
- fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
- let (hours, since) = get_task_options(self.config.task_last_hours);
- let content = Column::new()
- .class(FlexFit)
- .with_child(
- Container::new()
- .class("pwt-content-spacer-padding")
- .class("pwt-content-spacer-colors")
- .style("color", "var(--pwt-color)")
- .style("background-color", "var(--pwt-color-background)")
- .with_child(DashboardStatusRow::new(
- self.load_finished_time,
- self.config
- .refresh_interval
- .unwrap_or(DEFAULT_REFRESH_INTERVAL_S),
- ctx.link()
- .callback(|force| if force { Msg::ForceReload } else { Msg::Reload }),
- ctx.link().callback(|_| Msg::ConfigWindow(true)),
- )),
- )
- .with_child(
- Container::new()
- .class("pwt-content-spacer")
- .class(FlexDirection::Row)
- .class(FlexWrap::Wrap)
- .padding_top(0)
- .with_child(
- create_remote_panel(
- self.status.clone(),
- Some(
- ctx.link()
- .callback(|_| Msg::CreateWizard(Some(RemoteType::Pve))),
- ),
- Some(
- ctx.link()
- .callback(|_| Msg::CreateWizard(Some(RemoteType::Pbs))),
- ),
- )
- .flex(1.0)
- .width(300)
- .min_height(175),
- )
- .with_child(
- create_node_panel(Some(RemoteType::Pve), self.status.clone())
- .flex(1.0)
- .width(300),
- )
- .with_child(
- create_guest_panel(Some(GuestType::Qemu), self.status.clone())
- .flex(1.0)
- .width(300),
- )
- .with_child(
- create_guest_panel(Some(GuestType::Lxc), self.status.clone())
- .flex(1.0)
- .width(300),
- )
- // FIXME: add PBS support
- //.with_child(self.create_node_panel(
- // "building-o",
- // tr!("Backup Server Nodes"),
- // &self.status.pbs_nodes,
- //))
- //.with_child(
- // Panel::new()
- // .flex(1.0)
- // .width(300)
- // .title(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! {<i class={"pwt-loading-icon"} />})
- // } 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(
- create_subscription_panel()
- .flex(1.0)
- .width(500)
- .min_height(150),
- ),
- )
- .with_child(
- Container::new()
- .class("pwt-content-spacer")
- .class(FlexDirection::Row)
- .class("pwt-align-content-start")
- .padding_top(0)
- .class(FlexWrap::Wrap)
- //.min_height(175)
- .with_child(
- create_top_entities_panel(
- self.top_entities.as_ref().map(|e| e.guest_cpu.clone()),
- self.last_top_entities_error.as_ref(),
- types::LeaderboardType::GuestCpu,
- )
- .flex(1.0)
- .width(500)
- .min_width(400),
- )
- .with_child(
- create_top_entities_panel(
- self.top_entities.as_ref().map(|e| e.node_cpu.clone()),
- self.last_top_entities_error.as_ref(),
- types::LeaderboardType::NodeCpu,
- )
- .flex(1.0)
- .width(500)
- .min_width(400),
- )
- .with_child(
- create_top_entities_panel(
- self.top_entities.as_ref().map(|e| e.node_memory.clone()),
- self.last_top_entities_error.as_ref(),
- types::LeaderboardType::NodeCpu,
- )
- .flex(1.0)
- .width(500)
- .min_width(400),
- ),
- )
- .with_child(
- Container::new()
- .class("pwt-content-spacer")
- .class(FlexDirection::Row)
- .class("pwt-align-content-start")
- .style("padding-top", "0")
- .class(pwt::css::Flex::Fill)
- .class(FlexWrap::Wrap)
- .with_child(
- create_task_summary_panel(
- self.statistics.data.clone(),
- self.statistics.error.as_ref(),
- None,
- hours,
- since,
- )
- .flex(1.0)
- .width(500),
- )
- .with_child(
- create_task_summary_panel(
- self.statistics.data.clone(),
- self.statistics.error.as_ref(),
- Some(5),
- hours,
- since,
- )
- .flex(1.0)
- .width(500),
- )
- .with_child(create_sdn_panel(self.status.clone()).flex(1.0).width(200)),
- );
-
- Panel::new()
- .class(FlexFit)
- .with_child(content)
- .with_optional_child(self.show_wizard.map(|remote_type| {
- AddWizard::new(remote_type)
- .on_close(ctx.link().callback(|_| Msg::CreateWizard(None)))
- .on_submit(move |ctx| crate::remotes::create_remote(ctx, remote_type))
- }))
- .with_optional_child(
- self.show_config_window.then_some(
- create_refresh_config_edit_window("dashboard")
- .on_close(ctx.link().callback(|_| Msg::ConfigWindow(false)))
- .on_submit({
- let link = ctx.link().clone();
- move |ctx: FormContext| {
- let link = link.clone();
- async move {
- let data: RefreshConfig =
- serde_json::from_value(ctx.get_submit_data())?;
- link.send_message(Msg::UpdateConfig(data));
- Ok(())
- }
- }
- }),
- ),
- )
- .into()
- }
-}
-
-impl From<Dashboard> for VNode {
- fn from(val: Dashboard) -> Self {
- let comp = VComp::new::<PdmDashboard>(Rc::new(val), None);
- VNode::from(comp)
- }
-}
+pub use refresh_config_edit::create_refresh_config_edit_window;
fn loading_column() -> Column {
Column::new()
.padding(4)
- .class(FlexFit)
- .class(JustifyContent::Center)
- .class(AlignItems::Center)
+ .class(css::FlexFit)
+ .class(css::JustifyContent::Center)
+ .class(css::AlignItems::Center)
.with_child(html! {<i class={"pwt-loading-icon"} />})
}
/// Create a consistent title component for the given title and icon
-pub fn create_title_with_icon(icon: &str, title: String) -> Html {
+fn create_title_with_icon(icon: &str, title: String) -> Html {
Row::new()
- .class(AlignItems::Center)
+ .class(css::AlignItems::Center)
.gap(2)
.with_child(Fa::new(icon))
.with_child(title)
diff --git a/ui/src/dashboard/view.rs b/ui/src/dashboard/view.rs
index 7159d4e8..6cea9f87 100644
--- a/ui/src/dashboard/view.rs
+++ b/ui/src/dashboard/view.rs
@@ -446,7 +446,66 @@ async fn load_template() -> Result<ViewTemplate, Error> {
\"description\": \"some description\",
\"layout\": {
\"layout-type\": \"rows\",
- \"rows\": []
+ \"rows\": [
+ [
+ {
+ \"flex\": 3.0,
+ \"widget-type\": \"remotes\",
+ \"show-wizard\": true
+ },
+ {
+ \"flex\": 3.0,
+ \"widget-type\": \"nodes\",
+ \"remote-type\": \"pve\"
+ },
+ {
+ \"flex\": 3.0,
+ \"widget-type\": \"guests\",
+ \"guest-type\": \"qemu\"
+ },
+ {
+ \"flex\": 3.0,
+ \"widget-type\": \"guests\",
+ \"guest-type\": \"lxc\"
+ },
+ {
+ \"flex\": 5.0,
+ \"widget-type\": \"subscription\"
+ }
+ ],
+ [
+ {
+ \"widget-type\": \"leaderboard\",
+ \"leaderboard-type\": \"guest-cpu\"
+ },
+ {
+ \"widget-type\": \"leaderboard\",
+ \"leaderboard-type\": \"node-cpu\"
+ },
+ {
+ \"widget-type\": \"leaderboard\",
+ \"leaderboard-type\": \"node-memory\"
+ }
+ ],
+ [
+ {
+ \"flex\": 5.0,
+ \"widget-type\": \"task-summary\",
+ \"grouping\": \"category\",
+ \"sorting\": \"default\"
+ },
+ {
+ \"flex\": 5.0,
+ \"widget-type\": \"task-summary\",
+ \"grouping\": \"remote\",
+ \"sorting\": \"failed-tasks\"
+ },
+ {
+ \"flex\": 2.0,
+ \"widget-type\": \"sdn\"
+ }
+ ]
+ ]
}
}
";
diff --git a/ui/src/lib.rs b/ui/src/lib.rs
index de76e1c0..f9af023d 100644
--- a/ui/src/lib.rs
+++ b/ui/src/lib.rs
@@ -27,7 +27,7 @@ mod search_provider;
pub use search_provider::SearchProvider;
mod dashboard;
-pub use dashboard::Dashboard;
+
use yew_router::prelude::RouterScopeExt;
mod widget;
diff --git a/ui/src/main_menu.rs b/ui/src/main_menu.rs
index 7650b63f..b5044169 100644
--- a/ui/src/main_menu.rs
+++ b/ui/src/main_menu.rs
@@ -13,11 +13,12 @@ use proxmox_yew_comp::{NotesView, XTermJs};
use pdm_api_types::remotes::RemoteType;
+use crate::dashboard::view::View;
use crate::remotes::RemotesPanel;
use crate::sdn::evpn::EvpnPanel;
use crate::sdn::ZoneTree;
use crate::{
- AccessControl, CertificatesPanel, Dashboard, RemoteListCacheEntry, ServerAdministration,
+ AccessControl, CertificatesPanel, RemoteListCacheEntry, ServerAdministration,
SystemConfiguration,
};
@@ -141,7 +142,7 @@ impl Component for PdmMainMenu {
tr!("Dashboard"),
"dashboard",
Some("fa fa-tachometer"),
- move |_| Dashboard::new().into(),
+ move |_| View::new("dashboard").into(),
);
register_view(
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 17+ messages in thread
* [pdm-devel] superseded: [PATCH datacenter-manager 00/15] prepare ui fore customizable views
2025-10-21 14:03 [pdm-devel] [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
` (14 preceding siblings ...)
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 15/15] ui: dashboard: use 'View' instead of the Dashboard Dominik Csapak
@ 2025-10-23 8:33 ` Dominik Csapak
15 siblings, 0 replies; 17+ messages in thread
From: Dominik Csapak @ 2025-10-23 8:33 UTC (permalink / raw)
To: pdm-devel
superseded by v2:
https://lore.proxmox.com/all/20251023083253.1038119-1-d.csapak@proxmox.com/
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 17+ messages in thread