foreman_remote_execution 14.1.4 → 15.0.0

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +8 -0
  3. data/app/controllers/template_invocations_controller.rb +57 -0
  4. data/app/controllers/ui_job_wizard_controller.rb +6 -3
  5. data/app/helpers/remote_execution_helper.rb +5 -6
  6. data/app/views/api/v2/job_invocations/base.json.rabl +1 -1
  7. data/app/views/api/v2/job_invocations/hosts.json.rabl +1 -1
  8. data/app/views/api/v2/job_invocations/main.json.rabl +1 -1
  9. data/app/views/api/v2/job_invocations/show.json.rabl +18 -0
  10. data/app/views/templates/script/convert2rhel_analyze.erb +4 -4
  11. data/config/routes.rb +2 -0
  12. data/lib/foreman_remote_execution/engine.rb +3 -3
  13. data/lib/foreman_remote_execution/version.rb +1 -1
  14. data/webpack/JobInvocationDetail/JobAdditionInfo.js +214 -0
  15. data/webpack/JobInvocationDetail/JobInvocationConstants.js +40 -2
  16. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +70 -0
  17. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +177 -80
  18. data/webpack/JobInvocationDetail/JobInvocationHostTableToolbar.js +63 -0
  19. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +8 -1
  20. data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +61 -10
  21. data/webpack/JobInvocationDetail/OpenAlInvocations.js +111 -0
  22. data/webpack/JobInvocationDetail/TemplateInvocation.js +202 -0
  23. data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js +124 -0
  24. data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputToggleGroup.js +156 -0
  25. data/webpack/JobInvocationDetail/TemplateInvocationComponents/PreviewTemplate.js +50 -0
  26. data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +224 -0
  27. data/webpack/JobInvocationDetail/TemplateInvocationPage.js +53 -0
  28. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +1 -1
  29. data/webpack/JobInvocationDetail/__tests__/OpenAlInvocations.test.js +110 -0
  30. data/webpack/JobInvocationDetail/__tests__/OutputCodeBlock.test.js +69 -0
  31. data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +131 -0
  32. data/webpack/JobInvocationDetail/__tests__/fixtures.js +130 -0
  33. data/webpack/JobInvocationDetail/index.js +18 -3
  34. data/webpack/JobWizard/JobWizard.js +38 -16
  35. data/webpack/JobWizard/{StartsBeforeErrorAlert.js → StartsErrorAlert.js} +16 -1
  36. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +1 -1
  37. data/webpack/JobWizard/steps/Schedule/ScheduleFuture.js +1 -1
  38. data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +5 -3
  39. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +1 -1
  40. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +3 -3
  41. data/webpack/JobWizard/steps/form/DateTimePicker.js +13 -0
  42. data/webpack/JobWizard/steps/form/Formatter.js +1 -0
  43. data/webpack/JobWizard/steps/form/ResourceSelect.js +34 -9
  44. data/webpack/Routes/routes.js +6 -0
  45. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +1 -0
  46. data/webpack/react_app/components/RegistrationExtension/RexPull.js +27 -2
  47. data/webpack/react_app/components/TargetingHosts/components/HostStatus.js +1 -1
  48. metadata +15 -3
