foreman_remote_execution 16.6.5 → 17.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.
- checksums.yaml +4 -4
- data/app/controllers/api/v2/job_invocations_controller.rb +22 -13
- data/app/views/api/v2/job_invocations/main.json.rabl +16 -11
- data/app/views/job_templates/_alerts.html.erb +4 -0
- data/lib/foreman_remote_execution/plugin.rb +1 -1
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/package.json +3 -3
- data/test/functional/api/v2/job_invocations_controller_test.rb +68 -3
- data/webpack/JobInvocationDetail/CheckboxesActions.js +4 -10
- data/webpack/JobInvocationDetail/JobInvocationActions.js +80 -90
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +7 -6
- data/webpack/JobInvocationDetail/JobInvocationEmptyState.js +52 -0
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +76 -42
- data/webpack/JobInvocationDetail/JobInvocationSelectors.js +1 -19
- data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +202 -153
- data/webpack/JobInvocationDetail/TemplateInvocation.js +31 -25
- data/webpack/JobInvocationDetail/__tests__/JobInvocationDetailEmptyState.test.js +53 -0
- data/webpack/JobInvocationDetail/__tests__/JobInvocationHostTablePolling.test.js +336 -0
- data/webpack/JobInvocationDetail/__tests__/JobInvocationPolling.test.js +223 -0
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +260 -111
- data/webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js +3 -1
- data/webpack/JobInvocationDetail/__tests__/TemplateInvocationPolling.test.js +254 -0
- data/webpack/JobInvocationDetail/__tests__/fixtures.js +2 -0
- data/webpack/JobInvocationDetail/__tests__/foremanTestHelpers.js +30 -0
- data/webpack/JobInvocationDetail/index.js +50 -30
- data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +15 -0
- data/webpack/__mocks__/foremanReact/common/hooks/Permissions/permissionHooks.js +1 -0
- data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +5 -0
- data/webpack/test_setup.js +1 -13
- metadata +11 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
import PropTypes from 'prop-types';
|
|
2
|
-
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
4
|
import { useDispatch, useSelector } from 'react-redux';
|
|
4
5
|
import { Button, Split, SplitItem } from '@patternfly/react-core';
|
|
5
6
|
import { UndoIcon } from '@patternfly/react-icons';
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
} from '@patternfly/react-core/deprecated';
|
|
13
14
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
14
15
|
import { foremanUrl } from 'foremanReact/common/helpers';
|
|
16
|
+
import { usePermissions } from 'foremanReact/common/hooks/Permissions/permissionHooks';
|
|
15
17
|
import { get } from 'foremanReact/redux/API';
|
|
16
18
|
import {
|
|
17
19
|
cancelJob,
|
|
@@ -23,10 +25,7 @@ import {
|
|
|
23
25
|
GET_REPORT_TEMPLATES,
|
|
24
26
|
GET_REPORT_TEMPLATE_INPUTS,
|
|
25
27
|
} from './JobInvocationConstants';
|
|
26
|
-
import {
|
|
27
|
-
selectTaskCancelable,
|
|
28
|
-
selectHasPermission,
|
|
29
|
-
} from './JobInvocationSelectors';
|
|
28
|
+
import { selectTaskCancelable } from './JobInvocationSelectors';
|
|
30
29
|
|
|
31
30
|
const JobInvocationToolbarButtons = ({ jobId, data }) => {
|
|
32
31
|
const { succeeded, failed, task, recurrence, permissions } = data;
|
|
@@ -38,165 +37,240 @@ const JobInvocationToolbarButtons = ({ jobId, data }) => {
|
|
|
38
37
|
? permissions.edit_recurring_logics
|
|
39
38
|
: false;
|
|
40
39
|
const isTaskCancelable = useSelector(selectTaskCancelable);
|
|
41
|
-
const
|
|
42
|
-
|
|
40
|
+
const canCreateJobInvocations = usePermissions(['create_job_invocations']);
|
|
41
|
+
const canCancelJobInvocations = usePermissions(['cancel_job_invocations']);
|
|
42
|
+
const canGenerateReportTemplates = usePermissions([
|
|
43
|
+
'generate_report_templates',
|
|
44
|
+
]);
|
|
43
45
|
const [isActionOpen, setIsActionOpen] = useState(false);
|
|
44
46
|
const [reportTemplateJobId, setReportTemplateJobId] = useState(undefined);
|
|
45
47
|
const [templateInputId, setTemplateInputId] = useState(undefined);
|
|
46
|
-
const queryParams = new URLSearchParams({
|
|
47
|
-
[`report_template_report[input_values][${templateInputId}][value]`]: jobId,
|
|
48
|
-
});
|
|
49
48
|
const dispatch = useDispatch();
|
|
49
|
+
const reportHref = useMemo(() => {
|
|
50
|
+
if (reportTemplateJobId === undefined || templateInputId === undefined) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const queryParams = new URLSearchParams({
|
|
54
|
+
[`report_template_report[input_values][${templateInputId}][value]`]: jobId,
|
|
55
|
+
});
|
|
56
|
+
return foremanUrl(
|
|
57
|
+
`/templates/report_templates/${reportTemplateJobId}/generate?${queryParams.toString()}`
|
|
58
|
+
);
|
|
59
|
+
}, [jobId, reportTemplateJobId, templateInputId]);
|
|
50
60
|
|
|
51
|
-
const
|
|
61
|
+
const isCreateReportDisabled =
|
|
62
|
+
!canGenerateReportTemplates ||
|
|
63
|
+
task?.state === STATUS.RUNNING ||
|
|
64
|
+
task?.state === STATUS.PENDING ||
|
|
65
|
+
reportHref === undefined;
|
|
66
|
+
|
|
67
|
+
const onActionFocus = useCallback(() => {
|
|
52
68
|
const element = document.getElementById(
|
|
53
69
|
`toggle-split-button-action-primary-${jobId}`
|
|
54
70
|
);
|
|
55
|
-
element
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
if (element) {
|
|
72
|
+
element.focus();
|
|
73
|
+
}
|
|
74
|
+
}, [jobId]);
|
|
75
|
+
const onActionSelect = useCallback(() => {
|
|
58
76
|
setIsActionOpen(false);
|
|
59
77
|
onActionFocus();
|
|
60
|
-
};
|
|
78
|
+
}, [onActionFocus]);
|
|
79
|
+
const onActionToggle = useCallback((_event, val) => setIsActionOpen(val), []);
|
|
61
80
|
|
|
62
81
|
useEffect(() => {
|
|
82
|
+
let isMounted = true;
|
|
63
83
|
dispatch(
|
|
64
84
|
get({
|
|
65
85
|
key: GET_REPORT_TEMPLATES,
|
|
66
86
|
url: '/api/report_templates',
|
|
67
87
|
handleSuccess: ({ data: { results } }) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
88
|
+
if (isMounted) {
|
|
89
|
+
setReportTemplateJobId(
|
|
90
|
+
results.find(result => result.name === 'Job - Invocation Report')
|
|
91
|
+
?.id
|
|
92
|
+
);
|
|
93
|
+
}
|
|
72
94
|
},
|
|
73
95
|
handleError: () => {
|
|
74
|
-
|
|
96
|
+
if (isMounted) {
|
|
97
|
+
setReportTemplateJobId(undefined);
|
|
98
|
+
}
|
|
75
99
|
},
|
|
76
100
|
})
|
|
77
101
|
);
|
|
102
|
+
return () => {
|
|
103
|
+
isMounted = false;
|
|
104
|
+
};
|
|
78
105
|
}, [dispatch]);
|
|
79
106
|
useEffect(() => {
|
|
107
|
+
let isMounted = true;
|
|
80
108
|
if (reportTemplateJobId !== undefined) {
|
|
81
109
|
dispatch(
|
|
82
110
|
get({
|
|
83
111
|
key: GET_REPORT_TEMPLATE_INPUTS,
|
|
84
112
|
url: `/api/templates/${reportTemplateJobId}/template_inputs`,
|
|
85
113
|
handleSuccess: ({ data: { results } }) => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
114
|
+
if (isMounted) {
|
|
115
|
+
setTemplateInputId(
|
|
116
|
+
results.find(result => result.name === 'job_id')?.id
|
|
117
|
+
);
|
|
118
|
+
}
|
|
89
119
|
},
|
|
90
120
|
handleError: () => {
|
|
91
|
-
|
|
121
|
+
if (isMounted) {
|
|
122
|
+
setTemplateInputId(undefined);
|
|
123
|
+
}
|
|
92
124
|
},
|
|
93
125
|
})
|
|
94
126
|
);
|
|
95
127
|
}
|
|
128
|
+
return () => {
|
|
129
|
+
isMounted = false;
|
|
130
|
+
};
|
|
96
131
|
}, [dispatch, reportTemplateJobId]);
|
|
97
132
|
|
|
98
|
-
const recurrenceDropdownItems =
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
const recurrenceDropdownItems = useMemo(
|
|
134
|
+
() =>
|
|
135
|
+
recurrence
|
|
136
|
+
? [
|
|
137
|
+
<DropdownSeparator
|
|
138
|
+
ouiaId="dropdown-separator-1"
|
|
139
|
+
key="separator-1"
|
|
140
|
+
/>,
|
|
141
|
+
<DropdownItem
|
|
142
|
+
ouiaId="change-enabled-recurring-dropdown-item"
|
|
143
|
+
onClick={() =>
|
|
144
|
+
dispatch(enableRecurringLogic(recurrence?.id, recurringEnabled))
|
|
145
|
+
}
|
|
146
|
+
key="change-enabled-recurring"
|
|
147
|
+
component="button"
|
|
148
|
+
isDisabled={
|
|
149
|
+
recurrence?.id === undefined ||
|
|
150
|
+
recurrence?.state === 'cancelled' ||
|
|
151
|
+
!canEditRecurringLogic
|
|
152
|
+
}
|
|
153
|
+
>
|
|
154
|
+
{recurringEnabled
|
|
155
|
+
? __('Disable recurring')
|
|
156
|
+
: __('Enable recurring')}
|
|
157
|
+
</DropdownItem>,
|
|
158
|
+
<DropdownItem
|
|
159
|
+
ouiaId="cancel-recurring-dropdown-item"
|
|
160
|
+
onClick={() => dispatch(cancelRecurringLogic(recurrence?.id))}
|
|
161
|
+
key="cancel-recurring"
|
|
162
|
+
component="button"
|
|
163
|
+
isDisabled={
|
|
164
|
+
recurrence?.id === undefined ||
|
|
165
|
+
recurrence?.state === 'cancelled' ||
|
|
166
|
+
!canEditRecurringLogic
|
|
167
|
+
}
|
|
168
|
+
>
|
|
169
|
+
{__('Cancel recurring')}
|
|
170
|
+
</DropdownItem>,
|
|
171
|
+
]
|
|
172
|
+
: [],
|
|
173
|
+
[recurrence, recurringEnabled, canEditRecurringLogic, dispatch]
|
|
174
|
+
);
|
|
133
175
|
|
|
134
|
-
const dropdownItems =
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
!
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
176
|
+
const dropdownItems = useMemo(
|
|
177
|
+
() => [
|
|
178
|
+
<DropdownItem
|
|
179
|
+
ouiaId="rerun-succeeded-dropdown-item"
|
|
180
|
+
href={foremanUrl(`/job_invocations/${jobId}/rerun?succeeded_only=1`)}
|
|
181
|
+
key="rerun-succeeded"
|
|
182
|
+
isDisabled={!canCreateJobInvocations || !(succeeded > 0)}
|
|
183
|
+
description="Rerun job on successful hosts"
|
|
184
|
+
>
|
|
185
|
+
{__('Rerun successful')}
|
|
186
|
+
</DropdownItem>,
|
|
187
|
+
<DropdownItem
|
|
188
|
+
ouiaId="rerun-failed-dropdown-item"
|
|
189
|
+
href={foremanUrl(`/job_invocations/${jobId}/rerun?failed_only=1`)}
|
|
190
|
+
key="rerun-failed"
|
|
191
|
+
isDisabled={!canCreateJobInvocations || !(failed > 0)}
|
|
192
|
+
description="Rerun job on failed hosts"
|
|
193
|
+
>
|
|
194
|
+
{__('Rerun failed')}
|
|
195
|
+
</DropdownItem>,
|
|
196
|
+
<DropdownItem
|
|
197
|
+
ouiaId="view-task-dropdown-item"
|
|
198
|
+
href={foremanUrl(`/foreman_tasks/tasks/${task?.id}`)}
|
|
199
|
+
key="view-task"
|
|
200
|
+
isDisabled={!canViewForemanTasks || task === undefined}
|
|
201
|
+
description="See details of latest task"
|
|
202
|
+
>
|
|
203
|
+
{__('View task')}
|
|
204
|
+
</DropdownItem>,
|
|
205
|
+
<DropdownSeparator ouiaId="dropdown-separator-0" key="separator-0" />,
|
|
206
|
+
<DropdownItem
|
|
207
|
+
ouiaId="cancel-dropdown-item"
|
|
208
|
+
onClick={() => dispatch(cancelJob(jobId, false))}
|
|
209
|
+
key="cancel"
|
|
210
|
+
component="button"
|
|
211
|
+
isDisabled={!canCancelJobInvocations || !isTaskCancelable}
|
|
212
|
+
description="Cancel job gracefully"
|
|
213
|
+
>
|
|
214
|
+
{__('Cancel')}
|
|
215
|
+
</DropdownItem>,
|
|
216
|
+
<DropdownItem
|
|
217
|
+
ouiaId="abort-dropdown-item"
|
|
218
|
+
onClick={() => dispatch(cancelJob(jobId, true))}
|
|
219
|
+
key="abort"
|
|
220
|
+
component="button"
|
|
221
|
+
isDisabled={!canCancelJobInvocations || !isTaskCancelable}
|
|
222
|
+
description="Cancel job immediately"
|
|
223
|
+
>
|
|
224
|
+
{__('Abort')}
|
|
225
|
+
</DropdownItem>,
|
|
226
|
+
...recurrenceDropdownItems,
|
|
227
|
+
<DropdownSeparator ouiaId="dropdown-separator-2" key="separator-2" />,
|
|
228
|
+
<DropdownItem
|
|
229
|
+
ouiaId="legacy-ui-dropdown-item"
|
|
230
|
+
icon={<UndoIcon />}
|
|
231
|
+
href={`/legacy/job_invocations/${jobId}`}
|
|
232
|
+
key="legacy-ui"
|
|
233
|
+
>
|
|
234
|
+
{__('Legacy UI')}
|
|
235
|
+
</DropdownItem>,
|
|
236
|
+
],
|
|
237
|
+
[
|
|
238
|
+
canCancelJobInvocations,
|
|
239
|
+
canCreateJobInvocations,
|
|
240
|
+
canViewForemanTasks,
|
|
241
|
+
dispatch,
|
|
242
|
+
failed,
|
|
243
|
+
isTaskCancelable,
|
|
244
|
+
jobId,
|
|
245
|
+
recurrenceDropdownItems,
|
|
246
|
+
succeeded,
|
|
247
|
+
task,
|
|
248
|
+
]
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const dropdownToggle = useMemo(
|
|
252
|
+
() => (
|
|
253
|
+
<DropdownToggle
|
|
254
|
+
ouiaId="toggle-button-action-primary"
|
|
255
|
+
id={`toggle-split-button-action-primary-${jobId}`}
|
|
256
|
+
splitButtonItems={[
|
|
257
|
+
<Button
|
|
258
|
+
component="a"
|
|
259
|
+
ouiaId="button-rerun-all"
|
|
260
|
+
key="rerun"
|
|
261
|
+
href={foremanUrl(`/job_invocations/${jobId}/rerun`)}
|
|
262
|
+
variant="control"
|
|
263
|
+
isDisabled={!canCreateJobInvocations}
|
|
264
|
+
>
|
|
265
|
+
{__(`Rerun all`)}
|
|
266
|
+
</Button>,
|
|
267
|
+
]}
|
|
268
|
+
splitButtonVariant="action"
|
|
269
|
+
onToggle={onActionToggle}
|
|
270
|
+
/>
|
|
271
|
+
),
|
|
272
|
+
[canCreateJobInvocations, jobId, onActionToggle]
|
|
273
|
+
);
|
|
200
274
|
|
|
201
275
|
return (
|
|
202
276
|
<>
|
|
@@ -206,15 +280,9 @@ const JobInvocationToolbarButtons = ({ jobId, data }) => {
|
|
|
206
280
|
component="a"
|
|
207
281
|
ouiaId="button-create-report"
|
|
208
282
|
className="button-create-report"
|
|
209
|
-
href={
|
|
210
|
-
`/templates/report_templates/${reportTemplateJobId}/generate?${queryParams.toString()}`
|
|
211
|
-
)}
|
|
283
|
+
href={reportHref}
|
|
212
284
|
variant="secondary"
|
|
213
|
-
isDisabled={
|
|
214
|
-
!useHasPermission('generate_report_templates') ||
|
|
215
|
-
task?.state === STATUS.PENDING ||
|
|
216
|
-
templateInputId === undefined
|
|
217
|
-
}
|
|
285
|
+
isDisabled={isCreateReportDisabled}
|
|
218
286
|
>
|
|
219
287
|
{__(`Create report`)}
|
|
220
288
|
</Button>
|
|
@@ -224,26 +292,7 @@ const JobInvocationToolbarButtons = ({ jobId, data }) => {
|
|
|
224
292
|
ouiaId="job-invocation-global-actions-dropdown"
|
|
225
293
|
onSelect={onActionSelect}
|
|
226
294
|
position={DropdownPosition.right}
|
|
227
|
-
toggle={
|
|
228
|
-
<DropdownToggle
|
|
229
|
-
ouiaId="toggle-button-action-primary"
|
|
230
|
-
id={`toggle-split-button-action-primary-${jobId}`}
|
|
231
|
-
splitButtonItems={[
|
|
232
|
-
<Button
|
|
233
|
-
component="a"
|
|
234
|
-
ouiaId="button-rerun-all"
|
|
235
|
-
key="rerun"
|
|
236
|
-
href={foremanUrl(`/job_invocations/${jobId}/rerun`)}
|
|
237
|
-
variant="control"
|
|
238
|
-
isDisabled={!useHasPermission('create_job_invocations')}
|
|
239
|
-
>
|
|
240
|
-
{__(`Rerun all`)}
|
|
241
|
-
</Button>,
|
|
242
|
-
]}
|
|
243
|
-
splitButtonVariant="action"
|
|
244
|
-
onToggle={(_event, val) => setIsActionOpen(val)}
|
|
245
|
-
/>
|
|
246
|
-
}
|
|
295
|
+
toggle={dropdownToggle}
|
|
247
296
|
isOpen={isActionOpen}
|
|
248
297
|
dropdownItems={dropdownItems}
|
|
249
298
|
/>
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
showTemplateInvocationUrl,
|
|
12
12
|
templateInvocationPageUrl,
|
|
13
13
|
GET_TEMPLATE_INVOCATION,
|
|
14
|
+
AUTO_REFRESH_INTERVAL_MS,
|
|
14
15
|
} from './JobInvocationConstants';
|
|
15
16
|
import {
|
|
16
17
|
selectTemplateInvocationStatus,
|
|
@@ -67,7 +68,7 @@ export const TemplateInvocation = ({
|
|
|
67
68
|
showCommand,
|
|
68
69
|
setShowCommand,
|
|
69
70
|
}) => {
|
|
70
|
-
const
|
|
71
|
+
const timeoutRef = useRef(null);
|
|
71
72
|
const templateURL = showTemplateInvocationUrl(hostID, jobID);
|
|
72
73
|
const hostDetailsPageUrl = useForemanHostDetailsPageUrl();
|
|
73
74
|
|
|
@@ -81,43 +82,48 @@ export const TemplateInvocation = ({
|
|
|
81
82
|
}, [response]);
|
|
82
83
|
|
|
83
84
|
useEffect(() => {
|
|
84
|
-
|
|
85
|
+
let cancelled = false;
|
|
86
|
+
|
|
87
|
+
const schedulePoll = () => {
|
|
88
|
+
if (cancelled) return;
|
|
85
89
|
dispatch(
|
|
86
90
|
APIActions.get({
|
|
87
91
|
url: templateURL,
|
|
88
92
|
key: `${GET_TEMPLATE_INVOCATION}_${hostID}`,
|
|
93
|
+
handleSuccess: ({ data }) => {
|
|
94
|
+
if (cancelled) return;
|
|
95
|
+
const isFinished = data?.finished ?? true;
|
|
96
|
+
// eslint-disable-next-line camelcase
|
|
97
|
+
const autoRefresh = data?.auto_refresh || false;
|
|
98
|
+
if (!isFinished && autoRefresh) {
|
|
99
|
+
timeoutRef.current = setTimeout(
|
|
100
|
+
schedulePoll,
|
|
101
|
+
AUTO_REFRESH_INTERVAL_MS
|
|
102
|
+
);
|
|
103
|
+
} else {
|
|
104
|
+
timeoutRef.current = null;
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
handleError: () => {
|
|
108
|
+
if (cancelled) return;
|
|
109
|
+
timeoutRef.current = null;
|
|
110
|
+
},
|
|
89
111
|
})
|
|
90
112
|
);
|
|
91
113
|
};
|
|
92
114
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
intervalRef.current = null;
|
|
96
|
-
}
|
|
115
|
+
clearTimeout(timeoutRef.current);
|
|
116
|
+
timeoutRef.current = null;
|
|
97
117
|
|
|
98
118
|
if (isExpanded) {
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
intervalRef.current = setInterval(() => {
|
|
104
|
-
const latestResponse = responseRef.current;
|
|
105
|
-
const finished = latestResponse?.finished ?? true;
|
|
106
|
-
// eslint-disable-next-line camelcase
|
|
107
|
-
const autoRefresh = latestResponse?.auto_refresh || false;
|
|
108
|
-
|
|
109
|
-
if (!finished && autoRefresh) {
|
|
110
|
-
dispatchFetch();
|
|
111
|
-
} else if (intervalRef.current) {
|
|
112
|
-
clearInterval(intervalRef.current);
|
|
113
|
-
}
|
|
114
|
-
}, 5000);
|
|
119
|
+
if (responseRef.current?.finished) return undefined;
|
|
120
|
+
schedulePoll();
|
|
115
121
|
}
|
|
116
122
|
|
|
117
123
|
return () => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
124
|
+
cancelled = true;
|
|
125
|
+
clearTimeout(timeoutRef.current);
|
|
126
|
+
timeoutRef.current = null;
|
|
121
127
|
};
|
|
122
128
|
}, [isExpanded, dispatch, templateURL, hostID]);
|
|
123
129
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
4
|
+
import { createMemoryHistory } from 'history';
|
|
5
|
+
import { Router } from 'react-router-dom';
|
|
6
|
+
import JobInvocationEmptyState from '../JobInvocationEmptyState';
|
|
7
|
+
import { createForemanContextWrapper } from './foremanTestHelpers';
|
|
8
|
+
|
|
9
|
+
describe('JobInvocationEmptyState', () => {
|
|
10
|
+
it('renders the failed load empty state for a job invocation', () => {
|
|
11
|
+
const jobInvocationId = '99';
|
|
12
|
+
const errorMessage = 'Record not found';
|
|
13
|
+
const history = createMemoryHistory();
|
|
14
|
+
const ForemanContextWrapper = createForemanContextWrapper();
|
|
15
|
+
|
|
16
|
+
render(
|
|
17
|
+
<Router history={history}>
|
|
18
|
+
<ForemanContextWrapper>
|
|
19
|
+
<JobInvocationEmptyState
|
|
20
|
+
jobInvocationId={jobInvocationId}
|
|
21
|
+
httpStatus={404}
|
|
22
|
+
errorMessage={errorMessage}
|
|
23
|
+
/>
|
|
24
|
+
</ForemanContextWrapper>
|
|
25
|
+
</Router>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
expect(
|
|
29
|
+
screen.getByRole('heading', {
|
|
30
|
+
name: 'Unable to load job invocation',
|
|
31
|
+
level: 5,
|
|
32
|
+
})
|
|
33
|
+
).toBeInTheDocument();
|
|
34
|
+
expect(
|
|
35
|
+
screen.getByText(
|
|
36
|
+
(_, element) =>
|
|
37
|
+
element?.tagName === 'P' &&
|
|
38
|
+
element.textContent?.includes(
|
|
39
|
+
`The job invocation with id ${jobInvocationId} could not be found. It may have been deleted or may not be available in your current organization or location scope.`
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
).toBeInTheDocument();
|
|
43
|
+
expect(
|
|
44
|
+
screen.getByText(`Server returned: ${errorMessage}`)
|
|
45
|
+
).toBeInTheDocument();
|
|
46
|
+
expect(
|
|
47
|
+
screen.getByRole('button', { name: 'Go to job invocations' })
|
|
48
|
+
).toBeInTheDocument();
|
|
49
|
+
expect(
|
|
50
|
+
screen.getByRole('button', { name: 'Create a new job invocation' })
|
|
51
|
+
).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
});
|