foreman_remote_execution 16.0.4 → 16.0.5
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/job_invocations_controller.rb +28 -1
- data/app/controllers/template_invocations_controller.rb +1 -1
- data/config/routes.rb +1 -0
- data/lib/foreman_remote_execution/engine.rb +9 -256
- data/lib/foreman_remote_execution/plugin.rb +246 -0
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/functional/api/v2/job_invocations_controller_test.rb +1 -1
- data/test/unit/job_invocation_report_template_test.rb +6 -6
- data/webpack/JobInvocationDetail/CheckboxesActions.js +196 -0
- data/webpack/JobInvocationDetail/{JobInvocationHostTableToolbar.js → DropdownFilter.js} +3 -6
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +6 -6
- data/webpack/JobInvocationDetail/JobInvocationDetail.scss +18 -0
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +205 -89
- data/webpack/JobInvocationDetail/JobInvocationSelectors.js +30 -3
- data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +20 -27
- data/webpack/JobInvocationDetail/OpenAllInvocationsModal.js +118 -0
- data/webpack/JobInvocationDetail/TemplateInvocation.js +54 -24
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +8 -10
- data/webpack/JobInvocationDetail/TemplateInvocationPage.js +1 -1
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +1 -1
- data/webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js +202 -0
- data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +34 -28
- data/webpack/JobInvocationDetail/index.js +61 -30
- data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +26 -7
- data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +1 -1
- metadata +8 -6
- data/webpack/JobInvocationDetail/OpenAlInvocations.js +0 -111
- data/webpack/JobInvocationDetail/__tests__/OpenAlInvocations.test.js +0 -110
@@ -0,0 +1,118 @@
|
|
1
|
+
import {
|
2
|
+
Alert,
|
3
|
+
AlertActionCloseButton,
|
4
|
+
Button,
|
5
|
+
Modal,
|
6
|
+
} from '@patternfly/react-core';
|
7
|
+
import { sprintf, translate as __ } from 'foremanReact/common/I18n';
|
8
|
+
import PropTypes from 'prop-types';
|
9
|
+
import React from 'react';
|
10
|
+
import {
|
11
|
+
templateInvocationPageUrl,
|
12
|
+
MAX_HOSTS_API_SIZE,
|
13
|
+
} from './JobInvocationConstants';
|
14
|
+
|
15
|
+
export const PopupAlert = ({ setShowAlert }) => (
|
16
|
+
<Alert
|
17
|
+
ouiaId="template-invocation-new-tab-popup-alert"
|
18
|
+
variant="warning"
|
19
|
+
actionClose={<AlertActionCloseButton onClose={() => setShowAlert(false)} />}
|
20
|
+
title={__(
|
21
|
+
'Popups are blocked by your browser. Please allow popups for this site to open all invocations in new tabs.'
|
22
|
+
)}
|
23
|
+
/>
|
24
|
+
);
|
25
|
+
|
26
|
+
const OpenAllInvocationsModal = ({
|
27
|
+
failedCount,
|
28
|
+
failedHosts,
|
29
|
+
isOpen,
|
30
|
+
isOpenFailed,
|
31
|
+
jobID,
|
32
|
+
onClose,
|
33
|
+
setShowAlert,
|
34
|
+
selectedIds,
|
35
|
+
}) => {
|
36
|
+
const modalText = isOpenFailed ? 'failed' : 'selected';
|
37
|
+
|
38
|
+
const openLink = url => {
|
39
|
+
const newWin = window.open(url);
|
40
|
+
if (!newWin || newWin.closed || typeof newWin.closed === 'undefined') {
|
41
|
+
setShowAlert(true);
|
42
|
+
}
|
43
|
+
};
|
44
|
+
|
45
|
+
return (
|
46
|
+
<Modal
|
47
|
+
className="open-all-modal"
|
48
|
+
isOpen={isOpen}
|
49
|
+
onClose={onClose}
|
50
|
+
ouiaId="template-invocation-new-tab-modal"
|
51
|
+
title={sprintf(__('Open all %s invocations in new tabs'), modalText)}
|
52
|
+
titleIconVariant="warning"
|
53
|
+
width={590}
|
54
|
+
actions={[
|
55
|
+
<Button
|
56
|
+
ouiaId="template-invocation-new-tab-modal-confirm"
|
57
|
+
key="confirm"
|
58
|
+
variant="primary"
|
59
|
+
onClick={() => {
|
60
|
+
const hostsToOpen = isOpenFailed
|
61
|
+
? failedHosts
|
62
|
+
: selectedIds.map(id => ({ id }));
|
63
|
+
|
64
|
+
hostsToOpen
|
65
|
+
.slice(0, MAX_HOSTS_API_SIZE)
|
66
|
+
.forEach(({ id }) =>
|
67
|
+
openLink(templateInvocationPageUrl(id, jobID), '_blank')
|
68
|
+
);
|
69
|
+
|
70
|
+
onClose();
|
71
|
+
}}
|
72
|
+
>
|
73
|
+
{__('Open in new tabs')}
|
74
|
+
</Button>,
|
75
|
+
<Button
|
76
|
+
ouiaId="template-invocation-new-tab-modal-cancel"
|
77
|
+
key="cancel"
|
78
|
+
variant="link"
|
79
|
+
onClick={onClose}
|
80
|
+
>
|
81
|
+
{__('Cancel')}
|
82
|
+
</Button>,
|
83
|
+
]}
|
84
|
+
>
|
85
|
+
{sprintf(
|
86
|
+
__('Are you sure you want to open all %s invocations in new tabs?'),
|
87
|
+
modalText
|
88
|
+
)}
|
89
|
+
<br />
|
90
|
+
{__('This will open a new tab for each invocation. The maximum is 100.')}
|
91
|
+
<br />
|
92
|
+
{sprintf(__('The number of %s invocations is:'), modalText)}{' '}
|
93
|
+
<b>{isOpenFailed ? failedCount : selectedIds.length}</b>
|
94
|
+
</Modal>
|
95
|
+
);
|
96
|
+
};
|
97
|
+
|
98
|
+
OpenAllInvocationsModal.propTypes = {
|
99
|
+
failedCount: PropTypes.number.isRequired,
|
100
|
+
failedHosts: PropTypes.array,
|
101
|
+
isOpen: PropTypes.bool.isRequired,
|
102
|
+
isOpenFailed: PropTypes.bool,
|
103
|
+
jobID: PropTypes.string.isRequired,
|
104
|
+
onClose: PropTypes.func.isRequired,
|
105
|
+
setShowAlert: PropTypes.func.isRequired,
|
106
|
+
selectedIds: PropTypes.array.isRequired,
|
107
|
+
};
|
108
|
+
|
109
|
+
OpenAllInvocationsModal.defaultProps = {
|
110
|
+
failedHosts: [],
|
111
|
+
isOpenFailed: false,
|
112
|
+
};
|
113
|
+
|
114
|
+
PopupAlert.propTypes = {
|
115
|
+
setShowAlert: PropTypes.func.isRequired,
|
116
|
+
};
|
117
|
+
|
118
|
+
export default OpenAllInvocationsModal;
|
@@ -1,8 +1,9 @@
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
2
2
|
import { isEmpty } from 'lodash';
|
3
3
|
import PropTypes from 'prop-types';
|
4
4
|
import { ClipboardCopyButton, Alert, Skeleton } from '@patternfly/react-core';
|
5
|
-
import {
|
5
|
+
import { useDispatch, useSelector } from 'react-redux';
|
6
|
+
import { APIActions } from 'foremanReact/redux/API';
|
6
7
|
import { translate as __ } from 'foremanReact/common/I18n';
|
7
8
|
import { useForemanHostDetailsPageUrl } from 'foremanReact/Root/Context/ForemanContext';
|
8
9
|
import { STATUS } from 'foremanReact/constants';
|
@@ -11,6 +12,10 @@ import {
|
|
11
12
|
templateInvocationPageUrl,
|
12
13
|
GET_TEMPLATE_INVOCATION,
|
13
14
|
} from './JobInvocationConstants';
|
15
|
+
import {
|
16
|
+
selectTemplateInvocationStatus,
|
17
|
+
selectTemplateInvocation,
|
18
|
+
} from './JobInvocationSelectors';
|
14
19
|
import { OutputToggleGroup } from './TemplateInvocationComponents/OutputToggleGroup';
|
15
20
|
import { PreviewTemplate } from './TemplateInvocationComponents/PreviewTemplate';
|
16
21
|
import { OutputCodeBlock } from './TemplateInvocationComponents/OutputCodeBlock';
|
@@ -47,41 +52,64 @@ const CopyToClipboard = ({ fullOutput }) => {
|
|
47
52
|
</ClipboardCopyButton>
|
48
53
|
);
|
49
54
|
};
|
50
|
-
|
55
|
+
|
51
56
|
export const TemplateInvocation = ({
|
52
57
|
hostID,
|
53
58
|
jobID,
|
54
59
|
isInTableView,
|
55
60
|
hostName,
|
56
61
|
hostProxy,
|
62
|
+
isExpanded,
|
57
63
|
}) => {
|
64
|
+
const intervalRef = useRef(null);
|
58
65
|
const templateURL = showTemplateInvocationUrl(hostID, jobID);
|
59
66
|
const hostDetailsPageUrl = useForemanHostDetailsPageUrl();
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
});
|
67
|
-
const { finished, auto_refresh: autoRefresh } = response;
|
67
|
+
|
68
|
+
const status = useSelector(selectTemplateInvocationStatus(hostID));
|
69
|
+
const response = useSelector(selectTemplateInvocation(hostID));
|
70
|
+
const finished = response.finished ?? true;
|
71
|
+
const autoRefresh = response.auto_refresh || false;
|
72
|
+
const dispatch = useDispatch();
|
68
73
|
|
69
74
|
useEffect(() => {
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
75
|
+
const getData = async () => {
|
76
|
+
if (
|
77
|
+
(!isInTableView || (isInTableView && isExpanded)) &&
|
78
|
+
(Object.keys(response).length === 0 || autoRefresh)
|
79
|
+
) {
|
80
|
+
dispatch(
|
81
|
+
APIActions.get({
|
82
|
+
url: templateURL,
|
83
|
+
key: `${GET_TEMPLATE_INVOCATION}_${hostID}`,
|
84
|
+
handleError: () => {
|
85
|
+
if (intervalRef.current) clearInterval(intervalRef.current);
|
86
|
+
},
|
87
|
+
})
|
88
|
+
);
|
89
|
+
}
|
90
|
+
};
|
91
|
+
getData();
|
92
|
+
if (!finished && autoRefresh) {
|
93
|
+
intervalRef.current = setInterval(() => {
|
94
|
+
getData();
|
76
95
|
}, 5000);
|
77
96
|
}
|
78
|
-
|
79
|
-
clearInterval(intervalId);
|
80
|
-
}
|
97
|
+
|
81
98
|
return () => {
|
82
|
-
|
99
|
+
if (intervalRef.current) {
|
100
|
+
clearInterval(intervalRef.current);
|
101
|
+
intervalRef.current = null;
|
102
|
+
}
|
83
103
|
};
|
84
|
-
}, [
|
104
|
+
}, [
|
105
|
+
dispatch,
|
106
|
+
templateURL,
|
107
|
+
isExpanded,
|
108
|
+
isInTableView,
|
109
|
+
finished,
|
110
|
+
autoRefresh,
|
111
|
+
hostID,
|
112
|
+
]);
|
85
113
|
|
86
114
|
const errorMessage =
|
87
115
|
response?.response?.data?.error?.message ||
|
@@ -91,10 +119,10 @@ export const TemplateInvocation = ({
|
|
91
119
|
preview,
|
92
120
|
output,
|
93
121
|
input_values: inputValues,
|
94
|
-
|
95
|
-
task_cancellable: taskCancellable,
|
122
|
+
task,
|
96
123
|
permissions,
|
97
124
|
} = response;
|
125
|
+
const { id: taskID, cancellable: taskCancellable } = task || {};
|
98
126
|
const [showOutputType, setShowOutputType] = useState({
|
99
127
|
stderr: true,
|
100
128
|
stdout: true,
|
@@ -197,12 +225,14 @@ TemplateInvocation.propTypes = {
|
|
197
225
|
hostProxy: PropTypes.object, // only used when isInTableView is false
|
198
226
|
jobID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
199
227
|
isInTableView: PropTypes.bool,
|
228
|
+
isExpanded: PropTypes.bool,
|
200
229
|
};
|
201
230
|
|
202
231
|
TemplateInvocation.defaultProps = {
|
203
232
|
isInTableView: true,
|
204
233
|
hostName: '',
|
205
234
|
hostProxy: {},
|
235
|
+
isExpanded: false,
|
206
236
|
};
|
207
237
|
|
208
238
|
CopyToClipboard.propTypes = {
|
@@ -6,7 +6,7 @@ import { ActionsColumn } from '@patternfly/react-table';
|
|
6
6
|
import { APIActions } from 'foremanReact/redux/API';
|
7
7
|
import { addToast } from 'foremanReact/components/ToastsList';
|
8
8
|
import { translate as __ } from 'foremanReact/common/I18n';
|
9
|
-
import {
|
9
|
+
import { selectTemplateInvocationList } from '../JobInvocationSelectors';
|
10
10
|
import './index.scss';
|
11
11
|
|
12
12
|
const actions = ({
|
@@ -47,7 +47,8 @@ const actions = ({
|
|
47
47
|
APIActions.post({
|
48
48
|
url: `/foreman_tasks/tasks/${taskID}/cancel`,
|
49
49
|
key: 'CANCEL_TASK',
|
50
|
-
errorToast: ({ response }) =>
|
50
|
+
errorToast: ({ response }) =>
|
51
|
+
response?.data?.message || __('Could not cancel the task'),
|
51
52
|
successToast: () => __('Task for the host cancelled succesfully'),
|
52
53
|
})
|
53
54
|
);
|
@@ -70,7 +71,8 @@ const actions = ({
|
|
70
71
|
APIActions.post({
|
71
72
|
url: `/foreman_tasks/tasks/${taskID}/abort`,
|
72
73
|
key: 'ABORT_TASK',
|
73
|
-
errorToast: ({ response }) =>
|
74
|
+
errorToast: ({ response }) =>
|
75
|
+
response?.data?.message || __('Could not abort the task'),
|
74
76
|
successToast: () => __('task aborted succesfully'),
|
75
77
|
})
|
76
78
|
);
|
@@ -81,14 +83,10 @@ const actions = ({
|
|
81
83
|
|
82
84
|
export const RowActions = ({ hostID, jobID }) => {
|
83
85
|
const dispatch = useDispatch();
|
84
|
-
const response = useSelector(
|
86
|
+
const response = useSelector(selectTemplateInvocationList)?.[hostID];
|
85
87
|
if (!response?.permissions) return null;
|
86
|
-
const {
|
87
|
-
|
88
|
-
task_cancellable: taskCancellable,
|
89
|
-
permissions,
|
90
|
-
} = response;
|
91
|
-
|
88
|
+
const { task, permissions } = response;
|
89
|
+
const { id: taskID, cancellable: taskCancellable } = task || {};
|
92
90
|
const getActions = actions({
|
93
91
|
taskID,
|
94
92
|
jobID,
|
@@ -16,7 +16,7 @@ const TemplateInvocationPage = ({
|
|
16
16
|
job_invocation_description: jobDescription,
|
17
17
|
host_name: hostName,
|
18
18
|
proxy: hostProxy,
|
19
|
-
} = useSelector(selectTemplateInvocation);
|
19
|
+
} = useSelector(selectTemplateInvocation(hostID));
|
20
20
|
const description = sprintf(__('Template Invocation for %s'), hostName);
|
21
21
|
const breadcrumbOptions = {
|
22
22
|
breadcrumbItems: [
|
@@ -97,7 +97,7 @@ describe('JobInvocationDetailPage', () => {
|
|
97
97
|
|
98
98
|
expect(screen.getByText('Description')).toBeInTheDocument();
|
99
99
|
expect(
|
100
|
-
container.querySelector('.chart-donut .pf-c-chart')
|
100
|
+
container.querySelector('.chart-donut .pf-v5-c-chart')
|
101
101
|
).toBeInTheDocument();
|
102
102
|
expect(screen.getByText('2/6')).toBeInTheDocument();
|
103
103
|
expect(screen.getByText('Systems')).toBeInTheDocument();
|
@@ -0,0 +1,202 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Provider } from 'react-redux';
|
3
|
+
import configureStore from 'redux-mock-store';
|
4
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
5
|
+
import '@testing-library/jest-dom/extend-expect';
|
6
|
+
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
|
7
|
+
import { CheckboxesActions } from '../CheckboxesActions';
|
8
|
+
import * as selectors from '../JobInvocationSelectors';
|
9
|
+
import { PopupAlert } from '../OpenAllInvocationsModal';
|
10
|
+
|
11
|
+
jest.mock('foremanReact/common/hooks/API/APIHooks');
|
12
|
+
|
13
|
+
jest.mock('../JobInvocationSelectors');
|
14
|
+
jest.mock('../JobInvocationConstants', () => ({
|
15
|
+
...jest.requireActual('../JobInvocationConstants'),
|
16
|
+
templateInvocationPageUrl: jest.fn(
|
17
|
+
(hostId, jobId) => `url/${hostId}/${jobId}`
|
18
|
+
),
|
19
|
+
DIRECT_OPEN_HOST_LIMIT: 3,
|
20
|
+
}));
|
21
|
+
selectors.selectItems.mockImplementation(() => ({
|
22
|
+
targeting: { search_query: 'name~*' },
|
23
|
+
}));
|
24
|
+
selectors.selectHasPermission.mockImplementation(() => () => () => true);
|
25
|
+
const mockStore = configureStore([]);
|
26
|
+
const store = mockStore({
|
27
|
+
templateInvocation: {
|
28
|
+
permissions: {
|
29
|
+
execute_jobs: true,
|
30
|
+
},
|
31
|
+
},
|
32
|
+
});
|
33
|
+
|
34
|
+
describe('TableToolbarActions', () => {
|
35
|
+
const jobID = '42';
|
36
|
+
let openSpy;
|
37
|
+
|
38
|
+
beforeEach(() => {
|
39
|
+
openSpy = jest.spyOn(window, 'open').mockImplementation(jest.fn());
|
40
|
+
useAPI.mockClear();
|
41
|
+
useAPI.mockReturnValue({
|
42
|
+
response: null,
|
43
|
+
status: 'initial',
|
44
|
+
});
|
45
|
+
});
|
46
|
+
|
47
|
+
afterEach(() => {
|
48
|
+
openSpy.mockRestore();
|
49
|
+
jest.clearAllMocks();
|
50
|
+
});
|
51
|
+
|
52
|
+
describe('Opening selected in new tabs', () => {
|
53
|
+
test('opens links when results length is less than or equal to 3', () => {
|
54
|
+
const selectedIds = [1, 2, 3];
|
55
|
+
render(
|
56
|
+
<Provider store={store}>
|
57
|
+
<CheckboxesActions
|
58
|
+
selectedIds={selectedIds}
|
59
|
+
failedCount={0}
|
60
|
+
jobID={jobID}
|
61
|
+
/>
|
62
|
+
</Provider>
|
63
|
+
);
|
64
|
+
const openAllButton = screen.getByLabelText(
|
65
|
+
/open all template invocations in new tab/i
|
66
|
+
);
|
67
|
+
fireEvent.click(openAllButton);
|
68
|
+
expect(openSpy).toHaveBeenCalledTimes(selectedIds.length);
|
69
|
+
});
|
70
|
+
|
71
|
+
test('shows modal when results length is greater than 3', () => {
|
72
|
+
const selectedIds = [1, 2, 3, 4];
|
73
|
+
render(
|
74
|
+
<Provider store={store}>
|
75
|
+
<CheckboxesActions
|
76
|
+
selectedIds={selectedIds}
|
77
|
+
failedCount={0}
|
78
|
+
jobID={jobID}
|
79
|
+
/>
|
80
|
+
</Provider>
|
81
|
+
);
|
82
|
+
fireEvent.click(
|
83
|
+
screen.getByLabelText(/open all template invocations in new tab/i)
|
84
|
+
);
|
85
|
+
expect(
|
86
|
+
screen.getByRole('heading', {
|
87
|
+
name: /open all %s invocations in new tabs \+ selected/i,
|
88
|
+
})
|
89
|
+
).toBeInTheDocument();
|
90
|
+
});
|
91
|
+
|
92
|
+
test('shows alert when popups are blocked', () => {
|
93
|
+
openSpy.mockReturnValue(null);
|
94
|
+
const selectedIds = [1, 2];
|
95
|
+
render(
|
96
|
+
<Provider store={store}>
|
97
|
+
<CheckboxesActions
|
98
|
+
selectedIds={selectedIds}
|
99
|
+
failedCount={0}
|
100
|
+
jobID={jobID}
|
101
|
+
/>
|
102
|
+
</Provider>
|
103
|
+
);
|
104
|
+
fireEvent.click(
|
105
|
+
screen.getByLabelText(/open all template invocations in new tab/i)
|
106
|
+
);
|
107
|
+
expect(
|
108
|
+
screen.getByText(/Popups are blocked by your browser/)
|
109
|
+
).toBeInTheDocument();
|
110
|
+
});
|
111
|
+
});
|
112
|
+
|
113
|
+
describe('Opening failed in new tabs', () => {
|
114
|
+
test('opens links when results length is less than or equal to 3', async () => {
|
115
|
+
const failedHosts = [{ id: 301 }, { id: 302 }];
|
116
|
+
useAPI.mockReturnValue({
|
117
|
+
response: { results: failedHosts },
|
118
|
+
status: 'success',
|
119
|
+
});
|
120
|
+
render(
|
121
|
+
<Provider store={store}>
|
122
|
+
<CheckboxesActions selectedIds={[]} failedCount={2} jobID={jobID} />
|
123
|
+
</Provider>
|
124
|
+
);
|
125
|
+
fireEvent.click(screen.getByLabelText(/actions dropdown toggle/i));
|
126
|
+
fireEvent.click(screen.getByText(/open all failed runs/i));
|
127
|
+
await waitFor(() => {
|
128
|
+
expect(openSpy).toHaveBeenCalledTimes(failedHosts.length);
|
129
|
+
});
|
130
|
+
});
|
131
|
+
|
132
|
+
test('shows modal when results length is greater than 3', () => {
|
133
|
+
render(
|
134
|
+
<Provider store={store}>
|
135
|
+
<CheckboxesActions selectedIds={[]} failedCount={4} jobID={jobID} />
|
136
|
+
</Provider>
|
137
|
+
);
|
138
|
+
fireEvent.click(screen.getByLabelText(/actions dropdown toggle/i));
|
139
|
+
fireEvent.click(screen.getByText(/open all failed runs/i));
|
140
|
+
expect(
|
141
|
+
screen.getByRole('heading', {
|
142
|
+
name: /open all %s invocations in new tabs \+ failed/i,
|
143
|
+
})
|
144
|
+
).toBeInTheDocument();
|
145
|
+
});
|
146
|
+
|
147
|
+
test('calls useApi with skip: true when failedCount is 0', () => {
|
148
|
+
render(
|
149
|
+
<Provider store={store}>
|
150
|
+
<CheckboxesActions selectedIds={[]} failedCount={0} jobID={jobID} />
|
151
|
+
</Provider>
|
152
|
+
);
|
153
|
+
expect(useAPI).toHaveBeenCalledWith(
|
154
|
+
'get',
|
155
|
+
`foreman/api/job_invocations/${jobID}/hosts`,
|
156
|
+
expect.objectContaining({
|
157
|
+
skip: true,
|
158
|
+
})
|
159
|
+
);
|
160
|
+
});
|
161
|
+
});
|
162
|
+
|
163
|
+
describe('PopupAlert', () => {
|
164
|
+
test('renders without crashing', () => {
|
165
|
+
const mockSetShowAlert = jest.fn();
|
166
|
+
render(<PopupAlert setShowAlert={mockSetShowAlert} />);
|
167
|
+
expect(
|
168
|
+
screen.getByText(/Popups are blocked by your browser/)
|
169
|
+
).toBeInTheDocument();
|
170
|
+
});
|
171
|
+
|
172
|
+
test('closes alert when close button is clicked', () => {
|
173
|
+
const mockSetShowAlert = jest.fn();
|
174
|
+
render(<PopupAlert setShowAlert={mockSetShowAlert} />);
|
175
|
+
const closeButton = screen.getByRole('button', { name: /close/i });
|
176
|
+
fireEvent.click(closeButton);
|
177
|
+
expect(mockSetShowAlert).toHaveBeenCalledWith(false);
|
178
|
+
});
|
179
|
+
});
|
180
|
+
|
181
|
+
describe('Rerun button', () => {
|
182
|
+
test('is enabled when permissions and ids are valid', () => {
|
183
|
+
const selectedIds = [101, 102, 103];
|
184
|
+
render(
|
185
|
+
<Provider store={store}>
|
186
|
+
<CheckboxesActions
|
187
|
+
bulkParams="(id ^ (101, 102, 103))"
|
188
|
+
selectedIds={selectedIds}
|
189
|
+
failedCount={1}
|
190
|
+
jobID={jobID}
|
191
|
+
/>
|
192
|
+
</Provider>
|
193
|
+
);
|
194
|
+
const rerunLink = screen.getByRole('link', { name: /rerun/i });
|
195
|
+
expect(rerunLink).toBeEnabled();
|
196
|
+
const expectedSearchParams = new URLSearchParams();
|
197
|
+
selectedIds.forEach(id => expectedSearchParams.append('host_ids[]', id));
|
198
|
+
const expectedHref = `foreman/job_invocations/42/rerun?search=(name~*) AND ((id ^ (101, 102, 103)))`;
|
199
|
+
expect(rerunLink).toHaveAttribute('href', expectedHref);
|
200
|
+
});
|
201
|
+
});
|
202
|
+
});
|
@@ -3,18 +3,19 @@ import configureMockStore from 'redux-mock-store';
|
|
3
3
|
import { Provider } from 'react-redux';
|
4
4
|
import { render, screen, act, fireEvent } from '@testing-library/react';
|
5
5
|
import '@testing-library/jest-dom/extend-expect';
|
6
|
-
import * as APIHooks from 'foremanReact/common/hooks/API/APIHooks';
|
7
6
|
import * as api from 'foremanReact/redux/API';
|
7
|
+
import * as selectors from '../JobInvocationSelectors';
|
8
8
|
import { TemplateInvocation } from '../TemplateInvocation';
|
9
9
|
import { mockTemplateInvocationResponse } from './fixtures';
|
10
10
|
|
11
11
|
jest.spyOn(api, 'get');
|
12
|
-
jest.mock('
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
12
|
+
jest.mock('../JobInvocationSelectors');
|
13
|
+
selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
|
14
|
+
'RESOLVED'
|
15
|
+
);
|
16
|
+
selectors.selectTemplateInvocation.mockImplementation(() => () =>
|
17
|
+
mockTemplateInvocationResponse
|
18
|
+
);
|
18
19
|
const mockStore = configureMockStore([]);
|
19
20
|
const store = mockStore({
|
20
21
|
HOSTS_API: {
|
@@ -95,19 +96,22 @@ describe('TemplateInvocation', () => {
|
|
95
96
|
).toHaveLength(1);
|
96
97
|
});
|
97
98
|
test('displays an error alert when there is an error', async () => {
|
98
|
-
|
99
|
-
|
100
|
-
|
99
|
+
selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
|
100
|
+
'ERROR'
|
101
|
+
);
|
102
|
+
selectors.selectTemplateInvocation.mockImplementation(() => () => ({
|
103
|
+
response: { data: { error: 'Error message' } },
|
101
104
|
}));
|
102
|
-
|
103
105
|
render(
|
104
|
-
<
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
106
|
+
<Provider store={store}>
|
107
|
+
<TemplateInvocation
|
108
|
+
hostID="1"
|
109
|
+
jobID="1"
|
110
|
+
isInTableView={false}
|
111
|
+
hostName="example-host"
|
112
|
+
hostProxy={{ name: 'example-proxy', href: '#' }}
|
113
|
+
/>
|
114
|
+
</Provider>
|
111
115
|
);
|
112
116
|
|
113
117
|
expect(
|
@@ -119,17 +123,19 @@ describe('TemplateInvocation', () => {
|
|
119
123
|
});
|
120
124
|
|
121
125
|
test('displays a skeleton while loading', async () => {
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
}));
|
126
|
+
selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
|
127
|
+
'PENDING'
|
128
|
+
);
|
129
|
+
selectors.selectTemplateInvocation.mockImplementation(() => () => ({}));
|
126
130
|
render(
|
127
|
-
<
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
131
|
+
<Provider store={store}>
|
132
|
+
<TemplateInvocation
|
133
|
+
hostID="1"
|
134
|
+
jobID="1"
|
135
|
+
isInTableView={false}
|
136
|
+
hostName="example-host"
|
137
|
+
/>
|
138
|
+
</Provider>
|
133
139
|
);
|
134
140
|
|
135
141
|
expect(document.querySelectorAll('.pf-v5-c-skeleton')).toHaveLength(1);
|