* [PATCH datacenter-manager v2 0/4] add resource gauge panels to dashboard/views
@ 2026-03-30 13:07 Dominik Csapak
2026-03-30 13:07 ` [PATCH datacenter-manager v2 1/4] api: return global cpu/memory/storage statistics Dominik Csapak
` (5 more replies)
0 siblings, 6 replies; 12+ messages in thread
From: Dominik Csapak @ 2026-03-30 13:07 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.
changes from v1:
* use wider angle for the charts
* remove decimal precision in chart
* move 'title' to bottom with the other text
* reformat the cpu text a bit for clarity
* add a 'MemoryStatus' struct (same fields as StorageStatus but with
different description)
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 | 14 ++-
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 | 160 ++++++++++++++++++++++++++++++
ui/src/dashboard/mod.rs | 3 +
ui/src/dashboard/view.rs | 19 +++-
ui/src/dashboard/view/row_view.rs | 43 +++++++-
9 files changed, 347 insertions(+), 34 deletions(-)
create mode 100644 ui/src/dashboard/gauge_panel.rs
--
2.47.3
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH datacenter-manager v2 1/4] api: return global cpu/memory/storage statistics
2026-03-30 13:07 [PATCH datacenter-manager v2 0/4] add resource gauge panels to dashboard/views Dominik Csapak
@ 2026-03-30 13:07 ` Dominik Csapak
2026-04-01 11:34 ` Lukas Wagner
2026-03-30 13:07 ` [PATCH datacenter-manager v2 2/4] ui: css: use mask for svg icons Dominik Csapak
` (4 subsequent siblings)
5 siblings, 1 reply; 12+ messages in thread
From: Dominik Csapak @ 2026-03-30 13:07 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 | 14 ++++++-
lib/pdm-api-types/src/resource.rs | 27 +++++++++++++
server/src/api/resources.rs | 65 ++++++++++++++++++++++++-------
3 files changed, 92 insertions(+), 14 deletions(-)
diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs
index d4cc7ef0..28aed3f4 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).
@@ -202,6 +202,18 @@ pub struct StorageStatus {
pub avail: u64,
}
+#[api()]
+#[derive(Default, Serialize, Deserialize, PartialEq, Clone)]
+/// Storage space usage information.
+pub struct MemoryStatus {
+ /// Total memory size (bytes).
+ pub total: u64,
+ /// Used memory (bytes).
+ pub used: u64,
+ /// Available memory (bytes).
+ pub avail: u64,
+}
+
pub const PASSWORD_HINT_SCHEMA: Schema = StringSchema::new("Password hint.")
.format(&SINGLE_LINE_COMMENT_FORMAT)
.min_length(1)
diff --git a/lib/pdm-api-types/src/resource.rs b/lib/pdm-api-types/src/resource.rs
index d2db3b5a..01a5eb6c 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::{MemoryStatus, 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: MemoryStatus,
+ /// Combined Memory statistics for all PBS remotes
+ pub pbs_memory_stats: MemoryStatus,
+ /// 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] 12+ messages in thread
* [PATCH datacenter-manager v2 2/4] ui: css: use mask for svg icons
2026-03-30 13:07 [PATCH datacenter-manager v2 0/4] add resource gauge panels to dashboard/views Dominik Csapak
2026-03-30 13:07 ` [PATCH datacenter-manager v2 1/4] api: return global cpu/memory/storage statistics Dominik Csapak
@ 2026-03-30 13:07 ` Dominik Csapak
2026-03-30 13:07 ` [PATCH datacenter-manager v2 3/4] ui: dashboard: add new gauge panels widget type Dominik Csapak
` (3 subsequent siblings)
5 siblings, 0 replies; 12+ messages in thread
From: Dominik Csapak @ 2026-03-30 13:07 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] 12+ messages in thread
* [PATCH datacenter-manager v2 3/4] ui: dashboard: add new gauge panels widget type
2026-03-30 13:07 [PATCH datacenter-manager v2 0/4] add resource gauge panels to dashboard/views Dominik Csapak
2026-03-30 13:07 ` [PATCH datacenter-manager v2 1/4] api: return global cpu/memory/storage statistics Dominik Csapak
2026-03-30 13:07 ` [PATCH datacenter-manager v2 2/4] ui: css: use mask for svg icons Dominik Csapak
@ 2026-03-30 13:07 ` Dominik Csapak
2026-04-01 11:34 ` Lukas Wagner
2026-04-01 11:36 ` Lukas Wagner
2026-03-30 13:07 ` [PATCH datacenter-manager v2 4/4] ui: dashboard: add resource gauges to default dashboard Dominik Csapak
` (2 subsequent siblings)
5 siblings, 2 replies; 12+ messages in thread
From: Dominik Csapak @ 2026-03-30 13:07 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 | 160 ++++++++++++++++++++++++++++++
ui/src/dashboard/mod.rs | 3 +
ui/src/dashboard/view.rs | 9 +-
ui/src/dashboard/view/row_view.rs | 43 +++++++-
5 files changed, 227 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..8b30eb0f
--- /dev/null
+++ b/ui/src/dashboard/gauge_panel.rs
@@ -0,0 +1,160 @@
+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) => " - Virtual Environment",
+ Some(RemoteType::Pbs) => " - Backup Server",
+ 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 {
+ let subtitle = subtitle.then_some(
+ Row::new()
+ .gap(1)
+ .class(css::AlignItems::Center)
+ .class(css::JustifyContent::Center)
+ .with_child(icon)
+ .with_child(&title),
+ );
+ Column::new()
+ .flex(1.0)
+ .width("0") // correct flex base size for calculation
+ .max_height(250)
+ .with_child(
+ PieChart::new(title, percentage)
+ .class(css::Overflow::Auto)
+ .text(format!("{:.0}%", percentage * 100.))
+ .angle_start(75.0)
+ .angle_end(285.0)
+ .show_tooltip(false),
+ )
+ .with_optional_child(subtitle)
+ .with_child(
+ Container::new()
+ .padding_top(1)
+ .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} cores ({2} allocated)",
+ format!("{used:.2}"),
+ format!("{total:.0}"),
+ format!("{:.0}", data.pve_cpu_stats.allocated.unwrap_or(0.0)),
+ )
+ }
+ _ => {
+ tr!(
+ "{0} of {1} cores",
+ 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] 12+ messages in thread
* [PATCH datacenter-manager v2 4/4] ui: dashboard: add resource gauges to default dashboard
2026-03-30 13:07 [PATCH datacenter-manager v2 0/4] add resource gauge panels to dashboard/views Dominik Csapak
` (2 preceding siblings ...)
2026-03-30 13:07 ` [PATCH datacenter-manager v2 3/4] ui: dashboard: add new gauge panels widget type Dominik Csapak
@ 2026-03-30 13:07 ` Dominik Csapak
2026-04-01 11:34 ` [PATCH datacenter-manager v2 0/4] add resource gauge panels to dashboard/views Lukas Wagner
2026-04-02 13:24 ` superseded: " Dominik Csapak
5 siblings, 0 replies; 12+ messages in thread
From: Dominik Csapak @ 2026-03-30 13:07 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] 12+ messages in thread
* Re: [PATCH datacenter-manager v2 0/4] add resource gauge panels to dashboard/views
2026-03-30 13:07 [PATCH datacenter-manager v2 0/4] add resource gauge panels to dashboard/views Dominik Csapak
` (3 preceding siblings ...)
2026-03-30 13:07 ` [PATCH datacenter-manager v2 4/4] ui: dashboard: add resource gauges to default dashboard Dominik Csapak
@ 2026-04-01 11:34 ` Lukas Wagner
2026-04-02 13:24 ` superseded: " Dominik Csapak
5 siblings, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2026-04-01 11:34 UTC (permalink / raw)
To: Dominik Csapak, pdm-devel
On Mon Mar 30, 2026 at 3:07 PM CEST, 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.
>
> changes from v1:
> * use wider angle for the charts
> * remove decimal precision in chart
> * move 'title' to bottom with the other text
> * reformat the cpu text a bit for clarity
> * add a 'MemoryStatus' struct (same fields as StorageStatus but with
> different description)
>
> 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 | 14 ++-
> 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 | 160 ++++++++++++++++++++++++++++++
> ui/src/dashboard/mod.rs | 3 +
> ui/src/dashboard/view.rs | 19 +++-
> ui/src/dashboard/view/row_view.rs | 43 +++++++-
> 9 files changed, 347 insertions(+), 34 deletions(-)
> create mode 100644 ui/src/dashboard/gauge_panel.rs
I much prefer this new version from an aesthetic perspective, thanks for
the new version of this series!
This time I also reviewed the code more deeply. Looks good mostly, I
have some minor suggestions that were posted as replies to the
individual patches.
With the proposed changes included (and the proposed patch squashed in
or not - it's not a big deal if you don't):
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Tested-by: Lukas Wagner <l.wagner@proxmox.com>
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH datacenter-manager v2 1/4] api: return global cpu/memory/storage statistics
2026-03-30 13:07 ` [PATCH datacenter-manager v2 1/4] api: return global cpu/memory/storage statistics Dominik Csapak
@ 2026-04-01 11:34 ` Lukas Wagner
2026-04-02 13:02 ` Dominik Csapak
0 siblings, 1 reply; 12+ messages in thread
From: Lukas Wagner @ 2026-04-01 11:34 UTC (permalink / raw)
To: Dominik Csapak, pdm-devel
Looking good, some tiny suggestions for improvement inline.
Can also be fixed in a small follow-up patch in case there is nothing
else that needs to be changed.
On Mon Mar 30, 2026 at 3:07 PM CEST, Dominik Csapak wrote:
> 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 | 14 ++++++-
> lib/pdm-api-types/src/resource.rs | 27 +++++++++++++
> server/src/api/resources.rs | 65 ++++++++++++++++++++++++-------
> 3 files changed, 92 insertions(+), 14 deletions(-)
>
> diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs
> index d4cc7ef0..28aed3f4 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).
> @@ -202,6 +202,18 @@ pub struct StorageStatus {
> pub avail: u64,
> }
>
> +#[api()]
nit: you can leave out the parentheses: #[api]
> +#[derive(Default, Serialize, Deserialize, PartialEq, Clone)]
> +/// Storage space usage information.
^ The doc comment seems to be wrong (copy/paste mistake?)
> +pub struct MemoryStatus {
> + /// Total memory size (bytes).
> + pub total: u64,
> + /// Used memory (bytes).
> + pub used: u64,
> + /// Available memory (bytes).
> + pub avail: u64,
> +}
> +
> pub const PASSWORD_HINT_SCHEMA: Schema = StringSchema::new("Password hint.")
> .format(&SINGLE_LINE_COMMENT_FORMAT)
> .min_length(1)
> diff --git a/lib/pdm-api-types/src/resource.rs b/lib/pdm-api-types/src/resource.rs
> index d2db3b5a..01a5eb6c 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::{MemoryStatus, 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
'thread' is a countable noun, so it should rather be "Number of threads"
> + pub used: f64,
> + /// Amount of physically available cpu threads
same here
> + pub max: f64,
> + /// Currently allocated cores of running guests (only on PVE)
> + pub allocated: Option<f64>,
> +}
> +
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH datacenter-manager v2 3/4] ui: dashboard: add new gauge panels widget type
2026-03-30 13:07 ` [PATCH datacenter-manager v2 3/4] ui: dashboard: add new gauge panels widget type Dominik Csapak
@ 2026-04-01 11:34 ` Lukas Wagner
2026-04-01 11:36 ` Lukas Wagner
1 sibling, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2026-04-01 11:34 UTC (permalink / raw)
To: Dominik Csapak, pdm-devel
Hi Dominik,
thanks for the patch!
Some notes inline, along with a proposed patch that you can squash in if
you like at the end.
On Mon Mar 30, 2026 at 3:07 PM CEST, Dominik Csapak wrote:
> 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 | 160 ++++++++++++++++++++++++++++++
> ui/src/dashboard/mod.rs | 3 +
> ui/src/dashboard/view.rs | 9 +-
> ui/src/dashboard/view/row_view.rs | 43 +++++++-
> 5 files changed, 227 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,
> +}
nit: missing doc comments for a public type.
> +
> #[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")]
nit: missing doc-comments for this public enum variant
> + 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..8b30eb0f
> --- /dev/null
> +++ b/ui/src/dashboard/gauge_panel.rs
> @@ -0,0 +1,160 @@
> +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;
> +
This is a pub fn and should therefore have at least some basic documentation.
> +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"),
> + };
I find this tuple-based approach a bit hard to read, one has jump around
between the 'let (....)' and the returned tuple to see what is going on.
Having 4 different booleans in the assignment could also lead to subtle
logic bugs if one is not careful when changing this code.
I think it could be a nice idea to move these local variables into a struct.
See the end of this message for a full patch.
It's quite a bit more verbose, but personally I think its nicer than
tuple destructuring with such a large amount of parameters - plus it
makes the actual create_gauge_panel function a bit shorter.
Feel free to squash it in if you agree.
> +
> + let suffix = match remote_type {
> + Some(RemoteType::Pve) => " - Virtual Environment",
> + Some(RemoteType::Pbs) => " - Backup Server",
> + 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 {
> + let subtitle = subtitle.then_some(
> + Row::new()
> + .gap(1)
> + .class(css::AlignItems::Center)
> + .class(css::JustifyContent::Center)
> + .with_child(icon)
> + .with_child(&title),
> + );
> + Column::new()
> + .flex(1.0)
> + .width("0") // correct flex base size for calculation
> + .max_height(250)
> + .with_child(
> + PieChart::new(title, percentage)
> + .class(css::Overflow::Auto)
> + .text(format!("{:.0}%", percentage * 100.))
> + .angle_start(75.0)
> + .angle_end(285.0)
> + .show_tooltip(false),
> + )
> + .with_optional_child(subtitle)
> + .with_child(
> + Container::new()
> + .padding_top(1)
> + .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} cores ({2} allocated)",
> + format!("{used:.2}"),
> + format!("{total:.0}"),
> + format!("{:.0}", data.pve_cpu_stats.allocated.unwrap_or(0.0)),
> + )
> + }
> + _ => {
> + tr!(
> + "{0} of {1} cores",
> + 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))),
> + ),
> + ),
> + )
> }
>From f8e9e10c8a12dfd2cc03450c1ccb6cf6d978d94e Mon Sep 17 00:00:00 2001
From: Lukas Wagner <l.wagner@proxmox.com>
Date: Wed, 1 Apr 2026 13:12:15 +0200
Subject: [PATCH datacenter-manager] extract local vars to struct
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
ui/src/dashboard/gauge_panel.rs | 94 +++++++++++++++++++++++++--------
1 file changed, 73 insertions(+), 21 deletions(-)
diff --git a/ui/src/dashboard/gauge_panel.rs b/ui/src/dashboard/gauge_panel.rs
index 8b30eb0f..a88f56cb 100644
--- a/ui/src/dashboard/gauge_panel.rs
+++ b/ui/src/dashboard/gauge_panel.rs
@@ -14,22 +14,61 @@ use pdm_api_types::{resource::ResourcesStatus, views::NodeResourceType};
use crate::dashboard::{create_title_with_icon, loading_column};
use crate::LoadResult;
+struct PanelConfig {
+ show_cpu: bool,
+ show_mem: bool,
+ show_storage: bool,
+ title: String,
+ subtitle: bool,
+ icon: &'static str,
+}
+
+impl PanelConfig {
+ fn new(resource_type: Option<NodeResourceType>) -> Self {
+ match resource_type {
+ Some(NodeResourceType::Cpu) => PanelConfig {
+ show_cpu: true,
+ show_mem: false,
+ show_storage: false,
+ title: tr!("CPU Usage"),
+ subtitle: false,
+ icon: "cpu",
+ },
+ Some(NodeResourceType::Memory) => PanelConfig {
+ show_cpu: false,
+ show_mem: true,
+ show_storage: false,
+ title: tr!("Memory Usage"),
+ subtitle: false,
+ icon: "memory",
+ },
+ Some(NodeResourceType::Storage) => PanelConfig {
+ show_cpu: false,
+ show_mem: false,
+ show_storage: true,
+ title: tr!("Storage Usage"),
+ subtitle: false,
+ icon: "database",
+ },
+ None => PanelConfig {
+ show_cpu: true,
+ show_mem: true,
+ show_storage: true,
+ title: tr!("Resource Usage"),
+ subtitle: true,
+ icon: "tachometer",
+ },
+ }
+ }
+}
+
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 panel_config = PanelConfig::new(resource_type);
let suffix = match remote_type {
Some(RemoteType::Pve) => " - Virtual Environment",
@@ -40,32 +79,45 @@ pub fn create_gauge_panel(
let is_loading = !status.has_data();
Panel::new()
- .title(create_title_with_icon(icon, format!("{title}{suffix}")))
+ .title(create_title_with_icon(
+ panel_config.icon,
+ format!("{}{suffix}", panel_config.title),
+ ))
.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
+ panel_config
+ .show_cpu
+ .then_some((data.pve_cpu_stats.used, data.pve_cpu_stats.max)),
+ panel_config
+ .show_mem
+ .then_some((data.pve_memory_stats.used, data.pve_memory_stats.total)),
+ panel_config
+ .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
+ panel_config
+ .show_cpu
+ .then_some((data.pbs_cpu_stats.used, data.pbs_cpu_stats.max)),
+ panel_config
+ .show_mem
+ .then_some((data.pbs_memory_stats.used, data.pbs_memory_stats.total)),
+ panel_config
+ .show_storage
.then_some((data.pbs_storage_stats.used, data.pbs_storage_stats.total)),
),
None => (
- show_cpu.then_some((
+ panel_config.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((
+ panel_config.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((
+ panel_config.show_storage.then_some((
data.pve_storage_stats.used + data.pbs_storage_stats.used,
data.pve_storage_stats.total + data.pbs_storage_stats.total,
)),
@@ -73,7 +125,7 @@ pub fn create_gauge_panel(
};
let chart = |percentage: f64, icon: Fa, title: String, extra_text: String| -> Column {
- let subtitle = subtitle.then_some(
+ let subtitle = panel_config.subtitle.then_some(
Row::new()
.gap(1)
.class(css::AlignItems::Center)
--
2.47.3
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH datacenter-manager v2 3/4] ui: dashboard: add new gauge panels widget type
2026-03-30 13:07 ` [PATCH datacenter-manager v2 3/4] ui: dashboard: add new gauge panels widget type Dominik Csapak
2026-04-01 11:34 ` Lukas Wagner
@ 2026-04-01 11:36 ` Lukas Wagner
1 sibling, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2026-04-01 11:36 UTC (permalink / raw)
To: Dominik Csapak, pdm-devel
On Mon Mar 30, 2026 at 3:07 PM CEST, Dominik Csapak wrote:
> 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))),
> + ),
Sorry, forgot this in my initial reply:
I'd also use the full product name here instead of the colloquially used
acronyms.
> + ),
> + )
> }
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH datacenter-manager v2 1/4] api: return global cpu/memory/storage statistics
2026-04-01 11:34 ` Lukas Wagner
@ 2026-04-02 13:02 ` Dominik Csapak
2026-04-02 13:48 ` Lukas Wagner
0 siblings, 1 reply; 12+ messages in thread
From: Dominik Csapak @ 2026-04-02 13:02 UTC (permalink / raw)
To: Lukas Wagner, pdm-devel
On 4/1/26 1:33 PM, Lukas Wagner wrote:
> Looking good, some tiny suggestions for improvement inline.
>
> Can also be fixed in a small follow-up patch in case there is nothing
> else that needs to be changed.
>
> On Mon Mar 30, 2026 at 3:07 PM CEST, Dominik Csapak wrote:
>> 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 | 14 ++++++-
>> lib/pdm-api-types/src/resource.rs | 27 +++++++++++++
>> server/src/api/resources.rs | 65 ++++++++++++++++++++++++-------
>> 3 files changed, 92 insertions(+), 14 deletions(-)
>>
>> diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs
>> index d4cc7ef0..28aed3f4 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).
>> @@ -202,6 +202,18 @@ pub struct StorageStatus {
>> pub avail: u64,
>> }
>>
>> +#[api()]
>
> nit: you can leave out the parentheses: #[api]
>
>> +#[derive(Default, Serialize, Deserialize, PartialEq, Clone)]
>> +/// Storage space usage information.
>
> ^ The doc comment seems to be wrong (copy/paste mistake?)
>
>> +pub struct MemoryStatus {
>> + /// Total memory size (bytes).
>> + pub total: u64,
>> + /// Used memory (bytes).
>> + pub used: u64,
>> + /// Available memory (bytes).
>> + pub avail: u64,
>> +}
>> +
>> pub const PASSWORD_HINT_SCHEMA: Schema = StringSchema::new("Password hint.")
>> .format(&SINGLE_LINE_COMMENT_FORMAT)
>> .min_length(1)
>> diff --git a/lib/pdm-api-types/src/resource.rs b/lib/pdm-api-types/src/resource.rs
>> index d2db3b5a..01a5eb6c 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::{MemoryStatus, 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
>
> 'thread' is a countable noun, so it should rather be "Number of threads"
thanks, i'll fix it, but just to note, in that same file we have
many instances of this, for example:
"Amount of running guests"
"Amount of online datastores"
"Amount of configured remotes"
which all seem to me like countable nouns?
so we might want to clean that up for all of them
>
>> + pub used: f64,
>> + /// Amount of physically available cpu threads
>
> same here
>
>
>> + pub max: f64,
>> + /// Currently allocated cores of running guests (only on PVE)
>> + pub allocated: Option<f64>,
>> +}
>> +
>
^ permalink raw reply [flat|nested] 12+ messages in thread
* superseded: [PATCH datacenter-manager v2 0/4] add resource gauge panels to dashboard/views
2026-03-30 13:07 [PATCH datacenter-manager v2 0/4] add resource gauge panels to dashboard/views Dominik Csapak
` (4 preceding siblings ...)
2026-04-01 11:34 ` [PATCH datacenter-manager v2 0/4] add resource gauge panels to dashboard/views Lukas Wagner
@ 2026-04-02 13:24 ` Dominik Csapak
5 siblings, 0 replies; 12+ messages in thread
From: Dominik Csapak @ 2026-04-02 13:24 UTC (permalink / raw)
To: pdm-devel
superseded by v3:
https://lore.proxmox.com/pdm-devel/20260402132111.3221890-1-d.csapak@proxmox.com/
On 3/30/26 3:10 PM, 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.
>
> changes from v1:
> * use wider angle for the charts
> * remove decimal precision in chart
> * move 'title' to bottom with the other text
> * reformat the cpu text a bit for clarity
> * add a 'MemoryStatus' struct (same fields as StorageStatus but with
> different description)
>
> 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 | 14 ++-
> 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 | 160 ++++++++++++++++++++++++++++++
> ui/src/dashboard/mod.rs | 3 +
> ui/src/dashboard/view.rs | 19 +++-
> ui/src/dashboard/view/row_view.rs | 43 +++++++-
> 9 files changed, 347 insertions(+), 34 deletions(-)
> create mode 100644 ui/src/dashboard/gauge_panel.rs
>
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH datacenter-manager v2 1/4] api: return global cpu/memory/storage statistics
2026-04-02 13:02 ` Dominik Csapak
@ 2026-04-02 13:48 ` Lukas Wagner
0 siblings, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2026-04-02 13:48 UTC (permalink / raw)
To: Dominik Csapak, Lukas Wagner, pdm-devel
On Thu Apr 2, 2026 at 3:02 PM CEST, Dominik Csapak wrote:
>>> +#[api]
>>> +#[derive(Default, Serialize, Deserialize, Clone, PartialEq)]
>>> +/// Statistics for CPU utilization
>>> +pub struct CpuStatistics {
>>> + /// Amount of threads utilized
>>
>> 'thread' is a countable noun, so it should rather be "Number of threads"
>
> thanks, i'll fix it, but just to note, in that same file we have
> many instances of this, for example:
>
> "Amount of running guests"
> "Amount of online datastores"
> "Amount of configured remotes"
>
>
> which all seem to me like countable nouns?
>
> so we might want to clean that up for all of them
Yeah, this would make sense! Feel free to include it as a separate patch
in this series :)
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-04-02 13:47 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-03-30 13:07 [PATCH datacenter-manager v2 0/4] add resource gauge panels to dashboard/views Dominik Csapak
2026-03-30 13:07 ` [PATCH datacenter-manager v2 1/4] api: return global cpu/memory/storage statistics Dominik Csapak
2026-04-01 11:34 ` Lukas Wagner
2026-04-02 13:02 ` Dominik Csapak
2026-04-02 13:48 ` Lukas Wagner
2026-03-30 13:07 ` [PATCH datacenter-manager v2 2/4] ui: css: use mask for svg icons Dominik Csapak
2026-03-30 13:07 ` [PATCH datacenter-manager v2 3/4] ui: dashboard: add new gauge panels widget type Dominik Csapak
2026-04-01 11:34 ` Lukas Wagner
2026-04-01 11:36 ` Lukas Wagner
2026-03-30 13:07 ` [PATCH datacenter-manager v2 4/4] ui: dashboard: add resource gauges to default dashboard Dominik Csapak
2026-04-01 11:34 ` [PATCH datacenter-manager v2 0/4] add resource gauge panels to dashboard/views Lukas Wagner
2026-04-02 13:24 ` superseded: " Dominik Csapak
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox