import React, { useCallback, useState, useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { ActionList, ActionListItem, Dropdown, DropdownItem, DropdownSeparator, DropdownToggle, DropdownToggleAction, KebabToggle, Select, SelectOption, SelectVariant, Skeleton, Split, SplitItem, } from '@patternfly/react-core'; 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 { useSet, useBulkSelect, useUrlParams } from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks'; import { useTableSort } from 'foremanReact/components/PF4/Helpers/useTableSort'; import { urlBuilder } from 'foremanReact/common/urlHelpers'; import SelectableDropdown from '../../../../SelectableDropdown'; import TableWrapper from '../../../../../components/Table/TableWrapper'; import PackagesStatus from '../../../../../components/Packages'; import { getInstalledPackagesWithLatest, } from './HostPackagesActions'; import { selectHostPackagesStatus } from './HostPackagesSelectors'; import { HOST_PACKAGES_KEY, PACKAGES_VERSION_STATUSES, VERSION_STATUSES_TO_PARAM, } from './HostPackagesConstants'; import { removePackage, updatePackage, removePackages, updatePackages, installPackageBySearch } from '../RemoteExecutionActions'; import { katelloPackageUpdateUrl, packagesUpdateUrl } from '../customizedRexUrlHelpers'; import './PackagesTab.scss'; import hostIdNotReady, { getHostDetails } from '../../HostDetailsActions'; import PackageInstallModal from './PackageInstallModal'; import { hasRequiredPermissions as can, missingRequiredPermissions as cannot, 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']; export const hidePackagesTab = ({ hostDetails }) => !(hostDetails?.operatingsystem_family?.match(/RedHat|SUSE/i)); const UpdateVersionsSelect = ({ packageName, rowIndex, selections, upgradableVersions, toggleUpgradableVersionSelect, onUpgradableVersionSelect, upgradableVersionSelectOpen, }) => { if (upgradableVersions === null) { return ; } else if (upgradableVersions.length === 1) { return {upgradableVersions[0]}; } return (
); }; UpdateVersionsSelect.propTypes = { packageName: PropTypes.string.isRequired, rowIndex: PropTypes.number.isRequired, selections: PropTypes.string, upgradableVersions: PropTypes.arrayOf(PropTypes.string), toggleUpgradableVersionSelect: PropTypes.func, onUpgradableVersionSelect: PropTypes.func, upgradableVersionSelectOpen: PropTypes.shape({ has: PropTypes.func, rowIndex: PropTypes.number, }), }; UpdateVersionsSelect.defaultProps = { selections: null, upgradableVersions: null, toggleUpgradableVersionSelect: undefined, onUpgradableVersionSelect: undefined, upgradableVersionSelectOpen: null, }; export const PackagesTab = () => { const hostDetails = useSelector(state => selectAPIResponse(state, 'HOST_DETAILS')); const { id: hostId, name: hostname, } = hostDetails; const { searchParam, status: statusParam } = useUrlParams(); const PACKAGE_STATUS = __('Status'); const [packageStatusSelected, setPackageStatusSelected] = useState(statusParam ?? PACKAGE_STATUS); const activeFilters = [packageStatusSelected]; const defaultFilters = [PACKAGE_STATUS]; const [isBulkActionOpen, setIsBulkActionOpen] = useState(false); const toggleBulkAction = () => setIsBulkActionOpen(prev => !prev); const [isModalOpen, setIsModalOpen] = useState(false); const closeModal = () => setIsModalOpen(false); const showActions = can(invokeRexJobs, userPermissionsFromHostDetails({ hostDetails })); const [isActionOpen, setIsActionOpen] = useState(false); const onActionSelect = () => { setIsActionOpen(false); }; const onActionToggle = () => { setIsActionOpen(prev => !prev); }; const upgradableVersionSelectOpen = useSet([]); const toggleUpgradableVersionSelect = (isOpenState, rowIndex) => { if (isOpenState) { upgradableVersionSelectOpen.add(rowIndex); } else { upgradableVersionSelectOpen.delete(rowIndex); } }; const selectedNewVersions = useRef({}); const onUpgradableVersionSelect = (_event, selected, rowIndex, packageName) => { toggleUpgradableVersionSelect(false, rowIndex); selectedNewVersions.current[packageName] = selected; }; const selectedPackageUpgradeVersion = ({ packageName, upgradableVersions }) => ( selectedNewVersions.current[packageName] || upgradableVersions[0] ); const selectedNVRAVersions = Object.keys(selectedNewVersions.current).map(k => selectedNewVersions.current[k]); const emptyContentTitle = __('This host does not have any packages.'); const emptyContentBody = __('Packages will appear here when available.'); const emptySearchTitle = __('No matching packages found'); const emptySearchBody = __('Try changing your search settings.'); const errorSearchTitle = __('Problem searching packages'); const columnHeaders = [ __('Package'), __('Status'), __('Installed version'), __('Upgradable to'), ]; const COLUMNS_TO_SORT_PARAMS = { [columnHeaders[0]]: 'nvra', [columnHeaders[2]]: 'version', }; const { pfSortParams, apiSortParams, activeSortColumn, activeSortDirection, } = useTableSort({ allColumns: columnHeaders, columnsToSortParams: COLUMNS_TO_SORT_PARAMS, initialSortColumnName: 'Package', }); const fetchItems = useCallback( (params) => { if (!hostId) return hostIdNotReady; const modifiedParams = { ...params }; if (packageStatusSelected !== PACKAGE_STATUS) { modifiedParams.status = VERSION_STATUSES_TO_PARAM[packageStatusSelected]; } return getInstalledPackagesWithLatest(hostId, { ...apiSortParams, ...modifiedParams }); }, [hostId, PACKAGE_STATUS, packageStatusSelected, apiSortParams], ); const response = useSelector(state => selectAPIResponse(state, HOST_PACKAGES_KEY)); const { results, ...metadata } = response; const { error: errorSearchBody } = metadata; const status = useSelector(state => selectHostPackagesStatus(state)); const dispatch = useDispatch(); const { selectOne, isSelected, searchQuery, updateSearchQuery, selectedCount, isSelectable, selectedResults, selectNone, selectAllMode, areAllRowsSelected, fetchBulkParams, ...selectAll } = useBulkSelect({ results, metadata, initialSearchQuery: searchParam || '', }); const packageRemoveAction = packageName => removePackage({ hostname, packageName, }); const { triggerJobStart: triggerPackageRemove, lastCompletedJob: lastCompletedPackageRemove, isPolling: isRemoveInProgress, } = useRexJobPolling(packageRemoveAction); const packageBulkRemoveAction = (bulkParams, packageNames) => removePackages({ hostname, search: bulkParams, descriptionFormat: `Remove package(s) ${packageNames}`, }); const { triggerJobStart: triggerBulkPackageRemove, lastCompletedJob: lastCompletedBulkPackageRemove, isPolling: isBulkRemoveInProgress, } = useRexJobPolling(packageBulkRemoveAction); const packageUpgradeAction = ({ packageName, upgradableVersions }) => updatePackage({ hostname, packageName: selectedPackageUpgradeVersion({ packageName, upgradableVersions }), }); const { triggerJobStart: triggerPackageUpgrade, lastCompletedJob: lastCompletedPackageUpgrade, isPolling: isUpgradeInProgress, } = useRexJobPolling(packageUpgradeAction, getHostDetails({ hostname })); const packageBulkUpgradeAction = (bulkParams, descriptionFormat) => updatePackages({ hostname, search: bulkParams, versions: JSON.stringify(selectedNVRAVersions || []), descriptionFormat, }); const { triggerJobStart: triggerBulkPackageUpgrade, lastCompletedJob: lastCompletedBulkPackageUpgrade, isPolling: isBulkUpgradeInProgress, } = useRexJobPolling(packageBulkUpgradeAction, getHostDetails({ hostname })); const packageInstallAction = (bulkParams, packageNames) => installPackageBySearch({ hostname, search: bulkParams, descriptionFormat: `Install package(s) ${packageNames}` }); const { triggerJobStart: triggerPackageInstall, lastCompletedJob: lastCompletedPackageInstall, 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.'); if (!hostId) return ; const handleInstallPackagesClick = () => { setIsBulkActionOpen(false); setIsModalOpen(true); }; const removePackageViaRemoteExecution = packageName => triggerPackageRemove(packageName); const removePackagesViaRemoteExecution = () => { const selected = fetchBulkParams(); const packageNames = selectedResults.map(({ name }) => name); setIsBulkActionOpen(false); selectNone(); triggerBulkPackageRemove(selected, packageNames.join(', ')); }; const removeBulk = () => removePackagesViaRemoteExecution(); const handlePackageRemove = packageName => removePackageViaRemoteExecution(packageName); const upgradeViaRemoteExecution = ({ packageName, upgradableVersions }) => ( triggerPackageUpgrade({ packageName, upgradableVersions }) ); const upgradeBulkViaRemoteExecution = () => { const selected = fetchBulkParams(); const packageNames = selectedResults.map(({ name }) => name); const allRowsSelected = areAllRowsSelected(); let descriptionFormatText = allRowsSelected ? 'Upgrade all packages' : `Upgrade package(s) ${packageNames.join(', ')}`; if (selectAllMode && !allRowsSelected) descriptionFormatText = 'Upgrade lots of packages'; // we don't know the package names in the exclusion set setIsBulkActionOpen(false); selectNone(); triggerBulkPackageUpgrade(selected, descriptionFormatText); }; const upgradeBulk = () => upgradeBulkViaRemoteExecution(); const upgradeViaCustomizedRemoteExecution = selectedCount ? packagesUpdateUrl({ hostname, search: fetchBulkParams(), versions: JSON.stringify(selectedNVRAVersions), }) : '#'; const disableRemove = () => selectedCount === 0 || selectAllMode; const allUpgradable = () => selectedResults.length > 0 && selectedResults.every(item => item.upgradable_versions?.length > 0); const disableUpgrade = () => selectedCount === 0 || (selectAllMode && packageStatusSelected !== 'Upgradable') || (!selectAllMode && !allUpgradable()); const readOnlyBookmarks = cannot(createBookmarks, userPermissionsFromHostDetails({ hostDetails })); const dropdownUpgradeItems = [ {__('Upgrade via remote execution')} , {__('Upgrade via customized remote execution')} , ]; const kebabItems = [ {__('Remove')} , , {__('Install packages')} , {__('Refresh package applicability')} , ]; const handlePackageStatusSelected = newStatus => setPackageStatusSelected((prevStatus) => { if (prevStatus === newStatus) { return PACKAGE_STATUS; } return newStatus; }); const actionButtons = showActions ? ( {__('Upgrade')} , ]} isDisabled={actionInProgress || disableUpgrade()} splitButtonVariant="action" toggleVariant="primary" onToggle={onActionToggle} /> } isOpen={isActionOpen} dropdownItems={dropdownUpgradeItems} /> } isOpen={isBulkActionOpen} isPlain dropdownItems={kebabItems} ouiaId="bulk_actions_dropdown" /> ) : null; const statusFilters = ( ); const resetFilters = () => setPackageStatusSelected(PACKAGE_STATUS); return (
{results?.map((pkg, rowIndex) => { const { id, name: packageName, nvra: installedVersion, rpm_id: rpmId, upgradable_versions: upgradableVersions, } = pkg; const rowActions = [ { title: __('Remove'), isDisabled: actionInProgress, onClick: () => handlePackageRemove(packageName), }, ]; if (upgradableVersions) { rowActions.unshift( { title: __('Upgrade via remote execution'), onClick: () => upgradeViaRemoteExecution({ packageName, upgradableVersions }), isDisabled: actionInProgress, }, { title: __('Upgrade via customized remote execution'), component: 'a', href: katelloPackageUpdateUrl({ hostname, packageName: selectedPackageUpgradeVersion({ packageName, upgradableVersions, }), }), }, ); } return ( {showActions ? ( selectOne(selected, id, pkg), rowIndex, variant: 'checkbox', }} title={actionInProgress ? disabledReason : undefined} /> ) :  } {rpmId ? {packageName} : packageName } {installedVersion.replace(`${packageName}-`, '')} {showActions ? ( ) : null} ); }) }
{hostId && }
); }; export default PackagesTab;