katello 4.5.0.rc1 → 4.5.0.rc2

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 (114) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/katello/hosts/activation_key_edit.js +9 -2
  3. data/app/controllers/katello/api/registry/registry_proxies_controller.rb +3 -0
  4. data/app/controllers/katello/api/v2/alternate_content_sources_bulk_actions_controller.rb +44 -0
  5. data/app/controllers/katello/api/v2/alternate_content_sources_controller.rb +29 -6
  6. data/app/controllers/katello/api/v2/content_view_components_controller.rb +1 -1
  7. data/app/controllers/katello/api/v2/content_view_repositories_controller.rb +1 -1
  8. data/app/controllers/katello/api/v2/repositories_controller.rb +2 -8
  9. data/app/lib/actions/katello/alternate_content_source/refresh.rb +27 -0
  10. data/app/lib/actions/katello/cdn_configuration/update.rb +1 -1
  11. data/app/lib/actions/katello/content_view/publish.rb +1 -1
  12. data/app/lib/actions/katello/organization/manifest_refresh.rb +1 -1
  13. data/app/lib/actions/pulp3/alternate_content_source/delete.rb +2 -2
  14. data/app/lib/actions/pulp3/alternate_content_source/delete_remote.rb +2 -2
  15. data/app/lib/actions/pulp3/alternate_content_source/refresh.rb +23 -0
  16. data/app/lib/actions/pulp3/alternate_content_source/update.rb +2 -2
  17. data/app/lib/actions/pulp3/alternate_content_source/update_remote.rb +2 -2
  18. data/app/lib/actions/pulp3/orchestration/alternate_content_source/create.rb +0 -2
  19. data/app/lib/actions/pulp3/orchestration/alternate_content_source/refresh.rb +15 -0
  20. data/app/lib/actions/pulp3/orchestration/alternate_content_source/update.rb +0 -2
  21. data/app/lib/actions/pulp3/repository/refresh_distribution.rb +1 -4
  22. data/app/lib/actions/pulp3/repository/save_artifact.rb +1 -1
  23. data/app/lib/actions/pulp3/repository/save_distribution_references.rb +0 -2
  24. data/app/models/katello/alternate_content_source.rb +5 -0
  25. data/app/services/katello/pulp3/alternate_content_source.rb +6 -0
  26. data/app/services/katello/pulp3/content_view_version/metadata_map.rb +1 -1
  27. data/app/services/katello/pulp3/repository.rb +29 -1
  28. data/app/views/katello/api/v2/alternate_content_sources/base.json.rabl +10 -1
  29. data/app/views/katello/api/v2/content_facet/show.json.rabl +12 -0
  30. data/app/views/katello/api/v2/repository_sets/show.json.rabl +4 -0
  31. data/config/routes/api/v2.rb +16 -4
  32. data/db/migrate/20220303160220_remove_duplicate_errata.rb +1 -1
  33. data/db/migrate/20220428203334_add_last_refreshed_to_katello_alternate_content_sources.rb +5 -0
  34. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/capsule-content/capsule-content.controller.js +1 -1
  35. data/lib/katello/permission_creator.rb +4 -2
  36. data/lib/katello/tasks/refresh_alternate_content_sources.rake +10 -0
  37. data/lib/katello/version.rb +1 -1
  38. data/webpack/components/Bookmark/index.js +22 -14
  39. data/webpack/components/Search/Search.js +4 -0
  40. data/webpack/components/Table/MainTable.scss +5 -1
  41. data/webpack/components/Table/TableWrapper.js +5 -1
  42. data/webpack/components/TypeAhead/TypeAhead.js +4 -0
  43. data/webpack/components/TypeAhead/pf4Search/TypeAheadSearch.js +2 -0
  44. data/webpack/components/extensions/HostDetails/Cards/ContentViewDetailsCard/ChangeHostCVModal.js +2 -8
  45. data/webpack/components/extensions/HostDetails/Cards/ContentViewDetailsCard/ContentViewDetailsCard.js +41 -11
  46. data/webpack/components/extensions/HostDetails/Cards/HostCollectionsCard/HostCollectionsActions.js +2 -2
  47. data/webpack/components/extensions/HostDetails/Cards/HostCollectionsCard/HostCollectionsCard.js +32 -13
  48. data/webpack/components/extensions/HostDetails/Cards/HostCollectionsCard/__tests__/hostCollectionsCard.test.js +8 -0
  49. data/webpack/components/extensions/HostDetails/DetailsTabCards/RecentCommunicationCardExtensions.js +37 -0
  50. data/webpack/components/extensions/HostDetails/HostDetailsActions.js +11 -0
  51. data/webpack/components/extensions/HostDetails/Tabs/ContentTab/SecondaryTabsRoutes.js +4 -0
  52. data/webpack/components/extensions/HostDetails/Tabs/ContentTab/constants.js +2 -0
  53. data/webpack/components/extensions/HostDetails/Tabs/ContentTab/index.js +6 -1
  54. data/webpack/components/extensions/HostDetails/Tabs/ErrataTab/ErrataTab.js +120 -51
  55. data/webpack/components/extensions/HostDetails/Tabs/ModuleStreamsTab/ModuleStreamsTab.js +71 -37
  56. data/webpack/components/extensions/HostDetails/Tabs/PackagesTab/PackageInstallModal.js +4 -3
  57. data/webpack/components/extensions/HostDetails/Tabs/PackagesTab/PackagesTab.js +117 -40
  58. data/webpack/components/extensions/HostDetails/Tabs/RemoteExecutionActions.js +25 -3
  59. data/webpack/components/extensions/HostDetails/Tabs/RemoteExecutionHooks.js +85 -0
  60. data/webpack/components/extensions/HostDetails/Tabs/RepositorySetsTab/RepositorySetsTab.js +87 -33
  61. data/webpack/components/extensions/HostDetails/Tabs/TracesTab/EnableTracerModal.js +14 -7
  62. data/webpack/components/extensions/HostDetails/Tabs/TracesTab/HostTracesActions.js +2 -1
  63. data/webpack/components/extensions/HostDetails/Tabs/TracesTab/TracesEnabler.js +104 -0
  64. data/webpack/components/extensions/HostDetails/Tabs/TracesTab/TracesTab.js +92 -51
  65. data/webpack/components/extensions/HostDetails/Tabs/__tests__/errataTab.test.js +13 -23
  66. data/webpack/components/extensions/HostDetails/Tabs/{ModuleStreamsTab/__tests__/modules.fixtures.json → __tests__/moduleStreams.fixtures.json} +0 -0
  67. data/webpack/components/extensions/HostDetails/Tabs/{ModuleStreamsTab/__tests__ → __tests__}/moduleStreamsTab.test.js +13 -6
  68. data/webpack/components/extensions/HostDetails/Tabs/__tests__/packageInstallModal.test.js +21 -15
  69. data/webpack/components/extensions/HostDetails/Tabs/__tests__/packagesTab.test.js +8 -0
  70. data/webpack/components/extensions/HostDetails/Tabs/__tests__/repositorySets.fixtures.json +4 -1
  71. data/webpack/components/extensions/HostDetails/Tabs/__tests__/repositorySetsTab.test.js +26 -0
  72. data/webpack/components/extensions/HostDetails/Tabs/__tests__/tracesTab.test.js +7 -4
  73. data/webpack/components/extensions/HostDetails/hostDetailsHelpers.js +18 -0
  74. data/webpack/global_index.js +2 -2
  75. data/webpack/redux/actions/RedHatRepositories/helpers.js +5 -1
  76. data/webpack/scenes/AlternateContentSources/ACSActions.js +13 -1
  77. data/webpack/scenes/AlternateContentSources/ACSConstants.js +14 -0
  78. data/webpack/scenes/AlternateContentSources/ACSSelectors.js +10 -1
  79. data/webpack/scenes/AlternateContentSources/Create/ACSCreateContext.js +4 -0
  80. data/webpack/scenes/AlternateContentSources/Create/ACSCreateWizard.js +160 -0
  81. data/webpack/scenes/AlternateContentSources/Create/Steps/ACSCreateFinish.js +79 -0
  82. data/webpack/scenes/AlternateContentSources/Create/Steps/ACSCredentials.js +199 -0
  83. data/webpack/scenes/AlternateContentSources/Create/Steps/ACSReview.js +104 -0
  84. data/webpack/scenes/AlternateContentSources/Create/Steps/ACSSmartProxies.js +41 -0
  85. data/webpack/scenes/AlternateContentSources/Create/Steps/AcsUrlPaths.js +71 -0
  86. data/webpack/scenes/AlternateContentSources/Create/Steps/NameACS.js +57 -0
  87. data/webpack/scenes/AlternateContentSources/Create/Steps/SelectSource.js +77 -0
  88. data/webpack/scenes/AlternateContentSources/Create/__tests__/acsCreate.test.js +149 -0
  89. data/webpack/scenes/AlternateContentSources/Create/__tests__/acsCreateData.fixtures.json +3 -0
  90. data/webpack/scenes/AlternateContentSources/Create/__tests__/contentCredentials.fixtures.json +69 -0
  91. data/webpack/scenes/AlternateContentSources/Create/__tests__/smartProxy.fixtures.json +65 -0
  92. data/webpack/scenes/AlternateContentSources/MainTable/ACSTable.js +33 -23
  93. data/webpack/scenes/ContentCredentials/ContentCredentialSelectors.js +4 -1
  94. data/webpack/scenes/ContentViews/Create/CreateContentViewForm.js +2 -2
  95. data/webpack/scenes/ContentViews/Create/__tests__/createContentView.test.js +1 -1
  96. data/webpack/scenes/ContentViews/Details/ComponentContentViews/ComponentContentViewAddModal.js +1 -1
  97. data/webpack/scenes/ContentViews/Details/ComponentContentViews/ComponentContentViewBulkAddModal.js +2 -2
  98. data/webpack/scenes/ContentViews/Details/ComponentContentViews/__tests__/contentViewComponents.test.js +4 -4
  99. data/webpack/scenes/ContentViews/Details/ContentViewInfo.js +1 -1
  100. data/webpack/scenes/ContentViews/__tests__/contentViewPage.test.js +4 -4
  101. data/webpack/scenes/ContentViews/components/ContentViewIcon.js +1 -1
  102. data/webpack/scenes/ContentViews/components/ContentViewsCounter.js +1 -1
  103. data/webpack/scenes/ContentViews/components/EnvironmentPaths/EnvironmentPaths.js +1 -1
  104. data/webpack/scenes/ContentViews/expansions/DetailsExpansion.js +2 -2
  105. data/webpack/scenes/ContentViews/expansions/RelatedContentViewComponentsModal.js +2 -2
  106. data/webpack/scenes/ContentViews/expansions/__tests__/contentViewComponentsModal.test.js +1 -1
  107. data/webpack/scenes/RedHatRepositories/components/Search.js +4 -4
  108. data/webpack/scenes/SmartProxy/SmartProxyContentActions.js +9 -2
  109. data/webpack/scenes/SmartProxy/SmartProxyContentConstants.js +1 -1
  110. data/webpack/scenes/SmartProxy/SmartProxyContentSelectors.js +10 -1
  111. data/webpack/scenes/Tasks/helpers.js +30 -3
  112. metadata +34 -14
  113. data/db/seeds.d/107-enable_dynflow.rb +0 -8
  114. data/webpack/components/extensions/HostDetails/Tabs/TracesTab/EnableTracerEmptyState.js +0 -42
@@ -43,7 +43,7 @@ class RemoveDuplicateErrata < ActiveRecord::Migration[6.0]
43
43
  ::Katello::ModuleStreamErratumPackage.where(erratum_package_id: dup_err_package).each do |dup_mod_errata_package|
44
44
  if ::Katello::ModuleStreamErratumPackage.find_by(module_stream_id: dup_mod_errata_package.module_stream_id, erratum_package_id: erratum_package_to_keep&.id)
45
45
  dup_mod_errata_package.delete
46
- else
46
+ elsif erratum_package_to_keep&.id
47
47
  begin
48
48
  dup_mod_errata_package.update(erratum_package_id: erratum_package_to_keep&.id)
49
49
  rescue
@@ -0,0 +1,5 @@
1
+ class AddLastRefreshedToKatelloAlternateContentSources < ActiveRecord::Migration[6.0]
2
+ def change
3
+ add_column :katello_alternate_content_sources, :last_refreshed, :datetime
4
+ end
5
+ end
@@ -109,7 +109,7 @@ angular.module('Bastion.capsule-content').controller('CapsuleContentController',
109
109
  } else if (errorCount > 1) {
110
110
  errorMessage += " " + translate("Plus 1 more error");
111
111
  }
112
- Notification.setErrorMessage(errorMessage);
112
+ Notification.setErrorMessage(translate('Last sync failed: ') + errorMessage);
113
113
  }
114
114
  }
115
115
  $scope.syncState.set(stateFromTask(activeOrFailedTask));
@@ -404,13 +404,15 @@ module Katello
404
404
  :finder_scope => :editable
405
405
  @plugin.permission :edit_alternate_content_sources,
406
406
  {
407
- 'katello/api/v2/alternate_content_sources' => [:update]
407
+ 'katello/api/v2/alternate_content_sources' => [:update, :refresh],
408
+ 'katello/api/v2/alternate_content_sources_bulk_actions' => [:refresh_alternate_content_sources]
408
409
  },
409
410
  :resource_type => 'Katello::AlternateContentSource',
410
411
  :finder_scope => :editable
411
412
  @plugin.permission :destroy_alternate_content_sources,
412
413
  {
413
- 'katello/api/v2/alternate_content_sources' => [:destroy]
414
+ 'katello/api/v2/alternate_content_sources' => [:destroy],
415
+ 'katello/api/v2/alternate_content_sources_bulk_actions' => [:destroy_alternate_content_sources]
414
416
  },
415
417
  :resource_type => 'Katello::AlternateContentSource',
416
418
  :finder_scope => :deletable
@@ -0,0 +1,10 @@
1
+ namespace :katello do
2
+ desc 'Refresh all alternate content sources'
3
+ task :refresh_alternate_content_sources => ["dynflow:client"] do
4
+ User.current = User.anonymous_admin
5
+ ::ForemanTasks.async_task(::Actions::BulkAction,
6
+ ::Actions::Katello::AlternateContentSource::Refresh,
7
+ ::Katello::AlternateContentSource.all)
8
+ puts _("Alternate content source refreshing started in the background.")
9
+ end
10
+ end
@@ -1,3 +1,3 @@
1
1
  module Katello
2
- VERSION = "4.5.0.rc1".freeze
2
+ VERSION = "4.5.0.rc2".freeze
3
3
  end
@@ -4,14 +4,14 @@ import { useDispatch, useSelector, shallowEqual } from 'react-redux';
4
4
  import { translate as __ } from 'foremanReact/common/I18n';
5
5
  import { STATUS } from 'foremanReact/constants';
6
6
  import { Dropdown, DropdownItem, DropdownToggle, DropdownSeparator } from '@patternfly/react-core';
7
- import { CaretDownIcon } from '@patternfly/react-icons';
7
+ import { OutlinedBookmarkIcon } from '@patternfly/react-icons';
8
8
  import { getBookmarks } from './BookmarkActions';
9
9
  import { selectBookmarks, selectBookmarkStatus } from './BookmarkSelectors';
10
10
  import './Bookmark.scss';
11
11
  import AddBookmarkModal from './AddBookmarkModal';
12
12
 
13
13
  const Bookmark = ({
14
- selectItem, selectedItem, controller = '', isDisabled,
14
+ selectItem, selectedItem, controller = '', isDisabled, readOnlyBookmarks,
15
15
  }) => {
16
16
  const dispatch = useDispatch();
17
17
  const [dropdownOpen, setDropdownOpen] = useState(false);
@@ -20,6 +20,7 @@ const Bookmark = ({
20
20
  useSelector(state => selectBookmarks(state, controller), shallowEqual);
21
21
  const status =
22
22
  useSelector(state => selectBookmarkStatus(state, controller), shallowEqual);
23
+ const showActions = !readOnlyBookmarks;
23
24
 
24
25
  useEffect(() => {
25
26
  dispatch(getBookmarks(controller));
@@ -31,6 +32,7 @@ const Bookmark = ({
31
32
  }
32
33
  setDropdownOpen(false);
33
34
  };
35
+ if (!results.length && readOnlyBookmarks) return null;
34
36
 
35
37
  const dropDownItems = [
36
38
  ...results.map(({ name, id, query }) => (
@@ -41,16 +43,21 @@ const Bookmark = ({
41
43
  >
42
44
  {name}
43
45
  </DropdownItem >)),
44
- <DropdownSeparator key="separator" />,
45
- <DropdownItem
46
- onClick={() => {
47
- setDropdownOpen(false);
48
- setModalOpen(true);
49
- }}
50
- key="ADD_BOOKMARK"
51
- >
52
- {selectedItem ? __('Bookmark this search') : __('Add new bookmark')}
53
- </DropdownItem >];
46
+ ];
47
+ if (showActions) {
48
+ dropDownItems.push(
49
+ <DropdownSeparator key="separator" />,
50
+ <DropdownItem
51
+ onClick={() => {
52
+ setDropdownOpen(false);
53
+ setModalOpen(true);
54
+ }}
55
+ key="ADD_BOOKMARK"
56
+ >
57
+ {selectedItem ? __('Bookmark this search') : __('Add new bookmark')}
58
+ </DropdownItem >,
59
+ );
60
+ }
54
61
 
55
62
 
56
63
  return (
@@ -62,9 +69,8 @@ const Bookmark = ({
62
69
  isDisabled={isDisabled || status !== STATUS.RESOLVED}
63
70
  onToggle={setDropdownOpen}
64
71
  id="toggle-id"
65
- toggleIndicator={CaretDownIcon}
66
72
  >
67
- <i className="fas fa-bookmark" />
73
+ <OutlinedBookmarkIcon />
68
74
  </DropdownToggle>
69
75
  }
70
76
  isOpen={dropdownOpen}
@@ -85,10 +91,12 @@ Bookmark.propTypes = {
85
91
  controller: PropTypes.string.isRequired,
86
92
  selectItem: PropTypes.func.isRequired,
87
93
  selectedItem: PropTypes.string.isRequired,
94
+ readOnlyBookmarks: PropTypes.bool,
88
95
  };
89
96
 
90
97
  Bookmark.defaultProps = {
91
98
  isDisabled: undefined,
99
+ readOnlyBookmarks: false,
92
100
  };
93
101
 
94
102
  export default Bookmark;
@@ -22,6 +22,7 @@ const Search = ({
22
22
  getAutoCompleteParams,
23
23
  foremanApiAutoComplete,
24
24
  bookmarkController,
25
+ readOnlyBookmarks,
25
26
  placeholder,
26
27
  isTextInput,
27
28
  setTextInputValue,
@@ -84,6 +85,7 @@ const Search = ({
84
85
  <TypeAhead
85
86
  autoSearchDelay={autoSearchDelay}
86
87
  bookmarkController={bookmarkController}
88
+ readOnlyBookmarks={readOnlyBookmarks}
87
89
  isDisabled={isDisabled}
88
90
  items={items}
89
91
  onInputUpdate={onInputUpdate}
@@ -112,6 +114,7 @@ Search.propTypes = {
112
114
  autoSearchDelay: PropTypes.number,
113
115
  }),
114
116
  bookmarkController: PropTypes.string,
117
+ readOnlyBookmarks: PropTypes.bool,
115
118
  placeholder: PropTypes.string,
116
119
  isTextInput: PropTypes.bool,
117
120
  setTextInputValue: PropTypes.func,
@@ -127,6 +130,7 @@ Search.defaultProps = {
127
130
  },
128
131
  isDisabled: undefined,
129
132
  bookmarkController: undefined,
133
+ readOnlyBookmarks: false,
130
134
  placeholder: undefined,
131
135
  isTextInput: false,
132
136
  setTextInputValue: undefined,
@@ -14,4 +14,8 @@
14
14
 
15
15
  .pf-c-table tbody tr td {
16
16
  vertical-align: inherit;
17
- }
17
+ }
18
+
19
+ input[type="checkbox"][disabled] {
20
+ cursor: initial;
21
+ }
@@ -43,6 +43,7 @@ const TableWrapper = ({
43
43
  disableSearch,
44
44
  nodesBelowSearch,
45
45
  bookmarkController,
46
+ readOnlyBookmarks,
46
47
  ...allTableProps
47
48
  }) => {
48
49
  const dispatch = useDispatch();
@@ -191,6 +192,7 @@ const TableWrapper = ({
191
192
  getAutoCompleteParams={getAutoCompleteParams}
192
193
  foremanApiAutoComplete={foremanApiAutoComplete}
193
194
  bookmarkController={bookmarkController}
195
+ readOnlyBookmarks={readOnlyBookmarks}
194
196
  placeholder={searchPlaceholderText}
195
197
  />
196
198
  </FlexItem>
@@ -272,7 +274,7 @@ TableWrapper.propTypes = {
272
274
  actionButtons: PropTypes.node,
273
275
  toggleGroup: PropTypes.node,
274
276
  children: PropTypes.node,
275
- // additionalListeners are anything that can trigger another API call, e.g. a filter
277
+ // additionalListeners are anything that should trigger another API call, e.g. a filter
276
278
  additionalListeners: PropTypes.arrayOf(PropTypes.oneOfType([
277
279
  PropTypes.number,
278
280
  PropTypes.string,
@@ -300,6 +302,7 @@ TableWrapper.propTypes = {
300
302
  disableSearch: PropTypes.bool,
301
303
  nodesBelowSearch: PropTypes.node,
302
304
  bookmarkController: PropTypes.string,
305
+ readOnlyBookmarks: PropTypes.bool,
303
306
  };
304
307
 
305
308
  TableWrapper.defaultProps = {
@@ -326,6 +329,7 @@ TableWrapper.defaultProps = {
326
329
  disableSearch: false,
327
330
  nodesBelowSearch: null,
328
331
  bookmarkController: undefined,
332
+ readOnlyBookmarks: false,
329
333
  };
330
334
 
331
335
  export default TableWrapper;
@@ -22,6 +22,7 @@ const TypeAhead = ({
22
22
  autoSearchEnabled,
23
23
  autoSearchDelay,
24
24
  bookmarkController,
25
+ readOnlyBookmarks,
25
26
  placeholder,
26
27
  isTextInput,
27
28
  setTextInputValue,
@@ -69,6 +70,7 @@ const TypeAhead = ({
69
70
  }) => {
70
71
  const typeAheadProps = {
71
72
  bookmarkController,
73
+ readOnlyBookmarks,
72
74
  isDisabled,
73
75
  userInputValue: inputValue,
74
76
  clearSearch,
@@ -118,6 +120,7 @@ TypeAhead.propTypes = {
118
120
  autoSearchEnabled: PropTypes.bool.isRequired,
119
121
  autoSearchDelay: PropTypes.number,
120
122
  bookmarkController: PropTypes.string,
123
+ readOnlyBookmarks: PropTypes.bool,
121
124
  placeholder: PropTypes.string,
122
125
  isTextInput: PropTypes.bool,
123
126
  setTextInputValue: PropTypes.func,
@@ -130,6 +133,7 @@ TypeAhead.defaultProps = {
130
133
  isDisabled: undefined,
131
134
  autoSearchDelay: 500,
132
135
  bookmarkController: undefined,
136
+ readOnlyBookmarks: false,
133
137
  placeholder: undefined,
134
138
  isTextInput: false,
135
139
  setTextInputValue: undefined,
@@ -13,6 +13,7 @@ const TypeAheadSearch = ({
13
13
  userInputValue, clearSearch, getInputProps, getItemProps, isOpen, highlightedIndex,
14
14
  selectedItem, selectItem, openMenu, onSearch, items, activeItems, shouldShowItems,
15
15
  autoSearchEnabled, isDisabled, bookmarkController, inputValue, placeholder, isTextInput,
16
+ readOnlyBookmarks,
16
17
  }) => (
17
18
  <>
18
19
  <InputGroup>
@@ -44,6 +45,7 @@ const TypeAheadSearch = ({
44
45
  isDisabled,
45
46
  selectedItem,
46
47
  selectItem,
48
+ readOnlyBookmarks,
47
49
  }}
48
50
  controller={bookmarkController}
49
51
  />}
@@ -9,7 +9,6 @@ import {
9
9
  import { translate as __ } from 'foremanReact/common/I18n';
10
10
  import { STATUS } from 'foremanReact/constants';
11
11
  import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
12
- import { HOST_DETAILS_KEY } from 'foremanReact/components/HostDetails/consts';
13
12
  import { selectAPIStatus } from 'foremanReact/redux/API/APISelectors';
14
13
  import EnvironmentPaths from '../../../../../scenes/ContentViews/components/EnvironmentPaths/EnvironmentPaths';
15
14
  import { ENVIRONMENT_PATHS_KEY } from '../../../../../scenes/ContentViews/components/EnvironmentPaths/EnvironmentPathConstants';
@@ -20,6 +19,7 @@ import { uniq } from '../../../../../utils/helpers';
20
19
  import ContentViewIcon from '../../../../../scenes/ContentViews/components/ContentViewIcon';
21
20
  import updateHostContentViewAndEnvironment from './HostContentViewActions';
22
21
  import HOST_CV_AND_ENV_KEY from './HostContentViewConstants';
22
+ import { getHostDetails } from '../../HostDetailsActions';
23
23
 
24
24
  const ENV_PATH_OPTIONS = { key: ENVIRONMENT_PATHS_KEY };
25
25
 
@@ -110,13 +110,7 @@ const ChangeHostCVModal = ({
110
110
 
111
111
  const refreshHostDetails = () => {
112
112
  handleModalClose();
113
- return dispatch({
114
- type: 'API_GET',
115
- payload: {
116
- key: HOST_DETAILS_KEY,
117
- url: `/api/hosts/${hostName}`,
118
- },
119
- });
113
+ return dispatch(getHostDetails({ hostname: hostName }));
120
114
  };
121
115
 
122
116
  const handleSave = () => {
@@ -20,12 +20,18 @@ import { translate as __ } from 'foremanReact/common/I18n';
20
20
  import { propsToCamelCase } from 'foremanReact/common/helpers';
21
21
  import PropTypes from 'prop-types';
22
22
  import ContentViewIcon from '../../../../../scenes/ContentViews/components/ContentViewIcon';
23
- import { hostIsRegistered } from '../../hostDetailsHelpers';
23
+ import { hasRequiredPermissions, hostIsRegistered } from '../../hostDetailsHelpers';
24
24
  import ChangeHostCVModal from './ChangeHostCVModal';
25
25
 
26
+ const requiredPermissions = [
27
+ 'view_lifecycle_environments', 'view_content_views',
28
+ 'promote_or_remove_content_views_to_environments',
29
+ ];
30
+
26
31
  const HostContentViewDetails = ({
27
32
  contentView, lifecycleEnvironment, contentViewVersionId, contentViewDefault,
28
33
  contentViewVersion, contentViewVersionLatest, hostId, hostName, orgId, hostEnvId,
34
+ hostPermissions, permissions,
29
35
  }) => {
30
36
  let versionLabel = `Version ${contentViewVersion}`;
31
37
  if (contentViewVersionLatest) {
@@ -41,6 +47,9 @@ const HostContentViewDetails = ({
41
47
  setIsModalOpen(true);
42
48
  };
43
49
 
50
+ const userPermissions = { ...hostPermissions, ...permissions };
51
+ const showKebab = hasRequiredPermissions(requiredPermissions, userPermissions);
52
+
44
53
  const dropdownItems = [
45
54
  <DropdownItem
46
55
  aria-label="change-host-content-view"
@@ -71,16 +80,18 @@ const HostContentViewDetails = ({
71
80
  </FlexItem>
72
81
  </Flex>
73
82
  </FlexItem>
74
- <FlexItem>
75
- <Dropdown
76
- toggle={<KebabToggle aria-label="change_content_view_hamburger" onToggle={toggleHamburger} />}
77
- isOpen={isDropdownOpen}
78
- isPlain
79
- ouiaId="change-host-content-view-kebab"
80
- position="right"
81
- dropdownItems={dropdownItems}
82
- />
83
- </FlexItem>
83
+ {showKebab && (
84
+ <FlexItem>
85
+ <Dropdown
86
+ toggle={<KebabToggle aria-label="change_content_view_hamburger" onToggle={toggleHamburger} />}
87
+ isOpen={isDropdownOpen}
88
+ isPlain
89
+ ouiaId="change-host-content-view-kebab"
90
+ position="right"
91
+ dropdownItems={dropdownItems}
92
+ />
93
+ </FlexItem>
94
+ )}
84
95
  </Flex>
85
96
  </CardHeader>
86
97
  <CardBody>
@@ -146,6 +157,7 @@ const ContentViewDetailsCard = ({ hostDetails }) => {
146
157
  hostName={hostDetails.name}
147
158
  orgId={hostDetails.organization_id}
148
159
  hostEnvId={hostDetails.content_facet_attributes.lifecycle_environment_id}
160
+ hostPermissions={hostDetails.permissions}
149
161
  {...propsToCamelCase(hostDetails.content_facet_attributes)}
150
162
  />);
151
163
  }
@@ -172,6 +184,14 @@ HostContentViewDetails.propTypes = {
172
184
  hostName: PropTypes.string,
173
185
  orgId: PropTypes.number,
174
186
  hostEnvId: PropTypes.number,
187
+ hostPermissions: PropTypes.shape({
188
+ edit_hosts: PropTypes.bool,
189
+ }),
190
+ permissions: PropTypes.shape({
191
+ view_content_views: PropTypes.bool,
192
+ view_lifecycle_environments: PropTypes.bool,
193
+ promote_or_remove_content_views_to_environments: PropTypes.bool,
194
+ }),
175
195
  };
176
196
 
177
197
  HostContentViewDetails.defaultProps = {
@@ -182,6 +202,8 @@ HostContentViewDetails.defaultProps = {
182
202
  hostName: '',
183
203
  orgId: null,
184
204
  contentViewDefault: false,
205
+ hostPermissions: {},
206
+ permissions: {},
185
207
  };
186
208
 
187
209
  ContentViewDetailsCard.propTypes = {
@@ -191,6 +213,14 @@ ContentViewDetailsCard.propTypes = {
191
213
  organization_id: PropTypes.number,
192
214
  content_facet_attributes: PropTypes.shape({
193
215
  lifecycle_environment_id: PropTypes.number,
216
+ permissions: PropTypes.shape({
217
+ view_content_views: PropTypes.bool,
218
+ view_lifecycle_environments: PropTypes.bool,
219
+ promote_or_remove_content_views_to_environments: PropTypes.bool,
220
+ }),
221
+ }),
222
+ permissions: PropTypes.shape({
223
+ edit_hosts: PropTypes.bool,
194
224
  }),
195
225
  }),
196
226
  };
@@ -24,7 +24,7 @@ export const alterHostCollections = (hostId, params, refreshHostDetails) => put(
24
24
  url: foremanApi.getApiUrl(`/hosts/${hostId}/host_collections`),
25
25
  params,
26
26
  successToast: () => __('Host collections updated'),
27
- errorToast: error => errorToast(error),
28
- handleSuccess: () => refreshHostDetails(),
27
+ errorToast,
28
+ handleSuccess: refreshHostDetails,
29
29
  });
30
30
 
@@ -18,10 +18,13 @@ import { propsToCamelCase } from 'foremanReact/common/helpers';
18
18
  import PropTypes from 'prop-types';
19
19
  import { useSet } from '../../../../Table/TableHooks';
20
20
  import { HostCollectionsAddModal, HostCollectionsRemoveModal } from './HostCollectionsModal';
21
- import { hostIsRegistered } from '../../hostDetailsHelpers';
21
+ import { hasRequiredPermissions, hostIsRegistered, userPermissionsFromHostDetails } from '../../hostDetailsHelpers';
22
+
23
+ const requiredPermissions = ['edit_hosts', 'view_host_collections'];
22
24
 
23
25
  const HostCollectionsDetails = ({
24
26
  hostCollections, id: hostId, name: hostName,
27
+ showKebab,
25
28
  }) => {
26
29
  const [isDropdownOpen, setIsDropdownOpen] = useState(false);
27
30
  const toggleBulkAction = () => setIsDropdownOpen(prev => !prev);
@@ -85,15 +88,17 @@ const HostCollectionsDetails = ({
85
88
  </FlexItem>
86
89
  </Flex>
87
90
  </FlexItem>
88
- <FlexItem>
89
- <Dropdown
90
- toggle={<KebabToggle aria-label="host_collections_bulk_actions" onToggle={toggleBulkAction} />}
91
- isOpen={isDropdownOpen}
92
- isPlain
93
- position="right"
94
- dropdownItems={dropdownItems}
95
- />
96
- </FlexItem>
91
+ {showKebab && (
92
+ <FlexItem>
93
+ <Dropdown
94
+ toggle={<KebabToggle aria-label="host_collections_bulk_actions" onToggle={toggleBulkAction} />}
95
+ isOpen={isDropdownOpen}
96
+ isPlain
97
+ position="right"
98
+ dropdownItems={dropdownItems}
99
+ />
100
+ </FlexItem>)
101
+ }
97
102
  </Flex>
98
103
  </CardHeader>
99
104
  <CardBody>
@@ -159,7 +164,9 @@ const HostCollectionsDetails = ({
159
164
 
160
165
  const HostCollectionsCard = ({ hostDetails }) => {
161
166
  if (hostIsRegistered({ hostDetails })) {
162
- return <HostCollectionsDetails {...propsToCamelCase(hostDetails)} />;
167
+ const showKebab =
168
+ hasRequiredPermissions(requiredPermissions, userPermissionsFromHostDetails({ hostDetails }));
169
+ return <HostCollectionsDetails showKebab={showKebab} {...propsToCamelCase(hostDetails)} />;
163
170
  }
164
171
  return null;
165
172
  };
@@ -168,20 +175,32 @@ HostCollectionsDetails.propTypes = {
168
175
  hostCollections: PropTypes.arrayOf(PropTypes.shape({})),
169
176
  id: PropTypes.number,
170
177
  name: PropTypes.string,
178
+ showKebab: PropTypes.bool,
171
179
  };
172
180
 
173
181
  HostCollectionsDetails.defaultProps = {
174
182
  hostCollections: [],
175
183
  id: null,
176
184
  name: '',
185
+ showKebab: false,
177
186
  };
178
187
 
179
188
  HostCollectionsCard.propTypes = {
180
- hostDetails: PropTypes.shape({}),
189
+ hostDetails: PropTypes.shape({
190
+ permissions: PropTypes.shape({}),
191
+ contentFacetAttributes: PropTypes.shape({
192
+ permissions: PropTypes.shape({}),
193
+ }),
194
+ }),
181
195
  };
182
196
 
183
197
  HostCollectionsCard.defaultProps = {
184
- hostDetails: null,
198
+ hostDetails: {
199
+ permissions: {},
200
+ contentFacetAttributes: {
201
+ permissions: {},
202
+ },
203
+ },
185
204
  };
186
205
 
187
206
  export default HostCollectionsCard;
@@ -41,6 +41,14 @@ const emptyHostDetails = {
41
41
  },
42
42
  };
43
43
 
44
+ jest.mock('../../../hostDetailsHelpers', () => ({
45
+ ...jest.requireActual('../../../hostDetailsHelpers'),
46
+ userPermissionsFromHostDetails: () => ({
47
+ view_host_collections: true,
48
+ edit_hosts: true,
49
+ }),
50
+ }));
51
+
44
52
  test('shows host collections and host limits when present', () => {
45
53
  const { getByText } = render(<HostCollectionsCard hostDetails={hostDetails} />);
46
54
  expect(getByText('Host collections')).toBeInTheDocument();
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import RelativeDateTime from 'foremanReact/components/common/dates/RelativeDateTime';
4
+ import {
5
+ DescriptionListGroup,
6
+ DescriptionListTerm,
7
+ DescriptionListDescription,
8
+ } from '@patternfly/react-core';
9
+ import { translate as __ } from 'foremanReact/common/I18n';
10
+
11
+ const RecentCommunicationCardExtensions = ({ hostDetails }) => {
12
+ const { subscription_facet_attributes: subscriptionFacetAttributes } = hostDetails;
13
+ if (!Object.keys(subscriptionFacetAttributes ?? {}).includes('last_checkin')) return null;
14
+ const lastCheckin = subscriptionFacetAttributes?.last_checkin;
15
+ return (
16
+ <DescriptionListGroup>
17
+ <DescriptionListTerm>{__('Last check-in:')}</DescriptionListTerm>
18
+ <DescriptionListDescription>
19
+ <RelativeDateTime date={lastCheckin} defaultValue={__('Never')} />
20
+ </DescriptionListDescription>
21
+ </DescriptionListGroup>
22
+ );
23
+ };
24
+
25
+ RecentCommunicationCardExtensions.propTypes = {
26
+ hostDetails: PropTypes.shape({
27
+ subscription_facet_attributes: PropTypes.shape({
28
+ last_checkin: PropTypes.string,
29
+ }),
30
+ }),
31
+ };
32
+
33
+ RecentCommunicationCardExtensions.defaultProps = {
34
+ hostDetails: {},
35
+ };
36
+
37
+ export default RecentCommunicationCardExtensions;
@@ -1,2 +1,13 @@
1
+ import HOST_DETAILS_KEY from './HostDetailsConstants';
2
+
1
3
  const hostIdNotReady = { type: 'NOOP_HOST_ID_NOT_READY' };
4
+
5
+ export const getHostDetails = ({ hostname }) => ({
6
+ type: 'API_GET',
7
+ payload: {
8
+ key: HOST_DETAILS_KEY,
9
+ url: `/api/hosts/${hostname}`,
10
+ },
11
+ });
12
+
2
13
  export default hostIdNotReady;
@@ -3,6 +3,7 @@ import { Route, Switch, Redirect } from 'react-router-dom';
3
3
  import { PackagesTab } from '../PackagesTab/PackagesTab.js';
4
4
  import { ErrataTab } from '../ErrataTab/ErrataTab.js';
5
5
  import { ModuleStreamsTab } from '../ModuleStreamsTab/ModuleStreamsTab';
6
+ import RepositorySetsTab from '../RepositorySetsTab/RepositorySetsTab';
6
7
  import { route } from './helpers';
7
8
 
8
9
  const SecondaryTabRoutes = () => (
@@ -16,6 +17,9 @@ const SecondaryTabRoutes = () => (
16
17
  <Route path={route('module-streams')}>
17
18
  <ModuleStreamsTab />
18
19
  </Route>
20
+ <Route path={route('Repository sets')}>
21
+ <RepositorySetsTab />
22
+ </Route>
19
23
  <Redirect to={route('errata')} />
20
24
  </Switch>
21
25
  );
@@ -1,9 +1,11 @@
1
1
  import { translate as __ } from 'foremanReact/common/I18n';
2
+ import { hideRepoSetsTab } from '../RepositorySetsTab/RepositorySetsTab';
2
3
 
3
4
  const SECONDARY_TABS = [
4
5
  { key: 'packages', title: __('Packages') },
5
6
  { key: 'errata', title: __('Errata') },
6
7
  { key: 'module-streams', title: __('Module streams') },
8
+ { key: 'Repository sets', hideTab: hideRepoSetsTab, title: __('Repository sets') },
7
9
  ];
8
10
 
9
11
  export default SECONDARY_TABS;
@@ -2,12 +2,17 @@ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { useHistory } from 'react-router-dom';
4
4
  import { Tabs, Tab, TabTitleText } from '@patternfly/react-core';
5
+ import { useSelector } from 'react-redux';
6
+ import { selectAPIResponse } from 'foremanReact/redux/API/APISelectors';
5
7
  import SecondaryTabRoutes from './SecondaryTabsRoutes';
6
8
  import { activeTab } from './helpers';
7
9
  import SECONDARY_TABS from './constants';
8
10
 
9
11
  const ContentTab = ({ location: { pathname } }) => {
10
12
  const hashHistory = useHistory();
13
+ const hostDetails = useSelector(state => selectAPIResponse(state, 'HOST_DETAILS'));
14
+ const filteredTabs =
15
+ SECONDARY_TABS?.filter(tab => !tab.hideTab?.({ hostDetails })) ?? [];
11
16
  return (
12
17
  <>
13
18
  <Tabs
@@ -16,7 +21,7 @@ const ContentTab = ({ location: { pathname } }) => {
16
21
  isSecondary
17
22
  activeKey={activeTab(pathname)}
18
23
  >
19
- {SECONDARY_TABS.map(({ key, title }) => (
24
+ {filteredTabs.map(({ key, title }) => (
20
25
  <Tab
21
26
  key={key}
22
27
  eventKey={key}