From 12dba7260c0c473eaccc61d25add6204d781aa1e Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 4 Jan 2021 20:45:43 +0200 Subject: [PATCH] Fix toasts --- .../cases/components/case_view/index.tsx | 12 ++--- .../timeline_actions/add_to_case_action.tsx | 20 ++++++- .../timeline_actions/translations.ts | 7 +++ .../public/cases/containers/api.ts | 5 +- .../public/cases/containers/translations.ts | 13 +++++ .../public/cases/containers/types.ts | 16 ++++++ .../cases/containers/use_get_cases.test.tsx | 2 +- .../public/cases/containers/use_get_cases.tsx | 5 +- .../cases/containers/use_update_case.test.tsx | 5 +- .../cases/containers/use_update_case.tsx | 35 ++++--------- .../public/cases/containers/utils.ts | 52 ++++++++++++++++++- 11 files changed, 130 insertions(+), 42 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index 6007038b33ab7..25dde1370f534 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -175,7 +175,7 @@ export const CaseComponent = React.memo( updateKey: 'title', updateValue: titleUpdate, updateCase: handleUpdateNewCase, - version: caseData.version, + caseData, onSuccess, onError, }); @@ -189,7 +189,7 @@ export const CaseComponent = React.memo( updateKey: 'connector', updateValue: connector, updateCase: handleUpdateNewCase, - version: caseData.version, + caseData, onSuccess, onError, }); @@ -203,7 +203,7 @@ export const CaseComponent = React.memo( updateKey: 'description', updateValue: descriptionUpdate, updateCase: handleUpdateNewCase, - version: caseData.version, + caseData, onSuccess, onError, }); @@ -216,7 +216,7 @@ export const CaseComponent = React.memo( updateKey: 'tags', updateValue: tagsUpdate, updateCase: handleUpdateNewCase, - version: caseData.version, + caseData, onSuccess, onError, }); @@ -229,7 +229,7 @@ export const CaseComponent = React.memo( updateKey: 'status', updateValue: statusUpdate, updateCase: handleUpdateNewCase, - version: caseData.version, + caseData, onSuccess, onError, }); @@ -243,7 +243,7 @@ export const CaseComponent = React.memo( updateKey: 'settings', updateValue: settingsUpdate, updateCase: handleUpdateNewCase, - version: caseData.version, + caseData, onSuccess, onError, }); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index d2993fa63937d..ed9aca8daea78 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -5,6 +5,7 @@ */ import React, { memo, useState, useCallback, useMemo } from 'react'; +import uuid from 'uuid'; import { EuiPopover, EuiButtonIcon, @@ -20,10 +21,25 @@ import { ActionIconItem } from '../../../timelines/components/timeline/body/acti 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 { AppToast, useStateToaster } from '../../../common/components/toasters'; import { useCreateCaseModal } from '../use_create_case_modal'; import { useAllCasesModal } from '../use_all_cases_modal'; +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; +}; + interface AddToCaseActionProps { ariaLabel?: string; ecsRowData: Ecs; @@ -53,7 +69,7 @@ const AddToCaseActionComponent: React.FC = ({ alertId: eventId, index: eventIndex ?? '', }, - () => displaySuccessToast(i18n.CASE_CREATED_SUCCESS_TOAST(theCase.title), dispatchToaster) + () => dispatchToaster({ type: 'addToaster', toast: createUpdateSuccessToaster(theCase) }) ); }, [postComment, eventId, eventIndex, dispatchToaster] diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/translations.ts b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/translations.ts index 0ec6a5c89e65a..479323ed1301c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/translations.ts @@ -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', + } +); diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.ts b/x-pack/plugins/security_solution/public/cases/containers/api.ts index 07f7391ca94d9..fd130aa01196a 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/api.ts @@ -156,7 +156,10 @@ export const postCase = async (newCase: CasePostRequest, signal: AbortSignal): P export const patchCase = async ( caseId: string, - updatedCase: Pick, + updatedCase: Pick< + CasePatchRequest, + 'description' | 'status' | 'tags' | 'title' | 'settings' | 'connector' + >, version: string, signal: AbortSignal ): Promise => { diff --git a/x-pack/plugins/security_solution/public/cases/containers/translations.ts b/x-pack/plugins/security_solution/public/cases/containers/translations.ts index b0dafcec97cce..366252599c740 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/translations.ts @@ -72,3 +72,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', + } +); diff --git a/x-pack/plugins/security_solution/public/cases/containers/types.ts b/x-pack/plugins/security_solution/public/cases/containers/types.ts index f83f8c70e5d87..4e9baed62c644 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/types.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/types.ts @@ -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; +} diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.test.tsx index 9b4bf966a1434..f8e8d8d6c6969 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.test.tsx @@ -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'; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx index e773a25237d0a..3dd0ad3d564a3 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx @@ -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 { caseId: string; version: string; refetchCasesStatus: () => void; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx index 01e64fa780d52..7b0b7159f2601 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx @@ -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, }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx index 08333416d3c46..ba589acaca507 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx @@ -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(); } diff --git a/x-pack/plugins/security_solution/public/cases/containers/utils.ts b/x-pack/plugins/security_solution/public/cases/containers/utils.ts index 6d0d9fa0f030d..567e75fa656a7 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/utils.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/utils.ts @@ -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 = (a: unknown): T => a as T; @@ -107,3 +111,47 @@ export const decodeServiceConnectorCaseResponse = (respPushCase?: ServiceConnect ServiceConnectorCaseResponseRt.decode(respPushCase), fold(throwErrors(createToasterPlainError), identity) ); + +const valueToUpdateIsSettings = ( + key: UpdateByKey['updateKey'], + value: UpdateByKey['updateValue'] +): value is CasePatchRequest['settings'] => key === 'settings'; + +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; +};