Wizard: validate uniqueness of Blueprint name
This commit is contained in:
parent
ff0b2509cc
commit
facb71ceae
9 changed files with 80 additions and 28 deletions
|
|
@ -23,7 +23,6 @@ interface HookValidatedTextInputPropTypes extends TextInputProps {
|
|||
dataTestId?: string | undefined;
|
||||
ouiaId?: string;
|
||||
ariaLabel: string | undefined;
|
||||
helperText: string | undefined;
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
stepValidation: StepValidation;
|
||||
|
|
@ -34,7 +33,6 @@ export const HookValidatedInput = ({
|
|||
dataTestId,
|
||||
ouiaId,
|
||||
ariaLabel,
|
||||
helperText,
|
||||
value,
|
||||
placeholder,
|
||||
onChange,
|
||||
|
|
@ -43,7 +41,10 @@ export const HookValidatedInput = ({
|
|||
}: HookValidatedTextInputPropTypes) => {
|
||||
const [isPristine, setIsPristine] = useState(!value ? true : false);
|
||||
// Do not surface validation on pristine state components
|
||||
// Allow step validation to be set on pristine state, when needed
|
||||
const validated = isPristine
|
||||
? 'default'
|
||||
: stepValidation.errors[fieldName] === 'default'
|
||||
? 'default'
|
||||
: stepValidation.errors[fieldName]
|
||||
? 'error'
|
||||
|
|
@ -69,7 +70,7 @@ export const HookValidatedInput = ({
|
|||
{validated === 'error' && (
|
||||
<HelperText>
|
||||
<HelperTextItem variant="error" hasIcon>
|
||||
{helperText}
|
||||
{stepValidation.errors[fieldName]}
|
||||
</HelperTextItem>
|
||||
</HelperText>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ const DetailsStep = () => {
|
|||
dataTestId="blueprint"
|
||||
value={blueprintName}
|
||||
onChange={handleNameChange}
|
||||
helperText="Please enter a valid name"
|
||||
placeholder="Add blueprint name"
|
||||
stepValidation={stepValidation}
|
||||
fieldName="name"
|
||||
|
|
@ -79,7 +78,6 @@ const DetailsStep = () => {
|
|||
dataTestId="blueprint description"
|
||||
value={blueprintDescription || ''}
|
||||
onChange={handleDescriptionChange}
|
||||
helperText="Please enter a valid description"
|
||||
placeholder="Add description"
|
||||
stepValidation={stepValidation}
|
||||
fieldName="description"
|
||||
|
|
|
|||
|
|
@ -307,7 +307,6 @@ const MinimumSize = ({ partition }: MinimumSizePropTypes) => {
|
|||
return (
|
||||
<HookValidatedInput
|
||||
ariaLabel="minimum partition size"
|
||||
helperText="Must be larger than 0"
|
||||
value={partition.min_size}
|
||||
type="text"
|
||||
ouiaId="size"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useAppSelector } from '../../../store/hooks';
|
||||
import {
|
||||
BlueprintsResponse,
|
||||
useLazyGetBlueprintsQuery,
|
||||
} from '../../../store/imageBuilderApi';
|
||||
import {
|
||||
selectBlueprintName,
|
||||
selectBlueprintDescription,
|
||||
selectFileSystemPartitionMode,
|
||||
selectPartitions,
|
||||
selectWizardMode,
|
||||
} from '../../../store/wizardSlice';
|
||||
import {
|
||||
getDuplicateMountPoints,
|
||||
|
|
@ -38,7 +45,7 @@ export function useFilesystemValidation(): StepValidation {
|
|||
const duplicates = getDuplicateMountPoints(partitions);
|
||||
for (const partition of partitions) {
|
||||
if (!isMountpointMinSizeValid(partition.min_size)) {
|
||||
errors[`min-size-${partition.id}`] = 'Invalid size';
|
||||
errors[`min-size-${partition.id}`] = 'Must be larger than 0';
|
||||
disabledNext = true;
|
||||
}
|
||||
if (duplicates.includes(partition.mountpoint)) {
|
||||
|
|
@ -52,14 +59,47 @@ export function useFilesystemValidation(): StepValidation {
|
|||
export function useDetailsValidation(): StepValidation {
|
||||
const name = useAppSelector(selectBlueprintName);
|
||||
const description = useAppSelector(selectBlueprintDescription);
|
||||
const wizardMode = useAppSelector(selectWizardMode);
|
||||
|
||||
const nameValid = isBlueprintNameValid(name);
|
||||
const descriptionValid = isBlueprintDescriptionValid(description);
|
||||
const [uniqueName, setUniqueName] = useState<boolean | null>(null);
|
||||
|
||||
const [trigger] = useLazyGetBlueprintsQuery();
|
||||
|
||||
useEffect(() => {
|
||||
if (wizardMode === 'create' && name !== '' && nameValid) {
|
||||
trigger({ name })
|
||||
.unwrap()
|
||||
.then((response: BlueprintsResponse) => {
|
||||
if (response?.meta?.count > 0) {
|
||||
setUniqueName(false);
|
||||
} else {
|
||||
setUniqueName(true);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// If the request fails, we assume the name is unique
|
||||
setUniqueName(true);
|
||||
});
|
||||
}
|
||||
}, [wizardMode, name, setUniqueName, trigger]);
|
||||
|
||||
let nameError = '';
|
||||
if (!nameValid) {
|
||||
nameError = 'Invalid blueprint name';
|
||||
} else if (uniqueName === false) {
|
||||
nameError = 'Blueprint with this name already exists';
|
||||
} else if (wizardMode === 'create' && uniqueName === null) {
|
||||
// Hack to keep the error message from flickering
|
||||
nameError = 'default';
|
||||
}
|
||||
|
||||
return {
|
||||
errors: {
|
||||
name: nameValid ? '' : 'Invalid name',
|
||||
name: nameError,
|
||||
description: descriptionValid ? '' : 'Invalid description',
|
||||
},
|
||||
disabledNext: !nameValid || !descriptionValid,
|
||||
disabledNext: !!nameError || !descriptionValid,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -914,16 +914,17 @@ describe('Step Details', () => {
|
|||
await setUp();
|
||||
|
||||
// Enter image name
|
||||
|
||||
const invalidName = 'a'.repeat(101);
|
||||
await enterBlueprintName(invalidName);
|
||||
expect(await getNextButton()).toHaveClass('pf-m-disabled');
|
||||
expect(await getNextButton()).toBeDisabled();
|
||||
const nameInput = await screen.findByRole('textbox', {
|
||||
name: /blueprint name/i,
|
||||
});
|
||||
const invalidName = 'a'.repeat(101);
|
||||
await user.type(nameInput, invalidName);
|
||||
expect(await getNextButton()).toHaveClass('pf-m-disabled');
|
||||
expect(await getNextButton()).toBeDisabled();
|
||||
await user.clear(nameInput);
|
||||
|
||||
await user.type(nameInput, 'valid-name');
|
||||
await enterBlueprintName();
|
||||
expect(await getNextButton()).not.toHaveClass('pf-m-disabled');
|
||||
expect(await getNextButton()).toBeEnabled();
|
||||
|
||||
|
|
@ -992,10 +993,7 @@ describe('Step Review', () => {
|
|||
// skip firstboot
|
||||
await clickNext();
|
||||
// skip Details
|
||||
const blueprintName = await screen.findByRole('textbox', {
|
||||
name: /blueprint name/i,
|
||||
});
|
||||
await user.type(blueprintName, 'valid-name');
|
||||
await enterBlueprintName();
|
||||
await clickNext();
|
||||
};
|
||||
|
||||
|
|
@ -1056,10 +1054,7 @@ describe('Step Review', () => {
|
|||
await clickNext();
|
||||
// skip First boot
|
||||
await clickNext();
|
||||
const blueprintName = await screen.findByRole('textbox', {
|
||||
name: /blueprint name/i,
|
||||
});
|
||||
await user.type(blueprintName, 'valid-name');
|
||||
await enterBlueprintName();
|
||||
await clickNext();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -82,6 +82,17 @@ describe('validates name', () => {
|
|||
const nextButton = await getNextButton();
|
||||
expect(nextButton).toBeEnabled();
|
||||
});
|
||||
|
||||
test('with non-unique name', async () => {
|
||||
await renderCreateMode();
|
||||
await goToRegistrationStep();
|
||||
await clickRegisterLater();
|
||||
await goToDetailsStep();
|
||||
await enterBlueprintName('Lemon Pie');
|
||||
|
||||
const nextButton = await getNextButton();
|
||||
expect(nextButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('registration request generated correctly', () => {
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ const goToReviewStep = async () => {
|
|||
await clickNext(); // Additional packages
|
||||
await clickNext(); // FirstBoot
|
||||
await clickNext(); // Details
|
||||
await enterBlueprintName('oscap');
|
||||
await enterBlueprintName('Oscap test');
|
||||
await clickNext(); // Review
|
||||
};
|
||||
|
||||
|
|
@ -130,7 +130,10 @@ describe('oscap', () => {
|
|||
|
||||
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
|
||||
|
||||
const expectedRequest = oscapCreateBlueprintRequest;
|
||||
const expectedRequest: CreateBlueprintRequest = {
|
||||
...oscapCreateBlueprintRequest,
|
||||
name: 'Oscap test',
|
||||
};
|
||||
|
||||
expect(receivedRequest).toEqual(expectedRequest);
|
||||
});
|
||||
|
|
@ -146,7 +149,7 @@ describe('oscap', () => {
|
|||
|
||||
const expectedRequest: CreateBlueprintRequest = {
|
||||
...baseCreateBlueprintRequest,
|
||||
name: 'oscap',
|
||||
name: 'Oscap test',
|
||||
};
|
||||
|
||||
expect(receivedRequest).toEqual(expectedRequest);
|
||||
|
|
@ -170,7 +173,7 @@ describe('oscap', () => {
|
|||
kernel: expectedKernelCisL2,
|
||||
filesystem: expectedFilesystemCisL2,
|
||||
},
|
||||
name: 'oscap',
|
||||
name: 'Oscap test',
|
||||
};
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
|
|||
2
src/test/fixtures/blueprints.ts
vendored
2
src/test/fixtures/blueprints.ts
vendored
|
|
@ -157,7 +157,7 @@ export const mockGetBlueprints: GetBlueprintsApiResponse = {
|
|||
},
|
||||
{
|
||||
id: '147032db-8697-4638-8fdd-6f428100d8fc',
|
||||
name: 'Red Velvet',
|
||||
name: 'Pink Velvet',
|
||||
description: 'Layered cake with icing',
|
||||
version: 1,
|
||||
last_modified_at: '2021-09-08T21:00:00.000Z',
|
||||
|
|
|
|||
|
|
@ -155,11 +155,16 @@ export const handlers = [
|
|||
}
|
||||
),
|
||||
rest.get(`${IMAGE_BUILDER_API}/blueprints`, (req, res, ctx) => {
|
||||
const nameParam = req.url.searchParams.get('name');
|
||||
const search = req.url.searchParams.get('search');
|
||||
const limit = req.url.searchParams.get('limit') || '10';
|
||||
const offset = req.url.searchParams.get('offset') || '0';
|
||||
const resp = Object.assign({}, mockGetBlueprints);
|
||||
if (search) {
|
||||
if (nameParam) {
|
||||
resp.data = resp.data.filter(({ name }) => {
|
||||
return nameParam === name;
|
||||
});
|
||||
} else if (search) {
|
||||
let regexp;
|
||||
try {
|
||||
regexp = new RegExp(search);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue