diff --git a/src/Components/CreateImageWizard/ChippingInput.tsx b/src/Components/CreateImageWizard/ChippingInput.tsx index ba53683b..21ac0f51 100644 --- a/src/Components/CreateImageWizard/ChippingInput.tsx +++ b/src/Components/CreateImageWizard/ChippingInput.tsx @@ -13,6 +13,8 @@ import { import { PlusCircleIcon, TimesIcon } from '@patternfly/react-icons'; import { UnknownAction } from 'redux'; +import { StepValidation } from './utilities/useValidation'; + import { useAppDispatch } from '../../store/hooks'; type ChippingInputProps = { @@ -24,6 +26,8 @@ type ChippingInputProps = { item: string; addAction: (value: string) => UnknownAction; removeAction: (value: string) => UnknownAction; + stepValidation: StepValidation; + fieldName: string; }; const ChippingInput = ({ @@ -35,11 +39,13 @@ const ChippingInput = ({ item, addAction, removeAction, + stepValidation, + fieldName, }: ChippingInputProps) => { const dispatch = useAppDispatch(); const [inputValue, setInputValue] = useState(''); - const [errorText, setErrorText] = useState(''); + const [errorText, setErrorText] = useState(stepValidation.errors[fieldName]); const onTextInputChange = ( _event: React.FormEvent, @@ -76,6 +82,11 @@ const ChippingInput = ({ addItem(value); }; + const handleRemoveItem = (e: React.MouseEvent, value: string) => { + dispatch(removeAction(value)); + setErrorText(''); + }; + const handleClear = () => { setInputValue(''); setErrorText(''); @@ -121,11 +132,7 @@ const ChippingInput = ({ className="pf-v5-u-mt-sm pf-v5-u-w-100" > {requiredList.map((item) => ( - dispatch(removeAction(item))} - isReadOnly - > + {item} ))} @@ -133,7 +140,7 @@ const ChippingInput = ({ )} {list?.map((item) => ( - dispatch(removeAction(item))}> + handleRemoveItem(e, item)}> {item} ))} diff --git a/src/Components/CreateImageWizard/CreateImageWizard.tsx b/src/Components/CreateImageWizard/CreateImageWizard.tsx index 385a7a80..76473ae9 100644 --- a/src/Components/CreateImageWizard/CreateImageWizard.tsx +++ b/src/Components/CreateImageWizard/CreateImageWizard.tsx @@ -44,6 +44,8 @@ import { useHostnameValidation, useKernelValidation, useUsersValidation, + useTimezoneValidation, + useFirewallValidation, } from './utilities/useValidation'; import { isAwsAccountIdValid, @@ -244,10 +246,14 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => { // Filesystem const [filesystemPristine, setFilesystemPristine] = useState(true); const fileSystemValidation = useFilesystemValidation(); + // Timezone + const timezoneValidation = useTimezoneValidation(); // Hostname const hostnameValidation = useHostnameValidation(); // Kernel const kernelValidation = useKernelValidation(); + // Firewall + const firewallValidation = useFirewallValidation(); // Firstboot const firstBootValidation = useFirstBootValidation(); // Details @@ -505,8 +511,12 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => { key="wizard-timezone" navItem={customStatusNavItem} isHidden={!isTimezoneEnabled} + status={timezoneValidation.disabledNext ? 'error' : 'default'} footer={ - + } > @@ -561,8 +571,12 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => { key="wizard-firewall" navItem={customStatusNavItem} isHidden={!isFirewallEnabled} + status={firewallValidation.disabledNext ? 'error' : 'default'} footer={ - + } > diff --git a/src/Components/CreateImageWizard/steps/Firewall/components/PortsInput.tsx b/src/Components/CreateImageWizard/steps/Firewall/components/PortsInput.tsx index 0ffafe20..411afd29 100644 --- a/src/Components/CreateImageWizard/steps/Firewall/components/PortsInput.tsx +++ b/src/Components/CreateImageWizard/steps/Firewall/components/PortsInput.tsx @@ -9,11 +9,14 @@ import { selectFirewall, } from '../../../../../store/wizardSlice'; import ChippingInput from '../../../ChippingInput'; +import { useFirewallValidation } from '../../../utilities/useValidation'; import { isPortValid } from '../../../validators'; const PortsInput = () => { const ports = useAppSelector(selectFirewall).ports; + const stepValidation = useFirewallValidation(); + return ( { item="Port" addAction={addPort} removeAction={removePort} + stepValidation={stepValidation} + fieldName="ports" /> ); diff --git a/src/Components/CreateImageWizard/steps/Firewall/components/Services.tsx b/src/Components/CreateImageWizard/steps/Firewall/components/Services.tsx index 76327060..5fa3505d 100644 --- a/src/Components/CreateImageWizard/steps/Firewall/components/Services.tsx +++ b/src/Components/CreateImageWizard/steps/Firewall/components/Services.tsx @@ -11,12 +11,15 @@ import { selectFirewall, } from '../../../../../store/wizardSlice'; import ChippingInput from '../../../ChippingInput'; +import { useFirewallValidation } from '../../../utilities/useValidation'; import { isServiceValid } from '../../../validators'; const Services = () => { const disabledServices = useAppSelector(selectFirewall).services.disabled; const enabledServices = useAppSelector(selectFirewall).services.enabled; + const stepValidation = useFirewallValidation(); + return ( <> @@ -28,6 +31,8 @@ const Services = () => { item="Disabled service" addAction={addDisabledFirewallService} removeAction={removeDisabledFirewallService} + stepValidation={stepValidation} + fieldName="disabledServices" /> @@ -39,6 +44,8 @@ const Services = () => { item="Enabled service" addAction={addEnabledFirewallService} removeAction={removeEnabledFirewallService} + stepValidation={stepValidation} + fieldName="enabledServices" /> diff --git a/src/Components/CreateImageWizard/steps/Kernel/components/KernelArguments.tsx b/src/Components/CreateImageWizard/steps/Kernel/components/KernelArguments.tsx index 0b6c9ee6..8068185f 100644 --- a/src/Components/CreateImageWizard/steps/Kernel/components/KernelArguments.tsx +++ b/src/Components/CreateImageWizard/steps/Kernel/components/KernelArguments.tsx @@ -12,11 +12,14 @@ import { selectKernel, } from '../../../../../store/wizardSlice'; import ChippingInput from '../../../ChippingInput'; +import { useKernelValidation } from '../../../utilities/useValidation'; import { isKernelArgumentValid } from '../../../validators'; const KernelArguments = () => { const kernelAppend = useAppSelector(selectKernel).append; + const stepValidation = useKernelValidation(); + const release = useAppSelector(selectDistribution); const complianceProfileID = useAppSelector(selectComplianceProfileID); @@ -46,6 +49,8 @@ const KernelArguments = () => { item="Kernel argument" addAction={addKernelArg} removeAction={removeKernelArg} + stepValidation={stepValidation} + fieldName="kernelAppend" /> ); diff --git a/src/Components/CreateImageWizard/steps/Timezone/components/NtpServersInput.tsx b/src/Components/CreateImageWizard/steps/Timezone/components/NtpServersInput.tsx index 827df37e..9d073813 100644 --- a/src/Components/CreateImageWizard/steps/Timezone/components/NtpServersInput.tsx +++ b/src/Components/CreateImageWizard/steps/Timezone/components/NtpServersInput.tsx @@ -9,11 +9,14 @@ import { selectNtpServers, } from '../../../../../store/wizardSlice'; import ChippingInput from '../../../ChippingInput'; +import { useTimezoneValidation } from '../../../utilities/useValidation'; import { isNtpServerValid } from '../../../validators'; const NtpServersInput = () => { const ntpServers = useAppSelector(selectNtpServers); + const stepValidation = useTimezoneValidation(); + return ( { item="NTP server" addAction={addNtpServer} removeAction={removeNtpServer} + stepValidation={stepValidation} + fieldName="ntpServers" /> ); diff --git a/src/Components/CreateImageWizard/utilities/useValidation.tsx b/src/Components/CreateImageWizard/utilities/useValidation.tsx index 433039ee..7f6e5dfa 100644 --- a/src/Components/CreateImageWizard/utilities/useValidation.tsx +++ b/src/Components/CreateImageWizard/utilities/useValidation.tsx @@ -22,6 +22,8 @@ import { selectUsers, selectUserPasswordByIndex, selectUserSshKeyByIndex, + selectNtpServers, + selectFirewall, } from '../../../store/wizardSlice'; import { getDuplicateMountPoints, @@ -33,6 +35,10 @@ import { isKernelNameValid, isUserNameValid, isSshKeyValid, + isNtpServerValid, + isKernelArgumentValid, + isPortValid, + isServiceValid, } from '../validators'; export type StepValidation = { @@ -46,16 +52,20 @@ export function useIsBlueprintValid(): boolean { const registration = useRegistrationValidation(); const filesystem = useFilesystemValidation(); const snapshot = useSnapshotValidation(); + const timezone = useTimezoneValidation(); const hostname = useHostnameValidation(); const kernel = useKernelValidation(); + const firewall = useFirewallValidation(); const firstBoot = useFirstBootValidation(); const details = useDetailsValidation(); return ( !registration.disabledNext && !filesystem.disabledNext && !snapshot.disabledNext && + !timezone.disabledNext && !hostname.disabledNext && !kernel.disabledNext && + !firewall.disabledNext && !firstBoot.disabledNext && !details.disabledNext ); @@ -130,6 +140,29 @@ export function useSnapshotValidation(): StepValidation { return { errors: {}, disabledNext: false }; } +export function useTimezoneValidation(): StepValidation { + const ntpServers = useAppSelector(selectNtpServers); + + if (ntpServers) { + const invalidServers = []; + + for (const server of ntpServers) { + if (!isNtpServerValid(server)) { + invalidServers.push(server); + } + } + + if (invalidServers.length > 0) { + return { + errors: { ntpServers: `Invalid ntpServers: ${invalidServers}` }, + disabledNext: true, + }; + } + } + + return { errors: {}, disabledNext: false }; +} + export function useFirstBootValidation(): StepValidation { const script = useAppSelector(selectFirstBootScript); let hasShebang = false; @@ -174,9 +207,83 @@ export function useKernelValidation(): StepValidation { disabledNext: true, }; } + + if (kernel.append.length > 0) { + const invalidArgs = []; + + for (const arg of kernel.append) { + if (!isKernelArgumentValid(arg)) { + invalidArgs.push(arg); + } + } + + if (invalidArgs.length > 0) { + return { + errors: { kernelAppend: `Invalid kernel arguments: ${invalidArgs}` }, + disabledNext: true, + }; + } + } + return { errors: {}, disabledNext: false }; } +export function useFirewallValidation(): StepValidation { + const firewall = useAppSelector(selectFirewall); + const errors = {}; + const invalidPorts = []; + const invalidDisabled = []; + const invalidEnabled = []; + + if (firewall.ports.length > 0) { + for (const port of firewall.ports) { + if (!isPortValid(port)) { + invalidPorts.push(port); + } + } + + if (invalidPorts.length > 0) { + Object.assign(errors, { ports: `Invalid ports: ${invalidPorts}` }); + } + } + + if (firewall.services.disabled.length > 0) { + for (const s of firewall.services.disabled) { + if (!isServiceValid(s)) { + invalidDisabled.push(s); + } + } + + if (invalidDisabled.length > 0) { + Object.assign(errors, { + disabledServices: `Invalid disabled services: ${invalidDisabled}`, + }); + } + } + + if (firewall.services.enabled.length > 0) { + for (const s of firewall.services.enabled) { + if (!isServiceValid(s)) { + invalidEnabled.push(s); + } + } + + if (invalidEnabled.length > 0) { + Object.assign(errors, { + enabledServices: `Invalid enabled services: ${invalidEnabled}`, + }); + } + } + + return { + errors, + disabledNext: + invalidPorts.length > 0 || + invalidDisabled.length > 0 || + invalidEnabled.length > 0, + }; +} + export function useUsersValidation(): StepValidation { const index = 0; const userNameSelector = selectUserNameByIndex(index); diff --git a/src/Components/CreateImageWizard/validators.ts b/src/Components/CreateImageWizard/validators.ts index f667f858..49d9c34a 100644 --- a/src/Components/CreateImageWizard/validators.ts +++ b/src/Components/CreateImageWizard/validators.ts @@ -129,7 +129,7 @@ export const isKernelArgumentValid = (arg: string) => { return true; } - return /^[a-zA-Z0-9=-_,"']*$/.test(arg); + return /^[a-zA-Z0-9=-_,."']*$/.test(arg); }; export const isPortValid = (port: string) => {