foreman_remote_execution 4.3.1 → 4.5.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +27 -22
  3. data/app/controllers/foreman_remote_execution/concerns/api/v2/registration_commands_controller_extensions.rb +19 -0
  4. data/app/controllers/job_invocations_controller.rb +1 -1
  5. data/app/controllers/job_templates_controller.rb +4 -4
  6. data/app/controllers/ui_job_wizard_controller.rb +19 -0
  7. data/app/helpers/job_invocations_helper.rb +2 -2
  8. data/app/helpers/remote_execution_helper.rb +40 -9
  9. data/app/lib/actions/remote_execution/run_host_job.rb +36 -6
  10. data/app/lib/foreman_remote_execution/provider_input.rb +29 -0
  11. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +7 -5
  12. data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +6 -0
  13. data/app/models/host_proxy_invocation.rb +4 -0
  14. data/app/models/host_status/execution_status.rb +5 -5
  15. data/app/models/invocation_provider_input_value.rb +12 -0
  16. data/app/models/job_invocation.rb +35 -12
  17. data/app/models/job_invocation_composer.rb +74 -19
  18. data/app/models/remote_execution_provider.rb +18 -3
  19. data/app/models/setting/remote_execution.rb +11 -1
  20. data/app/models/ssh_execution_provider.rb +4 -4
  21. data/app/models/targeting.rb +5 -1
  22. data/app/models/template_invocation.rb +2 -0
  23. data/app/overrides/execution_interface.rb +8 -8
  24. data/app/overrides/subnet_proxies.rb +6 -6
  25. data/app/services/renderer_methods.rb +12 -0
  26. data/app/views/job_invocations/_form.html.erb +8 -0
  27. data/app/views/job_invocations/index.html.erb +1 -1
  28. data/app/views/templates/ssh/module_action.erb +1 -0
  29. data/app/views/templates/ssh/puppet_run_once.erb +1 -0
  30. data/config/routes.rb +1 -0
  31. data/db/migrate/20180110104432_rename_template_invocation_permission.rb +1 -1
  32. data/db/migrate/20190111153330_remove_remote_execution_without_proxy_setting.rb +4 -4
  33. data/db/migrate/20210312074713_add_provider_inputs.rb +10 -0
  34. data/db/migrate/2021051713291621250977_add_host_proxy_invocations.rb +12 -0
  35. data/extra/cockpit/foreman-cockpit-session +6 -6
  36. data/foreman_remote_execution.gemspec +1 -1
  37. data/lib/foreman_remote_execution/engine.rb +14 -12
  38. data/lib/foreman_remote_execution/version.rb +1 -1
  39. data/locale/action_names.rb +1 -0
  40. data/locale/de/foreman_remote_execution.po +77 -27
  41. data/locale/en/foreman_remote_execution.po +77 -27
  42. data/locale/en_GB/foreman_remote_execution.po +77 -27
  43. data/locale/es/foreman_remote_execution.po +77 -27
  44. data/locale/foreman_remote_execution.pot +241 -163
  45. data/locale/fr/foreman_remote_execution.po +77 -27
  46. data/locale/ja/foreman_remote_execution.po +77 -27
  47. data/locale/ko/foreman_remote_execution.po +77 -27
  48. data/locale/pt_BR/foreman_remote_execution.po +77 -27
  49. data/locale/ru/foreman_remote_execution.po +77 -27
  50. data/locale/zh_CN/foreman_remote_execution.po +77 -27
  51. data/locale/zh_TW/foreman_remote_execution.po +77 -27
  52. data/package.json +4 -2
  53. data/test/functional/api/v2/job_invocations_controller_test.rb +14 -1
  54. data/test/helpers/remote_execution_helper_test.rb +16 -0
  55. data/test/unit/job_invocation_composer_test.rb +100 -3
  56. data/test/unit/job_invocation_report_template_test.rb +57 -0
  57. data/test/unit/job_invocation_test.rb +1 -1
  58. data/webpack/JobWizard/JobWizard.js +95 -11
  59. data/webpack/JobWizard/JobWizard.scss +53 -0
  60. data/webpack/JobWizard/JobWizardConstants.js +16 -0
  61. data/webpack/JobWizard/JobWizardSelectors.js +47 -0
  62. data/webpack/JobWizard/__tests__/__snapshots__/integration.test.js.snap +43 -0
  63. data/webpack/JobWizard/__tests__/fixtures.js +128 -0
  64. data/webpack/JobWizard/__tests__/integration.test.js +84 -0
  65. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +110 -0
  66. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +67 -0
  67. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +195 -0
  68. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +144 -0
  69. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +23 -0
  70. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +109 -0
  71. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +123 -0
  72. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +94 -0
  73. data/webpack/JobWizard/steps/Schedule/QueryType.js +48 -0
  74. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +61 -0
  75. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +25 -0
  76. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +51 -0
  77. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +22 -0
  78. data/webpack/JobWizard/steps/Schedule/index.js +41 -0
  79. data/webpack/JobWizard/steps/form/FormHelpers.js +20 -0
  80. data/webpack/JobWizard/steps/form/Formatter.js +149 -0
  81. data/webpack/JobWizard/steps/form/GroupedSelectField.js +91 -0
  82. data/webpack/JobWizard/steps/form/NumberInput.js +33 -0
  83. data/webpack/JobWizard/steps/form/SelectField.js +60 -0
  84. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +76 -0
  85. data/webpack/__mocks__/foremanReact/common/helpers.js +1 -0
  86. data/webpack/__mocks__/foremanReact/components/SearchBar.js +18 -1
  87. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +21 -2
  88. data/webpack/__mocks__/foremanReact/redux/API/index.js +5 -0
  89. data/webpack/__mocks__/foremanReact/routes/common/PageLayout/PageLayout.js +10 -0
  90. data/webpack/global_index.js +6 -0
  91. data/webpack/index.js +3 -4
  92. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +83 -0
  93. data/webpack/react_app/components/RecentJobsCard/constants.js +1 -0
  94. data/webpack/react_app/components/RecentJobsCard/index.js +1 -0
  95. data/webpack/react_app/components/RecentJobsCard/styles.css +15 -0
  96. data/webpack/react_app/components/RegistrationExtension/RexInterface.js +50 -0
  97. data/webpack/react_app/components/RegistrationExtension/__tests__/RexInterface.test.js +9 -0
  98. data/webpack/react_app/components/RegistrationExtension/__tests__/__snapshots__/RexInterface.test.js.snap +35 -0
  99. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +8 -3
  100. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
  101. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +7 -2
  102. data/webpack/react_app/extend/fillRecentJobsCard.js +11 -0
  103. data/webpack/react_app/extend/fillregistrationAdvanced.js +11 -0
  104. data/webpack/react_app/extend/reducers.js +5 -0
  105. metadata +52 -8
  106. data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +0 -70
  107. data/app/views/api/v2/registration/_form.html.erb +0 -12
  108. data/test/models/orchestration/ssh_test.rb +0 -56
@@ -0,0 +1,48 @@
1
+ import React, { useState } from 'react';
2
+ import { FormGroup, Radio } from '@patternfly/react-core';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+ import { helpLabel } from '../form/FormHelpers';
5
+
6
+ export const QueryType = () => {
7
+ const [isTypeStatic, setIsTypeStatic] = useState(true);
8
+ return (
9
+ <FormGroup
10
+ label={__('Query type')}
11
+ fieldId="query-type"
12
+ labelIcon={helpLabel(
13
+ <p>
14
+ {__('Type has impact on when is the query evaluated to hosts.')}
15
+ <br />
16
+ <ul>
17
+ <li>
18
+ <b>{__('Static')}</b> -{' '}
19
+ {__('evaluates just after you submit this form')}
20
+ </li>
21
+ <li>
22
+ <b>{__('Dynamic')}</b> -{' '}
23
+ {__(
24
+ "evaluates just before the execution is started, so if it's planed in future, targeted hosts set may change before it"
25
+ )}
26
+ </li>
27
+ </ul>
28
+ </p>,
29
+ 'query-type'
30
+ )}
31
+ >
32
+ <Radio
33
+ isChecked={isTypeStatic}
34
+ name="query-type"
35
+ onChange={() => setIsTypeStatic(true)}
36
+ id="query-type-static"
37
+ label={__('Static query')}
38
+ />
39
+ <Radio
40
+ isChecked={!isTypeStatic}
41
+ name="query-type"
42
+ onChange={() => setIsTypeStatic(false)}
43
+ id="query-type-dynamic"
44
+ label={__('Dynamic query')}
45
+ />
46
+ </FormGroup>
47
+ );
48
+ };
@@ -0,0 +1,61 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { TextInput, Grid, GridItem, FormGroup } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { SelectField } from '../form/SelectField';
6
+ import { repeatTypes } from '../../JobWizardConstants';
7
+
8
+ export const RepeatOn = ({
9
+ repeatType,
10
+ setRepeatType,
11
+ repeatAmount,
12
+ setRepeatAmount,
13
+ }) => {
14
+ const [repeatValidated, setRepeatValidated] = useState('default');
15
+ const handleRepeatInputChange = newValue => {
16
+ setRepeatValidated(newValue >= 1 ? 'default' : 'error');
17
+ setRepeatAmount(newValue);
18
+ };
19
+ return (
20
+ <Grid>
21
+ <GridItem span={6}>
22
+ <SelectField
23
+ fieldId="repeat-select"
24
+ options={Object.values(repeatTypes)}
25
+ setValue={newValue => {
26
+ setRepeatType(newValue);
27
+ if (newValue === repeatTypes.noRepeat) {
28
+ setRepeatValidated('default');
29
+ }
30
+ }}
31
+ value={repeatType}
32
+ />
33
+ </GridItem>
34
+ <GridItem span={1} />
35
+ <GridItem span={5}>
36
+ <FormGroup
37
+ isInline
38
+ helperTextInvalid={__('Repeat amount can only be a positive number')}
39
+ validated={repeatValidated}
40
+ >
41
+ <TextInput
42
+ isDisabled={repeatType === repeatTypes.noRepeat}
43
+ id="repeat-amount"
44
+ value={repeatAmount}
45
+ type="text"
46
+ onChange={newValue => handleRepeatInputChange(newValue)}
47
+ placeholder={__('Repeat N times')}
48
+ />
49
+ </FormGroup>
50
+ </GridItem>
51
+ </Grid>
52
+ );
53
+ };
54
+
55
+ RepeatOn.propTypes = {
56
+ repeatType: PropTypes.oneOf(Object.values(repeatTypes)).isRequired,
57
+ setRepeatType: PropTypes.func.isRequired,
58
+ repeatAmount: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
59
+ .isRequired,
60
+ setRepeatAmount: PropTypes.func.isRequired,
61
+ };
@@ -0,0 +1,25 @@
1
+ import React, { useState } from 'react';
2
+ import { FormGroup, Radio } from '@patternfly/react-core';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+
5
+ export const ScheduleType = () => {
6
+ const [isFuture, setIsFuture] = useState(false);
7
+ return (
8
+ <FormGroup label={__('Schedule type')} fieldId="schedule-type">
9
+ <Radio
10
+ isChecked={!isFuture}
11
+ name="schedule-type"
12
+ onChange={() => setIsFuture(false)}
13
+ id="schedule-type-now"
14
+ label={__('Execute now')}
15
+ />
16
+ <Radio
17
+ isChecked={isFuture}
18
+ name="schedule-type"
19
+ onChange={() => setIsFuture(true)}
20
+ id="schedule-type-future"
21
+ label={__('Schedule for future execution')}
22
+ />
23
+ </FormGroup>
24
+ );
25
+ };
@@ -0,0 +1,51 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormGroup, TextInput, Checkbox } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+
6
+ // TODO: change to datepicker
7
+ export const StartEndDates = ({ starts, setStarts, ends, setEnds }) => {
8
+ const [isNeverEnds, setIsNeverEnds] = useState(false);
9
+ const toggleIsNeverEnds = (checked, event) => {
10
+ const value = event?.target?.checked;
11
+ setIsNeverEnds(value);
12
+ setEnds('');
13
+ };
14
+ return (
15
+ <>
16
+ <FormGroup label={__('Starts')} fieldId="start-date">
17
+ <TextInput
18
+ id="start-date"
19
+ value={starts}
20
+ type="text"
21
+ onChange={newValue => setStarts(newValue)}
22
+ placeholder="mm/dd/yy, hh:mm UTC"
23
+ />
24
+ </FormGroup>
25
+ <FormGroup label={__('Ends')} fieldId="end-date">
26
+ <TextInput
27
+ isDisabled={isNeverEnds}
28
+ id="end-date"
29
+ value={ends}
30
+ type="text"
31
+ onChange={newValue => setEnds(newValue)}
32
+ placeholder="mm/dd/yy, hh:mm UTC"
33
+ />
34
+ <Checkbox
35
+ label={__('Never ends')}
36
+ isChecked={isNeverEnds}
37
+ onChange={toggleIsNeverEnds}
38
+ id="never-ends"
39
+ name="never-ends"
40
+ />
41
+ </FormGroup>
42
+ </>
43
+ );
44
+ };
45
+
46
+ StartEndDates.propTypes = {
47
+ starts: PropTypes.string.isRequired,
48
+ setStarts: PropTypes.func.isRequired,
49
+ ends: PropTypes.string.isRequired,
50
+ setEnds: PropTypes.func.isRequired,
51
+ };
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, screen } from '@testing-library/react';
3
+ import { StartEndDates } from '../StartEndDates';
4
+
5
+ const setEnds = jest.fn();
6
+ const props = {
7
+ starts: '',
8
+ setStarts: jest.fn(),
9
+ ends: 'some-end-date',
10
+ setEnds,
11
+ };
12
+
13
+ describe('StartEndDates', () => {
14
+ it('never ends', () => {
15
+ render(<StartEndDates {...props} />);
16
+ const neverEnds = screen.getByLabelText('Never ends', {
17
+ selector: 'input',
18
+ });
19
+ fireEvent.click(neverEnds);
20
+ expect(setEnds).toBeCalledWith('');
21
+ });
22
+ });
@@ -0,0 +1,41 @@
1
+ import React, { useState } from 'react';
2
+ import { Title, Button, Form } from '@patternfly/react-core';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+ import { ScheduleType } from './ScheduleType';
5
+ import { RepeatOn } from './RepeatOn';
6
+ import { QueryType } from './QueryType';
7
+ import { StartEndDates } from './StartEndDates';
8
+ import { repeatTypes } from '../../JobWizardConstants';
9
+
10
+ const Schedule = () => {
11
+ const [repeatType, setRepeatType] = useState(repeatTypes.noRepeat);
12
+ const [repeatAmount, setRepeatAmount] = useState('');
13
+ const [starts, setStarts] = useState('');
14
+ const [ends, setEnds] = useState('');
15
+
16
+ return (
17
+ <Form className="schedule-tab">
18
+ <Title headingLevel="h2">{__('Schedule')}</Title>
19
+ <ScheduleType />
20
+
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>
38
+ );
39
+ };
40
+
41
+ export default Schedule;
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { Popover } from '@patternfly/react-core';
3
+ import { HelpIcon } from '@patternfly/react-icons';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+
6
+ export const helpLabel = (text, id) => {
7
+ if (!text) return null;
8
+ return (
9
+ <Popover id={`${id}-help`} bodyContent={text} aria-label="help-text">
10
+ <button
11
+ type="button"
12
+ aria-label={__('open-help-tooltip-button')}
13
+ onClick={e => e.preventDefault()}
14
+ className="pf-c-form__group-label-help"
15
+ >
16
+ <HelpIcon noVerticalAlign />
17
+ </button>
18
+ </Popover>
19
+ );
20
+ };
@@ -0,0 +1,149 @@
1
+ import React, { useEffect } from 'react';
2
+ import { useSelector } from 'react-redux';
3
+ import { FormGroup, TextInput, TextArea } from '@patternfly/react-core';
4
+ import PropTypes from 'prop-types';
5
+ import SearchBar from 'foremanReact/components/SearchBar';
6
+ import { helpLabel } from './FormHelpers';
7
+ import { SelectField } from './SelectField';
8
+
9
+ const TemplateSearchField = ({
10
+ name,
11
+ controller,
12
+ labelText,
13
+ required,
14
+ defaultValue,
15
+ setValue,
16
+ values,
17
+ }) => {
18
+ const searchQuery = useSelector(
19
+ state => state.autocomplete?.[name]?.searchQuery
20
+ );
21
+ useEffect(() => {
22
+ setValue({ ...values, [name]: searchQuery });
23
+ // eslint-disable-next-line react-hooks/exhaustive-deps
24
+ }, [searchQuery]);
25
+ return (
26
+ <FormGroup
27
+ label={name}
28
+ labelIcon={helpLabel(labelText, name)}
29
+ fieldId={name}
30
+ isRequired={required}
31
+ className="foreman-search-field"
32
+ >
33
+ <SearchBar
34
+ initialQuery={defaultValue}
35
+ data={{
36
+ controller,
37
+ autocomplete: {
38
+ id: name,
39
+ url: `/${controller}/auto_complete_search`,
40
+ useKeyShortcuts: true,
41
+ },
42
+ }}
43
+ onSearch={() => null}
44
+ />
45
+ </FormGroup>
46
+ );
47
+ };
48
+
49
+ export const formatter = (input, values, setValue) => {
50
+ const isSelectType = !!input?.options;
51
+ const inputType = input.value_type;
52
+ const isTextType = inputType === 'plain' || !inputType; // null defaults to plain
53
+
54
+ const { name, required, hidden_value: hidden } = input;
55
+ const labelText = input.description;
56
+ const value = values[name];
57
+
58
+ if (isSelectType) {
59
+ const options = input.options.split(/\r?\n/).map(option => option.trim());
60
+ return (
61
+ <SelectField
62
+ aria-label={name}
63
+ key={name}
64
+ isRequired={required}
65
+ label={name}
66
+ fieldId={name}
67
+ options={options}
68
+ labelIcon={helpLabel(labelText, name)}
69
+ value={value}
70
+ setValue={newValue => setValue({ ...values, [name]: newValue })}
71
+ />
72
+ );
73
+ }
74
+ if (isTextType) {
75
+ return (
76
+ <FormGroup
77
+ key={name}
78
+ label={name}
79
+ labelIcon={helpLabel(labelText, name)}
80
+ fieldId={name}
81
+ isRequired={required}
82
+ >
83
+ <TextArea
84
+ aria-label={name}
85
+ className={hidden ? 'masked-input' : null}
86
+ required={required}
87
+ rows={2}
88
+ id={name}
89
+ value={value}
90
+ onChange={newValue => setValue({ ...values, [name]: newValue })}
91
+ />
92
+ </FormGroup>
93
+ );
94
+ }
95
+ if (inputType === 'date') {
96
+ return (
97
+ <FormGroup
98
+ key={name}
99
+ label={name}
100
+ labelIcon={helpLabel(labelText, name)}
101
+ fieldId={name}
102
+ isRequired={required}
103
+ >
104
+ <TextInput
105
+ aria-label={name}
106
+ placeholder="YYYY-mm-dd HH:MM"
107
+ className={hidden ? 'masked-input' : null}
108
+ required={required}
109
+ id={name}
110
+ type="text"
111
+ value={value}
112
+ onChange={newValue => setValue({ ...values, [name]: newValue })}
113
+ />
114
+ </FormGroup>
115
+ );
116
+ }
117
+ if (inputType === 'search') {
118
+ const controller = input.resource_type;
119
+ // TODO: get text from redux autocomplete
120
+ return (
121
+ <TemplateSearchField
122
+ key={name}
123
+ name={name}
124
+ defaultValue={value}
125
+ controller={controller}
126
+ labelText={labelText}
127
+ required={required}
128
+ setValue={setValue}
129
+ values={values}
130
+ />
131
+ );
132
+ }
133
+
134
+ return null;
135
+ };
136
+
137
+ TemplateSearchField.propTypes = {
138
+ name: PropTypes.string.isRequired,
139
+ controller: PropTypes.string.isRequired,
140
+ labelText: PropTypes.string,
141
+ required: PropTypes.bool.isRequired,
142
+ defaultValue: PropTypes.string,
143
+ setValue: PropTypes.func.isRequired,
144
+ values: PropTypes.object.isRequired,
145
+ };
146
+ TemplateSearchField.defaultProps = {
147
+ labelText: null,
148
+ defaultValue: '',
149
+ };
@@ -0,0 +1,91 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ SelectOption,
5
+ Select,
6
+ SelectGroup,
7
+ SelectVariant,
8
+ FormGroup,
9
+ } from '@patternfly/react-core';
10
+
11
+ export const GroupedSelectField = ({
12
+ label,
13
+ fieldId,
14
+ groups,
15
+ selected,
16
+ setSelected,
17
+ ...props
18
+ }) => {
19
+ const [isOpen, setIsOpen] = useState(false);
20
+ const onSelect = selection => {
21
+ setIsOpen(false);
22
+ setSelected(selection);
23
+ };
24
+
25
+ const onClear = () => {
26
+ onSelect(null);
27
+ };
28
+
29
+ const options = groups.map((group, groupIndex) => (
30
+ <SelectGroup key={groupIndex} label={group.groupLabel}>
31
+ {group.options.map((option, optionIndex) => (
32
+ <SelectOption
33
+ key={optionIndex}
34
+ value={option.label}
35
+ onClick={() => onSelect(option.value)}
36
+ />
37
+ ))}
38
+ </SelectGroup>
39
+ ));
40
+
41
+ const onFilter = evt => {
42
+ const textInput = evt?.target?.value || '';
43
+ if (textInput === '') {
44
+ return options;
45
+ }
46
+ return options
47
+ .map(group => {
48
+ const filteredGroup = React.cloneElement(group, {
49
+ children: group.props.children.filter(item =>
50
+ item.props.value.toLowerCase().includes(textInput.toLowerCase())
51
+ ),
52
+ });
53
+ if (filteredGroup.props.children.length > 0) return filteredGroup;
54
+ return null;
55
+ })
56
+ .filter(newGroup => newGroup);
57
+ };
58
+
59
+ return (
60
+ <FormGroup label={label} fieldId={fieldId}>
61
+ <Select
62
+ isGrouped
63
+ variant={SelectVariant.typeahead}
64
+ onToggle={setIsOpen}
65
+ onFilter={onFilter}
66
+ isOpen={isOpen}
67
+ onSelect={() => null}
68
+ selections={selected}
69
+ className="without_select2"
70
+ onClear={onClear}
71
+ menuAppendTo={() => document.body}
72
+ {...props}
73
+ >
74
+ {options}
75
+ </Select>
76
+ </FormGroup>
77
+ );
78
+ };
79
+
80
+ GroupedSelectField.propTypes = {
81
+ label: PropTypes.string.isRequired,
82
+ fieldId: PropTypes.string.isRequired,
83
+ groups: PropTypes.array,
84
+ selected: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
85
+ setSelected: PropTypes.func.isRequired,
86
+ };
87
+
88
+ GroupedSelectField.defaultProps = {
89
+ groups: [],
90
+ selected: null,
91
+ };