Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ML] Anomaly Detection: Adds popover links menu to anomaly explorer charts. #186587

Merged
merged 22 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,7 @@ export const Explorer: FC<ExplorerUIProps> = ({
{...{
...chartsData,
severity,
tableData,
timefilter,
mlLocator,
timeBuckets,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { TableSeverity } from '../../components/controls/select_severity/se
import { SelectSeverityUI } from '../../components/controls/select_severity/select_severity';
import type { ExplorerChartsData } from './explorer_charts_container_service';
import type { MlLocator } from '../../../../common/types/locator';
import type { AnomaliesTableData } from '../explorer_utils';

interface ExplorerAnomaliesContainerProps {
id: string;
Expand All @@ -27,6 +28,7 @@ interface ExplorerAnomaliesContainerProps {
severity: TableSeverity;
setSeverity: (severity: TableSeverity) => void;
mlLocator: MlLocator;
tableData: AnomaliesTableData;
timeBuckets: TimeBuckets;
timefilter: TimefilterContract;
onSelectEntity: (
Expand Down Expand Up @@ -54,6 +56,7 @@ export const ExplorerAnomaliesContainer: FC<ExplorerAnomaliesContainerProps> = (
severity,
setSeverity,
mlLocator,
tableData,
timeBuckets,
timefilter,
onSelectEntity,
Expand Down Expand Up @@ -89,6 +92,7 @@ export const ExplorerAnomaliesContainer: FC<ExplorerAnomaliesContainerProps> = (
...chartsData,
severity: severity.val,
mlLocator,
tableData,
timeBuckets,
timefilter,
timeRange,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import React from 'react';
import d3 from 'd3';
import moment from 'moment';

import { EuiPopover } from '@elastic/eui';

import { i18n } from '@kbn/i18n';
import {
getFormattedSeverityScore,
Expand All @@ -25,6 +27,8 @@ import {
import { formatHumanReadableDateTime } from '@kbn/ml-date-utils';
import { context } from '@kbn/kibana-react-plugin/public';

import { LinksMenuUI } from '../../components/anomalies_table/links_menu';
import { RuleEditorFlyout } from '../../components/rule_editor';
import { formatValue } from '../../formatters/format_value';
import {
getChartType,
Expand All @@ -41,6 +45,7 @@ import { CHART_HEIGHT, TRANSPARENT_BACKGROUND } from './constants';
import { filter } from 'rxjs';
import { drawCursor } from './utils/draw_anomaly_explorer_charts_cursor';

const popoverMenuOffset = 0;
const CONTENT_WRAPPER_HEIGHT = 215;
const SCHEDULED_EVENT_MARKER_HEIGHT = 5;

Expand All @@ -57,6 +62,7 @@ export class ExplorerChartDistribution extends React.Component {
static propTypes = {
seriesConfig: PropTypes.object,
severity: PropTypes.number,
tableData: PropTypes.object,
tooltipService: PropTypes.object.isRequired,
cursor$: PropTypes.object,
};
Expand All @@ -65,7 +71,9 @@ export class ExplorerChartDistribution extends React.Component {
super(props);
this.chartScales = undefined;
this.cursorStateSubscription = undefined;
this.state = { popoverData: null, popoverCoords: [0, 0], showRuleEditorFlyout: () => {} };
}

componentDidMount() {
this.renderChart();
this.cursorStateSubscription = this.props.cursor$
Expand Down Expand Up @@ -428,6 +436,8 @@ export class ExplorerChartDistribution extends React.Component {
dots.exit().remove();
}

const that = this;

function drawRareChartHighlightedSpan() {
if (showSelectedInterval === false) return;
// Draws a rectangle which highlights the time span that has been selected for view.
Expand Down Expand Up @@ -465,6 +475,11 @@ export class ExplorerChartDistribution extends React.Component {
.enter()
.append('circle')
.attr('r', LINE_CHART_ANOMALY_RADIUS)
.on('click', function (d) {
d3.event.preventDefault();
if (d.anomalyScore === undefined) return;
showAnomalyPopover(d, this);
})
// Don't use an arrow function since we need access to `this`.
.on('mouseover', function (d) {
showLineChartTooltip(d, this);
Expand Down Expand Up @@ -511,6 +526,37 @@ export class ExplorerChartDistribution extends React.Component {
);
}

function showAnomalyPopover(marker, circle) {
const anomalyTime = marker.date;

// The table items could be aggregated, so we have to find the item
// that has the closest timestamp to the selected anomaly from the chart.
const tableItem = that.props.tableData.anomalies.reduce((closestItem, currentItem) => {
const closestItemDelta = Math.abs(anomalyTime - closestItem.source.timestamp);
const currentItemDelta = Math.abs(anomalyTime - currentItem.source.timestamp);
return currentItemDelta < closestItemDelta ? currentItem : closestItem;
}, that.props.tableData.anomalies[0]);

if (tableItem) {
// Overwrite the timestamp of the possibly aggregated table item with the
// timestamp of the anomaly clicked in the chart so we're able to pick
// the right baseline and deviation time ranges for Log Rate Analysis.
tableItem.source.timestamp = anomalyTime;

// Calculate the relative coordinates of the clicked anomaly marker
// so we're able to position the popover actions menu above it.
const dotRect = circle.getBoundingClientRect();
const rootRect = that.rootNode.getBoundingClientRect();
const x = Math.round(dotRect.x + dotRect.width / 2 - rootRect.x);
const y = Math.round(dotRect.y + dotRect.height / 2 - rootRect.y) - popoverMenuOffset;

// Hide any active tooltip
that.props.tooltipService.hide();
// Set the popover state to enable the actions menu
that.setState({ popoverData: tableItem, popoverCoords: [x, y] });
}
}

function showLineChartTooltip(marker, circle) {
// Show the time and metric values in the tooltip.
// Uses date, value, upper, lower and anomalyScore (optional) marker properties.
Expand Down Expand Up @@ -642,6 +688,22 @@ export class ExplorerChartDistribution extends React.Component {
this.rootNode = componentNode;
}

closePopover() {
this.setState({ popoverData: null, popoverCoords: [0, 0] });
}

setShowRuleEditorFlyoutFunction = (func) => {
this.setState({
showRuleEditorFlyout: func,
});
};

unsetShowRuleEditorFlyoutFunction = () => {
this.setState({
showRuleEditorFlyout: () => {},
});
};

render() {
const { seriesConfig } = this.props;

Expand All @@ -654,10 +716,47 @@ export class ExplorerChartDistribution extends React.Component {
const isLoading = seriesConfig.loading;

return (
<div className="ml-explorer-chart" ref={this.setRef.bind(this)}>
{isLoading && <LoadingIndicator height={CONTENT_WRAPPER_HEIGHT} />}
{!isLoading && <div className="content-wrapper" />}
</div>
<>
<RuleEditorFlyout
setShowFunction={this.setShowRuleEditorFlyoutFunction}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not to be addressed in this PR but I think we should move away from this pattern of having a child set a parent component setter since it's an antipattern. I know this is old code and this is the way it's done in the Rule editor flyout currently but just think we should add this to the list of things to improve.

unsetShowFunction={this.unsetShowRuleEditorFlyoutFunction}
/>
{this.state.popoverData !== null && (
<div
style={{
position: 'absolute',
marginLeft: this.state.popoverCoords[0],
marginTop: this.state.popoverCoords[1],
}}
>
<EuiPopover
isOpen={true}
closePopover={() => this.closePopover()}
panelPaddingSize="none"
anchorPosition="upLeft"
>
<LinksMenuUI
anomaly={this.state.popoverData}
bounds={{
min: moment(seriesConfig.plotEarliest),
max: moment(seriesConfig.plotLatest),
}}
showMapsLink={false}
showViewSeriesLink={true}
peteharverson marked this conversation as resolved.
Show resolved Hide resolved
isAggregatedData={this.props.tableData.interval !== 'second'}
interval={this.props.tableData.interval}
showRuleEditorFlyout={this.state.showRuleEditorFlyout}
onItemClick={() => this.closePopover()}
sourceIndicesWithGeoFields={this.props.sourceIndicesWithGeoFields}
/>
</EuiPopover>
</div>
)}
<div className="ml-explorer-chart" ref={this.setRef.bind(this)}>
{isLoading && <LoadingIndicator height={CONTENT_WRAPPER_HEIGHT} />}
{!isLoading && <div className="content-wrapper" />}
</div>
</>
);
}
}
Loading