all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: "Lukas Wagner" <l.wagner@proxmox.com>
To: "Proxmox Datacenter Manager development discussion"
	<pdm-devel@lists.proxmox.com>,
	"Christoph Heiss" <c.heiss@proxmox.com>
Subject: Re: [pdm-devel] [PATCH datacenter-manager v2 12/14] ui: auto-installer: add installations overview panel
Date: Tue, 09 Dec 2025 13:35:07 +0100	[thread overview]
Message-ID: <DETOUJX8CJOD.3AXSZCG6K3DND@proxmox.com> (raw)
In-Reply-To: <20251205112528.373387-13-c.heiss@proxmox.com>

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<AutoInstallerPanel> for VNode {
> +    fn from(value: AutoInstallerPanel) -> Self {
> +        let comp = VComp::new::<LoadableComponentMaster<AutoInstallerPanelComponent>>(
> +            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<Installation>,
> +    columns: Rc<Vec<DataTableHeader<Installation>>>,
> +}
> +
> +impl LoadableComponent for AutoInstallerPanelComponent {
> +    type Properties = AutoInstallerPanel;
> +    type Message = Message;
> +    type ViewState = ViewState;
> +
> +    fn create(ctx: &LoadableComponentContext<Self>) -> 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<Self>,
> +    ) -> Pin<Box<dyn Future<Output = Result<()>>>> {
> +        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<Self>, 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<Self>) -> Option<yew::Html> {
> +        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<Self>) -> 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<Self>,
> +        view_state: &Self::ViewState,
> +    ) -> Option<yew::Html> {
> +        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(|_| "<failed to decode>".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(|_| "<failed to decode>".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<DataTableHeader<Installation>> {
> +    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<std::rc::Rc<proxmox_yew_comp::HttpClientWasm>> {
>      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


  reply	other threads:[~2025-12-09 12:34 UTC|newest]

Thread overview: 41+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-12-05 11:25 [pdm-devel] [PATCH proxmox/datacenter-manager v2 00/14] initial auto-installer integration Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 01/14] api-macro: allow $ in identifier name Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 02/14] network-types: move `Fqdn` type from proxmox-installer-common Christoph Heiss
2025-12-09  9:13   ` Lukas Wagner
2025-12-09 12:26     ` Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 03/14] network-types: implement api type for Fqdn Christoph Heiss
2025-12-09  9:13   ` Lukas Wagner
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 04/14] network-types: add api wrapper type for std::net::IpAddr Christoph Heiss
2025-12-09  9:16   ` Lukas Wagner
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 05/14] installer-types: add common types used by the installer Christoph Heiss
2025-12-09  9:35   ` Lukas Wagner
2025-12-09 12:17     ` Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 06/14] installer-types: add types used by the auto-installer Christoph Heiss
2025-12-09  9:44   ` Lukas Wagner
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 07/14] installer-types: implement api type for all externally-used types Christoph Heiss
2025-12-09  9:52   ` Lukas Wagner
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 08/14] api-types: add api types for auto-installer integration Christoph Heiss
2025-12-09 10:03   ` Lukas Wagner
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 09/14] config: add auto-installer configuration module Christoph Heiss
2025-12-09 10:22   ` Lukas Wagner
2025-12-09 12:10     ` Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 10/14] acl: wire up new /system/auto-installation acl path Christoph Heiss
2025-12-09 10:23   ` Lukas Wagner
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 11/14] server: api: add auto-installer integration module Christoph Heiss
2025-12-09 11:01   ` Lukas Wagner
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 12/14] ui: auto-installer: add installations overview panel Christoph Heiss
2025-12-09 12:35   ` Lukas Wagner [this message]
2025-12-16 13:55     ` Christoph Heiss
2025-12-17  9:09       ` Lukas Wagner
2025-12-17  9:42         ` Thomas Lamprecht
2025-12-17 14:00           ` Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 13/14] ui: auto-installer: add prepared answer configuration panel Christoph Heiss
2025-12-09 13:01   ` Lukas Wagner
2025-12-16 14:28     ` Christoph Heiss
2025-12-16 14:57       ` Lukas Wagner
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 14/14] docs: add documentation for auto-installer integration Christoph Heiss
2025-12-09 13:12   ` Lukas Wagner
2025-12-05 11:53 ` [pdm-devel] [PATCH proxmox/datacenter-manager v2 00/14] initial " Thomas Lamprecht
2025-12-05 15:50   ` Christoph Heiss
2025-12-05 15:57     ` Thomas Lamprecht
2025-12-09 13:38 ` Lukas Wagner

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=DETOUJX8CJOD.3AXSZCG6K3DND@proxmox.com \
    --to=l.wagner@proxmox.com \
    --cc=c.heiss@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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal