all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: "Shannon Sterz" <s.sterz@proxmox.com>
To: "Lukas Wagner" <l.wagner@proxmox.com>
Cc: Proxmox Datacenter Manager development discussion
	<pdm-devel@lists.proxmox.com>
Subject: Re: [pdm-devel] [PATCH proxmox-datacenter-manager 11/12] ui: add remote update view
Date: Fri, 17 Oct 2025 12:15:35 +0200	[thread overview]
Message-ID: <DDKIOUIS30IF.G0T72O8I7AKV@proxmox.com> (raw)
In-Reply-To: <20251015124711.312943-12-l.wagner@proxmox.com>

On Wed Oct 15, 2025 at 2:47 PM CEST, Lukas Wagner wrote:
> This commit adds a new view for showing a global overview about
> available updates on managed remotes. Thew view is split in the middle.

nit: typo here, should be "The" not "Thew"

> On the left side, we display a tree view showing all remotes and nodes,
> on the right side we show a list of available updates for any node
> selected in the tree.
>
> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
> ---
>  ui/src/remotes/mod.rs     |   3 +
>  ui/src/remotes/updates.rs | 531 ++++++++++++++++++++++++++++++++++++++
>  2 files changed, 534 insertions(+)
>  create mode 100644 ui/src/remotes/updates.rs
>
> diff --git a/ui/src/remotes/mod.rs b/ui/src/remotes/mod.rs
> index 83b3331b..cce21563 100644
> --- a/ui/src/remotes/mod.rs
> +++ b/ui/src/remotes/mod.rs
> @@ -24,6 +24,9 @@ pub use config::{create_remote, RemoteConfigPanel};
>  mod tasks;
>  pub use tasks::RemoteTaskList;
>
> +mod updates;
> +pub use updates::UpdateTree;
> +
>  use yew::{function_component, Html};
>
>  use pwt::prelude::*;
> diff --git a/ui/src/remotes/updates.rs b/ui/src/remotes/updates.rs
> new file mode 100644
> index 00000000..1a2e9e25
> --- /dev/null
> +++ b/ui/src/remotes/updates.rs
> @@ -0,0 +1,531 @@
> +use std::cmp::Ordering;
> +use std::ops::Deref;
> +use std::pin::Pin;
> +use std::rc::Rc;
> +
> +use futures::Future;
> +use yew::virtual_dom::{Key, VComp, VNode};
> +use yew::{html, Html, Properties};
> +
> +use pdm_api_types::remote_updates::{
> +    NodeUpdateStatus, NodeUpdateSummary, RemoteUpdateStatus, UpdateSummary,
> +};
> +use pdm_api_types::remotes::RemoteType;
> +use pwt::css::{AlignItems, FlexFit, TextAlign};
> +use pwt::widget::data_table::{DataTableCellRenderArgs, DataTableCellRenderer};
> +
> +use proxmox_yew_comp::{
> +    AptPackageManager, LoadableComponent, LoadableComponentContext, LoadableComponentMaster,
> +};
> +use pwt::props::{CssBorderBuilder, CssMarginBuilder, CssPaddingBuilder, WidgetStyleBuilder};
> +use pwt::widget::{Button, Container, Panel, Tooltip};
> +use pwt::{
> +    css,
> +    css::FontColor,
> +    props::{ContainerBuilder, ExtractPrimaryKey, WidgetBuilder},
> +    state::{Selection, SlabTree, TreeStore},
> +    tr,
> +    widget::{
> +        data_table::{DataTable, DataTableColumn, DataTableHeader},
> +        Column, Fa, Row,
> +    },
> +};
> +
> +use crate::{get_deep_url, get_deep_url_low_level, pdm_client};
> +
> +#[derive(PartialEq, Properties)]
> +pub struct UpdateTree {}
> +
> +impl UpdateTree {
> +    pub fn new() -> Self {
> +        yew::props!(Self {})
> +    }
> +}
> +
> +impl From<UpdateTree> for VNode {
> +    fn from(value: UpdateTree) -> Self {
> +        let comp = VComp::new::<LoadableComponentMaster<UpdateTreeComponent>>(Rc::new(value), None);
> +        VNode::from(comp)
> +    }
> +}
> +
> +#[derive(Clone, PartialEq, Debug)]
> +struct RemoteEntry {
> +    remote: String,
> +    ty: RemoteType,
> +    number_of_failed_nodes: u32,
> +    number_of_nodes: u32,
> +    number_of_updatable_nodes: u32,
> +    poll_status: RemoteUpdateStatus,
> +}
> +
> +#[derive(Clone, PartialEq, Debug)]
> +struct NodeEntry {
> +    remote: String,
> +    node: String,
> +    ty: RemoteType,
> +    summary: NodeUpdateSummary,
> +}
> +
> +#[derive(Clone, PartialEq, Debug)]
> +enum UpdateTreeEntry {
> +    Root,
> +    Remote(RemoteEntry),
> +    Node(NodeEntry),
> +}
> +
> +impl UpdateTreeEntry {
> +    fn name(&self) -> &str {
> +        match &self {
> +            Self::Root => "",
> +            Self::Remote(data) => &data.remote,
> +            Self::Node(data) => &data.node,
> +        }
> +    }
> +}
> +
> +impl ExtractPrimaryKey for UpdateTreeEntry {
> +    fn extract_key(&self) -> yew::virtual_dom::Key {
> +        Key::from(match self {
> +            UpdateTreeEntry::Root => "/".to_string(),
> +            UpdateTreeEntry::Remote(data) => format!("/{}", data.remote),
> +            UpdateTreeEntry::Node(data) => format!("/{}/{}", data.remote, data.node),
> +        })
> +    }
> +}
> +
> +pub enum RemoteUpdateTreeMsg {
> +    LoadFinished(UpdateSummary),
> +    KeySelected(Option<Key>),
> +    RefreshAll,
> +}

i know this is pre-existing and also a theme throughout the yew stuff,
but i'd call making message enums public an anti-pattern. there isn't
really a point to having them outside of the component where they
matter. imo, the `pub` here should be dropped.

> +
> +pub struct UpdateTreeComponent {
> +    store: TreeStore<UpdateTreeEntry>,
> +    selection: Selection,
> +    selected_entry: Option<UpdateTreeEntry>,
> +}
> +
> +fn default_sorter(a: &UpdateTreeEntry, b: &UpdateTreeEntry) -> Ordering {
> +    a.name().cmp(b.name())
> +}
> +
> +impl UpdateTreeComponent {
> +    fn columns(
> +        _ctx: &LoadableComponentContext<Self>,
> +        store: TreeStore<UpdateTreeEntry>,
> +    ) -> Rc<Vec<DataTableHeader<UpdateTreeEntry>>> {
> +        Rc::new(vec![
> +            DataTableColumn::new(tr!("Name"))
> +                .tree_column(store)
> +                .width("200px")
> +                .render(|entry: &UpdateTreeEntry| {
> +                    let icon = match entry {
> +                        UpdateTreeEntry::Remote(_) => Some("server"),
> +                        UpdateTreeEntry::Node(_) => Some("building"),
> +                        _ => None,
> +                    };
> +
> +                    Row::new()
> +                        .class(css::AlignItems::Baseline)
> +                        .gap(2)
> +                        .with_optional_child(icon.map(|icon| Fa::new(icon)))
> +                        .with_child(entry.name())
> +                        .into()
> +                })
> +                .sorter(default_sorter)
> +                .into(),
> +            DataTableColumn::new(tr!("Status"))
> +                .render_cell(DataTableCellRenderer::new(
> +                    move |args: &mut DataTableCellRenderArgs<UpdateTreeEntry>| {
> +                        let mut row = Row::new().class(css::AlignItems::Baseline).gap(2);
> +
> +                        match args.record() {
> +                            UpdateTreeEntry::Remote(remote_info) => match remote_info.poll_status {
> +                                RemoteUpdateStatus::Unknown => {
> +                                    row = row.with_child(render_remote_status_icon(
> +                                        RemoteUpdateStatus::Unknown,
> +                                    ));
> +                                }
> +                                RemoteUpdateStatus::Success => {
> +                                    if !args.is_expanded() {
> +                                        let up_to_date_nodes = remote_info.number_of_nodes
> +                                            - remote_info.number_of_updatable_nodes
> +                                            - remote_info.number_of_failed_nodes;
> +
> +                                        if up_to_date_nodes > 0 {
> +                                            row = row.with_child(render_remote_summary_counter(
> +                                                up_to_date_nodes,
> +                                                RemoteSummaryIcon::UpToDate,
> +                                            ));
> +                                        }
> +
> +                                        if remote_info.number_of_updatable_nodes > 0 {
> +                                            row = row.with_child(render_remote_summary_counter(
> +                                                remote_info.number_of_updatable_nodes,
> +                                                RemoteSummaryIcon::Updatable,
> +                                            ));
> +                                        }
> +
> +                                        if remote_info.number_of_failed_nodes > 0 {
> +                                            row = row.with_child(render_remote_summary_counter(
> +                                                remote_info.number_of_failed_nodes,
> +                                                RemoteSummaryIcon::Error,
> +                                            ));
> +                                        }
> +                                    }
> +                                }
> +                                RemoteUpdateStatus::Error => {
> +                                    row = row.with_child(render_remote_status_icon(
> +                                        RemoteUpdateStatus::Error,
> +                                    ));
> +                                }
> +                            },
> +                            UpdateTreeEntry::Node(info) => {
> +                                if info.summary.status == NodeUpdateStatus::Error {
> +                                    row = row.with_child(
> +                                        Fa::new("times-circle").class(FontColor::Error),
> +                                    );
> +                                    row = row.with_child(tr!("Could not get update info"));
> +                                } else if info.summary.number_of_updates > 0 {
> +                                    row = row
> +                                        .with_child(Fa::new("refresh").class(FontColor::Primary));
> +                                    row = row.with_child(tr!(
> +                                        "{0} updates are available",
> +                                        info.summary.number_of_updates
> +                                    ));
> +                                } else {
> +                                    row =
> +                                        row.with_child(Fa::new("check").class(FontColor::Success));
> +                                    row = row.with_child(tr!("Up-to-date"));
> +                                }
> +                            }
> +                            _ => {}
> +                        }
> +
> +                        row.into()
> +                    },
> +                ))
> +                .into(),
> +        ])
> +    }
> +}
> +
> +fn build_store_from_response(update_summary: UpdateSummary) -> SlabTree<UpdateTreeEntry> {
> +    let mut tree = SlabTree::new();
> +
> +    let mut root = tree.set_root(UpdateTreeEntry::Root);
> +    root.set_expanded(true);
> +
> +    for (remote_name, remote_summary) in update_summary.remotes.deref() {
> +        let mut remote_entry = root.append(UpdateTreeEntry::Remote(RemoteEntry {
> +            remote: remote_name.clone(),
> +            ty: remote_summary.remote_type,
> +            number_of_nodes: 0,
> +            number_of_updatable_nodes: 0,
> +            number_of_failed_nodes: 0,
> +            poll_status: remote_summary.status.clone(),
> +        }));
> +        remote_entry.set_expanded(false);
> +
> +        let number_of_nodes = remote_summary.nodes.len();
> +        let mut number_of_updatable_nodes = 0;
> +        let mut number_of_failed_nodes = 0;
> +
> +        for (node_name, node_summary) in remote_summary.nodes.deref() {
> +            match node_summary.status {
> +                NodeUpdateStatus::Success => {
> +                    if node_summary.number_of_updates > 0 {
> +                        number_of_updatable_nodes += 1;
> +                    }
> +                }
> +                NodeUpdateStatus::Error => {
> +                    number_of_failed_nodes += 1;
> +                }
> +            }
> +
> +            remote_entry.append(UpdateTreeEntry::Node(NodeEntry {
> +                remote: remote_name.clone(),
> +                node: node_name.clone(),
> +                ty: remote_summary.remote_type,
> +                summary: node_summary.clone(),
> +            }));
> +        }
> +
> +        if let UpdateTreeEntry::Remote(info) = remote_entry.record_mut() {
> +            info.number_of_updatable_nodes = number_of_updatable_nodes;
> +            info.number_of_nodes = number_of_nodes as u32;
> +            info.number_of_failed_nodes = number_of_failed_nodes as u32;
> +        }
> +    }
> +
> +    tree
> +}
> +
> +impl LoadableComponent for UpdateTreeComponent {
> +    type Properties = UpdateTree;
> +    type Message = RemoteUpdateTreeMsg;
> +    type ViewState = ();
> +
> +    fn create(ctx: &LoadableComponentContext<Self>) -> Self {
> +        let link = ctx.link();
> +
> +        let store = TreeStore::new().view_root(false);
> +        store.set_sorter(default_sorter);
> +
> +        link.repeated_load(5000);
> +
> +        let selection = Selection::new().on_select(link.callback(|selection: Selection| {
> +            RemoteUpdateTreeMsg::KeySelected(selection.selected_key())
> +        }));
> +
> +        Self {
> +            store: store.clone(),
> +            selection,
> +            selected_entry: None,
> +        }
> +    }
> +
> +    fn load(
> +        &self,
> +        ctx: &LoadableComponentContext<Self>,
> +    ) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
> +        let link = ctx.link().clone();
> +
> +        Box::pin(async move {
> +            let client = pdm_client();
> +
> +            let updates = client.remote_update_summary().await?;
> +            link.send_message(Self::Message::LoadFinished(updates));
> +
> +            Ok(())
> +        })
> +    }
> +
> +    fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
> +        match msg {
> +            Self::Message::LoadFinished(updates) => {
> +                let data = build_store_from_response(updates);
> +                self.store.write().update_root_tree(data);
> +                self.store.set_sorter(default_sorter);
> +
> +                return true;
> +            }
> +            Self::Message::KeySelected(key) => {
> +                if let Some(key) = key {
> +                    let read_guard = self.store.read();
> +                    let node_ref = read_guard.lookup_node(&key).unwrap();
> +                    let record = node_ref.record();
> +
> +                    self.selected_entry = Some(record.clone());
> +
> +                    return true;
> +                }
> +            }
> +            Self::Message::RefreshAll => {
> +                let link = ctx.link();
> +
> +                link.clone().spawn(async move {
> +                    let client = pdm_client();
> +
> +                    match client.refresh_remote_update_summary().await {
> +                        Ok(upid) => {
> +                            link.show_task_progres(upid.to_string());
> +                        }
> +                        Err(err) => {
> +                            link.show_error(tr!("Could not refresh update status."), err, false);
> +                        }
> +                    }
> +                });
> +            }
> +        }
> +
> +        false
> +    }
> +
> +    fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> yew::Html {
> +        Container::new()
> +            .class("pwt-content-spacer")
> +            .class(FlexFit)
> +            .class("pwt-flex-direction-row")
> +            .with_child(self.render_update_tree_panel(ctx))
> +            .with_child(self.render_update_list_panel(ctx))
> +            .into()

haven't tested this, but why not just use a Row here?

> +    }
> +}
> +
> +impl UpdateTreeComponent {
> +    fn render_update_tree_panel(&self, ctx: &LoadableComponentContext<Self>) -> Panel {
> +        let table = DataTable::new(Self::columns(ctx, self.store.clone()), self.store.clone())
> +            .selection(self.selection.clone())
> +            .striped(false)
> +            .borderless(true)
> +            .show_header(false)
> +            .class(css::FlexFit);
> +
> +        let refresh_all_button = Button::new(tr!("Refresh all")).on_activate({
> +            let link = ctx.link().clone();
> +            move |_| {
> +                link.send_message(RemoteUpdateTreeMsg::RefreshAll);
> +            }
> +        });
> +
> +        let title: Html = Row::new()
> +            .gap(2)
> +            .class(AlignItems::Baseline)
> +            .with_child(Fa::new("refresh"))
> +            .with_child(tr!("Remote System Updates"))
> +            .into();
> +
> +        Panel::new()
> +            .min_width(500)
> +            .title(title)
> +            .with_tool(refresh_all_button)
> +            .style("flex", "1 1 0")
> +            .class(FlexFit)
> +            .border(true)
> +            .with_child(table)
> +    }
> +
> +    fn render_update_list_panel(&self, ctx: &LoadableComponentContext<Self>) -> Panel {
> +        let title: Html = Row::new()
> +            .gap(2)
> +            .class(AlignItems::Baseline)
> +            .with_child(Fa::new("list"))
> +            .with_child(tr!("Update List"))
> +            .into();
> +
> +        match &self.selected_entry {
> +            // (Some(remote), Some(remote_type), Some(node)) => {
> +            Some(UpdateTreeEntry::Node(NodeEntry {
> +                remote, node, ty, ..
> +            })) => {
> +                let base_url = format!("/{ty}/remotes/{remote}/nodes/{node}/apt",);
> +                let task_base_url = format!("/{ty}/remotes/{}/tasks", remote);

nit: remote should be inlined here

> +
> +                let apt = AptPackageManager::new()
> +                    .base_url(base_url)
> +                    .task_base_url(task_base_url)
> +                    .enable_upgrade(true)
> +                    .on_upgrade({
> +                        let remote = remote.clone();
> +                        let link = ctx.link().clone();
> +                        let remote = remote.clone();
> +                        let node = node.clone();
> +                        let ty = *ty;
> +
> +                        move |_| match ty {
> +                            RemoteType::Pve => {
> +                                let id = format!("node/{}::apt", node);

same for node here

> +                                if let Some(url) =
> +                                    get_deep_url(&link.yew_link(), &remote, None, &id)

you don't need references here for the first parameter, `yew_link()
already returns a reference


> +                                {
> +                                    let _ = web_sys::window().unwrap().open_with_url(&url.href());

gloo_utils::window() will safe you an unwrap here :)

> +                                }
> +                            }
> +                            RemoteType::Pbs => {
> +                                let hash = "#pbsServerAdministration:updates";
> +                                if let Some(url) =
> +                                    get_deep_url_low_level(&link.yew_link(), &remote, None, &hash)

see comment about yew_link() above, and you can drop the extra reference
for hash here too

> +                                {
> +                                    let _ = web_sys::window().unwrap().open_with_url(&url.href());

see comment about gloo_utils above

> +                                }
> +                            }
> +                        }
> +                    });
> +
> +                Panel::new()
> +                    .class(FlexFit)
> +                    .title(title)
> +                    .border(true)
> +                    .min_width(500)
> +                    .with_child(apt)
> +                    .style("flex", "1 1 0")
> +            }
> +            _ => {
> +                let header = tr!("No node selected");
> +                let msg = tr!("Select a node to show available updates.");
> +
> +                let select_node_msg = Column::new()
> +                    .class(FlexFit)
> +                    .padding(2)
> +                    .class(AlignItems::Center)
> +                    .class(TextAlign::Center)
> +                    .with_child(html! {<h1 class="pwt-font-headline-medium">{header}</h1>})
> +                    .with_child(Container::new().with_child(msg));
> +
> +                Panel::new()
> +                    .class(FlexFit)
> +                    .title(title)
> +                    .border(true)
> +                    .min_width(500)
> +                    .with_child(select_node_msg)
> +                    .style("flex", "1 1 0")
> +            }
> +        }
> +    }
> +}
> +
> +enum RemoteSummaryIcon {
> +    UpToDate,
> +    Updatable,
> +    Error,
> +}
> +
> +fn render_remote_summary_counter(count: u32, task_class: RemoteSummaryIcon) -> Html {
> +    let (icon_class, icon_scheme, state_text) = match task_class {
> +        RemoteSummaryIcon::UpToDate => (
> +            "check",
> +            FontColor::Success,
> +            tr!("One node is up-to-date." | "{n} nodes are up-to-date." % count),
> +        ),
> +        RemoteSummaryIcon::Error => (
> +            "times-circle",
> +            FontColor::Error,
> +            tr!("Failed to retrieve update info for one node."
> +                | "Failed to retrieve update info for {n} nodes." % count),
> +        ),
> +        RemoteSummaryIcon::Updatable => (
> +            "refresh",
> +            FontColor::Primary,
> +            tr!("One node has updates available." | "{n} nodes have updates available." % count),
> +        ),
> +    };
> +
> +    let icon = Fa::new(icon_class).margin_end(3).class(icon_scheme);
> +
> +    Tooltip::new(
> +        Container::from_tag("span")
> +            .with_child(icon)
> +            .with_child(count)
> +            .margin_end(5),
> +    )
> +    .tip(state_text)
> +    .into()
> +}
> +
> +fn render_remote_status_icon(task_class: RemoteUpdateStatus) -> Html {
> +    let (icon_class, icon_scheme, state_text) = match task_class {
> +        RemoteUpdateStatus::Success => (
> +            "check",
> +            FontColor::Success,
> +            tr!("All nodes of this remote are up-to-date."),
> +        ),
> +        RemoteUpdateStatus::Error => (
> +            "times-circle",
> +            FontColor::Error,
> +            tr!("Could not retrieve update info for remote."),
> +        ),
> +        RemoteUpdateStatus::Unknown => (
> +            "question-circle-o",
> +            FontColor::Primary,
> +            tr!("The update status is not known."),
> +        ),
> +    };
> +
> +    let icon = Fa::new(icon_class).margin_end(3).class(icon_scheme);
> +
> +    Tooltip::new(Container::from_tag("span").with_child(icon).margin_end(5))
> +        .tip(state_text)
> +        .into()
> +}



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


  reply	other threads:[~2025-10-17 10:15 UTC|newest]

