Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SIEM][Exceptions] Add success/error toast component on alert state change #67406

Merged
merged 7 commits into from
Jun 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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> =>
spong marked this conversation as resolved.
Show resolved Hide resolved
KibanaServices.get().http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, {
method: 'POST',
body: JSON.stringify({ status, ...query }),
Expand Down