foreman_remote_execution 12.0.5 → 13.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/js_ci.yml +1 -1
  3. data/.github/workflows/ruby_ci.yml +16 -81
  4. data/.packit.yaml +8 -3
  5. data/app/assets/javascripts/foreman_remote_execution/locale/de/foreman_remote_execution.js +23 -14
  6. data/app/assets/javascripts/foreman_remote_execution/locale/en/foreman_remote_execution.js +22 -4
  7. data/app/assets/javascripts/foreman_remote_execution/locale/en_GB/foreman_remote_execution.js +23 -14
  8. data/app/assets/javascripts/foreman_remote_execution/locale/es/foreman_remote_execution.js +23 -14
  9. data/app/assets/javascripts/foreman_remote_execution/locale/fr/foreman_remote_execution.js +23 -14
  10. data/app/assets/javascripts/foreman_remote_execution/locale/ja/foreman_remote_execution.js +23 -14
  11. data/app/assets/javascripts/foreman_remote_execution/locale/ka/foreman_remote_execution.js +23 -14
  12. data/app/assets/javascripts/foreman_remote_execution/locale/ko/foreman_remote_execution.js +23 -14
  13. data/app/assets/javascripts/foreman_remote_execution/locale/pt_BR/foreman_remote_execution.js +23 -14
  14. data/app/assets/javascripts/foreman_remote_execution/locale/ru/foreman_remote_execution.js +23 -14
  15. data/app/assets/javascripts/foreman_remote_execution/locale/zh_CN/foreman_remote_execution.js +23 -14
  16. data/app/assets/javascripts/foreman_remote_execution/locale/zh_TW/foreman_remote_execution.js +23 -14
  17. data/app/controllers/ui_job_wizard_controller.rb +1 -1
  18. data/app/helpers/job_invocations_helper.rb +1 -1
  19. data/app/helpers/remote_execution_helper.rb +2 -2
  20. data/app/lib/actions/remote_execution/proxy_action.rb +1 -1
  21. data/app/lib/actions/remote_execution/run_host_job.rb +9 -3
  22. data/app/models/host_status/execution_status.rb +2 -2
  23. data/app/views/job_invocations/_preview_hosts_list.html.erb +1 -1
  24. data/app/views/job_invocations/show.html.erb +12 -5
  25. data/app/views/job_invocations/show.js.erb +8 -1
  26. data/app/views/job_invocations/welcome.html.erb +1 -1
  27. data/app/views/template_invocations/_refresh.js.erb +10 -4
  28. data/app/views/template_invocations/show.html.erb +2 -2
  29. data/lib/foreman_remote_execution/engine.rb +1 -1
  30. data/lib/foreman_remote_execution/version.rb +1 -1
  31. data/locale/de/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  32. data/locale/de/foreman_remote_execution.po +24 -6
  33. data/locale/en/foreman_remote_execution.po +24 -6
  34. data/locale/en_GB/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  35. data/locale/en_GB/foreman_remote_execution.po +24 -6
  36. data/locale/es/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  37. data/locale/es/foreman_remote_execution.po +24 -6
  38. data/locale/foreman_remote_execution.pot +170 -142
  39. data/locale/fr/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  40. data/locale/fr/foreman_remote_execution.po +24 -6
  41. data/locale/ja/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  42. data/locale/ja/foreman_remote_execution.po +24 -6
  43. data/locale/ka/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  44. data/locale/ka/foreman_remote_execution.po +24 -6
  45. data/locale/ko/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  46. data/locale/ko/foreman_remote_execution.po +24 -6
  47. data/locale/pt_BR/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  48. data/locale/pt_BR/foreman_remote_execution.po +24 -6
  49. data/locale/ru/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  50. data/locale/ru/foreman_remote_execution.po +24 -6
  51. data/locale/zh_CN/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  52. data/locale/zh_CN/foreman_remote_execution.po +24 -6
  53. data/locale/zh_TW/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  54. data/locale/zh_TW/foreman_remote_execution.po +24 -6
  55. data/package.json +1 -5
  56. data/test/functional/api/v2/job_invocations_controller_test.rb +7 -7
  57. data/test/functional/api/v2/template_invocations_controller_test.rb +3 -3
  58. data/test/helpers/remote_execution_helper_test.rb +8 -7
  59. data/test/unit/actions/run_host_job_test.rb +1 -1
  60. data/test/unit/actions/run_hosts_job_test.rb +11 -11
  61. data/test/unit/concerns/foreman_tasks_cleaner_extensions_test.rb +5 -5
  62. data/test/unit/concerns/host_extensions_test.rb +34 -34
  63. data/test/unit/concerns/nic_extensions_test.rb +1 -1
  64. data/test/unit/execution_task_status_mapper_test.rb +10 -10
  65. data/test/unit/input_template_renderer_test.rb +53 -49
  66. data/test/unit/job_invocation_composer_test.rb +78 -78
  67. data/test/unit/job_invocation_test.rb +25 -25
  68. data/test/unit/job_template_effective_user_test.rb +3 -3
  69. data/test/unit/job_template_test.rb +28 -28
  70. data/test/unit/remote_execution_feature_test.rb +14 -14
  71. data/test/unit/remote_execution_provider_test.rb +39 -39
  72. data/test/unit/renderer_scope_input_test.rb +6 -6
  73. data/test/unit/targeting_test.rb +32 -32
  74. data/webpack/JobWizard/JobWizardConstants.js +4 -0
  75. data/webpack/JobWizard/JobWizardSelectors.js +31 -3
  76. data/webpack/JobWizard/PermissionDenied.js +64 -0
  77. data/webpack/JobWizard/__tests__/fixtures.js +3 -3
  78. data/webpack/JobWizard/autofill.js +8 -4
  79. data/webpack/JobWizard/index.js +40 -1
  80. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +26 -5
  81. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +3 -3
  82. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +1 -0
  83. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +13 -6
  84. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +1 -0
  85. data/webpack/JobWizard/steps/HostsAndInputs/index.js +21 -1
  86. data/webpack/JobWizard/steps/ReviewDetails/index.js +3 -2
  87. data/webpack/JobWizard/steps/form/SearchSelect.js +3 -1
  88. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +2 -0
  89. metadata +3 -2
@@ -1,4 +1,5 @@
1
1
  import URI from 'urijs';
2
+ import { get } from 'lodash';
2
3
  import {
3
4
  selectAPIResponse,
4
5
  selectAPIStatus,
@@ -42,6 +43,18 @@ export const selectJobCategoriesStatus = state =>
42
43
  export const selectCategoryError = state =>
43
44
  selectAPIErrorMessage(state, JOB_CATEGORIES);
44
45
 
46
+ export const selectJobCategoriesMissingPermissions = state => {
47
+ const jobCategoriesResponse = selectJobCategoriesResponse(state);
48
+ return (
49
+ get(jobCategoriesResponse, [
50
+ 'response',
51
+ 'data',
52
+ 'error',
53
+ 'missing_permissions',
54
+ ]) || []
55
+ );
56
+ };
57
+
45
58
  export const selectAllTemplatesError = state =>
46
59
  selectAPIErrorMessage(state, JOB_TEMPLATES);
47
60
 
@@ -60,11 +73,26 @@ export const selectAdvancedTemplateInputs = state =>
60
73
  export const selectTemplateInputs = state =>
61
74
  selectAPIResponse(state, JOB_TEMPLATE).template_inputs || [];
62
75
 
76
+ export const selectHostsResponse = state => selectAPIResponse(state, HOSTS_API);
77
+
63
78
  export const selectHostCount = state =>
64
- selectAPIResponse(state, HOSTS_API).subtotal || 0;
79
+ selectHostsResponse(state).subtotal || 0;
80
+
81
+ export const selectHosts = state => {
82
+ const hosts = selectHostsResponse(state).results || [];
83
+ return hosts.map(host => ({
84
+ name: host.name,
85
+ display_name: host.display_name,
86
+ }));
87
+ };
65
88
 
66
- export const selectHosts = state =>
67
- (selectAPIResponse(state, HOSTS_API).results || []).map(host => host.name);
89
+ export const selectHostsMissingPermissions = state => {
90
+ const hostsResponse = selectHostsResponse(state);
91
+ return (
92
+ get(hostsResponse, ['response', 'data', 'error', 'missing_permissions']) ||
93
+ []
94
+ );
95
+ };
68
96
 
69
97
  export const selectIsLoadingHosts = state =>
70
98
  !selectAPIStatus(state, HOSTS_API) ||
@@ -0,0 +1,64 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+ import { Icon } from 'patternfly-react';
5
+ import {
6
+ Title,
7
+ Button,
8
+ EmptyState,
9
+ EmptyStateVariant,
10
+ EmptyStateBody,
11
+ } from '@patternfly/react-core';
12
+
13
+ const PermissionDenied = ({ missingPermissions, setProceedAnyway }) => {
14
+ const description = (
15
+ <span>
16
+ {__('You are not authorized to perform this action.')}
17
+ <br />
18
+ {__(
19
+ 'Please request the required permissions listed below from a Foreman administrator:'
20
+ )}
21
+ <br />
22
+ <ul className="list-unstyled">
23
+ {missingPermissions.map(permission => (
24
+ <li key={permission}>
25
+ <strong>{permission}</strong>
26
+ </li>
27
+ ))}
28
+ </ul>
29
+ </span>
30
+ );
31
+ const handleProceedAnyway = () => {
32
+ setProceedAnyway(true);
33
+ };
34
+
35
+ return (
36
+ <EmptyState variant={EmptyStateVariant.xl}>
37
+ <span className="empty-state-icon">
38
+ <Icon name="lock" type="fa" size="2x" />
39
+ </span>
40
+ <Title ouiaId="empty-state-header" headingLevel="h5" size="4xl">
41
+ {__('Permission Denied')}
42
+ </Title>
43
+ <EmptyStateBody>{description}</EmptyStateBody>
44
+ <Button
45
+ ouiaId="job-invocation-proceed-anyway-button"
46
+ variant="primary"
47
+ onClick={handleProceedAnyway}
48
+ >
49
+ {__('Proceed Anyway')}
50
+ </Button>
51
+ </EmptyState>
52
+ );
53
+ };
54
+
55
+ PermissionDenied.propTypes = {
56
+ missingPermissions: PropTypes.array,
57
+ setProceedAnyway: PropTypes.func.isRequired,
58
+ };
59
+
60
+ PermissionDenied.defaultProps = {
61
+ missingPermissions: ['unknown'],
62
+ };
63
+
64
+ export default PermissionDenied;
@@ -227,9 +227,9 @@ export const gqlMock = [
227
227
  hosts: {
228
228
  totalCount: 3,
229
229
  nodes: [
230
- { id: 'MDE6SG9zdC0x', name: 'host1' },
231
- { id: 'MDE6SG9zdC0y', name: 'host2' },
232
- { id: 'MDE6SG9zdC0z', name: 'host3' },
230
+ { id: 'MDE6SG9zdC0x', name: 'host1', displayName: 'host1' },
231
+ { id: 'MDE6SG9zdC0y', name: 'host2', displayName: 'host2' },
232
+ { id: 'MDE6SG9zdC0z', name: 'host3', displayName: 'host3' },
233
233
  ],
234
234
  },
235
235
  },
@@ -39,10 +39,14 @@ export const useAutoFill = ({
39
39
  handleSuccess: ({ data }) => {
40
40
  setSelectedTargets(currentTargets => ({
41
41
  ...currentTargets,
42
- hosts: (data.results || []).map(({ id, name }) => ({
43
- id,
44
- name,
45
- })),
42
+ hosts: (data.results || []).map(
43
+ // eslint-disable-next-line camelcase
44
+ ({ id, name, display_name }) => ({
45
+ id,
46
+ // eslint-disable-next-line camelcase
47
+ name: display_name || name,
48
+ })
49
+ ),
46
50
  }));
47
51
  },
48
52
  })
@@ -1,9 +1,17 @@
1
- import React from 'react';
1
+ import React, { useState } from 'react';
2
+ import { isEmpty } from 'lodash';
2
3
  import PropTypes from 'prop-types';
3
4
  import { Button } from '@patternfly/react-core';
4
5
  import { translate as __ } from 'foremanReact/common/I18n';
6
+ import { STATUS } from 'foremanReact/constants';
5
7
  import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
8
+ import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
9
+ import PermissionDenied from './PermissionDenied';
6
10
  import { JobWizard } from './JobWizard';
11
+ import {
12
+ CURRENT_PERMISSIONS,
13
+ currentPermissionsUrl,
14
+ } from './JobWizardConstants';
7
15
 
8
16
  const JobWizardPage = ({ location: { search } }) => {
9
17
  const title = __('Run job');
@@ -13,6 +21,37 @@ const JobWizardPage = ({ location: { search } }) => {
13
21
  { caption: title },
14
22
  ],
15
23
  };
24
+ const [proceedAnyway, setProceedAnyway] = useState(false);
25
+
26
+ const { response, status } = useAPI(
27
+ 'get',
28
+ currentPermissionsUrl,
29
+ CURRENT_PERMISSIONS
30
+ );
31
+ const desiredPermissions = [
32
+ 'view_hosts',
33
+ 'view_smart_proxies',
34
+ 'view_job_templates',
35
+ 'create_job_invocations',
36
+ 'create_template_invocations',
37
+ ];
38
+ const missingPermissions =
39
+ status === STATUS.RESOLVED
40
+ ? desiredPermissions.filter(
41
+ permission =>
42
+ !response.results.map(result => result.name).includes(permission)
43
+ )
44
+ : [];
45
+
46
+ if (!isEmpty(missingPermissions) && !proceedAnyway) {
47
+ return (
48
+ <PermissionDenied
49
+ missingPermissions={missingPermissions}
50
+ setProceedAnyway={setProceedAnyway}
51
+ />
52
+ );
53
+ }
54
+
16
55
  return (
17
56
  <PageLayout
18
57
  header={title}
@@ -1,13 +1,17 @@
1
1
  import React from 'react';
2
+ import { isEmpty } from 'lodash';
2
3
  import PropTypes from 'prop-types';
3
4
  import { useSelector } from 'react-redux';
4
5
  import { Text, TextVariants, Form, Alert } from '@patternfly/react-core';
5
6
  import { translate as __ } from 'foremanReact/common/I18n';
7
+ import {
8
+ selectJobCategoriesMissingPermissions,
9
+ selectIsLoading,
10
+ } from '../../JobWizardSelectors';
6
11
  import { SelectField } from '../form/SelectField';
7
12
  import { GroupedSelectField } from '../form/GroupedSelectField';
8
13
  import { WizardTitle } from '../form/WizardTitle';
9
14
  import { WIZARD_TITLES, JOB_TEMPLATES } from '../../JobWizardConstants';
10
- import { selectIsLoading } from '../../JobWizardSelectors';
11
15
 
12
16
  export const CategoryAndTemplate = ({
13
17
  jobCategories,
@@ -57,7 +61,13 @@ export const CategoryAndTemplate = ({
57
61
  };
58
62
 
59
63
  const { categoryError, allTemplatesError, templateError } = errors;
60
- const isError = !!(categoryError || allTemplatesError || templateError);
64
+ const missingPermissions = useSelector(selectJobCategoriesMissingPermissions);
65
+ const isError = !!(
66
+ (categoryError && isEmpty(missingPermissions)) ||
67
+ allTemplatesError ||
68
+ templateError
69
+ );
70
+
61
71
  return (
62
72
  <>
63
73
  <WizardTitle title={WIZARD_TITLES.categoryAndTemplate} />
@@ -69,7 +79,7 @@ export const CategoryAndTemplate = ({
69
79
  options={jobCategories}
70
80
  setValue={onSelectCategory}
71
81
  value={selectedCategory}
72
- placeholderText={categoryError ? __('Error') : ''}
82
+ placeholderText={categoryError ? __('Not available') : ''}
73
83
  isDisabled={!!categoryError}
74
84
  isRequired
75
85
  />
@@ -82,11 +92,22 @@ export const CategoryAndTemplate = ({
82
92
  isDisabled={
83
93
  !!(categoryError || allTemplatesError || isTemplatesLoading)
84
94
  }
85
- placeholderText={allTemplatesError ? __('Error') : ''}
95
+ placeholderText={allTemplatesError ? __('Not available') : ''}
86
96
  />
97
+ {!isEmpty(missingPermissions) && (
98
+ <Alert variant="warning" title={__('Access denied')}>
99
+ <span>
100
+ {__(
101
+ `Missing the required permissions: ${missingPermissions.join(
102
+ ', '
103
+ )}`
104
+ )}
105
+ </span>
106
+ </Alert>
107
+ )}
87
108
  {isError && (
88
109
  <Alert variant="danger" title={__('Errors:')}>
89
- {categoryError && (
110
+ {categoryError && isEmpty(missingPermissions) && (
90
111
  <span>
91
112
  {__('Categories list failed with:')} {categoryError}
92
113
  </span>
@@ -22,16 +22,16 @@ export const HostPreviewModal = ({ isOpen, setIsOpen, searchQuery }) => {
22
22
  >
23
23
  <List isPlain>
24
24
  {hosts.map(host => (
25
- <ListItem key={host}>
25
+ <ListItem key={host.name}>
26
26
  <Button
27
27
  component="a"
28
- href={foremanUrl(`/hosts/${host}`)}
28
+ href={foremanUrl(`/hosts/${host.name}`)}
29
29
  variant="link"
30
30
  target="_blank"
31
31
  rel="noreferrer"
32
32
  isInline
33
33
  >
34
- {host}
34
+ {host.display_name}
35
35
  </Button>
36
36
  </ListItem>
37
37
  ))}
@@ -38,6 +38,7 @@ export const useNameSearchGQL = apiKey => {
38
38
  data?.[dataName[apiKey]]?.nodes.map(node => ({
39
39
  id: decodeId(node.id),
40
40
  name: node.name,
41
+ displayName: node.displayName,
41
42
  })) || [],
42
43
  },
43
44
  loading,
@@ -4,7 +4,7 @@ import { Chip, ChipGroup, Button } from '@patternfly/react-core';
4
4
  import { sprintf, translate as __ } from 'foremanReact/common/I18n';
5
5
  import { hostMethods } from '../../JobWizardConstants';
6
6
 
7
- const SelectedChip = ({ selected, setSelected, categoryName }) => {
7
+ const SelectedChip = ({ selected, setSelected, categoryName, setLabel }) => {
8
8
  const deleteItem = itemToRemove => {
9
9
  setSelected(oldSelected =>
10
10
  oldSelected.filter(({ id }) => id !== itemToRemove)
@@ -24,14 +24,14 @@ const SelectedChip = ({ selected, setSelected, categoryName }) => {
24
24
  setSelected(() => []);
25
25
  }}
26
26
  >
27
- {selected.map(({ name, id }, index) => (
27
+ {selected.map((result, index) => (
28
28
  <Chip
29
29
  key={index}
30
- id={`${categoryName}-${id}`}
31
- onClick={() => deleteItem(id)}
32
- closeBtnAriaLabel={`Remove ${name}`}
30
+ id={`${categoryName}-${result.id}`}
31
+ onClick={() => deleteItem(result.id)}
32
+ closeBtnAriaLabel={`Remove ${result.name}`}
33
33
  >
34
- {name}
34
+ {setLabel(result)}
35
35
  </Chip>
36
36
  ))}
37
37
  </ChipGroup>
@@ -49,6 +49,7 @@ export const SelectedChips = ({
49
49
  setSelectedHostGroups,
50
50
  hostsSearchQuery,
51
51
  clearSearch,
52
+ setLabel,
52
53
  }) => {
53
54
  const clearAll = () => {
54
55
  setSelectedHosts(() => []);
@@ -67,16 +68,19 @@ export const SelectedChips = ({
67
68
  selected={selectedHosts}
68
69
  categoryName={hostMethods.hosts}
69
70
  setSelected={setSelectedHosts}
71
+ setLabel={setLabel}
70
72
  />
71
73
  <SelectedChip
72
74
  selected={selectedHostCollections}
73
75
  categoryName={hostMethods.hostCollections}
74
76
  setSelected={setSelectedHostCollections}
77
+ setLabel={setLabel}
75
78
  />
76
79
  <SelectedChip
77
80
  selected={selectedHostGroups}
78
81
  categoryName={hostMethods.hostGroups}
79
82
  setSelected={setSelectedHostGroups}
83
+ setLabel={setLabel}
80
84
  />
81
85
  <SelectedChip
82
86
  selected={
@@ -86,6 +90,7 @@ export const SelectedChips = ({
86
90
  }
87
91
  categoryName={hostMethods.searchQuery}
88
92
  setSelected={clearSearch}
93
+ setLabel={setLabel}
89
94
  />
90
95
  {showClear && (
91
96
  <Button variant="link" className="clear-chips" onClick={clearAll}>
@@ -105,10 +110,12 @@ SelectedChips.propTypes = {
105
110
  setSelectedHostGroups: PropTypes.func.isRequired,
106
111
  hostsSearchQuery: PropTypes.string.isRequired,
107
112
  clearSearch: PropTypes.func.isRequired,
113
+ setLabel: PropTypes.func.isRequired,
108
114
  };
109
115
 
110
116
  SelectedChip.propTypes = {
111
117
  categoryName: PropTypes.string.isRequired,
112
118
  selected: PropTypes.array.isRequired,
113
119
  setSelected: PropTypes.func.isRequired,
120
+ setLabel: PropTypes.func.isRequired,
114
121
  };
@@ -4,6 +4,7 @@ query($search: String!) {
4
4
  nodes {
5
5
  id
6
6
  name
7
+ displayName
7
8
  }
8
9
  }
9
10
  }
@@ -1,5 +1,7 @@
1
1
  import React, { useEffect, useState } from 'react';
2
+ import { isEmpty, debounce } from 'lodash';
2
3
  import {
4
+ Alert,
3
5
  Button,
4
6
  Form,
5
7
  FormGroup,
@@ -10,13 +12,13 @@ import {
10
12
  import PropTypes from 'prop-types';
11
13
  import { useSelector, useDispatch } from 'react-redux';
12
14
  import { FilterIcon } from '@patternfly/react-icons';
13
- import { debounce } from 'lodash';
14
15
  import { get } from 'foremanReact/redux/API';
15
16
  import { translate as __ } from 'foremanReact/common/I18n';
16
17
  import {
17
18
  selectTemplateInputs,
18
19
  selectWithKatello,
19
20
  selectHostCount,
21
+ selectHostsMissingPermissions,
20
22
  selectIsLoadingHosts,
21
23
  } from '../../JobWizardSelectors';
22
24
  import { SelectField } from '../form/SelectField';
@@ -98,9 +100,11 @@ const HostsAndInputs = ({
98
100
  ]);
99
101
  const withKatello = useSelector(selectWithKatello);
100
102
  const hostCount = useSelector(selectHostCount);
103
+ const missingPermissions = useSelector(selectHostsMissingPermissions);
101
104
  const dispatch = useDispatch();
102
105
 
103
106
  const selectedHosts = selected.hosts;
107
+ const setLabel = result => result.displayName || result.name;
104
108
  const setSelectedHosts = newSelected =>
105
109
  setSelected(prevSelected => ({
106
110
  ...prevSelected,
@@ -126,6 +130,7 @@ const HostsAndInputs = ({
126
130
  const [errorText, setErrorText] = useState(
127
131
  __('Please select at least one host')
128
132
  );
133
+
129
134
  return (
130
135
  <div className="target-hosts-and-inputs">
131
136
  <WizardTitle title={WIZARD_TITLES.hostsAndInputs} />
@@ -187,6 +192,7 @@ const HostsAndInputs = ({
187
192
  apiKey={HOSTS}
188
193
  name="hosts"
189
194
  placeholderText={__('Filter by hosts')}
195
+ setLabel={setLabel}
190
196
  />
191
197
  )}
192
198
  {hostMethod === hostMethods.hostCollections && (
@@ -197,6 +203,7 @@ const HostsAndInputs = ({
197
203
  name="host collections"
198
204
  url="/katello/api/host_collections?per_page=100"
199
205
  placeholderText={__('Filter by host collections')}
206
+ setLabel={setLabel}
200
207
  />
201
208
  )}
202
209
  {hostMethod === hostMethods.hostGroups && (
@@ -206,6 +213,7 @@ const HostsAndInputs = ({
206
213
  apiKey={HOST_GROUPS}
207
214
  name="host groups"
208
215
  placeholderText={__('Filter by host groups')}
216
+ setLabel={setLabel}
209
217
  />
210
218
  )}
211
219
  </InputGroup>
@@ -219,6 +227,7 @@ const HostsAndInputs = ({
219
227
  setSelectedHostGroups={setSelectedHostGroups}
220
228
  hostsSearchQuery={hostsSearchQuery}
221
229
  clearSearch={clearSearch}
230
+ setLabel={setLabel}
222
231
  />
223
232
  <Text>
224
233
  {__('Apply to')}{' '}
@@ -237,6 +246,17 @@ const HostsAndInputs = ({
237
246
  value={templateValues}
238
247
  setValue={setTemplateValues}
239
248
  />
249
+ {!isEmpty(missingPermissions) && (
250
+ <Alert variant="warning" title={__('Access denied')}>
251
+ <span>
252
+ {__(
253
+ `Missing the required permissions: ${missingPermissions.join(
254
+ ', '
255
+ )}`
256
+ )}
257
+ </span>
258
+ </Alert>
259
+ )}
240
260
  </Form>
241
261
  </div>
242
262
  );
@@ -79,12 +79,13 @@ const ReviewDetails = ({
79
79
 
80
80
  const hostsCount = useSelector(selectHostCount);
81
81
  const [hostPreviewOpen, setHostPreviewOpen] = useState(false);
82
+ const NUM_CHIPS = 3;
82
83
  const stringHosts = () => {
83
84
  if (hosts.length === 0) {
84
85
  return __('No Target Hosts');
85
86
  }
86
- if (hosts.length === 1 || hosts.length === 2) {
87
- return hosts.join(', ');
87
+ if (hosts.length < NUM_CHIPS) {
88
+ return hosts.map(host => host.display_name).join(', ');
88
89
  }
89
90
  return (
90
91
  <div>
@@ -16,6 +16,7 @@ export const SearchSelect = ({
16
16
  apiKey,
17
17
  url,
18
18
  variant,
19
+ setLabel,
19
20
  }) => {
20
21
  const [onSearch, response, isLoading] = useNameSearch(apiKey, url);
21
22
  const [isOpen, setIsOpen] = useState(false);
@@ -48,7 +49,7 @@ export const SearchSelect = ({
48
49
  ...selectOptions,
49
50
  ...Immutable.asMutable(response?.results || [])?.map((result, index) => (
50
51
  <SelectOption key={index + 1} value={result.id}>
51
- {result.name}
52
+ {setLabel(result)}
52
53
  </SelectOption>
53
54
  )),
54
55
  ];
@@ -104,6 +105,7 @@ SearchSelect.propTypes = {
104
105
  name: PropTypes.string,
105
106
  selected: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
106
107
  setSelected: PropTypes.func.isRequired,
108
+ setLabel: PropTypes.func.isRequired,
107
109
  placeholderText: PropTypes.string,
108
110
  apiKey: PropTypes.string.isRequired,
109
111
  url: PropTypes.string,
@@ -7,6 +7,7 @@ const apiKey = 'HOSTS_KEY';
7
7
  describe('SearchSelect', () => {
8
8
  it('too many', () => {
9
9
  const onSearch = jest.fn();
10
+ const setLabel = jest.fn();
10
11
  render(
11
12
  <SearchSelect
12
13
  selected={['hosts1,host2']}
@@ -19,6 +20,7 @@ describe('SearchSelect', () => {
19
20
  { results: ['host1', 'host2', 'host3'], subtotal: 101 },
20
21
  false,
21
22
  ]}
23
+ setLabel={setLabel}
22
24
  />
23
25
  );
24
26
  const openSelectbutton = screen.getByRole('button', {
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman_remote_execution
3
3
  version: !ruby/object:Gem::Version
4
- version: 12.0.5
4
+ version: 13.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foreman Remote Execution team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-23 00:00:00.000000000 Z
11
+ date: 2024-03-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deface
@@ -446,6 +446,7 @@ files:
446
446
  - webpack/JobWizard/JobWizardHelpers.js
447
447
  - webpack/JobWizard/JobWizardPageRerun.js
448
448
  - webpack/JobWizard/JobWizardSelectors.js
449
+ - webpack/JobWizard/PermissionDenied.js
449
450
  - webpack/JobWizard/StartsBeforeErrorAlert.js
450
451
  - webpack/JobWizard/__tests__/JobWizardPageRerun.test.js
451
452
  - webpack/JobWizard/__tests__/__snapshots__/integration.test.js.snap