foreman_remote_execution 4.8.0 → 5.0.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +9 -0
  3. data/app/controllers/ui_job_wizard_controller.rb +16 -4
  4. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  5. data/app/graphql/types/job_invocation_input.rb +13 -0
  6. data/app/graphql/types/recurrence_input.rb +8 -0
  7. data/app/graphql/types/scheduling_input.rb +6 -0
  8. data/app/graphql/types/targeting_enum.rb +7 -0
  9. data/app/lib/actions/remote_execution/run_host_job.rb +4 -0
  10. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
  11. data/app/models/job_invocation_composer.rb +1 -1
  12. data/app/models/targeting.rb +2 -2
  13. data/app/views/job_invocations/refresh.js.erb +1 -0
  14. data/config/routes.rb +1 -0
  15. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  16. data/lib/foreman_remote_execution/engine.rb +110 -6
  17. data/lib/foreman_remote_execution/version.rb +1 -1
  18. data/package.json +6 -6
  19. data/test/functional/api/v2/job_invocations_controller_test.rb +10 -0
  20. data/test/functional/cockpit_controller_test.rb +0 -1
  21. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  22. data/test/helpers/remote_execution_helper_test.rb +0 -1
  23. data/test/unit/actions/run_host_job_test.rb +21 -0
  24. data/test/unit/concerns/host_extensions_test.rb +36 -3
  25. data/test/unit/job_invocation_composer_test.rb +3 -5
  26. data/test/unit/job_invocation_report_template_test.rb +1 -1
  27. data/test/unit/job_template_effective_user_test.rb +0 -4
  28. data/test/unit/remote_execution_provider_test.rb +0 -4
  29. data/test/unit/targeting_test.rb +68 -1
  30. data/webpack/JobWizard/JobWizard.js +94 -13
  31. data/webpack/JobWizard/JobWizard.scss +59 -35
  32. data/webpack/JobWizard/JobWizardConstants.js +28 -1
  33. data/webpack/JobWizard/JobWizardSelectors.js +32 -0
  34. data/webpack/JobWizard/__tests__/fixtures.js +81 -6
  35. data/webpack/JobWizard/__tests__/integration.test.js +26 -15
  36. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  37. data/webpack/JobWizard/autofill.js +38 -0
  38. data/webpack/JobWizard/index.js +7 -0
  39. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +7 -4
  40. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
  41. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +216 -12
  42. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
  43. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +1 -0
  44. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  45. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  46. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  47. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  48. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  49. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +82 -7
  50. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  51. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +7 -4
  52. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  53. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  54. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  55. data/webpack/JobWizard/steps/HostsAndInputs/index.js +182 -34
  56. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  57. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  58. data/webpack/JobWizard/steps/Schedule/QueryType.js +46 -43
  59. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  60. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  61. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  62. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  63. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
  64. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  65. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +59 -19
  66. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +258 -11
  67. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +11 -2
  68. data/webpack/JobWizard/steps/Schedule/index.js +97 -21
  69. data/webpack/JobWizard/steps/form/DateTimePicker.js +41 -8
  70. data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
  71. data/webpack/JobWizard/steps/form/Formatter.js +39 -8
  72. data/webpack/JobWizard/steps/form/NumberInput.js +3 -2
  73. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  74. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  75. data/webpack/JobWizard/steps/form/SelectField.js +14 -3
  76. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  77. data/webpack/JobWizard/submit.js +120 -0
  78. data/webpack/JobWizard/validation.js +53 -0
  79. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  80. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  81. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  82. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  83. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  84. data/webpack/helpers.js +1 -0
  85. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +1 -1
  86. metadata +38 -6
  87. data/app/models/setting/remote_execution.rb +0 -94
  88. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
  89. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js +0 -37
  90. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
@@ -3,12 +3,16 @@ import { useSelector } from 'react-redux';
3
3
  import { FormGroup, TextInput, TextArea } from '@patternfly/react-core';
4
4
  import PropTypes from 'prop-types';
5
5
  import SearchBar from 'foremanReact/components/SearchBar';
6
+ import { getControllerSearchProps } from 'foremanReact/constants';
6
7
  import { helpLabel } from './FormHelpers';
7
8
  import { SelectField } from './SelectField';
9
+ import { ResourceSelectAPI } from './ResourceSelect';
10
+ import { noop } from '../../../helpers';
8
11
 
9
12
  const TemplateSearchField = ({
10
13
  name,
11
14
  controller,
15
+ url,
12
16
  labelText,
13
17
  required,
14
18
  defaultValue,
@@ -23,6 +27,7 @@ const TemplateSearchField = ({
23
27
  // eslint-disable-next-line react-hooks/exhaustive-deps
24
28
  }, [searchQuery]);
25
29
  const id = name.replace(/ /g, '-');
30
+ const props = getControllerSearchProps(controller.replace('/', '_'), name);
26
31
  return (
27
32
  <FormGroup
28
33
  label={name}
@@ -34,14 +39,15 @@ const TemplateSearchField = ({
34
39
  <SearchBar
35
40
  initialQuery={defaultValue}
36
41
  data={{
37
- controller,
42
+ ...props,
38
43
  autocomplete: {
39
44
  id: name,
40
- url: `/${controller}/auto_complete_search`,
45
+ url,
41
46
  useKeyShortcuts: true,
42
47
  },
48
+ bookmarks: null,
43
49
  }}
44
- onSearch={() => null}
50
+ onSearch={noop}
45
51
  />
46
52
  </FormGroup>
47
53
  );
@@ -52,15 +58,39 @@ export const formatter = (input, values, setValue) => {
52
58
  const inputType = input.value_type;
53
59
  const isTextType = inputType === 'plain' || !inputType; // null defaults to plain
54
60
 
55
- const { name, required, hidden_value: hidden } = input;
61
+ const {
62
+ name,
63
+ required,
64
+ hidden_value: hidden,
65
+ resource_type: resourceType,
66
+ value_type: valueType,
67
+ } = input;
56
68
  const labelText = input.description;
57
69
  const value = values[name];
58
70
  const id = name.replace(/ /g, '-');
71
+ if (valueType === 'resource') {
72
+ return (
73
+ <FormGroup
74
+ label={name}
75
+ fieldId={id}
76
+ labelIcon={helpLabel(labelText, name)}
77
+ isRequired={required}
78
+ key={id}
79
+ >
80
+ <ResourceSelectAPI
81
+ name={name}
82
+ apiKey={resourceType.replace('::', '')}
83
+ url={`/ui_job_wizard/resources?resource=${resourceType}`}
84
+ selected={value || {}}
85
+ setSelected={newValue => setValue({ ...values, [name]: newValue })}
86
+ />
87
+ </FormGroup>
88
+ );
89
+ }
59
90
  if (isSelectType) {
60
91
  const options = input.options.split(/\r?\n/).map(option => option.trim());
61
92
  return (
62
93
  <SelectField
63
- aria-label={name}
64
94
  key={id}
65
95
  isRequired={required}
66
96
  label={name}
@@ -116,14 +146,14 @@ export const formatter = (input, values, setValue) => {
116
146
  );
117
147
  }
118
148
  if (inputType === 'search') {
119
- const controller = input.resource_type;
120
- // TODO: get text from redux autocomplete
149
+ const controller = input.resource_type_tableize;
121
150
  return (
122
151
  <TemplateSearchField
123
152
  key={id}
124
153
  name={name}
125
154
  defaultValue={value}
126
- controller={controller}
155
+ controller={resourceType}
156
+ url={`/${controller}/auto_complete_search`}
127
157
  labelText={labelText}
128
158
  required={required}
129
159
  setValue={setValue}
@@ -138,6 +168,7 @@ export const formatter = (input, values, setValue) => {
138
168
  TemplateSearchField.propTypes = {
139
169
  name: PropTypes.string.isRequired,
140
170
  controller: PropTypes.string.isRequired,
171
+ url: PropTypes.string.isRequired,
141
172
  labelText: PropTypes.string,
142
173
  required: PropTypes.bool.isRequired,
143
174
  defaultValue: PropTypes.string,
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { FormGroup, TextInput, ValidatedOptions } from '@patternfly/react-core';
4
4
  import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { isPositiveNumber } from './FormHelpers';
5
6
 
6
7
  export const NumberInput = ({ formProps, inputProps }) => {
7
8
  const [validated, setValidated] = useState();
@@ -9,7 +10,7 @@ export const NumberInput = ({ formProps, inputProps }) => {
9
10
  return (
10
11
  <FormGroup
11
12
  {...formProps}
12
- helperTextInvalid={__('Has to be a number')}
13
+ helperTextInvalid={__('Has to be a positive number')}
13
14
  validated={validated}
14
15
  >
15
16
  <TextInput
@@ -18,7 +19,7 @@ export const NumberInput = ({ formProps, inputProps }) => {
18
19
  {...inputProps}
19
20
  onChange={newValue => {
20
21
  setValidated(
21
- /^\d+$/.test(newValue) || newValue === ''
22
+ isPositiveNumber(newValue) || newValue === ''
22
23
  ? ValidatedOptions.noval
23
24
  : ValidatedOptions.error
24
25
  );
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import { useSelector, useDispatch } from 'react-redux';
3
+ import URI from 'urijs';
4
+ import { get } from 'foremanReact/redux/API';
5
+ import { selectResponse, selectIsLoading } from '../../JobWizardSelectors';
6
+ import { SearchSelect } from '../form/SearchSelect';
7
+
8
+ export const useNameSearchAPI = (apiKey, url) => {
9
+ const dispatch = useDispatch();
10
+ const uri = new URI(url);
11
+ const onSearch = search => {
12
+ dispatch(
13
+ get({
14
+ key: apiKey,
15
+ url: uri.addSearch({
16
+ name: search,
17
+ }),
18
+ })
19
+ );
20
+ };
21
+
22
+ const response = useSelector(state => selectResponse(state, apiKey));
23
+ const isLoading = useSelector(state => selectIsLoading(state, apiKey));
24
+ return [onSearch, response, isLoading];
25
+ };
26
+
27
+ export const ResourceSelectAPI = props => (
28
+ <SearchSelect {...props} useNameSearch={useNameSearchAPI} />
29
+ );
@@ -0,0 +1,121 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Select, SelectOption, SelectVariant } from '@patternfly/react-core';
4
+ import Immutable from 'seamless-immutable';
5
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
6
+
7
+ export const maxResults = 100;
8
+
9
+ export const SearchSelect = ({
10
+ name,
11
+ selected,
12
+ setSelected,
13
+ placeholderText,
14
+ useNameSearch,
15
+ apiKey,
16
+ url,
17
+ variant,
18
+ }) => {
19
+ const [onSearch, response, isLoading] = useNameSearch(apiKey, url);
20
+ const [isOpen, setIsOpen] = useState(false);
21
+ const [typingTimeout, setTypingTimeout] = useState(null);
22
+ const autoSearchDelay = 500;
23
+ useEffect(() => {
24
+ onSearch(selected.name || '');
25
+ if (typingTimeout) {
26
+ return () => clearTimeout(typingTimeout);
27
+ }
28
+ return undefined;
29
+ // eslint-disable-next-line react-hooks/exhaustive-deps
30
+ }, []);
31
+ let selectOptions = [];
32
+ if (response.subtotal > maxResults) {
33
+ selectOptions = [
34
+ <SelectOption
35
+ isDisabled
36
+ key={0}
37
+ description={__('Please refine your search.')}
38
+ >
39
+ {sprintf(
40
+ __('You have %s results to display. Showing first %s results'),
41
+ response.subtotal,
42
+ maxResults
43
+ )}
44
+ </SelectOption>,
45
+ ];
46
+ }
47
+ selectOptions = [
48
+ ...selectOptions,
49
+ ...Immutable.asMutable(response?.results || [])?.map((result, index) => (
50
+ <SelectOption key={index + 1} value={result.id}>
51
+ {result.name}
52
+ </SelectOption>
53
+ )),
54
+ ];
55
+
56
+ const onSelect = (event, selection) => {
57
+ if (variant === SelectVariant.typeahead) {
58
+ setSelected(response.results.find(r => r.id === selection)); // saving the name and id so we will have access to the name between steps
59
+ } else if (variant === SelectVariant.typeaheadMulti) {
60
+ if (selected.map(({ id }) => id).includes(selection)) {
61
+ setSelected(currentSelected =>
62
+ currentSelected.filter(({ id }) => id !== selection)
63
+ );
64
+ } else {
65
+ setSelected(currentSelected => [
66
+ ...currentSelected,
67
+ response.results.find(r => r.id === selection),
68
+ ]);
69
+ }
70
+ }
71
+ };
72
+ const autoSearch = searchTerm => {
73
+ if (typingTimeout) clearTimeout(typingTimeout);
74
+ setTypingTimeout(setTimeout(() => onSearch(searchTerm), autoSearchDelay));
75
+ };
76
+ return (
77
+ <Select
78
+ toggleAriaLabel={`${name} toggle`}
79
+ chipGroupComponent={<></>}
80
+ variant={variant}
81
+ selections={
82
+ variant === SelectVariant.typeahead
83
+ ? selected.id
84
+ : selected.map(({ id }) => id)
85
+ }
86
+ loadingVariant={isLoading ? 'spinner' : null}
87
+ onSelect={onSelect}
88
+ onToggle={setIsOpen}
89
+ isOpen={isOpen}
90
+ className="without_select2"
91
+ maxHeight="45vh"
92
+ onTypeaheadInputChanged={value => {
93
+ autoSearch(value || '');
94
+ }}
95
+ placeholderText={placeholderText}
96
+ onFilter={() => null} // https://github.com/patternfly/patternfly-react/issues/6321
97
+ typeAheadAriaLabel={`${name} typeahead input`}
98
+ >
99
+ {selectOptions}
100
+ </Select>
101
+ );
102
+ };
103
+
104
+ SearchSelect.propTypes = {
105
+ name: PropTypes.string,
106
+ selected: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
107
+ setSelected: PropTypes.func.isRequired,
108
+ placeholderText: PropTypes.string,
109
+ apiKey: PropTypes.string.isRequired,
110
+ url: PropTypes.string,
111
+ useNameSearch: PropTypes.func.isRequired,
112
+ variant: PropTypes.string,
113
+ };
114
+
115
+ SearchSelect.defaultProps = {
116
+ name: 'typeahead select',
117
+ selected: {},
118
+ placeholderText: '',
119
+ url: '',
120
+ variant: SelectVariant.typeahead,
121
+ };
@@ -17,6 +17,16 @@ export const SelectField = ({
17
17
  setIsOpen(false);
18
18
  };
19
19
  const [isOpen, setIsOpen] = useState(false);
20
+ const selectOptions = [
21
+ !isRequired && (
22
+ <SelectOption key={0} value="">
23
+ <p> </p>
24
+ </SelectOption>
25
+ ),
26
+ ...options.map((option, index) => (
27
+ <SelectOption key={index + 1} value={option} />
28
+ )),
29
+ ];
20
30
  return (
21
31
  <FormGroup
22
32
  label={label}
@@ -32,11 +42,12 @@ export const SelectField = ({
32
42
  className="without_select2"
33
43
  maxHeight="45vh"
34
44
  menuAppendTo={() => document.body}
45
+ placeholderText=" " // To prevent showing first option as selected
46
+ aria-labelledby={fieldId}
47
+ toggleAriaLabel={`${label} toggle`}
35
48
  {...props}
36
49
  >
37
- {options.map((option, index) => (
38
- <SelectOption key={index} value={option} />
39
- ))}
50
+ {selectOptions.filter(option => option)}
40
51
  </Select>
41
52
  </FormGroup>
42
53
  );
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, screen } from '@testing-library/react';
3
+ import { SearchSelect } from '../SearchSelect';
4
+
5
+ const apiKey = 'HOSTS_KEY';
6
+
7
+ describe('SearchSelect', () => {
8
+ it('too many', () => {
9
+ const onSearch = jest.fn();
10
+ render(
11
+ <SearchSelect
12
+ selected={['hosts1,host2']}
13
+ setSelected={jest.fn()}
14
+ apiKey={apiKey}
15
+ url="/hosts"
16
+ placeholderText="Test hosts"
17
+ useNameSearch={() => [
18
+ onSearch,
19
+ { results: ['host1', 'host2', 'host3'], subtotal: 101 },
20
+ false,
21
+ ]}
22
+ />
23
+ );
24
+ const openSelectbutton = screen.getByRole('button', {
25
+ name: 'typeahead select toggle',
26
+ });
27
+ fireEvent.click(openSelectbutton);
28
+ const tooMany = screen.queryAllByText(
29
+ 'You have %s results to display. Showing first %s results + 101 + 100'
30
+ );
31
+ expect(tooMany).toHaveLength(1);
32
+ });
33
+ });
@@ -0,0 +1,120 @@
1
+ import { post } from 'foremanReact/redux/API';
2
+ import { repeatTypes, JOB_INVOCATION } from './JobWizardConstants';
3
+ import { buildHostQuery } from './steps/HostsAndInputs/buildHostQuery';
4
+
5
+ export const submit = ({
6
+ jobTemplateID,
7
+ templateValues,
8
+ advancedValues,
9
+ scheduleValue,
10
+ selectedTargets,
11
+ hostsSearchQuery,
12
+ location,
13
+ organization,
14
+ dispatch,
15
+ }) => {
16
+ const {
17
+ repeatAmount,
18
+ repeatType,
19
+ repeatData,
20
+ startsAt,
21
+ startsBefore,
22
+ ends,
23
+ purpose,
24
+ } = scheduleValue;
25
+ const {
26
+ effectiveUserValue,
27
+ effectiveUserPassword,
28
+ description,
29
+ timeoutToKill,
30
+ isRandomizedOrdering,
31
+ timeSpan,
32
+ concurrencyLevel,
33
+ templateValues: advancedTemplateValues,
34
+ password,
35
+ keyPassphrase,
36
+ } = advancedValues;
37
+ const getCronLine = () => {
38
+ const [hour, minute] = repeatData.at
39
+ ? repeatData.at.split(':')
40
+ : [null, null];
41
+ switch (repeatType) {
42
+ case repeatTypes.cronline:
43
+ return repeatData.cronline;
44
+ case repeatTypes.monthly:
45
+ return `${minute} ${hour} ${repeatData.days} * *`;
46
+ case repeatTypes.weekly:
47
+ return `${minute} ${hour} * * ${repeatData.daysOfWeek}`;
48
+ case repeatTypes.daily:
49
+ return `${minute} ${hour} * * *`;
50
+ case repeatTypes.hourly:
51
+ return `${repeatData.minute} * * * *`;
52
+ case repeatTypes.noRepeat:
53
+ default:
54
+ return null;
55
+ }
56
+ };
57
+ const api = {
58
+ location,
59
+ organization,
60
+ job_invocation: {
61
+ job_template_id: jobTemplateID,
62
+ targeting_type: scheduleValue?.isTypeStatic
63
+ ? 'static_query'
64
+ : 'dynamic_query',
65
+ randomized_ordering: isRandomizedOrdering,
66
+ inputs: { ...templateValues, ...advancedTemplateValues },
67
+ ssh: {
68
+ effective_user: effectiveUserValue,
69
+ effective_user_password: effectiveUserPassword,
70
+ },
71
+ password,
72
+ key_passphrase: keyPassphrase,
73
+ recurrence:
74
+ repeatType !== repeatTypes.noRepeat
75
+ ? {
76
+ cron_line: getCronLine(),
77
+ max_iteration: repeatAmount,
78
+ end_time: ends.length ? new Date(ends).toISOString() : null,
79
+ purpose,
80
+ }
81
+ : null,
82
+ scheduling:
83
+ startsAt?.length || startsBefore?.length
84
+ ? {
85
+ start_at: startsAt?.length
86
+ ? new Date(startsAt).toISOString()
87
+ : null,
88
+ start_before: startsBefore?.length
89
+ ? new Date(startsBefore).toISOString()
90
+ : null,
91
+ }
92
+ : null,
93
+ concurrency_control: {
94
+ time_span: timeSpan,
95
+ concurrency_level: concurrencyLevel,
96
+ },
97
+ bookmark_id: null,
98
+ search_query:
99
+ buildHostQuery(selectedTargets, hostsSearchQuery) || 'name ~ *',
100
+ description_format: description,
101
+ execution_timeout_interval: timeoutToKill,
102
+ feature: '', // TODO add value after https://github.com/theforeman/foreman_remote_execution/pull/629
103
+ },
104
+ };
105
+
106
+ dispatch(
107
+ post({
108
+ key: JOB_INVOCATION,
109
+ url: '/api/job_invocations',
110
+ params: api,
111
+ handleSuccess: ({ data: { id } }) => {
112
+ window.location.href = `/job_invocations/${id}`;
113
+ },
114
+ errorToast: ({ response }) =>
115
+ response?.date?.error?.message ||
116
+ response?.message ||
117
+ response?.statusText,
118
+ })
119
+ );
120
+ };
@@ -0,0 +1,53 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useSelector } from 'react-redux';
3
+ import {
4
+ selectTemplateInputs,
5
+ selectAdvancedTemplateInputs,
6
+ } from './JobWizardSelectors';
7
+ import { isPositiveNumber, isValidDate } from './steps/form/FormHelpers';
8
+ import './JobWizard.scss';
9
+
10
+ export const useValidation = ({ advancedValues, templateValues }) => {
11
+ const [valid, setValid] = useState({});
12
+ const templateInputs = useSelector(selectTemplateInputs);
13
+ const advancedTemplateInputs = useSelector(selectAdvancedTemplateInputs);
14
+ useEffect(() => {
15
+ setValid({
16
+ hostsAndInputs: true,
17
+ advanced: true,
18
+ schedule: true,
19
+ });
20
+ const inputValidation = (inputs, values, setInvalid) => {
21
+ inputs.forEach(({ name, required, value_type: valueType }) => {
22
+ const value = values[name];
23
+ if (required && !value) {
24
+ setInvalid();
25
+ }
26
+ if (value && valueType === 'date') {
27
+ if (!isValidDate(value) && !isValidDate(new Date(value))) {
28
+ setInvalid();
29
+ }
30
+ }
31
+ });
32
+ };
33
+ inputValidation(templateInputs, templateValues, () =>
34
+ setValid(currValid => ({ ...currValid, hostsAndInputs: false }))
35
+ );
36
+
37
+ inputValidation(advancedTemplateInputs, advancedValues.templateValues, () =>
38
+ setValid(currValid => ({ ...currValid, advanced: false }))
39
+ );
40
+ [
41
+ advancedValues.timeoutToKill,
42
+ advancedValues.concurrencyLevel,
43
+ advancedValues.timeSpan,
44
+ ].forEach(value => {
45
+ if (value && !isPositiveNumber(value)) {
46
+ setValid(currValid => ({ ...currValid, advanced: false }));
47
+ }
48
+ });
49
+
50
+ // eslint-disable-next-line react-hooks/exhaustive-deps
51
+ }, [advancedValues, templateValues]);
52
+ return [valid, setValid];
53
+ };
@@ -0,0 +1,2 @@
1
+ export const useForemanOrganization = () => ({ id: 1 });
2
+ export const useForemanLocation = () => ({ id: 2 });
@@ -1 +1,3 @@
1
+ export const sprintf = (string, ...args) => [string, ...args].join(' + ');
1
2
  export const translate = s => s;
3
+ export const documentLocale = () => 'en';
@@ -0,0 +1 @@
1
+ export const resetData = payload => ({ type: 'REST_DATA', payload });
@@ -0,0 +1 @@
1
+ export const TRIGGERS = { INPUT_CHANGE: 'INPUT_CHANGE' };
@@ -0,0 +1 @@
1
+ export const selectRouterLocation = jest.fn(() => ({}));
@@ -0,0 +1 @@
1
+ export const noop = Function.prototype;
@@ -21,7 +21,7 @@ const RecentJobsCard = ({ hostDetails: { name, id } }) => {
21
21
 
22
22
  return (
23
23
  <CardTemplate
24
- overrideGridProps={{ xl: 8, lg: 8, md: 12 }}
24
+ overrideGridProps={{ xl2: 6, xl: 8, lg: 8, md: 12 }}
25
25
  header={__('Recent jobs')}
26
26
  dropdownItems={[
27
27
  <DropdownItem