public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH datacenter-manager v3 0/4] add resource gauge panels to dashboard/views
@ 2026-04-02 13:18 Dominik Csapak
  2026-04-02 13:18 ` [PATCH datacenter-manager v3 1/4] api: return global cpu/memory/storage statistics Dominik Csapak
                   ` (3 more replies)
  0 siblings, 4 replies; 5+ messages in thread
From: Dominik Csapak @ 2026-04-02 13:18 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)

I squashed in Lukas changes with a slight modification to reduce
the line-count a bit (conf vs panel_config variable name). I added
a 'co-developed-by' trailer.

NOTE: the pwt patches [0] have to be applied and the package
has to be bumped first.

changes from v2:
* better comments
* use longer name instead of PVE/PBS
* squashed in Lukas changes for the PanelConfig

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    |  17 +++
 server/src/api/resources.rs       |  65 +++++++--
 ui/css/pdm.scss                   |  35 ++---
 ui/src/dashboard/gauge_panel.rs   | 210 ++++++++++++++++++++++++++++++
 ui/src/dashboard/mod.rs           |   3 +
 ui/src/dashboard/view.rs          |  19 ++-
 ui/src/dashboard/view/row_view.rs |  45 ++++++-
 9 files changed, 401 insertions(+), 34 deletions(-)
 create mode 100644 ui/src/dashboard/gauge_panel.rs

-- 
2.47.3





^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH datacenter-manager v3 1/4] api: return global cpu/memory/storage statistics
  2026-04-02 13:18 [PATCH datacenter-manager v3 0/4] add resource gauge panels to dashboard/views Dominik Csapak
@ 2026-04-02 13:18 ` Dominik Csapak
  2026-04-02 13:18 ` [PATCH datacenter-manager v3 2/4] ui: css: use mask for svg icons Dominik Csapak
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 5+ messages in thread
From: Dominik Csapak @ 2026-04-02 13:18 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>
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Tested-by: Lukas Wagner <l.wagner@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..fe1f5167 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)]
+/// Memory 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..1d1a8521 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 {
+    /// Number of utilized threads
+    pub used: f64,
+    /// Number 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] 5+ messages in thread

* [PATCH datacenter-manager v3 2/4] ui: css: use mask for svg icons
  2026-04-02 13:18 [PATCH datacenter-manager v3 0/4] add resource gauge panels to dashboard/views Dominik Csapak
  2026-04-02 13:18 ` [PATCH datacenter-manager v3 1/4] api: return global cpu/memory/storage statistics Dominik Csapak
@ 2026-04-02 13:18 ` Dominik Csapak
  2026-04-02 13:18 ` [PATCH datacenter-manager v3 3/4] ui: dashboard: add new gauge panels widget type Dominik Csapak
  2026-04-02 13:18 ` [PATCH datacenter-manager v3 4/4] ui: dashboard: add resource gauges to default dashboard Dominik Csapak
  3 siblings, 0 replies; 5+ messages in thread
From: Dominik Csapak @ 2026-04-02 13:18 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>
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Tested-by: Lukas Wagner <l.wagner@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] 5+ messages in thread

* [PATCH datacenter-manager v3 3/4] ui: dashboard: add new gauge panels widget type
  2026-04-02 13:18 [PATCH datacenter-manager v3 0/4] add resource gauge panels to dashboard/views Dominik Csapak
  2026-04-02 13:18 ` [PATCH datacenter-manager v3 1/4] api: return global cpu/memory/storage statistics Dominik Csapak
  2026-04-02 13:18 ` [PATCH datacenter-manager v3 2/4] ui: css: use mask for svg icons Dominik Csapak
@ 2026-04-02 13:18 ` Dominik Csapak
  2026-04-02 13:18 ` [PATCH datacenter-manager v3 4/4] ui: dashboard: add resource gauges to default dashboard Dominik Csapak
  3 siblings, 0 replies; 5+ messages in thread
From: Dominik Csapak @ 2026-04-02 13:18 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.

Co-developed-by: Lukas Wagner <l.wagner@proxmox.com>
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Tested-by: Lukas Wagner <l.wagner@proxmox.com>
---
 lib/pdm-api-types/src/views.rs    |  17 +++
 ui/src/dashboard/gauge_panel.rs   | 210 ++++++++++++++++++++++++++++++
 ui/src/dashboard/mod.rs           |   3 +
 ui/src/dashboard/view.rs          |   9 +-
 ui/src/dashboard/view/row_view.rs |  45 ++++++-
 5 files changed, 281 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..c1885828 100644
--- a/lib/pdm-api-types/src/views.rs
+++ b/lib/pdm-api-types/src/views.rs
@@ -266,6 +266,15 @@ pub struct RowWidget {
     pub r#type: WidgetType,
 }
 
+#[derive(Serialize, Deserialize, PartialEq, Clone, Copy)]
+#[serde(rename_all = "kebab-case")]
+/// A type of resource of a node
+pub enum NodeResourceType {
+    Cpu,
+    Memory,
+    Storage,
+}
+
 #[derive(Serialize, Deserialize, PartialEq, Clone)]
 #[serde(rename_all = "kebab-case")]
 #[serde(tag = "widget-type")]
@@ -295,6 +304,14 @@ pub enum WidgetType {
         grouping: TaskSummaryGrouping,
     },
     ResourceTree,
+    #[serde(rename_all = "kebab-case")]
+    /// Display node resources as gauge chart
+    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..df4f89d6
--- /dev/null
+++ b/ui/src/dashboard/gauge_panel.rs
@@ -0,0 +1,210 @@
+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;
+
+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",
+            },
+        }
+    }
+}
+
+/// Creates a new gauge chart panel. Setting `resource_type` to `None` means we
+/// create one gauge chart for each type in one panel.
+///
+/// Using `None` as remote_type means the resources from PVE and PBS will be combined.
+pub fn create_gauge_panel(
+    resource_type: Option<NodeResourceType>,
+    remote_type: Option<RemoteType>,
+    status: SharedState<LoadResult<ResourcesStatus, Error>>,
+) -> Panel {
+    let status = status.read();
+    let conf = PanelConfig::new(resource_type);
+
+    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(
+            conf.icon,
+            format!("{}{suffix}", conf.title),
+        ))
+        .border(true)
+        .with_optional_child(status.data.as_ref().map(|data| {
+            let (cpu, mem, storage) = match remote_type {
+                Some(RemoteType::Pve) => (
+                    conf.show_cpu
+                        .then_some((data.pve_cpu_stats.used, data.pve_cpu_stats.max)),
+                    conf.show_mem
+                        .then_some((data.pve_memory_stats.used, data.pve_memory_stats.total)),
+                    conf.show_storage
+                        .then_some((data.pve_storage_stats.used, data.pve_storage_stats.total)),
+                ),
+                Some(RemoteType::Pbs) => (
+                    conf.show_cpu
+                        .then_some((data.pbs_cpu_stats.used, data.pbs_cpu_stats.max)),
+                    conf.show_mem
+                        .then_some((data.pbs_memory_stats.used, data.pbs_memory_stats.total)),
+                    conf.show_storage
+                        .then_some((data.pbs_storage_stats.used, data.pbs_storage_stats.total)),
+                ),
+                None => (
+                    conf.show_cpu.then_some((
+                        data.pve_cpu_stats.used + data.pbs_cpu_stats.used,
+                        data.pve_cpu_stats.max + data.pbs_cpu_stats.max,
+                    )),
+                    conf.show_mem.then_some((
+                        data.pve_memory_stats.used + data.pbs_memory_stats.used,
+                        data.pve_memory_stats.total + data.pbs_memory_stats.total,
+                    )),
+                    conf.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 = conf.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..3c5428ae 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,18 @@ 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!("Virtual Environment"))
+                            .menu(create_gauge_menu(Some(RemoteType::Pve))),
+                    )
+                    .with_item(
+                        MenuItem::new(tr!("Backup Server"))
+                            .menu(create_gauge_menu(Some(RemoteType::Pbs))),
+                    ),
+            ),
+        )
 }
-- 
2.47.3





^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH datacenter-manager v3 4/4] ui: dashboard: add resource gauges to default dashboard
  2026-04-02 13:18 [PATCH datacenter-manager v3 0/4] add resource gauge panels to dashboard/views Dominik Csapak
                   ` (2 preceding siblings ...)
  2026-04-02 13:18 ` [PATCH datacenter-manager v3 3/4] ui: dashboard: add new gauge panels widget type Dominik Csapak
@ 2026-04-02 13:18 ` Dominik Csapak
  3 siblings, 0 replies; 5+ messages in thread
From: Dominik Csapak @ 2026-04-02 13:18 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>
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Tested-by: Lukas Wagner <l.wagner@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] 5+ messages in thread

end of thread, other threads:[~2026-04-02 13:21 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-04-02 13:18 [PATCH datacenter-manager v3 0/4] add resource gauge panels to dashboard/views Dominik Csapak
2026-04-02 13:18 ` [PATCH datacenter-manager v3 1/4] api: return global cpu/memory/storage statistics Dominik Csapak
2026-04-02 13:18 ` [PATCH datacenter-manager v3 2/4] ui: css: use mask for svg icons Dominik Csapak
2026-04-02 13:18 ` [PATCH datacenter-manager v3 3/4] ui: dashboard: add new gauge panels widget type Dominik Csapak
2026-04-02 13:18 ` [PATCH datacenter-manager v3 4/4] ui: dashboard: add resource gauges to default dashboard Dominik Csapak

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal