From: Lukas Wagner <l.wagner@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager 09/13] ui: remote updates: show repository status column
Date: Thu, 27 Nov 2025 11:44:43 +0100 [thread overview]
Message-ID: <20251127104447.162951-15-l.wagner@proxmox.com> (raw)
In-Reply-To: <20251127104447.162951-1-l.wagner@proxmox.com>
Add a status column for possible repository configuration issues. Due to
space constraints this commit removes the text representation from the
pre-existing update column ('Up-to-date', '42 updates pending') and
replaces it with an icon only (up-to-date, checkmark) or an icon with a
number (number of pending updates). The status text is moved to a
tooltip, but IMO the icon only view is clear enough to understand
without the tool tip. The error message that was previously shown there
will be moved somewhere else in a future commit.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
ui/src/remotes/updates.rs | 176 ++++++++++++++++++++++++--------------
1 file changed, 113 insertions(+), 63 deletions(-)
diff --git a/ui/src/remotes/updates.rs b/ui/src/remotes/updates.rs
index e13e3831..1346b7f7 100644
--- a/ui/src/remotes/updates.rs
+++ b/ui/src/remotes/updates.rs
@@ -9,7 +9,7 @@ use yew::virtual_dom::{Key, VComp, VNode};
use yew::{html, Html, Properties};
use pdm_api_types::remote_updates::{
- NodeUpdateStatus, NodeUpdateSummary, RemoteUpdateStatus, UpdateSummary,
+ NodeUpdateStatus, NodeUpdateSummary, ProductRepositoryStatus, RemoteUpdateStatus, UpdateSummary,
};
use pdm_api_types::remotes::RemoteType;
use pwt::css::{AlignItems, FlexFit, TextAlign};
@@ -19,7 +19,7 @@ use proxmox_yew_comp::{
AptPackageManager, LoadableComponent, LoadableComponentContext, LoadableComponentMaster,
};
use pwt::props::{CssBorderBuilder, CssPaddingBuilder, WidgetStyleBuilder};
-use pwt::widget::{Button, Container, Panel};
+use pwt::widget::{Button, Container, Panel, Tooltip};
use pwt::{
css,
css::FontColor,
@@ -59,6 +59,7 @@ struct RemoteEntry {
number_of_failed_nodes: u32,
number_of_nodes: u32,
number_of_updatable_nodes: u32,
+ repo_status: ProductRepositoryStatus,
poll_status: RemoteUpdateStatus,
}
@@ -153,17 +154,19 @@ impl UpdateTreeComponent {
},
))
.into(),
- DataTableColumn::new(tr!("Status"))
- .flex(6)
+ DataTableColumn::new(tr!("Update Status"))
+ .flex(1)
.render_cell(DataTableCellRenderer::new(
- move |args: &mut DataTableCellRenderArgs<UpdateTreeEntry>| match args.record() {
- UpdateTreeEntry::Root => {
- html!()
- }
- UpdateTreeEntry::Remote(remote_info) => {
- render_remote_summary(remote_info, args.is_expanded()).into()
- }
- UpdateTreeEntry::Node(info) => render_node_info(info).into(),
+ move |args: &mut DataTableCellRenderArgs<UpdateTreeEntry>| {
+ render_update_status_column(args.record(), args.is_expanded())
+ },
+ ))
+ .into(),
+ DataTableColumn::new(tr!("Repository Status"))
+ .flex(1)
+ .render_cell(DataTableCellRenderer::new(
+ move |args: &mut DataTableCellRenderArgs<UpdateTreeEntry>| {
+ render_repo_status_column(args.record(), args.is_expanded())
},
))
.into(),
@@ -200,6 +203,7 @@ fn build_store_from_response(update_summary: UpdateSummary) -> SlabTree<UpdateTr
number_of_nodes: 0,
number_of_updatable_nodes: 0,
number_of_failed_nodes: 0,
+ repo_status: ProductRepositoryStatus::Ok,
poll_status: remote_summary.status.clone(),
}));
remote_entry.set_expanded(false);
@@ -209,6 +213,7 @@ fn build_store_from_response(update_summary: UpdateSummary) -> SlabTree<UpdateTr
let number_of_nodes = remote_summary.nodes.len();
let mut number_of_updatable_nodes = 0;
let mut number_of_failed_nodes = 0;
+ let mut repo_status = ProductRepositoryStatus::Ok;
// use a BTreeMap to get a stable order. Can be removed once there is proper version
// comparison in place.
@@ -226,6 +231,9 @@ fn build_store_from_response(update_summary: UpdateSummary) -> SlabTree<UpdateTr
}
}
+ if node_summary.repository_status > repo_status {
+ repo_status = node_summary.repository_status;
+ }
let entry = NodeEntry {
remote: remote_name.clone(),
node: node_name.clone(),
@@ -255,6 +263,7 @@ fn build_store_from_response(update_summary: UpdateSummary) -> SlabTree<UpdateTr
info.number_of_failed_nodes = number_of_failed_nodes as u32;
info.product_version = product_version;
info.mixed_versions = mixed_versions;
+ info.repo_status = repo_status;
}
}
@@ -470,7 +479,19 @@ impl UpdateTreeComponent {
}
}
-fn render_remote_summary(entry: &RemoteEntry, expanded: bool) -> Row {
+fn render_update_status_column(tree_entry: &UpdateTreeEntry, expanded: bool) -> Html {
+ match tree_entry {
+ UpdateTreeEntry::Root => {
+ html!()
+ }
+ UpdateTreeEntry::Remote(remote_info) => {
+ render_remote_update_status(remote_info, expanded).into()
+ }
+ UpdateTreeEntry::Node(node_info) => render_node_update_status(node_info).into(),
+ }
+}
+
+fn render_remote_update_status(entry: &RemoteEntry, expanded: bool) -> Row {
let mut row = Row::new().class(css::AlignItems::Baseline).gap(2);
match entry.poll_status {
RemoteUpdateStatus::Success => {
@@ -479,71 +500,98 @@ fn render_remote_summary(entry: &RemoteEntry, expanded: bool) -> Row {
- entry.number_of_updatable_nodes
- entry.number_of_failed_nodes;
- let text = if entry.number_of_nodes == up_to_date_nodes {
- row = row.with_child(render_remote_summary_icon(RemoteSummaryIcon::UpToDate));
- tr!("All nodes up-to-date")
+ if entry.number_of_nodes == up_to_date_nodes {
+ row = row.with_child(render_status_icon(StatusIcon::Ok));
} else if entry.number_of_updatable_nodes > 0 {
- row = row.with_child(render_remote_summary_icon(RemoteSummaryIcon::Updatable));
+ row = row.with_child(render_status_icon(StatusIcon::Updatable));
if entry.number_of_failed_nodes > 0 {
- row = row.with_child(render_remote_summary_icon(RemoteSummaryIcon::Error));
- // NOTE: This 'summary' line is only shown for remotes with multiple nodes,
- // so we don't really have to consider the singular form of 'x out of y
- // nodes'
- tr!("Some nodes have pending updates, some nodes unavailable")
- } else {
- tr!("Some nodes have pending updates")
+ row = row.with_child(render_status_icon(StatusIcon::Error));
}
} else if entry.number_of_failed_nodes > 0 {
- row = row.with_child(render_remote_summary_icon(RemoteSummaryIcon::Error));
- tr!("Some nodes unavailable")
- } else {
- String::new()
- };
-
- row = row.with_child(text);
+ row = row.with_child(render_status_icon(StatusIcon::Error));
+ }
}
}
RemoteUpdateStatus::Error => {
- row = row.with_child(render_remote_summary_icon(RemoteSummaryIcon::Error));
- row = row.with_child(tr!("Could not connect to remote"));
+ row = row.with_child(render_status_icon(StatusIcon::Error));
}
RemoteUpdateStatus::Unknown => {
- row = row.with_child(render_remote_summary_icon(RemoteSummaryIcon::Unknown));
- row = row.with_child(tr!("Update status unknown"));
+ row = row.with_child(render_status_icon(StatusIcon::Unknown));
}
}
row
}
-fn render_node_info(entry: &NodeEntry) -> Row {
- let (icon, text) = if entry.summary.status == NodeUpdateStatus::Error {
- let icon = render_remote_summary_icon(RemoteSummaryIcon::Error);
- let text = if let Some(status) = &entry.summary.status_message {
- tr!("Failed to retrieve update status: {}", status)
+fn render_node_update_status(entry: &NodeEntry) -> Html {
+ let mut row = Row::new().class(css::AlignItems::Baseline).gap(2);
+
+ let tooltip = if entry.summary.status == NodeUpdateStatus::Error {
+ row = row.with_child(render_status_icon(StatusIcon::Error));
+ if let Some(status) = &entry.summary.status_message {
+ tr!("Failed to retrieve update status: {0}", status)
} else {
tr!("Unknown error")
- };
-
- (icon, text)
+ }
} else if entry.summary.number_of_updates > 0 {
- (
- render_remote_summary_icon(RemoteSummaryIcon::Updatable),
- tr!("One update pending" | "{n} updates pending" % entry.summary.number_of_updates),
- )
+ row = row.with_child(render_status_icon(StatusIcon::Updatable));
+ row = row.with_child(format!("{}", entry.summary.number_of_updates));
+ tr!("One update pending" | "{n} updates pending" % entry.summary.number_of_updates)
} else {
- (
- render_remote_summary_icon(RemoteSummaryIcon::UpToDate),
- tr!("Up-to-date"),
- )
+ row = row.with_child(render_status_icon(StatusIcon::Ok));
+ tr!("Up-to-date")
};
- Row::new()
- .class(css::AlignItems::Baseline)
- .gap(2)
- .with_child(icon)
- .with_child(text)
+ Tooltip::new(row).tip(tooltip).into()
+}
+
+fn render_repo_status_column(tree_entry: &UpdateTreeEntry, expanded: bool) -> Html {
+ match tree_entry {
+ UpdateTreeEntry::Root => {
+ html!()
+ }
+ UpdateTreeEntry::Remote(remote_info) => {
+ if !expanded {
+ render_repo_status(remote_info.repo_status)
+ } else {
+ html!()
+ }
+ }
+ UpdateTreeEntry::Node(node_info) => {
+ render_repo_status(node_info.summary.repository_status).into()
+ }
+ }
+}
+
+fn render_repo_status(status: ProductRepositoryStatus) -> Html {
+ let mut row = Row::new().class(css::AlignItems::Baseline).gap(2);
+
+ let tooltip = match status {
+ ProductRepositoryStatus::Ok => {
+ row = row.with_child(render_status_icon(StatusIcon::Ok));
+ tr!("Production-ready enterprise repository enabled")
+ }
+ ProductRepositoryStatus::NoProductRepository => {
+ row = row.with_child(render_status_icon(StatusIcon::Error));
+ tr!("No product repository configured")
+ }
+ ProductRepositoryStatus::MissingSubscriptionForEnterprise => {
+ row = row.with_child(render_status_icon(StatusIcon::Warning));
+ tr!("Enterprise repository configured, but missing subscription")
+ }
+ ProductRepositoryStatus::NonProductionReady => {
+ row = row.with_child(render_status_icon(StatusIcon::Ok));
+ row = row.with_child(render_status_icon(StatusIcon::Warning));
+ tr!("Non-production-ready repositories enabled")
+ }
+ ProductRepositoryStatus::Error => {
+ row = row.with_child(render_status_icon(StatusIcon::Error));
+ tr!("Error")
+ }
+ };
+
+ Tooltip::new(row).tip(tooltip).into()
}
fn render_version_column(tree_entry: &UpdateTreeEntry, expanded: bool) -> Html {
@@ -580,19 +628,21 @@ fn get_product_version(node_entry: &NodeEntry) -> Option<String> {
.map(|p| p.version.to_string())
}
-enum RemoteSummaryIcon {
- UpToDate,
+enum StatusIcon {
+ Ok,
Updatable,
Error,
+ Warning,
Unknown,
}
-fn render_remote_summary_icon(icon: RemoteSummaryIcon) -> Fa {
+fn render_status_icon(icon: StatusIcon) -> Fa {
let (icon_class, icon_scheme) = match icon {
- RemoteSummaryIcon::UpToDate => ("check", FontColor::Success),
- RemoteSummaryIcon::Error => ("times-circle", FontColor::Error),
- RemoteSummaryIcon::Updatable => ("refresh", FontColor::Primary),
- RemoteSummaryIcon::Unknown => ("question-circle-o", FontColor::Primary),
+ StatusIcon::Ok => ("check", FontColor::Success),
+ StatusIcon::Error => ("times-circle", FontColor::Error),
+ StatusIcon::Warning => ("exclamation-triangle", FontColor::Warning),
+ StatusIcon::Updatable => ("refresh", FontColor::Primary),
+ StatusIcon::Unknown => ("question-circle-o", FontColor::Primary),
};
Fa::new(icon_class).class(icon_scheme)
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
next prev parent reply other threads:[~2025-11-27 10:45 UTC|newest]
Thread overview: 24+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-11-27 10:44 [pdm-devel] [PATCH datacenter-manager/proxmox{, -yew-comp} 00/18] remote update view: include product version and repository status Lukas Wagner
2025-11-27 10:44 ` [pdm-devel] [PATCH proxmox 1/4] add bindings for /nodes/{node}/apt/versions Lukas Wagner
2025-11-27 20:46 ` [pdm-devel] applied: " Thomas Lamprecht
2025-11-27 10:44 ` [pdm-devel] [PATCH proxmox 2/4] add bindings for /nodes/{node}/apt/repositories Lukas Wagner
2025-11-27 20:46 ` [pdm-devel] applied: " Thomas Lamprecht
2025-11-27 10:44 ` [pdm-devel] [PATCH proxmox 3/4] make refresh Lukas Wagner
2025-11-27 10:44 ` [pdm-devel] [PATCH proxmox 4/4] proxmox-apt-api-types: make APTStandardRepository compatible with PVE's serialization of the type Lukas Wagner
2025-11-27 20:46 ` [pdm-devel] applied: " Thomas Lamprecht
2025-11-27 10:44 ` [pdm-devel] [PATCH proxmox-yew-comp 1/1] apt repositories: add 'status_only' property Lukas Wagner
2025-11-27 21:26 ` [pdm-devel] applied: " Thomas Lamprecht
2025-11-27 10:44 ` [pdm-devel] [PATCH datacenter-manager 01/13] pdm-api-types: reuse APTUpdateInfo from proxmox_apt_api_types Lukas Wagner
2025-11-27 10:44 ` [pdm-devel] [PATCH datacenter-manager 02/13] pbs-client: add bindings for /nodes/localhost/apt/versions Lukas Wagner
2025-11-27 10:44 ` [pdm-devel] [PATCH datacenter-manager 03/13] pbs-client: add bindings for /nodes/localhost/apt/repositories Lukas Wagner
2025-11-27 10:44 ` [pdm-devel] [PATCH datacenter-manager 04/13] remote-updates: include version information in node update summary Lukas Wagner
2025-11-27 10:44 ` [pdm-devel] [PATCH datacenter-manager 05/13] remote updates: include repository status in node summary Lukas Wagner
2025-11-27 10:44 ` [pdm-devel] [PATCH datacenter-manager 06/13] api: pve/pbs: add passthrough endpoint for APT repo configuration Lukas Wagner
2025-11-27 10:44 ` [pdm-devel] [PATCH datacenter-manager 07/13] ui: remote updates: show table header Lukas Wagner
2025-11-27 10:44 ` [pdm-devel] [PATCH datacenter-manager 08/13] ui: remote updates: show main product version in overview table Lukas Wagner
2025-11-27 10:44 ` Lukas Wagner [this message]
2025-11-27 10:44 ` [pdm-devel] [PATCH datacenter-manager 10/13] ui: remote updates: show repo status details when selecting a node Lukas Wagner
2025-11-27 10:44 ` [pdm-devel] [PATCH datacenter-manager 11/13] ui: remote updates: don't attempt to load current status for unavailable nodes Lukas Wagner
2025-11-27 10:44 ` [pdm-devel] [PATCH datacenter-manager 12/13] ui: remote updates: use 'building-o' icon for PBS nodes Lukas Wagner
2025-11-27 10:44 ` [pdm-devel] [PATCH datacenter-manager 13/13] ui: remote updates: use explicit indices for parameters in tr! macro Lukas Wagner
2025-11-27 10:47 ` [pdm-devel] [PATCH datacenter-manager/proxmox{, -yew-comp} 00/18] remote update view: include product version and repository status Lukas Wagner
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=20251127104447.162951-15-l.wagner@proxmox.com \
--to=l.wagner@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