[ML] DF Analytics: Creation wizard part 1 (#67564) (#68296)

* create newJob route and start of wizard

* wip: create configStep component

* finish configStep form and details

* wip: create andvanced step components

* create details step component

* createStep component

* ensure advanced options are correct for each job type

* add validation to each step

* use custom table for excludes

* move customSelectionTable to shared components

* form validation for advanced fields

* wip: source index selection modal

* add source index preview

* update details

* ensure advanced parameters added to config on creation

* can create job from savedSearch. can set source query in ui

* validate source object has supported fields

* eslint updates

* update tests. comment out clone action for now

* add create button to advanced editor

* remove deprecated test helper functions

* fix translation errors

* update help text. read only once job created.

* fix functional tests

* add nextStepNav to df service for tests

* fix excludes table page jump and hyperParameter not showing in details

* fix checkbox width for custom table
This commit is contained in:
Melissa Alvarez 2020-06-04 17:26:45 -04:00 committed by GitHub
parent 2632b6c752
commit d6028c1f14
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 3605 additions and 323 deletions

View file

@ -29,7 +29,7 @@ import {
import { Pager } from '@elastic/eui/lib/services';
import { i18n } from '@kbn/i18n';
const JOBS_PER_PAGE = 20;
const ITEMS_PER_PAGE = 20;
function getError(error) {
if (error !== null) {
@ -43,15 +43,18 @@ function getError(error) {
}
export function CustomSelectionTable({
checkboxDisabledCheck,
columns,
filterDefaultFields,
filters,
items,
itemsPerPage = ITEMS_PER_PAGE,
onTableChange,
radioDisabledCheck,
selectedIds,
singleSelection,
sortableProperties,
timeseriesOnly,
tableItemId = 'id',
}) {
const [itemIdToSelectedMap, setItemIdToSelectedMap] = useState(getCurrentlySelectedItemIdsMap());
const [currentItems, setCurrentItems] = useState(items);
@ -59,7 +62,7 @@ export function CustomSelectionTable({
const [sortedColumn, setSortedColumn] = useState('');
const [pager, setPager] = useState();
const [pagerSettings, setPagerSettings] = useState({
itemsPerPage: JOBS_PER_PAGE,
itemsPerPage: itemsPerPage,
firstItemIndex: 0,
lastItemIndex: 1,
});
@ -77,9 +80,9 @@ export function CustomSelectionTable({
}, [selectedIds]); // eslint-disable-line
useEffect(() => {
const tablePager = new Pager(currentItems.length, JOBS_PER_PAGE);
const tablePager = new Pager(currentItems.length, itemsPerPage);
setPagerSettings({
itemsPerPage: JOBS_PER_PAGE,
itemsPerPage: itemsPerPage,
firstItemIndex: tablePager.getFirstItemIndex(),
lastItemIndex: tablePager.getLastItemIndex(),
});
@ -100,7 +103,7 @@ export function CustomSelectionTable({
function handleTableChange({ isSelected, itemId }) {
const selectedMapIds = Object.getOwnPropertyNames(itemIdToSelectedMap);
const currentItemIds = currentItems.map((item) => item.id);
const currentItemIds = currentItems.map((item) => item[tableItemId]);
let currentSelected = selectedMapIds.filter(
(id) => itemIdToSelectedMap[id] === true && id !== itemId
@ -124,11 +127,11 @@ export function CustomSelectionTable({
onTableChange(currentSelected);
}
function handleChangeItemsPerPage(itemsPerPage) {
pager.setItemsPerPage(itemsPerPage);
function handleChangeItemsPerPage(numItemsPerPage) {
pager.setItemsPerPage(numItemsPerPage);
setPagerSettings({
...pagerSettings,
itemsPerPage,
itemsPerPage: numItemsPerPage,
firstItemIndex: pager.getFirstItemIndex(),
lastItemIndex: pager.getLastItemIndex(),
});
@ -161,7 +164,9 @@ export function CustomSelectionTable({
}
function areAllItemsSelected() {
const indexOfUnselectedItem = currentItems.findIndex((item) => !isItemSelected(item.id));
const indexOfUnselectedItem = currentItems.findIndex(
(item) => !isItemSelected(item[tableItemId])
);
return indexOfUnselectedItem === -1;
}
@ -199,7 +204,7 @@ export function CustomSelectionTable({
function toggleAll() {
const allSelected = areAllItemsSelected() || itemIdToSelectedMap.all === true;
const newItemIdToSelectedMap = {};
currentItems.forEach((item) => (newItemIdToSelectedMap[item.id] = !allSelected));
currentItems.forEach((item) => (newItemIdToSelectedMap[item[tableItemId]] = !allSelected));
setItemIdToSelectedMap(newItemIdToSelectedMap);
handleTableChange({ isSelected: !allSelected, itemId: 'all' });
}
@ -255,20 +260,23 @@ export function CustomSelectionTable({
<EuiTableRowCellCheckbox key={column.id}>
{!singleSelection && (
<EuiCheckbox
id={`${item.id}-checkbox`}
data-test-subj={`${item.id}-checkbox`}
checked={isItemSelected(item.id)}
onChange={() => toggleItem(item.id)}
disabled={
checkboxDisabledCheck !== undefined ? checkboxDisabledCheck(item) : undefined
}
id={`${item[tableItemId]}-checkbox`}
data-test-subj={`${item[tableItemId]}-checkbox`}
checked={isItemSelected(item[tableItemId])}
onChange={() => toggleItem(item[tableItemId])}
type="inList"
/>
)}
{singleSelection && (
<EuiRadio
id={item.id}
data-test-subj={`${item.id}-radio-button`}
checked={isItemSelected(item.id)}
onChange={() => toggleItem(item.id)}
disabled={timeseriesOnly && item.isSingleMetricViewerJob === false}
id={item[tableItemId]}
data-test-subj={`${item[tableItemId]}-radio-button`}
checked={isItemSelected(item[tableItemId])}
onChange={() => toggleItem(item[tableItemId])}
disabled={radioDisabledCheck !== undefined ? radioDisabledCheck(item) : undefined}
/>
)}
</EuiTableRowCellCheckbox>
@ -299,11 +307,11 @@ export function CustomSelectionTable({
return (
<EuiTableRow
key={item.id}
isSelected={isItemSelected(item.id)}
key={item[tableItemId]}
isSelected={isItemSelected(item[tableItemId])}
isSelectable={true}
hasActions={true}
data-test-subj="mlFlyoutJobSelectorTableRow"
data-test-subj="mlCustomSelectionTableRow"
>
{cells}
</EuiTableRow>
@ -331,7 +339,7 @@ export function CustomSelectionTable({
<Fragment>
<EuiSpacer size="s" />
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false} data-test-subj="mlFlyoutJobSelectorSearchBar">
<EuiFlexItem grow={false} data-test-subj="mlCustomSelectionTableSearchBar">
<EuiSearchBar
defaultQuery={query}
box={{
@ -359,7 +367,7 @@ export function CustomSelectionTable({
<EuiFlexItem grow={false}>{renderSelectAll(true)}</EuiFlexItem>
</EuiFlexGroup>
</EuiTableHeaderMobile>
<EuiTable data-test-subj="mlFlyoutJobSelectorTable">
<EuiTable data-test-subj="mlCustomSelectionTable">
<EuiTableHeader>{renderHeaderCells()}</EuiTableHeader>
<EuiTableBody>{renderRows()}</EuiTableBody>
</EuiTable>
@ -368,7 +376,7 @@ export function CustomSelectionTable({
<EuiTablePagination
activePage={pager.getCurrentPageIndex()}
itemsPerPage={pagerSettings.itemsPerPage}
itemsPerPageOptions={[10, JOBS_PER_PAGE, 50]}
itemsPerPageOptions={[5, 10, 20, 50]}
pageCount={pager.getTotalPages()}
onChangeItemsPerPage={handleChangeItemsPerPage}
onChangePage={(pageIndex) => handlePageChange(pageIndex)}
@ -379,13 +387,16 @@ export function CustomSelectionTable({
}
CustomSelectionTable.propTypes = {
checkboxDisabledCheck: PropTypes.func,
columns: PropTypes.array.isRequired,
filterDefaultFields: PropTypes.array,
filters: PropTypes.array,
items: PropTypes.array.isRequired,
itemsPerPage: PropTypes.number,
onTableChange: PropTypes.func.isRequired,
radioDisabledCheck: PropTypes.func,
selectedId: PropTypes.array,
singleSelection: PropTypes.bool,
sortableProperties: PropTypes.object,
timeseriesOnly: PropTypes.bool,
tableItemId: PropTypes.string,
};

View file

@ -6,7 +6,7 @@
import React, { Fragment, useState, useEffect } from 'react';
import { PropTypes } from 'prop-types';
import { CustomSelectionTable } from '../custom_selection_table';
import { CustomSelectionTable } from '../../custom_selection_table';
import { JobSelectorBadge } from '../job_selector_badge';
import { TimeRangeBar } from '../timerange_bar';
@ -107,7 +107,7 @@ export function JobSelectorTable({
id: 'checkbox',
isCheckbox: true,
textOnly: false,
width: '24px',
width: '32px',
},
{
label: 'job ID',
@ -157,6 +157,9 @@ export function JobSelectorTable({
filterDefaultFields={!singleSelection ? JOB_FILTER_FIELDS : undefined}
items={jobs}
onTableChange={(selectionFromTable) => onSelection({ selectionFromTable })}
radioDisabledCheck={(item) => {
return timeseriesOnly && item.isSingleMetricViewerJob === false;
}}
selectedIds={selectedIds}
singleSelection={singleSelection}
sortableProperties={sortableProperties}

View file

@ -2,3 +2,4 @@
@import 'pages/analytics_management/components/analytics_list/index';
@import 'pages/analytics_management/components/create_analytics_form/index';
@import 'pages/analytics_management/components/create_analytics_flyout/index';
@import 'pages/analytics_management/components/create_analytics_button/index';

View file

@ -24,6 +24,21 @@ export enum ANALYSIS_CONFIG_TYPE {
CLASSIFICATION = 'classification',
}
export enum ANALYSIS_ADVANCED_FIELDS {
FEATURE_INFLUENCE_THRESHOLD = 'feature_influence_threshold',
GAMMA = 'gamma',
LAMBDA = 'lambda',
MAX_TREES = 'max_trees',
NUM_TOP_FEATURE_IMPORTANCE_VALUES = 'num_top_feature_importance_values',
}
export enum OUTLIER_ANALYSIS_METHOD {
LOF = 'lof',
LDOF = 'ldof',
DISTANCE_KTH_NN = 'distance_kth_nn',
DISTANCE_KNN = 'distance_knn',
}
interface OutlierAnalysis {
[key: string]: {};
outlier_detection: {};
@ -263,11 +278,13 @@ export const isClassificationAnalysis = (arg: any): arg is ClassificationAnalysi
};
export const isResultsSearchBoolQuery = (arg: any): arg is ResultsSearchBoolQuery => {
if (arg === undefined) return false;
const keys = Object.keys(arg);
return keys.length === 1 && keys[0] === 'bool';
};
export const isQueryStringQuery = (arg: any): arg is QueryStringQuery => {
if (arg === undefined) return false;
const keys = Object.keys(arg);
return keys.length === 1 && keys[0] === 'query_string';
};

View file

@ -17,6 +17,7 @@ export {
IndexPattern,
REFRESH_ANALYTICS_LIST_STATE,
ANALYSIS_CONFIG_TYPE,
OUTLIER_ANALYSIS_METHOD,
RegressionEvaluateResponse,
getValuesFromResponse,
loadEvalData,

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC } from 'react';
import { EuiForm } from '@elastic/eui';
import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form';
import { AdvancedStepForm } from './advanced_step_form';
import { AdvancedStepDetails } from './advanced_step_details';
import { ANALYTICS_STEPS } from '../../page';
export const AdvancedStep: FC<CreateAnalyticsStepProps> = ({
actions,
state,
step,
setCurrentStep,
stepActivated,
}) => {
return (
<EuiForm>
{step === ANALYTICS_STEPS.ADVANCED && (
<AdvancedStepForm actions={actions} state={state} setCurrentStep={setCurrentStep} />
)}
{step !== ANALYTICS_STEPS.ADVANCED && stepActivated === true && (
<AdvancedStepDetails setCurrentStep={setCurrentStep} state={state} />
)}
</EuiForm>
);
};

View file

@ -0,0 +1,274 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButtonEmpty,
EuiDescriptionList,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import {
UNSET_CONFIG_ITEM,
State,
} from '../../../analytics_management/hooks/use_create_analytics_form/state';
import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics';
import { ANALYTICS_STEPS } from '../../page';
function getStringValue(value: number | undefined) {
return value !== undefined ? `${value}` : UNSET_CONFIG_ITEM;
}
export interface ListItems {
title: string;
description: string | JSX.Element;
}
export const AdvancedStepDetails: FC<{ setCurrentStep: any; state: State }> = ({
setCurrentStep,
state,
}) => {
const { form, isJobCreated } = state;
const {
computeFeatureInfluence,
dependentVariable,
eta,
featureBagFraction,
featureInfluenceThreshold,
gamma,
jobType,
lambda,
method,
maxTrees,
modelMemoryLimit,
nNeighbors,
numTopClasses,
numTopFeatureImportanceValues,
outlierFraction,
predictionFieldName,
randomizeSeed,
standardizationEnabled,
} = form;
const isRegOrClassJob =
jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION;
const advancedFirstCol: ListItems[] = [];
const advancedSecondCol: ListItems[] = [];
const advancedThirdCol: ListItems[] = [];
const hyperFirstCol: ListItems[] = [];
const hyperSecondCol: ListItems[] = [];
const hyperThirdCol: ListItems[] = [];
if (jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION) {
advancedFirstCol.push({
title: i18n.translate(
'xpack.ml.dataframe.analytics.create.configDetails.computeFeatureInfluence',
{
defaultMessage: 'Compute feature influence',
}
),
description: computeFeatureInfluence,
});
advancedSecondCol.push({
title: i18n.translate(
'xpack.ml.dataframe.analytics.create.configDetails.featureInfluenceThreshold',
{
defaultMessage: 'Feature influence threshold',
}
),
description: getStringValue(featureInfluenceThreshold),
});
advancedThirdCol.push({
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.modelMemoryLimit', {
defaultMessage: 'Model memory limit',
}),
description: `${modelMemoryLimit}`,
});
hyperFirstCol.push(
{
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.nNeighbors', {
defaultMessage: 'N neighbors',
}),
description: getStringValue(nNeighbors),
},
{
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.outlierFraction', {
defaultMessage: 'Outlier fraction',
}),
description: getStringValue(outlierFraction),
}
);
hyperSecondCol.push({
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.method', {
defaultMessage: 'Method',
}),
description: method !== undefined ? method : UNSET_CONFIG_ITEM,
});
hyperThirdCol.push({
title: i18n.translate(
'xpack.ml.dataframe.analytics.create.configDetails.standardizationEnabled',
{
defaultMessage: 'Standardization enabled',
}
),
description: `${standardizationEnabled}`,
});
}
if (isRegOrClassJob) {
if (jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION) {
advancedFirstCol.push({
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.numTopClasses', {
defaultMessage: 'Top classes',
}),
description: `${numTopClasses}`,
});
}
advancedFirstCol.push({
title: i18n.translate(
'xpack.ml.dataframe.analytics.create.configDetails.numTopFeatureImportanceValues',
{
defaultMessage: 'Top feature importance values',
}
),
description: `${numTopFeatureImportanceValues}`,
});
hyperFirstCol.push(
{
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.lambdaFields', {
defaultMessage: 'Lambda',
}),
description: getStringValue(lambda),
},
{
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.eta', {
defaultMessage: 'Eta',
}),
description: getStringValue(eta),
}
);
advancedSecondCol.push({
title: i18n.translate(
'xpack.ml.dataframe.analytics.create.configDetails.predictionFieldName',
{
defaultMessage: 'Prediction field name',
}
),
description: predictionFieldName ? predictionFieldName : `${dependentVariable}_prediction`,
});
hyperSecondCol.push(
{
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.maxTreesFields', {
defaultMessage: 'Max trees',
}),
description: getStringValue(maxTrees),
},
{
title: i18n.translate(
'xpack.ml.dataframe.analytics.create.configDetails.featureBagFraction',
{
defaultMessage: 'Feature bag fraction',
}
),
description: getStringValue(featureBagFraction),
}
);
advancedThirdCol.push({
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.modelMemoryLimit', {
defaultMessage: 'Model memory limit',
}),
description: `${modelMemoryLimit}`,
});
hyperThirdCol.push(
{
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.gamma', {
defaultMessage: 'Gamma',
}),
description: getStringValue(gamma),
},
{
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.randomizedSeed', {
defaultMessage: 'Randomized seed',
}),
description: getStringValue(randomizeSeed),
}
);
}
return (
<Fragment>
<EuiTitle size="xs">
<h3>
{i18n.translate('xpack.ml.dataframe.analytics.create.advancedConfigDetailsTitle', {
defaultMessage: 'Advanced configuration',
})}
</h3>
</EuiTitle>
<EuiSpacer />
<EuiFlexGroup style={{ width: '70%' }} justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiDescriptionList compressed listItems={advancedFirstCol} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiDescriptionList compressed listItems={advancedSecondCol} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiDescriptionList compressed listItems={advancedThirdCol} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiTitle size="xs">
<h3>
{i18n.translate('xpack.ml.dataframe.analytics.create.hyperParametersDetailsTitle', {
defaultMessage: 'Hyper parameters',
})}
</h3>
</EuiTitle>
<EuiSpacer />
<EuiFlexGroup style={{ width: '70%' }} justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiDescriptionList compressed listItems={hyperFirstCol} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiDescriptionList compressed listItems={hyperSecondCol} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiDescriptionList compressed listItems={hyperThirdCol} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
{!isJobCreated && (
<EuiButtonEmpty
iconType="pencil"
size="s"
onClick={() => {
setCurrentStep(ANALYTICS_STEPS.ADVANCED);
}}
>
{i18n.translate('xpack.ml.dataframe.analytics.create.advancedDetails.editButtonText', {
defaultMessage: 'Edit',
})}
</EuiButtonEmpty>
)}
</Fragment>
);
};

View file

@ -0,0 +1,332 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, Fragment, useMemo } from 'react';
import {
EuiAccordion,
EuiFieldNumber,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiSelect,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { HyperParameters } from './hyper_parameters';
import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form';
import { getModelMemoryLimitErrors } from '../../../analytics_management/hooks/use_create_analytics_form/reducer';
import {
ANALYSIS_CONFIG_TYPE,
NUM_TOP_FEATURE_IMPORTANCE_VALUES_MIN,
} from '../../../../common/analytics';
import { DEFAULT_MODEL_MEMORY_LIMIT } from '../../../analytics_management/hooks/use_create_analytics_form/state';
import { ANALYTICS_STEPS } from '../../page';
import { ContinueButton } from '../continue_button';
import { OutlierHyperParameters } from './outlier_hyper_parameters';
export function getNumberValue(value?: number) {
return value === undefined ? '' : +value;
}
export const AdvancedStepForm: FC<CreateAnalyticsStepProps> = ({
actions,
state,
setCurrentStep,
}) => {
const { setFormState } = actions;
const { form, isJobCreated } = state;
const {
computeFeatureInfluence,
featureInfluenceThreshold,
jobType,
modelMemoryLimit,
modelMemoryLimitValidationResult,
numTopClasses,
numTopFeatureImportanceValues,
numTopFeatureImportanceValuesValid,
predictionFieldName,
} = form;
const mmlErrors = useMemo(() => getModelMemoryLimitErrors(modelMemoryLimitValidationResult), [
modelMemoryLimitValidationResult,
]);
const isRegOrClassJob =
jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION;
const mmlInvalid = modelMemoryLimitValidationResult !== null;
const outlierDetectionAdvancedConfig = (
<Fragment>
<EuiFlexItem style={{ minWidth: '30%' }}>
<EuiFormRow
label={i18n.translate(
'xpack.ml.dataframe.analytics.create.computeFeatureInfluenceLabel',
{
defaultMessage: 'Compute feature influence',
}
)}
helpText={i18n.translate(
'xpack.ml.dataframe.analytics.create.computeFeatureInfluenceLabelHelpText',
{
defaultMessage:
'Specifies whether the feature influence calculation is enabled. Defaults to true.',
}
)}
>
<EuiSelect
data-test-subj="mlAnalyticsCreateJobWizardComputeFeatureInfluenceLabelInput"
options={[
{
value: 'true',
text: i18n.translate(
'xpack.ml.dataframe.analytics.create.computeFeatureInfluenceTrueValue',
{
defaultMessage: 'True',
}
),
},
{
value: 'false',
text: i18n.translate(
'xpack.ml.dataframe.analytics.create.computeFeatureInfluenceFalseValue',
{
defaultMessage: 'False',
}
),
},
]}
value={computeFeatureInfluence}
hasNoInitialSelection={false}
onChange={(e) => {
setFormState({
computeFeatureInfluence: e.target.value,
});
}}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem style={{ minWidth: '30%' }}>
<EuiFormRow
label={i18n.translate(
'xpack.ml.dataframe.analytics.create.featureInfluenceThresholdLabel',
{
defaultMessage: 'Feature influence threshold',
}
)}
helpText={i18n.translate(
'xpack.ml.dataframe.analytics.create.featureInfluenceThresholdHelpText',
{
defaultMessage:
'The minimum outlier score that a document needs to have in order to calculate its feature influence score. Value range: 0-1. Defaults to 0.1.',
}
)}
>
<EuiFieldNumber
onChange={(e) =>
setFormState({
featureInfluenceThreshold: e.target.value === '' ? undefined : +e.target.value,
})
}
data-test-subj="mlAnalyticsCreateJobWizardFeatureInfluenceThresholdInput"
min={0}
max={1}
step={0.001}
value={getNumberValue(featureInfluenceThreshold)}
/>
</EuiFormRow>
</EuiFlexItem>
</Fragment>
);
const regAndClassAdvancedConfig = (
<Fragment>
<EuiFlexItem style={{ minWidth: '30%' }}>
<EuiFormRow
label={i18n.translate(
'xpack.ml.dataframe.analytics.create.numTopFeatureImportanceValuesLabel',
{
defaultMessage: 'Feature importance values',
}
)}
helpText={i18n.translate(
'xpack.ml.dataframe.analytics.create.numTopFeatureImportanceValuesHelpText',
{
defaultMessage:
'Specify the maximum number of feature importance values per document to return.',
}
)}
isInvalid={numTopFeatureImportanceValuesValid === false}
error={[
...(numTopFeatureImportanceValuesValid === false
? [
<Fragment>
{i18n.translate(
'xpack.ml.dataframe.analytics.create.numTopFeatureImportanceValuesErrorText',
{
defaultMessage: 'Invalid maximum number of feature importance values.',
}
)}
</Fragment>,
]
: []),
]}
>
<EuiFieldNumber
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.create.numTopFeatureImportanceValuesInputAriaLabel',
{
defaultMessage: 'Maximum number of feature importance values per document.',
}
)}
data-test-subj="mlAnalyticsCreateJobFlyoutnumTopFeatureImportanceValuesInput"
isInvalid={numTopFeatureImportanceValuesValid === false}
min={NUM_TOP_FEATURE_IMPORTANCE_VALUES_MIN}
onChange={(e) =>
setFormState({
numTopFeatureImportanceValues: e.target.value === '' ? undefined : +e.target.value,
})
}
step={1}
value={getNumberValue(numTopFeatureImportanceValues)}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem style={{ minWidth: '30%' }}>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.predictionFieldNameLabel', {
defaultMessage: 'Prediction field name',
})}
helpText={i18n.translate(
'xpack.ml.dataframe.analytics.create.predictionFieldNameHelpText',
{
defaultMessage:
'Defines the name of the prediction field in the results. Defaults to <dependent_variable>_prediction.',
}
)}
>
<EuiFieldText
disabled={isJobCreated}
value={predictionFieldName}
onChange={(e) => setFormState({ predictionFieldName: e.target.value })}
data-test-subj="mlAnalyticsCreateJobWizardPredictionFieldNameInput"
/>
</EuiFormRow>
</EuiFlexItem>
</Fragment>
);
return (
<Fragment>
<EuiTitle size="xs">
<h3>
{i18n.translate('xpack.ml.dataframe.analytics.create.advancedConfigSectionTitle', {
defaultMessage: 'Advanced configuration',
})}
</h3>
</EuiTitle>
<EuiFlexGroup wrap>
{jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION && outlierDetectionAdvancedConfig}
{isRegOrClassJob && regAndClassAdvancedConfig}
{jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && (
<EuiFlexItem style={{ minWidth: '30%' }}>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.numTopClassesLabel', {
defaultMessage: 'Top classes',
})}
helpText={i18n.translate(
'xpack.ml.dataframe.analytics.create.numTopClassesHelpText',
{
defaultMessage:
'The number of categories for which the predicted probabilities are reported.',
}
)}
>
<EuiFieldNumber
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.create.numTopClassesInputAriaLabel',
{
defaultMessage:
'The number of categories for which the predicted probabilities are reported',
}
)}
data-test-subj="mlAnalyticsCreateJobWizardnumTopClassesInput"
min={0}
onChange={(e) =>
setFormState({
numTopClasses: e.target.value === '' ? undefined : +e.target.value,
})
}
step={1}
value={getNumberValue(numTopClasses)}
/>
</EuiFormRow>
</EuiFlexItem>
)}
<EuiFlexItem style={{ width: '30%', minWidth: '30%' }}>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.modelMemoryLimitLabel', {
defaultMessage: 'Model memory limit',
})}
isInvalid={mmlInvalid}
error={mmlErrors}
helpText={i18n.translate(
'xpack.ml.dataframe.analytics.create.modelMemoryLimitHelpText',
{
defaultMessage:
'The approximate maximum amount of memory resources that are permitted for analytical processing.',
}
)}
>
<EuiFieldText
placeholder={
jobType !== undefined
? DEFAULT_MODEL_MEMORY_LIMIT[jobType]
: DEFAULT_MODEL_MEMORY_LIMIT.outlier_detection
}
disabled={isJobCreated}
value={modelMemoryLimit || ''}
onChange={(e) => setFormState({ modelMemoryLimit: e.target.value })}
isInvalid={mmlInvalid}
data-test-subj="mlAnalyticsCreateJobWizardModelMemoryInput"
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiAccordion
id="hyper-parameters"
buttonContent={
<EuiTitle size="xs">
<h3>
{i18n.translate('xpack.ml.dataframe.analytics.create.hyperParametersSectionTitle', {
defaultMessage: 'Hyper parameters',
})}
</h3>
</EuiTitle>
}
initialIsOpen={false}
data-test-subj="mlAnalyticsCreateJobWizardHyperParametersSection"
>
<EuiFlexGroup wrap>
{jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION && (
<OutlierHyperParameters actions={actions} state={state} />
)}
{isRegOrClassJob && <HyperParameters actions={actions} state={state} />}
</EuiFlexGroup>
</EuiAccordion>
<EuiSpacer />
<ContinueButton
isDisabled={mmlInvalid}
onClick={() => {
setCurrentStep(ANALYTICS_STEPS.DETAILS);
}}
/>
</Fragment>
);
};

View file

@ -0,0 +1,188 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, Fragment } from 'react';
import { EuiFieldNumber, EuiFlexItem, EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/use_create_analytics_form';
import { getNumberValue } from './advanced_step_form';
const MAX_TREES_LIMIT = 2000;
export const HyperParameters: FC<CreateAnalyticsFormProps> = ({ actions, state }) => {
const { setFormState } = actions;
const { eta, featureBagFraction, gamma, lambda, maxTrees, randomizeSeed } = state.form;
return (
<Fragment>
<EuiFlexItem style={{ minWidth: '30%' }}>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.lambdaLabel', {
defaultMessage: 'Lambda',
})}
helpText={i18n.translate('xpack.ml.dataframe.analytics.create.lambdaHelpText', {
defaultMessage:
'Regularization parameter to prevent overfitting on the training data set. Must be a non negative value.',
})}
>
<EuiFieldNumber
aria-label={i18n.translate('xpack.ml.dataframe.analytics.create.lambdaInputAriaLabel', {
defaultMessage:
'Regularization parameter to prevent overfitting on the training data set.',
})}
data-test-subj="mlAnalyticsCreateJobFlyoutLambdaInput"
onChange={(e) =>
setFormState({ lambda: e.target.value === '' ? undefined : +e.target.value })
}
step={0.001}
min={0}
value={getNumberValue(lambda)}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem style={{ minWidth: '30%' }}>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.maxTreesLabel', {
defaultMessage: 'Max trees',
})}
helpText={i18n.translate('xpack.ml.dataframe.analytics.create.maxTreesText', {
defaultMessage: 'The maximum number of trees the forest is allowed to contain.',
})}
>
<EuiFieldNumber
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.create.maxTreesInputAriaLabel',
{
defaultMessage: 'The maximum number of trees the forest is allowed to contain.',
}
)}
data-test-subj="mlAnalyticsCreateJobFlyoutMaxTreesInput"
onChange={(e) =>
setFormState({ maxTrees: e.target.value === '' ? undefined : +e.target.value })
}
isInvalid={maxTrees !== undefined && !Number.isInteger(maxTrees)}
step={1}
min={1}
max={MAX_TREES_LIMIT}
value={getNumberValue(maxTrees)}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem style={{ minWidth: '30%' }}>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.gammaLabel', {
defaultMessage: 'Gamma',
})}
helpText={i18n.translate('xpack.ml.dataframe.analytics.create.gammaText', {
defaultMessage:
'Multiplies a linear penalty associated with the size of individual trees in the forest. Must be non-negative value.',
})}
>
<EuiFieldNumber
aria-label={i18n.translate('xpack.ml.dataframe.analytics.create.gammaInputAriaLabel', {
defaultMessage:
'Multiplies a linear penalty associated with the size of individual trees in the forest',
})}
data-test-subj="mlAnalyticsCreateJobWizardGammaInput"
onChange={(e) =>
setFormState({ gamma: e.target.value === '' ? undefined : +e.target.value })
}
step={0.001}
min={0}
value={getNumberValue(gamma)}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem style={{ minWidth: '30%' }}>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.etaLabel', {
defaultMessage: 'Eta',
})}
helpText={i18n.translate('xpack.ml.dataframe.analytics.create.etaText', {
defaultMessage: 'The shrinkage applied to the weights. Must be between 0.001 and 1.',
})}
>
<EuiFieldNumber
aria-label={i18n.translate('xpack.ml.dataframe.analytics.create.etaInputAriaLabel', {
defaultMessage: 'The shrinkage applied to the weights',
})}
data-test-subj="mlAnalyticsCreateJobWizardEtaInput"
onChange={(e) =>
setFormState({ eta: e.target.value === '' ? undefined : +e.target.value })
}
step={0.001}
min={0.001}
max={1}
value={getNumberValue(eta)}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem style={{ minWidth: '30%' }}>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.featureBagFractionLabel', {
defaultMessage: 'Feature bag fraction',
})}
helpText={i18n.translate('xpack.ml.dataframe.analytics.create.featureBagFractionText', {
defaultMessage:
'The fraction of features used when selecting a random bag for each candidate split.',
})}
>
<EuiFieldNumber
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.create.featureBagFractionInputAriaLabel',
{
defaultMessage:
'The fraction of features used when selecting a random bag for each candidate split',
}
)}
data-test-subj="mlAnalyticsCreateJobWizardFeatureBagFractionInput"
onChange={(e) =>
setFormState({
featureBagFraction: e.target.value === '' ? undefined : +e.target.value,
})
}
isInvalid={
featureBagFraction !== undefined &&
(featureBagFraction > 1 || featureBagFraction <= 0)
}
step={0.001}
max={1}
value={getNumberValue(featureBagFraction)}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem style={{ minWidth: '30%' }}>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.randomizeSeedLabel', {
defaultMessage: 'Randomized seed',
})}
helpText={i18n.translate('xpack.ml.dataframe.analytics.create.randomizeSeedText', {
defaultMessage:
'The seed to the random generator that is used to pick which documents will be used for training.',
})}
>
<EuiFieldNumber
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.create.randomizeSeedInputAriaLabel',
{
defaultMessage:
'The seed to the random generator that is used to pick which documents will be used for training',
}
)}
data-test-subj="mlAnalyticsCreateJobWizardRandomizeSeedInput"
onChange={(e) =>
setFormState({ randomizeSeed: e.target.value === '' ? undefined : +e.target.value })
}
isInvalid={randomizeSeed !== undefined && typeof randomizeSeed !== 'number'}
value={getNumberValue(randomizeSeed)}
step={1}
/>
</EuiFormRow>
</EuiFlexItem>
</Fragment>
);
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { AdvancedStep } from './advanced_step';

View file

@ -0,0 +1,153 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, Fragment } from 'react';
import { EuiFieldNumber, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { OUTLIER_ANALYSIS_METHOD } from '../../../../common/analytics';
import { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/use_create_analytics_form';
import { getNumberValue } from './advanced_step_form';
export const OutlierHyperParameters: FC<CreateAnalyticsFormProps> = ({ actions, state }) => {
const { setFormState } = actions;
const { method, nNeighbors, outlierFraction, standardizationEnabled } = state.form;
return (
<Fragment>
<EuiFlexItem style={{ minWidth: '30%' }}>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.methodLabel', {
defaultMessage: 'Method',
})}
helpText={i18n.translate('xpack.ml.dataframe.analytics.create.methodHelpText', {
defaultMessage:
'Sets the method that outlier detection uses. If not set, uses an ensemble of different methods and normalises and combines their individual outlier scores to obtain the overall outlier score. We recommend to use the ensemble method',
})}
>
<EuiSelect
options={Object.values(OUTLIER_ANALYSIS_METHOD).map((outlierMethod) => ({
value: outlierMethod,
text: outlierMethod,
}))}
value={method}
hasNoInitialSelection={true}
onChange={(e) => {
setFormState({ method: e.target.value });
}}
data-test-subj="mlAnalyticsCreateJobWizardMethodInput"
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem style={{ minWidth: '30%' }}>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.nNeighborsLabel', {
defaultMessage: 'N neighbors',
})}
helpText={i18n.translate('xpack.ml.dataframe.analytics.create.nNeighborsHelpText', {
defaultMessage:
'The value for how many nearest neighbors each method of outlier detection will use to calculate its outlier score. When not set, different values will be used for different ensemble members. Must be a positive integer',
})}
>
<EuiFieldNumber
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.create.nNeighborsInputAriaLabel',
{
defaultMessage:
'The value for how many nearest neighbors each method of outlier detection will use to calculate its outlier score.',
}
)}
data-test-subj="mlAnalyticsCreateJobWizardnNeighborsInput"
onChange={(e) =>
setFormState({ nNeighbors: e.target.value === '' ? undefined : +e.target.value })
}
step={1}
min={1}
value={getNumberValue(nNeighbors)}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem style={{ minWidth: '30%' }}>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.outlierFractionLabel', {
defaultMessage: 'Outlier fraction',
})}
helpText={i18n.translate('xpack.ml.dataframe.analytics.create.outlierFractionHelpText', {
defaultMessage:
'Sets the proportion of the data set that is assumed to be outlying prior to outlier detection.',
})}
>
<EuiFieldNumber
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.create.outlierFractionInputAriaLabel',
{
defaultMessage:
'Sets the proportion of the data set that is assumed to be outlying prior to outlier detection.',
}
)}
data-test-subj="mlAnalyticsCreateJobWizardoutlierFractionInput"
onChange={(e) =>
setFormState({ outlierFraction: e.target.value === '' ? undefined : +e.target.value })
}
step={0.001}
min={0}
max={1}
value={getNumberValue(outlierFraction)}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem style={{ minWidth: '30%' }}>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.standardizationEnabledLabel', {
defaultMessage: 'Standardization enabled',
})}
helpText={i18n.translate(
'xpack.ml.dataframe.analytics.create.standardizationEnabledHelpText',
{
defaultMessage:
'If true, the following operation is performed on the columns before computing outlier scores: (x_i - mean(x_i)) / sd(x_i).',
}
)}
>
<EuiSelect
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.create.standardizationEnabledInputAriaLabel',
{
defaultMessage: 'Sets standardization enabled setting.',
}
)}
data-test-subj="mlAnalyticsCreateJobWizardStandardizationEnabledInput"
options={[
{
value: 'true',
text: i18n.translate(
'xpack.ml.dataframe.analytics.create.standardizationEnabledTrueValue',
{
defaultMessage: 'True',
}
),
},
{
value: 'false',
text: i18n.translate(
'xpack.ml.dataframe.analytics.create.standardizationEnabledFalseValue',
{
defaultMessage: 'False',
}
),
},
]}
value={standardizationEnabled}
hasNoInitialSelection={true}
onChange={(e) => {
setFormState({ standardizationEnabled: e.target.value });
}}
/>
</EuiFormRow>
</EuiFlexItem>
</Fragment>
);
};

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, Fragment } from 'react';
import { EuiCard, EuiHorizontalRule, EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
function redirectToAnalyticsManagementPage() {
window.location.href = '#/data_frame_analytics?';
}
export const BackToListPanel: FC = () => (
<Fragment>
<EuiHorizontalRule />
<EuiCard
// @ts-ignore
style={{ width: '300px' }}
icon={<EuiIcon size="xxl" type="list" />}
title={i18n.translate('xpack.ml.dataframe.analytics.create.analyticsListCardTitle', {
defaultMessage: 'Data Frame Analytics',
})}
description={i18n.translate(
'xpack.ml.dataframe.analytics.create.analyticsListCardDescription',
{
defaultMessage: 'Return to the analytics management page.',
}
)}
onClick={redirectToAnalyticsManagementPage}
data-test-subj="analyticsWizardCardManagement"
/>
</Fragment>
);

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { BackToListPanel } from './back_to_list_panel';

View file

@ -0,0 +1,207 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, Fragment, memo, 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';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { FieldSelectionItem } from '../../../../common/analytics';
// @ts-ignore could not find declaration file
import { CustomSelectionTable } from '../../../../../components/custom_selection_table';
const columns = [
{
id: 'checkbox',
isCheckbox: true,
textOnly: false,
width: '32px',
},
{
label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.fieldNameColumn', {
defaultMessage: 'Field name',
}),
id: 'name',
isSortable: true,
alignment: LEFT_ALIGNMENT,
},
{
id: 'mapping_types',
label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.mappingColumn', {
defaultMessage: 'Mapping',
}),
isSortable: false,
alignment: LEFT_ALIGNMENT,
},
{
label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.isIncludedColumn', {
defaultMessage: 'Is included',
}),
id: 'is_included',
alignment: LEFT_ALIGNMENT,
isSortable: true,
// eslint-disable-next-line @typescript-eslint/camelcase
render: ({ is_included }: { is_included: boolean }) => (is_included ? 'Yes' : 'No'),
},
{
label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.isRequiredColumn', {
defaultMessage: 'Is required',
}),
id: 'is_required',
alignment: LEFT_ALIGNMENT,
isSortable: true,
// eslint-disable-next-line @typescript-eslint/camelcase
render: ({ is_required }: { is_required: boolean }) => (is_required ? 'Yes' : 'No'),
},
{
label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.reasonColumn', {
defaultMessage: 'Reason',
}),
id: 'reason',
alignment: LEFT_ALIGNMENT,
isSortable: false,
},
];
const checkboxDisabledCheck = (item: FieldSelectionItem) =>
(item.is_included === false && !item.reason?.includes('in excludes list')) ||
item.is_required === true;
export const MemoizedAnalysisFieldsTable: FC<{
excludes: string[];
loadingItems: boolean;
setFormState: any;
tableItems: FieldSelectionItem[];
}> = memo(
({ excludes, loadingItems, setFormState, tableItems }) => {
const [sortableProperties, setSortableProperties] = useState();
const [currentSelection, setCurrentSelection] = useState<any[]>([]);
useEffect(() => {
if (excludes.length > 0) {
setCurrentSelection(excludes);
}
}, []);
// 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';
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 = [
{
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>
),
},
],
},
];
return (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.excludedFieldsLabel', {
defaultMessage: 'Excluded fields',
})}
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">
<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
);

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC } from 'react';
import { EuiForm } from '@elastic/eui';
import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form';
import { ConfigurationStepDetails } from './configuration_step_details';
import { ConfigurationStepForm } from './configuration_step_form';
import { ANALYTICS_STEPS } from '../../page';
export const ConfigurationStep: FC<CreateAnalyticsStepProps> = ({
actions,
state,
setCurrentStep,
step,
stepActivated,
}) => {
return (
<EuiForm
className="mlDataFrameAnalyticsCreateForm"
data-test-subj="mlAnalyticsCreateJobWizardConfigurationStep"
>
{step === ANALYTICS_STEPS.CONFIGURATION && (
<ConfigurationStepForm actions={actions} state={state} setCurrentStep={setCurrentStep} />
)}
{step !== ANALYTICS_STEPS.CONFIGURATION && stepActivated === true && (
<ConfigurationStepDetails setCurrentStep={setCurrentStep} state={state} />
)}
</EuiForm>
);
};

View file

@ -0,0 +1,115 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButtonEmpty,
EuiDescriptionList,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
} from '@elastic/eui';
import {
State,
UNSET_CONFIG_ITEM,
} from '../../../analytics_management/hooks/use_create_analytics_form/state';
import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics';
import { useMlContext } from '../../../../../contexts/ml';
import { ANALYTICS_STEPS } from '../../page';
interface Props {
setCurrentStep: React.Dispatch<React.SetStateAction<any>>;
state: State;
}
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 isJobTypeWithDepVar =
jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION;
const detailsFirstCol = [
{
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.sourceIndex', {
defaultMessage: 'Source index',
}),
description: currentIndexPattern.title || UNSET_CONFIG_ITEM,
},
{
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.Query', {
defaultMessage: 'Query',
}),
description: jobConfigQueryString || UNSET_CONFIG_ITEM,
},
];
const detailsSecondCol = [
{
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.jobType', {
defaultMessage: 'Job type',
}),
description: jobType! as string,
},
];
const detailsThirdCol = [
{
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.excludedFields', {
defaultMessage: 'Excluded fields',
}),
description: excludes.length > 0 ? excludes.join(', ') : UNSET_CONFIG_ITEM,
},
];
if (isJobTypeWithDepVar) {
detailsSecondCol.push({
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.trainingPercent', {
defaultMessage: 'Training percent',
}),
description: `${trainingPercent}`,
});
detailsThirdCol.unshift({
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.dependentVariable', {
defaultMessage: 'Dependent variable',
}),
description: dependentVariable,
});
}
return (
<Fragment>
<EuiFlexGroup style={{ width: '70%' }} justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiDescriptionList compressed listItems={detailsFirstCol} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiDescriptionList compressed listItems={detailsSecondCol} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiDescriptionList compressed listItems={detailsThirdCol} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
{!isJobCreated && (
<EuiButtonEmpty
iconType="pencil"
size="s"
onClick={() => {
setCurrentStep(ANALYTICS_STEPS.CONFIGURATION);
}}
>
{i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.editButtonText', {
defaultMessage: 'Edit',
})}
</EuiButtonEmpty>
)}
</Fragment>
);
};

View file

@ -0,0 +1,449 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, Fragment, useEffect, useRef } from 'react';
import { EuiBadge, EuiComboBox, EuiFormRow, EuiRange, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { debounce } from 'lodash';
import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
import { useMlContext } from '../../../../../contexts/ml';
import {
DfAnalyticsExplainResponse,
FieldSelectionItem,
ANALYSIS_CONFIG_TYPE,
TRAINING_PERCENT_MIN,
TRAINING_PERCENT_MAX,
} from '../../../../common/analytics';
import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form';
import { Messages } from '../../../analytics_management/components/create_analytics_form/messages';
import {
DEFAULT_MODEL_MEMORY_LIMIT,
getJobConfigFromFormState,
State,
} from '../../../analytics_management/hooks/use_create_analytics_form/state';
import { shouldAddAsDepVarOption } from '../../../analytics_management/components/create_analytics_form/form_options_validation';
import { ml } from '../../../../../services/ml_api_service';
import { getToastNotifications } from '../../../../../util/dependency_cache';
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 { DataGrid } from '../../../../../components/data_grid';
import { useIndexData } from '../../hooks';
import { ExplorationQueryBar } from '../../../analytics_exploration/components/exploration_query_bar';
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.',
}
);
export const ConfigurationStepForm: FC<CreateAnalyticsStepProps> = ({
actions,
state,
setCurrentStep,
}) => {
const mlContext = useMlContext();
const { currentSavedSearch, currentIndexPattern } = mlContext;
const { savedSearchQuery, savedSearchQueryStr } = useSavedSearch();
const { initiateWizard, setEstimatedModelMemoryLimit, setFormState } = actions;
const { estimatedModelMemoryLimit, form, isJobCreated, requestMessages } = state;
const firstUpdate = useRef<boolean>(true);
const {
dependentVariable,
dependentVariableFetchFail,
dependentVariableOptions,
excludes,
excludesTableItems,
fieldOptionsFetchFail,
jobConfigQuery,
jobConfigQueryString,
jobType,
loadingDepVarOptions,
loadingFieldOptions,
maxDistinctValuesError,
modelMemoryLimit,
previousJobType,
requiredFieldsError,
trainingPercent,
} = form;
const setJobConfigQuery = ({ query, queryString }: { query: any; queryString: string }) => {
setFormState({ jobConfigQuery: query, jobConfigQueryString: queryString });
};
const indexData = useIndexData(
currentIndexPattern,
savedSearchQuery !== undefined ? savedSearchQuery : jobConfigQuery
);
const indexPreviewProps = {
...indexData,
dataTestSubj: 'mlAnalyticsCreationDataGrid',
toastNotifications: getToastNotifications(),
};
const isJobTypeWithDepVar =
jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION;
const dependentVariableEmpty = isJobTypeWithDepVar && dependentVariable === '';
const isStepInvalid =
dependentVariableEmpty ||
jobType === undefined ||
maxDistinctValuesError !== undefined ||
requiredFieldsError !== undefined;
const loadDepVarOptions = async (formState: State['form']) => {
setFormState({
loadingDepVarOptions: true,
maxDistinctValuesError: undefined,
});
try {
if (currentIndexPattern !== undefined) {
const formStateUpdate: {
loadingDepVarOptions: boolean;
dependentVariableFetchFail: boolean;
dependentVariableOptions: State['form']['dependentVariableOptions'];
dependentVariable?: State['form']['dependentVariable'];
} = {
loadingDepVarOptions: false,
dependentVariableFetchFail: false,
dependentVariableOptions: [] as State['form']['dependentVariableOptions'],
};
// Get fields and filter for supported types for job type
const { fields } = newJobCapsService;
let resetDependentVariable = true;
for (const field of fields) {
if (shouldAddAsDepVarOption(field, jobType)) {
formStateUpdate.dependentVariableOptions.push({
label: field.id,
});
if (formState.dependentVariable === field.id) {
resetDependentVariable = false;
}
}
}
if (resetDependentVariable) {
formStateUpdate.dependentVariable = '';
}
setFormState(formStateUpdate);
}
} catch (e) {
setFormState({ loadingDepVarOptions: false, dependentVariableFetchFail: true });
}
};
const debouncedGetExplainData = debounce(async () => {
const jobTypeChanged = previousJobType !== jobType;
const shouldUpdateModelMemoryLimit = !firstUpdate.current || !modelMemoryLimit;
const shouldUpdateEstimatedMml =
!firstUpdate.current || !modelMemoryLimit || estimatedModelMemoryLimit === '';
if (firstUpdate.current) {
firstUpdate.current = false;
}
// Reset if jobType changes (jobType requires dependent_variable to be set -
// which won't be the case if switching from outlier detection)
if (jobTypeChanged) {
setFormState({
loadingFieldOptions: true,
});
}
try {
const jobConfig = getJobConfigFromFormState(form);
delete jobConfig.dest;
delete jobConfig.model_memory_limit;
const resp: DfAnalyticsExplainResponse = await ml.dataFrameAnalytics.explainDataFrameAnalytics(
jobConfig
);
const expectedMemoryWithoutDisk = resp.memory_estimation?.expected_memory_without_disk;
if (shouldUpdateEstimatedMml) {
setEstimatedModelMemoryLimit(expectedMemoryWithoutDisk);
}
const fieldSelection: FieldSelectionItem[] | undefined = resp.field_selection;
let hasRequiredFields = false;
if (fieldSelection) {
for (let i = 0; i < fieldSelection.length; i++) {
const field = fieldSelection[i];
if (field.is_included === true && field.is_required === false) {
hasRequiredFields = true;
break;
}
}
}
// If job type has changed load analysis field options again
if (jobTypeChanged) {
setFormState({
...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemoryWithoutDisk } : {}),
excludesTableItems: fieldSelection ? fieldSelection : [],
loadingFieldOptions: false,
fieldOptionsFetchFail: false,
maxDistinctValuesError: undefined,
requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined,
});
} else {
setFormState({
...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemoryWithoutDisk } : {}),
requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined,
});
}
} catch (e) {
let errorMessage;
if (
jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION &&
e.body &&
e.body.message !== undefined &&
e.body.message.includes('status_exception') &&
(e.body.message.includes('must have at most') ||
e.body.message.includes('must have at least'))
) {
errorMessage = e.body.message;
}
const fallbackModelMemoryLimit =
jobType !== undefined
? DEFAULT_MODEL_MEMORY_LIMIT[jobType]
: DEFAULT_MODEL_MEMORY_LIMIT.outlier_detection;
setEstimatedModelMemoryLimit(fallbackModelMemoryLimit);
setFormState({
fieldOptionsFetchFail: true,
maxDistinctValuesError: errorMessage,
loadingFieldOptions: false,
...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: fallbackModelMemoryLimit } : {}),
});
}
}, 300);
useEffect(() => {
initiateWizard();
}, []);
useEffect(() => {
setFormState({ sourceIndex: currentIndexPattern.title });
}, []);
useEffect(() => {
if (savedSearchQueryStr !== undefined) {
setFormState({ jobConfigQuery: savedSearchQuery, jobConfigQueryString: savedSearchQueryStr });
}
}, [JSON.stringify(savedSearchQuery), savedSearchQueryStr]);
useEffect(() => {
if (isJobTypeWithDepVar) {
loadDepVarOptions(form);
}
}, [jobType]);
useEffect(() => {
const hasBasicRequiredFields = jobType !== undefined;
const hasRequiredAnalysisFields =
(isJobTypeWithDepVar && dependentVariable !== '') ||
jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION;
if (hasBasicRequiredFields && hasRequiredAnalysisFields) {
debouncedGetExplainData();
}
return () => {
debouncedGetExplainData.cancel();
};
}, [jobType, dependentVariable, trainingPercent, JSON.stringify(excludes), jobConfigQueryString]);
return (
<Fragment>
<Messages messages={requestMessages} />
<SupportedFieldsMessage jobType={jobType} />
<JobType type={jobType} setFormState={setFormState} />
{savedSearchQuery === undefined && (
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.sourceQueryLabel', {
defaultMessage: 'Query',
})}
fullWidth
>
<ExplorationQueryBar
indexPattern={currentIndexPattern}
// @ts-ignore
setSearchQuery={setJobConfigQuery}
includeQueryString
defaultQueryString={jobConfigQueryString}
/>
</EuiFormRow>
)}
<EuiFormRow
label={
<Fragment>
{savedSearchQuery !== undefined && (
<EuiText>
{i18n.translate('xpack.ml.dataframe.analytics.create.savedSearchLabel', {
defaultMessage: 'Saved search',
})}
</EuiText>
)}
<EuiBadge color="hollow">
{savedSearchQuery !== undefined
? currentSavedSearch?.attributes.title
: currentIndexPattern.title}
</EuiBadge>
</Fragment>
}
fullWidth
>
<DataGrid {...indexPreviewProps} />
</EuiFormRow>
{isJobTypeWithDepVar && (
<Fragment>
<EuiFormRow
fullWidth
label={i18n.translate('xpack.ml.dataframe.analytics.create.dependentVariableLabel', {
defaultMessage: 'Dependent variable',
})}
helpText={
dependentVariableOptions.length === 0 &&
dependentVariableFetchFail === false &&
currentIndexPattern &&
i18n.translate(
'xpack.ml.dataframe.analytics.create.dependentVariableOptionsNoNumericalFields',
{
defaultMessage: 'No numeric type fields were found for this index pattern.',
}
)
}
isInvalid={maxDistinctValuesError !== undefined}
error={[
...(dependentVariableFetchFail === true
? [
<Fragment>
{i18n.translate(
'xpack.ml.dataframe.analytics.create.dependentVariableOptionsFetchError',
{
defaultMessage:
'There was a problem fetching fields. Please refresh the page and try again.',
}
)}
</Fragment>,
]
: []),
...(fieldOptionsFetchFail === true && maxDistinctValuesError !== undefined
? [
<Fragment>
{i18n.translate(
'xpack.ml.dataframe.analytics.create.dependentVariableMaxDistictValuesError',
{
defaultMessage: 'Invalid. {message}',
values: { message: maxDistinctValuesError },
}
)}
</Fragment>,
]
: []),
]}
>
<EuiComboBox
fullWidth
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.create.dependentVariableInputAriaLabel',
{
defaultMessage: 'Enter field to be used as dependent variable.',
}
)}
placeholder={i18n.translate(
'xpack.ml.dataframe.analytics.create.dependentVariablePlaceholder',
{
defaultMessage: 'dependent variable',
}
)}
isDisabled={isJobCreated}
isLoading={loadingDepVarOptions}
singleSelection={true}
options={dependentVariableOptions}
selectedOptions={dependentVariable ? [{ label: dependentVariable }] : []}
onChange={(selectedOptions) =>
setFormState({
dependentVariable: selectedOptions[0].label || '',
})
}
isClearable={false}
isInvalid={dependentVariable === ''}
data-test-subj="mlAnalyticsCreateJobWizardDependentVariableSelect"
/>
</EuiFormRow>
</Fragment>
)}
<EuiFormRow
fullWidth
isInvalid={requiredFieldsError !== undefined}
error={
requiredFieldsError !== undefined && [
i18n.translate('xpack.ml.dataframe.analytics.create.requiredFieldsError', {
defaultMessage: 'Invalid. {message}',
values: { message: requiredFieldsError },
}),
]
}
>
<Fragment />
</EuiFormRow>
<MemoizedAnalysisFieldsTable
excludes={excludes}
tableItems={excludesTableItems}
loadingItems={loadingFieldOptions}
setFormState={setFormState}
/>
{isJobTypeWithDepVar && (
<EuiFormRow
fullWidth
label={i18n.translate('xpack.ml.dataframe.analytics.create.trainingPercentLabel', {
defaultMessage: 'Training percent',
})}
helpText={i18n.translate('xpack.ml.dataframe.analytics.create.trainingPercentHelpText', {
defaultMessage:
'Defines the percentage of eligible documents that will be used for training.',
})}
>
<EuiRange
fullWidth
min={TRAINING_PERCENT_MIN}
max={TRAINING_PERCENT_MAX}
step={1}
showLabels
showRange
showValue
value={trainingPercent}
// @ts-ignore Property 'value' does not exist on type 'EventTarget' | (EventTarget & HTMLInputElement)
onChange={(e) => setFormState({ trainingPercent: +e.target.value })}
data-test-subj="mlAnalyticsCreateJobWizardTrainingPercentSlider"
/>
</EuiFormRow>
)}
<EuiSpacer />
<ContinueButton
isDisabled={isStepInvalid}
onClick={() => {
setCurrentStep(ANALYTICS_STEPS.ADVANCED);
}}
/>
</Fragment>
);
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { ConfigurationStep } from './configuration_step';

View file

@ -0,0 +1,83 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, FC } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFormRow, EuiSelect } from '@elastic/eui';
import { ANALYSIS_CONFIG_TYPE } from '../../../../common';
import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state';
interface Props {
type: AnalyticsJobType;
setFormState: React.Dispatch<React.SetStateAction<any>>;
}
export const JobType: FC<Props> = ({ type, setFormState }) => {
const outlierHelpText = i18n.translate(
'xpack.ml.dataframe.analytics.create.outlierDetectionHelpText',
{
defaultMessage:
'Outlier detection jobs require a source index that is mapped as a table-like data structure and analyze only numeric and boolean fields. Use the advanced editor to add custom options to the configuration.',
}
);
const regressionHelpText = i18n.translate(
'xpack.ml.dataframe.analytics.create.outlierRegressionHelpText',
{
defaultMessage:
'Regression jobs analyze only numeric fields. Use the advanced editor to apply custom options, such as the prediction field name.',
}
);
const classificationHelpText = i18n.translate(
'xpack.ml.dataframe.analytics.create.classificationHelpText',
{
defaultMessage:
'Classification jobs require a source index that is mapped as a table-like data structure and support fields that are numeric, boolean, text, keyword, or ip. Use the advanced editor to apply custom options, such as the prediction field name.',
}
);
const helpText = {
[ANALYSIS_CONFIG_TYPE.REGRESSION]: regressionHelpText,
[ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION]: outlierHelpText,
[ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: classificationHelpText,
};
return (
<Fragment>
<EuiFormRow
fullWidth
label={i18n.translate('xpack.ml.dataframe.analytics.create.jobTypeLabel', {
defaultMessage: 'Job type',
})}
helpText={type !== undefined ? helpText[type] : ''}
>
<EuiSelect
fullWidth
options={Object.values(ANALYSIS_CONFIG_TYPE).map((jobType) => ({
value: jobType,
text: jobType.replace(/_/g, ' '),
'data-test-subj': `mlAnalyticsCreation-${jobType}-option`,
}))}
value={type}
hasNoInitialSelection={true}
onChange={(e) => {
const value = e.target.value as AnalyticsJobType;
setFormState({
previousJobType: type,
jobType: value,
excludes: [],
requiredFieldsError: undefined,
});
}}
data-test-subj="mlAnalyticsCreateJobWizardJobTypeSelect"
/>
</EuiFormRow>
</Fragment>
);
};

View file

@ -0,0 +1,120 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, Fragment, useState, useEffect } from 'react';
import { EuiSpacer, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state';
import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics';
import { Field, EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields';
import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES } from '../../../../common/fields';
import {
OMIT_FIELDS,
CATEGORICAL_TYPES,
} from '../../../analytics_management/components/create_analytics_form/form_options_validation';
import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public';
import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
const containsClassificationFieldsCb = ({ name, type }: Field) =>
!OMIT_FIELDS.includes(name) &&
name !== EVENT_RATE_FIELD_ID &&
(BASIC_NUMERICAL_TYPES.has(type) ||
CATEGORICAL_TYPES.has(type) ||
type === ES_FIELD_TYPES.BOOLEAN);
const containsRegressionFieldsCb = ({ name, type }: Field) =>
!OMIT_FIELDS.includes(name) &&
name !== EVENT_RATE_FIELD_ID &&
(BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type));
const containsOutlierFieldsCb = ({ name, type }: Field) =>
!OMIT_FIELDS.includes(name) && name !== EVENT_RATE_FIELD_ID && BASIC_NUMERICAL_TYPES.has(type);
const callbacks: Record<ANALYSIS_CONFIG_TYPE, (f: Field) => boolean> = {
[ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: containsClassificationFieldsCb,
[ANALYSIS_CONFIG_TYPE.REGRESSION]: containsRegressionFieldsCb,
[ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION]: containsOutlierFieldsCb,
};
const messages: Record<ANALYSIS_CONFIG_TYPE, JSX.Element> = {
[ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: (
<FormattedMessage
id="xpack.ml.dataframe.analytics.create.sourceObjectClassificationHelpText"
defaultMessage="This index pattern does not contain any supported fields. Classification jobs require categorical, numeric, or boolean fields."
/>
),
[ANALYSIS_CONFIG_TYPE.REGRESSION]: (
<FormattedMessage
id="xpack.ml.dataframe.analytics.create.sourceObjectRegressionHelpText"
defaultMessage="This index pattern does not contain any supported fields. Regression jobs require numeric fields."
/>
),
[ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION]: (
<FormattedMessage
id="xpack.ml.dataframe.analytics.create.sourceObjectHelpText"
defaultMessage="This index pattern does not contain any numeric type fields. The analytics job may not be able to come up with any outliers."
/>
),
};
interface Props {
jobType: AnalyticsJobType;
}
export const SupportedFieldsMessage: FC<Props> = ({ jobType }) => {
const [sourceIndexContainsSupportedFields, setSourceIndexContainsSupportedFields] = useState<
boolean
>(true);
const [sourceIndexFieldsCheckFailed, setSourceIndexFieldsCheckFailed] = useState<boolean>(false);
const { fields } = newJobCapsService;
// Find out if index pattern contains supported fields for job type. Provides a hint in the form
// that job may not run correctly if no supported fields are found.
const validateFields = () => {
if (fields && jobType !== undefined) {
try {
const containsSupportedFields: boolean = fields.some(callbacks[jobType]);
setSourceIndexContainsSupportedFields(containsSupportedFields);
setSourceIndexFieldsCheckFailed(false);
} catch (e) {
setSourceIndexFieldsCheckFailed(true);
}
}
};
useEffect(() => {
if (jobType !== undefined) {
setSourceIndexContainsSupportedFields(true);
setSourceIndexFieldsCheckFailed(false);
validateFields();
}
}, [jobType]);
if (sourceIndexContainsSupportedFields === true) return null;
if (sourceIndexFieldsCheckFailed === true) {
return (
<Fragment>
<FormattedMessage
id="xpack.ml.dataframe.analytics.create.sourceIndexFieldsCheckError"
defaultMessage="There was a problem checking for supported fields for job type. Please refresh the page and try again."
/>
<EuiSpacer size="s" />
</Fragment>
);
}
return (
<Fragment>
<EuiText size="xs" color="danger">
{jobType !== undefined && messages[jobType]}
</EuiText>
<EuiSpacer size="s" />
</Fragment>
);
};

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useState, useEffect } from 'react';
import { useMlContext } from '../../../../../contexts/ml';
import { esQuery, esKuery } from '../../../../../../../../../../src/plugins/data/public';
import { SEARCH_QUERY_LANGUAGE } from '../../../../../../../common/constants/search';
import { getQueryFromSavedSearch } from '../../../../../util/index_utils';
export function useSavedSearch() {
const [savedSearchQuery, setSavedSearchQuery] = useState<any>(undefined);
const [savedSearchQueryStr, setSavedSearchQueryStr] = useState<any>(undefined);
const mlContext = useMlContext();
const { currentSavedSearch, currentIndexPattern, kibanaConfig } = mlContext;
const getQueryData = () => {
let qry;
let qryString;
if (currentSavedSearch !== null) {
const { query } = getQueryFromSavedSearch(currentSavedSearch);
const queryLanguage = query.language as SEARCH_QUERY_LANGUAGE;
qryString = query.query;
if (queryLanguage === SEARCH_QUERY_LANGUAGE.KUERY) {
const ast = esKuery.fromKueryExpression(qryString);
qry = esKuery.toElasticsearchQuery(ast, currentIndexPattern);
} else {
qry = esQuery.luceneStringToDsl(qryString);
esQuery.decorateQuery(qry, kibanaConfig.get('query:queryString:options'));
}
setSavedSearchQuery(qry);
setSavedSearchQueryStr(qryString);
}
};
useEffect(() => {
getQueryData();
}, []);
return {
savedSearchQuery,
savedSearchQueryStr,
};
}

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC } from 'react';
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
const continueButtonText = i18n.translate(
'xpack.ml.dataframe.analytics.creation.continueButtonText',
{
defaultMessage: 'Continue',
}
);
export const ContinueButton: FC<{ isDisabled: boolean; onClick: any }> = ({
isDisabled,
onClick,
}) => (
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="mlAnalyticsCreateJobWizardContinueButton"
isDisabled={isDisabled}
size="s"
onClick={onClick}
>
{continueButtonText}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -0,0 +1,95 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, Fragment, useState } from 'react';
import {
EuiButton,
EuiCheckbox,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/use_create_analytics_form';
import { Messages } from '../../../analytics_management/components/create_analytics_form/messages';
import { ANALYTICS_STEPS } from '../../page';
import { BackToListPanel } from '../back_to_list_panel';
interface Props extends CreateAnalyticsFormProps {
step: ANALYTICS_STEPS;
}
export const CreateStep: FC<Props> = ({ actions, state, step }) => {
const { createAnalyticsJob, startAnalyticsJob } = actions;
const {
isAdvancedEditorValidJson,
isJobCreated,
isJobStarted,
isModalButtonDisabled,
isValid,
requestMessages,
} = state;
const [checked, setChecked] = useState<boolean>(true);
if (step !== ANALYTICS_STEPS.CREATE) return null;
const handleCreation = async () => {
await createAnalyticsJob();
if (checked) {
startAnalyticsJob();
}
};
return (
<Fragment>
{!isJobCreated && !isJobStarted && (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiFormRow
helpText={i18n.translate(
'xpack.ml.dataframe.analytics.create.startCheckboxHelpText',
{
defaultMessage:
'If unselected, job can be started later by returning to the jobs list.',
}
)}
>
<EuiCheckbox
data-test-subj="mlAnalyticsCreateJobWizardStartJobCheckbox"
id={'dataframe-create-start-checkbox'}
label={i18n.translate('xpack.ml.dataframe.analytics.create.wizardStartCheckbox', {
defaultMessage: 'Start immediately',
})}
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
className="mlAnalyticsCreateWizard__footerButton"
disabled={!isValid || !isAdvancedEditorValidJson || isModalButtonDisabled}
onClick={handleCreation}
fill
data-test-subj="mlAnalyticsCreateJobWizardCreateButton"
>
{i18n.translate('xpack.ml.dataframe.analytics.create.wizardCreateButton', {
defaultMessage: 'Create',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiSpacer size="s" />
<Messages messages={requestMessages} />
{isJobCreated === true && <BackToListPanel />}
</Fragment>
);
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { CreateStep } from './create_step';

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC } from 'react';
import { EuiForm } from '@elastic/eui';
import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form';
import { DetailsStepDetails } from './details_step_details';
import { DetailsStepForm } from './details_step_form';
import { ANALYTICS_STEPS } from '../../page';
export const DetailsStep: FC<CreateAnalyticsStepProps> = ({
actions,
state,
setCurrentStep,
step,
stepActivated,
}) => {
return (
<EuiForm className="mlDataFrameAnalyticsCreateForm">
{step === ANALYTICS_STEPS.DETAILS && (
<DetailsStepForm actions={actions} state={state} setCurrentStep={setCurrentStep} />
)}
{step !== ANALYTICS_STEPS.DETAILS && stepActivated === true && (
<DetailsStepDetails setCurrentStep={setCurrentStep} state={state} />
)}
</EuiForm>
);
};

View file

@ -0,0 +1,87 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButtonEmpty,
EuiDescriptionList,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
} from '@elastic/eui';
import { State } from '../../../analytics_management/hooks/use_create_analytics_form/state';
import { ANALYTICS_STEPS } from '../../page';
export interface ListItems {
title: string;
description: string | JSX.Element;
}
export const DetailsStepDetails: FC<{ setCurrentStep: any; state: State }> = ({
setCurrentStep,
state,
}) => {
const { form, isJobCreated } = state;
const { description, jobId, destinationIndex } = form;
const detailsFirstCol: ListItems[] = [
{
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.jobId', {
defaultMessage: 'Job ID',
}),
description: jobId,
},
];
const detailsSecondCol: ListItems[] = [
{
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.jobDescription', {
defaultMessage: 'Job description',
}),
description,
},
];
const detailsThirdCol: ListItems[] = [
{
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.destIndex', {
defaultMessage: 'Destination index',
}),
description: destinationIndex || '',
},
];
return (
<Fragment>
<EuiFlexGroup style={{ width: '70%' }} justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiDescriptionList compressed listItems={detailsFirstCol} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiDescriptionList compressed listItems={detailsSecondCol} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiDescriptionList compressed listItems={detailsThirdCol} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
{!isJobCreated && (
<EuiButtonEmpty
iconType="pencil"
size="s"
onClick={() => {
setCurrentStep(ANALYTICS_STEPS.DETAILS);
}}
>
{i18n.translate('xpack.ml.dataframe.analytics.create.detailsDetails.editButtonText', {
defaultMessage: 'Edit',
})}
</EuiButtonEmpty>
)}
</Fragment>
);
};

View file

@ -0,0 +1,221 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, Fragment, useRef } from 'react';
import { EuiFieldText, EuiFormRow, EuiLink, EuiSpacer, EuiSwitch, EuiTextArea } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useMlKibana } from '../../../../../contexts/kibana';
import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form';
import { JOB_ID_MAX_LENGTH } from '../../../../../../../common/constants/validation';
import { ContinueButton } from '../continue_button';
import { ANALYTICS_STEPS } from '../../page';
export const DetailsStepForm: FC<CreateAnalyticsStepProps> = ({
actions,
state,
setCurrentStep,
}) => {
const {
services: { docLinks },
} = useMlKibana();
const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks;
const { setFormState } = actions;
const { form, isJobCreated } = state;
const {
createIndexPattern,
description,
destinationIndex,
destinationIndexNameEmpty,
destinationIndexNameExists,
destinationIndexNameValid,
destinationIndexPatternTitleExists,
jobId,
jobIdEmpty,
jobIdExists,
jobIdInvalidMaxLength,
jobIdValid,
} = form;
const forceInput = useRef<HTMLInputElement | null>(null);
const isStepInvalid =
jobIdEmpty === true ||
jobIdExists === true ||
jobIdValid === false ||
destinationIndexNameEmpty === true ||
destinationIndexNameValid === false ||
(destinationIndexPatternTitleExists === true && createIndexPattern === true);
return (
<Fragment>
<EuiFormRow
fullWidth
label={i18n.translate('xpack.ml.dataframe.analytics.create.jobIdLabel', {
defaultMessage: 'Job ID',
})}
isInvalid={(!jobIdEmpty && !jobIdValid) || jobIdExists || jobIdInvalidMaxLength}
error={[
...(!jobIdEmpty && !jobIdValid
? [
i18n.translate('xpack.ml.dataframe.analytics.create.jobIdInvalidError', {
defaultMessage:
'Must contain lowercase alphanumeric characters (a-z and 0-9), hyphens, and underscores only and must start and end with alphanumeric characters.',
}),
]
: []),
...(jobIdExists
? [
i18n.translate('xpack.ml.dataframe.analytics.create.jobIdExistsError', {
defaultMessage: 'An analytics job with this ID already exists.',
}),
]
: []),
...(jobIdInvalidMaxLength
? [
i18n.translate(
'xpack.ml.dataframe.analytics.create.jobIdInvalidMaxLengthErrorMessage',
{
defaultMessage:
'Job ID must be no more than {maxLength, plural, one {# character} other {# characters}} long.',
values: {
maxLength: JOB_ID_MAX_LENGTH,
},
}
),
]
: []),
]}
>
<EuiFieldText
fullWidth
inputRef={(input) => {
if (input) {
forceInput.current = input;
}
}}
disabled={isJobCreated}
placeholder={i18n.translate('xpack.ml.dataframe.analytics.create.jobIdPlaceholder', {
defaultMessage: 'Job ID',
})}
value={jobId}
onChange={(e) => setFormState({ jobId: e.target.value })}
aria-label={i18n.translate('xpack.ml.dataframe.analytics.create.jobIdInputAriaLabel', {
defaultMessage: 'Choose a unique analytics job ID.',
})}
isInvalid={(!jobIdEmpty && !jobIdValid) || jobIdExists || jobIdEmpty}
data-test-subj="mlAnalyticsCreateJobFlyoutJobIdInput"
/>
</EuiFormRow>
<EuiFormRow
fullWidth
label={i18n.translate('xpack.ml.dataframe.analytics.create.jobDescription.label', {
defaultMessage: 'Job description',
})}
>
<EuiTextArea
fullWidth
value={description}
placeholder={i18n.translate(
'xpack.ml.dataframe.analytics.create.jobDescription.helpText',
{
defaultMessage: 'Optional descriptive text',
}
)}
rows={2}
onChange={(e) => {
const value = e.target.value;
setFormState({ description: value });
}}
data-test-subj="mlDFAnalyticsJobCreationJobDescription"
/>
</EuiFormRow>
<EuiFormRow
fullWidth
label={i18n.translate('xpack.ml.dataframe.analytics.create.destinationIndexLabel', {
defaultMessage: 'Destination index',
})}
isInvalid={
destinationIndexNameEmpty || (!destinationIndexNameEmpty && !destinationIndexNameValid)
}
helpText={
destinationIndexNameExists &&
i18n.translate('xpack.ml.dataframe.analytics.create.destinationIndexHelpText', {
defaultMessage:
'An index with this name already exists. Be aware that running this analytics job will modify this destination index.',
})
}
error={
!destinationIndexNameEmpty &&
!destinationIndexNameValid && [
<Fragment>
{i18n.translate('xpack.ml.dataframe.analytics.create.destinationIndexInvalidError', {
defaultMessage: 'Invalid destination index name.',
})}
<br />
<EuiLink
href={`${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/indices-create-index.html#indices-create-index`}
target="_blank"
>
{i18n.translate(
'xpack.ml.dataframe.stepDetailsForm.destinationIndexInvalidErrorLink',
{
defaultMessage: 'Learn more about index name limitations.',
}
)}
</EuiLink>
</Fragment>,
]
}
>
<EuiFieldText
fullWidth
disabled={isJobCreated}
placeholder="destination index"
value={destinationIndex}
onChange={(e) => setFormState({ destinationIndex: e.target.value })}
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.create.destinationIndexInputAriaLabel',
{
defaultMessage: 'Choose a unique destination index name.',
}
)}
isInvalid={!destinationIndexNameEmpty && !destinationIndexNameValid}
data-test-subj="mlAnalyticsCreateJobFlyoutDestinationIndexInput"
/>
</EuiFormRow>
<EuiFormRow
isInvalid={createIndexPattern && destinationIndexPatternTitleExists}
error={
createIndexPattern &&
destinationIndexPatternTitleExists && [
i18n.translate('xpack.ml.dataframe.analytics.create.indexPatternExistsError', {
defaultMessage: 'An index pattern with this title already exists.',
}),
]
}
>
<EuiSwitch
disabled={isJobCreated}
name="mlDataFrameAnalyticsCreateIndexPattern"
label={i18n.translate('xpack.ml.dataframe.analytics.create.createIndexPatternLabel', {
defaultMessage: 'Create index pattern',
})}
checked={createIndexPattern === true}
onChange={() => setFormState({ createIndexPattern: !createIndexPattern })}
data-test-subj="mlAnalyticsCreateJobWizardCreateIndexPatternSwitch"
/>
</EuiFormRow>
<EuiSpacer />
<ContinueButton
isDisabled={isStepInvalid}
onClick={() => {
setCurrentStep(ANALYTICS_STEPS.CREATE);
}}
/>
</Fragment>
);
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { DetailsStep } from './details_step';

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { ConfigurationStep } from './configuration_step/index';
export { AdvancedStep } from './advanced_step/index';
export { DetailsStep } from './details_step/index';
export { CreateStep } from './create_step/index';

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { useIndexData } from './use_index_data';

View file

@ -0,0 +1,103 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useEffect } from 'react';
import { IndexPattern } from '../../../../../../../../../src/plugins/data/public';
import {
getDataGridSchemaFromKibanaFieldType,
getFieldsFromKibanaIndexPattern,
useDataGrid,
useRenderCellValue,
EsSorting,
SearchResponse7,
UseIndexDataReturnType,
} from '../../../../components/data_grid';
import { getErrorMessage } from '../../../../../../common/util/errors';
import { INDEX_STATUS } from '../../../common/analytics';
import { ml } from '../../../../services/ml_api_service';
type IndexSearchResponse = SearchResponse7;
export const useIndexData = (indexPattern: IndexPattern, query: any): UseIndexDataReturnType => {
const indexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern);
// EuiDataGrid State
const columns = [
...indexPatternFields.map((id) => {
const field = indexPattern.fields.getByName(id);
const schema = getDataGridSchemaFromKibanaFieldType(field);
return { id, schema };
}),
];
const dataGrid = useDataGrid(columns);
const {
pagination,
resetPagination,
setErrorMessage,
setRowCount,
setStatus,
setTableItems,
sortingColumns,
tableItems,
} = dataGrid;
useEffect(() => {
resetPagination();
// custom comparison
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(query)]);
const getIndexData = async function () {
setErrorMessage('');
setStatus(INDEX_STATUS.LOADING);
const sort: EsSorting = sortingColumns.reduce((s, column) => {
s[column.id] = { order: column.direction };
return s;
}, {} as EsSorting);
const esSearchRequest = {
index: indexPattern.title,
body: {
// Instead of using the default query (`*`), fall back to a more efficient `match_all` query.
query, // isDefaultQuery(query) ? matchAllQuery : query,
from: pagination.pageIndex * pagination.pageSize,
size: pagination.pageSize,
...(Object.keys(sort).length > 0 ? { sort } : {}),
},
};
try {
const resp: IndexSearchResponse = await ml.esSearch(esSearchRequest);
const docs = resp.hits.hits.map((d) => d._source);
setRowCount(resp.hits.total.value);
setTableItems(docs);
setStatus(INDEX_STATUS.LOADED);
} catch (e) {
setErrorMessage(getErrorMessage(e));
setStatus(INDEX_STATUS.ERROR);
}
};
useEffect(() => {
getIndexData();
// custom comparison
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]);
const renderCellValue = useRenderCellValue(indexPattern, pagination, tableItems);
return {
...dataGrid,
columns,
renderCellValue,
};
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { Page } from './page';

View file

@ -0,0 +1,191 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, useEffect, useState } from 'react';
import {
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiPage,
EuiPageBody,
EuiPageContent,
EuiSpacer,
EuiSteps,
EuiStepStatus,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { useMlContext } from '../../../contexts/ml';
import { newJobCapsService } from '../../../services/new_job_capabilities_service';
import { useCreateAnalyticsForm } from '../analytics_management/hooks/use_create_analytics_form';
import { CreateAnalyticsAdvancedEditor } from '../analytics_management/components/create_analytics_advanced_editor';
import { AdvancedStep, ConfigurationStep, CreateStep, DetailsStep } from './components';
export enum ANALYTICS_STEPS {
CONFIGURATION,
ADVANCED,
DETAILS,
CREATE,
}
export const Page: FC = () => {
const [currentStep, setCurrentStep] = useState<ANALYTICS_STEPS>(ANALYTICS_STEPS.CONFIGURATION);
const [activatedSteps, setActivatedSteps] = useState<boolean[]>([true, false, false, false]);
const mlContext = useMlContext();
const { currentIndexPattern } = mlContext;
const createAnalyticsForm = useCreateAnalyticsForm();
const { isAdvancedEditorEnabled } = createAnalyticsForm.state;
const { jobType } = createAnalyticsForm.state.form;
const { switchToAdvancedEditor } = createAnalyticsForm.actions;
useEffect(() => {
if (activatedSteps[currentStep] === false) {
activatedSteps.splice(currentStep, 1, true);
setActivatedSteps(activatedSteps);
}
}, [currentStep]);
useEffect(() => {
if (currentIndexPattern) {
(async function () {
await newJobCapsService.initializeFromIndexPattern(currentIndexPattern, false, false);
})();
}
}, []);
const analyticsWizardSteps = [
{
title: i18n.translate('xpack.ml.dataframe.analytics.creation.configurationStepTitle', {
defaultMessage: 'Configuration',
}),
children: (
<ConfigurationStep
{...createAnalyticsForm}
setCurrentStep={setCurrentStep}
step={currentStep}
stepActivated={activatedSteps[ANALYTICS_STEPS.CONFIGURATION]}
/>
),
status:
currentStep >= ANALYTICS_STEPS.CONFIGURATION ? undefined : ('incomplete' as EuiStepStatus),
},
{
title: i18n.translate('xpack.ml.dataframe.analytics.creation.advancedStepTitle', {
defaultMessage: 'Additional options',
}),
children: (
<AdvancedStep
{...createAnalyticsForm}
setCurrentStep={setCurrentStep}
step={currentStep}
stepActivated={activatedSteps[ANALYTICS_STEPS.ADVANCED]}
/>
),
status: currentStep >= ANALYTICS_STEPS.ADVANCED ? undefined : ('incomplete' as EuiStepStatus),
'data-test-subj': 'mlAnalyticsCreateJobWizardAdvancedStep',
},
{
title: i18n.translate('xpack.ml.dataframe.analytics.creation.detailsStepTitle', {
defaultMessage: 'Job details',
}),
children: (
<DetailsStep
{...createAnalyticsForm}
setCurrentStep={setCurrentStep}
step={currentStep}
stepActivated={activatedSteps[ANALYTICS_STEPS.DETAILS]}
/>
),
status: currentStep >= ANALYTICS_STEPS.DETAILS ? undefined : ('incomplete' as EuiStepStatus),
'data-test-subj': 'mlAnalyticsCreateJobWizardDetailsStep',
},
{
title: i18n.translate('xpack.ml.dataframe.analytics.creation.createStepTitle', {
defaultMessage: 'Create',
}),
children: <CreateStep {...createAnalyticsForm} step={currentStep} />,
status: currentStep >= ANALYTICS_STEPS.CREATE ? undefined : ('incomplete' as EuiStepStatus),
'data-test-subj': 'mlAnalyticsCreateJobWizardCreateStep',
},
];
return (
<EuiPage data-test-subj="mlAnalyticsCreationContainer">
<EuiPageBody restrictWidth={1200}>
<EuiPageContent>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiTitle size="m">
<h1>
<FormattedMessage
id="xpack.ml.dataframe.analytics.creationPageTitle"
defaultMessage="Create analytics job"
/>
</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<h2>
<FormattedMessage
id="xpack.ml.dataframe.analytics.creationPageSourceIndexTitle"
defaultMessage="Source index pattern: {indexTitle}"
values={{ indexTitle: currentIndexPattern.title }}
/>
</h2>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{isAdvancedEditorEnabled === false && (
<EuiFlexItem grow={false}>
<EuiFormRow
helpText={i18n.translate(
'xpack.ml.dataframe.analytics.create.enableJsonEditorHelpText',
{
defaultMessage: 'You cannot switch back to this form from the json editor.',
}
)}
>
<EuiButtonEmpty
isDisabled={jobType === undefined}
iconType="link"
onClick={switchToAdvancedEditor}
data-test-subj="mlAnalyticsCreateJobWizardAdvancedEditorSwitch"
>
<EuiText size="s" grow={false}>
{i18n.translate(
'xpack.ml.dataframe.analytics.create.switchToJsonEditorSwitch',
{
defaultMessage: 'Switch to json editor',
}
)}
</EuiText>
</EuiButtonEmpty>
</EuiFormRow>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiSpacer />
{isAdvancedEditorEnabled === true && (
<CreateAnalyticsAdvancedEditor {...createAnalyticsForm} />
)}
{isAdvancedEditorEnabled === false && (
<EuiSteps
data-test-subj="mlAnalyticsCreateJobWizardSteps"
steps={analyticsWizardSteps}
/>
)}
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
};

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Dispatch, FC, SetStateAction, useState } from 'react';
import React, { Dispatch, FC, SetStateAction, useEffect, useState } from 'react';
import { EuiCode, EuiInputPopover } from '@elastic/eui';
@ -30,11 +30,15 @@ interface ErrorMessage {
interface ExplorationQueryBarProps {
indexPattern: IIndexPattern;
setSearchQuery: Dispatch<SetStateAction<SavedSearchQuery>>;
includeQueryString?: boolean;
defaultQueryString?: string;
}
export const ExplorationQueryBar: FC<ExplorationQueryBarProps> = ({
indexPattern,
setSearchQuery,
includeQueryString = false,
defaultQueryString,
}) => {
// The internal state of the input query bar updated on every key stroke.
const [searchInput, setSearchInput] = useState<Query>({
@ -44,20 +48,34 @@ export const ExplorationQueryBar: FC<ExplorationQueryBarProps> = ({
const [errorMessage, setErrorMessage] = useState<ErrorMessage | undefined>(undefined);
useEffect(() => {
if (defaultQueryString !== undefined) {
setSearchInput({ query: defaultQueryString, language: SEARCH_QUERY_LANGUAGE.KUERY });
}
}, []);
const searchChangeHandler = (query: Query) => setSearchInput(query);
const searchSubmitHandler = (query: Query) => {
try {
switch (query.language) {
case SEARCH_QUERY_LANGUAGE.KUERY:
const convertedKQuery = esKuery.toElasticsearchQuery(
esKuery.fromKueryExpression(query.query as string),
indexPattern
);
setSearchQuery(
esKuery.toElasticsearchQuery(
esKuery.fromKueryExpression(query.query as string),
indexPattern
)
includeQueryString
? { queryString: query.query, query: convertedKQuery }
: convertedKQuery
);
return;
case SEARCH_QUERY_LANGUAGE.LUCENE:
setSearchQuery(esQuery.luceneStringToDsl(query.query as string));
const convertedLQuery = esQuery.luceneStringToDsl(query.query as string);
setSearchQuery(
includeQueryString
? { queryString: query.query, query: convertedLQuery }
: convertedLQuery
);
return;
}
} catch (e) {

View file

@ -7,7 +7,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
import { DeepReadonly } from '../../../../../../../common/types/common';
// import { DeepReadonly } from '../../../../../../../common/types/common';
import {
checkPermission,
@ -21,7 +21,7 @@ import {
isClassificationAnalysis,
} from '../../../../common/analytics';
import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form';
import { CloneAction } from './action_clone';
// import { CloneAction } from './action_clone';
import { getResultsUrl, isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common';
import { stopAnalytics } from '../../services/analytics_service';
@ -106,10 +106,10 @@ export const getActions = (createAnalyticsForm: CreateAnalyticsFormProps) => {
return <DeleteAction item={item} />;
},
},
{
render: (item: DeepReadonly<DataFrameAnalyticsListRow>) => {
return <CloneAction item={item} createAnalyticsForm={createAnalyticsForm} />;
},
},
// {
// render: (item: DeepReadonly<DataFrameAnalyticsListRow>) => {
// return <CloneAction item={item} createAnalyticsForm={createAnalyticsForm} />;
// },
// },
];
};

View file

@ -53,6 +53,7 @@ import { CreateAnalyticsButton } from '../create_analytics_button';
import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form';
import { CreateAnalyticsFlyoutWrapper } from '../create_analytics_flyout_wrapper';
import { getSelectedJobIdFromUrl } from '../../../../../jobs/jobs_list/components/utils';
import { SourceSelection } from '../source_selection';
function getItemIdToExpandedRowMap(
itemIds: DataFrameAnalyticsId[],
@ -90,6 +91,7 @@ export const DataFrameAnalyticsList: FC<Props> = ({
createAnalyticsForm,
}) => {
const [isInitialized, setIsInitialized] = useState(false);
const [isSourceIndexModalVisible, setIsSourceIndexModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [filterActive, setFilterActive] = useState(false);
@ -271,7 +273,7 @@ export const DataFrameAnalyticsList: FC<Props> = ({
!isManagementTable && createAnalyticsForm
? [
<EuiButtonEmpty
onClick={createAnalyticsForm.actions.openModal}
onClick={() => setIsSourceIndexModalVisible(true)}
isDisabled={disabled}
data-test-subj="mlAnalyticsCreateFirstButton"
>
@ -287,6 +289,9 @@ export const DataFrameAnalyticsList: FC<Props> = ({
{!isManagementTable && createAnalyticsForm && (
<CreateAnalyticsFlyoutWrapper {...createAnalyticsForm} />
)}
{isSourceIndexModalVisible === true && (
<SourceSelection onClose={() => setIsSourceIndexModalVisible(false)} />
)}
</Fragment>
);
}
@ -402,7 +407,10 @@ export const DataFrameAnalyticsList: FC<Props> = ({
</EuiFlexItem>
{!isManagementTable && createAnalyticsForm && (
<EuiFlexItem grow={false}>
<CreateAnalyticsButton {...createAnalyticsForm} />
<CreateAnalyticsButton
{...createAnalyticsForm}
setIsSourceIndexModalVisible={setIsSourceIndexModalVisible}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
@ -435,6 +443,9 @@ export const DataFrameAnalyticsList: FC<Props> = ({
{!isManagementTable && createAnalyticsForm?.state.isModalVisible && (
<CreateAnalyticsFlyoutWrapper {...createAnalyticsForm} />
)}
{isSourceIndexModalVisible === true && (
<SourceSelection onClose={() => setIsSourceIndexModalVisible(false)} />
)}
</Fragment>
);
};

View file

@ -23,11 +23,14 @@ import { XJsonMode } from '../../../../../../../shared_imports';
const xJsonMode = new XJsonMode();
import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form';
import { CreateStep } from '../../../analytics_creation/components/create_step';
import { ANALYTICS_STEPS } from '../../../analytics_creation/page';
export const CreateAnalyticsAdvancedEditor: FC<CreateAnalyticsFormProps> = ({ actions, state }) => {
export const CreateAnalyticsAdvancedEditor: FC<CreateAnalyticsFormProps> = (props) => {
const { actions, state } = props;
const { setAdvancedEditorRawString, setFormState } = actions;
const { advancedEditorMessages, advancedEditorRawString, isJobCreated, requestMessages } = state;
const { advancedEditorMessages, advancedEditorRawString, isJobCreated } = state;
const {
createIndexPattern,
@ -56,120 +59,105 @@ export const CreateAnalyticsAdvancedEditor: FC<CreateAnalyticsFormProps> = ({ ac
return (
<EuiForm className="mlDataFrameAnalyticsCreateForm">
{requestMessages.map((requestMessage, i) => (
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.advancedEditor.jobIdLabel', {
defaultMessage: 'Analytics job ID',
})}
isInvalid={(!jobIdEmpty && !jobIdValid) || jobIdExists}
error={[
...(!jobIdEmpty && !jobIdValid
? [
i18n.translate(
'xpack.ml.dataframe.analytics.create.advancedEditor.jobIdInvalidError',
{
defaultMessage:
'Must contain lowercase alphanumeric characters (a-z and 0-9), hyphens, and underscores only and must start and end with alphanumeric characters.',
}
),
]
: []),
...(jobIdExists
? [
i18n.translate(
'xpack.ml.dataframe.analytics.create.advancedEditor.jobIdExistsError',
{
defaultMessage: 'An analytics job with this ID already exists.',
}
),
]
: []),
]}
>
<EuiFieldText
inputRef={(input) => {
if (input) {
forceInput.current = input;
}
}}
disabled={isJobCreated}
placeholder="analytics job ID"
value={jobId}
onChange={(e) => setFormState({ jobId: e.target.value })}
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.create.advancedEditor.jobIdInputAriaLabel',
{
defaultMessage: 'Choose a unique analytics job ID.',
}
)}
isInvalid={(!jobIdEmpty && !jobIdValid) || jobIdExists}
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate(
'xpack.ml.dataframe.analytics.create.advancedEditor.configRequestBody',
{
defaultMessage: 'Configuration request body',
}
)}
style={{ maxWidth: '100%' }}
>
<EuiCodeEditor
isReadOnly={isJobCreated}
mode={xJsonMode}
width="100%"
value={advancedEditorRawString}
onChange={onChange}
setOptions={{
fontSize: '12px',
maxLines: 20,
}}
theme="textmate"
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.create.advancedEditor.codeEditorAriaLabel',
{
defaultMessage: 'Advanced analytics job editor',
}
)}
/>
</EuiFormRow>
<EuiSpacer />
{advancedEditorMessages.map((advancedEditorMessage, i) => (
<Fragment key={i}>
<EuiCallOut
title={requestMessage.message}
color={requestMessage.error !== undefined ? 'danger' : 'primary'}
iconType={requestMessage.error !== undefined ? 'alert' : 'checkInCircleFilled'}
title={
advancedEditorMessage.message !== ''
? advancedEditorMessage.message
: advancedEditorMessage.error
}
color={advancedEditorMessage.error !== undefined ? 'danger' : 'primary'}
iconType={advancedEditorMessage.error !== undefined ? 'alert' : 'checkInCircleFilled'}
size="s"
>
{requestMessage.error !== undefined ? <p>{requestMessage.error}</p> : null}
{advancedEditorMessage.message !== '' && advancedEditorMessage.error !== undefined ? (
<p>{advancedEditorMessage.error}</p>
) : null}
</EuiCallOut>
<EuiSpacer size="s" />
<EuiSpacer />
</Fragment>
))}
{!isJobCreated && (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.advancedEditor.jobIdLabel', {
defaultMessage: 'Analytics job ID',
})}
isInvalid={(!jobIdEmpty && !jobIdValid) || jobIdExists}
error={[
...(!jobIdEmpty && !jobIdValid
? [
i18n.translate(
'xpack.ml.dataframe.analytics.create.advancedEditor.jobIdInvalidError',
{
defaultMessage:
'Must contain lowercase alphanumeric characters (a-z and 0-9), hyphens, and underscores only and must start and end with alphanumeric characters.',
}
),
]
: []),
...(jobIdExists
? [
i18n.translate(
'xpack.ml.dataframe.analytics.create.advancedEditor.jobIdExistsError',
{
defaultMessage: 'An analytics job with this ID already exists.',
}
),
]
: []),
]}
>
<EuiFieldText
inputRef={(input) => {
if (input) {
forceInput.current = input;
}
}}
disabled={isJobCreated}
placeholder="analytics job ID"
value={jobId}
onChange={(e) => setFormState({ jobId: e.target.value })}
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.create.advancedEditor.jobIdInputAriaLabel',
{
defaultMessage: 'Choose a unique analytics job ID.',
}
)}
isInvalid={(!jobIdEmpty && !jobIdValid) || jobIdExists}
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate(
'xpack.ml.dataframe.analytics.create.advancedEditor.configRequestBody',
{
defaultMessage: 'Configuration request body',
}
)}
style={{ maxWidth: '100%' }}
>
<EuiCodeEditor
mode={xJsonMode}
width="100%"
value={advancedEditorRawString}
onChange={onChange}
setOptions={{
fontSize: '12px',
maxLines: 20,
}}
theme="textmate"
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.create.advancedEditor.codeEditorAriaLabel',
{
defaultMessage: 'Advanced analytics job editor',
}
)}
/>
</EuiFormRow>
<EuiSpacer />
{advancedEditorMessages.map((advancedEditorMessage, i) => (
<Fragment key={i}>
<EuiCallOut
title={
advancedEditorMessage.message !== ''
? advancedEditorMessage.message
: advancedEditorMessage.error
}
color={advancedEditorMessage.error !== undefined ? 'danger' : 'primary'}
iconType={
advancedEditorMessage.error !== undefined ? 'alert' : 'checkInCircleFilled'
}
size="s"
>
{advancedEditorMessage.message !== '' &&
advancedEditorMessage.error !== undefined ? (
<p>{advancedEditorMessage.error}</p>
) : null}
</EuiCallOut>
<EuiSpacer />
</Fragment>
))}
<EuiFormRow
isInvalid={createIndexPattern && destinationIndexPatternTitleExists}
error={
@ -196,6 +184,8 @@ export const CreateAnalyticsAdvancedEditor: FC<CreateAnalyticsFormProps> = ({ ac
</EuiFormRow>
</Fragment>
)}
<EuiSpacer />
<CreateStep {...props} step={ANALYTICS_STEPS.CREATE} />
</EuiForm>
);
};

View file

@ -0,0 +1,4 @@
.dataFrameAnalyticsCreateSearchDialog {
width: $euiSizeL * 30;
min-height: $euiSizeL * 25;
}

View file

@ -35,7 +35,9 @@ describe('Data Frame Analytics: <CreateAnalyticsButton />', () => {
test('Minimal initialization', () => {
const { getLastHookValue } = getMountedHook();
const props = getLastHookValue();
const wrapper = mount(<CreateAnalyticsButton {...props} />);
const wrapper = mount(
<CreateAnalyticsButton {...props} setIsSourceIndexModalVisible={jest.fn()} />
);
expect(wrapper.find('EuiButton').text()).toBe('Create analytics job');
});

View file

@ -10,15 +10,26 @@ import { i18n } from '@kbn/i18n';
import { createPermissionFailureMessage } from '../../../../../capabilities/check_capabilities';
import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form';
export const CreateAnalyticsButton: FC<CreateAnalyticsFormProps> = (props) => {
const { disabled } = props.state;
const { openModal } = props.actions;
interface Props extends CreateAnalyticsFormProps {
setIsSourceIndexModalVisible: React.Dispatch<React.SetStateAction<any>>;
}
export const CreateAnalyticsButton: FC<Props> = ({
state,
actions,
setIsSourceIndexModalVisible,
}) => {
const { disabled } = state;
const handleClick = () => {
setIsSourceIndexModalVisible(true);
};
const button = (
<EuiButton
disabled={disabled}
fill
onClick={openModal}
onClick={handleClick}
iconType="plusInCircle"
size="s"
data-test-subj="mlAnalyticsButtonCreate"

View file

@ -10,7 +10,7 @@ import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics';
import { AnalyticsJobType } from '../../hooks/use_create_analytics_form/state';
import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES } from '../../../../common/fields';
const CATEGORICAL_TYPES = new Set(['ip', 'keyword']);
export const CATEGORICAL_TYPES = new Set(['ip', 'keyword']);
// List of system fields we want to ignore for the numeric field check.
export const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score'];

View file

@ -20,6 +20,7 @@ export const Messages: FC<Props> = ({ messages }) => {
{messages.map((requestMessage, i) => (
<Fragment key={i}>
<EuiCallOut
data-test-subj={`analyticsWizardCreationCallout_${i}`}
title={requestMessage.message}
color={requestMessage.error !== undefined ? 'danger' : 'primary'}
iconType={requestMessage.error !== undefined ? 'alert' : 'checkInCircleFilled'}

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { SourceSelection } from './source_selection';

View file

@ -0,0 +1,100 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiModal,
EuiModalBody,
EuiModalHeader,
EuiModalHeaderTitle,
EuiOverlayMask,
} from '@elastic/eui';
import { SavedObjectFinderUi } from '../../../../../../../../../../src/plugins/saved_objects/public';
import { useMlKibana } from '../../../../../contexts/kibana';
const fixedPageSize: number = 8;
interface Props {
onClose: () => void;
}
export const SourceSelection: FC<Props> = ({ onClose }) => {
const { uiSettings, savedObjects } = useMlKibana().services;
const onSearchSelected = (id: string, type: string) => {
window.location.href = `ml#/data_frame_analytics/new_job?${
type === 'index-pattern' ? 'index' : 'savedSearchId'
}=${encodeURIComponent(id)}`;
};
return (
<>
<EuiOverlayMask>
<EuiModal
className="dataFrameAnalyticsCreateSearchDialog"
onClose={onClose}
data-test-subj="analyticsCreateSourceIndexModal"
>
<EuiModalHeader>
<EuiModalHeaderTitle>
<FormattedMessage
id="xpack.ml.dataframe.analytics.create.newAnalyticsTitle"
defaultMessage="New analytics job"
/>{' '}
/{' '}
<FormattedMessage
id="xpack.ml.dataframe.analytics.create.chooseSourceTitle"
defaultMessage="Choose a source index pattern"
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<SavedObjectFinderUi
key="searchSavedObjectFinder"
onChoose={onSearchSelected}
showFilter
noItemsMessage={i18n.translate(
'xpack.ml.dataFrame.analytics.create.searchSelection.notFoundLabel',
{
defaultMessage: 'No matching indices or saved searches found.',
}
)}
savedObjectMetaData={[
{
type: 'search',
getIconForSavedObject: () => 'search',
name: i18n.translate(
'xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.search',
{
defaultMessage: 'Saved search',
}
),
},
{
type: 'index-pattern',
getIconForSavedObject: () => 'indexPatternApp',
name: i18n.translate(
'xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.indexPattern',
{
defaultMessage: 'Index pattern',
}
),
},
]}
fixedPageSize={fixedPageSize}
uiSettings={uiSettings}
savedObjects={savedObjects}
/>
</EuiModalBody>
</EuiModal>
</EuiOverlayMask>
</>
);
};

View file

@ -72,6 +72,7 @@ export interface ActionDispatchers {
closeModal: () => void;
createAnalyticsJob: () => void;
openModal: () => Promise<void>;
initiateWizard: () => Promise<void>;
resetAdvancedEditorMessages: () => void;
setAdvancedEditorRawString: (payload: State['advancedEditorRawString']) => void;
setFormState: (payload: Partial<State['form']>) => void;

View file

@ -5,4 +5,9 @@
*/
export { DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES } from './state';
export { useCreateAnalyticsForm, CreateAnalyticsFormProps } from './use_create_analytics_form';
export {
AnalyticsCreationStep,
useCreateAnalyticsForm,
CreateAnalyticsFormProps,
CreateAnalyticsStepProps,
} from './use_create_analytics_form';

View file

@ -11,6 +11,7 @@ import { mlNodesAvailable } from '../../../../../ml_nodes_check';
import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
import {
FieldSelectionItem,
isClassificationAnalysis,
isRegressionAnalysis,
DataFrameAnalyticsId,
@ -26,7 +27,8 @@ export enum DEFAULT_MODEL_MEMORY_LIMIT {
classification = '100mb',
}
export const DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES = 2;
export const DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES = 0;
export const UNSET_CONFIG_ITEM = '--';
export type EsIndexName = string;
export type DependentVariable = string;
@ -47,6 +49,7 @@ export interface State {
advancedEditorMessages: FormMessage[];
advancedEditorRawString: string;
form: {
computeFeatureInfluence: string;
createIndexPattern: boolean;
dependentVariable: DependentVariable;
dependentVariableFetchFail: boolean;
@ -57,31 +60,47 @@ export interface State {
destinationIndexNameEmpty: boolean;
destinationIndexNameValid: boolean;
destinationIndexPatternTitleExists: boolean;
eta: undefined | number;
excludes: string[];
excludesTableItems: FieldSelectionItem[];
excludesOptions: EuiComboBoxOptionOption[];
featureBagFraction: undefined | number;
featureInfluenceThreshold: undefined | number;
fieldOptionsFetchFail: boolean;
gamma: undefined | number;
jobId: DataFrameAnalyticsId;
jobIdExists: boolean;
jobIdEmpty: boolean;
jobIdInvalidMaxLength: boolean;
jobIdValid: boolean;
jobType: AnalyticsJobType;
jobConfigQuery: any;
jobConfigQueryString: string | undefined;
lambda: number | undefined;
loadingDepVarOptions: boolean;
loadingFieldOptions: boolean;
maxDistinctValuesError: string | undefined;
maxTrees: undefined | number;
method: undefined | string;
modelMemoryLimit: string | undefined;
modelMemoryLimitUnitValid: boolean;
modelMemoryLimitValidationResult: any;
nNeighbors: undefined | number;
numTopFeatureImportanceValues: number | undefined;
numTopFeatureImportanceValuesValid: boolean;
numTopClasses: number;
outlierFraction: undefined | number;
predictionFieldName: undefined | string;
previousJobType: null | AnalyticsJobType;
previousSourceIndex: EsIndexName | undefined;
requiredFieldsError: string | undefined;
randomizeSeed: undefined | number;
sourceIndex: EsIndexName;
sourceIndexNameEmpty: boolean;
sourceIndexNameValid: boolean;
sourceIndexContainsNumericalFields: boolean;
sourceIndexFieldsCheckFailed: boolean;
standardizationEnabled: undefined | string;
trainingPercent: number;
};
disabled: boolean;
@ -105,7 +124,8 @@ export const getInitialState = (): State => ({
advancedEditorMessages: [],
advancedEditorRawString: '',
form: {
createIndexPattern: false,
computeFeatureInfluence: 'true',
createIndexPattern: true,
dependentVariable: '',
dependentVariableFetchFail: false,
dependentVariableOptions: [],
@ -115,8 +135,13 @@ export const getInitialState = (): State => ({
destinationIndexNameEmpty: true,
destinationIndexNameValid: false,
destinationIndexPatternTitleExists: false,
eta: undefined,
excludes: [],
featureBagFraction: undefined,
featureInfluenceThreshold: undefined,
fieldOptionsFetchFail: false,
gamma: undefined,
excludesTableItems: [],
excludesOptions: [],
jobId: '',
jobIdExists: false,
@ -124,22 +149,33 @@ export const getInitialState = (): State => ({
jobIdInvalidMaxLength: false,
jobIdValid: false,
jobType: undefined,
jobConfigQuery: { match_all: {} },
jobConfigQueryString: undefined,
lambda: undefined,
loadingDepVarOptions: false,
loadingFieldOptions: false,
maxDistinctValuesError: undefined,
maxTrees: undefined,
method: undefined,
modelMemoryLimit: undefined,
modelMemoryLimitUnitValid: true,
modelMemoryLimitValidationResult: null,
nNeighbors: undefined,
numTopFeatureImportanceValues: DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES,
numTopFeatureImportanceValuesValid: true,
numTopClasses: 2,
outlierFraction: undefined,
predictionFieldName: undefined,
previousJobType: null,
previousSourceIndex: undefined,
requiredFieldsError: undefined,
randomizeSeed: undefined,
sourceIndex: '',
sourceIndexNameEmpty: true,
sourceIndexNameValid: false,
sourceIndexContainsNumericalFields: true,
sourceIndexFieldsCheckFailed: false,
standardizationEnabled: 'true',
trainingPercent: 80,
},
jobConfig: {},
@ -222,6 +258,7 @@ export const getJobConfigFromFormState = (
index: formState.sourceIndex.includes(',')
? formState.sourceIndex.split(',').map((d) => d.trim())
: formState.sourceIndex,
query: formState.jobConfigQuery,
},
dest: {
index: formState.destinationIndex,
@ -239,13 +276,53 @@ export const getJobConfigFromFormState = (
formState.jobType === ANALYSIS_CONFIG_TYPE.REGRESSION ||
formState.jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION
) {
jobConfig.analysis = {
[formState.jobType]: {
dependent_variable: formState.dependentVariable,
num_top_feature_importance_values: formState.numTopFeatureImportanceValues,
training_percent: formState.trainingPercent,
},
let analysis = {
dependent_variable: formState.dependentVariable,
num_top_feature_importance_values: formState.numTopFeatureImportanceValues,
training_percent: formState.trainingPercent,
};
analysis = Object.assign(
analysis,
formState.predictionFieldName && { prediction_field_name: formState.predictionFieldName },
formState.eta && { eta: formState.eta },
formState.featureBagFraction && {
feature_bag_fraction: formState.featureBagFraction,
},
formState.gamma && { gamma: formState.gamma },
formState.lambda && { lambda: formState.lambda },
formState.maxTrees && { max_trees: formState.maxTrees },
formState.randomizeSeed && { randomize_seed: formState.randomizeSeed }
);
jobConfig.analysis = {
[formState.jobType]: analysis,
};
}
if (
formState.jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION &&
jobConfig?.analysis?.classification !== undefined &&
formState.numTopClasses !== undefined
) {
// @ts-ignore
jobConfig.analysis.classification.num_top_classes = formState.numTopClasses;
}
if (formState.jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION) {
const analysis = Object.assign(
{},
formState.method && { method: formState.method },
formState.nNeighbors && {
n_neighbors: formState.nNeighbors,
},
formState.outlierFraction && { outlier_fraction: formState.outlierFraction },
formState.standardizationEnabled && {
standardization_enabled: formState.standardizationEnabled,
}
);
// @ts-ignore
jobConfig.analysis.outlier_detection = analysis;
}
return jobConfig;
@ -279,6 +356,11 @@ export function getCloneFormStateFromJobConfig(
resultState.dependentVariable = analysisConfig.dependent_variable;
resultState.numTopFeatureImportanceValues = analysisConfig.num_top_feature_importance_values;
resultState.trainingPercent = analysisConfig.training_percent;
if (isClassificationAnalysis(analyticsJobConfig.analysis)) {
// @ts-ignore
resultState.numTopClasses = analysisConfig.num_top_classes;
}
}
return resultState;

View file

@ -36,11 +36,24 @@ import {
getCloneFormStateFromJobConfig,
} from './state';
import { ANALYTICS_STEPS } from '../../../analytics_creation/page';
export interface AnalyticsCreationStep {
number: ANALYTICS_STEPS;
completed: boolean;
}
export interface CreateAnalyticsFormProps {
actions: ActionDispatchers;
state: State;
}
export interface CreateAnalyticsStepProps extends CreateAnalyticsFormProps {
setCurrentStep: React.Dispatch<React.SetStateAction<any>>;
step?: ANALYTICS_STEPS;
stepActivated?: boolean;
}
export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => {
const mlContext = useMlContext();
const [state, dispatch] = useReducer(reducer, getInitialState());
@ -261,8 +274,12 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => {
dispatch({ type: ACTION.OPEN_MODAL });
};
const initiateWizard = async () => {
await mlContext.indexPatterns.clearCache();
await prepareFormValidation();
};
const startAnalyticsJob = async () => {
setIsModalButtonDisabled(true);
try {
const response = await ml.dataFrameAnalytics.startDataFrameAnalytics(jobId);
if (response.acknowledged !== true) {
@ -278,7 +295,6 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => {
),
});
setIsJobStarted(true);
setIsModalButtonDisabled(false);
refresh();
} catch (e) {
addRequestMessage({
@ -290,7 +306,6 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => {
}
),
});
setIsModalButtonDisabled(false);
}
};
@ -331,6 +346,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => {
closeModal,
createAnalyticsJob,
openModal,
initiateWizard,
resetAdvancedEditorMessages,
setAdvancedEditorRawString,
setFormState,

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { parse } from 'query-string';
import { MlRoute, PageLoader, PageProps } from '../../router';
import { useResolver } from '../../use_resolver';
import { basicResolvers } from '../../resolvers';
import { Page } from '../../../data_frame_analytics/pages/analytics_creation';
import { ML_BREADCRUMB } from '../../breadcrumbs';
const breadcrumbs = [
ML_BREADCRUMB,
{
text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameManagementLabel', {
defaultMessage: 'Data Frame Analytics',
}),
href: '#/data_frame_analytics',
},
];
export const analyticsJobsCreationRoute: MlRoute = {
path: '/data_frame_analytics/new_job',
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
breadcrumbs,
};
const PageWrapper: FC<PageProps> = ({ location, deps }) => {
const { index, savedSearchId }: Record<string, any> = parse(location.search, { sort: false });
const { context } = useResolver(index, savedSearchId, deps.config, basicResolvers(deps));
return (
<PageLoader context={context}>
<Page />
</PageLoader>
);
};

View file

@ -6,3 +6,4 @@
export * from './analytics_jobs_list';
export * from './analytics_job_exploration';
export * from './analytics_job_creation';

View file

@ -47,6 +47,7 @@ export const dataAnalyticsExplainSchema = schema.object({
/** Source */
source: schema.object({
index: schema.string(),
query: schema.maybe(schema.any()),
}),
analysis: schema.any(),
analyzed_fields: schema.maybe(schema.any()),

View file

@ -60,35 +60,19 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToDataFrameAnalytics();
});
it('loads the job creation flyout', async () => {
it('loads the source selection modal', async () => {
await ml.dataFrameAnalytics.startAnalyticsCreation();
});
it('selects the source data and loads the job wizard page', async () => {
ml.jobSourceSelection.selectSourceForAnalyticsJob(testData.source);
});
it('selects the job type', async () => {
await ml.dataFrameAnalyticsCreation.assertJobTypeSelectExists();
await ml.dataFrameAnalyticsCreation.selectJobType(testData.jobType);
});
it('inputs the job id', async () => {
await ml.dataFrameAnalyticsCreation.assertJobIdInputExists();
await ml.dataFrameAnalyticsCreation.setJobId(testData.jobId);
});
it('inputs the job description', async () => {
await ml.dataFrameAnalyticsCreation.assertJobDescriptionInputExists();
await ml.dataFrameAnalyticsCreation.setJobDescription(testData.jobDescription);
});
it('selects the source index', async () => {
await ml.dataFrameAnalyticsCreation.assertSourceIndexInputExists();
await ml.dataFrameAnalyticsCreation.selectSourceIndex(testData.source);
});
it('inputs the destination index', async () => {
await ml.dataFrameAnalyticsCreation.assertDestIndexInputExists();
await ml.dataFrameAnalyticsCreation.setDestIndex(testData.destinationIndex);
});
it('inputs the dependent variable', async () => {
await ml.dataFrameAnalyticsCreation.assertDependentVariableInputExists();
await ml.dataFrameAnalyticsCreation.selectDependentVariable(testData.dependentVariable);
@ -99,11 +83,34 @@ export default function ({ getService }: FtrProviderContext) {
await ml.dataFrameAnalyticsCreation.setTrainingPercent(testData.trainingPercent);
});
it('continues to the additional options step', async () => {
await ml.dataFrameAnalyticsCreation.continueToAdditionalOptionsStep();
});
it('inputs the model memory limit', async () => {
await ml.dataFrameAnalyticsCreation.assertModelMemoryInputExists();
await ml.dataFrameAnalyticsCreation.setModelMemory(testData.modelMemory);
});
it('continues to the details step', async () => {
await ml.dataFrameAnalyticsCreation.continueToDetailsStep();
});
it('inputs the job id', async () => {
await ml.dataFrameAnalyticsCreation.assertJobIdInputExists();
await ml.dataFrameAnalyticsCreation.setJobId(testData.jobId);
});
it('inputs the job description', async () => {
await ml.dataFrameAnalyticsCreation.assertJobDescriptionInputExists();
await ml.dataFrameAnalyticsCreation.setJobDescription(testData.jobDescription);
});
it('inputs the destination index', async () => {
await ml.dataFrameAnalyticsCreation.assertDestIndexInputExists();
await ml.dataFrameAnalyticsCreation.setDestIndex(testData.destinationIndex);
});
it('sets the create index pattern switch', async () => {
await ml.dataFrameAnalyticsCreation.assertCreateIndexPatternSwitchExists();
await ml.dataFrameAnalyticsCreation.setCreateIndexPatternSwitchState(
@ -111,26 +118,22 @@ export default function ({ getService }: FtrProviderContext) {
);
});
it('creates the analytics job', async () => {
it('continues to the create step', async () => {
await ml.dataFrameAnalyticsCreation.continueToCreateStep();
});
it('creates and starts the analytics job', async () => {
await ml.dataFrameAnalyticsCreation.assertCreateButtonExists();
await ml.dataFrameAnalyticsCreation.assertStartJobCheckboxCheckState(true);
await ml.dataFrameAnalyticsCreation.createAnalyticsJob(testData.jobId);
});
it('starts the analytics job', async () => {
await ml.dataFrameAnalyticsCreation.assertStartButtonExists();
await ml.dataFrameAnalyticsCreation.startAnalyticsJob();
});
it('closes the create job flyout', async () => {
await ml.dataFrameAnalyticsCreation.assertCloseButtonExists();
await ml.dataFrameAnalyticsCreation.closeCreateAnalyticsJobFlyout();
});
it('finishes analytics processing', async () => {
await ml.dataFrameAnalytics.waitForAnalyticsCompletion(testData.jobId);
});
it('displays the analytics table', async () => {
await ml.dataFrameAnalyticsCreation.navigateToJobManagementPage();
await ml.dataFrameAnalytics.assertAnalyticsTableExists();
});

View file

@ -182,16 +182,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.dataFrameAnalyticsCreation.createAnalyticsJob(cloneJobId);
});
it('should start the clone analytics job', async () => {
await ml.dataFrameAnalyticsCreation.assertStartButtonExists();
await ml.dataFrameAnalyticsCreation.startAnalyticsJob();
});
it('should close the create job flyout', async () => {
await ml.dataFrameAnalyticsCreation.assertCloseButtonExists();
await ml.dataFrameAnalyticsCreation.closeCreateAnalyticsJobFlyout();
});
it('finishes analytics processing', async () => {
await ml.dataFrameAnalytics.waitForAnalyticsCompletion(cloneJobId);
});

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

@ -58,10 +58,14 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToDataFrameAnalytics();
});
it('loads the job creation flyout', async () => {
it('loads the source selection modal', async () => {
await ml.dataFrameAnalytics.startAnalyticsCreation();
});
it('selects the source data and loads the job wizard page', async () => {
ml.jobSourceSelection.selectSourceForAnalyticsJob(testData.source);
});
it('selects the job type', async () => {
await ml.dataFrameAnalyticsCreation.assertJobTypeSelectExists();
await ml.dataFrameAnalyticsCreation.selectJobType(testData.jobType);
@ -75,6 +79,19 @@ export default function ({ getService }: FtrProviderContext) {
await ml.dataFrameAnalyticsCreation.assertTrainingPercentInputMissing();
});
it('continues to the additional options step', async () => {
await ml.dataFrameAnalyticsCreation.continueToAdditionalOptionsStep();
});
it('inputs the model memory limit', async () => {
await ml.dataFrameAnalyticsCreation.assertModelMemoryInputExists();
await ml.dataFrameAnalyticsCreation.setModelMemory(testData.modelMemory);
});
it('continues to the details step', async () => {
await ml.dataFrameAnalyticsCreation.continueToDetailsStep();
});
it('inputs the job id', async () => {
await ml.dataFrameAnalyticsCreation.assertJobIdInputExists();
await ml.dataFrameAnalyticsCreation.setJobId(testData.jobId);
@ -85,21 +102,11 @@ export default function ({ getService }: FtrProviderContext) {
await ml.dataFrameAnalyticsCreation.setJobDescription(testData.jobDescription);
});
it('selects the source index', async () => {
await ml.dataFrameAnalyticsCreation.assertSourceIndexInputExists();
await ml.dataFrameAnalyticsCreation.selectSourceIndex(testData.source);
});
it('inputs the destination index', async () => {
await ml.dataFrameAnalyticsCreation.assertDestIndexInputExists();
await ml.dataFrameAnalyticsCreation.setDestIndex(testData.destinationIndex);
});
it('inputs the model memory limit', async () => {
await ml.dataFrameAnalyticsCreation.assertModelMemoryInputExists();
await ml.dataFrameAnalyticsCreation.setModelMemory(testData.modelMemory);
});
it('sets the create index pattern switch', async () => {
await ml.dataFrameAnalyticsCreation.assertCreateIndexPatternSwitchExists();
await ml.dataFrameAnalyticsCreation.setCreateIndexPatternSwitchState(
@ -107,26 +114,22 @@ export default function ({ getService }: FtrProviderContext) {
);
});
it('creates the analytics job', async () => {
it('continues to the create step', async () => {
await ml.dataFrameAnalyticsCreation.continueToCreateStep();
});
it('creates and starts the analytics job', async () => {
await ml.dataFrameAnalyticsCreation.assertCreateButtonExists();
await ml.dataFrameAnalyticsCreation.assertStartJobCheckboxCheckState(true);
await ml.dataFrameAnalyticsCreation.createAnalyticsJob(testData.jobId);
});
it('starts the analytics job', async () => {
await ml.dataFrameAnalyticsCreation.assertStartButtonExists();
await ml.dataFrameAnalyticsCreation.startAnalyticsJob();
});
it('closes the create job flyout', async () => {
await ml.dataFrameAnalyticsCreation.assertCloseButtonExists();
await ml.dataFrameAnalyticsCreation.closeCreateAnalyticsJobFlyout();
});
it('finishes analytics processing', async () => {
await ml.dataFrameAnalytics.waitForAnalyticsCompletion(testData.jobId);
});
it('displays the analytics table', async () => {
await ml.dataFrameAnalyticsCreation.navigateToJobManagementPage();
await ml.dataFrameAnalytics.assertAnalyticsTableExists();
});

View file

@ -60,35 +60,19 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToDataFrameAnalytics();
});
it('loads the job creation flyout', async () => {
it('loads the source selection modal', async () => {
await ml.dataFrameAnalytics.startAnalyticsCreation();
});
it('selects the source data and loads the job wizard page', async () => {
ml.jobSourceSelection.selectSourceForAnalyticsJob(testData.source);
});
it('selects the job type', async () => {
await ml.dataFrameAnalyticsCreation.assertJobTypeSelectExists();
await ml.dataFrameAnalyticsCreation.selectJobType(testData.jobType);
});
it('inputs the job id', async () => {
await ml.dataFrameAnalyticsCreation.assertJobIdInputExists();
await ml.dataFrameAnalyticsCreation.setJobId(testData.jobId);
});
it('inputs the job description', async () => {
await ml.dataFrameAnalyticsCreation.assertJobDescriptionInputExists();
await ml.dataFrameAnalyticsCreation.setJobDescription(testData.jobDescription);
});
it('selects the source index', async () => {
await ml.dataFrameAnalyticsCreation.assertSourceIndexInputExists();
await ml.dataFrameAnalyticsCreation.selectSourceIndex(testData.source);
});
it('inputs the destination index', async () => {
await ml.dataFrameAnalyticsCreation.assertDestIndexInputExists();
await ml.dataFrameAnalyticsCreation.setDestIndex(testData.destinationIndex);
});
it('inputs the dependent variable', async () => {
await ml.dataFrameAnalyticsCreation.assertDependentVariableInputExists();
await ml.dataFrameAnalyticsCreation.selectDependentVariable(testData.dependentVariable);
@ -99,11 +83,34 @@ export default function ({ getService }: FtrProviderContext) {
await ml.dataFrameAnalyticsCreation.setTrainingPercent(testData.trainingPercent);
});
it('continues to the additional options step', async () => {
await ml.dataFrameAnalyticsCreation.continueToAdditionalOptionsStep();
});
it('inputs the model memory limit', async () => {
await ml.dataFrameAnalyticsCreation.assertModelMemoryInputExists();
await ml.dataFrameAnalyticsCreation.setModelMemory(testData.modelMemory);
});
it('continues to the details step', async () => {
await ml.dataFrameAnalyticsCreation.continueToDetailsStep();
});
it('inputs the job id', async () => {
await ml.dataFrameAnalyticsCreation.assertJobIdInputExists();
await ml.dataFrameAnalyticsCreation.setJobId(testData.jobId);
});
it('inputs the job description', async () => {
await ml.dataFrameAnalyticsCreation.assertJobDescriptionInputExists();
await ml.dataFrameAnalyticsCreation.setJobDescription(testData.jobDescription);
});
it('inputs the destination index', async () => {
await ml.dataFrameAnalyticsCreation.assertDestIndexInputExists();
await ml.dataFrameAnalyticsCreation.setDestIndex(testData.destinationIndex);
});
it('sets the create index pattern switch', async () => {
await ml.dataFrameAnalyticsCreation.assertCreateIndexPatternSwitchExists();
await ml.dataFrameAnalyticsCreation.setCreateIndexPatternSwitchState(
@ -111,26 +118,22 @@ export default function ({ getService }: FtrProviderContext) {
);
});
it('creates the analytics job', async () => {
it('continues to the create step', async () => {
await ml.dataFrameAnalyticsCreation.continueToCreateStep();
});
it('creates and starts the analytics job', async () => {
await ml.dataFrameAnalyticsCreation.assertCreateButtonExists();
await ml.dataFrameAnalyticsCreation.assertStartJobCheckboxCheckState(true);
await ml.dataFrameAnalyticsCreation.createAnalyticsJob(testData.jobId);
});
it('starts the analytics job', async () => {
await ml.dataFrameAnalyticsCreation.assertStartButtonExists();
await ml.dataFrameAnalyticsCreation.startAnalyticsJob();
});
it('closes the create job flyout', async () => {
await ml.dataFrameAnalyticsCreation.assertCloseButtonExists();
await ml.dataFrameAnalyticsCreation.closeCreateAnalyticsJobFlyout();
});
it('finishes analytics processing', async () => {
await ml.dataFrameAnalytics.waitForAnalyticsCompletion(testData.jobId);
});
it('displays the analytics table', async () => {
await ml.dataFrameAnalyticsCreation.navigateToJobManagementPage();
await ml.dataFrameAnalytics.assertAnalyticsTableExists();
});

View file

@ -214,22 +214,60 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) {
);
},
async getAnalyticsState(analyticsId: string): Promise<DATA_FRAME_TASK_STATE> {
log.debug(`Fetching analytics state for job ${analyticsId}`);
async getDFAJobStats(analyticsId: string): Promise<any> {
log.debug(`Fetching data frame analytics job stats for job ${analyticsId}...`);
const analyticsStats = await esSupertest
.get(`/_ml/data_frame/analytics/${analyticsId}/_stats`)
.expect(200)
.then((res: any) => res.body);
return analyticsStats;
},
async getAnalyticsState(analyticsId: string): Promise<DATA_FRAME_TASK_STATE> {
log.debug(`Fetching analytics state for job ${analyticsId}`);
const analyticsStats = await this.getDFAJobStats(analyticsId);
expect(analyticsStats.data_frame_analytics).to.have.length(
1,
`Expected dataframe analytics stats to have exactly one object (got '${analyticsStats.data_frame_analytics.length}')`
);
const state: DATA_FRAME_TASK_STATE = analyticsStats.data_frame_analytics[0].state;
return state;
},
async getDFAJobTrainingRecordCount(analyticsId: string): Promise<number> {
const analyticsStats = await this.getDFAJobStats(analyticsId);
expect(analyticsStats.data_frame_analytics).to.have.length(
1,
`Expected dataframe analytics stats to have exactly one object (got '${analyticsStats.data_frame_analytics.length}')`
);
const trainingRecordCount: number =
analyticsStats.data_frame_analytics[0].data_counts.training_docs_count;
return trainingRecordCount;
},
async waitForDFAJobTrainingRecordCountToBePositive(analyticsId: string) {
await retry.waitForWithTimeout(
`'${analyticsId}' to have training_docs_count > 0`,
10 * 1000,
async () => {
const trainingRecordCount = await this.getDFAJobTrainingRecordCount(analyticsId);
if (trainingRecordCount > 0) {
return true;
} else {
throw new Error(
`expected data frame analytics job '${analyticsId}' to have training_docs_count > 0 (got ${trainingRecordCount})`
);
}
}
);
},
async waitForAnalyticsState(
analyticsId: string,
expectedAnalyticsState: DATA_FRAME_TASK_STATE

View file

@ -67,10 +67,11 @@ export function MachineLearningDataFrameAnalyticsProvider(
} else {
await testSubjects.click('mlAnalyticsButtonCreate');
}
await testSubjects.existOrFail('mlAnalyticsCreateJobFlyout');
await testSubjects.existOrFail('analyticsCreateSourceIndexModal');
},
async waitForAnalyticsCompletion(analyticsId: string) {
await mlApi.waitForDFAJobTrainingRecordCountToBePositive(analyticsId);
await mlApi.waitForAnalyticsState(analyticsId, DATA_FRAME_TASK_STATE.STOPPED);
},
};

View file

@ -42,12 +42,12 @@ export function MachineLearningDataFrameAnalyticsCreationProvider(
return {
async assertJobTypeSelectExists() {
await testSubjects.existOrFail('mlAnalyticsCreateJobFlyoutJobTypeSelect');
await testSubjects.existOrFail('mlAnalyticsCreateJobWizardJobTypeSelect');
},
async assertJobTypeSelection(expectedSelection: string) {
const actualSelection = await testSubjects.getAttribute(
'mlAnalyticsCreateJobFlyoutJobTypeSelect',
'mlAnalyticsCreateJobWizardJobTypeSelect',
'value'
);
expect(actualSelection).to.eql(
@ -57,12 +57,13 @@ export function MachineLearningDataFrameAnalyticsCreationProvider(
},
async selectJobType(jobType: string) {
await testSubjects.selectValue('mlAnalyticsCreateJobFlyoutJobTypeSelect', jobType);
await testSubjects.click('mlAnalyticsCreateJobWizardJobTypeSelect');
await testSubjects.click(`mlAnalyticsCreation-${jobType}-option`);
await this.assertJobTypeSelection(jobType);
},
async assertAdvancedEditorSwitchExists() {
await testSubjects.existOrFail(`mlAnalyticsCreateJobFlyoutAdvancedEditorSwitch`, {
await testSubjects.existOrFail(`mlAnalyticsCreateJobWizardAdvancedEditorSwitch`, {
allowHidden: true,
});
},
@ -70,7 +71,7 @@ export function MachineLearningDataFrameAnalyticsCreationProvider(
async assertAdvancedEditorSwitchCheckState(expectedCheckState: boolean) {
const actualCheckState =
(await testSubjects.getAttribute(
'mlAnalyticsCreateJobFlyoutAdvancedEditorSwitch',
'mlAnalyticsCreateJobWizardAdvancedEditorSwitch',
'aria-checked'
)) === 'true';
expect(actualCheckState).to.eql(
@ -182,20 +183,22 @@ export function MachineLearningDataFrameAnalyticsCreationProvider(
},
async assertDependentVariableInputExists() {
await testSubjects.existOrFail(
'mlAnalyticsCreateJobFlyoutDependentVariableSelect > comboBoxInput'
);
await retry.tryForTime(8000, async () => {
await testSubjects.existOrFail(
'mlAnalyticsCreateJobWizardDependentVariableSelect > comboBoxInput'
);
});
},
async assertDependentVariableInputMissing() {
await testSubjects.missingOrFail(
'mlAnalyticsCreateJobFlyoutDependentVariableSelect > comboBoxInput'
'mlAnalyticsCreateJobWizardDependentVariableSelect > comboBoxInput'
);
},
async assertDependentVariableSelection(expectedSelection: string[]) {
const actualSelection = await comboBox.getComboBoxSelectedOptions(
'mlAnalyticsCreateJobFlyoutDependentVariableSelect > comboBoxInput'
'mlAnalyticsCreateJobWizardDependentVariableSelect > comboBoxInput'
);
expect(actualSelection).to.eql(
expectedSelection,
@ -205,23 +208,23 @@ export function MachineLearningDataFrameAnalyticsCreationProvider(
async selectDependentVariable(dependentVariable: string) {
await comboBox.set(
'mlAnalyticsCreateJobFlyoutDependentVariableSelect > comboBoxInput',
'mlAnalyticsCreateJobWizardDependentVariableSelect > comboBoxInput',
dependentVariable
);
await this.assertDependentVariableSelection([dependentVariable]);
},
async assertTrainingPercentInputExists() {
await testSubjects.existOrFail('mlAnalyticsCreateJobFlyoutTrainingPercentSlider');
await testSubjects.existOrFail('mlAnalyticsCreateJobWizardTrainingPercentSlider');
},
async assertTrainingPercentInputMissing() {
await testSubjects.missingOrFail('mlAnalyticsCreateJobFlyoutTrainingPercentSlider');
await testSubjects.missingOrFail('mlAnalyticsCreateJobWizardTrainingPercentSlider');
},
async assertTrainingPercentValue(expectedValue: string) {
const actualTrainingPercent = await testSubjects.getAttribute(
'mlAnalyticsCreateJobFlyoutTrainingPercentSlider',
'mlAnalyticsCreateJobWizardTrainingPercentSlider',
'value'
);
expect(actualTrainingPercent).to.eql(
@ -231,7 +234,7 @@ export function MachineLearningDataFrameAnalyticsCreationProvider(
},
async setTrainingPercent(trainingPercent: string) {
const slider = await testSubjects.find('mlAnalyticsCreateJobFlyoutTrainingPercentSlider');
const slider = await testSubjects.find('mlAnalyticsCreateJobWizardTrainingPercentSlider');
let currentValue = await slider.getAttribute('value');
let currentDiff = +currentValue - +trainingPercent;
@ -271,13 +274,28 @@ export function MachineLearningDataFrameAnalyticsCreationProvider(
await this.assertTrainingPercentValue(trainingPercent);
},
async continueToAdditionalOptionsStep() {
await testSubjects.click('mlAnalyticsCreateJobWizardContinueButton');
await testSubjects.existOrFail('mlAnalyticsCreateJobWizardAdvancedStep');
},
async continueToDetailsStep() {
await testSubjects.click('mlAnalyticsCreateJobWizardContinueButton');
await testSubjects.existOrFail('mlAnalyticsCreateJobWizardDetailsStep');
},
async continueToCreateStep() {
await testSubjects.click('mlAnalyticsCreateJobWizardContinueButton');
await testSubjects.existOrFail('mlAnalyticsCreateJobWizardCreateStep');
},
async assertModelMemoryInputExists() {
await testSubjects.existOrFail('mlAnalyticsCreateJobFlyoutModelMemoryInput');
await testSubjects.existOrFail('mlAnalyticsCreateJobWizardModelMemoryInput');
},
async assertModelMemoryValue(expectedValue: string) {
const actualModelMemory = await testSubjects.getAttribute(
'mlAnalyticsCreateJobFlyoutModelMemoryInput',
'mlAnalyticsCreateJobWizardModelMemoryInput',
'value'
);
expect(actualModelMemory).to.eql(
@ -289,7 +307,7 @@ export function MachineLearningDataFrameAnalyticsCreationProvider(
async setModelMemory(modelMemory: string) {
await retry.tryForTime(15 * 1000, async () => {
await mlCommon.setValueWithChecks(
'mlAnalyticsCreateJobFlyoutModelMemoryInput',
'mlAnalyticsCreateJobWizardModelMemoryInput',
modelMemory,
{
clearWithKeyboard: true,
@ -300,14 +318,14 @@ export function MachineLearningDataFrameAnalyticsCreationProvider(
},
async assertCreateIndexPatternSwitchExists() {
await testSubjects.existOrFail(`mlAnalyticsCreateJobFlyoutCreateIndexPatternSwitch`, {
await testSubjects.existOrFail(`mlAnalyticsCreateJobWizardCreateIndexPatternSwitch`, {
allowHidden: true,
});
},
async getCreateIndexPatternSwitchCheckState(): Promise<boolean> {
const state = await testSubjects.getAttribute(
'mlAnalyticsCreateJobFlyoutCreateIndexPatternSwitch',
'mlAnalyticsCreateJobWizardCreateIndexPatternSwitch',
'aria-checked'
);
return state === 'true';
@ -323,58 +341,46 @@ export function MachineLearningDataFrameAnalyticsCreationProvider(
async setCreateIndexPatternSwitchState(checkState: boolean) {
if ((await this.getCreateIndexPatternSwitchCheckState()) !== checkState) {
await testSubjects.click('mlAnalyticsCreateJobFlyoutCreateIndexPatternSwitch');
await testSubjects.click('mlAnalyticsCreateJobWizardCreateIndexPatternSwitch');
}
await this.assertCreateIndexPatternSwitchCheckState(checkState);
},
async assertCreateButtonExists() {
await testSubjects.existOrFail('mlAnalyticsCreateJobFlyoutCreateButton');
async assertStartJobCheckboxExists() {
await testSubjects.existOrFail('mlAnalyticsCreateJobWizardStartJobCheckbox');
},
async assertCreateButtonMissing() {
await testSubjects.missingOrFail('mlAnalyticsCreateJobFlyoutCreateButton');
async assertStartJobCheckboxCheckState(expectedCheckState: boolean) {
const actualCheckState =
(await testSubjects.getAttribute(
'mlAnalyticsCreateJobWizardStartJobCheckbox',
'checked'
)) === 'true';
expect(actualCheckState).to.eql(
expectedCheckState,
`Start job check state should be ${expectedCheckState} (got ${actualCheckState})`
);
},
async assertCreateButtonExists() {
await testSubjects.existOrFail('mlAnalyticsCreateJobWizardCreateButton');
},
async isCreateButtonDisabled() {
const isEnabled = await testSubjects.isEnabled('mlAnalyticsCreateJobFlyoutCreateButton');
const isEnabled = await testSubjects.isEnabled('mlAnalyticsCreateJobWizardCreateButton');
return !isEnabled;
},
async createAnalyticsJob(analyticsId: string) {
await testSubjects.click('mlAnalyticsCreateJobFlyoutCreateButton');
await testSubjects.click('mlAnalyticsCreateJobWizardCreateButton');
await retry.tryForTime(5000, async () => {
await this.assertCreateButtonMissing();
await this.assertStartButtonExists();
await this.assertBackToManagementCardExists();
});
await mlApi.waitForDataFrameAnalyticsJobToExist(analyticsId);
},
async assertStartButtonExists() {
await testSubjects.existOrFail('mlAnalyticsCreateJobFlyoutStartButton');
},
async assertStartButtonMissing() {
await testSubjects.missingOrFail('mlAnalyticsCreateJobFlyoutStartButton');
},
async startAnalyticsJob() {
await testSubjects.click('mlAnalyticsCreateJobFlyoutStartButton');
await retry.tryForTime(5000, async () => {
await this.assertStartButtonMissing();
await this.assertCloseButtonExists();
});
},
async assertCloseButtonExists() {
await testSubjects.existOrFail('mlAnalyticsCreateJobFlyoutCloseButton');
},
async closeCreateAnalyticsJobFlyout() {
await retry.tryForTime(10 * 1000, async () => {
await testSubjects.click('mlAnalyticsCreateJobFlyoutCloseButton');
await testSubjects.missingOrFail('mlAnalyticsCreateJobFlyout');
});
async assertBackToManagementCardExists() {
await testSubjects.existOrFail('analyticsWizardCardManagement');
},
async getHeaderText() {
@ -395,5 +401,19 @@ export function MachineLearningDataFrameAnalyticsCreationProvider(
await this.assertExcludedFieldsSelection(job.analyzed_fields.excludes);
await this.assertModelMemoryValue(job.model_memory_limit);
},
async assertCreationCalloutMessagesExist() {
await testSubjects.existOrFail('analyticsWizardCreationCallout_0');
await testSubjects.existOrFail('analyticsWizardCreationCallout_1');
await testSubjects.existOrFail('analyticsWizardCreationCallout_2');
},
async navigateToJobManagementPage() {
await retry.tryForTime(5000, async () => {
await this.assertCreationCalloutMessagesExist();
});
await testSubjects.click('analyticsWizardCardManagement');
await testSubjects.existOrFail('mlPageDataFrameAnalytics');
},
};
}

View file

@ -105,6 +105,7 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F
}
public async assertAnalyticsRowFields(analyticsId: string, expectedRow: object) {
await this.refreshAnalyticsTable();
const rows = await this.parseAnalyticsTable();
const analyticsRow = rows.filter((row) => row.id === analyticsId)[0];
expect(analyticsRow).to.eql(

View file

@ -34,6 +34,10 @@ export function MachineLearningJobSourceSelectionProvider({ getService }: FtrPro
await this.selectSource(sourceName, 'mlPageJobTypeSelection');
},
async selectSourceForAnalyticsJob(sourceName: string) {
await this.selectSource(sourceName, 'mlAnalyticsCreationContainer');
},
async selectSourceForIndexBasedDataVisualizer(sourceName: string) {
await this.selectSource(sourceName, 'mlPageIndexDataVisualizer');
},