From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 8A8F01FF17A for ; Tue, 9 Dec 2025 13:34:36 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id D147027834; Tue, 9 Dec 2025 13:35:15 +0100 (CET) Mime-Version: 1.0 Date: Tue, 09 Dec 2025 13:35:07 +0100 Message-Id: From: "Lukas Wagner" To: "Proxmox Datacenter Manager development discussion" , "Christoph Heiss" X-Mailer: aerc 0.21.0-0-g5549850facc2-dirty References: <20251205112528.373387-1-c.heiss@proxmox.com> <20251205112528.373387-13-c.heiss@proxmox.com> In-Reply-To: <20251205112528.373387-13-c.heiss@proxmox.com> X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1765283702010 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.033 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 SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: Re: [pdm-devel] [PATCH datacenter-manager v2 12/14] ui: auto-installer: add installations overview panel 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" Some notes inline :) On Fri Dec 5, 2025 at 12:25 PM CET, Christoph Heiss wrote: > + > +#[derive(Default, PartialEq, Properties)] > +pub struct AutoInstallerPanel {} > + > +impl From for VNode { > + fn from(value: AutoInstallerPanel) -> Self { > + let comp = VComp::new::>( > + Rc::new(value), > + None, > + ); > + VNode::from(comp) > + } > +} > + Message, ViewState and AutoInstallerPanelComponent can be private > +pub enum Message { > + Refresh, > + SelectionChange, > + RemoveEntry, > +} > + > +#[derive(PartialEq)] > +pub enum ViewState { > + ShowRawSystemInfo, > + ShowRawPostHookData, > +} > + > +#[derive(PartialEq, Properties)] > +pub struct AutoInstallerPanelComponent { > + selection: Selection, > + store: Store, > + columns: Rc>>, > +} > + > +impl LoadableComponent for AutoInstallerPanelComponent { > + type Properties = AutoInstallerPanel; > + type Message = Message; > + type ViewState = ViewState; > + > + fn create(ctx: &LoadableComponentContext) -> Self { > + let selection = > + Selection::new().on_select(ctx.link().callback(|_| Message::SelectionChange)); > + > + let store = > + Store::with_extract_key(|record: &Installation| Key::from(record.uuid.to_string())); > + store.set_sorter(|a: &Installation, b: &Installation| a.received_at.cmp(&b.received_at)); > + > + Self { > + selection, > + store, > + columns: Rc::new(columns()), > + } > + } > + > + fn load( > + &self, > + _ctx: &LoadableComponentContext, > + ) -> Pin>>> { > + let store = self.store.clone(); > + Box::pin(async move { > + let data = proxmox_yew_comp::http_get("/auto-install/installations", None).await?; > + store.write().set_data(data); > + Ok(()) > + }) > + } > + > + fn update(&mut self, ctx: &LoadableComponentContext, msg: Self::Message) -> bool { > + match msg { > + Self::Message::Refresh => { > + ctx.link().send_reload(); > + false > + } > + Self::Message::SelectionChange => true, > + Self::Message::RemoveEntry => { > + if let Some(key) = self.selection.selected_key() { > + let link = ctx.link(); > + link.clone().spawn(async move { > + if let Err(err) = delete_entry(key).await { > + link.show_error(tr!("Unable to delete entry"), err, true); > + } > + link.send_reload(); > + }) > + } > + false > + } > + } > + } > + > + fn toolbar(&self, ctx: &LoadableComponentContext) -> Option { > + let link = ctx.link(); > + > + let selection_has_post_hook_data = self > + .selection > + .selected_key() > + .and_then(|key| { > + self.store > + .read() > + .lookup_record(&key) > + .map(|data| data.post_hook_data.is_some()) > + }) > + .unwrap_or(false); > + > + let toolbar = Toolbar::new() > + .class("pwt-w-100") > + .class(pwt::css::Overflow::Hidden) > + .class("pwt-border-bottom") > + .with_child( > + Button::new(tr!("Raw system information")) > + .disabled(self.selection.is_empty()) > + .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) > + .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?")) > + .disabled(self.selection.is_empty()) > + .on_activate(link.callback(|_| Message::RemoveEntry)), > + ) > + .with_flex_spacer() > + .with_child( > + Button::refresh(ctx.loading()).onclick(ctx.link().callback(|_| Message::Refresh)), > + ); > + > + Some(toolbar.into()) > + } > + > + fn main_view(&self, ctx: &LoadableComponentContext) -> yew::Html { > + DataTable::new(self.columns.clone(), self.store.clone()) > + .class(pwt::css::FlexFit) > + .selection(self.selection.clone()) > + .on_row_dblclick({ > + let link = ctx.link(); > + move |_: &mut DataTableMouseEvent| { > + link.change_view(Some(Self::ViewState::ShowRawSystemInfo)); > + } > + }) > + .into() > + } > + > + fn dialog_view( > + &self, > + ctx: &LoadableComponentContext, > + view_state: &Self::ViewState, > + ) -> Option { > + let on_done = ctx.link().clone().change_view_callback(|_| None); > + > + let record = self > + .store > + .read() > + .lookup_record(&self.selection.selected_key()?)? > + .clone(); > + > + Some(match view_state { > + Self::ViewState::ShowRawSystemInfo => { > + DataViewWindow::new(tr!("Raw system information")) > + .on_done(on_done) > + .loader({ > + move || { > + let info = record.info.clone(); > + async move { Ok(info) } > + } > + }) > + .renderer(|data: &SystemInfo| -> yew::Html { > + let value = serde_json::to_string_pretty(data) > + .unwrap_or_else(|_| "".to_owned()); > + render_raw_info_container(value) > + }) > + .resizable(true) > + .into() > + } > + Self::ViewState::ShowRawPostHookData => { > + DataViewWindow::new(tr!("Raw post-installation webhook data")) > + .on_done(on_done) > + .loader({ > + move || { > + let data = record.post_hook_data.clone(); > + async move { > + data.ok_or_else(|| anyhow!("no post-installation webhook data")) > + } > + } > + }) > + .renderer(|data: &PostHookInfo| -> yew::Html { > + let value = serde_json::to_string_pretty(data) > + .unwrap_or_else(|_| "".to_owned()); > + render_raw_info_container(value) > + }) > + .resizable(true) > + .into() > + } > + }) > + } > +} > + > +async fn delete_entry(key: Key) -> Result<()> { > + let url = format!( > + "/auto-install/installations/{}", > + percent_encode_component(&key.to_string()) > + ); > + proxmox_yew_comp::http_delete(&url, None).await > +} > + > +fn render_raw_info_container(value: String) -> yew::Html { > + pwt::widget::Container::new() > + .class(Flex::Fill) > + .class(Overflow::Auto) > + .padding(4) > + .with_child( > + TextArea::new() > + .width("800px") > + .read_only(true) > + .attribute("rows", "40") > + .value(value), > + ) > + .into() > +} > + > +fn columns() -> Vec> { > + vec![ > + DataTableColumn::new(tr!("Received")) > + .width("170px") > + .render(|item: &Installation| { > + proxmox_yew_comp::utils::render_epoch(item.received_at).into() > + }) > + .into(), > + DataTableColumn::new(tr!("Product")) > + .width("300px") > + .render(|item: &Installation| { > + format!( > + "{} {}-{}", > + item.info.product.fullname, item.info.iso.release, item.info.iso.isorelease > + ) > + .into() > + }) > + .into(), > + DataTableColumn::new(tr!("Status")) > + .width("200px") > + .render(|item: &Installation| { > + match item.status { > + InstallationStatus::AnswerSent => tr!("Answer sent"), > + InstallationStatus::NoAnswerFound => tr!("No matching answer found"), > + InstallationStatus::InProgress => tr!("In Progress"), > + InstallationStatus::Finished => tr!("Finished"), > + } > + .into() > + }) > + .into(), > + DataTableColumn::new(tr!("Matched answer")) > + .flex(1) > + .render(|item: &Installation| match &item.answer_id { > + Some(s) => s.into(), > + None => "-".into(), > + }) > + .into(), > + ] > +} > diff --git a/ui/src/auto_installer/mod.rs b/ui/src/auto_installer/mod.rs > new file mode 100644 > index 0000000..810eade > --- /dev/null > +++ b/ui/src/auto_installer/mod.rs > @@ -0,0 +1,4 @@ > +//! Implements the UI for the proxmox-auto-installer integration. > + > +mod installations_panel; > +pub use installations_panel::*; > diff --git a/ui/src/lib.rs b/ui/src/lib.rs > index 1aac757..e5a8826 100644 > --- a/ui/src/lib.rs > +++ b/ui/src/lib.rs > @@ -59,6 +59,8 @@ pub use tasks::register_pve_tasks; > mod view_list_context; > pub use view_list_context::ViewListContext; > > +mod auto_installer; > + > pub fn pdm_client() -> pdm_client::PdmClient> { > pdm_client::PdmClient(proxmox_yew_comp::CLIENT.with(|c| std::rc::Rc::clone(&c.borrow()))) > } > diff --git a/ui/src/main_menu.rs b/ui/src/main_menu.rs > index 18988ea..073b84d 100644 > --- a/ui/src/main_menu.rs > +++ b/ui/src/main_menu.rs > @@ -14,6 +14,7 @@ use proxmox_yew_comp::{AclContext, NotesView, XTermJs}; > use pdm_api_types::remotes::RemoteType; > use pdm_api_types::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY}; > > +use crate::auto_installer::AutoInstallerPanel; > use crate::configuration::subscription_panel::SubscriptionPanel; > use crate::configuration::views::ViewGrid; > use crate::dashboard::view::View; > @@ -378,6 +379,18 @@ impl Component for PdmMainMenu { > remote_submenu, > ); > > + let mut autoinstaller_submenu = Menu::new(); > + > + register_submenu( > + &mut menu, > + &mut content, > + tr!("Automated Installations"), > + "auto-installer", > + Some("fa fa-cubes"), > + |_| AutoInstallerPanel::default().into(), > + autoinstaller_submenu, > + ); I'm not sure whether this should be a top-level menu - but then again, I'm not sure where it could go else. Maybe a tab under "Remotes"? Since any node that was automatically would realistically be added as a remote at some later time? > + > let drawer = NavigationDrawer::new(menu) > .aria_label("Datacenter Manager") > .class("pwt-border-end") _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel