diff --git a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/constants.ts b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/constants.ts index 497b779d24e7d..806a5d1dd1c28 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/constants.ts +++ b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/constants.ts @@ -4,5 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + +import { SettingsSpec } from '@elastic/charts'; + export const DEFAULT_DATE_FORMAT = 'HH:mm:ss'; export const CHART_ANNOTATION_RED_COLOR = '#BD271E'; +export const CHART_SETTINGS: Partial = { + showLegend: false, +}; diff --git a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/failed_transaction_chart.tsx b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/failed_transaction_chart.tsx index 02273f0f43141..f5031282ad7ad 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/failed_transaction_chart.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/failed_transaction_chart.tsx @@ -24,7 +24,7 @@ import { UI_SETTINGS } from '@kbn/data-plugin/public'; import { Theme } from '@elastic/charts'; import { AlertActiveTimeRangeAnnotation, AlertAnnotation } from '@kbn/observability-alert-details'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { DEFAULT_DATE_FORMAT } from './constants'; +import { CHART_SETTINGS, DEFAULT_DATE_FORMAT } from './constants'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { ChartType } from '../../../shared/charts/helper/get_timeseries_color'; import * as get_timeseries_color from '../../../shared/charts/helper/get_timeseries_color'; @@ -226,6 +226,7 @@ function FailedTransactionChart({ comparisonEnabled={false} customTheme={comparisonChartTheme} timeZone={timeZone} + settings={CHART_SETTINGS} /> diff --git a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/index.tsx index b9a7f4e8d8254..ada563851bdcc 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/index.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/index.tsx @@ -5,9 +5,15 @@ * 2.0. */ +import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { COMPARATORS } from '@kbn/alerting-comparators'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { formatAlertEvaluationValue } from '@kbn/observability-plugin/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { formatAlertEvaluationValue, Threshold } from '@kbn/observability-plugin/public'; +import { useChartThemes } from '@kbn/observability-shared-plugin/public'; +import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util'; import { ALERT_END, ALERT_EVALUATION_THRESHOLD, @@ -15,9 +21,6 @@ import { ALERT_RULE_TYPE_ID, ALERT_START, } from '@kbn/rule-data-utils'; -import React, { useEffect } from 'react'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util'; import { EuiCallOut } from '@elastic/eui'; import { CoreStart } from '@kbn/core/public'; import { @@ -36,12 +39,7 @@ import ThroughputChart from './throughput_chart'; import { AlertDetailsAppSectionProps } from './types'; import { createCallApmApi } from '../../../../services/rest/create_call_apm_api'; -export function AlertDetailsAppSection({ - rule, - alert, - timeZone, - setAlertSummaryFields, -}: AlertDetailsAppSectionProps) { +export function AlertDetailsAppSection({ rule, alert, timeZone }: AlertDetailsAppSectionProps) { const { services } = useKibana(); createCallApmApi(services as CoreStart); @@ -54,42 +52,25 @@ export function AlertDetailsAppSection({ const transactionName = alert.fields[TRANSACTION_NAME]; const transactionType = alert.fields[TRANSACTION_TYPE]; - useEffect(() => { - const alertSummaryFields = [ - { - label: ( - - ), - value: formatAlertEvaluationValue(alertRuleTypeId, alertEvaluationValue), - }, - { - label: ( - - ), - value: formatAlertEvaluationValue(alertRuleTypeId, alertEvaluationThreshold), - }, - ]; - setAlertSummaryFields(alertSummaryFields); - }, [ - alertRuleTypeId, - alertEvaluationValue, - alertEvaluationThreshold, - environment, - serviceName, - transactionName, - setAlertSummaryFields, - ]); - const params = rule.params; const latencyAggregationType = getAggsTypeFromRule(params.aggregationType); const timeRange = getPaddedAlertTimeRange(alert.fields[ALERT_START]!, alert.fields[ALERT_END]); const comparisonChartTheme = getComparisonChartTheme(); + const chartThemes = useChartThemes(); + const thresholdComponent = + alertEvaluationValue && alertEvaluationThreshold ? ( + String(formatAlertEvaluationValue(alertRuleTypeId, d))} + title={i18n.translate('xpack.apm.alertDetails.thresholdTitle', { + defaultMessage: 'Threshold breached', + })} + comparator={COMPARATORS.GREATER_THAN} + /> + ) : undefined; const { from, to } = timeRange; if (!from || !to) { @@ -138,6 +119,7 @@ export function AlertDetailsAppSection({ latencyAggregationType={latencyAggregationType} comparisonEnabled={false} offset={''} + threshold={thresholdComponent} /> diff --git a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/latency_chart.tsx b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/latency_chart.tsx index 82fa091fa6617..74468c5152ec5 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/latency_chart.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/latency_chart.tsx @@ -7,7 +7,7 @@ import { Theme } from '@elastic/charts'; import { RecursivePartial } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import React, { useMemo, ReactElement } from 'react'; import { EuiFlexItem, EuiPanel, EuiFlexGroup, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { BoolQuery } from '@kbn/es-query'; @@ -38,7 +38,7 @@ import { LatencyAggregationType } from '../../../../../common/latency_aggregatio import { isLatencyThresholdRuleType } from './helpers'; import { ApmDocumentType } from '../../../../../common/document_type'; import { usePreferredDataSourceAndBucketSize } from '../../../../hooks/use_preferred_data_source_and_bucket_size'; -import { DEFAULT_DATE_FORMAT } from './constants'; +import { CHART_SETTINGS, DEFAULT_DATE_FORMAT } from './constants'; import { TransactionTypeSelect } from './transaction_type_select'; import { ViewInAPMButton } from './view_in_apm_button'; @@ -61,6 +61,7 @@ function LatencyChart({ customAlertEvaluationThreshold, kuery = '', filters, + threshold, }: { alert: TopAlert; transactionType: string; @@ -78,6 +79,7 @@ function LatencyChart({ offset: string; timeZone: string; customAlertEvaluationThreshold?: number; + threshold?: ReactElement; kuery?: string; filters?: BoolQuery; }) { @@ -245,18 +247,28 @@ function LatencyChart({ - + + {!!threshold && ( + + {threshold} + + )} + + + + ); diff --git a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/throughput_chart.tsx b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/throughput_chart.tsx index b9eb0a4c729d0..f70de0861bb28 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/throughput_chart.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/throughput_chart.tsx @@ -17,6 +17,7 @@ import { EuiIconTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { CHART_SETTINGS } from './constants'; import { ChartType, getTimeSeriesColor } from '../../../shared/charts/helper/get_timeseries_color'; import { useFetcher } from '../../../../hooks/use_fetcher'; @@ -190,6 +191,7 @@ function ThroughputChart({ timeseries={timeseriesThroughput} yLabelFormat={asExactTransactionRate} timeZone={timeZone} + settings={CHART_SETTINGS} /> diff --git a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/types.ts b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/types.ts index a5c692de0584d..f1a05a285d16d 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/types.ts +++ b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/types.ts @@ -6,7 +6,7 @@ */ import type { Rule } from '@kbn/alerting-plugin/common'; -import type { TopAlert, AlertSummaryField } from '@kbn/observability-plugin/public'; +import type { TopAlert } from '@kbn/observability-plugin/public'; import type { TIME_UNITS } from '@kbn/triggers-actions-ui-plugin/public'; import type { SERVICE_NAME, @@ -28,5 +28,4 @@ export interface AlertDetailsAppSectionProps { [SERVICE_ENVIRONMENT]: string; }>; timeZone: string; - setAlertSummaryFields: React.Dispatch>; } diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/charts/timeseries_chart.tsx b/x-pack/plugins/observability_solution/apm/public/components/shared/charts/timeseries_chart.tsx index c3654ea41f251..7b90aeb3ee03c 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/shared/charts/timeseries_chart.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/shared/charts/timeseries_chart.tsx @@ -23,6 +23,7 @@ import { XYBrushEvent, XYChartSeriesIdentifier, Tooltip, + SettingsSpec, } from '@elastic/charts'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -46,11 +47,13 @@ const END_ZONE_LABEL = i18n.translate('xpack.apm.timeseries.endzone', { defaultMessage: 'The selected time range does not include this entire bucket. It might contain partial data.', }); + interface TimeseriesChartProps extends TimeseriesChartWithContextProps { comparisonEnabled: boolean; offset?: string; timeZone: string; annotations?: Array>; + settings?: Partial; } export function TimeseriesChart({ id, @@ -68,6 +71,7 @@ export function TimeseriesChart({ offset, timeZone, annotations, + settings, }: TimeseriesChartProps) { const history = useHistory(); const { chartRef, updatePointerEvent } = useChartPointerEventContext(); @@ -186,6 +190,7 @@ export function TimeseriesChart({ } }} locale={i18n.getLocale()} + {...settings} /> ; alert: TopAlert>; - setAlertSummaryFields: React.Dispatch>; } 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 5f8b99629eeb8..822173c5144df 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 @@ -69,7 +69,6 @@ jest.mock('../../../hooks/use_kibana', () => ({ describe('AlertDetailsAppSection', () => { const queryClient = new QueryClient(); - const mockedSetAlertSummaryFields = jest.fn(); const renderComponent = () => { return render( @@ -77,7 +76,6 @@ describe('AlertDetailsAppSection', () => { 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 78d908d85ad8c..8f3e22c5b8a84 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 @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import chroma from 'chroma-js'; -import { AlertSummaryField, RuleConditionChart, TopAlert } from '@kbn/observability-plugin/public'; +import { RuleConditionChart, TopAlert } from '@kbn/observability-plugin/public'; import { ALERT_END, ALERT_START, ALERT_EVALUATION_VALUES, ALERT_GROUP } from '@kbn/rule-data-utils'; import { Rule, RuleTypeParams } from '@kbn/alerting-plugin/common'; import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util'; @@ -57,10 +57,9 @@ export type MetricThresholdAlert = TopAlert; interface AppSectionProps { alert: MetricThresholdAlert; rule: MetricThresholdRule; - setAlertSummaryFields: React.Dispatch>; } -export function AlertDetailsAppSection({ alert, rule, setAlertSummaryFields }: AppSectionProps) { +export function AlertDetailsAppSection({ alert, rule }: AppSectionProps) { const { charts } = useKibanaContextForPlugin().services; const { euiTheme } = useEuiTheme(); const groups = alert.fields[ALERT_GROUP]; diff --git a/x-pack/plugins/observability_solution/observability/public/components/alert_sources/get_sources.ts b/x-pack/plugins/observability_solution/observability/public/components/alert_sources/get_sources.ts index 3832e744a31e6..836967ea88aa4 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alert_sources/get_sources.ts +++ b/x-pack/plugins/observability_solution/observability/public/components/alert_sources/get_sources.ts @@ -8,27 +8,33 @@ import { ALERT_GROUP_FIELD, ALERT_GROUP_VALUE, ALERT_GROUP } from '@kbn/rule-data-utils'; import { TopAlert } from '../../typings/alerts'; import { apmSources, infraSources } from './get_alert_source_links'; +import { Group } from '../../../common/typings'; interface AlertFields { [key: string]: any; } -export const getSources = (alert: TopAlert) => { +const isGroup = (item: Group | undefined): item is Group => { + return !!item; +}; + +export const getSources = (alert: TopAlert): Group[] => { // when `kibana.alert.group` is not flattened (for alert detail pages) - if (alert.fields[ALERT_GROUP]) return alert.fields[ALERT_GROUP]; + if (alert.fields[ALERT_GROUP]) return alert.fields[ALERT_GROUP] as Group[]; // when `kibana.alert.group` is flattened (for alert flyout) const groupsFromGroupFields = alert.fields[ALERT_GROUP_FIELD]?.map((field, index) => { const values = alert.fields[ALERT_GROUP_VALUE]; if (values?.length && values[index]) { - return { field, value: values[index] }; + const group: Group = { field, value: values[index] }; + return group; } - }); + }).filter(isGroup); if (groupsFromGroupFields?.length) return groupsFromGroupFields; // Not all rules has group.fields, in that case we search in the alert fields. - const matchedSources: Array<{ field: string; value: any }> = []; + const matchedSources: Group[] = []; const ALL_SOURCES = [...infraSources, ...apmSources]; const alertFields = alert.fields as AlertFields; ALL_SOURCES.forEach((source: string) => { diff --git a/x-pack/plugins/observability_solution/observability/public/components/alert_sources/groups.tsx b/x-pack/plugins/observability_solution/observability/public/components/alert_sources/groups.tsx index 5a14f39a97811..17c596b146d1a 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alert_sources/groups.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/alert_sources/groups.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiLink } from '@elastic/eui'; +import { EuiLink, EuiText } from '@elastic/eui'; import React, { useEffect, useState } from 'react'; import { SERVICE_NAME } from '@kbn/observability-shared-plugin/common'; import { useKibana } from '../../utils/kibana_react'; @@ -54,7 +54,7 @@ export function Groups({ groups, timeRange }: { groups: Group[]; timeRange: Time {groups && groups.map((group) => { return ( - + {group.field}:{' '} {sourceLinks[group.field] ? ( @@ -63,8 +63,7 @@ export function Groups({ groups, timeRange }: { groups: Group[]; timeRange: Time ) : ( {group.value} )} -
-
+ ); })} 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 d16df6b6120df..de74fe2ec14b9 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 @@ -103,7 +103,7 @@ describe('AlertDetailsAppSection', () => { const result = renderComponent(); expect((await result.findByTestId('thresholdAlertOverviewSection')).children.length).toBe(6); - expect(result.getByTestId('thresholdRule-2000-2500')).toBeTruthy(); + expect(result.getByTestId('threshold-2000-2500')).toBeTruthy(); }); it('should render annotations', async () => { 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 a7ec41f4ab660..7885301650ecf 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 @@ -38,7 +38,7 @@ import { useLicense } from '../../../../hooks/use_license'; import { useKibana } from '../../../../utils/kibana_react'; import { metricValueFormatter } from '../../../../../common/custom_threshold_rule/metric_value_formatter'; import { AlertParams } from '../../types'; -import { Threshold } from '../custom_threshold'; +import { Threshold } from '../threshold'; import { CustomThresholdRule, CustomThresholdAlert } from '../types'; import { LogRateAnalysis } from './log_rate_analysis'; import { RuleConditionChart } from '../../../rule_condition_chart/rule_condition_chart'; diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.stories.tsx b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.stories.tsx index f471333f53661..8eeca2e107b5f 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.stories.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.stories.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { ComponentMeta } from '@storybook/react'; import { LIGHT_THEME } from '@elastic/charts'; import { COMPARATORS } from '@kbn/alerting-comparators'; -import { Props, Threshold as Component } from './custom_threshold'; +import { Props, Threshold as Component } from './threshold'; export default { component: Component, @@ -35,7 +35,7 @@ const defaultProps: Props = { threshold: [90], title: 'Threshold breached', value: 93, - valueFormatter: (d) => `${d}%`, + valueFormatter: (d: number) => `${d}%`, }; export const Default = { diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.test.tsx b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/threshold.test.tsx similarity index 85% rename from x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.test.tsx rename to x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/threshold.test.tsx index e0c83d31ddb4c..9e592f2336dc1 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.test.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/threshold.test.tsx @@ -7,10 +7,10 @@ import { LIGHT_THEME } from '@elastic/charts'; -import { render } from '@testing-library/react'; -import { Props, Threshold } from './custom_threshold'; import React from 'react'; +import { render } from '@testing-library/react'; import { COMPARATORS } from '@kbn/alerting-comparators'; +import { Props, Threshold } from './threshold'; describe('Threshold', () => { const renderComponent = (props: Partial = {}) => { @@ -38,7 +38,7 @@ describe('Threshold', () => { it('shows component', () => { const component = renderComponent(); - expect(component.queryByTestId('thresholdRule-90-93')).toBeTruthy(); + expect(component.queryByTestId('threshold-90-93')).toBeTruthy(); }); it('shows component for between', () => { @@ -46,6 +46,6 @@ describe('Threshold', () => { comparator: COMPARATORS.BETWEEN, threshold: [90, 95], }); - expect(component.queryByTestId('thresholdRule-90-95-93')).toBeTruthy(); + expect(component.queryByTestId('threshold-90-95-93')).toBeTruthy(); }); }); diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.tsx b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/threshold.tsx similarity index 88% rename from x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.tsx rename to x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/threshold.tsx index a82c369c57737..347730fe4dea6 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/threshold.tsx @@ -6,14 +6,14 @@ */ import React from 'react'; -import { Chart, Metric, Settings } from '@elastic/charts'; +import { Chart, Metric, Settings, ValueFormatter } from '@elastic/charts'; import { EuiIcon, EuiPanel, useEuiBackgroundColor } from '@elastic/eui'; import type { PartialTheme, Theme } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import { COMPARATORS } from '@kbn/alerting-comparators'; export interface ChartProps { - theme?: PartialTheme; + theme?: PartialTheme[]; baseTheme: Theme; } @@ -24,7 +24,7 @@ export interface Props { threshold: number[]; title: string; value: number; - valueFormatter: (d: number) => string; + valueFormatter?: ValueFormatter; } export function Threshold({ @@ -34,7 +34,7 @@ export function Threshold({ threshold, title, value, - valueFormatter, + valueFormatter = (d) => String(d), }: Props) { const color = useEuiBackgroundColor('danger'); @@ -42,13 +42,14 @@ export function Threshold({ diff --git a/x-pack/plugins/observability_solution/observability/public/index.ts b/x-pack/plugins/observability_solution/observability/public/index.ts index 512e8e7e75bf9..58c3aa4cadd66 100644 --- a/x-pack/plugins/observability_solution/observability/public/index.ts +++ b/x-pack/plugins/observability_solution/observability/public/index.ts @@ -63,9 +63,9 @@ export const LazyAlertsFlyout = lazy(() => import('./components/alerts_flyout/al export * from './typings'; import { TopAlert } from './typings/alerts'; -import { AlertSummary } from './pages/alert_details/components'; -import type { AlertSummaryField } from './pages/alert_details/components/alert_summary'; -export type { TopAlert, AlertSummary, AlertSummaryField }; +export type { TopAlert }; +import type { AlertDetailsAppSectionProps } from './pages/alert_details/types'; +export type { AlertDetailsAppSectionProps }; export { observabilityFeatureId, observabilityAppId } from '../common'; @@ -102,3 +102,4 @@ export { useAnnotations } from './components/annotations/use_annotations'; export { RuleConditionChart } from './components/rule_condition_chart'; export { getGroupFilters } from '../common/custom_threshold_rule/helpers/get_group'; export type { GenericAggType } from './components/rule_condition_chart/rule_condition_chart'; +export { Threshold } from './components/custom_threshold/components/threshold'; diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.test.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.test.tsx index 877b0b965d1ce..be62ae6377bc6 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.test.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.test.tsx @@ -9,7 +9,7 @@ import { casesPluginMock } from '@kbn/cases-plugin/public/mocks'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import * as useUiSettingHook from '@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting'; import { observabilityAIAssistantPluginMock } from '@kbn/observability-ai-assistant-plugin/public/mock'; -import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public'; +import { useBreadcrumbs, TagsList } from '@kbn/observability-shared-plugin/public'; import { RuleTypeModel, ValidationResult } from '@kbn/triggers-actions-ui-plugin/public'; import { ruleTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/rule_type_registry.mock'; import { waitFor } from '@testing-library/react'; @@ -88,6 +88,7 @@ const useParamsMock = useParams as jest.Mock; const useLocationMock = useLocation as jest.Mock; const useHistoryMock = useHistory as jest.Mock; const useBreadcrumbsMock = useBreadcrumbs as jest.Mock; +const TagsListMock = TagsList as jest.Mock; const chance = new Chance(); @@ -114,6 +115,7 @@ describe('Alert details', () => { useLocationMock.mockReturnValue({ pathname: '/alerts/uuid', search: '', state: '', hash: '' }); useHistoryMock.mockReturnValue({ replace: jest.fn() }); useBreadcrumbsMock.mockReturnValue([]); + TagsListMock.mockReturnValue(
); ruleTypeRegistry.list.mockReturnValue([ruleType]); ruleTypeRegistry.get.mockReturnValue(ruleType); ruleTypeRegistry.has.mockReturnValue(true); diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.tsx index e7533c226df58..fbe1858858d05 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.tsx @@ -16,6 +16,7 @@ import { EuiLoadingSpinner, EuiTabbedContentTab, useEuiTheme, + EuiFlexGroup, } from '@elastic/eui'; import { AlertStatus, @@ -31,15 +32,16 @@ import dedent from 'dedent'; import { AlertFieldsTable } from '@kbn/alerts-ui-shared'; import { css } from '@emotion/react'; import { omit } from 'lodash'; +import { AlertDetailsSource } from './types'; +import { SourceBar } from './components'; +import { StatusBar } from './components/status_bar'; import { observabilityFeatureId } from '../../../common'; import { RelatedAlerts } from './components/related_alerts'; import { useKibana } from '../../utils/kibana_react'; import { useFetchRule } from '../../hooks/use_fetch_rule'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { AlertData, useFetchAlertDetail } from '../../hooks/use_fetch_alert_detail'; -import { PageTitleContent } from './components/page_title_content'; import { HeaderActions } from './components/header_actions'; -import { AlertSummary, AlertSummaryField } from './components/alert_summary'; import { CenterJustifiedSpinner } from '../../components/center_justified_spinner'; import { getTimeZone } from '../../utils/get_time_zone'; import { isAlertDetailsEnabledPerApp } from '../../utils/is_alert_details_enabled'; @@ -103,10 +105,10 @@ export function AlertDetails() { const { rule } = useFetchRule({ ruleId, }); - const [summaryFields, setSummaryFields] = useState(); const [alertStatus, setAlertStatus] = useState(); const { euiTheme } = useEuiTheme(); + const [sources, setSources] = useState(); const [relatedAlertsKuery, setRelatedAlertsKuery] = useState(); const [activeTabId, setActiveTabId] = useState(() => { const searchParams = new URLSearchParams(search); @@ -212,17 +214,19 @@ export function AlertDetails() { */ isAlertDetailsEnabledPerApp(alertDetail.formatted, config) ? ( <> - - - - + + + + + + {rule && alertDetail.formatted && ( <> @@ -290,13 +294,6 @@ export function AlertDetails() { ) : ( ), - children: ( - - ), rightSideItems: [ , ], bottomBorder: false, + 'data-test-subj': rule?.ruleTypeId || 'alertDetailsPageTitle', }} pageSectionProps={{ paddingSize: 'none', @@ -321,6 +319,8 @@ export function AlertDetails() { }} data-test-subj="alertDetails" > + + ({ - ...jest.requireActual('react-router-dom'), - useParams: jest.fn(), -})); - -jest.mock('../../../utils/kibana_react'); - -const useKibanaMock = useKibana as jest.Mock; - -const mockKibana = () => { - useKibanaMock.mockReturnValue({ - services: { - ...kibanaStartMock.startContract().services, - http: { - basePath: { - prepend: jest.fn(), - }, - }, - }, - }); -}; - -describe('Alert summary', () => { - jest - .spyOn(useUiSettingHook, 'useUiSetting') - .mockImplementation(() => 'MMM D, YYYY @ HH:mm:ss.SSS'); - - beforeEach(() => { - jest.clearAllMocks(); - mockKibana(); - }); - - it('should show alert data', async () => { - const alertSummary = render( - - ); - - const groups = alertWithGroupsAndTags.fields[ALERT_GROUP] as Group[]; - - expect(alertSummary.queryByText('Source')).toBeInTheDocument(); - expect(alertSummary.queryByText(groups[0].field, { exact: false })).toBeInTheDocument(); - expect(alertSummary.queryByText(groups[0].value)).toBeInTheDocument(); - expect(alertSummary.queryByText(groups[1].field, { exact: false })).toBeInTheDocument(); - expect(alertSummary.queryByText(groups[1].value)).toBeInTheDocument(); - expect(alertSummary.queryByText('Tags')).toBeInTheDocument(); - expect(alertSummary.queryByText(alertWithGroupsAndTags.fields[TAGS]![0])).toBeInTheDocument(); - expect(alertSummary.queryByText('Rule')).toBeInTheDocument(); - expect( - alertSummary.queryByText(alertWithGroupsAndTags.fields[ALERT_RULE_NAME]) - ).toBeInTheDocument(); - expect(alertSummary.queryByText('Actual value')).toBeInTheDocument(); - expect(alertSummary.queryByText(alertWithGroupsAndTags.fields[ALERT_EVALUATION_VALUE]!)); - expect(alertSummary.queryByText('Expected value')).toBeInTheDocument(); - expect(alertSummary.queryByText(alertWithGroupsAndTags.fields[ALERT_EVALUATION_THRESHOLD]!)); - }); -}); diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/alert_summary.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/alert_summary.tsx deleted file mode 100644 index 7738b27089a57..0000000000000 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/alert_summary.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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 React, { useEffect, useState, ReactNode } from 'react'; -import { EuiFlexItem, EuiFlexGroup, EuiText, EuiSpacer, EuiLink } from '@elastic/eui'; -import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util'; -import { - TAGS, - ALERT_START, - ALERT_END, - ALERT_RULE_NAME, - ALERT_RULE_UUID, -} from '@kbn/rule-data-utils'; -import { i18n } from '@kbn/i18n'; -import { TimeRange } from '@kbn/es-query'; -import { TopAlert } from '../../..'; -import { Groups } from '../../../components/alert_sources/groups'; -import { Tags } from '../../../components/tags'; -import { getSources } from '../../../components/alert_sources/get_sources'; -import { useKibana } from '../../../utils/kibana_react'; -import { paths } from '../../../../common/locators/paths'; - -export interface AlertSummaryField { - label: ReactNode | string; - value: ReactNode | string | number; -} -export interface AlertSummaryProps { - alert: TopAlert; - alertSummaryFields?: AlertSummaryField[]; -} - -export function AlertSummary({ alert, alertSummaryFields }: AlertSummaryProps) { - const { http } = useKibana().services; - - const [timeRange, setTimeRange] = useState({ from: 'now-15m', to: 'now' }); - - const alertStart = alert.fields[ALERT_START]; - const alertEnd = alert.fields[ALERT_END]; - const ruleName = alert.fields[ALERT_RULE_NAME]; - const ruleId = alert.fields[ALERT_RULE_UUID]; - const tags = alert.fields[TAGS]; - - const ruleLink = http.basePath.prepend(paths.observability.ruleDetails(ruleId)); - const commonFieldsAtStart = []; - const commonFieldsAtEnd = []; - const groups = getSources(alert) as Array<{ field: string; value: string }>; - - useEffect(() => { - setTimeRange(getPaddedAlertTimeRange(alertStart!, alertEnd)); - }, [alertStart, alertEnd]); - - if (groups && groups.length > 0) { - commonFieldsAtStart.push({ - label: i18n.translate('xpack.observability.alertDetails.alertSummaryField.source', { - defaultMessage: 'Source', - }), - value: ( - - ), - }); - } - - if (tags && tags.length > 0) { - commonFieldsAtEnd.push({ - label: i18n.translate('xpack.observability.alertDetails.alertSummaryField.tags', { - defaultMessage: 'Tags', - }), - value: , - }); - } - - commonFieldsAtEnd.push({ - label: i18n.translate('xpack.observability.alertDetails.alertSummaryField.rule', { - defaultMessage: 'Rule', - }), - value: ( - - {ruleName} - - ), - }); - - const alertSummary = [ - ...commonFieldsAtStart, - ...(alertSummaryFields ?? []), - ...commonFieldsAtEnd, - ]; - - return ( -
- {alertSummary && alertSummary.length > 0 && ( - <> - - {alertSummary.map((field, idx) => { - return ( - - {field.label} - {field.value} - - ); - })} - - - - )} -
- ); -} - -// eslint-disable-next-line import/no-default-export -export default AlertSummary; diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/index.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/index.tsx index 8af473faab59d..bf032ac31c127 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/index.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/index.tsx @@ -6,14 +6,14 @@ */ import React, { lazy, Suspense } from 'react'; -import type { AlertSummaryProps } from './alert_summary'; +import type { SourceBarProps } from './source_bar'; -const AlertSummaryLazy = lazy(() => import('./alert_summary')); +const SourceBarLazy = lazy(() => import('./source_bar')); -export function AlertSummary(props: AlertSummaryProps) { +export function SourceBar(props: SourceBarProps) { return ( - + ); } diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/page_title.stories.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/page_title.stories.tsx deleted file mode 100644 index 30ca4b6a108cb..0000000000000 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/page_title.stories.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 React from 'react'; -import { ComponentStory } from '@storybook/react'; -import { EuiPageTemplate } from '@elastic/eui'; - -import { PageTitleContent as Component, PageTitleContentProps } from './page_title_content'; -import { alert } from '../mock/alert'; - -export default { - component: Component, - title: 'app/AlertDetails/PageTitleContent', - alert, -}; - -const Template: ComponentStory = (props: PageTitleContentProps) => ( - -); - -const TemplateWithPageTemplate: ComponentStory = ( - props: PageTitleContentProps -) => ( - - } bottomBorder={false} /> - -); - -const defaultProps = { - alert, -}; - -export const PageTitleContent = Template.bind({}); -PageTitleContent.args = defaultProps; - -export const PageTitleUsedWithinPageTemplate = TemplateWithPageTemplate.bind({}); -PageTitleUsedWithinPageTemplate.args = { - ...defaultProps, -}; diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/page_title_content.test.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/page_title_content.test.tsx deleted file mode 100644 index d65c4f84c99dd..0000000000000 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/page_title_content.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 React from 'react'; -import { render } from '@testing-library/react'; -import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import { - AlertStatus, - ALERT_STATUS, - ALERT_STATUS_ACTIVE, - ALERT_STATUS_RECOVERED, - ALERT_STATUS_UNTRACKED, -} from '@kbn/rule-data-utils'; -import { PageTitleContent, PageTitleContentProps } from './page_title_content'; -import { alert } from '../mock/alert'; - -describe('Page Title Content', () => { - const defaultProps = { - alert, - alertStatus: ALERT_STATUS_ACTIVE as AlertStatus, - dataTestSubj: 'ruleTypeId', - }; - - const renderComp = (props: PageTitleContentProps) => { - return render( - - - - ); - }; - - it('should display an active badge when alert is active', async () => { - const { getByText } = renderComp(defaultProps); - expect(getByText('Active')).toBeTruthy(); - }); - - it('should display a recovered badge when alert is recovered', async () => { - const updatedProps = { - alert: { - ...defaultProps.alert, - fields: { - ...defaultProps.alert.fields, - [ALERT_STATUS]: ALERT_STATUS_RECOVERED, - }, - }, - alertStatus: ALERT_STATUS_RECOVERED as AlertStatus, - dataTestSubj: defaultProps.dataTestSubj, - }; - - const { getByText } = renderComp({ ...updatedProps }); - expect(getByText('Recovered')).toBeTruthy(); - }); - - it('should display an untracked badge when alert is untracked', async () => { - const updatedProps = { - alert: { - ...defaultProps.alert, - fields: { - ...defaultProps.alert.fields, - [ALERT_STATUS]: ALERT_STATUS_UNTRACKED, - }, - }, - alertStatus: ALERT_STATUS_UNTRACKED as AlertStatus, - dataTestSubj: defaultProps.dataTestSubj, - }; - - const { getByText } = renderComp({ ...updatedProps }); - expect(getByText('Untracked')).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/source_bar.test.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/source_bar.test.tsx new file mode 100644 index 0000000000000..6a68d77175fef --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/source_bar.test.tsx @@ -0,0 +1,70 @@ +/* + * 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 { EuiLink } from '@elastic/eui'; +import React from 'react'; +import { ALERT_GROUP } from '@kbn/rule-data-utils'; +import { render } from '../../../utils/test_helper'; +import { alertWithGroupsAndTags } from '../mock/alert'; +import { useKibana } from '../../../utils/kibana_react'; +import { kibanaStartMock } from '../../../utils/kibana_react.mock'; +import { Group } from '../../../../common/typings'; +import { SourceBar } from './source_bar'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(), +})); + +jest.mock('../../../utils/kibana_react'); + +const useKibanaMock = useKibana as jest.Mock; +const mockKibana = () => { + useKibanaMock.mockReturnValue({ + services: { + ...kibanaStartMock.startContract().services, + http: { + basePath: { + prepend: jest.fn(), + }, + }, + }, + }); +}; + +describe('Source bar', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockKibana(); + }); + + it('should show alert data', async () => { + const sourceBar = render(); + + const groups = alertWithGroupsAndTags.fields[ALERT_GROUP] as Group[]; + + expect(sourceBar.queryByText('Source')).toBeInTheDocument(); + expect(sourceBar.queryByText(groups[0].field, { exact: false })).toBeInTheDocument(); + expect(sourceBar.queryByText(groups[0].value)).toBeInTheDocument(); + expect(sourceBar.queryByText(groups[1].field, { exact: false })).toBeInTheDocument(); + expect(sourceBar.queryByText(groups[1].value)).toBeInTheDocument(); + }); + + it('Should show passed sources', async () => { + const sources = [ + { label: 'MyLabel', value: 'MyValue' }, + { label: 'SLO', value: }, + ]; + const sourceBar = render(); + + expect(sourceBar.queryByText('Source')).toBeInTheDocument(); + expect(sourceBar.queryByText(sources[0].label, { exact: false })).toBeInTheDocument(); + expect(sourceBar.queryByText(sources[0].value as string, { exact: false })).toBeInTheDocument(); + expect(sourceBar.queryByText(sources[1].label, { exact: false })).toBeInTheDocument(); + expect(sourceBar.queryByTestId('SourceSloLink')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/source_bar.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/source_bar.tsx new file mode 100644 index 0000000000000..be47a3f9ec3e9 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/source_bar.tsx @@ -0,0 +1,69 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n-react'; +import React, { useEffect, useState } from 'react'; +import { EuiFlexGroup, EuiTitle, EuiPanel, EuiFlexItem, EuiText } from '@elastic/eui'; +import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util'; +import { ALERT_START, ALERT_END } from '@kbn/rule-data-utils'; +import { TimeRange } from '@kbn/es-query'; +import { AlertDetailsSource } from '../types'; +import { TopAlert } from '../../..'; +import { Groups } from '../../../components/alert_sources/groups'; +import { getSources } from '../../../components/alert_sources/get_sources'; + +export interface SourceBarProps { + alert: TopAlert; + sources?: AlertDetailsSource[]; +} + +export function SourceBar({ alert, sources = [] }: SourceBarProps) { + const [timeRange, setTimeRange] = useState({ from: 'now-15m', to: 'now' }); + + const alertStart = alert.fields[ALERT_START]; + const alertEnd = alert.fields[ALERT_END]; + const groups = getSources(alert); + + useEffect(() => { + setTimeRange(getPaddedAlertTimeRange(alertStart!, alertEnd)); + }, [alertStart, alertEnd]); + + return ( + <> + {groups && groups.length > 0 && ( + + + +
+ +
+
+ + {sources.map((field, idx) => { + return ( + + + {field.label}: {field.value} + + + ); + })} +
+
+ )} + + ); +} + +// eslint-disable-next-line import/no-default-export +export default SourceBar; diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/status_bar.stories.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/status_bar.stories.tsx new file mode 100644 index 0000000000000..0c0eada41e1e7 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/status_bar.stories.tsx @@ -0,0 +1,43 @@ +/* + * 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 React from 'react'; +import { ComponentStory } from '@storybook/react'; +import { I18nProvider } from '@kbn/i18n-react'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; + +import { StatusBar as Component, StatusBarProps } from './status_bar'; +import { alert } from '../mock/alert'; + +export default { + component: Component, + title: 'app/AlertDetails/StatusBar', + alert, +}; + +const Template: ComponentStory = (props: StatusBarProps) => ( + + + + + +); + +const defaultProps = { + alert, +}; + +export const StatusBar = Template.bind({}); +StatusBar.args = defaultProps; + +const services = { + http: { + basePath: { + prepend: () => 'http://test', + }, + }, +}; diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/status_bar.test.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/status_bar.test.tsx new file mode 100644 index 0000000000000..fb92597afdc00 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/status_bar.test.tsx @@ -0,0 +1,91 @@ +/* + * 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 React from 'react'; +import { + ALERT_RULE_NAME, + ALERT_STATUS, + ALERT_STATUS_RECOVERED, + ALERT_STATUS_UNTRACKED, + AlertStatus, +} from '@kbn/rule-data-utils'; +import { render } from '../../../utils/test_helper'; +import { alertWithGroupsAndTags } from '../mock/alert'; +import { useKibana } from '../../../utils/kibana_react'; +import { kibanaStartMock } from '../../../utils/kibana_react.mock'; +import { StatusBar, StatusBarProps } from './status_bar'; + +jest.mock('../../../utils/kibana_react'); + +const useKibanaMock = useKibana as jest.Mock; +const mockKibana = () => { + useKibanaMock.mockReturnValue({ + services: { + ...kibanaStartMock.startContract().services, + http: { + basePath: { + prepend: jest.fn(), + }, + }, + }, + }); +}; + +describe('Source bar', () => { + const renderComponent = (props: StatusBarProps) => { + return render(); + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockKibana(); + }); + + it('should show alert data', async () => { + const statusBar = renderComponent({ + alert: alertWithGroupsAndTags, + alertStatus: alertWithGroupsAndTags.fields[ALERT_STATUS] as AlertStatus, + }); + + expect( + statusBar.queryByText(alertWithGroupsAndTags.fields[ALERT_RULE_NAME]) + ).toBeInTheDocument(); + expect(statusBar.getByText('Active')).toBeTruthy(); + }); + + it('should display a recovered badge when alert is recovered', async () => { + const updatedProps = { + alert: { + ...alertWithGroupsAndTags, + fields: { + ...alertWithGroupsAndTags.fields, + [ALERT_STATUS]: ALERT_STATUS_RECOVERED, + }, + }, + alertStatus: ALERT_STATUS_RECOVERED as AlertStatus, + }; + + const { getByText } = renderComponent({ ...updatedProps }); + expect(getByText('Recovered')).toBeTruthy(); + }); + + it('should display an untracked badge when alert is untracked', async () => { + const updatedProps = { + alert: { + ...alertWithGroupsAndTags, + fields: { + ...alertWithGroupsAndTags.fields, + [ALERT_STATUS]: ALERT_STATUS_UNTRACKED, + }, + }, + alertStatus: ALERT_STATUS_UNTRACKED as AlertStatus, + }; + + const { getByText } = renderComponent({ ...updatedProps }); + expect(getByText('Untracked')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/page_title_content.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/status_bar.tsx similarity index 55% rename from x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/page_title_content.tsx rename to x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/status_bar.tsx index 11fc6d0a476bb..eee2db8e98d52 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/page_title_content.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/status_bar.tsx @@ -7,30 +7,51 @@ import React from 'react'; import moment from 'moment'; -import { EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, useEuiTheme, EuiToolTip } from '@elastic/eui'; import { AlertLifecycleStatusBadge } from '@kbn/alerts-ui-shared'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { AlertStatus, ALERT_DURATION, ALERT_FLAPPING, TIMESTAMP } from '@kbn/rule-data-utils'; +import { + AlertStatus, + ALERT_DURATION, + ALERT_FLAPPING, + TIMESTAMP, + TAGS, + ALERT_RULE_NAME, + ALERT_RULE_UUID, +} from '@kbn/rule-data-utils'; import { css } from '@emotion/react'; +import { TagsList } from '@kbn/observability-shared-plugin/public'; +import { useKibana } from '../../../utils/kibana_react'; +import { paths } from '../../../../common/locators/paths'; import { asDuration } from '../../../../common/utils/formatters'; import { TopAlert } from '../../../typings/alerts'; -export interface PageTitleContentProps { +export interface StatusBarProps { alert: TopAlert | null; alertStatus?: AlertStatus; - dataTestSubj: string; } -export function PageTitleContent({ alert, alertStatus, dataTestSubj }: PageTitleContentProps) { +export function StatusBar({ alert, alertStatus }: StatusBarProps) { + const { http } = useKibana().services; const { euiTheme } = useEuiTheme(); + const tags = alert?.fields[TAGS]; + const ruleName = alert?.fields[ALERT_RULE_NAME]; + const ruleId = alert?.fields[ALERT_RULE_UUID]; + const ruleLink = ruleId ? http.basePath.prepend(paths.observability.ruleDetails(ruleId)) : ''; if (!alert) { return null; } return ( - + {alertStatus && ( + + + + + + + :  + + + + + {ruleName} + + + + + + - + - + >; +} diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/alert_details_app_section.tsx b/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/alert_details_app_section.tsx index be83b74a0cc19..44ab29e77e1bd 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/alert_details_app_section.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/alert_details_app_section.tsx @@ -4,28 +4,24 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + +import React, { useEffect } from 'react'; import { EuiFlexGroup, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AlertSummaryField } from '@kbn/observability-plugin/public'; -import React, { useEffect } from 'react'; -import { useFetchSloDetails } from '../../../../hooks/use_fetch_slo_details'; +import { AlertDetailsAppSectionProps } from '@kbn/observability-plugin/public'; import { useKibana } from '../../../../utils/kibana_react'; +import { useFetchSloDetails } from '../../../../hooks/use_fetch_slo_details'; import { CustomAlertDetailsPanel } from './components/custom_panels/custom_panels'; import { ErrorRatePanel } from './components/error_rate/error_rate_panel'; import { BurnRateAlert, BurnRateRule } from './types'; -interface AppSectionProps { +interface AppSectionProps extends AlertDetailsAppSectionProps { alert: BurnRateAlert; rule: BurnRateRule; - setAlertSummaryFields: React.Dispatch>; } // eslint-disable-next-line import/no-default-export -export default function AlertDetailsAppSection({ - alert, - rule, - setAlertSummaryFields, -}: AppSectionProps) { +export default function AlertDetailsAppSection({ alert, rule, setSources }: AppSectionProps) { const { services: { http: { basePath }, @@ -51,8 +47,8 @@ export default function AlertDetailsAppSection({ }, ]; - setAlertSummaryFields(fields); - }, [alertLink, rule, setAlertSummaryFields, basePath, slo, instanceId]); + setSources(fields); + }, [alertLink, rule, setSources, basePath, slo, instanceId]); return ( diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index e87b02f5ba4bb..b4b03c0b584f6 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -10774,8 +10774,6 @@ "xpack.apm.onboarding.shared_clients.configure.commands.serviceEnvironmentHint": "Le nom de l'environnement dans lequel ce service est déployé, par exemple \"production\" ou \"test\". Les environnements vous permettent de facilement filtrer les données à un niveau global dans l'interface utilisateur APM. Il est important de garantir la cohérence des noms d'environnements entre les différents agents.", "xpack.apm.onboarding.shared_clients.configure.commands.serviceNameHint": "Le nom de service est le filtre principal dans l'interface utilisateur APM et est utilisé pour regrouper les erreurs et suivre les données ensemble. Caractères autorisés : a-z, A-Z, 0-9, -, _ et espace.", "xpack.apm.onboarding.specProvider.longDescription": "Le monitoring des performances applicatives (APM) collecte les indicateurs et les erreurs de performance approfondies depuis votre application. Cela vous permet de monitorer les performances de milliers d'applications en temps réel. {learnMoreLink}.", - "xpack.apm.pages.alertDetails.alertSummary.actualValue": "Valeur réelle", - "xpack.apm.pages.alertDetails.alertSummary.expectedValue": "Valeur attendue", "xpack.apm.percentOfParent": "({value} de {parentType, select, transaction { transaction } trace {trace} other {parentType inconnu} })", "xpack.apm.profiling.callout.description": "Universal Profiling fournit une visibilité sans précédent du code au milieu du comportement en cours d'exécution de toutes les applications. La fonctionnalité profile chaque ligne de code chez le ou les hôtes qui exécutent vos services, y compris votre code applicatif, le kernel et même les bibliothèque tierces.", "xpack.apm.profiling.callout.dismiss": "Rejeter", @@ -32065,9 +32063,6 @@ "xpack.observability.alertDetailContextualInsights.InsightButtonLabel": "Aidez moi à comprendre cette alerte", "xpack.observability.alertDetails.actionsButtonLabel": "Actions", "xpack.observability.alertDetails.addToCase": "Ajouter au cas", - "xpack.observability.alertDetails.alertSummaryField.rule": "Règle", - "xpack.observability.alertDetails.alertSummaryField.source": "Source", - "xpack.observability.alertDetails.alertSummaryField.tags": "Balises", "xpack.observability.alertDetails.editRule": "Modifier la règle", "xpack.observability.alertDetails.editSnoozeRule": "Répéter la règle", "xpack.observability.alertDetails.errorPromptBody": "Une erreur s'est produite lors du chargement des détails de l'alerte.", @@ -41232,7 +41227,6 @@ "xpack.slo.burnRateRule.alertDetailsAppSection.lastDurationInHours": "{duration} dernières heures", "xpack.slo.burnRateRule.alertDetailsAppSection.lastDurationInMinutes": "{duration} dernières minutes", "xpack.slo.burnRateRule.alertDetailsAppSection.lastDurationInSeconds": "{duration} dernières secondes", - "xpack.slo.burnRateRule.alertDetailsAppSection.summaryField.slo": "SLO", "xpack.slo.burnRateRuleEditor.h5.chooseASLOToMonitorLabel": "Choisir un SLO pour monitorer", "xpack.slo.burnRateRuleEditor.h5.defineMultipleBurnRateLabel": "Définir des fenêtres du taux d'avancement multiples", "xpack.slo.burnRates.fromRange.label": "{duration}h", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9e165616ad06d..33c5301dbba86 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10523,8 +10523,6 @@ "xpack.apm.onboarding.shared_clients.configure.commands.serviceEnvironmentHint": "このサービスがデプロイされている環境の名前(例:「本番」、「ステージング」)。環境では、APM UIでグローバルレベルで簡単にデータをフィルタリングできます。すべてのエージェントで環境の命名方法を統一することが重要です。", "xpack.apm.onboarding.shared_clients.configure.commands.serviceNameHint": "このサービス名はAPM UIの主フィルターであり、エラーとトレースデータをグループ化するために使用されます。使用できる文字はA-Z、0-9、-、_、スペースです。", "xpack.apm.onboarding.specProvider.longDescription": "アプリケーションパフォーマンスモニタリング(APM)は、アプリケーション内から詳細なパフォーマンスメトリックやエラーを収集します。何千ものアプリケーションのパフォーマンスをリアルタイムで監視できます。{learnMoreLink}。", - "xpack.apm.pages.alertDetails.alertSummary.actualValue": "実際の値", - "xpack.apm.pages.alertDetails.alertSummary.expectedValue": "想定された値", "xpack.apm.percentOfParent": "({value} of {parentType, select, transaction { トランザクション } trace {トレース} other {不明なparentType} })", "xpack.apm.profiling.callout.description": "ユニバーサルプロファイリングは、すべてのアプリケーションの実行時の動作に関して、かつてないほどコードを可視化します。アプリケーションコードだけでなく、カーネルやサードパーティライブラリも含め、サービスを実行するホスト上のすべてのコード行をプロファイリングします。", "xpack.apm.profiling.callout.dismiss": "閉じる", @@ -31809,9 +31807,6 @@ "xpack.observability.alertDetailContextualInsights.InsightButtonLabel": "このアラートを理解できるように支援してください", "xpack.observability.alertDetails.actionsButtonLabel": "アクション", "xpack.observability.alertDetails.addToCase": "ケースに追加", - "xpack.observability.alertDetails.alertSummaryField.rule": "ルール", - "xpack.observability.alertDetails.alertSummaryField.source": "送信元", - "xpack.observability.alertDetails.alertSummaryField.tags": "タグ", "xpack.observability.alertDetails.editRule": "ルールを編集", "xpack.observability.alertDetails.editSnoozeRule": "ルールをスヌーズ", "xpack.observability.alertDetails.errorPromptBody": "アラート詳細の読み込みエラーが発生しました。", @@ -40976,7 +40971,6 @@ "xpack.slo.burnRateRule.alertDetailsAppSection.lastDurationInHours": "過去{duration}時間", "xpack.slo.burnRateRule.alertDetailsAppSection.lastDurationInMinutes": "過去{duration}分", "xpack.slo.burnRateRule.alertDetailsAppSection.lastDurationInSeconds": "過去{duration}秒", - "xpack.slo.burnRateRule.alertDetailsAppSection.summaryField.slo": "SLO", "xpack.slo.burnRateRule.name": "{name}バーンレートルール", "xpack.slo.burnRateRuleEditor.h5.chooseASLOToMonitorLabel": "監視するSLOを選択", "xpack.slo.burnRateRuleEditor.h5.defineMultipleBurnRateLabel": "複数のバーンレート時間枠を定義", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3df4f18938f9b..2084db1c087a1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10545,8 +10545,6 @@ "xpack.apm.onboarding.shared_clients.configure.commands.serviceEnvironmentHint": "在其中部署此服务的环境的名称,如“生产”或“暂存”。在 APM UI 中,您可以通过环境在全局级别轻松筛选数据。跨代理命名环境时,保持一致至关重要。", "xpack.apm.onboarding.shared_clients.configure.commands.serviceNameHint": "服务名称是 APM UI 中的初级筛选,用于分组错误并跟踪数据。允许使用的字符包括 a-z、A-Z、0-9、-、_ 和空格。", "xpack.apm.onboarding.specProvider.longDescription": "应用程序性能监测 (APM) 从您的应用程序内收集深入全面的性能指标和错误。其允许您实时监测数以千计的应用程序的性能。{learnMoreLink}。", - "xpack.apm.pages.alertDetails.alertSummary.actualValue": "实际值", - "xpack.apm.pages.alertDetails.alertSummary.expectedValue": "预期值", "xpack.apm.percentOfParent": "({parentType, select, transaction {事务} trace {追溯} other {parentType 未知}}的 {value})", "xpack.apm.profiling.callout.description": "Universal Profiling 为所有应用程序的运行时行为提供了前所未有的代码可见性。它会剖析运行服务的主机上的每一行代码,不仅包括您的应用程序代码,而且包括内核和第三方库。", "xpack.apm.profiling.callout.dismiss": "关闭", @@ -31852,9 +31850,6 @@ "xpack.observability.alertDetailContextualInsights.InsightButtonLabel": "帮助我了解此告警", "xpack.observability.alertDetails.actionsButtonLabel": "操作", "xpack.observability.alertDetails.addToCase": "添加到案例", - "xpack.observability.alertDetails.alertSummaryField.rule": "规则", - "xpack.observability.alertDetails.alertSummaryField.source": "源", - "xpack.observability.alertDetails.alertSummaryField.tags": "标签", "xpack.observability.alertDetails.editRule": "编辑规则", "xpack.observability.alertDetails.editSnoozeRule": "暂停规则", "xpack.observability.alertDetails.errorPromptBody": "加载告警详情时出错。", @@ -41022,7 +41017,6 @@ "xpack.slo.burnRateRule.alertDetailsAppSection.lastDurationInHours": "过去 {duration} 小时", "xpack.slo.burnRateRule.alertDetailsAppSection.lastDurationInMinutes": "过去 {duration} 分钟", "xpack.slo.burnRateRule.alertDetailsAppSection.lastDurationInSeconds": "过去 {duration} 秒", - "xpack.slo.burnRateRule.alertDetailsAppSection.summaryField.slo": "SLO", "xpack.slo.burnRateRule.name": "{name} 消耗速度规则", "xpack.slo.burnRateRuleEditor.h5.chooseASLOToMonitorLabel": "选择要监测的 SLO", "xpack.slo.burnRateRuleEditor.h5.defineMultipleBurnRateLabel": "定义多个消耗速度窗口",