mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* Add regression job type option and fields * fetch dependent variable options when regression * convert modal to flyout.disable delete if job not stopped * update modelMemoryLimit for regression * ensure eventRateFieldId not included in numerical fields * add evaluateDataFrameAnalyticsRegression endpoint * create regression result page generalization and training error panel * update advancedEditorToggle help text * remove unused translations * rename creationModal test to creationFlyout * Updates from PR feedback
This commit is contained in:
parent
c73a4d1204
commit
5f3faad168
32 changed files with 874 additions and 175 deletions
|
@ -1,3 +1,4 @@
|
|||
@import 'pages/analytics_exploration/components/exploration/index';
|
||||
@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';
|
||||
|
|
|
@ -24,13 +24,24 @@ interface RegressionAnalysis {
|
|||
};
|
||||
}
|
||||
|
||||
export interface RegressionEvaluateResponse {
|
||||
regression: {
|
||||
mean_squared_error: {
|
||||
error: number;
|
||||
};
|
||||
r_squared: {
|
||||
value: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface GenericAnalysis {
|
||||
[key: string]: Record<string, any>;
|
||||
}
|
||||
|
||||
type AnalysisConfig = OutlierAnalysis | RegressionAnalysis | GenericAnalysis;
|
||||
|
||||
enum ANALYSIS_CONFIG_TYPE {
|
||||
export enum ANALYSIS_CONFIG_TYPE {
|
||||
OUTLIER_DETECTION = 'outlier_detection',
|
||||
REGRESSION = 'regression',
|
||||
UNKNOWN = 'unknown',
|
||||
|
@ -46,11 +57,24 @@ export const getAnalysisType = (analysis: AnalysisConfig) => {
|
|||
return ANALYSIS_CONFIG_TYPE.UNKNOWN;
|
||||
};
|
||||
|
||||
export const getDependentVar = (analysis: AnalysisConfig) => {
|
||||
let depVar;
|
||||
if (isRegressionAnalysis(analysis)) {
|
||||
depVar = analysis.regression.dependent_variable;
|
||||
}
|
||||
return depVar;
|
||||
};
|
||||
|
||||
export const isOutlierAnalysis = (arg: any): arg is OutlierAnalysis => {
|
||||
const keys = Object.keys(arg);
|
||||
return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION;
|
||||
};
|
||||
|
||||
export const isRegressionAnalysis = (arg: any): arg is RegressionAnalysis => {
|
||||
const keys = Object.keys(arg);
|
||||
return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION;
|
||||
};
|
||||
|
||||
export interface DataFrameAnalyticsConfig {
|
||||
id: DataFrameAnalyticsId;
|
||||
// Description attribute is not supported yet
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
export {
|
||||
getAnalysisType,
|
||||
getDependentVar,
|
||||
isOutlierAnalysis,
|
||||
refreshAnalyticsList$,
|
||||
useRefreshAnalyticsList,
|
||||
|
@ -14,6 +15,8 @@ export {
|
|||
IndexName,
|
||||
IndexPattern,
|
||||
REFRESH_ANALYTICS_LIST_STATE,
|
||||
ANALYSIS_CONFIG_TYPE,
|
||||
RegressionEvaluateResponse,
|
||||
} from './analytics';
|
||||
|
||||
export {
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { EuiCallOut } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export const ErrorCallout: FC<Props> = ({ error }) => {
|
||||
let errorCallout = (
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.generalError', {
|
||||
defaultMessage: 'An error occurred loading the data.',
|
||||
})}
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
>
|
||||
<p>{error}</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
// Job was created but not started so the destination index has not been created
|
||||
if (error.includes('index_not_found')) {
|
||||
errorCallout = (
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.evaluateError', {
|
||||
defaultMessage: 'An error occurred loading the data.',
|
||||
})}
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
>
|
||||
<p>
|
||||
{i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.noIndexCalloutBody', {
|
||||
defaultMessage:
|
||||
'The query for the index returned no results. Please make sure the destination index exists and contains documents.',
|
||||
})}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
} else if (error.includes('No documents found')) {
|
||||
// Job was started but no results have been written yet
|
||||
errorCallout = (
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutTitle',
|
||||
{
|
||||
defaultMessage: 'Empty index query result.',
|
||||
}
|
||||
)}
|
||||
color="primary"
|
||||
>
|
||||
<p>
|
||||
{i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody', {
|
||||
defaultMessage:
|
||||
'The query for the index returned no results. Please make sure the job has been completed and the index contains documents.',
|
||||
})}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
|
||||
return <Fragment>{errorCallout}</Fragment>;
|
||||
};
|
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
* 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, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiStat, EuiTitle } from '@elastic/eui';
|
||||
import { idx } from '@kbn/elastic-idx';
|
||||
import { ml } from '../../../../../services/ml_api_service';
|
||||
import { getErrorMessage } from '../../../analytics_management/hooks/use_create_analytics_form';
|
||||
import { RegressionEvaluateResponse } from '../../../../common';
|
||||
|
||||
import { ErrorCallout } from './error_callout';
|
||||
|
||||
interface Props {
|
||||
jobId: string;
|
||||
index: string;
|
||||
dependentVariable: string;
|
||||
}
|
||||
|
||||
interface LoadEvaluateResult {
|
||||
success: boolean;
|
||||
eval: RegressionEvaluateResponse | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface Eval {
|
||||
meanSquaredError: number | '';
|
||||
rSquared: number | '';
|
||||
error: null | string;
|
||||
}
|
||||
|
||||
const meanSquaredErrorText = i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText',
|
||||
{
|
||||
defaultMessage: 'Mean squared error',
|
||||
}
|
||||
);
|
||||
const rSquaredText = i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.regressionExploration.rSquaredText',
|
||||
{
|
||||
defaultMessage: 'R squared',
|
||||
}
|
||||
);
|
||||
const defaultEval: Eval = { meanSquaredError: '', rSquared: '', error: null };
|
||||
const DEFAULT_SIG_FIGS = 3;
|
||||
|
||||
function getValuesFromResponse(response: RegressionEvaluateResponse) {
|
||||
let meanSquaredError = idx(response, _ => _.regression.mean_squared_error.error) as number;
|
||||
if (meanSquaredError) {
|
||||
meanSquaredError = Number(meanSquaredError.toPrecision(DEFAULT_SIG_FIGS));
|
||||
}
|
||||
|
||||
let rSquared = idx(response, _ => _.regression.r_squared.value) as number;
|
||||
if (rSquared) {
|
||||
rSquared = Number(rSquared.toPrecision(DEFAULT_SIG_FIGS));
|
||||
}
|
||||
|
||||
return { meanSquaredError, rSquared };
|
||||
}
|
||||
|
||||
export const EvaluatePanel: FC<Props> = ({ jobId, index, dependentVariable }) => {
|
||||
const [trainingEval, setTrainingEval] = useState<Eval>(defaultEval);
|
||||
const [generalizationEval, setGeneralizationEval] = useState<Eval>(defaultEval);
|
||||
const [isLoadingTraining, setIsLoadingTraining] = useState<boolean>(false);
|
||||
const [isLoadingGeneralization, setIsLoadingGeneralization] = useState<boolean>(false);
|
||||
|
||||
const loadEvalData = async (isTraining: boolean) => {
|
||||
const results: LoadEvaluateResult = { success: false, eval: null, error: null };
|
||||
|
||||
const config = {
|
||||
index,
|
||||
query: {
|
||||
term: {
|
||||
'ml.is_training': {
|
||||
value: isTraining,
|
||||
},
|
||||
},
|
||||
},
|
||||
evaluation: {
|
||||
regression: {
|
||||
actual_field: dependentVariable,
|
||||
predicted_field: `ml.${dependentVariable}_prediction`,
|
||||
metrics: {
|
||||
r_squared: {},
|
||||
mean_squared_error: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const evalResult = await ml.dataFrameAnalytics.evaluateDataFrameAnalytics(config);
|
||||
results.success = true;
|
||||
results.eval = evalResult;
|
||||
return results;
|
||||
} catch (e) {
|
||||
results.error = getErrorMessage(e);
|
||||
return results;
|
||||
}
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
setIsLoadingGeneralization(true);
|
||||
setIsLoadingTraining(true);
|
||||
|
||||
const genErrorEval = await loadEvalData(false);
|
||||
|
||||
if (genErrorEval.success === true && genErrorEval.eval) {
|
||||
const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval);
|
||||
setGeneralizationEval({
|
||||
meanSquaredError,
|
||||
rSquared,
|
||||
error: null,
|
||||
});
|
||||
setIsLoadingGeneralization(false);
|
||||
} else {
|
||||
setIsLoadingGeneralization(false);
|
||||
setGeneralizationEval({
|
||||
meanSquaredError: '',
|
||||
rSquared: '',
|
||||
error: genErrorEval.error,
|
||||
});
|
||||
}
|
||||
|
||||
const trainingErrorEval = await loadEvalData(true);
|
||||
|
||||
if (trainingErrorEval.success === true && trainingErrorEval.eval) {
|
||||
const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval);
|
||||
setTrainingEval({
|
||||
meanSquaredError,
|
||||
rSquared,
|
||||
error: null,
|
||||
});
|
||||
setIsLoadingTraining(false);
|
||||
} else {
|
||||
setIsLoadingTraining(false);
|
||||
setTrainingEval({
|
||||
meanSquaredError: '',
|
||||
rSquared: '',
|
||||
error: genErrorEval.error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
<span>
|
||||
{i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.jobIdTitle', {
|
||||
defaultMessage: 'Job ID {jobId}',
|
||||
values: { jobId },
|
||||
})}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s">
|
||||
<span>
|
||||
{i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle',
|
||||
{
|
||||
defaultMessage: 'Generalization error',
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
{generalizationEval.error !== null && <ErrorCallout error={generalizationEval.error} />}
|
||||
{generalizationEval.error === null && (
|
||||
<Fragment>
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
reverse
|
||||
isLoading={isLoadingGeneralization}
|
||||
title={generalizationEval.meanSquaredError}
|
||||
description={meanSquaredErrorText}
|
||||
titleSize="m"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
reverse
|
||||
isLoading={isLoadingGeneralization}
|
||||
title={generalizationEval.rSquared}
|
||||
description={rSquaredText}
|
||||
titleSize="m"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</Fragment>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s">
|
||||
<span>
|
||||
{i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.regressionExploration.trainingErrorTitle',
|
||||
{
|
||||
defaultMessage: 'Training error',
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
{trainingEval.error !== null && <ErrorCallout error={trainingEval.error} />}
|
||||
{trainingEval.error === null && (
|
||||
<Fragment>
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
reverse
|
||||
isLoading={isLoadingTraining}
|
||||
title={trainingEval.meanSquaredError}
|
||||
description={meanSquaredErrorText}
|
||||
titleSize="m"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
reverse
|
||||
isLoading={isLoadingTraining}
|
||||
title={trainingEval.rSquared}
|
||||
description={rSquaredText}
|
||||
titleSize="m"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</Fragment>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { CreateAnalyticsModal } from './create_analytics_modal';
|
||||
export { RegressionExploration } from './regression_exploration';
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { EvaluatePanel } from './evaluate_panel';
|
||||
// import { ResultsTable } from './results_table';
|
||||
|
||||
interface Props {
|
||||
jobId: string;
|
||||
destIndex: string;
|
||||
dependentVariable: string;
|
||||
}
|
||||
|
||||
export const RegressionExploration: FC<Props> = ({ jobId, destIndex, dependentVariable }) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<EvaluatePanel jobId={jobId} index={destIndex} dependentVariable={dependentVariable} />
|
||||
<EuiSpacer />
|
||||
{/* <ResultsTable jobId={jobId} /> */}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -50,7 +50,12 @@ module.directive('mlDataFrameAnalyticsExploration', ($injector: InjectorService)
|
|||
ReactDOM.render(
|
||||
<I18nContext>
|
||||
<KibanaContext.Provider value={kibanaContext}>
|
||||
<Page jobId={globalState.ml.jobId} />
|
||||
<Page
|
||||
jobId={globalState.ml.jobId}
|
||||
analysisType={globalState.ml.analysisType}
|
||||
destIndex={globalState.ml.destIndex}
|
||||
depVar={globalState.ml.depVar}
|
||||
/>
|
||||
</KibanaContext.Provider>
|
||||
</I18nContext>,
|
||||
element[0]
|
||||
|
|
|
@ -23,8 +23,16 @@ import {
|
|||
import { NavigationMenu } from '../../../components/navigation_menu/navigation_menu';
|
||||
|
||||
import { Exploration } from './components/exploration';
|
||||
import { RegressionExploration } from './components/regression_exploration';
|
||||
|
||||
export const Page: FC<{ jobId: string }> = ({ jobId }) => (
|
||||
import { ANALYSIS_CONFIG_TYPE } from '../../common/analytics';
|
||||
|
||||
export const Page: FC<{
|
||||
jobId: string;
|
||||
analysisType: string;
|
||||
destIndex: string;
|
||||
depVar: string;
|
||||
}> = ({ jobId, analysisType, destIndex, depVar }) => (
|
||||
<Fragment>
|
||||
<NavigationMenu tabId="data_frame_analytics" />
|
||||
<EuiPage data-test-subj="mlPageDataFrameAnalyticsExploration">
|
||||
|
@ -58,7 +66,10 @@ export const Page: FC<{ jobId: string }> = ({ jobId }) => (
|
|||
</EuiPageContentHeader>
|
||||
<EuiPageContentBody>
|
||||
<EuiSpacer size="l" />
|
||||
<Exploration jobId={jobId} />
|
||||
{analysisType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION && <Exploration jobId={jobId} />}
|
||||
{analysisType === ANALYSIS_CONFIG_TYPE.REGRESSION && (
|
||||
<RegressionExploration jobId={jobId} destIndex={destIndex} dependentVariable={depVar} />
|
||||
)}
|
||||
</EuiPageContentBody>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
|
|
|
@ -28,7 +28,10 @@ interface DeleteActionProps {
|
|||
}
|
||||
|
||||
export const DeleteAction: FC<DeleteActionProps> = ({ item }) => {
|
||||
const disabled = item.stats.state === DATA_FRAME_TASK_STATE.STARTED;
|
||||
const disabled =
|
||||
item.stats.state === DATA_FRAME_TASK_STATE.STARTED ||
|
||||
item.stats.state === DATA_FRAME_TASK_STATE.ANALYZING ||
|
||||
item.stats.state === DATA_FRAME_TASK_STATE.REINDEXING;
|
||||
|
||||
const canDeleteDataFrameAnalytics: boolean = checkPermission('canDeleteDataFrameAnalytics');
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
createPermissionFailureMessage,
|
||||
} from '../../../../../privilege/check_privilege';
|
||||
|
||||
import { isOutlierAnalysis } from '../../../../common/analytics';
|
||||
import { getAnalysisType, getDependentVar } from '../../../../common/analytics';
|
||||
|
||||
import { getResultsUrl, isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common';
|
||||
import { stopAnalytics } from '../../services/analytics_service';
|
||||
|
@ -24,10 +24,15 @@ import { DeleteAction } from './action_delete';
|
|||
export const AnalyticsViewAction = {
|
||||
isPrimary: true,
|
||||
render: (item: DataFrameAnalyticsListRow) => {
|
||||
const analysisType = getAnalysisType(item.config.analysis);
|
||||
const destIndex = item.config.dest.index;
|
||||
const dependentVariable = getDependentVar(item.config.analysis);
|
||||
|
||||
const url = getResultsUrl(item.id, analysisType, destIndex, dependentVariable);
|
||||
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
disabled={!isOutlierAnalysis(item.config.analysis)}
|
||||
onClick={() => (window.location.href = getResultsUrl(item.id))}
|
||||
onClick={() => (window.location.href = url)}
|
||||
size="xs"
|
||||
color="text"
|
||||
iconType="visTable"
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { DataFrameAnalyticsId, DataFrameAnalyticsConfig } from '../../../../common';
|
||||
import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics';
|
||||
|
||||
export enum DATA_FRAME_TASK_STATE {
|
||||
ANALYZING = 'analyzing',
|
||||
|
@ -107,6 +108,17 @@ export function isCompletedAnalyticsJob(stats: DataFrameAnalyticsStats) {
|
|||
return stats.state === DATA_FRAME_TASK_STATE.STOPPED && progress === 100;
|
||||
}
|
||||
|
||||
export function getResultsUrl(jobId: string) {
|
||||
return `ml#/data_frame_analytics/exploration?_g=(ml:(jobId:${jobId}))`;
|
||||
export function getResultsUrl(
|
||||
jobId: string,
|
||||
analysisType: string,
|
||||
destIndex: string = '',
|
||||
dependentVariable: string = ''
|
||||
) {
|
||||
const destIndexParam = `,destIndex:${destIndex}`;
|
||||
const depVarParam = `,depVar:${dependentVariable}`;
|
||||
const isRegression = analysisType === ANALYSIS_CONFIG_TYPE.REGRESSION;
|
||||
|
||||
return `ml#/data_frame_analytics/exploration?_g=(ml:(jobId:${jobId},analysisType:${analysisType}${
|
||||
isRegression && destIndex !== '' ? destIndexParam : ''
|
||||
}${isRegression && dependentVariable !== '' ? depVarParam : ''}))`;
|
||||
}
|
||||
|
|
|
@ -163,9 +163,12 @@ export const CreateAnalyticsAdvancedEditor: FC<CreateAnalyticsFormProps> = ({ ac
|
|||
error={
|
||||
createIndexPattern &&
|
||||
destinationIndexPatternTitleExists && [
|
||||
i18n.translate('xpack.ml.dataframe.analytics.create.indexPatternTitleError', {
|
||||
defaultMessage: 'An index pattern with this title already exists.',
|
||||
}),
|
||||
i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.indexPatternAlreadyExistsError',
|
||||
{
|
||||
defaultMessage: 'An index pattern with this title already exists.',
|
||||
}
|
||||
),
|
||||
]
|
||||
}
|
||||
>
|
||||
|
|
|
@ -16,7 +16,7 @@ import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'
|
|||
|
||||
import { CreateAnalyticsAdvancedEditor } from '../create_analytics_advanced_editor';
|
||||
import { CreateAnalyticsForm } from '../create_analytics_form';
|
||||
import { CreateAnalyticsModal } from '../create_analytics_modal';
|
||||
import { CreateAnalyticsFlyout } from '../create_analytics_flyout';
|
||||
|
||||
export const CreateAnalyticsButton: FC<CreateAnalyticsFormProps> = props => {
|
||||
const { disabled, isAdvancedEditorEnabled, isModalVisible } = props.state;
|
||||
|
@ -52,10 +52,10 @@ export const CreateAnalyticsButton: FC<CreateAnalyticsFormProps> = props => {
|
|||
<Fragment>
|
||||
{button}
|
||||
{isModalVisible && (
|
||||
<CreateAnalyticsModal {...props}>
|
||||
<CreateAnalyticsFlyout {...props}>
|
||||
{isAdvancedEditorEnabled === false && <CreateAnalyticsForm {...props} />}
|
||||
{isAdvancedEditorEnabled === true && <CreateAnalyticsAdvancedEditor {...props} />}
|
||||
</CreateAnalyticsModal>
|
||||
</CreateAnalyticsFlyout>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.mlAnalyticsCreateFlyout__footerButton {
|
||||
float: right;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
@import 'create_analytics_flyout';
|
|
@ -8,7 +8,7 @@ import { mount } from 'enzyme';
|
|||
import React from 'react';
|
||||
import { mountHook } from '../../../../../../../../../test_utils/enzyme_helpers';
|
||||
|
||||
import { CreateAnalyticsModal } from './create_analytics_modal';
|
||||
import { CreateAnalyticsFlyout } from './create_analytics_flyout';
|
||||
|
||||
import { KibanaContext } from '../../../../../contexts/kibana';
|
||||
import { kibanaContextValueMock } from '../../../../../contexts/kibana/__mocks__/kibana_context_value';
|
||||
|
@ -34,12 +34,14 @@ jest.mock('react', () => {
|
|||
return { ...r, memo: (x: any) => x };
|
||||
});
|
||||
|
||||
describe('Data Frame Analytics: <CreateAnalyticsModal />', () => {
|
||||
describe('Data Frame Analytics: <CreateAnalyticsFlyout />', () => {
|
||||
test('Minimal initialization', () => {
|
||||
const { getLastHookValue } = getMountedHook();
|
||||
const props = getLastHookValue();
|
||||
const wrapper = mount(<CreateAnalyticsModal {...props} />);
|
||||
const wrapper = mount(<CreateAnalyticsFlyout {...props} />);
|
||||
|
||||
expect(wrapper.find('EuiModalHeaderTitle').text()).toBe('Create analytics job');
|
||||
expect(wrapper.find('[data-test-subj="mlDataFrameAnalyticsFlyoutHeaderTitle"]').text()).toBe(
|
||||
'Create analytics job'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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,
|
||||
EuiButtonEmpty,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutFooter,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form';
|
||||
|
||||
export const CreateAnalyticsFlyout: FC<CreateAnalyticsFormProps> = ({
|
||||
actions,
|
||||
children,
|
||||
state,
|
||||
}) => {
|
||||
const { closeModal, createAnalyticsJob, startAnalyticsJob } = actions;
|
||||
const { isJobCreated, isJobStarted, isModalButtonDisabled, isValid } = state;
|
||||
|
||||
return (
|
||||
<EuiFlyout size="s" onClose={closeModal}>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle>
|
||||
<h2 data-test-subj="mlDataFrameAnalyticsFlyoutHeaderTitle">
|
||||
{i18n.translate('xpack.ml.dataframe.analytics.create.flyoutHeaderTitle', {
|
||||
defaultMessage: 'Create analytics job',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>{children}</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
{(!isJobCreated || !isJobStarted) && (
|
||||
<EuiButtonEmpty onClick={closeModal}>
|
||||
{i18n.translate('xpack.ml.dataframe.analytics.create.flyoutCancelButton', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
|
||||
{!isJobCreated && !isJobStarted && (
|
||||
<EuiButton
|
||||
className="mlAnalyticsCreateFlyout__footerButton"
|
||||
disabled={!isValid || isModalButtonDisabled}
|
||||
onClick={createAnalyticsJob}
|
||||
fill
|
||||
>
|
||||
{i18n.translate('xpack.ml.dataframe.analytics.create.flyoutCreateButton', {
|
||||
defaultMessage: 'Create',
|
||||
})}
|
||||
</EuiButton>
|
||||
)}
|
||||
{isJobCreated && !isJobStarted && (
|
||||
<EuiButton
|
||||
className="mlAnalyticsCreateFlyout__footerButton"
|
||||
disabled={isModalButtonDisabled}
|
||||
onClick={startAnalyticsJob}
|
||||
fill
|
||||
>
|
||||
{i18n.translate('xpack.ml.dataframe.analytics.create.flyoutStartButton', {
|
||||
defaultMessage: 'Start',
|
||||
})}
|
||||
</EuiButton>
|
||||
)}
|
||||
{isJobCreated && isJobStarted && (
|
||||
<EuiButton onClick={closeModal} fill>
|
||||
{i18n.translate('xpack.ml.dataframe.analytics.create.flyoutCloseButton', {
|
||||
defaultMessage: 'Close',
|
||||
})}
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
|
@ -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 { CreateAnalyticsFlyout } from './create_analytics_flyout';
|
|
@ -38,17 +38,24 @@ describe('Data Frame Analytics: <CreateAnalyticsForm />', () => {
|
|||
test('Minimal initialization', () => {
|
||||
const { getLastHookValue } = getMountedHook();
|
||||
const props = getLastHookValue();
|
||||
const wrapper = mount(<CreateAnalyticsForm {...props} />);
|
||||
const wrapper = mount(
|
||||
<KibanaContext.Provider value={kibanaContextValueMock}>
|
||||
<CreateAnalyticsForm {...props} />
|
||||
</KibanaContext.Provider>
|
||||
);
|
||||
|
||||
const euiFormRows = wrapper.find('EuiFormRow');
|
||||
expect(euiFormRows.length).toBe(5);
|
||||
expect(euiFormRows.length).toBe(6);
|
||||
|
||||
const row1 = euiFormRows.at(0);
|
||||
expect(row1.find('label').text()).toBe('Job type');
|
||||
expect(row1.find('EuiText').text()).toBe('Outlier detection');
|
||||
expect(row1.find('EuiLink').text()).toBe('advanced editor');
|
||||
|
||||
const options = row1.find('option');
|
||||
expect(options.at(0).props().value).toBe('');
|
||||
expect(options.at(1).props().value).toBe('outlier_detection');
|
||||
expect(options.at(2).props().value).toBe('regression');
|
||||
|
||||
const row2 = euiFormRows.at(1);
|
||||
expect(row2.find('label').text()).toBe('Job ID');
|
||||
expect(row2.find('label').text()).toBe('Enable advanced editor');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,47 +4,67 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment, FC } from 'react';
|
||||
import React, { Fragment, FC, useEffect } from 'react';
|
||||
|
||||
import {
|
||||
EuiCallOut,
|
||||
EuiComboBox,
|
||||
EuiForm,
|
||||
EuiFieldText,
|
||||
EuiFormRow,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiRange,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { metadata } from 'ui/metadata';
|
||||
import { INDEX_PATTERN_ILLEGAL_CHARACTERS } from 'ui/index_patterns';
|
||||
import { IndexPattern, INDEX_PATTERN_ILLEGAL_CHARACTERS } from 'ui/index_patterns';
|
||||
import { Field, EVENT_RATE_FIELD_ID } from '../../../../../../common/types/fields';
|
||||
|
||||
import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
|
||||
import { useKibanaContext } from '../../../../../contexts/kibana';
|
||||
import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form';
|
||||
import { JOB_TYPES } from '../../hooks/use_create_analytics_form/state';
|
||||
import { JOB_ID_MAX_LENGTH } from '../../../../../../common/constants/validation';
|
||||
import { Messages } from './messages';
|
||||
import { JobType } from './job_type';
|
||||
|
||||
// based on code used by `ui/index_patterns` internally
|
||||
// remove the space character from the list of illegal characters
|
||||
INDEX_PATTERN_ILLEGAL_CHARACTERS.pop();
|
||||
const characterList = INDEX_PATTERN_ILLEGAL_CHARACTERS.join(', ');
|
||||
|
||||
const NUMERICAL_FIELD_TYPES = new Set([
|
||||
'long',
|
||||
'integer',
|
||||
'short',
|
||||
'byte',
|
||||
'double',
|
||||
'float',
|
||||
'half_float',
|
||||
'scaled_float',
|
||||
]);
|
||||
|
||||
export const CreateAnalyticsForm: FC<CreateAnalyticsFormProps> = ({ actions, state }) => {
|
||||
const { setFormState } = actions;
|
||||
const kibanaContext = useKibanaContext();
|
||||
|
||||
const {
|
||||
form,
|
||||
indexPatternsMap,
|
||||
indexPatternsWithNumericFields,
|
||||
indexPatternTitles,
|
||||
isAdvancedEditorEnabled,
|
||||
isJobCreated,
|
||||
requestMessages,
|
||||
} = state;
|
||||
|
||||
const {
|
||||
createIndexPattern,
|
||||
dependentVariable,
|
||||
dependentVariableFetchFail,
|
||||
dependentVariableOptions,
|
||||
destinationIndex,
|
||||
destinationIndexNameEmpty,
|
||||
destinationIndexNameExists,
|
||||
|
@ -55,54 +75,78 @@ export const CreateAnalyticsForm: FC<CreateAnalyticsFormProps> = ({ actions, sta
|
|||
jobIdExists,
|
||||
jobIdValid,
|
||||
jobIdInvalidMaxLength,
|
||||
jobType,
|
||||
loadingDepFieldOptions,
|
||||
sourceIndex,
|
||||
sourceIndexNameEmpty,
|
||||
sourceIndexNameValid,
|
||||
trainingPercent,
|
||||
} = form;
|
||||
|
||||
const loadDependentFieldOptions = async () => {
|
||||
setFormState({ loadingDepFieldOptions: true, dependentVariable: '' });
|
||||
try {
|
||||
const indexPattern: IndexPattern = await kibanaContext.indexPatterns.get(
|
||||
indexPatternsMap[sourceIndex]
|
||||
);
|
||||
|
||||
if (indexPattern !== undefined) {
|
||||
await newJobCapsService.initializeFromIndexPattern(indexPattern);
|
||||
// Get fields and filter for numeric
|
||||
const { fields } = newJobCapsService;
|
||||
const options: Array<{ label: string }> = [];
|
||||
|
||||
fields.forEach((field: Field) => {
|
||||
if (NUMERICAL_FIELD_TYPES.has(field.type) && field.id !== EVENT_RATE_FIELD_ID) {
|
||||
options.push({ label: field.id });
|
||||
}
|
||||
});
|
||||
|
||||
setFormState({
|
||||
dependentVariableOptions: options,
|
||||
loadingDepFieldOptions: false,
|
||||
dependentVariableFetchFail: false,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// TODO: ensure error messages show up correctly
|
||||
setFormState({ loadingDepFieldOptions: false, dependentVariableFetchFail: true });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (jobType === JOB_TYPES.REGRESSION && sourceIndexNameEmpty === false) {
|
||||
loadDependentFieldOptions();
|
||||
}
|
||||
}, [sourceIndex, jobType, sourceIndexNameEmpty]);
|
||||
|
||||
return (
|
||||
<EuiForm className="mlDataFrameAnalyticsCreateForm">
|
||||
{requestMessages.map((requestMessage, i) => (
|
||||
<Fragment key={i}>
|
||||
<EuiCallOut
|
||||
title={requestMessage.message}
|
||||
color={requestMessage.error !== undefined ? 'danger' : 'primary'}
|
||||
iconType={requestMessage.error !== undefined ? 'alert' : 'checkInCircleFilled'}
|
||||
size="s"
|
||||
>
|
||||
{requestMessage.error !== undefined ? <p>{requestMessage.error}</p> : null}
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="s" />
|
||||
</Fragment>
|
||||
))}
|
||||
<Messages messages={requestMessages} />
|
||||
{!isJobCreated && (
|
||||
<Fragment>
|
||||
<JobType type={jobType} setFormState={setFormState} />
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.analytics.create.jobTypeLabel', {
|
||||
defaultMessage: 'Job type',
|
||||
})}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.dataframe.analytics.create.jobTypeHelpText"
|
||||
defaultMessage="Outlier detection jobs require a source index that is mapped as a table-like data structure and will only analyze numeric and boolean fields. Please use the {advancedEditorButton} to apply custom options such as the model memory limit and analysis type. You cannot switch back to this form from the advanced editor."
|
||||
values={{
|
||||
advancedEditorButton: (
|
||||
<EuiLink onClick={actions.switchToAdvancedEditor}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.dataframe.analytics.create.switchToAdvancedEditorButton"
|
||||
defaultMessage="advanced editor"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
helpText={i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.enableAdvancedEditorHelpText',
|
||||
{
|
||||
defaultMessage: 'You cannot switch back to this form from the advanced editor.',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiText>
|
||||
{i18n.translate('xpack.ml.dataframe.analytics.create.outlierDetectionText', {
|
||||
defaultMessage: 'Outlier detection',
|
||||
})}
|
||||
</EuiText>
|
||||
<EuiSwitch
|
||||
disabled={jobType === undefined}
|
||||
compressed={true}
|
||||
name="mlDataFrameAnalyticsEnableAdvancedEditor"
|
||||
label={i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.enableAdvancedEditorSwitch',
|
||||
{
|
||||
defaultMessage: 'Enable advanced editor',
|
||||
}
|
||||
)}
|
||||
checked={isAdvancedEditorEnabled}
|
||||
onChange={actions.switchToAdvancedEditor}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.analytics.create.jobIdLabel', {
|
||||
|
@ -157,6 +201,7 @@ export const CreateAnalyticsForm: FC<CreateAnalyticsFormProps> = ({ actions, sta
|
|||
isInvalid={(!jobIdEmpty && !jobIdValid) || jobIdExists}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{/* TODO: Does the source index message below apply for regression jobs as well? Same for all validation messages below */}
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.analytics.create.sourceIndexLabel', {
|
||||
defaultMessage: 'Source index',
|
||||
|
@ -267,12 +312,79 @@ export const CreateAnalyticsForm: FC<CreateAnalyticsFormProps> = ({ actions, sta
|
|||
isInvalid={!destinationIndexNameEmpty && !destinationIndexNameValid}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{jobType === JOB_TYPES.REGRESSION && (
|
||||
<Fragment>
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.dependentVariableLabel',
|
||||
{
|
||||
defaultMessage: 'Dependent variable',
|
||||
}
|
||||
)}
|
||||
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>,
|
||||
]
|
||||
}
|
||||
>
|
||||
<EuiComboBox
|
||||
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={loadingDepFieldOptions}
|
||||
singleSelection={true}
|
||||
options={dependentVariableOptions}
|
||||
selectedOptions={dependentVariable ? [{ label: dependentVariable }] : []}
|
||||
onChange={selectedOptions =>
|
||||
setFormState({ dependentVariable: selectedOptions[0].label || '' })
|
||||
}
|
||||
isClearable={false}
|
||||
isInvalid={dependentVariable === ''}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.analytics.create.trainingPercentLabel', {
|
||||
defaultMessage: 'Training percent',
|
||||
})}
|
||||
>
|
||||
<EuiRange
|
||||
min={0}
|
||||
max={100}
|
||||
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 })}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
)}
|
||||
<EuiFormRow
|
||||
isInvalid={createIndexPattern && destinationIndexPatternTitleExists}
|
||||
error={
|
||||
createIndexPattern &&
|
||||
destinationIndexPatternTitleExists && [
|
||||
i18n.translate('xpack.ml.dataframe.analytics.create.indexPatternTitleError', {
|
||||
i18n.translate('xpack.ml.dataframe.analytics.create.indexPatternExistsError', {
|
||||
defaultMessage: 'An index pattern with this title already exists.',
|
||||
}),
|
||||
]
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { AnalyticsJobType, JOB_TYPES } from '../../hooks/use_create_analytics_form/state';
|
||||
|
||||
interface Props {
|
||||
type: AnalyticsJobType;
|
||||
setFormState: any; // TODO update type
|
||||
}
|
||||
|
||||
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 will only analyze numeric and boolean fields. Please use the advanced editor to apply custom options such as the model memory limit and analysis type.',
|
||||
}
|
||||
);
|
||||
|
||||
const regressionHelpText = i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.outlierRegressionHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Regression jobs will only analyze numeric fields. Please use the advanced editor to apply custom options such as the model memory limit and prediction field name.',
|
||||
}
|
||||
);
|
||||
|
||||
const helpText = {
|
||||
outlier_detection: outlierHelpText,
|
||||
regression: regressionHelpText,
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.analytics.create.jobTypeLabel', {
|
||||
defaultMessage: 'Job type',
|
||||
})}
|
||||
helpText={type !== undefined ? helpText[type] : ''}
|
||||
>
|
||||
<EuiSelect
|
||||
options={Object.values(JOB_TYPES).map(jobType => ({
|
||||
value: jobType,
|
||||
text: jobType.replace(/_/g, ' '),
|
||||
}))}
|
||||
value={type}
|
||||
hasNoInitialSelection={true}
|
||||
onChange={e => {
|
||||
const value = e.target.value as AnalyticsJobType;
|
||||
setFormState({ jobType: value });
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { FormMessage } from '../../hooks/use_create_analytics_form/state'; // State
|
||||
|
||||
interface Props {
|
||||
messages: any; // TODO: fix --> something like State['requestMessages'];
|
||||
}
|
||||
|
||||
export const Messages: FC<Props> = ({ messages }) =>
|
||||
messages.map((requestMessage: FormMessage, i: number) => (
|
||||
<Fragment key={i}>
|
||||
<EuiCallOut
|
||||
title={requestMessage.message}
|
||||
color={requestMessage.error !== undefined ? 'danger' : 'primary'}
|
||||
iconType={requestMessage.error !== undefined ? 'alert' : 'checkInCircleFilled'}
|
||||
size="s"
|
||||
>
|
||||
{requestMessage.error !== undefined ? <p>{requestMessage.error}</p> : null}
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="s" />
|
||||
</Fragment>
|
||||
));
|
|
@ -1,91 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiOverlayMask,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form';
|
||||
|
||||
export const CreateAnalyticsModal: FC<CreateAnalyticsFormProps> = ({
|
||||
actions,
|
||||
children,
|
||||
state,
|
||||
}) => {
|
||||
const { closeModal, createAnalyticsJob, startAnalyticsJob } = actions;
|
||||
const {
|
||||
isAdvancedEditorEnabled,
|
||||
isJobCreated,
|
||||
isJobStarted,
|
||||
isModalButtonDisabled,
|
||||
isValid,
|
||||
} = state;
|
||||
|
||||
const width = isAdvancedEditorEnabled ? '640px' : '450px';
|
||||
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiModal onClose={closeModal} style={{ width }}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
{i18n.translate('xpack.ml.dataframe.analytics.create.modalHeaderTitle', {
|
||||
defaultMessage: 'Create analytics job',
|
||||
})}
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>{children}</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
{(!isJobCreated || !isJobStarted) && (
|
||||
<EuiButtonEmpty onClick={closeModal}>
|
||||
{i18n.translate('xpack.ml.dataframe.analytics.create.modalCancelButton', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
|
||||
{!isJobCreated && !isJobStarted && (
|
||||
<EuiButton
|
||||
disabled={!isValid || isModalButtonDisabled}
|
||||
onClick={createAnalyticsJob}
|
||||
fill
|
||||
>
|
||||
{i18n.translate('xpack.ml.dataframe.analytics.create.modalCreateButton', {
|
||||
defaultMessage: 'Create',
|
||||
})}
|
||||
</EuiButton>
|
||||
)}
|
||||
{isJobCreated && !isJobStarted && (
|
||||
<EuiButton disabled={isModalButtonDisabled} onClick={startAnalyticsJob} fill>
|
||||
{i18n.translate('xpack.ml.dataframe.analytics.create.modalStartButton', {
|
||||
defaultMessage: 'Start',
|
||||
})}
|
||||
</EuiButton>
|
||||
)}
|
||||
{isJobCreated && isJobStarted && (
|
||||
<EuiButton onClick={closeModal} fill>
|
||||
{i18n.translate('xpack.ml.dataframe.analytics.create.modalCloseButton', {
|
||||
defaultMessage: 'Close',
|
||||
})}
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
};
|
|
@ -4,4 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { useCreateAnalyticsForm, CreateAnalyticsFormProps } from './use_create_analytics_form';
|
||||
export {
|
||||
useCreateAnalyticsForm,
|
||||
CreateAnalyticsFormProps,
|
||||
getErrorMessage,
|
||||
} from './use_create_analytics_form';
|
||||
|
|
|
@ -9,21 +9,32 @@ import { checkPermission } from '../../../../../privilege/check_privilege';
|
|||
|
||||
import { DataFrameAnalyticsId, DataFrameAnalyticsConfig } from '../../../../common';
|
||||
|
||||
const ANALYTICS_DETAULT_MODEL_MEMORY_LIMIT = '50mb';
|
||||
const OUTLIER_DETECTION_DEFAULT_MODEL_MEMORY_LIMIT = '50mb';
|
||||
const REGRESSION_DEFAULT_MODEL_MEMORY_LIMIT = '100mb';
|
||||
|
||||
export type EsIndexName = string;
|
||||
export type DependentVariable = string;
|
||||
export type IndexPatternTitle = string;
|
||||
export type AnalyticsJobType = JOB_TYPES | undefined;
|
||||
|
||||
export interface FormMessage {
|
||||
error?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export enum JOB_TYPES {
|
||||
OUTLIER_DETECTION = 'outlier_detection',
|
||||
REGRESSION = 'regression',
|
||||
}
|
||||
|
||||
export interface State {
|
||||
advancedEditorMessages: FormMessage[];
|
||||
advancedEditorRawString: string;
|
||||
form: {
|
||||
createIndexPattern: boolean;
|
||||
dependentVariable: DependentVariable;
|
||||
dependentVariableFetchFail: boolean;
|
||||
dependentVariableOptions: Array<{ label: DependentVariable }> | [];
|
||||
destinationIndex: EsIndexName;
|
||||
destinationIndexNameExists: boolean;
|
||||
destinationIndexNameEmpty: boolean;
|
||||
|
@ -34,12 +45,16 @@ export interface State {
|
|||
jobIdEmpty: boolean;
|
||||
jobIdInvalidMaxLength: boolean;
|
||||
jobIdValid: boolean;
|
||||
jobType: AnalyticsJobType;
|
||||
loadingDepFieldOptions: boolean;
|
||||
sourceIndex: EsIndexName;
|
||||
sourceIndexNameEmpty: boolean;
|
||||
sourceIndexNameValid: boolean;
|
||||
trainingPercent: number;
|
||||
};
|
||||
disabled: boolean;
|
||||
indexNames: EsIndexName[];
|
||||
indexPatternsMap: any; // TODO: update type
|
||||
indexPatternTitles: IndexPatternTitle[];
|
||||
indexPatternsWithNumericFields: IndexPatternTitle[];
|
||||
isAdvancedEditorEnabled: boolean;
|
||||
|
@ -58,6 +73,9 @@ export const getInitialState = (): State => ({
|
|||
advancedEditorRawString: '',
|
||||
form: {
|
||||
createIndexPattern: false,
|
||||
dependentVariable: '',
|
||||
dependentVariableFetchFail: false,
|
||||
dependentVariableOptions: [],
|
||||
destinationIndex: '',
|
||||
destinationIndexNameExists: false,
|
||||
destinationIndexNameEmpty: true,
|
||||
|
@ -68,15 +86,19 @@ export const getInitialState = (): State => ({
|
|||
jobIdEmpty: true,
|
||||
jobIdInvalidMaxLength: false,
|
||||
jobIdValid: false,
|
||||
jobType: undefined,
|
||||
loadingDepFieldOptions: false,
|
||||
sourceIndex: '',
|
||||
sourceIndexNameEmpty: true,
|
||||
sourceIndexNameValid: false,
|
||||
trainingPercent: 80,
|
||||
},
|
||||
jobConfig: {},
|
||||
disabled:
|
||||
!checkPermission('canCreateDataFrameAnalytics') ||
|
||||
!checkPermission('canStartStopDataFrameAnalytics'),
|
||||
indexNames: [],
|
||||
indexPatternsMap: {},
|
||||
indexPatternTitles: [],
|
||||
indexPatternsWithNumericFields: [],
|
||||
isAdvancedEditorEnabled: false,
|
||||
|
@ -92,7 +114,12 @@ export const getInitialState = (): State => ({
|
|||
export const getJobConfigFromFormState = (
|
||||
formState: State['form']
|
||||
): DeepPartial<DataFrameAnalyticsConfig> => {
|
||||
return {
|
||||
const modelMemoryLimit =
|
||||
formState.jobType === JOB_TYPES.REGRESSION
|
||||
? REGRESSION_DEFAULT_MODEL_MEMORY_LIMIT
|
||||
: OUTLIER_DETECTION_DEFAULT_MODEL_MEMORY_LIMIT;
|
||||
|
||||
const jobConfig: DeepPartial<DataFrameAnalyticsConfig> = {
|
||||
source: {
|
||||
// If a Kibana index patterns includes commas, we need to split
|
||||
// the into an array of indices to be in the correct format for
|
||||
|
@ -110,6 +137,17 @@ export const getJobConfigFromFormState = (
|
|||
analysis: {
|
||||
outlier_detection: {},
|
||||
},
|
||||
model_memory_limit: ANALYTICS_DETAULT_MODEL_MEMORY_LIMIT,
|
||||
model_memory_limit: modelMemoryLimit,
|
||||
};
|
||||
|
||||
if (formState.jobType === JOB_TYPES.REGRESSION) {
|
||||
jobConfig.analysis = {
|
||||
regression: {
|
||||
dependent_variable: formState.dependentVariable,
|
||||
training_percent: formState.trainingPercent,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return jobConfig;
|
||||
};
|
||||
|
|
|
@ -69,6 +69,7 @@ export const useCreateAnalyticsForm = () => {
|
|||
const setIndexPatternTitles = (payload: {
|
||||
indexPatternTitles: IndexPatternTitle[];
|
||||
indexPatternsWithNumericFields: IndexPatternTitle[];
|
||||
indexPatternsMap: any; // TODO: update this type
|
||||
}) => dispatch({ type: ACTION.SET_INDEX_PATTERN_TITLES, payload });
|
||||
|
||||
const setIsJobCreated = (isJobCreated: boolean) =>
|
||||
|
@ -235,6 +236,7 @@ export const useCreateAnalyticsForm = () => {
|
|||
// able to identify outliers if there are no numeric fields present.
|
||||
const ids = await kibanaContext.indexPatterns.getIds(true);
|
||||
const indexPatternsWithNumericFields: IndexPatternTitle[] = [];
|
||||
const indexPatternsMap = {}; // TODO: add type, add to state to keep track of
|
||||
ids
|
||||
.filter(f => !!f)
|
||||
.forEach(async id => {
|
||||
|
@ -246,9 +248,15 @@ export const useCreateAnalyticsForm = () => {
|
|||
.includes('number')
|
||||
) {
|
||||
indexPatternsWithNumericFields.push(indexPattern.title);
|
||||
// @ts-ignore TODO: fix this type
|
||||
indexPatternsMap[indexPattern.title] = id;
|
||||
}
|
||||
});
|
||||
setIndexPatternTitles({ indexPatternTitles, indexPatternsWithNumericFields });
|
||||
setIndexPatternTitles({
|
||||
indexPatternTitles,
|
||||
indexPatternsWithNumericFields,
|
||||
indexPatternsMap,
|
||||
});
|
||||
} catch (e) {
|
||||
addRequestMessage({
|
||||
error: getErrorMessage(e),
|
||||
|
|
|
@ -40,6 +40,13 @@ export const dataFrameAnalytics = {
|
|||
data: analyticsConfig
|
||||
});
|
||||
},
|
||||
evaluateDataFrameAnalytics(evaluateConfig) {
|
||||
return http({
|
||||
url: `${basePath}/data_frame/_evaluate`,
|
||||
method: 'POST',
|
||||
data: evaluateConfig
|
||||
});
|
||||
},
|
||||
deleteDataFrameAnalytics(analyticsId) {
|
||||
return http({
|
||||
url: `${basePath}/data_frame/analytics/${analyticsId}`,
|
||||
|
|
|
@ -45,6 +45,7 @@ declare interface Ml {
|
|||
getDataFrameAnalytics(analyticsId?: string): Promise<any>;
|
||||
getDataFrameAnalyticsStats(analyticsId?: string): Promise<any>;
|
||||
createDataFrameAnalytics(analyticsId: string, analyticsConfig: any): Promise<any>;
|
||||
evaluateDataFrameAnalytics(evaluateConfig: any): Promise<any>;
|
||||
deleteDataFrameAnalytics(analyticsId: string): Promise<any>;
|
||||
startDataFrameAnalytics(analyticsId: string): Promise<any>;
|
||||
stopDataFrameAnalytics(
|
||||
|
|
|
@ -160,6 +160,16 @@ export const elasticsearchJsPlugin = (Client, config, components) => { // eslint
|
|||
method: 'PUT'
|
||||
});
|
||||
|
||||
ml.evaluateDataFrameAnalytics = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_ml/data_frame/_evaluate',
|
||||
}
|
||||
],
|
||||
needBody: true,
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
ml.deleteDataFrameAnalytics = ca({
|
||||
urls: [
|
||||
{
|
||||
|
|
|
@ -78,6 +78,19 @@ export function dataFrameAnalyticsRoutes({ commonRouteConfig, elasticsearchPlugi
|
|||
}
|
||||
});
|
||||
|
||||
route({
|
||||
method: 'POST',
|
||||
path: '/api/ml/data_frame/_evaluate',
|
||||
handler(request) {
|
||||
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
|
||||
return callWithRequest('ml.evaluateDataFrameAnalytics', { body: request.payload })
|
||||
.catch(resp => wrapError(resp));
|
||||
},
|
||||
config: {
|
||||
...commonRouteConfig
|
||||
}
|
||||
});
|
||||
|
||||
route({
|
||||
method: 'DELETE',
|
||||
path: '/api/ml/data_frame/analytics/{analyticsId}',
|
||||
|
|
|
@ -7266,27 +7266,18 @@
|
|||
"xpack.ml.dataframe.analytics.create.errorGettingDataFrameIndexNames": "获取现有索引名称时发生错误:",
|
||||
"xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "获取现有索引模式标题时发生错误:",
|
||||
"xpack.ml.dataframe.analytics.create.errorStartingDataFrameAnalyticsJob": "启动数据帧分析作业时发生错误:",
|
||||
"xpack.ml.dataframe.analytics.create.indexPatternTitleError": "具有此名称的索引模式已存在。",
|
||||
"xpack.ml.dataframe.analytics.create.jobIdExistsError": "已存在具有此 ID 的分析作业。",
|
||||
"xpack.ml.dataframe.analytics.create.jobIdInputAriaLabel": "选择唯一的分析作业 ID。",
|
||||
"xpack.ml.dataframe.analytics.create.jobIdInvalidError": "只能包含小写字母数字字符(a-z 和 0-9)、连字符和下划线,并且必须以字母数字字符开头和结尾。",
|
||||
"xpack.ml.dataframe.analytics.create.jobIdLabel": "作业 ID",
|
||||
"xpack.ml.dataframe.analytics.create.jobIdPlaceholder": "作业 ID",
|
||||
"xpack.ml.dataframe.analytics.create.jobTypeHelpText": "离群值检测作业需要映射为类表数据结构的源索引,将仅分析数值和布尔值字段。请使用“{advancedEditorButton}”应用定制选项,如模型内存限制和分析类型。您不能从高级编辑器切换回到此表单。",
|
||||
"xpack.ml.dataframe.analytics.create.jobTypeLabel": "作业类型",
|
||||
"xpack.ml.dataframe.analytics.create.modalCancelButton": "取消",
|
||||
"xpack.ml.dataframe.analytics.create.modalCloseButton": "关闭",
|
||||
"xpack.ml.dataframe.analytics.create.modalCreateButton": "创建",
|
||||
"xpack.ml.dataframe.analytics.create.modalHeaderTitle": "创建分析作业",
|
||||
"xpack.ml.dataframe.analytics.create.modalStartButton": "开始",
|
||||
"xpack.ml.dataframe.analytics.create.outlierDetectionText": "离群值检测",
|
||||
"xpack.ml.dataframe.analytics.create.sourceIndexHelpText": "此索引模式不包含任何数值类型字段。分析作业可能无法提供任何离群值。",
|
||||
"xpack.ml.dataframe.analytics.create.sourceIndexInputAriaLabel": "源索引模式或搜索。",
|
||||
"xpack.ml.dataframe.analytics.create.sourceIndexInvalidError": "源索引名称无效,其不能包含空格或以下字符:{characterList}",
|
||||
"xpack.ml.dataframe.analytics.create.sourceIndexLabel": "源索引",
|
||||
"xpack.ml.dataframe.analytics.create.sourceIndexPlaceholder": "选择源索引模式或已保存搜索。",
|
||||
"xpack.ml.dataframe.analytics.create.startDataFrameAnalyticsSuccessMessage": "分析作业 {jobId} 已启动。",
|
||||
"xpack.ml.dataframe.analytics.create.switchToAdvancedEditorButton": "高级编辑器",
|
||||
"xpack.ml.dataframe.analytics.exploration.experimentalBadgeLabel": "实验性",
|
||||
"xpack.ml.dataframe.analytics.exploration.experimentalBadgeTooltipContent": "数据帧分析为实验功能。我们很乐意听取您的反馈意见。",
|
||||
"xpack.ml.dataframe.analytics.exploration.fieldSelection": "已选择 {selectedFieldsLength, number} 个{docFieldsCount, plural, one {字段} other {字段}},共 {docFieldsCount, number} 个",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue