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
next prev parent 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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox