From 231c41f0c08cf1aebcd0f5eb58c34fe60b8a4a63 Mon Sep 17 00:00:00 2001 From: Partha Aji Date: Tue, 18 Jun 2024 11:07:45 -0400 Subject: [PATCH] Fixes #37596 - Bulk Errata Wizard Co-authored-by: Jeremy Lenz Refs #37596 - PR Suggestions Refs #37596 - updated select all Refs #37596 - added some errata status checks Refs #37596 - Addressed PR Comments to handle errata filtering Refs #37596 - imporved handling of selectAll --- .../concerns/host_managed_extensions.rb | 6 +- .../Tabs/RemoteExecutionActions.js | 8 +- .../Tabs/customizedRexUrlHelpers.js | 3 +- .../extensions/Hosts/ActionsBar/index.js | 11 ++ .../BulkErrataWizard/02_BulkErrataTable.js | 171 ++++++++++++++++++ .../BulkActions/BulkErrataWizard/04_Review.js | 160 ++++++++++++++++ .../BulkErrataWizard/04_ReviewFooter.js | 99 ++++++++++ .../BulkErrataWizard/BulkErrataWizard.js | 158 ++++++++++++++++ .../BulkActions/BulkErrataWizard/index.js | 24 +++ .../BulkPackagesWizard/BulkPackagesWizard.js | 2 +- .../03_HostReview.js => HostReview.js} | 0 webpack/global_index.js | 4 +- 12 files changed, 638 insertions(+), 8 deletions(-) create mode 100644 webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/02_BulkErrataTable.js create mode 100644 webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/04_Review.js create mode 100644 webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/04_ReviewFooter.js create mode 100644 webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/BulkErrataWizard.js create mode 100644 webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/index.js rename webpack/components/extensions/Hosts/BulkActions/{BulkPackagesWizard/03_HostReview.js => HostReview.js} (100%) diff --git a/app/models/katello/concerns/host_managed_extensions.rb b/app/models/katello/concerns/host_managed_extensions.rb index 8acf09c3c3f..94feb847f37 100644 --- a/app/models/katello/concerns/host_managed_extensions.rb +++ b/app/models/katello/concerns/host_managed_extensions.rb @@ -570,7 +570,11 @@ def deb_names_for_job_template(action:, search:) end def advisory_ids(search:) - ::Katello::Erratum.installable_for_hosts([self]).search_for(search).pluck(:errata_id) + ids = ::Katello::Erratum.installable_for_hosts([self]).search_for(search).pluck(:errata_id) + if ids.empty? + fail _("Cannot install errata: No installable errata found for search term '%s'") % search + end + ids end def filtered_entitlement_quantity_consumed(pool) diff --git a/webpack/components/extensions/HostDetails/Tabs/RemoteExecutionActions.js b/webpack/components/extensions/HostDetails/Tabs/RemoteExecutionActions.js index 7a14eca4360..399b7e5fc35 100644 --- a/webpack/components/extensions/HostDetails/Tabs/RemoteExecutionActions.js +++ b/webpack/components/extensions/HostDetails/Tabs/RemoteExecutionActions.js @@ -98,11 +98,13 @@ const katelloTracerResolveParams = ({ hostname, search }) => }); const katelloHostErrataInstallParams = ({ - hostname, search, + hostname, hostSearch, search, descriptionFormat, }) => baseParams({ hostname, + hostSearch, inputs: { [ERRATA_SEARCH_QUERY]: search }, feature: REX_FEATURES.KATELLO_HOST_ERRATA_INSTALL_BY_SEARCH, + descriptionFormat, }); const katelloModuleStreamActionsParams = ({ hostname, action, moduleSpec }) => @@ -218,13 +220,13 @@ export const resolveTraces = ({ hostname, search }) => post({ }); export const installErrata = ({ - hostname, search, + hostname, hostSearch, search, descriptionFormat, }) => post({ type: API_OPERATIONS.POST, key: REX_JOB_INVOCATIONS_KEY, url: foremanApi.getApiUrl('/job_invocations'), params: katelloHostErrataInstallParams({ - hostname, search, + hostname, search, hostSearch, descriptionFormat, }), handleSuccess: showRexToast, errorToast, diff --git a/webpack/components/extensions/HostDetails/Tabs/customizedRexUrlHelpers.js b/webpack/components/extensions/HostDetails/Tabs/customizedRexUrlHelpers.js index ac274271ac7..910de12df8e 100644 --- a/webpack/components/extensions/HostDetails/Tabs/customizedRexUrlHelpers.js +++ b/webpack/components/extensions/HostDetails/Tabs/customizedRexUrlHelpers.js @@ -54,8 +54,9 @@ export const resolveTraceUrl = ({ hostname, search }) => createJob({ inputs: { [TRACES_SEARCH_QUERY]: search }, }); -export const errataInstallUrl = ({ hostname, search }) => createJob({ +export const errataInstallUrl = ({ hostname, search, hostSearch }) => createJob({ hostname, + hostSearch, feature: REX_FEATURES.KATELLO_HOST_ERRATA_INSTALL_BY_SEARCH, inputs: { [ERRATA_SEARCH_QUERY]: search }, }); diff --git a/webpack/components/extensions/Hosts/ActionsBar/index.js b/webpack/components/extensions/Hosts/ActionsBar/index.js index e2288f83d5a..c71ca10c6c9 100644 --- a/webpack/components/extensions/Hosts/ActionsBar/index.js +++ b/webpack/components/extensions/Hosts/ActionsBar/index.js @@ -20,12 +20,14 @@ const HostActionsBar = () => { [ 'bulk-change-cv-modal', 'bulk-packages-wizard', + 'bulk-errata-wizard', ].forEach((id) => { dispatch(addModal({ id })); }); }, [dispatch]); const { setModalOpen: openBulkChangeCVModal } = useForemanModal({ id: 'bulk-change-cv-modal' }); const { setModalOpen: openBulkPackagesWizardModal } = useForemanModal({ id: 'bulk-packages-wizard' }); + const { setModalOpen: openBulkErrataWizardModal } = useForemanModal({ id: 'bulk-errata-wizard' }); const orgId = useForemanOrganization()?.id; @@ -67,6 +69,15 @@ const HostActionsBar = () => { > {__('Manage packages')} + + {__('Manage errata')} + + ); }; diff --git a/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/02_BulkErrataTable.js b/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/02_BulkErrataTable.js new file mode 100644 index 00000000000..f726c4260b0 --- /dev/null +++ b/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/02_BulkErrataTable.js @@ -0,0 +1,171 @@ +import React, { useEffect, useContext } from 'react'; +import { + Alert, + ToolbarItem, + Text, + TextContent, + TextVariants, +} from '@patternfly/react-core'; +import { TableText } from '@patternfly/react-table'; +import TableIndexPage from 'foremanReact/components/PF4/TableIndexPage/TableIndexPage'; +import { translate as __ } from 'foremanReact/common/I18n'; +import SelectAllCheckbox from 'foremanReact/components/PF4/TableIndexPage/Table/SelectAllCheckbox'; +import { STATUS, getControllerSearchProps } from 'foremanReact/constants'; +import { RowSelectTd } from 'foremanReact/components/HostsIndex/RowSelectTd'; +import { getPageStats } from 'foremanReact/components/PF4/TableIndexPage/Table/helpers'; +import { BulkErrataWizardContext, ERRATA_URL } from './BulkErrataWizard'; +import { ErrataType, ErrataSeverity } from '../../../../../components/Errata'; +import katelloApi from '../../../../../services/api'; + +const BulkErrataTable = () => { + const { + setShouldValidateStep2, + errataBulkSelect, + errataResults: results, + errataMetadata: { + total, per_page: perPage, page, subtotal, + }, + errataResponse: response, + } = useContext(BulkErrataWizardContext); + const apiOptions = { key: 'BULK_HOST_ERRATA' }; + const { + status: errataStatus, + } = response; + + + const origSearchProps = getControllerSearchProps('errata', 'searchBar-errata'); + const customSearchProps = { + ...origSearchProps, + autocomplete: { + ...origSearchProps.autocomplete, + url: katelloApi.getApiUrl('/errata/auto_complete_search'), + }, + }; + + const { + selectAll, + selectPage, + selectNone, + selectOne, + isSelected, + selectedCount, + areAllRowsSelected, + areAllRowsOnPageSelected, + updateSearchQuery, + hasInteracted, + } = errataBulkSelect; + + useEffect(() => { + if (results?.length && hasInteracted) { + setShouldValidateStep2(true); + } + }, [setShouldValidateStep2, results, hasInteracted]); + + const pageStats = getPageStats({ total: subtotal, page, perPage }); + const selectionToolbar = ( + + + + ); + + const columns = { + id: { + title: __('Erratum'), + wrapper: ({ id, errata_id: errataId }) => ( + {errataId} + ), + isSorted: true, + weight: 10, + }, + title: { + title: __('Title'), + wrapper: ({ title }) => {title}, + isSorted: true, + weight: 20, + }, + type: { + title: __('Type'), + wrapper: erratum => , + weight: 30, + isSorted: true, + }, + severity: { + title: __('Severity'), + wrapper: erratum => , + weight: 40, + isSorted: true, + }, + affectedHosts: { + title: __('Affected hosts'), + wrapper: ({ affected_hosts_count: affectedHostsCount }) => affectedHostsCount, + weight: 50, + }, + }; + + return ( + <> + + + {__('Apply errata')} + + + {__('Select errata to apply on the selected hosts. Some errata may already be applied on some hosts.')} + + + {selectedCount === 0 && hasInteracted && ( + + )} + { errataStatus === STATUS.RESOLVED && !results?.length && ( + + )} + + + ); +}; + + +export default BulkErrataTable; diff --git a/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/04_Review.js b/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/04_Review.js new file mode 100644 index 00000000000..985556c0735 --- /dev/null +++ b/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/04_Review.js @@ -0,0 +1,160 @@ +import React, { useContext, useState } from 'react'; +import { translate as __ } from 'foremanReact/common/I18n'; +import { FormattedMessage } from 'react-intl'; +import { TreeView, Button, Text, TextContent, TextVariants, Flex, FlexItem, Dropdown, DropdownItem, DropdownToggle } from '@patternfly/react-core'; +import { useWizardContext } from '@patternfly/react-core/next'; +import { CaretDownIcon } from '@patternfly/react-icons'; +import { BulkErrataWizardContext } from './BulkErrataWizard'; + +export const dropdownOptions = [ + __('via remote execution'), + __('via customized remote execution'), +]; + +export const BulkErrataReview = () => { + const { goToStepById } = useWizardContext(); + const { + finishButtonText, + selectedRexOption, + setSelectedRexOption, + finishButtonLoading, + errataBulkSelect: { + selectedResults: selectedErrataResults, + selectedCount: currentSelectedErrataCount, + areAllRowsSelected: allErrataSelected, + searchQuery: errataSearchQuery, + }, + hostsBulkSelect: { + selectedCount: currentSelectedHostsCount, + }, + } = useContext(BulkErrataWizardContext); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const toggleDropdownOpen = () => setIsDropdownOpen(prev => !prev); + const handleSelect = () => { + setIsDropdownOpen(false); + }; + + const dropdownItems = dropdownOptions.map(text => ( + setSelectedRexOption(text)}>{text} + )); + + const treeViewTitle = __('Errata to apply'); + const treeViewData = [ + { + name: treeViewTitle, + id: 'errata-treeview-title', + customBadgeContent: allErrataSelected() && errataSearchQuery === '' ? 'All' : currentSelectedErrataCount, + children: allErrataSelected() ? undefined : + selectedErrataResults.map(({ id, name, errata_id: errataId }) => ({ + name: `${errataId}: ${name}`, + id, + key: id, + })), + action: ( + + ), + actionProps: { + 'aria-label': 'Edit errata list', + }, + }, + ]; + + const hostTreeViewData = [ + { + name: __('Hosts'), + id: 'errata-host-treeview-title', + customBadgeContent: currentSelectedHostsCount, + expandedIcon: null, + action: ( + + ), + actionProps: { + 'aria-label': 'Edit host selection', + }, + }, + ]; + + return ( + <> + + + {__('Review')} + + + {finishButtonText}, + }} + /> + + +
+ + +
+ + + + + + + + + + + {selectedRexOption} + + } + onSelect={handleSelect} + isOpen={isDropdownOpen} + dropdownItems={dropdownItems} + menuAppendTo="parent" + /> + + + + ); +}; + +export default BulkErrataReview; diff --git a/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/04_ReviewFooter.js b/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/04_ReviewFooter.js new file mode 100644 index 00000000000..a70e0c4932b --- /dev/null +++ b/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/04_ReviewFooter.js @@ -0,0 +1,99 @@ +import React, { useContext } from 'react'; +import { translate as __ } from 'foremanReact/common/I18n'; +import { Button } from '@patternfly/react-core'; +import { WizardFooterWrapper, useWizardContext } from '@patternfly/react-core/next'; +import { BulkErrataWizardContext } from './BulkErrataWizard'; +import { dropdownOptions } from './04_Review'; +import { errataInstallUrl } from '../../../HostDetails/Tabs/customizedRexUrlHelpers'; +import { installErrata } from '../../../HostDetails/Tabs/RemoteExecutionActions'; +import { useRexJobPolling } from '../../../HostDetails/Tabs/RemoteExecutionHooks'; + +export const BulkErrataReviewFooter = () => { + const { + finishButtonText, + finishButtonLoading, + setFinishButtonLoading, + selectedRexOption, + closeModal, + errataBulkSelect: { + fetchBulkParams: getErrataBulkParams, + selectedCount: errataSelectedCount, + }, + hostsBulkSelect: { + fetchBulkParams: getHostsBulkParams, + selectedCount: hostsSelectedCount, + }, + } = useContext(BulkErrataWizardContext); + + const { goToStepById } = useWizardContext(); + + let errataBulkParams = ''; + let hostsBulkParams = ''; + if (errataSelectedCount) errataBulkParams = getErrataBulkParams(); + if (hostsSelectedCount) hostsBulkParams = getHostsBulkParams(); + // Customized REX + const [viaRex] = dropdownOptions; + const customizedRexUrl = errataInstallUrl({ + hostSearch: hostsBulkParams, + search: errataBulkParams, + }); + + const errataBulkInstallAction = () => installErrata({ + hostSearch: hostsBulkParams, + search: errataBulkParams, + }); + + const { + triggerJobStart: triggerBulkErrataInstall, + isPolling: isBulkInstallInProgress, + } = useRexJobPolling(errataBulkInstallAction); + + const handleFinishButtonClick = () => { + setFinishButtonLoading(true); + triggerBulkErrataInstall(); + closeModal(); + }; + + const finishButton = (selectedRexOption === viaRex) ? + ( + + ) : ( + + ); + + return ( + + {finishButton} + + + + ); +}; + +export default BulkErrataReviewFooter; diff --git a/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/BulkErrataWizard.js b/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/BulkErrataWizard.js new file mode 100644 index 00000000000..cb9cbbfc8c3 --- /dev/null +++ b/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/BulkErrataWizard.js @@ -0,0 +1,158 @@ +import React, { useState, createContext, useContext } from 'react'; +import { Wizard, WizardHeader, WizardStep } from '@patternfly/react-core/next'; +import { translate as __ } from 'foremanReact/common/I18n'; +import { useForemanModal } from 'foremanReact/components/ForemanModal/ForemanModalHooks'; +import { useBulkSelect } from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks'; +import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar'; +import { useTableIndexAPIResponse } from 'foremanReact/components/PF4/TableIndexPage/Table/TableIndexHooks'; +import { STATUS } from 'foremanReact/constants'; +import { HOSTS_API_PATH } from 'foremanReact/routes/Hosts/constants'; +import HostReview from '../HostReview'; +import { BulkErrataReview, dropdownOptions } from './04_Review'; +import BulkErrataTable from './02_BulkErrataTable'; +import { BulkErrataReviewFooter } from './04_ReviewFooter'; +import katelloApi from '../../../../../services/api'; + +export const BulkErrataWizardContext = createContext({}); + +export const useErrataHostsBulkSelect = ({ initialSelectedHosts, modalIsOpen }) => { + const defaultParams = { search: initialSelectedHosts }; + const apiOptions = { key: 'HOST_REVIEW' }; + const replacementResponse = !modalIsOpen ? { response: {} } : false; + const hostsResponse = useTableIndexAPIResponse({ + replacementResponse, // don't fetch data if modal is closed + apiUrl: `${HOSTS_API_PATH}?per_page=7`, + apiOptions, + defaultParams, + }); + + const { + response: { + results: hostsResults, + ...hostsMetadata + }, + } = hostsResponse; + + const { total, page, subtotal } = hostsMetadata; + + return { + hostsBulkSelect: useBulkSelect({ + results: hostsResults, + metadata: { total, page, selectable: subtotal }, + initialSearchQuery: initialSelectedHosts, + initialSelectAllMode: true, + }), + hostsResponse, + hostsMetadata, + }; +}; + +export const ERRATA_URL = `${katelloApi.getApiUrl('/errata')}?per_page=7&include_permissions=true`; + +const BulkErrataWizard = () => { + const { modalOpen, setModalClosed: closeModal } = useForemanModal({ id: 'bulk-errata-wizard' }); + const { selectedCount: initialSelectedHostCount, fetchBulkParams } + = useContext(ForemanActionsBarContext); + + const [shouldValidateStep2, setShouldValidateStep2] = useState(false); + const [shouldValidateStep3, setShouldValidateStep3] = useState(false); + const [finishButtonLoading, setFinishButtonLoading] = useState(false); + const [selectedRexOption, setSelectedRexOption] = useState(dropdownOptions[0]); + const finishButtonText = __('Apply'); + const replacementResponse = !modalOpen ? { response: {} } : false; + const initialSelectedHosts = fetchBulkParams(); + const apiOptions = { key: 'BULK_HOST_ERRATA' }; + const defaultParams = { included: { search: initialSelectedHosts } }; + const hostsBulkSelect = + useErrataHostsBulkSelect({ initialSelectedHosts, modalIsOpen: modalOpen }); + + const errataResponse = useTableIndexAPIResponse({ + replacementResponse, // don't fetch data if modal is closed + apiUrl: ERRATA_URL, + apiOptions, + defaultParams, + }); + + const { + status: errataStatus, + response: { + results: errataResults, + ...errataMetadata + }, + } = errataResponse; + + const { total, page, subtotal } = errataMetadata; + + const errataBulkSelect = useBulkSelect({ + results: errataResults, + metadata: { total, page, selectable: subtotal }, + idColumn: 'errata_id', + }); + + // eslint-disable-next-line no-restricted-globals + const selectionIsValid = count => count > 0 || isNaN(count); + const errataResultsPresent = errataResults?.length > 0; + const errataSelectionIsValid = + selectionIsValid(errataBulkSelect.selectedCount); + const hostSelectionIsValid = selectionIsValid(hostsBulkSelect.hostsBulkSelect.selectedCount); + let step2Valid = shouldValidateStep2 ? errataSelectionIsValid : true; + if (errataStatus === STATUS.RESOLVED && !errataResultsPresent) step2Valid = false; + const step3Valid = shouldValidateStep3 ? hostSelectionIsValid : true; + const step4Valid = hostSelectionIsValid && errataSelectionIsValid; + + const BulkErrataWizardContextData = { + finishButtonText, + initialSelectedHostCount, + setShouldValidateStep2, + finishButtonLoading, + setFinishButtonLoading, + selectedRexOption, + setSelectedRexOption, + closeModal, + errataBulkSelect, + errataResults, + errataMetadata, + errataResponse, + hostsBulkSelect: hostsBulkSelect.hostsBulkSelect, + }; + return ( + + } + > + + + + + + + } + isDisabled={!step4Valid || !errataResultsPresent} + > + + + + + ); +}; + +export default BulkErrataWizard; diff --git a/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/index.js b/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/index.js new file mode 100644 index 00000000000..779979f9f00 --- /dev/null +++ b/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/index.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { Modal, ModalVariant } from '@patternfly/react-core'; +import { useForemanModal } from 'foremanReact/components/ForemanModal/ForemanModalHooks'; +import BulkErrataWizard from './BulkErrataWizard'; + +const BulkErrataWizardModal = () => { + const { modalOpen: isOpen } = useForemanModal({ id: 'bulk-errata-wizard' }); + + return ( + + + + ); +}; + +export default BulkErrataWizardModal; diff --git a/webpack/components/extensions/Hosts/BulkActions/BulkPackagesWizard/BulkPackagesWizard.js b/webpack/components/extensions/Hosts/BulkActions/BulkPackagesWizard/BulkPackagesWizard.js index 7c5f0d4b855..56ca7d59d45 100644 --- a/webpack/components/extensions/Hosts/BulkActions/BulkPackagesWizard/BulkPackagesWizard.js +++ b/webpack/components/extensions/Hosts/BulkActions/BulkPackagesWizard/BulkPackagesWizard.js @@ -8,7 +8,7 @@ import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/Ac import { useTableIndexAPIResponse } from 'foremanReact/components/PF4/TableIndexPage/Table/TableIndexHooks'; import { STATUS } from 'foremanReact/constants'; import { HOSTS_API_PATH } from 'foremanReact/routes/Hosts/constants'; -import HostReview from './03_HostReview'; +import HostReview from '../HostReview'; import { BulkPackagesReview, dropdownOptions } from './04_Review'; import { BulkPackagesUpgradeTable, BulkPackagesInstallTable } from './02_BulkPackagesTable'; import { BulkPackagesReviewFooter } from './04_ReviewFooter'; diff --git a/webpack/components/extensions/Hosts/BulkActions/BulkPackagesWizard/03_HostReview.js b/webpack/components/extensions/Hosts/BulkActions/HostReview.js similarity index 100% rename from webpack/components/extensions/Hosts/BulkActions/BulkPackagesWizard/03_HostReview.js rename to webpack/components/extensions/Hosts/BulkActions/HostReview.js diff --git a/webpack/global_index.js b/webpack/global_index.js index af1ac7ed478..fc665fd0b8b 100644 --- a/webpack/global_index.js +++ b/webpack/global_index.js @@ -34,8 +34,7 @@ import RecentCommunicationCardExtensions from './components/extensions/HostDetai import SystemPurposeCard from './components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeCard'; import BulkChangeHostCVModal from './components/extensions/Hosts/BulkActions/BulkChangeHostCVModal/index.js'; import BulkPackagesWizardModal from './components/extensions/Hosts/BulkActions/BulkPackagesWizard/index.js'; - - +import BulkErrataWizardModal from './components/extensions/Hosts/BulkActions/BulkErrataWizard/index.js'; import ActivationKeysSearch from './components/ActivationKeysSearch'; registerReducer('katelloExtends', extendReducer); @@ -92,6 +91,7 @@ addGlobalFill('host-tab-details-cards', 'HW properties', , 100); addGlobalFill('_all-hosts-modals', 'BulkPackagesWizardModal', , 200); +addGlobalFill('_all-hosts-modals', 'BulkErrataWizardModal', , 200); registerColumns(hostsIndexColumnExtensions);