foreman_remote_execution 13.1.1 → 13.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +1 -0
  3. data/app/models/job_invocation.rb +23 -0
  4. data/app/models/job_invocation_composer.rb +2 -0
  5. data/app/views/api/v2/job_invocations/base.json.rabl +3 -2
  6. data/app/views/templates/script/package_action.erb +18 -10
  7. data/lib/foreman_remote_execution/version.rb +1 -1
  8. data/package.json +2 -1
  9. data/webpack/JobInvocationDetail/JobInvocationActions.js +134 -3
  10. data/webpack/JobInvocationDetail/JobInvocationConstants.js +14 -0
  11. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +5 -2
  12. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +5 -0
  13. data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +13 -9
  14. data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +268 -0
  15. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +259 -0
  16. data/webpack/JobInvocationDetail/__tests__/fixtures.js +117 -0
  17. data/webpack/JobInvocationDetail/index.js +58 -38
  18. data/webpack/JobWizard/JobWizard.scss +14 -5
  19. data/webpack/JobWizard/JobWizardPageRerun.js +15 -10
  20. data/webpack/JobWizard/steps/HostsAndInputs/index.js +4 -1
  21. data/webpack/__mocks__/foremanReact/components/BreadcrumbBar/index.js +4 -0
  22. data/webpack/__mocks__/foremanReact/components/Head/index.js +10 -0
  23. data/webpack/__mocks__/foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState.js +8 -0
  24. data/webpack/__mocks__/foremanReact/components/ToastsList/index.js +3 -0
  25. data/webpack/__mocks__/foremanReact/redux/API/APIActions.js +21 -0
  26. data/webpack/__mocks__/foremanReact/redux/API/APIConstants.js +7 -0
  27. data/webpack/__mocks__/foremanReact/redux/API/index.js +14 -0
  28. data/webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware/index.js +9 -0
  29. metadata +12 -2
