From: Dominik Csapak <d.csapak@proxmox.com>
To: Proxmox Datacenter Manager development discussion
<pdm-devel@lists.proxmox.com>,
Stefan Hanreich <s.hanreich@proxmox.com>
Subject: Re: [pdm-devel] [PATCH proxmox-datacenter-manager 3/5] ui: add sdn status report to dashboard
Date: Tue, 9 Sep 2025 15:10:46 +0200 [thread overview]
Message-ID: <cc4ddde6-64a4-4e3e-be0c-871401fd6203@proxmox.com> (raw)
In-Reply-To: <20250909100838.234778-5-s.hanreich@proxmox.com>
two comments inline
On 9/9/25 12:08 PM, Stefan Hanreich wrote:
> This also includes support for external links, searching and
> navigating to the dedicated SDN overview. For now, add it to the task
> summary row, since there are issues with only showing one element in a
> row due to the CSS rules. While this breaks a little bit with the
> current grouping, the widget is quite small so it would look weird in
> a single row and we can always decide to move it around as soon as we
> add more elements to the dashboard.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
> ui/src/dashboard/mod.rs | 17 +++-
> ui/src/dashboard/sdn_zone_panel.rs | 155 +++++++++++++++++++++++++++++
> ui/src/lib.rs | 14 ++-
> ui/src/pve/utils.rs | 16 ++-
> ui/src/renderer.rs | 4 +-
> 5 files changed, 199 insertions(+), 7 deletions(-)
> create mode 100644 ui/src/dashboard/sdn_zone_panel.rs
>
> diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
> index 0659dc0..626a6bf 100644
> --- a/ui/src/dashboard/mod.rs
> +++ b/ui/src/dashboard/mod.rs
> @@ -25,7 +25,7 @@ use pwt::{
> };
>
> use pdm_api_types::{
> - resource::{GuestStatusCount, NodeStatusCount, ResourcesStatus},
> + resource::{GuestStatusCount, NodeStatusCount, ResourcesStatus, SdnZoneCount},
> TaskStatistics,
> };
> use pdm_client::types::TopEntity;
> @@ -46,6 +46,9 @@ use remote_panel::RemotePanel;
> mod guest_panel;
> use guest_panel::GuestPanel;
>
> +mod sdn_zone_panel;
> +use sdn_zone_panel::SdnZonePanel;
> +
> mod status_row;
> use status_row::DashboardStatusRow;
>
> @@ -242,6 +245,15 @@ impl PdmDashboard {
> ))
> }
>
> + fn create_sdn_panel(&self, status: &SdnZoneCount) -> Panel {
> + Panel::new()
> + .flex(1.0)
> + .width(200)
> + .title(self.create_title_with_icon("sdn", tr!("SDN Zones")))
> + .border(true)
> + .with_child(SdnZonePanel::new((!self.loading).then_some(status.clone())))
> + }
> +
> fn create_task_summary_panel(
> &self,
> statistics: &StatisticsOptions,
> @@ -620,7 +632,8 @@ impl Component for PdmDashboard {
> .class(pwt::css::Flex::Fill)
> .class(FlexWrap::Wrap)
> .with_child(self.create_task_summary_panel(&self.statistics, None))
> - .with_child(self.create_task_summary_panel(&self.statistics, Some(5))),
> + .with_child(self.create_task_summary_panel(&self.statistics, Some(5)))
> + .with_child(self.create_sdn_panel(&self.status.sdn_zones)),
> );
>
> Panel::new()
> diff --git a/ui/src/dashboard/sdn_zone_panel.rs b/ui/src/dashboard/sdn_zone_panel.rs
> new file mode 100644
> index 0000000..bcac36b
> --- /dev/null
> +++ b/ui/src/dashboard/sdn_zone_panel.rs
> @@ -0,0 +1,155 @@
> +use std::rc::Rc;
> +
> +use pdm_api_types::resource::{ResourceType, SdnStatus, SdnZoneCount};
> +use pdm_search::{Search, SearchTerm};
> +use pwt::{
> + css::{self, FontColor, TextAlign},
> + prelude::*,
> + widget::{Container, Fa, List, ListTile},
> +};
> +use yew::{
> + virtual_dom::{VComp, VNode},
> + Properties,
> +};
> +
> +use crate::search_provider::get_search_provider;
> +
> +use super::loading_column;
> +
> +#[derive(PartialEq, Clone, Properties)]
> +pub struct SdnZonePanel {
> + status: Option<SdnZoneCount>,
> +}
> +
> +impl SdnZonePanel {
> + pub fn new(status: Option<SdnZoneCount>) -> Self {
> + yew::props!(Self { status })
> + }
> +}
> +
> +impl From<SdnZonePanel> for VNode {
> + fn from(value: SdnZonePanel) -> Self {
> + let comp = VComp::new::<SdnZonePanelComponent>(Rc::new(value), None);
> + VNode::from(comp)
> + }
> +}
> +
> +#[derive(PartialEq, Clone)]
> +pub enum StatusRow {
> + State(SdnStatus, u64),
> + All(u64),
> +}
> +
> +impl StatusRow {
> + fn icon(&self) -> Fa {
> + let (icon, color) = match self {
> + Self::All(_) => ("th", None),
> + Self::State(SdnStatus::Available, _) => ("check", Some(FontColor::Success)),
> + Self::State(SdnStatus::Error, _) => ("times-circle", Some(FontColor::Error)),
> + Self::State(SdnStatus::Unknown, _) => ("question", None),
> + };
> +
> + let mut icon = Fa::new(icon);
> +
> + if let Some(color) = color {
> + icon = icon.class(color);
> + }
> +
> + icon
> + }
> +}
> +
> +pub struct SdnZonePanelComponent {}
> +
> +impl yew::Component for SdnZonePanelComponent {
> + type Message = Search;
> + type Properties = SdnZonePanel;
> +
> + fn create(_ctx: &yew::Context<Self>) -> Self {
> + Self {}
> + }
> +
> + fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
> + if let Some(provider) = get_search_provider(ctx) {
> + provider.search(msg);
> + }
> +
> + false
> + }
> +
> + fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
> + let props = ctx.props();
> +
> + let Some(status) = &props.status else {
> + return loading_column().into();
> + };
> +
> + let data = vec![
> + StatusRow::State(SdnStatus::Available, status.available),
> + StatusRow::State(SdnStatus::Error, status.error),
> + StatusRow::All(status.available + status.error + status.unknown),
> + ];
> +
> + let tiles: Vec<_> = data
> + .into_iter()
> + .filter_map(|row| create_list_tile(ctx.link(), row))
> + .collect();
> +
> + let list = List::new(tiles.len() as u64, move |idx: u64| {
> + tiles[idx as usize].clone()
> + })
> + .padding(4)
> + .class(css::Flex::Fill)
> + .grid_template_columns("auto auto 1fr auto");
> +
> + list.into()
> + }
> +}
> +
> +fn create_list_tile(
> + link: &html::Scope<SdnZonePanelComponent>,
> + status_row: StatusRow,
> +) -> Option<ListTile> {
> + let (icon, status, count) = match status_row {
> + StatusRow::State(sdn_status, count) => (status_row.icon(), Some(sdn_status), count),
> + StatusRow::All(count) => (status_row.icon(), None, count),
> + };
> +
> + let name = status
> + .map(|status| status.to_string())
> + .unwrap_or_else(|| "All".to_string());
> +
> + Some(
> + ListTile::new()
> + .tabindex(0)
> + .interactive(true)
> + .with_child(icon)
> + .with_child(Container::new().padding_x(2).with_child(name))
> + .with_child(
> + Container::new()
> + .class(TextAlign::Right)
> + .padding_end(2)
> + .with_child(count),
> + )
> + .with_child(Fa::new("search"))
> + .onclick(link.callback(move |_| create_sdn_zone_search_term(status)))
> + .onkeydown(link.batch_callback(
> + move |event: KeyboardEvent| match event.key().as_str() {
> + "Enter" | " " => Some(create_sdn_zone_search_term(status)),
> + _ => None,
> + },
> + )),
> + )
> +}
> +
> +fn create_sdn_zone_search_term(status: Option<SdnStatus>) -> Search {
> + let resource_type: ResourceType = ResourceType::PveSdnZone;
> +
> + let mut terms = vec![SearchTerm::new(resource_type.as_str()).category(Some("type"))];
> +
> + if let Some(status) = status {
> + terms.push(SearchTerm::new(status.to_string()).category(Some("status")));
> + }
> +
> + Search::with_terms(terms)
> +}
> diff --git a/ui/src/lib.rs b/ui/src/lib.rs
> index 5ffbff3..e4bfbb7 100644
> --- a/ui/src/lib.rs
> +++ b/ui/src/lib.rs
> @@ -1,4 +1,4 @@
> -use pdm_api_types::resource::{PveLxcResource, PveQemuResource};
> +use pdm_api_types::resource::{PveLxcResource, PveQemuResource, PveSdnResource};
> use pdm_client::types::Resource;
> use serde::{Deserialize, Serialize};
>
> @@ -151,13 +151,21 @@ pub(crate) fn navigate_to<C: yew::Component>(
> pdm_client::types::Resource::PveStorage(storage) => {
> format!("storage+{}+{}", storage.node, storage.storage)
> }
> + pdm_client::types::Resource::PveSdn(PveSdnResource::Zone(_)) => {
> + "sdn/zones".to_string()
> + }
> pdm_client::types::Resource::PbsDatastore(store) => store.name.clone(),
> // FIXME: implement
> _ => return None,
> })
> })
> .unwrap_or_default();
> - nav.push(&yew_router::AnyRoute::new(format!("/remote-{remote}/{id}")));
> +
> + if let Some(pdm_client::types::Resource::PveSdn(_)) = resource {
> + nav.push(&yew_router::AnyRoute::new(format!("/{id}")));
> + } else {
> + nav.push(&yew_router::AnyRoute::new(format!("/remote-{remote}/{id}")));
> + }
i don't really like the special casing of sdn here. I think it would be
better, e.g. if the match would return a tuple of the (optional) remote
and the id
> }
> }
>
> @@ -167,7 +175,7 @@ pub(crate) fn get_resource_node(resource: &Resource) -> Option<&str> {
> Resource::PveQemu(qemu) => Some(&qemu.node),
> Resource::PveLxc(lxc) => Some(&lxc.node),
> Resource::PveNode(node) => Some(&node.node),
> - Resource::PveSdn(sdn) => Some(&sdn.sdn),
> + Resource::PveSdn(sdn) => Some(sdn.node()),
> Resource::PbsNode(_) => None,
> Resource::PbsDatastore(_) => None,
> }
> diff --git a/ui/src/pve/utils.rs b/ui/src/pve/utils.rs
> index 7663734..a49205d 100644
> --- a/ui/src/pve/utils.rs
> +++ b/ui/src/pve/utils.rs
> @@ -1,6 +1,7 @@
> use anyhow::Error;
> use pdm_api_types::resource::{
> - PveLxcResource, PveNodeResource, PveQemuResource, PveStorageResource,
> + PveLxcResource, PveNodeResource, PveQemuResource, PveStorageResource, SdnStatus,
> + SdnZoneResource,
> };
> use pdm_client::types::{
> LxcConfig, LxcConfigMp, LxcConfigRootfs, LxcConfigUnused, PveQmIde, QemuConfig, QemuConfigSata,
> @@ -88,6 +89,19 @@ pub fn render_node_status_icon(node: &PveNodeResource) -> Container {
> .with_child(Fa::from(extra).fixed_width().class("status-icon"))
> }
>
> +/// Renders the status icon for a PveNode
it's not for a PveNode ;)
> +pub fn render_sdn_status_icon(zone: &SdnZoneResource) -> Container {
> + let extra = match zone.status {
> + SdnStatus::Available => NodeState::Online,
> + SdnStatus::Error => NodeState::Offline,
> + _ => NodeState::Unknown,
> + };
> + Container::new()
> + .class("pdm-type-icon")
> + .with_child(Fa::new("th").fixed_width())
> + .with_child(Fa::from(extra).fixed_width().class("status-icon"))
> +}
> +
> /// Renders the status icon for a PveStorage
> pub fn render_storage_status_icon(node: &PveStorageResource) -> Container {
> let extra = match node.status.as_str() {
> diff --git a/ui/src/renderer.rs b/ui/src/renderer.rs
> index 5ebd9a3..e179cd5 100644
> --- a/ui/src/renderer.rs
> +++ b/ui/src/renderer.rs
> @@ -1,3 +1,4 @@
> +use pdm_api_types::resource::PveSdnResource;
> use pwt::{
> css,
> prelude::*,
> @@ -17,7 +18,7 @@ pub fn render_resource_name(resource: &Resource, vmid_first: bool) -> String {
> Resource::PveQemu(qemu) => pve::utils::render_qemu_name(qemu, vmid_first),
> Resource::PveLxc(lxc) => pve::utils::render_lxc_name(lxc, vmid_first),
> Resource::PveNode(node) => node.node.clone(),
> - Resource::PveSdn(sdn) => sdn.sdn.clone(),
> + Resource::PveSdn(sdn) => sdn.name().to_string(),
> Resource::PbsNode(node) => node.name.clone(),
> Resource::PbsDatastore(store) => store.name.clone(),
> }
> @@ -43,6 +44,7 @@ pub fn render_status_icon(resource: &Resource) -> Container {
> Resource::PveQemu(qemu) => pve::utils::render_qemu_status_icon(qemu),
> Resource::PveLxc(lxc) => pve::utils::render_lxc_status_icon(lxc),
> Resource::PveNode(node) => pve::utils::render_node_status_icon(node),
> + Resource::PveSdn(PveSdnResource::Zone(zone)) => pve::utils::render_sdn_status_icon(zone),
> // FIXME: implement remaining types
> _ => Container::new().with_child(render_resource_icon(resource)),
> }
_______________________________________________
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-09-09 13:11 UTC|newest]
Thread overview: 18+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-09-09 10:08 [pdm-devel] [PATCH manager/proxmox-datacenter-manager 0/6] Add SDN resources to dashboard + SDN zone overview tree Stefan Hanreich
2025-09-09 10:08 ` [pdm-devel] [PATCH pve-manager 1/1] cluster: resources: add sdn property to cluster resources schema Stefan Hanreich
2025-09-09 10:08 ` [pdm-devel] [PATCH proxmox-datacenter-manager 1/5] pdm-api-types: add sdn cluster resource Stefan Hanreich
2025-09-09 11:13 ` Stefan Hanreich
2025-09-09 11:24 ` Thomas Lamprecht
2025-09-09 11:26 ` Stefan Hanreich
2025-09-09 10:08 ` [pdm-devel] [PATCH proxmox-datacenter-manager 2/5] server: api: add resources_by_type api call Stefan Hanreich
2025-09-09 10:08 ` [pdm-devel] [PATCH proxmox-datacenter-manager 3/5] ui: add sdn status report to dashboard Stefan Hanreich
2025-09-09 13:10 ` Dominik Csapak [this message]
2025-09-09 13:22 ` Stefan Hanreich
2025-09-09 10:08 ` [pdm-devel] [PATCH proxmox-datacenter-manager 4/5] ui: images: add sdn icon Stefan Hanreich
2025-09-09 13:16 ` Dominik Csapak
2025-09-09 13:21 ` Stefan Hanreich
2025-09-09 10:08 ` [pdm-devel] [PATCH proxmox-datacenter-manager 5/5] ui: sdn: add zone tree Stefan Hanreich
2025-09-09 13:41 ` Dominik Csapak
2025-09-09 13:57 ` Stefan Hanreich
2025-09-09 13:43 ` [pdm-devel] [PATCH manager/proxmox-datacenter-manager 0/6] Add SDN resources to dashboard + SDN zone overview tree Dominik Csapak
2025-09-09 14:06 ` Stefan Hanreich
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=cc4ddde6-64a4-4e3e-be0c-871401fd6203@proxmox.com \
--to=d.csapak@proxmox.com \
--cc=pdm-devel@lists.proxmox.com \
--cc=s.hanreich@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