foreman_remote_execution 4.5.1 → 4.7.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby_ci.yml +7 -0
  3. data/app/controllers/ui_job_wizard_controller.rb +7 -0
  4. data/app/graphql/types/job_invocation.rb +16 -0
  5. data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +5 -1
  6. data/app/helpers/remote_execution_helper.rb +9 -3
  7. data/app/lib/actions/remote_execution/run_hosts_job.rb +1 -1
  8. data/app/models/job_invocation_composer.rb +3 -3
  9. data/app/models/job_template.rb +1 -1
  10. data/app/models/remote_execution_feature.rb +5 -1
  11. data/app/models/remote_execution_provider.rb +1 -1
  12. data/app/views/templates/ssh/module_action.erb +1 -0
  13. data/app/views/templates/ssh/power_action.erb +2 -0
  14. data/app/views/templates/ssh/puppet_run_once.erb +1 -0
  15. data/foreman_remote_execution.gemspec +2 -4
  16. data/lib/foreman_remote_execution/engine.rb +3 -0
  17. data/lib/foreman_remote_execution/version.rb +1 -1
  18. data/test/graphql/queries/job_invocation_query_test.rb +31 -0
  19. data/test/graphql/queries/job_invocations_query_test.rb +35 -0
  20. data/test/unit/concerns/host_extensions_test.rb +4 -4
  21. data/test/unit/input_template_renderer_test.rb +1 -89
  22. data/test/unit/job_invocation_composer_test.rb +1 -12
  23. data/webpack/JobWizard/JobWizard.js +28 -8
  24. data/webpack/JobWizard/JobWizard.scss +39 -0
  25. data/webpack/JobWizard/JobWizardConstants.js +10 -0
  26. data/webpack/JobWizard/JobWizardSelectors.js +9 -0
  27. data/webpack/JobWizard/__tests__/fixtures.js +104 -2
  28. data/webpack/JobWizard/__tests__/integration.test.js +13 -85
  29. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +21 -4
  30. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +67 -0
  31. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +73 -59
  32. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +135 -16
  33. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +23 -0
  34. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +122 -51
  35. data/webpack/JobWizard/steps/Schedule/QueryType.js +48 -0
  36. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +61 -0
  37. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +25 -0
  38. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +51 -0
  39. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +22 -0
  40. data/webpack/JobWizard/steps/Schedule/index.js +41 -0
  41. data/webpack/JobWizard/steps/form/FormHelpers.js +1 -0
  42. data/webpack/JobWizard/steps/form/Formatter.js +149 -0
  43. data/webpack/JobWizard/steps/form/NumberInput.js +33 -0
  44. data/webpack/JobWizard/steps/form/SelectField.js +14 -2
  45. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +76 -0
  46. data/webpack/__mocks__/foremanReact/components/SearchBar.js +18 -1
  47. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
  48. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +72 -66
  49. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
  50. data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
  51. data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
  52. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
  53. data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
  54. metadata +23 -27
  55. data/webpack/JobWizard/__tests__/JobWizard.test.js +0 -13
  56. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -32
  57. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +0 -249
  58. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +0 -113
  59. data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +0 -38
  60. data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +0 -23
  61. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +0 -37
  62. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +0 -23
  63. data/webpack/react_app/components/RecentJobsCard/styles.css +0 -15
@@ -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;
@@ -8,6 +8,7 @@ export const helpLabel = (text, id) => {
8
8
  return (
9
9
  <Popover id={`${id}-help`} bodyContent={text} aria-label="help-text">
10
10
  <button
11
+ type="button"
11
12
  aria-label={__('open-help-tooltip-button')}
12
13
  onClick={e => e.preventDefault()}
13
14
  className="pf-c-form__group-label-help"
@@ -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,33 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormGroup, TextInput, ValidatedOptions } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+
6
+ export const NumberInput = ({ formProps, inputProps }) => {
7
+ const [validated, setValidated] = useState();
8
+ return (
9
+ <FormGroup
10
+ {...formProps}
11
+ helperTextInvalid={__('Has to be a number')}
12
+ validated={validated}
13
+ >
14
+ <TextInput
15
+ type="text"
16
+ {...inputProps}
17
+ onChange={newValue => {
18
+ setValidated(
19
+ /^\d+$/.test(newValue) || newValue === ''
20
+ ? ValidatedOptions.noval
21
+ : ValidatedOptions.error
22
+ );
23
+ inputProps.onChange(newValue);
24
+ }}
25
+ />
26
+ </FormGroup>
27
+ );
28
+ };
29
+
30
+ NumberInput.propTypes = {
31
+ formProps: PropTypes.object.isRequired,
32
+ inputProps: PropTypes.object.isRequired,
33
+ };