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
next prev parent 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