LCOV - code coverage report
Current view: top level - pkg/networkmanager - network-interface-members.jsx Coverage Total Hit
Test: cockpit Lines: 66.7 % 156 104
Test Date: 2026-06-25 11:17:56

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2021 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5           37 : import cockpit from "cockpit";
       6           37 : import React, { useState, useContext } from "react";
       7              : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
       8              : import { Card, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
       9              : import { Dropdown, DropdownItem, DropdownList } from '@patternfly/react-core/dist/esm/components/Dropdown/index.js';
      10              : import { MenuToggle } from "@patternfly/react-core/dist/esm/components/MenuToggle";
      11              : import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
      12              : import { MinusIcon } from '@patternfly/react-icons';
      13              : 
      14              : import { ListingTable } from "cockpit-components-table.jsx";
      15              : import { ModelContext } from './model-context.jsx';
      16              : import { useEvent } from "hooks";
      17              : 
      18              : import {
      19              :     connection_settings,
      20              :     device_state_text,
      21              :     free_member_connection,
      22              :     is_interesting_interface,
      23              :     set_member,
      24              :     show_unexpected_error,
      25              :     syn_click,
      26              :     with_checkpoint,
      27              :     is_managed,
      28              : } from './interfaces.js';
      29              : import { fmt_to_fragments } from 'utils.jsx';
      30              : 
      31           37 : const _ = cockpit.gettext;
      32              : 
      33           14 : export const NetworkInterfaceMembers = ({
      34           14 :     members,
      35           14 :     memberIfaces,
      36           14 :     interfaces,
      37           14 :     iface,
      38           14 :     usage_monitor,
      39           14 :     privileged
      40           14 : }) => {
      41           14 :     const model = useContext(ModelContext);
      42           14 :     const [isOpen, setIsOpen] = useState(false);
      43           14 :     useEvent(usage_monitor.grid, "notify");
      44              : 
      45           14 :     function renderMemberRows() {
      46           14 :         const rows = [];
      47              : 
      48           13 :         members.forEach(iface => {
      49           13 :             const member_con = iface.MainConnection;
      50           13 :             const dev = iface.Device;
      51           13 :             const isActive = (dev && dev.State == 100 && dev.Carrier === true);
      52           13 :             const onoff = (
      53           13 :                 <Switch
      54           13 :                     aria-label={cockpit.format(_("Switch off $0"), iface.Name)}
      55           13 :                     isDisabled={!privileged}
      56           13 :                     isChecked={!!(dev && dev.ActiveConnection)}
      57            0 :                     onChange={(_event, val) => {
      58            0 :                         if (val) {
      59            0 :                             with_checkpoint(
      60            0 :                                 model,
      61            0 :                                 () => member_con.activate(dev).catch(show_unexpected_error),
      62            0 :                                 {
      63            0 :                                     devices: dev ? [dev] : [],
      64            0 :                                     fail_text: fmt_to_fragments(_("Switching on $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{iface.Name}</b>),
      65            0 :                                     anyway_text: cockpit.format(_("Switch on $0"), iface.Name)
      66            0 :                                 });
      67            0 :                         } else if (dev) {
      68            0 :                             with_checkpoint(
      69            0 :                                 model,
      70            0 :                                 () => dev.disconnect().catch(show_unexpected_error),
      71            0 :                                 {
      72            0 :                                     devices: [dev],
      73            0 :                                     fail_text: fmt_to_fragments(_("Switching off $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{iface.Name}</b>),
      74            0 :                                     anyway_text: cockpit.format(_("Switch off $0"), iface.Name)
      75            0 :                                 });
      76            0 :                         }
      77            0 :                     } } />
      78              :             );
      79              : 
      80           13 :             const row = ({
      81           13 :                 columns: [
      82            0 :                     { title: (!dev || is_managed(dev)) ? <Button variant="link" isInline onClick={() => cockpit.location.go([iface.Name])}>{iface.Name}</Button> : iface.Name },
      83              :                     // Will add traffic info right after
      84           13 :                     {
      85           13 :                         title: (
      86           13 :                             <div className="btn-group">
      87           13 :                                 {onoff}
      88           13 :                                 {privileged && <Button icon={<MinusIcon />} variant="secondary"
      89           13 :                                     size="sm"
      90            0 :                                     onClick={syn_click(model, () => {
      91            0 :                                         with_checkpoint(
      92            0 :                                             model,
      93            0 :                                             () => free_member_connection(member_con).catch(show_unexpected_error),
      94            0 :                                             {
      95            0 :                                                 devices: dev ? [dev] : [],
      96            0 :                                                 fail_text: fmt_to_fragments(_("Removing $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{iface.Name}</b>),
      97            0 :                                                 anyway_text: cockpit.format(_("Remove $0"), iface.Name),
      98            0 :                                                 hack_does_add_or_remove: true
      99            0 :                                             });
     100            0 :                                         return false;
     101            0 :                                     })} />}
     102           13 :                             </div>
     103              :                         ),
     104           13 :                         props: { className: "pf-v6-c-table__action" }
     105           13 :                     },
     106           13 :                 ],
     107           13 :                 props: {
     108           13 :                     key: iface.Name,
     109           13 :                     "data-interface": encodeURIComponent(iface.Name),
     110            6 :                     "data-sample-id": isActive ? encodeURIComponent(iface.Name) : null,
     111           13 :                     "data-row-id": iface.Name,
     112           13 :                 }
     113           13 :             });
     114              : 
     115           13 :             if (isActive) {
     116           13 :                 const samples = usage_monitor.samples[iface.Name];
     117            2 :                 row.columns.splice(1, 0, { title: samples ? cockpit.format_bits_per_sec(samples[1][0] * 8) : "" });
     118            2 :                 row.columns.splice(2, 0, { title: samples ? cockpit.format_bits_per_sec(samples[0][0] * 8) : "" });
     119            6 :             } else {
     120            6 :                 row.columns.splice(1, 0, { title: device_state_text() });
     121            6 :                 row.columns.splice(1, 0, { title: "" });
     122            6 :             }
     123              : 
     124           13 :             rows.push(row);
     125           13 :         });
     126           14 :         return rows;
     127           14 :     }
     128              : 
     129           14 :     const main_connection = iface.MainConnection;
     130           14 :     const cs = iface.MainConnection && connection_settings(iface.MainConnection);
     131           14 :     const dropdownItems = [];
     132              : 
     133           14 :     interfaces.forEach(i => {
     134           14 :         if (is_interesting_interface(i) && !memberIfaces[i.Name] && i != iface) {
     135            0 :             const onClick = () => {
     136            0 :                 with_checkpoint(
     137            0 :                     model,
     138            0 :                     () => {
     139            0 :                         return set_member(model, main_connection, main_connection.Settings,
     140            0 :                                           cs.type, i.Name, true)
     141            0 :                                 .catch(show_unexpected_error);
     142            0 :                     },
     143            0 :                     {
     144            0 :                         devices: i.Device ? [i.Device] : [],
     145            0 :                         fail_text: fmt_to_fragments(_("Adding $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{i.Name}</b>),
     146            0 :                         anyway_text: cockpit.format(_("Add $0"), i.Name),
     147            0 :                         hack_does_add_or_remove: true
     148            0 :                     }
     149            0 :                 );
     150            0 :             };
     151              : 
     152           14 :             dropdownItems.push(
     153           14 :                 <DropdownItem onClick={syn_click(model, onClick)}
     154           14 :                               key={"add-member-" + i.Name}
     155           14 :                               component="button">
     156           14 :                     {i.Name}
     157           14 :                 </DropdownItem>
     158           14 :             );
     159           14 :         }
     160           14 :     });
     161              : 
     162              :     // We want to show a Tooltip but that isn't supported by PatternFly.
     163              :     // https://github.com/patternfly/patternfly-react/issues/12500
     164            2 :     if (dropdownItems.length === 0) {
     165            2 :         dropdownItems.push(
     166            2 :             <DropdownItem isDisabled isAriaDisabled>
     167            2 :                 {_("No intefaces available")}
     168            2 :             </DropdownItem>
     169            2 :         );
     170            2 :     }
     171              : 
     172           14 :     const add_btn = (
     173            0 :         <Dropdown onSelect={() => setIsOpen(false)}
     174           14 :                   toggle={(toggleRef) => (
     175            0 :                       <MenuToggle id="add-member" ref={toggleRef} onClick={() => setIsOpen(!isOpen)}>
     176           14 :                           {_("Add member")}
     177           14 :                       </MenuToggle>
     178              :                   )}
     179           14 :                   isOpen={isOpen}
     180           14 :                   popperProps={{ position: "right" }}
     181              :         >
     182           14 :             <DropdownList>
     183           14 :                 {dropdownItems}
     184           14 :             </DropdownList>
     185           14 :         </Dropdown>
     186              :     );
     187              : 
     188           14 :     return (
     189           14 :         <Card isPlain id="network-interface-members" className="network-interface-members">
     190           14 :             <CardHeader actions={{ actions: add_btn }}>
     191           14 :                 <CardTitle component="h2">{_("Interface members")}</CardTitle>
     192           14 :             </CardHeader>
     193           14 :             <ListingTable aria-label={_("Interface members")}
     194           14 :                           className="networking-interface-members"
     195           14 :                           variant='compact'
     196           14 :                           columns={[
     197            2 :                               { title: (cs && cs.type == "bond") ? _("Interfaces") : _("Ports"), props: { width: 25 } },
     198           14 :                               { title: _("Sending"), props: { width: 25 } },
     199           14 :                               { title: _("Receiving"), props: { width: 25 } },
     200           14 :                               { title: "" },
     201           14 :                           ]}
     202           14 :                           rows={renderMemberRows()} />
     203           14 :         </Card>
     204              :     );
     205           14 : };
        

Generated by: LCOV version 2.0-1