foreman_remote_execution 16.7.0 → 17.1.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 +24 -2
- data/app/controllers/job_invocations_controller.rb +1 -28
- data/app/views/api/v2/job_invocations/hosts.json.rabl +8 -0
- data/app/views/api/v2/job_invocations/main.json.rabl +1 -0
- data/app/views/job_templates/_alerts.html.erb +4 -0
- data/config/routes.rb +0 -1
- data/lib/foreman_remote_execution/plugin.rb +3 -3
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/functional/api/v2/job_invocations_controller_test.rb +46 -7
- data/webpack/JobInvocationDetail/JobInvocationActions.js +86 -91
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +6 -4
- data/webpack/JobInvocationDetail/JobInvocationDetail.scss +5 -3
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +92 -46
- data/webpack/JobInvocationDetail/JobInvocationSelectors.js +1 -9
- data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +5 -2
- data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +10 -12
- data/webpack/JobInvocationDetail/TemplateInvocation.js +38 -26
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js +2 -1
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +17 -6
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/index.scss +0 -1
- data/webpack/JobInvocationDetail/__tests__/JobInvocationHostTablePolling.test.js +369 -0
- data/webpack/JobInvocationDetail/__tests__/JobInvocationPolling.test.js +228 -0
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +205 -154
- data/webpack/JobInvocationDetail/__tests__/TemplateInvocationPolling.test.js +254 -0
- data/webpack/JobInvocationDetail/__tests__/fixtures.js +2 -0
- data/webpack/JobInvocationDetail/index.js +25 -25
- data/webpack/JobWizard/JobWizard.js +5 -1
- data/webpack/JobWizard/JobWizard.scss +21 -0
- data/webpack/JobWizard/JobWizardConstants.js +10 -1
- data/webpack/JobWizard/steps/Schedule/RepeatHour.js +4 -2
- data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +4 -2
- data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +2 -1
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +4 -1
- data/webpack/JobWizard/steps/form/DateTimePicker.js +12 -6
- data/webpack/JobWizard/steps/form/FormHelpers.js +5 -4
- data/webpack/react_app/components/FeaturesDropdown/index.scss +2 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +6 -2
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -1
- data/webpack/react_app/components/TargetingHosts/index.js +2 -1
- data/webpack/react_app/redux/actions/jobInvocations/index.js +3 -2
- data/webpack/test_setup.js +1 -13
- metadata +7 -4
- data/webpack/__mocks__/foremanReact/constants.js +0 -25
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import '@testing-library/jest-dom/extend-expect';
|
|
3
|
+
import { fireEvent, screen, act } from '@testing-library/react';
|
|
4
|
+
import '@testing-library/jest-dom';
|
|
6
5
|
import { createMemoryHistory } from 'history';
|
|
7
6
|
import { Router } from 'react-router-dom';
|
|
8
|
-
import {
|
|
9
|
-
import thunk from 'redux-thunk';
|
|
7
|
+
import { rtlHelpers } from 'foremanReact/common/testHelpers';
|
|
10
8
|
import { STATUS } from 'foremanReact/constants';
|
|
11
9
|
import { foremanUrl } from 'foremanReact/common/helpers';
|
|
10
|
+
import { usePermissions } from 'foremanReact/common/hooks/Permissions/permissionHooks';
|
|
12
11
|
import * as api from 'foremanReact/redux/API';
|
|
12
|
+
import { APIActions } from 'foremanReact/redux/API';
|
|
13
13
|
import JobInvocationDetailPage from '../index';
|
|
14
14
|
import {
|
|
15
15
|
jobInvocationData,
|
|
@@ -23,18 +23,38 @@ import {
|
|
|
23
23
|
enableRecurringLogic,
|
|
24
24
|
cancelRecurringLogic,
|
|
25
25
|
} from '../JobInvocationActions';
|
|
26
|
+
import { createForemanContextWrapper } from './foremanTestHelpers';
|
|
26
27
|
import {
|
|
27
28
|
CANCEL_JOB,
|
|
28
29
|
CANCEL_RECURRING_LOGIC,
|
|
29
30
|
CHANGE_ENABLED_RECURRING_LOGIC,
|
|
30
31
|
GET_REPORT_TEMPLATES,
|
|
31
32
|
GET_REPORT_TEMPLATE_INPUTS,
|
|
32
|
-
GET_TASK,
|
|
33
33
|
JOB_INVOCATION_KEY,
|
|
34
34
|
} from '../JobInvocationConstants';
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
const { renderWithStore } = rtlHelpers;
|
|
37
|
+
|
|
38
|
+
jest.mock('foremanReact/redux/API/APISelectors', () =>
|
|
39
|
+
jest.requireActual('foremanReact/redux/API/APISelectors')
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
jest.mock('foremanReact/common/hooks/Permissions/permissionHooks');
|
|
43
|
+
jest.mock('../JobInvocationActions', () => {
|
|
44
|
+
const actual = jest.requireActual('../JobInvocationActions');
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
...actual,
|
|
48
|
+
getJobInvocation: jest.fn(() => () => undefined),
|
|
49
|
+
stopJobInvocationPolling: jest.fn(),
|
|
50
|
+
};
|
|
51
|
+
});
|
|
36
52
|
|
|
37
53
|
jest.spyOn(api, 'get');
|
|
54
|
+
jest.spyOn(APIActions, 'post');
|
|
55
|
+
jest.spyOn(APIActions, 'put');
|
|
56
|
+
|
|
57
|
+
usePermissions.mockReturnValue(true);
|
|
38
58
|
|
|
39
59
|
// Mock toLocaleString to always use UTC timezone for consistent test results
|
|
40
60
|
const originalToLocaleString = Date.prototype.toLocaleString;
|
|
@@ -62,46 +82,49 @@ jest.mock('foremanReact/routes/common/PageLayout/PageLayout', () =>
|
|
|
62
82
|
))
|
|
63
83
|
);
|
|
64
84
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
85
|
+
const setupApiMocks = () => {
|
|
86
|
+
api.get.mockImplementation(({ handleSuccess, key, ...action }) => {
|
|
87
|
+
if (key === GET_REPORT_TEMPLATES) {
|
|
88
|
+
if (handleSuccess) {
|
|
89
|
+
handleSuccess({
|
|
90
|
+
data: mockReportTemplatesResponse,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
} else if (key === GET_REPORT_TEMPLATE_INPUTS) {
|
|
94
|
+
if (handleSuccess) {
|
|
95
|
+
handleSuccess({
|
|
96
|
+
data: mockReportTemplateInputsResponse,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
73
100
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
},
|
|
79
|
-
extendable: {},
|
|
101
|
+
return { type: 'get', key, ...action };
|
|
102
|
+
});
|
|
103
|
+
APIActions.post.mockImplementation(payload => ({ type: 'post', ...payload }));
|
|
104
|
+
APIActions.put.mockImplementation(payload => ({ type: 'put', ...payload }));
|
|
80
105
|
};
|
|
81
106
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
handleSuccess &&
|
|
90
|
-
handleSuccess({
|
|
91
|
-
data: mockReportTemplateInputsResponse,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
107
|
+
const createJobInvocationDetailState = ({
|
|
108
|
+
jobInvocation = jobInvocationData,
|
|
109
|
+
jobInvocationStatus = STATUS.RESOLVED,
|
|
110
|
+
jobInvocationError = null,
|
|
111
|
+
} = {}) => {
|
|
112
|
+
const jobInvocationResponse =
|
|
113
|
+
jobInvocationError || JSON.parse(JSON.stringify(jobInvocation));
|
|
94
114
|
|
|
95
|
-
return {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
115
|
+
return {
|
|
116
|
+
API: {
|
|
117
|
+
[JOB_INVOCATION_KEY]: jobInvocationError
|
|
118
|
+
? {
|
|
119
|
+
response: jobInvocationResponse,
|
|
120
|
+
status: STATUS.ERROR,
|
|
121
|
+
}
|
|
122
|
+
: {
|
|
123
|
+
response: jobInvocationResponse,
|
|
124
|
+
status: jobInvocationStatus,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
};
|
|
105
128
|
};
|
|
106
129
|
|
|
107
130
|
jest.mock('../JobInvocationHostTable.js', () => () => (
|
|
@@ -110,21 +133,62 @@ jest.mock('../JobInvocationHostTable.js', () => () => (
|
|
|
110
133
|
|
|
111
134
|
const reportTemplateJobId = mockReportTemplatesResponse.results[0].id;
|
|
112
135
|
|
|
113
|
-
const
|
|
114
|
-
|
|
136
|
+
const defaultHistory = { push: jest.fn() };
|
|
137
|
+
|
|
138
|
+
const renderJobInvocationDetailPage = (
|
|
139
|
+
initialState,
|
|
140
|
+
{ jobId, history = defaultHistory } = {}
|
|
141
|
+
) => {
|
|
142
|
+
const id =
|
|
143
|
+
jobId ?? initialState.API?.[JOB_INVOCATION_KEY]?.response?.id ?? '1';
|
|
144
|
+
|
|
145
|
+
return renderWithStore(
|
|
146
|
+
<JobInvocationDetailPage
|
|
147
|
+
match={{ params: { id: `${id}` } }}
|
|
148
|
+
history={history}
|
|
149
|
+
/>,
|
|
150
|
+
initialState,
|
|
151
|
+
undefined
|
|
152
|
+
);
|
|
153
|
+
};
|
|
115
154
|
|
|
116
155
|
describe('JobInvocationDetailPage', () => {
|
|
156
|
+
beforeEach(() => {
|
|
157
|
+
setupApiMocks();
|
|
158
|
+
usePermissions.mockReturnValue(true);
|
|
159
|
+
|
|
160
|
+
const { getJobInvocation } = jest.requireMock('../JobInvocationActions');
|
|
161
|
+
getJobInvocation.mockImplementation(() => () => undefined);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
afterEach(() => {
|
|
165
|
+
api.get.mockClear();
|
|
166
|
+
APIActions.post.mockClear();
|
|
167
|
+
APIActions.put.mockClear();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('shows scheduled date', async () => {
|
|
171
|
+
const scheduledJobInvocation = JSON.parse(
|
|
172
|
+
JSON.stringify(jobInvocationDataScheduled)
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
renderJobInvocationDetailPage(
|
|
176
|
+
createJobInvocationDetailState({
|
|
177
|
+
jobInvocation: scheduledJobInvocation,
|
|
178
|
+
}),
|
|
179
|
+
{ jobId: jobInvocationDataScheduled.id }
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
expect(screen.getByText('Scheduled at:')).toBeInTheDocument();
|
|
183
|
+
expect(screen.getByText('Jan 1, 3000, 11:34 UTC')).toBeInTheDocument();
|
|
184
|
+
expect(screen.getByText('Not yet')).toBeInTheDocument();
|
|
185
|
+
});
|
|
186
|
+
|
|
117
187
|
it('renders main information', async () => {
|
|
118
188
|
const jobId = jobInvocationData.id;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<Provider store={store}>
|
|
123
|
-
<JobInvocationDetailPage
|
|
124
|
-
match={{ params: { id: `${jobId}` } }}
|
|
125
|
-
{...props}
|
|
126
|
-
/>
|
|
127
|
-
</Provider>
|
|
189
|
+
|
|
190
|
+
const { container } = renderJobInvocationDetailPage(
|
|
191
|
+
createJobInvocationDetailState()
|
|
128
192
|
);
|
|
129
193
|
|
|
130
194
|
expect(screen.getByText('Description')).toBeInTheDocument();
|
|
@@ -212,115 +276,107 @@ describe('JobInvocationDetailPage', () => {
|
|
|
212
276
|
});
|
|
213
277
|
|
|
214
278
|
it('keeps toolbar buttons mounted while job invocation data is refreshing', async () => {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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>
|
|
279
|
+
renderJobInvocationDetailPage(
|
|
280
|
+
createJobInvocationDetailState({
|
|
281
|
+
jobInvocationStatus: STATUS.PENDING,
|
|
282
|
+
})
|
|
231
283
|
);
|
|
232
284
|
|
|
233
285
|
expect(screen.getByText('Create report')).toBeInTheDocument();
|
|
234
286
|
expect(screen.getByText('Rerun all')).toBeInTheDocument();
|
|
235
287
|
});
|
|
236
288
|
|
|
237
|
-
it('
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
<Provider store={store}>
|
|
241
|
-
<JobInvocationDetailPage
|
|
242
|
-
match={{ params: { id: `${jobInvocationDataScheduled.id}` } }}
|
|
243
|
-
{...props}
|
|
244
|
-
/>
|
|
245
|
-
</Provider>
|
|
289
|
+
it('disables Create report when the job task state is running', async () => {
|
|
290
|
+
usePermissions.mockImplementation(requiredPermissions =>
|
|
291
|
+
requiredPermissions.includes('generate_report_templates')
|
|
246
292
|
);
|
|
247
293
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
294
|
+
renderJobInvocationDetailPage(
|
|
295
|
+
createJobInvocationDetailState({
|
|
296
|
+
jobInvocation: {
|
|
297
|
+
...jobInvocationData,
|
|
298
|
+
task: { ...jobInvocationData.task, state: 'running' },
|
|
299
|
+
},
|
|
300
|
+
})
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const createReportButton = screen.getByRole('link', {
|
|
304
|
+
name: 'Create report',
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
expect(createReportButton).toHaveAttribute('aria-disabled', 'true');
|
|
308
|
+
expect(createReportButton).toHaveClass('pf-m-disabled');
|
|
251
309
|
});
|
|
252
310
|
|
|
253
311
|
it('should dispatch global actions', async () => {
|
|
254
|
-
// recurring in the future
|
|
255
312
|
const jobId = jobInvocationDataRecurring.id;
|
|
256
|
-
const taskId = jobInvocationDataRecurring.task.id;
|
|
257
313
|
const recurrenceId = jobInvocationDataRecurring.recurrence.id;
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
/>
|
|
265
|
-
</Provider>
|
|
314
|
+
|
|
315
|
+
const { store } = renderJobInvocationDetailPage(
|
|
316
|
+
createJobInvocationDetailState({
|
|
317
|
+
jobInvocation: jobInvocationDataRecurring,
|
|
318
|
+
}),
|
|
319
|
+
{ jobId }
|
|
266
320
|
);
|
|
267
321
|
|
|
268
|
-
|
|
269
|
-
{
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
url: `/foreman_tasks/api/tasks/${taskId}`,
|
|
277
|
-
},
|
|
278
|
-
{
|
|
322
|
+
expect(api.get).toHaveBeenCalledWith(
|
|
323
|
+
expect.objectContaining({
|
|
324
|
+
key: GET_REPORT_TEMPLATES,
|
|
325
|
+
url: '/api/report_templates',
|
|
326
|
+
})
|
|
327
|
+
);
|
|
328
|
+
expect(api.get).toHaveBeenCalledWith(
|
|
329
|
+
expect.objectContaining({
|
|
279
330
|
key: GET_REPORT_TEMPLATE_INPUTS,
|
|
280
331
|
url: `/api/templates/${reportTemplateJobId}/template_inputs`,
|
|
281
|
-
}
|
|
282
|
-
|
|
332
|
+
})
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
api.get.mockClear();
|
|
336
|
+
APIActions.post.mockClear();
|
|
337
|
+
APIActions.put.mockClear();
|
|
338
|
+
|
|
339
|
+
store.dispatch(cancelJob(jobId, false));
|
|
340
|
+
store.dispatch(cancelJob(jobId, true));
|
|
341
|
+
store.dispatch(enableRecurringLogic(recurrenceId, true));
|
|
342
|
+
store.dispatch(enableRecurringLogic(recurrenceId, false));
|
|
343
|
+
store.dispatch(cancelRecurringLogic(recurrenceId));
|
|
344
|
+
|
|
345
|
+
expect(APIActions.post).toHaveBeenNthCalledWith(
|
|
346
|
+
1,
|
|
347
|
+
expect.objectContaining({
|
|
283
348
|
key: CANCEL_JOB,
|
|
284
349
|
url: `/job_invocations/${jobId}/cancel`,
|
|
285
|
-
}
|
|
286
|
-
|
|
350
|
+
})
|
|
351
|
+
);
|
|
352
|
+
expect(APIActions.post).toHaveBeenNthCalledWith(
|
|
353
|
+
2,
|
|
354
|
+
expect.objectContaining({
|
|
287
355
|
key: CANCEL_JOB,
|
|
288
356
|
url: `/job_invocations/${jobId}/cancel?force=true`,
|
|
289
|
-
}
|
|
290
|
-
|
|
357
|
+
})
|
|
358
|
+
);
|
|
359
|
+
expect(APIActions.put).toHaveBeenNthCalledWith(
|
|
360
|
+
1,
|
|
361
|
+
expect.objectContaining({
|
|
291
362
|
key: CHANGE_ENABLED_RECURRING_LOGIC,
|
|
292
363
|
url: `/foreman_tasks/api/recurring_logics/${recurrenceId}`,
|
|
293
|
-
}
|
|
294
|
-
|
|
364
|
+
})
|
|
365
|
+
);
|
|
366
|
+
expect(APIActions.put).toHaveBeenNthCalledWith(
|
|
367
|
+
2,
|
|
368
|
+
expect.objectContaining({
|
|
295
369
|
key: CHANGE_ENABLED_RECURRING_LOGIC,
|
|
296
370
|
url: `/foreman_tasks/api/recurring_logics/${recurrenceId}`,
|
|
297
|
-
}
|
|
298
|
-
|
|
371
|
+
})
|
|
372
|
+
);
|
|
373
|
+
expect(APIActions.post).toHaveBeenNthCalledWith(
|
|
374
|
+
3,
|
|
375
|
+
expect.objectContaining({
|
|
299
376
|
key: CANCEL_RECURRING_LOGIC,
|
|
300
377
|
url: `/foreman_tasks/recurring_logics/${recurrenceId}/cancel`,
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
store.dispatch(cancelJob(jobId, false));
|
|
305
|
-
store.dispatch(cancelJob(jobId, true));
|
|
306
|
-
store.dispatch(enableRecurringLogic(recurrenceId, true, jobId));
|
|
307
|
-
store.dispatch(enableRecurringLogic(recurrenceId, false, jobId));
|
|
308
|
-
store.dispatch(cancelRecurringLogic(recurrenceId, jobId));
|
|
309
|
-
|
|
310
|
-
const actualActions = store.getActions();
|
|
311
|
-
expect(actualActions).toHaveLength(expectedActions.length);
|
|
312
|
-
|
|
313
|
-
expectedActions.forEach((expectedAction, index) => {
|
|
314
|
-
if (actualActions[index].type === 'WITH_INTERVAL') {
|
|
315
|
-
expect(actualActions[index].key.key).toEqual(expectedAction.key);
|
|
316
|
-
expect(actualActions[index].key.url).toEqual(expectedAction.url);
|
|
317
|
-
} else {
|
|
318
|
-
expect(actualActions[index].key).toEqual(expectedAction.key);
|
|
319
|
-
if (expectedAction.url) {
|
|
320
|
-
expect(actualActions[index].url).toEqual(expectedAction.url);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
});
|
|
378
|
+
})
|
|
379
|
+
);
|
|
324
380
|
});
|
|
325
381
|
|
|
326
382
|
it('renders empty state when the job invocation fails to load', () => {
|
|
@@ -328,28 +384,23 @@ describe('JobInvocationDetailPage', () => {
|
|
|
328
384
|
const errorMessage = 'Record not found';
|
|
329
385
|
const history = createMemoryHistory();
|
|
330
386
|
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
387
|
|
|
342
|
-
|
|
388
|
+
renderWithStore(
|
|
343
389
|
<Router history={history}>
|
|
344
390
|
<ForemanContextWrapper>
|
|
345
|
-
<
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
/>
|
|
350
|
-
</Provider>
|
|
391
|
+
<JobInvocationDetailPage
|
|
392
|
+
match={{ params: { id: jobId } }}
|
|
393
|
+
history={history}
|
|
394
|
+
/>
|
|
351
395
|
</ForemanContextWrapper>
|
|
352
|
-
</Router
|
|
396
|
+
</Router>,
|
|
397
|
+
createJobInvocationDetailState({
|
|
398
|
+
jobInvocationError: {
|
|
399
|
+
message: errorMessage,
|
|
400
|
+
response: { status: 404 },
|
|
401
|
+
},
|
|
402
|
+
}),
|
|
403
|
+
undefined
|
|
353
404
|
);
|
|
354
405
|
|
|
355
406
|
expect(
|