[pdm-devel] [PATCH proxmox-datacenter-manager v2 14/15] ui: sdn: add evpn overview panel

Dominik Csapak d.csapak at proxmox.com
Mon Sep 1 15:44:50 CEST 2025


one question: couldn't have the toolbar stayed the same as before?

as in, create the toolbar in line in the `main_view` and just `clone()` 
it once for each panel?

not that i'm totally against factoring such things out, but the 
`EvpnToolbar` does not do anything special FWICT, so creating
the toolbar inline would have been fine...

On 8/29/25 4:53 PM, Stefan Hanreich wrote:
> This panel shows an overview of the state of SDN EVPN Zones across
> multiple remotes. It includes two different views: a per-remote and a
> per-VRF view. For details on the specific views consult the respective
> commits. It handles the fetching of data and passing them to the
> specific child components and it also handles the dialogues for
> creating new EVPN entities (zones, vnets).
> 
> Signed-off-by: Stefan Hanreich <s.hanreich at proxmox.com>
> ---
>   ui/src/sdn/evpn/evpn_panel.rs | 275 ++++++++++++++++++++++++++++++++++
>   ui/src/sdn/evpn/mod.rs        |   3 +
>   2 files changed, 278 insertions(+)
>   create mode 100644 ui/src/sdn/evpn/evpn_panel.rs
> 
> diff --git a/ui/src/sdn/evpn/evpn_panel.rs b/ui/src/sdn/evpn/evpn_panel.rs
> new file mode 100644
> index 0000000..29c3ee9
> --- /dev/null
> +++ b/ui/src/sdn/evpn/evpn_panel.rs
> @@ -0,0 +1,275 @@
> +use futures::try_join;
> +use std::rc::Rc;
> +
> +use anyhow::Error;
> +use yew::virtual_dom::{VComp, VNode};
> +use yew::{function_component, Callback, Component, Html, MouseEvent, Properties};
> +
> +use pdm_client::types::{ListController, ListControllersType, ListVnet, ListZone, ListZonesType};
> +use proxmox_yew_comp::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster};
> +
> +use pwt::props::{ContainerBuilder, EventSubscriber, StorageLocation, WidgetBuilder};
> +use pwt::state::NavigationContainer;
> +use pwt::tr;
> +use pwt::widget::menu::{Menu, MenuButton, MenuEvent, MenuItem};
> +use pwt::widget::{Button, Column, MiniScrollMode, TabBarItem, TabPanel, Toolbar};
> +use pwt_macros::widget;
> +
> +use crate::pdm_client;
> +use crate::sdn::evpn::{AddVnetWindow, AddZoneWindow, RemoteTree, VrfTree};
> +
> +#[widget(comp=EvpnToolbarComponent)]
> +#[derive(Properties, PartialEq, Clone)]
> +struct EvpnToolbar {
> +    on_add_zone: Callback<MenuEvent>,
> +    on_add_vnet: Callback<MenuEvent>,
> +    on_refresh: Callback<MouseEvent>,
> +}
> +
> +impl EvpnToolbar {
> +    pub fn new(
> +        on_add_zone: Callback<MenuEvent>,
> +        on_add_vnet: Callback<MenuEvent>,
> +        on_refresh: Callback<MouseEvent>,
> +    ) -> Self {
> +        yew::props!(Self {
> +            on_add_zone,
> +            on_add_vnet,
> +            on_refresh,
> +        })
> +    }
> +}
> +
> +struct EvpnToolbarComponent {}
> +
> +impl Component for EvpnToolbarComponent {
> +    type Message = ();
> +    type Properties = EvpnToolbar;
> +
> +    fn create(_ctx: &yew::Context<Self>) -> Self {
> +        Self {}
> +    }
> +
> +    fn view(&self, ctx: &yew::Context<Self>) -> Html {
> +        let add_menu = Menu::new()
> +            .with_item(
> +                MenuItem::new(tr!("Zone"))
> +                    .icon_class("fa fa-th")
> +                    .on_select(ctx.props().on_add_zone.clone()),
> +            )
> +            .with_item(
> +                MenuItem::new(tr!("VNet"))
> +                    .icon_class("fa fa-sdn-vnet")
> +                    .on_select(ctx.props().on_add_vnet.clone()),
> +            );
> +
> +        Toolbar::new()
> +            .class("pwt-w-100")
> +            .class("pwt-overflow-hidden")
> +            .class("pwt-border-bottom")
> +            .with_child(MenuButton::new(tr!("Add")).show_arrow(true).menu(add_menu))
> +            .with_flex_spacer()
> +            .with_child(Button::refresh(false).onclick(ctx.props().on_refresh.clone()))
> +            .into()
> +    }
> +}
> +
> +#[derive(PartialEq, Properties)]
> +pub struct EvpnPanel {}
> +
> +impl Default for EvpnPanel {
> +    fn default() -> Self {
> +        Self::new()
> +    }
> +}
> +
> +impl EvpnPanel {
> +    pub fn new() -> Self {
> +        Self {}
> +    }
> +}
> +
> +impl From<EvpnPanel> for VNode {
> +    fn from(value: EvpnPanel) -> Self {
> +        let comp = VComp::new::<LoadableComponentMaster<EvpnPanelComponent>>(Rc::new(value), None);
> +        VNode::from(comp)
> +    }
> +}
> +
> +pub enum EvpnPanelMsg {
> +    Reload,
> +    LoadFinished {
> +        controllers: Rc<Vec<ListController>>,
> +        zones: Rc<Vec<ListZone>>,
> +        vnets: Rc<Vec<ListVnet>>,
> +    },
> +}
> +
> +#[derive(Debug, PartialEq)]
> +pub enum EvpnPanelViewState {
> +    AddZone,
> +    AddVnet,
> +}
> +
> +async fn load_zones() -> Result<Vec<ListZone>, Error> {
> +    let client = pdm_client();
> +    let data = client
> +        .pve_sdn_list_zones(false, true, ListZonesType::Evpn)
> +        .await?;
> +    Ok(data)
> +}
> +
> +async fn load_controllers() -> Result<Vec<ListController>, Error> {
> +    let client = pdm_client();
> +    let data = client
> +        .pve_sdn_list_controllers(false, true, ListControllersType::Evpn)
> +        .await?;
> +    Ok(data)
> +}
> +
> +async fn load_vnets() -> Result<Vec<ListVnet>, Error> {
> +    let client = pdm_client();
> +    let data = client.pve_sdn_list_vnets(false, true).await?;
> +    Ok(data)
> +}
> +
> +pub struct EvpnPanelComponent {
> +    controllers: Rc<Vec<ListController>>,
> +    zones: Rc<Vec<ListZone>>,
> +    vnets: Rc<Vec<ListVnet>>,
> +}
> +
> +impl LoadableComponent for EvpnPanelComponent {
> +    type Properties = EvpnPanel;
> +    type Message = EvpnPanelMsg;
> +    type ViewState = EvpnPanelViewState;
> +
> +    fn create(_ctx: &LoadableComponentContext<Self>) -> Self {
> +        Self {
> +            controllers: Default::default(),
> +            zones: Default::default(),
> +            vnets: Default::default(),
> +        }
> +    }
> +
> +    fn load(
> +        &self,
> +        ctx: &LoadableComponentContext<Self>,
> +    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), Error>>>> {
> +        let link = ctx.link().clone();
> +
> +        Box::pin(async move {
> +            let (controllers, zones, vnets) =
> +                try_join!(load_controllers(), load_zones(), load_vnets())?;
> +
> +            link.send_message(Self::Message::LoadFinished {
> +                controllers: Rc::new(controllers),
> +                zones: Rc::new(zones),
> +                vnets: Rc::new(vnets),
> +            });
> +
> +            Ok(())
> +        })
> +    }
> +
> +    fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
> +        match msg {
> +            Self::Message::LoadFinished {
> +                controllers,
> +                zones,
> +                vnets,
> +            } => {
> +                self.controllers = controllers;
> +                self.zones = zones;
> +                self.vnets = vnets;
> +
> +                return true;
> +            }
> +            Self::Message::Reload => {
> +                ctx.link().send_reload();
> +            }
> +        }
> +
> +        false
> +    }
> +
> +    fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> Html {
> +        let on_add_zone = ctx
> +            .link()
> +            .change_view_callback(|_| Some(Self::ViewState::AddZone));
> +
> +        let on_add_vnet = ctx
> +            .link()
> +            .change_view_callback(|_| Some(Self::ViewState::AddVnet));
> +
> +        let on_refresh = ctx.link().callback(|_| Self::Message::Reload);
> +
> +        let panel = TabPanel::new()
> +            .state_id(StorageLocation::session("EvpnPanelState"))
> +            .class(pwt::css::FlexFit)
> +            .router(true)
> +            .scroll_mode(MiniScrollMode::Arrow)
> +            .with_item(
> +                TabBarItem::new()
> +                    .key("remotes")
> +                    .label(tr!("Remotes"))
> +                    .icon_class("fa fa-server"),
> +                Column::new()
> +                    .class(pwt::css::FlexFit)
> +                    .with_child(EvpnToolbar::new(
> +                        on_add_zone.clone(),
> +                        on_add_vnet.clone(),
> +                        on_refresh.clone(),
> +                    ))
> +                    .with_child(RemoteTree::new(
> +                        self.zones.clone(),
> +                        self.vnets.clone(),
> +                        self.controllers.clone(),
> +                    )),
> +            )
> +            .with_item(
> +                TabBarItem::new()
> +                    .key("vrfs")
> +                    .label(tr!("IP-VRFs"))
> +                    .icon_class("fa fa-th"),
> +                Column::new()
> +                    .class(pwt::css::FlexFit)
> +                    .with_child(EvpnToolbar::new(on_add_zone, on_add_vnet, on_refresh))
> +                    .with_child(VrfTree::new(
> +                        self.zones.clone(),
> +                        self.vnets.clone(),
> +                        self.controllers.clone(),
> +                    )),
> +            );
> +
> +        let navigation_container = NavigationContainer::new().with_child(panel);
> +
> +        Column::new()
> +            .class(pwt::css::FlexFit)
> +            .with_child(navigation_container)
> +            .into()
> +    }
> +
> +    fn dialog_view(
> +        &self,
> +        ctx: &LoadableComponentContext<Self>,
> +        view_state: &Self::ViewState,
> +    ) -> Option<Html> {
> +        let scope = ctx.link().clone();
> +
> +        let on_success = Callback::from(move |upid: String| {
> +            scope.show_task_log(upid, None);
> +        });
> +
> +        let on_close = ctx.link().clone().change_view_callback(|_| None);
> +
> +        Some(match view_state {
> +            EvpnPanelViewState::AddZone => {
> +                AddZoneWindow::new(self.controllers.clone(), on_success, on_close).into()
> +            }
> +            EvpnPanelViewState::AddVnet => {
> +                AddVnetWindow::new(self.zones.clone(), on_success, on_close).into()
> +            }
> +        })
> +    }
> +}
> diff --git a/ui/src/sdn/evpn/mod.rs b/ui/src/sdn/evpn/mod.rs
> index adcf272..1948ecf 100644
> --- a/ui/src/sdn/evpn/mod.rs
> +++ b/ui/src/sdn/evpn/mod.rs
> @@ -1,3 +1,6 @@
> +mod evpn_panel;
> +pub use evpn_panel::EvpnPanel;
> +
>   mod remote_tree;
>   pub use remote_tree::RemoteTree;
>   





More information about the pdm-devel mailing list