foreman_remote_execution 4.5.5 → 4.5.6

Sign up to get free protection for your applications and to get access to all the features.
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