foreman_remote_execution 4.5.5 → 4.5.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 99202dff05c36d40eac36950a23298ca1cf2ca4510e2506787bace85c72f0fd9
4
- data.tar.gz: '059cd0dd0b427bb2fc1a80c59a47ec26e2fedb4d0e27f989c1ec859e52e1191a'
3
+ metadata.gz: 0f14506c337ce696f794a63b0c30b59f6d301edc4d55b9d654e2b5360215beb9
4
+ data.tar.gz: 0b9d86b4a422b84d507dfd977dea5dd9f2dc8b30e464b3e4383c8ea421e0615b
5
5
  SHA512:
6
- metadata.gz: 19e978232f3ee68a30d65ceb7c1b799b180894c16f5060c80ed4c5b85870fbe1f0f7ed320b44924cbb7177e280deb32309a061ab2737ff9bc2cc944092a48b63
7
- data.tar.gz: 9ac86095eb5c6cdf2bed666e911895462e44b0cbf96ddf40023c062604f0de947ebf4851e48d556332ed6624a59d6748608bdb472ce63c3e593ebcc3e5a21d52
6
+ metadata.gz: 250ec14568078576a42664e40e2d96b974282765e6cfcffca8ec58bdff9e72c8948f2e28e7ba5fdafe36fb45457e2f38c03bd3c39842ba0bebaa303970ea5f09
7
+ data.tar.gz: 4f6469e5ad8535b7996d1a8c78d266edbba4c03e1e3eb9b52e0588c653351e153b87b4d6daaf082e039d7af47207fdf5e3e6267920cde4d2a233c4e11ac5ca57
@@ -636,7 +636,7 @@ class JobInvocationComposer
636
636
  setting_value = Setting['remote_execution_form_job_template']
637
637
  return default_value unless setting_value
638
638
 
639
- form_template = JobTemplate.find_by :name => setting_value
639
+ form_template = JobTemplate.authorized(:view_job_templates).find_by :name => setting_value
640
640
  return default_value unless form_template
641
641
 
642
642
  if block_given?
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '4.5.5'.freeze
2
+ VERSION = '4.5.6'.freeze
3
3
  end
@@ -3,43 +3,59 @@ import React, { useState, useEffect, useCallback } from 'react';
3
3
  import { useDispatch, useSelector } from 'react-redux';
4
4
  import { Wizard } from '@patternfly/react-core';
5
5
  import { get } from 'foremanReact/redux/API';
6
- import { translate as __ } from 'foremanReact/common/I18n';
7
6
  import history from 'foremanReact/history';
8
7
  import CategoryAndTemplate from './steps/CategoryAndTemplate/';
9
8
  import { AdvancedFields } from './steps/AdvancedFields/AdvancedFields';
10
- import { JOB_TEMPLATE } from './JobWizardConstants';
9
+ import { JOB_TEMPLATE, WIZARD_TITLES } from './JobWizardConstants';
11
10
  import { selectTemplateError } from './JobWizardSelectors';
12
11
  import Schedule from './steps/Schedule/';
12
+ import HostsAndInputs from './steps/HostsAndInputs/';
13
13
  import './JobWizard.scss';
14
14
 
15
15
  export const JobWizard = () => {
16
16
  const [jobTemplateID, setJobTemplateID] = useState(null);
17
17
  const [category, setCategory] = useState('');
18
18
  const [advancedValues, setAdvancedValues] = useState({});
19
+ const [templateValues, setTemplateValues] = useState({}); // TODO use templateValues in advanced fields - description https://github.com/theforeman/foreman_remote_execution/pull/605
20
+ const [selectedHosts, setSelectedHosts] = useState(['host1', 'host2']);
19
21
  const dispatch = useDispatch();
20
22
 
21
23
  const setDefaults = useCallback(
22
24
  ({
23
25
  data: {
26
+ template_inputs,
24
27
  advanced_template_inputs,
25
28
  effective_user,
26
- job_template: { executionTimeoutInterval, description_format },
29
+ job_template: { execution_timeout_interval, description_format },
27
30
  },
28
31
  }) => {
29
32
  const advancedTemplateValues = {};
33
+ const defaultTemplateValues = {};
34
+ const inputs = template_inputs;
30
35
  const advancedInputs = advanced_template_inputs;
31
- if (advancedInputs) {
32
- advancedInputs.forEach(input => {
33
- advancedTemplateValues[input.name] = input?.default || '';
36
+ if (inputs) {
37
+ setTemplateValues(() => {
38
+ inputs.forEach(input => {
39
+ defaultTemplateValues[input.name] = input?.default || '';
40
+ });
41
+ return defaultTemplateValues;
34
42
  });
35
43
  }
36
- setAdvancedValues(currentAdvancedValues => ({
37
- ...currentAdvancedValues,
38
- effectiveUserValue: effective_user?.value || '',
39
- timeoutToKill: executionTimeoutInterval || '',
40
- templateValues: advancedTemplateValues,
41
- description: description_format || '',
42
- }));
44
+ setAdvancedValues(currentAdvancedValues => {
45
+ if (advancedInputs) {
46
+ advancedInputs.forEach(input => {
47
+ advancedTemplateValues[input.name] = input?.default || '';
48
+ });
49
+ }
50
+ return {
51
+ ...currentAdvancedValues,
52
+ effectiveUserValue: effective_user?.value || '',
53
+ timeoutToKill: execution_timeout_interval || '',
54
+ templateValues: advancedTemplateValues,
55
+ description: description_format || '',
56
+ isRandomizedOrdering: false,
57
+ };
58
+ });
43
59
  },
44
60
  []
45
61
  );
@@ -59,7 +75,7 @@ export const JobWizard = () => {
59
75
  const isTemplate = !templateError && !!jobTemplateID;
60
76
  const steps = [
61
77
  {
62
- name: __('Category and Template'),
78
+ name: WIZARD_TITLES.categoryAndTemplate,
63
79
  component: (
64
80
  <CategoryAndTemplate
65
81
  jobTemplate={jobTemplateID}
@@ -70,12 +86,19 @@ export const JobWizard = () => {
70
86
  ),
71
87
  },
72
88
  {
73
- name: __('Target Hosts'),
74
- component: <p>Target Hosts</p>,
89
+ name: WIZARD_TITLES.hostsAndInputs,
90
+ component: (
91
+ <HostsAndInputs
92
+ templateValues={templateValues}
93
+ setTemplateValues={setTemplateValues}
94
+ selectedHosts={selectedHosts}
95
+ setSelectedHosts={setSelectedHosts}
96
+ />
97
+ ),
75
98
  canJumpTo: isTemplate,
76
99
  },
77
100
  {
78
- name: __('Advanced Fields'),
101
+ name: WIZARD_TITLES.advanced,
79
102
  component: (
80
103
  <AdvancedFields
81
104
  advancedValues={advancedValues}
@@ -91,12 +114,12 @@ export const JobWizard = () => {
91
114
  canJumpTo: isTemplate,
92
115
  },
93
116
  {
94
- name: __('Schedule'),
117
+ name: WIZARD_TITLES.schedule,
95
118
  component: <Schedule />,
96
119
  canJumpTo: isTemplate,
97
120
  },
98
121
  {
99
- name: __('Review Details'),
122
+ name: WIZARD_TITLES.review,
100
123
  component: <p>Review Details</p>,
101
124
  nextButtonText: 'Run',
102
125
  canJumpTo: isTemplate,
@@ -1,5 +1,10 @@
1
1
  .job-wizard {
2
+ .wizard-title {
3
+ margin-bottom: 25px;
4
+ }
5
+
2
6
  .pf-c-wizard__main {
7
+ overflow: visible;
3
8
  z-index: calc(
4
9
  var(--pf-c-wizard__footer--ZIndex) + 1
5
10
  ); // So the select box can be shown above the wizard footer
@@ -12,6 +17,9 @@
12
17
  }
13
18
  #advanced-fields-job-template {
14
19
  .foreman-search-field {
20
+ .rbt-input-hint input{
21
+ display: none;
22
+ }
15
23
  // Giving pf3 search bar a pf4 look
16
24
  .search-bar {
17
25
  display: block;
@@ -41,6 +49,13 @@
41
49
  }
42
50
  }
43
51
 
52
+ .hosts-chip-group {
53
+ margin-top: 8px;
54
+ }
55
+ input[type='radio'],
56
+ input[type='checkbox'] {
57
+ margin: 0;
58
+ }
44
59
  .schedule-tab {
45
60
  input[type='radio'],
46
61
  input[type='checkbox'] {
@@ -50,4 +65,8 @@
50
65
  text-align: start;
51
66
  }
52
67
  }
68
+ textarea {
69
+ min-height: 40px;
70
+ min-width: 100px;
71
+ }
53
72
  }
@@ -14,3 +14,11 @@ export const repeatTypes = {
14
14
  daily: __('Daily'),
15
15
  hourly: __('Hourly'),
16
16
  };
17
+
18
+ export const WIZARD_TITLES = {
19
+ categoryAndTemplate: __('Category and Template'),
20
+ hostsAndInputs: __('Target hosts and inputs'),
21
+ advanced: __('Advanced Fields'),
22
+ schedule: __('Schedule'),
23
+ review: __('Review Details'),
24
+ };
@@ -93,6 +93,14 @@ export const testSetup = (selectors, api) => {
93
93
  jest.spyOn(selectors, 'selectJobCategories');
94
94
  jest.spyOn(selectors, 'selectJobCategoriesStatus');
95
95
 
96
+ jest.spyOn(selectors, 'selectTemplateInputs');
97
+ jest.spyOn(selectors, 'selectAdvancedTemplateInputs');
98
+ selectors.selectTemplateInputs.mockImplementation(
99
+ () => jobTemplateResponse.template_inputs
100
+ );
101
+ selectors.selectAdvancedTemplateInputs.mockImplementation(
102
+ () => jobTemplateResponse.advanced_template_inputs
103
+ );
96
104
  selectors.selectJobCategories.mockImplementation(() => jobCategories);
97
105
  selectors.selectJobTemplates.mockImplementation(() => [
98
106
  jobTemplate,
@@ -5,6 +5,7 @@ import { render, fireEvent, screen, act } from '@testing-library/react';
5
5
  import * as api from 'foremanReact/redux/API';
6
6
  import { JobWizard } from '../JobWizard';
7
7
  import * as selectors from '../JobWizardSelectors';
8
+ import { WIZARD_TITLES } from '../JobWizardConstants';
8
9
  import {
9
10
  testSetup,
10
11
  mockApi,
@@ -62,13 +63,8 @@ describe('Job wizard fill', () => {
62
63
  <JobWizard />
63
64
  </Provider>
64
65
  );
65
- const steps = [
66
- 'Target Hosts',
67
- 'Advanced Fields',
68
- 'Schedule',
69
- 'Review Details',
70
- 'Category and Template',
71
- ];
66
+ const titles = Object.values(WIZARD_TITLES);
67
+ const steps = [titles[1], titles[0], ...titles.slice(2)]; // the first title is selected at the beggining
72
68
  // eslint-disable-next-line no-unused-vars
73
69
  for await (const step of steps) {
74
70
  const stepSelector = screen.getByText(step);
@@ -1,8 +1,7 @@
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,
@@ -17,8 +16,11 @@ import {
17
16
  ConcurrencyLevelField,
18
17
  TimeSpanLevelField,
19
18
  TemplateInputsFields,
19
+ ExecutionOrderingField,
20
20
  } from './Fields';
21
21
  import { DescriptionField } from './DescriptionField';
22
+ import { WIZARD_TITLES } from '../../JobWizardConstants';
23
+ import { WizardTitle } from '../form/WizardTitle';
22
24
 
23
25
  export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
24
26
  const effectiveUser = useSelector(selectEffectiveUser);
@@ -26,9 +28,10 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
26
28
  const templateInputs = useSelector(selectTemplateInputs);
27
29
  return (
28
30
  <>
29
- <Title headingLevel="h2" className="advanced-fields-title">
30
- {__('Advanced Fields')}
31
- </Title>
31
+ <WizardTitle
32
+ title={WIZARD_TITLES.advanced}
33
+ className="advanced-fields-title"
34
+ />
32
35
  <Form id="advanced-fields-job-template" autoComplete="off">
33
36
  <TemplateInputsFields
34
37
  inputs={advancedTemplateInputs}
@@ -98,6 +101,14 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
98
101
  })
99
102
  }
100
103
  />
104
+ <ExecutionOrderingField
105
+ isRandomizedOrdering={advancedValues.isRandomizedOrdering}
106
+ setValue={newValue =>
107
+ setAdvancedValues({
108
+ isRandomizedOrdering: newValue,
109
+ })
110
+ }
111
+ />
101
112
  </Form>
102
113
  </>
103
114
  );
@@ -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,
@@ -10,24 +10,16 @@ import {
10
10
  testSetup,
11
11
  mockApi,
12
12
  } from '../../../__tests__/fixtures';
13
+ import { WIZARD_TITLES } from '../../../JobWizardConstants';
13
14
 
14
15
  const store = testSetup(selectors, api);
15
16
  mockApi(api);
16
17
 
17
18
  jest.spyOn(selectors, 'selectEffectiveUser');
18
- jest.spyOn(selectors, 'selectTemplateInputs');
19
- jest.spyOn(selectors, 'selectAdvancedTemplateInputs');
20
19
 
21
20
  selectors.selectEffectiveUser.mockImplementation(
22
21
  () => jobTemplate.effective_user
23
22
  );
24
- selectors.selectTemplateInputs.mockImplementation(
25
- () => jobTemplate.template_inputs
26
- );
27
-
28
- selectors.selectAdvancedTemplateInputs.mockImplementation(
29
- () => jobTemplate.advanced_template_inputs
30
- );
31
23
  describe('AdvancedFields', () => {
32
24
  it('should save data between steps for advanced fields', async () => {
33
25
  const wrapper = mount(
@@ -72,7 +64,7 @@ describe('AdvancedFields', () => {
72
64
  .simulate('click');
73
65
 
74
66
  expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-current').text()).toEqual(
75
- 'Target Hosts'
67
+ 'Target hosts and inputs'
76
68
  );
77
69
  wrapper
78
70
  .find('.pf-c-wizard__nav-link')
@@ -91,7 +83,7 @@ describe('AdvancedFields', () => {
91
83
  </Provider>
92
84
  );
93
85
  await act(async () => {
94
- fireEvent.click(screen.getByText('Advanced Fields'));
86
+ fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
95
87
  });
96
88
  const searchValue = 'search test';
97
89
  const textValue = 'I am a text';
@@ -108,7 +100,7 @@ describe('AdvancedFields', () => {
108
100
  fireEvent.click(selectField);
109
101
  await act(async () => {
110
102
  await fireEvent.click(screen.getByText('option 2'));
111
- fireEvent.click(screen.getAllByText('Advanced Fields')[0]); // to remove focus
103
+ fireEvent.click(screen.getAllByText(WIZARD_TITLES.advanced)[0]); // to remove focus
112
104
  await fireEvent.change(textField, {
113
105
  target: { value: textValue },
114
106
  });
@@ -128,9 +120,11 @@ describe('AdvancedFields', () => {
128
120
  expect(searchField.value).toBe(searchValue);
129
121
  expect(dateField.value).toBe(dateValue);
130
122
  await act(async () => {
131
- fireEvent.click(screen.getByText('Category and Template'));
123
+ fireEvent.click(screen.getByText(WIZARD_TITLES.categoryAndTemplate));
132
124
  });
133
- expect(screen.getAllByText('Category and Template')).toHaveLength(3);
125
+ expect(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)).toHaveLength(
126
+ 3
127
+ );
134
128
 
135
129
  await act(async () => {
136
130
  fireEvent.click(screen.getByText('Advanced Fields'));
@@ -141,4 +135,25 @@ describe('AdvancedFields', () => {
141
135
  expect(screen.queryAllByText('option 1')).toHaveLength(0);
142
136
  expect(screen.queryAllByText('option 2')).toHaveLength(1);
143
137
  });
138
+ it('fill defaults into fields', async () => {
139
+ render(
140
+ <Provider store={store}>
141
+ <JobWizard />
142
+ </Provider>
143
+ );
144
+ await act(async () => {
145
+ fireEvent.click(screen.getByText('Advanced Fields'));
146
+ });
147
+
148
+ expect(
149
+ screen.getByLabelText('effective user', {
150
+ selector: 'input',
151
+ }).value
152
+ ).toBe('default effective user');
153
+ expect(
154
+ screen.getByLabelText('timeout to kill', {
155
+ selector: 'input',
156
+ }).value
157
+ ).toBe('2');
158
+ });
144
159
  });
@@ -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
@@ -5,6 +5,7 @@ import * as api from 'foremanReact/redux/API';
5
5
  import { JobWizard } from '../../JobWizard';
6
6
  import * as selectors from '../../JobWizardSelectors';
7
7
  import { testSetup, mockApi } from '../../__tests__/fixtures';
8
+ import { WIZARD_TITLES } from '../../JobWizardConstants';
8
9
 
9
10
  const store = testSetup(selectors, api);
10
11
  mockApi(api);
@@ -32,7 +33,7 @@ describe('Category And Template', () => {
32
33
  await act(async () => {
33
34
  await fireEvent.click(screen.getByText('Puppet'));
34
35
  });
35
- fireEvent.click(screen.getAllByText('Category and Template')[0]); // to remove focus
36
+ fireEvent.click(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)[0]); // to remove focus
36
37
  expect(
37
38
  screen.queryAllByLabelText('Ansible Commands', { selector: 'button' })
38
39
  ).toHaveLength(0);
@@ -47,7 +48,7 @@ describe('Category And Template', () => {
47
48
  await act(async () => {
48
49
  await fireEvent.click(screen.getByText('template2'));
49
50
  });
50
- fireEvent.click(screen.getAllByText('Category and Template')[0]); // to remove focus
51
+ fireEvent.click(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)[0]); // to remove focus
51
52
  expect(
52
53
  screen.queryAllByDisplayValue('template1', { selector: 'button' })
53
54
  ).toHaveLength(0);
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Chip, ChipGroup } from '@patternfly/react-core';
4
+
5
+ export const SelectedChips = ({ selected, setSelected }) => {
6
+ const deleteItem = itemToRemove => {
7
+ setSelected(oldSelected =>
8
+ oldSelected.filter(item => item !== itemToRemove)
9
+ );
10
+ };
11
+ return (
12
+ <ChipGroup className="hosts-chip-group">
13
+ {selected.map(chip => (
14
+ <Chip key={chip} id={chip} onClick={() => deleteItem(chip)}>
15
+ {chip}
16
+ </Chip>
17
+ ))}
18
+ </ChipGroup>
19
+ );
20
+ };
21
+
22
+ SelectedChips.propTypes = {
23
+ selected: PropTypes.array.isRequired,
24
+ setSelected: PropTypes.func.isRequired,
25
+ };
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+ import { formatter } from '../form/Formatter';
5
+
6
+ export const TemplateInputs = ({ inputs, value, setValue }) => {
7
+ if (inputs.length)
8
+ return inputs.map(input => formatter(input, value, setValue));
9
+ return (
10
+ <p className="gray-text">
11
+ {__('There are no available input fields for the selected template.')}
12
+ </p>
13
+ );
14
+ };
15
+ TemplateInputs.propTypes = {
16
+ inputs: PropTypes.array.isRequired,
17
+ value: PropTypes.object,
18
+ setValue: PropTypes.func.isRequired,
19
+ };
20
+
21
+ TemplateInputs.defaultProps = {
22
+ value: {},
23
+ };
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import { fireEvent, screen, render, act } from '@testing-library/react';
4
+ import * as api from 'foremanReact/redux/API';
5
+ import { JobWizard } from '../../../JobWizard';
6
+ import * as selectors from '../../../JobWizardSelectors';
7
+ import { testSetup, mockApi } from '../../../__tests__/fixtures';
8
+
9
+ const store = testSetup(selectors, api);
10
+ mockApi(api);
11
+
12
+ describe('TemplateInputs', () => {
13
+ it('should save data between steps for template input fields', async () => {
14
+ render(
15
+ <Provider store={store}>
16
+ <JobWizard advancedValues={{}} setAdvancedValues={jest.fn()} />
17
+ </Provider>
18
+ );
19
+ await act(async () => {
20
+ await fireEvent.click(
21
+ screen.getByText('Target hosts and inputs', { selector: 'button' })
22
+ );
23
+ });
24
+
25
+ expect(
26
+ screen.getAllByLabelText('host2', { selector: 'button' })
27
+ ).toHaveLength(1);
28
+ const chip1 = screen.getByLabelText('host1', { selector: 'button' });
29
+ fireEvent.click(chip1);
30
+ expect(
31
+ screen.queryAllByLabelText('host1', { selector: 'button' })
32
+ ).toHaveLength(0);
33
+ expect(
34
+ screen.queryAllByLabelText('host2', { selector: 'button' })
35
+ ).toHaveLength(1);
36
+ });
37
+ });
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import { fireEvent, screen, render, act } from '@testing-library/react';
4
+ import * as api from 'foremanReact/redux/API';
5
+ import { JobWizard } from '../../../JobWizard';
6
+ import * as selectors from '../../../JobWizardSelectors';
7
+ import { testSetup, mockApi } from '../../../__tests__/fixtures';
8
+ import { WIZARD_TITLES } from '../../../JobWizardConstants';
9
+
10
+ const store = testSetup(selectors, api);
11
+ mockApi(api);
12
+
13
+ describe('TemplateInputs', () => {
14
+ it('should save data between steps for template input fields', async () => {
15
+ render(
16
+ <Provider store={store}>
17
+ <JobWizard />
18
+ </Provider>
19
+ );
20
+ await act(async () => {
21
+ fireEvent.click(screen.getByText(WIZARD_TITLES.hostsAndInputs));
22
+ });
23
+ const textValue = 'I am a plain text';
24
+ const textField = screen.getByLabelText('plain hidden', {
25
+ selector: 'textarea',
26
+ });
27
+
28
+ await act(async () => {
29
+ await fireEvent.change(textField, {
30
+ target: { value: textValue },
31
+ });
32
+ });
33
+ expect(
34
+ screen.getByLabelText('plain hidden', {
35
+ selector: 'textarea',
36
+ }).value
37
+ ).toBe(textValue);
38
+ await act(async () => {
39
+ fireEvent.click(screen.getByText(WIZARD_TITLES.categoryAndTemplate));
40
+ });
41
+ expect(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)).toHaveLength(
42
+ 3
43
+ );
44
+
45
+ await act(async () => {
46
+ fireEvent.click(screen.getByText(WIZARD_TITLES.hostsAndInputs));
47
+ });
48
+ expect(textField.value).toBe(textValue);
49
+ });
50
+ });
@@ -0,0 +1,66 @@
1
+ import React, { useState } from 'react';
2
+ import { Button, Form, FormGroup } from '@patternfly/react-core';
3
+ import PropTypes from 'prop-types';
4
+ import { useSelector } from 'react-redux';
5
+ import { translate as __ } from 'foremanReact/common/I18n';
6
+ import { selectTemplateInputs } from '../../JobWizardSelectors';
7
+ import { SelectField } from '../form/SelectField';
8
+ import { SelectedChips } from './SelectedChips';
9
+ import { TemplateInputs } from './TemplateInputs';
10
+ import { WIZARD_TITLES } from '../../JobWizardConstants';
11
+ import { WizardTitle } from '../form/WizardTitle';
12
+
13
+ const HostsAndInputs = ({
14
+ templateValues,
15
+ setTemplateValues,
16
+ selectedHosts,
17
+ setSelectedHosts,
18
+ }) => {
19
+ const templateInputs = useSelector(selectTemplateInputs);
20
+ const hostMethods = [
21
+ __('Hosts'),
22
+ __('Host collection'),
23
+ __('Host group'),
24
+ __('Search query'),
25
+ ];
26
+ const [hostMethod, setHostMethod] = useState(hostMethods[0]);
27
+ return (
28
+ <>
29
+ <WizardTitle title={WIZARD_TITLES.hostsAndInputs} />
30
+ <Form>
31
+ <FormGroup fieldId="host_selection">
32
+ <SelectField
33
+ fieldId="host_methods"
34
+ options={hostMethods}
35
+ setValue={setHostMethod}
36
+ value={hostMethod}
37
+ />
38
+ <SelectedChips
39
+ selected={selectedHosts}
40
+ setSelected={setSelectedHosts}
41
+ />
42
+ </FormGroup>
43
+ <span>
44
+ {__('Apply to')}{' '}
45
+ <Button variant="link" isInline>
46
+ {selectedHosts.length} {__('hosts')}
47
+ </Button>
48
+ </span>
49
+ <TemplateInputs
50
+ inputs={templateInputs}
51
+ value={templateValues}
52
+ setValue={setTemplateValues}
53
+ />
54
+ </Form>
55
+ </>
56
+ );
57
+ };
58
+
59
+ HostsAndInputs.propTypes = {
60
+ templateValues: PropTypes.object.isRequired,
61
+ setTemplateValues: PropTypes.func.isRequired,
62
+ selectedHosts: PropTypes.array.isRequired,
63
+ setSelectedHosts: PropTypes.func.isRequired,
64
+ };
65
+
66
+ export default HostsAndInputs;
@@ -1,11 +1,12 @@
1
1
  import React, { useState } from 'react';
2
- import { Title, Button, Form } from '@patternfly/react-core';
2
+ import { Button, Form } from '@patternfly/react-core';
3
3
  import { translate as __ } from 'foremanReact/common/I18n';
4
4
  import { ScheduleType } from './ScheduleType';
5
5
  import { RepeatOn } from './RepeatOn';
6
6
  import { QueryType } from './QueryType';
7
7
  import { StartEndDates } from './StartEndDates';
8
- import { repeatTypes } from '../../JobWizardConstants';
8
+ import { repeatTypes, WIZARD_TITLES } from '../../JobWizardConstants';
9
+ import { WizardTitle } from '../form/WizardTitle';
9
10
 
10
11
  const Schedule = () => {
11
12
  const [repeatType, setRepeatType] = useState(repeatTypes.noRepeat);
@@ -14,27 +15,29 @@ const Schedule = () => {
14
15
  const [ends, setEnds] = useState('');
15
16
 
16
17
  return (
17
- <Form className="schedule-tab">
18
- <Title headingLevel="h2">{__('Schedule')}</Title>
19
- <ScheduleType />
18
+ <>
19
+ <WizardTitle title={WIZARD_TITLES.schedule} />
20
+ <Form className="schedule-tab">
21
+ <ScheduleType />
20
22
 
21
- <RepeatOn
22
- repeatType={repeatType}
23
- setRepeatType={setRepeatType}
24
- repeatAmount={repeatAmount}
25
- setRepeatAmount={setRepeatAmount}
26
- />
27
- <StartEndDates
28
- starts={starts}
29
- setStarts={setStarts}
30
- ends={ends}
31
- setEnds={setEnds}
32
- />
33
- <Button variant="link" className="advanced-scheduling-button" isInline>
34
- {__('Advanced scheduling')}
35
- </Button>
36
- <QueryType />
37
- </Form>
23
+ <RepeatOn
24
+ repeatType={repeatType}
25
+ setRepeatType={setRepeatType}
26
+ repeatAmount={repeatAmount}
27
+ setRepeatAmount={setRepeatAmount}
28
+ />
29
+ <StartEndDates
30
+ starts={starts}
31
+ setStarts={setStarts}
32
+ ends={ends}
33
+ setEnds={setEnds}
34
+ />
35
+ <Button variant="link" className="advanced-scheduling-button" isInline>
36
+ {__('Advanced scheduling')}
37
+ </Button>
38
+ <QueryType />
39
+ </Form>
40
+ </>
38
41
  );
39
42
  };
40
43
 
@@ -22,11 +22,12 @@ const TemplateSearchField = ({
22
22
  setValue({ ...values, [name]: searchQuery });
23
23
  // eslint-disable-next-line react-hooks/exhaustive-deps
24
24
  }, [searchQuery]);
25
+ const id = name.replace(/ /g, '-');
25
26
  return (
26
27
  <FormGroup
27
28
  label={name}
28
29
  labelIcon={helpLabel(labelText, name)}
29
- fieldId={name}
30
+ fieldId={id}
30
31
  isRequired={required}
31
32
  className="foreman-search-field"
32
33
  >
@@ -54,16 +55,16 @@ export const formatter = (input, values, setValue) => {
54
55
  const { name, required, hidden_value: hidden } = input;
55
56
  const labelText = input.description;
56
57
  const value = values[name];
57
-
58
+ const id = name.replace(/ /g, '-');
58
59
  if (isSelectType) {
59
60
  const options = input.options.split(/\r?\n/).map(option => option.trim());
60
61
  return (
61
62
  <SelectField
62
63
  aria-label={name}
63
- key={name}
64
+ key={id}
64
65
  isRequired={required}
65
66
  label={name}
66
- fieldId={name}
67
+ fieldId={id}
67
68
  options={options}
68
69
  labelIcon={helpLabel(labelText, name)}
69
70
  value={value}
@@ -77,7 +78,7 @@ export const formatter = (input, values, setValue) => {
77
78
  key={name}
78
79
  label={name}
79
80
  labelIcon={helpLabel(labelText, name)}
80
- fieldId={name}
81
+ fieldId={id}
81
82
  isRequired={required}
82
83
  >
83
84
  <TextArea
@@ -85,7 +86,7 @@ export const formatter = (input, values, setValue) => {
85
86
  className={hidden ? 'masked-input' : null}
86
87
  required={required}
87
88
  rows={2}
88
- id={name}
89
+ id={id}
89
90
  value={value}
90
91
  onChange={newValue => setValue({ ...values, [name]: newValue })}
91
92
  />
@@ -98,7 +99,7 @@ export const formatter = (input, values, setValue) => {
98
99
  key={name}
99
100
  label={name}
100
101
  labelIcon={helpLabel(labelText, name)}
101
- fieldId={name}
102
+ fieldId={id}
102
103
  isRequired={required}
103
104
  >
104
105
  <TextInput
@@ -106,7 +107,7 @@ export const formatter = (input, values, setValue) => {
106
107
  placeholder="YYYY-mm-dd HH:MM"
107
108
  className={hidden ? 'masked-input' : null}
108
109
  required={required}
109
- id={name}
110
+ id={id}
110
111
  type="text"
111
112
  value={value}
112
113
  onChange={newValue => setValue({ ...values, [name]: newValue })}
@@ -119,7 +120,7 @@ export const formatter = (input, values, setValue) => {
119
120
  // TODO: get text from redux autocomplete
120
121
  return (
121
122
  <TemplateSearchField
122
- key={name}
123
+ key={id}
123
124
  name={name}
124
125
  defaultValue={value}
125
126
  controller={controller}
@@ -5,6 +5,7 @@ import { translate as __ } from 'foremanReact/common/I18n';
5
5
 
6
6
  export const NumberInput = ({ formProps, inputProps }) => {
7
7
  const [validated, setValidated] = useState();
8
+ const name = inputProps.id.replace(/-/g, ' ');
8
9
  return (
9
10
  <FormGroup
10
11
  {...formProps}
@@ -12,6 +13,7 @@ export const NumberInput = ({ formProps, inputProps }) => {
12
13
  validated={validated}
13
14
  >
14
15
  <TextInput
16
+ aria-label={name}
15
17
  type="text"
16
18
  {...inputProps}
17
19
  onChange={newValue => {
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Title } from '@patternfly/react-core';
4
+
5
+ export const WizardTitle = ({ title, ...props }) => (
6
+ <Title headingLevel="h2" className="wizard-title" {...props}>
7
+ {title}
8
+ </Title>
9
+ );
10
+
11
+ WizardTitle.propTypes = {
12
+ title: PropTypes.string.isRequired,
13
+ };
14
+ export default WizardTitle;
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman_remote_execution
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.5.5
4
+ version: 4.5.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foreman Remote Execution team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-08-17 00:00:00.000000000 Z
11
+ date: 2021-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deface
@@ -416,6 +416,11 @@ files:
416
416
  - webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js
417
417
  - webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js
418
418
  - webpack/JobWizard/steps/CategoryAndTemplate/index.js
419
+ - webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js
420
+ - webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js
421
+ - webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js
422
+ - webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js
423
+ - webpack/JobWizard/steps/HostsAndInputs/index.js
419
424
  - webpack/JobWizard/steps/Schedule/QueryType.js
420
425
  - webpack/JobWizard/steps/Schedule/RepeatOn.js
421
426
  - webpack/JobWizard/steps/Schedule/ScheduleType.js
@@ -427,6 +432,7 @@ files:
427
432
  - webpack/JobWizard/steps/form/GroupedSelectField.js
428
433
  - webpack/JobWizard/steps/form/NumberInput.js
429
434
  - webpack/JobWizard/steps/form/SelectField.js
435
+ - webpack/JobWizard/steps/form/WizardTitle.js
430
436
  - webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example
431
437
  - webpack/Routes/routes.js
432
438
  - webpack/__mocks__/foremanReact/common/I18n.js