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 : };
|