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
@@ -1,8 +1,9 @@
1
1
  import React, { useCallback, useState } from 'react';
2
2
  import { useSelector, useDispatch } from 'react-redux';
3
3
  import {
4
- Button, Split, SplitItem, ActionList, ActionListItem, Dropdown,
4
+ Split, SplitItem, ActionList, ActionListItem, Dropdown,
5
5
  DropdownItem, KebabToggle, Skeleton, Tooltip, ToggleGroup, ToggleGroupItem,
6
+ DropdownToggle, DropdownToggleAction,
6
7
  } from '@patternfly/react-core';
7
8
  import { TimesIcon, CheckIcon } from '@patternfly/react-icons';
8
9
  import {
@@ -33,8 +34,18 @@ import { installErrata } from '../RemoteExecutionActions';
33
34
  import { errataInstallUrl } from '../customizedRexUrlHelpers';
34
35
  import './ErrataTab.scss';
35
36
  import hostIdNotReady from '../../HostDetailsActions';
36
- import { defaultRemoteActionMethod, KATELLO_AGENT } from '../../hostDetailsHelpers';
37
+ import { defaultRemoteActionMethod,
38
+ hasRequiredPermissions as can,
39
+ missingRequiredPermissions as cannot,
40
+ KATELLO_AGENT,
41
+ userPermissionsFromHostDetails } from '../../hostDetailsHelpers';
37
42
  import SortableColumnHeaders from '../../../../Table/components/SortableColumnHeaders';
43
+ import { useRexJobPolling } from '../RemoteExecutionHooks';
44
+
45
+ const recalculateApplicability = ['edit_hosts'];
46
+ const invokeRexJobs = ['create_job_invocations'];
47
+ const doKatelloAgentActions = ['edit_hosts'];
48
+ const createBookmarks = ['create_bookmarks'];
38
49
 
39
50
  export const ErrataTab = () => {
40
51
  const hostDetails = useSelector(state => selectAPIResponse(state, 'HOST_DETAILS'));
@@ -44,6 +55,12 @@ export const ErrataTab = () => {
44
55
  content_facet_attributes: contentFacetAttributes,
45
56
  errata_status: errataStatus,
46
57
  } = hostDetails;
58
+ const userPermissions = userPermissionsFromHostDetails({ hostDetails });
59
+ const showRecalculate =
60
+ can(
61
+ recalculateApplicability,
62
+ userPermissions,
63
+ );
47
64
  const contentFacet = propsToCamelCase(contentFacetAttributes ?? {});
48
65
  const dispatch = useDispatch();
49
66
  const toggleGroupStates = ['all', 'installable'];
@@ -67,6 +84,12 @@ export const ErrataTab = () => {
67
84
  = useState(PARAM_TO_FRIENDLY_NAME[initialSeverity] ?? ERRATA_SEVERITY);
68
85
  const activeFilters = [errataTypeSelected, errataSeveritySelected];
69
86
  const defaultFilters = [ERRATA_TYPE, ERRATA_SEVERITY];
87
+
88
+ const [isActionOpen, setIsActionOpen] = useState(false);
89
+ const onActionToggle = () => {
90
+ setIsActionOpen(prev => !prev);
91
+ };
92
+
70
93
  const allUpToDate = errataStatus === 0;
71
94
  const emptyContentTitle = allUpToDate ? __('All errata up-to-date') : __('This host has errata that are applicable, but not installable.');
72
95
  const emptyContentBody = allUpToDate ? __('No action is needed because there are no applicable errata for this host.') : __("You may want to check the host's content view and lifecycle environment.");
@@ -136,8 +159,29 @@ export const ErrataTab = () => {
136
159
  initialSearchQuery: searchParam || '',
137
160
  });
138
161
 
139
- const tdSelect = useCallback((errataId, rowIndex) => ({
140
- disable: !isSelectable(errataId),
162
+ const installErrataAction = () => installErrata({
163
+ hostname, search: fetchBulkParams(),
164
+ });
165
+ const {
166
+ triggerJobStart: triggerBulkApply, lastCompletedJob: lastCompletedBulkApply,
167
+ isPolling: isBulkApplyInProgress,
168
+ } = useRexJobPolling(installErrataAction);
169
+
170
+ const installErratumAction = id => installErrata({
171
+ hostname,
172
+ search: errataSearchQuery(id),
173
+ });
174
+
175
+ const {
176
+ triggerJobStart: triggerApply, lastCompletedJob: lastCompletedApply,
177
+ isPolling: isApplyInProgress,
178
+ } = useRexJobPolling(installErratumAction);
179
+
180
+ const actionInProgress = (isApplyInProgress || isBulkApplyInProgress);
181
+ const disabledReason = __('A remote execution job is in progress');
182
+
183
+ const tdSelect = useCallback((errataId, rowIndex, rexJobInProgress) => ({
184
+ disable: rexJobInProgress || !isSelectable(errataId),
141
185
  isSelected: isSelected(errataId),
142
186
  onSelect: (event, selected) => selectOne(selected, errataId),
143
187
  rowIndex,
@@ -146,21 +190,11 @@ export const ErrataTab = () => {
146
190
 
147
191
  if (!hostId) return <Skeleton />;
148
192
 
149
- const applyErratumViaRemoteExecution = id => dispatch(installErrata({
150
- hostname,
151
- search: errataSearchQuery(id),
152
- }));
193
+ const applyErratumViaRemoteExecution = id => triggerApply(id);
153
194
 
154
195
  const applyViaRemoteExecution = () => {
155
- dispatch(installErrata({
156
- hostname, search: fetchBulkParams(),
157
- }));
158
-
159
- const params = { page: metadata.page, per_page: metadata.per_page, search: metadata.search };
160
- dispatch(getInstallableErrata(
161
- hostId,
162
- { ...params, include_applicable: toggleGroupState === ALL },
163
- ));
196
+ triggerBulkApply();
197
+ selectNone();
164
198
  };
165
199
 
166
200
  const bulkCustomizedRexUrl = () => errataInstallUrl({
@@ -185,6 +219,10 @@ export const ErrataTab = () => {
185
219
  ));
186
220
 
187
221
  const defaultRemoteAction = defaultRemoteActionMethod({ hostDetails });
222
+ const showActions = defaultRemoteAction === KATELLO_AGENT ?
223
+ can(doKatelloAgentActions, userPermissions) :
224
+ can(invokeRexJobs, userPermissions);
225
+
188
226
  const apply = () => {
189
227
  if (defaultRemoteAction === KATELLO_AGENT) {
190
228
  applyByKatelloAgent();
@@ -193,7 +231,10 @@ export const ErrataTab = () => {
193
231
  }
194
232
  };
195
233
 
196
- const dropdownItems = [
234
+ const readOnlyBookmarks =
235
+ cannot(createBookmarks, userPermissionsFromHostDetails({ hostDetails }));
236
+
237
+ const dropdownKebabItems = [
197
238
  <DropdownItem
198
239
  aria-label="bulk_add"
199
240
  key="bulk_add"
@@ -204,20 +245,7 @@ export const ErrataTab = () => {
204
245
  </DropdownItem>,
205
246
  ];
206
247
 
207
- if (defaultRemoteAction === KATELLO_AGENT) {
208
- dropdownItems.push((
209
- <DropdownItem
210
- aria-label="apply_via_katello_agent"
211
- key="apply_via_katello_agent"
212
- component="button"
213
- onClick={applyByKatelloAgent}
214
- isDisabled={selectedCount === 0}
215
- >
216
- {__('Apply via Katello agent')}
217
- </DropdownItem>));
218
- }
219
-
220
- dropdownItems.push((
248
+ const dropdownItems = [
221
249
  <DropdownItem
222
250
  aria-label="apply_via_remote_execution"
223
251
  key="apply_via_remote_execution"
@@ -226,9 +254,7 @@ export const ErrataTab = () => {
226
254
  isDisabled={selectedCount === 0}
227
255
  >
228
256
  {__('Apply via remote execution')}
229
- </DropdownItem>));
230
-
231
- dropdownItems.push((
257
+ </DropdownItem>,
232
258
  <DropdownItem
233
259
  aria-label="apply_via_customized_remote_execution"
234
260
  key="apply_via_customized_remote_execution"
@@ -237,7 +263,21 @@ export const ErrataTab = () => {
237
263
  isDisabled={selectedCount === 0}
238
264
  >
239
265
  {__('Apply via customized remote execution')}
240
- </DropdownItem>));
266
+ </DropdownItem>,
267
+ ];
268
+
269
+ if (defaultRemoteAction === KATELLO_AGENT) {
270
+ dropdownItems.unshift((
271
+ <DropdownItem
272
+ aria-label="apply_via_katello_agent"
273
+ key="apply_via_katello_agent"
274
+ component="button"
275
+ onClick={applyByKatelloAgent}
276
+ isDisabled={selectedCount === 0}
277
+ >
278
+ {__('Apply via Katello agent')}
279
+ </DropdownItem>));
280
+ }
241
281
 
242
282
  const handleErrataTypeSelected = newType => setErrataTypeSelected((prevType) => {
243
283
  if (prevType === newType) {
@@ -253,27 +293,47 @@ export const ErrataTab = () => {
253
293
  return newSeverity;
254
294
  });
255
295
 
256
- const actionButtons = (
296
+ const actionButtons = showActions ? (
257
297
  <>
258
298
  <Split hasGutter>
259
299
  <SplitItem>
260
300
  <ActionList isIconList>
261
301
  <ActionListItem>
262
- <Button isDisabled={selectedCount === 0} onClick={apply}> {__('Apply')} </Button>
302
+ <Dropdown
303
+ aria-label="errata_dropdown"
304
+ toggle={
305
+ <DropdownToggle
306
+ aria-label="expand_errata_toggle"
307
+ splitButtonItems={[
308
+ <DropdownToggleAction key="action" aria-label="bulk_actions" onClick={apply}>
309
+ {__('Apply')}
310
+ </DropdownToggleAction>,
311
+ ]}
312
+ splitButtonVariant="action"
313
+ toggleVariant="primary"
314
+ onToggle={onActionToggle}
315
+ isDisabled={selectedCount === 0}
316
+ />
317
+ }
318
+ isOpen={isActionOpen}
319
+ dropdownItems={dropdownItems}
320
+ />
263
321
  </ActionListItem>
322
+ {showRecalculate &&
264
323
  <ActionListItem>
265
324
  <Dropdown
266
- toggle={<KebabToggle aria-label="bulk_actions" onToggle={toggleBulkAction} />}
325
+ toggle={<KebabToggle aria-label="bulk_actions_kebab" onToggle={toggleBulkAction} />}
267
326
  isOpen={isBulkActionOpen}
268
327
  isPlain
269
- dropdownItems={dropdownItems}
328
+ dropdownItems={dropdownKebabItems}
270
329
  />
271
330
  </ActionListItem>
331
+ }
272
332
  </ActionList>
273
333
  </SplitItem>
274
334
  </Split>
275
335
  </>
276
- );
336
+ ) : null;
277
337
 
278
338
  const hostIsNonLibrary = (
279
339
  contentFacet?.contentViewDefault === false && contentFacet.lifecycleEnvironmentLibrary === false
@@ -352,15 +412,17 @@ export const ErrataTab = () => {
352
412
  ouiaId="host-errata-table"
353
413
  additionalListeners={[
354
414
  hostId, toggleGroupState, errataTypeSelected,
355
- errataSeveritySelected, activeSortColumn, activeSortDirection]}
415
+ errataSeveritySelected, activeSortColumn, activeSortDirection,
416
+ lastCompletedApply, lastCompletedBulkApply]}
356
417
  fetchItems={fetchItems}
357
418
  bookmarkController="katello_errata"
419
+ readOnlyBookmarks={readOnlyBookmarks}
358
420
  autocompleteEndpoint={`/hosts/${hostId}/errata/auto_complete_search`}
359
421
  foremanApiAutoComplete
360
422
  rowsCount={results?.length}
361
423
  variant={TableVariant.compact}
362
424
  {...selectAll}
363
- displaySelectAllCheckbox
425
+ displaySelectAllCheckbox={showActions}
364
426
  >
365
427
  <Thead>
366
428
  <Tr>
@@ -391,6 +453,7 @@ export const ErrataTab = () => {
391
453
  {
392
454
  title: __('Apply via remote execution'),
393
455
  onClick: () => applyErratumViaRemoteExecution(errataId),
456
+ isDisabled: actionInProgress,
394
457
  },
395
458
  {
396
459
  title: __('Apply via customized remote execution'),
@@ -425,7 +488,12 @@ export const ErrataTab = () => {
425
488
  onToggle: (_event, _rInx, isOpen) => expandedErrata.onToggle(isOpen, id),
426
489
  }}
427
490
  />
428
- <Td select={tdSelect(errataId, rowIndex)} />
491
+ {showActions ? (
492
+ <Td
493
+ select={tdSelect(errataId, rowIndex, actionInProgress)}
494
+ title={actionInProgress && disabledReason}
495
+ />
496
+ ) : null}
429
497
  <Td>
430
498
  <a href={urlBuilder(`errata/${id}`, '')}>{errataId}</a>
431
499
  </Td>
@@ -449,13 +517,14 @@ export const ErrataTab = () => {
449
517
  </Td>
450
518
  <Td><TableText wrapModifier="truncate">{title}</TableText></Td>
451
519
  <Td key={publishedAt}><IsoDate date={publishedAt} /></Td>
452
- <Td
453
- key={`rowActions-${id}`}
454
- actions={{
455
- items: rowActions,
456
- }}
457
-
458
- />
520
+ {showActions ? (
521
+ <Td
522
+ key={`rowActions-${id}`}
523
+ actions={{
524
+ items: rowActions,
525
+ }}
526
+ />
527
+ ) : null}
459
528
  </Tr>
460
529
  <Tr key="child_row" isExpanded={isExpanded}>
461
530
  {isExpanded && (
@@ -1,5 +1,5 @@
1
1
  import React, { useCallback, useState } from 'react';
2
- import { useSelector, useDispatch } from 'react-redux';
2
+ import { useSelector } from 'react-redux';
3
3
  import { FormattedMessage } from 'react-intl';
4
4
  import { translate as __ } from 'foremanReact/common/I18n';
5
5
  import { Skeleton,
@@ -7,6 +7,7 @@ import { Skeleton,
7
7
  Button,
8
8
  Split,
9
9
  SplitItem,
10
+ Spinner,
10
11
  Checkbox,
11
12
  Dropdown,
12
13
  Text,
@@ -39,6 +40,13 @@ import {
39
40
  } from './ModuleStreamsConstants';
40
41
  import { moduleStreamAction } from '../RemoteExecutionActions';
41
42
  import { katelloModuleStreamActionUrl } from '../customizedRexUrlHelpers';
43
+ import { useRexJobPolling } from '../RemoteExecutionHooks';
44
+ import {
45
+ hasRequiredPermissions as can,
46
+ missingRequiredPermissions as cannot,
47
+ userPermissionsFromHostDetails,
48
+ } from '../../hostDetailsHelpers';
49
+
42
50
 
43
51
  const EnabledIcon = ({ streamText, streamInstallStatus, upgradable }) => {
44
52
  switch (true) {
@@ -99,14 +107,9 @@ HostInstalledProfiles.propTypes = {
99
107
  installedProfiles: PropTypes.arrayOf(PropTypes.string).isRequired,
100
108
  };
101
109
 
102
- const performModuleStreamAction = (hostName, action, moduleSpec, dispatch) => {
103
- dispatch(moduleStreamAction({ hostname: hostName, action, moduleSpec }));
104
- };
105
-
106
110
  const ModuleActionConfirmationModal = ({
107
- hostName, action, moduleSpec, actionModalOpen, setActionModalOpen,
111
+ hostname, action, moduleSpec, actionModalOpen, setActionModalOpen, triggerModuleStreamAction,
108
112
  }) => {
109
- const dispatch = useDispatch();
110
113
  let title;
111
114
  let body;
112
115
  let confirmText;
@@ -163,7 +166,7 @@ const ModuleActionConfirmationModal = ({
163
166
  aria-label="confirm-module-action"
164
167
  key="confirm-module-action"
165
168
  onClick={() => {
166
- performModuleStreamAction(hostName, action, moduleSpec, dispatch);
169
+ triggerModuleStreamAction({ hostname, action, moduleSpec });
167
170
  setActionModalOpen(false);
168
171
  }}
169
172
  >
@@ -188,21 +191,26 @@ const ModuleActionConfirmationModal = ({
188
191
  };
189
192
 
190
193
  ModuleActionConfirmationModal.propTypes = {
191
- hostName: PropTypes.string.isRequired,
194
+ hostname: PropTypes.string.isRequired,
192
195
  action: PropTypes.string.isRequired,
193
196
  moduleSpec: PropTypes.string.isRequired,
194
197
  actionModalOpen: PropTypes.bool.isRequired,
195
198
  setActionModalOpen: PropTypes.func.isRequired,
199
+ triggerModuleStreamAction: PropTypes.func.isRequired,
196
200
  };
197
201
 
202
+ const invokeRexJobs = ['create_job_invocations'];
203
+ const createBookmarks = ['create_bookmarks'];
204
+
198
205
  export const ModuleStreamsTab = () => {
199
- const { id: hostId, name: hostName } = useSelector(selectHostDetails);
206
+ const hostDetails = useSelector(selectHostDetails);
207
+ const { id: hostId, name: hostname } = hostDetails;
208
+ const showActions = can(invokeRexJobs, userPermissionsFromHostDetails({ hostDetails }));
200
209
  const [useCustomizedRex, setUseCustomizedRex] = useState('');
201
210
  const [dropdownOpen, setDropdownOpen] = useState('');
202
211
  const [actionModalOpen, setActionModalOpen] = useState(false);
203
212
  const [actionableModuleSpec, setActionableModuleSpec] = useState(null);
204
213
  const [hostModuleStreamAction, setHostModuleStreamAction] = useState(null);
205
- const dispatch = useDispatch();
206
214
 
207
215
  const emptyContentTitle = __('This host does not have any Module streams.');
208
216
  const emptyContentBody = __('Module streams will appear here when available.');
@@ -244,6 +252,18 @@ export const ModuleStreamsTab = () => {
244
252
  initialSortColumnName: 'Name',
245
253
  });
246
254
 
255
+ const {
256
+ triggerJobStart: triggerModuleStreamAction, lastCompletedJob: tableJobCompleted,
257
+ isPolling: isModuleStreamActionInProgress,
258
+ } = useRexJobPolling(moduleStreamAction);
259
+
260
+ const {
261
+ triggerJobStart: triggerConfirmModalAction, lastCompletedJob: confirmModalJobCompleted,
262
+ isPolling: isConfirmModalActionInProgress,
263
+ } = useRexJobPolling(moduleStreamAction);
264
+
265
+ const actionInProgress = (isModuleStreamActionInProgress || isConfirmModalActionInProgress);
266
+
247
267
  const fetchItems = useCallback(
248
268
  (params) => {
249
269
  let extraParams = params;
@@ -279,7 +299,7 @@ export const ModuleStreamsTab = () => {
279
299
  });
280
300
 
281
301
  const customizedActionURL = (action, moduleSpec) =>
282
- katelloModuleStreamActionUrl({ hostname: hostName, action, moduleSpec });
302
+ katelloModuleStreamActionUrl({ hostname, action, moduleSpec });
283
303
 
284
304
  const response = useSelector(selectModuleStream);
285
305
  const { results, ...metadata } = response;
@@ -297,11 +317,13 @@ export const ModuleStreamsTab = () => {
297
317
  });
298
318
  /* eslint-enable no-unused-vars */
299
319
 
320
+ const hideBookmarkActions =
321
+ cannot(createBookmarks, userPermissionsFromHostDetails({ hostDetails }));
322
+
300
323
  if (!hostId) return <Skeleton />;
301
324
 
302
325
  const activeFilters = [statusSelected, installStatusSelected];
303
326
  const defaultFilters = [MODULE_STREAM_STATUS, MODULE_STREAM_INSTALLATION_STATUS];
304
-
305
327
  return (
306
328
  <div>
307
329
  <div id="modulestreams-tab">
@@ -323,9 +345,11 @@ export const ModuleStreamsTab = () => {
323
345
  }}
324
346
  ouiaId="host-module-stream-table"
325
347
  additionalListeners={[hostId, activeSortColumn, activeSortDirection,
326
- statusSelected, installStatusSelected]}
348
+ statusSelected, installStatusSelected, confirmModalJobCompleted,
349
+ tableJobCompleted]}
327
350
  fetchItems={fetchItems}
328
351
  bookmarkController="katello_host_available_module_streams"
352
+ readOnlyBookmarks={hideBookmarkActions}
329
353
  autocompleteEndpoint={`/hosts/${hostId}/module_streams/auto_complete_search`}
330
354
  foremanApiAutoComplete
331
355
  rowsCount={results?.length}
@@ -352,6 +376,11 @@ export const ModuleStreamsTab = () => {
352
376
  setSelected={handleModuleStreamInstallationStatusSelected}
353
377
  />
354
378
  </SplitItem>
379
+ {actionInProgress && (
380
+ <SplitItem style={{ alignSelf: 'center' }}>
381
+ <Spinner size="lg" style={{ marginTop: '2px' }} />
382
+ </SplitItem>
383
+ )}
355
384
  </Split>
356
385
  }
357
386
  >
@@ -460,11 +489,11 @@ export const ModuleStreamsTab = () => {
460
489
  key={`dropdownItem-enable-${id}`}
461
490
  component="button"
462
491
  onClick={() => {
463
- performModuleStreamAction(hostName, 'enable', moduleSpec, dispatch);
492
+ triggerModuleStreamAction({ hostname, action: 'enable', moduleSpec });
464
493
  setUseCustomizedRex('');
465
494
  setDropdownOpen('');
466
495
  }}
467
- isDisabled={stateText(moduleStreamStatus) ===
496
+ isDisabled={actionInProgress || stateText(moduleStreamStatus) ===
468
497
  HOST_MODULE_STREAM_STATUSES.ENABLED}
469
498
  >
470
499
  {__('Enable')}
@@ -480,7 +509,7 @@ export const ModuleStreamsTab = () => {
480
509
  setUseCustomizedRex('');
481
510
  setDropdownOpen('');
482
511
  }}
483
- isDisabled={stateText(moduleStreamStatus) !==
512
+ isDisabled={actionInProgress || stateText(moduleStreamStatus) !==
484
513
  HOST_MODULE_STREAM_STATUSES.ENABLED}
485
514
  >
486
515
  {__('Disable')}
@@ -491,11 +520,11 @@ export const ModuleStreamsTab = () => {
491
520
  key={`dropdownItem-install-${id}`}
492
521
  component="button"
493
522
  onClick={() => {
494
- performModuleStreamAction(hostName, 'install', moduleSpec, dispatch);
523
+ triggerModuleStreamAction({ hostname, action: 'install', moduleSpec });
495
524
  setUseCustomizedRex('');
496
525
  setDropdownOpen('');
497
526
  }}
498
- isDisabled={(upgradable ||
527
+ isDisabled={(actionInProgress || upgradable ||
499
528
  (installedStatus !== INSTALLED_STATE.NOTINSTALLED) ||
500
529
  !(stateText(moduleStreamStatus) === HOST_MODULE_STREAM_STATUSES.ENABLED ||
501
530
  stateText(moduleStreamStatus) === HOST_MODULE_STREAM_STATUSES.DISABLED)
@@ -508,11 +537,11 @@ export const ModuleStreamsTab = () => {
508
537
  key={`dropdownItem-update-${id}`}
509
538
  component="button"
510
539
  onClick={() => {
511
- performModuleStreamAction(hostName, 'update', moduleSpec, dispatch);
540
+ triggerModuleStreamAction({ hostname, action: 'update', moduleSpec });
512
541
  setUseCustomizedRex('');
513
542
  setDropdownOpen('');
514
543
  }}
515
- isDisabled={!upgradable}
544
+ isDisabled={actionInProgress || !upgradable}
516
545
  >
517
546
  {__('Update')}
518
547
  </DropdownItem>,
@@ -527,6 +556,7 @@ export const ModuleStreamsTab = () => {
527
556
  setUseCustomizedRex('');
528
557
  setDropdownOpen('');
529
558
  }}
559
+ isDisabled={actionInProgress}
530
560
  >
531
561
  {__('Reset')}
532
562
  <InactiveText style={{ marginBottom: '1px' }} text={__('Reset to the default state')} />
@@ -542,6 +572,7 @@ export const ModuleStreamsTab = () => {
542
572
  setUseCustomizedRex('');
543
573
  setDropdownOpen('');
544
574
  }}
575
+ isDisabled={actionInProgress}
545
576
  >
546
577
  {__('Remove')}
547
578
  <InactiveText style={{ marginBottom: '1px' }} text={__('Uninstall and reset')} />
@@ -552,7 +583,7 @@ export const ModuleStreamsTab = () => {
552
583
  <Tr key={`${id} ${index}`}>
553
584
  <Td>
554
585
  <a
555
- href={`/module_streams?search=module_spec%3D${moduleSpec}+and+host%3D${hostName}`}
586
+ href={`/module_streams?search=module_spec%3D${moduleSpec}+and+host%3D${hostname}`}
556
587
  >
557
588
  {name}
558
589
  </a>
@@ -574,20 +605,22 @@ export const ModuleStreamsTab = () => {
574
605
  installedProfiles={installedProfiles}
575
606
  />
576
607
  </Td>
577
- <Td key={`actions-td-${id}-${dropdownOpen}`}>
578
- <Dropdown
579
- aria-label={`actions-dropdown-${id}`}
580
- key={`actions-dropdown-${id}-${dropdownOpen}`}
581
- isPlain
582
- style={{ width: 'inherit' }}
583
- position={DropdownPosition.right}
584
- toggle={
585
- <KebabToggle aria-label={`kebab-dropdown-${id}`} onToggle={() => ((dropdownOpen === id) ? setDropdownOpen('') : setDropdownOpen(id))} id={`toggle-dropdown-${id}`} />
586
- }
587
- isOpen={id === dropdownOpen}
588
- dropdownItems={dropdownItems}
589
- />
590
- </Td>
608
+ {showActions && (
609
+ <Td key={`actions-td-${id}-${dropdownOpen}`}>
610
+ <Dropdown
611
+ aria-label={`actions-dropdown-${id}`}
612
+ key={`actions-dropdown-${id}-${dropdownOpen}`}
613
+ isPlain
614
+ style={{ width: 'inherit' }}
615
+ position={DropdownPosition.right}
616
+ toggle={
617
+ <KebabToggle aria-label={`kebab-dropdown-${id}`} onToggle={() => ((dropdownOpen === id) ? setDropdownOpen('') : setDropdownOpen(id))} id={`toggle-dropdown-${id}`} />
618
+ }
619
+ isOpen={id === dropdownOpen}
620
+ dropdownItems={dropdownItems}
621
+ />
622
+ </Td>
623
+ )}
591
624
  </Tr>
592
625
  );
593
626
  })
@@ -596,11 +629,12 @@ export const ModuleStreamsTab = () => {
596
629
  </TableWrapper>
597
630
  {actionModalOpen &&
598
631
  <ModuleActionConfirmationModal
599
- hostName={hostName}
632
+ hostname={hostname}
600
633
  action={hostModuleStreamAction}
601
634
  moduleSpec={actionableModuleSpec}
602
635
  actionModalOpen={actionModalOpen}
603
636
  setActionModalOpen={setActionModalOpen}
637
+ triggerModuleStreamAction={triggerConfirmModalAction}
604
638
  />
605
639
  }
606
640
  </div>
@@ -15,7 +15,6 @@ import { HOST_YUM_INSTALLABLE_PACKAGES_KEY } from './YumInstallablePackagesConst
15
15
  import { selectHostYumInstallablePackagesStatus } from './YumInstallablePackagesSelectors';
16
16
  import { getHostYumInstallablePackages } from './YumInstallablePackagesActions';
17
17
  import './PackageInstallModal.scss';
18
- import { installPackageBySearch } from '../RemoteExecutionActions';
19
18
  import { katelloPackageInstallBySearchUrl, katelloPackageInstallUrl } from '../customizedRexUrlHelpers';
20
19
  import hostIdNotReady from '../../HostDetailsActions';
21
20
  import { installPackageViaKatelloAgent } from './HostPackagesActions';
@@ -101,7 +100,7 @@ InstallDropdown.defaultProps = {
101
100
  };
102
101
 
103
102
  const PackageInstallModal = ({
104
- isOpen, closeModal, hostId, hostName, showKatelloAgent,
103
+ isOpen, closeModal, hostId, hostName, showKatelloAgent, triggerPackageInstall,
105
104
  }) => {
106
105
  const emptyContentTitle = __('No packages available to install');
107
106
  const emptyContentBody = __('No packages available to install on this host. Please check the host\'s content view and lifecycle environment.');
@@ -127,6 +126,7 @@ const PackageInstallModal = ({
127
126
  selectedResults,
128
127
  ...selectAll
129
128
  } = useBulkSelect({ results, metadata });
129
+
130
130
  const fetchItems = (params) => {
131
131
  if (!hostId) return hostIdNotReady;
132
132
 
@@ -141,7 +141,7 @@ const PackageInstallModal = ({
141
141
  const selectedPackageNames = () => selectedResults.map(({ name }) => name);
142
142
 
143
143
  const installViaRex = () => {
144
- dispatch(installPackageBySearch({ hostname: hostName, search: fetchBulkParams() }));
144
+ triggerPackageInstall(fetchBulkParams());
145
145
  selectNone();
146
146
  closeModal();
147
147
  };
@@ -271,6 +271,7 @@ PackageInstallModal.propTypes = {
271
271
  hostId: PropTypes.number.isRequired,
272
272
  hostName: PropTypes.string.isRequired,
273
273
  showKatelloAgent: PropTypes.bool,
274
+ triggerPackageInstall: PropTypes.func.isRequired,
274
275
  };
275
276
 
276
277
  PackageInstallModal.defaultProps = {