* [PATCH datacenter-manager] ui: auto-installer: allow multi-select of installation entries
@ 2026-08-27 12:40 Christoph Heiss
2026-08-28 10:57 ` Lukas Wagner
0 siblings, 1 reply; 3+ messages in thread
From: Christoph Heiss @ 2026-08-27 12:40 UTC (permalink / raw)
To: pdm-devel
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 <c.heiss@proxmox.com>
---
.../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>) -> 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<DataTableHeader<Installation>> {
vec![
+ DataTableColumn::selection_indicator().into(),
DataTableColumn::new(tr!("Received"))
.width("170px")
.render(|item: &Installation| {
--
2.55.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [PATCH datacenter-manager] ui: auto-installer: allow multi-select of installation entries
2026-08-27 12:40 [PATCH datacenter-manager] ui: auto-installer: allow multi-select of installation entries Christoph Heiss
@ 2026-08-28 10:57 ` Lukas Wagner
2026-09-08 10:56 ` Christoph Heiss
0 siblings, 1 reply; 3+ messages in thread
From: Lukas Wagner @ 2026-08-28 10:57 UTC (permalink / raw)
To: Christoph Heiss, pdm-devel
On Thu Aug 27, 2026 at 2:40 PM CEST, Christoph Heiss wrote:
> 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 <c.heiss@proxmox.com>
Hi, thanks for the patch!
I think the UX could be improved a bit. Right now, it's a bit weird that
*any* click on the row automatically selects it. For instance, when
going through entries one by one to show the "System Information", this
is a bit annoying, since one cannot press the 'System Information'
button if multiple entries are selected.
I think this could be solved in two ways:
- Require the user to explicitly enable 'bulk-select', e.g. by
pressing a button or ticking a check-box in the header. Before doing
so, the checkbox column could be hidden
- Patch 'DataTable' so that only a click on the checkbox selects the
entry, but not when clicking anywhere else. This probably needs to
be configurable, not sure if we don't break other UIs if we do that.
I think the first option could be the preferable one.
> ---
> .../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>) -> 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);
> + }
> }
I guess in most cases a 'bulk-remove' API endpoint is preferable, mostly
to at least *enable* atomicity at the API level. But since removing
installations ends up removing individual files any way, which is
inherently not atomic, I guess the current approach is fine as well.
If we ever end up storing installations in something that supports
atomic operations (say, a sqlite DB or a single JSON file), a
bulk-delete endpoint can be added as well.
> link.send_reload();
> - })
> - }
> + }
> + });
> false
> }
> }
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH datacenter-manager] ui: auto-installer: allow multi-select of installation entries
2026-08-28 10:57 ` Lukas Wagner
@ 2026-09-08 10:56 ` Christoph Heiss
0 siblings, 0 replies; 3+ messages in thread
From: Christoph Heiss @ 2026-09-08 10:56 UTC (permalink / raw)
To: Lukas Wagner; +Cc: pdm-devel
Thanks for the review!
On Fri Aug 28, 2026 at 12:57 PM CEST, Lukas Wagner wrote:
[..]
> I think the UX could be improved a bit. Right now, it's a bit weird that
> *any* click on the row automatically selects it. For instance, when
> going through entries one by one to show the "System Information", this
> is a bit annoying, since one cannot press the 'System Information'
> button if multiple entries are selected.
>
> I think this could be solved in two ways:
>
> - Require the user to explicitly enable 'bulk-select', e.g. by
> pressing a button or ticking a check-box in the header. Before doing
> so, the checkbox column could be hidden
>
> - Patch 'DataTable' so that only a click on the checkbox selects the
> entry, but not when clicking anywhere else. This probably needs to
> be configurable, not sure if we don't break other UIs if we do that.
>
>
> I think the first option could be the preferable one.
Makes sense. I'll see and will try out what works and fits better.
I also guess we don't have that pattern of UI somewhere else yet, which
could be taken as reference?
[..]
>> 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);
>> + }
>> }
>
> I guess in most cases a 'bulk-remove' API endpoint is preferable, mostly
> to at least *enable* atomicity at the API level. But since removing
> installations ends up removing individual files any way, which is
> inherently not atomic, I guess the current approach is fine as well.
Yeah, thought about that too - but kept it simple here on purpose (as
you described) for now.
>
> If we ever end up storing installations in something that supports
> atomic operations (say, a sqlite DB or a single JSON file), a
> bulk-delete endpoint can be added as well.
>
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-09-08 10:56 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-27 12:40 [PATCH datacenter-manager] ui: auto-installer: allow multi-select of installation entries Christoph Heiss
2026-08-28 10:57 ` Lukas Wagner
2026-09-08 10:56 ` Christoph Heiss
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.