Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,18 @@ export type FetchData<T extends FetchDataResponse = FetchDataResponse> = (
fetchDataParams: FetchDataParams
) => Promise<T>;

export type HasData<T extends ObservabilityFetchDataPlugins> = (
export type HasData<T extends keyof ObservabilityHasDataResponse> = (
params?: HasDataParams
) => Promise<ObservabilityHasDataResponse[T]>;

export type ObservabilityFetchDataPlugins = Exclude<
ObservabilityApp,
'observability-overview' | 'fleet' | 'synthetics' | 'profiling' | 'observability-onboarding'
| 'observability-overview'
| 'fleet'
| 'synthetics'
| 'profiling'
| 'observability-onboarding'
| 'alerts'
>;

export interface DataHandler<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<AlertStatus>();
const { euiTheme } = useEuiTheme();

const [sources, setSources] = useState<AlertDetailsSource[]>();
const [activeTabId, setActiveTabId] = useState<TabId>(() => {
const searchParams = new URLSearchParams(search);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -227,6 +227,15 @@ export function AlertDetails() {
*/
isAlertDetailsEnabledPerApp(alertDetail.formatted, config) ? (
<>
<EuiSpacer size="m" />
<StaleAlert
alert={alertDetail.formatted}
alertStatus={alertStatus}
rule={rule}
onUntrackAlert={onUntrackAlert}
refetchRule={refetch}
/>

<EuiSpacer size="m" />
<EuiFlexGroup direction="column" gutterSize="m">
<SourceBar alert={alertDetail.formatted} sources={sources} />
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
<StaleAlert
alert={alert}
alertStatus="active"
rule={ruleMock}
refetchRule={() => {}}
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(
<StaleAlert
alert={alertUpdated}
alertStatus="active"
rule={ruleMock}
refetchRule={() => {}}
onUntrackAlert={() => {}}
/>
);

expect(staleAlert.queryByTestId('o11yAlertDetailsAlertStaleCallout')).toBeFalsy();
expect(staleAlert.queryByTestId('o11yAlertDetailsAlertStaleCalloutEditRule')).toBeFalsy();
expect(
staleAlert.queryByTestId('o11yAlertDetailsAlertStaleCalloutMarkAsUntrackedButton')
).toBeFalsy();
});
});
Original file line number Diff line number Diff line change
@@ -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<boolean>(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 && (
<EuiCallOut
data-test-subj="o11yAlertDetailsAlertStaleCallout"
title={i18n.translate('xpack.observability.alertDetails.staleAlertCallout.title', {
defaultMessage: 'This alert may be stale',
})}
color="warning"
iconType="warning"
>
<p>
{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',
},
})}
</p>
<EuiFlexGroup gutterSize="s" justifyContent="flexStart">
<EuiButton
data-test-subj="o11yAlertDetailsAlertStaleCalloutMarkAsUntrackedButton"
color="warning"
fill
iconType="eyeClosed"
onClick={handleUntrackAlert}
>
{i18n.translate(
'xpack.observability.alertDetails.alertStaleCallout.markAsUntrackedButton',
{
defaultMessage: 'Untrack',
}
)}
</EuiButton>
<EuiButton
data-test-subj="o11yAlertDetailsAlertStaleCalloutEditRule"
color="warning"
iconType="pencil"
onClick={handleEditRuleDetails}
>
{i18n.translate('xpack.observability.alertDetails.alertStaleCallout.editRuleButton', {
defaultMessage: 'Edit rule',
})}
</EuiButton>
</EuiFlexGroup>
</EuiCallOut>
)}
{rule && ruleConditionsFlyoutOpen ? (
<RuleFormFlyout
plugins={{ ...services, ruleTypeRegistry, actionTypeRegistry }}
id={rule.id}
onCancel={() => {
setRuleConditionsFlyoutOpen(false);
}}
onSubmit={() => {
setRuleConditionsFlyoutOpen(false);
refetchRule();
}}
/>
) : null}
</>
);
}

// eslint-disable-next-line import/no-default-export
export default StaleAlert;
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down