diff --git a/app/lib/actions/katello/content_view_environment/reassign_objects.rb b/app/lib/actions/katello/content_view_environment/reassign_objects.rb index b479b939768..05ad9ab1350 100644 --- a/app/lib/actions/katello/content_view_environment/reassign_objects.rb +++ b/app/lib/actions/katello/content_view_environment/reassign_objects.rb @@ -5,7 +5,12 @@ class ReassignObjects < Actions::Base def plan(content_view_environment, options) concurrence do content_view_environment.hosts.each do |host| - plan_action(Host::Reassign, host, options[:system_content_view_id], options[:system_environment_id]) + content_facet_attributes = host.content_facet + if content_facet_attributes.multi_content_view_environment? + content_facet_attributes.content_view_environments -= [content_view_environment] + else + plan_action(Host::Reassign, host, options[:system_content_view_id], options[:system_environment_id]) + end end content_view_environment.activation_keys.each do |key| diff --git a/app/views/katello/api/v2/content_view_versions/base.json.rabl b/app/views/katello/api/v2/content_view_versions/base.json.rabl index 41924a50e12..f0a5a1ed9d6 100644 --- a/app/views/katello/api/v2/content_view_versions/base.json.rabl +++ b/app/views/katello/api/v2/content_view_versions/base.json.rabl @@ -46,6 +46,10 @@ node :permissions do |cvv| } end +child :content_view_environments => :content_view_environments do + attributes :label, :environment_id, :environment_name +end + extends 'katello/api/v2/common/timestamps' version = @object || @resource diff --git a/webpack/scenes/ContentViews/Details/Versions/Delete/RemoveSteps/CVReassignHostsForm.js b/webpack/scenes/ContentViews/Details/Versions/Delete/RemoveSteps/CVReassignHostsForm.js index 91de93e95d4..e13d90bfb38 100644 --- a/webpack/scenes/ContentViews/Details/Versions/Delete/RemoveSteps/CVReassignHostsForm.js +++ b/webpack/scenes/ContentViews/Details/Versions/Delete/RemoveSteps/CVReassignHostsForm.js @@ -1,12 +1,13 @@ import React, { useState, useContext } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import useDeepCompareEffect from 'use-deep-compare-effect'; -import { ExpandableSection, SelectOption } from '@patternfly/react-core'; +import { ExpandableSection, SelectOption, Alert, AlertActionCloseButton } from '@patternfly/react-core'; import { STATUS } from 'foremanReact/constants'; import { translate as __ } from 'foremanReact/common/I18n'; import EnvironmentPaths from '../../../../components/EnvironmentPaths/EnvironmentPaths'; import getContentViews from '../../../../ContentViewsActions'; import { selectContentViewError, selectContentViews, selectContentViewStatus } from '../../../../ContentViewSelectors'; +import { selectCVHosts } from '../../../ContentViewDetailSelectors'; import AffectedHosts from '../affectedHosts'; import DeleteContext from '../DeleteContext'; import ContentViewSelect from '../../../../components/ContentViewSelect/ContentViewSelect'; @@ -25,6 +26,13 @@ const CVReassignHostsForm = () => { cvId, versionEnvironments, selectedEnvSet, selectedEnvForHost, setSelectedEnvForHost, currentStep, selectedCVForHosts, setSelectedCVNameForHosts, setSelectedCVForHosts, } = useContext(DeleteContext); + const [alertDismissed, setAlertDismissed] = useState(false); + const hostResponse = useSelector(selectCVHosts); + + const multiCVWarning = hostResponse?.results?.some?.(host => + host.content_facet_attributes?.multi_content_view_environment); + + const multiCVRemovalInfo = __('This content view version is used in one or more multi-environment hosts. The version will simply be removed from the multi-environment hosts. The content view and lifecycle environment you select here will only apply to single-environment hosts. See hammer activation-key --help for more details.'); // Fetch content views for selected environment to reassign hosts to. useDeepCompareEffect( @@ -103,6 +111,17 @@ const CVReassignHostsForm = () => { return ( <> + {!alertDismissed && multiCVWarning && ( + setAlertDismissed(true)} />} + > +

{multiCVRemovalInfo}

+
+ )} { const [alertDismissed, setAlertDismissed] = useState(false); const { - cvId, versionNameToRemove, versionEnvironments, selectedEnvSet, + cvId, versionIdToRemove, versionNameToRemove, selectedEnvSet, selectedEnvForAK, selectedCVNameForAK, selectedCVNameForHosts, selectedEnvForHost, affectedActivationKeys, affectedHosts, deleteFlow, removeDeletionFlow, } = useContext(DeleteContext); const activationKeysResponse = useSelector(state => selectCVActivationKeys(state, cvId)); const hostsResponse = useSelector(state => selectCVHosts(state, cvId)); - const { results: hostResponse } = hostsResponse; + const { results: hostResponse = [] } = hostsResponse || {}; const { results: akResponse = [] } = activationKeysResponse || {}; - const selectedEnv = versionEnvironments.filter(env => selectedEnvSet.has(env.id)); + const cvVersions = useSelector(state => selectCVVersions(state, cvId)); const versionDeleteInfo = __(`Version ${versionNameToRemove} will be deleted from all environments. It will no longer be available for promotion.`); const removalNotice = __(`Version ${versionNameToRemove} will be removed from the environments listed below, and will remain available for later promotion. ` + 'Changes listed below will be effective after clicking Remove.'); + const matchedCVResults = cvVersions?.results?.filter(cv => cv.id === versionIdToRemove) || []; + const selectedCVE = matchedCVResults + .flatMap(cv => cv.content_view_environments || []) + .filter(env => selectedEnvSet.has(env.environment_id)); + + const multiCVHosts = hostResponse?.filter(host => + host.content_facet_attributes?.multi_content_view_environment) || []; + const multiCVHostsCount = multiCVHosts.length; + + const singleCVHostsCount = (hostResponse?.length || 0) - multiCVHostsCount; + const multiCVActivationKeys = akResponse.filter(key => key.multi_content_view_environment); const multiCVActivationKeysCount = multiCVActivationKeys.length; @@ -43,7 +54,7 @@ const CVVersionRemoveReview = () => {

{versionDeleteInfo}

} {!(deleteFlow || removeDeletionFlow) && } - {(selectedEnv.length !== 0) && + {(selectedCVE?.length !== 0) && <>

{__('Environments')}

@@ -51,18 +62,65 @@ const CVVersionRemoveReview = () => { {__('This version will be removed from:')} - {selectedEnv?.map(({ name, id }) => + {selectedCVE?.map(({ environment_name: name, environment_id: id }) => )} } {affectedHosts && <>

{__('Content hosts')}

- - -

{__(`${pluralize(hostResponse.length, 'host')} will be moved to content view ${selectedCVNameForHosts} in `)}

- -
+ {singleCVHostsCount > 0 && ( + + + + + {selectedEnvForHost[0].name} + + ), + }} + /> + + + )} + {multiCVHostsCount > 0 && ( + + + + + ), + envCV: selectedCVE + ?.map(cve => cve.label) + .join(', '), + hostCount: multiCVHostsCount, + hostSingular: __('multi-environment host'), + hostPlural: __('multi-environment hosts'), + }} + /> + + + )} } {affectedActivationKeys && <> @@ -70,17 +128,52 @@ const CVVersionRemoveReview = () => { {singleCVActivationKeysCount > 0 && ( -

{__(`${pluralize(singleCVActivationKeysCount, 'activation key')} will be moved to content view ${selectedCVNameForAK} in `)}

- + + + {selectedEnvForAK[0].name} + + ), + }} + /> +
)} {multiCVActivationKeysCount > 0 && ( -

- {__(`Content view environment will be removed from ${pluralize(multiCVActivationKeysCount, 'multi-environment activation key')}.`)} -

+ + ), + envCV: selectedCVE + ?.map(cve => cve.label) + .join(', '), + akCount: multiCVActivationKeysCount, + keySingular: __('multi-environment activation key'), + keyPlural: __('multi-environment activation keys'), + }} + />
)} diff --git a/webpack/scenes/ContentViews/Details/Versions/Delete/__tests__/cvVersionRemove.test.js b/webpack/scenes/ContentViews/Details/Versions/Delete/__tests__/cvVersionRemove.test.js index 802ff4d0b76..86e0077f368 100644 --- a/webpack/scenes/ContentViews/Details/Versions/Delete/__tests__/cvVersionRemove.test.js +++ b/webpack/scenes/ContentViews/Details/Versions/Delete/__tests__/cvVersionRemove.test.js @@ -152,7 +152,8 @@ test('Can open Remove wizard and remove version from environment with hosts', as const { - getByText, getAllByText, getByLabelText, getAllByLabelText, queryByText, getByPlaceholderText, + getByText, getAllByText, getByLabelText, getAllByLabelText, queryByText, + getByPlaceholderText, getByTestId, } = renderWithRedux( , renderOptions, @@ -192,7 +193,7 @@ test('Can open Remove wizard and remove version from environment with hosts', as fireEvent.click(getByText('Next')); await patientlyWaitFor(() => { expect(getByText('Review details')).toBeInTheDocument(); - expect(getByText('1 host will be moved to content view cv2 in')).toBeInTheDocument(); + expect(getByTestId('single-cv-hosts-remove')).toBeInTheDocument(); }); fireEvent.click(getAllByText('Remove')[0]); assertNockRequest(scope); @@ -238,7 +239,8 @@ test('Can open Remove wizard and remove version from environment with activation const { - getByText, getAllByText, getByLabelText, getAllByLabelText, queryByText, getByPlaceholderText, + getByText, getAllByText, getByLabelText, getAllByLabelText, queryByText, + getByPlaceholderText, getByTestId, } = renderWithRedux( , renderOptions, @@ -278,7 +280,7 @@ test('Can open Remove wizard and remove version from environment with activation fireEvent.click(getByText('Next')); await patientlyWaitFor(() => { expect(getByText('Review details')).toBeInTheDocument(); - expect(getByText('1 activation key will be moved to content view cv2 in')).toBeInTheDocument(); + expect(getByTestId('single-cv-activation-keys-remove')).toBeInTheDocument(); }); fireEvent.click(getAllByText('Remove')[0]); diff --git a/webpack/scenes/ContentViews/Details/Versions/Delete/affectedHosts.js b/webpack/scenes/ContentViews/Details/Versions/Delete/affectedHosts.js index 4b5377df5d9..4ba6b646f32 100644 --- a/webpack/scenes/ContentViews/Details/Versions/Delete/affectedHosts.js +++ b/webpack/scenes/ContentViews/Details/Versions/Delete/affectedHosts.js @@ -30,6 +30,7 @@ const AffectedHosts = ({ const columnHeaders = [ __('Name'), __('Environment'), + __('Multi Content View Environment'), ]; const emptyContentTitle = __('No matching hosts found.'); const emptyContentBody = __("Given criteria doesn't match any hosts. Try changing your rule."); @@ -63,13 +64,17 @@ const AffectedHosts = ({ {results?.map(({ name, id, - content_facet_attributes: { lifecycle_environment: environment }, + content_facet_attributes: { + lifecycle_environment: environment, + multi_content_view_environment: multiContentViewEnvironment, + }, }) => ( {name} + { multiContentViewEnvironment ? __('Yes') : __('No') } )) }