diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardAzure/WizardAzure.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardAzure/WizardAzure.tsx index d2922ac9c..a5094d4c1 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardAzure/WizardAzure.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardAzure/WizardAzure.tsx @@ -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 (
@@ -64,7 +65,7 @@ export function WizardAzure({ onCreate }: Props) { onSubmit={handleSubmit} key={formKey} validateOnMount - validationSchema={validationSchema} + validationSchema={validation} > {({ errors, dirty, isValid }) => (
@@ -164,9 +165,9 @@ export function WizardAzure({ onCreate }: Props) { } } -function validationSchema(): SchemaOf { +function useValidation(): SchemaOf { 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'), diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx index 66a42d1c0..0b22fbd29 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx @@ -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 ( { +export function useValidation(): SchemaOf { return object({ - name: nameValidation(), + name: useNameValidation(), url: string().required('This field is required.'), tls: boolean().default(false), skipVerify: boolean(), diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx index 6088717bd..1333588a2 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx @@ -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 ( { +export function useValidation(): SchemaOf { return object({ - name: nameValidation(), + name: useNameValidation(), meta: metadataValidation(), overridePath: boolean().default(false), socketPath: string() diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx index 5a9f5ff33..8ddea90c7 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx @@ -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 ( { +export function useValidation(): SchemaOf { return object({ - name: nameValidation(), + name: useNameValidation(), environmentUrl: environmentValidation(), meta: metadataValidation(), gpus: gpusListValidation(), diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx index 92dab95c5..fd3e686ff 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx @@ -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 ( initialValues={initialValues} onSubmit={handleSubmit} validateOnMount - validationSchema={validationSchema} + validationSchema={validation} > {({ isValid, setFieldValue, values }) => ( diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.validation.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.validation.ts index 85a4068c5..b6cb856e8 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.validation.ts +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.validation.ts @@ -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 { +export function useValidationSchema(): SchemaOf { + const nameValidation = useNameValidation(); + return object().shape({ - name: nameValidation(), + name: nameValidation, portainerUrl: urlValidation(), pollFrequency: number().required(), meta: metadataValidation(), diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/NameField.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/NameField.tsx index ada0b4683..5ad4c3741 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/NameField.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/NameField.tsx @@ -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 | 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); }