[ML] Persist URL state for Anomaly detection jobs using metric function (#83507)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Quynh Nguyen 2020-11-18 10:57:22 -06:00 committed by GitHub
parent 7a7057eba7
commit 77da781144
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 167 additions and 75 deletions

View file

@ -132,6 +132,7 @@ export interface TimeSeriesExplorerAppState {
forecastId?: string;
detectorIndex?: number;
entities?: Record<string, string>;
functionDescription?: string;
};
query?: any;
}
@ -145,6 +146,7 @@ export interface TimeSeriesExplorerPageState
entities?: Record<string, string>;
forecastId?: string;
globalState?: MlCommonGlobalState;
functionDescription?: string;
}
export type TimeSeriesExplorerUrlState = MLPageState<

View file

@ -161,6 +161,11 @@ export const TimeSeriesExplorerUrlStateManager: FC<TimeSeriesExplorerUrlStateMan
: +appState?.mlTimeSeriesExplorer?.detectorIndex || 0;
const selectedEntities = isJobChange ? undefined : appState?.mlTimeSeriesExplorer?.entities;
const selectedForecastId = isJobChange ? undefined : appState?.mlTimeSeriesExplorer?.forecastId;
const selectedFunctionDescription = isJobChange
? undefined
: appState?.mlTimeSeriesExplorer?.functionDescription;
const zoom: AppStateZoom | undefined = isJobChange
? undefined
: appState?.mlTimeSeriesExplorer?.zoom;
@ -184,14 +189,19 @@ export const TimeSeriesExplorerUrlStateManager: FC<TimeSeriesExplorerUrlStateMan
delete mlTimeSeriesExplorer.entities;
delete mlTimeSeriesExplorer.forecastId;
delete mlTimeSeriesExplorer.zoom;
delete mlTimeSeriesExplorer.functionDescription;
break;
case APP_STATE_ACTION.SET_DETECTOR_INDEX:
mlTimeSeriesExplorer.detectorIndex = payload;
delete mlTimeSeriesExplorer.functionDescription;
break;
case APP_STATE_ACTION.SET_ENTITIES:
mlTimeSeriesExplorer.entities = payload;
delete mlTimeSeriesExplorer.functionDescription;
break;
case APP_STATE_ACTION.SET_FORECAST_ID:
@ -206,6 +216,10 @@ export const TimeSeriesExplorerUrlStateManager: FC<TimeSeriesExplorerUrlStateMan
case APP_STATE_ACTION.UNSET_ZOOM:
delete mlTimeSeriesExplorer.zoom;
break;
case APP_STATE_ACTION.SET_FUNCTION_DESCRIPTION:
mlTimeSeriesExplorer.functionDescription = payload;
break;
}
setAppState('mlTimeSeriesExplorer', mlTimeSeriesExplorer);
@ -315,6 +329,7 @@ export const TimeSeriesExplorerUrlStateManager: FC<TimeSeriesExplorerUrlStateMan
timefilter,
zoom: zoomProp,
invalidTimeRangeError,
functionDescription: selectedFunctionDescription,
}}
/>
);

View file

@ -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<string, any>;
}) => {
const toastNotificationService = useToastNotificationService();
const getFunctionDescriptionToPlot = useCallback(
async (
_selectedDetectorIndex: number,
_selectedEntities: Record<string, any>,
_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 (
<EuiFlexItem grow={false}>
<EuiFormRow

View file

@ -19,8 +19,6 @@ import {
EntityControlProps,
} from '../entity_control/entity_control';
import { getControlsForDetector } from '../../get_controls_for_detector';
// @ts-ignore
import { getViewableDetectors } from '../../timeseriesexplorer';
import {
ML_ENTITY_FIELDS_CONFIG,
PartitionFieldConfig,
@ -29,6 +27,7 @@ import {
import { useStorage } from '../../../contexts/ml/use_storage';
import { EntityFieldType } from '../../../../../common/types/anomalies';
import { FieldDefinition } from '../../../services/results_service/result_service_rx';
import { getViewableDetectors } from '../../timeseriesexplorer_utils/get_viewable_detectors';
function getEntityControlOptions(fieldValues: FieldDefinition['values']): ComboBoxOption[] {
if (!Array.isArray(fieldValues)) {
@ -63,7 +62,7 @@ const getDefaultFieldConfig = (
};
interface SeriesControlsProps {
selectedDetectorIndex: any;
selectedDetectorIndex: number;
selectedJobId: JobId;
bounds: any;
appStateHandler: Function;

View file

@ -11,6 +11,18 @@ import { getControlsForDetector } from './get_controls_for_detector';
import { getCriteriaFields } from './get_criteria_fields';
import { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
import { ML_JOB_AGGREGATION } from '../../../common/constants/aggregation_types';
import { getViewableDetectors } from './timeseriesexplorer_utils/get_viewable_detectors';
export function isMetricDetector(selectedJob: CombinedJob, selectedDetectorIndex: number) {
const detectors = getViewableDetectors(selectedJob);
if (Array.isArray(detectors) && detectors.length >= 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)

View file

@ -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 && (
<PlotByFunctionControls
selectedJobId={selectedJobId}
selectedDetectorIndex={selectedDetectorIndex}
selectedEntities={this.props.selectedEntities}
functionDescription={functionDescription}
setFunctionDescription={this.setFunctionDescription}
/>
)}
<PlotByFunctionControls
selectedJobId={selectedJobId}
selectedDetectorIndex={selectedDetectorIndex}
selectedEntities={this.props.selectedEntities}
functionDescription={this.props.functionDescription}
setFunctionDescription={this.setFunctionDescription}
/>
{arePartitioningFieldsProvided && (
<EuiFlexItem style={{ textAlign: 'right' }}>

View file

@ -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;

View file

@ -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;
}

View file

@ -163,6 +163,7 @@ export function createSingleMetricViewerUrl(
forecastId,
entities,
globalState,
functionDescription,
} = params;
let queryState: Partial<TimeSeriesExplorerGlobalState> = {};
@ -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;