From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 1E4561FF16B for ; Fri, 7 Nov 2025 13:26:36 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 22ECC1021C; Fri, 7 Nov 2025 13:27:18 +0100 (CET) Date: Fri, 07 Nov 2025 13:26:40 +0100 Message-Id: From: "Lukas Wagner" To: "Proxmox Datacenter Manager development discussion" Mime-Version: 1.0 X-Mailer: aerc 0.21.0-0-g5549850facc2-dirty References: <20251105163546.450094-1-h.laimer@proxmox.com> <20251105163546.450094-8-h.laimer@proxmox.com> In-Reply-To: <20251105163546.450094-8-h.laimer@proxmox.com> X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1762518381048 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.028 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_RPBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_SAFE_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: Re: [pdm-devel] [PATCH proxmox-yew-comp v2 3/4] firewall: add options edit form X-BeenThere: pdm-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Datacenter Manager development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" High level comment: I wonder whether we should 'clean up' all the snake_case parameters and make them kebab-case in the PDM API. I guess the API handler would then do some type mapping and use the actual pve_api_type for the forwarded request to PVE. Also, doc comments for any `pub` struct and function would be nice. On Wed Nov 5, 2025 at 5:35 PM CET, Hannes Laimer wrote: > This also includes the log-ratelimit field, its value is a property > string. > > Signed-off-by: Hannes Laimer > --- > src/firewall/log_ratelimit_field.rs | 318 ++++++++++++++++++++++ > src/firewall/mod.rs | 6 + > src/firewall/options_edit.rs | 404 ++++++++++++++++++++++++++++ > src/lib.rs | 2 +- > 4 files changed, 729 insertions(+), 1 deletion(-) > create mode 100644 src/firewall/log_ratelimit_field.rs > create mode 100644 src/firewall/options_edit.rs > > diff --git a/src/firewall/log_ratelimit_field.rs b/src/firewall/log_ratelimit_field.rs > new file mode 100644 > index 0000000..cafd74a > --- /dev/null > +++ b/src/firewall/log_ratelimit_field.rs > @@ -0,0 +1,318 @@ > +use anyhow::Error; > +use proxmox_schema::ApiType; > +use serde_json::Value; > +use yew::html::{IntoEventCallback, IntoPropValue}; > + > +use pwt::prelude::*; > +use pwt::props::FieldBuilder; > +use pwt::widget::form::{ > + Checkbox, Combobox, ManagedField, ManagedFieldContext, ManagedFieldMaster, ManagedFieldState, > + Number, > +}; > +use pwt::widget::{Container, InputPanel, Row}; > + > +use pwt::props::WidgetBuilder; > +use pwt_macros::{builder, widget}; > + > +use crate::SchemaValidation; > + > +const TIME_UNITS: &[&str] = &["second", "minute", "hour", "day"]; > + > +#[widget(comp=RateFieldImpl, @input)] > +#[derive(Clone, Properties, PartialEq)] > +#[builder] > +pub struct RateField { > + #[builder(IntoPropValue, into_prop_value)] > + #[prop_or_default] > + pub value: Option, > + > + #[builder_cb(IntoEventCallback, into_event_callback, String)] > + #[prop_or_default] > + pub on_input: Option>, > +} > + > +impl Default for RateField { > + fn default() -> Self { > + Self::new() > + } > +} > + > +impl RateField { > + pub fn new() -> Self { > + yew::props!(Self {}) > + } > +} > + > +pub enum RateMsg { > + ChangeNumber(Option), > + ChangeUnit(String), > +} > + > +pub struct RateFieldImpl { > + number: Option, > + unit: String, > +} ^ These two can be private > + > +impl yew::Component for RateFieldImpl { > + type Message = RateMsg; > + type Properties = RateField; > + > + fn create(ctx: &yew::Context) -> Self { > + let mut me = Self { > + number: None, > + unit: "second".to_string(), > + }; > + me.parse_value(&ctx.props().value); > + me > + } > + > + fn update(&mut self, ctx: &yew::Context, msg: Self::Message) -> bool { > + match msg { > + RateMsg::ChangeNumber(number) => { > + self.number = number; > + } > + RateMsg::ChangeUnit(unit) => self.unit = unit, > + } > + > + let new_value_str = if let Some(num) = self.number { > + format!("{}/{}", num, self.unit) > + } else { > + String::new() > + }; > + > + if let Some(callback) = &ctx.props().on_input { > + callback.emit(new_value_str); > + } > + > + true > + } > + > + fn changed(&mut self, ctx: &yew::Context, _old_props: &Self::Properties) -> bool { > + self.parse_value(&ctx.props().value); > + true > + } > + > + fn view(&self, ctx: &yew::Context) -> Html { > + let is_empty = self.number.is_none(); > + let number_value = self.number.map(|n| n.to_string()).unwrap_or_default(); > + > + let units: Vec = TIME_UNITS.iter().map(|&u| AttrValue::from(u)).collect(); > + > + Row::new() > + .style("align-items", "center") > + .gap(1) > + .with_child( > + Number::::new() > + .key("rate_number") > + .value(number_value) > + .placeholder("1") > + .min(1) > + .on_change(ctx.link().callback(|result: Option>| { > + RateMsg::ChangeNumber(result.and_then(|r| r.ok())) > + })), > + ) > + .with_child("/") > + .with_child( > + Combobox::new() > + .key("rate_unit") > + .items(std::rc::Rc::new(units)) > + .value(self.unit.clone()) > + .disabled(is_empty) > + .required(true) > + .on_change(ctx.link().callback(RateMsg::ChangeUnit)), > + ) > + .into() > + } > +} > + > +impl RateFieldImpl { > + fn parse_value(&mut self, value: &Option) { > + self.number = None; > + self.unit = "second".to_string(); > + > + if let Some(v) = value { > + if !v.is_empty() { > + if let Some((num, unit)) = v.split_once('/') { > + self.number = num.parse::().ok(); > + self.unit = unit.to_string(); > + } > + } > + } > + } > +} > + > +#[widget(comp=ManagedFieldMaster, @input)] > +#[derive(Clone, Properties, PartialEq)] > +#[builder] > +pub struct LogRatelimitField { > + #[builder(IntoPropValue, into_prop_value)] > + #[prop_or_default] > + pub default: Option, > +} > + > +impl Default for LogRatelimitField { > + fn default() -> Self { > + Self::new() > + } > +} > + > +impl LogRatelimitField { > + pub fn new() -> Self { > + yew::props!(Self {}) > + } > +} > + > +pub enum LogRatelimitMsg { > + Enable(bool), > + Rate(String), > + Burst(Option), > +} > + > +pub struct LogRatelimitFieldImpl { > + enable: bool, > + rate: String, > + burst: Option, > +} These two as well > + > +impl ManagedField for LogRatelimitFieldImpl { > + type Message = LogRatelimitMsg; > + type Properties = LogRatelimitField; > + type ValidateClosure = (); > + > + fn validation_args(_props: &Self::Properties) -> Self::ValidateClosure {} > + > + fn validator(_props: &Self::ValidateClosure, value: &Value) -> Result { > + Ok(value.clone()) > + } > + > + fn setup(props: &Self::Properties) -> ManagedFieldState { > + let value = Value::Null; > + let default = match &props.default { > + Some(d) => Value::String(d.to_string()), > + None => Value::String(String::new()), > + }; > + ManagedFieldState::new(value, default) > + } > + > + fn value_changed(&mut self, ctx: &ManagedFieldContext) { > + let state = ctx.state(); > + > + // Initialize with API defaults (enable=true is the API default) > + // When the property string is empty, rate and burst are unset > + self.enable = true; > + self.rate = String::new(); > + self.burst = None; > + > + // If value is Null, use the default instead > + let value_to_parse = match &state.value { > + Value::Null => &state.default, > + other => other, > + }; > + > + if let Value::String(v) = value_to_parse { > + if !v.is_empty() { > + match pve_api_types::ClusterFirewallOptionsLogRatelimit::API_SCHEMA > + .parse_property_string(v) > + { > + Ok(parsed) => { > + match serde_json::from_value::< > + pve_api_types::ClusterFirewallOptionsLogRatelimit, > + >(parsed) > + { > + Ok(ratelimit) => { > + self.enable = ratelimit.enable; > + if let Some(rate) = ratelimit.rate { > + self.rate = rate.clone(); > + } > + self.burst = ratelimit.burst; > + } > + Err(e) => { > + log::error!("Failed to parse log_ratelimit value: {:?}", e); You can inline this variable > + } > + } > + } > + Err(e) => { > + log::error!( > + "Failed to parse log_ratelimit property string '{}': {:?}", > + v, > + e Same here. > + ); > + } > + } > + } > + } > + } > + > + fn create(ctx: &ManagedFieldContext) -> Self { > + let mut me = Self { > + enable: true, > + rate: String::new(), > + burst: None, > + }; > + me.value_changed(ctx); > + me > + } > + > + fn update(&mut self, ctx: &ManagedFieldContext, msg: Self::Message) -> bool { > + match msg { > + LogRatelimitMsg::Enable(enable) => self.enable = enable, > + LogRatelimitMsg::Rate(rate) => self.rate = rate, > + LogRatelimitMsg::Burst(burst_value) => self.burst = burst_value, > + } > + > + let mut parts = Vec::new(); > + parts.push(format!("enable={}", if self.enable { 1 } else { 0 })); > + if !self.rate.is_empty() { > + parts.push(format!("rate={}", self.rate)); > + } > + if let Some(burst) = self.burst { > + parts.push(format!("burst={}", burst)); > + } > + let property_string = parts.join(","); > + let new_value = Value::String(property_string); > + > + ctx.link().update_value(new_value); > + true > + } > + > + fn view(&self, ctx: &ManagedFieldContext) -> Html { > + let props = ctx.props(); > + let base_schema = &pve_api_types::ClusterFirewallOptionsLogRatelimit::API_SCHEMA; > + > + Container::new() > + .style("border", "1px solid var(--pwt-border-color, #ccc)") > + .style("border-radius", "4px") > + .padding(2) > + .with_child( > + InputPanel::new() > + .with_std_props(&props.std_props) > + .with_field( > + tr!("Enable"), > + Checkbox::new() > + .key("enable") > + .checked(self.enable) > + .on_change(ctx.link().callback(LogRatelimitMsg::Enable)), > + ) > + .with_field( > + tr!("Rate"), > + RateField::new() > + .key("rate") > + .value(self.rate.clone()) > + .on_input(ctx.link().callback(LogRatelimitMsg::Rate)), > + ) > + .with_field( > + tr!("Burst"), > + Number::::new() > + .key("burst") > + .value(self.burst.map(|b| b.to_string())) > + .on_change(ctx.link().callback( > + |result: Option>| { > + LogRatelimitMsg::Burst(result.and_then(|r| r.ok())) > + }, > + )) > + .schema(crate::form::get_field_schema(base_schema, vec!["burst"])), > + ), > + ) > + .into() > + } > +} > diff --git a/src/firewall/mod.rs b/src/firewall/mod.rs > index 49dcf23..379b958 100644 > --- a/src/firewall/mod.rs > +++ b/src/firewall/mod.rs > @@ -1,2 +1,8 @@ > mod context; > pub use context::FirewallContext; > + > +mod options_edit; > +pub use options_edit::EditFirewallOptions; > + > +mod log_ratelimit_field; > +pub use log_ratelimit_field::LogRatelimitField; > diff --git a/src/firewall/options_edit.rs b/src/firewall/options_edit.rs > new file mode 100644 > index 0000000..6b0c4e2 > --- /dev/null > +++ b/src/firewall/options_edit.rs > @@ -0,0 +1,404 @@ > +use std::rc::Rc; > + > +use anyhow::Error; > +use proxmox_schema::ApiType; > +use pve_api_types::{ClusterFirewallOptions, GuestFirewallOptions, NodeFirewallOptions}; > +use serde_json::Value; > +use yew::html::{IntoEventCallback, IntoPropValue}; > +use yew::virtual_dom::{VComp, VNode}; > + > +use pwt::prelude::*; > +use pwt::widget::form::{Checkbox, Combobox, FormContext, Number}; > +use pwt::widget::InputPanel; > + > +use pwt_macros::builder; > + > +use crate::{form::delete_empty_values, ApiLoadCallback, EditWindow}; > + > +use super::{context::FirewallContext, LogRatelimitField}; > + > +fn enum_items_from_schema(name: &str) -> Vec { > + let s = crate::form::get_field_schema(&T::API_SCHEMA, vec![name]); > + crate::form::enum_items_from_schema(s) > +} > + > +fn placeholder_from_schema(name: &str) -> String { > + let s = crate::form::get_field_schema(&T::API_SCHEMA, vec![name]); > + crate::form::placeholder_from_schema(s) > +} > + > +fn create_firewall_options_loader(url: AttrValue, transform_fn: F) -> ApiLoadCallback > +where > + F: Fn(&mut serde_json::Map) + Clone + 'static, > +{ > + ApiLoadCallback::new(move || { > + let url = url.clone(); > + let transform_fn = transform_fn.clone(); > + async move { > + let mut resp = crate::http_get_full(url.to_string(), None).await?; > + if let serde_json::Value::Object(ref mut map) = resp.data { > + transform_fn(map); > + } > + Ok::<_, anyhow::Error>(resp) > + } > + }) > +} > + > +async fn update_firewall_options( > + form_ctx: FormContext, > + url: AttrValue, > + fields: &[&str], > + transform_fn: Option)>, > +) -> Result<(), Error> { > + let mut data = form_ctx.get_submit_data(); > + > + if let (Some(transform), serde_json::Value::Object(ref mut map)) = (transform_fn, &mut data) { > + transform(map); > + } > + > + let data = delete_empty_values(&data, fields, true); > + > + crate::http_put(&url.to_string(), Some(data)).await > +} > + > +#[derive(Clone, PartialEq, Properties)] > +#[builder] > +pub struct EditFirewallOptions { > + #[builder(IntoPropValue, into_prop_value)] > + pub context: FirewallContext, > + > + #[builder_cb(IntoEventCallback, into_event_callback, ())] > + #[prop_or_default] > + pub on_close: Option>, > +} > + > +impl EditFirewallOptions { > + pub fn cluster(remote: impl Into) -> Self { > + yew::props!(Self { > + context: FirewallContext::cluster(remote), > + }) > + } > + > + pub fn node(remote: impl Into, node: impl Into) -> Self { > + yew::props!(Self { > + context: FirewallContext::node(remote, node), > + }) > + } > + > + pub fn guest( > + remote: impl Into, > + node: impl Into, > + vmid: u64, > + vmtype: impl Into, > + ) -> Self { > + yew::props!(Self { > + context: FirewallContext::guest(remote, node, vmid, vmtype), > + }) > + } > +} > + > +pub struct ProxmoxEditFirewallOptions { > + loader: Option>, > +} > + > +impl Component for ProxmoxEditFirewallOptions { > + type Message = (); > + type Properties = EditFirewallOptions; > + > + fn create(ctx: &Context) -> Self { > + let props = ctx.props(); > + let url: AttrValue = props.context.options_url().into(); > + > + let loader = if !url.is_empty() { > + Some(create_firewall_options_loader(url, |map| { > + // Convert enable field from u64 to bool for cluster firewall > + if let Some(enable_num) = map.get("enable").and_then(|v| v.as_u64()) { > + map.insert("enable".into(), serde_json::Value::Bool(enable_num != 0)); > + } > + })) > + } else { > + None > + }; > + > + Self { loader } > + } > + > + fn view(&self, ctx: &Context) -> Html { > + let props = ctx.props(); > + let url: AttrValue = props.context.options_url().into(); > + > + type ContextConfig<'a> = ( > + String, > + fn(&FormContext) -> Html, > + &'a [&'a str], > + Option)>, > + ); > + let (title, renderer, fields, transform_fn): ContextConfig<'_> = match &props.context { > + FirewallContext::Cluster { .. } => ( > + props.context.title(&tr!("Edit Cluster Firewall")), > + edit_cluster_firewall_input_panel, > + &[ > + "enable", > + "ebtables", > + "policy_in", > + "policy_out", > + "policy_forward", > + "log_ratelimit", > + ], > + Some(|map: &mut serde_json::Map| { > + if let Some(enable) = map.get("enable").and_then(|v| v.as_bool()) { > + map.insert("enable".into(), Value::from(if enable { 1 } else { 0 })); > + } > + }), > + ), > + FirewallContext::Node { .. } => ( > + props.context.title(&tr!("Edit Node Firewall")), > + edit_node_firewall_input_panel, > + &[ > + "enable", > + "ndp", > + "log_nf_conntrack", > + "nf_conntrack_allow_invalid", > + "log_level_in", > + "log_level_out", > + "log_level_forward", > + "nf_conntrack_max", > + "nf_conntrack_tcp_timeout_established", > + "nf_conntrack_tcp_timeout_syn_recv", > + ], > + None, > + ), > + FirewallContext::Guest { .. } => ( > + props.context.title(&tr!("Edit Guest Firewall")), > + edit_guest_firewall_input_panel, > + &[ > + "enable", > + "dhcp", > + "ipfilter", > + "macfilter", > + "ndp", > + "radv", > + "policy_in", > + "policy_out", > + "log_level_in", > + "log_level_out", > + ], > + None, > + ), > + }; > + > + EditWindow::new(title) > + .loader(self.loader.clone()) > + .on_close(props.on_close.clone()) > + .on_done(props.on_close.clone()) > + .renderer(renderer) > + .on_submit({ > + let url = url.clone(); > + move |form_ctx: FormContext| { > + let url = url.clone(); > + let fields = fields.to_vec(); > + async move { update_firewall_options(form_ctx, url, &fields, transform_fn).await } > + } > + }) > + .into() > + } > +} > + > +fn edit_cluster_firewall_input_panel(_form_ctx: &FormContext) -> Html { > + InputPanel::new() > + .padding(4) > + .with_large_field( > + tr!("Enable Firewall"), > + Checkbox::new().name("enable").key("enable"), > + ) > + .with_large_field( > + tr!("Enable ebtables"), > + Checkbox::new().name("ebtables").key("ebtables"), > + ) > + .with_field( > + tr!("Input Policy"), > + Combobox::new() > + .name("policy_in") > + .key("policy_in") > + .placeholder(placeholder_from_schema::( > + "policy_in", > + )) > + .items(enum_items_from_schema::("policy_in").into()), > + ) > + .with_field( > + tr!("Output Policy"), > + Combobox::new() > + .name("policy_out") > + .key("policy_out") > + .placeholder(placeholder_from_schema::( > + "policy_out", > + )) > + .items(enum_items_from_schema::("policy_out").into()), > + ) > + .with_field( > + tr!("Forward Policy"), > + Combobox::new() > + .name("policy_forward") > + .key("policy_forward") > + .placeholder(placeholder_from_schema::( > + "policy_forward", > + )) > + .items(enum_items_from_schema::("policy_forward").into()), > + ) > + .with_large_field( > + tr!("Log Rate Limiting"), > + LogRatelimitField::new() > + .name("log_ratelimit") > + .key("log_ratelimit"), > + ) > + .into() > +} > + > +impl From for VNode { > + fn from(val: EditFirewallOptions) -> Self { > + let comp = VComp::new::(Rc::new(val), None); > + VNode::from(comp) > + } > +} > + > +fn edit_guest_firewall_input_panel(_form_ctx: &FormContext) -> Html { > + InputPanel::new() > + .padding(4) > + .with_field( > + tr!("Enable Firewall"), > + Checkbox::new().name("enable").key("enable"), > + ) > + .with_right_field(tr!("Enable DHCP"), Checkbox::new().name("dhcp").key("dhcp")) > + .with_field( > + tr!("Enable IP Filter"), > + Checkbox::new().name("ipfilter").key("ipfilter"), > + ) > + .with_right_field( > + tr!("Enable MAC Filter"), > + Checkbox::new().name("macfilter").key("macfilter"), > + ) > + .with_field(tr!("Enable NDP"), Checkbox::new().name("ndp").key("ndp")) > + .with_right_field( > + tr!("Allow Router Advertisement"), > + Checkbox::new().name("radv").key("radv"), > + ) > + .with_field( > + tr!("Input Policy"), > + Combobox::new() > + .name("policy_in") > + .key("policy_in") > + .placeholder(placeholder_from_schema::("policy_in")) > + .items(enum_items_from_schema::("policy_in").into()), > + ) > + .with_right_field( > + tr!("Output Policy"), > + Combobox::new() > + .name("policy_out") > + .key("policy_out") > + .placeholder(placeholder_from_schema::( > + "policy_out", > + )) > + .items(enum_items_from_schema::("policy_out").into()), > + ) > + .with_field( > + tr!("Log Level In"), > + Combobox::new() > + .name("log_level_in") > + .key("log_level_in") > + .placeholder(placeholder_from_schema::( > + "log_level_in", > + )) > + .items(enum_items_from_schema::("log_level_in").into()), > + ) > + .with_right_field( > + tr!("Log Level Out"), > + Combobox::new() > + .name("log_level_out") > + .key("log_level_out") > + .placeholder(placeholder_from_schema::( > + "log_level_out", > + )) > + .items(enum_items_from_schema::("log_level_out").into()), > + ) > + .into() > +} > + > +fn edit_node_firewall_input_panel(_form_ctx: &FormContext) -> Html { > + InputPanel::new() > + .padding(4) > + .with_field( > + tr!("Enable Firewall"), > + Checkbox::new().name("enable").key("enable"), > + ) > + .with_right_field(tr!("Enable NDP"), Checkbox::new().name("ndp").key("ndp")) > + .with_field( > + tr!("Log Connection Tracking"), > + Checkbox::new() > + .name("log_nf_conntrack") > + .key("log_nf_conntrack"), > + ) > + .with_right_field( > + tr!("Allow Invalid Connections"), > + Checkbox::new() > + .name("nf_conntrack_allow_invalid") > + .key("nf_conntrack_allow_invalid"), > + ) > + .with_field( > + tr!("Log Level In"), > + Combobox::new() > + .name("log_level_in") > + .key("log_level_in") > + .placeholder(placeholder_from_schema::( > + "log_level_in", > + )) > + .items(enum_items_from_schema::("log_level_in").into()), > + ) > + .with_right_field( > + tr!("Log Level Out"), > + Combobox::new() > + .name("log_level_out") > + .key("log_level_out") > + .placeholder(placeholder_from_schema::( > + "log_level_out", > + )) > + .items(enum_items_from_schema::("log_level_out").into()), > + ) > + .with_field( > + tr!("Log Level Forward"), > + Combobox::new() > + .name("log_level_forward") > + .key("log_level_forward") > + .placeholder(placeholder_from_schema::( > + "log_level_forward", > + )) > + .items(enum_items_from_schema::("log_level_forward").into()), > + ) > + .with_right_field( > + tr!("Connection Tracking Max"), > + Number::::new() > + .name("nf_conntrack_max") > + .key("nf_conntrack_max") > + .placeholder(placeholder_from_schema::( > + "nf_conntrack_max", > + )), > + ) > + .with_field( > + tr!("TCP Timeout Established"), > + Number::::new() > + .name("nf_conntrack_tcp_timeout_established") > + .key("nf_conntrack_tcp_timeout_established") > + .placeholder(placeholder_from_schema::( > + "nf_conntrack_tcp_timeout_established", > + )), > + ) > + .with_right_field( > + tr!("TCP Timeout SYN Recv"), > + Number::::new() > + .name("nf_conntrack_tcp_timeout_syn_recv") > + .key("nf_conntrack_tcp_timeout_syn_recv") > + .placeholder(placeholder_from_schema::( > + "nf_conntrack_tcp_timeout_syn_recv", > + )), > + ) > + .into() > +} > diff --git a/src/lib.rs b/src/lib.rs > index e2e2721..852d65d 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::FirewallContext; > +pub use firewall::{EditFirewallOptions, FirewallContext}; > > 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