/* * Copyright (C) 2021 Red Hat, Inc. * SPDX-License-Identifier: LGPL-2.1-or-later */ import cockpit from "cockpit"; import React, { useContext, useEffect, useRef, useState } from "react"; import { useEvent, useInit } from "hooks"; import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb/index.js"; import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js"; import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js'; import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js"; import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js"; import { Form } from "@patternfly/react-core/dist/esm/components/Form/index.js"; import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js"; import { Gallery } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js"; import { Modal, ModalBody, ModalFooter, ModalHeader } from '@patternfly/react-core/dist/esm/components/Modal/index.js'; import { Page, PageBreadcrumb, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js"; import { Progress } from "@patternfly/react-core/dist/esm/components/Progress/index.js"; import { SearchInput } from "@patternfly/react-core/dist/esm/components/SearchInput/index.js"; import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js"; import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js"; import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js"; import { ActionsColumn, SortByDirection } from '@patternfly/react-table'; import { ConnectedIcon, DisconnectedIcon, LockIcon, LockOpenIcon, PlusIcon, RedoIcon, ThumbtackIcon, } from "@patternfly/react-icons"; import { ListingTable } from "cockpit-components-table.jsx"; import { Privileged } from "cockpit-components-privileged"; import { distanceToNow } from "timeformat"; import { fmt_to_fragments, in_anaconda_mode } from "utils"; import { useDialogs } from "dialogs.jsx"; import { ModelContext } from './model-context.jsx'; import { NetworkInterfaceMembers } from "./network-interface-members.jsx"; import { NetworkAction } from './dialogs-common.jsx'; import { NetworkPlots } from "./plots"; import * as utils from "./utils.js"; import { array_join, choice_title, complete_settings, connection_settings, free_member_connection, is_managed, render_active_connection, settings_applier, show_error_dialog, show_unexpected_error, syn_click, with_checkpoint, } from './interfaces.js'; import { team_runner_choices, team_watch_choices, } from './team.jsx'; import { bond_mode_choices, } from './bond.jsx'; import { get_ip_method_choices } from './ip-settings.jsx'; import { useDialogState, DialogError, DialogErrorMessage, DialogTextInput, DialogPasswordInput, DialogDropdownSelect, DialogActionButton, DialogCancelButton, } from 'cockpit/dialog'; const _ = cockpit.gettext; // known networks: with ssid; hidden networks: no ssid const WiFiConnectDialog = ({ dev, model, ssid: knownSsid, ap }) => { useEvent(model, "changed"); const Dialogs = useDialogs(); const isHidden = !knownSsid; const idPrefix = "network-wifi-connect"; function validate() { if (isHidden) { dlg.field("ssid").validate(val => { if (val.trim() === "") return _("SSID can not be empty"); }); } if (!isHidden || dlg.values.security !== "none") { dlg.field("password").validate(val => { if (val.trim() === "") return _("Password can not be empty"); }); } } const dlg = useDialogState({ ssid: knownSsid || "", security: "wpa-psk", password: "", }, validate); const onConnect = async ({ ssid, security, password }) => { utils.debug("Connecting to", ssid, isHidden ? `with security ${security}` : "with password"); const settings = { connection: { id: ssid, type: "802-11-wireless", autoconnect: true, }, "802-11-wireless": { ssid: utils.ssid_to_nm(ssid), mode: "infrastructure", }, }; if (isHidden) { settings["802-11-wireless"].hidden = true; } if (!isHidden || security !== "none") { settings["802-11-wireless-security"] = { "key-mgmt": isHidden ? security : "wpa-psk", psk: password, }; } let connection = null; try { // ap might be stale if there was a scan since opening the dialog, so pass NULL // NM will find the right AP by SSID const result = await dev.activate_with_settings(settings, null); connection = result.connection; dlg.set_cancel( () => { utils.debug("Cancelling connection to", ssid); dev.cancel_pending_connection(); connection.delete_() .catch(err => console.warn("Failed to delete connection:", err)); }); utils.debug("Connection activation started"); await dev.wait_connection(ssid); utils.debug("Connected successfully to", ssid); } catch (err) { // just in case something survived, clean up connection?.delete_() .catch(err => utils.debug("Failed to delete failed connection:", err)); throw new DialogError( _("Failed to connect"), err.reason === 7 // NM_DEVICE_STATE_REASON_NO_SECRETS ? _("Check your password.") : err.toString()); } }; return ( ); }; export const NetworkInterfacePage = ({ privileged, operationInProgress, usage_monitor, plot_state, interfaces, iface }) => { const model = useContext(ModelContext); useEvent(model, "changed"); const [isScanning, setIsScanning] = useState(false); const [prevAPCount, setPrevAPCount] = useState(0); const [networkSearch, setNetworkSearch] = useState(""); const anaconda_mode = in_anaconda_mode(); const dev_name = iface.Name; const dev = iface.Device; const isManaged = iface && (!dev || is_managed(dev)); const accessPointCount = dev?.DeviceType === '802-11-wireless' ? (dev.AccessPoints?.length || 0) : 0; const Dialogs = useDialogs(); // Trigger (passive) scan on page load for wireless devices on page load useInit(() => { if (dev?.DeviceType === '802-11-wireless') { utils.debug("Requesting initial WiFi scan for", dev_name); dev.request_scan(); } }); // WiFi scanning: re-enable button when APs change or after timeout useEffect(() => { if (isScanning) { if (accessPointCount !== prevAPCount && prevAPCount !== 0) setIsScanning(false); const timer = setTimeout(() => setIsScanning(false), 5000); return () => clearTimeout(timer); } setPrevAPCount(accessPointCount); }, [isScanning, accessPointCount, prevAPCount]); // Track stable WiFi network order (by signal strength on first scan, preserved thereafter) const stableAPOrder = useRef([]); // Update stable AP order when APs are added/removed useEffect(() => { if (dev?.DeviceType !== '802-11-wireless') return; const accessPoints = dev.AccessPoints || []; const currentMACs = new Set(accessPoints.map(ap => ap.HwAddress)); const stableMACs = new Set(stableAPOrder.current); // Re-sort if APs added/removed const needsResort = currentMACs.size !== stableMACs.size || ![...currentMACs].every(mac => stableMACs.has(mac)); if (needsResort) { // Sort by signal strength const sorted = [...accessPoints].sort((a, b) => b.Strength - a.Strength); // Store MAC addresses stableAPOrder.current = sorted.map(ap => ap.HwAddress); } }, [dev?.AccessPoints, dev?.DeviceType]); let ghostSettings = null; let connectionSettings = null; if (iface) { if (iface.MainConnection) { connectionSettings = iface.MainConnection.Settings; } else { ghostSettings = createGhostConnectionSettings(); connectionSettings = ghostSettings; } } function deleteConnections() { function deleteConnectionAndMembers(con) { return Promise.all(con.Members.map(s => free_member_connection(s))).then(() => con.delete_()); } function deleteConnections(cons) { return Promise.all(cons.map(deleteConnectionAndMembers)); } function deleteIfaceConnections(iface) { return deleteConnections(iface.Connections); } const location = cockpit.location; function modify() { return deleteIfaceConnections(iface) .then(function () { location.go("/"); }) .catch(show_unexpected_error); } if (iface) { with_checkpoint(model, modify, { devices: dev ? [dev] : [], fail_text: fmt_to_fragments(_("Deleting $0 will break the connection to the server, and will make the administration UI unavailable."), {dev_name}), anyway_text: cockpit.format(_("Delete $0"), dev_name), hack_does_add_or_remove: true, rollback_on_failure: true }); } } function connect() { if (!(iface.MainConnection || (dev && ghostSettings))) return; function fail(error) { show_unexpected_error(error); } function modify() { if (iface.MainConnection) { return iface.MainConnection.activate(dev, null).catch(fail); } else { return dev.activate_with_settings(ghostSettings, null).catch(fail); } } with_checkpoint(model, modify, { devices: dev ? [dev] : [], fail_text: fmt_to_fragments(_("Switching on $0 will break the connection to the server, and will make the administration UI unavailable."), {dev_name}), anyway_text: cockpit.format(_("Switch on $0"), dev_name) }); } function disconnect() { if (!dev) { console.log("Trying to switch off without a device?"); return; } function modify () { return dev.disconnect() .catch(error => show_unexpected_error(error)); } with_checkpoint(model, modify, { devices: [dev], fail_text: fmt_to_fragments(_("Switching off $0 will break the connection to the server, and will make the administration UI unavailable."), {dev_name}), anyway_text: cockpit.format(_("Switch off $0"), dev_name) }); } function renderDesc() { let desc; let cs; if (dev) { if (dev.DeviceType === 'ethernet' || dev.IdVendor || dev.IdModel) { desc = cockpit.format("$IdVendor $IdModel $Driver", dev); } else if (dev.DeviceType === 'bond') { desc = _("Bond"); } else if (dev.DeviceType === 'team') { desc = _("Team"); } else if (dev.DeviceType === 'vlan') { desc = _("VLAN"); } else if (dev.DeviceType === 'bridge') { desc = _("Bridge"); } else if (dev.Driver === 'wireguard') { desc = "WireGuard"; } else desc = cockpit.format(_("Unknown \"$0\""), dev.DeviceType); } else if (iface) { cs = connection_settings(iface.Connections[0]); if (cs.type === "bond") desc = _("Bond"); else if (cs.type === "team") desc = _("Team"); else if (cs.type === "vlan") desc = _("VLAN"); else if (cs.type === "bridge") desc = _("Bridge"); else if (cs.type === "wireguard") desc = "WireGuard"; else if (cs.type) desc = cockpit.format(_("Unknown \"$0\""), cs.type); else desc = _("Unknown"); } else desc = _("Unknown"); return desc; } function renderMac() { let mac; if (dev && dev.HwAddress) { mac = dev.HwAddress; } else if (iface && iface.MainConnection && iface.MainConnection.Settings && iface.MainConnection.Settings.ethernet && iface.MainConnection.Settings.ethernet.assigned_mac_address) { mac = iface.MainConnection.Settings.ethernet.assigned_mac_address; } const can_edit_mac = (privileged && iface && iface.MainConnection && (connection_settings(iface.MainConnection).type === "802-3-ethernet" || connection_settings(iface.MainConnection).type === "bond")); let mac_desc; if (can_edit_mac) { mac_desc = ( ); } else { mac_desc = mac; } return mac_desc; } function renderCarrierStatusRow() { if (dev && dev.Carrier !== undefined) { return ( {_("Carrier")} {dev.Carrier ? (dev.Speed ? cockpit.format_bits_per_sec(dev.Speed * 1e6) : _("Yes")) : _("No")} ); } else return null; } function renderActiveStatusRow() { let state; if (iface.MainConnection && iface.MainConnection.Groups.length > 0) return null; if (!dev) state = _("Inactive"); else if (isManaged && dev.State !== 100) state = dev.StateText; else state = null; const activeConnection = render_active_connection(dev, true, false); return ( {_("Status")} {[activeConnection, state].filter(val => val).join(", ")} ); } function renderConnectionSettingsRows(con, settings) { if (!isManaged || !settings) return []; let group_settings = null; if (con && con.Groups.length > 0) group_settings = con.Groups[0].Settings; function renderIpSettings(topic) { const params = settings[topic]; const parts = []; if (params.method !== "manual") parts.push(choice_title(get_ip_method_choices(topic), params.method, _("Unknown configuration"))); const addr_is_extra = (params.method !== "manual"); const addrs = []; params.address_data?.forEach(function (a) { addrs.push(a.address + "/" + a.prefix); }); if (addrs.length > 0) parts.push(cockpit.format(addr_is_extra ? _("Additional address $val") : _("Address $val"), { val: addrs.join(", ") })); const gateway = params.gateway; if (gateway && gateway !== "0.0.0.0" && gateway !== "::") parts.push(cockpit.format(_("Gateway $gateway"), { gateway })); const dns_is_extra = (!params["ignore-auto-dns"] && params.method !== "manual"); if (params.dns_data?.length > 0) parts.push(cockpit.format(dns_is_extra ? _("Additional DNS $val") : _("DNS $val"), { val: params.dns_data.join(", ") })); if (params.dns_search?.length > 0) parts.push(cockpit.format(dns_is_extra ? _("Additional DNS search domains $val") : _("DNS search domains $val"), { val: params.dns_search.join(", ") })); return parts; } function renderAutoconnectRow() { if (settings.connection.autoconnect !== undefined) { return ( {_("General")} { settings.connection.autoconnect = checked; settings_applier(model, dev, con)(settings); }} isChecked={settings.connection.autoconnect} label={_("Connect automatically")} /> ); } } function renderSettingsRow(title, rows, configure) { const link_text = []; for (let i = 0; i < rows.length; i++) { link_text.push(rows[i]); if (i < rows.length - 1) link_text.push(
); } return ( {title} {link_text.length ? {link_text} : null} {privileged ? (typeof configure === 'function' ? : configure) : null} ); } function renderIpSettingsRow(topic, title) { if (!settings[topic]) return null; const configure = ; return renderSettingsRow(title, renderIpSettings(topic), configure); } function renderMtuSettingsRow() { const rows = []; const options = settings.ethernet; if (!options) return null; function addRow(fmt, args) { rows.push(cockpit.format(fmt, args)); } if (options.mtu) addRow("$mtu", options); else addRow(_("Automatic"), options); const configure = ; return renderSettingsRow(_("MTU"), rows, configure); } function render_connection_link(con, key) { return { array_join( con.Interfaces.map(iface => ), ", ") } ; } function render_group() { if (con && con.Groups.length > 0) { return ( {_("Group")} {array_join(con.Groups.map(render_connection_link), ", ")} ); } else return null; } function renderBondSettingsRow() { const parts = []; const rows = []; if (!settings.bond) return null; const options = settings.bond.options; parts.push(choice_title(bond_mode_choices, options.mode, options.mode)); if (options.arp_interval) parts.push(_("ARP monitoring")); if (parts.length > 0) rows.push(parts.join(", ")); const configure = ; return renderSettingsRow(_("Bond"), rows, configure); } function renderTeamSettingsRow() { const parts = []; const rows = []; if (!settings.team) return null; const config = settings.team.config; if (config === null) parts.push(_("Broken configuration")); else { if (config.runner) parts.push(choice_title(team_runner_choices, config.runner.name, config.runner.name)); if (config.link_watch && config.link_watch.name !== "ethtool") parts.push(choice_title(team_watch_choices, config.link_watch.name, config.link_watch.name)); } if (parts.length > 0) rows.push(parts.join(", ")); const configure = ; return renderSettingsRow(_("Team"), rows, configure); } function renderTeamPortSettingsRow() { const parts = []; const rows = []; if (!settings.team_port) return null; /* Only "activebackup" and "lacp" team ports have * something to configure. */ if (!group_settings || !group_settings.team || !group_settings.team.config || !group_settings.team.config.runner || !(group_settings.team.config.runner.name === "activebackup" || group_settings.team.config.runner.name === "lacp")) return null; const config = settings.team_port.config; if (config === null) parts.push(_("Broken configuration")); if (parts.length > 0) rows.push(parts.join(", ")); const configure = ; return renderSettingsRow(_("Team port"), rows, configure); } function renderBridgeSettingsRow() { const rows = []; const options = settings.bridge; if (!options) return null; function addRow(fmt, args) { rows.push(cockpit.format(fmt, args)); } if (options.stp) { addRow(_("Spanning tree protocol")); if (options.priority !== 32768) addRow(_("Priority $priority"), options); if (options.forward_delay !== 15) addRow(_("Forward delay $forward_delay"), options); if (options.hello_time !== 2) addRow(_("Hello time $hello_time"), options); if (options.max_age !== 20) addRow(_("Maximum message age $max_age"), options); } const configure = ; return renderSettingsRow(_("Bridge"), rows, configure); } function renderBridgePortSettingsRow() { const rows = []; const options = settings.bridge_port; if (!options) return null; function addRow(fmt, args) { rows.push(cockpit.format(fmt, args)); } if (options.priority !== 32) addRow(_("Priority $priority"), options); if (options.path_cost !== 100) addRow(_("Path cost $path_cost"), options); if (options.hairpin_mode) addRow(_("Hairpin mode")); const configure = ; return renderSettingsRow(_("Bridge port"), rows, configure); } function renderVlanSettingsRow() { const rows = []; const options = settings.vlan; if (!options) return null; function addRow(fmt, args) { rows.push(cockpit.format(fmt, args)); } addRow(_("Parent $parent"), options); addRow(_("ID $id"), options); const configure = ; return renderSettingsRow(_("VLAN"), rows, configure); } function renderWireGuardSettingsRow() { const rows = []; const options = settings.wireguard; if (!options) { return null; } const configure = ; return renderSettingsRow(_("WireGuard"), rows, configure); } return [ render_group(), renderAutoconnectRow(), renderIpSettingsRow("ipv4", _("IPv4")), renderIpSettingsRow("ipv6", _("IPv6")), renderMtuSettingsRow(), renderVlanSettingsRow(), renderBridgeSettingsRow(), renderBridgePortSettingsRow(), renderBondSettingsRow(), renderTeamSettingsRow(), renderTeamPortSettingsRow(), renderWireGuardSettingsRow(), ]; } function renderWiFiNetworks() { if (!dev || dev.DeviceType !== '802-11-wireless') return null; const accessPoints = dev.AccessPoints || []; if (accessPoints.length === 0) return null; const activeSSID = dev.ActiveAccessPoint ? dev.ActiveAccessPoint.Ssid : null; function forgetNetwork(ap) { utils.debug("Forgetting network", ap.Ssid); if (ap.Connection) { ap.Connection.delete_() .then(() => utils.debug("Forgot network", ap.Ssid)) .catch(show_unexpected_error); } } async function connectToAP(ap) { // we don't show a Connect button for hidden networks cockpit.assert(ap.Ssid); utils.debug("Connecting to", ap.Ssid); try { if (ap.Connection) { // Activate existing connection (which already has password if needed) utils.debug("Activating existing connection for", ap.Ssid); await ap.Connection.activate(dev, ap); utils.debug("Connection activation started for", ap.Ssid); await dev.wait_connection(ap.Ssid); utils.debug("Connected successfully to", ap.Ssid); return; } // Create new connection const isSecured = !!(ap.WpaFlags || ap.RsnFlags); if (isSecured) { // Show password dialog for secured networks utils.debug("Showing password dialog for", ap.Ssid); Dialogs.show(); return; } // Create new connection for open networks utils.debug("Creating new connection for", ap.Ssid); const settings = { connection: { id: ap.Ssid, type: "802-11-wireless", autoconnect: true, }, "802-11-wireless": { ssid: utils.ssid_to_nm(ap.Ssid), mode: "infrastructure", } }; // Pass null for specific_object - NM will find the right AP by SSID await dev.activate_with_settings(settings, null); utils.debug("Connection activation started for", ap.Ssid); await dev.wait_connection(ap.Ssid); utils.debug("Connected successfully to", ap.Ssid); } catch (error) { // Provide context-appropriate error message const errorMsg = error.reason === 7 // NM_DEVICE_STATE_REASON_NO_SECRETS ? _("Network password is not stored. Please forget and reconnect to this network.") : error.toString(); show_error_dialog( cockpit.format(_("Failed to connect to $0"), ap.Ssid), errorMsg ); } } const networkSort = (rows, direction, columnIndex) => { // Separate hidden networks row from named networks rows const hiddenRow = rows.find(r => r.props["data-hidden"]); const namedRows = rows.filter(r => !r.props["data-hidden"]); if (columnIndex === 0) { // Network column: simple alphabetical sort, no special cases const sorted = [...namedRows].sort((a, b) => a.columns[0].sortKey.localeCompare(b.columns[0].sortKey) ); // Always put hidden networks at the bottom const result = direction === SortByDirection.asc ? sorted : sorted.reverse(); return hiddenRow ? [...result, hiddenRow] : result; } else { // Signal column (default): group by connected > known > unknown, each sorted by signal strength // Separate into groups const activeRows = []; const knownRows = []; const unknownRows = []; namedRows.forEach(r => { const isActive = activeSSID && r.props["data-ssid"] === activeSSID; if (isActive) { activeRows.push(r); } else if (r.props["data-known"]) { knownRows.push(r); } else { unknownRows.push(r); } }); // Sort each group by stable signal order // Build a map for O(1) lookups instead of O(n) indexOf const orderMap = new Map(); stableAPOrder.current.forEach((mac, index) => orderMap.set(mac, index)); const sortByStableOrder = (a, b) => { const aMAC = a.props.key; const bMAC = b.props.key; const aOrder = orderMap.get(aMAC); const bOrder = orderMap.get(bMAC); if (aOrder === undefined || bOrder === undefined) { return a.columns[2].sortKey.localeCompare(b.columns[2].sortKey); } return aOrder - bOrder; }; knownRows.sort(sortByStableOrder); unknownRows.sort(sortByStableOrder); // Concatenate groups const result = [...activeRows, ...knownRows, ...unknownRows]; const sortedResult = direction === SortByDirection.asc ? result : result.reverse(); // Always put hidden networks at the bottom return hiddenRow ? [...sortedResult, hiddenRow] : sortedResult; } }; // Filter by name let filteredVisibleAPs = dev.visibleSsids; if (networkSearch) { const searchLower = networkSearch.toLowerCase(); filteredVisibleAPs = dev.visibleSsids.filter(ap => ap.Ssid.toLowerCase().includes(searchLower)); } const rows = filteredVisibleAPs.map((ap, index) => { const isActive = activeSSID && ap.Ssid === activeSSID; const isSecured = !!(ap.WpaFlags || ap.RsnFlags); const securityIcon = isSecured ? : ; const nameContent = ( <> {ap.Ssid} {isActive && <>{" "} } {!isActive && ap.Connection && <>{" "} } ); const timestamp = ap.Connection?.Settings?.connection?.timestamp || 0; const nameColumn = timestamp > 0 ? ( {nameContent} ) : nameContent; const signalColumn = ( ); let actionColumnContent; if (isActive) { actionColumnContent = ( ); } else { actionColumnContent = ( ); } const actionColumn = ( {actionColumnContent} ); const networkColumns = [ { title: nameColumn, sortKey: ap.Ssid, header: true }, { title: <>{securityIcon} {ap.Mode}, sortKey: ap.Mode }, { title: signalColumn, sortKey: String(ap.Strength).padStart(3, '0') }, { title: cockpit.format_bits_per_sec(ap.MaxBitrate * 1000) }, { title: actionColumn }, ]; if (ap.Connection) { const rowActions = forgetNetwork(ap), isDanger: true, "aria-label": _("Forget") } ]} isDisabled={!privileged} />; networkColumns.push({ title: rowActions, props: { isActionCell: true } }); } else { networkColumns.push({ title: "" }); } return { columns: networkColumns, props: { key: ap.HwAddress, "data-ssid": ap.Ssid, "data-known": !!ap.Connection } }; }); // Add aggregated hidden access points row at the bottom if (dev.hiddenAPCount > 0) { const hiddenLabel = cockpit.ngettext("$0 hidden network", "$0 hidden networks", dev.hiddenAPCount); rows.push({ columns: [ { title: cockpit.format(hiddenLabel, dev.hiddenAPCount), sortKey: "zzz-hidden", header: true }, { title: "" }, { title: "" }, { title: "" }, { title: "" }, { title: "" }, ], props: { key: "hidden-networks", "data-hidden": true } }); } return ( {dev.visibleSsids.length >= 3 && ( setNetworkSearch(value)} onClear={() => setNetworkSearch("")} /> )} ) }}> {_("Available networks")} ); } function renderConnectionMembers(con) { const memberIfaces = { }; const members = { }; const rx_plot_data = { direct: "network.interface.in.bytes", internal: "network.interface.rx", units: "bytes", derive: "rate", factor: 8 }; const tx_plot_data = { direct: "network.interface.out.bytes", internal: "network.interface.tx", units: "bytes", derive: "rate", factor: 8 }; const cs = con && connection_settings(con); if (plot_state && (!con || (cs.type !== "bond" && cs.type !== "team" && cs.type !== "bridge"))) { plot_state.plot_instances('rx', rx_plot_data, [dev_name], true); plot_state.plot_instances('tx', tx_plot_data, [dev_name], true); return null; } const plot_ifaces = []; con.Members.forEach(member_con => { member_con.Interfaces.forEach(iface => { if (iface.MainConnection !== member_con) return; const dev = iface.Device; /* Unmanaged devices shouldn't show up as members * but let's not take any chances. */ if (dev && !is_managed(dev)) return; plot_ifaces.push(iface.Name); usage_monitor.add(iface.Name); members[iface.Name] = iface; memberIfaces[iface.Name] = true; }); }); if (plot_state) { plot_state.plot_instances('rx', rx_plot_data, plot_ifaces, true); plot_state.plot_instances('tx', tx_plot_data, plot_ifaces, true); } const sorted_members = Object.keys(members).sort() .map(name => members[name]); return ( ); } function createGhostConnectionSettings() { const settings = { connection: { interface_name: iface.Name }, ipv4: { method: "auto", address_data: [], dns_data: [], dns_search: [], route_data: [] }, ipv6: { method: "auto", address_data: [], dns_data: [], dns_search: [], route_data: [] } }; complete_settings(settings, dev); return settings; } /* Disable the On/Off button for interfaces that we don't know about at all, and for devices that NM declares to be unavailable. Neither can be activated. */ let onoff; if (isManaged) { onoff = ( enable ? connect() : disconnect()} aria-label={_("Enable or disable the device")} /> ); } const isDeletable = (iface && !dev) || (dev && (dev.DeviceType === 'bond' || dev.DeviceType === 'team' || dev.DeviceType === 'vlan' || dev.DeviceType === 'bridge' || dev.DeviceType === 'wireguard')); const settingsRows = renderConnectionSettingsRows(iface.MainConnection, connectionSettings) .map((component, idx) => {component}); return ( { !anaconda_mode && ( {_("Networking")} {dev_name} ) } { plot_state && } {isDeletable && isManaged && } {onoff} ), }}> {dev_name} {renderDesc()} {renderMac()} {renderActiveStatusRow()} {renderCarrierStatusRow()} {settingsRows} { !isManaged ? {_("This device cannot be managed here.")} : null } {renderWiFiNetworks()} { !anaconda_mode && renderConnectionMembers(iface.MainConnection)} ); };