From 6095d8a23bab53bf76ecb88440782a6e74dc24ae Mon Sep 17 00:00:00 2001 From: adcoelho Date: Tue, 28 Feb 2023 12:25:30 +0100 Subject: [PATCH 01/23] [Cases] Initial commit. --- x-pack/plugins/cases/common/types.ts | 1 + x-pack/plugins/cases/common/ui/types.ts | 8 + .../attachments/attachments_table.test.tsx | 71 +++++++++ .../attachments/attachments_table.tsx | 142 +++++++++++++++++ .../case_view/case_view_page.test.tsx | 3 +- .../components/case_view/case_view_page.tsx | 4 + .../case_view/case_view_tabs.test.tsx | 24 +++ .../components/case_view/case_view_tabs.tsx | 6 +- .../components/case_view_attachments.test.tsx | 73 +++++++++ .../components/case_view_attachments.tsx | 150 ++++++++++++++++++ .../components/case_view/translations.ts | 4 + .../plugins/cases/public/containers/mock.ts | 7 + .../containers/use_get_case_attachments.tsx | 71 +++++++++ 13 files changed, 562 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/attachments/attachments_table.test.tsx create mode 100644 x-pack/plugins/cases/public/components/attachments/attachments_table.tsx create mode 100644 x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.test.tsx create mode 100644 x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.tsx create mode 100644 x-pack/plugins/cases/public/containers/use_get_case_attachments.tsx diff --git a/x-pack/plugins/cases/common/types.ts b/x-pack/plugins/cases/common/types.ts index 3ff14b0905110..32d6b34b11c16 100644 --- a/x-pack/plugins/cases/common/types.ts +++ b/x-pack/plugins/cases/common/types.ts @@ -24,4 +24,5 @@ export type SnakeToCamelCase = T extends Record export enum CASE_VIEW_PAGE_TABS { ALERTS = 'alerts', ACTIVITY = 'activity', + FILES = 'files', } diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 06765eb17ca4f..46248e139a2fc 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -103,6 +103,14 @@ export interface ResolvedCase { aliasPurpose?: ResolvedSimpleSavedObject['alias_purpose']; } +export type Attachment = { + fileName: string | null | undefined; + fileType: string; + dateAdded: string; +}; + +export type Attachments = Attachment[]; + export interface SortingParams { sortField: SortFieldCase; sortOrder: 'asc' | 'desc'; diff --git a/x-pack/plugins/cases/public/components/attachments/attachments_table.test.tsx b/x-pack/plugins/cases/public/components/attachments/attachments_table.test.tsx new file mode 100644 index 0000000000000..469022ac388e2 --- /dev/null +++ b/x-pack/plugins/cases/public/components/attachments/attachments_table.test.tsx @@ -0,0 +1,71 @@ +/* + * 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 userEvent from '@testing-library/user-event'; + +import { basicAttachment } from '../../containers/mock'; +import type { AppMockRenderer } from '../../common/mock'; +import { createAppMockRenderer } from '../../common/mock'; +import { AttachmentsTable } from './attachments_table'; + +const onDownload = jest.fn(); +const onDelete = jest.fn(); + +const defaultProps = { + items: [basicAttachment], + pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 1 }, + onChange: jest.fn(), + onDelete, + onDownload, + isLoading: false, +}; + +describe('AttachmentsTable', () => { + let appMockRender: AppMockRenderer; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('renders correctly', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('attachments-table-results-count')).toBeInTheDocument(); + expect(await screen.findByTestId('attachments-table-filename')).toBeInTheDocument(); + expect(await screen.findByTestId('attachments-table-filetype')).toBeInTheDocument(); + expect(await screen.findByTestId('attachments-table-date-added')).toBeInTheDocument(); + expect(await screen.findByTestId('attachments-table-action-download')).toBeInTheDocument(); + expect(await screen.findByTestId('attachments-table-action-delete')).toBeInTheDocument(); + }); + + it('renders loading state', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('attachments-table-loading')).toBeInTheDocument(); + }); + + it('renders empty table', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('attachments-table-empty')).toBeInTheDocument(); + }); + + it('calls delete action', async () => { + appMockRender.render(); + userEvent.click(await screen.findByTestId('attachments-table-action-delete')); + expect(onDelete).toHaveBeenCalled(); + }); + + it('calls download action', async () => { + appMockRender.render(); + userEvent.click(await screen.findByTestId('attachments-table-action-delete')); + expect(onDelete).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/attachments/attachments_table.tsx b/x-pack/plugins/cases/public/components/attachments/attachments_table.tsx new file mode 100644 index 0000000000000..76abb447e566f --- /dev/null +++ b/x-pack/plugins/cases/public/components/attachments/attachments_table.tsx @@ -0,0 +1,142 @@ +/* + * 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, { useMemo } from 'react'; + +import { + EuiLink, + EuiBasicTable, + EuiBasicTableColumn, + Pagination, + EuiBasicTableProps, + EuiLoadingContent, + EuiSpacer, + EuiText, + EuiEmptyPrompt, + EuiButton, +} from '@elastic/eui'; +import type { Attachment, Attachments } from '../../../common/ui/types'; + +interface AttachmentsTableProps { + isLoading: boolean; + items: Attachments; + onChange: EuiBasicTableProps['onChange']; + onDownload: () => void; + onDelete: () => void; + pagination: Pagination; +} + +export const AttachmentsTable = ({ + items, + pagination, + onChange, + onDelete, + onDownload, + isLoading, +}: AttachmentsTableProps) => { + const columns: Array> = [ + { + field: 'fileName', + name: 'Name', + 'data-test-subj': 'attachments-table-filename', + render: (fileName: Attachment['fileName']) => ( + + {fileName} + + ), + width: '60%', + }, + { + field: 'fileType', + 'data-test-subj': 'attachments-table-filetype', + name: 'Type', + }, + { + field: 'dateAdded', + name: 'Date Added', + 'data-test-subj': 'attachments-table-date-added', + dataType: 'date', + }, + { + name: 'Actions', + width: '120px', + actions: [ + { + name: 'Download', + isPrimary: true, + description: 'Download this file', + icon: 'download', + type: 'icon', + onClick: onDownload, + 'data-test-subj': 'attachments-table-action-download', + }, + { + name: 'Delete', + isPrimary: true, + description: 'Delete this file', + color: 'danger', + icon: 'trash', + type: 'icon', + onClick: onDelete, + 'data-test-subj': 'attachments-table-action-delete', + }, + ], + }, + ]; + + const resultsCount = useMemo( + () => ( + <> + + {pagination.pageSize * pagination.pageIndex + 1}- + {pagination.pageSize * pagination.pageIndex + pagination.pageSize} + {' '} + of {pagination.totalItemCount} + + ), + [pagination.pageIndex, pagination.pageSize, pagination.totalItemCount] + ); + + return isLoading ? ( + + ) : ( + <> + {pagination.totalItemCount > 0 && ( + <> + + + Showing {resultsCount} + + + )} + + No attachments available} + data-test-subj="attachments-table-empty" + titleSize="xs" + actions={ + + Upload File + + } + /> + } + /> + + ); +}; diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx index 3fa00c13888a1..99701a7c68be4 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx @@ -475,8 +475,9 @@ describe('CaseViewPage', () => { it('renders tabs correctly', async () => { const result = appMockRenderer.render(); await act(async () => { - expect(result.getByTestId('case-view-tab-title-alerts')).toBeTruthy(); expect(result.getByTestId('case-view-tab-title-activity')).toBeTruthy(); + expect(result.getByTestId('case-view-tab-title-alerts')).toBeTruthy(); + expect(result.getByTestId('case-view-tab-title-files')).toBeTruthy(); }); }); diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx index a26793e501897..004383c839738 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx @@ -18,6 +18,7 @@ import { useCasesTitleBreadcrumbs } from '../use_breadcrumbs'; import { WhitePageWrapperNoBorder } from '../wrappers'; import { CaseViewActivity } from './components/case_view_activity'; import { CaseViewAlerts } from './components/case_view_alerts'; +import { CaseViewAttachments } from './components/case_view_attachments'; import { CaseViewMetrics } from './metrics'; import type { CaseViewPageProps } from './types'; import { useRefreshCaseViewPage } from './use_on_refresh_case_view_page'; @@ -140,6 +141,9 @@ export const CaseViewPage = React.memo( {activeTabId === CASE_VIEW_PAGE_TABS.ALERTS && features.alerts.enabled && ( )} + {activeTabId === CASE_VIEW_PAGE_TABS.FILES && ( + + )} {timelineUi?.renderTimelineDetailsPanel ? timelineUi.renderTimelineDetailsPanel() : null} diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_tabs.test.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_tabs.test.tsx index a3da7d90267cf..2494602c58503 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_tabs.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_tabs.test.tsx @@ -62,6 +62,7 @@ describe('CaseViewTabs', () => { expect(await screen.findByTestId('case-view-tab-title-activity')).toBeInTheDocument(); expect(await screen.findByTestId('case-view-tab-title-alerts')).toBeInTheDocument(); + expect(await screen.findByTestId('case-view-tab-title-files')).toBeInTheDocument(); }); it('renders the activity tab by default', async () => { @@ -82,6 +83,15 @@ describe('CaseViewTabs', () => { ); }); + it('shows the files tab as active', async () => { + appMockRenderer.render(); + + expect(await screen.findByTestId('case-view-tab-title-files')).toHaveAttribute( + 'aria-selected', + 'true' + ); + }); + it('navigates to the activity tab when the activity tab is clicked', async () => { const navigateToCaseViewMock = useCaseViewNavigationMock().navigateToCaseView; appMockRenderer.render(); @@ -109,4 +119,18 @@ describe('CaseViewTabs', () => { }); }); }); + + it('navigates to the files tab when the files tab is clicked', async () => { + const navigateToCaseViewMock = useCaseViewNavigationMock().navigateToCaseView; + appMockRenderer.render(); + + userEvent.click(await screen.findByTestId('case-view-tab-title-files')); + + await waitFor(() => { + expect(navigateToCaseViewMock).toHaveBeenCalledWith({ + detailName: caseData.id, + tabId: CASE_VIEW_PAGE_TABS.FILES, + }); + }); + }); }); diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_tabs.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_tabs.tsx index 746311051f147..ebfaec30531b2 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_tabs.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_tabs.tsx @@ -12,7 +12,7 @@ import { CASE_VIEW_PAGE_TABS } from '../../../common/types'; import { useCaseViewNavigation } from '../../common/navigation'; import { useCasesContext } from '../cases_context/use_cases_context'; import { EXPERIMENTAL_DESC, EXPERIMENTAL_LABEL } from '../header_page/translations'; -import { ACTIVITY_TAB, ALERTS_TAB } from './translations'; +import { ACTIVITY_TAB, ALERTS_TAB, FILES_TAB } from './translations'; import type { Case } from '../../../common'; const ExperimentalBadge = styled(EuiBetaBadge)` @@ -56,6 +56,10 @@ export const CaseViewTabs = React.memo(({ caseData, activeTab }, ] : []), + { + id: CASE_VIEW_PAGE_TABS.FILES, + name: FILES_TAB, + }, ], [features.alerts.enabled, features.alerts.isExperimental] ); diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.test.tsx new file mode 100644 index 0000000000000..734704dc988f8 --- /dev/null +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.test.tsx @@ -0,0 +1,73 @@ +/* + * 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 { alertCommentWithIndices, basicAttachment, basicCase } from '../../../containers/mock'; +import type { AppMockRenderer } from '../../../common/mock'; +import { createAppMockRenderer } from '../../../common/mock'; +import type { Case } from '../../../../common'; +import { CaseViewAttachments } from './case_view_attachments'; +import { useGetCaseAttachments } from '../../../containers/use_get_case_attachments'; + +jest.mock('../../../containers/use_get_case_attachments'); + +const useGetCaseAttachmentsMock = useGetCaseAttachments as jest.Mock; + +const caseData: Case = { + ...basicCase, + comments: [...basicCase.comments, alertCommentWithIndices], +}; + +describe('Case View Page files tab', () => { + let appMockRender: AppMockRenderer; + useGetCaseAttachmentsMock.mockReturnValue({ + data: { + pageOfItems: [basicAttachment], + availableTypes: [basicAttachment.fileType], + totalItemCount: 1, + }, + isLoading: false, + }); + + beforeEach(() => { + appMockRender = createAppMockRenderer(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render the utility bar for the attachments table', async () => { + const result = appMockRender.render(); + + expect(await result.findByTestId('case-detail-upload-file')).toBeInTheDocument(); + expect(await result.findByTestId('case-detail-search-file')).toBeInTheDocument(); + expect(await result.findByTestId('case-detail-select-file-type')).toBeInTheDocument(); + }); + + it('should render the attachments table', async () => { + const result = appMockRender.render(); + + expect(await result.findByTestId('attachments-table')).toBeInTheDocument(); + }); + + it('should disable search and filter if there are no attachments', async () => { + useGetCaseAttachmentsMock.mockReturnValue({ + data: { + pageOfItems: [], + availableTypes: [], + totalItemCount: 0, + }, + isLoading: false, + }); + + const result = appMockRender.render(); + + expect(await result.findByTestId('case-detail-search-file')).toHaveAttribute('disabled'); + expect(await result.findByTestId('case-detail-select-file-type')).toHaveAttribute('disabled'); + }); +}); diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.tsx new file mode 100644 index 0000000000000..0155e298eea62 --- /dev/null +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.tsx @@ -0,0 +1,150 @@ +/* + * 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, { useMemo, useState } from 'react'; + +import { + EuiFlexItem, + EuiFlexGroup, + EuiButton, + Criteria, + EuiFieldSearch, + EuiSelect, + EuiButtonGroup, +} from '@elastic/eui'; +import type { Case, Attachment } from '../../../../common/ui/types'; + +import { CaseViewTabs } from '../case_view_tabs'; +import { CASE_VIEW_PAGE_TABS } from '../../../../common/types'; +import type { GetCaseAttachmentsParams } from '../../../containers/use_get_case_attachments'; +import { useGetCaseAttachments } from '../../../containers/use_get_case_attachments'; +import { AttachmentsTable } from '../../attachments/attachments_table'; + +interface CaseViewAttachmentsProps { + caseData: Case; +} + +export const CaseViewAttachments = ({ caseData }: CaseViewAttachmentsProps) => { + const [filteringOptions, setFilteringOptions] = useState({ + page: 0, + perPage: 5, + }); + const { data: attachmentsData, isLoading } = useGetCaseAttachments(filteringOptions); + + const onTableChange = ({ page }: Criteria) => { + if (page) { + setFilteringOptions({ + ...filteringOptions, + page: page.index, + perPage: page.size, + }); + } + }; + + const pagination = useMemo( + () => ({ + pageIndex: filteringOptions.page, + pageSize: filteringOptions.perPage, + totalItemCount: attachmentsData.totalItemCount, + pageSizeOptions: [5, 10, 0], + showPerPageOptions: true, + }), + [filteringOptions.page, filteringOptions.perPage, attachmentsData.totalItemCount] + ); + + const selectOptions = useMemo( + () => [ + { value: 'any', text: 'any' }, + ...attachmentsData.availableTypes.map((type) => ({ value: type, text: type })), + ], + [attachmentsData.availableTypes] + ); + + const [selectValue, setSelectValue] = useState(selectOptions[0].value); + + const tableViewSelectedId = 'tableViewSelectedId'; + const toggleButtonsIcons = [ + { + id: 'thumbnailViewSelectedId', + label: 'Thumbnail view', + iconType: 'grid', + isDisabled: true, + }, + { + id: tableViewSelectedId, + label: 'Table view', + iconType: 'editorUnorderedList', + }, + ]; + + return ( + + + + + + + + + Upload File + + + + + setFilteringOptions({ + ...filteringOptions, + searchTerm: event.target.value, + }) + } + isClearable={true} + data-test-subj="case-detail-search-file" + /> + + + setSelectValue(e.target.value)} + data-test-subj="case-detail-select-file-type" + /> + + + + {}} + isIconOnly + /> + + + {}} + onDownload={() => {}} + pagination={pagination} + /> + + + + + ); +}; +CaseViewAttachments.displayName = 'CaseViewAttachments'; diff --git a/x-pack/plugins/cases/public/components/case_view/translations.ts b/x-pack/plugins/cases/public/components/case_view/translations.ts index d71c56fc97fca..8fc80c1a0aba3 100644 --- a/x-pack/plugins/cases/public/components/case_view/translations.ts +++ b/x-pack/plugins/cases/public/components/case_view/translations.ts @@ -165,6 +165,10 @@ export const ALERTS_TAB = i18n.translate('xpack.cases.caseView.tabs.alerts', { defaultMessage: 'Alerts', }); +export const FILES_TAB = i18n.translate('xpack.cases.caseView.tabs.files', { + defaultMessage: 'Files', +}); + export const ALERTS_EMPTY_DESCRIPTION = i18n.translate( 'xpack.cases.caseView.tabs.alerts.emptyDescription', { diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index 4c6439231245a..972b82e9bc1a1 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -18,6 +18,7 @@ import type { FindCaseUserActions, CaseUsers, CaseUserActionsStats, + Attachment, } from '../../common/ui/types'; import type { CaseConnector, @@ -240,6 +241,12 @@ export const basicCase: Case = { assignees: [{ uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0' }], }; +export const basicAttachment: Attachment = { + fileName: 'my-super-cool-screenshot', + fileType: 'png', + dateAdded: basicCreatedAt, +}; + export const caseWithAlerts = { ...basicCase, totalAlerts: 2, diff --git a/x-pack/plugins/cases/public/containers/use_get_case_attachments.tsx b/x-pack/plugins/cases/public/containers/use_get_case_attachments.tsx new file mode 100644 index 0000000000000..ede8b48a320e1 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/use_get_case_attachments.tsx @@ -0,0 +1,71 @@ +/* + * 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 faker from 'faker'; +import { Attachments } from './types'; + +const files: Attachments = []; + +for (let i = 0; i < 15; i++) { + files.push({ + fileName: faker.system.fileName(), + fileType: faker.system.fileType(), + dateAdded: faker.date.past().toString(), + }); +} + +export type GetCaseAttachmentsParams = { + page: number; + perPage: number; + extension?: string[]; + mimeType?: string[]; + searchTerm?: string; +}; + +type GetCaseAttachmentsResponse = { + pageOfItems: Attachments; + availableTypes: string[]; + totalItemCount: number; +}; + +// Manually handle pagination of data +const findFiles = (files: Attachments, pageIndex: number, pageSize: number) => { + let pageOfItems; + + if (!pageIndex && !pageSize) { + pageOfItems = files; + } else { + const startIndex = pageIndex * pageSize; + pageOfItems = files.slice(startIndex, Math.min(startIndex + pageSize, files.length)); + } + + return { + pageOfItems, + totalItemCount: files.length, + }; +}; + +export const useGetCaseAttachments = ({ + page, + perPage, + extension, + mimeType, + searchTerm, +}: GetCaseAttachmentsParams): { + data: GetCaseAttachmentsResponse; + isLoading: boolean; +} => { + const availableTypes = [...new Set(files.map((item) => item.fileType))]; + + return { + data: { + ...findFiles(files, page, perPage), + availableTypes, + }, + isLoading: false, + }; +}; From 1948296b3fcc58816590be8d231cc8298493b6e3 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Mon, 6 Mar 2023 15:31:41 +0100 Subject: [PATCH 02/23] Include file context. Create Add file modal. --- .../file/file_upload/impl/src/file_upload.tsx | 2 +- x-pack/plugins/cases/common/ui/types.ts | 4 +- x-pack/plugins/cases/public/application.tsx | 21 ++- .../ui/get_all_cases_selector_modal.tsx | 2 + .../cases/public/client/ui/get_cases.tsx | 6 +- .../public/client/ui/get_cases_context.tsx | 12 +- .../client/ui/get_create_case_flyout.tsx | 6 +- .../public/client/ui/get_recent_cases.tsx | 6 +- .../public/components/add_file/index.tsx | 130 ++++++++++++++++++ .../components/add_file/translations.ts | 12 ++ .../cases/public/components/app/index.tsx | 10 +- .../attachments/attachments_table.tsx | 17 +-- .../components/case_view_attachments.tsx | 23 +--- .../public/components/cases_context/index.tsx | 14 +- .../containers/use_get_case_attachments.tsx | 19 +-- x-pack/plugins/cases/public/files/index.ts | 2 +- x-pack/plugins/cases/public/plugin.ts | 5 + x-pack/plugins/cases/tsconfig.json | 4 + .../common/plugins/cases/kibana.jsonc | 1 + 19 files changed, 243 insertions(+), 53 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/add_file/index.tsx create mode 100644 x-pack/plugins/cases/public/components/add_file/translations.ts diff --git a/packages/shared-ux/file/file_upload/impl/src/file_upload.tsx b/packages/shared-ux/file/file_upload/impl/src/file_upload.tsx index 498a4a93b5fe4..45e74312e1e55 100644 --- a/packages/shared-ux/file/file_upload/impl/src/file_upload.tsx +++ b/packages/shared-ux/file/file_upload/impl/src/file_upload.tsx @@ -19,7 +19,7 @@ import { context } from './context'; /** * An object representing an uploaded file */ -interface UploadedFile { +export interface UploadedFile { /** * The ID that was generated for the uploaded file */ diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 46248e139a2fc..cf5466b49a0d3 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -103,11 +103,11 @@ export interface ResolvedCase { aliasPurpose?: ResolvedSimpleSavedObject['alias_purpose']; } -export type Attachment = { +export interface Attachment { fileName: string | null | undefined; fileType: string; dateAdded: string; -}; +} export type Attachments = Attachment[]; diff --git a/x-pack/plugins/cases/public/application.tsx b/x-pack/plugins/cases/public/application.tsx index bac423a9f8292..32959122a2e6a 100644 --- a/x-pack/plugins/cases/public/application.tsx +++ b/x-pack/plugins/cases/public/application.tsx @@ -9,19 +9,21 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Router } from 'react-router-dom'; -import { I18nProvider } from '@kbn/i18n-react'; import { EuiErrorBoundary } from '@elastic/eui'; - +import { I18nProvider } from '@kbn/i18n-react'; +import { EuiThemeProvider as StyledComponentsThemeProvider } from '@kbn/kibana-react-plugin/common'; import { KibanaContextProvider, KibanaThemeProvider, useUiSetting$, } from '@kbn/kibana-react-plugin/public'; -import { EuiThemeProvider as StyledComponentsThemeProvider } from '@kbn/kibana-react-plugin/common'; -import type { RenderAppProps } from './types'; -import { CasesApp } from './components/app'; + +import type { FilesStart } from '@kbn/files-plugin/public'; import type { ExternalReferenceAttachmentTypeRegistry } from './client/attachment_framework/external_reference_registry'; import type { PersistableStateAttachmentTypeRegistry } from './client/attachment_framework/persistable_state_registry'; +import type { RenderAppProps } from './types'; + +import { CasesApp } from './components/app'; export const renderApp = (deps: RenderAppProps) => { const { mountParams } = deps; @@ -37,10 +39,15 @@ export const renderApp = (deps: RenderAppProps) => { interface CasesAppWithContextProps { externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry; persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry; + filesPlugin: FilesStart; } const CasesAppWithContext: React.FC = React.memo( - ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry }) => { + ({ + externalReferenceAttachmentTypeRegistry, + persistableStateAttachmentTypeRegistry, + filesPlugin, + }) => { const [darkMode] = useUiSetting$('theme:darkMode'); return ( @@ -48,6 +55,7 @@ const CasesAppWithContext: React.FC = React.memo( ); @@ -78,6 +86,7 @@ export const App: React.FC<{ deps: RenderAppProps }> = ({ deps }) => { deps.externalReferenceAttachmentTypeRegistry } persistableStateAttachmentTypeRegistry={deps.persistableStateAttachmentTypeRegistry} + filesPlugin={pluginsStart.files} /> diff --git a/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx b/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx index b0807b0509135..564ed1e8506ee 100644 --- a/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx +++ b/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx @@ -23,6 +23,7 @@ const AllCasesSelectorModalLazy: React.FC = lazy( export const getAllCasesSelectorModalLazy = ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + filesPlugin, owner, permissions, hiddenStatuses, @@ -33,6 +34,7 @@ export const getAllCasesSelectorModalLazy = ({ value={{ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + filesPlugin, owner, permissions, }} diff --git a/x-pack/plugins/cases/public/client/ui/get_cases.tsx b/x-pack/plugins/cases/public/client/ui/get_cases.tsx index 45c9f30b984d2..255e83b5704fe 100644 --- a/x-pack/plugins/cases/public/client/ui/get_cases.tsx +++ b/x-pack/plugins/cases/public/client/ui/get_cases.tsx @@ -14,7 +14,9 @@ import { CasesProvider } from '../../components/cases_context'; type GetCasesPropsInternal = CasesProps & CasesContextProps; export type GetCasesProps = Omit< GetCasesPropsInternal, - 'externalReferenceAttachmentTypeRegistry' | 'persistableStateAttachmentTypeRegistry' + | 'externalReferenceAttachmentTypeRegistry' + | 'persistableStateAttachmentTypeRegistry' + | 'filesPlugin' >; const CasesRoutesLazy: React.FC = lazy(() => import('../../components/app/routes')); @@ -22,6 +24,7 @@ const CasesRoutesLazy: React.FC = lazy(() => import('../../component export const getCasesLazy = ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + filesPlugin, owner, permissions, basePath, @@ -39,6 +42,7 @@ export const getCasesLazy = ({ value={{ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + filesPlugin, owner, permissions, basePath, diff --git a/x-pack/plugins/cases/public/client/ui/get_cases_context.tsx b/x-pack/plugins/cases/public/client/ui/get_cases_context.tsx index 77e6ca3c87e24..26519dcd61520 100644 --- a/x-pack/plugins/cases/public/client/ui/get_cases_context.tsx +++ b/x-pack/plugins/cases/public/client/ui/get_cases_context.tsx @@ -13,7 +13,9 @@ import type { CasesContextProps } from '../../components/cases_context'; export type GetCasesContextPropsInternal = CasesContextProps; export type GetCasesContextProps = Omit< CasesContextProps, - 'externalReferenceAttachmentTypeRegistry' | 'persistableStateAttachmentTypeRegistry' + | 'externalReferenceAttachmentTypeRegistry' + | 'persistableStateAttachmentTypeRegistry' + | 'filesPlugin' >; const CasesProviderLazy: React.FC<{ value: GetCasesContextPropsInternal }> = lazy( @@ -28,6 +30,7 @@ const CasesProviderLazyWrapper = ({ features, children, releasePhase, + filesPlugin, }: GetCasesContextPropsInternal & { children: ReactNode }) => { return ( }> @@ -39,6 +42,7 @@ const CasesProviderLazyWrapper = ({ permissions, features, releasePhase, + filesPlugin, }} > {children} @@ -52,9 +56,12 @@ CasesProviderLazyWrapper.displayName = 'CasesProviderLazyWrapper'; export const getCasesContextLazy = ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + filesPlugin, }: Pick< GetCasesContextPropsInternal, - 'externalReferenceAttachmentTypeRegistry' | 'persistableStateAttachmentTypeRegistry' + | 'externalReferenceAttachmentTypeRegistry' + | 'persistableStateAttachmentTypeRegistry' + | 'filesPlugin' >): (() => React.FC) => { const CasesProviderLazyWrapperWithRegistry: React.FC = ({ children, @@ -64,6 +71,7 @@ export const getCasesContextLazy = ({ {...props} externalReferenceAttachmentTypeRegistry={externalReferenceAttachmentTypeRegistry} persistableStateAttachmentTypeRegistry={persistableStateAttachmentTypeRegistry} + filesPlugin={filesPlugin} > {children} diff --git a/x-pack/plugins/cases/public/client/ui/get_create_case_flyout.tsx b/x-pack/plugins/cases/public/client/ui/get_create_case_flyout.tsx index af932b53e1dde..cdfeaef1c5ecf 100644 --- a/x-pack/plugins/cases/public/client/ui/get_create_case_flyout.tsx +++ b/x-pack/plugins/cases/public/client/ui/get_create_case_flyout.tsx @@ -14,7 +14,9 @@ import { CasesProvider } from '../../components/cases_context'; type GetCreateCaseFlyoutPropsInternal = CreateCaseFlyoutProps & CasesContextProps; export type GetCreateCaseFlyoutProps = Omit< GetCreateCaseFlyoutPropsInternal, - 'externalReferenceAttachmentTypeRegistry' | 'persistableStateAttachmentTypeRegistry' + | 'externalReferenceAttachmentTypeRegistry' + | 'persistableStateAttachmentTypeRegistry' + | 'filesPlugin' >; export const CreateCaseFlyoutLazy: React.FC = lazy( @@ -23,6 +25,7 @@ export const CreateCaseFlyoutLazy: React.FC = lazy( export const getCreateCaseFlyoutLazy = ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + filesPlugin, owner, permissions, features, @@ -35,6 +38,7 @@ export const getCreateCaseFlyoutLazy = ({ value={{ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + filesPlugin, owner, permissions, features, diff --git a/x-pack/plugins/cases/public/client/ui/get_recent_cases.tsx b/x-pack/plugins/cases/public/client/ui/get_recent_cases.tsx index a047c106246da..ce4ffa260a9b3 100644 --- a/x-pack/plugins/cases/public/client/ui/get_recent_cases.tsx +++ b/x-pack/plugins/cases/public/client/ui/get_recent_cases.tsx @@ -14,7 +14,9 @@ import type { RecentCasesProps } from '../../components/recent_cases'; type GetRecentCasesPropsInternal = RecentCasesProps & CasesContextProps; export type GetRecentCasesProps = Omit< GetRecentCasesPropsInternal, - 'externalReferenceAttachmentTypeRegistry' | 'persistableStateAttachmentTypeRegistry' + | 'externalReferenceAttachmentTypeRegistry' + | 'persistableStateAttachmentTypeRegistry' + | 'filesPlugin' >; const RecentCasesLazy: React.FC = lazy( @@ -23,6 +25,7 @@ const RecentCasesLazy: React.FC = lazy( export const getRecentCasesLazy = ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + filesPlugin, owner, permissions, maxCasesToShow, @@ -31,6 +34,7 @@ export const getRecentCasesLazy = ({ value={{ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + filesPlugin, owner, permissions, }} diff --git a/x-pack/plugins/cases/public/components/add_file/index.tsx b/x-pack/plugins/cases/public/components/add_file/index.tsx new file mode 100644 index 0000000000000..2d3edd2897541 --- /dev/null +++ b/x-pack/plugins/cases/public/components/add_file/index.tsx @@ -0,0 +1,130 @@ +/* + * 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 { + EuiButton, + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, +} from '@elastic/eui'; +import React, { useCallback, useState } from 'react'; + +import type { UploadedFile } from '@kbn/shared-ux-file-upload/src/file_upload'; +import { FileUpload } from '@kbn/shared-ux-file-upload'; +import { useFilesContext } from '@kbn/shared-ux-file-context'; + +import { APP_ID, CommentType, ExternalReferenceStorageType } from '../../../common'; +import { useKibana } from '../../common/lib/kibana'; +import { useCreateAttachments } from '../../containers/use_create_attachments'; +import { useCasesContext } from '../cases_context/use_cases_context'; +import * as i18n from './translations'; +import { CASES_FILE_KINDS } from '../../files'; + +interface AddFileProps { + caseId: string; + onFileAdded: () => void; +} + +const FILE_ATTACHMENT_TYPE = '.files'; + +const AddFileComponent: React.FC = ({ caseId, onFileAdded }) => { + const { notifications } = useKibana().services; + const { client: filesClient } = useFilesContext(); + + const { owner } = useCasesContext(); + + const { isLoading, createAttachments } = useCreateAttachments(); + const [isModalVisible, setIsModalVisible] = useState(false); + + const closeModal = () => setIsModalVisible(false); + const showModal = () => setIsModalVisible(true); + + const onUploadDone = useCallback( + async (chosenFiles: UploadedFile[]) => { + const file = chosenFiles[0]; + + try { + await createAttachments({ + caseId, + caseOwner: owner[0], + data: [ + { + type: CommentType.externalReference, + externalReferenceId: file.id, + externalReferenceStorage: { type: ExternalReferenceStorageType.elasticSearchDoc }, + externalReferenceAttachmentTypeId: FILE_ATTACHMENT_TYPE, + externalReferenceMetadata: { + file: { + name: file.fileJSON.name, + extension: file.fileJSON.extension ?? '', + mimeType: file.fileJSON.mimeType ?? '', + createdAt: file.fileJSON.created, + }, + }, + }, + ], + updateCase: onFileAdded, + }); + + notifications.toasts.addSuccess({ + title: 'File uploaded successfuly!', + text: `File ID: ${file.id}`, + }); + } catch (error) { + // error toast is handled inside createAttachments + + // we need to delete the file here + await filesClient.delete({ kind: CASES_FILE_KINDS[APP_ID].id, id: file.id }); + } + + closeModal(); + }, + [caseId, createAttachments, filesClient, notifications.toasts, onFileAdded, owner] + ); + + const onError = useCallback( + (error) => { + notifications.toasts.addError(error, { + title: 'Failed to upload', + }); + }, + [notifications.toasts] + ); + + return ( + <> + + {i18n.ADD_FILE} + + {isModalVisible && ( + + + {'Add File'} + + + + + + )} + + ); +}; +AddFileComponent.displayName = 'AddFile'; + +export const AddFile = React.memo(AddFileComponent); diff --git a/x-pack/plugins/cases/public/components/add_file/translations.ts b/x-pack/plugins/cases/public/components/add_file/translations.ts new file mode 100644 index 0000000000000..35eb5a3b6d471 --- /dev/null +++ b/x-pack/plugins/cases/public/components/add_file/translations.ts @@ -0,0 +1,12 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const ADD_FILE = i18n.translate('xpack.cases.caseView.addFile', { + defaultMessage: 'Add file', +}); diff --git a/x-pack/plugins/cases/public/components/app/index.tsx b/x-pack/plugins/cases/public/components/app/index.tsx index 42ef9b658fea7..da1ec8bfa54e1 100644 --- a/x-pack/plugins/cases/public/components/app/index.tsx +++ b/x-pack/plugins/cases/public/components/app/index.tsx @@ -6,12 +6,15 @@ */ import React from 'react'; -import { APP_OWNER } from '../../../common/constants'; + +import type { FilesStart } from '@kbn/files-plugin/public'; + import type { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry'; import type { PersistableStateAttachmentTypeRegistry } from '../../client/attachment_framework/persistable_state_registry'; + +import { APP_OWNER } from '../../../common/constants'; import { getCasesLazy } from '../../client/ui/get_cases'; import { useApplicationCapabilities } from '../../common/lib/kibana'; - import { Wrapper } from '../wrappers'; import type { CasesRoutesProps } from './types'; @@ -20,11 +23,13 @@ export type CasesProps = CasesRoutesProps; interface CasesAppProps { externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry; persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry; + filesPlugin: FilesStart; } const CasesAppComponent: React.FC = ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + filesPlugin, }) => { const userCapabilities = useApplicationCapabilities(); @@ -33,6 +38,7 @@ const CasesAppComponent: React.FC = ({ {getCasesLazy({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + filesPlugin, owner: [APP_OWNER], useFetchAlertData: () => [false, {}], permissions: userCapabilities.generalCases, diff --git a/x-pack/plugins/cases/public/components/attachments/attachments_table.tsx b/x-pack/plugins/cases/public/components/attachments/attachments_table.tsx index 76abb447e566f..1a6fb19741f6d 100644 --- a/x-pack/plugins/cases/public/components/attachments/attachments_table.tsx +++ b/x-pack/plugins/cases/public/components/attachments/attachments_table.tsx @@ -6,12 +6,10 @@ */ import React, { useMemo } from 'react'; +import type { EuiBasicTableColumn, Pagination, EuiBasicTableProps } from '@elastic/eui'; import { EuiLink, EuiBasicTable, - EuiBasicTableColumn, - Pagination, - EuiBasicTableProps, EuiLoadingContent, EuiSpacer, EuiText, @@ -91,10 +89,11 @@ export const AttachmentsTable = ({ () => ( <> - {pagination.pageSize * pagination.pageIndex + 1}- + {pagination.pageSize * pagination.pageIndex + 1} + {'-'} {pagination.pageSize * pagination.pageIndex + pagination.pageSize} {' '} - of {pagination.totalItemCount} + {'of'} {pagination.totalItemCount} ), [pagination.pageIndex, pagination.pageSize, pagination.totalItemCount] @@ -108,7 +107,7 @@ export const AttachmentsTable = ({ <> - Showing {resultsCount} + {`Showing ${resultsCount}`} )} @@ -122,7 +121,7 @@ export const AttachmentsTable = ({ data-test-subj="attachments-table" noItemsMessage={ No attachments available} + title={

{'No attachments available'}

} data-test-subj="attachments-table-empty" titleSize="xs" actions={ @@ -131,7 +130,7 @@ export const AttachmentsTable = ({ iconType="plusInCircle" data-test-subj="case-detail-attachments-table-upload-file" > - Upload File + {'Upload File'} } /> @@ -140,3 +139,5 @@ export const AttachmentsTable = ({ ); }; + +AttachmentsTable.displayName = 'AttachmentsTable'; diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.tsx index 0155e298eea62..1b7fd4f64a5bc 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.tsx @@ -6,15 +6,8 @@ */ import React, { useMemo, useState } from 'react'; -import { - EuiFlexItem, - EuiFlexGroup, - EuiButton, - Criteria, - EuiFieldSearch, - EuiSelect, - EuiButtonGroup, -} from '@elastic/eui'; +import type { Criteria } from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup, EuiFieldSearch, EuiSelect, EuiButtonGroup } from '@elastic/eui'; import type { Case, Attachment } from '../../../../common/ui/types'; import { CaseViewTabs } from '../case_view_tabs'; @@ -22,6 +15,7 @@ import { CASE_VIEW_PAGE_TABS } from '../../../../common/types'; import type { GetCaseAttachmentsParams } from '../../../containers/use_get_case_attachments'; import { useGetCaseAttachments } from '../../../containers/use_get_case_attachments'; import { AttachmentsTable } from '../../attachments/attachments_table'; +import { AddFile } from '../../add_file'; interface CaseViewAttachmentsProps { caseData: Case; @@ -80,6 +74,8 @@ export const CaseViewAttachments = ({ caseData }: CaseViewAttachmentsProps) => { }, ]; + const refreshAttachmentsTable = () => {}; + return ( @@ -88,14 +84,7 @@ export const CaseViewAttachments = ({ caseData }: CaseViewAttachmentsProps) => { - - Upload File - + { basePath?: string; features?: CasesFeatures; @@ -69,6 +74,7 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({ basePath = DEFAULT_BASE_PATH, features = {}, releasePhase = 'ga', + filesPlugin, }, }) => { const { appId, appTitle } = useApplication(); @@ -90,6 +96,7 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({ ), releasePhase, dispatch, + filesPlugin, })); /** @@ -116,8 +123,11 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({ return isCasesContextValue(value) ? ( - - {children} + {/* need to check if owner exists */} + + + {children} + ) : null; }; diff --git a/x-pack/plugins/cases/public/containers/use_get_case_attachments.tsx b/x-pack/plugins/cases/public/containers/use_get_case_attachments.tsx index ede8b48a320e1..b495b0ed878c7 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_attachments.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_attachments.tsx @@ -5,32 +5,33 @@ * 2.0. */ +// eslint-disable-next-line import/no-extraneous-dependencies import faker from 'faker'; -import { Attachments } from './types'; +import type { Attachments } from './types'; -const files: Attachments = []; +const fakeFiles: Attachments = []; for (let i = 0; i < 15; i++) { - files.push({ + fakeFiles.push({ fileName: faker.system.fileName(), fileType: faker.system.fileType(), dateAdded: faker.date.past().toString(), }); } -export type GetCaseAttachmentsParams = { +export interface GetCaseAttachmentsParams { page: number; perPage: number; extension?: string[]; mimeType?: string[]; searchTerm?: string; -}; +} -type GetCaseAttachmentsResponse = { +interface GetCaseAttachmentsResponse { pageOfItems: Attachments; availableTypes: string[]; totalItemCount: number; -}; +} // Manually handle pagination of data const findFiles = (files: Attachments, pageIndex: number, pageSize: number) => { @@ -59,11 +60,11 @@ export const useGetCaseAttachments = ({ data: GetCaseAttachmentsResponse; isLoading: boolean; } => { - const availableTypes = [...new Set(files.map((item) => item.fileType))]; + const availableTypes = [...new Set(fakeFiles.map((item) => item.fileType))]; return { data: { - ...findFiles(files, page, perPage), + ...findFiles(fakeFiles, page, perPage), availableTypes, }, isLoading: false, diff --git a/x-pack/plugins/cases/public/files/index.ts b/x-pack/plugins/cases/public/files/index.ts index 9da40d059cd2a..7c3e08575182b 100644 --- a/x-pack/plugins/cases/public/files/index.ts +++ b/x-pack/plugins/cases/public/files/index.ts @@ -23,7 +23,7 @@ const buildFileKind = (owner: Owner): FileKindBrowser => { /** * The file kind definition for interacting with the file service for the UI */ -const CASES_FILE_KINDS: Record = { +export const CASES_FILE_KINDS: Record = { [APP_ID]: buildFileKind(APP_ID), [SECURITY_SOLUTION_OWNER]: buildFileKind(SECURITY_SOLUTION_OWNER), [OBSERVABILITY_OWNER]: buildFileKind(OBSERVABILITY_OWNER), diff --git a/x-pack/plugins/cases/public/plugin.ts b/x-pack/plugins/cases/public/plugin.ts index 83b0f2fb0f009..5e26721af07ec 100644 --- a/x-pack/plugins/cases/public/plugin.ts +++ b/x-pack/plugins/cases/public/plugin.ts @@ -115,6 +115,7 @@ export class CasesUiPlugin const getCasesContext = getCasesContextLazy({ externalReferenceAttachmentTypeRegistry: this.externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry: this.persistableStateAttachmentTypeRegistry, + filesPlugin: plugins.files, }); return { @@ -125,6 +126,7 @@ export class CasesUiPlugin ...props, externalReferenceAttachmentTypeRegistry: this.externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry: this.persistableStateAttachmentTypeRegistry, + filesPlugin: plugins.files, }), getCasesContext, getRecentCases: (props) => @@ -132,6 +134,7 @@ export class CasesUiPlugin ...props, externalReferenceAttachmentTypeRegistry: this.externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry: this.persistableStateAttachmentTypeRegistry, + filesPlugin: plugins.files, }), // @deprecated Please use the hook getUseCasesAddToNewCaseFlyout getCreateCaseFlyout: (props) => @@ -139,6 +142,7 @@ export class CasesUiPlugin ...props, externalReferenceAttachmentTypeRegistry: this.externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry: this.persistableStateAttachmentTypeRegistry, + filesPlugin: plugins.files, }), // @deprecated Please use the hook getUseCasesAddToExistingCaseModal getAllCasesSelectorModal: (props) => @@ -146,6 +150,7 @@ export class CasesUiPlugin ...props, externalReferenceAttachmentTypeRegistry: this.externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry: this.persistableStateAttachmentTypeRegistry, + filesPlugin: plugins.files, }), }, hooks: { diff --git a/x-pack/plugins/cases/tsconfig.json b/x-pack/plugins/cases/tsconfig.json index 5ba7e85918975..190b9c7e04a46 100644 --- a/x-pack/plugins/cases/tsconfig.json +++ b/x-pack/plugins/cases/tsconfig.json @@ -58,6 +58,10 @@ "@kbn/shared-ux-router", "@kbn/files-plugin", "@kbn/shared-ux-file-types", + "@kbn/shared-ux-file-context", + "@kbn/shared-ux-file-image", + "@kbn/shared-ux-file-upload", + "@kbn/shared-ux-file-picker", ], "exclude": [ "target/**/*", diff --git a/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc b/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc index f80753ebbe744..53ad5d6ef363e 100644 --- a/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc +++ b/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc @@ -8,6 +8,7 @@ "browser": false, "requiredPlugins": [ "features", + "files", "cases" ], "optionalPlugins": [ From 45a60d38caa0fc24afe525304ca9422a16c4dc93 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Tue, 7 Mar 2023 14:41:58 +0100 Subject: [PATCH 03/23] List attachments. --- x-pack/plugins/cases/common/ui/types.ts | 1 + .../public/components/add_file/index.tsx | 11 ++- .../attachments/attachments_table.tsx | 36 +++++-- .../components/case_view_attachments.tsx | 47 +++++----- .../public/components/cases_context/index.tsx | 4 +- .../cases/public/containers/constants.ts | 2 + .../plugins/cases/public/containers/mock.ts | 1 + .../containers/use_get_case_attachments.tsx | 94 +++++++++---------- x-pack/plugins/cases/public/types.ts | 4 +- 9 files changed, 111 insertions(+), 89 deletions(-) diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index cf5466b49a0d3..c339594219048 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -104,6 +104,7 @@ export interface ResolvedCase { } export interface Attachment { + id: string; fileName: string | null | undefined; fileType: string; dateAdded: string; diff --git a/x-pack/plugins/cases/public/components/add_file/index.tsx b/x-pack/plugins/cases/public/components/add_file/index.tsx index 2d3edd2897541..35fae303142b2 100644 --- a/x-pack/plugins/cases/public/components/add_file/index.tsx +++ b/x-pack/plugins/cases/public/components/add_file/index.tsx @@ -15,15 +15,16 @@ import { import React, { useCallback, useState } from 'react'; import type { UploadedFile } from '@kbn/shared-ux-file-upload/src/file_upload'; + import { FileUpload } from '@kbn/shared-ux-file-upload'; import { useFilesContext } from '@kbn/shared-ux-file-context'; import { APP_ID, CommentType, ExternalReferenceStorageType } from '../../../common'; +import { CASES_FILE_KINDS } from '../../files'; import { useKibana } from '../../common/lib/kibana'; import { useCreateAttachments } from '../../containers/use_create_attachments'; import { useCasesContext } from '../cases_context/use_cases_context'; import * as i18n from './translations'; -import { CASES_FILE_KINDS } from '../../files'; interface AddFileProps { caseId: string; @@ -56,7 +57,10 @@ const AddFileComponent: React.FC = ({ caseId, onFileAdded }) => { { type: CommentType.externalReference, externalReferenceId: file.id, - externalReferenceStorage: { type: ExternalReferenceStorageType.elasticSearchDoc }, + externalReferenceStorage: { + type: ExternalReferenceStorageType.savedObject, + soType: FILE_ATTACHMENT_TYPE, + }, externalReferenceAttachmentTypeId: FILE_ATTACHMENT_TYPE, externalReferenceMetadata: { file: { @@ -75,6 +79,9 @@ const AddFileComponent: React.FC = ({ caseId, onFileAdded }) => { title: 'File uploaded successfuly!', text: `File ID: ${file.id}`, }); + + // used to refresh the attachments table + onFileAdded(); } catch (error) { // error toast is handled inside createAttachments diff --git a/x-pack/plugins/cases/public/components/attachments/attachments_table.tsx b/x-pack/plugins/cases/public/components/attachments/attachments_table.tsx index 1a6fb19741f6d..e03011d0614c9 100644 --- a/x-pack/plugins/cases/public/components/attachments/attachments_table.tsx +++ b/x-pack/plugins/cases/public/components/attachments/attachments_table.tsx @@ -7,7 +7,9 @@ import React, { useMemo } from 'react'; import type { EuiBasicTableColumn, Pagination, EuiBasicTableProps } from '@elastic/eui'; + import { + EuiButtonIcon, EuiLink, EuiBasicTable, EuiLoadingContent, @@ -16,8 +18,13 @@ import { EuiEmptyPrompt, EuiButton, } from '@elastic/eui'; +import { useFilesContext } from '@kbn/shared-ux-file-context'; + import type { Attachment, Attachments } from '../../../common/ui/types'; +import { APP_ID } from '../../../common'; +import { CASES_FILE_KINDS } from '../../files'; + interface AttachmentsTableProps { isLoading: boolean; items: Attachments; @@ -35,25 +42,27 @@ export const AttachmentsTable = ({ onDownload, isLoading, }: AttachmentsTableProps) => { + const { client: filesClient } = useFilesContext(); + const columns: Array> = [ { - field: 'fileName', + field: 'name', name: 'Name', 'data-test-subj': 'attachments-table-filename', - render: (fileName: Attachment['fileName']) => ( + render: (name: Attachment['fileName']) => ( - {fileName} + {name} ), width: '60%', }, { - field: 'fileType', + field: 'mimeType', 'data-test-subj': 'attachments-table-filetype', name: 'Type', }, { - field: 'dateAdded', + field: 'created', name: 'Date Added', 'data-test-subj': 'attachments-table-date-added', dataType: 'date', @@ -66,9 +75,18 @@ export const AttachmentsTable = ({ name: 'Download', isPrimary: true, description: 'Download this file', - icon: 'download', - type: 'icon', - onClick: onDownload, + render: (attachment: Attachment) => { + return ( + + ); + }, 'data-test-subj': 'attachments-table-action-download', }, { @@ -107,7 +125,7 @@ export const AttachmentsTable = ({ <> - {`Showing ${resultsCount}`} + {'Showing'} {resultsCount} )} diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.tsx index 1b7fd4f64a5bc..12556add97676 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.tsx @@ -7,24 +7,30 @@ import React, { useMemo, useState } from 'react'; import type { Criteria } from '@elastic/eui'; + import { EuiFlexItem, EuiFlexGroup, EuiFieldSearch, EuiSelect, EuiButtonGroup } from '@elastic/eui'; +import { useQueryClient } from '@tanstack/react-query'; + import type { Case, Attachment } from '../../../../common/ui/types'; +import type { GetCaseAttachmentsParams } from '../../../containers/use_get_case_attachments'; import { CaseViewTabs } from '../case_view_tabs'; import { CASE_VIEW_PAGE_TABS } from '../../../../common/types'; -import type { GetCaseAttachmentsParams } from '../../../containers/use_get_case_attachments'; import { useGetCaseAttachments } from '../../../containers/use_get_case_attachments'; import { AttachmentsTable } from '../../attachments/attachments_table'; import { AddFile } from '../../add_file'; +import { casesQueriesKeys } from '../../../containers/constants'; interface CaseViewAttachmentsProps { caseData: Case; } export const CaseViewAttachments = ({ caseData }: CaseViewAttachmentsProps) => { + const queryClient = useQueryClient(); const [filteringOptions, setFilteringOptions] = useState({ page: 0, perPage: 5, + caseId: caseData.id, }); const { data: attachmentsData, isLoading } = useGetCaseAttachments(filteringOptions); @@ -42,21 +48,14 @@ export const CaseViewAttachments = ({ caseData }: CaseViewAttachmentsProps) => { () => ({ pageIndex: filteringOptions.page, pageSize: filteringOptions.perPage, - totalItemCount: attachmentsData.totalItemCount, - pageSizeOptions: [5, 10, 0], + totalItemCount: attachmentsData?.total ?? 0, + pageSizeOptions: [1, 5, 10, 0], showPerPageOptions: true, }), - [filteringOptions.page, filteringOptions.perPage, attachmentsData.totalItemCount] - ); - - const selectOptions = useMemo( - () => [ - { value: 'any', text: 'any' }, - ...attachmentsData.availableTypes.map((type) => ({ value: type, text: type })), - ], - [attachmentsData.availableTypes] + [filteringOptions.page, filteringOptions.perPage, attachmentsData] ); + const selectOptions = [{ value: 'any', text: 'any' }]; const [selectValue, setSelectValue] = useState(selectOptions[0].value); const tableViewSelectedId = 'tableViewSelectedId'; @@ -74,7 +73,9 @@ export const CaseViewAttachments = ({ caseData }: CaseViewAttachmentsProps) => { }, ]; - const refreshAttachmentsTable = () => {}; + const refreshAttachmentsTable = () => { + queryClient.invalidateQueries(casesQueriesKeys.caseAttachments({ ...filteringOptions })); + }; return ( @@ -90,13 +91,15 @@ export const CaseViewAttachments = ({ caseData }: CaseViewAttachmentsProps) => { - setFilteringOptions({ - ...filteringOptions, - searchTerm: event.target.value, - }) + // value={filteringOptions.searchTerm} + value={''} + disabled={isLoading || attachmentsData?.total === 0} + onChange={ + (event) => {} + // setFilteringOptions({ + // ...filteringOptions, + // searchTerm: event.target.value, + // }) } isClearable={true} data-test-subj="case-detail-search-file" @@ -106,7 +109,7 @@ export const CaseViewAttachments = ({ caseData }: CaseViewAttachmentsProps) => { setSelectValue(e.target.value)} data-test-subj="case-detail-select-file-type" /> @@ -124,7 +127,7 @@ export const CaseViewAttachments = ({ caseData }: CaseViewAttachmentsProps) => { {}} onDownload={() => {}} 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 31d35995f3d4b..d104d913cf9cd 100644 --- a/x-pack/plugins/cases/public/components/cases_context/index.tsx +++ b/x-pack/plugins/cases/public/components/cases_context/index.tsx @@ -40,7 +40,6 @@ export interface CasesContextValue { features: CasesFeaturesAllRequired; releasePhase: ReleasePhase; dispatch: CasesContextValueDispatch; - filesPlugin: FilesStart; } export interface CasesContextProps @@ -50,11 +49,11 @@ export interface CasesContextProps | 'permissions' | 'externalReferenceAttachmentTypeRegistry' | 'persistableStateAttachmentTypeRegistry' - | 'filesPlugin' > { basePath?: string; features?: CasesFeatures; releasePhase?: ReleasePhase; + filesPlugin: FilesStart; } export const CasesContext = React.createContext(undefined); @@ -96,7 +95,6 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({ ), releasePhase, dispatch, - filesPlugin, })); /** diff --git a/x-pack/plugins/cases/public/containers/constants.ts b/x-pack/plugins/cases/public/containers/constants.ts index 904f52295c23c..9a83bb6539f39 100644 --- a/x-pack/plugins/cases/public/containers/constants.ts +++ b/x-pack/plugins/cases/public/containers/constants.ts @@ -23,6 +23,8 @@ export const casesQueriesKeys = { cases: (params: unknown) => [...casesQueriesKeys.casesList(), 'all-cases', params] as const, caseView: () => [...casesQueriesKeys.all, 'case'] as const, case: (id: string) => [...casesQueriesKeys.caseView(), id] as const, + caseAttachments: (params: unknown) => + [...casesQueriesKeys.caseView(), 'attachments', params] as const, caseMetrics: (id: string, features: SingleCaseMetricsFeature[]) => [...casesQueriesKeys.case(id), 'metrics', features] as const, caseConnectors: (id: string) => [...casesQueriesKeys.case(id), 'connectors'], diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index 972b82e9bc1a1..eea911bc258dd 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -242,6 +242,7 @@ export const basicCase: Case = { }; export const basicAttachment: Attachment = { + id: '7d47d130-bcec-11ed-afa1-0242ac120002', fileName: 'my-super-cool-screenshot', fileType: 'png', dateAdded: basicCreatedAt, diff --git a/x-pack/plugins/cases/public/containers/use_get_case_attachments.tsx b/x-pack/plugins/cases/public/containers/use_get_case_attachments.tsx index b495b0ed878c7..9d04a586063c4 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_attachments.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_attachments.tsx @@ -5,68 +5,58 @@ * 2.0. */ -// eslint-disable-next-line import/no-extraneous-dependencies -import faker from 'faker'; -import type { Attachments } from './types'; +import type { UseQueryResult } from '@tanstack/react-query'; -const fakeFiles: Attachments = []; +import { useFilesContext } from '@kbn/shared-ux-file-context'; +import { useQuery } from '@tanstack/react-query'; -for (let i = 0; i < 15; i++) { - fakeFiles.push({ - fileName: faker.system.fileName(), - fileType: faker.system.fileType(), - dateAdded: faker.date.past().toString(), - }); -} +import type { ServerError } from '../types'; +import type { Attachment } from './types'; + +import { APP_ID } from '../../common'; +import { useToasts } from '../common/lib/kibana'; +import { CASES_FILE_KINDS } from '../files'; +import { casesQueriesKeys } from './constants'; +import * as i18n from './translations'; export interface GetCaseAttachmentsParams { + caseId: string; page: number; perPage: number; - extension?: string[]; - mimeType?: string[]; - searchTerm?: string; + // extension?: string[]; + // mimeType?: string[]; + // searchTerm?: string; } -interface GetCaseAttachmentsResponse { - pageOfItems: Attachments; - availableTypes: string[]; - totalItemCount: number; -} - -// Manually handle pagination of data -const findFiles = (files: Attachments, pageIndex: number, pageSize: number) => { - let pageOfItems; - - if (!pageIndex && !pageSize) { - pageOfItems = files; - } else { - const startIndex = pageIndex * pageSize; - pageOfItems = files.slice(startIndex, Math.min(startIndex + pageSize, files.length)); - } - - return { - pageOfItems, - totalItemCount: files.length, - }; -}; - export const useGetCaseAttachments = ({ + caseId, page, perPage, - extension, - mimeType, - searchTerm, -}: GetCaseAttachmentsParams): { - data: GetCaseAttachmentsResponse; - isLoading: boolean; -} => { - const availableTypes = [...new Set(fakeFiles.map((item) => item.fileType))]; - - return { - data: { - ...findFiles(fakeFiles, page, perPage), - availableTypes, +}: GetCaseAttachmentsParams): UseQueryResult<{ files: Attachment[]; total: number }> => { + const toasts = useToasts(); + const { client: filesClient } = useFilesContext(); + const filePage = page + 1; + + return useQuery( + casesQueriesKeys.caseAttachments({ caseId, page: filePage, perPage }), + () => { + return filesClient.list({ + kind: CASES_FILE_KINDS[APP_ID].id, + page: filePage, + perPage, + meta: { caseId }, + }); }, - isLoading: false, - }; + { + keepPreviousData: true, + onError: (error: ServerError) => { + if (error.name !== 'AbortError') { + toasts.addError( + error.body && error.body.message ? new Error(error.body.message) : error, + { title: i18n.ERROR_TITLE } + ); + } + }, + } + ); }; diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts index f430b99ae17f2..52c2ac2a67e06 100644 --- a/x-pack/plugins/cases/public/types.ts +++ b/x-pack/plugins/cases/public/types.ts @@ -31,6 +31,7 @@ import type { CasesStatusRequest, CommentRequestAlertType, CommentRequestExternalReferenceNoSOType, + CommentRequestExternalReferenceSOType, CommentRequestPersistableStateType, CommentRequestUserType, } from '../common/api'; @@ -158,7 +159,8 @@ export type SupportedCaseAttachment = | CommentRequestAlertType | CommentRequestUserType | CommentRequestPersistableStateType - | CommentRequestExternalReferenceNoSOType; + | CommentRequestExternalReferenceNoSOType + | CommentRequestExternalReferenceSOType; export type CaseAttachments = SupportedCaseAttachment[]; export type CaseAttachmentWithoutOwner = DistributiveOmit; From 2c5594637ea8462012190546eb6b95b77e8c2302 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Wed, 8 Mar 2023 17:32:45 +0100 Subject: [PATCH 04/23] Rebase to use new file kinds in cases. Add search file feature. Add modal file preview. Adapt cases context for different owners. --- .../public/common/mock/test_providers.tsx | 26 +++++-- .../public/components/add_file/index.tsx | 17 ++-- .../attachments/attachments_table.tsx | 64 ++++++++++----- .../components/case_view_attachments.test.tsx | 2 +- .../components/case_view_attachments.tsx | 78 +++++++++---------- .../public/components/cases_context/index.tsx | 56 +++++++++---- .../plugins/cases/public/containers/mock.ts | 17 ++-- .../containers/use_get_case_attachments.tsx | 15 ++-- 8 files changed, 175 insertions(+), 100 deletions(-) diff --git a/x-pack/plugins/cases/public/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx index 2a5a75bf7a789..98e47df7315ac 100644 --- a/x-pack/plugins/cases/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx @@ -9,22 +9,27 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; +import { ThemeProvider } from 'styled-components'; + +import type { RenderOptions, RenderResult } from '@testing-library/react'; +import type { ILicense } from '@kbn/licensing-plugin/public'; +import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import type { FilesStart } from '@kbn/files-plugin/public'; + import { euiDarkVars } from '@kbn/ui-theme'; import { I18nProvider } from '@kbn/i18n-react'; -import { ThemeProvider } from 'styled-components'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import type { RenderOptions, RenderResult } from '@testing-library/react'; import { render as reactRender } from '@testing-library/react'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import type { ILicense } from '@kbn/licensing-plugin/public'; -import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; + import type { CasesFeatures, CasesPermissions } from '../../../common/ui/types'; -import { CasesProvider } from '../../components/cases_context'; -import { createStartServicesMock } from '../lib/kibana/kibana_react.mock'; import type { StartServices } from '../../types'; import type { ReleasePhase } from '../../components/types'; + +import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; +import { CasesProvider } from '../../components/cases_context'; +import { createStartServicesMock } from '../lib/kibana/kibana_react.mock'; import { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry'; import { PersistableStateAttachmentTypeRegistry } from '../../client/attachment_framework/persistable_state_registry'; import { allCasesPermissions } from './permissions'; @@ -37,12 +42,15 @@ interface TestProviderProps { releasePhase?: ReleasePhase; externalReferenceAttachmentTypeRegistry?: ExternalReferenceAttachmentTypeRegistry; persistableStateAttachmentTypeRegistry?: PersistableStateAttachmentTypeRegistry; + filesPlugin?: FilesStart; license?: ILicense; } type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; window.scrollTo = jest.fn(); +const mockFilesPlugin = { filesClientFactory: { asUnscoped: jest.fn(), asScoped: jest.fn() } }; + /** A utility for wrapping children in the providers required to run most tests */ const TestProvidersComponent: React.FC = ({ children, @@ -52,6 +60,7 @@ const TestProvidersComponent: React.FC = ({ releasePhase = 'ga', externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry(), persistableStateAttachmentTypeRegistry = new PersistableStateAttachmentTypeRegistry(), + filesPlugin = mockFilesPlugin, license, }) => { const queryClient = new QueryClient({ @@ -82,6 +91,7 @@ const TestProvidersComponent: React.FC = ({ features, owner, permissions, + filesPlugin, }} > {children} @@ -130,6 +140,7 @@ export const createAppMockRenderer = ({ releasePhase = 'ga', externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry(), persistableStateAttachmentTypeRegistry = new PersistableStateAttachmentTypeRegistry(), + filesPlugin = mockFilesPlugin, license, }: Omit = {}): AppMockRenderer => { const services = createStartServicesMock({ license }); @@ -161,6 +172,7 @@ export const createAppMockRenderer = ({ owner, permissions, releasePhase, + filesPlugin, }} > {children} diff --git a/x-pack/plugins/cases/public/components/add_file/index.tsx b/x-pack/plugins/cases/public/components/add_file/index.tsx index 35fae303142b2..1e3a2f42b07e3 100644 --- a/x-pack/plugins/cases/public/components/add_file/index.tsx +++ b/x-pack/plugins/cases/public/components/add_file/index.tsx @@ -63,21 +63,24 @@ const AddFileComponent: React.FC = ({ caseId, onFileAdded }) => { }, externalReferenceAttachmentTypeId: FILE_ATTACHMENT_TYPE, externalReferenceMetadata: { - file: { - name: file.fileJSON.name, - extension: file.fileJSON.extension ?? '', - mimeType: file.fileJSON.mimeType ?? '', - createdAt: file.fileJSON.created, - }, + file: [ + { + name: file.fileJSON.name, + extension: file.fileJSON.extension ?? '', + mimeType: file.fileJSON.mimeType ?? '', + createdAt: file.fileJSON.created, + }, + ], }, }, ], updateCase: onFileAdded, + throwOnError: true, }); notifications.toasts.addSuccess({ title: 'File uploaded successfuly!', - text: `File ID: ${file.id}`, + text: `File Name: ${file.fileJSON.name}`, }); // used to refresh the attachments table diff --git a/x-pack/plugins/cases/public/components/attachments/attachments_table.tsx b/x-pack/plugins/cases/public/components/attachments/attachments_table.tsx index e03011d0614c9..e56cd06645f09 100644 --- a/x-pack/plugins/cases/public/components/attachments/attachments_table.tsx +++ b/x-pack/plugins/cases/public/components/attachments/attachments_table.tsx @@ -4,11 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import type { EuiBasicTableColumn, Pagination, EuiBasicTableProps } from '@elastic/eui'; +import type { FileJSON } from '@kbn/shared-ux-file-types'; import { + EuiModalBody, + EuiModalFooter, EuiButtonIcon, EuiLink, EuiBasicTable, @@ -17,20 +20,20 @@ import { EuiText, EuiEmptyPrompt, EuiButton, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, } from '@elastic/eui'; import { useFilesContext } from '@kbn/shared-ux-file-context'; - -import type { Attachment, Attachments } from '../../../common/ui/types'; +import { FileImage } from '@kbn/shared-ux-file-image'; import { APP_ID } from '../../../common'; import { CASES_FILE_KINDS } from '../../files'; interface AttachmentsTableProps { isLoading: boolean; - items: Attachments; - onChange: EuiBasicTableProps['onChange']; - onDownload: () => void; - onDelete: () => void; + items: FileJSON[]; + onChange: EuiBasicTableProps['onChange']; pagination: Pagination; } @@ -38,21 +41,24 @@ export const AttachmentsTable = ({ items, pagination, onChange, - onDelete, - onDownload, isLoading, }: AttachmentsTableProps) => { const { client: filesClient } = useFilesContext(); + const [isModalVisible, setIsModalVisible] = useState(false); + const [selectedFile, setSelectedFile] = useState(); + + const closeModal = () => setIsModalVisible(false); + const showModal = (file: FileJSON) => { + setSelectedFile(file); + setIsModalVisible(true); + }; - const columns: Array> = [ + const columns: Array> = [ { - field: 'name', name: 'Name', 'data-test-subj': 'attachments-table-filename', - render: (name: Attachment['fileName']) => ( - - {name} - + render: (attachment: FileJSON) => ( + showModal(attachment)}>{attachment.name} ), width: '60%', }, @@ -75,7 +81,7 @@ export const AttachmentsTable = ({ name: 'Download', isPrimary: true, description: 'Download this file', - render: (attachment: Attachment) => { + render: (attachment: FileJSON) => { return ( {}, 'data-test-subj': 'attachments-table-action-delete', }, ], @@ -148,12 +154,34 @@ export const AttachmentsTable = ({ iconType="plusInCircle" data-test-subj="case-detail-attachments-table-upload-file" > - {'Upload File'} + {'Add File'} } /> } /> + {isModalVisible && ( + + + {selectedFile?.name} + + + + + + + {'Close'} + + + + )} ); }; diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.test.tsx index 734704dc988f8..e8e44c9842580 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.test.tsx @@ -27,7 +27,7 @@ describe('Case View Page files tab', () => { useGetCaseAttachmentsMock.mockReturnValue({ data: { pageOfItems: [basicAttachment], - availableTypes: [basicAttachment.fileType], + availableTypes: [basicAttachment.mimeType], totalItemCount: 1, }, isLoading: false, diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.tsx index 12556add97676..3d1e91db3d05b 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.tsx @@ -4,14 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useMemo, useState } from 'react'; +import { isEqual } from 'lodash/fp'; +import React, { useCallback, useMemo, useState } from 'react'; import type { Criteria } from '@elastic/eui'; +import type { FileJSON } from '@kbn/shared-ux-file-types'; -import { EuiFlexItem, EuiFlexGroup, EuiFieldSearch, EuiSelect, EuiButtonGroup } from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup, EuiFieldSearch, EuiButtonGroup } from '@elastic/eui'; import { useQueryClient } from '@tanstack/react-query'; -import type { Case, Attachment } from '../../../../common/ui/types'; +import type { Case } from '../../../../common/ui/types'; import type { GetCaseAttachmentsParams } from '../../../containers/use_get_case_attachments'; import { CaseViewTabs } from '../case_view_tabs'; @@ -34,15 +36,35 @@ export const CaseViewAttachments = ({ caseData }: CaseViewAttachmentsProps) => { }); const { data: attachmentsData, isLoading } = useGetCaseAttachments(filteringOptions); - const onTableChange = ({ page }: Criteria) => { - if (page) { - setFilteringOptions({ - ...filteringOptions, - page: page.index, - perPage: page.size, - }); - } - }; + const onTableChange = useCallback( + ({ page }: Criteria) => { + if (page) { + setFilteringOptions({ + ...filteringOptions, + page: page.index, + perPage: page.size, + }); + } + }, + [filteringOptions] + ); + + const onSearchChange = useCallback( + (newSearch) => { + const trimSearch = newSearch.trim(); + if (!isEqual(trimSearch, filteringOptions.searchTerm)) { + setFilteringOptions({ + ...filteringOptions, + searchTerm: trimSearch, + }); + } + }, + [filteringOptions] + ); + + const refreshAttachmentsTable = useCallback(() => { + queryClient.invalidateQueries(casesQueriesKeys.caseAttachments({ ...filteringOptions })); + }, [filteringOptions, queryClient]); const pagination = useMemo( () => ({ @@ -55,9 +77,6 @@ export const CaseViewAttachments = ({ caseData }: CaseViewAttachmentsProps) => { [filteringOptions.page, filteringOptions.perPage, attachmentsData] ); - const selectOptions = [{ value: 'any', text: 'any' }]; - const [selectValue, setSelectValue] = useState(selectOptions[0].value); - const tableViewSelectedId = 'tableViewSelectedId'; const toggleButtonsIcons = [ { @@ -73,10 +92,6 @@ export const CaseViewAttachments = ({ caseData }: CaseViewAttachmentsProps) => { }, ]; - const refreshAttachmentsTable = () => { - queryClient.invalidateQueries(casesQueriesKeys.caseAttachments({ ...filteringOptions })); - }; - return ( @@ -91,29 +106,11 @@ export const CaseViewAttachments = ({ caseData }: CaseViewAttachmentsProps) => { {} - // setFilteringOptions({ - // ...filteringOptions, - // searchTerm: event.target.value, - // }) - } - isClearable={true} + onSearch={onSearchChange} + incremental={false} data-test-subj="case-detail-search-file" /> - - setSelectValue(e.target.value)} - data-test-subj="case-detail-select-file-type" - /> - { options={toggleButtonsIcons} idSelected={tableViewSelectedId} onChange={() => {}} + hidden isIconOnly /> @@ -129,8 +127,6 @@ export const CaseViewAttachments = ({ caseData }: CaseViewAttachmentsProps) => { isLoading={isLoading} items={attachmentsData?.files ?? []} onChange={onTableChange} - onDelete={() => {}} - onDownload={() => {}} pagination={pagination} /> 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 d104d913cf9cd..81fe959e7a91f 100644 --- a/x-pack/plugins/cases/public/components/cases_context/index.tsx +++ b/x-pack/plugins/cases/public/components/cases_context/index.tsx @@ -5,27 +5,33 @@ * 2.0. */ -import type { Dispatch } from 'react'; -import React, { useState, useEffect, useReducer } from 'react'; +import type { Dispatch, ReactNode } from 'react'; + import { merge } from 'lodash'; +import React, { useCallback, useEffect, useState, useReducer } from 'react'; +import useDeepCompareEffect from 'react-use/lib/useDeepCompareEffect'; import type { FilesStart } from '@kbn/files-plugin/public'; + import { FilesContext } from '@kbn/shared-ux-file-context'; -import useDeepCompareEffect from 'react-use/lib/useDeepCompareEffect'; -import { DEFAULT_FEATURES } from '../../../common/constants'; -import { DEFAULT_BASE_PATH } from '../../common/navigation'; -import { useApplication } from './use_application'; + import type { CasesContextStoreAction } from './cases_context_reducer'; -import { casesContextReducer, getInitialCasesContextState } from './cases_context_reducer'; import type { CasesFeaturesAllRequired, CasesFeatures, CasesPermissions, } from '../../containers/types'; -import { CasesGlobalComponents } from './cases_global_components'; import type { ReleasePhase } from '../types'; import type { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry'; import type { PersistableStateAttachmentTypeRegistry } from '../../client/attachment_framework/persistable_state_registry'; +import type { Owner } from '../../../common/constants/types'; + +import { CasesGlobalComponents } from './cases_global_components'; +import { 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 { CASES_FILE_KINDS } from '../../files'; export type CasesContextValueDispatch = Dispatch; @@ -119,13 +125,37 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({ } }, [appTitle, appId]); + const applyFilesContext = useCallback( + (contextChildren: ReactNode) => { + if (owner.length === 0) { + return contextChildren; + } + + if (owner[0] in CASES_FILE_KINDS) { + return ( + + {contextChildren} + + ); + } else { + throw new Error( + 'Invalid owner provided to cases context. See https://github.com/elastic/kibana/blob/main/x-pack/plugins/cases/README.md#casescontext-setup' + ); + } + }, + [filesPlugin.filesClientFactory, owner] + ); + return isCasesContextValue(value) ? ( - {/* need to check if owner exists */} - - - {children} - + {applyFilesContext( + <> + + {children} + + )} ) : null; }; diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index eea911bc258dd..9209f24554e13 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { FileJSON } from '@kbn/shared-ux-file-types'; import type { ActionLicense, Cases, Case, CasesStatus, CaseUserActions, Comment } from './types'; @@ -18,7 +19,6 @@ import type { FindCaseUserActions, CaseUsers, CaseUserActionsStats, - Attachment, } from '../../common/ui/types'; import type { CaseConnector, @@ -241,11 +241,18 @@ export const basicCase: Case = { assignees: [{ uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0' }], }; -export const basicAttachment: Attachment = { +export const basicAttachment: FileJSON = { id: '7d47d130-bcec-11ed-afa1-0242ac120002', - fileName: 'my-super-cool-screenshot', - fileType: 'png', - dateAdded: basicCreatedAt, + name: 'my-super-cool-screenshot', + mimeType: 'png', + created: basicCreatedAt, + updated: basicCreatedAt, + size: 999, + meta: '', + alt: '', + fileKind: '', + status: 'READY', + extension: '.png', }; export const caseWithAlerts = { diff --git a/x-pack/plugins/cases/public/containers/use_get_case_attachments.tsx b/x-pack/plugins/cases/public/containers/use_get_case_attachments.tsx index 9d04a586063c4..af78ff11c033a 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_attachments.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_attachments.tsx @@ -5,13 +5,13 @@ * 2.0. */ +import type { FileJSON } from '@kbn/shared-ux-file-types'; import type { UseQueryResult } from '@tanstack/react-query'; import { useFilesContext } from '@kbn/shared-ux-file-context'; import { useQuery } from '@tanstack/react-query'; import type { ServerError } from '../types'; -import type { Attachment } from './types'; import { APP_ID } from '../../common'; import { useToasts } from '../common/lib/kibana'; @@ -23,26 +23,25 @@ export interface GetCaseAttachmentsParams { caseId: string; page: number; perPage: number; - // extension?: string[]; - // mimeType?: string[]; - // searchTerm?: string; + searchTerm?: string; } export const useGetCaseAttachments = ({ caseId, page, perPage, -}: GetCaseAttachmentsParams): UseQueryResult<{ files: Attachment[]; total: number }> => { + searchTerm, +}: GetCaseAttachmentsParams): UseQueryResult<{ files: FileJSON[]; total: number }> => { const toasts = useToasts(); const { client: filesClient } = useFilesContext(); - const filePage = page + 1; return useQuery( - casesQueriesKeys.caseAttachments({ caseId, page: filePage, perPage }), + casesQueriesKeys.caseAttachments({ caseId, page, perPage, searchTerm }), () => { return filesClient.list({ kind: CASES_FILE_KINDS[APP_ID].id, - page: filePage, + page: page + 1, + ...(searchTerm && { name: searchTerm }), perPage, meta: { caseId }, }); From c14555ead049821833ebcb62f7624ae661978111 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Fri, 10 Mar 2023 14:43:00 +0100 Subject: [PATCH 05/23] Addressing PR comments. --- x-pack/plugins/cases/common/ui/types.ts | 9 --- .../public/components/add_file/index.tsx | 5 +- .../attachments/attachments_table.test.tsx | 71 ----------------- .../components/case_view/case_view_page.tsx | 6 +- ...ents.test.tsx => case_view_files.test.tsx} | 18 ++--- ...ew_attachments.tsx => case_view_files.tsx} | 27 +++---- .../components/files/files_table.test.tsx | 71 +++++++++++++++++ .../files_table.tsx} | 78 +++++++++---------- .../public/components/files/translations.tsx | 40 ++++++++++ .../cases/public/containers/constants.ts | 3 +- ...attachments.tsx => use_get_case_files.tsx} | 21 ++--- 11 files changed, 187 insertions(+), 162 deletions(-) delete mode 100644 x-pack/plugins/cases/public/components/attachments/attachments_table.test.tsx rename x-pack/plugins/cases/public/components/case_view/components/{case_view_attachments.test.tsx => case_view_files.test.tsx} (75%) rename x-pack/plugins/cases/public/components/case_view/components/{case_view_attachments.tsx => case_view_files.tsx} (83%) create mode 100644 x-pack/plugins/cases/public/components/files/files_table.test.tsx rename x-pack/plugins/cases/public/components/{attachments/attachments_table.tsx => files/files_table.tsx} (73%) create mode 100644 x-pack/plugins/cases/public/components/files/translations.tsx rename x-pack/plugins/cases/public/containers/{use_get_case_attachments.tsx => use_get_case_files.tsx} (66%) diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index c339594219048..06765eb17ca4f 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -103,15 +103,6 @@ export interface ResolvedCase { aliasPurpose?: ResolvedSimpleSavedObject['alias_purpose']; } -export interface Attachment { - id: string; - fileName: string | null | undefined; - fileType: string; - dateAdded: string; -} - -export type Attachments = Attachment[]; - export interface SortingParams { sortField: SortFieldCase; sortOrder: 'asc' | 'desc'; diff --git a/x-pack/plugins/cases/public/components/add_file/index.tsx b/x-pack/plugins/cases/public/components/add_file/index.tsx index 1e3a2f42b07e3..ac589a4ceaac9 100644 --- a/x-pack/plugins/cases/public/components/add_file/index.tsx +++ b/x-pack/plugins/cases/public/components/add_file/index.tsx @@ -16,6 +16,7 @@ import React, { useCallback, useState } from 'react'; import type { UploadedFile } from '@kbn/shared-ux-file-upload/src/file_upload'; +import { FILE_SO_TYPE } from '@kbn/files-plugin/common'; import { FileUpload } from '@kbn/shared-ux-file-upload'; import { useFilesContext } from '@kbn/shared-ux-file-context'; @@ -59,11 +60,11 @@ const AddFileComponent: React.FC = ({ caseId, onFileAdded }) => { externalReferenceId: file.id, externalReferenceStorage: { type: ExternalReferenceStorageType.savedObject, - soType: FILE_ATTACHMENT_TYPE, + soType: FILE_SO_TYPE, }, externalReferenceAttachmentTypeId: FILE_ATTACHMENT_TYPE, externalReferenceMetadata: { - file: [ + files: [ { name: file.fileJSON.name, extension: file.fileJSON.extension ?? '', diff --git a/x-pack/plugins/cases/public/components/attachments/attachments_table.test.tsx b/x-pack/plugins/cases/public/components/attachments/attachments_table.test.tsx deleted file mode 100644 index 469022ac388e2..0000000000000 --- a/x-pack/plugins/cases/public/components/attachments/attachments_table.test.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 userEvent from '@testing-library/user-event'; - -import { basicAttachment } from '../../containers/mock'; -import type { AppMockRenderer } from '../../common/mock'; -import { createAppMockRenderer } from '../../common/mock'; -import { AttachmentsTable } from './attachments_table'; - -const onDownload = jest.fn(); -const onDelete = jest.fn(); - -const defaultProps = { - items: [basicAttachment], - pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 1 }, - onChange: jest.fn(), - onDelete, - onDownload, - isLoading: false, -}; - -describe('AttachmentsTable', () => { - let appMockRender: AppMockRenderer; - - beforeEach(() => { - jest.clearAllMocks(); - appMockRender = createAppMockRenderer(); - }); - - it('renders correctly', async () => { - appMockRender.render(); - - expect(await screen.findByTestId('attachments-table-results-count')).toBeInTheDocument(); - expect(await screen.findByTestId('attachments-table-filename')).toBeInTheDocument(); - expect(await screen.findByTestId('attachments-table-filetype')).toBeInTheDocument(); - expect(await screen.findByTestId('attachments-table-date-added')).toBeInTheDocument(); - expect(await screen.findByTestId('attachments-table-action-download')).toBeInTheDocument(); - expect(await screen.findByTestId('attachments-table-action-delete')).toBeInTheDocument(); - }); - - it('renders loading state', async () => { - appMockRender.render(); - - expect(await screen.findByTestId('attachments-table-loading')).toBeInTheDocument(); - }); - - it('renders empty table', async () => { - appMockRender.render(); - - expect(await screen.findByTestId('attachments-table-empty')).toBeInTheDocument(); - }); - - it('calls delete action', async () => { - appMockRender.render(); - userEvent.click(await screen.findByTestId('attachments-table-action-delete')); - expect(onDelete).toHaveBeenCalled(); - }); - - it('calls download action', async () => { - appMockRender.render(); - userEvent.click(await screen.findByTestId('attachments-table-action-delete')); - expect(onDelete).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx index 004383c839738..55245de4b22b2 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx @@ -18,7 +18,7 @@ import { useCasesTitleBreadcrumbs } from '../use_breadcrumbs'; import { WhitePageWrapperNoBorder } from '../wrappers'; import { CaseViewActivity } from './components/case_view_activity'; import { CaseViewAlerts } from './components/case_view_alerts'; -import { CaseViewAttachments } from './components/case_view_attachments'; +import { CaseViewFiles } from './components/case_view_files'; import { CaseViewMetrics } from './metrics'; import type { CaseViewPageProps } from './types'; import { useRefreshCaseViewPage } from './use_on_refresh_case_view_page'; @@ -141,9 +141,7 @@ export const CaseViewPage = React.memo( {activeTabId === CASE_VIEW_PAGE_TABS.ALERTS && features.alerts.enabled && ( )} - {activeTabId === CASE_VIEW_PAGE_TABS.FILES && ( - - )} + {activeTabId === CASE_VIEW_PAGE_TABS.FILES && } {timelineUi?.renderTimelineDetailsPanel ? timelineUi.renderTimelineDetailsPanel() : null} diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx similarity index 75% rename from x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.test.tsx rename to x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx index e8e44c9842580..97fa7c5246fab 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx @@ -10,12 +10,12 @@ import { alertCommentWithIndices, basicAttachment, basicCase } from '../../../co import type { AppMockRenderer } from '../../../common/mock'; import { createAppMockRenderer } from '../../../common/mock'; import type { Case } from '../../../../common'; -import { CaseViewAttachments } from './case_view_attachments'; -import { useGetCaseAttachments } from '../../../containers/use_get_case_attachments'; +import { CaseViewFiles } from './case_view_files'; +import { useGetCaseFiles } from '../../../containers/use_get_case_files'; -jest.mock('../../../containers/use_get_case_attachments'); +jest.mock('../../../containers/use_get_case_files'); -const useGetCaseAttachmentsMock = useGetCaseAttachments as jest.Mock; +const useGetCaseFilesMock = useGetCaseFiles as jest.Mock; const caseData: Case = { ...basicCase, @@ -24,7 +24,7 @@ const caseData: Case = { describe('Case View Page files tab', () => { let appMockRender: AppMockRenderer; - useGetCaseAttachmentsMock.mockReturnValue({ + useGetCaseFilesMock.mockReturnValue({ data: { pageOfItems: [basicAttachment], availableTypes: [basicAttachment.mimeType], @@ -42,7 +42,7 @@ describe('Case View Page files tab', () => { }); it('should render the utility bar for the attachments table', async () => { - const result = appMockRender.render(); + const result = appMockRender.render(); expect(await result.findByTestId('case-detail-upload-file')).toBeInTheDocument(); expect(await result.findByTestId('case-detail-search-file')).toBeInTheDocument(); @@ -50,13 +50,13 @@ describe('Case View Page files tab', () => { }); it('should render the attachments table', async () => { - const result = appMockRender.render(); + const result = appMockRender.render(); expect(await result.findByTestId('attachments-table')).toBeInTheDocument(); }); it('should disable search and filter if there are no attachments', async () => { - useGetCaseAttachmentsMock.mockReturnValue({ + useGetCaseFilesMock.mockReturnValue({ data: { pageOfItems: [], availableTypes: [], @@ -65,7 +65,7 @@ describe('Case View Page files tab', () => { isLoading: false, }); - const result = appMockRender.render(); + const result = appMockRender.render(); expect(await result.findByTestId('case-detail-search-file')).toHaveAttribute('disabled'); expect(await result.findByTestId('case-detail-select-file-type')).toHaveAttribute('disabled'); diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx similarity index 83% rename from x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.tsx rename to x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx index 3d1e91db3d05b..f0425396488c1 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_attachments.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx @@ -14,27 +14,27 @@ import { EuiFlexItem, EuiFlexGroup, EuiFieldSearch, EuiButtonGroup } from '@elas import { useQueryClient } from '@tanstack/react-query'; import type { Case } from '../../../../common/ui/types'; -import type { GetCaseAttachmentsParams } from '../../../containers/use_get_case_attachments'; +import type { GetCaseFilesParams } from '../../../containers/use_get_case_files'; import { CaseViewTabs } from '../case_view_tabs'; import { CASE_VIEW_PAGE_TABS } from '../../../../common/types'; -import { useGetCaseAttachments } from '../../../containers/use_get_case_attachments'; -import { AttachmentsTable } from '../../attachments/attachments_table'; +import { useGetCaseFiles } from '../../../containers/use_get_case_files'; +import { FilesTable } from '../../files/files_table'; import { AddFile } from '../../add_file'; import { casesQueriesKeys } from '../../../containers/constants'; -interface CaseViewAttachmentsProps { +interface CaseViewFilesProps { caseData: Case; } -export const CaseViewAttachments = ({ caseData }: CaseViewAttachmentsProps) => { +export const CaseViewFiles = ({ caseData }: CaseViewFilesProps) => { const queryClient = useQueryClient(); - const [filteringOptions, setFilteringOptions] = useState({ + const [filteringOptions, setFilteringOptions] = useState({ page: 0, - perPage: 5, + perPage: 10, caseId: caseData.id, }); - const { data: attachmentsData, isLoading } = useGetCaseAttachments(filteringOptions); + const { data: attachmentsData, isLoading } = useGetCaseFiles(filteringOptions); const onTableChange = useCallback( ({ page }: Criteria) => { @@ -63,15 +63,15 @@ export const CaseViewAttachments = ({ caseData }: CaseViewAttachmentsProps) => { ); const refreshAttachmentsTable = useCallback(() => { - queryClient.invalidateQueries(casesQueriesKeys.caseAttachments({ ...filteringOptions })); - }, [filteringOptions, queryClient]); + queryClient.invalidateQueries(casesQueriesKeys.caseView()); + }, [queryClient]); const pagination = useMemo( () => ({ pageIndex: filteringOptions.page, pageSize: filteringOptions.perPage, totalItemCount: attachmentsData?.total ?? 0, - pageSizeOptions: [1, 5, 10, 0], + pageSizeOptions: [5, 10, 0], showPerPageOptions: true, }), [filteringOptions.page, filteringOptions.perPage, attachmentsData] @@ -123,7 +123,7 @@ export const CaseViewAttachments = ({ caseData }: CaseViewAttachmentsProps) => { /> - { ); }; -CaseViewAttachments.displayName = 'CaseViewAttachments'; + +CaseViewFiles.displayName = 'CaseViewFiles'; 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 new file mode 100644 index 0000000000000..7bf046ed4559c --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/files_table.test.tsx @@ -0,0 +1,71 @@ +/* + * 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 userEvent from '@testing-library/user-event'; + +import { basicAttachment } from '../../containers/mock'; +import type { AppMockRenderer } from '../../common/mock'; +import { createAppMockRenderer } from '../../common/mock'; +import { FilesTable } from './files_table'; + +const onDownload = jest.fn(); +const onDelete = jest.fn(); + +const defaultProps = { + items: [basicAttachment], + pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 1 }, + onChange: jest.fn(), + onDelete, + onDownload, + isLoading: false, +}; + +describe('FilesTable', () => { + let appMockRender: AppMockRenderer; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('renders correctly', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('files-table-results-count')).toBeInTheDocument(); + expect(await screen.findByTestId('files-table-filename')).toBeInTheDocument(); + expect(await screen.findByTestId('files-table-filetype')).toBeInTheDocument(); + expect(await screen.findByTestId('files-table-date-added')).toBeInTheDocument(); + expect(await screen.findByTestId('files-table-action-download')).toBeInTheDocument(); + expect(await screen.findByTestId('files-table-action-delete')).toBeInTheDocument(); + }); + + it('renders loading state', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('files-table-loading')).toBeInTheDocument(); + }); + + it('renders empty table', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('files-table-empty')).toBeInTheDocument(); + }); + + it('calls delete action', async () => { + appMockRender.render(); + userEvent.click(await screen.findByTestId('files-table-action-delete')); + expect(onDelete).toHaveBeenCalled(); + }); + + it('calls download action', async () => { + appMockRender.render(); + userEvent.click(await screen.findByTestId('files-table-action-delete')); + expect(onDelete).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/attachments/attachments_table.tsx b/x-pack/plugins/cases/public/components/files/files_table.tsx similarity index 73% rename from x-pack/plugins/cases/public/components/attachments/attachments_table.tsx rename to x-pack/plugins/cases/public/components/files/files_table.tsx index e56cd06645f09..bb93591909414 100644 --- a/x-pack/plugins/cases/public/components/attachments/attachments_table.tsx +++ b/x-pack/plugins/cases/public/components/files/files_table.tsx @@ -27,22 +27,37 @@ import { import { useFilesContext } from '@kbn/shared-ux-file-context'; import { FileImage } from '@kbn/shared-ux-file-image'; +import * as i18n from './translations'; import { APP_ID } from '../../../common'; import { CASES_FILE_KINDS } from '../../files'; -interface AttachmentsTableProps { +const EmptyFilesTable = () => ( + {'No attachments available'}} + data-test-subj="files-table-empty" + titleSize="xs" + actions={ + + {i18n.ADD_FILE} + + } + /> +); + +EmptyFilesTable.displayName = 'EmptyFilesTable'; + +interface FilesTableProps { isLoading: boolean; items: FileJSON[]; onChange: EuiBasicTableProps['onChange']; pagination: Pagination; } -export const AttachmentsTable = ({ - items, - pagination, - onChange, - isLoading, -}: AttachmentsTableProps) => { +export const FilesTable = ({ items, pagination, onChange, isLoading }: FilesTableProps) => { const { client: filesClient } = useFilesContext(); const [isModalVisible, setIsModalVisible] = useState(false); const [selectedFile, setSelectedFile] = useState(); @@ -55,32 +70,32 @@ export const AttachmentsTable = ({ const columns: Array> = [ { - name: 'Name', - 'data-test-subj': 'attachments-table-filename', + name: i18n.NAME, + 'data-test-subj': 'files-table-filename', render: (attachment: FileJSON) => ( showModal(attachment)}>{attachment.name} ), width: '60%', }, { + name: i18n.TYPE, field: 'mimeType', - 'data-test-subj': 'attachments-table-filetype', - name: 'Type', + 'data-test-subj': 'files-table-filetype', }, { + name: i18n.DATE_ADDED, field: 'created', - name: 'Date Added', - 'data-test-subj': 'attachments-table-date-added', + 'data-test-subj': 'files-table-date-added', dataType: 'date', }, { - name: 'Actions', + name: i18n.ACTIONS, width: '120px', actions: [ { name: 'Download', isPrimary: true, - description: 'Download this file', + description: i18n.DOWNLOAD_FILE, render: (attachment: FileJSON) => { return ( ); }, - 'data-test-subj': 'attachments-table-action-download', + 'data-test-subj': 'files-table-action-download', }, { name: 'Delete', isPrimary: true, - description: 'Delete this file', + description: i18n.DELETE_FILE, color: 'danger', icon: 'trash', type: 'icon', onClick: () => {}, - 'data-test-subj': 'attachments-table-action-delete', + 'data-test-subj': 'files-table-action-delete', }, ], }, @@ -124,41 +139,26 @@ export const AttachmentsTable = ({ ); return isLoading ? ( - + ) : ( <> {pagination.totalItemCount > 0 && ( <> - - {'Showing'} {resultsCount} + + {i18n.RESULTS_COUNT} {resultsCount} )} {'No attachments available'}} - data-test-subj="attachments-table-empty" - titleSize="xs" - actions={ - - {'Add File'} - - } - /> - } + noItemsMessage={} /> {isModalVisible && ( @@ -186,4 +186,4 @@ export const AttachmentsTable = ({ ); }; -AttachmentsTable.displayName = 'AttachmentsTable'; +FilesTable.displayName = 'FilesTable'; diff --git a/x-pack/plugins/cases/public/components/files/translations.tsx b/x-pack/plugins/cases/public/components/files/translations.tsx new file mode 100644 index 0000000000000..dfaf8ba984584 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/translations.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 { i18n } from '@kbn/i18n'; + +export const ADD_FILE = i18n.translate('xpack.cases.caseView.files.addFile', { + defaultMessage: 'Add File', +}); + +export const DOWNLOAD_FILE = i18n.translate('xpack.cases.caseView.files.downloadFile', { + defaultMessage: 'Download File', +}); + +export const DELETE_FILE = i18n.translate('xpack.cases.caseView.files.deleteFile', { + defaultMessage: 'Delete File', +}); + +export const RESULTS_COUNT = i18n.translate('xpack.cases.caseView.files.resultsCount', { + defaultMessage: 'Showing', +}); + +export const NAME = i18n.translate('xpack.cases.caseView.files.name', { + defaultMessage: 'Name', +}); + +export const TYPE = i18n.translate('xpack.cases.caseView.files.type', { + defaultMessage: 'Type', +}); + +export const DATE_ADDED = i18n.translate('xpack.cases.caseView.files.dateAdded', { + defaultMessage: 'Date Added', +}); + +export const ACTIONS = i18n.translate('xpack.cases.caseView.files.actions', { + defaultMessage: 'Actions', +}); diff --git a/x-pack/plugins/cases/public/containers/constants.ts b/x-pack/plugins/cases/public/containers/constants.ts index 9a83bb6539f39..66d5675b6c72f 100644 --- a/x-pack/plugins/cases/public/containers/constants.ts +++ b/x-pack/plugins/cases/public/containers/constants.ts @@ -23,8 +23,7 @@ export const casesQueriesKeys = { cases: (params: unknown) => [...casesQueriesKeys.casesList(), 'all-cases', params] as const, caseView: () => [...casesQueriesKeys.all, 'case'] as const, case: (id: string) => [...casesQueriesKeys.caseView(), id] as const, - caseAttachments: (params: unknown) => - [...casesQueriesKeys.caseView(), 'attachments', params] as const, + caseFiles: (params: unknown) => [...casesQueriesKeys.caseView(), 'attachments', params] as const, caseMetrics: (id: string, features: SingleCaseMetricsFeature[]) => [...casesQueriesKeys.case(id), 'metrics', features] as const, caseConnectors: (id: string) => [...casesQueriesKeys.case(id), 'connectors'], diff --git a/x-pack/plugins/cases/public/containers/use_get_case_attachments.tsx b/x-pack/plugins/cases/public/containers/use_get_case_files.tsx similarity index 66% rename from x-pack/plugins/cases/public/containers/use_get_case_attachments.tsx rename to x-pack/plugins/cases/public/containers/use_get_case_files.tsx index af78ff11c033a..54401382e03c2 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_attachments.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_files.tsx @@ -14,34 +14,34 @@ import { useQuery } from '@tanstack/react-query'; import type { ServerError } from '../types'; import { APP_ID } from '../../common'; -import { useToasts } from '../common/lib/kibana'; +import { useCasesToast } from '../common/use_cases_toast'; import { CASES_FILE_KINDS } from '../files'; import { casesQueriesKeys } from './constants'; import * as i18n from './translations'; -export interface GetCaseAttachmentsParams { +export interface GetCaseFilesParams { caseId: string; page: number; perPage: number; searchTerm?: string; } -export const useGetCaseAttachments = ({ +export const useGetCaseFiles = ({ caseId, page, perPage, searchTerm, -}: GetCaseAttachmentsParams): UseQueryResult<{ files: FileJSON[]; total: number }> => { - const toasts = useToasts(); +}: GetCaseFilesParams): UseQueryResult<{ files: FileJSON[]; total: number }> => { + const { showErrorToast } = useCasesToast(); const { client: filesClient } = useFilesContext(); return useQuery( - casesQueriesKeys.caseAttachments({ caseId, page, perPage, searchTerm }), + casesQueriesKeys.caseFiles({ caseId, page, perPage, searchTerm }), () => { return filesClient.list({ kind: CASES_FILE_KINDS[APP_ID].id, page: page + 1, - ...(searchTerm && { name: searchTerm }), + ...(searchTerm && { name: `*${searchTerm}*` }), perPage, meta: { caseId }, }); @@ -49,12 +49,7 @@ export const useGetCaseAttachments = ({ { keepPreviousData: true, onError: (error: ServerError) => { - if (error.name !== 'AbortError') { - toasts.addError( - error.body && error.body.message ? new Error(error.body.message) : error, - { title: i18n.ERROR_TITLE } - ); - } + showErrorToast(error, { title: i18n.ERROR_TITLE }); }, } ); From fa72cd9606eb1b425b7ddbec3028548e29aef8fb Mon Sep 17 00:00:00 2001 From: adcoelho Date: Fri, 10 Mar 2023 17:02:53 +0100 Subject: [PATCH 06/23] Addressing PR comments 2. --- x-pack/plugins/cases/public/application.tsx | 10 +++++----- .../client/ui/get_all_cases_selector_modal.tsx | 4 ++-- .../cases/public/client/ui/get_cases.tsx | 6 +++--- .../public/client/ui/get_cases_context.tsx | 12 ++++++------ .../client/ui/get_create_case_flyout.tsx | 6 +++--- .../public/client/ui/get_recent_cases.tsx | 6 +++--- .../public/common/mock/test_providers.tsx | 14 +++++++------- .../cases/public/components/add_file/index.tsx | 11 +++++------ .../public/components/add_file/translations.ts | 13 +++++++++++++ .../cases/public/components/app/index.tsx | 8 ++++---- .../components/case_view_files.test.tsx | 4 ++-- .../public/components/cases_context/index.tsx | 12 +++++------- .../components/files/files_table.test.tsx | 18 ------------------ x-pack/plugins/cases/public/plugin.ts | 10 +++++----- 14 files changed, 63 insertions(+), 71 deletions(-) diff --git a/x-pack/plugins/cases/public/application.tsx b/x-pack/plugins/cases/public/application.tsx index 32959122a2e6a..742f254472160 100644 --- a/x-pack/plugins/cases/public/application.tsx +++ b/x-pack/plugins/cases/public/application.tsx @@ -18,7 +18,7 @@ import { useUiSetting$, } from '@kbn/kibana-react-plugin/public'; -import type { FilesStart } from '@kbn/files-plugin/public'; +import type { ScopedFilesClient } from '@kbn/files-plugin/public'; import type { ExternalReferenceAttachmentTypeRegistry } from './client/attachment_framework/external_reference_registry'; import type { PersistableStateAttachmentTypeRegistry } from './client/attachment_framework/persistable_state_registry'; import type { RenderAppProps } from './types'; @@ -39,14 +39,14 @@ export const renderApp = (deps: RenderAppProps) => { interface CasesAppWithContextProps { externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry; persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry; - filesPlugin: FilesStart; + getFilesClient: (scope: string) => ScopedFilesClient; } const CasesAppWithContext: React.FC = React.memo( ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, - filesPlugin, + getFilesClient, }) => { const [darkMode] = useUiSetting$('theme:darkMode'); @@ -55,7 +55,7 @@ const CasesAppWithContext: React.FC = React.memo( ); @@ -86,7 +86,7 @@ export const App: React.FC<{ deps: RenderAppProps }> = ({ deps }) => { deps.externalReferenceAttachmentTypeRegistry } persistableStateAttachmentTypeRegistry={deps.persistableStateAttachmentTypeRegistry} - filesPlugin={pluginsStart.files} + getFilesClient={pluginsStart.files.filesClientFactory.asScoped} /> diff --git a/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx b/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx index 564ed1e8506ee..79822bdf07c45 100644 --- a/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx +++ b/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx @@ -23,7 +23,7 @@ const AllCasesSelectorModalLazy: React.FC = lazy( export const getAllCasesSelectorModalLazy = ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, - filesPlugin, + getFilesClient, owner, permissions, hiddenStatuses, @@ -34,7 +34,7 @@ export const getAllCasesSelectorModalLazy = ({ value={{ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, - filesPlugin, + getFilesClient, owner, permissions, }} diff --git a/x-pack/plugins/cases/public/client/ui/get_cases.tsx b/x-pack/plugins/cases/public/client/ui/get_cases.tsx index 255e83b5704fe..36556523fc3a3 100644 --- a/x-pack/plugins/cases/public/client/ui/get_cases.tsx +++ b/x-pack/plugins/cases/public/client/ui/get_cases.tsx @@ -16,7 +16,7 @@ export type GetCasesProps = Omit< GetCasesPropsInternal, | 'externalReferenceAttachmentTypeRegistry' | 'persistableStateAttachmentTypeRegistry' - | 'filesPlugin' + | 'getFilesClient' >; const CasesRoutesLazy: React.FC = lazy(() => import('../../components/app/routes')); @@ -24,7 +24,7 @@ const CasesRoutesLazy: React.FC = lazy(() => import('../../component export const getCasesLazy = ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, - filesPlugin, + getFilesClient, owner, permissions, basePath, @@ -42,7 +42,7 @@ export const getCasesLazy = ({ value={{ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, - filesPlugin, + getFilesClient, owner, permissions, basePath, diff --git a/x-pack/plugins/cases/public/client/ui/get_cases_context.tsx b/x-pack/plugins/cases/public/client/ui/get_cases_context.tsx index 26519dcd61520..9db49ef9776ba 100644 --- a/x-pack/plugins/cases/public/client/ui/get_cases_context.tsx +++ b/x-pack/plugins/cases/public/client/ui/get_cases_context.tsx @@ -15,7 +15,7 @@ export type GetCasesContextProps = Omit< CasesContextProps, | 'externalReferenceAttachmentTypeRegistry' | 'persistableStateAttachmentTypeRegistry' - | 'filesPlugin' + | 'getFilesClient' >; const CasesProviderLazy: React.FC<{ value: GetCasesContextPropsInternal }> = lazy( @@ -30,7 +30,7 @@ const CasesProviderLazyWrapper = ({ features, children, releasePhase, - filesPlugin, + getFilesClient, }: GetCasesContextPropsInternal & { children: ReactNode }) => { return ( }> @@ -42,7 +42,7 @@ const CasesProviderLazyWrapper = ({ permissions, features, releasePhase, - filesPlugin, + getFilesClient, }} > {children} @@ -56,12 +56,12 @@ CasesProviderLazyWrapper.displayName = 'CasesProviderLazyWrapper'; export const getCasesContextLazy = ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, - filesPlugin, + getFilesClient, }: Pick< GetCasesContextPropsInternal, | 'externalReferenceAttachmentTypeRegistry' | 'persistableStateAttachmentTypeRegistry' - | 'filesPlugin' + | 'getFilesClient' >): (() => React.FC) => { const CasesProviderLazyWrapperWithRegistry: React.FC = ({ children, @@ -71,7 +71,7 @@ export const getCasesContextLazy = ({ {...props} externalReferenceAttachmentTypeRegistry={externalReferenceAttachmentTypeRegistry} persistableStateAttachmentTypeRegistry={persistableStateAttachmentTypeRegistry} - filesPlugin={filesPlugin} + getFilesClient={getFilesClient} > {children} diff --git a/x-pack/plugins/cases/public/client/ui/get_create_case_flyout.tsx b/x-pack/plugins/cases/public/client/ui/get_create_case_flyout.tsx index cdfeaef1c5ecf..e52a14033a614 100644 --- a/x-pack/plugins/cases/public/client/ui/get_create_case_flyout.tsx +++ b/x-pack/plugins/cases/public/client/ui/get_create_case_flyout.tsx @@ -16,7 +16,7 @@ export type GetCreateCaseFlyoutProps = Omit< GetCreateCaseFlyoutPropsInternal, | 'externalReferenceAttachmentTypeRegistry' | 'persistableStateAttachmentTypeRegistry' - | 'filesPlugin' + | 'getFilesClient' >; export const CreateCaseFlyoutLazy: React.FC = lazy( @@ -25,7 +25,7 @@ export const CreateCaseFlyoutLazy: React.FC = lazy( export const getCreateCaseFlyoutLazy = ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, - filesPlugin, + getFilesClient, owner, permissions, features, @@ -38,7 +38,7 @@ export const getCreateCaseFlyoutLazy = ({ value={{ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, - filesPlugin, + getFilesClient, owner, permissions, features, diff --git a/x-pack/plugins/cases/public/client/ui/get_recent_cases.tsx b/x-pack/plugins/cases/public/client/ui/get_recent_cases.tsx index ce4ffa260a9b3..7c41cc3842bf7 100644 --- a/x-pack/plugins/cases/public/client/ui/get_recent_cases.tsx +++ b/x-pack/plugins/cases/public/client/ui/get_recent_cases.tsx @@ -16,7 +16,7 @@ export type GetRecentCasesProps = Omit< GetRecentCasesPropsInternal, | 'externalReferenceAttachmentTypeRegistry' | 'persistableStateAttachmentTypeRegistry' - | 'filesPlugin' + | 'getFilesClient' >; const RecentCasesLazy: React.FC = lazy( @@ -25,7 +25,7 @@ const RecentCasesLazy: React.FC = lazy( export const getRecentCasesLazy = ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, - filesPlugin, + getFilesClient, owner, permissions, maxCasesToShow, @@ -34,7 +34,7 @@ export const getRecentCasesLazy = ({ value={{ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, - filesPlugin, + getFilesClient, owner, permissions, }} diff --git a/x-pack/plugins/cases/public/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx index 98e47df7315ac..832c0571e4f77 100644 --- a/x-pack/plugins/cases/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx @@ -14,7 +14,7 @@ import { ThemeProvider } from 'styled-components'; import type { RenderOptions, RenderResult } from '@testing-library/react'; import type { ILicense } from '@kbn/licensing-plugin/public'; import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -import type { FilesStart } from '@kbn/files-plugin/public'; +import type { ScopedFilesClient } from '@kbn/files-plugin/public'; import { euiDarkVars } from '@kbn/ui-theme'; import { I18nProvider } from '@kbn/i18n-react'; @@ -42,14 +42,14 @@ interface TestProviderProps { releasePhase?: ReleasePhase; externalReferenceAttachmentTypeRegistry?: ExternalReferenceAttachmentTypeRegistry; persistableStateAttachmentTypeRegistry?: PersistableStateAttachmentTypeRegistry; - filesPlugin?: FilesStart; + getFilesClient?: (scope: string) => ScopedFilesClient; license?: ILicense; } type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; window.scrollTo = jest.fn(); -const mockFilesPlugin = { filesClientFactory: { asUnscoped: jest.fn(), asScoped: jest.fn() } }; +const mockGetFilesClient = jest.fn(); /** A utility for wrapping children in the providers required to run most tests */ const TestProvidersComponent: React.FC = ({ @@ -60,7 +60,7 @@ const TestProvidersComponent: React.FC = ({ releasePhase = 'ga', externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry(), persistableStateAttachmentTypeRegistry = new PersistableStateAttachmentTypeRegistry(), - filesPlugin = mockFilesPlugin, + getFilesClient = mockGetFilesClient, license, }) => { const queryClient = new QueryClient({ @@ -91,7 +91,7 @@ const TestProvidersComponent: React.FC = ({ features, owner, permissions, - filesPlugin, + getFilesClient, }} > {children} @@ -140,7 +140,7 @@ export const createAppMockRenderer = ({ releasePhase = 'ga', externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry(), persistableStateAttachmentTypeRegistry = new PersistableStateAttachmentTypeRegistry(), - filesPlugin = mockFilesPlugin, + getFilesClient = mockGetFilesClient, license, }: Omit = {}): AppMockRenderer => { const services = createStartServicesMock({ license }); @@ -172,7 +172,7 @@ export const createAppMockRenderer = ({ owner, permissions, releasePhase, - filesPlugin, + getFilesClient, }} > {children} diff --git a/x-pack/plugins/cases/public/components/add_file/index.tsx b/x-pack/plugins/cases/public/components/add_file/index.tsx index ac589a4ceaac9..abc20be965e58 100644 --- a/x-pack/plugins/cases/public/components/add_file/index.tsx +++ b/x-pack/plugins/cases/public/components/add_file/index.tsx @@ -21,6 +21,7 @@ import { FileUpload } from '@kbn/shared-ux-file-upload'; import { useFilesContext } from '@kbn/shared-ux-file-context'; import { APP_ID, CommentType, ExternalReferenceStorageType } from '../../../common'; +import { FILE_ATTACHMENT_TYPE } from '../../../common/api'; import { CASES_FILE_KINDS } from '../../files'; import { useKibana } from '../../common/lib/kibana'; import { useCreateAttachments } from '../../containers/use_create_attachments'; @@ -32,8 +33,6 @@ interface AddFileProps { onFileAdded: () => void; } -const FILE_ATTACHMENT_TYPE = '.files'; - const AddFileComponent: React.FC = ({ caseId, onFileAdded }) => { const { notifications } = useKibana().services; const { client: filesClient } = useFilesContext(); @@ -80,8 +79,8 @@ const AddFileComponent: React.FC = ({ caseId, onFileAdded }) => { }); notifications.toasts.addSuccess({ - title: 'File uploaded successfuly!', - text: `File Name: ${file.fileJSON.name}`, + title: i18n.SUCCESSFUL_UPLOAD, + text: i18n.SUCCESSFUL_UPLOAD_FILE_NAME(file.fileJSON.name), }); // used to refresh the attachments table @@ -101,7 +100,7 @@ const AddFileComponent: React.FC = ({ caseId, onFileAdded }) => { const onError = useCallback( (error) => { notifications.toasts.addError(error, { - title: 'Failed to upload', + title: i18n.FAILED_UPLOAD, }); }, [notifications.toasts] @@ -121,7 +120,7 @@ const AddFileComponent: React.FC = ({ caseId, onFileAdded }) => { {isModalVisible && ( - {'Add File'} + {i18n.ADD_FILE} + i18n.translate('xpack.cases.caseView.successfulUpload', { + defaultMessage: `File ${fileName} uploaded successfully`, + }); diff --git a/x-pack/plugins/cases/public/components/app/index.tsx b/x-pack/plugins/cases/public/components/app/index.tsx index da1ec8bfa54e1..f53e7edf9356a 100644 --- a/x-pack/plugins/cases/public/components/app/index.tsx +++ b/x-pack/plugins/cases/public/components/app/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import type { FilesStart } from '@kbn/files-plugin/public'; +import type { ScopedFilesClient } from '@kbn/files-plugin/public'; import type { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry'; import type { PersistableStateAttachmentTypeRegistry } from '../../client/attachment_framework/persistable_state_registry'; @@ -23,13 +23,13 @@ export type CasesProps = CasesRoutesProps; interface CasesAppProps { externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry; persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry; - filesPlugin: FilesStart; + getFilesClient: (scope: string) => ScopedFilesClient; } const CasesAppComponent: React.FC = ({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, - filesPlugin, + getFilesClient, }) => { const userCapabilities = useApplicationCapabilities(); @@ -38,7 +38,7 @@ const CasesAppComponent: React.FC = ({ {getCasesLazy({ externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, - filesPlugin, + getFilesClient, owner: [APP_OWNER], useFetchAlertData: () => [false, {}], permissions: userCapabilities.generalCases, diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx index 97fa7c5246fab..b2901643a02a4 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx @@ -41,7 +41,7 @@ describe('Case View Page files tab', () => { jest.clearAllMocks(); }); - it('should render the utility bar for the attachments table', async () => { + it('should render the utility bar for the files table', async () => { const result = appMockRender.render(); expect(await result.findByTestId('case-detail-upload-file')).toBeInTheDocument(); @@ -49,7 +49,7 @@ describe('Case View Page files tab', () => { expect(await result.findByTestId('case-detail-select-file-type')).toBeInTheDocument(); }); - it('should render the attachments table', async () => { + it('should render the files table', async () => { const result = appMockRender.render(); expect(await result.findByTestId('attachments-table')).toBeInTheDocument(); 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 81fe959e7a91f..eb5d09171e26b 100644 --- a/x-pack/plugins/cases/public/components/cases_context/index.tsx +++ b/x-pack/plugins/cases/public/components/cases_context/index.tsx @@ -11,7 +11,7 @@ import { merge } from 'lodash'; import React, { useCallback, useEffect, useState, useReducer } from 'react'; import useDeepCompareEffect from 'react-use/lib/useDeepCompareEffect'; -import type { FilesStart } from '@kbn/files-plugin/public'; +import type { ScopedFilesClient } from '@kbn/files-plugin/public'; import { FilesContext } from '@kbn/shared-ux-file-context'; @@ -59,7 +59,7 @@ export interface CasesContextProps basePath?: string; features?: CasesFeatures; releasePhase?: ReleasePhase; - filesPlugin: FilesStart; + getFilesClient: (scope: string) => ScopedFilesClient; } export const CasesContext = React.createContext(undefined); @@ -79,7 +79,7 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({ basePath = DEFAULT_BASE_PATH, features = {}, releasePhase = 'ga', - filesPlugin, + getFilesClient, }, }) => { const { appId, appTitle } = useApplication(); @@ -133,9 +133,7 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({ if (owner[0] in CASES_FILE_KINDS) { return ( - + {contextChildren} ); @@ -145,7 +143,7 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({ ); } }, - [filesPlugin.filesClientFactory, owner] + [getFilesClient, owner] ); return isCasesContextValue(value) ? ( 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 7bf046ed4559c..6c8368a608e3d 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 @@ -7,22 +7,16 @@ import React from 'react'; import { screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import { basicAttachment } from '../../containers/mock'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; import { FilesTable } from './files_table'; -const onDownload = jest.fn(); -const onDelete = jest.fn(); - const defaultProps = { items: [basicAttachment], pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 1 }, onChange: jest.fn(), - onDelete, - onDownload, isLoading: false, }; @@ -56,16 +50,4 @@ describe('FilesTable', () => { expect(await screen.findByTestId('files-table-empty')).toBeInTheDocument(); }); - - it('calls delete action', async () => { - appMockRender.render(); - userEvent.click(await screen.findByTestId('files-table-action-delete')); - expect(onDelete).toHaveBeenCalled(); - }); - - it('calls download action', async () => { - appMockRender.render(); - userEvent.click(await screen.findByTestId('files-table-action-delete')); - expect(onDelete).toHaveBeenCalled(); - }); }); diff --git a/x-pack/plugins/cases/public/plugin.ts b/x-pack/plugins/cases/public/plugin.ts index 5e26721af07ec..6b828e72b279a 100644 --- a/x-pack/plugins/cases/public/plugin.ts +++ b/x-pack/plugins/cases/public/plugin.ts @@ -115,7 +115,7 @@ export class CasesUiPlugin const getCasesContext = getCasesContextLazy({ externalReferenceAttachmentTypeRegistry: this.externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry: this.persistableStateAttachmentTypeRegistry, - filesPlugin: plugins.files, + getFilesClient: plugins.files.filesClientFactory.asScoped, }); return { @@ -126,7 +126,7 @@ export class CasesUiPlugin ...props, externalReferenceAttachmentTypeRegistry: this.externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry: this.persistableStateAttachmentTypeRegistry, - filesPlugin: plugins.files, + getFilesClient: plugins.files.filesClientFactory.asScoped, }), getCasesContext, getRecentCases: (props) => @@ -134,7 +134,7 @@ export class CasesUiPlugin ...props, externalReferenceAttachmentTypeRegistry: this.externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry: this.persistableStateAttachmentTypeRegistry, - filesPlugin: plugins.files, + getFilesClient: plugins.files.filesClientFactory.asScoped, }), // @deprecated Please use the hook getUseCasesAddToNewCaseFlyout getCreateCaseFlyout: (props) => @@ -142,7 +142,7 @@ export class CasesUiPlugin ...props, externalReferenceAttachmentTypeRegistry: this.externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry: this.persistableStateAttachmentTypeRegistry, - filesPlugin: plugins.files, + getFilesClient: plugins.files.filesClientFactory.asScoped, }), // @deprecated Please use the hook getUseCasesAddToExistingCaseModal getAllCasesSelectorModal: (props) => @@ -150,7 +150,7 @@ export class CasesUiPlugin ...props, externalReferenceAttachmentTypeRegistry: this.externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry: this.persistableStateAttachmentTypeRegistry, - filesPlugin: plugins.files, + getFilesClient: plugins.files.filesClientFactory.asScoped, }), }, hooks: { From fef90949d150baef069a21e6eb40ab2a39a31648 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 10 Mar 2023 16:09:40 +0000 Subject: [PATCH 07/23] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/cases/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/cases/tsconfig.json b/x-pack/plugins/cases/tsconfig.json index 190b9c7e04a46..691dac55872ef 100644 --- a/x-pack/plugins/cases/tsconfig.json +++ b/x-pack/plugins/cases/tsconfig.json @@ -61,7 +61,6 @@ "@kbn/shared-ux-file-context", "@kbn/shared-ux-file-image", "@kbn/shared-ux-file-upload", - "@kbn/shared-ux-file-picker", ], "exclude": [ "target/**/*", From a5a009527b1f406cc4b2c4e3d353e64207158826 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Mon, 20 Mar 2023 16:28:55 +0100 Subject: [PATCH 08/23] Passed owner in file metadata. Hidden view toggle. Changed available page sizes. Created the FilePreviewModal component. Moved the FilesTable columns to a hook. Updated translations. Now checking if file is image before allowing preview. --- .../public/components/add_file/index.tsx | 10 +- .../case_view/components/case_view_files.tsx | 19 ++- .../components/files/file_preview_modal.tsx | 63 ++++++++++ .../public/components/files/files_table.tsx | 109 +++--------------- .../public/components/files/translations.tsx | 36 ++++-- .../files/use_files_table_columns.test.tsx | 80 +++++++++++++ .../files/use_files_table_columns.tsx | 88 ++++++++++++++ .../cases/public/components/files/utils.tsx | 10 ++ .../public/containers/use_get_case_files.tsx | 6 +- 9 files changed, 304 insertions(+), 117 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/files/file_preview_modal.tsx create mode 100644 x-pack/plugins/cases/public/components/files/use_files_table_columns.test.tsx create mode 100644 x-pack/plugins/cases/public/components/files/use_files_table_columns.tsx create mode 100644 x-pack/plugins/cases/public/components/files/utils.tsx diff --git a/x-pack/plugins/cases/public/components/add_file/index.tsx b/x-pack/plugins/cases/public/components/add_file/index.tsx index abc20be965e58..f83c696747ff2 100644 --- a/x-pack/plugins/cases/public/components/add_file/index.tsx +++ b/x-pack/plugins/cases/public/components/add_file/index.tsx @@ -25,20 +25,18 @@ import { FILE_ATTACHMENT_TYPE } from '../../../common/api'; import { CASES_FILE_KINDS } from '../../files'; import { useKibana } from '../../common/lib/kibana'; import { useCreateAttachments } from '../../containers/use_create_attachments'; -import { useCasesContext } from '../cases_context/use_cases_context'; import * as i18n from './translations'; interface AddFileProps { caseId: string; onFileAdded: () => void; + owner: string; } -const AddFileComponent: React.FC = ({ caseId, onFileAdded }) => { +const AddFileComponent: React.FC = ({ caseId, onFileAdded, owner }) => { const { notifications } = useKibana().services; const { client: filesClient } = useFilesContext(); - const { owner } = useCasesContext(); - const { isLoading, createAttachments } = useCreateAttachments(); const [isModalVisible, setIsModalVisible] = useState(false); @@ -52,7 +50,7 @@ const AddFileComponent: React.FC = ({ caseId, onFileAdded }) => { try { await createAttachments({ caseId, - caseOwner: owner[0], + caseOwner: owner, data: [ { type: CommentType.externalReference, @@ -127,7 +125,7 @@ const AddFileComponent: React.FC = ({ caseId, onFileAdded }) => { kind={CASES_FILE_KINDS[APP_ID].id} onDone={onUploadDone} onError={onError} - meta={{ caseId }} + meta={{ caseId, owner }} /> diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx index f0425396488c1..e06ebf8a26cd4 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx @@ -16,12 +16,13 @@ import { useQueryClient } from '@tanstack/react-query'; import type { Case } from '../../../../common/ui/types'; import type { GetCaseFilesParams } from '../../../containers/use_get_case_files'; -import { CaseViewTabs } from '../case_view_tabs'; import { CASE_VIEW_PAGE_TABS } from '../../../../common/types'; +import { casesQueriesKeys } from '../../../containers/constants'; import { useGetCaseFiles } from '../../../containers/use_get_case_files'; -import { FilesTable } from '../../files/files_table'; import { AddFile } from '../../add_file'; -import { casesQueriesKeys } from '../../../containers/constants'; +import { useCasesContext } from '../../cases_context/use_cases_context'; +import { FilesTable } from '../../files/files_table'; +import { CaseViewTabs } from '../case_view_tabs'; interface CaseViewFilesProps { caseData: Case; @@ -29,10 +30,12 @@ interface CaseViewFilesProps { export const CaseViewFiles = ({ caseData }: CaseViewFilesProps) => { const queryClient = useQueryClient(); + const { owner } = useCasesContext(); const [filteringOptions, setFilteringOptions] = useState({ page: 0, perPage: 10, caseId: caseData.id, + owner: owner[0], }); const { data: attachmentsData, isLoading } = useGetCaseFiles(filteringOptions); @@ -71,7 +74,7 @@ export const CaseViewFiles = ({ caseData }: CaseViewFilesProps) => { pageIndex: filteringOptions.page, pageSize: filteringOptions.perPage, totalItemCount: attachmentsData?.total ?? 0, - pageSizeOptions: [5, 10, 0], + pageSizeOptions: [10, 25, 50], showPerPageOptions: true, }), [filteringOptions.page, filteringOptions.perPage, attachmentsData] @@ -100,7 +103,11 @@ export const CaseViewFiles = ({ caseData }: CaseViewFilesProps) => { - + { options={toggleButtonsIcons} idSelected={tableViewSelectedId} onChange={() => {}} - hidden + css={'display:none'} isIconOnly /> diff --git a/x-pack/plugins/cases/public/components/files/file_preview_modal.tsx b/x-pack/plugins/cases/public/components/files/file_preview_modal.tsx new file mode 100644 index 0000000000000..e9413d4559613 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_preview_modal.tsx @@ -0,0 +1,63 @@ +/* + * 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 { + EuiModalBody, + EuiModalFooter, + EuiButton, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, +} from '@elastic/eui'; +import { FileImage } from '@kbn/shared-ux-file-image'; + +import { APP_ID } from '../../../common'; +import { CASES_FILE_KINDS } from '../../files'; +import * as i18n from './translations'; +import { isImage } from './utils'; + +interface FilePreviewModalProps { + closeModal: () => void; + getDownloadHref: (args: Pick, 'id' | 'fileKind'>) => string; + selectedFile: FileJSON; +} + +export const FilePreviewModal = ({ + closeModal, + selectedFile, + getDownloadHref, +}: FilePreviewModalProps) => { + return ( + + + {selectedFile?.name} + + + {isImage(selectedFile) && ( + + )} + + + + {i18n.CLOSE_MODAL} + + + + ); +}; + +FilePreviewModal.displayName = 'FilePreviewModal'; 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 bb93591909414..4c36af738d681 100644 --- a/x-pack/plugins/cases/public/components/files/files_table.tsx +++ b/x-pack/plugins/cases/public/components/files/files_table.tsx @@ -6,35 +6,27 @@ */ import React, { useMemo, useState } from 'react'; -import type { EuiBasicTableColumn, Pagination, EuiBasicTableProps } from '@elastic/eui'; +import type { Pagination, EuiBasicTableProps } from '@elastic/eui'; import type { FileJSON } from '@kbn/shared-ux-file-types'; import { - EuiModalBody, - EuiModalFooter, - EuiButtonIcon, - EuiLink, EuiBasicTable, EuiLoadingContent, EuiSpacer, EuiText, EuiEmptyPrompt, EuiButton, - EuiModal, - EuiModalHeader, - EuiModalHeaderTitle, } from '@elastic/eui'; import { useFilesContext } from '@kbn/shared-ux-file-context'; -import { FileImage } from '@kbn/shared-ux-file-image'; import * as i18n from './translations'; -import { APP_ID } from '../../../common'; -import { CASES_FILE_KINDS } from '../../files'; +import { useFilesTableColumns } from './use_files_table_columns'; +import { FilePreviewModal } from './file_preview_modal'; const EmptyFilesTable = () => ( {'No attachments available'}} - data-test-subj="files-table-empty" + title={

{i18n.NO_FILES}

} + data-test-subj="cases-files-table-empty" titleSize="xs" actions={ > = [ - { - name: i18n.NAME, - 'data-test-subj': 'files-table-filename', - render: (attachment: FileJSON) => ( - showModal(attachment)}>{attachment.name} - ), - width: '60%', - }, - { - name: i18n.TYPE, - field: 'mimeType', - 'data-test-subj': 'files-table-filetype', - }, - { - name: i18n.DATE_ADDED, - field: 'created', - 'data-test-subj': 'files-table-date-added', - dataType: 'date', - }, - { - name: i18n.ACTIONS, - width: '120px', - actions: [ - { - name: 'Download', - isPrimary: true, - description: i18n.DOWNLOAD_FILE, - render: (attachment: FileJSON) => { - return ( - - ); - }, - 'data-test-subj': 'files-table-action-download', - }, - { - name: 'Delete', - isPrimary: true, - description: i18n.DELETE_FILE, - color: 'danger', - icon: 'trash', - type: 'icon', - onClick: () => {}, - 'data-test-subj': 'files-table-action-delete', - }, - ], - }, - ]; + const columns = useFilesTableColumns({ showModal, filesClient }); const resultsCount = useMemo( () => ( @@ -139,48 +77,33 @@ export const FilesTable = ({ items, pagination, onChange, isLoading }: FilesTabl ); return isLoading ? ( - + ) : ( <> {pagination.totalItemCount > 0 && ( <> - + {i18n.RESULTS_COUNT} {resultsCount} )} } /> - {isModalVisible && ( - - - {selectedFile?.name} - - - - - - - {'Close'} - - - + {isModalVisible && 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 dfaf8ba984584..f455ebbead017 100644 --- a/x-pack/plugins/cases/public/components/files/translations.tsx +++ b/x-pack/plugins/cases/public/components/files/translations.tsx @@ -7,34 +7,50 @@ import { i18n } from '@kbn/i18n'; +export const ACTIONS = i18n.translate('xpack.cases.caseView.files.actions', { + defaultMessage: 'Actions', +}); + export const ADD_FILE = i18n.translate('xpack.cases.caseView.files.addFile', { defaultMessage: 'Add File', }); -export const DOWNLOAD_FILE = i18n.translate('xpack.cases.caseView.files.downloadFile', { - defaultMessage: 'Download File', +export const CLOSE_MODAL = i18n.translate('xpack.cases.caseView.files.closeModal', { + defaultMessage: 'Close', +}); + +export const DATE_ADDED = i18n.translate('xpack.cases.caseView.files.dateAdded', { + defaultMessage: 'Date Added', }); export const DELETE_FILE = i18n.translate('xpack.cases.caseView.files.deleteFile', { defaultMessage: 'Delete File', }); -export const RESULTS_COUNT = i18n.translate('xpack.cases.caseView.files.resultsCount', { - defaultMessage: 'Showing', +export const DOWNLOAD_FILE = i18n.translate('xpack.cases.caseView.files.downloadFile', { + defaultMessage: 'Download File', +}); + +export const FILES_TABLE = i18n.translate('xpack.cases.caseView.files.filesTable', { + defaultMessage: 'Files table', }); export const NAME = i18n.translate('xpack.cases.caseView.files.name', { defaultMessage: 'Name', }); -export const TYPE = i18n.translate('xpack.cases.caseView.files.type', { - defaultMessage: 'Type', +export const NO_FILES = i18n.translate('xpack.cases.caseView.files.noFilesAvailable', { + defaultMessage: 'No files available', }); -export const DATE_ADDED = i18n.translate('xpack.cases.caseView.files.dateAdded', { - defaultMessage: 'Date Added', +export const NO_PREVIEW = i18n.translate('xpack.cases.caseView.files.noPreviewAvailable', { + defaultMessage: 'No preview available', }); -export const ACTIONS = i18n.translate('xpack.cases.caseView.files.actions', { - defaultMessage: 'Actions', +export const RESULTS_COUNT = i18n.translate('xpack.cases.caseView.files.resultsCount', { + defaultMessage: 'Showing', +}); + +export const TYPE = i18n.translate('xpack.cases.caseView.files.type', { + defaultMessage: 'Type', }); 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 new file mode 100644 index 0000000000000..3495a563fecd2 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/use_files_table_columns.test.tsx @@ -0,0 +1,80 @@ +/* + * 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 { createFileClientMock } from '@kbn/files-plugin/server/mocks'; +import type { BaseFilesClient } from '@kbn/shared-ux-file-types'; + +import type { FilesTableColumnsProps } from './use_files_table_columns'; +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'; + +describe('useCasesColumns ', () => { + let appMockRender: AppMockRenderer; + + const useCasesColumnsProps: FilesTableColumnsProps = { + showModal: () => {}, + filesClient: createFileClientMock() as unknown as BaseFilesClient, + }; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('return all files table columns correctly', async () => { + const { result } = renderHook(() => useFilesTableColumns(useCasesColumnsProps), { + wrapper: appMockRender.AppWrapper, + }); + + expect(result.current).toMatchInlineSnapshot(` + Array [ + Object { + "data-test-subj": "cases-files-table-filename", + "name": "Name", + "render": [Function], + "width": "60%", + }, + Object { + "data-test-subj": "cases-files-table-filetype", + "field": "mimeType", + "name": "Type", + }, + Object { + "data-test-subj": "cases-files-table-date-added", + "dataType": "date", + "field": "created", + "name": "Date Added", + }, + Object { + "actions": Array [ + Object { + "data-test-subj": "cases-files-table-action-download", + "description": "Download File", + "isPrimary": true, + "name": "Download", + "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", + }, + ], + "name": "Actions", + "width": "120px", + }, + ] + `); + }); +}); 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 new file mode 100644 index 0000000000000..69993261d6336 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/use_files_table_columns.tsx @@ -0,0 +1,88 @@ +/* + * 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 { EuiBasicTableColumn } from '@elastic/eui'; +import type { BaseFilesClient, FileJSON } from '@kbn/shared-ux-file-types'; + +import { EuiLink, EuiButtonIcon } from '@elastic/eui'; + +import { APP_ID } from '../../../common'; +import { CASES_FILE_KINDS } from '../../files'; +import * as i18n from './translations'; +import { isImage } from './utils'; + +export interface FilesTableColumnsProps { + showModal: (file: FileJSON) => void; + filesClient: BaseFilesClient; +} + +export const useFilesTableColumns = ({ + showModal, + filesClient, +}: FilesTableColumnsProps): Array> => { + return [ + { + name: i18n.NAME, + 'data-test-subj': 'cases-files-table-filename', + render: (attachment: FileJSON) => { + if (isImage(attachment)) { + return showModal(attachment)}>{attachment.name}; + } else { + return {attachment.name}; + } + }, + width: '60%', + }, + { + name: i18n.TYPE, + field: 'mimeType', + 'data-test-subj': 'cases-files-table-filetype', + }, + { + name: i18n.DATE_ADDED, + field: 'created', + 'data-test-subj': 'cases-files-table-date-added', + dataType: 'date', + }, + { + name: i18n.ACTIONS, + width: '120px', + actions: [ + { + name: 'Download', + isPrimary: true, + description: i18n.DOWNLOAD_FILE, + render: (attachment: FileJSON) => { + return ( + + ); + }, + 'data-test-subj': 'cases-files-table-action-download', + }, + { + name: 'Delete', + isPrimary: true, + description: i18n.DELETE_FILE, + color: 'danger', + icon: 'trash', + type: 'icon', + onClick: () => {}, + 'data-test-subj': 'cases-files-table-action-delete', + }, + ], + }, + ]; +}; diff --git a/x-pack/plugins/cases/public/components/files/utils.tsx b/x-pack/plugins/cases/public/components/files/utils.tsx new file mode 100644 index 0000000000000..9a6a68cf6967e --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/utils.tsx @@ -0,0 +1,10 @@ +/* + * 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 { FileJSON } from '@kbn/shared-ux-file-types'; + +export const isImage = (file: FileJSON) => file.mimeType?.startsWith('image/'); 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 54401382e03c2..4fd14f5c2c196 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 @@ -21,6 +21,7 @@ import * as i18n from './translations'; export interface GetCaseFilesParams { caseId: string; + owner: string; page: number; perPage: number; searchTerm?: string; @@ -29,6 +30,7 @@ export interface GetCaseFilesParams { export const useGetCaseFiles = ({ caseId, page, + owner, perPage, searchTerm, }: GetCaseFilesParams): UseQueryResult<{ files: FileJSON[]; total: number }> => { @@ -36,14 +38,14 @@ export const useGetCaseFiles = ({ const { client: filesClient } = useFilesContext(); return useQuery( - casesQueriesKeys.caseFiles({ caseId, page, perPage, searchTerm }), + casesQueriesKeys.caseFiles({ caseId, page, perPage, searchTerm, owner }), () => { return filesClient.list({ kind: CASES_FILE_KINDS[APP_ID].id, page: page + 1, ...(searchTerm && { name: `*${searchTerm}*` }), perPage, - meta: { caseId }, + meta: { caseId, owner }, }); }, { From c2a4499b98a3d291308af536e3e683d16c94ebd8 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Mon, 20 Mar 2023 18:20:20 +0100 Subject: [PATCH 09/23] Fix type error on observability plugin. Add the FilesContext to the TestProviders. Update translations. Fix type check errors. Fix jest tests. --- .../ui/get_all_cases_selector_modal.tsx | 4 +- .../public/common/mock/test_providers.tsx | 4 +- .../components/add_file/translations.ts | 3 +- .../components/case_view_files.test.tsx | 35 +++++++---------- .../case_view/components/case_view_files.tsx | 11 ++++-- .../components/case_view/translations.ts | 4 ++ .../components/files/files_table.test.tsx | 39 ++++++++++++------- .../public/components/files/files_table.tsx | 8 +--- .../files/use_files_table_columns.test.tsx | 6 +-- .../files/use_files_table_columns.tsx | 10 ++--- .../plugins/cases/public/containers/mock.ts | 2 +- 11 files changed, 68 insertions(+), 58 deletions(-) diff --git a/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx b/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx index 79822bdf07c45..fc85e84639baa 100644 --- a/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx +++ b/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx @@ -14,7 +14,9 @@ import { CasesProvider } from '../../components/cases_context'; type GetAllCasesSelectorModalPropsInternal = AllCasesSelectorModalProps & CasesContextProps; export type GetAllCasesSelectorModalProps = Omit< GetAllCasesSelectorModalPropsInternal, - 'externalReferenceAttachmentTypeRegistry' | 'persistableStateAttachmentTypeRegistry' + | 'externalReferenceAttachmentTypeRegistry' + | 'persistableStateAttachmentTypeRegistry' + | 'getFilesClient' >; const AllCasesSelectorModalLazy: React.FC = lazy( diff --git a/x-pack/plugins/cases/public/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx index 832c0571e4f77..28eb6058a7412 100644 --- a/x-pack/plugins/cases/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx @@ -18,10 +18,12 @@ import type { ScopedFilesClient } from '@kbn/files-plugin/public'; import { euiDarkVars } from '@kbn/ui-theme'; import { I18nProvider } from '@kbn/i18n-react'; +import { createMockFilesClient } from '@kbn/shared-ux-file-mocks'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render as reactRender } from '@testing-library/react'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { FilesContext } from '@kbn/shared-ux-file-context'; import type { CasesFeatures, CasesPermissions } from '../../../common/ui/types'; import type { StartServices } from '../../types'; @@ -94,7 +96,7 @@ const TestProvidersComponent: React.FC = ({ getFilesClient, }} > - {children} + {children} diff --git a/x-pack/plugins/cases/public/components/add_file/translations.ts b/x-pack/plugins/cases/public/components/add_file/translations.ts index 4c92cc30db44d..8a6f5d398ecf4 100644 --- a/x-pack/plugins/cases/public/components/add_file/translations.ts +++ b/x-pack/plugins/cases/public/components/add_file/translations.ts @@ -21,5 +21,6 @@ export const SUCCESSFUL_UPLOAD = i18n.translate('xpack.cases.caseView.successful export const SUCCESSFUL_UPLOAD_FILE_NAME = (fileName: string) => i18n.translate('xpack.cases.caseView.successfulUpload', { - defaultMessage: `File ${fileName} uploaded successfully`, + values: { fileName }, + defaultMessage: 'File {fileName} uploaded successfully', }); diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx index b2901643a02a4..cbea8af2be688 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { alertCommentWithIndices, basicAttachment, basicCase } from '../../../containers/mock'; import type { AppMockRenderer } from '../../../common/mock'; -import { createAppMockRenderer } from '../../../common/mock'; +import { createAppMockRenderer, TestProviders } from '../../../common/mock'; import type { Case } from '../../../../common'; import { CaseViewFiles } from './case_view_files'; import { useGetCaseFiles } from '../../../containers/use_get_case_files'; @@ -42,32 +42,23 @@ describe('Case View Page files tab', () => { }); it('should render the utility bar for the files table', async () => { - const result = appMockRender.render(); + const result = appMockRender.render( + + + + ); - expect(await result.findByTestId('case-detail-upload-file')).toBeInTheDocument(); + expect(await result.findByTestId('cases-add-file')).toBeInTheDocument(); expect(await result.findByTestId('case-detail-search-file')).toBeInTheDocument(); - expect(await result.findByTestId('case-detail-select-file-type')).toBeInTheDocument(); }); it('should render the files table', async () => { - const result = appMockRender.render(); + const result = appMockRender.render( + + + + ); - expect(await result.findByTestId('attachments-table')).toBeInTheDocument(); - }); - - it('should disable search and filter if there are no attachments', async () => { - useGetCaseFilesMock.mockReturnValue({ - data: { - pageOfItems: [], - availableTypes: [], - totalItemCount: 0, - }, - isLoading: false, - }); - - const result = appMockRender.render(); - - expect(await result.findByTestId('case-detail-search-file')).toHaveAttribute('disabled'); - expect(await result.findByTestId('case-detail-select-file-type')).toHaveAttribute('disabled'); + expect(await result.findByTestId('cases-files-table')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx index e06ebf8a26cd4..cba7de2c85f66 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx @@ -6,6 +6,7 @@ */ import { isEqual } from 'lodash/fp'; import React, { useCallback, useMemo, useState } from 'react'; +import styled from 'styled-components'; import type { Criteria } from '@elastic/eui'; import type { FileJSON } from '@kbn/shared-ux-file-types'; @@ -23,6 +24,11 @@ import { AddFile } from '../../add_file'; import { useCasesContext } from '../../cases_context/use_cases_context'; import { FilesTable } from '../../files/files_table'; import { CaseViewTabs } from '../case_view_tabs'; +import * as i18n from '../translations'; + +const HiddenButtonGroup = styled(EuiButtonGroup)` + display: none; +`; interface CaseViewFilesProps { caseData: Case; @@ -112,7 +118,7 @@ export const CaseViewFiles = ({ caseData }: CaseViewFilesProps) => { { - {}} - css={'display:none'} isIconOnly /> diff --git a/x-pack/plugins/cases/public/components/case_view/translations.ts b/x-pack/plugins/cases/public/components/case_view/translations.ts index 8fc80c1a0aba3..22eb5bb63ba68 100644 --- a/x-pack/plugins/cases/public/components/case_view/translations.ts +++ b/x-pack/plugins/cases/public/components/case_view/translations.ts @@ -205,6 +205,10 @@ export const ASSIGN_YOURSELF = i18n.translate('xpack.cases.caseView.assignYourse defaultMessage: 'assign yourself', }); +export const SEARCH_PLACEHOLDER = i18n.translate('xpack.cases.caseViewFiles.searchPlaceholder', { + defaultMessage: 'Search', +}); + export const TOTAL_USERS_ASSIGNED = (total: number) => i18n.translate('xpack.cases.caseView.totalUsersAssigned', { defaultMessage: '{total} assigned', 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 6c8368a608e3d..dc7cf0cedcc4f 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 @@ -10,7 +10,7 @@ import { screen } from '@testing-library/react'; import { basicAttachment } from '../../containers/mock'; import type { AppMockRenderer } from '../../common/mock'; -import { createAppMockRenderer } from '../../common/mock'; +import { createAppMockRenderer, TestProviders } from '../../common/mock'; import { FilesTable } from './files_table'; const defaultProps = { @@ -29,25 +29,38 @@ describe('FilesTable', () => { }); it('renders correctly', async () => { - appMockRender.render(); - - expect(await screen.findByTestId('files-table-results-count')).toBeInTheDocument(); - expect(await screen.findByTestId('files-table-filename')).toBeInTheDocument(); - expect(await screen.findByTestId('files-table-filetype')).toBeInTheDocument(); - expect(await screen.findByTestId('files-table-date-added')).toBeInTheDocument(); - expect(await screen.findByTestId('files-table-action-download')).toBeInTheDocument(); - expect(await screen.findByTestId('files-table-action-delete')).toBeInTheDocument(); + appMockRender.render( + + + + ); + + expect(await screen.findByTestId('cases-files-table-results-count')).toBeInTheDocument(); + 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-table-action-delete')).toBeInTheDocument(); }); it('renders loading state', async () => { - appMockRender.render(); + appMockRender.render( + + + + ); - expect(await screen.findByTestId('files-table-loading')).toBeInTheDocument(); + expect(await screen.findByTestId('cases-files-table-loading')).toBeInTheDocument(); }); it('renders empty table', async () => { - appMockRender.render(); + appMockRender.render( + + + + ); - expect(await screen.findByTestId('files-table-empty')).toBeInTheDocument(); + expect(await screen.findByTestId('cases-files-table-empty')).toBeInTheDocument(); + expect(await screen.findByTestId('cases-files-table-empty')).toBeInTheDocument(); }); }); 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 4c36af738d681..0034433af6dbc 100644 --- a/x-pack/plugins/cases/public/components/files/files_table.tsx +++ b/x-pack/plugins/cases/public/components/files/files_table.tsx @@ -29,11 +29,7 @@ const EmptyFilesTable = () => ( data-test-subj="cases-files-table-empty" titleSize="xs" actions={ - + {i18n.ADD_FILE} } @@ -60,7 +56,7 @@ export const FilesTable = ({ items, pagination, onChange, isLoading }: FilesTabl setIsModalVisible(true); }; - const columns = useFilesTableColumns({ showModal, filesClient }); + const columns = useFilesTableColumns({ showModal, getDownloadHref: filesClient.getDownloadHref }); const resultsCount = useMemo( () => ( 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 3495a563fecd2..cbd3958336512 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 @@ -5,9 +5,6 @@ * 2.0. */ -import { createFileClientMock } from '@kbn/files-plugin/server/mocks'; -import type { BaseFilesClient } from '@kbn/shared-ux-file-types'; - import type { FilesTableColumnsProps } from './use_files_table_columns'; import { useFilesTableColumns } from './use_files_table_columns'; import type { AppMockRenderer } from '../../common/mock'; @@ -19,7 +16,7 @@ describe('useCasesColumns ', () => { const useCasesColumnsProps: FilesTableColumnsProps = { showModal: () => {}, - filesClient: createFileClientMock() as unknown as BaseFilesClient, + getDownloadHref: jest.fn(), }; beforeEach(() => { @@ -54,7 +51,6 @@ describe('useCasesColumns ', () => { Object { "actions": Array [ Object { - "data-test-subj": "cases-files-table-action-download", "description": "Download File", "isPrimary": true, "name": "Download", 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 69993261d6336..c32af48fccea8 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 @@ -8,7 +8,7 @@ import React from 'react'; import type { EuiBasicTableColumn } from '@elastic/eui'; -import type { BaseFilesClient, FileJSON } from '@kbn/shared-ux-file-types'; +import type { FileJSON } from '@kbn/shared-ux-file-types'; import { EuiLink, EuiButtonIcon } from '@elastic/eui'; @@ -19,12 +19,12 @@ import { isImage } from './utils'; export interface FilesTableColumnsProps { showModal: (file: FileJSON) => void; - filesClient: BaseFilesClient; + getDownloadHref: (args: Pick, 'id' | 'fileKind'>) => string; } export const useFilesTableColumns = ({ showModal, - filesClient, + getDownloadHref, }: FilesTableColumnsProps): Array> => { return [ { @@ -63,14 +63,14 @@ export const useFilesTableColumns = ({ ); }, - 'data-test-subj': 'cases-files-table-action-download', }, { name: 'Delete', diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index 9209f24554e13..bd063832788ec 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -244,7 +244,7 @@ export const basicCase: Case = { export const basicAttachment: FileJSON = { id: '7d47d130-bcec-11ed-afa1-0242ac120002', name: 'my-super-cool-screenshot', - mimeType: 'png', + mimeType: 'image/png', created: basicCreatedAt, updated: basicCreatedAt, size: 999, From b48f33b721e7a8b617ae3c3e8e2d1dd32610f3c5 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 20 Mar 2023 17:26:48 +0000 Subject: [PATCH 10/23] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/cases/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/cases/tsconfig.json b/x-pack/plugins/cases/tsconfig.json index 691dac55872ef..920a0ecb0cf58 100644 --- a/x-pack/plugins/cases/tsconfig.json +++ b/x-pack/plugins/cases/tsconfig.json @@ -61,6 +61,7 @@ "@kbn/shared-ux-file-context", "@kbn/shared-ux-file-image", "@kbn/shared-ux-file-upload", + "@kbn/shared-ux-file-mocks", ], "exclude": [ "target/**/*", From 9df97e3fec7ade18e22e32c01d3168568ee4648c Mon Sep 17 00:00:00 2001 From: adcoelho Date: Mon, 20 Mar 2023 18:50:52 +0100 Subject: [PATCH 11/23] Fixed translation issue --- .../plugins/cases/public/components/add_file/translations.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cases/public/components/add_file/translations.ts b/x-pack/plugins/cases/public/components/add_file/translations.ts index 8a6f5d398ecf4..6400217d54608 100644 --- a/x-pack/plugins/cases/public/components/add_file/translations.ts +++ b/x-pack/plugins/cases/public/components/add_file/translations.ts @@ -20,7 +20,7 @@ export const SUCCESSFUL_UPLOAD = i18n.translate('xpack.cases.caseView.successful }); export const SUCCESSFUL_UPLOAD_FILE_NAME = (fileName: string) => - i18n.translate('xpack.cases.caseView.successfulUpload', { - values: { fileName }, + i18n.translate('xpack.cases.caseView.successfulUploadFileName', { defaultMessage: 'File {fileName} uploaded successfully', + values: { fileName }, }); From 6df706464816675f5f2841d88bd00d3abd9093b7 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Tue, 21 Mar 2023 12:01:08 +0100 Subject: [PATCH 12/23] Created the FilesUtilityBar. Moved AddFile component to the files folder. Removed owner from props in different places. Updated translations. --- .../components/add_file/translations.ts | 26 ---- .../case_view/components/case_view_files.tsx | 64 +-------- .../components/case_view/translations.ts | 4 - .../index.tsx => files/add_file.tsx} | 124 ++++++++++-------- .../components/files/files_utility_bar.tsx | 68 ++++++++++ .../public/components/files/translations.tsx | 18 +++ .../public/containers/use_get_case_files.tsx | 8 +- 7 files changed, 160 insertions(+), 152 deletions(-) delete mode 100644 x-pack/plugins/cases/public/components/add_file/translations.ts rename x-pack/plugins/cases/public/components/{add_file/index.tsx => files/add_file.tsx} (55%) create mode 100644 x-pack/plugins/cases/public/components/files/files_utility_bar.tsx diff --git a/x-pack/plugins/cases/public/components/add_file/translations.ts b/x-pack/plugins/cases/public/components/add_file/translations.ts deleted file mode 100644 index 6400217d54608..0000000000000 --- a/x-pack/plugins/cases/public/components/add_file/translations.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; - -export const ADD_FILE = i18n.translate('xpack.cases.caseView.addFile', { - defaultMessage: 'Add file', -}); - -export const FAILED_UPLOAD = i18n.translate('xpack.cases.caseView.failedUpload', { - defaultMessage: 'Failed to upload file file', -}); - -export const SUCCESSFUL_UPLOAD = i18n.translate('xpack.cases.caseView.successfulUpload', { - defaultMessage: 'File uploaded successfuly!', -}); - -export const SUCCESSFUL_UPLOAD_FILE_NAME = (fileName: string) => - i18n.translate('xpack.cases.caseView.successfulUploadFileName', { - defaultMessage: 'File {fileName} uploaded successfully', - values: { fileName }, - }); diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx index cba7de2c85f66..9897dfde2ff3d 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx @@ -6,42 +6,30 @@ */ import { isEqual } from 'lodash/fp'; import React, { useCallback, useMemo, useState } from 'react'; -import styled from 'styled-components'; import type { Criteria } from '@elastic/eui'; import type { FileJSON } from '@kbn/shared-ux-file-types'; -import { EuiFlexItem, EuiFlexGroup, EuiFieldSearch, EuiButtonGroup } from '@elastic/eui'; -import { useQueryClient } from '@tanstack/react-query'; +import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import type { Case } from '../../../../common/ui/types'; import type { GetCaseFilesParams } from '../../../containers/use_get_case_files'; import { CASE_VIEW_PAGE_TABS } from '../../../../common/types'; -import { casesQueriesKeys } from '../../../containers/constants'; import { useGetCaseFiles } from '../../../containers/use_get_case_files'; -import { AddFile } from '../../add_file'; -import { useCasesContext } from '../../cases_context/use_cases_context'; import { FilesTable } from '../../files/files_table'; import { CaseViewTabs } from '../case_view_tabs'; -import * as i18n from '../translations'; - -const HiddenButtonGroup = styled(EuiButtonGroup)` - display: none; -`; +import { FilesUtilityBar } from '../../files/files_utility_bar'; interface CaseViewFilesProps { caseData: Case; } export const CaseViewFiles = ({ caseData }: CaseViewFilesProps) => { - const queryClient = useQueryClient(); - const { owner } = useCasesContext(); const [filteringOptions, setFilteringOptions] = useState({ page: 0, perPage: 10, caseId: caseData.id, - owner: owner[0], }); const { data: attachmentsData, isLoading } = useGetCaseFiles(filteringOptions); @@ -71,10 +59,6 @@ export const CaseViewFiles = ({ caseData }: CaseViewFilesProps) => { [filteringOptions] ); - const refreshAttachmentsTable = useCallback(() => { - queryClient.invalidateQueries(casesQueriesKeys.caseView()); - }, [queryClient]); - const pagination = useMemo( () => ({ pageIndex: filteringOptions.page, @@ -86,55 +70,13 @@ export const CaseViewFiles = ({ caseData }: CaseViewFilesProps) => { [filteringOptions.page, filteringOptions.perPage, attachmentsData] ); - const tableViewSelectedId = 'tableViewSelectedId'; - const toggleButtonsIcons = [ - { - id: 'thumbnailViewSelectedId', - label: 'Thumbnail view', - iconType: 'grid', - isDisabled: true, - }, - { - id: tableViewSelectedId, - label: 'Table view', - iconType: 'editorUnorderedList', - }, - ]; - return ( - - - - - - - - - - {}} - isIconOnly - /> - - + i18n.translate('xpack.cases.caseView.totalUsersAssigned', { defaultMessage: '{total} assigned', diff --git a/x-pack/plugins/cases/public/components/add_file/index.tsx b/x-pack/plugins/cases/public/components/files/add_file.tsx similarity index 55% rename from x-pack/plugins/cases/public/components/add_file/index.tsx rename to x-pack/plugins/cases/public/components/files/add_file.tsx index f83c696747ff2..831385b6994e3 100644 --- a/x-pack/plugins/cases/public/components/add_file/index.tsx +++ b/x-pack/plugins/cases/public/components/files/add_file.tsx @@ -19,23 +19,26 @@ import type { UploadedFile } from '@kbn/shared-ux-file-upload/src/file_upload'; import { FILE_SO_TYPE } from '@kbn/files-plugin/common'; import { FileUpload } from '@kbn/shared-ux-file-upload'; import { useFilesContext } from '@kbn/shared-ux-file-context'; +import { useQueryClient } from '@tanstack/react-query'; import { APP_ID, CommentType, ExternalReferenceStorageType } from '../../../common'; import { FILE_ATTACHMENT_TYPE } from '../../../common/api'; -import { CASES_FILE_KINDS } from '../../files'; import { useKibana } from '../../common/lib/kibana'; +import { casesQueriesKeys } from '../../containers/constants'; import { useCreateAttachments } from '../../containers/use_create_attachments'; +import { CASES_FILE_KINDS } from '../../files'; +import { useCasesContext } from '../cases_context/use_cases_context'; import * as i18n from './translations'; interface AddFileProps { caseId: string; - onFileAdded: () => void; - owner: string; } -const AddFileComponent: React.FC = ({ caseId, onFileAdded, owner }) => { - const { notifications } = useKibana().services; +const AddFileComponent: React.FC = ({ caseId }) => { + const { owner } = useCasesContext(); const { client: filesClient } = useFilesContext(); + const { notifications } = useKibana().services; + const queryClient = useQueryClient(); const { isLoading, createAttachments } = useCreateAttachments(); const [isModalVisible, setIsModalVisible] = useState(false); @@ -43,57 +46,9 @@ const AddFileComponent: React.FC = ({ caseId, onFileAdded, owner } const closeModal = () => setIsModalVisible(false); const showModal = () => setIsModalVisible(true); - const onUploadDone = useCallback( - async (chosenFiles: UploadedFile[]) => { - const file = chosenFiles[0]; - - try { - await createAttachments({ - caseId, - caseOwner: owner, - data: [ - { - type: CommentType.externalReference, - externalReferenceId: file.id, - externalReferenceStorage: { - type: ExternalReferenceStorageType.savedObject, - soType: FILE_SO_TYPE, - }, - externalReferenceAttachmentTypeId: FILE_ATTACHMENT_TYPE, - externalReferenceMetadata: { - files: [ - { - name: file.fileJSON.name, - extension: file.fileJSON.extension ?? '', - mimeType: file.fileJSON.mimeType ?? '', - createdAt: file.fileJSON.created, - }, - ], - }, - }, - ], - updateCase: onFileAdded, - throwOnError: true, - }); - - notifications.toasts.addSuccess({ - title: i18n.SUCCESSFUL_UPLOAD, - text: i18n.SUCCESSFUL_UPLOAD_FILE_NAME(file.fileJSON.name), - }); - - // used to refresh the attachments table - onFileAdded(); - } catch (error) { - // error toast is handled inside createAttachments - - // we need to delete the file here - await filesClient.delete({ kind: CASES_FILE_KINDS[APP_ID].id, id: file.id }); - } - - closeModal(); - }, - [caseId, createAttachments, filesClient, notifications.toasts, onFileAdded, owner] - ); + const refreshAttachmentsTable = useCallback(() => { + queryClient.invalidateQueries(casesQueriesKeys.caseView()); + }, [queryClient]); const onError = useCallback( (error) => { @@ -104,6 +59,61 @@ const AddFileComponent: React.FC = ({ caseId, onFileAdded, owner } [notifications.toasts] ); + const onUploadDone = useCallback( + async (chosenFiles: UploadedFile[]) => { + if (chosenFiles.length === 0) { + notifications.toasts.addDanger({ + title: i18n.FAILED_UPLOAD, + }); + } else { + const file = chosenFiles[0]; + + try { + await createAttachments({ + caseId, + caseOwner: owner[0], + data: [ + { + type: CommentType.externalReference, + externalReferenceId: file.id, + externalReferenceStorage: { + type: ExternalReferenceStorageType.savedObject, + soType: FILE_SO_TYPE, + }, + externalReferenceAttachmentTypeId: FILE_ATTACHMENT_TYPE, + externalReferenceMetadata: { + files: [ + { + name: file.fileJSON.name, + extension: file.fileJSON.extension ?? '', + mimeType: file.fileJSON.mimeType ?? '', + createdAt: file.fileJSON.created, + }, + ], + }, + }, + ], + updateCase: refreshAttachmentsTable, + throwOnError: true, + }); + + notifications.toasts.addSuccess({ + title: i18n.SUCCESSFUL_UPLOAD, + text: i18n.SUCCESSFUL_UPLOAD_FILE_NAME(file.fileJSON.name), + }); + } catch (error) { + // error toast is handled inside createAttachments + + // we need to delete the file if attachment creation failed + await filesClient.delete({ kind: CASES_FILE_KINDS[APP_ID].id, id: file.id }); + } + } + + closeModal(); + }, + [caseId, createAttachments, filesClient, notifications.toasts, refreshAttachmentsTable, owner] + ); + return ( <> = ({ caseId, onFileAdded, owner } kind={CASES_FILE_KINDS[APP_ID].id} onDone={onUploadDone} onError={onError} - meta={{ caseId, owner }} + meta={{ caseId, owner: owner[0] }} />
diff --git a/x-pack/plugins/cases/public/components/files/files_utility_bar.tsx b/x-pack/plugins/cases/public/components/files/files_utility_bar.tsx new file mode 100644 index 0000000000000..3660f51a7108f --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/files_utility_bar.tsx @@ -0,0 +1,68 @@ +/* + * 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 styled from 'styled-components'; + +import { EuiButtonGroup, EuiFlexGroup, EuiFlexItem, EuiFieldSearch } from '@elastic/eui'; +import { AddFile } from './add_file'; + +import * as i18n from './translations'; + +interface FilesUtilityBarProps { + caseId: string; + onSearch: (newSearch: string) => void; +} + +const HiddenButtonGroup = styled(EuiButtonGroup)` + display: none; +`; + +const tableViewSelectedId = 'tableViewSelectedId'; +const toggleButtonsIcons = [ + { + id: 'thumbnailViewSelectedId', + label: 'Thumbnail view', + iconType: 'grid', + isDisabled: true, + }, + { + id: tableViewSelectedId, + label: 'Table view', + iconType: 'editorUnorderedList', + }, +]; + +export const FilesUtilityBar = ({ caseId, onSearch }: FilesUtilityBarProps) => { + return ( + + + + + + + + + + {}} + isIconOnly + /> + + + ); +}; + +FilesUtilityBar.displayName = 'FilesUtilityBar'; diff --git a/x-pack/plugins/cases/public/components/files/translations.tsx b/x-pack/plugins/cases/public/components/files/translations.tsx index f455ebbead017..8a11a3b40f560 100644 --- a/x-pack/plugins/cases/public/components/files/translations.tsx +++ b/x-pack/plugins/cases/public/components/files/translations.tsx @@ -54,3 +54,21 @@ export const RESULTS_COUNT = i18n.translate('xpack.cases.caseView.files.resultsC export const TYPE = i18n.translate('xpack.cases.caseView.files.type', { defaultMessage: 'Type', }); + +export const SEARCH_PLACEHOLDER = i18n.translate('xpack.cases.caseViewFiles.searchPlaceholder', { + defaultMessage: 'Search files', +}); + +export const FAILED_UPLOAD = i18n.translate('xpack.cases.caseView.failedUpload', { + defaultMessage: 'Failed to upload file', +}); + +export const SUCCESSFUL_UPLOAD = i18n.translate('xpack.cases.caseView.successfulUpload', { + defaultMessage: 'File uploaded successfuly!', +}); + +export const SUCCESSFUL_UPLOAD_FILE_NAME = (fileName: string) => + i18n.translate('xpack.cases.caseView.successfulUploadFileName', { + defaultMessage: 'File {fileName} uploaded successfully', + values: { fileName }, + }); 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 4fd14f5c2c196..385bb8248cc3c 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 @@ -15,13 +15,13 @@ import type { ServerError } from '../types'; import { APP_ID } from '../../common'; import { useCasesToast } from '../common/use_cases_toast'; +import { useCasesContext } from '../components/cases_context/use_cases_context'; import { CASES_FILE_KINDS } from '../files'; import { casesQueriesKeys } from './constants'; import * as i18n from './translations'; export interface GetCaseFilesParams { caseId: string; - owner: string; page: number; perPage: number; searchTerm?: string; @@ -30,22 +30,22 @@ export interface GetCaseFilesParams { export const useGetCaseFiles = ({ caseId, page, - owner, perPage, searchTerm, }: GetCaseFilesParams): UseQueryResult<{ files: FileJSON[]; total: number }> => { + const { owner } = useCasesContext(); const { showErrorToast } = useCasesToast(); const { client: filesClient } = useFilesContext(); return useQuery( - casesQueriesKeys.caseFiles({ caseId, page, perPage, searchTerm, owner }), + casesQueriesKeys.caseFiles({ caseId, page, perPage, searchTerm, owner: owner[0] }), () => { return filesClient.list({ kind: CASES_FILE_KINDS[APP_ID].id, page: page + 1, ...(searchTerm && { name: `*${searchTerm}*` }), perPage, - meta: { caseId, owner }, + meta: { caseId, owner: owner[0] }, }); }, { From 32434d0670e8ec52b6811cf18b045a6756c7bd97 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Wed, 22 Mar 2023 16:43:27 +0100 Subject: [PATCH 13/23] Updated mock in test providers. FilePreview is no longer a modal. --- .../public/common/mock/test_providers.tsx | 4 +- .../public/components/files/file_preview.tsx | 50 +++++++++++++++ .../components/files/file_preview_modal.tsx | 63 ------------------- .../public/components/files/files_table.tsx | 21 ++++--- .../files/use_files_table_columns.test.tsx | 2 +- .../files/use_files_table_columns.tsx | 6 +- 6 files changed, 69 insertions(+), 77 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/files/file_preview.tsx delete mode 100644 x-pack/plugins/cases/public/components/files/file_preview_modal.tsx diff --git a/x-pack/plugins/cases/public/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx index 28eb6058a7412..6141e4fdcf40d 100644 --- a/x-pack/plugins/cases/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx @@ -51,7 +51,9 @@ type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResul window.scrollTo = jest.fn(); -const mockGetFilesClient = jest.fn(); +const mockGetFilesClient = createMockFilesClient as unknown as ( + scope: string +) => ScopedFilesClient; /** A utility for wrapping children in the providers required to run most tests */ const TestProvidersComponent: React.FC = ({ diff --git a/x-pack/plugins/cases/public/components/files/file_preview.tsx b/x-pack/plugins/cases/public/components/files/file_preview.tsx new file mode 100644 index 0000000000000..72aaf621ed047 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_preview.tsx @@ -0,0 +1,50 @@ +/* + * 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 styled from 'styled-components'; + +import type { FileJSON } from '@kbn/shared-ux-file-types'; + +import { EuiOverlayMask, EuiFocusTrap, EuiImage } from '@elastic/eui'; + +import { APP_ID } from '../../../common'; +import { CASES_FILE_KINDS } from '../../files'; + +interface FilePreviewProps { + closePreview: () => void; + getDownloadHref: (args: Pick, 'id' | 'fileKind'>) => string; + selectedFile: FileJSON; +} + +const StyledOverlayMask = styled(EuiOverlayMask)` + padding-block-end: 0vh !important; + + img { + max-height: 85vh; + max-width: 85vw; + object-fit: contain; + } +`; + +export const FilePreview = ({ closePreview, selectedFile, getDownloadHref }: FilePreviewProps) => { + return ( + + + + + + ); +}; + +FilePreview.displayName = 'FilePreview'; diff --git a/x-pack/plugins/cases/public/components/files/file_preview_modal.tsx b/x-pack/plugins/cases/public/components/files/file_preview_modal.tsx deleted file mode 100644 index e9413d4559613..0000000000000 --- a/x-pack/plugins/cases/public/components/files/file_preview_modal.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 { - EuiModalBody, - EuiModalFooter, - EuiButton, - EuiModal, - EuiModalHeader, - EuiModalHeaderTitle, -} from '@elastic/eui'; -import { FileImage } from '@kbn/shared-ux-file-image'; - -import { APP_ID } from '../../../common'; -import { CASES_FILE_KINDS } from '../../files'; -import * as i18n from './translations'; -import { isImage } from './utils'; - -interface FilePreviewModalProps { - closeModal: () => void; - getDownloadHref: (args: Pick, 'id' | 'fileKind'>) => string; - selectedFile: FileJSON; -} - -export const FilePreviewModal = ({ - closeModal, - selectedFile, - getDownloadHref, -}: FilePreviewModalProps) => { - return ( - - - {selectedFile?.name} - - - {isImage(selectedFile) && ( - - )} - - - - {i18n.CLOSE_MODAL} - - - - ); -}; - -FilePreviewModal.displayName = 'FilePreviewModal'; 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 0034433af6dbc..5f32684699cb2 100644 --- a/x-pack/plugins/cases/public/components/files/files_table.tsx +++ b/x-pack/plugins/cases/public/components/files/files_table.tsx @@ -21,7 +21,7 @@ import { useFilesContext } from '@kbn/shared-ux-file-context'; import * as i18n from './translations'; import { useFilesTableColumns } from './use_files_table_columns'; -import { FilePreviewModal } from './file_preview_modal'; +import { FilePreview } from './file_preview'; const EmptyFilesTable = () => ( { const { client: filesClient } = useFilesContext(); - const [isModalVisible, setIsModalVisible] = useState(false); + const [isPreviewVisible, setIsPreviewVisible] = useState(false); const [selectedFile, setSelectedFile] = useState(); - const closeModal = () => setIsModalVisible(false); - const showModal = (file: FileJSON) => { + const closePreview = () => setIsPreviewVisible(false); + const showPreview = (file: FileJSON) => { setSelectedFile(file); - setIsModalVisible(true); + setIsPreviewVisible(true); }; - const columns = useFilesTableColumns({ showModal, getDownloadHref: filesClient.getDownloadHref }); + const columns = useFilesTableColumns({ + showPreview, + getDownloadHref: filesClient.getDownloadHref, + }); const resultsCount = useMemo( () => ( @@ -94,9 +97,9 @@ export const FilesTable = ({ items, pagination, onChange, isLoading }: FilesTabl data-test-subj="cases-files-table" noItemsMessage={} /> - {isModalVisible && selectedFile !== undefined && ( - 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 cbd3958336512..c1d438369bd90 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 @@ -15,7 +15,7 @@ describe('useCasesColumns ', () => { let appMockRender: AppMockRenderer; const useCasesColumnsProps: FilesTableColumnsProps = { - showModal: () => {}, + showPreview: () => {}, getDownloadHref: jest.fn(), }; 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 c32af48fccea8..621077d0719ee 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 @@ -18,12 +18,12 @@ import * as i18n from './translations'; import { isImage } from './utils'; export interface FilesTableColumnsProps { - showModal: (file: FileJSON) => void; + showPreview: (file: FileJSON) => void; getDownloadHref: (args: Pick, 'id' | 'fileKind'>) => string; } export const useFilesTableColumns = ({ - showModal, + showPreview, getDownloadHref, }: FilesTableColumnsProps): Array> => { return [ @@ -32,7 +32,7 @@ export const useFilesTableColumns = ({ 'data-test-subj': 'cases-files-table-filename', render: (attachment: FileJSON) => { if (isImage(attachment)) { - return showModal(attachment)}>{attachment.name}; + return showPreview(attachment)}>{attachment.name}; } else { return {attachment.name}; } From 714d5011d7b887d1099f03434def975b56075df9 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 22 Mar 2023 15:51:42 +0000 Subject: [PATCH 14/23] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/cases/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/cases/tsconfig.json b/x-pack/plugins/cases/tsconfig.json index 920a0ecb0cf58..4f4d4f0e43c9d 100644 --- a/x-pack/plugins/cases/tsconfig.json +++ b/x-pack/plugins/cases/tsconfig.json @@ -59,7 +59,6 @@ "@kbn/files-plugin", "@kbn/shared-ux-file-types", "@kbn/shared-ux-file-context", - "@kbn/shared-ux-file-image", "@kbn/shared-ux-file-upload", "@kbn/shared-ux-file-mocks", ], From 1c03756498802ca007b6faae80ee82e980f8b7f0 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Thu, 23 Mar 2023 14:56:07 +0100 Subject: [PATCH 15/23] Include showDanger in useCasesToast. Cleanup case_view_files tests. Move caseId out of filteringOptions. Add type guard for owner in cases context. Use useCasesToast in add_file component. --- .../cases/public/common/use_cases_toast.tsx | 3 + .../components/case_view_files.test.tsx | 22 +--- .../case_view/components/case_view_files.tsx | 16 +-- .../public/components/cases_context/index.tsx | 7 +- .../public/components/files/add_file.tsx | 111 +++++++++--------- .../components/files/files_table.test.tsx | 4 +- .../public/components/files/translations.tsx | 4 - .../plugins/cases/public/containers/mock.ts | 2 +- .../public/containers/use_get_case_files.tsx | 7 +- 9 files changed, 85 insertions(+), 91 deletions(-) diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.tsx index 3d90005546464..70ad598e863e9 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.tsx @@ -169,6 +169,9 @@ export const useCasesToast = () => { showSuccessToast: (title: string) => { toasts.addSuccess({ title, className: 'eui-textBreakWord' }); }, + showDangerToast: (title: string) => { + toasts.addDanger({ title, className: 'eui-textBreakWord' }); + }, }; }; diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx index cbea8af2be688..66fe926d2754a 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx @@ -6,9 +6,9 @@ */ import React from 'react'; -import { alertCommentWithIndices, basicAttachment, basicCase } from '../../../containers/mock'; +import { alertCommentWithIndices, basicCase } from '../../../containers/mock'; import type { AppMockRenderer } from '../../../common/mock'; -import { createAppMockRenderer, TestProviders } from '../../../common/mock'; +import { createAppMockRenderer } from '../../../common/mock'; import type { Case } from '../../../../common'; import { CaseViewFiles } from './case_view_files'; import { useGetCaseFiles } from '../../../containers/use_get_case_files'; @@ -25,11 +25,7 @@ const caseData: Case = { describe('Case View Page files tab', () => { let appMockRender: AppMockRenderer; useGetCaseFilesMock.mockReturnValue({ - data: { - pageOfItems: [basicAttachment], - availableTypes: [basicAttachment.mimeType], - totalItemCount: 1, - }, + data: {}, isLoading: false, }); @@ -42,22 +38,14 @@ describe('Case View Page files tab', () => { }); it('should render the utility bar for the files table', async () => { - const result = appMockRender.render( - - - - ); + const result = appMockRender.render(); expect(await result.findByTestId('cases-add-file')).toBeInTheDocument(); expect(await result.findByTestId('case-detail-search-file')).toBeInTheDocument(); }); it('should render the files table', async () => { - const result = appMockRender.render( - - - - ); + const result = appMockRender.render(); expect(await result.findByTestId('cases-files-table')).toBeInTheDocument(); }); diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx index 9897dfde2ff3d..acaca10c0bb42 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx @@ -13,7 +13,7 @@ import type { FileJSON } from '@kbn/shared-ux-file-types'; import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import type { Case } from '../../../../common/ui/types'; -import type { GetCaseFilesParams } from '../../../containers/use_get_case_files'; +import type { CaseFilesFilteringOptions } from '../../../containers/use_get_case_files'; import { CASE_VIEW_PAGE_TABS } from '../../../../common/types'; import { useGetCaseFiles } from '../../../containers/use_get_case_files'; @@ -26,12 +26,14 @@ interface CaseViewFilesProps { } export const CaseViewFiles = ({ caseData }: CaseViewFilesProps) => { - const [filteringOptions, setFilteringOptions] = useState({ + const [filteringOptions, setFilteringOptions] = useState({ page: 0, perPage: 10, + }); + const { data: caseFiles, isLoading } = useGetCaseFiles({ + ...filteringOptions, caseId: caseData.id, }); - const { data: attachmentsData, isLoading } = useGetCaseFiles(filteringOptions); const onTableChange = useCallback( ({ page }: Criteria) => { @@ -63,11 +65,11 @@ export const CaseViewFiles = ({ caseData }: CaseViewFilesProps) => { () => ({ pageIndex: filteringOptions.page, pageSize: filteringOptions.perPage, - totalItemCount: attachmentsData?.total ?? 0, + totalItemCount: caseFiles?.total ?? 0, pageSizeOptions: [10, 25, 50], showPerPageOptions: true, }), - [filteringOptions.page, filteringOptions.perPage, attachmentsData] + [filteringOptions.page, filteringOptions.perPage, caseFiles?.total] ); return ( @@ -75,11 +77,11 @@ export const CaseViewFiles = ({ caseData }: CaseViewFilesProps) => { - + 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 eb5d09171e26b..ab22e694eed51 100644 --- a/x-pack/plugins/cases/public/components/cases_context/index.tsx +++ b/x-pack/plugins/cases/public/components/cases_context/index.tsx @@ -131,9 +131,12 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({ return contextChildren; } - if (owner[0] in CASES_FILE_KINDS) { + const isValidOwner = (ownerToCheck: string): ownerToCheck is Owner => + ownerToCheck in CASES_FILE_KINDS; + + if (isValidOwner(owner[0])) { return ( - + {contextChildren} ); 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 831385b6994e3..7d178b29a7dae 100644 --- a/x-pack/plugins/cases/public/components/files/add_file.tsx +++ b/x-pack/plugins/cases/public/components/files/add_file.tsx @@ -19,16 +19,15 @@ import type { UploadedFile } from '@kbn/shared-ux-file-upload/src/file_upload'; import { FILE_SO_TYPE } from '@kbn/files-plugin/common'; import { FileUpload } from '@kbn/shared-ux-file-upload'; import { useFilesContext } from '@kbn/shared-ux-file-context'; -import { useQueryClient } from '@tanstack/react-query'; import { APP_ID, CommentType, ExternalReferenceStorageType } from '../../../common'; import { FILE_ATTACHMENT_TYPE } from '../../../common/api'; -import { useKibana } from '../../common/lib/kibana'; -import { casesQueriesKeys } from '../../containers/constants'; import { useCreateAttachments } from '../../containers/use_create_attachments'; import { CASES_FILE_KINDS } from '../../files'; import { useCasesContext } from '../cases_context/use_cases_context'; import * as i18n from './translations'; +import { useRefreshCaseViewPage } from '../case_view/use_on_refresh_case_view_page'; +import { useCasesToast } from '../../common/use_cases_toast'; interface AddFileProps { caseId: string; @@ -37,81 +36,81 @@ interface AddFileProps { const AddFileComponent: React.FC = ({ caseId }) => { const { owner } = useCasesContext(); const { client: filesClient } = useFilesContext(); - const { notifications } = useKibana().services; - const queryClient = useQueryClient(); + const { showDangerToast, showErrorToast, showSuccessToast } = useCasesToast(); const { isLoading, createAttachments } = useCreateAttachments(); + const refreshAttachmentsTable = useRefreshCaseViewPage(); const [isModalVisible, setIsModalVisible] = useState(false); const closeModal = () => setIsModalVisible(false); const showModal = () => setIsModalVisible(true); - const refreshAttachmentsTable = useCallback(() => { - queryClient.invalidateQueries(casesQueriesKeys.caseView()); - }, [queryClient]); - const onError = useCallback( (error) => { - notifications.toasts.addError(error, { + showErrorToast(error, { title: i18n.FAILED_UPLOAD, }); }, - [notifications.toasts] + [showErrorToast] ); const onUploadDone = useCallback( async (chosenFiles: UploadedFile[]) => { if (chosenFiles.length === 0) { - notifications.toasts.addDanger({ - title: i18n.FAILED_UPLOAD, - }); - } else { - const file = chosenFiles[0]; - - try { - await createAttachments({ - caseId, - caseOwner: owner[0], - data: [ - { - type: CommentType.externalReference, - externalReferenceId: file.id, - externalReferenceStorage: { - type: ExternalReferenceStorageType.savedObject, - soType: FILE_SO_TYPE, - }, - externalReferenceAttachmentTypeId: FILE_ATTACHMENT_TYPE, - externalReferenceMetadata: { - files: [ - { - name: file.fileJSON.name, - extension: file.fileJSON.extension ?? '', - mimeType: file.fileJSON.mimeType ?? '', - createdAt: file.fileJSON.created, - }, - ], - }, + showDangerToast(i18n.FAILED_UPLOAD); + return; + } + + const file = chosenFiles[0]; + + try { + await createAttachments({ + caseId, + caseOwner: owner[0], + data: [ + { + type: CommentType.externalReference, + externalReferenceId: file.id, + externalReferenceStorage: { + type: ExternalReferenceStorageType.savedObject, + soType: FILE_SO_TYPE, }, - ], - updateCase: refreshAttachmentsTable, - throwOnError: true, - }); - - notifications.toasts.addSuccess({ - title: i18n.SUCCESSFUL_UPLOAD, - text: i18n.SUCCESSFUL_UPLOAD_FILE_NAME(file.fileJSON.name), - }); - } catch (error) { - // error toast is handled inside createAttachments - - // we need to delete the file if attachment creation failed - await filesClient.delete({ kind: CASES_FILE_KINDS[APP_ID].id, id: file.id }); - } + externalReferenceAttachmentTypeId: FILE_ATTACHMENT_TYPE, + externalReferenceMetadata: { + files: [ + { + name: file.fileJSON.name, + extension: file.fileJSON.extension ?? '', + mimeType: file.fileJSON.mimeType ?? '', + createdAt: file.fileJSON.created, + }, + ], + }, + }, + ], + updateCase: refreshAttachmentsTable, + throwOnError: true, + }); + + showSuccessToast(i18n.SUCCESSFUL_UPLOAD_FILE_NAME(file.fileJSON.name)); + } catch (error) { + // error toast is handled inside createAttachments + + // we need to delete the file if attachment creation failed + await filesClient.delete({ kind: CASES_FILE_KINDS[APP_ID].id, id: file.id }); } closeModal(); }, - [caseId, createAttachments, filesClient, notifications.toasts, refreshAttachmentsTable, owner] + [ + caseId, + createAttachments, + filesClient, + owner, + refreshAttachmentsTable, + showDangerToast, + showSuccessToast, + ] ); return ( 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 dc7cf0cedcc4f..a74f471c350a4 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 @@ -8,13 +8,13 @@ import React from 'react'; import { screen } from '@testing-library/react'; -import { basicAttachment } from '../../containers/mock'; +import { basicFileMock } from '../../containers/mock'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer, TestProviders } from '../../common/mock'; import { FilesTable } from './files_table'; const defaultProps = { - items: [basicAttachment], + items: [basicFileMock], pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 1 }, onChange: jest.fn(), isLoading: false, diff --git a/x-pack/plugins/cases/public/components/files/translations.tsx b/x-pack/plugins/cases/public/components/files/translations.tsx index 8a11a3b40f560..da62c5444d69a 100644 --- a/x-pack/plugins/cases/public/components/files/translations.tsx +++ b/x-pack/plugins/cases/public/components/files/translations.tsx @@ -63,10 +63,6 @@ export const FAILED_UPLOAD = i18n.translate('xpack.cases.caseView.failedUpload', defaultMessage: 'Failed to upload file', }); -export const SUCCESSFUL_UPLOAD = i18n.translate('xpack.cases.caseView.successfulUpload', { - defaultMessage: 'File uploaded successfuly!', -}); - export const SUCCESSFUL_UPLOAD_FILE_NAME = (fileName: string) => i18n.translate('xpack.cases.caseView.successfulUploadFileName', { defaultMessage: 'File {fileName} uploaded successfully', diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index bd063832788ec..5d5960043efb2 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -241,7 +241,7 @@ export const basicCase: Case = { assignees: [{ uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0' }], }; -export const basicAttachment: FileJSON = { +export const basicFileMock: FileJSON = { id: '7d47d130-bcec-11ed-afa1-0242ac120002', name: 'my-super-cool-screenshot', mimeType: 'image/png', 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 385bb8248cc3c..1a834638d2e30 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 @@ -20,13 +20,16 @@ import { CASES_FILE_KINDS } from '../files'; import { casesQueriesKeys } from './constants'; import * as i18n from './translations'; -export interface GetCaseFilesParams { - caseId: string; +export interface CaseFilesFilteringOptions { page: number; perPage: number; searchTerm?: string; } +export interface GetCaseFilesParams extends CaseFilesFilteringOptions { + caseId: string; +} + export const useGetCaseFiles = ({ caseId, page, From af1f15f159059f70b4d110b66b8a799f612b8d92 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Thu, 23 Mar 2023 15:41:38 +0100 Subject: [PATCH 16/23] Fix AddFile button in empty FilesTable. Add spacer for loading FilesTable. Use file name as "alt" in FilePreview. Remove HiddenButtonGroup from utility bar. Improve query keys for caseFiles. Remove owner from useGetCaseFiles. --- .../case_view/components/case_view_files.tsx | 1 + .../public/components/files/file_preview.tsx | 4 +-- .../components/files/files_table.test.tsx | 1 + .../public/components/files/files_table.tsx | 28 +++++++--------- .../components/files/files_utility_bar.tsx | 32 +------------------ .../cases/public/containers/constants.ts | 3 +- .../public/containers/use_get_case_files.tsx | 6 ++-- 7 files changed, 20 insertions(+), 55 deletions(-) diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx index acaca10c0bb42..c5522b1b5ce7d 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx @@ -80,6 +80,7 @@ export const CaseViewFiles = ({ caseData }: CaseViewFilesProps) => { 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 a74f471c350a4..ccc864ed27d65 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 @@ -14,6 +14,7 @@ import { createAppMockRenderer, TestProviders } from '../../common/mock'; import { FilesTable } from './files_table'; const defaultProps = { + caseId: 'foobar', items: [basicFileMock], pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 1 }, onChange: jest.fn(), 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 5f32684699cb2..64f28a0f85eb1 100644 --- a/x-pack/plugins/cases/public/components/files/files_table.tsx +++ b/x-pack/plugins/cases/public/components/files/files_table.tsx @@ -9,43 +9,34 @@ import React, { useMemo, useState } from 'react'; import type { Pagination, EuiBasicTableProps } from '@elastic/eui'; import type { FileJSON } from '@kbn/shared-ux-file-types'; -import { - EuiBasicTable, - EuiLoadingContent, - EuiSpacer, - EuiText, - EuiEmptyPrompt, - EuiButton, -} from '@elastic/eui'; +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'; -const EmptyFilesTable = () => ( +const EmptyFilesTable = ({ caseId }: { caseId: string }) => ( {i18n.NO_FILES}} data-test-subj="cases-files-table-empty" titleSize="xs" - actions={ - - {i18n.ADD_FILE} - - } + actions={} /> ); EmptyFilesTable.displayName = 'EmptyFilesTable'; interface FilesTableProps { + caseId: string; isLoading: boolean; items: FileJSON[]; onChange: EuiBasicTableProps['onChange']; pagination: Pagination; } -export const FilesTable = ({ items, pagination, onChange, isLoading }: FilesTableProps) => { +export const FilesTable = ({ caseId, items, pagination, onChange, isLoading }: FilesTableProps) => { const { client: filesClient } = useFilesContext(); const [isPreviewVisible, setIsPreviewVisible] = useState(false); const [selectedFile, setSelectedFile] = useState(); @@ -76,7 +67,10 @@ export const FilesTable = ({ items, pagination, onChange, isLoading }: FilesTabl ); return isLoading ? ( - + <> + + + ) : ( <> {pagination.totalItemCount > 0 && ( @@ -95,7 +89,7 @@ export const FilesTable = ({ items, pagination, onChange, isLoading }: FilesTabl pagination={pagination} onChange={onChange} data-test-subj="cases-files-table" - noItemsMessage={} + noItemsMessage={} /> {isPreviewVisible && selectedFile !== undefined && ( void; } -const HiddenButtonGroup = styled(EuiButtonGroup)` - display: none; -`; - -const tableViewSelectedId = 'tableViewSelectedId'; -const toggleButtonsIcons = [ - { - id: 'thumbnailViewSelectedId', - label: 'Thumbnail view', - iconType: 'grid', - isDisabled: true, - }, - { - id: tableViewSelectedId, - label: 'Table view', - iconType: 'editorUnorderedList', - }, -]; - export const FilesUtilityBar = ({ caseId, onSearch }: FilesUtilityBarProps) => { return ( @@ -51,16 +31,6 @@ export const FilesUtilityBar = ({ caseId, onSearch }: FilesUtilityBarProps) => { data-test-subj="case-detail-search-file" /> - - - {}} - isIconOnly - /> - ); }; diff --git a/x-pack/plugins/cases/public/containers/constants.ts b/x-pack/plugins/cases/public/containers/constants.ts index 66d5675b6c72f..6ad841e3b7f94 100644 --- a/x-pack/plugins/cases/public/containers/constants.ts +++ b/x-pack/plugins/cases/public/containers/constants.ts @@ -23,7 +23,8 @@ export const casesQueriesKeys = { cases: (params: unknown) => [...casesQueriesKeys.casesList(), 'all-cases', params] as const, caseView: () => [...casesQueriesKeys.all, 'case'] as const, case: (id: string) => [...casesQueriesKeys.caseView(), id] as const, - caseFiles: (params: unknown) => [...casesQueriesKeys.caseView(), 'attachments', params] as const, + caseFiles: (id: string, params: unknown) => + [...casesQueriesKeys.case(id), 'attachments', params] as const, caseMetrics: (id: string, features: SingleCaseMetricsFeature[]) => [...casesQueriesKeys.case(id), 'metrics', features] as const, caseConnectors: (id: string) => [...casesQueriesKeys.case(id), 'connectors'], 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 1a834638d2e30..2b1d29a6db0cb 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 @@ -15,7 +15,6 @@ import type { ServerError } from '../types'; import { APP_ID } from '../../common'; import { useCasesToast } from '../common/use_cases_toast'; -import { useCasesContext } from '../components/cases_context/use_cases_context'; import { CASES_FILE_KINDS } from '../files'; import { casesQueriesKeys } from './constants'; import * as i18n from './translations'; @@ -36,19 +35,18 @@ export const useGetCaseFiles = ({ perPage, searchTerm, }: GetCaseFilesParams): UseQueryResult<{ files: FileJSON[]; total: number }> => { - const { owner } = useCasesContext(); const { showErrorToast } = useCasesToast(); const { client: filesClient } = useFilesContext(); return useQuery( - casesQueriesKeys.caseFiles({ caseId, page, perPage, searchTerm, owner: owner[0] }), + casesQueriesKeys.caseFiles(caseId, { page, perPage, searchTerm }), () => { return filesClient.list({ kind: CASES_FILE_KINDS[APP_ID].id, page: page + 1, ...(searchTerm && { name: `*${searchTerm}*` }), perPage, - meta: { caseId, owner: owner[0] }, + meta: { caseId }, }); }, { From 3f1d76f9a2ed1050b62c6da4b3a035415771bc81 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Fri, 24 Mar 2023 11:30:20 +0100 Subject: [PATCH 17/23] Use isPreviousData in FilesTable pagination. Pass correct owner to definitions of the file kind. --- .../case_view/components/case_view_files.tsx | 10 +++++++--- .../public/components/cases_context/index.tsx | 8 ++------ .../cases/public/components/files/add_file.tsx | 15 ++++++++++----- .../public/components/files/file_preview.tsx | 10 +++++++--- .../components/files/use_files_table_columns.tsx | 10 +++++++--- .../public/containers/use_get_case_files.tsx | 8 +++++--- x-pack/plugins/cases/public/files/index.ts | 3 +++ 7 files changed, 41 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx index c5522b1b5ce7d..7c6126e17c8ab 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx @@ -30,14 +30,18 @@ export const CaseViewFiles = ({ caseData }: CaseViewFilesProps) => { page: 0, perPage: 10, }); - const { data: caseFiles, isLoading } = useGetCaseFiles({ + const { + data: caseFiles, + isLoading, + isPreviousData, + } = useGetCaseFiles({ ...filteringOptions, caseId: caseData.id, }); const onTableChange = useCallback( ({ page }: Criteria) => { - if (page) { + if (page && !isPreviousData) { setFilteringOptions({ ...filteringOptions, page: page.index, @@ -45,7 +49,7 @@ export const CaseViewFiles = ({ caseData }: CaseViewFilesProps) => { }); } }, - [filteringOptions] + [filteringOptions, isPreviousData] ); const onSearchChange = useCallback( 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 ab22e694eed51..1031ec2927609 100644 --- a/x-pack/plugins/cases/public/components/cases_context/index.tsx +++ b/x-pack/plugins/cases/public/components/cases_context/index.tsx @@ -24,14 +24,13 @@ import type { import type { ReleasePhase } from '../types'; import type { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry'; import type { PersistableStateAttachmentTypeRegistry } from '../../client/attachment_framework/persistable_state_registry'; -import type { Owner } from '../../../common/constants/types'; import { CasesGlobalComponents } from './cases_global_components'; import { 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 { CASES_FILE_KINDS } from '../../files'; +import { isRegisteredOwner, CASES_FILE_KINDS } from '../../files'; export type CasesContextValueDispatch = Dispatch; @@ -131,10 +130,7 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({ return contextChildren; } - const isValidOwner = (ownerToCheck: string): ownerToCheck is Owner => - ownerToCheck in CASES_FILE_KINDS; - - if (isValidOwner(owner[0])) { + if (isRegisteredOwner(owner[0])) { return ( {contextChildren} 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 7d178b29a7dae..7c1bb2ab55855 100644 --- a/x-pack/plugins/cases/public/components/files/add_file.tsx +++ b/x-pack/plugins/cases/public/components/files/add_file.tsx @@ -20,14 +20,16 @@ import { FILE_SO_TYPE } from '@kbn/files-plugin/common'; import { FileUpload } from '@kbn/shared-ux-file-upload'; import { useFilesContext } from '@kbn/shared-ux-file-context'; -import { APP_ID, CommentType, ExternalReferenceStorageType } from '../../../common'; +import type { Owner } from '../../../common/constants/types'; + +import { CommentType, ExternalReferenceStorageType } from '../../../common'; import { FILE_ATTACHMENT_TYPE } from '../../../common/api'; +import { constructFileKindIdByOwner } from '../../../common/constants'; +import { useCasesToast } from '../../common/use_cases_toast'; import { useCreateAttachments } from '../../containers/use_create_attachments'; -import { CASES_FILE_KINDS } from '../../files'; import { useCasesContext } from '../cases_context/use_cases_context'; import * as i18n from './translations'; import { useRefreshCaseViewPage } from '../case_view/use_on_refresh_case_view_page'; -import { useCasesToast } from '../../common/use_cases_toast'; interface AddFileProps { caseId: string; @@ -97,7 +99,10 @@ const AddFileComponent: React.FC = ({ caseId }) => { // error toast is handled inside createAttachments // we need to delete the file if attachment creation failed - await filesClient.delete({ kind: CASES_FILE_KINDS[APP_ID].id, id: file.id }); + await filesClient.delete({ + kind: constructFileKindIdByOwner(owner[0] as Owner), + id: file.id, + }); } closeModal(); @@ -131,7 +136,7 @@ const AddFileComponent: React.FC = ({ caseId }) => { void; @@ -31,6 +33,8 @@ const StyledOverlayMask = styled(EuiOverlayMask)` `; export const FilePreview = ({ closePreview, selectedFile, getDownloadHref }: FilePreviewProps) => { + const { owner } = useCasesContext(); + return ( @@ -39,7 +43,7 @@ export const FilePreview = ({ closePreview, selectedFile, getDownloadHref }: Fil size="original" src={getDownloadHref({ id: selectedFile.id || '', - fileKind: CASES_FILE_KINDS[APP_ID].id, + fileKind: constructFileKindIdByOwner(owner[0] as Owner), })} /> 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 621077d0719ee..7e3322047f7e2 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 @@ -12,8 +12,10 @@ import type { FileJSON } from '@kbn/shared-ux-file-types'; import { EuiLink, EuiButtonIcon } from '@elastic/eui'; -import { APP_ID } from '../../../common'; -import { CASES_FILE_KINDS } from '../../files'; +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 } from './utils'; @@ -26,6 +28,8 @@ export const useFilesTableColumns = ({ showPreview, getDownloadHref, }: FilesTableColumnsProps): Array> => { + const { owner } = useCasesContext(); + return [ { name: i18n.NAME, @@ -64,7 +68,7 @@ export const useFilesTableColumns = ({ iconType={'download'} aria-label={'download'} href={getDownloadHref({ - fileKind: CASES_FILE_KINDS[APP_ID].id, + fileKind: constructFileKindIdByOwner(owner[0] as Owner), id: attachment.id, })} data-test-subj={'cases-files-table-action-download'} 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 2b1d29a6db0cb..8e594dba4cbd5 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 @@ -11,13 +11,14 @@ import type { UseQueryResult } from '@tanstack/react-query'; import { useFilesContext } from '@kbn/shared-ux-file-context'; import { useQuery } from '@tanstack/react-query'; +import type { Owner } from '../../common/constants/types'; import type { ServerError } from '../types'; -import { APP_ID } from '../../common'; +import { constructFileKindIdByOwner } from '../../common/constants'; import { useCasesToast } from '../common/use_cases_toast'; -import { CASES_FILE_KINDS } from '../files'; import { casesQueriesKeys } from './constants'; import * as i18n from './translations'; +import { useCasesContext } from '../components/cases_context/use_cases_context'; export interface CaseFilesFilteringOptions { page: number; @@ -35,6 +36,7 @@ export const useGetCaseFiles = ({ perPage, searchTerm, }: GetCaseFilesParams): UseQueryResult<{ files: FileJSON[]; total: number }> => { + const { owner } = useCasesContext(); const { showErrorToast } = useCasesToast(); const { client: filesClient } = useFilesContext(); @@ -42,7 +44,7 @@ export const useGetCaseFiles = ({ casesQueriesKeys.caseFiles(caseId, { page, perPage, searchTerm }), () => { return filesClient.list({ - kind: CASES_FILE_KINDS[APP_ID].id, + kind: constructFileKindIdByOwner(owner[0] as Owner), page: page + 1, ...(searchTerm && { name: `*${searchTerm}*` }), perPage, diff --git a/x-pack/plugins/cases/public/files/index.ts b/x-pack/plugins/cases/public/files/index.ts index 7c3e08575182b..9066e9e53cabe 100644 --- a/x-pack/plugins/cases/public/files/index.ts +++ b/x-pack/plugins/cases/public/files/index.ts @@ -20,6 +20,9 @@ const buildFileKind = (owner: Owner): FileKindBrowser => { }; }; +export const isRegisteredOwner = (ownerToCheck: string): ownerToCheck is Owner => + ownerToCheck in CASES_FILE_KINDS; + /** * The file kind definition for interacting with the file service for the UI */ From 95926fcb8f12673d3c7061a0650e674febe00f7b Mon Sep 17 00:00:00 2001 From: adcoelho Date: Fri, 24 Mar 2023 12:07:04 +0100 Subject: [PATCH 18/23] Change files count information in files table. Change file name and mime type in files table. --- .../public/components/files/files_table.tsx | 20 +++---------------- .../public/components/files/translations.tsx | 6 ++++++ .../files/use_files_table_columns.tsx | 9 ++++++--- 3 files changed, 15 insertions(+), 20 deletions(-) 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 64f28a0f85eb1..247ca60fd3e8f 100644 --- a/x-pack/plugins/cases/public/components/files/files_table.tsx +++ b/x-pack/plugins/cases/public/components/files/files_table.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useMemo, useState } from 'react'; +import React, { useState } from 'react'; import type { Pagination, EuiBasicTableProps } from '@elastic/eui'; import type { FileJSON } from '@kbn/shared-ux-file-types'; @@ -52,20 +52,6 @@ export const FilesTable = ({ caseId, items, pagination, onChange, isLoading }: F getDownloadHref: filesClient.getDownloadHref, }); - const resultsCount = useMemo( - () => ( - <> - - {pagination.pageSize * pagination.pageIndex + 1} - {'-'} - {pagination.pageSize * pagination.pageIndex + pagination.pageSize} - {' '} - {'of'} {pagination.totalItemCount} - - ), - [pagination.pageIndex, pagination.pageSize, pagination.totalItemCount] - ); - return isLoading ? ( <> @@ -76,8 +62,8 @@ export const FilesTable = ({ caseId, items, pagination, onChange, isLoading }: F {pagination.totalItemCount > 0 && ( <> - - {i18n.RESULTS_COUNT} {resultsCount} + + {i18n.SHOWING_FILES(pagination.totalItemCount)} )} diff --git a/x-pack/plugins/cases/public/components/files/translations.tsx b/x-pack/plugins/cases/public/components/files/translations.tsx index da62c5444d69a..55d3f679610d9 100644 --- a/x-pack/plugins/cases/public/components/files/translations.tsx +++ b/x-pack/plugins/cases/public/components/files/translations.tsx @@ -68,3 +68,9 @@ export const SUCCESSFUL_UPLOAD_FILE_NAME = (fileName: string) => defaultMessage: 'File {fileName} uploaded successfully', values: { fileName }, }); + +export const SHOWING_FILES = (totalFiles: number) => + i18n.translate('xpack.cases.caseView.files.showingFilesTitle', { + values: { totalFiles }, + defaultMessage: 'Showing {totalFiles} {totalFiles, plural, =1 {file} other {files}}', + }); 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 7e3322047f7e2..cceabca38e281 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 @@ -35,18 +35,21 @@ export const useFilesTableColumns = ({ 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)}>{attachment.name}; + return showPreview(attachment)}>{fileName}; } else { - return {attachment.name}; + return {fileName}; } }, width: '60%', }, { name: i18n.TYPE, - field: 'mimeType', 'data-test-subj': 'cases-files-table-filetype', + render: (attachment: FileJSON) => { + return {`${attachment.mimeType?.split('/')[0]}` || ''}; + }, }, { name: i18n.DATE_ADDED, From 81b41d1351ce0372fac722e18c9e01d4c0df3d56 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Fri, 24 Mar 2023 16:03:56 +0100 Subject: [PATCH 19/23] Add FilePreview tests. Improve FilesTable tests. Add FilesUtilityBar tests. Add UseGetCaseFiles tests. --- .../public/common/mock/test_providers.tsx | 10 +-- .../public/components/files/add_file.tsx | 4 +- .../components/files/file_preview.test.tsx | 66 +++++++++++++++++ .../public/components/files/file_preview.tsx | 1 + .../components/files/files_table.test.tsx | 58 ++++++++++----- .../files/files_utility_bar.test.tsx | 42 +++++++++++ .../components/files/files_utility_bar.tsx | 2 +- .../files/use_files_table_columns.test.tsx | 2 +- .../plugins/cases/public/containers/mock.ts | 2 +- .../containers/use_get_case_files.test.tsx | 70 +++++++++++++++++++ 10 files changed, 230 insertions(+), 27 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/files/file_preview.test.tsx create mode 100644 x-pack/plugins/cases/public/components/files/files_utility_bar.test.tsx create mode 100644 x-pack/plugins/cases/public/containers/use_get_case_files.test.tsx diff --git a/x-pack/plugins/cases/public/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx index 6141e4fdcf40d..9e94e30cf063a 100644 --- a/x-pack/plugins/cases/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx @@ -51,15 +51,17 @@ type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResul window.scrollTo = jest.fn(); -const mockGetFilesClient = createMockFilesClient as unknown as ( - scope: string -) => ScopedFilesClient; +export const mockedFilesClient = createMockFilesClient() as unknown as ScopedFilesClient; + +const mockGetFilesClient = () => mockedFilesClient; + +export const mockedTestProvidersOwner = [SECURITY_SOLUTION_OWNER]; /** A utility for wrapping children in the providers required to run most tests */ const TestProvidersComponent: React.FC = ({ children, features, - owner = [SECURITY_SOLUTION_OWNER], + owner = mockedTestProvidersOwner, permissions = allCasesPermissions(), releasePhase = 'ga', externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry(), 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 7c1bb2ab55855..1b4e36dca4297 100644 --- a/x-pack/plugins/cases/public/components/files/add_file.tsx +++ b/x-pack/plugins/cases/public/components/files/add_file.tsx @@ -121,7 +121,7 @@ const AddFileComponent: React.FC = ({ caseId }) => { return ( <> = ({ caseId }) => { {i18n.ADD_FILE} {isModalVisible && ( - + {i18n.ADD_FILE} 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 new file mode 100644 index 0000000000000..04322799b46de --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/file_preview.test.tsx @@ -0,0 +1,66 @@ +/* + * 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 { constructFileKindIdByOwner } from '../../../common/constants'; +import { createAppMockRenderer, mockedTestProvidersOwner } from '../../common/mock'; +import { basicFileMock } from '../../containers/mock'; +import { FilePreview } from './file_preview'; + +describe('FilePreview', () => { + let appMockRender: AppMockRenderer; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('FilePreview rendered correctly', async () => { + const mockGetDownloadRef = jest.fn(); + + appMockRender.render( + + ); + + await waitFor(() => + expect(mockGetDownloadRef).toBeCalledWith({ + id: basicFileMock.id, + fileKind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), + }) + ); + + expect(await screen.findByTestId('cases-files-image-preview')).toBeInTheDocument(); + }); + + // I cannot get this test to work + it.skip('clicking outside Image triggers closePreview', () => { + const mockClosePreview = jest.fn(); + + appMockRender.render( + <> +
+ + + ); + + userEvent.click(screen.getByTestId('outsideClickDummy')); + expect(mockClosePreview).toBeCalled(); + }); +}); 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 d60cbc3d9eb15..19429861ba389 100644 --- a/x-pack/plugins/cases/public/components/files/file_preview.tsx +++ b/x-pack/plugins/cases/public/components/files/file_preview.tsx @@ -45,6 +45,7 @@ export const FilePreview = ({ closePreview, selectedFile, getDownloadHref }: Fil id: selectedFile.id || '', fileKind: constructFileKindIdByOwner(owner[0] as Owner), })} + data-test-subj="cases-files-image-preview" /> 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 ccc864ed27d65..ed881ce985a64 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 @@ -6,12 +6,13 @@ */ import React from 'react'; -import { screen } from '@testing-library/react'; +import { screen, within } from '@testing-library/react'; import { basicFileMock } from '../../containers/mock'; import type { AppMockRenderer } from '../../common/mock'; -import { createAppMockRenderer, TestProviders } from '../../common/mock'; +import { createAppMockRenderer } from '../../common/mock'; import { FilesTable } from './files_table'; +import userEvent from '@testing-library/user-event'; const defaultProps = { caseId: 'foobar', @@ -30,11 +31,7 @@ describe('FilesTable', () => { }); it('renders correctly', async () => { - appMockRender.render( - - - - ); + appMockRender.render(); expect(await screen.findByTestId('cases-files-table-results-count')).toBeInTheDocument(); expect(await screen.findByTestId('cases-files-table-filename')).toBeInTheDocument(); @@ -45,23 +42,48 @@ describe('FilesTable', () => { }); it('renders loading state', async () => { - appMockRender.render( - - - - ); + appMockRender.render(); expect(await screen.findByTestId('cases-files-table-loading')).toBeInTheDocument(); }); it('renders empty table', async () => { - appMockRender.render( - - - - ); + appMockRender.render(); expect(await screen.findByTestId('cases-files-table-empty')).toBeInTheDocument(); - expect(await screen.findByTestId('cases-files-table-empty')).toBeInTheDocument(); + }); + + it('renders single result count properly', async () => { + const mockPagination = { pageIndex: 0, pageSize: 10, totalItemCount: 4 }; + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-table-results-count')).toHaveTextContent( + 'Showing 4 files' + ); + }); + + it('non image rows dont open file preview', async () => { + const nonImageFileMock = { ...basicFileMock, mimeType: 'something/else' }; + + appMockRender.render(); + + userEvent.click( + await within(await screen.findByTestId('cases-files-table-filename')).findByTitle( + 'No preview available' + ) + ); + + expect(await screen.queryByTestId('case-files-image-preview')).not.toBeInTheDocument(); + }); + + it('image rows open file preview', async () => { + appMockRender.render(); + + userEvent.click( + await screen.findByRole('button', { + name: `${basicFileMock.name}.${basicFileMock.extension}`, + }) + ); + expect(await screen.findByTestId('case-files-image-preview')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/cases/public/components/files/files_utility_bar.test.tsx b/x-pack/plugins/cases/public/components/files/files_utility_bar.test.tsx new file mode 100644 index 0000000000000..bfac1998a857a --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/files_utility_bar.test.tsx @@ -0,0 +1,42 @@ +/* + * 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 } from '../../common/mock'; +import userEvent from '@testing-library/user-event'; +import { FilesUtilityBar } from './files_utility_bar'; + +const defaultProps = { + caseId: 'foobar', + onSearch: jest.fn(), +}; + +describe('FilesUtilityBar', () => { + let appMockRender: AppMockRenderer; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('renders correctly', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-add')).toBeInTheDocument(); + expect(await screen.findByTestId('cases-files-search')).toBeInTheDocument(); + }); + + it('search text passed correctly to callback', async () => { + appMockRender.render(); + + await userEvent.type(screen.getByTestId('cases-files-search'), 'My search{enter}'); + expect(defaultProps.onSearch).toBeCalledWith('My search'); + }); +}); diff --git a/x-pack/plugins/cases/public/components/files/files_utility_bar.tsx b/x-pack/plugins/cases/public/components/files/files_utility_bar.tsx index 9c1cd01d2c0a6..420985aa610b4 100644 --- a/x-pack/plugins/cases/public/components/files/files_utility_bar.tsx +++ b/x-pack/plugins/cases/public/components/files/files_utility_bar.tsx @@ -28,7 +28,7 @@ export const FilesUtilityBar = ({ caseId, onSearch }: FilesUtilityBarProps) => { placeholder={i18n.SEARCH_PLACEHOLDER} onSearch={onSearch} incremental={false} - data-test-subj="case-detail-search-file" + data-test-subj="cases-files-search" /> 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 c1d438369bd90..6ee4cbff94c6f 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 @@ -39,8 +39,8 @@ describe('useCasesColumns ', () => { }, Object { "data-test-subj": "cases-files-table-filetype", - "field": "mimeType", "name": "Type", + "render": [Function], }, Object { "data-test-subj": "cases-files-table-date-added", diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index 5d5960043efb2..4c0e9d67be0a8 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -252,7 +252,7 @@ export const basicFileMock: FileJSON = { alt: '', fileKind: '', status: 'READY', - extension: '.png', + extension: 'png', }; export const caseWithAlerts = { 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 new file mode 100644 index 0000000000000..7a47eab3f36d0 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/use_get_case_files.test.tsx @@ -0,0 +1,70 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; + +import { basicCase } from './mock'; + +import type { AppMockRenderer } from '../common/mock'; +import { mockedTestProvidersOwner, mockedFilesClient, createAppMockRenderer } from '../common/mock'; +import { useToasts } from '../common/lib/kibana'; +import { useGetCaseFiles } from './use_get_case_files'; +import { constructFileKindIdByOwner } from '../../common/constants/files'; + +jest.mock('../common/lib/kibana'); + +const hookParams = { + caseId: basicCase.id, + page: 1, + perPage: 1, + searchTerm: 'foobar', +}; + +const expectedCallParams = { + kind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), + page: hookParams.page + 1, + name: `*${hookParams.searchTerm}*`, + perPage: hookParams.perPage, + meta: { caseId: hookParams.caseId }, +}; + +describe('useGetCaseFiles', () => { + let appMockRender: AppMockRenderer; + + beforeEach(() => { + appMockRender = createAppMockRenderer(); + jest.clearAllMocks(); + }); + + it('shows an error toast when filesClient.list throws', async () => { + const addError = jest.fn(); + (useToasts as jest.Mock).mockReturnValue({ addError }); + + mockedFilesClient.list = jest.fn().mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + const { waitForNextUpdate } = renderHook(() => useGetCaseFiles(hookParams), { + wrapper: appMockRender.AppWrapper, + }); + await waitForNextUpdate(); + + expect(mockedFilesClient.list).toBeCalledWith(expectedCallParams); + expect(addError).toHaveBeenCalled(); + }); + + it('calls filesClient.list with correct arguments', async () => { + await act(async () => { + const { waitForNextUpdate } = renderHook(() => useGetCaseFiles(hookParams), { + wrapper: appMockRender.AppWrapper, + }); + await waitForNextUpdate(); + + expect(mockedFilesClient.list).toBeCalledWith(expectedCallParams); + }); + }); +}); From 68c3047835236f61523cbca63b411793e7d81fdd Mon Sep 17 00:00:00 2001 From: adcoelho Date: Mon, 27 Mar 2023 15:46:39 +0200 Subject: [PATCH 20/23] Changed implementation of isRegisteredOwner. Created tests for AddFile component. Added more tests for FilesTable component. Created parseMimeType util. Added a test for showDangerToast. --- .../public/common/mock/test_providers.tsx | 9 +- .../public/common/use_cases_toast.test.tsx | 19 ++ .../cases/public/common/use_cases_toast.tsx | 2 +- .../public/components/files/add_file.test.tsx | 217 ++++++++++++++++++ .../public/components/files/add_file.tsx | 1 - .../components/files/files_table.test.tsx | 49 +++- .../public/components/files/translations.tsx | 10 +- .../files/use_files_table_columns.tsx | 4 +- .../cases/public/components/files/utils.tsx | 16 ++ x-pack/plugins/cases/public/files/index.ts | 2 +- 10 files changed, 317 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/files/add_file.test.tsx diff --git a/x-pack/plugins/cases/public/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx index 9e94e30cf063a..7728ecb54d766 100644 --- a/x-pack/plugins/cases/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx @@ -53,6 +53,13 @@ window.scrollTo = jest.fn(); export const mockedFilesClient = createMockFilesClient() as unknown as ScopedFilesClient; +// @ts-ignore +mockedFilesClient.getFileKind.mockImplementation(() => ({ + id: 'test', + maxSizeBytes: 10000, + http: {}, +})); + const mockGetFilesClient = () => mockedFilesClient; export const mockedTestProvidersOwner = [SECURITY_SOLUTION_OWNER]; @@ -141,7 +148,7 @@ export const testQueryClient = new QueryClient({ export const createAppMockRenderer = ({ features, - owner = [SECURITY_SOLUTION_OWNER], + owner = mockedTestProvidersOwner, permissions = allCasesPermissions(), releasePhase = 'ga', externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry(), diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx index 6c33c86d29d51..7191767f780dd 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx @@ -25,6 +25,7 @@ const useKibanaMock = useKibana as jest.Mocked; describe('Use cases toast hook', () => { const successMock = jest.fn(); const errorMock = jest.fn(); + const dangerMock = jest.fn(); const getUrlForApp = jest.fn().mockReturnValue(`/app/cases/${mockCase.id}`); const navigateToUrl = jest.fn(); @@ -54,6 +55,7 @@ describe('Use cases toast hook', () => { return { addSuccess: successMock, addError: errorMock, + addDanger: dangerMock, }; }); @@ -352,4 +354,21 @@ describe('Use cases toast hook', () => { }); }); }); + + describe('showDangerToast', () => { + it('should show a danger toast', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + + result.current.showDangerToast('my danger toast'); + + expect(dangerMock).toHaveBeenCalledWith({ + title: 'my danger toast', + }); + }); + }); }); diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.tsx index 70ad598e863e9..a003226688011 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.tsx @@ -170,7 +170,7 @@ export const useCasesToast = () => { toasts.addSuccess({ title, className: 'eui-textBreakWord' }); }, showDangerToast: (title: string) => { - toasts.addDanger({ title, className: 'eui-textBreakWord' }); + toasts.addDanger({ title }); }, }; }; 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 new file mode 100644 index 0000000000000..7be441c4cb7c5 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/add_file.test.tsx @@ -0,0 +1,217 @@ +/* + * 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 { FileUploadProps } from '@kbn/shared-ux-file-upload'; + +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import type { AppMockRenderer } from '../../common/mock'; + +import { constructFileKindIdByOwner } from '../../../common/constants'; +import { + createAppMockRenderer, + mockedTestProvidersOwner, + mockedFilesClient, +} from '../../common/mock'; +import { AddFile } from './add_file'; +import { useToasts } from '../../common/lib/kibana'; + +import { useCreateAttachments } from '../../containers/use_create_attachments'; +import { basicFileMock } from '../../containers/mock'; + +jest.mock('../../containers/use_create_attachments'); +jest.mock('../../common/lib/kibana'); + +const useToastsMock = useToasts as jest.Mock; +const useCreateAttachmentsMock = useCreateAttachments as jest.Mock; + +const mockedExternalReferenceId = 'externalReferenceId'; +const validateMetadata = jest.fn(); +const mockFileUpload = jest + .fn() + .mockImplementation( + ({ + kind, + onDone, + onError, + meta, + }: Required>) => ( + <> + + + + + ) + ); + +jest.mock('@kbn/shared-ux-file-upload', () => { + const original = jest.requireActual('@kbn/shared-ux-file-upload'); + return { + ...original, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + FileUpload: (props: any) => mockFileUpload(props), + }; +}); + +describe('AddFile', () => { + let appMockRender: AppMockRenderer; + + const successMock = jest.fn(); + const errorMock = jest.fn(); + + useToastsMock.mockImplementation(() => { + return { + addSuccess: successMock, + addError: errorMock, + }; + }); + + const createAttachmentsMock = jest.fn(); + + useCreateAttachmentsMock.mockReturnValue({ + isLoading: false, + createAttachments: createAttachmentsMock, + }); + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('renders correctly', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-add')).toBeInTheDocument(); + }); + + it('clicking button renders modal', async () => { + appMockRender.render(); + + userEvent.click(await screen.findByTestId('cases-files-add')); + + expect(await screen.findByTestId('cases-files-add-modal')).toBeInTheDocument(); + }); + + it('createAttachments called with right parameters', async () => { + appMockRender.render(); + + userEvent.click(await screen.findByTestId('cases-files-add')); + + expect(await screen.findByTestId('cases-files-add-modal')).toBeInTheDocument(); + + 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', + }, + ], + throwOnError: true, + }) + ) + ); + await waitFor(() => + expect(successMock).toHaveBeenCalledWith({ + className: 'eui-textBreakWord', + title: `File ${basicFileMock.name} uploaded successfully`, + }) + ); + }); + + it('failed upload displays error toast', async () => { + appMockRender.render(); + + userEvent.click(await screen.findByTestId('cases-files-add')); + + expect(await screen.findByTestId('cases-files-add-modal')).toBeInTheDocument(); + + userEvent.click(await screen.findByTestId('testOnError')); + + expect(errorMock).toHaveBeenCalledWith( + { name: 'upload error name', message: 'upload error message' }, + { + title: 'Failed to upload file', + } + ); + }); + + it('correct metadata is passed to FileUpload component', async () => { + const caseId = 'foobar'; + + appMockRender.render(); + + userEvent.click(await screen.findByTestId('cases-files-add')); + + expect(await screen.findByTestId('cases-files-add-modal')).toBeInTheDocument(); + + userEvent.click(await screen.findByTestId('testMetadata')); + + await waitFor(() => + expect(validateMetadata).toHaveBeenCalledWith({ caseId, owner: mockedTestProvidersOwner[0] }) + ); + }); + + it('filesClient.delete is called correctly if createAttachments fails', async () => { + createAttachmentsMock.mockImplementation(() => { + throw new Error(); + }); + + appMockRender.render(); + + userEvent.click(await screen.findByTestId('cases-files-add')); + + expect(await screen.findByTestId('cases-files-add-modal')).toBeInTheDocument(); + + userEvent.click(await screen.findByTestId('testOnDone')); + + await waitFor(() => + expect(mockedFilesClient.delete).toHaveBeenCalledWith({ + id: mockedExternalReferenceId, + kind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), + }) + ); + + createAttachmentsMock.mockRestore(); + }); +}); 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 1b4e36dca4297..e6081b3c6a64b 100644 --- a/x-pack/plugins/cases/public/components/files/add_file.tsx +++ b/x-pack/plugins/cases/public/components/files/add_file.tsx @@ -39,7 +39,6 @@ const AddFileComponent: React.FC = ({ caseId }) => { const { owner } = useCasesContext(); const { client: filesClient } = useFilesContext(); const { showDangerToast, showErrorToast, showSuccessToast } = useCasesToast(); - const { isLoading, createAttachments } = useCreateAttachments(); const refreshAttachmentsTable = useRefreshCaseViewPage(); const [isModalVisible, setIsModalVisible] = useState(false); 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 ed881ce985a64..ce78be54b3529 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 @@ -10,7 +10,13 @@ import { screen, within } from '@testing-library/react'; import { basicFileMock } from '../../containers/mock'; import type { AppMockRenderer } from '../../common/mock'; -import { createAppMockRenderer } from '../../common/mock'; + +import { constructFileKindIdByOwner } from '../../../common/constants'; +import { + createAppMockRenderer, + mockedFilesClient, + mockedTestProvidersOwner, +} from '../../common/mock'; import { FilesTable } from './files_table'; import userEvent from '@testing-library/user-event'; @@ -73,7 +79,7 @@ describe('FilesTable', () => { ) ); - expect(await screen.queryByTestId('case-files-image-preview')).not.toBeInTheDocument(); + expect(await screen.queryByTestId('cases-files-image-preview')).not.toBeInTheDocument(); }); it('image rows open file preview', async () => { @@ -84,6 +90,43 @@ describe('FilesTable', () => { name: `${basicFileMock.name}.${basicFileMock.extension}`, }) ); - expect(await screen.findByTestId('case-files-image-preview')).toBeInTheDocument(); + + expect(await screen.findByTestId('cases-files-image-preview')).toBeInTheDocument(); + }); + + it('different mimeTypes are displayed correctly', async () => { + const mockPagination = { pageIndex: 0, pageSize: 10, totalItemCount: 7 }; + appMockRender.render( + + ); + + expect((await screen.findAllByText('Unknown')).length).toBe(4); + expect(await screen.findByText('Application')).toBeInTheDocument(); + expect(await screen.findByText('Text')).toBeInTheDocument(); + expect(await screen.findByText('Image')).toBeInTheDocument(); + }); + + it('download 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-table-action-download')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/cases/public/components/files/translations.tsx b/x-pack/plugins/cases/public/components/files/translations.tsx index 55d3f679610d9..6ea6080d3cb33 100644 --- a/x-pack/plugins/cases/public/components/files/translations.tsx +++ b/x-pack/plugins/cases/public/components/files/translations.tsx @@ -55,16 +55,20 @@ export const TYPE = i18n.translate('xpack.cases.caseView.files.type', { defaultMessage: 'Type', }); -export const SEARCH_PLACEHOLDER = i18n.translate('xpack.cases.caseViewFiles.searchPlaceholder', { +export const SEARCH_PLACEHOLDER = i18n.translate('xpack.cases.caseView.files.searchPlaceholder', { defaultMessage: 'Search files', }); -export const FAILED_UPLOAD = i18n.translate('xpack.cases.caseView.failedUpload', { +export const FAILED_UPLOAD = i18n.translate('xpack.cases.caseView.files.failedUpload', { defaultMessage: 'Failed to upload file', }); +export const UNKNOWN_MIME_TYPE = i18n.translate('xpack.cases.caseView.files.unknownMimeType', { + defaultMessage: 'Unknown', +}); + export const SUCCESSFUL_UPLOAD_FILE_NAME = (fileName: string) => - i18n.translate('xpack.cases.caseView.successfulUploadFileName', { + i18n.translate('xpack.cases.caseView.files.successfulUploadFileName', { defaultMessage: 'File {fileName} uploaded successfully', values: { fileName }, }); 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 cceabca38e281..7477f71128162 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 @@ -17,7 +17,7 @@ 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 } from './utils'; +import { isImage, parseMimeType } from './utils'; export interface FilesTableColumnsProps { showPreview: (file: FileJSON) => void; @@ -48,7 +48,7 @@ export const useFilesTableColumns = ({ name: i18n.TYPE, 'data-test-subj': 'cases-files-table-filetype', render: (attachment: FileJSON) => { - return {`${attachment.mimeType?.split('/')[0]}` || ''}; + return {parseMimeType(attachment.mimeType)}; }, }, { diff --git a/x-pack/plugins/cases/public/components/files/utils.tsx b/x-pack/plugins/cases/public/components/files/utils.tsx index 9a6a68cf6967e..dd18ee7d8e9cc 100644 --- a/x-pack/plugins/cases/public/components/files/utils.tsx +++ b/x-pack/plugins/cases/public/components/files/utils.tsx @@ -7,4 +7,20 @@ import type { FileJSON } from '@kbn/shared-ux-file-types'; +import * as i18n from './translations'; + export const isImage = (file: FileJSON) => file.mimeType?.startsWith('image/'); + +export const parseMimeType = (mimeType: string | undefined) => { + if (typeof mimeType === 'undefined') { + return i18n.UNKNOWN_MIME_TYPE; + } + + const result = mimeType.split('/'); + + if (result.length <= 1 || result[0] === '') { + return i18n.UNKNOWN_MIME_TYPE; + } + + return result[0].charAt(0).toUpperCase() + result[0].slice(1); +}; diff --git a/x-pack/plugins/cases/public/files/index.ts b/x-pack/plugins/cases/public/files/index.ts index 9066e9e53cabe..c3a051adf08d9 100644 --- a/x-pack/plugins/cases/public/files/index.ts +++ b/x-pack/plugins/cases/public/files/index.ts @@ -21,7 +21,7 @@ const buildFileKind = (owner: Owner): FileKindBrowser => { }; export const isRegisteredOwner = (ownerToCheck: string): ownerToCheck is Owner => - ownerToCheck in CASES_FILE_KINDS; + Object.hasOwn(CASES_FILE_KINDS, ownerToCheck); /** * The file kind definition for interacting with the file service for the UI From e2ad7a5033bf7a5091aa2425418838c1b24ff20a Mon Sep 17 00:00:00 2001 From: adcoelho Date: Mon, 27 Mar 2023 15:53:59 +0200 Subject: [PATCH 21/23] Fixed failing jest tests. --- .../components/case_view/components/case_view_files.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx index 66fe926d2754a..4f55c16dfce15 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx @@ -40,8 +40,8 @@ describe('Case View Page files tab', () => { it('should render the utility bar for the files table', async () => { const result = appMockRender.render(); - expect(await result.findByTestId('cases-add-file')).toBeInTheDocument(); - expect(await result.findByTestId('case-detail-search-file')).toBeInTheDocument(); + expect((await result.findAllByTestId('cases-files-add')).length).toBe(2); + expect(await result.findByTestId('cases-files-search')).toBeInTheDocument(); }); it('should render the files table', async () => { From 9fefef4c9e65eae0ec6a658e78732c2725b05b6c Mon Sep 17 00:00:00 2001 From: adcoelho Date: Mon, 27 Mar 2023 17:52:56 +0200 Subject: [PATCH 22/23] Add pagination tests for case view files. Add tests for file utils. --- .../cases/common/constants/mime_types.ts | 8 +-- .../components/case_view_files.test.tsx | 39 +++++++++--- .../case_view/components/case_view_files.tsx | 12 ++-- .../public/components/files/add_file.test.tsx | 1 + .../components/files/files_table.test.tsx | 59 ++++++++++++++++--- .../public/components/files/utils.test.tsx | 49 +++++++++++++++ 6 files changed, 142 insertions(+), 26 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/files/utils.test.tsx diff --git a/x-pack/plugins/cases/common/constants/mime_types.ts b/x-pack/plugins/cases/common/constants/mime_types.ts index 9f1f455513dab..c35e5ef674c81 100644 --- a/x-pack/plugins/cases/common/constants/mime_types.ts +++ b/x-pack/plugins/cases/common/constants/mime_types.ts @@ -8,7 +8,7 @@ /** * These were retrieved from https://www.iana.org/assignments/media-types/media-types.xhtml#image */ -const imageMimeTypes = [ +export const imageMimeTypes = [ 'image/aces', 'image/apng', 'image/avci', @@ -87,9 +87,9 @@ const imageMimeTypes = [ 'image/wmf', ]; -const textMimeTypes = ['text/plain', 'text/csv', 'text/json', 'application/json']; +export const textMimeTypes = ['text/plain', 'text/csv', 'text/json', 'application/json']; -const compressionMimeTypes = [ +export const compressionMimeTypes = [ 'application/zip', 'application/gzip', 'application/x-bzip', @@ -98,7 +98,7 @@ const compressionMimeTypes = [ 'application/x-tar', ]; -const pdfMimeTypes = ['application/pdf']; +export const pdfMimeTypes = ['application/pdf']; export const ALLOWED_MIME_TYPES = [ ...imageMimeTypes, diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx index 4f55c16dfce15..aceccca5b44fa 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.test.tsx @@ -6,12 +6,16 @@ */ import React from 'react'; -import { alertCommentWithIndices, basicCase } from '../../../containers/mock'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import type { Case } from '../../../../common'; import type { AppMockRenderer } from '../../../common/mock'; + import { createAppMockRenderer } from '../../../common/mock'; -import type { Case } from '../../../../common'; -import { CaseViewFiles } from './case_view_files'; +import { alertCommentWithIndices, basicCase } from '../../../containers/mock'; import { useGetCaseFiles } from '../../../containers/use_get_case_files'; +import { CaseViewFiles, DEFAULT_CASE_FILES_FILTERING_OPTIONS } from './case_view_files'; jest.mock('../../../containers/use_get_case_files'); @@ -24,8 +28,9 @@ const caseData: Case = { describe('Case View Page files tab', () => { let appMockRender: AppMockRenderer; + useGetCaseFilesMock.mockReturnValue({ - data: {}, + data: { files: [], total: 11 }, isLoading: false, }); @@ -38,15 +43,31 @@ describe('Case View Page files tab', () => { }); it('should render the utility bar for the files table', async () => { - const result = appMockRender.render(); + appMockRender.render(); - expect((await result.findAllByTestId('cases-files-add')).length).toBe(2); - expect(await result.findByTestId('cases-files-search')).toBeInTheDocument(); + expect((await screen.findAllByTestId('cases-files-add')).length).toBe(2); + expect(await screen.findByTestId('cases-files-search')).toBeInTheDocument(); }); it('should render the files table', async () => { - const result = appMockRender.render(); + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-table')).toBeInTheDocument(); + }); + + it('clicking table pagination triggers calls to useGetCaseFiles', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-table')).toBeInTheDocument(); + + userEvent.click(await screen.findByTestId('pagination-button-next')); - expect(await result.findByTestId('cases-files-table')).toBeInTheDocument(); + await waitFor(() => + expect(useGetCaseFilesMock).toHaveBeenCalledWith({ + caseId: basicCase.id, + page: DEFAULT_CASE_FILES_FILTERING_OPTIONS.page + 1, + perPage: DEFAULT_CASE_FILES_FILTERING_OPTIONS.perPage, + }) + ); }); }); diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx index 7c6126e17c8ab..54693acfa2390 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_files.tsx @@ -25,11 +25,15 @@ interface CaseViewFilesProps { caseData: Case; } +export const DEFAULT_CASE_FILES_FILTERING_OPTIONS = { + page: 0, + perPage: 10, +}; + export const CaseViewFiles = ({ caseData }: CaseViewFilesProps) => { - const [filteringOptions, setFilteringOptions] = useState({ - page: 0, - perPage: 10, - }); + const [filteringOptions, setFilteringOptions] = useState( + DEFAULT_CASE_FILES_FILTERING_OPTIONS + ); const { data: caseFiles, isLoading, 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 7be441c4cb7c5..8734407b91cc7 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 @@ -151,6 +151,7 @@ describe('AddFile', () => { }) ) ); + await waitFor(() => expect(successMock).toHaveBeenCalledWith({ className: 'eui-textBreakWord', 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 ce78be54b3529..8caa94c4caa21 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 @@ -6,7 +6,7 @@ */ import React from 'react'; -import { screen, within } from '@testing-library/react'; +import { screen, waitFor, within } from '@testing-library/react'; import { basicFileMock } from '../../containers/mock'; import type { AppMockRenderer } from '../../common/mock'; @@ -20,15 +20,16 @@ import { import { FilesTable } from './files_table'; import userEvent from '@testing-library/user-event'; -const defaultProps = { - caseId: 'foobar', - items: [basicFileMock], - pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 1 }, - onChange: jest.fn(), - isLoading: false, -}; - describe('FilesTable', () => { + const onTableChange = jest.fn(); + const defaultProps = { + caseId: 'foobar', + items: [basicFileMock], + pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 1 }, + isLoading: false, + onChange: onTableChange, + }; + let appMockRender: AppMockRenderer; beforeEach(() => { @@ -129,4 +130,44 @@ describe('FilesTable', () => { expect(await screen.findByTestId('cases-files-table-action-download')).toBeInTheDocument(); }); + + it('go to next page calls onTableChange with correct values', async () => { + const mockPagination = { pageIndex: 0, pageSize: 1, totalItemCount: 2 }; + + appMockRender.render( + + ); + + userEvent.click(await screen.findByTestId('pagination-button-next')); + + await waitFor(() => + expect(onTableChange).toHaveBeenCalledWith({ + page: { index: mockPagination.pageIndex + 1, size: mockPagination.pageSize }, + }) + ); + }); + + it('go to previous page calls onTableChange with correct values', async () => { + const mockPagination = { pageIndex: 1, pageSize: 1, totalItemCount: 2 }; + + appMockRender.render( + + ); + + userEvent.click(await screen.findByTestId('pagination-button-previous')); + + await waitFor(() => + expect(onTableChange).toHaveBeenCalledWith({ + page: { index: mockPagination.pageIndex - 1, size: mockPagination.pageSize }, + }) + ); + }); }); diff --git a/x-pack/plugins/cases/public/components/files/utils.test.tsx b/x-pack/plugins/cases/public/components/files/utils.test.tsx new file mode 100644 index 0000000000000..c45a7b32d779c --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/utils.test.tsx @@ -0,0 +1,49 @@ +/* + * 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 { isImage, parseMimeType } from './utils'; + +import type { FileJSON } from '@kbn/shared-ux-file-types'; +import { imageMimeTypes, textMimeTypes } from '../../../common/constants/mime_types'; + +describe('isImage', () => { + it('should return true for allowed image mime types', () => { + isImage({ mimeType: imageMimeTypes[0] } as FileJSON); + + // @ts-ignore + expect(imageMimeTypes.reduce((acc, curr) => acc && isImage({ mimeType: curr }))).toBeTruthy(); + }); + + it('should return false for allowed non-image mime types', () => { + isImage({ mimeType: imageMimeTypes[0] } as FileJSON); + + // @ts-ignore + expect(textMimeTypes.reduce((acc, curr) => acc && isImage({ mimeType: curr }))).toBeFalsy(); + }); +}); + +describe('parseMimeType', () => { + it('should return Unknown for empty strings', () => { + expect(parseMimeType('')).toBe('Unknown'); + }); + + it('should return Unknown for undefined', () => { + expect(parseMimeType(undefined)).toBe('Unknown'); + }); + + it('should return Unknown for strings starting with forward slash', () => { + expect(parseMimeType('/start')).toBe('Unknown'); + }); + + it('should return Unknown for strings with no forward slash', () => { + expect(parseMimeType('no-slash')).toBe('Unknown'); + }); + + it('should return capitalize first letter for valid strings', () => { + expect(parseMimeType('foo/bar')).toBe('Foo'); + }); +}); From bccf790df68ef7ff5a91d230132d109c60be732d Mon Sep 17 00:00:00 2001 From: adcoelho Date: Mon, 27 Mar 2023 18:04:22 +0200 Subject: [PATCH 23/23] AddFile disabled if user has no create permission. --- .../public/components/files/add_file.test.tsx | 11 ++++++++++ .../public/components/files/add_file.tsx | 4 ++-- .../components/files/file_preview.test.tsx | 20 ------------------- 3 files changed, 13 insertions(+), 22 deletions(-) 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 8734407b91cc7..49fcfd6a67273 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 @@ -16,6 +16,7 @@ import type { AppMockRenderer } from '../../common/mock'; import { constructFileKindIdByOwner } from '../../../common/constants'; import { + buildCasesPermissions, createAppMockRenderer, mockedTestProvidersOwner, mockedFilesClient, @@ -107,6 +108,16 @@ describe('AddFile', () => { expect(await screen.findByTestId('cases-files-add')).toBeInTheDocument(); }); + it('add button disable if user has no creat permission', async () => { + appMockRender = createAppMockRenderer({ + permissions: buildCasesPermissions({ create: false }), + }); + + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-add')).toBeDisabled(); + }); + it('clicking button renders modal', async () => { appMockRender.render(); 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 e6081b3c6a64b..ce2f87719cf88 100644 --- a/x-pack/plugins/cases/public/components/files/add_file.tsx +++ b/x-pack/plugins/cases/public/components/files/add_file.tsx @@ -36,7 +36,7 @@ interface AddFileProps { } const AddFileComponent: React.FC = ({ caseId }) => { - const { owner } = useCasesContext(); + const { owner, permissions } = useCasesContext(); const { client: filesClient } = useFilesContext(); const { showDangerToast, showErrorToast, showSuccessToast } = useCasesToast(); const { isLoading, createAttachments } = useCreateAttachments(); @@ -122,7 +122,7 @@ const AddFileComponent: React.FC = ({ caseId }) => { 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 04322799b46de..48efac8aa64b5 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 @@ -7,7 +7,6 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import type { AppMockRenderer } from '../../common/mock'; @@ -44,23 +43,4 @@ describe('FilePreview', () => { expect(await screen.findByTestId('cases-files-image-preview')).toBeInTheDocument(); }); - - // I cannot get this test to work - it.skip('clicking outside Image triggers closePreview', () => { - const mockClosePreview = jest.fn(); - - appMockRender.render( - <> -
- - - ); - - userEvent.click(screen.getByTestId('outsideClickDummy')); - expect(mockClosePreview).toBeCalled(); - }); });