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, { useState, useContext } from "react";
7 : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
8 : import { Card, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
9 : import { Dropdown, DropdownItem, DropdownList } from '@patternfly/react-core/dist/esm/components/Dropdown/index.js';
10 : import { MenuToggle } from "@patternfly/react-core/dist/esm/components/MenuToggle";
11 : import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
12 : import { MinusIcon } from '@patternfly/react-icons';
13 :
14 : import { ListingTable } from "cockpit-components-table.jsx";
15 : import { ModelContext } from './model-context.jsx';
16 : import { useEvent } from "hooks";
17 :
18 : import {
19 : connection_settings,
20 : device_state_text,
21 : free_member_connection,
22 : is_interesting_interface,
23 : set_member,
24 : show_unexpected_error,
25 : syn_click,
26 : with_checkpoint,
27 : is_managed,
28 : } from './interfaces.js';
29 : import { fmt_to_fragments } from 'utils.jsx';
30 :
31 37 : const _ = cockpit.gettext;
32 :
33 14 : export const NetworkInterfaceMembers = ({
34 14 : members,
35 14 : memberIfaces,
36 14 : interfaces,
37 14 : iface,
38 14 : usage_monitor,
39 14 : privileged
40 14 : }) => {
41 14 : const model = useContext(ModelContext);
42 14 : const [isOpen, setIsOpen] = useState(false);
43 14 : useEvent(usage_monitor.grid, "notify");
44 :
45 14 : function renderMemberRows() {
46 14 : const rows = [];
47 :
48 13 : members.forEach(iface => {
49 13 : const member_con = iface.MainConnection;
50 13 : const dev = iface.Device;
51 12 : const isActive = (dev && dev.State == 100 && dev.Carrier === true);
52 13 : const onoff = (
53 13 : <Switch
54 13 : aria-label={cockpit.format(_("Switch off $0"), iface.Name)}
55 13 : isDisabled={!privileged}
56 13 : isChecked={!!(dev && dev.ActiveConnection)}
57 0 : onChange={(_event, val) => {
58 0 : if (val) {
59 0 : with_checkpoint(
60 0 : model,
61 0 : () => member_con.activate(dev).catch(show_unexpected_error),
62 0 : {
63 0 : devices: dev ? [dev] : [],
64 0 : fail_text: fmt_to_fragments(_("Switching on $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{iface.Name}</b>),
65 0 : anyway_text: cockpit.format(_("Switch on $0"), iface.Name)
66 0 : });
67 0 : } else if (dev) {
68 0 : with_checkpoint(
69 0 : model,
70 0 : () => dev.disconnect().catch(show_unexpected_error),
71 0 : {
72 0 : devices: [dev],
73 0 : fail_text: fmt_to_fragments(_("Switching off $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{iface.Name}</b>),
74 0 : anyway_text: cockpit.format(_("Switch off $0"), iface.Name)
75 0 : });
76 0 : }
77 0 : } } />
78 : );
79 :
80 13 : const row = ({
81 13 : columns: [
82 0 : { title: (!dev || is_managed(dev)) ? <Button variant="link" isInline onClick={() => cockpit.location.go([iface.Name])}>{iface.Name}</Button> : iface.Name },
83 : // Will add traffic info right after
84 13 : {
85 13 : title: (
86 13 : <div className="btn-group">
87 13 : {onoff}
88 13 : {privileged && <Button icon={<MinusIcon />} variant="secondary"
89 13 : size="sm"
90 0 : onClick={syn_click(model, () => {
91 0 : with_checkpoint(
92 0 : model,
93 0 : () => free_member_connection(member_con).catch(show_unexpected_error),
94 0 : {
95 0 : devices: dev ? [dev] : [],
96 0 : fail_text: fmt_to_fragments(_("Removing $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{iface.Name}</b>),
97 0 : anyway_text: cockpit.format(_("Remove $0"), iface.Name),
98 0 : hack_does_add_or_remove: true
99 0 : });
100 0 : return false;
101 0 : })} />}
102 13 : </div>
103 : ),
104 13 : props: { className: "pf-v6-c-table__action" }
105 13 : },
106 13 : ],
107 13 : props: {
108 13 : key: iface.Name,
109 13 : "data-interface": encodeURIComponent(iface.Name),
110 11 : "data-sample-id": isActive ? encodeURIComponent(iface.Name) : null,
111 13 : "data-row-id": iface.Name,
112 13 : }
113 13 : });
114 :
115 12 : if (isActive) {
116 12 : const samples = usage_monitor.samples[iface.Name];
117 2 : row.columns.splice(1, 0, { title: samples ? cockpit.format_bits_per_sec(samples[1][0] * 8) : "" });
118 2 : row.columns.splice(2, 0, { title: samples ? cockpit.format_bits_per_sec(samples[0][0] * 8) : "" });
119 11 : } else {
120 12 : row.columns.splice(1, 0, { title: device_state_text(dev) });
121 12 : row.columns.splice(1, 0, { title: "" });
122 12 : }
123 :
124 13 : rows.push(row);
125 13 : });
126 14 : return rows;
127 14 : }
128 :
129 14 : const main_connection = iface.MainConnection;
130 14 : const cs = iface.MainConnection && connection_settings(iface.MainConnection);
131 14 : const dropdownItems = [];
132 :
133 14 : interfaces.forEach(i => {
134 14 : if (is_interesting_interface(i) && !memberIfaces[i.Name] && i != iface) {
135 0 : const onClick = () => {
136 0 : with_checkpoint(
137 0 : model,
138 0 : () => {
139 0 : return set_member(model, main_connection, main_connection.Settings,
140 0 : cs.type, i.Name, true)
141 0 : .catch(show_unexpected_error);
142 0 : },
143 0 : {
144 0 : devices: i.Device ? [i.Device] : [],
145 0 : fail_text: fmt_to_fragments(_("Adding $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{i.Name}</b>),
146 0 : anyway_text: cockpit.format(_("Add $0"), i.Name),
147 0 : hack_does_add_or_remove: true
148 0 : }
149 0 : );
150 0 : };
151 :
152 14 : dropdownItems.push(
153 14 : <DropdownItem onClick={syn_click(model, onClick)}
154 14 : key={"add-member-" + i.Name}
155 14 : component="button">
156 14 : {i.Name}
157 14 : </DropdownItem>
158 14 : );
159 14 : }
160 14 : });
161 :
162 : // We want to show a Tooltip but that isn't supported by PatternFly.
163 : // https://github.com/patternfly/patternfly-react/issues/12500
164 2 : if (dropdownItems.length === 0) {
165 2 : dropdownItems.push(
166 2 : <DropdownItem isDisabled isAriaDisabled>
167 2 : {_("No intefaces available")}
168 2 : </DropdownItem>
169 2 : );
170 2 : }
171 :
172 14 : const add_btn = (
173 0 : <Dropdown onSelect={() => setIsOpen(false)}
174 14 : toggle={(toggleRef) => (
175 0 : <MenuToggle id="add-member" ref={toggleRef} onClick={() => setIsOpen(!isOpen)}>
176 14 : {_("Add member")}
177 14 : </MenuToggle>
178 : )}
179 14 : isOpen={isOpen}
180 14 : popperProps={{ position: "right" }}
181 : >
182 14 : <DropdownList>
183 14 : {dropdownItems}
184 14 : </DropdownList>
185 14 : </Dropdown>
186 : );
187 :
188 14 : return (
189 14 : <Card isPlain id="network-interface-members" className="network-interface-members">
190 14 : <CardHeader actions={{ actions: add_btn }}>
191 14 : <CardTitle component="h2">{_("Interface members")}</CardTitle>
192 14 : </CardHeader>
193 14 : <ListingTable aria-label={_("Interface members")}
194 14 : className="networking-interface-members"
195 14 : variant='compact'
196 14 : columns={[
197 2 : { title: (cs && cs.type == "bond") ? _("Interfaces") : _("Ports"), props: { width: 25 } },
198 14 : { title: _("Sending"), props: { width: 25 } },
199 14 : { title: _("Receiving"), props: { width: 25 } },
200 14 : { title: "" },
201 14 : ]}
202 14 : rows={renderMemberRows()} />
203 14 : </Card>
204 : );
205 14 : };
|