From 21c18f07fcd69440faa8c66c830a1468f6cfdbb7 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Tue, 14 Apr 2020 16:08:51 +0100 Subject: [PATCH 01/21] init template timeline's tab --- .../open_timeline/open_timeline.tsx | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx index 6b2f953b82de42..397d3a1bcd3344 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx @@ -26,6 +26,7 @@ import { import { useEditTimelinBatchActions } from './edit_timeline_batch_actions'; import { useEditTimelineActions } from './edit_timeline_actions'; import { EditOneTimelineAction } from './export_timeline'; +import { EuiTab } from '@elastic/eui'; export const OpenTimeline = React.memo( ({ @@ -116,6 +117,38 @@ export const OpenTimeline = React.memo( } }, [setImportDataModalToggle, refetch]); + const tabs = [ + { + id: 'timeline', + name: 'timeline', + disabled: false, + }, + { + id: 'template', + name: 'template', + disabled: false, + }, + ]; + + const onSelectedTabChanged = id => { + this.setState({ + selectedTabId: id, + }); + }; + + const renderTabs = () => { + return tabs.map((tab, index) => ( + this.onSelectedTabChanged(tab.id)} + isSelected={tab.id === this.state.selectedTabId} + disabled={tab.disabled} + key={index} + > + {tab.name} + + )); + }; + return ( <> ( /> + {renderTabs()} + Date: Fri, 24 Apr 2020 16:37:04 +0100 Subject: [PATCH 02/21] add template filter --- .../public/components/open_timeline/index.tsx | 185 +++++++++++------- .../open_timeline/open_timeline.tsx | 62 +----- .../open_timeline_modal_body.tsx | 21 +- .../components/open_timeline/translations.ts | 8 + .../public/components/open_timeline/types.ts | 2 + .../components/recent_timelines/index.tsx | 1 + .../timeline/selectable_timeline/index.tsx | 4 +- .../timeline/all/index.gql_query.ts | 2 + .../public/containers/timeline/all/index.tsx | 3 + .../siem/public/graphql/introspection.json | 6 + .../plugins/siem/public/graphql/types.ts | 3 + .../siem/server/graphql/timeline/resolvers.ts | 1 + .../server/graphql/timeline/schema.gql.ts | 2 +- x-pack/plugins/siem/server/graphql/types.ts | 4 + .../siem/server/lib/timeline/saved_object.ts | 5 + 15 files changed, 178 insertions(+), 131 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx index c27a6039da29d9..0ce4c51b6da86e 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx @@ -9,6 +9,7 @@ import React, { useEffect, useState, useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; +import { EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui'; import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; import { deleteTimelineMutation } from '../../containers/timeline/delete/persist.gql_query'; import { AllTimelinesVariables, AllTimelinesQuery } from '../../containers/timeline/all'; @@ -41,6 +42,7 @@ import { OnDeleteOneTimeline, } from './types'; import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants'; +import * as i18n from './translations'; interface OwnProps { apolloClient: ApolloClient; @@ -68,6 +70,19 @@ export const getSelectedTimelineIds = (selectedItems: OpenTimelineResult[]): str [] ); +const tabs: Array<{ id: 'default' | 'template'; name: string; disabled: boolean }> = [ + { + id: 'default', + name: i18n.TAB_TIMELINES, + disabled: false, + }, + { + id: 'template', + name: i18n.TAB_TEMPLATES, + disabled: false, + }, +]; + /** Manages the state (e.g table selection) of the (pure) `OpenTimeline` component */ export const StatefulOpenTimelineComponent = React.memo( ({ @@ -91,6 +106,8 @@ export const StatefulOpenTimelineComponent = React.memo( >({}); /** Only query for favorite timelines when true */ const [onlyFavorites, setOnlyFavorites] = useState(false); + + const [timelineTypes, setTimelineTypes] = useState<'default' | 'template'>('default'); /** The requested page of results */ const [pageIndex, setPageIndex] = useState(0); /** The requested size of each page of search results */ @@ -104,6 +121,28 @@ export const StatefulOpenTimelineComponent = React.memo( /** The requested field to sort on */ const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); + const renderTabs = useCallback(() => { + return ( + <> + + {tabs.map((tab, index) => ( + { + setTimelineTypes(tab.id); + }, [setTimelineTypes, tab])} + isSelected={tab.id === timelineTypes} + disabled={tab.disabled} + key={index} + > + {tab.name} + + ))} + + + + ); + }, [tabs, timelineTypes]); + /** Invoked when the user presses enters to submit the text in the search input */ const onQueryChange: OnQueryChange = useCallback((query: EuiSearchBarQuery) => { setSearch(query.queryText.trim()); @@ -146,6 +185,7 @@ export const StatefulOpenTimelineComponent = React.memo( sortOrder: sortDirection as Direction, }, onlyUserFavorite: onlyFavorites, + timelineTypes, }); }, [search, pageIndex, pageSize, sortField, sortDirection, onlyFavorites] @@ -164,6 +204,7 @@ export const StatefulOpenTimelineComponent = React.memo( sortOrder: sortDirection as Direction, }, onlyUserFavorite: onlyFavorites, + timelineTypes, }); // NOTE: we clear the selection state below, but if the server fails to @@ -252,74 +293,82 @@ export const StatefulOpenTimelineComponent = React.memo( }, []); return ( - - {({ timelines, loading, totalCount, refetch }) => { - return !isModal ? ( - - ) : ( - - ); - }} - + <> + + {({ timelines, loading, totalCount, refetch }) => { + return !isModal ? ( + + ) : ( + + ); + }} + + ); } ); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx index 397d3a1bcd3344..d4704361c3c4c6 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx @@ -11,7 +11,6 @@ import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; import { OpenTimelineProps, OpenTimelineResult } from './types'; import { SearchRow } from './search_row'; import { TimelinesTable } from './timelines_table'; -import { TitleRow } from './title_row'; import { ImportDataModal } from '../import_data_modal'; import * as i18n from './translations'; import { importTimelines } from '../../containers/timeline/all/api'; @@ -26,7 +25,6 @@ import { import { useEditTimelinBatchActions } from './edit_timeline_batch_actions'; import { useEditTimelineActions } from './edit_timeline_actions'; import { EditOneTimelineAction } from './export_timeline'; -import { EuiTab } from '@elastic/eui'; export const OpenTimeline = React.memo( ({ @@ -53,7 +51,7 @@ export const OpenTimeline = React.memo( sortDirection, setImportDataModalToggle, sortField, - title, + tabs, totalSearchResultsCount, }) => { const tableRef = useRef>(); @@ -117,38 +115,6 @@ export const OpenTimeline = React.memo( } }, [setImportDataModalToggle, refetch]); - const tabs = [ - { - id: 'timeline', - name: 'timeline', - disabled: false, - }, - { - id: 'template', - name: 'template', - disabled: false, - }, - ]; - - const onSelectedTabChanged = id => { - this.setState({ - selectedTabId: id, - }); - }; - - const renderTabs = () => { - return tabs.map((tab, index) => ( - this.onSelectedTabChanged(tab.id)} - isSelected={tab.id === this.state.selectedTabId} - disabled={tab.disabled} - key={index} - > - {tab.name} - - )); - }; - return ( <> ( /> - {renderTabs()} - - - - + {tabs()} + diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx index 60ebf2118d556f..6dae5b9a8a9340 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx @@ -42,6 +42,7 @@ export const OpenTimelineModalBody = memo( selectedItems, sortDirection, sortField, + tabs, title, totalSearchResultsCount, }) => { @@ -62,15 +63,17 @@ export const OpenTimelineModalBody = memo( selectedTimelinesCount={selectedItems.length} title={title} /> - - + <> + {tabs()} + + diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/translations.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/translations.ts index 7914e368166dbe..81c4835c11976e 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/translations.ts @@ -146,6 +146,14 @@ export const SUCCESSFULLY_EXPORTED_TIMELINES = (totalTimelines: number) => 'Successfully exported {totalTimelines, plural, =0 {all timelines} =1 {{totalTimelines} timeline} other {{totalTimelines} timelines}}', }); +export const TAB_TIMELINES = i18n.translate('xpack.siem.timelines.components.tabs.timelinesTitle', { + defaultMessage: 'Timelines', +}); + +export const TAB_TEMPLATES = i18n.translate('xpack.siem.timelines.components.tabs.templatesTitle', { + defaultMessage: 'Templates', +}); + export const IMPORT_TIMELINE_BTN_TITLE = i18n.translate( 'xpack.siem.timelines.components.importTimelineModal.importTimelineTitle', { diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts index b7cc92ebd183f1..0ae3ef28de71dd 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts @@ -158,6 +158,8 @@ export interface OpenTimelineProps { sortDirection: 'asc' | 'desc'; /** the requested field to sort on */ sortField: string; + /** timeline / template timeline */ + tabs: () => React.element; /** The title of the Open Timeline component */ title: string; /** The total (server-side) count of the search results */ diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx b/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx index 5b851701b973c4..509e76d7643c99 100644 --- a/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx @@ -74,6 +74,7 @@ const StatefulRecentTimelinesComponent = React.memo( sortOrder: Direction.desc, }} onlyUserFavorite={filterBy === 'favorites'} + timelineTypes={null} > {({ timelines, loading }) => ( <> diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/selectable_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/selectable_timeline/index.tsx index 639d30bbe7bb90..86bfeb5fa27d3b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/selectable_timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/selectable_timeline/index.tsx @@ -71,6 +71,7 @@ const TIMELINE_ITEM_HEIGHT = 50; export interface GetSelectableOptions { timelines: OpenTimelineResult[]; onlyFavorites: boolean; + timelineTypes?: 'default' | 'template'; searchTimelineValue: string; } @@ -79,6 +80,7 @@ interface SelectableTimelineProps { getSelectableOptions: ({ timelines, onlyFavorites, + timelineTypes, searchTimelineValue, }: GetSelectableOptions) => EuiSelectableOption[]; onClosePopover: () => void; @@ -96,7 +98,6 @@ const SelectableTimelineComponent: React.FC = ({ const [searchTimelineValue, setSearchTimelineValue] = useState(''); const [onlyFavorites, setOnlyFavorites] = useState(false); const [searchRef, setSearchRef] = useState(null); - const onSearchTimeline = useCallback(val => { setSearchTimelineValue(val); }, []); @@ -225,6 +226,7 @@ const SelectableTimelineComponent: React.FC = ({ search={searchTimelineValue} sort={{ sortField: SortFieldTimeline.updated, sortOrder: Direction.desc }} onlyUserFavorite={onlyFavorites} + timelineTypes="default" > {({ timelines, loading, totalCount }) => ( diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.gql_query.ts index e380e46e77070c..9ba0c16b3d0519 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.gql_query.ts @@ -12,12 +12,14 @@ export const allTimelinesQuery = gql` $search: String $sort: SortTimeline $onlyUserFavorite: Boolean + $timelineTypes: String ) { getAllTimeline( pageInfo: $pageInfo search: $search sort: $sort onlyUserFavorite: $onlyUserFavorite + timelineTypes: $timelineTypes ) { totalCount timeline { diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.tsx b/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.tsx index b5c91ca287f0b8..8dad5575e2bbeb 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.tsx @@ -28,6 +28,7 @@ export interface AllTimelinesArgs { export interface AllTimelinesVariables { onlyUserFavorite: boolean; + timelineTypes: 'default' | 'template' | null; pageInfo: PageInfoTimeline; search: string; sort: SortTimeline; @@ -79,12 +80,14 @@ const getAllTimeline = memoizeOne( const AllTimelinesQueryComponent: React.FC = ({ children, onlyUserFavorite, + timelineTypes, pageInfo, search, sort, }) => { const variables: GetAllTimeline.Variables = { onlyUserFavorite, + timelineTypes, pageInfo, search, sort, diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index 2a9dd8f2aacfe8..a926bfaa2a56ad 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -249,6 +249,12 @@ "description": "", "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null }, "defaultValue": null + }, + { + "name": "timelineTypes", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null } ], "type": { diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index e15c099a007ad3..4e7f6a3f46a70a 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -2218,6 +2218,8 @@ export interface GetAllTimelineQueryArgs { sort?: Maybe; onlyUserFavorite?: Maybe; + + timelineTypes?: Maybe; } export interface AuthenticationsSourceArgs { timerange: TimerangeInput; @@ -3995,6 +3997,7 @@ export namespace GetAllTimeline { search?: Maybe; sort?: Maybe; onlyUserFavorite?: Maybe; + timelineTypes?: Maybe; }; export type Query = { diff --git a/x-pack/plugins/siem/server/graphql/timeline/resolvers.ts b/x-pack/plugins/siem/server/graphql/timeline/resolvers.ts index a33751179e93a8..1d77cd89183585 100644 --- a/x-pack/plugins/siem/server/graphql/timeline/resolvers.ts +++ b/x-pack/plugins/siem/server/graphql/timeline/resolvers.ts @@ -50,6 +50,7 @@ export const createTimelineResolvers = ( return libs.timeline.getAllTimeline( req, args.onlyUserFavorite || null, + args.timelineTypes || null, args.pageInfo || null, args.search || null, args.sort || null diff --git a/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts index 9dd04247b7f47c..e740400c0450a2 100644 --- a/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts @@ -267,7 +267,7 @@ export const timelineSchema = gql` extend type Query { getOneTimeline(id: ID!): TimelineResult! - getAllTimeline(pageInfo: PageInfoTimeline, search: String, sort: SortTimeline, onlyUserFavorite: Boolean): ResponseTimelines! + getAllTimeline(pageInfo: PageInfoTimeline, search: String, sort: SortTimeline, onlyUserFavorite: Boolean, timelineTypes: String): ResponseTimelines! } extend type Mutation { diff --git a/x-pack/plugins/siem/server/graphql/types.ts b/x-pack/plugins/siem/server/graphql/types.ts index d272b7ff59b79a..d66781b1ca3a7a 100644 --- a/x-pack/plugins/siem/server/graphql/types.ts +++ b/x-pack/plugins/siem/server/graphql/types.ts @@ -2220,6 +2220,8 @@ export interface GetAllTimelineQueryArgs { sort?: Maybe; onlyUserFavorite?: Maybe; + + timelineTypes?: Maybe; } export interface AuthenticationsSourceArgs { timerange: TimerangeInput; @@ -2676,6 +2678,8 @@ export namespace QueryResolvers { sort?: Maybe; onlyUserFavorite?: Maybe; + + timelineTypes?: Maybe; } } diff --git a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts index e8cd27947589fc..7ccd9f0fbc70ce 100644 --- a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts @@ -44,6 +44,7 @@ export class Timeline { public async getAllTimeline( request: FrameworkRequest, onlyUserFavorite: boolean | null, + timelineTypes: string | null, pageInfo: PageInfoTimeline | null, search: string | null, sort: SortTimeline | null @@ -56,6 +57,10 @@ export class Timeline { searchFields: onlyUserFavorite ? ['title', 'description', 'favorite.keySearch'] : ['title', 'description'], + filter: + timelineTypes === 'template' || timelineTypes === 'default' + ? `siem-ui-timeline.attributes.timelineType: ${timelineTypes}` + : undefined, sortField: sort != null ? sort.sortField : undefined, sortOrder: sort != null ? sort.sortOrder : undefined, }; From dfef1e69939c7b5b58d357381dd57b7174c70e9f Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Mon, 27 Apr 2020 14:56:29 +0100 Subject: [PATCH 03/21] add routes for timelines tabs --- .../link_to/redirect_to_timelines.tsx | 4 +- .../public/components/open_timeline/index.tsx | 43 ++----------- .../open_timeline/open_timeline.tsx | 2 +- .../open_timeline_modal_body.tsx | 6 +- .../open_timeline/useTimelineTabs.tsx | 62 +++++++++++++++++++ .../siem/public/pages/timelines/index.tsx | 20 +++++- 6 files changed, 91 insertions(+), 46 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/open_timeline/useTimelineTabs.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_timelines.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_timelines.tsx index 27765a4125afcd..f45a5ac4a59391 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_timelines.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_timelines.tsx @@ -17,8 +17,8 @@ export type TimelineComponentProps = RouteComponentProps<{ }>; export const RedirectToTimelinesPage = ({ location: { search } }: TimelineComponentProps) => ( - + ); export const getTimelinesUrl = (search?: string) => - `#/link-to/${SiemPageName.timelines}${appendSearch(search)}`; + `#/link-to/${SiemPageName.timelines}/default${appendSearch(search)}`; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx index 0ce4c51b6da86e..72e2d07530c464 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx @@ -10,6 +10,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; import { EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui'; +import { useLocation, useHistory, useParams } from 'react-router-dom'; import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; import { deleteTimelineMutation } from '../../containers/timeline/delete/persist.gql_query'; import { AllTimelinesVariables, AllTimelinesQuery } from '../../containers/timeline/all'; @@ -43,6 +44,8 @@ import { } from './types'; import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants'; import * as i18n from './translations'; +import { SiemPageName } from '../../pages/home/types'; +import { useTimelineTabs } from './useTimelineTabs'; interface OwnProps { apolloClient: ApolloClient; @@ -70,19 +73,6 @@ export const getSelectedTimelineIds = (selectedItems: OpenTimelineResult[]): str [] ); -const tabs: Array<{ id: 'default' | 'template'; name: string; disabled: boolean }> = [ - { - id: 'default', - name: i18n.TAB_TIMELINES, - disabled: false, - }, - { - id: 'template', - name: i18n.TAB_TEMPLATES, - disabled: false, - }, -]; - /** Manages the state (e.g table selection) of the (pure) `OpenTimeline` component */ export const StatefulOpenTimelineComponent = React.memo( ({ @@ -106,8 +96,6 @@ export const StatefulOpenTimelineComponent = React.memo( >({}); /** Only query for favorite timelines when true */ const [onlyFavorites, setOnlyFavorites] = useState(false); - - const [timelineTypes, setTimelineTypes] = useState<'default' | 'template'>('default'); /** The requested page of results */ const [pageIndex, setPageIndex] = useState(0); /** The requested size of each page of search results */ @@ -121,27 +109,7 @@ export const StatefulOpenTimelineComponent = React.memo( /** The requested field to sort on */ const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); - const renderTabs = useCallback(() => { - return ( - <> - - {tabs.map((tab, index) => ( - { - setTimelineTypes(tab.id); - }, [setTimelineTypes, tab])} - isSelected={tab.id === timelineTypes} - disabled={tab.disabled} - key={index} - > - {tab.name} - - ))} - - - - ); - }, [tabs, timelineTypes]); + const { timelineTypes, renderTabs } = useTimelineTabs(); /** Invoked when the user presses enters to submit the text in the search input */ const onQueryChange: OnQueryChange = useCallback((query: EuiSearchBarQuery) => { @@ -329,12 +297,12 @@ export const StatefulOpenTimelineComponent = React.memo( pageSize={pageSize} query={search} refetch={refetch} + renderTabs={renderTabs} searchResults={timelines} setImportDataModalToggle={setImportDataModalToggle} selectedItems={selectedItems} sortDirection={sortDirection} sortField={sortField} - tabs={renderTabs} title={title} totalSearchResultsCount={totalCount} /> @@ -361,7 +329,6 @@ export const StatefulOpenTimelineComponent = React.memo( selectedItems={selectedItems} sortDirection={sortDirection} sortField={sortField} - tabs={renderTabs} title={title} totalSearchResultsCount={totalCount} /> diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx index d4704361c3c4c6..72f0295ea8da2b 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx @@ -142,7 +142,7 @@ export const OpenTimeline = React.memo( /> - {tabs()} + {renderTabs(true)} ( selectedItems, sortDirection, sortField, - tabs, title, totalSearchResultsCount, }) => { @@ -53,6 +53,8 @@ export const OpenTimelineModalBody = memo( : ['duplicate']; return actions.filter(action => !hideActions.includes(action)); }, [onDeleteSelected, deleteTimelines, hideActions]); + const { timelineTypes, renderTabs } = useTimelineTabs(); + return ( <> @@ -64,7 +66,7 @@ export const OpenTimelineModalBody = memo( title={title} /> <> - {tabs()} + {renderTabs(false)} = [ + { + id: 'default', + name: i18n.TAB_TIMELINES, + href: `#/${SiemPageName.timelines}/default`, + disabled: false, + }, + { + id: 'template', + name: i18n.TAB_TEMPLATES, + href: `#/${SiemPageName.timelines}/template`, + disabled: false, + }, +]; + +export const useTimelineTabs = (enableRouting?: boolean) => { + const [timelineTypes, setTimelineTypes] = useState<'default' | 'template'>('default'); + + const history = useHistory(); + const handleTabClicked = useCallback(() => { + setTimelineTypes(tab.id); + if (enableRouting) { + history.push(`/${SiemPageName.timelines}/${tab.id}`); + } + }, [setTimelineTypes, tab]); + + const renderTabs = useCallback( + (enableRouting: false) => { + return ( + <> + + {tabs.map((tab, index) => ( + + {tab.name} + + ))} + + + + ); + }, + [tabs, timelineTypes, history] + ); + + return { timelineTypes, renderTabs }; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/timelines/index.tsx b/x-pack/legacy/plugins/siem/public/pages/timelines/index.tsx index aa5c891de36285..5e8bd2e124580a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/timelines/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/timelines/index.tsx @@ -7,10 +7,24 @@ import React from 'react'; import { ApolloConsumer } from 'react-apollo'; +import { Router, Switch, Route, Redirect, useHistory } from 'react-router-dom'; import { TimelinesPage } from './timelines_page'; +import { SiemPageName } from '../home/types'; -export const Timelines = React.memo(() => ( - {client => } -)); +const timelinesPagePath = `/:pageName(${SiemPageName.timelines})/:tabName(default|template)`; +const timelinesDefaultPath = `/${SiemPageName.timelines}/default`; +export const Timelines = React.memo(() => { + const history = useHistory(); + return ( + + + + {client => } + + + + + ); +}); Timelines.displayName = 'Timelines'; From 159244ddb6f82adb0e4240ad5871db5cb23a6696 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Tue, 28 Apr 2020 16:16:26 +0100 Subject: [PATCH 04/21] add tabs hook --- .../open_timeline/open_timeline.tsx | 1 + .../open_timeline/search_row/index.tsx | 1 + .../open_timeline/useTimelineTabs.tsx | 21 ++++++++++--------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx index 72f0295ea8da2b..23be9e6a1c28f8 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx @@ -114,6 +114,7 @@ export const OpenTimeline = React.memo( refetch(); } }, [setImportDataModalToggle, refetch]); + const tabs = return ( <> diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.tsx index 55fce1f1c1ed07..44beb6b1019a43 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.tsx @@ -63,6 +63,7 @@ export const SearchRow = React.memo( > {i18n.ONLY_FAVORITES} + {TimetypeAction} diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/useTimelineTabs.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/useTimelineTabs.tsx index 7a621141c478b5..2e06c741f7252a 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/useTimelineTabs.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/useTimelineTabs.tsx @@ -24,7 +24,9 @@ const tabs: Array<{ id: 'default' | 'template'; name: string; disabled: boolean; }, ]; -export const useTimelineTabs = (enableRouting?: boolean) => { +export const useTimelineTabs = ( + type: 'tab' | 'filter' +): { timelineType: 'default' | 'template'; timelineTypeActions: JSX.Element } => { const [timelineTypes, setTimelineTypes] = useState<'default' | 'template'>('default'); const history = useHistory(); @@ -35,9 +37,9 @@ export const useTimelineTabs = (enableRouting?: boolean) => { } }, [setTimelineTypes, tab]); - const renderTabs = useCallback( - (enableRouting: false) => { - return ( + const timelineTypeActions = useMemo(() => { + return ( + type === 'tab' && ( <> {tabs.map((tab, index) => ( @@ -45,7 +47,7 @@ export const useTimelineTabs = (enableRouting?: boolean) => { isSelected={tab.id === timelineTypes} disabled={tab.disabled} key={index} - onClick={handleTabClicked} + href="" > {tab.name} @@ -53,10 +55,9 @@ export const useTimelineTabs = (enableRouting?: boolean) => { - ); - }, - [tabs, timelineTypes, history] - ); + ) + ); + }, [tabs, timelineTypes, type, history]); - return { timelineTypes, renderTabs }; + return { timelineTypes, timelineTypeActions }; }; From c7b9c8425c2026c8de9bfc904fb53499bd18720e Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Wed, 29 Apr 2020 20:57:36 +0100 Subject: [PATCH 05/21] add filter type --- .../siem/common/types/timeline/index.ts | 5 + .../siem/public/components/link_to/index.ts | 2 +- .../public/components/link_to/link_to.tsx | 3 +- .../link_to/redirect_to_timelines.tsx | 10 +- .../navigation/breadcrumbs/index.ts | 29 +++++- .../public/components/open_timeline/index.tsx | 19 +--- .../open_timeline/use_timeline_types.tsx | 92 ++++++++++--------- .../components/recent_timelines/index.tsx | 4 +- .../timeline/selectable_timeline/index.tsx | 8 +- .../public/containers/timeline/all/index.tsx | 23 ++--- .../siem/public/pages/timelines/index.tsx | 53 ++++++++++- .../plugins/siem/public/utils/route/types.ts | 4 + 12 files changed, 159 insertions(+), 93 deletions(-) diff --git a/x-pack/plugins/siem/common/types/timeline/index.ts b/x-pack/plugins/siem/common/types/timeline/index.ts index 55b4f9c6aca4dc..43f66da6109df1 100644 --- a/x-pack/plugins/siem/common/types/timeline/index.ts +++ b/x-pack/plugins/siem/common/types/timeline/index.ts @@ -144,6 +144,11 @@ export const TimelineTypeLiteralRt = runtimeTypes.union([ runtimeTypes.literal(TimelineType.default), ]); +const TimelineTypeLiteralWithNullRt = unionWithNullType(TimelineTypeLiteralRt); + +export type TimelineTypeLiteral = runtimeTypes.TypeOf; +export type TimelineTypeLiteralWithNull = runtimeTypes.TypeOf; + export const SavedTimelineRuntimeType = runtimeTypes.partial({ columns: unionWithNullType(runtimeTypes.array(SavedColumnHeaderRuntimeType)), dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)), diff --git a/x-pack/plugins/siem/public/components/link_to/index.ts b/x-pack/plugins/siem/public/components/link_to/index.ts index 3306076b569447..c35a60766d7bd9 100644 --- a/x-pack/plugins/siem/public/components/link_to/index.ts +++ b/x-pack/plugins/siem/public/components/link_to/index.ts @@ -14,7 +14,7 @@ export { getHostDetailsUrl, getHostsUrl } from './redirect_to_hosts'; export { getNetworkUrl, getIPDetailsUrl, RedirectToNetworkPage } from './redirect_to_network'; export { getTimelinesUrl, - getTemplateTimelinesUrl, + getTimelineTabsUrl, RedirectToTimelinesPage, } from './redirect_to_timelines'; export { diff --git a/x-pack/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/plugins/siem/public/components/link_to/link_to.tsx index b9b3f9ff2ad897..703fb314ec5192 100644 --- a/x-pack/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/plugins/siem/public/components/link_to/link_to.tsx @@ -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<{}>; @@ -112,7 +113,7 @@ export const LinkToPage = React.memo(({ match }) => ( /> diff --git a/x-pack/plugins/siem/public/components/link_to/redirect_to_timelines.tsx b/x-pack/plugins/siem/public/components/link_to/redirect_to_timelines.tsx index d642436a212c98..d726811538c83e 100644 --- a/x-pack/plugins/siem/public/components/link_to/redirect_to_timelines.tsx +++ b/x-pack/plugins/siem/public/components/link_to/redirect_to_timelines.tsx @@ -11,10 +11,10 @@ import { SiemPageName } from '../../pages/home/types'; import { appendSearch } from './helpers'; import { RedirectWrapper } from './redirect_wrapper'; -import { TimelineTypes } from '../../../common/types/timelines'; +import { TimelineTypeLiteral, TimelineType } from '../../../common/types/timeline'; export type TimelineComponentProps = RouteComponentProps<{ - tabName: 'default' | 'template'; + tabName: TimelineTypeLiteral; search: string; }>; @@ -28,7 +28,7 @@ export const RedirectToTimelinesPage = ({ ); export const getTimelinesUrl = (search?: string) => - `#/link-to/${SiemPageName.timelines}/default${appendSearch(search)}`; + `#/link-to/${SiemPageName.timelines}/${TimelineType.default}${appendSearch(search)}`; -export const getTemplateTimelinesUrl = (search?: string) => - `#/link-to/${SiemPageName.timelines}/template${appendSearch(search)}`; +export const getTimelineTabsUrl = (tabName: TimelineTypeLiteral, search?: string) => + `#/link-to/${SiemPageName.timelines}/${tabName}${appendSearch(search)}`; diff --git a/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.ts index 85d77485830a52..8abc099ee7f693 100644 --- a/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.ts @@ -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'; @@ -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; @@ -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 && diff --git a/x-pack/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/index.tsx index 604e4c459a4b19..dc4ead816ee70e 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/index.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/index.tsx @@ -39,9 +39,10 @@ import { OpenTimelineResult, OnToggleShowNotes, OnDeleteOneTimeline, + TimelineTabsStyle, } from './types'; import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants'; -import { useTimelineTabs } from './use_timeline_types'; +import { useTimelineTypes } from './use_timeline_types'; interface OwnProps { apolloClient: ApolloClient; @@ -105,7 +106,7 @@ export const StatefulOpenTimelineComponent = React.memo( /** The requested field to sort on */ const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); - const { timelineTypes, timelineTabs, timelineFilters } = useTimelineTabs(); + const { timelineTypes, timelineTabs, timelineFilters } = useTimelineTypes(); const { fetchAllTimeline, timelines, loading, totalCount, refetch } = useGetAllTimeline(); /** Invoked when the user presses enters to submit the text in the search input */ @@ -240,21 +241,9 @@ export const StatefulOpenTimelineComponent = React.memo( search, sort: { sortField: sortField as SortFieldTimeline, sortOrder: sortDirection as Direction }, onlyUserFavorite: onlyFavorites, - timelines, timelineTypes, - totalCount, }); - }, [ - pageIndex, - pageSize, - search, - sortField, - sortDirection, - timelines, - timelineTypes, - totalCount, - onlyFavorites, - ]); + }, [pageIndex, pageSize, search, sortField, sortDirection, timelineTypes, onlyFavorites]); return !isModal ? ( { const urlSearch = useGetUrlSearch(navTabs.timelines); const { tabName } = useParams<{ pageName: string; tabName: string }>(); - const [timelineTypes, setTimelineTypes] = useState<'default' | 'template' | null>( - timelineTabsStyle === TimelineTabsStyle.tab ? 'default' : null - ); - const tabs: Array<{ - id: 'default' | 'template'; + const [timelineTypes, setTimelineTypes] = useState(tabName || null); + + const getTabs: Array<{ + id: TimelineTypeLiteral; name: string; disabled: boolean; href: string; - }> = useMemo( - () => [ - { - id: 'default', - name: - timelineTabsStyle === TimelineTabsStyle.filter - ? i18n.FILTER_TIMELINES(i18n.TAB_TIMELINES) - : i18n.TAB_TIMELINES, - href: getTimelinesUrl(urlSearch), - disabled: false, - }, - { - id: 'template', - name: - timelineTabsStyle === TimelineTabsStyle.filter - ? i18n.FILTER_TIMELINES(i18n.TAB_TEMPLATES) - : i18n.TAB_TEMPLATES, - href: getTemplateTimelinesUrl(urlSearch), - disabled: false, - }, - ], - [timelineTabsStyle] - ); + }> = (timelineTabsStyle: TimelineTabsStyle) => [ + { + id: TimelineType.default, + name: + timelineTabsStyle === TimelineTabsStyle.filter + ? i18n.FILTER_TIMELINES(i18n.TAB_TIMELINES) + : i18n.TAB_TIMELINES, + href: getTimelineTabsUrl(TimelineType.default, urlSearch), + disabled: false, + }, + { + id: TimelineType.template, + name: + timelineTabsStyle === TimelineTabsStyle.filter + ? i18n.FILTER_TIMELINES(i18n.TAB_TEMPLATES) + : i18n.TAB_TEMPLATES, + href: getTimelineTabsUrl(TimelineType.template, urlSearch), + disabled: false, + }, + ]; const onFilterClicked = useCallback( - tabId => { + (timelineTabsStyle, tabId) => { if (timelineTabsStyle === TimelineTabsStyle.filter && tabId === timelineTypes) { setTimelineTypes(null); } else { setTimelineTypes(tabId); } }, - [setTimelineTypes] + [timelineTypes, setTimelineTypes] ); const timelineTabs = useMemo(() => { return ( <> - {tabs.map(tab => ( + {getTabs(TimelineTabsStyle.tab).map(tab => ( {tab.name} @@ -83,23 +85,23 @@ export const useTimelineTabs = ( ); - }, [tabs, tabName]); + }, [getTabs, tabName]); const timelineFilters = useMemo(() => { return ( <> - {tabs.map(tab => ( + {getTabs(TimelineTabsStyle.tab).map(tab => ( {tab.name} ))} ); - }, [tabs, timelineTypes]); + }, [getTabs, timelineTypes]); return { timelineTypes, diff --git a/x-pack/plugins/siem/public/components/recent_timelines/index.tsx b/x-pack/plugins/siem/public/components/recent_timelines/index.tsx index 67fc2e9af6194a..b07f73936b2bae 100644 --- a/x-pack/plugins/siem/public/components/recent_timelines/index.tsx +++ b/x-pack/plugins/siem/public/components/recent_timelines/index.tsx @@ -77,10 +77,8 @@ const StatefulRecentTimelinesComponent = React.memo( }, onlyUserFavorite: filterBy === 'favorites', timelineTypes: null, - timelines, - totalCount, }); - }, [filterBy, timelines, totalCount]); + }, [filterBy]); return ( <> diff --git a/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx b/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx index 72dcd7e5d102a5..026770510cff3b 100644 --- a/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx +++ b/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx @@ -23,7 +23,7 @@ import styled from 'styled-components'; import { useGetAllTimeline } from '../../../containers/timeline/all'; import { SortFieldTimeline, Direction } from '../../../graphql/types'; -import { TimelineType } from '../../../../common/types/timeline'; +import { TimelineType, TimelineTypeLiteralWithNull } from '../../../../common/types/timeline'; import { isUntitled } from '../../open_timeline/helpers'; import * as i18nTimeline from '../../open_timeline/translations'; @@ -73,7 +73,7 @@ const TIMELINE_ITEM_HEIGHT = 50; export interface GetSelectableOptions { timelines: OpenTimelineResult[]; onlyFavorites: boolean; - timelineTypes?: 'default' | 'template'; + timelineTypes?: TimelineTypeLiteralWithNull; searchTimelineValue: string; } @@ -233,10 +233,8 @@ const SelectableTimelineComponent: React.FC = ({ }, onlyUserFavorite: onlyFavorites, timelineTypes: TimelineType.default, - timelines, - totalCount: timelineCount, }); - }, [onlyFavorites, pageSize, searchTimelineValue, timelines, timelineCount]); + }, [onlyFavorites, pageSize, searchTimelineValue]); return ( diff --git a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx index bac20161ac0814..48fbd89b106660 100644 --- a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx +++ b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx @@ -22,9 +22,16 @@ import { useApolloClient } from '../../../utils/apollo_context'; import { allTimelinesQuery } from './index.gql_query'; import * as i18n from '../../../pages/timelines/translations'; +import { TimelineTypeLiteralWithNull } from '../../../../common/types/timeline'; export interface AllTimelinesArgs { - fetchAllTimeline: ({ onlyUserFavorite, pageInfo, search, sort }: AllTimelinesVariables) => void; + fetchAllTimeline: ({ + onlyUserFavorite, + pageInfo, + search, + sort, + timelineTypes, + }: AllTimelinesVariables) => void; timelines: OpenTimelineResult[]; loading: boolean; totalCount: number; @@ -33,7 +40,7 @@ export interface AllTimelinesArgs { export interface AllTimelinesVariables { onlyUserFavorite: boolean; - timelineTypes: 'default' | 'template' | null; + timelineTypes: TimelineTypeLiteralWithNull; pageInfo: PageInfoTimeline; search: string; sort: SortTimeline; @@ -92,15 +99,7 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { }); const fetchAllTimeline = useCallback( - async ({ - onlyUserFavorite, - pageInfo, - search, - sort, - timelineTypes, - timelines, - totalCount, - }: AllTimelinesVariables) => { + async ({ onlyUserFavorite, pageInfo, search, sort, timelineTypes }: AllTimelinesVariables) => { let didCancel = false; const abortCtrl = new AbortController(); @@ -109,8 +108,6 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { if (apolloClient != null) { setAllTimelines({ ...allTimelines, - timelines: timelines ?? allTimelines.timelines, - totalCount: totalCount ?? allTimelines.totalCount, loading: true, }); const variables: GetAllTimeline.Variables = { diff --git a/x-pack/plugins/siem/public/pages/timelines/index.tsx b/x-pack/plugins/siem/public/pages/timelines/index.tsx index 5e8bd2e124580a..48875baebf9e7f 100644 --- a/x-pack/plugins/siem/public/pages/timelines/index.tsx +++ b/x-pack/plugins/siem/public/pages/timelines/index.tsx @@ -6,13 +6,58 @@ import React from 'react'; import { ApolloConsumer } from 'react-apollo'; - +import { isEmpty } from 'lodash/fp'; import { Router, Switch, Route, Redirect, useHistory } from 'react-router-dom'; -import { TimelinesPage } from './timelines_page'; + +import { TimelineType } from '../../../common/types/timeline'; +import { TAB_TIMELINES, TAB_TEMPLATES } from '../../components/open_timeline/translations'; +import { getTimelineTabsUrl } from '../../components/link_to'; + import { SiemPageName } from '../home/types'; -const timelinesPagePath = `/:pageName(${SiemPageName.timelines})/:tabName(default|template)`; -const timelinesDefaultPath = `/${SiemPageName.timelines}/default`; +import { TimelinesPage } from './timelines_page'; +import { PAGE_TITLE } from './translations'; +const timelinesPagePath = `/:pageName(${SiemPageName.timelines})/:tabName(${TimelineType.default}|${TimelineType.template})`; +const timelinesDefaultPath = `/${SiemPageName.timelines}/${TimelineType.default}`; + +const TabNameMappedToI18nKey: Record = { + [TimelineType.default]: TAB_TIMELINES, + [TimelineType.template]: TAB_TEMPLATES, +}; + +export const getBreadcrumbs = ( + params: TimelineRouteSpyState, + search: string[] +): ChromeBreadcrumb[] => { + let breadcrumb = [ + { + text: PAGE_TITLE, + href: `${getTimelineTabsUrl(TimelineType.default, !isEmpty(search[1]) ? search[1] : '')}`, + }, + ]; + if (params.detailName != null) { + breadcrumb = [ + ...breadcrumb, + { + text: params.detailName, + href: `${getTimelineTabsUrl(params.detailName, !isEmpty(search[1]) ? search[1] : '')}`, + }, + ]; + } + + const tabName = params?.tabName; + if (!tabName) return breadcrumb; + + breadcrumb = [ + ...breadcrumb, + { + text: TabNameMappedToI18nKey[tabName], + href: '', + }, + ]; + return breadcrumb; +}; + export const Timelines = React.memo(() => { const history = useHistory(); return ( diff --git a/x-pack/plugins/siem/public/utils/route/types.ts b/x-pack/plugins/siem/public/utils/route/types.ts index d3eca36bd0d96d..82352b59e37950 100644 --- a/x-pack/plugins/siem/public/utils/route/types.ts +++ b/x-pack/plugins/siem/public/utils/route/types.ts @@ -32,6 +32,10 @@ export interface NetworkRouteSpyState extends RouteSpyState { tabName: NetworkRouteType | undefined; } +export interface TimelineRouteSpyState extends RouteSpyState { + tabName: TimelineType | undefined; +} + export type RouteSpyAction = | { type: 'updateSearch'; From 23652877e8d3c41370d4aaebb558c9667c1a4dc5 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Thu, 30 Apr 2020 01:46:44 +0100 Subject: [PATCH 06/21] fix unit test --- .../components/open_timeline/index.test.tsx | 37 +- .../public/components/open_timeline/index.tsx | 15 +- .../open_timeline/open_timeline.test.tsx | 433 ++++-------------- .../open_timeline_modal/index.test.tsx | 9 + .../open_timeline_modal_body.test.tsx | 223 +++------ .../public/components/open_timeline/types.ts | 9 +- .../open_timeline/use_timeline_types.tsx | 27 +- .../components/recent_timelines/index.tsx | 4 +- .../timeline/selectable_timeline/index.tsx | 12 +- .../public/containers/timeline/all/index.tsx | 16 +- .../siem/public/pages/timelines/index.tsx | 12 +- .../plugins/siem/public/utils/route/types.ts | 4 +- 12 files changed, 226 insertions(+), 575 deletions(-) diff --git a/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx index ea28bc06ef915b..64f80c52b2d16c 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx @@ -23,10 +23,20 @@ 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({ + timelineTypes: 'default', + timelineTabs:
, + timelineFilters:
, + }), + }; +}); describe('StatefulOpenTimeline', () => { const theme = () => ({ eui: euiDarkVars, darkMode: true }); @@ -491,33 +501,6 @@ describe('StatefulOpenTimeline', () => { }); }); - test('it renders the title', async () => { - const wrapper = mount( - - - - - - - - ); - - await wait(); - - expect( - wrapper - .find('[data-test-subj="header-section-title"]') - .first() - .text() - ).toEqual(title); - }); - describe('#resetSelectionState', () => { test('when the user deletes selected timelines, resetSelectionState is invoked to clear the selection state', async () => { const wrapper = mount( diff --git a/x-pack/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/index.tsx index dc4ead816ee70e..8eb5f465041f6a 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/index.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/index.tsx @@ -39,7 +39,6 @@ import { OpenTimelineResult, OnToggleShowNotes, OnDeleteOneTimeline, - TimelineTabsStyle, } from './types'; import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants'; import { useTimelineTypes } from './use_timeline_types'; @@ -241,9 +240,21 @@ export const StatefulOpenTimelineComponent = React.memo( search, sort: { sortField: sortField as SortFieldTimeline, sortOrder: sortDirection as Direction }, onlyUserFavorite: onlyFavorites, + timelines, timelineTypes, + totalCount, }); - }, [pageIndex, pageSize, search, sortField, sortDirection, timelineTypes, onlyFavorites]); + }, [ + pageIndex, + pageSize, + search, + sortField, + sortDirection, + timelines, + timelineTypes, + totalCount, + onlyFavorites, + ]); return !isModal ? ( { let mockResults: OpenTimelineResult[]; - beforeEach(() => { - mockResults = cloneDeep(mockTimelineResults); + const getDefaultTestProps = (mockSearchResults: OpenTimelineResult[]): OpenTimelineProps => ({ + deleteTimelines: jest.fn(), + defaultPageSize: DEFAULT_SEARCH_RESULTS_PER_PAGE, + isLoading: false, + itemIdToExpandedNotesRowMap: {}, + onAddTimelinesToFavorites: jest.fn(), + onDeleteSelected: jest.fn(), + onlyFavorites: false, + onOpenTimeline: jest.fn(), + onQueryChange: jest.fn(), + onSelectionChange: jest.fn(), + onTableChange: jest.fn(), + onToggleOnlyFavorites: jest.fn(), + onToggleShowNotes: jest.fn(), + pageIndex: 0, + pageSize: DEFAULT_SEARCH_RESULTS_PER_PAGE, + query: '', + searchResults: mockSearchResults, + selectedItems: [], + sortDirection: DEFAULT_SORT_DIRECTION, + sortField: DEFAULT_SORT_FIELD, + tabs:
, + title, + totalSearchResultsCount: mockSearchResults.length, }); - test('it renders the title row', () => { - const wrapper = mountWithIntl( - - - - ); - - expect( - wrapper - .find('[data-test-subj="title-row"]') - .first() - .exists() - ).toBe(true); + beforeEach(() => { + mockResults = cloneDeep(mockTimelineResults); }); test('it renders the search row', () => { + const defaultProps = getDefaultTestProps(mockResults); const wrapper = mountWithIntl( - + ); @@ -106,32 +72,10 @@ describe('OpenTimeline', () => { }); test('it renders the timelines table', () => { + const defaultProps = getDefaultTestProps(mockResults); const wrapper = mountWithIntl( - + ); @@ -144,32 +88,10 @@ describe('OpenTimeline', () => { }); test('it shows the delete action columns when onDeleteSelected and deleteTimelines are specified', () => { + const defaultProps = getDefaultTestProps(mockResults); const wrapper = mountWithIntl( - + ); @@ -182,31 +104,14 @@ describe('OpenTimeline', () => { }); test('it does NOT show the delete action columns when is onDeleteSelected undefined and deleteTimelines is specified', () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + onDeleteSelected: undefined, + deleteTimelines: undefined, + }; const wrapper = mountWithIntl( - + ); @@ -219,31 +124,14 @@ describe('OpenTimeline', () => { }); test('it does NOT show the delete action columns when is onDeleteSelected provided and deleteTimelines is undefined', () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + onDeleteSelected: undefined, + deleteTimelines: undefined, + }; const wrapper = mountWithIntl( - + ); @@ -256,30 +144,14 @@ describe('OpenTimeline', () => { }); test('it does NOT show the delete action when both onDeleteSelected and deleteTimelines are undefined', () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + onDeleteSelected: undefined, + deleteTimelines: undefined, + }; const wrapper = mountWithIntl( - + ); @@ -292,32 +164,13 @@ describe('OpenTimeline', () => { }); test('it renders an empty string when the query is an empty string', () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + query: '', + }; const wrapper = mountWithIntl( - + ); @@ -330,32 +183,13 @@ describe('OpenTimeline', () => { }); test('it renders the expected message when the query just has spaces', () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + query: ' ', + }; const wrapper = mountWithIntl( - + ); @@ -368,32 +202,13 @@ describe('OpenTimeline', () => { }); test('it echos the query when the query has non-whitespace characters', () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + query: 'Would you like to go to Denver?', + }; const wrapper = mountWithIntl( - + ); @@ -406,32 +221,13 @@ describe('OpenTimeline', () => { }); test('trims whitespace from the ends of the query', () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + query: ' Is it starting to feel cramped in here? ', + }; const wrapper = mountWithIntl( - + ); @@ -444,32 +240,13 @@ describe('OpenTimeline', () => { }); test('it renders the expected message when the query is an empty string', () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + query: '', + }; const wrapper = mountWithIntl( - + ); @@ -482,32 +259,13 @@ describe('OpenTimeline', () => { }); test('it renders the expected message when the query just has whitespace', () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + query: ' ', + }; const wrapper = mountWithIntl( - + ); @@ -520,32 +278,13 @@ describe('OpenTimeline', () => { }); test('it includes the word "with" when the query has non-whitespace characters', () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + query: 'How was your day?', + }; const wrapper = mountWithIntl( - + ); diff --git a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx index 46a0d46c1e0d16..9c497c4fdcd600 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx @@ -28,6 +28,15 @@ jest.mock('../../../containers/timeline/all', () => { getAllTimeline: originalModule.getAllTimeline, }; }); +jest.mock('../use_timeline_types', () => { + return { + useTimelineTypes: jest.fn().mockReturnValue({ + timelineTypes: 'default', + timelineTabs:
, + timelineFilters:
, + }), + }; +}); describe('OpenTimelineModal', () => { const theme = () => ({ eui: euiDarkVars, darkMode: true }); diff --git a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx index 2c3adb138b7acf..a610884d287a62 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../../pages/timelines/timelines_page'; -import { OpenTimelineResult } from '../types'; +import { OpenTimelineResult, OpenTimelineProps } from '../types'; import { TimelinesTableProps } from '../timelines_table'; import { mockTimelineResults } from '../../../mock/timeline_results'; import { OpenTimelineModalBody } from './open_timeline_modal_body'; @@ -22,40 +22,43 @@ jest.mock('../../../lib/kibana'); describe('OpenTimelineModal', () => { const theme = () => ({ eui: euiDarkVars, darkMode: true }); const title = 'All Timelines / Open Timelines'; - let mockResults: OpenTimelineResult[]; + const getDefaultTestProps = (mockSearchResults: OpenTimelineResult[]): OpenTimelineProps => ({ + deleteTimelines: jest.fn(), + defaultPageSize: DEFAULT_SEARCH_RESULTS_PER_PAGE, + isLoading: false, + itemIdToExpandedNotesRowMap: {}, + onAddTimelinesToFavorites: jest.fn(), + onDeleteSelected: jest.fn(), + onlyFavorites: false, + onOpenTimeline: jest.fn(), + onQueryChange: jest.fn(), + onSelectionChange: jest.fn(), + onTableChange: jest.fn(), + onToggleOnlyFavorites: jest.fn(), + onToggleShowNotes: jest.fn(), + pageIndex: 0, + pageSize: DEFAULT_SEARCH_RESULTS_PER_PAGE, + query: '', + searchResults: mockSearchResults, + selectedItems: [], + sortDirection: DEFAULT_SORT_DIRECTION, + sortField: DEFAULT_SORT_FIELD, + tabs:
, + title, + totalSearchResultsCount: mockSearchResults.length, + }); + beforeEach(() => { mockResults = cloneDeep(mockTimelineResults); }); test('it renders the title row', () => { + const defaultProps = getDefaultTestProps(mockResults); const wrapper = mountWithIntl( - + ); @@ -68,32 +71,10 @@ describe('OpenTimelineModal', () => { }); test('it renders the search row', () => { + const defaultProps = getDefaultTestProps(mockResults); const wrapper = mountWithIntl( - + ); @@ -106,32 +87,10 @@ describe('OpenTimelineModal', () => { }); test('it renders the timelines table', () => { + const defaultProps = getDefaultTestProps(mockResults); const wrapper = mountWithIntl( - + ); @@ -144,32 +103,14 @@ describe('OpenTimelineModal', () => { }); test('it shows the delete action when onDeleteSelected and deleteTimelines are specified', () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + onDeleteSelected: jest.fn(), + deleteTimelines: jest.fn(), + }; const wrapper = mountWithIntl( - + ); @@ -182,31 +123,14 @@ describe('OpenTimelineModal', () => { }); test('it does NOT show the delete when is onDeleteSelected undefined and deleteTimelines is specified', () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + onDeleteSelected: undefined, + deleteTimelines: undefined, + }; const wrapper = mountWithIntl( - + ); @@ -219,31 +143,14 @@ describe('OpenTimelineModal', () => { }); test('it does NOT show the delete action when is onDeleteSelected provided and deleteTimelines is undefined', () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + onDeleteSelected: undefined, + deleteTimelines: undefined, + }; const wrapper = mountWithIntl( - + ); @@ -256,30 +163,14 @@ describe('OpenTimelineModal', () => { }); test('it does NOT show extended columns when both onDeleteSelected and deleteTimelines are undefined', () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + onDeleteSelected: undefined, + deleteTimelines: undefined, + }; const wrapper = mountWithIntl( - + ); diff --git a/x-pack/plugins/siem/public/components/open_timeline/types.ts b/x-pack/plugins/siem/public/components/open_timeline/types.ts index d0fcc3b3d91f23..bc91f0a52c3704 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/types.ts +++ b/x-pack/plugins/siem/public/components/open_timeline/types.ts @@ -9,7 +9,7 @@ import { AllTimelinesVariables } from '../../containers/timeline/all'; import { TimelineModel } from '../../store/timeline/model'; import { NoteResult } from '../../graphql/types'; import { Refetch } from '../../store/inputs/model'; -import { TimelineType } from '../../../common/types/timeline'; +import { TimelineType, TimelineTypeLiteral } from '../../../common/types/timeline'; /** The users who added a timeline to favorites */ export interface FavoriteTimelineResult { @@ -195,3 +195,10 @@ export enum TimelineTabsStyle { tab = 'tab', filter = 'filter', } + +export interface TimelineTab { + id: TimelineTypeLiteral; + name: string; + disabled: boolean; + href: string; +} diff --git a/x-pack/plugins/siem/public/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/siem/public/components/open_timeline/use_timeline_types.tsx index 71a69a1dd64917..0c991fa6744f61 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/use_timeline_types.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/use_timeline_types.tsx @@ -7,18 +7,14 @@ import React, { useState, useCallback, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { EuiTabs, EuiTab, EuiSpacer, EuiFilterButton } from '@elastic/eui'; -import { - TimelineTypeLiteralWithNull, - TimelineTypeLiteral, - TimelineType, -} from '../../../common/types/timeline'; +import { TimelineTypeLiteralWithNull, TimelineType } from '../../../common/types/timeline'; -import { getTimelinesUrl, getTimelineTabsUrl } from '../link_to'; +import { getTimelineTabsUrl } from '../link_to'; import { useGetUrlSearch } from '../navigation/use_get_url_search'; import { navTabs } from '../../pages/home/home_navigations'; import * as i18n from './translations'; -import { TimelineTabsStyle } from './types'; +import { TimelineTabsStyle, TimelineTab } from './types'; export const useTimelineTypes = (): { timelineTypes: TimelineTypeLiteralWithNull; @@ -27,14 +23,13 @@ export const useTimelineTypes = (): { } => { const urlSearch = useGetUrlSearch(navTabs.timelines); const { tabName } = useParams<{ pageName: string; tabName: string }>(); - const [timelineTypes, setTimelineTypes] = useState(tabName || null); + const [timelineTypes, setTimelineTypes] = useState( + tabName === TimelineType.default || tabName === TimelineType.template ? tabName : null + ); - const getTabs: Array<{ - id: TimelineTypeLiteral; - name: string; - disabled: boolean; - href: string; - }> = (timelineTabsStyle: TimelineTabsStyle) => [ + const getTabs: (timelineTabsStyle: TimelineTabsStyle) => TimelineTab[] = ( + timelineTabsStyle: TimelineTabsStyle + ) => [ { id: TimelineType.default, name: @@ -70,7 +65,7 @@ export const useTimelineTypes = (): { return ( <> - {getTabs(TimelineTabsStyle.tab).map(tab => ( + {getTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => ( { return ( <> - {getTabs(TimelineTabsStyle.tab).map(tab => ( + {getTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => ( ( }, onlyUserFavorite: filterBy === 'favorites', timelineTypes: null, + timelines, + totalCount, }); - }, [filterBy]); + }, [filterBy, timelines, totalCount]); return ( <> diff --git a/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx b/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx index 026770510cff3b..bfb684d49ce59c 100644 --- a/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx +++ b/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx @@ -233,8 +233,10 @@ const SelectableTimelineComponent: React.FC = ({ }, onlyUserFavorite: onlyFavorites, timelineTypes: TimelineType.default, + timelines, + totalCount: timelineCount, }); - }, [onlyFavorites, pageSize, searchTimelineValue]); + }, [onlyFavorites, pageSize, searchTimelineValue, timelines, timelineCount]); return ( @@ -266,8 +268,12 @@ const SelectableTimelineComponent: React.FC = ({ }, }} singleSelection={true} - options={getSelectableOptions({ timelines, onlyFavorites, searchTimelineValue })} - timelineTypes="default" + options={getSelectableOptions({ + timelines, + onlyFavorites, + searchTimelineValue, + timelineTypes: TimelineType.default, + })} > {(list, search) => ( <> diff --git a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx index 48fbd89b106660..e3ad357e3450b9 100644 --- a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx +++ b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx @@ -30,7 +30,9 @@ export interface AllTimelinesArgs { pageInfo, search, sort, + timelines, timelineTypes, + totalCount, }: AllTimelinesVariables) => void; timelines: OpenTimelineResult[]; loading: boolean; @@ -40,11 +42,11 @@ export interface AllTimelinesArgs { export interface AllTimelinesVariables { onlyUserFavorite: boolean; - timelineTypes: TimelineTypeLiteralWithNull; pageInfo: PageInfoTimeline; search: string; sort: SortTimeline; timelines: OpenTimelineResult[]; + timelineTypes: TimelineTypeLiteralWithNull; totalCount: number; } @@ -99,7 +101,15 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { }); const fetchAllTimeline = useCallback( - async ({ onlyUserFavorite, pageInfo, search, sort, timelineTypes }: AllTimelinesVariables) => { + async ({ + onlyUserFavorite, + pageInfo, + search, + sort, + timelines, + timelineTypes, + totalCount, + }: AllTimelinesVariables) => { let didCancel = false; const abortCtrl = new AbortController(); @@ -108,6 +118,8 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { if (apolloClient != null) { setAllTimelines({ ...allTimelines, + timelines: timelines ?? allTimelines.timelines, + totalCount: totalCount ?? allTimelines.totalCount, loading: true, }); const variables: GetAllTimeline.Variables = { diff --git a/x-pack/plugins/siem/public/pages/timelines/index.tsx b/x-pack/plugins/siem/public/pages/timelines/index.tsx index 48875baebf9e7f..d63f652e1ef257 100644 --- a/x-pack/plugins/siem/public/pages/timelines/index.tsx +++ b/x-pack/plugins/siem/public/pages/timelines/index.tsx @@ -9,9 +9,12 @@ import { ApolloConsumer } from 'react-apollo'; import { isEmpty } from 'lodash/fp'; import { Router, Switch, Route, Redirect, useHistory } from 'react-router-dom'; +import { ChromeBreadcrumb } from '../../../../../../src/core/public'; + import { TimelineType } from '../../../common/types/timeline'; import { TAB_TIMELINES, TAB_TEMPLATES } from '../../components/open_timeline/translations'; import { getTimelineTabsUrl } from '../../components/link_to'; +import { TimelineRouteSpyState } from '../../utils/route/types'; import { SiemPageName } from '../home/types'; @@ -35,15 +38,6 @@ export const getBreadcrumbs = ( href: `${getTimelineTabsUrl(TimelineType.default, !isEmpty(search[1]) ? search[1] : '')}`, }, ]; - if (params.detailName != null) { - breadcrumb = [ - ...breadcrumb, - { - text: params.detailName, - href: `${getTimelineTabsUrl(params.detailName, !isEmpty(search[1]) ? search[1] : '')}`, - }, - ]; - } const tabName = params?.tabName; if (!tabName) return breadcrumb; diff --git a/x-pack/plugins/siem/public/utils/route/types.ts b/x-pack/plugins/siem/public/utils/route/types.ts index 82352b59e37950..17b312a427c435 100644 --- a/x-pack/plugins/siem/public/utils/route/types.ts +++ b/x-pack/plugins/siem/public/utils/route/types.ts @@ -8,11 +8,13 @@ import * as H from 'history'; import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; +import { TimelineType } from '../../../common/types/timeline'; + import { HostsTableType } from '../../store/hosts/model'; import { NetworkRouteType } from '../../pages/network/navigation/types'; import { FlowTarget } from '../../graphql/types'; -export type SiemRouteType = HostsTableType | NetworkRouteType; +export type SiemRouteType = HostsTableType | NetworkRouteType | TimelineType; export interface RouteSpyState { pageName: string; detailName: string | undefined; From 6a1495e4845b963d64431f5bcc5db473ada047b0 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Thu, 30 Apr 2020 14:07:26 +0100 Subject: [PATCH 07/21] add breadcrumbs --- .../public/components/link_to/link_to.tsx | 10 +++++++ .../link_to/redirect_to_timelines.tsx | 10 +++++-- .../navigation/breadcrumbs/index.test.ts | 2 +- .../public/components/open_timeline/index.tsx | 12 +------- .../siem/public/pages/timelines/index.tsx | 28 ++++++++++--------- 5 files changed, 35 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/plugins/siem/public/components/link_to/link_to.tsx index 703fb314ec5192..adb32a8983dfb4 100644 --- a/x-pack/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/plugins/siem/public/components/link_to/link_to.tsx @@ -111,6 +111,16 @@ export const LinkToPage = React.memo(({ match }) => ( component={RedirectToEditRulePage} path={`${match.url}/:pageName(${SiemPageName.detections})/rules/id/:detailName/edit`} /> + + ( - + ); export const getTimelinesUrl = (search?: string) => - `#/link-to/${SiemPageName.timelines}/${TimelineType.default}${appendSearch(search)}`; + `#/link-to/${SiemPageName.timelines}${appendSearch(search)}`; export const getTimelineTabsUrl = (tabName: TimelineTypeLiteral, search?: string) => `#/link-to/${SiemPageName.timelines}/${tabName}${appendSearch(search)}`; diff --git a/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts index 7770780fb9613e..2acae92c390dde 100644 --- a/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts @@ -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' }, ]); }); diff --git a/x-pack/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/index.tsx index 8eb5f465041f6a..e1e20c1425f5d4 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/index.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/index.tsx @@ -244,17 +244,7 @@ export const StatefulOpenTimelineComponent = React.memo( timelineTypes, totalCount, }); - }, [ - pageIndex, - pageSize, - search, - sortField, - sortDirection, - timelines, - timelineTypes, - totalCount, - onlyFavorites, - ]); + }, [pageIndex, pageSize, search, sortField, sortDirection, timelineTypes, onlyFavorites]); return !isModal ? ( { - const history = useHistory(); return ( - - - - {client => } - - - - + + + {client => } + + ( + + )} + /> + ); }); From 6afc7068a78b4402cecb4e2d84fff1c772e1d1d3 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Thu, 30 Apr 2020 14:08:28 +0100 Subject: [PATCH 08/21] fix types error --- x-pack/plugins/siem/public/pages/timelines/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/siem/public/pages/timelines/index.tsx b/x-pack/plugins/siem/public/pages/timelines/index.tsx index cbc305069df150..343be5cbe3839e 100644 --- a/x-pack/plugins/siem/public/pages/timelines/index.tsx +++ b/x-pack/plugins/siem/public/pages/timelines/index.tsx @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; +import React from 'react'; import { ApolloConsumer } from 'react-apollo'; -import { Router, Switch, Route, Redirect, useHistory } from 'react-router-dom'; +import { Switch, Route, Redirect } from 'react-router-dom'; import { ChromeBreadcrumb } from '../../../../../../src/core/public'; import { TimelineType } from '../../../common/types/timeline'; import { TAB_TIMELINES, TAB_TEMPLATES } from '../../components/open_timeline/translations'; -import { getTimelineTabsUrl, getTimelinesUrl } from '../../components/link_to'; +import { getTimelinesUrl } from '../../components/link_to'; import { TimelineRouteSpyState } from '../../utils/route/types'; import { SiemPageName } from '../home/types'; From 28657083920345766335abab64e13d43e16a5a35 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Sun, 3 May 2020 11:53:02 +0100 Subject: [PATCH 09/21] fix flashing table --- .../public/components/open_timeline/index.tsx | 11 ++++---- .../components/recent_timelines/index.tsx | 4 +-- .../timeline/selectable_timeline/index.tsx | 4 +-- .../public/containers/timeline/all/index.tsx | 28 ++++++++----------- 4 files changed, 19 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/index.tsx index e1e20c1425f5d4..66aad6f7c978de 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/index.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/index.tsx @@ -139,9 +139,12 @@ export const StatefulOpenTimelineComponent = React.memo( const deleteTimelines: DeleteTimelines = useCallback( async (timelineIds: string[]) => { + const existingTimelines = [...timelines]; + const existingTimelinesCount = totalCount; if (timelineIds.includes(timeline.savedObjectId || '')) { createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false }); } + await apolloClient.mutate< DeleteTimelineMutation.Mutation, DeleteTimelineMutation.Variables @@ -150,9 +153,9 @@ export const StatefulOpenTimelineComponent = React.memo( fetchPolicy: 'no-cache', variables: { id: timelineIds }, }); - refetch(); + refetch(existingTimelines, existingTimelinesCount); }, - [apolloClient, createNewTimeline, refetch, timeline] + [apolloClient, createNewTimeline, refetch, timeline, timelines, totalCount] ); const onDeleteOneTimeline: OnDeleteOneTimeline = useCallback( @@ -240,9 +243,7 @@ export const StatefulOpenTimelineComponent = React.memo( search, sort: { sortField: sortField as SortFieldTimeline, sortOrder: sortDirection as Direction }, onlyUserFavorite: onlyFavorites, - timelines, timelineTypes, - totalCount, }); }, [pageIndex, pageSize, search, sortField, sortDirection, timelineTypes, onlyFavorites]); @@ -266,7 +267,7 @@ export const StatefulOpenTimelineComponent = React.memo( pageIndex={pageIndex} pageSize={pageSize} query={search} - refetch={refetch} + refetch={refetch.bind(null, timelines, totalCount)} searchResults={timelines} setImportDataModalToggle={setImportDataModalToggle} selectedItems={selectedItems} diff --git a/x-pack/plugins/siem/public/components/recent_timelines/index.tsx b/x-pack/plugins/siem/public/components/recent_timelines/index.tsx index 67fc2e9af6194a..b07f73936b2bae 100644 --- a/x-pack/plugins/siem/public/components/recent_timelines/index.tsx +++ b/x-pack/plugins/siem/public/components/recent_timelines/index.tsx @@ -77,10 +77,8 @@ const StatefulRecentTimelinesComponent = React.memo( }, onlyUserFavorite: filterBy === 'favorites', timelineTypes: null, - timelines, - totalCount, }); - }, [filterBy, timelines, totalCount]); + }, [filterBy]); return ( <> diff --git a/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx b/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx index bfb684d49ce59c..7228344d7cc0f6 100644 --- a/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx +++ b/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx @@ -233,10 +233,8 @@ const SelectableTimelineComponent: React.FC = ({ }, onlyUserFavorite: onlyFavorites, timelineTypes: TimelineType.default, - timelines, - totalCount: timelineCount, }); - }, [onlyFavorites, pageSize, searchTimelineValue, timelines, timelineCount]); + }, [onlyFavorites, pageSize, searchTimelineValue]); return ( diff --git a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx index e3ad357e3450b9..a4e70a3515aa4d 100644 --- a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx +++ b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx @@ -30,14 +30,12 @@ export interface AllTimelinesArgs { pageInfo, search, sort, - timelines, timelineTypes, - totalCount, }: AllTimelinesVariables) => void; timelines: OpenTimelineResult[]; loading: boolean; totalCount: number; - refetch: () => void; + refetch: (existingTimelines?: OpenTimelineResult[], existingTotalCount?: number) => void; } export interface AllTimelinesVariables { @@ -45,9 +43,7 @@ export interface AllTimelinesVariables { pageInfo: PageInfoTimeline; search: string; sort: SortTimeline; - timelines: OpenTimelineResult[]; timelineTypes: TimelineTypeLiteralWithNull; - totalCount: number; } export const ALL_TIMELINE_QUERY_ID = 'FETCH_ALL_TIMELINES'; @@ -101,27 +97,23 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { }); const fetchAllTimeline = useCallback( - async ({ - onlyUserFavorite, - pageInfo, - search, - sort, - timelines, - timelineTypes, - totalCount, - }: AllTimelinesVariables) => { + async ({ onlyUserFavorite, pageInfo, search, sort, timelineTypes }: AllTimelinesVariables) => { let didCancel = false; const abortCtrl = new AbortController(); - const fetchData = async () => { + const fetchData = async ( + existingTimelines?: OpenTimelineResult[], + existingTotalCount?: number + ) => { try { if (apolloClient != null) { setAllTimelines({ ...allTimelines, - timelines: timelines ?? allTimelines.timelines, - totalCount: totalCount ?? allTimelines.totalCount, + timelines: existingTimelines ?? allTimelines.timelines, + totalCount: existingTotalCount ?? allTimelines.totalCount, loading: true, }); + const variables: GetAllTimeline.Variables = { onlyUserFavorite, pageInfo, @@ -162,6 +154,7 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { getOr([], 'getAllTimeline.timeline', response.data) ), }); + // console.log('--done---', allTimelines); } } } catch (error) { @@ -178,6 +171,7 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { totalCount: 0, timelines: [], }); + // console.log('--error---', allTimelines); } } }; From 0f6b6792246655a3ea7c597b2b42a94a4860ef0b Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Sun, 3 May 2020 15:29:15 +0100 Subject: [PATCH 10/21] fix types --- .../plugins/siem/public/components/recent_timelines/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/siem/public/components/recent_timelines/index.tsx b/x-pack/plugins/siem/public/components/recent_timelines/index.tsx index b07f73936b2bae..d88f2bddf3159f 100644 --- a/x-pack/plugins/siem/public/components/recent_timelines/index.tsx +++ b/x-pack/plugins/siem/public/components/recent_timelines/index.tsx @@ -62,7 +62,7 @@ const StatefulRecentTimelinesComponent = React.memo( [filterBy] ); - const { fetchAllTimeline, timelines, totalCount, loading } = useGetAllTimeline(); + const { fetchAllTimeline, timelines, loading } = useGetAllTimeline(); useEffect(() => { fetchAllTimeline({ From 88dbd74e6ba7895726b7f3a2f380e68a474a546f Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Sun, 3 May 2020 21:22:01 +0100 Subject: [PATCH 11/21] fix flashing table --- .../public/components/open_timeline/index.tsx | 2 +- .../components/open_timeline/open_timeline.tsx | 8 ++++---- .../public/components/open_timeline/types.ts | 3 +-- .../public/containers/timeline/all/index.tsx | 18 ++++++++---------- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/index.tsx index 66aad6f7c978de..d7b1fe7ae5df11 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/index.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/index.tsx @@ -267,7 +267,7 @@ export const StatefulOpenTimelineComponent = React.memo( pageIndex={pageIndex} pageSize={pageSize} query={search} - refetch={refetch.bind(null, timelines, totalCount)} + refetch={refetch} searchResults={timelines} setImportDataModalToggle={setImportDataModalToggle} selectedItems={selectedItems} diff --git a/x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx index 9ccf79f6ebeef7..e172a006abe4b0 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx @@ -97,9 +97,9 @@ export const OpenTimeline = React.memo( const onRefreshBtnClick = useCallback(() => { if (refetch != null) { - refetch(); + refetch(searchResults, totalSearchResultsCount); } - }, [refetch]); + }, [refetch, searchResults, totalSearchResultsCount]); const handleCloseModal = useCallback(() => { if (setImportDataModalToggle != null) { @@ -111,9 +111,9 @@ export const OpenTimeline = React.memo( setImportDataModalToggle(false); } if (refetch != null) { - refetch(); + refetch(searchResults, totalSearchResultsCount); } - }, [setImportDataModalToggle, refetch]); + }, [setImportDataModalToggle, refetch, searchResults, totalSearchResultsCount]); return ( <> diff --git a/x-pack/plugins/siem/public/components/open_timeline/types.ts b/x-pack/plugins/siem/public/components/open_timeline/types.ts index bc91f0a52c3704..4d953f6fa775e1 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/types.ts +++ b/x-pack/plugins/siem/public/components/open_timeline/types.ts @@ -8,7 +8,6 @@ import { SetStateAction, Dispatch } from 'react'; import { AllTimelinesVariables } from '../../containers/timeline/all'; import { TimelineModel } from '../../store/timeline/model'; import { NoteResult } from '../../graphql/types'; -import { Refetch } from '../../store/inputs/model'; import { TimelineType, TimelineTypeLiteral } from '../../../common/types/timeline'; /** The users who added a timeline to favorites */ @@ -150,7 +149,7 @@ export interface OpenTimelineProps { /** The currently applied search criteria */ query: string; /** Refetch table */ - refetch?: Refetch; + refetch?: (existingTimeline?: OpenTimelineResult[], existingCount?: number) => void; /** The results of executing a search */ searchResults: OpenTimelineResult[]; /** the currently-selected timelines in the table */ diff --git a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx index a4e70a3515aa4d..85f891438426af 100644 --- a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx +++ b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx @@ -134,27 +134,26 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { }, }, }); + const totalCount = response?.data?.getAllTimeline?.totalCount ?? 0; + const timelines = response?.data?.getAllTimeline?.timeline ?? []; if (!didCancel) { dispatch( inputsActions.setQuery({ inputId: 'global', id: ALL_TIMELINE_QUERY_ID, loading: false, - refetch: refetch.current ?? noop, + refetch: refetch?.current?.bind(null, timelines, totalCount) ?? noop, inspect: null, }) ); + setAllTimelines({ fetchAllTimeline, loading: false, - refetch: refetch.current ?? noop, - totalCount: getOr(0, 'getAllTimeline.totalCount', response.data), - timelines: getAllTimeline( - JSON.stringify(variables), - getOr([], 'getAllTimeline.timeline', response.data) - ), + refetch: refetch?.current?.bind(null, timelines, totalCount) ?? noop, + totalCount, + timelines: getAllTimeline(JSON.stringify(variables), timelines as TimelineResult[]), }); - // console.log('--done---', allTimelines); } } } catch (error) { @@ -171,7 +170,6 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { totalCount: 0, timelines: [], }); - // console.log('--error---', allTimelines); } } }; @@ -182,7 +180,7 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { abortCtrl.abort(); }; }, - [apolloClient, allTimelines] + [apolloClient, allTimelines, refetch] ); useEffect(() => { From 187df00653d10aeb4bd198b324f7617f6f341731 Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Tue, 5 May 2020 12:22:10 +0200 Subject: [PATCH 12/21] fix filter --- .../siem/server/lib/timeline/saved_object.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts index 9bc1f852939815..0874233a5b8ef4 100644 --- a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts @@ -95,6 +95,18 @@ export const getTimelineByTemplateTimelineId = async ( return getAllSavedTimeline(request, options); }; +const getTimelineTypeFilter = (timelineTypes: string | null) => { + if (timelineTypes === TimelineType.template) { + return `siem-ui-timeline.attributes.timelineType: ${TimelineType.template}`; + } + + if (timelineTypes === TimelineType.default) { + return `not siem-ui-timeline.attributes.timelineType: ${TimelineType.template}`; + } + + return undefined; +}; + export const getAllTimeline = async ( request: FrameworkRequest, onlyUserFavorite: boolean | null, @@ -111,10 +123,7 @@ export const getAllTimeline = async ( searchFields: onlyUserFavorite ? ['title', 'description', 'favorite.keySearch'] : ['title', 'description'], - filter: - timelineTypes === 'template' || timelineTypes === 'default' - ? `siem-ui-timeline.attributes.timelineType: ${timelineTypes}` - : undefined, + filter: getTimelineTypeFilter(timelineTypes), sortField: sort != null ? sort.sortField : undefined, sortOrder: sort != null ? sort.sortOrder : undefined, }; From d8336bed20cc06eb8fa5a40bf8cd0c1597d897e5 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Tue, 5 May 2020 12:28:02 +0100 Subject: [PATCH 13/21] add comments for filters --- x-pack/plugins/siem/server/lib/timeline/saved_object.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts index 0874233a5b8ef4..84d39d58210e54 100644 --- a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts @@ -95,12 +95,18 @@ export const getTimelineByTemplateTimelineId = async ( return getAllSavedTimeline(request, options); }; +/** The filter here is able to handle the legacy data, + * which has no timelineType exists in the savedObject */ const getTimelineTypeFilter = (timelineTypes: string | null) => { if (timelineTypes === TimelineType.template) { + /** Show only whose timelineType is "template" */ return `siem-ui-timeline.attributes.timelineType: ${TimelineType.template}`; } if (timelineTypes === TimelineType.default) { + /** Show me every timeline whose timelineType is not "template". + * which includes timelineType === 'default' and + * those timelineType doesn't exists */ return `not siem-ui-timeline.attributes.timelineType: ${TimelineType.template}`; } From 7a6f23738e18dbb2cf7d37f5c64f8c85474357d8 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 5 May 2020 11:22:12 -0400 Subject: [PATCH 14/21] review X --- .../public/components/open_timeline/index.tsx | 34 ++++++++++--------- .../open_timeline/use_timeline_types.tsx | 10 +++--- .../public/containers/timeline/all/index.tsx | 19 ++--------- .../lib/timeline/pick_saved_timeline.ts | 2 ++ .../timeline/routes/import_timelines_route.ts | 28 ++++++++------- .../timeline/routes/utils/import_timelines.ts | 6 ++-- 6 files changed, 45 insertions(+), 54 deletions(-) diff --git a/x-pack/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/index.tsx index d7b1fe7ae5df11..24d63324c11f57 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/index.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/index.tsx @@ -106,7 +106,20 @@ export const StatefulOpenTimelineComponent = React.memo( const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); const { timelineTypes, timelineTabs, timelineFilters } = useTimelineTypes(); - const { fetchAllTimeline, timelines, loading, totalCount, refetch } = useGetAllTimeline(); + 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, + timelineTypes, + }); + }, [pageIndex, pageSize, search, sortField, sortDirection, timelineTypes, onlyFavorites]); /** Invoked when the user presses enters to submit the text in the search input */ const onQueryChange: OnQueryChange = useCallback((query: EuiSearchBarQuery) => { @@ -139,8 +152,6 @@ export const StatefulOpenTimelineComponent = React.memo( const deleteTimelines: DeleteTimelines = useCallback( async (timelineIds: string[]) => { - const existingTimelines = [...timelines]; - const existingTimelinesCount = totalCount; if (timelineIds.includes(timeline.savedObjectId || '')) { createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false }); } @@ -153,9 +164,9 @@ export const StatefulOpenTimelineComponent = React.memo( fetchPolicy: 'no-cache', variables: { id: timelineIds }, }); - refetch(existingTimelines, existingTimelinesCount); + refetch(); }, - [apolloClient, createNewTimeline, refetch, timeline, timelines, totalCount] + [apolloClient, createNewTimeline, refetch, timeline] ); const onDeleteOneTimeline: OnDeleteOneTimeline = useCallback( @@ -235,17 +246,8 @@ export const StatefulOpenTimelineComponent = React.memo( }, []); useEffect(() => { - fetchAllTimeline({ - pageInfo: { - pageIndex: pageIndex + 1, - pageSize, - }, - search, - sort: { sortField: sortField as SortFieldTimeline, sortOrder: sortDirection as Direction }, - onlyUserFavorite: onlyFavorites, - timelineTypes, - }); - }, [pageIndex, pageSize, search, sortField, sortDirection, timelineTypes, onlyFavorites]); + refetch(); + }, [refetch]); return !isModal ? ( TimelineTab[] = ( + const getFilterOrTabs: (timelineTabsStyle: TimelineTabsStyle) => TimelineTab[] = ( timelineTabsStyle: TimelineTabsStyle ) => [ { @@ -65,7 +65,7 @@ export const useTimelineTypes = (): { return ( <> - {getTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => ( + {getFilterOrTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => ( ); - }, [getTabs, tabName]); + }, [tabName]); const timelineFilters = useMemo(() => { return ( <> - {getTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => ( + {getFilterOrTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => ( ); - }, [getTabs, timelineTypes]); + }, [timelineTypes]); return { timelineTypes, diff --git a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx index 85f891438426af..273b267405ae10 100644 --- a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx +++ b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx @@ -35,7 +35,6 @@ export interface AllTimelinesArgs { timelines: OpenTimelineResult[]; loading: boolean; totalCount: number; - refetch: (existingTimelines?: OpenTimelineResult[], existingTotalCount?: number) => void; } export interface AllTimelinesVariables { @@ -86,12 +85,10 @@ export const getAllTimeline = memoizeOne( export const useGetAllTimeline = (): AllTimelinesArgs => { const dispatch = useDispatch(); const apolloClient = useApolloClient(); - const refetch = useRef(); const [, dispatchToaster] = useStateToaster(); const [allTimelines, setAllTimelines] = useState({ fetchAllTimeline: noop, loading: false, - refetch: refetch.current ?? noop, totalCount: 0, timelines: [], }); @@ -101,16 +98,11 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { let didCancel = false; const abortCtrl = new AbortController(); - const fetchData = async ( - existingTimelines?: OpenTimelineResult[], - existingTotalCount?: number - ) => { + const fetchData = async () => { try { if (apolloClient != null) { setAllTimelines({ ...allTimelines, - timelines: existingTimelines ?? allTimelines.timelines, - totalCount: existingTotalCount ?? allTimelines.totalCount, loading: true, }); @@ -142,15 +134,13 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { inputId: 'global', id: ALL_TIMELINE_QUERY_ID, loading: false, - refetch: refetch?.current?.bind(null, timelines, totalCount) ?? noop, + refetch: fetchData, inspect: null, }) ); - setAllTimelines({ fetchAllTimeline, loading: false, - refetch: refetch?.current?.bind(null, timelines, totalCount) ?? noop, totalCount, timelines: getAllTimeline(JSON.stringify(variables), timelines as TimelineResult[]), }); @@ -166,21 +156,19 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { setAllTimelines({ fetchAllTimeline, loading: false, - refetch: noop, totalCount: 0, timelines: [], }); } } }; - refetch.current = fetchData; fetchData(); return () => { didCancel = true; abortCtrl.abort(); }; }, - [apolloClient, allTimelines, refetch] + [apolloClient, allTimelines] ); useEffect(() => { @@ -192,6 +180,5 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { return { ...allTimelines, fetchAllTimeline, - refetch: refetch.current ?? noop, }; }; diff --git a/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts b/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts index 6de10bffb1325c..07bb3257e2ab8a 100644 --- a/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts +++ b/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts @@ -44,5 +44,7 @@ export const pickSavedTimeline = ( savedTimeline.templateTimelineVersion = null; } + console.log('pickSavedTimeline', JSON.stringify(savedTimeline)); + return savedTimeline; }; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts index 99621f1391acb9..6fe6d396ed0b06 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts @@ -119,35 +119,36 @@ export const importTimelinesRoute = ( return null; } const { - savedObjectId, + savedObjectId = null, pinnedEventIds, globalNotes, eventNotes, templateTimelineId, templateTimelineVersion, timelineType, + version = null, } = parsedTimeline; const parsedTimelineObject = omit( timelineSavedObjectOmittedFields, parsedTimeline ); + console.log('parsedTimeline', JSON.stringify(parsedTimeline)); + console.log('parsedTimelineObject', JSON.stringify(parsedTimelineObject)); let newTimeline = null; try { const templateTimeline = templateTimelineId != null ? await getTemplateTimeline(frameworkRequest, templateTimelineId) : null; + const timeline = - templateTimeline?.savedObjectId != null || savedObjectId != null - ? await getTimeline( - frameworkRequest, - templateTimeline?.savedObjectId ?? savedObjectId - ) - : null; + savedObjectId != null && + (await getTimeline(frameworkRequest, savedObjectId)); const isHandlingTemplateTimeline = timelineType === TimelineType.template; + if ( (timeline == null && !isHandlingTemplateTimeline) || - (templateTimeline == null && isHandlingTemplateTimeline) + (timeline == null && templateTimeline == null && isHandlingTemplateTimeline) ) { // create timeline / template timeline newTimeline = await createTimelines( @@ -165,6 +166,7 @@ export const importTimelinesRoute = ( status_code: 200, }); } else if ( + timeline && timeline != null && templateTimeline != null && isHandlingTemplateTimeline @@ -172,8 +174,8 @@ export const importTimelinesRoute = ( // update template timeline const errorObj = checkIsFailureCases( isHandlingTemplateTimeline, - timeline.version, - templateTimeline.templateTimelineVersion ?? null, + version, + templateTimelineVersion ?? null, timeline, templateTimeline ); @@ -198,16 +200,16 @@ export const importTimelinesRoute = ( } else { resolve( createBulkErrorObject({ - id: savedObjectId, + id: savedObjectId ?? 'unknown', statusCode: 409, - message: `timeline_id: "${timeline?.savedObjectId}" already exists`, + message: `timeline_id: "${savedObjectId}" already exists`, }) ); } } catch (err) { resolve( createBulkErrorObject({ - id: savedObjectId, + id: savedObjectId ?? 'unknown', statusCode: 400, message: err.message, }) diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts index a49627d40c8f57..b829b550334b5a 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts @@ -18,7 +18,8 @@ export interface ImportTimelinesSchema { } export type ImportedTimeline = SavedTimeline & { - savedObjectId: string; + savedObjectId: string | null; + version: string | null; pinnedEventIds: string[]; globalNotes: NoteResult[]; eventNotes: NoteResult[]; @@ -90,12 +91,9 @@ export const timelineSavedObjectOmittedFields = [ 'globalNotes', 'eventNotes', 'pinnedEventIds', - 'version', 'savedObjectId', 'created', 'createdBy', 'updated', 'updatedBy', - 'templateTimelineId', - 'templateTimelineVersion', ]; From 467bdde89e5fdad1590d565a9294acc270461de3 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 5 May 2020 12:15:13 -0400 Subject: [PATCH 15/21] review x --- .../open_timeline/use_timeline_types.tsx | 10 +++---- .../timeline/routes/import_timelines_route.ts | 27 ++++++++++--------- .../timeline/routes/utils/import_timelines.ts | 7 +++-- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/siem/public/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/siem/public/components/open_timeline/use_timeline_types.tsx index 0c991fa6744f61..8c9b40d7b1b944 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/use_timeline_types.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/use_timeline_types.tsx @@ -27,7 +27,7 @@ export const useTimelineTypes = (): { tabName === TimelineType.default || tabName === TimelineType.template ? tabName : null ); - const getTabs: (timelineTabsStyle: TimelineTabsStyle) => TimelineTab[] = ( + const getFilterOrTabs: (timelineTabsStyle: TimelineTabsStyle) => TimelineTab[] = ( timelineTabsStyle: TimelineTabsStyle ) => [ { @@ -65,7 +65,7 @@ export const useTimelineTypes = (): { return ( <> - {getTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => ( + {getFilterOrTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => ( ); - }, [getTabs, tabName]); + }, [tabName]); const timelineFilters = useMemo(() => { return ( <> - {getTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => ( + {getFilterOrTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => ( ); - }, [getTabs, timelineTypes]); + }, [timelineTypes]); return { timelineTypes, diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts index 99621f1391acb9..f1bfd41d0b7e26 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts @@ -119,35 +119,35 @@ export const importTimelinesRoute = ( return null; } const { - savedObjectId, + savedObjectId = null, pinnedEventIds, globalNotes, eventNotes, templateTimelineId, templateTimelineVersion, timelineType, + version = null, } = parsedTimeline; const parsedTimelineObject = omit( timelineSavedObjectOmittedFields, parsedTimeline ); + let newTimeline = null; try { const templateTimeline = templateTimelineId != null ? await getTemplateTimeline(frameworkRequest, templateTimelineId) : null; + const timeline = - templateTimeline?.savedObjectId != null || savedObjectId != null - ? await getTimeline( - frameworkRequest, - templateTimeline?.savedObjectId ?? savedObjectId - ) - : null; + savedObjectId != null && + (await getTimeline(frameworkRequest, savedObjectId)); const isHandlingTemplateTimeline = timelineType === TimelineType.template; + if ( (timeline == null && !isHandlingTemplateTimeline) || - (templateTimeline == null && isHandlingTemplateTimeline) + (timeline == null && templateTimeline == null && isHandlingTemplateTimeline) ) { // create timeline / template timeline newTimeline = await createTimelines( @@ -165,6 +165,7 @@ export const importTimelinesRoute = ( status_code: 200, }); } else if ( + timeline && timeline != null && templateTimeline != null && isHandlingTemplateTimeline @@ -172,8 +173,8 @@ export const importTimelinesRoute = ( // update template timeline const errorObj = checkIsFailureCases( isHandlingTemplateTimeline, - timeline.version, - templateTimeline.templateTimelineVersion ?? null, + version, + templateTimelineVersion ?? null, timeline, templateTimeline ); @@ -198,16 +199,16 @@ export const importTimelinesRoute = ( } else { resolve( createBulkErrorObject({ - id: savedObjectId, + id: savedObjectId ?? 'unknown', statusCode: 409, - message: `timeline_id: "${timeline?.savedObjectId}" already exists`, + message: `timeline_id: "${savedObjectId}" already exists`, }) ); } } catch (err) { resolve( createBulkErrorObject({ - id: savedObjectId, + id: savedObjectId ?? 'unknown', statusCode: 400, message: err.message, }) diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts index a49627d40c8f57..9b08ce6cd0ce01 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts @@ -18,7 +18,8 @@ export interface ImportTimelinesSchema { } export type ImportedTimeline = SavedTimeline & { - savedObjectId: string; + savedObjectId: string | null; + version: string | null; pinnedEventIds: string[]; globalNotes: NoteResult[]; eventNotes: NoteResult[]; @@ -90,12 +91,10 @@ export const timelineSavedObjectOmittedFields = [ 'globalNotes', 'eventNotes', 'pinnedEventIds', - 'version', 'savedObjectId', 'created', 'createdBy', 'updated', 'updatedBy', - 'templateTimelineId', - 'templateTimelineVersion', + 'version', ]; From faed495e03fbeefbbfbd12d3f332ed543469a128 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Tue, 5 May 2020 17:51:06 +0100 Subject: [PATCH 16/21] fix create note for template timeline --- .../server/lib/timeline/routes/import_timelines_route.ts | 8 ++++---- .../server/lib/timeline/routes/utils/import_timelines.ts | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts index 6fe6d396ed0b06..2b1cdfa7d5fb4a 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts @@ -128,12 +128,11 @@ export const importTimelinesRoute = ( timelineType, version = null, } = parsedTimeline; + const isHandlingTemplateTimeline = timelineType === TimelineType.template; const parsedTimelineObject = omit( timelineSavedObjectOmittedFields, parsedTimeline ); - console.log('parsedTimeline', JSON.stringify(parsedTimeline)); - console.log('parsedTimelineObject', JSON.stringify(parsedTimelineObject)); let newTimeline = null; try { const templateTimeline = @@ -144,7 +143,6 @@ export const importTimelinesRoute = ( const timeline = savedObjectId != null && (await getTimeline(frameworkRequest, savedObjectId)); - const isHandlingTemplateTimeline = timelineType === TimelineType.template; if ( (timeline == null && !isHandlingTemplateTimeline) || @@ -157,7 +155,9 @@ export const importTimelinesRoute = ( null, // timelineSavedObjectId null, // timelineVersion pinnedEventIds, - [...globalNotes, ...eventNotes], + isHandlingTemplateTimeline + ? globalNotes + : [...globalNotes, ...eventNotes], [] // existing note ids ); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts index b829b550334b5a..5e40ba24b3aa45 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts @@ -87,6 +87,10 @@ export const isBulkError = ( return has('error', importRuleResponse); }; +/** + * This fields do not exists in savedObject mapping, but exist in Users' import, + * exclude them here to avoid creating savedObject failure + */ export const timelineSavedObjectOmittedFields = [ 'globalNotes', 'eventNotes', From 1b506c7c93555fcaedc16ee95cda0bb885b57fcf Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Tue, 5 May 2020 17:57:03 +0100 Subject: [PATCH 17/21] rename timelineTypes to timelineType --- .../public/components/open_timeline/index.test.tsx | 2 +- .../siem/public/components/open_timeline/index.tsx | 6 +++--- .../open_timeline_modal/index.test.tsx | 2 +- .../components/open_timeline/translations.ts | 6 +++--- .../open_timeline/use_timeline_types.tsx | 14 +++++++------- .../public/components/recent_timelines/index.tsx | 2 +- .../timeline/selectable_timeline/index.tsx | 8 ++++---- .../containers/timeline/all/index.gql_query.ts | 4 ++-- .../siem/public/containers/timeline/all/index.tsx | 8 ++++---- .../plugins/siem/public/graphql/introspection.json | 2 +- x-pack/plugins/siem/public/graphql/types.ts | 4 ++-- x-pack/plugins/siem/public/store/timeline/model.ts | 2 +- .../siem/server/graphql/timeline/resolvers.ts | 2 +- .../siem/server/graphql/timeline/schema.gql.ts | 2 +- x-pack/plugins/siem/server/graphql/types.ts | 4 ++-- .../siem/server/lib/timeline/saved_object.ts | 12 ++++++------ 16 files changed, 40 insertions(+), 40 deletions(-) diff --git a/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx index 64f80c52b2d16c..8a18895381be0d 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx @@ -31,7 +31,7 @@ jest.mock('../../containers/timeline/all', () => { jest.mock('./use_timeline_types', () => { return { useTimelineTypes: jest.fn().mockReturnValue({ - timelineTypes: 'default', + timelineType: 'default', timelineTabs:
, timelineFilters:
, }), diff --git a/x-pack/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/index.tsx index 24d63324c11f57..ed22673f07a780 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/index.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/index.tsx @@ -105,7 +105,7 @@ export const StatefulOpenTimelineComponent = React.memo( /** The requested field to sort on */ const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); - const { timelineTypes, timelineTabs, timelineFilters } = useTimelineTypes(); + const { timelineType, timelineTabs, timelineFilters } = useTimelineTypes(); const { fetchAllTimeline, timelines, loading, totalCount } = useGetAllTimeline(); const refetch = useCallback(() => { @@ -117,9 +117,9 @@ export const StatefulOpenTimelineComponent = React.memo( search, sort: { sortField: sortField as SortFieldTimeline, sortOrder: sortDirection as Direction }, onlyUserFavorite: onlyFavorites, - timelineTypes, + timelineType, }); - }, [pageIndex, pageSize, search, sortField, sortDirection, timelineTypes, onlyFavorites]); + }, [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) => { diff --git a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx index 9c497c4fdcd600..178c69e6957e1d 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx @@ -31,7 +31,7 @@ jest.mock('../../../containers/timeline/all', () => { jest.mock('../use_timeline_types', () => { return { useTimelineTypes: jest.fn().mockReturnValue({ - timelineTypes: 'default', + timelineType: 'default', timelineTabs:
, timelineFilters:
, }), diff --git a/x-pack/plugins/siem/public/components/open_timeline/translations.ts b/x-pack/plugins/siem/public/components/open_timeline/translations.ts index 4fe48d9bc3ca48..80c044c0a1d9f8 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/translations.ts +++ b/x-pack/plugins/siem/public/components/open_timeline/translations.ts @@ -146,10 +146,10 @@ export const SUCCESSFULLY_EXPORTED_TIMELINES = (totalTimelines: number) => 'Successfully exported {totalTimelines, plural, =0 {all timelines} =1 {{totalTimelines} timeline} other {{totalTimelines} timelines}}', }); -export const FILTER_TIMELINES = (timelineTypes: string) => +export const FILTER_TIMELINES = (timelineType: string) => i18n.translate('xpack.siem.open.timeline.filterByTimelineTypesTitle', { - values: { timelineTypes }, - defaultMessage: 'Only {timelineTypes}', + values: { timelineType }, + defaultMessage: 'Only {timelineType}', }); export const TAB_TIMELINES = i18n.translate('xpack.siem.timelines.components.tabs.timelinesTitle', { diff --git a/x-pack/plugins/siem/public/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/siem/public/components/open_timeline/use_timeline_types.tsx index 8c9b40d7b1b944..1e23bc5bdda3cb 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/use_timeline_types.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/use_timeline_types.tsx @@ -17,13 +17,13 @@ import * as i18n from './translations'; import { TimelineTabsStyle, TimelineTab } from './types'; export const useTimelineTypes = (): { - timelineTypes: TimelineTypeLiteralWithNull; + timelineType: TimelineTypeLiteralWithNull; timelineTabs: JSX.Element; timelineFilters: JSX.Element; } => { const urlSearch = useGetUrlSearch(navTabs.timelines); const { tabName } = useParams<{ pageName: string; tabName: string }>(); - const [timelineTypes, setTimelineTypes] = useState( + const [timelineType, setTimelineTypes] = useState( tabName === TimelineType.default || tabName === TimelineType.template ? tabName : null ); @@ -52,13 +52,13 @@ export const useTimelineTypes = (): { const onFilterClicked = useCallback( (timelineTabsStyle, tabId) => { - if (timelineTabsStyle === TimelineTabsStyle.filter && tabId === timelineTypes) { + if (timelineTabsStyle === TimelineTabsStyle.filter && tabId === timelineType) { setTimelineTypes(null); } else { setTimelineTypes(tabId); } }, - [timelineTypes, setTimelineTypes] + [timelineType, setTimelineTypes] ); const timelineTabs = useMemo(() => { @@ -87,7 +87,7 @@ export const useTimelineTypes = (): { <> {getFilterOrTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => ( @@ -96,10 +96,10 @@ export const useTimelineTypes = (): { ))} ); - }, [timelineTypes]); + }, [timelineType]); return { - timelineTypes, + timelineType, timelineTabs, timelineFilters, }; diff --git a/x-pack/plugins/siem/public/components/recent_timelines/index.tsx b/x-pack/plugins/siem/public/components/recent_timelines/index.tsx index d88f2bddf3159f..d7f6a6f3955e26 100644 --- a/x-pack/plugins/siem/public/components/recent_timelines/index.tsx +++ b/x-pack/plugins/siem/public/components/recent_timelines/index.tsx @@ -76,7 +76,7 @@ const StatefulRecentTimelinesComponent = React.memo( sortOrder: Direction.desc, }, onlyUserFavorite: filterBy === 'favorites', - timelineTypes: null, + timelineType: null, }); }, [filterBy]); diff --git a/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx b/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx index 7228344d7cc0f6..964bb2061333d4 100644 --- a/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx +++ b/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx @@ -73,7 +73,7 @@ const TIMELINE_ITEM_HEIGHT = 50; export interface GetSelectableOptions { timelines: OpenTimelineResult[]; onlyFavorites: boolean; - timelineTypes?: TimelineTypeLiteralWithNull; + timelineType?: TimelineTypeLiteralWithNull; searchTimelineValue: string; } @@ -82,7 +82,7 @@ interface SelectableTimelineProps { getSelectableOptions: ({ timelines, onlyFavorites, - timelineTypes, + timelineType, searchTimelineValue, }: GetSelectableOptions) => EuiSelectableOption[]; onClosePopover: () => void; @@ -232,7 +232,7 @@ const SelectableTimelineComponent: React.FC = ({ sortOrder: Direction.desc, }, onlyUserFavorite: onlyFavorites, - timelineTypes: TimelineType.default, + timelineType: TimelineType.default, }); }, [onlyFavorites, pageSize, searchTimelineValue]); @@ -270,7 +270,7 @@ const SelectableTimelineComponent: React.FC = ({ timelines, onlyFavorites, searchTimelineValue, - timelineTypes: TimelineType.default, + timelineType: TimelineType.default, })} > {(list, search) => ( diff --git a/x-pack/plugins/siem/public/containers/timeline/all/index.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/all/index.gql_query.ts index 521ae77919a42a..76aef8de4ad84c 100644 --- a/x-pack/plugins/siem/public/containers/timeline/all/index.gql_query.ts +++ b/x-pack/plugins/siem/public/containers/timeline/all/index.gql_query.ts @@ -12,14 +12,14 @@ export const allTimelinesQuery = gql` $search: String $sort: SortTimeline $onlyUserFavorite: Boolean - $timelineTypes: String + $timelineType: String ) { getAllTimeline( pageInfo: $pageInfo search: $search sort: $sort onlyUserFavorite: $onlyUserFavorite - timelineTypes: $timelineTypes + timelineType: $timelineType ) { totalCount timeline { diff --git a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx index 273b267405ae10..05aa35f69b69db 100644 --- a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx +++ b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx @@ -30,7 +30,7 @@ export interface AllTimelinesArgs { pageInfo, search, sort, - timelineTypes, + timelineType, }: AllTimelinesVariables) => void; timelines: OpenTimelineResult[]; loading: boolean; @@ -42,7 +42,7 @@ export interface AllTimelinesVariables { pageInfo: PageInfoTimeline; search: string; sort: SortTimeline; - timelineTypes: TimelineTypeLiteralWithNull; + timelineType: TimelineTypeLiteralWithNull; } export const ALL_TIMELINE_QUERY_ID = 'FETCH_ALL_TIMELINES'; @@ -94,7 +94,7 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { }); const fetchAllTimeline = useCallback( - async ({ onlyUserFavorite, pageInfo, search, sort, timelineTypes }: AllTimelinesVariables) => { + async ({ onlyUserFavorite, pageInfo, search, sort, timelineType }: AllTimelinesVariables) => { let didCancel = false; const abortCtrl = new AbortController(); @@ -111,7 +111,7 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { pageInfo, search, sort, - timelineTypes, + timelineType, }; const response = await apolloClient.query< GetAllTimeline.Query, diff --git a/x-pack/plugins/siem/public/graphql/introspection.json b/x-pack/plugins/siem/public/graphql/introspection.json index 22fb703eec969d..c2b21957a90565 100644 --- a/x-pack/plugins/siem/public/graphql/introspection.json +++ b/x-pack/plugins/siem/public/graphql/introspection.json @@ -251,7 +251,7 @@ "defaultValue": null }, { - "name": "timelineTypes", + "name": "timelineType", "description": "", "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "defaultValue": null diff --git a/x-pack/plugins/siem/public/graphql/types.ts b/x-pack/plugins/siem/public/graphql/types.ts index d620cde636eabe..dd4e967b185b9b 100644 --- a/x-pack/plugins/siem/public/graphql/types.ts +++ b/x-pack/plugins/siem/public/graphql/types.ts @@ -2236,7 +2236,7 @@ export interface GetAllTimelineQueryArgs { onlyUserFavorite?: Maybe; - timelineTypes?: Maybe; + timelineType?: Maybe; } export interface AuthenticationsSourceArgs { timerange: TimerangeInput; @@ -4014,7 +4014,7 @@ export namespace GetAllTimeline { search?: Maybe; sort?: Maybe; onlyUserFavorite?: Maybe; - timelineTypes?: Maybe; + timelineType?: Maybe; }; export type Query = { diff --git a/x-pack/plugins/siem/public/store/timeline/model.ts b/x-pack/plugins/siem/public/store/timeline/model.ts index 7885064380eff4..54e19812634ac2 100644 --- a/x-pack/plugins/siem/public/store/timeline/model.ts +++ b/x-pack/plugins/siem/public/store/timeline/model.ts @@ -80,7 +80,7 @@ export interface TimelineModel { }; /** Title */ title: string; - /** timelineTypes: default | template */ + /** timelineType: default | template */ timelineType: TimelineTypeLiteralWithNull; /** an unique id for template timeline */ templateTimelineId: string | null; diff --git a/x-pack/plugins/siem/server/graphql/timeline/resolvers.ts b/x-pack/plugins/siem/server/graphql/timeline/resolvers.ts index 93dae73ec041fa..a40ef5466c7808 100644 --- a/x-pack/plugins/siem/server/graphql/timeline/resolvers.ts +++ b/x-pack/plugins/siem/server/graphql/timeline/resolvers.ts @@ -53,7 +53,7 @@ export const createTimelineResolvers = ( args.pageInfo || null, args.search || null, args.sort || null, - args.timelineTypes || null + args.timelineType || null ); }, }, diff --git a/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts index 0626662794783e..a1c13fd21a88e9 100644 --- a/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts @@ -278,7 +278,7 @@ export const timelineSchema = gql` extend type Query { getOneTimeline(id: ID!): TimelineResult! - getAllTimeline(pageInfo: PageInfoTimeline, search: String, sort: SortTimeline, onlyUserFavorite: Boolean, timelineTypes: String): ResponseTimelines! + getAllTimeline(pageInfo: PageInfoTimeline, search: String, sort: SortTimeline, onlyUserFavorite: Boolean, timelineType: String): ResponseTimelines! } extend type Mutation { diff --git a/x-pack/plugins/siem/server/graphql/types.ts b/x-pack/plugins/siem/server/graphql/types.ts index 956822cb92bd30..d74086357edbeb 100644 --- a/x-pack/plugins/siem/server/graphql/types.ts +++ b/x-pack/plugins/siem/server/graphql/types.ts @@ -2238,7 +2238,7 @@ export interface GetAllTimelineQueryArgs { onlyUserFavorite?: Maybe; - timelineTypes?: Maybe; + timelineType?: Maybe; } export interface AuthenticationsSourceArgs { timerange: TimerangeInput; @@ -2696,7 +2696,7 @@ export namespace QueryResolvers { onlyUserFavorite?: Maybe; - timelineTypes?: Maybe; + timelineType?: Maybe; } } diff --git a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts index 84d39d58210e54..99857fa8d7e140 100644 --- a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts @@ -48,7 +48,7 @@ export interface Timeline { pageInfo: PageInfoTimeline | null, search: string | null, sort: SortTimeline | null, - timelineTypes: string | null + timelineType: string | null ) => Promise; persistFavorite: ( @@ -97,13 +97,13 @@ export const getTimelineByTemplateTimelineId = async ( /** The filter here is able to handle the legacy data, * which has no timelineType exists in the savedObject */ -const getTimelineTypeFilter = (timelineTypes: string | null) => { - if (timelineTypes === TimelineType.template) { +const getTimelineTypeFilter = (timelineType: string | null) => { + if (timelineType === TimelineType.template) { /** Show only whose timelineType is "template" */ return `siem-ui-timeline.attributes.timelineType: ${TimelineType.template}`; } - if (timelineTypes === TimelineType.default) { + if (timelineType === TimelineType.default) { /** Show me every timeline whose timelineType is not "template". * which includes timelineType === 'default' and * those timelineType doesn't exists */ @@ -119,7 +119,7 @@ export const getAllTimeline = async ( pageInfo: PageInfoTimeline | null, search: string | null, sort: SortTimeline | null, - timelineTypes: string | null + timelineType: string | null ): Promise => { const options: SavedObjectsFindOptions = { type: timelineSavedObjectType, @@ -129,7 +129,7 @@ export const getAllTimeline = async ( searchFields: onlyUserFavorite ? ['title', 'description', 'favorite.keySearch'] : ['title', 'description'], - filter: getTimelineTypeFilter(timelineTypes), + filter: getTimelineTypeFilter(timelineType), sortField: sort != null ? sort.sortField : undefined, sortOrder: sort != null ? sort.sortOrder : undefined, }; From daf64417f072ab15fb74f7296da6c10546e1008e Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Tue, 5 May 2020 18:47:09 +0100 Subject: [PATCH 18/21] update unit test --- .../components/open_timeline/index.test.tsx | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx index 8a18895381be0d..b7c63ec7a47008 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx @@ -17,6 +17,8 @@ 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'); @@ -29,11 +31,12 @@ jest.mock('../../containers/timeline/all', () => { }; }); jest.mock('./use_timeline_types', () => { + const originalModule = jest.requireActual('../../containers/timeline/all'); return { useTimelineTypes: jest.fn().mockReturnValue({ timelineType: 'default', - timelineTabs:
, - timelineFilters:
, + timelineTabs:
, + timelineFilters:
, }), }; }); @@ -499,6 +502,30 @@ describe('StatefulOpenTimeline', () => { .text() ).toEqual('elastic'); }); + + test('it renders the title', async () => { + const wrapper = mount( + + + + + + + + ); + + await wait(); + + expect(wrapper.find(`[data-test-subj="timeline-${TimelineTabsStyle.tab}"]`).exists()).toEqual( + true + ); + }); }); describe('#resetSelectionState', () => { From 0090d226c191210ef0e23d287e7ba1825d6b0d0d Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Tue, 5 May 2020 19:05:24 +0100 Subject: [PATCH 19/21] fix types --- .../siem/public/components/open_timeline/index.test.tsx | 1 - .../plugins/siem/public/components/recent_timelines/index.tsx | 4 +++- x-pack/plugins/siem/public/containers/timeline/all/index.tsx | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx index b7c63ec7a47008..731c6d1ca9806a 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx @@ -31,7 +31,6 @@ jest.mock('../../containers/timeline/all', () => { }; }); jest.mock('./use_timeline_types', () => { - const originalModule = jest.requireActual('../../containers/timeline/all'); return { useTimelineTypes: jest.fn().mockReturnValue({ timelineType: 'default', diff --git a/x-pack/plugins/siem/public/components/recent_timelines/index.tsx b/x-pack/plugins/siem/public/components/recent_timelines/index.tsx index d7f6a6f3955e26..d3532d9fd1025f 100644 --- a/x-pack/plugins/siem/public/components/recent_timelines/index.tsx +++ b/x-pack/plugins/siem/public/components/recent_timelines/index.tsx @@ -10,6 +10,8 @@ import React, { useCallback, useMemo, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; +import { TimelineType } from '../../../common/types/timeline'; + import { useGetAllTimeline } from '../../containers/timeline/all'; import { SortFieldTimeline, Direction } from '../../graphql/types'; import { queryTimelineById, dispatchUpdateTimeline } from '../open_timeline/helpers'; @@ -76,7 +78,7 @@ const StatefulRecentTimelinesComponent = React.memo( sortOrder: Direction.desc, }, onlyUserFavorite: filterBy === 'favorites', - timelineType: null, + timelineType: TimelineType.default, }); }, [filterBy]); diff --git a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx index 05aa35f69b69db..e1d1edc1a8cecb 100644 --- a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx +++ b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx @@ -6,7 +6,7 @@ import { getOr, noop } from 'lodash/fp'; import memoizeOne from 'memoize-one'; -import { useCallback, useState, useRef, useEffect } from 'react'; +import { useCallback, useState, useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { OpenTimelineResult } from '../../../components/open_timeline/types'; @@ -17,7 +17,7 @@ import { SortTimeline, TimelineResult, } from '../../../graphql/types'; -import { inputsModel, inputsActions } from '../../../store/inputs'; +import { inputsActions } from '../../../store/inputs'; import { useApolloClient } from '../../../utils/apollo_context'; import { allTimelinesQuery } from './index.gql_query'; From 929ad24b06f90d101d697fb0647e07645cbe412c Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Tue, 5 May 2020 19:13:02 +0100 Subject: [PATCH 20/21] update filter for timeline savedObject --- .../siem/server/lib/timeline/saved_object.ts | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts index 99857fa8d7e140..6d022ab42fa7b1 100644 --- a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts @@ -98,19 +98,12 @@ export const getTimelineByTemplateTimelineId = async ( /** The filter here is able to handle the legacy data, * which has no timelineType exists in the savedObject */ const getTimelineTypeFilter = (timelineType: string | null) => { - if (timelineType === TimelineType.template) { - /** Show only whose timelineType is "template" */ - return `siem-ui-timeline.attributes.timelineType: ${TimelineType.template}`; - } - - if (timelineType === TimelineType.default) { - /** Show me every timeline whose timelineType is not "template". - * which includes timelineType === 'default' and - * those timelineType doesn't exists */ - return `not siem-ui-timeline.attributes.timelineType: ${TimelineType.template}`; - } - - return undefined; + return timelineType === TimelineType.template + ? `siem-ui-timeline.attributes.timelineType: ${TimelineType.template}` /** Show only whose timelineType exists and equals to "template" */ + : /** Show me every timeline whose timelineType is not "template". + * which includes timelineType === 'default' and + * those timelineType doesn't exists */ + `not siem-ui-timeline.attributes.timelineType: ${TimelineType.template}`; }; export const getAllTimeline = async ( From c42f1d7b0ddeaccfeabe133358bdeb4745ddc697 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Tue, 5 May 2020 19:56:32 +0100 Subject: [PATCH 21/21] fix lint error --- x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts b/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts index 07bb3257e2ab8a..6de10bffb1325c 100644 --- a/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts +++ b/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts @@ -44,7 +44,5 @@ export const pickSavedTimeline = ( savedTimeline.templateTimelineVersion = null; } - console.log('pickSavedTimeline', JSON.stringify(savedTimeline)); - return savedTimeline; };