Skip to content

Commit

Permalink
[Alerting UI] Display a banner to users when some alerts have failure…
Browse files Browse the repository at this point in the history
…s, added alert statuses column and filters (#79038) (#79403)

* 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
  • Loading branch information
YulNaumenko authored Oct 5, 2020
1 parent 5d9c0e1 commit 83ebceb
Show file tree
Hide file tree
Showing 8 changed files with 557 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,7 @@ import { fold } from 'fp-ts/lib/Either';
import { pick } from 'lodash';
import { alertStateSchema, AlertingFrameworkHealth } from '../../../../alerts/common';
import { BASE_ALERT_API_PATH } from '../constants';
import {
Alert,
AlertType,
AlertWithoutId,
AlertTaskState,
AlertInstanceSummary,
} from '../../types';
import { Alert, AlertType, AlertUpdates, AlertTaskState, AlertInstanceSummary } from '../../types';

export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise<AlertType[]> {
return await http.get(`${BASE_ALERT_API_PATH}/list_alert_types`);
Expand Down Expand Up @@ -70,12 +64,14 @@ export async function loadAlerts({
searchText,
typesFilter,
actionTypesFilter,
alertStatusesFilter,
}: {
http: HttpSetup;
page: { index: number; size: number };
searchText?: string;
typesFilter?: string[];
actionTypesFilter?: string[];
alertStatusesFilter?: string[];
}): Promise<{
page: number;
perPage: number;
Expand All @@ -97,6 +93,9 @@ export async function loadAlerts({
].join('')
);
}
if (alertStatusesFilter && alertStatusesFilter.length) {
filters.push(`alert.attributes.executionStatus.status:(${alertStatusesFilter.join(' or ')})`);
}
return await http.get(`${BASE_ALERT_API_PATH}/_find`, {
query: {
page: page.index + 1,
Expand Down Expand Up @@ -137,7 +136,7 @@ export async function createAlert({
}: {
http: HttpSetup;
alert: Omit<
AlertWithoutId,
AlertUpdates,
'createdBy' | 'updatedBy' | 'muteAll' | 'mutedInstanceIds' | 'executionStatus'
>;
}): Promise<Alert> {
Expand All @@ -152,7 +151,7 @@ export async function updateAlert({
id,
}: {
http: HttpSetup;
alert: Pick<AlertWithoutId, 'throttle' | 'name' | 'tags' | 'schedule' | 'params' | 'actions'>;
alert: Pick<AlertUpdates, 'throttle' | 'name' | 'tags' | 'schedule' | 'params' | 'actions'>;
id: string;
}): Promise<Alert> {
return await http.put(`${BASE_ALERT_API_PATH}/alert/${id}`, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
EuiSwitch,
EuiBetaBadge,
EuiButtonEmpty,
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ViewInApp } from './view_in_app';
Expand Down Expand Up @@ -142,6 +143,38 @@ describe('alert_details', () => {
).toBeTruthy();
});

it('renders the alert error banner with error message, when alert status is an error', () => {
const alert = mockAlert({
executionStatus: {
status: 'error',
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
error: {
reason: 'unknown',
message: 'test',
},
},
});
const alertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
};

expect(
shallow(
<AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} />
).containsMatchingElement(
<EuiText size="s" color="danger" data-test-subj="alertErrorMessageText">
{'test'}
</EuiText>
)
).toBeTruthy();
});

describe('actions', () => {
it('renders an alert action', () => {
const alert = mockAlert({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
EuiSpacer,
EuiBetaBadge,
EuiButtonEmpty,
EuiButton,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
Expand All @@ -42,6 +43,7 @@ import { PLUGIN } from '../../../constants/plugin';
import { AlertEdit } from '../../alert_form';
import { AlertsContextProvider } from '../../../context/alerts_context';
import { routeToAlertDetails } from '../../../constants';
import { alertsErrorReasonTranslationsMapping } from '../../alerts_list/translations';

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

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

const getAlertStatusErrorReasonText = () => {
if (alert.executionStatus.error && alert.executionStatus.error.reason) {
return alertsErrorReasonTranslationsMapping[alert.executionStatus.error.reason];
} else {
return alertsErrorReasonTranslationsMapping.unknown;
}
};

return (
<EuiPage>
<EuiPageBody>
Expand Down Expand Up @@ -275,6 +286,30 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
{!dissmissAlertErrors && alert.executionStatus.status === 'error' ? (
<EuiFlexGroup>
<EuiFlexItem>
<EuiCallOut
color="danger"
data-test-subj="alertErrorBanner"
size="s"
title={getAlertStatusErrorReasonText()}
iconType="alert"
>
<EuiText size="s" color="danger" data-test-subj="alertErrorMessageText">
{alert.executionStatus.error?.message}
</EuiText>
<EuiSpacer size="s" />
<EuiButton color="danger" onClick={() => setDissmissAlertErrors(true)}>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertDetails.dismissButtonTitle"
defaultMessage="Dismiss"
/>
</EuiButton>
</EuiCallOut>
</EuiFlexItem>
</EuiFlexGroup>
) : null}
<EuiFlexGroup>
<EuiFlexItem>
{alert.enabled ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFilterGroup,
EuiPopover,
EuiFilterButton,
EuiFilterSelectItem,
EuiHealth,
} from '@elastic/eui';
import {
AlertExecutionStatuses,
AlertExecutionStatusValues,
} from '../../../../../../alerts/common';
import { alertsStatusesTranslationsMapping } from '../translations';

interface AlertStatusFilterProps {
selectedStatuses: string[];
onChange?: (selectedAlertStatusesIds: string[]) => void;
}

export const AlertStatusFilter: React.FunctionComponent<AlertStatusFilterProps> = ({
selectedStatuses,
onChange,
}: AlertStatusFilterProps) => {
const [selectedValues, setSelectedValues] = useState<string[]>(selectedStatuses);
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);

useEffect(() => {
if (onChange) {
onChange(selectedValues);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedValues]);

useEffect(() => {
setSelectedValues(selectedStatuses);
}, [selectedStatuses]);

return (
<EuiFilterGroup>
<EuiPopover
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
button={
<EuiFilterButton
iconType="arrowDown"
hasActiveFilters={selectedValues.length > 0}
numActiveFilters={selectedValues.length}
numFilters={selectedValues.length}
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertsList.alertStatusFilterLabel"
defaultMessage="Status"
/>
</EuiFilterButton>
}
>
<div className="euiFilterSelect__items">
{[...AlertExecutionStatusValues].sort().map((item: AlertExecutionStatuses) => {
const healthColor = getHealthColor(item);
return (
<EuiFilterSelectItem
key={item}
style={{ textTransform: 'capitalize' }}
onClick={() => {
const isPreviouslyChecked = selectedValues.includes(item);
if (isPreviouslyChecked) {
setSelectedValues(selectedValues.filter((val) => val !== item));
} else {
setSelectedValues(selectedValues.concat(item));
}
}}
checked={selectedValues.includes(item) ? 'on' : undefined}
>
<EuiHealth color={healthColor}>{alertsStatusesTranslationsMapping[item]}</EuiHealth>
</EuiFilterSelectItem>
);
})}
</div>
</EuiPopover>
</EuiFilterGroup>
);
};

export function getHealthColor(status: AlertExecutionStatuses) {
switch (status) {
case 'active':
return 'primary';
case 'error':
return 'danger';
case 'ok':
return 'subdued';
case 'pending':
return 'success';
default:
return 'warning';
}
}
Loading

0 comments on commit 83ebceb

Please sign in to comment.