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