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.
Files changed (129) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintrc +4 -1
  3. data/.github/workflows/js_ci.yml +1 -1
  4. data/.github/workflows/release.yml +4 -2
  5. data/.github/workflows/ruby_ci.yml +16 -81
  6. data/.packit.yaml +8 -3
  7. data/app/assets/javascripts/foreman_remote_execution/locale/de/foreman_remote_execution.js +23 -14
  8. data/app/assets/javascripts/foreman_remote_execution/locale/en/foreman_remote_execution.js +22 -4
  9. data/app/assets/javascripts/foreman_remote_execution/locale/en_GB/foreman_remote_execution.js +23 -14
  10. data/app/assets/javascripts/foreman_remote_execution/locale/es/foreman_remote_execution.js +23 -14
  11. data/app/assets/javascripts/foreman_remote_execution/locale/fr/foreman_remote_execution.js +23 -14
  12. data/app/assets/javascripts/foreman_remote_execution/locale/ja/foreman_remote_execution.js +23 -14
  13. data/app/assets/javascripts/foreman_remote_execution/locale/ka/foreman_remote_execution.js +23 -14
  14. data/app/assets/javascripts/foreman_remote_execution/locale/ko/foreman_remote_execution.js +23 -14
  15. data/app/assets/javascripts/foreman_remote_execution/locale/pt_BR/foreman_remote_execution.js +23 -14
  16. data/app/assets/javascripts/foreman_remote_execution/locale/ru/foreman_remote_execution.js +23 -14
  17. data/app/assets/javascripts/foreman_remote_execution/locale/zh_CN/foreman_remote_execution.js +23 -14
  18. data/app/assets/javascripts/foreman_remote_execution/locale/zh_TW/foreman_remote_execution.js +23 -14
  19. data/app/controllers/ui_job_wizard_controller.rb +1 -1
  20. data/app/helpers/job_invocations_helper.rb +1 -1
  21. data/app/helpers/remote_execution_helper.rb +2 -2
  22. data/app/lib/actions/remote_execution/proxy_action.rb +1 -1
  23. data/app/lib/actions/remote_execution/run_host_job.rb +9 -17
  24. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
  25. data/app/models/host_status/execution_status.rb +2 -2
  26. data/app/models/job_invocation_composer.rb +4 -3
  27. data/app/views/api/v2/job_invocations/base.json.rabl +5 -3
  28. data/app/views/job_invocations/_preview_hosts_list.html.erb +1 -1
  29. data/app/views/job_invocations/show.html.erb +12 -5
  30. data/app/views/job_invocations/show.js.erb +8 -1
  31. data/app/views/job_invocations/welcome.html.erb +1 -1
  32. data/app/views/template_invocations/_refresh.js.erb +10 -4
  33. data/app/views/template_invocations/show.html.erb +2 -2
  34. data/app/views/templates/script/convert2rhel_analyze.erb +1 -12
  35. data/app/views/templates/script/package_action.erb +11 -1
  36. data/app/views/templates/script/puppet_run_once.erb +3 -3
  37. data/lib/foreman_remote_execution/engine.rb +1 -1
  38. data/lib/foreman_remote_execution/version.rb +1 -1
  39. data/locale/de/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  40. data/locale/de/foreman_remote_execution.po +24 -6
  41. data/locale/en/foreman_remote_execution.po +24 -6
  42. data/locale/en_GB/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  43. data/locale/en_GB/foreman_remote_execution.po +24 -6
  44. data/locale/es/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  45. data/locale/es/foreman_remote_execution.po +24 -6
  46. data/locale/foreman_remote_execution.pot +170 -142
  47. data/locale/fr/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  48. data/locale/fr/foreman_remote_execution.po +24 -6
  49. data/locale/ja/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  50. data/locale/ja/foreman_remote_execution.po +24 -6
  51. data/locale/ka/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  52. data/locale/ka/foreman_remote_execution.po +24 -6
  53. data/locale/ko/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  54. data/locale/ko/foreman_remote_execution.po +24 -6
  55. data/locale/pt_BR/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  56. data/locale/pt_BR/foreman_remote_execution.po +24 -6
  57. data/locale/ru/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  58. data/locale/ru/foreman_remote_execution.po +24 -6
  59. data/locale/zh_CN/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  60. data/locale/zh_CN/foreman_remote_execution.po +24 -6
  61. data/locale/zh_TW/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  62. data/locale/zh_TW/foreman_remote_execution.po +24 -6
  63. data/package.json +7 -11
  64. data/test/functional/api/v2/job_invocations_controller_test.rb +7 -7
  65. data/test/functional/api/v2/template_invocations_controller_test.rb +3 -3
  66. data/test/helpers/remote_execution_helper_test.rb +8 -7
  67. data/test/unit/actions/run_host_job_test.rb +1 -1
  68. data/test/unit/actions/run_hosts_job_test.rb +11 -11
  69. data/test/unit/concerns/foreman_tasks_cleaner_extensions_test.rb +5 -5
  70. data/test/unit/concerns/host_extensions_test.rb +34 -34
  71. data/test/unit/concerns/nic_extensions_test.rb +1 -1
  72. data/test/unit/execution_task_status_mapper_test.rb +10 -10
  73. data/test/unit/input_template_renderer_test.rb +53 -49
  74. data/test/unit/job_invocation_composer_test.rb +109 -81
  75. data/test/unit/job_invocation_test.rb +25 -25
  76. data/test/unit/job_template_effective_user_test.rb +3 -3
  77. data/test/unit/job_template_test.rb +28 -28
  78. data/test/unit/remote_execution_feature_test.rb +14 -14
  79. data/test/unit/remote_execution_provider_test.rb +39 -39
  80. data/test/unit/renderer_scope_input_test.rb +6 -6
  81. data/test/unit/targeting_test.rb +32 -32
  82. data/webpack/JobInvocationDetail/JobInvocationConstants.js +10 -0
  83. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +38 -0
  84. data/webpack/JobInvocationDetail/JobInvocationOverview.js +13 -25
  85. data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +153 -0
  86. data/webpack/JobInvocationDetail/index.js +48 -10
  87. data/webpack/JobWizard/Footer.js +5 -1
  88. data/webpack/JobWizard/JobWizardConstants.js +4 -0
  89. data/webpack/JobWizard/JobWizardPageRerun.js +3 -0
  90. data/webpack/JobWizard/JobWizardSelectors.js +31 -3
  91. data/webpack/JobWizard/PermissionDenied.js +64 -0
  92. data/webpack/JobWizard/StartsBeforeErrorAlert.js +1 -0
  93. data/webpack/JobWizard/__tests__/fixtures.js +3 -3
  94. data/webpack/JobWizard/autofill.js +8 -4
  95. data/webpack/JobWizard/index.js +41 -1
  96. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +8 -1
  97. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +7 -0
  98. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +41 -7
  99. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +7 -3
  100. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +1 -0
  101. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +21 -7
  102. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +1 -0
  103. data/webpack/JobWizard/steps/HostsAndInputs/index.js +27 -2
  104. data/webpack/JobWizard/steps/ReviewDetails/index.js +7 -2
  105. data/webpack/JobWizard/steps/Schedule/PurposeField.js +1 -0
  106. data/webpack/JobWizard/steps/Schedule/QueryType.js +2 -0
  107. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +1 -0
  108. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +2 -0
  109. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +1 -0
  110. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +1 -0
  111. data/webpack/JobWizard/steps/Schedule/ScheduleFuture.js +2 -0
  112. data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +6 -0
  113. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +3 -0
  114. data/webpack/JobWizard/steps/form/FormHelpers.js +1 -0
  115. data/webpack/JobWizard/steps/form/GroupedSelectField.js +1 -0
  116. data/webpack/JobWizard/steps/form/NumberInput.js +1 -0
  117. data/webpack/JobWizard/steps/form/ResourceSelect.js +1 -0
  118. data/webpack/JobWizard/steps/form/SearchSelect.js +4 -1
  119. data/webpack/JobWizard/steps/form/SelectField.js +1 -0
  120. data/webpack/JobWizard/steps/form/WizardTitle.js +6 -1
  121. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +2 -0
  122. data/webpack/__mocks__/foremanReact/routes/Hosts/constants.js +1 -0
  123. data/webpack/react_app/components/FeaturesDropdown/index.js +1 -0
  124. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +3 -0
  125. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +2 -1
  126. data/webpack/react_app/components/RegistrationExtension/RexInterface.js +1 -0
  127. data/webpack/react_app/components/RegistrationExtension/RexPull.js +1 -0
  128. data/webpack/react_app/components/RegistrationExtension/__tests__/__snapshots__/RexInterface.test.js.snap +1 -0
  129. 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;
@@ -5,6 +5,7 @@ import { translate as __ } from 'foremanReact/common/I18n';
5
5
  export const StartsBeforeErrorAlert = () => (
6
6
  <>
7
7
  <Alert
8
+ ouiaId="starts-before-error-alert"
8
9
  variant="danger"
9
10
  title={__("'Starts before' date must in the future")}
10
11
  >
@@ -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}
@@ -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 variant="link" isInline onClick={togglePreview}>
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 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} />
64
- <Text component={TextVariants.p}>{__('All fields are required.')}</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 ? __('Error') : ''}
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 ? __('Error') : ''}
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 variant="danger" title={__('Errors:')}>
89
- {categoryError && (
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(`/hosts/${host}`)}
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"
@@ -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)
@@ -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(({ name, id }, index) => (
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
- {name}
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 variant="link" className="clear-chips" onClick={clearAll}>
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
  };
@@ -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,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 === 1 || hosts.length === 2) {
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={() => {
@@ -14,6 +14,7 @@ export const PurposeField = ({ purpose, setPurpose }) => (
14
14
  )}
15
15
  >
16
16
  <TextInput
17
+ ouiaId="purpose"
17
18
  aria-label="purpose"
18
19
  type="text"
19
20
  value={purpose}
@@ -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)}
@@ -65,6 +65,7 @@ export const RepeatCron = ({ repeatData, setRepeatData, setValid }) => {
65
65
  isRequired
66
66
  >
67
67
  <TextInput
68
+ ouiaId="cronline"
68
69
  isRequired
69
70
  validated={cronline ? ValidatedOptions.noval : ValidatedOptions.error}
70
71
  aria-label="cronline"