[pdm-devel] [RFC PATCH datacenter-manager 3/3] ui: pve tree: add bulk start action

Dominik Csapak d.csapak at proxmox.com
Wed Jan 29 11:51:42 CET 2025


This adds a new checkbox column that is independent from the usual
selection. If one or more elements are selected, the 'Bulk Action' gets
enabled, and one can select the 'Start' action, which will start a Task
to start the selected guests.

Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
---
 ui/src/pve/tree.rs | 133 +++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 129 insertions(+), 4 deletions(-)

diff --git a/ui/src/pve/tree.rs b/ui/src/pve/tree.rs
index 95fb0ec..5184bdc 100644
--- a/ui/src/pve/tree.rs
+++ b/ui/src/pve/tree.rs
@@ -9,20 +9,26 @@ use yew::{
 
 use proxmox_yew_comp::{
     LoadableComponent, LoadableComponentContext, LoadableComponentLink, LoadableComponentMaster,
+    TaskViewer,
 };
 use pwt::css::{AlignItems, ColorScheme, FlexFit, JustifyContent};
+use pwt::prelude::*;
 use pwt::props::{ContainerBuilder, CssBorderBuilder, ExtractPrimaryKey, WidgetBuilder};
 use pwt::state::{KeyedSlabTree, NavigationContext, NavigationContextExt, Selection, TreeStore};
 use pwt::widget::{
-    data_table::{DataTable, DataTableColumn, DataTableHeader},
+    data_table::{
+        DataTable, DataTableCellRenderArgs, DataTableColumn, DataTableHeader,
+        DataTableKeyboardEvent, DataTableMouseEvent,
+    },
     form::Field,
-    ActionIcon, Column, Container, Fa, MessageBox, MessageBoxButtons, Row, Toolbar, Trigger,
+    menu::{Menu, MenuButton, MenuItem},
+    ActionIcon, Button, Column, Container, Fa, MessageBox, MessageBoxButtons, Row, Toolbar,
+    Trigger,
 };
-use pwt::{prelude::*, widget::Button};
 
 use pdm_api_types::{
     resource::{PveLxcResource, PveNodeResource, PveQemuResource, PveResource},
-    RemoteUpid,
+    RemoteUpid, UPID,
 };
 
 use crate::{get_deep_url, widget::MigrateWindow};
@@ -119,6 +125,7 @@ impl std::fmt::Display for Action {
 pub enum ViewState {
     Confirm(Action, String),  // ID
     MigrateWindow(GuestInfo), // ID
+    ShowPdmTask(UPID),
 }
 
 pub enum Msg {
@@ -126,6 +133,8 @@ pub enum Msg {
     GuestAction(Action, String), //ID
     KeySelected(Option<Key>),
     RouteChanged(String),
+    BulkSelected(Key),
+    BulkStart,
 }
 
 pub struct PveTreeComp {
@@ -135,6 +144,7 @@ pub struct PveTreeComp {
     filter: String,
     _nav_handle: ContextHandle<NavigationContext>,
     view_selection: Selection,
+    selection: Selection,
 }
 
 impl PveTreeComp {
@@ -269,6 +279,7 @@ impl LoadableComponent for PveTreeComp {
 
         let path = _nav_ctx.path();
         ctx.link().send_message(Msg::RouteChanged(path));
+        let selection = Selection::new().multiselect(true);
 
         Self {
             columns: columns(
@@ -276,12 +287,14 @@ impl LoadableComponent for PveTreeComp {
                 store.clone(),
                 ctx.props().remote.clone(),
                 ctx.props().loading,
+                selection.clone(),
             ),
             loaded: false,
             store,
             filter: String::new(),
             _nav_handle,
             view_selection,
+            selection,
         }
     }
 
@@ -390,6 +403,59 @@ impl LoadableComponent for PveTreeComp {
                     });
                 }
             }
+            Msg::BulkSelected(key) => {
+                let props = ctx.props();
+                self.selection.toggle(key.clone());
+                let selected = self.selection.contains(&key);
+
+                let store = self.store.read();
+                let item = store.lookup_node(&key);
+                if item.is_none() {
+                    return false;
+                }
+
+                let item = item.unwrap();
+
+                if let PveTreeNode::Node(_) = item.record() {
+                    for child in item.children() {
+                        let key = child.key();
+                        if selected != self.selection.contains(&key) {
+                            self.selection.toggle(key);
+                        }
+                    }
+                }
+
+                self.columns = columns(
+                    ctx.link(),
+                    self.store.clone(),
+                    props.remote.clone(),
+                    props.loading,
+                    self.selection.clone(),
+                );
+                return true;
+            }
+            Msg::BulkStart => {
+                let mut vmids = Vec::new();
+                for (_, item) in self.store.filtered_data() {
+                    let key = item.key();
+                    if self.selection.contains(&key) {
+                        match *item.record() {
+                            PveTreeNode::Lxc(PveLxcResource { vmid, .. })
+                            | PveTreeNode::Qemu(PveQemuResource { vmid, .. }) => vmids.push(vmid),
+                            _ => {}
+                        }
+                    }
+                }
+
+                let link = ctx.link().clone();
+                let remote = ctx.props().remote.clone();
+                ctx.link().spawn(async move {
+                    match crate::pdm_client().pve_bulk_start(&remote, vmids).await {
+                        Ok(upid) => link.change_view(Some(ViewState::ShowPdmTask(upid))),
+                        Err(err) => link.show_error(tr!("Start failed"), err.to_string(), true),
+                    }
+                });
+            }
         }
         true
     }
@@ -409,6 +475,7 @@ impl LoadableComponent for PveTreeComp {
             self.store.clone(),
             props.remote.clone(),
             props.loading,
+            self.selection.clone(),
         );
 
         true
@@ -447,6 +514,19 @@ impl LoadableComponent for PveTreeComp {
                             .on_input(link.callback(Msg::Filter)),
                     )
                     .with_flex_spacer()
+                    .with_child(
+                        MenuButton::new(tr!("Bulk Actions"))
+                            .disabled(self.selection.is_empty())
+                            .icon_class("fa fa-list")
+                            .show_arrow(true)
+                            .menu(
+                                Menu::new().with_item(
+                                    MenuItem::new(tr!("Start"))
+                                        .icon_class("fa fa-play")
+                                        .on_select(ctx.link().callback(|_| Msg::BulkStart)),
+                                ),
+                            ),
+                    )
                     .with_child(Button::refresh(ctx.props().loading).onclick({
                         let on_reload_click = ctx.props().on_reload_click.clone();
                         move |_| {
@@ -495,6 +575,11 @@ impl LoadableComponent for PveTreeComp {
                     })
                     .into(),
             ),
+            ViewState::ShowPdmTask(upid) => Some(
+                TaskViewer::new(upid.to_string())
+                    .on_close(ctx.link().change_view_callback(|_| None))
+                    .into(),
+            ),
         }
     }
 
@@ -526,8 +611,48 @@ fn columns(
     store: TreeStore<PveTreeNode>,
     remote: String,
     loading: bool,
+    selection: Selection,
 ) -> Rc<Vec<DataTableHeader<PveTreeNode>>> {
     Rc::new(vec![
+        DataTableColumn::new("selection indicator")
+            .width("max-content")
+            //   .width("2.5em")
+            .resizable(false)
+            .show_menu(false)
+            .render_header(|_args: &mut _| Fa::new("check").into())
+            .render_cell({
+                move |args: &mut DataTableCellRenderArgs<PveTreeNode>| {
+                    let selected = selection.contains(args.key());
+                    Fa::new(if selected {
+                        "check-square-o"
+                    } else {
+                        "square-o"
+                    })
+                    .class("pwt-pointer")
+                    .into()
+                }
+            })
+            .on_cell_click({
+                let link = link.clone();
+                move |event: &mut DataTableMouseEvent| {
+                    let record_key = event.record_key.clone();
+                    //selection.toggle(record_key.clone());
+                    event.prevent_default();
+                    event.stop_propagation();
+                    link.send_message(Msg::BulkSelected(record_key));
+                }
+            })
+            .on_cell_keydown({
+                let link = link.clone();
+                move |event: &mut DataTableKeyboardEvent| {
+                    if event.key() == " " {
+                        event.stop_propagation();
+                        event.prevent_default();
+                        link.send_message(Msg::BulkSelected(event.record_key.clone()));
+                    }
+                }
+            })
+            .into(),
         DataTableColumn::new("Type/ID")
             .flex(1)
             .tree_column(store)
-- 
2.39.5





More information about the pdm-devel mailing list