public inbox for pdm-devel@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>
Subject: Re: [pdm-devel] [PATCH proxmox-datacenter-manager 4/5] ui: sdn: evpn: add vnet status panel
Date: Tue, 18 Nov 2025 15:12:59 +0100	[thread overview]
Message-ID: <DEBVS1CD371U.22GW6BICA00LN@proxmox.com> (raw)
In-Reply-To: <20251107085934.118815-8-s.hanreich@proxmox.com>

Looks good to me, one small nit inline.

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>

On Fri Nov 7, 2025 at 9:59 AM CET, Stefan Hanreich wrote:
> This panel shows the status of the MAC-VRF of an EVPN vnet. It lists
> the neighbor table of the vnet on a given node, which can be selected
> via the dropdown in the panel. It will be used in the remote tree and
> the vrf tree of the EVPN view to display detailed information about
> the selected EVPN vnet.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
>  ui/src/sdn/evpn/mod.rs         |   3 +
>  ui/src/sdn/evpn/vnet_status.rs | 253 +++++++++++++++++++++++++++++++++
>  2 files changed, 256 insertions(+)
>  create mode 100644 ui/src/sdn/evpn/vnet_status.rs
>
> diff --git a/ui/src/sdn/evpn/mod.rs b/ui/src/sdn/evpn/mod.rs
> index 6c919ba..3320c0f 100644
> --- a/ui/src/sdn/evpn/mod.rs
> +++ b/ui/src/sdn/evpn/mod.rs
> @@ -16,6 +16,9 @@ pub use add_zone::AddZoneWindow;
>  mod zone_status;
>  pub use zone_status::ZoneStatusTable;
>  
> +mod vnet_status;
> +pub use vnet_status::VnetStatusTable;
> +
>  #[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)]
>  pub struct EvpnRouteTarget {
>      asn: u32,
> diff --git a/ui/src/sdn/evpn/vnet_status.rs b/ui/src/sdn/evpn/vnet_status.rs
> new file mode 100644
> index 0000000..6f59890
> --- /dev/null
> +++ b/ui/src/sdn/evpn/vnet_status.rs
> @@ -0,0 +1,253 @@
> +use std::cmp::Ordering;
> +use std::future::Future;
> +use std::pin::Pin;
> +use std::rc::Rc;
> +
> +use anyhow::{Context, Error};
> +
> +use proxmox_yew_comp::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster};
> +use pwt::props::ExtractPrimaryKey;
> +use yew::virtual_dom::{Key, VComp, VNode};
> +use yew::{AttrValue, Properties};
> +
> +use pdm_client::types::SdnVnetMacVrf;
> +use pwt::props::{
> +    ContainerBuilder, EventSubscriber, FieldBuilder, WidgetBuilder, WidgetStyleBuilder,
> +};
> +use pwt::state::Store;
> +use pwt::tr;
> +use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader};
> +use pwt::widget::form::Combobox;
> +use pwt::widget::{error_message, Button, Column, InputPanel, Toolbar};
> +
> +use crate::pdm_client;
> +use crate::sdn::evpn::NodeList;
> +
> +#[derive(Clone, PartialEq, Properties, Default)]
> +pub struct VnetStatusTable {
> +    remote: String,
> +    vnet: String,
> +    nodes: Option<NodeList>,
> +}
> +
> +impl VnetStatusTable {
> +    pub fn new(remote: String, vnet: String, nodes: Option<NodeList>) -> Self {
> +        yew::props!(Self {
> +            vnet,
> +            remote,
> +            nodes
> +        })
> +    }
> +}
> +
> +impl From<VnetStatusTable> for VNode {
> +    fn from(value: VnetStatusTable) -> Self {
> +        let comp = VComp::new::<LoadableComponentMaster<VnetStatusComponent>>(Rc::new(value), None);
> +        VNode::from(comp)
> +    }
> +}
> +
> +#[derive(Clone, PartialEq)]
> +#[repr(transparent)]
> +pub struct MacVrfEntry(pub SdnVnetMacVrf);
> +
> +impl From<SdnVnetMacVrf> for MacVrfEntry {
> +    fn from(value: SdnVnetMacVrf) -> Self {
> +        Self(value)
> +    }
> +}
> +
> +impl ExtractPrimaryKey for MacVrfEntry {
> +    fn extract_key(&self) -> Key {
> +        Key::from(self.0.ip.as_str())
> +    }
> +}
> +
> +fn default_sorter(a: &MacVrfEntry, b: &MacVrfEntry) -> Ordering {
> +    a.0.ip.cmp(&b.0.ip)
> +}
> +

