Skip to content

Commit

Permalink
Wizard: Add Kernel name input
Browse files Browse the repository at this point in the history
This adds a kernel name input.
  • Loading branch information
regexowl committed Jan 17, 2025
1 parent 52efa80 commit 5494579
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 6 deletions.
9 changes: 8 additions & 1 deletion src/Components/CreateImageWizard/CreateImageWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
useDetailsValidation,
useRegistrationValidation,
useHostnameValidation,
useKernelValidation,
} from './utilities/useValidation';
import {
isAwsAccountIdValid,
Expand Down Expand Up @@ -223,6 +224,8 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
const fileSystemValidation = useFilesystemValidation();
// Hostname
const hostnameValidation = useHostnameValidation();
// Kernel
const kernelValidation = useKernelValidation();
// Firstboot
const firstBootValidation = useFirstBootValidation();
// Details
Expand Down Expand Up @@ -510,8 +513,12 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
key="wizard-kernel"
navItem={customStatusNavItem}
isHidden={!isKernelEnabled}
status={kernelValidation.disabledNext ? 'error' : 'default'}
footer={
<CustomWizardFooter disableNext={false} optional={true} />
<CustomWizardFooter
disableNext={kernelValidation.disabledNext}
optional={true}
/>
}
>
<KernelStep />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <FormGroup isRequired={false} label="Name"></FormGroup>;
const dispatch = useAppDispatch();
const kernel = useAppSelector(selectKernel);

const stepValidation = useKernelValidation();

const handleChange = (e: React.FormEvent, value: string) => {
dispatch(changeKernelName(value));
};

return (
<FormGroup isRequired={false} label="Name">
<HookValidatedInput
ariaLabel="kernel input"
value={kernel.name}
onChange={handleChange}
placeholder="Add a kernel name"
stepValidation={stepValidation}
fieldName="kernel"
/>
</FormGroup>
);
};

export default KernelName;
18 changes: 15 additions & 3 deletions src/Components/CreateImageWizard/utilities/requestMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ function commonRequestToState(
disabled: request.customizations?.services?.disabled || [],
},
kernel: {
name: request.customizations.kernel?.name || '',
append: request.customizations?.kernel?.append || '',
},
timezone: {
Expand Down Expand Up @@ -521,9 +522,7 @@ const getCustomizations = (state: RootState, orgID: string): Customizations => {
users: getUsers(state),
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),
Expand Down Expand Up @@ -740,3 +739,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,
};
};
18 changes: 18 additions & 0 deletions src/Components/CreateImageWizard/utilities/useValidation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
selectActivationKey,
selectRegistrationType,
selectHostname,
selectKernel,
} from '../../../store/wizardSlice';
import {
getDuplicateMountPoints,
Expand All @@ -27,6 +28,7 @@ import {
isMountpointMinSizeValid,
isSnapshotValid,
isHostnameValid,
isKernelNameValid,
} from '../validators';

export type StepValidation = {
Expand All @@ -41,13 +43,15 @@ export function useIsBlueprintValid(): boolean {
const filesystem = useFilesystemValidation();
const snapshot = useSnapshotValidation();
const hostname = useHostnameValidation();
const kernel = useKernelValidation();
const firstBoot = useFirstBootValidation();
const details = useDetailsValidation();
return (
!registration.disabledNext &&
!filesystem.disabledNext &&
!snapshot.disabledNext &&
!hostname.disabledNext &&
!kernel.disabledNext &&
!firstBoot.disabledNext &&
!details.disabledNext
);
Expand Down Expand Up @@ -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);
Expand Down
11 changes: 11 additions & 0 deletions src/Components/CreateImageWizard/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@ 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)
);
};

export const isPortValid = (port: string) => {
return /^(\d{1,5}|[a-z]{1,6})(-\d{1,5})?:[a-z]{1,6}$/.test(port);
};
6 changes: 6 additions & 0 deletions src/store/wizardSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export type wizardState = {
disabled: string[];
};
kernel: {
name: string;
append: string;
};
locale: Locale;
Expand Down Expand Up @@ -204,6 +205,7 @@ export const initialState: wizardState = {
disabled: [],
},
kernel: {
name: '',
append: '',
},
locale: {
Expand Down Expand Up @@ -800,6 +802,9 @@ export const wizardSlice = createSlice({
changeDisabledServices: (state, action: PayloadAction<string[]>) => {
state.services.disabled = action.payload;
},
changeKernelName: (state, action: PayloadAction<string>) => {
state.kernel.name = action.payload;
},
changeKernelAppend: (state, action: PayloadAction<string>) => {
state.kernel.append = action.payload;
},
Expand Down Expand Up @@ -922,6 +927,7 @@ export const {
changeEnabledServices,
changeMaskedServices,
changeDisabledServices,
changeKernelName,
changeKernelAppend,
changeTimezone,
addNtpServer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();
Expand Down Expand Up @@ -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'

0 comments on commit 5494579

Please sign in to comment.