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 { Icon } from '@@/Icon';
import { NameField, nameValidation } from '../shared/NameField';
import { NameField, useNameValidation } from '../shared/NameField';
import { AnalyticsStateKey } from '../types';
import { metadataValidation } from '../shared/MetadataFieldset/validation';
import { MoreSettingsSection } from '../shared/MoreSettingsSection';
@ -49,6 +49,7 @@ export function WizardAzure({ onCreate }: Props) {
const [creationType, setCreationType] = useState(options[0].id);
const mutation = useCreateAzureEnvironmentMutation();
const validation = useValidation();
return (
<div className="form-horizontal">
@ -64,7 +65,7 @@ export function WizardAzure({ onCreate }: Props) {
onSubmit={handleSubmit}
key={formKey}
validateOnMount
validationSchema={validationSchema}
validationSchema={validation}
>
{({ errors, dirty, isValid }) => (
<Form>
@ -164,9 +165,9 @@ export function WizardAzure({ onCreate }: Props) {
}
}
function validationSchema(): SchemaOf<FormValues> {
function useValidation(): SchemaOf<FormValues> {
return object({
name: nameValidation(),
name: useNameValidation(),
applicationId: string().required('Application ID is required'),
tenantId: string().required('Tenant ID 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 { MoreSettingsSection } from '../../shared/MoreSettingsSection';
import { validation } from './APIForm.validation';
import { useValidation } from './APIForm.validation';
import { FormValues } from './types';
import { TLSFieldset } from './TLSFieldset';
@ -42,6 +42,8 @@ export function APIForm({ onCreate }: Props) {
EnvironmentCreationTypes.LocalDockerEnvironment
);
const validation = useValidation();
return (
<Formik
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 { metadataValidation } from '../../shared/MetadataFieldset/validation';
import { nameValidation } from '../../shared/NameField';
import { useNameValidation } from '../../shared/NameField';
import { validation as certsValidation } from './TLSFieldset';
import { FormValues } from './types';
export function validation(): SchemaOf<FormValues> {
export function useValidation(): SchemaOf<FormValues> {
return object({
name: nameValidation(),
name: useNameValidation(),
url: string().required('This field is required.'),
tls: boolean().default(false),
skipVerify: boolean(),

View File

@ -15,7 +15,7 @@ import { Icon } from '@@/Icon';
import { NameField } from '../../shared/NameField';
import { MoreSettingsSection } from '../../shared/MoreSettingsSection';
import { validation } from './SocketForm.validation';
import { useValidation } from './SocketForm.validation';
import { FormValues } from './types';
interface Props {
@ -33,6 +33,7 @@ export function SocketForm({ onCreate }: Props) {
};
const mutation = useCreateLocalDockerEnvironmentMutation();
const validation = useValidation();
return (
<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 { metadataValidation } from '../../shared/MetadataFieldset/validation';
import { nameValidation } from '../../shared/NameField';
import { useNameValidation } from '../../shared/NameField';
import { FormValues } from './types';
export function validation(): SchemaOf<FormValues> {
export function useValidation(): SchemaOf<FormValues> {
return object({
name: nameValidation(),
name: useNameValidation(),
meta: metadataValidation(),
overridePath: boolean().default(false),
socketPath: string()

View File

@ -14,7 +14,7 @@ import { MoreSettingsSection } from '../MoreSettingsSection';
import { Hardware } from '../Hardware/Hardware';
import { EnvironmentUrlField } from './EnvironmentUrlField';
import { validation } from './AgentForm.validation';
import { useValidation } from './AgentForm.validation';
interface Props {
onCreate(environment: Environment): void;
@ -35,6 +35,7 @@ export function AgentForm({ onCreate, showGpus = false }: Props) {
const [formKey, clearForm] = useReducer((state) => state + 1, 0);
const mutation = useCreateAgentEnvironmentMutation();
const validation = useValidation();
return (
<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 { metadataValidation } from '../MetadataFieldset/validation';
import { nameValidation } from '../NameField';
import { useNameValidation } from '../NameField';
export function validation(): SchemaOf<CreateAgentEnvironmentValues> {
export function useValidation(): SchemaOf<CreateAgentEnvironmentValues> {
return object({
name: nameValidation(),
name: useNameValidation(),
environmentUrl: environmentValidation(),
meta: metadataValidation(),
gpus: gpusListValidation(),

View File

@ -14,7 +14,7 @@ import { MoreSettingsSection } from '../../MoreSettingsSection';
import { Hardware } from '../../Hardware/Hardware';
import { EdgeAgentFieldset } from './EdgeAgentFieldset';
import { validationSchema } from './EdgeAgentForm.validation';
import { useValidationSchema } from './EdgeAgentForm.validation';
import { FormValues } from './types';
interface Props {
@ -29,13 +29,14 @@ export function EdgeAgentForm({ onCreate, readonly, showGpus = false }: Props) {
const createEdgeDevice = useCreateEdgeDeviceParam();
const createMutation = useCreateEdgeAgentEnvironmentMutation();
const validation = useValidationSchema();
return (
<Formik<FormValues>
initialValues={initialValues}
onSubmit={handleSubmit}
validateOnMount
validationSchema={validationSchema}
validationSchema={validation}
>
{({ isValid, setFieldValue, values }) => (
<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 { metadataValidation } from '../../MetadataFieldset/validation';
import { nameValidation } from '../../NameField';
import { useNameValidation } from '../../NameField';
import { validation as urlValidation } from './PortainerUrlField';
import { FormValues } from './types';
export function validationSchema(): SchemaOf<FormValues> {
export function useValidationSchema(): SchemaOf<FormValues> {
const nameValidation = useNameValidation();
return object().shape({
name: nameValidation(),
name: nameValidation,
portainerUrl: urlValidation(),
pollFrequency: number().required(),
meta: metadataValidation(),

View File

@ -1,6 +1,7 @@
import { Field, useField } from 'formik';
import { string } from 'yup';
import { debounce } from 'lodash';
import { useRef } from 'react';
import _ from 'lodash';
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) {
return true;
}
@ -46,14 +47,26 @@ export async function isNameUnique(name?: string) {
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()
.required('Name is required')
.test(
'unique-name',
'Name should be unique',
(name) => debouncedIsNameUnique(name) || false
);
.test('unique-name', 'Name should be unique', uniquenessTest.current);
}