From 82dd5bd91c80f9396cabcf565f1c525150012129 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 6 Feb 2020 20:54:26 -0500 Subject: [PATCH] [SIEM] update url state between page if date is relative (#56813) * update url state between page if needed it like time range * fix round up for to * simplify type * leftover cleanup * we forget to update relative date when loading the page by a refresh * pair with Garrett to make a minimal impact on the ux * fix detetections tabs --- .../public/components/link_to/link_to.tsx | 2 - .../public/components/navigation/helpers.ts | 2 +- .../public/components/url_state/helpers.ts | 137 +++++++++++-- .../components/url_state/index.test.tsx | 69 ++++++- .../url_state/initialize_redux_by_url.tsx | 162 +++++++-------- .../components/url_state/test_dependencies.ts | 12 ++ .../siem/public/components/url_state/types.ts | 20 ++ .../components/url_state/use_url_state.tsx | 185 ++++++++++-------- .../detection_engine/detection_engine.tsx | 36 ++-- 9 files changed, 413 insertions(+), 212 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx index 3eda945c9224e..dc8c696301611 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx @@ -62,13 +62,11 @@ export const LinkToPage = React.memo(({ match }) => ( component={RedirectToDetectionEnginePage} exact path={`${match.url}/:pageName(${SiemPageName.detections})`} - strict /> { +export const decodeRisonUrlState = (value: string | undefined): T | null => { try { - return value ? decode(value) : undefined; + return value ? ((decode(value) as unknown) as T) : null; } catch (error) { if (error instanceof Error && error.message.startsWith('rison decoder error')) { - return {}; + return null; } throw error; } @@ -30,18 +38,16 @@ export const decodeRisonUrlState = (value: string | undefined): RisonValue | any // eslint-disable-next-line @typescript-eslint/no-explicit-any export const encodeRisonUrlState = (state: any) => encode(state); -export const getQueryStringFromLocation = (location: Location) => location.search.substring(1); +export const getQueryStringFromLocation = (search: string) => search.substring(1); export const getParamFromQueryString = (queryString: string, key: string): string | undefined => { const queryParam = QueryString.decode(queryString)[key]; return Array.isArray(queryParam) ? queryParam[0] : queryParam; }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const replaceStateKeyInQueryString = ( - stateKey: string, - urlState: UrlState | undefined -) => (queryString: string) => { +export const replaceStateKeyInQueryString = (stateKey: string, urlState: T) => ( + queryString: string +): string => { const previousQueryValues = QueryString.decode(queryString); if (urlState == null || (typeof urlState === 'string' && urlState === '')) { delete previousQueryValues[stateKey]; @@ -60,8 +66,11 @@ export const replaceStateKeyInQueryString = ( }); }; -export const replaceQueryStringInLocation = (location: Location, queryString: string): Location => { - if (queryString === getQueryStringFromLocation(location)) { +export const replaceQueryStringInLocation = ( + location: H.Location, + queryString: string +): H.Location => { + if (queryString === getQueryStringFromLocation(location.search)) { return location; } else { return { @@ -173,3 +182,99 @@ export const makeMapStateToProps = () => { return mapStateToProps; }; + +export const updateTimerangeUrl = ( + timeRange: UrlInputsModel, + isInitializing: boolean +): UrlInputsModel => { + if (timeRange.global.timerange.kind === 'relative') { + timeRange.global.timerange.from = formatDate(timeRange.global.timerange.fromStr); + timeRange.global.timerange.to = formatDate(timeRange.global.timerange.toStr, { roundUp: true }); + } + if (timeRange.timeline.timerange.kind === 'relative' && isInitializing) { + timeRange.timeline.timerange.from = formatDate(timeRange.timeline.timerange.fromStr); + timeRange.timeline.timerange.to = formatDate(timeRange.timeline.timerange.toStr, { + roundUp: true, + }); + } + return timeRange; +}; + +export const updateUrlStateString = ({ + isInitializing, + history, + newUrlStateString, + pathName, + search, + updateTimerange, + urlKey, +}: UpdateUrlStateString): string => { + if (urlKey === CONSTANTS.appQuery) { + const queryState = decodeRisonUrlState(newUrlStateString); + if (queryState != null && queryState.query === '') { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: '', + urlStateKey: urlKey, + }); + } + } else if (urlKey === CONSTANTS.timerange && updateTimerange) { + const queryState = decodeRisonUrlState(newUrlStateString); + if (queryState != null && queryState.global != null) { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: updateTimerangeUrl(queryState, isInitializing), + urlStateKey: urlKey, + }); + } + } else if (urlKey === CONSTANTS.filters) { + const queryState = decodeRisonUrlState(newUrlStateString); + if (isEmpty(queryState)) { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: '', + urlStateKey: urlKey, + }); + } + } else if (urlKey === CONSTANTS.timeline) { + const queryState = decodeRisonUrlState(newUrlStateString); + if (queryState != null && queryState.id === '') { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: '', + urlStateKey: urlKey, + }); + } + } + return search; +}; + +export const replaceStateInLocation = ({ + history, + urlStateToReplace, + urlStateKey, + pathName, + search, +}: ReplaceStateInLocation) => { + const newLocation = replaceQueryStringInLocation( + { + hash: '', + pathname: pathName, + search, + state: '', + }, + replaceStateKeyInQueryString(urlStateKey, urlStateToReplace)(getQueryStringFromLocation(search)) + ); + if (history) { + history.replace(newLocation); + } + return newLocation.search; +}; 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 ab290c2f2fd67..5b401ed7aa460 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 @@ -10,7 +10,6 @@ import React from 'react'; import { HookWrapper } from '../../mock'; import { SiemPageName } from '../../pages/home/types'; import { RouteSpyState } from '../../utils/route/types'; - import { CONSTANTS } from './constants'; import { getMockPropsObj, @@ -22,6 +21,7 @@ import { } from './test_dependencies'; import { UrlStateContainerPropTypes } from './types'; import { useUrlStateHooks } from './use_url_state'; +import { wait } from '../../lib/helpers'; let mockProps: UrlStateContainerPropTypes; @@ -43,6 +43,12 @@ jest.mock('../search_bar', () => ({ }, })); +jest.mock('../super_date_picker', () => ({ + formatDate: (date: string) => { + return 11223344556677; + }, +})); + describe('UrlStateContainer', () => { afterEach(() => { jest.resetAllMocks(); @@ -63,19 +69,19 @@ describe('UrlStateContainer', () => { mount( useUrlStateHooks(args)} />); expect(mockSetRelativeRangeDatePicker.mock.calls[1][0]).toEqual({ - from: 1558591200000, + from: 11223344556677, fromStr: 'now-1d/d', kind: 'relative', - to: 1558677599999, + to: 11223344556677, toStr: 'now-1d/d', id: 'global', }); expect(mockSetRelativeRangeDatePicker.mock.calls[0][0]).toEqual({ - from: 1558732849370, + from: 11223344556677, fromStr: 'now-15m', kind: 'relative', - to: 1558733749370, + to: 11223344556677, toStr: 'now', id: 'timeline', }); @@ -155,4 +161,57 @@ describe('UrlStateContainer', () => { }); }); }); + + describe('After Initialization, keep Relative Date up to date for global only on detections page', () => { + test.each(testCases)( + '%o', + async (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { + mockProps = getMockPropsObj({ + page, + examplePath, + namespaceLower, + pageName, + detailName, + }).relativeTimeSearch.undefinedQuery; + const wrapper = mount( + useUrlStateHooks(args)} /> + ); + + wrapper.setProps({ + hookProps: getMockPropsObj({ + page: CONSTANTS.hostsPage, + examplePath: '/hosts', + namespaceLower: 'hosts', + pageName: SiemPageName.hosts, + detailName: undefined, + }).relativeTimeSearch.undefinedQuery, + }); + wrapper.update(); + await wait(); + + if (CONSTANTS.detectionsPage === page) { + expect(mockSetRelativeRangeDatePicker.mock.calls[3][0]).toEqual({ + from: 11223344556677, + fromStr: 'now-1d/d', + kind: 'relative', + to: 11223344556677, + toStr: 'now-1d/d', + id: 'global', + }); + + expect(mockSetRelativeRangeDatePicker.mock.calls[2][0]).toEqual({ + from: 1558732849370, + fromStr: 'now-15m', + kind: 'relative', + to: 1558733749370, + toStr: 'now', + id: 'timeline', + }); + } else { + // There is no change in url state, so that's expected we only have two actions + expect(mockSetRelativeRangeDatePicker.mock.calls.length).toEqual(2); + } + } + ); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx index 013983c78a3a5..50639289458cc 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx @@ -6,7 +6,6 @@ import { get, isEmpty } from 'lodash/fp'; import { Dispatch } from 'redux'; -import { SavedQuery } from 'src/legacy/core_plugins/data/public'; import { Query, esFilters } from 'src/plugins/data/public'; import { inputsActions } from '../../store/actions'; @@ -22,7 +21,7 @@ import { savedQueryService, siemFilterManager } from '../search_bar'; import { CONSTANTS } from './constants'; import { decodeRisonUrlState } from './helpers'; import { normalizeTimeRange } from './normalize_time_range'; -import { DispatchSetInitialStateFromUrl, SetInitialStateFromUrl } from './types'; +import { DispatchSetInitialStateFromUrl, SetInitialStateFromUrl, Timeline } from './types'; import { queryTimelineById } from '../open_timeline/helpers'; export const dispatchSetInitialStateFromUrl = ( @@ -38,80 +37,11 @@ export const dispatchSetInitialStateFromUrl = ( }: SetInitialStateFromUrl): (() => void) => () => { urlStateToUpdate.forEach(({ urlKey, newUrlStateString }) => { if (urlKey === CONSTANTS.timerange) { - const timerangeStateData: UrlInputsModel = decodeRisonUrlState(newUrlStateString); - - const globalId: InputsModelId = 'global'; - const globalLinkTo: LinkTo = { linkTo: get('global.linkTo', timerangeStateData) }; - const globalType: TimeRangeKinds = get('global.timerange.kind', timerangeStateData); - - const timelineId: InputsModelId = 'timeline'; - const timelineLinkTo: LinkTo = { linkTo: get('timeline.linkTo', timerangeStateData) }; - const timelineType: TimeRangeKinds = get('timeline.timerange.kind', timerangeStateData); - - if (isEmpty(globalLinkTo.linkTo)) { - dispatch(inputsActions.removeGlobalLinkTo()); - } else { - dispatch(inputsActions.addGlobalLinkTo({ linkToId: 'timeline' })); - } - - if (isEmpty(timelineLinkTo.linkTo)) { - dispatch(inputsActions.removeTimelineLinkTo()); - } else { - dispatch(inputsActions.addTimelineLinkTo({ linkToId: 'global' })); - } - - if (timelineType) { - if (timelineType === 'absolute') { - const absoluteRange = normalizeTimeRange( - get('timeline.timerange', timerangeStateData) - ); - dispatch( - inputsActions.setAbsoluteRangeDatePicker({ - ...absoluteRange, - id: timelineId, - }) - ); - } - if (timelineType === 'relative') { - const relativeRange = normalizeTimeRange( - get('timeline.timerange', timerangeStateData) - ); - dispatch( - inputsActions.setRelativeRangeDatePicker({ - ...relativeRange, - id: timelineId, - }) - ); - } - } - - if (globalType) { - if (globalType === 'absolute') { - const absoluteRange = normalizeTimeRange( - get('global.timerange', timerangeStateData) - ); - dispatch( - inputsActions.setAbsoluteRangeDatePicker({ - ...absoluteRange, - id: globalId, - }) - ); - } - if (globalType === 'relative') { - const relativeRange = normalizeTimeRange( - get('global.timerange', timerangeStateData) - ); - dispatch( - inputsActions.setRelativeRangeDatePicker({ - ...relativeRange, - id: globalId, - }) - ); - } - } + updateTimerange(newUrlStateString, dispatch); } + if (urlKey === CONSTANTS.appQuery && indexPattern != null) { - const appQuery: Query = decodeRisonUrlState(newUrlStateString); + const appQuery = decodeRisonUrlState(newUrlStateString); if (appQuery != null) { dispatch( inputsActions.setFilterQuery({ @@ -124,14 +54,14 @@ export const dispatchSetInitialStateFromUrl = ( } if (urlKey === CONSTANTS.filters) { - const filters: esFilters.Filter[] = decodeRisonUrlState(newUrlStateString); + const filters = decodeRisonUrlState(newUrlStateString); siemFilterManager.setFilters(filters || []); } if (urlKey === CONSTANTS.savedQuery) { - const savedQueryId: string = decodeRisonUrlState(newUrlStateString); - if (savedQueryId !== '') { - savedQueryService.getSavedQuery(savedQueryId).then((savedQueryData: SavedQuery) => { + const savedQueryId = decodeRisonUrlState(newUrlStateString); + if (savedQueryId != null && savedQueryId !== '') { + savedQueryService.getSavedQuery(savedQueryId).then(savedQueryData => { siemFilterManager.setFilters(savedQueryData.attributes.filters || []); dispatch( inputsActions.setFilterQuery({ @@ -145,7 +75,7 @@ export const dispatchSetInitialStateFromUrl = ( } if (urlKey === CONSTANTS.timeline) { - const timeline = decodeRisonUrlState(newUrlStateString); + const timeline = decodeRisonUrlState(newUrlStateString); if (timeline != null && timeline.id !== '') { queryTimelineById({ apolloClient, @@ -159,3 +89,77 @@ export const dispatchSetInitialStateFromUrl = ( } }); }; + +const updateTimerange = (newUrlStateString: string, dispatch: Dispatch) => { + const timerangeStateData = decodeRisonUrlState(newUrlStateString); + + const globalId: InputsModelId = 'global'; + const globalLinkTo: LinkTo = { linkTo: get('global.linkTo', timerangeStateData) }; + const globalType: TimeRangeKinds = get('global.timerange.kind', timerangeStateData); + + const timelineId: InputsModelId = 'timeline'; + const timelineLinkTo: LinkTo = { linkTo: get('timeline.linkTo', timerangeStateData) }; + const timelineType: TimeRangeKinds = get('timeline.timerange.kind', timerangeStateData); + + if (isEmpty(globalLinkTo.linkTo)) { + dispatch(inputsActions.removeGlobalLinkTo()); + } else { + dispatch(inputsActions.addGlobalLinkTo({ linkToId: 'timeline' })); + } + + if (isEmpty(timelineLinkTo.linkTo)) { + dispatch(inputsActions.removeTimelineLinkTo()); + } else { + dispatch(inputsActions.addTimelineLinkTo({ linkToId: 'global' })); + } + + if (timelineType) { + if (timelineType === 'absolute') { + const absoluteRange = normalizeTimeRange( + get('timeline.timerange', timerangeStateData) + ); + dispatch( + inputsActions.setAbsoluteRangeDatePicker({ + ...absoluteRange, + id: timelineId, + }) + ); + } + if (timelineType === 'relative') { + const relativeRange = normalizeTimeRange( + get('timeline.timerange', timerangeStateData) + ); + dispatch( + inputsActions.setRelativeRangeDatePicker({ + ...relativeRange, + id: timelineId, + }) + ); + } + } + + if (globalType) { + if (globalType === 'absolute') { + const absoluteRange = normalizeTimeRange( + get('global.timerange', timerangeStateData) + ); + dispatch( + inputsActions.setAbsoluteRangeDatePicker({ + ...absoluteRange, + id: globalId, + }) + ); + } + if (globalType === 'relative') { + const relativeRange = normalizeTimeRange( + get('global.timerange', timerangeStateData) + ); + dispatch( + inputsActions.setRelativeRangeDatePicker({ + ...relativeRange, + id: globalId, + }) + ); + } + } +}; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts b/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts index 4dd92ac58b0a3..dc1b8d428bb20 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts @@ -217,6 +217,18 @@ export const getMockPropsObj = ({ pageName, detailName ), + undefinedLinkQuery: getMockProps( + { + hash: '', + pathname: examplePath, + search: `?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558591200000,fromStr:now-1d%2Fd,kind:relative,to:1558677599999,toStr:now-1d%2Fd)),timeline:(linkTo:!(global),timerange:(from:1558732849370,fromStr:now-15m,kind:relative,to:1558733749370,toStr:now)))`, + state: '', + }, + page, + null, + pageName, + detailName + ), }, absoluteTimeSearch: { undefinedQuery: getMockProps( 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 be1ae1ad63bd4..05df4b1d152a0 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 @@ -5,6 +5,7 @@ */ import ApolloClient from 'apollo-client'; +import * as H from 'history'; import { ActionCreator } from 'typescript-fsa'; import { IIndexPattern, Query, esFilters } from 'src/plugins/data/public'; @@ -109,6 +110,7 @@ export type UrlStateContainerPropTypes = RouteSpyState & export interface PreviousLocationUrlState { pathName: string | undefined; + pageName: string | undefined; urlState: UrlState; } @@ -136,3 +138,21 @@ export type DispatchSetInitialStateFromUrl = ({ updateTimelineIsLoading, urlStateToUpdate, }: SetInitialStateFromUrl) => () => void; + +export interface ReplaceStateInLocation { + history?: H.History; + urlStateToReplace: T; + urlStateKey: string; + pathName: string; + search: string; +} + +export interface UpdateUrlStateString { + isInitializing: boolean; + history?: H.History; + newUrlStateString: string; + pathName: string; + search: string; + updateTimerange: boolean; + urlKey: KeyUrlState; +} diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx index d7fece5731972..28652bbe6c069 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx @@ -4,23 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Location } from 'history'; import { isEqual, difference, isEmpty } from 'lodash/fp'; import { useEffect, useRef, useState } from 'react'; -import { Query, esFilters } from 'src/plugins/data/public'; -import { UrlInputsModel } from '../../store/inputs/model'; import { useApolloClient } from '../../utils/apollo_context'; - import { CONSTANTS, UrlStateType } from './constants'; import { - replaceQueryStringInLocation, getQueryStringFromLocation, - replaceStateKeyInQueryString, getParamFromQueryString, - decodeRisonUrlState, getUrlType, getTitle, + replaceStateInLocation, + updateUrlStateString, } from './helpers'; import { UrlStateContainerPropTypes, @@ -29,8 +24,8 @@ import { KeyUrlState, ALL_URL_STATE_KEYS, UrlStateToRedux, - Timeline, } from './types'; +import { SiemPageName } from '../../pages/home/types'; function usePrevious(value: PreviousLocationUrlState) { const ref = useRef(value); @@ -56,87 +51,90 @@ export const useUrlStateHooks = ({ }: UrlStateContainerPropTypes) => { const [isInitializing, setIsInitializing] = useState(true); const apolloClient = useApolloClient(); - const prevProps = usePrevious({ pathName, urlState }); - - const replaceStateInLocation = ( - urlStateToReplace: UrlInputsModel | Query | esFilters.Filter[] | Timeline | string, - urlStateKey: string, - latestLocation: Location = { - hash: '', - pathname: pathName, - search, - state: '', - } - ) => { - const newLocation = replaceQueryStringInLocation( - { - hash: '', - pathname: pathName, - search, - state: '', - }, - replaceStateKeyInQueryString( - urlStateKey, - urlStateToReplace - )(getQueryStringFromLocation(latestLocation)) - ); - if (history) { - history.replace(newLocation); - } - return newLocation; - }; + const prevProps = usePrevious({ pathName, pageName, urlState }); - const handleInitialize = (initLocation: Location, type: UrlStateType) => { - let myLocation: Location = initLocation; + const handleInitialize = (type: UrlStateType, needUpdate?: boolean) => { + let mySearch = search; let urlStateToUpdate: UrlStateToRedux[] = []; URL_STATE_KEYS[type].forEach((urlKey: KeyUrlState) => { const newUrlStateString = getParamFromQueryString( - getQueryStringFromLocation(initLocation), + getQueryStringFromLocation(mySearch), urlKey ); if (newUrlStateString) { - const queryState: Query | Timeline | esFilters.Filter[] = decodeRisonUrlState( - newUrlStateString - ); - - if ( - urlKey === CONSTANTS.appQuery && - queryState != null && - (queryState as Query).query === '' - ) { - myLocation = replaceStateInLocation('', urlKey, myLocation); - } else if (urlKey === CONSTANTS.filters && isEmpty(queryState)) { - myLocation = replaceStateInLocation('', urlKey, myLocation); - } else if ( - urlKey === CONSTANTS.timeline && - queryState != null && - (queryState as Timeline).id === '' - ) { - myLocation = replaceStateInLocation('', urlKey, myLocation); - } - if (isInitializing) { - urlStateToUpdate = [...urlStateToUpdate, { urlKey, newUrlStateString }]; + mySearch = updateUrlStateString({ + history, + isInitializing, + newUrlStateString, + pathName, + search: mySearch, + updateTimerange: (needUpdate ?? false) || isInitializing, + urlKey, + }); + if (isInitializing || needUpdate) { + const updatedUrlStateString = + getParamFromQueryString(getQueryStringFromLocation(mySearch), urlKey) ?? + newUrlStateString; + if (isInitializing || !isEqual(updatedUrlStateString, newUrlStateString)) { + urlStateToUpdate = [ + ...urlStateToUpdate, + { + urlKey, + newUrlStateString: updatedUrlStateString, + }, + ]; + } } } else if ( urlKey === CONSTANTS.appQuery && urlState[urlKey] != null && - (urlState[urlKey] as Query).query === '' + urlState[urlKey]?.query === '' ) { - myLocation = replaceStateInLocation('', urlKey, myLocation); + mySearch = replaceStateInLocation({ + history, + pathName, + search: mySearch, + urlStateToReplace: '', + urlStateKey: urlKey, + }); } else if (urlKey === CONSTANTS.filters && isEmpty(urlState[urlKey])) { - myLocation = replaceStateInLocation('', urlKey, myLocation); + mySearch = replaceStateInLocation({ + history, + pathName, + search: mySearch, + urlStateToReplace: '', + urlStateKey: urlKey, + }); } else if ( urlKey === CONSTANTS.timeline && urlState[urlKey] != null && - (urlState[urlKey] as Timeline).id === '' + urlState[urlKey].id === '' ) { - myLocation = replaceStateInLocation('', urlKey, myLocation); + mySearch = replaceStateInLocation({ + history, + pathName, + search: mySearch, + urlStateToReplace: '', + urlStateKey: urlKey, + }); } else { - myLocation = replaceStateInLocation(urlState[urlKey] || '', urlKey, myLocation); + mySearch = replaceStateInLocation({ + history, + pathName, + search: mySearch, + urlStateToReplace: urlState[urlKey] || '', + urlStateKey: urlKey, + }); } }); difference(ALL_URL_STATE_KEYS, URL_STATE_KEYS[type]).forEach((urlKey: KeyUrlState) => { - myLocation = replaceStateInLocation('', urlKey, myLocation); + mySearch = replaceStateInLocation({ + history, + pathName, + search: mySearch, + urlStateToReplace: '', + urlStateKey: urlKey, + }); }); setInitialStateFromUrl({ @@ -152,41 +150,58 @@ export const useUrlStateHooks = ({ useEffect(() => { const type: UrlStateType = getUrlType(pageName); - const location: Location = { - hash: '', - pathname: pathName, - search, - state: '', - }; - if (isInitializing && pageName != null && pageName !== '') { - handleInitialize(location, type); + handleInitialize(type); setIsInitializing(false); } else if (!isEqual(urlState, prevProps.urlState) && !isInitializing) { - let newLocation: Location = location; + let mySearch = search; URL_STATE_KEYS[type].forEach((urlKey: KeyUrlState) => { if ( urlKey === CONSTANTS.appQuery && urlState[urlKey] != null && - (urlState[urlKey] as Query).query === '' + urlState[urlKey]?.query === '' ) { - newLocation = replaceStateInLocation('', urlKey, newLocation); + mySearch = replaceStateInLocation({ + history, + pathName, + search: mySearch, + urlStateToReplace: '', + urlStateKey: urlKey, + }); } else if (urlKey === CONSTANTS.filters && isEmpty(urlState[urlKey])) { - newLocation = replaceStateInLocation('', urlKey, newLocation); + mySearch = replaceStateInLocation({ + history, + pathName, + search: mySearch, + urlStateToReplace: '', + urlStateKey: urlKey, + }); } else if ( urlKey === CONSTANTS.timeline && urlState[urlKey] != null && - (urlState[urlKey] as Timeline).id === '' + urlState[urlKey].id === '' ) { - newLocation = replaceStateInLocation('', urlKey, newLocation); + mySearch = replaceStateInLocation({ + history, + pathName, + search: mySearch, + urlStateToReplace: '', + urlStateKey: urlKey, + }); } else { - newLocation = replaceStateInLocation(urlState[urlKey] || '', urlKey, newLocation); + mySearch = replaceStateInLocation({ + history, + pathName, + search: mySearch, + urlStateToReplace: urlState[urlKey] || '', + urlStateKey: urlKey, + }); } }); } else if (pathName !== prevProps.pathName) { - handleInitialize(location, type); + handleInitialize(type, pageName === SiemPageName.detections); } - }); + }, [isInitializing, history, pathName, pageName, prevProps, urlState]); useEffect(() => { document.title = `${getTitle(pageName, detailName, navTabs)} - Kibana`; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index ff6722840fd6b..229593901691b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton, EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui'; +import { EuiButton, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { StickyContainer } from 'react-sticky'; @@ -24,6 +24,8 @@ import { } from '../../components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../components/search_bar'; import { WrapperPage } from '../../components/wrapper_page'; +import { SiemNavigation } from '../../components/navigation'; +import { NavTab } from '../../components/navigation/types'; import { State } from '../../store'; import { inputsSelectors } from '../../store/inputs'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; @@ -60,18 +62,22 @@ export interface DispatchProps { type DetectionEnginePageComponentProps = ReduxProps & DispatchProps; -const detectionsTabs = [ - { +const detectionsTabs: Record = { + [DetectionEngineTab.signals]: { id: DetectionEngineTab.signals, name: i18n.SIGNAL, + href: getDetectionEngineTabUrl(DetectionEngineTab.signals), disabled: false, + urlKey: 'detections', }, - { + [DetectionEngineTab.alerts]: { id: DetectionEngineTab.alerts, name: i18n.ALERT, + href: getDetectionEngineTabUrl(DetectionEngineTab.alerts), disabled: false, + urlKey: 'detections', }, -]; +}; const DetectionEnginePageComponent: React.FC = ({ filters, @@ -98,24 +104,6 @@ const DetectionEnginePageComponent: React.FC [setAbsoluteRangeDatePicker] ); - const tabs = useMemo( - () => ( - - {detectionsTabs.map(tab => ( - - {tab.name} - - ))} - - ), - [detectionsTabs, tabName] - ); - const indexToAdd = useMemo(() => (signalIndexName == null ? [] : [signalIndexName]), [ signalIndexName, ]); @@ -169,7 +157,7 @@ const DetectionEnginePageComponent: React.FC {({ to, from, deleteQuery, setQuery }) => ( <> - {tabs} + {tabName === DetectionEngineTab.signals && ( <>