public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Stefan Hanreich <s.hanreich@proxmox.com>
To: Proxmox Datacenter Manager development discussion
	<pdm-devel@lists.proxmox.com>,
	Hannes Laimer <h.laimer@proxmox.com>
Subject: Re: [pdm-devel] [PATCH proxmox-yew-comp v3 4/4] firewall: add rules table
Date: Wed, 12 Nov 2025 14:06:12 +0100	[thread overview]
Message-ID: <9603fdf2-29fb-42ee-aa4c-4dd2b9d0be1d@proxmox.com> (raw)
In-Reply-To: <20251110172517.335741-9-h.laimer@proxmox.com>

one comment inline

On 11/10/25 6:25 PM, Hannes Laimer wrote:
> Displays the list of firewall rules, this is read-only currently, so it
> doesn't include any buttons for editing or adding rules.
> 
> Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
> ---
>  src/firewall/mod.rs   |   3 +
>  src/firewall/rules.rs | 278 ++++++++++++++++++++++++++++++++++++++++++
>  src/lib.rs            |   2 +-
>  3 files changed, 282 insertions(+), 1 deletion(-)
>  create mode 100644 src/firewall/rules.rs
> 
> diff --git a/src/firewall/mod.rs b/src/firewall/mod.rs
> index 379b958..8cc4977 100644
> --- a/src/firewall/mod.rs
> +++ b/src/firewall/mod.rs
> @@ -4,5 +4,8 @@ pub use context::FirewallContext;
>  mod options_edit;
>  pub use options_edit::EditFirewallOptions;
>  
> +mod rules;
> +pub use rules::FirewallRules;
> +
>  mod log_ratelimit_field;
>  pub use log_ratelimit_field::LogRatelimitField;
> diff --git a/src/firewall/rules.rs b/src/firewall/rules.rs
> new file mode 100644
> index 0000000..1a62965
> --- /dev/null
> +++ b/src/firewall/rules.rs
> @@ -0,0 +1,278 @@
> +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_macros::builder;
> +
> +use super::context::FirewallContext;
> +
> +/// Properties for displaying firewall rules in a read-only table.
> +///
> +/// Displays the list of firewall rules for a given context (cluster, node, or guest).
> +/// This is read-only currently, so it doesn't include any buttons for editing or adding rules.
> +#[derive(Clone, PartialEq, Properties)]
> +#[builder]
> +pub struct FirewallRules {
> +    /// The firewall context specifying which level to display rules for (cluster, node, or guest).
> +    #[builder(IntoPropValue, into_prop_value)]
> +    pub context: FirewallContext,
> +
> +    /// Callback invoked when the component is closed.
> +    #[builder_cb(IntoEventCallback, into_event_callback, ())]
> +    #[prop_or_default]
> +    pub on_close: Option<Callback<()>>,
> +}
> +
> +impl FirewallRules {
> +    /// Creates a new `FirewallRules` for displaying cluster-level firewall rules.
> +    ///
> +    /// # Arguments
> +    ///
> +    /// * `remote` - The remote identifier for the PVE cluster.
> +    pub fn cluster(remote: impl Into<AttrValue>) -> Self {
> +        yew::props!(Self {
> +            context: FirewallContext::cluster(remote),
> +        })
> +    }
> +
> +    /// Creates a new `FirewallRules` for displaying node-level firewall rules.
> +    ///
> +    /// # Arguments
> +    ///
> +    /// * `remote` - The remote identifier for the PVE cluster.
> +    /// * `node` - The node identifier.
> +    pub fn node(remote: impl Into<AttrValue>, node: impl Into<AttrValue>) -> Self {
> +        yew::props!(Self {
> +            context: FirewallContext::node(remote, node),
> +        })
> +    }
> +
> +    /// Creates a new `FirewallRules` for displaying guest-level firewall rules.
> +    ///
> +    /// # Arguments
> +    ///
> +    /// * `remote` - The remote identifier for the PVE cluster.
> +    /// * `node` - The node identifier where the guest is located.
> +    /// * `vmid` - The virtual machine ID.
> +    /// * `vmtype` - The type of guest ("lxc" or "qemu").
> +    pub fn guest(
> +        remote: impl Into<AttrValue>,
> +        node: impl Into<AttrValue>,
> +        vmid: u64,
> +        vmtype: impl Into<AttrValue>,
> +    ) -> Self {
> +        yew::props!(Self {
> +            context: FirewallContext::guest(remote, node, vmid, vmtype),
> +        })
> +    }
> +}
> +
> +pub enum FirewallMsg {
> +    DataChange,
> +}
> +
> +#[doc(hidden)]
> +pub struct ProxmoxFirewallRules {
> +    store: Store<pve_api_types::ListFirewallRules>,
> +    loader: Loader<Vec<pve_api_types::ListFirewallRules>>,
> +    _listener: SharedStateObserver<LoaderState<Vec<pve_api_types::ListFirewallRules>>>,
> +}
> +
> +fn pill(text: impl Into<AttrValue>) -> Container {
> +    Container::from_tag("span")
> +        .style("display", "inline-block")
> +        .style("margin", "0 1px")
> +        .style("background-color", "var(--pwt-color-neutral-container)")
> +        .style("color", "var(--pwt-color-on-neutral-container)")
> +        .style("border-radius", "var(--pwt-button-corner-shape)")
> +        .style("padding-inline", "var(--pwt-spacer-2)")
> +        .with_child(text.into())
> +}
> +
> +fn format_firewall_rule(rule: &pve_api_types::ListFirewallRules) -> Html {
> +    let mut parts: Vec<VNode> = Vec::new();
> +
> +    if let Some(iface) = &rule.iface {
> +        parts.push(pill(format!("iface: {iface}")).into());
> +    }
> +
> +    if let Some(macro_name) = &rule.r#macro {
> +        parts.push(pill(format!("macro: {macro_name}")).into());
> +    }
> +
> +    if let Some(proto) = &rule.proto {
> +        let mut proto_str = proto.to_uppercase();
> +        if proto == "icmp" || proto == "icmpv6" || proto == "ipv6-icmp" {
> +            if let Some(icmp_type) = &rule.icmp_type {
> +                proto_str = format!("{proto_str}, {icmp_type}");
> +            }
> +        }
> +        parts.push(pill(format!("proto: {proto_str}")).into());
> +    }
> +
> +    match (&rule.source, &rule.sport) {
> +        (Some(src), Some(source_port)) => {
> +            parts.push(pill(format!("src: {src}, port: {source_port}")).into());
> +        }
> +        (Some(src), None) => {
> +            parts.push(pill(format!("src: {src}")).into());
> +        }
> +        (None, Some(source_port)) => {
> +            parts.push(pill(format!("src: any, port: {source_port}")).into());
> +        }
> +        _ => {}
> +    }
> +
> +    match (&rule.dest, &rule.dport) {
> +        (Some(dst), Some(dest_port)) => {
> +            parts.push(pill(format!("dest: {dst}, port: {dest_port}")).into());
> +        }
> +        (Some(dst), None) => {
> +            parts.push(pill(format!("dest: {dst}")).into());
> +        }
> +        (None, Some(dest_port)) => {
> +            parts.push(pill(format!("dest: any, port: {dest_port}")).into());
> +        }
> +        _ => {}
> +    }
> +
> +    if parts.is_empty() {
> +        return "-".into();
> +    }
> +
> +    parts
> +        .into_iter()
> +        .enumerate()
> +        .flat_map(|(i, part)| {
> +            if i == 0 {
> +                vec![part]
> +            } else {
> +                vec![" ".into(), part]
> +            }
> +        })
> +        .collect::<Html>()
> +}
> +
> +impl ProxmoxFirewallRules {
> +    fn update_data(&mut self) {
> +        if let Some(Ok(data)) = &self.loader.read().data {
> +            self.store.set_data((**data).clone());
> +        }
> +    }
> +
> +    fn columns() -> Rc<Vec<DataTableHeader<pve_api_types::ListFirewallRules>>> {
> +        Rc::new(vec![
> +            DataTableColumn::new("")
> +                .width("30px")
> +                .justify("right")
> +                .show_menu(false)
> +                .resizable(false)
> +                .render(|rule: &pve_api_types::ListFirewallRules| html! {&rule.pos})
> +                .into(),
> +            DataTableColumn::new(tr!("On"))
> +                .width("40px")
> +                .justify("center")
> +                .resizable(false)
> +                .render(
> +                    |rule: &pve_api_types::ListFirewallRules| match rule.enable {
> +                        Some(1) => Fa::new("check").into(),
> +                        Some(0) | None => Fa::new("minus").into(),
> +                        _ => "-".into(),
> +                    },
> +                )
> +                .into(),
> +            DataTableColumn::new(tr!("Type"))
> +                .width("80px")
> +                .render(|rule: &pve_api_types::ListFirewallRules| html! {&rule.ty})
> +                .into(),
> +            DataTableColumn::new(tr!("Action"))
> +                .width("100px")
> +                .render(|rule: &pve_api_types::ListFirewallRules| html! {&rule.action})
> +                .into(),
> +            DataTableColumn::new(tr!("Rule"))
> +                .flex(1)
> +                .render(|rule: &pve_api_types::ListFirewallRules| format_firewall_rule(rule))
> +                .into(),
> +            DataTableColumn::new(tr!("Comment"))
> +                .width("150px")
> +                .render(|rule: &pve_api_types::ListFirewallRules| {
> +                    rule.comment.as_deref().unwrap_or("-").into()
> +                })
> +                .into(),
> +        ])
> +    }
> +}
> +
> +impl Component for ProxmoxFirewallRules {
> +    type Message = FirewallMsg;
> +    type Properties = FirewallRules;
> +
> +    fn create(ctx: &Context<Self>) -> Self {
> +        let props = ctx.props();
> +
> +        let url: AttrValue = props.context.rules_url().into();
> +
> +        let store = Store::with_extract_key(|item: &pve_api_types::ListFirewallRules| {
> +            Key::from(item.pos.to_string())
> +        });
> +
> +        let loader = Loader::new().loader({
> +            let url = url.clone();
> +            move || {
> +                let url = url.clone();
> +                async move { crate::http_get(url.to_string(), None).await }
> +            }
> +        });
> +
> +        let _listener = loader.add_listener(ctx.link().callback(|_| FirewallMsg::DataChange));
> +
> +        loader.load();
> +
> +        let mut me = Self {
> +            store,
> +            loader,
> +            _listener,
> +        };
> +
> +        me.update_data();
> +        me
> +    }
> +
> +    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
> +        match msg {
> +            FirewallMsg::DataChange => {
> +                self.update_data();
> +                true
> +            }
> +        }
> +    }
> +
> +    fn view(&self, _ctx: &Context<Self>) -> Html {
> +        self.loader.render(|_data| -> Html {
> +            if self.store.data_len() == 0 {
> +                Container::new()
> +                    .padding(2)
> +                    .with_child(tr!("No firewall rules configured"))
> +                    .into()
> +            } else {
> +                let columns = Self::columns();

We could store the columns as a property in the component on creation
and then use the property here - to avoid re-creating them on every redraw.

> +                DataTable::new(columns, self.store.clone())
> +                    .show_header(true)
> +                    .striped(true)
> +                    .into()
> +            }
> +        })
> +    }
> +}
> +
> +impl From<FirewallRules> for VNode {
> +    fn from(val: FirewallRules) -> Self {
> +        let comp = VComp::new::<ProxmoxFirewallRules>(Rc::new(val), None);
> +        VNode::from(comp)
> +    }
> +}
> diff --git a/src/lib.rs b/src/lib.rs
> index 3d13b13..9011a22 100644
> --- a/src/lib.rs
> +++ b/src/lib.rs
> @@ -133,7 +133,7 @@ mod rrd_timeframe_selector;
>  pub use rrd_timeframe_selector::{RRDTimeframe, RRDTimeframeSelector};
>  
>  mod firewall;
> -pub use firewall::{EditFirewallOptions, FirewallContext};
> +pub use firewall::{EditFirewallOptions, FirewallContext, FirewallRules};
>  
>  mod running_tasks;
>  pub use running_tasks::{ProxmoxRunningTasks, RunningTasks};



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


  reply	other threads:[~2025-11-12 13:05 UTC|newest]

