[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.
*/
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<SendToTimelineButtonP
const isEsqlTabInTimelineDisabled = useIsExperimentalFeatureEnabled('timelineEsqlTabDisabled');
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');

View file

@ -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');

View file

@ -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<SourcererComponentProps>(({ scope: scopeId }
const isDefaultSourcerer = scopeId === SourcererScopeName.default;
const updateUrlParam = useUpdateUrlParam<SourcererUrlState>(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(() => {

View file

@ -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<SourcererUrlState>(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(

View file

@ -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]

View file

@ -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;

View file

@ -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', () => ({
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 }) => (
<TestProviders store={createMockStore(initState)}>{children}</TestProviders>
),
});
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 }) => (
<TestProviders store={createMockStore(state)}>{children}</TestProviders>
),
});
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.
*/
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 scopeIdSelector = () => createSelector(sourcererScopeIdSelector, (scope) => scope);
export const kibanaDataViewsSelector = () =>
createSelector(sourcererKibanaDataViewsSelector, (dataViews) => dataViews);
export const signalIndexNameSelector = () =>
createSelector(sourcererSignalIndexNameSelector, (signalIndexName) => signalIndexName);
export const defaultDataViewSelector = () =>
createSelector(sourcererDefaultDataViewSelector, (dataViews) => dataViews);
export const sourcererDataViewSelector = () =>
createSelector(dataViewSelector, (dataView) => dataView);
export interface SourcererScopeSelector extends Omit<SourcererModel, 'sourcererScopes'> {
selectedDataView: SourcererDataView | undefined;
sourcererScope: SourcererScope;
export const sourcererScope = createSelector(
sourcererScopes,
(state: State, scopeId: SourcererScopeName) => scopeId,
(scopes, scopeId) => scopes[scopeId],
{
memoizeOptions: {
maxSize: SOURCERER_SCOPE_MAX_SIZE,
},
}
);
export const getSourcererDataViewsSelector = () => {
const getKibanaDataViewsSelector = kibanaDataViewsSelector();
const getDefaultDataViewSelector = defaultDataViewSelector();
const getSignalIndexNameSelector = signalIndexNameSelector();
return (state: State): Omit<SourcererModel, 'sourcererScopes'> => {
const kibanaDataViews = getKibanaDataViewsSelector(state);
const defaultDataView = getDefaultDataViewSelector(state);
const signalIndexName = getSignalIndexNameSelector(state);
export const sourcererScopeIsLoading = createSelector(sourcererScope, (scope) => scope.loading, {
memoizeOptions: {
maxSize: SOURCERER_SCOPE_MAX_SIZE,
},
});
return {
defaultDataView,
kibanaDataViews,
signalIndexName,
};
};
};
export const sourcererScopeSelectedDataViewId = createSelector(
sourcererScope,
(scope) => scope.selectedDataViewId,
{
memoizeOptions: {
maxSize: SOURCERER_SCOPE_MAX_SIZE,
},
}
);
/**
* 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();
export const sourcererScopeSelectedPatterns = createSelector(
sourcererScope,
(scope) => scope.selectedPatterns,
{
memoizeOptions: {
maxSize: SOURCERER_SCOPE_MAX_SIZE,
},
}
);
return (state: State, scopeId: SourcererScopeName): SourcererScopeSelector => {
const dataViews = getDataViewsSelector(state);
const scope = getScopeSelector(state, scopeId);
const selectedDataView = getSourcererDataViewSelector(state, scope.selectedDataViewId);
export const sourcererScopeMissingPatterns = createSelector(
sourcererScope,
(scope) => scope.missingPatterns,
{
memoizeOptions: {
maxSize: SOURCERER_SCOPE_MAX_SIZE,
},
}
);
return {
...dataViews,
selectedDataView,
sourcererScope: scope,
};
};
};
export const kibanaDataViews = createSelector(
selectSourcerer,
(sourcerer) => sourcerer.kibanaDataViews,
{
memoizeOptions: {
maxSize: SOURCERER_SCOPE_MAX_SIZE,
},
}
);
export const getSelectedDataviewSelector = () => {
const getSourcererDataViewSelector = sourcererDataViewSelector();
const getScopeSelector = scopeIdSelector();
export const defaultDataView = createSelector(
selectSourcerer,
(sourcerer) => sourcerer.defaultDataView,
{
memoizeOptions: {
maxSize: SOURCERER_SCOPE_MAX_SIZE,
},
}
);
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,
},
}
);

View file

@ -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(<div data-test-subj="EmbeddablePanel" />),
}));
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(
<TestProviders>
<TestProviders store={defaultMockStore}>
<EmbeddedMapComponent {...testProps} />
</TestProviders>
);
@ -132,7 +157,7 @@ describe('EmbeddedMapComponent', () => {
test('calls updateInput with time range filter', async () => {
render(
<TestProviders>
<TestProviders store={defaultMockStore}>
<EmbeddedMapComponent {...testProps} />
</TestProviders>
);
@ -146,7 +171,7 @@ describe('EmbeddedMapComponent', () => {
test('renders EmbeddablePanel from embeddable plugin', async () => {
const { getByTestId, queryByTestId } = render(
<TestProviders>
<TestProviders store={defaultMockStore}>
<EmbeddedMapComponent {...testProps} />
</TestProviders>
);
@ -159,13 +184,17 @@ describe('EmbeddedMapComponent', () => {
});
test('renders IndexPatternsMissingPrompt', async () => {
jest.spyOn(redux, 'useSelector').mockReturnValue({
...mockSelector,
const state = {
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
kibanaDataViews: [],
});
},
};
const store = createMockStore(state);
const { getByTestId, queryByTestId } = render(
<TestProviders>
<TestProviders store={store}>
<EmbeddedMapComponent {...testProps} />
</TestProviders>
);
@ -180,7 +209,7 @@ describe('EmbeddedMapComponent', () => {
mockCreateEmbeddable.mockResolvedValue(null);
const { getByTestId, queryByTestId } = render(
<TestProviders>
<TestProviders store={defaultMockStore}>
<EmbeddedMapComponent {...testProps} />
</TestProviders>
);
@ -194,7 +223,7 @@ describe('EmbeddedMapComponent', () => {
test('map hidden on close', async () => {
mockGetStorage.mockReturnValue(false);
const { getByTestId, queryByTestId } = render(
<TestProviders>
<TestProviders store={defaultMockStore}>
<EmbeddedMapComponent {...testProps} />
</TestProviders>
);
@ -210,7 +239,7 @@ describe('EmbeddedMapComponent', () => {
test('map visible on open', async () => {
const { getByTestId, queryByTestId } = render(
<TestProviders>
<TestProviders store={defaultMockStore}>
<EmbeddedMapComponent {...testProps} />
</TestProviders>
);
@ -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(
<TestProviders>
<TestProviders store={store}>
<EmbeddedMapComponent {...testProps} />
</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 () => {
mockUseSourcererDataView.mockReturnValue({
selectedPatterns: ['filebeat-*', 'auditbeat-*'],
});
const state = {
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
kibanaDataViews: [filebeatDataView],
},
};
const store = createMockStore(state);
const { rerender } = render(
<TestProviders>
<TestProviders store={store}>
<EmbeddedMapComponent {...testProps} />
</TestProviders>
);
@ -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(
<TestProviders>
<TestProviders store={defaultMockStore}>
<EmbeddedMapComponent {...testProps} />
</TestProviders>
);

View file

@ -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

View file

@ -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,

View file

@ -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,
},

View file

@ -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,6 +94,7 @@ export const MockResolver = React.memo((props: MockResolverProps) => {
}, [props.rasterWidth, props.rasterHeight, props.sideEffectSimulator.controls, resolverElement]);
return (
<TestProviders>
<I18nProvider>
<Router history={props.history}>
<KibanaContextProvider services={props.coreStart}>
@ -111,5 +113,6 @@ export const MockResolver = React.memo((props: MockResolverProps) => {
</KibanaContextProvider>
</Router>
</I18nProvider>
</TestProviders>
);
});

View file

@ -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(
<TestProviders>
<TimelineBottomBar show={false} timelineId={TimelineId.test} />
<TimelineBottomBar show={false} timelineId={TimelineId.test} openToggleRef={mockRef} />
</TestProviders>
);
@ -38,7 +42,7 @@ describe('TimelineBottomBar', () => {
test('should not render the event count badge if timeline is open', () => {
const { queryByTestId } = render(
<TestProviders>
<TimelineBottomBar show={true} timelineId={TimelineId.test} />
<TimelineBottomBar show={true} timelineId={TimelineId.test} openToggleRef={mockRef} />
</TestProviders>
);
@ -50,7 +54,7 @@ describe('TimelineBottomBar', () => {
const { getByTestId } = render(
<TestProviders>
<TimelineBottomBar show={true} timelineId={TimelineId.test} />
<TimelineBottomBar show={true} timelineId={TimelineId.test} openToggleRef={mockRef} />
</TestProviders>
);

View file

@ -26,12 +26,15 @@ interface TimelineBottomBarProps {
* True if the timeline modal is open
*/
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.
*/
export const TimelineBottomBar = React.memo<TimelineBottomBarProps>(({ show, timelineId }) => {
export const TimelineBottomBar = React.memo<TimelineBottomBarProps>(
({ show, timelineId, openToggleRef }) => {
const dispatch = useDispatch();
const openTimeline = useCallback(
@ -55,6 +58,7 @@ export const TimelineBottomBar = React.memo<TimelineBottomBarProps>(({ show, tim
aria-label={i18n.OPEN_TIMELINE_BUTTON(title)}
onClick={openTimeline}
data-test-subj="timeline-bottom-bar-title-button"
ref={openToggleRef}
>
{title}
</EuiLink>
@ -70,6 +74,7 @@ export const TimelineBottomBar = React.memo<TimelineBottomBarProps>(({ show, tim
</EuiFlexGroup>
</EuiPanel>
);
});
}
);
TimelineBottomBar.displayName = 'TimelineBottomBar';

View file

@ -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<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();
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<UseFieldBrowserOptionsProps> = {}) =>
renderHook<UseFieldBrowserOptionsProps, ReturnType<UseFieldBrowserOptions>>(
const renderUseFieldBrowserOptions = (
props: Partial<UseFieldBrowserOptionsProps & { store?: Store }> = {}
) =>
renderHook<UseFieldBrowserOptionsProps & { store?: Store }, ReturnType<UseFieldBrowserOptions>>(
() =>
useFieldBrowserOptions({
sourcererScope: SourcererScopeName.default,
@ -67,7 +54,12 @@ const renderUseFieldBrowserOptions = (props: Partial<UseFieldBrowserOptionsProps
...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,
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();

View file

@ -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) => {

View file

@ -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(<NewTimelineButton timelineId={TimelineId.test} />);
const renderNewTimelineButton = () =>
render(<NewTimelineButton timelineId={TimelineId.test} />, { 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(),
});

View file

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

View file

@ -62,12 +62,14 @@ interface FlyoutHeaderPanelProps {
* Id of the timeline to be displayed within the modal
*/
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
*/
export const TimelineModalHeader = React.memo<FlyoutHeaderPanelProps>(({ timelineId }) => {
export const TimelineModalHeader = React.memo<FlyoutHeaderPanelProps>(
({ timelineId, openToggleRef }) => {
const dispatch = useDispatch();
const { browserFields, indexPattern } = useSourcererDataView(SourcererScopeName.timeline);
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 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(
@ -98,9 +100,12 @@ export const TimelineModalHeader = React.memo<FlyoutHeaderPanelProps>(({ timelin
const isInspectDisabled = !isDataInTimeline || combinedQueries?.filterQuery === undefined;
const closeTimeline = useCallback(() => {
if (openToggleRef.current != null) {
openToggleRef.current.focus();
}
createHistoryEntry();
dispatch(timelineActions.showTimeline({ id: timelineId, show: false }));
}, [dispatch, timelineId]);
}, [dispatch, timelineId, openToggleRef]);
return (
<TimelinePanel
@ -186,6 +191,7 @@ export const TimelineModalHeader = React.memo<FlyoutHeaderPanelProps>(({ timelin
</EuiFlexGroup>
</TimelinePanel>
);
});
}
);
TimelineModalHeader.displayName = 'TimelineModalHeader';

View file

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

View file

@ -33,14 +33,17 @@ interface TimelineModalProps {
* If true the timeline modal will be visible
*/
visible?: boolean;
openToggleRef: React.MutableRefObject<null | HTMLAnchorElement | HTMLButtonElement>;
}
/**
* 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 isFullScreen = useShallowEqualSelector(inputsSelectors.timelineFullScreenSelector) ?? false;
const isFullScreen =
useShallowEqualSelector(inputsSelectors.timelineFullScreenSelector) ?? false;
const styles = usePaneStyles();
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,
});
const sibling: HTMLDivElement | null = useMemo(() => (!visible ? ref?.current : null), [visible]);
const sibling: HTMLDivElement | null = useMemo(
() => (!visible ? ref?.current : null),
[visible]
);
return (
<div data-test-subj="timeline-portal-ref" ref={ref}>
@ -63,6 +69,7 @@ export const TimelineModal = React.memo<TimelineModalProps>(({ timelineId, visib
renderCellValue={DefaultCellRenderer}
rowRenderers={defaultRowRenderers}
timelineId={timelineId}
openToggleRef={openToggleRef}
/>
</div>
</div>
@ -70,6 +77,7 @@ export const TimelineModal = React.memo<TimelineModalProps>(({ timelineId, visib
{visible && <OverflowHiddenGlobalStyles />}
</div>
);
});
}
);
TimelineModal.displayName = 'TimelineModal';

View file

@ -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(<NewTimelineButton type={type} />);
const renderNewTimelineButton = (type: TimelineType) =>
render(<NewTimelineButton type={type} />, { 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(),
});

View file

@ -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(() => {

View file

@ -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<null | HTMLAnchorElement | HTMLButtonElement>;
}
const TimelineSavingProgressComponent: React.FC<{ timelineId: TimelineId }> = ({ timelineId }) => {
@ -67,15 +69,17 @@ const StatefulTimelineComponent: React.FC<Props> = ({
renderCellValue,
rowRenderers,
timelineId,
openToggleRef,
}) => {
const dispatch = useDispatch();
const containerElement = useRef<HTMLDivElement | null>(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<Props> = ({
$isVisible={!timelineFullScreen}
data-test-subj="timeline-hide-show-container"
>
<TimelineModalHeader timelineId={timelineId} />
<TimelineModalHeader timelineId={timelineId} openToggleRef={openToggleRef} />
</HideShowContainer>
<TabsContent

View file

@ -6,6 +6,7 @@
*/
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { useDeepEqualSelector } from '../../common/hooks/use_selector';
import {
@ -47,11 +48,7 @@ export function useTimelineDataFilters(isActiveTimelines: boolean) {
return getEndSelector(state.inputs.global);
}
});
const getDefaultDataViewSelector = useMemo(
() => sourcererSelectors.defaultDataViewSelector(),
[]
);
const defaultDataView = useDeepEqualSelector(getDefaultDataViewSelector);
const defaultDataView = useSelector(sourcererSelectors.defaultDataView);
const { pathname } = useLocation();
const { selectedPatterns: nonTimelinePatterns } = useSourcererDataView(
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.
*/
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 }) => <TestProviders>{children}</TestProviders>,
}
);
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 }) => <TestProviders>{children}</TestProviders>,
}
);
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(() =>
const hookResult = renderHook(
() =>
useCreateTimeline({
timelineId: TimelineId.test,
timelineType: TimelineType.default,
onClick,
})
}),
{
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
}
);
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 }) => <TestProviders>{children}</TestProviders>,
}
);
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 }) => <TestProviders>{children}</TestProviders>,
}
);
const timeRange: TimeRange = { kind: 'relative', fromStr: '', toStr: '', from: '', to: '' };

View file

@ -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);

View file

@ -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<TimelineWrapperProps> = 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<TimelineWrapperProps> = React.memo(
return (
<>
<EuiFocusTrap disabled={!show}>
<TimelineModal timelineId={timelineId} visible={show} />
<TimelineModal timelineId={timelineId} visible={show} openToggleRef={openToggleRef} />
</EuiFocusTrap>
<TimelineBottomBar show={show} timelineId={timelineId} />
<TimelineBottomBar show={show} timelineId={timelineId} openToggleRef={openToggleRef} />
<EuiWindowEvent event="keydown" handler={onKeyDown} />
</>
);