diff --git a/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap index c3ce9a97bbea13..e15ce0ae5f5437 100644 --- a/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap @@ -38,18 +38,18 @@ exports[`Stat Items Component disable charts it renders the default widget 1`] = data-test-subj="stat-item" >

@@ -258,18 +258,18 @@ exports[`Stat Items Component disable charts it renders the default widget 2`] = data-test-subj="stat-item" >

@@ -548,18 +548,18 @@ exports[`Stat Items Component rendering kpis with charts it renders the default data-test-subj="stat-item" >

1,714 @@ -734,10 +734,10 @@ exports[`Stat Items Component rendering kpis with charts it renders the default key="stat-items-field-uniqueDestinationIps" >

2,359 @@ -815,10 +815,10 @@ exports[`Stat Items Component rendering kpis with charts it renders the default >

=> { - const response = await KibanaServices.get().http.fetch(`${CASES_URL}`, { + const response = await KibanaServices.get().http.fetch(CASES_URL, { method: 'POST', body: JSON.stringify(newCase), }); @@ -104,13 +112,21 @@ export const patchCase = async ( updatedCase: Partial, version: string ): Promise => { - const response = await KibanaServices.get().http.fetch(`${CASES_URL}`, { + const response = await KibanaServices.get().http.fetch(CASES_URL, { method: 'PATCH', body: JSON.stringify({ cases: [{ ...updatedCase, id: caseId, version }] }), }); return convertToCamelCase(decodeCasesResponse(response)); }; +export const patchCasesStatus = async (cases: BulkUpdateStatus[]): Promise => { + const response = await KibanaServices.get().http.fetch(CASES_URL, { + method: 'PATCH', + body: JSON.stringify({ cases }), + }); + return convertToCamelCase(decodeCasesResponse(response)); +}; + export const postComment = async (newComment: CommentRequest, caseId: string): Promise => { const response = await KibanaServices.get().http.fetch( `${CASES_URL}/${caseId}/comments`, @@ -139,7 +155,7 @@ export const patchComment = async ( }; export const deleteCases = async (caseIds: string[]): Promise => { - const response = await KibanaServices.get().http.fetch(`${CASES_URL}`, { + const response = await KibanaServices.get().http.fetch(CASES_URL, { method: 'DELETE', query: { ids: JSON.stringify(caseIds) }, }); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index 5b6ff8438be8c9..44519031e91cb7 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -78,3 +78,9 @@ export interface FetchCasesProps { export interface ApiProps { signal: AbortSignal; } + +export interface BulkUpdateStatus { + status: string; + id: string; + version: string; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx new file mode 100644 index 00000000000000..77d779ab906cf7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useReducer } from 'react'; +import { errorToToaster, useStateToaster } from '../../components/toasters'; +import * as i18n from './translations'; +import { patchCasesStatus } from './api'; +import { BulkUpdateStatus, Case } from './types'; + +interface UpdateState { + isUpdated: boolean; + isLoading: boolean; + isError: boolean; +} +type Action = + | { type: 'FETCH_INIT' } + | { type: 'FETCH_SUCCESS'; payload: boolean } + | { type: 'FETCH_FAILURE' } + | { type: 'RESET_IS_UPDATED' }; + +const dataFetchReducer = (state: UpdateState, action: Action): UpdateState => { + switch (action.type) { + case 'FETCH_INIT': + return { + ...state, + isLoading: true, + isError: false, + }; + case 'FETCH_SUCCESS': + return { + ...state, + isLoading: false, + isError: false, + isUpdated: action.payload, + }; + case 'FETCH_FAILURE': + return { + ...state, + isLoading: false, + isError: true, + }; + case 'RESET_IS_UPDATED': + return { + ...state, + isUpdated: false, + }; + default: + return state; + } +}; +interface UseUpdateCase extends UpdateState { + updateBulkStatus: (cases: Case[], status: string) => void; + dispatchResetIsUpdated: () => void; +} + +export const useUpdateCases = (): UseUpdateCase => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + isUpdated: false, + }); + const [, dispatchToaster] = useStateToaster(); + + const dispatchUpdateCases = useCallback((cases: BulkUpdateStatus[]) => { + let cancel = false; + const patchData = async () => { + try { + dispatch({ type: 'FETCH_INIT' }); + await patchCasesStatus(cases); + if (!cancel) { + dispatch({ type: 'FETCH_SUCCESS', payload: true }); + } + } catch (error) { + if (!cancel) { + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + dispatch({ type: 'FETCH_FAILURE' }); + } + } + }; + patchData(); + return () => { + cancel = true; + }; + }, []); + + const dispatchResetIsUpdated = useCallback(() => { + dispatch({ type: 'RESET_IS_UPDATED' }); + }, []); + + const updateBulkStatus = useCallback((cases: Case[], status: string) => { + const updateCasesStatus: BulkUpdateStatus[] = cases.map(theCase => ({ + status, + id: theCase.id, + version: theCase.version, + })); + dispatchUpdateCases(updateCasesStatus); + }, []); + return { ...state, updateBulkStatus, dispatchResetIsUpdated }; +}; diff --git a/x-pack/legacy/plugins/siem/public/legacy.ts b/x-pack/legacy/plugins/siem/public/legacy.ts index 157ec54353a3e0..b3a06a170bb807 100644 --- a/x-pack/legacy/plugins/siem/public/legacy.ts +++ b/x-pack/legacy/plugins/siem/public/legacy.ts @@ -5,19 +5,12 @@ */ import { npSetup, npStart } from 'ui/new_platform'; -import { PluginsSetup, PluginsStart } from 'ui/new_platform/new_platform'; import { PluginInitializerContext } from '../../../../../src/core/public'; import { plugin } from './'; -import { - TriggersAndActionsUIPublicPluginSetup, - TriggersAndActionsUIPublicPluginStart, -} from '../../../../plugins/triggers_actions_ui/public'; +import { SetupPlugins, StartPlugins } from './plugin'; const pluginInstance = plugin({} as PluginInitializerContext); -type myPluginsSetup = PluginsSetup & { triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup }; -type myPluginsStart = PluginsStart & { triggers_actions_ui: TriggersAndActionsUIPublicPluginStart }; - -pluginInstance.setup(npSetup.core, npSetup.plugins as myPluginsSetup); -pluginInstance.start(npStart.core, npStart.plugins as myPluginsStart); +pluginInstance.setup(npSetup.core, (npSetup.plugins as unknown) as SetupPlugins); +pluginInstance.start(npStart.core, (npStart.plugins as unknown) as StartPlugins); diff --git a/x-pack/legacy/plugins/siem/public/lib/kibana/hooks.ts b/x-pack/legacy/plugins/siem/public/lib/kibana/hooks.ts index a4a70c77833c05..95ecee7b12bb11 100644 --- a/x-pack/legacy/plugins/siem/public/lib/kibana/hooks.ts +++ b/x-pack/legacy/plugins/siem/public/lib/kibana/hooks.ts @@ -6,8 +6,13 @@ import moment from 'moment-timezone'; +import { useCallback, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../common/constants'; import { useUiSetting, useKibana } from './kibana_react'; +import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { AuthenticatedUser } from '../../../../../../plugins/security/common/model'; +import { convertToCamelCase } from '../../containers/case/utils'; export const useDateFormat = (): string => useUiSetting(DEFAULT_DATE_FORMAT); @@ -17,3 +22,62 @@ export const useTimeZone = (): string => { }; export const useBasePath = (): string => useKibana().services.http.basePath.get(); + +interface UserRealm { + name: string; + type: string; +} + +export interface AuthenticatedElasticUser { + username: string; + email: string; + fullName: string; + roles: string[]; + enabled: boolean; + metadata?: { + _reserved: boolean; + }; + authenticationRealm: UserRealm; + lookupRealm: UserRealm; + authenticationProvider: string; +} + +export const useCurrentUser = (): AuthenticatedElasticUser | null => { + const [user, setUser] = useState(null); + + const [, dispatchToaster] = useStateToaster(); + + const { security } = useKibana().services; + + const fetchUser = useCallback(() => { + let didCancel = false; + const fetchData = async () => { + try { + const response = await security.authc.getCurrentUser(); + if (!didCancel) { + setUser(convertToCamelCase(response)); + } + } catch (error) { + if (!didCancel) { + errorToToaster({ + title: i18n.translate('xpack.siem.getCurrentUser.Error', { + defaultMessage: 'Error getting user', + }), + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + setUser(null); + } + } + }; + fetchData(); + return () => { + didCancel = true; + }; + }, [security]); + + useEffect(() => { + fetchUser(); + }, []); + return user; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx index 5d00b770b3ca9c..48fbb4e74c4072 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx @@ -10,7 +10,7 @@ import { UseGetCasesState } from '../../../../../containers/case/use_get_cases'; export const useGetCasesMockState: UseGetCasesState = { data: { countClosedCases: 0, - countOpenCases: 0, + countOpenCases: 5, cases: [ { closedAt: null, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx index 001acc1d4d36ed..13869c79c45fd8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx @@ -10,35 +10,86 @@ import moment from 'moment-timezone'; import { AllCases } from './'; import { TestProviders } from '../../../../mock'; import { useGetCasesMockState } from './__mock__'; -import * as apiHook from '../../../../containers/case/use_get_cases'; -import { act } from '@testing-library/react'; -import { wait } from '../../../../lib/helpers'; +import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; +import { useGetCases } from '../../../../containers/case/use_get_cases'; +import { useGetCasesStatus } from '../../../../containers/case/use_get_cases_status'; +import { useUpdateCases } from '../../../../containers/case/use_bulk_update_case'; +jest.mock('../../../../containers/case/use_bulk_update_case'); +jest.mock('../../../../containers/case/use_delete_cases'); +jest.mock('../../../../containers/case/use_get_cases'); +jest.mock('../../../../containers/case/use_get_cases_status'); +const useDeleteCasesMock = useDeleteCases as jest.Mock; +const useGetCasesMock = useGetCases as jest.Mock; +const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; +const useUpdateCasesMock = useUpdateCases as jest.Mock; describe('AllCases', () => { + const dispatchResetIsDeleted = jest.fn(); + const dispatchResetIsUpdated = jest.fn(); const dispatchUpdateCaseProperty = jest.fn(); + const handleOnDeleteConfirm = jest.fn(); + const handleToggleModal = jest.fn(); const refetchCases = jest.fn(); const setFilters = jest.fn(); const setQueryParams = jest.fn(); const setSelectedCases = jest.fn(); + const updateBulkStatus = jest.fn(); + const fetchCasesStatus = jest.fn(); + + const defaultGetCases = { + ...useGetCasesMockState, + dispatchUpdateCaseProperty, + refetchCases, + setFilters, + setQueryParams, + setSelectedCases, + }; + const defaultDeleteCases = { + dispatchResetIsDeleted, + handleOnDeleteConfirm, + handleToggleModal, + isDeleted: false, + isDisplayConfirmDeleteModal: false, + isLoading: false, + }; + const defaultCasesStatus = { + countClosedCases: 0, + countOpenCases: 5, + fetchCasesStatus, + isError: false, + isLoading: true, + }; + const defaultUpdateCases = { + isUpdated: false, + isLoading: false, + isError: false, + dispatchResetIsUpdated, + updateBulkStatus, + }; + /* eslint-disable no-console */ + // Silence until enzyme fixed to use ReactTestUtils.act() + const originalError = console.error; + beforeAll(() => { + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + }); + /* eslint-enable no-console */ beforeEach(() => { jest.resetAllMocks(); - jest.spyOn(apiHook, 'useGetCases').mockReturnValue({ - ...useGetCasesMockState, - dispatchUpdateCaseProperty, - refetchCases, - setFilters, - setQueryParams, - setSelectedCases, - }); + useUpdateCasesMock.mockImplementation(() => defaultUpdateCases); + useGetCasesMock.mockImplementation(() => defaultGetCases); + useDeleteCasesMock.mockImplementation(() => defaultDeleteCases); + useGetCasesStatusMock.mockImplementation(() => defaultCasesStatus); moment.tz.setDefault('UTC'); }); - it('should render AllCases', async () => { + it('should render AllCases', () => { const wrapper = mount( ); - await act(() => wait()); expect( wrapper .find(`a[data-test-subj="case-details-link"]`) @@ -76,13 +127,12 @@ describe('AllCases', () => { .text() ).toEqual('Showing 10 cases'); }); - it('should tableHeaderSortButton AllCases', async () => { + it('should tableHeaderSortButton AllCases', () => { const wrapper = mount( ); - await act(() => wait()); wrapper .find('[data-test-subj="tableHeaderSortButton"]') .first() @@ -94,4 +144,139 @@ describe('AllCases', () => { sortOrder: 'asc', }); }); + it('closes case when row action icon clicked', () => { + const wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="action-close"]') + .first() + .simulate('click'); + const firstCase = useGetCasesMockState.data.cases[0]; + expect(dispatchUpdateCaseProperty).toBeCalledWith({ + caseId: firstCase.id, + updateKey: 'status', + updateValue: 'closed', + refetchCasesStatus: fetchCasesStatus, + version: firstCase.version, + }); + }); + it('Bulk delete', () => { + useGetCasesMock.mockImplementation(() => ({ + ...defaultGetCases, + selectedCases: useGetCasesMockState.data.cases, + })); + useDeleteCasesMock + .mockReturnValueOnce({ + ...defaultDeleteCases, + isDisplayConfirmDeleteModal: false, + }) + .mockReturnValue({ + ...defaultDeleteCases, + isDisplayConfirmDeleteModal: true, + }); + + const wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="case-table-bulk-actions"] button') + .first() + .simulate('click'); + wrapper + .find('[data-test-subj="cases-bulk-delete-button"]') + .first() + .simulate('click'); + expect(handleToggleModal).toBeCalled(); + + wrapper + .find( + '[data-test-subj="confirm-delete-case-modal"] [data-test-subj="confirmModalConfirmButton"]' + ) + .last() + .simulate('click'); + expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual( + useGetCasesMockState.data.cases.map(theCase => theCase.id) + ); + }); + it('Bulk close status update', () => { + useGetCasesMock.mockImplementation(() => ({ + ...defaultGetCases, + selectedCases: useGetCasesMockState.data.cases, + })); + + const wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="case-table-bulk-actions"] button') + .first() + .simulate('click'); + wrapper + .find('[data-test-subj="cases-bulk-close-button"]') + .first() + .simulate('click'); + expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, 'closed'); + }); + it('Bulk open status update', () => { + useGetCasesMock.mockImplementation(() => ({ + ...defaultGetCases, + selectedCases: useGetCasesMockState.data.cases, + filterOptions: { + ...defaultGetCases.filterOptions, + status: 'closed', + }, + })); + + const wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="case-table-bulk-actions"] button') + .first() + .simulate('click'); + wrapper + .find('[data-test-subj="cases-bulk-open-button"]') + .first() + .simulate('click'); + expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, 'open'); + }); + it('isDeleted is true, refetch', () => { + useDeleteCasesMock.mockImplementation(() => ({ + ...defaultDeleteCases, + isDeleted: true, + })); + + mount( + + + + ); + expect(refetchCases).toBeCalled(); + expect(fetchCasesStatus).toBeCalled(); + expect(dispatchResetIsDeleted).toBeCalled(); + }); + it('isUpdated is true, refetch', () => { + useUpdateCasesMock.mockImplementation(() => ({ + ...defaultUpdateCases, + isUpdated: true, + })); + + mount( + + + + ); + expect(refetchCases).toBeCalled(); + expect(fetchCasesStatus).toBeCalled(); + expect(dispatchResetIsUpdated).toBeCalled(); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 9a84dd07b0af44..e7e1e624ccba2d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -43,6 +43,7 @@ import { OpenClosedStats } from '../open_closed_stats'; import { getActions } from './actions'; import { CasesTableFilters } from './table_filters'; +import { useUpdateCases } from '../../../../containers/case/use_bulk_update_case'; const CONFIGURE_CASES_URL = getConfigureCasesUrl(); const CREATE_CASE_URL = getCreateCaseUrl(); @@ -106,13 +107,20 @@ export const AllCases = React.memo(() => { isDisplayConfirmDeleteModal, } = useDeleteCases(); + const { dispatchResetIsUpdated, isUpdated, updateBulkStatus } = useUpdateCases(); + useEffect(() => { if (isDeleted) { refetchCases(filterOptions, queryParams); fetchCasesStatus(); dispatchResetIsDeleted(); } - }, [isDeleted, filterOptions, queryParams]); + if (isUpdated) { + refetchCases(filterOptions, queryParams); + fetchCasesStatus(); + dispatchResetIsUpdated(); + } + }, [isDeleted, isUpdated, filterOptions, queryParams]); const [deleteThisCase, setDeleteThisCase] = useState({ title: '', @@ -135,36 +143,38 @@ export const AllCases = React.memo(() => { [deleteBulk, deleteThisCase, isDisplayConfirmDeleteModal] ); - const toggleDeleteModal = useCallback( - (deleteCase: Case) => { - handleToggleModal(); - setDeleteThisCase(deleteCase); - }, - [isDisplayConfirmDeleteModal] - ); + const toggleDeleteModal = useCallback((deleteCase: Case) => { + handleToggleModal(); + setDeleteThisCase(deleteCase); + }, []); + + const toggleBulkDeleteModal = useCallback((deleteCases: string[]) => { + handleToggleModal(); + setDeleteBulk(deleteCases); + }, []); - const toggleBulkDeleteModal = useCallback( - (deleteCases: string[]) => { - handleToggleModal(); - setDeleteBulk(deleteCases); + const handleUpdateCaseStatus = useCallback( + (status: string) => { + updateBulkStatus(selectedCases, status); }, - [isDisplayConfirmDeleteModal] + [selectedCases] ); const selectedCaseIds = useMemo( - (): string[] => - selectedCases.reduce((arr: string[], caseObj: Case) => [...arr, caseObj.id], []), + (): string[] => selectedCases.map((caseObj: Case) => caseObj.id), [selectedCases] ); const getBulkItemsPopoverContent = useCallback( (closePopover: () => void) => ( ), @@ -322,7 +332,7 @@ export const AllCases = React.memo(() => { void; deleteCasesAction: (cases: string[]) => void; selectedCaseIds: string[]; - caseStatus: string; + updateCaseStatus: (status: string) => void; } export const getBulkItems = ({ - deleteCasesAction, - closePopover, caseStatus, + closePopover, + deleteCasesAction, selectedCaseIds, + updateCaseStatus, }: GetBulkItems) => { return [ caseStatus === 'open' ? ( { + onClick={() => { closePopover(); + updateCaseStatus('closed'); }} > {i18n.BULK_ACTION_CLOSE_SELECTED} ) : ( { closePopover(); + updateCaseStatus('open'); }} > {i18n.BULK_ACTION_OPEN_SELECTED} ), { + onClick={() => { closePopover(); deleteCasesAction(selectedCaseIds); }} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/translations.ts index 0bf213868bd765..97045c8ebaf8b9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/translations.ts @@ -16,7 +16,7 @@ export const BULK_ACTION_CLOSE_SELECTED = i18n.translate( export const BULK_ACTION_OPEN_SELECTED = i18n.translate( 'xpack.siem.case.caseTable.bulkActions.openSelectedTitle', { - defaultMessage: 'Open selected', + defaultMessage: 'Reopen selected', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx index ec18bdb2bf9abe..41100ec6d50f14 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx @@ -5,6 +5,7 @@ */ import React from 'react'; +import { Router } from 'react-router-dom'; import { mount } from 'enzyme'; import { CaseComponent } from './'; import { caseProps, caseClosedProps, data, dataClosed } from './__mock__'; @@ -12,6 +13,27 @@ import { TestProviders } from '../../../../mock'; import { useUpdateCase } from '../../../../containers/case/use_update_case'; jest.mock('../../../../containers/case/use_update_case'); const useUpdateCaseMock = useUpdateCase as jest.Mock; +type Action = 'PUSH' | 'POP' | 'REPLACE'; +const pop: Action = 'POP'; +const location = { + pathname: '/network', + search: '', + state: '', + hash: '', +}; +const mockHistory = { + length: 2, + location, + action: pop, + push: jest.fn(), + replace: jest.fn(), + go: jest.fn(), + goBack: jest.fn(), + goForward: jest.fn(), + block: jest.fn(), + createHref: jest.fn(), + listen: jest.fn(), +}; describe('CaseView ', () => { const updateCaseProperty = jest.fn(); @@ -42,7 +64,9 @@ describe('CaseView ', () => { it('should render CaseComponent', () => { const wrapper = mount( - + + + ); expect( @@ -83,6 +107,7 @@ describe('CaseView ', () => { .prop('raw') ).toEqual(data.description); }); + it('should show closed indicators in header when case is closed', () => { useUpdateCaseMock.mockImplementation(() => ({ ...defaultUpdateCaseState, @@ -90,7 +115,9 @@ describe('CaseView ', () => { })); const wrapper = mount( - + + + ); expect(wrapper.contains(`[data-test-subj="case-view-createdAt"]`)).toBe(false); @@ -111,7 +138,9 @@ describe('CaseView ', () => { it('should dispatch update state when button is toggled', () => { const wrapper = mount( - + + + ); @@ -128,7 +157,9 @@ describe('CaseView ', () => { it('should render comments', () => { const wrapper = mount( - + + + ); expect( diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index dce7bde2225c92..08af603cb0dbfc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -23,6 +23,7 @@ import { getTypedPayload } from '../../../../containers/case/utils'; import { WhitePageWrapper } from '../wrappers'; import { useBasePath } from '../../../../lib/kibana'; import { CaseStatus } from '../case_status'; +import { SpyRoute } from '../../../../utils/route/spy_routes'; interface Props { caseId: string; @@ -93,6 +94,8 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => const onSubmitTitle = useCallback(newTitle => onUpdateField('title', newTitle), [onUpdateField]); const toggleStatusCase = useCallback(status => onUpdateField('status', status), [onUpdateField]); + const spyState = useMemo(() => ({ caseTitle: caseData.title }), [caseData.title]); + const caseStatusData = useMemo( () => caseData.status === 'open' @@ -179,6 +182,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => + ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx index cebc66a0c83631..04697e63b74513 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx @@ -12,6 +12,7 @@ import { useUpdateComment } from '../../../../containers/case/use_update_comment import { UserActionItem } from './user_action_item'; import { UserActionMarkdown } from './user_action_markdown'; import { AddComment } from '../add_comment'; +import { useCurrentUser } from '../../../../lib/kibana'; export interface UserActionTreeProps { data: Case; @@ -20,14 +21,14 @@ export interface UserActionTreeProps { } const DescriptionId = 'description'; -const NewId = 'newComent'; +const NewId = 'newComment'; export const UserActionTree = React.memo( ({ data: caseData, onUpdateField, isLoadingDescription }: UserActionTreeProps) => { const { comments, isLoadingIds, updateComment, addPostedComment } = useUpdateComment( caseData.comments ); - + const currentUser = useCurrentUser(); const [manageMarkdownEditIds, setManangeMardownEditIds] = useState([]); const handleManageMarkdownEditId = useCallback( @@ -112,10 +113,10 @@ export const UserActionTree = React.memo( id={NewId} isEditable={true} isLoading={isLoadingIds.includes(NewId)} - fullName="to be determined" + fullName={currentUser != null ? currentUser.fullName : ''} markdown={MarkdownNewComment} onEdit={handleManageMarkdownEditId.bind(null, NewId)} - userName="to be determined" + userName={currentUser != null ? currentUser.username : ''} /> ); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx index 0a33301010535e..7b99f2ef76ab3b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiPanel } from '@elastic/eui'; import React from 'react'; import styled, { css } from 'styled-components'; @@ -48,6 +48,12 @@ const UserActionItemContainer = styled(EuiFlexGroup)` margin-right: ${theme.eui.euiSize}; vertical-align: top; } + .userAction_loadingAvatar { + position: relative; + margin-right: ${theme.eui.euiSizeXL}; + top: ${theme.eui.euiSizeM}; + left: ${theme.eui.euiSizeS}; + } .userAction__title { padding: ${theme.eui.euiSizeS} ${theme.eui.euiSizeL}; background: ${theme.eui.euiColorLightestShade}; @@ -74,7 +80,11 @@ export const UserActionItem = ({ }: UserActionItemProps) => ( - + {fullName.length > 0 || userName.length > 0 ? ( + + ) : ( + + )} {isEditable && markdown} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/utils.ts b/x-pack/legacy/plugins/siem/public/pages/case/utils.ts index bd6cb5da5eb01a..ccb3b71a476ec4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/utils.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/utils.ts @@ -28,7 +28,7 @@ export const getBreadcrumbs = (params: RouteSpyState): Breadcrumb[] => { breadcrumb = [ ...breadcrumb, { - text: params.detailName, + text: params.state?.caseTitle ?? '', href: getCaseDetailsUrl(params.detailName), }, ]; diff --git a/x-pack/legacy/plugins/siem/public/plugin.tsx b/x-pack/legacy/plugins/siem/public/plugin.tsx index 71fa3a54df7689..da4aad97e5b485 100644 --- a/x-pack/legacy/plugins/siem/public/plugin.tsx +++ b/x-pack/legacy/plugins/siem/public/plugin.tsx @@ -27,21 +27,24 @@ import { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, } from '../../../../plugins/triggers_actions_ui/public'; +import { SecurityPluginSetup } from '../../../../plugins/security/public'; export { AppMountParameters, CoreSetup, CoreStart, PluginInitializerContext }; export interface SetupPlugins { home: HomePublicPluginSetup; - usageCollection: UsageCollectionSetup; + security: SecurityPluginSetup; triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; + usageCollection: UsageCollectionSetup; } export interface StartPlugins { data: DataPublicPluginStart; embeddable: EmbeddableStart; inspector: InspectorStart; newsfeed?: NewsfeedStart; - uiActions: UiActionsStart; + security: SecurityPluginSetup; triggers_actions_ui: TriggersAndActionsUIPublicPluginStart; + uiActions: UiActionsStart; } export type StartServices = CoreStart & StartPlugins; @@ -61,6 +64,8 @@ export class Plugin implements IPlugin { public setup(core: CoreSetup, plugins: SetupPlugins) { initTelemetry(plugins.usageCollection, this.id); + const security = plugins.security; + core.application.register({ id: this.id, title: this.name, @@ -69,8 +74,7 @@ export class Plugin implements IPlugin { const { renderApp } = await import('./app'); plugins.triggers_actions_ui.actionTypeRegistry.register(serviceNowActionType()); - - return renderApp(coreStart, startPlugins as StartPlugins, params); + return renderApp(coreStart, { ...startPlugins, security } as StartPlugins, params); }, }); diff --git a/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx b/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx index ddee2359b28ba4..9030e2713548bd 100644 --- a/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx +++ b/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx @@ -39,12 +39,13 @@ export const SpyRouteComponent = memo( dispatch({ type: 'updateRouteWithOutSearch', route: { - pageName, detailName, - tabName, - pathName: pathname, - history, flowTarget, + history, + pageName, + pathName: pathname, + state, + tabName, }, }); setIsInitializing(false); @@ -52,13 +53,14 @@ export const SpyRouteComponent = memo( dispatch({ type: 'updateRoute', route: { - pageName, detailName, - tabName, - search, - pathName: pathname, - history, flowTarget, + history, + pageName, + pathName: pathname, + search, + state, + tabName, }, }); } @@ -67,14 +69,14 @@ export const SpyRouteComponent = memo( dispatch({ type: 'updateRoute', route: { - pageName, detailName, - tabName, - search, - pathName: pathname, - history, flowTarget, + history, + pageName, + pathName: pathname, + search, state, + tabName, }, }); }