Skip to content

Commit 77da781

Browse files
qn895kibanamachine
andauthored
[ML] Persist URL state for Anomaly detection jobs using metric function (#83507)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
1 parent 7a7057e commit 77da781

File tree

9 files changed

+167
-75
lines changed

9 files changed

+167
-75
lines changed

x-pack/plugins/ml/common/types/ml_url_generator.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export interface TimeSeriesExplorerAppState {
132132
forecastId?: string;
133133
detectorIndex?: number;
134134
entities?: Record<string, string>;
135+
functionDescription?: string;
135136
};
136137
query?: any;
137138
}
@@ -145,6 +146,7 @@ export interface TimeSeriesExplorerPageState
145146
entities?: Record<string, string>;
146147
forecastId?: string;
147148
globalState?: MlCommonGlobalState;
149+
functionDescription?: string;
148150
}
149151

150152
export type TimeSeriesExplorerUrlState = MLPageState<

x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ export const TimeSeriesExplorerUrlStateManager: FC<TimeSeriesExplorerUrlStateMan
161161
: +appState?.mlTimeSeriesExplorer?.detectorIndex || 0;
162162
const selectedEntities = isJobChange ? undefined : appState?.mlTimeSeriesExplorer?.entities;
163163
const selectedForecastId = isJobChange ? undefined : appState?.mlTimeSeriesExplorer?.forecastId;
164+
165+
const selectedFunctionDescription = isJobChange
166+
? undefined
167+
: appState?.mlTimeSeriesExplorer?.functionDescription;
168+
164169
const zoom: AppStateZoom | undefined = isJobChange
165170
? undefined
166171
: appState?.mlTimeSeriesExplorer?.zoom;
@@ -184,14 +189,19 @@ export const TimeSeriesExplorerUrlStateManager: FC<TimeSeriesExplorerUrlStateMan
184189
delete mlTimeSeriesExplorer.entities;
185190
delete mlTimeSeriesExplorer.forecastId;
186191
delete mlTimeSeriesExplorer.zoom;
192+
delete mlTimeSeriesExplorer.functionDescription;
187193
break;
188194

189195
case APP_STATE_ACTION.SET_DETECTOR_INDEX:
190196
mlTimeSeriesExplorer.detectorIndex = payload;
197+
delete mlTimeSeriesExplorer.functionDescription;
198+
191199
break;
192200

193201
case APP_STATE_ACTION.SET_ENTITIES:
194202
mlTimeSeriesExplorer.entities = payload;
203+
delete mlTimeSeriesExplorer.functionDescription;
204+
195205
break;
196206

197207
case APP_STATE_ACTION.SET_FORECAST_ID:
@@ -206,6 +216,10 @@ export const TimeSeriesExplorerUrlStateManager: FC<TimeSeriesExplorerUrlStateMan
206216
case APP_STATE_ACTION.UNSET_ZOOM:
207217
delete mlTimeSeriesExplorer.zoom;
208218
break;
219+
220+
case APP_STATE_ACTION.SET_FUNCTION_DESCRIPTION:
221+
mlTimeSeriesExplorer.functionDescription = payload;
222+
break;
209223
}
210224

211225
setAppState('mlTimeSeriesExplorer', mlTimeSeriesExplorer);
@@ -315,6 +329,7 @@ export const TimeSeriesExplorerUrlStateManager: FC<TimeSeriesExplorerUrlStateMan
315329
timefilter,
316330
zoom: zoomProp,
317331
invalidTimeRangeError,
332+
functionDescription: selectedFunctionDescription,
318333
}}
319334
/>
320335
);

x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33
* or more contributor license agreements. Licensed under the Elastic License;
44
* you may not use this file except in compliance with the Elastic License.
55
*/
6-
import React from 'react';
6+
import React, { useCallback, useEffect } from 'react';
77
import { EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui';
88
import { i18n } from '@kbn/i18n';
9+
import { mlJobService } from '../../../services/job_service';
10+
import { getFunctionDescription, isMetricDetector } from '../../get_function_description';
11+
import { useToastNotificationService } from '../../../services/toast_notification_service';
12+
import { ML_JOB_AGGREGATION } from '../../../../../common/constants/aggregation_types';
13+
import type { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs';
914

1015
const plotByFunctionOptions = [
1116
{
@@ -30,11 +35,70 @@ const plotByFunctionOptions = [
3035
export const PlotByFunctionControls = ({
3136
functionDescription,
3237
setFunctionDescription,
38+
selectedDetectorIndex,
39+
selectedJobId,
40+
selectedEntities,
3341
}: {
3442
functionDescription: undefined | string;
3543
setFunctionDescription: (func: string) => void;
44+
selectedDetectorIndex: number;
45+
selectedJobId: string;
46+
selectedEntities: Record<string, any>;
3647
}) => {
48+
const toastNotificationService = useToastNotificationService();
49+
50+
const getFunctionDescriptionToPlot = useCallback(
51+
async (
52+
_selectedDetectorIndex: number,
53+
_selectedEntities: Record<string, any>,
54+
_selectedJobId: string,
55+
_selectedJob: CombinedJob
56+
) => {
57+
const functionToPlot = await getFunctionDescription(
58+
{
59+
selectedDetectorIndex: _selectedDetectorIndex,
60+
selectedEntities: _selectedEntities,
61+
selectedJobId: _selectedJobId,
62+
selectedJob: _selectedJob,
63+
},
64+
toastNotificationService
65+
);
66+
setFunctionDescription(functionToPlot);
67+
},
68+
[setFunctionDescription, toastNotificationService]
69+
);
70+
71+
useEffect(() => {
72+
if (functionDescription !== undefined) {
73+
return;
74+
}
75+
const selectedJob = mlJobService.getJob(selectedJobId);
76+
if (
77+
// set if only entity controls are picked
78+
selectedEntities !== undefined &&
79+
functionDescription === undefined &&
80+
isMetricDetector(selectedJob, selectedDetectorIndex)
81+
) {
82+
const detector = selectedJob.analysis_config.detectors[selectedDetectorIndex];
83+
if (detector?.function === ML_JOB_AGGREGATION.METRIC) {
84+
getFunctionDescriptionToPlot(
85+
selectedDetectorIndex,
86+
selectedEntities,
87+
selectedJobId,
88+
selectedJob
89+
);
90+
}
91+
}
92+
}, [
93+
setFunctionDescription,
94+
selectedDetectorIndex,
95+
selectedEntities,
96+
selectedJobId,
97+
functionDescription,
98+
]);
99+
37100
if (functionDescription === undefined) return null;
101+
38102
return (
39103
<EuiFlexItem grow={false}>
40104
<EuiFormRow

x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ import {
1919
EntityControlProps,
2020
} from '../entity_control/entity_control';
2121
import { getControlsForDetector } from '../../get_controls_for_detector';
22-
// @ts-ignore
23-
import { getViewableDetectors } from '../../timeseriesexplorer';
2422
import {
2523
ML_ENTITY_FIELDS_CONFIG,
2624
PartitionFieldConfig,
@@ -29,6 +27,7 @@ import {
2927
import { useStorage } from '../../../contexts/ml/use_storage';
3028
import { EntityFieldType } from '../../../../../common/types/anomalies';
3129
import { FieldDefinition } from '../../../services/results_service/result_service_rx';
30+
import { getViewableDetectors } from '../../timeseriesexplorer_utils/get_viewable_detectors';
3231

3332
function getEntityControlOptions(fieldValues: FieldDefinition['values']): ComboBoxOption[] {
3433
if (!Array.isArray(fieldValues)) {
@@ -63,7 +62,7 @@ const getDefaultFieldConfig = (
6362
};
6463

6564
interface SeriesControlsProps {
66-
selectedDetectorIndex: any;
65+
selectedDetectorIndex: number;
6766
selectedJobId: JobId;
6867
bounds: any;
6968
appStateHandler: Function;

x-pack/plugins/ml/public/application/timeseriesexplorer/get_function_description.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ import { getControlsForDetector } from './get_controls_for_detector';
1111
import { getCriteriaFields } from './get_criteria_fields';
1212
import { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
1313
import { ML_JOB_AGGREGATION } from '../../../common/constants/aggregation_types';
14+
import { getViewableDetectors } from './timeseriesexplorer_utils/get_viewable_detectors';
15+
16+
export function isMetricDetector(selectedJob: CombinedJob, selectedDetectorIndex: number) {
17+
const detectors = getViewableDetectors(selectedJob);
18+
if (Array.isArray(detectors) && detectors.length >= selectedDetectorIndex) {
19+
const detector = selectedJob.analysis_config.detectors[selectedDetectorIndex];
20+
if (detector?.function === ML_JOB_AGGREGATION.METRIC) {
21+
return true;
22+
}
23+
}
24+
return false;
25+
}
1426

1527
/**
1628
* Get the function description from the record with the highest anomaly score
@@ -31,18 +43,15 @@ export const getFunctionDescription = async (
3143
) => {
3244
// if the detector's function is metric, fetch the highest scoring anomaly record
3345
// and set to plot the function_description (avg/min/max) of that record by default
34-
if (
35-
selectedJob?.analysis_config?.detectors[selectedDetectorIndex]?.function !==
36-
ML_JOB_AGGREGATION.METRIC
37-
)
38-
return;
46+
if (!isMetricDetector(selectedJob, selectedDetectorIndex)) return;
3947

4048
const entityControls = getControlsForDetector(
4149
selectedDetectorIndex,
4250
selectedEntities,
4351
selectedJobId
4452
);
4553
const criteriaFields = getCriteriaFields(selectedDetectorIndex, entityControls);
54+
4655
try {
4756
const resp = await mlResultsService
4857
.getRecordsForCriteria([selectedJob.job_id], criteriaFields, 0, null, null, 1)

0 commit comments

Comments
 (0)