Skip to content

Commit

Permalink
Fixes #38116 - Handle version removal for multi-CV hosts
Browse files Browse the repository at this point in the history
  • Loading branch information
pavanshekar authored and chris1984 committed Feb 4, 2025
1 parent 002a543 commit 9c9d486
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
4 changes: 4 additions & 0 deletions app/views/katello/api/v2/content_view_versions/base.json.rabl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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(
Expand Down Expand Up @@ -103,6 +111,17 @@ const CVReassignHostsForm = () => {

return (
<>
{!alertDismissed && multiCVWarning && (
<Alert
ouiaId="multi-cv-warning-alert"
variant="warning"
isInline
title={__('Warning')}
actionClose={<AlertActionCloseButton onClose={() => setAlertDismissed(true)} />}
>
<p>{multiCVRemovalInfo}</p>
</Alert>
)}
<EnvironmentPaths
userCheckedItems={selectedEnvForHost}
setUserCheckedItems={setSelectedEnvForHost}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,39 @@ import React, { useContext, useState } from 'react';
import { useSelector } from 'react-redux';
import { Alert, Flex, FlexItem, Label, AlertActionCloseButton } from '@patternfly/react-core';
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
import { FormattedMessage } from 'react-intl';
import { translate as __ } from 'foremanReact/common/I18n';
import { selectCVActivationKeys, selectCVHosts } from '../../../ContentViewDetailSelectors';
import { selectCVActivationKeys, selectCVHosts, selectCVVersions } from '../../../ContentViewDetailSelectors';
import DeleteContext from '../DeleteContext';
import { pluralize } from '../../../../../../utils/helpers';
import WizardHeader from '../../../../components/WizardHeader';

const CVVersionRemoveReview = () => {
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;

Expand All @@ -43,44 +54,126 @@ const CVVersionRemoveReview = () => {
<p style={{ marginBottom: '0.5em' }}>{versionDeleteInfo}</p>
</Alert>}
{!(deleteFlow || removeDeletionFlow) && <WizardHeader description={removalNotice} />}
{(selectedEnv.length !== 0) &&
{(selectedCVE?.length !== 0) &&
<>
<h3>{__('Environments')}</h3>
<Flex>
<FlexItem><ExclamationTriangleIcon /></FlexItem>
<FlexItem style={{ marginBottom: '0.5em' }}>{__('This version will be removed from:')}</FlexItem>
</Flex>
<Flex>
{selectedEnv?.map(({ name, id }) =>
{selectedCVE?.map(({ environment_name: name, environment_id: id }) =>
<FlexItem key={name}><Label isTruncated color="purple" href={`/lifecycle_environments/${id}`}>{name}</Label></FlexItem>)}
</Flex>
</>}
{affectedHosts &&
<>
<h3>{__('Content hosts')}</h3>
<Flex>
<FlexItem><ExclamationTriangleIcon /></FlexItem>
<FlexItem><p>{__(`${pluralize(hostResponse.length, 'host')} will be moved to content view ${selectedCVNameForHosts} in `)}</p></FlexItem>
<FlexItem><Label isTruncated color="purple" href={`/lifecycle_environments/${selectedEnvForHost[0].id}`}>{selectedEnvForHost[0].name}</Label></FlexItem>
</Flex>
{singleCVHostsCount > 0 && (
<Flex>
<FlexItem><ExclamationTriangleIcon /></FlexItem>
<FlexItem data-testid="single-cv-hosts-remove">
<FormattedMessage
id="single-cv-hosts-remove"
defaultMessage="{count, plural, one {# {singular}} other {# {plural}}} will be moved to content view {cvName} in {envName}."
values={{
count: singleCVHostsCount,
singular: __('host'),
plural: __('hosts'),
cvName: selectedCVNameForHosts,
envName: selectedEnvForHost[0] && (
<Label isTruncated color="purple" href={`/lifecycle_environments/${selectedEnvForHost[0].id}`}>
{selectedEnvForHost[0].name}
</Label>
),
}}
/>
</FlexItem>
</Flex>
)}
{multiCVHostsCount > 0 && (
<Flex>
<FlexItem><ExclamationTriangleIcon /></FlexItem>
<FlexItem>
<FormattedMessage
id="multi-cv-hosts-remove"
defaultMessage="{envSingularOrPlural} {envCV} will be removed from {hostCount, plural, one {# {hostSingular}} other {# {hostPlural}}}."
values={{
envSingularOrPlural: (
<FormattedMessage
id="environment.plural"
defaultMessage="{count, plural, one {{envSingular}} other {{envPlural}}}"
values={{
count: selectedCVE?.length,
envSingular: __('Content view environment'),
envPlural: __('Content view environments'),
}}
/>
),
envCV: selectedCVE
?.map(cve => cve.label)
.join(', '),
hostCount: multiCVHostsCount,
hostSingular: __('multi-environment host'),
hostPlural: __('multi-environment hosts'),
}}
/>
</FlexItem>
</Flex>
)}
</>}
{affectedActivationKeys &&
<>
<h3>{__('Activation keys')}</h3>
{singleCVActivationKeysCount > 0 && (
<Flex>
<FlexItem><ExclamationTriangleIcon /></FlexItem>
<FlexItem><p>{__(`${pluralize(singleCVActivationKeysCount, 'activation key')} will be moved to content view ${selectedCVNameForAK} in `)}</p></FlexItem>
<FlexItem><Label isTruncated color="purple" href={`/lifecycle_environments/${selectedEnvForAK[0].id}`}>{selectedEnvForAK[0].name}</Label></FlexItem>
<FlexItem data-testid="single-cv-activation-keys-remove">
<FormattedMessage
id="single-cv-activation-keys-remove"
defaultMessage="{count, plural, one {# {singular}} other {# {plural}}} will be moved to content view {cvName} in {envName}."
values={{
count: singleCVActivationKeysCount,
singular: __('activation key'),
plural: __('activation keys'),
cvName: selectedCVNameForAK,
envName: selectedEnvForAK[0] && (
<Label isTruncated color="purple" href={`/lifecycle_environments/${selectedEnvForAK[0].id}`}>
{selectedEnvForAK[0].name}
</Label>
),
}}
/>
</FlexItem>
</Flex>
)}
{multiCVActivationKeysCount > 0 && (
<Flex>
<FlexItem><ExclamationTriangleIcon /></FlexItem>
<FlexItem>
<p>
{__(`Content view environment will be removed from ${pluralize(multiCVActivationKeysCount, 'multi-environment activation key')}.`)}
</p>
<FormattedMessage
id="multi-cv-activation-keys-remove"
defaultMessage="{envSingularOrPlural} {envCV} will be removed from {akCount, plural, one {# {keySingular}} other {# {keyPlural}}}."
values={{
envSingularOrPlural: (
<FormattedMessage
id="environment.plural"
defaultMessage="{count, plural, one {{envSingular}} other {{envPlural}}}"
values={{
count: selectedCVE?.length,
envSingular: __('Content view environment'),
envPlural: __('Content view environments'),
}}
/>
),
envCV: selectedCVE
?.map(cve => cve.label)
.join(', '),
akCount: multiCVActivationKeysCount,
keySingular: __('multi-environment activation key'),
keyPlural: __('multi-environment activation keys'),
}}
/>
</FlexItem>
</Flex>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ContentViewVersions cvId={2} details={cvDetailData} />,
renderOptions,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
<ContentViewVersions cvId={2} details={cvDetailData} />,
renderOptions,
Expand Down Expand Up @@ -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]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down Expand Up @@ -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,
},
}) => (
<Tr ouiaId={id} key={id}>
<Td>
<a rel="noreferrer" target="_blank" href={urlBuilder(`new/hosts/${id}`, '')}>{name}</a>
</Td>
<Td><EnvironmentLabels environments={environment} /></Td>
<Td>{ multiContentViewEnvironment ? __('Yes') : __('No') }</Td>
</Tr>
))
}
Expand Down

0 comments on commit 9c9d486

Please sign in to comment.