import { isEqual } from 'lodash' ; import { merge } from 'lodash' ; import { useReducer } from 'react' ; import { i18n } from '@kbn/i18n' ; import { PostTransformsUpdateRequestSchema } from '../../../../../../common/api_schemas/update_transforms' ; import { TransformConfigUnion } from '../../../../../../common/types/transform' ; import { getNestedProperty , setNestedProperty } from '../../../../../../common/utils/object_utils' ; import { isValidFrequency , isValidRetentionPolicyMaxAge , ParsedDuration , } from '../../../../common/validators' ; type EditTransformFormFields = 'description' | 'destinationIndex' | 'destinationPipeline' | 'frequency' | 'docsPerSecond' | 'maxPageSearchSize' | 'retentionPolicyField' | 'retentionPolicyMaxAge' ; type EditTransformFlyoutFieldsState = Record < EditTransformFormFields , FormField > ; export interface FormField { formFieldName : string ; configFieldName : string ; defaultValue : string ; dependsOn : EditTransformFormFields [ ] ; errorMessages : string [ ] ; isNullable : boolean ; isOptional : boolean ; validator : keyof typeof validate ; value : string ; valueParser : ( value : string ) => any ; } type Validator = ( value : any , isOptional ? : boolean ) => string [ ] ; const numberAboveZeroNotValidErrorMessage = i18n . translate ( 'xpack.transform.transformList.editFlyoutFormNumberNotValidErrorMessage' , { defaultMessage : 'Value needs to be an integer above zero.' , } ) ; export const integerAboveZeroValidator = ( value ) => ! isNaN ( value ) && Number . isInteger ( + value ) && + value > 0 && ! ( value + '' ) . includes ( '.' ) ? [ ] : [ numberAboveZeroNotValidErrorMessage ] ; const numberRange10To10000NotValidErrorMessage = i18n . translate ( 'xpack.transform.transformList.editFlyoutFormNumberRange10To10000NotValidErrorMessage' , { defaultMessage : 'Value needs to be an integer between 10 and 10000.' , } ) ; export const integerRange10To10000Validator = ( value ) => integerAboveZeroValidator ( value ) . length === 0 && + value >= 10 && + value <= 10000 ? [ ] : [ numberRange10To10000NotValidErrorMessage ] ; const requiredErrorMessage = i18n . translate ( 'xpack.transform.transformList.editFlyoutFormRequiredErrorMessage' , { defaultMessage : 'Required field.' , } ) ; const stringNotValidErrorMessage = i18n . translate ( 'xpack.transform.transformList.editFlyoutFormStringNotValidErrorMessage' , { defaultMessage : 'Value needs to be of type string.' , } ) ; export const stringValidator = ( value , isOptional = true ) => { if ( typeof value !== 'string' ) { return [ stringNotValidErrorMessage ] ; } if ( value . length === 0 && ! isOptional ) { return [ requiredErrorMessage ] ; } return [ ] ; } ; function parseDurationAboveZero ( arg , errorMessage ) { if ( typeof arg !== 'string' || arg === null ) { return [ stringNotValidErrorMessage ] ; } const regexStr = arg . match ( /[a-z]+|[^a-z]+/gi ) ; if ( regexStr === null || ( Array . isArray ( regexStr ) && regexStr . length !== 2 ) ) { return [ frequencyNotValidErrorMessage ] ; } const number = + regexStr [ 0 ] ; const timeUnit = regexStr [ 1 ] ; if ( isNaN ( number ) || ! Number . isInteger ( number ) || number === 0 ) { return [ frequencyNotValidErrorMessage ] ; } return { number , timeUnit } ; } const frequencyNotValidErrorMessage = i18n . translate ( 'xpack.transform.transformList.editFlyoutFormFrequencyNotValidErrorMessage' , { defaultMessage : 'The frequency value is not valid.' , } ) ; export const frequencyValidator = ( arg ) => { const parsedArg = parseDurationAboveZero ( arg , frequencyNotValidErrorMessage ) ; if ( Array . isArray ( parsedArg ) ) { return parsedArg ; } return isValidFrequency ( parsedArg ) ? [ ] : [ frequencyNotValidErrorMessage ] ; } ; const retentionPolicyMaxAgeNotValidErrorMessage = i18n . translate ( 'xpack.transform.transformList.editFlyoutFormRetentionPolicyMaxAgeNotValidErrorMessage' , { defaultMessage : 'Invalid max age format. Minimum of 60s required.' , } ) ; export const retentionPolicyMaxAgeValidator = ( arg ) => { const parsedArg = parseDurationAboveZero ( arg , retentionPolicyMaxAgeNotValidErrorMessage ) ; if ( Array . isArray ( parsedArg ) ) { return parsedArg ; } return isValidRetentionPolicyMaxAge ( parsedArg ) ? [ ] : [ retentionPolicyMaxAgeNotValidErrorMessage ] ; } ; const validate = { string : stringValidator , frequency : frequencyValidator , integerAboveZero : integerAboveZeroValidator , integerRange10To10000 : integerRange10To10000Validator , retentionPolicyMaxAge : retentionPolicyMaxAgeValidator , } as const ; export const initializeField = ( formFieldName , configFieldName , config , overloads ? ) : FormField => { const defaultValue = overloads ?. defaultValue !== undefined ? overloads . defaultValue : '' ; const rawValue = getNestedProperty ( config , configFieldName , undefined ) ; const value = rawValue !== null && rawValue !== undefined ? rawValue . toString ( ) : '' ; return { formFieldName , configFieldName , defaultValue , dependsOn : [ ] , errorMessages : [ ] , isNullable : false , isOptional : true , validator : 'string' , value , valueParser : ( v ) => v , ... ( overloads !== undefined ? { ... overloads } : { } ) , } ; } ; export interface EditTransformFlyoutState { formFields : EditTransformFlyoutFieldsState ; isFormTouched : boolean ; isFormValid : boolean ; } interface Action { field : EditTransformFormFields ; value : string ; } const getUpdateValue = ( attribute , config , formState , enforceFormValue = false ) => { const formStateAttribute = formState [ attribute ] ; const fallbackValue = formStateAttribute . isNullable ? null : formStateAttribute . defaultValue ; const formValue = formStateAttribute . value !== '' ? formStateAttribute . valueParser ( formStateAttribute . value ) : fallbackValue ; const configValue = getNestedProperty ( config , formStateAttribute . configFieldName , fallbackValue ) ; const dependsOnConfig = enforceFormValue === false ? formStateAttribute . dependsOn . reduce ( ( _dependsOnConfig , dependsOnField ) => { return merge ( { ... _dependsOnConfig } , getUpdateValue ( dependsOnField , config , formState , true ) ) ; } , { } ) : { } ; if ( formValue === formStateAttribute . defaultValue && formStateAttribute . isOptional ) { return formValue !== configValue ? dependsOnConfig : { } ; } return formValue !== configValue || enforceFormValue ? setNestedProperty ( dependsOnConfig , formStateAttribute . configFieldName , formValue ) : { } ; } ; export const applyFormFieldsToTransformConfig = ( config , formState ) : PostTransformsUpdateRequestSchema => ( Object . keys ( formState ) as EditTransformFormFields [ ] ) . reduce ( ( updateConfig , field ) => merge ( { ... updateConfig } , getUpdateValue ( field , config , formState ) ) , { } ) ; export const getDefaultState = ( config ) : EditTransformFlyoutState => ( { formFields : { description : initializeField ( 'description' , 'description' , config ) , frequency : initializeField ( 'frequency' , 'frequency' , config , { defaultValue : '1m' , validator : 'frequency' , } ) , destinationIndex : initializeField ( 'destinationIndex' , 'dest.index' , config , { dependsOn : [ 'destinationPipeline' ] , isOptional : false , } ) , destinationPipeline : initializeField ( 'destinationPipeline' , 'dest.pipeline' , config , { dependsOn : [ 'destinationIndex' ] , } ) , docsPerSecond : initializeField ( 'docsPerSecond' , 'settings.docs_per_second' , config , { isNullable : true , validator : 'integerAboveZero' , valueParser : ( v ) => ( v === '' ? null : + v ) , } ) , maxPageSearchSize : initializeField ( 'maxPageSearchSize' , 'settings.max_page_search_size' , config , { defaultValue : '500' , validator : 'integerRange10To10000' , valueParser : ( v ) => + v , } ) , retentionPolicyField : initializeField ( 'retentionPolicyField' , 'retention_policy.time.field' , config , { dependsOn : [ 'retentionPolicyMaxAge' ] , isNullable : false , isOptional : true } ) , retentionPolicyMaxAge : initializeField ( 'retentionPolicyMaxAge' , 'retention_policy.time.max_age' , config , { dependsOn : [ 'retentionPolicyField' ] , isNullable : false , isOptional : true , validator : 'retentionPolicyMaxAge' , } ) , } , isFormTouched : false , isFormValid : true , } ) ; const isFormValid = ( fieldsState ) => ( Object . keys ( fieldsState ) as EditTransformFormFields [ ] ) . reduce ( ( p , c ) => p && fieldsState [ c ] . errorMessages . length === 0 , true ) ; const formFieldReducer = ( state , value ) : FormField => { return { ... state , errorMessages : state . isOptional && typeof value === 'string' && value . length === 0 ? [ ] : validate [ state . validator ] ( value , state . isOptional ) , value , } ; } ; export const formReducerFactory = ( config ) => { const defaultState = getDefaultState ( config ) ; const defaultFieldValues = Object . values ( defaultState . formFields ) . map ( ( f ) => f . value ) ; return ( state , { field , value } ) : EditTransformFlyoutState => { const formFields = { ... state . formFields , [ field ] : formFieldReducer ( state . formFields [ field ] , value ) , } ; return { ... state , formFields , isFormTouched : ! isEqual ( defaultFieldValues , Object . values ( formFields ) . map ( ( f ) => f . value ) ) , isFormValid : isFormValid ( formFields ) , } ; } ; } ; export const useEditTransformFlyout = ( config ) => { return useReducer ( formReducerFactory ( config ) , getDefaultState ( config ) ) ; } ; export type UseEditTransformFlyoutReturnType = ReturnType < typeof useEditTransformFlyout > ;