From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id CE59F1FF191 for ; Tue, 9 Sep 2025 12:09:11 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 5245D5C67; Tue, 9 Sep 2025 12:09:15 +0200 (CEST) From: Stefan Hanreich To: pdm-devel@lists.proxmox.com Date: Tue, 9 Sep 2025 12:08:33 +0200 Message-ID: <20250909100838.234778-7-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20250909100838.234778-1-s.hanreich@proxmox.com> References: <20250909100838.234778-1-s.hanreich@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.182 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RDNS_NONE 0.793 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record Subject: [pdm-devel] [PATCH proxmox-datacenter-manager 5/5] ui: sdn: add zone tree X-BeenThere: pdm-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Datacenter Manager development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" This shows an overview of the state of all zones across all remotes, similar to the current overview in Proxmox VE, in the SDN tab. Add it as a top-level container and move the EVPN section below that, to mimic the menu structure from Proxmox VE. Signed-off-by: Stefan Hanreich --- ui/src/main_menu.rs | 15 +- ui/src/sdn/mod.rs | 3 + ui/src/sdn/zone_tree.rs | 299 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 ui/src/sdn/zone_tree.rs diff --git a/ui/src/main_menu.rs b/ui/src/main_menu.rs index 7eac775..f440be8 100644 --- a/ui/src/main_menu.rs +++ b/ui/src/main_menu.rs @@ -18,6 +18,7 @@ use pdm_api_types::remotes::RemoteType; use crate::remotes::RemotesPanel; use crate::sdn::evpn::EvpnPanel; +use crate::sdn::ZoneTree; use crate::{ AccessControl, CertificatesPanel, Dashboard, RemoteList, ServerAdministration, SystemConfiguration, @@ -248,8 +249,10 @@ impl Component for PdmMainMenu { admin_submenu, ); + let mut sdn_submenu = Menu::new(); + register_view( - &mut menu, + &mut sdn_submenu, &mut content, tr!("EVPN"), "evpn", @@ -257,6 +260,16 @@ impl Component for PdmMainMenu { |_| EvpnPanel::new().into(), ); + register_submenu( + &mut menu, + &mut content, + tr!("SDN"), + "sdn", + Some("fa fa-sdn"), + |_| ZoneTree::new().into(), + sdn_submenu, + ); + let mut remote_submenu = Menu::new(); for remote in self.remote_list_cache.iter() { diff --git a/ui/src/sdn/mod.rs b/ui/src/sdn/mod.rs index ef2eab9..b6ab8ad 100644 --- a/ui/src/sdn/mod.rs +++ b/ui/src/sdn/mod.rs @@ -1 +1,4 @@ pub mod evpn; + +mod zone_tree; +pub use zone_tree::ZoneTree; diff --git a/ui/src/sdn/zone_tree.rs b/ui/src/sdn/zone_tree.rs new file mode 100644 index 0000000..55a5889 --- /dev/null +++ b/ui/src/sdn/zone_tree.rs @@ -0,0 +1,299 @@ +use futures::Future; +use std::cmp::Ordering; +use std::pin::Pin; +use std::rc::Rc; + +use yew::virtual_dom::{Key, VComp, VNode}; +use yew::{Html, Properties}; + +use pdm_api_types::resource::{ + PveSdnResource, RemoteResources, ResourceType, SdnStatus, SdnZoneResource, +}; +use pdm_client::types::Resource; +use proxmox_yew_comp::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster}; +use pwt::props::EventSubscriber; +use pwt::widget::{Button, Toolbar}; +use pwt::{ + css, + css::FontColor, + props::{ContainerBuilder, ExtractPrimaryKey, WidgetBuilder}, + state::{Selection, SlabTree, TreeStore}, + tr, + widget::{ + data_table::{DataTable, DataTableColumn, DataTableHeader}, + error_message, Column, Fa, Row, + }, +}; + +use crate::pdm_client; + +#[derive(PartialEq, Properties)] +pub struct ZoneTree {} + +impl ZoneTree { + pub fn new() -> Self { + yew::props!(Self {}) + } +} + +impl From for VNode { + fn from(value: ZoneTree) -> Self { + let comp = VComp::new::>(Rc::new(value), None); + VNode::from(comp) + } +} + +#[derive(Clone, PartialEq, Debug)] +struct ZoneData { + remote: String, + node: String, + name: String, + status: SdnStatus, +} + +#[derive(Clone, PartialEq, Debug)] +enum ZoneTreeEntry { + Root, + Remote(String), + Node(String, String), + Zone(ZoneData), +} + +impl ZoneTreeEntry { + fn from_zone_resource(remote: String, value: SdnZoneResource) -> Self { + Self::Zone(ZoneData { + remote, + node: value.node.clone(), + name: value.name.clone(), + status: value.status, + }) + } + + fn name(&self) -> &str { + match &self { + Self::Root => "", + Self::Remote(name) => name, + Self::Node(_, name) => name, + Self::Zone(zone) => &zone.name, + } + } +} + +impl ExtractPrimaryKey for ZoneTreeEntry { + fn extract_key(&self) -> yew::virtual_dom::Key { + Key::from(match self { + ZoneTreeEntry::Root => "/".to_string(), + ZoneTreeEntry::Remote(name) => format!("/{name}"), + ZoneTreeEntry::Node(remote_name, name) => format!("/{remote_name}/{name}"), + ZoneTreeEntry::Zone(zone) => format!("/{}/{}/{}", zone.remote, zone.node, zone.name), + }) + } +} + +pub enum ZoneTreeMsg { + LoadFinished(Vec), + Reload, +} + +pub struct ZoneTreeComponent { + store: TreeStore, + selection: Selection, + remote_errors: Vec, + columns: Rc>>, +} + +fn default_sorter(a: &ZoneTreeEntry, b: &ZoneTreeEntry) -> Ordering { + a.name().cmp(b.name()) +} + +impl ZoneTreeComponent { + fn columns(store: TreeStore) -> Rc>> { + Rc::new(vec![ + DataTableColumn::new(tr!("Name")) + .tree_column(store) + .render(|entry: &ZoneTreeEntry| { + let mut row = Row::new().class(css::AlignItems::Baseline).gap(2); + let name = entry.name(); + + row = match entry { + ZoneTreeEntry::Remote(_) => row.with_child(Fa::new("server")), + ZoneTreeEntry::Node(_, _) => row.with_child(Fa::new("building")), + ZoneTreeEntry::Zone(_) => row.with_child(Fa::new("th")), + _ => row, + }; + + row.with_child(name).into() + }) + .sorter(default_sorter) + .into(), + DataTableColumn::new(tr!("Status")) + .render(|entry: &ZoneTreeEntry| { + let mut row = Row::new().class(css::AlignItems::Baseline).gap(2); + + if let ZoneTreeEntry::Zone(zone) = entry { + row = match zone.status { + SdnStatus::Available => { + row.with_child(Fa::new("check").class(FontColor::Success)) + } + SdnStatus::Error => { + row.with_child(Fa::new("times-circle").class(FontColor::Error)) + } + _ => row, + }; + + row = row.with_child(zone.status); + } else { + row = row.with_child(""); + } + + row.into() + }) + .into(), + ]) + } +} + +fn build_store_from_response( + remote_resources: Vec, +) -> (SlabTree, Vec) { + let mut tree = SlabTree::new(); + + let mut root = tree.set_root(ZoneTreeEntry::Root); + root.set_expanded(true); + + let mut remote_errors = Vec::new(); + + for resources in remote_resources { + if let Some(error) = resources.error { + remote_errors.push(format!( + "could not fetch resources from remote {}: {error}", + resources.remote, + )); + continue; + } + + let mut remote = root.append(ZoneTreeEntry::Remote(resources.remote.clone())); + remote.set_expanded(true); + + for resource in resources.resources { + if let Resource::PveSdn(PveSdnResource::Zone(zone_resource)) = resource { + let node_entry = remote.children_mut().find(|entry| { + if let ZoneTreeEntry::Node(_, name) = entry.record() { + if name == &zone_resource.node { + return true; + } + } + + false + }); + + let node_name = zone_resource.node.clone(); + + let entry = + ZoneTreeEntry::from_zone_resource(resources.remote.clone(), zone_resource); + + match node_entry { + Some(mut node_entry) => { + node_entry.append(entry); + } + None => { + let mut node_entry = + remote.append(ZoneTreeEntry::Node(resources.remote.clone(), node_name)); + + node_entry.set_expanded(true); + + node_entry.append(entry); + } + }; + } + } + } + + (tree, remote_errors) +} + +impl LoadableComponent for ZoneTreeComponent { + type Properties = ZoneTree; + type Message = ZoneTreeMsg; + type ViewState = (); + + fn create(_ctx: &LoadableComponentContext) -> Self { + let store = TreeStore::new().view_root(false); + store.set_sorter(default_sorter); + + let selection = Selection::new(); + + Self { + store: store.clone(), + selection, + remote_errors: Vec::new(), + columns: Self::columns(store), + } + } + + fn load( + &self, + ctx: &LoadableComponentContext, + ) -> Pin>>> { + let link = ctx.link().clone(); + + Box::pin(async move { + let client = pdm_client(); + let remote_resources = client + .resources_by_type(None, ResourceType::PveSdnZone) + .await?; + link.send_message(Self::Message::LoadFinished(remote_resources)); + + Ok(()) + }) + } + + fn update(&mut self, ctx: &LoadableComponentContext, msg: Self::Message) -> bool { + match msg { + Self::Message::LoadFinished(remote_resources) => { + let (data, remote_errors) = build_store_from_response(remote_resources); + self.store.write().update_root_tree(data); + self.store.set_sorter(default_sorter); + + self.remote_errors = remote_errors; + + return true; + } + Self::Message::Reload => { + ctx.link().send_reload(); + } + } + + false + } + + #[allow(unused_variables)] + fn toolbar(&self, ctx: &LoadableComponentContext) -> Option { + let on_refresh = ctx.link().callback(|_| ZoneTreeMsg::Reload); + + Some( + Toolbar::new() + .class("pwt-w-100") + .class("pwt-overflow-hidden") + .class("pwt-border-bottom") + .with_flex_spacer() + .with_child(Button::refresh(ctx.loading()).onclick(on_refresh)) + .into(), + ) + } + + fn main_view(&self, _ctx: &LoadableComponentContext) -> yew::Html { + let table = DataTable::new(self.columns.clone(), self.store.clone()) + .selection(self.selection.clone()) + .striped(false) + .class(css::FlexFit); + + let mut column = Column::new().class(pwt::css::FlexFit).with_child(table); + + for remote_error in &self.remote_errors { + column.add_child(error_message(remote_error)); + } + + column.into() + } +} -- 2.47.3 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel