From 85f12800bb80d959b3d1fb1c2d4c912b1a4e19c4 Mon Sep 17 00:00:00 2001 From: Faisal Kanout Date: Thu, 20 Jun 2024 14:16:16 +0200 Subject: [PATCH] [OBX-UI-MNGMT] Align the Metric rule charts by using Lens in Alert details page and Creation Rule flyout (#184950) ## Summary Fixes #184922 Fixes #184574 It uses the `RuleConditionChart`, a.k.a Lens chart, for the Metric Threshold rule. ### Implemented in both places: - Metric Alert Details page ![Screenshot 2024-06-10 at 16 12 43](https://github.com/elastic/kibana/assets/6838659/9d88d9b9-fe5d-4f8d-9e5a-538c52c58692) - Rule creation flyout ![Screenshot 2024-06-10 at 16 13 18](https://github.com/elastic/kibana/assets/6838659/8c9ca3b3-2fbf-4cfa-83c9-00278c5e8e77) --- .../alert_details_app_section.test.tsx.snap | 68 ++++++--- .../alert_details_app_section.test.tsx | 38 ++++- .../components/alert_details_app_section.tsx | 140 +++++++++++------- .../components/expression.tsx | 44 +++++- .../mocks/metric_threshold_rule.ts | 1 + .../infra/tsconfig.json | 4 +- .../alert_details_app_section.test.tsx | 4 +- .../alert_details_app_section.tsx | 4 +- .../custom_threshold_rule_expression.test.tsx | 2 +- .../custom_threshold_rule_expression.tsx | 2 +- .../rule_condition_chart/helpers.test.ts | 2 +- .../rule_condition_chart/helpers.ts | 12 +- .../painless_tinymath_parser.test.ts | 0 .../painless_tinymath_parser.ts | 0 .../rule_condition_chart.test.tsx | 16 +- .../rule_condition_chart.tsx | 140 ++++++++++++++++-- .../observability/public/index.ts | 3 + .../translations/translations/fr-FR.json | 3 - .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 20 files changed, 354 insertions(+), 135 deletions(-) rename x-pack/plugins/observability_solution/observability/public/components/{custom_threshold/components => }/rule_condition_chart/helpers.test.ts (98%) rename x-pack/plugins/observability_solution/observability/public/components/{custom_threshold/components => }/rule_condition_chart/helpers.ts (85%) rename x-pack/plugins/observability_solution/observability/public/components/{custom_threshold/components => }/rule_condition_chart/painless_tinymath_parser.test.ts (100%) rename x-pack/plugins/observability_solution/observability/public/components/{custom_threshold/components => }/rule_condition_chart/painless_tinymath_parser.ts (100%) rename x-pack/plugins/observability_solution/observability/public/components/{custom_threshold/components => }/rule_condition_chart/rule_condition_chart.test.tsx (78%) rename x-pack/plugins/observability_solution/observability/public/components/{custom_threshold/components => }/rule_condition_chart/rule_condition_chart.tsx (71%) diff --git a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/__snapshots__/alert_details_app_section.test.tsx.snap b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/__snapshots__/alert_details_app_section.test.tsx.snap index 72381f6e62d636..af8a5b3d8e0a96 100644 --- a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/__snapshots__/alert_details_app_section.test.tsx.snap +++ b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/__snapshots__/alert_details_app_section.test.tsx.snap @@ -3,37 +3,61 @@ exports[`AlertDetailsAppSection should render annotations 1`] = ` Array [ Object { + "additionalFilters": undefined, "annotations": Array [ - , - , + Object { + "color": "#BD271E", + "icon": "alert", + "id": "metric_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": "metric_threshold_active_alert_range_annotation", + "key": Object { + "endTimestamp": "2024-06-13T07:00:33.381Z", + "timestamp": "2023-03-28T13:40:00.000Z", + "type": "range", + }, + "label": "Active alert", + "type": "manual", + }, ], - "chartType": "line", - "expression": Object { - "aggType": "count", + "chartOptions": Object { + "seriesType": "bar_stacked", + }, + "dataView": "index", + "groupBy": Array [ + "host.hostname", + ], + "metricExpression": Object { "comparator": ">", + "metrics": Array [ + Object { + "aggType": "count", + "field": "", + "name": "A", + }, + ], "threshold": Array [ 2000, ], "timeSize": 15, "timeUnit": "m", + "warningComparator": undefined, + "warningThreshold": undefined, + }, + "searchConfiguration": Object { + "query": Object { + "language": "", + "query": "", + }, }, - "filterQuery": undefined, - "groupBy": Array [ - "host.hostname", - ], - "groupInstance": Array [ - "host-1", - ], - "hideTitle": true, "timeRange": Object { "from": "2023-03-28T10:43:13.802Z", "to": "2023-03-29T13:14:09.581Z", diff --git a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/alert_details_app_section.test.tsx b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/alert_details_app_section.test.tsx index eaf077684cf4da..5f8b99629eeb8d 100644 --- a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/alert_details_app_section.test.tsx +++ b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/alert_details_app_section.test.tsx @@ -16,15 +16,35 @@ import { buildMetricThresholdRule, } from '../mocks/metric_threshold_rule'; import { AlertDetailsAppSection } from './alert_details_app_section'; -import { ExpressionChart } from './expression_chart'; +import { RuleConditionChart } from '@kbn/observability-plugin/public'; +import { lensPluginMock } from '@kbn/lens-plugin/public/mocks'; const mockedChartStartContract = chartPluginMock.createStartContract(); +const mockedLensStartContract = lensPluginMock.createStartContract(); + +Date.now = jest.fn(() => new Date('2024-06-13T07:00:33.381Z').getTime()); + +jest.mock('../../../containers/metrics_source', () => ({ + useMetricsDataViewContext: () => ({ + metricsView: { dataViewReference: 'index' }, + }), + withSourceProvider: + (Component: React.FC) => + () => { + return function ComponentWithSourceProvider(props: ComponentProps) { + return
; + }; + }, +})); jest.mock('@kbn/observability-alert-details', () => ({ AlertAnnotation: () => {}, AlertActiveTimeRangeAnnotation: () => {}, })); - +jest.mock('@kbn/observability-alert-details', () => ({ + AlertAnnotation: () => {}, + AlertActiveTimeRangeAnnotation: () => {}, +})); jest.mock('@kbn/observability-get-padded-alert-time-range-util', () => ({ getPaddedAlertTimeRange: () => ({ from: '2023-03-28T10:43:13.802Z', @@ -32,8 +52,9 @@ jest.mock('@kbn/observability-get-padded-alert-time-range-util', () => ({ }), })); -jest.mock('./expression_chart', () => ({ - ExpressionChart: jest.fn(() =>
), +jest.mock('@kbn/observability-plugin/public', () => ({ + RuleConditionChart: jest.fn(() =>
), + getGroupFilters: jest.fn(), })); jest.mock('../../../hooks/use_kibana', () => ({ @@ -41,6 +62,7 @@ jest.mock('../../../hooks/use_kibana', () => ({ services: { ...mockCoreMock.createStart(), charts: mockedChartStartContract, + lens: mockedLensStartContract, }, }), })); @@ -74,11 +96,11 @@ describe('AlertDetailsAppSection', () => { }); it('should render annotations', async () => { - const mockedExpressionChart = jest.fn(() =>
); - (ExpressionChart as jest.Mock).mockImplementation(mockedExpressionChart); + const mockedRuleConditionChart = jest.fn(() =>
); + (RuleConditionChart as jest.Mock).mockImplementation(mockedRuleConditionChart); renderComponent(); - expect(mockedExpressionChart).toHaveBeenCalledTimes(3); - expect(mockedExpressionChart.mock.calls[0]).toMatchSnapshot(); + expect(mockedRuleConditionChart).toHaveBeenCalledTimes(3); + expect(mockedRuleConditionChart.mock.calls[0]).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/alert_details_app_section.tsx b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/alert_details_app_section.tsx index bee0035f210c1e..78d908d85ad8c7 100644 --- a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/alert_details_app_section.tsx +++ b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/alert_details_app_section.tsx @@ -15,29 +15,32 @@ import { EuiPanel, EuiSpacer, EuiTitle, + transparentize, useEuiTheme, } from '@elastic/eui'; -import { AlertSummaryField, TopAlert } from '@kbn/observability-plugin/public'; +import chroma from 'chroma-js'; + +import { AlertSummaryField, RuleConditionChart, TopAlert } from '@kbn/observability-plugin/public'; import { ALERT_END, ALERT_START, ALERT_EVALUATION_VALUES, ALERT_GROUP } from '@kbn/rule-data-utils'; -import { Rule } from '@kbn/alerting-plugin/common'; -import { AlertAnnotation, AlertActiveTimeRangeAnnotation } from '@kbn/observability-alert-details'; +import { Rule, RuleTypeParams } from '@kbn/alerting-plugin/common'; import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util'; +import type { + EventAnnotationConfig, + PointInTimeEventAnnotationConfig, + RangeEventAnnotationConfig, +} from '@kbn/event-annotation-common'; + +import { getGroupFilters } from '@kbn/observability-plugin/public'; +import type { GenericAggType } from '@kbn/observability-plugin/public'; import { metricValueFormatter } from '../../../../common/alerting/metrics/metric_value_formatter'; import { Threshold } from '../../common/components/threshold'; -import { withSourceProvider } from '../../../containers/metrics_source'; +import { useMetricsDataViewContext, withSourceProvider } from '../../../containers/metrics_source'; import { generateUniqueKey } from '../lib/generate_unique_key'; -import { MetricsExplorerChartType } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; -import { MetricThresholdRuleTypeParams } from '..'; -import { ExpressionChart } from './expression_chart'; +import { AlertParams } from '../types'; // TODO Use a generic props for app sections https://github.com/elastic/kibana/issues/152690 -export type MetricThresholdRule = Rule< - MetricThresholdRuleTypeParams & { - filterQueryText?: string; - groupBy?: string | string[]; - } ->; +export type MetricThresholdRule = Rule; interface Group { field: string; @@ -51,41 +54,49 @@ interface MetricThresholdAlertField { export type MetricThresholdAlert = 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: MetricThresholdAlert; rule: MetricThresholdRule; setAlertSummaryFields: React.Dispatch>; } -export function AlertDetailsAppSection({ alert, rule }: AppSectionProps) { - const { uiSettings, charts } = useKibanaContextForPlugin().services; +export function AlertDetailsAppSection({ alert, rule, setAlertSummaryFields }: AppSectionProps) { + const { charts } = useKibanaContextForPlugin().services; const { euiTheme } = useEuiTheme(); - const groupInstance = alert.fields[ALERT_GROUP]?.map((group: Group) => group.value); - + const groups = alert.fields[ALERT_GROUP]; + const { metricsView } = useMetricsDataViewContext(); const chartProps = { baseTheme: charts.theme.useChartsBaseTheme(), }; - const alertEnd = alert.fields[ALERT_END] ? moment(alert.fields[ALERT_END]).valueOf() : undefined; - const annotations = [ - , - , - ]; + const alertEnd = alert.fields[ALERT_END]; + const alertStart = alert.fields[ALERT_START]; + + const alertStartAnnotation: PointInTimeEventAnnotationConfig = { + label: 'Alert', + type: 'manual', + key: { + type: 'point_in_time', + timestamp: alertStart!, + }, + color: euiTheme.colors.danger, + icon: 'alert', + id: 'metric_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: `metric_threshold_${alertEnd ? 'recovered' : 'active'}_alert_range_annotation`, + }; + + const annotations: EventAnnotationConfig[] = []; + annotations.push(alertStartAnnotation, alertRangeAnnotation); return !!rule.params.criteria ? ( @@ -94,10 +105,25 @@ export function AlertDetailsAppSection({ alert, rule }: AppSectionProps) { alert.fields[ALERT_START]!, alert.fields[ALERT_END], { - size: criterion.timeSize, - unit: criterion.timeUnit, + size: criterion.timeSize!, + unit: criterion.timeUnit!, } ); + let metricExpression = [ + { + aggType: criterion.aggType as GenericAggType, + name: String.fromCharCode('A'.charCodeAt(0) + index), + field: criterion.metric || '', + }, + ]; + if (criterion.customMetrics) { + metricExpression = criterion.customMetrics.map((metric) => ({ + name: metric.name, + aggType: metric.aggType as GenericAggType, + field: metric.field || '', + filter: metric.filter, + })); + } return ( @@ -135,16 +161,30 @@ export function AlertDetailsAppSection({ alert, rule }: AppSectionProps) { /> - + {metricsView && ( + + )} diff --git a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/expression.tsx index 191c6ed8cd847c..9eaf5e2bd7b459 100644 --- a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/expression.tsx @@ -28,6 +28,7 @@ import { } from '@kbn/triggers-actions-ui-plugin/public'; import { TimeUnitChar } from '@kbn/observability-plugin/common/utils/formatters/duration'; import { COMPARATORS } from '@kbn/alerting-comparators'; +import { GenericAggType, RuleConditionChart } from '@kbn/observability-plugin/public'; import { Aggregators, QUERY_INVALID } from '../../../../common/alerting/metrics'; import { useMetricsDataViewContext, @@ -40,7 +41,6 @@ import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; import { convertKueryToElasticSearchQuery } from '../../../utils/kuery'; import { AlertContextMeta, AlertParams, MetricExpression } from '../types'; -import { ExpressionChart } from './expression_chart'; import { ExpressionRow } from './expression_row'; const FILTER_TYPING_DEBOUNCE_MS = 500; @@ -69,7 +69,6 @@ export const Expressions: React.FC = (props) => { const { docLinks } = useKibanaContextForPlugin().services; const { source } = useSourceContext(); const { metricsView } = useMetricsDataViewContext(); - const [timeSize, setTimeSize] = useState(1); const [timeUnit, setTimeUnit] = useState('m'); @@ -304,8 +303,24 @@ export const Expressions: React.FC = (props) => { - {ruleParams.criteria && + {metricsView && ruleParams.criteria.map((e, idx) => { + let metricExpression = [ + { + aggType: e.aggType as GenericAggType, + // RuleConditionChart uses A,B,C etc in its parser to identify multiple conditions + name: String.fromCharCode('A'.charCodeAt(0) + idx), + field: e.metric || '', + }, + ]; + if (e.customMetrics) { + metricExpression = e.customMetrics.map((metric) => ({ + name: metric.name, + aggType: metric.aggType as GenericAggType, + field: metric.field || '', + filter: metric.filter, + })); + } return ( 1) || false} @@ -317,9 +332,26 @@ export const Expressions: React.FC = (props) => { errors={(errors[idx] as IErrorObject) || emptyError} expression={e || {}} > - diff --git a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/mocks/metric_threshold_rule.ts b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/mocks/metric_threshold_rule.ts index 0ef6478ff12d7a..f7ec9022b4cad5 100644 --- a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/mocks/metric_threshold_rule.ts +++ b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/mocks/metric_threshold_rule.ts @@ -87,6 +87,7 @@ export const buildMetricThresholdRule = ( filterQuery: '{"bool":{"filter":[{"bool":{"should":[{"term":{"host.hostname":{"value":"Users-System.local"}}}],"minimum_should_match":1}},{"bool":{"should":[{"term":{"service.type":{"value":"system"}}}],"minimum_should_match":1}}]}}', groupBy: ['host.hostname'], + sourceId: 'sourceId', }, monitoring: { run: { diff --git a/x-pack/plugins/observability_solution/infra/tsconfig.json b/x-pack/plugins/observability_solution/infra/tsconfig.json index d1d1f0542da0a1..f23c9f7b30c349 100644 --- a/x-pack/plugins/observability_solution/infra/tsconfig.json +++ b/x-pack/plugins/observability_solution/infra/tsconfig.json @@ -63,7 +63,6 @@ "@kbn/shared-ux-router", "@kbn/shared-ux-link-redirect-app", "@kbn/discover-plugin", - "@kbn/observability-alert-details", "@kbn/observability-shared-plugin", "@kbn/observability-ai-assistant-plugin", "@kbn/ui-theme", @@ -105,7 +104,8 @@ "@kbn/react-kibana-context-theme", "@kbn/presentation-publishing", "@kbn/presentation-containers", - "@kbn/deeplinks-observability" + "@kbn/deeplinks-observability", + "@kbn/event-annotation-common" ], "exclude": [ "target/**/*" diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.test.tsx b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.test.tsx index 35104fd199d3ab..a98a519a1606a2 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.test.tsx +++ b/x-pack/plugins/observability_solution/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 { RuleConditionChart } from '../rule_condition_chart/rule_condition_chart'; +import { RuleConditionChart } from '../../../rule_condition_chart/rule_condition_chart'; import { CustomThresholdAlert } from '../types'; import AlertDetailsAppSection from './alert_details_app_section'; @@ -47,7 +47,7 @@ jest.mock('@kbn/observability-get-padded-alert-time-range-util', () => ({ }), })); -jest.mock('../rule_condition_chart/rule_condition_chart', () => ({ +jest.mock('../../../rule_condition_chart/rule_condition_chart', () => ({ RuleConditionChart: jest.fn(() =>
), })); diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx index 3a065d6e06f3e6..83aa8cf2a35d30 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx @@ -31,16 +31,16 @@ import type { import moment from 'moment'; import { LOGS_EXPLORER_LOCATOR_ID, LogsExplorerLocatorParams } from '@kbn/deeplinks-observability'; import { TimeRange } from '@kbn/es-query'; +import { getGroupFilters } from '../../../../../common/custom_threshold_rule/helpers/get_group'; import { useLicense } from '../../../../hooks/use_license'; import { useKibana } from '../../../../utils/kibana_react'; -import { getGroupFilters } from '../../../../../common/custom_threshold_rule/helpers/get_group'; import { metricValueFormatter } from '../../../../../common/custom_threshold_rule/metric_value_formatter'; import { AlertSummaryField } from '../../../..'; import { AlertParams } from '../../types'; import { Threshold } from '../custom_threshold'; import { CustomThresholdRule, CustomThresholdAlert } from '../types'; import { LogRateAnalysis } from './log_rate_analysis'; -import { RuleConditionChart } from '../rule_condition_chart/rule_condition_chart'; +import { RuleConditionChart } from '../../../rule_condition_chart/rule_condition_chart'; import { getViewInAppUrl } from '../../../../../common/custom_threshold_rule/get_view_in_app_url'; import { SearchConfigurationWithExtractedReferenceType } from '../../../../../common/custom_threshold_rule/types'; import { generateChartTitleAndTooltip } from './helpers/generate_chart_title_and_tooltip'; diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/custom_threshold_rule_expression.test.tsx b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/custom_threshold_rule_expression.test.tsx index 02c428ccf36984..62580b3a89f82b 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/custom_threshold_rule_expression.test.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/custom_threshold_rule_expression.test.tsx @@ -21,7 +21,7 @@ import Expressions from './custom_threshold_rule_expression'; import { AlertParams, CustomThresholdPrefillOptions } from './types'; jest.mock('../../utils/kibana_react'); -jest.mock('./components/rule_condition_chart/rule_condition_chart', () => ({ +jest.mock('../rule_condition_chart/rule_condition_chart', () => ({ RuleConditionChart: jest.fn(() =>
), })); diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx index fab9568b080a9c..0db9c2f048e113 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx +++ b/x-pack/plugins/observability_solution/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 { RuleConditionChart as PreviewChart } from './components/rule_condition_chart/rule_condition_chart'; +import { RuleConditionChart as PreviewChart } from '../rule_condition_chart/rule_condition_chart'; import { getSearchConfiguration } from './helpers/get_search_configuration'; const FILTER_TYPING_DEBOUNCE_MS = 500; diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/rule_condition_chart/helpers.test.ts b/x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/helpers.test.ts similarity index 98% rename from x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/rule_condition_chart/helpers.test.ts rename to x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/helpers.test.ts index 4211907b5d4a01..044b57c64da28d 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/rule_condition_chart/helpers.test.ts +++ b/x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/helpers.test.ts @@ -7,7 +7,7 @@ import { Aggregators, CustomThresholdExpressionMetric, -} from '../../../../../common/custom_threshold_rule/types'; +} from '../../../common/custom_threshold_rule/types'; import { getBufferThreshold, getLensOperationFromRuleMetric, lensFieldFormatter } from './helpers'; const useCases = [ [ diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/rule_condition_chart/helpers.ts b/x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/helpers.ts similarity index 85% rename from x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/rule_condition_chart/helpers.ts rename to x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/helpers.ts index 1875af6ceb93ee..7cedf19d0b6606 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/rule_condition_chart/helpers.ts +++ b/x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/helpers.ts @@ -5,12 +5,10 @@ * 2.0. */ -import { - Aggregators, - CustomThresholdExpressionMetric, -} from '../../../../../common/custom_threshold_rule/types'; +import { Aggregators } from '../../../common/custom_threshold_rule/types'; +import { GenericMetric } from './rule_condition_chart'; -export const getLensOperationFromRuleMetric = (metric: CustomThresholdExpressionMetric): string => { +export const getLensOperationFromRuleMetric = (metric: GenericMetric): string => { const { aggType, field, filter } = metric; let operation: string = aggType; const operationArgs: string[] = []; @@ -56,7 +54,7 @@ export const LensFieldFormat = { } as const; export const lensFieldFormatter = ( - metrics: CustomThresholdExpressionMetric[] + metrics: GenericMetric[] ): typeof LensFieldFormat[keyof typeof LensFieldFormat] => { if (metrics.length < 1 || !metrics[0].field) return LensFieldFormat.NUMBER; const firstMetricField = metrics[0].field; @@ -65,5 +63,5 @@ export const lensFieldFormatter = ( return LensFieldFormat.NUMBER; }; -export const isRate = (metrics: CustomThresholdExpressionMetric[]): boolean => +export const isRate = (metrics: GenericMetric[]): boolean => Boolean(metrics.length > 0 && metrics[0].aggType === Aggregators.RATE); diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/rule_condition_chart/painless_tinymath_parser.test.ts b/x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/painless_tinymath_parser.test.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/rule_condition_chart/painless_tinymath_parser.test.ts rename to x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/painless_tinymath_parser.test.ts diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/rule_condition_chart/painless_tinymath_parser.ts b/x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/painless_tinymath_parser.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/rule_condition_chart/painless_tinymath_parser.ts rename to x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/painless_tinymath_parser.ts diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.test.tsx b/x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/rule_condition_chart.test.tsx similarity index 78% rename from x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.test.tsx rename to x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/rule_condition_chart.test.tsx index d164e6670b4a7e..ac0624265be0b7 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.test.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/rule_condition_chart.test.tsx @@ -13,13 +13,12 @@ import { COMPARATORS } from '@kbn/alerting-comparators'; import { Aggregators, CustomThresholdSearchSourceFields, -} from '../../../../../common/custom_threshold_rule/types'; -import { useKibana } from '../../../../utils/kibana_react'; -import { kibanaStartMock } from '../../../../utils/kibana_react.mock'; -import { MetricExpression } from '../../types'; -import { RuleConditionChart } from './rule_condition_chart'; +} from '../../../common/custom_threshold_rule/types'; +import { useKibana } from '../../utils/kibana_react'; +import { kibanaStartMock } from '../../utils/kibana_react.mock'; +import { RuleConditionChart, RuleConditionChartExpressions } from './rule_condition_chart'; -jest.mock('../../../../utils/kibana_react'); +jest.mock('../../utils/kibana_react'); const useKibanaMock = useKibana as jest.Mock; @@ -34,7 +33,7 @@ describe('Rule condition chart', () => { jest.clearAllMocks(); mockKibana(); }); - async function setup(expression: MetricExpression, dataView?: DataView) { + async function setup(expression: RuleConditionChartExpressions, dataView?: DataView) { const wrapper = mountWithIntl( { } it('should display no data message', async () => { - const expression: MetricExpression = { + const expression: RuleConditionChartExpressions = { metrics: [ { name: 'A', @@ -67,7 +66,6 @@ describe('Rule condition chart', () => { ], timeSize: 1, timeUnit: 'm', - sourceId: 'default', threshold: [1], comparator: COMPARATORS.GREATER_THAN_OR_EQUALS, }; diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.tsx b/x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/rule_condition_chart.tsx similarity index 71% rename from x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.tsx rename to x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/rule_condition_chart.tsx index 1e326e3fa5f6de..a8710004876ffa 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/rule_condition_chart.tsx @@ -26,10 +26,12 @@ import { i18n } from '@kbn/i18n'; import { TimeRange } from '@kbn/es-query'; import { EventAnnotationConfig } from '@kbn/event-annotation-common'; import { COMPARATORS } from '@kbn/alerting-comparators'; -import { EventsAsUnit } from '../../../../../common/constants'; -import { CustomThresholdSearchSourceFields } from '../../../../../common/custom_threshold_rule/types'; -import { useKibana } from '../../../../utils/kibana_react'; -import { MetricExpression } from '../../types'; +import { SerializedSearchSourceFields } from '@kbn/data-plugin/common'; +import { TimeUnitChar } from '../../../common'; +import { LEGACY_COMPARATORS } from '../../../common/utils/convert_legacy_outside_comparator'; +import { EventsAsUnit } from '../../../common/constants'; +import { Aggregators } from '../../../common/custom_threshold_rule/types'; +import { useKibana } from '../../utils/kibana_react'; import { AggMap, PainlessTinyMathParser } from './painless_tinymath_parser'; import { lensFieldFormatter, @@ -38,15 +40,38 @@ import { isRate, LensFieldFormat, } from './helpers'; - interface ChartOptions { seriesType?: SeriesType; interval?: string; } +interface GenericSearchSourceFields extends SerializedSearchSourceFields { + query?: Query; + filter?: Array>; +} + +export type GenericAggType = Aggregators | 'custom'; + +export interface GenericMetric { + aggType: GenericAggType; + name: string; + field?: string; + filter?: string; +} + +export interface RuleConditionChartExpressions { + metrics: GenericMetric[]; + threshold: number[]; + comparator: COMPARATORS | LEGACY_COMPARATORS; + warningThreshold?: number[]; + warningComparator?: COMPARATORS | LEGACY_COMPARATORS; + timeSize?: number; + timeUnit?: TimeUnitChar; + equation?: string; +} interface RuleConditionChartProps { - metricExpression: MetricExpression; - searchConfiguration: CustomThresholdSearchSourceFields; + metricExpression: RuleConditionChartExpressions; + searchConfiguration: GenericSearchSourceFields; dataView?: DataView; groupBy?: string | string[]; error?: IErrorObject; @@ -76,11 +101,22 @@ export function RuleConditionChart({ services: { lens }, } = useKibana(); const { euiTheme } = useEuiTheme(); - const { metrics, timeSize, timeUnit, threshold, comparator, equation } = metricExpression; + const { + metrics, + timeSize, + timeUnit, + threshold, + comparator, + equation, + warningComparator, + warningThreshold, + } = metricExpression; const [attributes, setAttributes] = useState(); const [aggMap, setAggMap] = useState(); const [formula, setFormula] = useState(''); const [thresholdReferenceLine, setThresholdReferenceLine] = useState(); + const [warningThresholdReferenceLine, setWarningThresholdReferenceLine] = + useState(); const [alertAnnotation, setAlertAnnotation] = useState(); const [chartLoading, setChartLoading] = useState(false); const filters = [...(searchConfiguration.filter || []), ...additionalFilters]; @@ -98,13 +134,13 @@ export function RuleConditionChart({ const paragraphElements = errorDiv.querySelectorAll('p'); if (!paragraphElements || paragraphElements.length < 2) return; paragraphElements[0].innerText = i18n.translate( - 'xpack.observability.customThreshold.rule..charts.error_equation.title', + 'xpack.observability.ruleCondition.chart.error_equation.title', { defaultMessage: 'An error occurred while rendering the chart', } ); paragraphElements[1].innerText = i18n.translate( - 'xpack.observability.customThreshold.rule..charts.error_equation.description', + 'xpack.observability.ruleCondition.chart.error_equation.description', { defaultMessage: 'Check the rule equation.', } @@ -113,6 +149,77 @@ export function RuleConditionChart({ }); }, [chartLoading, attributes]); + // Build the warning threshold reference line + useEffect(() => { + if (!warningThreshold) { + if (warningThresholdReferenceLine?.length) { + setWarningThresholdReferenceLine([]); + } + return; + } + const refLayers = []; + if ( + warningComparator === COMPARATORS.NOT_BETWEEN || + (warningComparator === COMPARATORS.BETWEEN && warningThreshold.length === 2) + ) { + const refLineStart = new XYReferenceLinesLayer({ + data: [ + { + value: (warningThreshold[0] || 0).toString(), + color: euiTheme.colors.warning, + fill: warningComparator === COMPARATORS.NOT_BETWEEN ? 'below' : 'none', + }, + ], + }); + const refLineEnd = new XYReferenceLinesLayer({ + data: [ + { + value: (warningThreshold[1] || 0).toString(), + color: euiTheme.colors.warning, + fill: warningComparator === COMPARATORS.NOT_BETWEEN ? 'above' : 'none', + }, + ], + }); + + refLayers.push(refLineStart, refLineEnd); + } else { + let fill: FillStyle = 'above'; + if ( + warningComparator === COMPARATORS.LESS_THAN || + warningComparator === COMPARATORS.LESS_THAN_OR_EQUALS + ) { + fill = 'below'; + } + const warningThresholdRefLine = new XYReferenceLinesLayer({ + data: [ + { + value: (warningThreshold[0] || 0).toString(), + color: euiTheme.colors.warning, + fill, + }, + ], + }); + // A transparent line to add extra buffer at the top of threshold + const bufferRefLine = new XYReferenceLinesLayer({ + data: [ + { + value: getBufferThreshold(warningThreshold[0]), + color: 'transparent', + fill, + }, + ], + }); + refLayers.push(warningThresholdRefLine, bufferRefLine); + } + setWarningThresholdReferenceLine(refLayers); + }, [ + warningThreshold, + warningComparator, + euiTheme.colors.warning, + metrics, + warningThresholdReferenceLine?.length, + ]); + // Build the threshold reference line useEffect(() => { if (!threshold) return; @@ -225,7 +332,7 @@ export function RuleConditionChart({ const baseLayer = { type: 'formula', value: formula, - label: 'Custom Threshold', + label: formula, groupBy, format: { id: formatId, @@ -272,6 +379,9 @@ export function RuleConditionChart({ const layers: Array = [ xyDataLayer, ]; + if (warningThresholdReferenceLine) { + layers.push(...warningThresholdReferenceLine); + } if (thresholdReferenceLine) { layers.push(...thresholdReferenceLine); } @@ -311,13 +421,14 @@ export function RuleConditionChart({ timeSize, timeUnit, seriesType, + warningThresholdReferenceLine, ]); if ( !dataView || !attributes || error?.equation || - Object.keys(error?.metrics || {}).length !== 0 || + Object.keys(error?.metrics || error?.metric || {}).length !== 0 || !timeSize || !timeRange ) { @@ -329,7 +440,7 @@ export function RuleConditionChart({ data-test-subj="thresholdRuleNoChartData" body={ ); } - return (