[ML] Explain log rate spikes: Improve analysis workflow. (#137192)

- Adds an empty prompt when there's no current selection to describe the necessary click action and some general feature description.
- Adds an option to clear the current selection and start from scratch.
- Adds badges to the brush areas to label them as Baseline and Deviation.
- Adds EuiPanel as wrappers for the chart and table sections. The distinct sections make it easier to identify to which context an action like the analysis refresh button refers.
This commit is contained in:
Walter Rafelsberger 2022-07-26 18:54:42 +02:00 committed by GitHub
parent 153b6b7a19
commit 97c0b32159
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 193 additions and 45 deletions

View file

@ -56,7 +56,7 @@ interface DualBrushProps {
windowParameters: WindowParameters;
min: number;
max: number;
onChange?: (windowParameters: WindowParameters) => void;
onChange?: (windowParameters: WindowParameters, windowPxParameters: WindowParameters) => void;
marginLeft: number;
width: number;
}
@ -129,6 +129,12 @@ 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' &&
@ -141,6 +147,8 @@ export function DualBrush({
newWindowParameters.deviationMin = px2ts(newDeviationMin);
newWindowParameters.deviationMax = px2ts(newDeviationMax);
newBrushPx.deviationMin = newDeviationMin;
newBrushPx.deviationMax = newDeviationMax;
d3.select(this)
.transition()
@ -158,6 +166,8 @@ export function DualBrush({
newWindowParameters.baselineMin = px2ts(newBaselineMin);
newWindowParameters.baselineMax = px2ts(newBaselineMax);
newBrushPx.baselineMin = newBaselineMin;
newBrushPx.baselineMax = newBaselineMax;
d3.select(this)
.transition()
@ -172,7 +182,7 @@ export function DualBrush({
brushes.current[1].end = newWindowParameters.deviationMax;
if (onChange) {
onChange(newWindowParameters);
onChange(newWindowParameters, newBrushPx);
}
drawBrushes();
}

View file

@ -35,10 +35,10 @@ export const DualBrushAnnotation: FC<BrushAnnotationProps> = ({ id, min, max })
]}
id={`rect_brush_annotation_${id}`}
style={{
strokeWidth: 1,
strokeWidth: 0,
stroke: colors.lightShade,
fill: colors.lightShade,
opacity: 1,
opacity: 0.5,
}}
hideTooltips={true}
/>

View file

@ -20,8 +20,10 @@ import {
XYChartElementEvent,
XYBrushEvent,
} from '@elastic/charts';
import { EuiBadge } from '@elastic/eui';
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';
@ -37,7 +39,7 @@ export interface DocumentCountChartPoint {
}
interface DocumentCountChartProps {
brushSelectionUpdateHandler: (d: WindowParameters) => void;
brushSelectionUpdateHandler: (d: WindowParameters, force: boolean) => void;
width?: number;
chartPoints: DocumentCountChartPoint[];
chartPointsSplit?: DocumentCountChartPoint[];
@ -45,6 +47,7 @@ interface DocumentCountChartProps {
timeRangeLatest: number;
interval: number;
changePoint?: ChangePoint;
isBrushCleared: boolean;
}
const SPEC_ID = 'document_count';
@ -73,6 +76,7 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = ({
timeRangeLatest,
interval,
changePoint,
isBrushCleared,
}) => {
const {
services: { data, uiSettings, fieldFormats, charts },
@ -187,7 +191,7 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = ({
);
setOriginalWindowParameters(wp);
setWindowParameters(wp);
brushSelectionUpdateHandler(wp);
brushSelectionUpdateHandler(wp, true);
}
}
};
@ -198,10 +202,21 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = ({
WindowParameters | undefined
>();
const [windowParameters, setWindowParameters] = useState<WindowParameters | undefined>();
const [windowParametersAsPixels, setWindowParametersAsPixels] = useState<
WindowParameters | undefined
>();
function onWindowParametersChange(wp: WindowParameters) {
useEffect(() => {
if (isBrushCleared && originalWindowParameters !== undefined) {
setOriginalWindowParameters(undefined);
setWindowParameters(undefined);
}
}, [isBrushCleared, originalWindowParameters]);
function onWindowParametersChange(wp: WindowParameters, wpPx: WindowParameters) {
setWindowParameters(wp);
brushSelectionUpdateHandler(wp);
setWindowParametersAsPixels(wpPx);
brushSelectionUpdateHandler(wp, false);
}
const [mlBrushWidth, setMlBrushWidth] = useState<number>();
@ -221,17 +236,56 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = ({
<>
{isBrushVisible && (
<div className="aiopsHistogramBrushes">
<DualBrush
windowParameters={originalWindowParameters}
min={timeRangeEarliest}
max={timeRangeLatest + interval}
onChange={onWindowParametersChange}
marginLeft={mlBrushMarginLeft}
width={mlBrushWidth}
/>
<div
css={{
position: 'absolute',
'margin-left': `${
mlBrushMarginLeft + (windowParametersAsPixels?.baselineMin ?? 0)
}px`,
}}
>
<EuiBadge>
<FormattedMessage
id="xpack.aiops.documentCountChart.baselineBadgeContent"
defaultMessage="Baseline"
/>
</EuiBadge>
</div>
<div
css={{
position: 'absolute',
'margin-left': `${
mlBrushMarginLeft + (windowParametersAsPixels?.deviationMin ?? 0)
}px`,
}}
>
<EuiBadge>
<FormattedMessage
id="xpack.aiops.documentCountChart.deviationBadgeContent"
defaultMessage="Deviation"
/>
</EuiBadge>
</div>
<div
css={{
position: 'relative',
clear: 'both',
'padding-top': '20px',
'margin-bottom': '-4px',
}}
>
<DualBrush
windowParameters={originalWindowParameters}
min={timeRangeEarliest}
max={timeRangeLatest + interval}
onChange={onWindowParametersChange}
marginLeft={mlBrushMarginLeft}
width={mlBrushWidth}
/>
</div>
</div>
)}
<div style={{ width: width ?? '100%' }} data-test-subj="aiopsDocumentCountChart">
<div css={{ width: width ?? '100%' }} data-test-subj="aiopsDocumentCountChart">
<Chart
size={{
width: '100%',

View file

@ -4,8 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FC } from 'react';
import React, { useEffect, useState, FC } from 'react';
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { WindowParameters } from '@kbn/aiops-utils';
import type { ChangePoint } from '@kbn/ml-agg-utils';
@ -13,21 +16,38 @@ import { DocumentCountChart, DocumentCountChartPoint } from '../document_count_c
import { TotalCountHeader } from '../total_count_header';
import { DocumentCountStats } from '../../../get_document_stats';
const clearSelectionLabel = i18n.translate(
'xpack.aiops.documentCountContent.clearSelectionAriaLabel',
{
defaultMessage: 'Clear selection',
}
);
export interface DocumentCountContentProps {
brushSelectionUpdateHandler: (d: WindowParameters) => void;
clearSelectionHandler: () => void;
changePoint?: ChangePoint;
documentCountStats?: DocumentCountStats;
documentCountStatsSplit?: DocumentCountStats;
totalCount: number;
windowParameters?: WindowParameters;
}
export const DocumentCountContent: FC<DocumentCountContentProps> = ({
brushSelectionUpdateHandler,
clearSelectionHandler,
changePoint,
documentCountStats,
documentCountStatsSplit,
totalCount,
windowParameters,
}) => {
const [isBrushCleared, setIsBrushCleared] = useState(true);
useEffect(() => {
setIsBrushCleared(windowParameters === undefined);
}, [windowParameters]);
if (documentCountStats === undefined) {
return totalCount !== undefined ? <TotalCountHeader totalCount={totalCount} /> : null;
}
@ -48,18 +68,48 @@ export const DocumentCountContent: FC<DocumentCountContentProps> = ({
chartPointsSplit = Object.entries(buckets).map(([time, value]) => ({ time: +time, value }));
}
function brushSelectionUpdate(d: WindowParameters, force: boolean) {
if (!isBrushCleared || force) {
brushSelectionUpdateHandler(d);
}
if (force) {
setIsBrushCleared(false);
}
}
function clearSelection() {
setIsBrushCleared(true);
clearSelectionHandler();
}
return (
<>
<TotalCountHeader totalCount={totalCount} />
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem>
<TotalCountHeader totalCount={totalCount} />
</EuiFlexItem>
{!isBrushCleared && (
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={clearSelection}
size="xs"
data-test-sub="aiopsClearSelectionBadge"
>
{clearSelectionLabel}
</EuiButtonEmpty>
</EuiFlexItem>
)}
</EuiFlexGroup>
{documentCountStats.interval !== undefined && (
<DocumentCountChart
brushSelectionUpdateHandler={brushSelectionUpdateHandler}
brushSelectionUpdateHandler={brushSelectionUpdate}
chartPoints={chartPoints}
chartPointsSplit={chartPointsSplit}
timeRangeEarliest={timeRangeEarliest}
timeRangeLatest={timeRangeLatest}
interval={documentCountStats.interval}
changePoint={changePoint}
isBrushCleared={isBrushCleared}
/>
)}
</>

View file

@ -7,6 +7,7 @@
import React, { useCallback, useEffect, useMemo, useState, FC } from 'react';
import {
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
@ -14,7 +15,7 @@ import {
EuiPageContentBody,
EuiPageContentHeader,
EuiPageContentHeaderSection,
EuiSpacer,
EuiPanel,
EuiTitle,
} from '@elastic/eui';
@ -22,6 +23,7 @@ import type { DataView } from '@kbn/data-views-plugin/public';
import type { WindowParameters } from '@kbn/aiops-utils';
import type { ChangePoint } from '@kbn/ml-agg-utils';
import { Filter, Query } from '@kbn/es-query';
import { FormattedMessage } from '@kbn/i18n-react';
import { SavedSearch } from '@kbn/discover-plugin/public';
import { useAiOpsKibana } from '../../kibana_context';
@ -159,6 +161,12 @@ export const ExplainLogRateSpikesPage: FC<ExplainLogRateSpikesPageProps> = ({
});
}, [dataService, searchQueryLanguage, searchString]);
function clearSelection() {
setWindowParameters(undefined);
setPinnedChangePoint(null);
setSelectedChangePoint(null);
}
return (
<EuiPageBody data-test-subj="aiopsIndexPage" paddingSize="none" panelled={false}>
<EuiFlexGroup gutterSize="none">
@ -209,32 +217,58 @@ export const ExplainLogRateSpikesPage: FC<ExplainLogRateSpikesPageProps> = ({
</EuiFlexItem>
{overallDocStats?.totalCount !== undefined && (
<EuiFlexItem>
<DocumentCountContent
brushSelectionUpdateHandler={setWindowParameters}
documentCountStats={overallDocStats.documentCountStats}
documentCountStatsSplit={
currentSelectedChangePoint ? selectedDocStats.documentCountStats : undefined
}
totalCount={totalCount}
changePoint={currentSelectedChangePoint}
/>
</EuiFlexItem>
)}
<EuiSpacer size="m" />
{earliest !== undefined && latest !== undefined && windowParameters !== undefined && (
<EuiFlexItem>
<ExplainLogRateSpikesAnalysis
dataView={dataView}
earliest={earliest}
latest={latest}
windowParameters={windowParameters}
searchQuery={searchQuery}
onPinnedChangePoint={setPinnedChangePoint}
onSelectedChangePoint={setSelectedChangePoint}
selectedChangePoint={currentSelectedChangePoint}
/>
<EuiPanel paddingSize="m">
<DocumentCountContent
brushSelectionUpdateHandler={setWindowParameters}
clearSelectionHandler={clearSelection}
documentCountStats={overallDocStats.documentCountStats}
documentCountStatsSplit={
currentSelectedChangePoint ? selectedDocStats.documentCountStats : undefined
}
totalCount={totalCount}
changePoint={currentSelectedChangePoint}
windowParameters={windowParameters}
/>
</EuiPanel>
</EuiFlexItem>
)}
<EuiFlexItem>
<EuiPanel paddingSize="m">
{earliest !== undefined && latest !== undefined && windowParameters !== undefined && (
<ExplainLogRateSpikesAnalysis
dataView={dataView}
earliest={earliest}
latest={latest}
windowParameters={windowParameters}
searchQuery={searchQuery}
onPinnedChangePoint={setPinnedChangePoint}
onSelectedChangePoint={setSelectedChangePoint}
selectedChangePoint={currentSelectedChangePoint}
/>
)}
{windowParameters === undefined && (
<EuiEmptyPrompt
title={
<h2>
<FormattedMessage
id="xpack.aiops.explainLogRateSpikesPage.emptyPromptTitle"
defaultMessage="Click a spike in the histogram chart to start the analysis."
/>
</h2>
}
titleSize="xs"
body={
<p>
<FormattedMessage
id="xpack.aiops.explainLogRateSpikesPage.emptyPromptBody"
defaultMessage="The explain log rate spikes feature identifies statistically significant field/value combinations that contribute to a log rate spike."
/>
</p>
}
/>
)}
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentBody>
</EuiPageBody>