foreman_remote_execution 13.1.1 → 13.2.1
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 +1 -0
- data/app/models/job_invocation.rb +23 -0
- data/app/models/job_invocation_composer.rb +2 -0
- data/app/views/api/v2/job_invocations/base.json.rabl +3 -2
- data/app/views/templates/script/package_action.erb +18 -10
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/package.json +2 -1
- data/webpack/JobInvocationDetail/JobInvocationActions.js +134 -3
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +14 -0
- data/webpack/JobInvocationDetail/JobInvocationDetail.scss +5 -2
- data/webpack/JobInvocationDetail/JobInvocationSelectors.js +5 -0
- data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +13 -9
- data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +268 -0
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +259 -0
- data/webpack/JobInvocationDetail/__tests__/fixtures.js +117 -0
- data/webpack/JobInvocationDetail/index.js +58 -38
- data/webpack/JobWizard/JobWizard.scss +14 -5
- data/webpack/JobWizard/JobWizardPageRerun.js +15 -10
- data/webpack/JobWizard/steps/HostsAndInputs/index.js +4 -1
- data/webpack/__mocks__/foremanReact/components/BreadcrumbBar/index.js +4 -0
- data/webpack/__mocks__/foremanReact/components/Head/index.js +10 -0
- data/webpack/__mocks__/foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState.js +8 -0
- data/webpack/__mocks__/foremanReact/components/ToastsList/index.js +3 -0
- data/webpack/__mocks__/foremanReact/redux/API/APIActions.js +21 -0
- data/webpack/__mocks__/foremanReact/redux/API/APIConstants.js +7 -0
- data/webpack/__mocks__/foremanReact/redux/API/index.js +14 -0
- data/webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware/index.js +9 -0
- 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
|
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
|
-
|
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 ||
|
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
|
-
<
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
<
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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:
|
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
|
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
|
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,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
|
+
};
|
@@ -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
|
+
};
|