foreman_remote_execution 5.0.4 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/README.md +7 -0
  4. data/app/controllers/foreman_remote_execution/concerns/api/v2/registration_controller_extensions.rb +5 -0
  5. data/app/controllers/ui_job_wizard_controller.rb +13 -2
  6. data/app/helpers/remote_execution_helper.rb +1 -1
  7. data/app/lib/actions/remote_execution/run_host_job.rb +3 -1
  8. data/app/lib/foreman_remote_execution/renderer/scope/input.rb +1 -1
  9. data/app/mailers/rex_job_mailer.rb +1 -1
  10. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +4 -0
  11. data/app/models/job_invocation.rb +2 -0
  12. data/app/models/job_template.rb +3 -1
  13. data/app/models/remote_execution_provider.rb +10 -1
  14. data/app/models/ssh_execution_provider.rb +8 -2
  15. data/app/views/api/v2/host/main.rabl +1 -0
  16. data/app/views/job_templates/_custom_tabs.html.erb +4 -9
  17. data/app/views/template_invocations/show.html.erb +7 -1
  18. data/app/views/templates/{ssh → script}/check_update.erb +2 -2
  19. data/app/views/templates/{ssh → script}/module_action.erb +2 -2
  20. data/app/views/templates/{ssh → script}/package_action.erb +2 -2
  21. data/app/views/templates/{ssh → script}/power_action.erb +2 -2
  22. data/app/views/templates/{ssh → script}/puppet_agent_disable.erb +2 -2
  23. data/app/views/templates/{ssh → script}/puppet_agent_enable.erb +2 -2
  24. data/app/views/templates/{ssh → script}/puppet_install_modules_from_forge.erb +2 -2
  25. data/app/views/templates/{ssh → script}/puppet_install_modules_from_git.erb +2 -2
  26. data/app/views/templates/{ssh → script}/puppet_run_once.erb +2 -2
  27. data/app/views/templates/{ssh → script}/run_command.erb +2 -2
  28. data/app/views/templates/{ssh → script}/service_action.erb +2 -2
  29. data/db/migrate/20220321101835_rename_ssh_provider_to_script.rb +29 -0
  30. data/jsconfig.json +8 -0
  31. data/lib/foreman_remote_execution/engine.rb +6 -4
  32. data/lib/foreman_remote_execution/version.rb +1 -1
  33. data/locale/action_names.rb +3 -4
  34. data/locale/de/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  35. data/locale/de/foreman_remote_execution.po +356 -20
  36. data/locale/en/foreman_remote_execution.po +356 -20
  37. data/locale/en_GB/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  38. data/locale/en_GB/foreman_remote_execution.po +356 -20
  39. data/locale/es/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  40. data/locale/es/foreman_remote_execution.po +357 -21
  41. data/locale/foreman_remote_execution.pot +808 -296
  42. data/locale/fr/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  43. data/locale/fr/foreman_remote_execution.po +357 -21
  44. data/locale/ja/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  45. data/locale/ja/foreman_remote_execution.po +357 -21
  46. data/locale/ko/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  47. data/locale/ko/foreman_remote_execution.po +356 -20
  48. data/locale/pt_BR/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  49. data/locale/pt_BR/foreman_remote_execution.po +357 -21
  50. data/locale/ru/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  51. data/locale/ru/foreman_remote_execution.po +356 -20
  52. data/locale/zh_CN/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  53. data/locale/zh_CN/foreman_remote_execution.po +357 -21
  54. data/locale/zh_TW/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  55. data/locale/zh_TW/foreman_remote_execution.po +356 -20
  56. data/package.json +6 -7
  57. data/test/unit/job_invocation_report_template_test.rb +1 -1
  58. data/test/unit/remote_execution_provider_test.rb +2 -0
  59. data/test/unit/targeting_test.rb +1 -1
  60. data/webpack/JobWizard/JobWizard.js +30 -7
  61. data/webpack/JobWizard/JobWizard.scss +5 -0
  62. data/webpack/JobWizard/JobWizardConstants.js +2 -1
  63. data/webpack/JobWizard/__tests__/fixtures.js +13 -2
  64. data/webpack/JobWizard/__tests__/integration.test.js +6 -1
  65. data/webpack/JobWizard/autofill.js +34 -9
  66. data/webpack/JobWizard/index.js +0 -7
  67. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +24 -15
  68. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +25 -7
  69. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +1 -1
  70. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +28 -0
  71. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +1 -1
  72. data/webpack/JobWizard/steps/form/Formatter.js +18 -3
  73. data/webpack/JobWizard/steps/form/GroupedSelectField.js +7 -1
  74. data/webpack/__mocks__/foremanReact/components/Pagination.js +2 -0
  75. data/webpack/__mocks__/foremanReact/constants.js +1 -0
  76. data/webpack/global_index.js +4 -0
  77. data/webpack/react_app/components/FeaturesDropdown/actions.js +13 -0
  78. data/webpack/react_app/components/FeaturesDropdown/constant.js +2 -0
  79. data/webpack/react_app/components/FeaturesDropdown/index.js +74 -0
  80. data/webpack/react_app/components/HostKebab/KebabItems.js +22 -0
  81. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +6 -1
  82. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +2 -2
  83. data/webpack/react_app/components/RecentJobsCard/constants.js +1 -0
  84. data/webpack/react_app/components/TargetingHosts/TargetingHostsHelpers.js +1 -1
  85. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +1 -6
  86. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -9
  87. data/webpack/react_app/components/TargetingHosts/index.js +2 -3
  88. data/webpack/react_app/extend/fillKebabItems.js +11 -0
  89. data/webpack/react_app/extend/fillRexFeaturesDropdown.js +11 -0
  90. metadata +24 -16
  91. data/app/views/templates/README.md +0 -6
  92. data/webpack/__mocks__/foremanReact/components/Pagination/PaginationWrapper.js +0 -2
data/package.json CHANGED
@@ -21,20 +21,19 @@
21
21
  },
22
22
  "devDependencies": {
23
23
  "@babel/core": "^7.7.0",
24
- "@theforeman/builder": "^8.16.0",
25
- "@theforeman/eslint-plugin-foreman": "^8.16.0",
26
- "@theforeman/stories": "^8.16.0",
27
- "@theforeman/test": "^8.16.0",
28
- "@theforeman/vendor-dev": "^8.16.0",
24
+ "@theforeman/builder": "^10.1.0",
25
+ "@theforeman/eslint-plugin-foreman": "^10.1.0",
26
+ "@theforeman/stories": "^10.1.0",
27
+ "@theforeman/test": "^10.1.0",
28
+ "@theforeman/vendor-dev": "^10.1.0",
29
29
  "babel-eslint": "^10.0.0",
30
30
  "eslint": "^6.8.0",
31
31
  "prettier": "^1.19.1",
32
- "@patternfly/react-catalog-view-extension": "^4.8.126",
33
32
  "redux-mock-store": "^1.2.2",
34
33
  "graphql-tag": "^2.11.0",
35
34
  "graphql": "^15.5.0"
36
35
  },
37
36
  "peerDependencies": {
38
- "@theforeman/vendor": "^8.16.0"
37
+ "@theforeman/vendor": "^10.1.0"
39
38
  }
40
39
  }
@@ -9,7 +9,7 @@ class JobReportTemplateTest < ActiveSupport::TestCase
9
9
 
10
10
  context 'with valid job invocation report template' do
11
11
  let(:job_invocation_template) do
12
- file_path = File.read(File.expand_path(Rails.root + "app/views/unattended/report_templates/jobs_-_invocation_report_template.erb"))
12
+ file_path = File.read(File.expand_path(Rails.root + "app/views/unattended/report_templates/job_invocation_-_report_template.erb"))
13
13
  template = ReportTemplate.import_without_save("Job Invocation Report Template", file_path)
14
14
  template.save!
15
15
  template
@@ -6,9 +6,11 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
6
6
  it { _(providers).must_be_kind_of HashWithIndifferentAccess }
7
7
  it 'makes providers accessible using symbol' do
8
8
  _(providers[:SSH]).must_equal SSHExecutionProvider
9
+ _(providers[:script]).must_equal ScriptExecutionProvider
9
10
  end
