mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[ML] Add option for anomaly charts for metric detector should plot min, mean or max as appropriate (#81662)
This commit is contained in:
parent
2c05957582
commit
9c984f4723
21 changed files with 333 additions and 34 deletions
|
@ -38,6 +38,7 @@ import {
|
|||
import { MULTI_BUCKET_IMPACT } from '../../../../common/constants/multi_bucket_impact';
|
||||
import { formatValue } from '../../formatters/format_value';
|
||||
import { MAX_CHARS } from './anomalies_table_constants';
|
||||
import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types';
|
||||
|
||||
const TIME_FIELD_NAME = 'timestamp';
|
||||
|
||||
|
@ -130,7 +131,8 @@ function getDetailsItems(anomaly, examples, filter) {
|
|||
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.functionTitle', {
|
||||
defaultMessage: 'function',
|
||||
}),
|
||||
description: source.function !== 'metric' ? source.function : source.function_description,
|
||||
description:
|
||||
source.function !== ML_JOB_AGGREGATION.METRIC ? source.function : source.function_description,
|
||||
});
|
||||
|
||||
if (source.field_name !== undefined) {
|
||||
|
|
|
@ -13,6 +13,8 @@ import { parseInterval } from '../../../../common/util/parse_interval';
|
|||
import { getEntityFieldList } from '../../../../common/util/anomaly_utils';
|
||||
import { buildConfigFromDetector } from '../../util/chart_config_builder';
|
||||
import { mlJobService } from '../../services/job_service';
|
||||
import { mlFunctionToESAggregation } from '../../../../common/util/job_utils';
|
||||
import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types';
|
||||
|
||||
// Builds the chart configuration for the provided anomaly record, returning
|
||||
// an object with properties used for the display (series function and field, aggregation interval etc),
|
||||
|
@ -48,6 +50,10 @@ export function buildConfig(record) {
|
|||
// define the metric series to be plotted.
|
||||
config.entityFields = getEntityFieldList(record);
|
||||
|
||||
if (record.function === ML_JOB_AGGREGATION.METRIC) {
|
||||
config.metricFunction = mlFunctionToESAggregation(record.function_description);
|
||||
}
|
||||
|
||||
// Build the tooltip data for the chart info icon, showing further details on what is being plotted.
|
||||
let functionLabel = config.metricFunction;
|
||||
if (config.metricFieldName !== undefined) {
|
||||
|
|
|
@ -46,8 +46,6 @@ const ML_TIME_FIELD_NAME = 'timestamp';
|
|||
const USE_OVERALL_CHART_LIMITS = false;
|
||||
const MAX_CHARTS_PER_ROW = 4;
|
||||
|
||||
// callback(getDefaultChartsData());
|
||||
|
||||
export const anomalyDataChange = function (
|
||||
chartsContainerWidth,
|
||||
anomalyRecords,
|
||||
|
|
|
@ -38,6 +38,7 @@ import { useResolver } from '../use_resolver';
|
|||
import { basicResolvers } from '../resolvers';
|
||||
import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
|
||||
import { useTimefilter } from '../../contexts/kibana';
|
||||
import { useToastNotificationService } from '../../services/toast_notification_service';
|
||||
|
||||
export const timeSeriesExplorerRouteFactory = (
|
||||
navigateToPath: NavigateToPath,
|
||||
|
@ -88,6 +89,7 @@ export const TimeSeriesExplorerUrlStateManager: FC<TimeSeriesExplorerUrlStateMan
|
|||
config,
|
||||
jobsWithTimeRange,
|
||||
}) => {
|
||||
const toastNotificationService = useToastNotificationService();
|
||||
const [appState, setAppState] = useUrlState('_a');
|
||||
const [globalState, setGlobalState] = useUrlState('_g');
|
||||
const [lastRefresh, setLastRefresh] = useState(0);
|
||||
|
@ -293,6 +295,7 @@ export const TimeSeriesExplorerUrlStateManager: FC<TimeSeriesExplorerUrlStateMan
|
|||
return (
|
||||
<TimeSeriesExplorer
|
||||
{...{
|
||||
toastNotificationService,
|
||||
appStateHandler,
|
||||
autoZoomDuration,
|
||||
bounds,
|
||||
|
|
|
@ -24,8 +24,9 @@ export const resultsApiProvider = (httpService: HttpService) => ({
|
|||
latestMs: number,
|
||||
dateFormatTz: string,
|
||||
maxRecords: number,
|
||||
maxExamples: number,
|
||||
influencersFilterQuery: any
|
||||
maxExamples?: number,
|
||||
influencersFilterQuery?: any,
|
||||
functionDescription?: string
|
||||
) {
|
||||
const body = JSON.stringify({
|
||||
jobIds,
|
||||
|
@ -39,6 +40,7 @@ export const resultsApiProvider = (httpService: HttpService) => ({
|
|||
maxRecords,
|
||||
maxExamples,
|
||||
influencersFilterQuery,
|
||||
functionDescription,
|
||||
});
|
||||
|
||||
return httpService.http$<any>({
|
||||
|
|
|
@ -19,6 +19,7 @@ import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils';
|
|||
import { JobId } from '../../../../common/types/anomaly_detection_jobs';
|
||||
import { MlApiServices } from '../ml_api_service';
|
||||
import { CriteriaField } from './index';
|
||||
import { aggregationTypeTransform } from '../../../../common/util/anomaly_utils';
|
||||
|
||||
interface ResultResponse {
|
||||
success: boolean;
|
||||
|
@ -347,9 +348,10 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
|
|||
jobIds: string[],
|
||||
criteriaFields: CriteriaField[],
|
||||
threshold: any,
|
||||
earliestMs: number,
|
||||
latestMs: number,
|
||||
maxResults: number | undefined
|
||||
earliestMs: number | null,
|
||||
latestMs: number | null,
|
||||
maxResults: number | undefined,
|
||||
functionDescription?: string
|
||||
): Observable<RecordsForCriteria> {
|
||||
const obj: RecordsForCriteria = { success: true, records: [] };
|
||||
|
||||
|
@ -400,6 +402,19 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
|
|||
});
|
||||
});
|
||||
|
||||
if (functionDescription !== undefined) {
|
||||
const mlFunctionToPlotIfMetric =
|
||||
functionDescription !== undefined
|
||||
? aggregationTypeTransform.toML(functionDescription)
|
||||
: functionDescription;
|
||||
|
||||
boolCriteria.push({
|
||||
term: {
|
||||
function_description: mlFunctionToPlotIfMetric,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return mlApiServices.results
|
||||
.anomalySearch$(
|
||||
{
|
||||
|
|
|
@ -76,6 +76,7 @@ export function resultsServiceProvider(
|
|||
criteriaFields: any[],
|
||||
earliestMs: number,
|
||||
latestMs: number,
|
||||
intervalMs: number
|
||||
intervalMs: number,
|
||||
actualPlotFunctionIfMetric?: string
|
||||
): Promise<any>;
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
ANOMALY_SWIM_LANE_HARD_LIMIT,
|
||||
SWIM_LANE_DEFAULT_PAGE_SIZE,
|
||||
} from '../../explorer/explorer_constants';
|
||||
import { aggregationTypeTransform } from '../../../../common/util/anomaly_utils';
|
||||
|
||||
/**
|
||||
* Service for carrying out Elasticsearch queries to obtain data for the Ml Results dashboards.
|
||||
|
@ -1293,7 +1294,14 @@ export function resultsServiceProvider(mlApiServices) {
|
|||
// criteria, time range, and aggregation interval.
|
||||
// criteriaFields parameter must be an array, with each object in the array having 'fieldName'
|
||||
// 'fieldValue' properties.
|
||||
getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, latestMs, intervalMs) {
|
||||
getRecordMaxScoreByTime(
|
||||
jobId,
|
||||
criteriaFields,
|
||||
earliestMs,
|
||||
latestMs,
|
||||
intervalMs,
|
||||
actualPlotFunctionIfMetric
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const obj = {
|
||||
success: true,
|
||||
|
@ -1321,7 +1329,18 @@ export function resultsServiceProvider(mlApiServices) {
|
|||
},
|
||||
});
|
||||
});
|
||||
if (actualPlotFunctionIfMetric !== undefined) {
|
||||
const mlFunctionToPlotIfMetric =
|
||||
actualPlotFunctionIfMetric !== undefined
|
||||
? aggregationTypeTransform.toML(actualPlotFunctionIfMetric)
|
||||
: actualPlotFunctionIfMetric;
|
||||
|
||||
mustCriteria.push({
|
||||
term: {
|
||||
function_description: mlFunctionToPlotIfMetric,
|
||||
},
|
||||
});
|
||||
}
|
||||
mlApiServices.results
|
||||
.anomalySearch(
|
||||
{
|
||||
|
|
|
@ -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 { PlotByFunctionControls } from './plot_function_controls';
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
const plotByFunctionOptions = [
|
||||
{
|
||||
value: 'mean',
|
||||
text: i18n.translate('xpack.ml.timeSeriesExplorer.plotByAvgOptionLabel', {
|
||||
defaultMessage: 'mean',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'min',
|
||||
text: i18n.translate('xpack.ml.timeSeriesExplorer.plotByMinOptionLabel', {
|
||||
defaultMessage: 'min',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'max',
|
||||
text: i18n.translate('xpack.ml.timeSeriesExplorer.plotByMaxOptionLabel', {
|
||||
defaultMessage: 'max',
|
||||
}),
|
||||
},
|
||||
];
|
||||
export const PlotByFunctionControls = ({
|
||||
functionDescription,
|
||||
setFunctionDescription,
|
||||
}: {
|
||||
functionDescription: undefined | string;
|
||||
setFunctionDescription: (func: string) => void;
|
||||
}) => {
|
||||
if (functionDescription === undefined) return null;
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.timeSeriesExplorer.metricPlotByOption', {
|
||||
defaultMessage: 'Function',
|
||||
})}
|
||||
>
|
||||
<EuiSelect
|
||||
options={plotByFunctionOptions}
|
||||
value={functionDescription}
|
||||
onChange={(e) => setFunctionDescription(e.target.value)}
|
||||
aria-label={i18n.translate('xpack.ml.timeSeriesExplorer.metricPlotByOptionLabel', {
|
||||
defaultMessage: 'Pick function to plot by (min, max, or average) if metric function',
|
||||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
|
@ -1475,6 +1475,22 @@ class TimeseriesChartIntl extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
if (marker.metricFunction) {
|
||||
tooltipData.push({
|
||||
label: i18n.translate(
|
||||
'xpack.ml.timeSeriesExplorer.timeSeriesChart.metricActualPlotFunctionLabel',
|
||||
{
|
||||
defaultMessage: 'function',
|
||||
}
|
||||
),
|
||||
value: marker.metricFunction,
|
||||
seriesIdentifier: {
|
||||
key: seriesKey,
|
||||
},
|
||||
valueAccessor: 'metric_function',
|
||||
});
|
||||
}
|
||||
|
||||
if (modelPlotEnabled === false) {
|
||||
// Show actual/typical when available except for rare detectors.
|
||||
// Rare detectors always have 1 as actual and the probability as typical.
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Updates criteria fields for API calls, e.g. getAnomaliesTableData
|
||||
* @param detectorIndex
|
||||
* @param entities
|
||||
*/
|
||||
export const getCriteriaFields = (detectorIndex: number, entities: Record<string, any>) => {
|
||||
// Only filter on the entity if the field has a value.
|
||||
const nonBlankEntities = entities.filter(
|
||||
(entity: { fieldValue: any }) => entity.fieldValue !== null
|
||||
);
|
||||
return [
|
||||
{
|
||||
fieldName: 'detector_index',
|
||||
fieldValue: detectorIndex,
|
||||
},
|
||||
...nonBlankEntities,
|
||||
];
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { mlResultsService } from '../services/results_service';
|
||||
import { ToastNotificationService } from '../services/toast_notification_service';
|
||||
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';
|
||||
|
||||
/**
|
||||
* Get the function description from the record with the highest anomaly score
|
||||
*/
|
||||
export const getFunctionDescription = async (
|
||||
{
|
||||
selectedDetectorIndex,
|
||||
selectedEntities,
|
||||
selectedJobId,
|
||||
selectedJob,
|
||||
}: {
|
||||
selectedDetectorIndex: number;
|
||||
selectedEntities: Record<string, any>;
|
||||
selectedJobId: string;
|
||||
selectedJob: CombinedJob;
|
||||
},
|
||||
toastNotificationService: ToastNotificationService
|
||||
) => {
|
||||
// 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;
|
||||
|
||||
const entityControls = getControlsForDetector(
|
||||
selectedDetectorIndex,
|
||||
selectedEntities,
|
||||
selectedJobId
|
||||
);
|
||||
const criteriaFields = getCriteriaFields(selectedDetectorIndex, entityControls);
|
||||
try {
|
||||
const resp = await mlResultsService
|
||||
.getRecordsForCriteria([selectedJob.job_id], criteriaFields, 0, null, null, 1)
|
||||
.toPromise();
|
||||
if (Array.isArray(resp?.records) && resp.records.length === 1) {
|
||||
const highestScoringAnomaly = resp.records[0];
|
||||
return highestScoringAnomaly?.function_description;
|
||||
}
|
||||
} catch (error) {
|
||||
toastNotificationService.displayErrorToast(
|
||||
error,
|
||||
i18n.translate('xpack.ml.timeSeriesExplorer.highestAnomalyScoreErrorToastTitle', {
|
||||
defaultMessage: 'An error occurred getting record with the highest anomaly score',
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
|
@ -26,7 +26,8 @@ function getMetricData(
|
|||
entityFields: EntityField[],
|
||||
earliestMs: number,
|
||||
latestMs: number,
|
||||
intervalMs: number
|
||||
intervalMs: number,
|
||||
esMetricFunction?: string
|
||||
): Observable<ModelPlotOutput> {
|
||||
if (
|
||||
isModelPlotChartableForDetector(job, detectorIndex) &&
|
||||
|
@ -88,7 +89,7 @@ function getMetricData(
|
|||
chartConfig.datafeedConfig.indices,
|
||||
entityFields,
|
||||
chartConfig.datafeedConfig.query,
|
||||
chartConfig.metricFunction,
|
||||
esMetricFunction ?? chartConfig.metricFunction,
|
||||
chartConfig.metricFieldName,
|
||||
chartConfig.timeField,
|
||||
earliestMs,
|
||||
|
|
|
@ -82,6 +82,9 @@ import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/
|
|||
import { getControlsForDetector } from './get_controls_for_detector';
|
||||
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';
|
||||
|
||||
// Used to indicate the chart is being plotted across
|
||||
// all partition field values, where the cardinality of the field cannot be
|
||||
|
@ -140,6 +143,8 @@ function getTimeseriesexplorerDefaultState() {
|
|||
zoomTo: undefined,
|
||||
zoomFromFocusLoaded: undefined,
|
||||
zoomToFocusLoaded: undefined,
|
||||
// Sets function to plot by if original function is metric
|
||||
functionDescription: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -217,6 +222,12 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
setFunctionDescription = (selectedFuction) => {
|
||||
this.setState({
|
||||
functionDescription: selectedFuction,
|
||||
});
|
||||
};
|
||||
|
||||
previousChartProps = {};
|
||||
previousShowAnnotations = undefined;
|
||||
previousShowForecast = undefined;
|
||||
|
@ -270,7 +281,7 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
*/
|
||||
getFocusData(selection) {
|
||||
const { selectedJobId, selectedForecastId, selectedDetectorIndex } = this.props;
|
||||
const { modelPlotEnabled } = this.state;
|
||||
const { modelPlotEnabled, functionDescription } = this.state;
|
||||
const selectedJob = mlJobService.getJob(selectedJobId);
|
||||
const entityControls = this.getControlsForDetector();
|
||||
|
||||
|
@ -292,6 +303,7 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
entityControls.filter((entity) => entity.fieldValue !== null),
|
||||
searchBounds,
|
||||
selectedJob,
|
||||
functionDescription,
|
||||
TIME_FIELD_NAME
|
||||
);
|
||||
}
|
||||
|
@ -322,6 +334,7 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
tableInterval,
|
||||
tableSeverity,
|
||||
} = this.props;
|
||||
const { functionDescription } = this.state;
|
||||
const selectedJob = mlJobService.getJob(selectedJobId);
|
||||
const entityControls = this.getControlsForDetector();
|
||||
|
||||
|
@ -335,7 +348,10 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
earliestMs,
|
||||
latestMs,
|
||||
dateFormatTz,
|
||||
ANOMALIES_TABLE_DEFAULT_QUERY_SIZE
|
||||
ANOMALIES_TABLE_DEFAULT_QUERY_SIZE,
|
||||
undefined,
|
||||
undefined,
|
||||
functionDescription
|
||||
)
|
||||
.pipe(
|
||||
map((resp) => {
|
||||
|
@ -378,6 +394,24 @@ 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);
|
||||
};
|
||||
|
@ -392,13 +426,13 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
zoom,
|
||||
} = this.props;
|
||||
|
||||
const { loadCounter: currentLoadCounter } = this.state;
|
||||
const { loadCounter: currentLoadCounter, functionDescription } = this.state;
|
||||
|
||||
const currentSelectedJob = mlJobService.getJob(selectedJobId);
|
||||
|
||||
if (currentSelectedJob === undefined) {
|
||||
return;
|
||||
}
|
||||
const functionToPlotByIfMetric = aggregationTypeTransform.toES(functionDescription);
|
||||
|
||||
this.contextChartSelectedInitCallDone = false;
|
||||
|
||||
|
@ -533,7 +567,8 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
nonBlankEntities,
|
||||
searchBounds.min.valueOf(),
|
||||
searchBounds.max.valueOf(),
|
||||
stateUpdate.contextAggregationInterval.asMilliseconds()
|
||||
stateUpdate.contextAggregationInterval.asMilliseconds(),
|
||||
functionToPlotByIfMetric
|
||||
)
|
||||
.toPromise()
|
||||
.then((resp) => {
|
||||
|
@ -556,7 +591,8 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
this.getCriteriaFields(detectorIndex, entityControls),
|
||||
searchBounds.min.valueOf(),
|
||||
searchBounds.max.valueOf(),
|
||||
stateUpdate.contextAggregationInterval.asMilliseconds()
|
||||
stateUpdate.contextAggregationInterval.asMilliseconds(),
|
||||
functionToPlotByIfMetric
|
||||
)
|
||||
.then((resp) => {
|
||||
const fullRangeRecordScoreData = processRecordScoreResults(resp.results);
|
||||
|
@ -687,7 +723,6 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
if (detectorId !== selectedDetectorIndex) {
|
||||
appStateHandler(APP_STATE_ACTION.SET_DETECTOR_INDEX, detectorId);
|
||||
}
|
||||
|
||||
// Populate the map of jobs / detectors / field formatters for the selected IDs and refresh.
|
||||
mlFieldFormatService.populateFormats([jobId]).catch((err) => {
|
||||
console.log('Error populating field formats:', err);
|
||||
|
@ -810,7 +845,7 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
this.componentDidUpdate();
|
||||
}
|
||||
|
||||
componentDidUpdate(previousProps) {
|
||||
componentDidUpdate(previousProps, previousState) {
|
||||
if (previousProps === undefined || previousProps.selectedJobId !== this.props.selectedJobId) {
|
||||
this.contextChartSelectedInitCallDone = false;
|
||||
this.setState({ fullRefresh: false, loading: true }, () => {
|
||||
|
@ -818,6 +853,15 @@ 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
|
||||
|
@ -840,7 +884,8 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
!isEqual(previousProps.selectedDetectorIndex, this.props.selectedDetectorIndex) ||
|
||||
!isEqual(previousProps.selectedEntities, this.props.selectedEntities) ||
|
||||
previousProps.selectedForecastId !== this.props.selectedForecastId ||
|
||||
previousProps.selectedJobId !== this.props.selectedJobId
|
||||
previousProps.selectedJobId !== this.props.selectedJobId ||
|
||||
previousState.functionDescription !== this.state.functionDescription
|
||||
) {
|
||||
const fullRefresh =
|
||||
previousProps === undefined ||
|
||||
|
@ -848,7 +893,8 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
!isEqual(previousProps.selectedDetectorIndex, this.props.selectedDetectorIndex) ||
|
||||
!isEqual(previousProps.selectedEntities, this.props.selectedEntities) ||
|
||||
previousProps.selectedForecastId !== this.props.selectedForecastId ||
|
||||
previousProps.selectedJobId !== this.props.selectedJobId;
|
||||
previousProps.selectedJobId !== this.props.selectedJobId ||
|
||||
previousState.functionDescription !== this.state.functionDescription;
|
||||
this.loadSingleMetricData(fullRefresh);
|
||||
}
|
||||
|
||||
|
@ -919,8 +965,8 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
zoomTo,
|
||||
zoomFromFocusLoaded,
|
||||
zoomToFocusLoaded,
|
||||
functionDescription,
|
||||
} = this.state;
|
||||
|
||||
const chartProps = {
|
||||
modelPlotEnabled,
|
||||
contextChartData,
|
||||
|
@ -939,7 +985,6 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
zoomToFocusLoaded,
|
||||
autoZoomDuration,
|
||||
};
|
||||
|
||||
const jobs = createTimeSeriesJobData(mlJobService.jobs);
|
||||
|
||||
if (selectedDetectorIndex === undefined || mlJobService.getJob(selectedJobId) === undefined) {
|
||||
|
@ -992,7 +1037,6 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
<EuiSpacer size="m" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<SeriesControls
|
||||
selectedJobId={selectedJobId}
|
||||
appStateHandler={this.props.appStateHandler}
|
||||
|
@ -1000,6 +1044,16 @@ 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}
|
||||
/>
|
||||
)}
|
||||
|
||||
{arePartitioningFieldsProvided && (
|
||||
<EuiFlexItem style={{ textAlign: 'right' }}>
|
||||
<EuiFormRow hasEmptyLabelSpace style={{ maxWidth: '100%' }}>
|
||||
|
@ -1014,7 +1068,6 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
</EuiFlexItem>
|
||||
)}
|
||||
</SeriesControls>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
{fullRefresh && loading === true && (
|
||||
|
|
|
@ -26,6 +26,7 @@ import { mlForecastService } from '../../services/forecast_service';
|
|||
import { mlFunctionToESAggregation } from '../../../../common/util/job_utils';
|
||||
import { GetAnnotationsResponse } from '../../../../common/types/annotations';
|
||||
import { ANNOTATION_EVENT_USER } from '../../../../common/constants/annotations';
|
||||
import { aggregationTypeTransform } from '../../../../common/util/anomaly_utils';
|
||||
|
||||
export interface Interval {
|
||||
asMilliseconds: () => number;
|
||||
|
@ -51,8 +52,14 @@ export function getFocusData(
|
|||
modelPlotEnabled: boolean,
|
||||
nonBlankEntities: any[],
|
||||
searchBounds: any,
|
||||
selectedJob: Job
|
||||
selectedJob: Job,
|
||||
functionDescription?: string | undefined
|
||||
): Observable<FocusData> {
|
||||
const esFunctionToPlotIfMetric =
|
||||
functionDescription !== undefined
|
||||
? aggregationTypeTransform.toES(functionDescription)
|
||||
: functionDescription;
|
||||
|
||||
return forkJoin([
|
||||
// Query 1 - load metric data across selected time range.
|
||||
mlTimeSeriesSearchService.getMetricData(
|
||||
|
@ -61,7 +68,8 @@ export function getFocusData(
|
|||
nonBlankEntities,
|
||||
searchBounds.min.valueOf(),
|
||||
searchBounds.max.valueOf(),
|
||||
focusAggregationInterval.asMilliseconds()
|
||||
focusAggregationInterval.asMilliseconds(),
|
||||
esFunctionToPlotIfMetric
|
||||
),
|
||||
// Query 2 - load all the records across selected time range for the chart anomaly markers.
|
||||
mlResultsService.getRecordsForCriteria(
|
||||
|
@ -70,7 +78,8 @@ export function getFocusData(
|
|||
0,
|
||||
searchBounds.min.valueOf(),
|
||||
searchBounds.max.valueOf(),
|
||||
ANOMALIES_TABLE_DEFAULT_QUERY_SIZE
|
||||
ANOMALIES_TABLE_DEFAULT_QUERY_SIZE,
|
||||
functionDescription
|
||||
),
|
||||
// Query 3 - load any scheduled events for the selected job.
|
||||
mlResultsService.getScheduledEventsByBucket(
|
||||
|
@ -143,7 +152,8 @@ export function getFocusData(
|
|||
focusChartData,
|
||||
anomalyRecords,
|
||||
focusAggregationInterval,
|
||||
modelPlotEnabled
|
||||
modelPlotEnabled,
|
||||
functionDescription
|
||||
);
|
||||
focusChartData = processScheduledEventsForChart(focusChartData, scheduledEvents);
|
||||
|
||||
|
|
|
@ -16,7 +16,8 @@ export function processDataForFocusAnomalies(
|
|||
chartData: any,
|
||||
anomalyRecords: any,
|
||||
aggregationInterval: any,
|
||||
modelPlotEnabled: any
|
||||
modelPlotEnabled: any,
|
||||
functionDescription: any
|
||||
): any;
|
||||
|
||||
export function processScheduledEventsForChart(chartData: any, scheduledEvents: any): any;
|
||||
|
|
|
@ -19,6 +19,7 @@ import { parseInterval } from '../../../../common/util/parse_interval';
|
|||
import { getBoundsRoundedToInterval, getTimeBucketsFromCache } from '../../util/time_buckets';
|
||||
|
||||
import { CHARTS_POINT_TARGET, TIME_FIELD_NAME } from '../timeseriesexplorer_constants';
|
||||
import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types';
|
||||
|
||||
// create new job objects based on standard job config objects
|
||||
// new job objects just contain job id, bucket span in seconds and a selected flag.
|
||||
|
@ -100,7 +101,8 @@ export function processDataForFocusAnomalies(
|
|||
chartData,
|
||||
anomalyRecords,
|
||||
aggregationInterval,
|
||||
modelPlotEnabled
|
||||
modelPlotEnabled,
|
||||
functionDescription
|
||||
) {
|
||||
const timesToAddPointsFor = [];
|
||||
|
||||
|
@ -142,6 +144,12 @@ export function processDataForFocusAnomalies(
|
|||
// Look for a chart point with the same time as the record.
|
||||
// If none found, find closest time in chartData set.
|
||||
const recordTime = record[TIME_FIELD_NAME];
|
||||
if (
|
||||
record.function === ML_JOB_AGGREGATION.METRIC &&
|
||||
record.function_description !== functionDescription
|
||||
)
|
||||
return;
|
||||
|
||||
const chartPoint = findChartPointForAnomalyTime(chartData, recordTime, aggregationInterval);
|
||||
if (chartPoint !== undefined) {
|
||||
// If chart aggregation interval > bucket span, there may be more than
|
||||
|
@ -160,6 +168,10 @@ export function processDataForFocusAnomalies(
|
|||
chartPoint.value = record.actual;
|
||||
}
|
||||
|
||||
if (record.function === ML_JOB_AGGREGATION.METRIC) {
|
||||
chartPoint.value = Array.isArray(record.actual) ? record.actual[0] : record.actual;
|
||||
}
|
||||
|
||||
chartPoint.actual = record.actual;
|
||||
chartPoint.typical = record.typical;
|
||||
} else {
|
||||
|
|
|
@ -54,7 +54,8 @@ export function resultsServiceProvider(mlClient: MlClient) {
|
|||
dateFormatTz: string,
|
||||
maxRecords: number = ANOMALIES_TABLE_DEFAULT_QUERY_SIZE,
|
||||
maxExamples: number = DEFAULT_MAX_EXAMPLES,
|
||||
influencersFilterQuery: any
|
||||
influencersFilterQuery?: any,
|
||||
functionDescription?: string
|
||||
) {
|
||||
// Build the query to return the matching anomaly record results.
|
||||
// Add criteria for the time range, record score, plus any specified job IDs.
|
||||
|
@ -102,6 +103,13 @@ export function resultsServiceProvider(mlClient: MlClient) {
|
|||
},
|
||||
});
|
||||
});
|
||||
if (functionDescription !== undefined) {
|
||||
boolCriteria.push({
|
||||
term: {
|
||||
function_description: functionDescription,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (influencersFilterQuery !== undefined) {
|
||||
boolCriteria.push(influencersFilterQuery);
|
||||
|
|
|
@ -36,6 +36,7 @@ function getAnomaliesTableData(mlClient: MlClient, payload: any) {
|
|||
maxRecords,
|
||||
maxExamples,
|
||||
influencersFilterQuery,
|
||||
functionDescription,
|
||||
} = payload;
|
||||
return rs.getAnomaliesTableData(
|
||||
jobIds,
|
||||
|
@ -48,7 +49,8 @@ function getAnomaliesTableData(mlClient: MlClient, payload: any) {
|
|||
dateFormatTz,
|
||||
maxRecords,
|
||||
maxExamples,
|
||||
influencersFilterQuery
|
||||
influencersFilterQuery,
|
||||
functionDescription
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ export const anomaliesTableDataSchema = schema.object({
|
|||
maxRecords: schema.number(),
|
||||
maxExamples: schema.maybe(schema.number()),
|
||||
influencersFilterQuery: schema.maybe(schema.any()),
|
||||
functionDescription: schema.maybe(schema.nullable(schema.string())),
|
||||
});
|
||||
|
||||
export const categoryDefinitionSchema = schema.object({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue