From 7da827e8d9b1d354c3d0093941e72ca79e821c3d Mon Sep 17 00:00:00 2001 From: Bailey Cash Date: Tue, 24 Jun 2025 05:03:20 -0400 Subject: [PATCH] [Incident management] Callout for alerts that triggered around the same time (#223473) ## Summary Implements #213020 Partially implements filter bar seen with #213015 This PR adds a callout on the alert details page to encourage users to visit the related alerts page when at least one alert was triggered within 30 minutes of the current alert. If no alerts were triggered, the message remains without a call to action. https://github.com/user-attachments/assets/23b2d3e9-353b-45e1-a007-d188db5617fc ## Testing The related alert query usually find alerts that were raised within a day of each other. To find alerts that were raised within a few minutes, try creating an SLO with a chosen groupBy field that will easily violate a burn rate rule. Alerts should be triggered for each instance within seconds. Once the filter is executed, these alerts should appear without alerts that were triggered earlier in the day. --- .../apis/search_alerts/search_alerts.ts | 4 + .../common/hooks/use_search_alerts_query.ts | 8 +- .../alert_details/alert_details.test.tsx | 1 + .../pages/alert_details/alert_details.tsx | 40 ++++++---- .../related_alerts/related_alerts_table.tsx | 9 ++- .../related_alerts_table_filter.tsx | 45 +++++++++++ ...y.ts => get_build_related_alerts_query.ts} | 18 ++++- .../hooks/use_filter_proximal_param.ts | 23 ++++++ .../hooks/use_find_proximal_alerts.ts | 32 ++++++++ .../pages/alert_details/hooks/use_tab_id.ts | 42 ++++++++++ .../proximal_alerts_callout.test.tsx | 78 +++++++++++++++++++ .../alert_details/proximal_alerts_callout.tsx | 53 +++++++++++++ 12 files changed, 331 insertions(+), 22 deletions(-) create mode 100644 x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_alerts/related_alerts_table_filter.tsx rename x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/related_alerts/{use_build_related_alerts_query.ts => get_build_related_alerts_query.ts} (92%) create mode 100644 x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_filter_proximal_param.ts create mode 100644 x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_find_proximal_alerts.ts create mode 100644 x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_tab_id.ts create mode 100644 x-pack/solutions/observability/plugins/observability/public/pages/alert_details/proximal_alerts_callout.test.tsx create mode 100644 x-pack/solutions/observability/plugins/observability/public/pages/alert_details/proximal_alerts_callout.tsx diff --git a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/search_alerts/search_alerts.ts b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/search_alerts/search_alerts.ts index 21f33a768cba..517590f9f499 100644 --- a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/search_alerts/search_alerts.ts +++ b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/search_alerts/search_alerts.ts @@ -68,6 +68,10 @@ export interface SearchAlertsParams { * The page size to fetch */ pageSize: number; + /** + * Force using the default context, otherwise use the AlertQueryContext + */ + skipAlertsQueryContext?: boolean; /** * The minimum score to apply to the query */ diff --git a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/hooks/use_search_alerts_query.ts b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/hooks/use_search_alerts_query.ts index c94289280840..bae828084dd3 100644 --- a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/hooks/use_search_alerts_query.ts +++ b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/hooks/use_search_alerts_query.ts @@ -27,7 +27,11 @@ export const queryKeyPrefix = ['alerts', searchAlerts.name]; * When testing components that depend on this hook, prefer mocking the {@link searchAlerts} function instead of the hook itself. * @external https://tanstack.com/query/v4/docs/framework/react/guides/testing */ -export const useSearchAlertsQuery = ({ data, ...params }: UseSearchAlertsQueryParams) => { +export const useSearchAlertsQuery = ({ + data, + skipAlertsQueryContext, + ...params +}: UseSearchAlertsQueryParams) => { const { ruleTypeIds, consumers, @@ -64,7 +68,7 @@ export const useSearchAlertsQuery = ({ data, ...params }: UseSearchAlertsQueryPa trackScores, }), refetchOnWindowFocus: false, - context: AlertsQueryContext, + context: skipAlertsQueryContext ? undefined : AlertsQueryContext, enabled: ruleTypeIds.length > 0, // To avoid flash of empty state with pagination, see https://tanstack.com/query/latest/docs/framework/react/guides/paginated-queries#better-paginated-queries-with-placeholderdata keepPreviousData: true, diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.test.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.test.tsx index c28818302ac2..780e23bb0252 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.test.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.test.tsx @@ -204,6 +204,7 @@ describe('Alert details', () => { expect(alertDetails.queryByTestId('alert-summary-container')).toBeFalsy(); expect(alertDetails.queryByTestId('overviewTab')).toBeTruthy(); expect(alertDetails.queryByTestId('metadataTab')).toBeTruthy(); + expect(alertDetails.queryByTestId('relatedAlertsTab')).toBeTruthy(); }); it('should show Metadata tab', async () => { diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.tsx index cee55c918a78..3e0929a55fbf 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.tsx @@ -6,7 +6,7 @@ */ import React, { useCallback, useEffect, useState } from 'react'; -import { useHistory, useLocation, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { @@ -60,6 +60,8 @@ import StaleAlert from './components/stale_alert'; import { RelatedDashboards } from './components/related_dashboards'; import { getAlertTitle } from '../../utils/format_alert_title'; import { AlertSubtitle } from './components/alert_subtitle'; +import { ProximalAlertsCallout } from './proximal_alerts_callout'; +import { useTabId } from './hooks/use_tab_id'; import { useRelatedDashboards } from './hooks/use_related_dashboards'; interface AlertDetailsPathParams { @@ -74,7 +76,6 @@ const defaultBreadcrumb = i18n.translate('xpack.observability.breadcrumbs.alertD export const LOG_DOCUMENT_COUNT_RULE_TYPE_ID = 'logs.alert.document.count'; export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold'; export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold'; -const ALERT_DETAILS_TAB_URL_STORAGE_KEY = 'tabId'; const TAB_IDS = [ 'overview', @@ -91,6 +92,7 @@ const isTabId = (value: string): value is TabId => { }; export function AlertDetails() { + const { services } = useKibana(); const { cases: { helpers: { canUseCases }, @@ -101,12 +103,11 @@ export function AlertDetails() { observabilityAIAssistant, uiSettings, serverless, - } = useKibana().services; - - const { search } = useLocation(); - const history = useHistory(); + } = services; const { ObservabilityPageTemplate, config } = usePluginContext(); const { alertId } = useParams(); + const { getUrlTabId, setUrlTabId } = useTabId(); + const urlTabId = getUrlTabId(); const { isLoadingRelatedDashboards, suggestedDashboards, @@ -135,17 +136,17 @@ export function AlertDetails() { const { euiTheme } = useEuiTheme(); const [sources, setSources] = useState(); const [activeTabId, setActiveTabId] = useState(); + const handleSetTabId = async (tabId: TabId) => { setActiveTabId(tabId); - let searchParams = new URLSearchParams(search); if (tabId === 'related_alerts') { - searchParams.set(ALERT_DETAILS_TAB_URL_STORAGE_KEY, tabId); + setUrlTabId(tabId, true, { + filterProximal: 'true', + }); } else { - searchParams = new URLSearchParams(); - searchParams.set(ALERT_DETAILS_TAB_URL_STORAGE_KEY, tabId); + setUrlTabId(tabId, true); } - history.replace({ search: searchParams.toString() }); }; useEffect(() => { @@ -171,11 +172,9 @@ export function AlertDetails() { if (alertDetail) { setRuleTypeModel(ruleTypeRegistry.get(alertDetail?.formatted.fields[ALERT_RULE_TYPE_ID]!)); setAlertStatus(alertDetail?.formatted?.fields[ALERT_STATUS] as AlertStatus); - const searchParams = new URLSearchParams(search); - const urlTabId = searchParams.get(ALERT_DETAILS_TAB_URL_STORAGE_KEY); setActiveTabId(urlTabId && isTabId(urlTabId) ? urlTabId : 'overview'); } - }, [alertDetail, ruleTypeRegistry, search]); + }, [alertDetail, ruleTypeRegistry, urlTabId]); useBreadcrumbs( [ @@ -199,6 +198,10 @@ export function AlertDetails() { setAlertStatus(ALERT_STATUS_UNTRACKED); }, []); + const showRelatedAlertsFromCallout = () => { + handleSetTabId('related_alerts'); + }; + usePageReady({ isRefreshing: isLoading, isReady: !isLoading && !!alertDetail && activeTabId === 'overview', @@ -253,6 +256,10 @@ export function AlertDetails() { + {rule && alertDetail.formatted && ( @@ -273,6 +280,11 @@ export function AlertDetails() { ) : ( + + diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_alerts/related_alerts_table.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_alerts/related_alerts_table.tsx index dfe8894d93a0..30e2fa2f7465 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_alerts/related_alerts_table.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_alerts/related_alerts_table.tsx @@ -11,7 +11,7 @@ import { ALERT_START, ALERT_UUID } from '@kbn/rule-data-utils'; import { AlertsTable } from '@kbn/response-ops-alerts-table'; import { SortOrder } from '@elastic/elasticsearch/lib/api/types'; import { getRelatedColumns } from './get_related_columns'; -import { useBuildRelatedAlertsQuery } from '../../hooks/related_alerts/use_build_related_alerts_query'; +import { getBuildRelatedAlertsQuery } from '../../hooks/related_alerts/get_build_related_alerts_query'; import { AlertData } from '../../../../hooks/use_fetch_alert_detail'; import { GetObservabilityAlertsTableProp, @@ -25,6 +25,8 @@ import { AlertsFlyoutFooter } from '../../../../components/alerts_flyout/alerts_ import { OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES } from '../../../../../common/constants'; import { AlertsTableCellValue } from '../../../../components/alerts_table/common/cell_value'; import { casesFeatureIdV2 } from '../../../../../common'; +import { useFilterProximalParam } from '../../hooks/use_filter_proximal_param'; +import { RelatedAlertsTableFilter } from './related_alerts_table_filter'; interface Props { alertData: AlertData; @@ -52,14 +54,15 @@ const RELATED_ALERTS_TABLE_ID = 'xpack.observability.alerts.relatedAlerts'; export function RelatedAlertsTable({ alertData }: Props) { const { formatted: alert } = alertData; - const esQuery = useBuildRelatedAlertsQuery({ alert }); + const { filterProximal } = useFilterProximalParam(); + const esQuery = getBuildRelatedAlertsQuery({ alert, filterProximal }); const { observabilityRuleTypeRegistry, config } = usePluginContext(); - const services = useKibana().services; return ( + id={RELATED_ALERTS_TABLE_ID} query={esQuery} diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_alerts/related_alerts_table_filter.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_alerts/related_alerts_table_filter.tsx new file mode 100644 index 000000000000..d90f18a23d39 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_alerts/related_alerts_table_filter.tsx @@ -0,0 +1,45 @@ +/* + * 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 { EuiCheckbox, EuiFlexGroup, EuiFormRow, EuiPanel, EuiText } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { useFilterProximalParam } from '../../hooks/use_filter_proximal_param'; + +export function RelatedAlertsTableFilter() { + const { filterProximal, setProximalFilterParam } = useFilterProximalParam(); + + return ( + + + + + {i18n.translate('xpack.observability.alerts.relatedAlerts.filtersLabel', { + defaultMessage: 'Filters', + })} + + + + { + setProximalFilterParam(event.target.checked); + }} + id={'proximal-alerts-checkbox'} + data-test-subj="proximal-alerts-checkbox" + /> + + + + ); +} diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/related_alerts/use_build_related_alerts_query.ts b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/related_alerts/get_build_related_alerts_query.ts similarity index 92% rename from x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/related_alerts/use_build_related_alerts_query.ts rename to x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/related_alerts/get_build_related_alerts_query.ts index fff62250ba01..a1e8514066b6 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/related_alerts/use_build_related_alerts_query.ts +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/related_alerts/get_build_related_alerts_query.ts @@ -23,9 +23,13 @@ import { TopAlert } from '../../../../typings/alerts'; interface Props { alert: TopAlert; + filterProximal: boolean; } -export function useBuildRelatedAlertsQuery({ alert }: Props): QueryDslQueryContainer { +export function getBuildRelatedAlertsQuery({ + alert, + filterProximal, +}: Props): QueryDslQueryContainer { const groups = alert.fields[ALERT_GROUP]; const shouldGroups: QueryDslQueryContainer[] = []; groups?.forEach(({ field, value }) => { @@ -58,14 +62,22 @@ export function useBuildRelatedAlertsQuery({ alert }: Props): QueryDslQueryConta const tags = alert.fields[ALERT_RULE_TAGS] ?? []; const instanceId = alert.fields[ALERT_INSTANCE_ID]?.split(',') ?? []; + const range = filterProximal ? [30, 'minutes'] : [1, 'days']; + return { bool: { filter: [ { range: { [ALERT_START]: { - gte: startDate.clone().subtract(1, 'days').toISOString(), - lte: startDate.clone().add(1, 'days').toISOString(), + gte: startDate + .clone() + .subtract(...range) + .toISOString(), + lte: startDate + .clone() + .add(...range) + .toISOString(), }, }, }, diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_filter_proximal_param.ts b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_filter_proximal_param.ts new file mode 100644 index 000000000000..00ced92fecb1 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_filter_proximal_param.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useHistory, useLocation } from 'react-router-dom'; + +export const useFilterProximalParam = () => { + const { search } = useLocation(); + const searchParams = new URLSearchParams(search); + const history = useHistory(); + + const setProximalFilterParam = (proximalFilter: boolean) => { + searchParams.set('filterProximal', String(proximalFilter)); + history.replace({ search: searchParams.toString() }); + }; + + const filterProximal = searchParams.get('filterProximal') === 'true'; + + return { filterProximal, setProximalFilterParam }; +}; diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_find_proximal_alerts.ts b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_find_proximal_alerts.ts new file mode 100644 index 000000000000..e94520ddb4c0 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_find_proximal_alerts.ts @@ -0,0 +1,32 @@ +/* + * 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 { useSearchAlertsQuery } from '@kbn/alerts-ui-shared/src/common/hooks/use_search_alerts_query'; +import { + OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES, + observabilityAlertFeatureIds, +} from '../../../../common/constants'; +import { AlertData } from '../../../hooks/use_fetch_alert_detail'; +import { useKibana } from '../../../utils/kibana_react'; +import { getBuildRelatedAlertsQuery } from './related_alerts/get_build_related_alerts_query'; + +export const useFindProximalAlerts = (alertDetail: AlertData) => { + const { services } = useKibana(); + + const esQuery = getBuildRelatedAlertsQuery({ + alert: alertDetail.formatted, + filterProximal: true, + }); + + return useSearchAlertsQuery({ + data: services.data, + ruleTypeIds: OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES, + consumers: observabilityAlertFeatureIds, + query: esQuery, + skipAlertsQueryContext: true, + }); +}; diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_tab_id.ts b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_tab_id.ts new file mode 100644 index 000000000000..f0f004ddb689 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_tab_id.ts @@ -0,0 +1,42 @@ +/* + * 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 { useHistory, useLocation } from 'react-router-dom'; + +const ALERT_DETAILS_TAB_URL_STORAGE_KEY = 'tabId'; + +export const useTabId = () => { + const { search } = useLocation(); + const history = useHistory(); + + const getUrlTabId = () => { + const searchParams = new URLSearchParams(search); + return searchParams.get(ALERT_DETAILS_TAB_URL_STORAGE_KEY); + }; + + const setUrlTabId = ( + tabId: string, + overrideSearchState?: boolean, + newSearchState?: Record + ) => { + const searchParams = new URLSearchParams(overrideSearchState ? undefined : search); + searchParams.set(ALERT_DETAILS_TAB_URL_STORAGE_KEY, tabId); + + if (newSearchState) { + Object.entries(newSearchState).forEach(([key, value]) => { + searchParams.set(key, value); + }); + } + + history.replace({ search: searchParams.toString() }); + }; + + return { + getUrlTabId, + setUrlTabId, + }; +}; diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/proximal_alerts_callout.test.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/proximal_alerts_callout.test.tsx new file mode 100644 index 000000000000..5d4c2c121138 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/proximal_alerts_callout.test.tsx @@ -0,0 +1,78 @@ +/* + * 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 { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import React from 'react'; +import { AlertData, useFetchAlertDetail } from '../../hooks/use_fetch_alert_detail'; +import { useFindProximalAlerts } from './hooks/use_find_proximal_alerts'; +import { ConfigSchema } from '../../plugin'; +import { Subset } from '../../typings'; +import { render } from '../../utils/test_helper'; +import { alertDetail } from './mock/alert'; +import { ProximalAlertsCallout } from './proximal_alerts_callout'; +import { fireEvent } from '@testing-library/dom'; + +jest.mock('../../utils/kibana_react'); + +jest.mock('../../hooks/use_fetch_alert_detail'); + +jest.mock('./hooks/use_find_proximal_alerts'); +jest.mock('@kbn/observability-shared-plugin/public'); +jest.mock('@kbn/ebt-tools'); + +const useFetchAlertDetailMock = useFetchAlertDetail as jest.Mock; +const useFindProximalAlertsMock = useFindProximalAlerts as jest.Mock; + +const config: Subset = { + unsafe: { + alertDetails: { + uptime: { enabled: true }, + }, + }, +}; + +describe('Proximal callout', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const switchTabs = jest.fn(); + + const renderCallout = (alert: AlertData) => + render( + + + , + config + ); + + it('should recommend the user see more related alerts', async () => { + useFindProximalAlertsMock.mockReturnValue({ + data: { total: 5 }, + isError: false, + isLoading: false, + }); + + useFetchAlertDetailMock.mockReturnValue([false, alertDetail]); + const callout = renderCallout(alertDetail); + expect(callout.queryByTestId('see-proximal-alerts')).toBeTruthy(); + fireEvent.click(callout.getByText('See related alerts')); + expect(switchTabs).toHaveBeenCalled(); + }); + + it('should not recommend the user see more related alerts', async () => { + useFindProximalAlertsMock.mockReturnValue({ + data: { total: 0 }, + isError: false, + isLoading: false, + }); + + useFetchAlertDetailMock.mockReturnValue([false, alertDetail]); + const callout = renderCallout(alertDetail); + expect(callout.queryByTestId('see-proximal-alerts')).toBeFalsy(); + }); +}); diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/proximal_alerts_callout.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/proximal_alerts_callout.tsx new file mode 100644 index 000000000000..a433dd556dcf --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/proximal_alerts_callout.tsx @@ -0,0 +1,53 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiCallOut, EuiIcon, EuiLink, useEuiTheme } from '@elastic/eui'; +import { useFindProximalAlerts } from './hooks/use_find_proximal_alerts'; +import { AlertData } from '../../hooks/use_fetch_alert_detail'; + +interface Props { + alertDetail: AlertData; + switchTabs: () => void; +} + +export function ProximalAlertsCallout({ alertDetail, switchTabs }: Props) { + const { euiTheme } = useEuiTheme(); + + const { data, isError, isLoading } = useFindProximalAlerts(alertDetail); + + const count = data?.total; + + if (isLoading || isError || count === undefined || count < 0) { + return null; + } + + return ( + + {i18n.translate('xpack.observability.alertDetails.proximalAlert.description', { + defaultMessage: + '{count, plural, one {# alert was} other {# alerts were}} created around the same time.', + values: { + count, + }, + })} + {count > 0 && ( + switchTabs()} + > + {i18n.translate('xpack.observability.alertDetails.proximalAlert.action', { + defaultMessage: 'See related alerts', + })}{' '} + + + )} + + ); +}