import { i18n } from '@kbn/i18n' ; import { memoize , isEqual } from 'lodash' ; import numeral from '@elastic/numeral' ; import { isValidIndexName } from '../../../../../../../common/util/es_utils' ; import { collapseLiteralStrings } from '../../../../../../../shared_imports' ; import { Action , ACTION } from './actions' ; import { getInitialState , getFormStateFromJobConfig , getJobConfigFromFormState , State , } from './state' ; import { isJobIdValid , validateModelMemoryLimitUnits , } from '../../../../../../../common/util/job_utils' ; import { composeValidators , maxLengthValidator , memoryInputValidator , requiredValidator , } from '../../../../../../../common/util/validators' ; import { JOB_ID_MAX_LENGTH , ALLOWED_DATA_UNITS , } from '../../../../../../../common/constants/validation' ; import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics' ; import { getDependentVar , getNumTopFeatureImportanceValues , getTrainingPercent , isRegressionAnalysis , isClassificationAnalysis , NUM_TOP_FEATURE_IMPORTANCE_VALUES_MIN , TRAINING_PERCENT_MIN , TRAINING_PERCENT_MAX , } from '../../../../common/analytics' ; import { indexPatterns } from '../../../../../../../../../../src/plugins/data/public' ; import { isAdvancedConfig } from '../../components/action_clone/clone_action_name' ; const mmlAllowedUnitsStr = `${ ALLOWED_DATA_UNITS . slice ( 0 , ALLOWED_DATA_UNITS . length - 1 ) . join ( ', ' ) } or ${ [ ... ALLOWED_DATA_UNITS ] . pop ( ) }` ; export const mmlUnitInvalidErrorMessage = i18n . translate ( 'xpack.ml.dataframe.analytics.create.modelMemoryUnitsInvalidError' , { defaultMessage : 'Model memory limit data unit unrecognized. It must be {str}' , values : { str : mmlAllowedUnitsStr } , } ) ; param mmlValidationResult export function getModelMemoryLimitErrors ( mmlValidationResult ) { if ( mmlValidationResult === null ) { return null ; } return Object . keys ( mmlValidationResult ) . reduce ( ( acc , errorKey ) => { if ( errorKey === 'min' ) { acc . push ( i18n . translate ( 'xpack.ml.dataframe.analytics.create.modelMemoryUnitsMinError' , { defaultMessage : 'Model memory limit is lower than estimated value {mml}' , values : { mml : mmlValidationResult . min . minValue , } , } ) ) ; } if ( errorKey === 'invalidUnits' ) { acc . push ( i18n . translate ( 'xpack.ml.dataframe.analytics.create.modelMemoryUnitsInvalidError' , { defaultMessage : 'Model memory limit data unit unrecognized. It must be {str}' , values : { str : mmlAllowedUnitsStr } , } ) ) ; } return acc ; } , [ ] as string [ ] ) ; } const getSourceIndexString = ( state ) => { const { jobConfig } = state ; const sourceIndex = jobConfig ?. source ?. index ; if ( typeof sourceIndex === 'string' ) { return sourceIndex ; } if ( Array . isArray ( sourceIndex ) ) { return sourceIndex . join ( ',' ) ; } return '' ; } ; const isSourceIndexNameValid = ( sourceIndexName , sourceIndex ) => { let sourceIndexNameValid = Object . keys ( indexPatterns . validate ( sourceIndexName ) ) . length === 0 ; if ( sourceIndexNameValid ) { if ( typeof sourceIndex === 'string' ) { sourceIndexNameValid = ! sourceIndex . includes ( ',' ) ; } if ( Array . isArray ( sourceIndex ) ) { sourceIndexNameValid = ! sourceIndex . some ( ( d ) => d ?. includes ( ',' ) ) ; } } return sourceIndexNameValid ; } ; /**
 * Validates num_top_feature_importance_values. Must be an integer >= 0.
 */ export const validateNumTopFeatureImportanceValues = ( numTopFeatureImportanceValues ) : boolean => { return ( typeof numTopFeatureImportanceValues === 'number' && numTopFeatureImportanceValues >= NUM_TOP_FEATURE_IMPORTANCE_VALUES_MIN && Number . isInteger ( numTopFeatureImportanceValues ) ) ; } ; export const validateAdvancedEditor = ( state ) : State => { const { jobIdEmpty , jobIdValid , jobIdExists , jobType , createIndexPattern } = state . form ; const { jobConfig } = state ; state . advancedEditorMessages = [ ] ; const sourceIndexName = getSourceIndexString ( state ) ; const sourceIndexNameEmpty = sourceIndexName === '' ; const sourceIndex = jobConfig ?. source ?. index ; const sourceIndexNameValid = isSourceIndexNameValid ( sourceIndexName , sourceIndex ) ; const destinationIndexName = jobConfig ?. dest ?. index ?? '' ; const destinationIndexNameEmpty = destinationIndexName === '' ; const destinationIndexNameValid = isValidIndexName ( destinationIndexName ) ; const destinationIndexPatternTitleExists = state . indexPatternsMap [ destinationIndexName ] !== undefined ; const analyzedFields = jobConfig ?. analyzed_fields ?. includes || [ ] ; const resultsFieldEmptyString = typeof jobConfig ?. dest ?. results_field === 'string' && jobConfig ?. dest ?. results_field . trim ( ) === '' ; const mml = jobConfig . model_memory_limit ; const modelMemoryLimitEmpty = mml === '' || mml === undefined ; if ( ! modelMemoryLimitEmpty && mml !== undefined ) { const { valid } = validateModelMemoryLimitUnits ( mml ) ; state . form . modelMemoryLimitUnitValid = valid ; } let dependentVariableEmpty = false ; let includesValid = true ; let trainingPercentValid = true ; let numTopFeatureImportanceValuesValid = true ; if ( jobConfig . analysis === undefined && ( jobType === ANALYSIS_CONFIG_TYPE . CLASSIFICATION || jobType === ANALYSIS_CONFIG_TYPE . REGRESSION ) ) { dependentVariableEmpty = true ; } if ( jobConfig . analysis !== undefined && ( isRegressionAnalysis ( jobConfig . analysis ) || isClassificationAnalysis ( jobConfig . analysis ) ) ) { const dependentVariableName = getDependentVar ( jobConfig . analysis ) || '' ; dependentVariableEmpty = dependentVariableName === '' ; if ( ! dependentVariableEmpty && analyzedFields . length > 0 && ! analyzedFields . includes ( dependentVariableName ) ) { includesValid = false ; state . advancedEditorMessages . push ( { error : i18n . translate ( 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.includesInvalid' , { defaultMessage : 'The dependent variable must be included.' , } ) , message : '' , } ) ; } const trainingPercent = getTrainingPercent ( jobConfig . analysis ) ; if ( trainingPercent !== undefined && ( isNaN ( trainingPercent ) || typeof trainingPercent !== 'number' || trainingPercent < TRAINING_PERCENT_MIN || trainingPercent > TRAINING_PERCENT_MAX ) ) { trainingPercentValid = false ; state . advancedEditorMessages . push ( { error : i18n . translate ( 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.trainingPercentInvalid' , { defaultMessage : 'The training percent must be a number between {min} and {max}.' , values : { min : TRAINING_PERCENT_MIN , max : TRAINING_PERCENT_MAX , } , } ) , message : '' , } ) ; } const numTopFeatureImportanceValues = getNumTopFeatureImportanceValues ( jobConfig . analysis ) ; if ( numTopFeatureImportanceValues !== undefined ) { numTopFeatureImportanceValuesValid = validateNumTopFeatureImportanceValues ( numTopFeatureImportanceValues ) ; if ( numTopFeatureImportanceValuesValid === false ) { state . advancedEditorMessages . push ( { error : i18n . translate ( 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.numTopFeatureImportanceValuesInvalid' , { defaultMessage : 'The value for num_top_feature_importance_values must be an integer of {min} or higher.' , values : { min : 0 , } , } ) , message : '' , } ) ; } } } if ( sourceIndexNameEmpty ) { state . advancedEditorMessages . push ( { error : i18n . translate ( 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.sourceIndexNameEmpty' , { defaultMessage : 'The source index name must not be empty.' , } ) , message : '' , } ) ; } else if ( ! sourceIndexNameValid ) { state . advancedEditorMessages . push ( { error : i18n . translate ( 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.sourceIndexNameValid' , { defaultMessage : 'Invalid source index name.' , } ) , message : '' , } ) ; } if ( destinationIndexNameEmpty ) { state . advancedEditorMessages . push ( { error : i18n . translate ( 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameEmpty' , { defaultMessage : 'The destination index name must not be empty.' , } ) , message : '' , } ) ; } else if ( destinationIndexPatternTitleExists && ! createIndexPattern ) { state . advancedEditorMessages . push ( { error : i18n . translate ( 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameExistsWarn' , { defaultMessage : 'An index with this destination index name already exists. Be aware that running this analytics job will modify this destination index.' , } ) , message : '' , } ) ; } else if ( ! destinationIndexNameValid ) { state . advancedEditorMessages . push ( { error : i18n . translate ( 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameValid' , { defaultMessage : 'Invalid destination index name.' , } ) , message : '' , } ) ; } if ( resultsFieldEmptyString ) { state . advancedEditorMessages . push ( { error : i18n . translate ( 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.resultsFieldEmptyString' , { defaultMessage : 'The results field must not be an empty string.' , } ) , message : '' , } ) ; } if ( dependentVariableEmpty ) { state . advancedEditorMessages . push ( { error : i18n . translate ( 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.dependentVariableEmpty' , { defaultMessage : 'The dependent variable field must not be empty.' , } ) , message : '' , } ) ; } if ( modelMemoryLimitEmpty ) { state . advancedEditorMessages . push ( { error : i18n . translate ( 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.modelMemoryLimitEmpty' , { defaultMessage : 'The model memory limit field must not be empty.' , } ) , message : '' , } ) ; } if ( ! state . form . modelMemoryLimitUnitValid ) { state . advancedEditorMessages . push ( { error : mmlUnitInvalidErrorMessage , message : '' , } ) ; } state . form . destinationIndexPatternTitleExists = destinationIndexPatternTitleExists ; state . isValid = includesValid && trainingPercentValid && state . form . modelMemoryLimitUnitValid && ! jobIdEmpty && jobIdValid && ! jobIdExists && ! sourceIndexNameEmpty && sourceIndexNameValid && ! destinationIndexNameEmpty && destinationIndexNameValid && ! resultsFieldEmptyString && ! dependentVariableEmpty && ! modelMemoryLimitEmpty && numTopFeatureImportanceValuesValid && ( ! destinationIndexPatternTitleExists || ! createIndexPattern ) ; return state ; } ; /**
 * Validates provided MML isn't lower than the estimated one.
 */ export function validateMinMML ( estimatedMml ) { return ( mml ) => { if ( ! mml || ! estimatedMml ) { return null ; } const mmlInBytes = numeral ( mml . toUpperCase ( ) ) . value ( ) ; const estimatedMmlInBytes = numeral ( estimatedMml . toUpperCase ( ) ) . value ( ) ; return estimatedMmlInBytes > mmlInBytes ? { min : { minValue : estimatedMml , actualValue : mml } } : null ; } ; } /**
 * Result validator function for the MML.
 * Re-init only if the estimated mml has been changed.
 */ const mmlValidator = memoize ( ( estimatedMml ) => composeValidators ( requiredValidator ( ) , validateMinMML ( estimatedMml ) , memoryInputValidator ( ) ) ) ; const validateMml = memoize ( ( estimatedMml , mml ) => mmlValidator ( estimatedMml ) ( mml ) , ( ... args ) => args . join ( '_' ) ) ; const validateForm = ( state ) : State => { const { jobIdEmpty , jobIdValid , jobIdExists , jobType , sourceIndexNameEmpty , sourceIndexNameValid , destinationIndexNameEmpty , destinationIndexNameValid , destinationIndexPatternTitleExists , createIndexPattern , dependentVariable , modelMemoryLimit , numTopFeatureImportanceValuesValid , } = state . form ; const { estimatedModelMemoryLimit } = state ; const jobTypeEmpty = jobType === undefined ; const dependentVariableEmpty = ( jobType === ANALYSIS_CONFIG_TYPE . REGRESSION || jobType === ANALYSIS_CONFIG_TYPE . CLASSIFICATION ) && dependentVariable === '' ; const mmlValidationResult = validateMml ( estimatedModelMemoryLimit , modelMemoryLimit ) ; const mmlInvalid = mmlValidationResult !== null && ( mmlValidationResult . invalidUnits !== undefined || mmlValidationResult . required === true ) ; state . form . modelMemoryLimitValidationResult = mmlValidationResult ; state . isValid = ! jobTypeEmpty && ! mmlInvalid && ! jobIdEmpty && jobIdValid && ! jobIdExists && ! sourceIndexNameEmpty && sourceIndexNameValid && ! destinationIndexNameEmpty && destinationIndexNameValid && ! dependentVariableEmpty && numTopFeatureImportanceValuesValid && ( ! destinationIndexPatternTitleExists || ! createIndexPattern ) ; return state ; } ; export function reducer ( state , action ) { switch ( action . type ) { case ACTION . ADD_REQUEST_MESSAGE : const requestMessages = state . requestMessages ; requestMessages . push ( action . requestMessage ) ; return { ... state , requestMessages } ; case ACTION . RESET_REQUEST_MESSAGES : return { ... state , requestMessages : [ ] } ; case ACTION . RESET_ADVANCED_EDITOR_MESSAGES : return { ... state , advancedEditorMessages : [ ] } ; case ACTION . RESET_FORM : return getInitialState ( ) ; case ACTION . SET_ADVANCED_EDITOR_RAW_STRING : let resultJobConfig ; let disableSwitchToForm = false ; try { resultJobConfig = JSON . parse ( collapseLiteralStrings ( action . advancedEditorRawString ) ) ; const runtimeMappingsChanged = state . form . runtimeMappings && resultJobConfig . source . runtime_mappings && ! isEqual ( state . form . runtimeMappings , resultJobConfig . source . runtime_mappings ) ; disableSwitchToForm = isAdvancedConfig ( resultJobConfig ) || runtimeMappingsChanged ; } catch ( e ) { return { ... state , advancedEditorRawString : action . advancedEditorRawString , isAdvancedEditorValidJson : false , disableSwitchToForm : true , advancedEditorMessages : [ ] , } ; } return { ... validateAdvancedEditor ( { ... state , jobConfig : resultJobConfig } ) , advancedEditorRawString : action . advancedEditorRawString , isAdvancedEditorValidJson : true , disableSwitchToForm , } ; case ACTION . SET_FORM_STATE : const newFormState = { ... state . form , ... action . payload } ; if ( action . payload . destinationIndex !== undefined ) { newFormState . destinationIndexNameEmpty = newFormState . destinationIndex === '' ; newFormState . destinationIndexNameValid = isValidIndexName ( newFormState . destinationIndex ) ; newFormState . destinationIndexPatternTitleExists = state . indexPatternsMap [ newFormState . destinationIndex ] !== undefined ; } if ( action . payload . jobId !== undefined ) { newFormState . jobIdEmpty = newFormState . jobId === '' ; newFormState . jobIdValid = isJobIdValid ( newFormState . jobId ) ; newFormState . jobIdInvalidMaxLength = ! ! maxLengthValidator ( JOB_ID_MAX_LENGTH ) ( newFormState . jobId ) ; } if ( action . payload . sourceIndex !== undefined ) { newFormState . sourceIndexNameEmpty = newFormState . sourceIndex === '' ; const validationMessages = indexPatterns . validate ( newFormState . sourceIndex ) ; newFormState . sourceIndexNameValid = Object . keys ( validationMessages ) . length === 0 ; } if ( action . payload . numTopFeatureImportanceValues !== undefined ) { newFormState . numTopFeatureImportanceValuesValid = validateNumTopFeatureImportanceValues ( newFormState ?. numTopFeatureImportanceValues ) ; } return state . isAdvancedEditorEnabled ? validateAdvancedEditor ( { ... state , form : newFormState } ) : validateForm ( { ... state , form : newFormState } ) ; case ACTION . SET_INDEX_PATTERN_TITLES : { const newState = { ... state , ... action . payload , } ; newState . form . destinationIndexPatternTitleExists = newState . indexPatternsMap [ newState . form . destinationIndex ] !== undefined ; return newState ; } case ACTION . SET_IS_JOB_CREATED : return { ... state , isJobCreated : action . isJobCreated } ; case ACTION . SET_IS_JOB_STARTED : return { ... state , isJobStarted : action . isJobStarted } ; case ACTION . SET_JOB_CONFIG : return validateAdvancedEditor ( { ... state , jobConfig : action . payload } ) ; case ACTION . SWITCH_TO_ADVANCED_EDITOR : const jobConfig = getJobConfigFromFormState ( state . form ) ; const shouldDisableSwitchToForm = isAdvancedConfig ( jobConfig ) ; return validateAdvancedEditor ( { ... state , advancedEditorRawString : JSON . stringify ( jobConfig , null , 2 ) , isAdvancedEditorEnabled : true , disableSwitchToForm : shouldDisableSwitchToForm , hasSwitchedToEditor : true , jobConfig , } ) ; case ACTION . SWITCH_TO_FORM : const { jobConfig : config } = state ; const { jobId } = state . form ; const formState = getFormStateFromJobConfig ( config , false ) ; if ( typeof jobId === 'string' && jobId . trim ( ) !== '' ) { formState . jobId = jobId ; } formState . jobIdEmpty = jobId === '' ; formState . jobIdValid = isJobIdValid ( jobId ) ; formState . jobIdInvalidMaxLength = ! ! maxLengthValidator ( JOB_ID_MAX_LENGTH ) ( jobId ) ; formState . destinationIndexNameEmpty = formState . destinationIndex === '' ; formState . destinationIndexNameValid = isValidIndexName ( formState . destinationIndex || '' ) ; formState . destinationIndexPatternTitleExists = state . indexPatternsMap [ formState . destinationIndex || '' ] !== undefined ; if ( formState . numTopFeatureImportanceValues !== undefined ) { formState . numTopFeatureImportanceValuesValid = validateNumTopFeatureImportanceValues ( formState . numTopFeatureImportanceValues ) ; } const sourceIndexName = getSourceIndexString ( state ) ; const sourceIndex = config ?. source ?. index ; const sourceIndexNameValid = isSourceIndexNameValid ( sourceIndexName , sourceIndex ) ; formState . sourceIndexNameValid = sourceIndexNameValid ; return validateForm ( { ... state , form : formState , isAdvancedEditorEnabled : false , advancedEditorRawString : JSON . stringify ( config , null , 2 ) , jobConfig : config , } ) ; case ACTION . SET_ESTIMATED_MODEL_MEMORY_LIMIT : return { ... state , estimatedModelMemoryLimit : action . value , } ; case ACTION . SET_JOB_CLONE : return { ... state , cloneJob : action . cloneJob , } ; } return state ; }