10
11
  it 'makes providers accessible using string' do
11
12
  _(providers['SSH']).must_equal SSHExecutionProvider
13
+ _(providers['script']).must_equal ScriptExecutionProvider
12
14
  end
13
15
  end
14
16
 
@@ -61,7 +61,7 @@ class TargetingTest < ActiveSupport::TestCase
61
61
  users(:one).destroy
62
62
  end
63
63
 
64
- it { assert targeting.reload.user.nil? }
64
+ it { assert_nil targeting.reload.user }
65
65
  it do
66
66
  -> { targeting.resolve_hosts! }.must_raise(Foreman::Exception)
67
67
  end
@@ -20,6 +20,8 @@ import {
20
20
  selectTemplateError,
21
21
  selectJobTemplate,
22
22
  selectIsSubmitting,
23
+ selectRouterSearch,
24
+ selectIsLoading,
23
25
  } from './JobWizardSelectors';
24
26
  import Schedule from './steps/Schedule/';
25
27
  import HostsAndInputs from './steps/HostsAndInputs/';
@@ -41,6 +43,7 @@ export const JobWizard = () => {
41
43
  hostGroups: [],
42
44
  });
43
45
  const [hostsSearchQuery, setHostsSearchQuery] = useState('');
46
+ const [fills, setFills] = useState(useSelector(selectRouterSearch));
44
47
  const dispatch = useDispatch();
45
48
 
46
49
  const setDefaults = useCallback(
@@ -49,17 +52,26 @@ export const JobWizard = () => {
49
52
  template_inputs,
50
53
  advanced_template_inputs,
51
54
  effective_user,
52
- job_template: { name, execution_timeout_interval, description_format },
55
+ job_template: {
56
+ name,
57
+ execution_timeout_interval,
58
+ description_format,
59
+ job_category,
60
+ },
53
61
  },
54
62
  }) => {
63
+ if (!category.length) {
64
+ setCategory(current => (current.length ? current : job_category));
65
+ }
55
66
  const advancedTemplateValues = {};
56
67
  const defaultTemplateValues = {};
57
68
  const inputs = template_inputs;
58
69
  const advancedInputs = advanced_template_inputs;
59
70
  if (inputs) {
60
- setTemplateValues(() => {
71
+ setTemplateValues(prev => {
61
72
  inputs.forEach(input => {
62
- defaultTemplateValues[input.name] = input?.default || '';
73
+ defaultTemplateValues[input.name] =
74
+ prev[input.name] || input?.default || '';
63
75
  });
64
76
  return defaultTemplateValues;
65
77
  });
@@ -67,7 +79,8 @@ export const JobWizard = () => {
67
79
  setAdvancedValues(currentAdvancedValues => {
68
80
  if (advancedInputs) {
69
81
  advancedInputs.forEach(input => {
70
- advancedTemplateValues[input.name] = input?.default || '';
82
+ advancedTemplateValues[input.name] =
83
+ currentAdvancedValues[input.name] || input?.default || '';
71
84
  });
72
85
  }
73
86
  const generateDefaultDescription = () => {
@@ -89,7 +102,7 @@ export const JobWizard = () => {
89
102
  };
90
103
  });
91
104
  },
92
- []
105
+ [category.length]
93
106
  );
94
107
  useEffect(() => {
95
108
  if (jobTemplateID) {
@@ -108,15 +121,24 @@ export const JobWizard = () => {
108
121
  templateValues,
109
122
  });
110
123
  useAutoFill({
124
+ fills,
125
+ setFills,
111
126
  setSelectedTargets,
112
127
  setHostsSearchQuery,
128
+ setJobTemplateID,
129
+ setTemplateValues,
113
130
  });
114
131
  const templateError = !!useSelector(selectTemplateError);
115
132
  const templateResponse = useSelector(selectJobTemplate);
116
133
  const isSubmitting = useSelector(selectIsSubmitting);
134
+ const isTemplatesLoading = useSelector(state =>
135
+ selectIsLoading(state, JOB_TEMPLATE)
136
+ );
117
137
  const isTemplate =
118
- !templateError && !!jobTemplateID && templateResponse.job_template;
119
-
138
+ !isTemplatesLoading &&
139
+ !templateError &&
140
+ !!jobTemplateID &&
141
+ templateResponse.job_template;
120
142
  const steps = [
121
143
  {
122
144
  name: WIZARD_TITLES.categoryAndTemplate,
@@ -126,6 +148,7 @@ export const JobWizard = () => {
126
148
  setJobTemplate={setJobTemplateID}
127
149
  category={category}
128
150
  setCategory={setCategory}
151
+ isFeature={!!fills.feature}
129
152
  />
130
153
  ),
131
154
  enableNext: isTemplate,
@@ -103,4 +103,9 @@
103
103
  margin-left: 10px;
104
104
  }
105
105
  }
106
+ .foreman-search-field {
107
+ .autocomplete-search-btn {
108
+ display: none;
109
+ }
110
+ }
106
111
  }
@@ -47,7 +47,7 @@ export const hostMethods = {
47
47
  searchQuery: __('Search query'),
48
48
  };
49
49
 
50
- export const hostQuerySearchID = 'searchBar'; // until https://projects.theforeman.org/issues/33737 is used
50
+ export const hostQuerySearchID = 'mainHostQuery';
51
51
  export const hostsController = 'hosts';
52
52
 
53
53
  export const dataName = {
@@ -58,3 +58,4 @@ export const HOSTS_TO_PREVIEW_AMOUNT = 20;
58
58
 
59
59
  export const DEBOUNCE_HOST_COUNT = 700;
60
60
  export const HOST_IDS = 'HOST_IDS';
61
+ export const REX_FEATURE = 'REX_FEATURE';
@@ -69,6 +69,7 @@ export const jobTemplateResponse = {
69
69
  default: '',
70
70
  hidden_value: false,
71
71
  url: 'foreman_tasks/tasks',
72
+ resource_type_tableize: 'hosts',
72
73
  },
73
74
  {
74
75
  name: 'adv date',
@@ -96,7 +97,7 @@ export const jobTemplateResponse = {
96
97
  ],
97
98
  };
98
99
 
99
- export const jobCategories = ['Ansible Commands', 'Puppet', 'Services'];
100
+ export const jobCategories = ['Services', 'Ansible Commands', 'Puppet'];
100
101
 
101
102
  export const testSetup = (selectors, api) => {
102
103
  jest.spyOn(api, 'get');
@@ -149,7 +150,12 @@ export const mockApi = api => {
149
150
  api.get.mockImplementation(({ handleSuccess, ...action }) => {
150
151
  if (action.key === 'JOB_CATEGORIES') {
151
152
  handleSuccess &&
152
- handleSuccess({ data: { job_categories: jobCategories } });
153
+ handleSuccess({
154
+ data: {
155
+ job_categories: jobCategories,
156
+ default_category: 'Ansible Commands',
157
+ },
158
+ });
153
159
  } else if (action.key === 'JOB_TEMPLATE') {
154
160
  handleSuccess &&
155
161
  handleSuccess({
@@ -165,6 +171,11 @@ export const mockApi = api => {
165
171
  handleSuccess({
166
172
  data: { results: [{ name: 'host1' }, { name: 'host3' }] },
167
173
  });
174
+ } else if (action.key === 'REX_FEATURE') {
175
+ handleSuccess &&
176
+ handleSuccess({
177
+ data: { job_template_id: 178 },
178
+ });
168
179
  }
169
180
  return { type: 'get', ...action };
170
181
  });
@@ -22,7 +22,12 @@ describe('Job wizard fill', () => {
22
22
  api.get.mockImplementation(({ handleSuccess, ...action }) => {
23
23
  if (action.key === 'JOB_CATEGORIES') {
24
24
  handleSuccess &&
25
- handleSuccess({ data: { job_categories: jobCategories } });
25
+ handleSuccess({
26
+ data: {
27
+ job_categories: jobCategories,
28
+ default_category: 'Ansible Commands',
29
+ },
30
+ });
26
31
  } else if (action.key === 'JOB_TEMPLATE') {
27
32
  handleSuccess &&
28
33
  handleSuccess({
@@ -1,22 +1,29 @@
1
1
  import { useEffect } from 'react';
2
- import { useDispatch, useSelector } from 'react-redux';
2
+ import { useDispatch } from 'react-redux';
3
3
  import { get } from 'foremanReact/redux/API';
4
- import { HOST_IDS } from './JobWizardConstants';
5
- import { selectRouterSearch } from './JobWizardSelectors';
4
+ import { HOST_IDS, REX_FEATURE } from './JobWizardConstants';
6
5
  import './JobWizard.scss';
7
6
 
8
- export const useAutoFill = ({ setSelectedTargets, setHostsSearchQuery }) => {
9
- const fills = useSelector(selectRouterSearch);
7
+ export const useAutoFill = ({
8
+ fills,
9
+ setFills,
10
+ setSelectedTargets,
11
+ setHostsSearchQuery,
12
+ setJobTemplateID,
13
+ setTemplateValues,
14
+ }) => {
10
15
  const dispatch = useDispatch();
11
16
 
12
17
  useEffect(() => {
13
18
  if (Object.keys(fills).length) {
14
- if (fills['host_ids[]']) {
19
+ const { 'host_ids[]': hostIds, search, feature, ...rest } = { ...fills };
20
+ setFills({});
21
+ if (hostIds) {
15
22
  dispatch(
16
23
  get({
17
24
  key: HOST_IDS,
18
25
  url: '/api/hosts',
19
- params: { search: `id = ${fills['host_ids[]'].join(' or id = ')}` },
26
+ params: { search: `id = ${hostIds.join(' or id = ')}` },
20
27
  handleSuccess: ({ data }) => {
21
28
  setSelectedTargets(currentTargets => ({
22
29
  ...currentTargets,
@@ -29,8 +36,26 @@ export const useAutoFill = ({ setSelectedTargets, setHostsSearchQuery }) => {
29
36
  })
30
37
  );
31
38
  }
32
- if (fills.search) {
33
- setHostsSearchQuery(fills.search);
39
+ if (search) {
40
+ setHostsSearchQuery(search);
41
+ }
42
+ if (feature) {
43
+ dispatch(
44
+ get({
45
+ key: REX_FEATURE,
46
+ url: `/api/remote_execution_features/${feature}`,
47
+ handleSuccess: ({ data }) => {
48
+ setJobTemplateID(data.job_template_id);
49
+ },
50
+ })
51
+ );
52
+ Object.keys(rest).forEach(key => {
53
+ const re = /inputs\[(?<input>.*)\]/g;
54
+ const input = re.exec(key)?.groups?.input;
55
+ if (input) {
56
+ setTemplateValues(prev => ({ ...prev, [input]: rest[key] }));
57
+ }
58
+ });
34
59
  }
35
60
  }
36
61
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -1,5 +1,4 @@
1
1
  import React from 'react';
2
- import PropTypes from 'prop-types';
3
2
  import { Title, Divider } from '@patternfly/react-core';
4
3
  import { translate as __ } from 'foremanReact/common/I18n';
5
4
  import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
@@ -30,10 +29,4 @@ const JobWizardPage = () => {
30
29
  );
31
30
  };
32
31
 
33
- JobWizardPage.propTypes = {
34
- location: PropTypes.shape({
35
- search: PropTypes.string,
36
- }).isRequired,
37
- };
38
-
39
32
  export default JobWizardPage;
@@ -1,11 +1,13 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
+ import { useSelector } from 'react-redux';
3
4
  import { Text, TextVariants, Form, Alert } from '@patternfly/react-core';
4
5
  import { translate as __ } from 'foremanReact/common/I18n';
5
6
  import { SelectField } from '../form/SelectField';
6
7
  import { GroupedSelectField } from '../form/GroupedSelectField';
7
8
  import { WizardTitle } from '../form/WizardTitle';
8
- import { WIZARD_TITLES } from '../../JobWizardConstants';
9
+ import { WIZARD_TITLES, JOB_TEMPLATES } from '../../JobWizardConstants';
10
+ import { selectIsLoading } from '../../JobWizardSelectors';
9
11
 
10
12
  export const CategoryAndTemplate = ({
11
13
  jobCategories,
@@ -17,18 +19,23 @@ export const CategoryAndTemplate = ({
17
19
  errors,
18
20
  }) => {
19
21
  const templatesGroups = {};
20
- jobTemplates.forEach(template => {
21
- if (templatesGroups[template.provider_type]?.options)
22
- templatesGroups[template.provider_type].options.push({
23
- label: template.name,
24
- value: template.id,
25
- });
26
- else
27
- templatesGroups[template.provider_type] = {
28
- options: [{ label: template.name, value: template.id }],
29
- groupLabel: template.provider_type,
30
- };
31
- });
22
+ const isTemplatesLoading = useSelector(state =>
23
+ selectIsLoading(state, JOB_TEMPLATES)
24
+ );
25
+ if (!isTemplatesLoading) {
26
+ jobTemplates.forEach(template => {
27
+ if (templatesGroups[template.provider_type]?.options)
28
+ templatesGroups[template.provider_type].options.push({
29
+ label: template.name,
30
+ value: template.id,
31
+ });
32
+ else
33
+ templatesGroups[template.provider_type] = {
34
+ options: [{ label: template.name, value: template.id }],
35
+ groupLabel: template.provider_type,
36
+ };
37
+ });
38
+ }
32
39
 
33
40
  const selectedTemplate = jobTemplates.find(
34
41
  template => template.id === selectedTemplateID
@@ -60,8 +67,10 @@ export const CategoryAndTemplate = ({
60
67
  fieldId="job_template"
61
68
  groups={Object.values(templatesGroups)}
62
69
  setSelected={setJobTemplate}
63
- selected={selectedTemplate}
64
- isDisabled={!!(categoryError || allTemplatesError)}
70
+ selected={isTemplatesLoading ? [] : selectedTemplate}
71
+ isDisabled={
72
+ !!(categoryError || allTemplatesError || isTemplatesLoading)
73
+ }
65
74
  placeholderText={allTemplatesError ? __('Error') : ''}
66
75
  />
67
76
  {isError && (
@@ -25,6 +25,7 @@ const ConnectedCategoryAndTemplate = ({
25
25
  setJobTemplate,
26
26
  category,
27
27
  setCategory,
28
+ isFeature,
28
29
  }) => {
29
30
  const dispatch = useDispatch();
30
31
 
@@ -36,12 +37,23 @@ const ConnectedCategoryAndTemplate = ({
36
37
  get({
37
38
  key: JOB_CATEGORIES,
38
39
  url: '/ui_job_wizard/categories',
39
- handleSuccess: response =>
40
- setCategory(response.data.job_categories[0] || ''),
40
+ handleSuccess: ({
41
+ data: {
42
+ default_category: defaultCategory,
43
+ job_categories: jobCategories,
44
+ default_template: defaultTemplate,
45
+ },
46
+ }) => {
47
+ if (!isFeature) {
48
+ setCategory(defaultCategory || jobCategories[0] || '');
49
+ if (defaultTemplate) setJobTemplate(defaultTemplate);
50
+ }
51
+ },
41
52
  })
42
53
  );
43
54
  }
44
- }, [jobCategoriesStatus, dispatch, setCategory]);
55
+ // eslint-disable-next-line react-hooks/exhaustive-deps
56
+ }, [jobCategoriesStatus]);
45
57
 
46
58
  const jobCategories = useSelector(selectJobCategories);
47
59
  useEffect(() => {
@@ -55,14 +67,19 @@ const ConnectedCategoryAndTemplate = ({
55
67
  per_page: 'all',
56
68
  }),
57
69
  handleSuccess: response => {
58
- setJobTemplate(
59
- Number(filterJobTemplates(response?.data?.results)[0]?.id) || null
60
- );
70
+ if (!jobTemplate)
71
+ setJobTemplate(
72
+ current =>
73
+ current ||
74
+ Number(filterJobTemplates(response?.data?.results)[0]?.id) ||
75
+ null
76
+ );
61
77
  },
62
78
  })
63
79
  );
64
80
  }
65
- }, [category, dispatch, setJobTemplate]);
81
+ // eslint-disable-next-line react-hooks/exhaustive-deps
82
+ }, [category, dispatch]);
66
83
 
67
84
  const jobTemplates = useSelector(selectJobTemplates);
68
85
 
@@ -89,6 +106,7 @@ ConnectedCategoryAndTemplate.propTypes = {
89
106
  setJobTemplate: PropTypes.func.isRequired,
90
107
  category: PropTypes.string.isRequired,
91
108
  setCategory: PropTypes.func.isRequired,
109
+ isFeature: PropTypes.bool.isRequired,
92
110
  };
93
111
  ConnectedCategoryAndTemplate.defaultProps = { jobTemplate: null };
94
112
 
@@ -10,7 +10,7 @@ import { noop } from '../../../helpers';
10
10
 
11
11
  export const HostSearch = ({ value, setValue }) => {
12
12
  const searchQuery = useSelector(
13
- state => state.autocomplete?.hostsSearch?.searchQuery
13
+ state => state.autocomplete?.[hostQuerySearchID]?.searchQuery
14
14
  );
15
15
  useEffect(() => {
16
16
  setValue(searchQuery || '');
@@ -148,4 +148,32 @@ describe('Hosts', () => {
148
148
  });
149
149
  expect(screen.queryAllByText('os=gnome')).toHaveLength(1);
150
150
  });
151
+
152
+ it('input fill from url', async () => {
153
+ const inputText = 'test text';
154
+ routerSelectors.selectRouterLocation.mockImplementation(() => ({
155
+ search: `feature=test_feature&inputs[plain hidden]=${inputText}`,
156
+ }));
157
+ render(
158
+ <MockedProvider mocks={gqlMock} addTypename={false}>
159
+ <Provider store={store}>
160
+ <JobWizard />
161
+ </Provider>
162
+ </MockedProvider>
163
+ );
164
+ api.get.mock.calls.forEach(call => {
165
+ if (call[0].key === 'REX_FEATURE') {
166
+ expect(call[0].url).toEqual(
167
+ '/api/remote_execution_features/test_feature'
168
+ );
169
+ }
170
+ });
171
+ await act(async () => {
172
+ fireEvent.click(screen.getByText('Target hosts and inputs'));
173
+ });
174
+ const textField = screen.getByLabelText('plain hidden', {
175
+ selector: 'textarea',
176
+ });
177
+ expect(textField.value).toBe(inputText);
178
+ });
151
179
  });
@@ -21,7 +21,7 @@ export const RepeatOn = ({
21
21
  }) => {
22
22
  const [repeatValidated, setRepeatValidated] = useState('default');
23
23
  const handleRepeatInputChange = newValue => {
24
- setRepeatValidated(newValue >= 1 ? 'default' : 'error');
24
+ setRepeatValidated(!newValue || newValue >= 1 ? 'default' : 'error');
25
25
  setRepeatAmount(newValue);
26
26
  };
27
27
 
@@ -1,9 +1,11 @@
1
1
  import React, { useEffect } from 'react';
2
- import { useSelector } from 'react-redux';
2
+ import { useSelector, useDispatch } 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
6
  import { getControllerSearchProps } from 'foremanReact/constants';
7
+ import { TRIGGERS } from 'foremanReact/components/AutoComplete/AutoCompleteConstants';
8
+ import { getResults } from 'foremanReact/components/AutoComplete/AutoCompleteActions';
7
9
  import { helpLabel } from './FormHelpers';
8
10
  import { SelectField } from './SelectField';
9
11
  import { ResourceSelectAPI } from './ResourceSelect';
@@ -22,12 +24,25 @@ const TemplateSearchField = ({
22
24
  const searchQuery = useSelector(
23
25
  state => state.autocomplete?.[name]?.searchQuery
24
26
  );
27
+ const dispatch = useDispatch();
25
28
  useEffect(() => {
26
29
  setValue({ ...values, [name]: searchQuery });
27
30
  // eslint-disable-next-line react-hooks/exhaustive-deps
28
31
  }, [searchQuery]);
29
32
  const id = name.replace(/ /g, '-');
30
33
  const props = getControllerSearchProps(controller.replace('/', '_'), name);
34
+
35
+ const setSearch = newSearchQuery => {
36
+ dispatch(
37
+ getResults({
38
+ url,
39
+ searchQuery: newSearchQuery,
40
+ controller,
41
+ trigger: TRIGGERS.INPUT_CHANGE,
42
+ id: name,
43
+ })
44
+ );
45
+ };
31
46
  return (
32
47
  <FormGroup
33
48
  label={name}
@@ -45,9 +60,9 @@ const TemplateSearchField = ({
45
60
  url,
46
61
  useKeyShortcuts: true,
47
62
  },
48
- bookmarks: null,
49
63
  }}
50
64
  onSearch={noop}
65
+ onBookmarkClick={search => setSearch(search)}
51
66
  />
52
67
  </FormGroup>
53
68
  );
@@ -152,7 +167,7 @@ export const formatter = (input, values, setValue) => {
152
167
  key={id}
153
168
  name={name}
154
169
  defaultValue={value}
155
- controller={resourceType}
170
+ controller={controller}
156
171
  url={`/${controller}/auto_complete_search`}
157
172
  labelText={labelText}
158
173
  required={required}
@@ -69,6 +69,8 @@ export const GroupedSelectField = ({
69
69
  className="without_select2"
70
70
  onClear={onClear}
71
71
  menuAppendTo={() => document.body}
72
+ aria-labelledby={fieldId}
73
+ toggleAriaLabel={`${label} toggle`}
72
74
  {...props}
73
75
  >
74
76
  {options}
@@ -81,7 +83,11 @@ GroupedSelectField.propTypes = {
81
83
  label: PropTypes.string.isRequired,
82
84
  fieldId: PropTypes.string.isRequired,
83
85
  groups: PropTypes.array,
84
- selected: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
86
+ selected: PropTypes.oneOfType([
87
+ PropTypes.string,
88
+ PropTypes.number,
89
+ PropTypes.array,
90
+ ]),
85
91
  setSelected: PropTypes.func.isRequired,
86
92
  };
87
93
 
@@ -0,0 +1,2 @@
1
+ const Pagination = () => jest.fn();
2
+ export default Pagination;
@@ -17,6 +17,7 @@ export const getControllerSearchProps = (
17
17
  useKeyShortcuts: true,
18
18
  },
19
19
  bookmarks: {
20
+ id,
20
21
  url: '/api/bookmarks',
21
22
  canCreate,
22
23
  documentationUrl: `4.1.5Searching`,
@@ -2,9 +2,13 @@ import { registerRoutes } from 'foremanReact/routes/RoutingService';
2
2
  import routes from './Routes/routes';
3
3
  import fillregistrationAdvanced from './react_app/extend/fillregistrationAdvanced';
4
4
  import fillRecentJobsCard from './react_app/extend/fillRecentJobsCard';
5
+ import fillFeaturesDropdown from './react_app/extend/fillRexFeaturesDropdown';
6
+ import fillKebabItems from './react_app/extend/fillKebabItems';
5
7
  import registerReducers from './react_app/extend/reducers';
6
8
 
7
9
  registerReducers();
8
10
  registerRoutes('foreman_remote_execution', routes);
11
+ fillFeaturesDropdown();
9
12
  fillRecentJobsCard();
10
13
  fillregistrationAdvanced();
14
+ fillKebabItems();
@@ -0,0 +1,13 @@
1
+ import { foremanUrl } from 'foremanReact/common/helpers';
2
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
3
+ import { post } from 'foremanReact/redux/API';
4
+
5
+ export const runFeature = (hostId, feature, label) => dispatch => {
6
+ const url = foremanUrl(
7
+ `/job_invocations?feature=${feature}&host_ids%5B%5D=${hostId}`
8
+ );
9
+
10
+ const successToast = () => sprintf(__('%s job has been invoked'), label);
11
+ const errorToast = ({ message }) => message;
12
+ dispatch(post({ key: feature.toUpperCase(), url, successToast, errorToast }));
13
+ };
@@ -0,0 +1,2 @@
1
+ export const REX_FEATURES_API = '/api/remote_execution_features';
2
+ export const NEW_JOB_PAGE = '/job_invocations/new?host_ids%5B%5D';