foreman_remote_execution 16.6.5 → 16.7.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 +15 -11
- 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 +2 -1
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +4 -4
- data/webpack/JobInvocationDetail/JobInvocationEmptyState.js +52 -0
- data/webpack/JobInvocationDetail/JobInvocationSelectors.js +0 -15
- data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +201 -150
- data/webpack/JobInvocationDetail/__tests__/JobInvocationDetailEmptyState.test.js +53 -0
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +109 -11
- data/webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js +3 -1
- data/webpack/JobInvocationDetail/__tests__/foremanTestHelpers.js +30 -0
- data/webpack/JobInvocationDetail/index.js +42 -9
- 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
- metadata +6 -2
|
@@ -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,238 @@ 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 onActionFocus = () => {
|
|
61
|
+
const onActionFocus = useCallback(() => {
|
|
52
62
|
const element = document.getElementById(
|
|
53
63
|
`toggle-split-button-action-primary-${jobId}`
|
|
54
64
|
);
|
|
55
|
-
element
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
if (element) {
|
|
66
|
+
element.focus();
|
|
67
|
+
}
|
|
68
|
+
}, [jobId]);
|
|
69
|
+
const onActionSelect = useCallback(() => {
|
|
58
70
|
setIsActionOpen(false);
|
|
59
71
|
onActionFocus();
|
|
60
|
-
};
|
|
72
|
+
}, [onActionFocus]);
|
|
73
|
+
const onActionToggle = useCallback((_event, val) => setIsActionOpen(val), []);
|
|
61
74
|
|
|
62
75
|
useEffect(() => {
|
|
76
|
+
let isMounted = true;
|
|
63
77
|
dispatch(
|
|
64
78
|
get({
|
|
65
79
|
key: GET_REPORT_TEMPLATES,
|
|
66
80
|
url: '/api/report_templates',
|
|
67
81
|
handleSuccess: ({ data: { results } }) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
82
|
+
if (isMounted) {
|
|
83
|
+
setReportTemplateJobId(
|
|
84
|
+
results.find(result => result.name === 'Job - Invocation Report')
|
|
85
|
+
?.id
|
|
86
|
+
);
|
|
87
|
+
}
|
|
72
88
|
},
|
|
73
89
|
handleError: () => {
|
|
74
|
-
|
|
90
|
+
if (isMounted) {
|
|
91
|
+
setReportTemplateJobId(undefined);
|
|
92
|
+
}
|
|
75
93
|
},
|
|
76
94
|
})
|
|
77
95
|
);
|
|
96
|
+
return () => {
|
|
97
|
+
isMounted = false;
|
|
98
|
+
};
|
|
78
99
|
}, [dispatch]);
|
|
79
100
|
useEffect(() => {
|
|
101
|
+
let isMounted = true;
|
|
80
102
|
if (reportTemplateJobId !== undefined) {
|
|
81
103
|
dispatch(
|
|
82
104
|
get({
|
|
83
105
|
key: GET_REPORT_TEMPLATE_INPUTS,
|
|
84
106
|
url: `/api/templates/${reportTemplateJobId}/template_inputs`,
|
|
85
107
|
handleSuccess: ({ data: { results } }) => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
108
|
+
if (isMounted) {
|
|
109
|
+
setTemplateInputId(
|
|
110
|
+
results.find(result => result.name === 'job_id')?.id
|
|
111
|
+
);
|
|
112
|
+
}
|
|
89
113
|
},
|
|
90
114
|
handleError: () => {
|
|
91
|
-
|
|
115
|
+
if (isMounted) {
|
|
116
|
+
setTemplateInputId(undefined);
|
|
117
|
+
}
|
|
92
118
|
},
|
|
93
119
|
})
|
|
94
120
|
);
|
|
95
121
|
}
|
|
122
|
+
return () => {
|
|
123
|
+
isMounted = false;
|
|
124
|
+
};
|
|
96
125
|
}, [dispatch, reportTemplateJobId]);
|
|
97
126
|
|
|
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
|
-
|
|
127
|
+
const recurrenceDropdownItems = useMemo(
|
|
128
|
+
() =>
|
|
129
|
+
recurrence
|
|
130
|
+
? [
|
|
131
|
+
<DropdownSeparator
|
|
132
|
+
ouiaId="dropdown-separator-1"
|
|
133
|
+
key="separator-1"
|
|
134
|
+
/>,
|
|
135
|
+
<DropdownItem
|
|
136
|
+
ouiaId="change-enabled-recurring-dropdown-item"
|
|
137
|
+
onClick={() =>
|
|
138
|
+
dispatch(
|
|
139
|
+
enableRecurringLogic(recurrence?.id, recurringEnabled, jobId)
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
key="change-enabled-recurring"
|
|
143
|
+
component="button"
|
|
144
|
+
isDisabled={
|
|
145
|
+
recurrence?.id === undefined ||
|
|
146
|
+
recurrence?.state === 'cancelled' ||
|
|
147
|
+
!canEditRecurringLogic
|
|
148
|
+
}
|
|
149
|
+
>
|
|
150
|
+
{recurringEnabled
|
|
151
|
+
? __('Disable recurring')
|
|
152
|
+
: __('Enable recurring')}
|
|
153
|
+
</DropdownItem>,
|
|
154
|
+
<DropdownItem
|
|
155
|
+
ouiaId="cancel-recurring-dropdown-item"
|
|
156
|
+
onClick={() =>
|
|
157
|
+
dispatch(cancelRecurringLogic(recurrence?.id, jobId))
|
|
158
|
+
}
|
|
159
|
+
key="cancel-recurring"
|
|
160
|
+
component="button"
|
|
161
|
+
isDisabled={
|
|
162
|
+
recurrence?.id === undefined ||
|
|
163
|
+
recurrence?.state === 'cancelled' ||
|
|
164
|
+
!canEditRecurringLogic
|
|
165
|
+
}
|
|
166
|
+
>
|
|
167
|
+
{__('Cancel recurring')}
|
|
168
|
+
</DropdownItem>,
|
|
169
|
+
]
|
|
170
|
+
: [],
|
|
171
|
+
[recurrence, recurringEnabled, canEditRecurringLogic, dispatch, jobId]
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const dropdownItems = useMemo(
|
|
175
|
+
() => [
|
|
176
|
+
<DropdownItem
|
|
177
|
+
ouiaId="rerun-succeeded-dropdown-item"
|
|
178
|
+
href={foremanUrl(`/job_invocations/${jobId}/rerun?succeeded_only=1`)}
|
|
179
|
+
key="rerun-succeeded"
|
|
180
|
+
isDisabled={!canCreateJobInvocations || !(succeeded > 0)}
|
|
181
|
+
description="Rerun job on successful hosts"
|
|
182
|
+
>
|
|
183
|
+
{__('Rerun successful')}
|
|
184
|
+
</DropdownItem>,
|
|
185
|
+
<DropdownItem
|
|
186
|
+
ouiaId="rerun-failed-dropdown-item"
|
|
187
|
+
href={foremanUrl(`/job_invocations/${jobId}/rerun?failed_only=1`)}
|
|
188
|
+
key="rerun-failed"
|
|
189
|
+
isDisabled={!canCreateJobInvocations || !(failed > 0)}
|
|
190
|
+
description="Rerun job on failed hosts"
|
|
191
|
+
>
|
|
192
|
+
{__('Rerun failed')}
|
|
193
|
+
</DropdownItem>,
|
|
194
|
+
<DropdownItem
|
|
195
|
+
ouiaId="view-task-dropdown-item"
|
|
196
|
+
href={foremanUrl(`/foreman_tasks/tasks/${task?.id}`)}
|
|
197
|
+
key="view-task"
|
|
198
|
+
isDisabled={!canViewForemanTasks || task === undefined}
|
|
199
|
+
description="See details of latest task"
|
|
200
|
+
>
|
|
201
|
+
{__('View task')}
|
|
202
|
+
</DropdownItem>,
|
|
203
|
+
<DropdownSeparator ouiaId="dropdown-separator-0" key="separator-0" />,
|
|
204
|
+
<DropdownItem
|
|
205
|
+
ouiaId="cancel-dropdown-item"
|
|
206
|
+
onClick={() => dispatch(cancelJob(jobId, false))}
|
|
207
|
+
key="cancel"
|
|
208
|
+
component="button"
|
|
209
|
+
isDisabled={!canCancelJobInvocations || !isTaskCancelable}
|
|
210
|
+
description="Cancel job gracefully"
|
|
211
|
+
>
|
|
212
|
+
{__('Cancel')}
|
|
213
|
+
</DropdownItem>,
|
|
214
|
+
<DropdownItem
|
|
215
|
+
ouiaId="abort-dropdown-item"
|
|
216
|
+
onClick={() => dispatch(cancelJob(jobId, true))}
|
|
217
|
+
key="abort"
|
|
218
|
+
component="button"
|
|
219
|
+
isDisabled={!canCancelJobInvocations || !isTaskCancelable}
|
|
220
|
+
description="Cancel job immediately"
|
|
221
|
+
>
|
|
222
|
+
{__('Abort')}
|
|
223
|
+
</DropdownItem>,
|
|
224
|
+
...recurrenceDropdownItems,
|
|
225
|
+
<DropdownSeparator ouiaId="dropdown-separator-2" key="separator-2" />,
|
|
226
|
+
<DropdownItem
|
|
227
|
+
ouiaId="legacy-ui-dropdown-item"
|
|
228
|
+
icon={<UndoIcon />}
|
|
229
|
+
href={`/legacy/job_invocations/${jobId}`}
|
|
230
|
+
key="legacy-ui"
|
|
231
|
+
>
|
|
232
|
+
{__('Legacy UI')}
|
|
233
|
+
</DropdownItem>,
|
|
234
|
+
],
|
|
235
|
+
[
|
|
236
|
+
canCancelJobInvocations,
|
|
237
|
+
canCreateJobInvocations,
|
|
238
|
+
canViewForemanTasks,
|
|
239
|
+
dispatch,
|
|
240
|
+
failed,
|
|
241
|
+
isTaskCancelable,
|
|
242
|
+
jobId,
|
|
243
|
+
recurrenceDropdownItems,
|
|
244
|
+
succeeded,
|
|
245
|
+
task,
|
|
246
|
+
]
|
|
247
|
+
);
|
|
133
248
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
href={foremanUrl(`/foreman_tasks/tasks/${task?.id}`)}
|
|
158
|
-
key="view-task"
|
|
159
|
-
isDisabled={!canViewForemanTasks || task === undefined}
|
|
160
|
-
description="See details of latest task"
|
|
161
|
-
>
|
|
162
|
-
{__('View task')}
|
|
163
|
-
</DropdownItem>,
|
|
164
|
-
<DropdownSeparator ouiaId="dropdown-separator-0" key="separator-0" />,
|
|
165
|
-
<DropdownItem
|
|
166
|
-
ouiaId="cancel-dropdown-item"
|
|
167
|
-
onClick={() => dispatch(cancelJob(jobId, false))}
|
|
168
|
-
key="cancel"
|
|
169
|
-
component="button"
|
|
170
|
-
isDisabled={
|
|
171
|
-
!useHasPermission('cancel_job_invocations') || !isTaskCancelable
|
|
172
|
-
}
|
|
173
|
-
description="Cancel job gracefully"
|
|
174
|
-
>
|
|
175
|
-
{__('Cancel')}
|
|
176
|
-
</DropdownItem>,
|
|
177
|
-
<DropdownItem
|
|
178
|
-
ouiaId="abort-dropdown-item"
|
|
179
|
-
onClick={() => dispatch(cancelJob(jobId, true))}
|
|
180
|
-
key="abort"
|
|
181
|
-
component="button"
|
|
182
|
-
isDisabled={
|
|
183
|
-
!useHasPermission('cancel_job_invocations') || !isTaskCancelable
|
|
184
|
-
}
|
|
185
|
-
description="Cancel job immediately"
|
|
186
|
-
>
|
|
187
|
-
{__('Abort')}
|
|
188
|
-
</DropdownItem>,
|
|
189
|
-
...recurrenceDropdownItems,
|
|
190
|
-
<DropdownSeparator ouiaId="dropdown-separator-2" key="separator-2" />,
|
|
191
|
-
<DropdownItem
|
|
192
|
-
ouiaId="legacy-ui-dropdown-item"
|
|
193
|
-
icon={<UndoIcon />}
|
|
194
|
-
href={`/legacy/job_invocations/${jobId}`}
|
|
195
|
-
key="legacy-ui"
|
|
196
|
-
>
|
|
197
|
-
{__('Legacy UI')}
|
|
198
|
-
</DropdownItem>,
|
|
199
|
-
];
|
|
249
|
+
const dropdownToggle = useMemo(
|
|
250
|
+
() => (
|
|
251
|
+
<DropdownToggle
|
|
252
|
+
ouiaId="toggle-button-action-primary"
|
|
253
|
+
id={`toggle-split-button-action-primary-${jobId}`}
|
|
254
|
+
splitButtonItems={[
|
|
255
|
+
<Button
|
|
256
|
+
component="a"
|
|
257
|
+
ouiaId="button-rerun-all"
|
|
258
|
+
key="rerun"
|
|
259
|
+
href={foremanUrl(`/job_invocations/${jobId}/rerun`)}
|
|
260
|
+
variant="control"
|
|
261
|
+
isDisabled={!canCreateJobInvocations}
|
|
262
|
+
>
|
|
263
|
+
{__(`Rerun all`)}
|
|
264
|
+
</Button>,
|
|
265
|
+
]}
|
|
266
|
+
splitButtonVariant="action"
|
|
267
|
+
onToggle={onActionToggle}
|
|
268
|
+
/>
|
|
269
|
+
),
|
|
270
|
+
[canCreateJobInvocations, jobId, onActionToggle]
|
|
271
|
+
);
|
|
200
272
|
|
|
201
273
|
return (
|
|
202
274
|
<>
|
|
@@ -206,14 +278,12 @@ const JobInvocationToolbarButtons = ({ jobId, data }) => {
|
|
|
206
278
|
component="a"
|
|
207
279
|
ouiaId="button-create-report"
|
|
208
280
|
className="button-create-report"
|
|
209
|
-
href={
|
|
210
|
-
`/templates/report_templates/${reportTemplateJobId}/generate?${queryParams.toString()}`
|
|
211
|
-
)}
|
|
281
|
+
href={reportHref}
|
|
212
282
|
variant="secondary"
|
|
213
283
|
isDisabled={
|
|
214
|
-
!
|
|
284
|
+
!canGenerateReportTemplates ||
|
|
215
285
|
task?.state === STATUS.PENDING ||
|
|
216
|
-
|
|
286
|
+
reportHref === undefined
|
|
217
287
|
}
|
|
218
288
|
>
|
|
219
289
|
{__(`Create report`)}
|
|
@@ -224,26 +294,7 @@ const JobInvocationToolbarButtons = ({ jobId, data }) => {
|
|
|
224
294
|
ouiaId="job-invocation-global-actions-dropdown"
|
|
225
295
|
onSelect={onActionSelect}
|
|
226
296
|
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
|
-
}
|
|
297
|
+
toggle={dropdownToggle}
|
|
247
298
|
isOpen={isActionOpen}
|
|
248
299
|
dropdownItems={dropdownItems}
|
|
249
300
|
/>
|
|
@@ -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
|
+
});
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
import React from 'react';
|
|
2
3
|
import configureMockStore from 'redux-mock-store';
|
|
3
4
|
import { fireEvent, render, screen, act } from '@testing-library/react';
|
|
4
5
|
import '@testing-library/jest-dom/extend-expect';
|
|
6
|
+
import { createMemoryHistory } from 'history';
|
|
7
|
+
import { Router } from 'react-router-dom';
|
|
5
8
|
import { Provider } from 'react-redux';
|
|
6
9
|
import thunk from 'redux-thunk';
|
|
10
|
+
import { STATUS } from 'foremanReact/constants';
|
|
7
11
|
import { foremanUrl } from 'foremanReact/common/helpers';
|
|
8
12
|
import * as api from 'foremanReact/redux/API';
|
|
9
13
|
import JobInvocationDetailPage from '../index';
|
|
@@ -11,7 +15,6 @@ import {
|
|
|
11
15
|
jobInvocationData,
|
|
12
16
|
jobInvocationDataScheduled,
|
|
13
17
|
jobInvocationDataRecurring,
|
|
14
|
-
mockPermissionsData,
|
|
15
18
|
mockReportTemplatesResponse,
|
|
16
19
|
mockReportTemplateInputsResponse,
|
|
17
20
|
} from './fixtures';
|
|
@@ -26,8 +29,10 @@ import {
|
|
|
26
29
|
CHANGE_ENABLED_RECURRING_LOGIC,
|
|
27
30
|
GET_REPORT_TEMPLATES,
|
|
28
31
|
GET_REPORT_TEMPLATE_INPUTS,
|
|
32
|
+
GET_TASK,
|
|
29
33
|
JOB_INVOCATION_KEY,
|
|
30
34
|
} from '../JobInvocationConstants';
|
|
35
|
+
import { createForemanContextWrapper } from './foremanTestHelpers';
|
|
31
36
|
|
|
32
37
|
jest.spyOn(api, 'get');
|
|
33
38
|
|
|
@@ -35,8 +40,11 @@ jest.spyOn(api, 'get');
|
|
|
35
40
|
const originalToLocaleString = Date.prototype.toLocaleString;
|
|
36
41
|
beforeAll(() => {
|
|
37
42
|
// eslint-disable-next-line no-extend-native
|
|
38
|
-
Date.prototype.toLocaleString = function (locale, options) {
|
|
39
|
-
return originalToLocaleString.call(this, locale, {
|
|
43
|
+
Date.prototype.toLocaleString = function toLocaleStringUTC(locale, options) {
|
|
44
|
+
return originalToLocaleString.call(this, locale, {
|
|
45
|
+
...options,
|
|
46
|
+
timeZone: 'UTC',
|
|
47
|
+
});
|
|
40
48
|
};
|
|
41
49
|
});
|
|
42
50
|
afterAll(() => {
|
|
@@ -44,12 +52,6 @@ afterAll(() => {
|
|
|
44
52
|
Date.prototype.toLocaleString = originalToLocaleString;
|
|
45
53
|
});
|
|
46
54
|
|
|
47
|
-
jest.mock('foremanReact/common/hooks/API/APIHooks', () => ({
|
|
48
|
-
useAPI: jest.fn(() => ({
|
|
49
|
-
response: mockPermissionsData,
|
|
50
|
-
})),
|
|
51
|
-
}));
|
|
52
|
-
|
|
53
55
|
jest.mock('foremanReact/routes/common/PageLayout/PageLayout', () =>
|
|
54
56
|
jest.fn(props => (
|
|
55
57
|
<div>
|
|
@@ -63,6 +65,7 @@ jest.mock('foremanReact/routes/common/PageLayout/PageLayout', () =>
|
|
|
63
65
|
const initialState = {
|
|
64
66
|
JOB_INVOCATION_KEY: {
|
|
65
67
|
response: jobInvocationData,
|
|
68
|
+
status: STATUS.RESOLVED,
|
|
66
69
|
},
|
|
67
70
|
GET_REPORT_TEMPLATES: mockReportTemplatesResponse,
|
|
68
71
|
extendable: {},
|
|
@@ -71,6 +74,7 @@ const initialState = {
|
|
|
71
74
|
const initialStateScheduled = {
|
|
72
75
|
JOB_INVOCATION_KEY: {
|
|
73
76
|
response: jobInvocationDataScheduled,
|
|
77
|
+
status: STATUS.RESOLVED,
|
|
74
78
|
},
|
|
75
79
|
extendable: {},
|
|
76
80
|
};
|
|
@@ -91,6 +95,15 @@ api.get.mockImplementation(({ handleSuccess, ...action }) => {
|
|
|
91
95
|
return { type: 'get', ...action };
|
|
92
96
|
});
|
|
93
97
|
|
|
98
|
+
const initialStateRecurring = {
|
|
99
|
+
JOB_INVOCATION_KEY: {
|
|
100
|
+
response: jobInvocationDataRecurring,
|
|
101
|
+
status: STATUS.RESOLVED,
|
|
102
|
+
},
|
|
103
|
+
GET_REPORT_TEMPLATES: mockReportTemplatesResponse,
|
|
104
|
+
extendable: {},
|
|
105
|
+
};
|
|
106
|
+
|
|
94
107
|
jest.mock('../JobInvocationHostTable.js', () => () => (
|
|
95
108
|
<div data-testid="mock-table">Mock Table</div>
|
|
96
109
|
));
|
|
@@ -198,6 +211,29 @@ describe('JobInvocationDetailPage', () => {
|
|
|
198
211
|
).toEqual(`/legacy/job_invocations/${jobId}`);
|
|
199
212
|
});
|
|
200
213
|
|
|
214
|
+
it('keeps toolbar buttons mounted while job invocation data is refreshing', async () => {
|
|
215
|
+
const jobId = jobInvocationData.id;
|
|
216
|
+
const store = mockStore({
|
|
217
|
+
...initialState,
|
|
218
|
+
JOB_INVOCATION_KEY: {
|
|
219
|
+
response: jobInvocationData,
|
|
220
|
+
status: STATUS.PENDING,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
render(
|
|
225
|
+
<Provider store={store}>
|
|
226
|
+
<JobInvocationDetailPage
|
|
227
|
+
match={{ params: { id: `${jobId}` } }}
|
|
228
|
+
{...props}
|
|
229
|
+
/>
|
|
230
|
+
</Provider>
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
expect(screen.getByText('Create report')).toBeInTheDocument();
|
|
234
|
+
expect(screen.getByText('Rerun all')).toBeInTheDocument();
|
|
235
|
+
});
|
|
236
|
+
|
|
201
237
|
it('shows scheduled date', async () => {
|
|
202
238
|
const store = mockStore(initialStateScheduled);
|
|
203
239
|
render(
|
|
@@ -217,8 +253,9 @@ describe('JobInvocationDetailPage', () => {
|
|
|
217
253
|
it('should dispatch global actions', async () => {
|
|
218
254
|
// recurring in the future
|
|
219
255
|
const jobId = jobInvocationDataRecurring.id;
|
|
256
|
+
const taskId = jobInvocationDataRecurring.task.id;
|
|
220
257
|
const recurrenceId = jobInvocationDataRecurring.recurrence.id;
|
|
221
|
-
const store = mockStore(
|
|
258
|
+
const store = mockStore(initialStateRecurring);
|
|
222
259
|
render(
|
|
223
260
|
<Provider store={store}>
|
|
224
261
|
<JobInvocationDetailPage
|
|
@@ -232,7 +269,11 @@ describe('JobInvocationDetailPage', () => {
|
|
|
232
269
|
{ key: GET_REPORT_TEMPLATES, url: '/api/report_templates' },
|
|
233
270
|
{
|
|
234
271
|
key: JOB_INVOCATION_KEY,
|
|
235
|
-
url: `/api/job_invocations/${jobId}
|
|
272
|
+
url: `/api/job_invocations/${jobId}`,
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
key: GET_TASK,
|
|
276
|
+
url: `/foreman_tasks/api/tasks/${taskId}`,
|
|
236
277
|
},
|
|
237
278
|
{
|
|
238
279
|
key: GET_REPORT_TEMPLATE_INPUTS,
|
|
@@ -281,4 +322,61 @@ describe('JobInvocationDetailPage', () => {
|
|
|
281
322
|
}
|
|
282
323
|
});
|
|
283
324
|
});
|
|
325
|
+
|
|
326
|
+
it('renders empty state when the job invocation fails to load', () => {
|
|
327
|
+
const jobId = '99';
|
|
328
|
+
const errorMessage = 'Record not found';
|
|
329
|
+
const history = createMemoryHistory();
|
|
330
|
+
const ForemanContextWrapper = createForemanContextWrapper();
|
|
331
|
+
const store = mockStore({
|
|
332
|
+
JOB_INVOCATION_KEY: {
|
|
333
|
+
status: STATUS.ERROR,
|
|
334
|
+
response: {
|
|
335
|
+
message: errorMessage,
|
|
336
|
+
response: { status: 404 },
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
extendable: {},
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
render(
|
|
343
|
+
<Router history={history}>
|
|
344
|
+
<ForemanContextWrapper>
|
|
345
|
+
<Provider store={store}>
|
|
346
|
+
<JobInvocationDetailPage
|
|
347
|
+
match={{ params: { id: jobId } }}
|
|
348
|
+
history={history}
|
|
349
|
+
/>
|
|
350
|
+
</Provider>
|
|
351
|
+
</ForemanContextWrapper>
|
|
352
|
+
</Router>
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
expect(
|
|
356
|
+
screen.getByRole('heading', {
|
|
357
|
+
name: 'Unable to load job invocation',
|
|
358
|
+
level: 5,
|
|
359
|
+
})
|
|
360
|
+
).toBeInTheDocument();
|
|
361
|
+
expect(
|
|
362
|
+
screen.getByText(
|
|
363
|
+
(_, element) =>
|
|
364
|
+
element?.tagName === 'P' &&
|
|
365
|
+
element.textContent?.includes(
|
|
366
|
+
`The job invocation with id ${jobId} could not be found. It may have been deleted or may not be available in your current organization or location scope.`
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
).toBeInTheDocument();
|
|
370
|
+
expect(
|
|
371
|
+
screen.getByText(`Server returned: ${errorMessage}`)
|
|
372
|
+
).toBeInTheDocument();
|
|
373
|
+
expect(
|
|
374
|
+
screen.getByRole('button', { name: 'Go to job invocations' })
|
|
375
|
+
).toBeInTheDocument();
|
|
376
|
+
expect(
|
|
377
|
+
screen.getByRole('button', { name: 'Create a new job invocation' })
|
|
378
|
+
).toBeInTheDocument();
|
|
379
|
+
expect(screen.queryByText('Description')).not.toBeInTheDocument();
|
|
380
|
+
expect(screen.queryByTestId('mock-table')).not.toBeInTheDocument();
|
|
381
|
+
});
|
|
284
382
|
});
|