* [PATCH datacenter-manager 0/4] add resource gauge panels to dashboard/views
@ 2026-03-23 11:03 Dominik Csapak
2026-03-23 11:03 ` [PATCH datacenter-manager 1/4] api: return global cpu/memory/storage statistics Dominik Csapak
` (4 more replies)
0 siblings, 5 replies; 10+ messages in thread
From: Dominik Csapak @ 2026-03-23 11:03 UTC (permalink / raw)
To: pdm-devel
This uses the new pie charts[0] to add gauge panels for resources to
the dashboards/views. Either combined cpu/memory/storage or
indidivually, for pve/pbs or combined counts.
for this we have to sum the data up in the backend.
I also added these to the default dashboard (since it's data from an api
call we already query) but put that in a separate patch so we can easily
decide to not apply that. (not sure if we want to change the default
dashboard)
Note that the pwt patches [0] have to be applied and the package
has to be bumped first.
0: https://lore.proxmox.com/yew-devel/20260320160816.4113364-1-d.csapak@proxmox.com/
Dominik Csapak (4):
api: return global cpu/memory/storage statistics
ui: css: use mask for svg icons
ui: dashboard: add new gauge panels widget type
ui: dashboard: add resource gauges to default dashboard
lib/pdm-api-types/src/lib.rs | 2 +-
lib/pdm-api-types/src/resource.rs | 27 ++++++
lib/pdm-api-types/src/views.rs | 15 +++
server/src/api/resources.rs | 65 ++++++++++---
ui/css/pdm.scss | 35 +++----
ui/src/dashboard/gauge_panel.rs | 156 ++++++++++++++++++++++++++++++
ui/src/dashboard/mod.rs | 3 +
ui/src/dashboard/view.rs | 19 +++-
ui/src/dashboard/view/row_view.rs | 43 +++++++-
9 files changed, 331 insertions(+), 34 deletions(-)
create mode 100644 ui/src/dashboard/gauge_panel.rs
--
2.47.3
^ permalink raw reply [flat|nested] 10+ messages in thread* [PATCH datacenter-manager 1/4] api: return global cpu/memory/storage statistics 2026-03-23 11:03 [PATCH datacenter-manager 0/4] add resource gauge panels to dashboard/views Dominik Csapak @ 2026-03-23 11:03 ` Dominik Csapak 2026-03-25 16:49 ` Thomas Lamprecht 2026-03-23 11:03 ` [PATCH datacenter-manager 2/4] ui: css: use mask for svg icons Dominik Csapak ` (3 subsequent siblings) 4 siblings, 1 reply; 10+ messages in thread From: Dominik Csapak @ 2026-03-23 11:03 UTC (permalink / raw) To: pdm-devel Global CPU/memory/storage usage (per remote type) is useful and interesting from an administration POV. Calculate and return these so we can use them on the dashboards. Signed-off-by: Dominik Csapak <d.csapak@proxmox.com> --- lib/pdm-api-types/src/lib.rs | 2 +- lib/pdm-api-types/src/resource.rs | 27 +++++++++++++ server/src/api/resources.rs | 65 ++++++++++++++++++++++++------- 3 files changed, 80 insertions(+), 14 deletions(-) diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs index d4cc7ef0..9bccd50f 100644 --- a/lib/pdm-api-types/src/lib.rs +++ b/lib/pdm-api-types/src/lib.rs @@ -191,7 +191,7 @@ pub const PVE_STORAGE_ID_SCHEMA: Schema = StringSchema::new("Storage ID.") // Complex type definitions #[api()] -#[derive(Default, Serialize, Deserialize)] +#[derive(Default, Serialize, Deserialize, PartialEq, Clone)] /// Storage space usage information. pub struct StorageStatus { /// Total space (bytes). diff --git a/lib/pdm-api-types/src/resource.rs b/lib/pdm-api-types/src/resource.rs index d2db3b5a..1f74e09c 100644 --- a/lib/pdm-api-types/src/resource.rs +++ b/lib/pdm-api-types/src/resource.rs @@ -6,6 +6,8 @@ use serde::{Deserialize, Serialize}; use proxmox_schema::{api, ApiStringFormat, ApiType, EnumEntry, OneOfSchema, Schema, StringSchema}; use super::remotes::{RemoteType, REMOTE_ID_SCHEMA}; +use super::StorageStatus; + use pve_api_types::ClusterResourceNetworkType; /// High PBS datastore usage threshold @@ -666,6 +668,18 @@ pub struct SdnZoneCount { pub unknown: u64, } +#[api] +#[derive(Default, Serialize, Deserialize, Clone, PartialEq)] +/// Statistics for CPU utilization +pub struct CpuStatistics { + /// Amount of threads utilized + pub used: f64, + /// Amount of physically available cpu threads + pub max: f64, + /// Currently allocated cores of running guests (only on PVE) + pub allocated: Option<f64>, +} + #[api( properties: { "failed_remotes_list": { @@ -697,6 +711,19 @@ pub struct ResourcesStatus { pub pbs_nodes: NodeStatusCount, /// Status of PBS Datastores pub pbs_datastores: PbsDatastoreStatusCount, + /// Combined CPU statistics for all PVE remotes + pub pve_cpu_stats: CpuStatistics, + /// Combined CPU statistics for all PBS remotes + pub pbs_cpu_stats: CpuStatistics, + /// Combined Memory statistics for all PVE remotes + pub pve_memory_stats: StorageStatus, + /// Combined Memory statistics for all PBS remotes + pub pbs_memory_stats: StorageStatus, + /// Combined Storage statistics for all PVE remotes (shared storages are only counted once per + /// remote). + pub pve_storage_stats: StorageStatus, + /// Combined Storage statistics for all PBS remotes + pub pbs_storage_stats: StorageStatus, /// List of the failed remotes including type and error #[serde(default, skip_serializing_if = "Vec::is_empty")] pub failed_remotes_list: Vec<FailedRemote>, diff --git a/server/src/api/resources.rs b/server/src/api/resources.rs index 5cb67bf5..04628a81 100644 --- a/server/src/api/resources.rs +++ b/server/src/api/resources.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::str::FromStr; use std::sync::{LazyLock, RwLock}; @@ -468,6 +468,7 @@ pub async fn get_status( let remotes_with_resources = get_resources_impl(max_age, None, None, view.as_deref(), Some(rpcenv)).await?; let mut counts = ResourcesStatus::default(); + let mut pve_cpu_allocated = 0.0; for remote_with_resources in remotes_with_resources { if let Some(err) = remote_with_resources.error { counts.failed_remotes += 1; @@ -479,29 +480,52 @@ pub async fn get_status( } else { counts.remotes += 1; } + let mut seen_storages = HashSet::new(); for resource in remote_with_resources.resources { match resource { - Resource::PveStorage(r) => match r.status.as_str() { - "available" => counts.storages.available += 1, - _ => counts.storages.unknown += 1, - }, + Resource::PveStorage(r) => { + match r.status.as_str() { + "available" => counts.storages.available += 1, + _ => counts.storages.unknown += 1, + } + if !r.shared || !seen_storages.contains(&r.storage) { + counts.pve_storage_stats.total += r.maxdisk; + counts.pve_storage_stats.used += r.disk; + counts.pve_storage_stats.avail += r.maxdisk - r.disk; + seen_storages.insert(r.storage); + } + } Resource::PveQemu(r) => match r.status.as_str() { _ if r.template => counts.qemu.template += 1, - "running" => counts.qemu.running += 1, + "running" => { + counts.qemu.running += 1; + pve_cpu_allocated += r.maxcpu; + } "stopped" => counts.qemu.stopped += 1, _ => counts.qemu.unknown += 1, }, Resource::PveLxc(r) => match r.status.as_str() { _ if r.template => counts.lxc.template += 1, - "running" => counts.lxc.running += 1, + "running" => { + counts.lxc.running += 1; + pve_cpu_allocated += r.maxcpu; + } "stopped" => counts.lxc.stopped += 1, _ => counts.lxc.unknown += 1, }, - Resource::PveNode(r) => match r.status.as_str() { - "online" => counts.pve_nodes.online += 1, - "offline" => counts.pve_nodes.offline += 1, - _ => counts.pve_nodes.unknown += 1, - }, + Resource::PveNode(r) => { + match r.status.as_str() { + "online" => counts.pve_nodes.online += 1, + "offline" => counts.pve_nodes.offline += 1, + _ => counts.pve_nodes.unknown += 1, + } + counts.pve_cpu_stats.used += r.cpu * r.maxcpu; + counts.pve_cpu_stats.max += r.maxcpu; + + counts.pve_memory_stats.total += r.maxmem; + counts.pve_memory_stats.used += r.mem; + counts.pve_memory_stats.avail += r.maxmem - r.mem; + } Resource::PveNetwork(r) => { if let PveNetworkResource::Zone(zone) = r { match zone.status() { @@ -521,7 +545,16 @@ pub async fn get_status( } } // FIXME better status for pbs/datastores - Resource::PbsNode(_) => counts.pbs_nodes.online += 1, + Resource::PbsNode(r) => { + counts.pbs_nodes.online += 1; + + counts.pbs_cpu_stats.used += r.cpu * r.maxcpu; + counts.pbs_cpu_stats.max += r.maxcpu; + + counts.pbs_memory_stats.total += r.maxmem; + counts.pbs_memory_stats.used += r.mem; + counts.pbs_memory_stats.avail += r.maxmem - r.mem; + } Resource::PbsDatastore(r) => { if r.maintenance.is_none() { counts.pbs_datastores.online += 1; @@ -546,11 +579,17 @@ pub async fn get_status( } _ => (), } + + counts.pbs_storage_stats.total += r.maxdisk; + counts.pbs_storage_stats.used += r.disk; + counts.pbs_storage_stats.avail += r.maxdisk - r.disk; } } } } + counts.pve_cpu_stats.allocated = Some(pve_cpu_allocated); + Ok(counts) } -- 2.47.3 ^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH datacenter-manager 1/4] api: return global cpu/memory/storage statistics 2026-03-23 11:03 ` [PATCH datacenter-manager 1/4] api: return global cpu/memory/storage statistics Dominik Csapak @ 2026-03-25 16:49 ` Thomas Lamprecht 2026-03-26 7:59 ` Dominik Csapak 0 siblings, 1 reply; 10+ messages in thread From: Thomas Lamprecht @ 2026-03-25 16:49 UTC (permalink / raw) To: Dominik Csapak, pdm-devel Am 23.03.26 um 12:06 schrieb Dominik Csapak: > Global CPU/memory/storage usage (per remote type) is useful and > interesting from an administration POV. Calculate and return these so > we can use them on the dashboards. > > Signed-off-by: Dominik Csapak <d.csapak@proxmox.com> > --- > lib/pdm-api-types/src/lib.rs | 2 +- > lib/pdm-api-types/src/resource.rs | 27 +++++++++++++ > server/src/api/resources.rs | 65 ++++++++++++++++++++++++------- > 3 files changed, 80 insertions(+), 14 deletions(-) > > diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs > index d4cc7ef0..9bccd50f 100644 > --- a/lib/pdm-api-types/src/lib.rs > +++ b/lib/pdm-api-types/src/lib.rs > @@ -191,7 +191,7 @@ pub const PVE_STORAGE_ID_SCHEMA: Schema = StringSchema::new("Storage ID.") > // Complex type definitions > > #[api()] > -#[derive(Default, Serialize, Deserialize)] > +#[derive(Default, Serialize, Deserialize, PartialEq, Clone)] > /// Storage space usage information. > pub struct StorageStatus { > /// Total space (bytes). > diff --git a/lib/pdm-api-types/src/resource.rs b/lib/pdm-api-types/src/resource.rs > index d2db3b5a..1f74e09c 100644 > --- a/lib/pdm-api-types/src/resource.rs > +++ b/lib/pdm-api-types/src/resource.rs > @@ -666,6 +668,18 @@ pub struct SdnZoneCount { > pub unknown: u64, > } > > +#[api] > +#[derive(Default, Serialize, Deserialize, Clone, PartialEq)] > +/// Statistics for CPU utilization > +pub struct CpuStatistics { > + /// Amount of threads utilized > + pub used: f64, > + /// Amount of physically available cpu threads > + pub max: f64, > + /// Currently allocated cores of running guests (only on PVE) > + pub allocated: Option<f64>, > +} > + > #[api( > properties: { > "failed_remotes_list": { > @@ -697,6 +711,19 @@ pub struct ResourcesStatus { > pub pbs_nodes: NodeStatusCount, > /// Status of PBS Datastores > pub pbs_datastores: PbsDatastoreStatusCount, > + /// Combined CPU statistics for all PVE remotes > + pub pve_cpu_stats: CpuStatistics, > + /// Combined CPU statistics for all PBS remotes > + pub pbs_cpu_stats: CpuStatistics, > + /// Combined Memory statistics for all PVE remotes > + pub pve_memory_stats: StorageStatus, > + /// Combined Memory statistics for all PBS remotes > + pub pbs_memory_stats: StorageStatus, should above two memory fields also use a type named MemoryStatus or the like, or is this reused because the fields are basically the same anyway? A type alias might still make sense to have in the latter case for more clarity that this is on purpose and nothing against dedicated types even if they got the same fields, as they refer to different things nonetheless, as e.g. a hypothetical Display impl would likely differ (IEC vs SI units). > + /// Combined Storage statistics for all PVE remotes (shared storages are only counted once per > + /// remote). > + pub pve_storage_stats: StorageStatus, > + /// Combined Storage statistics for all PBS remotes > + pub pbs_storage_stats: StorageStatus, > /// List of the failed remotes including type and error > #[serde(default, skip_serializing_if = "Vec::is_empty")] > pub failed_remotes_list: Vec<FailedRemote>, ^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH datacenter-manager 1/4] api: return global cpu/memory/storage statistics 2026-03-25 16:49 ` Thomas Lamprecht @ 2026-03-26 7:59 ` Dominik Csapak 0 siblings, 0 replies; 10+ messages in thread From: Dominik Csapak @ 2026-03-26 7:59 UTC (permalink / raw) To: Thomas Lamprecht, pdm-devel On 3/25/26 5:48 PM, Thomas Lamprecht wrote: > Am 23.03.26 um 12:06 schrieb Dominik Csapak: >> Global CPU/memory/storage usage (per remote type) is useful and >> interesting from an administration POV. Calculate and return these so >> we can use them on the dashboards. >> >> Signed-off-by: Dominik Csapak <d.csapak@proxmox.com> >> --- >> lib/pdm-api-types/src/lib.rs | 2 +- >> lib/pdm-api-types/src/resource.rs | 27 +++++++++++++ >> server/src/api/resources.rs | 65 ++++++++++++++++++++++++------- >> 3 files changed, 80 insertions(+), 14 deletions(-) >> >> diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs >> index d4cc7ef0..9bccd50f 100644 >> --- a/lib/pdm-api-types/src/lib.rs >> +++ b/lib/pdm-api-types/src/lib.rs >> @@ -191,7 +191,7 @@ pub const PVE_STORAGE_ID_SCHEMA: Schema = StringSchema::new("Storage ID.") >> // Complex type definitions >> >> #[api()] >> -#[derive(Default, Serialize, Deserialize)] >> +#[derive(Default, Serialize, Deserialize, PartialEq, Clone)] >> /// Storage space usage information. >> pub struct StorageStatus { >> /// Total space (bytes). >> diff --git a/lib/pdm-api-types/src/resource.rs b/lib/pdm-api-types/src/resource.rs >> index d2db3b5a..1f74e09c 100644 >> --- a/lib/pdm-api-types/src/resource.rs >> +++ b/lib/pdm-api-types/src/resource.rs > >> @@ -666,6 +668,18 @@ pub struct SdnZoneCount { >> pub unknown: u64, >> } >> >> +#[api] >> +#[derive(Default, Serialize, Deserialize, Clone, PartialEq)] >> +/// Statistics for CPU utilization >> +pub struct CpuStatistics { >> + /// Amount of threads utilized >> + pub used: f64, >> + /// Amount of physically available cpu threads >> + pub max: f64, >> + /// Currently allocated cores of running guests (only on PVE) >> + pub allocated: Option<f64>, >> +} >> + >> #[api( >> properties: { >> "failed_remotes_list": { >> @@ -697,6 +711,19 @@ pub struct ResourcesStatus { >> pub pbs_nodes: NodeStatusCount, >> /// Status of PBS Datastores >> pub pbs_datastores: PbsDatastoreStatusCount, >> + /// Combined CPU statistics for all PVE remotes >> + pub pve_cpu_stats: CpuStatistics, >> + /// Combined CPU statistics for all PBS remotes >> + pub pbs_cpu_stats: CpuStatistics, >> + /// Combined Memory statistics for all PVE remotes >> + pub pve_memory_stats: StorageStatus, >> + /// Combined Memory statistics for all PBS remotes >> + pub pbs_memory_stats: StorageStatus, > > should above two memory fields also use a type named MemoryStatus or the > like, or is this reused because the fields are basically the same anyway? > A type alias might still make sense to have in the latter case for more > clarity that this is on purpose and nothing against dedicated types even > if they got the same fields, as they refer to different things nonetheless, > as e.g. a hypothetical Display impl would likely differ (IEC vs SI units). I just reused it because of the same fields, but having a different struct makes yeah. > >> + /// Combined Storage statistics for all PVE remotes (shared storages are only counted once per >> + /// remote). >> + pub pve_storage_stats: StorageStatus, >> + /// Combined Storage statistics for all PBS remotes >> + pub pbs_storage_stats: StorageStatus, >> /// List of the failed remotes including type and error >> #[serde(default, skip_serializing_if = "Vec::is_empty")] >> pub failed_remotes_list: Vec<FailedRemote>, > ^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH datacenter-manager 2/4] ui: css: use mask for svg icons 2026-03-23 11:03 [PATCH datacenter-manager 0/4] add resource gauge panels to dashboard/views Dominik Csapak 2026-03-23 11:03 ` [PATCH datacenter-manager 1/4] api: return global cpu/memory/storage statistics Dominik Csapak @ 2026-03-23 11:03 ` Dominik Csapak 2026-03-23 11:03 ` [PATCH datacenter-manager 3/4] ui: dashboard: add new gauge panels widget type Dominik Csapak ` (2 subsequent siblings) 4 siblings, 0 replies; 10+ messages in thread From: Dominik Csapak @ 2026-03-23 11:03 UTC (permalink / raw) To: pdm-devel That way the color can be overwritten instead of relying on the svg color and using invert for dark mode. We already did this (somewhat) for the sdn icons, so do it here too. Aligning of the icons to text isn't always perfect, but that is something we can improve later too. Signed-off-by: Dominik Csapak <d.csapak@proxmox.com> --- ui/css/pdm.scss | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/ui/css/pdm.scss b/ui/css/pdm.scss index a15b4e8e..a6f25210 100644 --- a/ui/css/pdm.scss +++ b/ui/css/pdm.scss @@ -37,35 +37,38 @@ .fa-cpu::before { content: " "; - background-image: url(./images/icon-cpu.svg); - background-size: 16px 16px; - background-repeat: no-repeat; + mask-image: url(./images/icon-cpu.svg); + mask-size: 16px 16px; + mask-repeat: no-repeat; width: 16px; height: 16px; vertical-align: bottom; display: inline-block; + background-color: var(--pwt-color); } .fa-memory::before { content: " "; - background-image: url(./images/icon-memory.svg); - background-size: 16px 16px; - background-repeat: no-repeat; + mask-image: url(./images/icon-memory.svg); + mask-size: 16px 16px; + mask-repeat: no-repeat; width: 16px; height: 16px; vertical-align: bottom; display: inline-block; + background-color: var(--pwt-color); } .fa-cdrom::before { content: " "; - background-image: url(./images/icon-cd-drive.svg); - background-size: 16px 16px; - background-repeat: no-repeat; + mask-image: url(./images/icon-cd-drive.svg); + mask-size: 16px 16px; + mask-repeat: no-repeat; width: 16px; height: 16px; vertical-align: bottom; display: inline-block; + background-color: var(--pwt-color); } .fa-sdn-vnet::before { @@ -93,6 +96,9 @@ } .pwt-nav-menu .pwt-nav-link.active{ + .fa-cdrom:before, + .fa-memory:before, + .fa-cpu:before, .fa-sdn:before, .fa-sdn-vnet:before { background-color: var(--pwt-accent-color); @@ -100,20 +106,15 @@ } .pwt-panel-header-text{ + .fa-cdrom:before, + .fa-memory:before, + .fa-cpu:before, .fa-sdn:before, .fa-sdn-vnet:before { background-color: var(--pwt-accent-color-background); } } -:root.pwt-dark-mode { - .fa-cdrom, - .fa-memory, - .fa-cpu { - filter: invert(90%); - } -} - .proxmox-content-spacer { @include color-scheme-vars("surface"); color: var(--pwt-color); -- 2.47.3 ^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH datacenter-manager 3/4] ui: dashboard: add new gauge panels widget type 2026-03-23 11:03 [PATCH datacenter-manager 0/4] add resource gauge panels to dashboard/views Dominik Csapak 2026-03-23 11:03 ` [PATCH datacenter-manager 1/4] api: return global cpu/memory/storage statistics Dominik Csapak 2026-03-23 11:03 ` [PATCH datacenter-manager 2/4] ui: css: use mask for svg icons Dominik Csapak @ 2026-03-23 11:03 ` Dominik Csapak 2026-03-23 11:03 ` [PATCH datacenter-manager 4/4] ui: dashboard: add resource gauges to default dashboard Dominik Csapak 2026-03-24 10:25 ` [PATCH datacenter-manager 0/4] add resource gauge panels to dashboard/views Lukas Wagner 4 siblings, 0 replies; 10+ messages in thread From: Dominik Csapak @ 2026-03-23 11:03 UTC (permalink / raw) To: pdm-devel This adds gauge chart panels for cpu/memory/storage. Either individually or combined all three. For either PVE hosts, PBS hosts, or combined. We use the new pie chart component for this. Signed-off-by: Dominik Csapak <d.csapak@proxmox.com> --- lib/pdm-api-types/src/views.rs | 15 +++ ui/src/dashboard/gauge_panel.rs | 156 ++++++++++++++++++++++++++++++ ui/src/dashboard/mod.rs | 3 + ui/src/dashboard/view.rs | 9 +- ui/src/dashboard/view/row_view.rs | 43 +++++++- 5 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 ui/src/dashboard/gauge_panel.rs diff --git a/lib/pdm-api-types/src/views.rs b/lib/pdm-api-types/src/views.rs index 5e9b4b31..50181e72 100644 --- a/lib/pdm-api-types/src/views.rs +++ b/lib/pdm-api-types/src/views.rs @@ -266,6 +266,14 @@ pub struct RowWidget { pub r#type: WidgetType, } +#[derive(Serialize, Deserialize, PartialEq, Clone, Copy)] +#[serde(rename_all = "kebab-case")] +pub enum NodeResourceType { + Cpu, + Memory, + Storage, +} + #[derive(Serialize, Deserialize, PartialEq, Clone)] #[serde(rename_all = "kebab-case")] #[serde(tag = "widget-type")] @@ -295,6 +303,13 @@ pub enum WidgetType { grouping: TaskSummaryGrouping, }, ResourceTree, + #[serde(rename_all = "kebab-case")] + NodeResourceGauge { + #[serde(skip_serializing_if = "Option::is_none")] + resource: Option<NodeResourceType>, + #[serde(skip_serializing_if = "Option::is_none")] + remote_type: Option<RemoteType>, + }, } #[derive(Serialize, Deserialize, PartialEq, Clone, Copy)] diff --git a/ui/src/dashboard/gauge_panel.rs b/ui/src/dashboard/gauge_panel.rs new file mode 100644 index 00000000..89094f65 --- /dev/null +++ b/ui/src/dashboard/gauge_panel.rs @@ -0,0 +1,156 @@ +use anyhow::Error; + +use proxmox_human_byte::HumanByte; +use pwt::css; +use pwt::prelude::*; +use pwt::state::SharedState; +use pwt::widget::Fa; +use pwt::widget::{charts::PieChart, Panel}; +use pwt::widget::{error_message, Column, Container, Row}; + +use pdm_api_types::remotes::RemoteType; +use pdm_api_types::{resource::ResourcesStatus, views::NodeResourceType}; + +use crate::dashboard::{create_title_with_icon, loading_column}; +use crate::LoadResult; + +pub fn create_gauge_panel( + resource_type: Option<NodeResourceType>, + remote_type: Option<RemoteType>, + status: SharedState<LoadResult<ResourcesStatus, Error>>, +) -> Panel { + let status = status.read(); + let (show_cpu, show_mem, show_storage, title, subtitle, icon) = match resource_type { + Some(NodeResourceType::Cpu) => (true, false, false, tr!("CPU Usage"), false, "cpu"), + Some(NodeResourceType::Memory) => { + (false, true, false, tr!("Memory Usage"), false, "memory") + } + Some(NodeResourceType::Storage) => { + (false, false, true, tr!("Storage Usage"), false, "database") + } + None => (true, true, true, tr!("Resource Usage"), true, "tachometer"), + }; + + let suffix = match remote_type { + Some(RemoteType::Pve) => " - PVE", + Some(RemoteType::Pbs) => " - PBS", + None => "", + }; + + let is_loading = !status.has_data(); + + Panel::new() + .title(create_title_with_icon(icon, format!("{title}{suffix}"))) + .border(true) + .with_optional_child(status.data.as_ref().map(|data| { + let (cpu, mem, storage) = match remote_type { + Some(RemoteType::Pve) => ( + show_cpu.then_some((data.pve_cpu_stats.used, data.pve_cpu_stats.max)), + show_mem.then_some((data.pve_memory_stats.used, data.pve_memory_stats.total)), + show_storage + .then_some((data.pve_storage_stats.used, data.pve_storage_stats.total)), + ), + Some(RemoteType::Pbs) => ( + show_cpu.then_some((data.pbs_cpu_stats.used, data.pbs_cpu_stats.max)), + show_mem.then_some((data.pbs_memory_stats.used, data.pbs_memory_stats.total)), + show_storage + .then_some((data.pbs_storage_stats.used, data.pbs_storage_stats.total)), + ), + None => ( + show_cpu.then_some(( + data.pve_cpu_stats.used + data.pbs_cpu_stats.used, + data.pve_cpu_stats.max + data.pbs_cpu_stats.max, + )), + show_mem.then_some(( + data.pve_memory_stats.used + data.pbs_memory_stats.used, + data.pve_memory_stats.total + data.pbs_memory_stats.total, + )), + show_storage.then_some(( + data.pve_storage_stats.used + data.pbs_storage_stats.used, + data.pve_storage_stats.total + data.pbs_storage_stats.total, + )), + ), + }; + + let chart = |percentage: f64, icon: Fa, title: String, extra_text: String| -> Column { + Column::new() + .flex(1.0) + .width("0") // correct flex base size for calculation + .max_height(250) + .with_optional_child( + subtitle.then_some( + Row::new() + .gap(1) + .class(css::AlignItems::Center) + .class(css::JustifyContent::Center) + .with_child(icon) + .with_child(&title), + ), + ) + .with_child( + PieChart::new(title, percentage) + .class(css::Overflow::Auto) + .text(format!("{:.2}%", percentage * 100.)) + .angle_start(45.0) + .angle_end(315.0) + .show_tooltip(false), + ) + .with_child( + Container::new() + .class(css::TextAlign::Center) + .with_child(extra_text), + ) + }; + + Row::new() + .padding(4) + .with_optional_child(cpu.map(|(used, total)| { + let pct = if total == 0.0 { 0.0 } else { used / total }; + let extra_text = match remote_type { + Some(RemoteType::Pve) => { + tr!( + "{0} of {1} ({2} allocated)", + format!("{used:.2}"), + format!("{total:.0}"), + format!("{:.0}", data.pve_cpu_stats.allocated.unwrap_or(0.0)), + ) + } + _ => { + tr!("{0} of {1}", format!("{used:.2}"), format!("{total:.0}")) + } + }; + chart(pct, Fa::new("cpu"), tr!("CPU"), extra_text) + })) + .with_optional_child(mem.map(|(used, total)| { + chart( + if total == 0 { + 0.0 + } else { + used as f64 / total as f64 + }, + Fa::new("memory"), + tr!("Memory"), + tr!("{0} of {1}", HumanByte::from(used), HumanByte::from(total)), + ) + })) + .with_optional_child(storage.map(|(used, total)| { + chart( + if total == 0 { + 0.0 + } else { + used as f64 / total as f64 + }, + Fa::new("database"), + tr!("Storage"), + tr!("{0} of {1}", HumanByte::from(used), HumanByte::from(total)), + ) + })) + })) + .with_optional_child(is_loading.then_some(loading_column())) + .with_optional_child( + status + .error + .as_ref() + .map(|err| error_message(&err.to_string())), + ) +} diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs index 17e5ccd3..194000c2 100644 --- a/ui/src/dashboard/mod.rs +++ b/ui/src/dashboard/mod.rs @@ -14,6 +14,9 @@ pub use subscriptions_list::SubscriptionsList; mod remote_panel; pub use remote_panel::create_remote_panel; +mod gauge_panel; +pub use gauge_panel::create_gauge_panel; + mod guest_panel; pub use guest_panel::create_guest_panel; diff --git a/ui/src/dashboard/view.rs b/ui/src/dashboard/view.rs index 4a0400ac..eb1a348e 100644 --- a/ui/src/dashboard/view.rs +++ b/ui/src/dashboard/view.rs @@ -22,7 +22,7 @@ use crate::dashboard::refresh_config_edit::{ use crate::dashboard::subscription_info::create_subscriptions_dialog; use crate::dashboard::tasks::get_task_options; use crate::dashboard::{ - create_guest_panel, create_node_panel, create_pbs_datastores_panel, + create_gauge_panel, create_guest_panel, create_node_panel, create_pbs_datastores_panel, create_refresh_config_edit_window, create_remote_panel, create_resource_tree, create_sdn_panel, create_subscription_panel, create_task_summary_panel, create_top_entities_panel, DashboardStatusRow, @@ -167,6 +167,10 @@ fn render_widget( create_task_summary_panel(statistics, remotes, hours, since) } WidgetType::ResourceTree => create_resource_tree(redraw_controller), + WidgetType::NodeResourceGauge { + resource, + remote_type, + } => create_gauge_panel(*resource, *remote_type, status), }; if let Some(title) = &item.title { @@ -268,7 +272,8 @@ fn required_api_calls(layout: &ViewLayout) -> (bool, bool, bool) { | WidgetType::Guests { .. } | WidgetType::Remotes { .. } | WidgetType::Sdn - | WidgetType::PbsDatastores => { + | WidgetType::PbsDatastores + | WidgetType::NodeResourceGauge { .. } => { status = true; } WidgetType::Subscription => { diff --git a/ui/src/dashboard/view/row_view.rs b/ui/src/dashboard/view/row_view.rs index 673b4627..f220f954 100644 --- a/ui/src/dashboard/view/row_view.rs +++ b/ui/src/dashboard/view/row_view.rs @@ -21,7 +21,7 @@ use crate::dashboard::view::EditingMessage; use pdm_api_types::remotes::RemoteType; use pdm_api_types::views::{ - LeaderboardType, RowWidget, TaskSummaryGrouping, ViewLayout, WidgetType, + LeaderboardType, NodeResourceType, RowWidget, TaskSummaryGrouping, ViewLayout, WidgetType, }; #[derive(Properties, PartialEq)] @@ -539,6 +539,35 @@ fn create_menu(ctx: &yew::Context<RowViewComp>, new_coords: Position) -> Menu { ctx.link() .callback(move |_| Msg::AddWidget(new_coords, widget.clone())) }; + let create_gauge_menu = |remote_type: Option<RemoteType>| -> Menu { + Menu::new() + .with_item( + MenuItem::new(tr!("All Resources")).on_select(create_callback( + WidgetType::NodeResourceGauge { + resource: None, + remote_type, + }, + )), + ) + .with_item(MenuItem::new(tr!("CPU")).on_select(create_callback( + WidgetType::NodeResourceGauge { + resource: Some(NodeResourceType::Cpu), + remote_type, + }, + ))) + .with_item(MenuItem::new(tr!("Memory")).on_select(create_callback( + WidgetType::NodeResourceGauge { + resource: Some(NodeResourceType::Memory), + remote_type, + }, + ))) + .with_item(MenuItem::new(tr!("Storage")).on_select(create_callback( + WidgetType::NodeResourceGauge { + resource: Some(NodeResourceType::Storage), + remote_type, + }, + ))) + }; Menu::new() .with_item( MenuItem::new(tr!("Remote Panel")) @@ -642,4 +671,16 @@ fn create_menu(ctx: &yew::Context<RowViewComp>, new_coords: Position) -> Menu { MenuItem::new(tr!("Resource Tree")) .on_select(create_callback(WidgetType::ResourceTree)), ) + .with_item( + MenuItem::new(tr!("Resource Usage")).menu( + Menu::new() + .with_item(MenuItem::new(tr!("All")).menu(create_gauge_menu(None))) + .with_item( + MenuItem::new(tr!("PVE")).menu(create_gauge_menu(Some(RemoteType::Pve))), + ) + .with_item( + MenuItem::new(tr!("PBS")).menu(create_gauge_menu(Some(RemoteType::Pbs))), + ), + ), + ) } -- 2.47.3 ^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH datacenter-manager 4/4] ui: dashboard: add resource gauges to default dashboard 2026-03-23 11:03 [PATCH datacenter-manager 0/4] add resource gauge panels to dashboard/views Dominik Csapak ` (2 preceding siblings ...) 2026-03-23 11:03 ` [PATCH datacenter-manager 3/4] ui: dashboard: add new gauge panels widget type Dominik Csapak @ 2026-03-23 11:03 ` Dominik Csapak 2026-03-24 10:25 ` [PATCH datacenter-manager 0/4] add resource gauge panels to dashboard/views Lukas Wagner 4 siblings, 0 replies; 10+ messages in thread From: Dominik Csapak @ 2026-03-23 11:03 UTC (permalink / raw) To: pdm-devel Add a new extra second row: a gauges panel for PVE and one for PBS Signed-off-by: Dominik Csapak <d.csapak@proxmox.com> --- ui/src/dashboard/view.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ui/src/dashboard/view.rs b/ui/src/dashboard/view.rs index eb1a348e..81810664 100644 --- a/ui/src/dashboard/view.rs +++ b/ui/src/dashboard/view.rs @@ -596,6 +596,16 @@ const DEFAULT_DASHBOARD: &str = " \"widget-type\": \"subscription\" } ], + [ + { + \"widget-type\": \"node-resource-gauge\", + \"remote-type\": \"pve\" + }, + { + \"widget-type\": \"node-resource-gauge\", + \"remote-type\": \"pbs\" + } + ], [ { \"widget-type\": \"leaderboard\", -- 2.47.3 ^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH datacenter-manager 0/4] add resource gauge panels to dashboard/views 2026-03-23 11:03 [PATCH datacenter-manager 0/4] add resource gauge panels to dashboard/views Dominik Csapak ` (3 preceding siblings ...) 2026-03-23 11:03 ` [PATCH datacenter-manager 4/4] ui: dashboard: add resource gauges to default dashboard Dominik Csapak @ 2026-03-24 10:25 ` Lukas Wagner 2026-03-25 11:48 ` Thomas Lamprecht 4 siblings, 1 reply; 10+ messages in thread From: Lukas Wagner @ 2026-03-24 10:25 UTC (permalink / raw) To: Dominik Csapak, pdm-devel On Mon Mar 23, 2026 at 12:03 PM CET, Dominik Csapak wrote: > This uses the new pie charts[0] to add gauge panels for resources to > the dashboards/views. Either combined cpu/memory/storage or > indidivually, for pve/pbs or combined counts. > > for this we have to sum the data up in the backend. > > I also added these to the default dashboard (since it's data from an api > call we already query) but put that in a separate patch so we can easily > decide to not apply that. (not sure if we want to change the default > dashboard) > > Note that the pwt patches [0] have to be applied and the package > has to be bumped first. > > 0: https://lore.proxmox.com/yew-devel/20260320160816.4113364-1-d.csapak@proxmox.com/ > > Dominik Csapak (4): > api: return global cpu/memory/storage statistics > ui: css: use mask for svg icons > ui: dashboard: add new gauge panels widget type > ui: dashboard: add resource gauges to default dashboard > > lib/pdm-api-types/src/lib.rs | 2 +- > lib/pdm-api-types/src/resource.rs | 27 ++++++ > lib/pdm-api-types/src/views.rs | 15 +++ > server/src/api/resources.rs | 65 ++++++++++--- > ui/css/pdm.scss | 35 +++---- > ui/src/dashboard/gauge_panel.rs | 156 ++++++++++++++++++++++++++++++ > ui/src/dashboard/mod.rs | 3 + > ui/src/dashboard/view.rs | 19 +++- > ui/src/dashboard/view/row_view.rs | 43 +++++++- > 9 files changed, 331 insertions(+), 34 deletions(-) > create mode 100644 ui/src/dashboard/gauge_panel.rs Thanks for these patches! Some thoughts, some of which we already discussed off-list: - I'd suggest a wider opening angle for the gauges, so that the gauges are a bit more compact and look less like a horse shoe To me, 75/285 looked quite good, but this is of course highly subjective, so no hard feelings - For the percent-label inside the gauge, I think 0 (zero) significant digits are okay, for such a 'global' infrastructure gauge two significant digits are hardly useful, I think - We probably should use full product names (Proxmox VE, Proxmox Backup Server) in the card headers, instead of just "PVE" and "PBS". The overall text length could become quite long though, so maybe we need some other approach for the card title. ^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH datacenter-manager 0/4] add resource gauge panels to dashboard/views 2026-03-24 10:25 ` [PATCH datacenter-manager 0/4] add resource gauge panels to dashboard/views Lukas Wagner @ 2026-03-25 11:48 ` Thomas Lamprecht 2026-03-25 13:12 ` Dominik Csapak 0 siblings, 1 reply; 10+ messages in thread From: Thomas Lamprecht @ 2026-03-25 11:48 UTC (permalink / raw) To: Dominik Csapak, pdm-devel Am 24.03.26 um 11:25 schrieb Lukas Wagner: > On Mon Mar 23, 2026 at 12:03 PM CET, Dominik Csapak wrote: >> This uses the new pie charts[0] to add gauge panels for resources to >> the dashboards/views. Either combined cpu/memory/storage or >> indidivually, for pve/pbs or combined counts. >> >> for this we have to sum the data up in the backend. >> >> I also added these to the default dashboard (since it's data from an api >> call we already query) but put that in a separate patch so we can easily >> decide to not apply that. (not sure if we want to change the default >> dashboard) >> >> Note that the pwt patches [0] have to be applied and the package >> has to be bumped first. >> >> 0: https://lore.proxmox.com/yew-devel/20260320160816.4113364-1-d.csapak@proxmox.com/ >> >> Dominik Csapak (4): >> api: return global cpu/memory/storage statistics >> ui: css: use mask for svg icons >> ui: dashboard: add new gauge panels widget type >> ui: dashboard: add resource gauges to default dashboard >> >> lib/pdm-api-types/src/lib.rs | 2 +- >> lib/pdm-api-types/src/resource.rs | 27 ++++++ >> lib/pdm-api-types/src/views.rs | 15 +++ >> server/src/api/resources.rs | 65 ++++++++++--- >> ui/css/pdm.scss | 35 +++---- >> ui/src/dashboard/gauge_panel.rs | 156 ++++++++++++++++++++++++++++++ >> ui/src/dashboard/mod.rs | 3 + >> ui/src/dashboard/view.rs | 19 +++- >> ui/src/dashboard/view/row_view.rs | 43 +++++++- >> 9 files changed, 331 insertions(+), 34 deletions(-) >> create mode 100644 ui/src/dashboard/gauge_panel.rs > > Thanks for these patches! > > Some thoughts, some of which we already discussed off-list: > > - I'd suggest a wider opening angle for the gauges, so that the gauges > are a bit more compact and look less like a horse shoe > To me, 75/285 looked quite good, but this is of course highly > subjective, so no hard feelings Didn't bother me that much when initially looking at it, but you might have a point, especially w.r.t. how the text below the gauge fits visually. As of now the angle of the gauge edges seem slightly "pointy" w.r.t. horizontal line the text gives off (for the lack of a better description). FWIW, Grafana defaults to then moving the value inside a bit lower, so that it basically lines up with the bottom edges of the graph, no hard feelings here, just mentioning it as comparison. > - For the percent-label inside the gauge, I think 0 (zero) significant > digits are okay, for such a 'global' infrastructure gauge two > significant digits are hardly useful, I think one might be OK, at least for <1 and >99 %, and then we might just always add it. but, tbh, using zero for now might be fine too. For large setups a single decimal point in percentage might be still some sizeable real absolute amount though. A space between value and unit might be nice. > > - We probably should use full product names (Proxmox VE, Proxmox > Backup Server) in the card headers, instead of just "PVE" and "PBS". > The overall text length could become quite long though, so maybe we > need some other approach for the card title. +1, but no good solution from top of my head, might be easiest to live with the long texts. Additionally: For widgets multiple gauges placing the icon+label inside or below the gauge might look a bit better. At the top it looks slightly out of place as is. Or written differently: there's now some info on top, in the mid and below, so visually many differnet locations a users eye has to wander, feels "unruhig". Small nit: For PVE CPU Usage it's not immediate clear how the load values and the allocated relay to each other. Font is handled by the SVG, how is the font family and sizing chosen there? does the size always scales nicely? I'll try to reply on the yew-devel series too, but from a quick check I did not found any note w.r.t. this. ^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH datacenter-manager 0/4] add resource gauge panels to dashboard/views 2026-03-25 11:48 ` Thomas Lamprecht @ 2026-03-25 13:12 ` Dominik Csapak 0 siblings, 0 replies; 10+ messages in thread From: Dominik Csapak @ 2026-03-25 13:12 UTC (permalink / raw) To: Thomas Lamprecht, pdm-devel On 3/25/26 12:47 PM, Thomas Lamprecht wrote: > Am 24.03.26 um 11:25 schrieb Lukas Wagner: >> On Mon Mar 23, 2026 at 12:03 PM CET, Dominik Csapak wrote: >>> This uses the new pie charts[0] to add gauge panels for resources to >>> the dashboards/views. Either combined cpu/memory/storage or >>> indidivually, for pve/pbs or combined counts. >>> >>> for this we have to sum the data up in the backend. >>> >>> I also added these to the default dashboard (since it's data from an api >>> call we already query) but put that in a separate patch so we can easily >>> decide to not apply that. (not sure if we want to change the default >>> dashboard) >>> >>> Note that the pwt patches [0] have to be applied and the package >>> has to be bumped first. >>> >>> 0: https://lore.proxmox.com/yew-devel/20260320160816.4113364-1-d.csapak@proxmox.com/ >>> >>> Dominik Csapak (4): >>> api: return global cpu/memory/storage statistics >>> ui: css: use mask for svg icons >>> ui: dashboard: add new gauge panels widget type >>> ui: dashboard: add resource gauges to default dashboard >>> >>> lib/pdm-api-types/src/lib.rs | 2 +- >>> lib/pdm-api-types/src/resource.rs | 27 ++++++ >>> lib/pdm-api-types/src/views.rs | 15 +++ >>> server/src/api/resources.rs | 65 ++++++++++--- >>> ui/css/pdm.scss | 35 +++---- >>> ui/src/dashboard/gauge_panel.rs | 156 ++++++++++++++++++++++++++++++ >>> ui/src/dashboard/mod.rs | 3 + >>> ui/src/dashboard/view.rs | 19 +++- >>> ui/src/dashboard/view/row_view.rs | 43 +++++++- >>> 9 files changed, 331 insertions(+), 34 deletions(-) >>> create mode 100644 ui/src/dashboard/gauge_panel.rs >> >> Thanks for these patches! >> >> Some thoughts, some of which we already discussed off-list: >> >> - I'd suggest a wider opening angle for the gauges, so that the gauges >> are a bit more compact and look less like a horse shoe >> To me, 75/285 looked quite good, but this is of course highly >> subjective, so no hard feelings > > Didn't bother me that much when initially looking at it, but you might have > a point, especially w.r.t. how the text below the gauge fits visually. > As of now the angle of the gauge edges seem slightly "pointy" w.r.t. > horizontal line the text gives off (for the lack of a better description). > > FWIW, Grafana defaults to then moving the value inside a bit lower, so that > it basically lines up with the bottom edges of the graph, no hard feelings > here, just mentioning it as comparison. i talked off list with lukas yesterday, and I don't have anything, against using this wider angle. I'm just used to 3/4 gauges probably. > >> - For the percent-label inside the gauge, I think 0 (zero) significant >> digits are okay, for such a 'global' infrastructure gauge two >> significant digits are hardly useful, I think > > one might be OK, at least for <1 and >99 %, and then we might just always > add it. but, tbh, using zero for now might be fine too. For large setups > a single decimal point in percentage might be still some sizeable real > absolute amount though. > > A space between value and unit might be nice. ok, i'll change it to sthg like '23 %' > >> >> - We probably should use full product names (Proxmox VE, Proxmox >> Backup Server) in the card headers, instead of just "PVE" and "PBS". >> The overall text length could become quite long though, so maybe we >> need some other approach for the card title. > > +1, but no good solution from top of my head, might be easiest to live > with the long texts. yeah i think so too. I'd change it to 'Virtual Environment' and 'Backup Server' like the other widgets. (no need to repeat proxmox here on every widget IMO) > > Additionally: > For widgets multiple gauges placing the icon+label inside or below the > gauge might look a bit better. At the top it looks slightly out of place > as is. Or written differently: there's now some info on top, in the mid > and below, so visually many differnet locations a users eye has to wander, > feels "unruhig". i get what you mean. I'll think about where this fits better (shortly though about simply putting it in the status line, but that makes the text there often too long) > > Small nit: For PVE CPU Usage it's not immediate clear how the load values > and the allocated relay to each other. mhmm do you think it'd be better to leave out the 'allocated' part completely for now? it's a bit of a mixed representation anyway, since e.g. i missed adding that info for memory and it only shows for PVE graphs. otherwise maybe adding a unit/short explanation makes it better? --- 1.5 of 45 threads/cores used (13 allocated to guests) --- (not sure if threads or cores is better here) > > Font is handled by the SVG, how is the font family and sizing chosen there? > does the size always scales nicely? > I'll try to reply on the yew-devel series too, but from a quick check I did > not found any note w.r.t. this. font-family and size is inherited from css, where we set a global font family/size. i chose the svg sizes so that such percentages should fit well inside but one can override that from outside with e.g. 'font-size: 10px' atm, the piechart does not have any configs for text positioning. i didn't need it yet, and auto positioning/sizing (aside from centering) is cumbersome in svg.. (e.g. converting from svg to browser coordinates, etc) ^ permalink raw reply [flat|nested] 10+ messages in thread
end of thread, other threads:[~2026-03-26 7:59 UTC | newest] Thread overview: 10+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-03-23 11:03 [PATCH datacenter-manager 0/4] add resource gauge panels to dashboard/views Dominik Csapak 2026-03-23 11:03 ` [PATCH datacenter-manager 1/4] api: return global cpu/memory/storage statistics Dominik Csapak 2026-03-25 16:49 ` Thomas Lamprecht 2026-03-26 7:59 ` Dominik Csapak 2026-03-23 11:03 ` [PATCH datacenter-manager 2/4] ui: css: use mask for svg icons Dominik Csapak 2026-03-23 11:03 ` [PATCH datacenter-manager 3/4] ui: dashboard: add new gauge panels widget type Dominik Csapak 2026-03-23 11:03 ` [PATCH datacenter-manager 4/4] ui: dashboard: add resource gauges to default dashboard Dominik Csapak 2026-03-24 10:25 ` [PATCH datacenter-manager 0/4] add resource gauge panels to dashboard/views Lukas Wagner 2026-03-25 11:48 ` Thomas Lamprecht 2026-03-25 13:12 ` Dominik Csapak
This is an external index of several public inboxes, see mirroring instructions on how to clone and mirror all data and code used by this external index.