From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id B2FA91FF165 for ; 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 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 List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Datacenter Manager development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" 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 --- 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), RouteChanged(String), + BulkSelected(Key), + BulkStart, } pub struct PveTreeComp { @@ -135,6 +144,7 @@ pub struct PveTreeComp { filter: String, _nav_handle: ContextHandle, 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, remote: String, loading: bool, + selection: Selection, ) -> Rc>> { 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| { + 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