Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SIEM] [Cases] Insert timeline and reporters/tags in table bug fixes #63642

Merged
merged 6 commits into from
Apr 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const mockLocationWithState = {
state: {
insertTimeline: {
timelineId: 'timeline-id',
timelineSavedObjectId: '34578-3497-5893-47589-34759',
timelineTitle: 'Timeline title',
},
},
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface InsertTimelinePopoverProps {
interface RouterState {
insertTimeline: {
timelineId: string;
timelineSavedObjectId: string;
timelineTitle: string;
};
}
Expand All @@ -46,7 +47,7 @@ export const InsertTimelinePopoverComponent: React.FC<Props> = ({
);
onTimelineChange(
routerState.insertTimeline.timelineTitle,
routerState.insertTimeline.timelineId
routerState.insertTimeline.timelineSavedObjectId
);
setRouterState(null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -121,13 +124,17 @@ interface NewCaseProps {

export const NewCase = React.memo<NewCaseProps>(({ onClosePopover, timelineId, timelineTitle }) => {
const history = useHistory();
const { savedObjectId } = useSelector((state: State) =>
timelineSelectors.selectTimeline(state, timelineId)
);
const handleClick = useCallback(() => {
onClosePopover();
history.push({
pathname: `/${SiemPageName.case}/create`,
state: {
insertTimeline: {
timelineId,
timelineSavedObjectId: savedObjectId,
timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE,
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@ describe('useGetReporters', () => {
});
});

it('refetch reporters', async () => {
const spyOnGetReporters = jest.spyOn(api, 'getReporters');
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetReporters>(() =>
useGetReporters()
);
await waitForNextUpdate();
await waitForNextUpdate();
result.current.fetchReporters();
expect(spyOnGetReporters).toHaveBeenCalledTimes(2);
});
});

it('unhappy path', async () => {
const spyOnGetReporters = jest.spyOn(api, 'getReporters');
spyOnGetReporters.mockImplementation(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -20,20 +20,21 @@ describe('useGetTags', () => {

it('init', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, TagsState>(() => useGetTags());
const { result, waitForNextUpdate } = renderHook<string, UseGetTags>(() => useGetTags());
await waitForNextUpdate();
expect(result.current).toEqual({
tags: [],
isLoading: true,
isError: false,
fetchTags: result.current.fetchTags,
});
});
});

it('calls getTags api', async () => {
const spyOnGetTags = jest.spyOn(api, 'getTags');
await act(async () => {
const { waitForNextUpdate } = renderHook<string, TagsState>(() => useGetTags());
const { waitForNextUpdate } = renderHook<string, UseGetTags>(() => useGetTags());
await waitForNextUpdate();
await waitForNextUpdate();
expect(spyOnGetTags).toBeCalledWith(abortCtrl.signal);
Expand All @@ -42,32 +43,45 @@ describe('useGetTags', () => {

it('fetch tags', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, TagsState>(() => useGetTags());
const { result, waitForNextUpdate } = renderHook<string, UseGetTags>(() => 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<string, UseGetTags>(() => useGetTags());
await waitForNextUpdate();
await waitForNextUpdate();
result.current.fetchTags();
expect(spyOnGetTags).toHaveBeenCalledTimes(2);
});
});

it('unhappy path', async () => {
const spyOnGetTags = jest.spyOn(api, 'getTags');
spyOnGetTags.mockImplementation(() => {
throw new Error('Something went wrong');
});

await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, TagsState>(() => useGetTags());
const { result, waitForNextUpdate } = renderHook<string, UseGetTags>(() => useGetTags());
await waitForNextUpdate();
await waitForNextUpdate();

expect(result.current).toEqual({
tags: [],
isLoading: false,
isError: true,
fetchTags: result.current.fetchTags,
});
});
});
Expand Down
13 changes: 10 additions & 3 deletions x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down Expand Up @@ -47,15 +51,15 @@ 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,
tags: initialData,
});
const [, dispatchToaster] = useStateToaster();

useEffect(() => {
const callFetch = () => {
let didCancel = false;
const abortCtrl = new AbortController();

Expand All @@ -82,6 +86,9 @@ export const useGetTags = (): TagsState => {
abortCtrl.abort();
didCancel = true;
};
};
useEffect(() => {
callFetch();
}, []);
return state;
return { ...state, fetchTags: callFetch };
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -129,13 +129,25 @@ export const AllCases = React.memo<AllCasesProps>(({ userCanCrud }) => {
id: '',
});
const [deleteBulk, setDeleteBulk] = useState<DeleteCase[]>([]);

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) {
Expand Down Expand Up @@ -247,6 +259,7 @@ export const AllCases = React.memo<AllCasesProps>(({ userCanCrud }) => {
};
}
setQueryParams(newQueryParams);
refreshCases(false);
},
[queryParams]
);
Expand All @@ -259,6 +272,7 @@ export const AllCases = React.memo<AllCasesProps>(({ userCanCrud }) => {
setQueryParams({ sortField: SortFieldCase.createdAt });
}
setFilters(newFilterOptions);
refreshCases(false);
},
[filterOptions, queryParams]
);
Expand Down Expand Up @@ -347,6 +361,7 @@ export const AllCases = React.memo<AllCasesProps>(({ userCanCrud }) => {
tags: filterOptions.tags,
status: filterOptions.status,
}}
setFilterRefetch={setFilterRefetch}
/>
{isCasesLoading && isDataEmpty ? (
<Div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' }],
Expand Down Expand Up @@ -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(
<TestProviders>
<CasesTableFilters {...props} />
Expand All @@ -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(
<TestProviders>
<CasesTableFilters {...props} />
Expand Down Expand Up @@ -118,4 +121,45 @@ describe('CasesTableFilters ', () => {

expect(onFilterChanged).toBeCalledWith({ status: 'closed' });
});
it('should call on load setFilterRefetch', () => {
mount(
<TestProviders>
<CasesTableFilters {...props} />
</TestProviders>
);
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(
<TestProviders>
<CasesTableFilters {...ourProps} />
</TestProviders>
);
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(
<TestProviders>
<CasesTableFilters {...ourProps} />
</TestProviders>
);
expect(onFilterChanged).toHaveBeenCalledWith({ reporters: [{ username: 'casetester' }] });
});
});
Loading