Wizard: Add Kernel name input
This adds a kernel name input.
This commit is contained in:
parent
7129cf866e
commit
c121e5caba
7 changed files with 169 additions and 6 deletions
|
|
@ -40,6 +40,7 @@ import {
|
|||
useDetailsValidation,
|
||||
useRegistrationValidation,
|
||||
useHostnameValidation,
|
||||
useKernelValidation,
|
||||
} from './utilities/useValidation';
|
||||
import {
|
||||
isAwsAccountIdValid,
|
||||
|
|
@ -223,6 +224,8 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
const fileSystemValidation = useFilesystemValidation();
|
||||
// Hostname
|
||||
const hostnameValidation = useHostnameValidation();
|
||||
// Kernel
|
||||
const kernelValidation = useKernelValidation();
|
||||
// Firstboot
|
||||
const firstBootValidation = useFirstBootValidation();
|
||||
// Details
|
||||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -335,6 +335,7 @@ function commonRequestToState(
|
|||
disabled: request.customizations?.services?.disabled || [],
|
||||
},
|
||||
kernel: {
|
||||
name: request.customizations.kernel?.name || '',
|
||||
append: request.customizations?.kernel?.append || '',
|
||||
},
|
||||
timezone: {
|
||||
|
|
@ -539,9 +540,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),
|
||||
|
|
@ -758,3 +757,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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -91,6 +91,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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ export type wizardState = {
|
|||
disabled: string[];
|
||||
};
|
||||
kernel: {
|
||||
name: string;
|
||||
append: string;
|
||||
};
|
||||
locale: Locale;
|
||||
|
|
@ -204,6 +205,7 @@ export const initialState: wizardState = {
|
|||
disabled: [],
|
||||
},
|
||||
kernel: {
|
||||
name: '',
|
||||
append: '',
|
||||
},
|
||||
locale: {
|
||||
|
|
@ -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;
|
||||
},
|
||||
|
|
@ -922,6 +927,7 @@ export const {
|
|||
changeEnabledServices,
|
||||
changeMaskedServices,
|
||||
changeDisabledServices,
|
||||
changeKernelName,
|
||||
changeKernelAppend,
|
||||
changeTimezone,
|
||||
addNtpServer,
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue