foreman_remote_execution 5.1.0 → 6.2.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -0
  3. data/app/controllers/foreman_remote_execution/concerns/api/v2/registration_controller_extensions.rb +5 -0
  4. data/app/controllers/ui_job_wizard_controller.rb +13 -2
  5. data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +20 -9
  6. data/app/helpers/remote_execution_helper.rb +5 -4
  7. data/app/lib/actions/remote_execution/run_host_job.rb +1 -1
  8. data/app/mailers/rex_job_mailer.rb +1 -1
  9. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +9 -1
  10. data/app/models/concerns/foreman_remote_execution/nic_extensions.rb +6 -4
  11. data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +1 -1
  12. data/app/models/job_invocation_composer.rb +1 -1
  13. data/app/models/remote_execution_provider.rb +10 -1
  14. data/app/models/ssh_execution_provider.rb +17 -4
  15. data/app/models/targeting.rb +1 -1
  16. data/app/services/default_proxy_proxy_selector.rb +1 -1
  17. data/app/services/remote_execution_proxy_selector.rb +7 -2
  18. data/app/views/api/v2/host/main.rabl +1 -0
  19. data/app/views/dashboard/_latest-jobs.html.erb +1 -1
  20. data/app/views/job_invocations/_preview_hosts_list.html.erb +1 -1
  21. data/app/views/template_invocations/show.html.erb +1 -1
  22. data/app/views/templates/{ssh → script}/check_update.erb +2 -2
  23. data/app/views/templates/{ssh → script}/module_action.erb +2 -2
  24. data/app/views/templates/{ssh → script}/package_action.erb +5 -2
  25. data/app/views/templates/{ssh → script}/power_action.erb +2 -2
  26. data/app/views/templates/{ssh → script}/puppet_agent_disable.erb +2 -2
  27. data/app/views/templates/{ssh → script}/puppet_agent_enable.erb +2 -2
  28. data/app/views/templates/{ssh → script}/puppet_install_modules_from_forge.erb +2 -2
  29. data/app/views/templates/{ssh → script}/puppet_install_modules_from_git.erb +2 -2
  30. data/app/views/templates/{ssh → script}/puppet_run_once.erb +2 -2
  31. data/app/views/templates/{ssh → script}/run_command.erb +2 -2
  32. data/app/views/templates/{ssh → script}/service_action.erb +2 -2
  33. data/db/migrate/20220321101835_rename_ssh_provider_to_script.rb +29 -0
  34. data/db/seeds.d/60-ssh_proxy_feature.rb +3 -0
  35. data/jsconfig.json +8 -0
  36. data/lib/foreman_remote_execution/engine.rb +10 -4
  37. data/lib/foreman_remote_execution/version.rb +1 -1
  38. data/locale/action_names.rb +3 -4
  39. data/locale/de/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  40. data/locale/de/foreman_remote_execution.po +356 -20
  41. data/locale/en/foreman_remote_execution.po +356 -20
  42. data/locale/en_GB/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  43. data/locale/en_GB/foreman_remote_execution.po +356 -20
  44. data/locale/es/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  45. data/locale/es/foreman_remote_execution.po +357 -21
  46. data/locale/foreman_remote_execution.pot +808 -296
  47. data/locale/fr/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  48. data/locale/fr/foreman_remote_execution.po +357 -21
  49. data/locale/ja/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  50. data/locale/ja/foreman_remote_execution.po +357 -21
  51. data/locale/ko/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  52. data/locale/ko/foreman_remote_execution.po +356 -20
  53. data/locale/pt_BR/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  54. data/locale/pt_BR/foreman_remote_execution.po +357 -21
  55. data/locale/ru/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  56. data/locale/ru/foreman_remote_execution.po +356 -20
  57. data/locale/zh_CN/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  58. data/locale/zh_CN/foreman_remote_execution.po +357 -21
  59. data/locale/zh_TW/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  60. data/locale/zh_TW/foreman_remote_execution.po +356 -20
  61. data/package.json +6 -7
  62. data/test/unit/concerns/host_extensions_test.rb +2 -1
  63. data/test/unit/remote_execution_provider_test.rb +2 -0
  64. data/webpack/JobWizard/JobWizard.js +30 -7
  65. data/webpack/JobWizard/JobWizard.scss +5 -0
  66. data/webpack/JobWizard/JobWizardConstants.js +2 -1
  67. data/webpack/JobWizard/__tests__/fixtures.js +13 -2
  68. data/webpack/JobWizard/__tests__/integration.test.js +6 -1
  69. data/webpack/JobWizard/autofill.js +34 -9
  70. data/webpack/JobWizard/index.js +0 -7
  71. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +24 -15
  72. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +25 -7
  73. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +1 -1
  74. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +28 -0
  75. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +1 -1
  76. data/webpack/JobWizard/steps/form/Formatter.js +18 -3
  77. data/webpack/JobWizard/steps/form/GroupedSelectField.js +7 -1
  78. data/webpack/__mocks__/foremanReact/components/Pagination.js +2 -0
  79. data/webpack/__mocks__/foremanReact/constants.js +1 -0
  80. data/webpack/global_index.js +4 -0
  81. data/webpack/react_app/components/FeaturesDropdown/actions.js +13 -0
  82. data/webpack/react_app/components/FeaturesDropdown/constant.js +2 -0
  83. data/webpack/react_app/components/FeaturesDropdown/index.js +74 -0
  84. data/webpack/react_app/components/HostKebab/KebabItems.js +22 -0
  85. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +6 -1
  86. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +2 -2
  87. data/webpack/react_app/components/RecentJobsCard/constants.js +1 -0
  88. data/webpack/react_app/components/TargetingHosts/TargetingHostsHelpers.js +1 -1
  89. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +1 -6
  90. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -9
  91. data/webpack/react_app/components/TargetingHosts/index.js +2 -3
  92. data/webpack/react_app/extend/fillKebabItems.js +11 -0
  93. data/webpack/react_app/extend/fillRexFeaturesDropdown.js +11 -0
  94. metadata +24 -16
  95. data/app/views/templates/README.md +0 -6
  96. 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
  }
@@ -67,7 +67,8 @@ class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase
67
67
  it 'should only have one execution interface' do
68
68
  host.interfaces << FactoryBot.build(:nic_managed)
69
69
  host.interfaces.each { |interface| interface.execution = true }
70
- _(host).wont_be :valid?
70
+ _(host).must_be :valid?
71
+ _(host.interfaces.count(&:execution?)).must_equal 1
71
72
  end
72
73
 
73
74
  it 'returns the execution interface' do
@@ -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
 
@@ -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';