Skip to content

Commit

Permalink
[Alerting UI] Added visual indicator when enable switched click is pr…
Browse files Browse the repository at this point in the history
…ocessed on the server side. (elastic#107272)

* [Alerting UI] Added visual indicator when enable switched click is processed on the server side.

* fixed rule details

* fixed functional tests

* fixed unit tests

* fixed due to comments

* fixed due to comments
  • Loading branch information
YulNaumenko authored and vadimkibana committed Aug 8, 2021
1 parent d77bcc3 commit 32a7e1b
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,11 @@ describe('disable button', () => {
});
expect(disableAlert).toHaveBeenCalled();

await act(async () => {
await nextTick();
wrapper.update();
});

// Enable the alert
await act(async () => {
wrapper.find('[data-test-subj="enableSwitch"] .euiSwitch__button').first().simulate('click');
Expand All @@ -546,6 +551,77 @@ describe('disable button', () => {
// Ensure error banner is back
expect(wrapper.find('[data-test-subj="dismiss-execution-error"]').length).toBeGreaterThan(0);
});

it('should show the loading spinner when the rule enabled switch was clicked and the server responded with some delay', async () => {
const alert = mockAlert({
enabled: true,
executionStatus: {
status: 'error',
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
error: {
reason: AlertExecutionStatusErrorReasons.Execute,
message: 'Fail',
},
},
});

const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
};

const disableAlert = jest.fn(async () => {
await new Promise((resolve) => setTimeout(resolve, 6000));
});
const enableAlert = jest.fn();
const wrapper = mountWithIntl(
<AlertDetails
alert={alert}
alertType={alertType}
actionTypes={[]}
{...mockAlertApis}
disableAlert={disableAlert}
enableAlert={enableAlert}
/>
);

await act(async () => {
await nextTick();
wrapper.update();
});

// Dismiss the error banner
await act(async () => {
wrapper.find('[data-test-subj="dismiss-execution-error"]').first().simulate('click');
await nextTick();
});

// Disable the alert
await act(async () => {
wrapper.find('[data-test-subj="enableSwitch"] .euiSwitch__button').first().simulate('click');
await nextTick();
});
expect(disableAlert).toHaveBeenCalled();

await act(async () => {
await nextTick();
wrapper.update();
});

// Enable the alert
await act(async () => {
expect(wrapper.find('[data-test-subj="enableSpinner"]').length).toBeGreaterThan(0);
await nextTick();
});
});
});

describe('mute button', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
EuiSpacer,
EuiButtonEmpty,
EuiButton,
EuiLoadingSpinner,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { AlertExecutionStatusErrorReasons } from '../../../../../../alerting/common';
Expand Down Expand Up @@ -99,6 +100,8 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
const alertActions = alert.actions;
const uniqueActions = Array.from(new Set(alertActions.map((item: any) => item.actionTypeId)));
const [isEnabled, setIsEnabled] = useState<boolean>(alert.enabled);
const [isEnabledUpdating, setIsEnabledUpdating] = useState<boolean>(false);
const [isMutedUpdating, setIsMutedUpdating] = useState<boolean>(false);
const [isMuted, setIsMuted] = useState<boolean>(alert.muteAll);
const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false);
const [dissmissAlertErrors, setDissmissAlertErrors] = useState<boolean>(false);
Expand Down Expand Up @@ -218,54 +221,92 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
<EuiSpacer />
<EuiFlexGroup justifyContent="flexEnd" wrap responsive={false} gutterSize="m">
<EuiFlexItem grow={false}>
<EuiSwitch
name="enable"
disabled={!canSaveAlert || !alertType.enabledInLicense}
checked={isEnabled}
data-test-subj="enableSwitch"
onChange={async () => {
if (isEnabled) {
setIsEnabled(false);
await disableAlert(alert);
// Reset dismiss if previously clicked
setDissmissAlertErrors(false);
} else {
setIsEnabled(true);
await enableAlert(alert);
{isEnabledUpdating ? (
<EuiFlexGroup>
<EuiFlexItem>
<EuiLoadingSpinner data-test-subj="enableSpinner" size="m" />
</EuiFlexItem>

<EuiFlexItem>
<EuiText size="s">
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.enableLoadingTitle"
defaultMessage="Enable"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<EuiSwitch
name="enable"
disabled={!canSaveAlert || !alertType.enabledInLicense}
checked={isEnabled}
data-test-subj="enableSwitch"
onChange={async () => {
setIsEnabledUpdating(true);
if (isEnabled) {
setIsEnabled(false);
await disableAlert(alert);
// Reset dismiss if previously clicked
setDissmissAlertErrors(false);
} else {
setIsEnabled(true);
await enableAlert(alert);
}
requestRefresh();
setIsEnabledUpdating(false);
}}
label={
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.enableTitle"
defaultMessage="Enable"
/>
}
requestRefresh();
}}
label={
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.enableTitle"
defaultMessage="Enable"
/>
}
/>
/>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSwitch
name="mute"
checked={isMuted}
disabled={!canSaveAlert || !isEnabled || !alertType.enabledInLicense}
data-test-subj="muteSwitch"
onChange={async () => {
if (isMuted) {
setIsMuted(false);
await unmuteAlert(alert);
} else {
setIsMuted(true);
await muteAlert(alert);
{isMutedUpdating ? (
<EuiFlexGroup>
<EuiFlexItem>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>

<EuiFlexItem>
<EuiText size="s">
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.muteLoadingTitle"
defaultMessage="Mute"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<EuiSwitch
name="mute"
checked={isMuted}
disabled={!canSaveAlert || !isEnabled || !alertType.enabledInLicense}
data-test-subj="muteSwitch"
onChange={async () => {
setIsMutedUpdating(true);
if (isMuted) {
setIsMuted(false);
await unmuteAlert(alert);
} else {
setIsMuted(true);
await muteAlert(alert);
}
requestRefresh();
setIsMutedUpdating(false);
}}
label={
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.muteTitle"
defaultMessage="Mute"
/>
}
requestRefresh();
}}
label={
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.muteTitle"
defaultMessage="Mute"
/>
}
/>
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import React, { useState } from 'react';
import moment, { Duration } from 'moment';
import { i18n } from '@kbn/i18n';
import { EuiBasicTable, EuiHealth, EuiSpacer, EuiSwitch, EuiToolTip } from '@elastic/eui';
import { EuiBasicTable, EuiHealth, EuiSpacer, EuiToolTip } from '@elastic/eui';
// @ts-ignore
import { RIGHT_ALIGNMENT, CENTER_ALIGNMENT } from '@elastic/eui/lib/services';
import { padStart, chunk } from 'lodash';
Expand All @@ -26,6 +26,7 @@ import {
} from '../../common/components/with_bulk_alert_api_operations';
import { DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants';
import './alert_instances.scss';
import { RuleMutedSwitch } from './rule_muted_switch';

type AlertInstancesProps = {
alert: Alert;
Expand Down Expand Up @@ -112,17 +113,11 @@ export const alertInstancesTableColumns = (
),
render: (alertInstance: AlertInstanceListItem) => {
return (
<>
<EuiSwitch
label="mute"
showLabel={false}
compressed={true}
checked={alertInstance.isMuted}
disabled={readOnly}
data-test-subj={`muteAlertInstanceButton_${alertInstance.instance}`}
onChange={() => onMuteAction(alertInstance)}
/>
</>
<RuleMutedSwitch
disabled={readOnly}
onMuteAction={async () => await onMuteAction(alertInstance)}
alertInstance={alertInstance}
/>
);
},
sortable: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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, { useState } from 'react';
import { EuiSwitch, EuiLoadingSpinner } from '@elastic/eui';

import { AlertInstanceListItem } from './alert_instances';

interface ComponentOpts {
alertInstance: AlertInstanceListItem;
onMuteAction: (instance: AlertInstanceListItem) => Promise<void>;
disabled: boolean;
}

export const RuleMutedSwitch: React.FunctionComponent<ComponentOpts> = ({
alertInstance,
onMuteAction,
disabled,
}: ComponentOpts) => {
const [isMuted, setIsMuted] = useState<boolean>(alertInstance?.isMuted);
const [isUpdating, setIsUpdating] = useState<boolean>(false);

return isUpdating ? (
<EuiLoadingSpinner size="m" />
) : (
<EuiSwitch
name="mute"
disabled={disabled}
compressed={true}
checked={isMuted}
onChange={async () => {
setIsUpdating(true);
await onMuteAction(alertInstance);
setIsMuted(!isMuted);
setIsUpdating(false);
}}
data-test-subj={`muteAlertInstanceButton_${alertInstance.instance}`}
showLabel={false}
label="mute"
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
* 2.0.
*/

import { asyncScheduler } from 'rxjs';
import React, { useEffect, useState } from 'react';
import { EuiSwitch } from '@elastic/eui';
import React, { useState, useEffect } from 'react';
import { EuiSwitch, EuiLoadingSpinner } from '@elastic/eui';

import { Alert, AlertTableItem } from '../../../../types';

Expand All @@ -28,25 +27,28 @@ export const RuleEnabledSwitch: React.FunctionComponent<ComponentOpts> = ({
useEffect(() => {
setIsEnabled(item.enabled);
}, [item.enabled]);
const [isUpdating, setIsUpdating] = useState<boolean>(false);

return (
return isUpdating ? (
<EuiLoadingSpinner data-test-subj="enableSpinner" size="m" />
) : (
<EuiSwitch
name="enable"
disabled={!item.isEditable || !item.enabledInLicense}
compressed
checked={isEnabled}
data-test-subj="enableSwitch"
onChange={async () => {
const enabled = isEnabled;
asyncScheduler.schedule(async () => {
if (enabled) {
await disableAlert({ ...item, enabled });
} else {
await enableAlert({ ...item, enabled });
}
onAlertChanged();
}, 10);
setIsUpdating(true);
const enabled = item.enabled;
if (enabled) {
await disableAlert({ ...item, enabled });
} else {
await enableAlert({ ...item, enabled });
}
setIsEnabled(!isEnabled);
setIsUpdating(false);
onAlertChanged();
}}
label=""
/>
Expand Down

0 comments on commit 32a7e1b

Please sign in to comment.