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({
|
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)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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`}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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.${
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue