all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Shan Shaji <s.shaji@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager v2] fix #6797: ui: add dialog to view remote subscriptions
Date: Wed, 22 Oct 2025 16:32:04 +0200	[thread overview]
Message-ID: <20251022143204.381727-1-s.shaji@proxmox.com> (raw)

When clicking on the subscription status panel, no details appeared that
will help the user to see the subscription details of remote/nodes. For
example if a remote had a "mixed" subscription state, users couldn't see
which remote had that state.

Fixed the issue by adding a subscription dialog. Now, when the users
click the subscription status panel, a dialog opens showing the remote
subscription state and the individual subscriptions for each node.

Signed-off-by: Shan Shaji <s.shaji@proxmox.com>
---
 changes since v1: Thanks @Shannon
 - Removed unused imports. 
 - Removed the use multiple messages for showing dialog, and instead 
   used a single message that accepts `Option<Dialog>`. Also added
   `Option<Dialog>` as a component property. Now, this property is
   updated whenever the `ShowDialog` message is called. 
 - Reorder the module imports in `subscptions_list.rs` file. 
 
 ui/src/dashboard/mod.rs                |   3 +
 ui/src/dashboard/subscription_info.rs  |  70 +++++++--
 ui/src/dashboard/subscriptions_list.rs | 206 +++++++++++++++++++++++++
 3 files changed, 263 insertions(+), 16 deletions(-)
 create mode 100644 ui/src/dashboard/subscriptions_list.rs

diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index f54c509..ec5df9b 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -42,6 +42,9 @@ pub use top_entities::TopEntities;
 mod subscription_info;
 pub use subscription_info::SubscriptionInfo;
 
+mod subscriptions_list;
+pub use subscriptions_list::SubscriptionsList;
+
 mod remote_panel;
 use remote_panel::RemotePanel;
 
diff --git a/ui/src/dashboard/subscription_info.rs b/ui/src/dashboard/subscription_info.rs
index 08658d1..c545a80 100644
--- a/ui/src/dashboard/subscription_info.rs
+++ b/ui/src/dashboard/subscription_info.rs
@@ -12,14 +12,17 @@ use pwt::{
     css::{AlignItems, FlexFit, JustifyContent, TextAlign},
     prelude::tr,
     props::{
-        ContainerBuilder, CssBorderBuilder, CssPaddingBuilder, WidgetBuilder, WidgetStyleBuilder,
+        ContainerBuilder, CssBorderBuilder, CssPaddingBuilder, EventSubscriber, WidgetBuilder,
+        WidgetStyleBuilder,
     },
-    widget::{Column, Container, Fa, Panel, Row},
+    widget::{Column, Container, Dialog, Fa, Panel, Row},
     AsyncPool,
 };
 
 use pdm_api_types::subscription::{RemoteSubscriptionState, RemoteSubscriptions};
 
+use super::SubscriptionsList;
+
 #[derive(Properties, PartialEq)]
 pub struct SubscriptionInfo {}
 
@@ -29,10 +32,16 @@ impl SubscriptionInfo {
     }
 }
 
+enum Msg {
+    LoadResults(Result<Vec<RemoteSubscriptions>, Error>),
+    ShowDialog(Option<Dialog>),
+}
+
 struct PdmSubscriptionInfo {
     status: Vec<RemoteSubscriptions>,
     loading: bool,
     _async_pool: AsyncPool,
+    dialog: Option<Dialog>,
 }
 
 fn render_subscription_status(subs: &[RemoteSubscriptions]) -> Row {
@@ -100,7 +109,7 @@ fn render_subscription_status(subs: &[RemoteSubscriptions]) -> Row {
 }
 
 impl Component for PdmSubscriptionInfo {
-    type Message = Result<Vec<RemoteSubscriptions>, Error>;
+    type Message = Msg;
 
     type Properties = SubscriptionInfo;
 
@@ -108,31 +117,36 @@ impl Component for PdmSubscriptionInfo {
         let link = ctx.link().clone();
         let mut _async_pool = AsyncPool::new();
         _async_pool.spawn(async move {
-            let result = http_get("/resources/subscription", None).await;
-            link.send_message(result);
+            let result = http_get("/resources/subscription?verbose=true", None).await;
+            link.send_message(Msg::LoadResults(result));
         });
 
         Self {
             status: Vec::new(),
             loading: true,
             _async_pool,
+            dialog: None,
         }
     }
 
     fn update(&mut self, _ctx: &yew::Context<Self>, msg: Self::Message) -> bool {
         match msg {
-            Ok(result) => {
-                self.status = result;
+            Msg::LoadResults(result) => {
+                self.status = match result {
+                    Ok(result) => result,
+                    Err(_) => Vec::new(),
+                };
+                self.loading = false;
+                true
+            }
+            Msg::ShowDialog(dialog) => {
+                self.dialog = dialog;
+                true
             }
-            Err(_) => self.status = Vec::new(),
         }
-
-        self.loading = false;
-
-        true
     }
 
-    fn view(&self, _ctx: &yew::Context<Self>) -> yew::Html {
+    fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
         let title: Html = Row::new()
             .class(AlignItems::Center)
             .gap(2)
@@ -140,12 +154,35 @@ impl Component for PdmSubscriptionInfo {
             .with_child(tr!("Subscription Status"))
             .into();
 
-        Panel::new()
+        let panel = Panel::new()
             .flex(1.0)
             .width(500)
             .min_height(150)
             .title(title)
-            .border(true)
+            .border(true);
+
+        let is_panel_clickable = !self.loading && !self.status.is_empty();
+        let panel = if is_panel_clickable {
+            let dialog = Dialog::new(tr!("Your Subscriptions"))
+                .resizable(true)
+                .width(500)
+                .height(400)
+                .min_width(200)
+                .min_height(50)
+                .with_child(SubscriptionsList::new(self.status.clone()))
+                .on_close(ctx.link().callback(|_| Msg::ShowDialog(None)));
+
+            panel
+                .onclick(
+                    ctx.link()
+                        .callback(move |_| Msg::ShowDialog(Some(dialog.clone()))),
+                )
+                .style("cursor", "pointer")
+        } else {
+            panel
+        };
+
+        panel
             .with_child(
                 Column::new()
                     .class(FlexFit)
@@ -157,7 +194,8 @@ impl Component for PdmSubscriptionInfo {
                     )
                     .with_optional_child(
                         (!self.loading).then_some(render_subscription_status(&self.status)),
-                    ),
+                    )
+                    .with_optional_child(self.dialog.clone()),
             )
             .into()
     }
diff --git a/ui/src/dashboard/subscriptions_list.rs b/ui/src/dashboard/subscriptions_list.rs
new file mode 100644
index 0000000..7a13c52
--- /dev/null
+++ b/ui/src/dashboard/subscriptions_list.rs
@@ -0,0 +1,206 @@
+use std::rc::Rc;
+
+use yew::{
+    html,
+    virtual_dom::{Key, VComp, VNode},
+    Html,
+    Component, Properties
+};
+
+use pdm_api_types::subscription::{
+    RemoteSubscriptionState, RemoteSubscriptions, SubscriptionLevel,
+};
+use proxmox_yew_comp::Status;
+use pwt::{
+    css::{AlignItems, Overflow},
+    props::{ContainerBuilder, ExtractPrimaryKey, WidgetBuilder},
+    state::{KeyedSlabTree, TreeStore},
+    tr,
+    widget::{
+        data_table::{DataTable, DataTableColumn, DataTableHeader},
+        Fa, Row,
+    },
+};
+
+#[derive(Properties, PartialEq)]
+pub struct SubscriptionsList {
+    subscriptions: Vec<RemoteSubscriptions>,
+}
+
+impl SubscriptionsList {
+    pub fn new(subscriptions: Vec<RemoteSubscriptions>) -> Self {
+        yew::props!(Self { subscriptions })
+    }
+}
+
+pub struct PdmSubscriptionsList {
+    store: TreeStore<SubscriptionTreeEntry>,
+}
+
+#[derive(Clone, PartialEq)]
+struct RemoteEntry {
+    name: String,
+    state: RemoteSubscriptionState,
+    error: Option<String>,
+}
+
+#[derive(Clone, PartialEq)]
+struct NodeEntry {
+    remote: String,
+    name: String,
+    level: SubscriptionLevel,
+}
+
+#[derive(Clone, PartialEq)]
+enum SubscriptionTreeEntry {
+    Root,
+    Remote(RemoteEntry),
+    Node(NodeEntry),
+}
+
+impl SubscriptionTreeEntry {
+    fn name(&self) -> &str {
+        match self {
+            SubscriptionTreeEntry::Root => "",
+            SubscriptionTreeEntry::Remote(remote_entry) => &remote_entry.name,
+            SubscriptionTreeEntry::Node(node_entry) => &node_entry.name,
+        }
+    }
+}
+
+impl ExtractPrimaryKey for SubscriptionTreeEntry {
+    fn extract_key(&self) -> Key {
+        match self {
+            SubscriptionTreeEntry::Root => Key::from("root"),
+            SubscriptionTreeEntry::Remote(remote) => Key::from(format!("{}", remote.name)),
+            SubscriptionTreeEntry::Node(node) => {
+                Key::from(format!("{}/{}", node.remote, node.name))
+            }
+        }
+    }
+}
+
+impl Component for PdmSubscriptionsList {
+    type Message = ();
+    type Properties = SubscriptionsList;
+
+    fn create(ctx: &yew::Context<Self>) -> Self {
+        let subscriptions = sort_subscriptions(&ctx.props().subscriptions);
+
+        let store = TreeStore::new().view_root(false);
+        let mut tree = KeyedSlabTree::new();
+        let mut root = tree.set_root(SubscriptionTreeEntry::Root);
+        root.set_expanded(true);
+
+        for remote in subscriptions {
+            let mut remote_node = root.append(SubscriptionTreeEntry::Remote(RemoteEntry {
+                name: remote.remote.clone(),
+                state: remote.state.clone(),
+                error: remote.error.clone(),
+            }));
+
+            if let Some(node_status) = remote.node_status.as_ref() {
+                if node_status.is_empty() {
+                    continue;
+                }
+
+                for (node_name, info) in node_status {
+                    if let Some(info) = info {
+                        remote_node.append(SubscriptionTreeEntry::Node(NodeEntry {
+                            remote: remote.remote.clone(),
+                            name: node_name.clone(),
+                            level: info.level.clone(),
+                        }));
+                    }
+                }
+            }
+        }
+
+        store.write().update_root_tree(tree);
+        Self { store }
+    }
+
+    fn view(&self, _ctx: &yew::Context<Self>) -> Html {
+        DataTable::new(columns(self.store.clone()), self.store.clone())
+            .class(Overflow::Auto)
+            .into()
+    }
+}
+
+fn columns(
+    store: TreeStore<SubscriptionTreeEntry>,
+) -> Rc<Vec<DataTableHeader<SubscriptionTreeEntry>>> {
+    let tree_column = DataTableColumn::new(tr!("Remote"))
+        .tree_column(store)
+        .render(|entry: &SubscriptionTreeEntry| {
+            let row = Row::new().class(AlignItems::Center).gap(2);
+            match entry {
+                SubscriptionTreeEntry::Remote(remote) => row.with_child(remote.name.clone()).into(),
+                SubscriptionTreeEntry::Node(node) => row
+                    .with_child(Fa::new("server"))
+                    .with_child(node.name.clone())
+                    .into(),
+                SubscriptionTreeEntry::Root => row.into(),
+            }
+        })
+        .sorter(|a: &SubscriptionTreeEntry, b: &SubscriptionTreeEntry| a.name().cmp(b.name()))
+        .into();
+
+    let subscription_column = DataTableColumn::new(tr!("Subcription"))
+        .render(|entry: &SubscriptionTreeEntry| match entry {
+            SubscriptionTreeEntry::Node(node) => {
+                let text = match node.level {
+                    SubscriptionLevel::None => "None",
+                    SubscriptionLevel::Basic => "Basic",
+                    SubscriptionLevel::Community => "Community",
+                    SubscriptionLevel::Premium => "Premium",
+                    SubscriptionLevel::Standard => "Standard",
+                    SubscriptionLevel::Unknown => "Unknown",
+                };
+                text.into()
+            }
+            SubscriptionTreeEntry::Remote(remote) => {
+                if let Some(error) = &remote.error {
+                    html! { <span class="pwt-font-label-small">{error}</span> }.into()
+                } else {
+                    let icon = match remote.state {
+                        RemoteSubscriptionState::Mixed => Fa::from(Status::Warning),
+                        RemoteSubscriptionState::Active => Fa::from(Status::Success),
+                        _ => Fa::from(Status::Unknown),
+                    };
+
+                    let text = match remote.state {
+                        RemoteSubscriptionState::None => "None",
+                        RemoteSubscriptionState::Unknown => "Unknown",
+                        RemoteSubscriptionState::Mixed => "Mixed",
+                        RemoteSubscriptionState::Active => "Active",
+                    };
+
+                    Row::new().gap(2).with_child(icon).with_child(text).into()
+                }
+            }
+            SubscriptionTreeEntry::Root => "".into(),
+        })
+        .into();
+
+    Rc::new(vec![tree_column, subscription_column])
+}
+
+fn sort_subscriptions(subs: &[RemoteSubscriptions]) -> Vec<RemoteSubscriptions> {
+    let mut subscriptions = subs.to_vec();
+    subscriptions.sort_by_key(|rs| match rs.state {
+        RemoteSubscriptionState::None => 0,
+        RemoteSubscriptionState::Unknown => 1,
+        RemoteSubscriptionState::Mixed => 2,
+        RemoteSubscriptionState::Active => 3,
+    });
+    subscriptions
+}
+
+
+impl From<SubscriptionsList> for VNode {
+    fn from(val: SubscriptionsList) -> Self {
+        let comp = VComp::new::<PdmSubscriptionsList>(Rc::new(val), None);
+        VNode::from(comp)
+    }
+}
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


                 reply	other threads:[~2025-10-22 14:31 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

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=20251022143204.381727-1-s.shaji@proxmox.com \
    --to=s.shaji@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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal