all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Stefan Hanreich <s.hanreich@proxmox.com>
To: Dominik Csapak <d.csapak@proxmox.com>,
	Proxmox Datacenter Manager development discussion
	<pdm-devel@lists.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:22:36 +0200	[thread overview]
Message-ID: <c4bbf55c-53e6-436b-beac-9ca0dfccdfba@proxmox.com> (raw)
In-Reply-To: <cc4ddde6-64a4-4e3e-be0c-871401fd6203@proxmox.com>

thanks! will address both in a new version

On 9/9/25 3:10 PM, Dominik Csapak wrote:
> 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

Yeah, makes sense I'll try to adjust it

>>       }
>>   }
>>   @@ -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 ;)

copy-paste mistake :(

> 
>> +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(),

while I'm at it will also amend this change into the earlier commit
since this line is touched twice in the patch series and a remnant of an
earlier version of the Resource types..

>>           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

  reply	other threads:[~2025-09-09 13:22 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
2025-09-09 13:22     ` Stefan Hanreich [this message]
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=c4bbf55c-53e6-436b-beac-9ca0dfccdfba@proxmox.com \
    --to=s.hanreich@proxmox.com \
    --cc=d.csapak@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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal