[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