mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution] Fixes a bug with timeline sourcerer state (#143237)
This commit is contained in:
parent
35e8170a5a
commit
8fba39c2da
8 changed files with 307 additions and 56 deletions
|
@ -195,11 +195,7 @@ export const usePickIndexPatterns = ({
|
|||
// constantly getting destroy and re-init
|
||||
const pickedDataViewData = await getSourcererDataView(newSelectedDataViewId);
|
||||
if (isHookAlive.current) {
|
||||
dispatch(
|
||||
sourcererActions.updateSourcererDataViews({
|
||||
dataView: pickedDataViewData,
|
||||
})
|
||||
);
|
||||
dispatch(sourcererActions.setDataView(pickedDataViewData));
|
||||
setSelectedOptions(
|
||||
isOnlyDetectionAlerts
|
||||
? alertsOptions
|
||||
|
|
|
@ -33,6 +33,7 @@ export type IndexFieldSearch = (param: {
|
|||
scopeId?: SourcererScopeName;
|
||||
needToBeInit?: boolean;
|
||||
cleanCache?: boolean;
|
||||
skipScopeUpdate?: boolean;
|
||||
}) => Promise<void>;
|
||||
|
||||
type DangerCastForBrowserFieldsMutation = Record<
|
||||
|
@ -102,6 +103,7 @@ export const useDataView = (): {
|
|||
scopeId = SourcererScopeName.default,
|
||||
needToBeInit = false,
|
||||
cleanCache = false,
|
||||
skipScopeUpdate = false,
|
||||
}) => {
|
||||
const unsubscribe = () => {
|
||||
searchSubscription$.current[dataViewId]?.unsubscribe();
|
||||
|
@ -123,11 +125,7 @@ export const useDataView = (): {
|
|||
dataViewId,
|
||||
abortCtrl.current[dataViewId].signal
|
||||
);
|
||||
dispatch(
|
||||
sourcererActions.updateSourcererDataViews({
|
||||
dataView: dataViewToUpdate,
|
||||
})
|
||||
);
|
||||
dispatch(sourcererActions.setDataView(dataViewToUpdate));
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
|
@ -148,7 +146,7 @@ export const useDataView = (): {
|
|||
endTracking('success');
|
||||
|
||||
const patternString = response.indicesExist.sort().join();
|
||||
if (needToBeInit && scopeId) {
|
||||
if (needToBeInit && scopeId && !skipScopeUpdate) {
|
||||
dispatch(
|
||||
sourcererActions.setSelectedDataView({
|
||||
id: scopeId,
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
import type { SelectedDataView } from '../../store/sourcerer/model';
|
||||
import { SourcererScopeName } from '../../store/sourcerer/model';
|
||||
import { postSourcererDataView } from './api';
|
||||
import * as source from '../source/use_data_view';
|
||||
import { sourcererActions } from '../../store/sourcerer';
|
||||
import { useInitializeUrlParam, useUpdateUrlParam } from '../../utils/global_query_string';
|
||||
|
||||
|
@ -200,7 +201,7 @@ describe('Sourcerer Hooks', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('initilizes dataview with data from query string', async () => {
|
||||
it('initializes dataview with data from query string', async () => {
|
||||
const selectedPatterns = ['testPattern-*'];
|
||||
const selectedDataViewId = 'security-solution-default';
|
||||
(useInitializeUrlParam as jest.Mock).mockImplementation((_, onInitialize) =>
|
||||
|
@ -342,6 +343,198 @@ describe('Sourcerer Hooks', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
describe('initialization settings', () => {
|
||||
const mockIndexFieldsSearch = jest.fn();
|
||||
beforeAll(() => {
|
||||
// 👇️ not using dot-notation + the ignore clears up a ts error
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line dot-notation
|
||||
source['useDataView'] = jest.fn(() => ({
|
||||
indexFieldsSearch: mockIndexFieldsSearch,
|
||||
}));
|
||||
});
|
||||
it('does not needToBeInit if scope is default and selectedPatterns/missingPatterns have values', async () => {
|
||||
await act(async () => {
|
||||
const { rerender, waitForNextUpdate } = renderHook<string, void>(() => useInitSourcerer(), {
|
||||
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
rerender();
|
||||
await waitFor(() => {
|
||||
expect(mockIndexFieldsSearch).toHaveBeenCalledWith({
|
||||
dataViewId: mockSourcererState.defaultDataView.id,
|
||||
needToBeInit: false,
|
||||
scopeId: SourcererScopeName.default,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does needToBeInit if scope is default and selectedPatterns/missingPatterns are empty', async () => {
|
||||
store = createStore(
|
||||
{
|
||||
...mockGlobalState,
|
||||
sourcerer: {
|
||||
...mockGlobalState.sourcerer,
|
||||
sourcererScopes: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes,
|
||||
[SourcererScopeName.default]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
|
||||
selectedPatterns: [],
|
||||
missingPatterns: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
await act(async () => {
|
||||
const { rerender, waitForNextUpdate } = renderHook<string, void>(() => useInitSourcerer(), {
|
||||
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
rerender();
|
||||
await waitFor(() => {
|
||||
expect(mockIndexFieldsSearch).toHaveBeenCalledWith({
|
||||
dataViewId: mockSourcererState.defaultDataView.id,
|
||||
needToBeInit: true,
|
||||
scopeId: SourcererScopeName.default,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does needToBeInit and skipScopeUpdate=false if scope is timeline and selectedPatterns/missingPatterns are empty', async () => {
|
||||
store = createStore(
|
||||
{
|
||||
...mockGlobalState,
|
||||
sourcerer: {
|
||||
...mockGlobalState.sourcerer,
|
||||
kibanaDataViews: [
|
||||
...mockGlobalState.sourcerer.kibanaDataViews,
|
||||
{ ...mockSourcererState.defaultDataView, id: 'something-weird', patternList: [] },
|
||||
],
|
||||
sourcererScopes: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes,
|
||||
[SourcererScopeName.timeline]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
|
||||
selectedDataViewId: 'something-weird',
|
||||
selectedPatterns: [],
|
||||
missingPatterns: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
await act(async () => {
|
||||
const { rerender, waitForNextUpdate } = renderHook<string, void>(() => useInitSourcerer(), {
|
||||
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
rerender();
|
||||
await waitFor(() => {
|
||||
expect(mockIndexFieldsSearch).toHaveBeenNthCalledWith(2, {
|
||||
dataViewId: 'something-weird',
|
||||
needToBeInit: true,
|
||||
scopeId: SourcererScopeName.timeline,
|
||||
skipScopeUpdate: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does needToBeInit and skipScopeUpdate=true if scope is timeline and selectedPatterns have value', async () => {
|
||||
store = createStore(
|
||||
{
|
||||
...mockGlobalState,
|
||||
sourcerer: {
|
||||
...mockGlobalState.sourcerer,
|
||||
kibanaDataViews: [
|
||||
...mockGlobalState.sourcerer.kibanaDataViews,
|
||||
{ ...mockSourcererState.defaultDataView, id: 'something-weird', patternList: [] },
|
||||
],
|
||||
sourcererScopes: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes,
|
||||
[SourcererScopeName.timeline]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
|
||||
selectedDataViewId: 'something-weird',
|
||||
selectedPatterns: ['ohboy'],
|
||||
missingPatterns: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
await act(async () => {
|
||||
const { rerender, waitForNextUpdate } = renderHook<string, void>(() => useInitSourcerer(), {
|
||||
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
rerender();
|
||||
await waitFor(() => {
|
||||
expect(mockIndexFieldsSearch).toHaveBeenNthCalledWith(2, {
|
||||
dataViewId: 'something-weird',
|
||||
needToBeInit: true,
|
||||
scopeId: SourcererScopeName.timeline,
|
||||
skipScopeUpdate: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not needToBeInit if scope is timeline and data view has patternList', async () => {
|
||||
store = createStore(
|
||||
{
|
||||
...mockGlobalState,
|
||||
sourcerer: {
|
||||
...mockGlobalState.sourcerer,
|
||||
kibanaDataViews: [
|
||||
...mockGlobalState.sourcerer.kibanaDataViews,
|
||||
{
|
||||
...mockSourcererState.defaultDataView,
|
||||
id: 'something-weird',
|
||||
patternList: ['ohboy'],
|
||||
},
|
||||
],
|
||||
sourcererScopes: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes,
|
||||
[SourcererScopeName.timeline]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
|
||||
selectedDataViewId: 'something-weird',
|
||||
selectedPatterns: [],
|
||||
missingPatterns: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
await act(async () => {
|
||||
const { rerender, waitForNextUpdate } = renderHook<string, void>(() => useInitSourcerer(), {
|
||||
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
rerender();
|
||||
await waitFor(() => {
|
||||
expect(mockIndexFieldsSearch).toHaveBeenNthCalledWith(2, {
|
||||
dataViewId: 'something-weird',
|
||||
needToBeInit: false,
|
||||
scopeId: SourcererScopeName.timeline,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useSourcererDataView', () => {
|
||||
it('Should put any excludes in the index pattern at the end of the pattern list, and sort both the includes and excludes', async () => {
|
||||
|
|
|
@ -76,17 +76,21 @@ export const useInitSourcerer = (
|
|||
const activeTimeline = useDeepEqualSelector((state) =>
|
||||
getTimelineSelector(state, TimelineId.active)
|
||||
);
|
||||
const scopeIdSelector = useMemo(() => sourcererSelectors.scopeIdSelector(), []);
|
||||
|
||||
const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []);
|
||||
const {
|
||||
selectedDataViewId: scopeDataViewId,
|
||||
selectedPatterns,
|
||||
missingPatterns,
|
||||
} = useDeepEqualSelector((state) => scopeIdSelector(state, scopeId));
|
||||
sourcererScope: { selectedDataViewId: scopeDataViewId, selectedPatterns, missingPatterns },
|
||||
} = useDeepEqualSelector((state) => sourcererScopeSelector(state, scopeId));
|
||||
|
||||
const {
|
||||
selectedDataViewId: timelineDataViewId,
|
||||
selectedPatterns: timelineSelectedPatterns,
|
||||
missingPatterns: timelineMissingPatterns,
|
||||
} = useDeepEqualSelector((state) => scopeIdSelector(state, SourcererScopeName.timeline));
|
||||
selectedDataView: timelineSelectedDataView,
|
||||
sourcererScope: {
|
||||
selectedDataViewId: timelineDataViewId,
|
||||
selectedPatterns: timelineSelectedPatterns,
|
||||
missingPatterns: timelineMissingPatterns,
|
||||
},
|
||||
} = useDeepEqualSelector((state) => sourcererScopeSelector(state, SourcererScopeName.timeline));
|
||||
|
||||
const { indexFieldsSearch } = useDataView();
|
||||
|
||||
const onInitializeUrlParam = useCallback(
|
||||
|
@ -137,19 +141,29 @@ export const useInitSourcerer = (
|
|||
const searchedIds = useRef<string[]>([]);
|
||||
useEffect(() => {
|
||||
const activeDataViewIds = [...new Set([scopeDataViewId, timelineDataViewId])];
|
||||
activeDataViewIds.forEach((id) => {
|
||||
activeDataViewIds.forEach((id, i) => {
|
||||
if (id != null && id.length > 0 && !searchedIds.current.includes(id)) {
|
||||
searchedIds.current = [...searchedIds.current, id];
|
||||
|
||||
const currentScope = i === 0 ? SourcererScopeName.default : SourcererScopeName.timeline;
|
||||
|
||||
const needToBeInit =
|
||||
id === scopeDataViewId
|
||||
? selectedPatterns.length === 0 && missingPatterns.length === 0
|
||||
: timelineDataViewId === id
|
||||
? timelineMissingPatterns.length === 0 &&
|
||||
timelineSelectedDataView?.patternList.length === 0
|
||||
: false;
|
||||
|
||||
indexFieldsSearch({
|
||||
dataViewId: id,
|
||||
scopeId:
|
||||
id === scopeDataViewId ? SourcererScopeName.default : SourcererScopeName.timeline,
|
||||
needToBeInit:
|
||||
id === scopeDataViewId
|
||||
? selectedPatterns.length === 0 && missingPatterns.length === 0
|
||||
: timelineDataViewId === id
|
||||
? timelineMissingPatterns.length === 0 && timelineSelectedPatterns.length === 0
|
||||
: false,
|
||||
scopeId: currentScope,
|
||||
needToBeInit,
|
||||
...(needToBeInit && currentScope === SourcererScopeName.timeline
|
||||
? {
|
||||
skipScopeUpdate: timelineSelectedPatterns.length > 0,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -160,6 +174,7 @@ export const useInitSourcerer = (
|
|||
selectedPatterns.length,
|
||||
timelineDataViewId,
|
||||
timelineMissingPatterns.length,
|
||||
timelineSelectedDataView,
|
||||
timelineSelectedPatterns.length,
|
||||
]);
|
||||
|
||||
|
|
|
@ -7,23 +7,12 @@
|
|||
|
||||
import actionCreatorFactory from 'typescript-fsa';
|
||||
|
||||
import type {
|
||||
KibanaDataView,
|
||||
SelectedDataView,
|
||||
SourcererDataView,
|
||||
SourcererScopeName,
|
||||
} from './model';
|
||||
import type { SelectedDataView, SourcererDataView, SourcererScopeName } from './model';
|
||||
import type { SecurityDataView } from '../../containers/sourcerer/api';
|
||||
|
||||
const actionCreator = actionCreatorFactory('x-pack/security_solution/local/sourcerer');
|
||||
|
||||
export const setDataView = actionCreator<{
|
||||
browserFields: SourcererDataView['browserFields'];
|
||||
id: SourcererDataView['id'];
|
||||
indexFields: SourcererDataView['indexFields'];
|
||||
loading: SourcererDataView['loading'];
|
||||
runtimeMappings: SourcererDataView['runtimeMappings'];
|
||||
}>('SET_DATA_VIEW');
|
||||
export const setDataView = actionCreator<Partial<SourcererDataView>>('SET_DATA_VIEW');
|
||||
|
||||
export const setDataViewLoading = actionCreator<{
|
||||
id: string;
|
||||
|
@ -48,7 +37,3 @@ export interface SelectedDataViewPayload {
|
|||
shouldValidateSelectedPatterns?: boolean;
|
||||
}
|
||||
export const setSelectedDataView = actionCreator<SelectedDataViewPayload>('SET_SELECTED_DATA_VIEW');
|
||||
|
||||
export const updateSourcererDataViews = actionCreator<{
|
||||
dataView: KibanaDataView;
|
||||
}>('UPDATE_SOURCERER_DATA_VIEWS');
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { mockGlobalState } from '../../mock';
|
||||
import { mockGlobalState, mockSourcererState } from '../../mock';
|
||||
import { SourcererScopeName } from './model';
|
||||
import { getScopePatternListSelection, validateSelectedPatterns } from './helpers';
|
||||
import { sortWithExcludesAtEnd } from '../../../../common/utils/sourcerer';
|
||||
|
@ -210,5 +210,71 @@ describe('sourcerer store helpers', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not attempt to validate when missing patterns', () => {
|
||||
const state = {
|
||||
...mockGlobalState.sourcerer,
|
||||
defaultDataView: {
|
||||
...mockSourcererState.defaultDataView,
|
||||
patternList: [],
|
||||
},
|
||||
kibanaDataViews: [
|
||||
{
|
||||
...mockSourcererState.defaultDataView,
|
||||
patternList: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = validateSelectedPatterns(
|
||||
state,
|
||||
{
|
||||
...payload,
|
||||
id: SourcererScopeName.default,
|
||||
selectedPatterns: ['auditbeat-*', 'yoohoo'],
|
||||
},
|
||||
true
|
||||
);
|
||||
expect(result).toEqual({
|
||||
[SourcererScopeName.default]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
|
||||
missingPatterns: ['yoohoo'],
|
||||
selectedPatterns: ['auditbeat-*', 'yoohoo'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not attempt to validate if non-default data view has not been initialized', () => {
|
||||
const state = {
|
||||
...mockGlobalState.sourcerer,
|
||||
defaultDataView: {
|
||||
...mockSourcererState.defaultDataView,
|
||||
patternList: [],
|
||||
},
|
||||
kibanaDataViews: [
|
||||
{
|
||||
...mockSourcererState.defaultDataView,
|
||||
id: 'wow',
|
||||
patternList: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = validateSelectedPatterns(
|
||||
state,
|
||||
{
|
||||
...payload,
|
||||
id: SourcererScopeName.default,
|
||||
selectedDataViewId: 'wow',
|
||||
selectedPatterns: ['auditbeat-*', 'yoohoo'],
|
||||
},
|
||||
true
|
||||
);
|
||||
expect(result).toEqual({
|
||||
[SourcererScopeName.default]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
|
||||
selectedDataViewId: 'wow',
|
||||
selectedPatterns: ['auditbeat-*', 'yoohoo'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -58,7 +58,12 @@ export const validateSelectedPatterns = (
|
|||
const selectedPatterns =
|
||||
// shouldValidateSelectedPatterns is false when upgrading from
|
||||
// legacy pre-8.0 timeline index patterns to data view.
|
||||
shouldValidateSelectedPatterns && dataView != null && missingPatterns.length === 0
|
||||
shouldValidateSelectedPatterns &&
|
||||
dataView != null &&
|
||||
missingPatterns.length === 0 &&
|
||||
// don't validate when the data view has not been initialized (default is initialized already always)
|
||||
dataView.id !== state.defaultDataView.id &&
|
||||
dataView.patternList.length > 0
|
||||
? dedupePatterns.filter(
|
||||
(pattern) =>
|
||||
(dataView != null && dataView.patternList.includes(pattern)) ||
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
setSignalIndexName,
|
||||
setDataView,
|
||||
setDataViewLoading,
|
||||
updateSourcererDataViews,
|
||||
} from './actions';
|
||||
import type { SourcererModel } from './model';
|
||||
import { initDataView, initialSourcererState, SourcererScopeName } from './model';
|
||||
|
@ -47,12 +46,6 @@ export const sourcererReducer = reducerWithInitialState(initialSourcererState)
|
|||
...dataView,
|
||||
})),
|
||||
}))
|
||||
.case(updateSourcererDataViews, (state, { dataView }) => ({
|
||||
...state,
|
||||
kibanaDataViews: state.kibanaDataViews.map((dv) =>
|
||||
dv.id === dataView.id ? { ...dv, ...dataView } : dv
|
||||
),
|
||||
}))
|
||||
.case(setSourcererScopeLoading, (state, { id, loading }) => ({
|
||||
...state,
|
||||
sourcererScopes: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue