From 77da781144c9d694da4b605c36364237191f1167 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Wed, 18 Nov 2020 10:57:22 -0600 Subject: [PATCH] [ML] Persist URL state for Anomaly detection jobs using metric function (#83507) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../ml/common/types/ml_url_generator.ts | 2 + .../routing/routes/timeseriesexplorer.tsx | 15 +++ .../plot_function_controls.tsx | 66 +++++++++++- .../series_controls/series_controls.tsx | 5 +- .../get_function_description.ts | 19 +++- .../timeseriesexplorer/timeseriesexplorer.js | 100 ++++++------------ .../timeseriesexplorer_constants.ts | 1 + .../get_viewable_detectors.ts | 29 +++++ .../anomaly_detection_urls_generator.ts | 5 + 9 files changed, 167 insertions(+), 75 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_viewable_detectors.ts diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts index b188ac0a87571e..9a3d8fc4a4f021 100644 --- a/x-pack/plugins/ml/common/types/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts @@ -132,6 +132,7 @@ export interface TimeSeriesExplorerAppState { forecastId?: string; detectorIndex?: number; entities?: Record; + functionDescription?: string; }; query?: any; } @@ -145,6 +146,7 @@ export interface TimeSeriesExplorerPageState entities?: Record; forecastId?: string; globalState?: MlCommonGlobalState; + functionDescription?: string; } export type TimeSeriesExplorerUrlState = MLPageState< diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index 2653781ce1a302..f0fb4558bcfa95 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -161,6 +161,11 @@ export const TimeSeriesExplorerUrlStateManager: FC ); diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx index 0356c20fecb9a3..8e26a912a6051e 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx @@ -3,9 +3,14 @@ * 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 from 'react'; +import React, { useCallback, useEffect } from 'react'; import { EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { mlJobService } from '../../../services/job_service'; +import { getFunctionDescription, isMetricDetector } from '../../get_function_description'; +import { useToastNotificationService } from '../../../services/toast_notification_service'; +import { ML_JOB_AGGREGATION } from '../../../../../common/constants/aggregation_types'; +import type { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs'; const plotByFunctionOptions = [ { @@ -30,11 +35,70 @@ const plotByFunctionOptions = [ export const PlotByFunctionControls = ({ functionDescription, setFunctionDescription, + selectedDetectorIndex, + selectedJobId, + selectedEntities, }: { functionDescription: undefined | string; setFunctionDescription: (func: string) => void; + selectedDetectorIndex: number; + selectedJobId: string; + selectedEntities: Record; }) => { + const toastNotificationService = useToastNotificationService(); + + const getFunctionDescriptionToPlot = useCallback( + async ( + _selectedDetectorIndex: number, + _selectedEntities: Record, + _selectedJobId: string, + _selectedJob: CombinedJob + ) => { + const functionToPlot = await getFunctionDescription( + { + selectedDetectorIndex: _selectedDetectorIndex, + selectedEntities: _selectedEntities, + selectedJobId: _selectedJobId, + selectedJob: _selectedJob, + }, + toastNotificationService + ); + setFunctionDescription(functionToPlot); + }, + [setFunctionDescription, toastNotificationService] + ); + + useEffect(() => { + if (functionDescription !== undefined) { + return; + } + const selectedJob = mlJobService.getJob(selectedJobId); + if ( + // set if only entity controls are picked + selectedEntities !== undefined && + functionDescription === undefined && + isMetricDetector(selectedJob, selectedDetectorIndex) + ) { + const detector = selectedJob.analysis_config.detectors[selectedDetectorIndex]; + if (detector?.function === ML_JOB_AGGREGATION.METRIC) { + getFunctionDescriptionToPlot( + selectedDetectorIndex, + selectedEntities, + selectedJobId, + selectedJob + ); + } + } + }, [ + setFunctionDescription, + selectedDetectorIndex, + selectedEntities, + selectedJobId, + functionDescription, + ]); + if (functionDescription === undefined) return null; + return ( = selectedDetectorIndex) { + const detector = selectedJob.analysis_config.detectors[selectedDetectorIndex]; + if (detector?.function === ML_JOB_AGGREGATION.METRIC) { + return true; + } + } + return false; +} /** * Get the function description from the record with the highest anomaly score @@ -31,11 +43,7 @@ export const getFunctionDescription = async ( ) => { // if the detector's function is metric, fetch the highest scoring anomaly record // and set to plot the function_description (avg/min/max) of that record by default - if ( - selectedJob?.analysis_config?.detectors[selectedDetectorIndex]?.function !== - ML_JOB_AGGREGATION.METRIC - ) - return; + if (!isMetricDetector(selectedJob, selectedDetectorIndex)) return; const entityControls = getControlsForDetector( selectedDetectorIndex, @@ -43,6 +51,7 @@ export const getFunctionDescription = async ( selectedJobId ); const criteriaFields = getCriteriaFields(selectedDetectorIndex, entityControls); + try { const resp = await mlResultsService .getRecordsForCriteria([selectedJob.job_id], criteriaFields, 0, null, null, 1) diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index e3b6e38f47babf..f22cc191ef8444 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -8,7 +8,7 @@ * React component for rendering Single Metric Viewer. */ -import { each, find, get, has, isEqual } from 'lodash'; +import { find, get, has, isEqual } from 'lodash'; import moment from 'moment-timezone'; import { Subject, Subscription, forkJoin } from 'rxjs'; import { map, debounceTime, switchMap, tap, withLatestFrom } from 'rxjs/operators'; @@ -40,7 +40,6 @@ import { isModelPlotEnabled, isModelPlotChartableForDetector, isSourceDataChartableForDetector, - isTimeSeriesViewDetector, mlFunctionToESAggregation, } from '../../../common/util/job_utils'; @@ -84,7 +83,8 @@ import { SeriesControls } from './components/series_controls'; import { TimeSeriesChartWithTooltips } from './components/timeseries_chart/timeseries_chart_with_tooltip'; import { PlotByFunctionControls } from './components/plot_function_controls'; import { aggregationTypeTransform } from '../../../common/util/anomaly_utils'; -import { getFunctionDescription } from './get_function_description'; +import { isMetricDetector } from './get_function_description'; +import { getViewableDetectors } from './timeseriesexplorer_utils/get_viewable_detectors'; // Used to indicate the chart is being plotted across // all partition field values, where the cardinality of the field cannot be @@ -93,20 +93,6 @@ const allValuesLabel = i18n.translate('xpack.ml.timeSeriesExplorer.allPartitionV defaultMessage: 'all', }); -export function getViewableDetectors(selectedJob) { - const jobDetectors = selectedJob.analysis_config.detectors; - const viewableDetectors = []; - each(jobDetectors, (dtr, index) => { - if (isTimeSeriesViewDetector(selectedJob, index)) { - viewableDetectors.push({ - index, - detector_description: dtr.detector_description, - }); - } - }); - return viewableDetectors; -} - function getTimeseriesexplorerDefaultState() { return { chartDetails: undefined, @@ -143,8 +129,6 @@ function getTimeseriesexplorerDefaultState() { zoomTo: undefined, zoomFromFocusLoaded: undefined, zoomToFocusLoaded: undefined, - // Sets function to plot by if original function is metric - functionDescription: undefined, }; } @@ -223,9 +207,7 @@ export class TimeSeriesExplorer extends React.Component { }; setFunctionDescription = (selectedFuction) => { - this.setState({ - functionDescription: selectedFuction, - }); + this.props.appStateHandler(APP_STATE_ACTION.SET_FUNCTION_DESCRIPTION, selectedFuction); }; previousChartProps = {}; @@ -280,9 +262,17 @@ export class TimeSeriesExplorer extends React.Component { * Gets focus data for the current component state/ */ getFocusData(selection) { - const { selectedJobId, selectedForecastId, selectedDetectorIndex } = this.props; - const { modelPlotEnabled, functionDescription } = this.state; + const { + selectedJobId, + selectedForecastId, + selectedDetectorIndex, + functionDescription, + } = this.props; + const { modelPlotEnabled } = this.state; const selectedJob = mlJobService.getJob(selectedJobId); + if (isMetricDetector(selectedJob, selectedDetectorIndex) && functionDescription === undefined) { + return; + } const entityControls = this.getControlsForDetector(); // Calculate the aggregation interval for the focus chart. @@ -333,8 +323,8 @@ export class TimeSeriesExplorer extends React.Component { selectedJobId, tableInterval, tableSeverity, + functionDescription, } = this.props; - const { functionDescription } = this.state; const selectedJob = mlJobService.getJob(selectedJobId); const entityControls = this.getControlsForDetector(); @@ -394,24 +384,6 @@ export class TimeSeriesExplorer extends React.Component { ); }; - getFunctionDescription = async () => { - const { selectedDetectorIndex, selectedEntities, selectedJobId } = this.props; - const selectedJob = mlJobService.getJob(selectedJobId); - - const functionDescriptionToPlot = await getFunctionDescription( - { - selectedDetectorIndex, - selectedEntities, - selectedJobId, - selectedJob, - }, - this.props.toastNotificationService - ); - if (!this.unmounted) { - this.setFunctionDescription(functionDescriptionToPlot); - } - }; - setForecastId = (forecastId) => { this.props.appStateHandler(APP_STATE_ACTION.SET_FORECAST_ID, forecastId); }; @@ -424,14 +396,22 @@ export class TimeSeriesExplorer extends React.Component { selectedForecastId, selectedJobId, zoom, + functionDescription, } = this.props; - const { loadCounter: currentLoadCounter, functionDescription } = this.state; + const { loadCounter: currentLoadCounter } = this.state; const currentSelectedJob = mlJobService.getJob(selectedJobId); if (currentSelectedJob === undefined) { return; } + if ( + isMetricDetector(currentSelectedJob, selectedDetectorIndex) && + functionDescription === undefined + ) { + return; + } + const functionToPlotByIfMetric = aggregationTypeTransform.toES(functionDescription); this.contextChartSelectedInitCallDone = false; @@ -845,7 +825,7 @@ export class TimeSeriesExplorer extends React.Component { this.componentDidUpdate(); } - componentDidUpdate(previousProps, previousState) { + componentDidUpdate(previousProps) { if (previousProps === undefined || previousProps.selectedJobId !== this.props.selectedJobId) { this.contextChartSelectedInitCallDone = false; this.setState({ fullRefresh: false, loading: true }, () => { @@ -853,15 +833,6 @@ export class TimeSeriesExplorer extends React.Component { }); } - if ( - previousProps === undefined || - previousProps.selectedJobId !== this.props.selectedJobId || - previousProps.selectedDetectorIndex !== this.props.selectedDetectorIndex || - !isEqual(previousProps.selectedEntities, this.props.selectedEntities) - ) { - this.getFunctionDescription(); - } - if ( previousProps === undefined || previousProps.selectedForecastId !== this.props.selectedForecastId @@ -885,7 +856,7 @@ export class TimeSeriesExplorer extends React.Component { !isEqual(previousProps.selectedEntities, this.props.selectedEntities) || previousProps.selectedForecastId !== this.props.selectedForecastId || previousProps.selectedJobId !== this.props.selectedJobId || - previousState.functionDescription !== this.state.functionDescription + previousProps.functionDescription !== this.props.functionDescription ) { const fullRefresh = previousProps === undefined || @@ -894,7 +865,7 @@ export class TimeSeriesExplorer extends React.Component { !isEqual(previousProps.selectedEntities, this.props.selectedEntities) || previousProps.selectedForecastId !== this.props.selectedForecastId || previousProps.selectedJobId !== this.props.selectedJobId || - previousState.functionDescription !== this.state.functionDescription; + previousProps.functionDescription !== this.props.functionDescription; this.loadSingleMetricData(fullRefresh); } @@ -965,7 +936,6 @@ export class TimeSeriesExplorer extends React.Component { zoomTo, zoomFromFocusLoaded, zoomToFocusLoaded, - functionDescription, } = this.state; const chartProps = { modelPlotEnabled, @@ -1044,15 +1014,13 @@ export class TimeSeriesExplorer extends React.Component { selectedEntities={this.props.selectedEntities} bounds={bounds} > - {functionDescription && ( - - )} + {arePartitioningFieldsProvided && ( diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts index a801a1c5ce6f58..6cd58f42f929a9 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts @@ -15,6 +15,7 @@ export const APP_STATE_ACTION = { SET_FORECAST_ID: 'SET_FORECAST_ID', SET_ZOOM: 'SET_ZOOM', UNSET_ZOOM: 'UNSET_ZOOM', + SET_FUNCTION_DESCRIPTION: 'SET_FUNCTION_DESCRIPTION', }; export const CHARTS_POINT_TARGET = 500; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_viewable_detectors.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_viewable_detectors.ts new file mode 100644 index 00000000000000..25d7751da82776 --- /dev/null +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_viewable_detectors.ts @@ -0,0 +1,29 @@ +/* + * 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 { CombinedJob } from '../../../../common/types/anomaly_detection_jobs'; +import { isTimeSeriesViewDetector } from '../../../../common/util/job_utils'; + +interface ViewableDetector { + index: number; + detector_description: string | undefined; + function: string; +} +export function getViewableDetectors(selectedJob: CombinedJob): ViewableDetector[] { + const jobDetectors = selectedJob.analysis_config.detectors; + const viewableDetectors: ViewableDetector[] = []; + jobDetectors.forEach((dtr, index) => { + if (isTimeSeriesViewDetector(selectedJob, index)) { + viewableDetectors.push({ + index, + detector_description: dtr.detector_description, + function: dtr.function, + }); + } + }); + + return viewableDetectors; +} diff --git a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts index 6d7e286a294761..d53dfa8fd19c9f 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts @@ -163,6 +163,7 @@ export function createSingleMetricViewerUrl( forecastId, entities, globalState, + functionDescription, } = params; let queryState: Partial = {}; @@ -189,6 +190,10 @@ export function createSingleMetricViewerUrl( if (entities !== undefined) { mlTimeSeriesExplorer.entities = entities; } + if (functionDescription !== undefined) { + mlTimeSeriesExplorer.functionDescription = functionDescription; + } + appState.mlTimeSeriesExplorer = mlTimeSeriesExplorer; if (zoom) appState.zoom = zoom;