foreman_remote_execution 4.4.0 → 4.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +13 -24
  3. data/app/controllers/job_templates_controller.rb +4 -4
  4. data/app/controllers/ui_job_wizard_controller.rb +12 -0
  5. data/app/helpers/job_invocations_helper.rb +2 -2
  6. data/app/helpers/remote_execution_helper.rb +8 -8
  7. data/app/lib/actions/remote_execution/run_host_job.rb +31 -5
  8. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +5 -5
  9. data/app/models/host_status/execution_status.rb +5 -5
  10. data/app/models/job_invocation.rb +23 -7
  11. data/app/models/job_invocation_composer.rb +59 -17
  12. data/app/models/ssh_execution_provider.rb +4 -4
  13. data/app/overrides/execution_interface.rb +8 -8
  14. data/app/overrides/subnet_proxies.rb +6 -6
  15. data/config/routes.rb +1 -0
  16. data/db/migrate/20180110104432_rename_template_invocation_permission.rb +1 -1
  17. data/db/migrate/20190111153330_remove_remote_execution_without_proxy_setting.rb +4 -4
  18. data/extra/cockpit/foreman-cockpit-session +6 -6
  19. data/lib/foreman_remote_execution/engine.rb +11 -6
  20. data/lib/foreman_remote_execution/version.rb +1 -1
  21. data/package.json +2 -1
  22. data/test/functional/api/v2/job_invocations_controller_test.rb +14 -1
  23. data/test/unit/job_invocation_composer_test.rb +45 -1
  24. data/webpack/JobWizard/JobWizard.js +59 -18
  25. data/webpack/JobWizard/JobWizard.scss +3 -1
  26. data/webpack/JobWizard/JobWizardConstants.js +1 -0
  27. data/webpack/JobWizard/JobWizardSelectors.js +18 -1
  28. data/webpack/JobWizard/__tests__/JobWizard.test.js +4 -11
  29. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -51
  30. data/webpack/JobWizard/__tests__/__snapshots__/integration.test.js.snap +43 -0
  31. data/webpack/JobWizard/__tests__/fixtures.js +26 -0
  32. data/webpack/JobWizard/__tests__/integration.test.js +156 -0
  33. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +93 -0
  34. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +181 -0
  35. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +25 -0
  36. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +249 -0
  37. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +34 -2
  38. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +10 -3
  39. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +50 -1
  40. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +9 -1
  41. data/webpack/JobWizard/steps/form/FormHelpers.js +19 -0
  42. data/webpack/JobWizard/steps/form/GroupedSelectField.js +3 -0
  43. data/webpack/JobWizard/steps/form/SelectField.js +10 -1
  44. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +1 -0
  45. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +1 -0
  46. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +21 -2
  47. data/webpack/global_index.js +5 -3
  48. data/webpack/index.js +3 -0
  49. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +1 -5
  50. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +8 -3
  51. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +7 -2
  52. data/webpack/react_app/extend/{fills.js → fillRecentJobsCard.js} +7 -6
  53. data/webpack/react_app/extend/fillregistrationAdvanced.js +11 -0
  54. data/webpack/react_app/extend/reducers.js +2 -1
  55. metadata +12 -4
  56. data/webpack/fills_index.js +0 -11
@@ -2,19 +2,12 @@ import React from 'react';
2
2
  import * as patternfly from '@patternfly/react-core';
3
3
  import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
4
4
  import JobWizardPage from '../index';
5
- import { JobWizard } from '../JobWizard';
6
5
 
7
6
  jest.spyOn(patternfly, 'Wizard');
8
- patternfly.Wizard.mockImplementation(props => <div>{props}</div>);
7
+ patternfly.Wizard.mockImplementation(props => <div>{props.navAriaLabel}</div>);
8
+
9
9
  const fixtures = {
10
10
  'renders ': {},
11
11
  };
12
- describe('JobWizardPage', () => {
13
- describe('rendering', () =>
14
- testComponentSnapshotsWithFixtures(JobWizardPage, fixtures));
15
- });
16
-
17
- describe('JobWizard', () => {
18
- describe('rendering', () =>
19
- testComponentSnapshotsWithFixtures(JobWizard, fixtures));
20
- });
12
+ describe('JobWizardPage rendering', () =>
13
+ testComponentSnapshotsWithFixtures(JobWizardPage, fixtures));
@@ -1,56 +1,5 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
- exports[`JobWizard rendering renders 1`] = `
4
- <mockConstructor
5
- className="job-wizard"
6
- height="70vh"
7
- navAriaLabel="Run Job steps"
8
- onClose={[Function]}
9
- steps={
10
- Array [
11
- Object {
12
- "component": <ConnectedCategoryAndTemplate
13
- category=""
14
- jobTemplate={null}
15
- setCategory={[Function]}
16
- setJobTemplate={[Function]}
17
- />,
18
- "name": "Category and template",
19
- },
20
- Object {
21
- "canJumpTo": false,
22
- "component": <p>
23
- TargetHosts
24
- </p>,
25
- "name": "Target hosts",
26
- },
27
- Object {
28
- "canJumpTo": false,
29
- "component": <p>
30
- AdvancedFields
31
- </p>,
32
- "name": "Advanced fields",
33
- },
34
- Object {
35
- "canJumpTo": false,
36
- "component": <p>
37
- Schedule
38
- </p>,
39
- "name": "Schedule",
40
- },
41
- Object {
42
- "canJumpTo": false,
43
- "component": <p>
44
- ReviewDetails
45
- </p>,
46
- "name": "Review details",
47
- "nextButtonText": "Run",
48
- },
49
- ]
50
- }
51
- />
52
- `;
53
-
54
3
  exports[`JobWizardPage rendering renders 1`] = `
55
4
  <PageLayout
56
5
  breadcrumbOptions={
@@ -0,0 +1,43 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`Job wizard fill should select template: initial 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
+ ]
33
+ `;
34
+
35
+ exports[`Job wizard fill should select template: select template 1`] = `
36
+ Array [
37
+ Object {
38
+ "key": "JOB_TEMPLATE",
39
+ "type": "get",
40
+ "url": "/ui_job_wizard/template/178",
41
+ },
42
+ ]
43
+ `;
@@ -0,0 +1,26 @@
1
+ const jobTemplate = {
2
+ id: 178,
3
+ name: 'Run Command - Ansible Default',
4
+ template:
5
+ "---\n- hosts: all\n tasks:\n - shell:\n cmd: |\n<%= indent(10) { input('command') } %>\n register: out\n - debug: var=out",
6
+ snippet: false,
7
+ default: true,
8
+ job_category: 'Ansible Commands',
9
+ provider_type: 'Ansible',
10
+ description_format: 'Run %{command}',
11
+ execution_timeout_interval: 2,
12
+ description: null,
13
+ };
14
+
15
+ export const jobTemplates = [jobTemplate];
16
+
17
+ export const jobTemplateResponse = {
18
+ job_template: jobTemplate,
19
+ effective_user: {
20
+ id: null,
21
+ job_template_id: 178,
22
+ value: 'default effective user',
23
+ overridable: true,
24
+ current_user: false,
25
+ },
26
+ };
@@ -0,0 +1,156 @@
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import configureMockStore from 'redux-mock-store';
4
+ import { mount } from '@theforeman/test';
5
+ import { render, fireEvent, screen, act } from '@testing-library/react';
6
+ import * as api from 'foremanReact/redux/API';
7
+ import { JobWizard } from '../JobWizard';
8
+ import * as selectors from '../JobWizardSelectors';
9
+ import { jobTemplates, jobTemplateResponse as jobTemplate } from './fixtures';
10
+
11
+ jest.spyOn(api, 'get');
12
+ jest.spyOn(selectors, 'selectJobTemplate');
13
+ jest.spyOn(selectors, 'selectJobTemplates');
14
+ jest.spyOn(selectors, 'selectJobCategories');
15
+ jest.spyOn(selectors, 'selectJobCategoriesStatus');
16
+
17
+ const jobCategories = ['Ansible Commands', 'Puppet', 'Services'];
18
+
19
+ api.get.mockImplementation(({ handleSuccess, ...action }) => {
20
+ if (action.key === 'JOB_CATEGORIES') {
21
+ handleSuccess && handleSuccess({ data: { job_categories: jobCategories } });
22
+ } else if (action.key === 'JOB_TEMPLATE') {
23
+ handleSuccess &&
24
+ handleSuccess({
25
+ data: jobTemplate,
26
+ });
27
+ }
28
+ return { type: 'get', ...action };
29
+ });
30
+
31
+ selectors.selectJobTemplate.mockImplementation(() => null);
32
+ selectors.selectJobCategories.mockImplementation(() => jobCategories);
33
+ selectors.selectJobCategoriesStatus.mockImplementation(() => null);
34
+ selectors.selectJobTemplates.mockImplementation(() => jobTemplates);
35
+
36
+ const mockStore = configureMockStore([]);
37
+ const store = mockStore({});
38
+ describe('Job wizard fill', () => {
39
+ it('should select template', async () => {
40
+ const wrapper = mount(
41
+ <Provider store={store}>
42
+ <JobWizard advancedValues={{}} setAdvancedValues={jest.fn()} />
43
+ </Provider>
44
+ );
45
+ expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
46
+ 4
47
+ );
48
+ selectors.selectJobCategoriesStatus.mockImplementation(() => 'RESOLVED');
49
+ expect(store.getActions()).toMatchSnapshot('initial');
50
+
51
+ selectors.selectJobTemplate.mockImplementation(() => jobTemplate);
52
+ wrapper.find('.pf-c-button.pf-c-select__toggle-button').simulate('click');
53
+ await act(async () => {
54
+ await wrapper.find('.pf-c-select__menu-item').simulate('click');
55
+ await wrapper.update();
56
+ });
57
+ expect(store.getActions().slice(-1)).toMatchSnapshot('select template');
58
+ expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
59
+ 0
60
+ );
61
+ });
62
+
63
+ it('should save data between steps for advanced fields', async () => {
64
+ const wrapper = mount(
65
+ <Provider store={store}>
66
+ <JobWizard advancedValues={{}} setAdvancedValues={jest.fn()} />
67
+ </Provider>
68
+ );
69
+ // setup
70
+ selectors.selectJobCategoriesStatus.mockImplementation(() => 'RESOLVED');
71
+ selectors.selectJobTemplate.mockImplementation(() => jobTemplate);
72
+ wrapper.find('.pf-c-button.pf-c-select__toggle-button').simulate('click');
73
+ wrapper.find('.pf-c-select__menu-item').simulate('click');
74
+
75
+ // test
76
+ expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
77
+ 0
78
+ );
79
+ wrapper
80
+ .find('.pf-c-wizard__nav-link')
81
+ .at(2)
82
+ .simulate('click'); // Advanced step
83
+ const effectiveUserInput = () => wrapper.find('input#effective-user');
84
+ const effectiveUesrValue = 'effective user new value';
85
+ effectiveUserInput().getDOMNode().value = effectiveUesrValue;
86
+ await act(async () => {
87
+ await effectiveUserInput().simulate('change');
88
+ wrapper.update();
89
+ });
90
+
91
+ expect(effectiveUserInput().prop('value')).toEqual(effectiveUesrValue);
92
+
93
+ wrapper
94
+ .find('.pf-c-wizard__nav-link')
95
+ .at(1)
96
+ .simulate('click');
97
+
98
+ expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-current').text()).toEqual(
99
+ 'Target Hosts'
100
+ );
101
+ wrapper
102
+ .find('.pf-c-wizard__nav-link')
103
+ .at(2)
104
+ .simulate('click'); // Advanced step
105
+
106
+ expect(effectiveUserInput().prop('value')).toEqual(effectiveUesrValue);
107
+ });
108
+
109
+ it('have all steps', async () => {
110
+ selectors.selectJobCategoriesStatus.mockImplementation(() => null);
111
+ selectors.selectJobTemplate.mockRestore();
112
+ selectors.selectJobTemplates.mockRestore();
113
+ selectors.selectJobCategories.mockRestore();
114
+ api.get.mockImplementation(({ handleSuccess, ...action }) => {
115
+ if (action.key === 'JOB_CATEGORIES') {
116
+ handleSuccess &&
117
+ handleSuccess({ data: { job_categories: jobCategories } });
118
+ } else if (action.key === 'JOB_TEMPLATE') {
119
+ handleSuccess &&
120
+ handleSuccess({
121
+ data: jobTemplate,
122
+ });
123
+ } else if (action.key === 'JOB_TEMPLATES') {
124
+ handleSuccess &&
125
+ handleSuccess({
126
+ data: { results: [jobTemplate.job_template] },
127
+ });
128
+ }
129
+ return { type: 'get', ...action };
130
+ });
131
+
132
+ render(
133
+ <Provider store={store}>
134
+ <JobWizard />
135
+ </Provider>
136
+ );
137
+ const steps = [
138
+ 'Target Hosts',
139
+ 'Advanced Fields',
140
+ 'Schedule',
141
+ 'Review Details',
142
+ 'Category and Template',
143
+ ];
144
+ // eslint-disable-next-line no-unused-vars
145
+ for await (const step of steps) {
146
+ const stepSelector = screen.getByText(step);
147
+ const stepTitle = screen.getAllByText(step);
148
+ expect(stepTitle).toHaveLength(1);
149
+ await act(async () => {
150
+ await fireEvent.click(stepSelector);
151
+ });
152
+ const stepTitles = screen.getAllByText(step);
153
+ expect(stepTitles).toHaveLength(3);
154
+ }
155
+ });
156
+ });
@@ -0,0 +1,93 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useSelector } from 'react-redux';
4
+ import { Title, Form } from '@patternfly/react-core';
5
+ import { translate as __ } from 'foremanReact/common/I18n';
6
+ import { selectJobTemplate } from '../../JobWizardSelectors';
7
+ import {
8
+ EffectiveUserField,
9
+ TimeoutToKillField,
10
+ PasswordField,
11
+ KeyPassphraseField,
12
+ EffectiveUserPasswordField,
13
+ ConcurrencyLevelField,
14
+ TimeSpanLevelField,
15
+ } from './Fields';
16
+
17
+ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
18
+ const jobTemplate = useSelector(selectJobTemplate);
19
+ const effectiveUser = jobTemplate.effective_user;
20
+ return (
21
+ <>
22
+ <Title headingLevel="h2" className="advanced-fields-title">
23
+ {__('Advanced Fields')}
24
+ </Title>
25
+ <Form>
26
+ {effectiveUser?.overridable && (
27
+ <EffectiveUserField
28
+ value={advancedValues.effectiveUserValue}
29
+ setValue={newValue =>
30
+ setAdvancedValues({
31
+ effectiveUserValue: newValue,
32
+ })
33
+ }
34
+ />
35
+ )}
36
+ <TimeoutToKillField
37
+ value={advancedValues.timeoutToKill}
38
+ setValue={newValue =>
39
+ setAdvancedValues({
40
+ timeoutToKill: newValue,
41
+ })
42
+ }
43
+ />
44
+ <PasswordField
45
+ value={advancedValues.password}
46
+ setValue={newValue =>
47
+ setAdvancedValues({
48
+ password: newValue,
49
+ })
50
+ }
51
+ />
52
+ <KeyPassphraseField
53
+ value={advancedValues.keyPassphrase}
54
+ setValue={newValue =>
55
+ setAdvancedValues({
56
+ keyPassphrase: newValue,
57
+ })
58
+ }
59
+ />
60
+ <EffectiveUserPasswordField
61
+ value={advancedValues.effectiveUserPassword}
62
+ setValue={newValue =>
63
+ setAdvancedValues({
64
+ effectiveUserPassword: newValue,
65
+ })
66
+ }
67
+ />
68
+ <ConcurrencyLevelField
69
+ value={advancedValues.concurrencyLevel}
70
+ setValue={newValue =>
71
+ setAdvancedValues({
72
+ concurrencyLevel: newValue,
73
+ })
74
+ }
75
+ />
76
+ <TimeSpanLevelField
77
+ value={advancedValues.timeSpan}
78
+ setValue={newValue =>
79
+ setAdvancedValues({
80
+ timeSpan: newValue,
81
+ })
82
+ }
83
+ />
84
+ </Form>
85
+ </>
86
+ );
87
+ };
88
+
89
+ AdvancedFields.propTypes = {
90
+ advancedValues: PropTypes.object.isRequired,
91
+ setAdvancedValues: PropTypes.func.isRequired,
92
+ };
93
+ export default AdvancedFields;
@@ -0,0 +1,181 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormGroup, TextInput } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { helpLabel } from '../form/FormHelpers';
6
+
7
+ export const EffectiveUserField = ({ value, setValue }) => (
8
+ <FormGroup
9
+ label={__('Effective user')}
10
+ labelIcon={helpLabel(
11
+ __(
12
+ 'A user to be used for executing the script. If it differs from the SSH user, su or sudo is used to switch the accounts.'
13
+ ),
14
+ 'effective-user'
15
+ )}
16
+ fieldId="effective-user"
17
+ >
18
+ <TextInput
19
+ autoComplete="effective-user"
20
+ id="effective-user"
21
+ type="text"
22
+ value={value}
23
+ onChange={newValue => setValue(newValue)}
24
+ />
25
+ </FormGroup>
26
+ );
27
+
28
+ export const TimeoutToKillField = ({ value, setValue }) => (
29
+ <FormGroup
30
+ label={__('Timeout to kill')}
31
+ labelIcon={helpLabel(
32
+ __(
33
+ 'Time in seconds from the start on the remote host after which the job should be killed.'
34
+ ),
35
+ 'timeout-to-kill'
36
+ )}
37
+ fieldId="timeout-to-kill"
38
+ >
39
+ <TextInput
40
+ type="number"
41
+ value={value}
42
+ placeholder={__('For example: 1, 2, 3, 4, 5...')}
43
+ autoComplete="timeout-to-kill"
44
+ id="timeout-to-kill"
45
+ onChange={newValue => setValue(newValue)}
46
+ />
47
+ </FormGroup>
48
+ );
49
+
50
+ export const PasswordField = ({ value, setValue }) => (
51
+ <FormGroup
52
+ label={__('Password')}
53
+ labelIcon={helpLabel(
54
+ __(
55
+ 'Password is stored encrypted in DB until the job finishes. For future or recurring executions, it is removed after the last execution.'
56
+ ),
57
+ 'password'
58
+ )}
59
+ fieldId="password"
60
+ >
61
+ <TextInput
62
+ autoComplete="password"
63
+ id="password"
64
+ type="password"
65
+ placeholder="*****"
66
+ value={value}
67
+ onChange={newValue => setValue(newValue)}
68
+ />
69
+ </FormGroup>
70
+ );
71
+
72
+ export const KeyPassphraseField = ({ value, setValue }) => (
73
+ <FormGroup
74
+ label={__('Private key passphrase')}
75
+ labelIcon={helpLabel(
76
+ __(
77
+ 'Key passphrase is only applicable for SSH provider. Other providers ignore this field. Passphrase is stored encrypted in DB until the job finishes. For future or recurring executions, it is removed after the last execution.'
78
+ ),
79
+ 'key-passphrase'
80
+ )}
81
+ fieldId="key-passphrase"
82
+ >
83
+ <TextInput
84
+ autoComplete="key-passphrase"
85
+ id="key-passphrase"
86
+ type="password"
87
+ placeholder="*****"
88
+ value={value}
89
+ onChange={newValue => setValue(newValue)}
90
+ />
91
+ </FormGroup>
92
+ );
93
+
94
+ export const EffectiveUserPasswordField = ({ value, setValue }) => (
95
+ <FormGroup
96
+ label={__('Effective user password')}
97
+ labelIcon={helpLabel(
98
+ __(
99
+ 'Effective user password is only applicable for SSH provider. Other providers ignore this field. Password is stored encrypted in DB until the job finishes. For future or recurring executions, it is removed after the last execution.'
100
+ ),
101
+ 'effective-user-password'
102
+ )}
103
+ fieldId="effective-user-password"
104
+ >
105
+ <TextInput
106
+ autoComplete="effective-user-password"
107
+ id="effective-user-password"
108
+ type="password"
109
+ placeholder="*****"
110
+ value={value}
111
+ onChange={newValue => setValue(newValue)}
112
+ />
113
+ </FormGroup>
114
+ );
115
+
116
+ export const ConcurrencyLevelField = ({ value, setValue }) => (
117
+ <FormGroup
118
+ label={__('Concurrency level')}
119
+ labelIcon={helpLabel(
120
+ __(
121
+ 'Run at most N tasks at a time. If this is set and proxy batch triggering is enabled, then tasks are triggered on the smart proxy in batches of size 1.'
122
+ ),
123
+ 'concurrency-level'
124
+ )}
125
+ fieldId="concurrency-level"
126
+ >
127
+ <TextInput
128
+ min={1}
129
+ type="number"
130
+ autoComplete="concurrency-level"
131
+ id="concurrency-level"
132
+ placeholder={__('For example: 1, 2, 3, 4, 5...')}
133
+ value={value}
134
+ onChange={newValue => setValue(newValue)}
135
+ />
136
+ </FormGroup>
137
+ );
138
+
139
+ export const TimeSpanLevelField = ({ value, setValue }) => (
140
+ <FormGroup
141
+ label={__('Time span')}
142
+ labelIcon={helpLabel(
143
+ __(
144
+ 'Distribute execution over N seconds. If this is set and proxy batch triggering is enabled, then tasks are triggered on the smart proxy in batches of size 1.'
145
+ ),
146
+ 'time-span'
147
+ )}
148
+ fieldId="time-span"
149
+ >
150
+ <TextInput
151
+ min={1}
152
+ type="number"
153
+ autoComplete="time-span"
154
+ id="time-span"
155
+ placeholder={__('For example: 1, 2, 3, 4, 5...')}
156
+ value={value}
157
+ onChange={newValue => setValue(newValue)}
158
+ />
159
+ </FormGroup>
160
+ );
161
+
162
+ EffectiveUserField.propTypes = {
163
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
164
+ setValue: PropTypes.func.isRequired,
165
+ };
166
+ EffectiveUserField.defaultProps = {
167
+ value: '',
168
+ };
169
+
170
+ TimeoutToKillField.propTypes = EffectiveUserField.propTypes;
171
+ TimeoutToKillField.defaultProps = EffectiveUserField.defaultProps;
172
+ PasswordField.propTypes = EffectiveUserField.propTypes;
173
+ PasswordField.defaultProps = EffectiveUserField.defaultProps;
174
+ KeyPassphraseField.propTypes = EffectiveUserField.propTypes;
175
+ KeyPassphraseField.defaultProps = EffectiveUserField.defaultProps;
176
+ EffectiveUserPasswordField.propTypes = EffectiveUserField.propTypes;
177
+ EffectiveUserPasswordField.defaultProps = EffectiveUserField.defaultProps;
178
+ ConcurrencyLevelField.propTypes = EffectiveUserField.propTypes;
179
+ ConcurrencyLevelField.defaultProps = EffectiveUserField.defaultProps;
180
+ TimeSpanLevelField.propTypes = EffectiveUserField.propTypes;
181
+ TimeSpanLevelField.defaultProps = EffectiveUserField.defaultProps;