Wizard: Fix blueprint name update on Architecture/Distribution changes

This commit resolves an issue where the blueprint name did not update when the user changed the Architecture or Distribution.
Additionally, it sets an initial value for blueprintName in the WizardSlice.
This commit is contained in:
Michal Gold 2025-02-25 12:22:08 +02:00 committed by Lucas Garfield
parent b2255de04e
commit 978237bf84
8 changed files with 96 additions and 64 deletions

View file

@ -16,8 +16,8 @@ import {
changeBlueprintName,
selectBlueprintDescription,
selectBlueprintName,
setIsCustomName,
} from '../../../../store/wizardSlice';
import { useGenerateDefaultName } from '../../utilities/useGenerateDefaultName';
import { useDetailsValidation } from '../../utilities/useValidation';
import { ValidatedInputAndTextArea } from '../../ValidatedInput';
@ -26,13 +26,12 @@ const DetailsStep = () => {
const blueprintName = useAppSelector(selectBlueprintName);
const blueprintDescription = useAppSelector(selectBlueprintDescription);
useGenerateDefaultName();
const handleNameChange = (
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
name: string
) => {
dispatch(changeBlueprintName(name));
dispatch(setIsCustomName());
};
const handleDescriptionChange = (

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import { Text, Form, Title } from '@patternfly/react-core';
@ -8,12 +8,30 @@ import ReleaseLifecycle from './ReleaseLifecycle';
import ReleaseSelect from './ReleaseSelect';
import TargetEnvironment from './TargetEnvironment';
import { useAppSelector } from '../../../../store/hooks';
import { selectDistribution } from '../../../../store/wizardSlice';
import { useAppDispatch, useAppSelector } from '../../../../store/hooks';
import {
changeBlueprintName,
selectArchitecture,
selectBlueprintName,
selectDistribution,
selectIsCustomName,
} from '../../../../store/wizardSlice';
import DocumentationButton from '../../../sharedComponents/DocumentationButton';
import { generateDefaultName } from '../../utilities/useGenerateDefaultName';
const ImageOutputStep = () => {
const dispatch = useAppDispatch();
const blueprintName = useAppSelector(selectBlueprintName);
const distribution = useAppSelector(selectDistribution);
const arch = useAppSelector(selectArchitecture);
const isCustomName = useAppSelector(selectIsCustomName);
useEffect(() => {
const defaultName = generateDefaultName(distribution, arch);
if (!isCustomName && blueprintName !== defaultName) {
dispatch(changeBlueprintName(defaultName));
}
}, [dispatch, distribution, arch, isCustomName]);
return (
<Form>

View file

@ -9,14 +9,11 @@ import {
selectBlueprintDescription,
selectBlueprintName,
} from '../../../../store/wizardSlice';
import { useGenerateDefaultName } from '../../utilities/useGenerateDefaultName';
const ReviewStep = () => {
const blueprintName = useAppSelector(selectBlueprintName);
const blueprintDescription = useAppSelector(selectBlueprintDescription);
useGenerateDefaultName();
return (
<Form>
<Title headingLevel="h1" size="xl">

View file

@ -222,6 +222,7 @@ function commonRequestToState(
return {
details: {
blueprintName: request.name || '',
isCustomName: true,
blueprintDescription: request.description || '',
},
users:

View file

@ -1,15 +1,6 @@
import { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
import { Distributions, ImageRequest } from '../../../store/imageBuilderApi';
import {
changeBlueprintName,
selectArchitecture,
selectBlueprintName,
selectDistribution,
} from '../../../store/wizardSlice';
const generateDefaultName = (
export const generateDefaultName = (
distribution: Distributions,
arch: ImageRequest['architecture']
) => {
@ -24,19 +15,3 @@ const generateDefaultName = (
return `${distribution}-${arch}-${dateTimeString}`;
};
export const useGenerateDefaultName = () => {
const dispatch = useAppDispatch();
const blueprintName = useAppSelector(selectBlueprintName);
const distribution = useAppSelector(selectDistribution);
const arch = useAppSelector(selectArchitecture);
useEffect(() => {
if (!blueprintName) {
dispatch(changeBlueprintName(generateDefaultName(distribution, arch)));
}
// This useEffect hook should run *only* on mount and therefore has an empty
// dependency array. eslint's exhaustive-deps rule does not support this use.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
};

View file

@ -30,6 +30,7 @@ import type {
GcpShareMethod,
} from '../Components/CreateImageWizard/steps/TargetEnvironment/Gcp';
import type { V1ListSourceResponseItem } from '../Components/CreateImageWizard/types';
import { generateDefaultName } from '../Components/CreateImageWizard/utilities/useGenerateDefaultName';
import { RHEL_9, X86_64 } from '../constants';
import type { RootState } from '.';
@ -140,6 +141,7 @@ export type wizardState = {
locale: Locale;
details: {
blueprintName: string;
isCustomName: boolean;
blueprintDescription: string;
};
timezone: Timezone;
@ -226,7 +228,8 @@ export const initialState: wizardState = {
keyboard: '',
},
details: {
blueprintName: '',
blueprintName: generateDefaultName(RHEL_9, X86_64),
isCustomName: false,
blueprintDescription: '',
},
timezone: {
@ -424,6 +427,10 @@ export const selectBlueprintName = (state: RootState) => {
return state.wizard.details.blueprintName;
};
export const selectIsCustomName = (state: RootState) => {
return state.wizard.details.isCustomName;
};
export const selectMetadata = (state: RootState) => {
return state.wizard.metadata;
};
@ -815,6 +822,9 @@ export const wizardSlice = createSlice({
changeBlueprintName: (state, action: PayloadAction<string>) => {
state.details.blueprintName = action.payload;
},
setIsCustomName: (state) => {
state.details.isCustomName = true;
},
changeBlueprintDescription: (state, action: PayloadAction<string>) => {
state.details.blueprintDescription = action.payload;
},
@ -1069,6 +1079,7 @@ export const {
clearLanguages,
changeKeyboard,
changeBlueprintName,
setIsCustomName,
changeBlueprintDescription,
loadWizardState,
setFirstBootScript,

View file

@ -17,7 +17,7 @@ import {
renderEditMode,
} from '../../wizardTestUtils';
const goToDetailsStep = async () => {
export const goToDetailsStep = async () => {
await clickNext(); // OpenSCAP
await clickNext(); // File system configuration
await clickNext(); // Repository snapshot

View file

@ -36,10 +36,10 @@ import {
imageRequest,
interceptBlueprintRequest,
interceptEditBlueprintRequest,
openAndDismissSaveAndBuildModal,
renderCreateMode,
renderEditMode,
} from '../../wizardTestUtils';
import { goToDetailsStep } from '../Details/Details.test';
let router: RemixRouter | undefined = undefined;
@ -117,6 +117,14 @@ const selectGuestImageTarget = async () => {
await waitFor(() => user.click(guestImageCheckBox));
};
const verifyNameInReviewStep = async (name: string) => {
const region = screen.getByRole('region', {
name: /details revisit step/i,
});
const definition = within(region).getByRole('definition');
expect(definition).toHaveTextContent(name);
};
const selectVMwareTarget = async () => {
const user = userEvent.setup();
const vmwareImageCheckBox = await screen.findByTestId('checkbox-vmware');
@ -128,21 +136,7 @@ const handleRegistration = async () => {
await clickRegisterLater();
};
const goToReviewStep = async () => {
await clickNext(); // OpenSCAP
await clickNext(); // File system customization
await clickNext(); // Repository snapshot
await clickNext(); // Custom repositories
await clickNext(); // Additional packages
await clickNext(); // Users
await clickNext(); // Timezone
await clickNext(); // Locale
await clickNext(); // Hostname
await clickNext(); // Kernel
await clickNext(); // Firewall
await clickNext(); // Services
await clickNext(); // First boot
await clickNext(); // Details
const enterNameAndGoToReviewStep = async () => {
await enterBlueprintName();
await clickNext(); // Review
};
@ -308,10 +302,43 @@ describe('Step Image output', () => {
await renderCreateMode();
await selectGuestImageTarget();
await handleRegistration();
await goToReviewStep();
await goToDetailsStep();
await enterNameAndGoToReviewStep();
await clickRevisitButton();
await screen.findByRole('heading', { name: /Image output/ });
});
test('change image type and check the update in Review step', async () => {
await renderCreateMode();
await selectGuestImageTarget();
await handleRegistration();
await goToDetailsStep();
await clickNext(); // Review
await clickRevisitButton();
await selectRhel8();
await selectAarch64();
await selectGuestImageTarget();
await handleRegistration();
await goToDetailsStep();
await clickNext(); // Review
await verifyNameInReviewStep('rhel-8-aarch64');
});
test('change blueprint name and image type, then verify the updated blueprint name in the Review step', async () => {
await renderCreateMode();
await selectGuestImageTarget();
await handleRegistration();
await goToDetailsStep();
await enterNameAndGoToReviewStep();
await clickRevisitButton();
await selectRhel8();
await selectAarch64();
await selectGuestImageTarget();
await handleRegistration();
await goToDetailsStep();
await clickNext(); // Review
await verifyNameInReviewStep('Red Velvet');
});
});
describe('Check that the target filtering is in accordance to mock content', () => {
@ -568,7 +595,8 @@ describe('Set target using query parameter', () => {
await renderCreateMode({ target: 'iso' });
expect(await screen.findByTestId('checkbox-image-installer')).toBeChecked();
await handleRegistration();
await goToReviewStep();
await goToDetailsStep();
await enterNameAndGoToReviewStep();
const targetExpandable = await screen.findByTestId(
'target-environments-expandable'
);
@ -580,7 +608,8 @@ describe('Set target using query parameter', () => {
await renderCreateMode({ target: 'qcow2' });
expect(await screen.findByTestId('checkbox-guest-image')).toBeChecked();
await handleRegistration();
await goToReviewStep();
await goToDetailsStep();
await enterNameAndGoToReviewStep();
const targetExpandable = await screen.findByTestId(
'target-environments-expandable'
);
@ -599,10 +628,8 @@ describe('Distribution request generated correctly', () => {
await selectRhel8();
await selectGuestImageTarget();
await handleRegistration();
await goToReviewStep();
// informational modal pops up in the first test only as it's tied
// to a 'imageBuilder.saveAndBuildModalSeen' variable in localStorage
await openAndDismissSaveAndBuildModal();
await goToDetailsStep();
await enterNameAndGoToReviewStep();
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
const expectedRequest: CreateBlueprintRequest = {
@ -618,7 +645,8 @@ describe('Distribution request generated correctly', () => {
await selectRhel9();
await selectGuestImageTarget();
await handleRegistration();
await goToReviewStep();
await goToDetailsStep();
await enterNameAndGoToReviewStep();
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
const expectedRequest: CreateBlueprintRequest = {
@ -633,7 +661,8 @@ describe('Distribution request generated correctly', () => {
await renderCreateMode();
await selectCentos9();
await selectGuestImageTarget();
await goToReviewStep();
await goToDetailsStep();
await enterNameAndGoToReviewStep();
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
const expectedRequest: CreateBlueprintRequest = {
@ -655,7 +684,8 @@ describe('Architecture request generated correctly', () => {
await selectX86_64();
await selectGuestImageTarget();
await handleRegistration();
await goToReviewStep();
await goToDetailsStep();
await enterNameAndGoToReviewStep();
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
const expectedImageRequest: ImageRequest = {
@ -675,7 +705,8 @@ describe('Architecture request generated correctly', () => {
await selectAarch64();
await selectGuestImageTarget();
await handleRegistration();
await goToReviewStep();
await goToDetailsStep();
await enterNameAndGoToReviewStep();
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
const expectedImageRequest: ImageRequest = {