v  Can be private, I think?

> +pub struct VnetStatusComponent {
> +    store: Store<MacVrfEntry>,
> +    columns: Rc<Vec<DataTableHeader<MacVrfEntry>>>,
> +    nodes: Option<Rc<Vec<AttrValue>>>,
> +    selected_node: Option<AttrValue>,
> +    error_msg: Option<String>,
> +    vrf_loading: bool,
> +}
> +
> +impl VnetStatusComponent {
> +    fn columns() -> Rc<Vec<DataTableHeader<MacVrfEntry>>> {
> +        Rc::new(vec![
> +            DataTableColumn::new(tr!("IP Address"))
> +                .get_property(|entry: &MacVrfEntry| &entry.0.ip)
> +                .into(),
> +            DataTableColumn::new(tr!("MAC Address"))
> +                .get_property(|entry: &MacVrfEntry| &entry.0.mac)
> +                .into(),
> +            DataTableColumn::new(tr!("via"))
> +                .get_property(|entry: &MacVrfEntry| &entry.0.nexthop)
> +                .into(),
> +        ])
> +    }
> +}
> +

v can be private, I think?

> +#[derive(Debug)]
> +pub enum VnetStatusComponentMsg {
> +    NodeSelected(Option<String>),
> +    NodeListLoaded(Rc<Vec<AttrValue>>),
> +    VnetStatusLoaded(Result<Vec<SdnVnetMacVrf>, Error>),
> +}
> +
> +impl LoadableComponent for VnetStatusComponent {
> +    type Message = VnetStatusComponentMsg;
> +    type Properties = VnetStatusTable;
> +    type ViewState = ();
> +
> +    fn create(_ctx: &LoadableComponentContext<Self>) -> Self {
> +        Self {
> +            store: Store::new(),
> +            columns: Self::columns(),
> +            selected_node: None,
> +            nodes: Default::default(),
> +            error_msg: None,
> +            vrf_loading: false,
> +        }
> +    }
> +
> +    fn load(
> +        &self,
> +        ctx: &proxmox_yew_comp::LoadableComponentContext<Self>,
> +    ) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
> +        let link = ctx.link().clone();
> +        let props = ctx.props().clone();
> +
> +        Box::pin(async move {
> +            let node_list = if let Some(nodes) = props.nodes {
> +                nodes.iter().cloned().map(AttrValue::from).collect()
> +            } else {
> +                pdm_client()
> +                    .pve_list_nodes(&props.remote)
> +                    .await?
> +                    .into_iter()
> +                    .map(|node_index| AttrValue::from(node_index.node))
> +                    .collect()
> +            };
> +
> +            link.send_message(Self::Message::NodeListLoaded(Rc::new(node_list)));
> +
> +            Ok(())
> +        })
> +    }
> +
> +    fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
> +        match msg {
> +            Self::Message::NodeListLoaded(node_list) => {
> +                let selected_node = node_list.iter().next().cloned();
> +
> +                self.nodes = Some(node_list);
> +
> +                if let Some(node) = selected_node {
> +                    ctx.link()
> +                        .send_message(Self::Message::NodeSelected(Some(node.to_string())));
> +                }
> +            }
> +            Self::Message::NodeSelected(node_name) => {
> +                if let Some(node_name) = node_name {
> +                    self.vrf_loading = true;
> +                    self.selected_node = Some(node_name.clone().into());
> +
> +                    let link = ctx.link().clone();
> +                    let props = ctx.props().clone();
> +
> +                    ctx.link().spawn(async move {
> +                        let status_result = pdm_client()
> +                            .pve_sdn_vnet_get_mac_vrf(&props.remote, &node_name, &props.vnet)
> +                            .await;
> +
> +                        link.send_message(Self::Message::VnetStatusLoaded(
> +                            status_result.with_context(|| "could not load vnet status".to_string()),
> +                        ));
> +                    });
> +                }
> +            }
> +            Self::Message::VnetStatusLoaded(vnet_status_result) => {
> +                self.vrf_loading = false;
> +
> +                match vnet_status_result {
> +                    Ok(vnet_status) => {
> +                        self.store
> +                            .write()
> +                            .set_data(vnet_status.into_iter().map(MacVrfEntry::from).collect());
> +
> +                        self.store.set_sorter(default_sorter);
> +
> +                        self.error_msg = None;
> +                    }
> +                    Err(error) => {
> +                        self.store.write().clear();
> +                        self.error_msg = Some(format!("{error:?}"));
> +                    }
> +                }
> +            }
> +        }
> +
> +        true
> +    }
> +
> +    fn main_view(&self, ctx: &proxmox_yew_comp::LoadableComponentContext<Self>) -> yew::Html {
> +        let selected_node = self.selected_node.clone();
> +
> +        let toolbar = Toolbar::new()
> +            .class("pwt-w-100")
> +            .class("pwt-overflow-hidden")
> +            .class("pwt-border-bottom")
> +            .with_child(
> +                InputPanel::new().with_field(
> +                    tr!("Node"),
> +                    Combobox::new()
> +                        .min_width(100)
> +                        .required(true)
> +                        .value(self.selected_node.clone())
> +                        .items(self.nodes.clone().unwrap_or_default())
> +                        .on_change(
> +                            ctx.link()
> +                                .callback(|node| Self::Message::NodeSelected(Some(node))),
> +                        ),
> +                ),
> +            )
> +            .with_flex_spacer()
> +            .with_child(Button::refresh(ctx.loading() || self.vrf_loading).onclick(
> +                ctx.link().callback(move |_| {
> +                    Self::Message::NodeSelected(selected_node.as_ref().map(ToString::to_string))
> +                }),
> +            ));
> +
> +        let table =
> +            DataTable::new(self.columns.clone(), self.store.clone()).class(pwt::css::FlexFit);
> +
> +        let mut column = Column::new()
> +            .class(pwt::css::FlexFit)
> +            .with_child(toolbar)
> +            .with_child(table);
> +
> +        if let Some(msg) = &self.error_msg {
> +            column.add_child(error_message(msg));
> +        }
> +
> +        column.into()
> +    }
> +
> +    fn changed(
> +        &mut self,
> +        ctx: &LoadableComponentContext<Self>,
> +        _old_props: &Self::Properties,
> +    ) -> bool {
> +        self.selected_node = None;
> +        self.nodes = None;
> +
> +        ctx.link().send_reload();
> +
> +        true
> +    }
> +}



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


  reply	other threads:[~2025-11-18 14:13 UTC|newest]

