Wizard: Add validation to ChippingInput

This adds step validation to ChippingInput, allowing to validate imported values.
This commit is contained in:
regexowl 2025-02-04 10:39:33 +01:00 committed by Klara Simickova
parent 6ec433f9d3
commit f11ab64262
8 changed files with 160 additions and 10 deletions

View file

@ -13,6 +13,8 @@ import {
import { PlusCircleIcon, TimesIcon } from '@patternfly/react-icons'; import { PlusCircleIcon, TimesIcon } from '@patternfly/react-icons';
import { UnknownAction } from 'redux'; import { UnknownAction } from 'redux';
import { StepValidation } from './utilities/useValidation';
import { useAppDispatch } from '../../store/hooks'; import { useAppDispatch } from '../../store/hooks';
type ChippingInputProps = { type ChippingInputProps = {
@ -24,6 +26,8 @@ type ChippingInputProps = {
item: string; item: string;
addAction: (value: string) => UnknownAction; addAction: (value: string) => UnknownAction;
removeAction: (value: string) => UnknownAction; removeAction: (value: string) => UnknownAction;
stepValidation: StepValidation;
fieldName: string;
}; };
const ChippingInput = ({ const ChippingInput = ({
@ -35,11 +39,13 @@ const ChippingInput = ({
item, item,
addAction, addAction,
removeAction, removeAction,
stepValidation,
fieldName,
}: ChippingInputProps) => { }: ChippingInputProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const [errorText, setErrorText] = useState(''); const [errorText, setErrorText] = useState(stepValidation.errors[fieldName]);
const onTextInputChange = ( const onTextInputChange = (
_event: React.FormEvent<HTMLInputElement>, _event: React.FormEvent<HTMLInputElement>,
@ -76,6 +82,11 @@ const ChippingInput = ({
addItem(value); addItem(value);
}; };
const handleRemoveItem = (e: React.MouseEvent, value: string) => {
dispatch(removeAction(value));
setErrorText('');
};
const handleClear = () => { const handleClear = () => {
setInputValue(''); setInputValue('');
setErrorText(''); setErrorText('');
@ -121,11 +132,7 @@ const ChippingInput = ({
className="pf-v5-u-mt-sm pf-v5-u-w-100" className="pf-v5-u-mt-sm pf-v5-u-w-100"
> >
{requiredList.map((item) => ( {requiredList.map((item) => (
<Chip <Chip key={item} isReadOnly>
key={item}
onClick={() => dispatch(removeAction(item))}
isReadOnly
>
{item} {item}
</Chip> </Chip>
))} ))}
@ -133,7 +140,7 @@ const ChippingInput = ({
)} )}
<ChipGroup numChips={20} className="pf-v5-u-mt-sm pf-v5-u-w-100"> <ChipGroup numChips={20} className="pf-v5-u-mt-sm pf-v5-u-w-100">
{list?.map((item) => ( {list?.map((item) => (
<Chip key={item} onClick={() => dispatch(removeAction(item))}> <Chip key={item} onClick={(e) => handleRemoveItem(e, item)}>
{item} {item}
</Chip> </Chip>
))} ))}

View file

@ -44,6 +44,8 @@ import {
useHostnameValidation, useHostnameValidation,
useKernelValidation, useKernelValidation,
useUsersValidation, useUsersValidation,
useTimezoneValidation,
useFirewallValidation,
} from './utilities/useValidation'; } from './utilities/useValidation';
import { import {
isAwsAccountIdValid, isAwsAccountIdValid,
@ -244,10 +246,14 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
// Filesystem // Filesystem
const [filesystemPristine, setFilesystemPristine] = useState(true); const [filesystemPristine, setFilesystemPristine] = useState(true);
const fileSystemValidation = useFilesystemValidation(); const fileSystemValidation = useFilesystemValidation();
// Timezone
const timezoneValidation = useTimezoneValidation();
// Hostname // Hostname
const hostnameValidation = useHostnameValidation(); const hostnameValidation = useHostnameValidation();
// Kernel // Kernel
const kernelValidation = useKernelValidation(); const kernelValidation = useKernelValidation();
// Firewall
const firewallValidation = useFirewallValidation();
// Firstboot // Firstboot
const firstBootValidation = useFirstBootValidation(); const firstBootValidation = useFirstBootValidation();
// Details // Details
@ -505,8 +511,12 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
key="wizard-timezone" key="wizard-timezone"
navItem={customStatusNavItem} navItem={customStatusNavItem}
isHidden={!isTimezoneEnabled} isHidden={!isTimezoneEnabled}
status={timezoneValidation.disabledNext ? 'error' : 'default'}
footer={ footer={
<CustomWizardFooter disableNext={false} optional={true} /> <CustomWizardFooter
disableNext={timezoneValidation.disabledNext}
optional={true}
/>
} }
> >
<TimezoneStep /> <TimezoneStep />
@ -561,8 +571,12 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
key="wizard-firewall" key="wizard-firewall"
navItem={customStatusNavItem} navItem={customStatusNavItem}
isHidden={!isFirewallEnabled} isHidden={!isFirewallEnabled}
status={firewallValidation.disabledNext ? 'error' : 'default'}
footer={ footer={
<CustomWizardFooter disableNext={false} optional={true} /> <CustomWizardFooter
disableNext={firewallValidation.disabledNext}
optional={true}
/>
} }
> >
<FirewallStep /> <FirewallStep />

View file

@ -9,11 +9,14 @@ import {
selectFirewall, selectFirewall,
} from '../../../../../store/wizardSlice'; } from '../../../../../store/wizardSlice';
import ChippingInput from '../../../ChippingInput'; import ChippingInput from '../../../ChippingInput';
import { useFirewallValidation } from '../../../utilities/useValidation';
import { isPortValid } from '../../../validators'; import { isPortValid } from '../../../validators';
const PortsInput = () => { const PortsInput = () => {
const ports = useAppSelector(selectFirewall).ports; const ports = useAppSelector(selectFirewall).ports;
const stepValidation = useFirewallValidation();
return ( return (
<FormGroup label="Ports"> <FormGroup label="Ports">
<ChippingInput <ChippingInput
@ -24,6 +27,8 @@ const PortsInput = () => {
item="Port" item="Port"
addAction={addPort} addAction={addPort}
removeAction={removePort} removeAction={removePort}
stepValidation={stepValidation}
fieldName="ports"
/> />
</FormGroup> </FormGroup>
); );

View file

@ -11,12 +11,15 @@ import {
selectFirewall, selectFirewall,
} from '../../../../../store/wizardSlice'; } from '../../../../../store/wizardSlice';
import ChippingInput from '../../../ChippingInput'; import ChippingInput from '../../../ChippingInput';
import { useFirewallValidation } from '../../../utilities/useValidation';
import { isServiceValid } from '../../../validators'; import { isServiceValid } from '../../../validators';
const Services = () => { const Services = () => {
const disabledServices = useAppSelector(selectFirewall).services.disabled; const disabledServices = useAppSelector(selectFirewall).services.disabled;
const enabledServices = useAppSelector(selectFirewall).services.enabled; const enabledServices = useAppSelector(selectFirewall).services.enabled;
const stepValidation = useFirewallValidation();
return ( return (
<> <>
<FormGroup label="Disabled services"> <FormGroup label="Disabled services">
@ -28,6 +31,8 @@ const Services = () => {
item="Disabled service" item="Disabled service"
addAction={addDisabledFirewallService} addAction={addDisabledFirewallService}
removeAction={removeDisabledFirewallService} removeAction={removeDisabledFirewallService}
stepValidation={stepValidation}
fieldName="disabledServices"
/> />
</FormGroup> </FormGroup>
<FormGroup label="Enabled services"> <FormGroup label="Enabled services">
@ -39,6 +44,8 @@ const Services = () => {
item="Enabled service" item="Enabled service"
addAction={addEnabledFirewallService} addAction={addEnabledFirewallService}
removeAction={removeEnabledFirewallService} removeAction={removeEnabledFirewallService}
stepValidation={stepValidation}
fieldName="enabledServices"
/> />
</FormGroup> </FormGroup>
</> </>

View file

@ -12,11 +12,14 @@ import {
selectKernel, selectKernel,
} from '../../../../../store/wizardSlice'; } from '../../../../../store/wizardSlice';
import ChippingInput from '../../../ChippingInput'; import ChippingInput from '../../../ChippingInput';
import { useKernelValidation } from '../../../utilities/useValidation';
import { isKernelArgumentValid } from '../../../validators'; import { isKernelArgumentValid } from '../../../validators';
const KernelArguments = () => { const KernelArguments = () => {
const kernelAppend = useAppSelector(selectKernel).append; const kernelAppend = useAppSelector(selectKernel).append;
const stepValidation = useKernelValidation();
const release = useAppSelector(selectDistribution); const release = useAppSelector(selectDistribution);
const complianceProfileID = useAppSelector(selectComplianceProfileID); const complianceProfileID = useAppSelector(selectComplianceProfileID);
@ -46,6 +49,8 @@ const KernelArguments = () => {
item="Kernel argument" item="Kernel argument"
addAction={addKernelArg} addAction={addKernelArg}
removeAction={removeKernelArg} removeAction={removeKernelArg}
stepValidation={stepValidation}
fieldName="kernelAppend"
/> />
</FormGroup> </FormGroup>
); );

View file

@ -9,11 +9,14 @@ import {
selectNtpServers, selectNtpServers,
} from '../../../../../store/wizardSlice'; } from '../../../../../store/wizardSlice';
import ChippingInput from '../../../ChippingInput'; import ChippingInput from '../../../ChippingInput';
import { useTimezoneValidation } from '../../../utilities/useValidation';
import { isNtpServerValid } from '../../../validators'; import { isNtpServerValid } from '../../../validators';
const NtpServersInput = () => { const NtpServersInput = () => {
const ntpServers = useAppSelector(selectNtpServers); const ntpServers = useAppSelector(selectNtpServers);
const stepValidation = useTimezoneValidation();
return ( return (
<FormGroup isRequired={false} label="NTP servers"> <FormGroup isRequired={false} label="NTP servers">
<ChippingInput <ChippingInput
@ -24,6 +27,8 @@ const NtpServersInput = () => {
item="NTP server" item="NTP server"
addAction={addNtpServer} addAction={addNtpServer}
removeAction={removeNtpServer} removeAction={removeNtpServer}
stepValidation={stepValidation}
fieldName="ntpServers"
/> />
</FormGroup> </FormGroup>
); );

View file

@ -22,6 +22,8 @@ import {
selectUsers, selectUsers,
selectUserPasswordByIndex, selectUserPasswordByIndex,
selectUserSshKeyByIndex, selectUserSshKeyByIndex,
selectNtpServers,
selectFirewall,
} from '../../../store/wizardSlice'; } from '../../../store/wizardSlice';
import { import {
getDuplicateMountPoints, getDuplicateMountPoints,
@ -33,6 +35,10 @@ import {
isKernelNameValid, isKernelNameValid,
isUserNameValid, isUserNameValid,
isSshKeyValid, isSshKeyValid,
isNtpServerValid,
isKernelArgumentValid,
isPortValid,
isServiceValid,
} from '../validators'; } from '../validators';
export type StepValidation = { export type StepValidation = {
@ -46,16 +52,20 @@ export function useIsBlueprintValid(): boolean {
const registration = useRegistrationValidation(); const registration = useRegistrationValidation();
const filesystem = useFilesystemValidation(); const filesystem = useFilesystemValidation();
const snapshot = useSnapshotValidation(); const snapshot = useSnapshotValidation();
const timezone = useTimezoneValidation();
const hostname = useHostnameValidation(); const hostname = useHostnameValidation();
const kernel = useKernelValidation(); const kernel = useKernelValidation();
const firewall = useFirewallValidation();
const firstBoot = useFirstBootValidation(); const firstBoot = useFirstBootValidation();
const details = useDetailsValidation(); const details = useDetailsValidation();
return ( return (
!registration.disabledNext && !registration.disabledNext &&
!filesystem.disabledNext && !filesystem.disabledNext &&
!snapshot.disabledNext && !snapshot.disabledNext &&
!timezone.disabledNext &&
!hostname.disabledNext && !hostname.disabledNext &&
!kernel.disabledNext && !kernel.disabledNext &&
!firewall.disabledNext &&
!firstBoot.disabledNext && !firstBoot.disabledNext &&
!details.disabledNext !details.disabledNext
); );
@ -130,6 +140,29 @@ export function useSnapshotValidation(): StepValidation {
return { errors: {}, disabledNext: false }; 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 { export function useFirstBootValidation(): StepValidation {
const script = useAppSelector(selectFirstBootScript); const script = useAppSelector(selectFirstBootScript);
let hasShebang = false; let hasShebang = false;
@ -174,9 +207,83 @@ export function useKernelValidation(): StepValidation {
disabledNext: true, 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 }; 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 { export function useUsersValidation(): StepValidation {
const index = 0; const index = 0;
const userNameSelector = selectUserNameByIndex(index); const userNameSelector = selectUserNameByIndex(index);

View file

@ -129,7 +129,7 @@ export const isKernelArgumentValid = (arg: string) => {
return true; return true;
} }
return /^[a-zA-Z0-9=-_,"']*$/.test(arg); return /^[a-zA-Z0-9=-_,."']*$/.test(arg);
}; };
export const isPortValid = (port: string) => { export const isPortValid = (port: string) => {