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