foreman_remote_execution 4.7.0 → 5.0.2

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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +1 -0
  3. data/app/controllers/api/v2/job_invocations_controller.rb +16 -1
  4. data/app/controllers/ui_job_wizard_controller.rb +16 -4
  5. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  6. data/app/graphql/types/job_invocation_input.rb +13 -0
  7. data/app/graphql/types/recurrence_input.rb +8 -0
  8. data/app/graphql/types/scheduling_input.rb +6 -0
  9. data/app/graphql/types/targeting_enum.rb +7 -0
  10. data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +20 -9
  11. data/app/helpers/remote_execution_helper.rb +1 -1
  12. data/app/lib/actions/remote_execution/run_host_job.rb +6 -1
  13. data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
  14. data/app/mailers/rex_job_mailer.rb +15 -0
  15. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +12 -0
  16. data/app/models/job_invocation.rb +4 -0
  17. data/app/models/job_invocation_composer.rb +21 -13
  18. data/app/models/remote_execution_provider.rb +18 -2
  19. data/app/models/rex_mail_notification.rb +13 -0
  20. data/app/models/targeting.rb +3 -3
  21. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  22. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  23. data/app/views/job_invocations/_preview_hosts_list.html.erb +1 -1
  24. data/app/views/job_invocations/refresh.js.erb +1 -0
  25. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  26. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  27. data/app/views/template_invocations/show.html.erb +3 -2
  28. data/config/routes.rb +1 -0
  29. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  30. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  31. data/db/seeds.d/95-mail_notifications.rb +24 -0
  32. data/foreman_remote_execution.gemspec +1 -1
  33. data/lib/foreman_remote_execution/engine.rb +116 -7
  34. data/lib/foreman_remote_execution/version.rb +1 -1
  35. data/package.json +9 -7
  36. data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
  37. data/test/functional/cockpit_controller_test.rb +0 -1
  38. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  39. data/test/helpers/remote_execution_helper_test.rb +0 -1
  40. data/test/unit/actions/run_host_job_test.rb +21 -0
  41. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  42. data/test/unit/concerns/host_extensions_test.rb +36 -3
  43. data/test/unit/job_invocation_composer_test.rb +3 -5
  44. data/test/unit/job_invocation_report_template_test.rb +16 -13
  45. data/test/unit/job_template_effective_user_test.rb +0 -4
  46. data/test/unit/remote_execution_provider_test.rb +46 -4
  47. data/test/unit/targeting_test.rb +68 -1
  48. data/webpack/JobWizard/JobWizard.js +142 -28
  49. data/webpack/JobWizard/JobWizard.scss +86 -33
  50. data/webpack/JobWizard/JobWizardConstants.js +44 -0
  51. data/webpack/JobWizard/JobWizardSelectors.js +32 -0
  52. data/webpack/JobWizard/__tests__/fixtures.js +89 -6
  53. data/webpack/JobWizard/__tests__/integration.test.js +29 -22
  54. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  55. data/webpack/JobWizard/autofill.js +38 -0
  56. data/webpack/JobWizard/index.js +7 -0
  57. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +23 -9
  58. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
  59. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +48 -1
  60. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +242 -23
  61. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
  62. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
  63. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +3 -2
  64. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  65. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  66. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  67. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  68. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  69. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +100 -0
  70. data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
  71. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  72. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +53 -0
  73. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  74. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  75. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  76. data/webpack/JobWizard/steps/HostsAndInputs/index.js +214 -0
  77. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  78. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  79. data/webpack/JobWizard/steps/Schedule/QueryType.js +46 -43
  80. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  81. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  82. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  83. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  84. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
  85. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  86. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
  87. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +78 -23
  88. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
  89. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +20 -10
  90. data/webpack/JobWizard/steps/Schedule/index.js +166 -29
  91. data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
  92. data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
  93. data/webpack/JobWizard/steps/form/Formatter.js +49 -17
  94. data/webpack/JobWizard/steps/form/NumberInput.js +5 -2
  95. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  96. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  97. data/webpack/JobWizard/steps/form/SelectField.js +14 -3
  98. data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
  99. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  100. data/webpack/JobWizard/submit.js +120 -0
  101. data/webpack/JobWizard/validation.js +53 -0
  102. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  103. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  104. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  105. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  106. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  107. data/webpack/helpers.js +1 -0
  108. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +2 -1
  109. metadata +53 -7
  110. data/app/models/setting/remote_execution.rb +0 -88
  111. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
  112. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
@@ -1,12 +1,10 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { useSelector } from 'react-redux';
4
- import { Title, Form } from '@patternfly/react-core';
5
- import { translate as __ } from 'foremanReact/common/I18n';
4
+ import { Form } from '@patternfly/react-core';
6
5
  import {
7
6
  selectEffectiveUser,
8
7
  selectAdvancedTemplateInputs,
9
- selectTemplateInputs,
10
8
  } from '../../JobWizardSelectors';
11
9
  import {
12
10
  EffectiveUserField,
@@ -17,18 +15,25 @@ import {
17
15
  ConcurrencyLevelField,
18
16
  TimeSpanLevelField,
19
17
  TemplateInputsFields,
18
+ ExecutionOrderingField,
20
19
  } from './Fields';
21
20
  import { DescriptionField } from './DescriptionField';
21
+ import { WIZARD_TITLES } from '../../JobWizardConstants';
22
+ import { WizardTitle } from '../form/WizardTitle';
22
23
 
23
- export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
24
+ export const AdvancedFields = ({
25
+ templateValues,
26
+ advancedValues,
27
+ setAdvancedValues,
28
+ }) => {
24
29
  const effectiveUser = useSelector(selectEffectiveUser);
25
30
  const advancedTemplateInputs = useSelector(selectAdvancedTemplateInputs);
26
- const templateInputs = useSelector(selectTemplateInputs);
27
31
  return (
28
32
  <>
29
- <Title headingLevel="h2" className="advanced-fields-title">
30
- {__('Advanced Fields')}
31
- </Title>
33
+ <WizardTitle
34
+ title={WIZARD_TITLES.advanced}
35
+ className="advanced-fields-title"
36
+ />
32
37
  <Form id="advanced-fields-job-template" autoComplete="off">
33
38
  <TemplateInputsFields
34
39
  inputs={advancedTemplateInputs}
@@ -46,7 +51,7 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
46
51
  />
47
52
  )}
48
53
  <DescriptionField
49
- inputs={templateInputs}
54
+ inputValues={{ ...templateValues, ...advancedValues.templateValues }}
50
55
  value={advancedValues.description}
51
56
  setValue={newValue => setAdvancedValues({ description: newValue })}
52
57
  />
@@ -98,6 +103,14 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
98
103
  })
99
104
  }
100
105
  />
106
+ <ExecutionOrderingField
107
+ isRandomizedOrdering={advancedValues.isRandomizedOrdering}
108
+ setValue={newValue =>
109
+ setAdvancedValues({
110
+ isRandomizedOrdering: newValue,
111
+ })
112
+ }
113
+ />
101
114
  </Form>
102
115
  </>
103
116
  );
@@ -106,5 +119,6 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
106
119
  AdvancedFields.propTypes = {
107
120
  advancedValues: PropTypes.object.isRequired,
108
121
  setAdvancedValues: PropTypes.func.isRequired,
122
+ templateValues: PropTypes.object.isRequired,
109
123
  };
110
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 = ({ inputs, value, setValue }) => {
7
- const generateDesc = () => {
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
- // TODO: Replace with the value of the input from Target Hosts step
19
- inputs.find(input => input.name === result.name)?.name || result.text
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
- <TextInput id="description-preview" value={generatedDesc} isDisabled />
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
- inputs: PropTypes.array.isRequired,
84
+ inputValues: PropTypes.object.isRequired,
62
85
  value: PropTypes.string,
63
86
  setValue: PropTypes.func.isRequired,
64
87
  };
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { FormGroup, TextInput } from '@patternfly/react-core';
3
+ import { FormGroup, TextInput, Radio } from '@patternfly/react-core';
4
4
  import { translate as __ } from 'foremanReact/common/I18n';
5
5
  import { helpLabel } from '../form/FormHelpers';
6
6
  import { formatter } from '../form/Formatter';
@@ -18,6 +18,7 @@ export const EffectiveUserField = ({ value, setValue }) => (
18
18
  fieldId="effective-user"
19
19
  >
20
20
  <TextInput
21
+ aria-label="effective user"
21
22
  autoComplete="effective-user"
22
23
  id="effective-user"
23
24
  type="text"
@@ -61,6 +62,7 @@ export const PasswordField = ({ value, setValue }) => (
61
62
  fieldId="job-password"
62
63
  >
63
64
  <TextInput
65
+ aria-label="job password"
64
66
  autoComplete="new-password" // to prevent firefox from autofilling the user password
65
67
  id="job-password"
66
68
  type="password"
@@ -83,6 +85,7 @@ export const KeyPassphraseField = ({ value, setValue }) => (
83
85
  fieldId="key-passphrase"
84
86
  >
85
87
  <TextInput
88
+ aria-label="key passphrase"
86
89
  autoComplete="key-passphrase"
87
90
  id="key-passphrase"
88
91
  type="password"
@@ -105,6 +108,7 @@ export const EffectiveUserPasswordField = ({ value, setValue }) => (
105
108
  fieldId="effective-user-password"
106
109
  >
107
110
  <TextInput
111
+ aria-label="effective userpassword"
108
112
  autoComplete="effective-user-password"
109
113
  id="effective-user-password"
110
114
  type="password"
@@ -161,6 +165,41 @@ export const TimeSpanLevelField = ({ value, setValue }) => (
161
165
  />
162
166
  );
163
167
 
168
+ export const ExecutionOrderingField = ({ isRandomizedOrdering, setValue }) => (
169
+ <FormGroup
170
+ label={__('Execution ordering')}
171
+ fieldId="schedule-type"
172
+ labelIcon={helpLabel(
173
+ <div
174
+ dangerouslySetInnerHTML={{
175
+ __html: __(
176
+ 'Execution ordering determines whether the jobs should be executed on hosts in alphabetical order or in randomized order.<br><ul><li><b>Ordered</b> - executes the jobs on hosts in alphabetical order</li><li><b>Randomized</b> - randomizes the order in which jobs are executed on hosts</li></ul>'
177
+ ),
178
+ }}
179
+ />,
180
+ 'effective-user-password'
181
+ )}
182
+ isInline
183
+ >
184
+ <Radio
185
+ aria-label="execution order alphabetical"
186
+ isChecked={!isRandomizedOrdering}
187
+ name="execution-order"
188
+ onChange={() => setValue(false)}
189
+ id="execution-order-alphabetical"
190
+ label={__('Alphabetical')}
191
+ />
192
+ <Radio
193
+ aria-label="execution order randomized"
194
+ isChecked={isRandomizedOrdering}
195
+ name="execution-order"
196
+ onChange={() => setValue(true)}
197
+ id="execution-order-randomized"
198
+ label={__('Randomized')}
199
+ />
200
+ </FormGroup>
201
+ );
202
+
164
203
  export const TemplateInputsFields = ({ inputs, value, setValue }) => (
165
204
  <>{inputs?.map(input => formatter(input, value, setValue))}</>
166
205
  );
@@ -184,6 +223,14 @@ ConcurrencyLevelField.propTypes = EffectiveUserField.propTypes;
184
223
  ConcurrencyLevelField.defaultProps = EffectiveUserField.defaultProps;
185
224
  TimeSpanLevelField.propTypes = EffectiveUserField.propTypes;
186
225
  TimeSpanLevelField.defaultProps = EffectiveUserField.defaultProps;
226
+ ExecutionOrderingField.propTypes = {
227
+ isRandomizedOrdering: PropTypes.bool,
228
+ setValue: PropTypes.func.isRequired,
229
+ };
230
+ ExecutionOrderingField.defaultProps = {
231
+ isRandomizedOrdering: false,
232
+ };
233
+
187
234
  TemplateInputsFields.propTypes = {
188
235
  inputs: PropTypes.array.isRequired,
189
236
  value: PropTypes.object,
@@ -1,39 +1,38 @@
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 as jobTemplate,
11
+ jobTemplateResponse,
12
+ jobTemplate,
10
13
  testSetup,
11
14
  mockApi,
15
+ jobCategories,
16
+ gqlMock,
12
17
  } from '../../../__tests__/fixtures';
18
+ import { WIZARD_TITLES } from '../../../JobWizardConstants';
13
19
 
14
20
  const store = testSetup(selectors, api);
15
21
  mockApi(api);
16
22
 
17
23
  jest.spyOn(selectors, 'selectEffectiveUser');
18
- jest.spyOn(selectors, 'selectTemplateInputs');
19
- jest.spyOn(selectors, 'selectAdvancedTemplateInputs');
20
24
 
21
25
  selectors.selectEffectiveUser.mockImplementation(
22
- () => jobTemplate.effective_user
23
- );
24
- selectors.selectTemplateInputs.mockImplementation(
25
- () => jobTemplate.template_inputs
26
- );
27
-
28
- selectors.selectAdvancedTemplateInputs.mockImplementation(
29
- () => jobTemplate.advanced_template_inputs
26
+ () => jobTemplateResponse.effective_user
30
27
  );
31
28
  describe('AdvancedFields', () => {
32
29
  it('should save data between steps for advanced fields', async () => {
33
30
  const wrapper = mount(
34
- <Provider store={store}>
35
- <JobWizard advancedValues={{}} setAdvancedValues={jest.fn()} />
36
- </Provider>
31
+ <MockedProvider mocks={gqlMock} addTypename={false}>
32
+ <Provider store={store}>
33
+ <JobWizard />
34
+ </Provider>
35
+ </MockedProvider>
37
36
  );
38
37
  // setup
39
38
  wrapper.find('.pf-c-button.pf-c-select__toggle-button').simulate('click');
@@ -72,7 +71,7 @@ describe('AdvancedFields', () => {
72
71
  .simulate('click');
73
72
 
74
73
  expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-current').text()).toEqual(
75
- 'Target Hosts'
74
+ 'Target hosts and inputs'
76
75
  );
77
76
  wrapper
78
77
  .find('.pf-c-wizard__nav-link')
@@ -86,12 +85,14 @@ describe('AdvancedFields', () => {
86
85
  });
87
86
  it('fill template fields', async () => {
88
87
  render(
89
- <Provider store={store}>
90
- <JobWizard />
91
- </Provider>
88
+ <MockedProvider mocks={gqlMock} addTypename={false}>
89
+ <Provider store={store}>
90
+ <JobWizard />
91
+ </Provider>
92
+ </MockedProvider>
92
93
  );
93
94
  await act(async () => {
94
- fireEvent.click(screen.getByText('Advanced Fields'));
95
+ fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
95
96
  });
96
97
  const searchValue = 'search test';
97
98
  const textValue = 'I am a text';
@@ -99,7 +100,10 @@ describe('AdvancedFields', () => {
99
100
  const textField = screen.getByLabelText('adv plain hidden', {
100
101
  selector: 'textarea',
101
102
  });
102
- const selectField = screen.getByText('option 1');
103
+ const selectField = screen.getByLabelText('adv plain select toggle');
104
+ const resourceSelectField = screen.getByLabelText(
105
+ 'adv resource select toggle'
106
+ );
103
107
  const searchField = screen.getByPlaceholderText('Filter...');
104
108
  const dateField = screen.getByLabelText('adv date', {
105
109
  selector: 'input',
@@ -108,7 +112,11 @@ describe('AdvancedFields', () => {
108
112
  fireEvent.click(selectField);
109
113
  await act(async () => {
110
114
  await fireEvent.click(screen.getByText('option 2'));
111
- fireEvent.click(screen.getAllByText('Advanced Fields')[0]); // to remove focus
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
+
112
120
  await fireEvent.change(textField, {
113
121
  target: { value: textValue },
114
122
  });
@@ -128,9 +136,11 @@ describe('AdvancedFields', () => {
128
136
  expect(searchField.value).toBe(searchValue);
129
137
  expect(dateField.value).toBe(dateValue);
130
138
  await act(async () => {
131
- fireEvent.click(screen.getByText('Category and Template'));
139
+ fireEvent.click(screen.getByText(WIZARD_TITLES.categoryAndTemplate));
132
140
  });
133
- expect(screen.getAllByText('Category and Template')).toHaveLength(3);
141
+ expect(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)).toHaveLength(
142
+ 3
143
+ );
134
144
 
135
145
  await act(async () => {
136
146
  fireEvent.click(screen.getByText('Advanced Fields'));
@@ -140,5 +150,214 @@ describe('AdvancedFields', () => {
140
150
  expect(dateField.value).toBe(dateValue);
141
151
  expect(screen.queryAllByText('option 1')).toHaveLength(0);
142
152
  expect(screen.queryAllByText('option 2')).toHaveLength(1);
153
+ expect(screen.queryAllByDisplayValue('resource1')).toHaveLength(0);
154
+ expect(screen.queryAllByDisplayValue('resource2')).toHaveLength(1);
155
+ });
156
+ it('fill defaults into fields', async () => {
157
+ render(
158
+ <MockedProvider mocks={gqlMock} addTypename={false}>
159
+ <Provider store={store}>
160
+ <JobWizard />
161
+ </Provider>
162
+ </MockedProvider>
163
+ );
164
+ await act(async () => {
165
+ fireEvent.click(screen.getByText('Advanced Fields'));
166
+ });
167
+
168
+ expect(
169
+ screen.getByLabelText('effective user', {
170
+ selector: 'input',
171
+ }).value
172
+ ).toBe('default effective user');
173
+ expect(
174
+ screen.getByLabelText('timeout to kill', {
175
+ selector: 'input',
176
+ }).value
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');
143
362
  });
144
363
  });
@@ -0,0 +1,82 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`AdvancedFields search resources action: resource search 1`] = `
4
+ Array [
5
+ Object {
6
+ "key": "JOB_CATEGORIES",
7
+ "type": "get",
8
+ "url": "/ui_job_wizard/categories",
9
+ },
10
+ Object {
11
+ "key": "JOB_TEMPLATES",
12
+ "type": "get",
13
+ "url": URI {
14
+ "_deferred_build": true,
15
+ "_parts": Object {
16
+ "duplicateQueryParameters": false,
17
+ "escapeQuerySpace": true,
18
+ "fragment": null,
19
+ "hostname": null,
20
+ "password": null,
21
+ "path": "foreman/api/v2/job_templates",
22
+ "port": null,
23
+ "preventInvalidHostname": false,
24
+ "protocol": null,
25
+ "query": "search=job_category%3D%22Ansible+Commands%22&per_page=all",
26
+ "urn": null,
27
+ "username": null,
28
+ },
29
+ "_string": "",
30
+ },
31
+ },
32
+ Object {
33
+ "key": "JOB_TEMPLATE",
34
+ "type": "get",
35
+ "url": "/ui_job_wizard/template/178",
36
+ },
37
+ Object {
38
+ "key": "ForemanTasksTask",
39
+ "type": "get",
40
+ "url": URI {
41
+ "_deferred_build": true,
42
+ "_parts": Object {
43
+ "duplicateQueryParameters": false,
44
+ "escapeQuerySpace": true,
45
+ "fragment": null,
46
+ "hostname": null,
47
+ "password": null,
48
+ "path": "/ui_job_wizard/resources",
49
+ "port": null,
50
+ "preventInvalidHostname": false,
51
+ "protocol": null,
52
+ "query": "resource=ForemanTasks%3A%3ATask&name=",
53
+ "urn": null,
54
+ "username": null,
55
+ },
56
+ "_string": "",
57
+ },
58
+ },
59
+ Object {
60
+ "key": "ForemanTasksTask",
61
+ "type": "get",
62
+ "url": URI {
63
+ "_deferred_build": true,
64
+ "_parts": Object {
65
+ "duplicateQueryParameters": false,
66
+ "escapeQuerySpace": true,
67
+ "fragment": null,
68
+ "hostname": null,
69
+ "password": null,
70
+ "path": "/ui_job_wizard/resources",
71
+ "port": null,
72
+ "preventInvalidHostname": false,
73
+ "protocol": null,
74
+ "query": "resource=ForemanTasks%3A%3ATask&name=some+search",
75
+ "urn": null,
76
+ "username": null,
77
+ },
78
+ "_string": "",
79
+ },
80
+ },
81
+ ]
82
+ `;
@@ -1,9 +1,11 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { Title, Text, TextVariants, Form, Alert } from '@patternfly/react-core';
3
+ import { Text, TextVariants, Form, Alert } from '@patternfly/react-core';
4
4
  import { translate as __ } from 'foremanReact/common/I18n';
5
5
  import { SelectField } from '../form/SelectField';
6
6
  import { GroupedSelectField } from '../form/GroupedSelectField';
7
+ import { WizardTitle } from '../form/WizardTitle';
8
+ import { WIZARD_TITLES } from '../../JobWizardConstants';
7
9
 
8
10
  export const CategoryAndTemplate = ({
9
11
  jobCategories,
@@ -40,7 +42,7 @@ export const CategoryAndTemplate = ({
40
42
  const isError = !!(categoryError || allTemplatesError || templateError);
41
43
  return (
42
44
  <>
43
- <Title headingLevel="h2">{__('Category and Template')}</Title>
45
+ <WizardTitle title={WIZARD_TITLES.categoryAndTemplate} />
44
46
  <Text component={TextVariants.p}>{__('All fields are required.')}</Text>
45
47
  <Form>
46
48
  <SelectField
@@ -51,6 +53,7 @@ export const CategoryAndTemplate = ({
51
53
  value={selectedCategory}
52
54
  placeholderText={categoryError ? __('Error') : ''}
53
55
  isDisabled={!!categoryError}
56
+ isRequired
54
57
  />
55
58
  <GroupedSelectField
56
59
  label={__('Job template')}