diff --git a/src/Components/CreateImageWizard/CreateImageWizard.tsx b/src/Components/CreateImageWizard/CreateImageWizard.tsx index 3404c494..e8a8aac2 100644 --- a/src/Components/CreateImageWizard/CreateImageWizard.tsx +++ b/src/Components/CreateImageWizard/CreateImageWizard.tsx @@ -28,6 +28,7 @@ import Azure from './steps/TargetEnvironment/Azure'; import Gcp from './steps/TargetEnvironment/Gcp'; import { useFilesystemValidation, + useFirstBootValidation, useDetailsValidation, } from './utilities/useValidation'; import { @@ -179,9 +180,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => { const snapshotStepRequiresChoice = !useLatest && !snapshotDate; - const detailsValidation = useDetailsValidation(); - const fileSystemValidation = useFilesystemValidation(); const [filesystemPristine, setFilesystemPristine] = useState(true); + const fileSystemValidation = useFilesystemValidation(); + const firstBootValidation = useFirstBootValidation(); + const detailsValidation = useDetailsValidation(); let startIndex = 1; // default index if (isEdit) { @@ -192,7 +194,9 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => { } } - const detailsNavItem = ( + // Duplicating some of the logic from the Wizard component to allow for custom nav items status + // for original code see https://github.com/patternfly/patternfly-react/blob/184c55f8d10e1d94ffd72e09212db56c15387c5e/packages/react-core/src/components/Wizard/WizardNavInternal.tsx#L128 + const customStatusNavItem = ( step: WizardStepType, activeStep: WizardStepType, steps: WizardStepType[], @@ -203,6 +207,11 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => { (s) => s.index > step.index && s.isVisited ); + // Only this code is different from the original + const status = + (step.isVisited && step.id !== activeStep?.id && step.status) || + 'default'; + return ( { isVisited={step.isVisited} stepIndex={step.index} onClick={() => goToStepByIndex(step.index)} - status={ - (step.isVisited && step.id !== activeStep?.id && step.status) || - 'default' - } + status={status} /> ); }; @@ -400,7 +406,13 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => { name="First boot script configuration" id="wizard-first-boot" key="wizard-first-boot" - footer={} + navItem={customStatusNavItem} + status={firstBootValidation.disabledNext ? 'error' : 'default'} + footer={ + + } > @@ -409,7 +421,7 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => { name="Details" id={'step-details'} isDisabled={snapshotStepRequiresChoice} - navItem={detailsNavItem} + navItem={customStatusNavItem} status={detailsValidation.disabledNext ? 'error' : 'default'} footer={ { const lines = scriptString.split('\n'); @@ -32,6 +42,7 @@ const FirstBootStep = () => { const dispatch = useAppDispatch(); const selectedScript = useAppSelector(selectFirstBootScript); const language = detectScriptType(selectedScript); + const { errors } = useFirstBootValidation(); return (
@@ -55,20 +66,31 @@ const FirstBootStep = () => { privacy. - dispatch(setFirstBootScript(code))} - code={selectedScript} - height="35vh" - emptyStateButton={Browse} - emptyStateLink={ - Start from scratch - } - /> + + dispatch(setFirstBootScript(code))} + code={selectedScript} + height="35vh" + emptyStateButton={Browse} + emptyStateLink={ + Start from scratch + } + /> + {errors.script && ( + + + + {errors.script} + + + + )} + ); }; diff --git a/src/Components/CreateImageWizard/utilities/useValidation.tsx b/src/Components/CreateImageWizard/utilities/useValidation.tsx index 2d6c6ef1..e07f34a8 100644 --- a/src/Components/CreateImageWizard/utilities/useValidation.tsx +++ b/src/Components/CreateImageWizard/utilities/useValidation.tsx @@ -10,6 +10,7 @@ import { selectBlueprintName, selectBlueprintDescription, selectFileSystemPartitionMode, + selectFirstBootScript, selectPartitions, } from '../../../store/wizardSlice'; import { @@ -28,8 +29,11 @@ export type StepValidation = { export function useIsBlueprintValid(): boolean { const filesystem = useFilesystemValidation(); + const firstBoot = useFirstBootValidation(); const details = useDetailsValidation(); - return !filesystem.disabledNext && !details.disabledNext; + return ( + !filesystem.disabledNext && !firstBoot.disabledNext && !details.disabledNext + ); } export function useFilesystemValidation(): StepValidation { @@ -56,6 +60,21 @@ export function useFilesystemValidation(): StepValidation { return { errors, disabledNext }; } +export function useFirstBootValidation(): StepValidation { + const script = useAppSelector(selectFirstBootScript); + let hasShebang = false; + if (script) { + hasShebang = script.split('\n')[0].startsWith('#!'); + } + const valid = !script || hasShebang; + return { + errors: { + script: valid ? '' : 'Missing shebang at first line, e.g. #!/bin/bash', + }, + disabledNext: !valid, + }; +} + export function useDetailsValidation(): StepValidation { const name = useAppSelector(selectBlueprintName); const description = useAppSelector(selectBlueprintDescription); diff --git a/src/test/Components/CreateImageWizard/steps/FirstBoot/Firstboot.test.tsx b/src/test/Components/CreateImageWizard/steps/FirstBoot/Firstboot.test.tsx index 3e65d1ac..0d4bd164 100644 --- a/src/test/Components/CreateImageWizard/steps/FirstBoot/Firstboot.test.tsx +++ b/src/test/Components/CreateImageWizard/steps/FirstBoot/Firstboot.test.tsx @@ -13,7 +13,7 @@ import { firstBootCreateBlueprintRequest, firstBootData, } from '../../../../fixtures/editMode'; -import { clickNext } from '../../../../testUtils'; +import { clickNext, getNextButton } from '../../../../testUtils'; import { blueprintRequest, clickRegisterLater, @@ -104,6 +104,22 @@ describe('First Boot step', () => { await goToFirstBootStep(); await screen.findByText('First boot configuration'); }); + + // test('should validate shebang', async () => { + // await renderCreateMode(); + // await goToFirstBootStep(); + // await openCodeEditor(); + // const editor = await screen.findByRole('textbox'); + // await userEvent.type(editor, 'echo "Hello, world!"'); + // expect(await screen.findByText('Missing shebang')).toBeInTheDocument(); + // expect(await getNextButton()).toBeDisabled(); + + // await userEvent.clear(editor); + // await userEvent.type(editor, '#!/bin/bash'); + // expect(screen.queryByText('Missing shebang')).not.toBeInTheDocument(); + // expect(await getNextButton()).toBeEnabled(); + // }); + // describe('validate first boot request ', () => { // test('should validate first boot request', async () => { // await renderCreateMode();