fix(input): allow clearing number inputs [EE-6714] (#11187)

pull/11238/head
Ali 2024-02-21 10:43:28 +13:00 committed by GitHub
parent 4e95139909
commit 1cdd3fdfe2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 60 additions and 29 deletions

View File

@ -0,0 +1,19 @@
import { NumberSchema, number } from 'yup';
/**
* Returns a Yup schema for a number that can also be NaN.
*
* This function is a workaround for a known issue in Yup where it throws type errors
* when the number input is empty, having a value NaN. Yup doesn't like NaN values.
* More details can be found in these GitHub issues:
* https://github.com/jquense/yup/issues/1330
* https://github.com/jquense/yup/issues/211
*
* @param errorMessage The custom error message to display when the value is required.
* @returns A Yup number schema with a custom type error message.
*/
export function nanNumberSchema(
errorMessage = 'Value is required'
): NumberSchema {
return number().typeError(errorMessage);
}

View File

@ -12,6 +12,8 @@ export const InputWithRef = forwardRef<
export function Input({ export function Input({
className, className,
mRef: ref, mRef: ref,
value,
type,
...props ...props
}: InputHTMLAttributes<HTMLInputElement> & { }: InputHTMLAttributes<HTMLInputElement> & {
mRef?: Ref<HTMLInputElement>; mRef?: Ref<HTMLInputElement>;
@ -20,6 +22,8 @@ export function Input({
<input <input
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...props} {...props}
type={type}
value={type === 'number' && Number.isNaN(value) ? '' : value} // avoid the `"NaN" cannot be parsed, or is out of range.` error for an empty number input
ref={ref} ref={ref}
className={clsx('form-control', className)} className={clsx('form-control', className)}
/> />

View File

@ -20,7 +20,7 @@ export function SliderWithInput({
visibleTooltip?: boolean; visibleTooltip?: boolean;
}) { }) {
return ( return (
<div className="flex items-center gap-4"> <div className="flex items-center gap-6">
{max && ( {max && (
<div className="mr-2 flex-1"> <div className="mr-2 flex-1">
<Slider <Slider
@ -41,9 +41,7 @@ export function SliderWithInput({
min="0" min="0"
max={max} max={max}
value={value} value={value}
onChange={({ target: { valueAsNumber: value } }) => onChange={(e) => onChange(e.target.valueAsNumber)}
onChange(Number.isNaN(value) ? 0 : value)
}
className="w-32" className="w-32"
data-cy={`${dataCy}Input`} data-cy={`${dataCy}Input`}
/> />

View File

@ -1,8 +1,9 @@
import { FormikErrors } from 'formik'; import { FormikErrors } from 'formik';
import { number, object, SchemaOf } from 'yup'; import { object, SchemaOf } from 'yup';
import { useSystemLimits } from '@/react/docker/proxy/queries/useInfo'; import { useSystemLimits } from '@/react/docker/proxy/queries/useInfo';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { nanNumberSchema } from '@/react-tools/yup-schemas';
import { FormControl } from '@@/form-components/FormControl'; import { FormControl } from '@@/form-components/FormControl';
import { FormSection } from '@@/form-components/FormSection'; import { FormSection } from '@@/form-components/FormSection';
@ -94,15 +95,15 @@ export function resourcesValidation({
maxCpu?: number; maxCpu?: number;
} = {}): SchemaOf<Values> { } = {}): SchemaOf<Values> {
return object({ return object({
reservation: number() reservation: nanNumberSchema()
.min(0) .min(0)
.max(maxMemory, `Value must be between 0 and ${maxMemory}`) .max(maxMemory, `Value must be between 0 and ${maxMemory}`)
.default(0), .default(0),
limit: number() limit: nanNumberSchema()
.min(0) .min(0)
.max(maxMemory, `Value must be between 0 and ${maxMemory}`) .max(maxMemory, `Value must be between 0 and ${maxMemory}`)
.default(0), .default(0),
cpu: number() cpu: nanNumberSchema()
.min(0) .min(0)
.max(maxCpu, `Value must be between 0 and ${maxCpu}`) .max(maxCpu, `Value must be between 0 and ${maxCpu}`)
.default(0), .default(0),

View File

@ -32,7 +32,7 @@ export function ScaleForm({
type="number" type="number"
min={0} min={0}
step={1} step={1}
value={values.replicas} value={Number.isNaN(values.replicas) ? '' : values.replicas}
onKeyUp={(event) => { onKeyUp={(event) => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
onClose(); onClose();
@ -48,6 +48,11 @@ export function ScaleForm({
<Button color="none" icon={X} onClick={() => onClose()} /> <Button color="none" icon={X} onClick={() => onClose()} />
<LoadingButton <LoadingButton
isLoading={mutation.isLoading} isLoading={mutation.isLoading}
disabled={
values.replicas === service.Replicas ||
values.replicas < 0 ||
Number.isNaN(values.replicas)
}
loadingText="Scaling..." loadingText="Scaling..."
color="none" color="none"
icon={CheckSquare} icon={CheckSquare}

View File

@ -1,5 +1,7 @@
import { SchemaOf, array, object, boolean, string, mixed, number } from 'yup'; import { SchemaOf, array, object, boolean, string, mixed, number } from 'yup';
import { nanNumberSchema } from '@/react-tools/yup-schemas';
import { ServiceFormValues, ServicePort } from './types'; import { ServiceFormValues, ServicePort } from './types';
import { prependWithSlash } from './utils'; import { prependWithSlash } from './utils';
@ -53,9 +55,8 @@ export function kubeServicesValidation(
Selector: object(), Selector: object(),
Ports: array( Ports: array(
object({ object({
port: number() port: nanNumberSchema('Service port number is required.')
.required('Service port number is required.') .required('Service port number is required.')
.typeError('Service port number is required.')
.min(1, 'Service port number must be inside the range 1-65535.') .min(1, 'Service port number must be inside the range 1-65535.')
.max(65535, 'Service port number must be inside the range 1-65535.') .max(65535, 'Service port number must be inside the range 1-65535.')
.test( .test(
@ -86,9 +87,8 @@ export function kubeServicesValidation(
return duplicateServicePortCount <= 1; return duplicateServicePortCount <= 1;
} }
), ),
targetPort: number() targetPort: nanNumberSchema('Container port number is required.')
.required('Container port number is required.') .required('Container port number is required.')
.typeError('Container port number is required.')
.min(1, 'Container port number must be inside the range 1-65535.') .min(1, 'Container port number must be inside the range 1-65535.')
.max( .max(
65535, 65535,

View File

@ -64,7 +64,7 @@ export function AutoScalingFormSection({
onChange={(e) => onChange={(e) =>
onChange({ onChange({
...values, ...values,
minReplicas: Number(e.target.value) || 0, minReplicas: e.target.valueAsNumber,
}) })
} }
data-cy="k8sAppCreate-autoScaleMin" data-cy="k8sAppCreate-autoScaleMin"
@ -83,7 +83,7 @@ export function AutoScalingFormSection({
onChange={(e) => onChange={(e) =>
onChange({ onChange({
...values, ...values,
maxReplicas: Number(e.target.value) || 1, maxReplicas: e.target.valueAsNumber,
}) })
} }
data-cy="k8sAppCreate-autoScaleMax" data-cy="k8sAppCreate-autoScaleMax"
@ -107,7 +107,7 @@ export function AutoScalingFormSection({
onChange={(e) => onChange={(e) =>
onChange({ onChange({
...values, ...values,
targetCpuUtilizationPercentage: Number(e.target.value) || 1, targetCpuUtilizationPercentage: e.target.valueAsNumber,
}) })
} }
data-cy="k8sAppCreate-targetCPUInput" data-cy="k8sAppCreate-targetCPUInput"

View File

@ -4,7 +4,6 @@ import { round } from 'lodash';
import { FormControl } from '@@/form-components/FormControl'; import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input'; import { Input } from '@@/form-components/Input';
import { TextTip } from '@@/Tip/TextTip'; import { TextTip } from '@@/Tip/TextTip';
import { FormError } from '@@/form-components/FormError';
import { ReplicaCountFormValues } from './types'; import { ReplicaCountFormValues } from './types';
@ -31,16 +30,18 @@ export function ReplicationFormSection({
return ( return (
<> <>
<FormControl label="Instance count" required> <FormControl
label="Instance count"
required
errors={errors?.replicaCount}
>
<Input <Input
type="number" type="number"
min="0" min="0"
max="9999" max="9999"
value={values.replicaCount} value={values.replicaCount}
disabled={!supportScalableReplicaDeployment} disabled={!supportScalableReplicaDeployment}
onChange={(e) => onChange={(e) => onChange({ replicaCount: e.target.valueAsNumber })}
onChange({ replicaCount: e.target.valueAsNumber || 0 })
}
className="w-1/4" className="w-1/4"
data-cy="k8sAppCreate-replicaCountInput" data-cy="k8sAppCreate-replicaCountInput"
/> />
@ -54,7 +55,6 @@ export function ReplicationFormSection({
<b>{memoryLimit * values.replicaCount} MB</b> of memory. <b>{memoryLimit * values.replicaCount} MB</b> of memory.
</TextTip> </TextTip>
)} )}
{errors?.replicaCount && <FormError>{errors.replicaCount}</FormError>}
</> </>
); );
} }

View File

@ -1,4 +1,6 @@
import { SchemaOf, number, object } from 'yup'; import { SchemaOf, object } from 'yup';
import { nanNumberSchema } from '@/react-tools/yup-schemas';
import { ReplicaCountFormValues } from './types'; import { ReplicaCountFormValues } from './types';
@ -19,7 +21,7 @@ export function replicationValidation(
supportScalableReplicaDeployment, supportScalableReplicaDeployment,
} = validationData || {}; } = validationData || {};
return object({ return object({
replicaCount: number() replicaCount: nanNumberSchema('Instance count is required')
.min(0, 'Instance count must be greater than or equal to 0.') .min(0, 'Instance count must be greater than or equal to 0.')
.test( .test(
'overflow', 'overflow',

View File

@ -1,6 +1,7 @@
import { SchemaOf, TestContext, number, object } from 'yup'; import { SchemaOf, TestContext, number, object } from 'yup';
import KubernetesResourceReservationHelper from '@/kubernetes/helpers/resourceReservationHelper'; import KubernetesResourceReservationHelper from '@/kubernetes/helpers/resourceReservationHelper';
import { nanNumberSchema } from '@/react-tools/yup-schemas';
import { ResourceQuotaFormValues } from './types'; import { ResourceQuotaFormValues } from './types';
@ -22,8 +23,8 @@ export function resourceReservationValidation(
validationData?: ValidationData validationData?: ValidationData
): SchemaOf<ResourceQuotaFormValues> { ): SchemaOf<ResourceQuotaFormValues> {
return object().shape({ return object().shape({
memoryLimit: number() memoryLimit: nanNumberSchema()
.min(0) .min(0, 'Value must be greater than or equal to 0')
.test( .test(
'exhaused', 'exhaused',
`The memory capacity for this namespace has been exhausted, so you cannot deploy the application.${ `The memory capacity for this namespace has been exhausted, so you cannot deploy the application.${

View File

@ -1,4 +1,6 @@
import { SchemaOf, object, string, boolean, number } from 'yup'; import { SchemaOf, object, string, boolean } from 'yup';
import { nanNumberSchema } from '@/react-tools/yup-schemas';
import { isValidUrl } from '@@/form-components/validate-url'; import { isValidUrl } from '@@/form-components/validate-url';
@ -17,8 +19,7 @@ export function validation(): SchemaOf<FormValues> {
hideFileUpload: boolean().required(), hideFileUpload: boolean().required(),
requireNoteOnApplications: boolean().required(), requireNoteOnApplications: boolean().required(),
hideStacksFunctionality: boolean().required(), hideStacksFunctionality: boolean().required(),
minApplicationNoteLength: number() minApplicationNoteLength: nanNumberSchema('Must be a number')
.typeError('Must be a number')
.default(0) .default(0)
.when('requireNoteOnApplications', { .when('requireNoteOnApplications', {
is: true, is: true,