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