From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id C8F591FF0E1 for ; Thu, 27 Aug 2026 14:40:42 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 8399D21408; Thu, 27 Aug 2026 14:40:42 +0200 (CEST) From: Christoph Heiss To: pdm-devel@lists.proxmox.com Subject: [PATCH datacenter-manager] ui: auto-installer: allow multi-select of installation entries Date: Thu, 27 Aug 2026 14:40:27 +0200 Message-ID: <20260827124033.871256-1-c.heiss@proxmox.com> X-Mailer: git-send-email 2.55.0 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1787834430614 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.453 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 725LLEFQJQBLG3BNAM3RZKO3E2EKBYKZ X-Message-ID-Hash: 725LLEFQJQBLG3BNAM3RZKO3E2EKBYKZ X-MailFrom: c.heiss@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Depending on the environment/usage, a lot of entries of past installations can accumulate here over time. Having the possibility for bulk deletion makes it easier to deal with this. Signed-off-by: Christoph Heiss --- .../auto_installer/installations_panel.rs | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/ui/src/remotes/auto_installer/installations_panel.rs b/ui/src/remotes/auto_installer/installations_panel.rs index 2bb52278..c40628e8 100644 --- a/ui/src/remotes/auto_installer/installations_panel.rs +++ b/ui/src/remotes/auto_installer/installations_panel.rs @@ -22,7 +22,7 @@ use pwt::{ tr, widget::{ Button, Toolbar, - data_table::{DataTable, DataTableColumn, DataTableHeader}, + data_table::{DataTable, DataTableColumn, DataTableHeader, MultiSelectMode}, form::TextArea, }, }; @@ -77,8 +77,9 @@ impl LoadableComponent for InstallationsPanelComponent { type ViewState = ViewState; fn create(ctx: &LoadableComponentContext) -> Self { - let selection = - Selection::new().on_select(ctx.link().callback(|_| Message::SelectionChange)); + let selection = Selection::new() + .multiselect(true) + .on_select(ctx.link().callback(|_| Message::SelectionChange)); let store = Store::with_extract_key(|record: &Installation| Key::from(record.uuid.to_string())); @@ -112,15 +113,18 @@ impl LoadableComponent for InstallationsPanelComponent { } Self::Message::SelectionChange => true, Self::Message::RemoveEntry => { - if let Some(key) = self.selection.selected_key() { + self.spawn({ let link = ctx.link().clone(); - self.spawn(async move { - if let Err(err) = delete_entry(key).await { - link.show_error(tr!("Unable to delete entry"), err, true); + let selection = self.selection.clone(); + async move { + for key in selection.selected_keys() { + if let Err(err) = delete_entry(key).await { + link.show_error(tr!("Unable to delete entry {key}"), err, true); + } } link.send_reload(); - }) - } + } + }); false } } @@ -131,11 +135,12 @@ impl LoadableComponent for InstallationsPanelComponent { let selection_has_post_hook_data = self .selection - .selected_key() + .selected_keys() + .first() .and_then(|key| { self.store .read() - .lookup_record(&key) + .lookup_record(key) .map(|data| data.post_hook_data.is_some()) }) .unwrap_or(false); @@ -146,18 +151,19 @@ impl LoadableComponent for InstallationsPanelComponent { .class("pwt-border-bottom") .with_child( Button::new(tr!("System Information")) - .disabled(self.selection.is_empty()) + .disabled(self.selection.len() != 1) .onclick(link.change_view_callback(|_| Some(ViewState::ShowRawSystemInfo))), ) .with_child( Button::new(tr!("Post-Installation Webhook Data")) - .disabled(self.selection.is_empty() || !selection_has_post_hook_data) + .disabled(self.selection.len() != 1 || !selection_has_post_hook_data) .onclick(link.change_view_callback(|_| Some(ViewState::ShowRawPostHookData))), ) .with_spacer() .with_child( ConfirmButton::new(tr!("Remove")) - .confirm_message(tr!("Are you sure you want to remove this entry?")) + .confirm_message(tr!("Are you sure you want to remove this entry?" + | "Are you sure you want to remove {n} entries?" % self.selection.len())) .disabled(self.selection.is_empty()) .on_activate(link.callback(|_| Message::RemoveEntry)), ) @@ -175,6 +181,7 @@ impl LoadableComponent for InstallationsPanelComponent { DataTable::new(self.columns.clone(), self.store.clone()) .class(FlexFit) .selection(self.selection.clone()) + .multiselect_mode(MultiSelectMode::Simple) .on_row_dblclick({ move |_: &mut _| { link.change_view(Some(Self::ViewState::ShowRawSystemInfo)); @@ -193,7 +200,7 @@ impl LoadableComponent for InstallationsPanelComponent { let record = self .store .read() - .lookup_record(&self.selection.selected_key()?)? + .lookup_record(self.selection.selected_keys().first()?)? .clone(); Some(match view_state { @@ -257,6 +264,7 @@ fn render_raw_info_container(value: String) -> yew::Html { fn columns() -> Vec> { vec![ + DataTableColumn::selection_indicator().into(), DataTableColumn::new(tr!("Received")) .width("170px") .render(|item: &Installation| { -- 2.55.0