From: Hannes Laimer <h.laimer@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH proxmox-yew-comp 2/2] firewall: rules: make security group entries expandable
Date: Fri, 5 Dec 2025 16:25:43 +0100 [thread overview]
Message-ID: <20251205152543.91431-9-h.laimer@proxmox.com> (raw)
In-Reply-To: <20251205152543.91431-1-h.laimer@proxmox.com>
With this is is possible to see what rules a security group contains
within the list of normal firewall rules.
Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
src/firewall/context.rs | 12 +++
src/firewall/rules.rs | 202 ++++++++++++++++++++++++++++++++++++----
2 files changed, 194 insertions(+), 20 deletions(-)
diff --git a/src/firewall/context.rs b/src/firewall/context.rs
index 6495fa3..79aa69b 100644
--- a/src/firewall/context.rs
+++ b/src/firewall/context.rs
@@ -82,6 +82,18 @@ impl FirewallContext {
}
}
+ pub fn group_rules_url(&self, group: &str) -> String {
+ match self {
+ Self::Cluster { remote } | Self::Node { remote, .. } | Self::Guest { remote, .. } => {
+ format!(
+ "/pve/remotes/{}/firewall/groups/{}",
+ percent_encode_component(remote),
+ percent_encode_component(group)
+ )
+ }
+ }
+ }
+
pub fn options_url(&self) -> String {
match self {
Self::Cluster { remote } => {
diff --git a/src/firewall/rules.rs b/src/firewall/rules.rs
index c40fab7..234fd3c 100644
--- a/src/firewall/rules.rs
+++ b/src/firewall/rules.rs
@@ -1,12 +1,17 @@
+use std::collections::HashSet;
+use std::ops::Deref;
use std::rc::Rc;
use yew::html::{IntoEventCallback, IntoPropValue};
use yew::virtual_dom::{Key, VComp, VNode};
use pwt::prelude::*;
-use pwt::state::{Loader, LoaderState, SharedStateObserver, Store};
-use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader};
-use pwt::widget::{Container, Fa};
+use pwt::props::ExtractPrimaryKey;
+use pwt::state::{Loader, LoaderState, SharedStateObserver, SlabTree, TreeStore};
+use pwt::widget::data_table::{
+ DataTable, DataTableCellRenderArgs, DataTableCellRenderer, DataTableColumn, DataTableHeader,
+};
+use pwt::widget::{Container, Fa, Row};
use pwt_macros::builder;
use super::context::FirewallContext;
@@ -76,16 +81,45 @@ impl FirewallRules {
}
}
+#[derive(Clone, Debug, PartialEq)]
+pub struct FirewallRuleEntry {
+ pub rule: pve_api_types::FirewallRule,
+ pub parent_group: Option<String>,
+}
+
+impl Deref for FirewallRuleEntry {
+ type Target = pve_api_types::FirewallRule;
+
+ fn deref(&self) -> &Self::Target {
+ &self.rule
+ }
+}
+
+impl ExtractPrimaryKey for FirewallRuleEntry {
+ fn extract_key(&self) -> Key {
+ match &self.parent_group {
+ Some(group) => Key::from(format!("group-{group}-{}", self.pos)),
+ None => Key::from(format!("top-{}", self.pos)),
+ }
+ }
+}
+
pub enum FirewallMsg {
DataChange,
+ ToggleGroup(Key, String),
+ GroupRulesLoaded(
+ String,
+ Result<Vec<pve_api_types::FirewallRule>, anyhow::Error>,
+ ),
}
#[doc(hidden)]
pub struct ProxmoxFirewallRules {
- store: Store<pve_api_types::FirewallRule>,
+ store: TreeStore<FirewallRuleEntry>,
loader: Loader<Vec<pve_api_types::FirewallRule>>,
_listener: SharedStateObserver<LoaderState<Vec<pve_api_types::FirewallRule>>>,
- columns: Rc<Vec<DataTableHeader<pve_api_types::FirewallRule>>>,
+ columns: Rc<Vec<DataTableHeader<FirewallRuleEntry>>>,
+ loaded_groups: HashSet<String>,
}
fn pill(text: impl Into<AttrValue>) -> Container {
@@ -148,28 +182,108 @@ fn format_firewall_rule(rule: &pve_api_types::FirewallRule) -> Html {
.collect::<Html>()
}
+// Helper to create a dummy root entry since TreeStore needs a root
+impl Default for FirewallRuleEntry {
+ fn default() -> Self {
+ Self {
+ rule: pve_api_types::FirewallRule {
+ action: "".to_string(),
+ comment: None,
+ dest: None,
+ dport: None,
+ enable: None,
+ icmp_type: None,
+ iface: None,
+ ipversion: None,
+ log: None,
+ r#macro: None,
+ pos: 0,
+ proto: None,
+ source: None,
+ sport: None,
+ ty: "".to_string(),
+ },
+ parent_group: None,
+ }
+ }
+}
+
impl ProxmoxFirewallRules {
fn update_data(&mut self) {
if let Some(Ok(data)) = &self.loader.read().data {
- self.store.set_data((**data).clone());
+ let mut tree = SlabTree::new();
+ let mut root = tree.set_root(FirewallRuleEntry::default());
+
+ for rule in data.iter() {
+ let entry = FirewallRuleEntry {
+ rule: rule.clone(),
+ parent_group: None,
+ };
+ root.append(entry);
+ }
+
+ self.store.set_data(tree);
+ self.loaded_groups.clear();
}
}
- fn build_columns() -> Rc<Vec<DataTableHeader<pve_api_types::FirewallRule>>> {
+ fn build_columns(
+ on_toggle: Callback<(Key, String)>,
+ ) -> Rc<Vec<DataTableHeader<FirewallRuleEntry>>> {
Rc::new(vec![
DataTableColumn::new("")
- .width("30px")
+ .width("40px")
.justify("right")
.show_menu(false)
.resizable(false)
- .render(|rule: &pve_api_types::FirewallRule| html! {&rule.pos})
+ .render_cell(DataTableCellRenderer::new(move |args: &mut DataTableCellRenderArgs<FirewallRuleEntry>| {
+ let on_toggle = on_toggle.clone();
+ let rule = &args.record().rule;
+ let is_group = rule.ty == "group";
+ let group_name = rule.action.clone();
+ let key = args.key().clone();
+
+ let expander = if is_group {
+ let caret = if args.is_expanded() {
+ "pwt-tree-expander fa fa-fw fa-caret-down"
+ } else {
+ "pwt-tree-expander fa fa-fw fa-caret-right"
+ };
+
+ let onclick = move |event: MouseEvent| {
+ event.stop_propagation();
+ on_toggle.emit((key.clone(), group_name.clone()));
+ };
+
+ html! { <i role="none" style="cursor: pointer;" class={caret} {onclick} /> }
+ } else {
+ html! {}
+ };
+
+ let content = if !is_group {
+ html! { &rule.pos }
+ } else {
+ html! {}
+ };
+
+ let indent = Container::from_tag("span")
+ .style("flex", "0 0 auto")
+ .padding_start(4 * args.level());
+
+ Row::new()
+ .class(pwt::css::AlignItems::Baseline)
+ .with_child(indent)
+ .with_child(expander)
+ .with_child(content)
+ .into()
+ }))
.into(),
DataTableColumn::new(tr!("On"))
.width("40px")
.justify("center")
.resizable(false)
.render(
- |rule: &pve_api_types::FirewallRule| match rule.enable {
+ |rule: &FirewallRuleEntry| match rule.enable {
Some(1) => Fa::new("check").into(),
Some(0) | None => Fa::new("minus").into(),
_ => "-".into(),
@@ -178,19 +292,19 @@ impl ProxmoxFirewallRules {
.into(),
DataTableColumn::new(tr!("Type"))
.width("80px")
- .render(|rule: &pve_api_types::FirewallRule| html! {&rule.ty})
+ .render(|rule: &FirewallRuleEntry| html! {&rule.ty})
.into(),
DataTableColumn::new(tr!("Action"))
.width("100px")
- .render(|rule: &pve_api_types::FirewallRule| html! {&rule.action})
+ .render(|rule: &FirewallRuleEntry| html! {&rule.action})
.into(),
DataTableColumn::new(tr!("Rule"))
.flex(1)
- .render(|rule: &pve_api_types::FirewallRule| format_firewall_rule(rule))
+ .render(|rule: &FirewallRuleEntry| format_firewall_rule(&rule.rule))
.into(),
DataTableColumn::new(tr!("Comment"))
.width("150px")
- .render(|rule: &pve_api_types::FirewallRule| {
+ .render(|rule: &FirewallRuleEntry| {
rule.comment.as_deref().unwrap_or("-").into()
})
.into(),
@@ -207,9 +321,7 @@ impl Component for ProxmoxFirewallRules {
let url: AttrValue = props.context.rules_url().into();
- let store = Store::with_extract_key(|item: &pve_api_types::FirewallRule| {
- Key::from(item.pos.to_string())
- });
+ let store = TreeStore::new().view_root(false);
let loader = Loader::new().loader({
let url = url.clone();
@@ -224,12 +336,18 @@ impl Component for ProxmoxFirewallRules {
loader.load();
let mut me = Self {
- store,
+ store: store.clone(),
loader,
_listener,
- columns: Self::build_columns(),
+ columns: Rc::new(Vec::new()), // Initial empty columns, will be set below
+ loaded_groups: HashSet::new(),
};
+ me.columns = Self::build_columns(
+ ctx.link()
+ .callback(|(key, group)| FirewallMsg::ToggleGroup(key, group)),
+ );
+
me.update_data();
me
}
@@ -241,12 +359,56 @@ impl Component for ProxmoxFirewallRules {
true
}
- fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
+ fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
FirewallMsg::DataChange => {
self.update_data();
true
}
+ FirewallMsg::ToggleGroup(key, group_name) => {
+ if let Some(mut node) = self.store.write().lookup_node_mut(&key) {
+ let expanded = node.expanded();
+ node.set_expanded(!expanded);
+
+ if !expanded && !self.loaded_groups.contains(&group_name) {
+ let url = ctx.props().context.group_rules_url(&group_name);
+ let group_name_clone = group_name.clone();
+ ctx.link().send_future(async move {
+ let result = crate::http_get(url, None).await;
+ FirewallMsg::GroupRulesLoaded(group_name_clone, result)
+ });
+ }
+ }
+ false
+ }
+ FirewallMsg::GroupRulesLoaded(group_name, result) => {
+ if let Ok(rules) = result {
+ self.loaded_groups.insert(group_name.clone());
+
+ let mut parent_key = None;
+ {
+ for (_, item) in self.store.filtered_data() {
+ if item.record().ty == "group" && item.record().action == group_name {
+ parent_key = Some(self.store.extract_key(&item.record()));
+ break;
+ }
+ }
+ }
+
+ if let Some(key) = parent_key {
+ if let Some(mut parent_node) = self.store.write().lookup_node_mut(&key) {
+ for rule in rules {
+ let entry = FirewallRuleEntry {
+ rule,
+ parent_group: Some(group_name.clone()),
+ };
+ parent_node.append(entry);
+ }
+ }
+ }
+ }
+ true
+ }
}
}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
prev parent reply other threads:[~2025-12-05 15:26 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-12-05 15:25 [pdm-devel] [RFC proxmox{, -datacenter-manager, -yew-comp} 0/8] make security groups expandable in firewall rules list Hannes Laimer
2025-12-05 15:25 ` [pdm-devel] [PATCH proxmox 1/4] pve-api-types: rename ListFirewallRules to FirewallRule Hannes Laimer
2025-12-05 15:25 ` [pdm-devel] [PATCH proxmox 2/4] pve-api-types: update pve-api.json Hannes Laimer
2025-12-05 15:25 ` [pdm-devel] [PATCH proxmox 3/4] pve-api-types: add security group GET endpoints Hannes Laimer
2025-12-05 15:25 ` [pdm-devel] [PATCH proxmox 4/4] pve-api-types: regenerate Hannes Laimer
2025-12-05 15:25 ` [pdm-devel] [PATCH proxmox-datacenter-manager 1/2] pdm: rename ListFirewallRules to FirewallRule Hannes Laimer
2025-12-05 15:25 ` [pdm-devel] [PATCH proxmox-datacenter-manager 2/2] api: firewall: add pve firewall security group GET endpoints Hannes Laimer
2025-12-05 15:25 ` [pdm-devel] [PATCH proxmox-yew-comp 1/2] firewall: rules: rename ListFirewallRules to FirewallRule Hannes Laimer
2025-12-05 15:25 ` Hannes Laimer [this message]
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=20251205152543.91431-9-h.laimer@proxmox.com \
--to=h.laimer@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