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,6 +12,7 @@ import {
12
12
  Skeleton,
13
13
  Split,
14
14
  SplitItem,
15
+ Spinner,
15
16
  } from '@patternfly/react-core';
16
17
  import { TableVariant, Thead, Tbody, Tr, Th, Td } from '@patternfly/react-table';
17
18
  import { translate as __ } from 'foremanReact/common/I18n';
@@ -32,13 +33,22 @@ import { selectHostPackagesStatus } from './HostPackagesSelectors';
32
33
  import {
33
34
  HOST_PACKAGES_KEY, PACKAGES_VERSION_STATUSES, VERSION_STATUSES_TO_PARAM,
34
35
  } from './HostPackagesConstants';
35
- import { removePackage, updatePackage, removePackages, updatePackages } from '../RemoteExecutionActions';
36
+ import { removePackage, updatePackage, removePackages, updatePackages, installPackageBySearch } from '../RemoteExecutionActions';
36
37
  import { katelloPackageUpdateUrl, packagesUpdateUrl } from '../customizedRexUrlHelpers';
37
38
  import './PackagesTab.scss';
38
39
  import hostIdNotReady from '../../HostDetailsActions';
39
40
  import PackageInstallModal from './PackageInstallModal';
40
- import { defaultRemoteActionMethod, KATELLO_AGENT } from '../../hostDetailsHelpers';
41
+ import { defaultRemoteActionMethod,
42
+ hasRequiredPermissions as can,
43
+ missingRequiredPermissions as cannot,
44
+ KATELLO_AGENT,
45
+ userPermissionsFromHostDetails } from '../../hostDetailsHelpers';
41
46
  import SortableColumnHeaders from '../../../../Table/components/SortableColumnHeaders';
47
+ import { useRexJobPolling } from '../RemoteExecutionHooks';
48
+
49
+ const invokeRexJobs = ['create_job_invocations'];
50
+ const doKatelloAgentActions = ['edit_hosts'];
51
+ const createBookmarks = ['create_bookmarks'];
42
52
 