Thread overview: 21+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-11-10 17:25 [pdm-devel] [PATCH proxmox{, -yew-comp, -datacenter-manager} v3 00/12] add basic integration of PVE firewall Hannes Laimer
2025-11-10 17:25 ` [pdm-devel] [PATCH proxmox v3 1/4] pve-api-types: update pve-api.json Hannes Laimer
2025-11-10 17:25 ` [pdm-devel] [PATCH proxmox v3 2/4] pve-api-types: add get/update firewall options endpoints Hannes Laimer
2025-11-10 17:25 ` [pdm-devel] [PATCH proxmox v3 3/4] pve-api-types: add list firewall rules endpoints Hannes Laimer
2025-11-10 17:25 ` [pdm-devel] [PATCH proxmox v3 4/4] pve-api-types: regenerate Hannes Laimer
2025-11-10 17:25 ` [pdm-devel] [PATCH proxmox-yew-comp v3 1/4] form: add helpers for extractig data out of schemas Hannes Laimer
2025-11-10 17:25 ` [pdm-devel] [PATCH proxmox-yew-comp v3 2/4] firewall: add FirewallContext Hannes Laimer
2025-11-10 17:25 ` [pdm-devel] [PATCH proxmox-yew-comp v3 3/4] firewall: add options edit form Hannes Laimer
2025-11-10 17:25 ` [pdm-devel] [PATCH proxmox-yew-comp v3 4/4] firewall: add rules table Hannes Laimer
2025-11-12 13:06   ` Stefan Hanreich [this message]
2025-11-10 17:25 ` [pdm-devel] [PATCH proxmox-datacenter-manager v3 1/4] pdm-api-types: add firewall status types Hannes Laimer
2025-11-10 17:25 ` [pdm-devel] [PATCH proxmox-datacenter-manager v3 2/4] api: firewall: add option, rules and status endpoints Hannes Laimer
2025-11-12 10:52   ` Stefan Hanreich
2025-11-12 11:09     ` Hannes Laimer
2025-11-12 11:22     ` Thomas Lamprecht
2025-11-12 11:27       ` Stefan Hanreich
2025-11-10 17:25 ` [pdm-devel] [PATCH proxmox-datacenter-manager v3 3/4] pdm-client: add api methods for firewall options, " Hannes Laimer
2025-11-10 17:25 ` [pdm-devel] [PATCH proxmox-datacenter-manager v3 4/4] ui: add firewall status tree Hannes Laimer
2025-11-12 11:21   ` Stefan Hanreich
2025-11-12 14:41     ` Lukas Wagner
2025-11-12 13:07 ` [pdm-devel] [PATCH proxmox{, -yew-comp, -datacenter-manager} v3 00/12] add basic integration of PVE firewall 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=9603fdf2-29fb-42ee-aa4c-4dd2b9d0be1d@proxmox.com \
    --to=s.hanreich@proxmox.com \
    --cc=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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal