LCOV - code coverage report
Current view: top level - pkg/networkmanager - network-main.jsx Coverage Total Hit
Test: cockpit Lines: 99.4 % 171 170
Test Date: 2026-06-01 17:00:21

            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 : };
        

Generated by: LCOV version 2.0-1