43
53
  export const PackagesTab = () => {
44
54
  const hostDetails = useSelector(state => selectAPIResponse(state, 'HOST_DETAILS'));
@@ -58,6 +68,9 @@ export const PackagesTab = () => {
58
68
  const [isModalOpen, setIsModalOpen] = useState(false);
59
69
  const closeModal = () => setIsModalOpen(false);
60
70
  const showKatelloAgent = (defaultRemoteActionMethod({ hostDetails }) === KATELLO_AGENT);
71
+ const showActions = showKatelloAgent ?
72
+ can(doKatelloAgentActions, userPermissionsFromHostDetails({ hostDetails })) :
73
+ can(invokeRexJobs, userPermissionsFromHostDetails({ hostDetails }));
61
74
 
62
75
  const [isActionOpen, setIsActionOpen] = useState(false);
63
76
  const onActionSelect = () => {
@@ -128,6 +141,62 @@ export const PackagesTab = () => {
128
141
  initialSearchQuery: searchParam || '',
129
142
  });
130
143
 
144
+ const packageRemoveAction = packageName => removePackage({
145
+ hostname,
146
+ packageName,
147
+ });
148
+
149
+ const {
150
+ triggerJobStart: triggerPackageRemove, lastCompletedJob: lastCompletedPackageRemove,
151
+ isPolling: isRemoveInProgress,
152
+ } = useRexJobPolling(packageRemoveAction);
153
+
154
+ const packageBulkRemoveAction = bulkParams => removePackages({
155
+ hostname,
156
+ search: bulkParams,
157
+ });
158
+
159
+ const {
160
+ triggerJobStart: triggerBulkPackageRemove,
161
+ lastCompletedJob: lastCompletedBulkPackageRemove,
162
+ isPolling: isBulkRemoveInProgress,
163
+ } = useRexJobPolling(packageBulkRemoveAction);
164
+
165
+ const packageUpgradeAction = packageName => updatePackage({
166
+ hostname,
167
+ packageName,
168
+ });
169
+
170
+ const {
171
+ triggerJobStart: triggerPackageUpgrade,
172
+ lastCompletedJob: lastCompletedPackageUpgrade,
173
+ isPolling: isUpgradeInProgress,
174
+ } = useRexJobPolling(packageUpgradeAction);
175
+
176
+ const packageBulkUpgradeAction = bulkParams => updatePackages({
177
+ hostname,
178
+ search: bulkParams,
179
+ });
180
+
181
+ const {
182
+ triggerJobStart: triggerBulkPackageUpgrade,
183
+ lastCompletedJob: lastCompletedBulkPackageUpgrade,
184
+ isPolling: isBulkUpgradeInProgress,
185
+ } = useRexJobPolling(packageBulkUpgradeAction);
186
+
187
+ const packageInstallAction
188
+ = bulkParams => installPackageBySearch({ hostname, search: bulkParams });
189
+
190
+ const {
191
+ triggerJobStart: triggerPackageInstall,
192
+ lastCompletedJob: lastCompletedPackageInstall,
193
+ isPolling: isInstallInProgress,
194
+ } = useRexJobPolling(packageInstallAction);
195
+
196
+ const actionInProgress = (isRemoveInProgress || isUpgradeInProgress
197
+ || isBulkRemoveInProgress || isBulkUpgradeInProgress || isInstallInProgress);
198
+ const disabledReason = __('A remote execution job is in progress.');
199
+
131
200
  if (!hostId) return <Skeleton />;
132
201
 
133
202
  const handleInstallPackagesClick = () => {
@@ -135,10 +204,7 @@ export const PackagesTab = () => {
135
204
  setIsModalOpen(true);
136
205
  };
137
206
 
138
- const removePackageViaRemoteExecution = packageName => dispatch(removePackage({
139
- hostname,
140
- packageName,
141
- }));
207
+ const removePackageViaRemoteExecution = packageName => triggerPackageRemove(packageName);
142
208
 
143
209
  const removeViaKatelloAgent = (packageName) => {
144
210
  dispatch(removePackageViaKatelloAgent(hostId, { packages: [packageName] }));
@@ -149,7 +215,7 @@ export const PackagesTab = () => {
149
215
  const selected = fetchBulkParams();
150
216
  setIsBulkActionOpen(false);
151
217
  selectNone();
152
- dispatch(removePackages({ hostname, search: selected }));
218
+ triggerBulkPackageRemove(selected);
153
219
  };
154
220
 
155
221
  const selectedPackageNames = () => selectedResults.map(({ name }) => name);
@@ -178,19 +244,13 @@ export const PackagesTab = () => {
178
244
  }
179
245
  };
180
246
 
181
- const upgradeViaRemoteExecution = packageName => dispatch(updatePackage({
182
- hostname,
183
- packageName,
184
- }));
247
+ const upgradeViaRemoteExecution = packageName => triggerPackageUpgrade(packageName);
185
248
 
186
249
  const upgradeBulkViaRemoteExecution = () => {
187
250
  const selected = fetchBulkParams();
188
251
  setIsBulkActionOpen(false);
189
252
  selectNone();
190
- dispatch(updatePackages({
191
- hostname,
192
- search: selected,
193
- }));
253
+ triggerBulkPackageUpgrade(selected);
194
254
  };
195
255
 
196
256
  const upgradeBulkViaKatelloAgent = () => {
@@ -223,6 +283,9 @@ export const PackagesTab = () => {
223
283
  (defaultRemoteAction === KATELLO_AGENT && selectAllMode && !areAllRowsSelected()) ||
224
284
  (!selectAllMode && !allUpgradable());
225
285
 
286
+ const readOnlyBookmarks =
287
+ cannot(createBookmarks, userPermissionsFromHostDetails({ hostDetails }));
288
+
226
289
  const dropdownUpgradeItems = [
227
290
  <DropdownItem
228
291
  aria-label="bulk_upgrade_rex"
@@ -270,7 +333,7 @@ export const PackagesTab = () => {
270
333
  return newStatus;
271
334
  });
272
335
 
273
- const actionButtons = (
336
+ const actionButtons = showActions ? (
274
337
  <Split hasGutter>
275
338
  <SplitItem>
276
339
  <ActionList isIconList>
@@ -284,7 +347,7 @@ export const PackagesTab = () => {
284
347
  {__('Upgrade')}
285
348
  </DropdownToggleAction>,
286
349
  ]}
287
- isDisabled={disableUpgrade()}
350
+ isDisabled={actionInProgress || disableUpgrade()}
288
351
  splitButtonVariant="action"
289
352
  toggleVariant="primary"
290
353
  onToggle={onActionToggle}
@@ -295,17 +358,19 @@ export const PackagesTab = () => {
295
358
  />
296
359
  </ActionListItem>
297
360
  <ActionListItem>
298
- <Dropdown
299
- toggle={<KebabToggle aria-label="bulk_actions" onToggle={toggleBulkAction} />}
300
- isOpen={isBulkActionOpen}
301
- isPlain
302
- dropdownItems={dropdownRemoveItems}
303
- />
361
+ {actionInProgress ? <Spinner size="lg" style={{ marginLeft: '1em', marginTop: '4px' }} /> : (
362
+ <Dropdown
363
+ toggle={<KebabToggle aria-label="bulk_actions" onToggle={toggleBulkAction} />}
364
+ isOpen={isBulkActionOpen}
365
+ isPlain
366
+ dropdownItems={dropdownRemoveItems}
367
+ />
368
+ )}
304
369
  </ActionListItem>
305
370
  </ActionList>
306
371
  </SplitItem>
307
372
  </Split>
308
- );
373
+ ) : null;
309
374
 
310
375
  const statusFilters = (
311
376
  <Split hasGutter>
@@ -348,15 +413,18 @@ export const PackagesTab = () => {
348
413
  }
349
414
  ouiaId="host-packages-table"
350
415
  additionalListeners={[hostId, packageStatusSelected,
351
- activeSortDirection, activeSortColumn]}
416
+ activeSortDirection, activeSortColumn, lastCompletedPackageUpgrade,
417
+ lastCompletedPackageRemove, lastCompletedBulkPackageRemove,
418
+ lastCompletedBulkPackageUpgrade, lastCompletedPackageInstall]}
352
419
  fetchItems={fetchItems}
353
420
  bookmarkController="katello_host_installed_packages"
421
+ readOnlyBookmarks={readOnlyBookmarks}
354
422
  autocompleteEndpoint={`/hosts/${hostId}/packages/auto_complete_search`}
355
423
  foremanApiAutoComplete
356
424
  rowsCount={results?.length}
357
425
  variant={TableVariant.compact}
358
426
  {...selectAll}
359
- displaySelectAllCheckbox
427
+ displaySelectAllCheckbox={showActions}
360
428
  >
361
429
  <Thead>
362
430
  <Tr>
@@ -382,6 +450,7 @@ export const PackagesTab = () => {
382
450
  const rowActions = [
383
451
  {
384
452
  title: __('Remove'),
453
+ isDisabled: actionInProgress,
385
454
  onClick: () => handlePackageRemove(packageName),
386
455
  },
387
456
  ];
@@ -391,6 +460,7 @@ export const PackagesTab = () => {
391
460
  {
392
461
  title: __('Upgrade via remote execution'),
393
462
  onClick: () => upgradeViaRemoteExecution(upgradableVersion),
463
+ isDisabled: actionInProgress,
394
464
  },
395
465
  {
396
466
  title: __('Upgrade via customized remote execution'),
@@ -402,14 +472,18 @@ export const PackagesTab = () => {
402
472
 
403
473
  return (
404
474
  <Tr key={`${id}`}>
405
- <Td select={{
406
- disable: false,
407
- isSelected: isSelected(id),
408
- onSelect: (event, selected) => selectOne(selected, id, pkg),
409
- rowIndex,
410
- variant: 'checkbox',
411
- }}
412
- />
475
+ {showActions ? (
476
+ <Td
477
+ select={{
478
+ disable: actionInProgress,
479
+ isSelected: isSelected(id),
480
+ onSelect: (event, selected) => selectOne(selected, id, pkg),
481
+ rowIndex,
482
+ variant: 'checkbox',
483
+ }}
484
+ title={actionInProgress ? disabledReason : undefined}
485
+ />
486
+ ) : <Td>&nbsp;</Td>}
413
487
  <Td>
414
488
  {rpmId
415
489
  ? <a href={urlBuilder(`packages/${rpmId}`, '')}>{packageName}</a>
@@ -419,12 +493,14 @@ export const PackagesTab = () => {
419
493
  <Td><PackagesStatus {...pkg} /></Td>
420
494
  <Td>{installedVersion.replace(`${packageName}-`, '')}</Td>
421
495
  <Td><PackagesLatestVersion {...pkg} /></Td>
422
- <Td
423
- key={`rowActions-${id}`}
424
- actions={{
425
- items: rowActions,
426
- }}
427
- />
496
+ {showActions ? (
497
+ <Td
498
+ key={`rowActions-${id}`}
499
+ actions={{
500
+ items: rowActions,
501
+ }}
502
+ />
503
+ ) : null}
428
504
  </Tr>
429
505
  );
430
506
  })
@@ -440,6 +516,7 @@ export const PackagesTab = () => {
440
516
  key={hostId}
441
517
  hostName={hostname}
442
518
  showKatelloAgent={showKatelloAgent}
519
+ triggerPackageInstall={triggerPackageInstall}
443
520
  />
444
521
  }
445
522
  </div>
@@ -1,4 +1,5 @@
1
- import { API_OPERATIONS, post } from 'foremanReact/redux/API';
1
+ import { API_OPERATIONS, get, post } from 'foremanReact/redux/API';
2
+ import { stopInterval, withInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
2
3
  import { REX_JOB_INVOCATIONS_KEY, REX_FEATURES } from './RemoteExecutionConstants';
3
4
  import { foremanApi } from '../../../../services/api';
4
5
  import { errorToast, renderRexJobStartedToast } from '../../../../scenes/Tasks/helpers';
@@ -7,6 +8,7 @@ import { TRACES_SEARCH_QUERY } from './TracesTab/HostTracesConstants';
7
8
  import { PACKAGE_SEARCH_QUERY } from './PackagesTab/YumInstallablePackagesConstants';
8
9
  import { PACKAGES_SEARCH_QUERY } from './PackagesTab/HostPackagesConstants';
9
10
 
11
+ // PARAM BUILDING
10
12
  const baseParams = ({ feature, hostname, inputs = {} }) => ({
11
13
  job_invocation: {
12
14
  feature,
@@ -83,12 +85,32 @@ const katelloModuleStreamActionsParams = ({ hostname, action, moduleSpec }) =>
83
85
 
84
86
  const showRexToast = response => renderRexJobStartedToast(response.data);
85
87
 
86
- export const installPackage = ({ hostname, packageName }) => post({
88
+ // JOB POLLING
89
+ const pollJobKey = key => `${key}_POLL_REX_JOB`;
90
+
91
+ export const getJob = (key, jobId, handleSuccess) => get({
92
+ key,
93
+ url: foremanApi.getApiUrl(`/job_invocations/${jobId}`),
94
+ handleSuccess,
95
+ });
96
+
97
+ export const startPollingJob = ({
98
+ key, jobId, handleSuccess, interval = 1000,
99
+ }) =>
100
+ withInterval(getJob(pollJobKey(key), jobId, handleSuccess), interval);
101
+
102
+ export const stopPollingJob = ({ key }) => stopInterval(pollJobKey(key));
103
+
104
+ // JOB INVOCATIONS
105
+ export const installPackage = ({ hostname, packageName, handleSuccess }) => post({
87
106
  type: API_OPERATIONS.POST,
88
107
  key: REX_JOB_INVOCATIONS_KEY,
89
108
  url: foremanApi.getApiUrl('/job_invocations'),
90
109
  params: katelloPackageInstallParams({ hostname, packageName }),
91
- handleSuccess: showRexToast,
110
+ handleSuccess: (response) => {
111
+ showRexToast(response);
112
+ if (handleSuccess) handleSuccess(response);
113
+ },
92
114
  errorToast,
93
115
  });
94
116
 
@@ -0,0 +1,85 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { useDispatch } from 'react-redux';
3
+ import { propsToCamelCase } from 'foremanReact/common/helpers';
4
+ import { deleteToast } from 'foremanReact/components/ToastsList/slice';
5
+ import { startPollingJob, stopPollingJob } from './RemoteExecutionActions';
6
+ import { renderRexJobFailedToast, renderRexJobStartedToast, renderRexJobSucceededToast } from '../../../../scenes/Tasks/helpers';
7
+
8
+ export const useRexJobPolling = (initialAction, successAction = null, failureAction = null) => {
9
+ const [isPolling, setIsPolling] = useState(null);
10
+ const [succeeded, setSucceeded] = useState(null);
11
+ const [rexJobId, setRexJobId] = useState(null);
12
+ // A value that only changes when the job succeeds. Pass to TableWrapper as an additionalListener
13
+ // to reload results.
14
+ const [lastCompletedJob, setLastCompletedJob] = useState(null);
15
+ const dispatch = useDispatch();
16
+
17
+ const stopRexJobPolling = useCallback(({ jobId, statusLabel }) => {
18
+ if (statusLabel) setIsPolling(false);
19
+ if (statusLabel === 'succeeded') {
20
+ setSucceeded(true);
21
+ setLastCompletedJob(jobId);
22
+ } else {
23
+ setSucceeded(false);
24
+ }
25
+ dispatch(stopPollingJob({ key: `REX_JOB_POLLING_${jobId}` }));
26
+ dispatch(deleteToast(`REX_TOAST_${jobId}`));
27
+ }, [dispatch]);
28
+
29
+ const tick = (resp) => {
30
+ const { data } = resp;
31
+ const { statusLabel, id, description } = propsToCamelCase(data);
32
+ setRexJobId(id);
33
+ if (statusLabel && statusLabel !== 'running') {
34
+ stopRexJobPolling({ jobId: id, statusLabel });
35
+ if (statusLabel === 'succeeded') {
36
+ renderRexJobSucceededToast({ id, description });
37
+ if (successAction) dispatch(typeof successAction === 'function' ? successAction() : successAction);
38
+ } else {
39
+ renderRexJobFailedToast({ id, description });
40
+ if (failureAction) dispatch(typeof failureAction === 'function' ? failureAction() : failureAction);
41
+ }
42
+ }
43
+ };
44
+ const startRexJobPolling = ({ jobId }) => {
45
+ dispatch(startPollingJob({ key: `REX_JOB_POLLING_${jobId}`, jobId, handleSuccess: tick }));
46
+ };
47
+ const pollingStarted = !!(isPolling || succeeded);
48
+
49
+ const dispatchInitialAction = (...args) => {
50
+ const originalAction = typeof initialAction === 'function' ? initialAction(...args) : initialAction;
51
+ const modifiedAction = {
52
+ ...originalAction,
53
+ payload: {
54
+ ...originalAction.payload,
55
+ handleSuccess: (resp) => {
56
+ const jobId = resp?.data?.id;
57
+ if (!jobId) return;
58
+ renderRexJobStartedToast({ key: `REX_TOAST_${jobId}`, ...resp.data });
59
+ startRexJobPolling({ jobId });
60
+ },
61
+ },
62
+ };
63
+ setIsPolling(true);
64
+ dispatch(modifiedAction);
65
+ };
66
+
67
+ // eslint-disable-next-line arrow-body-style
68
+ useEffect(() => {
69
+ // clean up polling when component unmounts
70
+ return function cleanupRexPolling() {
71
+ if (rexJobId) stopRexJobPolling({ jobId: rexJobId });
72
+ };
73
+ }, [rexJobId, stopRexJobPolling]);
74
+ return ({
75
+ pollingStarted,
76
+ isPolling,
77
+ succeeded,
78
+ rexJobId,
79
+ lastCompletedJob,
80
+ startRexJobPolling,
81
+ triggerJobStart: dispatchInitialAction,
82
+ });
83
+ };
84
+
85
+ export default useRexJobPolling;
@@ -59,7 +59,22 @@ import { selectRepositorySetsStatus } from './RepositorySetsSelectors';
59
59
  import './RepositorySetsTab.scss';
60
60
  import SortableColumnHeaders from '../../../../Table/components/SortableColumnHeaders';
61
61
  import SelectableDropdown from '../../../../SelectableDropdown';
62
+ import { hasRequiredPermissions as can,
63
+ missingRequiredPermissions as cannot,
64
+ userPermissionsFromHostDetails } from '../../hostDetailsHelpers';
62
65
 
66
+ const viewRepoSets = [
67
+ 'view_hosts', 'view_activation_keys', 'view_products',
68
+ ];
69
+ const createBookmarks = ['create_bookmarks'];
70
+
71
+ export const hideRepoSetsTab = ({ hostDetails }) =>
72
+ cannot(
73
+ viewRepoSets,
74
+ userPermissionsFromHostDetails({ hostDetails }),
75
+ );
76
+
77
+ const editHosts = ['edit_hosts'];
63
78
  const getEnabledValue = ({ enabled, enabledContentOverride }) => {
64
79
  const isOverridden = (enabledContentOverride !== null);
65
80
  return {
@@ -96,6 +111,29 @@ EnabledIcon.propTypes = {
96
111
  isOverridden: PropTypes.bool.isRequired,
97
112
  };
98
113
 
114
+ const OsRestrictedIcon = ({ osRestricted }) => (
115
+ <Tooltip
116
+ position="right"
117
+ content={<FormattedMessage
118
+ id="os-restricted-tooltip"
119
+ defaultMessage={__('OS restricted to {osRestricted}. If host OS does not match, the repository will not be available on this host.')}
120
+ values={{ osRestricted }}
121
+ />}
122
+ >
123
+ <Label color="blue" className="os-restricted-label" style={{ marginLeft: '8px' }}>
124
+ {__(osRestricted)}
125
+ </Label>
126
+ </Tooltip>
127
+ );
128
+
129
+ OsRestrictedIcon.propTypes = {
130
+ osRestricted: PropTypes.string,
131
+ };
132
+
133
+ OsRestrictedIcon.defaultProps = {
134
+ osRestricted: null,
135
+ };
136
+
99
137
  const RepositorySetsTab = () => {
100
138
  const hostDetails = useSelector(state => selectAPIResponse(state, 'HOST_DETAILS'));
101
139
  const {
@@ -103,6 +141,10 @@ const RepositorySetsTab = () => {
103
141
  subscription_status: subscriptionStatus,
104
142
  content_facet_attributes: contentFacetAttributes,
105
143
  } = hostDetails;
144
+ const canDoContentOverrides = can(
145
+ editHosts,
146
+ userPermissionsFromHostDetails({ hostDetails }),
147
+ );
106
148
  const STATUS_LABEL = __('Status');
107
149
 
108
150
  const contentFacet = propsToCamelCase(contentFacetAttributes ?? {});
@@ -260,6 +302,9 @@ const RepositorySetsTab = () => {
260
302
  singular: true,
261
303
  });
262
304
 
305
+ const readOnlyBookmarks =
306
+ cannot(createBookmarks, userPermissionsFromHostDetails({ hostDetails }));
307
+
263
308
  const dropdownItems = [
264
309
  <DropdownItem aria-label="bulk_enable" key="bulk_enable" component="button" onClick={enableRepoSets} isDisabled={selectedCount === 0}>
265
310
  {__('Override to enabled')}
@@ -307,7 +352,7 @@ const RepositorySetsTab = () => {
307
352
  </Split>
308
353
  );
309
354
 
310
- const actionButtons = (
355
+ const actionButtons = canDoContentOverrides ? (
311
356
  <Split hasGutter>
312
357
  <SplitItem>
313
358
  <ActionList isIconList>
@@ -322,7 +367,7 @@ const RepositorySetsTab = () => {
322
367
  </ActionList>
323
368
  </SplitItem>
324
369
  </Split>
325
- );
370
+ ) : null;
326
371
 
327
372
  const hostEnvText = 'the "{contentViewName}" content view and "{lifecycleEnvironmentName}" environment';
328
373
 
@@ -400,10 +445,11 @@ const RepositorySetsTab = () => {
400
445
  fetchItems={fetchItems}
401
446
  autocompleteEndpoint="/repository_sets/auto_complete_search"
402
447
  bookmarkController="katello_product_contents" // Katello::ProductContent.table_name
448
+ readOnlyBookmarks={readOnlyBookmarks}
403
449
  rowsCount={results?.length}
404
450
  variant={TableVariant.compact}
405
451
  {...selectAll}
406
- displaySelectAllCheckbox
452
+ displaySelectAllCheckbox={canDoContentOverrides}
407
453
  >
408
454
  <Thead>
409
455
  <Tr>
@@ -426,19 +472,22 @@ const RepositorySetsTab = () => {
426
472
  enabled_content_override: enabledContentOverride,
427
473
  contentUrl: repoPath,
428
474
  product: { name: productName, id: productId },
475
+ osRestricted,
429
476
  } = repoSet;
430
477
  const { isEnabled, isOverridden } =
431
478
  getEnabledValue({ enabled, enabledContentOverride });
432
479
  return (
433
480
  <Tr key={id}>
434
- <Td select={{
435
- disable: !isSelectable(id),
436
- isSelected: isSelected(id),
437
- onSelect: (event, selected) => selectOne(selected, id),
438
- rowIndex,
439
- variant: 'checkbox',
440
- }}
441
- />
481
+ {canDoContentOverrides ? (
482
+ <Td select={{
483
+ disable: !isSelectable(id),
484
+ isSelected: isSelected(id),
485
+ onSelect: (event, selected) => selectOne(selected, id),
486
+ rowIndex,
487
+ variant: 'checkbox',
488
+ }}
489
+ />
490
+ ) : <Td>&nbsp;</Td>}
442
491
  <Td>
443
492
  <span>{repoName}</span>
444
493
  </Td>
@@ -450,29 +499,34 @@ const RepositorySetsTab = () => {
450
499
  </Td>
451
500
  <Td>
452
501
  <span><EnabledIcon key={`enabled-icon-${id}`} {...{ isEnabled, isOverridden }} /></span>
502
+ {osRestricted &&
503
+ <span><OsRestrictedIcon key={`os-restricted-icon-${id}`} {...{ osRestricted }} /></span>
504
+ }
453
505
  </Td>
454
- <Td
455
- key={`rowActions-${id}`}
456
- actions={{
457
- items: [
458
- {
459
- title: __('Override to disabled'),
460
- isDisabled: isOverridden && !isEnabled,
461
- onClick: () => disableRepoSet(id),
462
- },
463
- {
464
- title: __('Override to enabled'),
465
- isDisabled: isOverridden && isEnabled,
466
- onClick: () => enableRepoSet(id),
467
- },
468
- {
469
- title: __('Reset to default'),
470
- isDisabled: !isOverridden,
471
- onClick: () => resetToDefaultRepoSet(id),
472
- },
473
- ],
474
- }}
475
- />
506
+ {canDoContentOverrides ? (
507
+ <Td
508
+ key={`rowActions-${id}`}
509
+ actions={{
510
+ items: [
511
+ {
512
+ title: __('Override to disabled'),
513
+ isDisabled: isOverridden && !isEnabled,
514
+ onClick: () => disableRepoSet(id),
515
+ },
516
+ {
517
+ title: __('Override to enabled'),
518
+ isDisabled: isOverridden && isEnabled,
519
+ onClick: () => enableRepoSet(id),
520
+ },
521
+ {
522
+ title: __('Reset to default'),
523
+ isDisabled: !isOverridden,
524
+ onClick: () => resetToDefaultRepoSet(id),
525
+ },
526
+ ],
527
+ }}
528
+ />
529
+ ) : <Td />}
476
530
  </Tr>
477
531
  );
478
532
  })