[Security Solution][Case] Attach/Sync alert to case: Fix toast messages (#87206)
This commit is contained in:
parent
34a3982f3a
commit
8ead390813
|
@ -180,7 +180,7 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
updateKey: 'title',
|
||||
updateValue: titleUpdate,
|
||||
updateCase: handleUpdateNewCase,
|
||||
version: caseData.version,
|
||||
caseData,
|
||||
onSuccess,
|
||||
onError,
|
||||
});
|
||||
|
@ -194,7 +194,7 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
updateKey: 'connector',
|
||||
updateValue: connector,
|
||||
updateCase: handleUpdateNewCase,
|
||||
version: caseData.version,
|
||||
caseData,
|
||||
onSuccess,
|
||||
onError,
|
||||
});
|
||||
|
@ -208,7 +208,7 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
updateKey: 'description',
|
||||
updateValue: descriptionUpdate,
|
||||
updateCase: handleUpdateNewCase,
|
||||
version: caseData.version,
|
||||
caseData,
|
||||
onSuccess,
|
||||
onError,
|
||||
});
|
||||
|
@ -221,7 +221,7 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
updateKey: 'tags',
|
||||
updateValue: tagsUpdate,
|
||||
updateCase: handleUpdateNewCase,
|
||||
version: caseData.version,
|
||||
caseData,
|
||||
onSuccess,
|
||||
onError,
|
||||
});
|
||||
|
@ -234,7 +234,7 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
updateKey: 'status',
|
||||
updateValue: statusUpdate,
|
||||
updateCase: handleUpdateNewCase,
|
||||
version: caseData.version,
|
||||
caseData,
|
||||
onSuccess,
|
||||
onError,
|
||||
});
|
||||
|
@ -248,7 +248,7 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
updateKey: 'settings',
|
||||
updateValue: settingsUpdate,
|
||||
updateCase: handleUpdateNewCase,
|
||||
version: caseData.version,
|
||||
caseData,
|
||||
onSuccess,
|
||||
onError,
|
||||
});
|
||||
|
|
|
@ -17,12 +17,13 @@ import {
|
|||
import { CommentType } from '../../../../../case/common/api';
|
||||
import { Ecs } from '../../../../common/ecs';
|
||||
import { ActionIconItem } from '../../../timelines/components/timeline/body/actions/action_icon_item';
|
||||
import * as i18n from './translations';
|
||||
import { usePostComment } from '../../containers/use_post_comment';
|
||||
import { Case } from '../../containers/types';
|
||||
import { displaySuccessToast, useStateToaster } from '../../../common/components/toasters';
|
||||
import { useStateToaster } from '../../../common/components/toasters';
|
||||
import { useCreateCaseModal } from '../use_create_case_modal';
|
||||
import { useAllCasesModal } from '../use_all_cases_modal';
|
||||
import { createUpdateSuccessToaster } from './helpers';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface AddToCaseActionProps {
|
||||
ariaLabel?: string;
|
||||
|
@ -53,7 +54,7 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
|
|||
alertId: eventId,
|
||||
index: eventIndex ?? '',
|
||||
},
|
||||
() => displaySuccessToast(i18n.CASE_CREATED_SUCCESS_TOAST(theCase.title), dispatchToaster)
|
||||
() => dispatchToaster({ type: 'addToaster', toast: createUpdateSuccessToaster(theCase) })
|
||||
);
|
||||
},
|
||||
[postComment, eventId, eventIndex, dispatchToaster]
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { createUpdateSuccessToaster } from './helpers';
|
||||
import { Case } from '../../containers/types';
|
||||
|
||||
const theCase = {
|
||||
title: 'My case',
|
||||
settings: {
|
||||
syncAlerts: true,
|
||||
},
|
||||
} as Case;
|
||||
|
||||
describe('helpers', () => {
|
||||
describe('createUpdateSuccessToaster', () => {
|
||||
it('creates the correct toast when the sync alerts is on', () => {
|
||||
// We remove the id as is randomly generated
|
||||
const { id, ...toast } = createUpdateSuccessToaster(theCase);
|
||||
expect(toast).toEqual({
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
text: 'Alerts in this case have their status synched with the case status',
|
||||
title: 'An alert has been added to "My case"',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates the correct toast when the sync alerts is off', () => {
|
||||
// We remove the id as is randomly generated
|
||||
const { id, ...toast } = createUpdateSuccessToaster({
|
||||
...theCase,
|
||||
settings: { syncAlerts: false },
|
||||
});
|
||||
expect(toast).toEqual({
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
title: 'An alert has been added to "My case"',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import uuid from 'uuid';
|
||||
import { AppToast } from '../../../common/components/toasters';
|
||||
import { Case } from '../../containers/types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const createUpdateSuccessToaster = (theCase: Case): AppToast => {
|
||||
const toast: AppToast = {
|
||||
id: uuid.v4(),
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
title: i18n.CASE_CREATED_SUCCESS_TOAST(theCase.title),
|
||||
};
|
||||
|
||||
if (theCase.settings.syncAlerts) {
|
||||
return { ...toast, text: i18n.CASE_CREATED_SUCCESS_TOAST_TEXT };
|
||||
}
|
||||
|
||||
return toast;
|
||||
};
|
|
@ -46,3 +46,10 @@ export const CASE_CREATED_SUCCESS_TOAST = (title: string) =>
|
|||
values: { title },
|
||||
defaultMessage: 'An alert has been added to "{title}"',
|
||||
});
|
||||
|
||||
export const CASE_CREATED_SUCCESS_TOAST_TEXT = i18n.translate(
|
||||
'xpack.securitySolution.case.timeline.actions.caseCreatedSuccessToastText',
|
||||
{
|
||||
defaultMessage: 'Alerts in this case have their status synched with the case status',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -156,7 +156,10 @@ export const postCase = async (newCase: CasePostRequest, signal: AbortSignal): P
|
|||
|
||||
export const patchCase = async (
|
||||
caseId: string,
|
||||
updatedCase: Pick<CasePatchRequest, 'description' | 'status' | 'tags' | 'title'>,
|
||||
updatedCase: Pick<
|
||||
CasePatchRequest,
|
||||
'description' | 'status' | 'tags' | 'title' | 'settings' | 'connector'
|
||||
>,
|
||||
version: string,
|
||||
signal: AbortSignal
|
||||
): Promise<Case[]> => {
|
||||
|
|
|
@ -74,3 +74,16 @@ export const ERROR_GET_FIELDS = i18n.translate(
|
|||
defaultMessage: 'Error getting fields from service',
|
||||
}
|
||||
);
|
||||
|
||||
export const SYNC_CASE = (caseTitle: string) =>
|
||||
i18n.translate('xpack.securitySolution.containers.case.syncCase', {
|
||||
values: { caseTitle },
|
||||
defaultMessage: 'Alerts in "{caseTitle}" have been synced',
|
||||
});
|
||||
|
||||
export const STATUS_CHANGED_TOASTER_TEXT = i18n.translate(
|
||||
'xpack.securitySolution.case.containers.statusChangeToasterText',
|
||||
{
|
||||
defaultMessage: 'Alerts in this case have been also had their status updated',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
CommentRequest,
|
||||
CaseStatuses,
|
||||
CaseAttributes,
|
||||
CasePatchRequest,
|
||||
} from '../../../../case/common/api';
|
||||
|
||||
export { CaseConnector, ActionConnector } from '../../../../case/common/api';
|
||||
|
@ -137,3 +138,18 @@ export interface FieldMappings {
|
|||
id: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export type UpdateKey = keyof Pick<
|
||||
CasePatchRequest,
|
||||
'connector' | 'description' | 'status' | 'tags' | 'title' | 'settings'
|
||||
>;
|
||||
|
||||
export interface UpdateByKey {
|
||||
updateKey: UpdateKey;
|
||||
updateValue: CasePatchRequest[UpdateKey];
|
||||
fetchCaseUserActions?: (caseId: string) => void;
|
||||
updateCase?: (newCase: Case) => void;
|
||||
caseData: Case;
|
||||
onSuccess?: () => void;
|
||||
onError?: () => void;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
useGetCases,
|
||||
UseGetCases,
|
||||
} from './use_get_cases';
|
||||
import { UpdateKey } from './use_update_case';
|
||||
import { UpdateKey } from './types';
|
||||
import { allCases, basicCase } from './mock';
|
||||
import * as api from './api';
|
||||
|
||||
|
|
|
@ -7,10 +7,9 @@
|
|||
import { useCallback, useEffect, useReducer } from 'react';
|
||||
import { CaseStatuses } from '../../../../case/common/api';
|
||||
import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from './constants';
|
||||
import { AllCases, SortFieldCase, FilterOptions, QueryParams, Case } from './types';
|
||||
import { AllCases, SortFieldCase, FilterOptions, QueryParams, Case, UpdateByKey } from './types';
|
||||
import { errorToToaster, useStateToaster } from '../../common/components/toasters';
|
||||
import * as i18n from './translations';
|
||||
import { UpdateByKey } from './use_update_case';
|
||||
import { getCases, patchCase } from './api';
|
||||
|
||||
export interface UseGetCasesState {
|
||||
|
@ -22,7 +21,7 @@ export interface UseGetCasesState {
|
|||
selectedCases: Case[];
|
||||
}
|
||||
|
||||
export interface UpdateCase extends UpdateByKey {
|
||||
export interface UpdateCase extends Omit<UpdateByKey, 'caseData'> {
|
||||
caseId: string;
|
||||
version: string;
|
||||
refetchCasesStatus: () => void;
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
*/
|
||||
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useUpdateCase, UseUpdateCase, UpdateKey } from './use_update_case';
|
||||
import { useUpdateCase, UseUpdateCase } from './use_update_case';
|
||||
import { basicCase } from './mock';
|
||||
import * as api from './api';
|
||||
import { UpdateKey } from './types';
|
||||
|
||||
jest.mock('./api');
|
||||
|
||||
|
@ -24,7 +25,7 @@ describe('useUpdateCase', () => {
|
|||
updateKey,
|
||||
updateValue: 'updated description',
|
||||
updateCase,
|
||||
version: basicCase.version,
|
||||
caseData: basicCase,
|
||||
onSuccess,
|
||||
onError,
|
||||
};
|
||||
|
|
|
@ -6,21 +6,12 @@
|
|||
|
||||
import { useReducer, useCallback } from 'react';
|
||||
|
||||
import {
|
||||
displaySuccessToast,
|
||||
errorToToaster,
|
||||
useStateToaster,
|
||||
} from '../../common/components/toasters';
|
||||
import { CasePatchRequest } from '../../../../case/common/api';
|
||||
import { errorToToaster, useStateToaster } from '../../common/components/toasters';
|
||||
|
||||
import { patchCase } from './api';
|
||||
import { UpdateKey, UpdateByKey } from './types';
|
||||
import * as i18n from './translations';
|
||||
import { Case } from './types';
|
||||
|
||||
export type UpdateKey = keyof Pick<
|
||||
CasePatchRequest,
|
||||
'connector' | 'description' | 'status' | 'tags' | 'title' | 'settings'
|
||||
>;
|
||||
import { createUpdateSuccessToaster } from './utils';
|
||||
|
||||
interface NewCaseState {
|
||||
isLoading: boolean;
|
||||
|
@ -28,16 +19,6 @@ interface NewCaseState {
|
|||
updateKey: UpdateKey | null;
|
||||
}
|
||||
|
||||
export interface UpdateByKey {
|
||||
updateKey: UpdateKey;
|
||||
updateValue: CasePatchRequest[UpdateKey];
|
||||
fetchCaseUserActions?: (caseId: string) => void;
|
||||
updateCase?: (newCase: Case) => void;
|
||||
version: string;
|
||||
onSuccess?: () => void;
|
||||
onError?: () => void;
|
||||
}
|
||||
|
||||
type Action =
|
||||
| { type: 'FETCH_INIT'; payload: UpdateKey }
|
||||
| { type: 'FETCH_SUCCESS' }
|
||||
|
@ -89,7 +70,7 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase =>
|
|||
updateKey,
|
||||
updateValue,
|
||||
updateCase,
|
||||
version,
|
||||
caseData,
|
||||
onSuccess,
|
||||
onError,
|
||||
}: UpdateByKey) => {
|
||||
|
@ -101,7 +82,7 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase =>
|
|||
const response = await patchCase(
|
||||
caseId,
|
||||
{ [updateKey]: updateValue },
|
||||
version,
|
||||
caseData.version,
|
||||
abortCtrl.signal
|
||||
);
|
||||
if (!cancel) {
|
||||
|
@ -112,7 +93,11 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase =>
|
|||
updateCase(response[0]);
|
||||
}
|
||||
dispatch({ type: 'FETCH_SUCCESS' });
|
||||
displaySuccessToast(i18n.UPDATED_CASE(response[0].title), dispatchToaster);
|
||||
dispatchToaster({
|
||||
type: 'addToaster',
|
||||
toast: createUpdateSuccessToaster(caseData, response[0], updateKey, updateValue),
|
||||
});
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
valueToUpdateIsSettings,
|
||||
valueToUpdateIsStatus,
|
||||
createUpdateSuccessToaster,
|
||||
} from './utils';
|
||||
|
||||
import { Case } from './types';
|
||||
|
||||
const caseBeforeUpdate = {
|
||||
comments: [
|
||||
{
|
||||
type: 'alert',
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
syncAlerts: true,
|
||||
},
|
||||
} as Case;
|
||||
|
||||
const caseAfterUpdate = { title: 'My case' } as Case;
|
||||
|
||||
describe('utils', () => {
|
||||
describe('valueToUpdateIsSettings', () => {
|
||||
it('returns true if key is settings', () => {
|
||||
expect(valueToUpdateIsSettings('settings', 'value')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if key is NOT settings', () => {
|
||||
expect(valueToUpdateIsSettings('tags', 'value')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('valueToUpdateIsStatus', () => {
|
||||
it('returns true if key is status', () => {
|
||||
expect(valueToUpdateIsStatus('status', 'value')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if key is NOT status', () => {
|
||||
expect(valueToUpdateIsStatus('tags', 'value')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createUpdateSuccessToaster', () => {
|
||||
it('creates the correct toast when sync alerts is turned on and case has alerts', () => {
|
||||
// We remove the id as is randomly generated
|
||||
const { id, ...toast } = createUpdateSuccessToaster(
|
||||
caseBeforeUpdate,
|
||||
caseAfterUpdate,
|
||||
'settings',
|
||||
{
|
||||
syncAlerts: true,
|
||||
}
|
||||
);
|
||||
|
||||
expect(toast).toEqual({
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
title: 'Alerts in "My case" have been synced',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates the correct toast when sync alerts is turned on and case does NOT have alerts', () => {
|
||||
// We remove the id as is randomly generated
|
||||
const { id, ...toast } = createUpdateSuccessToaster(
|
||||
{ ...caseBeforeUpdate, comments: [] },
|
||||
caseAfterUpdate,
|
||||
'settings',
|
||||
{
|
||||
syncAlerts: true,
|
||||
}
|
||||
);
|
||||
|
||||
expect(toast).toEqual({
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
title: 'Updated "My case"',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates the correct toast when sync alerts is turned off and case has alerts', () => {
|
||||
// We remove the id as is randomly generated
|
||||
const { id, ...toast } = createUpdateSuccessToaster(
|
||||
caseBeforeUpdate,
|
||||
caseAfterUpdate,
|
||||
'settings',
|
||||
{
|
||||
syncAlerts: false,
|
||||
}
|
||||
);
|
||||
|
||||
expect(toast).toEqual({
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
title: 'Updated "My case"',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates the correct toast when the status change, case has alerts, and sync alerts is on', () => {
|
||||
// We remove the id as is randomly generated
|
||||
const { id, ...toast } = createUpdateSuccessToaster(
|
||||
caseBeforeUpdate,
|
||||
caseAfterUpdate,
|
||||
'status',
|
||||
'closed'
|
||||
);
|
||||
|
||||
expect(toast).toEqual({
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
title: 'Updated "My case"',
|
||||
text: 'Alerts in this case have been also had their status updated',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates the correct toast when the status change, case has alerts, and sync alerts is off', () => {
|
||||
// We remove the id as is randomly generated
|
||||
const { id, ...toast } = createUpdateSuccessToaster(
|
||||
{ ...caseBeforeUpdate, settings: { syncAlerts: false } },
|
||||
caseAfterUpdate,
|
||||
'status',
|
||||
'closed'
|
||||
);
|
||||
|
||||
expect(toast).toEqual({
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
title: 'Updated "My case"',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates the correct toast when the status change, case does NOT have alerts, and sync alerts is on', () => {
|
||||
// We remove the id as is randomly generated
|
||||
const { id, ...toast } = createUpdateSuccessToaster(
|
||||
{ ...caseBeforeUpdate, comments: [] },
|
||||
caseAfterUpdate,
|
||||
'status',
|
||||
'closed'
|
||||
);
|
||||
|
||||
expect(toast).toEqual({
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
title: 'Updated "My case"',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates the correct toast if not a status or a setting', () => {
|
||||
// We remove the id as is randomly generated
|
||||
const { id, ...toast } = createUpdateSuccessToaster(
|
||||
caseBeforeUpdate,
|
||||
caseAfterUpdate,
|
||||
'title',
|
||||
'My new title'
|
||||
);
|
||||
|
||||
expect(toast).toEqual({
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
title: 'Updated "My case"',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import uuid from 'uuid';
|
||||
import { set } from '@elastic/safer-lodash-set';
|
||||
import { camelCase, isArray, isObject } from 'lodash';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
|
@ -26,9 +27,12 @@ import {
|
|||
CaseUserActionsResponseRt,
|
||||
ServiceConnectorCaseResponseRt,
|
||||
ServiceConnectorCaseResponse,
|
||||
CommentType,
|
||||
CasePatchRequest,
|
||||
} from '../../../../case/common/api';
|
||||
import { ToasterError } from '../../common/components/toasters';
|
||||
import { AllCases, Case } from './types';
|
||||
import { AppToast, ToasterError } from '../../common/components/toasters';
|
||||
import { AllCases, Case, UpdateByKey } from './types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const getTypedPayload = <T>(a: unknown): T => a as T;
|
||||
|
||||
|
@ -107,3 +111,47 @@ export const decodeServiceConnectorCaseResponse = (respPushCase?: ServiceConnect
|
|||
ServiceConnectorCaseResponseRt.decode(respPushCase),
|
||||
fold(throwErrors(createToasterPlainError), identity)
|
||||
);
|
||||
|
||||
export const valueToUpdateIsSettings = (
|
||||
key: UpdateByKey['updateKey'],
|
||||
value: UpdateByKey['updateValue']
|
||||
): value is CasePatchRequest['settings'] => key === 'settings';
|
||||
|
||||
export const valueToUpdateIsStatus = (
|
||||
key: UpdateByKey['updateKey'],
|
||||
value: UpdateByKey['updateValue']
|
||||
): value is CasePatchRequest['status'] => key === 'status';
|
||||
|
||||
export const createUpdateSuccessToaster = (
|
||||
caseBeforeUpdate: Case,
|
||||
caseAfterUpdate: Case,
|
||||
key: UpdateByKey['updateKey'],
|
||||
value: UpdateByKey['updateValue']
|
||||
): AppToast => {
|
||||
const caseHasAlerts = caseBeforeUpdate.comments.some(
|
||||
(comment) => comment.type === CommentType.alert
|
||||
);
|
||||
|
||||
const toast: AppToast = {
|
||||
id: uuid.v4(),
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
title: i18n.UPDATED_CASE(caseAfterUpdate.title),
|
||||
};
|
||||
|
||||
if (valueToUpdateIsSettings(key, value) && value?.syncAlerts && caseHasAlerts) {
|
||||
return {
|
||||
...toast,
|
||||
title: i18n.SYNC_CASE(caseAfterUpdate.title),
|
||||
};
|
||||
}
|
||||
|
||||
if (valueToUpdateIsStatus(key, value) && caseHasAlerts && caseBeforeUpdate.settings.syncAlerts) {
|
||||
return {
|
||||
...toast,
|
||||
text: i18n.STATUS_CHANGED_TOASTER_TEXT,
|
||||
};
|
||||
}
|
||||
|
||||
return toast;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue