mirror of https://github.com/portainer/portainer
fix(input): allow clearing number inputs [EE-6714] (#11187)
parent
4e95139909
commit
1cdd3fdfe2
|
@ -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);
|
||||
}
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
@ -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`}
|
||||
/>
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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.${
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue