foreman_remote_execution 4.8.0 → 5.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/job_invocations_controller.rb +9 -0
- data/app/controllers/ui_job_wizard_controller.rb +16 -4
- data/app/graphql/mutations/job_invocations/create.rb +43 -0
- data/app/graphql/types/job_invocation_input.rb +13 -0
- data/app/graphql/types/recurrence_input.rb +8 -0
- data/app/graphql/types/scheduling_input.rb +6 -0
- data/app/graphql/types/targeting_enum.rb +7 -0
- data/app/lib/actions/remote_execution/run_host_job.rb +4 -0
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
- data/app/models/job_invocation_composer.rb +1 -1
- data/app/models/targeting.rb +2 -2
- data/app/views/job_invocations/refresh.js.erb +1 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
- data/lib/foreman_remote_execution/engine.rb +110 -6
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/package.json +6 -6
- data/test/functional/api/v2/job_invocations_controller_test.rb +10 -0
- data/test/functional/cockpit_controller_test.rb +0 -1
- data/test/graphql/mutations/job_invocations/create.rb +58 -0
- data/test/helpers/remote_execution_helper_test.rb +0 -1
- data/test/unit/actions/run_host_job_test.rb +21 -0
- data/test/unit/concerns/host_extensions_test.rb +36 -3
- data/test/unit/job_invocation_composer_test.rb +3 -5
- data/test/unit/job_invocation_report_template_test.rb +1 -1
- data/test/unit/job_template_effective_user_test.rb +0 -4
- data/test/unit/remote_execution_provider_test.rb +0 -4
- data/test/unit/targeting_test.rb +68 -1
- data/webpack/JobWizard/JobWizard.js +94 -13
- data/webpack/JobWizard/JobWizard.scss +59 -35
- data/webpack/JobWizard/JobWizardConstants.js +28 -1
- data/webpack/JobWizard/JobWizardSelectors.js +32 -0
- data/webpack/JobWizard/__tests__/fixtures.js +81 -6
- data/webpack/JobWizard/__tests__/integration.test.js +26 -15
- data/webpack/JobWizard/__tests__/validation.test.js +141 -0
- data/webpack/JobWizard/autofill.js +38 -0
- data/webpack/JobWizard/index.js +7 -0
- data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +7 -4
- data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +216 -12
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +1 -0
- data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
- data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
- data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
- data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
- data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
- data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +82 -7
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +7 -4
- data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
- data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
- data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
- data/webpack/JobWizard/steps/HostsAndInputs/index.js +182 -34
- data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
- data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
- data/webpack/JobWizard/steps/Schedule/QueryType.js +46 -43
- data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
- data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
- data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
- data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
- data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
- data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
- data/webpack/JobWizard/steps/Schedule/StartEndDates.js +59 -19
- data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +258 -11
- data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +11 -2
- data/webpack/JobWizard/steps/Schedule/index.js +97 -21
- data/webpack/JobWizard/steps/form/DateTimePicker.js +41 -8
- data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
- data/webpack/JobWizard/steps/form/Formatter.js +39 -8
- data/webpack/JobWizard/steps/form/NumberInput.js +3 -2
- data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
- data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
- data/webpack/JobWizard/steps/form/SelectField.js +14 -3
- data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
- data/webpack/JobWizard/submit.js +120 -0
- data/webpack/JobWizard/validation.js +53 -0
- data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
- data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
- data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
- data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
- data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
- data/webpack/helpers.js +1 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +1 -1
- metadata +38 -6
- data/app/models/setting/remote_execution.rb +0 -94
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js +0 -37
- data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { Provider } from 'react-redux';
|
|
3
3
|
import { mount } from '@theforeman/test';
|
|
4
4
|
import { render, fireEvent, screen, act } from '@testing-library/react';
|
|
5
|
+
import { MockedProvider } from '@apollo/client/testing';
|
|
5
6
|
import * as api from 'foremanReact/redux/API';
|
|
6
7
|
import { JobWizard } from '../JobWizard';
|
|
7
8
|
import * as selectors from '../JobWizardSelectors';
|
|
@@ -11,23 +12,31 @@ import {
|
|
|
11
12
|
mockApi,
|
|
12
13
|
jobCategories,
|
|
13
14
|
jobTemplateResponse as jobTemplate,
|
|
15
|
+
gqlMock,
|
|
14
16
|
} from './fixtures';
|
|
15
17
|
|
|
16
18
|
const store = testSetup(selectors, api);
|
|
17
19
|
|
|
18
|
-
selectors.selectJobTemplate.mockImplementation(() => {});
|
|
19
|
-
|
|
20
|
-
api.get.mockImplementation(({ handleSuccess, ...action }) => {
|
|
21
|
-
if (action.key === 'JOB_CATEGORIES') {
|
|
22
|
-
handleSuccess && handleSuccess({ data: { job_categories: jobCategories } });
|
|
23
|
-
}
|
|
24
|
-
return { type: 'get', ...action };
|
|
25
|
-
});
|
|
26
20
|
describe('Job wizard fill', () => {
|
|
27
21
|
it('should select template', async () => {
|
|
22
|
+
api.get.mockImplementation(({ handleSuccess, ...action }) => {
|
|
23
|
+
if (action.key === 'JOB_CATEGORIES') {
|
|
24
|
+
handleSuccess &&
|
|
25
|
+
handleSuccess({ data: { job_categories: jobCategories } });
|
|
26
|
+
} else if (action.key === 'JOB_TEMPLATE') {
|
|
27
|
+
handleSuccess &&
|
|
28
|
+
handleSuccess({
|
|
29
|
+
data: jobTemplate,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return { type: 'get', ...action };
|
|
33
|
+
});
|
|
34
|
+
selectors.selectJobTemplate.mockRestore();
|
|
35
|
+
jest.spyOn(selectors, 'selectJobTemplate');
|
|
36
|
+
selectors.selectJobTemplate.mockImplementation(() => ({}));
|
|
28
37
|
const wrapper = mount(
|
|
29
38
|
<Provider store={store}>
|
|
30
|
-
<JobWizard
|
|
39
|
+
<JobWizard />
|
|
31
40
|
</Provider>
|
|
32
41
|
);
|
|
33
42
|
expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
|
|
@@ -35,7 +44,8 @@ describe('Job wizard fill', () => {
|
|
|
35
44
|
);
|
|
36
45
|
selectors.selectJobCategoriesStatus.mockImplementation(() => 'RESOLVED');
|
|
37
46
|
expect(store.getActions()).toMatchSnapshot('initial');
|
|
38
|
-
|
|
47
|
+
selectors.selectJobTemplate.mockRestore();
|
|
48
|
+
jest.spyOn(selectors, 'selectJobTemplate');
|
|
39
49
|
selectors.selectJobTemplate.mockImplementation(() => jobTemplate);
|
|
40
50
|
wrapper.find('.pf-c-button.pf-c-select__toggle-button').simulate('click');
|
|
41
51
|
await act(async () => {
|
|
@@ -43,9 +53,9 @@ describe('Job wizard fill', () => {
|
|
|
43
53
|
.find('.pf-c-select__menu-item')
|
|
44
54
|
.first()
|
|
45
55
|
.simulate('click');
|
|
46
|
-
await wrapper.update();
|
|
47
56
|
});
|
|
48
57
|
expect(store.getActions().slice(-1)).toMatchSnapshot('select template');
|
|
58
|
+
wrapper.update();
|
|
49
59
|
expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
|
|
50
60
|
0
|
|
51
61
|
);
|
|
@@ -53,15 +63,16 @@ describe('Job wizard fill', () => {
|
|
|
53
63
|
|
|
54
64
|
it('have all steps', async () => {
|
|
55
65
|
selectors.selectJobCategoriesStatus.mockImplementation(() => null);
|
|
56
|
-
selectors.selectJobTemplate.mockRestore();
|
|
57
66
|
selectors.selectJobTemplates.mockRestore();
|
|
58
67
|
selectors.selectJobCategories.mockRestore();
|
|
59
68
|
mockApi(api);
|
|
60
69
|
|
|
61
70
|
render(
|
|
62
|
-
<
|
|
63
|
-
<
|
|
64
|
-
|
|
71
|
+
<MockedProvider mocks={gqlMock} addTypename={false}>
|
|
72
|
+
<Provider store={store}>
|
|
73
|
+
<JobWizard />
|
|
74
|
+
</Provider>
|
|
75
|
+
</MockedProvider>
|
|
65
76
|
);
|
|
66
77
|
const titles = Object.values(WIZARD_TITLES);
|
|
67
78
|
const steps = [titles[1], titles[0], ...titles.slice(2)]; // the first title is selected at the beggining
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Provider } from 'react-redux';
|
|
3
|
+
import { render, fireEvent, screen, act } from '@testing-library/react';
|
|
4
|
+
import { MockedProvider } from '@apollo/client/testing';
|
|
5
|
+
import '@testing-library/jest-dom';
|
|
6
|
+
import * as api from 'foremanReact/redux/API';
|
|
7
|
+
import { JobWizard } from '../JobWizard';
|
|
8
|
+
import * as selectors from '../JobWizardSelectors';
|
|
9
|
+
import { testSetup, mockApi, jobTemplateResponse, gqlMock } from './fixtures';
|
|
10
|
+
import { WIZARD_TITLES } from '../JobWizardConstants';
|
|
11
|
+
|
|
12
|
+
const store = testSetup(selectors, api);
|
|
13
|
+
|
|
14
|
+
mockApi(api);
|
|
15
|
+
const templateInputs = [...jobTemplateResponse.template_inputs];
|
|
16
|
+
const advancedTemplateInputs = [
|
|
17
|
+
...jobTemplateResponse.advanced_template_inputs,
|
|
18
|
+
];
|
|
19
|
+
templateInputs[0].default = null;
|
|
20
|
+
advancedTemplateInputs[0].default = null;
|
|
21
|
+
selectors.selectTemplateInputs.mockImplementation(() => templateInputs);
|
|
22
|
+
selectors.selectAdvancedTemplateInputs.mockImplementation(
|
|
23
|
+
() => advancedTemplateInputs
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
describe('Job wizard validation', () => {
|
|
27
|
+
afterAll(() => {
|
|
28
|
+
selectors.selectTemplateInputs.mockRestore();
|
|
29
|
+
selectors.selectAdvancedTemplateInputs.mockRestore();
|
|
30
|
+
});
|
|
31
|
+
it('requeried', async () => {
|
|
32
|
+
render(
|
|
33
|
+
<MockedProvider mocks={gqlMock} addTypename={false}>
|
|
34
|
+
<Provider store={store}>
|
|
35
|
+
<JobWizard />
|
|
36
|
+
</Provider>
|
|
37
|
+
</MockedProvider>
|
|
38
|
+
);
|
|
39
|
+
expect(screen.getByText(WIZARD_TITLES.advanced)).toBeDisabled();
|
|
40
|
+
expect(screen.getByText(WIZARD_TITLES.schedule)).toBeDisabled();
|
|
41
|
+
expect(screen.getByText(WIZARD_TITLES.review)).toBeDisabled();
|
|
42
|
+
await act(async () => {
|
|
43
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.hostsAndInputs));
|
|
44
|
+
});
|
|
45
|
+
const textField = screen.getByLabelText('plain hidden', {
|
|
46
|
+
selector: 'textarea',
|
|
47
|
+
});
|
|
48
|
+
await act(async () => {
|
|
49
|
+
await fireEvent.change(textField, {
|
|
50
|
+
target: { value: 'text' },
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
expect(screen.getByText(WIZARD_TITLES.advanced)).toBeEnabled();
|
|
54
|
+
expect(screen.getByText(WIZARD_TITLES.schedule)).toBeDisabled();
|
|
55
|
+
expect(screen.getByText(WIZARD_TITLES.review)).toBeDisabled();
|
|
56
|
+
|
|
57
|
+
await act(async () => {
|
|
58
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
59
|
+
});
|
|
60
|
+
const advTextField = screen.getByLabelText('adv plain hidden', {
|
|
61
|
+
selector: 'textarea',
|
|
62
|
+
});
|
|
63
|
+
await act(async () => {
|
|
64
|
+
await fireEvent.change(advTextField, {
|
|
65
|
+
target: { value: 'text' },
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(
|
|
70
|
+
screen.getByText(WIZARD_TITLES.advanced, { selector: 'button' })
|
|
71
|
+
).toBeEnabled();
|
|
72
|
+
expect(screen.getByText(WIZARD_TITLES.schedule)).toBeEnabled();
|
|
73
|
+
expect(screen.getByText(WIZARD_TITLES.review)).toBeEnabled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('advanced number', async () => {
|
|
77
|
+
render(
|
|
78
|
+
<MockedProvider mocks={gqlMock} addTypename={false}>
|
|
79
|
+
<Provider store={store}>
|
|
80
|
+
<JobWizard />
|
|
81
|
+
</Provider>
|
|
82
|
+
</MockedProvider>
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// setup
|
|
86
|
+
await act(async () => {
|
|
87
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.hostsAndInputs));
|
|
88
|
+
});
|
|
89
|
+
await act(async () => {
|
|
90
|
+
await fireEvent.change(
|
|
91
|
+
screen.getByLabelText('plain hidden', {
|
|
92
|
+
selector: 'textarea',
|
|
93
|
+
}),
|
|
94
|
+
{
|
|
95
|
+
target: { value: 'text' },
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await act(async () => {
|
|
101
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
102
|
+
});
|
|
103
|
+
await act(async () => {
|
|
104
|
+
await fireEvent.change(
|
|
105
|
+
screen.getByLabelText('adv plain hidden', {
|
|
106
|
+
selector: 'textarea',
|
|
107
|
+
}),
|
|
108
|
+
{
|
|
109
|
+
target: { value: 'text' },
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
expect(
|
|
114
|
+
screen.getByText(WIZARD_TITLES.advanced, { selector: 'button' })
|
|
115
|
+
).toBeEnabled();
|
|
116
|
+
expect(screen.getByText(WIZARD_TITLES.schedule)).toBeEnabled();
|
|
117
|
+
expect(screen.getByText(WIZARD_TITLES.review)).toBeEnabled();
|
|
118
|
+
|
|
119
|
+
// test
|
|
120
|
+
const timeoutField = screen.getByLabelText('timeout to kill', {
|
|
121
|
+
selector: 'input',
|
|
122
|
+
});
|
|
123
|
+
await act(async () => {
|
|
124
|
+
await fireEvent.change(timeoutField, {
|
|
125
|
+
target: { value: 'text' },
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(screen.getByText(WIZARD_TITLES.schedule)).toBeDisabled();
|
|
130
|
+
expect(screen.getByText(WIZARD_TITLES.review)).toBeDisabled();
|
|
131
|
+
|
|
132
|
+
await act(async () => {
|
|
133
|
+
await fireEvent.change(timeoutField, {
|
|
134
|
+
target: { value: 123 },
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(screen.getByText(WIZARD_TITLES.schedule)).toBeEnabled();
|
|
139
|
+
expect(screen.getByText(WIZARD_TITLES.review)).toBeEnabled();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
3
|
+
import { get } from 'foremanReact/redux/API';
|
|
4
|
+
import { HOST_IDS } from './JobWizardConstants';
|
|
5
|
+
import { selectRouterSearch } from './JobWizardSelectors';
|
|
6
|
+
import './JobWizard.scss';
|
|
7
|
+
|
|
8
|
+
export const useAutoFill = ({ setSelectedTargets, setHostsSearchQuery }) => {
|
|
9
|
+
const fills = useSelector(selectRouterSearch);
|
|
10
|
+
const dispatch = useDispatch();
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (Object.keys(fills).length) {
|
|
14
|
+
if (fills['host_ids[]']) {
|
|
15
|
+
dispatch(
|
|
16
|
+
get({
|
|
17
|
+
key: HOST_IDS,
|
|
18
|
+
url: '/api/hosts',
|
|
19
|
+
params: { search: `id = ${fills['host_ids[]'].join(' or id = ')}` },
|
|
20
|
+
handleSuccess: ({ data }) => {
|
|
21
|
+
setSelectedTargets(currentTargets => ({
|
|
22
|
+
...currentTargets,
|
|
23
|
+
hosts: (data.results || []).map(({ name }) => ({
|
|
24
|
+
id: name,
|
|
25
|
+
name,
|
|
26
|
+
})),
|
|
27
|
+
}));
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (fills.search) {
|
|
33
|
+
setHostsSearchQuery(fills.search);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
37
|
+
}, []);
|
|
38
|
+
};
|
data/webpack/JobWizard/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
2
3
|
import { Title, Divider } from '@patternfly/react-core';
|
|
3
4
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
4
5
|
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
|
|
@@ -29,4 +30,10 @@ const JobWizardPage = () => {
|
|
|
29
30
|
);
|
|
30
31
|
};
|
|
31
32
|
|
|
33
|
+
JobWizardPage.propTypes = {
|
|
34
|
+
location: PropTypes.shape({
|
|
35
|
+
search: PropTypes.string,
|
|
36
|
+
}).isRequired,
|
|
37
|
+
};
|
|
38
|
+
|
|
32
39
|
export default JobWizardPage;
|
|
@@ -5,7 +5,6 @@ import { Form } from '@patternfly/react-core';
|
|
|
5
5
|
import {
|
|
6
6
|
selectEffectiveUser,
|
|
7
7
|
selectAdvancedTemplateInputs,
|
|
8
|
-
selectTemplateInputs,
|
|
9
8
|
} from '../../JobWizardSelectors';
|
|
10
9
|
import {
|
|
11
10
|
EffectiveUserField,
|
|
@@ -22,10 +21,13 @@ import { DescriptionField } from './DescriptionField';
|
|
|
22
21
|
import { WIZARD_TITLES } from '../../JobWizardConstants';
|
|
23
22
|
import { WizardTitle } from '../form/WizardTitle';
|
|
24
23
|
|
|
25
|
-
export const AdvancedFields = ({
|
|
24
|
+
export const AdvancedFields = ({
|
|
25
|
+
templateValues,
|
|
26
|
+
advancedValues,
|
|
27
|
+
setAdvancedValues,
|
|
28
|
+
}) => {
|
|
26
29
|
const effectiveUser = useSelector(selectEffectiveUser);
|
|
27
30
|
const advancedTemplateInputs = useSelector(selectAdvancedTemplateInputs);
|
|
28
|
-
const templateInputs = useSelector(selectTemplateInputs);
|
|
29
31
|
return (
|
|
30
32
|
<>
|
|
31
33
|
<WizardTitle
|
|
@@ -49,7 +51,7 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
|
|
|
49
51
|
/>
|
|
50
52
|
)}
|
|
51
53
|
<DescriptionField
|
|
52
|
-
|
|
54
|
+
inputValues={{ ...templateValues, ...advancedValues.templateValues }}
|
|
53
55
|
value={advancedValues.description}
|
|
54
56
|
setValue={newValue => setAdvancedValues({ description: newValue })}
|
|
55
57
|
/>
|
|
@@ -117,5 +119,6 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
|
|
|
117
119
|
AdvancedFields.propTypes = {
|
|
118
120
|
advancedValues: PropTypes.object.isRequired,
|
|
119
121
|
setAdvancedValues: PropTypes.func.isRequired,
|
|
122
|
+
templateValues: PropTypes.object.isRequired,
|
|
120
123
|
};
|
|
121
124
|
export default AdvancedFields;
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { useSelector } from 'react-redux';
|
|
2
3
|
import PropTypes from 'prop-types';
|
|
3
|
-
import { FormGroup, TextInput, Button } from '@patternfly/react-core';
|
|
4
|
+
import { FormGroup, TextInput, Tooltip, Button } from '@patternfly/react-core';
|
|
4
5
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
6
|
+
import {
|
|
7
|
+
selectTemplateInputs,
|
|
8
|
+
selectAdvancedTemplateInputs,
|
|
9
|
+
} from '../../JobWizardSelectors';
|
|
5
10
|
|
|
6
|
-
export const DescriptionField = ({
|
|
7
|
-
const
|
|
11
|
+
export const DescriptionField = ({ inputValues, value, setValue }) => {
|
|
12
|
+
const inputs = [
|
|
13
|
+
...useSelector(selectTemplateInputs),
|
|
14
|
+
...useSelector(selectAdvancedTemplateInputs),
|
|
15
|
+
].map(input => input.name);
|
|
16
|
+
const generateDesc = useCallback(() => {
|
|
8
17
|
let newDesc = value;
|
|
9
18
|
if (value) {
|
|
10
19
|
const re = new RegExp('%\\{([^\\}]+)\\}', 'gm');
|
|
@@ -15,16 +24,19 @@ export const DescriptionField = ({ inputs, value, setValue }) => {
|
|
|
15
24
|
results.forEach(result => {
|
|
16
25
|
newDesc = newDesc.replace(
|
|
17
26
|
result.text,
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
inputValues[result.name] ||
|
|
28
|
+
(inputs.includes(result.name) ? '' : result.text)
|
|
20
29
|
);
|
|
21
30
|
});
|
|
22
31
|
}
|
|
23
32
|
return newDesc;
|
|
24
|
-
};
|
|
33
|
+
}, [inputs, value, inputValues]);
|
|
25
34
|
const [generatedDesc, setGeneratedDesc] = useState(generateDesc());
|
|
26
35
|
const [isPreview, setIsPreview] = useState(true);
|
|
27
36
|
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
setGeneratedDesc(generateDesc());
|
|
39
|
+
}, [generateDesc]);
|
|
28
40
|
const togglePreview = () => {
|
|
29
41
|
setGeneratedDesc(generateDesc());
|
|
30
42
|
setIsPreview(v => !v);
|
|
@@ -43,9 +55,20 @@ export const DescriptionField = ({ inputs, value, setValue }) => {
|
|
|
43
55
|
}
|
|
44
56
|
>
|
|
45
57
|
{isPreview ? (
|
|
46
|
-
<
|
|
58
|
+
<Tooltip content={generatedDesc}>
|
|
59
|
+
<div>
|
|
60
|
+
{/* div wrapper so the tooltip will be shown in chrome */}
|
|
61
|
+
<TextInput
|
|
62
|
+
aria-label="description preview"
|
|
63
|
+
id="description-preview"
|
|
64
|
+
value={generatedDesc}
|
|
65
|
+
isDisabled
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
</Tooltip>
|
|
47
69
|
) : (
|
|
48
70
|
<TextInput
|
|
71
|
+
aria-label="description edit"
|
|
49
72
|
type="text"
|
|
50
73
|
autoComplete="description"
|
|
51
74
|
id="description"
|
|
@@ -58,7 +81,7 @@ export const DescriptionField = ({ inputs, value, setValue }) => {
|
|
|
58
81
|
};
|
|
59
82
|
|
|
60
83
|
DescriptionField.propTypes = {
|
|
61
|
-
|
|
84
|
+
inputValues: PropTypes.object.isRequired,
|
|
62
85
|
value: PropTypes.string,
|
|
63
86
|
setValue: PropTypes.func.isRequired,
|
|
64
87
|
};
|
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
import React from 'react';
|
|
2
3
|
import { Provider } from 'react-redux';
|
|
3
4
|
import { mount } from '@theforeman/test';
|
|
4
5
|
import { fireEvent, screen, render, act } from '@testing-library/react';
|
|
6
|
+
import { MockedProvider } from '@apollo/client/testing';
|
|
5
7
|
import * as api from 'foremanReact/redux/API';
|
|
6
8
|
import { JobWizard } from '../../../JobWizard';
|
|
7
9
|
import * as selectors from '../../../JobWizardSelectors';
|
|
8
10
|
import {
|
|
9
|
-
jobTemplateResponse
|
|
11
|
+
jobTemplateResponse,
|
|
12
|
+
jobTemplate,
|
|
10
13
|
testSetup,
|
|
11
14
|
mockApi,
|
|
15
|
+
jobCategories,
|
|
16
|
+
gqlMock,
|
|
12
17
|
} from '../../../__tests__/fixtures';
|
|
13
18
|
import { WIZARD_TITLES } from '../../../JobWizardConstants';
|
|
14
19
|
|
|
@@ -18,14 +23,16 @@ mockApi(api);
|
|
|
18
23
|
jest.spyOn(selectors, 'selectEffectiveUser');
|
|
19
24
|
|
|
20
25
|
selectors.selectEffectiveUser.mockImplementation(
|
|
21
|
-
() =>
|
|
26
|
+
() => jobTemplateResponse.effective_user
|
|
22
27
|
);
|
|
23
28
|
describe('AdvancedFields', () => {
|
|
24
29
|
it('should save data between steps for advanced fields', async () => {
|
|
25
30
|
const wrapper = mount(
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
|
|
31
|
+
<MockedProvider mocks={gqlMock} addTypename={false}>
|
|
32
|
+
<Provider store={store}>
|
|
33
|
+
<JobWizard />
|
|
34
|
+
</Provider>
|
|
35
|
+
</MockedProvider>
|
|
29
36
|
);
|
|
30
37
|
// setup
|
|
31
38
|
wrapper.find('.pf-c-button.pf-c-select__toggle-button').simulate('click');
|
|
@@ -78,9 +85,11 @@ describe('AdvancedFields', () => {
|
|
|
78
85
|
});
|
|
79
86
|
it('fill template fields', async () => {
|
|
80
87
|
render(
|
|
81
|
-
<
|
|
82
|
-
<
|
|
83
|
-
|
|
88
|
+
<MockedProvider mocks={gqlMock} addTypename={false}>
|
|
89
|
+
<Provider store={store}>
|
|
90
|
+
<JobWizard />
|
|
91
|
+
</Provider>
|
|
92
|
+
</MockedProvider>
|
|
84
93
|
);
|
|
85
94
|
await act(async () => {
|
|
86
95
|
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
@@ -91,7 +100,10 @@ describe('AdvancedFields', () => {
|
|
|
91
100
|
const textField = screen.getByLabelText('adv plain hidden', {
|
|
92
101
|
selector: 'textarea',
|
|
93
102
|
});
|
|
94
|
-
const selectField = screen.
|
|
103
|
+
const selectField = screen.getByLabelText('adv plain select toggle');
|
|
104
|
+
const resourceSelectField = screen.getByLabelText(
|
|
105
|
+
'adv resource select toggle'
|
|
106
|
+
);
|
|
95
107
|
const searchField = screen.getByPlaceholderText('Filter...');
|
|
96
108
|
const dateField = screen.getByLabelText('adv date', {
|
|
97
109
|
selector: 'input',
|
|
@@ -101,6 +113,10 @@ describe('AdvancedFields', () => {
|
|
|
101
113
|
await act(async () => {
|
|
102
114
|
await fireEvent.click(screen.getByText('option 2'));
|
|
103
115
|
fireEvent.click(screen.getAllByText(WIZARD_TITLES.advanced)[0]); // to remove focus
|
|
116
|
+
|
|
117
|
+
fireEvent.click(resourceSelectField);
|
|
118
|
+
await fireEvent.click(screen.getByText('resource2'));
|
|
119
|
+
|
|
104
120
|
await fireEvent.change(textField, {
|
|
105
121
|
target: { value: textValue },
|
|
106
122
|
});
|
|
@@ -134,12 +150,16 @@ describe('AdvancedFields', () => {
|
|
|
134
150
|
expect(dateField.value).toBe(dateValue);
|
|
135
151
|
expect(screen.queryAllByText('option 1')).toHaveLength(0);
|
|
136
152
|
expect(screen.queryAllByText('option 2')).toHaveLength(1);
|
|
153
|
+
expect(screen.queryAllByDisplayValue('resource1')).toHaveLength(0);
|
|
154
|
+
expect(screen.queryAllByDisplayValue('resource2')).toHaveLength(1);
|
|
137
155
|
});
|
|
138
156
|
it('fill defaults into fields', async () => {
|
|
139
157
|
render(
|
|
140
|
-
<
|
|
141
|
-
<
|
|
142
|
-
|
|
158
|
+
<MockedProvider mocks={gqlMock} addTypename={false}>
|
|
159
|
+
<Provider store={store}>
|
|
160
|
+
<JobWizard />
|
|
161
|
+
</Provider>
|
|
162
|
+
</MockedProvider>
|
|
143
163
|
);
|
|
144
164
|
await act(async () => {
|
|
145
165
|
fireEvent.click(screen.getByText('Advanced Fields'));
|
|
@@ -155,5 +175,189 @@ describe('AdvancedFields', () => {
|
|
|
155
175
|
selector: 'input',
|
|
156
176
|
}).value
|
|
157
177
|
).toBe('2');
|
|
178
|
+
|
|
179
|
+
expect(
|
|
180
|
+
screen.getByLabelText('description preview', {
|
|
181
|
+
selector: 'input',
|
|
182
|
+
}).value
|
|
183
|
+
).toBe(
|
|
184
|
+
'template1 with inputs adv plain hidden="Default val" adv plain select="" adv resource select="" adv search="" adv date="" plain hidden="Default val"'
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
it('DescriptionField', async () => {
|
|
188
|
+
render(
|
|
189
|
+
<Provider store={store}>
|
|
190
|
+
<JobWizard />
|
|
191
|
+
</Provider>
|
|
192
|
+
);
|
|
193
|
+
await act(async () => {
|
|
194
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const textField = screen.getByLabelText('adv plain hidden', {
|
|
198
|
+
selector: 'textarea',
|
|
199
|
+
});
|
|
200
|
+
await act(async () => {
|
|
201
|
+
await fireEvent.change(textField, {
|
|
202
|
+
target: { value: 'test command' },
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
const descriptionValue = 'Run %{adv plain hidden} %{wrong command name}';
|
|
206
|
+
|
|
207
|
+
await act(async () => {
|
|
208
|
+
fireEvent.click(screen.getByText('Edit job description template'));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const editText = screen.getByLabelText('description edit', {
|
|
212
|
+
selector: 'input',
|
|
213
|
+
});
|
|
214
|
+
await fireEvent.change(editText, {
|
|
215
|
+
target: { value: descriptionValue },
|
|
216
|
+
});
|
|
217
|
+
await act(async () => {
|
|
218
|
+
fireEvent.click(screen.getByText('Preview job description'));
|
|
219
|
+
});
|
|
220
|
+
expect(
|
|
221
|
+
screen.getByLabelText('description preview', {
|
|
222
|
+
selector: 'input',
|
|
223
|
+
}).value
|
|
224
|
+
).toBe('Run test command %{wrong command name}');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('DescriptionField with no inputs', async () => {
|
|
228
|
+
jest.spyOn(api, 'get');
|
|
229
|
+
|
|
230
|
+
jest.spyOn(selectors, 'selectTemplateInputs');
|
|
231
|
+
jest.spyOn(selectors, 'selectAdvancedTemplateInputs');
|
|
232
|
+
selectors.selectTemplateInputs.mockImplementation(() => []);
|
|
233
|
+
selectors.selectAdvancedTemplateInputs.mockImplementation(() => []);
|
|
234
|
+
api.get.mockImplementation(({ handleSuccess, ...action }) => {
|
|
235
|
+
if (action.key === 'JOB_CATEGORIES') {
|
|
236
|
+
handleSuccess &&
|
|
237
|
+
handleSuccess({ data: { job_categories: jobCategories } });
|
|
238
|
+
} else if (action.key === 'JOB_TEMPLATE') {
|
|
239
|
+
handleSuccess &&
|
|
240
|
+
handleSuccess({
|
|
241
|
+
data: {
|
|
242
|
+
...jobTemplateResponse,
|
|
243
|
+
advanced_template_inputs: [],
|
|
244
|
+
template_inputs: [],
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
} else if (action.key === 'JOB_TEMPLATES') {
|
|
248
|
+
handleSuccess &&
|
|
249
|
+
handleSuccess({
|
|
250
|
+
data: { results: [jobTemplate] },
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return { type: 'get', ...action };
|
|
254
|
+
});
|
|
255
|
+
render(
|
|
256
|
+
<Provider store={store}>
|
|
257
|
+
<JobWizard />
|
|
258
|
+
</Provider>
|
|
259
|
+
);
|
|
260
|
+
await act(async () => {
|
|
261
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
262
|
+
});
|
|
263
|
+
expect(
|
|
264
|
+
screen.getByLabelText('description preview', {
|
|
265
|
+
selector: 'input',
|
|
266
|
+
}).value
|
|
267
|
+
).toBe('template1');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('DescriptionField with description_format', async () => {
|
|
271
|
+
jest.spyOn(api, 'get');
|
|
272
|
+
jest.spyOn(selectors, 'selectTemplateInputs');
|
|
273
|
+
selectors.selectTemplateInputs.mockImplementation(() => [
|
|
274
|
+
{
|
|
275
|
+
name: 'command',
|
|
276
|
+
required: true,
|
|
277
|
+
input_type: 'user',
|
|
278
|
+
description: 'some Description',
|
|
279
|
+
advanced: true,
|
|
280
|
+
value_type: 'plain',
|
|
281
|
+
resource_type: 'ansible_roles',
|
|
282
|
+
default: 'Default val',
|
|
283
|
+
hidden_value: true,
|
|
284
|
+
},
|
|
285
|
+
]);
|
|
286
|
+
api.get.mockImplementation(({ handleSuccess, ...action }) => {
|
|
287
|
+
if (action.key === 'JOB_CATEGORIES') {
|
|
288
|
+
handleSuccess &&
|
|
289
|
+
handleSuccess({ data: { job_categories: jobCategories } });
|
|
290
|
+
} else if (action.key === 'JOB_TEMPLATE') {
|
|
291
|
+
handleSuccess &&
|
|
292
|
+
handleSuccess({
|
|
293
|
+
data: {
|
|
294
|
+
...jobTemplateResponse,
|
|
295
|
+
job_template: {
|
|
296
|
+
...jobTemplateResponse.jobTemplate,
|
|
297
|
+
description_format: 'Run %{command}',
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
template_inputs: [
|
|
301
|
+
{
|
|
302
|
+
name: 'command',
|
|
303
|
+
required: true,
|
|
304
|
+
input_type: 'user',
|
|
305
|
+
description: 'some Description',
|
|
306
|
+
advanced: true,
|
|
307
|
+
value_type: 'plain',
|
|
308
|
+
resource_type: 'ansible_roles',
|
|
309
|
+
default: 'Default val',
|
|
310
|
+
hidden_value: true,
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
} else if (action.key === 'JOB_TEMPLATES') {
|
|
316
|
+
handleSuccess &&
|
|
317
|
+
handleSuccess({
|
|
318
|
+
data: { results: [jobTemplate] },
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return { type: 'get', ...action };
|
|
322
|
+
});
|
|
323
|
+
render(
|
|
324
|
+
<Provider store={store}>
|
|
325
|
+
<JobWizard />
|
|
326
|
+
</Provider>
|
|
327
|
+
);
|
|
328
|
+
await act(async () => {
|
|
329
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
330
|
+
});
|
|
331
|
+
expect(
|
|
332
|
+
screen.getByLabelText('description preview', {
|
|
333
|
+
selector: 'input',
|
|
334
|
+
}).value
|
|
335
|
+
).toBe('Run Default val');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('search resources action', async () => {
|
|
339
|
+
jest.useFakeTimers();
|
|
340
|
+
mockApi(api);
|
|
341
|
+
const newStore = testSetup(selectors, api);
|
|
342
|
+
render(
|
|
343
|
+
<Provider store={newStore}>
|
|
344
|
+
<JobWizard />
|
|
345
|
+
</Provider>
|
|
346
|
+
);
|
|
347
|
+
await act(async () => {
|
|
348
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
349
|
+
});
|
|
350
|
+
const resourceSelectField = screen.getByLabelText(
|
|
351
|
+
'adv resource select typeahead input'
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
await act(async () => {
|
|
355
|
+
await fireEvent.change(resourceSelectField, {
|
|
356
|
+
target: { value: 'some search' },
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
await jest.runAllTimers();
|
|
360
|
+
});
|
|
361
|
+
expect(newStore.getActions()).toMatchSnapshot('resource search');
|
|
158
362
|
});
|
|
159
363
|
});
|