LCOV - code coverage report
Current view: top level - pkg/networkmanager - network-interface.jsx Coverage Total Hit
Test: cockpit Lines: 97.7 % 969 947
Test Date: 2026-07-17 07:33:01

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2021 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5           37 : import cockpit from "cockpit";
       6           37 : import React, { useContext, useEffect, useRef, useState } from "react";
       7              : import { useEvent, useInit } from "hooks";
       8              : import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb/index.js";
       9              : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
      10              : import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
      11              : import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
      12              : import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
      13              : import { DropdownItem } from "@patternfly/react-core/dist/esm/components/Dropdown/index.js";
      14              : import { Form } from "@patternfly/react-core/dist/esm/components/Form/index.js";
      15              : import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
      16              : import { Gallery } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js";
      17              : import { Modal, ModalBody, ModalFooter, ModalHeader } from '@patternfly/react-core/dist/esm/components/Modal/index.js';
      18              : import { Page, PageBreadcrumb, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
      19              : import { Progress } from "@patternfly/react-core/dist/esm/components/Progress/index.js";
      20              : import { SearchInput } from "@patternfly/react-core/dist/esm/components/SearchInput/index.js";
      21              : import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
      22              : import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
      23              : import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
      24              : import { SortByDirection } from '@patternfly/react-table';
      25              : import {
      26              :     ConnectedIcon,
      27              :     DisconnectedIcon,
      28              :     LockIcon,
      29              :     LockOpenIcon,
      30              :     PlusIcon,
      31              :     RedoIcon,
      32              :     ThumbtackIcon,
      33              : } from "@patternfly/react-icons";
      34              : 
      35              : import { KebabDropdown } from "cockpit-components-dropdown";
      36              : import { ListingTable } from "cockpit-components-table.jsx";
      37              : import { Privileged } from "cockpit-components-privileged";
      38              : import { distanceToNow } from "timeformat";
      39              : import { fmt_to_fragments, in_anaconda_mode } from "utils";
      40              : import { useDialogs } from "dialogs.jsx";
      41              : 
      42              : import { ModelContext } from './model-context.jsx';
      43              : import { NetworkInterfaceMembers } from "./network-interface-members.jsx";
      44              : import { NetworkAction } from './dialogs-common.jsx';
      45              : import { NetworkPlots } from "./plots";
      46              : import * as utils from "./utils.js";
      47              : 
      48              : import {
      49              :     array_join,
      50              :     choice_title,
      51              :     complete_settings,
      52              :     connection_settings,
      53              :     free_member_connection,
      54              :     is_managed,
      55              :     render_active_connection,
      56              :     settings_applier,
      57              :     show_error_dialog,
      58              :     show_unexpected_error,
      59              :     syn_click,
      60              :     with_checkpoint,
      61              : } from './interfaces.js';
      62              : import {
      63              :     team_runner_choices,
      64              :     team_watch_choices,
      65              : } from './team.jsx';
      66              : import {
      67              :     bond_mode_choices,
      68              : } from './bond.jsx';
      69              : 
      70              : import { get_ip_method_choices } from './ip-settings.jsx';
      71              : 
      72              : import {
      73              :     useDialogState,
      74              :     DialogError, DialogErrorMessage,
      75              :     DialogTextInput,
      76              :     DialogPasswordInput,
      77              :     DialogDropdownSelect,
      78              :     DialogActionButton, DialogCancelButton,
      79              : } from 'cockpit/dialog';
      80              : 
      81           37 : const _ = cockpit.gettext;
      82              : 
      83              : // known networks: with ssid; hidden networks: no ssid
      84            2 : const WiFiConnectDialog = ({ dev, model, ssid: knownSsid, ap }) => {
      85            2 :     useEvent(model, "changed");
      86            2 :     const Dialogs = useDialogs();
      87              : 
      88            2 :     const isHidden = !knownSsid;
      89            2 :     const idPrefix = "network-wifi-connect";
      90              : 
      91            2 :     function validate() {
      92            2 :         if (isHidden) {
      93            1 :             dlg.field("ssid").validate(val => {
      94            1 :                 if (val.trim() === "")
      95            1 :                     return _("SSID can not be empty");
      96            1 :             });
      97            2 :         }
      98            2 :         if (!isHidden || dlg.values.security != "none") {
      99            2 :             dlg.field("password").validate(val => {
     100            2 :                 if (val.trim() === "")
     101            2 :                     return _("Password can not be empty");
     102            2 :             });
     103            2 :         }
     104            2 :     }
     105              : 
     106            2 :     const dlg = useDialogState({
     107            2 :         ssid: knownSsid || "",
     108            2 :         security: "wpa-psk",
     109            2 :         password: "",
     110            2 :     }, validate);
     111              : 
     112            2 :     const onConnect = async ({ ssid, security, password }) => {
     113            1 :         utils.debug("Connecting to", ssid, isHidden ? `with security ${security}` : "with password");
     114              : 
     115            2 :         const settings = {
     116            2 :             connection: {
     117            2 :                 id: ssid,
     118            2 :                 type: "802-11-wireless",
     119            2 :                 autoconnect: true,
     120            2 :             },
     121            2 :             "802-11-wireless": {
     122            2 :                 ssid: utils.ssid_to_nm(ssid),
     123            2 :                 mode: "infrastructure",
     124            2 :             },
     125            2 :         };
     126              : 
     127            2 :         if (isHidden) {
     128            2 :             settings["802-11-wireless"].hidden = true;
     129            2 :         }
     130              : 
     131            2 :         if (!isHidden || security !== "none") {
     132            2 :             settings["802-11-wireless-security"] = {
     133            1 :                 "key-mgmt": isHidden ? security : "wpa-psk",
     134            2 :                 psk: password,
     135            2 :             };
     136            2 :         }
     137              : 
     138            2 :         let connection = null;
     139            2 :         try {
     140              :             // ap might be stale if there was a scan since opening the dialog, so pass NULL
     141              :             // NM will find the right AP by SSID
     142            2 :             const result = await dev.activate_with_settings(settings, null);
     143            2 :             connection = result.connection;
     144              : 
     145            2 :             dlg.set_cancel(
     146            1 :                 () => {
     147            1 :                     utils.debug("Cancelling connection to", ssid);
     148            1 :                     dev.cancel_pending_connection();
     149            1 :                     connection.delete_()
     150            0 :                             .catch(err => console.warn("Failed to delete connection:", err));
     151            1 :                 });
     152              : 
     153            2 :             utils.debug("Connection activation started");
     154            2 :             await dev.wait_connection(ssid);
     155            2 :             utils.debug("Connected successfully to", ssid);
     156            2 :         } catch (err) {
     157              :             // just in case something survived, clean up
     158            2 :             connection?.delete_()
     159            0 :                     .catch(err => utils.debug("Failed to delete failed connection:", err));
     160              : 
     161            2 :             throw new DialogError(
     162            2 :                 _("Failed to connect"),
     163            2 :                 err.reason === 7 // NM_DEVICE_STATE_REASON_NO_SECRETS
     164            2 :                     ? _("Check your password.")
     165            2 :                     : err.toString());
     166            2 :         }
     167            2 :     };
     168              : 
     169            2 :     return (
     170            2 :         <Modal id={idPrefix + "-dialog"}
     171            2 :                position="top"
     172            2 :                variant="small"
     173            2 :                isOpen
     174            2 :                onClose={Dialogs.close}>
     175            1 :             <ModalHeader title={isHidden ? _("Connect to hidden network") : cockpit.format(_("Connect to $0"), knownSsid)} />
     176            2 :             <ModalBody>
     177            2 :                 <DialogErrorMessage dialog={dlg} />
     178            0 :                 <Form id={idPrefix + "-body"} onSubmit={ev => ev.preventDefault()} isHorizontal>
     179            2 :                     {isHidden && (
     180            2 :                         <>
     181            2 :                             <DialogTextInput
     182            2 :                                 label={_("Network name")}
     183            2 :                                 field={dlg.field("ssid")}
     184            2 :                             />
     185            2 :                             <DialogDropdownSelect
     186            2 :                                 label={_("Security")}
     187            2 :                                 field={dlg.field("security")}
     188            2 :                                 options={[
     189            2 :                                     { value: "none", label: _("None") },
     190            2 :                                     { value: "wpa-psk", label: _("WPA/WPA2 Personal") },
     191            2 :                                 ]}
     192            2 :                             />
     193            2 :                         </>
     194              :                     )}
     195            2 :                     {(!isHidden || dlg.values.security !== "none") && (
     196            2 :                         <DialogPasswordInput
     197            2 :                             label={_("Password")}
     198            2 :                             field={dlg.field("password")}
     199            2 :                             autoFocus={!isHidden} // eslint-disable-line jsx-a11y/no-autofocus
     200            2 :                         />
     201              :                     )}
     202            2 :                 </Form>
     203            2 :             </ModalBody>
     204            2 :             <ModalFooter>
     205            2 :                 <DialogActionButton dialog={dlg} action={onConnect} onClose={Dialogs.close}>
     206            2 :                     {_("Connect")}
     207            2 :                 </DialogActionButton>
     208            2 :                 <DialogCancelButton dialog={dlg} onClose={Dialogs.close} />
     209            2 :             </ModalFooter>
     210            2 :         </Modal>
     211              :     );
     212            2 : };
     213              : 
     214           31 : export const NetworkInterfacePage = ({
     215           31 :     privileged,
     216           31 :     operationInProgress,
     217           31 :     usage_monitor,
     218           31 :     plot_state,
     219           31 :     interfaces,
     220           31 :     iface
     221           31 : }) => {
     222           31 :     const model = useContext(ModelContext);
     223           31 :     useEvent(model, "changed");
     224           31 :     const [isScanning, setIsScanning] = useState(false);
     225           31 :     const [prevAPCount, setPrevAPCount] = useState(0);
     226           31 :     const [networkSearch, setNetworkSearch] = useState("");
     227              : 
     228           31 :     const dev_name = iface.Name;
     229           31 :     const dev = iface.Device;
     230           31 :     const isManaged = iface && (!dev || is_managed(dev));
     231              : 
     232            4 :     const accessPointCount = dev?.DeviceType === '802-11-wireless' ? (dev.AccessPoints?.length || 0) : 0;
     233              : 
     234           31 :     const Dialogs = useDialogs();
     235              : 
     236              :     // Trigger (passive) scan on page load for wireless devices on page load
     237           31 :     useInit(() => {
     238            5 :         if (dev?.DeviceType === '802-11-wireless') {
     239            5 :             utils.debug("Requesting initial WiFi scan for", dev_name);
     240            5 :             dev.request_scan();
     241            5 :         }
     242           31 :     });
     243              : 
     244              :     // WiFi scanning: re-enable button when APs change or after timeout
     245           31 :     useEffect(() => {
     246            4 :         if (isScanning) {
     247            4 :             if (accessPointCount !== prevAPCount && prevAPCount !== 0)
     248            4 :                 setIsScanning(false);
     249            0 :             const timer = setTimeout(() => setIsScanning(false), 5000);
     250            0 :             return () => clearTimeout(timer);
     251            4 :         }
     252           31 :         setPrevAPCount(accessPointCount);
     253           31 :     }, [isScanning, accessPointCount, prevAPCount]);
     254              : 
     255              :     // Track stable WiFi network order (by signal strength on first scan, preserved thereafter)
     256           31 :     const stableAPOrder = useRef([]);
     257              : 
     258              :     // Update stable AP order when APs are added/removed
     259           31 :     useEffect(() => {
     260           31 :         if (dev?.DeviceType !== '802-11-wireless')
     261           31 :             return;
     262              : 
     263            4 :         const accessPoints = dev.AccessPoints || [];
     264            2 :         const currentMACs = new Set(accessPoints.map(ap => ap.HwAddress));
     265           31 :         const stableMACs = new Set(stableAPOrder.current);
     266              : 
     267              :         // Re-sort if APs added/removed
     268           31 :         const needsResort = currentMACs.size !== stableMACs.size ||
     269            1 :                            ![...currentMACs].every(mac => stableMACs.has(mac));
     270              : 
     271            5 :         if (needsResort) {
     272              :             // Sort by signal strength
     273            2 :             const sorted = [...accessPoints].sort((a, b) => b.Strength - a.Strength);
     274              :             // Store MAC addresses
     275            2 :             stableAPOrder.current = sorted.map(ap => ap.HwAddress);
     276            5 :         }
     277           31 :     }, [dev?.AccessPoints, dev?.DeviceType]);
     278              : 
     279           31 :     let ghostSettings = null;
     280           31 :     let connectionSettings = null;
     281              : 
     282           31 :     if (iface) {
     283           31 :         if (iface.MainConnection) {
     284           31 :             connectionSettings = iface.MainConnection.Settings;
     285            8 :         } else {
     286            8 :             ghostSettings = createGhostConnectionSettings();
     287            8 :             connectionSettings = ghostSettings;
     288            8 :         }
     289           31 :     }
     290              : 
     291            6 :     function deleteConnections() {
     292            6 :         function deleteConnectionAndMembers(con) {
     293            6 :             return Promise.all(con.Members.map(s => free_member_connection(s))).then(() => con.delete_());
     294            6 :         }
     295              : 
     296            6 :         function deleteConnections(cons) {
     297            6 :             return Promise.all(cons.map(deleteConnectionAndMembers));
     298            6 :         }
     299              : 
     300            6 :         function deleteIfaceConnections(iface) {
     301            6 :             return deleteConnections(iface.Connections);
     302            6 :         }
     303              : 
     304            6 :         const location = cockpit.location;
     305              : 
     306            6 :         function modify() {
     307            6 :             return deleteIfaceConnections(iface)
     308            5 :                     .then(function () {
     309            5 :                         location.go("/");
     310            5 :                     })
     311            6 :                     .catch(show_unexpected_error);
     312            6 :         }
     313              : 
     314            6 :         if (iface) {
     315            6 :             with_checkpoint(model, modify,
     316            6 :                             {
     317            0 :                                 devices: dev ? [dev] : [],
     318            6 :                                 fail_text: fmt_to_fragments(_("Deleting $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{dev_name}</b>),
     319            6 :                                 anyway_text: cockpit.format(_("Delete $0"), dev_name),
     320            6 :                                 hack_does_add_or_remove: true,
     321            6 :                                 rollback_on_failure: true
     322            6 :                             });
     323            6 :         }
     324            6 :     }
     325              : 
     326            2 :     function connect() {
     327            1 :         if (!(iface.MainConnection || (dev && ghostSettings)))
     328            2 :             return;
     329              : 
     330            0 :         function fail(error) {
     331            0 :             show_unexpected_error(error);
     332            0 :         }
     333              : 
     334            2 :         function modify() {
     335            1 :             if (iface.MainConnection) {
     336            1 :                 return iface.MainConnection.activate(dev, null).catch(fail);
     337            0 :             } else {
     338            1 :                 return dev.activate_with_settings(ghostSettings, null).catch(fail);
     339            1 :             }
     340            2 :         }
     341              : 
     342            2 :         with_checkpoint(model, modify,
     343            2 :                         {
     344            1 :                             devices: dev ? [dev] : [],
     345            2 :                             fail_text: fmt_to_fragments(_("Switching on $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{dev_name}</b>),
     346            2 :                             anyway_text: cockpit.format(_("Switch on $0"), dev_name)
     347            2 :                         });
     348            2 :     }
     349              : 
     350            6 :     function disconnect() {
     351            0 :         if (!dev) {
     352            0 :             console.log("Trying to switch off without a device?");
     353            0 :             return;
     354            0 :         }
     355              : 
     356            6 :         function modify () {
     357            6 :             return dev.disconnect()
     358            0 :                     .catch(error => show_unexpected_error(error));
     359            6 :         }
     360              : 
     361            6 :         with_checkpoint(model, modify,
     362            6 :                         {
     363            6 :                             devices: [dev],
     364            6 :                             fail_text: fmt_to_fragments(_("Switching off $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{dev_name}</b>),
     365            6 :                             anyway_text: cockpit.format(_("Switch off $0"), dev_name)
     366            6 :                         });
     367            6 :     }
     368              : 
     369           31 :     function renderDesc() {
     370           31 :         let desc;
     371           31 :         let cs;
     372           31 :         if (dev) {
     373            5 :             if (dev.DeviceType == 'ethernet' || dev.IdVendor || dev.IdModel) {
     374           10 :                 desc = cockpit.format("$IdVendor $IdModel $Driver", dev);
     375            4 :             } else if (dev.DeviceType == 'bond') {
     376           12 :                 desc = _("Bond");
     377            4 :             } else if (dev.DeviceType == 'team') {
     378            5 :                 desc = _("Team");
     379            4 :             } else if (dev.DeviceType == 'vlan') {
     380            5 :                 desc = _("VLAN");
     381            4 :             } else if (dev.DeviceType == 'bridge') {
     382            7 :                 desc = _("Bridge");
     383            4 :             } else if (dev.Driver == 'wireguard') {
     384            5 :                 desc = "WireGuard";
     385            5 :             } else
     386           15 :                 desc = cockpit.format(_("Unknown \"$0\""), dev.DeviceType);
     387            5 :         } else if (iface) {
     388            5 :             cs = connection_settings(iface.Connections[0]);
     389            5 :             if (cs.type == "bond")
     390            4 :                 desc = _("Bond");
     391            5 :             else if (cs.type == "team")
     392            4 :                 desc = _("Team");
     393            5 :             else if (cs.type == "vlan")
     394            4 :                 desc = _("VLAN");
     395            5 :             else if (cs.type == "bridge")
     396            4 :                 desc = _("Bridge");
     397            4 :             else if (cs.type == "wireguard")
     398            4 :                 desc = "WireGuard";
     399            4 :             else if (cs.type)
     400            4 :                 desc = cockpit.format(_("Unknown \"$0\""), cs.type);
     401              :             else
     402            4 :                 desc = _("Unknown");
     403            5 :         } else
     404            4 :             desc = _("Unknown");
     405              : 
     406           31 :         return desc;
     407           31 :     }
     408              : 
     409           31 :     function renderMac() {
     410           31 :         let mac;
     411           31 :         if (dev &&
     412           30 :             dev.HwAddress) {
     413           30 :             mac = dev.HwAddress;
     414            6 :         } else if (iface &&
     415            7 :                    iface.MainConnection &&
     416            7 :                    iface.MainConnection.Settings &&
     417            7 :                    iface.MainConnection.Settings.ethernet &&
     418            4 :                    iface.MainConnection.Settings.ethernet.assigned_mac_address) {
     419            4 :             mac = iface.MainConnection.Settings.ethernet.assigned_mac_address;
     420            4 :         }
     421              : 
     422           31 :         const can_edit_mac = (privileged && iface && iface.MainConnection &&
     423           31 :                               (connection_settings(iface.MainConnection).type == "802-3-ethernet" ||
     424           21 :                                connection_settings(iface.MainConnection).type == "bond"));
     425              : 
     426           31 :         let mac_desc;
     427           26 :         if (can_edit_mac) {
     428           26 :             mac_desc = (
     429           26 :                 <NetworkAction type="mac" iface={iface} buttonText={mac} connectionSettings={iface.MainConnection.Settings} />
     430              :             );
     431            9 :         } else {
     432           14 :             mac_desc = mac;
     433           14 :         }
     434              : 
     435           31 :         return mac_desc;
     436           31 :     }
     437              : 
     438           31 :     function renderCarrierStatusRow() {
     439           31 :         if (dev && dev.Carrier !== undefined) {
     440           31 :             return (
     441           31 :                 <DescriptionListGroup>
     442           31 :                     <DescriptionListTerm>{_("Carrier")}</DescriptionListTerm>
     443           31 :                     <DescriptionListDescription data-label="Carrier">
     444            6 :                         {dev.Carrier ? (dev.Speed ? cockpit.format_bits_per_sec(dev.Speed * 1e6) : _("Yes")) : _("No")}
     445           31 :                     </DescriptionListDescription>
     446           31 :                 </DescriptionListGroup>
     447              :             );
     448           31 :         } else
     449            5 :             return null;
     450           31 :     }
     451              : 
     452           31 :     function renderActiveStatusRow() {
     453           31 :         let state;
     454              : 
     455           31 :         if (iface.MainConnection && iface.MainConnection.Groups.length > 0)
     456            6 :             return null;
     457              : 
     458           31 :         if (!dev)
     459            5 :             state = _("Inactive");
     460           30 :         else if (isManaged && dev.State != 100)
     461           25 :             state = dev.StateText;
     462              :         else
     463           27 :             state = null;
     464              : 
     465           31 :         const activeConnection = render_active_connection(dev, true, false);
     466           31 :         return (
     467           31 :             <DescriptionListGroup>
     468           31 :                 <DescriptionListTerm>{_("Status")}</DescriptionListTerm>
     469           31 :                 <DescriptionListDescription data-label="Status" className="networking-interface-status">
     470           31 :                     {[activeConnection, state].filter(val => val).join(", ")}
     471           31 :                 </DescriptionListDescription>
     472           31 :             </DescriptionListGroup>
     473              :         );
     474           31 :     }
     475              : 
     476           31 :     function renderConnectionSettingsRows(con, settings) {
     477           30 :         if (!isManaged || !settings)
     478            5 :             return [];
     479              : 
     480           30 :         let group_settings = null;
     481           30 :         if (con && con.Groups.length > 0)
     482            6 :             group_settings = con.Groups[0].Settings;
     483              : 
     484           30 :         function renderIpSettings(topic) {
     485           30 :             const params = settings[topic];
     486           30 :             const parts = [];
     487              : 
     488           30 :             if (params.method != "manual")
     489           30 :                 parts.push(choice_title(get_ip_method_choices(topic), params.method, _("Unknown configuration")));
     490              : 
     491           30 :             const addr_is_extra = (params.method != "manual");
     492           30 :             const addrs = [];
     493           10 :             params.address_data?.forEach(function (a) {
     494           10 :                 addrs.push(a.address + "/" + a.prefix);
     495           10 :             });
     496              : 
     497           30 :             if (addrs.length > 0)
     498            5 :                 parts.push(cockpit.format(addr_is_extra ? _("Additional address $val") : _("Address $val"),
     499           13 :                                           { val: addrs.join(", ") }));
     500              : 
     501           30 :             const gateway = params.gateway;
     502           11 :             if (gateway && gateway != "0.0.0.0" && gateway != "::")
     503           11 :                 parts.push(cockpit.format(_("Gateway $gateway"), { gateway }));
     504              : 
     505           30 :             const dns_is_extra = (!params["ignore-auto-dns"] && params.method != "manual");
     506           30 :             if (params.dns_data?.length > 0)
     507            6 :                 parts.push(cockpit.format(dns_is_extra ? _("Additional DNS $val") : _("DNS $val"),
     508            7 :                                           { val: params.dns_data.join(", ") }));
     509           30 :             if (params.dns_search?.length > 0)
     510            4 :                 parts.push(cockpit.format(dns_is_extra ? _("Additional DNS search domains $val") : _("DNS search domains $val"),
     511            5 :                                           { val: params.dns_search.join(", ") }));
     512              : 
     513           30 :             return parts;
     514           30 :         }
     515              : 
     516           30 :         function renderAutoconnectRow() {
     517           30 :             if (settings.connection.autoconnect !== undefined) {
     518           30 :                 return (
     519           30 :                     <DescriptionListGroup>
     520           30 :                         <DescriptionListTerm>{_("General")}</DescriptionListTerm>
     521           30 :                         <DescriptionListDescription data-label="General">
     522           30 :                             <Checkbox id="autoreconnect" isDisabled={!privileged}
     523            2 :                                       onChange={(_event, checked) => {
     524            2 :                                           settings.connection.autoconnect = checked;
     525            2 :                                           settings_applier(model, dev, con)(settings);
     526            2 :                                       }}
     527           30 :                                       isChecked={settings.connection.autoconnect}
     528           30 :                                       label={_("Connect automatically")} />
     529           30 :                         </DescriptionListDescription>
     530           30 :                     </DescriptionListGroup>
     531              :                 );
     532           30 :             }
     533           30 :         }
     534              : 
     535           30 :         function renderSettingsRow(title, rows, configure) {
     536           30 :             const link_text = [];
     537           30 :             for (let i = 0; i < rows.length; i++) {
     538           30 :                 link_text.push(rows[i]);
     539           30 :                 if (i < rows.length - 1)
     540           14 :                     link_text.push(<br key={"break-" + i} />);
     541           30 :             }
     542              : 
     543           30 :             return (
     544           30 :                 <DescriptionListGroup>
     545           30 :                     <DescriptionListTerm>{title}</DescriptionListTerm>
     546           30 :                     <DescriptionListDescription data-label={title}>
     547           30 :                         {link_text.length
     548           30 :                             ? <span className="network-interface-settings-text">
     549           30 :                                 {link_text}
     550           30 :                             </span>
     551            8 :                             : null}
     552           30 :                         {privileged
     553            4 :                             ? (typeof configure === 'function' ? <Button variant="link" isInline onClick={syn_click(model, configure)}>{_("edit")}</Button> : configure)
     554            5 :                             : null}
     555           30 :                     </DescriptionListDescription>
     556           30 :                 </DescriptionListGroup>
     557              :             );
     558           30 :         }
     559              : 
     560           30 :         function renderIpSettingsRow(topic, title) {
     561           30 :             if (!settings[topic])
     562            6 :                 return null;
     563              : 
     564           30 :             const configure = <NetworkAction type={topic} iface={iface} connectionSettings={settings} />;
     565           30 :             return renderSettingsRow(title, renderIpSettings(topic), configure);
     566           30 :         }
     567              : 
     568           30 :         function renderMtuSettingsRow() {
     569           30 :             const rows = [];
     570           30 :             const options = settings.ethernet;
     571              : 
     572           30 :             if (!options)
     573            9 :                 return null;
     574              : 
     575           24 :             function addRow(fmt, args) {
     576           24 :                 rows.push(cockpit.format(fmt, args));
     577           24 :             }
     578              : 
     579           27 :             if (options.mtu)
     580            5 :                 addRow("$mtu", options);
     581              :             else
     582           27 :                 addRow(_("Automatic"), options);
     583              : 
     584           27 :             const configure = <NetworkAction type="mtu" iface={iface} connectionSettings={settings} />;
     585           27 :             return renderSettingsRow(_("MTU"), rows, configure);
     586           30 :         }
     587              : 
     588            2 :         function render_connection_link(con, key) {
     589            2 :             return <span key={key}>
     590              :                 {
     591            2 :                     array_join(
     592            2 :                         con.Interfaces.map(iface =>
     593            2 :                             <Button variant="link" key={iface.Name}
     594            2 :                                     isInline
     595            0 :                                     onClick={() => cockpit.location.go([iface.Name])}>{iface.Name}</Button>),
     596            2 :                         ", ")
     597              :                 }
     598            2 :             </span>;
     599            2 :         }
     600              : 
     601           30 :         function render_group() {
     602            6 :             if (con && con.Groups.length > 0) {
     603            6 :                 return (
     604            6 :                     <DescriptionListGroup>
     605            6 :                         <DescriptionListTerm>{_("Group")}</DescriptionListTerm>
     606            6 :                         <DescriptionListDescription data-label="Group">
     607            6 :                             {array_join(con.Groups.map(render_connection_link), ", ")}
     608            6 :                         </DescriptionListDescription>
     609            6 :                     </DescriptionListGroup>
     610              :                 );
     611            6 :             } else
     612           30 :                 return null;
     613           30 :         }
     614              : 
     615           30 :         function renderBondSettingsRow() {
     616           30 :             const parts = [];
     617           30 :             const rows = [];
     618              : 
     619           30 :             if (!settings.bond)
     620           22 :                 return null;
     621              : 
     622           12 :             const options = settings.bond.options;
     623              : 
     624           12 :             parts.push(choice_title(bond_mode_choices, options.mode, options.mode));
     625           12 :             if (options.arp_interval)
     626            5 :                 parts.push(_("ARP monitoring"));
     627              : 
     628           12 :             if (parts.length > 0)
     629           12 :                 rows.push(parts.join(", "));
     630              : 
     631           12 :             const configure = <NetworkAction type="bond" iface={iface} connectionSettings={settings} />;
     632           12 :             return renderSettingsRow(_("Bond"), rows, configure);
     633           30 :         }
     634              : 
     635           30 :         function renderTeamSettingsRow() {
     636           30 :             const parts = [];
     637           30 :             const rows = [];
     638              : 
     639           30 :             if (!settings.team)
     640           30 :                 return null;
     641              : 
     642            5 :             const config = settings.team.config;
     643              : 
     644            5 :             if (config === null)
     645            4 :                 parts.push(_("Broken configuration"));
     646            5 :             else {
     647            5 :                 if (config.runner)
     648            5 :                     parts.push(choice_title(team_runner_choices, config.runner.name, config.runner.name));
     649            5 :                 if (config.link_watch && config.link_watch.name != "ethtool")
     650            4 :                     parts.push(choice_title(team_watch_choices, config.link_watch.name, config.link_watch.name));
     651            5 :             }
     652              : 
     653            5 :             if (parts.length > 0)
     654            5 :                 rows.push(parts.join(", "));
     655              : 
     656            5 :             const configure = <NetworkAction type="team" iface={iface} connectionSettings={settings} />;
     657            5 :             return renderSettingsRow(_("Team"), rows, configure);
     658           30 :         }
     659              : 
     660           30 :         function renderTeamPortSettingsRow() {
     661           30 :             const parts = [];
     662           30 :             const rows = [];
     663              : 
     664           30 :             if (!settings.team_port)
     665           30 :                 return null;
     666              : 
     667              :             /* Only "activebackup" and "lacp" team ports have
     668              :              * something to configure.
     669              :              */
     670            5 :             if (!group_settings ||
     671            5 :                 !group_settings.team ||
     672            5 :                 !group_settings.team.config ||
     673            5 :                 !group_settings.team.config.runner ||
     674            5 :                 !(group_settings.team.config.runner.name == "activebackup" ||
     675            4 :                   group_settings.team.config.runner.name == "lacp"))
     676            4 :                 return null;
     677              : 
     678            5 :             const config = settings.team_port.config;
     679              : 
     680            5 :             if (config === null)
     681            4 :                 parts.push(_("Broken configuration"));
     682              : 
     683            5 :             if (parts.length > 0)
     684            4 :                 rows.push(parts.join(", "));
     685              : 
     686            5 :             const configure = <NetworkAction type="teamport" iface={iface} connectionSettings={settings} />;
     687            5 :             return renderSettingsRow(_("Team port"), rows, configure);
     688           30 :         }
     689              : 
     690           30 :         function renderBridgeSettingsRow() {
     691           30 :             const rows = [];
     692           30 :             const options = settings.bridge;
     693              : 
     694           30 :             if (!options)
     695           29 :                 return null;
     696              : 
     697            2 :             function addRow(fmt, args) {
     698            2 :                 rows.push(cockpit.format(fmt, args));
     699            2 :             }
     700              : 
     701            6 :             if (options.stp) {
     702            6 :                 addRow(_("Spanning tree protocol"));
     703            6 :                 if (options.priority != 32768)
     704            5 :                     addRow(_("Priority $priority"), options);
     705            6 :                 if (options.forward_delay != 15)
     706            4 :                     addRow(_("Forward delay $forward_delay"), options);
     707            6 :                 if (options.hello_time != 2)
     708            5 :                     addRow(_("Hello time $hello_time"), options);
     709            6 :                 if (options.max_age != 20)
     710            4 :                     addRow(_("Maximum message age $max_age"), options);
     711            6 :             }
     712              : 
     713            7 :             const configure = <NetworkAction type="bridge" iface={iface} connectionSettings={settings} />;
     714            7 :             return renderSettingsRow(_("Bridge"), rows, configure);
     715           30 :         }
     716              : 
     717           30 :         function renderBridgePortSettingsRow() {
     718           30 :             const rows = [];
     719           30 :             const options = settings.bridge_port;
     720              : 
     721           30 :             if (!options)
     722           30 :                 return null;
     723              : 
     724            1 :             function addRow(fmt, args) {
     725            1 :                 rows.push(cockpit.format(fmt, args));
     726            1 :             }
     727              : 
     728            5 :             if (options.priority != 32)
     729            5 :                 addRow(_("Priority $priority"), options);
     730            5 :             if (options.path_cost != 100)
     731            5 :                 addRow(_("Path cost $path_cost"), options);
     732            5 :             if (options.hairpin_mode)
     733            5 :                 addRow(_("Hairpin mode"));
     734              : 
     735            5 :             const configure = <NetworkAction type="bridgeport" iface={iface} connectionSettings={settings} />;
     736            5 :             return renderSettingsRow(_("Bridge port"), rows, configure);
     737           30 :         }
     738              : 
     739           30 :         function renderVlanSettingsRow() {
     740           30 :             const rows = [];
     741           30 :             const options = settings.vlan;
     742              : 
     743           30 :             if (!options)
     744           29 :                 return null;
     745              : 
     746            1 :             function addRow(fmt, args) {
     747            1 :                 rows.push(cockpit.format(fmt, args));
     748            1 :             }
     749              : 
     750            5 :             addRow(_("Parent $parent"), options);
     751            5 :             addRow(_("ID $id"), options);
     752              : 
     753            5 :             const configure = <NetworkAction type="vlan" iface={iface} connectionSettings={settings} />;
     754            5 :             return renderSettingsRow(_("VLAN"), rows, configure);
     755           30 :         }
     756              : 
     757           30 :         function renderWireGuardSettingsRow() {
     758           30 :             const rows = [];
     759           30 :             const options = settings.wireguard;
     760              : 
     761           30 :             if (!options) {
     762           30 :                 return null;
     763           30 :             }
     764              : 
     765            5 :             const configure = <NetworkAction type="wg" iface={iface} connectionSettings={settings} />;
     766              : 
     767            5 :             return renderSettingsRow(_("WireGuard"), rows, configure);
     768           30 :         }
     769              : 
     770           30 :         return [
     771           30 :             render_group(),
     772           30 :             renderAutoconnectRow(),
     773           30 :             renderIpSettingsRow("ipv4", _("IPv4")),
     774           30 :             renderIpSettingsRow("ipv6", _("IPv6")),
     775           30 :             renderMtuSettingsRow(),
     776           30 :             renderVlanSettingsRow(),
     777           30 :             renderBridgeSettingsRow(),
     778           30 :             renderBridgePortSettingsRow(),
     779           30 :             renderBondSettingsRow(),
     780           30 :             renderTeamSettingsRow(),
     781           30 :             renderTeamPortSettingsRow(),
     782           30 :             renderWireGuardSettingsRow(),
     783           30 :         ];
     784           31 :     }
     785              : 
     786           31 :     function renderWiFiNetworks() {
     787           31 :         if (!dev || dev.DeviceType !== '802-11-wireless')
     788           30 :             return null;
     789              : 
     790            4 :         const accessPoints = dev.AccessPoints || [];
     791           31 :         if (accessPoints.length === 0)
     792            5 :             return null;
     793              : 
     794            5 :         const activeSSID = dev.ActiveAccessPoint ? dev.ActiveAccessPoint.Ssid : null;
     795              : 
     796            2 :         function forgetNetwork(ap) {
     797            2 :             utils.debug("Forgetting network", ap.Ssid);
     798              : 
     799            2 :             if (ap.Connection) {
     800            2 :                 ap.Connection.delete_()
     801            2 :                         .then(() => utils.debug("Forgot network", ap.Ssid))
     802            2 :                         .catch(show_unexpected_error);
     803            2 :             }
     804            2 :         }
     805              : 
     806            1 :         async function connectToAP(ap) {
     807              :             // we don't show a Connect button for hidden networks
     808            1 :             cockpit.assert(ap.Ssid);
     809            1 :             utils.debug("Connecting to", ap.Ssid);
     810              : 
     811            1 :             try {
     812            1 :                 if (ap.Connection) {
     813              :                     // Activate existing connection (which already has password if needed)
     814            1 :                     utils.debug("Activating existing connection for", ap.Ssid);
     815            1 :                     await ap.Connection.activate(dev, ap);
     816            1 :                     utils.debug("Connection activation started for", ap.Ssid);
     817            1 :                     await dev.wait_connection(ap.Ssid);
     818            1 :                     utils.debug("Connected successfully to", ap.Ssid);
     819            1 :                     return;
     820            1 :                 }
     821              : 
     822              :                 // Create new connection
     823            1 :                 const isSecured = !!(ap.WpaFlags || ap.RsnFlags);
     824              : 
     825            1 :                 if (isSecured) {
     826              :                     // Show password dialog for secured networks
     827            1 :                     utils.debug("Showing password dialog for", ap.Ssid);
     828            1 :                     Dialogs.show(<WiFiConnectDialog dev={dev} ap={ap} ssid={ap.Ssid} model={model} />);
     829            1 :                     return;
     830            1 :                 }
     831              : 
     832              :                 // Create new connection for open networks
     833            1 :                 utils.debug("Creating new connection for", ap.Ssid);
     834            1 :                 const settings = {
     835            1 :                     connection: {
     836            1 :                         id: ap.Ssid,
     837            1 :                         type: "802-11-wireless",
     838            1 :                         autoconnect: true,
     839            1 :                     },
     840            1 :                     "802-11-wireless": {
     841            1 :                         ssid: utils.ssid_to_nm(ap.Ssid),
     842            1 :                         mode: "infrastructure",
     843            1 :                     }
     844            1 :                 };
     845              : 
     846              :                 // Pass null for specific_object - NM will find the right AP by SSID
     847            1 :                 await dev.activate_with_settings(settings, null);
     848            1 :                 utils.debug("Connection activation started for", ap.Ssid);
     849            1 :                 await dev.wait_connection(ap.Ssid);
     850            1 :                 utils.debug("Connected successfully to", ap.Ssid);
     851            1 :             } catch (error) {
     852              :                 // Provide context-appropriate error message
     853            1 :                 const errorMsg = error.reason === 7 // NM_DEVICE_STATE_REASON_NO_SECRETS
     854            1 :                     ? _("Network password is not stored. Please forget and reconnect to this network.")
     855            1 :                     : error.toString();
     856            1 :                 show_error_dialog(
     857            1 :                     cockpit.format(_("Failed to connect to $0"), ap.Ssid),
     858            1 :                     errorMsg
     859            1 :                 );
     860            1 :             }
     861            1 :         }
     862              : 
     863            2 :         const networkSort = (rows, direction, columnIndex) => {
     864              :             // Separate hidden networks row from named networks rows
     865            2 :             const hiddenRow = rows.find(r => r.props["data-hidden"]);
     866            2 :             const namedRows = rows.filter(r => !r.props["data-hidden"]);
     867              : 
     868            1 :             if (columnIndex === 0) {
     869              :                 // Network column: simple alphabetical sort, no special cases
     870            0 :                 const sorted = [...namedRows].sort((a, b) =>
     871            0 :                     a.columns[0].sortKey.localeCompare(b.columns[0].sortKey)
     872            1 :                 );
     873              :                 // Always put hidden networks at the bottom
     874            1 :                 const result = direction === SortByDirection.asc ? sorted : sorted.reverse();
     875            1 :                 return hiddenRow ? [...result, hiddenRow] : result;
     876            1 :             } else {
     877              :                 // Signal column (default): group by connected > known > unknown, each sorted by signal strength
     878              : 
     879              :                 // Separate into groups
     880            2 :                 const activeRows = [];
     881            2 :                 const knownRows = [];
     882            2 :                 const unknownRows = [];
     883              : 
     884            2 :                 namedRows.forEach(r => {
     885            2 :                     const isActive = activeSSID && r.props["data-ssid"] === activeSSID;
     886            2 :                     if (isActive) {
     887            2 :                         activeRows.push(r);
     888            2 :                     } else if (r.props["data-known"]) {
     889            2 :                         knownRows.push(r);
     890            2 :                     } else {
     891            2 :                         unknownRows.push(r);
     892            2 :                     }
     893            2 :                 });
     894              : 
     895              :                 // Sort each group by stable signal order
     896              :                 // Build a map for O(1) lookups instead of O(n) indexOf
     897            2 :                 const orderMap = new Map();
     898            2 :                 stableAPOrder.current.forEach((mac, index) => orderMap.set(mac, index));
     899              : 
     900            2 :                 const sortByStableOrder = (a, b) => {
     901            2 :                     const aMAC = a.props.key;
     902            2 :                     const bMAC = b.props.key;
     903            2 :                     const aOrder = orderMap.get(aMAC);
     904            2 :                     const bOrder = orderMap.get(bMAC);
     905            2 :                     if (aOrder === undefined || bOrder === undefined) {
     906            2 :                         return a.columns[2].sortKey.localeCompare(b.columns[2].sortKey);
     907            2 :                     }
     908            2 :                     return aOrder - bOrder;
     909            2 :                 };
     910              : 
     911            2 :                 knownRows.sort(sortByStableOrder);
     912            2 :                 unknownRows.sort(sortByStableOrder);
     913              : 
     914              :                 // Concatenate groups
     915            2 :                 const result = [...activeRows, ...knownRows, ...unknownRows];
     916            1 :                 const sortedResult = direction === SortByDirection.asc ? result : result.reverse();
     917              :                 // Always put hidden networks at the bottom
     918            2 :                 return hiddenRow ? [...sortedResult, hiddenRow] : sortedResult;
     919            2 :             }
     920            2 :         };
     921              : 
     922              :         // Filter by name
     923           31 :         let filteredVisibleAPs = dev.visibleSsids;
     924            4 :         if (networkSearch) {
     925            4 :             const searchLower = networkSearch.toLowerCase();
     926            0 :             filteredVisibleAPs = dev.visibleSsids.filter(ap => ap.Ssid.toLowerCase().includes(searchLower));
     927            4 :         }
     928              : 
     929            2 :         const rows = filteredVisibleAPs.map((ap, index) => {
     930            2 :             const isActive = activeSSID && ap.Ssid === activeSSID;
     931            2 :             const isSecured = !!(ap.WpaFlags || ap.RsnFlags);
     932              : 
     933            2 :             const securityIcon = isSecured
     934            2 :                 ? <LockIcon aria-label={_("secured")} />
     935            2 :                 : <LockOpenIcon aria-label={_("open")} />;
     936              : 
     937            2 :             const nameContent = (
     938            2 :                 <>
     939            2 :                     {ap.Ssid}
     940            2 :                     {isActive && <>{" "} <ConnectedIcon className="nm-icon-connected" /></>}
     941            2 :                     {!isActive && ap.Connection && <>{" "} <ThumbtackIcon className="nm-icon-known" /></>}
     942            2 :                 </>
     943              :             );
     944              : 
     945            2 :             const timestamp = ap.Connection?.Settings?.connection?.timestamp || 0;
     946            2 :             const nameColumn = timestamp > 0
     947              :                 ? (
     948            1 :                     <Tooltip content={cockpit.format(_("Last connected: $0"), distanceToNow(timestamp * 1000))}>
     949            1 :                         <span>{nameContent}</span>
     950            1 :                     </Tooltip>
     951              :                 )
     952            2 :                 : nameContent;
     953              : 
     954            2 :             const signalColumn = (
     955            2 :                 <Progress value={ap.Strength}
     956            2 :                           label={ap.Strength + "%"}
     957            2 :                           aria-label={_("Signal strength")}
     958            2 :                           size="sm" />
     959              :             );
     960              : 
     961            2 :             let actionColumn;
     962            2 :             if (isActive) {
     963            2 :                 actionColumn = (
     964            2 :                     <Privileged allowed={privileged}
     965            2 :                                 tooltipId={"wifi-disconnect-" + index}
     966            2 :                                 excuse={_("Not permitted to disconnect network")}>
     967            2 :                         <Button variant="danger"
     968            2 :                                 size="sm"
     969            2 :                                 icon={<DisconnectedIcon />}
     970            2 :                                 isDisabled={!privileged}
     971            2 :                                 onClick={() => {
     972            2 :                                     dev.disconnect()
     973            2 :                                             .then(() => utils.debug("Disconnected successfully from", ap.Ssid))
     974            2 :                                             .catch(show_unexpected_error);
     975            2 :                                 }}
     976            2 :                                 aria-label={_("Disconnect")}>
     977            2 :                             {_("Disconnect")}
     978            2 :                         </Button>
     979            2 :                     </Privileged>
     980              :                 );
     981            2 :             } else {
     982            2 :                 actionColumn = (
     983            2 :                     <>
     984            2 :                         <Privileged allowed={privileged}
     985            2 :                                     tooltipId={"wifi-connect-" + index}
     986            2 :                                     excuse={_("Not permitted to connect to network")}>
     987            2 :                             <Button variant="secondary"
     988            2 :                                     size="sm"
     989            2 :                                     icon={<ConnectedIcon />}
     990            2 :                                     isDisabled={!privileged}
     991            1 :                                     onClick={() => connectToAP(ap)}
     992            2 :                                     aria-label={_("Connect")}>
     993            2 :                                 {_("Connect")}
     994            2 :                             </Button>
     995            2 :                         </Privileged>
     996            2 :                         {ap.Connection && (
     997            2 :                             <>
     998            2 :                                 {" "}
     999            2 :                                 <KebabDropdown
    1000            2 :                                     toggleButtonId={"wifi-kebab-" + index}
    1001            2 :                                     isDisabled={!privileged}
    1002            2 :                                     dropdownItems={[
    1003            2 :                                         <DropdownItem key="forget"
    1004            2 :                                                       className="pf-m-danger"
    1005            2 :                                                       onClick={() => forgetNetwork(ap)}
    1006            2 :                                                       aria-label={_("Forget")}>
    1007            2 :                                             {_("Forget")}
    1008            2 :                                         </DropdownItem>
    1009            2 :                                     ]} />
    1010            2 :                             </>
    1011              :                         )}
    1012            2 :                     </>
    1013              :                 );
    1014            2 :             }
    1015              : 
    1016            2 :             return {
    1017            2 :                 columns: [
    1018            2 :                     { title: nameColumn, sortKey: ap.Ssid, header: true },
    1019            2 :                     { title: <>{securityIcon} {ap.Mode}</>, sortKey: ap.Mode },
    1020            2 :                     { title: signalColumn, sortKey: String(ap.Strength).padStart(3, '0') },
    1021            2 :                     { title: cockpit.format_bits_per_sec(ap.MaxBitrate * 1000) },
    1022            2 :                     { title: actionColumn },
    1023            2 :                 ],
    1024            2 :                 props: { key: ap.HwAddress, "data-ssid": ap.Ssid, "data-known": !!ap.Connection }
    1025            2 :             };
    1026            2 :         });
    1027              : 
    1028              :         // Add aggregated hidden access points row at the bottom
    1029            5 :         if (dev.hiddenAPCount > 0) {
    1030            5 :             const hiddenLabel = cockpit.ngettext("$0 hidden network", "$0 hidden networks", dev.hiddenAPCount);
    1031            5 :             rows.push({
    1032            5 :                 columns: [
    1033            5 :                     { title: cockpit.format(hiddenLabel, dev.hiddenAPCount), sortKey: "zzz-hidden", header: true },
    1034            5 :                     { title: "" },
    1035            5 :                     { title: "" },
    1036            5 :                     { title: "" },
    1037            5 :                     { title: "" },
    1038            5 :                 ],
    1039            5 :                 props: { key: "hidden-networks", "data-hidden": true }
    1040            5 :             });
    1041            5 :         }
    1042              : 
    1043            5 :         return (
    1044            5 :             <Card isPlain id="network-interface-wifi-networks">
    1045            5 :                 <CardHeader actions={{
    1046            5 :                     actions: (
    1047            5 :                         <Flex>
    1048            5 :                             {dev.visibleSsids.length >= 3 && (
    1049            5 :                                 <FlexItem>
    1050            5 :                                     <SearchInput
    1051            5 :                                         placeholder={_("Filter")}
    1052            5 :                                         value={networkSearch}
    1053            0 :                                         onChange={(_event, value) => setNetworkSearch(value)}
    1054            0 :                                         onClear={() => setNetworkSearch("")}
    1055            5 :                                     />
    1056            5 :                                 </FlexItem>
    1057              :                             )}
    1058           31 :                             <FlexItem>
    1059           31 :                                 <Button variant="secondary"
    1060            1 :                                         onClick={() => Dialogs.show(<WiFiConnectDialog dev={dev} model={model} />)}
    1061           31 :                                         icon={<PlusIcon />}>
    1062           31 :                                     {_("Connect to hidden network")}
    1063           31 :                                 </Button>
    1064           31 :                             </FlexItem>
    1065           31 :                             <FlexItem>
    1066           31 :                                 <Button variant="secondary"
    1067            0 :                                         onClick={() => { setIsScanning(true); dev.request_scan() }}
    1068           31 :                                         isDisabled={isScanning}
    1069            4 :                                         icon={isScanning ? <Spinner size="md" /> : <RedoIcon />}>
    1070           31 :                                     {_("Refresh")}
    1071           31 :                                 </Button>
    1072           31 :                             </FlexItem>
    1073           31 :                         </Flex>
    1074              :                     )
    1075           31 :                 }}>
    1076           31 :                     <CardTitle component="h2">{_("Available networks")}</CardTitle>
    1077           31 :                 </CardHeader>
    1078           31 :                 <ListingTable aria-label={_("Available networks")}
    1079           31 :                               variant='compact'
    1080           31 :                               columns={[
    1081           31 :                                   { title: _("Network"), header: true, sortable: true },
    1082           31 :                                   { title: _("Mode") },
    1083           31 :                                   { title: _("Signal"), sortable: true },
    1084           31 :                                   { title: _("Rate") },
    1085           31 :                                   { title: "", props: { screenReaderText: _("Actions") } },
    1086           31 :                               ]}
    1087           31 :                               sortBy={{ index: 2, direction: SortByDirection.asc }}
    1088           31 :                               sortMethod={networkSort}
    1089           31 :                               rows={rows} />
    1090           31 :             </Card>
    1091              :         );
    1092           31 :     }
    1093              : 
    1094           31 :     function renderConnectionMembers(con) {
    1095           31 :         const memberIfaces = { };
    1096           31 :         const members = { };
    1097              : 
    1098           31 :         const rx_plot_data = {
    1099           31 :             direct: "network.interface.in.bytes",
    1100           31 :             internal: "network.interface.rx",
    1101           31 :             units: "bytes",
    1102           31 :             derive: "rate",
    1103           31 :             factor: 8
    1104           31 :         };
    1105              : 
    1106           31 :         const tx_plot_data = {
    1107           31 :             direct: "network.interface.out.bytes",
    1108           31 :             internal: "network.interface.tx",
    1109           31 :             units: "bytes",
    1110           31 :             derive: "rate",
    1111           31 :             factor: 8
    1112           31 :         };
    1113              : 
    1114           31 :         const cs = con && connection_settings(con);
    1115           22 :         if (!con || (cs.type != "bond" && cs.type != "team" && cs.type != "bridge")) {
    1116           22 :             plot_state.plot_instances('rx', rx_plot_data, [dev_name], true);
    1117           22 :             plot_state.plot_instances('tx', tx_plot_data, [dev_name], true);
    1118           22 :             return null;
    1119           22 :         }
    1120              : 
    1121           16 :         const plot_ifaces = [];
    1122              : 
    1123           13 :         con.Members.forEach(member_con => {
    1124           13 :             member_con.Interfaces.forEach(iface => {
    1125           13 :                 if (iface.MainConnection != member_con)
    1126           13 :                     return;
    1127              : 
    1128           13 :                 const dev = iface.Device;
    1129              : 
    1130              :                 /* Unmanaged devices shouldn't show up as members
    1131              :                  * but let's not take any chances.
    1132              :                  */
    1133           13 :                 if (dev && !is_managed(dev))
    1134           13 :                     return;
    1135              : 
    1136           13 :                 plot_ifaces.push(iface.Name);
    1137           13 :                 usage_monitor.add(iface.Name);
    1138           13 :                 members[iface.Name] = iface;
    1139           13 :                 memberIfaces[iface.Name] = true;
    1140           13 :             });
    1141           13 :         });
    1142              : 
    1143           16 :         plot_state.plot_instances('rx', rx_plot_data, plot_ifaces, true);
    1144           16 :         plot_state.plot_instances('tx', tx_plot_data, plot_ifaces, true);
    1145              : 
    1146           16 :         const sorted_members = Object.keys(members).sort()
    1147           13 :                 .map(name => members[name]);
    1148              : 
    1149           16 :         return (
    1150           16 :             <NetworkInterfaceMembers members={sorted_members}
    1151           16 :                                      memberIfaces={memberIfaces}
    1152           16 :                                      interfaces={interfaces}
    1153           16 :                                      iface={iface}
    1154           16 :                                      usage_monitor={usage_monitor}
    1155           16 :                                      privileged={privileged} />
    1156              :         );
    1157           31 :     }
    1158              : 
    1159            4 :     function createGhostConnectionSettings() {
    1160            4 :         const settings = {
    1161            4 :             connection: {
    1162            4 :                 interface_name: iface.Name
    1163            4 :             },
    1164            4 :             ipv4: {
    1165            4 :                 method: "auto",
    1166            4 :                 address_data: [],
    1167            4 :                 dns_data: [],
    1168            4 :                 dns_search: [],
    1169            4 :                 route_data: []
    1170            4 :             },
    1171            4 :             ipv6: {
    1172            4 :                 method: "auto",
    1173            4 :                 address_data: [],
    1174            4 :                 dns_data: [],
    1175            4 :                 dns_search: [],
    1176            4 :                 route_data: []
    1177            4 :             }
    1178            4 :         };
    1179            4 :         complete_settings(settings, dev);
    1180            4 :         return settings;
    1181            4 :     }
    1182              : 
    1183              :     /* Disable the On/Off button for interfaces that we don't know about at all,
    1184              :        and for devices that NM declares to be unavailable. Neither can be activated.
    1185              :     */
    1186              : 
    1187           31 :     let onoff;
    1188           30 :     if (isManaged) {
    1189           30 :         onoff = (
    1190           30 :             <Privileged allowed={privileged}
    1191           30 :                         tooltipId="interface-switch"
    1192           30 :                         excuse={ _("Not permitted to configure network devices") }>
    1193           30 :                 <Switch id="interface-switch"
    1194           30 :                         isChecked={!!(dev && dev.ActiveConnection)}
    1195           30 :                         isDisabled={!iface || (dev && dev.State == 20) || !privileged}
    1196            2 :                         onChange={(_event, enable) => enable ? connect() : disconnect()}
    1197           30 :                         aria-label={_("Enable or disable the device")} />
    1198           30 :             </Privileged>
    1199              :         );
    1200           30 :     }
    1201              : 
    1202           31 :     const isDeletable = (iface && !dev) || (dev && (dev.DeviceType == 'bond' ||
    1203           23 :                                                     dev.DeviceType == 'team' ||
    1204           23 :                                                     dev.DeviceType == 'vlan' ||
    1205           22 :                                                     dev.DeviceType == 'bridge' ||
    1206           21 :                                                     dev.DeviceType == 'wireguard'));
    1207              : 
    1208           31 :     const settingsRows = renderConnectionSettingsRows(iface.MainConnection, connectionSettings)
    1209           30 :             .map((component, idx) => <React.Fragment key={idx}>{component}</React.Fragment>);
    1210              : 
    1211           31 :     const anaconda = in_anaconda_mode();
    1212              : 
    1213           31 :     return (
    1214           31 :         <Page id="network-interface"
    1215           31 :               data-test-wait={operationInProgress}
    1216            4 :               className={"pf-m-no-sidebar" + (anaconda ? " anaconda" : "")}>
    1217           31 :             <PageBreadcrumb hasBodyWrapper={false} stickyOnBreakpoint={{ default: "top" }}>
    1218           31 :                 <Breadcrumb>
    1219           31 :                     <BreadcrumbItem to='#/'>
    1220           31 :                         {_("Networking")}
    1221           31 :                     </BreadcrumbItem>
    1222           31 :                     <BreadcrumbItem isActive>
    1223           31 :                         {dev_name}
    1224           31 :                     </BreadcrumbItem>
    1225           31 :                 </Breadcrumb>
    1226           31 :             </PageBreadcrumb>
    1227           31 :             <PageSection hasBodyWrapper={false}>
    1228           31 :                 <NetworkPlots plot_state={plot_state} />
    1229           31 :             </PageSection>
    1230           31 :             <PageSection hasBodyWrapper={false}>
    1231           31 :                 <Gallery hasGutter>
    1232           31 :                     <Card isPlain className="network-interface-details">
    1233           31 :                         <CardHeader actions={{
    1234           31 :                             actions: (
    1235           31 :                                 <>
    1236           18 :                                     {isDeletable && isManaged &&
    1237           18 :                                     <Button variant="danger"
    1238           18 :                                                  onClick={syn_click(model, deleteConnections)}
    1239           18 :                                                  id="network-interface-delete">
    1240           18 :                                         {_("Delete")}
    1241           18 :                                     </Button>}
    1242           31 :                                     {onoff}
    1243           31 :                                 </>
    1244              :                             ),
    1245           31 :                         }}>
    1246           31 :                             <CardTitle className="network-interface-details-title">
    1247           31 :                                 <span id="network-interface-name">{dev_name}</span>
    1248           31 :                                 <span id="network-interface-hw">{renderDesc()}</span>
    1249           31 :                                 <span id="network-interface-mac">{renderMac()}</span>
    1250           31 :                             </CardTitle>
    1251           31 :                         </CardHeader>
    1252           31 :                         <CardBody>
    1253           31 :                             <DescriptionList id="network-interface-settings" className="network-interface-settings pf-m-horizontal-on-sm">
    1254           31 :                                 {renderActiveStatusRow()}
    1255           31 :                                 {renderCarrierStatusRow()}
    1256           31 :                                 {settingsRows}
    1257           31 :                             </DescriptionList>
    1258           31 :                         </CardBody>
    1259           31 :                         { !isManaged
    1260            5 :                             ? <CardBody>
    1261            5 :                                 {_("This device cannot be managed here.")}
    1262            5 :                             </CardBody>
    1263           30 :                             : null
    1264              :                         }
    1265           31 :                     </Card>
    1266           31 :                     {renderWiFiNetworks()}
    1267           31 :                     {renderConnectionMembers(iface.MainConnection)}
    1268           31 :                 </Gallery>
    1269           31 :             </PageSection>
    1270           31 :         </Page>
    1271              :     );
    1272           31 : };
        

Generated by: LCOV version 2.0-1