public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pdm-devel] [PATCH proxmox-datacenter-manager 0/2] change to tabbed panel when selecting an item in the firewall tree
@ 2025-12-01 15:31 Hannes Laimer
  2025-12-01 15:31 ` [pdm-devel] [PATCH proxmox-datacenter-manager 1/2] ui: firewall: use property view for options instead of dialog Hannes Laimer
                   ` (2 more replies)
  0 siblings, 3 replies; 4+ messages in thread
From: Hannes Laimer @ 2025-12-01 15:31 UTC (permalink / raw)
  To: pdm-devel

This replaces the firewall option edit form with a (read-only) property view.
Also adds direct links to pve for the cluster/node/guest, the currently
active tab (rules or options) defines where the link goes.

Hannes Laimer (2):
  ui: firewall: use property view for options instead of dialog
  ui: firewall: add link to pve to rules/options panel

 ui/src/remotes/firewall/columns.rs    |  37 +----
 ui/src/remotes/firewall/tree.rs       | 227 ++++++++++++++++++++------
 ui/src/remotes/firewall/types.rs      |  64 ++------
 ui/src/remotes/firewall/ui_helpers.rs |  46 +-----
 4 files changed, 199 insertions(+), 175 deletions(-)

-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager 1/2] ui: firewall: use property view for options instead of dialog
  2025-12-01 15:31 [pdm-devel] [PATCH proxmox-datacenter-manager 0/2] change to tabbed panel when selecting an item in the firewall tree Hannes Laimer
@ 2025-12-01 15:31 ` Hannes Laimer
  2025-12-01 15:31 ` [pdm-devel] [PATCH proxmox-datacenter-manager 2/2] ui: firewall: add link to pve to rules/options panel Hannes Laimer
  2025-12-01 23:40 ` [pdm-devel] applied: [PATCH proxmox-datacenter-manager 0/2] change to tabbed panel when selecting an item in the firewall tree Thomas Lamprecht
  2 siblings, 0 replies; 4+ messages in thread
From: Hannes Laimer @ 2025-12-01 15:31 UTC (permalink / raw)
  To: pdm-devel

Replace the edit `EditFirewallOptions` dialog form with inline property
view panels (`FirewallOptions{Cluster,Node,Guest}Panel`). The UI is
restructured to use a `TabPanel` with "Rules" and "Options" tabs, making
firewall options directly accessible alongside the rules list. The
"Options" are read-only. Also removes the cog icon in the tree.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 ui/src/remotes/firewall/columns.rs    |  37 +-----
 ui/src/remotes/firewall/tree.rs       | 162 ++++++++++++++++++--------
 ui/src/remotes/firewall/types.rs      |  64 +++-------
 ui/src/remotes/firewall/ui_helpers.rs |  46 +-------
 4 files changed, 134 insertions(+), 175 deletions(-)

diff --git a/ui/src/remotes/firewall/columns.rs b/ui/src/remotes/firewall/columns.rs
index 454fd81..41cca33 100644
--- a/ui/src/remotes/firewall/columns.rs
+++ b/ui/src/remotes/firewall/columns.rs
@@ -1,4 +1,3 @@
-use proxmox_yew_comp::LoadableComponentContext;
 use pwt::css::AlignItems;
 use pwt::prelude::*;
 use pwt::state::TreeStore;
@@ -8,15 +7,12 @@ use pwt::widget::{Container, Fa, Row};
 use std::rc::Rc;
 use yew::Html;
 
-use super::types::{Scope, TreeEntry, ViewState};
+use super::types::{Scope, TreeEntry};
 use super::ui_helpers::{
     render_firewall_status, render_load_error_message, render_rule_stats, render_warning_icon,
 };
 
-use crate::remotes::firewall::tree::FirewallTreeComponent;
-
 pub fn create_columns(
-    ctx: &LoadableComponentContext<FirewallTreeComponent>,
     store: TreeStore<TreeEntry>,
     loading: bool,
     scope: &Scope,
@@ -27,7 +23,6 @@ pub fn create_columns(
         create_name_column(store, loading, scope.clone()),
         create_enabled_column(scope.clone()),
         create_rules_column(scope),
-        create_actions_column(ctx),
     ])
 }
 
@@ -121,33 +116,3 @@ fn create_rules_column(scope: Rc<Scope>) -> DataTableHeader<TreeEntry> {
         })
         .into()
 }
-
-fn create_actions_column(
-    ctx: &LoadableComponentContext<FirewallTreeComponent>,
-) -> DataTableHeader<TreeEntry> {
-    let link = ctx.link().clone();
-
-    DataTableColumn::new(tr!("Actions"))
-        .width("50px")
-        .justify("right")
-        .render(move |entry: &TreeEntry| {
-            if !entry.is_editable() {
-                return Html::default();
-            }
-
-            let view_state = match ViewState::from_entry(entry) {
-                Some(state) => state,
-                None => return Html::default(),
-            };
-
-            let link_clone = link.clone();
-            pwt::widget::Tooltip::new(pwt::widget::ActionIcon::new("fa fa-fw fa-cog").on_activate(
-                move |_| {
-                    link_clone.change_view(Some(view_state.clone()));
-                },
-            ))
-            .tip(tr!("Edit Options"))
-            .into()
-        })
-        .into()
-}
diff --git a/ui/src/remotes/firewall/tree.rs b/ui/src/remotes/firewall/tree.rs
index 8d0b73d..18510b5 100644
--- a/ui/src/remotes/firewall/tree.rs
+++ b/ui/src/remotes/firewall/tree.rs
@@ -3,7 +3,7 @@ use std::pin::Pin;
 use std::rc::Rc;
 use yew::{ContextHandle, Html};
 
-use proxmox_yew_comp::{EditFirewallOptions, LoadableComponent, LoadableComponentContext};
+use proxmox_yew_comp::{LoadableComponent, LoadableComponentContext};
 use pwt::css;
 use pwt::prelude::*;
 use pwt::props::{FieldBuilder, WidgetBuilder};
@@ -11,13 +11,13 @@ use pwt::state::{Selection, TreeStore};
 use pwt::tr;
 use pwt::widget::data_table::DataTable;
 use pwt::widget::form::{Combobox, Field};
-use pwt::widget::{Button, Container, Panel, Toolbar, Trigger};
+use pwt::widget::{Button, Container, Panel, TabBarItem, TabPanel, Toolbar, Trigger};
 
 use crate::RemoteList;
 
 use super::columns::create_columns;
 use super::types::{
-    FirewallError, GuestEntry, LoadState, NodeEntry, RemoteEntry, Scope, TreeEntry, ViewState,
+    FirewallError, GuestEntry, LoadState, NodeEntry, RemoteEntry, Scope, TreeEntry,
 };
 use super::ui_helpers::PanelConfig;
 
@@ -25,6 +25,11 @@ use pdm_api_types::firewall::{GuestKind, NodeFirewallStatus, RemoteFirewallStatu
 use pdm_api_types::remotes::RemoteType;
 use std::cmp::Ordering;
 
+use proxmox_yew_comp::configuration::pve::{
+    FirewallOptionsClusterPanel, FirewallOptionsGuestPanel, FirewallOptionsNodePanel,
+};
+use proxmox_yew_comp::form::pve::PveGuestType;
+
 fn create_loading_tree() -> pwt::state::SlabTree<TreeEntry> {
     let mut tree = pwt::state::SlabTree::new();
     tree.set_root(TreeEntry::Root);
@@ -206,7 +211,7 @@ impl FirewallTreeComponent {
     }
 
     fn render_tree_panel(&self, ctx: &LoadableComponentContext<Self>) -> Panel {
-        let columns = create_columns(ctx, self.store.clone(), ctx.loading(), &self.scope);
+        let columns = create_columns(self.store.clone(), ctx.loading(), &self.scope);
         let table = DataTable::new(columns, self.store.clone())
             .selection(self.selection.clone())
             .striped(false)
@@ -272,29 +277,121 @@ impl FirewallTreeComponent {
         Panel::new().border(true).with_child(column)
     }
 
-    fn render_rules_panel(&self, ctx: &LoadableComponentContext<Self>) -> Panel {
-        let mut config = match &self.selected_entry {
-            Some(entry) => PanelConfig::from_entry(entry, self.load_state.data_generation),
-            None => PanelConfig::for_no_selection(),
+    fn render_content_panel(&self, ctx: &LoadableComponentContext<Self>) -> Html {
+        let entry = match &self.selected_entry {
+            Some(entry) => entry,
+            None => return PanelConfig::for_no_selection().content.into(),
         };
 
-        if self.tree_collapsed {
+        let config = PanelConfig::from_entry(entry, self.load_state.data_generation);
+
+        let title = if self.tree_collapsed {
             let expand_button: Html = Button::new_icon("fa fa-angle-double-right")
                 .onclick(ctx.link().callback(|_| Msg::ToggleTreePanel))
                 .aria_label(tr!("Show tree panel"))
                 .into();
 
-            config.title_prefix = Some(expand_button);
-        }
+            pwt::widget::Row::new()
+                .gap(2)
+                .class(pwt::css::AlignItems::Baseline)
+                .with_child(expand_button)
+                .with_child(config.title)
+                .into()
+        } else {
+            config.title
+        };
+
+        let mut tab_panel = TabPanel::new()
+            .class(css::FlexFit)
+            .class(css::ColorScheme::Neutral)
+            .title(title)
+            .with_item_builder(
+                TabBarItem::new()
+                    .key("rules")
+                    .label(tr!("Rules"))
+                    .icon_class("fa fa-list"),
+                {
+                    let content = config.content;
+                    let key = format!(
+                        "{}-{}-{}",
+                        entry.type_name(),
+                        entry.name(),
+                        self.load_state.data_generation
+                    );
+                    move |_| {
+                        Container::new()
+                            .key(key.clone())
+                            .class(css::FlexFit)
+                            .with_child(content.clone())
+                            .into()
+                    }
+                },
+            );
+
+        let add_options_tab = |panel: TabPanel, content: Html| {
+            panel.with_item_builder(
+                TabBarItem::new()
+                    .key("options")
+                    .label(tr!("Options"))
+                    .icon_class("fa fa-cog"),
+                move |_| content.clone(),
+            )
+        };
+
+        tab_panel = match entry {
+            TreeEntry::Remote(remote) => {
+                let remote_name = remote.name.clone();
+                add_options_tab(
+                    tab_panel,
+                    FirewallOptionsClusterPanel::new()
+                        .remote(remote_name.clone())
+                        .readonly(true)
+                        .into(),
+                )
+            }
+            TreeEntry::Node(node) => {
+                let remote_name = node.remote.clone();
+                let node_name = node.name.clone();
+                add_options_tab(
+                    tab_panel,
+                    FirewallOptionsNodePanel::new(yew::AttrValue::from(node_name.clone()))
+                        .remote(remote_name.clone())
+                        .readonly(true)
+                        .into(),
+                )
+            }
+            TreeEntry::Guest(guest, kind) => {
+                let remote_name = guest.remote.clone();
+                let node_name = guest.node.clone();
+                let vmid = guest.guest.vmid;
+                let guest_type = match kind {
+                    GuestKind::Lxc => PveGuestType::Lxc,
+                    GuestKind::Qemu => PveGuestType::Qemu,
+                };
+
+                add_options_tab(
+                    tab_panel,
+                    FirewallOptionsGuestPanel::new(
+                        guest_type,
+                        yew::AttrValue::from(node_name.clone()),
+                        vmid as u32,
+                    )
+                    .remote(remote_name.clone())
+                    .readonly(true)
+                    .into(),
+                )
+            }
+            _ => tab_panel,
+        };
 
-        config.build()
+        tab_panel.into()
     }
 }
 
 impl LoadableComponent for FirewallTreeComponent {
     type Properties = super::FirewallTree;
     type Message = Msg;
-    type ViewState = ViewState;
+    type ViewState = ();
 
     fn create(ctx: &LoadableComponentContext<Self>) -> Self {
         let tree = create_loading_tree();
@@ -391,7 +488,7 @@ impl LoadableComponent for FirewallTreeComponent {
                 if self.filter_text.is_empty() {
                     self.store.set_filter(None);
                 } else {
-                    let filter_text = Rc::new(self.filter_text.clone());
+                    let filter_text = Rc::new(self.filter_text.to_lowercase());
                     self.store
                         .set_filter(move |entry: &TreeEntry| entry.matches_filter(&filter_text));
                 }
@@ -460,42 +557,7 @@ impl LoadableComponent for FirewallTreeComponent {
             container = container.with_child(self.render_tree_panel(ctx));
         }
 
-        container.with_child(self.render_rules_panel(ctx)).into()
-    }
-
-    fn dialog_view(
-        &self,
-        ctx: &LoadableComponentContext<Self>,
-        view_state: &Self::ViewState,
-    ) -> Option<Html> {
-        let dialog = match view_state {
-            ViewState::EditRemote { remote } => EditFirewallOptions::cluster(remote.to_string())
-                .on_close(ctx.link().change_view_callback(|_| None))
-                .into(),
-            ViewState::EditNode { remote, node } => {
-                EditFirewallOptions::node(remote.to_string(), node.to_string())
-                    .on_close(ctx.link().change_view_callback(|_| None))
-                    .into()
-            }
-            ViewState::EditGuest {
-                remote,
-                node,
-                vmid,
-                ty,
-            } => {
-                let vmtype: &str = ty.into();
-                EditFirewallOptions::guest(
-                    remote.to_string(),
-                    node.to_string(),
-                    *vmid as u64,
-                    vmtype,
-                )
-                .on_close(ctx.link().change_view_callback(|_| None))
-                .into()
-            }
-        };
-
-        Some(dialog)
+        container.with_child(self.render_content_panel(ctx)).into()
     }
 }
 
diff --git a/ui/src/remotes/firewall/types.rs b/ui/src/remotes/firewall/types.rs
index 84aa657..aac9587 100644
--- a/ui/src/remotes/firewall/types.rs
+++ b/ui/src/remotes/firewall/types.rs
@@ -148,17 +148,15 @@ impl TreeEntry {
     }
 
     pub fn matches_filter(&self, filter_text: &str) -> bool {
-        let text = filter_text.to_lowercase();
-
         match self {
             Self::Root | Self::Remote(..) | Self::Node(..) => true,
             Self::Guest(guest, kind) => {
                 let type_name = kind.as_str();
-                guest.guest.name.to_lowercase().contains(&text)
-                    || guest.guest.vmid.to_string().contains(&text)
-                    || type_name.contains(&text)
-                    || guest.node.to_lowercase().contains(&text)
-                    || guest.remote.to_lowercase().contains(&text)
+                guest.guest.name.to_lowercase().contains(filter_text)
+                    || guest.guest.vmid.to_string().contains(filter_text)
+                    || type_name.contains(filter_text)
+                    || guest.node.to_lowercase().contains(filter_text)
+                    || guest.remote.to_lowercase().contains(filter_text)
             }
         }
     }
@@ -173,10 +171,6 @@ impl TreeEntry {
         }
     }
 
-    pub fn is_editable(&self) -> bool {
-        !matches!(self, Self::Root)
-    }
-
     pub fn firewall_status(&self) -> Option<(&FirewallStatus, bool)> {
         match self {
             Self::Remote(entry) => entry.status.as_ref().map(|s| (s, false)),
@@ -199,6 +193,16 @@ impl TreeEntry {
             Self::Guest(_, GuestKind::Qemu) => 4,
         }
     }
+
+    pub fn type_name(&self) -> &'static str {
+        match self {
+            Self::Root => "root",
+            Self::Remote(..) => "remote",
+            Self::Node(..) => "node",
+            Self::Guest(_, GuestKind::Lxc) => "lxc",
+            Self::Guest(_, GuestKind::Qemu) => "qemu",
+        }
+    }
 }
 
 impl ExtractPrimaryKey for TreeEntry {
@@ -216,44 +220,6 @@ impl ExtractPrimaryKey for TreeEntry {
     }
 }
 
-#[derive(PartialEq, Debug, Clone)]
-pub enum ViewState {
-    EditRemote {
-        remote: String,
-    },
-    EditNode {
-        remote: String,
-        node: String,
-    },
-    EditGuest {
-        remote: String,
-        node: String,
-        vmid: u32,
-        ty: GuestKind,
-    },
-}
-
-impl ViewState {
-    pub fn from_entry(entry: &TreeEntry) -> Option<Self> {
-        match entry {
-            TreeEntry::Remote(e) => Some(Self::EditRemote {
-                remote: e.name.clone(),
-            }),
-            TreeEntry::Node(e) => Some(Self::EditNode {
-                remote: e.remote.clone(),
-                node: e.name.clone(),
-            }),
-            TreeEntry::Guest(guest, _) => Some(Self::EditGuest {
-                remote: guest.remote.clone(),
-                node: guest.node.clone(),
-                vmid: guest.guest.vmid,
-                ty: guest.guest.kind,
-            }),
-            TreeEntry::Root => None,
-        }
-    }
-}
-
 #[derive(Debug, Clone)]
 pub enum FirewallError {
     RemoteListLoadFailed(String),
diff --git a/ui/src/remotes/firewall/ui_helpers.rs b/ui/src/remotes/firewall/ui_helpers.rs
index 741064d..8d2202b 100644
--- a/ui/src/remotes/firewall/ui_helpers.rs
+++ b/ui/src/remotes/firewall/ui_helpers.rs
@@ -2,7 +2,7 @@ use pdm_api_types::firewall::{FirewallStatus, GuestKind, RuleStat};
 use pwt::css::{AlignItems, FontColor};
 use pwt::prelude::*;
 use pwt::tr;
-use pwt::widget::{Container, Fa, Panel, Row};
+use pwt::widget::{Container, Fa, Row};
 use yew::{html, Html};
 
 use super::types::TreeEntry;
@@ -56,21 +56,9 @@ pub fn create_panel_title(icon_name: &str, title_text: String) -> Html {
         .into()
 }
 
-pub fn create_rules_panel(title: Html, key: String, content: Html) -> Panel {
-    Panel::new()
-        .class(pwt::css::FlexFit)
-        .title(title)
-        .border(true)
-        .min_width(500)
-        .with_child(Container::new().key(key).with_child(content))
-        .style("flex", "1 1 0")
-}
-
 pub struct PanelConfig {
     pub title: Html,
-    pub key: String,
     pub content: Html,
-    pub title_prefix: Option<Html>,
 }
 
 impl PanelConfig {
@@ -78,10 +66,8 @@ impl PanelConfig {
         let mut rules = proxmox_yew_comp::FirewallRules::cluster(remote.to_string());
         rules.reload_token = reload_token;
         Self {
-            title: create_panel_title("list", tr!("Cluster Firewall Rules - {}", remote)),
-            key: format!("cluster-{}", remote),
+            title: create_panel_title("server", tr!("Cluster Firewall - {}", remote)),
             content: rules.into(),
-            title_prefix: None,
         }
     }
 
@@ -89,10 +75,8 @@ impl PanelConfig {
         let mut rules = proxmox_yew_comp::FirewallRules::node(remote.to_string(), node.to_string());
         rules.reload_token = reload_token;
         Self {
-            title: create_panel_title("list", tr!("Node Firewall Rules - {}/{}", remote, node)),
-            key: format!("node-{}-{}", remote, node),
+            title: create_panel_title("building", tr!("Node Firewall - {}/{}", remote, node)),
             content: rules.into(),
-            title_prefix: None,
         }
     }
 
@@ -113,18 +97,16 @@ impl PanelConfig {
         rules.reload_token = reload_token;
         Self {
             title: create_panel_title(
-                "list",
+                if vmtype == "lxc" { "cube" } else { "desktop" },
                 tr!(
-                    "Guest Firewall Rules - {}/{}/{} {}",
+                    "Guest Firewall - {}/{}/{} {}",
                     remote,
                     node,
                     vmtype.to_uppercase(),
                     vmid
                 ),
             ),
-            key: format!("guest-{}-{}-{}-{}", remote, node, vmid, vmtype),
             content: rules.into(),
-            title_prefix: None,
         }
     }
 
@@ -142,10 +124,8 @@ impl PanelConfig {
             .into();
 
         Self {
-            title: create_panel_title("list", tr!("Firewall Rules")),
-            key: String::new(),
+            title: create_panel_title("shield", tr!("Firewall")),
             content,
-            title_prefix: None,
         }
     }
 
@@ -165,18 +145,4 @@ impl PanelConfig {
             TreeEntry::Root => Self::for_no_selection(),
         }
     }
-
-    pub fn build(self) -> Panel {
-        let title = if let Some(prefix) = self.title_prefix {
-            Row::new()
-                .gap(2)
-                .class(AlignItems::Baseline)
-                .with_child(prefix)
-                .with_child(self.title)
-                .into()
-        } else {
-            self.title
-        };
-        create_rules_panel(title, self.key, self.content)
-    }
 }
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager 2/2] ui: firewall: add link to pve to rules/options panel
  2025-12-01 15:31 [pdm-devel] [PATCH proxmox-datacenter-manager 0/2] change to tabbed panel when selecting an item in the firewall tree Hannes Laimer
  2025-12-01 15:31 ` [pdm-devel] [PATCH proxmox-datacenter-manager 1/2] ui: firewall: use property view for options instead of dialog Hannes Laimer
@ 2025-12-01 15:31 ` Hannes Laimer
  2025-12-01 23:40 ` [pdm-devel] applied: [PATCH proxmox-datacenter-manager 0/2] change to tabbed panel when selecting an item in the firewall tree Thomas Lamprecht
  2 siblings, 0 replies; 4+ messages in thread
From: Hannes Laimer @ 2025-12-01 15:31 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 ui/src/remotes/firewall/tree.rs | 65 +++++++++++++++++++++++++++++++++
 1 file changed, 65 insertions(+)

diff --git a/ui/src/remotes/firewall/tree.rs b/ui/src/remotes/firewall/tree.rs
index 18510b5..ad538e0 100644
--- a/ui/src/remotes/firewall/tree.rs
+++ b/ui/src/remotes/firewall/tree.rs
@@ -1,4 +1,5 @@
 use futures::Future;
+use gloo_utils::window;
 use std::pin::Pin;
 use std::rc::Rc;
 use yew::{ContextHandle, Html};
@@ -139,6 +140,7 @@ pub enum Msg {
         nodes: Vec<String>,
     },
     SelectionChanged,
+    TabChanged,
     ToggleTreePanel,
     Error(FirewallError),
     NoOp,
@@ -147,6 +149,7 @@ pub enum Msg {
 pub struct FirewallTreeComponent {
     store: TreeStore<TreeEntry>,
     selection: Selection,
+    tab_selection: Selection,
     _context_listener: ContextHandle<RemoteList>,
     filter_text: String,
     scope: Scope,
@@ -169,6 +172,46 @@ impl FirewallTreeComponent {
         self.selected_entry = None;
     }
 
+    fn get_pve_url(&self, ctx: &LoadableComponentContext<Self>, tab: &str) -> Option<String> {
+        let entry = self.selected_entry.as_ref()?;
+        let (remote, node, vmid, kind) = match entry {
+            TreeEntry::Remote(r) => (r.name.as_str(), None, None, None),
+            TreeEntry::Node(n) => (n.remote.as_str(), Some(n.name.as_str()), None, None),
+            TreeEntry::Guest(g, kind) => (
+                g.remote.as_str(),
+                Some(g.node.as_str()),
+                Some(g.guest.vmid),
+                Some(kind),
+            ),
+            _ => return None,
+        };
+
+        let is_options = tab == "options";
+        let index = if is_options { 36 } else { 32 };
+
+        match (node, vmid, kind) {
+            (None, None, _) => {
+                let id = format!("v1:0:18:4:::::::{index}");
+                let url = crate::get_deep_url_low_level(ctx.link().yew_link(), remote, None, &id)?;
+                Some(url.href())
+            }
+            (Some(node), None, _) => {
+                let id = format!("node/{node}:4:{index}");
+                let url = crate::get_deep_url(ctx.link().yew_link(), remote, Some(node), &id)?;
+                Some(url.href())
+            }
+            (Some(node), Some(vmid), Some(kind)) => {
+                let id = match kind {
+                    GuestKind::Lxc => format!("lxc/{vmid}:4::::::{index}"),
+                    GuestKind::Qemu => format!("qemu/{vmid}:4:::::{index}"),
+                };
+                let url = crate::get_deep_url(ctx.link().yew_link(), remote, Some(node), &id)?;
+                Some(url.href())
+            }
+            _ => None,
+        }
+    }
+
     fn handle_scope_change(&mut self, ctx: &LoadableComponentContext<Self>, new_scope: Scope) {
         let remote_changed = self.scope.remote_name() != new_scope.remote_name();
 
@@ -301,10 +344,28 @@ impl FirewallTreeComponent {
             config.title
         };
 
+        let current_tab = self
+            .tab_selection
+            .selected_key()
+            .map(|k| k.to_string())
+            .unwrap_or_else(|| "rules".to_string());
+
+        let pve_url = self.get_pve_url(ctx, &current_tab);
+
         let mut tab_panel = TabPanel::new()
+            .selection(self.tab_selection.clone())
             .class(css::FlexFit)
             .class(css::ColorScheme::Neutral)
             .title(title)
+            .tool(
+                Button::new(tr!("Open Web UI"))
+                    .icon_class("fa fa-external-link")
+                    .on_activate(move |_| {
+                        if let Some(url) = &pve_url {
+                            let _ = window().open_with_url(url);
+                        }
+                    }),
+            )
             .with_item_builder(
                 TabBarItem::new()
                     .key("rules")
@@ -403,6 +464,8 @@ impl LoadableComponent for FirewallTreeComponent {
         let selection = Selection::new()
             .on_select(link.callback(|_selection: Selection| Msg::SelectionChanged));
 
+        let tab_selection = Selection::new().on_select(link.callback(|_| Msg::TabChanged));
+
         let (_, context_listener) = ctx
             .link()
             .yew_link()
@@ -413,6 +476,7 @@ impl LoadableComponent for FirewallTreeComponent {
         Self {
             store,
             selection,
+            tab_selection,
             _context_listener: context_listener,
             filter_text: String::new(),
             scope: Scope::default(),
@@ -533,6 +597,7 @@ impl LoadableComponent for FirewallTreeComponent {
                 }
                 true
             }
+            Msg::TabChanged => true,
             Msg::ToggleTreePanel => {
                 self.tree_collapsed = !self.tree_collapsed;
                 true
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [pdm-devel] applied: [PATCH proxmox-datacenter-manager 0/2] change to tabbed panel when selecting an item in the firewall tree
  2025-12-01 15:31 [pdm-devel] [PATCH proxmox-datacenter-manager 0/2] change to tabbed panel when selecting an item in the firewall tree Hannes Laimer
  2025-12-01 15:31 ` [pdm-devel] [PATCH proxmox-datacenter-manager 1/2] ui: firewall: use property view for options instead of dialog Hannes Laimer
  2025-12-01 15:31 ` [pdm-devel] [PATCH proxmox-datacenter-manager 2/2] ui: firewall: add link to pve to rules/options panel Hannes Laimer
@ 2025-12-01 23:40 ` Thomas Lamprecht
  2 siblings, 0 replies; 4+ messages in thread
From: Thomas Lamprecht @ 2025-12-01 23:40 UTC (permalink / raw)
  To: pdm-devel, Hannes Laimer

On Mon, 01 Dec 2025 16:31:07 +0100, Hannes Laimer wrote:
> This replaces the firewall option edit form with a (read-only) property view.
> Also adds direct links to pve for the cluster/node/guest, the currently
> active tab (rules or options) defines where the link goes.
> 
> Hannes Laimer (2):
>   ui: firewall: use property view for options instead of dialog
>   ui: firewall: add link to pve to rules/options panel
> 
> [...]

Applied, thanks!

[1/2] ui: firewall: use property view for options instead of dialog
      commit: 596ab0f22229fa095223583295a05cc32bfc35d0
[2/2] ui: firewall: add link to pve to rules/options panel
      commit: 586449e9b3118cff6f68737060538da6995a9032


_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2025-12-01 23:40 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-12-01 15:31 [pdm-devel] [PATCH proxmox-datacenter-manager 0/2] change to tabbed panel when selecting an item in the firewall tree Hannes Laimer
2025-12-01 15:31 ` [pdm-devel] [PATCH proxmox-datacenter-manager 1/2] ui: firewall: use property view for options instead of dialog Hannes Laimer
2025-12-01 15:31 ` [pdm-devel] [PATCH proxmox-datacenter-manager 2/2] ui: firewall: add link to pve to rules/options panel Hannes Laimer
2025-12-01 23:40 ` [pdm-devel] applied: [PATCH proxmox-datacenter-manager 0/2] change to tabbed panel when selecting an item in the firewall tree Thomas Lamprecht

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal