Skip to content

Commit

Permalink
Wizard: Add Hostname functionality
Browse files Browse the repository at this point in the history
This adds a validated hostname input and new tests.
  • Loading branch information
regexowl authored and lucasgarfield committed Dec 11, 2024
1 parent c98b7d9 commit 095c55e
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 4 deletions.
9 changes: 8 additions & 1 deletion src/Components/CreateImageWizard/CreateImageWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
useFirstBootValidation,
useDetailsValidation,
useRegistrationValidation,
useHostnameValidation,
} from './utilities/useValidation';
import {
isAwsAccountIdValid,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 />
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 3 additions & 1 deletion src/Components/CreateImageWizard/utilities/requestMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import {
selectNtpServers,
selectLanguages,
selectKeyboard,
selectHostname,
} from '../../../store/wizardSlice';
import { FileSystemConfigurationType } from '../steps/FileSystem';
import {
Expand Down Expand Up @@ -310,6 +311,7 @@ function commonRequestToState(
timezone: request.customizations.timezone?.timezone || '',
ntpservers: request.customizations.timezone?.ntpservers || [],
},
hostname: request.customizations.hostname || '',
};
}

Expand Down Expand Up @@ -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,
Expand Down
18 changes: 18 additions & 0 deletions src/Components/CreateImageWizard/utilities/useValidation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ import {
selectUseLatest,
selectActivationKey,
selectRegistrationType,
selectHostname,
} from '../../../store/wizardSlice';
import {
getDuplicateMountPoints,
isBlueprintNameValid,
isBlueprintDescriptionValid,
isMountpointMinSizeValid,
isSnapshotValid,
isHostnameValid,
} from '../validators';

export type StepValidation = {
Expand All @@ -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
);
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions src/Components/CreateImageWizard/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
10 changes: 10 additions & 0 deletions src/store/wizardSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export type wizardState = {
blueprintDescription: string;
};
timezone: Timezone;
hostname: string;
metadata?: {
parent_id: string | null;
exported_at: string;
Expand Down Expand Up @@ -193,6 +194,7 @@ export const initialState: wizardState = {
timezone: '',
ntpservers: [],
},
hostname: '',
firstBoot: { script: '' },
};

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -749,6 +755,9 @@ export const wizardSlice = createSlice({
1
);
},
changeHostname: (state, action: PayloadAction<string>) => {
state.hostname = action.payload;
},
},
});

Expand Down Expand Up @@ -815,5 +824,6 @@ export const {
changeTimezone,
addNtpServer,
removeNtpServer,
changeHostname,
} = wizardSlice.actions;
export default wizardSlice.reducer;
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 @@ -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();
Expand Down Expand Up @@ -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'

0 comments on commit 095c55e

Please sign in to comment.