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-08-04 16:34:20

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

Generated by: LCOV version 2.0-1