Line data Source code
1 : /*
2 : * Copyright (C) 2021 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 35 : import cockpit from "cockpit";
7 35 : import React from 'react';
8 : import { useEvent } from "hooks";
9 :
10 : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
11 : import { Label } from "@patternfly/react-core/dist/esm/components/Label/index.js";
12 : import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
13 : import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
14 : import { Gallery } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js";
15 : import { Page, PageSection, } from "@patternfly/react-core/dist/esm/components/Page/index.js";
16 : import { ConnectedIcon } from "@patternfly/react-icons";
17 :
18 : import { FirewallSwitch } from "./firewall-switch.jsx";
19 : import { ListingTable } from "cockpit-components-table.jsx";
20 : import { NetworkAction } from "./dialogs-common.jsx";
21 : import { LogsPanel } from "cockpit-components-logs-panel.jsx";
22 : import { NetworkPlots } from "./plots";
23 : import { in_anaconda_mode } from "utils";
24 :
25 : import firewall from './firewall-client.js';
26 : import {
27 : device_state_text,
28 : is_managed,
29 : render_active_connection,
30 : } from './interfaces.js';
31 :
32 35 : const _ = cockpit.gettext;
33 :
34 34 : export const NetworkPage = ({ privileged, operationInProgress, usage_monitor, plot_state, interfaces }) => {
35 34 : useEvent(firewall, "changed");
36 34 : useEvent(usage_monitor.grid, "notify");
37 :
38 34 : const managed = [];
39 34 : const unmanaged = [];
40 34 : const plot_ifaces = [];
41 34 : let hasDetails = false;
42 :
43 34 : interfaces.forEach(iface => {
44 34 : function hasGroup(iface) {
45 34 : return ((iface.Device &&
46 34 : iface.Device.ActiveConnection &&
47 34 : iface.Device.ActiveConnection.Group &&
48 6 : iface.Device.ActiveConnection.Group.Members.length > 0) ||
49 34 : (iface.MainConnection &&
50 34 : iface.MainConnection.Groups.length > 0));
51 34 : }
52 :
53 : // Skip loopback
54 34 : if (iface.Name == "lo" || (iface.Device && iface.Device.DeviceType == 'loopback'))
55 34 : return;
56 :
57 : // Skip members
58 34 : if (hasGroup(iface))
59 34 : return;
60 :
61 34 : const dev = iface.Device;
62 34 : const show_traffic = (dev && (dev.State == 100 || dev.State == 10) && dev.Carrier === true);
63 :
64 34 : plot_ifaces.push(iface.Name);
65 34 : usage_monitor.add(iface.Name);
66 :
67 34 : const activeConnection = render_active_connection(dev, false, true);
68 34 : const row = {
69 34 : columns: [
70 17 : { title: (!dev || is_managed(dev)) ? <Button variant="link" isInline onClick={() => cockpit.location.go([iface.Name])}>{iface.Name}</Button> : iface.Name },
71 34 : { title: activeConnection },
72 34 : ],
73 34 : props: {
74 34 : key: iface.Name,
75 34 : "data-interface": encodeURIComponent(iface.Name),
76 34 : "data-sample-id": show_traffic ? encodeURIComponent(iface.Name) : null,
77 34 : "data-row-id": iface.Name,
78 34 : }
79 34 : };
80 :
81 34 : if (show_traffic) {
82 34 : const samples = usage_monitor.samples[iface.Name];
83 6 : row.columns.push({ title: samples ? cockpit.format_bits_per_sec(samples[1][0] * 8) : "" });
84 6 : row.columns.push({ title: samples ? cockpit.format_bits_per_sec(samples[0][0] * 8) : "" });
85 34 : } else {
86 34 : row.columns.push({ title: device_state_text(dev), props: { colSpan: 2 } });
87 34 : }
88 :
89 : // Details column: show type-specific information
90 34 : let detailsColumn = null;
91 6 : if (dev?.DeviceType === '802-11-wireless') {
92 6 : const networkCount = dev.visibleSsids.length;
93 6 : if (networkCount > 0 || dev.ActiveAccessPoint?.Ssid) {
94 6 : hasDetails = true;
95 6 : detailsColumn = (
96 6 : <Flex columnGap={{ default: 'columnGapSm' }}>
97 6 : {networkCount > 0 && (
98 6 : <FlexItem>
99 6 : <Label status="info">
100 6 : {cockpit.format(cockpit.ngettext("$0 network", "$0 networks", networkCount), networkCount)}
101 6 : </Label>
102 6 : </FlexItem>
103 : )}
104 6 : {dev.ActiveAccessPoint?.Ssid && (
105 6 : <FlexItem>
106 6 : <Label status="success" icon={<ConnectedIcon />}>{dev.ActiveAccessPoint?.Ssid}</Label>
107 6 : </FlexItem>
108 : )}
109 6 : </Flex>
110 : );
111 6 : }
112 6 : }
113 34 : row.columns.push({ title: detailsColumn });
114 :
115 34 : if (!dev || is_managed(dev)) {
116 34 : managed.push(row);
117 24 : } else {
118 24 : unmanaged.push(row);
119 24 : }
120 34 : });
121 :
122 34 : const rx_plot_data = {
123 34 : direct: "network.interface.in.bytes",
124 34 : internal: "network.interface.rx",
125 34 : units: "bytes",
126 34 : derive: "rate",
127 34 : threshold: 200,
128 34 : factor: 8
129 34 : };
130 :
131 34 : const tx_plot_data = {
132 34 : direct: "network.interface.out.bytes",
133 34 : internal: "network.interface.tx",
134 34 : units: "bytes",
135 34 : derive: "rate",
136 34 : threshold: 200,
137 34 : factor: 8
138 34 : };
139 :
140 34 : plot_state.plot_instances('rx', rx_plot_data, plot_ifaces);
141 34 : plot_state.plot_instances('tx', tx_plot_data, plot_ifaces);
142 :
143 : /* Start of properties for the LogsPanel component */
144 34 : const match = [
145 34 : "_SYSTEMD_UNIT=NetworkManager.service", "+",
146 34 : "_SYSTEMD_UNIT=firewalld.service"
147 34 : ];
148 34 : const search_options = {
149 34 : prio: "debug",
150 34 : _SYSTEMD_UNIT: "NetworkManager.service,firewalld.service"
151 34 : };
152 34 : const url = "/system/logs/#/?prio=debug&_SYSTEMD_UNIT=NetworkManager.service,firewalld.service";
153 : /* End of properties for the LogsPanel component */
154 :
155 34 : const actions = privileged && (
156 33 : <>
157 33 : <NetworkAction buttonText={_("Add VPN")} type='wg' />
158 33 : <NetworkAction buttonText={_("Add bond")} type='bond' />
159 33 : <NetworkAction buttonText={_("Add team")} type='team' />
160 33 : <NetworkAction buttonText={_("Add bridge")} type='bridge' />
161 33 : <NetworkAction buttonText={_("Add VLAN")} type='vlan' />
162 33 : </>
163 : );
164 :
165 34 : const anaconda = in_anaconda_mode();
166 :
167 34 : return (
168 6 : <Page data-test-wait={operationInProgress} id="networking" className={"pf-m-no-sidebar" + (anaconda ? " anaconda" : "")}>
169 34 : <PageSection hasBodyWrapper={false} id="networking-graphs" className="networking-graphs">
170 34 : <NetworkPlots plot_state={plot_state} />
171 34 : </PageSection>
172 34 : <PageSection hasBodyWrapper={false}>
173 34 : <Gallery hasGutter>
174 34 : {firewall.installed && <Card isPlain id="networking-firewall-summary">
175 34 : <CardHeader actions={{
176 34 : actions: <Button variant="secondary" id="networking-firewall-link"
177 34 : component="a"
178 1 : onClick={() => cockpit.jump("/network/firewall", cockpit.transport.host)}>
179 34 : {_("Edit rules and zones")}
180 34 : </Button>,
181 34 : }}>
182 34 : <Flex gap={{ default: 'gapMd' }} alignItems={{ default: 'alignItemsCenter' }}>
183 34 : <CardTitle component="h2">{_("Firewall")}</CardTitle>
184 34 : <FirewallSwitch firewall={firewall} />
185 34 : </Flex>
186 34 : </CardHeader>
187 34 : <CardBody>
188 34 : <Button variant="link"
189 34 : component="a"
190 34 : isInline
191 0 : onClick={() => cockpit.jump("/network/firewall", cockpit.transport.host)}>
192 34 : {cockpit.format(cockpit.ngettext("$0 active zone", "$0 active zones", firewall.activeZones.size), firewall.activeZones.size)}
193 34 : </Button>
194 34 : </CardBody>
195 34 : </Card>}
196 34 : <Card isPlain id="networking-interfaces">
197 34 : <CardHeader actions={{ actions }}>
198 34 : <CardTitle component="h2">{_("Interfaces")}</CardTitle>
199 34 : </CardHeader>
200 34 : <ListingTable aria-label={_("Managed interfaces")}
201 34 : variant='compact'
202 34 : columns={[
203 6 : { title: _("Name"), header: true, props: { width: hasDetails ? 15 : 25 } },
204 6 : { title: _("IP address"), props: { width: hasDetails ? 35 : 25 } },
205 6 : { title: _("Sending"), props: { width: hasDetails ? 15 : 25 } },
206 6 : { title: _("Receiving"), props: { width: hasDetails ? 15 : 25 } },
207 6 : ...(hasDetails ? [{ title: _("Details"), props: { width: 20 } }] : []),
208 34 : ]}
209 34 : rows={managed} />
210 34 : </Card>
211 34 : {unmanaged.length > 0 &&
212 24 : <Card isPlain id="networking-unmanaged-interfaces">
213 24 : <CardHeader>
214 24 : <CardTitle component="h2">{_("Unmanaged interfaces")}</CardTitle>
215 24 : </CardHeader>
216 24 : <ListingTable aria-label={_("Unmanaged interfaces")}
217 24 : variant='compact'
218 24 : columns={[
219 6 : { title: _("Name"), header: true, props: { width: hasDetails ? 15 : 25 } },
220 6 : { title: _("IP address"), props: { width: hasDetails ? 35 : 25 } },
221 6 : { title: _("Sending"), props: { width: hasDetails ? 15 : 25 } },
222 6 : { title: _("Receiving"), props: { width: hasDetails ? 15 : 25 } },
223 6 : ...(hasDetails ? [{ title: _("Details"), props: { width: 20 } }] : []),
224 24 : ]}
225 24 : rows={unmanaged} />
226 24 : </Card>}
227 34 : <LogsPanel title={_("Network logs")} match={match}
228 34 : max={10} search_options={search_options}
229 34 : goto_url={url}
230 34 : className="contains-list" />
231 34 : </Gallery>
232 34 : </PageSection>
233 34 : </Page>
234 : );
235 34 : };
|