public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Dominik Csapak <d.csapak@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [RFC PATCH datacenter-manager 3/3] ui: pve tree: add bulk start action
Date: Wed, 29 Jan 2025 11:51:42 +0100	[thread overview]
Message-ID: <20250129105142.1291843-4-d.csapak@proxmox.com> (raw)
In-Reply-To: <20250129105142.1291843-1-d.csapak@proxmox.com>

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@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



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


  parent reply	other threads:[~2025-01-29 10:52 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-01-29 10:51 [pdm-devel] [RFC PATCH datacenter-manager 0/3] implement bulk start Dominik Csapak
2025-01-29 10:51 ` [pdm-devel] [RFC PATCH datacenter-manager 1/3] server: pve api: add new bulkstart api call Dominik Csapak
2025-01-29 10:51 ` [pdm-devel] [RFC PATCH datacenter-manager 2/3] pdm-client: add bulk_start method Dominik Csapak
2025-01-29 10:51 ` Dominik Csapak [this message]
2025-01-29 18:48 ` [pdm-devel] [RFC PATCH datacenter-manager 0/3] implement bulk start Thomas Lamprecht
2025-01-30  8:14   ` Dominik Csapak
2025-01-30 16:15     ` Thomas Lamprecht

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=20250129105142.1291843-4-d.csapak@proxmox.com \
    --to=d.csapak@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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal