diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts index 89190afabef9f..ce98dd3573d30 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -119,3 +119,11 @@ export const patchComment = async ( ); return convertToCamelCase(decodeCommentResponse(response)); }; + +export const deleteCases = async (caseIds: string[]): Promise => { + const response = await KibanaServices.get().http.fetch(`${CASES_URL}`, { + method: 'DELETE', + query: { ids: JSON.stringify(caseIds) }, + }); + return response === 'true' ? true : false; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts index ac62ba7b6f997..a0e57faa7661f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts @@ -7,11 +7,3 @@ export const CASES_URL = `/api/cases`; export const DEFAULT_TABLE_ACTIVE_PAGE = 1; export const DEFAULT_TABLE_LIMIT = 5; -export const FETCH_FAILURE = 'FETCH_FAILURE'; -export const FETCH_INIT = 'FETCH_INIT'; -export const FETCH_SUCCESS = 'FETCH_SUCCESS'; -export const POST_NEW_CASE = 'POST_NEW_CASE'; -export const POST_NEW_COMMENT = 'POST_NEW_COMMENT'; -export const UPDATE_FILTER_OPTIONS = 'UPDATE_FILTER_OPTIONS'; -export const UPDATE_TABLE_SELECTIONS = 'UPDATE_TABLE_SELECTIONS'; -export const UPDATE_QUERY_PARAMS = 'UPDATE_QUERY_PARAMS'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index d479abdbd4489..c89993ec67179 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -16,6 +16,7 @@ export interface Comment { export interface Case { id: string; comments: Comment[]; + commentIds: string[]; createdAt: string; createdBy: ElasticUser; description: string; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx new file mode 100644 index 0000000000000..d5a3b3cf9314c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx @@ -0,0 +1,121 @@ +/* + * 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 { useCallback, useReducer } from 'react'; +import { errorToToaster, useStateToaster } from '../../components/toasters'; +import * as i18n from './translations'; +import { deleteCases } from './api'; + +interface DeleteState { + isDisplayConfirmDeleteModal: boolean; + isDeleted: boolean; + isLoading: boolean; + isError: boolean; +} +type Action = + | { type: 'DISPLAY_MODAL'; payload: boolean } + | { type: 'FETCH_INIT' } + | { type: 'FETCH_SUCCESS'; payload: boolean } + | { type: 'FETCH_FAILURE' } + | { type: 'RESET_IS_DELETED' }; + +const dataFetchReducer = (state: DeleteState, action: Action): DeleteState => { + switch (action.type) { + case 'DISPLAY_MODAL': + return { + ...state, + isDisplayConfirmDeleteModal: action.payload, + }; + case 'FETCH_INIT': + return { + ...state, + isLoading: true, + isError: false, + }; + case 'FETCH_SUCCESS': + return { + ...state, + isLoading: false, + isError: false, + isDeleted: action.payload, + }; + case 'FETCH_FAILURE': + return { + ...state, + isLoading: false, + isError: true, + }; + case 'RESET_IS_DELETED': + return { + ...state, + isDeleted: false, + }; + default: + return state; + } +}; +interface UseDeleteCase extends DeleteState { + dispatchResetIsDeleted: () => void; + handleOnDeleteConfirm: (caseIds: string[]) => void; + handleToggleModal: () => void; +} + +export const useDeleteCases = (): UseDeleteCase => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isDisplayConfirmDeleteModal: false, + isLoading: false, + isError: false, + isDeleted: false, + }); + const [, dispatchToaster] = useStateToaster(); + + const dispatchDeleteCases = useCallback((caseIds: string[]) => { + let cancel = false; + const deleteData = async () => { + try { + dispatch({ type: 'FETCH_INIT' }); + await deleteCases(caseIds); + if (!cancel) { + dispatch({ type: 'FETCH_SUCCESS', payload: true }); + } + } catch (error) { + if (!cancel) { + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + dispatch({ type: 'FETCH_FAILURE' }); + } + } + }; + deleteData(); + return () => { + cancel = true; + }; + }, []); + + const dispatchToggleDeleteModal = useCallback(() => { + dispatch({ type: 'DISPLAY_MODAL', payload: !state.isDisplayConfirmDeleteModal }); + }, [state.isDisplayConfirmDeleteModal]); + + const dispatchResetIsDeleted = useCallback(() => { + dispatch({ type: 'RESET_IS_DELETED' }); + }, [state.isDisplayConfirmDeleteModal]); + + const handleOnDeleteConfirm = useCallback( + caseIds => { + dispatchDeleteCases(caseIds); + dispatchToggleDeleteModal(); + }, + [state.isDisplayConfirmDeleteModal] + ); + const handleToggleModal = useCallback(() => { + dispatchToggleDeleteModal(); + }, [state.isDisplayConfirmDeleteModal]); + + return { ...state, dispatchResetIsDeleted, handleOnDeleteConfirm, handleToggleModal }; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx index b758f914c991e..6020969ed6375 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx @@ -7,8 +7,6 @@ import { useEffect, useReducer } from 'react'; import { Case } from './types'; -import { FETCH_INIT, FETCH_FAILURE, FETCH_SUCCESS } from './constants'; -import { getTypedPayload } from './utils'; import * as i18n from './translations'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { getCase } from './api'; @@ -18,40 +16,42 @@ interface CaseState { isLoading: boolean; isError: boolean; } -interface Action { - type: string; - payload?: Case; -} + +type Action = + | { type: 'FETCH_INIT' } + | { type: 'FETCH_SUCCESS'; payload: Case } + | { type: 'FETCH_FAILURE' }; const dataFetchReducer = (state: CaseState, action: Action): CaseState => { switch (action.type) { - case FETCH_INIT: + case 'FETCH_INIT': return { ...state, isLoading: true, isError: false, }; - case FETCH_SUCCESS: + case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, - data: getTypedPayload(action.payload), + data: action.payload, }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, isLoading: false, isError: true, }; default: - throw new Error(); + return state; } }; const initialData: Case = { id: '', createdAt: '', comments: [], + commentIds: [], createdBy: { username: '', }, @@ -63,7 +63,7 @@ const initialData: Case = { version: '', }; -export const useGetCase = (caseId: string): [CaseState] => { +export const useGetCase = (caseId: string): CaseState => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: true, isError: false, @@ -74,11 +74,11 @@ export const useGetCase = (caseId: string): [CaseState] => { const callFetch = () => { let didCancel = false; const fetchData = async () => { - dispatch({ type: FETCH_INIT }); + dispatch({ type: 'FETCH_INIT' }); try { const response = await getCase(caseId); if (!didCancel) { - dispatch({ type: FETCH_SUCCESS, payload: response }); + dispatch({ type: 'FETCH_SUCCESS', payload: response }); } } catch (error) { if (!didCancel) { @@ -87,7 +87,7 @@ export const useGetCase = (caseId: string): [CaseState] => { error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: FETCH_FAILURE }); + dispatch({ type: 'FETCH_FAILURE' }); } } }; @@ -100,5 +100,5 @@ export const useGetCase = (caseId: string): [CaseState] => { useEffect(() => { callFetch(); }, [caseId]); - return [state]; + return state; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index 99c7ef0c757c7..1c7c30ae9da18 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -96,7 +96,7 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS selectedCases: action.payload, }; default: - throw new Error(); + return state; } }; @@ -109,6 +109,7 @@ const initialData: AllCases = { interface UseGetCases extends UseGetCasesState { dispatchUpdateCaseProperty: ({ updateKey, updateValue, caseId, version }: UpdateCase) => void; getCaseCount: (caseState: keyof CaseCount) => void; + refetchCases: (filters: FilterOptions, queryParams: QueryParams) => void; setFilters: (filters: FilterOptions) => void; setQueryParams: (queryParams: QueryParams) => void; setSelectedCases: (mySelectedCases: Case[]) => void; @@ -245,10 +246,17 @@ export const useGetCases = (): UseGetCases => { [state.filterOptions, state.queryParams] ); + const refetchCases = useCallback(() => { + fetchCases(state.filterOptions, state.queryParams); + getCaseCount('open'); + getCaseCount('closed'); + }, [state.filterOptions, state.queryParams]); + return { ...state, dispatchUpdateCaseProperty, getCaseCount, + refetchCases, setFilters, setQueryParams, setSelectedCases, diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx index 5e6df9b92f462..e3657f5b09da9 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx @@ -8,63 +8,61 @@ import { useEffect, useReducer } from 'react'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { getTags } from './api'; -import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; import * as i18n from './translations'; interface TagsState { - data: string[]; + tags: string[]; isLoading: boolean; isError: boolean; } -interface Action { - type: string; - payload?: string[]; -} +type Action = + | { type: 'FETCH_INIT' } + | { type: 'FETCH_SUCCESS'; payload: string[] } + | { type: 'FETCH_FAILURE' }; const dataFetchReducer = (state: TagsState, action: Action): TagsState => { switch (action.type) { - case FETCH_INIT: + case 'FETCH_INIT': return { ...state, isLoading: true, isError: false, }; - case FETCH_SUCCESS: - const getTypedPayload = (a: Action['payload']) => a as string[]; + case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, - data: getTypedPayload(action.payload), + tags: action.payload, }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, isLoading: false, isError: true, }; default: - throw new Error(); + return state; } }; const initialData: string[] = []; -export const useGetTags = (): [TagsState] => { +export const useGetTags = (): TagsState => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, - data: initialData, + tags: initialData, }); const [, dispatchToaster] = useStateToaster(); useEffect(() => { let didCancel = false; const fetchData = async () => { - dispatch({ type: FETCH_INIT }); + dispatch({ type: 'FETCH_INIT' }); try { const response = await getTags(); if (!didCancel) { - dispatch({ type: FETCH_SUCCESS, payload: response }); + dispatch({ type: 'FETCH_SUCCESS', payload: response }); } } catch (error) { if (!didCancel) { @@ -73,7 +71,7 @@ export const useGetTags = (): [TagsState] => { error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: FETCH_FAILURE }); + dispatch({ type: 'FETCH_FAILURE' }); } } }; @@ -82,5 +80,5 @@ export const useGetTags = (): [TagsState] => { didCancel = true; }; }, []); - return [state]; + return state; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx index 5cd0911fae81a..14b9e78846906 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx @@ -9,7 +9,6 @@ import { useReducer, useCallback } from 'react'; import { CaseRequest } from '../../../../../../plugins/case/common/api'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { postCase } from './api'; -import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; import * as i18n from './translations'; import { Case } from './types'; @@ -18,34 +17,34 @@ interface NewCaseState { isLoading: boolean; isError: boolean; } -interface Action { - type: string; - payload?: Case; -} +type Action = + | { type: 'FETCH_INIT' } + | { type: 'FETCH_SUCCESS'; payload: Case } + | { type: 'FETCH_FAILURE' }; const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => { switch (action.type) { - case FETCH_INIT: + case 'FETCH_INIT': return { ...state, isLoading: true, isError: false, }; - case FETCH_SUCCESS: + case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, caseData: action.payload ?? null, }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, isLoading: false, isError: true, }; default: - throw new Error(); + return state; } }; @@ -63,11 +62,11 @@ export const usePostCase = (): UsePostCase => { const postMyCase = useCallback(async (data: CaseRequest) => { let cancel = false; try { - dispatch({ type: FETCH_INIT }); + dispatch({ type: 'FETCH_INIT' }); const response = await postCase({ ...data, state: 'open' }); if (!cancel) { dispatch({ - type: FETCH_SUCCESS, + type: 'FETCH_SUCCESS', payload: response, }); } @@ -78,7 +77,7 @@ export const usePostCase = (): UsePostCase => { error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: FETCH_FAILURE }); + dispatch({ type: 'FETCH_FAILURE' }); } } return () => { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx index 1467c691f547e..a96cb97d7cc7b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx @@ -10,7 +10,6 @@ import { CommentRequest } from '../../../../../../plugins/case/common/api'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { postComment } from './api'; -import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; import * as i18n from './translations'; import { Comment } from './types'; @@ -20,39 +19,46 @@ interface NewCommentState { isError: boolean; caseId: string; } -interface Action { - type: string; - payload?: Comment; -} +type Action = + | { type: 'RESET_COMMENT_DATA' } + | { type: 'FETCH_INIT' } + | { type: 'FETCH_SUCCESS'; payload: Comment } + | { type: 'FETCH_FAILURE' }; const dataFetchReducer = (state: NewCommentState, action: Action): NewCommentState => { switch (action.type) { - case FETCH_INIT: + case 'RESET_COMMENT_DATA': + return { + ...state, + commentData: null, + }; + case 'FETCH_INIT': return { ...state, isLoading: true, isError: false, }; - case FETCH_SUCCESS: + case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, commentData: action.payload ?? null, }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, isLoading: false, isError: true, }; default: - throw new Error(); + return state; } }; interface UsePostComment extends NewCommentState { postComment: (data: CommentRequest) => void; + resetCommentData: () => void; } export const usePostComment = (caseId: string): UsePostComment => { @@ -67,10 +73,10 @@ export const usePostComment = (caseId: string): UsePostComment => { const postMyComment = useCallback(async (data: CommentRequest) => { let cancel = false; try { - dispatch({ type: FETCH_INIT }); + dispatch({ type: 'FETCH_INIT' }); const response = await postComment(data, state.caseId); if (!cancel) { - dispatch({ type: FETCH_SUCCESS, payload: response }); + dispatch({ type: 'FETCH_SUCCESS', payload: response }); } } catch (error) { if (!cancel) { @@ -79,7 +85,7 @@ export const usePostComment = (caseId: string): UsePostComment => { error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: FETCH_FAILURE }); + dispatch({ type: 'FETCH_FAILURE' }); } } return () => { @@ -87,5 +93,7 @@ export const usePostComment = (caseId: string): UsePostComment => { }; }, []); - return { ...state, postComment: postMyComment }; + const resetCommentData = useCallback(() => dispatch({ type: 'RESET_COMMENT_DATA' }), []); + + return { ...state, postComment: postMyComment, resetCommentData }; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx index 594677aefe245..2b1081b9b901c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -10,10 +10,8 @@ import { CaseRequest } from '../../../../../../plugins/case/common/api'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { patchCase } from './api'; -import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; import * as i18n from './translations'; import { Case } from './types'; -import { getTypedPayload } from './utils'; type UpdateKey = keyof CaseRequest; @@ -29,30 +27,30 @@ export interface UpdateByKey { updateValue: CaseRequest[UpdateKey]; } -interface Action { - type: string; - payload?: Case | UpdateKey; -} +type Action = + | { type: 'FETCH_INIT'; payload: UpdateKey } + | { type: 'FETCH_SUCCESS'; payload: Case } + | { type: 'FETCH_FAILURE' }; const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => { switch (action.type) { - case FETCH_INIT: + case 'FETCH_INIT': return { ...state, isLoading: true, isError: false, - updateKey: getTypedPayload(action.payload), + updateKey: action.payload, }; - case FETCH_SUCCESS: + case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, - caseData: getTypedPayload(action.payload), + caseData: action.payload, updateKey: null, }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, isLoading: false, @@ -60,7 +58,7 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => updateKey: null, }; default: - throw new Error(); + return state; } }; @@ -80,14 +78,14 @@ export const useUpdateCase = (caseId: string, initialData: Case): UseUpdateCase async ({ updateKey, updateValue }: UpdateByKey) => { let cancel = false; try { - dispatch({ type: FETCH_INIT, payload: updateKey }); + dispatch({ type: 'FETCH_INIT', payload: updateKey }); const response = await patchCase( caseId, { [updateKey]: updateValue }, state.caseData.version ); if (!cancel) { - dispatch({ type: FETCH_SUCCESS, payload: response }); + dispatch({ type: 'FETCH_SUCCESS', payload: response }); } } catch (error) { if (!cancel) { @@ -96,7 +94,7 @@ export const useUpdateCase = (caseId: string, initialData: Case): UseUpdateCase error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: FETCH_FAILURE }); + dispatch({ type: 'FETCH_FAILURE' }); } } return () => { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx index 0e39d2303a32a..a40a1100ca735 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx @@ -4,15 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useReducer, useCallback } from 'react'; +import { useReducer, useCallback, Dispatch } from 'react'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { patchComment } from './api'; -import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; import * as i18n from './translations'; import { Comment } from './types'; -import { getTypedPayload } from './utils'; interface CommentUpdateState { comments: Comment[]; @@ -25,22 +23,28 @@ interface CommentUpdate { commentId: string; } -interface Action { - type: string; - payload?: CommentUpdate | string; -} +type Action = + | { type: 'APPEND_COMMENT'; payload: Comment } + | { type: 'FETCH_INIT'; payload: string } + | { type: 'FETCH_SUCCESS'; payload: CommentUpdate } + | { type: 'FETCH_FAILURE'; payload: string }; const dataFetchReducer = (state: CommentUpdateState, action: Action): CommentUpdateState => { switch (action.type) { - case FETCH_INIT: + case 'APPEND_COMMENT': return { ...state, - isLoadingIds: [...state.isLoadingIds, getTypedPayload(action.payload)], + comments: [...state.comments, action.payload], + }; + case 'FETCH_INIT': + return { + ...state, + isLoadingIds: [...state.isLoadingIds, action.payload], isError: false, }; - case FETCH_SUCCESS: - const updatePayload = getTypedPayload(action.payload); + case 'FETCH_SUCCESS': + const updatePayload = action.payload; const foundIndex = state.comments.findIndex( comment => comment.id === updatePayload.commentId ); @@ -55,21 +59,20 @@ const dataFetchReducer = (state: CommentUpdateState, action: Action): CommentUpd isError: false, comments: newComments, }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, - isLoadingIds: state.isLoadingIds.filter( - id => getTypedPayload(action.payload) !== id - ), + isLoadingIds: state.isLoadingIds.filter(id => action.payload !== id), isError: true, }; default: - throw new Error(); + return state; } }; interface UseUpdateComment extends CommentUpdateState { updateComment: (caseId: string, commentId: string, commentUpdate: string) => void; + addPostedComment: Dispatch; } export const useUpdateComment = (comments: Comment[]): UseUpdateComment => { @@ -84,7 +87,7 @@ export const useUpdateComment = (comments: Comment[]): UseUpdateComment => { async (caseId: string, commentId: string, commentUpdate: string) => { let cancel = false; try { - dispatch({ type: FETCH_INIT, payload: commentId }); + dispatch({ type: 'FETCH_INIT', payload: commentId }); const currentComment = state.comments.find(comment => comment.id === commentId) ?? { version: '', }; @@ -95,7 +98,7 @@ export const useUpdateComment = (comments: Comment[]): UseUpdateComment => { currentComment.version ); if (!cancel) { - dispatch({ type: FETCH_SUCCESS, payload: { update: response, commentId } }); + dispatch({ type: 'FETCH_SUCCESS', payload: { update: response, commentId } }); } } catch (error) { if (!cancel) { @@ -104,7 +107,7 @@ export const useUpdateComment = (comments: Comment[]): UseUpdateComment => { error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: FETCH_FAILURE, payload: commentId }); + dispatch({ type: 'FETCH_FAILURE', payload: commentId }); } } return () => { @@ -113,6 +116,10 @@ export const useUpdateComment = (comments: Comment[]): UseUpdateComment => { }, [state] ); + const addPostedComment = useCallback( + (comment: Comment) => dispatch({ type: 'APPEND_COMMENT', payload: comment }), + [] + ); - return { ...state, updateComment: dispatchUpdateComment }; + return { ...state, updateComment: dispatchUpdateComment, addPostedComment }; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx index 6b63961b4194f..0b3b0daaf4bbc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; +import React, { useCallback, useEffect } from 'react'; import styled from 'styled-components'; import { CommentRequest } from '../../../../../../../../plugins/case/common/api'; @@ -16,6 +16,7 @@ import * as i18n from '../../translations'; import { schema } from './schema'; import { InsertTimelinePopover } from '../../../../components/timeline/insert_timeline_popover'; import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; +import { Comment } from '../../../../containers/case/types'; const MySpinner = styled(EuiLoadingSpinner)` position: absolute; @@ -27,10 +28,13 @@ const initialCommentValue: CommentRequest = { comment: '', }; -export const AddComment = React.memo<{ +interface AddCommentProps { caseId: string; -}>(({ caseId }) => { - const { commentData, isLoading, postComment } = usePostComment(caseId); + onCommentPosted: (commentResponse: Comment) => void; +} + +export const AddComment = React.memo(({ caseId, onCommentPosted }) => { + const { commentData, isLoading, postComment, resetCommentData } = usePostComment(caseId); const { form } = useForm({ defaultValue: initialCommentValue, options: { stripEmptyFields: false }, @@ -40,6 +44,15 @@ export const AddComment = React.memo<{ form, 'comment' ); + + useEffect(() => { + if (commentData !== null) { + onCommentPosted(commentData); + form.reset(); + resetCommentData(); + } + }, [commentData]); + const onSubmit = useCallback(async () => { const { isValid, data } = await form.submit(); if (isValid) { @@ -81,8 +94,6 @@ export const AddComment = React.memo<{ }} /> - {commentData != null && - 'TO DO new comment got added but we didnt update the UI yet. Refresh the page to see your comment ;)'} ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx index 2e57e5f2f95d9..bc6dfe4af25ff 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx @@ -14,6 +14,7 @@ export const useGetCasesMockState: UseGetCasesState = { id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:23.627Z', createdBy: { username: 'elastic' }, + commentIds: [], comments: [], description: 'Security banana Issue', state: 'open', @@ -26,6 +27,7 @@ export const useGetCasesMockState: UseGetCasesState = { id: '362a5c10-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:13.328Z', createdBy: { username: 'elastic' }, + commentIds: [], comments: [], description: 'Security banana Issue', state: 'open', @@ -38,6 +40,7 @@ export const useGetCasesMockState: UseGetCasesState = { id: '34f8b9e0-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:11.328Z', createdBy: { username: 'elastic' }, + commentIds: [], comments: [], description: 'Security banana Issue', state: 'open', @@ -50,6 +53,7 @@ export const useGetCasesMockState: UseGetCasesState = { id: '31890e90-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:05.563Z', createdBy: { username: 'elastic' }, + commentIds: [], comments: [], description: 'Security banana Issue', state: 'closed', @@ -62,6 +66,7 @@ export const useGetCasesMockState: UseGetCasesState = { id: '2f5b3210-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:01.901Z', createdBy: { username: 'elastic' }, + commentIds: [], comments: [], description: 'Security banana Issue', state: 'open', diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx index 0ec09f2b57918..33a1953b9d2f8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx @@ -13,18 +13,19 @@ import { UpdateCase } from '../../../../containers/case/use_get_cases'; interface GetActions { caseStatus: string; dispatchUpdate: Dispatch; + deleteCaseOnClick: (deleteCase: Case) => void; } export const getActions = ({ caseStatus, dispatchUpdate, + deleteCaseOnClick, }: GetActions): Array> => [ { description: i18n.DELETE, icon: 'trash', name: i18n.DELETE, - // eslint-disable-next-line no-console - onClick: ({ id }: Case) => console.log('TO DO Delete case', id), + onClick: deleteCaseOnClick, type: 'icon', 'data-test-subj': 'action-delete', }, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index f6ed2694fdc40..db3313d843547 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -33,9 +33,8 @@ const Spacer = styled.span` margin-left: ${({ theme }) => theme.eui.paddingSizes.s}; `; -const TempNumberComponent = () => {1}; -TempNumberComponent.displayName = 'TempNumberComponent'; - +const renderStringField = (field: string, dataTestSubj: string) => + field != null ? {field} : getEmptyTagValue(); export const getCasesColumns = ( actions: Array> ): CasesColumns[] => [ @@ -59,6 +58,7 @@ export const getCasesColumns = ( } return getEmptyTagValue(); }, + width: '25%', }, { field: 'createdBy', @@ -101,13 +101,15 @@ export const getCasesColumns = ( return getEmptyTagValue(); }, truncateText: true, + width: '20%', }, { align: 'right', - field: 'commentCount', // TO DO once we have commentCount returned in the API: https://github.com/elastic/kibana/issues/58525 + field: 'commentIds', name: i18n.COMMENTS, sortable: true, - render: TempNumberComponent, + render: (comments: Case['commentIds']) => + renderStringField(`${comments.length}`, `case-table-column-commentCount`), }, { field: 'createdAt', diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx index a9dd15086df27..10786940eee7f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx @@ -15,17 +15,19 @@ import { act } from '@testing-library/react'; import { wait } from '../../../../lib/helpers'; describe('AllCases', () => { + const dispatchUpdateCaseProperty = jest.fn(); + const getCaseCount = jest.fn(); + const refetchCases = jest.fn(); const setFilters = jest.fn(); const setQueryParams = jest.fn(); const setSelectedCases = jest.fn(); - const getCaseCount = jest.fn(); - const dispatchUpdateCaseProperty = jest.fn(); beforeEach(() => { jest.resetAllMocks(); jest.spyOn(apiHook, 'useGetCases').mockReturnValue({ ...useGetCasesMockState, dispatchUpdateCaseProperty, getCaseCount, + refetchCases, setFilters, setQueryParams, setSelectedCases, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 484d9051ee43f..1d22f6a246960 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiBasicTable, EuiButton, @@ -25,6 +25,7 @@ import { getCasesColumns } from './columns'; import { Case, FilterOptions, SortFieldCase } from '../../../../containers/case/types'; import { useGetCases } from '../../../../containers/case/use_get_cases'; +import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; import { EuiBasicTableOnChange } from '../../../detection_engine/rules/types'; import { Panel } from '../../../../components/panel'; import { CasesTableFilters } from './table_filters'; @@ -41,6 +42,7 @@ import { getBulkItems } from '../bulk_actions'; import { CaseHeaderPage } from '../case_header_page'; import { OpenClosedStats } from '../open_closed_stats'; import { getActions } from './actions'; +import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; const Div = styled.div` margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; @@ -57,11 +59,9 @@ const FlexItemDivider = styled(EuiFlexItem)` const ProgressLoader = styled(EuiProgress)` ${({ theme }) => css` - .euiFlexGroup--gutterMedium > &.euiFlexItem { - top: 2px; - border-radius: ${theme.eui.euiBorderRadius}; - z-index: ${theme.eui.euiZHeader}; - } + top: 2px; + border-radius: ${theme.eui.euiBorderRadius}; + z-index: ${theme.eui.euiZHeader}; `} `; @@ -83,11 +83,95 @@ export const AllCases = React.memo(() => { loading, queryParams, selectedCases, + refetchCases, setFilters, setQueryParams, setSelectedCases, } = useGetCases(); + // Delete case + const { + dispatchResetIsDeleted, + handleOnDeleteConfirm, + handleToggleModal, + isLoading: isDeleting, + isDeleted, + isDisplayConfirmDeleteModal, + } = useDeleteCases(); + + useEffect(() => { + if (isDeleted) { + refetchCases(filterOptions, queryParams); + dispatchResetIsDeleted(); + } + }, [isDeleted, filterOptions, queryParams]); + + const [deleteThisCase, setDeleteThisCase] = useState({ + title: '', + id: '', + }); + const [deleteBulk, setDeleteBulk] = useState([]); + const confirmDeleteModal = useMemo( + () => ( + 0} + onCancel={handleToggleModal} + onConfirm={handleOnDeleteConfirm.bind( + null, + deleteBulk.length > 0 ? deleteBulk : [deleteThisCase.id] + )} + /> + ), + [deleteBulk, deleteThisCase, isDisplayConfirmDeleteModal] + ); + + const toggleDeleteModal = useCallback( + (deleteCase: Case) => { + handleToggleModal(); + setDeleteThisCase(deleteCase); + }, + [isDisplayConfirmDeleteModal] + ); + + const toggleBulkDeleteModal = useCallback( + (deleteCases: string[]) => { + handleToggleModal(); + setDeleteBulk(deleteCases); + }, + [isDisplayConfirmDeleteModal] + ); + + const selectedCaseIds = useMemo( + (): string[] => + selectedCases.reduce((arr: string[], caseObj: Case) => [...arr, caseObj.id], []), + [selectedCases] + ); + + const getBulkItemsPopoverContent = useCallback( + (closePopover: () => void) => ( + + ), + [selectedCaseIds, filterOptions.state] + ); + const actions = useMemo( + () => + getActions({ + caseStatus: filterOptions.state, + deleteCaseOnClick: toggleDeleteModal, + dispatchUpdate: dispatchUpdateCaseProperty, + }), + [filterOptions.state] + ); + const tableOnChangeCallback = useCallback( ({ page, sort }: EuiBasicTableOnChange) => { let newQueryParams = queryParams; @@ -117,12 +201,6 @@ export const AllCases = React.memo(() => { [filterOptions, setFilters] ); - const actions = useMemo( - () => - getActions({ caseStatus: filterOptions.state, dispatchUpdate: dispatchUpdateCaseProperty }), - [filterOptions.state, dispatchUpdateCaseProperty] - ); - const memoizedGetCasesColumns = useMemo(() => getCasesColumns(actions), [filterOptions.state]); const memoizedPagination = useMemo( () => ({ @@ -134,19 +212,6 @@ export const AllCases = React.memo(() => { [data, queryParams] ); - const getBulkItemsPopoverContent = useCallback( - (closePopover: () => void) => ( - - ), - [selectedCases, filterOptions.state] - ); - const sorting: EuiTableSortingType = { sort: { field: queryParams.sortField, direction: queryParams.sortOrder }, }; @@ -162,7 +227,6 @@ export const AllCases = React.memo(() => { [loading] ); const isDataEmpty = useMemo(() => data.total === 0, [data]); - return ( <> @@ -197,7 +261,9 @@ export const AllCases = React.memo(() => { - {isCasesLoading && !isDataEmpty && } + {(isCasesLoading || isDeleting) && !isDataEmpty && ( + + )} { - {i18n.SELECTED_CASES(selectedCases.length)} + {i18n.SHOWING_SELECTED_CASES(selectedCases.length)} { { )} + {confirmDeleteModal} ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx index 5256fb6d7b3ee..9356577fd1888 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx @@ -38,7 +38,7 @@ const CasesTableFiltersComponent = ({ const [search, setSearch] = useState(initial.search); const [selectedTags, setSelectedTags] = useState(initial.tags); const [showOpenCases, setShowOpenCases] = useState(initial.state === 'open'); - const [{ data }] = useGetTags(); + const { tags } = useGetTags(); const handleSelectedTags = useCallback( newTags => { @@ -106,7 +106,7 @@ const CasesTableFiltersComponent = ({ buttonLabel={i18n.TAGS} onSelectedOptionsChanged={handleSelectedTags} selectedOptions={selectedTags} - options={data} + options={tags} optionsEmptyLabel={i18n.NO_TAGS_AVAILABLE} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts index 19117136ed046..27532e57166e1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts @@ -18,7 +18,7 @@ export const ADD_NEW_CASE = i18n.translate('xpack.siem.case.caseTable.addNewCase defaultMessage: 'Add New Case', }); -export const SELECTED_CASES = (totalRules: number) => +export const SHOWING_SELECTED_CASES = (totalRules: number) => i18n.translate('xpack.siem.case.caseTable.selectedCasesTitle', { values: { totalRules }, defaultMessage: 'Selected {totalRules} {totalRules, plural, =1 {case} other {cases}}', @@ -66,6 +66,3 @@ export const REOPEN_CASE = i18n.translate('xpack.siem.case.caseTable.reopenCase' export const CLOSE_CASE = i18n.translate('xpack.siem.case.caseTable.closeCase', { defaultMessage: 'Close case', }); -export const DUPLICATE_CASE = i18n.translate('xpack.siem.case.caseTable.duplicateCase', { - defaultMessage: 'Duplicate case', -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx index 2fe25a7d1f5d0..f171ebf91b787 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx @@ -4,29 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiContextMenuItem } from '@elastic/eui'; import React from 'react'; +import { EuiContextMenuItem } from '@elastic/eui'; import * as i18n from './translations'; -import { Case } from '../../../../containers/case/types'; interface GetBulkItems { - // cases: Case[]; closePopover: () => void; - // dispatch: Dispatch; - // dispatchToaster: Dispatch; - // reFetchCases: (refreshPrePackagedCase?: boolean) => void; - selectedCases: Case[]; + deleteCasesAction: (cases: string[]) => void; + selectedCaseIds: string[]; caseStatus: string; } export const getBulkItems = ({ - // cases, + deleteCasesAction, closePopover, caseStatus, - // dispatch, - // dispatchToaster, - // reFetchCases, - selectedCases, + selectedCaseIds, }: GetBulkItems) => { return [ caseStatus === 'open' ? ( @@ -36,8 +29,6 @@ export const getBulkItems = ({ disabled={true} // TO DO onClick={async () => { closePopover(); - // await deleteCasesAction(selectedCases, dispatch, dispatchToaster); - // reFetchCases(true); }} > {i18n.BULK_ACTION_CLOSE_SELECTED} @@ -47,10 +38,8 @@ export const getBulkItems = ({ key={i18n.BULK_ACTION_OPEN_SELECTED} icon="magnet" disabled={true} // TO DO - onClick={async () => { + onClick={() => { closePopover(); - // await deleteCasesAction(selectedCases, dispatch, dispatchToaster); - // reFetchCases(true); }} > {i18n.BULK_ACTION_OPEN_SELECTED} @@ -59,11 +48,9 @@ export const getBulkItems = ({ { closePopover(); - // await deleteCasesAction(selectedCases, dispatch, dispatchToaster); - // reFetchCases(true); + deleteCasesAction(selectedCaseIds); }} > {i18n.BULK_ACTION_DELETE_SELECTED} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx index c2d3cae6774b0..3875c316e80d1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx @@ -11,6 +11,7 @@ export const caseProps: CaseProps = { caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', initialData: { id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', + commentIds: ['a357c6a0-5435-11ea-b427-fb51a1fcb7b8'], comments: [ { comment: 'Solve this fast!', @@ -37,6 +38,7 @@ export const caseProps: CaseProps = { export const data: Case = { id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', + commentIds: ['a357c6a0-5435-11ea-b427-fb51a1fcb7b8'], comments: [ { comment: 'Solve this fast!', diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index c917d27aebea3..080cbdc143593 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiBadge, EuiButtonToggle, @@ -17,6 +17,7 @@ import { } from '@elastic/eui'; import styled, { css } from 'styled-components'; +import { Redirect } from 'react-router-dom'; import * as i18n from './translations'; import { Case } from '../../../../containers/case/types'; import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; @@ -32,6 +33,9 @@ import { useUpdateCase } from '../../../../containers/case/use_update_case'; import { WrapperPage } from '../../../../components/wrapper_page'; import { getTypedPayload } from '../../../../containers/case/utils'; import { WhitePageWrapper } from '../wrappers'; +import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; +import { SiemPageName } from '../../../home/types'; +import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; interface Props { caseId: string; @@ -62,6 +66,7 @@ export interface CaseProps { export const CaseComponent = React.memo(({ caseId, initialData }) => { const { caseData, isLoading, updateKey, updateCaseProperty } = useUpdateCase(caseId, initialData); + // Update Fields const onUpdateField = useCallback( (newUpdateKey: keyof Case, updateValue: Case[keyof Case]) => { switch (newUpdateKey) { @@ -104,13 +109,39 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => }, [updateCaseProperty, caseData.state] ); + const toggleStateCase = useCallback( + e => onUpdateField('state', e.target.checked ? 'open' : 'closed'), + [onUpdateField] + ); + const onSubmitTitle = useCallback(newTitle => onUpdateField('title', newTitle), [onUpdateField]); + const onSubmitTags = useCallback(newTags => onUpdateField('tags', newTags), [onUpdateField]); + + // Delete case + const { + handleToggleModal, + handleOnDeleteConfirm, + isDeleted, + isDisplayConfirmDeleteModal, + } = useDeleteCases(); + const confirmDeleteModal = useMemo( + () => ( + + ), + [isDisplayConfirmDeleteModal] + ); // TO DO refactor each of these const's into their own components const propertyActions = [ { iconType: 'trash', label: 'Delete case', - onClick: () => null, + onClick: handleToggleModal, }, { iconType: 'popout', @@ -124,12 +155,9 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => }, ]; - const onSubmit = useCallback(newTitle => onUpdateField('title', newTitle), [onUpdateField]); - const toggleStateCase = useCallback( - e => onUpdateField('state', e.target.checked ? 'open' : 'closed'), - [onUpdateField] - ); - const onSubmitTags = useCallback(newTags => onUpdateField('tags', newTags), [onUpdateField]); + if (isDeleted) { + return ; + } return ( <> @@ -144,7 +172,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => } title={caseData.title} @@ -222,12 +250,13 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => + {confirmDeleteModal} ); }); export const CaseView = React.memo(({ caseId }: Props) => { - const [{ data, isLoading, isError }] = useGetCase(caseId); + const { data, isLoading, isError } = useGetCase(caseId); if (isError) { return null; } diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/index.tsx new file mode 100644 index 0000000000000..dff36a6dac571 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/index.tsx @@ -0,0 +1,46 @@ +/* + * 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 React from 'react'; +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import * as i18n from './translations'; + +interface ConfirmDeleteCaseModalProps { + caseTitle: string; + isModalVisible: boolean; + isPlural: boolean; + onCancel: () => void; + onConfirm: () => void; +} + +const ConfirmDeleteCaseModalComp: React.FC = ({ + caseTitle, + isModalVisible, + isPlural, + onCancel, + onConfirm, +}) => { + if (!isModalVisible) { + return null; + } + return ( + + + {isPlural ? i18n.CONFIRM_QUESTION_PLURAL : i18n.CONFIRM_QUESTION} + + + ); +}; + +export const ConfirmDeleteCaseModal = React.memo(ConfirmDeleteCaseModalComp); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/translations.ts new file mode 100644 index 0000000000000..06e940c60d0a1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/translations.ts @@ -0,0 +1,36 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +export * from '../../translations'; + +export const DELETE_TITLE = (caseTitle: string) => + i18n.translate('xpack.siem.case.confirmDeleteCase.deleteTitle', { + values: { caseTitle }, + defaultMessage: 'Delete "{caseTitle}"', + }); + +export const CONFIRM_QUESTION = i18n.translate( + 'xpack.siem.case.confirmDeleteCase.confirmQuestion', + { + defaultMessage: + 'By deleting this case, all related case data will be permanently removed and you will no longer be able to push data to a third-party case management system. Are you sure you wish to proceed?', + } +); +export const DELETE_SELECTED_CASES = i18n.translate( + 'xpack.siem.case.confirmDeleteCase.selectedCases', + { + defaultMessage: 'Delete selected cases', + } +); + +export const CONFIRM_QUESTION_PLURAL = i18n.translate( + 'xpack.siem.case.confirmDeleteCase.confirmQuestionPlural', + { + defaultMessage: + 'By deleting these cases, all related case data will be permanently removed and you will no longer be able to push data to a third-party case management system. Are you sure you wish to proceed?', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx index d9c4175b2d2bd..cebc66a0c8363 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx @@ -24,7 +24,9 @@ const NewId = 'newComent'; export const UserActionTree = React.memo( ({ data: caseData, onUpdateField, isLoadingDescription }: UserActionTreeProps) => { - const { comments, isLoadingIds, updateComment } = useUpdateComment(caseData.comments); + const { comments, isLoadingIds, updateComment, addPostedComment } = useUpdateComment( + caseData.comments + ); const [manageMarkdownEditIds, setManangeMardownEditIds] = useState([]); @@ -63,7 +65,10 @@ export const UserActionTree = React.memo( [caseData.description, handleManageMarkdownEditId, manageMarkdownEditIds, onUpdateField] ); - const MarkdownNewComment = useMemo(() => , [caseData.id]); + const MarkdownNewComment = useMemo( + () => , + [caseData.id] + ); return ( <> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index d9e4c2725cb10..9c0287a56ccbc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -14,6 +14,14 @@ export const CANCEL = i18n.translate('xpack.siem.case.caseView.cancel', { defaultMessage: 'Cancel', }); +export const DELETE_CASE = i18n.translate('xpack.siem.case.confirmDeleteCase.deleteCase', { + defaultMessage: 'Delete case', +}); + +export const DELETE_CASES = i18n.translate('xpack.siem.case.confirmDeleteCase.deleteCases', { + defaultMessage: 'Delete cases', +}); + export const NAME = i18n.translate('xpack.siem.case.caseView.name', { defaultMessage: 'Name', }); @@ -64,26 +72,10 @@ export const OPTIONAL = i18n.translate('xpack.siem.case.caseView.optional', { defaultMessage: 'Optional', }); -export const LAST_UPDATED = i18n.translate('xpack.siem.case.caseView.updatedAt', { - defaultMessage: 'Last updated', -}); - -export const PAGE_SUBTITLE = i18n.translate('xpack.siem.case.caseView.pageSubtitle', { - defaultMessage: 'Cases within the Elastic SIEM', -}); - export const PAGE_TITLE = i18n.translate('xpack.siem.case.pageTitle', { defaultMessage: 'Cases', }); -export const STATE = i18n.translate('xpack.siem.case.caseView.state', { - defaultMessage: 'State', -}); - -export const SUBMIT = i18n.translate('xpack.siem.case.caseView.submit', { - defaultMessage: 'Submit', -}); - export const CREATE_CASE = i18n.translate('xpack.siem.case.caseView.createCase', { defaultMessage: 'Create case', });