[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