diff --git a/x-pack/plugins/cases/common/api/cases/comment/files.ts b/x-pack/plugins/cases/common/api/cases/comment/files.ts index 58fee11997c74..cafa92df1b79a 100644 --- a/x-pack/plugins/cases/common/api/cases/comment/files.ts +++ b/x-pack/plugins/cases/common/api/cases/comment/files.ts @@ -13,7 +13,7 @@ export const FileAttachmentMetadataRt = rt.type({ name: rt.string, extension: rt.string, mimeType: rt.string, - createdAt: rt.string, + created: rt.string, }) ), }); diff --git a/x-pack/plugins/cases/public/client/attachment_framework/types.ts b/x-pack/plugins/cases/public/client/attachment_framework/types.ts index ea5c046a37e7c..55f3a104b391e 100644 --- a/x-pack/plugins/cases/public/client/attachment_framework/types.ts +++ b/x-pack/plugins/cases/public/client/attachment_framework/types.ts @@ -18,6 +18,7 @@ export interface AttachmentViewObject { actions?: EuiCommentProps['actions']; event?: EuiCommentProps['event']; children?: React.LazyExoticComponent>; + hideDefaultActions?: boolean; } export interface CommonAttachmentViewProps { @@ -38,7 +39,7 @@ export interface AttachmentType { id: string; icon: IconType; displayName: string; - getAttachmentViewObject: () => AttachmentViewObject; + getAttachmentViewObject: (props: Props) => AttachmentViewObject; } export type ExternalReferenceAttachmentType = AttachmentType; diff --git a/x-pack/plugins/cases/public/components/cases_context/index.tsx b/x-pack/plugins/cases/public/components/cases_context/index.tsx index 1031ec2927609..2fdfdfafefe8b 100644 --- a/x-pack/plugins/cases/public/components/cases_context/index.tsx +++ b/x-pack/plugins/cases/public/components/cases_context/index.tsx @@ -26,11 +26,11 @@ import type { ExternalReferenceAttachmentTypeRegistry } from '../../client/attac import type { PersistableStateAttachmentTypeRegistry } from '../../client/attachment_framework/persistable_state_registry'; import { CasesGlobalComponents } from './cases_global_components'; -import { DEFAULT_FEATURES } from '../../../common/constants'; +import { constructFileKindIdByOwner, DEFAULT_FEATURES } from '../../../common/constants'; import { DEFAULT_BASE_PATH } from '../../common/navigation'; import { useApplication } from './use_application'; import { casesContextReducer, getInitialCasesContextState } from './cases_context_reducer'; -import { isRegisteredOwner, CASES_FILE_KINDS } from '../../files'; +import { isRegisteredOwner } from '../../files'; export type CasesContextValueDispatch = Dispatch; @@ -132,7 +132,7 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({ if (isRegisteredOwner(owner[0])) { return ( - + {contextChildren} ); 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 0ca08823bebc5..93c75fc79bafa 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 @@ -146,31 +146,30 @@ describe('AddFile', () => { userEvent.click(await screen.findByTestId('testOnDone')); await waitFor(() => - expect(createAttachmentsMock).toBeCalledWith( - expect.objectContaining({ - caseId: 'foobar', - caseOwner: mockedTestProvidersOwner[0], - data: [ - { - externalReferenceAttachmentTypeId: '.files', - externalReferenceId: mockedExternalReferenceId, - externalReferenceMetadata: { - files: [ - { - createdAt: '2020-02-19T23:06:33.798Z', - extension: 'png', - mimeType: 'image/png', - name: 'my-super-cool-screenshot', - }, - ], - }, - externalReferenceStorage: { soType: 'file', type: 'savedObject' }, - type: 'externalReference', + expect(createAttachmentsMock).toBeCalledWith({ + caseId: 'foobar', + caseOwner: mockedTestProvidersOwner[0], + data: [ + { + externalReferenceAttachmentTypeId: '.files', + externalReferenceId: mockedExternalReferenceId, + externalReferenceMetadata: { + files: [ + { + created: '2020-02-19T23:06:33.798Z', + extension: 'png', + mimeType: 'image/png', + name: 'my-super-cool-screenshot', + }, + ], }, - ], - throwOnError: true, - }) - ) + externalReferenceStorage: { soType: 'file', type: 'savedObject' }, + type: 'externalReference', + }, + ], + throwOnError: true, + updateCase: expect.any(Function), + }) ); await waitFor(() => 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 e87e56b6250fc..b628e93f72751 100644 --- a/x-pack/plugins/cases/public/components/files/add_file.tsx +++ b/x-pack/plugins/cases/public/components/files/add_file.tsx @@ -83,7 +83,7 @@ const AddFileComponent: React.FC = ({ caseId }) => { name: file.fileJSON.name, extension: file.fileJSON.extension ?? '', mimeType: file.fileJSON.mimeType ?? '', - createdAt: file.fileJSON.created, + created: file.fileJSON.created, }, ], }, diff --git a/x-pack/plugins/cases/public/components/files/file_download_button_icon.test.tsx b/x-pack/plugins/cases/public/components/files/file_download_button_icon.test.tsx new file mode 100644 index 0000000000000..3491c51656bbb --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_download_button_icon.test.tsx @@ -0,0 +1,40 @@ +/* + * 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 } from '@testing-library/react'; + +import type { AppMockRenderer } from '../../common/mock'; +import { + createAppMockRenderer, + mockedFilesClient, + mockedTestProvidersOwner, +} from '../../common/mock'; +import { FileDownloadButtonIcon } from './file_download_button_icon'; +import { basicFileMock } from '../../containers/mock'; +import { constructFileKindIdByOwner } from '../../../common/constants'; + +describe('FileDownloadButtonIcon', () => { + let appMockRender: AppMockRenderer; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('renders download button with correct href', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-download-button')).toBeInTheDocument(); + + expect(mockedFilesClient.getDownloadHref).toBeCalledTimes(1); + expect(mockedFilesClient.getDownloadHref).toHaveBeenCalledWith({ + fileKind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), + id: basicFileMock.id, + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/files/file_download_button_icon.tsx b/x-pack/plugins/cases/public/components/files/file_download_button_icon.tsx new file mode 100644 index 0000000000000..d731b9a31f335 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_download_button_icon.tsx @@ -0,0 +1,41 @@ +/* + * 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 { useFilesContext } from '@kbn/shared-ux-file-context'; + +import type { Owner } from '../../../common/constants/types'; + +import { constructFileKindIdByOwner } from '../../../common/constants'; +import { useCasesContext } from '../cases_context/use_cases_context'; +import * as i18n from './translations'; + +interface FileDownloadButtonIconProps { + fileId: string; +} + +const FileDownloadButtonIconComponent: React.FC = ({ fileId }) => { + const { owner } = useCasesContext(); + const { client: filesClient } = useFilesContext(); + + return ( + + ); +}; +FileDownloadButtonIconComponent.displayName = 'FileDownloadButtonIcon'; + +export const FileDownloadButtonIcon = React.memo(FileDownloadButtonIconComponent); diff --git a/x-pack/plugins/cases/public/components/files/file_name_link.test.tsx b/x-pack/plugins/cases/public/components/files/file_name_link.test.tsx new file mode 100644 index 0000000000000..e86d85fb02d43 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_name_link.test.tsx @@ -0,0 +1,58 @@ +/* + * 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 type { AppMockRenderer } from '../../common/mock'; +import { createAppMockRenderer } from '../../common/mock'; +import userEvent from '@testing-library/user-event'; +import { FileNameLink } from './file_name_link'; +import { basicFileMock } from '../../containers/mock'; + +describe('FileNameLink', () => { + let appMockRender: AppMockRenderer; + + const defaultProps = { + file: basicFileMock, + showPreview: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('renders clickable name if file is image', async () => { + appMockRender.render(); + + const nameLink = await screen.findByTestId('cases-files-name-link'); + + expect(nameLink).toBeInTheDocument(); + + userEvent.click(nameLink); + + await waitFor(() => expect(defaultProps.showPreview).toHaveBeenCalled()); + }); + + it('renders simple text name if file is not image', async () => { + appMockRender.render( + + ); + + const nameLink = await screen.findByTestId('cases-files-name-text'); + + expect(nameLink).toBeInTheDocument(); + + userEvent.click(nameLink); + + await waitFor(() => expect(defaultProps.showPreview).not.toHaveBeenCalled()); + }); +}); diff --git a/x-pack/plugins/cases/public/components/files/file_name_link.tsx b/x-pack/plugins/cases/public/components/files/file_name_link.tsx new file mode 100644 index 0000000000000..9401460410a8f --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_name_link.tsx @@ -0,0 +1,40 @@ +/* + * 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 { EuiLink } from '@elastic/eui'; +import type { FileJSON } from '@kbn/shared-ux-file-types'; + +import * as i18n from './translations'; +import { isImage } from './utils'; + +interface FileNameLinkProps { + file: FileJSON; + showPreview: () => void; +} + +const FileNameLinkComponent: React.FC = ({ file, showPreview }) => { + const fileName = `${file.name}.${file.extension}`; + + if (isImage(file)) { + return ( + + {fileName} + + ); + } else { + return ( + + {fileName} + + ); + } +}; +FileNameLinkComponent.displayName = 'FileNameLink'; + +export const FileNameLink = React.memo(FileNameLinkComponent); diff --git a/x-pack/plugins/cases/public/components/files/file_preview.test.tsx b/x-pack/plugins/cases/public/components/files/file_preview.test.tsx index 48efac8aa64b5..43221f7843592 100644 --- a/x-pack/plugins/cases/public/components/files/file_preview.test.tsx +++ b/x-pack/plugins/cases/public/components/files/file_preview.test.tsx @@ -11,7 +11,11 @@ import { screen, waitFor } from '@testing-library/react'; import type { AppMockRenderer } from '../../common/mock'; import { constructFileKindIdByOwner } from '../../../common/constants'; -import { createAppMockRenderer, mockedTestProvidersOwner } from '../../common/mock'; +import { + createAppMockRenderer, + mockedTestProvidersOwner, + mockedFilesClient, +} from '../../common/mock'; import { basicFileMock } from '../../containers/mock'; import { FilePreview } from './file_preview'; @@ -24,18 +28,10 @@ describe('FilePreview', () => { }); it('FilePreview rendered correctly', async () => { - const mockGetDownloadRef = jest.fn(); - - appMockRender.render( - - ); + appMockRender.render(); await waitFor(() => - expect(mockGetDownloadRef).toBeCalledWith({ + expect(mockedFilesClient.getDownloadHref).toHaveBeenCalledWith({ id: basicFileMock.id, fileKind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), }) diff --git a/x-pack/plugins/cases/public/components/files/file_preview.tsx b/x-pack/plugins/cases/public/components/files/file_preview.tsx index 19429861ba389..48041bfc75649 100644 --- a/x-pack/plugins/cases/public/components/files/file_preview.tsx +++ b/x-pack/plugins/cases/public/components/files/file_preview.tsx @@ -10,6 +10,7 @@ import styled from 'styled-components'; import type { FileJSON } from '@kbn/shared-ux-file-types'; import { EuiOverlayMask, EuiFocusTrap, EuiImage } from '@elastic/eui'; +import { useFilesContext } from '@kbn/shared-ux-file-context'; import type { Owner } from '../../../common/constants/types'; @@ -18,7 +19,6 @@ import { useCasesContext } from '../cases_context/use_cases_context'; interface FilePreviewProps { closePreview: () => void; - getDownloadHref: (args: Pick, 'id' | 'fileKind'>) => string; selectedFile: FileJSON; } @@ -32,7 +32,8 @@ const StyledOverlayMask = styled(EuiOverlayMask)` } `; -export const FilePreview = ({ closePreview, selectedFile, getDownloadHref }: FilePreviewProps) => { +export const FilePreview = ({ closePreview, selectedFile }: FilePreviewProps) => { + const { client: filesClient } = useFilesContext(); const { owner } = useCasesContext(); return ( @@ -41,7 +42,7 @@ export const FilePreview = ({ closePreview, selectedFile, getDownloadHref }: Fil { + const fileType = getFileType(); + + it('invalid props return blank FileAttachmentViewObject', () => { + expect(fileType).toStrictEqual({ + id: FILE_ATTACHMENT_TYPE, + icon: 'document', + displayName: 'File Attachment Type', + getAttachmentViewObject: expect.any(Function), + }); + }); + + describe('getFileAttachmentViewObject', () => { + let appMockRender: AppMockRenderer; + + const attachmentViewProps = { + externalReferenceId: basicFileMock.id, + externalReferenceMetadata: { files: [basicFileMock] }, + caseData: { title: basicCase.title, id: basicCase.id }, + } as unknown as ExternalReferenceAttachmentViewProps; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('event renders a clickable name if the file is an image', async () => { + appMockRender = createAppMockRenderer(); + + // @ts-ignore + appMockRender.render(fileType.getAttachmentViewObject({ ...attachmentViewProps }).event); + + const nameLink = await screen.findByTestId('cases-files-name-link'); + + expect(nameLink).toBeInTheDocument(); + expect(nameLink).toHaveTextContent('my-super-cool-screenshot.png'); + }); + + it('actions renders a download button', async () => { + appMockRender = createAppMockRenderer(); + + // @ts-ignore + appMockRender.render(fileType.getAttachmentViewObject({ ...attachmentViewProps }).actions); + + expect(await screen.findByTestId('cases-files-download-button')).toBeInTheDocument(); + }); + + it('empty externalReferenceMetadata returns blank FileAttachmentViewObject', () => { + expect( + fileType.getAttachmentViewObject({ ...attachmentViewProps, externalReferenceMetadata: {} }) + ).toEqual({ + event: 'added an unknown file', + hideDefaultActions: true, + timelineAvatar: 'document', + type: 'regular', + }); + }); + + it('timelineAvatar is image if file is an image', () => { + expect(fileType.getAttachmentViewObject(attachmentViewProps)).toEqual( + expect.objectContaining({ + timelineAvatar: 'image', + }) + ); + }); + + it('timelineAvatar is document if file is not an image', () => { + expect( + fileType.getAttachmentViewObject({ + ...attachmentViewProps, + externalReferenceMetadata: { + files: [{ ...basicFileMock, mimeType: 'text/csv' } as JsonValue], + }, + }) + ).toEqual( + expect.objectContaining({ + timelineAvatar: 'document', + }) + ); + }); + + it('default actions should be hidden', () => { + expect(fileType.getAttachmentViewObject(attachmentViewProps)).toEqual( + expect.objectContaining({ + hideDefaultActions: true, + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/files/file_type.tsx b/x-pack/plugins/cases/public/components/files/file_type.tsx new file mode 100644 index 0000000000000..b9d1f5e1342d0 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_type.tsx @@ -0,0 +1,75 @@ +/* + * 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 type { FileJSON } from '@kbn/shared-ux-file-types'; + +import type { + ExternalReferenceAttachmentType, + ExternalReferenceAttachmentViewProps, +} from '../../client/attachment_framework/types'; + +import { FILE_ATTACHMENT_TYPE } from '../../../common/api'; +import { FileDownloadButtonIcon } from './file_download_button_icon'; +import { FileNameLink } from './file_name_link'; +import { FilePreview } from './file_preview'; +import * as i18n from './translations'; +import { isImage, isValidFileExternalReferenceMetadata } from './utils'; +import { useFilePreview } from './use_file_preview'; + +interface FileAttachmentEventProps { + file: FileJSON; +} + +const FileAttachmentEvent = ({ file }: FileAttachmentEventProps) => { + const { isPreviewVisible, showPreview, closePreview } = useFilePreview(); + + return ( + <> + {i18n.ADDED} + + {isPreviewVisible && } + + ); +}; + +FileAttachmentEvent.displayName = 'FileAttachmentEvent'; + +const getFileAttachmentViewObject = (props: ExternalReferenceAttachmentViewProps) => { + if (!isValidFileExternalReferenceMetadata(props.externalReferenceMetadata)) { + return { + type: 'regular', + event: i18n.ADDED_UNKNOWN_FILE, + timelineAvatar: 'document', + hideDefaultActions: true, + }; + } + + const fileId = props.externalReferenceId; + + // @ts-ignore + const partialFileJSON = props.externalReferenceMetadata?.files[0] as Partial; + + const file = { + id: fileId, + ...partialFileJSON, + } as FileJSON; + + return { + event: , + timelineAvatar: isImage(file) ? 'image' : 'document', + actions: , + hideDefaultActions: true, + }; +}; + +export const getFileType = (): ExternalReferenceAttachmentType => ({ + id: FILE_ATTACHMENT_TYPE, + icon: 'document', + displayName: 'File Attachment Type', + getAttachmentViewObject: getFileAttachmentViewObject, +}); 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 8caa94c4caa21..822a3a1402b1e 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 @@ -44,7 +44,7 @@ describe('FilesTable', () => { expect(await screen.findByTestId('cases-files-table-filename')).toBeInTheDocument(); 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-table-action-download')).toBeInTheDocument(); + expect(await screen.findByTestId('cases-files-download-button')).toBeInTheDocument(); expect(await screen.findByTestId('cases-files-table-action-delete')).toBeInTheDocument(); }); @@ -128,7 +128,7 @@ describe('FilesTable', () => { id: basicFileMock.id, }); - expect(await screen.findByTestId('cases-files-table-action-download')).toBeInTheDocument(); + expect(await screen.findByTestId('cases-files-download-button')).toBeInTheDocument(); }); it('go to next page calls onTableChange with correct values', 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 247ca60fd3e8f..c8cc6320b3bbe 100644 --- a/x-pack/plugins/cases/public/components/files/files_table.tsx +++ b/x-pack/plugins/cases/public/components/files/files_table.tsx @@ -10,12 +10,12 @@ import type { Pagination, EuiBasicTableProps } from '@elastic/eui'; import type { FileJSON } from '@kbn/shared-ux-file-types'; import { EuiBasicTable, EuiLoadingContent, EuiSpacer, EuiText, EuiEmptyPrompt } from '@elastic/eui'; -import { useFilesContext } from '@kbn/shared-ux-file-context'; import * as i18n from './translations'; import { useFilesTableColumns } from './use_files_table_columns'; import { FilePreview } from './file_preview'; import { AddFile } from './add_file'; +import { useFilePreview } from './use_file_preview'; const EmptyFilesTable = ({ caseId }: { caseId: string }) => ( { - const { client: filesClient } = useFilesContext(); - const [isPreviewVisible, setIsPreviewVisible] = useState(false); + const { isPreviewVisible, showPreview, closePreview } = useFilePreview(); + const [selectedFile, setSelectedFile] = useState(); - const closePreview = () => setIsPreviewVisible(false); - const showPreview = (file: FileJSON) => { + const displayPreview = (file: FileJSON) => { setSelectedFile(file); - setIsPreviewVisible(true); + showPreview(); }; - const columns = useFilesTableColumns({ - showPreview, - getDownloadHref: filesClient.getDownloadHref, - }); + const columns = useFilesTableColumns({ showPreview: displayPreview }); return isLoading ? ( <> @@ -78,11 +74,7 @@ export const FilesTable = ({ caseId, items, pagination, onChange, isLoading }: F noItemsMessage={} /> {isPreviewVisible && selectedFile !== undefined && ( - + )} ); diff --git a/x-pack/plugins/cases/public/components/files/translations.tsx b/x-pack/plugins/cases/public/components/files/translations.tsx index 6ea6080d3cb33..bec9fea1447a0 100644 --- a/x-pack/plugins/cases/public/components/files/translations.tsx +++ b/x-pack/plugins/cases/public/components/files/translations.tsx @@ -78,3 +78,15 @@ export const SHOWING_FILES = (totalFiles: number) => values: { totalFiles }, defaultMessage: 'Showing {totalFiles} {totalFiles, plural, =1 {file} other {files}}', }); + +export const ADDED = i18n.translate('xpack.cases.caseView.files.added', { + defaultMessage: 'added ', +}); + +export const ADDED_UNKNOWN_FILE = i18n.translate('xpack.cases.caseView.files.addedUnknownFile', { + defaultMessage: 'added an unknown file', +}); + +export const DOWNLOAD = i18n.translate('xpack.cases.caseView.files.download', { + defaultMessage: 'download', +}); diff --git a/x-pack/plugins/cases/public/components/files/use_file_preview.test.tsx b/x-pack/plugins/cases/public/components/files/use_file_preview.test.tsx new file mode 100644 index 0000000000000..49e18fb818cd9 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/use_file_preview.test.tsx @@ -0,0 +1,54 @@ +/* + * 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 { useFilePreview } from './use_file_preview'; + +describe('useFilePreview', () => { + it('isPreviewVisible is false by default', () => { + const { result } = renderHook(() => { + return useFilePreview(); + }); + + expect(result.current.isPreviewVisible).toBeFalsy(); + }); + + it('showPreview sets isPreviewVisible to true', () => { + const { result } = renderHook(() => { + return useFilePreview(); + }); + + expect(result.current.isPreviewVisible).toBeFalsy(); + + act(() => { + result.current.showPreview(); + }); + + expect(result.current.isPreviewVisible).toBeTruthy(); + }); + + it('closePreview sets isPreviewVisible to false', () => { + const { result } = renderHook(() => { + return useFilePreview(); + }); + + expect(result.current.isPreviewVisible).toBeFalsy(); + + act(() => { + result.current.showPreview(); + }); + + expect(result.current.isPreviewVisible).toBeTruthy(); + + act(() => { + result.current.closePreview(); + }); + + expect(result.current.isPreviewVisible).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/files/use_file_preview.tsx b/x-pack/plugins/cases/public/components/files/use_file_preview.tsx new file mode 100644 index 0000000000000..c802aa38fc688 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/use_file_preview.tsx @@ -0,0 +1,17 @@ +/* + * 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 { useState } from 'react'; + +export const useFilePreview = () => { + const [isPreviewVisible, setIsPreviewVisible] = useState(false); + + const closePreview = () => setIsPreviewVisible(false); + const showPreview = () => setIsPreviewVisible(true); + + return { isPreviewVisible, showPreview, closePreview }; +}; 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 6ee4cbff94c6f..dab14c297b76f 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 @@ -16,7 +16,6 @@ describe('useCasesColumns ', () => { const useCasesColumnsProps: FilesTableColumnsProps = { showPreview: () => {}, - getDownloadHref: jest.fn(), }; beforeEach(() => { 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 7477f71128162..6664dabed8ed0 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 @@ -10,38 +10,25 @@ import React from 'react'; import type { EuiBasicTableColumn } from '@elastic/eui'; import type { FileJSON } from '@kbn/shared-ux-file-types'; -import { EuiLink, EuiButtonIcon } from '@elastic/eui'; - -import type { Owner } from '../../../common/constants/types'; - -import { constructFileKindIdByOwner } from '../../../common/constants'; -import { useCasesContext } from '../cases_context/use_cases_context'; import * as i18n from './translations'; -import { isImage, parseMimeType } from './utils'; +import { parseMimeType } from './utils'; +import { FileNameLink } from './file_name_link'; +import { FileDownloadButtonIcon } from './file_download_button_icon'; export interface FilesTableColumnsProps { showPreview: (file: FileJSON) => void; - getDownloadHref: (args: Pick, 'id' | 'fileKind'>) => string; } export const useFilesTableColumns = ({ showPreview, - getDownloadHref, }: FilesTableColumnsProps): Array> => { - const { owner } = useCasesContext(); - return [ { name: i18n.NAME, 'data-test-subj': 'cases-files-table-filename', - render: (attachment: FileJSON) => { - const fileName = `${attachment.name}.${attachment.extension}`; - if (isImage(attachment)) { - return showPreview(attachment)}>{fileName}; - } else { - return {fileName}; - } - }, + render: (file: FileJSON) => ( + showPreview(file)} /> + ), width: '60%', }, { @@ -65,19 +52,7 @@ export const useFilesTableColumns = ({ name: 'Download', isPrimary: true, description: i18n.DOWNLOAD_FILE, - render: (attachment: FileJSON) => { - return ( - - ); - }, + render: (file: FileJSON) => , }, { name: 'Delete', diff --git a/x-pack/plugins/cases/public/components/files/utils.test.tsx b/x-pack/plugins/cases/public/components/files/utils.test.tsx index d202a5dd15433..a77d5cc39c31f 100644 --- a/x-pack/plugins/cases/public/components/files/utils.test.tsx +++ b/x-pack/plugins/cases/public/components/files/utils.test.tsx @@ -4,10 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { isImage, parseMimeType } from './utils'; +import type { JsonValue } from '@kbn/utility-types'; import { imageMimeTypes, textMimeTypes } from '../../../common/constants/mime_types'; +import { basicFileMock } from '../../containers/mock'; +import { isImage, isValidFileExternalReferenceMetadata, parseMimeType } from './utils'; describe('isImage', () => { it('should return true for allowed image mime types', () => { @@ -42,3 +43,33 @@ describe('parseMimeType', () => { expect(parseMimeType('foo/bar')).toBe('Foo'); }); }); + +describe('isValidFileExternalReferenceMetadata', () => { + it('should return false for empty objects', () => { + expect(isValidFileExternalReferenceMetadata({})).toBeFalsy(); + }); + + it('should return false if the files property is missing', () => { + expect(isValidFileExternalReferenceMetadata({ foo: 'bar' })).toBeFalsy(); + }); + + it('should return false if the files property is not an array', () => { + expect(isValidFileExternalReferenceMetadata({ files: 'bar' })).toBeFalsy(); + }); + + it('should return false if files is not an array of file metadata', () => { + expect(isValidFileExternalReferenceMetadata({ files: [3] })).toBeFalsy(); + }); + + it('should return false if files is not an array of file metadata 2', () => { + expect( + isValidFileExternalReferenceMetadata({ files: [{ name: 'foo', mimeType: 'bar' }] }) + ).toBeFalsy(); + }); + + it('should return true if the metadata is as expected', () => { + expect( + isValidFileExternalReferenceMetadata({ files: [basicFileMock as unknown as JsonValue] }) + ).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/files/utils.tsx b/x-pack/plugins/cases/public/components/files/utils.tsx index dd18ee7d8e9cc..8918ac0cbca1f 100644 --- a/x-pack/plugins/cases/public/components/files/utils.tsx +++ b/x-pack/plugins/cases/public/components/files/utils.tsx @@ -7,6 +7,9 @@ import type { FileJSON } from '@kbn/shared-ux-file-types'; +import type { CommentRequestExternalReferenceType } from '../../../common/api'; + +import { FileAttachmentMetadataRt } from '../../../common/api'; import * as i18n from './translations'; export const isImage = (file: FileJSON) => file.mimeType?.startsWith('image/'); @@ -24,3 +27,12 @@ export const parseMimeType = (mimeType: string | undefined) => { return result[0].charAt(0).toUpperCase() + result[0].slice(1); }; + +export const isValidFileExternalReferenceMetadata = ( + externalReferenceMetadata: CommentRequestExternalReferenceType['externalReferenceMetadata'] +): boolean => { + return ( + FileAttachmentMetadataRt.is(externalReferenceMetadata) && + externalReferenceMetadata?.files?.length >= 1 + ); +}; diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/registered_attachments.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/registered_attachments.tsx index 0b314aeb0de1e..b6d7f11a7d385 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/registered_attachments.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/registered_attachments.tsx @@ -15,12 +15,17 @@ import React, { Suspense } from 'react'; import { memoize } from 'lodash'; import { EuiCallOut, EuiCode, EuiLoadingSpinner } from '@elastic/eui'; -import type { AttachmentType } from '../../../client/attachment_framework/types'; + +import type { + AttachmentType, + AttachmentViewObject, +} from '../../../client/attachment_framework/types'; import type { AttachmentTypeRegistry } from '../../../../common/registry'; import type { CommentResponse } from '../../../../common/api'; import type { UserActionBuilder, UserActionBuilderArgs } from '../types'; -import { UserActionTimestamp } from '../timestamp'; import type { SnakeToCamelCase } from '../../../../common/types'; + +import { UserActionTimestamp } from '../timestamp'; import { ATTACHMENT_NOT_REGISTERED_ERROR, DEFAULT_EVENT_ATTACHMENT_TITLE } from './translations'; import { UserActionContentToolbar } from '../content_toolbar'; import { HoverableUserWithAvatarResolver } from '../../user_profiles/hoverable_user_with_avatar_resolver'; @@ -40,9 +45,7 @@ type BuilderArgs = Pick< /** * Provides a render function for attachment type */ -const getAttachmentRenderer = memoize((attachmentType: AttachmentType) => { - const attachmentViewObject = attachmentType.getAttachmentViewObject(); - +const getAttachmentRenderer = memoize((attachmentViewObject: AttachmentViewObject) => { let AttachmentElement: React.ReactElement; const renderCallback = (props: object) => { @@ -104,14 +107,16 @@ export const createRegisteredAttachmentUserActionBuilder = < } const attachmentType = registry.get(attachmentTypeId); - const renderer = getAttachmentRenderer(attachmentType); - const attachmentViewObject = attachmentType.getAttachmentViewObject(); const props = { ...getAttachmentViewProps(), caseData: { id: caseData.id, title: caseData.title }, }; + const attachmentViewObject = attachmentType.getAttachmentViewObject(props); + + const renderer = getAttachmentRenderer(attachmentViewObject); + return [ { username: ( @@ -125,10 +130,12 @@ export const createRegisteredAttachmentUserActionBuilder = < actions: ( {attachmentViewObject.actions} - handleDeleteComment(comment.id)} - /> + {!attachmentViewObject.hideDefaultActions && ( + handleDeleteComment(comment.id)} + /> + )} ), children: renderer(props), diff --git a/x-pack/plugins/cases/public/internal_attachments/index.ts b/x-pack/plugins/cases/public/internal_attachments/index.ts new file mode 100644 index 0000000000000..c8457d9a16a1b --- /dev/null +++ b/x-pack/plugins/cases/public/internal_attachments/index.ts @@ -0,0 +1,15 @@ +/* + * 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 type { ExternalReferenceAttachmentTypeRegistry } from '../client/attachment_framework/external_reference_registry'; +import { getFileType } from '../components/files/file_type'; + +export const registerInternalAttachments = ( + externalRefRegistry: ExternalReferenceAttachmentTypeRegistry +) => { + externalRefRegistry.register(getFileType()); +}; diff --git a/x-pack/plugins/cases/public/plugin.ts b/x-pack/plugins/cases/public/plugin.ts index 6b828e72b279a..230a609c50f1d 100644 --- a/x-pack/plugins/cases/public/plugin.ts +++ b/x-pack/plugins/cases/public/plugin.ts @@ -28,6 +28,7 @@ import { getUICapabilities } from './client/helpers/capabilities'; import { ExternalReferenceAttachmentTypeRegistry } from './client/attachment_framework/external_reference_registry'; import { PersistableStateAttachmentTypeRegistry } from './client/attachment_framework/persistable_state_registry'; import { registerCaseFileKinds } from './files'; +import { registerInternalAttachments } from './internal_attachments'; /** * @public @@ -53,6 +54,7 @@ export class CasesUiPlugin const externalReferenceAttachmentTypeRegistry = this.externalReferenceAttachmentTypeRegistry; const persistableStateAttachmentTypeRegistry = this.persistableStateAttachmentTypeRegistry; + registerInternalAttachments(externalReferenceAttachmentTypeRegistry); registerCaseFileKinds(plugins.files); if (plugins.home) { diff --git a/x-pack/plugins/cases/server/common/limiter_checker/test_utils.ts b/x-pack/plugins/cases/server/common/limiter_checker/test_utils.ts index 6fa5e0cbbe561..1f0423dc323f5 100644 --- a/x-pack/plugins/cases/server/common/limiter_checker/test_utils.ts +++ b/x-pack/plugins/cases/server/common/limiter_checker/test_utils.ts @@ -39,7 +39,7 @@ export const createFileRequests = ({ const files: FileAttachmentMetadata['files'] = [...Array(numFiles).keys()].map((value) => { return { name: `${value}`, - createdAt: '2023-02-27T20:26:54.345Z', + created: '2023-02-27T20:26:54.345Z', extension: 'png', mimeType: 'image/png', }; diff --git a/x-pack/test/cases_api_integration/common/lib/mock.ts b/x-pack/test/cases_api_integration/common/lib/mock.ts index 149db8b0e316a..3eab32d1a26de 100644 --- a/x-pack/test/cases_api_integration/common/lib/mock.ts +++ b/x-pack/test/cases_api_integration/common/lib/mock.ts @@ -128,7 +128,7 @@ export const fileMetadata = () => ({ name: 'test_file', extension: 'png', mimeType: 'image/png', - createdAt: '2023-02-27T20:26:54.345Z', + created: '2023-02-27T20:26:54.345Z', }); export const fileAttachmentMetadata: FileAttachmentMetadata = {