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 : };
|