From 2f4ec3d52b231259f07b206b87eb7705604a3092 Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Thu, 19 Mar 2020 18:37:56 -0600 Subject: [PATCH] ## [SIEM] Overview: Recent cases widget Implements the new `Recent cases` widget on the Overview page. Recent cases shows the last 3 recently created cases, per the following animated gif: ![recent-cases](https://user-images.githubusercontent.com/4459398/77357982-ae550a80-6d0e-11ea-90d0-62fa5407eea5.gif) ### Markdown case descriptions Markdown case descriptions are rendered, per the following animated gif: ![markdown-description](https://user-images.githubusercontent.com/4459398/77358163-f7a55a00-6d0e-11ea-8b85-dd4b3ff093ee.gif) ### My recently reported cases My recently reported cases filters the widget to show only cases created by the logged-in user, per the following animated gif: ![my-recent-cases](https://user-images.githubusercontent.com/4459398/77358223-14419200-6d0f-11ea-8e4a-25cd55fdfc44.gif) ### No cases state A message welcoming the user to create a case is displayed when no cases exist, per the following screenshot: ![no-cases-created](https://user-images.githubusercontent.com/4459398/77358338-4ce16b80-6d0f-11ea-98d3-5de1be19a935.png) ### Other changes - [x] Case-related links were updated to ensure URL state parameters, e.g. global date selection, carry-over as the user navigates through case views - [x] Recent timelines was updated to only show the last 3 recent timelines (down from 5) - [x] All sidebar widgets have slightly more compact spacing Tested in: * Chrome `80.0.3987.149` * Firefox `74.0` * Safari `13.0.5` --- .../components/link_to/redirect_to_case.tsx | 13 +- .../siem/public/components/links/index.tsx | 30 +++-- .../navigation/breadcrumbs/index.ts | 17 ++- .../public/components/news_feed/news_feed.tsx | 8 +- .../components/news_feed/post/index.tsx | 1 + .../components/recent_cases/filters/index.tsx | 54 ++++++++ .../public/components/recent_cases/index.tsx | 75 +++++++++++ .../recent_cases/no_cases/index.tsx | 34 +++++ .../components/recent_cases/recent_cases.tsx | 60 +++++++++ .../components/recent_cases/translations.ts | 37 ++++++ .../public/components/recent_cases/types.ts | 7 + .../recent_timelines/counts/index.tsx | 2 +- .../recent_timelines/filters/index.tsx | 6 +- .../components/recent_timelines/index.tsx | 9 +- .../recent_timelines/recent_timelines.tsx | 9 +- .../recent_timelines/translations.ts | 8 ++ .../components/url_state/index.test.tsx | 4 +- .../siem/public/components/url_state/types.ts | 16 ++- .../public/containers/case/use_get_cases.tsx | 31 +++-- .../case/components/all_cases/index.test.tsx | 4 +- .../pages/case/components/all_cases/index.tsx | 10 +- .../pages/case/components/case_view/index.tsx | 5 +- .../case/components/configure_cases/index.tsx | 10 +- .../public/pages/case/configure_cases.tsx | 47 ++++--- .../siem/public/pages/case/create_case.tsx | 37 ++++-- .../plugins/siem/public/pages/case/utils.ts | 12 +- .../public/pages/home/home_navigations.tsx | 2 +- .../public/pages/overview/sidebar/index.tsx | 20 ++- .../public/pages/overview/sidebar/sidebar.tsx | 123 +++++++++++++----- .../public/pages/overview/translations.ts | 4 + 30 files changed, 561 insertions(+), 134 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/recent_cases/filters/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/recent_cases/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/recent_cases/no_cases/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/recent_cases/recent_cases.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/recent_cases/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/recent_cases/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_case.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_case.tsx index 3056b166c1153..da20d989e135c 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_case.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_case.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; +import { appendSearch } from './helpers'; import { RedirectWrapper } from './redirect_wrapper'; import { SiemPageName } from '../../pages/home/types'; @@ -30,7 +31,11 @@ export const RedirectToConfigureCasesPage = () => ( const baseCaseUrl = `#/link-to/${SiemPageName.case}`; -export const getCaseUrl = () => baseCaseUrl; -export const getCaseDetailsUrl = (detailName: string) => `${baseCaseUrl}/${detailName}`; -export const getCreateCaseUrl = () => `${baseCaseUrl}/create`; -export const getConfigureCasesUrl = () => `${baseCaseUrl}/configure`; +export const getCaseUrl = (search: string | null) => + `${baseCaseUrl}${appendSearch(search ?? undefined)}`; +export const getCaseDetailsUrl = ({ id, search }: { id: string; search: string | null }) => + `${baseCaseUrl}/${encodeURIComponent(id)}${appendSearch(search ?? undefined)}`; +export const getCreateCaseUrl = (search: string | null) => + `${baseCaseUrl}/create${appendSearch(search ?? undefined)}`; +export const getConfigureCasesUrl = (search: string | null) => + `${baseCaseUrl}/configure${appendSearch(search ?? undefined)}`; diff --git a/x-pack/legacy/plugins/siem/public/components/links/index.tsx b/x-pack/legacy/plugins/siem/public/components/links/index.tsx index 04de0b1d5d3bf..39952a3a4ebed 100644 --- a/x-pack/legacy/plugins/siem/public/components/links/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/links/index.tsx @@ -26,6 +26,8 @@ import { IP_REPUTATION_LINKS_SETTING } from '../../../common/constants'; import * as i18n from '../page/network/ip_overview/translations'; import { isUrlInvalid } from '../../pages/detection_engine/rules/components/step_about_rule/helpers'; import { ExternalLinkIcon } from '../external_link_icon'; +import { navTabs } from '../../pages/home/home_navigations'; +import { useGetUrlSearch } from '../navigation/use_get_url_search'; export const DEFAULT_NUMBER_OF_LINK = 5; @@ -89,20 +91,26 @@ export const IPDetailsLink = React.memo(IPDetailsLinkComponent); const CaseDetailsLinkComponent: React.FC<{ children?: React.ReactNode; detailName: string }> = ({ children, detailName, -}) => ( - - {children ? children : detailName} - -); +}) => { + const search = useGetUrlSearch(navTabs.case); + + return ( + + {children ? children : detailName} + + ); +}; export const CaseDetailsLink = React.memo(CaseDetailsLinkComponent); CaseDetailsLink.displayName = 'CaseDetailsLink'; -export const CreateCaseLink = React.memo<{ children: React.ReactNode }>(({ children }) => ( - {children} -)); +export const CreateCaseLink = React.memo<{ children: React.ReactNode }>(({ children }) => { + const search = useGetUrlSearch(navTabs.case); + + return {children}; +}); CreateCaseLink.displayName = 'CreateCaseLink'; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts index e25fb4374bb14..155f63145ca95 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts @@ -107,7 +107,22 @@ export const getBreadcrumbsForRoute = ( ]; } if (isCaseRoutes(spyState) && object.navTabs) { - return [...siemRootBreadcrumb, ...getCaseDetailsBreadcrumbs(spyState)]; + const tempNav: SearchNavTab = { urlKey: 'case', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } + + return [ + ...siemRootBreadcrumb, + ...getCaseDetailsBreadcrumbs( + spyState, + urlStateKeys.reduce( + (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], + [] + ) + ), + ]; } if ( spyState != null && diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/news_feed.tsx b/x-pack/legacy/plugins/siem/public/components/news_feed/news_feed.tsx index 98eea1eaa6454..cd356212b4400 100644 --- a/x-pack/legacy/plugins/siem/public/components/news_feed/news_feed.tsx +++ b/x-pack/legacy/plugins/siem/public/components/news_feed/news_feed.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiSpacer } from '@elastic/eui'; import React from 'react'; import { LoadingPlaceholders } from '../page/overview/loading_placeholders'; @@ -30,12 +29,7 @@ const NewsFeedComponent: React.FC = ({ news }) => ( ) : news.length === 0 ? ( ) : ( - news.map((n: NewsItem) => ( - - - - - )) + news.map((n: NewsItem) => ) )} ); diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/post/index.tsx b/x-pack/legacy/plugins/siem/public/components/news_feed/post/index.tsx index cb2542a497f08..9cab78c9f20b1 100644 --- a/x-pack/legacy/plugins/siem/public/components/news_feed/post/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/news_feed/post/index.tsx @@ -45,6 +45,7 @@ export const Post = React.memo<{ newsItem: NewsItem }>(({ newsItem }) => {
{description}
+ diff --git a/x-pack/legacy/plugins/siem/public/components/recent_cases/filters/index.tsx b/x-pack/legacy/plugins/siem/public/components/recent_cases/filters/index.tsx new file mode 100644 index 0000000000000..6c2f64ffa1a39 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/recent_cases/filters/index.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonGroup, EuiButtonGroupOption } from '@elastic/eui'; +import React, { useMemo } from 'react'; + +import { FilterMode } from '../types'; + +import * as i18n from '../translations'; + +const MY_RECENTLY_REPORTED_ID = 'myRecentlyReported'; + +const toggleButtonIcons: EuiButtonGroupOption[] = [ + { + id: 'recentlyCreated', + label: i18n.RECENTLY_CREATED_CASES, + iconType: 'folderExclamation', + }, + { + id: MY_RECENTLY_REPORTED_ID, + label: i18n.MY_RECENTLY_REPORTED_CASES, + iconType: 'reporter', + }, +]; + +export const Filters = React.memo<{ + filterBy: FilterMode; + setFilterBy: (filterBy: FilterMode) => void; + showMyRecentlyReported: boolean; +}>(({ filterBy, setFilterBy, showMyRecentlyReported }) => { + const options = useMemo( + () => + showMyRecentlyReported + ? toggleButtonIcons + : toggleButtonIcons.filter(x => x.id !== MY_RECENTLY_REPORTED_ID), + [showMyRecentlyReported] + ); + + return ( + { + setFilterBy(f as FilterMode); + }} + isIconOnly + /> + ); +}); + +Filters.displayName = 'Filters'; diff --git a/x-pack/legacy/plugins/siem/public/components/recent_cases/index.tsx b/x-pack/legacy/plugins/siem/public/components/recent_cases/index.tsx new file mode 100644 index 0000000000000..b69cda5226406 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/recent_cases/index.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui'; +import React, { useEffect, useMemo, useRef } from 'react'; + +import { FilterOptions, QueryParams } from '../../containers/case/types'; +import { DEFAULT_QUERY_PARAMS, useGetCases } from '../../containers/case/use_get_cases'; +import { getCaseUrl } from '../link_to/redirect_to_case'; +import { useGetUrlSearch } from '../navigation/use_get_url_search'; +import { LoadingPlaceholders } from '../page/overview/loading_placeholders'; +import { navTabs } from '../../pages/home/home_navigations'; + +import { NoCases } from './no_cases'; +import { RecentCases } from './recent_cases'; +import * as i18n from './translations'; + +const usePrevious = (value: FilterOptions) => { + const ref = useRef(); + useEffect(() => { + (ref.current as unknown) = value; + }); + return ref.current; +}; + +const MAX_CASES_TO_SHOW = 3; + +const queryParams: QueryParams = { + ...DEFAULT_QUERY_PARAMS, + perPage: MAX_CASES_TO_SHOW, +}; + +const StatefulRecentCasesComponent = React.memo( + ({ filterOptions }: { filterOptions: FilterOptions }) => { + const previousFilterOptions = usePrevious(filterOptions); + const { data, loading, setFilters } = useGetCases(queryParams); + const isLoadingCases = useMemo( + () => loading.indexOf('cases') > -1 || loading.indexOf('caseUpdate') > -1, + [loading] + ); + const search = useGetUrlSearch(navTabs.case); + const allCasesLink = useMemo( + () => {i18n.VIEW_ALL_CASES}, + [search] + ); + + useEffect(() => { + if (previousFilterOptions !== undefined && previousFilterOptions !== filterOptions) { + setFilters(filterOptions); + } + }, [previousFilterOptions, filterOptions, setFilters]); + + return ( + + {isLoadingCases ? ( + + ) : !isLoadingCases && data.cases.length === 0 ? ( + + ) : ( + + )} + + + {allCasesLink} + + ); + } +); + +StatefulRecentCasesComponent.displayName = 'StatefulRecentCasesComponent'; + +export const StatefulRecentCases = React.memo(StatefulRecentCasesComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/recent_cases/no_cases/index.tsx b/x-pack/legacy/plugins/siem/public/components/recent_cases/no_cases/index.tsx new file mode 100644 index 0000000000000..9f0361311b7b6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/recent_cases/no_cases/index.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiLink } from '@elastic/eui'; +import React, { useMemo } from 'react'; + +import { getCreateCaseUrl } from '../../link_to/redirect_to_case'; +import { useGetUrlSearch } from '../../navigation/use_get_url_search'; +import { navTabs } from '../../../pages/home/home_navigations'; + +import * as i18n from '../translations'; + +const NoCasesComponent = () => { + const urlSearch = useGetUrlSearch(navTabs.case); + const newCaseLink = useMemo( + () => {` ${i18n.START_A_NEW_CASE}`}, + [urlSearch] + ); + + return ( + <> + {i18n.NO_CASES} + {newCaseLink} + {'!'} + + ); +}; + +NoCasesComponent.displayName = 'NoCasesComponent'; + +export const NoCases = React.memo(NoCasesComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/recent_cases/recent_cases.tsx b/x-pack/legacy/plugins/siem/public/components/recent_cases/recent_cases.tsx new file mode 100644 index 0000000000000..53b353e6d7fc5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/recent_cases/recent_cases.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { Case } from '../../containers/case/types'; +import { getCaseDetailsUrl } from '../link_to/redirect_to_case'; +import { Markdown } from '../markdown'; +import { useGetUrlSearch } from '../navigation/use_get_url_search'; +import { navTabs } from '../../pages/home/home_navigations'; +import { IconWithCount } from '../recent_timelines/counts'; + +import * as i18n from './translations'; + +const MarkdownContainer = styled.div` + max-height: 150px; + overflow-y: auto; + width: 300px; +`; + +const RecentCasesComponent = ({ cases }: { cases: Case[] }) => { + const search = useGetUrlSearch(navTabs.case); + + return ( + <> + {cases.map((c, i) => ( + + + + {c.title} + + + + {c.description && c.description.length && ( + + + + + + )} + {i !== cases.length - 1 && } + + + ))} + + ); +}; + +RecentCasesComponent.displayName = 'RecentCasesComponent'; + +export const RecentCases = React.memo(RecentCasesComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/recent_cases/translations.ts b/x-pack/legacy/plugins/siem/public/components/recent_cases/translations.ts new file mode 100644 index 0000000000000..d2318e5db88c3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/recent_cases/translations.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const COMMENTS = i18n.translate('xpack.siem.recentCases.commentsTooltip', { + defaultMessage: 'Comments', +}); + +export const MY_RECENTLY_REPORTED_CASES = i18n.translate( + 'xpack.siem.overview.myRecentlyReportedCasesButtonLabel', + { + defaultMessage: 'My recently reported cases', + } +); + +export const NO_CASES = i18n.translate('xpack.siem.recentCases.noCasesMessage', { + defaultMessage: 'No cases have been created yet. Put your detective hat on and', +}); + +export const RECENTLY_CREATED_CASES = i18n.translate( + 'xpack.siem.overview.recentlyCreatedCasesButtonLabel', + { + defaultMessage: 'Recently created cases', + } +); + +export const START_A_NEW_CASE = i18n.translate('xpack.siem.recentCases.startNewCaseLink', { + defaultMessage: 'start a new case', +}); + +export const VIEW_ALL_CASES = i18n.translate('xpack.siem.recentCases.viewAllCasesLink', { + defaultMessage: 'View all cases', +}); diff --git a/x-pack/legacy/plugins/siem/public/components/recent_cases/types.ts b/x-pack/legacy/plugins/siem/public/components/recent_cases/types.ts new file mode 100644 index 0000000000000..29c7072ce0be6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/recent_cases/types.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type FilterMode = 'recentlyCreated' | 'myRecentlyReported'; diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/counts/index.tsx b/x-pack/legacy/plugins/siem/public/components/recent_timelines/counts/index.tsx index e04b6319cfb24..c80530b245cf3 100644 --- a/x-pack/legacy/plugins/siem/public/components/recent_timelines/counts/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/recent_timelines/counts/index.tsx @@ -21,7 +21,7 @@ const FlexGroup = styled(EuiFlexGroup)` margin-right: 16px; `; -const IconWithCount = React.memo<{ count: number; icon: string; tooltip: string }>( +export const IconWithCount = React.memo<{ count: number; icon: string; tooltip: string }>( ({ count, icon, tooltip }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/filters/index.tsx b/x-pack/legacy/plugins/siem/public/components/recent_timelines/filters/index.tsx index de8a3de8094d0..d7271197b9cea 100644 --- a/x-pack/legacy/plugins/siem/public/components/recent_timelines/filters/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/recent_timelines/filters/index.tsx @@ -9,15 +9,17 @@ import React from 'react'; import { FilterMode } from '../types'; +import * as i18n from '../translations'; + const toggleButtonIcons: EuiButtonGroupOption[] = [ { id: 'favorites', - label: 'Favorites', + label: i18n.FAVORITES, iconType: 'starFilled', }, { id: `recently-updated`, - label: 'Last updated', + label: i18n.LAST_UPDATED, iconType: 'documentEdit', }, ]; 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 007665b47dedb..c0b13ede9611f 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 @@ -31,6 +31,8 @@ interface OwnProps { export type Props = OwnProps & PropsFromRedux; +const PAGE_SIZE = 3; + const StatefulRecentTimelinesComponent = React.memo( ({ apolloClient, filterBy, updateIsLoading, updateTimeline }) => { const onOpenTimeline: OnOpenTimeline = useCallback( @@ -58,7 +60,7 @@ const StatefulRecentTimelinesComponent = React.memo( ( {({ timelines, loading }) => ( <> {loading ? ( - + ) : ( {t.description && t.description.length && ( - <> - - - {t.description} - - + + {t.description} + )} diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/translations.ts b/x-pack/legacy/plugins/siem/public/components/recent_timelines/translations.ts index e547272fde6e1..f5934aa317242 100644 --- a/x-pack/legacy/plugins/siem/public/components/recent_timelines/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/recent_timelines/translations.ts @@ -13,6 +13,10 @@ export const ERROR_RETRIEVING_USER_DETAILS = i18n.translate( } ); +export const FAVORITES = i18n.translate('xpack.siem.recentTimelines.favoritesButtonLabel', { + defaultMessage: 'Favorites', +}); + export const NO_FAVORITE_TIMELINES = i18n.translate( 'xpack.siem.recentTimelines.noFavoriteTimelinesMessage', { @@ -21,6 +25,10 @@ export const NO_FAVORITE_TIMELINES = i18n.translate( } ); +export const LAST_UPDATED = i18n.translate('xpack.siem.recentTimelines.lastUpdatedButtonLabel', { + defaultMessage: 'Last updated', +}); + export const NO_TIMELINES = i18n.translate('xpack.siem.recentTimelines.noTimelinesMessage', { defaultMessage: "You haven't created any timelines yet. Get out there and start threat hunting!", }); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx index 10aa388449d91..4d2a717153894 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx @@ -157,9 +157,7 @@ describe('UrlStateContainer', () => { ).toEqual({ hash: '', pathname: examplePath, - search: [CONSTANTS.timelinePage].includes(page) - ? '?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))' - : `?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))`, + search: `?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))`, state: '', }); } diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts index 2cb1b0c96ad79..9d8a4a8e6a908 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts @@ -60,8 +60,20 @@ export const URL_STATE_KEYS: Record = { CONSTANTS.timerange, CONSTANTS.timeline, ], - timeline: [CONSTANTS.timeline, CONSTANTS.timerange], - case: [], + timeline: [ + CONSTANTS.appQuery, + CONSTANTS.filters, + CONSTANTS.savedQuery, + CONSTANTS.timerange, + CONSTANTS.timeline, + ], + case: [ + CONSTANTS.appQuery, + CONSTANTS.filters, + CONSTANTS.savedQuery, + CONSTANTS.timerange, + CONSTANTS.timeline, + ], }; export type LocationTypes = diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index 6c4a6ac4fe58a..ae7b8f3c043fa 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -88,6 +88,20 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS } }; +export const DEFAULT_FILTER_OPTIONS: FilterOptions = { + search: '', + reporters: [], + status: 'open', + tags: [], +}; + +export const DEFAULT_QUERY_PARAMS: QueryParams = { + page: DEFAULT_TABLE_ACTIVE_PAGE, + perPage: DEFAULT_TABLE_LIMIT, + sortField: SortFieldCase.createdAt, + sortOrder: 'desc', +}; + const initialData: AllCases = { cases: [], countClosedCases: null, @@ -109,23 +123,14 @@ interface UseGetCases extends UseGetCasesState { setQueryParams: (queryParams: QueryParams) => void; setSelectedCases: (mySelectedCases: Case[]) => void; } -export const useGetCases = (): UseGetCases => { + +export const useGetCases = (initialQueryParams?: QueryParams): UseGetCases => { const [state, dispatch] = useReducer(dataFetchReducer, { data: initialData, - filterOptions: { - search: '', - reporters: [], - status: 'open', - tags: [], - }, + filterOptions: DEFAULT_FILTER_OPTIONS, isError: false, loading: [], - queryParams: { - page: DEFAULT_TABLE_ACTIVE_PAGE, - perPage: DEFAULT_TABLE_LIMIT, - sortField: SortFieldCase.createdAt, - sortOrder: 'desc', - }, + queryParams: initialQueryParams ?? DEFAULT_QUERY_PARAMS, selectedCases: [], }); const [, dispatchToaster] = useStateToaster(); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx index 13869c79c45fd..bdcb87b483851 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx @@ -95,7 +95,9 @@ describe('AllCases', () => { .find(`a[data-test-subj="case-details-link"]`) .first() .prop('href') - ).toEqual(`#/link-to/case/${useGetCasesMockState.data.cases[0].id}`); + ).toEqual( + `#/link-to/case/${useGetCasesMockState.data.cases[0].id}?timerange=(global:(linkTo:!(timeline),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)),timeline:(linkTo:!(global),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)))` + ); expect( wrapper .find(`a[data-test-subj="case-details-link"]`) diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index e7e1e624ccba2..117975f865967 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -26,6 +26,7 @@ import { useGetCases, UpdateCase } from '../../../../containers/case/use_get_cas import { useGetCasesStatus } from '../../../../containers/case/use_get_cases_status'; import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; import { EuiBasicTableOnChange } from '../../../detection_engine/rules/types'; +import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; import { Panel } from '../../../../components/panel'; import { UtilityBar, @@ -35,19 +36,16 @@ import { UtilityBarText, } from '../../../../components/utility_bar'; import { getConfigureCasesUrl, getCreateCaseUrl } from '../../../../components/link_to'; - import { getBulkItems } from '../bulk_actions'; import { CaseHeaderPage } from '../case_header_page'; import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; import { OpenClosedStats } from '../open_closed_stats'; +import { navTabs } from '../../../home/home_navigations'; import { getActions } from './actions'; import { CasesTableFilters } from './table_filters'; import { useUpdateCases } from '../../../../containers/case/use_bulk_update_case'; -const CONFIGURE_CASES_URL = getConfigureCasesUrl(); -const CREATE_CASE_URL = getCreateCaseUrl(); - const Div = styled.div` margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; `; @@ -78,6 +76,10 @@ const getSortField = (field: string): SortFieldCase => { return SortFieldCase.createdAt; }; export const AllCases = React.memo(() => { + const search = useGetUrlSearch(navTabs.case); + const CONFIGURE_CASES_URL = useMemo(() => getConfigureCasesUrl(search), [search]); + const CREATE_CASE_URL = useMemo(() => getCreateCaseUrl(search), [search]); + const { countClosedCases, countOpenCases, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index 0ac3adeb860ff..c95b0ded526d9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -18,11 +18,13 @@ import { useGetCase } from '../../../../containers/case/use_get_case'; import { UserActionTree } from '../user_action_tree'; import { UserList } from '../user_list'; import { useUpdateCase } from '../../../../containers/case/use_update_case'; +import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; import { WrapperPage } from '../../../../components/wrapper_page'; import { getTypedPayload } from '../../../../containers/case/utils'; import { WhitePageWrapper } from '../wrappers'; import { useBasePath } from '../../../../lib/kibana'; import { CaseStatus } from '../case_status'; +import { navTabs } from '../../../home/home_navigations'; import { SpyRoute } from '../../../../utils/route/spy_routes'; interface Props { @@ -46,6 +48,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => const basePath = window.location.origin + useBasePath(); const caseLink = `${basePath}/app/siem#/case/${caseId}`; const { caseData, isLoading, updateKey, updateCaseProperty } = useUpdateCase(caseId, initialData); + const search = useGetUrlSearch(navTabs.case); // Update Fields const onUpdateField = useCallback( @@ -133,7 +136,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => css` @@ -73,6 +72,7 @@ const actionTypes: ActionType[] = [ ]; const ConfigureCasesComponent: React.FC = () => { + const search = useGetUrlSearch(navTabs.case); const { http, triggers_actions_ui, notifications, application } = useKibana().services; const [connectorIsValid, setConnectorIsValid] = useState(true); @@ -231,7 +231,7 @@ const ConfigureCasesComponent: React.FC = () => { isDisabled={isLoadingAny} isLoading={persistLoading} aria-label="Cancel" - href={CASE_URL} + href={getCaseUrl(search)} > {i18n.CANCEL} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx b/x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx index b546a88744439..b7e7ced308331 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { WrapperPage } from '../../components/wrapper_page'; import { CaseHeaderPage } from './components/case_header_page'; @@ -13,11 +13,8 @@ import { getCaseUrl } from '../../components/link_to'; import { WhitePageWrapper, SectionWrapper } from './components/wrappers'; import * as i18n from './translations'; import { ConfigureCases } from './components/configure_cases'; - -const backOptions = { - href: getCaseUrl(), - text: i18n.BACK_TO_ALL, -}; +import { useGetUrlSearch } from '../../components/navigation/use_get_url_search'; +import { navTabs } from '../home/home_navigations'; const wrapperPageStyle: Record = { paddingLeft: '0', @@ -25,18 +22,30 @@ const wrapperPageStyle: Record = { paddingBottom: '0', }; -const ConfigureCasesPageComponent: React.FC = () => ( - <> - - - - - - - - - - -); +const ConfigureCasesPageComponent: React.FC = () => { + const search = useGetUrlSearch(navTabs.case); + + const backOptions = useMemo( + () => ({ + href: getCaseUrl(search), + text: i18n.BACK_TO_ALL, + }), + [search] + ); + + return ( + <> + + + + + + + + + + + ); +}; export const ConfigureCasesPage = React.memo(ConfigureCasesPageComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx index 2c7525264f71b..bd1f6da0ca28b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { WrapperPage } from '../../components/wrapper_page'; import { Create } from './components/create'; @@ -12,20 +12,29 @@ import { SpyRoute } from '../../utils/route/spy_routes'; import { CaseHeaderPage } from './components/case_header_page'; import * as i18n from './translations'; import { getCaseUrl } from '../../components/link_to'; +import { useGetUrlSearch } from '../../components/navigation/use_get_url_search'; +import { navTabs } from '../home/home_navigations'; -const backOptions = { - href: getCaseUrl(), - text: i18n.BACK_TO_ALL, -}; +export const CreateCasePage = React.memo(() => { + const search = useGetUrlSearch(navTabs.case); -export const CreateCasePage = React.memo(() => ( - <> - - - - - - -)); + const backOptions = useMemo( + () => ({ + href: getCaseUrl(search), + text: i18n.BACK_TO_ALL, + }), + [search] + ); + + return ( + <> + + + + + + + ); +}); CreateCasePage.displayName = 'CreateCasePage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/utils.ts b/x-pack/legacy/plugins/siem/public/pages/case/utils.ts index ccb3b71a476ec..df9f0d08e728c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/utils.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/utils.ts @@ -4,16 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; import { Breadcrumb } from 'ui/chrome'; + import { getCaseDetailsUrl, getCaseUrl, getCreateCaseUrl } from '../../components/link_to'; import { RouteSpyState } from '../../utils/route/types'; import * as i18n from './translations'; -export const getBreadcrumbs = (params: RouteSpyState): Breadcrumb[] => { +export const getBreadcrumbs = (params: RouteSpyState, search: string[]): Breadcrumb[] => { + const queryParameters = !isEmpty(search[0]) ? search[0] : null; + let breadcrumb = [ { text: i18n.PAGE_TITLE, - href: getCaseUrl(), + href: getCaseUrl(queryParameters), }, ]; if (params.detailName === 'create') { @@ -21,7 +25,7 @@ export const getBreadcrumbs = (params: RouteSpyState): Breadcrumb[] => { ...breadcrumb, { text: i18n.CREATE_BC_TITLE, - href: getCreateCaseUrl(), + href: getCreateCaseUrl(queryParameters), }, ]; } else if (params.detailName != null) { @@ -29,7 +33,7 @@ export const getBreadcrumbs = (params: RouteSpyState): Breadcrumb[] => { ...breadcrumb, { text: params.state?.caseTitle ?? '', - href: getCaseDetailsUrl(params.detailName), + href: getCaseDetailsUrl({ id: params.detailName, search: queryParameters }), }, ]; } diff --git a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx index a087dca38de00..543469e2fddb7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx @@ -54,7 +54,7 @@ export const navTabs: SiemNavTab = { [SiemPageName.case]: { id: SiemPageName.case, name: i18n.CASE, - href: getCaseUrl(), + href: getCaseUrl(null), disabled: false, urlKey: 'case', }, diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/index.tsx index ad2821edde411..3797eae2bb853 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/index.tsx @@ -6,13 +6,27 @@ import React, { useState } from 'react'; -import { FilterMode } from '../../../components/recent_timelines/types'; +import { FilterMode as RecentTimelinesFilterMode } from '../../../components/recent_timelines/types'; +import { FilterMode as RecentCasesFilterMode } from '../../../components/recent_cases/types'; + import { Sidebar } from './sidebar'; export const StatefulSidebar = React.memo(() => { - const [filterBy, setFilterBy] = useState('favorites'); + const [recentTimelinesFilterBy, setRecentTimelinesFilterBy] = useState( + 'favorites' + ); + const [recentCasesFilterBy, setRecentCasesFilterBy] = useState( + 'recentlyCreated' + ); - return ; + return ( + + ); }); StatefulSidebar.displayName = 'StatefulSidebar'; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/sidebar.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/sidebar.tsx index d3b85afe62a2a..52e36b472a0ec 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/sidebar.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/sidebar.tsx @@ -8,12 +8,17 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { Filters } from '../../../components/recent_timelines/filters'; +import { Filters as RecentCasesFilters } from '../../../components/recent_cases/filters'; +import { Filters as RecentTimelinesFilters } from '../../../components/recent_timelines/filters'; import { ENABLE_NEWS_FEED_SETTING, NEWS_FEED_URL_SETTING } from '../../../../common/constants'; +import { StatefulRecentCases } from '../../../components/recent_cases'; import { StatefulRecentTimelines } from '../../../components/recent_timelines'; import { StatefulNewsFeed } from '../../../components/news_feed'; -import { FilterMode } from '../../../components/recent_timelines/types'; +import { FilterMode as RecentTimelinesFilterMode } from '../../../components/recent_timelines/types'; +import { FilterMode as RecentCasesFilterMode } from '../../../components/recent_cases/types'; +import { DEFAULT_FILTER_OPTIONS } from '../../../containers/case/use_get_cases'; import { SidebarHeader } from '../../../components/sidebar_header'; +import { useCurrentUser } from '../../../lib/kibana'; import { useApolloClient } from '../../../utils/apollo_context'; import * as i18n from '../translations'; @@ -22,35 +27,93 @@ const SidebarFlexGroup = styled(EuiFlexGroup)` width: 305px; `; +const SidebarSpacerComponent = () => ( + + + +); + +SidebarSpacerComponent.displayName = 'SidebarSpacerComponent'; +const Spacer = React.memo(SidebarSpacerComponent); + export const Sidebar = React.memo<{ - filterBy: FilterMode; - setFilterBy: (filterBy: FilterMode) => void; -}>(({ filterBy, setFilterBy }) => { - const apolloClient = useApolloClient(); - const RecentTimelinesFilters = useMemo( - () => , - [filterBy, setFilterBy] - ); - - return ( - - - {RecentTimelinesFilters} - - - - - - - - - void; + setRecentTimelinesFilterBy: (filterBy: RecentTimelinesFilterMode) => void; +}>( + ({ + recentCasesFilterBy, + recentTimelinesFilterBy, + setRecentCasesFilterBy, + setRecentTimelinesFilterBy, + }) => { + const currentUser = useCurrentUser(); + const apolloClient = useApolloClient(); + const recentCasesFilters = useMemo( + () => ( + - - - ); -}); + ), + [currentUser, recentCasesFilterBy, setRecentCasesFilterBy] + ); + const recentCasesFilterOptions = useMemo( + () => + recentCasesFilterBy === 'myRecentlyReported' && currentUser != null + ? { + ...DEFAULT_FILTER_OPTIONS, + reporters: [ + { + email: currentUser.email, + full_name: currentUser.fullName, + username: currentUser.username, + }, + ], + } + : DEFAULT_FILTER_OPTIONS, + [currentUser, recentCasesFilterBy] + ); + const recentTimelinesFilters = useMemo( + () => ( + + ), + [recentTimelinesFilterBy, setRecentTimelinesFilterBy] + ); + + return ( + + + {recentCasesFilters} + + + + + + + {recentTimelinesFilters} + + + + + + + + + + ); + } +); Sidebar.displayName = 'Sidebar'; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/translations.ts b/x-pack/legacy/plugins/siem/public/pages/overview/translations.ts index 5ccd25984bc40..601a629d86e57 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/overview/translations.ts @@ -26,6 +26,10 @@ export const PAGE_SUBTITLE = i18n.translate('xpack.siem.overview.pageSubtitle', defaultMessage: 'Security Information & Event Management with the Elastic Stack', }); +export const RECENT_CASES = i18n.translate('xpack.siem.overview.recentCasesSidebarTitle', { + defaultMessage: 'Recent cases', +}); + export const RECENT_TIMELINES = i18n.translate('xpack.siem.overview.recentTimelinesSidebarTitle', { defaultMessage: 'Recent timelines', });