diff --git a/x-pack/solutions/observability/plugins/exploratory_view/public/typings/fetch_overview_data/index.ts b/x-pack/solutions/observability/plugins/exploratory_view/public/typings/fetch_overview_data/index.ts index f338354d52265..24770f448873d 100644 --- a/x-pack/solutions/observability/plugins/exploratory_view/public/typings/fetch_overview_data/index.ts +++ b/x-pack/solutions/observability/plugins/exploratory_view/public/typings/fetch_overview_data/index.ts @@ -69,13 +69,18 @@ export type FetchData = ( fetchDataParams: FetchDataParams ) => Promise; -export type HasData = ( +export type HasData = ( params?: HasDataParams ) => Promise; export type ObservabilityFetchDataPlugins = Exclude< ObservabilityApp, - 'observability-overview' | 'fleet' | 'synthetics' | 'profiling' | 'observability-onboarding' + | 'observability-overview' + | 'fleet' + | 'synthetics' + | 'profiling' + | 'observability-onboarding' + | 'alerts' >; export interface DataHandler< 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 b03d9d52da2a1..68546f8e1454f 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 @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useHistory, useLocation, useParams } from 'react-router-dom'; import { usePerformanceContext } from '@kbn/ebt-tools'; import { i18n } from '@kbn/i18n'; @@ -54,6 +54,7 @@ import { AlertOverview } from '../../components/alert_overview/alert_overview'; import { CustomThresholdRule } from '../../components/custom_threshold/components/types'; import { AlertDetailContextualInsights } from './alert_details_contextual_insights'; import { AlertHistoryChart } from './components/alert_history'; +import StaleAlert from './components/stale_alert'; interface AlertDetailsPathParams { alertId: string; @@ -107,12 +108,11 @@ export function AlertDetails() { const CasesContext = getCasesContext(); const userCasesPermissions = canUseCases([observabilityFeatureId]); const ruleId = alertDetail?.formatted.fields[ALERT_RULE_UUID]; - const { rule } = useFetchRule({ + const { rule, refetch } = useFetchRule({ ruleId, }); const [alertStatus, setAlertStatus] = useState(); const { euiTheme } = useEuiTheme(); - const [sources, setSources] = useState(); const [activeTabId, setActiveTabId] = useState(() => { const searchParams = new URLSearchParams(search); @@ -179,9 +179,9 @@ export function AlertDetails() { { serverless } ); - const onUntrackAlert = () => { + const onUntrackAlert = useCallback(() => { setAlertStatus(ALERT_STATUS_UNTRACKED); - }; + }, []); useEffect(() => { if (!isLoading && !!alertDetail && activeTabId === OVERVIEW_TAB_ID) { @@ -227,6 +227,15 @@ export function AlertDetails() { */ isAlertDetailsEnabledPerApp(alertDetail.formatted, config) ? ( <> + + + diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/stale_alert.test.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/stale_alert.test.tsx new file mode 100644 index 0000000000000..9c8288da36d5b --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/stale_alert.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 { render } from '../../../utils/test_helper'; +import { alert } from '../mock/alert'; +import { useKibana } from '../../../utils/kibana_react'; +import { kibanaStartMock } from '../../../utils/kibana_react.mock'; +import { useBulkUntrackAlerts } from '../hooks/use_bulk_untrack_alerts'; +import StaleAlert from './stale_alert'; +import { Rule } from '@kbn/triggers-actions-ui-plugin/public'; +import { TopAlert } from '../../../typings/alerts'; + +jest.mock('../../../utils/kibana_react'); +jest.mock('../hooks/use_bulk_untrack_alerts'); + +const useKibanaMock = useKibana as jest.Mock; +const mockKibana = () => { + useKibanaMock.mockReturnValue({ + services: { + ...kibanaStartMock.startContract().services, + http: { + basePath: { + prepend: jest.fn(), + }, + }, + }, + }); +}; + +const useBulkUntrackAlertsMock = useBulkUntrackAlerts as jest.Mock; + +useBulkUntrackAlertsMock.mockReturnValue({ + mutateAsync: jest.fn(), +}); + +const ruleMock = { + ruleTypeId: 'apm', +} as unknown as Rule; +describe('Stale alert', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockKibana(); + }); + + it('should show alert stale callout', async () => { + const staleAlert = render( + {}} + onUntrackAlert={() => {}} + /> + ); + + expect(staleAlert.queryByTestId('o11yAlertDetailsAlertStaleCallout')).toBeInTheDocument(); + expect( + staleAlert.queryByTestId('o11yAlertDetailsAlertStaleCalloutEditRule') + ).toBeInTheDocument(); + expect( + staleAlert.queryByTestId('o11yAlertDetailsAlertStaleCalloutMarkAsUntrackedButton') + ).toBeInTheDocument(); + }); + + it('should NOT show alert stale callout < 5 days', async () => { + const alertUpdated = { + ...alert, + start: new Date(Date.now() - 4 * 24 * 60 * 60 * 1000), // 4 days ago + } as unknown as TopAlert; + const staleAlert = render( + {}} + onUntrackAlert={() => {}} + /> + ); + + expect(staleAlert.queryByTestId('o11yAlertDetailsAlertStaleCallout')).toBeFalsy(); + expect(staleAlert.queryByTestId('o11yAlertDetailsAlertStaleCalloutEditRule')).toBeFalsy(); + expect( + staleAlert.queryByTestId('o11yAlertDetailsAlertStaleCalloutMarkAsUntrackedButton') + ).toBeFalsy(); + }); +}); diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/stale_alert.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/stale_alert.tsx new file mode 100644 index 0000000000000..f505b07d183ba --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/stale_alert.tsx @@ -0,0 +1,153 @@ +/* + * 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, { useCallback, useMemo, useState } from 'react'; +import type { Rule } from '@kbn/triggers-actions-ui-plugin/public'; +import { ALERT_CASE_IDS, ALERT_STATUS_ACTIVE, ALERT_UUID } from '@kbn/rule-data-utils'; +import moment from 'moment'; +import { EuiButton, EuiCallOut, EuiFlexGroup } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { RuleFormFlyout } from '@kbn/response-ops-rule-form/flyout'; +import { METRIC_TYPE, useUiTracker } from '@kbn/observability-shared-plugin/public'; +import { TopAlert } from '../../../typings/alerts'; +import { useKibana } from '../../../utils/kibana_react'; +import { useBulkUntrackAlerts } from '../hooks/use_bulk_untrack_alerts'; + +function StaleAlert({ + alert, + alertStatus, + rule, + refetchRule, + onUntrackAlert, +}: { + alert: TopAlert; + alertStatus: string | undefined; + rule: Rule | undefined; + refetchRule: () => void; + onUntrackAlert: () => void; +}) { + const { services } = useKibana(); + const { + triggersActionsUi: { ruleTypeRegistry, actionTypeRegistry }, + } = services; + const [ruleConditionsFlyoutOpen, setRuleConditionsFlyoutOpen] = useState(false); + const { mutateAsync: untrackAlerts } = useBulkUntrackAlerts(); + const trackEvent = useUiTracker(); + const handleUntrackAlert = useCallback(async () => { + const alertUuid = alert?.fields[ALERT_UUID]; + if (alertUuid) { + await untrackAlerts({ + indices: ['.internal.alerts-observability.*'], + alertUuids: [alertUuid], + }); + onUntrackAlert(); + } + }, [alert?.fields, untrackAlerts, onUntrackAlert]); + const handleEditRuleDetails = () => { + setRuleConditionsFlyoutOpen(true); + }; + const isAlertStale = useMemo(() => { + if (alertStatus === ALERT_STATUS_ACTIVE) { + const numOfCases = alert.fields[ALERT_CASE_IDS]?.length || 0; + const timestamp = alert.start; + const givenDate = moment(timestamp); + const now = moment(); + const diffInDays = now.diff(givenDate, 'days'); + + // The heuristics to show the stale alert callout are: + // 1. The alert has been active for more than 5 days + + if (diffInDays >= 5) { + trackEvent({ + app: 'alerts', + metricType: METRIC_TYPE.LOADED, + metric: `alert_details_alert_stale_callout__ruleType_${rule?.ruleTypeId}`, + }); + return { + isStale: true, + days: diffInDays, + cases: numOfCases, + }; + } + } else { + return { + isStale: false, + days: 0, + cases: 0, + }; + } + }, [alert.fields, alert.start, alertStatus, rule?.ruleTypeId, trackEvent]); + + return ( + <> + {isAlertStale?.isStale && ( + +

+ {i18n.translate('xpack.observability.alertDetails.staleAlertCallout.message', { + defaultMessage: + 'This alert has been active for {numOfDays} days and is assigned to {numOfCases} {cases}.', + values: { + numOfDays: isAlertStale?.days, + numOfCases: isAlertStale?.cases, + cases: isAlertStale?.cases > 1 ? 'cases' : 'case', + }, + })} +

+ + + {i18n.translate( + 'xpack.observability.alertDetails.alertStaleCallout.markAsUntrackedButton', + { + defaultMessage: 'Untrack', + } + )} + + + {i18n.translate('xpack.observability.alertDetails.alertStaleCallout.editRuleButton', { + defaultMessage: 'Edit rule', + })} + + +
+ )} + {rule && ruleConditionsFlyoutOpen ? ( + { + setRuleConditionsFlyoutOpen(false); + }} + onSubmit={() => { + setRuleConditionsFlyoutOpen(false); + refetchRule(); + }} + /> + ) : null} + + ); +} + +// eslint-disable-next-line import/no-default-export +export default StaleAlert; diff --git a/x-pack/solutions/observability/plugins/observability_shared/typings/common.ts b/x-pack/solutions/observability/plugins/observability_shared/typings/common.ts index 6e7340700259a..0f912833d44cf 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/typings/common.ts +++ b/x-pack/solutions/observability/plugins/observability_shared/typings/common.ts @@ -12,6 +12,7 @@ export type ObservabilityApp = // we will remove uptime in future to replace to be replace by synthetics | 'uptime' | 'synthetics' + | 'alerts' | 'observability-overview' | 'ux' | 'fleet'