foreman_remote_execution 12.0.7 → 13.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/js_ci.yml +1 -1
- data/.github/workflows/ruby_ci.yml +16 -81
- data/.packit.yaml +8 -3
- data/app/assets/javascripts/foreman_remote_execution/locale/de/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/en/foreman_remote_execution.js +22 -4
- data/app/assets/javascripts/foreman_remote_execution/locale/en_GB/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/es/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/fr/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/ja/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/ka/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/ko/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/pt_BR/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/ru/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/zh_CN/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/zh_TW/foreman_remote_execution.js +23 -14
- data/app/controllers/ui_job_wizard_controller.rb +1 -1
- data/app/helpers/job_invocations_helper.rb +1 -1
- data/app/helpers/remote_execution_helper.rb +2 -2
- data/app/lib/actions/remote_execution/proxy_action.rb +1 -1
- data/app/lib/actions/remote_execution/run_host_job.rb +9 -3
- data/app/lib/actions/remote_execution/run_hosts_job.rb +0 -1
- data/app/models/host_status/execution_status.rb +2 -2
- data/app/views/job_invocations/_preview_hosts_list.html.erb +1 -1
- data/app/views/job_invocations/show.html.erb +12 -5
- data/app/views/job_invocations/show.js.erb +8 -1
- data/app/views/job_invocations/welcome.html.erb +1 -1
- data/app/views/template_invocations/_refresh.js.erb +10 -4
- data/app/views/template_invocations/show.html.erb +2 -2
- data/app/views/templates/script/convert2rhel_analyze.erb +12 -1
- data/lib/foreman_remote_execution/engine.rb +1 -1
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/locale/de/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/de/foreman_remote_execution.po +24 -6
- data/locale/en/foreman_remote_execution.po +24 -6
- data/locale/en_GB/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/en_GB/foreman_remote_execution.po +24 -6
- data/locale/es/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/es/foreman_remote_execution.po +24 -6
- data/locale/foreman_remote_execution.pot +170 -142
- data/locale/fr/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/fr/foreman_remote_execution.po +24 -6
- data/locale/ja/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ja/foreman_remote_execution.po +24 -6
- data/locale/ka/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ka/foreman_remote_execution.po +24 -6
- data/locale/ko/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ko/foreman_remote_execution.po +24 -6
- data/locale/pt_BR/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/pt_BR/foreman_remote_execution.po +24 -6
- data/locale/ru/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ru/foreman_remote_execution.po +24 -6
- data/locale/zh_CN/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/zh_CN/foreman_remote_execution.po +24 -6
- data/locale/zh_TW/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/zh_TW/foreman_remote_execution.po +24 -6
- data/package.json +1 -5
- data/test/functional/api/v2/job_invocations_controller_test.rb +7 -7
- data/test/functional/api/v2/template_invocations_controller_test.rb +3 -3
- data/test/helpers/remote_execution_helper_test.rb +8 -7
- data/test/unit/actions/run_host_job_test.rb +1 -1
- data/test/unit/actions/run_hosts_job_test.rb +11 -11
- data/test/unit/concerns/foreman_tasks_cleaner_extensions_test.rb +5 -5
- data/test/unit/concerns/host_extensions_test.rb +34 -34
- data/test/unit/concerns/nic_extensions_test.rb +1 -1
- data/test/unit/execution_task_status_mapper_test.rb +10 -10
- data/test/unit/input_template_renderer_test.rb +53 -49
- data/test/unit/job_invocation_composer_test.rb +78 -78
- data/test/unit/job_invocation_test.rb +25 -25
- data/test/unit/job_template_effective_user_test.rb +3 -3
- data/test/unit/job_template_test.rb +28 -28
- data/test/unit/remote_execution_feature_test.rb +14 -14
- data/test/unit/remote_execution_provider_test.rb +39 -39
- data/test/unit/renderer_scope_input_test.rb +6 -6
- data/test/unit/targeting_test.rb +32 -32
- data/webpack/JobWizard/JobWizardConstants.js +4 -0
- data/webpack/JobWizard/JobWizardSelectors.js +31 -3
- data/webpack/JobWizard/PermissionDenied.js +64 -0
- data/webpack/JobWizard/__tests__/fixtures.js +3 -3
- data/webpack/JobWizard/autofill.js +8 -4
- data/webpack/JobWizard/index.js +40 -1
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +26 -5
- data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +3 -3
- data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +1 -0
- data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +13 -6
- data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +1 -0
- data/webpack/JobWizard/steps/HostsAndInputs/index.js +21 -1
- data/webpack/JobWizard/steps/ReviewDetails/index.js +3 -2
- data/webpack/JobWizard/steps/form/SearchSelect.js +3 -1
- data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +2 -0
- 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
|
-
|
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
|
67
|
-
|
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(
|
43
|
-
|
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
|
})
|
data/webpack/JobWizard/index.js
CHANGED
@@ -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
|
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 ? __('
|
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 ? __('
|
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
|
))}
|
@@ -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((
|
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
|
-
{
|
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
|
};
|
@@ -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
|
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
|
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:
|
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-
|
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.
|
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.
|