import React, { useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; import { FormattedMessage } from 'react-intl'; import { translate as __ } from 'foremanReact/common/I18n'; import { Skeleton, Label, Button, Split, SplitItem, Checkbox, Dropdown, Text, TextVariants, DropdownItem, KebabToggle, DropdownPosition, DropdownSeparator, Modal, ModalVariant } from '@patternfly/react-core'; import PropTypes from 'prop-types'; import { upperFirst, lowerCase } from 'lodash'; import { TableText, TableVariant, Thead, Tbody, Tr, Td } from '@patternfly/react-table'; import { LongArrowAltUpIcon, CheckIcon, } from '@patternfly/react-icons'; import { selectModuleStreamStatus, selectModuleStream } from './ModuleStreamsSelectors'; import { useBulkSelect, useTableSort, useUrlParams } from '../../../../Table/TableHooks'; import { getHostModuleStreams } from './ModuleStreamsActions'; import InactiveText from '../../../../../scenes/ContentViews/components/InactiveText'; import TableWrapper from '../../../../../components/Table/TableWrapper'; import hostIdNotReady from '../../HostDetailsActions'; import { selectHostDetails } from '../../HostDetailsSelectors'; import SortableColumnHeaders from '../../../../Table/components/SortableColumnHeaders'; import SelectableDropdown from '../../../../SelectableDropdown/SelectableDropdown'; import { HOST_MODULE_STREAM_STATUSES, INSTALL_STATUS_PARAM_TO_FRIENDLY_NAME, INSTALLED_STATE, STATUS_PARAM_TO_FRIENDLY_NAME, MODULE_STREAMS_KEY, } from './ModuleStreamsConstants'; import { moduleStreamAction } from '../RemoteExecutionActions'; import { katelloModuleStreamActionUrl } from '../customizedRexUrlHelpers'; import { useRexJobPolling } from '../RemoteExecutionHooks'; import { hasRequiredPermissions as can, missingRequiredPermissions as cannot, userPermissionsFromHostDetails, } from '../../hostDetailsHelpers'; const moduleStreamSupported = ({ os, version }) => os.match(/RedHat|RHEL|CentOS|Rocky|AlmaLinux|OracleLinux/i) && Number(version) > 7; export const hideModuleStreamsTab = ({ hostDetails }) => { const osMatch = hostDetails?.operatingsystem_name?.match(/(\D+) (\d+)/); if (!osMatch) return false; const [, os, version] = osMatch; return !(osMatch && moduleStreamSupported({ os, version })); }; const EnabledIcon = ({ streamText, streamInstallStatus, upgradable }) => { switch (true) { case (streamInstallStatus?.length > 0 && streamText === 'disabled'): return {INSTALLED_STATE.INSTALLED}; case (streamInstallStatus?.length > 0 && streamText === 'enabled' && upgradable !== true): return <> {INSTALLED_STATE.UPTODATE}; case (streamInstallStatus?.length > 0 && streamText === 'enabled' && upgradable): return <> {INSTALLED_STATE.UPGRADEABLE}; default: return ; } }; EnabledIcon.propTypes = { streamText: PropTypes.string.isRequired, streamInstallStatus: PropTypes.arrayOf(PropTypes.string).isRequired, upgradable: PropTypes.bool.isRequired, }; const stateText = (moduleStreamStatus) => { let streamText = moduleStreamStatus?.charAt(0)?.toUpperCase() + moduleStreamStatus?.slice(1); streamText = streamText?.replace('Unknown', 'Default'); return streamText; }; const StreamState = ({ moduleStreamStatus }) => { const streamText = stateText(moduleStreamStatus); switch (true) { case (streamText === 'Default'): return ; case (streamText === 'Disabled'): return ; case (streamText === 'Enabled'): return ; default: return null; } }; StreamState.propTypes = { moduleStreamStatus: PropTypes.string.isRequired, }; const HostInstalledProfiles = ({ moduleStreamStatus, installedProfiles }) => { let installedProfile; if (installedProfiles?.length > 0) { installedProfile = installedProfiles?.map(profile => upperFirst(profile)).join(', '); } else { installedProfile = 'No profile installed'; } const disabledText = moduleStreamStatus === 'disabled' || moduleStreamStatus === 'unknown'; return disabledText ? : installedProfile; }; HostInstalledProfiles.propTypes = { moduleStreamStatus: PropTypes.string.isRequired, installedProfiles: PropTypes.arrayOf(PropTypes.string).isRequired, }; const ModuleActionConfirmationModal = ({ hostname, action, moduleSpec, actionModalOpen, setActionModalOpen, triggerModuleStreamAction, }) => { let title; let body; let confirmText; switch (action) { case 'disable': confirmText = __('Disable'); title = __('Disable module stream'); body = ( ); break; case 'reset': confirmText = __('Reset'); title = __('Reset module stream'); body = ( ); break; case 'remove': confirmText = __('Remove'); title = __('Remove module stream'); body = __(`Installed module profiles will be removed. Additionally, all packages whose names are provided by specific modules will be removed. Packages required by other installed modules profiles and packages whose names are also provided by other modules are not removed.`); break; default: // No action selected. Should not be here! setActionModalOpen(false); } return ( setActionModalOpen(false)} actions={[ , , ]} > {body} ); }; ModuleActionConfirmationModal.propTypes = { hostname: PropTypes.string.isRequired, action: PropTypes.string.isRequired, moduleSpec: PropTypes.string.isRequired, actionModalOpen: PropTypes.bool.isRequired, setActionModalOpen: PropTypes.func.isRequired, triggerModuleStreamAction: PropTypes.func.isRequired, }; const invokeRexJobs = ['create_job_invocations']; const createBookmarks = ['create_bookmarks']; export const ModuleStreamsTab = () => { const hostDetails = useSelector(selectHostDetails); const { id: hostId, name: hostname } = hostDetails; const showActions = can(invokeRexJobs, userPermissionsFromHostDetails({ hostDetails })); const [useCustomizedRex, setUseCustomizedRex] = useState(''); const [dropdownOpen, setDropdownOpen] = useState(''); const [actionModalOpen, setActionModalOpen] = useState(false); const [actionableModuleSpec, setActionableModuleSpec] = useState(null); const [hostModuleStreamAction, setHostModuleStreamAction] = useState(null); const emptyContentTitle = __('This host does not have any Module streams.'); const emptyContentBody = __('Module streams will appear here after enabling Red Hat repositories or creating custom products.'); const emptySearchTitle = __('Your search returned no matching Module streams.'); const emptySearchBody = __('Try changing your search criteria.'); const showPrimaryAction = true; const showSecondaryAction = true; const primaryActionTitle = __('Enable Red Hat repositories'); const secondaryActionTitle = __('Create a custom product'); const primaryActionLink = '/redhat_repositories'; const secondaryActionLink = '/products/new'; const errorSearchTitle = __('Problem searching module streams'); const { status: initialStatus, installStatus: initialInstallStatusSelected, searchParam, } = useUrlParams(); const MODULE_STREAM_STATUS = __('Status'); const MODULE_STREAM_INSTALLATION_STATUS = __('Installation status'); const [statusSelected, setStatusSelected] = useState(STATUS_PARAM_TO_FRIENDLY_NAME[initialStatus] ?? MODULE_STREAM_STATUS); const [installStatusSelected, setInstallStatusSelected] = useState(INSTALL_STATUS_PARAM_TO_FRIENDLY_NAME[initialInstallStatusSelected] ?? MODULE_STREAM_INSTALLATION_STATUS); const columnHeaders = [ __('Name'), __('State'), __('Stream'), __('Installation status'), __('Installed profile'), ]; const COLUMNS_TO_SORT_PARAMS = { [columnHeaders[0]]: 'name', [columnHeaders[1]]: 'status', [columnHeaders[3]]: 'installed_profiles', }; const { pfSortParams, apiSortParams, activeSortColumn, activeSortDirection, } = useTableSort({ allColumns: columnHeaders, columnsToSortParams: COLUMNS_TO_SORT_PARAMS, initialSortColumnName: 'Name', }); const { triggerJobStart: triggerModuleStreamAction, lastCompletedJob: tableJobCompleted, isPolling: isModuleStreamActionInProgress, } = useRexJobPolling(moduleStreamAction); const { triggerJobStart: triggerConfirmModalAction, lastCompletedJob: confirmModalJobCompleted, isPolling: isConfirmModalActionInProgress, } = useRexJobPolling(moduleStreamAction); const actionInProgress = (isModuleStreamActionInProgress || isConfirmModalActionInProgress); const fetchItems = useCallback( (params) => { let extraParams = params; if (!hostId) return hostIdNotReady; if (statusSelected !== MODULE_STREAM_STATUS) { extraParams = { ...extraParams, status: lowerCase(statusSelected) }; } if (installStatusSelected !== MODULE_STREAM_INSTALLATION_STATUS) { extraParams = { ...extraParams, install_status: lowerCase(installStatusSelected) }; } return getHostModuleStreams( hostId, { ...apiSortParams, ...extraParams }, ); }, [hostId, statusSelected, installStatusSelected, MODULE_STREAM_STATUS, MODULE_STREAM_INSTALLATION_STATUS, apiSortParams], ); const handleModuleStreamStatusSelected = newStatus => setStatusSelected((prevStatus) => { if (prevStatus === newStatus) { return MODULE_STREAM_STATUS; } return newStatus; }); const handleModuleStreamInstallationStatusSelected = newInstallationStatus => setInstallStatusSelected((prevInstallationStatus) => { if (prevInstallationStatus === newInstallationStatus) { return MODULE_STREAM_INSTALLATION_STATUS; } return newInstallationStatus; }); const customizedActionURL = (action, moduleSpec) => katelloModuleStreamActionUrl({ hostname, action, moduleSpec }); const response = useSelector(selectModuleStream); const { results, ...metadata } = response; const { error: errorSearchBody } = metadata; const status = useSelector(state => selectModuleStreamStatus(state)); /* eslint-disable no-unused-vars */ const { selectOne, isSelected, searchQuery, selectedCount, isSelectable, updateSearchQuery, selectNone, fetchBulkParams, ...selectAll } = useBulkSelect({ results, metadata, isSelectable: _result => false, initialSearchQuery: searchParam || '', }); /* eslint-enable no-unused-vars */ const hideBookmarkActions = cannot(createBookmarks, userPermissionsFromHostDetails({ hostDetails })); if (!hostId) return ; const activeFilters = [statusSelected, installStatusSelected]; const defaultFilters = [MODULE_STREAM_STATUS, MODULE_STREAM_INSTALLATION_STATUS]; const resetFilters = () => { setStatusSelected(MODULE_STREAM_STATUS); setInstallStatusSelected(MODULE_STREAM_INSTALLATION_STATUS); }; return (
} > {results?.map(({ id, status: moduleStreamStatus, name, stream, installed_profiles: installedProfiles, upgradable, install_status: installedStatus, module_spec: moduleSpec, }, index) => { /* eslint-disable react/no-array-index-key */ const dropdownItems = [ (checked ? setUseCustomizedRex(id) : setUseCustomizedRex(''))} /> , , ]; if (id === useCustomizedRex) { dropdownItems.push( {__('Enable')} , {__('Disable')} , {__('Install')} , {__('Update')} , {__('Reset')} , {__('Remove')} , ); } else { dropdownItems.push( { triggerModuleStreamAction({ hostname, action: 'enable', moduleSpec }); setUseCustomizedRex(''); setDropdownOpen(''); }} isDisabled={actionInProgress || stateText(moduleStreamStatus) === HOST_MODULE_STREAM_STATUSES.ENABLED} > {__('Enable')} , { setActionableModuleSpec(moduleSpec); setHostModuleStreamAction('disable'); setActionModalOpen(true); setUseCustomizedRex(''); setDropdownOpen(''); }} isDisabled={actionInProgress || stateText(moduleStreamStatus) !== HOST_MODULE_STREAM_STATUSES.ENABLED} > {__('Disable')} , { triggerModuleStreamAction({ hostname, action: 'install', moduleSpec }); setUseCustomizedRex(''); setDropdownOpen(''); }} isDisabled={(actionInProgress || upgradable || (installedStatus !== INSTALLED_STATE.NOTINSTALLED) || !(stateText(moduleStreamStatus) === HOST_MODULE_STREAM_STATUSES.ENABLED || stateText(moduleStreamStatus) === HOST_MODULE_STREAM_STATUSES.DISABLED) )} > {__('Install')} , { triggerModuleStreamAction({ hostname, action: 'update', moduleSpec }); setUseCustomizedRex(''); setDropdownOpen(''); }} isDisabled={actionInProgress || !upgradable} > {__('Update')} , { setActionableModuleSpec(moduleSpec); setHostModuleStreamAction('reset'); setActionModalOpen(true); setUseCustomizedRex(''); setDropdownOpen(''); }} isDisabled={actionInProgress} > {__('Reset')} , { setActionableModuleSpec(moduleSpec); setHostModuleStreamAction('remove'); setActionModalOpen(true); setUseCustomizedRex(''); setDropdownOpen(''); }} isDisabled={actionInProgress} > {__('Remove')} , ); } return ( {name} {stream} {showActions && ( ((dropdownOpen === id) ? setDropdownOpen('') : setDropdownOpen(id))} id={`toggle-dropdown-${id}`} /> } isOpen={id === dropdownOpen} dropdownItems={dropdownItems} /> )} ); }) } {actionModalOpen && }
); }; export default ModuleStreamsTab;