diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx index 3e4ce4a412b3b..25448dff18e8a 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; +import React, { useState } from 'react'; import { act } from 'react-dom/test-utils'; import { registerTestBed, TestBed } from '../shared_imports'; @@ -36,13 +36,14 @@ describe('', () => { return (
- {(formData) => { onFormData(formData); return null; }} + {/* Putting one field below to make sure the order in the DOM does not affect behaviour */} + ); }; @@ -95,6 +96,63 @@ describe('', () => { }); }); + test('should subscribe to the latest updated form data when mounting late', async () => { + const onFormData = jest.fn(); + + const TestComp = () => { + const { form } = useForm(); + const [isOn, setIsOn] = useState(false); + + return ( +
+ + + {isOn && ( + + {(formData) => { + onFormData(formData); + return null; + }} + + )} + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + const { + form: { setInputValue }, + find, + } = setup() as TestBed; + + expect(onFormData.mock.calls.length).toBe(0); // Not present in the DOM yet + + // Make some changes to the form fields + await act(async () => { + setInputValue('nameField', 'updated value'); + }); + + // Update state to trigger the mounting of the FormDataProvider + await act(async () => { + find('btn').simulate('click').update(); + }); + + expect(onFormData.mock.calls.length).toBe(1); + + const [formDataUpdated] = onFormData.mock.calls[onFormData.mock.calls.length - 1] as Parameters< + OnUpdateHandler + >; + + expect(formDataUpdated).toEqual({ + name: 'updated value', + }); + }); + test('props.pathsToWatch (string): should not re-render the children when the field that changed is not the one provided', async () => { const onFormData = jest.fn(); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts index 4c8e91b13b1b7..3630b902f0564 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts @@ -31,6 +31,7 @@ export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) = const form = useFormContext(); const { subscribe } = form; const previousRawData = useRef(form.__getFormData$().value); + const isMounted = useRef(false); const [formData, setFormData] = useState(previousRawData.current); const onFormData = useCallback( @@ -59,5 +60,17 @@ export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) = return subscription.unsubscribe; }, [subscribe, onFormData]); + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); + + if (!isMounted.current && Object.keys(formData).length === 0) { + // No field has mounted yet, don't render anything + return null; + } + return children(formData); }); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index 01d9f8a59129a..9d22e4eb2ee5e 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -43,38 +43,41 @@ export const useField = ( deserializer, } = config; - const { getFormData, __removeField, __updateFormDataAt, __validateFields } = form; + const { + getFormData, + getFields, + __addField, + __removeField, + __updateFormDataAt, + __validateFields, + } = form; - /** - * This callback is both used as the initial "value" state getter, **and** for when we reset the form - * (and thus reset the field value). When we reset the form, we can provide a new default value (which will be - * passed through this "initialValueGetter" handler). - */ - const initialValueGetter = useCallback( - (updatedDefaultValue = initialValue) => { - if (typeof updatedDefaultValue === 'function') { - return deserializer ? deserializer(updatedDefaultValue()) : updatedDefaultValue(); + const deserializeValue = useCallback( + (rawValue = initialValue) => { + if (typeof rawValue === 'function') { + return deserializer ? deserializer(rawValue()) : rawValue(); } - return deserializer ? deserializer(updatedDefaultValue) : updatedDefaultValue; + return deserializer ? deserializer(rawValue) : rawValue; }, [initialValue, deserializer] ); - const [value, setStateValue] = useState(initialValueGetter); + const [value, setStateValue] = useState(deserializeValue); const [errors, setErrors] = useState([]); const [isPristine, setPristine] = useState(true); const [isValidating, setValidating] = useState(false); const [isChangingValue, setIsChangingValue] = useState(false); const [isValidated, setIsValidated] = useState(false); + const validateCounter = useRef(0); const changeCounter = useRef(0); const inflightValidation = useRef | null>(null); const debounceTimeout = useRef(null); - const isUnmounted = useRef(false); + const isMounted = useRef(false); // -- HELPERS // ---------------------------------- - const serializeOutput: FieldHook['__serializeOutput'] = useCallback( + const serializeValue: FieldHook['__serializeValue'] = useCallback( (rawValue = value) => { return serializer ? serializer(rawValue) : rawValue; }, @@ -121,8 +124,11 @@ export const useField = ( if (debounceTimeout.current) { clearTimeout(debounceTimeout.current); + debounceTimeout.current = null; } + setPristine(false); + if (errorDisplayDelay > 0) { setIsChangingValue(true); } @@ -135,10 +141,14 @@ export const useField = ( // Update the form data observable __updateFormDataAt(path, value); - // Validate field(s) and update form.isValid state - await __validateFields(fieldsToValidateOnChange ?? [path]); + // Validate field(s) (that will update form.isValid state) + // We only validate if the value is different than the initial or default value + // to avoid validating after a form.reset() call. + if (value !== initialValue && value !== defaultValue) { + await __validateFields(fieldsToValidateOnChange ?? [path]); + } - if (isUnmounted.current) { + if (isMounted.current === false) { return; } @@ -160,10 +170,12 @@ export const useField = ( } } }, [ - valueChangeListener, - errorDisplayDelay, path, value, + defaultValue, + initialValue, + valueChangeListener, + errorDisplayDelay, fieldsToValidateOnChange, __updateFormDataAt, __validateFields, @@ -229,7 +241,7 @@ export const useField = ( inflightValidation.current = validator({ value: (valueToValidate as unknown) as string, errors: validationErrors, - form, + form: { getFormData, getFields }, formData, path, }) as Promise; @@ -273,7 +285,7 @@ export const useField = ( const validationResult = validator({ value: (valueToValidate as unknown) as string, errors: validationErrors, - form, + form: { getFormData, getFields }, formData, path, }); @@ -308,7 +320,7 @@ export const useField = ( // We first try to run the validations synchronously return runSync(); }, - [clearErrors, cancelInflightValidation, validations, form, path] + [clearErrors, cancelInflightValidation, validations, getFormData, getFields, path] ); // -- API @@ -331,12 +343,12 @@ export const useField = ( setValidating(true); // By the time our validate function has reached completion, it’s possible - // that validate() will have been called again. If this is the case, we need + // that we have called validate() again. If this is the case, we need // to ignore the results of this invocation and only use the results of // the most recent invocation to update the error state for a field const validateIteration = ++validateCounter.current; - const onValidationErrors = (_validationErrors: ValidationError[]): FieldValidateResponse => { + const onValidationResult = (_validationErrors: ValidationError[]): FieldValidateResponse => { if (validateIteration === validateCounter.current) { // This is the most recent invocation setValidating(false); @@ -360,9 +372,9 @@ export const useField = ( }); if (Reflect.has(validationErrors, 'then')) { - return (validationErrors as Promise).then(onValidationErrors); + return (validationErrors as Promise).then(onValidationResult); } - return onValidationErrors(validationErrors as ValidationError[]); + return onValidationResult(validationErrors as ValidationError[]); }, [getFormData, value, runValidations] ); @@ -374,15 +386,11 @@ export const useField = ( */ const setValue: FieldHook['setValue'] = useCallback( (newValue) => { - if (isPristine) { - setPristine(false); - } - const formattedValue = formatInputValue(newValue); setStateValue(formattedValue); return formattedValue; }, - [formatInputValue, isPristine] + [formatInputValue] ); const _setErrors: FieldHook['setErrors'] = useCallback((_errors) => { @@ -447,32 +455,17 @@ export const useField = ( setErrors([]); if (resetValue) { - const newValue = initialValueGetter(updatedDefaultValue ?? defaultValue); + const newValue = deserializeValue(updatedDefaultValue ?? defaultValue); setValue(newValue); return newValue; } }, - [setValue, initialValueGetter, defaultValue] + [setValue, deserializeValue, defaultValue] ); - // -- EFFECTS - // ---------------------------------- - useEffect(() => { - if (isPristine) { - // Avoid validate on mount - return; - } - - onValueChange(); + const isValid = errors.length === 0; - return () => { - if (debounceTimeout.current) { - clearTimeout(debounceTimeout.current); - } - }; - }, [isPristine, onValueChange]); - - const field: FieldHook = useMemo(() => { + const field = useMemo>(() => { return { path, type, @@ -481,9 +474,8 @@ export const useField = ( helpText, value, errors, - form, isPristine, - isValid: errors.length === 0, + isValid, isValidating, isValidated, isChangingValue, @@ -494,7 +486,7 @@ export const useField = ( clearErrors, validate, reset, - __serializeOutput: serializeOutput, + __serializeValue: serializeValue, }; }, [ path, @@ -503,9 +495,9 @@ export const useField = ( labelAppend, helpText, value, - form, isPristine, errors, + isValid, isValidating, isValidated, isChangingValue, @@ -516,18 +508,43 @@ export const useField = ( clearErrors, validate, reset, - serializeOutput, + serializeValue, ]); - form.__addField(field as FieldHook); + // ---------------------------------- + // -- EFFECTS + // ---------------------------------- + useEffect(() => { + __addField(field as FieldHook); + }, [field, __addField]); useEffect(() => { return () => { - // Remove field from the form when it is unmounted or if its path changes. - isUnmounted.current = true; __removeField(path); }; }, [path, __removeField]); + useEffect(() => { + if (!isMounted.current) { + return; + } + + onValueChange(); + + return () => { + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); + } + }; + }, [onValueChange]); + + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + }; + }, []); + return field; }; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index c3f6ecc7f4831..35bac5b9a58c6 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -40,19 +40,26 @@ export function useForm( const { onSubmit, schema, serializer, deserializer, options, id = 'default', defaultValue } = formConfig ?? {}; - const formDefaultValue = useMemo<{ [key: string]: any }>(() => { - if (defaultValue === undefined || Object.keys(defaultValue).length === 0) { - return {}; - } + const initDefaultValue = useCallback( + (_defaultValue?: Partial): { [key: string]: any } => { + if (_defaultValue === undefined || Object.keys(_defaultValue).length === 0) { + return {}; + } - const defaultValueFiltered = Object.entries(defaultValue as object) - .filter(({ 1: value }) => value !== undefined) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); + const filtered = Object.entries(_defaultValue as object) + .filter(({ 1: value }) => value !== undefined) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); - return deserializer ? (deserializer(defaultValueFiltered) as any) : defaultValueFiltered; - }, [defaultValue, deserializer]); + return deserializer ? (deserializer(filtered) as any) : filtered; + }, + [deserializer] + ); - const defaultValueDeserialized = useRef(formDefaultValue); + const defaultValueMemoized = useMemo<{ [key: string]: any }>(() => { + return initDefaultValue(defaultValue); + }, [defaultValue, initDefaultValue]); + + const defaultValueDeserialized = useRef(defaultValueMemoized); const { errorDisplayDelay, stripEmptyFields: doStripEmptyFields } = options ?? {}; const formOptions = useMemo( @@ -68,7 +75,7 @@ export function useForm( const [isValid, setIsValid] = useState(undefined); const fieldsRefs = useRef({}); const formUpdateSubscribers = useRef([]); - const isUnmounted = useRef(false); + const isMounted = useRef(false); // formData$ is an observable we can subscribe to in order to receive live // update of the raw form data. As an observable it does not trigger any React @@ -77,14 +84,6 @@ export function useForm( // and updating its state to trigger the necessary view render. const formData$ = useRef | null>(null); - useEffect(() => { - return () => { - formUpdateSubscribers.current.forEach((subscription) => subscription.unsubscribe()); - formUpdateSubscribers.current = []; - isUnmounted.current = true; - }; - }, []); - // -- HELPERS // ---------------------------------- const getFormData$ = useCallback((): Subject => { @@ -135,7 +134,7 @@ export function useForm( (getDataOptions: Parameters['getFormData']>[0] = { unflatten: true }) => { if (getDataOptions.unflatten) { const nonEmptyFields = stripEmptyFields(fieldsRefs.current); - const fieldsValue = mapFormFields(nonEmptyFields, (field) => field.__serializeOutput()); + const fieldsValue = mapFormFields(nonEmptyFields, (field) => field.__serializeValue()); return serializer ? (serializer(unflattenObject(fieldsValue)) as T) : (unflattenObject(fieldsValue) as T); @@ -168,45 +167,53 @@ export function useForm( const isFieldValid = (field: FieldHook) => field.isValid && !field.isValidating; - const updateFormValidity = useCallback(() => { - if (isUnmounted.current) { - return; - } - - const fieldsArray = fieldsToArray(); - const areAllFieldsValidated = fieldsArray.every((field) => field.isValidated); - - if (!areAllFieldsValidated) { - // If *not* all the fiels have been validated, the validity of the form is unknown, thus still "undefined" - return undefined; - } - - const isFormValid = fieldsArray.every(isFieldValid); - - setIsValid(isFormValid); - return isFormValid; - }, [fieldsToArray]); - const validateFields: FormHook['__validateFields'] = useCallback( async (fieldNames) => { const fieldsToValidate = fieldNames .map((name) => fieldsRefs.current[name]) .filter((field) => field !== undefined); - if (fieldsToValidate.length === 0) { - // Nothing to validate + const formData = getFormData({ unflatten: false }); + const validationResult = await Promise.all( + fieldsToValidate.map((field) => field.validate({ formData })) + ); + + if (isMounted.current === false) { return { areFieldsValid: true, isFormValid: true }; } - const formData = getFormData({ unflatten: false }); - await Promise.all(fieldsToValidate.map((field) => field.validate({ formData }))); + const areFieldsValid = validationResult.every(Boolean); - const isFormValid = updateFormValidity(); - const areFieldsValid = fieldsToValidate.every(isFieldValid); + const validationResultByPath = fieldsToValidate.reduce((acc, field, i) => { + acc[field.path] = validationResult[i].isValid; + return acc; + }, {} as { [key: string]: boolean }); + + // At this stage we have an updated field validation state inside the "validationResultByPath" object. + // The fields we have in our "fieldsRefs.current" have not been updated yet with the new validation state + // (isValid, isValidated...) as this will happen _after_, when the "useEffect" triggers and calls "addField()". + // This means that we have **stale state value** in our fieldsRefs. + // To know the current form validity, we will then merge the "validationResult" _with_ the fieldsRefs object state, + // the "validationResult" taking presedence over the fieldsRefs values. + const formFieldsValidity = fieldsToArray().map((field) => { + const _isValid = validationResultByPath[field.path] ?? field.isValid; + const _isValidated = + validationResultByPath[field.path] !== undefined ? true : field.isValidated; + return [_isValid, _isValidated]; + }); + + const areAllFieldsValidated = formFieldsValidity.every(({ 1: isValidated }) => isValidated); + + // If *not* all the fiels have been validated, the validity of the form is unknown, thus still "undefined" + const isFormValid = areAllFieldsValidated + ? formFieldsValidity.every(([_isValid]) => _isValid) + : undefined; + + setIsValid(isFormValid); return { areFieldsValid, isFormValid }; }, - [getFormData, updateFormValidity] + [getFormData, fieldsToArray] ); const validateAllFields = useCallback(async (): Promise => { @@ -216,19 +223,12 @@ export function useForm( let isFormValid: boolean | undefined; if (fieldsToValidate.length === 0) { - // We should never enter this condition as the form validity is updated each time - // a field is validated. But sometimes, during tests or race conditions it does not happen and we need - // to wait the next tick (hooks lifecycle being tricky) to make sure the "isValid" state is updated. - // In order to avoid this unintentional behaviour, we add this if condition here. - - // TODO: Fix this when adding tests to the form lib. isFormValid = fieldsArray.every(isFieldValid); - setIsValid(isFormValid); - return isFormValid; + } else { + ({ isFormValid } = await validateFields(fieldsToValidate.map((field) => field.path))); } - ({ isFormValid } = await validateFields(fieldsToValidate.map((field) => field.path))); - + setIsValid(isFormValid); return isFormValid!; }, [fieldsToArray, validateFields]); @@ -236,11 +236,13 @@ export function useForm( (field) => { fieldsRefs.current[field.path] = field; - if (!{}.hasOwnProperty.call(getFormData$().value, field.path)) { - updateFormDataAt(field.path, field.value); + updateFormDataAt(field.path, field.value); + + if (!field.isValidated) { + setIsValid(undefined); } }, - [getFormData$, updateFormDataAt] + [updateFormDataAt] ); const removeField: FormHook['__removeField'] = useCallback( @@ -259,9 +261,16 @@ export function useForm( * After removing a field, the form validity might have changed * (an invalid field might have been removed and now the form is valid) */ - updateFormValidity(); + setIsValid((prev) => { + if (prev === false) { + const isFormValid = fieldsToArray().every(isFieldValid); + return isFormValid; + } + // If the form validity is "true" or "undefined", it does not change after removing a field + return prev; + }); }, - [getFormData$, updateFormValidity] + [getFormData$, fieldsToArray] ); const setFieldValue: FormHook['setFieldValue'] = useCallback((fieldName, value) => { @@ -310,7 +319,7 @@ export function useForm( await onSubmit(formData, isFormValid!); } - if (isUnmounted.current === false) { + if (isMounted.current) { setSubmitting(false); } @@ -322,9 +331,7 @@ export function useForm( const subscribe: FormHook['subscribe'] = useCallback( (handler) => { const subscription = getFormData$().subscribe((raw) => { - if (!isUnmounted.current) { - handler({ isValid, data: { raw, format: getFormData }, validate: validateAllFields }); - } + handler({ isValid, data: { raw, format: getFormData }, validate: validateAllFields }); }); formUpdateSubscribers.current.push(subscription); @@ -351,9 +358,7 @@ export function useForm( const currentFormData = { ...getFormData$().value } as FormData; if (updatedDefaultValue) { - defaultValueDeserialized.current = deserializer - ? (deserializer(updatedDefaultValue) as any) - : updatedDefaultValue; + defaultValueDeserialized.current = initDefaultValue(updatedDefaultValue); } Object.entries(fieldsRefs.current).forEach(([path, field]) => { @@ -374,7 +379,7 @@ export function useForm( setSubmitting(false); setIsValid(undefined); }, - [getFormData$, deserializer, getFieldDefaultValue] + [getFormData$, initDefaultValue, getFieldDefaultValue] ); const form = useMemo>(() => { @@ -425,6 +430,25 @@ export function useForm( validateFields, ]); + useEffect(() => { + if (!isMounted.current) { + return; + } + + // Whenever the "defaultValue" prop changes, reinitialize our ref + defaultValueDeserialized.current = defaultValueMemoized; + }, [defaultValueMemoized]); + + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + formUpdateSubscribers.current.forEach((subscription) => subscription.unsubscribe()); + formUpdateSubscribers.current = []; + }; + }, []); + return { form, }; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index 4b203c3927ffd..dc495f6eb56b4 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -38,7 +38,7 @@ export interface FormHook { getFieldDefaultValue: (fieldName: string) => unknown; /* Returns a list of all errors in the form */ getErrors: () => string[]; - reset: (options?: { resetValues?: boolean; defaultValue?: FormData }) => void; + reset: (options?: { resetValues?: boolean; defaultValue?: Partial }) => void; readonly __options: Required; __getFormData$: () => Subject; __addField: (field: FieldHook) => void; @@ -102,7 +102,6 @@ export interface FieldHook { readonly isValidating: boolean; readonly isValidated: boolean; readonly isChangingValue: boolean; - readonly form: FormHook; getErrorsMessages: (args?: { validationType?: 'field' | string; errorCode?: string; @@ -117,7 +116,7 @@ export interface FieldHook { validationType?: string; }) => FieldValidateResponse | Promise; reset: (options?: { resetValue?: boolean; defaultValue?: T }) => unknown | undefined; - __serializeOutput: (rawValue?: unknown) => unknown; + __serializeValue: (rawValue?: unknown) => unknown; } export interface FieldConfig { @@ -154,7 +153,10 @@ export interface ValidationError { export interface ValidationFuncArg { path: string; value: V; - form: FormHook; + form: { + getFormData: FormHook['getFormData']; + getFields: FormHook['getFields']; + }; formData: T; errors: readonly ValidationError[]; } diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts index f92f46d71e7c7..870b8b7ec5509 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts @@ -85,6 +85,9 @@ export const getFormActions = (testBed: TestBed) => { value: type, }, ]); + }); + + await act(async () => { find('createFieldForm.addButton').simulate('click'); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx index 0320f2ff51da3..9b27b930b47c4 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx @@ -4,33 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { TextField, UseField, FieldConfig } from '../../../shared_imports'; import { validateUniqueName } from '../../../lib'; import { PARAMETERS_DEFINITION } from '../../../constants'; import { useMappingsState } from '../../../mappings_state_context'; +const { validations, ...rest } = PARAMETERS_DEFINITION.name.fieldConfig as FieldConfig; + export const NameParameter = () => { const { fields: { rootLevelFields, byId }, documentFields: { fieldToAddFieldTo, fieldToEdit }, } = useMappingsState(); - const { validations, ...rest } = PARAMETERS_DEFINITION.name.fieldConfig as FieldConfig; const initialName = fieldToEdit ? byId[fieldToEdit].source.name : undefined; const parentId = fieldToEdit ? byId[fieldToEdit].parentId : fieldToAddFieldTo; - const uniqueNameValidator = validateUniqueName({ rootLevelFields, byId }, initialName, parentId); + const uniqueNameValidator = useCallback( + (arg: any) => { + return validateUniqueName({ rootLevelFields, byId }, initialName, parentId)(arg); + }, + [rootLevelFields, byId, initialName, parentId] + ); - const nameConfig: FieldConfig = { - ...rest, - validations: [ - ...validations!, - { - validator: uniqueNameValidator, - }, - ], - }; + const nameConfig: FieldConfig = useMemo( + () => ({ + ...rest, + validations: [ + ...validations!, + { + validator: uniqueNameValidator, + }, + ], + }), + [uniqueNameValidator] + ); return ( { const suggestedFields = getSuggestedFields(allFields, field); + const fieldConfig = useMemo( + () => ({ + ...getFieldConfig('path'), + deserializer: getDeserializer(allFields), + }), + [allFields] + ); + return ( - + {(pathField) => { const error = pathField.getErrorsMessages(); const isInvalid = error ? Boolean(error.length) : false; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx index 95575124b6abd..6b5a848ce85d3 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx @@ -163,7 +163,7 @@ export const EditField = React.memo(({ form, field, allFields, exitEdit, updateF - {form.isSubmitted && !form.isValid && ( + {form.isSubmitted && form.isValid === false && ( <> {i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldUpdateButtonLabel', { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx index f2ad37cb45818..3b55c5ac076c2 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx @@ -59,6 +59,9 @@ export const EditFieldHeaderForm = React.memo( {({ type, subType }) => { + if (!type) { + return null; + } const typeDefinition = TYPE_DEFINITION[type[0].value as MainType]; const hasSubType = typeDefinition.subTypes !== undefined; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx index 6a70592bc2f70..9adb3957ea9f4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx @@ -35,7 +35,7 @@ export const ProcessorSettingsFields: FunctionComponent = ({ processor }) if (formDescriptor?.FieldsComponent) { return ( <> - + diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx index 09d0981adf1c2..23425297f3420 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx @@ -21,6 +21,7 @@ const { emptyField } = fieldValidators; const fieldsConfig: FieldsConfig = { value: { + defaultValue: [], type: FIELD_TYPES.COMBO_BOX, deserializer: to.arrayOfStrings, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.appendForm.valueFieldLabel', { diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index 9ead8171bfef6..c7810af13eb74 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -23,7 +23,7 @@ import { createKibanaContextProviderMock, createStartServicesMock, } from '../lib/kibana/kibana_react.mock'; -import { FieldHook, useForm } from '../../shared_imports'; +import { FieldHook } from '../../shared_imports'; import { SUB_PLUGINS_REDUCER } from './utils'; import { createSecuritySolutionStorageMock, localStorageMock } from './mock_local_storage'; @@ -78,8 +78,6 @@ const TestProvidersComponent: React.FC = ({ export const TestProviders = React.memo(TestProvidersComponent); export const useFormFieldMock = (options?: Partial): FieldHook => { - const { form } = useForm(); - return { path: 'path', type: 'type', @@ -88,7 +86,6 @@ export const useFormFieldMock = (options?: Partial): FieldHook => { isValidating: false, isValidated: false, isChangingValue: false, - form, errors: [], isValid: true, getErrorsMessages: jest.fn(), @@ -98,7 +95,7 @@ export const useFormFieldMock = (options?: Partial): FieldHook => { clearErrors: jest.fn(), validate: jest.fn(), reset: jest.fn(), - __serializeOutput: jest.fn(), + __serializeValue: jest.fn(), ...options, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx index a0384ef52a841..cdeca54bfc39b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx @@ -13,13 +13,13 @@ import { EuiFormLabel, EuiIcon, EuiSpacer, + EuiRange, } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { noop } from 'lodash/fp'; import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -import { CommonUseField } from '../../../../cases/components/create'; import { AboutStepRiskScore } from '../../../pages/detection_engine/rules/types'; import { FieldComponent } from '../../../../common/components/autocomplete/field'; import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; @@ -59,11 +59,12 @@ export const RiskScoreField = ({ placeholder, }: RiskScoreFieldProps) => { const fieldTypeFilter = useMemo(() => ['number'], []); + const { value: fieldValue, setValue } = field; const handleFieldChange = useCallback( ([newField]: IFieldType[]): void => { - const values = field.value as AboutStepRiskScore; - field.setValue({ + const values = fieldValue as AboutStepRiskScore; + setValue({ value: values.value, isMappingChecked: values.isMappingChecked, mapping: [ @@ -76,25 +77,37 @@ export const RiskScoreField = ({ ], }); }, - [field] + [setValue, fieldValue] + ); + + const handleRangeFieldChange = useCallback( + (e: React.ChangeEvent | React.MouseEvent): void => { + const range = (e.target as HTMLInputElement).value; + setValue({ + value: range.trim() === '' ? '' : +range, + isMappingChecked: (fieldValue as AboutStepRiskScore).isMappingChecked, + mapping: (fieldValue as AboutStepRiskScore).mapping, + }); + }, + [fieldValue, setValue] ); const selectedField = useMemo(() => { - const existingField = (field.value as AboutStepRiskScore).mapping?.[0]?.field ?? ''; + const existingField = (fieldValue as AboutStepRiskScore).mapping?.[0]?.field ?? ''; const [newSelectedField] = indices.fields.filter( ({ name }) => existingField != null && existingField === name ); return newSelectedField; - }, [field.value, indices]); + }, [fieldValue, indices]); const handleRiskScoreMappingChecked = useCallback(() => { - const values = field.value as AboutStepRiskScore; - field.setValue({ + const values = fieldValue as AboutStepRiskScore; + setValue({ value: values.value, mapping: [...values.mapping], isMappingChecked: !values.isMappingChecked, }); - }, [field]); + }, [fieldValue, setValue]); const riskScoreLabel = useMemo(() => { return ( @@ -119,7 +132,7 @@ export const RiskScoreField = ({ @@ -132,7 +145,7 @@ export const RiskScoreField = ({ ); - }, [field.value, handleRiskScoreMappingChecked, isDisabled]); + }, [fieldValue, handleRiskScoreMappingChecked, isDisabled]); return ( @@ -144,24 +157,20 @@ export const RiskScoreField = ({ error={'errorMessage'} isInvalid={false} fullWidth - data-test-subj={dataTestSubj} - describedByIds={idAria ? [idAria] : undefined} + data-test-subj="detectionEngineStepAboutRuleRiskScore" + describedByIds={['detectionEngineStepAboutRuleRiskScore']} > - @@ -170,7 +179,7 @@ export const RiskScoreField = ({ label={riskScoreMappingLabel} labelAppend={field.labelAppend} helpText={ - (field.value as AboutStepRiskScore).isMappingChecked ? ( + (fieldValue as AboutStepRiskScore).isMappingChecked ? ( {i18n.RISK_SCORE_MAPPING_DETAILS} ) : ( '' @@ -184,7 +193,7 @@ export const RiskScoreField = ({ > - {(field.value as AboutStepRiskScore).isMappingChecked && ( + {(fieldValue as AboutStepRiskScore).isMappingChecked && ( diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx index a9bde76126b6e..20c3073789b2a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { RuleActionsField } from './index'; +import { useForm, Form } from '../../../../shared_imports'; import { useKibana } from '../../../../common/lib/kibana'; import { useFormFieldMock } from '../../../../common/mock'; jest.mock('../../../../common/lib/kibana'); @@ -32,8 +33,13 @@ describe('RuleActionsField', () => { }); const Component = () => { const field = useFormFieldMock(); + const { form } = useForm(); - return ; + return ( +
+ + + ); }; const wrapper = shallow(); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx index c6ff25f311d9c..b9097949bd20a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx @@ -12,7 +12,7 @@ import ReactMarkdown from 'react-markdown'; import styled from 'styled-components'; import { NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS } from '../../../../../common/constants'; -import { SelectField } from '../../../../shared_imports'; +import { SelectField, useFormContext } from '../../../../shared_imports'; import { ActionForm, ActionType, @@ -37,6 +37,8 @@ const FieldErrorsContainer = styled.div` export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables }) => { const [fieldErrors, setFieldErrors] = useState(null); const [supportedActionTypes, setSupportedActionTypes] = useState(); + const form = useFormContext(); + const { isSubmitted, isSubmitting, isValid } = form; const { http, triggers_actions_ui: { actionTypeRegistry }, @@ -88,26 +90,14 @@ export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables }, []); useEffect(() => { - if (field.form.isSubmitting || !field.errors.length) { + if (isSubmitting || !field.errors.length) { return setFieldErrors(null); } - if ( - field.form.isSubmitted && - !field.form.isSubmitting && - field.form.isValid === false && - field.errors.length - ) { + if (isSubmitted && !isSubmitting && isValid === false && field.errors.length) { const errorsString = field.errors.map(({ message }) => message).join('\n'); return setFieldErrors(errorsString); } - }, [ - field.form.isSubmitted, - field.form.isSubmitting, - field.isChangingValue, - field.form.isValid, - field.errors, - setFieldErrors, - ]); + }, [isSubmitted, isSubmitting, field.isChangingValue, isValid, field.errors, setFieldErrors]); if (!supportedActionTypes) return <>; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx index 733e701cff204..70e66af25f69e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx @@ -13,6 +13,7 @@ import { EuiFormLabel, EuiIcon, EuiSpacer, + EuiSuperSelect, } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; @@ -20,7 +21,6 @@ import styled from 'styled-components'; import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { SeverityOptionItem } from '../step_about_rule/data'; -import { CommonUseField } from '../../../../cases/components/create'; import { AboutStepSeverity } from '../../../pages/detection_engine/rules/types'; import { IFieldType, @@ -68,58 +68,61 @@ export const SeverityField = ({ options, }: SeverityFieldProps) => { const fieldValueInputWidth = 160; + const { setValue } = field; + const { value, isMappingChecked, mapping } = field.value as AboutStepSeverity; const handleFieldValueChange = useCallback( (newMappingItems: SeverityMapping, index: number): void => { - const values = field.value as AboutStepSeverity; - field.setValue({ - value: values.value, - isMappingChecked: values.isMappingChecked, - mapping: [ - ...values.mapping.slice(0, index), - ...newMappingItems, - ...values.mapping.slice(index + 1), - ], + setValue({ + value, + isMappingChecked, + mapping: [...mapping.slice(0, index), ...newMappingItems, ...mapping.slice(index + 1)], }); }, - [field] + [value, isMappingChecked, mapping, setValue] ); const handleFieldChange = useCallback( (index: number, severity: Severity, [newField]: IFieldType[]): void => { - const values = field.value as AboutStepSeverity; const newMappingItems: SeverityMapping = [ { - ...values.mapping[index], + ...mapping[index], field: newField?.name ?? '', - value: newField != null ? values.mapping[index].value : '', + value: newField != null ? mapping[index].value : '', operator: 'equals', severity, }, ]; handleFieldValueChange(newMappingItems, index); }, - [field, handleFieldValueChange] + [mapping, handleFieldValueChange] + ); + + const handleSecurityLevelChange = useCallback( + (newValue: string) => { + setValue({ + value: newValue, + isMappingChecked, + mapping, + }); + }, + [isMappingChecked, mapping, setValue] ); const handleFieldMatchValueChange = useCallback( (index: number, severity: Severity, newMatchValue: string): void => { - const values = field.value as AboutStepSeverity; const newMappingItems: SeverityMapping = [ { - ...values.mapping[index], - field: values.mapping[index].field, - value: - values.mapping[index].field != null && values.mapping[index].field !== '' - ? newMatchValue - : '', + ...mapping[index], + field: mapping[index].field, + value: mapping[index].field != null && mapping[index].field !== '' ? newMatchValue : '', operator: 'equals', severity, }, ]; handleFieldValueChange(newMappingItems, index); }, - [field, handleFieldValueChange] + [mapping, handleFieldValueChange] ); const getIFieldTypeFromFieldName = ( @@ -131,13 +134,12 @@ export const SeverityField = ({ }; const handleSeverityMappingChecked = useCallback(() => { - const values = field.value as AboutStepSeverity; - field.setValue({ - value: values.value, - mapping: [...values.mapping], - isMappingChecked: !values.isMappingChecked, + setValue({ + value, + mapping: [...mapping], + isMappingChecked: !isMappingChecked, }); - }, [field]); + }, [isMappingChecked, mapping, value, setValue]); const severityLabel = useMemo(() => { return ( @@ -162,7 +164,7 @@ export const SeverityField = ({ @@ -175,7 +177,7 @@ export const SeverityField = ({
); - }, [field.value, handleSeverityMappingChecked, isDisabled]); + }, [handleSeverityMappingChecked, isDisabled, isMappingChecked]); return ( @@ -187,21 +189,16 @@ export const SeverityField = ({ error={'errorMessage'} isInvalid={false} fullWidth - data-test-subj={dataTestSubj} - describedByIds={idAria ? [idAria] : undefined} + data-test-subj="detectionEngineStepAboutRuleSeverity" + describedByIds={['detectionEngineStepAboutRuleSeverity']} > - @@ -211,11 +208,7 @@ export const SeverityField = ({ label={severityMappingLabel} labelAppend={field.labelAppend} helpText={ - (field.value as AboutStepSeverity).isMappingChecked ? ( - {i18n.SEVERITY_MAPPING_DETAILS} - ) : ( - '' - ) + isMappingChecked ? {i18n.SEVERITY_MAPPING_DETAILS} : '' } error={'errorMessage'} isInvalid={false} @@ -225,7 +218,7 @@ export const SeverityField = ({ > - {(field.value as AboutStepSeverity).isMappingChecked && ( + {isMappingChecked && ( @@ -242,71 +235,69 @@ export const SeverityField = ({ - {(field.value as AboutStepSeverity).mapping.map( - (severityMappingItem: SeverityMappingItem, index) => ( - - - - - + {mapping.map((severityMappingItem: SeverityMappingItem, index) => ( + + + + + - - - - - - - - { - options.find((o) => o.value === severityMappingItem.severity) - ?.inputDisplay - } - - - - ) - )} + + + + + + + + { + options.find((o) => o.value === severityMappingItem.severity) + ?.inputDisplay + } + + + + ))} )} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx index cb3fd5e5bec32..0c834b9fff33a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { mount, shallow } from 'enzyme'; import { ThemeProvider } from 'styled-components'; import euiDarkVars from '@elastic/eui/dist/eui_theme_light.json'; @@ -223,32 +224,33 @@ describe('StepAboutRuleComponent', () => { .first() .simulate('change', { target: { value: '80' } }); - wrapper.find('[data-test-subj="about-continue"]').first().simulate('click').update(); - await waitFor(() => { - const expected: Omit = { - author: [], - isAssociatedToEndpointList: false, - isBuildingBlock: false, - license: '', - ruleNameOverride: '', - timestampOverride: '', - description: 'Test description text', - falsePositives: [''], - name: 'Test name text', - note: '', - references: [''], - riskScore: { value: 80, mapping: [], isMappingChecked: false }, - severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false }, - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { id: 'none', name: 'none', reference: 'none' }, - technique: [], - }, - ], - }; - expect(stepDataMock.mock.calls[1][1]).toEqual(expected); + await act(async () => { + wrapper.find('[data-test-subj="about-continue"]').first().simulate('click').update(); }); + + const expected: Omit = { + author: [], + isAssociatedToEndpointList: false, + isBuildingBlock: false, + license: '', + ruleNameOverride: '', + timestampOverride: '', + description: 'Test description text', + falsePositives: [''], + name: 'Test name text', + note: '', + references: [''], + riskScore: { value: 80, mapping: [], isMappingChecked: false }, + severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false }, + tags: [], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { id: 'none', name: 'none', reference: 'none' }, + technique: [], + }, + ], + }; + expect(stepDataMock.mock.calls[1][1]).toEqual(expected); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx index a3db8fe659d84..2264a11341eb8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx @@ -102,29 +102,12 @@ export const schema: FormSchema = { labelAppend: OptionalFieldLabel, }, severity: { - value: { - type: FIELD_TYPES.SUPER_SELECT, - validations: [ - { - validator: emptyField( - i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.severityFieldRequiredError', - { - defaultMessage: 'A severity is required.', - } - ) - ), - }, - ], - }, + value: {}, mapping: {}, isMappingChecked: {}, }, riskScore: { - value: { - type: FIELD_TYPES.RANGE, - serializer: (input: string) => Number(input), - }, + value: {}, mapping: {}, isMappingChecked: {}, }, diff --git a/x-pack/plugins/security_solution/public/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts index b2c7319b94576..097166a9c866a 100644 --- a/x-pack/plugins/security_solution/public/shared_imports.ts +++ b/x-pack/plugins/security_solution/public/shared_imports.ts @@ -20,6 +20,7 @@ export { UseField, UseMultiFields, useForm, + useFormContext, ValidationFunc, VALIDATION_TYPES, } from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7c99127982cf4..6c86888145f49 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15651,7 +15651,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideLabel": "調査ガイド", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "名前が必要です。", "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "ルール調査ガイドを追加...", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.severityFieldRequiredError": "深刻度が必要です。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addFalsePositiveDescription": "誤検出の例を追加します", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addReferenceDescription": "参照URLを追加します", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.advancedSettingsButton": "高度な設定", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f81b989575be9..84d4387a4cbbd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15657,7 +15657,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideLabel": "调查指南", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "名称必填。", "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "添加规则调查指南......", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.severityFieldRequiredError": "严重性必填。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addFalsePositiveDescription": "添加误报示例", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addReferenceDescription": "添加引用 URL", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.advancedSettingsButton": "高级设置",