Wizard: Add validation to ChippingInput
This adds step validation to ChippingInput, allowing to validate imported values.
This commit is contained in:
parent
6ec433f9d3
commit
f11ab64262
8 changed files with 160 additions and 10 deletions
|
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue