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({
className,
mRef: ref,
value,
type,
...props
}: InputHTMLAttributes<HTMLInputElement> & {
mRef?: Ref<HTMLInputElement>;
@ -20,6 +22,8 @@ export function Input({
<input
// eslint-disable-next-line react/jsx-props-no-spreading
{...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}
className={clsx('form-control', className)}
/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import { round } from 'lodash';
import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input';
import { TextTip } from '@@/Tip/TextTip';
import { FormError } from '@@/form-components/FormError';
import { ReplicaCountFormValues } from './types';
@ -31,16 +30,18 @@ export function ReplicationFormSection({
return (
<>
<FormControl label="Instance count" required>
<FormControl
label="Instance count"
required
errors={errors?.replicaCount}
>
<Input
type="number"
min="0"
max="9999"
value={values.replicaCount}
disabled={!supportScalableReplicaDeployment}
onChange={(e) =>
onChange({ replicaCount: e.target.valueAsNumber || 0 })
}
onChange={(e) => onChange({ replicaCount: e.target.valueAsNumber })}
className="w-1/4"
data-cy="k8sAppCreate-replicaCountInput"
/>
@ -54,7 +55,6 @@ export function ReplicationFormSection({
<b>{memoryLimit * values.replicaCount} MB</b> of memory.
</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';
@ -19,7 +21,7 @@ export function replicationValidation(
supportScalableReplicaDeployment,
} = validationData || {};
return object({
replicaCount: number()
replicaCount: nanNumberSchema('Instance count is required')
.min(0, 'Instance count must be greater than or equal to 0.')
.test(
'overflow',

View File

@ -1,6 +1,7 @@
import { SchemaOf, TestContext, number, object } from 'yup';
import KubernetesResourceReservationHelper from '@/kubernetes/helpers/resourceReservationHelper';
import { nanNumberSchema } from '@/react-tools/yup-schemas';
import { ResourceQuotaFormValues } from './types';
@ -22,8 +23,8 @@ export function resourceReservationValidation(
validationData?: ValidationData
): SchemaOf<ResourceQuotaFormValues> {
return object().shape({
memoryLimit: number()
.min(0)
memoryLimit: nanNumberSchema()
.min(0, 'Value must be greater than or equal to 0')
.test(
'exhaused',
`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';
@ -17,8 +19,7 @@ export function validation(): SchemaOf<FormValues> {
hideFileUpload: boolean().required(),
requireNoteOnApplications: boolean().required(),
hideStacksFunctionality: boolean().required(),
minApplicationNoteLength: number()
.typeError('Must be a number')
minApplicationNoteLength: nanNumberSchema('Must be a number')
.default(0)
.when('requireNoteOnApplications', {
is: true,