foreman_openbolt 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +619 -0
- data/README.md +46 -0
- data/Rakefile +106 -0
- data/app/controllers/foreman_openbolt/task_controller.rb +298 -0
- data/app/lib/actions/foreman_openbolt/cleanup_proxy_artifacts.rb +40 -0
- data/app/lib/actions/foreman_openbolt/poll_task_status.rb +151 -0
- data/app/models/foreman_openbolt/task_job.rb +110 -0
- data/app/views/foreman_openbolt/react_page.html.erb +1 -0
- data/config/routes.rb +24 -0
- data/db/migrate/20250819000000_create_openbolt_task_jobs.rb +25 -0
- data/db/migrate/20250925000000_add_command_to_openbolt_task_jobs.rb +7 -0
- data/db/migrate/20251001000000_add_task_description_to_task_jobs.rb +7 -0
- data/db/seeds.d/001_add_openbolt_feature.rb +4 -0
- data/lib/foreman_openbolt/engine.rb +169 -0
- data/lib/foreman_openbolt/version.rb +5 -0
- data/lib/foreman_openbolt.rb +7 -0
- data/lib/proxy_api/openbolt.rb +53 -0
- data/lib/tasks/foreman_openbolt_tasks.rake +48 -0
- data/locale/Makefile +73 -0
- data/locale/en/foreman_openbolt.po +19 -0
- data/locale/foreman_openbolt.pot +19 -0
- data/locale/gemspec.rb +7 -0
- data/package.json +41 -0
- data/test/factories/foreman_openbolt_factories.rb +7 -0
- data/test/test_plugin_helper.rb +8 -0
- data/test/unit/foreman_openbolt_test.rb +13 -0
- data/webpack/global_index.js +4 -0
- data/webpack/global_test_setup.js +11 -0
- data/webpack/index.js +19 -0
- data/webpack/src/Components/LaunchTask/EmptyContent.js +24 -0
- data/webpack/src/Components/LaunchTask/FieldTable.js +147 -0
- data/webpack/src/Components/LaunchTask/HostSelector/HostSearch.js +29 -0
- data/webpack/src/Components/LaunchTask/HostSelector/SearchSelect.js +208 -0
- data/webpack/src/Components/LaunchTask/HostSelector/SelectedChips.js +113 -0
- data/webpack/src/Components/LaunchTask/HostSelector/hostgroups.gql +9 -0
- data/webpack/src/Components/LaunchTask/HostSelector/hosts.gql +10 -0
- data/webpack/src/Components/LaunchTask/HostSelector/index.js +261 -0
- data/webpack/src/Components/LaunchTask/OpenBoltOptionsSection.js +116 -0
- data/webpack/src/Components/LaunchTask/ParameterField.js +145 -0
- data/webpack/src/Components/LaunchTask/ParametersSection.js +66 -0
- data/webpack/src/Components/LaunchTask/SmartProxySelect.js +51 -0
- data/webpack/src/Components/LaunchTask/TaskSelect.js +84 -0
- data/webpack/src/Components/LaunchTask/hooks/useOpenBoltOptions.js +63 -0
- data/webpack/src/Components/LaunchTask/hooks/useSmartProxies.js +48 -0
- data/webpack/src/Components/LaunchTask/hooks/useTasksData.js +64 -0
- data/webpack/src/Components/LaunchTask/index.js +333 -0
- data/webpack/src/Components/TaskExecution/ExecutionDetails.js +188 -0
- data/webpack/src/Components/TaskExecution/ExecutionDisplay.js +99 -0
- data/webpack/src/Components/TaskExecution/LoadingIndicator.js +51 -0
- data/webpack/src/Components/TaskExecution/ResultDisplay.js +174 -0
- data/webpack/src/Components/TaskExecution/TaskDetails.js +99 -0
- data/webpack/src/Components/TaskExecution/hooks/useJobPolling.js +142 -0
- data/webpack/src/Components/TaskExecution/index.js +130 -0
- data/webpack/src/Components/TaskHistory/TaskPopover.js +95 -0
- data/webpack/src/Components/TaskHistory/index.js +199 -0
- data/webpack/src/Components/common/HostsPopover.js +49 -0
- data/webpack/src/Components/common/constants.js +44 -0
- data/webpack/src/Components/common/helpers.js +19 -0
- data/webpack/src/Pages/LaunchTaskPage.js +12 -0
- data/webpack/src/Pages/TaskExecutionPage.js +12 -0
- data/webpack/src/Pages/TaskHistoryPage.js +12 -0
- data/webpack/src/Router/routes.js +30 -0
- data/webpack/test_setup.js +17 -0
- data/webpack/webpack.config.js +7 -0
- metadata +208 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { translate as __, sprintf } from 'foremanReact/common/I18n';
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardBody,
|
|
7
|
+
DescriptionList,
|
|
8
|
+
DescriptionListGroup,
|
|
9
|
+
DescriptionListTerm,
|
|
10
|
+
DescriptionListDescription,
|
|
11
|
+
Label,
|
|
12
|
+
Flex,
|
|
13
|
+
FlexItem,
|
|
14
|
+
Text,
|
|
15
|
+
TextVariants,
|
|
16
|
+
} from '@patternfly/react-core';
|
|
17
|
+
import {
|
|
18
|
+
CheckCircleIcon,
|
|
19
|
+
ExclamationCircleIcon,
|
|
20
|
+
InProgressIcon,
|
|
21
|
+
ClockIcon,
|
|
22
|
+
TimesCircleIcon,
|
|
23
|
+
} from '@patternfly/react-icons';
|
|
24
|
+
|
|
25
|
+
import { STATUS, POLLING_CONFIG } from '../common/constants';
|
|
26
|
+
import HostsPopover from '../common/HostsPopover';
|
|
27
|
+
|
|
28
|
+
const STATUS_CONFIGS = {
|
|
29
|
+
[STATUS.SUCCESS]: {
|
|
30
|
+
icon: CheckCircleIcon,
|
|
31
|
+
color: 'green',
|
|
32
|
+
label: __('Success'),
|
|
33
|
+
},
|
|
34
|
+
[STATUS.FAILURE]: {
|
|
35
|
+
icon: ExclamationCircleIcon,
|
|
36
|
+
color: 'red',
|
|
37
|
+
label: __('Failed'),
|
|
38
|
+
},
|
|
39
|
+
[STATUS.EXCEPTION]: {
|
|
40
|
+
icon: ExclamationCircleIcon,
|
|
41
|
+
color: 'red',
|
|
42
|
+
label: __('Exception'),
|
|
43
|
+
},
|
|
44
|
+
[STATUS.INVALID]: {
|
|
45
|
+
icon: ExclamationCircleIcon,
|
|
46
|
+
color: 'red',
|
|
47
|
+
label: __('Invalid'),
|
|
48
|
+
},
|
|
49
|
+
[STATUS.RUNNING]: {
|
|
50
|
+
icon: InProgressIcon,
|
|
51
|
+
color: 'blue',
|
|
52
|
+
label: __('Running'),
|
|
53
|
+
},
|
|
54
|
+
[STATUS.PENDING]: {
|
|
55
|
+
icon: ClockIcon,
|
|
56
|
+
color: 'blue',
|
|
57
|
+
label: __('Pending'),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const getStatusConfig = status =>
|
|
62
|
+
STATUS_CONFIGS[status] || {
|
|
63
|
+
icon: TimesCircleIcon,
|
|
64
|
+
color: 'grey',
|
|
65
|
+
label: __('Unknown'),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const formatExecutionTime = (submittedAt, completedAt) => {
|
|
69
|
+
if (!submittedAt) return __('-');
|
|
70
|
+
|
|
71
|
+
const start = new Date(submittedAt);
|
|
72
|
+
const end = completedAt ? new Date(completedAt) : new Date();
|
|
73
|
+
const totalSeconds = Math.floor((end - start) / 1000);
|
|
74
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
75
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
76
|
+
const seconds = totalSeconds % 60;
|
|
77
|
+
|
|
78
|
+
const parts = [];
|
|
79
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
80
|
+
if (minutes > 0 || hours > 0) parts.push(`${minutes}m`);
|
|
81
|
+
parts.push(`${seconds}s`);
|
|
82
|
+
|
|
83
|
+
return parts.join('');
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const DescriptionItem = ({ label, children }) => (
|
|
87
|
+
<DescriptionListGroup>
|
|
88
|
+
<DescriptionListTerm>{label}</DescriptionListTerm>
|
|
89
|
+
<DescriptionListDescription>{children}</DescriptionListDescription>
|
|
90
|
+
</DescriptionListGroup>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
DescriptionItem.propTypes = {
|
|
94
|
+
label: PropTypes.string.isRequired,
|
|
95
|
+
children: PropTypes.node.isRequired,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const StatusLabel = ({ status, isPolling }) => {
|
|
99
|
+
const statusConfig = getStatusConfig(status);
|
|
100
|
+
const StatusIcon = statusConfig.icon;
|
|
101
|
+
const intervalSeconds = Math.round(POLLING_CONFIG.INTERVAL / 1000);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<Flex
|
|
105
|
+
spaceItems={{ default: 'spaceItemsSm' }}
|
|
106
|
+
alignItems={{ default: 'alignItemsCenter' }}
|
|
107
|
+
>
|
|
108
|
+
<FlexItem>
|
|
109
|
+
<Label color={statusConfig.color} icon={<StatusIcon />}>
|
|
110
|
+
{statusConfig.label}
|
|
111
|
+
</Label>
|
|
112
|
+
</FlexItem>
|
|
113
|
+
{isPolling && (
|
|
114
|
+
<>
|
|
115
|
+
<FlexItem>
|
|
116
|
+
<Text component={TextVariants.small} className="pf-v5-u-color-200">
|
|
117
|
+
{sprintf(__('Updating every %s seconds...'), intervalSeconds)}
|
|
118
|
+
</Text>
|
|
119
|
+
</FlexItem>
|
|
120
|
+
</>
|
|
121
|
+
)}
|
|
122
|
+
</Flex>
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
StatusLabel.propTypes = {
|
|
127
|
+
status: PropTypes.string.isRequired,
|
|
128
|
+
isPolling: PropTypes.bool.isRequired,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const ExecutionDetails = ({
|
|
132
|
+
proxyId,
|
|
133
|
+
proxyName,
|
|
134
|
+
jobId,
|
|
135
|
+
jobStatus,
|
|
136
|
+
isPolling,
|
|
137
|
+
targets,
|
|
138
|
+
submittedAt,
|
|
139
|
+
completedAt,
|
|
140
|
+
}) => (
|
|
141
|
+
<Card>
|
|
142
|
+
<CardBody>
|
|
143
|
+
<DescriptionList isHorizontal>
|
|
144
|
+
<DescriptionItem label={__('Smart Proxy')}>
|
|
145
|
+
{proxyId && proxyName ? (
|
|
146
|
+
<a href={`/smart_proxies/${proxyId}`}>{proxyName}</a>
|
|
147
|
+
) : (
|
|
148
|
+
<em>{__('Unknown')}</em>
|
|
149
|
+
)}
|
|
150
|
+
</DescriptionItem>
|
|
151
|
+
|
|
152
|
+
<DescriptionItem label={__('Job ID')}>
|
|
153
|
+
<span className="pf-v5-u-font-family-monospace">{jobId}</span>
|
|
154
|
+
</DescriptionItem>
|
|
155
|
+
|
|
156
|
+
<DescriptionItem label={__('Hosts')}>
|
|
157
|
+
<HostsPopover targets={targets} />
|
|
158
|
+
</DescriptionItem>
|
|
159
|
+
|
|
160
|
+
<DescriptionItem label={__('Status')}>
|
|
161
|
+
<StatusLabel status={jobStatus} isPolling={isPolling} />
|
|
162
|
+
</DescriptionItem>
|
|
163
|
+
|
|
164
|
+
<DescriptionItem label={__('Execution Time')}>
|
|
165
|
+
{formatExecutionTime(submittedAt, completedAt)}
|
|
166
|
+
</DescriptionItem>
|
|
167
|
+
</DescriptionList>
|
|
168
|
+
</CardBody>
|
|
169
|
+
</Card>
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
ExecutionDetails.propTypes = {
|
|
173
|
+
proxyId: PropTypes.string.isRequired,
|
|
174
|
+
proxyName: PropTypes.string.isRequired,
|
|
175
|
+
jobId: PropTypes.string.isRequired,
|
|
176
|
+
jobStatus: PropTypes.string.isRequired,
|
|
177
|
+
isPolling: PropTypes.bool.isRequired,
|
|
178
|
+
targets: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
179
|
+
submittedAt: PropTypes.string,
|
|
180
|
+
completedAt: PropTypes.string,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
ExecutionDetails.defaultProps = {
|
|
184
|
+
submittedAt: null,
|
|
185
|
+
completedAt: null,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export default ExecutionDetails;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
4
|
+
import { Tab, TabTitleIcon, TabTitleText, Tabs } from '@patternfly/react-core';
|
|
5
|
+
|
|
6
|
+
import { MonitoringIcon, TaskIcon } from '@patternfly/react-icons';
|
|
7
|
+
|
|
8
|
+
import ExecutionDetails from './ExecutionDetails';
|
|
9
|
+
import TaskDetails from './TaskDetails';
|
|
10
|
+
|
|
11
|
+
const ExecutionDisplay = ({
|
|
12
|
+
proxyId,
|
|
13
|
+
proxyName,
|
|
14
|
+
jobId,
|
|
15
|
+
jobStatus,
|
|
16
|
+
pollCount,
|
|
17
|
+
isPolling,
|
|
18
|
+
targets,
|
|
19
|
+
submittedAt,
|
|
20
|
+
completedAt,
|
|
21
|
+
taskName,
|
|
22
|
+
taskDescription,
|
|
23
|
+
taskParameters,
|
|
24
|
+
}) => {
|
|
25
|
+
const [activeTabKey, setActiveTabKey] = useState(0);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Tabs
|
|
29
|
+
activeKey={activeTabKey}
|
|
30
|
+
onSelect={(_event, tabIndex) => setActiveTabKey(tabIndex)}
|
|
31
|
+
>
|
|
32
|
+
<Tab
|
|
33
|
+
eventKey={0}
|
|
34
|
+
title={
|
|
35
|
+
<>
|
|
36
|
+
<TabTitleIcon>
|
|
37
|
+
<MonitoringIcon />
|
|
38
|
+
</TabTitleIcon>
|
|
39
|
+
<TabTitleText>{__('Execution Details')}</TabTitleText>
|
|
40
|
+
</>
|
|
41
|
+
}
|
|
42
|
+
>
|
|
43
|
+
<ExecutionDetails
|
|
44
|
+
proxyId={proxyId}
|
|
45
|
+
proxyName={proxyName}
|
|
46
|
+
jobId={jobId}
|
|
47
|
+
jobStatus={jobStatus}
|
|
48
|
+
pollCount={pollCount}
|
|
49
|
+
isPolling={isPolling}
|
|
50
|
+
targets={targets}
|
|
51
|
+
submittedAt={submittedAt}
|
|
52
|
+
completedAt={completedAt}
|
|
53
|
+
/>
|
|
54
|
+
</Tab>
|
|
55
|
+
|
|
56
|
+
<Tab
|
|
57
|
+
eventKey={1}
|
|
58
|
+
title={
|
|
59
|
+
<>
|
|
60
|
+
<TabTitleIcon>
|
|
61
|
+
<TaskIcon />
|
|
62
|
+
</TabTitleIcon>
|
|
63
|
+
<TabTitleText>{__('Task Details')}</TabTitleText>
|
|
64
|
+
</>
|
|
65
|
+
}
|
|
66
|
+
>
|
|
67
|
+
<TaskDetails
|
|
68
|
+
taskName={taskName}
|
|
69
|
+
taskDescription={taskDescription}
|
|
70
|
+
taskParameters={taskParameters}
|
|
71
|
+
/>
|
|
72
|
+
</Tab>
|
|
73
|
+
</Tabs>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
ExecutionDisplay.propTypes = {
|
|
78
|
+
proxyId: PropTypes.string.isRequired,
|
|
79
|
+
proxyName: PropTypes.string.isRequired,
|
|
80
|
+
jobId: PropTypes.string.isRequired,
|
|
81
|
+
jobStatus: PropTypes.string.isRequired,
|
|
82
|
+
pollCount: PropTypes.number.isRequired,
|
|
83
|
+
isPolling: PropTypes.bool.isRequired,
|
|
84
|
+
targets: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
85
|
+
submittedAt: PropTypes.string,
|
|
86
|
+
completedAt: PropTypes.string,
|
|
87
|
+
taskName: PropTypes.string.isRequired,
|
|
88
|
+
taskDescription: PropTypes.string,
|
|
89
|
+
taskParameters: PropTypes.object,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
ExecutionDisplay.defaultProps = {
|
|
93
|
+
submittedAt: null,
|
|
94
|
+
completedAt: null,
|
|
95
|
+
taskDescription: null,
|
|
96
|
+
taskParameters: {},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default ExecutionDisplay;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { sprintf, translate as __ } from 'foremanReact/common/I18n';
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardBody,
|
|
7
|
+
Bullseye,
|
|
8
|
+
EmptyState,
|
|
9
|
+
EmptyStateVariant,
|
|
10
|
+
EmptyStateHeader,
|
|
11
|
+
EmptyStateIcon,
|
|
12
|
+
EmptyStateBody,
|
|
13
|
+
Spinner,
|
|
14
|
+
} from '@patternfly/react-core';
|
|
15
|
+
import { RUNNING_STATUSES } from '../common/constants';
|
|
16
|
+
|
|
17
|
+
const LoadingIndicator = ({ jobStatus }) => {
|
|
18
|
+
const getMessage = () => {
|
|
19
|
+
if (RUNNING_STATUSES.includes(jobStatus)) {
|
|
20
|
+
return sprintf(__('Task is %s...'), jobStatus.toLowerCase());
|
|
21
|
+
}
|
|
22
|
+
return __('Processing task results...');
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Card>
|
|
27
|
+
<CardBody>
|
|
28
|
+
<Bullseye>
|
|
29
|
+
<EmptyState variant={EmptyStateVariant.lg}>
|
|
30
|
+
<EmptyStateHeader
|
|
31
|
+
titleText={getMessage()}
|
|
32
|
+
icon={<EmptyStateIcon icon={Spinner} />}
|
|
33
|
+
headingLevel="h3"
|
|
34
|
+
/>
|
|
35
|
+
<EmptyStateBody role="status" aria-live="polite" aria-atomic="true">
|
|
36
|
+
{__(
|
|
37
|
+
'This page will update automatically when the task completes.'
|
|
38
|
+
)}
|
|
39
|
+
</EmptyStateBody>
|
|
40
|
+
</EmptyState>
|
|
41
|
+
</Bullseye>
|
|
42
|
+
</CardBody>
|
|
43
|
+
</Card>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
LoadingIndicator.propTypes = {
|
|
48
|
+
jobStatus: PropTypes.string.isRequired,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default LoadingIndicator;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
Card,
|
|
7
|
+
CardBody,
|
|
8
|
+
CodeBlock,
|
|
9
|
+
CodeBlockCode,
|
|
10
|
+
EmptyState,
|
|
11
|
+
EmptyStateVariant,
|
|
12
|
+
EmptyStateIcon,
|
|
13
|
+
EmptyStateHeader,
|
|
14
|
+
EmptyStateBody,
|
|
15
|
+
Tabs,
|
|
16
|
+
Tab,
|
|
17
|
+
TabTitleText,
|
|
18
|
+
TabTitleIcon,
|
|
19
|
+
} from '@patternfly/react-core';
|
|
20
|
+
import {
|
|
21
|
+
ClipboardCheckIcon,
|
|
22
|
+
CopyIcon,
|
|
23
|
+
ExclamationCircleIcon,
|
|
24
|
+
FileAltIcon,
|
|
25
|
+
} from '@patternfly/react-icons';
|
|
26
|
+
|
|
27
|
+
const CopyButton = ({ getText }) => {
|
|
28
|
+
const [copied, setCopied] = useState(false);
|
|
29
|
+
|
|
30
|
+
const handleCopy = () => {
|
|
31
|
+
/* eslint-disable promise/prefer-await-to-then */
|
|
32
|
+
navigator.clipboard.writeText(getText()).then(() => {
|
|
33
|
+
setCopied(true);
|
|
34
|
+
setTimeout(() => setCopied(false), 2000);
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Button
|
|
40
|
+
variant="control"
|
|
41
|
+
aria-label="Copy"
|
|
42
|
+
onClick={handleCopy}
|
|
43
|
+
icon={<CopyIcon />}
|
|
44
|
+
>
|
|
45
|
+
{copied ? __('Copied!') : __('Copy')}
|
|
46
|
+
</Button>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
CopyButton.propTypes = {
|
|
51
|
+
getText: PropTypes.func.isRequired,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const EmptyTab = ({ message }) => (
|
|
55
|
+
<div className="pf-v5-u-mt-md">
|
|
56
|
+
<EmptyState variant={EmptyStateVariant.sm}>
|
|
57
|
+
<EmptyStateHeader
|
|
58
|
+
titleText={__('No data available')}
|
|
59
|
+
icon={<EmptyStateIcon icon={ExclamationCircleIcon} />}
|
|
60
|
+
headingLevel="h5"
|
|
61
|
+
/>
|
|
62
|
+
<EmptyStateBody>{message}</EmptyStateBody>
|
|
63
|
+
</EmptyState>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
EmptyTab.propTypes = {
|
|
68
|
+
message: PropTypes.string.isRequired,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const DataTab = ({ content }) => (
|
|
72
|
+
<div className="pf-v5-u-mt-md">
|
|
73
|
+
<CodeBlock>
|
|
74
|
+
<CodeBlockCode>{content}</CodeBlockCode>
|
|
75
|
+
</CodeBlock>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
DataTab.propTypes = {
|
|
80
|
+
content: PropTypes.string.isRequired,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const ResultDisplay = ({ jobResult, jobLog }) => {
|
|
84
|
+
const [activeTabKey, setActiveTabKey] = useState(0);
|
|
85
|
+
|
|
86
|
+
const formatResult = result => {
|
|
87
|
+
if (!result) return '';
|
|
88
|
+
try {
|
|
89
|
+
return JSON.stringify(result, null, 2);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return String(result);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const resultJson = formatResult(jobResult);
|
|
96
|
+
const hasResult = jobResult && Object.keys(jobResult).length > 0;
|
|
97
|
+
const hasLog = jobLog?.trim?.().length > 0;
|
|
98
|
+
|
|
99
|
+
const getActiveContent = () => (activeTabKey === 0 ? resultJson : jobLog);
|
|
100
|
+
|
|
101
|
+
// A bit of a messy solution for the copy button but good enough for now
|
|
102
|
+
return (
|
|
103
|
+
<Card>
|
|
104
|
+
<CardBody>
|
|
105
|
+
<div style={{ position: 'relative' }}>
|
|
106
|
+
<div
|
|
107
|
+
style={{
|
|
108
|
+
position: 'absolute',
|
|
109
|
+
right: 0,
|
|
110
|
+
zIndex: 1,
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
{((activeTabKey === 0 && hasResult) ||
|
|
114
|
+
(activeTabKey === 1 && hasLog)) && (
|
|
115
|
+
<CopyButton getText={getActiveContent} />
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
<Tabs
|
|
119
|
+
activeKey={activeTabKey}
|
|
120
|
+
onSelect={(_event, tabIndex) => setActiveTabKey(tabIndex)}
|
|
121
|
+
actions={<CopyButton getText={getActiveContent} />}
|
|
122
|
+
>
|
|
123
|
+
<Tab
|
|
124
|
+
eventKey={0}
|
|
125
|
+
title={
|
|
126
|
+
<>
|
|
127
|
+
<TabTitleIcon>
|
|
128
|
+
<ClipboardCheckIcon />
|
|
129
|
+
</TabTitleIcon>
|
|
130
|
+
<TabTitleText>{__('Result')}</TabTitleText>
|
|
131
|
+
</>
|
|
132
|
+
}
|
|
133
|
+
>
|
|
134
|
+
{hasResult ? (
|
|
135
|
+
<DataTab content={resultJson} />
|
|
136
|
+
) : (
|
|
137
|
+
<EmptyTab
|
|
138
|
+
message={__('No result data returned from the task.')}
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
141
|
+
</Tab>
|
|
142
|
+
|
|
143
|
+
<Tab
|
|
144
|
+
eventKey={1}
|
|
145
|
+
title={
|
|
146
|
+
<>
|
|
147
|
+
<TabTitleIcon>
|
|
148
|
+
<FileAltIcon />
|
|
149
|
+
</TabTitleIcon>
|
|
150
|
+
<TabTitleText>{__('Log Output')}</TabTitleText>
|
|
151
|
+
</>
|
|
152
|
+
}
|
|
153
|
+
>
|
|
154
|
+
{hasLog ? (
|
|
155
|
+
<DataTab content={jobLog} />
|
|
156
|
+
) : (
|
|
157
|
+
<EmptyTab
|
|
158
|
+
message={__('No log output generated by the task.')}
|
|
159
|
+
/>
|
|
160
|
+
)}
|
|
161
|
+
</Tab>
|
|
162
|
+
</Tabs>
|
|
163
|
+
</div>
|
|
164
|
+
</CardBody>
|
|
165
|
+
</Card>
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
ResultDisplay.propTypes = {
|
|
170
|
+
jobResult: PropTypes.object.isRequired,
|
|
171
|
+
jobLog: PropTypes.string.isRequired,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export default ResultDisplay;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardBody,
|
|
7
|
+
DescriptionList,
|
|
8
|
+
DescriptionListDescription,
|
|
9
|
+
DescriptionListGroup,
|
|
10
|
+
DescriptionListTerm,
|
|
11
|
+
} from '@patternfly/react-core';
|
|
12
|
+
import { Table, Thead, Tbody, Tr, Th, Td } from '@patternfly/react-table';
|
|
13
|
+
|
|
14
|
+
const TaskDetails = ({ taskName, taskDescription, taskParameters }) => {
|
|
15
|
+
const displayValue = value => {
|
|
16
|
+
if (value === null || value === undefined) {
|
|
17
|
+
return '-';
|
|
18
|
+
}
|
|
19
|
+
if (typeof value === 'object') {
|
|
20
|
+
return JSON.stringify(value);
|
|
21
|
+
}
|
|
22
|
+
return String(value);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Card>
|
|
27
|
+
<CardBody>
|
|
28
|
+
<DescriptionList isHorizontal>
|
|
29
|
+
<DescriptionListGroup>
|
|
30
|
+
<DescriptionListTerm>{__('Task Name')}</DescriptionListTerm>
|
|
31
|
+
<DescriptionListDescription>
|
|
32
|
+
<span className="pf-v5-u-font-family-monospace">{taskName}</span>
|
|
33
|
+
</DescriptionListDescription>
|
|
34
|
+
</DescriptionListGroup>
|
|
35
|
+
|
|
36
|
+
{taskDescription && (
|
|
37
|
+
<DescriptionListGroup>
|
|
38
|
+
<DescriptionListTerm>{__('Description')}</DescriptionListTerm>
|
|
39
|
+
<DescriptionListDescription>
|
|
40
|
+
{taskDescription}
|
|
41
|
+
</DescriptionListDescription>
|
|
42
|
+
</DescriptionListGroup>
|
|
43
|
+
)}
|
|
44
|
+
|
|
45
|
+
{taskParameters && Object.keys(taskParameters).length > 0 && (
|
|
46
|
+
<DescriptionListGroup>
|
|
47
|
+
<DescriptionListTerm>{__('Parameters')}</DescriptionListTerm>
|
|
48
|
+
<DescriptionListDescription>
|
|
49
|
+
<Table
|
|
50
|
+
variant="compact"
|
|
51
|
+
borders
|
|
52
|
+
isStriped
|
|
53
|
+
gridBreakPoint="grid-md"
|
|
54
|
+
style={{ wordBreak: 'break-word' }}
|
|
55
|
+
>
|
|
56
|
+
<Thead>
|
|
57
|
+
<Tr>
|
|
58
|
+
<Th width={30}>{__('Name')}</Th>
|
|
59
|
+
<Th width={70}>{__('Value')}</Th>
|
|
60
|
+
</Tr>
|
|
61
|
+
</Thead>
|
|
62
|
+
<Tbody>
|
|
63
|
+
{Object.entries(taskParameters).map(([key, value]) => (
|
|
64
|
+
<Tr key={key}>
|
|
65
|
+
<Td>
|
|
66
|
+
<span className="pf-v5-u-font-family-monospace">
|
|
67
|
+
{key}
|
|
68
|
+
</span>
|
|
69
|
+
</Td>
|
|
70
|
+
<Td>
|
|
71
|
+
<span className="pf-v5-u-font-family-monospace">
|
|
72
|
+
{displayValue(value)}
|
|
73
|
+
</span>
|
|
74
|
+
</Td>
|
|
75
|
+
</Tr>
|
|
76
|
+
))}
|
|
77
|
+
</Tbody>
|
|
78
|
+
</Table>
|
|
79
|
+
</DescriptionListDescription>
|
|
80
|
+
</DescriptionListGroup>
|
|
81
|
+
)}
|
|
82
|
+
</DescriptionList>
|
|
83
|
+
</CardBody>
|
|
84
|
+
</Card>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
TaskDetails.propTypes = {
|
|
89
|
+
taskName: PropTypes.string.isRequired,
|
|
90
|
+
taskDescription: PropTypes.string,
|
|
91
|
+
taskParameters: PropTypes.object,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
TaskDetails.defaultProps = {
|
|
95
|
+
taskDescription: null,
|
|
96
|
+
taskParameters: {},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default TaskDetails;
|