Thread overview: 15+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-11-07  8:59 [pdm-devel] [PATCH proxmox{, -datacenter-manager} 0/8] Integration of IP-VRF and MAC-VRF status to EVPN panel Stefan Hanreich
2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox 1/3] pve-api-types: add zone / vnet status reporting endpoints Stefan Hanreich
2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox 2/3] pve-api-types: generate ip-vrf / mac-vrf endpoints Stefan Hanreich
2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox 3/3] pve-api-types: regenerate Stefan Hanreich
2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 1/5] server: api: sdn: add ip-vrf endpoint Stefan Hanreich
2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 2/5] server: api: sdn: add mac-vrf endpoint Stefan Hanreich
2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 3/5] ui: sdn: evpn: add zone status panel Stefan Hanreich
2025-11-18 14:12   ` Lukas Wagner
2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 4/5] ui: sdn: evpn: add vnet " Stefan Hanreich
2025-11-18 14:12   ` Lukas Wagner [this message]
2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 5/5] sdn: evpn: add detail panel to the evpn panel Stefan Hanreich
2025-11-18 14:13   ` Lukas Wagner
2025-11-19 12:16     ` Stefan Hanreich
2025-11-12 16:18 ` [pdm-devel] [PATCH proxmox{, -datacenter-manager} 0/8] Integration of IP-VRF and MAC-VRF status to EVPN panel Hannes Duerr
2025-11-18 14:14 ` 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=DEBVS1CD371U.22GW6BICA00LN@proxmox.com \
    --to=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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal