foreman_remote_execution 4.7.0 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +1 -0
- data/app/controllers/api/v2/job_invocations_controller.rb +7 -1
- data/app/lib/actions/remote_execution/run_host_job.rb +2 -1
- data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
- data/app/mailers/rex_job_mailer.rb +15 -0
- data/app/models/job_invocation.rb +4 -0
- data/app/models/job_invocation_composer.rb +20 -12
- data/app/models/remote_execution_provider.rb +18 -2
- data/app/models/rex_mail_notification.rb +13 -0
- data/app/models/setting/remote_execution.rb +7 -1
- data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
- data/app/views/dashboard/_latest-jobs.html.erb +21 -0
- data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
- data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
- data/app/views/template_invocations/show.html.erb +2 -1
- data/db/seeds.d/50-notification_blueprints.rb +14 -0
- data/db/seeds.d/95-mail_notifications.rb +24 -0
- data/foreman_remote_execution.gemspec +1 -1
- data/lib/foreman_remote_execution/engine.rb +1 -0
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/package.json +6 -6
- data/test/functional/api/v2/job_invocations_controller_test.rb +10 -0
- data/test/unit/actions/run_hosts_job_test.rb +99 -4
- data/test/unit/job_invocation_report_template_test.rb +15 -12
- data/test/unit/remote_execution_provider_test.rb +46 -0
- data/webpack/JobWizard/JobWizard.js +53 -20
- data/webpack/JobWizard/JobWizard.scss +33 -4
- data/webpack/JobWizard/JobWizardConstants.js +17 -0
- data/webpack/JobWizard/__tests__/fixtures.js +8 -0
- data/webpack/JobWizard/__tests__/integration.test.js +3 -7
- data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +16 -5
- data/webpack/JobWizard/steps/AdvancedFields/Fields.js +48 -1
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +29 -14
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +4 -2
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +3 -2
- data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +25 -0
- data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js +37 -0
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +50 -0
- data/webpack/JobWizard/steps/HostsAndInputs/index.js +66 -0
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
- data/webpack/JobWizard/steps/Schedule/StartEndDates.js +36 -21
- data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +155 -0
- data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +9 -8
- data/webpack/JobWizard/steps/Schedule/index.js +89 -28
- data/webpack/JobWizard/steps/form/DateTimePicker.js +93 -0
- data/webpack/JobWizard/steps/form/Formatter.js +10 -9
- data/webpack/JobWizard/steps/form/NumberInput.js +2 -0
- data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +2 -1
- metadata +18 -4
@@ -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,13 +49,34 @@
|
|
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
|
-
input[type='radio'],
|
46
|
-
input[type='checkbox'] {
|
47
|
-
margin: 0;
|
48
|
-
}
|
49
60
|
.advanced-scheduling-button {
|
50
61
|
text-align: start;
|
51
62
|
}
|
52
63
|
}
|
64
|
+
|
65
|
+
.pf-c-date-picker {
|
66
|
+
vertical-align: top;
|
67
|
+
}
|
68
|
+
|
69
|
+
.time-picker {
|
70
|
+
width: 150px;
|
71
|
+
}
|
72
|
+
|
73
|
+
input[type='radio'],
|
74
|
+
input[type='checkbox'] {
|
75
|
+
// overwriting bootstrap/_forms.scss margin: 4px 0 0;
|
76
|
+
margin: 0;
|
77
|
+
}
|
78
|
+
textarea {
|
79
|
+
min-height: 40px;
|
80
|
+
min-width: 100px;
|
81
|
+
}
|
53
82
|
}
|
@@ -14,3 +14,20 @@ 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
|
+
};
|
25
|
+
|
26
|
+
export const initialScheduleState = {
|
27
|
+
repeatType: repeatTypes.noRepeat,
|
28
|
+
repeatAmount: '',
|
29
|
+
starts: '',
|
30
|
+
ends: '',
|
31
|
+
isFuture: false,
|
32
|
+
isNeverEnds: false,
|
33
|
+
};
|
@@ -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
|
66
|
-
|
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 {
|
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
|
-
<
|
30
|
-
{
|
31
|
-
|
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
|
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(
|
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(
|
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(
|
123
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.categoryAndTemplate));
|
132
124
|
});
|
133
|
-
expect(screen.getAllByText(
|
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 {
|
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
|
-
<
|
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(
|
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(
|
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
|
+
};
|