* [pdm-devel] [PATCH datacenter-manager 00/15] prepare ui fore customizable views
@ 2025-10-21 14:03 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
` (14 more replies)
0 siblings, 15 replies; 16+ messages in thread
From: Dominik Csapak @ 2025-10-21 14:03 UTC (permalink / raw)
To: pdm-devel
This is the first step to have customizable views in the PDM ui.
Patches 1-11 are refactors mostly and should not change behavior.
Patch 12 is an improvement I noticed while doing this series, I can
send it upfront if wanted.
Patches 13-15 are RFCs because:
* The `LoadResult` struct should probably live in either pwt or
yew-comp, I think Dietmar has already something local so I did not
want to interfere there. (We can switch to the one there if it's
committed and bumped)
* Not super sure if this kind of structure is the one we desire.
The View is (for now) only a single layout (Row) that provides
the same behavior as the current layout. The whole logic is in the
View and every bit we want to add has to be handled there.
Dominik Csapak (15):
ui: dashboard: refactor guest panel creation to its own module
ui: dashboard: refactor creating the node panel into its own module
ui: dashboard: refactor remote panel creation into its own module
ui: dashboard: remote panel: make wizard menu optional
ui: dashboard: refactor sdn panel creation into its own module
ui: dashboard: refactor task summary panel creation to its own module
ui: dashboard: refactor subscription panel creation to its own module
ui: dashboard: refactor top entities panel creation to its own module
ui: dashboard: refactor DashboardConfig editing/constants to their
module
ui: dashboard: factor out task parameter calculation
ui: dashboard: remove unused remote list
ui: dashboard: status row: make loading less jarring
ui: introduce `LoadResult` helper type
ui: dashboard: implement 'View'
ui: dashboard: use 'View' instead of the Dashboard
ui/src/dashboard/guest_panel.rs | 75 ++-
ui/src/dashboard/mod.rs | 784 +-----------------------
ui/src/dashboard/node_panel.rs | 150 +++++
ui/src/dashboard/refresh_config_edit.rs | 107 ++++
ui/src/dashboard/remote_panel.rs | 51 +-
ui/src/dashboard/sdn_zone_panel.rs | 15 +-
ui/src/dashboard/status_row.rs | 11 +-
ui/src/dashboard/subscription_info.rs | 54 +-
ui/src/dashboard/tasks.rs | 38 ++
ui/src/dashboard/top_entities.rs | 45 +-
ui/src/dashboard/types.rs | 78 +++
ui/src/dashboard/view.rs | 515 ++++++++++++++++
ui/src/lib.rs | 5 +-
ui/src/load_result.rs | 42 ++
ui/src/main_menu.rs | 5 +-
ui/src/pbs/remote.rs | 30 +-
ui/src/pve/lxc.rs | 28 +-
ui/src/pve/mod.rs | 4 +-
ui/src/pve/node/overview.rs | 29 +-
ui/src/pve/qemu.rs | 28 +-
ui/src/pve/storage.rs | 29 +-
21 files changed, 1199 insertions(+), 924 deletions(-)
create mode 100644 ui/src/dashboard/node_panel.rs
create mode 100644 ui/src/dashboard/refresh_config_edit.rs
create mode 100644 ui/src/dashboard/types.rs
create mode 100644 ui/src/dashboard/view.rs
create mode 100644 ui/src/load_result.rs
--
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] 16+ messages in thread
* [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
` (13 subsequent siblings)
14 siblings, 0 replies; 16+ 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] 16+ 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
` (12 subsequent siblings)
14 siblings, 0 replies; 16+ 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] 16+ 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
` (11 subsequent siblings)
14 siblings, 0 replies; 16+ 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] 16+ 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
` (10 subsequent siblings)
14 siblings, 0 replies; 16+ 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] 16+ 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
` (9 subsequent siblings)
14 siblings, 0 replies; 16+ 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] 16+ 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
` (8 subsequent siblings)
14 siblings, 0 replies; 16+ 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] 16+ 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
` (7 subsequent siblings)
14 siblings, 0 replies; 16+ 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] 16+ 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
` (6 subsequent siblings)
14 siblings, 0 replies; 16+ 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] 16+ 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
` (5 subsequent siblings)
14 siblings, 0 replies; 16+ 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] 16+ 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
` (4 subsequent siblings)
14 siblings, 0 replies; 16+ 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] 16+ 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
` (3 subsequent siblings)
14 siblings, 0 replies; 16+ 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] 16+ 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
` (2 subsequent siblings)
14 siblings, 0 replies; 16+ 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] 16+ 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
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 15/15] ui: dashboard: use 'View' instead of the Dashboard Dominik Csapak
14 siblings, 0 replies; 16+ 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] 16+ 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
14 siblings, 0 replies; 16+ 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] 16+ 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
14 siblings, 0 replies; 16+ 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] 16+ messages in thread
end of thread, other threads:[~2025-10-21 14:08 UTC | newest]
Thread overview: 16+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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 ` [pdm-devel] [PATCH datacenter-manager 03/15] ui: dashboard: refactor remote panel creation " Dominik Csapak
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 ` [pdm-devel] [PATCH datacenter-manager 05/15] ui: dashboard: refactor sdn panel creation into its own module Dominik Csapak
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 ` [pdm-devel] [PATCH datacenter-manager 07/15] ui: dashboard: refactor subscription " Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 08/15] ui: dashboard: refactor top entities " 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
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 ` [pdm-devel] [PATCH datacenter-manager 11/15] ui: dashboard: remove unused remote list Dominik Csapak
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 ` [pdm-devel] [PATCH datacenter-manager 13/15] ui: introduce `LoadResult` helper type Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 14/15] ui: dashboard: implement 'View' Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 15/15] ui: dashboard: use 'View' instead of the Dashboard Dominik Csapak
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox