Wizard: Add Hostname functionality
This adds a validated hostname input and new tests.
This commit is contained in:
parent
c98b7d9997
commit
5a514d1d04
7 changed files with 153 additions and 4 deletions
|
|
@ -37,6 +37,7 @@ import {
|
|||
useFirstBootValidation,
|
||||
useDetailsValidation,
|
||||
useRegistrationValidation,
|
||||
useHostnameValidation,
|
||||
} from './utilities/useValidation';
|
||||
import {
|
||||
isAwsAccountIdValid,
|
||||
|
|
@ -216,6 +217,8 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
// Filesystem
|
||||
const [filesystemPristine, setFilesystemPristine] = useState(true);
|
||||
const fileSystemValidation = useFilesystemValidation();
|
||||
// Hostname
|
||||
const hostnameValidation = useHostnameValidation();
|
||||
// Firstboot
|
||||
const firstBootValidation = useFirstBootValidation();
|
||||
// Details
|
||||
|
|
@ -487,8 +490,12 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
key="wizard-hostname"
|
||||
navItem={customStatusNavItem}
|
||||
isHidden={!isHostnameEnabled}
|
||||
status={hostnameValidation.disabledNext ? 'error' : 'default'}
|
||||
footer={
|
||||
<CustomWizardFooter disableNext={false} optional={true} />
|
||||
<CustomWizardFooter
|
||||
disableNext={hostnameValidation.disabledNext}
|
||||
optional={true}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<HostnameStep />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,37 @@
|
|||
import React from 'react';
|
||||
|
||||
import { FormGroup } from '@patternfly/react-core';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
|
||||
import {
|
||||
changeHostname,
|
||||
selectHostname,
|
||||
} from '../../../../../store/wizardSlice';
|
||||
import { useHostnameValidation } from '../../../utilities/useValidation';
|
||||
import { HookValidatedInput } from '../../../ValidatedTextInput';
|
||||
|
||||
const HostnameInput = () => {
|
||||
return <></>;
|
||||
const dispatch = useAppDispatch();
|
||||
const hostname = useAppSelector(selectHostname);
|
||||
|
||||
const stepValidation = useHostnameValidation();
|
||||
|
||||
const handleChange = (e: React.FormEvent, value: string) => {
|
||||
dispatch(changeHostname(value));
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup label="Hostname">
|
||||
<HookValidatedInput
|
||||
ariaLabel="hostname input"
|
||||
value={hostname}
|
||||
onChange={handleChange}
|
||||
placeholder="Add a hostname"
|
||||
stepValidation={stepValidation}
|
||||
fieldName="hostname"
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default HostnameInput;
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ import {
|
|||
selectNtpServers,
|
||||
selectLanguages,
|
||||
selectKeyboard,
|
||||
selectHostname,
|
||||
} from '../../../store/wizardSlice';
|
||||
import { FileSystemConfigurationType } from '../steps/FileSystem';
|
||||
import {
|
||||
|
|
@ -310,6 +311,7 @@ function commonRequestToState(
|
|||
timezone: request.customizations.timezone?.timezone || '',
|
||||
ntpservers: request.customizations.timezone?.ntpservers || [],
|
||||
},
|
||||
hostname: request.customizations.hostname || '',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -502,7 +504,7 @@ const getCustomizations = (state: RootState, orgID: string): Customizations => {
|
|||
filesystem: getFileSystem(state),
|
||||
users: undefined,
|
||||
services: getServices(state),
|
||||
hostname: undefined,
|
||||
hostname: selectHostname(state) || undefined,
|
||||
kernel: selectKernel(state).append
|
||||
? { append: selectKernel(state).append }
|
||||
: undefined,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
selectUseLatest,
|
||||
selectActivationKey,
|
||||
selectRegistrationType,
|
||||
selectHostname,
|
||||
} from '../../../store/wizardSlice';
|
||||
import {
|
||||
getDuplicateMountPoints,
|
||||
|
|
@ -25,6 +26,7 @@ import {
|
|||
isBlueprintDescriptionValid,
|
||||
isMountpointMinSizeValid,
|
||||
isSnapshotValid,
|
||||
isHostnameValid,
|
||||
} from '../validators';
|
||||
|
||||
export type StepValidation = {
|
||||
|
|
@ -38,12 +40,14 @@ export function useIsBlueprintValid(): boolean {
|
|||
const registration = useRegistrationValidation();
|
||||
const filesystem = useFilesystemValidation();
|
||||
const snapshot = useSnapshotValidation();
|
||||
const hostname = useHostnameValidation();
|
||||
const firstBoot = useFirstBootValidation();
|
||||
const details = useDetailsValidation();
|
||||
return (
|
||||
!registration.disabledNext &&
|
||||
!filesystem.disabledNext &&
|
||||
!snapshot.disabledNext &&
|
||||
!hostname.disabledNext &&
|
||||
!firstBoot.disabledNext &&
|
||||
!details.disabledNext
|
||||
);
|
||||
|
|
@ -133,6 +137,20 @@ export function useFirstBootValidation(): StepValidation {
|
|||
};
|
||||
}
|
||||
|
||||
export function useHostnameValidation(): StepValidation {
|
||||
const hostname = useAppSelector(selectHostname);
|
||||
|
||||
if (!isHostnameValid(hostname)) {
|
||||
return {
|
||||
errors: {
|
||||
hostname: 'Invalid hostname',
|
||||
},
|
||||
disabledNext: true,
|
||||
};
|
||||
}
|
||||
return { errors: {}, disabledNext: false };
|
||||
}
|
||||
|
||||
export function useDetailsValidation(): StepValidation {
|
||||
const name = useAppSelector(selectBlueprintName);
|
||||
const description = useAppSelector(selectBlueprintDescription);
|
||||
|
|
|
|||
|
|
@ -90,3 +90,12 @@ export const isNtpServerValid = (ntpServer: string) => {
|
|||
/^([a-z0-9-]+)?(([.:/]{1,3}[a-z0-9-]+)){1,}$/.test(ntpServer)
|
||||
);
|
||||
};
|
||||
|
||||
export const isHostnameValid = (hostname: string) => {
|
||||
if (hostname !== '') {
|
||||
return /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/.test(
|
||||
hostname
|
||||
);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ export type wizardState = {
|
|||
blueprintDescription: string;
|
||||
};
|
||||
timezone: Timezone;
|
||||
hostname: string;
|
||||
metadata?: {
|
||||
parent_id: string | null;
|
||||
exported_at: string;
|
||||
|
|
@ -193,6 +194,7 @@ export const initialState: wizardState = {
|
|||
timezone: '',
|
||||
ntpservers: [],
|
||||
},
|
||||
hostname: '',
|
||||
firstBoot: { script: '' },
|
||||
};
|
||||
|
||||
|
|
@ -371,6 +373,10 @@ export const selectNtpServers = (state: RootState) => {
|
|||
return state.wizard.timezone.ntpservers;
|
||||
};
|
||||
|
||||
export const selectHostname = (state: RootState) => {
|
||||
return state.wizard.hostname;
|
||||
};
|
||||
|
||||
export const wizardSlice = createSlice({
|
||||
name: 'wizard',
|
||||
initialState,
|
||||
|
|
@ -749,6 +755,9 @@ export const wizardSlice = createSlice({
|
|||
1
|
||||
);
|
||||
},
|
||||
changeHostname: (state, action: PayloadAction<string>) => {
|
||||
state.hostname = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -815,5 +824,6 @@ export const {
|
|||
changeTimezone,
|
||||
addNtpServer,
|
||||
removeNtpServer,
|
||||
changeHostname,
|
||||
} = wizardSlice.actions;
|
||||
export default wizardSlice.reducer;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -30,6 +36,25 @@ const goToHostnameStep = async () => {
|
|||
await clickNext(); // Hostname
|
||||
};
|
||||
|
||||
const goToReviewStep = async () => {
|
||||
await clickNext(); // First boot script
|
||||
await clickNext(); // Details
|
||||
await enterBlueprintName();
|
||||
await clickNext(); // Review
|
||||
};
|
||||
|
||||
const enterHostname = async (hostname: string) => {
|
||||
const user = userEvent.setup();
|
||||
const hostnameInput = await screen.findByPlaceholderText(/Add a hostname/i);
|
||||
await waitFor(() => user.type(hostnameInput, hostname));
|
||||
};
|
||||
|
||||
const clearHostname = async () => {
|
||||
const user = userEvent.setup();
|
||||
const hostnameInput = await screen.findByPlaceholderText(/Add a hostname/i);
|
||||
await waitFor(() => user.clear(hostnameInput));
|
||||
};
|
||||
|
||||
describe('Step Hostname', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
|
@ -57,8 +82,56 @@ describe('Step Hostname', () => {
|
|||
await goToHostnameStep();
|
||||
await verifyCancelButton(router);
|
||||
});
|
||||
|
||||
test('validation works', async () => {
|
||||
await renderCreateMode();
|
||||
await goToHostnameStep();
|
||||
|
||||
// with empty hostname input
|
||||
const nextButton = await getNextButton();
|
||||
expect(nextButton).toBeEnabled();
|
||||
|
||||
// invalid name
|
||||
await enterHostname('-invalid-hostname-');
|
||||
expect(nextButton).toBeDisabled();
|
||||
await clickNext(); // dummy click to blur and render error (doesn't render when pristine)
|
||||
await screen.findByText(/Invalid hostname/);
|
||||
|
||||
// valid name
|
||||
await clearHostname();
|
||||
await enterHostname('valid-hostname');
|
||||
expect(nextButton).toBeEnabled();
|
||||
expect(screen.queryByText(/Invalid hostname/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Hostname request generated correctly', () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('with valid hostname', async () => {
|
||||
await renderCreateMode();
|
||||
await goToHostnameStep();
|
||||
await enterHostname('hostname');
|
||||
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: {
|
||||
hostname: 'hostname',
|
||||
},
|
||||
};
|
||||
|
||||
await waitFor(() => {
|
||||
expect(receivedRequest).toEqual(expectedRequest);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TO DO 'Step Hostname' -> 'revisit step button on Review works'
|
||||
// TO DO 'Hostname request generated correctly'
|
||||
// TO DO 'Hostname edit mode'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue