Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #38116 - Handle version removal for multi-CV hosts #11282

Merged
merged 1 commit into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]
chris1984 marked this conversation as resolved.
Show resolved Hide resolved
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(', '),
chris1984 marked this conversation as resolved.
Show resolved Hide resolved
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();
chris1984 marked this conversation as resolved.
Show resolved Hide resolved
});
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