[pdm-devel] [PATCH datacenter-manager 3/3] ui: dashboard: refactor guest panel

Dominik Csapak d.csapak at proxmox.com
Fri Apr 11 16:05:20 CEST 2025


so we can keep the dashboard code smaller when adding new features here.
Also implement showing unknown guests and templates

To have a more structured output, re-implement it with a DataTable
instead of a custom flex layout. This has the advantage of being
a proper grid.

Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
---
 ui/src/dashboard/guest_panel.rs | 143 ++++++++++++++++++++++++++++++++
 ui/src/dashboard/mod.rs         |  65 ++++-----------
 ui/src/pve/mod.rs               |  11 ++-
 3 files changed, 169 insertions(+), 50 deletions(-)
 create mode 100644 ui/src/dashboard/guest_panel.rs

diff --git a/ui/src/dashboard/guest_panel.rs b/ui/src/dashboard/guest_panel.rs
new file mode 100644
index 0000000..d885056
--- /dev/null
+++ b/ui/src/dashboard/guest_panel.rs
@@ -0,0 +1,143 @@
+use std::rc::Rc;
+
+use pdm_api_types::resource::GuestStatusCount;
+use proxmox_yew_comp::GuestState;
+use pwt::{
+    prelude::*,
+    props::ExtractPrimaryKey,
+    state::Store,
+    widget::{
+        data_table::{DataTable, DataTableColumn, DataTableHeader},
+        Fa,
+    },
+};
+use yew::virtual_dom::{VComp, VNode};
+
+use crate::pve::GuestType;
+
+use super::loading_column;
+
+#[derive(PartialEq, Clone, Properties)]
+pub struct GuestPanel {
+    guest_type: GuestType,
+    status: Option<GuestStatusCount>,
+}
+
+impl GuestPanel {
+    pub fn new(guest_type: GuestType, status: Option<GuestStatusCount>) -> Self {
+        yew::props!(Self { guest_type, status })
+    }
+}
+
+impl From<GuestPanel> for VNode {
+    fn from(value: GuestPanel) -> Self {
+        let comp = VComp::new::<PdmGuestPanel>(Rc::new(value), None);
+        VNode::from(comp)
+    }
+}
+
+#[derive(PartialEq, Clone)]
+pub enum StatusRow {
+    State(GuestState, u64),
+    All(u64),
+}
+
+impl ExtractPrimaryKey for StatusRow {
+    fn extract_key(&self) -> yew::virtual_dom::Key {
+        yew::virtual_dom::Key::from(match self {
+            StatusRow::State(state, _) => match state {
+                GuestState::Running => "running",
+                GuestState::Paused => "paused",
+                GuestState::Stopped => "stopped",
+                GuestState::Template => "template",
+                GuestState::Unknown => "unknown",
+            },
+            StatusRow::All(_) => "all",
+        })
+    }
+}
+
+fn columns(guest_type: GuestType) -> Rc<Vec<DataTableHeader<StatusRow>>> {
+    Rc::new(vec![
+        DataTableColumn::new("icon")
+            .width("3em")
+            .render(move |item: &StatusRow| {
+                match item {
+                    StatusRow::State(state, _) => state.to_fa_icon(),
+                    StatusRow::All(_) => match guest_type {
+                        GuestType::Qemu => Fa::new("desktop"),
+                        GuestType::Lxc => Fa::new("cubes"),
+                    },
+                }
+                .fixed_width()
+                .into()
+            })
+            .into(),
+        DataTableColumn::new("text")
+            .flex(5)
+            .render(|item: &StatusRow| {
+                match item {
+                    StatusRow::State(GuestState::Running, _) => tr!("running"),
+                    StatusRow::State(GuestState::Stopped, _) => tr!("stopped"),
+                    StatusRow::State(GuestState::Paused, _) => tr!("paused"),
+                    StatusRow::State(GuestState::Template, _) => tr!("Template"),
+                    StatusRow::State(GuestState::Unknown, _) => tr!("Unknown"),
+                    StatusRow::All(_) => tr!("All"),
+                }
+                .into()
+            })
+            .into(),
+        DataTableColumn::new("count")
+            .flex(1)
+            .justify("right")
+            .render(|item: &StatusRow| match item {
+                StatusRow::State(_, count) => count.into(),
+                StatusRow::All(count) => count.into(),
+            })
+            .into(),
+    ])
+}
+
+pub struct PdmGuestPanel {}
+
+impl yew::Component for PdmGuestPanel {
+    type Message = String;
+    type Properties = GuestPanel;
+
+    fn create(_ctx: &yew::Context<Self>) -> Self {
+        Self {}
+    }
+
+    fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
+        let props = ctx.props();
+        if props.status.is_none() {
+            return loading_column().into();
+        }
+        let guest_type = props.guest_type;
+        let status = ctx.props().status.clone().unwrap();
+
+        let store = Store::new();
+        store.set_data(vec![
+            StatusRow::State(GuestState::Running, status.running),
+            StatusRow::State(GuestState::Stopped, status.stopped),
+            StatusRow::State(GuestState::Template, status.template),
+            StatusRow::State(GuestState::Unknown, status.unknown),
+            StatusRow::All(status.running + status.stopped + status.template + status.unknown),
+        ]);
+
+        store.set_filter(|rec: &StatusRow| match rec {
+            StatusRow::State(_, count) if *count > 0 => true,
+            StatusRow::State(GuestState::Running | GuestState::Stopped, _) => true,
+            StatusRow::All(_) => true,
+            _ => false,
+        });
+
+        DataTable::new(columns(guest_type), store.clone())
+            .padding(4)
+            .striped(false)
+            .borderless(true)
+            .bordered(false)
+            .show_header(false)
+            .into()
+    }
+}
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 016fccb..7b7ec81 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -7,7 +7,7 @@ use yew::{
     Component,
 };
 
-use proxmox_yew_comp::{http_get, GuestState, Status};
+use proxmox_yew_comp::{http_get, Status};
 use pwt::{
     css::{AlignItems, FlexFit, FlexWrap, JustifyContent},
     prelude::*,
@@ -18,7 +18,7 @@ use pwt::{
 use pdm_api_types::resource::{GuestStatusCount, NodeStatusCount, ResourcesStatus};
 use pdm_client::types::TopEntity;
 
-use crate::{remotes::AddWizard, RemoteList};
+use crate::{pve::GuestType, remotes::AddWizard, RemoteList};
 
 mod top_entities;
 pub use top_entities::TopEntities;
@@ -29,6 +29,9 @@ pub use subscription_info::SubscriptionInfo;
 mod remote_panel;
 use remote_panel::RemotePanel;
 
+mod guest_panel;
+use guest_panel::GuestPanel;
+
 #[derive(Properties, PartialEq)]
 pub struct Dashboard {
     #[prop_or(60)]
@@ -120,48 +123,20 @@ impl PdmDashboard {
             )
     }
 
-    fn create_guest_panel(&self, icon: &str, title: String, status: &GuestStatusCount) -> Panel {
+    fn create_guest_panel(&self, guest_type: GuestType, status: &GuestStatusCount) -> Panel {
+        let (icon, title) = match guest_type {
+            GuestType::Qemu => ("desktop", tr!("Virtual Machines")),
+            GuestType::Lxc => ("cubses", tr!("Linux Container")),
+        };
         Panel::new()
             .flex(1.0)
             .width(300)
             .title(self.create_title_with_icon(icon, title))
             .border(true)
-            .with_child(if self.loading {
-                loading_column()
-            } else {
-                Column::new()
-                    .padding(4)
-                    .gap(2)
-                    .class(FlexFit)
-                    .class(JustifyContent::Center)
-                    .with_child(
-                        Row::new()
-                            .gap(2)
-                            .with_child(GuestState::Running.to_fa_icon().fixed_width())
-                            .with_child(tr!("running"))
-                            .with_flex_spacer()
-                            .with_child(Container::from_tag("span").with_child(status.running)),
-                    )
-                    .with_child(
-                        Row::new()
-                            .gap(2)
-                            .with_child(GuestState::Stopped.to_fa_icon().fixed_width())
-                            .with_child(tr!("stopped"))
-                            .with_flex_spacer()
-                            .with_child(Container::from_tag("span").with_child(status.stopped)),
-                    )
-                    // FIXME: show templates?
-                    .with_optional_child(
-                        (self.status.qemu.unknown > 0).then_some(
-                            Row::new()
-                                .gap(2)
-                                .with_child(GuestState::Unknown.to_fa_icon().fixed_width())
-                                .with_child(tr!("unknown"))
-                                .with_flex_spacer()
-                                .with_child(Container::from_tag("span").with_child(status.unknown)),
-                        ),
-                    )
-            })
+            .with_child(GuestPanel::new(
+                guest_type,
+                (!self.loading).then_some(status.clone()),
+            ))
     }
 
     fn create_top_entities_panel(
@@ -292,16 +267,8 @@ impl Component for PdmDashboard {
                         tr!("Virtual Environment Nodes"),
                         &self.status.pve_nodes,
                     ))
-                    .with_child(self.create_guest_panel(
-                        "desktop",
-                        tr!("Virtual Machines"),
-                        &self.status.qemu,
-                    ))
-                    .with_child(self.create_guest_panel(
-                        "cubes",
-                        tr!("Linux Container"),
-                        &self.status.lxc,
-                    ))
+                    .with_child(self.create_guest_panel(GuestType::Qemu, &self.status.qemu))
+                    .with_child(self.create_guest_panel(GuestType::Lxc, &self.status.lxc))
                     // FIXME: add PBS support
                     //.with_child(self.create_node_panel(
                     //    "building-o",
diff --git a/ui/src/pve/mod.rs b/ui/src/pve/mod.rs
index ed0ee04..c5a83cf 100644
--- a/ui/src/pve/mod.rs
+++ b/ui/src/pve/mod.rs
@@ -1,4 +1,4 @@
-use std::rc::Rc;
+use std::{fmt::Display, rc::Rc};
 
 use gloo_utils::window;
 use proxmox_client::Error;
@@ -72,6 +72,15 @@ pub enum GuestType {
     Lxc,
 }
 
+impl Display for GuestType {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            GuestType::Qemu => f.write_str("qemu"),
+            GuestType::Lxc => f.write_str("lxc"),
+        }
+    }
+}
+
 #[derive(PartialEq, Clone, Copy)]
 pub struct GuestInfo {
     pub guest_type: GuestType,
-- 
2.39.5





More information about the pdm-devel mailing list