Skip to content

Commit 1084d08

Browse files
committed
[Alerting UI] Display a banner to users when some alerts have failures, added alert statuses column and filters (#79038)
* Added ui for alert failures banner * Added UI for alerts statuses * Adjusted form * Added banned on the details page * Fixed failing intern. check and type checks * Added unit test for displaying alert error banner * Fixed type check * Fixed due to comments * Changes due to comments * Fixed due to comments * Fixed text on banners * Added i18n translations
1 parent b963d2a commit 1084d08

File tree

8 files changed

+557
-41
lines changed

8 files changed

+557
-41
lines changed

x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,7 @@ import { fold } from 'fp-ts/lib/Either';
1111
import { pick } from 'lodash';
1212
import { alertStateSchema, AlertingFrameworkHealth } from '../../../../alerts/common';
1313
import { BASE_ALERT_API_PATH } from '../constants';
14-
import {
15-
Alert,
16-
AlertType,
17-
AlertWithoutId,
18-
AlertTaskState,
19-
AlertInstanceSummary,
20-
} from '../../types';
14+
import { Alert, AlertType, AlertUpdates, AlertTaskState, AlertInstanceSummary } from '../../types';
2115

2216
export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise<AlertType[]> {
2317
return await http.get(`${BASE_ALERT_API_PATH}/list_alert_types`);
@@ -70,12 +64,14 @@ export async function loadAlerts({
7064
searchText,
7165
typesFilter,
7266
actionTypesFilter,
67+
alertStatusesFilter,
7368
}: {
7469
http: HttpSetup;
7570
page: { index: number; size: number };
7671
searchText?: string;
7772
typesFilter?: string[];
7873
actionTypesFilter?: string[];
74+
alertStatusesFilter?: string[];
7975
}): Promise<{
8076
page: number;
8177
perPage: number;
@@ -97,6 +93,9 @@ export async function loadAlerts({
9793
].join('')
9894
);
9995
}
96+
if (alertStatusesFilter && alertStatusesFilter.length) {
97+
filters.push(`alert.attributes.executionStatus.status:(${alertStatusesFilter.join(' or ')})`);
98+
}
10099
return await http.get(`${BASE_ALERT_API_PATH}/_find`, {
101100
query: {
102101
page: page.index + 1,
@@ -137,7 +136,7 @@ export async function createAlert({
137136
}: {
138137
http: HttpSetup;
139138
alert: Omit<
140-
AlertWithoutId,
139+
AlertUpdates,
141140
'createdBy' | 'updatedBy' | 'muteAll' | 'mutedInstanceIds' | 'executionStatus'
142141
>;
143142
}): Promise<Alert> {
@@ -152,7 +151,7 @@ export async function updateAlert({
152151
id,
153152
}: {
154153
http: HttpSetup;
155-
alert: Pick<AlertWithoutId, 'throttle' | 'name' | 'tags' | 'schedule' | 'params' | 'actions'>;
154+
alert: Pick<AlertUpdates, 'throttle' | 'name' | 'tags' | 'schedule' | 'params' | 'actions'>;
156155
id: string;
157156
}): Promise<Alert> {
158157
return await http.put(`${BASE_ALERT_API_PATH}/alert/${id}`, {

x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
EuiSwitch,
1616
EuiBetaBadge,
1717
EuiButtonEmpty,
18+
EuiText,
1819
} from '@elastic/eui';
1920
import { i18n } from '@kbn/i18n';
2021
import { ViewInApp } from './view_in_app';
@@ -142,6 +143,38 @@ describe('alert_details', () => {
142143
).toBeTruthy();
143144
});
144145

146+
it('renders the alert error banner with error message, when alert status is an error', () => {
147+
const alert = mockAlert({
148+
executionStatus: {
149+
status: 'error',
150+
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
151+
error: {
152+
reason: 'unknown',
153+
message: 'test',
154+
},
155+
},
156+
});
157+
const alertType = {
158+
id: '.noop',
159+
name: 'No Op',
160+
actionGroups: [{ id: 'default', name: 'Default' }],
161+
actionVariables: { context: [], state: [], params: [] },
162+
defaultActionGroupId: 'default',
163+
producer: ALERTS_FEATURE_ID,
164+
authorizedConsumers,
165+
};
166+
167+
expect(
168+
shallow(
169+
<AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} />
170+
).containsMatchingElement(
171+
<EuiText size="s" color="danger" data-test-subj="alertErrorMessageText">
172+
{'test'}
173+
</EuiText>
174+
)
175+
).toBeTruthy();
176+
});
177+
145178
describe('actions', () => {
146179
it('renders an alert action', () => {
147180
const alert = mockAlert({

x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
EuiSpacer,
2525
EuiBetaBadge,
2626
EuiButtonEmpty,
27+
EuiButton,
2728
} from '@elastic/eui';
2829
import { FormattedMessage } from '@kbn/i18n/react';
2930
import { i18n } from '@kbn/i18n';
@@ -42,6 +43,7 @@ import { PLUGIN } from '../../../constants/plugin';
4243
import { AlertEdit } from '../../alert_form';
4344
import { AlertsContextProvider } from '../../../context/alerts_context';
4445
import { routeToAlertDetails } from '../../../constants';
46+
import { alertsErrorReasonTranslationsMapping } from '../../alerts_list/translations';
4547

4648
type AlertDetailsProps = {
4749
alert: Alert;
@@ -105,11 +107,20 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
105107
const [isEnabled, setIsEnabled] = useState<boolean>(alert.enabled);
106108
const [isMuted, setIsMuted] = useState<boolean>(alert.muteAll);
107109
const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false);
110+
const [dissmissAlertErrors, setDissmissAlertErrors] = useState<boolean>(false);
108111

109112
const setAlert = async () => {
110113
history.push(routeToAlertDetails.replace(`:alertId`, alert.id));
111114
};
112115

116+
const getAlertStatusErrorReasonText = () => {
117+
if (alert.executionStatus.error && alert.executionStatus.error.reason) {
118+
return alertsErrorReasonTranslationsMapping[alert.executionStatus.error.reason];
119+
} else {
120+
return alertsErrorReasonTranslationsMapping.unknown;
121+
}
122+
};
123+
113124
return (
114125
<EuiPage>
115126
<EuiPageBody>
@@ -275,6 +286,30 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
275286
</EuiFlexGroup>
276287
</EuiFlexItem>
277288
</EuiFlexGroup>
289+
{!dissmissAlertErrors && alert.executionStatus.status === 'error' ? (
290+
<EuiFlexGroup>
291+
<EuiFlexItem>
292+
<EuiCallOut
293+
color="danger"
294+
data-test-subj="alertErrorBanner"
295+
size="s"
296+
title={getAlertStatusErrorReasonText()}
297+
iconType="alert"
298+
>
299+
<EuiText size="s" color="danger" data-test-subj="alertErrorMessageText">
300+
{alert.executionStatus.error?.message}
301+
</EuiText>
302+
<EuiSpacer size="s" />
303+
<EuiButton color="danger" onClick={() => setDissmissAlertErrors(true)}>
304+
<FormattedMessage
305+
id="xpack.triggersActionsUI.sections.alertDetails.dismissButtonTitle"
306+
defaultMessage="Dismiss"
307+
/>
308+
</EuiButton>
309+
</EuiCallOut>
310+
</EuiFlexItem>
311+
</EuiFlexGroup>
312+
) : null}
278313
<EuiFlexGroup>
279314
<EuiFlexItem>
280315
{alert.enabled ? (
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React, { useEffect, useState } from 'react';
8+
import { FormattedMessage } from '@kbn/i18n/react';
9+
import {
10+
EuiFilterGroup,
11+
EuiPopover,
12+
EuiFilterButton,
13+
EuiFilterSelectItem,
14+
EuiHealth,
15+
} from '@elastic/eui';
16+
import {
17+
AlertExecutionStatuses,
18+
AlertExecutionStatusValues,
19+
} from '../../../../../../alerts/common';
20+
import { alertsStatusesTranslationsMapping } from '../translations';
21+
22+
interface AlertStatusFilterProps {
23+
selectedStatuses: string[];
24+
onChange?: (selectedAlertStatusesIds: string[]) => void;
25+
}
26+
27+
export const AlertStatusFilter: React.FunctionComponent<AlertStatusFilterProps> = ({
28+
selectedStatuses,
29+
onChange,
30+
}: AlertStatusFilterProps) => {
31+
const [selectedValues, setSelectedValues] = useState<string[]>(selectedStatuses);
32+
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
33+
34+
useEffect(() => {
35+
if (onChange) {
36+
onChange(selectedValues);
37+
}
38+
// eslint-disable-next-line react-hooks/exhaustive-deps
39+
}, [selectedValues]);
40+
41+
useEffect(() => {
42+
setSelectedValues(selectedStatuses);
43+
}, [selectedStatuses]);
44+
45+
return (
46+
<EuiFilterGroup>
47+
<EuiPopover
48+
isOpen={isPopoverOpen}
49+
closePopover={() => setIsPopoverOpen(false)}
50+
button={
51+
<EuiFilterButton
52+
iconType="arrowDown"
53+
hasActiveFilters={selectedValues.length > 0}
54+
numActiveFilters={selectedValues.length}
55+
numFilters={selectedValues.length}
56+
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
57+
>
58+
<FormattedMessage
59+
id="xpack.triggersActionsUI.sections.alertsList.alertStatusFilterLabel"
60+
defaultMessage="Status"
61+
/>
62+
</EuiFilterButton>
63+
}
64+
>
65+
<div className="euiFilterSelect__items">
66+
{[...AlertExecutionStatusValues].sort().map((item: AlertExecutionStatuses) => {
67+
const healthColor = getHealthColor(item);
68+
return (
69+
<EuiFilterSelectItem
70+
key={item}
71+
style={{ textTransform: 'capitalize' }}
72+
onClick={() => {
73+
const isPreviouslyChecked = selectedValues.includes(item);
74+
if (isPreviouslyChecked) {
75+
setSelectedValues(selectedValues.filter((val) => val !== item));
76+
} else {
77+
setSelectedValues(selectedValues.concat(item));
78+
}
79+
}}
80+
checked={selectedValues.includes(item) ? 'on' : undefined}
81+
>
82+
<EuiHealth color={healthColor}>{alertsStatusesTranslationsMapping[item]}</EuiHealth>
83+
</EuiFilterSelectItem>
84+
);
85+
})}
86+
</div>
87+
</EuiPopover>
88+
</EuiFilterGroup>
89+
);
90+
};
91+
92+
export function getHealthColor(status: AlertExecutionStatuses) {
93+
switch (status) {
94+
case 'active':
95+
return 'primary';
96+
case 'error':
97+
return 'danger';
98+
case 'ok':
99+
return 'subdued';
100+
case 'pending':
101+
return 'success';
102+
default:
103+
return 'warning';
104+
}
105+
}

0 commit comments

Comments
 (0)