public inbox for pve-devel@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 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