katello 4.12.0 → 4.13.0.rc1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of katello might be problematic. Click here for more details.

Files changed (239) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +0 -1
  3. data/app/assets/javascripts/katello/locale/bn/katello.js +3365 -3350
  4. data/app/assets/javascripts/katello/locale/bn_IN/katello.js +3136 -3121
  5. data/app/assets/javascripts/katello/locale/ca/katello.js +3588 -3576
  6. data/app/assets/javascripts/katello/locale/cs/katello.js +3499 -3487
  7. data/app/assets/javascripts/katello/locale/cs_CZ/katello.js +4186 -4186
  8. data/app/assets/javascripts/katello/locale/de/katello.js +5553 -5562
  9. data/app/assets/javascripts/katello/locale/de_AT/katello.js +3008 -2993
  10. data/app/assets/javascripts/katello/locale/de_DE/katello.js +3066 -3051
  11. data/app/assets/javascripts/katello/locale/el/katello.js +3376 -3370
  12. data/app/assets/javascripts/katello/locale/en/katello.js +3008 -2993
  13. data/app/assets/javascripts/katello/locale/en_GB/katello.js +3076 -3073
  14. data/app/assets/javascripts/katello/locale/en_US/katello.js +3008 -2993
  15. data/app/assets/javascripts/katello/locale/es/katello.js +5366 -5372
  16. data/app/assets/javascripts/katello/locale/et_EE/katello.js +3008 -2993
  17. data/app/assets/javascripts/katello/locale/fr/katello.js +5975 -5984
  18. data/app/assets/javascripts/katello/locale/gl/katello.js +3125 -3113
  19. data/app/assets/javascripts/katello/locale/gu/katello.js +3119 -3104
  20. data/app/assets/javascripts/katello/locale/he_IL/katello.js +3020 -3005
  21. data/app/assets/javascripts/katello/locale/hi/katello.js +3137 -3122
  22. data/app/assets/javascripts/katello/locale/id/katello.js +3008 -2993
  23. data/app/assets/javascripts/katello/locale/it/katello.js +4469 -4466
  24. data/app/assets/javascripts/katello/locale/ja/katello.js +5969 -5978
  25. data/app/assets/javascripts/katello/locale/ka/katello.js +5649 -5652
  26. data/app/assets/javascripts/katello/locale/kn/katello.js +3136 -3121
  27. data/app/assets/javascripts/katello/locale/ko/katello.js +4717 -4720
  28. data/app/assets/javascripts/katello/locale/locale/katello.js +1050 -1084
  29. data/app/assets/javascripts/katello/locale/ml_IN/katello.js +3008 -2993
  30. data/app/assets/javascripts/katello/locale/mr/katello.js +3136 -3121
  31. data/app/assets/javascripts/katello/locale/nl_NL/katello.js +3116 -3101
  32. data/app/assets/javascripts/katello/locale/or/katello.js +3137 -3122
  33. data/app/assets/javascripts/katello/locale/pa/katello.js +3136 -3121
  34. data/app/assets/javascripts/katello/locale/pl/katello.js +3210 -3195
  35. data/app/assets/javascripts/katello/locale/pl_PL/katello.js +3008 -2993
  36. data/app/assets/javascripts/katello/locale/pt/katello.js +3009 -2994
  37. data/app/assets/javascripts/katello/locale/pt_BR/katello.js +5362 -5368
  38. data/app/assets/javascripts/katello/locale/ro/katello.js +3008 -2993
  39. data/app/assets/javascripts/katello/locale/ro_RO/katello.js +3008 -2993
  40. data/app/assets/javascripts/katello/locale/ru/katello.js +4638 -4641
  41. data/app/assets/javascripts/katello/locale/sl/katello.js +3051 -3036
  42. data/app/assets/javascripts/katello/locale/sv_SE/katello.js +3156 -3144
  43. data/app/assets/javascripts/katello/locale/ta/katello.js +3365 -3350
  44. data/app/assets/javascripts/katello/locale/ta_IN/katello.js +3121 -3106
  45. data/app/assets/javascripts/katello/locale/te/katello.js +3136 -3121
  46. data/app/assets/javascripts/katello/locale/tr/katello.js +3025 -3010
  47. data/app/assets/javascripts/katello/locale/vi/katello.js +3008 -2993
  48. data/app/assets/javascripts/katello/locale/vi_VN/katello.js +3008 -2993
  49. data/app/assets/javascripts/katello/locale/zh/katello.js +3008 -2993
  50. data/app/assets/javascripts/katello/locale/zh_CN/katello.js +5968 -5977
  51. data/app/assets/javascripts/katello/locale/zh_TW/katello.js +4694 -4697
  52. data/app/assets/javascripts/katello/sync_management/sync_management.js +1 -0
  53. data/app/controllers/katello/api/registry/registry_proxies_controller.rb +51 -124
  54. data/app/controllers/katello/api/rhsm/candlepin_dynflow_proxy_controller.rb +12 -20
  55. data/app/controllers/katello/api/v2/activation_keys_controller.rb +10 -4
  56. data/app/controllers/katello/api/v2/capsule_content_controller.rb +24 -0
  57. data/app/controllers/katello/api/v2/content_view_versions_controller.rb +9 -2
  58. data/app/controllers/katello/api/v2/debs_controller.rb +1 -1
  59. data/app/controllers/katello/api/v2/errata_controller.rb +1 -1
  60. data/app/controllers/katello/api/v2/host_subscriptions_controller.rb +12 -4
  61. data/app/controllers/katello/api/v2/hosts_bulk_actions_controller.rb +3 -3
  62. data/app/controllers/katello/api/v2/organizations_controller.rb +0 -11
  63. data/app/controllers/katello/api/v2/packages_controller.rb +1 -1
  64. data/app/controllers/katello/api/v2/products_bulk_actions_controller.rb +1 -1
  65. data/app/controllers/katello/api/v2/repositories_controller.rb +18 -12
  66. data/app/controllers/katello/api/v2/repository_sets_controller.rb +2 -1
  67. data/app/controllers/katello/api/v2/simple_content_access_controller.rb +9 -22
  68. data/app/controllers/katello/concerns/api/v2/authorization.rb +1 -1
  69. data/app/helpers/katello/katello_urls_helper.rb +26 -1
  70. data/app/helpers/katello/subscription_mailer_helper.rb +1 -1
  71. data/app/jobs/create_manifest_expire_soon_warning_notifications.rb +11 -0
  72. data/app/lib/actions/candlepin/owner/regenerate_upstream_identity_cert.rb +21 -0
  73. data/app/lib/actions/katello/capsule_content/sync.rb +1 -1
  74. data/app/lib/actions/katello/capsule_content/verify_checksum.rb +75 -0
  75. data/app/lib/actions/katello/content_view/promote.rb +1 -1
  76. data/app/lib/actions/katello/content_view/publish.rb +1 -1
  77. data/app/lib/actions/katello/content_view_version/verify_checksum.rb +29 -0
  78. data/app/lib/actions/katello/host/hypervisors_update.rb +1 -0
  79. data/app/lib/actions/katello/host/update_content_view.rb +2 -2
  80. data/app/lib/actions/katello/organization/manifest_import.rb +5 -0
  81. data/app/lib/actions/katello/organization/manifest_refresh.rb +3 -0
  82. data/app/lib/actions/katello/repository/metadata_generate.rb +7 -1
  83. data/app/lib/actions/katello/repository/remove_content.rb +1 -0
  84. data/app/lib/actions/katello/repository/sync.rb +2 -1
  85. data/app/lib/actions/katello/repository/upload_files.rb +1 -0
  86. data/app/lib/actions/pulp3/capsule_content/verify_checksum.rb +27 -0
  87. data/app/lib/actions/pulp3/orchestration/content_view_version/export_repository.rb +7 -9
  88. data/app/lib/actions/pulp3/orchestration/content_view_version/syncable_export.rb +5 -4
  89. data/app/lib/katello/concerns/base_template_scope_extensions.rb +7 -2
  90. data/app/lib/katello/errors.rb +4 -0
  91. data/app/lib/katello/http_resource.rb +6 -1
  92. data/app/lib/katello/resources/candlepin/consumer.rb +1 -1
  93. data/app/lib/katello/resources/candlepin/upstream_consumer.rb +18 -6
  94. data/app/lib/katello/resources/candlepin/upstream_job.rb +1 -1
  95. data/app/lib/katello/resources/registry.rb +25 -0
  96. data/app/mailers/katello/subscription_mailer.rb +3 -6
  97. data/app/models/katello/candlepin/repository_mapper.rb +1 -1
  98. data/app/models/katello/concerns/organization_extensions.rb +42 -3
  99. data/app/models/katello/content_view.rb +28 -0
  100. data/app/models/katello/content_view_environment_content_facet.rb +4 -2
  101. data/app/models/katello/glue/provider.rb +19 -12
  102. data/app/models/katello/glue/pulp/repos.rb +3 -8
  103. data/app/models/katello/host/content_facet.rb +1 -1
  104. data/app/models/katello/host/subscription_facet.rb +1 -1
  105. data/app/models/katello/host_collection.rb +12 -3
  106. data/app/models/katello/ping.rb +1 -1
  107. data/app/models/katello/repository.rb +33 -0
  108. data/app/models/katello/root_repository.rb +0 -4
  109. data/app/services/katello/content_unit_indexer.rb +9 -0
  110. data/app/services/katello/pulp3/alternate_content_source.rb +4 -6
  111. data/app/services/katello/pulp3/api/core.rb +13 -0
  112. data/app/services/katello/pulp3/api/yum.rb +11 -0
  113. data/app/services/katello/pulp3/docker_manifest.rb +5 -1
  114. data/app/services/katello/pulp3/repository/generic.rb +1 -1
  115. data/app/services/katello/pulp3/repository.rb +26 -6
  116. data/app/services/katello/pulp3/repository_mirror.rb +13 -12
  117. data/app/services/katello/pulp3/service_common.rb +2 -10
  118. data/app/services/katello/pulp3/smart_proxy_repository.rb +0 -2
  119. data/app/services/katello/ui_notifications/subscriptions/manifest_expire_soon_warning.rb +75 -0
  120. data/app/views/foreman/job_templates/update_package_-_katello_ansible_default.erb +5 -1
  121. data/app/views/foreman/job_templates/update_packages_by_search_query_-_katello_ansible_default.erb +2 -2
  122. data/app/views/foreman/job_templates/upload_profile.erb +16 -0
  123. data/app/views/katello/api/v2/content_view_filter_rules/show.json.rabl +9 -0
  124. data/app/views/katello/api/v2/docker_manifests/show.json.rabl +1 -0
  125. data/app/views/katello/api/v2/hosts/host_collections.json.rabl +5 -1
  126. data/app/views/katello/api/v2/organizations/show.json.rabl +9 -1
  127. data/app/views/katello/hosts/_errata_counts.html.erb +1 -1
  128. data/app/views/overrides/activation_keys/_host_environment_select.html.erb +1 -1
  129. data/app/views/overrides/activation_keys/_host_media_type_select.html.erb +15 -5
  130. data/app/views/overrides/activation_keys/_host_tab_pane.html.erb +1 -29
  131. data/config/routes/api/registry.rb +4 -8
  132. data/config/routes/api/v2.rb +2 -0
  133. data/db/migrate/20240423112842_add_fields_to_katello_docker_manifest.rb +8 -0
  134. data/db/migrate/20240502192021_change_katello_repository_rpms_id_seq_to_big_int.rb +9 -0
  135. data/db/seeds.d/109-katello-notification-blueprints.rb +6 -0
  136. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/activation-keys/details/activation-key-repository-sets.controller.js +3 -3
  137. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/content-credentials/new/views/new-content-credential.html +2 -1
  138. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/content-hosts/details/content-host-repository-sets.controller.js +3 -3
  139. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/i18n/bastion_katello.pot +0 -15
  140. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/products/details/repositories/details/views/repository-info.html +8 -6
  141. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/products/details/repositories/new/views/new-repository.html +12 -10
  142. data/lib/katello/permission_creator.rb +3 -3
  143. data/lib/katello/permissions/registry_permissions.rb +4 -7
  144. data/lib/katello/plugin.rb +10 -9
  145. data/lib/katello/repository_types/ostree.rb +7 -0
  146. data/lib/katello/scheduled_jobs.rb +7 -1
  147. data/lib/katello/tasks/clean_backend_objects.rake +1 -1
  148. data/lib/katello/tasks/repository.rake +22 -0
  149. data/lib/katello/version.rb +1 -1
  150. data/locale/action_names.rb +4 -3
  151. data/locale/bn/katello.po +166 -151
  152. data/locale/bn_IN/katello.po +166 -151
  153. data/locale/ca/katello.po +166 -151
  154. data/locale/cs/katello.po +166 -151
  155. data/locale/cs_CZ/LC_MESSAGES/katello.mo +0 -0
  156. data/locale/cs_CZ/katello.po +172 -157
  157. data/locale/de/LC_MESSAGES/katello.mo +0 -0
  158. data/locale/de/katello.po +178 -163
  159. data/locale/de_AT/katello.po +166 -151
  160. data/locale/de_DE/katello.po +166 -151
  161. data/locale/el/katello.po +166 -151
  162. data/locale/en/katello.po +166 -151
  163. data/locale/en_GB/katello.po +166 -151
  164. data/locale/en_US/katello.po +166 -151
  165. data/locale/es/LC_MESSAGES/katello.mo +0 -0
  166. data/locale/es/katello.po +178 -163
  167. data/locale/et_EE/katello.po +166 -151
  168. data/locale/fr/LC_MESSAGES/katello.mo +0 -0
  169. data/locale/fr/katello.po +179 -164
  170. data/locale/gl/katello.po +166 -151
  171. data/locale/gu/katello.po +166 -151
  172. data/locale/he_IL/katello.po +166 -151
  173. data/locale/hi/katello.po +166 -151
  174. data/locale/id/katello.po +166 -151
  175. data/locale/it/LC_MESSAGES/katello.mo +0 -0
  176. data/locale/it/katello.po +169 -154
  177. data/locale/ja/LC_MESSAGES/katello.mo +0 -0
  178. data/locale/ja/katello.po +179 -164
  179. data/locale/ka/LC_MESSAGES/katello.mo +0 -0
  180. data/locale/ka/katello.po +177 -162
  181. data/locale/katello.pot +1119 -1062
  182. data/locale/kn/katello.po +166 -151
  183. data/locale/ko/LC_MESSAGES/katello.mo +0 -0
  184. data/locale/ko/katello.po +174 -159
  185. data/locale/ml_IN/katello.po +166 -151
  186. data/locale/mr/katello.po +166 -151
  187. data/locale/nl_NL/katello.po +166 -151
  188. data/locale/or/katello.po +166 -151
  189. data/locale/pa/katello.po +166 -151
  190. data/locale/pl/katello.po +166 -151
  191. data/locale/pl_PL/katello.po +166 -151
  192. data/locale/pt/katello.po +166 -151
  193. data/locale/pt_BR/LC_MESSAGES/katello.mo +0 -0
  194. data/locale/pt_BR/katello.po +178 -163
  195. data/locale/ro/katello.po +166 -151
  196. data/locale/ro_RO/katello.po +166 -151
  197. data/locale/ru/LC_MESSAGES/katello.mo +0 -0
  198. data/locale/ru/katello.po +171 -156
  199. data/locale/sl/katello.po +166 -151
  200. data/locale/sv_SE/katello.po +166 -151
  201. data/locale/ta/katello.po +166 -151
  202. data/locale/ta_IN/katello.po +166 -151
  203. data/locale/te/katello.po +166 -151
  204. data/locale/tr/katello.po +166 -151
  205. data/locale/vi/katello.po +166 -151
  206. data/locale/vi_VN/katello.po +166 -151
  207. data/locale/zh/katello.po +166 -151
  208. data/locale/zh_CN/LC_MESSAGES/katello.mo +0 -0
  209. data/locale/zh_CN/katello.po +179 -164
  210. data/locale/zh_TW/LC_MESSAGES/katello.mo +0 -0
  211. data/locale/zh_TW/katello.po +171 -156
  212. data/webpack/ForemanColumnExtensions/index.js +129 -0
  213. data/webpack/components/ActivationKeysSearch/ActivationKeysSearch.test.js +28 -0
  214. data/webpack/components/ActivationKeysSearch/index.js +222 -0
  215. data/webpack/components/Table/TableWrapper.js +14 -0
  216. data/webpack/components/extensions/HostDetails/ActionsBar/index.js +1 -1
  217. data/webpack/components/extensions/HostDetails/Tabs/PackagesTab/PackagesTab.js +1 -1
  218. data/webpack/components/extensions/HostDetails/Tabs/__tests__/packageInstallModal.test.js +1 -0
  219. data/webpack/components/extensions/HostDetails/Tabs/__tests__/packagesTab.test.js +1 -0
  220. data/webpack/components/extensions/Hosts/ActionsBar/index.js +20 -1
  221. data/webpack/components/extensions/Hosts/BulkActions/BulkChangeHostCVModal/BulkChangeHostCVModal.js +220 -0
  222. data/webpack/components/extensions/Hosts/BulkActions/BulkChangeHostCVModal/actions.js +23 -0
  223. data/webpack/components/extensions/Hosts/BulkActions/BulkChangeHostCVModal/index.js +25 -0
  224. data/webpack/components/extensions/Hosts/BulkActions/__tests__/bulkChangeHostCVModal.test.js +133 -0
  225. data/webpack/global_index.js +19 -0
  226. data/webpack/scenes/ContentViews/Details/ComponentContentViews/ContentViewComponents.js +6 -3
  227. data/webpack/scenes/Hosts/ChangeContentSource/actions.js +3 -1
  228. data/webpack/scenes/Hosts/ChangeContentSource/components/ContentSourceForm.js +63 -25
  229. data/webpack/scenes/Hosts/ChangeContentSource/index.js +24 -16
  230. data/webpack/scenes/RedHatRepositories/__tests__/__snapshots__/RedHatRepositoriesPage.test.js.snap +1 -0
  231. data/webpack/scenes/Subscriptions/Manifest/ManageManifestModal.js +64 -5
  232. data/webpack/scenes/Subscriptions/UpstreamSubscriptions/UpstreamSubscriptionsPage.js +16 -13
  233. data/webpack/scenes/Subscriptions/UpstreamSubscriptions/__tests__/__snapshots__/UpstreamSubscriptionsPage.test.js.snap +14 -8
  234. data/webpack/scenes/Subscriptions/__tests__/__snapshots__/SubscriptionsPage.test.js.snap +1 -0
  235. data/webpack/scenes/Subscriptions/components/SubscriptionsToolbar/SubscriptionsToolbar.js +1 -1
  236. metadata +61 -42
  237. data/app/assets/javascripts/katello/hosts/activation_key_edit.js +0 -167
  238. data/app/lib/actions/katello/host/upload_package_profile.rb +0 -45
  239. data/app/lib/actions/katello/host/upload_profiles.rb +0 -47
