From bff539c1cd44af866b77583890971c6415ee43ca Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Wed, 3 Jun 2020 10:14:01 -0600 Subject: [PATCH] [SIEM][Exceptions] Add success/error toast component on alert state change (#67406) (#68095) ## Summary Adds a success and error toast on open or close of alert(s). #65947 Screen Shot 2020-05-26 at 2 28 57 PM Screen Shot 2020-05-28 at 12 01 03 PM ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) Co-authored-by: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> --- .../alerts/components/signals/actions.tsx | 10 ++++-- .../signals/default_config.test.tsx | 15 ++++++++ .../components/signals/default_config.tsx | 6 ++++ .../alerts/components/signals/index.tsx | 35 +++++++++++++++++++ .../alerts/components/signals/translations.ts | 28 +++++++++++++++ .../public/alerts/components/signals/types.ts | 2 ++ .../detection_engine/signals/api.ts | 3 +- 7 files changed, 95 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/siem/public/alerts/components/signals/actions.tsx b/x-pack/plugins/siem/public/alerts/components/signals/actions.tsx index c13e064bd1c3c..f01b39ccaba0d 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/actions.tsx +++ b/x-pack/plugins/siem/public/alerts/components/signals/actions.tsx @@ -56,17 +56,21 @@ export const updateSignalStatusAction = async ({ status, setEventsLoading, setEventsDeleted, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }: UpdateSignalStatusActionProps) => { try { setEventsLoading({ eventIds: signalIds, isLoading: true }); const queryObject = query ? { query: JSON.parse(query) } : getUpdateSignalsQuery(signalIds); - await updateSignalStatus({ query: queryObject, status }); + const response = await updateSignalStatus({ query: queryObject, status }); // TODO: Only delete those that were successfully updated from updatedRules setEventsDeleted({ eventIds: signalIds, isDeleted: true }); - } catch (e) { - // TODO: Show error toasts + + onAlertStatusUpdateSuccess(response.updated, status); + } catch (error) { + onAlertStatusUpdateFailure(status, error); } finally { setEventsLoading({ eventIds: signalIds, isLoading: false }); } diff --git a/x-pack/plugins/siem/public/alerts/components/signals/default_config.test.tsx b/x-pack/plugins/siem/public/alerts/components/signals/default_config.test.tsx index 71da68108da7e..7821bfaaf9575 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/default_config.test.tsx +++ b/x-pack/plugins/siem/public/alerts/components/signals/default_config.test.tsx @@ -54,11 +54,16 @@ describe('signals default_config', () => { let createTimeline: CreateTimeline; let updateTimelineIsLoading: UpdateTimelineLoading; + let onAlertStatusUpdateSuccess: (count: number, status: string) => void; + let onAlertStatusUpdateFailure: (status: string, error: Error) => void; + beforeEach(() => { setEventsLoading = jest.fn(); setEventsDeleted = jest.fn(); createTimeline = jest.fn(); updateTimelineIsLoading = jest.fn(); + onAlertStatusUpdateSuccess = jest.fn(); + onAlertStatusUpdateFailure = jest.fn(); }); describe('timeline tooltip', () => { @@ -71,6 +76,8 @@ describe('signals default_config', () => { createTimeline, status: 'open', updateTimelineIsLoading, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }); const timelineAction = signalsActions[0].getAction({ eventId: 'even-id', @@ -97,6 +104,8 @@ describe('signals default_config', () => { createTimeline, status: 'open', updateTimelineIsLoading, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }); signalOpenAction = signalsActions[1].getAction({ @@ -119,6 +128,8 @@ describe('signals default_config', () => { status: 'open', setEventsLoading, setEventsDeleted, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }); }); @@ -151,6 +162,8 @@ describe('signals default_config', () => { createTimeline, status: 'closed', updateTimelineIsLoading, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }); signalCloseAction = signalsActions[1].getAction({ @@ -173,6 +186,8 @@ describe('signals default_config', () => { status: 'closed', setEventsLoading, setEventsDeleted, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }); }); diff --git a/x-pack/plugins/siem/public/alerts/components/signals/default_config.tsx b/x-pack/plugins/siem/public/alerts/components/signals/default_config.tsx index 05e0baba66d0a..1269f31064e9e 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/default_config.tsx +++ b/x-pack/plugins/siem/public/alerts/components/signals/default_config.tsx @@ -198,6 +198,8 @@ export const getSignalsActions = ({ createTimeline, status, updateTimelineIsLoading, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }: { apolloClient?: ApolloClient<{}>; canUserCRUD: boolean; @@ -207,6 +209,8 @@ export const getSignalsActions = ({ createTimeline: CreateTimeline; status: 'open' | 'closed'; updateTimelineIsLoading: UpdateTimelineLoading; + onAlertStatusUpdateSuccess: (count: number, status: string) => void; + onAlertStatusUpdateFailure: (status: string, error: Error) => void; }): TimelineAction[] => [ { getAction: ({ ecsData }: TimelineActionProps): JSX.Element => ( @@ -246,6 +250,8 @@ export const getSignalsActions = ({ status, setEventsLoading, setEventsDeleted, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }) } isDisabled={!canUserCRUD || !hasIndexWrite} diff --git a/x-pack/plugins/siem/public/alerts/components/signals/index.tsx b/x-pack/plugins/siem/public/alerts/components/signals/index.tsx index eb19cfea97324..effb6a2450dc2 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/index.tsx +++ b/x-pack/plugins/siem/public/alerts/components/signals/index.tsx @@ -46,6 +46,11 @@ import { UpdateSignalsStatusProps, } from './types'; import { dispatchUpdateTimeline } from '../../../timelines/components/open_timeline/helpers'; +import { + useStateToaster, + displaySuccessToast, + displayErrorToast, +} from '../../../common/components/toasters'; export const SIGNALS_PAGE_TIMELINE_ID = 'signals-page'; @@ -91,6 +96,7 @@ export const SignalsTableComponent: React.FC = ({ signalsIndex !== '' ? [signalsIndex] : [] ); const kibana = useKibana(); + const [, dispatchToaster] = useStateToaster(); const getGlobalQuery = useCallback(() => { if (browserFields != null && indexPatterns != null) { @@ -146,6 +152,27 @@ export const SignalsTableComponent: React.FC = ({ [setEventsDeleted, SIGNALS_PAGE_TIMELINE_ID] ); + const onAlertStatusUpdateSuccess = useCallback( + (count: number, status: string) => { + const title = + status === 'closed' + ? i18n.CLOSED_ALERT_SUCCESS_TOAST(count) + : i18n.OPENED_ALERT_SUCCESS_TOAST(count); + + displaySuccessToast(title, dispatchToaster); + }, + [dispatchToaster] + ); + + const onAlertStatusUpdateFailure = useCallback( + (status: string, error: Error) => { + const title = + status === 'closed' ? i18n.CLOSED_ALERT_FAILED_TOAST : i18n.OPENED_ALERT_FAILED_TOAST; + displayErrorToast(title, [error.message], dispatchToaster); + }, + [dispatchToaster] + ); + // Catches state change isSelectAllChecked->false upon user selection change to reset utility bar useEffect(() => { if (!isSelectAllChecked) { @@ -189,6 +216,8 @@ export const SignalsTableComponent: React.FC = ({ status, setEventsDeleted: setEventsDeletedCallback, setEventsLoading: setEventsLoadingCallback, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }); refetchQuery(); }, @@ -198,6 +227,8 @@ export const SignalsTableComponent: React.FC = ({ setEventsDeletedCallback, setEventsLoadingCallback, showClearSelectionAction, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, ] ); @@ -244,6 +275,8 @@ export const SignalsTableComponent: React.FC = ({ setEventsDeleted: setEventsDeletedCallback, status: filterGroup === FILTER_OPEN ? FILTER_CLOSED : FILTER_OPEN, updateTimelineIsLoading, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }), [ apolloClient, @@ -254,6 +287,8 @@ export const SignalsTableComponent: React.FC = ({ setEventsLoadingCallback, setEventsDeletedCallback, updateTimelineIsLoading, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, ] ); diff --git a/x-pack/plugins/siem/public/alerts/components/signals/translations.ts b/x-pack/plugins/siem/public/alerts/components/signals/translations.ts index f68dcd932bc32..e49ff9846b9b7 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/translations.ts +++ b/x-pack/plugins/siem/public/alerts/components/signals/translations.ts @@ -101,3 +101,31 @@ export const ACTION_INVESTIGATE_IN_TIMELINE = i18n.translate( defaultMessage: 'Investigate in timeline', } ); + +export const CLOSED_ALERT_SUCCESS_TOAST = (totalAlerts: number) => + i18n.translate('xpack.siem.detectionEngine.signals.closedAlertSuccessToastMessage', { + values: { totalAlerts }, + defaultMessage: + 'Successfully closed {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.', + }); + +export const OPENED_ALERT_SUCCESS_TOAST = (totalAlerts: number) => + i18n.translate('xpack.siem.detectionEngine.signals.openedAlertSuccessToastMessage', { + values: { totalAlerts }, + defaultMessage: + 'Successfully opened {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.', + }); + +export const CLOSED_ALERT_FAILED_TOAST = i18n.translate( + 'xpack.siem.detectionEngine.signals.closedAlertFailedToastMessage', + { + defaultMessage: 'Failed to close alert(s).', + } +); + +export const OPENED_ALERT_FAILED_TOAST = i18n.translate( + 'xpack.siem.detectionEngine.signals.openedAlertFailedToastMessage', + { + defaultMessage: 'Failed to open alert(s)', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/signals/types.ts b/x-pack/plugins/siem/public/alerts/components/signals/types.ts index b3c770415ed57..542aa61074ce1 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/types.ts +++ b/x-pack/plugins/siem/public/alerts/components/signals/types.ts @@ -37,6 +37,8 @@ export interface UpdateSignalStatusActionProps { status: 'open' | 'closed'; setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; + onAlertStatusUpdateSuccess: (count: number, status: string) => void; + onAlertStatusUpdateFailure: (status: string, error: Error) => void; } export type SendSignalsToTimeline = () => void; diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.ts index 860305dd58e67..445f66457c59e 100644 --- a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.ts +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ReindexResponse } from 'elasticsearch'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL, DETECTION_ENGINE_SIGNALS_STATUS_URL, @@ -54,7 +55,7 @@ export const updateSignalStatus = async ({ query, status, signal, -}: UpdateSignalStatusProps): Promise => +}: UpdateSignalStatusProps): Promise => KibanaServices.get().http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { method: 'POST', body: JSON.stringify({ status, ...query }),