@@ -0,0 +1,202 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { isEmpty } from 'lodash';
3
+ import PropTypes from 'prop-types';
4
+ import { ClipboardCopyButton, Alert, Skeleton } from '@patternfly/react-core';
5
+ import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
6
+ import { translate as __ } from 'foremanReact/common/I18n';
7
+ import { useForemanHostDetailsPageUrl } from 'foremanReact/Root/Context/ForemanContext';
8
+ import { STATUS } from 'foremanReact/constants';
9
+ import {
10
+ showTemplateInvocationUrl,
11
+ templateInvocationPageUrl,
12
+ GET_TEMPLATE_INVOCATION,
13
+ } from './JobInvocationConstants';
14
+ import { OutputToggleGroup } from './TemplateInvocationComponents/OutputToggleGroup';
15
+ import { PreviewTemplate } from './TemplateInvocationComponents/PreviewTemplate';
16
+ import { OutputCodeBlock } from './TemplateInvocationComponents/OutputCodeBlock';
17
+
18
+ const CopyToClipboard = ({ fullOutput }) => {
19
+ const clipboardCopyFunc = async (event, text) => {
20
+ try {
21
+ await navigator.clipboard.writeText(text.toString());
22
+ } catch (error) {
23
+ // eslint-disable-next-line no-console
24
+ console.error(error?.message);
25
+ }
26
+ };
27
+
28
+ const onClick = (event, text) => {
29
+ clipboardCopyFunc(event, text);
30
+ setCopied(true);
31
+ };
32
+ const [copied, setCopied] = React.useState(false);
33
+ return (
34
+ <ClipboardCopyButton
35
+ id="expandable-copy-button"
36
+ textId="code-content"
37
+ aria-label="Copy to clipboard"
38
+ onClick={e => onClick(e, fullOutput)}
39
+ exitDelay={copied ? 1500 : 600}
40
+ maxWidth="110px"
41
+ variant="plain"
42
+ onTooltipHidden={() => setCopied(false)}
43
+ >
44
+ {copied
45
+ ? __('Successfully copied to clipboard!')
46
+ : __('Copy to clipboard')}
47
+ </ClipboardCopyButton>
48
+ );
49
+ };
50
+ let intervalId;
51
+ export const TemplateInvocation = ({
52
+ hostID,
53
+ jobID,
54
+ isInTableView,
55
+ hostName,
56
+ }) => {
57
+ const templateURL = showTemplateInvocationUrl(hostID, jobID);
58
+ const hostDetailsPageUrl = useForemanHostDetailsPageUrl();
59
+ const { response, status, setAPIOptions } = useAPI('get', templateURL, {
60
+ key: GET_TEMPLATE_INVOCATION,
61
+ headers: { Accept: 'application/json' },
62
+ handleError: () => {
63
+ if (intervalId) clearInterval(intervalId);
64
+ },
65
+ });
66
+ const { finished, auto_refresh: autoRefresh } = response;
67
+
68
+ useEffect(() => {
69
+ if (!finished || autoRefresh) {
70
+ intervalId = setInterval(() => {
71
+ // Re call api
72
+ setAPIOptions(prevOptions => ({
73
+ ...prevOptions,
74
+ }));
75
+ }, 5000);
76
+ }
77
+ if (intervalId && finished && !autoRefresh) {
78
+ clearInterval(intervalId);
79
+ }
80
+ return () => {
81
+ clearInterval(intervalId);
82
+ };
83
+ }, [finished, autoRefresh, setAPIOptions]);
84
+
85
+ const errorMessage =
86
+ response?.response?.data?.error?.message ||
87
+ response?.response?.data?.error ||
88
+ JSON.stringify(response);
89
+ const {
90
+ preview,
91
+ output,
92
+ input_values: inputValues,
93
+ task_id: taskID,
94
+ task_cancellable: taskCancellable,
95
+ permissions,
96
+ } = response;
97
+ const [showOutputType, setShowOutputType] = useState({
98
+ stderr: true,
99
+ stdout: true,
100
+ debug: true,
101
+ });
102
+ const [showTemplatePreview, setShowTemplatePreview] = useState(false);
103
+ const [showCommand, setCommand] = useState(false);
104
+ if (status === STATUS.PENDING && isEmpty(response)) {
105
+ return <Skeleton />;
106
+ } else if (status === STATUS.ERROR) {
107
+ return (
108
+ <Alert
109
+ ouiaId="template-invocation-error-alert"
110
+ variant="danger"
111
+ title={__(
112
+ 'An error occurred while fetching the template invocation details.'
113
+ )}
114
+ >
115
+ {errorMessage}
116
+ </Alert>
117
+ );
118
+ }
119
+
120
+ return (
121
+ <div
122
+ id={`template-invocation-${hostID}`}
123
+ className={`template-invocation ${
124
+ isInTableView ? ' output-in-table-view' : ''
125
+ }`}
126
+ >
127
+ <OutputToggleGroup
128
+ showOutputType={showOutputType}
129
+ setShowOutputType={setShowOutputType}
130
+ setShowTemplatePreview={setShowTemplatePreview}
131
+ showTemplatePreview={showTemplatePreview}
132
+ showCommand={showCommand}
133
+ setShowCommand={setCommand}
134
+ newTabUrl={templateInvocationPageUrl(hostID, jobID)}
135
+ isInTableView={isInTableView}
136
+ copyToClipboard={
137
+ <CopyToClipboard
138
+ fullOutput={output
139
+ ?.filter(
140
+ ({ output_type: outputType }) => showOutputType[outputType]
141
+ )
142
+ .map(({ output: _output }) => _output)
143
+ .join('\n')}
144
+ />
145
+ }
146
+ taskID={taskID}
147
+ jobID={jobID}
148
+ hostID={hostID}
149
+ taskCancellable={taskCancellable}
150
+ permissions={permissions}
151
+ />
152
+ {!isInTableView && (
153
+ <div>
154
+ {__('Target:')}{' '}
155
+ <a href={`${hostDetailsPageUrl}${hostName}`}>{hostName}</a>
156
+ </div>
157
+ )}
158
+ {showTemplatePreview && <PreviewTemplate inputValues={inputValues} />}
159
+ {showCommand && (
160
+ <>
161
+ {preview?.status ? (
162
+ <Alert
163
+ variant="danger"
164
+ ouiaId="template-invocation-preview-alert"
165
+ title={preview.plain}
166
+ />
167
+ ) : (
168
+ <pre className="template-invocation-preview">{preview.plain}</pre>
169
+ )}
170
+ </>
171
+ )}
172
+ <OutputCodeBlock
173
+ code={output || []}
174
+ showOutputType={showOutputType}
175
+ scrollElement={
176
+ isInTableView
177
+ ? `#template-invocation-${hostID} .invocation-output`
178
+ : '#foreman-main-container'
179
+ }
180
+ />
181
+ </div>
182
+ );
183
+ };
184
+
185
+ TemplateInvocation.propTypes = {
186
+ hostID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
187
+ hostName: PropTypes.string, // only used when isInTableView is false
188
+ jobID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
189
+ isInTableView: PropTypes.bool,
190
+ };
191
+
192
+ TemplateInvocation.defaultProps = {
193
+ isInTableView: true,
194
+ hostName: '',
195
+ };
196
+
197
+ CopyToClipboard.propTypes = {
198
+ fullOutput: PropTypes.string,
199
+ };
200
+ CopyToClipboard.defaultProps = {
201
+ fullOutput: '',
202
+ };
@@ -0,0 +1,124 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Button } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+
6
+ export const OutputCodeBlock = ({ code, showOutputType, scrollElement }) => {
7
+ let lineCounter = 0;
8
+ // eslint-disable-next-line no-control-regex
9
+ const COLOR_PATTERN = /\x1b\[(\d+)m/g;
10
+ const CONSOLE_COLOR = {
11
+ '31': 'red',
12
+ '32': 'lightgreen',
13
+ '33': 'orange',
14
+ '34': 'deepskyblue',
15
+ '35': 'mediumpurple',
16
+ '36': 'cyan',
17
+ '37': 'grey',
18
+ '91': 'red',
19
+ '92': 'lightgreen',
20
+ '93': 'yellow',
21
+ '94': 'lightblue',
22
+ '95': 'violet',
23
+ '96': 'turquoise',
24
+ '0': 'default',
25
+ };
26
+
27
+ const colorizeLine = line => {
28
+ line = line.replace(COLOR_PATTERN, seq => {
29
+ const color = seq.match(/(\d+)m/)[1];
30
+ return `{{{format color:${color}}}}`;
31
+ });
32
+
33
+ let currentColor = 'default';
34
+ const parts = line.split(/({{{format.*?}}})/).filter(Boolean);
35
+ if (parts.length === 0) {
36
+ return <span>{'\n'}</span>;
37
+ }
38
+ // eslint-disable-next-line array-callback-return, consistent-return
39
+ return parts.map((consoleLine, index) => {
40
+ if (consoleLine.includes('{{{format')) {
41
+ const colorMatch = consoleLine.match(/color:(\d+)/);
42
+ if (colorMatch) {
43
+ const colorIndex = colorMatch[1];
44
+ currentColor = CONSOLE_COLOR[colorIndex] || 'default';
45
+ }
46
+ } else {
47
+ return (
48
+ <span key={index} style={{ color: currentColor }}>
49
+ {consoleLine.length ? consoleLine : '\n'}
50
+ </span>
51
+ );
52
+ }
53
+ });
54
+ };
55
+ const filteredCode = code.filter(
56
+ ({ output_type: outputType }) => showOutputType[outputType]
57
+ );
58
+ if (!filteredCode.length) {
59
+ return <div>{__('No output for the selected filters')}</div>;
60
+ }
61
+ const codeParse = filteredCode.map(line => {
62
+ if (line.output === '\n') {
63
+ return null;
64
+ }
65
+ const lineOutputs = line.output
66
+ .replace(/\r\n/g, '\n')
67
+ .replace(/\n$/, '')
68
+ .split('\n');
69
+ return lineOutputs.map((lineOutput, index) => {
70
+ lineCounter += 1;
71
+ return (
72
+ <div key={index} className={`line ${line.output_type}`}>
73
+ <span
74
+ className="counter"
75
+ title={new Date(line.timestamp * 1000).toISOString()}
76
+ >
77
+ {lineCounter.toString().padStart(4, '\u00A0')}:{' '}
78
+ </span>
79
+ <div className="content">{colorizeLine(lineOutput)}</div>
80
+ </div>
81
+ );
82
+ });
83
+ });
84
+ const scrollElementSeleceted = () => document.querySelector(scrollElement);
85
+ const onClickScrollToTop = () => {
86
+ scrollElementSeleceted().scrollTo(0, 0);
87
+ };
88
+ const onClickScrollToBottom = () => {
89
+ scrollElementSeleceted().scrollTo(0, scrollElementSeleceted().scrollHeight);
90
+ };
91
+ return (
92
+ <div className="invocation-output">
93
+ <Button
94
+ component="a"
95
+ href="#"
96
+ variant="link"
97
+ isInline
98
+ className="scroll-link"
99
+ onClick={onClickScrollToBottom}
100
+ ouiaId="scroll-to-bottom"
101
+ >
102
+ {__('Scroll to bottom')}
103
+ </Button>
104
+ {codeParse}
105
+ <Button
106
+ component="a"
107
+ href="#"
108
+ variant="link"
109
+ isInline
110
+ className="scroll-link"
111
+ onClick={onClickScrollToTop}
112
+ ouiaId="scroll-to-top"
113
+ >
114
+ {__('Scroll to top')}
115
+ </Button>
116
+ </div>
117
+ );
118
+ };
119
+
120
+ OutputCodeBlock.propTypes = {
121
+ code: PropTypes.array.isRequired,
122
+ showOutputType: PropTypes.object.isRequired,
123
+ scrollElement: PropTypes.string.isRequired,
124
+ };
@@ -0,0 +1,156 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ ToggleGroup,
5
+ ToggleGroupItem,
6
+ Flex,
7
+ FlexItem,
8
+ Button,
9
+ } from '@patternfly/react-core';
10
+ import { OutlinedWindowRestoreIcon } from '@patternfly/react-icons';
11
+ import { translate as __ } from 'foremanReact/common/I18n';
12
+ import { TemplateActionButtons } from './TemplateActionButtons';
13
+
14
+ export const OutputToggleGroup = ({
15
+ showOutputType,
16
+ setShowOutputType,
17
+ showTemplatePreview,
18
+ setShowTemplatePreview,
19
+ showCommand,
20
+ setShowCommand,
21
+ newTabUrl,
22
+ isInTableView,
23
+ copyToClipboard,
24
+ taskID,
25
+ jobID,
26
+ hostID,
27
+ taskCancellable,
28
+ permissions,
29
+ }) => {
30
+ const handleSTDERRClick = _isSelected => {
31
+ setShowOutputType(prevShowOutputType => ({
32
+ ...prevShowOutputType,
33
+ stderr: _isSelected,
34
+ }));
35
+ };
36
+
37
+ const handleSTDOUTClick = _isSelected => {
38
+ setShowOutputType(prevShowOutputType => ({
39
+ ...prevShowOutputType,
40
+ stdout: _isSelected,
41
+ }));
42
+ };
43
+ const handleDEBUGClick = _isSelected => {
44
+ setShowOutputType(prevShowOutputType => ({
45
+ ...prevShowOutputType,
46
+ debug: _isSelected,
47
+ }));
48
+ };
49
+ const handlePreviewTemplateClick = _isSelected => {
50
+ setShowTemplatePreview(_isSelected);
51
+ };
52
+ const handleCommandClick = _isSelected => {
53
+ setShowCommand(_isSelected);
54
+ };
55
+
56
+ const toggleGroupItems = {
57
+ stderr: {
58
+ id: 'stderr-toggle',
59
+ text: __('STDERR'),
60
+ onClick: handleSTDERRClick,
61
+ isSelected: showOutputType.stderr,
62
+ },
63
+ stdout: {
64
+ id: 'stdout-toggle',
65
+ text: __('STDOUT'),
66
+ onClick: handleSTDOUTClick,
67
+ isSelected: showOutputType.stdout,
68
+ },
69
+ debug: {
70
+ id: 'debug-toggle',
71
+ text: __('DEBUG'),
72
+ onClick: handleDEBUGClick,
73
+ isSelected: showOutputType.debug,
74
+ },
75
+ previewTemplate: {
76
+ id: 'preview-template-toggle',
77
+ text: __('Preview Template'),
78
+ onClick: handlePreviewTemplateClick,
79
+ isSelected: showTemplatePreview,
80
+ },
81
+ command: {
82
+ id: 'command-toggle',
83
+ text: __('Command'),
84
+ onClick: handleCommandClick,
85
+ isSelected: showCommand,
86
+ },
87
+ };
88
+
89
+ return (
90
+ <Flex>
91
+ <FlexItem>
92
+ <ToggleGroup>
93
+ {Object.values(toggleGroupItems).map(
94
+ ({ id, text, onClick, isSelected }) => (
95
+ <ToggleGroupItem
96
+ key={id}
97
+ text={text}
98
+ buttonId={id}
99
+ isSelected={isSelected}
100
+ onChange={onClick}
101
+ />
102
+ )
103
+ )}
104
+ </ToggleGroup>
105
+ </FlexItem>
106
+ {isInTableView ? null : (
107
+ <TemplateActionButtons
108
+ taskID={taskID}
109
+ jobID={jobID}
110
+ hostID={hostID}
111
+ taskCancellable={taskCancellable}
112
+ permissions={permissions}
113
+ />
114
+ )}
115
+ <FlexItem>{copyToClipboard}</FlexItem>
116
+ {isInTableView && (
117
+ <FlexItem>
118
+ <Button
119
+ title={__('Open in new tab')}
120
+ variant="link"
121
+ isInline
122
+ ouiaId="template-invocation-new-tab-button"
123
+ component="a"
124
+ href={newTabUrl}
125
+ target="_blank"
126
+ >
127
+ <OutlinedWindowRestoreIcon />
128
+ </Button>
129
+ </FlexItem>
130
+ )}
131
+ </Flex>
132
+ );
133
+ };
134
+
135
+ OutputToggleGroup.propTypes = {
136
+ showOutputType: PropTypes.shape({
137
+ stderr: PropTypes.bool,
138
+ stdout: PropTypes.bool,
139
+ debug: PropTypes.bool,
140
+ }).isRequired,
141
+ setShowOutputType: PropTypes.func.isRequired,
142
+ setShowTemplatePreview: PropTypes.func.isRequired,
143
+ showTemplatePreview: PropTypes.bool.isRequired,
144
+ showCommand: PropTypes.bool.isRequired,
145
+ setShowCommand: PropTypes.func.isRequired,
146
+ newTabUrl: PropTypes.string,
147
+ copyToClipboard: PropTypes.node.isRequired,
148
+ isInTableView: PropTypes.bool,
149
+ ...TemplateActionButtons.propTypes,
150
+ };
151
+
152
+ OutputToggleGroup.defaultProps = {
153
+ newTabUrl: null,
154
+ isInTableView: false,
155
+ ...TemplateActionButtons.defaultProps,
156
+ };
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ TableComposable,
5
+ Thead,
6
+ Tr,
7
+ Th,
8
+ Tbody,
9
+ Td,
10
+ } from '@patternfly/react-table';
11
+ import { translate as __ } from 'foremanReact/common/I18n';
12
+
13
+ export const PreviewTemplate = ({ inputValues }) =>
14
+ inputValues.length ? (
15
+ <TableComposable
16
+ ouiaId="template-invocation-preview-table"
17
+ isStriped
18
+ variant="compact"
19
+ >
20
+ <Thead>
21
+ <Tr ouiaId="template-invocation-preview-table-head">
22
+ <Th modifier="fitContent">{__('User input')}</Th>
23
+ <Th modifier="fitContent">{__('Value')}</Th>
24
+ </Tr>
25
+ </Thead>
26
+ <Tbody>
27
+ {inputValues.map(({ name, value }, index) => (
28
+ <Tr
29
+ key={index}
30
+ ouiaId={`template-invocation-preview-table-row-${name}`}
31
+ >
32
+ <Td>
33
+ <b>{name}</b>
34
+ </Td>
35
+ <Td>{value}</Td>
36
+ </Tr>
37
+ ))}
38
+ </Tbody>
39
+ </TableComposable>
40
+ ) : (
41
+ <span>{__('No user input')}</span>
42
+ );
43
+
44
+ PreviewTemplate.propTypes = {
45
+ inputValues: PropTypes.array,
46
+ };
47
+
48
+ PreviewTemplate.defaultProps = {
49
+ inputValues: [],
50
+ };