Skip to content

Commit

Permalink
[SIEM][Exceptions] Add success/error toast component on alert state c…
Browse files Browse the repository at this point in the history
…hange (elastic#67406) (elastic#68095)

## Summary

Adds a success and error toast on open or close of alert(s). elastic#65947

<img width="1035" alt="Screen Shot 2020-05-26 at 2 28 57 PM" src="https://user-images.githubusercontent.com/56367316/82947446-5086ae00-9f5d-11ea-848a-9edae48c47dc.png">

<img width="1022" alt="Screen Shot 2020-05-28 at 12 01 03 PM" src="https://user-images.githubusercontent.com/56367316/83176760-3f63ab80-a0db-11ea-9f4d-7b745d0b4bf0.png">


### 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 <[email protected]>
  • Loading branch information
spong and dplumlee authored Jun 3, 2020
1 parent 1fca142 commit bff539c
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 4 deletions.
10 changes: 7 additions & 3 deletions x-pack/plugins/siem/public/alerts/components/signals/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -71,6 +76,8 @@ describe('signals default_config', () => {
createTimeline,
status: 'open',
updateTimelineIsLoading,
onAlertStatusUpdateSuccess,
onAlertStatusUpdateFailure,
});
const timelineAction = signalsActions[0].getAction({
eventId: 'even-id',
Expand All @@ -97,6 +104,8 @@ describe('signals default_config', () => {
createTimeline,
status: 'open',
updateTimelineIsLoading,
onAlertStatusUpdateSuccess,
onAlertStatusUpdateFailure,
});

signalOpenAction = signalsActions[1].getAction({
Expand All @@ -119,6 +128,8 @@ describe('signals default_config', () => {
status: 'open',
setEventsLoading,
setEventsDeleted,
onAlertStatusUpdateSuccess,
onAlertStatusUpdateFailure,
});
});

Expand Down Expand Up @@ -151,6 +162,8 @@ describe('signals default_config', () => {
createTimeline,
status: 'closed',
updateTimelineIsLoading,
onAlertStatusUpdateSuccess,
onAlertStatusUpdateFailure,
});

signalCloseAction = signalsActions[1].getAction({
Expand All @@ -173,6 +186,8 @@ describe('signals default_config', () => {
status: 'closed',
setEventsLoading,
setEventsDeleted,
onAlertStatusUpdateSuccess,
onAlertStatusUpdateFailure,
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ export const getSignalsActions = ({
createTimeline,
status,
updateTimelineIsLoading,
onAlertStatusUpdateSuccess,
onAlertStatusUpdateFailure,
}: {
apolloClient?: ApolloClient<{}>;
canUserCRUD: boolean;
Expand All @@ -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 => (
Expand Down Expand Up @@ -246,6 +250,8 @@ export const getSignalsActions = ({
status,
setEventsLoading,
setEventsDeleted,
onAlertStatusUpdateSuccess,
onAlertStatusUpdateFailure,
})
}
isDisabled={!canUserCRUD || !hasIndexWrite}
Expand Down
35 changes: 35 additions & 0 deletions x-pack/plugins/siem/public/alerts/components/signals/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -91,6 +96,7 @@ export const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({
signalsIndex !== '' ? [signalsIndex] : []
);
const kibana = useKibana();
const [, dispatchToaster] = useStateToaster();

const getGlobalQuery = useCallback(() => {
if (browserFields != null && indexPatterns != null) {
Expand Down Expand Up @@ -146,6 +152,27 @@ export const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({
[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) {
Expand Down Expand Up @@ -189,6 +216,8 @@ export const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({
status,
setEventsDeleted: setEventsDeletedCallback,
setEventsLoading: setEventsLoadingCallback,
onAlertStatusUpdateSuccess,
onAlertStatusUpdateFailure,
});
refetchQuery();
},
Expand All @@ -198,6 +227,8 @@ export const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({
setEventsDeletedCallback,
setEventsLoadingCallback,
showClearSelectionAction,
onAlertStatusUpdateSuccess,
onAlertStatusUpdateFailure,
]
);

Expand Down Expand Up @@ -244,6 +275,8 @@ export const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({
setEventsDeleted: setEventsDeletedCallback,
status: filterGroup === FILTER_OPEN ? FILTER_CLOSED : FILTER_OPEN,
updateTimelineIsLoading,
onAlertStatusUpdateSuccess,
onAlertStatusUpdateFailure,
}),
[
apolloClient,
Expand All @@ -254,6 +287,8 @@ export const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({
setEventsLoadingCallback,
setEventsDeletedCallback,
updateTimelineIsLoading,
onAlertStatusUpdateSuccess,
onAlertStatusUpdateFailure,
]
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
}
);
2 changes: 2 additions & 0 deletions x-pack/plugins/siem/public/alerts/components/signals/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -54,7 +55,7 @@ export const updateSignalStatus = async ({
query,
status,
signal,
}: UpdateSignalStatusProps): Promise<unknown> =>
}: UpdateSignalStatusProps): Promise<ReindexResponse> =>
KibanaServices.get().http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, {
method: 'POST',
body: JSON.stringify({ status, ...query }),
Expand Down

0 comments on commit bff539c

Please sign in to comment.