[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,
This commit is contained in:
Kevin Qualters 2024-02-22 20:56:52 -05:00 committed by GitHub
parent b207ff4534
commit 7745d36703
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 633 additions and 587 deletions

View file

@ -5,13 +5,12 @@
* 2.0. * 2.0.
*/ */
import React, { useCallback, useMemo } from 'react'; import React, { useCallback } from 'react';
import { EuiButton, EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; import { EuiButton, EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
import type { Filter } from '@kbn/es-query'; 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 { useAssistantContext } from '@kbn/elastic-assistant';
import { useDeepEqualSelector } from '../../common/hooks/use_selector';
import { sourcererSelectors } from '../../common/store'; import { sourcererSelectors } from '../../common/store';
import { sourcererActions } from '../../common/store/actions'; import { sourcererActions } from '../../common/store/actions';
import { inputsActions } from '../../common/store/inputs'; import { inputsActions } from '../../common/store/inputs';
@ -63,13 +62,8 @@ export const SendToTimelineButton: React.FunctionComponent<SendToTimelineButtonP
const isEsqlTabInTimelineDisabled = useIsExperimentalFeatureEnabled('timelineEsqlTabDisabled'); const isEsqlTabInTimelineDisabled = useIsExperimentalFeatureEnabled('timelineEsqlTabDisabled');
const getDataViewsSelector = useMemo( const signalIndexName = useSelector(sourcererSelectors.signalIndexName);
() => sourcererSelectors.getSourcererDataViewsSelector(), const defaultDataView = useSelector(sourcererSelectors.defaultDataView);
[]
);
const { defaultDataView, signalIndexName } = useDeepEqualSelector((state) =>
getDataViewsSelector(state)
);
const hasTemplateProviders = const hasTemplateProviders =
dataProviders && dataProviders.find((provider) => provider.type === 'template'); dataProviders && dataProviders.find((provider) => provider.type === 'template');

View file

@ -5,11 +5,11 @@
* 2.0. * 2.0.
*/ */
import React, { useMemo, useCallback } from 'react'; import React, { useCallback } from 'react';
import { EuiButton, EuiButtonEmpty } from '@elastic/eui'; import { EuiButton, EuiButtonEmpty } from '@elastic/eui';
import type { IconType } from '@elastic/eui'; import type { IconType } from '@elastic/eui';
import type { Filter } from '@kbn/es-query'; import type { Filter } from '@kbn/es-query';
import { useDispatch } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { sourcererSelectors } from '../../../store'; import { sourcererSelectors } from '../../../store';
import { InputsModelId } from '../../../store/inputs/constants'; import { InputsModelId } from '../../../store/inputs/constants';
@ -23,7 +23,6 @@ import { TimelineId } from '../../../../../common/types/timeline';
import { TimelineType } from '../../../../../common/api/timeline'; import { TimelineType } from '../../../../../common/api/timeline';
import { useCreateTimeline } from '../../../../timelines/hooks/use_create_timeline'; import { useCreateTimeline } from '../../../../timelines/hooks/use_create_timeline';
import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations'; import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations';
import { useDeepEqualSelector } from '../../../hooks/use_selector';
export interface InvestigateInTimelineButtonProps { export interface InvestigateInTimelineButtonProps {
asEmptyButton: boolean; asEmptyButton: boolean;
@ -49,13 +48,8 @@ export const InvestigateInTimelineButton: React.FunctionComponent<
}) => { }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const getDataViewsSelector = useMemo( const signalIndexName = useSelector(sourcererSelectors.signalIndexName);
() => sourcererSelectors.getSourcererDataViewsSelector(), const defaultDataView = useSelector(sourcererSelectors.defaultDataView);
[]
);
const { defaultDataView, signalIndexName } = useDeepEqualSelector((state) =>
getDataViewsSelector(state)
);
const hasTemplateProviders = const hasTemplateProviders =
dataProviders && dataProviders.find((provider) => provider.type === 'template'); dataProviders && dataProviders.find((provider) => provider.type === 'template');

View file

@ -15,13 +15,13 @@ import {
} from '@elastic/eui'; } from '@elastic/eui';
import type { ChangeEventHandler } from 'react'; import type { ChangeEventHandler } from 'react';
import React, { useCallback, useEffect, useMemo, useState } 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 * as i18n from './translations';
import type { sourcererModel } from '../../store/sourcerer'; import type { sourcererModel } from '../../store/sourcerer';
import { sourcererActions, sourcererSelectors } from '../../store/sourcerer'; import { sourcererActions, sourcererSelectors } from '../../store/sourcerer';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import type { SourcererUrlState } from '../../store/sourcerer/model'; import type { SourcererUrlState } from '../../store/sourcerer/model';
import type { State } from '../../store';
import type { ModifiedTypes } from './use_pick_index_patterns'; import type { ModifiedTypes } from './use_pick_index_patterns';
import { SourcererScopeName } from '../../store/sourcerer/model'; import { SourcererScopeName } from '../../store/sourcerer/model';
import { usePickIndexPatterns } from './use_pick_index_patterns'; import { usePickIndexPatterns } from './use_pick_index_patterns';
@ -129,17 +129,18 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
const isDefaultSourcerer = scopeId === SourcererScopeName.default; const isDefaultSourcerer = scopeId === SourcererScopeName.default;
const updateUrlParam = useUpdateUrlParam<SourcererUrlState>(URL_PARAM_KEY.sourcerer); const updateUrlParam = useUpdateUrlParam<SourcererUrlState>(URL_PARAM_KEY.sourcerer);
const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []); const signalIndexName = useSelector(sourcererSelectors.signalIndexName);
const { const defaultDataView = useSelector(sourcererSelectors.defaultDataView);
defaultDataView, const kibanaDataViews = useSelector(sourcererSelectors.kibanaDataViews);
kibanaDataViews, const selectedDataViewId = useSelector((state: State) => {
signalIndexName, return sourcererSelectors.sourcererScopeSelectedDataViewId(state, scopeId);
sourcererScope: { });
selectedDataViewId, const selectedPatterns = useSelector((state: State) => {
selectedPatterns, return sourcererSelectors.sourcererScopeSelectedPatterns(state, scopeId);
missingPatterns: sourcererMissingPatterns, });
}, const sourcererMissingPatterns = useSelector((state: State) => {
} = useDeepEqualSelector((state) => sourcererScopeSelector(state, scopeId)); return sourcererSelectors.sourcererScopeMissingPatterns(state, scopeId);
});
const { pollForSignalIndex } = useSignalHelpers(); const { pollForSignalIndex } = useSignalHelpers();
useEffect(() => { useEffect(() => {

View file

@ -6,7 +6,7 @@
*/ */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; 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 { i18n } from '@kbn/i18n';
import { matchPath } from 'react-router-dom'; import { matchPath } from 'react-router-dom';
import { sourcererActions, sourcererSelectors } from '../../store/sourcerer'; 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 { createSourcererDataView } from './create_sourcerer_data_view';
import { getDataViewStateFromIndexFields, useDataView } from '../source/use_data_view'; import { getDataViewStateFromIndexFields, useDataView } from '../source/use_data_view';
import { useFetchIndex } from '../source'; import { useFetchIndex } from '../source';
import type { State } from '../../store';
import { useInitializeUrlParam, useUpdateUrlParam } from '../../utils/global_query_string'; import { useInitializeUrlParam, useUpdateUrlParam } from '../../utils/global_query_string';
import { URL_PARAM_KEY } from '../../hooks/use_url_state'; import { URL_PARAM_KEY } from '../../hooks/use_url_state';
import { sortWithExcludesAtEnd } from '../../../../common/utils/sourcerer'; import { sortWithExcludesAtEnd } from '../../../../common/utils/sourcerer';
@ -54,14 +55,8 @@ export const useInitSourcerer = (
const { loading: loadingSignalIndex, isSignalIndexExists, signalIndexName } = useUserInfo(); const { loading: loadingSignalIndex, isSignalIndexExists, signalIndexName } = useUserInfo();
const updateUrlParam = useUpdateUrlParam<SourcererUrlState>(URL_PARAM_KEY.sourcerer); const updateUrlParam = useUpdateUrlParam<SourcererUrlState>(URL_PARAM_KEY.sourcerer);
const getDataViewsSelector = useMemo( const signalIndexNameSourcerer = useSelector(sourcererSelectors.signalIndexName);
() => sourcererSelectors.getSourcererDataViewsSelector(), const defaultDataView = useSelector(sourcererSelectors.defaultDataView);
[]
);
const { defaultDataView, signalIndexName: signalIndexNameSourcerer } = useDeepEqualSelector(
(state) => getDataViewsSelector(state)
);
const { addError, addWarning } = useAppToasts(); const { addError, addWarning } = useAppToasts();
useEffect(() => { useEffect(() => {
@ -83,19 +78,29 @@ export const useInitSourcerer = (
getTimelineSelector(state, TimelineId.active) getTimelineSelector(state, TimelineId.active)
); );
const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []); const scopeDataViewId = useSelector((state: State) => {
const { return sourcererSelectors.sourcererScopeSelectedDataViewId(state, scopeId);
sourcererScope: { selectedDataViewId: scopeDataViewId, selectedPatterns, missingPatterns }, });
} = useDeepEqualSelector((state) => sourcererScopeSelector(state, scopeId)); const selectedPatterns = useSelector((state: State) => {
return sourcererSelectors.sourcererScopeSelectedPatterns(state, scopeId);
});
const missingPatterns = useSelector((state: State) => {
return sourcererSelectors.sourcererScopeMissingPatterns(state, scopeId);
});
const { const kibanaDataViews = useSelector(sourcererSelectors.kibanaDataViews);
selectedDataView: timelineSelectedDataView, const timelineDataViewId = useSelector((state: State) => {
sourcererScope: { return sourcererSelectors.sourcererScopeSelectedDataViewId(state, SourcererScopeName.timeline);
selectedDataViewId: timelineDataViewId, });
selectedPatterns: timelineSelectedPatterns, const timelineSelectedPatterns = useSelector((state: State) => {
missingPatterns: timelineMissingPatterns, return sourcererSelectors.sourcererScopeSelectedPatterns(state, SourcererScopeName.timeline);
}, });
} = useDeepEqualSelector((state) => sourcererScopeSelector(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(); const { indexFieldsSearch } = useDataView();
@ -387,26 +392,23 @@ export const useInitSourcerer = (
export const useSourcererDataView = ( export const useSourcererDataView = (
scopeId: SourcererScopeName = SourcererScopeName.default scopeId: SourcererScopeName = SourcererScopeName.default
): SelectedDataView => { ): SelectedDataView => {
const { getDataViewsSelector, getSourcererDataViewSelector, getScopeSelector } = useMemo( const kibanaDataViews = useSelector(sourcererSelectors.kibanaDataViews);
() => ({ const signalIndexName = useSelector(sourcererSelectors.signalIndexName);
getDataViewsSelector: sourcererSelectors.getSourcererDataViewsSelector(), const defaultDataView = useSelector(sourcererSelectors.defaultDataView);
getSourcererDataViewSelector: sourcererSelectors.sourcererDataViewSelector(), const selectedDataViewId = useSelector((state: State) => {
getScopeSelector: sourcererSelectors.scopeIdSelector(), return sourcererSelectors.sourcererScopeSelectedDataViewId(state, scopeId);
}), });
[] const selectedDataView = useMemo(() => {
); return kibanaDataViews.find((dataView) => dataView.id === selectedDataViewId);
const { }, [kibanaDataViews, selectedDataViewId]);
defaultDataView, const loading = useSelector((state: State) => {
signalIndexName, return sourcererSelectors.sourcererScopeIsLoading(state, scopeId);
selectedDataView, });
sourcererScope: { missingPatterns, selectedPatterns: scopeSelectedPatterns, loading }, const scopeSelectedPatterns = useSelector((state: State) => {
}: sourcererSelectors.SourcererScopeSelector = useDeepEqualSelector((state) => { return sourcererSelectors.sourcererScopeSelectedPatterns(state, scopeId);
const sourcererScope = getScopeSelector(state, scopeId); });
return { const missingPatterns = useSelector((state: State) => {
...getDataViewsSelector(state), return sourcererSelectors.sourcererScopeMissingPatterns(state, scopeId);
selectedDataView: getSourcererDataViewSelector(state, sourcererScope.selectedDataViewId),
sourcererScope,
};
}); });
const selectedPatterns = useMemo( const selectedPatterns = useMemo(

View file

@ -7,9 +7,8 @@
import { useCallback, useMemo, useRef } from 'react'; import { useCallback, useMemo, useRef } from 'react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { useDispatch } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { sourcererSelectors } from '../../store'; import { sourcererSelectors } from '../../store';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { useSourcererDataView } from '.'; import { useSourcererDataView } from '.';
import { SourcererScopeName } from '../../store/sourcerer/model'; import { SourcererScopeName } from '../../store/sourcerer/model';
import { useDataView } from '../source/use_data_view'; import { useDataView } from '../source/use_data_view';
@ -33,17 +32,8 @@ export const useSignalHelpers = (): {
data: { dataViews }, data: { dataViews },
} = useKibana().services; } = useKibana().services;
const getDefaultDataViewSelector = useMemo( const signalIndexNameSourcerer = useSelector(sourcererSelectors.signalIndexName);
() => sourcererSelectors.defaultDataViewSelector(), const defaultDataView = useSelector(sourcererSelectors.defaultDataView);
[]
);
const getSignalIndexNameSelector = useMemo(
() => sourcererSelectors.signalIndexNameSelector(),
[]
);
const signalIndexNameSourcerer = useDeepEqualSelector(getSignalIndexNameSelector);
const defaultDataView = useDeepEqualSelector(getDefaultDataViewSelector);
const signalIndexNeedsInit = useMemo( const signalIndexNeedsInit = useMemo(
() => !defaultDataView.title.includes(`${signalIndexNameSourcerer}`), () => !defaultDataView.title.includes(`${signalIndexNameSourcerer}`),
[defaultDataView.title, signalIndexNameSourcerer] [defaultDataView.title, signalIndexNameSourcerer]

View file

@ -5,17 +5,21 @@
* 2.0. * 2.0.
*/ */
import { useCallback } from 'react'; import { useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import type { SourcererScopeName } from '../store/sourcerer/model'; import type { SourcererScopeName } from '../store/sourcerer/model';
import { getSelectedDataviewSelector } from '../store/sourcerer/selectors'; import { sourcererSelectors } from '../store/sourcerer';
import { useDeepEqualSelector } from './use_selector'; import type { State } from '../store';
// Calls it from the module scope due to non memoized selectors https://github.com/elastic/kibana/issues/159315
const selectedDataviewSelector = getSelectedDataviewSelector();
export const useGetFieldSpec = (scopeId: SourcererScopeName) => { 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( return useCallback(
(fieldName: string) => { (fieldName: string) => {
const fields = dataView?.fields; const fields = dataView?.fields;

View file

@ -4,23 +4,39 @@
* 2.0; you may not use this file except in compliance with the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License
* 2.0. * 2.0.
*/ */
import React from 'react';
import { parseExperimentalConfigValue } from '../../../common/experimental_features'; import { parseExperimentalConfigValue } from '../../../common/experimental_features';
import type { SecuritySubPlugins } from '../../app/types'; import type { SecuritySubPlugins } from '../../app/types';
import { createInitialState } from './reducer'; import { createInitialState } from './reducer';
import { mockIndexPattern, mockSourcererState } from '../mock'; import { mockIndexPattern, mockSourcererState, TestProviders, createMockStore } from '../mock';
import { useSourcererDataView } from '../containers/sourcerer'; import { useSourcererDataView } from '../containers/sourcerer';
import { useDeepEqualSelector } from '../hooks/use_selector';
import { renderHook } from '@testing-library/react-hooks'; import { renderHook } from '@testing-library/react-hooks';
import { initialGroupingState } from './grouping/reducer'; import { initialGroupingState } from './grouping/reducer';
import { initialAnalyzerState } from '../../resolver/store/helpers'; import { initialAnalyzerState } from '../../resolver/store/helpers';
jest.mock('../hooks/use_selector'); jest.mock('../hooks/use_selector');
jest.mock('../lib/kibana', () => ({ 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: { KibanaServices: {
get: jest.fn(() => ({ uiSettings: { get: () => ({ from: 'now-24h', to: 'now' }) } })), get: jest.fn(() => ({ uiSettings: { get: () => ({ from: 'now-24h', to: 'now' }) } })),
}, },
})); };
});
jest.mock('../containers/source', () => ({ jest.mock('../containers/source', () => ({
useFetchIndex: () => [ useFetchIndex: () => [
false, 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('createInitialState', () => {
describe('sourcerer -> default -> indicesExist', () => { describe('sourcerer -> default -> indicesExist', () => {
const mockPluginState = {} as Omit< const mockPluginState = {} as Omit<
@ -53,15 +70,13 @@ describe('createInitialState', () => {
analyzer: initialAnalyzerState, 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 () => { test('indicesExist should be TRUE if patternList is NOT empty', async () => {
const { result } = renderHook(() => useSourcererDataView()); const { result } = renderHook(() => useSourcererDataView(), {
wrapper: ({ children }) => (
<TestProviders store={createMockStore(initState)}>{children}</TestProviders>
),
});
expect(result.current.indicesExist).toEqual(true); expect(result.current.indicesExist).toEqual(true);
}); });
@ -93,8 +108,11 @@ describe('createInitialState', () => {
analyzer: initialAnalyzerState, analyzer: initialAnalyzerState,
} }
); );
(useDeepEqualSelector as jest.Mock).mockImplementation((cb) => cb(state)); const { result } = renderHook(() => useSourcererDataView(), {
const { result } = renderHook(() => useSourcererDataView()); wrapper: ({ children }) => (
<TestProviders store={createMockStore(state)}>{children}</TestProviders>
),
});
expect(result.current.indicesExist).toEqual(false); expect(result.current.indicesExist).toEqual(false);
}); });
}); });

View file

@ -4,108 +4,93 @@
* 2.0; you may not use this file except in compliance with the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License
* 2.0. * 2.0.
*/ */
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import type { DataViewSpec } from '@kbn/data-views-plugin/common';
import type { State } from '../types'; import type { State } from '../types';
import type { import type { SourcererModel } from './model';
SourcererDataView, import { SourcererScopeName } from './model';
SourcererModel,
SourcererScope,
SourcererScopeName,
} from './model';
export const sourcererKibanaDataViewsSelector = ({
sourcerer,
}: State): SourcererModel['kibanaDataViews'] => sourcerer.kibanaDataViews;
export const sourcererSignalIndexNameSelector = ({ sourcerer }: State): string | null => const SOURCERER_SCOPE_MAX_SIZE = Object.keys(SourcererScopeName).length;
sourcerer.signalIndexName;
export const sourcererDefaultDataViewSelector = ({ const selectSourcerer = (state: State): SourcererModel => state.sourcerer;
sourcerer,
}: State): SourcererModel['defaultDataView'] => sourcerer.defaultDataView;
export const dataViewSelector = ( export const sourcererScopes = createSelector(
{ sourcerer }: State, selectSourcerer,
id: string | null (sourcerer) => sourcerer.sourcererScopes
): SourcererDataView | undefined => );
sourcerer.kibanaDataViews.find((dataView) => dataView.id === id);
export const sourcererScopeIdSelector = ( export const sourcererScope = createSelector(
{ sourcerer }: State, sourcererScopes,
scopeId: SourcererScopeName (state: State, scopeId: SourcererScopeName) => scopeId,
): SourcererScope => sourcerer.sourcererScopes[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 = () => export const sourcererScopeSelectedDataViewId = createSelector(
createSelector(sourcererKibanaDataViewsSelector, (dataViews) => dataViews); sourcererScope,
(scope) => scope.selectedDataViewId,
{
memoizeOptions: {
maxSize: SOURCERER_SCOPE_MAX_SIZE,
},
}
);
export const signalIndexNameSelector = () => export const sourcererScopeSelectedPatterns = createSelector(
createSelector(sourcererSignalIndexNameSelector, (signalIndexName) => signalIndexName); sourcererScope,
(scope) => scope.selectedPatterns,
{
memoizeOptions: {
maxSize: SOURCERER_SCOPE_MAX_SIZE,
},
}
);
export const defaultDataViewSelector = () => export const sourcererScopeMissingPatterns = createSelector(
createSelector(sourcererDefaultDataViewSelector, (dataViews) => dataViews); sourcererScope,
(scope) => scope.missingPatterns,
{
memoizeOptions: {
maxSize: SOURCERER_SCOPE_MAX_SIZE,
},
}
);
export const sourcererDataViewSelector = () => export const kibanaDataViews = createSelector(
createSelector(dataViewSelector, (dataView) => dataView); selectSourcerer,
(sourcerer) => sourcerer.kibanaDataViews,
{
memoizeOptions: {
maxSize: SOURCERER_SCOPE_MAX_SIZE,
},
}
);
export interface SourcererScopeSelector extends Omit<SourcererModel, 'sourcererScopes'> { export const defaultDataView = createSelector(
selectedDataView: SourcererDataView | undefined; selectSourcerer,
sourcererScope: SourcererScope; (sourcerer) => sourcerer.defaultDataView,
} {
memoizeOptions: {
maxSize: SOURCERER_SCOPE_MAX_SIZE,
},
}
);
export const getSourcererDataViewsSelector = () => { export const signalIndexName = createSelector(
const getKibanaDataViewsSelector = kibanaDataViewsSelector(); selectSourcerer,
const getDefaultDataViewSelector = defaultDataViewSelector(); (sourcerer) => sourcerer.signalIndexName,
const getSignalIndexNameSelector = signalIndexNameSelector(); {
return (state: State): Omit<SourcererModel, 'sourcererScopes'> => { memoizeOptions: {
const kibanaDataViews = getKibanaDataViewsSelector(state); maxSize: SOURCERER_SCOPE_MAX_SIZE,
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;
};
};

View file

@ -7,14 +7,11 @@
import { render, waitFor } from '@testing-library/react'; import { render, waitFor } from '@testing-library/react';
import React from 'react'; import React from 'react';
import * as redux from 'react-redux';
import '../../../../common/mock/match_media'; import '../../../../common/mock/match_media';
import { TestProviders } from '../../../../common/mock'; import { TestProviders, mockGlobalState, createMockStore } from '../../../../common/mock';
import { EmbeddedMapComponent } from './embedded_map'; import { EmbeddedMapComponent } from './embedded_map';
import { createEmbeddable } from './create_embeddable'; import { createEmbeddable } from './create_embeddable';
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
import { getLayerList } from './map_config'; import { getLayerList } from './map_config';
import { useIsFieldInIndexPattern } from '../../../containers/fields'; import { useIsFieldInIndexPattern } from '../../../containers/fields';
import { buildTimeRangeFilter } from '../../../../detections/components/alerts_table/helpers'; import { buildTimeRangeFilter } from '../../../../detections/components/alerts_table/helpers';
@ -55,17 +52,47 @@ jest.mock('@kbn/embeddable-plugin/public', () => ({
EmbeddablePanel: jest.fn().mockReturnValue(<div data-test-subj="EmbeddablePanel" />), EmbeddablePanel: jest.fn().mockReturnValue(<div data-test-subj="EmbeddablePanel" />),
})); }));
const mockUseSourcererDataView = useSourcererDataView as jest.Mock;
const mockCreateEmbeddable = createEmbeddable as jest.Mock; const mockCreateEmbeddable = createEmbeddable as jest.Mock;
const mockUseIsFieldInIndexPattern = useIsFieldInIndexPattern as jest.Mock; const mockUseIsFieldInIndexPattern = useIsFieldInIndexPattern as jest.Mock;
const mockGetStorage = jest.fn(); const mockGetStorage = jest.fn();
const mockSetStorage = jest.fn(); const mockSetStorage = jest.fn();
const setQuery: jest.Mock = jest.fn(); const setQuery: jest.Mock = jest.fn();
const filebeatDataView = { id: '6f1eeb50-023d-11eb-bcb6-6ba0578012a9', title: 'filebeat-*' }; const filebeatDataView = {
const packetbeatDataView = { id: '28995490-023d-11eb-bcb6-6ba0578012a9', title: 'packetbeat-*' }; id: '6f1eeb50-023d-11eb-bcb6-6ba0578012a9',
const mockSelector = { title: 'filebeat-*',
kibanaDataViews: [filebeatDataView, packetbeatDataView], 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 mockUpdateInput = jest.fn();
const embeddableValue = { const embeddableValue = {
destroyed: false, destroyed: false,
@ -106,8 +133,6 @@ describe('EmbeddedMapComponent', () => {
beforeEach(() => { beforeEach(() => {
setQuery.mockClear(); setQuery.mockClear();
mockGetStorage.mockReturnValue(true); mockGetStorage.mockReturnValue(true);
jest.spyOn(redux, 'useSelector').mockReturnValue(mockSelector);
mockUseSourcererDataView.mockReturnValue({ selectedPatterns: ['filebeat-*', 'auditbeat-*'] });
mockCreateEmbeddable.mockResolvedValue(embeddableValue); mockCreateEmbeddable.mockResolvedValue(embeddableValue);
mockUseIsFieldInIndexPattern.mockReturnValue(() => true); mockUseIsFieldInIndexPattern.mockReturnValue(() => true);
@ -121,7 +146,7 @@ describe('EmbeddedMapComponent', () => {
test('renders', async () => { test('renders', async () => {
const { getByTestId } = render( const { getByTestId } = render(
<TestProviders> <TestProviders store={defaultMockStore}>
<EmbeddedMapComponent {...testProps} /> <EmbeddedMapComponent {...testProps} />
</TestProviders> </TestProviders>
); );
@ -132,7 +157,7 @@ describe('EmbeddedMapComponent', () => {
test('calls updateInput with time range filter', async () => { test('calls updateInput with time range filter', async () => {
render( render(
<TestProviders> <TestProviders store={defaultMockStore}>
<EmbeddedMapComponent {...testProps} /> <EmbeddedMapComponent {...testProps} />
</TestProviders> </TestProviders>
); );
@ -146,7 +171,7 @@ describe('EmbeddedMapComponent', () => {
test('renders EmbeddablePanel from embeddable plugin', async () => { test('renders EmbeddablePanel from embeddable plugin', async () => {
const { getByTestId, queryByTestId } = render( const { getByTestId, queryByTestId } = render(
<TestProviders> <TestProviders store={defaultMockStore}>
<EmbeddedMapComponent {...testProps} /> <EmbeddedMapComponent {...testProps} />
</TestProviders> </TestProviders>
); );
@ -159,13 +184,17 @@ describe('EmbeddedMapComponent', () => {
}); });
test('renders IndexPatternsMissingPrompt', async () => { test('renders IndexPatternsMissingPrompt', async () => {
jest.spyOn(redux, 'useSelector').mockReturnValue({ const state = {
...mockSelector, ...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
kibanaDataViews: [], kibanaDataViews: [],
}); },
};
const store = createMockStore(state);
const { getByTestId, queryByTestId } = render( const { getByTestId, queryByTestId } = render(
<TestProviders> <TestProviders store={store}>
<EmbeddedMapComponent {...testProps} /> <EmbeddedMapComponent {...testProps} />
</TestProviders> </TestProviders>
); );
@ -180,7 +209,7 @@ describe('EmbeddedMapComponent', () => {
mockCreateEmbeddable.mockResolvedValue(null); mockCreateEmbeddable.mockResolvedValue(null);
const { getByTestId, queryByTestId } = render( const { getByTestId, queryByTestId } = render(
<TestProviders> <TestProviders store={defaultMockStore}>
<EmbeddedMapComponent {...testProps} /> <EmbeddedMapComponent {...testProps} />
</TestProviders> </TestProviders>
); );
@ -194,7 +223,7 @@ describe('EmbeddedMapComponent', () => {
test('map hidden on close', async () => { test('map hidden on close', async () => {
mockGetStorage.mockReturnValue(false); mockGetStorage.mockReturnValue(false);
const { getByTestId, queryByTestId } = render( const { getByTestId, queryByTestId } = render(
<TestProviders> <TestProviders store={defaultMockStore}>
<EmbeddedMapComponent {...testProps} /> <EmbeddedMapComponent {...testProps} />
</TestProviders> </TestProviders>
); );
@ -210,7 +239,7 @@ describe('EmbeddedMapComponent', () => {
test('map visible on open', async () => { test('map visible on open', async () => {
const { getByTestId, queryByTestId } = render( const { getByTestId, queryByTestId } = render(
<TestProviders> <TestProviders store={defaultMockStore}>
<EmbeddedMapComponent {...testProps} /> <EmbeddedMapComponent {...testProps} />
</TestProviders> </TestProviders>
); );
@ -225,8 +254,16 @@ describe('EmbeddedMapComponent', () => {
}); });
test('On mount, selects existing Kibana data views that match any selected index pattern', async () => { 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( render(
<TestProviders> <TestProviders store={store}>
<EmbeddedMapComponent {...testProps} /> <EmbeddedMapComponent {...testProps} />
</TestProviders> </TestProviders>
); );
@ -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 () => { test('On rerender with new selected patterns, selects existing Kibana data views that match any selected index pattern', async () => {
mockUseSourcererDataView.mockReturnValue({ const state = {
selectedPatterns: ['filebeat-*', 'auditbeat-*'], ...mockGlobalState,
}); sourcerer: {
...mockGlobalState.sourcerer,
kibanaDataViews: [filebeatDataView],
},
};
const store = createMockStore(state);
const { rerender } = render( const { rerender } = render(
<TestProviders> <TestProviders store={store}>
<EmbeddedMapComponent {...testProps} /> <EmbeddedMapComponent {...testProps} />
</TestProviders> </TestProviders>
); );
@ -249,11 +291,8 @@ describe('EmbeddedMapComponent', () => {
const dataViewArg = (getLayerList as jest.Mock).mock.calls[0][0]; const dataViewArg = (getLayerList as jest.Mock).mock.calls[0][0];
expect(dataViewArg).toEqual([filebeatDataView]); expect(dataViewArg).toEqual([filebeatDataView]);
}); });
mockUseSourcererDataView.mockReturnValue({
selectedPatterns: ['filebeat-*', 'packetbeat-*'],
});
rerender( rerender(
<TestProviders> <TestProviders store={defaultMockStore}>
<EmbeddedMapComponent {...testProps} /> <EmbeddedMapComponent {...testProps} />
</TestProviders> </TestProviders>
); );

View file

@ -9,6 +9,7 @@
import { EuiAccordion, EuiLink, EuiText } from '@elastic/eui'; import { EuiAccordion, EuiLink, EuiText } from '@elastic/eui';
import React, { useCallback, useEffect, useState, useMemo } from 'react'; import React, { useCallback, useEffect, useState, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { createHtmlPortalNode, InPortal } from 'react-reverse-portal'; import { createHtmlPortalNode, InPortal } from 'react-reverse-portal';
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import type { Filter, Query } from '@kbn/es-query'; import type { Filter, Query } from '@kbn/es-query';
@ -32,10 +33,9 @@ import * as i18n from './translations';
import { useKibana } from '../../../../common/lib/kibana'; import { useKibana } from '../../../../common/lib/kibana';
import { getLayerList } from './map_config'; import { getLayerList } from './map_config';
import { sourcererSelectors } from '../../../../common/store/sourcerer'; import { sourcererSelectors } from '../../../../common/store/sourcerer';
import type { State } from '../../../../common/store';
import type { SourcererDataView } from '../../../../common/store/sourcerer/model'; import type { SourcererDataView } from '../../../../common/store/sourcerer/model';
import { SourcererScopeName } 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'; export const NETWORK_MAP_VISIBLE = 'network_map_visbile';
@ -119,12 +119,10 @@ export const EmbeddedMapComponent = ({
const { addError } = useAppToasts(); const { addError } = useAppToasts();
const getDataViewsSelector = useMemo( const kibanaDataViews = useSelector(sourcererSelectors.kibanaDataViews);
() => sourcererSelectors.getSourcererDataViewsSelector(), const selectedPatterns = useSelector((state: State) => {
[] return sourcererSelectors.sourcererScopeSelectedPatterns(state, SourcererScopeName.default);
); });
const { kibanaDataViews } = useDeepEqualSelector((state) => getDataViewsSelector(state));
const { selectedPatterns } = useSourcererDataView(SourcererScopeName.default);
const isFieldInIndexPattern = useIsFieldInIndexPattern(); const isFieldInIndexPattern = useIsFieldInIndexPattern();
@ -250,7 +248,6 @@ export const EmbeddedMapComponent = ({
() => buildTimeRangeFilter(startDate, endDate), () => buildTimeRangeFilter(startDate, endDate),
[startDate, endDate] [startDate, endDate]
); );
useEffect(() => { useEffect(() => {
if (embeddable != null) { if (embeddable != null) {
// pass time range as filter instead of via timeRange param // pass time range as filter instead of via timeRange param

View file

@ -5,11 +5,9 @@
* 2.0. * 2.0.
*/ */
import { useCallback, useMemo } from 'react'; import { useCallback } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
import { sourcererActions } from '../../../../common/store/sourcerer'; import { sourcererActions } from '../../../../common/store/sourcerer';
import { import {
@ -33,13 +31,8 @@ export interface Filter {
export const useNavigateToTimeline = () => { export const useNavigateToTimeline = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const getDataViewsSelector = useMemo( const signalIndexName = useSelector(sourcererSelectors.signalIndexName);
() => sourcererSelectors.getSourcererDataViewsSelector(), const defaultDataView = useSelector(sourcererSelectors.defaultDataView);
[]
);
const { defaultDataView, signalIndexName } = useDeepEqualSelector((state) =>
getDataViewsSelector(state)
);
const clearTimeline = useCreateTimeline({ const clearTimeline = useCreateTimeline({
timelineId: TimelineId.active, timelineId: TimelineId.active,

View file

@ -109,6 +109,16 @@ export class Simulator {
this.store = createMockStore( this.store = createMockStore(
{ {
...mockGlobalState, ...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
sourcererScopes: {
...mockGlobalState.sourcerer.sourcererScopes,
analyzer: {
...mockGlobalState.sourcerer.sourcererScopes.default,
selectedPatterns: indices,
},
},
},
analyzer: { analyzer: {
[resolverComponentInstanceID]: EMPTY_RESOLVER, [resolverComponentInstanceID]: EMPTY_RESOLVER,
}, },

View file

@ -17,6 +17,7 @@ import type { SideEffectSimulator, ResolverProps } from '../../types';
import { ResolverWithoutProviders } from '../../view/resolver_without_providers'; import { ResolverWithoutProviders } from '../../view/resolver_without_providers';
import { SideEffectContext } from '../../view/side_effect_context'; import { SideEffectContext } from '../../view/side_effect_context';
import type { State } from '../../../common/store/types'; import type { State } from '../../../common/store/types';
import { TestProviders } from '../../../common/mock';
enableMapSet(); enableMapSet();
@ -93,6 +94,7 @@ export const MockResolver = React.memo((props: MockResolverProps) => {
}, [props.rasterWidth, props.rasterHeight, props.sideEffectSimulator.controls, resolverElement]); }, [props.rasterWidth, props.rasterHeight, props.sideEffectSimulator.controls, resolverElement]);
return ( return (
<TestProviders>
<I18nProvider> <I18nProvider>
<Router history={props.history}> <Router history={props.history}>
<KibanaContextProvider services={props.coreStart}> <KibanaContextProvider services={props.coreStart}>
@ -111,5 +113,6 @@ export const MockResolver = React.memo((props: MockResolverProps) => {
</KibanaContextProvider> </KibanaContextProvider>
</Router> </Router>
</I18nProvider> </I18nProvider>
</TestProviders>
); );
}); });

View file

@ -20,11 +20,15 @@ jest.mock('react-redux', () => {
}; };
}); });
const mockRef = {
current: null,
};
describe('TimelineBottomBar', () => { describe('TimelineBottomBar', () => {
test('should render all components for bottom bar', () => { test('should render all components for bottom bar', () => {
const { getByTestId } = render( const { getByTestId } = render(
<TestProviders> <TestProviders>
<TimelineBottomBar show={false} timelineId={TimelineId.test} /> <TimelineBottomBar show={false} timelineId={TimelineId.test} openToggleRef={mockRef} />
</TestProviders> </TestProviders>
); );
@ -38,7 +42,7 @@ describe('TimelineBottomBar', () => {
test('should not render the event count badge if timeline is open', () => { test('should not render the event count badge if timeline is open', () => {
const { queryByTestId } = render( const { queryByTestId } = render(
<TestProviders> <TestProviders>
<TimelineBottomBar show={true} timelineId={TimelineId.test} /> <TimelineBottomBar show={true} timelineId={TimelineId.test} openToggleRef={mockRef} />
</TestProviders> </TestProviders>
); );
@ -50,7 +54,7 @@ describe('TimelineBottomBar', () => {
const { getByTestId } = render( const { getByTestId } = render(
<TestProviders> <TestProviders>
<TimelineBottomBar show={true} timelineId={TimelineId.test} /> <TimelineBottomBar show={true} timelineId={TimelineId.test} openToggleRef={mockRef} />
</TestProviders> </TestProviders>
); );

View file

@ -26,12 +26,15 @@ interface TimelineBottomBarProps {
* True if the timeline modal is open * True if the timeline modal is open
*/ */
show: boolean; show: boolean;
openToggleRef: React.MutableRefObject<null | HTMLAnchorElement | HTMLButtonElement>;
} }
/** /**
* This component renders the bottom bar for timeline displayed or most of the pages within Security Solution. * This component renders the bottom bar for timeline displayed or most of the pages within Security Solution.
*/ */
export const TimelineBottomBar = React.memo<TimelineBottomBarProps>(({ show, timelineId }) => { export const TimelineBottomBar = React.memo<TimelineBottomBarProps>(
({ show, timelineId, openToggleRef }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const openTimeline = useCallback( const openTimeline = useCallback(
@ -55,6 +58,7 @@ export const TimelineBottomBar = React.memo<TimelineBottomBarProps>(({ show, tim
aria-label={i18n.OPEN_TIMELINE_BUTTON(title)} aria-label={i18n.OPEN_TIMELINE_BUTTON(title)}
onClick={openTimeline} onClick={openTimeline}
data-test-subj="timeline-bottom-bar-title-button" data-test-subj="timeline-bottom-bar-title-button"
ref={openToggleRef}
> >
{title} {title}
</EuiLink> </EuiLink>
@ -70,6 +74,7 @@ export const TimelineBottomBar = React.memo<TimelineBottomBarProps>(({ show, tim
</EuiFlexGroup> </EuiFlexGroup>
</EuiPanel> </EuiPanel>
); );
}); }
);
TimelineBottomBar.displayName = 'TimelineBottomBar'; TimelineBottomBar.displayName = 'TimelineBottomBar';

View file

@ -7,6 +7,7 @@
import React from 'react'; import React from 'react';
import { render, act } from '@testing-library/react'; import { render, act } from '@testing-library/react';
import type { Store } from 'redux';
import type { UseFieldBrowserOptionsProps, UseFieldBrowserOptions, FieldEditorActionsRef } from '.'; import type { UseFieldBrowserOptionsProps, UseFieldBrowserOptions, FieldEditorActionsRef } from '.';
import { useFieldBrowserOptions } from '.'; import { useFieldBrowserOptions } from '.';
import type { Start } from '@kbn/data-view-field-editor-plugin/public/mocks'; import type { Start } from '@kbn/data-view-field-editor-plugin/public/mocks';
@ -27,22 +28,6 @@ let mockIndexPatternFieldEditor: Start;
jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/lib/kibana');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>; const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
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(); const mockIndexFieldsSearch = jest.fn();
jest.mock('../../../common/containers/source/use_data_view', () => ({ jest.mock('../../../common/containers/source/use_data_view', () => ({
useDataView: () => ({ useDataView: () => ({
@ -57,8 +42,10 @@ const mockOnHide = jest.fn();
const runAllPromises = () => new Promise(setImmediate); const runAllPromises = () => new Promise(setImmediate);
// helper function to render the hook // helper function to render the hook
const renderUseFieldBrowserOptions = (props: Partial<UseFieldBrowserOptionsProps> = {}) => const renderUseFieldBrowserOptions = (
renderHook<UseFieldBrowserOptionsProps, ReturnType<UseFieldBrowserOptions>>( props: Partial<UseFieldBrowserOptionsProps & { store?: Store }> = {}
) =>
renderHook<UseFieldBrowserOptionsProps & { store?: Store }, ReturnType<UseFieldBrowserOptions>>(
() => () =>
useFieldBrowserOptions({ useFieldBrowserOptions({
sourcererScope: SourcererScopeName.default, sourcererScope: SourcererScopeName.default,
@ -67,7 +54,12 @@ const renderUseFieldBrowserOptions = (props: Partial<UseFieldBrowserOptionsProps
...props, ...props,
}), }),
{ {
wrapper: TestProviders, wrapper: ({ children, store }) => {
if (store) {
return <TestProviders store={store}>{children}</TestProviders>;
}
return <TestProviders>{children}</TestProviders>;
},
} }
); );
@ -104,7 +96,6 @@ describe('useFieldBrowserOptions', () => {
...useKibanaMock().services.application.capabilities, ...useKibanaMock().services.application.capabilities,
indexPatterns: { save: true }, indexPatterns: { save: true },
}; };
mockScopeIdSelector.mockReturnValue(defaultDataviewState);
jest.clearAllMocks(); 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 () => { it('should call onHide when button is pressed', async () => {
useKibanaMock().services.data.dataViews.get = () => Promise.resolve({} as DataView); useKibanaMock().services.data.dataViews.get = () => Promise.resolve({} as DataView);
const { result } = await renderUpdatedUseFieldBrowserOptions(); const { result } = await renderUpdatedUseFieldBrowserOptions();

View file

@ -7,6 +7,7 @@
import type { MutableRefObject } from 'react'; import type { MutableRefObject } from 'react';
import { useCallback, useEffect, useMemo, useState } 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 { DataViewField, DataView } from '@kbn/data-views-plugin/common';
import type { import type {
CreateFieldComponent, CreateFieldComponent,
@ -14,9 +15,9 @@ import type {
} from '@kbn/triggers-actions-ui-plugin/public/types'; } from '@kbn/triggers-actions-ui-plugin/public/types';
import type { ColumnHeaderOptions } from '../../../../common/types'; import type { ColumnHeaderOptions } from '../../../../common/types';
import { useDataView } from '../../../common/containers/source/use_data_view'; import { useDataView } from '../../../common/containers/source/use_data_view';
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
import { useKibana } from '../../../common/lib/kibana'; import { useKibana } from '../../../common/lib/kibana';
import { sourcererSelectors } from '../../../common/store'; import { sourcererSelectors } from '../../../common/store';
import type { State } from '../../../common/store';
import type { SourcererScopeName } from '../../../common/store/sourcerer/model'; import type { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers';
import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants';
@ -57,11 +58,12 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({
dataViewFieldEditor, dataViewFieldEditor,
data: { dataViews }, data: { dataViews },
} = useKibana().services; } = useKibana().services;
const missingPatterns = useSelector((state: State) => {
const scopeIdSelector = useMemo(() => sourcererSelectors.scopeIdSelector(), []); return sourcererSelectors.sourcererScopeMissingPatterns(state, sourcererScope);
const { missingPatterns, selectedDataViewId } = useDeepEqualSelector((state) => });
scopeIdSelector(state, sourcererScope) const selectedDataViewId = useSelector((state: State) => {
); return sourcererSelectors.sourcererScopeSelectedDataViewId(state, sourcererScope);
});
useEffect(() => { useEffect(() => {
let ignore = false; let ignore = false;
const fetchAndSetDataView = async (dataViewId: string) => { const fetchAndSetDataView = async (dataViewId: string) => {

View file

@ -10,9 +10,9 @@ import React from 'react';
import { NewTimelineButton } from './new_timeline_button'; import { NewTimelineButton } from './new_timeline_button';
import { TimelineId } from '../../../../../common/types'; import { TimelineId } from '../../../../../common/types';
import { timelineActions } from '../../../store'; 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 { useDiscoverInTimelineContext } from '../../../../common/components/discover_in_timeline/use_discover_in_timeline_context';
import { defaultHeaders } from '../../timeline/body/column_headers/default_headers'; 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/components/discover_in_timeline/use_discover_in_timeline_context');
jest.mock('../../../../common/hooks/use_selector'); jest.mock('../../../../common/hooks/use_selector');
@ -26,11 +26,11 @@ jest.mock('react-redux', () => {
}; };
}); });
const renderNewTimelineButton = () => render(<NewTimelineButton timelineId={TimelineId.test} />); const renderNewTimelineButton = () =>
render(<NewTimelineButton timelineId={TimelineId.test} />, { wrapper: TestProviders });
describe('NewTimelineButton', () => { describe('NewTimelineButton', () => {
it('should render 2 options in the popover when clicking on the button', async () => { it('should render 2 options in the popover when clicking on the button', async () => {
(useDeepEqualSelector as jest.Mock).mockReturnValue({});
(useDiscoverInTimelineContext as jest.Mock).mockReturnValue({}); (useDiscoverInTimelineContext as jest.Mock).mockReturnValue({});
const { getByTestId, getByText } = renderNewTimelineButton(); const { getByTestId, getByText } = renderNewTimelineButton();
@ -52,12 +52,8 @@ describe('NewTimelineButton', () => {
}); });
it('should call the correct action with clicking on the new timeline button', () => { it('should call the correct action with clicking on the new timeline button', () => {
const dataViewId = 'dataViewId'; const dataViewId = '';
const selectedPatterns = ['selectedPatterns']; const selectedPatterns: string[] = [];
(useDeepEqualSelector as jest.Mock).mockReturnValue({
id: dataViewId,
patternList: selectedPatterns,
});
(useDiscoverInTimelineContext as jest.Mock).mockReturnValue({ (useDiscoverInTimelineContext as jest.Mock).mockReturnValue({
resetDiscoverAppState: jest.fn(), resetDiscoverAppState: jest.fn(),
}); });

View file

@ -41,10 +41,13 @@ jest.mock('react-redux', () => {
}); });
const timelineId = 'timeline-1'; const timelineId = 'timeline-1';
const mockRef = {
current: null,
};
const renderTimelineModalHeader = () => const renderTimelineModalHeader = () =>
render( render(
<TestProviders> <TestProviders>
<TimelineModalHeader timelineId={timelineId} /> <TimelineModalHeader timelineId={timelineId} openToggleRef={mockRef} />
</TestProviders> </TestProviders>
); );

View file

@ -62,12 +62,14 @@ interface FlyoutHeaderPanelProps {
* Id of the timeline to be displayed within the modal * Id of the timeline to be displayed within the modal
*/ */
timelineId: string; timelineId: string;
openToggleRef: React.MutableRefObject<null | HTMLAnchorElement | HTMLButtonElement>;
} }
/** /**
* 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 * 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<FlyoutHeaderPanelProps>(({ timelineId }) => { export const TimelineModalHeader = React.memo<FlyoutHeaderPanelProps>(
({ timelineId, openToggleRef }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { browserFields, indexPattern } = useSourcererDataView(SourcererScopeName.timeline); const { browserFields, indexPattern } = useSourcererDataView(SourcererScopeName.timeline);
const { cases, uiSettings } = useKibana().services; const { cases, uiSettings } = useKibana().services;
@ -78,8 +80,8 @@ export const TimelineModalHeader = React.memo<FlyoutHeaderPanelProps>(({ timelin
const isDataInTimeline = useSelector((state: State) => selectDataInTimeline(state, timelineId)); const isDataInTimeline = useSelector((state: State) => selectDataInTimeline(state, timelineId));
const kqlQueryObj = useSelector((state: State) => selectKqlQuery(state, timelineId)); const kqlQueryObj = useSelector((state: State) => selectKqlQuery(state, timelineId));
const { activeTab, dataProviders, timelineType, filters, kqlMode } = useSelector((state: State) => const { activeTab, dataProviders, timelineType, filters, kqlMode } = useSelector(
selectTimelineById(state, timelineId) (state: State) => selectTimelineById(state, timelineId)
); );
const combinedQueries = useMemo( const combinedQueries = useMemo(
@ -98,9 +100,12 @@ export const TimelineModalHeader = React.memo<FlyoutHeaderPanelProps>(({ timelin
const isInspectDisabled = !isDataInTimeline || combinedQueries?.filterQuery === undefined; const isInspectDisabled = !isDataInTimeline || combinedQueries?.filterQuery === undefined;
const closeTimeline = useCallback(() => { const closeTimeline = useCallback(() => {
if (openToggleRef.current != null) {
openToggleRef.current.focus();
}
createHistoryEntry(); createHistoryEntry();
dispatch(timelineActions.showTimeline({ id: timelineId, show: false })); dispatch(timelineActions.showTimeline({ id: timelineId, show: false }));
}, [dispatch, timelineId]); }, [dispatch, timelineId, openToggleRef]);
return ( return (
<TimelinePanel <TimelinePanel
@ -186,6 +191,7 @@ export const TimelineModalHeader = React.memo<FlyoutHeaderPanelProps>(({ timelin
</EuiFlexGroup> </EuiFlexGroup>
</TimelinePanel> </TimelinePanel>
); );
}); }
);
TimelineModalHeader.displayName = 'TimelineModalHeader'; TimelineModalHeader.displayName = 'TimelineModalHeader';

View file

@ -21,10 +21,14 @@ jest.mock('../../../common/store/selectors', () => ({
inputsSelectors: { timelineFullScreenSelector: () => mockIsFullScreen() }, inputsSelectors: { timelineFullScreenSelector: () => mockIsFullScreen() },
})); }));
const mockRef = {
current: null,
};
const renderTimelineModal = () => const renderTimelineModal = () =>
render( render(
<TestProviders> <TestProviders>
<TimelineModal timelineId={TimelineId.test} /> <TimelineModal timelineId={TimelineId.test} openToggleRef={mockRef} />
</TestProviders> </TestProviders>
); );

View file

@ -33,14 +33,17 @@ interface TimelineModalProps {
* If true the timeline modal will be visible * If true the timeline modal will be visible
*/ */
visible?: boolean; visible?: boolean;
openToggleRef: React.MutableRefObject<null | HTMLAnchorElement | HTMLButtonElement>;
} }
/** /**
* Renders the timeline modal. Internally this is using an EuiPortal. * Renders the timeline modal. Internally this is using an EuiPortal.
*/ */
export const TimelineModal = React.memo<TimelineModalProps>(({ timelineId, visible = true }) => { export const TimelineModal = React.memo<TimelineModalProps>(
({ timelineId, openToggleRef, visible = true }) => {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const isFullScreen = useShallowEqualSelector(inputsSelectors.timelineFullScreenSelector) ?? false; const isFullScreen =
useShallowEqualSelector(inputsSelectors.timelineFullScreenSelector) ?? false;
const styles = usePaneStyles(); const styles = usePaneStyles();
const wrapperClassName = classNames('timeline-portal-overlay-mask', styles, { const wrapperClassName = classNames('timeline-portal-overlay-mask', styles, {
@ -48,7 +51,10 @@ export const TimelineModal = React.memo<TimelineModalProps>(({ timelineId, visib
'timeline-portal-overlay-mask--hidden': !visible, '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 (
<div data-test-subj="timeline-portal-ref" ref={ref}> <div data-test-subj="timeline-portal-ref" ref={ref}>
@ -63,6 +69,7 @@ export const TimelineModal = React.memo<TimelineModalProps>(({ timelineId, visib
renderCellValue={DefaultCellRenderer} renderCellValue={DefaultCellRenderer}
rowRenderers={defaultRowRenderers} rowRenderers={defaultRowRenderers}
timelineId={timelineId} timelineId={timelineId}
openToggleRef={openToggleRef}
/> />
</div> </div>
</div> </div>
@ -70,6 +77,7 @@ export const TimelineModal = React.memo<TimelineModalProps>(({ timelineId, visib
{visible && <OverflowHiddenGlobalStyles />} {visible && <OverflowHiddenGlobalStyles />}
</div> </div>
); );
}); }
);
TimelineModal.displayName = 'TimelineModal'; TimelineModal.displayName = 'TimelineModal';

View file

@ -10,10 +10,10 @@ import React from 'react';
import { NewTimelineButton } from '.'; import { NewTimelineButton } from '.';
import { TimelineId } from '../../../../common/types'; import { TimelineId } from '../../../../common/types';
import { timelineActions } from '../../store'; 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 { useDiscoverInTimelineContext } from '../../../common/components/discover_in_timeline/use_discover_in_timeline_context';
import { defaultHeaders } from '../timeline/body/column_headers/default_headers'; import { defaultHeaders } from '../timeline/body/column_headers/default_headers';
import { TimelineType } from '../../../../common/api/timeline'; 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/components/discover_in_timeline/use_discover_in_timeline_context');
jest.mock('../../../common/hooks/use_selector'); jest.mock('../../../common/hooks/use_selector');
@ -27,15 +27,12 @@ jest.mock('react-redux', () => {
}; };
}); });
const renderNewTimelineButton = (type: TimelineType) => render(<NewTimelineButton type={type} />); const renderNewTimelineButton = (type: TimelineType) =>
render(<NewTimelineButton type={type} />, { wrapper: TestProviders });
describe('NewTimelineButton', () => { describe('NewTimelineButton', () => {
const dataViewId = 'dataViewId'; const dataViewId = '';
const selectedPatterns = ['selectedPatterns']; const selectedPatterns: string[] = [];
(useDeepEqualSelector as jest.Mock).mockReturnValue({
id: dataViewId,
patternList: selectedPatterns,
});
(useDiscoverInTimelineContext as jest.Mock).mockReturnValue({ (useDiscoverInTimelineContext as jest.Mock).mockReturnValue({
resetDiscoverAppState: jest.fn(), resetDiscoverAppState: jest.fn(),
}); });

View file

@ -70,6 +70,9 @@ jest.mock('react-router-dom', () => {
}); });
const mockDispatch = jest.fn(); const mockDispatch = jest.fn();
const mockRef = {
current: null,
};
jest.mock('react-redux', () => { jest.mock('react-redux', () => {
const actual = jest.requireActual('react-redux'); const actual = jest.requireActual('react-redux');
@ -95,6 +98,7 @@ describe('StatefulTimeline', () => {
renderCellValue: DefaultCellRenderer, renderCellValue: DefaultCellRenderer,
rowRenderers: defaultRowRenderers, rowRenderers: defaultRowRenderers,
timelineId: TimelineId.test, timelineId: TimelineId.test,
openToggleRef: mockRef,
}; };
beforeEach(() => { beforeEach(() => {

View file

@ -8,7 +8,7 @@
import { pick } from 'lodash/fp'; import { pick } from 'lodash/fp';
import { EuiProgress } from '@elastic/eui'; import { EuiProgress } from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useRef, createContext } from 'react'; 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 styled from 'styled-components';
import { isTab } from '@kbn/timelines-plugin/public'; 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 type { TimelineId, RowRenderer, TimelineTabs } from '../../../../common/types/timeline';
import { TimelineType } from '../../../../common/api/timeline'; import { TimelineType } from '../../../../common/api/timeline';
import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector';
import type { State } from '../../../common/store';
import { EVENTS_COUNT_BUTTON_CLASS_NAME, onTimelineTabKeyPressed } from './helpers'; import { EVENTS_COUNT_BUTTON_CLASS_NAME, onTimelineTabKeyPressed } from './helpers';
import * as i18n from './translations'; import * as i18n from './translations';
import { TabsContent } from './tabs_content'; import { TabsContent } from './tabs_content';
@ -50,6 +51,7 @@ export interface Props {
renderCellValue: (props: CellValueElementProps) => React.ReactNode; renderCellValue: (props: CellValueElementProps) => React.ReactNode;
rowRenderers: RowRenderer[]; rowRenderers: RowRenderer[];
timelineId: TimelineId; timelineId: TimelineId;
openToggleRef: React.MutableRefObject<null | HTMLAnchorElement | HTMLButtonElement>;
} }
const TimelineSavingProgressComponent: React.FC<{ timelineId: TimelineId }> = ({ timelineId }) => { const TimelineSavingProgressComponent: React.FC<{ timelineId: TimelineId }> = ({ timelineId }) => {
@ -67,15 +69,17 @@ const StatefulTimelineComponent: React.FC<Props> = ({
renderCellValue, renderCellValue,
rowRenderers, rowRenderers,
timelineId, timelineId,
openToggleRef,
}) => { }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const containerElement = useRef<HTMLDivElement | null>(null); const containerElement = useRef<HTMLDivElement | null>(null);
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const scopeIdSelector = useMemo(() => sourcererSelectors.scopeIdSelector(), []); const selectedPatternsSourcerer = useSelector((state: State) => {
const { return sourcererSelectors.sourcererScopeSelectedPatterns(state, SourcererScopeName.timeline);
selectedPatterns: selectedPatternsSourcerer, });
selectedDataViewId: selectedDataViewIdSourcerer, const selectedDataViewIdSourcerer = useSelector((state: State) => {
} = useDeepEqualSelector((state) => scopeIdSelector(state, SourcererScopeName.timeline)); return sourcererSelectors.sourcererScopeSelectedDataViewId(state, SourcererScopeName.timeline);
});
const { const {
dataViewId: selectedDataViewIdTimeline, dataViewId: selectedDataViewIdTimeline,
indexNames: selectedPatternsTimeline, indexNames: selectedPatternsTimeline,
@ -233,7 +237,7 @@ const StatefulTimelineComponent: React.FC<Props> = ({
$isVisible={!timelineFullScreen} $isVisible={!timelineFullScreen}
data-test-subj="timeline-hide-show-container" data-test-subj="timeline-hide-show-container"
> >
<TimelineModalHeader timelineId={timelineId} /> <TimelineModalHeader timelineId={timelineId} openToggleRef={openToggleRef} />
</HideShowContainer> </HideShowContainer>
<TabsContent <TabsContent

View file

@ -6,6 +6,7 @@
*/ */
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { useDeepEqualSelector } from '../../common/hooks/use_selector'; import { useDeepEqualSelector } from '../../common/hooks/use_selector';
import { import {
@ -47,11 +48,7 @@ export function useTimelineDataFilters(isActiveTimelines: boolean) {
return getEndSelector(state.inputs.global); return getEndSelector(state.inputs.global);
} }
}); });
const getDefaultDataViewSelector = useMemo( const defaultDataView = useSelector(sourcererSelectors.defaultDataView);
() => sourcererSelectors.defaultDataViewSelector(),
[]
);
const defaultDataView = useDeepEqualSelector(getDefaultDataViewSelector);
const { pathname } = useLocation(); const { pathname } = useLocation();
const { selectedPatterns: nonTimelinePatterns } = useSourcererDataView( const { selectedPatterns: nonTimelinePatterns } = useSourcererDataView(
getScopeFromPath(pathname) getScopeFromPath(pathname)

View file

@ -4,15 +4,12 @@
* 2.0; you may not use this file except in compliance with the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License
* 2.0. * 2.0.
*/ */
import React from 'react';
import type { RenderHookResult } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react-hooks'; import { renderHook } from '@testing-library/react-hooks';
import type { UseCreateTimelineParams } from './use_create_timeline';
import { useCreateTimeline } from './use_create_timeline'; import { useCreateTimeline } from './use_create_timeline';
import type { TimeRange } from '../../common/store/inputs/model'; import type { TimeRange } from '../../common/store/inputs/model';
import { TimelineType } from '../../../common/api/timeline'; import { TimelineType } from '../../../common/api/timeline';
import { TimelineId } from '../../../common/types'; 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 { useDiscoverInTimelineContext } from '../../common/components/discover_in_timeline/use_discover_in_timeline_context';
import { timelineActions } from '../store'; import { timelineActions } from '../store';
import { inputsActions } from '../../common/store/inputs'; 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 { defaultHeaders } from '../components/timeline/body/column_headers/default_headers';
import { SourcererScopeName } from '../../common/store/sourcerer/model'; import { SourcererScopeName } from '../../common/store/sourcerer/model';
import { InputsModelId } from '../../common/store/inputs/constants'; 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/components/discover_in_timeline/use_discover_in_timeline_context');
jest.mock('../../common/hooks/use_selector'); jest.mock('../../common/containers/use_global_time', () => {
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
return { return {
...original, useGlobalTime: jest.fn().mockReturnValue({
useSelector: jest.fn(), from: '2022-04-05T12:00:00.000Z',
useDispatch: () => jest.fn(), to: '2022-04-08T12:00:00.000Z',
setQuery: () => jest.fn(),
deleteQuery: () => jest.fn(),
}),
}; };
}); });
jest.mock('../../common/lib/kibana');
describe('useCreateTimeline', () => { describe('useCreateTimeline', () => {
let hookResult: RenderHookResult<
UseCreateTimelineParams,
(options?: { timeRange?: TimeRange }) => void
>;
const resetDiscoverAppState = jest.fn(); const resetDiscoverAppState = jest.fn();
(useDiscoverInTimelineContext as jest.Mock).mockReturnValue({ resetDiscoverAppState }); (useDiscoverInTimelineContext as jest.Mock).mockReturnValue({ resetDiscoverAppState });
it('should return a function', () => { it('should return a function', () => {
(useDeepEqualSelector as jest.Mock).mockReturnValue({}); const hookResult = renderHook(
() => useCreateTimeline({ timelineId: TimelineId.test, timelineType: TimelineType.default }),
hookResult = renderHook(() => {
useCreateTimeline({ timelineId: TimelineId.test, timelineType: TimelineType.default }) wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
}
); );
expect(hookResult.result.current).toEqual(expect.any(Function)); expect(hookResult.result.current).toEqual(expect.any(Function));
}); });
it('should dispatch correct actions when calling the returned 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 createTimeline = jest.spyOn(timelineActions, 'createTimeline');
const setSelectedDataView = jest.spyOn(sourcererActions, 'setSelectedDataView'); const setSelectedDataView = jest.spyOn(sourcererActions, 'setSelectedDataView');
const addLinkTo = jest.spyOn(inputsActions, 'addLinkTo'); const addLinkTo = jest.spyOn(inputsActions, 'addLinkTo');
const addNotes = jest.spyOn(appActions, 'addNotes'); const addNotes = jest.spyOn(appActions, 'addNotes');
hookResult = renderHook(() => const hookResult = renderHook(
useCreateTimeline({ timelineId: TimelineId.test, timelineType: TimelineType.default }) () => useCreateTimeline({ timelineId: TimelineId.test, timelineType: TimelineType.default }),
{
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
}
); );
expect(hookResult.result.current).toEqual(expect.any(Function)); expect(hookResult.result.current).toEqual(expect.any(Function));
hookResult.result.current(); hookResult.result.current();
expect(createTimeline.mock.calls[0][0].id).toEqual(TimelineId.test);
expect(createTimeline).toHaveBeenCalledWith({ expect(createTimeline.mock.calls[0][0].timelineType).toEqual(TimelineType.default);
columns: defaultHeaders, expect(createTimeline.mock.calls[0][0].columns).toEqual(defaultHeaders);
dataViewId, expect(createTimeline.mock.calls[0][0].dataViewId).toEqual(
id: TimelineId.test, mockGlobalState.sourcerer.defaultDataView.id
indexNames: selectedPatterns, );
show: true, expect(createTimeline.mock.calls[0][0].indexNames).toEqual(
timelineType: 'default', expect.arrayContaining(
updated: undefined, mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline].selectedPatterns
}); )
expect(setSelectedDataView).toHaveBeenCalledWith({ );
id: SourcererScopeName.timeline, expect(createTimeline.mock.calls[0][0].show).toEqual(true);
selectedDataViewId: dataViewId, expect(createTimeline.mock.calls[0][0].updated).toEqual(undefined);
selectedPatterns, 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(addLinkTo).toHaveBeenCalledWith([InputsModelId.global, InputsModelId.timeline]);
expect(addNotes).toHaveBeenCalledWith({ notes: [] }); expect(addNotes).toHaveBeenCalledWith({ notes: [] });
}); });
it('should run the onClick method if provided', () => { it('should run the onClick method if provided', () => {
(useDeepEqualSelector as jest.Mock).mockReturnValue({});
const onClick = jest.fn(); const onClick = jest.fn();
hookResult = renderHook(() => const hookResult = renderHook(
() =>
useCreateTimeline({ useCreateTimeline({
timelineId: TimelineId.test, timelineId: TimelineId.test,
timelineType: TimelineType.default, timelineType: TimelineType.default,
onClick, onClick,
}) }),
{
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
}
); );
hookResult.result.current(); hookResult.result.current();
@ -111,13 +111,14 @@ describe('useCreateTimeline', () => {
}); });
it('should dispatch removeLinkTo action if absolute timeRange is passed to callback', () => { it('should dispatch removeLinkTo action if absolute timeRange is passed to callback', () => {
(useDeepEqualSelector as jest.Mock).mockReturnValue({});
const removeLinkTo = jest.spyOn(inputsActions, 'removeLinkTo'); const removeLinkTo = jest.spyOn(inputsActions, 'removeLinkTo');
const setAbsoluteRangeDatePicker = jest.spyOn(inputsActions, 'setAbsoluteRangeDatePicker'); const setAbsoluteRangeDatePicker = jest.spyOn(inputsActions, 'setAbsoluteRangeDatePicker');
hookResult = renderHook(() => const hookResult = renderHook(
useCreateTimeline({ timelineId: TimelineId.test, timelineType: TimelineType.default }) () => useCreateTimeline({ timelineId: TimelineId.test, timelineType: TimelineType.default }),
{
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
}
); );
const timeRange: TimeRange = { kind: 'absolute', from: '', to: '' }; 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', () => { it('should dispatch removeLinkTo action if relative timeRange is passed to callback', () => {
(useDeepEqualSelector as jest.Mock).mockReturnValue({});
const setRelativeRangeDatePicker = jest.spyOn(inputsActions, 'setRelativeRangeDatePicker'); const setRelativeRangeDatePicker = jest.spyOn(inputsActions, 'setRelativeRangeDatePicker');
hookResult = renderHook(() => const hookResult = renderHook(
useCreateTimeline({ timelineId: TimelineId.test, timelineType: TimelineType.default }) () => useCreateTimeline({ timelineId: TimelineId.test, timelineType: TimelineType.default }),
{
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
}
); );
const timeRange: TimeRange = { kind: 'relative', fromStr: '', toStr: '', from: '', to: '' }; const timeRange: TimeRange = { kind: 'relative', fromStr: '', toStr: '', from: '', to: '' };

View file

@ -5,8 +5,8 @@
* 2.0. * 2.0.
*/ */
import { useCallback, useMemo } from 'react'; import { useCallback } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { InputsModelId } from '../../common/store/inputs/constants'; import { InputsModelId } from '../../common/store/inputs/constants';
import { defaultHeaders } from '../components/timeline/body/column_headers/default_headers'; import { defaultHeaders } from '../components/timeline/body/column_headers/default_headers';
import { timelineActions } from '../store'; import { timelineActions } from '../store';
@ -47,9 +47,9 @@ export const useCreateTimeline = ({
onClick, onClick,
}: UseCreateTimelineParams): ((options?: { timeRange?: TimeRange }) => void) => { }: UseCreateTimelineParams): ((options?: { timeRange?: TimeRange }) => void) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const defaultDataViewSelector = useMemo(() => sourcererSelectors.defaultDataViewSelector(), []); const { id: dataViewId, patternList: selectedPatterns } = useSelector(
const { id: dataViewId, patternList: selectedPatterns } = sourcererSelectors.defaultDataView
useDeepEqualSelector(defaultDataViewSelector); ) ?? { id: '', patternList: [] };
const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen();
const globalTimeRange = useDeepEqualSelector(inputsSelectors.globalTimeRangeSelector); const globalTimeRange = useDeepEqualSelector(inputsSelectors.globalTimeRangeSelector);

View file

@ -6,7 +6,7 @@
*/ */
import { EuiFocusTrap, EuiWindowEvent, keys } from '@elastic/eui'; 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 type { AppLeaveHandler } from '@kbn/core/public';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { TimelineModal } from '../components/modal'; import { TimelineModal } from '../components/modal';
@ -38,7 +38,7 @@ export const TimelineWrapper: React.FC<TimelineWrapperProps> = React.memo(
const getTimelineShowStatus = useMemo(() => getTimelineShowStatusByIdSelector(), []); const getTimelineShowStatus = useMemo(() => getTimelineShowStatusByIdSelector(), []);
const { show } = useDeepEqualSelector((state) => getTimelineShowStatus(state, timelineId)); const { show } = useDeepEqualSelector((state) => getTimelineShowStatus(state, timelineId));
const dispatch = useDispatch(); const dispatch = useDispatch();
const openToggleRef = useRef(null);
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
dispatch(timelineActions.showTimeline({ id: timelineId, show: false })); dispatch(timelineActions.showTimeline({ id: timelineId, show: false }));
}, [dispatch, timelineId]); }, [dispatch, timelineId]);
@ -58,9 +58,9 @@ export const TimelineWrapper: React.FC<TimelineWrapperProps> = React.memo(
return ( return (
<> <>
<EuiFocusTrap disabled={!show}> <EuiFocusTrap disabled={!show}>
<TimelineModal timelineId={timelineId} visible={show} /> <TimelineModal timelineId={timelineId} visible={show} openToggleRef={openToggleRef} />
</EuiFocusTrap> </EuiFocusTrap>
<TimelineBottomBar show={show} timelineId={timelineId} /> <TimelineBottomBar show={show} timelineId={timelineId} openToggleRef={openToggleRef} />
<EuiWindowEvent event="keydown" handler={onKeyDown} /> <EuiWindowEvent event="keydown" handler={onKeyDown} />
</> </>
); );