From ac81d4aef21651f182f496f6e008902c34902a07 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Fri, 30 Jul 2021 12:58:09 +0300 Subject: [PATCH 1/6] [Alerting UI] Added visual indicator when enable switched click is processed on the server side. --- .../components/rule_enabled_switch.tsx | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx index c031f189ffa4d..073c5664eb4cc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import { asyncScheduler } from 'rxjs'; -import React, { useEffect, useState } from 'react'; -import { EuiSwitch } from '@elastic/eui'; +import React, { useState } from 'react'; +import { EuiSwitch, EuiLoadingSpinner } from '@elastic/eui'; import { Alert, AlertTableItem } from '../../../../types'; @@ -24,12 +23,12 @@ export const RuleEnabledSwitch: React.FunctionComponent = ({ disableAlert, enableAlert, }: ComponentOpts) => { - const [isEnabled, setIsEnabled] = useState(!item.enabled); - useEffect(() => { - setIsEnabled(item.enabled); - }, [item.enabled]); + const [isEnabled, setIsEnabled] = useState(item.enabled); + const [isUpdating, setIsUpdating] = useState(false); - return ( + const res = isUpdating ? ( + + ) : ( = ({ 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="" /> ); + return res; }; From 96a9788df1f835052e77a9c30d5715d01fa21ca1 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Mon, 2 Aug 2021 21:57:16 -0700 Subject: [PATCH 2/6] fixed rule details --- .../components/alert_details.tsx | 129 ++++++++++++------ .../components/alert_instances.tsx | 17 +-- .../components/rule_muted_switch.tsx | 47 +++++++ 3 files changed, 138 insertions(+), 55 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 1328ba6479f68..2558993a13fe6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -21,6 +21,7 @@ import { EuiSpacer, EuiButtonEmpty, EuiButton, + EuiLoadingSpinner, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { AlertExecutionStatusErrorReasons } from '../../../../../../alerting/common'; @@ -99,6 +100,8 @@ export const AlertDetails: React.FunctionComponent = ({ const alertActions = alert.actions; const uniqueActions = Array.from(new Set(alertActions.map((item: any) => item.actionTypeId))); const [isEnabled, setIsEnabled] = useState(alert.enabled); + const [isEnabledUpdating, setIsEnabledUpdating] = useState(false); + const [isMutedUpdating, setIsMutedUpdating] = useState(false); const [isMuted, setIsMuted] = useState(alert.muteAll); const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); const [dissmissAlertErrors, setDissmissAlertErrors] = useState(false); @@ -218,54 +221,92 @@ export const AlertDetails: React.FunctionComponent = ({ - { - if (isEnabled) { - setIsEnabled(false); - await disableAlert(alert); - // Reset dismiss if previously clicked - setDissmissAlertErrors(false); - } else { - setIsEnabled(true); - await enableAlert(alert); + {isEnabledUpdating ? ( + + + + + + + + + + + + ) : ( + { + 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={ + } - requestRefresh(); - }} - label={ - - } - /> + /> + )} - { - if (isMuted) { - setIsMuted(false); - await unmuteAlert(alert); - } else { - setIsMuted(true); - await muteAlert(alert); + {isMutedUpdating ? ( + + + + + + + + + + + + ) : ( + { + setIsMutedUpdating(true); + if (isMuted) { + setIsMuted(false); + await unmuteAlert(alert); + } else { + setIsMuted(true); + await muteAlert(alert); + } + requestRefresh(); + setIsMutedUpdating(false); + }} + label={ + } - requestRefresh(); - }} - label={ - - } - /> + /> + )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx index 29290af0d0285..70ad41c47b768 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx @@ -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; @@ -112,17 +113,11 @@ export const alertInstancesTableColumns = ( ), render: (alertInstance: AlertInstanceListItem) => { return ( - <> - onMuteAction(alertInstance)} - /> - + await onMuteAction(alertInstance)} + alertInstance={alertInstance} + /> ); }, sortable: false, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx new file mode 100644 index 0000000000000..6c17ba1ce52bf --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx @@ -0,0 +1,47 @@ +/* + * 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; + disabled: boolean; +} + +export const RuleMutedSwitch: React.FunctionComponent = ({ + alertInstance, + onMuteAction, + disabled, +}: ComponentOpts) => { + const [isMuted, setIsMuted] = useState(alertInstance?.isMuted); + const [isUpdating, setIsUpdating] = useState(false); + + const res = isUpdating ? ( + + ) : ( + { + setIsUpdating(true); + await onMuteAction(alertInstance); + setIsMuted(!isMuted); + setIsUpdating(false); + }} + data-test-subj={`muteAlertInstanceButton_${alertInstance.instance}`} + showLabel={false} + label="mute" + /> + ); + return res; +}; From 0abcd2565171df3fced020479e497625c4a8ccb7 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Tue, 3 Aug 2021 08:47:26 -0700 Subject: [PATCH 3/6] fixed functional tests --- .../sections/alert_details/components/alert_instances.tsx | 2 +- .../alerts_list/components/rule_enabled_switch.tsx | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx index 70ad41c47b768..1583cb188f1c1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx @@ -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'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx index 073c5664eb4cc..e458a495c8ec2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { EuiSwitch, EuiLoadingSpinner } from '@elastic/eui'; import { Alert, AlertTableItem } from '../../../../types'; @@ -23,7 +23,10 @@ export const RuleEnabledSwitch: React.FunctionComponent = ({ disableAlert, enableAlert, }: ComponentOpts) => { - const [isEnabled, setIsEnabled] = useState(item.enabled); + const [isEnabled, setIsEnabled] = useState(!item.enabled); + useEffect(() => { + setIsEnabled(item.enabled); + }, [item.enabled]); const [isUpdating, setIsUpdating] = useState(false); const res = isUpdating ? ( From fd04ea2a55aeef642bc2dad1ddb5ee3d4d20d653 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Tue, 3 Aug 2021 14:21:06 -0700 Subject: [PATCH 4/6] fixed unit tests --- .../components/alert_details.test.tsx | 76 +++++++++++++++++++ .../components/rule_enabled_switch.tsx | 2 +- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index 5d4237e2176a0..d198c82366fbf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -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'); @@ -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( + + ); + + 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', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx index e458a495c8ec2..f5b981a9d78d7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx @@ -30,7 +30,7 @@ export const RuleEnabledSwitch: React.FunctionComponent = ({ const [isUpdating, setIsUpdating] = useState(false); const res = isUpdating ? ( - + ) : ( Date: Wed, 4 Aug 2021 11:20:13 -0700 Subject: [PATCH 5/6] fixed due to comments --- .../sections/alert_details/components/rule_muted_switch.tsx | 2 +- .../sections/alerts_list/components/rule_enabled_switch.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx index 6c17ba1ce52bf..c1b4a983ce2ad 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx @@ -28,7 +28,7 @@ export const RuleMutedSwitch: React.FunctionComponent = ({ ) : ( = ({ }, [item.enabled]); const [isUpdating, setIsUpdating] = useState(false); - const res = isUpdating ? ( + return isUpdating ? ( ) : ( = ({ label="" /> ); - return res; }; From d386f02fe13f6b7224cf50417da8d37736dd9871 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Wed, 4 Aug 2021 11:23:38 -0700 Subject: [PATCH 6/6] fixed due to comments --- .../sections/alert_details/components/rule_muted_switch.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx index c1b4a983ce2ad..bee0c8aef706d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx @@ -24,7 +24,7 @@ export const RuleMutedSwitch: React.FunctionComponent = ({ const [isMuted, setIsMuted] = useState(alertInstance?.isMuted); const [isUpdating, setIsUpdating] = useState(false); - const res = isUpdating ? ( + return isUpdating ? ( ) : ( = ({ label="mute" /> ); - return res; };