diff --git a/webpack/components/extensions/HostDetails/ActionsBar/index.js b/webpack/components/extensions/HostDetails/ActionsBar/index.js
index 6aca3bcc4a9..a7fc601d766 100644
--- a/webpack/components/extensions/HostDetails/ActionsBar/index.js
+++ b/webpack/components/extensions/HostDetails/ActionsBar/index.js
@@ -1,15 +1,44 @@
-import React from 'react';
-import { useSelector } from 'react-redux';
-import { DropdownItem } from '@patternfly/react-core';
-import { CubeIcon, UndoIcon } from '@patternfly/react-icons';
+import React, { useContext } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import { DropdownItem, DropdownSeparator } from '@patternfly/react-core';
+import { CubeIcon, UndoIcon, RedoIcon } from '@patternfly/react-icons';
import { translate as __ } from 'foremanReact/common/I18n';
+import { HOST_DETAILS_KEY } from 'foremanReact/components/HostDetails/consts';
+import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar';
import { foremanUrl } from 'foremanReact/common/helpers';
import { selectHostDetails } from '../HostDetailsSelectors';
+import { useRexJobPolling } from '../Tabs/RemoteExecutionHooks';
+import { runSubmanRepos } from '../Cards/ContentViewDetailsCard/HostContentViewActions';
+import { hasRequiredPermissions as can, userPermissionsFromHostDetails } from '../hostDetailsHelpers';
const HostActionsBar = () => {
const hostDetails = useSelector(selectHostDetails);
+ const dispatch = useDispatch();
+ const hostname = hostDetails?.name;
+ const { onKebabToggle } = useContext(ForemanActionsBarContext);
+
+ const recalculateApplicability = ['edit_hosts', 'create_job_invocations'];
+ const showRecalculate =
+ can(recalculateApplicability, userPermissionsFromHostDetails({ hostDetails }));
+
+ const refreshHostDetails = () => dispatch({
+ type: 'API_GET',
+ payload: {
+ key: HOST_DETAILS_KEY,
+ url: `/api/hosts/${hostname}`,
+ },
+ });
+
+ const {
+ triggerJobStart: triggerRecalculate,
+ } = useRexJobPolling(() => runSubmanRepos(hostname, refreshHostDetails));
+
+ const handleRefreshApplicabilityClick = () => {
+ triggerRecalculate();
+ onKebabToggle();
+ };
return (
<>
@@ -21,6 +50,18 @@ const HostActionsBar = () => {
>
{__('Legacy content host UI')}
+
+ {showRecalculate && (
+ }
+ >
+ {__('Refresh applicability')}
+
+ )
+ }
{
setToggleGroupState(APPLICABLE);
};
+ const refreshHostDetails = () => dispatch({
+ type: 'API_GET',
+ payload: {
+ key: HOST_DETAILS_KEY,
+ url: `/api/hosts/${hostname}`,
+ },
+ });
+
+ const {
+ triggerJobStart: triggerRecalculate, lastCompletedJob: lastCompletedRecalculate,
+ } = useRexJobPolling(() => runSubmanRepos(hostname, refreshHostDetails));
+
const recalculateErrata = () => {
setIsBulkActionOpen(false);
- dispatch(regenerateApplicability(hostId));
+ triggerRecalculate();
};
let resetFilters = resetFiltersOnly;
@@ -120,7 +134,7 @@ export const ErrataTab = () => {
emptyContentTitle = __('All up to date');
emptyContentBody = __('No action is needed because there are no applicable errata for this host.');
resetFilters = recalculateErrata;
- secondaryActionTextOverride = __('Recalculate');
+ secondaryActionTextOverride = __('Refresh errata applicability');
break;
case 'Needed':
emptyContentTitle = __('No matching errata found');
@@ -286,7 +300,7 @@ export const ErrataTab = () => {
component="button"
onClick={recalculateErrata}
>
- {__('Recalculate')}
+ {__('Refresh errata applicability')}
,
];
@@ -455,7 +469,7 @@ export const ErrataTab = () => {
additionalListeners={[
hostId, toggleGroupState, errataTypeSelected,
errataSeveritySelected, activeSortColumn, activeSortDirection,
- lastCompletedApply, lastCompletedBulkApply]}
+ lastCompletedApply, lastCompletedBulkApply, lastCompletedRecalculate]}
fetchItems={fetchItems}
bookmarkController="katello_errata"
readOnlyBookmarks={readOnlyBookmarks}
diff --git a/webpack/components/extensions/HostDetails/Tabs/ErrataTab/HostErrataActions.js b/webpack/components/extensions/HostDetails/Tabs/ErrataTab/HostErrataActions.js
index 5f82955efb3..76d5a9febf5 100644
--- a/webpack/components/extensions/HostDetails/Tabs/ErrataTab/HostErrataActions.js
+++ b/webpack/components/extensions/HostDetails/Tabs/ErrataTab/HostErrataActions.js
@@ -1,7 +1,6 @@
-import { API_OPERATIONS, get, put } from 'foremanReact/redux/API';
+import { API_OPERATIONS, get } from 'foremanReact/redux/API';
import { foremanApi } from '../../../../../services/api';
-import { HOST_ERRATA_KEY, HOST_ERRATA_APPLICABILITY_KEY } from './HostErrataConstants';
-import { errorToast } from '../../../../../scenes/Tasks/helpers';
+import { HOST_ERRATA_KEY } from './HostErrataConstants';
export const getInstallableErrata = (hostId, params) => get({
type: API_OPERATIONS.GET,
@@ -10,23 +9,5 @@ export const getInstallableErrata = (hostId, params) => get({
params,
});
-export const regenerateApplicability = (hostId, params) => put({
- type: API_OPERATIONS.PUT,
- key: HOST_ERRATA_APPLICABILITY_KEY,
- url: foremanApi.getApiUrl(`/hosts/${hostId}/errata/applicability`),
- // This endpoint doesn't return a task, so can't use renderTaskStartedToast
- // also can't use successToast because we want the type to be 'info'
- handleSuccess: () => {
- window.tfm.toastNotifications.notify({
- message: 'Regenerating errata applicability.',
- type: 'info',
- link: {
- children: 'View related tasks',
- href: '/foreman_tasks/tasks?search=action+~+applicability&page=1',
- },
- });
- },
- errorToast: error => errorToast(error),
- params,
-});
+export default getInstallableErrata;
diff --git a/webpack/components/extensions/HostDetails/Tabs/ErrataTab/HostErrataConstants.js b/webpack/components/extensions/HostDetails/Tabs/ErrataTab/HostErrataConstants.js
index 8bbed5c4beb..17fc090d4f1 100644
--- a/webpack/components/extensions/HostDetails/Tabs/ErrataTab/HostErrataConstants.js
+++ b/webpack/components/extensions/HostDetails/Tabs/ErrataTab/HostErrataConstants.js
@@ -1,5 +1,4 @@
export const HOST_ERRATA_KEY = 'HOST_ERRATUM';
-export const HOST_ERRATA_APPLICABILITY_KEY = 'HOST_ERRATA_APPLICABILITY';
export const HOST_ERRATA_APPLY_KEY = 'HOST_ERRATUM_APPLY';
export const ERRATA_TYPES = {
diff --git a/webpack/components/extensions/HostDetails/Tabs/PackagesTab/PackagesTab.js b/webpack/components/extensions/HostDetails/Tabs/PackagesTab/PackagesTab.js
index 99fa8c9776b..1baedc0d0f1 100644
--- a/webpack/components/extensions/HostDetails/Tabs/PackagesTab/PackagesTab.js
+++ b/webpack/components/extensions/HostDetails/Tabs/PackagesTab/PackagesTab.js
@@ -1,5 +1,5 @@
import React, { useCallback, useState, useRef } from 'react';
-import { useSelector } from 'react-redux';
+import { useSelector, useDispatch } from 'react-redux';
import {
ActionList,
ActionListItem,
@@ -19,6 +19,7 @@ import {
import { TableVariant, Thead, Tbody, Tr, Th, Td, TableText } from '@patternfly/react-table';
import PropTypes from 'prop-types';
import { translate as __ } from 'foremanReact/common/I18n';
+import { HOST_DETAILS_KEY } from 'foremanReact/components/HostDetails/consts';
import { selectAPIResponse } from 'foremanReact/redux/API/APISelectors';
import { urlBuilder } from 'foremanReact/common/urlHelpers';
@@ -43,6 +44,7 @@ import { hasRequiredPermissions as can,
userPermissionsFromHostDetails } from '../../hostDetailsHelpers';
import SortableColumnHeaders from '../../../../Table/components/SortableColumnHeaders';
import { useRexJobPolling } from '../RemoteExecutionHooks';
+import { runSubmanRepos } from '../../Cards/ContentViewDetailsCard/HostContentViewActions';
const invokeRexJobs = ['create_job_invocations'];
const createBookmarks = ['create_bookmarks'];
@@ -199,6 +201,7 @@ export const PackagesTab = () => {
const { results, ...metadata } = response;
const { error: errorSearchBody } = metadata;
const status = useSelector(state => selectHostPackagesStatus(state));
+ const dispatch = useDispatch();
const {
selectOne,
isSelected,
@@ -271,6 +274,23 @@ export const PackagesTab = () => {
isPolling: isInstallInProgress,
} = useRexJobPolling(packageInstallAction, getHostDetails({ hostname }));
+ const refreshHostDetails = () => dispatch({
+ type: 'API_GET',
+ payload: {
+ key: HOST_DETAILS_KEY,
+ url: `/api/hosts/${hostname}`,
+ },
+ });
+
+ const {
+ triggerJobStart: triggerRecalculate, lastCompletedJob: lastCompletedRecalculate,
+ } = useRexJobPolling(() => runSubmanRepos(hostname, refreshHostDetails));
+
+ const handleRefreshApplicabilityClick = () => {
+ setIsBulkActionOpen(false);
+ triggerRecalculate();
+ };
+
const actionInProgress = (isRemoveInProgress || isUpgradeInProgress
|| isBulkRemoveInProgress || isBulkUpgradeInProgress || isInstallInProgress);
const disabledReason = __('A remote execution job is in progress.');
@@ -347,7 +367,7 @@ export const PackagesTab = () => {
,
];
- const dropdownRemoveItems = [
+ const kebabItems = [
{
>
{__('Install packages')}
,
+
+ {__('Refresh package applicability')}
+ ,
];
const handlePackageStatusSelected = newStatus => setPackageStatusSelected((prevStatus) => {
@@ -408,7 +437,7 @@ export const PackagesTab = () => {
toggle={}
isOpen={isBulkActionOpen}
isPlain
- dropdownItems={dropdownRemoveItems}
+ dropdownItems={kebabItems}
ouiaId="bulk_actions_dropdown"
/>
@@ -463,7 +492,7 @@ export const PackagesTab = () => {
additionalListeners={[hostId, packageStatusSelected,
activeSortDirection, activeSortColumn, lastCompletedPackageUpgrade,
lastCompletedPackageRemove, lastCompletedBulkPackageRemove,
- lastCompletedBulkPackageUpgrade, lastCompletedPackageInstall]}
+ lastCompletedBulkPackageUpgrade, lastCompletedPackageInstall, lastCompletedRecalculate]}
fetchItems={fetchItems}
bookmarkController="katello_host_installed_packages"
readOnlyBookmarks={readOnlyBookmarks}
diff --git a/webpack/components/extensions/HostDetails/Tabs/RemoteExecutionHooks.js b/webpack/components/extensions/HostDetails/Tabs/RemoteExecutionHooks.js
index 507a8bafc29..d8040a482d3 100644
--- a/webpack/components/extensions/HostDetails/Tabs/RemoteExecutionHooks.js
+++ b/webpack/components/extensions/HostDetails/Tabs/RemoteExecutionHooks.js
@@ -29,7 +29,7 @@ export const useRexJobPolling = (initialAction, successAction = null, failureAct
const tick = (resp) => {
const { data } = resp;
const { statusLabel, id, description } = propsToCamelCase(data);
- setRexJobId(id);
+ if (!id) setRexJobId(id);
if (statusLabel && statusLabel !== 'running') {
stopRexJobPolling({ jobId: id, statusLabel });
if (statusLabel === 'succeeded') {