LCOV - code coverage report
Current view: top level - pkg/lib - cockpit-components-modifications.tsx Coverage Total Hit
Test: cockpit Lines: 41.4 % 116 48
Test Date: 2026-07-02 14:11:36

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2019 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6            2 : import React from 'react';
       7              : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
       8              : import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
       9              : import { DataList, DataListCell, DataListItem, DataListItemCells, DataListItemRow } from "@patternfly/react-core/dist/esm/components/DataList/index.js";
      10              : import {
      11              :     Modal, ModalBody, ModalFooter, ModalHeader
      12              : } from '@patternfly/react-core/dist/esm/components/Modal/index.js';
      13              : import { Tab, Tabs } from "@patternfly/react-core/dist/esm/components/Tabs/index.js";
      14              : import { TextArea } from "@patternfly/react-core/dist/esm/components/TextArea/index.js";
      15              : import { Icon } from "@patternfly/react-core/dist/esm/components/Icon/index.js";
      16              : import { CheckIcon, CopyIcon, ExternalLinkAltIcon, OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
      17              : 
      18            2 : import cockpit from "cockpit";
      19              : import 'cockpit-components-modifications.css';
      20              : 
      21            2 : const _ = cockpit.gettext;
      22              : 
      23              : /* Dialog for showing scripts to modify system
      24              :  *
      25              :  * Enables showing shell and ansible script. Shell one is mandatory and ansible one can be omitted.
      26              :  *
      27              :  */
      28            0 : export const ModificationsExportDialog = ({ onClose, shell, ansible }: {
      29              :     onClose: () => void;
      30              :     shell: string;
      31              :     ansible?: string | undefined;
      32            0 : }) => {
      33            0 :     const [active_tab, setActiveTab] = React.useState<string | number>(ansible ? "ansible" : "shell");
      34            0 :     const [copied, setCopied] = React.useState(false);
      35            0 :     const [timeoutId, setTimeoutId] = React.useState<number | null>(null);
      36              : 
      37            0 :     const handleSelect = (_event: React.MouseEvent, active_tab: string | number) => {
      38            0 :         setCopied(false);
      39            0 :         setActiveTab(active_tab);
      40            0 :         if (timeoutId !== null) {
      41            0 :             clearTimeout(timeoutId);
      42            0 :             setTimeoutId(null);
      43            0 :         }
      44            0 :     };
      45              : 
      46            0 :     const copyToClipboard = () => {
      47            0 :         try {
      48            0 :             navigator.clipboard.writeText((active_tab === "ansible" ? ansible! : shell).trim())
      49            0 :                     .then(() => {
      50            0 :                         setCopied(true);
      51            0 :                         setTimeoutId(setTimeout(() => {
      52            0 :                             setCopied(false);
      53            0 :                             setTimeoutId(null);
      54            0 :                         }, 3000));
      55            0 :                     })
      56            0 :                     .catch(e => console.error('Text could not be copied: ', e ? e.toString() : ""));
      57            0 :         } catch (error) {
      58            0 :             console.error('Text could not be copied: ', String(error));
      59            0 :         }
      60            0 :     };
      61              : 
      62            0 :     const footer = (
      63            0 :         <ModalFooter>
      64            0 :             <Button variant='secondary' className="btn-clipboard" onClick={copyToClipboard} icon={copied ? <Icon status="success"><CheckIcon /></Icon> : <CopyIcon />}>
      65            0 :                 { _("Copy to clipboard") }
      66            0 :             </Button>
      67            0 :             <Button variant='secondary' className='btn-cancel' onClick={onClose}>
      68            0 :                 { _("Close") }
      69            0 :             </Button>
      70            0 :         </ModalFooter>
      71              :     );
      72              : 
      73            0 :     return (
      74            0 :         <Modal isOpen className="automation-script-modal"
      75            0 :                position="top" variant="medium"
      76            0 :                onClose={onClose}>
      77            0 :             <ModalHeader title={_("Automation script") } />
      78            0 :             <ModalBody>
      79            0 :                 <Tabs activeKey={active_tab} onSelect={handleSelect}>
      80            0 :                     { ansible
      81            0 :                         ? <Tab eventKey="ansible" title={_("Ansible")}>
      82            0 :                             <TextArea resizeOrientation='vertical' readOnlyVariant="default" defaultValue={ansible.trim()} />
      83            0 :                             <div className="ansible-docs-link">
      84            0 :                                 <OutlinedQuestionCircleIcon />
      85            0 :                                 { _("Create new task file with this content.") }
      86            0 :                                 <Button variant="link" component="a" href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html"
      87            0 :                                         target="_blank" rel="noopener noreferrer"
      88            0 :                                         icon={<ExternalLinkAltIcon />}>
      89            0 :                                     { _("Ansible roles documentation") }
      90            0 :                                 </Button>
      91            0 :                             </div>
      92            0 :                         </Tab>
      93            0 :                         : undefined
      94              :                     }
      95            0 :                     <Tab eventKey="shell" title={_("Shell script")}>
      96            0 :                         <TextArea resizeOrientation='vertical' readOnlyVariant="default" defaultValue={shell.trim()} />
      97            0 :                     </Tab>
      98            0 :                 </Tabs>
      99            0 :             </ModalBody>
     100            0 :             {footer}
     101            0 :         </Modal>
     102              :     );
     103            0 : };
     104              : 
     105              : /* Display list of modifications in human readable format
     106              :  *
     107              :  * Also show `View automation script` button which opens dialog in which different
     108              :  * scripts are available. With these scripts it is possible to apply the same
     109              :  * configurations to  other machines.
     110              :  *
     111              :  * Pass array `entries` to show human readable messages.
     112              :  * Pass string `shell` and `ansible` with scripts.
     113              :  *
     114              :  */
     115            2 : export const Modifications = ({ entries, failed, permitted, title, shell, ansible }: {
     116              :     entries: string[] | null;
     117              :     failed?: string;
     118              :     permitted: boolean;
     119              :     title: string;
     120              :     shell: string;
     121              :     ansible?: string;
     122            2 : }) => {
     123            2 :     const [showDialog, setShowDialog] = React.useState(false);
     124              : 
     125            2 :     let emptyRow = null;
     126            1 :     let fail_message = permitted ? _("No system modifications") : _("The logged in user is not permitted to view system modifications");
     127            2 :     fail_message = failed || fail_message;
     128            2 :     if (entries === null) {
     129            2 :         emptyRow = <DataListItem>
     130            2 :             <DataListItemRow>
     131            2 :                 <DataListItemCells dataListCells={[<DataListCell key="loading">{_("Loading system modifications...")}</DataListCell>]} />
     132            2 :             </DataListItemRow>
     133            2 :         </DataListItem>;
     134            2 :     }
     135            1 :     if (entries?.length === 0) {
     136            1 :         emptyRow = <DataListItem>
     137            1 :             <DataListItemRow>
     138            1 :                 <DataListItemCells dataListCells={[<DataListCell key={fail_message}>{fail_message}</DataListCell>]} />
     139            1 :             </DataListItemRow>
     140            1 :         </DataListItem>;
     141            1 :     }
     142              : 
     143            2 :     return (
     144            2 :         <>
     145            2 :             { showDialog &&
     146            1 :                 <ModificationsExportDialog shell={shell} ansible={ansible}
     147            0 :                 onClose={() => setShowDialog(false)} />
     148              :             }
     149            2 :             <Card isPlain className="modifications-table">
     150            2 :                 <CardHeader>
     151            2 :                     <CardTitle component="h2">{title}</CardTitle>
     152            2 :                     { !emptyRow &&
     153            0 :                         <Button variant="secondary" onClick={() => setShowDialog(true)}>
     154            2 :                             {_("View automation script")}
     155            2 :                         </Button>
     156              :                     }
     157            2 :                 </CardHeader>
     158            2 :                 <CardBody className="contains-list">
     159            2 :                     <DataList aria-label={title} isCompact>
     160            2 :                         { emptyRow ||
     161            1 :                             entries?.map(entry => <DataListItem key={entry}>
     162            1 :                                 <DataListItemRow>
     163            1 :                                     <DataListItemCells dataListCells={[<DataListCell key={entry}>{entry}</DataListCell>]} />
     164            1 :                                 </DataListItemRow>
     165            1 :                             </DataListItem>
     166            2 :                             )
     167              :                         }
     168            2 :                     </DataList>
     169            2 :                 </CardBody>
     170            2 :             </Card>
     171            2 :         </>
     172              :     );
     173            2 : };
        

Generated by: LCOV version 2.0-1