@@ -0,0 +1,220 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useDispatch, useSelector } from 'react-redux';
4
+ import { FormattedMessage } from 'react-intl';
5
+ import { Modal, Button, Alert, TextContent, Text, TextVariants } from '@patternfly/react-core';
6
+ import { translate as __ } from 'foremanReact/common/I18n';
7
+ import { STATUS } from 'foremanReact/constants';
8
+ import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
9
+ import { selectAPIStatus } from 'foremanReact/redux/API/APISelectors';
10
+ import { ENVIRONMENT_PATHS_KEY } from '../../../../../scenes/ContentViews/components/EnvironmentPaths/EnvironmentPathConstants';
11
+ import EnvironmentPaths from '../../../../../scenes/ContentViews/components/EnvironmentPaths/EnvironmentPaths';
12
+ import ContentViewSelect from '../../../../../scenes/ContentViews/components/ContentViewSelect/ContentViewSelect';
13
+ import ContentViewSelectOption from '../../../../../scenes/ContentViews/components/ContentViewSelect/ContentViewSelectOption';
14
+ import api from '../../../../../services/api';
15
+ import getContentViews from '../../../../../scenes/ContentViews/ContentViewsActions';
16
+ import { selectContentViews, selectContentViewStatus } from '../../../../../scenes/ContentViews/ContentViewSelectors';
17
+ import { bulkUpdateHostContentViewAndEnvironment } from './actions';
18
+ import { getCVPlaceholderText } from '../../../../../scenes/ContentViews/components/ContentViewSelect/helpers';
19
+ import HOST_CV_AND_ENV_KEY from '../../../HostDetails/Cards/ContentViewDetailsCard/HostContentViewConstants';
20
+
21
+ const ENV_PATH_OPTIONS = { key: ENVIRONMENT_PATHS_KEY };
22
+
23
+ const BulkChangeHostCVModal = ({
24
+ isOpen,
25
+ closeModal,
26
+ selectedCount,
27
+ orgId,
28
+ fetchBulkParams,
29
+ }) => {
30
+ const [selectedLifecycleEnv, setSelectedLifecycleEnv]
31
+ = useState([]);
32
+
33
+ const [selectedContentView, setSelectedContentView] = useState(null);
34
+ const [cvSelectOpen, setCVSelectOpen] = useState(false);
35
+ const dispatch = useDispatch();
36
+ const contentViewsInEnvResponse = useSelector(state => selectContentViews(state, '_FOR_DEFAULT_ENV'));
37
+ const { results } = contentViewsInEnvResponse;
38
+ const contentViewsInEnvStatus = useSelector(state => selectContentViewStatus(state, '_FOR_DEFAULT_ENV'));
39
+ const hostUpdateStatus = useSelector(state => selectAPIStatus(state, HOST_CV_AND_ENV_KEY));
40
+ const pathsUrl = `/organizations/${orgId}/environments/paths?permission_type=promotable`;
41
+ useAPI( // No TableWrapper here, so we can useAPI from Foreman
42
+ 'get',
43
+ api.getApiUrl(pathsUrl),
44
+ ENV_PATH_OPTIONS,
45
+ );
46
+ const selectedContentViewId = results?.find(cv => cv.name === selectedContentView)?.id;
47
+
48
+ const handleModalClose = () => {
49
+ setCVSelectOpen(false);
50
+ setSelectedContentView(null);
51
+ setSelectedLifecycleEnv([]);
52
+ closeModal();
53
+ };
54
+
55
+ const selectedEnv = selectedLifecycleEnv?.[0];
56
+ const selectedEnvId = selectedEnv?.id;
57
+
58
+ const handleCVSelect = (event, selection) => {
59
+ setSelectedContentView(selection);
60
+ setCVSelectOpen(false);
61
+ };
62
+
63
+ const handleEnvSelect = (selection) => {
64
+ dispatch(getContentViews({
65
+ environment_id: selection[0].id,
66
+ include_default: true,
67
+ full_result: true,
68
+ order: 'default DESC', // show Default Organization View first
69
+ }, '_FOR_DEFAULT_ENV'));
70
+ setSelectedContentView(null);
71
+ setSelectedLifecycleEnv(selection);
72
+ };
73
+ const { results: contentViewsInEnv = [] } = contentViewsInEnvResponse;
74
+ const canSave = !!(selectedContentView && selectedLifecycleEnv.length);
75
+
76
+ const handleSave = () => {
77
+ const requestBody = {
78
+ content_view_id: selectedContentViewId,
79
+ environment_id: selectedEnvId,
80
+ organization_id: orgId,
81
+ included: {
82
+ search: fetchBulkParams(),
83
+ },
84
+ };
85
+ dispatch(bulkUpdateHostContentViewAndEnvironment(
86
+ requestBody, fetchBulkParams(),
87
+ handleModalClose, handleModalClose,
88
+ ));
89
+ };
90
+
91
+ const cvPlaceholderText = getCVPlaceholderText({
92
+ environments: selectedLifecycleEnv,
93
+ cvSelectOptions: contentViewsInEnv,
94
+ contentViewsStatus: contentViewsInEnvStatus,
95
+ });
96
+
97
+ const stillLoading =
98
+ (contentViewsInEnvStatus === STATUS.PENDING || hostUpdateStatus === STATUS.PENDING);
99
+ const noContentViewsAvailable =
100
+ (contentViewsInEnv.length === 0 || selectedLifecycleEnv.length === 0);
101
+
102
+ const modalActions = ([
103
+ <Button
104
+ key="add"
105
+ ouiaId="bulk-change-host-cv-modal-add-button"
106
+ variant="primary"
107
+ onClick={handleSave}
108
+ isDisabled={!canSave || hostUpdateStatus === STATUS.PENDING}
109
+ isLoading={hostUpdateStatus === STATUS.PENDING}
110
+ >
111
+ {__('Save')}
112
+ </Button>,
113
+ <Button key="cancel" ouiaId="change-host-cv-modal-cancel-button" variant="link" onClick={handleModalClose}>
114
+ Cancel
115
+ </Button>,
116
+ ]);
117
+ return (
118
+ <Modal
119
+ isOpen={isOpen}
120
+ onClose={handleModalClose}
121
+ onEscapePress={handleModalClose}
122
+ title={__('Edit content view environments')}
123
+ width="50%"
124
+ position="top"
125
+ actions={modalActions}
126
+ id="bulk-change-host-cv-modal"
127
+ key="bulk-change-host-cv-modal"
128
+ ouiaId="bulk-change-host-cv-modal"
129
+ >
130
+ <TextContent>
131
+ <Text
132
+ ouiaId="bulk-change-cv-options-description"
133
+ >
134
+ <FormattedMessage
135
+ defaultMessage={__('This will update the content view environments for {hosts}.')}
136
+ values={{
137
+ hosts: (
138
+ <strong>
139
+ <FormattedMessage
140
+ defaultMessage="{count, plural, one {# {singular}} other {# {plural}}}"
141
+ values={{
142
+ count: selectedCount,
143
+ singular: __('selected host'),
144
+ plural: __('selected hosts'),
145
+ }}
146
+ id="ccs-options-i18n"
147
+ />
148
+ </strong>
149
+ ),
150
+ }}
151
+ id="bulk-change-cv-options-description-i18n"
152
+ />
153
+ </Text>
154
+ </TextContent>
155
+ {contentViewsInEnvStatus === STATUS.RESOLVED &&
156
+ !!selectedLifecycleEnv.length && contentViewsInEnv.length === 0 &&
157
+ <Alert
158
+ ouiaId="no-cv-alert"
159
+ variant="warning"
160
+ isInline
161
+ title={__('No content views available for the selected environment')}
162
+ style={{ marginBottom: '1rem' }}
163
+ >
164
+ <a href="/content_views">{__('View the Content Views page')}</a>
165
+ {__(' to manage and promote content views, or select a different environment.')}
166
+ </Alert>
167
+ }
168
+ <EnvironmentPaths
169
+ userCheckedItems={selectedLifecycleEnv}
170
+ setUserCheckedItems={handleEnvSelect}
171
+ publishing={false}
172
+ multiSelect={false}
173
+ headerText={__('Select environment')}
174
+ isDisabled={hostUpdateStatus === STATUS.PENDING}
175
+ />
176
+ <ContentViewSelect
177
+ selections={selectedContentView}
178
+ onClear={() => setSelectedContentView(null)}
179
+ onSelect={handleCVSelect}
180
+ isOpen={cvSelectOpen}
181
+ isDisabled={stillLoading || noContentViewsAvailable}
182
+ onToggle={isExpanded => setCVSelectOpen(isExpanded)}
183
+ placeholderText={cvPlaceholderText}
184
+ >
185
+ {(contentViewsInEnv.length !== 0 && selectedLifecycleEnv.length !== 0) &&
186
+ contentViewsInEnv?.map(cv => (
187
+ <ContentViewSelectOption
188
+ key={cv.id}
189
+ value={cv.name}
190
+ cv={cv}
191
+ env={selectedLifecycleEnv[0]}
192
+ />
193
+ ))}
194
+ </ContentViewSelect>
195
+ <hr />
196
+ <TextContent>
197
+ <Text component={TextVariants.small} ouiaId="profile-upload-reminder-text">
198
+ {__('Errata and package information will be updated at the next host check-in or package action.')}
199
+ </Text>
200
+ </TextContent>
201
+ <hr />
202
+ </Modal>
203
+ );
204
+ };
205
+
206
+ BulkChangeHostCVModal.propTypes = {
207
+ isOpen: PropTypes.bool,
208
+ closeModal: PropTypes.func,
209
+ selectedCount: PropTypes.number.isRequired,
210
+ orgId: PropTypes.number.isRequired,
211
+ fetchBulkParams: PropTypes.func.isRequired,
212
+ };
213
+
214
+ BulkChangeHostCVModal.defaultProps = {
215
+ isOpen: false,
216
+ closeModal: () => {},
217
+ };
218
+
219
+
220
+ export default BulkChangeHostCVModal;
@@ -0,0 +1,23 @@
1
+ import { translate as __ } from 'foremanReact/common/I18n';
2
+ import { API_OPERATIONS, put } from 'foremanReact/redux/API';
3
+ import { errorToast, renderTaskStartedToast } from '../../../../../scenes/Tasks/helpers';
4
+ import { foremanApi } from '../../../../../services/api';
5
+ import HOST_CV_AND_ENV_KEY from '../../../HostDetails/Cards/ContentViewDetailsCard/HostContentViewConstants';
6
+
7
+ export const bulkUpdateHostContentViewAndEnvironment =
8
+ (params, bulkParams, handleSuccess, handleError) => put({
9
+ type: API_OPERATIONS.PUT,
10
+ key: HOST_CV_AND_ENV_KEY,
11
+ url: foremanApi.getApiUrl('/hosts/bulk/environment_content_view'),
12
+ ...bulkParams,
13
+ successToast: () => __('Host content view environments updating.'),
14
+ handleSuccess: (response) => {
15
+ if (handleSuccess) handleSuccess(response);
16
+ return renderTaskStartedToast(response.data);
17
+ },
18
+ handleError,
19
+ errorToast,
20
+ params,
21
+ });
22
+
23
+ export default bulkUpdateHostContentViewAndEnvironment;
@@ -0,0 +1,25 @@
1
+ import React, { useContext } from 'react';
2
+ import { useForemanOrganization } from 'foremanReact/Root/Context/ForemanContext';
3
+ import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar';
4
+ import { useForemanModal } from 'foremanReact/components/ForemanModal/ForemanModalHooks';
5
+ import BulkChangeHostCVModal from './BulkChangeHostCVModal';
6
+
7
+ const BulkChangeHostCVModalScene = () => {
8
+ const org = useForemanOrganization();
9
+ const { selectedCount, fetchBulkParams } = useContext(ForemanActionsBarContext);
10
+ const { modalOpen, setModalClosed } = useForemanModal({ id: 'bulk-change-cv-modal' });
11
+
12
+ return (
13
+ <BulkChangeHostCVModal
14
+ key="bulk-change-cv-modal"
15
+ selectedCount={selectedCount}
16
+ fetchBulkParams={fetchBulkParams}
17
+ isOpen={modalOpen}
18
+ closeModal={setModalClosed}
19
+ orgId={org?.id}
20
+ />
21
+
22
+ );
23
+ };
24
+
25
+ export default BulkChangeHostCVModalScene;
@@ -0,0 +1,133 @@
1
+ import React from 'react';
2
+ import { renderWithRedux, patientlyWaitFor, act } from 'react-testing-lib-wrapper';
3
+ import userEvent from '@testing-library/user-event';
4
+ import BulkChangeHostCVModal from '../BulkChangeHostCVModal/BulkChangeHostCVModal.js';
5
+ import mockEnvPaths from '../../../HostDetails/Cards/ContentViewDetailsCard/__tests__/envPaths.fixtures.json';
6
+ import mockContentViews from '../../../HostDetails/Cards/ContentViewDetailsCard/__tests__/contentViews.fixtures.json';
7
+ import HOST_CV_AND_ENV_KEY from '../../../HostDetails/Cards/ContentViewDetailsCard/HostContentViewConstants';
8
+ import { assertNockRequest, nockInstance } from '../../../../../test-utils/nockWrapper';
9
+ import katelloApi from '../../../../../services/api';
10
+
11
+ const contentViews = katelloApi.getApiUrl('/content_views');
12
+ const renderOptions = () => ({
13
+ apiNamespace: HOST_CV_AND_ENV_KEY,
14
+ initialState: {
15
+ API: {
16
+ HOST_DETAILS: {
17
+ response: {
18
+ id: 1,
19
+ name: 'test-host',
20
+ content_facet_attributes: {
21
+ content_view_id: 1,
22
+ lifecycle_environment_id: 1,
23
+ },
24
+ organization_id: 1,
25
+ },
26
+ status: 'RESOLVED',
27
+ },
28
+ ENVIRONMENT_PATHS: {
29
+ response: mockEnvPaths,
30
+ status: 'RESOLVED',
31
+ },
32
+ },
33
+ },
34
+ });
35
+
36
+ let firstEnvPath;
37
+ let firstCV;
38
+ let secondCV;
39
+ let firstEnv;
40
+
41
+ const cvQuery = {
42
+ organization_id: 1,
43
+ include_permissions: true,
44
+ include_default: true,
45
+ environment_id: 1,
46
+ full_result: true,
47
+ order: 'default DESC',
48
+ };
49
+
50
+ beforeEach(() => {
51
+ const { results } = mockEnvPaths;
52
+ [firstEnvPath] = results;
53
+ const { environments: envResults } = firstEnvPath;
54
+ [firstEnv] = envResults;
55
+ const { results: cvResults } = mockContentViews;
56
+ [firstCV, secondCV] = cvResults;
57
+ });
58
+
59
+ jest.mock('foremanReact/common/hooks/API/APIHooks', () => ({
60
+ useAPI: jest.fn(),
61
+ }));
62
+
63
+ test('Displays environment paths', async (done) => {
64
+ const jsx = (
65
+ <BulkChangeHostCVModal
66
+ isOpen
67
+ closeModal={jest.fn()}
68
+ selectedCount={1}
69
+ fetchBulkParams={() => 'id ^ 1'}
70
+ orgId={1}
71
+ />
72
+ );
73
+ const { getAllByText }
74
+ = renderWithRedux(jsx, renderOptions());
75
+
76
+ await patientlyWaitFor(() =>
77
+ expect(getAllByText(firstEnv.name)[0]).toBeInTheDocument());
78
+ done();
79
+ });
80
+
81
+ test('Select an env > call CV API > select a CV > Save button is enabled', async (done) => {
82
+ const contentViewsScope = nockInstance
83
+ .get(contentViews)
84
+ .query(cvQuery)
85
+ .reply(200, mockContentViews);
86
+
87
+ const jsx = (
88
+ <BulkChangeHostCVModal
89
+ isOpen
90
+ closeModal={jest.fn()}
91
+ selectedCount={1}
92
+ fetchBulkParams={() => 'id ^ 1'}
93
+ orgId={1}
94
+ />
95
+ );
96
+ const {
97
+ getAllByText, getByText,
98
+ findByPlaceholderText, getAllByRole,
99
+ } = renderWithRedux(jsx, renderOptions());
100
+
101
+ await patientlyWaitFor(() => {
102
+ const envLabel = getAllByText(firstEnv.name)[0];
103
+ expect(envLabel).toBeInTheDocument();
104
+ });
105
+
106
+ const envRadio = getAllByRole('radio', { name: firstEnv.name })[0];
107
+ expect(envRadio).toBeInTheDocument();
108
+
109
+ await act(async () => {
110
+ userEvent.click(envRadio); // Select the Library environment
111
+
112
+ const cvDropdown = await findByPlaceholderText('Select a content view');
113
+ expect(cvDropdown).toBeInTheDocument();
114
+
115
+ userEvent.click(cvDropdown); // Open the CV dropdown
116
+
117
+
118
+ [firstCV, secondCV].forEach((cv) => {
119
+ expect(getByText(cv.name)).toBeInTheDocument(); // the content view names should be showing
120
+ });
121
+
122
+
123
+ userEvent.click(getByText(secondCV.name)); // Select the second content view
124
+ });
125
+
126
+ // find the Save button and assert that it is enabled
127
+ const saveButton = getAllByRole('button', { name: 'Save' })[0];
128
+ expect(saveButton).toBeInTheDocument();
129
+ expect(saveButton).toHaveAttribute('aria-disabled', 'false');
130
+
131
+ assertNockRequest(contentViewsScope, done);
132
+ act(done);
133
+ });
@@ -2,7 +2,10 @@ import React from 'react';
2
2
  import { addGlobalFill } from 'foremanReact/components/common/Fill/GlobalFill';
3
3
  import { registerReducer } from 'foremanReact/common/MountingService';
4
4
  import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { registerColumns } from 'foremanReact/components/HostsIndex/Columns/core';
6
+ import componentRegistry from 'foremanReact/components/componentRegistry';
5
7
 
8
+ import hostsIndexColumnExtensions from './ForemanColumnExtensions/index';
6
9
  import SystemStatuses from './components/extensions/about';
7
10
  import {
8
11
  RegistrationCommands,
@@ -29,6 +32,10 @@ import HostDetailsActionsBar from './components/extensions/HostDetails/ActionsBa
29
32
  import HostsIndexActionsBar from './components/extensions/Hosts/ActionsBar';
30
33
  import RecentCommunicationCardExtensions from './components/extensions/HostDetails/DetailsTabCards/RecentCommunicationCardExtensions';
31
34
  import SystemPurposeCard from './components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeCard';
35
+ import BulkChangeHostCVModal from './components/extensions/Hosts/BulkActions/BulkChangeHostCVModal/index.js';
36
+
37
+
38
+ import ActivationKeysSearch from './components/ActivationKeysSearch';
32
39
 
33
40
  registerReducer('katelloExtends', extendReducer);
34
41
  registerReducer('katello', rootReducer);
@@ -80,3 +87,15 @@ addGlobalFill(
80
87
  );
81
88
 
82
89
  addGlobalFill('host-tab-details-cards', 'HW properties', <HwPropertiesCard key="hw-properties" />, 200);
90
+
91
+ // Hosts Index page extensions
92
+ addGlobalFill('_all-hosts-modals', 'BulkChangeHostCVModal', <BulkChangeHostCVModal key="bulk-change-host-cv-modal" />, 100);
93
+
94
+ registerColumns(hostsIndexColumnExtensions);
95
+
96
+
97
+ componentRegistry.register({
98
+ name: 'ActivationKeysSearch',
99
+ type: ActivationKeysSearch,
100
+ });
101
+
@@ -5,7 +5,6 @@ import {
5
5
  Bullseye, Split, SplitItem, Button, ActionList,
6
6
  ActionListItem, Dropdown, DropdownItem, KebabToggle,
7
7
  } from '@patternfly/react-core';
8
- import { Link } from 'react-router-dom';
9
8
  import { TableVariant, fitContent, TableText } from '@patternfly/react-table';
10
9
  import { PencilAltIcon } from '@patternfly/react-icons';
11
10
  import { STATUS } from 'foremanReact/constants';
@@ -136,7 +135,7 @@ const ContentViewComponents = ({ cvId, details }) => {
136
135
  id: componentCvId, content_view: cv, content_view_version: cvVersion,
137
136
  latest, component_content_view_versions: componentCvVersions,
138
137
  } = componentCV;
139
- const { environments, repositories } = cvVersion || {};
138
+ const { environments, repositories, id: cvVersionId } = cvVersion || {};
140
139
  const {
141
140
  id,
142
141
  name,
@@ -171,7 +170,11 @@ const ContentViewComponents = ({ cvId, details }) => {
171
170
  </Split>),
172
171
  },
173
172
  { title: environments ? <ComponentEnvironments {...{ environments }} /> : <InactiveText text={__('Not yet published')} /> },
174
- { title: <Link to={urlBuilder(`content_views/${id}#repositories`, '')}>{repositories ? repositories.length : 0}</Link> },
173
+ {
174
+ title: cvVersionId ?
175
+ <a href={urlBuilder(`content_views/${id}#/versions/${cvVersionId}/repositories`, '')}>{repositories ? repositories.length : 0}</a> :
176
+ 0,
177
+ },
175
178
  {
176
179
  title: <AddedStatusLabel added={!!componentCvId} />,
177
180
  },
@@ -18,7 +18,7 @@ export const getFormData = (hostIds, search) => (post({
18
18
  }));
19
19
 
20
20
  export const changeContentSource =
21
- (environmentId, contentViewId, contentSourceId, hostIds, handleSuccess) =>
21
+ (environmentId, contentViewId, contentSourceId, hostIds, handleSuccess, successToast) =>
22
22
  put({
23
23
  key: CHANGE_CONTENT_SOURCE,
24
24
  url: foremanUrl('/api/v2/hosts/bulk/change_content_source'),
@@ -29,6 +29,7 @@ export const changeContentSource =
29
29
  host_ids: hostIds,
30
30
  },
31
31
  errorToast: () => __('Something went wrong while updating the content source. See the logs for more information'),
32
+ successToast,
32
33
  handleSuccess,
33
34
  });
34
35
 
@@ -46,6 +47,7 @@ export const getContentViews = environmentId =>
46
47
  params: {
47
48
  environment_id: environmentId,
48
49
  full_result: true,
50
+ order: 'default DESC', // shows the default CV before all other options
49
51
  },
50
52
  errorToast: () => __('Something went wrong while loading the content views. See the logs for more information'),
51
53
  });
@@ -23,7 +23,7 @@ import { ENVIRONMENT_PATHS_KEY } from '../../../../scenes/ContentViews/component
23
23
  import EnvironmentPaths from '../../../../scenes/ContentViews/components/EnvironmentPaths/EnvironmentPaths';
24
24
  import ContentViewSelect from '../../../../scenes/ContentViews/components/ContentViewSelect/ContentViewSelect';
25
25
  import ContentViewSelectOption from '../../../../scenes/ContentViews/components/ContentViewSelect/ContentViewSelectOption';
26
- import { selectContentViewsStatus } from '../selectors';
26
+ import { selectContentViewsStatus, selectJobInvocationPath } from '../selectors';
27
27
  import { getCVPlaceholderText, shouldDisableCVSelect } from '../../../ContentViews/components/ContentViewSelect/helpers';
28
28
  import { selectEnvironmentPaths } from '../../../ContentViews/components/EnvironmentPaths/EnvironmentPathSelectors';
29
29
 
@@ -95,6 +95,9 @@ const ContentSourceForm = ({
95
95
  contentSources,
96
96
  handleContentSource,
97
97
  contentSourceId,
98
+ showCVOnlyAlert,
99
+ hostDetailsPath,
100
+ hostEditPath,
98
101
  contentHosts,
99
102
  isLoading,
100
103
  hostsUpdated,
@@ -108,13 +111,14 @@ const ContentSourceForm = ({
108
111
  );
109
112
  const contentViewsStatus = useSelector(selectContentViewsStatus);
110
113
  const environmentPathResponse = useSelector(selectEnvironmentPaths);
114
+ const jobInvocationPath = useSelector(selectJobInvocationPath);
111
115
  const envList = environmentPathResponse?.results?.map(path => path.environments).flat();
112
116
  const [csSelectOpen, setCSSelectOpen] = useState(false);
113
117
  const [cvSelectOpen, setCVSelectOpen] = useState(false);
114
118
  const hostCount = contentHosts.length;
115
119
 
116
120
  const handleCSSelect = (_event, selection) => {
117
- handleContentSource(selection);
121
+ handleContentSource(typeof selection === 'number' ? selection.toString() : selection);
118
122
  setCSSelectOpen(false);
119
123
  };
120
124
 
@@ -127,13 +131,12 @@ const ContentSourceForm = ({
127
131
  !!contentViewName &&
128
132
  !!contentSourceId &&
129
133
  hostCount !== 0);
130
-
131
134
  const contentSourcesIsDisabled = (isLoading || contentSources.length === 0 ||
132
135
  hostCount === 0);
133
- const environmentIsDisabled = (isLoading || environments === [] ||
136
+ const environmentIsDisabled = (isLoading ||
134
137
  contentSourceId === '');
135
138
  const viewIsDisabled = (isLoading || contentViews.length === 0 ||
136
- contentSourceId === '' || environments === []);
139
+ contentSourceId === '');
137
140
 
138
141
  const cvPlaceholderText = getCVPlaceholderText({
139
142
  contentSourceId,
@@ -229,6 +232,15 @@ const ContentSourceForm = ({
229
232
  env={environments[0]}
230
233
  />))}
231
234
  </ContentViewSelect>
235
+ {showCVOnlyAlert &&
236
+ <Alert
237
+ ouiaId="cv-only-alert"
238
+ variant="info"
239
+ className="margin-top-20"
240
+ title={__('Host content source will remain the same. Click Save below to update the host\'s content view environment.')}
241
+ />
242
+ }
243
+ {!showCVOnlyAlert &&
232
244
  <TextContent>
233
245
  <Text
234
246
  ouiaId="ccs-options-description"
@@ -252,27 +264,49 @@ const ContentSourceForm = ({
252
264
  />
253
265
  </Text>
254
266
  </TextContent>
267
+ }
255
268
  <ActionGroup style={{ display: 'block' }}>
256
- <Button
257
- variant="primary"
258
- id="generate_btn"
259
- ouiaId="update-source-button"
260
- onClick={e => handleSubmit(e, { shouldRedirect: true })}
261
- isDisabled={isLoading || !formIsValid() || hostsUpdated}
262
- isLoading={isLoading}
263
- >
264
- {__('Run job invocation')}
265
- </Button>
266
- <Button
267
- variant="secondary"
268
- id="generate_btn"
269
- ouiaId="update-source-button"
270
- onClick={showTemplate}
271
- isDisabled={isLoading || !formIsValid() || hostsUpdated}
272
- isLoading={isLoading}
273
- >
274
- {__('Update hosts manually')}
275
- </Button>
269
+ {!showCVOnlyAlert &&
270
+ <>
271
+ <Button
272
+ variant="primary"
273
+ id="generate_btn"
274
+ ouiaId="run-job-invocation-button"
275
+ onClick={e => handleSubmit(e, { redirectTo: jobInvocationPath })}
276
+ isDisabled={isLoading || !formIsValid() || hostsUpdated}
277
+ isLoading={isLoading}
278
+ >
279
+ {__('Run job invocation')}
280
+ </Button>
281
+ <Button
282
+ variant="secondary"
283
+ id="generate_btn"
284
+ ouiaId="update-source-button"
285
+ onClick={showTemplate}
286
+ isDisabled={isLoading || !formIsValid() || hostsUpdated}
287
+ isLoading={isLoading}
288
+ >
289
+ {__('Update hosts manually')}
290
+ </Button>
291
+ </>
292
+ }
293
+ {showCVOnlyAlert &&
294
+ <Button
295
+ variant="primary"
296
+ id="generate_btn"
297
+ ouiaId="change-cv-button"
298
+ onClick={e =>
299
+ handleSubmit(e, {
300
+ redirectTo: hostEditPath || hostDetailsPath,
301
+ showSuccessToast: true,
302
+ })
303
+ }
304
+ isDisabled={isLoading || !formIsValid() || hostsUpdated}
305
+ isLoading={isLoading}
306
+ >
307
+ {__('Save')}
308
+ </Button>
309
+ }
276
310
 
277
311
  </ActionGroup>
278
312
  </Form>);
@@ -288,6 +322,9 @@ ContentSourceForm.propTypes = {
288
322
  contentSources: PropTypes.arrayOf(PropTypes.shape({})),
289
323
  handleContentSource: PropTypes.func.isRequired,
290
324
  contentSourceId: PropTypes.string,
325
+ showCVOnlyAlert: PropTypes.bool,
326
+ hostDetailsPath: PropTypes.string.isRequired,
327
+ hostEditPath: PropTypes.string.isRequired,
291
328
  contentHosts: PropTypes.arrayOf(PropTypes.shape({})),
292
329
  isLoading: PropTypes.bool,
293
330
  hostsUpdated: PropTypes.bool,
@@ -300,6 +337,7 @@ ContentSourceForm.defaultProps = {
300
337
  contentViewName: '',
301
338
  contentSources: [],
302
339
  contentSourceId: '',
340
+ showCVOnlyAlert: false,
303
341
  contentHosts: [],
304
342
  isLoading: false,
305
343
  hostsUpdated: false,