From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id B889C1FF165 for ; Thu, 23 Oct 2025 13:20:07 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 3B4E17EAB; Thu, 23 Oct 2025 13:20:35 +0200 (CEST) Mime-Version: 1.0 Date: Thu, 23 Oct 2025 13:19:56 +0200 Message-Id: To: "Dominik Csapak" X-Mailer: aerc 0.20.0 References: <20251023083253.1038119-1-d.csapak@proxmox.com> <20251023083253.1038119-17-d.csapak@proxmox.com> In-Reply-To: <20251023083253.1038119-17-d.csapak@proxmox.com> From: "Shannon Sterz" X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1761218388691 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.056 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: Re: [pdm-devel] [RFC PATCH datacenter-manager v2 16/16] ui: dashboard: use 'View' instead of the Dashboard X-BeenThere: pdm-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Datacenter Manager development discussion Cc: Proxmox Datacenter Manager development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" On Thu Oct 23, 2025 at 10:28 AM CEST, Dominik Csapak wrote: > 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 > --- > changes from v1: > * correctly mark as RFC > > 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), > - TopEntities(Result), > - TaskStatistics(Result), > - All, > -} > - > -pub enum Msg { > - LoadingFinished(LoadingResult), > - CreateWizard(Option), > - Reload, > - ForceReload, > - UpdateConfig(RefreshConfig), > - ConfigWindow(bool), > -} > - > -struct StatisticsOptions { > - data: Option, > - error: Option, > -} > - > -pub struct PdmDashboard { > - status: Option, > - last_error: Option, > - top_entities: Option, > - last_top_entities_error: Option, > - statistics: StatisticsOptions, > - load_finished_time: Option, > - show_wizard: Option, > - show_config_window: bool, > - async_pool: AsyncPool, > - config: PersistentState, > -} > - > -impl PdmDashboard { > - fn reload(&mut self, ctx: &yew::Context) { > - 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, 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 = > - 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 = > - 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 { > - let config: PersistentState = > - 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, 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) -> 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 this patch removes these FIXMEs without really addressing the issue. i understand that this is just due to showcasing how the new view feature can replace the dashboard, but it'd be nice to preserve them somewhere imo. > - //.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! {}) > - // } 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 for VNode { > - fn from(val: Dashboard) -> Self { > - let comp = VComp::new::(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! {}) > } > > /// 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 { > \"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( _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel