mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* 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:
parent
2632b6c752
commit
d6028c1f14
62 changed files with 3605 additions and 323 deletions
|
@ -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,
|
||||
};
|
|
@ -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}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
};
|
||||
|
|
|
@ -17,6 +17,7 @@ export {
|
|||
IndexPattern,
|
||||
REFRESH_ANALYTICS_LIST_STATE,
|
||||
ANALYSIS_CONFIG_TYPE,
|
||||
OUTLIER_ANALYSIS_METHOD,
|
||||
RegressionEvaluateResponse,
|
||||
getValuesFromResponse,
|
||||
loadEvalData,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
|
@ -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';
|
|
@ -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
|
||||
);
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
);
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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';
|
|
@ -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';
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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) {
|
||||
|
|
|
@ -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} />;
|
||||
// },
|
||||
// },
|
||||
];
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.dataFrameAnalyticsCreateSearchDialog {
|
||||
width: $euiSizeL * 30;
|
||||
min-height: $euiSizeL * 25;
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -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';
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -6,3 +6,4 @@
|
|||
|
||||
export * from './analytics_jobs_list';
|
||||
export * from './analytics_job_exploration';
|
||||
export * from './analytics_job_creation';
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue