all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Christoph Heiss <c.heiss@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH installer 14/15] tui: network: factor out name pinning into advanced options dialog
Date: Tue,  8 Sep 2026 12:16:28 +0200	[thread overview]
Message-ID: <20260908101647.1057780-15-c.heiss@proxmox.com> (raw)
In-Reply-To: <20260908101647.1057780-1-c.heiss@proxmox.com>

This dialog can now be easily extended with additional panels for other,
advanced network-related options.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 proxmox-tui-installer/src/views/network.rs | 272 +++++++++++----------
 1 file changed, 138 insertions(+), 134 deletions(-)

diff --git a/proxmox-tui-installer/src/views/network.rs b/proxmox-tui-installer/src/views/network.rs
index 6401174..f040881 100644
--- a/proxmox-tui-installer/src/views/network.rs
+++ b/proxmox-tui-installer/src/views/network.rs
@@ -1,8 +1,9 @@
+use anyhow::{Result, anyhow, bail};
 use cursive::{
     Cursive, View,
     view::{Nameable, Resizable, ViewWrapper},
     views::{
-        Button, Checkbox, Dialog, DummyView, EditView, LinearLayout, NamedView, ResizedView,
+        Button, Checkbox, Dialog, DummyView, EditView, LinearLayout, NamedView, Panel, ResizedView,
         ScrollView, SelectView, TextView,
     },
 };
@@ -22,10 +23,10 @@ use proxmox_network_types::{MacAddress, fqdn::Fqdn, ip_address::Cidr};
 use super::{CidrAddressEditView, FormView};
 
 struct NetworkViewOptions {
-    selected_mac: MacAddress,
     pinning_enabled: bool,
-    // For UI purposes, we want to always save the mapping, to save the state
-    // between toggling the checkbox
+    interfaces: Vec<Interface>,
+    // For UI purposes, we want to always save the state for the below options, even if not enabled
+    // by the user - to preserve the state between toggling the checkbox
     pinning_options: NetworkInterfacePinningOptions,
 }
 
