Skip to content

Commit

Permalink
[SIEM] template timeline UI (#64439)
Browse files Browse the repository at this point in the history
* init template timeline's tab

* add template filter

* add routes for timelines tabs

* add tabs hook

* add filter type

* fix unit test

* add breadcrumbs

* fix types error

* fix flashing table

* fix types

* fix flashing table

* fix filter

* add comments for filters

* review X

* review x

* fix create note for template timeline

* rename timelineTypes to timelineType

* update unit test

* fix types

* update filter for timeline savedObject

* fix lint error

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Patryk Kopycinski <contact@patrykkopycinski.com>
Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
  • Loading branch information
4 people committed May 6, 2020
1 parent daceebb commit a5fe3ce
Show file tree
Hide file tree
Showing 31 changed files with 608 additions and 670 deletions.
6 changes: 5 additions & 1 deletion x-pack/plugins/siem/public/components/link_to/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ export {
export { getOverviewUrl, RedirectToOverviewPage } from './redirect_to_overview';
export { getHostDetailsUrl, getHostsUrl } from './redirect_to_hosts';
export { getNetworkUrl, getIPDetailsUrl, RedirectToNetworkPage } from './redirect_to_network';
export { getTimelinesUrl, RedirectToTimelinesPage } from './redirect_to_timelines';
export {
getTimelinesUrl,
getTimelineTabsUrl,
RedirectToTimelinesPage,
} from './redirect_to_timelines';
export {
getCaseDetailsUrl,
getCaseUrl,
Expand Down
6 changes: 6 additions & 0 deletions x-pack/plugins/siem/public/components/link_to/link_to.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
RedirectToConfigureCasesPage,
} from './redirect_to_case';
import { DetectionEngineTab } from '../../pages/detection_engine/types';
import { TimelineType } from '../../../common/types/timeline';

interface LinkToPageProps {
match: RouteMatch<{}>;
Expand Down Expand Up @@ -112,8 +113,13 @@ export const LinkToPage = React.memo<LinkToPageProps>(({ match }) => (
/>
<Route
component={RedirectToTimelinesPage}
exact
path={`${match.url}/:pageName(${SiemPageName.timelines})`}
/>
<Route
component={RedirectToTimelinesPage}
path={`${match.url}/:pageName(${SiemPageName.timelines})/:tabName(${TimelineType.default}|${TimelineType.template})`}
/>
<Redirect to="/" />
</Switch>
));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,30 @@ import { SiemPageName } from '../../pages/home/types';

import { appendSearch } from './helpers';
import { RedirectWrapper } from './redirect_wrapper';
import { TimelineTypeLiteral, TimelineType } from '../../../common/types/timeline';

export type TimelineComponentProps = RouteComponentProps<{
tabName: TimelineTypeLiteral;
search: string;
}>;

export const RedirectToTimelinesPage = ({ location: { search } }: TimelineComponentProps) => (
<RedirectWrapper to={`/${SiemPageName.timelines}${search}`} />
export const RedirectToTimelinesPage = ({
match: {
params: { tabName },
},
location: { search },
}: TimelineComponentProps) => (
<RedirectWrapper
to={
tabName
? `/${SiemPageName.timelines}/${tabName}${search}`
: `/${SiemPageName.timelines}/${TimelineType.default}${search}`
}
/>
);

export const getTimelinesUrl = (search?: string) =>
`#/link-to/${SiemPageName.timelines}${appendSearch(search)}`;

export const getTimelineTabsUrl = (tabName: TimelineTypeLiteral, search?: string) =>
`#/link-to/${SiemPageName.timelines}/${tabName}${appendSearch(search)}`;
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ describe('Navigation Breadcrumbs', () => {
);
expect(breadcrumbs).toEqual([
{ text: 'SIEM', href: '#/link-to/overview' },
{ text: 'Timelines', href: '' },
{ text: 'Timelines', href: '#/link-to/timelines' },
]);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@ import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/host
import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details';
import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../pages/case/utils';
import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../pages/detection_engine/rules/utils';
import { getBreadcrumbs as getTimelinesBreadcrumbs } from '../../../pages/timelines';
import { SiemPageName } from '../../../pages/home/types';
import { RouteSpyState, HostRouteSpyState, NetworkRouteSpyState } from '../../../utils/route/types';
import {
RouteSpyState,
HostRouteSpyState,
NetworkRouteSpyState,
TimelineRouteSpyState,
} from '../../../utils/route/types';
import { getOverviewUrl } from '../../link_to';

import { TabNavigationProps } from '../tab_navigation/types';
Expand Down Expand Up @@ -44,6 +50,9 @@ const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpySt
const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState =>
spyState != null && spyState.pageName === SiemPageName.hosts;

const isTimelinesRoutes = (spyState: RouteSpyState): spyState is TimelineRouteSpyState =>
spyState != null && spyState.pageName === SiemPageName.timelines;

const isCaseRoutes = (spyState: RouteSpyState): spyState is RouteSpyState =>
spyState != null && spyState.pageName === SiemPageName.case;

Expand Down Expand Up @@ -124,6 +133,24 @@ export const getBreadcrumbsForRoute = (
),
];
}
if (isTimelinesRoutes(spyState) && object.navTabs) {
const tempNav: SearchNavTab = { urlKey: 'timeline', isDetailPage: false };
let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)];
if (spyState.tabName != null) {
urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)];
}

return [
...siemRootBreadcrumb,
...getTimelinesBreadcrumbs(
spyState,
urlStateKeys.reduce(
(acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)],
[]
)
),
];
}
if (
spyState != null &&
object.navTabs &&
Expand Down
57 changes: 33 additions & 24 deletions x-pack/plugins/siem/public/components/open_timeline/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,28 @@ import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines/timelines

import { NotePreviews } from './note_previews';
import { OPEN_TIMELINE_CLASS_NAME } from './helpers';
import { TimelineTabsStyle } from './types';

import { StatefulOpenTimeline } from '.';
import { useGetAllTimeline, getAllTimeline } from '../../containers/timeline/all';
jest.mock('../../lib/kibana');
jest.mock('../../containers/timeline/all', () => {
const originalModule = jest.requireActual('../../containers/timeline/all');
return {
...originalModule,
useGetAllTimeline: jest.fn(),
getAllTimeline: originalModule.getAllTimeline,
};
});
jest.mock('./use_timeline_types', () => {
return {
useTimelineTypes: jest.fn().mockReturnValue({
timelineType: 'default',
timelineTabs: <div data-test-subj="timeline-tab" />,
timelineFilters: <div data-test-subj="timeline-filter" />,
}),
};
});

describe('StatefulOpenTimeline', () => {
const theme = () => ({ eui: euiDarkVars, darkMode: true });
Expand Down Expand Up @@ -489,33 +501,30 @@ describe('StatefulOpenTimeline', () => {
.text()
).toEqual('elastic');
});
});

test('it renders the title', async () => {
const wrapper = mount(
<ThemeProvider theme={theme}>
<TestProviderWithoutDragAndDrop>
<MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}>
<StatefulOpenTimeline
data-test-subj="stateful-timeline"
apolloClient={apolloClient}
isModal={false}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
title={title}
/>
</MockedProvider>
</TestProviderWithoutDragAndDrop>
</ThemeProvider>
);
test('it renders the title', async () => {
const wrapper = mount(
<ThemeProvider theme={theme}>
<TestProviderWithoutDragAndDrop>
<MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}>
<StatefulOpenTimeline
data-test-subj="stateful-timeline"
apolloClient={apolloClient}
isModal={false}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
title={title}
/>
</MockedProvider>
</TestProviderWithoutDragAndDrop>
</ThemeProvider>
);

await wait();
await wait();

expect(
wrapper
.find('[data-test-subj="header-section-title"]')
.first()
.text()
).toEqual(title);
expect(wrapper.find(`[data-test-subj="timeline-${TimelineTabsStyle.tab}"]`).exists()).toEqual(
true
);
});
});

describe('#resetSelectionState', () => {
Expand Down
44 changes: 22 additions & 22 deletions x-pack/plugins/siem/public/components/open_timeline/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import React, { useEffect, useState, useCallback } from 'react';
import { connect, ConnectedProps } from 'react-redux';

import { Dispatch } from 'redux';

import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers';
import { deleteTimelineMutation } from '../../containers/timeline/delete/persist.gql_query';
import { useGetAllTimeline } from '../../containers/timeline/all';
Expand Down Expand Up @@ -40,6 +41,7 @@ import {
OnDeleteOneTimeline,
} from './types';
import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants';
import { useTimelineTypes } from './use_timeline_types';

interface OwnProps<TCache = object> {
apolloClient: ApolloClient<TCache>;
Expand Down Expand Up @@ -103,7 +105,21 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
/** The requested field to sort on */
const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD);

const { fetchAllTimeline, timelines, loading, totalCount, refetch } = useGetAllTimeline();
const { timelineType, timelineTabs, timelineFilters } = useTimelineTypes();
const { fetchAllTimeline, timelines, loading, totalCount } = useGetAllTimeline();

const refetch = useCallback(() => {
fetchAllTimeline({
pageInfo: {
pageIndex: pageIndex + 1,
pageSize,
},
search,
sort: { sortField: sortField as SortFieldTimeline, sortOrder: sortDirection as Direction },
onlyUserFavorite: onlyFavorites,
timelineType,
});
}, [pageIndex, pageSize, search, sortField, sortDirection, timelineType, onlyFavorites]);

/** Invoked when the user presses enters to submit the text in the search input */
const onQueryChange: OnQueryChange = useCallback((query: EuiSearchBarQuery) => {
Expand Down Expand Up @@ -139,6 +155,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
if (timelineIds.includes(timeline.savedObjectId || '')) {
createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false });
}

await apolloClient.mutate<
DeleteTimelineMutation.Mutation,
DeleteTimelineMutation.Variables
Expand Down Expand Up @@ -229,27 +246,8 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
}, []);

useEffect(() => {
fetchAllTimeline({
pageInfo: {
pageIndex: pageIndex + 1,
pageSize,
},
search,
sort: { sortField: sortField as SortFieldTimeline, sortOrder: sortDirection as Direction },
onlyUserFavorite: onlyFavorites,
timelines,
totalCount,
});
}, [
pageIndex,
pageSize,
search,
sortField,
sortDirection,
timelines,
totalCount,
onlyFavorites,
]);
refetch();
}, [refetch]);

return !isModal ? (
<OpenTimeline
Expand Down Expand Up @@ -277,6 +275,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
selectedItems={selectedItems}
sortDirection={sortDirection}
sortField={sortField}
tabs={timelineTabs}
title={title}
totalSearchResultsCount={totalCount}
/>
Expand All @@ -303,6 +302,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
selectedItems={selectedItems}
sortDirection={sortDirection}
sortField={sortField}
tabs={timelineFilters}
title={title}
totalSearchResultsCount={totalCount}
/>
Expand Down
Loading

0 comments on commit a5fe3ce

Please sign in to comment.