@@ -0,0 +1,259 @@
1
+ import React from 'react';
2
+ import configureMockStore from 'redux-mock-store';
3
+ import { fireEvent, render, screen, act } from '@testing-library/react';
4
+ import '@testing-library/jest-dom/extend-expect';
5
+ import { Provider } from 'react-redux';
6
+ import thunk from 'redux-thunk';
7
+ import { foremanUrl } from 'foremanReact/common/helpers';
8
+ import * as api from 'foremanReact/redux/API';
9
+ import JobInvocationDetailPage from '../index';
10
+ import {
11
+ jobInvocationData,
12
+ jobInvocationDataScheduled,
13
+ jobInvocationDataRecurring,
14
+ mockPermissionsData,
15
+ mockReportTemplatesResponse,
16
+ mockReportTemplateInputsResponse,
17
+ } from './fixtures';
18
+ import {
19
+ cancelJob,
20
+ enableRecurringLogic,
21
+ cancelRecurringLogic,
22
+ } from '../JobInvocationActions';
23
+ import {
24
+ CANCEL_JOB,
25
+ CANCEL_RECURRING_LOGIC,
26
+ CHANGE_ENABLED_RECURRING_LOGIC,
27
+ GET_REPORT_TEMPLATES,
28
+ GET_REPORT_TEMPLATE_INPUTS,
29
+ JOB_INVOCATION_KEY,
30
+ } from '../JobInvocationConstants';
31
+
32
+ jest.spyOn(api, 'get');
33
+
34
+ jest.mock('foremanReact/common/hooks/API/APIHooks', () => ({
35
+ useAPI: jest.fn(() => ({
36
+ response: mockPermissionsData,
37
+ })),
38
+ }));
39
+
40
+ jest.mock('foremanReact/routes/common/PageLayout/PageLayout', () =>
41
+ jest.fn(props => (
42
+ <div>
43
+ {props.header && <h1>{props.header}</h1>}
44
+ {props.toolbarButtons && <div>{props.toolbarButtons}</div>}
45
+ {props.children}
46
+ </div>
47
+ ))
48
+ );
49
+
50
+ const initialState = {
51
+ JOB_INVOCATION_KEY: {
52
+ response: jobInvocationData,
53
+ },
54
+ GET_REPORT_TEMPLATES: mockReportTemplatesResponse,
55
+ };
56
+
57
+ const initialStateScheduled = {
58
+ JOB_INVOCATION_KEY: {
59
+ response: jobInvocationDataScheduled,
60
+ },
61
+ };
62
+
63
+ api.get.mockImplementation(({ handleSuccess, ...action }) => {
64
+ if (action.key === 'GET_REPORT_TEMPLATES') {
65
+ handleSuccess &&
66
+ handleSuccess({
67
+ data: mockReportTemplatesResponse,
68
+ });
69
+ } else if (action.key === 'GET_REPORT_TEMPLATE_INPUTS') {
70
+ handleSuccess &&
71
+ handleSuccess({
72
+ data: mockReportTemplateInputsResponse,
73
+ });
74
+ }
75
+
76
+ return { type: 'get', ...action };
77
+ });
78
+
79
+ const reportTemplateJobId = mockReportTemplatesResponse.results[0].id;
80
+
81
+ const mockStore = configureMockStore([thunk]);
82
+
83
+ describe('JobInvocationDetailPage', () => {
84
+ it('renders main information', async () => {
85
+ const jobId = jobInvocationData.id;
86
+ const store = mockStore(initialState);
87
+
88
+ const { container } = render(
89
+ <Provider store={store}>
90
+ <JobInvocationDetailPage match={{ params: { id: `${jobId}` } }} />
91
+ </Provider>
92
+ );
93
+
94
+ expect(screen.getByText('Description')).toBeInTheDocument();
95
+ expect(
96
+ container.querySelector('.chart-donut .pf-c-chart')
97
+ ).toBeInTheDocument();
98
+ expect(screen.getByText('2/6')).toBeInTheDocument();
99
+ expect(screen.getByText('Systems')).toBeInTheDocument();
100
+ expect(screen.getByText('System status')).toBeInTheDocument();
101
+ expect(screen.getByText('Succeeded: 2')).toBeInTheDocument();
102
+ expect(screen.getByText('Failed: 4')).toBeInTheDocument();
103
+ expect(screen.getByText('In Progress: 0')).toBeInTheDocument();
104
+ expect(screen.getByText('Canceled: 0')).toBeInTheDocument();
105
+
106
+ const informationToCheck = {
107
+ 'Effective user:': jobInvocationData.effective_user,
108
+ 'Started at:': 'Jan 1, 2024, 11:34 UTC',
109
+ 'SSH user:': 'Not available',
110
+ 'Template:': jobInvocationData.template_name,
111
+ };
112
+
113
+ Object.entries(informationToCheck).forEach(([term, expectedText]) => {
114
+ const termContainers = container.querySelectorAll(
115
+ '.pf-c-description-list__term .pf-c-description-list__text'
116
+ );
117
+ termContainers.forEach(termContainer => {
118
+ if (termContainer.textContent.includes(term)) {
119
+ let descriptionContainer;
120
+ if (term === 'SSH user:') {
121
+ descriptionContainer = termContainer
122
+ .closest('.pf-c-description-list__group')
123
+ .querySelector(
124
+ '.pf-c-description-list__description .pf-c-description-list__text .disabled-text'
125
+ );
126
+ } else {
127
+ descriptionContainer = termContainer
128
+ .closest('.pf-c-description-list__group')
129
+ .querySelector(
130
+ '.pf-c-description-list__description .pf-c-description-list__text'
131
+ );
132
+ }
133
+ expect(descriptionContainer.textContent).toContain(expectedText);
134
+ }
135
+ });
136
+ });
137
+
138
+ // checks the global actions and if they link to the correct url
139
+ expect(screen.getByText('Create report').getAttribute('href')).toEqual(
140
+ foremanUrl(
141
+ `/templates/report_templates/${mockReportTemplatesResponse.results[0].id}/generate?report_template_report%5Binput_values%5D%5B${mockReportTemplateInputsResponse.results[0].id}%5D%5Bvalue%5D=${jobId}`
142
+ )
143
+ );
144
+ expect(screen.getByText('Rerun all').getAttribute('href')).toEqual(
145
+ foremanUrl(`/job_invocations/${jobId}/rerun`)
146
+ );
147
+ act(() => {
148
+ fireEvent.click(screen.getByRole('button', { name: 'Select' }));
149
+ });
150
+ expect(
151
+ screen
152
+ .getByText('Rerun successful')
153
+ .closest('a')
154
+ .getAttribute('href')
155
+ ).toEqual(foremanUrl(`/job_invocations/${jobId}/rerun?succeeded_only=1`));
156
+ expect(
157
+ screen
158
+ .getByText('Rerun failed')
159
+ .closest('a')
160
+ .getAttribute('href')
161
+ ).toEqual(foremanUrl(`/job_invocations/${jobId}/rerun?failed_only=1`));
162
+ expect(
163
+ screen
164
+ .getByText('View task')
165
+ .closest('a')
166
+ .getAttribute('href')
167
+ ).toEqual(foremanUrl(`/foreman_tasks/tasks/${jobInvocationData.task.id}`));
168
+ expect(screen.getByText('Cancel')).toBeInTheDocument();
169
+ expect(screen.getByText('Abort')).toBeInTheDocument();
170
+ expect(screen.queryByText('Enable recurring')).not.toBeInTheDocument();
171
+ expect(screen.queryByText('Cancel recurring')).not.toBeInTheDocument();
172
+ expect(
173
+ screen
174
+ .getByText('Legacy UI')
175
+ .closest('a')
176
+ .getAttribute('href')
177
+ ).toEqual(`/job_invocations/${jobId}`);
178
+ });
179
+
180
+ it('shows scheduled date', async () => {
181
+ const store = mockStore(initialStateScheduled);
182
+ render(
183
+ <Provider store={store}>
184
+ <JobInvocationDetailPage
185
+ match={{ params: { id: `${jobInvocationDataScheduled.id}` } }}
186
+ />
187
+ </Provider>
188
+ );
189
+
190
+ expect(screen.getByText('Scheduled at:')).toBeInTheDocument();
191
+ expect(screen.getByText('Jan 1, 3000, 11:34 UTC')).toBeInTheDocument();
192
+ expect(screen.getByText('Not yet')).toBeInTheDocument();
193
+ });
194
+
195
+ it('should dispatch global actions', async () => {
196
+ // recurring in the future
197
+ const jobId = jobInvocationDataRecurring.id;
198
+ const recurrenceId = jobInvocationDataRecurring.recurrence.id;
199
+ const store = mockStore(jobInvocationDataRecurring);
200
+ render(
201
+ <Provider store={store}>
202
+ <JobInvocationDetailPage match={{ params: { id: `${jobId}` } }} />
203
+ </Provider>
204
+ );
205
+
206
+ const expectedActions = [
207
+ { key: GET_REPORT_TEMPLATES, url: '/api/report_templates' },
208
+ {
209
+ key: JOB_INVOCATION_KEY,
210
+ url: `/api/job_invocations/${jobId}`,
211
+ },
212
+ {
213
+ key: GET_REPORT_TEMPLATE_INPUTS,
214
+ url: `/api/templates/${reportTemplateJobId}/template_inputs`,
215
+ },
216
+ {
217
+ key: CANCEL_JOB,
218
+ url: `/job_invocations/${jobId}/cancel`,
219
+ },
220
+ {
221
+ key: CANCEL_JOB,
222
+ url: `/job_invocations/${jobId}/cancel?force=true`,
223
+ },
224
+ {
225
+ key: CHANGE_ENABLED_RECURRING_LOGIC,
226
+ url: `/foreman_tasks/api/recurring_logics/${recurrenceId}`,
227
+ },
228
+ {
229
+ key: CHANGE_ENABLED_RECURRING_LOGIC,
230
+ url: `/foreman_tasks/api/recurring_logics/${recurrenceId}`,
231
+ },
232
+ {
233
+ key: CANCEL_RECURRING_LOGIC,
234
+ url: `/foreman_tasks/recurring_logics/${recurrenceId}/cancel`,
235
+ },
236
+ ];
237
+
238
+ store.dispatch(cancelJob(jobId, false));
239
+ store.dispatch(cancelJob(jobId, true));
240
+ store.dispatch(enableRecurringLogic(recurrenceId, true, jobId));
241
+ store.dispatch(enableRecurringLogic(recurrenceId, false, jobId));
242
+ store.dispatch(cancelRecurringLogic(recurrenceId, jobId));
243
+
244
+ const actualActions = store.getActions();
245
+ expect(actualActions).toHaveLength(expectedActions.length);
246
+
247
+ expectedActions.forEach((expectedAction, index) => {
248
+ if (actualActions[index].type === 'WITH_INTERVAL') {
249
+ expect(actualActions[index].key.key).toEqual(expectedAction.key);
250
+ expect(actualActions[index].key.url).toEqual(expectedAction.url);
251
+ } else {
252
+ expect(actualActions[index].key).toEqual(expectedAction.key);
253
+ if (expectedAction.url) {
254
+ expect(actualActions[index].url).toEqual(expectedAction.url);
255
+ }
256
+ }
257
+ });
258
+ });
259
+ });
@@ -0,0 +1,117 @@
1
+ export const jobInvocationData = {
2
+ id: 123,
3
+ description: 'Description',
4
+ job_category: 'Commands',
5
+ targeting_id: 123,
6
+ status: 1,
7
+ start_at: '2024-01-01 12:34:56 +0100',
8
+ status_label: 'failed',
9
+ ssh_user: null,
10
+ time_to_pickup: null,
11
+ template_id: 321,
12
+ template_name: 'Run Command - Script Default',
13
+ effective_user: 'root',
14
+ succeeded: 2,
15
+ failed: 4,
16
+ pending: 0,
17
+ cancelled: 0,
18
+ total: 6,
19
+ missing: 5,
20
+ total_hosts: 6,
21
+ task: {
22
+ id: '37ad5ead-51de-4798-bc73-a17687c4d5aa',
23
+ state: 'stopped',
24
+ },
25
+ template_invocations: [
26
+ {
27
+ template_id: 321,
28
+ template_name: 'Run Command - Script Default',
29
+ host_id: 1,
30
+ template_invocation_input_values: [
31
+ {
32
+ template_input_name: 'command',
33
+ template_input_id: 59,
34
+ value:
35
+ 'echo start; for i in $(seq 1 120); do echo $i; sleep 1; done; echo done',
36
+ },
37
+ ],
38
+ },
39
+ ],
40
+ };
41
+
42
+ export const jobInvocationDataScheduled = {
43
+ id: 456,
44
+ description: 'Description',
45
+ job_category: 'Commands',
46
+ targeting_id: 456,
47
+ status: 1,
48
+ start_at: '3000-01-01 12:34:56 +0100',
49
+ status_label: 'failed',
50
+ ssh_user: null,
51
+ time_to_pickup: null,
52
+ template_id: 321,
53
+ template_name: 'Run Command - Script Default',
54
+ effective_user: 'root',
55
+ succeeded: 2,
56
+ failed: 4,
57
+ pending: 0,
58
+ cancelled: 0,
59
+ total: 6,
60
+ missing: 5,
61
+ total_hosts: 6,
62
+ };
63
+
64
+ export const jobInvocationDataRecurring = {
65
+ id: 789,
66
+ description: 'Description',
67
+ job_category: 'Commands',
68
+ targeting_id: 456,
69
+ status: 2,
70
+ start_at: '3000-01-01 12:00:00 +0100',
71
+ status_label: 'queued',
72
+ ssh_user: null,
73
+ time_to_pickup: null,
74
+ template_id: 321,
75
+ template_name: 'Run Command - Script Default',
76
+ effective_user: 'root',
77
+ succeeded: 0,
78
+ failed: 0,
79
+ pending: 0,
80
+ cancelled: 0,
81
+ total: 'N/A',
82
+ missing: 0,
83
+ total_hosts: 1,
84
+ task: {
85
+ id: '37ad5ead-51de-4798-bc73-a17687c4d5aa',
86
+ state: 'scheduled',
87
+ },
88
+ mode: 'recurring',
89
+ recurrence: {
90
+ id: 1,
91
+ cron_line: '00 12 * * *',
92
+ end_time: null,
93
+ iteration: 1,
94
+ task_group_id: 12,
95
+ state: 'active',
96
+ max_iteration: null,
97
+ purpose: null,
98
+ task_count: 1,
99
+ action: 'Run hosts job:',
100
+ last_occurence: null,
101
+ next_occurence: '3000-01-01 12:00:00 +0100',
102
+ },
103
+ };
104
+
105
+ export const mockPermissionsData = {
106
+ edit_job_templates: true,
107
+ view_foreman_tasks: true,
108
+ edit_recurring_logics: true,
109
+ };
110
+
111
+ export const mockReportTemplatesResponse = {
112
+ results: [{ id: '12', name: 'Job - Invocation Report' }],
113
+ };
114
+
115
+ export const mockReportTemplateInputsResponse = {
116
+ results: [{ id: '34', name: 'job_id' }],
117
+ };
@@ -1,20 +1,24 @@
1
- import React, { useEffect } from 'react';
2
- import { useSelector, useDispatch } from 'react-redux';
3
1
  import PropTypes from 'prop-types';
