From 1a268b85640e54f4d4079463ca5abaf917f404e9 Mon Sep 17 00:00:00 2001 From: Bena Kansara <69037875+benakansara@users.noreply.github.com> Date: Sat, 3 Feb 2024 00:02:27 +0100 Subject: [PATCH] [Custom threshold rule] Use lens chart with annotations in alert details page (#175513) Resolves https://github.com/elastic/kibana/issues/174075, https://github.com/elastic/kibana/issues/155691 - Using same chart in alert details page as in rule flyout for preview - Group by info is applied as filter on chart data - Added annotations - For active alerts, added point annotation with alert start time and range annotation with range from alert start time till current time - For recovered alerts, added range annotation for alert duration ### Active alert Screenshot 2024-01-26 at 15 05 26 Screenshot 2024-01-26 at 15 09 44 ### Recovered alert Screenshot 2024-01-26 at 14 53 08 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../visualization_types/constants.ts | 1 + .../visualization_types/layers/index.ts | 5 +- .../layers/xy_by_value_annotation_layer.ts | 66 ++++++++++++ packages/kbn-lens-embeddable-utils/index.ts | 1 + .../alert_details_app_section.test.tsx.snap | 51 +++++---- .../alert_details_app_section.test.tsx | 19 ++-- .../alert_details_app_section.tsx | 100 ++++++++++-------- .../helpers/get_filter_query.test.ts | 39 +++++++ .../helpers/get_filter_query.ts | 21 ++++ .../painless_tinymath_parser.test.ts | 0 .../painless_tinymath_parser.ts | 0 .../rule_condition_chart.test.tsx} | 7 +- .../rule_condition_chart.tsx} | 47 ++++++-- .../custom_threshold_rule_expression.test.tsx | 4 +- .../custom_threshold_rule_expression.tsx | 3 +- .../components/header_actions.tsx | 2 +- x-pack/plugins/observability/tsconfig.json | 1 + 17 files changed, 276 insertions(+), 91 deletions(-) create mode 100644 packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/xy_by_value_annotation_layer.ts create mode 100644 x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/helpers/get_filter_query.test.ts create mode 100644 x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/helpers/get_filter_query.ts rename x-pack/plugins/observability/public/components/custom_threshold/components/{preview_chart => rule_condition_chart}/painless_tinymath_parser.test.ts (100%) rename x-pack/plugins/observability/public/components/custom_threshold/components/{preview_chart => rule_condition_chart}/painless_tinymath_parser.ts (100%) rename x-pack/plugins/observability/public/components/custom_threshold/components/{preview_chart/preview_chart.test.tsx => rule_condition_chart/rule_condition_chart.test.tsx} (92%) rename x-pack/plugins/observability/public/components/custom_threshold/components/{preview_chart/preview_chart.tsx => rule_condition_chart/rule_condition_chart.tsx} (88%) diff --git a/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/constants.ts b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/constants.ts index f2d6a056c09db..0f71486ac65f2 100644 --- a/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/constants.ts +++ b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/constants.ts @@ -11,4 +11,5 @@ export const METRIC_ID = 'lnsMetric'; export const METRIC_TREND_LINE_ID = 'metricTrendline'; export const XY_REFERENCE_LINE_ID = 'referenceLine'; +export const XY_ANNOTATIONS_ID = 'annotations'; export const XY_DATA_ID = 'data'; diff --git a/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/index.ts b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/index.ts index 2620d7639f5ff..ed7dd2fd971c1 100644 --- a/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/index.ts +++ b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/index.ts @@ -12,6 +12,9 @@ export { XYReferenceLinesLayer, type XYReferenceLinesLayerConfig, } from './xy_reference_lines_layer'; - +export { + XYByValueAnnotationsLayer, + type XYByValueAnnotationsLayerConfig, +} from './xy_by_value_annotation_layer'; export { FormulaColumn } from './columns/formula'; export { StaticColumn } from './columns/static'; diff --git a/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/xy_by_value_annotation_layer.ts b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/xy_by_value_annotation_layer.ts new file mode 100644 index 0000000000000..41e62caab9a23 --- /dev/null +++ b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/xy_by_value_annotation_layer.ts @@ -0,0 +1,66 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SavedObjectReference } from '@kbn/core/server'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { EventAnnotationConfig } from '@kbn/event-annotation-common'; +import type { FormBasedPersistedState, PersistedIndexPatternLayer } from '@kbn/lens-plugin/public'; +import type { XYByValueAnnotationLayerConfig } from '@kbn/lens-plugin/public/visualizations/xy/types'; +import type { ChartLayer } from '../../types'; +import { getDefaultReferences } from '../../utils'; +import { XY_ANNOTATIONS_ID } from '../constants'; + +export interface XYByValueAnnotationsLayerConfig { + annotations: EventAnnotationConfig[]; + layerType?: typeof XY_ANNOTATIONS_ID; + /** + * It is possible to define a specific dataView for the layer. It will override the global chart one + **/ + dataView?: DataView; + ignoreGlobalFilters?: boolean; +} + +export class XYByValueAnnotationsLayer implements ChartLayer { + private layerConfig: XYByValueAnnotationsLayerConfig; + + constructor(layerConfig: XYByValueAnnotationsLayerConfig) { + this.layerConfig = { + ...layerConfig, + layerType: layerConfig.layerType ?? 'annotations', + }; + } + + getName(): string | undefined { + return this.layerConfig.annotations[0].label; + } + + getLayer(layerId: string): FormBasedPersistedState['layers'] { + const baseLayer = { columnOrder: [], columns: {} } as PersistedIndexPatternLayer; + return { + [`${layerId}_annotation`]: baseLayer, + }; + } + + getReference(layerId: string, chartDataView: DataView): SavedObjectReference[] { + return getDefaultReferences(this.layerConfig.dataView ?? chartDataView, `${layerId}_reference`); + } + + getLayerConfig(layerId: string): XYByValueAnnotationLayerConfig { + return { + layerId: `${layerId}_annotation`, + layerType: 'annotations', + annotations: this.layerConfig.annotations, + ignoreGlobalFilters: this.layerConfig.ignoreGlobalFilters || false, + indexPatternId: this.layerConfig.dataView?.id || '', + }; + } + + getDataView(): DataView | undefined { + return this.layerConfig.dataView; + } +} diff --git a/packages/kbn-lens-embeddable-utils/index.ts b/packages/kbn-lens-embeddable-utils/index.ts index ffe9d4e87d788..45d324c41fa2c 100644 --- a/packages/kbn-lens-embeddable-utils/index.ts +++ b/packages/kbn-lens-embeddable-utils/index.ts @@ -30,6 +30,7 @@ export { XYChart, XYDataLayer, XYReferenceLinesLayer, + XYByValueAnnotationsLayer, METRIC_ID, METRIC_TREND_LINE_ID, XY_ID, diff --git a/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/__snapshots__/alert_details_app_section.test.tsx.snap b/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/__snapshots__/alert_details_app_section.test.tsx.snap index 0446f2fe1d4a2..b963137281b70 100644 --- a/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/__snapshots__/alert_details_app_section.test.tsx.snap +++ b/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/__snapshots__/alert_details_app_section.test.tsx.snap @@ -4,24 +4,35 @@ exports[`AlertDetailsAppSection should render annotations 1`] = ` Array [ Object { "annotations": Array [ - , - , + Object { + "color": "#BD271E", + "icon": "alert", + "id": "custom_threshold_alert_start_annotation", + "key": Object { + "timestamp": "2023-03-28T13:40:00.000Z", + "type": "point_in_time", + }, + "label": "Alert", + "type": "manual", + }, + Object { + "color": "#F04E9833", + "id": "custom_threshold_recovered_alert_range_annotation", + "key": Object { + "endTimestamp": "2023-03-28T14:40:00.000Z", + "timestamp": "2023-03-28T13:40:00.000Z", + "type": "range", + }, + "label": "Alert duration", + "type": "manual", + }, ], - "chartType": "line", - "derivedIndexPattern": Object { - "fields": Array [], - "title": "unknown-index", - }, - "expression": Object { + "dataView": undefined, + "filterQuery": "", + "groupBy": Array [ + "host.hostname", + ], + "metricExpression": Object { "comparator": ">", "metrics": Array [ Object { @@ -35,11 +46,7 @@ Array [ "timeSize": 15, "timeUnit": "m", }, - "filterQuery": "host.hostname: Users-System.local and service.type: system", - "groupBy": Array [ - "host.hostname", - ], - "hideTitle": true, + "seriesType": "bar_stacked", "timeRange": Object { "from": "2023-03-28T10:43:13.802Z", "to": "2023-03-29T13:14:09.581Z", diff --git a/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.test.tsx b/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.test.tsx index 1e7c91af675ba..80d7feb20a4e6 100644 --- a/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.test.tsx +++ b/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.test.tsx @@ -18,7 +18,7 @@ import { buildCustomThresholdRule, } from '../../mocks/custom_threshold_rule'; import { CustomThresholdAlertFields } from '../../types'; -import { ExpressionChart } from '../expression_chart'; +import { RuleConditionChart } from '../rule_condition_chart/rule_condition_chart'; import AlertDetailsAppSection, { CustomThresholdAlert } from './alert_details_app_section'; import { Groups } from './groups'; import { Tags } from './tags'; @@ -37,8 +37,8 @@ jest.mock('@kbn/observability-get-padded-alert-time-range-util', () => ({ }), })); -jest.mock('../expression_chart', () => ({ - ExpressionChart: jest.fn(() =>
), +jest.mock('../rule_condition_chart/rule_condition_chart', () => ({ + RuleConditionChart: jest.fn(() =>
), })); jest.mock('../../../../utils/kibana_react', () => ({ @@ -141,11 +141,14 @@ describe('AlertDetailsAppSection', () => { }); it('should render annotations', async () => { - const mockedExpressionChart = jest.fn(() =>
); - (ExpressionChart as jest.Mock).mockImplementation(mockedExpressionChart); - const alertDetailsAppSectionComponent = renderComponent(); + const mockedRuleConditionChart = jest.fn(() =>
); + (RuleConditionChart as jest.Mock).mockImplementation(mockedRuleConditionChart); + const alertDetailsAppSectionComponent = renderComponent( + {}, + { ['kibana.alert.end']: '2023-03-28T14:40:00.000Z' } + ); - expect(alertDetailsAppSectionComponent.getAllByTestId('ExpressionChart').length).toBe(3); - expect(mockedExpressionChart.mock.calls[0]).toMatchSnapshot(); + expect(alertDetailsAppSectionComponent.getAllByTestId('RuleConditionChart').length).toBe(3); + expect(mockedRuleConditionChart.mock.calls[0]).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx b/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx index d5022c2dc1f7d..9d2aebddb43ee 100644 --- a/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx +++ b/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx @@ -5,11 +5,10 @@ * 2.0. */ -import moment from 'moment'; -import { DataViewBase, Query } from '@kbn/es-query'; +import { Query } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -18,10 +17,8 @@ import { EuiSpacer, EuiText, EuiTitle, - useEuiTheme, } from '@elastic/eui'; import { Rule, RuleTypeParams } from '@kbn/alerting-plugin/common'; -import { AlertAnnotation, AlertActiveTimeRangeAnnotation } from '@kbn/observability-alert-details'; import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util'; import { ALERT_END, @@ -31,7 +28,14 @@ import { TAGS, } from '@kbn/rule-data-utils'; import { DataView } from '@kbn/data-views-plugin/common'; -import { MetricsExplorerChartType } from '../../../../../common/custom_threshold_rule/types'; +import chroma from 'chroma-js'; +import type { + EventAnnotationConfig, + PointInTimeEventAnnotationConfig, + RangeEventAnnotationConfig, +} from '@kbn/event-annotation-common'; +import moment from 'moment'; +import { transparentize, useEuiTheme } from '@elastic/eui'; import { useLicense } from '../../../../hooks/use_license'; import { useKibana } from '../../../../utils/kibana_react'; import { metricValueFormatter } from '../../../../../common/custom_threshold_rule/metric_value_formatter'; @@ -41,21 +45,18 @@ import { CustomThresholdAlertFields, CustomThresholdRuleTypeParams, } from '../../types'; -import { ExpressionChart } from '../expression_chart'; import { TIME_LABELS } from '../criterion_preview_chart/criterion_preview_chart'; import { Threshold } from '../custom_threshold'; import { LogRateAnalysis } from './log_rate_analysis'; import { Groups } from './groups'; import { Tags } from './tags'; +import { RuleConditionChart } from '../rule_condition_chart/rule_condition_chart'; +import { getFilterQuery } from './helpers/get_filter_query'; // TODO Use a generic props for app sections https://github.com/elastic/kibana/issues/152690 export type CustomThresholdRule = Rule; export type CustomThresholdAlert = TopAlert; -const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD HH:mm'; -const ALERT_START_ANNOTATION_ID = 'alert_start_annotation'; -const ALERT_TIME_RANGE_ANNOTATION_ID = 'alert_time_range_annotation'; - interface AppSectionProps { alert: CustomThresholdAlert; rule: CustomThresholdRule; @@ -71,37 +72,49 @@ export default function AlertDetailsAppSection({ setAlertSummaryFields, }: AppSectionProps) { const services = useKibana().services; - const { uiSettings, charts, data } = services; - const { euiTheme } = useEuiTheme(); + const { charts, data } = services; const { hasAtLeast } = useLicense(); + const { euiTheme } = useEuiTheme(); const hasLogRateAnalysisLicense = hasAtLeast('platinum'); const [dataView, setDataView] = useState(); + const [filterQuery, setFilterQuery] = useState(''); const [, setDataViewError] = useState(); const ruleParams = rule.params as RuleTypeParams & AlertParams; const chartProps = { baseTheme: charts.theme.useChartsBaseTheme(), }; - const timeRange = getPaddedAlertTimeRange(alert.fields[ALERT_START]!, alert.fields[ALERT_END]); - const alertEnd = alert.fields[ALERT_END] ? moment(alert.fields[ALERT_END]).valueOf() : undefined; + const alertStart = alert.fields[ALERT_START]; + const alertEnd = alert.fields[ALERT_END]; + const timeRange = getPaddedAlertTimeRange(alertStart!, alertEnd); const groups = alert.fields[ALERT_GROUP]; const tags = alert.fields[TAGS]; - const annotations = [ - , - , - ]; + const alertStartAnnotation: PointInTimeEventAnnotationConfig = { + label: 'Alert', + type: 'manual', + key: { + type: 'point_in_time', + timestamp: alertStart!, + }, + color: euiTheme.colors.danger, + icon: 'alert', + id: 'custom_threshold_alert_start_annotation', + }; + + const alertRangeAnnotation: RangeEventAnnotationConfig = { + label: `${alertEnd ? 'Alert duration' : 'Active alert'}`, + type: 'manual', + key: { + type: 'range', + timestamp: alertStart!, + endTimestamp: alertEnd ?? moment().toISOString(), + }, + color: chroma(transparentize('#F04E981A', 0.2)).hex().toUpperCase(), + id: `custom_threshold_${alertEnd ? 'recovered' : 'active'}_alert_range_annotation`, + }; + + const annotations: EventAnnotationConfig[] = []; + annotations.push(alertStartAnnotation, alertRangeAnnotation); useEffect(() => { const alertSummaryFields = []; @@ -144,13 +157,10 @@ export default function AlertDetailsAppSection({ setAlertSummaryFields(alertSummaryFields); }, [groups, tags, rule, ruleLink, setAlertSummaryFields]); - const derivedIndexPattern = useMemo( - () => ({ - fields: dataView?.fields || [], - title: dataView?.getIndexPattern() || 'unknown-index', - }), - [dataView] - ); + useEffect(() => { + const query = `${(ruleParams.searchConfiguration?.query as Query)?.query as string}`; + setFilterQuery(getFilterQuery(query, groups)); + }, [groups, ruleParams.searchConfiguration]); useEffect(() => { const initDataView = async () => { @@ -209,15 +219,15 @@ export default function AlertDetailsAppSection({ /> - diff --git a/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/helpers/get_filter_query.test.ts b/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/helpers/get_filter_query.test.ts new file mode 100644 index 0000000000000..91cfa4f7abc01 --- /dev/null +++ b/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/helpers/get_filter_query.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { getFilterQuery } from './get_filter_query'; + +describe('getFilterQuery', () => { + it('should generate correct filter query when original query is not empty', () => { + const query = 'container.id: container-1'; + const groups = [ + { field: 'container.id', value: 'container-0' }, + { field: 'host.name', value: 'host-0' }, + ]; + + expect(getFilterQuery(query, groups)).toBe( + '(container.id: container-1) and container.id: container-0 and host.name: host-0' + ); + }); + + it('should generate correct filter query when original query is empty', () => { + const query = ''; + const groups = [ + { field: 'container.id', value: 'container-0' }, + { field: 'host.name', value: 'host-0' }, + ]; + + expect(getFilterQuery(query, groups)).toBe('container.id: container-0 and host.name: host-0'); + }); + + it('should generate correct filter query when original query and groups both are empty', () => { + const query = ''; + const groups = undefined; + + expect(getFilterQuery(query, groups)).toBe(''); + }); +}); diff --git a/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/helpers/get_filter_query.ts b/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/helpers/get_filter_query.ts new file mode 100644 index 0000000000000..2e1dd2ee0e543 --- /dev/null +++ b/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/helpers/get_filter_query.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +export const getFilterQuery = ( + filter: string, + groups?: Array<{ + field: string; + value: string; + }> +) => { + let query = filter; + if (groups) { + const groupQueries = groups?.map(({ field, value }) => `${field}: ${value}`).join(' and '); + query = query ? `(${query}) and ${groupQueries}` : groupQueries; + } + return query; +}; diff --git a/x-pack/plugins/observability/public/components/custom_threshold/components/preview_chart/painless_tinymath_parser.test.ts b/x-pack/plugins/observability/public/components/custom_threshold/components/rule_condition_chart/painless_tinymath_parser.test.ts similarity index 100% rename from x-pack/plugins/observability/public/components/custom_threshold/components/preview_chart/painless_tinymath_parser.test.ts rename to x-pack/plugins/observability/public/components/custom_threshold/components/rule_condition_chart/painless_tinymath_parser.test.ts diff --git a/x-pack/plugins/observability/public/components/custom_threshold/components/preview_chart/painless_tinymath_parser.ts b/x-pack/plugins/observability/public/components/custom_threshold/components/rule_condition_chart/painless_tinymath_parser.ts similarity index 100% rename from x-pack/plugins/observability/public/components/custom_threshold/components/preview_chart/painless_tinymath_parser.ts rename to x-pack/plugins/observability/public/components/custom_threshold/components/rule_condition_chart/painless_tinymath_parser.ts diff --git a/x-pack/plugins/observability/public/components/custom_threshold/components/preview_chart/preview_chart.test.tsx b/x-pack/plugins/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.test.tsx similarity index 92% rename from x-pack/plugins/observability/public/components/custom_threshold/components/preview_chart/preview_chart.test.tsx rename to x-pack/plugins/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.test.tsx index bc478e54135ba..320958a20c0e8 100644 --- a/x-pack/plugins/observability/public/components/custom_threshold/components/preview_chart/preview_chart.test.tsx +++ b/x-pack/plugins/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.test.tsx @@ -13,7 +13,7 @@ import { Comparator, Aggregators } from '../../../../../common/custom_threshold_ import { useKibana } from '../../../../utils/kibana_react'; import { kibanaStartMock } from '../../../../utils/kibana_react.mock'; import { MetricExpression } from '../../types'; -import { getBufferThreshold, PreviewChart } from './preview_chart'; +import { getBufferThreshold, RuleConditionChart } from './rule_condition_chart'; jest.mock('../../../../utils/kibana_react'); @@ -25,19 +25,20 @@ const mockKibana = () => { }); }; -describe('Preview chart', () => { +describe('Rule condition chart', () => { beforeEach(() => { jest.clearAllMocks(); mockKibana(); }); async function setup(expression: MetricExpression, dataView?: DataView) { const wrapper = mountWithIntl( - ); diff --git a/x-pack/plugins/observability/public/components/custom_threshold/components/preview_chart/preview_chart.tsx b/x-pack/plugins/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.tsx similarity index 88% rename from x-pack/plugins/observability/public/components/custom_threshold/components/preview_chart/preview_chart.tsx rename to x-pack/plugins/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.tsx index bcaf87e2cce3d..bc2701eb79489 100644 --- a/x-pack/plugins/observability/public/components/custom_threshold/components/preview_chart/preview_chart.tsx +++ b/x-pack/plugins/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.tsx @@ -6,7 +6,7 @@ */ import React, { useState, useEffect } from 'react'; import { EuiEmptyPrompt, useEuiTheme } from '@elastic/eui'; -import { FillStyle, OperationType } from '@kbn/lens-plugin/public'; +import { FillStyle, OperationType, SeriesType } from '@kbn/lens-plugin/public'; import { DataView } from '@kbn/data-views-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; import useAsync from 'react-use/lib/useAsync'; @@ -17,10 +17,13 @@ import { XYDataLayer, XYLayerOptions, XYReferenceLinesLayer, + XYByValueAnnotationsLayer, } from '@kbn/lens-embeddable-utils'; import { IErrorObject } from '@kbn/triggers-actions-ui-plugin/public'; import { i18n } from '@kbn/i18n'; +import { TimeRange } from '@kbn/es-query'; +import { EventAnnotationConfig } from '@kbn/event-annotation-common'; import { Aggregators, Comparator, @@ -30,12 +33,15 @@ import { useKibana } from '../../../../utils/kibana_react'; import { MetricExpression } from '../../types'; import { AggMap, PainlessTinyMathParser } from './painless_tinymath_parser'; -interface PreviewChartPros { +interface RuleConditionChartProps { metricExpression: MetricExpression; dataView?: DataView; filterQuery?: string; groupBy?: string | string[]; error?: IErrorObject; + timeRange: TimeRange; + annotations?: EventAnnotationConfig[]; + seriesType?: SeriesType; } const getOperationTypeFromRuleAggType = (aggType: AggType): OperationType => { @@ -47,13 +53,16 @@ const getOperationTypeFromRuleAggType = (aggType: AggType): OperationType => { export const getBufferThreshold = (threshold?: number): string => (Math.ceil((threshold || 0) * 1.1 * 100) / 100).toFixed(2).toString(); -export function PreviewChart({ +export function RuleConditionChart({ metricExpression, dataView, filterQuery, groupBy, error, -}: PreviewChartPros) { + annotations, + timeRange, + seriesType, +}: RuleConditionChartProps) { const { services: { lens }, } = useKibana(); @@ -63,6 +72,7 @@ export function PreviewChart({ const [aggMap, setAggMap] = useState(); const [formula, setFormula] = useState(''); const [thresholdReferenceLine, setThresholdReferenceLine] = useState(); + const [alertAnnotation, setAlertAnnotation] = useState(); const [chartLoading, setChartLoading] = useState(false); const formulaAsync = useAsync(() => { return lens.stateHelperApi(); @@ -162,6 +172,19 @@ export function PreviewChart({ setThresholdReferenceLine(refLayers); }, [threshold, comparator, euiTheme.colors.danger, metrics]); + // Build alert annotation + useEffect(() => { + if (!annotations) return; + + const alertAnnotationLayer = new XYByValueAnnotationsLayer({ + annotations, + ignoreGlobalFilters: true, + dataView, + }); + + setAlertAnnotation(alertAnnotationLayer); + }, [euiTheme.colors.danger, dataView, annotations]); + // Build the aggregation map from the metrics useEffect(() => { if (!metrics || metrics.length === 0) { @@ -229,7 +252,7 @@ export function PreviewChart({ interval: `${timeSize}${timeUnit}`, }, }, - seriesType: 'bar', + seriesType: seriesType ? seriesType : 'bar', }; if (groupBy && groupBy?.length) { @@ -254,10 +277,15 @@ export function PreviewChart({ options: xYDataLayerOptions, }); - const layers: Array = [xyDataLayer]; + const layers: Array = [ + xyDataLayer, + ]; if (thresholdReferenceLine) { layers.push(...thresholdReferenceLine); } + if (alertAnnotation) { + layers.push(alertAnnotation); + } const attributesLens = new LensAttributesBuilder({ visualization: new XYChart({ visualOptions: { @@ -286,8 +314,10 @@ export function PreviewChart({ metrics, threshold, thresholdReferenceLine, + alertAnnotation, timeSize, timeUnit, + seriesType, ]); if ( @@ -295,7 +325,8 @@ export function PreviewChart({ !attributes || error?.equation || Object.keys(error?.metrics || {}).length !== 0 || - !timeSize + !timeSize || + !timeRange ) { return (
@@ -327,7 +358,7 @@ export function PreviewChart({ onLoad={setChartLoading} id="customThresholdPreviewChart" style={{ height: 180 }} - timeRange={{ from: `now-${timeSize * 20}${timeUnit}`, to: 'now' }} + timeRange={timeRange} attributes={attributes} disableTriggers={true} query={{ diff --git a/x-pack/plugins/observability/public/components/custom_threshold/custom_threshold_rule_expression.test.tsx b/x-pack/plugins/observability/public/components/custom_threshold/custom_threshold_rule_expression.test.tsx index a97de99ec8e78..8f569e1d80b17 100644 --- a/x-pack/plugins/observability/public/components/custom_threshold/custom_threshold_rule_expression.test.tsx +++ b/x-pack/plugins/observability/public/components/custom_threshold/custom_threshold_rule_expression.test.tsx @@ -20,8 +20,8 @@ import Expressions from './custom_threshold_rule_expression'; import { AlertParams, CustomThresholdPrefillOptions } from './types'; jest.mock('../../utils/kibana_react'); -jest.mock('./components/preview_chart/preview_chart', () => ({ - PreviewChart: jest.fn(() =>
), +jest.mock('./components/rule_condition_chart/rule_condition_chart', () => ({ + RuleConditionChart: jest.fn(() =>
), })); const useKibanaMock = useKibana as jest.Mock; diff --git a/x-pack/plugins/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx b/x-pack/plugins/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx index da883e2bc36ed..43cd185d55c4a 100644 --- a/x-pack/plugins/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx +++ b/x-pack/plugins/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx @@ -42,7 +42,7 @@ import { TimeUnitChar } from '../../../common/utils/formatters/duration'; import { AlertContextMeta, AlertParams, MetricExpression } from './types'; import { ExpressionRow } from './components/expression_row'; import { MetricsExplorerFields, GroupBy } from './components/group_by'; -import { PreviewChart } from './components/preview_chart/preview_chart'; +import { RuleConditionChart as PreviewChart } from './components/rule_condition_chart/rule_condition_chart'; const FILTER_TYPING_DEBOUNCE_MS = 500; @@ -458,6 +458,7 @@ export default function Expressions(props: Props) { filterQuery={(ruleParams.searchConfiguration?.query as Query)?.query as string} groupBy={ruleParams.groupBy} error={(errors[idx] as IErrorObject) || emptyError} + timeRange={{ from: `now-${(timeSize ?? 1) * 20}${timeUnit}`, to: 'now' }} />
diff --git a/x-pack/plugins/observability/public/pages/alert_details/components/header_actions.tsx b/x-pack/plugins/observability/public/pages/alert_details/components/header_actions.tsx index fa6ae4c72758a..88418530eb757 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/components/header_actions.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/components/header_actions.tsx @@ -139,7 +139,7 @@ export function HeaderActions({ alert, alertStatus, onUntrackAlert }: HeaderActi /> } > -
+
diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index 82a58c0f53b61..8f60c6472d706 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -106,6 +106,7 @@ "@kbn/core-ui-settings-browser-mocks", "@kbn/field-formats-plugin", "@kbn/aiops-utils", + "@kbn/event-annotation-common", "@kbn/controls-plugin" ], "exclude": [