@@ -40,32 +41,29 @@ pub struct NetworkOptionsView {
 }
 
 impl NetworkOptionsView {
-    const PINNING_OPTIONS_BUTTON_NAME: &str = "network-pinning-options-button";
+    const FORM_VIEW_NAME: &str = "network-management-form";
     const MGMT_IFNAME_SELECTVIEW_NAME: &str = "network-management-ifname-selectview";
+    const ADVANCED_OPTIONS_BUTTON_NAME: &str = "network-pinning-options-button";
+
+    const INTERFACE_PINNING_VIEW_NAME: &str = "network-name-pinning-view";
 
     pub fn new(options: &NetworkOptions, network_info: &NetworkInfo) -> Self {
-        let mut ifaces = network_info
+        let mut interfaces = network_info
             .interfaces
             .values()
-            .collect::<Vec<&Interface>>();
+            .cloned()
+            .collect::<Vec<Interface>>();
 
         // First, sort interfaces by their link state and then name
-        ifaces.sort_unstable_by_key(|x| (&x.state, &x.name));
-
-        let selected_mac = network_info
-            .interfaces
-            .get(&options.ifname)
-            .map(|iface| iface.mac)
-            .unwrap_or_else(|| ifaces.first().expect("at least one network interface").mac);
+        interfaces.sort_unstable_by(|a, b| (&a.state, &a.name).cmp(&(&b.state, &b.name)));
 
         let options_ref = Arc::new(Mutex::new(NetworkViewOptions {
-            selected_mac,
             pinning_enabled: options.pinning_opts.is_some(),
+            interfaces,
             pinning_options: options.pinning_opts.clone().unwrap_or_default(),
         }));
 
-        let iface_selection =
-            Self::build_mgmt_ifname_selectview(ifaces.clone(), options_ref.clone());
+        let iface_selection = Self::build_mgmt_ifname_selectview(options_ref.clone(), None);
 
         let form = FormView::<()>::new()
             .child(
@@ -89,36 +87,13 @@ impl NetworkOptionsView {
                 EditView::new().content(options.dns_server.to_string()),
             );
 
-        let pinning_checkbox = LinearLayout::horizontal()
-            .child(Checkbox::new().checked().on_change({
-                let ifaces = ifaces
-                    .iter()
-                    .map(|iface| (*iface).clone())
-                    .collect::<Vec<Interface>>();
-                let options_ref = options_ref.clone();
-                move |siv, enable_pinning| {
-                    siv.call_on_name(Self::PINNING_OPTIONS_BUTTON_NAME, {
-                        let options_ref = options_ref.clone();
-                        move |view: &mut Button| {
-                            view.set_enabled(enable_pinning);
-
-                            options_ref.lock().expect("unpoisoned lock").pinning_enabled =
-                                enable_pinning;
-                        }
-                    });
-
-                    Self::refresh_ifname_selectview(siv, &ifaces, options_ref.clone());
-                }
-            }))
-            .child(TextView::new(" Pin network interface names").no_wrap())
+        let advanced_options = LinearLayout::horizontal()
             .child(DummyView.full_width())
             .child(
-                Button::new("Pinning options", {
+                Button::new("Advanced options", {
                     let options_ref = options_ref.clone();
-                    let network_info = network_info.clone();
                     move |siv| {
-                        let mut view =
-                            Self::custom_name_mapping_view(&network_info, options_ref.clone());
+                        let mut view = Self::advanced_options_view(options_ref.clone());
 
                         // Pre-compute the child's layout, since it might depend on the size. Without this,
                         // the view will be empty until focused.
@@ -128,13 +103,13 @@ impl NetworkOptionsView {
                         siv.add_layer(view);
                     }
                 })
-                .with_name(Self::PINNING_OPTIONS_BUTTON_NAME),
+                .with_name(Self::ADVANCED_OPTIONS_BUTTON_NAME),
             );
 
         let view = LinearLayout::vertical()
-            .child(form)
+            .child(form.with_name(Self::FORM_VIEW_NAME))
             .child(DummyView.full_width())
-            .child(pinning_checkbox);
+            .child(advanced_options);
 
         Self {
             view,
@@ -145,13 +120,10 @@ impl NetworkOptionsView {
     pub fn get_values(&mut self) -> Result<NetworkOptions, String> {
         let form = self
             .view
-            .get_child(0)
-            .and_then(|v| v.downcast_ref::<FormView>())
-            .ok_or("failed to retrieve network options form")?;
-
-        let iface = form
-            .get_value::<NamedView<SelectView<Interface>>, _>(0)
-            .ok_or("failed to retrieve management interface name")?;
+            .get_child_mut(0)
+            .and_then(|v| v.downcast_mut::<NamedView<FormView>>())
+            .ok_or("failed to retrieve network options form")?
+            .get_mut();
 
         let fqdn = form
             .get_value::<EditView, _>(1)
@@ -208,83 +180,69 @@ impl NetworkOptionsView {
         }
     }
 
-    fn custom_name_mapping_view(
-        network_info: &NetworkInfo,
-        options_ref: NetworkViewOptionsRef,
-    ) -> impl View {
-        const DIALOG_NAME: &str = "network-interface-name-pinning-dialog";
+    fn advanced_options_view(options_ref: NetworkViewOptionsRef) -> impl View {
+        let inner = ScrollView::new(
+            LinearLayout::vertical().child(Panel::new(
+                InterfacePinningOptionsView::new(options_ref.clone())
+                    .with_name(Self::INTERFACE_PINNING_VIEW_NAME),
+            )),
+        );
 
-        let mut interfaces = network_info
-            .interfaces
-            .values()
-            .collect::<Vec<&Interface>>();
-
-        interfaces.sort_by(|a, b| (&a.state, &a.name).cmp(&(&b.state, &b.name)));
-
-        Dialog::around(InterfacePinningOptionsView::new(
-            &interfaces,
-            options_ref.clone(),
-        ))
-        .title("Interface Name Pinning Options")
-        .button("Ok", {
-            let interfaces = interfaces
-                .iter()
-                .map(|v| (*v).clone())
-                .collect::<Vec<Interface>>();
-            move |siv| {
-                let options = siv
-                    .call_on_name(DIALOG_NAME, |view: &mut Dialog| {
-                        view.get_content_mut()
-                            .downcast_mut::<InterfacePinningOptionsView>()
-                            .map(InterfacePinningOptionsView::get_values)
-                    })
-                    .flatten();
-
-                let options = match options {
-                    Some(Ok(options)) => options,
-                    Some(Err(err)) => {
-                        siv.add_layer(Dialog::info(err));
-                        return;
+        Dialog::around(inner)
+            .title("Advanced Network Options")
+            .button("Ok", {
+                move |siv| {
+                    match Self::get_view_values(siv) {
+                        Ok(pin_opts) => {
+                            siv.pop_layer();
+                            let options = &mut options_ref.lock().expect("unpoisoned lock");
+                            options.pinning_options = pin_opts;
+                        }
+                        Err(err) => {
+                            siv.pop_layer();
+                            siv.add_layer(Dialog::info(err.to_string()));
+                            return;
+                        }
                     }
-                    None => {
-                        siv.add_layer(Dialog::info(
-                            "Failed to retrieve network interface name pinning options view",
-                        ));
-                        return;
-                    }
-                };
 
-                siv.pop_layer();
-                options_ref.lock().expect("unpoisoned lock").pinning_options = options;
-
-                Self::refresh_ifname_selectview(siv, &interfaces, options_ref.clone());
-            }
-        })
-        .with_name(DIALOG_NAME)
-        .max_size((80, 40))
+                    Self::refresh_ifname_selectview(siv, options_ref.clone());
+                }
+            })
+            .max_size((80, 40))
     }
 
-    fn refresh_ifname_selectview(
-        siv: &mut Cursive,
-        ifaces: &[Interface],
-        options_ref: NetworkViewOptionsRef,
-    ) {
+    fn get_view_values(siv: &mut Cursive) -> Result<NetworkInterfacePinningOptions> {
+        let pin_opts = siv
+            .call_on_name(
+                Self::INTERFACE_PINNING_VIEW_NAME,
+                InterfacePinningOptionsView::get_values,
+            )
+            .unwrap_or_else(|| {
+                bail!("failed to retrieve network interface name pinning options")
+            })?;
+
+        Ok(pin_opts)
+    }
+
+    fn refresh_ifname_selectview(siv: &mut Cursive, options_ref: NetworkViewOptionsRef) {
         siv.call_on_name(
             Self::MGMT_IFNAME_SELECTVIEW_NAME,
             |view: &mut SelectView<Interface>| {
-                *view = Self::build_mgmt_ifname_selectview(ifaces.iter().collect(), options_ref);
+                let selected_mac = view.selection().map(|iface| iface.mac);
+                *view = Self::build_mgmt_ifname_selectview(options_ref, selected_mac);
             },
         );
     }
 
     fn build_mgmt_ifname_selectview(
-        ifaces: Vec<&Interface>,
         options_ref: NetworkViewOptionsRef,
+        selected_mac: Option<MacAddress>,
     ) -> SelectView<Interface> {
         let options = options_ref.lock().expect("unpoisoned lock");
 
         // Map all interfaces to a list of (human-readable interface name, [Interface]) pairs
-        let ifnames = ifaces
+        let ifnames = options
+            .interfaces
             .iter()
             .map(|iface| {
                 if options.pinning_enabled
@@ -297,20 +255,20 @@ impl NetworkOptionsView {
             })
             .collect::<Vec<(String, Interface)>>();
 
-        let mut view = SelectView::new()
-            .popup()
-            .with_all(ifnames.clone())
-            .on_submit({
-                let options_ref = options_ref.clone();
-                move |_, iface| {
-                    options_ref.lock().expect("unpoisoned lock").selected_mac = iface.mac;
-                }
-            });
+        let mut view = SelectView::new().popup().with_all(ifnames.clone());
+
+        let selected_mac = selected_mac.unwrap_or_else(|| {
+            options
+                .interfaces
+                .first()
+                .expect("at least one network interface")
+                .mac
+        });
 
         // Finally, (try to) select the current one
         let selected = view
             .iter()
-            .position(|(_label, iface)| iface.mac == options.selected_mac)
+            .position(|(_label, iface)| iface.mac == selected_mac)
             .unwrap_or(0); // we sort UP interfaces first, so select the first UP interface
         //
         view.set_selection(selected);
@@ -324,19 +282,44 @@ impl ViewWrapper for NetworkOptionsView {
 }
 
 struct InterfacePinningOptionsView {
-    view: ScrollView<NamedView<FormView<MacAddress>>>,
+    view: LinearLayout,
 }
 
 impl InterfacePinningOptionsView {
+    const ENABLED_CHECKBOX_NAME: &str = "network-interface-name-pinning-enabled";
     const FORM_NAME: &str = "network-interface-name-pinning-form";
 
-    fn new(interfaces: &[&Interface], options_ref: NetworkViewOptionsRef) -> Self {
+    fn new(options_ref: NetworkViewOptionsRef) -> Self {
         let options = options_ref.lock().expect("unpoisoned lock");
 
         // Filter out all non-physical links, as it does not make sense to pin their names
         // in this way.
         // The low-level installer will skip them anyway.
-        let interfaces = interfaces.iter().filter(|iface| iface.pinned_id.is_some());
+        let interfaces = options
+            .interfaces
+            .iter()
+            .filter(|iface| iface.pinned_id.is_some());
+
+        let mut view = LinearLayout::vertical()
+            .child(
+                LinearLayout::horizontal()
+                    .child(
+                        Checkbox::new()
+                            .with_checked(options.pinning_enabled)
+                            .on_change({
+                                let options_ref = options_ref.clone();
+                                move |siv, enabled| {
+                                    options_ref.lock().expect("unpoisoned lock").pinning_enabled =
+                                        enabled;
+
+                                    Self::toggle_all(siv, enabled);
+                                }
+                            })
+                            .with_name(Self::ENABLED_CHECKBOX_NAME),
+                    )
+                    .child(TextView::new(" Pin network interface names").no_wrap()),
+            )
+            .child(DummyView::new());
 
         let mut form = FormView::<MacAddress>::new();
 
@@ -356,7 +339,7 @@ impl InterfacePinningOptionsView {
                                 .expect("always pinnable interface")
                                 .name,
                         )
-                        .max_content_width(MAX_IFNAME_LEN)
+                        .with_enabled(options.pinning_enabled)
                         .fixed_width(MAX_IFNAME_LEN),
                 );
 
@@ -375,13 +358,31 @@ impl InterfacePinningOptionsView {
             }
         }
 
-        Self {
-            view: ScrollView::new(form.with_name(Self::FORM_NAME)),
-        }
+        view.add_child(form.with_name(Self::FORM_NAME));
+
+        Self { view }
     }
 
-    fn get_values(&mut self) -> Result<NetworkInterfacePinningOptions, String> {
-        let form = self.view.get_inner_mut().get_mut();
+    fn toggle_all(siv: &mut Cursive, enabled: bool) {
+        siv.call_on_name(Self::FORM_NAME, |v: &mut FormView<MacAddress>| {
+            v.call_on_childs(&|v: &mut LinearLayout| {
+                if let Some(v) = v
+                    .get_child_mut(1)
+                    .and_then(|v| v.downcast_mut::<ResizedView<EditView>>())
+                {
+                    v.get_inner_mut().set_enabled(enabled);
+                }
+            });
+        });
+    }
+
+    fn get_values(&mut self) -> Result<NetworkInterfacePinningOptions> {
+        let form = self
+            .view
+            .get_child_mut(2)
+            .and_then(|v| v.downcast_mut::<NamedView<FormView<MacAddress>>>())
+            .ok_or(anyhow!("failed to retrieve interface pinning form"))?
+            .get_mut();
 
         let mut mapping = HashMap::new();
 
@@ -395,18 +396,21 @@ impl InterfacePinningOptionsView {
                 .get_child(1)
                 .and_then(|v| v.downcast_ref::<ResizedView<EditView>>())
                 .map(|v| v.get_inner().get_content())
-                .ok_or_else(|| format!("failed to retrieve pinning ID for interface {}", mac))?;
+                .ok_or(anyhow!(
+                    "failed to retrieve pinning ID for interface {}",
+                    mac
+                ))?;
 
             mapping.insert(*mac, (*name).clone());
         }
 
         let opts = NetworkInterfacePinningOptions { mapping };
-        opts.verify().map_err(|err| err.to_string())?;
+        opts.verify()?;
 
         Ok(opts)
     }
 }
 
 impl ViewWrapper for InterfacePinningOptionsView {
-    cursive::wrap_impl!(self.view: ScrollView<NamedView<FormView<MacAddress>>>);
+    cursive::wrap_impl!(self.view: LinearLayout);
 }
-- 
2.55.0





  parent reply	other threads:[~2026-09-08 10:19 UTC|newest]

Thread overview: 16+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-08 10:16 [PATCH installer/proxmox 00/15] partially fix #2164: add bond setup across all installers Christoph Heiss
2026-09-08 10:16 ` [PATCH proxmox 01/15] installer-types: use `MacAddress` type where applicable Christoph Heiss
2026-09-08 10:16 ` [PATCH proxmox 02/15] installer-types: answer: add options for configuring management bond Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 03/15] install: run make tidy Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 04/15] tree-wide: use `MacAddress` type instead of string for MAC addresses Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 05/15] install: factor out /etc/network/interfaces setup into own subroutine Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 06/15] install: do not rely on interface name map to be fully populated Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 07/15] install: config: add option for setting up bond on management interface Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 08/15] install: network: set up management bond if requested Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 09/15] gui: network: factor out name pinning into advanced options dialog Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 10/15] gui: network: add bond setup options to " Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 11/15] auto: pass trough management network bond options to low-level installer Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 12/15] tui: drop long obsolete comment about logo formatting Christoph Heiss
2026-09-08 10:16 ` [PATCH installer 13/15] tui: views: add widget for displaying a group of checkboxes Christoph Heiss
2026-09-08 10:16 ` Christoph Heiss [this message]
2026-09-08 10:16 ` [PATCH installer 15/15] tui: network: add bond setup options to advanced network dialog Christoph Heiss

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=20260908101647.1057780-15-c.heiss@proxmox.com \
    --to=c.heiss@proxmox.com \
    --cc=pve-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 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