From f5167f1105b905a0c5bba50af4c186a0f7a43358 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Tue, 4 Apr 2023 14:36:45 +0200 Subject: [PATCH 1/6] Changed file metadata to have caseId array. Created FileDeleteButtonIcon. Created deleteFileAttachments public api. Created useDeleteFileAttachment hook. --- .../public/components/files/add_file.test.tsx | 5 ++- .../public/components/files/add_file.tsx | 2 +- .../files/file_delete_button_icon.tsx | 37 ++++++++++++++++ .../public/components/files/file_type.tsx | 13 +++++- .../components/files/files_table.test.tsx | 2 +- .../public/components/files/files_table.tsx | 2 +- .../public/components/files/translations.tsx | 4 ++ .../files/use_files_table_columns.test.tsx | 14 +++--- .../files/use_files_table_columns.tsx | 9 ++-- x-pack/plugins/cases/public/containers/api.ts | 20 +++++++++ .../cases/public/containers/constants.ts | 1 + .../containers/use_delete_file_attachment.tsx | 44 +++++++++++++++++++ .../use_get_case_file_stats.test.tsx | 2 +- .../containers/use_get_case_file_stats.tsx | 2 +- .../containers/use_get_case_files.test.tsx | 2 +- .../public/containers/use_get_case_files.tsx | 2 +- 16 files changed, 139 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/files/file_delete_button_icon.tsx create mode 100644 x-pack/plugins/cases/public/containers/use_delete_file_attachment.tsx diff --git a/x-pack/plugins/cases/public/components/files/add_file.test.tsx b/x-pack/plugins/cases/public/components/files/add_file.test.tsx index 6605e666cdc7e..cb1d12f0cf9cc 100644 --- a/x-pack/plugins/cases/public/components/files/add_file.test.tsx +++ b/x-pack/plugins/cases/public/components/files/add_file.test.tsx @@ -209,7 +209,10 @@ describe('AddFile', () => { userEvent.click(await screen.findByTestId('testMetadata')); await waitFor(() => - expect(validateMetadata).toHaveBeenCalledWith({ caseId, owner: mockedTestProvidersOwner[0] }) + expect(validateMetadata).toHaveBeenCalledWith({ + caseIds: [caseId], + owner: mockedTestProvidersOwner[0], + }) ); }); diff --git a/x-pack/plugins/cases/public/components/files/add_file.tsx b/x-pack/plugins/cases/public/components/files/add_file.tsx index 546e247e4de3d..0735131e81741 100644 --- a/x-pack/plugins/cases/public/components/files/add_file.tsx +++ b/x-pack/plugins/cases/public/components/files/add_file.tsx @@ -138,7 +138,7 @@ const AddFileComponent: React.FC = ({ caseId }) => { kind={constructFileKindIdByOwner(owner[0] as Owner)} onDone={onUploadDone} onError={onError} - meta={{ caseId, owner: owner[0] }} + meta={{ caseIds: [caseId], owner: owner[0] }} /> diff --git a/x-pack/plugins/cases/public/components/files/file_delete_button_icon.tsx b/x-pack/plugins/cases/public/components/files/file_delete_button_icon.tsx new file mode 100644 index 0000000000000..8f02b4d04e99f --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_delete_button_icon.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiButtonIcon } from '@elastic/eui'; +import * as i18n from './translations'; +import { useDeleteFileAttachment } from '../../containers/use_delete_file_attachment'; + +interface FileDeleteButtonIconProps { + caseId: string; + fileId: string; +} + +const FileDeleteButtonIconComponent: React.FC = ({ caseId, fileId }) => { + const { isLoading, mutate: deleteFileAttachment } = useDeleteFileAttachment(); + + return ( + + deleteFileAttachment({ caseId, fileId, successToasterTitle: i18n.FILE_DELETE_SUCCESS }) + } + data-test-subj={'cases-files-delete-button'} + /> + ); +}; +FileDeleteButtonIconComponent.displayName = 'FileDeleteButtonIcon'; + +export const FileDeleteButtonIcon = React.memo(FileDeleteButtonIconComponent); diff --git a/x-pack/plugins/cases/public/components/files/file_type.tsx b/x-pack/plugins/cases/public/components/files/file_type.tsx index b9d1f5e1342d0..dce70e7e82f5b 100644 --- a/x-pack/plugins/cases/public/components/files/file_type.tsx +++ b/x-pack/plugins/cases/public/components/files/file_type.tsx @@ -20,6 +20,7 @@ import { FilePreview } from './file_preview'; import * as i18n from './translations'; import { isImage, isValidFileExternalReferenceMetadata } from './utils'; import { useFilePreview } from './use_file_preview'; +import { FileDeleteButtonIcon } from './file_delete_button_icon'; interface FileAttachmentEventProps { file: FileJSON; @@ -39,6 +40,15 @@ const FileAttachmentEvent = ({ file }: FileAttachmentEventProps) => { FileAttachmentEvent.displayName = 'FileAttachmentEvent'; +const FileAttachmentActions = ({ caseId, fileId }: { caseId: string; fileId: string }) => ( + <> + + + +); + +FileAttachmentActions.displayName = 'FileAttachmentActions'; + const getFileAttachmentViewObject = (props: ExternalReferenceAttachmentViewProps) => { if (!isValidFileExternalReferenceMetadata(props.externalReferenceMetadata)) { return { @@ -50,6 +60,7 @@ const getFileAttachmentViewObject = (props: ExternalReferenceAttachmentViewProps } const fileId = props.externalReferenceId; + const caseId = props.caseData.id; // @ts-ignore const partialFileJSON = props.externalReferenceMetadata?.files[0] as Partial; @@ -62,7 +73,7 @@ const getFileAttachmentViewObject = (props: ExternalReferenceAttachmentViewProps return { event: , timelineAvatar: isImage(file) ? 'image' : 'document', - actions: , + actions: , hideDefaultActions: true, }; }; diff --git a/x-pack/plugins/cases/public/components/files/files_table.test.tsx b/x-pack/plugins/cases/public/components/files/files_table.test.tsx index 445179e5d106e..144d48a688992 100644 --- a/x-pack/plugins/cases/public/components/files/files_table.test.tsx +++ b/x-pack/plugins/cases/public/components/files/files_table.test.tsx @@ -45,7 +45,7 @@ describe('FilesTable', () => { expect(await screen.findByTestId('cases-files-table-filetype')).toBeInTheDocument(); expect(await screen.findByTestId('cases-files-table-date-added')).toBeInTheDocument(); expect(await screen.findByTestId('cases-files-download-button')).toBeInTheDocument(); - expect(await screen.findByTestId('cases-files-table-action-delete')).toBeInTheDocument(); + expect(await screen.findByTestId('cases-files-delete-button')).toBeInTheDocument(); }); it('renders loading state', async () => { diff --git a/x-pack/plugins/cases/public/components/files/files_table.tsx b/x-pack/plugins/cases/public/components/files/files_table.tsx index c8cc6320b3bbe..59224a250c10d 100644 --- a/x-pack/plugins/cases/public/components/files/files_table.tsx +++ b/x-pack/plugins/cases/public/components/files/files_table.tsx @@ -46,7 +46,7 @@ export const FilesTable = ({ caseId, items, pagination, onChange, isLoading }: F showPreview(); }; - const columns = useFilesTableColumns({ showPreview: displayPreview }); + const columns = useFilesTableColumns({ caseId, showPreview: displayPreview }); return isLoading ? ( <> diff --git a/x-pack/plugins/cases/public/components/files/translations.tsx b/x-pack/plugins/cases/public/components/files/translations.tsx index bec9fea1447a0..a3f5cb7395301 100644 --- a/x-pack/plugins/cases/public/components/files/translations.tsx +++ b/x-pack/plugins/cases/public/components/files/translations.tsx @@ -90,3 +90,7 @@ export const ADDED_UNKNOWN_FILE = i18n.translate('xpack.cases.caseView.files.add export const DOWNLOAD = i18n.translate('xpack.cases.caseView.files.download', { defaultMessage: 'download', }); + +export const FILE_DELETE_SUCCESS = i18n.translate('xpack.cases.caseView.files.deleteSuccess', { + defaultMessage: 'File deleted successfully', +}); diff --git a/x-pack/plugins/cases/public/components/files/use_files_table_columns.test.tsx b/x-pack/plugins/cases/public/components/files/use_files_table_columns.test.tsx index dab14c297b76f..214d5c734082a 100644 --- a/x-pack/plugins/cases/public/components/files/use_files_table_columns.test.tsx +++ b/x-pack/plugins/cases/public/components/files/use_files_table_columns.test.tsx @@ -10,11 +10,13 @@ import { useFilesTableColumns } from './use_files_table_columns'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; import { renderHook } from '@testing-library/react-hooks'; +import { basicCase } from '../../containers/mock'; -describe('useCasesColumns ', () => { +describe('useFilesTableColumns ', () => { let appMockRender: AppMockRenderer; - const useCasesColumnsProps: FilesTableColumnsProps = { + const useFilesTableColumnsProps: FilesTableColumnsProps = { + caseId: basicCase.id, showPreview: () => {}, }; @@ -24,7 +26,7 @@ describe('useCasesColumns ', () => { }); it('return all files table columns correctly', async () => { - const { result } = renderHook(() => useFilesTableColumns(useCasesColumnsProps), { + const { result } = renderHook(() => useFilesTableColumns(useFilesTableColumnsProps), { wrapper: appMockRender.AppWrapper, }); @@ -56,14 +58,10 @@ describe('useCasesColumns ', () => { "render": [Function], }, Object { - "color": "danger", - "data-test-subj": "cases-files-table-action-delete", "description": "Delete File", - "icon": "trash", "isPrimary": true, "name": "Delete", - "onClick": [Function], - "type": "icon", + "render": [Function], }, ], "name": "Actions", diff --git a/x-pack/plugins/cases/public/components/files/use_files_table_columns.tsx b/x-pack/plugins/cases/public/components/files/use_files_table_columns.tsx index 6664dabed8ed0..f129b81665390 100644 --- a/x-pack/plugins/cases/public/components/files/use_files_table_columns.tsx +++ b/x-pack/plugins/cases/public/components/files/use_files_table_columns.tsx @@ -14,12 +14,15 @@ import * as i18n from './translations'; import { parseMimeType } from './utils'; import { FileNameLink } from './file_name_link'; import { FileDownloadButtonIcon } from './file_download_button_icon'; +import { FileDeleteButtonIcon } from './file_delete_button_icon'; export interface FilesTableColumnsProps { + caseId: string; showPreview: (file: FileJSON) => void; } export const useFilesTableColumns = ({ + caseId, showPreview, }: FilesTableColumnsProps): Array> => { return [ @@ -58,11 +61,7 @@ export const useFilesTableColumns = ({ name: 'Delete', isPrimary: true, description: i18n.DELETE_FILE, - color: 'danger', - icon: 'trash', - type: 'icon', - onClick: () => {}, - 'data-test-subj': 'cases-files-table-action-delete', + render: (file: FileJSON) => , }, ], }, diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 05bef55a2b508..b8da6bd37b20a 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -51,6 +51,7 @@ import { CASE_TAGS_URL, CASES_URL, INTERNAL_BULK_CREATE_ATTACHMENTS_URL, + INTERNAL_DELETE_FILE_ATTACHMENTS_URL, } from '../../common/constants'; import { getAllConnectorTypesUrl } from '../../common/utils/connectors_api'; @@ -401,6 +402,25 @@ export const createAttachments = async ( return convertCaseToCamelCase(decodeCaseResponse(response)); }; +export const deleteFileAttachments = async ({ + caseId, + fileIds, + signal, +}: { + caseId: string; + fileIds: string[]; + signal: AbortSignal; +}): Promise => { + await KibanaServices.get().http.fetch( + INTERNAL_DELETE_FILE_ATTACHMENTS_URL.replace('{case_id}', caseId), + { + method: 'POST', + body: JSON.stringify({ ids: fileIds }), + signal, + } + ); +}; + export const getFeatureIds = async ( query: { registrationContext: string[] }, signal: AbortSignal diff --git a/x-pack/plugins/cases/public/containers/constants.ts b/x-pack/plugins/cases/public/containers/constants.ts index d5dc15c28bb9e..35c607c2d760e 100644 --- a/x-pack/plugins/cases/public/containers/constants.ts +++ b/x-pack/plugins/cases/public/containers/constants.ts @@ -52,4 +52,5 @@ export const casesMutationsKeys = { deleteCases: ['delete-cases'] as const, updateCases: ['update-cases'] as const, deleteComment: ['delete-comment'] as const, + deleteFileAttachment: ['delete-file-attachment'] as const, }; diff --git a/x-pack/plugins/cases/public/containers/use_delete_file_attachment.tsx b/x-pack/plugins/cases/public/containers/use_delete_file_attachment.tsx new file mode 100644 index 0000000000000..04b5a77946bb8 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/use_delete_file_attachment.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMutation } from '@tanstack/react-query'; +import { casesMutationsKeys } from './constants'; +import type { ServerError } from '../types'; +import { useRefreshCaseViewPage } from '../components/case_view/use_on_refresh_case_view_page'; +import { useCasesToast } from '../common/use_cases_toast'; +import { deleteFileAttachments } from './api'; +import * as i18n from './translations'; + +interface MutationArgs { + caseId: string; + fileId: string; + successToasterTitle: string; +} + +export const useDeleteFileAttachment = () => { + const { showErrorToast, showSuccessToast } = useCasesToast(); + const refreshCaseViewPage = useRefreshCaseViewPage(); + + return useMutation( + ({ caseId, fileId }: MutationArgs) => { + const abortCtrlRef = new AbortController(); + return deleteFileAttachments({ caseId, fileIds: [fileId], signal: abortCtrlRef.signal }); + }, + { + mutationKey: casesMutationsKeys.deleteFileAttachment, + onSuccess: (_, { successToasterTitle }) => { + showSuccessToast(successToasterTitle); + refreshCaseViewPage(); + }, + onError: (error: ServerError) => { + showErrorToast(error, { title: i18n.ERROR_TITLE }); + }, + } + ); +}; + +export type UseDeleteFileAttachment = ReturnType; diff --git a/x-pack/plugins/cases/public/containers/use_get_case_file_stats.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case_file_stats.test.tsx index ac7816624d093..c21679ab7b8a7 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_file_stats.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_file_stats.test.tsx @@ -25,7 +25,7 @@ const expectedCallParams = { kind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), page: 1, perPage: 1, - meta: hookParams, + meta: { caseIds: [hookParams.caseId] }, }; describe('useGetCaseFileStats', () => { diff --git a/x-pack/plugins/cases/public/containers/use_get_case_file_stats.tsx b/x-pack/plugins/cases/public/containers/use_get_case_file_stats.tsx index dddd6c8c7a26f..dd444a5bbb5e9 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_file_stats.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_file_stats.tsx @@ -43,7 +43,7 @@ export const useGetCaseFileStats = ({ kind: constructFileKindIdByOwner(owner[0] as Owner), page: 1, perPage: 1, - meta: { caseId }, + meta: { caseIds: [caseId] }, }); }, { diff --git a/x-pack/plugins/cases/public/containers/use_get_case_files.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case_files.test.tsx index e4df138f785d9..88546620aa527 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_files.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_files.test.tsx @@ -29,7 +29,7 @@ const expectedCallParams = { page: hookParams.page + 1, name: `*${hookParams.searchTerm}*`, perPage: hookParams.perPage, - meta: { caseId: hookParams.caseId }, + meta: { caseIds: [hookParams.caseId] }, }; describe('useGetCaseFiles', () => { diff --git a/x-pack/plugins/cases/public/containers/use_get_case_files.tsx b/x-pack/plugins/cases/public/containers/use_get_case_files.tsx index 0166b133e6b26..88fca6d201285 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_files.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_files.tsx @@ -48,7 +48,7 @@ export const useGetCaseFiles = ({ page: page + 1, ...(searchTerm && { name: `*${searchTerm}*` }), perPage, - meta: { caseId }, + meta: { caseIds: [caseId] }, }); }, { From a8159178638d40d6675d099a644080c73eb5b679 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Wed, 5 Apr 2023 09:59:42 +0200 Subject: [PATCH 2/6] Added api tests for deleteFileAttachments. Added FileDeleteButtonIcon tests. Added useDeleteFileAttachment tests. --- .../files/file_delete_button_icon.test.tsx | 61 ++++++++ .../cases/public/containers/__mocks__/api.ts | 10 ++ .../cases/public/containers/api.test.tsx | 27 ++++ .../use_delete_file_attachment.test.tsx | 134 ++++++++++++++++++ .../containers/use_delete_file_attachment.tsx | 4 +- 5 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/files/file_delete_button_icon.test.tsx create mode 100644 x-pack/plugins/cases/public/containers/use_delete_file_attachment.test.tsx diff --git a/x-pack/plugins/cases/public/components/files/file_delete_button_icon.test.tsx b/x-pack/plugins/cases/public/components/files/file_delete_button_icon.test.tsx new file mode 100644 index 0000000000000..2aeb46303c4b3 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_delete_button_icon.test.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import type { AppMockRenderer } from '../../common/mock'; + +import { createAppMockRenderer } from '../../common/mock'; +import { basicCaseId, basicFileMock } from '../../containers/mock'; +import { useDeleteFileAttachment } from '../../containers/use_delete_file_attachment'; +import { FileDeleteButtonIcon } from './file_delete_button_icon'; + +jest.mock('../../containers/use_delete_file_attachment'); + +const useDeleteFileAttachmentMock = useDeleteFileAttachment as jest.Mock; + +describe('FileDeleteButtonIcon', () => { + let appMockRender: AppMockRenderer; + const mutate = jest.fn(); + + useDeleteFileAttachmentMock.mockReturnValue({ isLoading: false, mutate }); + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('renders delete button correctly', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-delete-button')).toBeInTheDocument(); + + expect(useDeleteFileAttachmentMock).toBeCalledTimes(1); + }); + + it('clicking delete button calls deleteFileAttachment with proper params', async () => { + appMockRender.render(); + + const deleteButton = await screen.findByTestId('cases-files-delete-button'); + + expect(deleteButton).toBeInTheDocument(); + + userEvent.click(deleteButton); + + await waitFor(() => { + expect(mutate).toHaveBeenCalledTimes(1); + expect(mutate).toHaveBeenCalledWith({ + caseId: basicCaseId, + fileId: basicFileMock.id, + successToasterTitle: 'File deleted successfully', + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/containers/__mocks__/api.ts b/x-pack/plugins/cases/public/containers/__mocks__/api.ts index 000c8a2f4634b..b29e45f6f101e 100644 --- a/x-pack/plugins/cases/public/containers/__mocks__/api.ts +++ b/x-pack/plugins/cases/public/containers/__mocks__/api.ts @@ -162,3 +162,13 @@ export const getCaseConnectors = async ( export const getCaseUsers = async (caseId: string, signal: AbortSignal): Promise => Promise.resolve(getCaseUsersMockResponse()); + +export const deleteFileAttachments = async ({ + caseId, + fileIds, + signal, +}: { + caseId: string; + fileIds: string[]; + signal: AbortSignal; +}): Promise => Promise.resolve(undefined); diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index e4d7626a36402..3f623795c819c 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -16,6 +16,7 @@ import { INTERNAL_BULK_CREATE_ATTACHMENTS_URL, SECURITY_SOLUTION_OWNER, INTERNAL_GET_CASE_USER_ACTIONS_STATS_URL, + INTERNAL_DELETE_FILE_ATTACHMENTS_URL, } from '../../common/constants'; import { @@ -37,6 +38,7 @@ import { postComment, getCaseConnectors, getCaseUserActionsStats, + deleteFileAttachments, } from './api'; import { @@ -59,6 +61,7 @@ import { caseUserActionsWithRegisteredAttachmentsSnake, basicPushSnake, getCaseUserActionsStatsResponse, + basicFileMock, } from './mock'; import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; @@ -820,6 +823,30 @@ describe('Cases API', () => { }); }); + describe('deleteFileAttachments', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(null); + }); + + it('should be called with correct url, method, signal and body', async () => { + const resp = await deleteFileAttachments({ + caseId: basicCaseId, + fileIds: [basicFileMock.id], + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith( + INTERNAL_DELETE_FILE_ATTACHMENTS_URL.replace('{case_id}', basicCaseId), + { + method: 'POST', + body: JSON.stringify({ ids: [basicFileMock.id] }), + signal: abortCtrl.signal, + } + ); + expect(resp).toBe(undefined); + }); + }); + describe('pushCase', () => { const connectorId = 'connectorId'; diff --git a/x-pack/plugins/cases/public/containers/use_delete_file_attachment.test.tsx b/x-pack/plugins/cases/public/containers/use_delete_file_attachment.test.tsx new file mode 100644 index 0000000000000..e5153185fe284 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/use_delete_file_attachment.test.tsx @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import * as api from './api'; +import { basicCaseId, basicFileMock } from './mock'; +import { useRefreshCaseViewPage } from '../components/case_view/use_on_refresh_case_view_page'; +import { useToasts } from '../common/lib/kibana'; +import type { AppMockRenderer } from '../common/mock'; +import { createAppMockRenderer } from '../common/mock'; +import { useDeleteFileAttachment } from './use_delete_file_attachment'; + +jest.mock('./api'); +jest.mock('../common/lib/kibana'); +jest.mock('../components/case_view/use_on_refresh_case_view_page'); + +const successToasterTitle = 'Deleted'; + +describe('useDeleteFileAttachment', () => { + const addSuccess = jest.fn(); + const addError = jest.fn(); + + (useToasts as jest.Mock).mockReturnValue({ addSuccess, addError }); + + let appMockRender: AppMockRenderer; + + beforeEach(() => { + appMockRender = createAppMockRenderer(); + jest.clearAllMocks(); + }); + + it('init', async () => { + const { result } = renderHook(() => useDeleteFileAttachment(), { + wrapper: appMockRender.AppWrapper, + }); + + expect(result.current).toBeTruthy(); + }); + + it('calls deleteFileAttachment with correct arguments - case', async () => { + const spyOnDeleteFileAttachments = jest.spyOn(api, 'deleteFileAttachments'); + + const { waitForNextUpdate, result } = renderHook(() => useDeleteFileAttachment(), { + wrapper: appMockRender.AppWrapper, + }); + + act(() => { + result.current.mutate({ + caseId: basicCaseId, + fileId: basicFileMock.id, + successToasterTitle, + }); + }); + + await waitForNextUpdate(); + + expect(spyOnDeleteFileAttachments).toHaveBeenCalledWith({ + caseId: basicCaseId, + fileIds: [basicFileMock.id], + signal: expect.any(AbortSignal), + }); + }); + + it('refreshes the case page view', async () => { + const { waitForNextUpdate, result } = renderHook(() => useDeleteFileAttachment(), { + wrapper: appMockRender.AppWrapper, + }); + + act(() => + result.current.mutate({ + caseId: basicCaseId, + fileId: basicFileMock.id, + successToasterTitle, + }) + ); + + await waitForNextUpdate(); + + expect(useRefreshCaseViewPage()).toBeCalled(); + }); + + it('shows a success toaster correctly', async () => { + const { waitForNextUpdate, result } = renderHook(() => useDeleteFileAttachment(), { + wrapper: appMockRender.AppWrapper, + }); + + act(() => + result.current.mutate({ + caseId: basicCaseId, + fileId: basicFileMock.id, + successToasterTitle, + }) + ); + + await waitForNextUpdate(); + + expect(addSuccess).toHaveBeenCalledWith({ + title: successToasterTitle, + className: 'eui-textBreakWord', + }); + }); + + it('sets isError when fails to delete a file attachment', async () => { + const spyOnDeleteFileAttachments = jest.spyOn(api, 'deleteFileAttachments'); + spyOnDeleteFileAttachments.mockRejectedValue(new Error('Error')); + + const { waitForNextUpdate, result } = renderHook(() => useDeleteFileAttachment(), { + wrapper: appMockRender.AppWrapper, + }); + + act(() => + result.current.mutate({ + caseId: basicCaseId, + fileId: basicFileMock.id, + successToasterTitle, + }) + ); + + await waitForNextUpdate(); + + expect(spyOnDeleteFileAttachments).toBeCalledWith({ + caseId: basicCaseId, + fileIds: [basicFileMock.id], + signal: expect.any(AbortSignal), + }); + + expect(addError).toHaveBeenCalled(); + expect(result.current.isError).toBe(true); + }); +}); diff --git a/x-pack/plugins/cases/public/containers/use_delete_file_attachment.tsx b/x-pack/plugins/cases/public/containers/use_delete_file_attachment.tsx index 04b5a77946bb8..056cdff4dd0c3 100644 --- a/x-pack/plugins/cases/public/containers/use_delete_file_attachment.tsx +++ b/x-pack/plugins/cases/public/containers/use_delete_file_attachment.tsx @@ -21,7 +21,7 @@ interface MutationArgs { export const useDeleteFileAttachment = () => { const { showErrorToast, showSuccessToast } = useCasesToast(); - const refreshCaseViewPage = useRefreshCaseViewPage(); + const refreshAttachmentsTable = useRefreshCaseViewPage(); return useMutation( ({ caseId, fileId }: MutationArgs) => { @@ -32,7 +32,7 @@ export const useDeleteFileAttachment = () => { mutationKey: casesMutationsKeys.deleteFileAttachment, onSuccess: (_, { successToasterTitle }) => { showSuccessToast(successToasterTitle); - refreshCaseViewPage(); + refreshAttachmentsTable(); }, onError: (error: ServerError) => { showErrorToast(error, { title: i18n.ERROR_TITLE }); From 483ffce8618460ba908b9d0cdb6f6b0cc2b5684b Mon Sep 17 00:00:00 2001 From: adcoelho Date: Wed, 5 Apr 2023 10:25:18 +0200 Subject: [PATCH 3/6] Changed the gutterSize for actions in case view. --- .../public/components/files/use_files_table_columns.test.tsx | 2 +- .../cases/public/components/user_actions/content_toolbar.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cases/public/components/files/use_files_table_columns.test.tsx b/x-pack/plugins/cases/public/components/files/use_files_table_columns.test.tsx index 214d5c734082a..77070da0dbc57 100644 --- a/x-pack/plugins/cases/public/components/files/use_files_table_columns.test.tsx +++ b/x-pack/plugins/cases/public/components/files/use_files_table_columns.test.tsx @@ -12,7 +12,7 @@ import { createAppMockRenderer } from '../../common/mock'; import { renderHook } from '@testing-library/react-hooks'; import { basicCase } from '../../containers/mock'; -describe('useFilesTableColumns ', () => { +describe('useFilesTableColumns', () => { let appMockRender: AppMockRenderer; const useFilesTableColumnsProps: FilesTableColumnsProps = { diff --git a/x-pack/plugins/cases/public/components/user_actions/content_toolbar.tsx b/x-pack/plugins/cases/public/components/user_actions/content_toolbar.tsx index e7d1dd7ba5eaa..7a0a0de03e18d 100644 --- a/x-pack/plugins/cases/public/components/user_actions/content_toolbar.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/content_toolbar.tsx @@ -21,7 +21,7 @@ const UserActionContentToolbarComponent: React.FC withCopyLinkAction = true, children, }) => ( - + {withCopyLinkAction ? ( From 7fba826e3daf9ab325c6e89e683f61e03ec5fc79 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Wed, 5 Apr 2023 15:21:13 +0200 Subject: [PATCH 4/6] Added file deletion confirmation modal. Addressed PR comments. --- .../files/file_delete_button_icon.test.tsx | 19 +++++++++-- .../files/file_delete_button_icon.tsx | 34 +++++++++++++------ .../public/components/files/translations.tsx | 8 +++-- x-pack/plugins/cases/public/containers/api.ts | 2 +- .../cases/public/containers/translations.ts | 8 +++++ .../use_delete_file_attachment.test.tsx | 16 +-------- .../containers/use_delete_file_attachment.tsx | 7 ++-- 7 files changed, 60 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/cases/public/components/files/file_delete_button_icon.test.tsx b/x-pack/plugins/cases/public/components/files/file_delete_button_icon.test.tsx index 2aeb46303c4b3..7d24f2bd5dfe2 100644 --- a/x-pack/plugins/cases/public/components/files/file_delete_button_icon.test.tsx +++ b/x-pack/plugins/cases/public/components/files/file_delete_button_icon.test.tsx @@ -40,7 +40,7 @@ describe('FileDeleteButtonIcon', () => { expect(useDeleteFileAttachmentMock).toBeCalledTimes(1); }); - it('clicking delete button calls deleteFileAttachment with proper params', async () => { + it('clicking delete button opens the confirmation modal', async () => { appMockRender.render(); const deleteButton = await screen.findByTestId('cases-files-delete-button'); @@ -49,12 +49,27 @@ describe('FileDeleteButtonIcon', () => { userEvent.click(deleteButton); + expect(await screen.findAllByTestId('property-actions-confirm-modal')); + }); + + it('clicking delete button in the confirmation modal calls deleteFileAttachment with proper params', async () => { + appMockRender.render(); + + const deleteButton = await screen.findByTestId('cases-files-delete-button'); + + expect(deleteButton).toBeInTheDocument(); + + userEvent.click(deleteButton); + + expect(await screen.findAllByTestId('property-actions-confirm-modal')); + + userEvent.click(await screen.findByTestId('confirmModalConfirmButton')); + await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1); expect(mutate).toHaveBeenCalledWith({ caseId: basicCaseId, fileId: basicFileMock.id, - successToasterTitle: 'File deleted successfully', }); }); }); diff --git a/x-pack/plugins/cases/public/components/files/file_delete_button_icon.tsx b/x-pack/plugins/cases/public/components/files/file_delete_button_icon.tsx index 8f02b4d04e99f..2e50c76eacaf0 100644 --- a/x-pack/plugins/cases/public/components/files/file_delete_button_icon.tsx +++ b/x-pack/plugins/cases/public/components/files/file_delete_button_icon.tsx @@ -10,6 +10,8 @@ import React from 'react'; import { EuiButtonIcon } from '@elastic/eui'; import * as i18n from './translations'; import { useDeleteFileAttachment } from '../../containers/use_delete_file_attachment'; +import { useDeletePropertyAction } from '../user_actions/property_actions/use_delete_property_action'; +import { DeleteAttachmentConfirmationModal } from '../user_actions/delete_attachment_confirmation_modal'; interface FileDeleteButtonIconProps { caseId: string; @@ -19,17 +21,29 @@ interface FileDeleteButtonIconProps { const FileDeleteButtonIconComponent: React.FC = ({ caseId, fileId }) => { const { isLoading, mutate: deleteFileAttachment } = useDeleteFileAttachment(); + const { showDeletionModal, onModalOpen, onConfirm, onCancel } = useDeletePropertyAction({ + onDelete: () => deleteFileAttachment({ caseId, fileId }), + }); + return ( - - deleteFileAttachment({ caseId, fileId, successToasterTitle: i18n.FILE_DELETE_SUCCESS }) - } - data-test-subj={'cases-files-delete-button'} - /> + <> + + {showDeletionModal ? ( + + ) : null} + ); }; FileDeleteButtonIconComponent.displayName = 'FileDeleteButtonIcon'; diff --git a/x-pack/plugins/cases/public/components/files/translations.tsx b/x-pack/plugins/cases/public/components/files/translations.tsx index a3f5cb7395301..93e2333b211f7 100644 --- a/x-pack/plugins/cases/public/components/files/translations.tsx +++ b/x-pack/plugins/cases/public/components/files/translations.tsx @@ -91,6 +91,10 @@ export const DOWNLOAD = i18n.translate('xpack.cases.caseView.files.download', { defaultMessage: 'download', }); -export const FILE_DELETE_SUCCESS = i18n.translate('xpack.cases.caseView.files.deleteSuccess', { - defaultMessage: 'File deleted successfully', +export const DELETE = i18n.translate('xpack.cases.caseView.files.delete', { + defaultMessage: 'Delete', +}); + +export const DELETE_FILE_TITLE = i18n.translate('xpack.cases.caseView.files.deleteThisFile', { + defaultMessage: 'Delete this file?', }); diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index b8da6bd37b20a..bf3607e65c182 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -411,7 +411,7 @@ export const deleteFileAttachments = async ({ fileIds: string[]; signal: AbortSignal; }): Promise => { - await KibanaServices.get().http.fetch( + await KibanaServices.get().http.fetch( INTERNAL_DELETE_FILE_ATTACHMENTS_URL.replace('{case_id}', caseId), { method: 'POST', diff --git a/x-pack/plugins/cases/public/containers/translations.ts b/x-pack/plugins/cases/public/containers/translations.ts index 892af5864cdc3..0ce62c9196daf 100644 --- a/x-pack/plugins/cases/public/containers/translations.ts +++ b/x-pack/plugins/cases/public/containers/translations.ts @@ -17,6 +17,10 @@ export const ERROR_DELETING = i18n.translate('xpack.cases.containers.errorDeleti defaultMessage: 'Error deleting data', }); +export const ERROR_DELETING_FILE = i18n.translate('xpack.cases.containers.errorDeletingFile', { + defaultMessage: 'Error deleting file', +}); + export const ERROR_UPDATING = i18n.translate('xpack.cases.containers.errorUpdatingTitle', { defaultMessage: 'Error updating data', }); @@ -49,3 +53,7 @@ export const STATUS_CHANGED_TOASTER_TEXT = i18n.translate( defaultMessage: 'Updated the statuses of attached alerts.', } ); + +export const FILE_DELETE_SUCCESS = i18n.translate('xpack.cases.containers.deleteSuccess', { + defaultMessage: 'File deleted successfully', +}); diff --git a/x-pack/plugins/cases/public/containers/use_delete_file_attachment.test.tsx b/x-pack/plugins/cases/public/containers/use_delete_file_attachment.test.tsx index e5153185fe284..b738f2b50febd 100644 --- a/x-pack/plugins/cases/public/containers/use_delete_file_attachment.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_delete_file_attachment.test.tsx @@ -18,8 +18,6 @@ jest.mock('./api'); jest.mock('../common/lib/kibana'); jest.mock('../components/case_view/use_on_refresh_case_view_page'); -const successToasterTitle = 'Deleted'; - describe('useDeleteFileAttachment', () => { const addSuccess = jest.fn(); const addError = jest.fn(); @@ -33,14 +31,6 @@ describe('useDeleteFileAttachment', () => { jest.clearAllMocks(); }); - it('init', async () => { - const { result } = renderHook(() => useDeleteFileAttachment(), { - wrapper: appMockRender.AppWrapper, - }); - - expect(result.current).toBeTruthy(); - }); - it('calls deleteFileAttachment with correct arguments - case', async () => { const spyOnDeleteFileAttachments = jest.spyOn(api, 'deleteFileAttachments'); @@ -52,7 +42,6 @@ describe('useDeleteFileAttachment', () => { result.current.mutate({ caseId: basicCaseId, fileId: basicFileMock.id, - successToasterTitle, }); }); @@ -74,7 +63,6 @@ describe('useDeleteFileAttachment', () => { result.current.mutate({ caseId: basicCaseId, fileId: basicFileMock.id, - successToasterTitle, }) ); @@ -92,14 +80,13 @@ describe('useDeleteFileAttachment', () => { result.current.mutate({ caseId: basicCaseId, fileId: basicFileMock.id, - successToasterTitle, }) ); await waitForNextUpdate(); expect(addSuccess).toHaveBeenCalledWith({ - title: successToasterTitle, + title: 'File deleted successfully', className: 'eui-textBreakWord', }); }); @@ -116,7 +103,6 @@ describe('useDeleteFileAttachment', () => { result.current.mutate({ caseId: basicCaseId, fileId: basicFileMock.id, - successToasterTitle, }) ); diff --git a/x-pack/plugins/cases/public/containers/use_delete_file_attachment.tsx b/x-pack/plugins/cases/public/containers/use_delete_file_attachment.tsx index 056cdff4dd0c3..8f6c0effb7b2c 100644 --- a/x-pack/plugins/cases/public/containers/use_delete_file_attachment.tsx +++ b/x-pack/plugins/cases/public/containers/use_delete_file_attachment.tsx @@ -16,7 +16,6 @@ import * as i18n from './translations'; interface MutationArgs { caseId: string; fileId: string; - successToasterTitle: string; } export const useDeleteFileAttachment = () => { @@ -30,12 +29,12 @@ export const useDeleteFileAttachment = () => { }, { mutationKey: casesMutationsKeys.deleteFileAttachment, - onSuccess: (_, { successToasterTitle }) => { - showSuccessToast(successToasterTitle); + onSuccess: () => { + showSuccessToast(i18n.FILE_DELETE_SUCCESS); refreshAttachmentsTable(); }, onError: (error: ServerError) => { - showErrorToast(error, { title: i18n.ERROR_TITLE }); + showErrorToast(error, { title: i18n.ERROR_DELETING_FILE }); }, } ); From 9009f411958bc3651281c78334b925e0bf18ce40 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Wed, 5 Apr 2023 17:26:48 +0200 Subject: [PATCH 5/6] Added delete button tests for file_type. Added delete button tests for files_table. --- .../files/file_delete_button_icon.test.tsx | 4 +-- .../components/files/file_type.test.tsx | 25 ++++++++++++++++ .../components/files/files_table.test.tsx | 30 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cases/public/components/files/file_delete_button_icon.test.tsx b/x-pack/plugins/cases/public/components/files/file_delete_button_icon.test.tsx index 7d24f2bd5dfe2..5629ae4fcf6c0 100644 --- a/x-pack/plugins/cases/public/components/files/file_delete_button_icon.test.tsx +++ b/x-pack/plugins/cases/public/components/files/file_delete_button_icon.test.tsx @@ -49,7 +49,7 @@ describe('FileDeleteButtonIcon', () => { userEvent.click(deleteButton); - expect(await screen.findAllByTestId('property-actions-confirm-modal')); + expect(await screen.findByTestId('property-actions-confirm-modal')).toBeInTheDocument(); }); it('clicking delete button in the confirmation modal calls deleteFileAttachment with proper params', async () => { @@ -61,7 +61,7 @@ describe('FileDeleteButtonIcon', () => { userEvent.click(deleteButton); - expect(await screen.findAllByTestId('property-actions-confirm-modal')); + expect(await screen.findByTestId('property-actions-confirm-modal')).toBeInTheDocument(); userEvent.click(await screen.findByTestId('confirmModalConfirmButton')); diff --git a/x-pack/plugins/cases/public/components/files/file_type.test.tsx b/x-pack/plugins/cases/public/components/files/file_type.test.tsx index bc8de8d43d40f..28ff713b4a63a 100644 --- a/x-pack/plugins/cases/public/components/files/file_type.test.tsx +++ b/x-pack/plugins/cases/public/components/files/file_type.test.tsx @@ -15,6 +15,7 @@ import { FILE_ATTACHMENT_TYPE } from '../../../common/api'; import { createAppMockRenderer } from '../../common/mock'; import { basicCase, basicFileMock } from '../../containers/mock'; import { getFileType } from './file_type'; +import userEvent from '@testing-library/user-event'; describe('getFileType', () => { const fileType = getFileType(); @@ -62,6 +63,30 @@ describe('getFileType', () => { expect(await screen.findByTestId('cases-files-download-button')).toBeInTheDocument(); }); + it('actions renders a delete button', async () => { + appMockRender = createAppMockRenderer(); + + // @ts-ignore + appMockRender.render(fileType.getAttachmentViewObject({ ...attachmentViewProps }).actions); + + expect(await screen.findByTestId('cases-files-delete-button')).toBeInTheDocument(); + }); + + it('clicking the delete button in actions opens deletion modal', async () => { + appMockRender = createAppMockRenderer(); + + // @ts-ignore + appMockRender.render(fileType.getAttachmentViewObject({ ...attachmentViewProps }).actions); + + const deleteButton = await screen.findByTestId('cases-files-delete-button'); + + expect(deleteButton).toBeInTheDocument(); + + userEvent.click(deleteButton); + + expect(await screen.findByTestId('property-actions-confirm-modal')).toBeInTheDocument(); + }); + it('empty externalReferenceMetadata returns blank FileAttachmentViewObject', () => { expect( fileType.getAttachmentViewObject({ ...attachmentViewProps, externalReferenceMetadata: {} }) diff --git a/x-pack/plugins/cases/public/components/files/files_table.test.tsx b/x-pack/plugins/cases/public/components/files/files_table.test.tsx index 144d48a688992..5f877db3df9b4 100644 --- a/x-pack/plugins/cases/public/components/files/files_table.test.tsx +++ b/x-pack/plugins/cases/public/components/files/files_table.test.tsx @@ -131,6 +131,36 @@ describe('FilesTable', () => { expect(await screen.findByTestId('cases-files-download-button')).toBeInTheDocument(); }); + it('delete button renders correctly', async () => { + appMockRender.render(); + + expect(mockedFilesClient.getDownloadHref).toBeCalledTimes(1); + expect(mockedFilesClient.getDownloadHref).toHaveBeenCalledWith({ + fileKind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), + id: basicFileMock.id, + }); + + expect(await screen.findByTestId('cases-files-delete-button')).toBeInTheDocument(); + }); + + it('clicking delete button opens deletion modal', async () => { + appMockRender.render(); + + expect(mockedFilesClient.getDownloadHref).toBeCalledTimes(1); + expect(mockedFilesClient.getDownloadHref).toHaveBeenCalledWith({ + fileKind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), + id: basicFileMock.id, + }); + + const deleteButton = await screen.findByTestId('cases-files-delete-button'); + + expect(deleteButton).toBeInTheDocument(); + + userEvent.click(deleteButton); + + expect(await screen.findByTestId('property-actions-confirm-modal')).toBeInTheDocument(); + }); + it('go to next page calls onTableChange with correct values', async () => { const mockPagination = { pageIndex: 0, pageSize: 1, totalItemCount: 2 }; From 89416646578c7ec2091e606eb5667faa289fa75f Mon Sep 17 00:00:00 2001 From: adcoelho Date: Thu, 6 Apr 2023 11:48:49 +0200 Subject: [PATCH 6/6] Disable delete button if user has no permissions. Tests. --- .../files/file_delete_button_icon.test.tsx | 12 +++++++++++- .../components/files/file_delete_button_icon.tsx | 4 +++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cases/public/components/files/file_delete_button_icon.test.tsx b/x-pack/plugins/cases/public/components/files/file_delete_button_icon.test.tsx index 5629ae4fcf6c0..aeabb56b977c8 100644 --- a/x-pack/plugins/cases/public/components/files/file_delete_button_icon.test.tsx +++ b/x-pack/plugins/cases/public/components/files/file_delete_button_icon.test.tsx @@ -12,7 +12,7 @@ import userEvent from '@testing-library/user-event'; import type { AppMockRenderer } from '../../common/mock'; -import { createAppMockRenderer } from '../../common/mock'; +import { buildCasesPermissions, createAppMockRenderer } from '../../common/mock'; import { basicCaseId, basicFileMock } from '../../containers/mock'; import { useDeleteFileAttachment } from '../../containers/use_delete_file_attachment'; import { FileDeleteButtonIcon } from './file_delete_button_icon'; @@ -73,4 +73,14 @@ describe('FileDeleteButtonIcon', () => { }); }); }); + + it('delete button is disabled if user has no delete permission', async () => { + appMockRender = createAppMockRenderer({ + permissions: buildCasesPermissions({ delete: false }), + }); + + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-delete-button')).toBeDisabled(); + }); }); diff --git a/x-pack/plugins/cases/public/components/files/file_delete_button_icon.tsx b/x-pack/plugins/cases/public/components/files/file_delete_button_icon.tsx index 2e50c76eacaf0..86929d5b49e29 100644 --- a/x-pack/plugins/cases/public/components/files/file_delete_button_icon.tsx +++ b/x-pack/plugins/cases/public/components/files/file_delete_button_icon.tsx @@ -12,6 +12,7 @@ import * as i18n from './translations'; import { useDeleteFileAttachment } from '../../containers/use_delete_file_attachment'; import { useDeletePropertyAction } from '../user_actions/property_actions/use_delete_property_action'; import { DeleteAttachmentConfirmationModal } from '../user_actions/delete_attachment_confirmation_modal'; +import { useCasesContext } from '../cases_context/use_cases_context'; interface FileDeleteButtonIconProps { caseId: string; @@ -19,6 +20,7 @@ interface FileDeleteButtonIconProps { } const FileDeleteButtonIconComponent: React.FC = ({ caseId, fileId }) => { + const { permissions } = useCasesContext(); const { isLoading, mutate: deleteFileAttachment } = useDeleteFileAttachment(); const { showDeletionModal, onModalOpen, onConfirm, onCancel } = useDeletePropertyAction({ @@ -31,7 +33,7 @@ const FileDeleteButtonIconComponent: React.FC = ({ ca iconType={'trash'} aria-label={i18n.DELETE_FILE} color={'danger'} - isDisabled={isLoading} + isDisabled={isLoading || !permissions.delete} onClick={onModalOpen} data-test-subj={'cases-files-delete-button'} />