mirror of https://github.com/portainer/portainer
fix(environments): debounce name validation [EE-4177] (#7889)
parent
9e1f80cf37
commit
9ef2e27aae
|
@ -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'),
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue