all lists on 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 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