foreman_remote_execution 12.0.7 → 13.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 (91) 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/lib/actions/remote_execution/run_hosts_job.rb +0 -1
  23. data/app/models/host_status/execution_status.rb +2 -2
  24. data/app/views/job_invocations/_preview_hosts_list.html.erb +1 -1
  25. data/app/views/job_invocations/show.html.erb +12 -5
  26. data/app/views/job_invocations/show.js.erb +8 -1
  27. data/app/views/job_invocations/welcome.html.erb +1 -1
  28. data/app/views/template_invocations/_refresh.js.erb +10 -4
  29. data/app/views/template_invocations/show.html.erb +2 -2
  30. data/app/views/templates/script/convert2rhel_analyze.erb +12 -1
  31. data/lib/foreman_remote_execution/engine.rb +1 -1
  32. data/lib/foreman_remote_execution/version.rb +1 -1
  33. data/locale/de/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  34. data/locale/de/foreman_remote_execution.po +24 -6
  35. data/locale/en/foreman_remote_execution.po +24 -6
  36. data/locale/en_GB/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  37. data/locale/en_GB/foreman_remote_execution.po +24 -6
  38. data/locale/es/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  39. data/locale/es/foreman_remote_execution.po +24 -6
  40. data/locale/foreman_remote_execution.pot +170 -142
  41. data/locale/fr/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  42. data/locale/fr/foreman_remote_execution.po +24 -6
  43. data/locale/ja/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  44. data/locale/ja/foreman_remote_execution.po +24 -6
  45. data/locale/ka/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  46. data/locale/ka/foreman_remote_execution.po +24 -6
  47. data/locale/ko/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  48. data/locale/ko/foreman_remote_execution.po +24 -6
  49. data/locale/pt_BR/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  50. data/locale/pt_BR/foreman_remote_execution.po +24 -6
  51. data/locale/ru/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  52. data/locale/ru/foreman_remote_execution.po +24 -6
  53. data/locale/zh_CN/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  54. data/locale/zh_CN/foreman_remote_execution.po +24 -6
  55. data/locale/zh_TW/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  56. data/locale/zh_TW/foreman_remote_execution.po +24 -6
  57. data/package.json +1 -5
  58. data/test/functional/api/v2/job_invocations_controller_test.rb +7 -7
  59. data/test/functional/api/v2/template_invocations_controller_test.rb +3 -3
  60. data/test/helpers/remote_execution_helper_test.rb +8 -7
  61. data/test/unit/actions/run_host_job_test.rb +1 -1
  62. data/test/unit/actions/run_hosts_job_test.rb +11 -11
  63. data/test/unit/concerns/foreman_tasks_cleaner_extensions_test.rb +5 -5
  64. data/test/unit/concerns/host_extensions_test.rb +34 -34
  65. data/test/unit/concerns/nic_extensions_test.rb +1 -1
  66. data/test/unit/execution_task_status_mapper_test.rb +10 -10
  67. data/test/unit/input_template_renderer_test.rb +53 -49
  68. data/test/unit/job_invocation_composer_test.rb +78 -78
  69. data/test/unit/job_invocation_test.rb +25 -25
  70. data/test/unit/job_template_effective_user_test.rb +3 -3
  71. data/test/unit/job_template_test.rb +28 -28
  72. data/test/unit/remote_execution_feature_test.rb +14 -14
  73. data/test/unit/remote_execution_provider_test.rb +39 -39
  74. data/test/unit/renderer_scope_input_test.rb +6 -6
  75. data/test/unit/targeting_test.rb +32 -32
  76. data/webpack/JobWizard/JobWizardConstants.js +4 -0
  77. data/webpack/JobWizard/JobWizardSelectors.js +31 -3
  78. data/webpack/JobWizard/PermissionDenied.js +64 -0
  79. data/webpack/JobWizard/__tests__/fixtures.js +3 -3
  80. data/webpack/JobWizard/autofill.js +8 -4
  81. data/webpack/JobWizard/index.js +40 -1
  82. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +26 -5
  83. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +3 -3
  84. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +1 -0
  85. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +13 -6
  86. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +1 -0
  87. data/webpack/JobWizard/steps/HostsAndInputs/index.js +21 -1
  88. data/webpack/JobWizard/steps/ReviewDetails/index.js +3 -2
  89. data/webpack/JobWizard/steps/form/SearchSelect.js +3 -1
  90. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +2 -0
  91. metadata +7 -6
@@ -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.7
4
+ version: 13.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foreman Remote Execution team
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-06-28 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
@@ -575,7 +576,7 @@ homepage: https://github.com/theforeman/foreman_remote_execution
575
576
  licenses:
576
577
  - GPL-3.0
577
578
  metadata: {}
578
- post_install_message:
579
+ post_install_message:
579
580
  rdoc_options: []
580
581
  require_paths:
581
582
  - lib
@@ -593,8 +594,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
593
594
  - !ruby/object:Gem::Version
594
595
  version: '0'
595
596
  requirements: []
596
- rubygems_version: 3.3.27
597
- signing_key:
597
+ rubygems_version: 3.3.26
598
+ signing_key:
598
599
  specification_version: 4
599
600
  summary: A plugin bringing remote execution to the Foreman, completing the config
600
601
  management functionality with remote management functionality.