Thread overview: 22+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-10-15 12:46 [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global " Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 01/12] metric collection task: tests: add missing parameter for cluster_metric_export Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 02/12] pdm-api-types: add types for remote upgrade summary Lukas Wagner
2025-10-17 10:15   ` Shannon Sterz
2025-10-17 11:12     ` Lukas Wagner
2025-10-17 11:52     ` Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 03/12] remote updates: add cache for remote update availability Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 04/12] api: add API for retrieving/refreshing the remote update summary Lukas Wagner
2025-10-17  7:44   ` Lukas Wagner
2025-10-17 10:15   ` Shannon Sterz
2025-10-17 11:00     ` Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 05/12] unprivileged api daemon: tasks: add remote update refresh task Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 06/12] pdm-client: add API methods for remote update summaries Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 07/12] pbs-client: add bindings for APT-related API calls Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 08/12] task cache: use separate functions for tracking PVE and PBS tasks Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 09/12] remote updates: add support for PBS remotes Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 10/12] api: add APT endpoints " Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 11/12] ui: add remote update view Lukas Wagner
2025-10-17 10:15   ` Shannon Sterz [this message]
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 12/12] ui: show new remote update view in the 'Remotes' section Lukas Wagner
2025-10-17 10:15 ` [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view Shannon Sterz
2025-10-17 12:14 ` [pdm-devel] superseded: " 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=DDKIOUIS30IF.G0T72O8I7AKV@proxmox.com \
    --to=s.sterz@proxmox.com \
    --cc=l.wagner@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