From: Dominik Csapak <d.csapak@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager 15/15] ui: dashboard: use 'View' instead of the Dashboard
Date: Tue, 21 Oct 2025 16:03:31 +0200 [thread overview]
Message-ID: <20251021140801.3611022-16-d.csapak@proxmox.com> (raw)
In-Reply-To: <20251021140801.3611022-1-d.csapak@proxmox.com>
this uses our new `View` with a (currently) static configuration to
replicate our Dashboard. Since all functionality of that is available
in the View, the `Dashboard` struct can be removed.
This should be functionally the same as before.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/dashboard/mod.rs | 486 +--------------------------------------
ui/src/dashboard/view.rs | 61 ++++-
ui/src/lib.rs | 2 +-
ui/src/main_menu.rs | 5 +-
4 files changed, 76 insertions(+), 478 deletions(-)
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 8a0bdad0..8e979417 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -1,27 +1,6 @@
-use std::rc::Rc;
-
-use anyhow::Error;
-use futures::join;
-use js_sys::Date;
-use serde_json::json;
-use yew::{
- virtual_dom::{VComp, VNode},
- Component,
-};
-
-use proxmox_yew_comp::http_get;
-use pwt::{
- css::{AlignItems, FlexDirection, FlexFit, FlexWrap, JustifyContent},
- prelude::*,
- props::StorageLocation,
- state::PersistentState,
- widget::{form::FormContext, Column, Container, Fa, Panel, Row},
- AsyncPool,
-};
-
-use pdm_api_types::{remotes::RemoteType, resource::ResourcesStatus, TaskStatistics};
-
-use crate::{pve::GuestType, remotes::AddWizard};
+use pwt::css;
+use pwt::prelude::*;
+use pwt::widget::{Column, Fa, Row};
mod top_entities;
pub use top_entities::create_top_entities_panel;
@@ -39,477 +18,36 @@ mod guest_panel;
pub use guest_panel::create_guest_panel;
mod sdn_zone_panel;
-use sdn_zone_panel::create_sdn_panel;
+pub use sdn_zone_panel::create_sdn_panel;
mod status_row;
-use status_row::DashboardStatusRow;
+pub use status_row::DashboardStatusRow;
mod filtered_tasks;
mod tasks;
-use tasks::{create_task_summary_panel, get_task_options};
+pub use tasks::create_task_summary_panel;
pub mod types;
pub mod view;
mod refresh_config_edit;
-pub use refresh_config_edit::{
- create_refresh_config_edit_window, refresh_config_id, RefreshConfig,
-};
-use refresh_config_edit::{
- DEFAULT_MAX_AGE_S, DEFAULT_REFRESH_INTERVAL_S, FORCE_RELOAD_MAX_AGE_S, INITIAL_MAX_AGE_S,
-};
-
-#[derive(Properties, PartialEq)]
-pub struct Dashboard {}
-
-impl Dashboard {
- pub fn new() -> Self {
- yew::props!(Self {})
- }
-}
-
-impl Default for Dashboard {
- fn default() -> Self {
- Self::new()
- }
-}
-
-pub enum LoadingResult {
- Resources(Result<ResourcesStatus, Error>),
- TopEntities(Result<pdm_client::types::TopEntities, proxmox_client::Error>),
- TaskStatistics(Result<TaskStatistics, Error>),
- All,
-}
-
-pub enum Msg {
- LoadingFinished(LoadingResult),
- CreateWizard(Option<RemoteType>),
- Reload,
- ForceReload,
- UpdateConfig(RefreshConfig),
- ConfigWindow(bool),
-}
-
-struct StatisticsOptions {
- data: Option<TaskStatistics>,
- error: Option<Error>,
-}
-
-pub struct PdmDashboard {
- status: Option<ResourcesStatus>,
- last_error: Option<Error>,
- top_entities: Option<pdm_client::types::TopEntities>,
- last_top_entities_error: Option<proxmox_client::Error>,
- statistics: StatisticsOptions,
- load_finished_time: Option<f64>,
- show_wizard: Option<RemoteType>,
- show_config_window: bool,
- async_pool: AsyncPool,
- config: PersistentState<RefreshConfig>,
-}
-
-impl PdmDashboard {
- fn reload(&mut self, ctx: &yew::Context<Self>) {
- let max_age = if self.load_finished_time.is_some() {
- self.config.max_age.unwrap_or(DEFAULT_MAX_AGE_S)
- } else {
- INITIAL_MAX_AGE_S
- };
- self.do_reload(ctx, max_age)
- }
-
- fn do_reload(&mut self, ctx: &yew::Context<Self>, max_age: u64) {
- let link = ctx.link().clone();
- let (_, since) = get_task_options(self.config.task_last_hours);
-
- self.async_pool.spawn(async move {
- let client = crate::pdm_client();
-
- let top_entities_future = {
- let link = link.clone();
- async move {
- let res = client.get_top_entities().await;
- link.send_message(Msg::LoadingFinished(LoadingResult::TopEntities(res)));
- }
- };
- let status_future = {
- let link = link.clone();
- async move {
- let res: Result<ResourcesStatus, _> =
- http_get("/resources/status", Some(json!({"max-age": max_age}))).await;
- link.send_message(Msg::LoadingFinished(LoadingResult::Resources(res)));
- }
- };
-
- let params = Some(json!({
- "since": since,
- "limit": 0,
- }));
-
- // TODO replace with pdm client call
- let statistics_future = {
- let link = link.clone();
- async move {
- let res: Result<TaskStatistics, _> =
- http_get("/remote-tasks/statistics", params).await;
- link.send_message(Msg::LoadingFinished(LoadingResult::TaskStatistics(res)));
- }
- };
- join!(top_entities_future, status_future, statistics_future);
- link.send_message(Msg::LoadingFinished(LoadingResult::All));
- });
- }
-}
-
-impl Component for PdmDashboard {
- type Message = Msg;
- type Properties = Dashboard;
-
- fn create(ctx: &yew::Context<Self>) -> Self {
- let config: PersistentState<RefreshConfig> =
- PersistentState::new(StorageLocation::local(refresh_config_id("dashboard")));
- let async_pool = AsyncPool::new();
-
- let mut this = Self {
- status: None,
- last_error: None,
- top_entities: None,
- last_top_entities_error: None,
- statistics: StatisticsOptions {
- data: None,
- error: None,
- },
- load_finished_time: None,
- show_wizard: None,
- show_config_window: false,
- async_pool,
- config,
- };
-
- this.reload(ctx);
-
- this
- }
-
- fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
- match msg {
- Msg::LoadingFinished(res) => {
- match res {
- LoadingResult::Resources(resources_status) => match resources_status {
- Ok(status) => {
- self.last_error = None;
- self.status = Some(status);
- }
- Err(err) => self.last_error = Some(err),
- },
- LoadingResult::TopEntities(top_entities) => match top_entities {
- Ok(data) => {
- self.last_top_entities_error = None;
- self.top_entities = Some(data);
- }
- Err(err) => self.last_top_entities_error = Some(err),
- },
-
- LoadingResult::TaskStatistics(task_statistics) => match task_statistics {
- Ok(statistics) => {
- self.statistics.error = None;
- self.statistics.data = Some(statistics);
- }
- Err(err) => self.statistics.error = Some(err),
- },
- LoadingResult::All => {
- if self.load_finished_time.is_none() {
- // immediately trigger a "normal" reload after the first load with the
- // configured or default max-age to ensure users sees more current data.
- ctx.link().send_message(Msg::Reload);
- }
- self.load_finished_time = Some(Date::now() / 1000.0);
- }
- }
- true
- }
- Msg::CreateWizard(remote_type) => {
- self.show_wizard = remote_type;
- true
- }
- Msg::Reload => {
- self.reload(ctx);
- true
- }
- Msg::ForceReload => {
- self.do_reload(ctx, FORCE_RELOAD_MAX_AGE_S);
- true
- }
- Msg::ConfigWindow(show) => {
- self.show_config_window = show;
- true
- }
- Msg::UpdateConfig(dashboard_config) => {
- let (old_hours, _) = get_task_options(self.config.task_last_hours);
- self.config.update(dashboard_config);
- let (new_hours, _) = get_task_options(self.config.task_last_hours);
-
- if old_hours != new_hours {
- self.reload(ctx);
- }
-
- self.show_config_window = false;
- true
- }
- }
- }
-
- fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
- let (hours, since) = get_task_options(self.config.task_last_hours);
- let content = Column::new()
- .class(FlexFit)
- .with_child(
- Container::new()
- .class("pwt-content-spacer-padding")
- .class("pwt-content-spacer-colors")
- .style("color", "var(--pwt-color)")
- .style("background-color", "var(--pwt-color-background)")
- .with_child(DashboardStatusRow::new(
- self.load_finished_time,
- self.config
- .refresh_interval
- .unwrap_or(DEFAULT_REFRESH_INTERVAL_S),
- ctx.link()
- .callback(|force| if force { Msg::ForceReload } else { Msg::Reload }),
- ctx.link().callback(|_| Msg::ConfigWindow(true)),
- )),
- )
- .with_child(
- Container::new()
- .class("pwt-content-spacer")
- .class(FlexDirection::Row)
- .class(FlexWrap::Wrap)
- .padding_top(0)
- .with_child(
- create_remote_panel(
- self.status.clone(),
- Some(
- ctx.link()
- .callback(|_| Msg::CreateWizard(Some(RemoteType::Pve))),
- ),
- Some(
- ctx.link()
- .callback(|_| Msg::CreateWizard(Some(RemoteType::Pbs))),
- ),
- )
- .flex(1.0)
- .width(300)
- .min_height(175),
- )
- .with_child(
- create_node_panel(Some(RemoteType::Pve), self.status.clone())
- .flex(1.0)
- .width(300),
- )
- .with_child(
- create_guest_panel(Some(GuestType::Qemu), self.status.clone())
- .flex(1.0)
- .width(300),
- )
- .with_child(
- create_guest_panel(Some(GuestType::Lxc), self.status.clone())
- .flex(1.0)
- .width(300),
- )
- // FIXME: add PBS support
- //.with_child(self.create_node_panel(
- // "building-o",
- // tr!("Backup Server Nodes"),
- // &self.status.pbs_nodes,
- //))
- //.with_child(
- // Panel::new()
- // .flex(1.0)
- // .width(300)
- // .title(create_title_with_icon(
- // "floppy-o",
- // tr!("Backup Server Datastores"),
- // ))
- // .border(true)
- // .with_child(if self.loading {
- // Column::new()
- // .padding(4)
- // .class(FlexFit)
- // .class(JustifyContent::Center)
- // .class(AlignItems::Center)
- // .with_child(html! {<i class={"pwt-loading-icon"} />})
- // } else {
- // Column::new()
- // .padding(4)
- // .class(FlexFit)
- // .class(JustifyContent::Center)
- // .gap(2)
- // // FIXME: show more detailed status (usage?)
- // .with_child(
- // Row::new()
- // .gap(2)
- // .with_child(
- // StorageState::Available.to_fa_icon().fixed_width(),
- // )
- // .with_child(tr!("available"))
- // .with_flex_spacer()
- // .with_child(
- // Container::from_tag("span").with_child(
- // self.status.pbs_datastores.available,
- // ),
- // ),
- // )
- // .with_optional_child(
- // (self.status.pbs_datastores.unknown > 0).then_some(
- // Row::new()
- // .gap(2)
- // .with_child(
- // StorageState::Unknown
- // .to_fa_icon()
- // .fixed_width(),
- // )
- // .with_child(tr!("unknown"))
- // .with_flex_spacer()
- // .with_child(
- // Container::from_tag("span").with_child(
- // self.status.pbs_datastores.unknown,
- // ),
- // ),
- // ),
- // )
- // }),
- //)
- .with_child(
- create_subscription_panel()
- .flex(1.0)
- .width(500)
- .min_height(150),
- ),
- )
- .with_child(
- Container::new()
- .class("pwt-content-spacer")
- .class(FlexDirection::Row)
- .class("pwt-align-content-start")
- .padding_top(0)
- .class(FlexWrap::Wrap)
- //.min_height(175)
- .with_child(
- create_top_entities_panel(
- self.top_entities.as_ref().map(|e| e.guest_cpu.clone()),
- self.last_top_entities_error.as_ref(),
- types::LeaderboardType::GuestCpu,
- )
- .flex(1.0)
- .width(500)
- .min_width(400),
- )
- .with_child(
- create_top_entities_panel(
- self.top_entities.as_ref().map(|e| e.node_cpu.clone()),
- self.last_top_entities_error.as_ref(),
- types::LeaderboardType::NodeCpu,
- )
- .flex(1.0)
- .width(500)
- .min_width(400),
- )
- .with_child(
- create_top_entities_panel(
- self.top_entities.as_ref().map(|e| e.node_memory.clone()),
- self.last_top_entities_error.as_ref(),
- types::LeaderboardType::NodeCpu,
- )
- .flex(1.0)
- .width(500)
- .min_width(400),
- ),
- )
- .with_child(
- Container::new()
- .class("pwt-content-spacer")
- .class(FlexDirection::Row)
- .class("pwt-align-content-start")
- .style("padding-top", "0")
- .class(pwt::css::Flex::Fill)
- .class(FlexWrap::Wrap)
- .with_child(
- create_task_summary_panel(
- self.statistics.data.clone(),
- self.statistics.error.as_ref(),
- None,
- hours,
- since,
- )
- .flex(1.0)
- .width(500),
- )
- .with_child(
- create_task_summary_panel(
- self.statistics.data.clone(),
- self.statistics.error.as_ref(),
- Some(5),
- hours,
- since,
- )
- .flex(1.0)
- .width(500),
- )
- .with_child(create_sdn_panel(self.status.clone()).flex(1.0).width(200)),
- );
-
- Panel::new()
- .class(FlexFit)
- .with_child(content)
- .with_optional_child(self.show_wizard.map(|remote_type| {
- AddWizard::new(remote_type)
- .on_close(ctx.link().callback(|_| Msg::CreateWizard(None)))
- .on_submit(move |ctx| crate::remotes::create_remote(ctx, remote_type))
- }))
- .with_optional_child(
- self.show_config_window.then_some(
- create_refresh_config_edit_window("dashboard")
- .on_close(ctx.link().callback(|_| Msg::ConfigWindow(false)))
- .on_submit({
- let link = ctx.link().clone();
- move |ctx: FormContext| {
- let link = link.clone();
- async move {
- let data: RefreshConfig =
- serde_json::from_value(ctx.get_submit_data())?;
- link.send_message(Msg::UpdateConfig(data));
- Ok(())
- }
- }
- }),
- ),
- )
- .into()
- }
-}
-
-impl From<Dashboard> for VNode {
- fn from(val: Dashboard) -> Self {
- let comp = VComp::new::<PdmDashboard>(Rc::new(val), None);
- VNode::from(comp)
- }
-}
+pub use refresh_config_edit::create_refresh_config_edit_window;
fn loading_column() -> Column {
Column::new()
.padding(4)
- .class(FlexFit)
- .class(JustifyContent::Center)
- .class(AlignItems::Center)
+ .class(css::FlexFit)
+ .class(css::JustifyContent::Center)
+ .class(css::AlignItems::Center)
.with_child(html! {<i class={"pwt-loading-icon"} />})
}
/// Create a consistent title component for the given title and icon
-pub fn create_title_with_icon(icon: &str, title: String) -> Html {
+fn create_title_with_icon(icon: &str, title: String) -> Html {
Row::new()
- .class(AlignItems::Center)
+ .class(css::AlignItems::Center)
.gap(2)
.with_child(Fa::new(icon))
.with_child(title)
diff --git a/ui/src/dashboard/view.rs b/ui/src/dashboard/view.rs
index 7159d4e8..6cea9f87 100644
--- a/ui/src/dashboard/view.rs
+++ b/ui/src/dashboard/view.rs
@@ -446,7 +446,66 @@ async fn load_template() -> Result<ViewTemplate, Error> {
\"description\": \"some description\",
\"layout\": {
\"layout-type\": \"rows\",
- \"rows\": []
+ \"rows\": [
+ [
+ {
+ \"flex\": 3.0,
+ \"widget-type\": \"remotes\",
+ \"show-wizard\": true
+ },
+ {
+ \"flex\": 3.0,
+ \"widget-type\": \"nodes\",
+ \"remote-type\": \"pve\"
+ },
+ {
+ \"flex\": 3.0,
+ \"widget-type\": \"guests\",
+ \"guest-type\": \"qemu\"
+ },
+ {
+ \"flex\": 3.0,
+ \"widget-type\": \"guests\",
+ \"guest-type\": \"lxc\"
+ },
+ {
+ \"flex\": 5.0,
+ \"widget-type\": \"subscription\"
+ }
+ ],
+ [
+ {
+ \"widget-type\": \"leaderboard\",
+ \"leaderboard-type\": \"guest-cpu\"
+ },
+ {
+ \"widget-type\": \"leaderboard\",
+ \"leaderboard-type\": \"node-cpu\"
+ },
+ {
+ \"widget-type\": \"leaderboard\",
+ \"leaderboard-type\": \"node-memory\"
+ }
+ ],
+ [
+ {
+ \"flex\": 5.0,
+ \"widget-type\": \"task-summary\",
+ \"grouping\": \"category\",
+ \"sorting\": \"default\"
+ },
+ {
+ \"flex\": 5.0,
+ \"widget-type\": \"task-summary\",
+ \"grouping\": \"remote\",
+ \"sorting\": \"failed-tasks\"
+ },
+ {
+ \"flex\": 2.0,
+ \"widget-type\": \"sdn\"
+ }
+ ]
+ ]
}
}
";
diff --git a/ui/src/lib.rs b/ui/src/lib.rs
index de76e1c0..f9af023d 100644
--- a/ui/src/lib.rs
+++ b/ui/src/lib.rs
@@ -27,7 +27,7 @@ mod search_provider;
pub use search_provider::SearchProvider;
mod dashboard;
-pub use dashboard::Dashboard;
+
use yew_router::prelude::RouterScopeExt;
mod widget;
diff --git a/ui/src/main_menu.rs b/ui/src/main_menu.rs
index 7650b63f..b5044169 100644
--- a/ui/src/main_menu.rs
+++ b/ui/src/main_menu.rs
@@ -13,11 +13,12 @@ use proxmox_yew_comp::{NotesView, XTermJs};
use pdm_api_types::remotes::RemoteType;
+use crate::dashboard::view::View;
use crate::remotes::RemotesPanel;
use crate::sdn::evpn::EvpnPanel;
use crate::sdn::ZoneTree;
use crate::{
- AccessControl, CertificatesPanel, Dashboard, RemoteListCacheEntry, ServerAdministration,
+ AccessControl, CertificatesPanel, RemoteListCacheEntry, ServerAdministration,
SystemConfiguration,
};
@@ -141,7 +142,7 @@ impl Component for PdmMainMenu {
tr!("Dashboard"),
"dashboard",
Some("fa fa-tachometer"),
- move |_| Dashboard::new().into(),
+ move |_| View::new("dashboard").into(),
);
register_view(
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
next prev parent reply other threads:[~2025-10-21 14:08 UTC|newest]
Thread overview: 17+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-10-21 14:03 [pdm-devel] [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 01/15] ui: dashboard: refactor guest panel creation to its own module Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 02/15] ui: dashboard: refactor creating the node panel into " Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 03/15] ui: dashboard: refactor remote panel creation " Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 04/15] ui: dashboard: remote panel: make wizard menu optional Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 05/15] ui: dashboard: refactor sdn panel creation into its own module Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 06/15] ui: dashboard: refactor task summary panel creation to " Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 07/15] ui: dashboard: refactor subscription " Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 08/15] ui: dashboard: refactor top entities " Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 09/15] ui: dashboard: refactor DashboardConfig editing/constants to their module Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 10/15] ui: dashboard: factor out task parameter calculation Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 11/15] ui: dashboard: remove unused remote list Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 12/15] ui: dashboard: status row: make loading less jarring Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 13/15] ui: introduce `LoadResult` helper type Dominik Csapak
2025-10-21 14:03 ` [pdm-devel] [PATCH datacenter-manager 14/15] ui: dashboard: implement 'View' Dominik Csapak
2025-10-21 14:03 ` Dominik Csapak [this message]
2025-10-23 8:33 ` [pdm-devel] superseded: [PATCH datacenter-manager 00/15] prepare ui fore customizable views Dominik Csapak
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20251021140801.3611022-16-d.csapak@proxmox.com \
--to=d.csapak@proxmox.com \
--cc=pdm-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox