diff --git a/x-pack/packages/ml/aiops_components/src/dual_brush/dual_brush.tsx b/x-pack/packages/ml/aiops_components/src/dual_brush/dual_brush.tsx index 1c03b909ded1c..b117d11909fd8 100644 --- a/x-pack/packages/ml/aiops_components/src/dual_brush/dual_brush.tsx +++ b/x-pack/packages/ml/aiops_components/src/dual_brush/dual_brush.tsx @@ -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, [ + newBrushPx.baselineMin, + newBrushPx.baselineMax, + ]); + } + + 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[0].brush.move, [newBaselineMin, newBaselineMax]); + .call(brushes.current[1].brush.move, [ + newBrushPx.deviationMin, + newBrushPx.deviationMax, + ]); } - brushes.current[0].start = newWindowParameters.baselineMin; - brushes.current[0].end = newWindowParameters.baselineMax; - brushes.current[1].start = newWindowParameters.deviationMin; - brushes.current[1].end = newWindowParameters.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 ( <> diff --git a/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx b/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx index 5130a511a92c3..be82f11f778b2 100644 --- a/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx +++ b/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx @@ -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 ( @@ -56,11 +65,34 @@ export function ProgressControls({ {!isRunning && ( - - + + + + + + {shouldRerunAnalysis && ( + <> + + + + + )} + )} {isRunning && ( diff --git a/x-pack/packages/ml/aiops_utils/src/get_window_parameters.ts b/x-pack/packages/ml/aiops_utils/src/get_window_parameters.ts index 0cdcf891b053f..9408e360ba5eb 100644 --- a/x-pack/packages/ml/aiops_utils/src/get_window_parameters.ts +++ b/x-pack/packages/ml/aiops_utils/src/get_window_parameters.ts @@ -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, + }; +}; diff --git a/x-pack/packages/ml/aiops_utils/src/index.ts b/x-pack/packages/ml/aiops_utils/src/index.ts index a02ecc2d41958..554d7cf23b0c2 100644 --- a/x-pack/packages/ml/aiops_utils/src/index.ts +++ b/x-pack/packages/ml/aiops_utils/src/index.ts @@ -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'; diff --git a/x-pack/plugins/aiops/public/components/document_count_content/document_count_chart/document_count_chart.tsx b/x-pack/plugins/aiops/public/components/document_count_content/document_count_chart/document_count_chart.tsx index 4f2c723791e1c..9069046bd884a 100644 --- a/x-pack/plugins/aiops/public/components/document_count_content/document_count_chart/document_count_chart.tsx +++ b/x-pack/plugins/aiops/public/components/document_count_content/document_count_chart/document_count_chart.tsx @@ -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 = ({ // 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 = ({ 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 = ({ max={timeRangeLatest + interval} onChange={onWindowParametersChange} marginLeft={mlBrushMarginLeft} + snapTimestamps={snapTimestamps} width={mlBrushWidth} /> diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx index 6dfa853fe3723..18b99b9e68f61 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx @@ -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 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 ); useEffect(() => { + setCurrentAnalysisWindowParameters(windowParameters); start(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -85,9 +91,18 @@ export const ExplainLogRateSpikesAnalysis: FC 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 isRunning={isRunning} onRefresh={startHandler} onCancel={cancel} + shouldRerunAnalysis={shouldRerunAnalysis} /> {!isRunning && !showSpikeAnalysisTable && (