LCOV - code coverage report
Current view: top level - pkg/networkmanager - network-interface-members.jsx Coverage Total Hit
Test: cockpit Lines: 65.8 % 152 100
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           35 : import cockpit from "cockpit";
       6           35 : 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           35 : 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            5 :                     "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            5 :             } else {
     120            5 :                 row.columns.splice(1, 0, { title: device_state_text() });
     121            5 :                 row.columns.splice(1, 0, { title: "" });
     122            5 :             }
     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              : 
     132           14 :     const dropdownItems = (
     133           14 :         interfaces
     134           14 :                 .filter(i => {
     135           14 :                     return (is_interesting_interface(i) &&
     136           14 :                         !memberIfaces[i.Name] &&
     137           14 :                         i != iface);
     138           14 :                 })
     139           14 :                 .map(iface => {
     140            0 :                     const onClick = () => {
     141            0 :                         with_checkpoint(
     142            0 :                             model,
     143            0 :                             () => {
     144            0 :                                 return set_member(model, main_connection, main_connection.Settings,
     145            0 :                                                   cs.type, iface.Name, true)
     146            0 :                                         .catch(show_unexpected_error);
     147            0 :                             },
     148            0 :                             {
     149            0 :                                 devices: iface.Device ? [iface.Device] : [],
     150            0 :                                 fail_text: fmt_to_fragments(_("Adding $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{iface.Name}</b>),
     151            0 :                                 anyway_text: cockpit.format(_("Add $0"), iface.Name),
     152            0 :                                 hack_does_add_or_remove: true
     153            0 :                             }
     154            0 :                         );
     155            0 :                     };
     156              : 
     157           14 :                     return (
     158           14 :                         <DropdownItem onClick={syn_click(model, onClick)}
     159           14 :                                   key={"add-member-" + iface.Name}
     160           14 :                                   component="button">
     161           14 :                             {iface.Name}
     162           14 :                         </DropdownItem>
     163              :                     );
     164           14 :                 })
     165              :     );
     166              : 
     167           14 :     const add_btn = (
     168            0 :         <Dropdown onSelect={() => setIsOpen(false)}
     169           14 :                   toggle={(toggleRef) => (
     170            0 :                       <MenuToggle id="add-member" ref={toggleRef} onClick={() => setIsOpen(!isOpen)}>
     171           14 :                           {_("Add member")}
     172           14 :                       </MenuToggle>
     173              :                   )}
     174           14 :                   isOpen={isOpen}
     175           14 :                   popperProps={{ position: "right" }}
     176              :         >
     177           14 :             <DropdownList>
     178           14 :                 {dropdownItems}
     179           14 :             </DropdownList>
     180           14 :         </Dropdown>
     181              :     );
     182              : 
     183           14 :     return (
     184           14 :         <Card isPlain id="network-interface-members" className="network-interface-members">
     185           14 :             <CardHeader actions={{ actions: add_btn }}>
     186           14 :                 <CardTitle component="h2">{_("Interface members")}</CardTitle>
     187           14 :             </CardHeader>
     188           14 :             <ListingTable aria-label={_("Interface members")}
     189           14 :                           className="networking-interface-members"
     190           14 :                           variant='compact'
     191           14 :                           columns={[
     192            2 :                               { title: (cs && cs.type == "bond") ? _("Interfaces") : _("Ports"), props: { width: 25 } },
     193           14 :                               { title: _("Sending"), props: { width: 25 } },
     194           14 :                               { title: _("Receiving"), props: { width: 25 } },
     195           14 :                               { title: "" },
     196           14 :                           ]}
     197           14 :                           rows={renderMemberRows()} />
     198           14 :         </Card>
     199              :     );
     200           14 : };
        

Generated by: LCOV version 2.0-1