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, { 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 35 : 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 13 : 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 5 : "data-sample-id": isActive ? encodeURIComponent(iface.Name) : null,
111 13 : "data-row-id": iface.Name,
112 13 : }
113 13 : });
114 :
115 13 : if (isActive) {
116 13 : 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 5 : } else {
120 5 : row.columns.splice(1, 0, { title: device_state_text() });
121 5 : row.columns.splice(1, 0, { title: "" });
122 5 : }
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 :
132 14 : const dropdownItems = (
133 14 : interfaces
134 14 : .filter(i => {
135 14 : return (is_interesting_interface(i) &&
136 14 : !memberIfaces[i.Name] &&
137 14 : i != iface);
138 14 : })
139 14 : .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 14 : return (
158 14 : <DropdownItem onClick={syn_click(model, onClick)}
159 14 : key={"add-member-" + iface.Name}
160 14 : component="button">
161 14 : {iface.Name}
162 14 : </DropdownItem>
163 : );
164 14 : })
165 : );
166 :
167 14 : const add_btn = (
168 0 : <Dropdown onSelect={() => setIsOpen(false)}
169 14 : toggle={(toggleRef) => (
170 0 : <MenuToggle id="add-member" ref={toggleRef} onClick={() => setIsOpen(!isOpen)}>
171 14 : {_("Add member")}
172 14 : </MenuToggle>
173 : )}
174 14 : isOpen={isOpen}
175 14 : popperProps={{ position: "right" }}
176 : >
177 14 : <DropdownList>
178 14 : {dropdownItems}
179 14 : </DropdownList>
180 14 : </Dropdown>
181 : );
182 :
183 14 : return (
184 14 : <Card isPlain id="network-interface-members" className="network-interface-members">
185 14 : <CardHeader actions={{ actions: add_btn }}>
186 14 : <CardTitle component="h2">{_("Interface members")}</CardTitle>
187 14 : </CardHeader>
188 14 : <ListingTable aria-label={_("Interface members")}
189 14 : className="networking-interface-members"
190 14 : variant='compact'
191 14 : columns={[
192 2 : { title: (cs && cs.type == "bond") ? _("Interfaces") : _("Ports"), props: { width: 25 } },
193 14 : { title: _("Sending"), props: { width: 25 } },
194 14 : { title: _("Receiving"), props: { width: 25 } },
195 14 : { title: "" },
196 14 : ]}
197 14 : rows={renderMemberRows()} />
198 14 : </Card>
199 : );
200 14 : };
|