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