foreman_remote_execution 17.2.0 → 18.0.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/foreign_input_sets_controller.rb +4 -0
- data/app/controllers/job_invocations_controller.rb +16 -1
- data/config/routes.rb +2 -1
- data/db/seeds.d/60-ssh_proxy_feature.rb +2 -2
- data/db/seeds.d/90-bookmarks.rb +1 -1
- data/db/seeds.d/95-mail_notifications.rb +1 -1
- data/lib/foreman_remote_execution/plugin.rb +2 -2
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/functional/job_invocations_controller_test.rb +84 -0
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +10 -2
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +8 -1
- data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +6 -78
- data/webpack/JobInvocationDetail/__tests__/JobInvocationHostTablePolling.test.js +113 -7
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +4 -39
- data/webpack/JobInvocationDetail/__tests__/areAllHostsTerminal.test.js +40 -0
- data/webpack/JobInvocationDetail/__tests__/fixtures.js +0 -8
- data/webpack/JobWizard/Footer.js +16 -13
- data/webpack/JobWizard/__tests__/Footer.test.js +64 -0
- data/webpack/JobWizard/__tests__/integration.test.js +98 -53
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +191 -212
- data/webpack/react_app/components/TargetingHosts/__tests__/HostItem.test.js +73 -3
- data/webpack/react_app/components/jobInvocations/AggregateStatus/index.test.js +69 -36
- metadata +5 -5
- data/webpack/JobWizard/__tests__/__snapshots__/integration.test.js.snap +0 -51
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostItem.test.js.snap +0 -31
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { areAllHostsTerminal } from '../JobInvocationConstants';
|
|
2
|
+
|
|
3
|
+
describe('areAllHostsTerminal', () => {
|
|
4
|
+
it('returns false for an empty results array', () => {
|
|
5
|
+
expect(areAllHostsTerminal([])).toBe(false);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('returns false for undefined results', () => {
|
|
9
|
+
expect(areAllHostsTerminal(undefined)).toBe(false);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('returns true when every host is in a terminal state', () => {
|
|
13
|
+
expect(
|
|
14
|
+
areAllHostsTerminal([
|
|
15
|
+
{ id: 1, job_status: 'success' },
|
|
16
|
+
{ id: 2, job_status: 'error' },
|
|
17
|
+
{ id: 3, job_status: 'cancelled' },
|
|
18
|
+
])
|
|
19
|
+
).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns false when at least one host is non-terminal', () => {
|
|
23
|
+
expect(
|
|
24
|
+
areAllHostsTerminal([
|
|
25
|
+
{ id: 1, job_status: 'success' },
|
|
26
|
+
{ id: 2, job_status: 'running' },
|
|
27
|
+
])
|
|
28
|
+
).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns false when every host is non-terminal', () => {
|
|
32
|
+
expect(
|
|
33
|
+
areAllHostsTerminal([
|
|
34
|
+
{ id: 1, job_status: 'running' },
|
|
35
|
+
{ id: 2, job_status: 'planned' },
|
|
36
|
+
{ id: 3, job_status: 'N/A' },
|
|
37
|
+
])
|
|
38
|
+
).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -140,14 +140,6 @@ export const mockPermissionsData = {
|
|
|
140
140
|
edit_recurring_logics: true,
|
|
141
141
|
};
|
|
142
142
|
|
|
143
|
-
export const mockReportTemplatesResponse = {
|
|
144
|
-
results: [{ id: '12', name: 'Job - Invocation Report' }],
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
export const mockReportTemplateInputsResponse = {
|
|
148
|
-
results: [{ id: '34', name: 'job_id' }],
|
|
149
|
-
};
|
|
150
|
-
|
|
151
143
|
const templateInvocationID = 157;
|
|
152
144
|
|
|
153
145
|
export const jobInvocationOutput = [
|
data/webpack/JobWizard/Footer.js
CHANGED
|
@@ -22,6 +22,19 @@ export const Footer = ({ canSubmit, onSave }) => {
|
|
|
22
22
|
activeStep && activeStep.enableNext !== undefined
|
|
23
23
|
? activeStep.enableNext
|
|
24
24
|
: true;
|
|
25
|
+
const isReviewStep = activeStep.name === WIZARD_TITLES.review;
|
|
26
|
+
const canSkipToReview = canSubmit && !isReviewStep;
|
|
27
|
+
const skipToReviewTooltip = () => {
|
|
28
|
+
if (canSkipToReview) {
|
|
29
|
+
return __('Skip to review step');
|
|
30
|
+
}
|
|
31
|
+
if (isReviewStep) {
|
|
32
|
+
return __('Already on the review step');
|
|
33
|
+
}
|
|
34
|
+
return __(
|
|
35
|
+
'Fill all required fields in all the steps to start the job'
|
|
36
|
+
);
|
|
37
|
+
};
|
|
25
38
|
|
|
26
39
|
return (
|
|
27
40
|
<>
|
|
@@ -32,9 +45,7 @@ export const Footer = ({ canSubmit, onSave }) => {
|
|
|
32
45
|
onClick={onNext}
|
|
33
46
|
isDisabled={!isValid || isSubmitting}
|
|
34
47
|
>
|
|
35
|
-
{
|
|
36
|
-
? __('Submit')
|
|
37
|
-
: __('Next')}
|
|
48
|
+
{isReviewStep ? __('Submit') : __('Next')}
|
|
38
49
|
</Button>
|
|
39
50
|
<Button
|
|
40
51
|
ouiaId="back-footer"
|
|
@@ -68,22 +79,14 @@ export const Footer = ({ canSubmit, onSave }) => {
|
|
|
68
79
|
</Button>
|
|
69
80
|
</Tooltip>
|
|
70
81
|
<Tooltip
|
|
71
|
-
content={
|
|
72
|
-
<div>
|
|
73
|
-
{canSubmit
|
|
74
|
-
? __('Skip to review step')
|
|
75
|
-
: __(
|
|
76
|
-
'Fill all required fields in all the steps to start the job'
|
|
77
|
-
)}
|
|
78
|
-
</div>
|
|
79
|
-
}
|
|
82
|
+
content={<div>{skipToReviewTooltip()}</div>}
|
|
80
83
|
triggerRef={tooltipSkipTo}
|
|
81
84
|
>
|
|
82
85
|
<Button
|
|
83
86
|
ouiaId="skip-to-review-footer"
|
|
84
87
|
variant="tertiary"
|
|
85
88
|
onClick={() => goToStepByName(WIZARD_TITLES.review)}
|
|
86
|
-
isAriaDisabled={!
|
|
89
|
+
isAriaDisabled={!canSkipToReview}
|
|
87
90
|
isDisabled={isSubmitting}
|
|
88
91
|
ref={tooltipSkipTo}
|
|
89
92
|
>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Provider } from 'react-redux';
|
|
3
|
+
import { render, fireEvent, screen, act } from '@testing-library/react';
|
|
4
|
+
import '@testing-library/jest-dom';
|
|
5
|
+
import { Wizard } from '@patternfly/react-core/deprecated';
|
|
6
|
+
import configureMockStore from 'redux-mock-store';
|
|
7
|
+
import { Footer } from '../Footer';
|
|
8
|
+
import { WIZARD_TITLES } from '../JobWizardConstants';
|
|
9
|
+
|
|
10
|
+
const store = configureMockStore([])({ API: {} });
|
|
11
|
+
|
|
12
|
+
const renderFooterWizard = ({ canSubmit = true, startAtStep = 1 } = {}) =>
|
|
13
|
+
render(
|
|
14
|
+
<Provider store={store}>
|
|
15
|
+
<Wizard
|
|
16
|
+
steps={[
|
|
17
|
+
{
|
|
18
|
+
name: WIZARD_TITLES.categoryAndTemplate,
|
|
19
|
+
component: <div>category step</div>,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: WIZARD_TITLES.review,
|
|
23
|
+
component: <div>review step</div>,
|
|
24
|
+
},
|
|
25
|
+
]}
|
|
26
|
+
startAtStep={startAtStep}
|
|
27
|
+
footer={<Footer canSubmit={canSubmit} onSave={jest.fn()} />}
|
|
28
|
+
/>
|
|
29
|
+
</Provider>
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
describe('Job wizard footer', () => {
|
|
33
|
+
it('enables Skip to review when required fields are filled', () => {
|
|
34
|
+
renderFooterWizard({ canSubmit: true });
|
|
35
|
+
|
|
36
|
+
expect(screen.getByText('Skip to review')).not.toHaveAttribute(
|
|
37
|
+
'aria-disabled',
|
|
38
|
+
'true'
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('disables Skip to review when required fields are missing', () => {
|
|
43
|
+
renderFooterWizard({ canSubmit: false });
|
|
44
|
+
|
|
45
|
+
expect(screen.getByText('Skip to review')).toHaveAttribute(
|
|
46
|
+
'aria-disabled',
|
|
47
|
+
'true'
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('disables Skip to review on the review step', async () => {
|
|
52
|
+
renderFooterWizard({ canSubmit: true });
|
|
53
|
+
|
|
54
|
+
await act(async () => {
|
|
55
|
+
fireEvent.click(screen.getByText('Skip to review'));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(screen.getByText('Submit')).toBeInTheDocument();
|
|
59
|
+
expect(screen.getByText('Skip to review')).toHaveAttribute(
|
|
60
|
+
'aria-disabled',
|
|
61
|
+
'true'
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Provider } from 'react-redux';
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
3
|
+
import { render, screen, within, waitFor } from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import '@testing-library/jest-dom';
|
|
5
6
|
import { MockedProvider } from '@apollo/client/testing';
|
|
6
7
|
import * as api from 'foremanReact/redux/API';
|
|
7
8
|
import { JobWizard } from '../JobWizard';
|
|
@@ -17,17 +18,55 @@ import {
|
|
|
17
18
|
|
|
18
19
|
const store = testSetup(selectors, api);
|
|
19
20
|
|
|
21
|
+
const renderJobWizard = ({ withGql = false } = {}) => {
|
|
22
|
+
const wizard = (
|
|
23
|
+
<Provider store={store}>
|
|
24
|
+
<JobWizard />
|
|
25
|
+
</Provider>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (withGql) {
|
|
29
|
+
return render(
|
|
30
|
+
<MockedProvider mocks={gqlMock} addTypename={false}>
|
|
31
|
+
{wizard}
|
|
32
|
+
</MockedProvider>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return render(wizard);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const getWizardNavigation = () =>
|
|
40
|
+
screen.getByRole('navigation', { name: 'Run Job steps' });
|
|
41
|
+
|
|
42
|
+
const getWizardStep = stepName =>
|
|
43
|
+
within(getWizardNavigation()).getByRole('button', { name: stepName });
|
|
44
|
+
|
|
45
|
+
const getDisabledWizardSteps = () =>
|
|
46
|
+
within(getWizardNavigation())
|
|
47
|
+
.getAllByRole('button')
|
|
48
|
+
.filter(button => button.getAttribute('aria-disabled') === 'true');
|
|
49
|
+
|
|
50
|
+
const expectDispatchedGet = expected => {
|
|
51
|
+
expect(store.getActions()).toEqual(
|
|
52
|
+
expect.arrayContaining([expect.objectContaining({ type: 'get', ...expected })])
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
20
56
|
describe('Job wizard fill', () => {
|
|
21
57
|
beforeEach(() => {
|
|
58
|
+
store.clearActions();
|
|
22
59
|
jest.spyOn(selectors, 'selectRouterSearch');
|
|
23
60
|
selectors.selectRouterSearch.mockImplementation(() => ({
|
|
24
61
|
'host_ids[]': ['105', '37'],
|
|
25
62
|
}));
|
|
26
63
|
});
|
|
64
|
+
|
|
27
65
|
afterEach(() => {
|
|
28
66
|
selectors.selectRouterSearch.mockRestore();
|
|
29
67
|
});
|
|
30
|
-
|
|
68
|
+
|
|
69
|
+
it('selects a template and enables wizard steps', async () => {
|
|
31
70
|
api.get.mockImplementation(({ handleSuccess, ...action }) => {
|
|
32
71
|
if (action.key === 'JOB_CATEGORIES') {
|
|
33
72
|
handleSuccess &&
|
|
@@ -54,74 +93,80 @@ describe('Job wizard fill', () => {
|
|
|
54
93
|
selectors.selectJobTemplate.mockRestore();
|
|
55
94
|
jest.spyOn(selectors, 'selectJobTemplate');
|
|
56
95
|
selectors.selectJobTemplate.mockImplementation(() => ({}));
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
96
|
+
|
|
97
|
+
renderJobWizard();
|
|
98
|
+
|
|
99
|
+
expect(getDisabledWizardSteps()).toHaveLength(5);
|
|
100
|
+
expect(getWizardStep(WIZARD_TITLES.categoryAndTemplate)).toBeInTheDocument();
|
|
101
|
+
|
|
102
|
+
expectDispatchedGet({
|
|
103
|
+
key: 'JOB_CATEGORIES',
|
|
104
|
+
url: '/ui_job_wizard/categories',
|
|
105
|
+
});
|
|
106
|
+
expectDispatchedGet({
|
|
107
|
+
key: 'HOST_IDS',
|
|
108
|
+
params: { search: 'id = 105 or id = 37' },
|
|
109
|
+
url: '/api/hosts',
|
|
110
|
+
});
|
|
111
|
+
expectDispatchedGet({
|
|
112
|
+
key: 'JOB_TEMPLATES',
|
|
113
|
+
});
|
|
114
|
+
|
|
65
115
|
selectors.selectJobCategoriesStatus.mockImplementation(() => 'RESOLVED');
|
|
66
|
-
expect(store.getActions()).toMatchSnapshot('initial');
|
|
67
116
|
selectors.selectJobTemplate.mockRestore();
|
|
68
117
|
jest.spyOn(selectors, 'selectJobTemplate');
|
|
69
118
|
selectors.selectJobTemplate.mockImplementation(() => jobTemplate);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
119
|
+
|
|
120
|
+
await userEvent.click(
|
|
121
|
+
screen.getByRole('button', { name: 'Job template toggle' })
|
|
122
|
+
);
|
|
123
|
+
await userEvent.click(screen.getByText(jobTemplate.job_template.name));
|
|
124
|
+
|
|
125
|
+
expect(store.getActions().at(-1)).toEqual(
|
|
126
|
+
expect.objectContaining({
|
|
127
|
+
key: 'JOB_TEMPLATE',
|
|
128
|
+
type: 'get',
|
|
129
|
+
url: '/ui_job_wizard/template/178',
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
await waitFor(() => {
|
|
134
|
+
expect(getDisabledWizardSteps()).toHaveLength(0);
|
|
78
135
|
});
|
|
79
|
-
expect(store.getActions().slice(-1)).toMatchSnapshot('select template');
|
|
80
|
-
wrapper.update();
|
|
81
|
-
expect(
|
|
82
|
-
wrapper.find('.pf-v5-c-wizard__nav-link.pf-m-disabled')
|
|
83
|
-
).toHaveLength(0);
|
|
84
136
|
});
|
|
85
137
|
|
|
86
|
-
it('
|
|
138
|
+
it('renders all wizard steps and navigates between them', async () => {
|
|
87
139
|
selectors.selectJobCategoriesStatus.mockImplementation(() => null);
|
|
88
140
|
selectors.selectJobTemplates.mockRestore();
|
|
89
141
|
selectors.selectJobCategories.mockRestore();
|
|
90
142
|
mockApi(api);
|
|
91
143
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<Provider store={store}>
|
|
95
|
-
<JobWizard />
|
|
96
|
-
</Provider>
|
|
97
|
-
</MockedProvider>
|
|
98
|
-
);
|
|
144
|
+
renderJobWizard({ withGql: true });
|
|
145
|
+
|
|
99
146
|
const steps = [
|
|
100
147
|
WIZARD_TITLES.hostsAndInputs,
|
|
101
148
|
WIZARD_TITLES.categoryAndTemplate,
|
|
102
149
|
WIZARD_TITLES.advanced,
|
|
103
150
|
WIZARD_TITLES.review,
|
|
104
151
|
];
|
|
105
|
-
|
|
106
|
-
for
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
});
|
|
113
|
-
const stepTitles = screen.getAllByText(step);
|
|
114
|
-
expect(stepTitles).toHaveLength(3);
|
|
152
|
+
|
|
153
|
+
for (const step of steps) {
|
|
154
|
+
expect(screen.getAllByText(step)).toHaveLength(1);
|
|
155
|
+
|
|
156
|
+
await userEvent.click(getWizardStep(step));
|
|
157
|
+
|
|
158
|
+
expect(screen.getAllByText(step)).toHaveLength(3);
|
|
115
159
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
expect(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
await
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
160
|
+
|
|
161
|
+
expect(screen.getAllByText(WIZARD_TITLES.typeOfExecution)).toHaveLength(1);
|
|
162
|
+
expect(
|
|
163
|
+
screen.queryByText('Select the type of execution')
|
|
164
|
+
).not.toBeInTheDocument();
|
|
165
|
+
|
|
166
|
+
await userEvent.click(getWizardStep(WIZARD_TITLES.typeOfExecution));
|
|
167
|
+
|
|
168
|
+
expect(
|
|
169
|
+
screen.getByText('Select the type of execution')
|
|
170
|
+
).toBeInTheDocument();
|
|
126
171
|
});
|
|
127
172
|
});
|