Line data Source code
1 : /*
2 : * Copyright (C) 2021 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 1 : import cockpit from "cockpit";
6 1 : 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 1 : const _ = cockpit.gettext;
32 :
33 0 : export const NetworkInterfaceMembers = ({
34 0 : members,
35 0 : memberIfaces,
36 0 : interfaces,
37 0 : iface,
38 0 : usage_monitor,
39 0 : privileged
40 0 : }) => {
41 0 : const model = useContext(ModelContext);
42 0 : const [isOpen, setIsOpen] = useState(false);
43 0 : useEvent(usage_monitor.grid, "notify");
44 :
45 0 : function renderMemberRows() {
46 0 : const rows = [];
47 :
48 0 : members.forEach(iface => {
49 0 : const member_con = iface.MainConnection;
50 0 : const dev = iface.Device;
51 0 : const isActive = (dev && dev.State == 100 && dev.Carrier === true);
52 0 : const onoff = (
53 0 : <Switch
54 0 : aria-label={cockpit.format(_("Switch off $0"), iface.Name)}
55 0 : isDisabled={!privileged}
56 0 : 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 0 : const row = ({
81 0 : 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 0 : {
85 0 : title: (
86 0 : <div className="btn-group">
87 0 : {onoff}
88 0 : {privileged && <Button icon={<MinusIcon />} variant="secondary"
89 0 : 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 0 : </div>
103 : ),
104 0 : props: { className: "pf-v6-c-table__action" }
105 0 : },
106 0 : ],
107 0 : props: {
108 0 : key: iface.Name,
109 0 : "data-interface": encodeURIComponent(iface.Name),
110 0 : "data-sample-id": isActive ? encodeURIComponent(iface.Name) : null,
111 0 : "data-row-id": iface.Name,
112 0 : }
113 0 : });
114 :
115 0 : if (isActive) {
116 0 : const samples = usage_monitor.samples[iface.Name];
117 0 : row.columns.splice(1, 0, { title: samples ? cockpit.format_bits_per_sec(samples[1][0] * 8) : "" });
118 0 : row.columns.splice(2, 0, { title: samples ? cockpit.format_bits_per_sec(samples[0][0] * 8) : "" });
119 0 : } else {
120 0 : row.columns.splice(1, 0, { title: device_state_text() });
121 0 : row.columns.splice(1, 0, { title: "" });
122 0 : }
123 :
124 0 : rows.push(row);
125 0 : });
126 0 : return rows;
127 0 : }
128 :
129 0 : const main_connection = iface.MainConnection;
130 0 : const cs = iface.MainConnection && connection_settings(iface.MainConnection);
131 :
132 0 : const dropdownItems = (
133 0 : interfaces
134 0 : .filter(i => {
135 0 : return (is_interesting_interface(i) &&
136 0 : !memberIfaces[i.Name] &&
137 0 : i != iface);
138 0 : })
139 0 : .map(iface => {
140 0 : const onClick = () => {
141 0 : with_checkpoint(
142 0 : model,
143 0 : () => {
144 0 : return set_member(model, main_connection, main_connection.Settings,
145 0 : cs.type, iface.Name, true)
146 0 : .catch(show_unexpected_error);
147 0 : },
148 0 : {
149 0 : devices: iface.Device ? [iface.Device] : [],
150 0 : fail_text: fmt_to_fragments(_("Adding $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{iface.Name}</b>),
151 0 : anyway_text: cockpit.format(_("Add $0"), iface.Name),
152 0 : hack_does_add_or_remove: true
153 0 : }
154 0 : );
155 0 : };
156 :
157 0 : return (
158 0 : <DropdownItem onClick={syn_click(model, onClick)}
159 0 : key={"add-member-" + iface.Name}
160 0 : component="button">
161 0 : {iface.Name}
162 0 : </DropdownItem>
163 : );
164 0 : })
165 : );
166 :
167 0 : const add_btn = (
168 0 : <Dropdown onSelect={() => setIsOpen(false)}
169 0 : toggle={(toggleRef) => (
170 0 : <MenuToggle id="add-member" ref={toggleRef} onClick={() => setIsOpen(!isOpen)}>
171 0 : {_("Add member")}
172 0 : </MenuToggle>
173 : )}
174 0 : isOpen={isOpen}
175 0 : popperProps={{ position: "right" }}
176 : >
177 0 : <DropdownList>
178 0 : {dropdownItems}
179 0 : </DropdownList>
180 0 : </Dropdown>
181 : );
182 :
183 0 : return (
184 0 : <Card isPlain id="network-interface-members" className="network-interface-members">
185 0 : <CardHeader actions={{ actions: add_btn }}>
186 0 : <CardTitle component="h2">{_("Interface members")}</CardTitle>
187 0 : </CardHeader>
188 0 : <ListingTable aria-label={_("Interface members")}
189 0 : className="networking-interface-members"
190 0 : variant='compact'
191 0 : columns={[
192 0 : { title: (cs && cs.type == "bond") ? _("Interfaces") : _("Ports"), props: { width: 25 } },
193 0 : { title: _("Sending"), props: { width: 25 } },
194 0 : { title: _("Receiving"), props: { width: 25 } },
195 0 : { title: "" },
196 0 : ]}
197 0 : rows={renderMemberRows()} />
198 0 : </Card>
199 : );
200 0 : };
|