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') {