diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx index adac26a8ac92bc..e63bce388ae800 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx @@ -26,6 +26,7 @@ const mockLocationWithState = { state: { insertTimeline: { timelineId: 'timeline-id', + timelineSavedObjectId: '34578-3497-5893-47589-34759', timelineTitle: 'Timeline title', }, }, @@ -49,7 +50,7 @@ describe('Insert timeline popover ', () => { payload: { id: 'timeline-id', show: false }, type: 'x-pack/siem/local/timeline/SHOW_TIMELINE', }); - expect(onTimelineChange).toBeCalledWith('Timeline title', 'timeline-id'); + expect(onTimelineChange).toBeCalledWith('Timeline title', '34578-3497-5893-47589-34759'); }); it('should do nothing when router state', () => { jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx index cf1a4ebec9bb66..573e010868babd 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx @@ -23,6 +23,7 @@ interface InsertTimelinePopoverProps { interface RouterState { insertTimeline: { timelineId: string; + timelineSavedObjectId: string; timelineTitle: string; }; } @@ -46,7 +47,7 @@ export const InsertTimelinePopoverComponent: React.FC = ({ ); onTimelineChange( routerState.insertTimeline.timelineTitle, - routerState.insertTimeline.timelineId + routerState.insertTimeline.timelineSavedObjectId ); setRouterState(null); } diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx index 0a2ab5c9d186ad..6f7e1f782d3f67 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx @@ -21,6 +21,7 @@ import React, { useCallback } from 'react'; import uuid from 'uuid'; import styled from 'styled-components'; import { useHistory } from 'react-router-dom'; +import { useSelector } from 'react-redux'; import { Note } from '../../../lib/note'; import { Notes } from '../../notes'; @@ -29,6 +30,8 @@ import { NOTES_PANEL_WIDTH } from './notes_size'; import { ButtonContainer, DescriptionContainer, LabelText, NameField, StyledStar } from './styles'; import * as i18n from './translations'; import { SiemPageName } from '../../../pages/home/types'; +import { timelineSelectors } from '../../../store/timeline'; +import { State } from '../../../store'; export const historyToolTip = 'The chronological history of actions related to this timeline'; export const streamLiveToolTip = 'Update the Timeline as new data arrives'; @@ -121,6 +124,9 @@ interface NewCaseProps { export const NewCase = React.memo(({ onClosePopover, timelineId, timelineTitle }) => { const history = useHistory(); + const { savedObjectId } = useSelector((state: State) => + timelineSelectors.selectTimeline(state, timelineId) + ); const handleClick = useCallback(() => { onClosePopover(); history.push({ @@ -128,6 +134,7 @@ export const NewCase = React.memo(({ onClosePopover, timelineId, t state: { insertTimeline: { timelineId, + timelineSavedObjectId: savedObjectId, timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, }, }, diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.test.tsx index 3629fbc60e4d3e..27b963eb6cb54a 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.test.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.test.tsx @@ -61,6 +61,19 @@ describe('useGetReporters', () => { }); }); + it('refetch reporters', async () => { + const spyOnGetReporters = jest.spyOn(api, 'getReporters'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetReporters() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.fetchReporters(); + expect(spyOnGetReporters).toHaveBeenCalledTimes(2); + }); + }); + it('unhappy path', async () => { const spyOnGetReporters = jest.spyOn(api, 'getReporters'); spyOnGetReporters.mockImplementation(() => { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.test.tsx index 3df83d1c8a596e..2d70c4390e4dd3 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.test.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.test.tsx @@ -5,7 +5,7 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; -import { useGetTags, TagsState } from './use_get_tags'; +import { useGetTags, UseGetTags } from './use_get_tags'; import { tags } from './mock'; import * as api from './api'; @@ -20,12 +20,13 @@ describe('useGetTags', () => { it('init', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useGetTags()); + const { result, waitForNextUpdate } = renderHook(() => useGetTags()); await waitForNextUpdate(); expect(result.current).toEqual({ tags: [], isLoading: true, isError: false, + fetchTags: result.current.fetchTags, }); }); }); @@ -33,7 +34,7 @@ describe('useGetTags', () => { it('calls getTags api', async () => { const spyOnGetTags = jest.spyOn(api, 'getTags'); await act(async () => { - const { waitForNextUpdate } = renderHook(() => useGetTags()); + const { waitForNextUpdate } = renderHook(() => useGetTags()); await waitForNextUpdate(); await waitForNextUpdate(); expect(spyOnGetTags).toBeCalledWith(abortCtrl.signal); @@ -42,17 +43,29 @@ describe('useGetTags', () => { it('fetch tags', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useGetTags()); + const { result, waitForNextUpdate } = renderHook(() => useGetTags()); await waitForNextUpdate(); await waitForNextUpdate(); expect(result.current).toEqual({ tags, isLoading: false, isError: false, + fetchTags: result.current.fetchTags, }); }); }); + it('refetch tags', async () => { + const spyOnGetTags = jest.spyOn(api, 'getTags'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useGetTags()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.fetchTags(); + expect(spyOnGetTags).toHaveBeenCalledTimes(2); + }); + }); + it('unhappy path', async () => { const spyOnGetTags = jest.spyOn(api, 'getTags'); spyOnGetTags.mockImplementation(() => { @@ -60,7 +73,7 @@ describe('useGetTags', () => { }); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useGetTags()); + const { result, waitForNextUpdate } = renderHook(() => useGetTags()); await waitForNextUpdate(); await waitForNextUpdate(); @@ -68,6 +81,7 @@ describe('useGetTags', () => { tags: [], isLoading: false, isError: true, + fetchTags: result.current.fetchTags, }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx index 7c58316ac3fe91..99bb65fa160f7c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx @@ -20,6 +20,10 @@ type Action = | { type: 'FETCH_SUCCESS'; payload: string[] } | { type: 'FETCH_FAILURE' }; +export interface UseGetTags extends TagsState { + fetchTags: () => void; +} + const dataFetchReducer = (state: TagsState, action: Action): TagsState => { switch (action.type) { case 'FETCH_INIT': @@ -47,7 +51,7 @@ const dataFetchReducer = (state: TagsState, action: Action): TagsState => { }; const initialData: string[] = []; -export const useGetTags = (): TagsState => { +export const useGetTags = (): UseGetTags => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: true, isError: false, @@ -55,7 +59,7 @@ export const useGetTags = (): TagsState => { }); const [, dispatchToaster] = useStateToaster(); - useEffect(() => { + const callFetch = () => { let didCancel = false; const abortCtrl = new AbortController(); @@ -82,6 +86,9 @@ export const useGetTags = (): TagsState => { abortCtrl.abort(); didCancel = true; }; + }; + useEffect(() => { + callFetch(); }, []); - return state; + return { ...state, fetchTags: callFetch }; }; 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 c50b7d8c17abcf..a6eca717a82a3e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { EuiBasicTable, EuiButton, @@ -129,13 +129,25 @@ export const AllCases = React.memo(({ userCanCrud }) => { id: '', }); const [deleteBulk, setDeleteBulk] = useState([]); - - const refreshCases = useCallback(() => { - refetchCases(); - fetchCasesStatus(); - setSelectedCases([]); - setDeleteBulk([]); - }, []); + const filterRefetch = useRef<() => void>(); + const setFilterRefetch = useCallback( + (refetchFilter: () => void) => { + filterRefetch.current = refetchFilter; + }, + [filterRefetch.current] + ); + const refreshCases = useCallback( + (dataRefresh = true) => { + if (dataRefresh) refetchCases(); + fetchCasesStatus(); + setSelectedCases([]); + setDeleteBulk([]); + if (filterRefetch.current != null) { + filterRefetch.current(); + } + }, + [filterOptions, queryParams, filterRefetch.current] + ); useEffect(() => { if (isDeleted) { @@ -247,6 +259,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { }; } setQueryParams(newQueryParams); + refreshCases(false); }, [queryParams] ); @@ -259,6 +272,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { setQueryParams({ sortField: SortFieldCase.createdAt }); } setFilters(newFilterOptions); + refreshCases(false); }, [filterOptions, queryParams] ); @@ -347,6 +361,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { tags: filterOptions.tags, status: filterOptions.status, }} + setFilterRefetch={setFilterRefetch} /> {isCasesLoading && isDataEmpty ? (
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx index 615d052347203d..21dcc9732440d3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx @@ -19,17 +19,20 @@ jest.mock('../../../../containers/case/use_get_tags'); const onFilterChanged = jest.fn(); const fetchReporters = jest.fn(); +const fetchTags = jest.fn(); +const setFilterRefetch = jest.fn(); const props = { countClosedCases: 1234, countOpenCases: 99, onFilterChanged, initial: DEFAULT_FILTER_OPTIONS, + setFilterRefetch, }; describe('CasesTableFilters ', () => { beforeEach(() => { jest.resetAllMocks(); - (useGetTags as jest.Mock).mockReturnValue({ tags: ['coke', 'pepsi'] }); + (useGetTags as jest.Mock).mockReturnValue({ tags: ['coke', 'pepsi'], fetchTags }); (useGetReporters as jest.Mock).mockReturnValue({ reporters: ['casetester'], respReporters: [{ username: 'casetester' }], @@ -57,7 +60,7 @@ describe('CasesTableFilters ', () => { .text() ).toEqual('Closed cases (1234)'); }); - it('should call onFilterChange when tags change', () => { + it('should call onFilterChange when selected tags change', () => { const wrapper = mount( @@ -74,7 +77,7 @@ describe('CasesTableFilters ', () => { expect(onFilterChanged).toBeCalledWith({ tags: ['coke'] }); }); - it('should call onFilterChange when reporters change', () => { + it('should call onFilterChange when selected reporters change', () => { const wrapper = mount( @@ -118,4 +121,45 @@ describe('CasesTableFilters ', () => { expect(onFilterChanged).toBeCalledWith({ status: 'closed' }); }); + it('should call on load setFilterRefetch', () => { + mount( + + + + ); + expect(setFilterRefetch).toHaveBeenCalled(); + }); + it('should remove tag from selected tags when tag no longer exists', () => { + const ourProps = { + ...props, + initial: { + ...DEFAULT_FILTER_OPTIONS, + tags: ['pepsi', 'rc'], + }, + }; + mount( + + + + ); + expect(onFilterChanged).toHaveBeenCalledWith({ tags: ['pepsi'] }); + }); + it('should remove reporter from selected reporters when reporter no longer exists', () => { + const ourProps = { + ...props, + initial: { + ...DEFAULT_FILTER_OPTIONS, + reporters: [ + { username: 'casetester', full_name: null, email: null }, + { username: 'batman', full_name: null, email: null }, + ], + }, + }; + mount( + + + + ); + expect(onFilterChanged).toHaveBeenCalledWith({ reporters: [{ username: 'casetester' }] }); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx index da477a56c0a229..901fb133753e82 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { isEqual } from 'lodash/fp'; import { EuiFieldSearch, @@ -25,6 +25,7 @@ interface CasesTableFiltersProps { countOpenCases: number | null; onFilterChanged: (filterOptions: Partial) => void; initial: FilterOptions; + setFilterRefetch: (val: () => void) => void; } /** @@ -41,6 +42,7 @@ const CasesTableFiltersComponent = ({ countOpenCases, onFilterChanged, initial = defaultInitial, + setFilterRefetch, }: CasesTableFiltersProps) => { const [selectedReporters, setSelectedReporters] = useState( initial.reporters.map(r => r.full_name ?? r.username ?? '') @@ -48,8 +50,29 @@ const CasesTableFiltersComponent = ({ const [search, setSearch] = useState(initial.search); const [selectedTags, setSelectedTags] = useState(initial.tags); const [showOpenCases, setShowOpenCases] = useState(initial.status === 'open'); - const { tags } = useGetTags(); - const { reporters, respReporters } = useGetReporters(); + const { tags, fetchTags } = useGetTags(); + const { reporters, respReporters, fetchReporters } = useGetReporters(); + const refetch = useCallback(() => { + fetchTags(); + fetchReporters(); + }, [fetchReporters, fetchTags]); + useEffect(() => { + if (setFilterRefetch != null) { + setFilterRefetch(refetch); + } + }, [refetch, setFilterRefetch]); + useEffect(() => { + if (selectedReporters.length) { + const newReporters = selectedReporters.filter(r => reporters.includes(r)); + handleSelectedReporters(newReporters); + } + }, [reporters]); + useEffect(() => { + if (selectedTags.length) { + const newTags = selectedTags.filter(t => tags.includes(t)); + handleSelectedTags(newTags); + } + }, [tags]); const handleSelectedReporters = useCallback( newReporters => {