mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[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:
parent
153b6b7a19
commit
97c0b32159
5 changed files with 193 additions and 45 deletions
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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%',
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue