From ef9170c8de0565d080fde2f8579151a892118c31 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 10 Mar 2020 16:45:17 +0100 Subject: [PATCH 01/26] [ML] clone analytics job --- .../analytics_list/action_clone.tsx | 41 ++++++++++++++++++ .../components/analytics_list/actions.tsx | 9 +++- .../analytics_list/analytics_list.tsx | 7 ++- .../components/analytics_list/columns.tsx | 7 ++- .../create_analytics_form.tsx | 43 +++++++++++++------ .../use_create_analytics_form/actions.ts | 2 +- .../hooks/use_create_analytics_form/state.ts | 30 +++++++++++++ 7 files changed, 120 insertions(+), 19 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx new file mode 100644 index 00000000000000..d314fa904847fa --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonEmpty } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { FC } from 'react'; +import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; +import { getCloneFormStateFromJobConfig } from '../../hooks/use_create_analytics_form/state'; +import { DataFrameAnalyticsListRow } from './common'; + +interface CloneActionProps extends CreateAnalyticsFormProps { + item: DataFrameAnalyticsListRow; +} + +export const CloneAction: FC = ({ item, actions }) => { + const buttonText = i18n.translate('xpack.ml.dataframe.analyticsList.cloneJobButtonLabel', { + defaultMessage: 'Clone job', + }); + + const onClick = async () => { + await actions.openModal(); + + actions.setFormState(getCloneFormStateFromJobConfig(item!.config)); + }; + + return ( + + {buttonText} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx index eb87bfd96c149f..34b36a42b5d43d 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx @@ -19,6 +19,8 @@ import { isOutlierAnalysis, isClassificationAnalysis, } from '../../../../common/analytics'; +import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; +import { CloneAction } from './action_clone'; import { getResultsUrl, isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; import { stopAnalytics } from '../../services/analytics_service'; @@ -57,7 +59,7 @@ export const AnalyticsViewAction = { }, }; -export const getActions = () => { +export const getActions = (createAnalyticsForm: CreateAnalyticsFormProps) => { const canStartStopDataFrameAnalytics: boolean = checkPermission('canStartStopDataFrameAnalytics'); return [ @@ -104,5 +106,10 @@ export const getActions = () => { return ; }, }, + { + render: (item: DataFrameAnalyticsListRow) => { + return ; + }, + }, ]; }; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 412779513e533d..10be0a74e17e61 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -254,7 +254,8 @@ export const DataFrameAnalyticsList: FC = ({ expandedRowItemIds, setExpandedRowItemIds, isManagementTable, - isMlEnabledInSpace + isMlEnabledInSpace, + createAnalyticsForm ); const sorting = { @@ -375,6 +376,10 @@ export const DataFrameAnalyticsList: FC = ({ })} /> + + {!isManagementTable && createAnalyticsForm?.state.isModalVisible && ( + + )} ); }; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx index 07ae2c176c3632..00cd9e3f1e0ddc 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx @@ -20,6 +20,7 @@ import { } from '@elastic/eui'; import { getAnalysisType, DataFrameAnalyticsId } from '../../../../common'; +import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; import { getDataFrameAnalyticsProgress, isDataFrameAnalyticsFailed, @@ -125,9 +126,11 @@ export const getColumns = ( expandedRowItemIds: DataFrameAnalyticsId[], setExpandedRowItemIds: React.Dispatch>, isManagementTable: boolean = false, - isMlEnabledInSpace: boolean = true + isMlEnabledInSpace: boolean = true, + createAnalyticsForm?: CreateAnalyticsFormProps ) => { - const actions = isManagementTable === true ? [AnalyticsViewAction] : getActions(); + const actions = + isManagementTable === true ? [AnalyticsViewAction] : getActions(createAnalyticsForm!); function toggleDetails(item: DataFrameAnalyticsListRow) { const index = expandedRowItemIds.indexOf(item.config.id); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index c744c357c9550d..bd0a453d9e66a7 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -23,7 +23,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { useMlKibana } from '../../../../../contexts/kibana'; import { ml } from '../../../../../services/ml_api_service'; -import { Field } from '../../../../../../../common/types/fields'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { useMlContext } from '../../../../../contexts/ml'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; @@ -31,6 +30,7 @@ import { JOB_TYPES, DEFAULT_MODEL_MEMORY_LIMIT, getJobConfigFromFormState, + State, } from '../../hooks/use_create_analytics_form/state'; import { JOB_ID_MAX_LENGTH } from '../../../../../../../common/constants/validation'; import { Messages } from './messages'; @@ -210,11 +210,10 @@ export const CreateAnalyticsForm: FC = ({ actions, sta } }, 400); - const loadDepVarOptions = async () => { + const loadDepVarOptions = async (formState: State['form']) => { setFormState({ loadingDepVarOptions: true, // clear when the source index changes - dependentVariable: '', maxDistinctValuesError: undefined, sourceIndexFieldsCheckFailed: false, sourceIndexContainsNumericalFields: true, @@ -225,23 +224,39 @@ export const CreateAnalyticsForm: FC = ({ actions, sta ); if (indexPattern !== undefined) { + const formStateUpdate: { + loadingDepVarOptions: boolean; + dependentVariableFetchFail: boolean; + dependentVariableOptions: State['form']['dependentVariableOptions']; + dependentVariable?: State['form']['dependentVariable']; + } = { + loadingDepVarOptions: false, + dependentVariableFetchFail: false, + dependentVariableOptions: [] as State['form']['dependentVariableOptions'], + }; + await newJobCapsService.initializeFromIndexPattern(indexPattern); // Get fields and filter for supported types for job type const { fields } = newJobCapsService; - const depVarOptions: EuiComboBoxOptionOption[] = []; - - fields.forEach((field: Field) => { + let resetDependentVariable = true; + for (const field of fields) { if (shouldAddAsDepVarOption(field, jobType)) { - depVarOptions.push({ label: field.id }); + formStateUpdate.dependentVariableOptions.push({ + label: field.id, + }); + + if (formState.dependentVariable === field.id) { + resetDependentVariable = false; + } } - }); + } - setFormState({ - dependentVariableOptions: depVarOptions, - loadingDepVarOptions: false, - dependentVariableFetchFail: false, - }); + if (resetDependentVariable) { + formStateUpdate.dependentVariable = ''; + } + + setFormState(formStateUpdate); } } catch (e) { setFormState({ loadingDepVarOptions: false, dependentVariableFetchFail: true }); @@ -287,7 +302,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta useEffect(() => { if (isJobTypeWithDepVar && sourceIndexNameEmpty === false) { - loadDepVarOptions(); + loadDepVarOptions(form); } if (jobType === JOB_TYPES.OUTLIER_DETECTION && sourceIndexNameEmpty === false) { diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts index 70228f0238fda0..e7487f020ee66d 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts @@ -67,7 +67,7 @@ export type Action = export interface ActionDispatchers { closeModal: () => void; createAnalyticsJob: () => void; - openModal: () => void; + openModal: () => Promise; resetAdvancedEditorMessages: () => void; setAdvancedEditorRawString: (payload: State['advancedEditorRawString']) => void; setFormState: (payload: Partial) => void; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 170700d35e6511..1955bd2883e581 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -187,3 +187,33 @@ export const getJobConfigFromFormState = ( return jobConfig; }; + +/** + * Extracts form state for a job clone from the analytics job configuration. + * For cloning we keep job id and destination index empty. + */ +export function getCloneFormStateFromJobConfig( + analyticsJobConfig: DataFrameAnalyticsConfig +): Partial { + const jobType: string = Object.keys(analyticsJobConfig.analysis)[0]; + + const resultState: Partial = { + jobType: jobType as AnalyticsJobType, + description: analyticsJobConfig.description ?? '', + sourceIndex: Array.isArray(analyticsJobConfig.source.index) + ? analyticsJobConfig.source.index.join(',') + : analyticsJobConfig.source.index, + modelMemoryLimit: analyticsJobConfig.model_memory_limit, + excludes: analyticsJobConfig.analyzed_fields.excludes, + }; + + if (jobType === JOB_TYPES.REGRESSION || jobType === JOB_TYPES.CLASSIFICATION) { + // @ts-ignore + const analysisConfig = analyticsJobConfig.analysis[jobType]; + + resultState.dependentVariable = analysisConfig!.dependent_variable; + resultState.trainingPercent = analysisConfig!.training_percent; + } + + return resultState; +} From 86be82eca693f35a6749be198d2545b26ee6d465 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 10 Mar 2020 17:31:47 +0100 Subject: [PATCH 02/26] [ML] flyout clone header --- .../analytics_list/action_clone.tsx | 1 - .../create_analytics_flyout.tsx | 22 ++++++++++++++----- .../hooks/use_create_analytics_form/state.ts | 2 ++ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx index d314fa904847fa..a2dd3f01c1262f 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx @@ -22,7 +22,6 @@ export const CloneAction: FC = ({ item, actions }) => { const onClick = async () => { await actions.openModal(); - actions.setFormState(getCloneFormStateFromJobConfig(item!.config)); }; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx index e31c12e2c62d0e..ad70f29b19e7dd 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx @@ -26,17 +26,27 @@ export const CreateAnalyticsFlyout: FC = ({ state, }) => { const { closeModal, createAnalyticsJob, startAnalyticsJob } = actions; - const { isJobCreated, isJobStarted, isModalButtonDisabled, isValid } = state; + const { + isJobCreated, + isJobStarted, + isModalButtonDisabled, + isValid, + form: { isClone }, + } = state; + + const headerText = isClone + ? i18n.translate('xpack.ml.dataframe.analytics.clone.flyoutHeaderTitle', { + defaultMessage: 'Clone analytics job', + }) + : i18n.translate('xpack.ml.dataframe.analytics.create.flyoutHeaderTitle', { + defaultMessage: 'Create analytics job', + }); return ( -

- {i18n.translate('xpack.ml.dataframe.analytics.create.flyoutHeaderTitle', { - defaultMessage: 'Create analytics job', - })} -

+

{headerText}

{children} diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 1955bd2883e581..b59c76d5eef6a4 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -76,6 +76,7 @@ export interface State { sourceIndexContainsNumericalFields: boolean; sourceIndexFieldsCheckFailed: boolean; trainingPercent: number; + isClone?: boolean; }; disabled: boolean; indexNames: EsIndexName[]; @@ -198,6 +199,7 @@ export function getCloneFormStateFromJobConfig( const jobType: string = Object.keys(analyticsJobConfig.analysis)[0]; const resultState: Partial = { + isClone: true, jobType: jobType as AnalyticsJobType, description: analyticsJobConfig.description ?? '', sourceIndex: Array.isArray(analyticsJobConfig.source.index) From 49234238e15b7351f2fcd38c167b5b5608e235a4 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 10 Mar 2020 17:49:43 +0100 Subject: [PATCH 03/26] [ML] improve clone action context menu item --- .../analytics_list/action_clone.tsx | 33 +++++++------------ .../components/analytics_list/actions.tsx | 8 ++--- 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx index a2dd3f01c1262f..ab1d6d9e46507e 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx @@ -4,37 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { FC } from 'react'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; import { getCloneFormStateFromJobConfig } from '../../hooks/use_create_analytics_form/state'; import { DataFrameAnalyticsListRow } from './common'; -interface CloneActionProps extends CreateAnalyticsFormProps { - item: DataFrameAnalyticsListRow; -} - -export const CloneAction: FC = ({ item, actions }) => { +export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) { const buttonText = i18n.translate('xpack.ml.dataframe.analyticsList.cloneJobButtonLabel', { defaultMessage: 'Clone job', }); - const onClick = async () => { + const { actions } = createAnalyticsForm; + + const onClick = async (item: DataFrameAnalyticsListRow) => { await actions.openModal(); actions.setFormState(getCloneFormStateFromJobConfig(item!.config)); }; - return ( - - {buttonText} - - ); -}; + return { + name: buttonText, + description: buttonText, + icon: 'copy', + onClick, + 'data-test-subj': 'mlAnalyticsJobCloneButton', + }; +} diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx index 34b36a42b5d43d..4d26704e2423d4 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx @@ -20,7 +20,7 @@ import { isClassificationAnalysis, } from '../../../../common/analytics'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; -import { CloneAction } from './action_clone'; +import { getCloneAction } from './action_clone'; import { getResultsUrl, isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; import { stopAnalytics } from '../../services/analytics_service'; @@ -106,10 +106,6 @@ export const getActions = (createAnalyticsForm: CreateAnalyticsFormProps) => { return ; }, }, - { - render: (item: DataFrameAnalyticsListRow) => { - return ; - }, - }, + getCloneAction(createAnalyticsForm), ]; }; From 307b32b94c772f745046734f4a1da19456f015c5 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Wed, 11 Mar 2020 16:05:30 +0100 Subject: [PATCH 04/26] [ML] support advanced job cloning --- .../analytics_list/action_clone.test.ts | 222 ++++++++++++++++ .../components/analytics_list/action_clone.ts | 249 ++++++++++++++++++ .../analytics_list/action_clone.tsx | 31 --- .../use_create_analytics_form/reducer.ts | 7 +- 4 files changed, 477 insertions(+), 32 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts new file mode 100644 index 00000000000000..3eae7bc587e382 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts @@ -0,0 +1,222 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isAdvancedConfig } from './action_clone'; + +describe('Analytics job clone action', () => { + describe('isAdvancedConfig', () => { + test('should detect a classification job created with the form', () => { + const formCreatedClassificationJob = { + id: 'bank_classification_1', + description: "Classification job with 'bank-marketing' dataset", + source: { + index: ['bank-marketing'], + query: { + match_all: {}, + }, + }, + dest: { + index: 'dest_bank_1', + results_field: 'ml', + }, + analysis: { + classification: { + dependent_variable: 'y', + num_top_classes: 2, + prediction_field_name: 'y_prediction', + training_percent: 2, + randomize_seed: 6233212276062807000, + }, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '350mb', + create_time: 1583417086689, + version: '8.0.0', + allow_lazy_start: false, + }; + + expect(isAdvancedConfig(formCreatedClassificationJob)).toBe(false); + }); + + test('should detect a outlier_detection job created with the form', () => { + const formCreatedOutlierDetectionJob = { + id: 'glass_outlier_detection_1', + description: "Outlier detection job with 'glass' dataset", + source: { + index: ['glass_withoutdupl_norm'], + query: { + match_all: {}, + }, + }, + dest: { + index: 'dest_glass_1', + results_field: 'ml', + }, + analysis: { + outlier_detection: { + compute_feature_influence: true, + outlier_fraction: 0.05, + standardization_enabled: true, + }, + }, + analyzed_fields: { + includes: [], + excludes: ['id', 'outlier'], + }, + model_memory_limit: '1mb', + create_time: 1583417347446, + version: '8.0.0', + allow_lazy_start: false, + }; + expect(isAdvancedConfig(formCreatedOutlierDetectionJob)).toBe(false); + }); + + test('should detect a regression job created with the form', () => { + const formCreatedRegressionJob = { + id: 'grid_regression_1', + description: "Regression job with 'electrical-grid-stability' dataset", + source: { + index: ['electrical-grid-stability'], + query: { + match_all: {}, + }, + }, + dest: { + index: 'dest_grid_1', + results_field: 'ml', + }, + analysis: { + regression: { + dependent_variable: 'stab', + prediction_field_name: 'stab_prediction', + training_percent: 20, + randomize_seed: -2228827740028660200, + }, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '150mb', + create_time: 1583417178919, + version: '8.0.0', + allow_lazy_start: false, + }; + + expect(isAdvancedConfig(formCreatedRegressionJob)).toBe(false); + }); + + test('should detect advanced classification job', () => { + const advancedClassificationJob = { + id: 'bank_classification_1', + description: "Classification job with 'bank-marketing' dataset", + source: { + index: ['bank-marketing'], + query: { + match_all: {}, + }, + }, + dest: { + index: 'dest_bank_1', + results_field: 'CUSTOM_RESULT_FIELD', + }, + analysis: { + classification: { + dependent_variable: 'y', + num_top_classes: 2, + prediction_field_name: 'y_prediction', + training_percent: 2, + randomize_seed: 6233212276062807000, + }, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '350mb', + create_time: 1583417086689, + version: '8.0.0', + allow_lazy_start: false, + }; + + expect(isAdvancedConfig(advancedClassificationJob)).toBe(true); + }); + + test('should detect advanced outlier_detection job', () => { + const advancedOutlierDetectionJob = { + id: 'glass_outlier_detection_1', + description: "Outlier detection job with 'glass' dataset", + source: { + index: ['glass_withoutdupl_norm'], + query: { + // TODO check default for `match` + match_all: {}, + }, + }, + dest: { + index: 'dest_glass_1', + results_field: 'ml', + }, + analysis: { + outlier_detection: { + compute_feature_influence: false, + outlier_fraction: 0.05, + standardization_enabled: true, + }, + }, + analyzed_fields: { + includes: [], + excludes: ['id', 'outlier'], + }, + model_memory_limit: '1mb', + create_time: 1583417347446, + version: '8.0.0', + allow_lazy_start: false, + }; + expect(isAdvancedConfig(advancedOutlierDetectionJob)).toBe(true); + }); + + test('should detect advanced regression job', () => { + const advancedRegressionJob = { + id: 'grid_regression_1', + description: "Regression job with 'electrical-grid-stability' dataset", + source: { + index: ['electrical-grid-stability'], + query: { + match: { + custom_field: 'custom_match', + }, + }, + }, + dest: { + index: 'dest_grid_1', + results_field: 'ml', + }, + analysis: { + regression: { + dependent_variable: 'stab', + prediction_field_name: 'stab_prediction', + training_percent: 20, + randomize_seed: -2228827740028660200, + }, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '150mb', + create_time: 1583417178919, + version: '8.0.0', + allow_lazy_start: false, + }; + + expect(isAdvancedConfig(advancedRegressionJob)).toBe(true); + }); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts new file mode 100644 index 00000000000000..9034eea537c055 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts @@ -0,0 +1,249 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEqual } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { DataFrameAnalyticsConfig, isOutlierAnalysis } from '../../../../common'; +import { isClassificationAnalysis, isRegressionAnalysis } from '../../../../common/analytics'; +import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; +import { getCloneFormStateFromJobConfig, State } from '../../hooks/use_create_analytics_form/state'; +import { DataFrameAnalyticsListRow } from './common'; + +interface PropDefinition { + optional: boolean; + formKey?: keyof State['form']; + defaultValue?: any; +} + +function isPropDefinition(a: PropDefinition | object): a is PropDefinition { + return a.hasOwnProperty('optional'); +} + +interface AnalyticsJobMetaData { + [key: string]: PropDefinition | AnalyticsJobMetaData; +} + +/** + * Provides a config definition. + */ +const getAnalyticsJobMeta = (config: DataFrameAnalyticsConfig): AnalyticsJobMetaData => ({ + allow_lazy_start: { + optional: true, + defaultValue: false, + }, + analysis: { + ...(isClassificationAnalysis(config.analysis) + ? { + classification: { + dependent_variable: { + optional: false, + formKey: 'dependentVariable', + }, + training_percent: { + optional: true, + formKey: 'trainingPercent', + }, + eta: { + optional: true, + }, + feature_bag_fraction: { + optional: true, + }, + maximum_number_trees: { + optional: true, + }, + gamma: { + optional: true, + }, + lambda: { + optional: true, + }, + num_top_classes: { + optional: true, + defaultValue: 2, + }, + prediction_field_name: { + optional: true, + defaultValue: `${config.analysis.classification.dependent_variable}_prediction`, + }, + randomize_seed: { + optional: true, + // By default it is randomly generated + }, + num_top_feature_importance_values: { + optional: true, + }, + }, + } + : {}), + ...(isOutlierAnalysis(config.analysis) + ? { + outlier_detection: { + standardization_enabled: { + defaultValue: true, + optional: true, + }, + compute_feature_influence: { + defaultValue: true, + optional: true, + }, + outlier_fraction: { + defaultValue: 0.05, + optional: true, + }, + feature_influence_threshold: { + optional: true, + }, + method: { + optional: true, + }, + n_neighbors: { + optional: true, + }, + }, + } + : {}), + ...(isRegressionAnalysis(config.analysis) + ? { + regression: { + dependent_variable: { + optional: false, + formKey: 'dependentVariable', + }, + training_percent: { + optional: true, + formKey: 'trainingPercent', + }, + eta: { + optional: true, + }, + feature_bag_fraction: { + optional: true, + }, + maximum_number_trees: { + optional: true, + }, + gamma: { + optional: true, + }, + lambda: { + optional: true, + }, + prediction_field_name: { + optional: true, + defaultValue: `${config.analysis!.regression!.dependent_variable}_prediction`, + }, + num_top_feature_importance_values: { + optional: true, + }, + randomize_seed: { + optional: true, + // By default it is randomly generated + }, + }, + } + : {}), + }, + analyzed_fields: { + excludes: { + optional: true, + formKey: 'excludes', + defaultValue: [], + }, + includes: { + optional: true, + defaultValue: [], + }, + }, + source: { + index: { + formKey: 'sourceIndex', + optional: false, + }, + query: { + optional: true, + defaultValue: { + match_all: {}, + }, + }, + _source: { + optional: true, + }, + }, + dest: { + index: { + optional: false, + }, + results_field: { + optional: true, + defaultValue: 'ml', + }, + }, + model_memory_limit: { + optional: true, + formKey: 'modelMemoryLimit', + }, +}); + +/** + * Detects if analytics job configuration were created with + * the advanced editor and not supported by the regular form. + */ +export function isAdvancedConfig(config: any, meta?: AnalyticsJobMetaData): boolean; +export function isAdvancedConfig( + config: DataFrameAnalyticsConfig, + meta: AnalyticsJobMetaData = getAnalyticsJobMeta(config) +): boolean { + for (const configKey in config) { + if (config.hasOwnProperty(configKey)) { + const fieldConfig = config[configKey as keyof typeof config]; + const fieldMeta = meta[configKey as keyof typeof meta]; + + if (fieldMeta) { + if (isPropDefinition(fieldMeta)) { + const isAdvancedSetting = + !fieldMeta.formKey && + fieldMeta.defaultValue !== undefined && + !isEqual(fieldMeta.defaultValue, fieldConfig); + + if (isAdvancedSetting) { + return true; + } + } else if (isAdvancedConfig(fieldConfig, fieldMeta)) { + return true; + } + } + } + } + return false; +} + +export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) { + const buttonText = i18n.translate('xpack.ml.dataframe.analyticsList.cloneJobButtonLabel', { + defaultMessage: 'Clone job', + }); + + const { actions } = createAnalyticsForm; + + const onClick = async (item: DataFrameAnalyticsListRow) => { + await actions.openModal(); + + if (isAdvancedConfig(item.config)) { + actions.setJobConfig(item.config); + actions.switchToAdvancedEditor(); + } else { + actions.setFormState(getCloneFormStateFromJobConfig(item.config)); + } + }; + + return { + name: buttonText, + description: buttonText, + icon: 'copy', + onClick, + 'data-test-subj': 'mlAnalyticsJobCloneButton', + }; +} diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx deleted file mode 100644 index ab1d6d9e46507e..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; -import { getCloneFormStateFromJobConfig } from '../../hooks/use_create_analytics_form/state'; -import { DataFrameAnalyticsListRow } from './common'; - -export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) { - const buttonText = i18n.translate('xpack.ml.dataframe.analyticsList.cloneJobButtonLabel', { - defaultMessage: 'Clone job', - }); - - const { actions } = createAnalyticsForm; - - const onClick = async (item: DataFrameAnalyticsListRow) => { - await actions.openModal(); - actions.setFormState(getCloneFormStateFromJobConfig(item!.config)); - }; - - return { - name: buttonText, - description: buttonText, - icon: 'copy', - onClick, - 'data-test-subj': 'mlAnalyticsJobCloneButton', - }; -} diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index 42c2413607570c..4b1a9c6fe5da10 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { memoize } from 'lodash'; // @ts-ignore import numeral from '@elastic/numeral'; +import { isEmpty } from 'lodash'; import { isValidIndexName } from '../../../../../../../common/util/es_utils'; import { Action, ACTION } from './actions'; @@ -437,7 +438,11 @@ export function reducer(state: State, action: Action): State { } case ACTION.SWITCH_TO_ADVANCED_EDITOR: - const jobConfig = getJobConfigFromFormState(state.form); + let { jobConfig } = state; + const isJobConfigEmpty = isEmpty(state.jobConfig); + if (isJobConfigEmpty) { + jobConfig = getJobConfigFromFormState(state.form); + } return validateAdvancedEditor({ ...state, advancedEditorRawString: JSON.stringify(jobConfig, null, 2), From d1356bc2c24465aae5038656cceda483dfe79874 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Wed, 11 Mar 2020 17:20:29 +0100 Subject: [PATCH 05/26] [ML] extractCloningConfig --- .../components/analytics_list/action_clone.ts | 29 +++++++++++++++++-- .../hooks/use_create_analytics_form/state.ts | 5 ++-- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts index 9034eea537c055..a9f7ac8724f291 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts @@ -221,6 +221,27 @@ export function isAdvancedConfig( return false; } +export type CloneDataFrameAnalyticsConfig = Omit< + DataFrameAnalyticsConfig, + 'id' | 'version' | 'create_time' +>; + +function extractCloningConfig( + originalConfig: DataFrameAnalyticsConfig +): CloneDataFrameAnalyticsConfig { + const { + // Omit non-relevant props from the configuration + id, + version, + create_time, + ...config + } = originalConfig; + + // Reset the destination index + config.dest.index = ''; + return config; +} + export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) { const buttonText = i18n.translate('xpack.ml.dataframe.analyticsList.cloneJobButtonLabel', { defaultMessage: 'Clone job', @@ -231,11 +252,13 @@ export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) { const onClick = async (item: DataFrameAnalyticsListRow) => { await actions.openModal(); - if (isAdvancedConfig(item.config)) { - actions.setJobConfig(item.config); + const config = extractCloningConfig(item.config); + + if (isAdvancedConfig(config)) { + actions.setJobConfig(config); actions.switchToAdvancedEditor(); } else { - actions.setFormState(getCloneFormStateFromJobConfig(item.config)); + actions.setFormState(getCloneFormStateFromJobConfig(config)); } }; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index b59c76d5eef6a4..832588a67466f9 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -7,9 +7,10 @@ import { EuiComboBoxOptionOption } from '@elastic/eui'; import { DeepPartial } from '../../../../../../../common/types/common'; import { checkPermission } from '../../../../../privilege/check_privilege'; -import { mlNodesAvailable } from '../../../../../ml_nodes_check/check_ml_nodes'; +import { mlNodesAvailable } from '../../../../../ml_nodes_check'; import { DataFrameAnalyticsId, DataFrameAnalyticsConfig } from '../../../../common'; +import { CloneDataFrameAnalyticsConfig } from '../../components/analytics_list/action_clone'; export enum DEFAULT_MODEL_MEMORY_LIMIT { regression = '100mb', @@ -194,7 +195,7 @@ export const getJobConfigFromFormState = ( * For cloning we keep job id and destination index empty. */ export function getCloneFormStateFromJobConfig( - analyticsJobConfig: DataFrameAnalyticsConfig + analyticsJobConfig: CloneDataFrameAnalyticsConfig ): Partial { const jobType: string = Object.keys(analyticsJobConfig.analysis)[0]; From ba2cabd1aff904c1b8e033657d7960bfb4abe94d Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Wed, 11 Mar 2020 21:30:17 +0100 Subject: [PATCH 06/26] [ML] fix isAdvancedSetting condition, add test --- .../analytics_list/action_clone.test.ts | 27 ++++++++++++++++++- .../components/analytics_list/action_clone.ts | 12 +++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts index 3eae7bc587e382..a0c52f7cba691f 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts @@ -182,7 +182,7 @@ describe('Analytics job clone action', () => { expect(isAdvancedConfig(advancedOutlierDetectionJob)).toBe(true); }); - test('should detect advanced regression job', () => { + test('should detect a custom query', () => { const advancedRegressionJob = { id: 'grid_regression_1', description: "Regression job with 'electrical-grid-stability' dataset", @@ -218,5 +218,30 @@ describe('Analytics job clone action', () => { expect(isAdvancedConfig(advancedRegressionJob)).toBe(true); }); + + test('should detect custom analysis settings', () => { + const config = { + description: "Classification clone with 'bank-marketing' dataset", + source: { + index: 'bank-marketing', + }, + dest: { + index: 'bank_classification4', + }, + analyzed_fields: { + excludes: [], + }, + analysis: { + classification: { + dependent_variable: 'y', + training_percent: 71, + maximum_number_trees: 1500, + }, + }, + model_memory_limit: '400mb', + }; + + expect(isAdvancedConfig(config)).toBe(true); + }); }); }); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts index a9f7ac8724f291..5f4163f007c978 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts @@ -16,6 +16,7 @@ interface PropDefinition { optional: boolean; formKey?: keyof State['form']; defaultValue?: any; + ignore?: boolean; } function isPropDefinition(a: PropDefinition | object): a is PropDefinition { @@ -72,6 +73,7 @@ const getAnalyticsJobMeta = (config: DataFrameAnalyticsConfig): AnalyticsJobMeta randomize_seed: { optional: true, // By default it is randomly generated + ignore: true, }, num_top_feature_importance_values: { optional: true, @@ -142,6 +144,7 @@ const getAnalyticsJobMeta = (config: DataFrameAnalyticsConfig): AnalyticsJobMeta randomize_seed: { optional: true, // By default it is randomly generated + ignore: true, }, }, } @@ -176,6 +179,7 @@ const getAnalyticsJobMeta = (config: DataFrameAnalyticsConfig): AnalyticsJobMeta dest: { index: { optional: false, + formKey: 'destinationIndex', }, results_field: { optional: true, @@ -205,11 +209,15 @@ export function isAdvancedConfig( if (fieldMeta) { if (isPropDefinition(fieldMeta)) { const isAdvancedSetting = - !fieldMeta.formKey && - fieldMeta.defaultValue !== undefined && + fieldMeta.formKey === undefined && + fieldMeta.ignore !== true && !isEqual(fieldMeta.defaultValue, fieldConfig); if (isAdvancedSetting) { + // eslint-disable-next-line no-console + console.info( + `Property "${configKey}" is not supported by the form or has a value distinguished from the default one.` + ); return true; } } else if (isAdvancedConfig(fieldConfig, fieldMeta)) { From 9fc4fdbff61513bbbf6e3bee63735aeb1db0a041 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 12 Mar 2020 09:33:00 +0100 Subject: [PATCH 07/26] [ML] clone job header --- .../components/analytics_list/action_clone.ts | 23 +++++++++++++++---- .../create_analytics_flyout.tsx | 13 ++++------- .../use_create_analytics_form/actions.ts | 6 ++++- .../use_create_analytics_form/reducer.ts | 6 +++++ .../hooks/use_create_analytics_form/state.ts | 3 +-- .../use_create_analytics_form.ts | 5 ++++ 6 files changed, 40 insertions(+), 16 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts index 5f4163f007c978..e73f71ec439cd6 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts @@ -13,9 +13,22 @@ import { getCloneFormStateFromJobConfig, State } from '../../hooks/use_create_an import { DataFrameAnalyticsListRow } from './common'; interface PropDefinition { + /** + * Indicates if the property is optional + */ optional: boolean; + /** + * Corresponding property from the form + */ formKey?: keyof State['form']; + /** + * Default value of the property + */ defaultValue?: any; + /** + * Indicates if the value has to be ignored + * during detecting advanced configuration + */ ignore?: boolean; } @@ -136,7 +149,7 @@ const getAnalyticsJobMeta = (config: DataFrameAnalyticsConfig): AnalyticsJobMeta }, prediction_field_name: { optional: true, - defaultValue: `${config.analysis!.regression!.dependent_variable}_prediction`, + defaultValue: `${config.analysis.regression.dependent_variable}_prediction`, }, num_top_feature_importance_values: { optional: true, @@ -242,12 +255,12 @@ function extractCloningConfig( id, version, create_time, - ...config + ...cloneConfig } = originalConfig; // Reset the destination index - config.dest.index = ''; - return config; + cloneConfig.dest.index = ''; + return cloneConfig; } export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) { @@ -262,6 +275,8 @@ export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) { const config = extractCloningConfig(item.config); + actions.setJobClone(item.config); + if (isAdvancedConfig(config)) { actions.setJobConfig(config); actions.switchToAdvancedEditor(); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx index ad70f29b19e7dd..32384e1949d0a3 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx @@ -26,17 +26,12 @@ export const CreateAnalyticsFlyout: FC = ({ state, }) => { const { closeModal, createAnalyticsJob, startAnalyticsJob } = actions; - const { - isJobCreated, - isJobStarted, - isModalButtonDisabled, - isValid, - form: { isClone }, - } = state; + const { isJobCreated, isJobStarted, isModalButtonDisabled, isValid, cloneJob } = state; - const headerText = isClone + const headerText = !!cloneJob ? i18n.translate('xpack.ml.dataframe.analytics.clone.flyoutHeaderTitle', { - defaultMessage: 'Clone analytics job', + defaultMessage: 'Clone job from {job_id}', + values: { job_id: cloneJob.id }, }) : i18n.translate('xpack.ml.dataframe.analytics.create.flyoutHeaderTitle', { defaultMessage: 'Create analytics job', diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts index e7487f020ee66d..9a888346dc4919 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { DataFrameAnalyticsConfig } from '../../../../common'; import { FormMessage, State, SourceIndexMap } from './state'; export enum ACTION { @@ -25,6 +26,7 @@ export enum ACTION { SET_JOB_IDS, SWITCH_TO_ADVANCED_EDITOR, SET_ESTIMATED_MODEL_MEMORY_LIMIT, + SET_JOB_CLONE, } export type Action = @@ -61,7 +63,8 @@ export type Action = | { type: ACTION.SET_IS_MODAL_VISIBLE; isModalVisible: State['isModalVisible'] } | { type: ACTION.SET_JOB_CONFIG; payload: State['jobConfig'] } | { type: ACTION.SET_JOB_IDS; jobIds: State['jobIds'] } - | { type: ACTION.SET_ESTIMATED_MODEL_MEMORY_LIMIT; value: State['estimatedModelMemoryLimit'] }; + | { type: ACTION.SET_ESTIMATED_MODEL_MEMORY_LIMIT; value: State['estimatedModelMemoryLimit'] } + | { type: ACTION.SET_JOB_CLONE; cloneJob: DataFrameAnalyticsConfig }; // Actions wrapping the dispatcher exposed by the custom hook export interface ActionDispatchers { @@ -76,4 +79,5 @@ export interface ActionDispatchers { startAnalyticsJob: () => void; switchToAdvancedEditor: () => void; setEstimatedModelMemoryLimit: (value: State['estimatedModelMemoryLimit']) => void; + setJobClone: (cloneJob: DataFrameAnalyticsConfig) => void; } diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index 4b1a9c6fe5da10..81028c7ed38944 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -455,6 +455,12 @@ export function reducer(state: State, action: Action): State { ...state, estimatedModelMemoryLimit: action.value, }; + + case ACTION.SET_JOB_CLONE: + return { + ...state, + cloneJob: action.cloneJob, + }; } return state; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 832588a67466f9..82fec467c57322 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -77,7 +77,6 @@ export interface State { sourceIndexContainsNumericalFields: boolean; sourceIndexFieldsCheckFailed: boolean; trainingPercent: number; - isClone?: boolean; }; disabled: boolean; indexNames: EsIndexName[]; @@ -92,6 +91,7 @@ export interface State { jobIds: DataFrameAnalyticsId[]; requestMessages: FormMessage[]; estimatedModelMemoryLimit: string; + cloneJob?: DataFrameAnalyticsConfig; } export const getInitialState = (): State => ({ @@ -200,7 +200,6 @@ export function getCloneFormStateFromJobConfig( const jobType: string = Object.keys(analyticsJobConfig.analysis)[0]; const resultState: Partial = { - isClone: true, jobType: jobType as AnalyticsJobType, description: analyticsJobConfig.description ?? '', sourceIndex: Array.isArray(analyticsJobConfig.source.index) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 350b3f98d46731..338d2040d767f4 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -301,6 +301,10 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { dispatch({ type: ACTION.SET_ESTIMATED_MODEL_MEMORY_LIMIT, value }); }; + const setJobClone = (cloneJob: DataFrameAnalyticsConfig) => { + dispatch({ type: ACTION.SET_JOB_CLONE, cloneJob }); + }; + const actions: ActionDispatchers = { closeModal, createAnalyticsJob, @@ -313,6 +317,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { startAnalyticsJob, switchToAdvancedEditor, setEstimatedModelMemoryLimit, + setJobClone, }; return { state, actions }; From 84deee6522ec85f48bd6aa2c325252865f2aa562 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 12 Mar 2020 09:50:53 +0100 Subject: [PATCH 08/26] [ML] job description placeholder --- .../components/create_analytics_form/job_description.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_description.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_description.tsx index a94012c172f61d..58f3129280c095 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_description.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_description.tsx @@ -22,10 +22,10 @@ export const JobDescriptionInput: FC = ({ description, setFormState }) => label={i18n.translate('xpack.ml.dataframe.analytics.create.jobDescription.label', { defaultMessage: 'Job description', })} - helpText={helpText} > { const value = e.target.value; From 67221869195f508b156b11d8e758007a4d1f4007 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 12 Mar 2020 10:28:00 +0100 Subject: [PATCH 09/26] [ML] setEstimatedModelMemoryLimit on source index change --- .../create_analytics_form/create_analytics_form.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index 5f905152a415e0..06ac677ba18c2c 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -160,10 +160,10 @@ export const CreateAnalyticsForm: FC = ({ actions, sta ); const expectedMemoryWithoutDisk = resp.memory_estimation?.expected_memory_without_disk; - setEstimatedModelMemoryLimit(expectedMemoryWithoutDisk); - // If sourceIndex has changed load analysis field options again if (previousSourceIndex !== sourceIndex || previousJobType !== jobType) { + setEstimatedModelMemoryLimit(expectedMemoryWithoutDisk); + const analyzedFieldsOptions: EuiComboBoxOptionOption[] = []; if (resp.field_selection) { From e476f28d33feb9b2792e28d04e143c116e951941 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 12 Mar 2020 11:40:12 +0100 Subject: [PATCH 10/26] [ML] Fix types. --- .../data_frame_analytics/common/analytics.ts | 41 +++++++++++-------- .../form_options_validation.ts | 7 ++-- .../hooks/use_create_analytics_form/state.ts | 34 +++++++-------- 3 files changed, 45 insertions(+), 37 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index f87578c4bce48d..00c4c773b0941d 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -19,25 +19,37 @@ export type IndexName = string; export type IndexPattern = string; export type DataFrameAnalyticsId = string; +export enum ANALYSIS_CONFIG_TYPE { + OUTLIER_DETECTION = 'outlier_detection', + REGRESSION = 'regression', + CLASSIFICATION = 'classification', + UNKNOWN = 'unknown', +} + interface OutlierAnalysis { + [key: string]: {}; outlier_detection: {}; } +interface Regression { + dependent_variable: string; + training_percent?: number; + prediction_field_name?: string; +} interface RegressionAnalysis { - regression: { - dependent_variable: string; - training_percent?: number; - prediction_field_name?: string; - }; + [key: string]: Regression; + regression: Regression; } +interface Classification { + dependent_variable: string; + training_percent?: number; + num_top_classes?: string; + prediction_field_name?: string; +} interface ClassificationAnalysis { - classification: { - dependent_variable: string; - training_percent?: number; - num_top_classes?: string; - prediction_field_name?: string; - }; + [key: string]: Classification; + classification: Classification; } export interface LoadExploreDataArg { @@ -136,13 +148,6 @@ type AnalysisConfig = | ClassificationAnalysis | GenericAnalysis; -export enum ANALYSIS_CONFIG_TYPE { - OUTLIER_DETECTION = 'outlier_detection', - REGRESSION = 'regression', - CLASSIFICATION = 'classification', - UNKNOWN = 'unknown', -} - export const getAnalysisType = (analysis: AnalysisConfig) => { const keys = Object.keys(analysis); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/form_options_validation.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/form_options_validation.ts index 51982541ccc3b1..c1c4ceccc0d9df 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/form_options_validation.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/form_options_validation.ts @@ -6,7 +6,8 @@ import { ES_FIELD_TYPES } from '../../../../../../../../../../../src/plugins/data/public'; import { Field, EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields'; -import { JOB_TYPES, AnalyticsJobType } from '../../hooks/use_create_analytics_form/state'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; +import { AnalyticsJobType } from '../../hooks/use_create_analytics_form/state'; import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES } from '../../../../common/fields'; const CATEGORICAL_TYPES = new Set(['ip', 'keyword', 'text']); @@ -23,8 +24,8 @@ export const shouldAddAsDepVarOption = (field: Field, jobType: AnalyticsJobType) const isSupportedByClassification = isBasicNumerical || CATEGORICAL_TYPES.has(field.type) || field.type === ES_FIELD_TYPES.BOOLEAN; - if (jobType === JOB_TYPES.REGRESSION) { + if (jobType === ANALYSIS_CONFIG_TYPE.REGRESSION) { return isBasicNumerical || EXTENDED_NUMERICAL_TYPES.has(field.type); } - if (jobType === JOB_TYPES.CLASSIFICATION) return isSupportedByClassification; + if (jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION) return isSupportedByClassification; }; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 82fec467c57322..515e0e42bd873a 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -9,7 +9,13 @@ import { DeepPartial } from '../../../../../../../common/types/common'; import { checkPermission } from '../../../../../privilege/check_privilege'; import { mlNodesAvailable } from '../../../../../ml_nodes_check'; -import { DataFrameAnalyticsId, DataFrameAnalyticsConfig } from '../../../../common'; +import { + isClassificationAnalysis, + isRegressionAnalysis, + DataFrameAnalyticsId, + DataFrameAnalyticsConfig, + ANALYSIS_CONFIG_TYPE, +} from '../../../../common/analytics'; import { CloneDataFrameAnalyticsConfig } from '../../components/analytics_list/action_clone'; export enum DEFAULT_MODEL_MEMORY_LIMIT { @@ -22,7 +28,7 @@ export enum DEFAULT_MODEL_MEMORY_LIMIT { export type EsIndexName = string; export type DependentVariable = string; export type IndexPatternTitle = string; -export type AnalyticsJobType = JOB_TYPES | undefined; +export type AnalyticsJobType = ANALYSIS_CONFIG_TYPE | undefined; type IndexPatternId = string; export type SourceIndexMap = Record< IndexPatternTitle, @@ -34,12 +40,6 @@ export interface FormMessage { message: string; } -export enum JOB_TYPES { - OUTLIER_DETECTION = 'outlier_detection', - REGRESSION = 'regression', - CLASSIFICATION = 'classification', -} - export interface State { advancedEditorMessages: FormMessage[]; advancedEditorRawString: string; @@ -176,8 +176,8 @@ export const getJobConfigFromFormState = ( }; if ( - formState.jobType === JOB_TYPES.REGRESSION || - formState.jobType === JOB_TYPES.CLASSIFICATION + formState.jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || + formState.jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION ) { jobConfig.analysis = { [formState.jobType]: { @@ -197,10 +197,10 @@ export const getJobConfigFromFormState = ( export function getCloneFormStateFromJobConfig( analyticsJobConfig: CloneDataFrameAnalyticsConfig ): Partial { - const jobType: string = Object.keys(analyticsJobConfig.analysis)[0]; + const jobType = Object.keys(analyticsJobConfig.analysis)[0] as ANALYSIS_CONFIG_TYPE; const resultState: Partial = { - jobType: jobType as AnalyticsJobType, + jobType, description: analyticsJobConfig.description ?? '', sourceIndex: Array.isArray(analyticsJobConfig.source.index) ? analyticsJobConfig.source.index.join(',') @@ -209,12 +209,14 @@ export function getCloneFormStateFromJobConfig( excludes: analyticsJobConfig.analyzed_fields.excludes, }; - if (jobType === JOB_TYPES.REGRESSION || jobType === JOB_TYPES.CLASSIFICATION) { - // @ts-ignore + if ( + isRegressionAnalysis(analyticsJobConfig.analysis) || + isClassificationAnalysis(analyticsJobConfig.analysis) + ) { const analysisConfig = analyticsJobConfig.analysis[jobType]; - resultState.dependentVariable = analysisConfig!.dependent_variable; - resultState.trainingPercent = analysisConfig!.training_percent; + resultState.dependentVariable = analysisConfig.dependent_variable; + resultState.trainingPercent = analysisConfig.training_percent; } return resultState; From ff146760e97ad4bc4d9dbc7c79e5fe464e6a1cb5 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 12 Mar 2020 13:04:43 +0100 Subject: [PATCH 11/26] [ML] useUpdateEffect in create_analytics_form.tsx --- .../components/create_analytics_form/create_analytics_form.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index 06ac677ba18c2c..33460c841c2b46 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -20,6 +20,7 @@ import { debounce } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import useUpdateEffect from 'react-use/lib/useUpdateEffect'; import { useMlKibana } from '../../../../../contexts/kibana'; import { ml } from '../../../../../services/ml_api_service'; @@ -310,7 +311,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta } }, [sourceIndex, jobType, sourceIndexNameEmpty]); - useEffect(() => { + useUpdateEffect(() => { const hasBasicRequiredFields = jobType !== undefined && sourceIndex !== '' && sourceIndexNameValid === true; From 254daf4d368d63c2c922aba392df981c3c323892 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 12 Mar 2020 13:46:38 +0100 Subject: [PATCH 12/26] [ML] setJobClone action --- .../components/analytics_list/action_clone.ts | 17 +++--------- .../use_create_analytics_form/actions.ts | 2 +- .../use_create_analytics_form.ts | 27 ++++++++++++++++--- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts index e73f71ec439cd6..51721d9d2658d2 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { DataFrameAnalyticsConfig, isOutlierAnalysis } from '../../../../common'; import { isClassificationAnalysis, isRegressionAnalysis } from '../../../../common/analytics'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; -import { getCloneFormStateFromJobConfig, State } from '../../hooks/use_create_analytics_form/state'; +import { State } from '../../hooks/use_create_analytics_form/state'; import { DataFrameAnalyticsListRow } from './common'; interface PropDefinition { @@ -247,7 +247,7 @@ export type CloneDataFrameAnalyticsConfig = Omit< 'id' | 'version' | 'create_time' >; -function extractCloningConfig( +export function extractCloningConfig( originalConfig: DataFrameAnalyticsConfig ): CloneDataFrameAnalyticsConfig { const { @@ -271,18 +271,7 @@ export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) { const { actions } = createAnalyticsForm; const onClick = async (item: DataFrameAnalyticsListRow) => { - await actions.openModal(); - - const config = extractCloningConfig(item.config); - - actions.setJobClone(item.config); - - if (isAdvancedConfig(config)) { - actions.setJobConfig(config); - actions.switchToAdvancedEditor(); - } else { - actions.setFormState(getCloneFormStateFromJobConfig(config)); - } + await actions.setJobClone(item.config); }; return { diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts index 9a888346dc4919..8cedc38b1b59b2 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts @@ -79,5 +79,5 @@ export interface ActionDispatchers { startAnalyticsJob: () => void; switchToAdvancedEditor: () => void; setEstimatedModelMemoryLimit: (value: State['estimatedModelMemoryLimit']) => void; - setJobClone: (cloneJob: DataFrameAnalyticsConfig) => void; + setJobClone: (cloneJob: DataFrameAnalyticsConfig) => Promise; } diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 338d2040d767f4..bb01cfaf7b042a 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -17,6 +17,10 @@ import { DataFrameAnalyticsId, DataFrameAnalyticsConfig, } from '../../../../common'; +import { + extractCloningConfig, + isAdvancedConfig, +} from '../../components/analytics_list/action_clone'; import { ActionDispatchers, ACTION } from './actions'; import { reducer } from './reducer'; @@ -27,6 +31,7 @@ import { FormMessage, State, SourceIndexMap, + getCloneFormStateFromJobConfig, } from './state'; export interface CreateAnalyticsFormProps { @@ -187,9 +192,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { } }; - const openModal = async () => { - resetForm(); - + const prepareFormValidation = async () => { // re-fetch existing analytics job IDs and indices for form validation try { setJobIds( @@ -248,7 +251,11 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { ), }); } + }; + const openModal = async () => { + resetForm(); + await prepareFormValidation(); dispatch({ type: ACTION.OPEN_MODAL }); }; @@ -301,8 +308,20 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { dispatch({ type: ACTION.SET_ESTIMATED_MODEL_MEMORY_LIMIT, value }); }; - const setJobClone = (cloneJob: DataFrameAnalyticsConfig) => { + const setJobClone = async (cloneJob: DataFrameAnalyticsConfig) => { + resetForm(); + await prepareFormValidation(); + + const config = extractCloningConfig(cloneJob); + if (isAdvancedConfig(config)) { + setJobConfig(config); + switchToAdvancedEditor(); + } else { + setFormState(getCloneFormStateFromJobConfig(config)); + } + dispatch({ type: ACTION.SET_JOB_CLONE, cloneJob }); + dispatch({ type: ACTION.OPEN_MODAL }); }; const actions: ActionDispatchers = { From 2c180c17cb84dded9743d02c3cf1f8e0b04a88a8 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 12 Mar 2020 14:42:09 +0100 Subject: [PATCH 13/26] [ML] remove CreateAnalyticsFlyoutWrapper instance from the create_analytics_button.tsx --- .../create_analytics_button.tsx | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx index 0958dff7a3f513..e5054e8a6ad2c0 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx @@ -4,18 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC } from 'react'; - +import React, { FC } from 'react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; - import { i18n } from '@kbn/i18n'; - import { createPermissionFailureMessage } from '../../../../../privilege/check_privilege'; - import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; -import { CreateAnalyticsFlyoutWrapper } from '../create_analytics_flyout_wrapper'; - export const CreateAnalyticsButton: FC = props => { const { disabled } = props.state; const { openModal } = props.actions; @@ -46,10 +40,5 @@ export const CreateAnalyticsButton: FC = props => { ); } - return ( - - {button} - - - ); + return button; }; From 9f05bb8ec510cabaf1c3dfdbec68bc7a15d68080 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 12 Mar 2020 15:30:15 +0100 Subject: [PATCH 14/26] [ML] fix types --- .../data_frame_analytics/common/analytics.ts | 3 +-- .../components/analytics_list/action_clone.ts | 2 +- .../analytics_list/action_delete.tsx | 1 + .../create_analytics_form.tsx | 19 ++++++++++++------- .../create_analytics_form/job_type.tsx | 11 ++++++----- .../use_create_analytics_form/reducer.test.ts | 6 +++--- .../use_create_analytics_form/reducer.ts | 8 +++++--- 7 files changed, 29 insertions(+), 21 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 00c4c773b0941d..ede2fa813a1d0c 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -23,7 +23,6 @@ export enum ANALYSIS_CONFIG_TYPE { OUTLIER_DETECTION = 'outlier_detection', REGRESSION = 'regression', CLASSIFICATION = 'classification', - UNKNOWN = 'unknown', } interface OutlierAnalysis { @@ -155,7 +154,7 @@ export const getAnalysisType = (analysis: AnalysisConfig) => { return keys[0]; } - return ANALYSIS_CONFIG_TYPE.UNKNOWN; + return 'unknown'; }; export const getDependentVar = (analysis: AnalysisConfig) => { diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts index 51721d9d2658d2..aeb503843c8b2d 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts @@ -229,7 +229,7 @@ export function isAdvancedConfig( if (isAdvancedSetting) { // eslint-disable-next-line no-console console.info( - `Property "${configKey}" is not supported by the form or has a value distinguished from the default one.` + `Property "${configKey}" is not supported by the form or has a different value to the default.` ); return true; } diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx index 75841b52521bd5..47fc84cf450c04 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx @@ -54,6 +54,7 @@ export const DeleteAction: FC = ({ item }) => { iconType="trash" onClick={openModal} aria-label={buttonDeleteText} + style={{ padding: 0 }} > {buttonDeleteText} diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index 33460c841c2b46..9ba67ac4b9f958 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -28,7 +28,6 @@ import { newJobCapsService } from '../../../../../services/new_job_capabilities_ import { useMlContext } from '../../../../../contexts/ml'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; import { - JOB_TYPES, DEFAULT_MODEL_MEMORY_LIMIT, getJobConfigFromFormState, State, @@ -42,7 +41,11 @@ import { IndexPattern, indexPatterns, } from '../../../../../../../../../../../src/plugins/data/public'; -import { DfAnalyticsExplainResponse, FieldSelectionItem } from '../../../../common/analytics'; +import { + ANALYSIS_CONFIG_TYPE, + DfAnalyticsExplainResponse, + FieldSelectionItem, +} from '../../../../common/analytics'; import { shouldAddAsDepVarOption, OMIT_FIELDS } from './form_options_validation'; export const CreateAnalyticsForm: FC = ({ actions, state }) => { @@ -95,7 +98,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta ]); const isJobTypeWithDepVar = - jobType === JOB_TYPES.REGRESSION || jobType === JOB_TYPES.CLASSIFICATION; + jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; // Find out if index pattern contain numeric fields. Provides a hint in the form // that an analytics jobs is not able to identify outliers if there are no numeric fields present. @@ -190,7 +193,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta } catch (e) { let errorMessage; if ( - jobType === JOB_TYPES.CLASSIFICATION && + jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && e.message !== undefined && e.message.includes('status_exception') && e.message.includes('must have at most') @@ -306,7 +309,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta loadDepVarOptions(form); } - if (jobType === JOB_TYPES.OUTLIER_DETECTION && sourceIndexNameEmpty === false) { + if (jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION && sourceIndexNameEmpty === false) { validateSourceIndexFields(); } }, [sourceIndex, jobType, sourceIndexNameEmpty]); @@ -316,7 +319,8 @@ export const CreateAnalyticsForm: FC = ({ actions, sta jobType !== undefined && sourceIndex !== '' && sourceIndexNameValid === true; const hasRequiredAnalysisFields = - (isJobTypeWithDepVar && dependentVariable !== '') || jobType === JOB_TYPES.OUTLIER_DETECTION; + (isJobTypeWithDepVar && dependentVariable !== '') || + jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION; if (hasBasicRequiredFields && hasRequiredAnalysisFields) { debouncedGetExplainData(); @@ -514,7 +518,8 @@ export const CreateAnalyticsForm: FC = ({ actions, sta data-test-subj="mlAnalyticsCreateJobFlyoutDestinationIndexInput" /> - {(jobType === JOB_TYPES.REGRESSION || jobType === JOB_TYPES.CLASSIFICATION) && ( + {(jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || + jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION) && ( = ({ type, setFormState }) => { ); const helpText = { - outlier_detection: outlierHelpText, - regression: regressionHelpText, - classification: classificationHelpText, + [ANALYSIS_CONFIG_TYPE.REGRESSION]: regressionHelpText, + [ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION]: outlierHelpText, + [ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: classificationHelpText, }; return ( @@ -56,7 +57,7 @@ export const JobType: FC = ({ type, setFormState }) => { helpText={type !== undefined ? helpText[type] : ''} > ({ + options={Object.values(ANALYSIS_CONFIG_TYPE).map(jobType => ({ value: jobType, text: jobType.replace(/_/g, ' '), }))} diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts index 5c989f7248a9eb..8112a0fdb9e29c 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts @@ -6,11 +6,11 @@ import { merge } from 'lodash'; -import { DataFrameAnalyticsConfig } from '../../../../common'; +import { ANALYSIS_CONFIG_TYPE, DataFrameAnalyticsConfig } from '../../../../common'; import { ACTION } from './actions'; import { reducer, validateAdvancedEditor, validateMinMML } from './reducer'; -import { getInitialState, JOB_TYPES } from './state'; +import { getInitialState } from './state'; type SourceIndex = DataFrameAnalyticsConfig['source']['index']; @@ -52,7 +52,7 @@ describe('useCreateAnalyticsForm', () => { destinationIndex: 'the-destination-index', jobId: 'the-analytics-job-id', sourceIndex: 'the-source-index', - jobType: JOB_TYPES.OUTLIER_DETECTION, + jobType: ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION, modelMemoryLimit: '200mb', }, }); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index 81028c7ed38944..9a5fcd0ab08c01 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -12,7 +12,7 @@ import { isEmpty } from 'lodash'; import { isValidIndexName } from '../../../../../../../common/util/es_utils'; import { Action, ACTION } from './actions'; -import { getInitialState, getJobConfigFromFormState, State, JOB_TYPES } from './state'; +import { getInitialState, getJobConfigFromFormState, State } from './state'; import { isJobIdValid, validateModelMemoryLimitUnits, @@ -31,6 +31,7 @@ import { getDependentVar, isRegressionAnalysis, isClassificationAnalysis, + ANALYSIS_CONFIG_TYPE, } from '../../../../common/analytics'; import { indexPatterns } from '../../../../../../../../../../../src/plugins/data/public'; @@ -143,7 +144,7 @@ export const validateAdvancedEditor = (state: State): State => { if ( jobConfig.analysis === undefined && - (jobType === JOB_TYPES.CLASSIFICATION || jobType === JOB_TYPES.REGRESSION) + (jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION || jobType === ANALYSIS_CONFIG_TYPE.REGRESSION) ) { dependentVariableEmpty = true; } @@ -316,7 +317,8 @@ const validateForm = (state: State): State => { const jobTypeEmpty = jobType === undefined; const dependentVariableEmpty = - (jobType === JOB_TYPES.REGRESSION || jobType === JOB_TYPES.CLASSIFICATION) && + (jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || + jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION) && dependentVariable === ''; const mmlValidationResult = validateMml(estimatedModelMemoryLimit, modelMemoryLimit); From 458ec21ba614373a77867a6b942a52b9e8efb2ea Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 12 Mar 2020 17:09:04 +0100 Subject: [PATCH 15/26] [ML] hack to align Clone button with the other actions --- .../{action_clone.ts => action_clone.tsx} | 35 +++++++++++++++++++ .../components/analytics_list/actions.tsx | 8 +++-- .../create_analytics_advanced_editor.tsx | 16 ++++++++- .../create_analytics_form.tsx | 16 ++++++++- 4 files changed, 71 insertions(+), 4 deletions(-) rename x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/{action_clone.ts => action_clone.tsx} (87%) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx similarity index 87% rename from x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx index aeb503843c8b2d..0d3e753c7ed267 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiButtonEmpty } from '@elastic/eui'; +import React, { FC } from 'react'; import { isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; import { DataFrameAnalyticsConfig, isOutlierAnalysis } from '../../../../common'; @@ -282,3 +284,36 @@ export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) { 'data-test-subj': 'mlAnalyticsJobCloneButton', }; } + +interface CloneActionProps { + item: DataFrameAnalyticsListRow; + createAnalyticsForm: CreateAnalyticsFormProps; +} + +/** + * Temp component to have Clone job button with the same look as the other actions. + * Replace with {@link getCloneAction} as soon as all the actions are refactored + * to support EuiContext with a valid DOM structure without nested buttons. + */ +export const CloneAction: FC = ({ createAnalyticsForm, item }) => { + const buttonText = i18n.translate('xpack.ml.dataframe.analyticsList.cloneJobButtonLabel', { + defaultMessage: 'Clone job', + }); + const { actions } = createAnalyticsForm; + const onClick = async () => { + await actions.setJobClone(item.config); + }; + + return ( + + {buttonText} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx index 4d26704e2423d4..0436bcfc368470 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx @@ -20,7 +20,7 @@ import { isClassificationAnalysis, } from '../../../../common/analytics'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; -import { getCloneAction } from './action_clone'; +import { CloneAction } from './action_clone'; import { getResultsUrl, isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; import { stopAnalytics } from '../../services/analytics_service'; @@ -106,6 +106,10 @@ export const getActions = (createAnalyticsForm: CreateAnalyticsFormProps) => { return ; }, }, - getCloneAction(createAnalyticsForm), + { + render: (item: DataFrameAnalyticsListRow) => { + return ; + }, + }, ]; }; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx index 05715f7b9c42e9..f3c808e43f210c 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment } from 'react'; +import React, { FC, Fragment, useEffect, useRef } from 'react'; import { EuiCallOut, @@ -41,6 +41,8 @@ export const CreateAnalyticsAdvancedEditor: FC = ({ ac jobIdValid, } = state.form; + let myInput: any = useRef(); + const onChange = (str: string) => { setAdvancedEditorRawString(str); try { @@ -51,6 +53,13 @@ export const CreateAnalyticsAdvancedEditor: FC = ({ ac } }; + // Temp effect to close the context menu popover on Clone button click + useEffect(() => { + const evt = document.createEvent('MouseEvents'); + evt.initEvent('mouseup', true, true); + myInput.dispatchEvent(evt); + }, []); + return ( {requestMessages.map((requestMessage, i) => ( @@ -98,6 +107,11 @@ export const CreateAnalyticsAdvancedEditor: FC = ({ ac ]} > { + if (input) { + myInput = input; + } + }} disabled={isJobCreated} placeholder="analytics job ID" value={jobId} diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index 9ba67ac4b9f958..d3daa165f177d6 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useEffect, useMemo } from 'react'; +import React, { Fragment, FC, useEffect, useMemo, useRef } from 'react'; import { EuiComboBox, @@ -57,6 +57,8 @@ export const CreateAnalyticsForm: FC = ({ actions, sta const mlContext = useMlContext(); const { form, indexPatternsMap, isAdvancedEditorEnabled, isJobCreated, requestMessages } = state; + let myInput: any = useRef(null); + const { createIndexPattern, dependentVariable, @@ -331,6 +333,13 @@ export const CreateAnalyticsForm: FC = ({ actions, sta }; }, [jobType, sourceIndex, sourceIndexNameEmpty, dependentVariable, trainingPercent]); + // Temp effect to close the context menu popover on Clone button click + useEffect(() => { + const evt = document.createEvent('MouseEvents'); + evt.initEvent('mouseup', true, true); + myInput.dispatchEvent(evt); + }, []); + return ( @@ -398,6 +407,11 @@ export const CreateAnalyticsForm: FC = ({ actions, sta ]} > { + if (input) { + myInput = input; + } + }} disabled={isJobCreated} placeholder={i18n.translate('xpack.ml.dataframe.analytics.create.jobIdPlaceholder', { defaultMessage: 'Job ID', From d3992a044fdb663c835a43d456127ce24c426302 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 12 Mar 2020 17:22:42 +0100 Subject: [PATCH 16/26] [ML] unknown props lead to advanced editor --- .../analytics_list/action_clone.test.ts | 43 +++++++++++-------- .../analytics_list/action_clone.tsx | 40 ++++++++++------- 2 files changed, 49 insertions(+), 34 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts index a0c52f7cba691f..867de934d6f043 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts @@ -10,7 +10,6 @@ describe('Analytics job clone action', () => { describe('isAdvancedConfig', () => { test('should detect a classification job created with the form', () => { const formCreatedClassificationJob = { - id: 'bank_classification_1', description: "Classification job with 'bank-marketing' dataset", source: { index: ['bank-marketing'], @@ -36,8 +35,6 @@ describe('Analytics job clone action', () => { excludes: [], }, model_memory_limit: '350mb', - create_time: 1583417086689, - version: '8.0.0', allow_lazy_start: false, }; @@ -46,7 +43,6 @@ describe('Analytics job clone action', () => { test('should detect a outlier_detection job created with the form', () => { const formCreatedOutlierDetectionJob = { - id: 'glass_outlier_detection_1', description: "Outlier detection job with 'glass' dataset", source: { index: ['glass_withoutdupl_norm'], @@ -70,8 +66,6 @@ describe('Analytics job clone action', () => { excludes: ['id', 'outlier'], }, model_memory_limit: '1mb', - create_time: 1583417347446, - version: '8.0.0', allow_lazy_start: false, }; expect(isAdvancedConfig(formCreatedOutlierDetectionJob)).toBe(false); @@ -79,7 +73,6 @@ describe('Analytics job clone action', () => { test('should detect a regression job created with the form', () => { const formCreatedRegressionJob = { - id: 'grid_regression_1', description: "Regression job with 'electrical-grid-stability' dataset", source: { index: ['electrical-grid-stability'], @@ -104,8 +97,6 @@ describe('Analytics job clone action', () => { excludes: [], }, model_memory_limit: '150mb', - create_time: 1583417178919, - version: '8.0.0', allow_lazy_start: false, }; @@ -114,7 +105,6 @@ describe('Analytics job clone action', () => { test('should detect advanced classification job', () => { const advancedClassificationJob = { - id: 'bank_classification_1', description: "Classification job with 'bank-marketing' dataset", source: { index: ['bank-marketing'], @@ -140,8 +130,6 @@ describe('Analytics job clone action', () => { excludes: [], }, model_memory_limit: '350mb', - create_time: 1583417086689, - version: '8.0.0', allow_lazy_start: false, }; @@ -150,7 +138,6 @@ describe('Analytics job clone action', () => { test('should detect advanced outlier_detection job', () => { const advancedOutlierDetectionJob = { - id: 'glass_outlier_detection_1', description: "Outlier detection job with 'glass' dataset", source: { index: ['glass_withoutdupl_norm'], @@ -175,8 +162,6 @@ describe('Analytics job clone action', () => { excludes: ['id', 'outlier'], }, model_memory_limit: '1mb', - create_time: 1583417347446, - version: '8.0.0', allow_lazy_start: false, }; expect(isAdvancedConfig(advancedOutlierDetectionJob)).toBe(true); @@ -184,7 +169,6 @@ describe('Analytics job clone action', () => { test('should detect a custom query', () => { const advancedRegressionJob = { - id: 'grid_regression_1', description: "Regression job with 'electrical-grid-stability' dataset", source: { index: ['electrical-grid-stability'], @@ -211,8 +195,6 @@ describe('Analytics job clone action', () => { excludes: [], }, model_memory_limit: '150mb', - create_time: 1583417178919, - version: '8.0.0', allow_lazy_start: false, }; @@ -243,5 +225,30 @@ describe('Analytics job clone action', () => { expect(isAdvancedConfig(config)).toBe(true); }); + + test('should detect as advanced if the prop is unknown', () => { + const config = { + description: "Classification clone with 'bank-marketing' dataset", + source: { + index: 'bank-marketing', + }, + dest: { + index: 'bank_classification4', + }, + analyzed_fields: { + excludes: [], + }, + analysis: { + classification: { + dependent_variable: 'y', + training_percent: 71, + max_trees: 1500, + }, + }, + model_memory_limit: '400mb', + }; + + expect(isAdvancedConfig(config)).toBe(true); + }); }); }); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx index 0d3e753c7ed267..e1763eb2d5a0de 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx @@ -45,11 +45,15 @@ interface AnalyticsJobMetaData { /** * Provides a config definition. */ -const getAnalyticsJobMeta = (config: DataFrameAnalyticsConfig): AnalyticsJobMetaData => ({ +const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJobMetaData => ({ allow_lazy_start: { optional: true, defaultValue: false, }, + description: { + optional: true, + formKey: 'description', + }, analysis: { ...(isClassificationAnalysis(config.analysis) ? { @@ -213,7 +217,7 @@ const getAnalyticsJobMeta = (config: DataFrameAnalyticsConfig): AnalyticsJobMeta */ export function isAdvancedConfig(config: any, meta?: AnalyticsJobMetaData): boolean; export function isAdvancedConfig( - config: DataFrameAnalyticsConfig, + config: CloneDataFrameAnalyticsConfig, meta: AnalyticsJobMetaData = getAnalyticsJobMeta(config) ): boolean { for (const configKey in config) { @@ -221,23 +225,27 @@ export function isAdvancedConfig( const fieldConfig = config[configKey as keyof typeof config]; const fieldMeta = meta[configKey as keyof typeof meta]; - if (fieldMeta) { - if (isPropDefinition(fieldMeta)) { - const isAdvancedSetting = - fieldMeta.formKey === undefined && - fieldMeta.ignore !== true && - !isEqual(fieldMeta.defaultValue, fieldConfig); + if (!fieldMeta) { + // eslint-disable-next-line no-console + console.info(`Property "${configKey}" is unknown.`); + return true; + } + + if (isPropDefinition(fieldMeta)) { + const isAdvancedSetting = + fieldMeta.formKey === undefined && + fieldMeta.ignore !== true && + !isEqual(fieldMeta.defaultValue, fieldConfig); - if (isAdvancedSetting) { - // eslint-disable-next-line no-console - console.info( - `Property "${configKey}" is not supported by the form or has a different value to the default.` - ); - return true; - } - } else if (isAdvancedConfig(fieldConfig, fieldMeta)) { + if (isAdvancedSetting) { + // eslint-disable-next-line no-console + console.info( + `Property "${configKey}" is not supported by the form or has a different value to the default.` + ); return true; } + } else if (isAdvancedConfig(fieldConfig, fieldMeta)) { + return true; } } } From 44cc41ba0909eac4595378a0769474c474b1f188 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 13 Mar 2020 09:42:32 +0100 Subject: [PATCH 17/26] [ML] rename maximum_number_trees ot max_trees --- .../components/analytics_list/action_clone.test.ts | 4 ++-- .../components/analytics_list/action_clone.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts index 867de934d6f043..6225bca592be39 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts @@ -217,7 +217,7 @@ describe('Analytics job clone action', () => { classification: { dependent_variable: 'y', training_percent: 71, - maximum_number_trees: 1500, + max_trees: 1500, }, }, model_memory_limit: '400mb', @@ -242,7 +242,7 @@ describe('Analytics job clone action', () => { classification: { dependent_variable: 'y', training_percent: 71, - max_trees: 1500, + maximum_number_trees: 1500, }, }, model_memory_limit: '400mb', diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx index e1763eb2d5a0de..7199453a15d7f9 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx @@ -72,7 +72,7 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo feature_bag_fraction: { optional: true, }, - maximum_number_trees: { + max_trees: { optional: true, }, gamma: { @@ -144,7 +144,7 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo feature_bag_fraction: { optional: true, }, - maximum_number_trees: { + max_trees: { optional: true, }, gamma: { From 6b7d0bf5610870dd722702831333baca42e844c7 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 13 Mar 2020 10:03:38 +0100 Subject: [PATCH 18/26] [ML] fix forceInput --- .../create_analytics_advanced_editor.tsx | 9 ++++++--- .../create_analytics_form/create_analytics_form.tsx | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx index f3c808e43f210c..bce384248b0318 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx @@ -41,7 +41,7 @@ export const CreateAnalyticsAdvancedEditor: FC = ({ ac jobIdValid, } = state.form; - let myInput: any = useRef(); + const forceInput = useRef(null); const onChange = (str: string) => { setAdvancedEditorRawString(str); @@ -55,9 +55,12 @@ export const CreateAnalyticsAdvancedEditor: FC = ({ ac // Temp effect to close the context menu popover on Clone button click useEffect(() => { + if (forceInput.current === null) { + return; + } const evt = document.createEvent('MouseEvents'); evt.initEvent('mouseup', true, true); - myInput.dispatchEvent(evt); + forceInput.current.dispatchEvent(evt); }, []); return ( @@ -109,7 +112,7 @@ export const CreateAnalyticsAdvancedEditor: FC = ({ ac { if (input) { - myInput = input; + forceInput.current = input; } }} disabled={isJobCreated} diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index d3daa165f177d6..e419d8b340e76d 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -57,7 +57,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta const mlContext = useMlContext(); const { form, indexPatternsMap, isAdvancedEditorEnabled, isJobCreated, requestMessages } = state; - let myInput: any = useRef(null); + const forceInput = useRef(null); const { createIndexPattern, @@ -335,9 +335,12 @@ export const CreateAnalyticsForm: FC = ({ actions, sta // Temp effect to close the context menu popover on Clone button click useEffect(() => { + if (forceInput.current === null) { + return; + } const evt = document.createEvent('MouseEvents'); evt.initEvent('mouseup', true, true); - myInput.dispatchEvent(evt); + forceInput.current.dispatchEvent(evt); }, []); return ( @@ -409,7 +412,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta { if (input) { - myInput = input; + forceInput.current = input; } }} disabled={isJobCreated} From 5939ddc167e1f5775f0b16dff43a38436faa3169 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 13 Mar 2020 10:41:43 +0100 Subject: [PATCH 19/26] [ML] populate excludesOptions on the first update, skip setting mml on the fist update --- .../create_analytics_form.tsx | 20 ++++++++++++------- .../use_create_analytics_form.ts | 1 + 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index e419d8b340e76d..c8377360a9d15f 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -20,7 +20,6 @@ import { debounce } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import useUpdateEffect from 'react-use/lib/useUpdateEffect'; import { useMlKibana } from '../../../../../contexts/kibana'; import { ml } from '../../../../../services/ml_api_service'; @@ -58,6 +57,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta const { form, indexPatternsMap, isAdvancedEditorEnabled, isJobCreated, requestMessages } = state; const forceInput = useRef(null); + const firstUpdate = useRef(true); const { createIndexPattern, @@ -148,6 +148,10 @@ export const CreateAnalyticsForm: FC = ({ actions, sta }; const debouncedGetExplainData = debounce(async () => { + const shouldUpdateModelMemoryLimit = !firstUpdate.current || !modelMemoryLimit; + if (firstUpdate.current) { + firstUpdate.current = false; + } // Reset if sourceIndex or jobType changes (jobType requires dependent_variable to be set - // which won't be the case if switching from outlier detection) if (previousSourceIndex !== sourceIndex || previousJobType !== jobType) { @@ -166,10 +170,12 @@ export const CreateAnalyticsForm: FC = ({ actions, sta ); const expectedMemoryWithoutDisk = resp.memory_estimation?.expected_memory_without_disk; - // If sourceIndex has changed load analysis field options again - if (previousSourceIndex !== sourceIndex || previousJobType !== jobType) { + if (shouldUpdateModelMemoryLimit) { setEstimatedModelMemoryLimit(expectedMemoryWithoutDisk); + } + // If sourceIndex has changed load analysis field options again + if (previousSourceIndex !== sourceIndex || previousJobType !== jobType) { const analyzedFieldsOptions: EuiComboBoxOptionOption[] = []; if (resp.field_selection) { @@ -181,7 +187,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta } setFormState({ - ...(!modelMemoryLimit ? { modelMemoryLimit: expectedMemoryWithoutDisk } : {}), + ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemoryWithoutDisk } : {}), excludesOptions: analyzedFieldsOptions, loadingFieldOptions: false, fieldOptionsFetchFail: false, @@ -189,7 +195,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta }); } else { setFormState({ - ...(!modelMemoryLimit ? { modelMemoryLimit: expectedMemoryWithoutDisk } : {}), + ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemoryWithoutDisk } : {}), }); } } catch (e) { @@ -211,7 +217,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta fieldOptionsFetchFail: true, maxDistinctValuesError: errorMessage, loadingFieldOptions: false, - modelMemoryLimit: fallbackModelMemoryLimit, + ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: fallbackModelMemoryLimit } : {}), }); } }, 400); @@ -316,7 +322,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta } }, [sourceIndex, jobType, sourceIndexNameEmpty]); - useUpdateEffect(() => { + useEffect(() => { const hasBasicRequiredFields = jobType !== undefined && sourceIndex !== '' && sourceIndexNameValid === true; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index bb01cfaf7b042a..39a71c43bb3f99 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -318,6 +318,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { switchToAdvancedEditor(); } else { setFormState(getCloneFormStateFromJobConfig(config)); + setEstimatedModelMemoryLimit(config.model_memory_limit); } dispatch({ type: ACTION.SET_JOB_CLONE, cloneJob }); From cac0b3685b28fb777b06f1ddd3fb7a0232df34b1 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 13 Mar 2020 18:51:20 +0100 Subject: [PATCH 20/26] [ML] init functional test for cloning analytics jobs --- .../data_frame_analytics/common/analytics.ts | 2 + .../data_frame_analytics/cloning.ts | 88 +++++++++++++++++++ .../data_frame_analytics/index.ts | 1 + .../services/machine_learning/api.ts | 24 ++++- .../data_frame_analytics_creation.ts | 4 + .../data_frame_analytics_table.ts | 14 +++ 6 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index ede2fa813a1d0c..42ab04b58fb27d 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -249,6 +249,7 @@ export interface DataFrameAnalyticsConfig { }; source: { index: IndexName | IndexName[]; + query?: any; }; analysis: AnalysisConfig; analyzed_fields: { @@ -258,6 +259,7 @@ export interface DataFrameAnalyticsConfig { model_memory_limit: string; create_time: number; version: string; + allow_lazy_start: boolean; } export enum REFRESH_ANALYTICS_LIST_STATE { diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts new file mode 100644 index 00000000000000..d99416948521aa --- /dev/null +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { DataFrameAnalyticsConfig } from '../../../../../legacy/plugins/ml/public/application/data_frame_analytics/common'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + describe('classification cloning', function() { + this.tags(['smoke', 'dima']); + + const testDataList: Array<{ + suiteTitle: string; + job: Partial; + }> = [ + { + suiteTitle: 'classification job supported by the form', + job: { + id: 'bm_classification_job', + source: { + index: ['bank-marketing'], + query: { + match_all: {}, + }, + }, + dest: { + index: 'dest_bank_1', + results_field: 'ml', + }, + analysis: { + classification: { + dependent_variable: 'y', + training_percent: 2, + }, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '350mb', + allow_lazy_start: false, + }, + }, + ]; + + before(async () => { + await esArchiver.load('ml/bm_classification'); + // Create jobs for cloning + for (const testData of testDataList) { + await ml.api.createDataFrameAnalyticsJob(testData.job as DataFrameAnalyticsConfig); + } + + await ml.securityUI.loginAsMlPowerUser(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + await esArchiver.unload('ml/bm_classification'); + }); + + for (const testData of testDataList) { + describe(`${testData.suiteTitle}`, function() { + before(async () => { + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToDataFrameAnalytics(); + await ml.dataFrameAnalyticsTable.waitForAnalyticsToLoad(); + await ml.dataFrameAnalyticsTable.filterWithSearchString(testData.job.id as string); + }); + + after(async () => { + await ml.api.deleteIndices(testData.job.dest!.index); + }); + + it('should opens the flyout with a proper header', async () => { + await ml.dataFrameAnalyticsTable.cloneJob(testData.job.id as string); + expect(await ml.dataFrameAnalyticsCreation.getHeaderText()).to.be( + 'Clone job from bm_classification_job' + ); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/index.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/index.ts index fda0c5d203f2e0..fe94f4aea9220d 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/index.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/index.ts @@ -12,5 +12,6 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./outlier_detection_creation')); loadTestFile(require.resolve('./regression_creation')); loadTestFile(require.resolve('./classification_creation')); + loadTestFile(require.resolve('./cloning')); }); } diff --git a/x-pack/test/functional/services/machine_learning/api.ts b/x-pack/test/functional/services/machine_learning/api.ts index 976eb51318915b..d70ba24a5c52ca 100644 --- a/x-pack/test/functional/services/machine_learning/api.ts +++ b/x-pack/test/functional/services/machine_learning/api.ts @@ -5,12 +5,13 @@ */ import expect from '@kbn/expect'; import { ProvidedType } from '@kbn/test/types/ftr'; +import { DataFrameAnalyticsConfig } from '../../../../legacy/plugins/ml/public/application/data_frame_analytics/common'; import { FtrProviderContext } from '../../ftr_provider_context'; import { JOB_STATE, DATAFEED_STATE } from '../../../../legacy/plugins/ml/common/constants/states'; import { DATA_FRAME_TASK_STATE } from '../../../../legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common'; -import { Job, Datafeed } from '../../../..//legacy/plugins/ml/common/types/anomaly_detection_jobs'; +import { Job, Datafeed } from '../../../../legacy/plugins/ml/common/types/anomaly_detection_jobs'; export type MlApi = ProvidedType; @@ -355,5 +356,26 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { await this.waitForDatafeedState(datafeedConfig.datafeed_id, DATAFEED_STATE.STOPPED); await this.waitForJobState(jobConfig.job_id, JOB_STATE.CLOSED); }, + + async getDataFrameAnalyticsJob(analyticsId: string) { + return await esSupertest.get(`/_ml/data_frame/analytics/${analyticsId}`).expect(200); + }, + + async createDataFrameAnalyticsJob(jobConfig: DataFrameAnalyticsConfig) { + const { id: analyticsId, ...analyticsConfig } = jobConfig; + log.debug(`Creating data frame analytic job with id '${analyticsId}'...`); + await esSupertest + .put(`/_ml/data_frame/analytics/${analyticsId}`) + .send(analyticsConfig) + .expect(200); + + await retry.waitForWithTimeout(`'${analyticsId}' to be created`, 5 * 1000, async () => { + if (await this.getDataFrameAnalyticsJob(analyticsId)) { + return true; + } else { + throw new Error(`expected data frame analytics job '${analyticsId}' to be created`); + } + }); + }, }; } diff --git a/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts b/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts index 96dc8993c3d35e..0e281ff145a1b5 100644 --- a/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts @@ -331,5 +331,9 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( await testSubjects.missingOrFail('mlAnalyticsCreateJobFlyout'); }); }, + + async getHeaderText() { + return await testSubjects.getVisibleText('mlDataFrameAnalyticsFlyoutHeaderTitle'); + }, }; } diff --git a/x-pack/test/functional/services/machine_learning/data_frame_analytics_table.ts b/x-pack/test/functional/services/machine_learning/data_frame_analytics_table.ts index 1d710a1c4cec72..921981768dabaf 100644 --- a/x-pack/test/functional/services/machine_learning/data_frame_analytics_table.ts +++ b/x-pack/test/functional/services/machine_learning/data_frame_analytics_table.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); + const find = getService('find'); return new (class AnalyticsTable { public async parseAnalyticsTable() { @@ -108,5 +109,18 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F const analyticsRow = rows.filter(row => row.id === analyticsId)[0]; expect(analyticsRow).to.eql(expectedRow); } + + public async openRowActions(analyticsId: string) { + await find.clickByCssSelector( + `[data-test-subj="mlAnalyticsTableRow row-${analyticsId}"] [data-test-subj=euiCollapsedItemActionsButton]` + ); + await find.existsByCssSelector('.euiPanel', 20 * 1000); + } + + public async cloneJob(analyticsId: string) { + await this.openRowActions(analyticsId); + await testSubjects.click(`mlAnalyticsJobCloneButton`); + await testSubjects.existOrFail('mlAnalyticsCreateJobFlyout'); + } })(); } From a9b742a055e91f741a9b0743aa052be086f83e32 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Sat, 14 Mar 2020 14:35:32 +0100 Subject: [PATCH 21/26] [ML] functional tests --- .../data_frame_analytics/cloning.ts | 50 +++++++++++++++++-- .../data_frame_analytics_creation.ts | 41 +++++++++++++++ 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts index 15eb760c50a7d9..d40b1947c87aa2 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts @@ -12,7 +12,7 @@ export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); describe('classification cloning', function() { - this.tags(['smoke', 'dima']); + this.tags(['smoke']); const testDataList: Array<{ suiteTitle: string; @@ -65,11 +65,15 @@ export default function({ getService }: FtrProviderContext) { for (const testData of testDataList) { describe(`${testData.suiteTitle}`, function() { + const cloneJobId = `clone_${testData.job.id}`; + const cloneDestIndex = `clone_${testData.job!.dest!.index}`; + before(async () => { await ml.navigation.navigateToMl(); await ml.navigation.navigateToDataFrameAnalytics(); await ml.dataFrameAnalyticsTable.waitForAnalyticsToLoad(); await ml.dataFrameAnalyticsTable.filterWithSearchString(testData.job.id as string); + await ml.dataFrameAnalyticsTable.cloneJob(testData.job.id as string); }); after(async () => { @@ -77,9 +81,49 @@ export default function({ getService }: FtrProviderContext) { }); it('should open the flyout with a proper header', async () => { - await ml.dataFrameAnalyticsTable.cloneJob(testData.job.id as string); expect(await ml.dataFrameAnalyticsCreation.getHeaderText()).to.be( - 'Clone job from bm_classification_job' + `Clone job from ${testData.job.id}` + ); + }); + + it('should have correct init form values', async () => { + await ml.dataFrameAnalyticsCreation.assertInitialCloneJobForm( + testData.job as DataFrameAnalyticsConfig + ); + }); + + it('should have disabled Create button on open', async () => { + expect(await ml.dataFrameAnalyticsCreation.isCreateButtonDisabled()).to.be(true); + }); + + it('should enable Create button on a valid form input', async () => { + await ml.dataFrameAnalyticsCreation.setJobId(cloneJobId); + await ml.dataFrameAnalyticsCreation.setDestIndex(cloneDestIndex); + expect(await ml.dataFrameAnalyticsCreation.isCreateButtonDisabled()).to.be(false); + }); + + it('should create a clone job', async () => { + await ml.dataFrameAnalyticsCreation.createAnalyticsJob(); + }); + + it('should start the clone analytics job', async () => { + await ml.dataFrameAnalyticsCreation.assertStartButtonExists(); + await ml.dataFrameAnalyticsCreation.startAnalyticsJob(); + }); + + it('should close the create job flyout', async () => { + await ml.dataFrameAnalyticsCreation.assertCloseButtonExists(); + await ml.dataFrameAnalyticsCreation.closeCreateAnalyticsJobFlyout(); + }); + + it('displays the created job in the analytics table', async () => { + await ml.dataFrameAnalyticsTable.refreshAnalyticsTable(); + await ml.dataFrameAnalyticsTable.filterWithSearchString(cloneJobId); + const rows = await ml.dataFrameAnalyticsTable.parseAnalyticsTable(); + const filteredRows = rows.filter(row => row.id === cloneJobId); + expect(filteredRows).to.have.length( + 1, + `Filtered analytics table should have 1 row for job id '${cloneJobId}' (got matching items '${filteredRows}')` ); }); }); diff --git a/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts b/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts index 0e281ff145a1b5..235fe56f22e995 100644 --- a/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts @@ -4,6 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import { + DataFrameAnalyticsConfig, + getAnalysisType, +} from '../../../../plugins/ml/public/application/data_frame_analytics/common'; +import { + isClassificationAnalysis, + isRegressionAnalysis, +} from '../../../../plugins/ml/public/application/data_frame_analytics/common/analytics'; import { FtrProviderContext } from '../../ftr_provider_context'; import { MlCommon } from './common'; @@ -114,6 +122,16 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( ); }, + async assertExcludedFieldsSelection(expectedSelection: string[]) { + const actualSelection = await comboBox.getComboBoxSelectedOptions( + 'mlAnalyticsCreateJobFlyoutExcludesSelect > comboBoxInput' + ); + expect(actualSelection).to.eql( + expectedSelection, + `Excluded fields should be '${expectedSelection}' (got '${actualSelection}')` + ); + }, + async selectSourceIndex(sourceIndex: string) { await comboBox.set( 'mlAnalyticsCreateJobFlyoutSourceIndexSelect > comboBoxInput', @@ -297,6 +315,14 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( await testSubjects.missingOrFail('mlAnalyticsCreateJobFlyoutCreateButton'); }, + async isCreateButtonDisabled() { + const attrValue = await testSubjects.getAttribute( + 'mlAnalyticsCreateJobFlyoutCreateButton', + 'disabled' + ); + return attrValue === ''; + }, + async createAnalyticsJob() { await testSubjects.click('mlAnalyticsCreateJobFlyoutCreateButton'); await retry.tryForTime(5000, async () => { @@ -335,5 +361,20 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( async getHeaderText() { return await testSubjects.getVisibleText('mlDataFrameAnalyticsFlyoutHeaderTitle'); }, + + async assertInitialCloneJobForm(job: DataFrameAnalyticsConfig) { + const jobType = getAnalysisType(job.analysis); + await this.assertJobTypeSelection(jobType); + await this.assertJobIdValue(''); // id should be empty + await this.assertJobDescriptionValue(String(job.description)); + await this.assertSourceIndexSelection(job.source.index as string[]); + await this.assertDestIndexValue(''); // destination index should be empty + if (isClassificationAnalysis(job.analysis) || isRegressionAnalysis(job.analysis)) { + await this.assertDependentVariableSelection([job.analysis[jobType].dependent_variable]); + await this.assertTrainingPercentValue(String(job.analysis[jobType].training_percent)); + } + await this.assertExcludedFieldsSelection(job.analyzed_fields.excludes); + await this.assertModelMemoryValue(job.model_memory_limit); + }, }; } From fee64e25c3703ad4a83d2a02367c278192a804b7 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Sun, 15 Mar 2020 11:46:45 +0100 Subject: [PATCH 22/26] [ML] fix functional tests imports --- .../data_frame_analytics/common/analytics.ts | 4 +-- .../data_frame_analytics_creation.ts | 27 ++++++++++++++----- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 54437da08b3137..9c239df3571635 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -35,7 +35,7 @@ interface Regression { training_percent?: number; prediction_field_name?: string; } -interface RegressionAnalysis { +export interface RegressionAnalysis { [key: string]: Regression; regression: Regression; } @@ -46,7 +46,7 @@ interface Classification { num_top_classes?: string; prediction_field_name?: string; } -interface ClassificationAnalysis { +export interface ClassificationAnalysis { [key: string]: Classification; classification: Classification; } diff --git a/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts b/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts index 235fe56f22e995..629a42bb82812f 100644 --- a/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts @@ -4,18 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; import { - DataFrameAnalyticsConfig, - getAnalysisType, -} from '../../../../plugins/ml/public/application/data_frame_analytics/common'; -import { - isClassificationAnalysis, - isRegressionAnalysis, + ClassificationAnalysis, + RegressionAnalysis, } from '../../../../plugins/ml/public/application/data_frame_analytics/common/analytics'; import { FtrProviderContext } from '../../ftr_provider_context'; import { MlCommon } from './common'; +enum ANALYSIS_CONFIG_TYPE { + OUTLIER_DETECTION = 'outlier_detection', + REGRESSION = 'regression', + CLASSIFICATION = 'classification', +} + +const isRegressionAnalysis = (arg: any): arg is RegressionAnalysis => { + const keys = Object.keys(arg); + return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION; +}; + +const isClassificationAnalysis = (arg: any): arg is ClassificationAnalysis => { + const keys = Object.keys(arg); + return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; +}; + export function MachineLearningDataFrameAnalyticsCreationProvider( { getService }: FtrProviderContext, mlCommon: MlCommon @@ -363,7 +376,7 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( }, async assertInitialCloneJobForm(job: DataFrameAnalyticsConfig) { - const jobType = getAnalysisType(job.analysis); + const jobType = Object.keys(job.analysis)[0]; await this.assertJobTypeSelection(jobType); await this.assertJobIdValue(''); // id should be empty await this.assertJobDescriptionValue(String(job.description)); From 8c2968ed76e43a79c415cee6cf1d4198985de840 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Sun, 15 Mar 2020 16:25:38 +0100 Subject: [PATCH 23/26] [ML] fix indices names for functional tests --- .../data_frame_analytics/cloning.ts | 70 +++++++++++-------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts index d40b1947c87aa2..039e515c80634e 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts @@ -17,36 +17,44 @@ export default function({ getService }: FtrProviderContext) { const testDataList: Array<{ suiteTitle: string; job: Partial; - }> = [ - { - suiteTitle: 'classification job supported by the form', - job: { - id: 'bm_classification_job', - source: { - index: ['bank-marketing'], - query: { - match_all: {}, + }> = (() => { + const timestamp = Date.now(); + + return [ + { + suiteTitle: 'classification job supported by the form', + job: { + id: `bm_1_${timestamp}`, + description: + "Classification job based on 'bank-marketing' dataset with dependentVariable 'y' and trainingPercent '20'", + source: { + index: ['bank-marketing*'], + query: { + match_all: {}, + }, }, - }, - dest: { - index: 'dest_bank_1', - results_field: 'ml', - }, - analysis: { - classification: { - dependent_variable: 'y', - training_percent: 2, + dest: { + get index(): string { + return `user-bm_1_${timestamp}`; + }, + results_field: 'ml', }, + analysis: { + classification: { + dependent_variable: 'y', + training_percent: 2, + }, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '350mb', + allow_lazy_start: false, }, - analyzed_fields: { - includes: [], - excludes: [], - }, - model_memory_limit: '350mb', - allow_lazy_start: false, }, - }, - ]; + ]; + })(); before(async () => { await esArchiver.load('ml/bm_classification'); @@ -60,13 +68,17 @@ export default function({ getService }: FtrProviderContext) { after(async () => { await ml.api.cleanMlIndices(); + // Clean destination indices of the original jobs + for (const testData of testDataList) { + await ml.api.deleteIndices(testData.job.dest!.index); + } await esArchiver.unload('ml/bm_classification'); }); for (const testData of testDataList) { describe(`${testData.suiteTitle}`, function() { - const cloneJobId = `clone_${testData.job.id}`; - const cloneDestIndex = `clone_${testData.job!.dest!.index}`; + const cloneJobId = `${testData.job.id}_clone`; + const cloneDestIndex = `${testData.job!.dest!.index}_clone`; before(async () => { await ml.navigation.navigateToMl(); @@ -77,7 +89,7 @@ export default function({ getService }: FtrProviderContext) { }); after(async () => { - await ml.api.deleteIndices(testData.job.dest!.index); + await ml.api.deleteIndices(cloneDestIndex); }); it('should open the flyout with a proper header', async () => { From bfd985b115e041da88039ef5689385033e40f4c3 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Sun, 15 Mar 2020 17:03:07 +0100 Subject: [PATCH 24/26] [ML] functional tests for outlier detection and regression jobs cloning --- .../data_frame_analytics/cloning.ts | 75 +++++++++++++++++-- 1 file changed, 68 insertions(+), 7 deletions(-) diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts index 039e515c80634e..bc0a3e63fbb3c0 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import { DeepPartial } from '../../../../../plugins/ml/common/types/common'; import { DataFrameAnalyticsConfig } from '../../../../../plugins/ml/public/application/data_frame_analytics/common'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -11,18 +12,20 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('classification cloning', function() { + describe('data frame analytics jobs cloning supported by UI form', function() { this.tags(['smoke']); const testDataList: Array<{ suiteTitle: string; - job: Partial; + archive: string; + job: DeepPartial; }> = (() => { const timestamp = Date.now(); return [ { suiteTitle: 'classification job supported by the form', + archive: 'ml/bm_classification', job: { id: `bm_1_${timestamp}`, description: @@ -42,7 +45,7 @@ export default function({ getService }: FtrProviderContext) { analysis: { classification: { dependent_variable: 'y', - training_percent: 2, + training_percent: 20, }, }, analyzed_fields: { @@ -53,16 +56,74 @@ export default function({ getService }: FtrProviderContext) { allow_lazy_start: false, }, }, + { + suiteTitle: 'outlier detection job supported by the form', + archive: 'ml/ihp_outlier', + job: { + id: `ihp_1_${timestamp}`, + description: 'This is the job description', + source: { + index: ['ihp_outlier'], + query: { + match_all: {}, + }, + }, + dest: { + get index(): string { + return `user-ihp_1_${timestamp}`; + }, + results_field: 'ml', + }, + analysis: { + outlier_detection: {}, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '55mb', + }, + }, + { + suiteTitle: 'regression job supported by the form', + archive: 'ml/egs_regression', + job: { + id: `egs_1_${timestamp}`, + description: 'This is the job description', + source: { + index: ['egs_regression'], + query: { + match_all: {}, + }, + }, + dest: { + get index(): string { + return `user-egs_1_${timestamp}`; + }, + results_field: 'ml', + }, + analysis: { + regression: { + dependent_variable: 'stab', + training_percent: 20, + }, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '105mb', + }, + }, ]; })(); before(async () => { - await esArchiver.load('ml/bm_classification'); // Create jobs for cloning for (const testData of testDataList) { + await esArchiver.load(testData.archive); await ml.api.createDataFrameAnalyticsJob(testData.job as DataFrameAnalyticsConfig); } - await ml.securityUI.loginAsMlPowerUser(); }); @@ -70,9 +131,9 @@ export default function({ getService }: FtrProviderContext) { await ml.api.cleanMlIndices(); // Clean destination indices of the original jobs for (const testData of testDataList) { - await ml.api.deleteIndices(testData.job.dest!.index); + await ml.api.deleteIndices(testData.job.dest!.index as string); + await esArchiver.unload(testData.archive); } - await esArchiver.unload('ml/bm_classification'); }); for (const testData of testDataList) { From 24d4b57546181ca00f44b04022f75256e0243db3 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Sun, 15 Mar 2020 17:26:40 +0100 Subject: [PATCH 25/26] [ML] delete james tag --- x-pack/plugins/ml/__mocks__/shared_imports.ts | 2 +- .../apps/machine_learning/feature_controls/ml_security.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/__mocks__/shared_imports.ts b/x-pack/plugins/ml/__mocks__/shared_imports.ts index d044ab409eb7a5..f5fbbf32d30d7a 100644 --- a/x-pack/plugins/ml/__mocks__/shared_imports.ts +++ b/x-pack/plugins/ml/__mocks__/shared_imports.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export function XJsonMode() {} +export const XJsonMode = jest.fn(); diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts index 405e9575f4222c..f3731f46a5bcee 100644 --- a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts @@ -13,7 +13,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'security']); describe('security', function() { - this.tags(['james']); before(async () => { await esArchiver.load('empty_kibana'); From 526f88c4d34888d66275bee2bf9246f6f80501ed Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Sun, 15 Mar 2020 20:49:10 +0100 Subject: [PATCH 26/26] [ML] fix tests arrangement --- .../data_frame_analytics/cloning.ts | 17 ++++++----------- .../data_frame_analytics_creation.ts | 7 ++----- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts index bc0a3e63fbb3c0..512de861e673a3 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts @@ -12,7 +12,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('data frame analytics jobs cloning supported by UI form', function() { + describe('jobs cloning supported by UI form', function() { this.tags(['smoke']); const testDataList: Array<{ @@ -119,21 +119,11 @@ export default function({ getService }: FtrProviderContext) { })(); before(async () => { - // Create jobs for cloning - for (const testData of testDataList) { - await esArchiver.load(testData.archive); - await ml.api.createDataFrameAnalyticsJob(testData.job as DataFrameAnalyticsConfig); - } await ml.securityUI.loginAsMlPowerUser(); }); after(async () => { await ml.api.cleanMlIndices(); - // Clean destination indices of the original jobs - for (const testData of testDataList) { - await ml.api.deleteIndices(testData.job.dest!.index as string); - await esArchiver.unload(testData.archive); - } }); for (const testData of testDataList) { @@ -142,6 +132,9 @@ export default function({ getService }: FtrProviderContext) { const cloneDestIndex = `${testData.job!.dest!.index}_clone`; before(async () => { + await esArchiver.load(testData.archive); + await ml.api.createDataFrameAnalyticsJob(testData.job as DataFrameAnalyticsConfig); + await ml.navigation.navigateToMl(); await ml.navigation.navigateToDataFrameAnalytics(); await ml.dataFrameAnalyticsTable.waitForAnalyticsToLoad(); @@ -151,6 +144,8 @@ export default function({ getService }: FtrProviderContext) { after(async () => { await ml.api.deleteIndices(cloneDestIndex); + await ml.api.deleteIndices(testData.job.dest!.index as string); + await esArchiver.unload(testData.archive); }); it('should open the flyout with a proper header', async () => { diff --git a/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts b/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts index 629a42bb82812f..9d5f5753e8b041 100644 --- a/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts @@ -329,11 +329,8 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( }, async isCreateButtonDisabled() { - const attrValue = await testSubjects.getAttribute( - 'mlAnalyticsCreateJobFlyoutCreateButton', - 'disabled' - ); - return attrValue === ''; + const isEnabled = await testSubjects.isEnabled('mlAnalyticsCreateJobFlyoutCreateButton'); + return !isEnabled; }, async createAnalyticsJob() {