LCOV - code coverage report
Current view: top level - pkg/networkmanager - network-interface-members.jsx Coverage Total Hit
Test: cockpit Lines: 2.0 % 152 3
Test Date: 2026-06-17 06:28:00

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2021 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5            1 : import cockpit from "cockpit";
       6            1 : 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            1 : const _ = cockpit.gettext;
      32              : 
      33            0 : export const NetworkInterfaceMembers = ({
      34            0 :     members,
      35            0 :     memberIfaces,
      36            0 :     interfaces,
      37            0 :     iface,
      38            0 :     usage_monitor,
      39            0 :     privileged
      40            0 : }) => {
      41            0 :     const model = useContext(ModelContext);
      42            0 :     const [isOpen, setIsOpen] = useState(false);
      43            0 :     useEvent(usage_monitor.grid, "notify");
      44              : 
      45            0 :     function renderMemberRows() {
      46            0 :         const rows = [];
      47              : 
      48            0 :         members.forEach(iface => {
      49            0 :             const member_con = iface.MainConnection;
      50            0 :             const dev = iface.Device;
      51            0 :             const isActive = (dev && dev.State == 100 && dev.Carrier === true);
      52            0 :             const onoff = (
      53            0 :                 <Switch
      54            0 :                     aria-label={cockpit.format(_("Switch off $0"), iface.Name)}
      55            0 :                     isDisabled={!privileged}
      56            0 :                     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            0 :             const row = ({
      81            0 :                 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            0 :                     {
      85            0 :                         title: (
      86            0 :                             <div className="btn-group">
      87            0 :                                 {onoff}
      88            0 :                                 {privileged && <Button icon={<MinusIcon />} variant="secondary"
      89            0 :                                     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            0 :                             </div>
     103              :                         ),
     104            0 :                         props: { className: "pf-v6-c-table__action" }
     105            0 :                     },
     106            0 :                 ],
     107            0 :                 props: {
     108            0 :                     key: iface.Name,
     109            0 :                     "data-interface": encodeURIComponent(iface.Name),
     110            0 :                     "data-sample-id": isActive ? encodeURIComponent(iface.Name) : null,
     111            0 :                     "data-row-id": iface.Name,
     112            0 :                 }
     113            0 :             });
     114              : 
     115            0 :             if (isActive) {
     116            0 :                 const samples = usage_monitor.samples[iface.Name];
     117            0 :                 row.columns.splice(1, 0, { title: samples ? cockpit.format_bits_per_sec(samples[1][0] * 8) : "" });
     118            0 :                 row.columns.splice(2, 0, { title: samples ? cockpit.format_bits_per_sec(samples[0][0] * 8) : "" });
     119            0 :             } else {
     120            0 :                 row.columns.splice(1, 0, { title: device_state_text() });
     121            0 :                 row.columns.splice(1, 0, { title: "" });
     122            0 :             }
     123              : 
     124            0 :             rows.push(row);
     125            0 :         });
     126            0 :         return rows;
     127            0 :     }
     128              : 
     129            0 :     const main_connection = iface.MainConnection;
     130            0 :     const cs = iface.MainConnection && connection_settings(iface.MainConnection);
     131              : 
     132            0 :     const dropdownItems = (
     133            0 :         interfaces
     134            0 :                 .filter(i => {
     135            0 :                     return (is_interesting_interface(i) &&
     136            0 :                         !memberIfaces[i.Name] &&
     137            0 :                         i != iface);
     138            0 :                 })
     139            0 :                 .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            0 :                     return (
     158            0 :                         <DropdownItem onClick={syn_click(model, onClick)}
     159            0 :                                   key={"add-member-" + iface.Name}
     160            0 :                                   component="button">
     161            0 :                             {iface.Name}
     162            0 :                         </DropdownItem>
     163              :                     );
     164            0 :                 })
     165              :     );
     166              : 
     167            0 :     const add_btn = (
     168            0 :         <Dropdown onSelect={() => setIsOpen(false)}
     169            0 :                   toggle={(toggleRef) => (
     170            0 :                       <MenuToggle id="add-member" ref={toggleRef} onClick={() => setIsOpen(!isOpen)}>
     171            0 :                           {_("Add member")}
     172            0 :                       </MenuToggle>
     173              :                   )}
     174            0 :                   isOpen={isOpen}
     175            0 :                   popperProps={{ position: "right" }}
     176              :         >
     177            0 :             <DropdownList>
     178            0 :                 {dropdownItems}
     179            0 :             </DropdownList>
     180            0 :         </Dropdown>
     181              :     );
     182              : 
     183            0 :     return (
     184            0 :         <Card isPlain id="network-interface-members" className="network-interface-members">
     185            0 :             <CardHeader actions={{ actions: add_btn }}>
     186            0 :                 <CardTitle component="h2">{_("Interface members")}</CardTitle>
     187            0 :             </CardHeader>
     188            0 :             <ListingTable aria-label={_("Interface members")}
     189            0 :                           className="networking-interface-members"
     190            0 :                           variant='compact'
     191            0 :                           columns={[
     192            0 :                               { title: (cs && cs.type == "bond") ? _("Interfaces") : _("Ports"), props: { width: 25 } },
     193            0 :                               { title: _("Sending"), props: { width: 25 } },
     194            0 :                               { title: _("Receiving"), props: { width: 25 } },
     195            0 :                               { title: "" },
     196            0 :                           ]}
     197            0 :                           rows={renderMemberRows()} />
     198            0 :         </Card>
     199              :     );
     200            0 : };
        

Generated by: LCOV version 2.0-1