From 7745d367038b167783fc27b1df794dc5c696cc23 Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Thu, 22 Feb 2024 20:56:52 -0500 Subject: [PATCH] [Security Solution] [Sourcerer] Make use of reselect in sourcerer selectors (#176916) ## Summary This pr should change nothing functionally, but changes the selectors used in components for sourcerer to make use of createSelector and benefit from memoization at all times, --- .../assistant/send_to_timeline/index.tsx | 14 +- .../table/investigate_in_timeline_button.tsx | 14 +- .../common/components/sourcerer/index.tsx | 27 +- .../common/containers/sourcerer/index.tsx | 84 +++---- .../sourcerer/use_signal_helpers.tsx | 16 +- .../public/common/hooks/use_get_field_spec.ts | 20 +- .../{reducer.test.ts => reducer.test.tsx} | 52 ++-- .../common/store/sourcerer/selectors.ts | 169 ++++++------- .../embeddables/embedded_map.test.tsx | 101 +++++--- .../components/embeddables/embedded_map.tsx | 15 +- .../hooks/use_navigate_to_timeline.tsx | 15 +- .../test_utilities/simulator/index.tsx | 10 + .../simulator/mock_resolver.tsx | 39 +-- .../components/bottom_bar/index.test.tsx | 10 +- .../timelines/components/bottom_bar/index.tsx | 77 +++--- .../components/fields_browser/index.test.tsx | 38 +-- .../components/fields_browser/index.tsx | 14 +- .../actions/new_timeline_button.test.tsx | 14 +- .../components/modal/header/index.test.tsx | 5 +- .../components/modal/header/index.tsx | 232 +++++++++--------- .../timelines/components/modal/index.test.tsx | 6 +- .../timelines/components/modal/index.tsx | 66 ++--- .../components/new_timeline/index.test.tsx | 13 +- .../components/timeline/index.test.tsx | 4 + .../timelines/components/timeline/index.tsx | 18 +- .../containers/use_timeline_data_filters.ts | 7 +- .../hooks/use_create_timeline.test.tsx | 122 ++++----- .../timelines/hooks/use_create_timeline.tsx | 10 +- .../public/timelines/wrapper/index.tsx | 8 +- 29 files changed, 633 insertions(+), 587 deletions(-) rename x-pack/plugins/security_solution/public/common/store/{reducer.test.ts => reducer.test.tsx} (70%) diff --git a/x-pack/plugins/security_solution/public/assistant/send_to_timeline/index.tsx b/x-pack/plugins/security_solution/public/assistant/send_to_timeline/index.tsx index 73b79e013709..5a142177277a 100644 --- a/x-pack/plugins/security_solution/public/assistant/send_to_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/assistant/send_to_timeline/index.tsx @@ -5,13 +5,12 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback } from 'react'; import { EuiButton, EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; import type { Filter } from '@kbn/es-query'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { useAssistantContext } from '@kbn/elastic-assistant'; -import { useDeepEqualSelector } from '../../common/hooks/use_selector'; import { sourcererSelectors } from '../../common/store'; import { sourcererActions } from '../../common/store/actions'; import { inputsActions } from '../../common/store/inputs'; @@ -63,13 +62,8 @@ export const SendToTimelineButton: React.FunctionComponent sourcererSelectors.getSourcererDataViewsSelector(), - [] - ); - const { defaultDataView, signalIndexName } = useDeepEqualSelector((state) => - getDataViewsSelector(state) - ); + const signalIndexName = useSelector(sourcererSelectors.signalIndexName); + const defaultDataView = useSelector(sourcererSelectors.defaultDataView); const hasTemplateProviders = dataProviders && dataProviders.find((provider) => provider.type === 'template'); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx index fed25aca5f15..fb9a8b5d3c60 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import React, { useMemo, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { EuiButton, EuiButtonEmpty } from '@elastic/eui'; import type { IconType } from '@elastic/eui'; import type { Filter } from '@kbn/es-query'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { sourcererSelectors } from '../../../store'; import { InputsModelId } from '../../../store/inputs/constants'; @@ -23,7 +23,6 @@ import { TimelineId } from '../../../../../common/types/timeline'; import { TimelineType } from '../../../../../common/api/timeline'; import { useCreateTimeline } from '../../../../timelines/hooks/use_create_timeline'; import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations'; -import { useDeepEqualSelector } from '../../../hooks/use_selector'; export interface InvestigateInTimelineButtonProps { asEmptyButton: boolean; @@ -49,13 +48,8 @@ export const InvestigateInTimelineButton: React.FunctionComponent< }) => { const dispatch = useDispatch(); - const getDataViewsSelector = useMemo( - () => sourcererSelectors.getSourcererDataViewsSelector(), - [] - ); - const { defaultDataView, signalIndexName } = useDeepEqualSelector((state) => - getDataViewsSelector(state) - ); + const signalIndexName = useSelector(sourcererSelectors.signalIndexName); + const defaultDataView = useSelector(sourcererSelectors.defaultDataView); const hasTemplateProviders = dataProviders && dataProviders.find((provider) => provider.type === 'template'); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx index 6d6e336f6873..d42d8d9213ef 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx @@ -15,13 +15,13 @@ import { } from '@elastic/eui'; import type { ChangeEventHandler } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import * as i18n from './translations'; import type { sourcererModel } from '../../store/sourcerer'; import { sourcererActions, sourcererSelectors } from '../../store/sourcerer'; -import { useDeepEqualSelector } from '../../hooks/use_selector'; import type { SourcererUrlState } from '../../store/sourcerer/model'; +import type { State } from '../../store'; import type { ModifiedTypes } from './use_pick_index_patterns'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { usePickIndexPatterns } from './use_pick_index_patterns'; @@ -129,17 +129,18 @@ export const Sourcerer = React.memo(({ scope: scopeId } const isDefaultSourcerer = scopeId === SourcererScopeName.default; const updateUrlParam = useUpdateUrlParam(URL_PARAM_KEY.sourcerer); - const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []); - const { - defaultDataView, - kibanaDataViews, - signalIndexName, - sourcererScope: { - selectedDataViewId, - selectedPatterns, - missingPatterns: sourcererMissingPatterns, - }, - } = useDeepEqualSelector((state) => sourcererScopeSelector(state, scopeId)); + const signalIndexName = useSelector(sourcererSelectors.signalIndexName); + const defaultDataView = useSelector(sourcererSelectors.defaultDataView); + const kibanaDataViews = useSelector(sourcererSelectors.kibanaDataViews); + const selectedDataViewId = useSelector((state: State) => { + return sourcererSelectors.sourcererScopeSelectedDataViewId(state, scopeId); + }); + const selectedPatterns = useSelector((state: State) => { + return sourcererSelectors.sourcererScopeSelectedPatterns(state, scopeId); + }); + const sourcererMissingPatterns = useSelector((state: State) => { + return sourcererSelectors.sourcererScopeMissingPatterns(state, scopeId); + }); const { pollForSignalIndex } = useSignalHelpers(); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx index aece36b85499..0a8985d5bfb8 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx @@ -6,7 +6,7 @@ */ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { matchPath } from 'react-router-dom'; import { sourcererActions, sourcererSelectors } from '../../store/sourcerer'; @@ -36,6 +36,7 @@ import { useAppToasts } from '../../hooks/use_app_toasts'; import { createSourcererDataView } from './create_sourcerer_data_view'; import { getDataViewStateFromIndexFields, useDataView } from '../source/use_data_view'; import { useFetchIndex } from '../source'; +import type { State } from '../../store'; import { useInitializeUrlParam, useUpdateUrlParam } from '../../utils/global_query_string'; import { URL_PARAM_KEY } from '../../hooks/use_url_state'; import { sortWithExcludesAtEnd } from '../../../../common/utils/sourcerer'; @@ -54,14 +55,8 @@ export const useInitSourcerer = ( const { loading: loadingSignalIndex, isSignalIndexExists, signalIndexName } = useUserInfo(); const updateUrlParam = useUpdateUrlParam(URL_PARAM_KEY.sourcerer); - const getDataViewsSelector = useMemo( - () => sourcererSelectors.getSourcererDataViewsSelector(), - [] - ); - const { defaultDataView, signalIndexName: signalIndexNameSourcerer } = useDeepEqualSelector( - (state) => getDataViewsSelector(state) - ); - + const signalIndexNameSourcerer = useSelector(sourcererSelectors.signalIndexName); + const defaultDataView = useSelector(sourcererSelectors.defaultDataView); const { addError, addWarning } = useAppToasts(); useEffect(() => { @@ -83,19 +78,29 @@ export const useInitSourcerer = ( getTimelineSelector(state, TimelineId.active) ); - const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []); - const { - sourcererScope: { selectedDataViewId: scopeDataViewId, selectedPatterns, missingPatterns }, - } = useDeepEqualSelector((state) => sourcererScopeSelector(state, scopeId)); + const scopeDataViewId = useSelector((state: State) => { + return sourcererSelectors.sourcererScopeSelectedDataViewId(state, scopeId); + }); + const selectedPatterns = useSelector((state: State) => { + return sourcererSelectors.sourcererScopeSelectedPatterns(state, scopeId); + }); + const missingPatterns = useSelector((state: State) => { + return sourcererSelectors.sourcererScopeMissingPatterns(state, scopeId); + }); - const { - selectedDataView: timelineSelectedDataView, - sourcererScope: { - selectedDataViewId: timelineDataViewId, - selectedPatterns: timelineSelectedPatterns, - missingPatterns: timelineMissingPatterns, - }, - } = useDeepEqualSelector((state) => sourcererScopeSelector(state, SourcererScopeName.timeline)); + const kibanaDataViews = useSelector(sourcererSelectors.kibanaDataViews); + const timelineDataViewId = useSelector((state: State) => { + return sourcererSelectors.sourcererScopeSelectedDataViewId(state, SourcererScopeName.timeline); + }); + const timelineSelectedPatterns = useSelector((state: State) => { + return sourcererSelectors.sourcererScopeSelectedPatterns(state, SourcererScopeName.timeline); + }); + const timelineMissingPatterns = useSelector((state: State) => { + return sourcererSelectors.sourcererScopeMissingPatterns(state, SourcererScopeName.timeline); + }); + const timelineSelectedDataView = useMemo(() => { + return kibanaDataViews.find((dataView) => dataView.id === timelineDataViewId); + }, [kibanaDataViews, timelineDataViewId]); const { indexFieldsSearch } = useDataView(); @@ -387,26 +392,23 @@ export const useInitSourcerer = ( export const useSourcererDataView = ( scopeId: SourcererScopeName = SourcererScopeName.default ): SelectedDataView => { - const { getDataViewsSelector, getSourcererDataViewSelector, getScopeSelector } = useMemo( - () => ({ - getDataViewsSelector: sourcererSelectors.getSourcererDataViewsSelector(), - getSourcererDataViewSelector: sourcererSelectors.sourcererDataViewSelector(), - getScopeSelector: sourcererSelectors.scopeIdSelector(), - }), - [] - ); - const { - defaultDataView, - signalIndexName, - selectedDataView, - sourcererScope: { missingPatterns, selectedPatterns: scopeSelectedPatterns, loading }, - }: sourcererSelectors.SourcererScopeSelector = useDeepEqualSelector((state) => { - const sourcererScope = getScopeSelector(state, scopeId); - return { - ...getDataViewsSelector(state), - selectedDataView: getSourcererDataViewSelector(state, sourcererScope.selectedDataViewId), - sourcererScope, - }; + const kibanaDataViews = useSelector(sourcererSelectors.kibanaDataViews); + const signalIndexName = useSelector(sourcererSelectors.signalIndexName); + const defaultDataView = useSelector(sourcererSelectors.defaultDataView); + const selectedDataViewId = useSelector((state: State) => { + return sourcererSelectors.sourcererScopeSelectedDataViewId(state, scopeId); + }); + const selectedDataView = useMemo(() => { + return kibanaDataViews.find((dataView) => dataView.id === selectedDataViewId); + }, [kibanaDataViews, selectedDataViewId]); + const loading = useSelector((state: State) => { + return sourcererSelectors.sourcererScopeIsLoading(state, scopeId); + }); + const scopeSelectedPatterns = useSelector((state: State) => { + return sourcererSelectors.sourcererScopeSelectedPatterns(state, scopeId); + }); + const missingPatterns = useSelector((state: State) => { + return sourcererSelectors.sourcererScopeMissingPatterns(state, scopeId); }); const selectedPatterns = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/use_signal_helpers.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/use_signal_helpers.tsx index 2da246ebf929..8bf9f59fa91c 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/use_signal_helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/use_signal_helpers.tsx @@ -7,9 +7,8 @@ import { useCallback, useMemo, useRef } from 'react'; import { i18n } from '@kbn/i18n'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { sourcererSelectors } from '../../store'; -import { useDeepEqualSelector } from '../../hooks/use_selector'; import { useSourcererDataView } from '.'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useDataView } from '../source/use_data_view'; @@ -33,17 +32,8 @@ export const useSignalHelpers = (): { data: { dataViews }, } = useKibana().services; - const getDefaultDataViewSelector = useMemo( - () => sourcererSelectors.defaultDataViewSelector(), - [] - ); - const getSignalIndexNameSelector = useMemo( - () => sourcererSelectors.signalIndexNameSelector(), - [] - ); - const signalIndexNameSourcerer = useDeepEqualSelector(getSignalIndexNameSelector); - const defaultDataView = useDeepEqualSelector(getDefaultDataViewSelector); - + const signalIndexNameSourcerer = useSelector(sourcererSelectors.signalIndexName); + const defaultDataView = useSelector(sourcererSelectors.defaultDataView); const signalIndexNeedsInit = useMemo( () => !defaultDataView.title.includes(`${signalIndexNameSourcerer}`), [defaultDataView.title, signalIndexNameSourcerer] diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_get_field_spec.ts b/x-pack/plugins/security_solution/public/common/hooks/use_get_field_spec.ts index 2330ee26b7bc..7a91bd19103c 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_get_field_spec.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_get_field_spec.ts @@ -5,17 +5,21 @@ * 2.0. */ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; +import { useSelector } from 'react-redux'; import type { SourcererScopeName } from '../store/sourcerer/model'; -import { getSelectedDataviewSelector } from '../store/sourcerer/selectors'; -import { useDeepEqualSelector } from './use_selector'; - -// Calls it from the module scope due to non memoized selectors https://github.com/elastic/kibana/issues/159315 -const selectedDataviewSelector = getSelectedDataviewSelector(); +import { sourcererSelectors } from '../store/sourcerer'; +import type { State } from '../store'; export const useGetFieldSpec = (scopeId: SourcererScopeName) => { - const dataView = useDeepEqualSelector((state) => selectedDataviewSelector(state, scopeId)); - + const kibanaDataViews = useSelector(sourcererSelectors.kibanaDataViews); + const selectedDataViewId = useSelector((state: State) => + sourcererSelectors.sourcererScopeSelectedDataViewId(state, scopeId) + ); + const dataView = useMemo( + () => kibanaDataViews.find((dv) => dv.id === selectedDataViewId), + [kibanaDataViews, selectedDataViewId] + ); return useCallback( (fieldName: string) => { const fields = dataView?.fields; diff --git a/x-pack/plugins/security_solution/public/common/store/reducer.test.ts b/x-pack/plugins/security_solution/public/common/store/reducer.test.tsx similarity index 70% rename from x-pack/plugins/security_solution/public/common/store/reducer.test.ts rename to x-pack/plugins/security_solution/public/common/store/reducer.test.tsx index a54eb378db69..7feb01b344e3 100644 --- a/x-pack/plugins/security_solution/public/common/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/common/store/reducer.test.tsx @@ -4,23 +4,39 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import React from 'react'; import { parseExperimentalConfigValue } from '../../../common/experimental_features'; import type { SecuritySubPlugins } from '../../app/types'; import { createInitialState } from './reducer'; -import { mockIndexPattern, mockSourcererState } from '../mock'; +import { mockIndexPattern, mockSourcererState, TestProviders, createMockStore } from '../mock'; import { useSourcererDataView } from '../containers/sourcerer'; -import { useDeepEqualSelector } from '../hooks/use_selector'; import { renderHook } from '@testing-library/react-hooks'; import { initialGroupingState } from './grouping/reducer'; import { initialAnalyzerState } from '../../resolver/store/helpers'; jest.mock('../hooks/use_selector'); -jest.mock('../lib/kibana', () => ({ - KibanaServices: { - get: jest.fn(() => ({ uiSettings: { get: () => ({ from: 'now-24h', to: 'now' }) } })), - }, -})); +jest.mock('../lib/kibana', () => { + const original = jest.requireActual('../lib/kibana'); + return { + ...original, + useKibana: () => ({ + ...original.useKibana(), + services: { + ...original.useKibana().services, + upselling: { + ...original.useKibana().services.upselling, + featureUsage: { + ...original.useKibana().services.upselling.featureUsage, + hasShown: jest.fn(), + }, + }, + }, + }), + KibanaServices: { + get: jest.fn(() => ({ uiSettings: { get: () => ({ from: 'now-24h', to: 'now' }) } })), + }, + }; +}); jest.mock('../containers/source', () => ({ useFetchIndex: () => [ false, @@ -28,6 +44,7 @@ jest.mock('../containers/source', () => ({ ], })); +// TODO: this is more of a hook test, a reducer is a pure function and should not need hooks and context to test. describe('createInitialState', () => { describe('sourcerer -> default -> indicesExist', () => { const mockPluginState = {} as Omit< @@ -53,15 +70,13 @@ describe('createInitialState', () => { analyzer: initialAnalyzerState, } ); - beforeEach(() => { - (useDeepEqualSelector as jest.Mock).mockImplementation((cb) => cb(initState)); - }); - afterEach(() => { - (useDeepEqualSelector as jest.Mock).mockClear(); - }); test('indicesExist should be TRUE if patternList is NOT empty', async () => { - const { result } = renderHook(() => useSourcererDataView()); + const { result } = renderHook(() => useSourcererDataView(), { + wrapper: ({ children }) => ( + {children} + ), + }); expect(result.current.indicesExist).toEqual(true); }); @@ -93,8 +108,11 @@ describe('createInitialState', () => { analyzer: initialAnalyzerState, } ); - (useDeepEqualSelector as jest.Mock).mockImplementation((cb) => cb(state)); - const { result } = renderHook(() => useSourcererDataView()); + const { result } = renderHook(() => useSourcererDataView(), { + wrapper: ({ children }) => ( + {children} + ), + }); expect(result.current.indicesExist).toEqual(false); }); }); diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts index 8900e07efb01..a110d294fd0f 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts @@ -4,108 +4,93 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { createSelector } from 'reselect'; -import type { DataViewSpec } from '@kbn/data-views-plugin/common'; import type { State } from '../types'; -import type { - SourcererDataView, - SourcererModel, - SourcererScope, - SourcererScopeName, -} from './model'; -export const sourcererKibanaDataViewsSelector = ({ - sourcerer, -}: State): SourcererModel['kibanaDataViews'] => sourcerer.kibanaDataViews; +import type { SourcererModel } from './model'; +import { SourcererScopeName } from './model'; -export const sourcererSignalIndexNameSelector = ({ sourcerer }: State): string | null => - sourcerer.signalIndexName; +const SOURCERER_SCOPE_MAX_SIZE = Object.keys(SourcererScopeName).length; -export const sourcererDefaultDataViewSelector = ({ - sourcerer, -}: State): SourcererModel['defaultDataView'] => sourcerer.defaultDataView; +const selectSourcerer = (state: State): SourcererModel => state.sourcerer; -export const dataViewSelector = ( - { sourcerer }: State, - id: string | null -): SourcererDataView | undefined => - sourcerer.kibanaDataViews.find((dataView) => dataView.id === id); +export const sourcererScopes = createSelector( + selectSourcerer, + (sourcerer) => sourcerer.sourcererScopes +); -export const sourcererScopeIdSelector = ( - { sourcerer }: State, - scopeId: SourcererScopeName -): SourcererScope => sourcerer.sourcererScopes[scopeId]; +export const sourcererScope = createSelector( + sourcererScopes, + (state: State, scopeId: SourcererScopeName) => scopeId, + (scopes, scopeId) => scopes[scopeId], + { + memoizeOptions: { + maxSize: SOURCERER_SCOPE_MAX_SIZE, + }, + } +); -export const scopeIdSelector = () => createSelector(sourcererScopeIdSelector, (scope) => scope); +export const sourcererScopeIsLoading = createSelector(sourcererScope, (scope) => scope.loading, { + memoizeOptions: { + maxSize: SOURCERER_SCOPE_MAX_SIZE, + }, +}); -export const kibanaDataViewsSelector = () => - createSelector(sourcererKibanaDataViewsSelector, (dataViews) => dataViews); +export const sourcererScopeSelectedDataViewId = createSelector( + sourcererScope, + (scope) => scope.selectedDataViewId, + { + memoizeOptions: { + maxSize: SOURCERER_SCOPE_MAX_SIZE, + }, + } +); -export const signalIndexNameSelector = () => - createSelector(sourcererSignalIndexNameSelector, (signalIndexName) => signalIndexName); +export const sourcererScopeSelectedPatterns = createSelector( + sourcererScope, + (scope) => scope.selectedPatterns, + { + memoizeOptions: { + maxSize: SOURCERER_SCOPE_MAX_SIZE, + }, + } +); -export const defaultDataViewSelector = () => - createSelector(sourcererDefaultDataViewSelector, (dataViews) => dataViews); +export const sourcererScopeMissingPatterns = createSelector( + sourcererScope, + (scope) => scope.missingPatterns, + { + memoizeOptions: { + maxSize: SOURCERER_SCOPE_MAX_SIZE, + }, + } +); -export const sourcererDataViewSelector = () => - createSelector(dataViewSelector, (dataView) => dataView); +export const kibanaDataViews = createSelector( + selectSourcerer, + (sourcerer) => sourcerer.kibanaDataViews, + { + memoizeOptions: { + maxSize: SOURCERER_SCOPE_MAX_SIZE, + }, + } +); -export interface SourcererScopeSelector extends Omit { - selectedDataView: SourcererDataView | undefined; - sourcererScope: SourcererScope; -} +export const defaultDataView = createSelector( + selectSourcerer, + (sourcerer) => sourcerer.defaultDataView, + { + memoizeOptions: { + maxSize: SOURCERER_SCOPE_MAX_SIZE, + }, + } +); -export const getSourcererDataViewsSelector = () => { - const getKibanaDataViewsSelector = kibanaDataViewsSelector(); - const getDefaultDataViewSelector = defaultDataViewSelector(); - const getSignalIndexNameSelector = signalIndexNameSelector(); - return (state: State): Omit => { - const kibanaDataViews = getKibanaDataViewsSelector(state); - const defaultDataView = getDefaultDataViewSelector(state); - const signalIndexName = getSignalIndexNameSelector(state); - - return { - defaultDataView, - kibanaDataViews, - signalIndexName, - }; - }; -}; - -/** - * Attn Future Developer - * Access sourcererScope.selectedPatterns from - * hook useSourcererDataView in `common/containers/sourcerer/index` - * in order to get exclude patterns for searches - * Access sourcererScope.selectedPatterns - * from this function for display purposes only - * */ -export const getSourcererScopeSelector = () => { - const getDataViewsSelector = getSourcererDataViewsSelector(); - const getSourcererDataViewSelector = sourcererDataViewSelector(); - const getScopeSelector = scopeIdSelector(); - - return (state: State, scopeId: SourcererScopeName): SourcererScopeSelector => { - const dataViews = getDataViewsSelector(state); - const scope = getScopeSelector(state, scopeId); - const selectedDataView = getSourcererDataViewSelector(state, scope.selectedDataViewId); - - return { - ...dataViews, - selectedDataView, - sourcererScope: scope, - }; - }; -}; - -export const getSelectedDataviewSelector = () => { - const getSourcererDataViewSelector = sourcererDataViewSelector(); - const getScopeSelector = scopeIdSelector(); - - return (state: State, scopeId: SourcererScopeName): DataViewSpec | undefined => { - const scope = getScopeSelector(state, scopeId); - const selectedDataView = getSourcererDataViewSelector(state, scope.selectedDataViewId); - - return selectedDataView?.dataView; - }; -}; +export const signalIndexName = createSelector( + selectSourcerer, + (sourcerer) => sourcerer.signalIndexName, + { + memoizeOptions: { + maxSize: SOURCERER_SCOPE_MAX_SIZE, + }, + } +); diff --git a/x-pack/plugins/security_solution/public/explore/network/components/embeddables/embedded_map.test.tsx b/x-pack/plugins/security_solution/public/explore/network/components/embeddables/embedded_map.test.tsx index d7f4c6f0573d..7dc84b8ceb4a 100644 --- a/x-pack/plugins/security_solution/public/explore/network/components/embeddables/embedded_map.test.tsx +++ b/x-pack/plugins/security_solution/public/explore/network/components/embeddables/embedded_map.test.tsx @@ -7,14 +7,11 @@ import { render, waitFor } from '@testing-library/react'; import React from 'react'; -import * as redux from 'react-redux'; - import '../../../../common/mock/match_media'; -import { TestProviders } from '../../../../common/mock'; +import { TestProviders, mockGlobalState, createMockStore } from '../../../../common/mock'; import { EmbeddedMapComponent } from './embedded_map'; import { createEmbeddable } from './create_embeddable'; -import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import { getLayerList } from './map_config'; import { useIsFieldInIndexPattern } from '../../../containers/fields'; import { buildTimeRangeFilter } from '../../../../detections/components/alerts_table/helpers'; @@ -55,17 +52,47 @@ jest.mock('@kbn/embeddable-plugin/public', () => ({ EmbeddablePanel: jest.fn().mockReturnValue(
), })); -const mockUseSourcererDataView = useSourcererDataView as jest.Mock; const mockCreateEmbeddable = createEmbeddable as jest.Mock; const mockUseIsFieldInIndexPattern = useIsFieldInIndexPattern as jest.Mock; const mockGetStorage = jest.fn(); const mockSetStorage = jest.fn(); const setQuery: jest.Mock = jest.fn(); -const filebeatDataView = { id: '6f1eeb50-023d-11eb-bcb6-6ba0578012a9', title: 'filebeat-*' }; -const packetbeatDataView = { id: '28995490-023d-11eb-bcb6-6ba0578012a9', title: 'packetbeat-*' }; -const mockSelector = { - kibanaDataViews: [filebeatDataView, packetbeatDataView], +const filebeatDataView = { + id: '6f1eeb50-023d-11eb-bcb6-6ba0578012a9', + title: 'filebeat-*', + browserFields: {}, + fields: {}, + loading: false, + patternList: ['filebeat-*'], + dataView: { + id: '6f1eeb50-023d-11eb-bcb6-6ba0578012a9', + fields: {}, + }, + runtimeMappings: {}, + indexFields: [], }; +const packetbeatDataView = { + id: '28995490-023d-11eb-bcb6-6ba0578012a9', + title: 'packetbeat-*', + browserFields: {}, + fields: {}, + loading: false, + patternList: ['packetbeat-*'], + dataView: { + id: '28995490-023d-11eb-bcb6-6ba0578012a9', + fields: {}, + }, + runtimeMappings: {}, + indexFields: [], +}; +const mockState = { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + kibanaDataViews: [filebeatDataView, packetbeatDataView], + }, +}; +const defaultMockStore = createMockStore(mockState); const mockUpdateInput = jest.fn(); const embeddableValue = { destroyed: false, @@ -106,8 +133,6 @@ describe('EmbeddedMapComponent', () => { beforeEach(() => { setQuery.mockClear(); mockGetStorage.mockReturnValue(true); - jest.spyOn(redux, 'useSelector').mockReturnValue(mockSelector); - mockUseSourcererDataView.mockReturnValue({ selectedPatterns: ['filebeat-*', 'auditbeat-*'] }); mockCreateEmbeddable.mockResolvedValue(embeddableValue); mockUseIsFieldInIndexPattern.mockReturnValue(() => true); @@ -121,7 +146,7 @@ describe('EmbeddedMapComponent', () => { test('renders', async () => { const { getByTestId } = render( - + ); @@ -132,7 +157,7 @@ describe('EmbeddedMapComponent', () => { test('calls updateInput with time range filter', async () => { render( - + ); @@ -146,7 +171,7 @@ describe('EmbeddedMapComponent', () => { test('renders EmbeddablePanel from embeddable plugin', async () => { const { getByTestId, queryByTestId } = render( - + ); @@ -159,13 +184,17 @@ describe('EmbeddedMapComponent', () => { }); test('renders IndexPatternsMissingPrompt', async () => { - jest.spyOn(redux, 'useSelector').mockReturnValue({ - ...mockSelector, - kibanaDataViews: [], - }); + const state = { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + kibanaDataViews: [], + }, + }; + const store = createMockStore(state); const { getByTestId, queryByTestId } = render( - + ); @@ -180,7 +209,7 @@ describe('EmbeddedMapComponent', () => { mockCreateEmbeddable.mockResolvedValue(null); const { getByTestId, queryByTestId } = render( - + ); @@ -194,7 +223,7 @@ describe('EmbeddedMapComponent', () => { test('map hidden on close', async () => { mockGetStorage.mockReturnValue(false); const { getByTestId, queryByTestId } = render( - + ); @@ -210,7 +239,7 @@ describe('EmbeddedMapComponent', () => { test('map visible on open', async () => { const { getByTestId, queryByTestId } = render( - + ); @@ -225,8 +254,16 @@ describe('EmbeddedMapComponent', () => { }); test('On mount, selects existing Kibana data views that match any selected index pattern', async () => { + const state = { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + kibanaDataViews: [filebeatDataView], + }, + }; + const store = createMockStore(state); render( - + ); @@ -237,11 +274,16 @@ describe('EmbeddedMapComponent', () => { }); test('On rerender with new selected patterns, selects existing Kibana data views that match any selected index pattern', async () => { - mockUseSourcererDataView.mockReturnValue({ - selectedPatterns: ['filebeat-*', 'auditbeat-*'], - }); + const state = { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + kibanaDataViews: [filebeatDataView], + }, + }; + const store = createMockStore(state); const { rerender } = render( - + ); @@ -249,11 +291,8 @@ describe('EmbeddedMapComponent', () => { const dataViewArg = (getLayerList as jest.Mock).mock.calls[0][0]; expect(dataViewArg).toEqual([filebeatDataView]); }); - mockUseSourcererDataView.mockReturnValue({ - selectedPatterns: ['filebeat-*', 'packetbeat-*'], - }); rerender( - + ); diff --git a/x-pack/plugins/security_solution/public/explore/network/components/embeddables/embedded_map.tsx b/x-pack/plugins/security_solution/public/explore/network/components/embeddables/embedded_map.tsx index d20fd4d1fb49..ee36fb22f8a4 100644 --- a/x-pack/plugins/security_solution/public/explore/network/components/embeddables/embedded_map.tsx +++ b/x-pack/plugins/security_solution/public/explore/network/components/embeddables/embedded_map.tsx @@ -9,6 +9,7 @@ import { EuiAccordion, EuiLink, EuiText } from '@elastic/eui'; import React, { useCallback, useEffect, useState, useMemo } from 'react'; +import { useSelector } from 'react-redux'; import { createHtmlPortalNode, InPortal } from 'react-reverse-portal'; import styled, { css } from 'styled-components'; import type { Filter, Query } from '@kbn/es-query'; @@ -32,10 +33,9 @@ import * as i18n from './translations'; import { useKibana } from '../../../../common/lib/kibana'; import { getLayerList } from './map_config'; import { sourcererSelectors } from '../../../../common/store/sourcerer'; +import type { State } from '../../../../common/store'; import type { SourcererDataView } from '../../../../common/store/sourcerer/model'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; -import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; -import { useSourcererDataView } from '../../../../common/containers/sourcerer'; export const NETWORK_MAP_VISIBLE = 'network_map_visbile'; @@ -119,12 +119,10 @@ export const EmbeddedMapComponent = ({ const { addError } = useAppToasts(); - const getDataViewsSelector = useMemo( - () => sourcererSelectors.getSourcererDataViewsSelector(), - [] - ); - const { kibanaDataViews } = useDeepEqualSelector((state) => getDataViewsSelector(state)); - const { selectedPatterns } = useSourcererDataView(SourcererScopeName.default); + const kibanaDataViews = useSelector(sourcererSelectors.kibanaDataViews); + const selectedPatterns = useSelector((state: State) => { + return sourcererSelectors.sourcererScopeSelectedPatterns(state, SourcererScopeName.default); + }); const isFieldInIndexPattern = useIsFieldInIndexPattern(); @@ -250,7 +248,6 @@ export const EmbeddedMapComponent = ({ () => buildTimeRangeFilter(startDate, endDate), [startDate, endDate] ); - useEffect(() => { if (embeddable != null) { // pass time range as filter instead of via timeRange param diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/hooks/use_navigate_to_timeline.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/hooks/use_navigate_to_timeline.tsx index 85e8fc2b55fb..593ad1d4bb35 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/hooks/use_navigate_to_timeline.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/hooks/use_navigate_to_timeline.tsx @@ -5,11 +5,9 @@ * 2.0. */ -import { useCallback, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; +import { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { v4 as uuidv4 } from 'uuid'; - -import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { sourcererActions } from '../../../../common/store/sourcerer'; import { @@ -33,13 +31,8 @@ export interface Filter { export const useNavigateToTimeline = () => { const dispatch = useDispatch(); - const getDataViewsSelector = useMemo( - () => sourcererSelectors.getSourcererDataViewsSelector(), - [] - ); - const { defaultDataView, signalIndexName } = useDeepEqualSelector((state) => - getDataViewsSelector(state) - ); + const signalIndexName = useSelector(sourcererSelectors.signalIndexName); + const defaultDataView = useSelector(sourcererSelectors.defaultDataView); const clearTimeline = useCreateTimeline({ timelineId: TimelineId.active, diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index f40ea2ad5a6a..7037be2f6189 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -109,6 +109,16 @@ export class Simulator { this.store = createMockStore( { ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + analyzer: { + ...mockGlobalState.sourcerer.sourcererScopes.default, + selectedPatterns: indices, + }, + }, + }, analyzer: { [resolverComponentInstanceID]: EMPTY_RESOLVER, }, diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx index 8a96aa37c467..a37f2185c441 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx @@ -17,6 +17,7 @@ import type { SideEffectSimulator, ResolverProps } from '../../types'; import { ResolverWithoutProviders } from '../../view/resolver_without_providers'; import { SideEffectContext } from '../../view/side_effect_context'; import type { State } from '../../../common/store/types'; +import { TestProviders } from '../../../common/mock'; enableMapSet(); @@ -93,23 +94,25 @@ export const MockResolver = React.memo((props: MockResolverProps) => { }, [props.rasterWidth, props.rasterHeight, props.sideEffectSimulator.controls, resolverElement]); return ( - - - - - - - - - - - + + + + + + + + + + + + + ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/bottom_bar/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/bottom_bar/index.test.tsx index 05828ff9afac..5b395979978f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/bottom_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/bottom_bar/index.test.tsx @@ -20,11 +20,15 @@ jest.mock('react-redux', () => { }; }); +const mockRef = { + current: null, +}; + describe('TimelineBottomBar', () => { test('should render all components for bottom bar', () => { const { getByTestId } = render( - + ); @@ -38,7 +42,7 @@ describe('TimelineBottomBar', () => { test('should not render the event count badge if timeline is open', () => { const { queryByTestId } = render( - + ); @@ -50,7 +54,7 @@ describe('TimelineBottomBar', () => { const { getByTestId } = render( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/bottom_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/bottom_bar/index.tsx index cd7abe5fe5a4..fa71f93dccb9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/bottom_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/bottom_bar/index.tsx @@ -26,50 +26,55 @@ interface TimelineBottomBarProps { * True if the timeline modal is open */ show: boolean; + + openToggleRef: React.MutableRefObject; } /** * This component renders the bottom bar for timeline displayed or most of the pages within Security Solution. */ -export const TimelineBottomBar = React.memo(({ show, timelineId }) => { - const dispatch = useDispatch(); +export const TimelineBottomBar = React.memo( + ({ show, timelineId, openToggleRef }) => { + const dispatch = useDispatch(); - const openTimeline = useCallback( - () => dispatch(timelineActions.showTimeline({ id: timelineId, show: true })), - [dispatch, timelineId] - ); + const openTimeline = useCallback( + () => dispatch(timelineActions.showTimeline({ id: timelineId, show: true })), + [dispatch, timelineId] + ); - const title = useSelector((state: State) => selectTitleByTimelineById(state, timelineId)); + const title = useSelector((state: State) => selectTitleByTimelineById(state, timelineId)); - return ( - - - - - - - - - - - {title} - - - {!show && ( // this is a hack because TimelineEventsCountBadge is using react-reverse-portal so the component which is used in multiple places cannot be visible in multiple places at the same time - - + return ( + + + + - )} - - - - - - ); -}); + + + + + + {title} + + + {!show && ( // this is a hack because TimelineEventsCountBadge is using react-reverse-portal so the component which is used in multiple places cannot be visible in multiple places at the same time + + + + )} + + + + + + ); + } +); TimelineBottomBar.displayName = 'TimelineBottomBar'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx index 2779e36fce2f..a08e947d8b87 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { render, act } from '@testing-library/react'; +import type { Store } from 'redux'; import type { UseFieldBrowserOptionsProps, UseFieldBrowserOptions, FieldEditorActionsRef } from '.'; import { useFieldBrowserOptions } from '.'; import type { Start } from '@kbn/data-view-field-editor-plugin/public/mocks'; @@ -27,22 +28,6 @@ let mockIndexPatternFieldEditor: Start; jest.mock('../../../common/lib/kibana'); const useKibanaMock = useKibana as jest.Mocked; -const defaultDataviewState: { - missingPatterns: string[]; - selectedDataViewId: string | null; -} = { - missingPatterns: [], - selectedDataViewId: 'security-solution', -}; -const mockScopeIdSelector = jest.fn(() => defaultDataviewState); -jest.mock('../../../common/store', () => { - const original = jest.requireActual('../../../common/store'); - return { - ...original, - sourcererSelectors: { scopeIdSelector: () => mockScopeIdSelector }, - }; -}); - const mockIndexFieldsSearch = jest.fn(); jest.mock('../../../common/containers/source/use_data_view', () => ({ useDataView: () => ({ @@ -57,8 +42,10 @@ const mockOnHide = jest.fn(); const runAllPromises = () => new Promise(setImmediate); // helper function to render the hook -const renderUseFieldBrowserOptions = (props: Partial = {}) => - renderHook>( +const renderUseFieldBrowserOptions = ( + props: Partial = {} +) => + renderHook>( () => useFieldBrowserOptions({ sourcererScope: SourcererScopeName.default, @@ -67,7 +54,12 @@ const renderUseFieldBrowserOptions = (props: Partial { + if (store) { + return {children}; + } + return {children}; + }, } ); @@ -104,7 +96,6 @@ describe('useFieldBrowserOptions', () => { ...useKibanaMock().services.application.capabilities, indexPatterns: { save: true }, }; - mockScopeIdSelector.mockReturnValue(defaultDataviewState); jest.clearAllMocks(); }); @@ -137,13 +128,6 @@ describe('useFieldBrowserOptions', () => { ); }); - it('should not return the button when a dataView is not present', () => { - mockScopeIdSelector.mockReturnValue({ missingPatterns: [], selectedDataViewId: null }); - const { result } = renderUseFieldBrowserOptions(); - - expect(result.current.createFieldButton).toBeUndefined(); - }); - it('should call onHide when button is pressed', async () => { useKibanaMock().services.data.dataViews.get = () => Promise.resolve({} as DataView); const { result } = await renderUpdatedUseFieldBrowserOptions(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx index 8c070b6961b6..bc1ca7bb8726 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx @@ -7,6 +7,7 @@ import type { MutableRefObject } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; import type { DataViewField, DataView } from '@kbn/data-views-plugin/common'; import type { CreateFieldComponent, @@ -14,9 +15,9 @@ import type { } from '@kbn/triggers-actions-ui-plugin/public/types'; import type { ColumnHeaderOptions } from '../../../../common/types'; import { useDataView } from '../../../common/containers/source/use_data_view'; -import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { useKibana } from '../../../common/lib/kibana'; import { sourcererSelectors } from '../../../common/store'; +import type { State } from '../../../common/store'; import type { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; @@ -57,11 +58,12 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ dataViewFieldEditor, data: { dataViews }, } = useKibana().services; - - const scopeIdSelector = useMemo(() => sourcererSelectors.scopeIdSelector(), []); - const { missingPatterns, selectedDataViewId } = useDeepEqualSelector((state) => - scopeIdSelector(state, sourcererScope) - ); + const missingPatterns = useSelector((state: State) => { + return sourcererSelectors.sourcererScopeMissingPatterns(state, sourcererScope); + }); + const selectedDataViewId = useSelector((state: State) => { + return sourcererSelectors.sourcererScopeSelectedDataViewId(state, sourcererScope); + }); useEffect(() => { let ignore = false; const fetchAndSetDataView = async (dataViewId: string) => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx index 01c0f1e77e68..e0e61fe722b2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx @@ -10,9 +10,9 @@ import React from 'react'; import { NewTimelineButton } from './new_timeline_button'; import { TimelineId } from '../../../../../common/types'; import { timelineActions } from '../../../store'; -import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { useDiscoverInTimelineContext } from '../../../../common/components/discover_in_timeline/use_discover_in_timeline_context'; import { defaultHeaders } from '../../timeline/body/column_headers/default_headers'; +import { TestProviders } from '../../../../common/mock'; jest.mock('../../../../common/components/discover_in_timeline/use_discover_in_timeline_context'); jest.mock('../../../../common/hooks/use_selector'); @@ -26,11 +26,11 @@ jest.mock('react-redux', () => { }; }); -const renderNewTimelineButton = () => render(); +const renderNewTimelineButton = () => + render(, { wrapper: TestProviders }); describe('NewTimelineButton', () => { it('should render 2 options in the popover when clicking on the button', async () => { - (useDeepEqualSelector as jest.Mock).mockReturnValue({}); (useDiscoverInTimelineContext as jest.Mock).mockReturnValue({}); const { getByTestId, getByText } = renderNewTimelineButton(); @@ -52,12 +52,8 @@ describe('NewTimelineButton', () => { }); it('should call the correct action with clicking on the new timeline button', () => { - const dataViewId = 'dataViewId'; - const selectedPatterns = ['selectedPatterns']; - (useDeepEqualSelector as jest.Mock).mockReturnValue({ - id: dataViewId, - patternList: selectedPatterns, - }); + const dataViewId = ''; + const selectedPatterns: string[] = []; (useDiscoverInTimelineContext as jest.Mock).mockReturnValue({ resetDiscoverAppState: jest.fn(), }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx index 15246435852f..2f06c359f2ec 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx @@ -41,10 +41,13 @@ jest.mock('react-redux', () => { }); const timelineId = 'timeline-1'; +const mockRef = { + current: null, +}; const renderTimelineModalHeader = () => render( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.tsx index aca4fda13f69..3d3936521c3a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.tsx @@ -62,130 +62,136 @@ interface FlyoutHeaderPanelProps { * Id of the timeline to be displayed within the modal */ timelineId: string; + openToggleRef: React.MutableRefObject; } /** * Component rendered at the top of the timeline modal. It contains the timeline title, all the action buttons (save, open, favorite...) and the close button */ -export const TimelineModalHeader = React.memo(({ timelineId }) => { - const dispatch = useDispatch(); - const { browserFields, indexPattern } = useSourcererDataView(SourcererScopeName.timeline); - const { cases, uiSettings } = useKibana().services; - const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); - const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); +export const TimelineModalHeader = React.memo( + ({ timelineId, openToggleRef }) => { + const dispatch = useDispatch(); + const { browserFields, indexPattern } = useSourcererDataView(SourcererScopeName.timeline); + const { cases, uiSettings } = useKibana().services; + const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); - const title = useSelector((state: State) => selectTitleByTimelineById(state, timelineId)); - const isDataInTimeline = useSelector((state: State) => selectDataInTimeline(state, timelineId)); - const kqlQueryObj = useSelector((state: State) => selectKqlQuery(state, timelineId)); + const title = useSelector((state: State) => selectTitleByTimelineById(state, timelineId)); + const isDataInTimeline = useSelector((state: State) => selectDataInTimeline(state, timelineId)); + const kqlQueryObj = useSelector((state: State) => selectKqlQuery(state, timelineId)); - const { activeTab, dataProviders, timelineType, filters, kqlMode } = useSelector((state: State) => - selectTimelineById(state, timelineId) - ); + const { activeTab, dataProviders, timelineType, filters, kqlMode } = useSelector( + (state: State) => selectTimelineById(state, timelineId) + ); - const combinedQueries = useMemo( - () => - combineQueries({ - config: esQueryConfig, - dataProviders, - indexPattern, - browserFields, - filters: filters ? filters : [], - kqlQuery: kqlQueryObj, - kqlMode, - }), - [browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQueryObj] - ); - const isInspectDisabled = !isDataInTimeline || combinedQueries?.filterQuery === undefined; + const combinedQueries = useMemo( + () => + combineQueries({ + config: esQueryConfig, + dataProviders, + indexPattern, + browserFields, + filters: filters ? filters : [], + kqlQuery: kqlQueryObj, + kqlMode, + }), + [browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQueryObj] + ); + const isInspectDisabled = !isDataInTimeline || combinedQueries?.filterQuery === undefined; - const closeTimeline = useCallback(() => { - createHistoryEntry(); - dispatch(timelineActions.showTimeline({ id: timelineId, show: false })); - }, [dispatch, timelineId]); + const closeTimeline = useCallback(() => { + if (openToggleRef.current != null) { + openToggleRef.current.focus(); + } + createHistoryEntry(); + dispatch(timelineActions.showTimeline({ id: timelineId, show: false })); + }, [dispatch, timelineId, openToggleRef]); - return ( - - - - - - - - - -

{title}

-
-
- - - -
-
- - - - - - - - - - - - {userCasesPermissions.create && userCasesPermissions.read ? ( - <> - - - - - - - - ) : null} - - - - - - + + + + + + + +

{title}

+
+
+ + + +
+
+ + + + + + + + + + -
-
-
-
-
-
- ); -}); + + {userCasesPermissions.create && userCasesPermissions.read ? ( + <> + + + + + + + + ) : null} + + + + + + + + + + + + + ); + } +); TimelineModalHeader.displayName = 'TimelineModalHeader'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/modal/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/index.test.tsx index fdcdc5a501ae..3fa01c0dd409 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/modal/index.test.tsx @@ -21,10 +21,14 @@ jest.mock('../../../common/store/selectors', () => ({ inputsSelectors: { timelineFullScreenSelector: () => mockIsFullScreen() }, })); +const mockRef = { + current: null, +}; + const renderTimelineModal = () => render( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/modal/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/index.tsx index feb5cf74494a..4d8c5cdf6915 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/modal/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/modal/index.tsx @@ -33,43 +33,51 @@ interface TimelineModalProps { * If true the timeline modal will be visible */ visible?: boolean; + openToggleRef: React.MutableRefObject; } /** * Renders the timeline modal. Internally this is using an EuiPortal. */ -export const TimelineModal = React.memo(({ timelineId, visible = true }) => { - const ref = useRef(null); - const isFullScreen = useShallowEqualSelector(inputsSelectors.timelineFullScreenSelector) ?? false; +export const TimelineModal = React.memo( + ({ timelineId, openToggleRef, visible = true }) => { + const ref = useRef(null); + const isFullScreen = + useShallowEqualSelector(inputsSelectors.timelineFullScreenSelector) ?? false; - const styles = usePaneStyles(); - const wrapperClassName = classNames('timeline-portal-overlay-mask', styles, { - 'timeline-portal-overlay-mask--full-screen': isFullScreen, - 'timeline-portal-overlay-mask--hidden': !visible, - }); + const styles = usePaneStyles(); + const wrapperClassName = classNames('timeline-portal-overlay-mask', styles, { + 'timeline-portal-overlay-mask--full-screen': isFullScreen, + 'timeline-portal-overlay-mask--hidden': !visible, + }); - const sibling: HTMLDivElement | null = useMemo(() => (!visible ? ref?.current : null), [visible]); + const sibling: HTMLDivElement | null = useMemo( + () => (!visible ? ref?.current : null), + [visible] + ); - return ( -
- -
-
- + return ( +
+ +
+
+ +
-
- - {visible && } -
- ); -}); + + {visible && } +
+ ); + } +); TimelineModal.displayName = 'TimelineModal'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx index 55a414b9df60..11f700cd8085 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx @@ -10,10 +10,10 @@ import React from 'react'; import { NewTimelineButton } from '.'; import { TimelineId } from '../../../../common/types'; import { timelineActions } from '../../store'; -import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { useDiscoverInTimelineContext } from '../../../common/components/discover_in_timeline/use_discover_in_timeline_context'; import { defaultHeaders } from '../timeline/body/column_headers/default_headers'; import { TimelineType } from '../../../../common/api/timeline'; +import { TestProviders } from '../../../common/mock'; jest.mock('../../../common/components/discover_in_timeline/use_discover_in_timeline_context'); jest.mock('../../../common/hooks/use_selector'); @@ -27,15 +27,12 @@ jest.mock('react-redux', () => { }; }); -const renderNewTimelineButton = (type: TimelineType) => render(); +const renderNewTimelineButton = (type: TimelineType) => + render(, { wrapper: TestProviders }); describe('NewTimelineButton', () => { - const dataViewId = 'dataViewId'; - const selectedPatterns = ['selectedPatterns']; - (useDeepEqualSelector as jest.Mock).mockReturnValue({ - id: dataViewId, - patternList: selectedPatterns, - }); + const dataViewId = ''; + const selectedPatterns: string[] = []; (useDiscoverInTimelineContext as jest.Mock).mockReturnValue({ resetDiscoverAppState: jest.fn(), }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index cfe749a69a4b..fbc8336fed07 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -70,6 +70,9 @@ jest.mock('react-router-dom', () => { }); const mockDispatch = jest.fn(); +const mockRef = { + current: null, +}; jest.mock('react-redux', () => { const actual = jest.requireActual('react-redux'); @@ -95,6 +98,7 @@ describe('StatefulTimeline', () => { renderCellValue: DefaultCellRenderer, rowRenderers: defaultRowRenderers, timelineId: TimelineId.test, + openToggleRef: mockRef, }; beforeEach(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index c40b54d8cd3b..5faf91b9fd6e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -8,7 +8,7 @@ import { pick } from 'lodash/fp'; import { EuiProgress } from '@elastic/eui'; import React, { useCallback, useEffect, useMemo, useRef, createContext } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; import { isTab } from '@kbn/timelines-plugin/public'; @@ -22,6 +22,7 @@ import { TimelineModalHeader } from '../modal/header'; import type { TimelineId, RowRenderer, TimelineTabs } from '../../../../common/types/timeline'; import { TimelineType } from '../../../../common/api/timeline'; import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import type { State } from '../../../common/store'; import { EVENTS_COUNT_BUTTON_CLASS_NAME, onTimelineTabKeyPressed } from './helpers'; import * as i18n from './translations'; import { TabsContent } from './tabs_content'; @@ -50,6 +51,7 @@ export interface Props { renderCellValue: (props: CellValueElementProps) => React.ReactNode; rowRenderers: RowRenderer[]; timelineId: TimelineId; + openToggleRef: React.MutableRefObject; } const TimelineSavingProgressComponent: React.FC<{ timelineId: TimelineId }> = ({ timelineId }) => { @@ -67,15 +69,17 @@ const StatefulTimelineComponent: React.FC = ({ renderCellValue, rowRenderers, timelineId, + openToggleRef, }) => { const dispatch = useDispatch(); const containerElement = useRef(null); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const scopeIdSelector = useMemo(() => sourcererSelectors.scopeIdSelector(), []); - const { - selectedPatterns: selectedPatternsSourcerer, - selectedDataViewId: selectedDataViewIdSourcerer, - } = useDeepEqualSelector((state) => scopeIdSelector(state, SourcererScopeName.timeline)); + const selectedPatternsSourcerer = useSelector((state: State) => { + return sourcererSelectors.sourcererScopeSelectedPatterns(state, SourcererScopeName.timeline); + }); + const selectedDataViewIdSourcerer = useSelector((state: State) => { + return sourcererSelectors.sourcererScopeSelectedDataViewId(state, SourcererScopeName.timeline); + }); const { dataViewId: selectedDataViewIdTimeline, indexNames: selectedPatternsTimeline, @@ -233,7 +237,7 @@ const StatefulTimelineComponent: React.FC = ({ $isVisible={!timelineFullScreen} data-test-subj="timeline-hide-show-container" > - + sourcererSelectors.defaultDataViewSelector(), - [] - ); - const defaultDataView = useDeepEqualSelector(getDefaultDataViewSelector); + const defaultDataView = useSelector(sourcererSelectors.defaultDataView); const { pathname } = useLocation(); const { selectedPatterns: nonTimelinePatterns } = useSourcererDataView( getScopeFromPath(pathname) diff --git a/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx index 0e11db28e391..866044da3462 100644 --- a/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx @@ -4,15 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import type { RenderHookResult } from '@testing-library/react-hooks'; +import React from 'react'; import { renderHook } from '@testing-library/react-hooks'; -import type { UseCreateTimelineParams } from './use_create_timeline'; import { useCreateTimeline } from './use_create_timeline'; import type { TimeRange } from '../../common/store/inputs/model'; import { TimelineType } from '../../../common/api/timeline'; import { TimelineId } from '../../../common/types'; -import { useDeepEqualSelector } from '../../common/hooks/use_selector'; import { useDiscoverInTimelineContext } from '../../common/components/discover_in_timeline/use_discover_in_timeline_context'; import { timelineActions } from '../store'; import { inputsActions } from '../../common/store/inputs'; @@ -21,87 +18,90 @@ import { appActions } from '../../common/store/app'; import { defaultHeaders } from '../components/timeline/body/column_headers/default_headers'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; import { InputsModelId } from '../../common/store/inputs/constants'; +import { TestProviders, mockGlobalState } from '../../common/mock'; jest.mock('../../common/components/discover_in_timeline/use_discover_in_timeline_context'); -jest.mock('../../common/hooks/use_selector'); -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); - +jest.mock('../../common/containers/use_global_time', () => { return { - ...original, - useSelector: jest.fn(), - useDispatch: () => jest.fn(), + useGlobalTime: jest.fn().mockReturnValue({ + from: '2022-04-05T12:00:00.000Z', + to: '2022-04-08T12:00:00.000Z', + setQuery: () => jest.fn(), + deleteQuery: () => jest.fn(), + }), }; }); +jest.mock('../../common/lib/kibana'); describe('useCreateTimeline', () => { - let hookResult: RenderHookResult< - UseCreateTimelineParams, - (options?: { timeRange?: TimeRange }) => void - >; - const resetDiscoverAppState = jest.fn(); (useDiscoverInTimelineContext as jest.Mock).mockReturnValue({ resetDiscoverAppState }); it('should return a function', () => { - (useDeepEqualSelector as jest.Mock).mockReturnValue({}); - - hookResult = renderHook(() => - useCreateTimeline({ timelineId: TimelineId.test, timelineType: TimelineType.default }) + const hookResult = renderHook( + () => useCreateTimeline({ timelineId: TimelineId.test, timelineType: TimelineType.default }), + { + wrapper: ({ children }) => {children}, + } ); expect(hookResult.result.current).toEqual(expect.any(Function)); }); it('should dispatch correct actions when calling the returned function', () => { - const dataViewId = 'dataViewId'; - const selectedPatterns = ['selectedPatterns']; - (useDeepEqualSelector as jest.Mock).mockReturnValue({ - id: dataViewId, - patternList: selectedPatterns, - }); - const createTimeline = jest.spyOn(timelineActions, 'createTimeline'); const setSelectedDataView = jest.spyOn(sourcererActions, 'setSelectedDataView'); const addLinkTo = jest.spyOn(inputsActions, 'addLinkTo'); const addNotes = jest.spyOn(appActions, 'addNotes'); - hookResult = renderHook(() => - useCreateTimeline({ timelineId: TimelineId.test, timelineType: TimelineType.default }) + const hookResult = renderHook( + () => useCreateTimeline({ timelineId: TimelineId.test, timelineType: TimelineType.default }), + { + wrapper: ({ children }) => {children}, + } ); expect(hookResult.result.current).toEqual(expect.any(Function)); hookResult.result.current(); - - expect(createTimeline).toHaveBeenCalledWith({ - columns: defaultHeaders, - dataViewId, - id: TimelineId.test, - indexNames: selectedPatterns, - show: true, - timelineType: 'default', - updated: undefined, - }); - expect(setSelectedDataView).toHaveBeenCalledWith({ - id: SourcererScopeName.timeline, - selectedDataViewId: dataViewId, - selectedPatterns, - }); + expect(createTimeline.mock.calls[0][0].id).toEqual(TimelineId.test); + expect(createTimeline.mock.calls[0][0].timelineType).toEqual(TimelineType.default); + expect(createTimeline.mock.calls[0][0].columns).toEqual(defaultHeaders); + expect(createTimeline.mock.calls[0][0].dataViewId).toEqual( + mockGlobalState.sourcerer.defaultDataView.id + ); + expect(createTimeline.mock.calls[0][0].indexNames).toEqual( + expect.arrayContaining( + mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline].selectedPatterns + ) + ); + expect(createTimeline.mock.calls[0][0].show).toEqual(true); + expect(createTimeline.mock.calls[0][0].updated).toEqual(undefined); + expect(setSelectedDataView.mock.calls[0][0].id).toEqual(SourcererScopeName.timeline); + expect(setSelectedDataView.mock.calls[0][0].selectedDataViewId).toEqual( + mockGlobalState.sourcerer.defaultDataView.id + ); + expect(setSelectedDataView.mock.calls[0][0].selectedPatterns).toEqual( + expect.arrayContaining( + mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline].selectedPatterns + ) + ); expect(addLinkTo).toHaveBeenCalledWith([InputsModelId.global, InputsModelId.timeline]); expect(addNotes).toHaveBeenCalledWith({ notes: [] }); }); it('should run the onClick method if provided', () => { - (useDeepEqualSelector as jest.Mock).mockReturnValue({}); - const onClick = jest.fn(); - hookResult = renderHook(() => - useCreateTimeline({ - timelineId: TimelineId.test, - timelineType: TimelineType.default, - onClick, - }) + const hookResult = renderHook( + () => + useCreateTimeline({ + timelineId: TimelineId.test, + timelineType: TimelineType.default, + onClick, + }), + { + wrapper: ({ children }) => {children}, + } ); hookResult.result.current(); @@ -111,13 +111,14 @@ describe('useCreateTimeline', () => { }); it('should dispatch removeLinkTo action if absolute timeRange is passed to callback', () => { - (useDeepEqualSelector as jest.Mock).mockReturnValue({}); - const removeLinkTo = jest.spyOn(inputsActions, 'removeLinkTo'); const setAbsoluteRangeDatePicker = jest.spyOn(inputsActions, 'setAbsoluteRangeDatePicker'); - hookResult = renderHook(() => - useCreateTimeline({ timelineId: TimelineId.test, timelineType: TimelineType.default }) + const hookResult = renderHook( + () => useCreateTimeline({ timelineId: TimelineId.test, timelineType: TimelineType.default }), + { + wrapper: ({ children }) => {children}, + } ); const timeRange: TimeRange = { kind: 'absolute', from: '', to: '' }; @@ -131,12 +132,13 @@ describe('useCreateTimeline', () => { }); it('should dispatch removeLinkTo action if relative timeRange is passed to callback', () => { - (useDeepEqualSelector as jest.Mock).mockReturnValue({}); - const setRelativeRangeDatePicker = jest.spyOn(inputsActions, 'setRelativeRangeDatePicker'); - hookResult = renderHook(() => - useCreateTimeline({ timelineId: TimelineId.test, timelineType: TimelineType.default }) + const hookResult = renderHook( + () => useCreateTimeline({ timelineId: TimelineId.test, timelineType: TimelineType.default }), + { + wrapper: ({ children }) => {children}, + } ); const timeRange: TimeRange = { kind: 'relative', fromStr: '', toStr: '', from: '', to: '' }; diff --git a/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx index 89e5d74ead76..f4c7cbc0ebce 100644 --- a/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import { useCallback, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; +import { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { InputsModelId } from '../../common/store/inputs/constants'; import { defaultHeaders } from '../components/timeline/body/column_headers/default_headers'; import { timelineActions } from '../store'; @@ -47,9 +47,9 @@ export const useCreateTimeline = ({ onClick, }: UseCreateTimelineParams): ((options?: { timeRange?: TimeRange }) => void) => { const dispatch = useDispatch(); - const defaultDataViewSelector = useMemo(() => sourcererSelectors.defaultDataViewSelector(), []); - const { id: dataViewId, patternList: selectedPatterns } = - useDeepEqualSelector(defaultDataViewSelector); + const { id: dataViewId, patternList: selectedPatterns } = useSelector( + sourcererSelectors.defaultDataView + ) ?? { id: '', patternList: [] }; const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); const globalTimeRange = useDeepEqualSelector(inputsSelectors.globalTimeRangeSelector); diff --git a/x-pack/plugins/security_solution/public/timelines/wrapper/index.tsx b/x-pack/plugins/security_solution/public/timelines/wrapper/index.tsx index 0a6e95996740..bb5389193161 100644 --- a/x-pack/plugins/security_solution/public/timelines/wrapper/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/wrapper/index.tsx @@ -6,7 +6,7 @@ */ import { EuiFocusTrap, EuiWindowEvent, keys } from '@elastic/eui'; -import React, { useMemo, useCallback } from 'react'; +import React, { useMemo, useCallback, useRef } from 'react'; import type { AppLeaveHandler } from '@kbn/core/public'; import { useDispatch } from 'react-redux'; import { TimelineModal } from '../components/modal'; @@ -38,7 +38,7 @@ export const TimelineWrapper: React.FC = React.memo( const getTimelineShowStatus = useMemo(() => getTimelineShowStatusByIdSelector(), []); const { show } = useDeepEqualSelector((state) => getTimelineShowStatus(state, timelineId)); const dispatch = useDispatch(); - + const openToggleRef = useRef(null); const handleClose = useCallback(() => { dispatch(timelineActions.showTimeline({ id: timelineId, show: false })); }, [dispatch, timelineId]); @@ -58,9 +58,9 @@ export const TimelineWrapper: React.FC = React.memo( return ( <> - + - + );