[ML] Explain log rate spikes: Fix data out of date when brush selection changes (#137791)

* update run analysis button content when selection changges

* fix brush overlap causing endless rerender

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

* fix resize triggering rerun analysis prompt

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

* add comments to getSnappedWindowParameters function

* use memo instead of using component state

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

* fix eslint error and simplify usememo callback

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Melissa Alvarez 2022-08-03 12:11:15 -04:00 committed by GitHub
parent 8231e0f47f
commit b17579afa3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 185 additions and 35 deletions

View file

@ -12,6 +12,7 @@ import * as d3Scale from 'd3-scale';
import * as d3Selection from 'd3-selection';
import * as d3Transition from 'd3-transition';
import { getSnappedWindowParameters } from '@kbn/aiops-utils';
import type { WindowParameters } from '@kbn/aiops-utils';
import './dual_brush.scss';
@ -58,6 +59,7 @@ interface DualBrushProps {
max: number;
onChange?: (windowParameters: WindowParameters, windowPxParameters: WindowParameters) => void;
marginLeft: number;
snapTimestamps?: number[];
width: number;
}
@ -67,6 +69,7 @@ export function DualBrush({
max,
onChange,
marginLeft,
snapTimestamps,
width,
}: DualBrushProps) {
const d3BrushContainer = useRef(null);
@ -129,12 +132,6 @@ export function DualBrush({
deviationMin: px2ts(deviationSelection[0]),
deviationMax: px2ts(deviationSelection[1]),
};
const newBrushPx = {
baselineMin: baselineSelection[0],
baselineMax: baselineSelection[1],
deviationMin: deviationSelection[0],
deviationMax: deviationSelection[1],
};
if (
id === 'deviation' &&
@ -147,14 +144,6 @@ export function DualBrush({
newWindowParameters.deviationMin = px2ts(newDeviationMin);
newWindowParameters.deviationMax = px2ts(newDeviationMax);
newBrushPx.deviationMin = newDeviationMin;
newBrushPx.deviationMax = newDeviationMax;
d3.select(this)
.transition()
.duration(200)
// @ts-expect-error call doesn't allow the brush move function
.call(brushes.current[1].brush.move, [newDeviationMin, newDeviationMax]);
} else if (
id === 'baseline' &&
deviationSelection &&
@ -166,23 +155,56 @@ export function DualBrush({
newWindowParameters.baselineMin = px2ts(newBaselineMin);
newWindowParameters.baselineMax = px2ts(newBaselineMax);
newBrushPx.baselineMin = newBaselineMin;
newBrushPx.baselineMax = newBaselineMax;
}
const snappedWindowParameters = snapTimestamps
? getSnappedWindowParameters(newWindowParameters, snapTimestamps)
: newWindowParameters;
const newBrushPx = {
baselineMin: x(snappedWindowParameters.baselineMin) ?? 0,
baselineMax: x(snappedWindowParameters.baselineMax) ?? 0,
deviationMin: x(snappedWindowParameters.deviationMin) ?? 0,
deviationMax: x(snappedWindowParameters.deviationMax) ?? 0,
};
if (
id === 'baseline' &&
(baselineSelection[0] !== newBrushPx.baselineMin ||
baselineSelection[1] !== newBrushPx.baselineMax)
) {
d3.select(this)
.transition()
.duration(200)
// @ts-expect-error call doesn't allow the brush move function
.call(brushes.current[0].brush.move, [newBaselineMin, newBaselineMax]);
.call(brushes.current[0].brush.move, [
newBrushPx.baselineMin,
newBrushPx.baselineMax,
]);
}
brushes.current[0].start = newWindowParameters.baselineMin;
brushes.current[0].end = newWindowParameters.baselineMax;
brushes.current[1].start = newWindowParameters.deviationMin;
brushes.current[1].end = newWindowParameters.deviationMax;
if (
id === 'deviation' &&
(deviationSelection[0] !== newBrushPx.deviationMin ||
deviationSelection[1] !== newBrushPx.deviationMax)
) {
d3.select(this)
.transition()
.duration(200)
// @ts-expect-error call doesn't allow the brush move function
.call(brushes.current[1].brush.move, [
newBrushPx.deviationMin,
newBrushPx.deviationMax,
]);
}
brushes.current[0].start = snappedWindowParameters.baselineMin;
brushes.current[0].end = snappedWindowParameters.baselineMax;
brushes.current[1].start = snappedWindowParameters.deviationMin;
brushes.current[1].end = snappedWindowParameters.deviationMax;
if (onChange) {
onChange(newWindowParameters, newBrushPx);
onChange(snappedWindowParameters, newBrushPx);
}
drawBrushes();
}
@ -255,7 +277,17 @@ export function DualBrush({
drawBrushes();
}
}, [min, max, width, baselineMin, baselineMax, deviationMin, deviationMax, onChange]);
}, [
min,
max,
width,
baselineMin,
baselineMax,
deviationMin,
deviationMax,
snapTimestamps,
onChange,
]);
return (
<>

View file

@ -5,7 +5,14 @@
* 2.0.
*/
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiProgress, EuiText } from '@elastic/eui';
import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiIconTip,
EuiProgress,
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
@ -19,6 +26,7 @@ interface ProgressControlProps {
onRefresh: () => void;
onCancel: () => void;
isRunning: boolean;
shouldRerunAnalysis: boolean;
}
export function ProgressControls({
@ -27,6 +35,7 @@ export function ProgressControls({
onRefresh,
onCancel,
isRunning,
shouldRerunAnalysis,
}: ProgressControlProps) {
return (
<EuiFlexGroup>
@ -56,11 +65,34 @@ export function ProgressControls({
</EuiFlexItem>
<EuiFlexItem grow={false}>
{!isRunning && (
<EuiButton size="s" onClick={onRefresh}>
<FormattedMessage
id="xpack.aiops.rerunAnalysisButtonTitle"
defaultMessage="Rerun analysis"
/>
<EuiButton
size="s"
onClick={onRefresh}
color={shouldRerunAnalysis ? 'warning' : 'primary'}
>
<EuiFlexGroup>
<EuiFlexItem>
<FormattedMessage
id="xpack.aiops.rerunAnalysisButtonTitle"
defaultMessage="Rerun analysis"
/>
</EuiFlexItem>
{shouldRerunAnalysis && (
<>
<EuiFlexItem>
<EuiIconTip
aria-label="Warning"
type="alert"
color="warning"
content={i18n.translate('xpack.aiops.rerunAnalysisTooltipContent', {
defaultMessage:
'Analysis data may be out of date due to selection update. Rerun analysis.',
})}
/>
</EuiFlexItem>
</>
)}
</EuiFlexGroup>
</EuiButton>
)}
{isRunning && (

View file

@ -65,3 +65,63 @@ export const getWindowParameters = (
deviationMax: Math.round(deviationMax),
};
};
/**
*
* Converts window paramaters from the brushes to snap the brushes to the chart histogram bar width and ensure timestamps
* correspond to bucket timestamps
*
* @param windowParameters time range definition for baseline and deviation to be used by spike log analysis
* @param snapTimestamps time range definition that always corresponds to histogram bucket timestamps
* @returns WindowParameters
*/
export const getSnappedWindowParameters = (
windowParameters: WindowParameters,
snapTimestamps: number[]
): WindowParameters => {
const snappedBaselineMin = snapTimestamps.reduce((pts, cts) => {
if (
Math.abs(cts - windowParameters.baselineMin) < Math.abs(pts - windowParameters.baselineMin)
) {
return cts;
}
return pts;
}, snapTimestamps[0]);
const baselineMaxTimestamps = snapTimestamps.filter((ts) => ts > snappedBaselineMin);
const snappedBaselineMax = baselineMaxTimestamps.reduce((pts, cts) => {
if (
Math.abs(cts - windowParameters.baselineMax) < Math.abs(pts - windowParameters.baselineMax)
) {
return cts;
}
return pts;
}, baselineMaxTimestamps[0]);
const deviationMinTss = baselineMaxTimestamps.filter((ts) => ts > snappedBaselineMax);
const snappedDeviationMin = deviationMinTss.reduce((pts, cts) => {
if (
Math.abs(cts - windowParameters.deviationMin) < Math.abs(pts - windowParameters.deviationMin)
) {
return cts;
}
return pts;
}, deviationMinTss[0]);
const deviationMaxTss = deviationMinTss.filter((ts) => ts > snappedDeviationMin);
const snappedDeviationMax = deviationMaxTss.reduce((pts, cts) => {
if (
Math.abs(cts - windowParameters.deviationMax) < Math.abs(pts - windowParameters.deviationMax)
) {
return cts;
}
return pts;
}, deviationMaxTss[0]);
return {
baselineMin: snappedBaselineMin,
baselineMax: snappedBaselineMax,
deviationMin: snappedDeviationMin,
deviationMax: snappedDeviationMax,
};
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
export { getWindowParameters } from './get_window_parameters';
export { getSnappedWindowParameters, getWindowParameters } from './get_window_parameters';
export type { WindowParameters } from './get_window_parameters';
export { streamFactory } from './stream_factory';
export { useFetchStream } from './use_fetch_stream';

View file

@ -26,7 +26,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { IUiSettingsClient } from '@kbn/core/public';
import { DualBrush, DualBrushAnnotation } from '@kbn/aiops-components';
import { getWindowParameters } from '@kbn/aiops-utils';
import { getSnappedWindowParameters, getWindowParameters } from '@kbn/aiops-utils';
import type { WindowParameters } from '@kbn/aiops-utils';
import { MULTILAYER_TIME_AXIS_STYLE } from '@kbn/charts-plugin/common';
import type { ChangePoint } from '@kbn/ml-agg-utils';
@ -148,6 +148,14 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [chartPointsSplit, timeRangeEarliest, timeRangeLatest, interval]);
const snapTimestamps = useMemo(() => {
return adjustedChartPoints
.map((d) => d.time)
.filter(function (arg: unknown): arg is number {
return typeof arg === 'number';
});
}, [adjustedChartPoints]);
const timefilterUpdateHandler = useCallback(
(ranges: { from: number; to: number }) => {
data.query.timefilter.timefilter.setTime({
@ -189,9 +197,10 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = ({
xDomain.min,
xDomain.max + interval
);
setOriginalWindowParameters(wp);
setWindowParameters(wp);
brushSelectionUpdateHandler(wp, true);
const wpSnap = getSnappedWindowParameters(wp, snapTimestamps);
setOriginalWindowParameters(wpSnap);
setWindowParameters(wpSnap);
brushSelectionUpdateHandler(wpSnap, true);
}
}
};
@ -280,6 +289,7 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = ({
max={timeRangeLatest + interval}
onChange={onWindowParametersChange}
marginLeft={mlBrushMarginLeft}
snapTimestamps={snapTimestamps}
width={mlBrushWidth}
/>
</div>

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import React, { useEffect, FC } from 'react';
import React, { useEffect, useMemo, useState, FC } from 'react';
import { isEqual } from 'lodash';
import { EuiEmptyPrompt } from '@elastic/eui';
@ -54,6 +55,10 @@ export const ExplainLogRateSpikesAnalysis: FC<ExplainLogRateSpikesAnalysisProps>
const { services } = useAiOpsKibana();
const basePath = services.http?.basePath.get() ?? '';
const [currentAnalysisWindowParameters, setCurrentAnalysisWindowParameters] = useState<
WindowParameters | undefined
>();
const { cancel, start, data, isRunning, error } = useFetchStream<
ApiExplainLogRateSpikes,
typeof basePath
@ -72,6 +77,7 @@ export const ExplainLogRateSpikesAnalysis: FC<ExplainLogRateSpikesAnalysisProps>
);
useEffect(() => {
setCurrentAnalysisWindowParameters(windowParameters);
start();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@ -85,9 +91,18 @@ export const ExplainLogRateSpikesAnalysis: FC<ExplainLogRateSpikesAnalysisProps>
if (onSelectedChangePoint) {
onSelectedChangePoint(null);
}
setCurrentAnalysisWindowParameters(windowParameters);
start();
}
const shouldRerunAnalysis = useMemo(
() =>
currentAnalysisWindowParameters !== undefined &&
!isEqual(currentAnalysisWindowParameters, windowParameters),
[currentAnalysisWindowParameters, windowParameters]
);
const showSpikeAnalysisTable = data?.changePoints.length > 0;
return (
@ -98,6 +113,7 @@ export const ExplainLogRateSpikesAnalysis: FC<ExplainLogRateSpikesAnalysisProps>
isRunning={isRunning}
onRefresh={startHandler}
onCancel={cancel}
shouldRerunAnalysis={shouldRerunAnalysis}
/>
{!isRunning && !showSpikeAnalysisTable && (
<EuiEmptyPrompt