foreman_remote_execution 4.5.1 → 4.7.0

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