LCOV - code coverage report
Current view: top level - pkg/networkmanager - network-interface.jsx Coverage Total Hit
Test: cockpit Lines: 68.4 % 989 676
Test Date: 2026-06-01 17:00:21

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

Generated by: LCOV version 2.0-1