4
- import { Divider, PageSection, Flex } from '@patternfly/react-core';
2
+ import React, { useEffect } from 'react';
3
+ import { useDispatch, useSelector } from 'react-redux';
4
+ import { Divider, Flex } from '@patternfly/react-core';
5
5
  import { translate as __, documentLocale } from 'foremanReact/common/I18n';
6
6
  import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
7
+ import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
7
8
  import { stopInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
8
- import { getData } from './JobInvocationActions';
9
- import { selectItems } from './JobInvocationSelectors';
10
- import JobInvocationOverview from './JobInvocationOverview';
11
- import JobInvocationSystemStatusChart from './JobInvocationSystemStatusChart';
9
+ import { getData, getTask } from './JobInvocationActions';
12
10
  import {
11
+ CURRENT_PERMISSIONS,
12
+ DATE_OPTIONS,
13
13
  JOB_INVOCATION_KEY,
14
14
  STATUS,
15
- DATE_OPTIONS,
15
+ currentPermissionsUrl,
16
16
  } from './JobInvocationConstants';
17
17
  import './JobInvocationDetail.scss';
18
+ import JobInvocationOverview from './JobInvocationOverview';
19
+ import { selectItems } from './JobInvocationSelectors';
20
+ import JobInvocationSystemStatusChart from './JobInvocationSystemStatusChart';
21
+ import JobInvocationToolbarButtons from './JobInvocationToolbarButtons';
18
22
 
19
23
  const JobInvocationDetailPage = ({
20
24
  match: {
@@ -30,8 +34,15 @@ const JobInvocationDetailPage = ({
30
34
  start_at: startAt,
31
35
  } = items;
32
36
  const finished =
33
- statusLabel === STATUS.FAILED || statusLabel === STATUS.SUCCEEDED;
37
+ statusLabel === STATUS.FAILED ||
38
+ statusLabel === STATUS.SUCCEEDED ||
39
+ statusLabel === STATUS.CANCELLED;
34
40
  const autoRefresh = task?.state === STATUS.PENDING || false;
41
+ const { response, status } = useAPI(
42
+ 'get',
43
+ currentPermissionsUrl,
44
+ CURRENT_PERMISSIONS
45
+ );
35
46
 
36
47
  let isAlreadyStarted = false;
37
48
  let formattedStartDate;
@@ -56,6 +67,12 @@ const JobInvocationDetailPage = ({
56
67
  // eslint-disable-next-line react-hooks/exhaustive-deps
57
68
  }, [dispatch, id, finished, autoRefresh]);
58
69
 
70
+ useEffect(() => {
71
+ if (task?.id !== undefined) {
72
+ dispatch(getTask(`${task?.id}`));
73
+ }
74
+ }, [dispatch, task]);
75
+
59
76
  const breadcrumbOptions = {
60
77
  breadcrumbItems: [
61
78
  { caption: __('Jobs'), url: `/job_invocations` },
@@ -68,38 +85,41 @@ const JobInvocationDetailPage = ({
68
85
  <PageLayout
69
86
  header={description}
70
87
  breadcrumbOptions={breadcrumbOptions}
88
+ toolbarButtons={
89
+ <JobInvocationToolbarButtons
90
+ jobId={id}
91
+ data={items}
92
+ currentPermissions={response.results}
93
+ permissionsStatus={status}
94
+ />
95
+ }
71
96
  searchable={false}
72
97
  >
73
- <React.Fragment>
74
- <PageSection
75
- className="job-invocation-detail-page-section"
76
- isFilled
77
- variant="light"
98
+ <Flex
99
+ className="job-invocation-detail-flex"
100
+ alignItems={{ default: 'alignItemsFlexStart' }}
101
+ >
102
+ <JobInvocationSystemStatusChart
103
+ data={items}
104
+ isAlreadyStarted={isAlreadyStarted}
105
+ formattedStartDate={formattedStartDate}
106
+ />
107
+ <Divider
108
+ orientation={{
109
+ default: 'vertical',
110
+ }}
111
+ />
112
+ <Flex
113
+ className="job-overview"
114
+ alignItems={{ default: 'alignItemsCenter' }}
78
115
  >
79
- <Flex alignItems={{ default: 'alignItemsFlexStart' }}>
80
- <JobInvocationSystemStatusChart
81
- data={items}
82
- isAlreadyStarted={isAlreadyStarted}
83
- formattedStartDate={formattedStartDate}
84
- />
85
- <Divider
86
- orientation={{
87
- default: 'vertical',
88
- }}
89
- />
90
- <Flex
91
- className="job-overview"
92
- alignItems={{ default: 'alignItemsCenter' }}
93
- >
94
- <JobInvocationOverview
95
- data={items}
96
- isAlreadyStarted={isAlreadyStarted}
97
- formattedStartDate={formattedStartDate}
98
- />
99
- </Flex>
100
- </Flex>
101
- </PageSection>
102
- </React.Fragment>
116
+ <JobInvocationOverview
117
+ data={items}
118
+ isAlreadyStarted={isAlreadyStarted}
119
+ formattedStartDate={formattedStartDate}
120
+ />
121
+ </Flex>
122
+ </Flex>
103
123
  </PageLayout>
104
124
  );
105
125
  };
@@ -16,7 +16,17 @@
16
16
  ); // So the select box can be shown above the wizard footer and navigation toggle
17
17
  }
18
18
  .pf-c-wizard__main-body {
19
- max-width: 500px;
19
+ @media (max-width: 600px) {
20
+ max-width: 100%;
21
+ }
22
+
23
+ @media (min-width: 601px) and (max-width: 1300px) {
24
+ max-width: 65vw;
25
+ }
26
+ @media (min-width: 1301px) {
27
+ max-width: 50vw;
28
+ }
29
+
20
30
  .advanced-fields-title {
21
31
  margin-bottom: 10px;
22
32
  }
@@ -40,6 +50,9 @@
40
50
  flex-wrap: nowrap;
41
51
  }
42
52
  }
53
+ .foreman-search-field {
54
+ width: 100%;
55
+ }
43
56
  }
44
57
  input[type='radio'],
45
58
  input[type='checkbox'] {
@@ -110,10 +123,6 @@
110
123
  min-height: 40px;
111
124
  min-width: 100px;
112
125
  }
113
- #host-selection {
114
- width: 500px;
115
- }
116
-
117
126
  .pf-c-modal-box {
118
127
  width: auto;
119
128
  }
@@ -1,15 +1,15 @@
1
- import React from 'react';
2
1
  import PropTypes from 'prop-types';
2
+ import React from 'react';
3
3
  import URI from 'urijs';
4
- import { Alert, Divider, Skeleton, Button } from '@patternfly/react-core';
5
- import { sprintf, translate as __ } from 'foremanReact/common/I18n';
6
- import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
7
- import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
8
- import { STATUS } from 'foremanReact/constants';
4
+ import { Alert, Button, Divider, Skeleton } from '@patternfly/react-core';
9
5
  import {
10
- useForemanOrganization,
11
6
  useForemanLocation,
7
+ useForemanOrganization,
12
8
  } from 'foremanReact/Root/Context/ForemanContext';
9
+ import { translate as __, sprintf } from 'foremanReact/common/I18n';
10
+ import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
11
+ import { STATUS } from 'foremanReact/constants';
12
+ import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
13
13
  import { JobWizard } from './JobWizard';
14
14
  import { JOB_API_KEY } from './JobWizardConstants';
15
15
 
@@ -21,11 +21,16 @@ const JobWizardPageRerun = ({
21
21
  }) => {
22
22
  const uri = new URI(search);
23
23
  const { failed_only: failedOnly } = uri.search(true);
24
+ const { succeeded_only: succeededOnly } = uri.search(true);
25
+ let queryParams = '';
26
+ if (failedOnly) {
27
+ queryParams = '&failed_only=1';
28
+ } else if (succeededOnly) {
29
+ queryParams = '&succeeded_only=1';
30
+ }
24
31
  const { response, status } = useAPI(
25
32
  'get',
26
- `/ui_job_wizard/job_invocation?id=${id}${
27
- failedOnly ? '&failed_only=1' : ''
28
- }`,
33
+ `/ui_job_wizard/job_invocation?id=${id}${queryParams}`,
29
34
  JOB_API_KEY
30
35
  );
31
36
  const title = __('Run job');
@@ -49,7 +49,10 @@ const HostsAndInputs = ({
49
49
  hostsSearchQuery,
50
50
  setHostsSearchQuery,
51
51
  }) => {
52
- const [hostMethod, setHostMethod] = useState(hostMethods.hosts);
52
+ const defaultHostMethod = hostsSearchQuery.length
53
+ ? hostMethods.searchQuery
54
+ : hostMethods.hosts;
55
+ const [hostMethod, setHostMethod] = useState(defaultHostMethod);
53
56
  const isLoading = useSelector(selectIsLoadingHosts);
54
57
  const templateInputs = useSelector(selectTemplateInputs);
55
58
  const [hostPreviewOpen, setHostPreviewOpen] = useState(false);
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+
3
+ const BreadcrumbBar = () => <div />;
4
+ export default BreadcrumbBar;
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ const Head = ({ children }) => <div>{children}</div>;
5
+
6
+ Head.propTypes = {
7
+ children: PropTypes.node.isRequired,
8
+ };
9
+
10
+ export default Head;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { translate as __ } from '../../../common/I18n';
3
+
4
+ const DefaultLoaderEmptyState = () => (
5
+ <span className="disabled-text">{__('Not available')}</span>
6
+ );
7
+
8
+ export default DefaultLoaderEmptyState;
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+
3
+ export const ToastsList = () => <div />;
@@ -0,0 +1,21 @@
1
+ import { API_OPERATIONS } from 'foremanReact/redux/API/APIConstants';
2
+
3
+ const { GET, POST, PUT, DELETE, PATCH } = API_OPERATIONS;
4
+
5
+ const apiAction = (type, payload) => ({ type, payload });
6
+
7
+ export const get = payload => apiAction(GET, payload);
8
+
9
+ export const post = payload => apiAction(POST, payload);
10
+
11
+ export const put = payload => apiAction(PUT, payload);
12
+
13
+ export const patch = payload => apiAction(PATCH, payload);
14
+
15
+ export const APIActions = {
16
+ get,
17
+ post,
18
+ put,
19
+ patch,
20
+ delete: payload => apiAction(DELETE, payload),
21
+ };
@@ -0,0 +1,7 @@
1
+ export const API_OPERATIONS = {
2
+ GET: 'API_GET',
3
+ POST: 'API_POST',
4
+ PUT: 'API_PUT',
5
+ DELETE: 'API_DELETE',
6
+ PATCH: 'API_PATCH',
7
+ };
@@ -1,5 +1,19 @@
1
1
  export const API = {
2
2
  get: jest.fn(),
3
+ put: jest.fn(),
4
+ post: jest.fn(),
5
+ delete: jest.fn(),
6
+ patch: jest.fn(),
3
7
  };
4
8
 
5
9
  export const get = data => ({ type: 'get-some-type', ...data });
10
+ export const post = data => ({ type: 'post-some-type', ...data });
11
+ export const put = data => ({ type: 'put-some-type', ...data });
12
+ export const patch = data => ({ type: 'patch-some-type', ...data });
13
+
14
+ export const APIActions = {
15
+ get,
16
+ post,
17
+ put,
18
+ patch,
19
+ };