LCOV - code coverage report
Current view: top level - pkg/storaged - storage-controls.jsx Coverage Total Hit
Test: cockpit Lines: 92.1 % 178 164
Test Date: 2026-06-25 09:20:42

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2016 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6          113 : import React, { useState } from 'react';
       7              : 
       8              : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
       9              : import { Dropdown, DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/index.js';
      10              : import { MenuToggle } from '@patternfly/react-core/dist/esm/components/MenuToggle/index.js';
      11              : import { Tooltip, TooltipPosition } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
      12              : import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
      13              : import { Icon } from "@patternfly/react-core/dist/esm/components/Icon/index.js";
      14              : import { BarsIcon, EllipsisVIcon } from '@patternfly/react-icons';
      15              : 
      16          113 : import cockpit from "cockpit";
      17              : import * as utils from "./utils.js";
      18              : import client from "./client.js";
      19              : 
      20              : import { dialog_open } from "./dialog.jsx";
      21              : 
      22          113 : const _ = cockpit.gettext;
      23              : 
      24              : /* StorageControl - a button or similar that triggers
      25              :  *                  a privileged action.
      26              :  *
      27              :  * It can be disabled and will show a tooltip then.  It will
      28              :  * automatically disappear when the logged in user doesn't
      29              :  * have permission.
      30              :  *
      31              :  * Properties:
      32              :  *
      33              :  * - excuse:  If set, the button/link is disabled and will show the
      34              :  *            excuse in a tooltip.
      35              :  */
      36              : 
      37          113 : class StorageControl extends React.Component {
      38           85 :     render() {
      39           85 :         const excuse = this.props.excuse;
      40           85 :         if (!client.superuser.allowed)
      41            8 :             return <div />;
      42              : 
      43           55 :         if (excuse) {
      44           55 :             return (
      45           55 :                 <Tooltip id="tip-storage" content={excuse}
      46           55 :                          position={this.props.excuse_placement || TooltipPosition.top}>
      47           55 :                     <span>
      48           55 :                         { this.props.content(excuse) }
      49           55 :                     </span>
      50           55 :                 </Tooltip>
      51              :             );
      52           55 :         } else {
      53           85 :             return this.props.content();
      54           85 :         }
      55           85 :     }
      56          113 : }
      57              : 
      58          107 : function checked(callback, setSpinning, excuse) {
      59           93 :     return function (event) {
      60           93 :         if (!event)
      61           93 :             return;
      62              : 
      63              :         // only consider primary mouse button for clicks
      64           93 :         if (event.type === 'click' && event.button !== 0)
      65           93 :             return;
      66              : 
      67              :         // only consider enter button for keyboard events
      68            3 :         if (event.type === 'KeyDown' && event.key !== "Enter")
      69           93 :             return;
      70              : 
      71           93 :         event.stopPropagation();
      72              : 
      73            3 :         if (excuse) {
      74            3 :             dialog_open({
      75            3 :                 Title: _("Sorry"),
      76            3 :                 Body: excuse
      77            3 :             });
      78            3 :             return;
      79            3 :         }
      80              : 
      81           93 :         const promise = client.run(callback);
      82           93 :         if (promise) {
      83           93 :             if (setSpinning)
      84           54 :                 setSpinning(true);
      85           93 :             promise.finally(() => {
      86           93 :                 if (setSpinning)
      87           54 :                     setSpinning(false);
      88           93 :             });
      89            0 :             promise.catch(function (error) {
      90            0 :                 console.warn(error.toString());
      91            0 :                 dialog_open({
      92            0 :                     Title: _("Error"),
      93            0 :                     Body: error.toString()
      94            0 :                 });
      95            0 :             });
      96           93 :         }
      97           93 :     };
      98          107 : }
      99              : 
     100           81 : export const StorageButton = ({ id, kind, excuse, onClick, children, ariaLabel, spinner }) => {
     101           81 :     const [spinning, setSpinning] = useState(false);
     102              : 
     103           81 :     return <StorageControl excuse={excuse}
     104           81 :                            content={excuse => (
     105           81 :                                <Button id={id}
     106           81 :                                        aria-label={ariaLabel}
     107           81 :                                        onClick={checked(onClick, setSpinning)}
     108           81 :                                        variant={kind || "secondary"}
     109           11 :                                        isDisabled={!!excuse || (spinner && spinning)}
     110           11 :                                        isLoading={spinner ? spinning : undefined}>
     111           81 :                                    {children}
     112           81 :                                </Button>
     113           81 :                            )} />;
     114           81 : };
     115              : 
     116           81 : export const StorageLink = ({ id, excuse, onClick, children }) => (
     117           81 :     <StorageControl excuse={excuse}
     118           81 :                     content={excuse => (
     119           81 :                         <Button onClick={checked(onClick)}
     120           81 :                                 variant="link"
     121           81 :                                 isInline
     122           81 :                                 isDisabled={!!excuse}>
     123           81 :                             {children}
     124           81 :                         </Button>
     125           81 :                     )} />
     126              : );
     127              : 
     128              : // StorageOnOff - OnOff switch for asynchronous actions.
     129              : //
     130              : 
     131          113 : export class StorageOnOff extends React.Component {
     132           17 :     constructor() {
     133           17 :         super();
     134           17 :         this.state = { promise: null };
     135           17 :     }
     136              : 
     137           17 :     render() {
     138           17 :         const self = this;
     139              : 
     140            2 :         function onChange(_event, val) {
     141            2 :             const promise = self.props.onChange(val);
     142            2 :             if (promise) {
     143            0 :                 promise.catch(error => {
     144            0 :                     dialog_open({
     145            0 :                         Title: _("Error"),
     146            0 :                         Body: error.toString()
     147            0 :                     });
     148            0 :                 })
     149            2 :                         .finally(() => { self.setState({ promise: null }) });
     150            2 :             }
     151              : 
     152            2 :             self.setState({ promise, promise_goal_state: val });
     153            2 :         }
     154              : 
     155           17 :         return (
     156           17 :             <StorageControl excuse={this.props.excuse}
     157           17 :                             content={(excuse) => (
     158           17 :                                 <Switch isChecked={this.state.promise
     159            6 :                                     ? this.state.promise_goal_state
     160           17 :                                     : this.props.state}
     161           17 :                                                  aria-label={this.props['aria-label']}
     162           16 :                                                  isDisabled={!!(excuse || this.state.promise)}
     163           17 :                                                  onChange={onChange} />
     164           17 :                             )} />
     165              :         );
     166           17 :     }
     167          113 : }
     168              : 
     169              : /* Render a usage bar showing props.stats[0] out of props.stats[1]
     170              :  * bytes in use.  If the ratio is above props.critical, the bar will be
     171              :  * in a dangerous color.
     172              :  */
     173              : 
     174          101 : export const StorageUsageBar = ({ stats, critical, block, offset, total, short }) => {
     175          101 :     if (!stats)
     176           13 :         return null;
     177              : 
     178          101 :     const fraction = stats[0] / stats[1];
     179          101 :     const off_fraction = offset / stats[1];
     180          101 :     const total_fraction = total / stats[1];
     181          101 :     const labelText = utils.format_fsys_usage(stats[0], stats[1]);
     182              : 
     183          101 :     return (
     184          101 :         <div>
     185          101 :             <span className="usage-text pf-v6-u-text-nowrap">
     186          101 :                 {labelText}
     187          101 :             </span>
     188           10 :             <div className={"usage-bar" + (fraction > critical ? " usage-bar-danger" : "") + (short ? " usage-bar-short" : "")}
     189          101 :                  role="progressbar"
     190          101 :                  aria-valuemin={0} aria-valuemax={stats[1]} aria-valuenow={stats[0]}
     191          101 :                  aria-label={cockpit.format(_("Usage of $0"), block)}
     192          101 :                  aria-valuetext={labelText}>
     193          101 :                 <div className="usage-bar-indicator usage-bar-other" aria-hidden="true" style={{ width: total_fraction * 100 + "%" }} />
     194          101 :                 <div className="usage-bar-indicator" style={{ insetInlineStart: off_fraction * 100 + "%", width: fraction * 100 + "%" }} />
     195          101 :             </div>
     196          101 :         </div>);
     197          101 : };
     198              : 
     199              : /* Render a static size that goes well with a short StorageusageBar in
     200              :    the same table column, and also works well with the tests.
     201              : */
     202              : 
     203          104 : export const StorageSize = ({ size }) => {
     204          104 :     return (
     205          104 :         <div>
     206          104 :             <span className="usage-text pf-v6-u-text-nowrap">
     207          104 :                 {utils.fmt_size(size)}
     208          104 :             </span>
     209          104 :             <div className="usage-bar usage-bar-short usage-bar-empty" />
     210          104 :         </div>);
     211          104 : };
     212              : 
     213           83 : export const StorageMenuItem = ({ onClick, danger, excuse, children, isDisabled }) => (
     214           44 :     <DropdownItem className={danger && !excuse ? " delete-resource-dangerous" : ""}
     215           83 :                   description={excuse}
     216           83 :                   isDisabled={isDisabled || !!excuse}
     217           83 :                   onClick={checked(onClick, null, excuse)}>
     218           83 :         {children}
     219           83 :     </DropdownItem>
     220              : );
     221              : 
     222          112 : export const StorageBarMenu = ({ label, isKebab, menuItems }) => {
     223          112 :     const [isOpen, setIsOpen] = useState(false);
     224              : 
     225          112 :     if (!client.superuser.allowed)
     226           13 :         return null;
     227              : 
     228           83 :     const onToggleClick = (event) => {
     229           83 :         setIsOpen(!isOpen);
     230           83 :     };
     231              : 
     232           82 :     const onSelect = (event) => {
     233           82 :         setIsOpen(false);
     234           82 :     };
     235              : 
     236          112 :     const toggle = ref => (
     237          112 :         <MenuToggle
     238          112 :             ref={ref}
     239          112 :             variant="plain"
     240          112 :             onClick={onToggleClick}
     241          112 :             isExpanded={isOpen}
     242          112 :             aria-label={label}>
     243          112 :             {isKebab ? <EllipsisVIcon /> : <Icon size="lg"><BarsIcon /></Icon>}
     244          112 :         </MenuToggle>
     245              :     );
     246              : 
     247          112 :     return (
     248          112 :         <Dropdown isOpen={isOpen}
     249          112 :                   onSelect={onSelect}
     250            0 :                   onOpenChange={isOpen => setIsOpen(isOpen)}
     251          112 :                   toggle={toggle}
     252          112 :                   popperProps={{ position: "right" }}
     253          112 :                   shouldFocusToggleOnSelect>
     254          112 :             {menuItems}
     255          112 :         </Dropdown>);
     256          112 : };
        

Generated by: LCOV version 2.0-1