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 0DB631FF165 for ; Thu, 23 Oct 2025 10:32:41 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 66A1A37E9; Thu, 23 Oct 2025 10:33:08 +0200 (CEST) From: Dominik Csapak To: pdm-devel@lists.proxmox.com Date: Thu, 23 Oct 2025 10:28:20 +0200 Message-ID: <20251023083253.1038119-17-d.csapak@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20251023083253.1038119-1-d.csapak@proxmox.com> References: <20251023083253.1038119-1-d.csapak@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.028 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: [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 Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" 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 - //.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( -- 2.47.3 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel