From 79352726cf2ecb3eb5b97a34c3816bd3947a2d55 Mon Sep 17 00:00:00 2001 From: regexowl Date: Tue, 17 Dec 2024 08:54:46 +0100 Subject: [PATCH] Wizard: Add Kernel name input This adds a kernel name input. --- .../CreateImageWizard/CreateImageWizard.tsx | 9 +- .../steps/Kernel/components/KernelName.tsx | 30 ++++++- .../utilities/requestMapper.ts | 18 +++- .../utilities/useValidation.tsx | 18 ++++ .../CreateImageWizard/validators.ts | 11 +++ src/store/wizardSlice.ts | 6 ++ .../steps/Kernel/Kernel.test.tsx | 83 ++++++++++++++++++- 7 files changed, 169 insertions(+), 6 deletions(-) diff --git a/src/Components/CreateImageWizard/CreateImageWizard.tsx b/src/Components/CreateImageWizard/CreateImageWizard.tsx index f1c5b38d5..5f89e11c3 100644 --- a/src/Components/CreateImageWizard/CreateImageWizard.tsx +++ b/src/Components/CreateImageWizard/CreateImageWizard.tsx @@ -39,6 +39,7 @@ import { useDetailsValidation, useRegistrationValidation, useHostnameValidation, + useKernelValidation, } from './utilities/useValidation'; import { isAwsAccountIdValid, @@ -221,6 +222,8 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => { const fileSystemValidation = useFilesystemValidation(); // Hostname const hostnameValidation = useHostnameValidation(); + // Kernel + const kernelValidation = useKernelValidation(); // Firstboot const firstBootValidation = useFirstBootValidation(); // Details @@ -508,8 +511,12 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => { key="wizard-kernel" navItem={customStatusNavItem} isHidden={!isKernelEnabled} + status={kernelValidation.disabledNext ? 'error' : 'default'} footer={ - + } > diff --git a/src/Components/CreateImageWizard/steps/Kernel/components/KernelName.tsx b/src/Components/CreateImageWizard/steps/Kernel/components/KernelName.tsx index d5f413fe4..602209b3c 100644 --- a/src/Components/CreateImageWizard/steps/Kernel/components/KernelName.tsx +++ b/src/Components/CreateImageWizard/steps/Kernel/components/KernelName.tsx @@ -2,8 +2,36 @@ import React from 'react'; import { FormGroup } from '@patternfly/react-core'; +import { useAppDispatch, useAppSelector } from '../../../../../store/hooks'; +import { + changeKernelName, + selectKernel, +} from '../../../../../store/wizardSlice'; +import { useKernelValidation } from '../../../utilities/useValidation'; +import { HookValidatedInput } from '../../../ValidatedTextInput'; + const KernelName = () => { - return ; + const dispatch = useAppDispatch(); + const kernel = useAppSelector(selectKernel); + + const stepValidation = useKernelValidation(); + + const handleChange = (e: React.FormEvent, value: string) => { + dispatch(changeKernelName(value)); + }; + + return ( + + + + ); }; export default KernelName; diff --git a/src/Components/CreateImageWizard/utilities/requestMapper.ts b/src/Components/CreateImageWizard/utilities/requestMapper.ts index adc4459a7..4c62279f5 100644 --- a/src/Components/CreateImageWizard/utilities/requestMapper.ts +++ b/src/Components/CreateImageWizard/utilities/requestMapper.ts @@ -305,6 +305,7 @@ function commonRequestToState( disabled: request.customizations?.services?.disabled || [], }, kernel: { + name: request.customizations.kernel?.name || '', append: request.customizations?.kernel?.append || '', }, timezone: { @@ -505,9 +506,7 @@ const getCustomizations = (state: RootState, orgID: string): Customizations => { users: undefined, services: getServices(state), hostname: selectHostname(state) || undefined, - kernel: selectKernel(state).append - ? { append: selectKernel(state).append } - : undefined, + kernel: getKernel(state), groups: undefined, timezone: getTimezone(state), locale: getLocale(state), @@ -691,3 +690,16 @@ const getPayloadRepositories = (state: RootState) => { } return payloadAndRecommendedRepositories; }; + +const getKernel = (state: RootState) => { + const kernel = selectKernel(state); + + if (!kernel.name && !kernel.append) { + return undefined; + } + + return { + name: selectKernel(state).name || undefined, + append: selectKernel(state).append || undefined, + }; +}; diff --git a/src/Components/CreateImageWizard/utilities/useValidation.tsx b/src/Components/CreateImageWizard/utilities/useValidation.tsx index 2307c6d2d..c79023927 100644 --- a/src/Components/CreateImageWizard/utilities/useValidation.tsx +++ b/src/Components/CreateImageWizard/utilities/useValidation.tsx @@ -19,6 +19,7 @@ import { selectActivationKey, selectRegistrationType, selectHostname, + selectKernel, } from '../../../store/wizardSlice'; import { getDuplicateMountPoints, @@ -27,6 +28,7 @@ import { isMountpointMinSizeValid, isSnapshotValid, isHostnameValid, + isKernelNameValid, } from '../validators'; export type StepValidation = { @@ -41,6 +43,7 @@ export function useIsBlueprintValid(): boolean { const filesystem = useFilesystemValidation(); const snapshot = useSnapshotValidation(); const hostname = useHostnameValidation(); + const kernel = useKernelValidation(); const firstBoot = useFirstBootValidation(); const details = useDetailsValidation(); return ( @@ -48,6 +51,7 @@ export function useIsBlueprintValid(): boolean { !filesystem.disabledNext && !snapshot.disabledNext && !hostname.disabledNext && + !kernel.disabledNext && !firstBoot.disabledNext && !details.disabledNext ); @@ -155,6 +159,20 @@ export function useHostnameValidation(): StepValidation { return { errors: {}, disabledNext: false }; } +export function useKernelValidation(): StepValidation { + const kernel = useAppSelector(selectKernel); + + if (!isKernelNameValid(kernel.name)) { + return { + errors: { + kernel: 'Invalid kernel name', + }, + disabledNext: true, + }; + } + return { errors: {}, disabledNext: false }; +} + export function useDetailsValidation(): StepValidation { const name = useAppSelector(selectBlueprintName); const description = useAppSelector(selectBlueprintDescription); diff --git a/src/Components/CreateImageWizard/validators.ts b/src/Components/CreateImageWizard/validators.ts index 9704b36e2..2ac76cf52 100644 --- a/src/Components/CreateImageWizard/validators.ts +++ b/src/Components/CreateImageWizard/validators.ts @@ -103,3 +103,14 @@ export const isHostnameValid = (hostname: string) => { ) ); }; + +export const isKernelNameValid = (kernelName: string) => { + if (!kernelName) { + return true; + } + + return ( + kernelName.length < 65 && + /^[a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(kernelName) + ); +}; diff --git a/src/store/wizardSlice.ts b/src/store/wizardSlice.ts index 73fcfa339..83d410b59 100644 --- a/src/store/wizardSlice.ts +++ b/src/store/wizardSlice.ts @@ -106,6 +106,7 @@ export type wizardState = { disabled: string[]; }; kernel: { + name: string; append: string; }; locale: Locale; @@ -180,6 +181,7 @@ export const initialState: wizardState = { disabled: [], }, kernel: { + name: '', append: '', }, locale: { @@ -734,6 +736,9 @@ export const wizardSlice = createSlice({ changeDisabledServices: (state, action: PayloadAction) => { state.services.disabled = action.payload; }, + changeKernelName: (state, action: PayloadAction) => { + state.kernel.name = action.payload; + }, changeKernelAppend: (state, action: PayloadAction) => { state.kernel.append = action.payload; }, @@ -820,6 +825,7 @@ export const { changeEnabledServices, changeMaskedServices, changeDisabledServices, + changeKernelName, changeKernelAppend, changeTimezone, addNtpServer, diff --git a/src/test/Components/CreateImageWizard/steps/Kernel/Kernel.test.tsx b/src/test/Components/CreateImageWizard/steps/Kernel/Kernel.test.tsx index cada521c2..9e18e223c 100644 --- a/src/test/Components/CreateImageWizard/steps/Kernel/Kernel.test.tsx +++ b/src/test/Components/CreateImageWizard/steps/Kernel/Kernel.test.tsx @@ -2,9 +2,15 @@ import type { Router as RemixRouter } from '@remix-run/router'; import { screen, waitFor } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; +import { CREATE_BLUEPRINT } from '../../../../../constants'; import { + blueprintRequest, clickBack, clickNext, + enterBlueprintName, + getNextButton, + interceptBlueprintRequest, + openAndDismissSaveAndBuildModal, verifyCancelButton, } from '../../wizardTestUtils'; import { clickRegisterLater, renderCreateMode } from '../../wizardTestUtils'; @@ -31,6 +37,29 @@ const goToKernelStep = async () => { await clickNext(); // Kernel }; +const goToReviewStep = async () => { + await clickNext(); // First boot script + await clickNext(); // Details + await enterBlueprintName(); + await clickNext(); // Review +}; + +const enterKernelName = async (kernelName: string) => { + const user = userEvent.setup(); + const kernelNameInput = await screen.findByPlaceholderText( + /Add a kernel name/i + ); + await waitFor(() => user.type(kernelNameInput, kernelName)); +}; + +const clearKernelName = async () => { + const user = userEvent.setup(); + const kernelNameInput = await screen.findByPlaceholderText( + /Add a kernel name/i + ); + await waitFor(() => user.clear(kernelNameInput)); +}; + describe('Step Kernel', () => { beforeEach(() => { vi.clearAllMocks(); @@ -58,8 +87,60 @@ describe('Step Kernel', () => { await goToKernelStep(); await verifyCancelButton(router); }); + + test('validation works', async () => { + await renderCreateMode(); + await goToKernelStep(); + + // with empty kernel name input + const nextButton = await getNextButton(); + expect(nextButton).toBeEnabled(); + + // invalid name + await enterKernelName('INVALID/NAME'); + expect(nextButton).toBeDisabled(); + await clickNext(); // dummy click to blur and render error (doesn't render when pristine) + await screen.findByText(/Invalid kernel name/); + + // valid name + await clearKernelName(); + await enterKernelName('valid-kernel-name'); + expect(nextButton).toBeEnabled(); + expect(screen.queryByText(/Invalid kernel name/)).not.toBeInTheDocument(); + }); +}); + +describe('Kernel request generated correctly', () => { + beforeEach(async () => { + vi.clearAllMocks(); + }); + + test('with valid kernel name', async () => { + await renderCreateMode(); + await goToKernelStep(); + await enterKernelName('kernel-name'); + await goToReviewStep(); + // informational modal pops up in the first test only as it's tied + // to a 'imageBuilder.saveAndBuildModalSeen' variable in localStorage + await openAndDismissSaveAndBuildModal(); + const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT); + + const expectedRequest = { + ...blueprintRequest, + customizations: { + kernel: { + name: 'kernel-name', + }, + }, + }; + + await waitFor(() => { + expect(receivedRequest).toEqual(expectedRequest); + }); + }); }); // TO DO 'Kernel step' -> 'revisit step button on Review works' -// TO DO 'Kernel request generated correctly' +// TO DO 'Kernel request generated correctly' -> 'with valid kernel append' +// TO DO 'Kernel request generated correctly' -> 'with valid kernel name and kernel append' // TO DO 'Kernel edit mode'