foreman_remote_execution 12.0.5 → 13.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.eslintrc +4 -1
- data/.github/workflows/js_ci.yml +1 -1
- data/.github/workflows/release.yml +4 -2
- 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 -17
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
- data/app/models/host_status/execution_status.rb +2 -2
- data/app/models/job_invocation_composer.rb +4 -3
- data/app/views/api/v2/job_invocations/base.json.rabl +5 -3
- 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 +1 -12
- data/app/views/templates/script/package_action.erb +11 -1
- data/app/views/templates/script/puppet_run_once.erb +3 -3
- 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 +7 -11
- 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 +109 -81
- 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/JobInvocationDetail/JobInvocationConstants.js +10 -0
- data/webpack/JobInvocationDetail/JobInvocationDetail.scss +38 -0
- data/webpack/JobInvocationDetail/JobInvocationOverview.js +13 -25
- data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +153 -0
- data/webpack/JobInvocationDetail/index.js +48 -10
- data/webpack/JobWizard/Footer.js +5 -1
- data/webpack/JobWizard/JobWizardConstants.js +4 -0
- data/webpack/JobWizard/JobWizardPageRerun.js +3 -0
- data/webpack/JobWizard/JobWizardSelectors.js +31 -3
- data/webpack/JobWizard/PermissionDenied.js +64 -0
- data/webpack/JobWizard/StartsBeforeErrorAlert.js +1 -0
- data/webpack/JobWizard/__tests__/fixtures.js +3 -3
- data/webpack/JobWizard/autofill.js +8 -4
- data/webpack/JobWizard/index.js +41 -1
- data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +8 -1
- data/webpack/JobWizard/steps/AdvancedFields/Fields.js +7 -0
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +41 -7
- data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +7 -3
- data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +1 -0
- data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +21 -7
- data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +1 -0
- data/webpack/JobWizard/steps/HostsAndInputs/index.js +27 -2
- data/webpack/JobWizard/steps/ReviewDetails/index.js +7 -2
- data/webpack/JobWizard/steps/Schedule/PurposeField.js +1 -0
- data/webpack/JobWizard/steps/Schedule/QueryType.js +2 -0
- data/webpack/JobWizard/steps/Schedule/RepeatCron.js +1 -0
- data/webpack/JobWizard/steps/Schedule/RepeatHour.js +2 -0
- data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +1 -0
- data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +1 -0
- data/webpack/JobWizard/steps/Schedule/ScheduleFuture.js +2 -0
- data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +6 -0
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +3 -0
- data/webpack/JobWizard/steps/form/FormHelpers.js +1 -0
- data/webpack/JobWizard/steps/form/GroupedSelectField.js +1 -0
- data/webpack/JobWizard/steps/form/NumberInput.js +1 -0
- data/webpack/JobWizard/steps/form/ResourceSelect.js +1 -0
- data/webpack/JobWizard/steps/form/SearchSelect.js +4 -1
- data/webpack/JobWizard/steps/form/SelectField.js +1 -0
- data/webpack/JobWizard/steps/form/WizardTitle.js +6 -1
- data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +2 -0
- data/webpack/__mocks__/foremanReact/routes/Hosts/constants.js +1 -0
- data/webpack/react_app/components/FeaturesDropdown/index.js +1 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +3 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +2 -1
- data/webpack/react_app/components/RegistrationExtension/RexInterface.js +1 -0
- data/webpack/react_app/components/RegistrationExtension/RexPull.js +1 -0
- data/webpack/react_app/components/RegistrationExtension/__tests__/__snapshots__/RexInterface.test.js.snap +1 -0
- metadata +10 -6
@@ -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}
|
@@ -20,6 +59,7 @@ const JobWizardPage = ({ location: { search } }) => {
|
|
20
59
|
searchable={false}
|
21
60
|
toolbarButtons={
|
22
61
|
<Button
|
62
|
+
ouiaId="legacy-form"
|
23
63
|
variant="link"
|
24
64
|
component="a"
|
25
65
|
href={`/old/job_invocations/new${search}`}
|
@@ -56,7 +56,12 @@ export const DescriptionField = ({
|
|
56
56
|
}
|
57
57
|
fieldId="description"
|
58
58
|
helperText={
|
59
|
-
<Button
|
59
|
+
<Button
|
60
|
+
ouiaId="description-preview-button"
|
61
|
+
variant="link"
|
62
|
+
isInline
|
63
|
+
onClick={togglePreview}
|
64
|
+
>
|
60
65
|
{isPreview
|
61
66
|
? __('Edit job description template')
|
62
67
|
: __('Preview job description')}
|
@@ -68,6 +73,7 @@ export const DescriptionField = ({
|
|
68
73
|
<div>
|
69
74
|
{/* div wrapper so the tooltip will be shown in chrome */}
|
70
75
|
<TextInput
|
76
|
+
ouiaId="description-preview"
|
71
77
|
aria-label="description preview"
|
72
78
|
id="description-preview"
|
73
79
|
value={generatedDesc}
|
@@ -77,6 +83,7 @@ export const DescriptionField = ({
|
|
77
83
|
</Tooltip>
|
78
84
|
) : (
|
79
85
|
<TextInput
|
86
|
+
ouiaId="description-edit"
|
80
87
|
aria-label="description edit"
|
81
88
|
type="text"
|
82
89
|
autoComplete="description"
|
@@ -19,6 +19,7 @@ export const EffectiveUserField = ({ value, setValue, defaultValue }) => (
|
|
19
19
|
labelInfo={<ResetDefault setValue={setValue} defaultValue={defaultValue} />}
|
20
20
|
>
|
21
21
|
<TextInput
|
22
|
+
ouiaId="effective-user"
|
22
23
|
aria-label="effective user"
|
23
24
|
autoComplete="effective-user"
|
24
25
|
id="effective-user"
|
@@ -86,6 +87,7 @@ export const PasswordField = ({ value, setValue }) => (
|
|
86
87
|
fieldId="job-password"
|
87
88
|
>
|
88
89
|
<TextInput
|
90
|
+
ouiaId="job-password"
|
89
91
|
aria-label="job password"
|
90
92
|
autoComplete="new-password" // to prevent firefox from autofilling the user password
|
91
93
|
id="job-password"
|
@@ -109,6 +111,7 @@ export const KeyPassphraseField = ({ value, setValue }) => (
|
|
109
111
|
fieldId="key-passphrase"
|
110
112
|
>
|
111
113
|
<TextInput
|
114
|
+
ouiaId="key-passphrase"
|
112
115
|
aria-label="key passphrase"
|
113
116
|
autoComplete="key-passphrase"
|
114
117
|
id="key-passphrase"
|
@@ -132,6 +135,7 @@ export const EffectiveUserPasswordField = ({ value, setValue }) => (
|
|
132
135
|
fieldId="effective-user-password"
|
133
136
|
>
|
134
137
|
<TextInput
|
138
|
+
ouiaId="effective-user-password"
|
135
139
|
aria-label="effective userpassword"
|
136
140
|
autoComplete="effective-user-password"
|
137
141
|
id="effective-user-password"
|
@@ -186,6 +190,7 @@ export const ExecutionOrderingField = ({ isRandomizedOrdering, setValue }) => (
|
|
186
190
|
isInline
|
187
191
|
>
|
188
192
|
<Radio
|
193
|
+
ouiaId="execution-order-alphabetical"
|
189
194
|
aria-label="execution order alphabetical"
|
190
195
|
isChecked={!isRandomizedOrdering}
|
191
196
|
name="execution-order"
|
@@ -194,6 +199,7 @@ export const ExecutionOrderingField = ({ isRandomizedOrdering, setValue }) => (
|
|
194
199
|
label={__('Alphabetical')}
|
195
200
|
/>
|
196
201
|
<Radio
|
202
|
+
ouiaId="execution-order-randomized"
|
197
203
|
aria-label="execution order randomized"
|
198
204
|
isChecked={isRandomizedOrdering}
|
199
205
|
name="execution-order"
|
@@ -216,6 +222,7 @@ export const SSHUserField = ({ value, setValue, defaultValue }) => (
|
|
216
222
|
labelInfo={<ResetDefault setValue={setValue} defaultValue={defaultValue} />}
|
217
223
|
>
|
218
224
|
<TextInput
|
225
|
+
ouiaId="ssh-user"
|
219
226
|
aria-label="ssh user"
|
220
227
|
autoComplete="ssh-user"
|
221
228
|
id="ssh-user"
|
@@ -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,11 +61,22 @@ 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} />
|
64
|
-
<Text
|
74
|
+
<Text
|
75
|
+
ouiaId="category-and-template-required-fields"
|
76
|
+
component={TextVariants.p}
|
77
|
+
>
|
78
|
+
{__('All fields are required.')}
|
79
|
+
</Text>
|
65
80
|
<Form>
|
66
81
|
<SelectField
|
67
82
|
label={__('Job category')}
|
@@ -69,7 +84,7 @@ export const CategoryAndTemplate = ({
|
|
69
84
|
options={jobCategories}
|
70
85
|
setValue={onSelectCategory}
|
71
86
|
value={selectedCategory}
|
72
|
-
placeholderText={categoryError ? __('
|
87
|
+
placeholderText={categoryError ? __('Not available') : ''}
|
73
88
|
isDisabled={!!categoryError}
|
74
89
|
isRequired
|
75
90
|
/>
|
@@ -82,11 +97,30 @@ export const CategoryAndTemplate = ({
|
|
82
97
|
isDisabled={
|
83
98
|
!!(categoryError || allTemplatesError || isTemplatesLoading)
|
84
99
|
}
|
85
|
-
placeholderText={allTemplatesError ? __('
|
100
|
+
placeholderText={allTemplatesError ? __('Not available') : ''}
|
86
101
|
/>
|
102
|
+
{!isEmpty(missingPermissions) && (
|
103
|
+
<Alert
|
104
|
+
ouiaId="category-and-template-access-denied"
|
105
|
+
variant="warning"
|
106
|
+
title={__('Access denied')}
|
107
|
+
>
|
108
|
+
<span>
|
109
|
+
{__(
|
110
|
+
`Missing the required permissions: ${missingPermissions.join(
|
111
|
+
', '
|
112
|
+
)}`
|
113
|
+
)}
|
114
|
+
</span>
|
115
|
+
</Alert>
|
116
|
+
)}
|
87
117
|
{isError && (
|
88
|
-
<Alert
|
89
|
-
|
118
|
+
<Alert
|
119
|
+
variant="danger"
|
120
|
+
title={__('Errors:')}
|
121
|
+
ouiaId="category-and-template-errors"
|
122
|
+
>
|
123
|
+
{categoryError && isEmpty(missingPermissions) && (
|
90
124
|
<span>
|
91
125
|
{__('Categories list failed with:')} {categoryError}
|
92
126
|
</span>
|
@@ -5,6 +5,7 @@ import URI from 'urijs';
|
|
5
5
|
import { List, ListItem, Modal, Button } from '@patternfly/react-core';
|
6
6
|
import { translate as __, sprintf } from 'foremanReact/common/I18n';
|
7
7
|
import { foremanUrl } from 'foremanReact/common/helpers';
|
8
|
+
import { HOSTS_PATH } from 'foremanReact/routes/Hosts/constants';
|
8
9
|
import { selectHosts, selectHostCount } from '../../JobWizardSelectors';
|
9
10
|
import { HOSTS_TO_PREVIEW_AMOUNT } from '../../JobWizardConstants';
|
10
11
|
|
@@ -15,6 +16,7 @@ export const HostPreviewModal = ({ isOpen, setIsOpen, searchQuery }) => {
|
|
15
16
|
|
16
17
|
return (
|
17
18
|
<Modal
|
19
|
+
ouiaId="host-preview-modal"
|
18
20
|
title={__('Preview Hosts')}
|
19
21
|
isOpen={isOpen}
|
20
22
|
onClose={() => setIsOpen(false)}
|
@@ -22,22 +24,24 @@ export const HostPreviewModal = ({ isOpen, setIsOpen, searchQuery }) => {
|
|
22
24
|
>
|
23
25
|
<List isPlain>
|
24
26
|
{hosts.map(host => (
|
25
|
-
<ListItem key={host}>
|
27
|
+
<ListItem key={host.name}>
|
26
28
|
<Button
|
29
|
+
ouiaId={`host-preview-${host}`}
|
27
30
|
component="a"
|
28
|
-
href={foremanUrl(
|
31
|
+
href={foremanUrl(`${HOSTS_PATH}/${host.name}`)}
|
29
32
|
variant="link"
|
30
33
|
target="_blank"
|
31
34
|
rel="noreferrer"
|
32
35
|
isInline
|
33
36
|
>
|
34
|
-
{host}
|
37
|
+
{host.display_name}
|
35
38
|
</Button>
|
36
39
|
</ListItem>
|
37
40
|
))}
|
38
41
|
{hostsCount > HOSTS_TO_PREVIEW_AMOUNT && (
|
39
42
|
<ListItem>
|
40
43
|
<Button
|
44
|
+
ouiaId="host-preview-more"
|
41
45
|
component="a"
|
42
46
|
href={url.addSearch({ search: searchQuery })}
|
43
47
|
variant="link"
|
@@ -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)
|
@@ -14,6 +14,7 @@ const SelectedChip = ({ selected, setSelected, categoryName }) => {
|
|
14
14
|
return (
|
15
15
|
<>
|
16
16
|
<ChipGroup
|
17
|
+
ouiaId="hosts-chip-group"
|
17
18
|
className="hosts-chip-group"
|
18
19
|
categoryName={categoryName}
|
19
20
|
isClosable
|
@@ -24,14 +25,15 @@ const SelectedChip = ({ selected, setSelected, categoryName }) => {
|
|
24
25
|
setSelected(() => []);
|
25
26
|
}}
|
26
27
|
>
|
27
|
-
{selected.map((
|
28
|
+
{selected.map((result, index) => (
|
28
29
|
<Chip
|
30
|
+
ouiaId={`${categoryName}-${result.id}`}
|
29
31
|
key={index}
|
30
|
-
id={`${categoryName}-${id}`}
|
31
|
-
onClick={() => deleteItem(id)}
|
32
|
-
closeBtnAriaLabel={`Remove ${name}`}
|
32
|
+
id={`${categoryName}-${result.id}`}
|
33
|
+
onClick={() => deleteItem(result.id)}
|
34
|
+
closeBtnAriaLabel={`Remove ${result.name}`}
|
33
35
|
>
|
34
|
-
{
|
36
|
+
{setLabel(result)}
|
35
37
|
</Chip>
|
36
38
|
))}
|
37
39
|
</ChipGroup>
|
@@ -49,6 +51,7 @@ export const SelectedChips = ({
|
|
49
51
|
setSelectedHostGroups,
|
50
52
|
hostsSearchQuery,
|
51
53
|
clearSearch,
|
54
|
+
setLabel,
|
52
55
|
}) => {
|
53
56
|
const clearAll = () => {
|
54
57
|
setSelectedHosts(() => []);
|
@@ -67,16 +70,19 @@ export const SelectedChips = ({
|
|
67
70
|
selected={selectedHosts}
|
68
71
|
categoryName={hostMethods.hosts}
|
69
72
|
setSelected={setSelectedHosts}
|
73
|
+
setLabel={setLabel}
|
70
74
|
/>
|
71
75
|
<SelectedChip
|
72
76
|
selected={selectedHostCollections}
|
73
77
|
categoryName={hostMethods.hostCollections}
|
74
78
|
setSelected={setSelectedHostCollections}
|
79
|
+
setLabel={setLabel}
|
75
80
|
/>
|
76
81
|
<SelectedChip
|
77
82
|
selected={selectedHostGroups}
|
78
83
|
categoryName={hostMethods.hostGroups}
|
79
84
|
setSelected={setSelectedHostGroups}
|
85
|
+
setLabel={setLabel}
|
80
86
|
/>
|
81
87
|
<SelectedChip
|
82
88
|
selected={
|
@@ -86,9 +92,15 @@ export const SelectedChips = ({
|
|
86
92
|
}
|
87
93
|
categoryName={hostMethods.searchQuery}
|
88
94
|
setSelected={clearSearch}
|
95
|
+
setLabel={setLabel}
|
89
96
|
/>
|
90
97
|
{showClear && (
|
91
|
-
<Button
|
98
|
+
<Button
|
99
|
+
ouiaId="clear-chips"
|
100
|
+
variant="link"
|
101
|
+
className="clear-chips"
|
102
|
+
onClick={clearAll}
|
103
|
+
>
|
92
104
|
{__('Clear all filters')}
|
93
105
|
</Button>
|
94
106
|
)}
|
@@ -105,10 +117,12 @@ SelectedChips.propTypes = {
|
|
105
117
|
setSelectedHostGroups: PropTypes.func.isRequired,
|
106
118
|
hostsSearchQuery: PropTypes.string.isRequired,
|
107
119
|
clearSearch: PropTypes.func.isRequired,
|
120
|
+
setLabel: PropTypes.func.isRequired,
|
108
121
|
};
|
109
122
|
|
110
123
|
SelectedChip.propTypes = {
|
111
124
|
categoryName: PropTypes.string.isRequired,
|
112
125
|
selected: PropTypes.array.isRequired,
|
113
126
|
setSelected: PropTypes.func.isRequired,
|
127
|
+
setLabel: PropTypes.func.isRequired,
|
114
128
|
};
|
@@ -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,10 +227,12 @@ const HostsAndInputs = ({
|
|
219
227
|
setSelectedHostGroups={setSelectedHostGroups}
|
220
228
|
hostsSearchQuery={hostsSearchQuery}
|
221
229
|
clearSearch={clearSearch}
|
230
|
+
setLabel={setLabel}
|
222
231
|
/>
|
223
|
-
<Text>
|
232
|
+
<Text ouiaId="host-preview-label">
|
224
233
|
{__('Apply to')}{' '}
|
225
234
|
<Button
|
235
|
+
ouiaId="host-preview-open-button"
|
226
236
|
variant="link"
|
227
237
|
isInline
|
228
238
|
onClick={() => setHostPreviewOpen(true)}
|
@@ -237,6 +247,21 @@ const HostsAndInputs = ({
|
|
237
247
|
value={templateValues}
|
238
248
|
setValue={setTemplateValues}
|
239
249
|
/>
|
250
|
+
{!isEmpty(missingPermissions) && (
|
251
|
+
<Alert
|
252
|
+
ouiaId="host-access-denied"
|
253
|
+
variant="warning"
|
254
|
+
title={__('Access denied')}
|
255
|
+
>
|
256
|
+
<span>
|
257
|
+
{__(
|
258
|
+
`Missing the required permissions: ${missingPermissions.join(
|
259
|
+
', '
|
260
|
+
)}`
|
261
|
+
)}
|
262
|
+
</span>
|
263
|
+
</Alert>
|
264
|
+
)}
|
240
265
|
</Form>
|
241
266
|
</div>
|
242
267
|
);
|
@@ -46,6 +46,7 @@ const ReviewDetails = ({
|
|
46
46
|
// eslint-disable-next-line react/prop-types
|
47
47
|
const StepButton = ({ stepName, children }) => (
|
48
48
|
<Button
|
49
|
+
ouiaId={`step-button-${stepName}`}
|
49
50
|
variant="link"
|
50
51
|
isInline
|
51
52
|
onClick={() => {
|
@@ -79,17 +80,19 @@ const ReviewDetails = ({
|
|
79
80
|
|
80
81
|
const hostsCount = useSelector(selectHostCount);
|
81
82
|
const [hostPreviewOpen, setHostPreviewOpen] = useState(false);
|
83
|
+
const NUM_CHIPS = 3;
|
82
84
|
const stringHosts = () => {
|
83
85
|
if (hosts.length === 0) {
|
84
86
|
return __('No Target Hosts');
|
85
87
|
}
|
86
|
-
if (hosts.length
|
87
|
-
return hosts.join(', ');
|
88
|
+
if (hosts.length < NUM_CHIPS) {
|
89
|
+
return hosts.map(host => host.display_name).join(', ');
|
88
90
|
}
|
89
91
|
return (
|
90
92
|
<div>
|
91
93
|
{hostsCount} {__('hosts')}{' '}
|
92
94
|
<Button
|
95
|
+
ouiaId="view-host-names"
|
93
96
|
variant="link"
|
94
97
|
isInline
|
95
98
|
onClick={() => setHostPreviewOpen(true)}
|
@@ -139,6 +142,7 @@ const ReviewDetails = ({
|
|
139
142
|
),
|
140
143
|
value: isAdvancedShown ? (
|
141
144
|
<Button
|
145
|
+
ouiaId="hide-advanced-fields"
|
142
146
|
variant="link"
|
143
147
|
isInline
|
144
148
|
onClick={() => {
|
@@ -149,6 +153,7 @@ const ReviewDetails = ({
|
|
149
153
|
</Button>
|
150
154
|
) : (
|
151
155
|
<Button
|
156
|
+
ouiaId="show-advanced-fields"
|
152
157
|
variant="link"
|
153
158
|
isInline
|
154
159
|
onClick={() => {
|
@@ -15,6 +15,7 @@ export const QueryType = ({ isTypeStatic, setIsTypeStatic }) => (
|
|
15
15
|
)}
|
16
16
|
>
|
17
17
|
<Radio
|
18
|
+
ouiaId="query-type-static"
|
18
19
|
isChecked={isTypeStatic}
|
19
20
|
name="query-type"
|
20
21
|
onChange={() => setIsTypeStatic(true)}
|
@@ -25,6 +26,7 @@ export const QueryType = ({ isTypeStatic, setIsTypeStatic }) => (
|
|
25
26
|
</FormGroup>
|
26
27
|
<FormGroup fieldId="query-type-dynamic">
|
27
28
|
<Radio
|
29
|
+
ouiaId="query-type-dynamic"
|
28
30
|
isChecked={!isTypeStatic}
|
29
31
|
name="query-type"
|
30
32
|
onChange={() => setIsTypeStatic(false)}
|