LCOV - code coverage report
Current view: top level - pkg/networkmanager - network-interface.jsx Coverage Total Hit
Test: cockpit Lines: 23.5 % 969 228
Test Date: 2026-07-02 14:11:36

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

Generated by: LCOV version 2.0-1