Firstboot: validate shebang is defined
This commit is contained in:
parent
6e138b5274
commit
dbfa934b38
4 changed files with 95 additions and 26 deletions
|
|
@ -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 (
|
||||
<WizardNavItem
|
||||
key={step.id}
|
||||
|
|
@ -216,10 +225,7 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
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={<CustomWizardFooter disableNext={false} />}
|
||||
navItem={customStatusNavItem}
|
||||
status={firstBootValidation.disabledNext ? 'error' : 'default'}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
disableNext={firstBootValidation.disabledNext}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<FirstBootStep />
|
||||
</WizardStep>
|
||||
|
|
@ -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={
|
||||
<CustomWizardFooter
|
||||
|
|
|
|||
|
|
@ -1,13 +1,23 @@
|
|||
import React from 'react';
|
||||
|
||||
import { CodeEditor, Language } from '@patternfly/react-code-editor';
|
||||
import { Text, Form, Title, Alert } from '@patternfly/react-core';
|
||||
import {
|
||||
Text,
|
||||
Form,
|
||||
FormGroup,
|
||||
FormHelperText,
|
||||
Title,
|
||||
Alert,
|
||||
HelperText,
|
||||
HelperTextItem,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../../../../store/hooks';
|
||||
import {
|
||||
selectFirstBootScript,
|
||||
setFirstBootScript,
|
||||
} from '../../../../store/wizardSlice';
|
||||
import { useFirstBootValidation } from '../../utilities/useValidation';
|
||||
|
||||
const detectScriptType = (scriptString: string): Language => {
|
||||
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 (
|
||||
<Form>
|
||||
|
|
@ -55,20 +66,31 @@ const FirstBootStep = () => {
|
|||
privacy.
|
||||
</Text>
|
||||
</Alert>
|
||||
<CodeEditor
|
||||
isUploadEnabled
|
||||
isDownloadEnabled
|
||||
isCopyEnabled
|
||||
isLanguageLabelVisible
|
||||
language={language}
|
||||
onCodeChange={(code) => dispatch(setFirstBootScript(code))}
|
||||
code={selectedScript}
|
||||
height="35vh"
|
||||
emptyStateButton={<span data-testid="firstboot_browse">Browse</span>}
|
||||
emptyStateLink={
|
||||
<span data-testid="firstboot_write_manual">Start from scratch</span>
|
||||
}
|
||||
/>
|
||||
<FormGroup>
|
||||
<CodeEditor
|
||||
isUploadEnabled
|
||||
isDownloadEnabled
|
||||
isCopyEnabled
|
||||
isLanguageLabelVisible
|
||||
language={language}
|
||||
onCodeChange={(code) => dispatch(setFirstBootScript(code))}
|
||||
code={selectedScript}
|
||||
height="35vh"
|
||||
emptyStateButton={<span data-testid="firstboot_browse">Browse</span>}
|
||||
emptyStateLink={
|
||||
<span data-testid="firstboot_write_manual">Start from scratch</span>
|
||||
}
|
||||
/>
|
||||
{errors.script && (
|
||||
<FormHelperText>
|
||||
<HelperText>
|
||||
<HelperTextItem variant="error" hasIcon>
|
||||
{errors.script}
|
||||
</HelperTextItem>
|
||||
</HelperText>
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormGroup>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue