From 7b10f357f44a4a7156f8ff0748a170267d25b406 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 21 Jun 2024 07:53:03 +0200 Subject: [PATCH 01/16] adds popover links menu to anomaly explorer charts --- .../public/application/explorer/explorer.tsx | 1 + .../explorer_anomalies_container.tsx | 3 + .../explorer_chart_single_metric.js | 94 ++++++++++++++++++- .../explorer_charts_container.js | 4 + .../cases/anomaly_charts_attachments.tsx | 1 + .../anomaly_charts_embeddable_factory.tsx | 1 + .../anomaly_charts_react_container.tsx | 2 + 7 files changed, 101 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.tsx b/x-pack/plugins/ml/public/application/explorer/explorer.tsx index 23a4cbed76bbc..b633993247aa4 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer.tsx @@ -621,6 +621,7 @@ export const Explorer: FC = ({ {...{ ...chartsData, severity, + tableData, timefilter, mlLocator, timeBuckets, diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_anomalies_container.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_anomalies_container.tsx index d1a43559cbc98..6db1969736632 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_anomalies_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_anomalies_container.tsx @@ -27,6 +27,7 @@ interface ExplorerAnomaliesContainerProps { severity: TableSeverity; setSeverity: (severity: TableSeverity) => void; mlLocator: MlLocator; + tableData: any; timeBuckets: TimeBuckets; timefilter: TimefilterContract; onSelectEntity: ( @@ -54,6 +55,7 @@ export const ExplorerAnomaliesContainer: FC = ( severity, setSeverity, mlLocator, + tableData, timeBuckets, timefilter, onSelectEntity, @@ -91,6 +93,7 @@ export const ExplorerAnomaliesContainer: FC = ( ...chartsData, severity: severity.val, mlLocator, + tableData, timeBuckets, timefilter, timeRange, diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index 77118b376e97a..8362e84e31f45 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -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, @@ -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 { formatValue } from '../../formatters/format_value'; import { LINE_CHART_ANOMALY_RADIUS, @@ -43,6 +47,7 @@ import { CHART_HEIGHT, TRANSPARENT_BACKGROUND } from './constants'; import { filter } from 'rxjs'; import { drawCursor } from './utils/draw_anomaly_explorer_charts_cursor'; +const popoverMenuOffset = 28; const CONTENT_WRAPPER_HEIGHT = 215; const CONTENT_WRAPPER_CLASS = 'ml-explorer-chart-content-wrapper'; @@ -52,6 +57,7 @@ export class ExplorerChartSingleMetric extends React.Component { tooManyBuckets: PropTypes.bool, seriesConfig: PropTypes.object, severity: PropTypes.number.isRequired, + tableData: PropTypes.object, tooltipService: PropTypes.object.isRequired, timeBuckets: PropTypes.object.isRequired, onPointerUpdate: PropTypes.func.isRequired, @@ -63,6 +69,7 @@ export class ExplorerChartSingleMetric extends React.Component { constructor(props) { super(props); this.chartScales = undefined; + this.state = { popoverData: null, popoverCoords: [0, 0], showRuleEditorFlyout: () => {} }; } componentDidMount() { this.renderChart(); @@ -351,6 +358,8 @@ export class ExplorerChartSingleMetric extends React.Component { .attr('d', lineChartValuesLine(data)); } + const that = this; + function drawLineChartMarkers(data) { // Render circle markers for the points. // These are used for displaying tooltips on mouseover. @@ -375,9 +384,18 @@ export class ExplorerChartSingleMetric extends React.Component { .enter() .append('circle') .attr('r', LINE_CHART_ANOMALY_RADIUS) + .on('click', function (d) { + d3.event.preventDefault(); + if (d.anomalyScore === undefined) return; + console.log('CLICK', d); + showAnomalyPopover(d, this); + }) // Don't use an arrow function since we need access to `this`. .on('mouseover', function (d) { - showLineChartTooltip(d, this); + // Show the tooltip only if the actions menu isn't active + if (that.state.popoverData === null) { + showLineChartTooltip(d, this); + } }) .on('mouseout', () => tooltipService.hide()); @@ -448,6 +466,38 @@ export class ExplorerChartSingleMetric extends React.Component { .attr('y', (d) => lineChartYScale(d.value) - SCHEDULED_EVENT_SYMBOL_HEIGHT / 2); } + 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 + console.log('anomaly popover AE', tableItem); + 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. @@ -589,6 +639,10 @@ export class ExplorerChartSingleMetric extends React.Component { this.rootNode = componentNode; } + closePopover() { + this.setState({ popoverData: null, popoverCoords: [0, 0] }); + } + render() { const { seriesConfig } = this.props; @@ -601,10 +655,40 @@ export class ExplorerChartSingleMetric extends React.Component { const isLoading = seriesConfig.loading; return ( -
- {isLoading && } - {!isLoading &&
} -
+ <> + {this.state.popoverData !== null && ( +
+ this.closePopover()} + panelPaddingSize="none" + anchorPosition="upLeft" + > + this.closePopover()} + sourceIndicesWithGeoFields={this.props.sourceIndicesWithGeoFields} + /> + +
+ )} +
+ {isLoading && } + {!isLoading &&
} +
+ ); } } diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index 702aeed891ebc..23299adc54a14 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -91,6 +91,7 @@ function ExplorerChartContainer({ tooManyBuckets, wrapLabel, mlLocator, + tableData, timeBuckets, timefilter, timeRange, @@ -351,6 +352,7 @@ function ExplorerChartContainer({ {(tooltipService) => ( diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.tsx index 3f5afc4065e1f..a4abc8cce68d5 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.tsx @@ -186,6 +186,7 @@ export const getAnomalyChartsReactEmbeddableFactory = ( onLoading={onLoading} onRenderComplete={onRenderComplete} onError={onError} + tableData={tableData} timeRange$={appliedTimeRange$} />
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx index d31ee17ef0780..8ce362581e9f6 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx @@ -46,6 +46,7 @@ export interface AnomalyChartsContainerProps onRenderComplete: () => void; onLoading: (v: boolean) => void; onError: (error: Error) => void; + tableData: any; } const AnomalyChartsContainer: FC = ({ @@ -207,6 +208,7 @@ const AnomalyChartsContainer: FC = ({ severity={severity} setSeverity={setSeverity} mlLocator={mlLocator} + tableData={tableData} timeBuckets={timeBuckets} timefilter={timefilter} onSelectEntity={addEntityFieldFilter} From c003f344d9de379cbc07964e7ac98d69818b208a Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 21 Jun 2024 08:00:15 +0200 Subject: [PATCH 02/16] fix RuleEditorFlyout --- .../explorer_chart_single_metric.js | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index 8362e84e31f45..bac57034d4c57 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -28,7 +28,7 @@ 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 { LINE_CHART_ANOMALY_RADIUS, @@ -47,7 +47,7 @@ import { CHART_HEIGHT, TRANSPARENT_BACKGROUND } from './constants'; import { filter } from 'rxjs'; import { drawCursor } from './utils/draw_anomaly_explorer_charts_cursor'; -const popoverMenuOffset = 28; +const popoverMenuOffset = 0; const CONTENT_WRAPPER_HEIGHT = 215; const CONTENT_WRAPPER_CLASS = 'ml-explorer-chart-content-wrapper'; @@ -643,6 +643,18 @@ export class ExplorerChartSingleMetric extends React.Component { this.setState({ popoverData: null, popoverCoords: [0, 0] }); } + setShowRuleEditorFlyoutFunction = (func) => { + this.setState({ + showRuleEditorFlyout: func, + }); + }; + + unsetShowRuleEditorFlyoutFunction = () => { + this.setState({ + showRuleEditorFlyout: () => {}, + }); + }; + render() { const { seriesConfig } = this.props; @@ -656,6 +668,10 @@ export class ExplorerChartSingleMetric extends React.Component { return ( <> + {this.state.popoverData !== null && (
Date: Fri, 21 Jun 2024 09:16:33 +0200 Subject: [PATCH 03/16] add popover to ExplorerChartDistribution --- .../explorer_chart_distribution.js | 104 +++++++++++++++++- .../explorer_chart_single_metric.js | 3 +- .../explorer_charts_container.js | 1 + 3 files changed, 102 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js index 58052f5f35a65..6c3ed3f25aba3 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js @@ -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, @@ -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, @@ -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; @@ -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, }; @@ -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$ @@ -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. @@ -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); @@ -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. @@ -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; @@ -654,10 +716,44 @@ export class ExplorerChartDistribution extends React.Component { const isLoading = seriesConfig.loading; return ( -
- {isLoading && } - {!isLoading &&
} -
+ <> + + {this.state.popoverData !== null && ( +
+ this.closePopover()} + panelPaddingSize="none" + anchorPosition="upLeft" + > + this.closePopover()} + sourceIndicesWithGeoFields={this.props.sourceIndicesWithGeoFields} + /> + +
+ )} +
+ {isLoading && } + {!isLoading &&
} +
+ ); } } diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index bac57034d4c57..a67e297a55f42 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -71,6 +71,7 @@ export class ExplorerChartSingleMetric extends React.Component { this.chartScales = undefined; this.state = { popoverData: null, popoverCoords: [0, 0], showRuleEditorFlyout: () => {} }; } + componentDidMount() { this.renderChart(); @@ -387,7 +388,6 @@ export class ExplorerChartSingleMetric extends React.Component { .on('click', function (d) { d3.event.preventDefault(); if (d.anomalyScore === undefined) return; - console.log('CLICK', d); showAnomalyPopover(d, this); }) // Don't use an arrow function since we need access to `this`. @@ -493,7 +493,6 @@ export class ExplorerChartSingleMetric extends React.Component { // Hide any active tooltip that.props.tooltipService.hide(); // Set the popover state to enable the actions menu - console.log('anomaly popover AE', tableItem); that.setState({ popoverData: tableItem, popoverCoords: [x, y] }); } } diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index 23299adc54a14..5a06fc74a9a0b 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -332,6 +332,7 @@ function ExplorerChartContainer({ {(tooltipService) => ( Date: Fri, 21 Jun 2024 09:26:51 +0200 Subject: [PATCH 04/16] show view series actions --- .../explorer/explorer_charts/explorer_chart_distribution.js | 2 +- .../explorer/explorer_charts/explorer_chart_single_metric.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js index 6c3ed3f25aba3..48288186d208f 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js @@ -739,7 +739,7 @@ export class ExplorerChartDistribution extends React.Component { anomaly={this.state.popoverData} bounds={this.props.bounds} showMapsLink={false} - showViewSeriesLink={false} + showViewSeriesLink={true} isAggregatedData={this.props.tableData.interval !== 'second'} interval={this.props.tableData.interval} showRuleEditorFlyout={this.state.showRuleEditorFlyout} diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index a67e297a55f42..b44ac6f847860 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -689,7 +689,7 @@ export class ExplorerChartSingleMetric extends React.Component { anomaly={this.state.popoverData} bounds={this.props.bounds} showMapsLink={false} - showViewSeriesLink={false} + showViewSeriesLink={true} isAggregatedData={this.props.tableData.interval !== 'second'} interval={this.props.tableData.interval} showRuleEditorFlyout={this.state.showRuleEditorFlyout} From 0ae1576409010dca8d60725837da52681999cd7a Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 21 Jun 2024 09:38:41 +0200 Subject: [PATCH 05/16] fix embeddings --- .../ml/public/cases/anomaly_charts_attachments.tsx | 1 - .../anomaly_charts_embeddable_factory.tsx | 1 - .../anomaly_charts/anomaly_charts_react_container.tsx | 10 +++++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/cases/anomaly_charts_attachments.tsx b/x-pack/plugins/ml/public/cases/anomaly_charts_attachments.tsx index 7c2859ca2bf86..71b854100bd4d 100644 --- a/x-pack/plugins/ml/public/cases/anomaly_charts_attachments.tsx +++ b/x-pack/plugins/ml/public/cases/anomaly_charts_attachments.tsx @@ -79,7 +79,6 @@ const AnomalyChartsCaseAttachment = ({ onLoading={api.onLoading} onRenderComplete={api.onRenderComplete} onError={api.onError} - tableData={tableData} timeRange$={api.parentApi.timeRange$} /> diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.tsx index a4abc8cce68d5..3f5afc4065e1f 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.tsx @@ -186,7 +186,6 @@ export const getAnomalyChartsReactEmbeddableFactory = ( onLoading={onLoading} onRenderComplete={onRenderComplete} onError={onError} - tableData={tableData} timeRange$={appliedTimeRange$} />
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx index 8ce362581e9f6..320ebe2609a55 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx @@ -27,6 +27,7 @@ import type { AnomalyChartsAttachmentApi, } from '..'; +import type { AnomaliesTableData } from '../../application/explorer/explorer_utils'; import { ExplorerAnomaliesContainer } from '../../application/explorer/explorer_charts/explorer_anomalies_container'; import { ML_APP_LOCATOR } from '../../../common/constants/locator'; import { optionValueToThreshold } from '../../application/components/controls/select_severity/select_severity'; @@ -46,7 +47,6 @@ export interface AnomalyChartsContainerProps onRenderComplete: () => void; onLoading: (v: boolean) => void; onError: (error: Error) => void; - tableData: any; } const AnomalyChartsContainer: FC = ({ @@ -59,6 +59,14 @@ const AnomalyChartsContainer: FC = ({ onLoading, api, }) => { + const tableData: AnomaliesTableData = { + anomalies: [], + examplesByJobId: [''], + interval: 0, + jobIds: [], + showViewSeriesLink: false, + }; + const [chartWidth, setChartWidth] = useState(0); const [severity, setSeverity] = useState( optionValueToThreshold( From f07cdb42124a877e47c7661bc8c94a5545936b76 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 21 Jun 2024 13:21:57 +0200 Subject: [PATCH 06/16] fix popover in embeddings --- .../application/explorer/explorer_utils.ts | 12 +++- .../anomaly_charts_react_container.tsx | 66 +++++++++++++++++-- ...et_anomaly_charts_services_dependencies.ts | 13 ++++ x-pack/plugins/ml/public/embeddables/types.ts | 2 + 4 files changed, 87 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts index 4f8ee3556c872..acfacdda0dec6 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts @@ -42,6 +42,7 @@ import { parseInterval } from '../../../common/util/parse_interval'; import { ml } from '../services/ml_api_service'; import { mlJobService } from '../services/job_service'; import { getUiSettings } from '../util/dependency_cache'; +import { useMlKibana } from '../contexts/kibana'; import type { SwimlaneType } from './explorer_constants'; import { @@ -250,6 +251,15 @@ export function getInfluencers(selectedJobs: any[]): string[] { return influencers; } +export function useDateFormatTz(): string { + const { services } = useMlKibana(); + const { uiSettings } = services; + // Pass the timezone to the server for use when aggregating anomalies (by day / hour) for the table. + const tzConfig = uiSettings.get('dateFormat:tz'); + const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess(); + return dateFormatTz; +} + export function getDateFormatTz(): string { const uiSettings = getUiSettings(); // Pass the timezone to the server for use when aggregating anomalies (by day / hour) for the table. @@ -472,7 +482,7 @@ export async function loadAnomaliesTableData( fieldName: string, tableInterval: string, tableSeverity: number, - influencersFilterQuery: InfluencersFilterQuery + influencersFilterQuery?: InfluencersFilterQuery ): Promise { const jobIds = getSelectionJobIds(selectedCells, selectedJobs); const influencers = getSelectionInfluencers(selectedCells, fieldName); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx index 320ebe2609a55..dfffac54cef0e 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx @@ -7,6 +7,8 @@ import type { FC } from 'react'; import React, { useCallback, useState, useMemo, useEffect, useRef } from 'react'; +import moment from 'moment-timezone'; +import useMountedState from 'react-use/lib/useMountedState'; import { EuiCallOut, EuiLoadingChart, EuiResizeObserver, EuiText } from '@elastic/eui'; import type { Observable } from 'rxjs'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -27,13 +29,14 @@ import type { AnomalyChartsAttachmentApi, } from '..'; -import type { AnomaliesTableData } from '../../application/explorer/explorer_utils'; +import type { AnomaliesTableData, ExplorerJob } from '../../application/explorer/explorer_utils'; import { ExplorerAnomaliesContainer } from '../../application/explorer/explorer_charts/explorer_anomalies_container'; import { ML_APP_LOCATOR } from '../../../common/constants/locator'; import { optionValueToThreshold } from '../../application/components/controls/select_severity/select_severity'; import { EXPLORER_ENTITY_FIELD_SELECTION_TRIGGER } from '../../ui_actions/triggers'; import type { MlLocatorParams } from '../../../common/types/locator'; import { useAnomalyChartsData } from './use_anomaly_charts_data'; +import { useDateFormatTz, loadAnomaliesTableData } from '../../application/explorer/explorer_utils'; const RESIZE_THROTTLE_TIME_MS = 500; @@ -59,13 +62,15 @@ const AnomalyChartsContainer: FC = ({ onLoading, api, }) => { - const tableData: AnomaliesTableData = { + const isMounted = useMountedState(); + + const [tableData, setTableData] = useState({ anomalies: [], examplesByJobId: [''], interval: 0, jobIds: [], showViewSeriesLink: false, - }; + }); const [chartWidth, setChartWidth] = useState(0); const [severity, setSeverity] = useState( @@ -74,8 +79,12 @@ const AnomalyChartsContainer: FC = ({ ) ); const [selectedEntities, setSelectedEntities] = useState(); - const [{ uiSettings }, { data: dataServices, share, uiActions, charts: chartsService }] = - services; + const [ + { uiSettings }, + { data: dataServices, share, uiActions, charts: chartsService }, + { mlJobService }, + ] = services; + const { timefilter } = dataServices.query.timefilter; const timeRange = useObservable(timeRange$); @@ -117,6 +126,53 @@ const AnomalyChartsContainer: FC = ({ error, } = useAnomalyChartsData(api, services, chartWidth, severity.val, renderCallbacks); + const dateFormatTz = useDateFormatTz(); + + useEffect(() => { + // async IFEE + (async () => { + if (chartsData === undefined) { + return; + } + + try { + await mlJobService.loadJobsWrapper(); + + const explorerJobs: ExplorerJob[] = + chartsData.seriesToPlot.map(({ jobId, bucketSpanSeconds }) => { + return { + id: jobId, + selected: true, + bucketSpanSeconds, + modelPlotEnabled: false, + }; + }) ?? []; + + const timeRangeBounds = { + min: moment(chartsData.seriesToPlot[0].plotEarliest), + max: moment(chartsData.seriesToPlot[0].plotLatest), + }; + + const newTableData = await loadAnomaliesTableData( + undefined, + explorerJobs, + dateFormatTz, + timeRangeBounds, + 'job ID', + 'auto', + 0 + ); + + if (isMounted()) { + setTableData(newTableData); + } + } catch (err) { + console.log(err); // eslint-disable-line no-console + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [chartsData]); + // Holds the container height for previously fetched data const containerHeightRef = useRef(); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts b/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts index b0c365088beed..9c91be9545d1e 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts @@ -20,18 +20,29 @@ export const getAnomalyChartsServiceDependencies = async ( { fieldFormatServiceFactory }, { indexServiceFactory }, { mlApiServicesProvider }, + { mlJobServiceFactory }, { mlResultsServiceProvider }, + { MlCapabilitiesService }, + { toastNotificationServiceProvider }, ] = await Promise.all([ await import('../../application/services/anomaly_detector_service'), await import('../../application/services/field_format_service_factory'), await import('../../application/util/index_service'), await import('../../application/services/ml_api_service'), + await import('../../application/services/job_service'), await import('../../application/services/results_service'), + await import('../../application/capabilities/check_capabilities'), + await import('../../application/services/toast_notification_service'), ]); const httpService = new HttpService(coreStartServices.http); const anomalyDetectorService = new AnomalyDetectorService(httpService); const mlApiServices = mlApiServicesProvider(httpService); + const toastNotificationService = toastNotificationServiceProvider( + coreStartServices.notifications.toasts + ); + const mlJobService = mlJobServiceFactory(toastNotificationService, mlApiServices); const mlResultsService = mlResultsServiceProvider(mlApiServices); + const mlCapabilities = new MlCapabilitiesService(mlApiServices); const anomalyExplorerService = new AnomalyExplorerChartsService( pluginsStartServices.data.query.timefilter.timefilter, mlApiServices, @@ -56,7 +67,9 @@ export const getAnomalyChartsServiceDependencies = async ( { anomalyDetectorService, anomalyExplorerService, + mlCapabilities, mlFieldFormatService, + mlJobService, mlResultsService, }, ]; diff --git a/x-pack/plugins/ml/public/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts index 94389c69c20fc..a523c199622fb 100644 --- a/x-pack/plugins/ml/public/embeddables/types.ts +++ b/x-pack/plugins/ml/public/embeddables/types.ts @@ -230,7 +230,9 @@ export interface SingleMetricViewerComponentApi { export interface AnomalyChartsServices { anomalyDetectorService: AnomalyDetectorService; anomalyExplorerService: AnomalyExplorerChartsService; + mlCapabilities: MlCapabilitiesService; mlFieldFormatService: MlFieldFormatService; + mlJobService: MlJobService; mlResultsService: MlResultsService; mlApiServices?: MlApiServices; } From db0bd6f8adb5fc953f85b1b714a4732ee0e2d62c Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 21 Jun 2024 17:33:13 +0200 Subject: [PATCH 07/16] fix link to single metric viewer --- .../explorer/explorer_charts/explorer_chart_distribution.js | 5 ++++- .../explorer/explorer_charts/explorer_chart_single_metric.js | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js index 48288186d208f..bfcb196ca1d16 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js @@ -737,7 +737,10 @@ export class ExplorerChartDistribution extends React.Component { > Date: Fri, 21 Jun 2024 18:22:12 +0200 Subject: [PATCH 08/16] fix mlJobService --- .../components/anomalies_table/links_menu.tsx | 14 +++++++++++--- .../timeseriesexplorer_embeddable_chart.js | 15 +++++++++++---- .../anomaly_charts_react_container.tsx | 9 ++++++++- .../get_anomaly_charts_services_dependencies.ts | 9 --------- .../single_metric_viewer/get_services.ts | 4 ---- x-pack/plugins/ml/public/embeddables/types.ts | 3 --- .../single_metric_viewer/single_metric_viewer.tsx | 10 ++++++++-- 7 files changed, 38 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx index eda1df772904c..96204abec9d5a 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx @@ -42,8 +42,9 @@ import { CATEGORIZE_FIELD_TRIGGER } from '@kbn/ml-ui-actions'; import { isDefined } from '@kbn/ml-is-defined'; import { escapeQuotes } from '@kbn/es-query'; import { isQuery } from '@kbn/data-plugin/public'; - import type { TimeRangeBounds } from '@kbn/ml-time-buckets'; + +import { mlJobServiceFactory } from '../../services/job_service'; import { PLUGIN_ID } from '../../../../common/constants/app'; import { findMessageField } from '../../util/index_utils'; import { getInitialAnomaliesLayers, getInitialSourceIndexFieldLayers } from '../../../maps/util'; @@ -51,7 +52,6 @@ import { parseInterval } from '../../../../common/util/parse_interval'; import { ML_APP_LOCATOR, ML_PAGES } from '../../../../common/constants/locator'; import { getFiltersForDSLQuery } from '../../../../common/util/job_utils'; -import { mlJobService } from '../../services/job_service'; import { ml } from '../../services/ml_api_service'; import { escapeKueryForFieldValuePair, replaceStringTokens } from '../../util/string_utils'; import { getUrlForRecord, openCustomUrlWindow } from '../../util/custom_url_utils'; @@ -99,12 +99,20 @@ export const LinksMenuUI = (props: LinksMenuProps) => { const kibana = useMlKibana(); const { - services: { data, share, application, uiActions }, + services: { data, share, application, uiActions, mlServices }, } = kibana; + + const mlJobService = useMemo( + () => mlJobServiceFactory(undefined, mlServices.mlApiServices), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + const { getDataViewById, getDataViewIdFromName } = useMlIndexUtils(); const job = useMemo(() => { return mlJobService.getJob(props.anomaly.jobId); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.anomaly.jobId]); const categorizationFieldName = job.analysis_config.categorization_field_name; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js index 4c09d4fe4adb8..3ae3a587ed1bb 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js @@ -64,6 +64,7 @@ import { TimeseriesExplorerCheckbox } from './timeseriesexplorer_checkbox'; import { timeBucketsServiceFactory } from '../../util/time_buckets_service'; import { timeSeriesExplorerServiceFactory } from '../../util/time_series_explorer_service'; import { getTimeseriesexplorerDefaultState } from '../timeseriesexplorer_utils'; +import { mlJobServiceFactory } from '../../services/job_service'; // Used to indicate the chart is being plotted across // all partition field values, where the cardinality of the field cannot be @@ -282,9 +283,8 @@ export class TimeSeriesExplorerEmbeddableChart extends React.Component { ) .pipe( map((resp) => { - const { mlJobService } = this.context.services.mlServices; const anomalies = resp.anomalies; - const detectorsByJob = mlJobService.detectorsByJob; + const detectorsByJob = this.mlJobService.detectorsByJob; anomalies.forEach((anomaly) => { // Add a detector property to each anomaly. // Default to functionDescription if no description available. @@ -305,8 +305,8 @@ export class TimeSeriesExplorerEmbeddableChart extends React.Component { // Add properties used for building the links menu. // TODO - when job_service is moved server_side, move this to server endpoint. - if (has(mlJobService.customUrlsByJob, jobId)) { - anomaly.customUrls = mlJobService.customUrlsByJob[jobId]; + if (has(this.mlJobService.customUrlsByJob, jobId)) { + anomaly.customUrls = this.mlJobService.customUrlsByJob[jobId]; } }); @@ -694,6 +694,13 @@ export class TimeSeriesExplorerEmbeddableChart extends React.Component { ]); } + // Populate mlJobService to work with LinksMenuUI. + this.mlJobService = mlJobServiceFactory( + undefined, + this.context.services.mlServices.mlApiServices + ); + await this.mlJobService.loadJobsWrapper(); + this.componentDidUpdate(); } diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx index dfffac54cef0e..810446e615716 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx @@ -37,6 +37,7 @@ import { EXPLORER_ENTITY_FIELD_SELECTION_TRIGGER } from '../../ui_actions/trigge import type { MlLocatorParams } from '../../../common/types/locator'; import { useAnomalyChartsData } from './use_anomaly_charts_data'; import { useDateFormatTz, loadAnomaliesTableData } from '../../application/explorer/explorer_utils'; +import { mlJobServiceFactory } from '../../application/services/job_service'; const RESIZE_THROTTLE_TIME_MS = 500; @@ -82,9 +83,15 @@ const AnomalyChartsContainer: FC = ({ const [ { uiSettings }, { data: dataServices, share, uiActions, charts: chartsService }, - { mlJobService }, + { mlApiServices }, ] = services; + const mlJobService = useMemo( + () => mlJobServiceFactory(undefined, mlApiServices), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + const { timefilter } = dataServices.query.timefilter; const timeRange = useObservable(timeRange$); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts b/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts index 9c91be9545d1e..8ba36d8c17443 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts @@ -20,27 +20,19 @@ export const getAnomalyChartsServiceDependencies = async ( { fieldFormatServiceFactory }, { indexServiceFactory }, { mlApiServicesProvider }, - { mlJobServiceFactory }, { mlResultsServiceProvider }, { MlCapabilitiesService }, - { toastNotificationServiceProvider }, ] = await Promise.all([ await import('../../application/services/anomaly_detector_service'), await import('../../application/services/field_format_service_factory'), await import('../../application/util/index_service'), await import('../../application/services/ml_api_service'), - await import('../../application/services/job_service'), await import('../../application/services/results_service'), await import('../../application/capabilities/check_capabilities'), - await import('../../application/services/toast_notification_service'), ]); const httpService = new HttpService(coreStartServices.http); const anomalyDetectorService = new AnomalyDetectorService(httpService); const mlApiServices = mlApiServicesProvider(httpService); - const toastNotificationService = toastNotificationServiceProvider( - coreStartServices.notifications.toasts - ); - const mlJobService = mlJobServiceFactory(toastNotificationService, mlApiServices); const mlResultsService = mlResultsServiceProvider(mlApiServices); const mlCapabilities = new MlCapabilitiesService(mlApiServices); const anomalyExplorerService = new AnomalyExplorerChartsService( @@ -69,7 +61,6 @@ export const getAnomalyChartsServiceDependencies = async ( anomalyExplorerService, mlCapabilities, mlFieldFormatService, - mlJobService, mlResultsService, }, ]; diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_services.ts b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_services.ts index 8dab0f4b65ea0..ab61224d030f3 100644 --- a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_services.ts +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_services.ts @@ -26,7 +26,6 @@ export const getMlServices = async ( { indexServiceFactory }, { timeSeriesExplorerServiceFactory }, { mlApiServicesProvider }, - { mlJobServiceFactory }, { mlResultsServiceProvider }, { MlCapabilitiesService }, { timeSeriesSearchServiceFactory }, @@ -37,7 +36,6 @@ export const getMlServices = async ( await import('../../application/util/index_service'), await import('../../application/util/time_series_explorer_service'), await import('../../application/services/ml_api_service'), - await import('../../application/services/job_service'), await import('../../application/services/results_service'), await import('../../application/capabilities/check_capabilities'), await import( @@ -50,7 +48,6 @@ export const getMlServices = async ( const anomalyDetectorService = new AnomalyDetectorService(httpService); const mlApiServices = mlApiServicesProvider(httpService); const toastNotificationService = toastNotificationServiceProvider(coreStart.notifications.toasts); - const mlJobService = mlJobServiceFactory(toastNotificationService, mlApiServices); const mlResultsService = mlResultsServiceProvider(mlApiServices); const mlTimeSeriesSearchService = timeSeriesSearchServiceFactory(mlResultsService, mlApiServices); const mlTimeSeriesExplorerService = timeSeriesExplorerServiceFactory( @@ -81,7 +78,6 @@ export const getMlServices = async ( mlApiServices, mlCapabilities, mlFieldFormatService, - mlJobService, mlResultsService, mlTimeSeriesSearchService, mlTimeSeriesExplorerService, diff --git a/x-pack/plugins/ml/public/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts index a523c199622fb..2a2920ac75aee 100644 --- a/x-pack/plugins/ml/public/embeddables/types.ts +++ b/x-pack/plugins/ml/public/embeddables/types.ts @@ -32,7 +32,6 @@ import type { AnomalyDetectorService } from '../application/services/anomaly_det import type { AnomalyExplorerChartsService } from '../application/services/anomaly_explorer_charts_service'; import type { AnomalyTimelineService } from '../application/services/anomaly_timeline_service'; import type { MlFieldFormatService } from '../application/services/field_format_service'; -import type { MlJobService } from '../application/services/job_service'; import type { MlApiServices } from '../application/services/ml_api_service'; import type { MlResultsService } from '../application/services/results_service'; import type { MlTimeSeriesSearchService } from '../application/timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service'; @@ -232,7 +231,6 @@ export interface AnomalyChartsServices { anomalyExplorerService: AnomalyExplorerChartsService; mlCapabilities: MlCapabilitiesService; mlFieldFormatService: MlFieldFormatService; - mlJobService: MlJobService; mlResultsService: MlResultsService; mlApiServices?: MlApiServices; } @@ -243,7 +241,6 @@ export interface SingleMetricViewerServices { mlApiServices: MlApiServices; mlCapabilities: MlCapabilitiesService; mlFieldFormatService: MlFieldFormatService; - mlJobService: MlJobService; mlResultsService: MlResultsService; mlTimeSeriesSearchService?: MlTimeSeriesSearchService; mlTimeSeriesExplorerService?: TimeSeriesExplorerService; diff --git a/x-pack/plugins/ml/public/shared_components/single_metric_viewer/single_metric_viewer.tsx b/x-pack/plugins/ml/public/shared_components/single_metric_viewer/single_metric_viewer.tsx index a4c6681b45ef0..1b5f138dceacf 100644 --- a/x-pack/plugins/ml/public/shared_components/single_metric_viewer/single_metric_viewer.tsx +++ b/x-pack/plugins/ml/public/shared_components/single_metric_viewer/single_metric_viewer.tsx @@ -26,6 +26,7 @@ import { TimeSeriesExplorerEmbeddableChart } from '../../application/timeseriese import { APP_STATE_ACTION } from '../../application/timeseriesexplorer/timeseriesexplorer_constants'; import type { SingleMetricViewerServices, MlEntity } from '../../embeddables/types'; import './_index.scss'; +import { mlJobServiceFactory } from '../../application/services/job_service'; const containerPadding = 10; const minElemAndChartDiff = 20; @@ -92,8 +93,7 @@ const SingleMetricViewerWrapper: FC = ({ const isMounted = useMountedState(); - const { mlApiServices, mlJobService, mlTimeSeriesExplorerService, toastNotificationService } = - mlServices; + const { mlApiServices, mlTimeSeriesExplorerService, toastNotificationService } = mlServices; const startServices = pick(coreStart, 'analytics', 'i18n', 'theme'); const datePickerDeps: DatePickerDependencies = { ...pick(coreStart, ['http', 'notifications', 'theme', 'uiSettings', 'i18n']), @@ -102,6 +102,12 @@ const SingleMetricViewerWrapper: FC = ({ showFrozenDataTierChoice: false, }; + const mlJobService = useMemo( + () => mlJobServiceFactory(undefined, mlApiServices), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + const previousRefresh = usePrevious(lastRefresh ?? 0); useEffect( From 6cff8ba3b018e3690856789dad3b1a2e0aa80179 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 21 Jun 2024 18:24:32 +0200 Subject: [PATCH 09/16] fix types --- .../explorer/explorer_charts/explorer_anomalies_container.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_anomalies_container.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_anomalies_container.tsx index 6db1969736632..db530cb0d3f8d 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_anomalies_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_anomalies_container.tsx @@ -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; @@ -27,7 +28,7 @@ interface ExplorerAnomaliesContainerProps { severity: TableSeverity; setSeverity: (severity: TableSeverity) => void; mlLocator: MlLocator; - tableData: any; + tableData: AnomaliesTableData; timeBuckets: TimeBuckets; timefilter: TimefilterContract; onSelectEntity: ( From 78290120ff0cb48bda0527dcefe01d63b0504183 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 21 Jun 2024 18:32:28 +0200 Subject: [PATCH 10/16] fix click on multi bucket anomalies --- .../explorer/explorer_charts/explorer_chart_single_metric.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index e9f6a6b33943a..74579cf173e98 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -436,6 +436,11 @@ export class ExplorerChartSingleMetric extends React.Component { 'class', (d) => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore).id}` ) + .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); From 19ba3119d4c59349c3154baafd57b4c2a95dc7b7 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 2 Sep 2024 11:04:35 +0200 Subject: [PATCH 11/16] fix dependencies --- .../components/anomalies_table/links_menu.tsx | 8 -------- .../ml/public/application/explorer/explorer_utils.ts | 1 + .../anomaly_charts/anomaly_charts_react_container.tsx | 10 ++++++++-- .../get_anomaly_charts_services_dependencies.ts | 1 + .../embeddables/single_metric_viewer/get_services.ts | 3 +++ x-pack/plugins/ml/public/embeddables/types.ts | 2 +- 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx index de953c8e9a045..ed0b3640dec76 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx @@ -45,7 +45,6 @@ import { escapeQuotes } from '@kbn/es-query'; import { isQuery } from '@kbn/data-plugin/public'; import type { TimeRangeBounds } from '@kbn/ml-time-buckets'; -import { mlJobServiceFactory } from '../../services/job_service'; import { PLUGIN_ID } from '../../../../common/constants/app'; import { findMessageField } from '../../util/index_utils'; import { getInitialAnomaliesLayers, getInitialSourceIndexFieldLayers } from '../../../maps/util'; @@ -105,19 +104,12 @@ export const LinksMenuUI = (props: LinksMenuProps) => { data, share, application, - mlServices, uiActions, uiSettings, notifications: { toasts }, }, } = kibana; - const mlJobService = useMemo( - () => mlJobServiceFactory(undefined, mlServices.mlApiServices), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - const { getDataViewById, getDataViewIdFromName } = useMlIndexUtils(); const ml = useMlApiContext(); const mlJobService = useMlJobService(); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts index c209626fb0915..b546068a918c2 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts @@ -53,6 +53,7 @@ import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import type { MlResultsService } from '../services/results_service'; import type { Annotations, AnnotationsTable } from '../../../common/types/annotations'; import type { MlApiServices } from '../services/ml_api_service'; +import { useMlKibana } from '../contexts/kibana'; export interface ExplorerJob { id: string; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx index 810446e615716..caf67b4ee91d9 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx @@ -38,6 +38,7 @@ import type { MlLocatorParams } from '../../../common/types/locator'; import { useAnomalyChartsData } from './use_anomaly_charts_data'; import { useDateFormatTz, loadAnomaliesTableData } from '../../application/explorer/explorer_utils'; import { mlJobServiceFactory } from '../../application/services/job_service'; +import { toastNotificationServiceProvider } from '../../application/services/toast_notification_service'; const RESIZE_THROTTLE_TIME_MS = 500; @@ -81,13 +82,16 @@ const AnomalyChartsContainer: FC = ({ ); const [selectedEntities, setSelectedEntities] = useState(); const [ - { uiSettings }, + { + uiSettings, + notifications: { toasts }, + }, { data: dataServices, share, uiActions, charts: chartsService }, { mlApiServices }, ] = services; const mlJobService = useMemo( - () => mlJobServiceFactory(undefined, mlApiServices), + () => mlJobServiceFactory(toastNotificationServiceProvider(toasts), mlApiServices), // eslint-disable-next-line react-hooks/exhaustive-deps [] ); @@ -161,6 +165,8 @@ const AnomalyChartsContainer: FC = ({ }; const newTableData = await loadAnomaliesTableData( + mlApiServices, + mlJobService, undefined, explorerJobs, dateFormatTz, diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts b/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts index b2b9ffc707f90..735b6f2c1e88a 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts @@ -68,6 +68,7 @@ export const getAnomalyChartsServiceDependencies = async ( mlCapabilities, mlFieldFormatService, mlResultsService, + mlApiServices, }, ]; return anomalyChartsEmbeddableServices; diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_services.ts b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_services.ts index 2d7b616c52933..a5beee19f1b33 100644 --- a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_services.ts +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_services.ts @@ -30,6 +30,7 @@ export const getMlServices = async ( { MlCapabilitiesService }, { timeSeriesSearchServiceFactory }, { toastNotificationServiceProvider }, + { mlJobServiceFactory }, ] = await Promise.all([ await import('../../application/services/anomaly_detector_service'), await import('../../application/services/field_format_service_factory'), @@ -42,12 +43,14 @@ export const getMlServices = async ( '../../application/timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service' ), await import('../../application/services/toast_notification_service'), + await import('../../application/services/job_service'), ]); const httpService = new HttpService(coreStart.http); const anomalyDetectorService = new AnomalyDetectorService(httpService); const mlApiServices = mlApiServicesProvider(httpService); const toastNotificationService = toastNotificationServiceProvider(coreStart.notifications.toasts); + const mlJobService = mlJobServiceFactory(toastNotificationService, mlApiServices); const mlResultsService = mlResultsServiceProvider(mlApiServices); const mlTimeSeriesSearchService = timeSeriesSearchServiceFactory(mlResultsService, mlApiServices); const mlTimeSeriesExplorerService = timeSeriesExplorerServiceFactory( diff --git a/x-pack/plugins/ml/public/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts index 939db4ac0c81b..94bd8733bb7b4 100644 --- a/x-pack/plugins/ml/public/embeddables/types.ts +++ b/x-pack/plugins/ml/public/embeddables/types.ts @@ -235,7 +235,7 @@ export interface AnomalyChartsServices { mlCapabilities: MlCapabilitiesService; mlFieldFormatService: MlFieldFormatService; mlResultsService: MlResultsService; - mlApiServices?: MlApiServices; + mlApiServices: MlApiServices; } export interface SingleMetricViewerServices { From 49598648fd2176d8cf27bc427911c9a007e57a7b Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 4 Sep 2024 15:02:29 +0200 Subject: [PATCH 12/16] revert links_menu.tsx formatting --- .../application/components/anomalies_table/links_menu.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx index fd49bcd91a6b5..ccf9083c90791 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx @@ -43,8 +43,8 @@ import { CATEGORIZE_FIELD_TRIGGER } from '@kbn/ml-ui-actions'; import { isDefined } from '@kbn/ml-is-defined'; import { escapeQuotes } from '@kbn/es-query'; import { isQuery } from '@kbn/data-plugin/public'; -import type { TimeRangeBounds } from '@kbn/ml-time-buckets'; +import type { TimeRangeBounds } from '@kbn/ml-time-buckets'; import { PLUGIN_ID } from '../../../../common/constants/app'; import { findMessageField } from '../../util/index_utils'; import { getInitialAnomaliesLayers, getInitialSourceIndexFieldLayers } from '../../../maps/util'; @@ -109,7 +109,6 @@ export const LinksMenuUI = (props: LinksMenuProps) => { notifications: { toasts }, }, } = kibana; - const { getDataViewById, getDataViewIdFromName } = useMlIndexUtils(); const mlApi = useMlApi(); const mlJobService = useMlJobService(); From 5ac1cbbd8208f2ff7c8c519cca40c950083d0e4e Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 16 Sep 2024 15:22:20 +0200 Subject: [PATCH 13/16] getTableItemClosestToTimestamp --- x-pack/packages/ml/anomaly_utils/types.ts | 2 +- .../__mocks__/mock_anomalies_table_data.json | 39 +++++++++++----- .../common/util/anomalies_table_utils.test.ts | 46 +++++++++++++++++++ .../ml/common/util/anomalies_table_utils.ts | 23 ++++++++++ .../anomalies_table/anomalies_table.test.js | 2 +- .../explorer_chart_distribution.js | 10 ++-- .../explorer_chart_single_metric.js | 10 ++-- .../timeseries_chart/timeseries_chart.js | 10 ++-- 8 files changed, 107 insertions(+), 35 deletions(-) rename x-pack/plugins/ml/{public/application/explorer => common}/__mocks__/mock_anomalies_table_data.json (97%) create mode 100644 x-pack/plugins/ml/common/util/anomalies_table_utils.test.ts create mode 100644 x-pack/plugins/ml/common/util/anomalies_table_utils.ts diff --git a/x-pack/packages/ml/anomaly_utils/types.ts b/x-pack/packages/ml/anomaly_utils/types.ts index 2d7082848b48f..457330fc4d7e0 100644 --- a/x-pack/packages/ml/anomaly_utils/types.ts +++ b/x-pack/packages/ml/anomaly_utils/types.ts @@ -343,7 +343,7 @@ export interface MlAnomaliesTableRecord { /** * Returns true if the job has the model plot enabled */ - modelPlotEnabled: boolean; + modelPlotEnabled?: boolean; } /** diff --git a/x-pack/plugins/ml/public/application/explorer/__mocks__/mock_anomalies_table_data.json b/x-pack/plugins/ml/common/__mocks__/mock_anomalies_table_data.json similarity index 97% rename from x-pack/plugins/ml/public/application/explorer/__mocks__/mock_anomalies_table_data.json rename to x-pack/plugins/ml/common/__mocks__/mock_anomalies_table_data.json index 2827e87a05d1e..12cd80644efcf 100644 --- a/x-pack/plugins/ml/public/application/explorer/__mocks__/mock_anomalies_table_data.json +++ b/x-pack/plugins/ml/common/__mocks__/mock_anomalies_table_data.json @@ -1,4 +1,5 @@ -{ "default": { +{ + "default": { "anomalies": [ { "time": 1486018800000, @@ -44,9 +45,11 @@ "metricDescriptionSort": 82.83851409101328, "detector": "count by mlcategory", "isTimeSeriesViewDetector": false, - "influencers": [ - "mockInfluencer" - ] + "influencers": [ + { + "mockInfluencerField": "mockInfluencerValue" + } + ] }, { "time": 1486018800000, @@ -92,12 +95,16 @@ "metricDescriptionSort": 38.82201810127708, "detector": "count by mlcategory", "isTimeSeriesViewDetector": false, - "influencers": [ - "mockInfluencer" - ] + "influencers": [ + { + "mockInfluencerField": "mockInfluencerValue" + } + ] } ], - "jobIds": ["it-ops-count-by-mlcategory-one"], + "jobIds": [ + "it-ops-count-by-mlcategory-one" + ], "interval": "day", "examplesByJobId": { "it-ops-count-by-mlcategory-one": { @@ -161,7 +168,9 @@ "detector": "count by mlcategory", "isTimeSeriesViewDetector": false, "influencers": [ - "mockInfluencer" + { + "mockInfluencerField": "mockInfluencerValue" + } ] }, { @@ -208,7 +217,9 @@ "detector": "count by mlcategory", "isTimeSeriesViewDetector": false, "influencers": [ - "mockInfluencer" + { + "mockInfluencerField": "mockInfluencerValue" + } ] } ], @@ -496,7 +507,9 @@ "detector": "count by mlcategory", "isTimeSeriesViewDetector": false, "influencers": [ - "mockInfluencer" + { + "mockInfluencerField": "mockInfluencerValue" + } ] }, { @@ -541,7 +554,9 @@ "detector": "count by mlcategory", "isTimeSeriesViewDetector": false, "influencers": [ - "mockInfluencer" + { + "mockInfluencerField": "mockInfluencerValue" + } ] } ], diff --git a/x-pack/plugins/ml/common/util/anomalies_table_utils.test.ts b/x-pack/plugins/ml/common/util/anomalies_table_utils.test.ts new file mode 100644 index 0000000000000..fca9a4670a039 --- /dev/null +++ b/x-pack/plugins/ml/common/util/anomalies_table_utils.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { cloneDeep } from 'lodash'; + +import type { MlAnomaliesTableRecord } from '@kbn/ml-anomaly-utils'; + +import { getTableItemClosestToTimestamp } from './anomalies_table_utils'; + +import mockAnomaliesTableData from '../__mocks__/mock_anomalies_table_data.json'; + +describe('getTableItemClosestToTimestamp', () => { + const anomalies: MlAnomaliesTableRecord[] = mockAnomaliesTableData.default.anomalies; + anomalies.push(cloneDeep(anomalies[0])); + anomalies[0].source.timestamp = 1000; + anomalies[1].source.timestamp = 2000; + anomalies[2].source.timestamp = 3000; + + it('should return the first item if it is the closest', () => { + const anomalyTime = 1400; + const closestItem = getTableItemClosestToTimestamp(anomalies, anomalyTime); + expect(closestItem && closestItem.source.timestamp).toBe(1000); + }); + + it('should return the last item if it is the closest', () => { + const anomalyTime = 5000; + const closestItem = getTableItemClosestToTimestamp(anomalies, anomalyTime); + expect(closestItem && closestItem.source.timestamp).toBe(3000); + }); + + it('should return the second item if it is the closest', () => { + const anomalyTime = 2600; + const closestItem = getTableItemClosestToTimestamp(anomalies, anomalyTime); + expect(closestItem && closestItem.source.timestamp).toBe(3000); + }); + + it('should handle an empty anomalies array', () => { + const anomalyTime = 2000; + const closestItem = getTableItemClosestToTimestamp([], anomalyTime); + expect(closestItem).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/ml/common/util/anomalies_table_utils.ts b/x-pack/plugins/ml/common/util/anomalies_table_utils.ts new file mode 100644 index 0000000000000..23d88cbfd491a --- /dev/null +++ b/x-pack/plugins/ml/common/util/anomalies_table_utils.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MlAnomaliesTableRecord } from '@kbn/ml-anomaly-utils'; + +// 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. +export function getTableItemClosestToTimestamp( + anomalies: MlAnomaliesTableRecord[], + anomalyTime: number +) { + return anomalies.reduce((closestItem, currentItem) => { + if (!closestItem) return currentItem; + + const closestItemDelta = Math.abs(anomalyTime - closestItem.source.timestamp); + const currentItemDelta = Math.abs(anomalyTime - currentItem.source.timestamp); + return currentItemDelta < closestItemDelta ? currentItem : closestItem; + }, undefined); +} diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js index d8f00b92992c6..59b4fcb042186 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js @@ -6,7 +6,7 @@ */ import React from 'react'; -import mockAnomaliesTableData from '../../explorer/__mocks__/mock_anomalies_table_data.json'; +import mockAnomaliesTableData from '../../../../common/__mocks__/mock_anomalies_table_data.json'; import { getColumns } from './anomalies_table_columns'; jest.mock('../../capabilities/check_capabilities', () => ({ diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js index bfcb196ca1d16..2b3e8738bcf64 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js @@ -27,6 +27,8 @@ import { import { formatHumanReadableDateTime } from '@kbn/ml-date-utils'; import { context } from '@kbn/kibana-react-plugin/public'; +import { getTableItemClosestToTimestamp } from '../../../../common/util/anomalies_table_utils'; + import { LinksMenuUI } from '../../components/anomalies_table/links_menu'; import { RuleEditorFlyout } from '../../components/rule_editor'; import { formatValue } from '../../formatters/format_value'; @@ -529,13 +531,7 @@ 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]); + const tableItem = getTableItemClosestToTimestamp(that.props.tableData.anomalies, anomalyTime); if (tableItem) { // Overwrite the timestamp of the possibly aggregated table item with the diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index 74579cf173e98..e5ccf3430ca9f 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -27,6 +27,8 @@ import { import { formatHumanReadableDateTime } from '@kbn/ml-date-utils'; import { context } from '@kbn/kibana-react-plugin/public'; +import { getTableItemClosestToTimestamp } from '../../../../common/util/anomalies_table_utils'; + import { LinksMenuUI } from '../../components/anomalies_table/links_menu'; import { RuleEditorFlyout } from '../../components/rule_editor'; import { formatValue } from '../../formatters/format_value'; @@ -474,13 +476,7 @@ export class ExplorerChartSingleMetric 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]); + const tableItem = getTableItemClosestToTimestamp(that.props.tableData.anomalies, anomalyTime); if (tableItem) { // Overwrite the timestamp of the possibly aggregated table item with the diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 20d94aaabc3bb..51abbf77dbeba 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -24,6 +24,8 @@ import { getFormattedSeverityScore, getSeverityWithLow } from '@kbn/ml-anomaly-u import { formatHumanReadableDateTimeSeconds } from '@kbn/ml-date-utils'; import { context } from '@kbn/kibana-react-plugin/public'; +import { getTableItemClosestToTimestamp } from '../../../../../common/util/anomalies_table_utils'; + import { formatValue } from '../../../formatters/format_value'; import { LINE_CHART_ANOMALY_RADIUS, @@ -1582,13 +1584,7 @@ class TimeseriesChartIntl extends Component { showAnomalyPopover(marker, circle) { const anomalyTime = marker.date.getTime(); - // 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 = this.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; - }, this.props.tableData.anomalies[0]); + const tableItem = getTableItemClosestToTimestamp(this.props.tableData.anomalies, anomalyTime); if (tableItem) { // Overwrite the timestamp of the possibly aggregated table item with the From 511750730054fea38c1a63cd5050948100b95995 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 16 Sep 2024 15:26:53 +0200 Subject: [PATCH 14/16] useMlJobService --- .../anomaly_charts_react_container.tsx | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx index 1e5c77fbe5e2a..aefdac533f859 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx @@ -37,8 +37,7 @@ import { EXPLORER_ENTITY_FIELD_SELECTION_TRIGGER } from '../../ui_actions/trigge import type { MlLocatorParams } from '../../../common/types/locator'; import { useAnomalyChartsData } from './use_anomaly_charts_data'; import { useDateFormatTz, loadAnomaliesTableData } from '../../application/explorer/explorer_utils'; -import { mlJobServiceFactory } from '../../application/services/job_service'; -import { toastNotificationServiceProvider } from '../../application/services/toast_notification_service'; +import { useMlJobService } from '../../application/services/job_service'; const RESIZE_THROTTLE_TIME_MS = 500; @@ -82,19 +81,12 @@ const AnomalyChartsContainer: FC = ({ ); const [selectedEntities, setSelectedEntities] = useState(); const [ - { - uiSettings, - notifications: { toasts }, - }, + { uiSettings }, { data: dataServices, share, uiActions, charts: chartsService }, { mlApi }, ] = services; - const mlJobService = useMemo( - () => mlJobServiceFactory(toastNotificationServiceProvider(toasts), mlApi), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); + const mlJobService = useMlJobService(); const { timefilter } = dataServices.query.timefilter; const timeRange = useObservable(timeRange$); From 8e8185b8f27c811688ebfa23ed17265a1f751660 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 16 Sep 2024 18:39:47 +0200 Subject: [PATCH 15/16] fix multiple detectors --- ...omalies_table_data_multiple_detectors.json | 975 ++++++++++++++++++ .../common/util/anomalies_table_utils.test.ts | 42 +- .../ml/common/util/anomalies_table_utils.ts | 38 +- .../explorer_chart_distribution.js | 6 +- .../explorer_chart_single_metric.js | 6 +- 5 files changed, 1056 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/ml/common/__mocks__/mock_anomalies_table_data_multiple_detectors.json diff --git a/x-pack/plugins/ml/common/__mocks__/mock_anomalies_table_data_multiple_detectors.json b/x-pack/plugins/ml/common/__mocks__/mock_anomalies_table_data_multiple_detectors.json new file mode 100644 index 0000000000000..bd886f4745af3 --- /dev/null +++ b/x-pack/plugins/ml/common/__mocks__/mock_anomalies_table_data_multiple_detectors.json @@ -0,0 +1,975 @@ +{ + "default": { + "anomalies": [ + { + "time": 1725840000000, + "source": { + "job_id": "ecom_dect_01", + "result_type": "record", + "probability": 0.00011921600143273021, + "multi_bucket_impact": -5, + "record_score": 94.31236, + "initial_record_score": 91.59429607036628, + "bucket_span": 900, + "detector_index": 0, + "is_interim": false, + "timestamp": 1725862500000, + "partition_field_name": "category.keyword", + "partition_field_value": "Men's Clothing", + "function": "mean", + "function_description": "mean", + "typical": [ + 15.851794819088305 + ], + "actual": [ + 350 + ], + "field_name": "products.price", + "influencers": [ + { + "influencer_field_name": "category.keyword", + "influencer_field_values": [ + "Men's Clothing" + ] + }, + { + "influencer_field_name": "geoip.country_iso_code", + "influencer_field_values": [ + "SA" + ] + } + ], + "anomaly_score_explanation": { + "single_bucket_impact": 8, + "lower_confidence_bound": 5.733288153284621, + "typical_value": 15.851794819088305, + "upper_confidence_bound": 41.175974376816086 + }, + "category.keyword": [ + "Men's Clothing" + ], + "geoip.country_iso_code": [ + "SA" + ] + }, + "rowId": "1726503845974_0", + "jobId": "ecom_dect_01", + "detectorIndex": 0, + "severity": 94.31236, + "entityName": "category.keyword", + "entityValue": "Men's Clothing", + "influencers": [ + { + "category.keyword": "Men's Clothing" + }, + { + "geoip.country_iso_code": "SA" + } + ], + "actual": [ + 350 + ], + "actualSort": 350, + "typical": [ + 15.851794819088305 + ], + "typicalSort": 15.851794819088305, + "metricDescriptionSort": 22.079518691381207, + "detector": "mean(\"products.price\") partitionfield=\"category.keyword\"", + "isTimeSeriesViewRecord": true, + "isGeoRecord": false + }, + { + "time": 1725840000000, + "source": { + "job_id": "ecom_dect_01", + "result_type": "record", + "probability": 0.03461785745665291, + "multi_bucket_impact": -5, + "record_score": 10.620156126608986, + "initial_record_score": 10.620156126608986, + "bucket_span": 900, + "detector_index": 0, + "is_interim": false, + "timestamp": 1725862500000, + "partition_field_name": "category.keyword", + "partition_field_value": "Women's Clothing", + "function": "mean", + "function_description": "mean", + "typical": [ + 17.553578210957593 + ], + "actual": [ + 45 + ], + "field_name": "products.price", + "influencers": [ + { + "influencer_field_name": "category.keyword", + "influencer_field_values": [ + "Women's Clothing" + ] + }, + { + "influencer_field_name": "geoip.country_iso_code", + "influencer_field_values": [ + "SA" + ] + } + ], + "anomaly_score_explanation": { + "single_bucket_impact": 2, + "lower_confidence_bound": 7.606026510878394, + "typical_value": 17.553578210957593, + "upper_confidence_bound": 37.852824407923066 + }, + "category.keyword": [ + "Women's Clothing" + ], + "geoip.country_iso_code": [ + "SA" + ] + }, + "rowId": "1726503845974_1", + "jobId": "ecom_dect_01", + "detectorIndex": 0, + "severity": 10.620156126608986, + "entityName": "category.keyword", + "entityValue": "Women's Clothing", + "influencers": [ + { + "category.keyword": "Women's Clothing" + }, + { + "geoip.country_iso_code": "SA" + } + ], + "actual": [ + 45 + ], + "actualSort": 45, + "typical": [ + 17.553578210957593 + ], + "typicalSort": 17.553578210957593, + "metricDescriptionSort": 2.5635798843514044, + "detector": "mean(\"products.price\") partitionfield=\"category.keyword\"", + "isTimeSeriesViewRecord": true, + "isGeoRecord": false + }, + { + "time": 1725926400000, + "source": { + "job_id": "ecom_dect_01", + "result_type": "record", + "probability": 0.01787543314280476, + "multi_bucket_impact": -5, + "record_score": 0.6916846762237938, + "initial_record_score": 0.6916846762237938, + "bucket_span": 900, + "detector_index": 0, + "is_interim": false, + "timestamp": 1725990300000, + "partition_field_name": "category.keyword", + "partition_field_value": "Men's Clothing", + "function": "mean", + "function_description": "mean", + "typical": [ + 15.779865757388727 + ], + "actual": [ + 65 + ], + "field_name": "products.price", + "influencers": [ + { + "influencer_field_name": "category.keyword", + "influencer_field_values": [ + "Men's Clothing" + ] + }, + { + "influencer_field_name": "geoip.country_iso_code", + "influencer_field_values": [ + "US" + ] + } + ], + "anomaly_score_explanation": { + "single_bucket_impact": 3, + "lower_confidence_bound": 5.889827882876367, + "typical_value": 15.779865757388727, + "upper_confidence_bound": 39.8666079359938 + }, + "category.keyword": [ + "Men's Clothing" + ], + "geoip.country_iso_code": [ + "US" + ] + }, + "rowId": "1726503845974_2", + "jobId": "ecom_dect_01", + "detectorIndex": 0, + "severity": 0.6916846762237938, + "entityName": "category.keyword", + "entityValue": "Men's Clothing", + "influencers": [ + { + "category.keyword": "Men's Clothing" + }, + { + "geoip.country_iso_code": "US" + } + ], + "actual": [ + 65 + ], + "actualSort": 65, + "typical": [ + 15.779865757388727 + ], + "typicalSort": 15.779865757388727, + "metricDescriptionSort": 4.1191731919243075, + "detector": "mean(\"products.price\") partitionfield=\"category.keyword\"", + "isTimeSeriesViewRecord": true, + "isGeoRecord": false + }, + { + "time": 1726012800000, + "source": { + "job_id": "ecom_dect_01", + "result_type": "record", + "probability": 0.013914839080385263, + "multi_bucket_impact": -5, + "record_score": 24.552541318692445, + "initial_record_score": 24.552541318692445, + "bucket_span": 900, + "detector_index": 0, + "is_interim": false, + "timestamp": 1726056000000, + "partition_field_name": "category.keyword", + "partition_field_value": "Women's Accessories,Women's Clothing", + "function": "mean", + "function_description": "mean", + "typical": [ + 16.072466356993818 + ], + "actual": [ + 42 + ], + "field_name": "products.price", + "influencers": [ + { + "influencer_field_name": "category.keyword", + "influencer_field_values": [ + "Women's Accessories,Women's Clothing" + ] + }, + { + "influencer_field_name": "geoip.country_iso_code", + "influencer_field_values": [ + "AE" + ] + } + ], + "anomaly_score_explanation": { + "single_bucket_impact": 2, + "lower_confidence_bound": 7.901062666669044, + "typical_value": 16.072466356993818, + "upper_confidence_bound": 31.621998523165693 + }, + "category.keyword": [ + "Women's Accessories,Women's Clothing" + ], + "geoip.country_iso_code": [ + "AE" + ] + }, + "rowId": "1726503845974_3", + "jobId": "ecom_dect_01", + "detectorIndex": 0, + "severity": 24.552541318692445, + "entityName": "category.keyword", + "entityValue": "Women's Accessories,Women's Clothing", + "influencers": [ + { + "category.keyword": "Women's Accessories,Women's Clothing" + }, + { + "geoip.country_iso_code": "AE" + } + ], + "actual": [ + 42 + ], + "actualSort": 42, + "typical": [ + 16.072466356993818 + ], + "typicalSort": 16.072466356993818, + "metricDescriptionSort": 2.613164592609273, + "detector": "mean(\"products.price\") partitionfield=\"category.keyword\"", + "isTimeSeriesViewRecord": true, + "isGeoRecord": false + }, + { + "time": 1726012800000, + "source": { + "job_id": "ecom_dect_01", + "result_type": "record", + "probability": 0.013569993829022374, + "multi_bucket_impact": -5, + "record_score": 1.08840456688412, + "initial_record_score": 1.08840456688412, + "bucket_span": 900, + "detector_index": 0, + "is_interim": false, + "timestamp": 1726083900000, + "partition_field_name": "category.keyword", + "partition_field_value": "Men's Clothing", + "function": "mean", + "function_description": "mean", + "typical": [ + 15.91745643577788 + ], + "actual": [ + 75 + ], + "field_name": "products.price", + "influencers": [ + { + "influencer_field_name": "category.keyword", + "influencer_field_values": [ + "Men's Clothing" + ] + }, + { + "influencer_field_name": "geoip.country_iso_code", + "influencer_field_values": [ + "AE" + ] + } + ], + "anomaly_score_explanation": { + "single_bucket_impact": 3, + "lower_confidence_bound": 5.831067931970601, + "typical_value": 15.91745643577788, + "upper_confidence_bound": 40.89686519101566 + }, + "category.keyword": [ + "Men's Clothing" + ], + "geoip.country_iso_code": [ + "AE" + ] + }, + "rowId": "1726503845974_4", + "jobId": "ecom_dect_01", + "detectorIndex": 0, + "severity": 1.08840456688412, + "entityName": "category.keyword", + "entityValue": "Men's Clothing", + "influencers": [ + { + "category.keyword": "Men's Clothing" + }, + { + "geoip.country_iso_code": "AE" + } + ], + "actual": [ + 75 + ], + "actualSort": 75, + "typical": [ + 15.91745643577788 + ], + "typicalSort": 15.91745643577788, + "metricDescriptionSort": 4.711808089602902, + "detector": "mean(\"products.price\") partitionfield=\"category.keyword\"", + "isTimeSeriesViewRecord": true, + "isGeoRecord": false + }, + { + "time": 1726012800000, + "source": { + "job_id": "ecom_dect_01", + "result_type": "record", + "probability": 0.012483410285350998, + "multi_bucket_impact": -5, + "record_score": 26.21197793819939, + "initial_record_score": 26.21197793819939, + "bucket_span": 900, + "detector_index": 1, + "is_interim": false, + "timestamp": 1726065000000, + "partition_field_name": "category.keyword", + "partition_field_value": "Men's Clothing", + "function": "distinct_count", + "function_description": "distinct_count", + "typical": [ + 1 + ], + "actual": [ + 2 + ], + "field_name": "total_unique_products", + "influencers": [ + { + "influencer_field_name": "category.keyword", + "influencer_field_values": [ + "Men's Clothing" + ] + }, + { + "influencer_field_name": "geoip.country_iso_code", + "influencer_field_values": [ + "FR", + "MA", + "TR" + ] + } + ], + "anomaly_score_explanation": { + "single_bucket_impact": 3, + "lower_confidence_bound": 0, + "typical_value": 0, + "upper_confidence_bound": 0 + }, + "category.keyword": [ + "Men's Clothing" + ], + "geoip.country_iso_code": [ + "FR", + "MA", + "TR" + ] + }, + "rowId": "1726503845974_5", + "jobId": "ecom_dect_01", + "detectorIndex": 1, + "severity": 26.21197793819939, + "entityName": "category.keyword", + "entityValue": "Men's Clothing", + "influencers": [ + { + "category.keyword": "Men's Clothing" + }, + { + "geoip.country_iso_code": "FR" + }, + { + "geoip.country_iso_code": "MA" + }, + { + "geoip.country_iso_code": "TR" + } + ], + "actual": [ + 2 + ], + "actualSort": 2, + "typical": [ + 1 + ], + "typicalSort": 1, + "metricDescriptionSort": 2, + "detector": "distinct_count(total_unique_products) partitionfield=\"category.keyword\"", + "isTimeSeriesViewRecord": true, + "isGeoRecord": false + }, + { + "time": 1726012800000, + "source": { + "job_id": "ecom_dect_01", + "result_type": "record", + "probability": 0.029221215176833293, + "multi_bucket_impact": -5, + "record_score": 13.210841360998488, + "initial_record_score": 13.210841360998488, + "bucket_span": 900, + "detector_index": 1, + "is_interim": false, + "timestamp": 1726065000000, + "partition_field_name": "category.keyword", + "partition_field_value": "Men's Clothing,Men's Shoes", + "function": "distinct_count", + "function_description": "distinct_count", + "typical": [ + 1 + ], + "actual": [ + 2 + ], + "field_name": "total_unique_products", + "influencers": [ + { + "influencer_field_name": "category.keyword", + "influencer_field_values": [ + "Men's Clothing,Men's Shoes" + ] + }, + { + "influencer_field_name": "geoip.country_iso_code", + "influencer_field_values": [ + "EG", + "US" + ] + } + ], + "anomaly_score_explanation": { + "single_bucket_impact": 2, + "lower_confidence_bound": 0, + "typical_value": 0, + "upper_confidence_bound": 0 + }, + "category.keyword": [ + "Men's Clothing,Men's Shoes" + ], + "geoip.country_iso_code": [ + "EG", + "US" + ] + }, + "rowId": "1726503845974_6", + "jobId": "ecom_dect_01", + "detectorIndex": 1, + "severity": 13.210841360998488, + "entityName": "category.keyword", + "entityValue": "Men's Clothing,Men's Shoes", + "influencers": [ + { + "category.keyword": "Men's Clothing,Men's Shoes" + }, + { + "geoip.country_iso_code": "EG" + }, + { + "geoip.country_iso_code": "US" + } + ], + "actual": [ + 2 + ], + "actualSort": 2, + "typical": [ + 1 + ], + "typicalSort": 1, + "metricDescriptionSort": 2, + "detector": "distinct_count(total_unique_products) partitionfield=\"category.keyword\"", + "isTimeSeriesViewRecord": true, + "isGeoRecord": false + }, + { + "time": 1726099200000, + "source": { + "job_id": "ecom_dect_01", + "result_type": "record", + "probability": 0.01614665640555698, + "multi_bucket_impact": -5, + "record_score": 22.27855551555865, + "initial_record_score": 22.27855551555865, + "bucket_span": 900, + "detector_index": 0, + "is_interim": false, + "timestamp": 1726133400000, + "partition_field_name": "category.keyword", + "partition_field_value": "Women's Accessories,Women's Shoes", + "function": "mean", + "function_description": "mean", + "typical": [ + 17.70351589303024 + ], + "actual": [ + 65 + ], + "field_name": "products.price", + "influencers": [ + { + "influencer_field_name": "category.keyword", + "influencer_field_values": [ + "Women's Accessories,Women's Shoes" + ] + }, + { + "influencer_field_name": "geoip.country_iso_code", + "influencer_field_values": [ + "US" + ] + } + ], + "anomaly_score_explanation": { + "single_bucket_impact": 2, + "lower_confidence_bound": 6.522434202795708, + "typical_value": 17.70351589303024, + "upper_confidence_bound": 44.79010478599095 + }, + "category.keyword": [ + "Women's Accessories,Women's Shoes" + ], + "geoip.country_iso_code": [ + "US" + ] + }, + "rowId": "1726503845974_7", + "jobId": "ecom_dect_01", + "detectorIndex": 0, + "severity": 22.27855551555865, + "entityName": "category.keyword", + "entityValue": "Women's Accessories,Women's Shoes", + "influencers": [ + { + "category.keyword": "Women's Accessories,Women's Shoes" + }, + { + "geoip.country_iso_code": "US" + } + ], + "actual": [ + 65 + ], + "actualSort": 65, + "typical": [ + 17.70351589303024 + ], + "typicalSort": 17.70351589303024, + "metricDescriptionSort": 3.6715870673796545, + "detector": "mean(\"products.price\") partitionfield=\"category.keyword\"", + "isTimeSeriesViewRecord": true, + "isGeoRecord": false + }, + { + "time": 1726099200000, + "source": { + "job_id": "ecom_dect_01", + "result_type": "record", + "probability": 0.01894227723384407, + "multi_bucket_impact": -5, + "record_score": 19.837546336871547, + "initial_record_score": 19.837546336871547, + "bucket_span": 900, + "detector_index": 1, + "is_interim": false, + "timestamp": 1726121700000, + "partition_field_name": "category.keyword", + "partition_field_value": "Men's Clothing", + "function": "distinct_count", + "function_description": "distinct_count", + "typical": [ + 1 + ], + "actual": [ + 2 + ], + "field_name": "total_unique_products", + "influencers": [ + { + "influencer_field_name": "category.keyword", + "influencer_field_values": [ + "Men's Clothing" + ] + }, + { + "influencer_field_name": "geoip.country_iso_code", + "influencer_field_values": [ + "AE", + "US" + ] + } + ], + "anomaly_score_explanation": { + "single_bucket_impact": 2, + "lower_confidence_bound": 0, + "typical_value": 0, + "upper_confidence_bound": 0 + }, + "category.keyword": [ + "Men's Clothing" + ], + "geoip.country_iso_code": [ + "AE", + "US" + ] + }, + "rowId": "1726503845974_8", + "jobId": "ecom_dect_01", + "detectorIndex": 1, + "severity": 19.837546336871547, + "entityName": "category.keyword", + "entityValue": "Men's Clothing", + "influencers": [ + { + "category.keyword": "Men's Clothing" + }, + { + "geoip.country_iso_code": "AE" + }, + { + "geoip.country_iso_code": "US" + } + ], + "actual": [ + 2 + ], + "actualSort": 2, + "typical": [ + 1 + ], + "typicalSort": 1, + "metricDescriptionSort": 2, + "detector": "distinct_count(total_unique_products) partitionfield=\"category.keyword\"", + "isTimeSeriesViewRecord": true, + "isGeoRecord": false + }, + { + "time": 1726185600000, + "source": { + "job_id": "ecom_dect_01", + "result_type": "record", + "probability": 0.005452151333957955, + "multi_bucket_impact": -5, + "record_score": 38.875218910067446, + "initial_record_score": 38.875218910067446, + "bucket_span": 900, + "detector_index": 0, + "is_interim": false, + "timestamp": 1726186500000, + "partition_field_name": "category.keyword", + "partition_field_value": "Men's Accessories,Men's Clothing", + "function": "mean", + "function_description": "mean", + "typical": [ + 15.943152908258694 + ], + "actual": [ + 60 + ], + "field_name": "products.price", + "influencers": [ + { + "influencer_field_name": "category.keyword", + "influencer_field_values": [ + "Men's Accessories,Men's Clothing" + ] + }, + { + "influencer_field_name": "geoip.country_iso_code", + "influencer_field_values": [ + "AE" + ] + } + ], + "anomaly_score_explanation": { + "single_bucket_impact": 3, + "lower_confidence_bound": 6.39588429144841, + "typical_value": 15.943152908258694, + "upper_confidence_bound": 37.04143942410928 + }, + "category.keyword": [ + "Men's Accessories,Men's Clothing" + ], + "geoip.country_iso_code": [ + "AE" + ] + }, + "rowId": "1726503845974_9", + "jobId": "ecom_dect_01", + "detectorIndex": 0, + "severity": 38.875218910067446, + "entityName": "category.keyword", + "entityValue": "Men's Accessories,Men's Clothing", + "influencers": [ + { + "category.keyword": "Men's Accessories,Men's Clothing" + }, + { + "geoip.country_iso_code": "AE" + } + ], + "actual": [ + 60 + ], + "actualSort": 60, + "typical": [ + 15.943152908258694 + ], + "typicalSort": 15.943152908258694, + "metricDescriptionSort": 3.763371043686062, + "detector": "mean(\"products.price\") partitionfield=\"category.keyword\"", + "isTimeSeriesViewRecord": true, + "isGeoRecord": false + }, + { + "time": 1726185600000, + "source": { + "job_id": "ecom_dect_01", + "result_type": "record", + "probability": 0.030245620963532376, + "multi_bucket_impact": -5, + "record_score": 12.684121118422576, + "initial_record_score": 12.684121118422576, + "bucket_span": 900, + "detector_index": 0, + "is_interim": false, + "timestamp": 1726225200000, + "partition_field_name": "category.keyword", + "partition_field_value": "Men's Clothing,Men's Shoes", + "function": "mean", + "function_description": "mean", + "typical": [ + 19.693271090135916 + ], + "actual": [ + 75 + ], + "field_name": "products.price", + "influencers": [ + { + "influencer_field_name": "category.keyword", + "influencer_field_values": [ + "Men's Clothing,Men's Shoes" + ] + }, + { + "influencer_field_name": "geoip.country_iso_code", + "influencer_field_values": [ + "AE" + ] + } + ], + "anomaly_score_explanation": { + "single_bucket_impact": 2, + "lower_confidence_bound": 6.850458655008051, + "typical_value": 19.693271090135916, + "upper_confidence_bound": 53.49007700314821 + }, + "category.keyword": [ + "Men's Clothing,Men's Shoes" + ], + "geoip.country_iso_code": [ + "AE" + ] + }, + "rowId": "1726503845974_10", + "jobId": "ecom_dect_01", + "detectorIndex": 0, + "severity": 12.684121118422576, + "entityName": "category.keyword", + "entityValue": "Men's Clothing,Men's Shoes", + "influencers": [ + { + "category.keyword": "Men's Clothing,Men's Shoes" + }, + { + "geoip.country_iso_code": "AE" + } + ], + "actual": [ + 75 + ], + "actualSort": 75, + "typical": [ + 19.693271090135916 + ], + "typicalSort": 19.693271090135916, + "metricDescriptionSort": 3.808407433012307, + "detector": "mean(\"products.price\") partitionfield=\"category.keyword\"", + "isTimeSeriesViewRecord": true, + "isGeoRecord": false + }, + { + "time": 1726185600000, + "source": { + "job_id": "ecom_dect_01", + "result_type": "record", + "probability": 0.024450443154858347, + "multi_bucket_impact": -5, + "record_score": 15.93562008623777, + "initial_record_score": 15.93562008623777, + "bucket_span": 900, + "detector_index": 1, + "is_interim": false, + "timestamp": 1726194600000, + "partition_field_name": "category.keyword", + "partition_field_value": "Men's Clothing", + "function": "distinct_count", + "function_description": "distinct_count", + "typical": [ + 1 + ], + "actual": [ + 2 + ], + "field_name": "total_unique_products", + "influencers": [ + { + "influencer_field_name": "category.keyword", + "influencer_field_values": [ + "Men's Clothing" + ] + }, + { + "influencer_field_name": "geoip.country_iso_code", + "influencer_field_values": [ + "TR", + "US" + ] + } + ], + "anomaly_score_explanation": { + "single_bucket_impact": 2, + "lower_confidence_bound": 0, + "typical_value": 0, + "upper_confidence_bound": 0 + }, + "category.keyword": [ + "Men's Clothing" + ], + "geoip.country_iso_code": [ + "TR", + "US" + ] + }, + "rowId": "1726503845974_11", + "jobId": "ecom_dect_01", + "detectorIndex": 1, + "severity": 15.93562008623777, + "entityName": "category.keyword", + "entityValue": "Men's Clothing", + "influencers": [ + { + "category.keyword": "Men's Clothing" + }, + { + "geoip.country_iso_code": "TR" + }, + { + "geoip.country_iso_code": "US" + } + ], + "actual": [ + 2 + ], + "actualSort": 2, + "typical": [ + 1 + ], + "typicalSort": 1, + "metricDescriptionSort": 2, + "detector": "distinct_count(total_unique_products) partitionfield=\"category.keyword\"", + "isTimeSeriesViewRecord": true, + "isGeoRecord": false + } + ], + "jobIds": [ + "ecommerce-multiple-detectors" + ], + "interval": "day", + "examplesByJobId": { + "ecommerce-multiple-detectors": {} + }, + "showViewSeriesLink": true + } +} diff --git a/x-pack/plugins/ml/common/util/anomalies_table_utils.test.ts b/x-pack/plugins/ml/common/util/anomalies_table_utils.test.ts index fca9a4670a039..e64c0daf983eb 100644 --- a/x-pack/plugins/ml/common/util/anomalies_table_utils.test.ts +++ b/x-pack/plugins/ml/common/util/anomalies_table_utils.test.ts @@ -12,8 +12,9 @@ import type { MlAnomaliesTableRecord } from '@kbn/ml-anomaly-utils'; import { getTableItemClosestToTimestamp } from './anomalies_table_utils'; import mockAnomaliesTableData from '../__mocks__/mock_anomalies_table_data.json'; +import mockAnomaliesTableDataMultipleDetectors from '../__mocks__/mock_anomalies_table_data_multiple_detectors.json'; -describe('getTableItemClosestToTimestamp', () => { +describe('getTableItemClosestToTimestamp without entities filter', () => { const anomalies: MlAnomaliesTableRecord[] = mockAnomaliesTableData.default.anomalies; anomalies.push(cloneDeep(anomalies[0])); anomalies[0].source.timestamp = 1000; @@ -44,3 +45,42 @@ describe('getTableItemClosestToTimestamp', () => { expect(closestItem).toBeUndefined(); }); }); + +// These tests test for the case when there's multiple anomalies with the same +// timestamp but different entity values. +describe('getTableItemClosestToTimestamp with entities filter', () => { + const anomalies: MlAnomaliesTableRecord[] = + mockAnomaliesTableDataMultipleDetectors.default.anomalies; + + it("should return the closest item matching the filter for Men's Clothing", () => { + const anomalyTime = 1725862500000; + const entityFields = [{ fieldName: 'category.keyword', fieldValue: "Men's Clothing" }]; + + const closestItem = getTableItemClosestToTimestamp(anomalies, anomalyTime, entityFields); + + expect(closestItem).toBeDefined(); + + // This is just to satisfy TypeScript. + if (!closestItem) throw new Error('closestItem is undefined'); + + expect(closestItem.source.timestamp).toBe(1725862500000); + expect(closestItem.entityName).toBe('category.keyword'); + expect(closestItem.entityValue).toBe("Men's Clothing"); + }); + + it("should return the closest item matching the filter for Women's Clothing", () => { + const anomalyTime = 1725862500000; + const entityFields = [{ fieldName: 'category.keyword', fieldValue: "Women's Clothing" }]; + + const closestItem = getTableItemClosestToTimestamp(anomalies, anomalyTime, entityFields); + + expect(closestItem).toBeDefined(); + + // This is just to satisfy TypeScript. + if (!closestItem) throw new Error('closestItem is undefined'); + + expect(closestItem.source.timestamp).toBe(1725862500000); + expect(closestItem.entityName).toBe('category.keyword'); + expect(closestItem.entityValue).toBe("Women's Clothing"); + }); +}); diff --git a/x-pack/plugins/ml/common/util/anomalies_table_utils.ts b/x-pack/plugins/ml/common/util/anomalies_table_utils.ts index 23d88cbfd491a..1964ccfd85ca6 100644 --- a/x-pack/plugins/ml/common/util/anomalies_table_utils.ts +++ b/x-pack/plugins/ml/common/util/anomalies_table_utils.ts @@ -5,19 +5,41 @@ * 2.0. */ -import type { MlAnomaliesTableRecord } from '@kbn/ml-anomaly-utils'; +import type { MlAnomaliesTableRecord, MlEntityField } from '@kbn/ml-anomaly-utils'; // 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. export function getTableItemClosestToTimestamp( anomalies: MlAnomaliesTableRecord[], - anomalyTime: number + anomalyTime: number, + entityFields?: MlEntityField[] ) { - return anomalies.reduce((closestItem, currentItem) => { - if (!closestItem) return currentItem; + const filteredAnomalies = entityFields + ? anomalies.filter((anomaly) => { + const currentEntity = { + entityName: anomaly.entityName, + entityValue: anomaly.entityValue, + }; - const closestItemDelta = Math.abs(anomalyTime - closestItem.source.timestamp); - const currentItemDelta = Math.abs(anomalyTime - currentItem.source.timestamp); - return currentItemDelta < closestItemDelta ? currentItem : closestItem; - }, undefined); + return entityFields.some( + (field) => + field.fieldName === currentEntity.entityName && + field.fieldValue === currentEntity.entityValue + ); + }) + : anomalies; + + return filteredAnomalies.reduce( + (closestItem, currentItem) => { + // If the closest item is not defined, return the current item. + // This is the case when we start the reducer. For the case of an empty + // array the reducer will not be called and the value will stay undefined. + if (!closestItem) return currentItem; + + const closestItemDelta = Math.abs(anomalyTime - closestItem.source.timestamp); + const currentItemDelta = Math.abs(anomalyTime - currentItem.source.timestamp); + return currentItemDelta < closestItemDelta ? currentItem : closestItem; + }, + undefined + ); } diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js index 2b3e8738bcf64..df1faf0218228 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js @@ -531,7 +531,11 @@ export class ExplorerChartDistribution extends React.Component { function showAnomalyPopover(marker, circle) { const anomalyTime = marker.date; - const tableItem = getTableItemClosestToTimestamp(that.props.tableData.anomalies, anomalyTime); + const tableItem = getTableItemClosestToTimestamp( + that.props.tableData.anomalies, + anomalyTime, + that.props.seriesConfig.entityFields + ); if (tableItem) { // Overwrite the timestamp of the possibly aggregated table item with the diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index e5ccf3430ca9f..e358c381288d3 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -476,7 +476,11 @@ export class ExplorerChartSingleMetric extends React.Component { function showAnomalyPopover(marker, circle) { const anomalyTime = marker.date; - const tableItem = getTableItemClosestToTimestamp(that.props.tableData.anomalies, anomalyTime); + const tableItem = getTableItemClosestToTimestamp( + that.props.tableData.anomalies, + anomalyTime, + that.props.seriesConfig.entityFields + ); if (tableItem) { // Overwrite the timestamp of the possibly aggregated table item with the From d7488d2368da5f99615b2e5a3b6e35ee2e4beb71 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 17 Sep 2024 10:49:56 +0200 Subject: [PATCH 16/16] update x-pack/plugins/ml/tsconfig.json --- x-pack/plugins/ml/tsconfig.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index 8353c023f1955..2f980378de923 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -11,6 +11,7 @@ "__mocks__/**/*", "../../../typings/**/*", // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 + "common/**/*.json", "public/**/*.json", "server/**/*.json" ], @@ -19,7 +20,9 @@ ], "kbn_references": [ "@kbn/core", - { "path": "../../../src/setup_node_env/tsconfig.json" }, + { + "path": "../../../src/setup_node_env/tsconfig.json" + }, // add references to other TypeScript projects the plugin depends on "@kbn/ace", "@kbn/actions-plugin",