public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pdm-devel] [PATCH datacenter-manager v3] fix #6797: ui: add dialog to view remote subscriptions
@ 2025-11-05 12:45 Shan Shaji
  0 siblings, 0 replies; only message in thread
From: Shan Shaji @ 2025-11-05 12:45 UTC (permalink / raw)
  To: pdm-devel

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 v2:
 - Rebased with master as there were conflicts.
 - updated the dialog show implementation based on the latest master
   changes.

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  |  51 +++++-
 ui/src/dashboard/subscriptions_list.rs | 206 +++++++++++++++++++++++++
 ui/src/dashboard/view.rs               |   2 +-
 4 files changed, 254 insertions(+), 8 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 c56a3fa..31581c1 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -8,6 +8,9 @@ pub use top_entities::create_top_entities_panel;
 mod subscription_info;
 pub use subscription_info::create_subscription_panel;
 
+mod subscriptions_list;
+pub use subscriptions_list::SubscriptionsList;
+
 mod remote_panel;
 pub use remote_panel::create_remote_panel;
 
diff --git a/ui/src/dashboard/subscription_info.rs b/ui/src/dashboard/subscription_info.rs
index cf8d726..a420601 100644
--- a/ui/src/dashboard/subscription_info.rs
+++ b/ui/src/dashboard/subscription_info.rs
@@ -9,7 +9,7 @@ use yew::{
 
 use proxmox_yew_comp::Status;
 use pwt::prelude::*;
-use pwt::widget::{Column, Container, Fa, Panel, Row};
+use pwt::widget::{Column, Container, Dialog, Fa, Panel, Row};
 use pwt::{
     css::{AlignItems, FlexFit, JustifyContent, TextAlign},
     state::SharedState,
@@ -17,7 +17,7 @@ use pwt::{
 
 use pdm_api_types::subscription::{RemoteSubscriptionState, RemoteSubscriptions};
 
-use crate::LoadResult;
+use crate::{dashboard::SubscriptionsList, LoadResult};
 
 #[derive(Properties, PartialEq)]
 pub struct SubscriptionInfo {
@@ -30,7 +30,13 @@ impl SubscriptionInfo {
     }
 }
 
-struct PdmSubscriptionInfo {}
+enum Msg {
+    ShowDialog(Option<Dialog>),
+}
+
+struct PdmSubscriptionInfo {
+    dialog: Option<Dialog>,
+}
 
 fn render_subscription_status(subs: &[RemoteSubscriptions]) -> Row {
     let mut none = 0;
@@ -97,19 +103,49 @@ fn render_subscription_status(subs: &[RemoteSubscriptions]) -> Row {
 }
 
 impl Component for PdmSubscriptionInfo {
-    type Message = ();
+    type Message = Msg;
     type Properties = SubscriptionInfo;
 
     fn create(_ctx: &yew::Context<Self>) -> Self {
-        Self {}
+        Self { dialog: None }
+    }
+
+    fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
+        match msg {
+            Msg::ShowDialog(dialog) => {
+                self.dialog = dialog;
+                true
+            }
+        }
     }
 
     fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
         let props = ctx.props();
-        Column::new()
+
+        let mut column = Column::new()
             .class(FlexFit)
             .class(JustifyContent::Center)
-            .class(AlignItems::Center)
+            .class(AlignItems::Center);
+
+        if let Some(subs) = props.subs.as_ref() {
+            let dialog = Dialog::new(tr!("Your Subscriptions"))
+                .resizable(true)
+                .width(500)
+                .height(400)
+                .min_width(200)
+                .min_height(50)
+                .with_child(SubscriptionsList::new(subs.clone()))
+                .on_close(ctx.link().callback(|_| Msg::ShowDialog(None)));
+
+            column = column
+                .onclick(
+                    ctx.link()
+                        .callback(move |_| Msg::ShowDialog(Some(dialog.clone()))),
+                )
+                .style("cursor", "pointer");
+        }
+
+        column
             .with_optional_child(
                 props.subs.is_none().then_some(
                     Container::new()
@@ -123,6 +159,7 @@ impl Component for PdmSubscriptionInfo {
                     .as_ref()
                     .map(|subs| render_subscription_status(subs)),
             )
+            .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)
+    }
+}
diff --git a/ui/src/dashboard/view.rs b/ui/src/dashboard/view.rs
index c781d99..aab8d26 100644
--- a/ui/src/dashboard/view.rs
+++ b/ui/src/dashboard/view.rs
@@ -179,7 +179,7 @@ impl ViewComp {
                 };
 
                 let subs_future = async {
-                    let res = http_get("/resources/subscription", None).await;
+                    let res = http_get("/resources/subscription?verbose=true", None).await;
                     link.send_message(Msg::LoadingResult(LoadingResult::SubscriptionInfo(res)));
                 };
 
-- 
2.47.3



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


^ permalink raw reply	[flat|nested] only message in thread

only message in thread, other threads:[~2025-11-05 12:45 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-11-05 12:45 [pdm-devel] [PATCH datacenter-manager v3] fix #6797: ui: add dialog to view remote subscriptions Shan Shaji

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal