fix(environments): debounce name validation [EE-4177] (#7889)

pull/7994/head
Chaim Lev-Ari 2022-11-02 12:44:31 +02:00 committed by GitHub
parent 9e1f80cf37
commit 9ef2e27aae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 51 additions and 30 deletions

View File

@ -14,7 +14,7 @@ import { FormControl } from '@@/form-components/FormControl';
import { BoxSelector } from '@@/BoxSelector'; import { BoxSelector } from '@@/BoxSelector';
import { Icon } from '@@/Icon'; import { Icon } from '@@/Icon';
import { NameField, nameValidation } from '../shared/NameField'; import { NameField, useNameValidation } from '../shared/NameField';
import { AnalyticsStateKey } from '../types'; import { AnalyticsStateKey } from '../types';
import { metadataValidation } from '../shared/MetadataFieldset/validation'; import { metadataValidation } from '../shared/MetadataFieldset/validation';
import { MoreSettingsSection } from '../shared/MoreSettingsSection'; import { MoreSettingsSection } from '../shared/MoreSettingsSection';
@ -49,6 +49,7 @@ export function WizardAzure({ onCreate }: Props) {
const [creationType, setCreationType] = useState(options[0].id); const [creationType, setCreationType] = useState(options[0].id);
const mutation = useCreateAzureEnvironmentMutation(); const mutation = useCreateAzureEnvironmentMutation();
const validation = useValidation();
return ( return (
<div className="form-horizontal"> <div className="form-horizontal">
@ -64,7 +65,7 @@ export function WizardAzure({ onCreate }: Props) {
onSubmit={handleSubmit} onSubmit={handleSubmit}
key={formKey} key={formKey}
validateOnMount validateOnMount
validationSchema={validationSchema} validationSchema={validation}
> >
{({ errors, dirty, isValid }) => ( {({ errors, dirty, isValid }) => (
<Form> <Form>
@ -164,9 +165,9 @@ export function WizardAzure({ onCreate }: Props) {
} }
} }
function validationSchema(): SchemaOf<FormValues> { function useValidation(): SchemaOf<FormValues> {
return object({ return object({
name: nameValidation(), name: useNameValidation(),
applicationId: string().required('Application ID is required'), applicationId: string().required('Application ID is required'),
tenantId: string().required('Tenant ID is required'), tenantId: string().required('Tenant ID is required'),
authenticationKey: string().required('Authentication Key is required'), authenticationKey: string().required('Authentication Key is required'),

View File

@ -17,7 +17,7 @@ import { Icon } from '@@/Icon';
import { NameField } from '../../shared/NameField'; import { NameField } from '../../shared/NameField';
import { MoreSettingsSection } from '../../shared/MoreSettingsSection'; import { MoreSettingsSection } from '../../shared/MoreSettingsSection';
import { validation } from './APIForm.validation'; import { useValidation } from './APIForm.validation';
import { FormValues } from './types'; import { FormValues } from './types';
import { TLSFieldset } from './TLSFieldset'; import { TLSFieldset } from './TLSFieldset';
@ -42,6 +42,8 @@ export function APIForm({ onCreate }: Props) {
EnvironmentCreationTypes.LocalDockerEnvironment EnvironmentCreationTypes.LocalDockerEnvironment
); );
const validation = useValidation();
return ( return (
<Formik <Formik
initialValues={initialValues} initialValues={initialValues}

View File

@ -3,14 +3,14 @@ import { boolean, object, SchemaOf, string } from 'yup';
import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
import { metadataValidation } from '../../shared/MetadataFieldset/validation'; import { metadataValidation } from '../../shared/MetadataFieldset/validation';
import { nameValidation } from '../../shared/NameField'; import { useNameValidation } from '../../shared/NameField';
import { validation as certsValidation } from './TLSFieldset'; import { validation as certsValidation } from './TLSFieldset';
import { FormValues } from './types'; import { FormValues } from './types';
export function validation(): SchemaOf<FormValues> { export function useValidation(): SchemaOf<FormValues> {
return object({ return object({
name: nameValidation(), name: useNameValidation(),
url: string().required('This field is required.'), url: string().required('This field is required.'),
tls: boolean().default(false), tls: boolean().default(false),
skipVerify: boolean(), skipVerify: boolean(),

View File

@ -15,7 +15,7 @@ import { Icon } from '@@/Icon';
import { NameField } from '../../shared/NameField'; import { NameField } from '../../shared/NameField';
import { MoreSettingsSection } from '../../shared/MoreSettingsSection'; import { MoreSettingsSection } from '../../shared/MoreSettingsSection';
import { validation } from './SocketForm.validation'; import { useValidation } from './SocketForm.validation';
import { FormValues } from './types'; import { FormValues } from './types';
interface Props { interface Props {
@ -33,6 +33,7 @@ export function SocketForm({ onCreate }: Props) {
}; };
const mutation = useCreateLocalDockerEnvironmentMutation(); const mutation = useCreateLocalDockerEnvironmentMutation();
const validation = useValidation();
return ( return (
<Formik <Formik

View File

@ -3,13 +3,13 @@ import { boolean, object, SchemaOf, string } from 'yup';
import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
import { metadataValidation } from '../../shared/MetadataFieldset/validation'; import { metadataValidation } from '../../shared/MetadataFieldset/validation';
import { nameValidation } from '../../shared/NameField'; import { useNameValidation } from '../../shared/NameField';
import { FormValues } from './types'; import { FormValues } from './types';
export function validation(): SchemaOf<FormValues> { export function useValidation(): SchemaOf<FormValues> {
return object({ return object({
name: nameValidation(), name: useNameValidation(),
meta: metadataValidation(), meta: metadataValidation(),
overridePath: boolean().default(false), overridePath: boolean().default(false),
socketPath: string() socketPath: string()

View File

@ -14,7 +14,7 @@ import { MoreSettingsSection } from '../MoreSettingsSection';
import { Hardware } from '../Hardware/Hardware'; import { Hardware } from '../Hardware/Hardware';
import { EnvironmentUrlField } from './EnvironmentUrlField'; import { EnvironmentUrlField } from './EnvironmentUrlField';
import { validation } from './AgentForm.validation'; import { useValidation } from './AgentForm.validation';
interface Props { interface Props {
onCreate(environment: Environment): void; onCreate(environment: Environment): void;
@ -35,6 +35,7 @@ export function AgentForm({ onCreate, showGpus = false }: Props) {
const [formKey, clearForm] = useReducer((state) => state + 1, 0); const [formKey, clearForm] = useReducer((state) => state + 1, 0);
const mutation = useCreateAgentEnvironmentMutation(); const mutation = useCreateAgentEnvironmentMutation();
const validation = useValidation();
return ( return (
<Formik <Formik

View File

@ -4,11 +4,11 @@ import { gpusListValidation } from '@/react/portainer/environments/wizard/Enviro
import { CreateAgentEnvironmentValues } from '@/react/portainer/environments/environment.service/create'; import { CreateAgentEnvironmentValues } from '@/react/portainer/environments/environment.service/create';
import { metadataValidation } from '../MetadataFieldset/validation'; import { metadataValidation } from '../MetadataFieldset/validation';
import { nameValidation } from '../NameField'; import { useNameValidation } from '../NameField';
export function validation(): SchemaOf<CreateAgentEnvironmentValues> { export function useValidation(): SchemaOf<CreateAgentEnvironmentValues> {
return object({ return object({
name: nameValidation(), name: useNameValidation(),
environmentUrl: environmentValidation(), environmentUrl: environmentValidation(),
meta: metadataValidation(), meta: metadataValidation(),
gpus: gpusListValidation(), gpus: gpusListValidation(),

View File

@ -14,7 +14,7 @@ import { MoreSettingsSection } from '../../MoreSettingsSection';
import { Hardware } from '../../Hardware/Hardware'; import { Hardware } from '../../Hardware/Hardware';
import { EdgeAgentFieldset } from './EdgeAgentFieldset'; import { EdgeAgentFieldset } from './EdgeAgentFieldset';
import { validationSchema } from './EdgeAgentForm.validation'; import { useValidationSchema } from './EdgeAgentForm.validation';
import { FormValues } from './types'; import { FormValues } from './types';
interface Props { interface Props {
@ -29,13 +29,14 @@ export function EdgeAgentForm({ onCreate, readonly, showGpus = false }: Props) {
const createEdgeDevice = useCreateEdgeDeviceParam(); const createEdgeDevice = useCreateEdgeDeviceParam();
const createMutation = useCreateEdgeAgentEnvironmentMutation(); const createMutation = useCreateEdgeAgentEnvironmentMutation();
const validation = useValidationSchema();
return ( return (
<Formik<FormValues> <Formik<FormValues>
initialValues={initialValues} initialValues={initialValues}
onSubmit={handleSubmit} onSubmit={handleSubmit}
validateOnMount validateOnMount
validationSchema={validationSchema} validationSchema={validation}
> >
{({ isValid, setFieldValue, values }) => ( {({ isValid, setFieldValue, values }) => (
<Form> <Form>

View File

@ -3,14 +3,16 @@ import { number, object, SchemaOf } from 'yup';
import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
import { metadataValidation } from '../../MetadataFieldset/validation'; import { metadataValidation } from '../../MetadataFieldset/validation';
import { nameValidation } from '../../NameField'; import { useNameValidation } from '../../NameField';
import { validation as urlValidation } from './PortainerUrlField'; import { validation as urlValidation } from './PortainerUrlField';
import { FormValues } from './types'; import { FormValues } from './types';
export function validationSchema(): SchemaOf<FormValues> { export function useValidationSchema(): SchemaOf<FormValues> {
const nameValidation = useNameValidation();
return object().shape({ return object().shape({
name: nameValidation(), name: nameValidation,
portainerUrl: urlValidation(), portainerUrl: urlValidation(),
pollFrequency: number().required(), pollFrequency: number().required(),
meta: metadataValidation(), meta: metadataValidation(),

View File

@ -1,6 +1,7 @@
import { Field, useField } from 'formik'; import { Field, useField } from 'formik';
import { string } from 'yup'; import { string } from 'yup';
import { debounce } from 'lodash'; import { useRef } from 'react';
import _ from 'lodash';
import { getEnvironments } from '@/react/portainer/environments/environment.service'; import { getEnvironments } from '@/react/portainer/environments/environment.service';
@ -30,7 +31,7 @@ export function NameField({ readonly }: Props) {
); );
} }
export async function isNameUnique(name?: string) { export async function isNameUnique(name = '') {
if (!name) { if (!name) {
return true; return true;
} }
@ -46,14 +47,26 @@ export async function isNameUnique(name?: string) {
return true; return true;
} }
const debouncedIsNameUnique = debounce(isNameUnique, 500); function cacheTest(
asyncValidate: (val?: string) => Promise<boolean> | undefined
) {
let valid = false;
let value = '';
return async (newValue = '') => {
if (newValue !== value) {
const response = await asyncValidate(newValue);
value = newValue;
valid = !!response;
}
return valid;
};
}
export function useNameValidation() {
const uniquenessTest = useRef(cacheTest(_.debounce(isNameUnique, 300)));
export function nameValidation() {
return string() return string()
.required('Name is required') .required('Name is required')
.test( .test('unique-name', 'Name should be unique', uniquenessTest.current);
'unique-name',
'Name should be unique',
(name) => debouncedIsNameUnique(name) || false
);
} }