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
@@ -12,13 +12,12 @@ import {
12
12
  } from '@patternfly/react-core';
13
13
  import { CaretDownIcon } from '@patternfly/react-icons';
14
14
  import { translate as __ } from 'foremanReact/common/I18n';
15
- import { useSelector, useDispatch } from 'react-redux';
15
+ import { useSelector } from 'react-redux';
16
16
  import { selectAPIResponse } from 'foremanReact/redux/API/APISelectors';
17
- import { installTracerPackage } from './HostTracesActions';
18
17
  import { katelloPackageInstallUrl } from '../customizedRexUrlHelpers';
19
18
  import { KATELLO_TRACER_PACKAGE } from './HostTracesConstants';
20
19
 
21
- const EnableTracerModal = ({ isOpen, setIsOpen }) => {
20
+ const EnableTracerModal = ({ isOpen, setIsOpen, triggerJobStart }) => {
22
21
  const title = __('Enable Tracer');
23
22
  const body = __('Enabling will install the katello-host-tools-tracer package on the host.');
24
23
  const [isDropdownOpen, setIsDropdownOpen] = useState(false);
@@ -29,16 +28,19 @@ const EnableTracerModal = ({ isOpen, setIsOpen }) => {
29
28
  __('via customized remote execution'),
30
29
  ];
31
30
  const [selectedOption, setSelectedOption] = useState(dropdownOptions[0]);
32
- const dispatch = useDispatch();
33
31
  const hostDetails = useSelector(state => selectAPIResponse(state, 'HOST_DETAILS'));
34
32
  const { name: hostname } = hostDetails;
35
33
  const handleSelect = () => {
36
34
  setIsDropdownOpen(false);
37
35
  };
38
- const enableTracer = () => {
39
- dispatch(installTracerPackage({ hostname }));
36
+ const handleClose = () => {
40
37
  setIsOpen(false);
41
38
  };
39
+ const enableTracer = () => {
40
+ setButtonLoading(true);
41
+ triggerJobStart();
42
+ handleClose();
43
+ };
42
44
 
43
45
  const dropdownItems = dropdownOptions.map(text => (
44
46
  <DropdownItem key={`option_${text}`} onClick={() => setSelectedOption(text)}>{text}</DropdownItem>
@@ -54,6 +56,8 @@ const EnableTracerModal = ({ isOpen, setIsOpen }) => {
54
56
  key="enable_button"
55
57
  type="submit"
56
58
  variant="primary"
59
+ isLoading={buttonLoading}
60
+ isDisabled={buttonLoading}
57
61
  onClick={enableTracer}
58
62
  >
59
63
  {title}
@@ -65,6 +69,7 @@ const EnableTracerModal = ({ isOpen, setIsOpen }) => {
65
69
  key="enable_button"
66
70
  component="a"
67
71
  isLoading={buttonLoading}
72
+ isDisabled={buttonLoading}
68
73
  onClick={() => setButtonLoading(true)}
69
74
  variant="primary"
70
75
  href={customizedRexUrl}
@@ -80,7 +85,7 @@ const EnableTracerModal = ({ isOpen, setIsOpen }) => {
80
85
  title={title}
81
86
  width="28em"
82
87
  isOpen={isOpen}
83
- onClose={() => setIsOpen(false)}
88
+ onClose={handleClose}
84
89
  actions={[
85
90
  getEnableTracerButton(),
86
91
  <Button key="cancel_button" variant="link" onClick={() => setIsOpen(false)}>{__('Cancel')}</Button>,
@@ -96,6 +101,7 @@ const EnableTracerModal = ({ isOpen, setIsOpen }) => {
96
101
  id="toggle-enable-tracer-modal-dropdown"
97
102
  onToggle={toggleDropdownOpen}
98
103
  toggleIndicator={CaretDownIcon}
104
+ isDisabled={buttonLoading}
99
105
  >
100
106
  {selectedOption}
101
107
  </DropdownToggle>
@@ -114,6 +120,7 @@ const EnableTracerModal = ({ isOpen, setIsOpen }) => {
114
120
  EnableTracerModal.propTypes = {
115
121
  isOpen: PropTypes.bool.isRequired,
116
122
  setIsOpen: PropTypes.func.isRequired,
123
+ triggerJobStart: PropTypes.func.isRequired,
117
124
  };
118
125
 
119
126
  export default EnableTracerModal;
@@ -13,7 +13,8 @@ export const getHostTraces = (hostId, params) => get({
13
13
  params,
14
14
  });
15
15
 
16
- export const installTracerPackage = ({ hostname }) => installPackage({
16
+ export const installTracerPackage = ({ hostname, handleSuccess }) => installPackage({
17
17
  hostname,
18
18
  packageName: KATELLO_TRACER_PACKAGE,
19
+ handleSuccess,
19
20
  });
@@ -0,0 +1,104 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ EmptyState,
5
+ EmptyStateIcon,
6
+ EmptyStateBody,
7
+ Title,
8
+ EmptyStateVariant,
9
+ Button,
10
+ Flex,
11
+ FlexItem,
12
+ Spinner,
13
+ } from '@patternfly/react-core';
14
+ import { WrenchIcon } from '@patternfly/react-icons';
15
+ import { translate as __ } from 'foremanReact/common/I18n';
16
+ import { urlBuilder } from 'foremanReact/common/urlHelpers';
17
+ import EnableTracerModal from './EnableTracerModal';
18
+ import { useRexJobPolling } from '../RemoteExecutionHooks';
19
+ import { installTracerPackage } from './HostTracesActions';
20
+ import { getHostDetails } from '../../HostDetailsActions';
21
+
22
+ const EnableTracerButton = ({ setEnableTracerModalOpen, pollingStarted }) => (
23
+ <Button
24
+ onClick={() => setEnableTracerModalOpen(true)}
25
+ isDisabled={pollingStarted}
26
+ >
27
+ {__('Enable Traces')}
28
+ </Button>
29
+ );
30
+
31
+ const ViewTaskButton = ({ jobId }) => (
32
+ <Button
33
+ component="a"
34
+ href={urlBuilder('job_invocations', '', jobId)}
35
+ variant="secondary"
36
+ isDisabled={!jobId}
37
+ >
38
+ {__('View the job')}
39
+ </Button>
40
+ );
41
+
42
+ ViewTaskButton.propTypes = {
43
+ jobId: PropTypes.number,
44
+ };
45
+ ViewTaskButton.defaultProps = {
46
+ jobId: null,
47
+ };
48
+
49
+ EnableTracerButton.propTypes = {
50
+ setEnableTracerModalOpen: PropTypes.func.isRequired,
51
+ pollingStarted: PropTypes.bool.isRequired,
52
+ };
53
+
54
+ const TracesEnabler = ({ hostname }) => {
55
+ const title = __('Traces are not enabled');
56
+ const enablingTitle = __('Traces are being enabled');
57
+ const body = __('Traces help administrators identify applications that need to be restarted after a system is patched.');
58
+ const [enableTracerModalOpen, setEnableTracerModalOpen] = useState(false);
59
+ const initialAction = installTracerPackage({ hostname });
60
+ const successAction = getHostDetails({ hostname });
61
+ const {
62
+ pollingStarted,
63
+ rexJobId,
64
+ triggerJobStart,
65
+ } = useRexJobPolling(initialAction, successAction);
66
+
67
+ return (
68
+ <EmptyState variant={EmptyStateVariant.small}>
69
+ {pollingStarted ?
70
+ <Spinner /> :
71
+ <EmptyStateIcon icon={WrenchIcon} />
72
+ }
73
+ <Title headingLevel="h2" size="lg">
74
+ {pollingStarted ? enablingTitle : title}
75
+ </Title>
76
+ <EmptyStateBody>
77
+ <Flex direction={{ default: 'column' }}>
78
+ <FlexItem>{body}</FlexItem>
79
+ <FlexItem>
80
+ {pollingStarted ?
81
+ <ViewTaskButton jobId={rexJobId} /> :
82
+ <EnableTracerButton {...{
83
+ setEnableTracerModalOpen,
84
+ pollingStarted,
85
+ }}
86
+ />
87
+ }
88
+ </FlexItem>
89
+ </Flex>
90
+ </EmptyStateBody>
91
+ <EnableTracerModal
92
+ isOpen={enableTracerModalOpen}
93
+ setIsOpen={setEnableTracerModalOpen}
94
+ triggerJobStart={triggerJobStart}
95
+ />
96
+ </EmptyState>
97
+ );
98
+ };
99
+
100
+ TracesEnabler.propTypes = {
101
+ hostname: PropTypes.string.isRequired,
102
+ };
103
+
104
+ export default TracesEnabler;
@@ -1,14 +1,14 @@
1
1
  import React, { useState, useCallback } from 'react';
2
2
  import { FormattedMessage } from 'react-intl';
3
3
  import {
4
- Skeleton, Button, Split, SplitItem, ActionList, ActionListItem, Dropdown,
5
- DropdownItem, KebabToggle,
4
+ Skeleton, Split, SplitItem, ActionList, ActionListItem, Dropdown,
5
+ DropdownItem, DropdownToggle, DropdownToggleAction,
6
6
  } from '@patternfly/react-core';
7
7
  import { translate as __ } from 'foremanReact/common/I18n';
8
8
  import { TableVariant, Thead, Tbody, Tr, Th, Td } from '@patternfly/react-table';
9
- import { useSelector, useDispatch } from 'react-redux';
9
+ import { useSelector } from 'react-redux';
10
10
  import { selectAPIResponse } from 'foremanReact/redux/API/APISelectors';
11
- import EnableTracerEmptyState from './EnableTracerEmptyState';
11
+ import TracesEnabler from './TracesEnabler';
12
12
  import TableWrapper from '../../../../Table/TableWrapper';
13
13
  import { useBulkSelect, useTableSort, useUrlParams } from '../../../../Table/TableHooks';
14
14
  import { getHostTraces } from './HostTracesActions';
@@ -18,24 +18,33 @@ import { resolveTraceUrl } from '../customizedRexUrlHelpers';
18
18
  import './TracesTab.scss';
19
19
  import hostIdNotReady from '../../HostDetailsActions';
20
20
  import SortableColumnHeaders from '../../../../Table/components/SortableColumnHeaders';
21
+ import { useRexJobPolling } from '../RemoteExecutionHooks';
22
+ import { hasRequiredPermissions as can,
23
+ missingRequiredPermissions as cannot,
24
+ userPermissionsFromHostDetails } from '../../hostDetailsHelpers';
25
+
26
+ const invokeRexJobs = ['create_job_invocations'];
27
+ const createBookmarks = ['create_bookmarks'];
21
28
 
22
29
  const TracesTab = () => {
23
30
  const hostDetails = useSelector(state => selectAPIResponse(state, 'HOST_DETAILS'));
24
- const dispatch = useDispatch();
25
31
  const {
26
32
  id: hostId,
27
33
  name: hostname,
28
34
  content_facet_attributes: contentFacetAttributes,
29
35
  } = hostDetails;
36
+ const showActions = can(invokeRexJobs, userPermissionsFromHostDetails({ hostDetails }));
30
37
  const showEnableTracer = (contentFacetAttributes?.katello_tracer_installed === false);
31
- const emptyContentTitle = __('No applications to restart');
32
- const emptyContentBody = (<FormattedMessage
38
+ const emptyContentTitle = showActions ? __('No applications to restart') : __('Traces not available');
39
+ const tracesNotAvailBody = showEnableTracer ? __('Traces may be enabled by a user with the appropriate permissions.') :
40
+ __('Traces will be shown here to a user with the appropriate permissions.');
41
+ const emptyContentBody = showActions ? (<FormattedMessage
33
42
  id="traces-happy-empty"
34
43
  values={{
35
44
  pkgLink: <a href="#/Content/packages?status=Upgradable">{__('installing or updating packages')}</a>,
36
45
  }}
37
46
  defaultMessage={__('Traces may be listed here after {pkgLink}.')}
38
- />);
47
+ />) : tracesNotAvailBody;
39
48
  const emptySearchTitle = __('No matching traces found');
40
49
  const emptySearchBody = __('Try changing your search settings.');
41
50
  const errorSearchTitle = __('Problem searching traces');
@@ -74,6 +83,26 @@ const TracesTab = () => {
74
83
  initialSearchQuery: searchParam || '',
75
84
  });
76
85
 
86
+ const BulkRestartTracesAction = () => resolveTraces({
87
+ hostname, search: fetchBulkParams(),
88
+ });
89
+ const {
90
+ triggerJobStart: triggerBulkRestart, lastCompletedJob: lastCompletedBulkRestart,
91
+ isPolling: isBulkRestartInProgress,
92
+ } = useRexJobPolling(BulkRestartTracesAction);
93
+
94
+ const restartTraceAction = id => resolveTraces({
95
+ hostname,
96
+ search: tracesSearchQuery(id),
97
+ });
98
+
99
+ const {
100
+ triggerJobStart: triggerAppRestart, lastCompletedJob: lastCompletedAppRestart,
101
+ isPolling: isAppRestartInProgress,
102
+ } = useRexJobPolling(restartTraceAction);
103
+
104
+ const actionInProgress = (isBulkRestartInProgress || isAppRestartInProgress);
105
+
77
106
  const fetchItems = useCallback(
78
107
  params =>
79
108
  (hostId ? getHostTraces(hostId, { ...apiSortParams, ...params }) : hostIdNotReady),
@@ -81,23 +110,19 @@ const TracesTab = () => {
81
110
  );
82
111
 
83
112
  const onBulkRestartApp = () => {
84
- dispatch(resolveTraces({
85
- hostname, search: fetchBulkParams(),
86
- }));
113
+ triggerBulkRestart();
87
114
  selectNone();
88
- const params = { page: meta.page, per_page: meta.per_page, search: meta.search };
89
- dispatch(getHostTraces(hostId, params));
90
115
  };
91
116
 
92
- const onRestartApp = id => dispatch(resolveTraces({
93
- hostname,
94
- search: tracesSearchQuery(id),
95
- }));
117
+ const onRestartApp = id => triggerAppRestart(id);
96
118
 
97
119
  const bulkCustomizedRexUrl = () => resolveTraceUrl({
98
120
  hostname, search: (selectedCount > 0) ? fetchBulkParams() : '',
99
121
  });
100
122
 
123
+ const readOnlyBookmarks =
124
+ cannot(createBookmarks, userPermissionsFromHostDetails({ hostDetails }));
125
+
101
126
  const dropdownItems = [
102
127
  <DropdownItem isDisabled={selectedCount === 0} aria-label="bulk_rex" key="bulk_rex" component="button" onClick={onBulkRestartApp}>
103
128
  {__('Restart via remote execution')}
@@ -107,24 +132,28 @@ const TracesTab = () => {
107
132
  </DropdownItem>,
108
133
  ];
109
134
 
110
- const actionButtons = (
135
+ const actionButtons = showActions ? (
111
136
  <Split hasGutter>
112
137
  <SplitItem>
113
138
  <ActionList isIconList>
114
- <ActionListItem>
115
- <Button
116
- variant="secondary"
117
- isDisabled={selectedCount === 0}
118
- onClick={onBulkRestartApp}
119
- >
120
- {__('Restart app')}
121
- </Button>
122
- </ActionListItem>
123
139
  <ActionListItem>
124
140
  <Dropdown
125
- toggle={<KebabToggle aria-label="bulk_actions" onToggle={toggleBulkAction} />}
141
+ aria-label="bulk_actions_dropdown"
142
+ toggle={
143
+ <DropdownToggle
144
+ aria-label="bulk_actions"
145
+ splitButtonItems={[
146
+ <DropdownToggleAction key="action" onClick={onBulkRestartApp}>
147
+ {__('Restart app')}
148
+ </DropdownToggleAction>,
149
+ ]}
150
+ isDisabled={selectedCount === 0}
151
+ splitButtonVariant="action"
152
+ toggleVariant="primary"
153
+ onToggle={toggleBulkAction}
154
+ />
155
+ }
126
156
  isOpen={isBulkActionOpen}
127
- isPlain
128
157
  dropdownItems={dropdownItems}
129
158
  />
130
159
  </ActionListItem>
@@ -132,9 +161,9 @@ const TracesTab = () => {
132
161
  </SplitItem>
133
162
  </Split>
134
163
 
135
- );
164
+ ) : null;
136
165
  const status = useSelector(state => selectHostTracesStatus(state));
137
- if (showEnableTracer) return <EnableTracerEmptyState />;
166
+ if (showEnableTracer && showActions) return <TracesEnabler hostname={hostname} />;
138
167
 
139
168
  if (!hostId) return <Skeleton />;
140
169
 
@@ -159,16 +188,18 @@ const TracesTab = () => {
159
188
  actionButtons,
160
189
  }
161
190
  }
162
- happyEmptyContent
191
+ happyEmptyContent={showActions}
163
192
  ouiaId="host-traces-table"
164
193
  metadata={meta}
165
194
  bookmarkController="katello_host_tracers"
195
+ readOnlyBookmarks={readOnlyBookmarks}
166
196
  autocompleteEndpoint={`/hosts/${hostId}/traces/auto_complete_search`}
167
197
  foremanApiAutoComplete
168
198
  rowsCount={results?.length}
169
199
  variant={TableVariant.compact}
170
- displaySelectAllCheckbox
171
- additionalListeners={[activeSortColumn, activeSortDirection]}
200
+ additionalListeners={[activeSortColumn, activeSortDirection,
201
+ lastCompletedAppRestart, lastCompletedBulkRestart]}
202
+ displaySelectAllCheckbox={showActions}
172
203
  {...selectAll}
173
204
  >
174
205
  <Thead>
@@ -191,8 +222,11 @@ const TracesTab = () => {
191
222
  app_type: appType,
192
223
  } = result;
193
224
  const resolveDisabled = !isSelectable(id);
225
+ let disabledReason;
226
+ if (resolveDisabled) disabledReason = __('Traces that require logout cannot be restarted remotely');
227
+ if (actionInProgress) disabledReason = __('A remote execution job is in progress');
194
228
  let rowDropdownItems = [
195
- { title: 'Restart via remote execution', onClick: () => onRestartApp(id) },
229
+ { title: 'Restart via remote execution', onClick: () => onRestartApp(id), isDisabled: actionInProgress },
196
230
  {
197
231
  component: 'a', href: resolveTraceUrl({ hostname, search: tracesSearchQuery(id) }), title: 'Restart via customized remote execution',
198
232
  },
@@ -204,25 +238,32 @@ const TracesTab = () => {
204
238
  }
205
239
  return (
206
240
  <Tr key={id} >
207
- <Td select={{
208
- disable: resolveDisabled,
209
- props: {
210
- 'aria-label': `check-${application}`,
211
- },
212
- isSelected: isSelected(id),
213
- onSelect: (event, selected) => selectOne(selected, id),
214
- rowIndex,
215
- variant: 'checkbox',
216
- }}
217
- />
241
+ {showActions ? (
242
+ <Td
243
+ select={{
244
+ disable: actionInProgress || resolveDisabled,
245
+ props: {
246
+ 'aria-label': `check-${application}`,
247
+ },
248
+ isSelected: isSelected(id),
249
+ onSelect: (event, selected) => selectOne(selected, id),
250
+ rowIndex,
251
+ variant: 'checkbox',
252
+ }}
253
+ title={disabledReason}
254
+ />
255
+ ) : <Td>&nbsp;</Td>
256
+ }
218
257
  <Td>{application}</Td>
219
258
  <Td>{appType}</Td>
220
259
  <Td>{helper}</Td>
221
- <Td
222
- actions={{
223
- items: rowDropdownItems,
224
- }}
225
- />
260
+ {showActions && (
261
+ <Td
262
+ actions={{
263
+ items: rowDropdownItems,
264
+ }}
265
+ />
266
+ )}
226
267
  </Tr>
227
268
  );
228
269
  })
@@ -10,6 +10,14 @@ import mockErrataData from './errata.fixtures.json';
10
10
  import mockResolveErrataTask from './resolveErrata.fixtures.json';
11
11
  import mockBookmarkData from './bookmarks.fixtures.json';
12
12
 
13
+ jest.mock('../../hostDetailsHelpers', () => ({
14
+ ...jest.requireActual('../../hostDetailsHelpers'),
15
+ userPermissionsFromHostDetails: () => ({
16
+ create_job_invocations: true,
17
+ edit_hosts: true,
18
+ }),
19
+ }));
20
+
13
21
  const contentFacetAttributes = {
14
22
  id: 11,
15
23
  uuid: 'e5761ea3-4117-4ecf-83d0-b694f99b389e',
@@ -875,7 +883,7 @@ test('Can bulk apply via katello agent', async (done) => {
875
883
  getByLabelText('Select row 0').click();
876
884
  getByLabelText('Select row 1').click();
877
885
 
878
- const actionMenu = getByLabelText('bulk_actions');
886
+ const actionMenu = getByLabelText('expand_errata_toggle');
879
887
  actionMenu.click();
880
888
  const viaAction = queryByText('Apply via Katello agent');
881
889
  expect(viaAction).toBeInTheDocument();
@@ -923,7 +931,7 @@ test('Can select all, exclude and bulk apply via katello agent', async (done) =>
923
931
 
924
932
  getByLabelText('Select row 0').click(); // deselect
925
933
 
926
- const actionMenu = getByLabelText('bulk_actions');
934
+ const actionMenu = getByLabelText('expand_errata_toggle');
927
935
  actionMenu.click();
928
936
  const viaAction = queryByText('Apply via Katello agent');
929
937
  expect(viaAction).toBeInTheDocument();
@@ -949,11 +957,6 @@ test('Apply button chooses remote execution', async (done) => {
949
957
  .query(defaultQuery)
950
958
  .reply(200, mockErrata);
951
959
 
952
- const scope1 = nockInstance
953
- .get(hostErrata)
954
- .query(baseQuery)
955
- .reply(200, mockErrata);
956
-
957
960
  const resolveErrataScope = nockInstance
958
961
  .post(jobInvocations)
959
962
  .reply(201, mockResolveErrataTask);
@@ -974,7 +977,6 @@ test('Apply button chooses remote execution', async (done) => {
974
977
 
975
978
  assertNockRequest(autocompleteScope);
976
979
  assertNockRequest(resolveErrataScope);
977
- assertNockRequest(scope1);
978
980
  assertNockRequest(scope, done);
979
981
  });
980
982
 
@@ -987,11 +989,6 @@ test('Can bulk apply via remote execution', async (done) => {
987
989
  .query(defaultQuery)
988
990
  .reply(200, mockErrata);
989
991
 
990
- const scope1 = nockInstance
991
- .get(hostErrata)
992
- .query(baseQuery)
993
- .reply(200, mockErrata);
994
-
995
992
  // eslint-disable-next-line camelcase
996
993
  const jobInvocationBody = ({ job_invocation: { inputs, feature, search_query } }) =>
997
994
  inputs[ERRATA_SEARCH_QUERY] === `errata_id ^ (${results[0].errata_id},${results[1].errata_id})` &&
@@ -1013,7 +1010,7 @@ test('Can bulk apply via remote execution', async (done) => {
1013
1010
  getByLabelText('Select row 0').click();
1014
1011
  getByLabelText('Select row 1').click();
1015
1012
 
1016
- const actionMenu = getByLabelText('bulk_actions');
1013
+ const actionMenu = getByLabelText('expand_errata_toggle');
1017
1014
  actionMenu.click();
1018
1015
  const viaRexAction = queryByText('Apply via remote execution');
1019
1016
  expect(viaRexAction).toBeInTheDocument();
@@ -1021,7 +1018,6 @@ test('Can bulk apply via remote execution', async (done) => {
1021
1018
 
1022
1019
  assertNockRequest(autocompleteScope);
1023
1020
  assertNockRequest(resolveErrataScope);
1024
- assertNockRequest(scope1);
1025
1021
  assertNockRequest(scope, done);
1026
1022
  });
1027
1023
 
@@ -1036,11 +1032,6 @@ test('Can select all, exclude and bulk apply via remote execution', async (done)
1036
1032
  .query(defaultQuery)
1037
1033
  .reply(200, mockErrata);
1038
1034
 
1039
- const scope1 = nockInstance
1040
- .get(hostErrata)
1041
- .query(baseQuery)
1042
- .reply(200, mockErrata);
1043
-
1044
1035
  const jobInvocationBody = ({ job_invocation: { inputs } }) =>
1045
1036
  inputs[ERRATA_SEARCH_QUERY] === `errata_id !^ (${results[0].errata_id})`;
1046
1037
 
@@ -1059,7 +1050,7 @@ test('Can select all, exclude and bulk apply via remote execution', async (done)
1059
1050
 
1060
1051
  getByLabelText('Select row 0').click(); // de select
1061
1052
 
1062
- const actionMenu = getByLabelText('bulk_actions');
1053
+ const actionMenu = getByLabelText('expand_errata_toggle');
1063
1054
  actionMenu.click();
1064
1055
  const viaRexAction = queryByText('Apply via remote execution');
1065
1056
  expect(viaRexAction).toBeInTheDocument();
@@ -1067,7 +1058,6 @@ test('Can select all, exclude and bulk apply via remote execution', async (done)
1067
1058
 
1068
1059
  assertNockRequest(autocompleteScope);
1069
1060
  assertNockRequest(resolveErrataScope);
1070
- assertNockRequest(scope1);
1071
1061
  assertNockRequest(scope, done);
1072
1062
  });
1073
1063
 
@@ -1091,7 +1081,7 @@ test('Can apply errata in bulk via customized remote execution', async (done) =>
1091
1081
  getByLabelText('Select row 1').click();
1092
1082
  const errata = `${results[0].errata_id},${results[1].errata_id}`;
1093
1083
  const feature = REX_FEATURES.KATELLO_HOST_ERRATA_INSTALL_BY_SEARCH;
1094
- const actionMenu = getByLabelText('bulk_actions');
1084
+ const actionMenu = getByLabelText('expand_errata_toggle');
1095
1085
  actionMenu.click();
1096
1086
  const viaRexAction = queryByText('Apply via customized remote execution');
1097
1087
  expect(viaRexAction).toBeInTheDocument();
@@ -1,12 +1,19 @@
1
1
  import React from 'react';
2
2
  import { act } from 'react-test-renderer';
3
3
  import { renderWithRedux, patientlyWaitFor, within, fireEvent } from 'react-testing-lib-wrapper';
4
- import { nockInstance, assertNockRequest, mockForemanAutocomplete, mockSetting } from '../../../../../../test-utils/nockWrapper';
5
- import { foremanApi } from '../../../../../../services/api';
6
- import { ModuleStreamsTab } from '../ModuleStreamsTab';
7
- import mockModuleStreams from './modules.fixtures.json';
8
- import { MODULE_STREAMS_KEY } from '../ModuleStreamsConstants';
9
- import mockBookmarkData from '../../__tests__/bookmarks.fixtures.json';
4
+ import { nockInstance, assertNockRequest, mockForemanAutocomplete, mockSetting } from '../../../../../test-utils/nockWrapper';
5
+ import { ModuleStreamsTab } from '../ModuleStreamsTab/ModuleStreamsTab.js';
6
+ import mockModuleStreams from './moduleStreams.fixtures.json';
7
+ import mockBookmarkData from './bookmarks.fixtures.json';
8
+ import { MODULE_STREAMS_KEY } from '../../../../../scenes/ModuleStreams/ModuleStreamsConstants';
9
+ import { foremanApi } from '../../../../../services/api';
10
+
11
+ jest.mock('../../hostDetailsHelpers', () => ({
12
+ ...jest.requireActual('../../hostDetailsHelpers'),
13
+ userPermissionsFromHostDetails: () => ({
14
+ create_job_invocations: true,
15
+ }),
16
+ }));
10
17
 
11
18
  const moduleBookmarks = foremanApi.getApiUrl('/bookmarks?search=controller%3Dkatello_host_available_module_streams');
12
19