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 { 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<HTMLInputElement>,
@ -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) => (
<Chip
key={item}
onClick={() => dispatch(removeAction(item))}
isReadOnly
>
<Chip key={item} isReadOnly>
{item}
</Chip>
))}
@ -133,7 +140,7 @@ const ChippingInput = ({
)}
<ChipGroup numChips={20} className="pf-v5-u-mt-sm pf-v5-u-w-100">
{list?.map((item) => (
<Chip key={item} onClick={() => dispatch(removeAction(item))}>
<Chip key={item} onClick={(e) => handleRemoveItem(e, item)}>
{item}
</Chip>
))}

View file

@ -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={
<CustomWizardFooter disableNext={false} optional={true} />
<CustomWizardFooter
disableNext={timezoneValidation.disabledNext}
optional={true}
/>
}
>
<TimezoneStep />
@ -561,8 +571,12 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
key="wizard-firewall"
navItem={customStatusNavItem}
isHidden={!isFirewallEnabled}
status={firewallValidation.disabledNext ? 'error' : 'default'}
footer={
<CustomWizardFooter disableNext={false} optional={true} />
<CustomWizardFooter
disableNext={firewallValidation.disabledNext}
optional={true}
/>
}
>
<FirewallStep />

View file

@ -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 (
<FormGroup label="Ports">
<ChippingInput
@ -24,6 +27,8 @@ const PortsInput = () => {
item="Port"
addAction={addPort}
removeAction={removePort}
stepValidation={stepValidation}
fieldName="ports"
/>
</FormGroup>
);

View file

@ -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 (
<>
<FormGroup label="Disabled services">
@ -28,6 +31,8 @@ const Services = () => {
item="Disabled service"
addAction={addDisabledFirewallService}
removeAction={removeDisabledFirewallService}
stepValidation={stepValidation}
fieldName="disabledServices"
/>
</FormGroup>
<FormGroup label="Enabled services">
@ -39,6 +44,8 @@ const Services = () => {
item="Enabled service"
addAction={addEnabledFirewallService}
removeAction={removeEnabledFirewallService}
stepValidation={stepValidation}
fieldName="enabledServices"
/>
</FormGroup>
</>

View file

@ -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"
/>
</FormGroup>
);

View file

@ -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 (
<FormGroup isRequired={false} label="NTP servers">
<ChippingInput
@ -24,6 +27,8 @@ const NtpServersInput = () => {
item="NTP server"
addAction={addNtpServer}
removeAction={removeNtpServer}
stepValidation={stepValidation}
fieldName="ntpServers"
/>
</FormGroup>
);

View file

@ -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);

View file

@ -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) => {