mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
89dcdbbbee
commit
31abd6dc28
24 changed files with 461 additions and 286 deletions
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}]`)) {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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(', '),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -71,7 +71,7 @@ export const JobType: FC<Props> = ({ type, setFormState }) => {
|
|||
setFormState({
|
||||
previousJobType: type,
|
||||
jobType: value,
|
||||
excludes: [],
|
||||
includes: [],
|
||||
requiredFieldsError: undefined,
|
||||
});
|
||||
}}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -217,11 +217,11 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo
|
|||
analyzed_fields: {
|
||||
excludes: {
|
||||
optional: true,
|
||||
formKey: 'excludes',
|
||||
defaultValue: [],
|
||||
},
|
||||
includes: {
|
||||
optional: true,
|
||||
formKey: 'includes',
|
||||
defaultValue: [],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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": "オプションの説明テキストです",
|
||||
|
|
|
@ -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": "可选的描述文本",
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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']
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue