From: Shan Shaji <s.shaji@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager v3] fix #6797: ui: add dialog to view remote subscriptions
Date: Wed, 5 Nov 2025 13:45:05 +0100 [thread overview]
Message-ID: <20251105124505.189918-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 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
reply other threads:[~2025-11-05 12:45 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=20251105124505.189918-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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox