From mboxrd@z Thu Jan  1 00:00:00 1970
Return-Path: <pdm-devel-bounces@lists.proxmox.com>
Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68])
	by lore.proxmox.com (Postfix) with ESMTPS id B2FA91FF165
	for <inbox@lore.proxmox.com>; Wed, 29 Jan 2025 11:52:17 +0100 (CET)
Received: from firstgate.proxmox.com (localhost [127.0.0.1])
	by firstgate.proxmox.com (Proxmox) with ESMTP id 53E251FCF9;
	Wed, 29 Jan 2025 11:52:16 +0100 (CET)
From: Dominik Csapak <d.csapak@proxmox.com>
To: pdm-devel@lists.proxmox.com
Date: Wed, 29 Jan 2025 11:51:42 +0100
Message-Id: <20250129105142.1291843-4-d.csapak@proxmox.com>
X-Mailer: git-send-email 2.39.5
In-Reply-To: <20250129105142.1291843-1-d.csapak@proxmox.com>
References: <20250129105142.1291843-1-d.csapak@proxmox.com>
MIME-Version: 1.0
X-SPAM-LEVEL: Spam detection results:  0
 AWL -0.129 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DMARC_MISSING             0.1 Missing DMARC policy
 KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment
 POISEN_SPAM_PILL          0.1 Meta: its spam
 POISEN_SPAM_PILL_2        0.1 random spam to be learned in bayes
 POISEN_SPAM_PILL_4        0.1 random spam to be learned in bayes
 SPF_HELO_NONE           0.001 SPF: HELO does not publish an SPF Record
 SPF_PASS               -0.001 SPF: sender matches SPF record
Subject: [pdm-devel] [RFC PATCH datacenter-manager 3/3] ui: pve tree: add
 bulk start action
X-BeenThere: pdm-devel@lists.proxmox.com
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: Proxmox Datacenter Manager development discussion
 <pdm-devel.lists.proxmox.com>
List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pdm-devel>, 
 <mailto:pdm-devel-request@lists.proxmox.com?subject=unsubscribe>
List-Archive: <http://lists.proxmox.com/pipermail/pdm-devel/>
List-Post: <mailto:pdm-devel@lists.proxmox.com>
List-Help: <mailto:pdm-devel-request@lists.proxmox.com?subject=help>
List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel>, 
 <mailto:pdm-devel-request@lists.proxmox.com?subject=subscribe>
Reply-To: Proxmox Datacenter Manager development discussion
 <pdm-devel@lists.proxmox.com>
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit
Errors-To: pdm-devel-bounces@lists.proxmox.com
Sender: "pdm-devel" <pdm-devel-bounces@lists.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