[ML] DF Analytics creation: switch to includes table (#70009)

* update modelMemoryLimit when hyperParams change

* update functional clone tests

* switch excludes table to includes table

* Job configuration details update

* fix jest tests and types

* fix translations and validate includes fields

* fix functional test

* handle empty includes selection

* switch filter to field_value_toggle_group

* update clone functional test
This commit is contained in:
Melissa Alvarez 2020-07-06 13:10:01 -04:00 committed by GitHub
parent 89dcdbbbee
commit 31abd6dc28
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 461 additions and 286 deletions

View file

@ -45,6 +45,7 @@ function getError(error) {
export function CustomSelectionTable({
checkboxDisabledCheck,
columns,
currentPage = 0,
filterDefaultFields,
filters,
items,
@ -52,6 +53,7 @@ export function CustomSelectionTable({
onTableChange,
radioDisabledCheck,
selectedIds,
setCurrentPaginationData,
singleSelection,
sortableProperties,
tableItemId = 'id',
@ -80,7 +82,7 @@ export function CustomSelectionTable({
}, [selectedIds]); // eslint-disable-line
useEffect(() => {
const tablePager = new Pager(currentItems.length, itemsPerPage);
const tablePager = new Pager(currentItems.length, itemsPerPage, currentPage);
setPagerSettings({
itemsPerPage: itemsPerPage,
firstItemIndex: tablePager.getFirstItemIndex(),
@ -124,6 +126,13 @@ export function CustomSelectionTable({
}
}
if (setCurrentPaginationData) {
setCurrentPaginationData({
pageIndex: pager.getCurrentPageIndex(),
itemsPerPage: pagerSettings.itemsPerPage,
});
}
onTableChange(currentSelected);
}
@ -389,6 +398,7 @@ export function CustomSelectionTable({
CustomSelectionTable.propTypes = {
checkboxDisabledCheck: PropTypes.func,
columns: PropTypes.array.isRequired,
currentPage: PropTypes.number,
filterDefaultFields: PropTypes.array,
filters: PropTypes.array,
items: PropTypes.array.isRequired,
@ -396,6 +406,7 @@ CustomSelectionTable.propTypes = {
onTableChange: PropTypes.func.isRequired,
radioDisabledCheck: PropTypes.func,
selectedId: PropTypes.array,
setCurrentPaginationData: PropTypes.func,
singleSelection: PropTypes.bool,
sortableProperties: PropTypes.object,
tableItemId: PropTypes.string,

View file

@ -19,14 +19,19 @@ export const AdvancedStep: FC<CreateAnalyticsStepProps> = ({
setCurrentStep,
stepActivated,
}) => {
const showForm = step === ANALYTICS_STEPS.ADVANCED;
const showDetails = step !== ANALYTICS_STEPS.ADVANCED && stepActivated === true;
const dataTestSubj = `mlAnalyticsCreateJobWizardAdvancedStep${showForm ? ' active' : ''}${
showDetails ? ' summary' : ''
}`;
return (
<EuiForm>
{step === ANALYTICS_STEPS.ADVANCED && (
<EuiForm data-test-subj={dataTestSubj}>
{showForm && (
<AdvancedStepForm actions={actions} state={state} setCurrentStep={setCurrentStep} />
)}
{step !== ANALYTICS_STEPS.ADVANCED && stepActivated === true && (
<AdvancedStepDetails setCurrentStep={setCurrentStep} state={state} />
)}
{showDetails && <AdvancedStepDetails setCurrentStep={setCurrentStep} state={state} />}
</EuiForm>
);
};

View file

@ -47,7 +47,7 @@ export const AdvancedStepForm: FC<CreateAnalyticsStepProps> = ({
const [advancedParamErrors, setAdvancedParamErrors] = useState<AdvancedParamErrors>({});
const [fetchingAdvancedParamErrors, setFetchingAdvancedParamErrors] = useState<boolean>(false);
const { setFormState } = actions;
const { setEstimatedModelMemoryLimit, setFormState } = actions;
const { form, isJobCreated } = state;
const {
computeFeatureInfluence,
@ -87,10 +87,15 @@ export const AdvancedStepForm: FC<CreateAnalyticsStepProps> = ({
useEffect(() => {
setFetchingAdvancedParamErrors(true);
(async function () {
const { success, errorMessage } = await fetchExplainData(form);
const { success, errorMessage, expectedMemory } = await fetchExplainData(form);
const paramErrors: AdvancedParamErrors = {};
if (!success) {
if (success) {
if (modelMemoryLimit !== expectedMemory) {
setEstimatedModelMemoryLimit(expectedMemory);
setFormState({ modelMemoryLimit: expectedMemory });
}
} else {
// Check which field is invalid
Object.values(ANALYSIS_ADVANCED_FIELDS).forEach((param) => {
if (errorMessage.includes(`[${param}]`)) {

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, Fragment, memo, useEffect, useState } from 'react';
import React, { FC, Fragment, useEffect, useState } from 'react';
import { EuiCallOut, EuiFormRow, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
// @ts-ignore no declaration
import { LEFT_ALIGNMENT, CENTER_ALIGNMENT, SortableProperties } from '@elastic/eui/lib/services';
@ -14,6 +14,13 @@ import { FieldSelectionItem } from '../../../../common/analytics';
// @ts-ignore could not find declaration file
import { CustomSelectionTable } from '../../../../../components/custom_selection_table';
const minimumFieldsMessage = i18n.translate(
'xpack.ml.dataframe.analytics.create.analysisFieldsTable.minimumFieldsMessage',
{
defaultMessage: 'At least one field must be selected.',
}
);
const columns = [
{
id: 'checkbox',
@ -22,9 +29,12 @@ const columns = [
width: '32px',
},
{
label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.fieldNameColumn', {
defaultMessage: 'Field name',
}),
label: i18n.translate(
'xpack.ml.dataframe.analytics.create.analysisFieldsTable.fieldNameColumn',
{
defaultMessage: 'Field name',
}
),
id: 'name',
isSortable: true,
alignment: LEFT_ALIGNMENT,
@ -68,140 +78,154 @@ const columns = [
];
const checkboxDisabledCheck = (item: FieldSelectionItem) =>
(item.is_included === false && !item.reason?.includes('in excludes list')) ||
item.is_required === true;
item.is_required === true || (item.reason && item.reason.includes('unsupported type'));
export const MemoizedAnalysisFieldsTable: FC<{
excludes: string[];
export const AnalysisFieldsTable: FC<{
dependentVariable?: string;
includes: string[];
loadingItems: boolean;
setFormState: any;
setFormState: React.Dispatch<React.SetStateAction<any>>;
tableItems: FieldSelectionItem[];
}> = memo(
({ excludes, loadingItems, setFormState, tableItems }) => {
const [sortableProperties, setSortableProperties] = useState();
const [currentSelection, setCurrentSelection] = useState<any[]>([]);
}> = ({ dependentVariable, includes, loadingItems, setFormState, tableItems }) => {
const [sortableProperties, setSortableProperties] = useState();
const [currentPaginationData, setCurrentPaginationData] = useState<{
pageIndex: number;
itemsPerPage: number;
}>({ pageIndex: 0, itemsPerPage: 5 });
const [minimumFieldsRequiredMessage, setMinimumFieldsRequiredMessage] = useState<
undefined | string
>(undefined);
useEffect(() => {
if (excludes.length > 0) {
setCurrentSelection(excludes);
}
}, [tableItems]);
useEffect(() => {
if (includes.length === 0 && tableItems.length > 0) {
const includedFields: string[] = [];
tableItems.forEach((field) => {
if (field.is_included === true) {
includedFields.push(field.name);
}
});
setFormState({ includes: includedFields });
} else if (includes.length > 0) {
setFormState({ includes });
}
setMinimumFieldsRequiredMessage(undefined);
}, [tableItems]);
// Only set form state on unmount to prevent re-renders due to props changing if exludes was updated on each selection
useEffect(() => {
return () => {
setFormState({ excludes: currentSelection });
};
}, [currentSelection]);
useEffect(() => {
let sortablePropertyItems = [];
const defaultSortProperty = 'name';
useEffect(() => {
let sortablePropertyItems = [];
const defaultSortProperty = 'name';
sortablePropertyItems = [
{
name: 'name',
getValue: (item: any) => item.name.toLowerCase(),
isAscending: true,
},
{
name: 'is_included',
getValue: (item: any) => item.is_included,
isAscending: true,
},
{
name: 'is_required',
getValue: (item: any) => item.is_required,
isAscending: true,
},
];
const sortableProps = new SortableProperties(sortablePropertyItems, defaultSortProperty);
setSortableProperties(sortableProps);
}, []);
const filters = [
sortablePropertyItems = [
{
type: 'field_value_selection',
field: 'is_included',
name: i18n.translate('xpack.ml.dataframe.analytics.create.excludedFilterLabel', {
defaultMessage: 'Is included',
}),
multiSelect: false,
options: [
{
value: true,
view: (
<EuiText grow={false}>
{i18n.translate('xpack.ml.dataframe.analytics.create.isIncludedOption', {
defaultMessage: 'Yes',
})}
</EuiText>
),
},
{
value: false,
view: (
<EuiText grow={false}>
{i18n.translate('xpack.ml.dataframe.analytics.create.isNotIncludedOption', {
defaultMessage: 'No',
})}
</EuiText>
),
},
],
name: 'name',
getValue: (item: any) => item.name.toLowerCase(),
isAscending: true,
},
{
name: 'is_included',
getValue: (item: any) => item.is_included,
isAscending: true,
},
{
name: 'is_required',
getValue: (item: any) => item.is_required,
isAscending: true,
},
];
const sortableProps = new SortableProperties(sortablePropertyItems, defaultSortProperty);
return (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.excludedFieldsLabel', {
defaultMessage: 'Excluded fields',
setSortableProperties(sortableProps);
}, []);
const filters = [
{
type: 'field_value_toggle_group',
field: 'is_included',
items: [
{
value: true,
name: i18n.translate('xpack.ml.dataframe.analytics.create.isIncludedOption', {
defaultMessage: 'Is included',
}),
},
{
value: false,
name: i18n.translate('xpack.ml.dataframe.analytics.create.isNotIncludedOption', {
defaultMessage: 'Is not included',
}),
},
],
},
];
return (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.includedFieldsLabel', {
defaultMessage: 'Included fields',
})}
isInvalid={minimumFieldsRequiredMessage !== undefined}
error={minimumFieldsRequiredMessage}
>
<Fragment />
</EuiFormRow>
{tableItems.length > 0 && minimumFieldsRequiredMessage === undefined && (
<EuiText size="xs">
{i18n.translate('xpack.ml.dataframe.analytics.create.includedFieldsCount', {
defaultMessage:
'{numFields, plural, one {# field} other {# fields}} included in the analysis',
values: { numFields: includes.length },
})}
</EuiText>
)}
{tableItems.length === 0 && (
<EuiCallOut
title={i18n.translate('xpack.ml.dataframe.analytics.create.calloutTitle', {
defaultMessage: 'Analysis fields not available',
})}
helpText={i18n.translate(
'xpack.ml.dataframe.analytics.create.excludedFieldsLabelHelpText',
{
defaultMessage: 'From included fields, select fields to exclude from analysis.',
}
)}
>
<Fragment />
</EuiFormRow>
{tableItems.length === 0 && (
<EuiCallOut
title={i18n.translate('xpack.ml.dataframe.analytics.create.calloutTitle', {
defaultMessage: 'Analysis fields not available',
})}
>
<FormattedMessage
id="xpack.ml.dataframe.analytics.create.calloutMessage"
defaultMessage="Additional data required to load analysis fields."
/>
</EuiCallOut>
)}
{tableItems.length > 0 && (
<EuiPanel paddingSize="m" data-test-subj="mlAnalyticsCreateJobWizardExcludesSelect">
<CustomSelectionTable
data-test-subj="mlAnalyticsCreationAnalysisFieldsTable"
checkboxDisabledCheck={checkboxDisabledCheck}
columns={columns}
filters={filters}
items={tableItems}
itemsPerPage={5}
onTableChange={(selection: FieldSelectionItem[]) => {
setCurrentSelection(selection);
}}
selectedIds={currentSelection}
singleSelection={false}
sortableProperties={sortableProperties}
tableItemId={'name'}
/>
</EuiPanel>
)}
<EuiSpacer />
</Fragment>
);
},
(prevProps, nextProps) => prevProps.tableItems.length === nextProps.tableItems.length
);
<FormattedMessage
id="xpack.ml.dataframe.analytics.create.calloutMessage"
defaultMessage="Additional data required to load analysis fields."
/>
</EuiCallOut>
)}
{tableItems.length > 0 && (
<EuiPanel paddingSize="m" data-test-subj="mlAnalyticsCreateJobWizardIncludesSelect">
<CustomSelectionTable
currentPage={currentPaginationData.pageIndex}
data-test-subj="mlAnalyticsCreationAnalysisFieldsTable"
checkboxDisabledCheck={checkboxDisabledCheck}
columns={columns}
filters={filters}
items={tableItems}
itemsPerPage={currentPaginationData.itemsPerPage}
onTableChange={(selection: string[]) => {
// dependent variable must always be in includes
if (
dependentVariable !== undefined &&
dependentVariable !== '' &&
selection.length === 0
) {
selection = [dependentVariable];
}
// If nothing selected show minimum fields required message and don't update form yet
if (selection.length === 0) {
setMinimumFieldsRequiredMessage(minimumFieldsMessage);
} else {
setMinimumFieldsRequiredMessage(undefined);
setFormState({ includes: selection });
}
}}
selectedIds={includes}
setCurrentPaginationData={setCurrentPaginationData}
singleSelection={false}
sortableProperties={sortableProperties}
tableItemId={'name'}
/>
</EuiPanel>
)}
<EuiSpacer />
</Fragment>
);
};

View file

@ -19,17 +19,19 @@ export const ConfigurationStep: FC<CreateAnalyticsStepProps> = ({
step,
stepActivated,
}) => {
const showForm = step === ANALYTICS_STEPS.CONFIGURATION;
const showDetails = step !== ANALYTICS_STEPS.CONFIGURATION && stepActivated === true;
const dataTestSubj = `mlAnalyticsCreateJobWizardConfigurationStep${showForm ? ' active' : ''}${
showDetails ? ' summary' : ''
}`;
return (
<EuiForm
className="mlDataFrameAnalyticsCreateForm"
data-test-subj="mlAnalyticsCreateJobWizardConfigurationStep"
>
{step === ANALYTICS_STEPS.CONFIGURATION && (
<EuiForm className="mlDataFrameAnalyticsCreateForm" data-test-subj={dataTestSubj}>
{showForm && (
<ConfigurationStepForm actions={actions} state={state} setCurrentStep={setCurrentStep} />
)}
{step !== ANALYTICS_STEPS.CONFIGURATION && stepActivated === true && (
<ConfigurationStepDetails setCurrentStep={setCurrentStep} state={state} />
)}
{showDetails && <ConfigurationStepDetails setCurrentStep={setCurrentStep} state={state} />}
</EuiForm>
);
};

View file

@ -21,6 +21,8 @@ import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics';
import { useMlContext } from '../../../../../contexts/ml';
import { ANALYTICS_STEPS } from '../../page';
const MAX_INCLUDES_LENGTH = 5;
interface Props {
setCurrentStep: React.Dispatch<React.SetStateAction<any>>;
state: State;
@ -30,7 +32,7 @@ export const ConfigurationStepDetails: FC<Props> = ({ setCurrentStep, state }) =
const mlContext = useMlContext();
const { currentIndexPattern } = mlContext;
const { form, isJobCreated } = state;
const { dependentVariable, excludes, jobConfigQueryString, jobType, trainingPercent } = form;
const { dependentVariable, includes, jobConfigQueryString, jobType, trainingPercent } = form;
const isJobTypeWithDepVar =
jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION;
@ -61,10 +63,15 @@ export const ConfigurationStepDetails: FC<Props> = ({ setCurrentStep, state }) =
const detailsThirdCol = [
{
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.excludedFields', {
defaultMessage: 'Excluded fields',
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.includedFields', {
defaultMessage: 'Included fields',
}),
description: excludes.length > 0 ? excludes.join(', ') : UNSET_CONFIG_ITEM,
description:
includes.length > MAX_INCLUDES_LENGTH
? `${includes.slice(0, MAX_INCLUDES_LENGTH).join(', ')} ... (and ${
includes.length - MAX_INCLUDES_LENGTH
} more)`
: includes.join(', '),
},
];

View file

@ -39,7 +39,7 @@ import { ANALYTICS_STEPS } from '../../page';
import { ContinueButton } from '../continue_button';
import { JobType } from './job_type';
import { SupportedFieldsMessage } from './supported_fields_message';
import { MemoizedAnalysisFieldsTable } from './analysis_fields_table';
import { AnalysisFieldsTable } from './analysis_fields_table';
import { DataGrid } from '../../../../../components/data_grid';
import { fetchExplainData } from '../shared';
import { useIndexData } from '../../hooks';
@ -49,7 +49,8 @@ import { useSavedSearch } from './use_saved_search';
const requiredFieldsErrorText = i18n.translate(
'xpack.ml.dataframe.analytics.createWizard.requiredFieldsErrorMessage',
{
defaultMessage: 'At least one field must be included in the analysis.',
defaultMessage:
'At least one field must be included in the analysis in addition to the dependent variable.',
}
);
@ -69,17 +70,20 @@ export const ConfigurationStepForm: FC<CreateAnalyticsStepProps> = ({
const [dependentVariableOptions, setDependentVariableOptions] = useState<
EuiComboBoxOptionOption[]
>([]);
const [excludesTableItems, setExcludesTableItems] = useState<FieldSelectionItem[]>([]);
const [includesTableItems, setIncludesTableItems] = useState<FieldSelectionItem[]>([]);
const [maxDistinctValuesError, setMaxDistinctValuesError] = useState<string | undefined>(
undefined
);
const [unsupportedFieldsError, setUnsupportedFieldsError] = useState<string | undefined>(
undefined
);
const { setEstimatedModelMemoryLimit, setFormState } = actions;
const { estimatedModelMemoryLimit, form, isJobCreated, requestMessages } = state;
const firstUpdate = useRef<boolean>(true);
const {
dependentVariable,
excludes,
includes,
jobConfigQuery,
jobConfigQueryString,
jobType,
@ -117,7 +121,8 @@ export const ConfigurationStepForm: FC<CreateAnalyticsStepProps> = ({
dependentVariableEmpty ||
jobType === undefined ||
maxDistinctValuesError !== undefined ||
requiredFieldsError !== undefined;
requiredFieldsError !== undefined ||
unsupportedFieldsError !== undefined;
const loadDepVarOptions = async (formState: State['form']) => {
setLoadingDepVarOptions(true);
@ -187,7 +192,8 @@ export const ConfigurationStepForm: FC<CreateAnalyticsStepProps> = ({
setLoadingFieldOptions(false);
setFieldOptionsFetchFail(false);
setMaxDistinctValuesError(undefined);
setExcludesTableItems(fieldSelection ? fieldSelection : []);
setUnsupportedFieldsError(undefined);
setIncludesTableItems(fieldSelection ? fieldSelection : []);
setFormState({
...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemory } : {}),
requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined,
@ -200,6 +206,7 @@ export const ConfigurationStepForm: FC<CreateAnalyticsStepProps> = ({
}
} else {
let maxDistinctValuesErrorMessage;
let unsupportedFieldsErrorMessage;
if (
jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION &&
errorMessage.includes('status_exception') &&
@ -208,6 +215,10 @@ export const ConfigurationStepForm: FC<CreateAnalyticsStepProps> = ({
maxDistinctValuesErrorMessage = errorMessage;
}
if (errorMessage.includes('status_exception') && errorMessage.includes('unsupported type')) {
unsupportedFieldsErrorMessage = errorMessage;
}
if (
errorMessage.includes('status_exception') &&
errorMessage.includes('Unable to estimate memory usage as no documents')
@ -231,6 +242,7 @@ export const ConfigurationStepForm: FC<CreateAnalyticsStepProps> = ({
setLoadingFieldOptions(false);
setFieldOptionsFetchFail(true);
setMaxDistinctValuesError(maxDistinctValuesErrorMessage);
setUnsupportedFieldsError(unsupportedFieldsErrorMessage);
setFormState({
...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: fallbackModelMemoryLimit } : {}),
});
@ -267,7 +279,7 @@ export const ConfigurationStepForm: FC<CreateAnalyticsStepProps> = ({
return () => {
debouncedGetExplainData.cancel();
};
}, [jobType, dependentVariable, trainingPercent, JSON.stringify(excludes), jobConfigQueryString]);
}, [jobType, dependentVariable, trainingPercent, JSON.stringify(includes), jobConfigQueryString]);
return (
<Fragment>
@ -392,21 +404,32 @@ export const ConfigurationStepForm: FC<CreateAnalyticsStepProps> = ({
)}
<EuiFormRow
fullWidth
isInvalid={requiredFieldsError !== undefined}
error={
requiredFieldsError !== undefined && [
i18n.translate('xpack.ml.dataframe.analytics.create.requiredFieldsError', {
defaultMessage: 'Invalid. {message}',
values: { message: requiredFieldsError },
}),
]
}
isInvalid={requiredFieldsError !== undefined || unsupportedFieldsError !== undefined}
error={[
...(requiredFieldsError !== undefined
? [
i18n.translate('xpack.ml.dataframe.analytics.create.requiredFieldsError', {
defaultMessage: 'Invalid. {message}',
values: { message: requiredFieldsError },
}),
]
: []),
...(unsupportedFieldsError !== undefined
? [
i18n.translate('xpack.ml.dataframe.analytics.create.unsupportedFieldsError', {
defaultMessage: 'Invalid. {message}',
values: { message: unsupportedFieldsError },
}),
]
: []),
]}
>
<Fragment />
</EuiFormRow>
<MemoizedAnalysisFieldsTable
excludes={excludes}
tableItems={excludesTableItems}
<AnalysisFieldsTable
dependentVariable={dependentVariable}
includes={includes}
tableItems={includesTableItems}
loadingItems={loadingFieldOptions}
setFormState={setFormState}
/>

View file

@ -71,7 +71,7 @@ export const JobType: FC<Props> = ({ type, setFormState }) => {
setFormState({
previousJobType: type,
jobType: value,
excludes: [],
includes: [],
requiredFieldsError: undefined,
});
}}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, Fragment, useState } from 'react';
import React, { FC, useState } from 'react';
import {
EuiButton,
EuiCheckbox,
@ -45,7 +45,7 @@ export const CreateStep: FC<Props> = ({ actions, state, step }) => {
};
return (
<Fragment>
<div data-test-subj="mlAnalyticsCreateJobWizardCreateStep active">
{!isJobCreated && !isJobStarted && (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
@ -88,6 +88,6 @@ export const CreateStep: FC<Props> = ({ actions, state, step }) => {
<Messages messages={requestMessages} />
{isJobCreated === true && showProgress && <ProgressStats jobId={jobId} />}
{isJobCreated === true && <BackToListPanel />}
</Fragment>
</div>
);
};

View file

@ -19,14 +19,19 @@ export const DetailsStep: FC<CreateAnalyticsStepProps> = ({
step,
stepActivated,
}) => {
const showForm = step === ANALYTICS_STEPS.DETAILS;
const showDetails = step !== ANALYTICS_STEPS.DETAILS && stepActivated === true;
const dataTestSubj = `mlAnalyticsCreateJobWizardDetailsStep${showForm ? ' active' : ''}${
showDetails ? ' summary' : ''
}`;
return (
<EuiForm className="mlDataFrameAnalyticsCreateForm">
{step === ANALYTICS_STEPS.DETAILS && (
<EuiForm className="mlDataFrameAnalyticsCreateForm" data-test-subj={dataTestSubj}>
{showForm && (
<DetailsStepForm actions={actions} state={state} setCurrentStep={setCurrentStep} />
)}
{step !== ANALYTICS_STEPS.DETAILS && stepActivated === true && (
<DetailsStepDetails setCurrentStep={setCurrentStep} state={state} />
)}
{showDetails && <DetailsStepDetails setCurrentStep={setCurrentStep} state={state} />}
</EuiForm>
);
};

View file

@ -109,7 +109,6 @@ export const Page: FC<Props> = ({ jobId }) => {
/>
),
status: currentStep >= ANALYTICS_STEPS.ADVANCED ? undefined : ('incomplete' as EuiStepStatus),
'data-test-subj': 'mlAnalyticsCreateJobWizardAdvancedStep',
},
{
title: i18n.translate('xpack.ml.dataframe.analytics.creation.detailsStepTitle', {
@ -124,7 +123,6 @@ export const Page: FC<Props> = ({ jobId }) => {
/>
),
status: currentStep >= ANALYTICS_STEPS.DETAILS ? undefined : ('incomplete' as EuiStepStatus),
'data-test-subj': 'mlAnalyticsCreateJobWizardDetailsStep',
},
{
title: i18n.translate('xpack.ml.dataframe.analytics.creation.createStepTitle', {
@ -132,7 +130,6 @@ export const Page: FC<Props> = ({ jobId }) => {
}),
children: <CreateStep {...createAnalyticsForm} step={currentStep} />,
status: currentStep >= ANALYTICS_STEPS.CREATE ? undefined : ('incomplete' as EuiStepStatus),
'data-test-subj': 'mlAnalyticsCreateJobWizardCreateStep',
},
];

View file

@ -64,7 +64,7 @@ describe('Analytics job clone action', () => {
},
analyzed_fields: {
includes: [],
excludes: ['id', 'outlier'],
excludes: [],
},
model_memory_limit: '1mb',
allow_lazy_start: false,
@ -96,7 +96,7 @@ describe('Analytics job clone action', () => {
},
},
analyzed_fields: {
includes: [],
includes: ['included_field', 'other_included_field'],
excludes: [],
},
model_memory_limit: '150mb',
@ -140,6 +140,40 @@ describe('Analytics job clone action', () => {
expect(isAdvancedConfig(advancedClassificationJob)).toBe(true);
});
test('should detect advanced classification job with excludes set', () => {
const advancedClassificationJob = {
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,
num_top_feature_importance_values: 4,
prediction_field_name: 'y_prediction',
training_percent: 2,
randomize_seed: 6233212276062807000,
},
},
analyzed_fields: {
includes: [],
excludes: ['excluded_field', 'other_excluded_field'],
},
model_memory_limit: '350mb',
allow_lazy_start: false,
};
expect(isAdvancedConfig(advancedClassificationJob)).toBe(true);
});
test('should detect advanced regression job', () => {
const advancedRegressionJob = {
description: "Outlier detection job with 'glass' dataset",
@ -161,7 +195,7 @@ describe('Analytics job clone action', () => {
},
analyzed_fields: {
includes: [],
excludes: ['id', 'outlier'],
excludes: [],
},
model_memory_limit: '1mb',
allow_lazy_start: false,

View file

@ -217,11 +217,11 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo
analyzed_fields: {
excludes: {
optional: true,
formKey: 'excludes',
defaultValue: [],
},
includes: {
optional: true,
formKey: 'includes',
defaultValue: [],
},
},

View file

@ -116,7 +116,7 @@ export const validateNumTopFeatureImportanceValues = (
};
export const validateAdvancedEditor = (state: State): State => {
const { jobIdEmpty, jobIdValid, jobIdExists, jobType, createIndexPattern, excludes } = state.form;
const { jobIdEmpty, jobIdValid, jobIdExists, jobType, createIndexPattern, includes } = state.form;
const { jobConfig } = state;
state.advancedEditorMessages = [];
@ -152,7 +152,7 @@ export const validateAdvancedEditor = (state: State): State => {
}
let dependentVariableEmpty = false;
let excludesValid = true;
let includesValid = true;
let trainingPercentValid = true;
let numTopFeatureImportanceValuesValid = true;
@ -170,14 +170,19 @@ export const validateAdvancedEditor = (state: State): State => {
const dependentVariableName = getDependentVar(jobConfig.analysis) || '';
dependentVariableEmpty = dependentVariableName === '';
if (!dependentVariableEmpty && excludes.includes(dependentVariableName)) {
excludesValid = false;
if (
!dependentVariableEmpty &&
includes !== undefined &&
includes.length > 0 &&
!includes.includes(dependentVariableName)
) {
includesValid = false;
state.advancedEditorMessages.push({
error: i18n.translate(
'xpack.ml.dataframe.analytics.create.advancedEditorMessage.excludesInvalid',
'xpack.ml.dataframe.analytics.create.advancedEditorMessage.includesInvalid',
{
defaultMessage: 'The dependent variable cannot be excluded.',
defaultMessage: 'The dependent variable must be included.',
}
),
message: '',
@ -321,7 +326,7 @@ export const validateAdvancedEditor = (state: State): State => {
state.form.destinationIndexPatternTitleExists = destinationIndexPatternTitleExists;
state.isValid =
excludesValid &&
includesValid &&
trainingPercentValid &&
state.form.modelMemoryLimitUnitValid &&
!jobIdEmpty &&

View file

@ -42,6 +42,37 @@ const regJobConfig = {
allow_lazy_start: false,
};
const outlierJobConfig = {
id: 'outlier-test-01',
description: 'outlier test job description',
source: {
index: ['outlier-test-index'],
query: {
match_all: {},
},
},
dest: {
index: 'outlier-test-01-index',
results_field: 'ml',
},
analysis: {
outlier_detection: {
feature_influence_threshold: 0.01,
outlier_fraction: 0.05,
compute_feature_influence: false,
method: 'lof',
},
},
analyzed_fields: {
includes: ['field', 'other_field'],
excludes: [],
},
model_memory_limit: '22mb',
create_time: 1590514291395,
version: '8.0.0',
allow_lazy_start: false,
};
describe('useCreateAnalyticsForm', () => {
test('state: getJobConfigFromFormState()', () => {
const state = getInitialState();
@ -53,8 +84,8 @@ describe('useCreateAnalyticsForm', () => {
expect(jobConfig?.dest?.index).toBe('the-destination-index');
expect(jobConfig?.source?.index).toBe('the-source-index');
expect(jobConfig?.analyzed_fields?.excludes).toStrictEqual([]);
expect(typeof jobConfig?.analyzed_fields?.includes).toBe('undefined');
expect(jobConfig?.analyzed_fields?.includes).toStrictEqual([]);
expect(typeof jobConfig?.analyzed_fields?.excludes).toBe('undefined');
// test the conversion of comma-separated Kibana index patterns to ES array based index patterns
state.form.sourceIndex = 'the-source-index-1,the-source-index-2';
@ -65,11 +96,11 @@ describe('useCreateAnalyticsForm', () => {
]);
});
test('state: getCloneFormStateFromJobConfig()', () => {
test('state: getCloneFormStateFromJobConfig() regression', () => {
const clonedState = getCloneFormStateFromJobConfig(regJobConfig);
expect(clonedState?.sourceIndex).toBe('reg-test-index');
expect(clonedState?.excludes).toStrictEqual([]);
expect(clonedState?.includes).toStrictEqual([]);
expect(clonedState?.dependentVariable).toBe('price');
expect(clonedState?.numTopFeatureImportanceValues).toBe(2);
expect(clonedState?.predictionFieldName).toBe('airbnb_test');
@ -80,4 +111,19 @@ describe('useCreateAnalyticsForm', () => {
expect(clonedState?.destinationIndex).toBe(undefined);
expect(clonedState?.jobId).toBe(undefined);
});
test('state: getCloneFormStateFromJobConfig() outlier detection', () => {
const clonedState = getCloneFormStateFromJobConfig(outlierJobConfig);
expect(clonedState?.sourceIndex).toBe('outlier-test-index');
expect(clonedState?.includes).toStrictEqual(['field', 'other_field']);
expect(clonedState?.featureInfluenceThreshold).toBe(0.01);
expect(clonedState?.outlierFraction).toBe(0.05);
expect(clonedState?.computeFeatureInfluence).toBe(false);
expect(clonedState?.method).toBe('lof');
expect(clonedState?.modelMemoryLimit).toBe('22mb');
// destination index and job id should be undefined
expect(clonedState?.destinationIndex).toBe(undefined);
expect(clonedState?.jobId).toBe(undefined);
});
});

View file

@ -7,11 +7,8 @@
import { DeepPartial, DeepReadonly } from '../../../../../../../common/types/common';
import { checkPermission } from '../../../../../capabilities/check_capabilities';
import { mlNodesAvailable } from '../../../../../ml_nodes_check';
import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
import {
isClassificationAnalysis,
isRegressionAnalysis,
DataFrameAnalyticsId,
DataFrameAnalyticsConfig,
ANALYSIS_CONFIG_TYPE,
@ -57,10 +54,10 @@ export interface State {
destinationIndexNameValid: boolean;
destinationIndexPatternTitleExists: boolean;
eta: undefined | number;
excludes: string[];
featureBagFraction: undefined | number;
featureInfluenceThreshold: undefined | number;
gamma: undefined | number;
includes: string[];
jobId: DataFrameAnalyticsId;
jobIdExists: boolean;
jobIdEmpty: boolean;
@ -122,10 +119,10 @@ export const getInitialState = (): State => ({
destinationIndexNameValid: false,
destinationIndexPatternTitleExists: false,
eta: undefined,
excludes: [],
featureBagFraction: undefined,
featureInfluenceThreshold: undefined,
gamma: undefined,
includes: [],
jobId: '',
jobIdExists: false,
jobIdEmpty: true,
@ -175,55 +172,6 @@ export const getInitialState = (): State => ({
estimatedModelMemoryLimit: '',
});
const getExcludesFields = (excluded: string[]) => {
const { fields } = newJobCapsService;
const updatedExcluded: string[] = [];
// Loop through excluded fields to check for multiple types of same field
for (let i = 0; i < excluded.length; i++) {
const fieldName = excluded[i];
let mainField;
// No dot in fieldName - it is the main field
if (fieldName.includes('.') === false) {
mainField = fieldName;
} else {
// Dot in fieldName - check if there's a field whose name equals the fieldName with the last dot suffix removed
const regex = /\.[^.]*$/;
const suffixRemovedField = fieldName.replace(regex, '');
const fieldMatch = newJobCapsService.getFieldById(suffixRemovedField);
// There's a match - set as the main field
if (fieldMatch !== null) {
mainField = suffixRemovedField;
} else {
// No main field to be found - add the fieldName to updatedExcluded array if it's not already there
if (updatedExcluded.includes(fieldName) === false) {
updatedExcluded.push(fieldName);
}
}
}
if (mainField !== undefined) {
// Add the main field to the updatedExcluded array if it's not already there
if (updatedExcluded.includes(mainField) === false) {
updatedExcluded.push(mainField);
}
// Create regex to find all other fields whose names begin with main field followed by a dot
const regex = new RegExp(`${mainField}\\..+`);
// Loop through fields and add fields matching the pattern to updatedExcluded array
for (let j = 0; j < fields.length; j++) {
const field = fields[j].name;
if (updatedExcluded.includes(field) === false && field.match(regex) !== null) {
updatedExcluded.push(field);
}
}
}
}
return updatedExcluded;
};
export const getJobConfigFromFormState = (
formState: State['form']
): DeepPartial<DataFrameAnalyticsConfig> => {
@ -242,7 +190,7 @@ export const getJobConfigFromFormState = (
index: formState.destinationIndex,
},
analyzed_fields: {
excludes: getExcludesFields(formState.excludes),
includes: formState.includes,
},
analysis: {
outlier_detection: {},
@ -333,21 +281,16 @@ export function getCloneFormStateFromJobConfig(
? analyticsJobConfig.source.index.join(',')
: analyticsJobConfig.source.index,
modelMemoryLimit: analyticsJobConfig.model_memory_limit,
excludes: analyticsJobConfig.analyzed_fields.excludes,
includes: analyticsJobConfig.analyzed_fields.includes,
};
if (
isRegressionAnalysis(analyticsJobConfig.analysis) ||
isClassificationAnalysis(analyticsJobConfig.analysis)
) {
const analysisConfig = analyticsJobConfig.analysis[jobType];
const analysisConfig = analyticsJobConfig.analysis[jobType];
for (const key in analysisConfig) {
if (analysisConfig.hasOwnProperty(key)) {
const camelCased = toCamelCase(key);
// @ts-ignore
resultState[camelCased] = analysisConfig[key];
}
for (const key in analysisConfig) {
if (analysisConfig.hasOwnProperty(key)) {
const camelCased = toCamelCase(key);
// @ts-ignore
resultState[camelCased] = analysisConfig[key];
}
}

View file

@ -9564,7 +9564,6 @@
"xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameEmpty": "デスティネーションインデックス名は未入力のままにできません。",
"xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameExistsWarn": "この対象インデックス名のインデックスは既に存在します。この分析ジョブを実行すると、デスティネーションインデックスが変更されます。",
"xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameValid": "無効なデスティネーションインデックス名。",
"xpack.ml.dataframe.analytics.create.advancedEditorMessage.excludesInvalid": "従属変数を除外できません。",
"xpack.ml.dataframe.analytics.create.advancedEditorMessage.modelMemoryLimitEmpty": "モデルメモリー制限フィールドを空にすることはできません。",
"xpack.ml.dataframe.analytics.create.advancedEditorMessage.numTopFeatureImportanceValuesInvalid": "num_top_feature_importance_valuesの値は整数の{min}以上でなければなりません。",
"xpack.ml.dataframe.analytics.create.advancedEditorMessage.sourceIndexNameEmpty": "ソースインデックス名は未入力のままにできません。",
@ -9591,7 +9590,6 @@
"xpack.ml.dataframe.analytics.create.errorGettingDataFrameIndexNames": "既存のインデックス名の取得中にエラーが発生しました。",
"xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました。",
"xpack.ml.dataframe.analytics.create.errorStartingDataFrameAnalyticsJob": "データフレーム分析ジョブの開始中にエラーが発生しました。",
"xpack.ml.dataframe.analytics.create.excludedFieldsLabel": "除外されたフィールド",
"xpack.ml.dataframe.analytics.create.indexPatternAlreadyExistsError": "このタイトルのインデックスパターンが既に存在します。",
"xpack.ml.dataframe.analytics.create.indexPatternExistsError": "このタイトルのインデックスパターンが既に存在します。",
"xpack.ml.dataframe.analytics.create.jobDescription.helpText": "オプションの説明テキストです",

View file

@ -9568,7 +9568,6 @@
"xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameEmpty": "目标索引名称不得为空。",
"xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameExistsWarn": "具有此目标索引名称的索引已存在。请注意,运行此分析作业将会修改此目标索引。",
"xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameValid": "目标索引名称无效。",
"xpack.ml.dataframe.analytics.create.advancedEditorMessage.excludesInvalid": "无法排除依赖变量。",
"xpack.ml.dataframe.analytics.create.advancedEditorMessage.modelMemoryLimitEmpty": "模型内存限制字段不得为空。",
"xpack.ml.dataframe.analytics.create.advancedEditorMessage.numTopFeatureImportanceValuesInvalid": "num_top_feature_importance_values 的值必须是 {min} 或更高的整数。",
"xpack.ml.dataframe.analytics.create.advancedEditorMessage.sourceIndexNameEmpty": "源索引名称不得为空。",
@ -9595,7 +9594,6 @@
"xpack.ml.dataframe.analytics.create.errorGettingDataFrameIndexNames": "获取现有索引名称时发生错误:",
"xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "获取现有索引模式标题时发生错误:",
"xpack.ml.dataframe.analytics.create.errorStartingDataFrameAnalyticsJob": "启动数据帧分析作业时发生错误:",
"xpack.ml.dataframe.analytics.create.excludedFieldsLabel": "排除的字段",
"xpack.ml.dataframe.analytics.create.indexPatternAlreadyExistsError": "具有此名称的索引模式已存在。",
"xpack.ml.dataframe.analytics.create.indexPatternExistsError": "具有此名称的索引模式已存在。",
"xpack.ml.dataframe.analytics.create.jobDescription.helpText": "可选的描述文本",

View file

@ -66,6 +66,7 @@ export default function ({ getService }: FtrProviderContext) {
it('selects the source data and loads the job wizard page', async () => {
await ml.jobSourceSelection.selectSourceForAnalyticsJob(testData.source);
await ml.dataFrameAnalyticsCreation.assertConfigurationStepActive();
});
it('selects the job type', async () => {
@ -83,6 +84,14 @@ export default function ({ getService }: FtrProviderContext) {
await ml.dataFrameAnalyticsCreation.setTrainingPercent(testData.trainingPercent);
});
it('displays the source data preview', async () => {
await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewExists();
});
it('displays the include fields selection', async () => {
await ml.dataFrameAnalyticsCreation.assertIncludeFieldsSelectionExists();
});
it('continues to the additional options step', async () => {
await ml.dataFrameAnalyticsCreation.continueToAdditionalOptionsStep();
});

View file

@ -45,6 +45,7 @@ export default function ({ getService }: FtrProviderContext) {
},
analysis: {
classification: {
prediction_field_name: 'test',
dependent_variable: 'y',
training_percent: 20,
},
@ -107,6 +108,7 @@ export default function ({ getService }: FtrProviderContext) {
},
analysis: {
regression: {
prediction_field_name: 'test',
dependent_variable: 'stab',
training_percent: 20,
},
@ -157,9 +159,9 @@ export default function ({ getService }: FtrProviderContext) {
});
it('should open the wizard with a proper header', async () => {
expect(await ml.dataFrameAnalyticsCreation.getHeaderText()).to.match(
/Clone analytics job/
);
const headerText = await ml.dataFrameAnalyticsCreation.getHeaderText();
expect(headerText).to.match(/Clone job/);
await ml.dataFrameAnalyticsCreation.assertConfigurationStepActive();
});
it('should have correct init form values for config step', async () => {
@ -174,7 +176,7 @@ export default function ({ getService }: FtrProviderContext) {
it('should have correct init form values for additional options step', async () => {
await ml.dataFrameAnalyticsCreation.assertInitialCloneJobAdditionalOptionsStep(
testData.job as DataFrameAnalyticsConfig
testData.job.analysis as DataFrameAnalyticsConfig['analysis']
);
});

View file

@ -12,6 +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'));
loadTestFile(require.resolve('./cloning'));
});
}

View file

@ -64,6 +64,7 @@ export default function ({ getService }: FtrProviderContext) {
it('selects the source data and loads the job wizard page', async () => {
await ml.jobSourceSelection.selectSourceForAnalyticsJob(testData.source);
await ml.dataFrameAnalyticsCreation.assertConfigurationStepActive();
});
it('selects the job type', async () => {
@ -79,6 +80,14 @@ export default function ({ getService }: FtrProviderContext) {
await ml.dataFrameAnalyticsCreation.assertTrainingPercentInputMissing();
});
it('displays the source data preview', async () => {
await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewExists();
});
it('displays the include fields selection', async () => {
await ml.dataFrameAnalyticsCreation.assertIncludeFieldsSelectionExists();
});
it('continues to the additional options step', async () => {
await ml.dataFrameAnalyticsCreation.continueToAdditionalOptionsStep();
});

View file

@ -66,6 +66,7 @@ export default function ({ getService }: FtrProviderContext) {
it('selects the source data and loads the job wizard page', async () => {
await ml.jobSourceSelection.selectSourceForAnalyticsJob(testData.source);
await ml.dataFrameAnalyticsCreation.assertConfigurationStepActive();
});
it('selects the job type', async () => {
@ -83,6 +84,14 @@ export default function ({ getService }: FtrProviderContext) {
await ml.dataFrameAnalyticsCreation.setTrainingPercent(testData.trainingPercent);
});
it('displays the source data preview', async () => {
await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewExists();
});
it('displays the include fields selection', async () => {
await ml.dataFrameAnalyticsCreation.assertIncludeFieldsSelectionExists();
});
it('continues to the additional options step', async () => {
await ml.dataFrameAnalyticsCreation.continueToAdditionalOptionsStep();
});

View file

@ -124,13 +124,21 @@ export function MachineLearningDataFrameAnalyticsCreationProvider(
await this.assertJobDescriptionValue(jobDescription);
},
// async assertExcludedFieldsSelection(expectedSelection: string[]) {
// const actualSelection = await comboBox.getComboBoxSelectedOptions(
// 'mlAnalyticsCreateJobWizardExcludesSelect'
// );
async assertSourceDataPreviewExists() {
await testSubjects.existOrFail('mlAnalyticsCreationDataGrid loaded', { timeout: 5000 });
},
async assertIncludeFieldsSelectionExists() {
await testSubjects.existOrFail('mlAnalyticsCreateJobWizardIncludesSelect', { timeout: 5000 });
},
// async assertIncludedFieldsSelection(expectedSelection: string[]) {
// const includesTable = await testSubjects.find('mlAnalyticsCreateJobWizardIncludesSelect');
// const actualSelection = await includesTable.findByClassName('euiTableRow-isSelected');
// expect(actualSelection).to.eql(
// expectedSelection,
// `Excluded fields should be '${expectedSelection}' (got '${actualSelection}')`
// `Included fields should be '${expectedSelection}' (got '${actualSelection}')`
// );
// },
@ -252,19 +260,35 @@ export function MachineLearningDataFrameAnalyticsCreationProvider(
await this.assertTrainingPercentValue(trainingPercent);
},
async assertConfigurationStepActive() {
await testSubjects.existOrFail('mlAnalyticsCreateJobWizardConfigurationStep active');
},
async assertAdditionalOptionsStepActive() {
await testSubjects.existOrFail('mlAnalyticsCreateJobWizardAdvancedStep active');
},
async assertDetailsStepActive() {
await testSubjects.existOrFail('mlAnalyticsCreateJobWizardDetailsStep active');
},
async assertCreateStepActive() {
await testSubjects.existOrFail('mlAnalyticsCreateJobWizardCreateStep active');
},
async continueToAdditionalOptionsStep() {
await testSubjects.click('mlAnalyticsCreateJobWizardContinueButton');
await testSubjects.existOrFail('mlAnalyticsCreateJobWizardAdvancedStep');
await testSubjects.clickWhenNotDisabled('mlAnalyticsCreateJobWizardContinueButton');
await this.assertAdditionalOptionsStepActive();
},
async continueToDetailsStep() {
await testSubjects.click('mlAnalyticsCreateJobWizardContinueButton');
await testSubjects.existOrFail('mlAnalyticsCreateJobWizardDetailsStep');
await testSubjects.clickWhenNotDisabled('mlAnalyticsCreateJobWizardContinueButton');
await this.assertDetailsStepActive();
},
async continueToCreateStep() {
await testSubjects.click('mlAnalyticsCreateJobWizardContinueButton');
await testSubjects.existOrFail('mlAnalyticsCreateJobWizardCreateStep');
await testSubjects.clickWhenNotDisabled('mlAnalyticsCreateJobWizardContinueButton');
await this.assertCreateStepActive();
},
async assertModelMemoryInputExists() {
@ -282,6 +306,17 @@ export function MachineLearningDataFrameAnalyticsCreationProvider(
);
},
async assertPredictionFieldNameValue(expectedValue: string) {
const actualPredictedFieldName = await testSubjects.getAttribute(
'mlAnalyticsCreateJobWizardPredictionFieldNameInput',
'value'
);
expect(actualPredictedFieldName).to.eql(
expectedValue,
`Prediction field name should be '${expectedValue}' (got '${actualPredictedFieldName}')`
);
},
async setModelMemory(modelMemory: string) {
await retry.tryForTime(15 * 1000, async () => {
await mlCommon.setValueWithChecks(
@ -372,11 +407,19 @@ export function MachineLearningDataFrameAnalyticsCreationProvider(
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.assertSourceDataPreviewExists();
await this.assertIncludeFieldsSelectionExists();
// await this.assertIncludedFieldsSelection(job.analyzed_fields.includes);
},
async assertInitialCloneJobAdditionalOptionsStep(job: DataFrameAnalyticsConfig) {
await this.assertModelMemoryValue(job.model_memory_limit);
async assertInitialCloneJobAdditionalOptionsStep(
analysis: DataFrameAnalyticsConfig['analysis']
) {
const jobType = Object.keys(analysis)[0];
if (isClassificationAnalysis(analysis) || isRegressionAnalysis(analysis)) {
// @ts-ignore
await this.assertPredictionFieldNameValue(analysis[jobType].prediction_field_name);
}
},
async assertInitialCloneJobDetailsStep(job: DataFrameAnalyticsConfig) {