From: "Lukas Wagner" <l.wagner@proxmox.com>
To: "Proxmox Datacenter Manager development discussion"
<pdm-devel@lists.proxmox.com>
Subject: Re: [pdm-devel] [PATCH proxmox-yew-comp v2 4/4] firewall: add rules table
Date: Fri, 07 Nov 2025 13:26:58 +0100 [thread overview]
Message-ID: <DE2GMVOEFBJC.3M2N4R4085WA8@proxmox.com> (raw)
In-Reply-To: <20251105163546.450094-9-h.laimer@proxmox.com>
Looking great, I think this new style should work pretty well.
General note, since this is a component library, the public structs/fns
should have doc comments.
Some further notes inline.
On Wed Nov 5, 2025 at 5:35 PM CET, 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 | 253 ++++++++++++++++++++++++++++++++++++++++++
> src/lib.rs | 2 +-
> 3 files changed, 257 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..0add7db
> --- /dev/null
> +++ b/src/firewall/rules.rs
> @@ -0,0 +1,253 @@
> +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;
> +use pwt_macros::builder;
> +
> +use super::context::FirewallContext;
> +
> +#[derive(Clone, PartialEq, Properties)]
> +#[builder]
> +pub struct FirewallRules {
> + #[builder(IntoPropValue, into_prop_value)]
> + pub context: FirewallContext,
> +
> + #[builder_cb(IntoEventCallback, into_event_callback, ())]
> + #[prop_or_default]
> + pub on_close: Option<Callback<()>>,
> +}
> +
> +impl FirewallRules {
> + pub fn cluster(remote: impl Into<AttrValue>) -> Self {
> + yew::props!(Self {
> + context: FirewallContext::cluster(remote),
> + })
> + }
> +
> + pub fn node(remote: impl Into<AttrValue>, node: impl Into<AttrValue>) -> Self {
> + yew::props!(Self {
> + context: FirewallContext::node(remote, node),
> + })
> + }
> +
> + 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,
> +}
I *think* this one does not need to be public
> +
> +#[doc(hidden)]
> +pub struct ProxmoxFirewallRules {
Along with this one
> + 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());
You can inline the variable into the format! here - same for the
format!s below.
> + }
> +
> + if let Some(proto) = &rule.proto {
> + let mut proto_str = proto.to_uppercase();
> + if proto.eq("icmp") || proto.eq("icmpv6") || proto.eq("ipv6-icmp") {
It's more idiomatic to use `==` instead of .eq
> + if let Some(icmp_type) = &rule.icmp_type {
> + proto_str = format!("{}, {}", proto_str, icmp_type);
> + }
> + }
> + parts.push(pill(tr!("proto: {}", proto_str)).into());
Any reason why this one specifically should be translated?
I'd be tempted to not translate any of these abbreviations for new until
somebody complains about it, but maybe others have different opinions
here.
> + }
> +
> + match (&rule.source, &rule.sport) {
> + (Some(src), Some(sport)) => {
> + parts.push(pill(format!("src: {}, port: {}", src, sport)).into());
> + }
> + (Some(src), None) => {
> + parts.push(pill(format!("src: {}", src)).into());
> + }
> + (None, Some(sport)) => {
> + parts.push(pill(format!("src: any, port: {}", sport)).into());
I know it comes from the API type, but if possible, use something like
`source_port` or `src_port` as variable names.
> + }
> + _ => {}
> + }
> +
> + match (&rule.dest, &rule.dport) {
> + (Some(dst), Some(dport)) => {
> + parts.push(pill(format!("dest: {}:, port: {}", dst, dport)).into());
^
superfluous ':' here
> + }
> + (Some(dst), None) => {
> + parts.push(pill(format!("dest: {}", dst)).into());
> + }
> + (None, Some(dport)) => {
> + parts.push(pill(format!("dest: any, port: {}", dport)).into());
> + }
> + _ => {}
> + }
> +
> + if parts.is_empty() {
> + return "-".into();
Personally I'd just leave the row completely empty if there are no
rules, but that's just a matter of personal preference, no need to
change if you think this is better.
> + }
> +
> + 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) => html! {<i class="fa fa-check"></i>},
> + Some(0) | None => html! {<i class="fa fa fa-minus"></i>},
Any reason why you use the html! macro here and not Fa::new("check") or
Fa::new("minus")?
> + _ => html! {"-"},
> + },
> + )
> + .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();
> + 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 852d65d..d7d8c7e 100644
> --- a/src/lib.rs
> +++ b/src/lib.rs
> @@ -130,7 +130,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
next prev parent reply other threads:[~2025-11-07 12:26 UTC|newest]
Thread overview: 20+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-11-05 16:35 [pdm-devel] [PATCH proxmox{, -yew-comp, -datacenter-manager} v2 00/12] add basic integration of PVE firewall Hannes Laimer
2025-11-05 16:35 ` [pdm-devel] [PATCH proxmox v2 1/4] pve-api-types: update pve-api.json Hannes Laimer
2025-11-05 16:35 ` [pdm-devel] [PATCH proxmox v2 2/4] pve-api-types: add get/update firewall options endpoints Hannes Laimer
2025-11-05 16:35 ` [pdm-devel] [PATCH proxmox v2 3/4] pve-api-types: add list firewall rules endpoints Hannes Laimer
2025-11-05 16:35 ` [pdm-devel] [PATCH proxmox v2 4/4] pve-api-types: regenerate Hannes Laimer
2025-11-05 16:35 ` [pdm-devel] [PATCH proxmox-yew-comp v2 1/4] form: add helpers for extractig data out of schemas Hannes Laimer
2025-11-05 16:35 ` [pdm-devel] [PATCH proxmox-yew-comp v2 2/4] firewall: add FirewallContext Hannes Laimer
2025-11-05 16:35 ` [pdm-devel] [PATCH proxmox-yew-comp v2 3/4] firewall: add options edit form Hannes Laimer
2025-11-07 12:26 ` Lukas Wagner
2025-11-05 16:35 ` [pdm-devel] [PATCH proxmox-yew-comp v2 4/4] firewall: add rules table Hannes Laimer
2025-11-07 12:26 ` Lukas Wagner [this message]
2025-11-05 16:35 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 1/4] pdm-api-types: add firewall status types Hannes Laimer
2025-11-07 12:27 ` Lukas Wagner
2025-11-05 16:35 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 2/4] api: firewall: add option, rules and status endpoints Hannes Laimer
2025-11-06 16:58 ` Michael Köppl
2025-11-07 6:43 ` Hannes Laimer
2025-11-07 12:27 ` Lukas Wagner
2025-11-05 16:35 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 3/4] pdm-client: add api methods for firewall options, " Hannes Laimer
2025-11-05 16:35 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 4/4] ui: add firewall status tree Hannes Laimer
2025-11-07 12:27 ` Lukas Wagner
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=DE2GMVOEFBJC.3M2N4R4085WA8@proxmox.com \
--to=l.wagner@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