From e97d4423a9683000d5038adb819d07cf67895958 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 4 Feb 2020 18:50:53 -0500 Subject: [PATCH 1/7] update url state between page if needed it like time range --- .../public/components/navigation/helpers.ts | 2 +- .../public/components/url_state/helpers.ts | 112 ++++++++++- .../components/url_state/index.test.tsx | 60 +++++- .../siem/public/components/url_state/types.ts | 17 ++ .../components/url_state/use_url_state.tsx | 178 ++++++++++-------- 5 files changed, 283 insertions(+), 86 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts b/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts index b61ef7165dcc35..7a5cc106dbcd8d 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts @@ -53,7 +53,7 @@ export const getSearch = (tab: SearchNavTab, urlState: TabNavigationProps): stri replaceStateKeyInQueryString( urlKey, urlStateToReplace - )(getQueryStringFromLocation(myLocation)) + )(getQueryStringFromLocation(myLocation.search)) ); }, { diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts index 6ba5810f794b09..4c0f871cfbb3fa 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts @@ -5,15 +5,24 @@ */ import { decode, encode, RisonValue } from 'rison-node'; -import { Location } from 'history'; +import * as H from 'history'; import { QueryString } from 'ui/utils/query_string'; import { Query, esFilters } from 'src/plugins/data/public'; -import { inputsSelectors, State, timelineSelectors } from '../../store'; +import { isEmpty } from 'lodash/fp'; import { SiemPageName } from '../../pages/home/types'; +import { inputsSelectors, State, timelineSelectors } from '../../store'; +import { UrlInputsModel } from '../../store/inputs/model'; +import { formatDate } from '../super_date_picker'; import { NavTab } from '../navigation/types'; import { CONSTANTS, UrlStateType } from './constants'; -import { LocationTypes, UrlStateContainerPropTypes } from './types'; +import { + LocationTypes, + UrlStateContainerPropTypes, + ReplaceStateInLocation, + Timeline, + UpdateUrlStateString, +} from './types'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const decodeRisonUrlState = (value: string | undefined): RisonValue | any | undefined => { @@ -30,7 +39,7 @@ 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]; @@ -60,8 +69,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 +185,91 @@ export const makeMapStateToProps = () => { return mapStateToProps; }; + +export const updateTimerangeUrl = (timeRange: UrlInputsModel): 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); + } + if (timeRange.timeline.timerange.kind === 'relative') { + timeRange.timeline.timerange.from = formatDate(timeRange.timeline.timerange.fromStr); + timeRange.timeline.timerange.to = formatDate(timeRange.timeline.timerange.toStr); + } + return timeRange; +}; + +export const updateUrlStateString = ({ + history, + newUrlStateString, + pathName, + search, + urlKey, +}: UpdateUrlStateString): string => { + const queryState: Query | Timeline | esFilters.Filter[] | UrlInputsModel = decodeRisonUrlState( + newUrlStateString + ); + if (urlKey === CONSTANTS.appQuery && queryState != null && (queryState as Query).query === '') { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: '', + urlStateKey: urlKey, + }); + } else if ( + urlKey === CONSTANTS.timerange && + queryState != null && + (queryState as UrlInputsModel).global != null + ) { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: updateTimerangeUrl(queryState as UrlInputsModel), + urlStateKey: urlKey, + }); + } else if (urlKey === CONSTANTS.filters && isEmpty(queryState)) { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: '', + urlStateKey: urlKey, + }); + } else if ( + urlKey === CONSTANTS.timeline && + queryState != null && + (queryState as Timeline).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 ab290c2f2fd67b..f6164da217ab83 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; @@ -37,6 +37,12 @@ jest.mock('../../utils/route/use_route_spy', () => ({ useRouteSpy: () => [mockRouteSpy], })); +jest.mock('../super_date_picker', () => ({ + formatDate: (date: string) => { + return 11223344556677; + }, +})); + jest.mock('../search_bar', () => ({ siemFilterManager: { setFilters: jest.fn(), @@ -155,4 +161,56 @@ describe('UrlStateContainer', () => { }); }); }); + + describe('After Initialization, keep Relative Date up to date', () => { + 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, + pageName, + detailName, + }).relativeTimeSearch.undefinedQuery, + }); + wrapper.update(); + await wait(); + if (CONSTANTS.hostsPage === page) { + // There is no change in url state, so that's expected + expect(mockSetRelativeRangeDatePicker.mock.calls.length).toEqual(2); + } else { + 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: 11223344556677, + fromStr: 'now-15m', + kind: 'relative', + to: 11223344556677, + toStr: 'now', + id: 'timeline', + }); + } + } + ); + }); }); 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 be1ae1ad63bd4b..1aad88c67f4552 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'; @@ -136,3 +137,19 @@ export type DispatchSetInitialStateFromUrl = ({ updateTimelineIsLoading, urlStateToUpdate, }: SetInitialStateFromUrl) => () => void; + +export interface ReplaceStateInLocation { + history?: H.History; + urlStateToReplace: UrlInputsModel | Query | esFilters.Filter[] | Timeline | string; + urlStateKey: string; + pathName: string; + search: string; +} + +export interface UpdateUrlStateString { + history?: H.History; + newUrlStateString: string; + pathName: string; + search: string; + 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 d7fece57319729..19c1bf386948c8 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,19 @@ * 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 { Query } 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, @@ -58,85 +54,94 @@ export const useUrlStateHooks = ({ 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 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); - } + mySearch = updateUrlStateString({ + history, + newUrlStateString, + pathName, + search: mySearch, + urlKey, + }); if (isInitializing) { - urlStateToUpdate = [...urlStateToUpdate, { urlKey, newUrlStateString }]; + urlStateToUpdate = [ + ...urlStateToUpdate, + { + urlKey, + newUrlStateString, + }, + ]; + } else if (needUpdate) { + const updatedUrlStateString = + getParamFromQueryString(getQueryStringFromLocation(mySearch), urlKey) ?? + newUrlStateString; + if (!isEqual(updatedUrlStateString, newUrlStateString)) { + urlStateToUpdate = [ + ...urlStateToUpdate, + { + urlKey, + newUrlStateString: updatedUrlStateString, + }, + ]; + } } } else if ( urlKey === CONSTANTS.appQuery && urlState[urlKey] != null && (urlState[urlKey] as Query).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 === '' ) { - 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 +157,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 === '' ) { - 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 === '' ) { - 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, true); } - }); + }, [isInitializing, pathName, pageName, prevProps, urlState]); useEffect(() => { document.title = `${getTitle(pageName, detailName, navTabs)} - Kibana`; From 8d4aabd11a3a3434cd008cb4e1b9be23b5d0f531 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 4 Feb 2020 20:58:03 -0500 Subject: [PATCH 2/7] fix round up for to --- .../plugins/siem/public/components/url_state/helpers.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts index 4c0f871cfbb3fa..206d6b0deebf44 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts @@ -189,11 +189,13 @@ export const makeMapStateToProps = () => { export const updateTimerangeUrl = (timeRange: UrlInputsModel): 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); + timeRange.global.timerange.to = formatDate(timeRange.global.timerange.toStr, { roundUp: true }); } if (timeRange.timeline.timerange.kind === 'relative') { timeRange.timeline.timerange.from = formatDate(timeRange.timeline.timerange.fromStr); - timeRange.timeline.timerange.to = formatDate(timeRange.timeline.timerange.toStr); + timeRange.timeline.timerange.to = formatDate(timeRange.timeline.timerange.toStr, { + roundUp: true, + }); } return timeRange; }; From dbcb1b660327d28427dabec5cde70206db71eb15 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 4 Feb 2020 22:07:17 -0500 Subject: [PATCH 3/7] simplify type --- .../public/components/url_state/helpers.ts | 100 +++++------ .../url_state/initialize_redux_by_url.tsx | 158 +++++++++--------- .../siem/public/components/url_state/types.ts | 4 +- 3 files changed, 133 insertions(+), 129 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts index 206d6b0deebf44..acee00360f6565 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { decode, encode, RisonValue } from 'rison-node'; +import { decode, encode } from 'rison-node'; import * as H from 'history'; import { QueryString } from 'ui/utils/query_string'; import { Query, esFilters } from 'src/plugins/data/public'; @@ -24,13 +24,12 @@ import { UpdateUrlStateString, } from './types'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const decodeRisonUrlState = (value: string | undefined): RisonValue | any | undefined => { +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; } @@ -207,60 +206,61 @@ export const updateUrlStateString = ({ search, urlKey, }: UpdateUrlStateString): string => { - const queryState: Query | Timeline | esFilters.Filter[] | UrlInputsModel = decodeRisonUrlState( - newUrlStateString - ); - if (urlKey === CONSTANTS.appQuery && queryState != null && (queryState as Query).query === '') { - return replaceStateInLocation({ - history, - pathName, - search, - urlStateToReplace: '', - urlStateKey: urlKey, - }); - } else if ( - urlKey === CONSTANTS.timerange && - queryState != null && - (queryState as UrlInputsModel).global != null - ) { - return replaceStateInLocation({ - history, - pathName, - search, - urlStateToReplace: updateTimerangeUrl(queryState as UrlInputsModel), - urlStateKey: urlKey, - }); - } else if (urlKey === CONSTANTS.filters && isEmpty(queryState)) { - return replaceStateInLocation({ - history, - pathName, - search, - urlStateToReplace: '', - urlStateKey: urlKey, - }); - } else if ( - urlKey === CONSTANTS.timeline && - queryState != null && - (queryState as Timeline).id === '' - ) { - return replaceStateInLocation({ - history, - pathName, - search, - urlStateToReplace: '', - urlStateKey: urlKey, - }); + 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) { + const queryState = decodeRisonUrlState(newUrlStateString); + if (queryState != null && queryState.global != null) { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: updateTimerangeUrl(queryState), + 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 = ({ +export const replaceStateInLocation = ({ history, urlStateToReplace, urlStateKey, pathName, search, -}: ReplaceStateInLocation) => { +}: ReplaceStateInLocation) => { const newLocation = replaceQueryStringInLocation( { hash: '', 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 013983c78a3a51..01cd5a4e70571c 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 @@ -22,7 +22,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 +38,10 @@ 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,13 +54,13 @@ 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 !== '') { + const savedQueryId = decodeRisonUrlState(newUrlStateString); + if (savedQueryId != null && savedQueryId !== '') { savedQueryService.getSavedQuery(savedQueryId).then((savedQueryData: SavedQuery) => { siemFilterManager.setFilters(savedQueryData.attributes.filters || []); dispatch( @@ -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/types.ts b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts index 1aad88c67f4552..8981250cb9f193 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 @@ -138,9 +138,9 @@ export type DispatchSetInitialStateFromUrl = ({ urlStateToUpdate, }: SetInitialStateFromUrl) => () => void; -export interface ReplaceStateInLocation { +export interface ReplaceStateInLocation { history?: H.History; - urlStateToReplace: UrlInputsModel | Query | esFilters.Filter[] | Timeline | string; + urlStateToReplace: T; urlStateKey: string; pathName: string; search: string; From b6aa68cd9a05482104f171e51f5f659e0f66d63c Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 4 Feb 2020 22:53:41 -0500 Subject: [PATCH 4/7] leftover cleanup --- .../siem/public/components/url_state/use_url_state.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) 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 19c1bf386948c8..1b96cae3ef3f4c 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 @@ -6,7 +6,6 @@ import { isEqual, difference, isEmpty } from 'lodash/fp'; import { useEffect, useRef, useState } from 'react'; -import { Query } from 'src/plugins/data/public'; import { useApolloClient } from '../../utils/apollo_context'; import { CONSTANTS, UrlStateType } from './constants'; @@ -25,7 +24,6 @@ import { KeyUrlState, ALL_URL_STATE_KEYS, UrlStateToRedux, - Timeline, } from './types'; function usePrevious(value: PreviousLocationUrlState) { @@ -95,7 +93,7 @@ export const useUrlStateHooks = ({ } else if ( urlKey === CONSTANTS.appQuery && urlState[urlKey] != null && - (urlState[urlKey] as Query).query === '' + urlState[urlKey]?.query === '' ) { mySearch = replaceStateInLocation({ history, @@ -115,7 +113,7 @@ export const useUrlStateHooks = ({ } else if ( urlKey === CONSTANTS.timeline && urlState[urlKey] != null && - (urlState[urlKey] as Timeline).id === '' + urlState[urlKey].id === '' ) { mySearch = replaceStateInLocation({ history, @@ -166,7 +164,7 @@ export const useUrlStateHooks = ({ if ( urlKey === CONSTANTS.appQuery && urlState[urlKey] != null && - (urlState[urlKey] as Query).query === '' + urlState[urlKey]?.query === '' ) { mySearch = replaceStateInLocation({ history, @@ -186,7 +184,7 @@ export const useUrlStateHooks = ({ } else if ( urlKey === CONSTANTS.timeline && urlState[urlKey] != null && - (urlState[urlKey] as Timeline).id === '' + urlState[urlKey].id === '' ) { mySearch = replaceStateInLocation({ history, From 49659d07de4557b68253b89f6bcfcb88e65fcc24 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Wed, 5 Feb 2020 09:51:52 -0500 Subject: [PATCH 5/7] we forget to update relative date when loading the page by a refresh --- .../siem/public/components/url_state/helpers.ts | 8 +++----- .../public/components/url_state/index.test.tsx | 10 +++++----- .../public/components/url_state/use_url_state.tsx | 14 +++----------- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts index acee00360f6565..efa5ad5b2ed98f 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts @@ -45,11 +45,9 @@ export const getParamFromQueryString = (queryString: string, key: string): strin 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]; 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 f6164da217ab83..2bea0b96f8c505 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 @@ -69,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', }); @@ -189,7 +189,7 @@ describe('UrlStateContainer', () => { wrapper.update(); await wait(); if (CONSTANTS.hostsPage === page) { - // There is no change in url state, so that's expected + // There is no change in url state, so that's expected we only have two actions expect(mockSetRelativeRangeDatePicker.mock.calls.length).toEqual(2); } else { expect(mockSetRelativeRangeDatePicker.mock.calls[3][0]).toEqual({ 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 1b96cae3ef3f4c..bb3d7eba87d30f 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 @@ -68,19 +68,11 @@ export const useUrlStateHooks = ({ search: mySearch, urlKey, }); - if (isInitializing) { - urlStateToUpdate = [ - ...urlStateToUpdate, - { - urlKey, - newUrlStateString, - }, - ]; - } else if (needUpdate) { + if (isInitializing || needUpdate) { const updatedUrlStateString = getParamFromQueryString(getQueryStringFromLocation(mySearch), urlKey) ?? newUrlStateString; - if (!isEqual(updatedUrlStateString, newUrlStateString)) { + if (isInitializing || !isEqual(updatedUrlStateString, newUrlStateString)) { urlStateToUpdate = [ ...urlStateToUpdate, { @@ -206,7 +198,7 @@ export const useUrlStateHooks = ({ } else if (pathName !== prevProps.pathName) { handleInitialize(type, true); } - }, [isInitializing, pathName, pageName, prevProps, urlState]); + }, [isInitializing, history, pathName, pageName, prevProps, urlState]); useEffect(() => { document.title = `${getTitle(pageName, detailName, navTabs)} - Kibana`; From 2d10cdd9dbeb27d6752a8cc0c87f810f9d873eeb Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 6 Feb 2020 14:14:28 -0500 Subject: [PATCH 6/7] pair with Garrett to make a minimal impact on the ux --- .../public/components/url_state/helpers.ts | 13 ++++++++---- .../components/url_state/index.test.tsx | 21 ++++++++++--------- .../components/url_state/test_dependencies.ts | 12 +++++++++++ .../siem/public/components/url_state/types.ts | 3 +++ .../components/url_state/use_url_state.tsx | 7 +++++-- 5 files changed, 40 insertions(+), 16 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts index efa5ad5b2ed98f..03fdbe2219accd 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts @@ -183,12 +183,15 @@ export const makeMapStateToProps = () => { return mapStateToProps; }; -export const updateTimerangeUrl = (timeRange: UrlInputsModel): UrlInputsModel => { +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') { + 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, @@ -198,10 +201,12 @@ export const updateTimerangeUrl = (timeRange: UrlInputsModel): UrlInputsModel => }; export const updateUrlStateString = ({ + isInitializing, history, newUrlStateString, pathName, search, + updateTimerange, urlKey, }: UpdateUrlStateString): string => { if (urlKey === CONSTANTS.appQuery) { @@ -215,14 +220,14 @@ export const updateUrlStateString = ({ urlStateKey: urlKey, }); } - } else if (urlKey === CONSTANTS.timerange) { + } else if (urlKey === CONSTANTS.timerange && updateTimerange) { const queryState = decodeRisonUrlState(newUrlStateString); if (queryState != null && queryState.global != null) { return replaceStateInLocation({ history, pathName, search, - urlStateToReplace: updateTimerangeUrl(queryState), + urlStateToReplace: updateTimerangeUrl(queryState, isInitializing), urlStateKey: urlKey, }); } 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 6106211ef0018f..10aa388449d911 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 @@ -168,7 +168,7 @@ describe('UrlStateContainer', () => { }); }); - describe('After Initialization, keep Relative Date up to date', () => { + 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) => { @@ -187,17 +187,15 @@ describe('UrlStateContainer', () => { hookProps: getMockPropsObj({ page: CONSTANTS.hostsPage, examplePath: '/hosts', - namespaceLower, - pageName, - detailName, + namespaceLower: 'hosts', + pageName: SiemPageName.hosts, + detailName: undefined, }).relativeTimeSearch.undefinedQuery, }); wrapper.update(); await wait(); - if (CONSTANTS.hostsPage === page) { - // There is no change in url state, so that's expected we only have two actions - expect(mockSetRelativeRangeDatePicker.mock.calls.length).toEqual(2); - } else { + + if (CONSTANTS.detectionsPage === page) { expect(mockSetRelativeRangeDatePicker.mock.calls[3][0]).toEqual({ from: 11223344556677, fromStr: 'now-1d/d', @@ -208,13 +206,16 @@ describe('UrlStateContainer', () => { }); expect(mockSetRelativeRangeDatePicker.mock.calls[2][0]).toEqual({ - from: 11223344556677, + from: 1558732849370, fromStr: 'now-15m', kind: 'relative', - to: 11223344556677, + 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/test_dependencies.ts b/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts index 4dd92ac58b0a34..dc1b8d428bb20d 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 608c15b88a5181..9ee469f4fd4272 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 @@ -116,6 +116,7 @@ export type UrlStateContainerPropTypes = RouteSpyState & export interface PreviousLocationUrlState { pathName: string | undefined; + pageName: string | undefined; urlState: UrlState; } @@ -155,9 +156,11 @@ export interface ReplaceStateInLocation { } 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 d5b6fff9e4c221..deaf9bbf5011d8 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 @@ -26,6 +26,7 @@ import { ALL_URL_STATE_KEYS, UrlStateToRedux, } from './types'; +import { SiemPageName } from '../../pages/home/types'; function usePrevious(value: PreviousLocationUrlState) { const ref = useRef(value); @@ -52,7 +53,7 @@ export const useUrlStateHooks = ({ const [isInitializing, setIsInitializing] = useState(true); const apolloClient = useApolloClient(); const { filterManager, savedQueries } = useKibana().services.data.query; - const prevProps = usePrevious({ pathName, urlState }); + const prevProps = usePrevious({ pathName, pageName, urlState }); const handleInitialize = (type: UrlStateType, needUpdate?: boolean) => { let mySearch = search; @@ -65,9 +66,11 @@ export const useUrlStateHooks = ({ if (newUrlStateString) { mySearch = updateUrlStateString({ history, + isInitializing, newUrlStateString, pathName, search: mySearch, + updateTimerange: (needUpdate ?? false) || isInitializing, urlKey, }); if (isInitializing || needUpdate) { @@ -200,7 +203,7 @@ export const useUrlStateHooks = ({ } }); } else if (pathName !== prevProps.pathName) { - handleInitialize(type, true); + handleInitialize(type, pageName === SiemPageName.detections); } }, [isInitializing, history, pathName, pageName, prevProps, urlState]); From 9ff2b2cd78cd5ca2609b41c723cde14d740e387f Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 6 Feb 2020 19:21:40 -0500 Subject: [PATCH 7/7] fix detetections tabs --- .../public/components/link_to/link_to.tsx | 2 -- .../detection_engine/detection_engine.tsx | 35 ++++++------------- 2 files changed, 11 insertions(+), 26 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 3eda945c9224e6..dc8c696301611b 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 /> = ({ filters, @@ -98,24 +103,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 +156,7 @@ const DetectionEnginePageComponent: React.FC {({ to, from, deleteQuery, setQuery }) => ( <> - {tabs} + {tabName === DetectionEngineTab.signals && ( <>