From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 8C9291FF187 for ; Tue, 2 Dec 2025 15:32:36 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 629931493A; Tue, 2 Dec 2025 15:33:01 +0100 (CET) From: Dominik Csapak To: pdm-devel@lists.proxmox.com Date: Tue, 2 Dec 2025 15:31:06 +0100 Message-ID: <20251202143226.3681712-6-d.csapak@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20251202143226.3681712-1-d.csapak@proxmox.com> References: <20251202143226.3681712-1-d.csapak@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.030 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] [PATCH datacenter-manager 3/5] ui: configuration: add subscription panel 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" it's very similar to the pve/pbs/pmg one, but only shows relevant information. It also shows the statistics that determines the validity state. Note: the thresholds and calculation is copied from the backend, to further improve this, we should move that code somwhere we can reuse it for both. Signed-off-by: Dominik Csapak --- ui/src/configuration/mod.rs | 2 + ui/src/configuration/subscription_panel.rs | 187 +++++++++++++++++++++ ui/src/main_menu.rs | 18 +- 3 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 ui/src/configuration/subscription_panel.rs diff --git a/ui/src/configuration/mod.rs b/ui/src/configuration/mod.rs index 35114336..18fc3966 100644 --- a/ui/src/configuration/mod.rs +++ b/ui/src/configuration/mod.rs @@ -13,6 +13,8 @@ mod permission_path_selector; mod webauthn; pub use webauthn::WebauthnPanel; +pub mod subscription_panel; + pub mod views; #[function_component(SystemConfiguration)] diff --git a/ui/src/configuration/subscription_panel.rs b/ui/src/configuration/subscription_panel.rs new file mode 100644 index 00000000..797eab0e --- /dev/null +++ b/ui/src/configuration/subscription_panel.rs @@ -0,0 +1,187 @@ +use std::future::Future; +use std::pin::Pin; +use std::rc::Rc; + +use pdm_api_types::subscription::SubscriptionStatistics; +use serde_json::Value; + +use yew::virtual_dom::{VComp, VNode}; + +use pwt::prelude::*; +use pwt::widget::{error_message, Button, Column, Container, Row, Toolbar}; + +use proxmox_yew_comp::{http_get, http_post, KVGrid, KVGridRow}; +use proxmox_yew_comp::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster}; + +const SUBSCRIPTION_URL: &str = "/nodes/localhost/subscription"; + +#[derive(Properties, PartialEq, Clone)] +pub struct SubscriptionPanel {} + +impl SubscriptionPanel { + pub fn new() -> Self { + yew::props!(Self {}) + } +} + +pub enum Msg { + LoadFinished(Value), +} + +pub struct ProxmoxSubscriptionPanel { + rows: Rc>, + data: Option>, +} + +impl LoadableComponent for ProxmoxSubscriptionPanel { + type Message = Msg; + type Properties = SubscriptionPanel; + type ViewState = (); + + fn create(_ctx: &LoadableComponentContext) -> Self { + Self { + rows: Rc::new(rows()), + data: None, + } + } + + fn update(&mut self, _ctx: &LoadableComponentContext, msg: Self::Message) -> bool { + match msg { + Msg::LoadFinished(value) => { + self.data = Some(Rc::new(value)); + } + } + true + } + + fn load( + &self, + _ctx: &LoadableComponentContext, + ) -> Pin>>> { + let link = _ctx.link().clone(); + Box::pin(async move { + let info = http_get(SUBSCRIPTION_URL, None).await?; + link.send_message(Msg::LoadFinished(info)); + Ok(()) + }) + } + + fn toolbar(&self, ctx: &LoadableComponentContext) -> Option { + let toolbar = Toolbar::new() + .class("pwt-overflow-hidden") + .border_bottom(true) + .with_child( + Button::new(tr!("Check")) + .icon_class("fa fa-check-square-o") + .on_activate({ + let link = ctx.link(); + + move |_| { + link.spawn({ + let link = link.clone(); + async move { + match http_post(SUBSCRIPTION_URL, None).await { + Ok(()) => link.send_reload(), + Err(err) => { + link.show_error(tr!("Error"), err.to_string(), true) + } + } + } + }) + } + }), + ) + .with_spacer() + .with_flex_spacer() + .with_child({ + let loading = ctx.loading(); + let link = ctx.link(); + Button::refresh(loading).on_activate(move |_| link.send_reload()) + }); + + Some(toolbar.into()) + } + + fn main_view(&self, _ctx: &LoadableComponentContext) -> Html { + let data = match &self.data { + Some(data) => data.clone(), + None => Rc::new(Value::Null), + }; + + KVGrid::new() + .class("pwt-flex-fit") + .data(data.clone()) + .rows(Rc::clone(&self.rows)) + .into() + } +} + +impl From for VNode { + fn from(val: SubscriptionPanel) -> Self { + let comp = + VComp::new::>(Rc::new(val), None); + VNode::from(comp) + } +} + +// FIXME: ratios copied from backend + +// minimum ratio of nodes with active subscriptions +const SUBSCRIPTION_THRESHOLD: f64 = 0.9; +// max ratio of nodes with community subscriptions, among nodes with subscriptions +const COMMUNITY_THRESHOLD: f64 = 0.4; + +fn rows() -> Vec { + vec![ + KVGridRow::new("status", tr!("Status")).renderer(move |_name, value, record| { + let value = match value { + Value::String(data) => data, + Value::Null => return Container::from_tag("i").class("pwt-loading-icon").into(), + _ => return error_message(&tr!("invalid data")).into(), + }; + match record["message"].as_str() { + Some(msg) => format!("{value}: {msg}").into(), + None => value.into(), + } + }), + KVGridRow::new("statistics", tr!("Statistics")).renderer(move |_name, value, _record| { + let statistics = serde_json::from_value::(value.clone()); + match statistics { + Ok(stats) => { + let subscribed_ratio = + stats.active_subscriptions as f64 / stats.total_nodes as f64; + let community_ratio = + stats.community as f64 / stats.active_subscriptions as f64; + + fn operator(a: f64, b: f64) -> &'static str { + if a >= b { + ">=" + } else { + "<" + } + } + + Column::new() + .with_child(Row::new().with_child(tr!( + "Subscribed Ratio: {0} ({1} {2})", + format!("{:.0}%", subscribed_ratio * 100.0), + operator(subscribed_ratio, SUBSCRIPTION_THRESHOLD), + format!("{:.0}%", SUBSCRIPTION_THRESHOLD * 100.0), + ))) + .with_child(Row::new().with_child(tr!( + "Community Ratio: {0} ({1} {2})", + format!("{:.0}%", community_ratio * 100.0), + operator(community_ratio, COMMUNITY_THRESHOLD), + format!("{:.0}%", COMMUNITY_THRESHOLD * 100.0), + ))) + .into() + } + Err(err) => error_message(&format!("api error: {err}")).into(), + } + }), + KVGridRow::new("url", tr!("Info URL")).renderer(|_name, value, _record| { + let url = value.as_str().unwrap().to_string(); + html! { {url} } + }), + ] +} diff --git a/ui/src/main_menu.rs b/ui/src/main_menu.rs index 0847c7a2..18988eaf 100644 --- a/ui/src/main_menu.rs +++ b/ui/src/main_menu.rs @@ -7,13 +7,14 @@ use pwt::css::{self, Display, FlexFit}; use pwt::prelude::*; use pwt::state::{NavigationContextExt, Selection}; use pwt::widget::nav::{Menu, MenuItem, NavigationDrawer}; -use pwt::widget::{Container, Row, SelectionView, SelectionViewRenderInfo}; +use pwt::widget::{Container, Panel, Row, SelectionView, SelectionViewRenderInfo}; use proxmox_yew_comp::{AclContext, NotesView, XTermJs}; use pdm_api_types::remotes::RemoteType; use pdm_api_types::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY}; +use crate::configuration::subscription_panel::SubscriptionPanel; use crate::configuration::views::ViewGrid; use crate::dashboard::view::View; use crate::remotes::RemotesPanel; @@ -266,6 +267,21 @@ impl Component for PdmMainMenu { |_| html! {}, ); + register_view( + &mut config_submenu, + &mut content, + tr!("Subscription"), + "subscription", + Some("fa fa-support"), + |_| { + Panel::new() + .class(css::FlexFit) + .title(tr!("Subscription")) + .with_child(SubscriptionPanel::new()) + .into() + }, + ); + register_submenu( &mut menu, &mut content, -- 2.47.3 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel