mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
7a7057eba7
commit
77da781144
9 changed files with 167 additions and 75 deletions
|
@ -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<
|
||||
|
|
|
@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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' }}>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue