From 6a1d3e328227ce9ffe314df48411a56a27ff70a2 Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 11 Feb 2025 12:29:41 +0100 Subject: [PATCH 001/115] --wip-- [skip ci] --- .../public/app/home/index.tsx | 2 + .../public/common/store/store.ts | 6 +- .../components/data_view_picker/index.tsx | 100 +++++++++++++ .../public/data_view_picker/constants.ts | 12 ++ .../hooks/use_init_data_view_picker.ts | 91 ++++++++++++ .../public/data_view_picker/readme.md | 0 .../redux/effects/middleware.ts | 10 ++ .../public/data_view_picker/redux/index.ts | 134 ++++++++++++++++++ .../search_or_filter/search_or_filter.tsx | 4 +- 9 files changed, 356 insertions(+), 3 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/constants.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/readme.md create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/effects/middleware.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/home/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/app/home/index.tsx index dfaa8ea7fda6e..9e3b7101b11df 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/app/home/index.tsx @@ -27,6 +27,7 @@ import { useSetupDetectionEngineHealthApi } from '../../detection_engine/rule_mo import { TopValuesPopover } from '../components/top_values_popover/top_values_popover'; import { AssistantOverlay } from '../../assistant/overlay'; import { useInitSourcerer } from '../../sourcerer/containers/use_init_sourcerer'; +import { useInitDataViewPicker } from '../../data_view_picker/hooks/use_init_data_view_picker'; interface HomePageProps { children: React.ReactNode; @@ -38,6 +39,7 @@ const HomePageComponent: React.FC = ({ children }) => { useUrlState(); useUpdateBrowserTitle(); useUpdateExecutionContext(); + useInitDataViewPicker(); const { browserFields } = useSourcererDataView(getScopeFromPath(pathname)); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts b/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts index 8b3f8ead09175..43c8e6aa2ea06 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts @@ -56,6 +56,8 @@ import { createMiddlewares } from './middlewares'; import { addNewTimeline } from '../../timelines/store/helpers'; import { initialNotesState } from '../../notes/store/notes.slice'; import { hasAccessToSecuritySolution } from '../../helpers_access'; +import { dataViewPickerReducer } from '../../data_view_picker/redux'; +import { listenerMiddleware } from '../../data_view_picker/redux/effects/middleware'; let store: Store | null = null; @@ -178,6 +180,7 @@ export const createStoreFactory = async ( ...subPlugins.explore.store.reducer, timeline: timelineReducer, ...subPlugins.management.store.reducer, + dataViewPicker: dataViewPickerReducer, }; return createStore(initialState, rootReducer, coreStart, storage, [ @@ -288,7 +291,8 @@ export const createStore = ( ...createMiddlewares(kibana, storage), telemetryMiddleware, ...(additionalMiddleware ?? []), - thunk + thunk, + listenerMiddleware.middleware ); store = createReduxStore( diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx new file mode 100644 index 0000000000000..41ee9053bb2cc --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DataViewPicker as USDataViewPicker } from '@kbn/unified-search-plugin/public'; +import React, { useCallback, useRef, useMemo, memo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import type { DataViewPickerScopeName } from '../../constants'; +import { useKibana } from '../../../common/lib/kibana/kibana_react'; +import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../../constants'; +import { selectDataViewAsync, sourcererAdapterSelector } from '../../redux'; + +export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) => { + const dispatch = useDispatch(); + + const { + services: { dataViewEditor, data, dataViewFieldEditor }, + } = useKibana(); + + const closeDataViewEditor = useRef<() => void | undefined>(); + const closeFieldEditor = useRef<() => void | undefined>(); + + // TODO: should this be implemented like that? If yes, we need to source dataView somehow or implement the same thing based on the existing state value. + // const canEditDataView = + // Boolean(dataViewEditor?.userPermissions.editDataView()) || !dataView.isPersisted(); + const canEditDataView = true; + + const { dataView } = useSelector(sourcererAdapterSelector(props.scope)); + + const dataViewId = dataView?.id; + + const createNewDataView = useCallback(async () => { + closeDataViewEditor.current = await dataViewEditor.openEditor({ + // eslint-disable-next-line no-console + onSave: () => console.log('new data view saved'), + allowAdHocDataView: true, + }); + }, [dataViewEditor]); + + const onFieldEdited = useCallback(() => {}, []); + + const editField = useMemo(() => { + if (!canEditDataView) { + return; + } + return async (fieldName?: string, _uiAction: 'edit' | 'add' = 'edit') => { + if (!dataViewId) { + return; + } + + const dataViewInstance = await data.dataViews.get(dataViewId); + closeFieldEditor.current = await dataViewFieldEditor.openEditor({ + ctx: { + dataView: dataViewInstance, + }, + fieldName, + onSave: async () => { + onFieldEdited(); + }, + }); + }; + }, [canEditDataView, dataViewId, data.dataViews, dataViewFieldEditor, onFieldEdited]); + + const addField = useMemo( + () => (canEditDataView && editField ? () => editField(undefined, 'add') : undefined), + [editField, canEditDataView] + ); + + const handleChangeDataView = useCallback( + (id: string) => { + dispatch(selectDataViewAsync({ id, scope: props.scope })); + }, + [dispatch, props.scope] + ); + + const handleEditDataView = useCallback(() => {}, []); + + const triggerConfig = useMemo(() => { + return { + label: dataView?.name ?? dataView?.id ?? 'Data view', + }; + }, [dataView]); + + return ( + + ); +}); + +DataViewPicker.displayName = 'DataviewPicker'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/constants.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/constants.ts new file mode 100644 index 0000000000000..df17faa22550d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID = 'security-solution-default'; + +export { SourcererScopeName as DataViewPickerScopeName } from '../sourcerer/store/model'; + +export const SLICE_PREFIX = 'x-pack/security_solution/dataViewPicker' as const; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts new file mode 100644 index 0000000000000..d48673347abe7 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useDispatch } from 'react-redux'; +import { useEffect } from 'react'; +import type { AnyAction, Dispatch, ListenerEffectAPI } from '@reduxjs/toolkit'; +import { addListener, removeListener } from '@reduxjs/toolkit'; +import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public'; +import { shared, scopes, type RootState, selectDataViewAsync } from '../redux'; +import { useKibana } from '../../common/lib/kibana'; + +const createDataViewsLoadingListener = (dependencies: { dataViews: DataViewsServicePublic }) => { + return { + actionCreator: shared.actions.init, + effect: async ( + _action: AnyAction, + listenerApi: ListenerEffectAPI> + ) => { + try { + const dataViews = await dependencies.dataViews.getAllDataViewLazy(); + const dataViewSpecs = await Promise.all(dataViews.map((dataView) => dataView.toSpec())); + + listenerApi.dispatch(shared.actions.setDataViews(dataViewSpecs)); + } catch (error: unknown) { + listenerApi.dispatch(shared.actions.error()); + } + }, + } as any; +}; + +const createDataViewSelectedListener = (dependencies: { dataViews: DataViewsServicePublic }) => { + return { + actionCreator: selectDataViewAsync, + effect: async ( + action: ReturnType, + listenerApi: ListenerEffectAPI> + ) => { + console.log('selectDataViewAsync', action); + + try { + if (action.payload.id) { + const dataViewById = await dependencies.dataViews.get(action.payload.id); + const dataViewSpec = dataViewById.toSpec(); + listenerApi.dispatch( + scopes[action.payload.scope].actions.setSelectedDataView(dataViewSpec) + ); + } else { + const adhocDataView = await dependencies.dataViews.create({ + id: 'adhoc', + title: action.payload.patterns?.join(','), + }); + const dataViewSpec = adhocDataView.toSpec(); + listenerApi.dispatch( + scopes[action.payload.scope].actions.setSelectedDataView(dataViewSpec) + ); + } + } catch (error: unknown) { + console.error(error); + } + }, + } as any; +}; + +export const useInitDataViewPicker = () => { + const dispatch = useDispatch(); + const services = useKibana().services; + + useEffect(() => { + const dataViewsLoadingListener = createDataViewsLoadingListener({ + dataViews: services.dataViews, + }); + + const dataViewSelectedListener = createDataViewSelectedListener({ + dataViews: services.dataViews, + }); + + dispatch(addListener(dataViewsLoadingListener)); + dispatch(addListener(dataViewSelectedListener)); + + dispatch(shared.actions.init()); + + return () => { + dispatch(removeListener(dataViewsLoadingListener)); + dispatch(removeListener(dataViewSelectedListener)); + }; + }, [dispatch, services.dataViews]); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/readme.md b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/readme.md new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/effects/middleware.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/effects/middleware.ts new file mode 100644 index 0000000000000..dc6e6eb62e165 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/effects/middleware.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createListenerMiddleware } from '@reduxjs/toolkit'; + +export const listenerMiddleware = createListenerMiddleware(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts new file mode 100644 index 0000000000000..6c79bb7a312d1 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DataViewSpec } from '@kbn/data-views-plugin/common'; +import { + combineReducers, + createSlice, + createSelector, + createAction, + type PayloadAction, +} from '@reduxjs/toolkit'; + +import { DataViewPickerScopeName, SLICE_PREFIX } from '../constants'; + +export interface ScopedDataViewSelectionState { + dataView: DataViewSpec | null; + /** + * There are several states the picker can be in internally: + * - pristine - not initialized yet + * - loading + * - error - some kind of a problem during data init + * - ready - ready to provide index information to the client + */ + status: 'pristine' | 'loading' | 'error' | 'ready'; +} + +export interface SharedDataViewSelectionState { + dataViews: Record; + status: 'pristine' | 'loading' | 'error' | 'ready'; +} + +export const initialScopeState: ScopedDataViewSelectionState = { + dataView: null, + status: 'pristine', +}; + +export const initialSharedState: SharedDataViewSelectionState = { + dataViews: {}, + status: 'pristine', +}; + +export const selectDataViewAsync = createAction<{ + id?: string; + patterns?: string[]; + scope: DataViewPickerScopeName; +}>(`${SLICE_PREFIX}/selectDataView`); + +const createDataViewSelectionSlice = (scopeName: T) => + createSlice({ + name: `${SLICE_PREFIX}/${scopeName}`, + initialState: initialScopeState, + reducers: { + setSelectedDataView: (state, action: PayloadAction) => { + state.dataView = action.payload; + }, + }, + extraReducers(builder) { + builder.addCase(selectDataViewAsync, (state, action) => { + if (action.payload.scope !== scopeName) { + return state; + } + + state.status = 'loading'; + }); + }, + }); + +export const shared = createSlice({ + name: `${SLICE_PREFIX}/shared`, + initialState: initialSharedState, + reducers: { + setDataViews: (state, action: PayloadAction) => { + state.dataViews = action.payload.reduce((viewsMap, dataView) => { + if (!dataView.id) { + return viewsMap; + } + + viewsMap[dataView.id] = dataView; + + return viewsMap; + }, {} as Record); + }, + init: (state) => { + state.status = 'loading'; + }, + error: (state) => { + state.status = 'error'; + }, + }, +}); + +export const scopes = { + [DataViewPickerScopeName.default]: createDataViewSelectionSlice(DataViewPickerScopeName.default), + [DataViewPickerScopeName.timeline]: createDataViewSelectionSlice( + DataViewPickerScopeName.timeline + ), + [DataViewPickerScopeName.detections]: createDataViewSelectionSlice( + DataViewPickerScopeName.detections + ), + [DataViewPickerScopeName.analyzer]: createDataViewSelectionSlice( + DataViewPickerScopeName.analyzer + ), +} as const; + +export const dataViewPickerReducer = combineReducers({ + [DataViewPickerScopeName.default]: scopes[DataViewPickerScopeName.default].reducer, + [DataViewPickerScopeName.timeline]: scopes[DataViewPickerScopeName.timeline].reducer, + [DataViewPickerScopeName.detections]: scopes[DataViewPickerScopeName.detections].reducer, + [DataViewPickerScopeName.analyzer]: scopes[DataViewPickerScopeName.analyzer].reducer, + shared: shared.reducer, +}); + +export type DataviewPickerState = ReturnType; + +export interface RootState { + dataViewPicker: DataviewPickerState; +} + +export const sourcererAdapterSelector = (scope: DataViewPickerScopeName) => + createSelector( + [(state: RootState) => state.dataViewPicker], + (dataViewPicker): ScopedDataViewSelectionState => { + return dataViewPicker[scope]; + } + ); + +// export const sharedStateSelector = createSelector( +// [(state: RootState) => state.dataViewPicker], +// (dataViewPicker) => dataViewPicker.shared +// ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx index 3ec2a965d8021..f3fc346b09b03 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx @@ -11,6 +11,7 @@ import styled from 'styled-components'; import type { Filter } from '@kbn/es-query'; import type { FilterManager } from '@kbn/data-plugin/public'; +import { DataViewPicker } from '../../../../data_view_picker/components/data_view_picker'; import { type TimelineType, TimelineTypeEnum } from '../../../../../common/api/timeline'; import { InputsModelId } from '../../../../common/store/inputs/constants'; import type { KqlMode } from '../../../store/model'; @@ -22,7 +23,6 @@ import { QueryBarTimeline } from '../query_bar'; import { TimelineDatePickerLock } from '../date_picker_lock'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; -import { Sourcerer } from '../../../../sourcerer/components'; import { DATA_PROVIDER_HIDDEN_EMPTY, DATA_PROVIDER_HIDDEN_POPULATED, @@ -112,7 +112,7 @@ export const SearchOrFilter = React.memo( responsive={false} > - + Date: Tue, 11 Feb 2025 14:05:26 +0100 Subject: [PATCH 002/115] --wip-- [skip ci] --- .../components/data_view_picker/index.tsx | 7 +++--- .../data_view_picker/hooks/use_data_view.ts | 17 ++++++++++++++ .../hooks/use_init_data_view_picker.ts | 12 ++++++++++ .../hooks/use_select_data_view.ts | 22 +++++++++++++++++++ .../public/data_view_picker/redux/index.ts | 12 +++++----- .../open_timeline/use_update_timeline.tsx | 9 ++++++++ .../timelines/hooks/use_create_timeline.tsx | 17 +++++++++++--- .../public/timelines/pages/timelines_page.tsx | 6 +++-- 8 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_data_view.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx index 41ee9053bb2cc..cf552ac9f4f78 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx @@ -7,12 +7,13 @@ import { DataViewPicker as USDataViewPicker } from '@kbn/unified-search-plugin/public'; import React, { useCallback, useRef, useMemo, memo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import type { DataViewPickerScopeName } from '../../constants'; import { useKibana } from '../../../common/lib/kibana/kibana_react'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../../constants'; -import { selectDataViewAsync, sourcererAdapterSelector } from '../../redux'; +import { selectDataViewAsync } from '../../redux'; +import { useDataView } from '../../hooks/use_data_view'; export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) => { const dispatch = useDispatch(); @@ -29,7 +30,7 @@ export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) = // Boolean(dataViewEditor?.userPermissions.editDataView()) || !dataView.isPersisted(); const canEditDataView = true; - const { dataView } = useSelector(sourcererAdapterSelector(props.scope)); + const { dataView } = useDataView(props.scope); const dataViewId = dataView?.id; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_data_view.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_data_view.ts new file mode 100644 index 0000000000000..23c9509e5c1c8 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_data_view.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useSelector } from 'react-redux'; +import type { DataViewPickerScopeName } from '../constants'; +import { sourcererAdapterSelector } from '../redux'; + +/** + * Returns data view selection for given scopeName + */ +export const useDataView = (scopeName: DataViewPickerScopeName) => { + return useSelector(sourcererAdapterSelector(scopeName)); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts index d48673347abe7..6afdd7d9b1ad0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts @@ -12,6 +12,7 @@ import { addListener, removeListener } from '@reduxjs/toolkit'; import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public'; import { shared, scopes, type RootState, selectDataViewAsync } from '../redux'; import { useKibana } from '../../common/lib/kibana'; +import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewPickerScopeName } from '../constants'; const createDataViewsLoadingListener = (dependencies: { dataViews: DataViewsServicePublic }) => { return { @@ -65,6 +66,9 @@ const createDataViewSelectedListener = (dependencies: { dataViews: DataViewsServ } as any; }; +/** + * Should only be used once in the application, on the top level of the rendering tree + */ export const useInitDataViewPicker = () => { const dispatch = useDispatch(); const services = useKibana().services; @@ -83,6 +87,14 @@ export const useInitDataViewPicker = () => { dispatch(shared.actions.init()); + // Preload the default view + dispatch( + selectDataViewAsync({ + id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, + scope: DataViewPickerScopeName.default, + }) + ); + return () => { dispatch(removeListener(dataViewsLoadingListener)); dispatch(removeListener(dataViewSelectedListener)); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts new file mode 100644 index 0000000000000..3edfbf844ee11 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useDispatch } from 'react-redux'; +import { useCallback } from 'react'; +import type { DataViewPickerScopeName } from '../constants'; +import { selectDataViewAsync } from '../redux'; + +export const useSelectDataView = () => { + const dispatch = useDispatch(); + + return useCallback( + (params: { id?: string; patterns?: string[]; scope: DataViewPickerScopeName }) => { + dispatch(selectDataViewAsync(params)); + }, + [dispatch] + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts index 6c79bb7a312d1..be3db07f919ee 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts @@ -121,12 +121,12 @@ export interface RootState { } export const sourcererAdapterSelector = (scope: DataViewPickerScopeName) => - createSelector( - [(state: RootState) => state.dataViewPicker], - (dataViewPicker): ScopedDataViewSelectionState => { - return dataViewPicker[scope]; - } - ); + createSelector([(state: RootState) => state.dataViewPicker], (dataViewPicker) => { + return { + ...dataViewPicker[scope], + indicesExist: !!dataViewPicker[scope]?.dataView?.title?.split(',')?.length, + }; + }); // export const sharedStateSelector = createSelector( // [(state: RootState) => state.dataViewPicker], diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx index 2927b2365221d..9b0d202403852 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx @@ -8,6 +8,8 @@ import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { isEmpty } from 'lodash/fp'; +import { DataViewPickerScopeName } from '../../../data_view_picker/constants'; +import { useSelectDataView } from '../../../data_view_picker/hooks/use_select_data_view'; import type { Note } from '../../../../common/api/timeline'; import { TimelineStatusEnum, TimelineTypeEnum } from '../../../../common/api/timeline'; import { createNote } from '../notes/helpers'; @@ -36,6 +38,7 @@ import type { UpdateTimeline } from './types'; export const useUpdateTimeline = () => { const dispatch = useDispatch(); + const selectDataView = useSelectDataView(); return useCallback( ({ @@ -65,6 +68,12 @@ export const useUpdateTimeline = () => { selectedPatterns: _timeline.indexNames, }) ); + + selectDataView({ + id: _timeline.dataViewId ?? undefined, + patterns: _timeline.indexNames, + scope: DataViewPickerScopeName.timeline, + }); } if ( _timeline.status === TimelineStatusEnum.immutable && diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx index 2f80e969cab5e..06781679baeac 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx @@ -21,6 +21,8 @@ import type { TimeRange } from '../../common/store/inputs/model'; import { useDiscoverInTimelineContext } from '../../common/components/discover_in_timeline/use_discover_in_timeline_context'; import { defaultUdtHeaders } from '../components/timeline/body/column_headers/default_headers'; import { timelineDefaults } from '../store/defaults'; +import { useSelectDataView } from '../../data_view_picker/hooks/use_select_data_view'; +import { DataViewPickerScopeName } from '../../data_view_picker/constants'; export interface UseCreateTimelineParams { /** @@ -57,6 +59,8 @@ export const useCreateTimeline = ({ const { resetDiscoverAppState } = useDiscoverInTimelineContext(); + const setSelectedDataView = useSelectDataView(); + const createTimeline = useCallback( ({ id, @@ -80,6 +84,12 @@ export const useCreateTimeline = ({ }) ); + setSelectedDataView({ + id: dataViewId, + patterns: selectedPatterns, + scope: DataViewPickerScopeName.timeline, + }); + dispatch( timelineActions.createTimeline({ columns: defaultUdtHeaders, @@ -120,13 +130,14 @@ export const useCreateTimeline = ({ } }, [ - dispatch, globalTimeRange, + timelineFullScreen, + dispatch, dataViewId, selectedPatterns, - setTimelineFullScreen, - timelineFullScreen, + setSelectedDataView, timelineType, + setTimelineFullScreen, ] ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.tsx index d213eeaf8f5f0..ac08654129139 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.tsx @@ -16,15 +16,17 @@ import { useUserPrivileges } from '../../common/components/user_privileges'; import { StatefulOpenTimeline } from '../components/open_timeline'; import * as i18n from './translations'; import { SecurityPageName } from '../../app/types'; -import { useSourcererDataView } from '../../sourcerer/containers'; import { EmptyPrompt } from '../../common/components/empty_prompt'; import { SecurityRoutePageWrapper } from '../../common/components/security_route_page_wrapper'; +import { useDataView } from '../../data_view_picker/hooks/use_data_view'; +import { DataViewPickerScopeName } from '../../data_view_picker/constants'; export const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10; export const TimelinesPage = React.memo(() => { const { tabName } = useParams<{ pageName: SecurityPageName; tabName: string }>(); - const { indicesExist } = useSourcererDataView(); + const { indicesExist } = useDataView(DataViewPickerScopeName.default); + const { timelinePrivileges: { crud: canWriteTimeline }, } = useUserPrivileges(); From cc44f6cc504666116bce2f1849f2ef1be0730c01 Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 11 Feb 2025 15:04:21 +0100 Subject: [PATCH 003/115] --wip-- [skip ci] --- .../hooks/use_browser_fields.ts | 29 +++++++++++++++++++ .../hooks/use_init_data_view_picker.ts | 19 ++++++------ .../hooks/use_selected_patterns.ts | 16 ++++++++++ .../public/data_view_picker/redux/index.ts | 5 +++- .../components/modal/header/index.tsx | 10 +++++-- .../components/open_timeline/index.tsx | 5 +++- .../open_timeline/note_previews/index.tsx | 4 +-- .../timeline/data_providers/index.tsx | 6 ++-- .../components/timeline/kpi/kpi_container.tsx | 10 ++++--- .../timeline/query_bar/eql/index.tsx | 13 ++++----- .../components/timeline/query_bar/index.tsx | 7 +++-- .../timeline/search_or_filter/index.tsx | 4 +-- .../components/timeline/tabs/esql/index.tsx | 18 ++---------- .../components/timeline/tabs/pinned/index.tsx | 13 +++++---- .../components/timeline/tabs/query/index.tsx | 27 +++++++++-------- .../tabs/session/use_session_view.tsx | 4 +-- .../tabs/shared/use_timeline_columns.tsx | 4 +-- .../containers/use_timeline_data_filters.ts | 4 +-- 18 files changed, 123 insertions(+), 75 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_browser_fields.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_selected_patterns.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_browser_fields.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_browser_fields.ts new file mode 100644 index 0000000000000..1d8167e13bc32 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_browser_fields.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import type { BrowserFields } from '@kbn/timelines-plugin/common'; +import { type DataViewPickerScopeName } from '../constants'; +import { useDataView } from './use_data_view'; +import { getDataViewStateFromIndexFields } from '../../common/containers/source/use_data_view'; + +export const useBrowserFields = (scope: DataViewPickerScopeName): BrowserFields => { + const { dataView } = useDataView(scope); + + return useMemo(() => { + if (!dataView) { + return {}; + } + + const { browserFields } = getDataViewStateFromIndexFields( + dataView?.title ?? '', + dataView.fields + ); + + return browserFields; + }, [dataView]); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts index 6afdd7d9b1ad0..78560e3f4d003 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts @@ -9,10 +9,11 @@ import { useDispatch } from 'react-redux'; import { useEffect } from 'react'; import type { AnyAction, Dispatch, ListenerEffectAPI } from '@reduxjs/toolkit'; import { addListener, removeListener } from '@reduxjs/toolkit'; -import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public'; +import type { DataViewSpec, DataViewsServicePublic } from '@kbn/data-views-plugin/public'; import { shared, scopes, type RootState, selectDataViewAsync } from '../redux'; import { useKibana } from '../../common/lib/kibana'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewPickerScopeName } from '../constants'; +import { getDataViewStateFromIndexFields } from '../../common/containers/source/use_data_view'; const createDataViewsLoadingListener = (dependencies: { dataViews: DataViewsServicePublic }) => { return { @@ -43,22 +44,22 @@ const createDataViewSelectedListener = (dependencies: { dataViews: DataViewsServ console.log('selectDataViewAsync', action); try { + let dataViewSpec: DataViewSpec; + if (action.payload.id) { const dataViewById = await dependencies.dataViews.get(action.payload.id); - const dataViewSpec = dataViewById.toSpec(); - listenerApi.dispatch( - scopes[action.payload.scope].actions.setSelectedDataView(dataViewSpec) - ); + dataViewSpec = dataViewById.toSpec(); } else { const adhocDataView = await dependencies.dataViews.create({ id: 'adhoc', title: action.payload.patterns?.join(','), }); - const dataViewSpec = adhocDataView.toSpec(); - listenerApi.dispatch( - scopes[action.payload.scope].actions.setSelectedDataView(dataViewSpec) - ); + dataViewSpec = adhocDataView.toSpec(); } + + listenerApi.dispatch( + scopes[action.payload.scope].actions.setSelectedDataView(dataViewSpec) + ); } catch (error: unknown) { console.error(error); } diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_selected_patterns.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_selected_patterns.ts new file mode 100644 index 0000000000000..146099b926d1a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_selected_patterns.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import type { DataViewPickerScopeName } from '../constants'; +import { useDataView } from './use_data_view'; + +export const useSelectedPatterns = (scope: DataViewPickerScopeName): string[] => { + const { dataView } = useDataView(scope); + + return useMemo(() => dataView?.title?.split(',') ?? [], [dataView?.title]); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts index be3db07f919ee..ab96f0f788c36 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts @@ -122,8 +122,11 @@ export interface RootState { export const sourcererAdapterSelector = (scope: DataViewPickerScopeName) => createSelector([(state: RootState) => state.dataViewPicker], (dataViewPicker) => { + const scopedState = dataViewPicker[scope]; + return { - ...dataViewPicker[scope], + ...scopedState, + dataView: scopedState.dataView ? scopedState.dataView : { title: '', id: '' }, indicesExist: !!dataViewPicker[scope]?.dataView?.title?.split(',')?.length, }; }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.tsx index e42e856b9ca74..df7c978000acb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.tsx @@ -18,6 +18,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { getEsQueryConfig } from '@kbn/data-plugin/common'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; import styled from 'styled-components'; +import { useDataView } from '../../../../data_view_picker/hooks/use_data_view'; import { NewTimelineButton } from '../actions/new_timeline_button'; import { OpenTimelineButton } from '../actions/open_timeline_button'; import { APP_ID } from '../../../../../common'; @@ -31,9 +32,7 @@ import { createHistoryEntry } from '../../../../common/utils/global_query_string import { timelineActions } from '../../../store'; import type { State } from '../../../../common/store'; import { useKibana } from '../../../../common/lib/kibana'; -import { useSourcererDataView } from '../../../../sourcerer/containers'; import { combineQueries } from '../../../../common/lib/kuery'; -import { SourcererScopeName } from '../../../../sourcerer/store/model'; import * as i18n from '../translations'; import { AddToFavoritesButton } from '../../add_to_favorites'; import { TimelineSaveStatus } from '../../save_status'; @@ -41,6 +40,8 @@ import { InspectButton } from '../../../../common/components/inspect'; import { InputsModelId } from '../../../../common/store/inputs/constants'; import { AttachToCaseButton } from '../actions/attach_to_case_button'; import { SaveTimelineButton } from '../actions/save_timeline_button'; +import { useBrowserFields } from '../../../../data_view_picker/hooks/use_browser_fields'; +import { DataViewPickerScopeName } from '../../../../data_view_picker/constants'; const whiteSpaceNoWrapCSS = { 'white-space': 'nowrap' }; const autoOverflowXCSS = { 'overflow-x': 'auto' }; @@ -70,7 +71,10 @@ interface FlyoutHeaderPanelProps { export const TimelineModalHeader = React.memo( ({ timelineId, openToggleRef }) => { const dispatch = useDispatch(); - const { browserFields, sourcererDataView } = useSourcererDataView(SourcererScopeName.timeline); + + const { dataView: sourcererDataView } = useDataView(DataViewPickerScopeName.timeline); + const browserFields = useBrowserFields(DataViewPickerScopeName.timeline); + const { cases, uiSettings } = useKibana().services; const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index 7cd7cbe18927f..ab8ca1288e8dc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -56,6 +56,8 @@ import { useStartTransaction } from '../../../common/lib/apm/use_start_transacti import { TIMELINE_ACTIONS } from '../../../common/lib/apm/user_actions'; import { defaultUdtHeaders } from '../timeline/body/column_headers/default_headers'; import { timelineDefaults } from '../../store/defaults'; +import { useSelectedPatterns } from '@kbn/security-solution-plugin/public/data_view_picker/hooks/use_selected_patterns'; +import { useDataView } from '@kbn/security-solution-plugin/public/data_view_picker/hooks/use_data_view'; interface OwnProps { /** Displays open timeline in modal */ @@ -157,7 +159,8 @@ export const StatefulOpenTimelineComponent = React.memo( (state) => getTimeline(state, TimelineId.active)?.savedObjectId ?? '' ); - const { dataViewId, selectedPatterns } = useSourcererDataView(SourcererScopeName.timeline); + const { dataViewId } = useDataView(SourcererScopeName.timeline); + const selectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); const { customTemplateTimelineCount, diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx index 4080e254303a2..963b184a6415e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx @@ -20,6 +20,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useSelectedPatterns } from '../../../../data_view_picker/hooks/use_selected_patterns'; import { useKibana } from '../../../../common/lib/kibana'; import { DocumentDetailsRightPanelKey } from '../../../../flyout/document_details/shared/constants/panel_keys'; import type { TimelineResultNote } from '../types'; @@ -31,7 +32,6 @@ import * as i18n from './translations'; import { TimelineId } from '../../../../../common/types/timeline'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; -import { useSourcererDataView } from '../../../../sourcerer/containers'; import { useDeleteNote } from './hooks/use_delete_note'; import { getTimelineNoteSelector } from '../../timeline/tabs/notes/selectors'; import { DocumentEventTypes } from '../../../../common/lib/telemetry'; @@ -52,7 +52,7 @@ const ToggleEventDetailsButtonComponent: React.FC eventId, timelineId, }) => { - const { selectedPatterns } = useSourcererDataView(SourcererScopeName.timeline); + const selectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); const { telemetry } = useKibana().services; const { openFlyout } = useExpandableFlyoutApi(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx index 7a90b5254e445..746c511b23d4f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx @@ -13,9 +13,9 @@ import { v4 as uuidv4 } from 'uuid'; import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid'; import { EuiToolTip, EuiSuperSelect, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { SourcererScopeName } from '../../../../sourcerer/store/model'; -import { useSourcererDataView } from '../../../../sourcerer/containers'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { useBrowserFields } from '../../../../data_view_picker/hooks/use_browser_fields'; +import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper'; import { droppableTimelineProvidersPrefix } from '../../../../common/components/drag_and_drop/helpers'; @@ -106,7 +106,7 @@ const CustomTooltipDiv = styled.div` export const DataProviders = React.memo(({ timelineId }) => { const dispatch = useDispatch(); - const { browserFields } = useSourcererDataView(SourcererScopeName.timeline); + const browserFields = useBrowserFields(SourcererScopeName.timeline); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const dataProviders = useDeepEqualSelector( diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx index dce963737fb5a..11c69dc4a5698 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx @@ -11,8 +11,8 @@ import { useSelector } from 'react-redux'; import { getEsQueryConfig } from '@kbn/data-plugin/common'; import type { TimerangeInput } from '@kbn/timelines-plugin/common'; import { EuiPanel } from '@elastic/eui'; +import { useBrowserFields } from '../../../../data_view_picker/hooks/use_browser_fields'; import { TimelineId } from '../../../../../common/types'; -import { useSourcererDataView } from '../../../../sourcerer/containers'; import type { State } from '../../../../common/store'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; @@ -26,15 +26,17 @@ import { endSelector, startSelector, } from '../../../../common/components/super_date_picker/selectors'; +import { useSelectedPatterns } from '../../../../data_view_picker/hooks/use_selected_patterns'; +import { useDataView } from '../../../../data_view_picker/hooks/use_data_view'; interface KpiExpandedProps { timelineId: string; } export const TimelineKpisContainer = ({ timelineId }: KpiExpandedProps) => { - const { browserFields, sourcererDataView, selectedPatterns } = useSourcererDataView( - SourcererScopeName.timeline - ); + const browserFields = useBrowserFields(SourcererScopeName.timeline); + const { dataView: sourcererDataView } = useDataView(SourcererScopeName.timeline); + const selectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); const { uiSettings } = useKibana().services; const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/eql/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/eql/index.tsx index c378f5290cde2..b6257d848e5c3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/eql/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/eql/index.tsx @@ -11,8 +11,8 @@ import { EuiOutsideClickDetector } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import { css } from '@emotion/css'; +import { useDataView } from '../../../../../data_view_picker/hooks/use_data_view'; import type { EqlOptions } from '../../../../../../common/search_strategy'; -import { useSourcererDataView } from '../../../../../sourcerer/containers'; import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; import { SourcererScopeName } from '../../../../../sourcerer/store/model'; import { EqlQueryEdit } from '../../../../../detection_engine/rule_creation/components/eql_query_edit'; @@ -22,6 +22,7 @@ import type { FormSchema, FormSubmitHandler } from '../../../../../shared_import import { Form, UseField, useForm } from '../../../../../shared_imports'; import { timelineActions } from '../../../../store'; import { getEqlOptions } from './selectors'; +import { useSelectedPatterns } from '../../../../../data_view_picker/hooks/use_selected_patterns'; interface TimelineEqlQueryBar { index: string[]; @@ -59,11 +60,9 @@ export const EqlQueryBarTimeline = memo(({ timelineId }: { timelineId: string }) const getOptionsSelected = useMemo(() => getEqlOptions(), []); const eqlOptions = useDeepEqualSelector((state) => getOptionsSelected(state, timelineId)); - const { - loading: indexPatternsLoading, - sourcererDataView, - selectedPatterns, - } = useSourcererDataView(SourcererScopeName.timeline); + const { dataView: sourcererDataView, status } = useDataView(SourcererScopeName.timeline); + const selectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); + const indexPatternsLoading = status !== 'ready'; const initialState = useMemo( () => ({ @@ -165,7 +164,7 @@ export const EqlQueryBarTimeline = memo(({ timelineId }: { timelineId: string }) /* Force casting `sourcererDataView` to `DataViewBase` is required since EqlQueryEdit accepts DataViewBase but `useSourcererDataView()` returns `DataViewSpec`. - + When using `UseField` with `EqlQueryBar` such casting isn't required by TS since `UseField` component props are types as `Record`. */ return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx index 1bb39aa4796d2..5984065c7e47b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx @@ -13,8 +13,8 @@ import type { Filter, Query } from '@kbn/es-query'; import { FilterStateStore } from '@kbn/es-query'; import type { FilterManager, SavedQuery, SavedQueryTimeFilter } from '@kbn/data-plugin/public'; import styled from '@emotion/styled'; +import { useDataView } from '../../../../data_view_picker/hooks/use_data_view'; import { InputsModelId } from '../../../../common/store/inputs/constants'; -import { useSourcererDataView } from '../../../../sourcerer/containers'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { @@ -29,6 +29,7 @@ import type { DataProvider } from '../data_providers/data_provider'; import { TIMELINE_FILTER_DROP_AREA, buildGlobalQuery, getNonDropAreaFilters } from '../helpers'; import { timelineActions } from '../../../store'; import type { KueryFilterQuery, KueryFilterQueryKind } from '../../../../../common/types/timeline'; +import { useBrowserFields } from '../../../../data_view_picker/hooks/use_browser_fields'; export interface QueryBarTimelineComponentProps { dataProviders: DataProvider[]; @@ -110,7 +111,9 @@ export const QueryBarTimeline = memo( const [dateRangeTo, setDateRangTo] = useState( toStr != null ? toStr : new Date(to).toISOString() ); - const { browserFields, sourcererDataView } = useSourcererDataView(SourcererScopeName.timeline); + const { dataView: sourcererDataView } = useDataView(SourcererScopeName.timeline); + const browserFields = useBrowserFields(SourcererScopeName.timeline); + const [savedQuery, setSavedQuery] = useState(undefined); const [filterQueryConverted, setFilterQueryConverted] = useState({ query: filterQuery != null ? filterQuery.expression : '', diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx index d3233858813ae..bc225141b37c0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx @@ -17,11 +17,11 @@ import type { FilterManager } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; import { FilterItems } from '@kbn/unified-search-plugin/public'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useDataView } from '../../../../data_view_picker/hooks/use_data_view'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { useKibana } from '../../../../common/lib/kibana'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; -import { useSourcererDataView } from '../../../../sourcerer/containers'; import type { State, inputsModel } from '../../../../common/store'; import { inputsSelectors } from '../../../../common/store'; import { timelineActions, timelineSelectors } from '../../../store'; @@ -73,7 +73,7 @@ const StatefulSearchOrFilterComponent = React.memo( services: { data }, } = useKibana(); - const { sourcererDataView } = useSourcererDataView(SourcererScopeName.timeline); + const { dataView: sourcererDataView } = useDataView(SourcererScopeName.timeline); const getIsDataProviderVisible = useMemo( () => timelineSelectors.dataProviderVisibilitySelector(), diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx index 9e7386c5a7560..d040d69989649 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx @@ -12,15 +12,14 @@ import type { CustomizationCallback } from '@kbn/discover-plugin/public/customiz import { createGlobalStyle } from 'styled-components'; import type { ScopedHistory } from '@kbn/core/public'; import type { Subscription } from 'rxjs'; -import type { DataView } from '@kbn/data-views-plugin/common'; import { useQuery } from '@tanstack/react-query'; import { isEqualWith } from 'lodash'; import type { SavedSearch } from '@kbn/saved-search-plugin/common'; import type { TimeRange } from '@kbn/es-query'; import { useDispatch } from 'react-redux'; +import { useDataView } from '../../../../../data_view_picker/hooks/use_data_view'; import { updateSavedSearchId } from '../../../../store/actions'; import { useDiscoverInTimelineContext } from '../../../../../common/components/discover_in_timeline/use_discover_in_timeline_context'; -import { useSourcererDataView } from '../../../../../sourcerer/containers'; import { useKibana } from '../../../../../common/lib/kibana'; import { useDiscoverState } from './use_discover_state'; import { SourcererScopeName } from '../../../../../sourcerer/store/model'; @@ -46,12 +45,7 @@ interface DiscoverTabContentProps { export const DiscoverTabContent: FC = ({ timelineId }) => { const history = useHistory(); const { - services: { - customDataService: discoverDataService, - discover, - dataViews: dataViewService, - savedSearch: savedSearchService, - }, + services: { customDataService: discoverDataService, discover, savedSearch: savedSearchService }, } = useKibana(); const { timelinePrivileges: { crud: canSaveTimeline }, @@ -59,9 +53,8 @@ export const DiscoverTabContent: FC = ({ timelineId }) const dispatch = useDispatch(); - const { dataViewId } = useSourcererDataView(SourcererScopeName.detections); + const { dataView } = useDataView(SourcererScopeName.detections); - const [dataView, setDataView] = useState(); const [discoverTimerange, setDiscoverTimerange] = useState(); const discoverAppStateSubscription = useRef(); @@ -160,11 +153,6 @@ export const DiscoverTabContent: FC = ({ timelineId }) canSaveTimeline, ]); - useEffect(() => { - if (!dataViewId) return; - dataViewService.get(dataViewId).then(setDataView); - }, [dataViewId, dataViewService]); - useEffect(() => { const unSubscribeAll = () => { [ diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx index f70d51de3d755..42579d3ab303f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx @@ -13,6 +13,8 @@ import deepEqual from 'fast-deep-equal'; import type { EuiDataGridControlColumn } from '@elastic/eui'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy'; +import { useDataView } from '../../../../../data_view_picker/hooks/use_data_view'; +import { useSelectedPatterns } from '../../../../../data_view_picker/hooks/use_selected_patterns'; import { useFetchNotes } from '../../../../../notes/hooks/use_fetch_notes'; import { DocumentDetailsLeftPanelKey, @@ -26,7 +28,6 @@ import { requiredFieldsForActions } from '../../../../../detections/components/a import { SourcererScopeName } from '../../../../../sourcerer/store/model'; import { timelineDefaults } from '../../../../store/defaults'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; -import { useSourcererDataView } from '../../../../../sourcerer/containers'; import type { TimelineModel } from '../../../../store/model'; import type { State } from '../../../../../common/store'; import { TimelineTabs } from '../../../../../../common/types/timeline'; @@ -78,9 +79,9 @@ export const PinnedTabContentComponent: React.FC = ({ const [pageIndex, setPageIndex] = useState(0); const { telemetry } = useKibana().services; - const { dataViewId, sourcererDataView, selectedPatterns } = useSourcererDataView( - SourcererScopeName.timeline - ); + + const selectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); + const { dataView } = useDataView(SourcererScopeName.timeline); const filterQuery = useMemo(() => { if (isEmpty(pinnedEventIds)) { @@ -144,11 +145,11 @@ export const PinnedTabContentComponent: React.FC = ({ endDate: '', id: `pinned-${timelineId}`, indexNames: selectedPatterns, - dataViewId, + dataViewId: dataView.id ?? '', fields: timelineQueryFields, limit: itemsPerPage, filterQuery, - runtimeMappings: sourcererDataView.runtimeFieldMap as RunTimeMappings, + runtimeMappings: dataView.runtimeFieldMap as RunTimeMappings, skip: filterQuery === '', startDate: '', sort: timelineQuerySortField, diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx index 7a08a22ef5a81..0a8944a6006f2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx @@ -15,6 +15,9 @@ import { getEsQueryConfig } from '@kbn/data-plugin/common'; import { DataLoadingState } from '@kbn/unified-data-table'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy'; +import { useSelectedPatterns } from '../../../../../data_view_picker/hooks/use_selected_patterns'; +import { useBrowserFields } from '../../../../../data_view_picker/hooks/use_browser_fields'; +import { useDataView } from '../../../../../data_view_picker/hooks/use_data_view'; import { useFetchNotes } from '../../../../../notes/hooks/use_fetch_notes'; import { DocumentDetailsLeftPanelKey, @@ -39,7 +42,6 @@ import type { inputsModel, State } from '../../../../../common/store'; import { inputsSelectors } from '../../../../../common/store'; import { SourcererScopeName } from '../../../../../sourcerer/store/model'; import { timelineDefaults } from '../../../../store/defaults'; -import { useSourcererDataView } from '../../../../../sourcerer/containers'; import { isActiveTimeline } from '../../../../../helpers'; import type { TimelineModel } from '../../../../store/model'; import { UnifiedTimelineBody } from '../../body/unified_timeline_body'; @@ -83,15 +85,12 @@ export const QueryTabContentComponent: React.FC = ({ eventIdToNoteIds, }) => { const dispatch = useDispatch(); - const { - browserFields, - dataViewId, - loading: loadingSourcerer, - // important to get selectedPatterns from useSourcererDataView - // in order to include the exclude filters in the search that are not stored in the timeline - selectedPatterns, - sourcererDataView, - } = useSourcererDataView(SourcererScopeName.timeline); + + const { dataView, status: sourcererStatus } = useDataView(SourcererScopeName.timeline); + const browserFields = useBrowserFields(SourcererScopeName.timeline); + const loadingSourcerer = sourcererStatus !== 'ready'; + const selectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); + /* * `pageIndex` needs to be maintained for each table in each tab independently * and consequently it cannot be the part of common redux state @@ -126,13 +125,13 @@ export const QueryTabContentComponent: React.FC = ({ return combineQueries({ config: esQueryConfig, dataProviders, - indexPattern: sourcererDataView, + indexPattern: dataView, browserFields, filters, kqlQuery, kqlMode, }); - }, [esQueryConfig, dataProviders, sourcererDataView, browserFields, filters, kqlQuery, kqlMode]); + }, [esQueryConfig, dataProviders, dataView, browserFields, filters, kqlQuery, kqlMode]); useInvalidFilterQuery({ id: timelineId, @@ -174,7 +173,7 @@ export const QueryTabContentComponent: React.FC = ({ const [dataLoadingState, { events, inspect, totalCount, loadNextBatch, refreshedAt, refetch }] = useTimelineEvents({ - dataViewId, + dataViewId: dataView.id ?? '', endDate: end, fields: timelineQueryFieldsFromColumns, filterQuery: combinedQueries?.filterQuery, @@ -182,7 +181,7 @@ export const QueryTabContentComponent: React.FC = ({ indexNames: selectedPatterns, language: kqlQuery.language, limit: sampleSize, - runtimeMappings: sourcererDataView.runtimeFieldMap as RunTimeMappings, + runtimeMappings: dataView.runtimeFieldMap as RunTimeMappings, skip: !canQueryTimeline, sort: timelineQuerySortField, startDate: start, diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx index 6460dc220a934..c4b6ddc6b12fe 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx @@ -18,8 +18,8 @@ import { import { useDispatch } from 'react-redux'; import { dataTableSelectors, tableDefaults } from '@kbn/securitysolution-data-table'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useSelectedPatterns } from '../../../../../data_view_picker/hooks/use_selected_patterns'; import { DocumentDetailsRightPanelKey } from '../../../../../flyout/document_details/shared/constants/panel_keys'; -import { useSourcererDataView } from '../../../../../sourcerer/containers'; import { getScopedActions, isActiveTimeline, @@ -279,7 +279,7 @@ export const useSessionView = ({ scopeId, height }: { scopeId: string; height?: [globalFullScreen, scopeId, timelineFullScreen] ); - const { selectedPatterns } = useSourcererDataView(SourcererScopeName.detections); + const selectedPatterns = useSelectedPatterns(SourcererScopeName.detections); const alertsIndex = useMemo(() => selectedPatterns.join(','), [selectedPatterns]); const { openFlyout } = useExpandableFlyoutApi(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx index 006c6ba1eb679..6dc26bc50ac08 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx @@ -6,15 +6,15 @@ */ import { useMemo } from 'react'; +import { useBrowserFields } from '../../../../../data_view_picker/hooks/use_browser_fields'; import { SourcererScopeName } from '../../../../../sourcerer/store/model'; -import { useSourcererDataView } from '../../../../../sourcerer/containers'; import { requiredFieldsForActions } from '../../../../../detections/components/alerts_table/default_config'; import { defaultUdtHeaders } from '../../body/column_headers/default_headers'; import type { ColumnHeaderOptions } from '../../../../../../common/types'; import { memoizedGetTimelineColumnHeaders } from './utils'; export const useTimelineColumns = (columns: ColumnHeaderOptions[]) => { - const { browserFields } = useSourcererDataView(SourcererScopeName.timeline); + const browserFields = useBrowserFields(SourcererScopeName.timeline); const localColumns = useMemo(() => columns ?? defaultUdtHeaders, [columns]); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts index cd27ff06020ba..26682468a056b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts @@ -13,7 +13,7 @@ import { endSelector, } from '../../common/components/super_date_picker/selectors'; import { SourcererScopeName } from '../../sourcerer/store/model'; -import { useSourcererDataView } from '../../sourcerer/containers'; +import { useSelectedPatterns } from '../../data_view_picker/hooks/use_selected_patterns'; export function useTimelineDataFilters(isActiveTimelines: boolean) { const getStartSelector = useMemo(() => startSelector(), []); @@ -42,7 +42,7 @@ export function useTimelineDataFilters(isActiveTimelines: boolean) { } }); - const { selectedPatterns: analyzerPatterns } = useSourcererDataView(SourcererScopeName.analyzer); + const analyzerPatterns = useSelectedPatterns(SourcererScopeName.analyzer); return useMemo(() => { return { From d0c197017964d800241b0dca9da880da25e668cb Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 11 Feb 2025 15:13:48 +0100 Subject: [PATCH 004/115] --wip-- [skip ci] --- .../components/open_timeline/index.tsx | 11 +++++------ .../public/timelines/containers/index.tsx | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index ab8ca1288e8dc..d427f8cf4c89e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -9,6 +9,7 @@ import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { encode } from '@kbn/rison'; +import { useSelectedPatterns } from '../../../data_view_picker/hooks/use_selected_patterns'; import { RULE_FROM_EQL_URL_PARAM, RULE_FROM_TIMELINE_URL_PARAM, @@ -51,13 +52,11 @@ import { useTimelineStatus } from './use_timeline_status'; import { deleteTimelinesByIds } from '../../containers/api'; import type { Direction } from '../../../../common/search_strategy'; import { SourcererScopeName } from '../../../sourcerer/store/model'; -import { useSourcererDataView } from '../../../sourcerer/containers'; import { useStartTransaction } from '../../../common/lib/apm/use_start_transaction'; import { TIMELINE_ACTIONS } from '../../../common/lib/apm/user_actions'; import { defaultUdtHeaders } from '../timeline/body/column_headers/default_headers'; import { timelineDefaults } from '../../store/defaults'; -import { useSelectedPatterns } from '@kbn/security-solution-plugin/public/data_view_picker/hooks/use_selected_patterns'; -import { useDataView } from '@kbn/security-solution-plugin/public/data_view_picker/hooks/use_data_view'; +import { useDataView } from '../../../data_view_picker/hooks/use_data_view'; interface OwnProps { /** Displays open timeline in modal */ @@ -159,7 +158,7 @@ export const StatefulOpenTimelineComponent = React.memo( (state) => getTimeline(state, TimelineId.active)?.savedObjectId ?? '' ); - const { dataViewId } = useDataView(SourcererScopeName.timeline); + const { dataView } = useDataView(SourcererScopeName.timeline); const selectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); const { @@ -250,7 +249,7 @@ export const StatefulOpenTimelineComponent = React.memo( dispatchCreateNewTimeline({ id: TimelineId.active, columns: defaultUdtHeaders, - dataViewId, + dataViewId: dataView.id ?? '', indexNames: selectedPatterns, show: false, excludedRowRendererIds: timelineDefaults.excludedRowRendererIds, @@ -261,7 +260,7 @@ export const StatefulOpenTimelineComponent = React.memo( await deleteTimelinesByIds(timelineIds, searchIds); refetch(); }, - [startTransaction, timelineSavedObjectId, refetch, dispatch, dataViewId, selectedPatterns] + [startTransaction, timelineSavedObjectId, refetch, dispatch, dataView.id, selectedPatterns] ); const onDeleteOneTimeline: OnDeleteOneTimeline = useCallback( diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.tsx index 7b261981c4062..f4972210995c1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.tsx @@ -545,6 +545,23 @@ export const useTimelineEvents = ({ skip = false, timerangeKind, }: UseTimelineEventsProps): [DataLoadingState, TimelineArgs] => { + console.log('useTimelineEvents', { + dataViewId, + endDate, + eqlOptions, + id, + indexNames, + fields, + filterQuery, + runtimeMappings, + startDate, + language, + limit, + sort, + skip, + timerangeKind, + }); + const [eventsPerPage, setEventsPerPage] = useState([[]]); const [dataLoadingState, timelineResponse, timelineSearchHandler] = useTimelineEventsHandler({ dataViewId, From 99f434a570dae3599f50d417e30e23d60ede18d7 Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 11 Feb 2025 15:21:04 +0100 Subject: [PATCH 005/115] --wip-- [skip ci] --- .../public/data_view_picker/redux/index.ts | 1 + .../public/timelines/containers/index.tsx | 17 ----------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts index ab96f0f788c36..96f236bb7bc97 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts @@ -56,6 +56,7 @@ const createDataViewSelectionSlice = (scopeName: T) => reducers: { setSelectedDataView: (state, action: PayloadAction) => { state.dataView = action.payload; + state.status = 'ready'; }, }, extraReducers(builder) { diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.tsx index f4972210995c1..7b261981c4062 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.tsx @@ -545,23 +545,6 @@ export const useTimelineEvents = ({ skip = false, timerangeKind, }: UseTimelineEventsProps): [DataLoadingState, TimelineArgs] => { - console.log('useTimelineEvents', { - dataViewId, - endDate, - eqlOptions, - id, - indexNames, - fields, - filterQuery, - runtimeMappings, - startDate, - language, - limit, - sort, - skip, - timerangeKind, - }); - const [eventsPerPage, setEventsPerPage] = useState([[]]); const [dataLoadingState, timelineResponse, timelineSearchHandler] = useTimelineEventsHandler({ dataViewId, From 85d723337a19329337aeafa2ce1c2a0595c7f679 Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 11 Feb 2025 15:24:47 +0100 Subject: [PATCH 006/115] --wip-- [skip ci] --- .../data_view_picker/hooks/use_init_data_view_picker.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts index 78560e3f4d003..80a09f718099c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts @@ -13,7 +13,6 @@ import type { DataViewSpec, DataViewsServicePublic } from '@kbn/data-views-plugi import { shared, scopes, type RootState, selectDataViewAsync } from '../redux'; import { useKibana } from '../../common/lib/kibana'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewPickerScopeName } from '../constants'; -import { getDataViewStateFromIndexFields } from '../../common/containers/source/use_data_view'; const createDataViewsLoadingListener = (dependencies: { dataViews: DataViewsServicePublic }) => { return { @@ -49,7 +48,9 @@ const createDataViewSelectedListener = (dependencies: { dataViews: DataViewsServ if (action.payload.id) { const dataViewById = await dependencies.dataViews.get(action.payload.id); dataViewSpec = dataViewById.toSpec(); - } else { + } + + if (!dataViewSpec) { const adhocDataView = await dependencies.dataViews.create({ id: 'adhoc', title: action.payload.patterns?.join(','), From 73db16de2691a1ce906bda446cfc41d12e78a6ac Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 11 Feb 2025 15:25:40 +0100 Subject: [PATCH 007/115] --wip-- [skip ci] --- .../hooks/use_init_data_view_picker.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts index 80a09f718099c..3a0762d49702e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts @@ -42,14 +42,18 @@ const createDataViewSelectedListener = (dependencies: { dataViews: DataViewsServ ) => { console.log('selectDataViewAsync', action); - try { - let dataViewSpec: DataViewSpec; + let dataViewSpec: DataViewSpec; + try { if (action.payload.id) { const dataViewById = await dependencies.dataViews.get(action.payload.id); dataViewSpec = dataViewById.toSpec(); } + } catch (error: unknown) { + console.error(error); + } + try { if (!dataViewSpec) { const adhocDataView = await dependencies.dataViews.create({ id: 'adhoc', @@ -57,13 +61,11 @@ const createDataViewSelectedListener = (dependencies: { dataViews: DataViewsServ }); dataViewSpec = adhocDataView.toSpec(); } - - listenerApi.dispatch( - scopes[action.payload.scope].actions.setSelectedDataView(dataViewSpec) - ); } catch (error: unknown) { console.error(error); } + + listenerApi.dispatch(scopes[action.payload.scope].actions.setSelectedDataView(dataViewSpec)); }, } as any; }; From eadb7e616e10c75ae0cf4df04cf924789f8fceee Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 11 Feb 2025 15:30:06 +0100 Subject: [PATCH 008/115] --wip-- [skip ci] --- .../hooks/use_init_data_view_picker.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts index 3a0762d49702e..d9b47a3ea7b5e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts @@ -42,7 +42,10 @@ const createDataViewSelectedListener = (dependencies: { dataViews: DataViewsServ ) => { console.log('selectDataViewAsync', action); - let dataViewSpec: DataViewSpec; + let dataViewSpec: DataViewSpec | null = null; + + let searchError: unknown; + let adhocError: unknown; try { if (action.payload.id) { @@ -50,7 +53,7 @@ const createDataViewSelectedListener = (dependencies: { dataViews: DataViewsServ dataViewSpec = dataViewById.toSpec(); } } catch (error: unknown) { - console.error(error); + searchError = error; } try { @@ -62,10 +65,16 @@ const createDataViewSelectedListener = (dependencies: { dataViews: DataViewsServ dataViewSpec = adhocDataView.toSpec(); } } catch (error: unknown) { - console.error(error); + adhocError = error; } - listenerApi.dispatch(scopes[action.payload.scope].actions.setSelectedDataView(dataViewSpec)); + if (dataViewSpec) { + listenerApi.dispatch( + scopes[action.payload.scope].actions.setSelectedDataView(dataViewSpec) + ); + } else { + console.error('data view picker error', searchError ?? adhocError); + } }, } as any; }; From a0b30c765adcb55bfe6fb1f2467c41125434747d Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 11 Feb 2025 15:31:12 +0100 Subject: [PATCH 009/115] --wip-- [skip ci] --- .../security_solution/public/data_view_picker/redux/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts index 96f236bb7bc97..16ef76faaafdd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts @@ -55,6 +55,8 @@ const createDataViewSelectionSlice = (scopeName: T) => initialState: initialScopeState, reducers: { setSelectedDataView: (state, action: PayloadAction) => { + console.log('setSelectedDataView', action.payload); + state.dataView = action.payload; state.status = 'ready'; }, From 73ee667f0e4d316515848357362ae003e1b95cb9 Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 11 Feb 2025 16:29:07 +0100 Subject: [PATCH 010/115] --wip-- [skip ci] --- .../components/data_view_picker/index.tsx | 21 ++++++++++++--- .../hooks/use_init_data_view_picker.ts | 1 + .../public/data_view_picker/redux/index.ts | 27 +++++++++---------- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx index cf552ac9f4f78..f7296c941b1d3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx @@ -6,13 +6,14 @@ */ import { DataViewPicker as USDataViewPicker } from '@kbn/unified-search-plugin/public'; -import React, { useCallback, useRef, useMemo, memo } from 'react'; -import { useDispatch } from 'react-redux'; +import React, { useCallback, useRef, useMemo, memo, useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import type { DataView } from '@kbn/data-views-plugin/public'; import type { DataViewPickerScopeName } from '../../constants'; import { useKibana } from '../../../common/lib/kibana/kibana_react'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../../constants'; -import { selectDataViewAsync } from '../../redux'; +import { selectDataViewAsync, sharedStateSelector } from '../../redux'; import { useDataView } from '../../hooks/use_data_view'; export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) => { @@ -86,6 +87,19 @@ export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) = }; }, [dataView]); + const { adhocDataViews: adhocDataViewSpecs } = useSelector(sharedStateSelector); + + const [adhocDataViews, setAdhocDataViews] = useState([]); + + useEffect(() => { + (async () => { + const dataViews = await Promise.all( + adhocDataViewSpecs.map((dvSpec) => data.dataViews.create(dvSpec)) + ); + setAdhocDataViews(dataViews); + })(); + }, [data.dataViews, adhocDataViewSpecs]); + return ( ); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts index d9b47a3ea7b5e..c6e7fcaf920d3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts @@ -63,6 +63,7 @@ const createDataViewSelectedListener = (dependencies: { dataViews: DataViewsServ title: action.payload.patterns?.join(','), }); dataViewSpec = adhocDataView.toSpec(); + listenerApi.dispatch(shared.actions.addAdhocDataView(dataViewSpec)); } } catch (error: unknown) { adhocError = error; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts index 16ef76faaafdd..dc10ebb032384 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts @@ -29,7 +29,8 @@ export interface ScopedDataViewSelectionState { } export interface SharedDataViewSelectionState { - dataViews: Record; + dataViews: DataViewSpec[]; + adhocDataViews: DataViewSpec[]; status: 'pristine' | 'loading' | 'error' | 'ready'; } @@ -39,7 +40,8 @@ export const initialScopeState: ScopedDataViewSelectionState = { }; export const initialSharedState: SharedDataViewSelectionState = { - dataViews: {}, + dataViews: [], + adhocDataViews: [], status: 'pristine', }; @@ -77,15 +79,10 @@ export const shared = createSlice({ initialState: initialSharedState, reducers: { setDataViews: (state, action: PayloadAction) => { - state.dataViews = action.payload.reduce((viewsMap, dataView) => { - if (!dataView.id) { - return viewsMap; - } - - viewsMap[dataView.id] = dataView; - - return viewsMap; - }, {} as Record); + state.dataViews = action.payload; + }, + addAdhocDataView: (state, action: PayloadAction) => { + state.adhocDataViews.push(action.payload); }, init: (state) => { state.status = 'loading'; @@ -134,7 +131,7 @@ export const sourcererAdapterSelector = (scope: DataViewPickerScopeName) => }; }); -// export const sharedStateSelector = createSelector( -// [(state: RootState) => state.dataViewPicker], -// (dataViewPicker) => dataViewPicker.shared -// ); +export const sharedStateSelector = createSelector( + [(state: RootState) => state.dataViewPicker], + (dataViewPicker) => dataViewPicker.shared +); From df8d885f387073ca4f9dafe70949127587b7dd6a Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 11 Feb 2025 16:43:44 +0100 Subject: [PATCH 011/115] --wip-- [skip ci] --- .../security_solution/public/data_view_picker/redux/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts index dc10ebb032384..f3400c089a164 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts @@ -82,6 +82,10 @@ export const shared = createSlice({ state.dataViews = action.payload; }, addAdhocDataView: (state, action: PayloadAction) => { + if (state.adhocDataViews.find((dv) => dv.title === action.payload.title)) { + return; + } + state.adhocDataViews.push(action.payload); }, init: (state) => { From 6cf58d634ebb143e9391ec326f491e0f6d4b238a Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 11 Feb 2025 17:06:43 +0100 Subject: [PATCH 012/115] fix stephbeat --- .../public/data_view_picker/hooks/use_init_data_view_picker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts index c6e7fcaf920d3..6bafcb6097c97 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts @@ -59,7 +59,7 @@ const createDataViewSelectedListener = (dependencies: { dataViews: DataViewsServ try { if (!dataViewSpec) { const adhocDataView = await dependencies.dataViews.create({ - id: 'adhoc', + id: 'adhoc' + action.payload.patterns?.join(','), title: action.payload.patterns?.join(','), }); dataViewSpec = adhocDataView.toSpec(); From c39eb3b2ae8fb18418770b83f79d2097056ac912 Mon Sep 17 00:00:00 2001 From: lgestc Date: Wed, 12 Feb 2025 09:44:37 +0100 Subject: [PATCH 013/115] --wip-- [skip ci] --- .../public/common/store/store.ts | 4 +- .../components/data_view_picker/index.tsx | 3 +- .../data_view_picker/hooks/use_data_view.ts | 2 +- .../hooks/use_init_data_view_picker.ts | 74 +------------------ .../hooks/use_select_data_view.ts | 4 +- .../redux/listeners/data_view_selected.ts | 59 +++++++++++++++ .../redux/listeners/init_listener.ts | 30 ++++++++ .../redux/{effects => }/middleware.ts | 0 .../redux/{index.ts => reducer.ts} | 52 ++----------- .../data_view_picker/redux/selectors.ts | 27 +++++++ .../public/data_view_picker/redux/types.ts | 26 +++++++ .../open_timeline/use_update_timeline.tsx | 14 +--- 12 files changed, 163 insertions(+), 132 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.ts rename x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/{effects => }/middleware.ts (100%) rename x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/{index.ts => reducer.ts} (69%) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/selectors.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/types.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts b/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts index 43c8e6aa2ea06..fc79754749d34 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts @@ -56,8 +56,8 @@ import { createMiddlewares } from './middlewares'; import { addNewTimeline } from '../../timelines/store/helpers'; import { initialNotesState } from '../../notes/store/notes.slice'; import { hasAccessToSecuritySolution } from '../../helpers_access'; -import { dataViewPickerReducer } from '../../data_view_picker/redux'; -import { listenerMiddleware } from '../../data_view_picker/redux/effects/middleware'; +import { dataViewPickerReducer } from '../../data_view_picker/redux/reducer'; +import { listenerMiddleware } from '../../data_view_picker/redux/middleware'; let store: Store | null = null; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx index f7296c941b1d3..37818daca64b2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx @@ -13,8 +13,9 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { DataViewPickerScopeName } from '../../constants'; import { useKibana } from '../../../common/lib/kibana/kibana_react'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../../constants'; -import { selectDataViewAsync, sharedStateSelector } from '../../redux'; +import { selectDataViewAsync } from '../../redux/reducer'; import { useDataView } from '../../hooks/use_data_view'; +import { sharedStateSelector } from '../../redux/selectors'; export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) => { const dispatch = useDispatch(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_data_view.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_data_view.ts index 23c9509e5c1c8..32ec53e744360 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_data_view.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_data_view.ts @@ -7,7 +7,7 @@ import { useSelector } from 'react-redux'; import type { DataViewPickerScopeName } from '../constants'; -import { sourcererAdapterSelector } from '../redux'; +import { sourcererAdapterSelector } from '../redux/selectors'; /** * Returns data view selection for given scopeName diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts index 6bafcb6097c97..bf5d9088f2daa 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts @@ -7,78 +7,12 @@ import { useDispatch } from 'react-redux'; import { useEffect } from 'react'; -import type { AnyAction, Dispatch, ListenerEffectAPI } from '@reduxjs/toolkit'; import { addListener, removeListener } from '@reduxjs/toolkit'; -import type { DataViewSpec, DataViewsServicePublic } from '@kbn/data-views-plugin/public'; -import { shared, scopes, type RootState, selectDataViewAsync } from '../redux'; +import { shared, selectDataViewAsync } from '../redux/reducer'; import { useKibana } from '../../common/lib/kibana'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewPickerScopeName } from '../constants'; - -const createDataViewsLoadingListener = (dependencies: { dataViews: DataViewsServicePublic }) => { - return { - actionCreator: shared.actions.init, - effect: async ( - _action: AnyAction, - listenerApi: ListenerEffectAPI> - ) => { - try { - const dataViews = await dependencies.dataViews.getAllDataViewLazy(); - const dataViewSpecs = await Promise.all(dataViews.map((dataView) => dataView.toSpec())); - - listenerApi.dispatch(shared.actions.setDataViews(dataViewSpecs)); - } catch (error: unknown) { - listenerApi.dispatch(shared.actions.error()); - } - }, - } as any; -}; - -const createDataViewSelectedListener = (dependencies: { dataViews: DataViewsServicePublic }) => { - return { - actionCreator: selectDataViewAsync, - effect: async ( - action: ReturnType, - listenerApi: ListenerEffectAPI> - ) => { - console.log('selectDataViewAsync', action); - - let dataViewSpec: DataViewSpec | null = null; - - let searchError: unknown; - let adhocError: unknown; - - try { - if (action.payload.id) { - const dataViewById = await dependencies.dataViews.get(action.payload.id); - dataViewSpec = dataViewById.toSpec(); - } - } catch (error: unknown) { - searchError = error; - } - - try { - if (!dataViewSpec) { - const adhocDataView = await dependencies.dataViews.create({ - id: 'adhoc' + action.payload.patterns?.join(','), - title: action.payload.patterns?.join(','), - }); - dataViewSpec = adhocDataView.toSpec(); - listenerApi.dispatch(shared.actions.addAdhocDataView(dataViewSpec)); - } - } catch (error: unknown) { - adhocError = error; - } - - if (dataViewSpec) { - listenerApi.dispatch( - scopes[action.payload.scope].actions.setSelectedDataView(dataViewSpec) - ); - } else { - console.error('data view picker error', searchError ?? adhocError); - } - }, - } as any; -}; +import { createDataViewSelectedListener } from '../redux/listeners/data_view_selected'; +import { createInitListener } from '../redux/listeners/init_listener'; /** * Should only be used once in the application, on the top level of the rendering tree @@ -88,7 +22,7 @@ export const useInitDataViewPicker = () => { const services = useKibana().services; useEffect(() => { - const dataViewsLoadingListener = createDataViewsLoadingListener({ + const dataViewsLoadingListener = createInitListener({ dataViews: services.dataViews, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts index 3edfbf844ee11..aef7f8b657863 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts @@ -8,13 +8,13 @@ import { useDispatch } from 'react-redux'; import { useCallback } from 'react'; import type { DataViewPickerScopeName } from '../constants'; -import { selectDataViewAsync } from '../redux'; +import { selectDataViewAsync } from '../redux/reducer'; export const useSelectDataView = () => { const dispatch = useDispatch(); return useCallback( - (params: { id?: string; patterns?: string[]; scope: DataViewPickerScopeName }) => { + (params: { id?: string | null; patterns?: string[]; scope: DataViewPickerScopeName }) => { dispatch(selectDataViewAsync(params)); }, [dispatch] diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts new file mode 100644 index 0000000000000..aff44144d360a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DataViewSpec, DataViewsServicePublic } from '@kbn/data-views-plugin/public'; +import type { AnyAction, Dispatch, ListenerEffectAPI } from '@reduxjs/toolkit'; +import type { RootState } from '../reducer'; +import { scopes, selectDataViewAsync, shared } from '../reducer'; + +export const createDataViewSelectedListener = (dependencies: { + dataViews: DataViewsServicePublic; +}) => { + return { + actionCreator: selectDataViewAsync, + effect: async ( + action: ReturnType, + listenerApi: ListenerEffectAPI> + ) => { + const currentScopeActions = scopes[action.payload.scope].actions; + + let dataViewSpec: DataViewSpec | null = null; + + let dataViewByIdError: unknown; + let adhocDataViewCreationError: unknown; + + try { + if (action.payload.id) { + const dataViewById = await dependencies.dataViews.get(action.payload.id); + dataViewSpec = dataViewById.toSpec(); + } + } catch (error: unknown) { + dataViewByIdError = error; + } + + try { + if (!dataViewSpec) { + const title = action.payload.patterns?.join(',') ?? ''; + const adhocDataView = await dependencies.dataViews.create({ + id: `adhoc_${title}`, + title, + }); + dataViewSpec = adhocDataView.toSpec(); + listenerApi.dispatch(shared.actions.addAdhocDataView(dataViewSpec)); + } + } catch (error: unknown) { + adhocDataViewCreationError = error; + } + + if (dataViewSpec) { + listenerApi.dispatch(currentScopeActions.setSelectedDataView(dataViewSpec)); + } else { + listenerApi.dispatch(currentScopeActions.dataViewSelectionError()); + } + }, + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.ts new file mode 100644 index 0000000000000..2e72ba8cbb01f --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AnyAction, Dispatch, ListenerEffectAPI } from '@reduxjs/toolkit'; +import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public'; +import type { RootState } from '../reducer'; +import { shared } from '../reducer'; + +export const createInitListener = (dependencies: { dataViews: DataViewsServicePublic }) => { + return { + actionCreator: shared.actions.init, + effect: async ( + _action: AnyAction, + listenerApi: ListenerEffectAPI> + ) => { + try { + const dataViews = await dependencies.dataViews.getAllDataViewLazy(); + const dataViewSpecs = await Promise.all(dataViews.map((dataView) => dataView.toSpec())); + + listenerApi.dispatch(shared.actions.setDataViews(dataViewSpecs)); + } catch (error: unknown) { + listenerApi.dispatch(shared.actions.error()); + } + }, + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/effects/middleware.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/middleware.ts similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/effects/middleware.ts rename to x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/middleware.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts similarity index 69% rename from x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts rename to x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts index f3400c089a164..3ec29e370fb1e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts @@ -6,33 +6,12 @@ */ import type { DataViewSpec } from '@kbn/data-views-plugin/common'; -import { - combineReducers, - createSlice, - createSelector, - createAction, - type PayloadAction, -} from '@reduxjs/toolkit'; +import type { AnyAction } from '@reduxjs/toolkit'; +import { combineReducers, createAction, createSlice, type PayloadAction } from '@reduxjs/toolkit'; import { DataViewPickerScopeName, SLICE_PREFIX } from '../constants'; - -export interface ScopedDataViewSelectionState { - dataView: DataViewSpec | null; - /** - * There are several states the picker can be in internally: - * - pristine - not initialized yet - * - loading - * - error - some kind of a problem during data init - * - ready - ready to provide index information to the client - */ - status: 'pristine' | 'loading' | 'error' | 'ready'; -} - -export interface SharedDataViewSelectionState { - dataViews: DataViewSpec[]; - adhocDataViews: DataViewSpec[]; - status: 'pristine' | 'loading' | 'error' | 'ready'; -} +import type { SharedDataViewSelectionState } from './types'; +import { type ScopedDataViewSelectionState } from './types'; export const initialScopeState: ScopedDataViewSelectionState = { dataView: null, @@ -46,7 +25,7 @@ export const initialSharedState: SharedDataViewSelectionState = { }; export const selectDataViewAsync = createAction<{ - id?: string; + id?: string | null; patterns?: string[]; scope: DataViewPickerScopeName; }>(`${SLICE_PREFIX}/selectDataView`); @@ -57,11 +36,12 @@ const createDataViewSelectionSlice = (scopeName: T) => initialState: initialScopeState, reducers: { setSelectedDataView: (state, action: PayloadAction) => { - console.log('setSelectedDataView', action.payload); - state.dataView = action.payload; state.status = 'ready'; }, + dataViewSelectionError: (state, action: AnyAction) => { + state.status = 'error'; + }, }, extraReducers(builder) { builder.addCase(selectDataViewAsync, (state, action) => { @@ -123,19 +103,3 @@ export type DataviewPickerState = ReturnType; export interface RootState { dataViewPicker: DataviewPickerState; } - -export const sourcererAdapterSelector = (scope: DataViewPickerScopeName) => - createSelector([(state: RootState) => state.dataViewPicker], (dataViewPicker) => { - const scopedState = dataViewPicker[scope]; - - return { - ...scopedState, - dataView: scopedState.dataView ? scopedState.dataView : { title: '', id: '' }, - indicesExist: !!dataViewPicker[scope]?.dataView?.title?.split(',')?.length, - }; - }); - -export const sharedStateSelector = createSelector( - [(state: RootState) => state.dataViewPicker], - (dataViewPicker) => dataViewPicker.shared -); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/selectors.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/selectors.ts new file mode 100644 index 0000000000000..036fd1889161b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/selectors.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createSelector } from '@reduxjs/toolkit'; + +import type { DataViewPickerScopeName } from '../constants'; +import type { RootState } from './reducer'; + +export const sourcererAdapterSelector = (scope: DataViewPickerScopeName) => + createSelector([(state: RootState) => state.dataViewPicker], (dataViewPicker) => { + const scopedState = dataViewPicker[scope]; + + return { + ...scopedState, + dataView: scopedState.dataView ? scopedState.dataView : { title: '', id: '' }, + indicesExist: !!dataViewPicker[scope]?.dataView?.title?.split(',')?.length, + }; + }); + +export const sharedStateSelector = createSelector( + [(state: RootState) => state.dataViewPicker], + (dataViewPicker) => dataViewPicker.shared +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/types.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/types.ts new file mode 100644 index 0000000000000..e885d7ab6cdf8 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/types.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DataViewSpec } from '@kbn/data-views-plugin/common'; + +export interface ScopedDataViewSelectionState { + dataView: DataViewSpec | null; + /** + * There are several states the picker can be in internally: + * - pristine - not initialized yet + * - loading + * - error - some kind of a problem during data init + * - ready - ready to provide index information to the client + */ + status: 'pristine' | 'loading' | 'error' | 'ready'; +} + +export interface SharedDataViewSelectionState { + dataViews: DataViewSpec[]; + adhocDataViews: DataViewSpec[]; + status: 'pristine' | 'loading' | 'error' | 'ready'; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx index 9b0d202403852..4401844f5288f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx @@ -15,8 +15,6 @@ import { TimelineStatusEnum, TimelineTypeEnum } from '../../../../common/api/tim import { createNote } from '../notes/helpers'; import { InputsModelId } from '../../../common/store/inputs/constants'; -import { sourcererActions } from '../../../sourcerer/store'; -import { SourcererScopeName } from '../../../sourcerer/store/model'; import { addNotes as dispatchAddNotes, updateNote as dispatchUpdateNote, @@ -61,16 +59,8 @@ export const useUpdateTimeline = () => { _timeline = { ...timeline, updated: undefined, changed: true, version: null }; } if (!isEmpty(_timeline.indexNames)) { - dispatch( - sourcererActions.setSelectedDataView({ - id: SourcererScopeName.timeline, - selectedDataViewId: _timeline.dataViewId, - selectedPatterns: _timeline.indexNames, - }) - ); - selectDataView({ - id: _timeline.dataViewId ?? undefined, + id: _timeline.dataViewId, patterns: _timeline.indexNames, scope: DataViewPickerScopeName.timeline, }); @@ -147,6 +137,6 @@ export const useUpdateTimeline = () => { ); } }, - [dispatch] + [dispatch, selectDataView] ); }; From 7020ef4018320e631f31582e2ff43e2c0657f3a0 Mon Sep 17 00:00:00 2001 From: lgestc Date: Wed, 12 Feb 2025 09:46:47 +0100 Subject: [PATCH 014/115] --wip-- [skip ci] --- .../public/data_view_picker/redux/listeners/readme.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/readme.md diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/readme.md b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/readme.md new file mode 100644 index 0000000000000..49eba0b170eca --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/readme.md @@ -0,0 +1,3 @@ +# Side effects for data view picker + +We are currently using redux toolkit listener middleware to implement side effects logic in Data View Picker for Security Solution plugin. \ No newline at end of file From 3d47008547b6627fc9d37176f2157ae4c8a10018 Mon Sep 17 00:00:00 2001 From: lgestc Date: Wed, 12 Feb 2025 09:54:28 +0100 Subject: [PATCH 015/115] --wip-- [skip ci] --- .../timelines/hooks/use_create_timeline.tsx | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx index 06781679baeac..d753f87979860 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx @@ -6,7 +6,7 @@ */ import { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { InputsModelId } from '../../common/store/inputs/constants'; import { timelineActions } from '../store'; import { useTimelineFullScreen } from '../../common/containers/use_full_screen'; @@ -14,8 +14,6 @@ import { TimelineId } from '../../../common/types/timeline'; import { type TimelineType, TimelineTypeEnum } from '../../../common/api/timeline'; import { useDeepEqualSelector } from '../../common/hooks/use_selector'; import { inputsActions, inputsSelectors } from '../../common/store/inputs'; -import { sourcererActions, sourcererSelectors } from '../../sourcerer/store'; -import { SourcererScopeName } from '../../sourcerer/store/model'; import { appActions } from '../../common/store/app'; import type { TimeRange } from '../../common/store/inputs/model'; import { useDiscoverInTimelineContext } from '../../common/components/discover_in_timeline/use_discover_in_timeline_context'; @@ -23,6 +21,8 @@ import { defaultUdtHeaders } from '../components/timeline/body/column_headers/de import { timelineDefaults } from '../store/defaults'; import { useSelectDataView } from '../../data_view_picker/hooks/use_select_data_view'; import { DataViewPickerScopeName } from '../../data_view_picker/constants'; +import { useDataView } from '../../data_view_picker/hooks/use_data_view'; +import { useSelectedPatterns } from '../../data_view_picker/hooks/use_selected_patterns'; export interface UseCreateTimelineParams { /** @@ -50,9 +50,9 @@ export const useCreateTimeline = ({ onClick, }: UseCreateTimelineParams): ((options?: { timeRange?: TimeRange }) => Promise) => { const dispatch = useDispatch(); - const { id: dataViewId, patternList: selectedPatterns } = useSelector( - sourcererSelectors.defaultDataView - ) ?? { id: '', patternList: [] }; + const { dataView } = useDataView(DataViewPickerScopeName.default); + const dataViewId = dataView.id ?? ''; + const selectedPatterns = useSelectedPatterns(DataViewPickerScopeName.default); const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); const globalTimeRange = useDeepEqualSelector(inputsSelectors.globalTimeRangeSelector); @@ -76,13 +76,6 @@ export const useCreateTimeline = ({ if (id === TimelineId.active && timelineFullScreen) { setTimelineFullScreen(false); } - dispatch( - sourcererActions.setSelectedDataView({ - id: SourcererScopeName.timeline, - selectedDataViewId: dataViewId, - selectedPatterns, - }) - ); setSelectedDataView({ id: dataViewId, From 83667ba8d132cfe9bfa883cfdb5dd0249edccfde Mon Sep 17 00:00:00 2001 From: lgestc Date: Wed, 12 Feb 2025 10:05:53 +0100 Subject: [PATCH 016/115] --wip-- [skip ci] --- .../timeline/use_investigate_in_timeline.ts | 25 ++++++++++++------- .../timeline/use_show_timeline_for_path.ts | 5 ++-- .../use_add_bulk_to_timeline.tsx | 23 ++++++++--------- .../components/timeline/tabs/eql/index.tsx | 1 + 4 files changed, 31 insertions(+), 23 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts index c1c195b0c5965..8aa2a3c58f8c9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts @@ -8,12 +8,12 @@ import { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import type { Filter, Query } from '@kbn/es-query'; +import { useSelectDataView } from '../../../data_view_picker/hooks/use_select_data_view'; import { useCreateTimeline } from '../../../timelines/hooks/use_create_timeline'; import { updateProviders, setFilters, applyKqlFilterQuery } from '../../../timelines/store/actions'; import { SourcererScopeName } from '../../../sourcerer/store/model'; import type { DataProvider } from '../../../../common/types'; import { sourcererSelectors } from '../../store'; -import { sourcererActions } from '../../store/actions'; import { inputsActions } from '../../store/inputs'; import { InputsModelId } from '../../store/inputs/constants'; import type { TimeRange } from '../../store/inputs/model'; @@ -68,6 +68,8 @@ export const useInvestigateInTimeline = () => { timelineType: TimelineTypeEnum.default, }); + const setSelectedDataView = useSelectDataView(); + const investigateInTimeline = useCallback( async ({ query, @@ -124,19 +126,24 @@ export const useInvestigateInTimeline = () => { // Only show detection alerts // (This is required so the timeline event count matches the prevalence count) if (!keepDataView) { - dispatch( - sourcererActions.setSelectedDataView({ - id: SourcererScopeName.timeline, - selectedDataViewId: defaultDataView.id, - selectedPatterns: [signalIndexName || ''], - }) - ); + setSelectedDataView({ + scope: SourcererScopeName.timeline, + id: defaultDataView.id, + patterns: [signalIndexName || ''], + }); } // Unlock the time range from the global time range dispatch(inputsActions.removeLinkTo([InputsModelId.timeline, InputsModelId.global])); } }, - [clearTimelineTemplate, clearTimelineDefault, dispatch, defaultDataView.id, signalIndexName] + [ + clearTimelineTemplate, + clearTimelineDefault, + dispatch, + setSelectedDataView, + defaultDataView.id, + signalIndexName, + ] ); return { investigateInTimeline }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts index 30f5b078770b5..d26367d24ad5f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts @@ -8,9 +8,9 @@ import { useCallback, useMemo } from 'react'; import { matchPath } from 'react-router-dom'; +import { useDataView } from '../../../data_view_picker/hooks/use_data_view'; import { getLinksWithHiddenTimeline } from '../../links'; import { SourcererScopeName } from '../../../sourcerer/store/model'; -import { useSourcererDataView } from '../../../sourcerer/containers'; import { useKibana } from '../../lib/kibana'; import { hasAccessToSecuritySolution } from '../../../helpers_access'; @@ -21,13 +21,14 @@ const isTimelinePathVisible = (currentPath: string): boolean => { }; export const useShowTimelineForGivenPath = () => { - const { indicesExist, dataViewId } = useSourcererDataView(SourcererScopeName.timeline); + const { indicesExist, dataView } = useDataView(SourcererScopeName.timeline); const { services: { application: { capabilities }, }, } = useKibana(); const userHasSecuritySolutionVisible = hasAccessToSecuritySolution(capabilities); + const dataViewId = dataView?.id ?? ''; const isTimelineAllowed = useMemo( () => userHasSecuritySolutionVisible && (indicesExist || dataViewId === null), diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx index e402dfe2488fa..407ceb220ea43 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx @@ -13,11 +13,13 @@ import { getEsQueryConfig } from '@kbn/data-plugin/public'; import type { BulkActionsConfig } from '@kbn/triggers-actions-ui-plugin/public/types'; import { dataTableActions, TableId, tableDefaults } from '@kbn/securitysolution-data-table'; import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy'; +import { useSelectedPatterns } from '../../../../data_view_picker/hooks/use_selected_patterns'; +import { useBrowserFields } from '../../../../data_view_picker/hooks/use_browser_fields'; +import { useDataView } from '../../../../data_view_picker/hooks/use_data_view'; import type { CustomBulkAction } from '../../../../../common/types'; import { combineQueries } from '../../../../common/lib/kuery'; import { useKibana } from '../../../../common/lib/kibana'; import { BULK_ADD_TO_TIMELINE_LIMIT } from '../../../../../common/constants'; -import { useSourcererDataView } from '../../../../sourcerer/containers'; import type { TimelineArgs } from '../../../../timelines/containers'; import { useTimelineEventsHandler } from '../../../../timelines/containers'; import { eventsViewerSelector } from '../../../../common/components/events_viewer/selectors'; @@ -63,14 +65,11 @@ export const useAddBulkToTimelineAction = ({ }: UseAddBulkToTimelineActionProps) => { const [disableActionOnSelectAll, setDisabledActionOnSelectAll] = useState(false); - const { - browserFields, - dataViewId, - sourcererDataView, - // important to get selectedPatterns from useSourcererDataView - // in order to include the exclude filters in the search that are not stored in the timeline - selectedPatterns, - } = useSourcererDataView(scopeId); + const { dataView } = useDataView(scopeId); + const browserFields = useBrowserFields(scopeId); + const selectedPatterns = useSelectedPatterns(scopeId); + const dataViewId = dataView?.id ?? ''; + const dispatch = useDispatch(); const { uiSettings } = useKibana().services; @@ -94,13 +93,13 @@ export const useAddBulkToTimelineAction = ({ return combineQueries({ config: esQueryConfig, dataProviders: [], - indexPattern: sourcererDataView, + indexPattern: dataView, filters: combinedFilters, kqlQuery: { query: '', language: 'kuery' }, browserFields, kqlMode: 'filter', }); - }, [esQueryConfig, sourcererDataView, combinedFilters, browserFields]); + }, [esQueryConfig, dataView, combinedFilters, browserFields]); const filterQuery = useMemo(() => { if (!combinedQuery) return ''; @@ -118,7 +117,7 @@ export const useAddBulkToTimelineAction = ({ sort: timelineQuerySortField, indexNames: selectedPatterns, filterQuery, - runtimeMappings: sourcererDataView.runtimeFieldMap as RunTimeMappings, + runtimeMappings: dataView.runtimeFieldMap as RunTimeMappings, limit: Math.min(BULK_ADD_TO_TIMELINE_LIMIT, totalCount), timerangeKind: 'absolute', }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx index 72e887c53b4f5..1b86e7f6526bf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx @@ -32,6 +32,7 @@ import type { inputsModel, State } from '../../../../../common/store'; import { inputsSelectors } from '../../../../../common/store'; import { SourcererScopeName } from '../../../../../sourcerer/store/model'; import { timelineDefaults } from '../../../../store/defaults'; +// FIXME: why is it even needed here? @mike import { useSourcererDataView } from '../../../../../sourcerer/containers'; import { useEqlEventsCountPortal } from '../../../../../common/hooks/use_timeline_events_count'; import type { TimelineModel } from '../../../../store/model'; From 28aa4f0793c3089bfcde7f4d708cdbf764f4b42a Mon Sep 17 00:00:00 2001 From: lgestc Date: Wed, 12 Feb 2025 11:21:04 +0100 Subject: [PATCH 017/115] --wip-- [skip ci] --- .../components/data_view_picker/index.tsx | 34 +++++++++----- .../redux/listeners/data_view_selected.ts | 45 ++++++++++++++----- .../public/data_view_picker/redux/reducer.ts | 22 ++++++--- 3 files changed, 72 insertions(+), 29 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx index 37818daca64b2..eabe7456fd8b3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx @@ -9,11 +9,11 @@ import { DataViewPicker as USDataViewPicker } from '@kbn/unified-search-plugin/p import React, { useCallback, useRef, useMemo, memo, useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import type { DataView } from '@kbn/data-views-plugin/public'; +import type { DataView, DataViewListItem } from '@kbn/data-views-plugin/public'; import type { DataViewPickerScopeName } from '../../constants'; import { useKibana } from '../../../common/lib/kibana/kibana_react'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../../constants'; -import { selectDataViewAsync } from '../../redux/reducer'; +import { selectDataViewAsync, shared } from '../../redux/reducer'; import { useDataView } from '../../hooks/use_data_view'; import { sharedStateSelector } from '../../redux/selectors'; @@ -36,13 +36,16 @@ export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) = const dataViewId = dataView?.id; - const createNewDataView = useCallback(async () => { - closeDataViewEditor.current = await dataViewEditor.openEditor({ - // eslint-disable-next-line no-console - onSave: () => console.log('new data view saved'), + const createNewDataView = useCallback(() => { + closeDataViewEditor.current = dataViewEditor.openEditor({ + onSave: async (newDataView) => { + dispatch(shared.actions.addDataView(newDataView)); + dispatch(selectDataViewAsync({ id: newDataView.id, scope: props.scope })); + // TODO: reload data views + }, allowAdHocDataView: true, }); - }, [dataViewEditor]); + }, [dataViewEditor, dispatch, props.scope]); const onFieldEdited = useCallback(() => {}, []); @@ -88,18 +91,26 @@ export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) = }; }, [dataView]); - const { adhocDataViews: adhocDataViewSpecs } = useSelector(sharedStateSelector); + const { adhocDataViews: adhocDataViewSpecs, dataViews } = useSelector(sharedStateSelector); const [adhocDataViews, setAdhocDataViews] = useState([]); + const [managedDataViews, setManagedDataViews] = useState([]); useEffect(() => { (async () => { - const dataViews = await Promise.all( + const adhoc = await Promise.all( adhocDataViewSpecs.map((dvSpec) => data.dataViews.create(dvSpec)) ); - setAdhocDataViews(dataViews); + setAdhocDataViews(adhoc); + + const managed: DataViewListItem[] = dataViews.map((spec) => ({ + id: spec.id ?? '', + title: spec.title ?? '', + name: spec.name, + })); + setManagedDataViews(managed); })(); - }, [data.dataViews, adhocDataViewSpecs]); + }, [data.dataViews, adhocDataViewSpecs, dataViews]); return ( ); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts index aff44144d360a..d40b09d0089ec 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts @@ -19,34 +19,55 @@ export const createDataViewSelectedListener = (dependencies: { action: ReturnType, listenerApi: ListenerEffectAPI> ) => { + const state = listenerApi.getState(); + const currentScopeActions = scopes[action.payload.scope].actions; - let dataViewSpec: DataViewSpec | null = null; + const findCachedDataView = (id: string | null | undefined) => { + const savedDataView = state.dataViewPicker.shared.dataViews.find((dv) => dv.id === id); + + if (savedDataView) { + return savedDataView; + } + + return state.dataViewPicker.shared.adhocDataViews.find((dv) => dv.id === id) ?? null; + }; let dataViewByIdError: unknown; let adhocDataViewCreationError: unknown; - try { - if (action.payload.id) { - const dataViewById = await dependencies.dataViews.get(action.payload.id); - dataViewSpec = dataViewById.toSpec(); + /** + * Try to locate the data view in cached entries first + */ + let dataViewSpec: DataViewSpec | null = findCachedDataView(action.payload.id); + + if (!dataViewSpec) { + try { + if (action.payload.id) { + const dataViewById = await dependencies.dataViews.get(action.payload.id); + dataViewSpec = dataViewById.toSpec(); + } + } catch (error: unknown) { + dataViewByIdError = error; } - } catch (error: unknown) { - dataViewByIdError = error; } - try { - if (!dataViewSpec) { + if (!dataViewSpec) { + try { const title = action.payload.patterns?.join(',') ?? ''; + if (!title.length) { + throw new Error('empty adhoc title field'); + } + const adhocDataView = await dependencies.dataViews.create({ id: `adhoc_${title}`, title, }); + listenerApi.dispatch(shared.actions.addDataView(adhocDataView)); dataViewSpec = adhocDataView.toSpec(); - listenerApi.dispatch(shared.actions.addAdhocDataView(dataViewSpec)); + } catch (error: unknown) { + adhocDataViewCreationError = error; } - } catch (error: unknown) { - adhocDataViewCreationError = error; } if (dataViewSpec) { diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts index 3ec29e370fb1e..6133a7b242f39 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { DataViewSpec } from '@kbn/data-views-plugin/common'; +import type { DataViewSpec, DataView } from '@kbn/data-views-plugin/common'; import type { AnyAction } from '@reduxjs/toolkit'; import { combineReducers, createAction, createSlice, type PayloadAction } from '@reduxjs/toolkit'; @@ -61,12 +61,22 @@ export const shared = createSlice({ setDataViews: (state, action: PayloadAction) => { state.dataViews = action.payload; }, - addAdhocDataView: (state, action: PayloadAction) => { - if (state.adhocDataViews.find((dv) => dv.title === action.payload.title)) { - return; - } + addDataView: (state, action: PayloadAction) => { + const dataViewSpec = action.payload.toSpec(); + + if (action.payload.isPersisted()) { + if (state.dataViews.find((dv) => dv.id === dataViewSpec.id)) { + return; + } - state.adhocDataViews.push(action.payload); + state.dataViews.push(dataViewSpec); + } else { + if (state.adhocDataViews.find((dv) => dv.title === dataViewSpec.title)) { + return; + } + + state.adhocDataViews.push(dataViewSpec); + } }, init: (state) => { state.status = 'loading'; From f92eab58737099402460b48e79cde973c539371d Mon Sep 17 00:00:00 2001 From: lgestc Date: Wed, 12 Feb 2025 11:59:17 +0100 Subject: [PATCH 018/115] --wip-- [skip ci] --- .../hooks/use_init_data_view_picker.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts index bf5d9088f2daa..405b02247dea5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts @@ -7,13 +7,31 @@ import { useDispatch } from 'react-redux'; import { useEffect } from 'react'; -import { addListener, removeListener } from '@reduxjs/toolkit'; +import type { AnyAction, Dispatch, ListenerEffectAPI } from '@reduxjs/toolkit'; +import { + addListener as originalAddListener, + removeListener as originalRemoveListener, +} from '@reduxjs/toolkit'; +import type { RootState } from '../redux/reducer'; import { shared, selectDataViewAsync } from '../redux/reducer'; import { useKibana } from '../../common/lib/kibana'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewPickerScopeName } from '../constants'; import { createDataViewSelectedListener } from '../redux/listeners/data_view_selected'; import { createInitListener } from '../redux/listeners/init_listener'; +type OriginalListener = Parameters[0]; + +interface Listener { + actionCreator?: unknown; + effect: (action: Action, listenerApi: ListenerEffectAPI) => void; +} + +const addListener = (listener: Listener) => + originalAddListener(listener as unknown as OriginalListener); + +const removeListener = (listener: Listener) => + originalRemoveListener(listener as unknown as OriginalListener); + /** * Should only be used once in the application, on the top level of the rendering tree */ From 0805163530a04cc4b09c41aebcf7410a6580539f Mon Sep 17 00:00:00 2001 From: lgestc Date: Wed, 12 Feb 2025 12:16:37 +0100 Subject: [PATCH 019/115] --wip-- [skip ci] --- .../data_view_picker/components/data_view_picker/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx index eabe7456fd8b3..9ce183e16411c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { DataViewPicker as USDataViewPicker } from '@kbn/unified-search-plugin/public'; +import { DataViewPicker as UnifiedDataViewPicker } from '@kbn/unified-search-plugin/public'; import React, { useCallback, useRef, useMemo, memo, useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -113,7 +113,7 @@ export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) = }, [data.dataViews, adhocDataViewSpecs, dataViews]); return ( - Date: Wed, 12 Feb 2025 15:33:11 +0100 Subject: [PATCH 020/115] --wip-- [skip ci] --- .../components/data_view_picker/index.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx index 9ce183e16411c..18a3efb935179 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx @@ -86,8 +86,14 @@ export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) = const handleEditDataView = useCallback(() => {}, []); const triggerConfig = useMemo(() => { + if (dataView?.id === DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID) { + return { + label: 'Default Security Data View', + }; + } + return { - label: dataView?.name ?? dataView?.id ?? 'Data view', + label: dataView?.name || dataView?.id || 'Data view', }; }, [dataView]); From f49c643f9d4a02eb82a6e69b6cf7a18218e78433 Mon Sep 17 00:00:00 2001 From: lgestc Date: Fri, 14 Feb 2025 09:39:21 +0100 Subject: [PATCH 021/115] fix types --- .../data_view_picker/redux/listeners/data_view_selected.ts | 6 ++++-- .../public/data_view_picker/redux/reducer.ts | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts index d40b09d0089ec..e5efe8ccf63ba 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts @@ -72,8 +72,10 @@ export const createDataViewSelectedListener = (dependencies: { if (dataViewSpec) { listenerApi.dispatch(currentScopeActions.setSelectedDataView(dataViewSpec)); - } else { - listenerApi.dispatch(currentScopeActions.dataViewSelectionError()); + } else if (dataViewByIdError || adhocDataViewCreationError) { + listenerApi.dispatch( + currentScopeActions.dataViewSelectionError('An error occured when setting data view') + ); } }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts index 6133a7b242f39..297ec82486941 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts @@ -6,7 +6,6 @@ */ import type { DataViewSpec, DataView } from '@kbn/data-views-plugin/common'; -import type { AnyAction } from '@reduxjs/toolkit'; import { combineReducers, createAction, createSlice, type PayloadAction } from '@reduxjs/toolkit'; import { DataViewPickerScopeName, SLICE_PREFIX } from '../constants'; @@ -39,7 +38,7 @@ const createDataViewSelectionSlice = (scopeName: T) => state.dataView = action.payload; state.status = 'ready'; }, - dataViewSelectionError: (state, action: AnyAction) => { + dataViewSelectionError: (state, action: PayloadAction) => { state.status = 'error'; }, }, From 63054f5fb28fdea1f9410a0b17de88647ed49eb1 Mon Sep 17 00:00:00 2001 From: lgestc Date: Fri, 14 Feb 2025 11:56:42 +0100 Subject: [PATCH 022/115] remove todo --- .../plugins/security_solution/public/data_view_picker/readme.md | 0 .../public/timelines/components/timeline/tabs/eql/index.tsx | 1 - 2 files changed, 1 deletion(-) delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/readme.md diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/readme.md b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/readme.md deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx index 1b86e7f6526bf..72e887c53b4f5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx @@ -32,7 +32,6 @@ import type { inputsModel, State } from '../../../../../common/store'; import { inputsSelectors } from '../../../../../common/store'; import { SourcererScopeName } from '../../../../../sourcerer/store/model'; import { timelineDefaults } from '../../../../store/defaults'; -// FIXME: why is it even needed here? @mike import { useSourcererDataView } from '../../../../../sourcerer/containers'; import { useEqlEventsCountPortal } from '../../../../../common/hooks/use_timeline_events_count'; import type { TimelineModel } from '../../../../store/model'; From c7e0665c179bb96ed54d8a84416a3275d74d113b Mon Sep 17 00:00:00 2001 From: lgestc Date: Fri, 14 Feb 2025 12:46:52 +0100 Subject: [PATCH 023/115] fixing tests --- .../public/common/mock/global_state.ts | 2 ++ .../security_solution/public/common/store/reducer.ts | 6 ++++++ .../security_solution/public/common/store/store.ts | 1 - .../security_solution/public/common/store/types.ts | 3 ++- .../public/data_view_picker/redux/mock.ts | 10 ++++++++++ .../public/data_view_picker/redux/reducer.ts | 10 ++++++++++ 6 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/solutions/security/plugins/security_solution/public/common/mock/global_state.ts index 79bd0eb558683..1c8c4d13beded 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/mock/global_state.ts @@ -48,6 +48,7 @@ import { initialGroupingState } from '../store/grouping/reducer'; import type { SourcererState } from '../../sourcerer/store'; import { EMPTY_RESOLVER } from '../../resolver/store/helpers'; import { getMockDiscoverInTimelineState } from './mock_discover_state'; +import { mockDataViewPickerState } from '../../data_view_picker/redux/mock'; const mockFieldMap: DataViewSpec['fields'] = Object.fromEntries( mockIndexFields.map((field) => [field.name, field]) @@ -554,4 +555,5 @@ export const mockGlobalState: State = { selectedIds: [], pendingDeleteIds: [], }, + ...mockDataViewPickerState, }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/store/reducer.ts b/x-pack/solutions/security/plugins/security_solution/public/common/store/reducer.ts index ea684909a8776..189cb1cea2a35 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/store/reducer.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/store/reducer.ts @@ -37,6 +37,10 @@ import { securitySolutionDiscoverReducer } from './discover/reducer'; import type { AnalyzerState } from '../../resolver/types'; import type { NotesState } from '../../notes/store/notes.slice'; import { notesReducer } from '../../notes/store/notes.slice'; +import { + dataViewPickerReducer, + initialDataViewPickerState, +} from '../../data_view_picker/redux/reducer'; enableMapSet(); @@ -132,6 +136,7 @@ export const createInitialState = ( savedSearch: undefined, }, notes: notesState, + dataViewPicker: initialDataViewPickerState.dataViewPicker, }; return preloadedState; @@ -155,4 +160,5 @@ export const createReducer: ( discover: securitySolutionDiscoverReducer, ...pluginsReducer, notes: notesReducer, + dataViewPicker: dataViewPickerReducer, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts b/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts index fc79754749d34..bdde8a547e180 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts @@ -180,7 +180,6 @@ export const createStoreFactory = async ( ...subPlugins.explore.store.reducer, timeline: timelineReducer, ...subPlugins.management.store.reducer, - dataViewPicker: dataViewPickerReducer, }; return createStore(initialState, rootReducer, coreStart, storage, [ diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/store/types.ts b/x-pack/solutions/security/plugins/security_solution/public/common/store/types.ts index bf83f9146bdb2..960d4935fca01 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/store/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/store/types.ts @@ -26,6 +26,7 @@ import type { GroupState } from './grouping/types'; import type { SecuritySolutionDiscoverState } from './discover/model'; import type { AnalyzerState } from '../../resolver/types'; import type { NotesState } from '../../notes/store/notes.slice'; +import type { RootState as DataViewPickerState } from '../../data_view_picker/redux/reducer'; export type State = HostsPluginState & UsersPluginState & @@ -40,7 +41,7 @@ export type State = HostsPluginState & discover: SecuritySolutionDiscoverState; } & DataTableState & GroupState & - AnalyzerState & { notes: NotesState }; + AnalyzerState & { notes: NotesState } & DataViewPickerState; /** * The Redux store type for the Security app. */ diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts new file mode 100644 index 0000000000000..0b2b07f6dabed --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { initialDataViewPickerState, type RootState } from './reducer'; + +export const mockDataViewPickerState: RootState = structuredClone(initialDataViewPickerState); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts index 297ec82486941..0706bd1a8e654 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts @@ -112,3 +112,13 @@ export type DataviewPickerState = ReturnType; export interface RootState { dataViewPicker: DataviewPickerState; } + +export const initialDataViewPickerState: RootState = { + dataViewPicker: { + shared: initialSharedState, + [DataViewPickerScopeName.default]: initialScopeState, + [DataViewPickerScopeName.timeline]: initialScopeState, + [DataViewPickerScopeName.detections]: initialScopeState, + [DataViewPickerScopeName.analyzer]: initialScopeState, + }, +}; From f261ab86c999358cf086a12492b3f8f0c05aa0a4 Mon Sep 17 00:00:00 2001 From: lgestc Date: Fri, 14 Feb 2025 12:47:17 +0100 Subject: [PATCH 024/115] remove unused import --- .../plugins/security_solution/public/common/store/store.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts b/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts index bdde8a547e180..11c644e0e35ac 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts @@ -56,7 +56,6 @@ import { createMiddlewares } from './middlewares'; import { addNewTimeline } from '../../timelines/store/helpers'; import { initialNotesState } from '../../notes/store/notes.slice'; import { hasAccessToSecuritySolution } from '../../helpers_access'; -import { dataViewPickerReducer } from '../../data_view_picker/redux/reducer'; import { listenerMiddleware } from '../../data_view_picker/redux/middleware'; let store: Store | null = null; From 8c3e9a056ed052aef6906a33c7da68359f6dd8ae Mon Sep 17 00:00:00 2001 From: lgestc Date: Fri, 14 Feb 2025 14:16:07 +0100 Subject: [PATCH 025/115] --wip-- [skip ci] --- .../components/with_data_view/index.tsx | 8 ++-- .../hooks/use_full_data_view.ts | 41 +++++++++++++++++++ .../public/data_view_picker/redux/mock.ts | 19 ++++++++- .../timeline/tabs/query/index.test.tsx | 10 +---- 4 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.tsx index 41d3b451a61e6..f03d0aee2c06d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.tsx @@ -9,9 +9,9 @@ import React from 'react'; import type { ComponentType } from 'react'; import type { ReactElement } from 'react-markdown'; import type { DataView } from '@kbn/data-views-plugin/common'; +import { DataViewPickerScopeName } from '../../../data_view_picker/constants'; +import { useFullDataView } from '../../../data_view_picker/hooks/use_full_data_view'; import { DataViewErrorComponent } from './data_view_error'; -import { useGetScopedSourcererDataView } from '../../../sourcerer/components/use_get_sourcerer_data_view'; -import { SourcererScopeName } from '../../../sourcerer/store/model'; type OmitDataView = T extends { dataView: DataView } ? Omit : T; @@ -30,8 +30,8 @@ export const withDataView =

( fallback?: ReactElement ) => { const ComponentWithDataView = (props: OmitDataView

) => { - const dataView = useGetScopedSourcererDataView({ - sourcererScope: SourcererScopeName.timeline, + const dataView = useFullDataView({ + dataViewPickerScope: DataViewPickerScopeName.timeline, }); if (!dataView) { diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.ts new file mode 100644 index 0000000000000..6b83dbdfaaa89 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { DataView } from '@kbn/data-views-plugin/public'; +import { useKibana } from '../../common/lib/kibana'; +import { type DataViewPickerScopeName } from '../constants'; +import { useDataView } from './use_data_view'; + +export interface UseGetScopedSourcererDataViewArgs { + dataViewPickerScope: DataViewPickerScopeName; +} + +/* + * + * returns the created dataView based on dataView spec + * returned from useDataView + * + * */ +export const useFullDataView = ({ + dataViewPickerScope, +}: UseGetScopedSourcererDataViewArgs): DataView | undefined => { + const { + services: { fieldFormats }, + } = useKibana(); + const { dataView: dataViewSpec } = useDataView(dataViewPickerScope); + + const dataView = useMemo(() => { + if (Object.keys(dataViewSpec).length) { + return new DataView({ spec: dataViewSpec, fieldFormats }); + } else { + return undefined; + } + }, [dataViewSpec, fieldFormats]); + + return dataView; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts index 0b2b07f6dabed..2fcd62296435d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts @@ -5,6 +5,23 @@ * 2.0. */ +import type { DataViewSpec } from '@kbn/data-views-plugin/common'; +import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../constants'; import { initialDataViewPickerState, type RootState } from './reducer'; -export const mockDataViewPickerState: RootState = structuredClone(initialDataViewPickerState); +const dataViewPickerState = structuredClone(initialDataViewPickerState).dataViewPicker; + +const mockDefaultDataViewSpec: DataViewSpec = { + fields: {}, + id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, + title: '', +}; +export const mockDataViewPickerState: RootState = { + dataViewPicker: { + ...dataViewPickerState, + timeline: { + ...dataViewPickerState.timeline, + dataView: mockDefaultDataViewSpec, + }, + }, +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx index 746b3b1ceb1dd..51481660a3273 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx @@ -11,8 +11,6 @@ import QueryTabContent from '.'; import { defaultRowRenderers } from '../../body/renderers'; import { TimelineId } from '../../../../../../common/types/timeline'; import { useTimelineEventsDetails } from '../../../../containers/details'; -import { useSourcererDataView } from '../../../../../sourcerer/containers'; -import { mockSourcererScope } from '../../../../../sourcerer/containers/mocks'; import { createMockStore, createSecuritySolutionStorageMock, @@ -175,10 +173,6 @@ const renderTestComponents = (props?: Partial { (useTimelineEventsDetails as jest.Mock).mockImplementation(() => [false, {}]); - (useSourcererDataView as jest.Mock).mockImplementation(useSourcererDataViewMocked); - (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation( useIsExperimentalFeatureEnabledMock ); @@ -257,7 +249,7 @@ describe('query tab with unified timeline', () => { }); describe('render', () => { - it( + it.only( 'should render unifiedDataTable in timeline', async () => { renderTestComponents(); From 23439aa015a00fab53b1c542882532f3595d603d Mon Sep 17 00:00:00 2001 From: lgestc Date: Fri, 14 Feb 2025 14:27:30 +0100 Subject: [PATCH 026/115] --wip-- [skip ci] --- .../security_solution/public/data_view_picker/redux/mock.ts | 1 + .../timelines/components/timeline/tabs/query/index.test.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts index 2fcd62296435d..d32ad41a9e01a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts @@ -22,6 +22,7 @@ export const mockDataViewPickerState: RootState = { timeline: { ...dataViewPickerState.timeline, dataView: mockDefaultDataViewSpec, + status: 'ready', }, }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx index 51481660a3273..5828807649228 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx @@ -249,7 +249,7 @@ describe('query tab with unified timeline', () => { }); describe('render', () => { - it.only( + it( 'should render unifiedDataTable in timeline', async () => { renderTestComponents(); From 770993c4371c90c7b10a7e290a12427145c733ca Mon Sep 17 00:00:00 2001 From: lgestc Date: Fri, 14 Feb 2025 14:35:09 +0100 Subject: [PATCH 027/115] --wip-- [skip ci] --- .../public/data_view_picker/redux/mock.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts index d32ad41a9e01a..3cd28b79ce498 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts @@ -8,11 +8,16 @@ import type { DataViewSpec } from '@kbn/data-views-plugin/common'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../constants'; import { initialDataViewPickerState, type RootState } from './reducer'; +import { mockIndexFields } from '../../common/containers/source/mock'; const dataViewPickerState = structuredClone(initialDataViewPickerState).dataViewPicker; +const mockFieldMap: DataViewSpec['fields'] = Object.fromEntries( + mockIndexFields.map((field) => [field.name, field]) +); + const mockDefaultDataViewSpec: DataViewSpec = { - fields: {}, + fields: mockFieldMap, id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, title: '', }; From fa80256797800b3dc48bfc41379c9669751eccd5 Mon Sep 17 00:00:00 2001 From: lgestc Date: Mon, 17 Feb 2025 10:47:09 +0100 Subject: [PATCH 028/115] fix data view test --- .../components/with_data_view/index.test.tsx | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.test.tsx index 9313f20784474..d93013ba0f6c0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.test.tsx @@ -8,14 +8,15 @@ import React, { useEffect } from 'react'; import type { DataView } from '@kbn/data-views-plugin/common'; import { render, screen } from '@testing-library/react'; import { withDataView } from '.'; -import { useGetScopedSourcererDataView } from '../../../sourcerer/components/use_get_sourcerer_data_view'; +import { TestProvidersComponent } from '../../mock'; +import { useFullDataView } from '../../../data_view_picker/hooks/use_full_data_view'; + +jest.mock('../../../data_view_picker/hooks/use_full_data_view'); interface TestComponentProps { dataView: DataView; } -jest.mock('../../../sourcerer/components/use_get_sourcerer_data_view'); - const TEST_ID = { DATA_VIEW_ERROR_COMPONENT: 'dataViewErrorComponent', TEST_COMPONENT: 'test_component', @@ -34,24 +35,38 @@ const TestComponent = (props: TestComponentProps) => { }; describe('withDataViewId', () => { - beforeEach(() => { - (useGetScopedSourcererDataView as jest.Mock).mockReturnValue(undefined); - }); - it('should render default error components when there is not fallback provided and dataViewId is null', async () => { - const RenderedComponent = withDataView(TestComponent); - render(); - expect(screen.getByTestId(TEST_ID.DATA_VIEW_ERROR_COMPONENT)).toBeVisible(); - }); - it('should render provided fallback and dataViewId is null', async () => { - const RenderedComponent = withDataView(TestComponent, ); - render(); - expect(screen.getByTestId(TEST_ID.FALLBACK_COMPONENT)).toBeVisible(); + describe('when data view is null', () => { + beforeEach(() => {}); + + it('should render provided fallback', async () => { + const RenderedComponent = withDataView(TestComponent, ); + render(, { wrapper: TestProvidersComponent }); + expect(screen.getByTestId(TEST_ID.FALLBACK_COMPONENT)).toBeVisible(); + }); + + it('should render default error components when there is not fallback provided', async () => { + const RenderedComponent = withDataView(TestComponent); + render(, { wrapper: TestProvidersComponent }); + expect(screen.getByTestId(TEST_ID.DATA_VIEW_ERROR_COMPONENT)).toBeVisible(); + }); }); - it('should render provided component when dataViewId is not null', async () => { - (useGetScopedSourcererDataView as jest.Mock).mockReturnValue({ id: 'test' }); - const RenderedComponent = withDataView(TestComponent); - render(); - expect(screen.getByTestId(TEST_ID.TEST_COMPONENT)).toBeVisible(); - expect(dataViewMockFn).toHaveBeenCalledWith({ id: 'test' }); + + describe('when data view is set', () => { + beforeEach(() => { + jest + .mocked(useFullDataView) + .mockImplementation( + jest.requireActual('../../../data_view_picker/hooks/use_full_data_view').useFullDataView + ); + }); + + it('should render provided component when dataViewId is not null', async () => { + const RenderedComponent = withDataView(TestComponent); + render(, { wrapper: TestProvidersComponent }); + expect(screen.getByTestId(TEST_ID.TEST_COMPONENT)).toBeVisible(); + expect(dataViewMockFn).toHaveBeenCalledWith( + expect.objectContaining({ id: 'security-solution-default' }) + ); + }); }); }); From 2baf50b8c8e8ae389d70b28459cdd65547db259f Mon Sep 17 00:00:00 2001 From: lgestc Date: Mon, 17 Feb 2025 11:21:52 +0100 Subject: [PATCH 029/115] fix create timeline tests --- .../public/data_view_picker/redux/mock.ts | 18 ++++++++++++++- .../hooks/use_create_timeline.test.tsx | 22 +++++++++++++------ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts index 3cd28b79ce498..94badaaca55cc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts @@ -19,7 +19,18 @@ const mockFieldMap: DataViewSpec['fields'] = Object.fromEntries( const mockDefaultDataViewSpec: DataViewSpec = { fields: mockFieldMap, id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, - title: '', + title: [ + '.siem-signals-spacename', + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'traces-apm*', + 'winlogbeat-*', + '-*elastic-cloud-logs-*', + ].join(), }; export const mockDataViewPickerState: RootState = { dataViewPicker: { @@ -29,5 +40,10 @@ export const mockDataViewPickerState: RootState = { dataView: mockDefaultDataViewSpec, status: 'ready', }, + default: { + ...dataViewPickerState.default, + dataView: mockDefaultDataViewSpec, + status: 'ready', + }, }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx index 0864c2bb024bd..d41279e08c167 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx @@ -13,12 +13,19 @@ import { TimelineId } from '../../../common/types'; import { useDiscoverInTimelineContext } from '../../common/components/discover_in_timeline/use_discover_in_timeline_context'; import { timelineActions } from '../store'; import { inputsActions } from '../../common/store/inputs'; -import { sourcererActions } from '../../sourcerer/store'; import { appActions } from '../../common/store/app'; import { SourcererScopeName } from '../../sourcerer/store/model'; import { InputsModelId } from '../../common/store/inputs/constants'; import { TestProviders, mockGlobalState } from '../../common/mock'; import { defaultUdtHeaders } from '../components/timeline/body/column_headers/default_headers'; +import { useSelectDataView } from '../../data_view_picker/hooks/use_select_data_view'; + +jest.mock('../../data_view_picker/hooks/use_select_data_view', () => { + const mock = jest.fn(); + return { + useSelectDataView: () => mock, + }; +}); jest.mock('../../common/components/discover_in_timeline/use_discover_in_timeline_context'); jest.mock('../../common/containers/use_global_time', () => { @@ -55,10 +62,11 @@ describe('useCreateTimeline', () => { it('should dispatch correct actions when calling the returned function', async () => { const createTimeline = jest.spyOn(timelineActions, 'createTimeline'); - const setSelectedDataView = jest.spyOn(sourcererActions, 'setSelectedDataView'); const addLinkTo = jest.spyOn(inputsActions, 'addLinkTo'); const addNotes = jest.spyOn(appActions, 'addNotes'); + const setSelectedDataView = jest.mocked(useSelectDataView()); + const hookResult = renderHook( () => useCreateTimeline({ timelineId: TimelineId.test, timelineType: TimelineTypeEnum.default }), @@ -76,7 +84,7 @@ describe('useCreateTimeline', () => { expect(createTimeline.mock.calls[0][0].timelineType).toEqual(TimelineTypeEnum.default); expect(createTimeline.mock.calls[0][0].columns).toEqual(defaultUdtHeaders); expect(createTimeline.mock.calls[0][0].dataViewId).toEqual( - mockGlobalState.sourcerer.defaultDataView.id + mockGlobalState.dataViewPicker.default.dataView?.id ); expect(createTimeline.mock.calls[0][0].indexNames).toEqual( expect.arrayContaining( @@ -86,11 +94,11 @@ describe('useCreateTimeline', () => { expect(createTimeline.mock.calls[0][0].show).toEqual(true); expect(createTimeline.mock.calls[0][0].updated).toEqual(undefined); expect(createTimeline.mock.calls[0][0].excludedRowRendererIds).toHaveLength(RowRendererCount); - 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].scope).toEqual(SourcererScopeName.timeline); + expect(setSelectedDataView.mock.calls[0][0].id).toEqual( + mockGlobalState.dataViewPicker.default.dataView?.id ); - expect(setSelectedDataView.mock.calls[0][0].selectedPatterns).toEqual( + expect(setSelectedDataView.mock.calls[0][0].patterns).toEqual( expect.arrayContaining( mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline].selectedPatterns ) From 40b395c758aaab91a1439d5a006d482aaae6e4de Mon Sep 17 00:00:00 2001 From: lgestc Date: Mon, 17 Feb 2025 12:20:16 +0100 Subject: [PATCH 030/115] fix unit tests --- .../utils/timeline/use_show_timeline.test.tsx | 82 ++++++++++++------- .../timeline/use_show_timeline_for_path.ts | 4 +- 2 files changed, 56 insertions(+), 30 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx index 9bc192880ea89..d80f42e7c6352 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx @@ -13,9 +13,24 @@ import { appLinks } from '../../../app_links'; import { useUserPrivileges } from '../../components/user_privileges'; import { useShowTimeline } from './use_show_timeline'; import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks'; +import { TestProviders } from '../../mock'; +import { useDataView } from '../../../data_view_picker/hooks/use_data_view'; +import { hasAccessToSecuritySolution } from '../../../helpers_access'; + +jest.mock('../../../data_view_picker/hooks/use_data_view', () => ({ + useDataView: jest.fn().mockReturnValue({ + indicesExist: true, + dataView: { + title: '', + }, + status: 'ready', + }), +})); jest.mock('../../components/user_privileges'); +jest.mock('../../../helpers_access', () => ({ hasAccessToSecuritySolution: jest.fn(() => true) })); + const mockUseLocation = jest.fn().mockReturnValue({ pathname: '/overview' }); jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -25,17 +40,8 @@ jest.mock('react-router-dom', () => { }; }); -const mockUseSourcererDataView = jest.fn( - (): { indicesExist: boolean; dataViewId: string | null } => ({ - indicesExist: true, - dataViewId: null, - }) -); -jest.mock('../../../sourcerer/containers', () => ({ - useSourcererDataView: () => mockUseSourcererDataView(), -})); - const mockSiemUserCanRead = jest.fn(() => true); + jest.mock('../../lib/kibana', () => { const original = jest.requireActual('../../lib/kibana'); @@ -59,6 +65,8 @@ jest.mock('../../lib/kibana', () => { const mockUpselling = new UpsellingService(); const mockUiSettingsClient = uiSettingsServiceMock.createStartContract(); +const renderShowTimeline = () => renderHook(() => useShowTimeline(), { wrapper: TestProviders }); + describe('use show timeline', () => { beforeAll(() => { (useUserPrivileges as unknown as jest.Mock).mockReturnValue({ @@ -84,25 +92,25 @@ describe('use show timeline', () => { }); it('shows timeline for routes on default', async () => { - const { result } = renderHook(() => useShowTimeline()); + const { result } = renderShowTimeline(); await waitFor(() => expect(result.current).toEqual([true])); }); it('hides timeline for blacklist routes', async () => { mockUseLocation.mockReturnValueOnce({ pathname: '/rules/add_rules' }); - const { result } = renderHook(() => useShowTimeline()); + const { result } = renderShowTimeline(); await waitFor(() => expect(result.current).toEqual([false])); }); it('shows timeline for partial blacklist routes', async () => { mockUseLocation.mockReturnValueOnce({ pathname: '/rules' }); - const { result } = renderHook(() => useShowTimeline()); + const { result } = renderShowTimeline(); await waitFor(() => expect(result.current).toEqual([true])); }); it('hides timeline for sub blacklist routes', async () => { mockUseLocation.mockReturnValueOnce({ pathname: '/administration/policy' }); - const { result } = renderHook(() => useShowTimeline()); + const { result } = renderShowTimeline(); await waitFor(() => expect(result.current).toEqual([false])); }); it('hides timeline for users without timeline access', async () => { @@ -110,7 +118,7 @@ describe('use show timeline', () => { timelinePrivileges: { read: false }, }); - const { result } = renderHook(() => useShowTimeline()); + const { result } = renderShowTimeline(); const showTimeline = result.current; expect(showTimeline).toEqual([false]); }); @@ -120,41 +128,59 @@ it('shows timeline for users with timeline read access', async () => { timelinePrivileges: { read: true }, }); - const { result } = renderHook(() => useShowTimeline()); + const { result } = renderShowTimeline(); const showTimeline = result.current; expect(showTimeline).toEqual([true]); }); -describe('sourcererDataView', () => { +describe('useDataView', () => { it('should show timeline when indices exist', () => { - mockUseSourcererDataView.mockReturnValueOnce({ indicesExist: true, dataViewId: 'test' }); - const { result } = renderHook(() => useShowTimeline()); + jest.mocked(useDataView).mockReturnValueOnce({ + indicesExist: true, + dataView: { id: 'test', title: '' }, + status: 'ready', + }); + const { result } = renderShowTimeline(); expect(result.current).toEqual([true]); }); - it('should show timeline when dataViewId is null', () => { - mockUseSourcererDataView.mockReturnValueOnce({ indicesExist: false, dataViewId: null }); - const { result } = renderHook(() => useShowTimeline()); + it('should show timeline when dataView.id is undefined', () => { + jest.mocked(useDataView).mockReturnValueOnce({ + indicesExist: false, + dataView: { title: '' }, + status: 'ready', + }); + const { result } = renderShowTimeline(); expect(result.current).toEqual([true]); }); it('should not show timeline when dataViewId is not null and indices does not exist', () => { - mockUseSourcererDataView.mockReturnValueOnce({ indicesExist: false, dataViewId: 'test' }); - const { result } = renderHook(() => useShowTimeline()); + jest.mocked(useDataView).mockReturnValueOnce({ + indicesExist: false, + dataView: { id: 'test', title: '' }, + status: 'ready', + }); + const { result } = renderShowTimeline(); expect(result.current).toEqual([false]); }); }); describe('Security solution capabilities', () => { it('should show timeline when user has read capabilities', () => { - mockSiemUserCanRead.mockReturnValueOnce(true); - const { result } = renderHook(() => useShowTimeline()); + jest.mocked(hasAccessToSecuritySolution).mockReturnValueOnce(true); + const { result } = renderShowTimeline(); expect(result.current).toEqual([true]); }); it('should not show timeline when user does not have read capabilities', () => { - mockSiemUserCanRead.mockReturnValueOnce(false); - const { result } = renderHook(() => useShowTimeline()); + jest.mocked(hasAccessToSecuritySolution).mockReturnValueOnce(false); + jest.mocked(useDataView).mockReturnValueOnce({ + indicesExist: true, + dataView: { title: '' }, + status: 'ready', + }); + + const { result } = renderShowTimeline(); expect(result.current).toEqual([false]); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts index d26367d24ad5f..70a93fef8619d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts @@ -28,10 +28,10 @@ export const useShowTimelineForGivenPath = () => { }, } = useKibana(); const userHasSecuritySolutionVisible = hasAccessToSecuritySolution(capabilities); - const dataViewId = dataView?.id ?? ''; + const dataViewId = dataView?.id; const isTimelineAllowed = useMemo( - () => userHasSecuritySolutionVisible && (indicesExist || dataViewId === null), + () => userHasSecuritySolutionVisible && (indicesExist || typeof dataViewId === 'undefined'), [indicesExist, dataViewId, userHasSecuritySolutionVisible] ); From c26acc7c117dc1fbbdf6ff230c5a32b3d2e91f5d Mon Sep 17 00:00:00 2001 From: lgestc Date: Mon, 17 Feb 2025 12:38:47 +0100 Subject: [PATCH 031/115] fix timeline pages tests --- .../timelines/pages/timelines_page.test.tsx | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx index ae82e2a381010..d3901b05ae1e5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import type { ShallowWrapper } from 'enzyme'; -import { shallow } from 'enzyme'; +import { render } from '@testing-library/react'; +import type { PropsWithChildren } from 'react'; import React from 'react'; import { TimelinesPage } from './timelines_page'; -import { useSourcererDataView } from '../../sourcerer/containers'; +import { useDataView } from '../../data_view_picker/hooks/use_data_view'; import { useUserPrivileges } from '../../common/components/user_privileges'; +import { TestProviders } from '../../common/mock'; jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); @@ -23,16 +24,22 @@ jest.mock('react-router-dom', () => { }; }); jest.mock('../../overview/components/events_by_dataset'); -jest.mock('../../sourcerer/containers'); jest.mock('../../common/components/user_privileges'); +jest.mock('../../data_view_picker/hooks/use_data_view'); -describe('TimelinesPage', () => { - let wrapper: ShallowWrapper; +jest.mock('../../common/components/security_route_page_wrapper', () => ({ + SecurityRoutePageWrapper: (props: PropsWithChildren<{}>) => <>{props.children}, +})); +jest.mock('../components/open_timeline', () => ({ + StatefulOpenTimeline: () =>

, +})); +describe('TimelinesPage', () => { it('should render landing page if no indicesExist', () => { - (useSourcererDataView as unknown as jest.Mock).mockReturnValue({ + jest.mocked(useDataView).mockReturnValue({ indicesExist: false, - sourcererDataView: {}, + dataView: {}, + status: 'ready', }); (useUserPrivileges as jest.Mock).mockReturnValue({ timelinePrivileges: { @@ -40,17 +47,18 @@ describe('TimelinesPage', () => { }, }); - wrapper = shallow(); + const wrapper = render(, { wrapper: TestProviders }); - expect(wrapper.exists('[data-test-subj="timelines-page-open-import-data"]')).toBeFalsy(); - expect(wrapper.exists('[data-test-subj="timelines-page-new"]')).toBeFalsy(); - expect(wrapper.exists('[data-test-subj="stateful-open-timeline"]')).toBeFalsy(); + expect(wrapper.queryByTestId('timelines-page-open-import-data')).toBeFalsy(); + expect(wrapper.queryByTestId('timelines-page-new')).toBeFalsy(); + expect(wrapper.queryByTestId('stateful-open-timeline')).toBeFalsy(); }); it('should show the correct elements if user has crud', () => { - (useSourcererDataView as unknown as jest.Mock).mockReturnValue({ + jest.mocked(useDataView).mockReturnValue({ indicesExist: true, - sourcererDataView: {}, + dataView: {}, + status: 'ready', }); (useUserPrivileges as jest.Mock).mockReturnValue({ timelinePrivileges: { @@ -58,11 +66,11 @@ describe('TimelinesPage', () => { }, }); - wrapper = shallow(); + const wrapper = render(, { wrapper: TestProviders }); - expect(wrapper.exists('[data-test-subj="timelines-page-open-import-data"]')).toBeTruthy(); - expect(wrapper.exists('[data-test-subj="timelines-page-new"]')).toBeTruthy(); - expect(wrapper.exists('[data-test-subj="stateful-open-timeline"]')).toBeTruthy(); + expect(wrapper.queryByTestId('timelines-page-open-import-data')).toBeTruthy(); + expect(wrapper.queryByTestId('timelines-page-new')).toBeTruthy(); + expect(wrapper.queryByTestId('stateful-open-timeline')).toBeTruthy(); }); it('should not show import button or modal if user does not have crud privileges but it should show the new timeline button', () => { @@ -73,10 +81,10 @@ describe('TimelinesPage', () => { }, }); - wrapper = shallow(); + const wrapper = render(, { wrapper: TestProviders }); - expect(wrapper.exists('[data-test-subj="timelines-page-open-import-data"]')).toBeFalsy(); - expect(wrapper.exists('[data-test-subj="timelines-page-new"]')).toBeTruthy(); - expect(wrapper.exists('[data-test-subj="stateful-open-timeline"]')).toBeTruthy(); + expect(wrapper.queryByTestId('timelines-page-open-import-data')).toBeFalsy(); + expect(wrapper.queryByTestId('timelines-page-new')).toBeTruthy(); + expect(wrapper.queryByTestId('stateful-open-timeline')).toBeTruthy(); }); }); From 1de086e6f4cfd456f500a145068496b8dfea1148 Mon Sep 17 00:00:00 2001 From: lgestc Date: Mon, 17 Feb 2025 12:59:11 +0100 Subject: [PATCH 032/115] fix header tests --- .../components/modal/header/index.test.tsx | 55 +++---------------- 1 file changed, 9 insertions(+), 46 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx index 793cd12f99451..de09904ea7d6c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx @@ -9,59 +9,33 @@ import React from 'react'; import { TestProviders } from '../../../../common/mock'; import { TimelineModalHeader } from '.'; import { render } from '@testing-library/react'; -import { useSourcererDataView } from '../../../../sourcerer/containers'; import { useCreateTimeline } from '../../../hooks/use_create_timeline'; import { useInspect } from '../../../../common/components/inspect/use_inspect'; import { useKibana } from '../../../../common/lib/kibana'; import { timelineActions } from '../../../store'; +import { TimelineId } from '../../../../../common/types'; -jest.mock('../../../../sourcerer/containers'); jest.mock('../../../hooks/use_create_timeline'); jest.mock('../../../../common/components/inspect/use_inspect'); jest.mock('../../../../common/lib/kibana'); +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: () => jest.fn(), +})); -const mockGetState = jest.fn(); -jest.mock('react-redux', () => { - const actual = jest.requireActual('react-redux'); - return { - ...actual, - useDispatch: jest.fn(), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - useSelector: (selector: any) => - selector({ - timeline: { - timelineById: { - 'timeline-1': { - ...mockGetState(), - }, - }, - }, - }), - }; -}); - -const timelineId = 'timeline-1'; const mockRef = { current: null, }; const renderTimelineModalHeader = () => - render( - - - - ); + render(, { + wrapper: TestProviders, + }); describe('TimelineModalHeader', () => { (useCreateTimeline as jest.Mock).mockReturnValue(jest.fn()); (useInspect as jest.Mock).mockReturnValue(jest.fn()); it('should render all dom elements', () => { - (useSourcererDataView as jest.Mock).mockReturnValue({ - browserFields: {}, - indexPattern: { fields: [], title: '' }, - sourcererDataView: {}, - }); - const { getByTestId, getByText } = renderTimelineModalHeader(); expect(getByTestId('timeline-favorite-empty-star')).toBeInTheDocument(); @@ -76,11 +50,6 @@ describe('TimelineModalHeader', () => { }); it('should show attach to case if user has the correct permissions', () => { - (useSourcererDataView as jest.Mock).mockReturnValue({ - browserFields: {}, - indexPattern: { fields: [], title: '' }, - sourcererDataView: {}, - }); (useKibana as jest.Mock).mockReturnValue({ services: { application: { @@ -106,12 +75,6 @@ describe('TimelineModalHeader', () => { }); it('should call showTimeline action when closing timeline', () => { - (useSourcererDataView as jest.Mock).mockReturnValue({ - browserFields: {}, - indexPattern: { fields: [], title: '' }, - sourcererDataView: {}, - }); - const spy = jest.spyOn(timelineActions, 'showTimeline'); const { getByTestId } = renderTimelineModalHeader(); @@ -119,7 +82,7 @@ describe('TimelineModalHeader', () => { getByTestId('timeline-modal-header-close-button').click(); expect(spy).toHaveBeenCalledWith({ - id: timelineId, + id: TimelineId.test, show: false, }); }); From b088e6b09627baff062c8a2c46231dc3272cb016 Mon Sep 17 00:00:00 2001 From: lgestc Date: Mon, 17 Feb 2025 13:04:45 +0100 Subject: [PATCH 033/115] fix tests --- .../modal/actions/new_timeline_button.test.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx index 92f9b874f8743..0d352d26e586c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx @@ -21,7 +21,6 @@ jest.mock('react-redux', () => { return { ...original, - useSelector: jest.fn(), useDispatch: () => jest.fn(), }; }); @@ -52,8 +51,7 @@ describe('NewTimelineButton', () => { }); it('should call the correct action with clicking on the new timeline button', async () => { - const dataViewId = ''; - const selectedPatterns: string[] = []; + const dataViewId = 'security-solution-default'; const spy = jest.spyOn(timelineActions, 'createTimeline'); @@ -67,7 +65,18 @@ describe('NewTimelineButton', () => { columns: defaultUdtHeaders, dataViewId, id: TimelineId.test, - indexNames: selectedPatterns, + indexNames: [ + '.siem-signals-spacename', + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'traces-apm*', + 'winlogbeat-*', + '-*elastic-cloud-logs-*', + ], show: true, timelineType: 'default', updated: undefined, From 09daf62d46af58a6ca1b11e6f8ee42137450ff9d Mon Sep 17 00:00:00 2001 From: lgestc Date: Mon, 17 Feb 2025 15:27:46 +0100 Subject: [PATCH 034/115] fix new timeline button tests --- .../components/new_timeline/index.test.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx index 0d1ed98f0bc99..adea6b3526a8b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx @@ -26,7 +26,6 @@ jest.mock('react-redux', () => { return { ...original, - useSelector: jest.fn(), useDispatch: () => jest.fn(), }; }); @@ -37,8 +36,19 @@ const renderNewTimelineButton = (type: TimelineType) => render(, { wrapper: TestProviders }); describe('NewTimelineButton', () => { - const dataViewId = ''; - const selectedPatterns: string[] = []; + const dataViewId = 'security-solution-default'; + const selectedPatterns: string[] = [ + '.siem-signals-spacename', + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'traces-apm*', + 'winlogbeat-*', + '-*elastic-cloud-logs-*', + ]; (useDiscoverInTimelineContext as jest.Mock).mockReturnValue({ resetDiscoverAppState: jest.fn(), }); From 829f25521123cec83810caa5f814e732ae9e9e67 Mon Sep 17 00:00:00 2001 From: lgestc Date: Mon, 17 Feb 2025 15:31:17 +0100 Subject: [PATCH 035/115] update unified tests --- .../components/timeline/unified_components/index.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx index 872f3c760f38b..4f9534e164da7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx @@ -500,7 +500,7 @@ describe('unified timeline', () => { expect(kibanaServiceMock.dataViewFieldEditor.openEditor).toHaveBeenNthCalledWith(1, { ctx: { dataView: expect.objectContaining({ - id: 'security-solution', + id: 'security-solution-default', }), }, fieldName: 'message', From 8e1dee0da00c586ae2a1abcc983ae13eb518f17d Mon Sep 17 00:00:00 2001 From: lgestc Date: Mon, 17 Feb 2025 17:16:44 +0100 Subject: [PATCH 036/115] support prefilling multiple scopes --- .../hooks/use_init_data_view_picker.ts | 4 ++-- .../hooks/use_select_data_view.ts | 2 +- .../redux/listeners/data_view_selected.ts | 21 +++++++++++-------- .../public/data_view_picker/redux/reducer.ts | 6 +++--- .../open_timeline/use_update_timeline.tsx | 2 +- .../timelines/hooks/use_create_timeline.tsx | 2 +- 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts index 405b02247dea5..280f1dd8182d2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts @@ -53,11 +53,11 @@ export const useInitDataViewPicker = () => { dispatch(shared.actions.init()); - // Preload the default view + // Preload the default view for related scopes dispatch( selectDataViewAsync({ id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, - scope: DataViewPickerScopeName.default, + scope: [DataViewPickerScopeName.default, DataViewPickerScopeName.timeline], }) ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts index aef7f8b657863..5ff5dde741080 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts @@ -14,7 +14,7 @@ export const useSelectDataView = () => { const dispatch = useDispatch(); return useCallback( - (params: { id?: string | null; patterns?: string[]; scope: DataViewPickerScopeName }) => { + (params: { id?: string | null; patterns?: string[]; scope: DataViewPickerScopeName[] }) => { dispatch(selectDataViewAsync(params)); }, [dispatch] diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts index e5efe8ccf63ba..4c657b8432474 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts @@ -21,8 +21,6 @@ export const createDataViewSelectedListener = (dependencies: { ) => { const state = listenerApi.getState(); - const currentScopeActions = scopes[action.payload.scope].actions; - const findCachedDataView = (id: string | null | undefined) => { const savedDataView = state.dataViewPicker.shared.dataViews.find((dv) => dv.id === id); @@ -45,6 +43,7 @@ export const createDataViewSelectedListener = (dependencies: { try { if (action.payload.id) { const dataViewById = await dependencies.dataViews.get(action.payload.id); + // eslint-disable-next-line require-atomic-updates dataViewSpec = dataViewById.toSpec(); } } catch (error: unknown) { @@ -64,19 +63,23 @@ export const createDataViewSelectedListener = (dependencies: { title, }); listenerApi.dispatch(shared.actions.addDataView(adhocDataView)); + // eslint-disable-next-line require-atomic-updates dataViewSpec = adhocDataView.toSpec(); } catch (error: unknown) { adhocDataViewCreationError = error; } } - if (dataViewSpec) { - listenerApi.dispatch(currentScopeActions.setSelectedDataView(dataViewSpec)); - } else if (dataViewByIdError || adhocDataViewCreationError) { - listenerApi.dispatch( - currentScopeActions.dataViewSelectionError('An error occured when setting data view') - ); - } + action.payload.scope.forEach((scope) => { + const currentScopeActions = scopes[scope].actions; + if (dataViewSpec) { + listenerApi.dispatch(currentScopeActions.setSelectedDataView(dataViewSpec)); + } else if (dataViewByIdError || adhocDataViewCreationError) { + listenerApi.dispatch( + currentScopeActions.dataViewSelectionError('An error occured when setting data view') + ); + } + }); }, }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts index 0706bd1a8e654..67b61c89359a0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts @@ -26,10 +26,10 @@ export const initialSharedState: SharedDataViewSelectionState = { export const selectDataViewAsync = createAction<{ id?: string | null; patterns?: string[]; - scope: DataViewPickerScopeName; + scope: DataViewPickerScopeName[]; }>(`${SLICE_PREFIX}/selectDataView`); -const createDataViewSelectionSlice = (scopeName: T) => +const createDataViewSelectionSlice = (scopeName: T) => createSlice({ name: `${SLICE_PREFIX}/${scopeName}`, initialState: initialScopeState, @@ -44,7 +44,7 @@ const createDataViewSelectionSlice = (scopeName: T) => }, extraReducers(builder) { builder.addCase(selectDataViewAsync, (state, action) => { - if (action.payload.scope !== scopeName) { + if (!action.payload.scope.includes(scopeName)) { return state; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx index 4401844f5288f..289cdf42c5948 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx @@ -62,7 +62,7 @@ export const useUpdateTimeline = () => { selectDataView({ id: _timeline.dataViewId, patterns: _timeline.indexNames, - scope: DataViewPickerScopeName.timeline, + scope: [DataViewPickerScopeName.timeline], }); } if ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx index d753f87979860..821710ec23808 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx @@ -80,7 +80,7 @@ export const useCreateTimeline = ({ setSelectedDataView({ id: dataViewId, patterns: selectedPatterns, - scope: DataViewPickerScopeName.timeline, + scope: [DataViewPickerScopeName.timeline], }); dispatch( From 10c8e8e8783914addc2132dd3c948826ddfa0f23 Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 18 Feb 2025 09:30:19 +0100 Subject: [PATCH 037/115] --wip-- [skip ci] --- .../redux/listeners/data_view_selected.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts index 4c657b8432474..ccf36776baac6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts @@ -22,13 +22,16 @@ export const createDataViewSelectedListener = (dependencies: { const state = listenerApi.getState(); const findCachedDataView = (id: string | null | undefined) => { - const savedDataView = state.dataViewPicker.shared.dataViews.find((dv) => dv.id === id); + const dataView = + state.dataViewPicker.shared.adhocDataViews.find((dv) => dv.id === id) ?? null; - if (savedDataView) { - return savedDataView; + // NOTE: validate if fields are available, otherwise dont return the view + // This is required to compute browserFields later. + if (!Object.keys(dataView?.fields || {})) { + return null; } - return state.dataViewPicker.shared.adhocDataViews.find((dv) => dv.id === id) ?? null; + return dataView; }; let dataViewByIdError: unknown; From 1b8774f6c431b0e9f46469f22b9a442ba1deaf29 Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 18 Feb 2025 09:55:31 +0100 Subject: [PATCH 038/115] fix broken test --- .../security_solution/public/data_view_picker/redux/mock.ts | 5 +++++ .../public/timelines/components/graph_overlay/index.test.tsx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts index 94badaaca55cc..71bca0d394d07 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts @@ -45,5 +45,10 @@ export const mockDataViewPickerState: RootState = { dataView: mockDefaultDataViewSpec, status: 'ready', }, + analyzer: { + ...dataViewPickerState.analyzer, + dataView: mockDefaultDataViewSpec, + status: 'ready', + }, }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx index 2f76aeae8159e..f345c8b2fc9bb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx @@ -129,7 +129,7 @@ describe('GraphOverlay', () => { ); expect(useStateSyncingActionsMock.mock.calls[0][0].indices).toEqual( - mockGlobalState.sourcerer.sourcererScopes.analyzer.selectedPatterns + mockGlobalState.dataViewPicker.analyzer.dataView?.title?.split(',') ); }); }); From 0fc0db5fdf65a64c975a470d27adf2b156b7cbc1 Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 18 Feb 2025 10:54:02 +0100 Subject: [PATCH 039/115] --wip-- [skip ci] --- .../common/hooks/timeline/use_investigate_in_timeline.ts | 2 +- .../data_view_picker/components/data_view_picker/index.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts index 8aa2a3c58f8c9..8a9c89f067845 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts @@ -127,7 +127,7 @@ export const useInvestigateInTimeline = () => { // (This is required so the timeline event count matches the prevalence count) if (!keepDataView) { setSelectedDataView({ - scope: SourcererScopeName.timeline, + scope: [SourcererScopeName.timeline], id: defaultDataView.id, patterns: [signalIndexName || ''], }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx index 18a3efb935179..7a9f64c3eb11a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx @@ -40,7 +40,7 @@ export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) = closeDataViewEditor.current = dataViewEditor.openEditor({ onSave: async (newDataView) => { dispatch(shared.actions.addDataView(newDataView)); - dispatch(selectDataViewAsync({ id: newDataView.id, scope: props.scope })); + dispatch(selectDataViewAsync({ id: newDataView.id, scope: [props.scope] })); // TODO: reload data views }, allowAdHocDataView: true, @@ -78,7 +78,7 @@ export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) = const handleChangeDataView = useCallback( (id: string) => { - dispatch(selectDataViewAsync({ id, scope: props.scope })); + dispatch(selectDataViewAsync({ id, scope: [props.scope] })); }, [dispatch, props.scope] ); From 77be180aea7286398a2fa35301ac1925b4fe8308 Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 18 Feb 2025 13:05:37 +0100 Subject: [PATCH 040/115] --wip-- [skip ci] --- .../public/timelines/hooks/use_create_timeline.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx index d41279e08c167..e684cd17d3bda 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx @@ -94,7 +94,7 @@ describe('useCreateTimeline', () => { expect(createTimeline.mock.calls[0][0].show).toEqual(true); expect(createTimeline.mock.calls[0][0].updated).toEqual(undefined); expect(createTimeline.mock.calls[0][0].excludedRowRendererIds).toHaveLength(RowRendererCount); - expect(setSelectedDataView.mock.calls[0][0].scope).toEqual(SourcererScopeName.timeline); + expect(setSelectedDataView.mock.calls[0][0].scope).toEqual([SourcererScopeName.timeline]); expect(setSelectedDataView.mock.calls[0][0].id).toEqual( mockGlobalState.dataViewPicker.default.dataView?.id ); From 19a80118fcd0efc44ac4f99de8635396a16778f5 Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 18 Feb 2025 13:13:14 +0100 Subject: [PATCH 041/115] skip osquery test --- .../plugins/shared/osquery/cypress/e2e/all/timelines.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/timelines.cy.ts b/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/timelines.cy.ts index f7541cc1a9b89..68110173a7873 100644 --- a/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/timelines.cy.ts +++ b/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/timelines.cy.ts @@ -18,7 +18,8 @@ describe('ALL - Timelines', { tags: ['@ess'] }, () => { cy.login(ServerlessRoleName.SOC_MANAGER); }); - it('should substitute osquery parameter on non-alert event take action', () => { + // TODO: how exactly should this be ported to discover data view picker? + it.skip('should substitute osquery parameter on non-alert event take action', () => { cy.visit('/app/security/timelines', { onBeforeLoad: (win) => { disableNewFeaturesTours(win); From 83b194a95bbd7c40e1a24f2dcc79a1b64f747322 Mon Sep 17 00:00:00 2001 From: lgestc Date: Wed, 19 Feb 2025 10:19:39 +0100 Subject: [PATCH 042/115] fix flyout e2e --- .../common/utils/timeline/use_show_timeline_for_path.ts | 9 +++++---- .../data_view_picker/hooks/use_init_data_view_picker.ts | 6 +++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts index 70a93fef8619d..cb2dc1aaf68f5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts @@ -21,18 +21,19 @@ const isTimelinePathVisible = (currentPath: string): boolean => { }; export const useShowTimelineForGivenPath = () => { - const { indicesExist, dataView } = useDataView(SourcererScopeName.timeline); + const { indicesExist, dataView, status } = useDataView(SourcererScopeName.timeline); const { services: { application: { capabilities }, }, } = useKibana(); const userHasSecuritySolutionVisible = hasAccessToSecuritySolution(capabilities); - const dataViewId = dataView?.id; const isTimelineAllowed = useMemo( - () => userHasSecuritySolutionVisible && (indicesExist || typeof dataViewId === 'undefined'), - [indicesExist, dataViewId, userHasSecuritySolutionVisible] + () => + userHasSecuritySolutionVisible && + (indicesExist || (status === 'ready' && dataView && dataView?.id === '')), + [userHasSecuritySolutionVisible, indicesExist, status, dataView] ); const getIsTimelineVisible = useCallback( diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts index 280f1dd8182d2..3d89738a1ae1d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts @@ -57,7 +57,11 @@ export const useInitDataViewPicker = () => { dispatch( selectDataViewAsync({ id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, - scope: [DataViewPickerScopeName.default, DataViewPickerScopeName.timeline], + scope: [ + DataViewPickerScopeName.default, + DataViewPickerScopeName.timeline, + DataViewPickerScopeName.analyzer, + ], }) ); From 3b25db5fec8e0d6e1558ff75481aa1d2e5db7695 Mon Sep 17 00:00:00 2001 From: lgestc Date: Wed, 19 Feb 2025 10:39:58 +0100 Subject: [PATCH 043/115] remove useEffect --- .../components/data_view_picker/index.tsx | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx index 7a9f64c3eb11a..970e1e3aadaf7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx @@ -6,10 +6,10 @@ */ import { DataViewPicker as UnifiedDataViewPicker } from '@kbn/unified-search-plugin/public'; -import React, { useCallback, useRef, useMemo, memo, useState, useEffect } from 'react'; +import React, { useCallback, useRef, useMemo, memo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import type { DataView, DataViewListItem } from '@kbn/data-views-plugin/public'; +import { DataView, type DataViewListItem } from '@kbn/data-views-plugin/public'; import type { DataViewPickerScopeName } from '../../constants'; import { useKibana } from '../../../common/lib/kibana/kibana_react'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../../constants'; @@ -21,7 +21,7 @@ export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) = const dispatch = useDispatch(); const { - services: { dataViewEditor, data, dataViewFieldEditor }, + services: { dataViewEditor, data, dataViewFieldEditor, fieldFormats }, } = useKibana(); const closeDataViewEditor = useRef<() => void | undefined>(); @@ -99,24 +99,19 @@ export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) = const { adhocDataViews: adhocDataViewSpecs, dataViews } = useSelector(sharedStateSelector); - const [adhocDataViews, setAdhocDataViews] = useState([]); - const [managedDataViews, setManagedDataViews] = useState([]); - - useEffect(() => { - (async () => { - const adhoc = await Promise.all( - adhocDataViewSpecs.map((dvSpec) => data.dataViews.create(dvSpec)) - ); - setAdhocDataViews(adhoc); - - const managed: DataViewListItem[] = dataViews.map((spec) => ({ - id: spec.id ?? '', - title: spec.title ?? '', - name: spec.name, - })); - setManagedDataViews(managed); - })(); - }, [data.dataViews, adhocDataViewSpecs, dataViews]); + const managedDataViews = useMemo(() => { + const managed: DataViewListItem[] = dataViews.map((spec) => ({ + id: spec.id ?? '', + title: spec.title ?? '', + name: spec.name, + })); + + return managed; + }, [dataViews]); + + const adhocDataViews = useMemo(() => { + return adhocDataViewSpecs.map((spec) => new DataView({ spec, fieldFormats })); + }, [adhocDataViewSpecs, fieldFormats]); return ( Date: Wed, 19 Feb 2025 14:15:37 +0100 Subject: [PATCH 044/115] fixing more tests --- .../utils/timeline/use_show_timeline.test.tsx | 6 +++--- .../utils/timeline/use_show_timeline_for_path.ts | 14 ++++++-------- .../components/data_view_picker/index.tsx | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx index d80f42e7c6352..c83d33523b91b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx @@ -144,14 +144,14 @@ describe('useDataView', () => { expect(result.current).toEqual([true]); }); - it('should show timeline when dataView.id is undefined', () => { + it('should not show timeline when indices do not exist', () => { jest.mocked(useDataView).mockReturnValueOnce({ indicesExist: false, - dataView: { title: '' }, + dataView: { title: 'pattern-1' }, status: 'ready', }); const { result } = renderShowTimeline(); - expect(result.current).toEqual([true]); + expect(result.current).toEqual([false]); }); it('should not show timeline when dataViewId is not null and indices does not exist', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts index cb2dc1aaf68f5..ff6f97a8a7cfd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts @@ -8,20 +8,20 @@ import { useCallback, useMemo } from 'react'; import { matchPath } from 'react-router-dom'; -import { useDataView } from '../../../data_view_picker/hooks/use_data_view'; import { getLinksWithHiddenTimeline } from '../../links'; -import { SourcererScopeName } from '../../../sourcerer/store/model'; import { useKibana } from '../../lib/kibana'; import { hasAccessToSecuritySolution } from '../../../helpers_access'; +// FIXME: there must be a better way of doing this +const extraNonTimelinePaths: string[] = ['security/explore']; + const isTimelinePathVisible = (currentPath: string): boolean => { const groupLinksWithHiddenTimelinePaths = getLinksWithHiddenTimeline().map((l) => l.path); - const hiddenTimelineRoutes = groupLinksWithHiddenTimelinePaths; + const hiddenTimelineRoutes = [...groupLinksWithHiddenTimelinePaths, ...extraNonTimelinePaths]; return !hiddenTimelineRoutes.find((route) => matchPath(currentPath, route)); }; export const useShowTimelineForGivenPath = () => { - const { indicesExist, dataView, status } = useDataView(SourcererScopeName.timeline); const { services: { application: { capabilities }, @@ -30,10 +30,8 @@ export const useShowTimelineForGivenPath = () => { const userHasSecuritySolutionVisible = hasAccessToSecuritySolution(capabilities); const isTimelineAllowed = useMemo( - () => - userHasSecuritySolutionVisible && - (indicesExist || (status === 'ready' && dataView && dataView?.id === '')), - [userHasSecuritySolutionVisible, indicesExist, status, dataView] + () => userHasSecuritySolutionVisible, + [userHasSecuritySolutionVisible] ); const getIsTimelineVisible = useCallback( diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx index 970e1e3aadaf7..c9b3ba606f5a4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx @@ -86,7 +86,7 @@ export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) = const handleEditDataView = useCallback(() => {}, []); const triggerConfig = useMemo(() => { - if (dataView?.id === DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID) { + if (dataView.id === DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID) { return { label: 'Default Security Data View', }; From 0e1e7a0022f3495922907fd5226e07c74712e805 Mon Sep 17 00:00:00 2001 From: lgestc Date: Wed, 19 Feb 2025 16:52:21 +0100 Subject: [PATCH 045/115] skip use_show_timeline tests based on the useDataView --- .../public/common/utils/timeline/use_show_timeline.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx index c83d33523b91b..1ec2814bfa636 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx @@ -133,7 +133,9 @@ it('shows timeline for users with timeline read access', async () => { expect(showTimeline).toEqual([true]); }); -describe('useDataView', () => { +// FIXME: skipping this as useDataView should not dictate if the timeline bar is not visible (timeline scope could be cached); +// we need a long term solution. +describe.skip('useDataView', () => { it('should show timeline when indices exist', () => { jest.mocked(useDataView).mockReturnValueOnce({ indicesExist: true, From d3814a094c0858cef08089e9640adbd17d7fc742 Mon Sep 17 00:00:00 2001 From: lgestc Date: Thu, 20 Feb 2025 13:08:39 +0100 Subject: [PATCH 046/115] skip test that should no longer apply to current sourcerer logic --- .../common/utils/timeline/use_show_timeline_for_path.ts | 3 ++- .../e2e/investigations/timelines/unsaved_timeline.cy.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts index ff6f97a8a7cfd..ed4bb97c513a5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts @@ -13,7 +13,7 @@ import { useKibana } from '../../lib/kibana'; import { hasAccessToSecuritySolution } from '../../../helpers_access'; // FIXME: there must be a better way of doing this -const extraNonTimelinePaths: string[] = ['security/explore']; +const extraNonTimelinePaths: string[] = ['/explore']; const isTimelinePathVisible = (currentPath: string): boolean => { const groupLinksWithHiddenTimelinePaths = getLinksWithHiddenTimeline().map((l) => l.path); @@ -29,6 +29,7 @@ export const useShowTimelineForGivenPath = () => { } = useKibana(); const userHasSecuritySolutionVisible = hasAccessToSecuritySolution(capabilities); + // FIXME: this used to be () => userHasSecuritySolutionVisible && (indicesExist || dataViewId === null) const isTimelineAllowed = useMemo( () => userHasSecuritySolutionVisible, [userHasSecuritySolutionVisible] diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/unsaved_timeline.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/unsaved_timeline.cy.ts index 0d938a00ac4f3..ff4d595b503ca 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/unsaved_timeline.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/unsaved_timeline.cy.ts @@ -180,7 +180,9 @@ describe('[serverless] Save Timeline Prompts', { tags: ['@serverless'] }, () => cy.get(APP_LEAVE_CONFIRM_MODAL).should('be.visible'); }); - it('should NOT prompt when navigating with a changed & saved timeline to pages where timelines are disabled', () => { + // FIXME: data view is always present now, see the check following file for isTimlineAllowedCheck: + // x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts + it.skip('should NOT prompt when navigating with a changed & saved timeline to pages where timelines are disabled', () => { populateTimeline(); addNameToTimelineAndSave('Test'); closeTimelineUsingCloseButton(); From 0506aacfb9d66644ac4eec1140ed0c0ee69c270e Mon Sep 17 00:00:00 2001 From: lgestc Date: Thu, 20 Feb 2025 15:00:07 +0100 Subject: [PATCH 047/115] revert unused change --- .../common/utils/timeline/use_show_timeline_for_path.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts index ed4bb97c513a5..d0e3f53ce771e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts @@ -12,12 +12,9 @@ import { getLinksWithHiddenTimeline } from '../../links'; import { useKibana } from '../../lib/kibana'; import { hasAccessToSecuritySolution } from '../../../helpers_access'; -// FIXME: there must be a better way of doing this -const extraNonTimelinePaths: string[] = ['/explore']; - const isTimelinePathVisible = (currentPath: string): boolean => { const groupLinksWithHiddenTimelinePaths = getLinksWithHiddenTimeline().map((l) => l.path); - const hiddenTimelineRoutes = [...groupLinksWithHiddenTimelinePaths, ...extraNonTimelinePaths]; + const hiddenTimelineRoutes = [...groupLinksWithHiddenTimelinePaths]; return !hiddenTimelineRoutes.find((route) => matchPath(currentPath, route)); }; From 1ec5817ce23872ac4c402a4e5429d7d943f278ab Mon Sep 17 00:00:00 2001 From: lgestc Date: Thu, 20 Feb 2025 16:51:47 +0100 Subject: [PATCH 048/115] revert changes made to the timeline --- .../osquery/cypress/e2e/all/timelines.cy.ts | 3 +- .../components/with_data_view/index.test.tsx | 57 +++++------- .../components/with_data_view/index.tsx | 8 +- .../timeline/use_investigate_in_timeline.ts | 25 ++---- .../utils/timeline/use_show_timeline.test.tsx | 86 +++++++------------ .../timeline/use_show_timeline_for_path.ts | 10 ++- .../use_add_bulk_to_timeline.tsx | 23 ++--- .../components/graph_overlay/index.test.tsx | 2 +- .../actions/new_timeline_button.test.tsx | 17 +--- .../components/modal/header/index.test.tsx | 55 ++++++++++-- .../components/modal/header/index.tsx | 10 +-- .../components/new_timeline/index.test.tsx | 16 +--- .../components/open_timeline/index.tsx | 10 +-- .../open_timeline/note_previews/index.tsx | 4 +- .../open_timeline/use_update_timeline.tsx | 19 ++-- .../timeline/data_providers/index.tsx | 6 +- .../components/timeline/kpi/kpi_container.tsx | 10 +-- .../timeline/query_bar/eql/index.tsx | 13 +-- .../components/timeline/query_bar/index.tsx | 7 +- .../timeline/search_or_filter/index.tsx | 4 +- .../search_or_filter/search_or_filter.tsx | 4 +- .../components/timeline/tabs/esql/index.tsx | 18 +++- .../components/timeline/tabs/pinned/index.tsx | 13 ++- .../timeline/tabs/query/index.test.tsx | 8 ++ .../components/timeline/tabs/query/index.tsx | 27 +++--- .../tabs/session/use_session_view.tsx | 4 +- .../tabs/shared/use_timeline_columns.tsx | 4 +- .../unified_components/index.test.tsx | 2 +- .../containers/use_timeline_data_filters.ts | 4 +- .../hooks/use_create_timeline.test.tsx | 22 ++--- .../timelines/hooks/use_create_timeline.tsx | 36 ++++---- .../timelines/pages/timelines_page.test.tsx | 52 +++++------ .../public/timelines/pages/timelines_page.tsx | 6 +- .../timelines/unsaved_timeline.cy.ts | 4 +- 34 files changed, 273 insertions(+), 316 deletions(-) diff --git a/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/timelines.cy.ts b/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/timelines.cy.ts index 68110173a7873..f7541cc1a9b89 100644 --- a/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/timelines.cy.ts +++ b/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/timelines.cy.ts @@ -18,8 +18,7 @@ describe('ALL - Timelines', { tags: ['@ess'] }, () => { cy.login(ServerlessRoleName.SOC_MANAGER); }); - // TODO: how exactly should this be ported to discover data view picker? - it.skip('should substitute osquery parameter on non-alert event take action', () => { + it('should substitute osquery parameter on non-alert event take action', () => { cy.visit('/app/security/timelines', { onBeforeLoad: (win) => { disableNewFeaturesTours(win); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.test.tsx index d93013ba0f6c0..9313f20784474 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.test.tsx @@ -8,15 +8,14 @@ import React, { useEffect } from 'react'; import type { DataView } from '@kbn/data-views-plugin/common'; import { render, screen } from '@testing-library/react'; import { withDataView } from '.'; -import { TestProvidersComponent } from '../../mock'; -import { useFullDataView } from '../../../data_view_picker/hooks/use_full_data_view'; - -jest.mock('../../../data_view_picker/hooks/use_full_data_view'); +import { useGetScopedSourcererDataView } from '../../../sourcerer/components/use_get_sourcerer_data_view'; interface TestComponentProps { dataView: DataView; } +jest.mock('../../../sourcerer/components/use_get_sourcerer_data_view'); + const TEST_ID = { DATA_VIEW_ERROR_COMPONENT: 'dataViewErrorComponent', TEST_COMPONENT: 'test_component', @@ -35,38 +34,24 @@ const TestComponent = (props: TestComponentProps) => { }; describe('withDataViewId', () => { - describe('when data view is null', () => { - beforeEach(() => {}); - - it('should render provided fallback', async () => { - const RenderedComponent = withDataView(TestComponent, ); - render(, { wrapper: TestProvidersComponent }); - expect(screen.getByTestId(TEST_ID.FALLBACK_COMPONENT)).toBeVisible(); - }); - - it('should render default error components when there is not fallback provided', async () => { - const RenderedComponent = withDataView(TestComponent); - render(, { wrapper: TestProvidersComponent }); - expect(screen.getByTestId(TEST_ID.DATA_VIEW_ERROR_COMPONENT)).toBeVisible(); - }); + beforeEach(() => { + (useGetScopedSourcererDataView as jest.Mock).mockReturnValue(undefined); }); - - describe('when data view is set', () => { - beforeEach(() => { - jest - .mocked(useFullDataView) - .mockImplementation( - jest.requireActual('../../../data_view_picker/hooks/use_full_data_view').useFullDataView - ); - }); - - it('should render provided component when dataViewId is not null', async () => { - const RenderedComponent = withDataView(TestComponent); - render(, { wrapper: TestProvidersComponent }); - expect(screen.getByTestId(TEST_ID.TEST_COMPONENT)).toBeVisible(); - expect(dataViewMockFn).toHaveBeenCalledWith( - expect.objectContaining({ id: 'security-solution-default' }) - ); - }); + it('should render default error components when there is not fallback provided and dataViewId is null', async () => { + const RenderedComponent = withDataView(TestComponent); + render(); + expect(screen.getByTestId(TEST_ID.DATA_VIEW_ERROR_COMPONENT)).toBeVisible(); + }); + it('should render provided fallback and dataViewId is null', async () => { + const RenderedComponent = withDataView(TestComponent, ); + render(); + expect(screen.getByTestId(TEST_ID.FALLBACK_COMPONENT)).toBeVisible(); + }); + it('should render provided component when dataViewId is not null', async () => { + (useGetScopedSourcererDataView as jest.Mock).mockReturnValue({ id: 'test' }); + const RenderedComponent = withDataView(TestComponent); + render(); + expect(screen.getByTestId(TEST_ID.TEST_COMPONENT)).toBeVisible(); + expect(dataViewMockFn).toHaveBeenCalledWith({ id: 'test' }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.tsx index f03d0aee2c06d..41d3b451a61e6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.tsx @@ -9,9 +9,9 @@ import React from 'react'; import type { ComponentType } from 'react'; import type { ReactElement } from 'react-markdown'; import type { DataView } from '@kbn/data-views-plugin/common'; -import { DataViewPickerScopeName } from '../../../data_view_picker/constants'; -import { useFullDataView } from '../../../data_view_picker/hooks/use_full_data_view'; import { DataViewErrorComponent } from './data_view_error'; +import { useGetScopedSourcererDataView } from '../../../sourcerer/components/use_get_sourcerer_data_view'; +import { SourcererScopeName } from '../../../sourcerer/store/model'; type OmitDataView = T extends { dataView: DataView } ? Omit : T; @@ -30,8 +30,8 @@ export const withDataView =

( fallback?: ReactElement ) => { const ComponentWithDataView = (props: OmitDataView

) => { - const dataView = useFullDataView({ - dataViewPickerScope: DataViewPickerScopeName.timeline, + const dataView = useGetScopedSourcererDataView({ + sourcererScope: SourcererScopeName.timeline, }); if (!dataView) { diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts index 8a9c89f067845..c1c195b0c5965 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts @@ -8,12 +8,12 @@ import { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import type { Filter, Query } from '@kbn/es-query'; -import { useSelectDataView } from '../../../data_view_picker/hooks/use_select_data_view'; import { useCreateTimeline } from '../../../timelines/hooks/use_create_timeline'; import { updateProviders, setFilters, applyKqlFilterQuery } from '../../../timelines/store/actions'; import { SourcererScopeName } from '../../../sourcerer/store/model'; import type { DataProvider } from '../../../../common/types'; import { sourcererSelectors } from '../../store'; +import { sourcererActions } from '../../store/actions'; import { inputsActions } from '../../store/inputs'; import { InputsModelId } from '../../store/inputs/constants'; import type { TimeRange } from '../../store/inputs/model'; @@ -68,8 +68,6 @@ export const useInvestigateInTimeline = () => { timelineType: TimelineTypeEnum.default, }); - const setSelectedDataView = useSelectDataView(); - const investigateInTimeline = useCallback( async ({ query, @@ -126,24 +124,19 @@ export const useInvestigateInTimeline = () => { // Only show detection alerts // (This is required so the timeline event count matches the prevalence count) if (!keepDataView) { - setSelectedDataView({ - scope: [SourcererScopeName.timeline], - id: defaultDataView.id, - patterns: [signalIndexName || ''], - }); + dispatch( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.timeline, + selectedDataViewId: defaultDataView.id, + selectedPatterns: [signalIndexName || ''], + }) + ); } // Unlock the time range from the global time range dispatch(inputsActions.removeLinkTo([InputsModelId.timeline, InputsModelId.global])); } }, - [ - clearTimelineTemplate, - clearTimelineDefault, - dispatch, - setSelectedDataView, - defaultDataView.id, - signalIndexName, - ] + [clearTimelineTemplate, clearTimelineDefault, dispatch, defaultDataView.id, signalIndexName] ); return { investigateInTimeline }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx index 1ec2814bfa636..9bc192880ea89 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx @@ -13,24 +13,9 @@ import { appLinks } from '../../../app_links'; import { useUserPrivileges } from '../../components/user_privileges'; import { useShowTimeline } from './use_show_timeline'; import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks'; -import { TestProviders } from '../../mock'; -import { useDataView } from '../../../data_view_picker/hooks/use_data_view'; -import { hasAccessToSecuritySolution } from '../../../helpers_access'; - -jest.mock('../../../data_view_picker/hooks/use_data_view', () => ({ - useDataView: jest.fn().mockReturnValue({ - indicesExist: true, - dataView: { - title: '', - }, - status: 'ready', - }), -})); jest.mock('../../components/user_privileges'); -jest.mock('../../../helpers_access', () => ({ hasAccessToSecuritySolution: jest.fn(() => true) })); - const mockUseLocation = jest.fn().mockReturnValue({ pathname: '/overview' }); jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -40,8 +25,17 @@ jest.mock('react-router-dom', () => { }; }); -const mockSiemUserCanRead = jest.fn(() => true); +const mockUseSourcererDataView = jest.fn( + (): { indicesExist: boolean; dataViewId: string | null } => ({ + indicesExist: true, + dataViewId: null, + }) +); +jest.mock('../../../sourcerer/containers', () => ({ + useSourcererDataView: () => mockUseSourcererDataView(), +})); +const mockSiemUserCanRead = jest.fn(() => true); jest.mock('../../lib/kibana', () => { const original = jest.requireActual('../../lib/kibana'); @@ -65,8 +59,6 @@ jest.mock('../../lib/kibana', () => { const mockUpselling = new UpsellingService(); const mockUiSettingsClient = uiSettingsServiceMock.createStartContract(); -const renderShowTimeline = () => renderHook(() => useShowTimeline(), { wrapper: TestProviders }); - describe('use show timeline', () => { beforeAll(() => { (useUserPrivileges as unknown as jest.Mock).mockReturnValue({ @@ -92,25 +84,25 @@ describe('use show timeline', () => { }); it('shows timeline for routes on default', async () => { - const { result } = renderShowTimeline(); + const { result } = renderHook(() => useShowTimeline()); await waitFor(() => expect(result.current).toEqual([true])); }); it('hides timeline for blacklist routes', async () => { mockUseLocation.mockReturnValueOnce({ pathname: '/rules/add_rules' }); - const { result } = renderShowTimeline(); + const { result } = renderHook(() => useShowTimeline()); await waitFor(() => expect(result.current).toEqual([false])); }); it('shows timeline for partial blacklist routes', async () => { mockUseLocation.mockReturnValueOnce({ pathname: '/rules' }); - const { result } = renderShowTimeline(); + const { result } = renderHook(() => useShowTimeline()); await waitFor(() => expect(result.current).toEqual([true])); }); it('hides timeline for sub blacklist routes', async () => { mockUseLocation.mockReturnValueOnce({ pathname: '/administration/policy' }); - const { result } = renderShowTimeline(); + const { result } = renderHook(() => useShowTimeline()); await waitFor(() => expect(result.current).toEqual([false])); }); it('hides timeline for users without timeline access', async () => { @@ -118,7 +110,7 @@ describe('use show timeline', () => { timelinePrivileges: { read: false }, }); - const { result } = renderShowTimeline(); + const { result } = renderHook(() => useShowTimeline()); const showTimeline = result.current; expect(showTimeline).toEqual([false]); }); @@ -128,61 +120,41 @@ it('shows timeline for users with timeline read access', async () => { timelinePrivileges: { read: true }, }); - const { result } = renderShowTimeline(); + const { result } = renderHook(() => useShowTimeline()); const showTimeline = result.current; expect(showTimeline).toEqual([true]); }); -// FIXME: skipping this as useDataView should not dictate if the timeline bar is not visible (timeline scope could be cached); -// we need a long term solution. -describe.skip('useDataView', () => { +describe('sourcererDataView', () => { it('should show timeline when indices exist', () => { - jest.mocked(useDataView).mockReturnValueOnce({ - indicesExist: true, - dataView: { id: 'test', title: '' }, - status: 'ready', - }); - const { result } = renderShowTimeline(); + mockUseSourcererDataView.mockReturnValueOnce({ indicesExist: true, dataViewId: 'test' }); + const { result } = renderHook(() => useShowTimeline()); expect(result.current).toEqual([true]); }); - it('should not show timeline when indices do not exist', () => { - jest.mocked(useDataView).mockReturnValueOnce({ - indicesExist: false, - dataView: { title: 'pattern-1' }, - status: 'ready', - }); - const { result } = renderShowTimeline(); - expect(result.current).toEqual([false]); + it('should show timeline when dataViewId is null', () => { + mockUseSourcererDataView.mockReturnValueOnce({ indicesExist: false, dataViewId: null }); + const { result } = renderHook(() => useShowTimeline()); + expect(result.current).toEqual([true]); }); it('should not show timeline when dataViewId is not null and indices does not exist', () => { - jest.mocked(useDataView).mockReturnValueOnce({ - indicesExist: false, - dataView: { id: 'test', title: '' }, - status: 'ready', - }); - const { result } = renderShowTimeline(); + mockUseSourcererDataView.mockReturnValueOnce({ indicesExist: false, dataViewId: 'test' }); + const { result } = renderHook(() => useShowTimeline()); expect(result.current).toEqual([false]); }); }); describe('Security solution capabilities', () => { it('should show timeline when user has read capabilities', () => { - jest.mocked(hasAccessToSecuritySolution).mockReturnValueOnce(true); - const { result } = renderShowTimeline(); + mockSiemUserCanRead.mockReturnValueOnce(true); + const { result } = renderHook(() => useShowTimeline()); expect(result.current).toEqual([true]); }); it('should not show timeline when user does not have read capabilities', () => { - jest.mocked(hasAccessToSecuritySolution).mockReturnValueOnce(false); - jest.mocked(useDataView).mockReturnValueOnce({ - indicesExist: true, - dataView: { title: '' }, - status: 'ready', - }); - - const { result } = renderShowTimeline(); + mockSiemUserCanRead.mockReturnValueOnce(false); + const { result } = renderHook(() => useShowTimeline()); expect(result.current).toEqual([false]); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts index d0e3f53ce771e..30f5b078770b5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts @@ -9,16 +9,19 @@ import { useCallback, useMemo } from 'react'; import { matchPath } from 'react-router-dom'; import { getLinksWithHiddenTimeline } from '../../links'; +import { SourcererScopeName } from '../../../sourcerer/store/model'; +import { useSourcererDataView } from '../../../sourcerer/containers'; import { useKibana } from '../../lib/kibana'; import { hasAccessToSecuritySolution } from '../../../helpers_access'; const isTimelinePathVisible = (currentPath: string): boolean => { const groupLinksWithHiddenTimelinePaths = getLinksWithHiddenTimeline().map((l) => l.path); - const hiddenTimelineRoutes = [...groupLinksWithHiddenTimelinePaths]; + const hiddenTimelineRoutes = groupLinksWithHiddenTimelinePaths; return !hiddenTimelineRoutes.find((route) => matchPath(currentPath, route)); }; export const useShowTimelineForGivenPath = () => { + const { indicesExist, dataViewId } = useSourcererDataView(SourcererScopeName.timeline); const { services: { application: { capabilities }, @@ -26,10 +29,9 @@ export const useShowTimelineForGivenPath = () => { } = useKibana(); const userHasSecuritySolutionVisible = hasAccessToSecuritySolution(capabilities); - // FIXME: this used to be () => userHasSecuritySolutionVisible && (indicesExist || dataViewId === null) const isTimelineAllowed = useMemo( - () => userHasSecuritySolutionVisible, - [userHasSecuritySolutionVisible] + () => userHasSecuritySolutionVisible && (indicesExist || dataViewId === null), + [indicesExist, dataViewId, userHasSecuritySolutionVisible] ); const getIsTimelineVisible = useCallback( diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx index 13c8d445f3271..2900f1196eff6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx @@ -13,13 +13,11 @@ import { getEsQueryConfig } from '@kbn/data-plugin/public'; import type { BulkActionsConfig } from '@kbn/response-ops-alerts-table/types'; import { dataTableActions, TableId, tableDefaults } from '@kbn/securitysolution-data-table'; import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy'; -import { useSelectedPatterns } from '../../../../data_view_picker/hooks/use_selected_patterns'; -import { useBrowserFields } from '../../../../data_view_picker/hooks/use_browser_fields'; -import { useDataView } from '../../../../data_view_picker/hooks/use_data_view'; import type { CustomBulkAction } from '../../../../../common/types'; import { combineQueries } from '../../../../common/lib/kuery'; import { useKibana } from '../../../../common/lib/kibana'; import { BULK_ADD_TO_TIMELINE_LIMIT } from '../../../../../common/constants'; +import { useSourcererDataView } from '../../../../sourcerer/containers'; import type { TimelineArgs } from '../../../../timelines/containers'; import { useTimelineEventsHandler } from '../../../../timelines/containers'; import { eventsViewerSelector } from '../../../../common/components/events_viewer/selectors'; @@ -65,11 +63,14 @@ export const useAddBulkToTimelineAction = ({ }: UseAddBulkToTimelineActionProps) => { const [disableActionOnSelectAll, setDisabledActionOnSelectAll] = useState(false); - const { dataView } = useDataView(scopeId); - const browserFields = useBrowserFields(scopeId); - const selectedPatterns = useSelectedPatterns(scopeId); - const dataViewId = dataView?.id ?? ''; - + const { + browserFields, + dataViewId, + sourcererDataView, + // important to get selectedPatterns from useSourcererDataView + // in order to include the exclude filters in the search that are not stored in the timeline + selectedPatterns, + } = useSourcererDataView(scopeId); const dispatch = useDispatch(); const { uiSettings } = useKibana().services; @@ -93,13 +94,13 @@ export const useAddBulkToTimelineAction = ({ return combineQueries({ config: esQueryConfig, dataProviders: [], - indexPattern: dataView, + indexPattern: sourcererDataView, filters: combinedFilters, kqlQuery: { query: '', language: 'kuery' }, browserFields, kqlMode: 'filter', }); - }, [esQueryConfig, dataView, combinedFilters, browserFields]); + }, [esQueryConfig, sourcererDataView, combinedFilters, browserFields]); const filterQuery = useMemo(() => { if (!combinedQuery) return ''; @@ -117,7 +118,7 @@ export const useAddBulkToTimelineAction = ({ sort: timelineQuerySortField, indexNames: selectedPatterns, filterQuery, - runtimeMappings: dataView.runtimeFieldMap as RunTimeMappings, + runtimeMappings: sourcererDataView.runtimeFieldMap as RunTimeMappings, limit: Math.min(BULK_ADD_TO_TIMELINE_LIMIT, totalCount), timerangeKind: 'absolute', }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx index f345c8b2fc9bb..2f76aeae8159e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx @@ -129,7 +129,7 @@ describe('GraphOverlay', () => { ); expect(useStateSyncingActionsMock.mock.calls[0][0].indices).toEqual( - mockGlobalState.dataViewPicker.analyzer.dataView?.title?.split(',') + mockGlobalState.sourcerer.sourcererScopes.analyzer.selectedPatterns ); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx index 0d352d26e586c..92f9b874f8743 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx @@ -21,6 +21,7 @@ jest.mock('react-redux', () => { return { ...original, + useSelector: jest.fn(), useDispatch: () => jest.fn(), }; }); @@ -51,7 +52,8 @@ describe('NewTimelineButton', () => { }); it('should call the correct action with clicking on the new timeline button', async () => { - const dataViewId = 'security-solution-default'; + const dataViewId = ''; + const selectedPatterns: string[] = []; const spy = jest.spyOn(timelineActions, 'createTimeline'); @@ -65,18 +67,7 @@ describe('NewTimelineButton', () => { columns: defaultUdtHeaders, dataViewId, id: TimelineId.test, - indexNames: [ - '.siem-signals-spacename', - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'traces-apm*', - 'winlogbeat-*', - '-*elastic-cloud-logs-*', - ], + indexNames: selectedPatterns, show: true, timelineType: 'default', updated: undefined, diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx index de09904ea7d6c..793cd12f99451 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx @@ -9,33 +9,59 @@ import React from 'react'; import { TestProviders } from '../../../../common/mock'; import { TimelineModalHeader } from '.'; import { render } from '@testing-library/react'; +import { useSourcererDataView } from '../../../../sourcerer/containers'; import { useCreateTimeline } from '../../../hooks/use_create_timeline'; import { useInspect } from '../../../../common/components/inspect/use_inspect'; import { useKibana } from '../../../../common/lib/kibana'; import { timelineActions } from '../../../store'; -import { TimelineId } from '../../../../../common/types'; +jest.mock('../../../../sourcerer/containers'); jest.mock('../../../hooks/use_create_timeline'); jest.mock('../../../../common/components/inspect/use_inspect'); jest.mock('../../../../common/lib/kibana'); -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), - useDispatch: () => jest.fn(), -})); +const mockGetState = jest.fn(); +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + return { + ...actual, + useDispatch: jest.fn(), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + useSelector: (selector: any) => + selector({ + timeline: { + timelineById: { + 'timeline-1': { + ...mockGetState(), + }, + }, + }, + }), + }; +}); + +const timelineId = 'timeline-1'; const mockRef = { current: null, }; const renderTimelineModalHeader = () => - render(, { - wrapper: TestProviders, - }); + render( + + + + ); describe('TimelineModalHeader', () => { (useCreateTimeline as jest.Mock).mockReturnValue(jest.fn()); (useInspect as jest.Mock).mockReturnValue(jest.fn()); it('should render all dom elements', () => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + browserFields: {}, + indexPattern: { fields: [], title: '' }, + sourcererDataView: {}, + }); + const { getByTestId, getByText } = renderTimelineModalHeader(); expect(getByTestId('timeline-favorite-empty-star')).toBeInTheDocument(); @@ -50,6 +76,11 @@ describe('TimelineModalHeader', () => { }); it('should show attach to case if user has the correct permissions', () => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + browserFields: {}, + indexPattern: { fields: [], title: '' }, + sourcererDataView: {}, + }); (useKibana as jest.Mock).mockReturnValue({ services: { application: { @@ -75,6 +106,12 @@ describe('TimelineModalHeader', () => { }); it('should call showTimeline action when closing timeline', () => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + browserFields: {}, + indexPattern: { fields: [], title: '' }, + sourcererDataView: {}, + }); + const spy = jest.spyOn(timelineActions, 'showTimeline'); const { getByTestId } = renderTimelineModalHeader(); @@ -82,7 +119,7 @@ describe('TimelineModalHeader', () => { getByTestId('timeline-modal-header-close-button').click(); expect(spy).toHaveBeenCalledWith({ - id: TimelineId.test, + id: timelineId, show: false, }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.tsx index df7c978000acb..e42e856b9ca74 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.tsx @@ -18,7 +18,6 @@ import { useDispatch, useSelector } from 'react-redux'; import { getEsQueryConfig } from '@kbn/data-plugin/common'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; import styled from 'styled-components'; -import { useDataView } from '../../../../data_view_picker/hooks/use_data_view'; import { NewTimelineButton } from '../actions/new_timeline_button'; import { OpenTimelineButton } from '../actions/open_timeline_button'; import { APP_ID } from '../../../../../common'; @@ -32,7 +31,9 @@ import { createHistoryEntry } from '../../../../common/utils/global_query_string import { timelineActions } from '../../../store'; import type { State } from '../../../../common/store'; import { useKibana } from '../../../../common/lib/kibana'; +import { useSourcererDataView } from '../../../../sourcerer/containers'; import { combineQueries } from '../../../../common/lib/kuery'; +import { SourcererScopeName } from '../../../../sourcerer/store/model'; import * as i18n from '../translations'; import { AddToFavoritesButton } from '../../add_to_favorites'; import { TimelineSaveStatus } from '../../save_status'; @@ -40,8 +41,6 @@ import { InspectButton } from '../../../../common/components/inspect'; import { InputsModelId } from '../../../../common/store/inputs/constants'; import { AttachToCaseButton } from '../actions/attach_to_case_button'; import { SaveTimelineButton } from '../actions/save_timeline_button'; -import { useBrowserFields } from '../../../../data_view_picker/hooks/use_browser_fields'; -import { DataViewPickerScopeName } from '../../../../data_view_picker/constants'; const whiteSpaceNoWrapCSS = { 'white-space': 'nowrap' }; const autoOverflowXCSS = { 'overflow-x': 'auto' }; @@ -71,10 +70,7 @@ interface FlyoutHeaderPanelProps { export const TimelineModalHeader = React.memo( ({ timelineId, openToggleRef }) => { const dispatch = useDispatch(); - - const { dataView: sourcererDataView } = useDataView(DataViewPickerScopeName.timeline); - const browserFields = useBrowserFields(DataViewPickerScopeName.timeline); - + const { browserFields, sourcererDataView } = useSourcererDataView(SourcererScopeName.timeline); const { cases, uiSettings } = useKibana().services; const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx index adea6b3526a8b..0d1ed98f0bc99 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx @@ -26,6 +26,7 @@ jest.mock('react-redux', () => { return { ...original, + useSelector: jest.fn(), useDispatch: () => jest.fn(), }; }); @@ -36,19 +37,8 @@ const renderNewTimelineButton = (type: TimelineType) => render(, { wrapper: TestProviders }); describe('NewTimelineButton', () => { - const dataViewId = 'security-solution-default'; - const selectedPatterns: string[] = [ - '.siem-signals-spacename', - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'traces-apm*', - 'winlogbeat-*', - '-*elastic-cloud-logs-*', - ]; + const dataViewId = ''; + const selectedPatterns: string[] = []; (useDiscoverInTimelineContext as jest.Mock).mockReturnValue({ resetDiscoverAppState: jest.fn(), }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index d427f8cf4c89e..7cd7cbe18927f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -9,7 +9,6 @@ import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { encode } from '@kbn/rison'; -import { useSelectedPatterns } from '../../../data_view_picker/hooks/use_selected_patterns'; import { RULE_FROM_EQL_URL_PARAM, RULE_FROM_TIMELINE_URL_PARAM, @@ -52,11 +51,11 @@ import { useTimelineStatus } from './use_timeline_status'; import { deleteTimelinesByIds } from '../../containers/api'; import type { Direction } from '../../../../common/search_strategy'; import { SourcererScopeName } from '../../../sourcerer/store/model'; +import { useSourcererDataView } from '../../../sourcerer/containers'; import { useStartTransaction } from '../../../common/lib/apm/use_start_transaction'; import { TIMELINE_ACTIONS } from '../../../common/lib/apm/user_actions'; import { defaultUdtHeaders } from '../timeline/body/column_headers/default_headers'; import { timelineDefaults } from '../../store/defaults'; -import { useDataView } from '../../../data_view_picker/hooks/use_data_view'; interface OwnProps { /** Displays open timeline in modal */ @@ -158,8 +157,7 @@ export const StatefulOpenTimelineComponent = React.memo( (state) => getTimeline(state, TimelineId.active)?.savedObjectId ?? '' ); - const { dataView } = useDataView(SourcererScopeName.timeline); - const selectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); + const { dataViewId, selectedPatterns } = useSourcererDataView(SourcererScopeName.timeline); const { customTemplateTimelineCount, @@ -249,7 +247,7 @@ export const StatefulOpenTimelineComponent = React.memo( dispatchCreateNewTimeline({ id: TimelineId.active, columns: defaultUdtHeaders, - dataViewId: dataView.id ?? '', + dataViewId, indexNames: selectedPatterns, show: false, excludedRowRendererIds: timelineDefaults.excludedRowRendererIds, @@ -260,7 +258,7 @@ export const StatefulOpenTimelineComponent = React.memo( await deleteTimelinesByIds(timelineIds, searchIds); refetch(); }, - [startTransaction, timelineSavedObjectId, refetch, dispatch, dataView.id, selectedPatterns] + [startTransaction, timelineSavedObjectId, refetch, dispatch, dataViewId, selectedPatterns] ); const onDeleteOneTimeline: OnDeleteOneTimeline = useCallback( diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx index 963b184a6415e..4080e254303a2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx @@ -20,7 +20,6 @@ import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; -import { useSelectedPatterns } from '../../../../data_view_picker/hooks/use_selected_patterns'; import { useKibana } from '../../../../common/lib/kibana'; import { DocumentDetailsRightPanelKey } from '../../../../flyout/document_details/shared/constants/panel_keys'; import type { TimelineResultNote } from '../types'; @@ -32,6 +31,7 @@ import * as i18n from './translations'; import { TimelineId } from '../../../../../common/types/timeline'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; +import { useSourcererDataView } from '../../../../sourcerer/containers'; import { useDeleteNote } from './hooks/use_delete_note'; import { getTimelineNoteSelector } from '../../timeline/tabs/notes/selectors'; import { DocumentEventTypes } from '../../../../common/lib/telemetry'; @@ -52,7 +52,7 @@ const ToggleEventDetailsButtonComponent: React.FC eventId, timelineId, }) => { - const selectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); + const { selectedPatterns } = useSourcererDataView(SourcererScopeName.timeline); const { telemetry } = useKibana().services; const { openFlyout } = useExpandableFlyoutApi(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx index 289cdf42c5948..2927b2365221d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx @@ -8,13 +8,13 @@ import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { isEmpty } from 'lodash/fp'; -import { DataViewPickerScopeName } from '../../../data_view_picker/constants'; -import { useSelectDataView } from '../../../data_view_picker/hooks/use_select_data_view'; import type { Note } from '../../../../common/api/timeline'; import { TimelineStatusEnum, TimelineTypeEnum } from '../../../../common/api/timeline'; import { createNote } from '../notes/helpers'; import { InputsModelId } from '../../../common/store/inputs/constants'; +import { sourcererActions } from '../../../sourcerer/store'; +import { SourcererScopeName } from '../../../sourcerer/store/model'; import { addNotes as dispatchAddNotes, updateNote as dispatchUpdateNote, @@ -36,7 +36,6 @@ import type { UpdateTimeline } from './types'; export const useUpdateTimeline = () => { const dispatch = useDispatch(); - const selectDataView = useSelectDataView(); return useCallback( ({ @@ -59,11 +58,13 @@ export const useUpdateTimeline = () => { _timeline = { ...timeline, updated: undefined, changed: true, version: null }; } if (!isEmpty(_timeline.indexNames)) { - selectDataView({ - id: _timeline.dataViewId, - patterns: _timeline.indexNames, - scope: [DataViewPickerScopeName.timeline], - }); + dispatch( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.timeline, + selectedDataViewId: _timeline.dataViewId, + selectedPatterns: _timeline.indexNames, + }) + ); } if ( _timeline.status === TimelineStatusEnum.immutable && @@ -137,6 +138,6 @@ export const useUpdateTimeline = () => { ); } }, - [dispatch, selectDataView] + [dispatch] ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx index 746c511b23d4f..7a90b5254e445 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx @@ -13,9 +13,9 @@ import { v4 as uuidv4 } from 'uuid'; import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid'; import { EuiToolTip, EuiSuperSelect, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; -import { useBrowserFields } from '../../../../data_view_picker/hooks/use_browser_fields'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; +import { useSourcererDataView } from '../../../../sourcerer/containers'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper'; import { droppableTimelineProvidersPrefix } from '../../../../common/components/drag_and_drop/helpers'; @@ -106,7 +106,7 @@ const CustomTooltipDiv = styled.div` export const DataProviders = React.memo(({ timelineId }) => { const dispatch = useDispatch(); - const browserFields = useBrowserFields(SourcererScopeName.timeline); + const { browserFields } = useSourcererDataView(SourcererScopeName.timeline); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const dataProviders = useDeepEqualSelector( diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx index 11c69dc4a5698..dce963737fb5a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx @@ -11,8 +11,8 @@ import { useSelector } from 'react-redux'; import { getEsQueryConfig } from '@kbn/data-plugin/common'; import type { TimerangeInput } from '@kbn/timelines-plugin/common'; import { EuiPanel } from '@elastic/eui'; -import { useBrowserFields } from '../../../../data_view_picker/hooks/use_browser_fields'; import { TimelineId } from '../../../../../common/types'; +import { useSourcererDataView } from '../../../../sourcerer/containers'; import type { State } from '../../../../common/store'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; @@ -26,17 +26,15 @@ import { endSelector, startSelector, } from '../../../../common/components/super_date_picker/selectors'; -import { useSelectedPatterns } from '../../../../data_view_picker/hooks/use_selected_patterns'; -import { useDataView } from '../../../../data_view_picker/hooks/use_data_view'; interface KpiExpandedProps { timelineId: string; } export const TimelineKpisContainer = ({ timelineId }: KpiExpandedProps) => { - const browserFields = useBrowserFields(SourcererScopeName.timeline); - const { dataView: sourcererDataView } = useDataView(SourcererScopeName.timeline); - const selectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); + const { browserFields, sourcererDataView, selectedPatterns } = useSourcererDataView( + SourcererScopeName.timeline + ); const { uiSettings } = useKibana().services; const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/eql/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/eql/index.tsx index b6257d848e5c3..c378f5290cde2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/eql/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/eql/index.tsx @@ -11,8 +11,8 @@ import { EuiOutsideClickDetector } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import { css } from '@emotion/css'; -import { useDataView } from '../../../../../data_view_picker/hooks/use_data_view'; import type { EqlOptions } from '../../../../../../common/search_strategy'; +import { useSourcererDataView } from '../../../../../sourcerer/containers'; import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; import { SourcererScopeName } from '../../../../../sourcerer/store/model'; import { EqlQueryEdit } from '../../../../../detection_engine/rule_creation/components/eql_query_edit'; @@ -22,7 +22,6 @@ import type { FormSchema, FormSubmitHandler } from '../../../../../shared_import import { Form, UseField, useForm } from '../../../../../shared_imports'; import { timelineActions } from '../../../../store'; import { getEqlOptions } from './selectors'; -import { useSelectedPatterns } from '../../../../../data_view_picker/hooks/use_selected_patterns'; interface TimelineEqlQueryBar { index: string[]; @@ -60,9 +59,11 @@ export const EqlQueryBarTimeline = memo(({ timelineId }: { timelineId: string }) const getOptionsSelected = useMemo(() => getEqlOptions(), []); const eqlOptions = useDeepEqualSelector((state) => getOptionsSelected(state, timelineId)); - const { dataView: sourcererDataView, status } = useDataView(SourcererScopeName.timeline); - const selectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); - const indexPatternsLoading = status !== 'ready'; + const { + loading: indexPatternsLoading, + sourcererDataView, + selectedPatterns, + } = useSourcererDataView(SourcererScopeName.timeline); const initialState = useMemo( () => ({ @@ -164,7 +165,7 @@ export const EqlQueryBarTimeline = memo(({ timelineId }: { timelineId: string }) /* Force casting `sourcererDataView` to `DataViewBase` is required since EqlQueryEdit accepts DataViewBase but `useSourcererDataView()` returns `DataViewSpec`. - + When using `UseField` with `EqlQueryBar` such casting isn't required by TS since `UseField` component props are types as `Record`. */ return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx index 5984065c7e47b..1bb39aa4796d2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx @@ -13,8 +13,8 @@ import type { Filter, Query } from '@kbn/es-query'; import { FilterStateStore } from '@kbn/es-query'; import type { FilterManager, SavedQuery, SavedQueryTimeFilter } from '@kbn/data-plugin/public'; import styled from '@emotion/styled'; -import { useDataView } from '../../../../data_view_picker/hooks/use_data_view'; import { InputsModelId } from '../../../../common/store/inputs/constants'; +import { useSourcererDataView } from '../../../../sourcerer/containers'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { @@ -29,7 +29,6 @@ import type { DataProvider } from '../data_providers/data_provider'; import { TIMELINE_FILTER_DROP_AREA, buildGlobalQuery, getNonDropAreaFilters } from '../helpers'; import { timelineActions } from '../../../store'; import type { KueryFilterQuery, KueryFilterQueryKind } from '../../../../../common/types/timeline'; -import { useBrowserFields } from '../../../../data_view_picker/hooks/use_browser_fields'; export interface QueryBarTimelineComponentProps { dataProviders: DataProvider[]; @@ -111,9 +110,7 @@ export const QueryBarTimeline = memo( const [dateRangeTo, setDateRangTo] = useState( toStr != null ? toStr : new Date(to).toISOString() ); - const { dataView: sourcererDataView } = useDataView(SourcererScopeName.timeline); - const browserFields = useBrowserFields(SourcererScopeName.timeline); - + const { browserFields, sourcererDataView } = useSourcererDataView(SourcererScopeName.timeline); const [savedQuery, setSavedQuery] = useState(undefined); const [filterQueryConverted, setFilterQueryConverted] = useState({ query: filterQuery != null ? filterQuery.expression : '', diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx index bc225141b37c0..d3233858813ae 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx @@ -17,11 +17,11 @@ import type { FilterManager } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; import { FilterItems } from '@kbn/unified-search-plugin/public'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useDataView } from '../../../../data_view_picker/hooks/use_data_view'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { useKibana } from '../../../../common/lib/kibana'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; +import { useSourcererDataView } from '../../../../sourcerer/containers'; import type { State, inputsModel } from '../../../../common/store'; import { inputsSelectors } from '../../../../common/store'; import { timelineActions, timelineSelectors } from '../../../store'; @@ -73,7 +73,7 @@ const StatefulSearchOrFilterComponent = React.memo( services: { data }, } = useKibana(); - const { dataView: sourcererDataView } = useDataView(SourcererScopeName.timeline); + const { sourcererDataView } = useSourcererDataView(SourcererScopeName.timeline); const getIsDataProviderVisible = useMemo( () => timelineSelectors.dataProviderVisibilitySelector(), diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx index f3fc346b09b03..3ec2a965d8021 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx @@ -11,7 +11,6 @@ import styled from 'styled-components'; import type { Filter } from '@kbn/es-query'; import type { FilterManager } from '@kbn/data-plugin/public'; -import { DataViewPicker } from '../../../../data_view_picker/components/data_view_picker'; import { type TimelineType, TimelineTypeEnum } from '../../../../../common/api/timeline'; import { InputsModelId } from '../../../../common/store/inputs/constants'; import type { KqlMode } from '../../../store/model'; @@ -23,6 +22,7 @@ import { QueryBarTimeline } from '../query_bar'; import { TimelineDatePickerLock } from '../date_picker_lock'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; +import { Sourcerer } from '../../../../sourcerer/components'; import { DATA_PROVIDER_HIDDEN_EMPTY, DATA_PROVIDER_HIDDEN_POPULATED, @@ -112,7 +112,7 @@ export const SearchOrFilter = React.memo( responsive={false} > - + = ({ timelineId }) => { const history = useHistory(); const { - services: { customDataService: discoverDataService, discover, savedSearch: savedSearchService }, + services: { + customDataService: discoverDataService, + discover, + dataViews: dataViewService, + savedSearch: savedSearchService, + }, } = useKibana(); const { timelinePrivileges: { crud: canSaveTimeline }, @@ -53,8 +59,9 @@ export const DiscoverTabContent: FC = ({ timelineId }) const dispatch = useDispatch(); - const { dataView } = useDataView(SourcererScopeName.detections); + const { dataViewId } = useSourcererDataView(SourcererScopeName.detections); + const [dataView, setDataView] = useState(); const [discoverTimerange, setDiscoverTimerange] = useState(); const discoverAppStateSubscription = useRef(); @@ -153,6 +160,11 @@ export const DiscoverTabContent: FC = ({ timelineId }) canSaveTimeline, ]); + useEffect(() => { + if (!dataViewId) return; + dataViewService.get(dataViewId).then(setDataView); + }, [dataViewId, dataViewService]); + useEffect(() => { const unSubscribeAll = () => { [ diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx index 42579d3ab303f..f70d51de3d755 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx @@ -13,8 +13,6 @@ import deepEqual from 'fast-deep-equal'; import type { EuiDataGridControlColumn } from '@elastic/eui'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy'; -import { useDataView } from '../../../../../data_view_picker/hooks/use_data_view'; -import { useSelectedPatterns } from '../../../../../data_view_picker/hooks/use_selected_patterns'; import { useFetchNotes } from '../../../../../notes/hooks/use_fetch_notes'; import { DocumentDetailsLeftPanelKey, @@ -28,6 +26,7 @@ import { requiredFieldsForActions } from '../../../../../detections/components/a import { SourcererScopeName } from '../../../../../sourcerer/store/model'; import { timelineDefaults } from '../../../../store/defaults'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +import { useSourcererDataView } from '../../../../../sourcerer/containers'; import type { TimelineModel } from '../../../../store/model'; import type { State } from '../../../../../common/store'; import { TimelineTabs } from '../../../../../../common/types/timeline'; @@ -79,9 +78,9 @@ export const PinnedTabContentComponent: React.FC = ({ const [pageIndex, setPageIndex] = useState(0); const { telemetry } = useKibana().services; - - const selectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); - const { dataView } = useDataView(SourcererScopeName.timeline); + const { dataViewId, sourcererDataView, selectedPatterns } = useSourcererDataView( + SourcererScopeName.timeline + ); const filterQuery = useMemo(() => { if (isEmpty(pinnedEventIds)) { @@ -145,11 +144,11 @@ export const PinnedTabContentComponent: React.FC = ({ endDate: '', id: `pinned-${timelineId}`, indexNames: selectedPatterns, - dataViewId: dataView.id ?? '', + dataViewId, fields: timelineQueryFields, limit: itemsPerPage, filterQuery, - runtimeMappings: dataView.runtimeFieldMap as RunTimeMappings, + runtimeMappings: sourcererDataView.runtimeFieldMap as RunTimeMappings, skip: filterQuery === '', startDate: '', sort: timelineQuerySortField, diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx index 5828807649228..746b3b1ceb1dd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx @@ -11,6 +11,8 @@ import QueryTabContent from '.'; import { defaultRowRenderers } from '../../body/renderers'; import { TimelineId } from '../../../../../../common/types/timeline'; import { useTimelineEventsDetails } from '../../../../containers/details'; +import { useSourcererDataView } from '../../../../../sourcerer/containers'; +import { mockSourcererScope } from '../../../../../sourcerer/containers/mocks'; import { createMockStore, createSecuritySolutionStorageMock, @@ -173,6 +175,10 @@ const renderTestComponents = (props?: Partial { (useTimelineEventsDetails as jest.Mock).mockImplementation(() => [false, {}]); + (useSourcererDataView as jest.Mock).mockImplementation(useSourcererDataViewMocked); + (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation( useIsExperimentalFeatureEnabledMock ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx index 0a8944a6006f2..7a08a22ef5a81 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx @@ -15,9 +15,6 @@ import { getEsQueryConfig } from '@kbn/data-plugin/common'; import { DataLoadingState } from '@kbn/unified-data-table'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy'; -import { useSelectedPatterns } from '../../../../../data_view_picker/hooks/use_selected_patterns'; -import { useBrowserFields } from '../../../../../data_view_picker/hooks/use_browser_fields'; -import { useDataView } from '../../../../../data_view_picker/hooks/use_data_view'; import { useFetchNotes } from '../../../../../notes/hooks/use_fetch_notes'; import { DocumentDetailsLeftPanelKey, @@ -42,6 +39,7 @@ import type { inputsModel, State } from '../../../../../common/store'; import { inputsSelectors } from '../../../../../common/store'; import { SourcererScopeName } from '../../../../../sourcerer/store/model'; import { timelineDefaults } from '../../../../store/defaults'; +import { useSourcererDataView } from '../../../../../sourcerer/containers'; import { isActiveTimeline } from '../../../../../helpers'; import type { TimelineModel } from '../../../../store/model'; import { UnifiedTimelineBody } from '../../body/unified_timeline_body'; @@ -85,12 +83,15 @@ export const QueryTabContentComponent: React.FC = ({ eventIdToNoteIds, }) => { const dispatch = useDispatch(); - - const { dataView, status: sourcererStatus } = useDataView(SourcererScopeName.timeline); - const browserFields = useBrowserFields(SourcererScopeName.timeline); - const loadingSourcerer = sourcererStatus !== 'ready'; - const selectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); - + const { + browserFields, + dataViewId, + loading: loadingSourcerer, + // important to get selectedPatterns from useSourcererDataView + // in order to include the exclude filters in the search that are not stored in the timeline + selectedPatterns, + sourcererDataView, + } = useSourcererDataView(SourcererScopeName.timeline); /* * `pageIndex` needs to be maintained for each table in each tab independently * and consequently it cannot be the part of common redux state @@ -125,13 +126,13 @@ export const QueryTabContentComponent: React.FC = ({ return combineQueries({ config: esQueryConfig, dataProviders, - indexPattern: dataView, + indexPattern: sourcererDataView, browserFields, filters, kqlQuery, kqlMode, }); - }, [esQueryConfig, dataProviders, dataView, browserFields, filters, kqlQuery, kqlMode]); + }, [esQueryConfig, dataProviders, sourcererDataView, browserFields, filters, kqlQuery, kqlMode]); useInvalidFilterQuery({ id: timelineId, @@ -173,7 +174,7 @@ export const QueryTabContentComponent: React.FC = ({ const [dataLoadingState, { events, inspect, totalCount, loadNextBatch, refreshedAt, refetch }] = useTimelineEvents({ - dataViewId: dataView.id ?? '', + dataViewId, endDate: end, fields: timelineQueryFieldsFromColumns, filterQuery: combinedQueries?.filterQuery, @@ -181,7 +182,7 @@ export const QueryTabContentComponent: React.FC = ({ indexNames: selectedPatterns, language: kqlQuery.language, limit: sampleSize, - runtimeMappings: dataView.runtimeFieldMap as RunTimeMappings, + runtimeMappings: sourcererDataView.runtimeFieldMap as RunTimeMappings, skip: !canQueryTimeline, sort: timelineQuerySortField, startDate: start, diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx index c4b6ddc6b12fe..6460dc220a934 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx @@ -18,8 +18,8 @@ import { import { useDispatch } from 'react-redux'; import { dataTableSelectors, tableDefaults } from '@kbn/securitysolution-data-table'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; -import { useSelectedPatterns } from '../../../../../data_view_picker/hooks/use_selected_patterns'; import { DocumentDetailsRightPanelKey } from '../../../../../flyout/document_details/shared/constants/panel_keys'; +import { useSourcererDataView } from '../../../../../sourcerer/containers'; import { getScopedActions, isActiveTimeline, @@ -279,7 +279,7 @@ export const useSessionView = ({ scopeId, height }: { scopeId: string; height?: [globalFullScreen, scopeId, timelineFullScreen] ); - const selectedPatterns = useSelectedPatterns(SourcererScopeName.detections); + const { selectedPatterns } = useSourcererDataView(SourcererScopeName.detections); const alertsIndex = useMemo(() => selectedPatterns.join(','), [selectedPatterns]); const { openFlyout } = useExpandableFlyoutApi(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx index 6dc26bc50ac08..006c6ba1eb679 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx @@ -6,15 +6,15 @@ */ import { useMemo } from 'react'; -import { useBrowserFields } from '../../../../../data_view_picker/hooks/use_browser_fields'; import { SourcererScopeName } from '../../../../../sourcerer/store/model'; +import { useSourcererDataView } from '../../../../../sourcerer/containers'; import { requiredFieldsForActions } from '../../../../../detections/components/alerts_table/default_config'; import { defaultUdtHeaders } from '../../body/column_headers/default_headers'; import type { ColumnHeaderOptions } from '../../../../../../common/types'; import { memoizedGetTimelineColumnHeaders } from './utils'; export const useTimelineColumns = (columns: ColumnHeaderOptions[]) => { - const browserFields = useBrowserFields(SourcererScopeName.timeline); + const { browserFields } = useSourcererDataView(SourcererScopeName.timeline); const localColumns = useMemo(() => columns ?? defaultUdtHeaders, [columns]); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx index 4f9534e164da7..872f3c760f38b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx @@ -500,7 +500,7 @@ describe('unified timeline', () => { expect(kibanaServiceMock.dataViewFieldEditor.openEditor).toHaveBeenNthCalledWith(1, { ctx: { dataView: expect.objectContaining({ - id: 'security-solution-default', + id: 'security-solution', }), }, fieldName: 'message', diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts index 26682468a056b..cd27ff06020ba 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts @@ -13,7 +13,7 @@ import { endSelector, } from '../../common/components/super_date_picker/selectors'; import { SourcererScopeName } from '../../sourcerer/store/model'; -import { useSelectedPatterns } from '../../data_view_picker/hooks/use_selected_patterns'; +import { useSourcererDataView } from '../../sourcerer/containers'; export function useTimelineDataFilters(isActiveTimelines: boolean) { const getStartSelector = useMemo(() => startSelector(), []); @@ -42,7 +42,7 @@ export function useTimelineDataFilters(isActiveTimelines: boolean) { } }); - const analyzerPatterns = useSelectedPatterns(SourcererScopeName.analyzer); + const { selectedPatterns: analyzerPatterns } = useSourcererDataView(SourcererScopeName.analyzer); return useMemo(() => { return { diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx index e684cd17d3bda..0864c2bb024bd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx @@ -13,19 +13,12 @@ import { TimelineId } from '../../../common/types'; import { useDiscoverInTimelineContext } from '../../common/components/discover_in_timeline/use_discover_in_timeline_context'; import { timelineActions } from '../store'; import { inputsActions } from '../../common/store/inputs'; +import { sourcererActions } from '../../sourcerer/store'; import { appActions } from '../../common/store/app'; import { SourcererScopeName } from '../../sourcerer/store/model'; import { InputsModelId } from '../../common/store/inputs/constants'; import { TestProviders, mockGlobalState } from '../../common/mock'; import { defaultUdtHeaders } from '../components/timeline/body/column_headers/default_headers'; -import { useSelectDataView } from '../../data_view_picker/hooks/use_select_data_view'; - -jest.mock('../../data_view_picker/hooks/use_select_data_view', () => { - const mock = jest.fn(); - return { - useSelectDataView: () => mock, - }; -}); jest.mock('../../common/components/discover_in_timeline/use_discover_in_timeline_context'); jest.mock('../../common/containers/use_global_time', () => { @@ -62,11 +55,10 @@ describe('useCreateTimeline', () => { it('should dispatch correct actions when calling the returned function', async () => { const createTimeline = jest.spyOn(timelineActions, 'createTimeline'); + const setSelectedDataView = jest.spyOn(sourcererActions, 'setSelectedDataView'); const addLinkTo = jest.spyOn(inputsActions, 'addLinkTo'); const addNotes = jest.spyOn(appActions, 'addNotes'); - const setSelectedDataView = jest.mocked(useSelectDataView()); - const hookResult = renderHook( () => useCreateTimeline({ timelineId: TimelineId.test, timelineType: TimelineTypeEnum.default }), @@ -84,7 +76,7 @@ describe('useCreateTimeline', () => { expect(createTimeline.mock.calls[0][0].timelineType).toEqual(TimelineTypeEnum.default); expect(createTimeline.mock.calls[0][0].columns).toEqual(defaultUdtHeaders); expect(createTimeline.mock.calls[0][0].dataViewId).toEqual( - mockGlobalState.dataViewPicker.default.dataView?.id + mockGlobalState.sourcerer.defaultDataView.id ); expect(createTimeline.mock.calls[0][0].indexNames).toEqual( expect.arrayContaining( @@ -94,11 +86,11 @@ describe('useCreateTimeline', () => { expect(createTimeline.mock.calls[0][0].show).toEqual(true); expect(createTimeline.mock.calls[0][0].updated).toEqual(undefined); expect(createTimeline.mock.calls[0][0].excludedRowRendererIds).toHaveLength(RowRendererCount); - expect(setSelectedDataView.mock.calls[0][0].scope).toEqual([SourcererScopeName.timeline]); - expect(setSelectedDataView.mock.calls[0][0].id).toEqual( - mockGlobalState.dataViewPicker.default.dataView?.id + 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].patterns).toEqual( + expect(setSelectedDataView.mock.calls[0][0].selectedPatterns).toEqual( expect.arrayContaining( mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline].selectedPatterns ) diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx index 821710ec23808..2f80e969cab5e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx @@ -6,7 +6,7 @@ */ import { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { InputsModelId } from '../../common/store/inputs/constants'; import { timelineActions } from '../store'; import { useTimelineFullScreen } from '../../common/containers/use_full_screen'; @@ -14,15 +14,13 @@ import { TimelineId } from '../../../common/types/timeline'; import { type TimelineType, TimelineTypeEnum } from '../../../common/api/timeline'; import { useDeepEqualSelector } from '../../common/hooks/use_selector'; import { inputsActions, inputsSelectors } from '../../common/store/inputs'; +import { sourcererActions, sourcererSelectors } from '../../sourcerer/store'; +import { SourcererScopeName } from '../../sourcerer/store/model'; import { appActions } from '../../common/store/app'; import type { TimeRange } from '../../common/store/inputs/model'; import { useDiscoverInTimelineContext } from '../../common/components/discover_in_timeline/use_discover_in_timeline_context'; import { defaultUdtHeaders } from '../components/timeline/body/column_headers/default_headers'; import { timelineDefaults } from '../store/defaults'; -import { useSelectDataView } from '../../data_view_picker/hooks/use_select_data_view'; -import { DataViewPickerScopeName } from '../../data_view_picker/constants'; -import { useDataView } from '../../data_view_picker/hooks/use_data_view'; -import { useSelectedPatterns } from '../../data_view_picker/hooks/use_selected_patterns'; export interface UseCreateTimelineParams { /** @@ -50,17 +48,15 @@ export const useCreateTimeline = ({ onClick, }: UseCreateTimelineParams): ((options?: { timeRange?: TimeRange }) => Promise) => { const dispatch = useDispatch(); - const { dataView } = useDataView(DataViewPickerScopeName.default); - const dataViewId = dataView.id ?? ''; - const selectedPatterns = useSelectedPatterns(DataViewPickerScopeName.default); + const { id: dataViewId, patternList: selectedPatterns } = useSelector( + sourcererSelectors.defaultDataView + ) ?? { id: '', patternList: [] }; const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); const globalTimeRange = useDeepEqualSelector(inputsSelectors.globalTimeRangeSelector); const { resetDiscoverAppState } = useDiscoverInTimelineContext(); - const setSelectedDataView = useSelectDataView(); - const createTimeline = useCallback( ({ id, @@ -76,12 +72,13 @@ export const useCreateTimeline = ({ if (id === TimelineId.active && timelineFullScreen) { setTimelineFullScreen(false); } - - setSelectedDataView({ - id: dataViewId, - patterns: selectedPatterns, - scope: [DataViewPickerScopeName.timeline], - }); + dispatch( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.timeline, + selectedDataViewId: dataViewId, + selectedPatterns, + }) + ); dispatch( timelineActions.createTimeline({ @@ -123,14 +120,13 @@ export const useCreateTimeline = ({ } }, [ - globalTimeRange, - timelineFullScreen, dispatch, + globalTimeRange, dataViewId, selectedPatterns, - setSelectedDataView, - timelineType, setTimelineFullScreen, + timelineFullScreen, + timelineType, ] ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx index d3901b05ae1e5..ae82e2a381010 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx @@ -5,13 +5,12 @@ * 2.0. */ -import { render } from '@testing-library/react'; -import type { PropsWithChildren } from 'react'; +import type { ShallowWrapper } from 'enzyme'; +import { shallow } from 'enzyme'; import React from 'react'; import { TimelinesPage } from './timelines_page'; -import { useDataView } from '../../data_view_picker/hooks/use_data_view'; +import { useSourcererDataView } from '../../sourcerer/containers'; import { useUserPrivileges } from '../../common/components/user_privileges'; -import { TestProviders } from '../../common/mock'; jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); @@ -24,22 +23,16 @@ jest.mock('react-router-dom', () => { }; }); jest.mock('../../overview/components/events_by_dataset'); +jest.mock('../../sourcerer/containers'); jest.mock('../../common/components/user_privileges'); -jest.mock('../../data_view_picker/hooks/use_data_view'); - -jest.mock('../../common/components/security_route_page_wrapper', () => ({ - SecurityRoutePageWrapper: (props: PropsWithChildren<{}>) => <>{props.children}, -})); -jest.mock('../components/open_timeline', () => ({ - StatefulOpenTimeline: () =>

, -})); describe('TimelinesPage', () => { + let wrapper: ShallowWrapper; + it('should render landing page if no indicesExist', () => { - jest.mocked(useDataView).mockReturnValue({ + (useSourcererDataView as unknown as jest.Mock).mockReturnValue({ indicesExist: false, - dataView: {}, - status: 'ready', + sourcererDataView: {}, }); (useUserPrivileges as jest.Mock).mockReturnValue({ timelinePrivileges: { @@ -47,18 +40,17 @@ describe('TimelinesPage', () => { }, }); - const wrapper = render(, { wrapper: TestProviders }); + wrapper = shallow(); - expect(wrapper.queryByTestId('timelines-page-open-import-data')).toBeFalsy(); - expect(wrapper.queryByTestId('timelines-page-new')).toBeFalsy(); - expect(wrapper.queryByTestId('stateful-open-timeline')).toBeFalsy(); + expect(wrapper.exists('[data-test-subj="timelines-page-open-import-data"]')).toBeFalsy(); + expect(wrapper.exists('[data-test-subj="timelines-page-new"]')).toBeFalsy(); + expect(wrapper.exists('[data-test-subj="stateful-open-timeline"]')).toBeFalsy(); }); it('should show the correct elements if user has crud', () => { - jest.mocked(useDataView).mockReturnValue({ + (useSourcererDataView as unknown as jest.Mock).mockReturnValue({ indicesExist: true, - dataView: {}, - status: 'ready', + sourcererDataView: {}, }); (useUserPrivileges as jest.Mock).mockReturnValue({ timelinePrivileges: { @@ -66,11 +58,11 @@ describe('TimelinesPage', () => { }, }); - const wrapper = render(, { wrapper: TestProviders }); + wrapper = shallow(); - expect(wrapper.queryByTestId('timelines-page-open-import-data')).toBeTruthy(); - expect(wrapper.queryByTestId('timelines-page-new')).toBeTruthy(); - expect(wrapper.queryByTestId('stateful-open-timeline')).toBeTruthy(); + expect(wrapper.exists('[data-test-subj="timelines-page-open-import-data"]')).toBeTruthy(); + expect(wrapper.exists('[data-test-subj="timelines-page-new"]')).toBeTruthy(); + expect(wrapper.exists('[data-test-subj="stateful-open-timeline"]')).toBeTruthy(); }); it('should not show import button or modal if user does not have crud privileges but it should show the new timeline button', () => { @@ -81,10 +73,10 @@ describe('TimelinesPage', () => { }, }); - const wrapper = render(, { wrapper: TestProviders }); + wrapper = shallow(); - expect(wrapper.queryByTestId('timelines-page-open-import-data')).toBeFalsy(); - expect(wrapper.queryByTestId('timelines-page-new')).toBeTruthy(); - expect(wrapper.queryByTestId('stateful-open-timeline')).toBeTruthy(); + expect(wrapper.exists('[data-test-subj="timelines-page-open-import-data"]')).toBeFalsy(); + expect(wrapper.exists('[data-test-subj="timelines-page-new"]')).toBeTruthy(); + expect(wrapper.exists('[data-test-subj="stateful-open-timeline"]')).toBeTruthy(); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.tsx index ac08654129139..d213eeaf8f5f0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.tsx @@ -16,17 +16,15 @@ import { useUserPrivileges } from '../../common/components/user_privileges'; import { StatefulOpenTimeline } from '../components/open_timeline'; import * as i18n from './translations'; import { SecurityPageName } from '../../app/types'; +import { useSourcererDataView } from '../../sourcerer/containers'; import { EmptyPrompt } from '../../common/components/empty_prompt'; import { SecurityRoutePageWrapper } from '../../common/components/security_route_page_wrapper'; -import { useDataView } from '../../data_view_picker/hooks/use_data_view'; -import { DataViewPickerScopeName } from '../../data_view_picker/constants'; export const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10; export const TimelinesPage = React.memo(() => { const { tabName } = useParams<{ pageName: SecurityPageName; tabName: string }>(); - const { indicesExist } = useDataView(DataViewPickerScopeName.default); - + const { indicesExist } = useSourcererDataView(); const { timelinePrivileges: { crud: canWriteTimeline }, } = useUserPrivileges(); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/unsaved_timeline.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/unsaved_timeline.cy.ts index ff4d595b503ca..0d938a00ac4f3 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/unsaved_timeline.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/unsaved_timeline.cy.ts @@ -180,9 +180,7 @@ describe('[serverless] Save Timeline Prompts', { tags: ['@serverless'] }, () => cy.get(APP_LEAVE_CONFIRM_MODAL).should('be.visible'); }); - // FIXME: data view is always present now, see the check following file for isTimlineAllowedCheck: - // x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts - it.skip('should NOT prompt when navigating with a changed & saved timeline to pages where timelines are disabled', () => { + it('should NOT prompt when navigating with a changed & saved timeline to pages where timelines are disabled', () => { populateTimeline(); addNameToTimelineAndSave('Test'); closeTimelineUsingCloseButton(); From 3e0c64721f64bf0622e69e213d9908e65e61a51b Mon Sep 17 00:00:00 2001 From: lgestc Date: Fri, 21 Feb 2025 09:45:00 +0100 Subject: [PATCH 049/115] add feature flag --- .../security_solution/common/experimental_features.ts | 3 +++ .../hooks/use_init_data_view_picker.ts | 10 +++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts index 02f3e85d143ca..a6fb10fa8b86d 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts @@ -272,6 +272,9 @@ export const allowedExperimentalValues = Object.freeze({ * Enables banner for informing users about changes in data collection. */ eventCollectionDataReductionBannerEnabled: false, + + /** Enables new Data View Picker */ + newDataViewPickerEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts index 3d89738a1ae1d..1c12513fcb0f7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts @@ -18,6 +18,7 @@ import { useKibana } from '../../common/lib/kibana'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewPickerScopeName } from '../constants'; import { createDataViewSelectedListener } from '../redux/listeners/data_view_selected'; import { createInitListener } from '../redux/listeners/init_listener'; +import { useEnableExperimental } from '../../common/hooks/use_experimental_features'; type OriginalListener = Parameters[0]; @@ -38,8 +39,15 @@ const removeListener = (listener: Listener) => export const useInitDataViewPicker = () => { const dispatch = useDispatch(); const services = useKibana().services; + const { newDataViewPickerEnabled } = useEnableExperimental(); + + console.log(' newDataViewPickerEnabled', newDataViewPickerEnabled); useEffect(() => { + if (!newDataViewPickerEnabled) { + return; + } + const dataViewsLoadingListener = createInitListener({ dataViews: services.dataViews, }); @@ -69,5 +77,5 @@ export const useInitDataViewPicker = () => { dispatch(removeListener(dataViewsLoadingListener)); dispatch(removeListener(dataViewSelectedListener)); }; - }, [dispatch, services.dataViews]); + }, [dispatch, newDataViewPickerEnabled, services.dataViews]); }; From 90c5561db03d85ddb82d1b7e8e8039be9db06d81 Mon Sep 17 00:00:00 2001 From: lgestc Date: Fri, 21 Feb 2025 11:26:20 +0100 Subject: [PATCH 050/115] put the new dataview picker behind a flag --- .../components/with_data_view/index.tsx | 15 ++++++- .../timeline/use_investigate_in_timeline.ts | 39 ++++++++++++++----- .../timeline/use_show_timeline_for_path.ts | 23 +++++++---- .../hooks/use_init_data_view_picker.ts | 2 - .../use_add_bulk_to_timeline.tsx | 21 +++++++++- .../components/modal/header/index.tsx | 22 +++++++++-- .../components/open_timeline/index.tsx | 18 +++++++-- .../open_timeline/note_previews/index.tsx | 13 ++++++- .../open_timeline/use_update_timeline.tsx | 35 ++++++++++++----- .../timeline/data_providers/index.tsx | 17 ++++++-- .../components/timeline/kpi/kpi_container.tsx | 19 ++++++++- .../timeline/query_bar/eql/index.tsx | 20 ++++++++-- .../components/timeline/query_bar/index.tsx | 17 +++++++- .../timeline/search_or_filter/index.tsx | 13 ++++++- .../search_or_filter/search_or_filter.tsx | 11 +++++- .../components/timeline/tabs/esql/index.tsx | 33 +++++++++++----- .../components/timeline/tabs/pinned/index.tsx | 25 ++++++++++-- .../components/timeline/tabs/query/index.tsx | 29 ++++++++++++-- .../tabs/session/use_session_view.tsx | 12 +++++- .../tabs/shared/use_timeline_columns.tsx | 10 ++++- .../containers/use_timeline_data_filters.ts | 10 ++++- .../timelines/hooks/use_create_timeline.tsx | 38 +++++++++++++++--- .../public/timelines/pages/timelines_page.tsx | 15 ++++++- 23 files changed, 375 insertions(+), 82 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.tsx index 41d3b451a61e6..9219d90540025 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.tsx @@ -9,7 +9,11 @@ import React from 'react'; import type { ComponentType } from 'react'; import type { ReactElement } from 'react-markdown'; import type { DataView } from '@kbn/data-views-plugin/common'; +import { DataViewPickerScopeName } from '../../../data_view_picker/constants'; +import { useFullDataView } from '../../../data_view_picker/hooks/use_full_data_view'; import { DataViewErrorComponent } from './data_view_error'; +import { useEnableExperimental } from '../../hooks/use_experimental_features'; + import { useGetScopedSourcererDataView } from '../../../sourcerer/components/use_get_sourcerer_data_view'; import { SourcererScopeName } from '../../../sourcerer/store/model'; @@ -30,10 +34,19 @@ export const withDataView =

( fallback?: ReactElement ) => { const ComponentWithDataView = (props: OmitDataView

) => { - const dataView = useGetScopedSourcererDataView({ + const experimentalDataView = useFullDataView({ + dataViewPickerScope: DataViewPickerScopeName.timeline, + }); + + let dataView = useGetScopedSourcererDataView({ sourcererScope: SourcererScopeName.timeline, }); + const { newDataViewPickerEnabled } = useEnableExperimental(); + if (newDataViewPickerEnabled) { + dataView = experimentalDataView; + } + if (!dataView) { return fallback ?? ; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts index c1c195b0c5965..3b7ef769718ba 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts @@ -8,17 +8,19 @@ import { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import type { Filter, Query } from '@kbn/es-query'; +import { useSelectDataView } from '../../../data_view_picker/hooks/use_select_data_view'; import { useCreateTimeline } from '../../../timelines/hooks/use_create_timeline'; import { updateProviders, setFilters, applyKqlFilterQuery } from '../../../timelines/store/actions'; import { SourcererScopeName } from '../../../sourcerer/store/model'; import type { DataProvider } from '../../../../common/types'; import { sourcererSelectors } from '../../store'; -import { sourcererActions } from '../../store/actions'; import { inputsActions } from '../../store/inputs'; import { InputsModelId } from '../../store/inputs/constants'; import type { TimeRange } from '../../store/inputs/model'; import { TimelineId } from '../../../../common/types/timeline'; import { TimelineTypeEnum } from '../../../../common/api/timeline'; +import { sourcererActions } from '../../store/actions'; +import { useEnableExperimental } from '../use_experimental_features'; interface InvestigateInTimelineArgs { /** @@ -57,6 +59,7 @@ export const useInvestigateInTimeline = () => { const signalIndexName = useSelector(sourcererSelectors.signalIndexName); const defaultDataView = useSelector(sourcererSelectors.defaultDataView); + const { newDataViewPickerEnabled } = useEnableExperimental(); const clearTimelineTemplate = useCreateTimeline({ timelineId: TimelineId.active, @@ -68,6 +71,8 @@ export const useInvestigateInTimeline = () => { timelineType: TimelineTypeEnum.default, }); + const setSelectedDataView = useSelectDataView(); + const investigateInTimeline = useCallback( async ({ query, @@ -124,19 +129,35 @@ export const useInvestigateInTimeline = () => { // Only show detection alerts // (This is required so the timeline event count matches the prevalence count) if (!keepDataView) { - dispatch( - sourcererActions.setSelectedDataView({ - id: SourcererScopeName.timeline, - selectedDataViewId: defaultDataView.id, - selectedPatterns: [signalIndexName || ''], - }) - ); + if (newDataViewPickerEnabled) { + setSelectedDataView({ + scope: [SourcererScopeName.timeline], + id: defaultDataView.id, + patterns: [signalIndexName || ''], + }); + } else { + dispatch( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.timeline, + selectedDataViewId: defaultDataView.id, + selectedPatterns: [signalIndexName || ''], + }) + ); + } } // Unlock the time range from the global time range dispatch(inputsActions.removeLinkTo([InputsModelId.timeline, InputsModelId.global])); } }, - [clearTimelineTemplate, clearTimelineDefault, dispatch, defaultDataView.id, signalIndexName] + [ + clearTimelineTemplate, + clearTimelineDefault, + dispatch, + newDataViewPickerEnabled, + setSelectedDataView, + defaultDataView.id, + signalIndexName, + ] ); return { investigateInTimeline }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts index 30f5b078770b5..a1867793067f2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts @@ -9,11 +9,13 @@ import { useCallback, useMemo } from 'react'; import { matchPath } from 'react-router-dom'; import { getLinksWithHiddenTimeline } from '../../links'; -import { SourcererScopeName } from '../../../sourcerer/store/model'; -import { useSourcererDataView } from '../../../sourcerer/containers'; import { useKibana } from '../../lib/kibana'; import { hasAccessToSecuritySolution } from '../../../helpers_access'; +import { SourcererScopeName } from '../../../sourcerer/store/model'; +import { useSourcererDataView } from '../../../sourcerer/containers'; +import { useEnableExperimental } from '../../hooks/use_experimental_features'; + const isTimelinePathVisible = (currentPath: string): boolean => { const groupLinksWithHiddenTimelinePaths = getLinksWithHiddenTimeline().map((l) => l.path); const hiddenTimelineRoutes = groupLinksWithHiddenTimelinePaths; @@ -21,7 +23,6 @@ const isTimelinePathVisible = (currentPath: string): boolean => { }; export const useShowTimelineForGivenPath = () => { - const { indicesExist, dataViewId } = useSourcererDataView(SourcererScopeName.timeline); const { services: { application: { capabilities }, @@ -29,10 +30,18 @@ export const useShowTimelineForGivenPath = () => { } = useKibana(); const userHasSecuritySolutionVisible = hasAccessToSecuritySolution(capabilities); - const isTimelineAllowed = useMemo( - () => userHasSecuritySolutionVisible && (indicesExist || dataViewId === null), - [indicesExist, dataViewId, userHasSecuritySolutionVisible] - ); + const { indicesExist, dataViewId } = useSourcererDataView(SourcererScopeName.timeline); + + const { newDataViewPickerEnabled } = useEnableExperimental(); + + const isTimelineAllowed = useMemo(() => { + // NOTE: with new Data View Picker, data view is always defined + if (newDataViewPickerEnabled && userHasSecuritySolutionVisible) { + return true; + } + + return userHasSecuritySolutionVisible && (indicesExist || dataViewId === null); + }, [newDataViewPickerEnabled, userHasSecuritySolutionVisible, indicesExist, dataViewId]); const getIsTimelineVisible = useCallback( (pathname: string) => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts index 1c12513fcb0f7..ca1312329a0d2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts @@ -41,8 +41,6 @@ export const useInitDataViewPicker = () => { const services = useKibana().services; const { newDataViewPickerEnabled } = useEnableExperimental(); - console.log(' newDataViewPickerEnabled', newDataViewPickerEnabled); - useEffect(() => { if (!newDataViewPickerEnabled) { return; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx index 2900f1196eff6..fbaa14a639d0f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx @@ -13,11 +13,14 @@ import { getEsQueryConfig } from '@kbn/data-plugin/public'; import type { BulkActionsConfig } from '@kbn/response-ops-alerts-table/types'; import { dataTableActions, TableId, tableDefaults } from '@kbn/securitysolution-data-table'; import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; +import { useSelectedPatterns } from '../../../../data_view_picker/hooks/use_selected_patterns'; +import { useBrowserFields } from '../../../../data_view_picker/hooks/use_browser_fields'; +import { useDataView } from '../../../../data_view_picker/hooks/use_data_view'; import type { CustomBulkAction } from '../../../../../common/types'; import { combineQueries } from '../../../../common/lib/kuery'; import { useKibana } from '../../../../common/lib/kibana'; import { BULK_ADD_TO_TIMELINE_LIMIT } from '../../../../../common/constants'; -import { useSourcererDataView } from '../../../../sourcerer/containers'; import type { TimelineArgs } from '../../../../timelines/containers'; import { useTimelineEventsHandler } from '../../../../timelines/containers'; import { eventsViewerSelector } from '../../../../common/components/events_viewer/selectors'; @@ -31,6 +34,7 @@ import { sendBulkEventsToTimelineAction } from '../actions'; import type { CreateTimelineProps } from '../types'; import type { SourcererScopeName } from '../../../../sourcerer/store/model'; import type { Direction } from '../../../../../common/search_strategy'; +import { useSourcererDataView } from '../../../../sourcerer/containers'; const { setEventsLoading, setSelected } = dataTableActions; @@ -62,8 +66,13 @@ export const useAddBulkToTimelineAction = ({ scopeId, }: UseAddBulkToTimelineActionProps) => { const [disableActionOnSelectAll, setDisabledActionOnSelectAll] = useState(false); + const { newDataViewPickerEnabled } = useEnableExperimental(); + + const { dataView: experimentalDataView } = useDataView(scopeId); + const experimentalBrowserFields = useBrowserFields(scopeId); + const experimentalSelectedPatterns = useSelectedPatterns(scopeId); - const { + let { browserFields, dataViewId, sourcererDataView, @@ -71,6 +80,14 @@ export const useAddBulkToTimelineAction = ({ // in order to include the exclude filters in the search that are not stored in the timeline selectedPatterns, } = useSourcererDataView(scopeId); + + if (newDataViewPickerEnabled) { + dataViewId = experimentalDataView.id ?? ''; + browserFields = experimentalBrowserFields; + sourcererDataView = experimentalDataView; + selectedPatterns = experimentalSelectedPatterns; + } + const dispatch = useDispatch(); const { uiSettings } = useKibana().services; diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.tsx index e42e856b9ca74..d1148df9e5393 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.tsx @@ -18,6 +18,11 @@ import { useDispatch, useSelector } from 'react-redux'; import { getEsQueryConfig } from '@kbn/data-plugin/common'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; import styled from 'styled-components'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; +import { useSourcererDataView } from '../../../../sourcerer/containers'; +import { SourcererScopeName } from '../../../../sourcerer/store/model'; + +import { useDataView } from '../../../../data_view_picker/hooks/use_data_view'; import { NewTimelineButton } from '../actions/new_timeline_button'; import { OpenTimelineButton } from '../actions/open_timeline_button'; import { APP_ID } from '../../../../../common'; @@ -31,9 +36,7 @@ import { createHistoryEntry } from '../../../../common/utils/global_query_string import { timelineActions } from '../../../store'; import type { State } from '../../../../common/store'; import { useKibana } from '../../../../common/lib/kibana'; -import { useSourcererDataView } from '../../../../sourcerer/containers'; import { combineQueries } from '../../../../common/lib/kuery'; -import { SourcererScopeName } from '../../../../sourcerer/store/model'; import * as i18n from '../translations'; import { AddToFavoritesButton } from '../../add_to_favorites'; import { TimelineSaveStatus } from '../../save_status'; @@ -41,6 +44,8 @@ import { InspectButton } from '../../../../common/components/inspect'; import { InputsModelId } from '../../../../common/store/inputs/constants'; import { AttachToCaseButton } from '../actions/attach_to_case_button'; import { SaveTimelineButton } from '../actions/save_timeline_button'; +import { useBrowserFields } from '../../../../data_view_picker/hooks/use_browser_fields'; +import { DataViewPickerScopeName } from '../../../../data_view_picker/constants'; const whiteSpaceNoWrapCSS = { 'white-space': 'nowrap' }; const autoOverflowXCSS = { 'overflow-x': 'auto' }; @@ -70,7 +75,18 @@ interface FlyoutHeaderPanelProps { export const TimelineModalHeader = React.memo( ({ timelineId, openToggleRef }) => { const dispatch = useDispatch(); - const { browserFields, sourcererDataView } = useSourcererDataView(SourcererScopeName.timeline); + const { newDataViewPickerEnabled } = useEnableExperimental(); + + let { browserFields, sourcererDataView } = useSourcererDataView(SourcererScopeName.timeline); + + const { dataView: experimentalDataView } = useDataView(DataViewPickerScopeName.timeline); + const experimentalBrowserFields = useBrowserFields(DataViewPickerScopeName.timeline); + + if (newDataViewPickerEnabled) { + browserFields = experimentalBrowserFields; + sourcererDataView = experimentalDataView; + } + const { cases, uiSettings } = useKibana().services; const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index 7cd7cbe18927f..62686c9f36d38 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -9,6 +9,9 @@ import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { encode } from '@kbn/rison'; +import { useEnableExperimental } from '../../../common/hooks/use_experimental_features'; +import { useSourcererDataView } from '../../../sourcerer/containers'; +import { useSelectedPatterns } from '../../../data_view_picker/hooks/use_selected_patterns'; import { RULE_FROM_EQL_URL_PARAM, RULE_FROM_TIMELINE_URL_PARAM, @@ -51,11 +54,11 @@ import { useTimelineStatus } from './use_timeline_status'; import { deleteTimelinesByIds } from '../../containers/api'; import type { Direction } from '../../../../common/search_strategy'; import { SourcererScopeName } from '../../../sourcerer/store/model'; -import { useSourcererDataView } from '../../../sourcerer/containers'; import { useStartTransaction } from '../../../common/lib/apm/use_start_transaction'; import { TIMELINE_ACTIONS } from '../../../common/lib/apm/user_actions'; import { defaultUdtHeaders } from '../timeline/body/column_headers/default_headers'; import { timelineDefaults } from '../../store/defaults'; +import { useDataView } from '../../../data_view_picker/hooks/use_data_view'; interface OwnProps { /** Displays open timeline in modal */ @@ -157,7 +160,16 @@ export const StatefulOpenTimelineComponent = React.memo( (state) => getTimeline(state, TimelineId.active)?.savedObjectId ?? '' ); - const { dataViewId, selectedPatterns } = useSourcererDataView(SourcererScopeName.timeline); + let { dataViewId, selectedPatterns } = useSourcererDataView(SourcererScopeName.timeline); + const { newDataViewPickerEnabled } = useEnableExperimental(); + + const { dataView: experimentalDataView } = useDataView(SourcererScopeName.timeline); + const experimentalSelectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); + + if (newDataViewPickerEnabled) { + dataViewId = experimentalDataView?.id || ''; + selectedPatterns = experimentalSelectedPatterns; + } const { customTemplateTimelineCount, @@ -247,7 +259,7 @@ export const StatefulOpenTimelineComponent = React.memo( dispatchCreateNewTimeline({ id: TimelineId.active, columns: defaultUdtHeaders, - dataViewId, + dataViewId: dataViewId ?? '', indexNames: selectedPatterns, show: false, excludedRowRendererIds: timelineDefaults.excludedRowRendererIds, diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx index 4080e254303a2..606aac6598bc4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx @@ -20,6 +20,8 @@ import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; +import { useSelectedPatterns } from '../../../../data_view_picker/hooks/use_selected_patterns'; import { useKibana } from '../../../../common/lib/kibana'; import { DocumentDetailsRightPanelKey } from '../../../../flyout/document_details/shared/constants/panel_keys'; import type { TimelineResultNote } from '../types'; @@ -31,11 +33,11 @@ import * as i18n from './translations'; import { TimelineId } from '../../../../../common/types/timeline'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; -import { useSourcererDataView } from '../../../../sourcerer/containers'; import { useDeleteNote } from './hooks/use_delete_note'; import { getTimelineNoteSelector } from '../../timeline/tabs/notes/selectors'; import { DocumentEventTypes } from '../../../../common/lib/telemetry'; import { useUserPrivileges } from '../../../../common/components/user_privileges'; +import { useSourcererDataView } from '../../../../sourcerer/containers'; export const NotePreviewsContainer = styled.section` padding-top: ${({ theme }) => `${theme.eui.euiSizeS}`}; @@ -52,7 +54,14 @@ const ToggleEventDetailsButtonComponent: React.FC eventId, timelineId, }) => { - const { selectedPatterns } = useSourcererDataView(SourcererScopeName.timeline); + const experimentalSelectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); + let { selectedPatterns } = useSourcererDataView(SourcererScopeName.timeline); + + const { newDataViewPickerEnabled } = useEnableExperimental(); + + if (newDataViewPickerEnabled) { + selectedPatterns = experimentalSelectedPatterns; + } const { telemetry } = useKibana().services; const { openFlyout } = useExpandableFlyoutApi(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx index 2927b2365221d..21c3b5f92653b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx @@ -8,13 +8,16 @@ import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { isEmpty } from 'lodash/fp'; +import { useEnableExperimental } from '../../../common/hooks/use_experimental_features'; +import { DataViewPickerScopeName } from '../../../data_view_picker/constants'; +import { useSelectDataView } from '../../../data_view_picker/hooks/use_select_data_view'; import type { Note } from '../../../../common/api/timeline'; import { TimelineStatusEnum, TimelineTypeEnum } from '../../../../common/api/timeline'; import { createNote } from '../notes/helpers'; - -import { InputsModelId } from '../../../common/store/inputs/constants'; import { sourcererActions } from '../../../sourcerer/store'; import { SourcererScopeName } from '../../../sourcerer/store/model'; + +import { InputsModelId } from '../../../common/store/inputs/constants'; import { addNotes as dispatchAddNotes, updateNote as dispatchUpdateNote, @@ -36,8 +39,12 @@ import type { UpdateTimeline } from './types'; export const useUpdateTimeline = () => { const dispatch = useDispatch(); + const selectDataView = useSelectDataView(); + const { newDataViewPickerEnabled } = useEnableExperimental(); return useCallback( + // NOTE: this is only enabled for the data view picker test + // eslint-disable-next-line complexity ({ duplicate, id, @@ -58,13 +65,21 @@ export const useUpdateTimeline = () => { _timeline = { ...timeline, updated: undefined, changed: true, version: null }; } if (!isEmpty(_timeline.indexNames)) { - dispatch( - sourcererActions.setSelectedDataView({ - id: SourcererScopeName.timeline, - selectedDataViewId: _timeline.dataViewId, - selectedPatterns: _timeline.indexNames, - }) - ); + if (newDataViewPickerEnabled) { + selectDataView({ + id: _timeline.dataViewId, + patterns: _timeline.indexNames, + scope: [DataViewPickerScopeName.timeline], + }); + } else { + dispatch( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.timeline, + selectedDataViewId: _timeline.dataViewId, + selectedPatterns: _timeline.indexNames, + }) + ); + } } if ( _timeline.status === TimelineStatusEnum.immutable && @@ -138,6 +153,6 @@ export const useUpdateTimeline = () => { ); } }, - [dispatch] + [dispatch, newDataViewPickerEnabled, selectDataView] ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx index 7a90b5254e445..27d484d0e0707 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx @@ -13,11 +13,13 @@ import { v4 as uuidv4 } from 'uuid'; import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid'; import { EuiToolTip, EuiSuperSelect, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; +import { useBrowserFields } from '../../../../data_view_picker/hooks/use_browser_fields'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; -import { useSourcererDataView } from '../../../../sourcerer/containers'; -import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper'; import { droppableTimelineProvidersPrefix } from '../../../../common/components/drag_and_drop/helpers'; +import { useSourcererDataView } from '../../../../sourcerer/containers'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { Empty } from './empty'; import { Providers } from './providers'; @@ -106,7 +108,16 @@ const CustomTooltipDiv = styled.div` export const DataProviders = React.memo(({ timelineId }) => { const dispatch = useDispatch(); - const { browserFields } = useSourcererDataView(SourcererScopeName.timeline); + const { newDataViewPickerEnabled } = useEnableExperimental(); + + let { browserFields } = useSourcererDataView(SourcererScopeName.timeline); + + const experimentalBrowserFields = useBrowserFields(SourcererScopeName.timeline); + + if (newDataViewPickerEnabled) { + browserFields = experimentalBrowserFields; + } + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const dataProviders = useDeepEqualSelector( diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx index dce963737fb5a..19e8a076d0a32 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx @@ -11,10 +11,12 @@ import { useSelector } from 'react-redux'; import { getEsQueryConfig } from '@kbn/data-plugin/common'; import type { TimerangeInput } from '@kbn/timelines-plugin/common'; import { EuiPanel } from '@elastic/eui'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; +import { useBrowserFields } from '../../../../data_view_picker/hooks/use_browser_fields'; import { TimelineId } from '../../../../../common/types'; -import { useSourcererDataView } from '../../../../sourcerer/containers'; import type { State } from '../../../../common/store'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { useSourcererDataView } from '../../../../sourcerer/containers'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { TimelineKPIs } from './kpis'; import { useTimelineKpis } from '../../../containers/kpis'; @@ -26,16 +28,29 @@ import { endSelector, startSelector, } from '../../../../common/components/super_date_picker/selectors'; +import { useSelectedPatterns } from '../../../../data_view_picker/hooks/use_selected_patterns'; +import { useDataView } from '../../../../data_view_picker/hooks/use_data_view'; interface KpiExpandedProps { timelineId: string; } export const TimelineKpisContainer = ({ timelineId }: KpiExpandedProps) => { - const { browserFields, sourcererDataView, selectedPatterns } = useSourcererDataView( + const { newDataViewPickerEnabled } = useEnableExperimental(); + const experimentalBrowserFields = useBrowserFields(SourcererScopeName.timeline); + const { dataView: experimentalDataView } = useDataView(SourcererScopeName.timeline); + const experimentalSelectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); + + let { browserFields, sourcererDataView, selectedPatterns } = useSourcererDataView( SourcererScopeName.timeline ); + if (newDataViewPickerEnabled) { + browserFields = experimentalBrowserFields; + sourcererDataView = experimentalDataView; + selectedPatterns = experimentalSelectedPatterns; + } + const { uiSettings } = useKibana().services; const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/eql/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/eql/index.tsx index c378f5290cde2..60d7cbf2e5878 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/eql/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/eql/index.tsx @@ -11,8 +11,9 @@ import { EuiOutsideClickDetector } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import { css } from '@emotion/css'; +import { useEnableExperimental } from '../../../../../common/hooks/use_experimental_features'; +import { useDataView } from '../../../../../data_view_picker/hooks/use_data_view'; import type { EqlOptions } from '../../../../../../common/search_strategy'; -import { useSourcererDataView } from '../../../../../sourcerer/containers'; import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; import { SourcererScopeName } from '../../../../../sourcerer/store/model'; import { EqlQueryEdit } from '../../../../../detection_engine/rule_creation/components/eql_query_edit'; @@ -22,6 +23,8 @@ import type { FormSchema, FormSubmitHandler } from '../../../../../shared_import import { Form, UseField, useForm } from '../../../../../shared_imports'; import { timelineActions } from '../../../../store'; import { getEqlOptions } from './selectors'; +import { useSelectedPatterns } from '../../../../../data_view_picker/hooks/use_selected_patterns'; +import { useSourcererDataView } from '../../../../../sourcerer/containers'; interface TimelineEqlQueryBar { index: string[]; @@ -59,12 +62,23 @@ export const EqlQueryBarTimeline = memo(({ timelineId }: { timelineId: string }) const getOptionsSelected = useMemo(() => getEqlOptions(), []); const eqlOptions = useDeepEqualSelector((state) => getOptionsSelected(state, timelineId)); - const { + let { loading: indexPatternsLoading, sourcererDataView, selectedPatterns, } = useSourcererDataView(SourcererScopeName.timeline); + const { newDataViewPickerEnabled } = useEnableExperimental(); + + const { dataView: experimentalDataView, status } = useDataView(SourcererScopeName.timeline); + const experimentalSelectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); + + if (newDataViewPickerEnabled) { + indexPatternsLoading = status !== 'ready'; + sourcererDataView = experimentalDataView; + selectedPatterns = experimentalSelectedPatterns; + } + const initialState = useMemo( () => ({ ...defaultValues, @@ -165,7 +179,7 @@ export const EqlQueryBarTimeline = memo(({ timelineId }: { timelineId: string }) /* Force casting `sourcererDataView` to `DataViewBase` is required since EqlQueryEdit accepts DataViewBase but `useSourcererDataView()` returns `DataViewSpec`. - + When using `UseField` with `EqlQueryBar` such casting isn't required by TS since `UseField` component props are types as `Record`. */ return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx index 1bb39aa4796d2..22b32037692ee 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx @@ -13,8 +13,9 @@ import type { Filter, Query } from '@kbn/es-query'; import { FilterStateStore } from '@kbn/es-query'; import type { FilterManager, SavedQuery, SavedQueryTimeFilter } from '@kbn/data-plugin/public'; import styled from '@emotion/styled'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; +import { useDataView } from '../../../../data_view_picker/hooks/use_data_view'; import { InputsModelId } from '../../../../common/store/inputs/constants'; -import { useSourcererDataView } from '../../../../sourcerer/containers'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { @@ -29,6 +30,8 @@ import type { DataProvider } from '../data_providers/data_provider'; import { TIMELINE_FILTER_DROP_AREA, buildGlobalQuery, getNonDropAreaFilters } from '../helpers'; import { timelineActions } from '../../../store'; import type { KueryFilterQuery, KueryFilterQueryKind } from '../../../../../common/types/timeline'; +import { useBrowserFields } from '../../../../data_view_picker/hooks/use_browser_fields'; +import { useSourcererDataView } from '../../../../sourcerer/containers'; export interface QueryBarTimelineComponentProps { dataProviders: DataProvider[]; @@ -110,7 +113,17 @@ export const QueryBarTimeline = memo( const [dateRangeTo, setDateRangTo] = useState( toStr != null ? toStr : new Date(to).toISOString() ); - const { browserFields, sourcererDataView } = useSourcererDataView(SourcererScopeName.timeline); + let { browserFields, sourcererDataView } = useSourcererDataView(SourcererScopeName.timeline); + + const { newDataViewPickerEnabled } = useEnableExperimental(); + const { dataView: experimentalDataView } = useDataView(SourcererScopeName.timeline); + const experimentalBrowserFields = useBrowserFields(SourcererScopeName.timeline); + + if (newDataViewPickerEnabled) { + sourcererDataView = experimentalDataView; + browserFields = experimentalBrowserFields; + } + const [savedQuery, setSavedQuery] = useState(undefined); const [filterQueryConverted, setFilterQueryConverted] = useState({ query: filterQuery != null ? filterQuery.expression : '', diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx index d3233858813ae..82d8999c1cf25 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx @@ -17,11 +17,12 @@ import type { FilterManager } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; import { FilterItems } from '@kbn/unified-search-plugin/public'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; +import { useDataView } from '../../../../data_view_picker/hooks/use_data_view'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { useKibana } from '../../../../common/lib/kibana'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; -import { useSourcererDataView } from '../../../../sourcerer/containers'; import type { State, inputsModel } from '../../../../common/store'; import { inputsSelectors } from '../../../../common/store'; import { timelineActions, timelineSelectors } from '../../../store'; @@ -32,6 +33,7 @@ import { SearchOrFilter } from './search_or_filter'; import { setDataProviderVisibility } from '../../../store/actions'; import { getNonDropAreaFilters } from '../helpers'; import * as i18n from './translations'; +import { useSourcererDataView } from '../../../../sourcerer/containers'; interface OwnProps { filterManager: FilterManager; @@ -73,7 +75,14 @@ const StatefulSearchOrFilterComponent = React.memo( services: { data }, } = useKibana(); - const { sourcererDataView } = useSourcererDataView(SourcererScopeName.timeline); + let { sourcererDataView } = useSourcererDataView(SourcererScopeName.timeline); + + const { dataView: experimentalDataView } = useDataView(SourcererScopeName.timeline); + const { newDataViewPickerEnabled } = useEnableExperimental(); + + if (newDataViewPickerEnabled) { + sourcererDataView = experimentalDataView; + } const getIsDataProviderVisible = useMemo( () => timelineSelectors.dataProviderVisibilitySelector(), diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx index 3ec2a965d8021..7c82e79061a45 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx @@ -11,6 +11,7 @@ import styled from 'styled-components'; import type { Filter } from '@kbn/es-query'; import type { FilterManager } from '@kbn/data-plugin/public'; +import { useEnableExperimental } from '../../../../common/hooks/use_experimental_features'; import { type TimelineType, TimelineTypeEnum } from '../../../../../common/api/timeline'; import { InputsModelId } from '../../../../common/store/inputs/constants'; import type { KqlMode } from '../../../store/model'; @@ -19,10 +20,11 @@ import { SuperDatePicker } from '../../../../common/components/super_date_picker import type { KueryFilterQuery } from '../../../../../common/types/timeline'; import type { DataProvider } from '../data_providers/data_provider'; import { QueryBarTimeline } from '../query_bar'; +import { Sourcerer } from '../../../../sourcerer/components'; +import { DataViewPicker } from '../../../../data_view_picker/components/data_view_picker'; import { TimelineDatePickerLock } from '../date_picker_lock'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; -import { Sourcerer } from '../../../../sourcerer/components'; import { DATA_PROVIDER_HIDDEN_EMPTY, DATA_PROVIDER_HIDDEN_POPULATED, @@ -85,6 +87,7 @@ export const SearchOrFilter = React.memo( toggleDataProviderVisibility, timelineType, }) => { + const { newDataViewPickerEnabled } = useEnableExperimental(); const isDataProviderEmpty = useMemo(() => dataProviders?.length === 0, [dataProviders]); const dataProviderIconTooltipContent = useMemo(() => { @@ -112,7 +115,11 @@ export const SearchOrFilter = React.memo( responsive={false} > - + {newDataViewPickerEnabled ? ( + + ) : ( + + )} = ({ timelineId }) services: { customDataService: discoverDataService, discover, - dataViews: dataViewService, savedSearch: savedSearchService, + dataViews: dataViewService, }, } = useKibana(); const { @@ -59,9 +61,18 @@ export const DiscoverTabContent: FC = ({ timelineId }) const dispatch = useDispatch(); + const { newDataViewPickerEnabled } = useEnableExperimental(); + const { dataView: experimentalDataView } = useDataView(SourcererScopeName.detections); + const { dataViewId } = useSourcererDataView(SourcererScopeName.detections); - const [dataView, setDataView] = useState(); + // eslint-disable-next-line prefer-const + let [dataView, setDataView] = useState(); + + if (newDataViewPickerEnabled) { + dataView = experimentalDataView; + } + const [discoverTimerange, setDiscoverTimerange] = useState(); const discoverAppStateSubscription = useRef(); @@ -69,6 +80,12 @@ export const DiscoverTabContent: FC = ({ timelineId }) const discoverSavedSearchStateSubscription = useRef(); const discoverTimerangeSubscription = useRef(); + // TODO: should not be here, used to make discover container work I suppose + useEffect(() => { + if (!dataViewId) return; + dataViewService.get(dataViewId).then((dv) => setDataView(dv.toSpec())); + }, [dataViewId, dataViewService]); + const { discoverStateContainer, setDiscoverStateContainer, @@ -160,11 +177,6 @@ export const DiscoverTabContent: FC = ({ timelineId }) canSaveTimeline, ]); - useEffect(() => { - if (!dataViewId) return; - dataViewService.get(dataViewId).then(setDataView); - }, [dataViewId, dataViewService]); - useEffect(() => { const unSubscribeAll = () => { [ @@ -277,7 +289,8 @@ export const DiscoverTabContent: FC = ({ timelineId }) const DiscoverContainer = discover.DiscoverContainer; - const isLoading = Boolean(!dataView); + // TODO this should not work like that + const isLoading = !dataView; return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx index f70d51de3d755..2b9c2000a8b3a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx @@ -13,6 +13,9 @@ import deepEqual from 'fast-deep-equal'; import type { EuiDataGridControlColumn } from '@elastic/eui'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy'; +import { useSourcererDataView } from '../../../../../sourcerer/containers'; +import { useDataView } from '../../../../../data_view_picker/hooks/use_data_view'; +import { useSelectedPatterns } from '../../../../../data_view_picker/hooks/use_selected_patterns'; import { useFetchNotes } from '../../../../../notes/hooks/use_fetch_notes'; import { DocumentDetailsLeftPanelKey, @@ -25,8 +28,10 @@ import { useTimelineEvents } from '../../../../containers'; import { requiredFieldsForActions } from '../../../../../detections/components/alerts_table/default_config'; import { SourcererScopeName } from '../../../../../sourcerer/store/model'; import { timelineDefaults } from '../../../../store/defaults'; -import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; -import { useSourcererDataView } from '../../../../../sourcerer/containers'; +import { + useEnableExperimental, + useIsExperimentalFeatureEnabled, +} from '../../../../../common/hooks/use_experimental_features'; import type { TimelineModel } from '../../../../store/model'; import type { State } from '../../../../../common/store'; import { TimelineTabs } from '../../../../../../common/types/timeline'; @@ -78,10 +83,22 @@ export const PinnedTabContentComponent: React.FC = ({ const [pageIndex, setPageIndex] = useState(0); const { telemetry } = useKibana().services; - const { dataViewId, sourcererDataView, selectedPatterns } = useSourcererDataView( + + const { newDataViewPickerEnabled } = useEnableExperimental(); + + let { dataViewId, sourcererDataView, selectedPatterns } = useSourcererDataView( SourcererScopeName.timeline ); + const experimentalSelectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); + const { dataView: experimentalDataView } = useDataView(SourcererScopeName.timeline); + + if (newDataViewPickerEnabled) { + selectedPatterns = experimentalSelectedPatterns; + sourcererDataView = experimentalDataView; + dataViewId = experimentalDataView.id ?? ''; + } + const filterQuery = useMemo(() => { if (isEmpty(pinnedEventIds)) { return ''; @@ -144,7 +161,7 @@ export const PinnedTabContentComponent: React.FC = ({ endDate: '', id: `pinned-${timelineId}`, indexNames: selectedPatterns, - dataViewId, + dataViewId: dataViewId ?? '', fields: timelineQueryFields, limit: itemsPerPage, filterQuery, diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx index 7a08a22ef5a81..b241bbcc83cd9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx @@ -15,6 +15,9 @@ import { getEsQueryConfig } from '@kbn/data-plugin/common'; import { DataLoadingState } from '@kbn/unified-data-table'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy'; +import { useSelectedPatterns } from '../../../../../data_view_picker/hooks/use_selected_patterns'; +import { useBrowserFields } from '../../../../../data_view_picker/hooks/use_browser_fields'; +import { useDataView } from '../../../../../data_view_picker/hooks/use_data_view'; import { useFetchNotes } from '../../../../../notes/hooks/use_fetch_notes'; import { DocumentDetailsLeftPanelKey, @@ -22,7 +25,10 @@ import { } from '../../../../../flyout/document_details/shared/constants/panel_keys'; import { LeftPanelNotesTab } from '../../../../../flyout/document_details/left'; import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; -import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +import { + useEnableExperimental, + useIsExperimentalFeatureEnabled, +} from '../../../../../common/hooks/use_experimental_features'; import { useTimelineDataFilters } from '../../../../containers/use_timeline_data_filters'; import { InputsModelId } from '../../../../../common/store/inputs/constants'; import { useInvalidFilterQuery } from '../../../../../common/hooks/use_invalid_filter_query'; @@ -83,7 +89,15 @@ export const QueryTabContentComponent: React.FC = ({ eventIdToNoteIds, }) => { const dispatch = useDispatch(); - const { + const { newDataViewPickerEnabled } = useEnableExperimental(); + + const { dataView: experimentalDataView, status: sourcererStatus } = useDataView( + SourcererScopeName.timeline + ); + const experimentalBrowserFields = useBrowserFields(SourcererScopeName.timeline); + const experimentalSelectedPatterns = useSelectedPatterns(SourcererScopeName.timeline); + + let { browserFields, dataViewId, loading: loadingSourcerer, @@ -92,6 +106,15 @@ export const QueryTabContentComponent: React.FC = ({ selectedPatterns, sourcererDataView, } = useSourcererDataView(SourcererScopeName.timeline); + + if (newDataViewPickerEnabled) { + loadingSourcerer = sourcererStatus !== 'ready'; + sourcererDataView = experimentalDataView; + browserFields = experimentalBrowserFields; + selectedPatterns = experimentalSelectedPatterns; + dataViewId = experimentalDataView.id ?? ''; + } + /* * `pageIndex` needs to be maintained for each table in each tab independently * and consequently it cannot be the part of common redux state @@ -174,7 +197,7 @@ export const QueryTabContentComponent: React.FC = ({ const [dataLoadingState, { events, inspect, totalCount, loadNextBatch, refreshedAt, refetch }] = useTimelineEvents({ - dataViewId, + dataViewId: dataViewId ?? '', endDate: end, fields: timelineQueryFieldsFromColumns, filterQuery: combinedQueries?.filterQuery, diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx index 6460dc220a934..23c20301cd0e7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx @@ -18,8 +18,9 @@ import { import { useDispatch } from 'react-redux'; import { dataTableSelectors, tableDefaults } from '@kbn/securitysolution-data-table'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useEnableExperimental } from '../../../../../common/hooks/use_experimental_features'; +import { useSelectedPatterns } from '../../../../../data_view_picker/hooks/use_selected_patterns'; import { DocumentDetailsRightPanelKey } from '../../../../../flyout/document_details/shared/constants/panel_keys'; -import { useSourcererDataView } from '../../../../../sourcerer/containers'; import { getScopedActions, isActiveTimeline, @@ -42,6 +43,7 @@ import { timelineDefaults } from '../../../../store/defaults'; import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; import { DocumentEventTypes } from '../../../../../common/lib/telemetry'; import { isFullScreen } from '../../helpers'; +import { useSourcererDataView } from '../../../../../sourcerer/containers'; interface NavigationProps { fullScreen: boolean; @@ -279,7 +281,13 @@ export const useSessionView = ({ scopeId, height }: { scopeId: string; height?: [globalFullScreen, scopeId, timelineFullScreen] ); - const { selectedPatterns } = useSourcererDataView(SourcererScopeName.detections); + const { newDataViewPickerEnabled } = useEnableExperimental(); + let { selectedPatterns } = useSourcererDataView(SourcererScopeName.detections); + + const experimentalSelectedPatterns = useSelectedPatterns(SourcererScopeName.detections); + if (newDataViewPickerEnabled) { + selectedPatterns = experimentalSelectedPatterns; + } const alertsIndex = useMemo(() => selectedPatterns.join(','), [selectedPatterns]); const { openFlyout } = useExpandableFlyoutApi(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx index 006c6ba1eb679..36a2fcb5c6c56 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx @@ -6,6 +6,8 @@ */ import { useMemo } from 'react'; +import { useEnableExperimental } from '../../../../../common/hooks/use_experimental_features'; +import { useBrowserFields } from '../../../../../data_view_picker/hooks/use_browser_fields'; import { SourcererScopeName } from '../../../../../sourcerer/store/model'; import { useSourcererDataView } from '../../../../../sourcerer/containers'; import { requiredFieldsForActions } from '../../../../../detections/components/alerts_table/default_config'; @@ -14,7 +16,13 @@ import type { ColumnHeaderOptions } from '../../../../../../common/types'; import { memoizedGetTimelineColumnHeaders } from './utils'; export const useTimelineColumns = (columns: ColumnHeaderOptions[]) => { - const { browserFields } = useSourcererDataView(SourcererScopeName.timeline); + const { newDataViewPickerEnabled } = useEnableExperimental(); + let { browserFields } = useSourcererDataView(SourcererScopeName.timeline); + const experimentalBrowserFields = useBrowserFields(SourcererScopeName.timeline); + + if (newDataViewPickerEnabled) { + browserFields = experimentalBrowserFields; + } const localColumns = useMemo(() => columns ?? defaultUdtHeaders, [columns]); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts index cd27ff06020ba..6bdf9a4bffe12 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts @@ -13,7 +13,9 @@ import { endSelector, } from '../../common/components/super_date_picker/selectors'; import { SourcererScopeName } from '../../sourcerer/store/model'; +import { useSelectedPatterns } from '../../data_view_picker/hooks/use_selected_patterns'; import { useSourcererDataView } from '../../sourcerer/containers'; +import { useEnableExperimental } from '../../common/hooks/use_experimental_features'; export function useTimelineDataFilters(isActiveTimelines: boolean) { const getStartSelector = useMemo(() => startSelector(), []); @@ -42,7 +44,13 @@ export function useTimelineDataFilters(isActiveTimelines: boolean) { } }); - const { selectedPatterns: analyzerPatterns } = useSourcererDataView(SourcererScopeName.analyzer); + const { newDataViewPickerEnabled } = useEnableExperimental(); + let { selectedPatterns: analyzerPatterns } = useSourcererDataView(SourcererScopeName.analyzer); + const experimentalAnalyzerPatterns = useSelectedPatterns(SourcererScopeName.analyzer); + + if (newDataViewPickerEnabled) { + analyzerPatterns = experimentalAnalyzerPatterns; + } return useMemo(() => { return { diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx index 2f80e969cab5e..2b2d5a3f80cd0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx @@ -14,13 +14,18 @@ import { TimelineId } from '../../../common/types/timeline'; import { type TimelineType, TimelineTypeEnum } from '../../../common/api/timeline'; import { useDeepEqualSelector } from '../../common/hooks/use_selector'; import { inputsActions, inputsSelectors } from '../../common/store/inputs'; -import { sourcererActions, sourcererSelectors } from '../../sourcerer/store'; -import { SourcererScopeName } from '../../sourcerer/store/model'; import { appActions } from '../../common/store/app'; import type { TimeRange } from '../../common/store/inputs/model'; import { useDiscoverInTimelineContext } from '../../common/components/discover_in_timeline/use_discover_in_timeline_context'; import { defaultUdtHeaders } from '../components/timeline/body/column_headers/default_headers'; import { timelineDefaults } from '../store/defaults'; +import { useSelectDataView } from '../../data_view_picker/hooks/use_select_data_view'; +import { DataViewPickerScopeName } from '../../data_view_picker/constants'; +import { useDataView } from '../../data_view_picker/hooks/use_data_view'; +import { useSelectedPatterns } from '../../data_view_picker/hooks/use_selected_patterns'; +import { sourcererActions, sourcererSelectors } from '../../sourcerer/store'; +import { SourcererScopeName } from '../../sourcerer/store/model'; +import { useEnableExperimental } from '../../common/hooks/use_experimental_features'; export interface UseCreateTimelineParams { /** @@ -48,15 +53,28 @@ export const useCreateTimeline = ({ onClick, }: UseCreateTimelineParams): ((options?: { timeRange?: TimeRange }) => Promise) => { const dispatch = useDispatch(); - const { id: dataViewId, patternList: selectedPatterns } = useSelector( + + let { id: dataViewId, patternList: selectedPatterns } = useSelector( sourcererSelectors.defaultDataView ) ?? { id: '', patternList: [] }; + const { newDataViewPickerEnabled } = useEnableExperimental(); + const { dataView: experimentalDataView } = useDataView(DataViewPickerScopeName.default); + + const experimentalSelectedPatterns = useSelectedPatterns(DataViewPickerScopeName.default); + + if (newDataViewPickerEnabled) { + dataViewId = experimentalDataView.id ?? ''; + selectedPatterns = experimentalSelectedPatterns; + } + const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); const globalTimeRange = useDeepEqualSelector(inputsSelectors.globalTimeRangeSelector); const { resetDiscoverAppState } = useDiscoverInTimelineContext(); + const setSelectedDataView = useSelectDataView(); + const createTimeline = useCallback( ({ id, @@ -72,6 +90,13 @@ export const useCreateTimeline = ({ if (id === TimelineId.active && timelineFullScreen) { setTimelineFullScreen(false); } + + setSelectedDataView({ + id: dataViewId, + patterns: selectedPatterns, + scope: [DataViewPickerScopeName.timeline], + }); + dispatch( sourcererActions.setSelectedDataView({ id: SourcererScopeName.timeline, @@ -120,13 +145,14 @@ export const useCreateTimeline = ({ } }, [ - dispatch, globalTimeRange, + timelineFullScreen, + dispatch, dataViewId, selectedPatterns, - setTimelineFullScreen, - timelineFullScreen, + setSelectedDataView, timelineType, + setTimelineFullScreen, ] ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.tsx index d213eeaf8f5f0..ce69f339bf83c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.tsx @@ -16,15 +16,26 @@ import { useUserPrivileges } from '../../common/components/user_privileges'; import { StatefulOpenTimeline } from '../components/open_timeline'; import * as i18n from './translations'; import { SecurityPageName } from '../../app/types'; -import { useSourcererDataView } from '../../sourcerer/containers'; import { EmptyPrompt } from '../../common/components/empty_prompt'; import { SecurityRoutePageWrapper } from '../../common/components/security_route_page_wrapper'; +import { useDataView } from '../../data_view_picker/hooks/use_data_view'; +import { DataViewPickerScopeName } from '../../data_view_picker/constants'; +import { useSourcererDataView } from '../../sourcerer/containers'; +import { useEnableExperimental } from '../../common/hooks/use_experimental_features'; export const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10; export const TimelinesPage = React.memo(() => { const { tabName } = useParams<{ pageName: SecurityPageName; tabName: string }>(); - const { indicesExist } = useSourcererDataView(); + + const { newDataViewPickerEnabled } = useEnableExperimental(); + let { indicesExist } = useSourcererDataView(); + const { indicesExist: experimentalIndicesExist } = useDataView(DataViewPickerScopeName.default); + + if (newDataViewPickerEnabled) { + indicesExist = experimentalIndicesExist; + } + const { timelinePrivileges: { crud: canWriteTimeline }, } = useUserPrivileges(); From 4540f966793a387d0b73945e7fdc67d8957bfa0d Mon Sep 17 00:00:00 2001 From: lgestc Date: Mon, 24 Feb 2025 09:37:46 +0100 Subject: [PATCH 051/115] fix tests --- .../public/common/components/with_data_view/index.test.tsx | 7 ++++--- .../components/use_get_sourcerer_data_view.test.ts | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.test.tsx index 9313f20784474..9c0bb2b35c084 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.test.tsx @@ -9,6 +9,7 @@ import type { DataView } from '@kbn/data-views-plugin/common'; import { render, screen } from '@testing-library/react'; import { withDataView } from '.'; import { useGetScopedSourcererDataView } from '../../../sourcerer/components/use_get_sourcerer_data_view'; +import { TestProviders } from '../../mock'; interface TestComponentProps { dataView: DataView; @@ -39,18 +40,18 @@ describe('withDataViewId', () => { }); it('should render default error components when there is not fallback provided and dataViewId is null', async () => { const RenderedComponent = withDataView(TestComponent); - render(); + render(, { wrapper: TestProviders }); expect(screen.getByTestId(TEST_ID.DATA_VIEW_ERROR_COMPONENT)).toBeVisible(); }); it('should render provided fallback and dataViewId is null', async () => { const RenderedComponent = withDataView(TestComponent, ); - render(); + render(, { wrapper: TestProviders }); expect(screen.getByTestId(TEST_ID.FALLBACK_COMPONENT)).toBeVisible(); }); it('should render provided component when dataViewId is not null', async () => { (useGetScopedSourcererDataView as jest.Mock).mockReturnValue({ id: 'test' }); const RenderedComponent = withDataView(TestComponent); - render(); + render(, { wrapper: TestProviders }); expect(screen.getByTestId(TEST_ID.TEST_COMPONENT)).toBeVisible(); expect(dataViewMockFn).toHaveBeenCalledWith({ id: 'test' }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_get_sourcerer_data_view.test.ts b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_get_sourcerer_data_view.test.ts index bf687fa361db5..d2881b6091920 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_get_sourcerer_data_view.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_get_sourcerer_data_view.test.ts @@ -12,12 +12,14 @@ import { mockSourcererScope } from '../containers/mocks'; import { SourcererScopeName } from '../store/model'; import type { UseGetScopedSourcererDataViewArgs } from './use_get_sourcerer_data_view'; import { useGetScopedSourcererDataView } from './use_get_sourcerer_data_view'; +import { TestProviders } from '../../common/mock'; const renderHookCustom = (args: UseGetScopedSourcererDataViewArgs) => { return renderHook(({ sourcererScope }) => useGetScopedSourcererDataView({ sourcererScope }), { initialProps: { ...args, }, + wrapper: TestProviders, }); }; From abf09aae384853e9de503da4fd5b1778c2bde462 Mon Sep 17 00:00:00 2001 From: lgestc Date: Mon, 24 Feb 2025 10:01:14 +0100 Subject: [PATCH 052/115] fix tests --- .../__mocks__/use_experimental_features.ts | 2 ++ .../utils/timeline/use_show_timeline.test.tsx | 25 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/__mocks__/use_experimental_features.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/__mocks__/use_experimental_features.ts index 2084685c9b6a0..d7f9efc28429d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/__mocks__/use_experimental_features.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/__mocks__/use_experimental_features.ts @@ -17,3 +17,5 @@ export const useIsExperimentalFeatureEnabled = jest throw new Error(`Invalid experimental value ${feature}}`); }); + +export const useEnableExperimental = jest.fn(() => ({ newDataViewPickerEnabled: false })); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx index 9bc192880ea89..fb5665404c5f9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx @@ -13,6 +13,7 @@ import { appLinks } from '../../../app_links'; import { useUserPrivileges } from '../../components/user_privileges'; import { useShowTimeline } from './use_show_timeline'; import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks'; +import { TestProviders } from '../../mock'; jest.mock('../../components/user_privileges'); @@ -59,6 +60,8 @@ jest.mock('../../lib/kibana', () => { const mockUpselling = new UpsellingService(); const mockUiSettingsClient = uiSettingsServiceMock.createStartContract(); +const renderUseShowTimeline = () => renderHook(() => useShowTimeline(), { wrapper: TestProviders }); + describe('use show timeline', () => { beforeAll(() => { (useUserPrivileges as unknown as jest.Mock).mockReturnValue({ @@ -84,25 +87,25 @@ describe('use show timeline', () => { }); it('shows timeline for routes on default', async () => { - const { result } = renderHook(() => useShowTimeline()); + const { result } = renderUseShowTimeline(); await waitFor(() => expect(result.current).toEqual([true])); }); it('hides timeline for blacklist routes', async () => { mockUseLocation.mockReturnValueOnce({ pathname: '/rules/add_rules' }); - const { result } = renderHook(() => useShowTimeline()); + const { result } = renderUseShowTimeline(); await waitFor(() => expect(result.current).toEqual([false])); }); it('shows timeline for partial blacklist routes', async () => { mockUseLocation.mockReturnValueOnce({ pathname: '/rules' }); - const { result } = renderHook(() => useShowTimeline()); + const { result } = renderUseShowTimeline(); await waitFor(() => expect(result.current).toEqual([true])); }); it('hides timeline for sub blacklist routes', async () => { mockUseLocation.mockReturnValueOnce({ pathname: '/administration/policy' }); - const { result } = renderHook(() => useShowTimeline()); + const { result } = renderUseShowTimeline(); await waitFor(() => expect(result.current).toEqual([false])); }); it('hides timeline for users without timeline access', async () => { @@ -110,7 +113,7 @@ describe('use show timeline', () => { timelinePrivileges: { read: false }, }); - const { result } = renderHook(() => useShowTimeline()); + const { result } = renderUseShowTimeline(); const showTimeline = result.current; expect(showTimeline).toEqual([false]); }); @@ -120,7 +123,7 @@ it('shows timeline for users with timeline read access', async () => { timelinePrivileges: { read: true }, }); - const { result } = renderHook(() => useShowTimeline()); + const { result } = renderUseShowTimeline(); const showTimeline = result.current; expect(showTimeline).toEqual([true]); }); @@ -128,19 +131,19 @@ it('shows timeline for users with timeline read access', async () => { describe('sourcererDataView', () => { it('should show timeline when indices exist', () => { mockUseSourcererDataView.mockReturnValueOnce({ indicesExist: true, dataViewId: 'test' }); - const { result } = renderHook(() => useShowTimeline()); + const { result } = renderUseShowTimeline(); expect(result.current).toEqual([true]); }); it('should show timeline when dataViewId is null', () => { mockUseSourcererDataView.mockReturnValueOnce({ indicesExist: false, dataViewId: null }); - const { result } = renderHook(() => useShowTimeline()); + const { result } = renderUseShowTimeline(); expect(result.current).toEqual([true]); }); it('should not show timeline when dataViewId is not null and indices does not exist', () => { mockUseSourcererDataView.mockReturnValueOnce({ indicesExist: false, dataViewId: 'test' }); - const { result } = renderHook(() => useShowTimeline()); + const { result } = renderUseShowTimeline(); expect(result.current).toEqual([false]); }); }); @@ -148,13 +151,13 @@ describe('sourcererDataView', () => { describe('Security solution capabilities', () => { it('should show timeline when user has read capabilities', () => { mockSiemUserCanRead.mockReturnValueOnce(true); - const { result } = renderHook(() => useShowTimeline()); + const { result } = renderUseShowTimeline(); expect(result.current).toEqual([true]); }); it('should not show timeline when user does not have read capabilities', () => { mockSiemUserCanRead.mockReturnValueOnce(false); - const { result } = renderHook(() => useShowTimeline()); + const { result } = renderUseShowTimeline(); expect(result.current).toEqual([false]); }); }); From b3ecd5b089b35a3fb0304b258a113296870dc0c6 Mon Sep 17 00:00:00 2001 From: lgestc Date: Mon, 24 Feb 2025 12:24:42 +0100 Subject: [PATCH 053/115] more test fixing --- .../rule_preview/preview_histogram.test.tsx | 1 + .../alerts_by_status/alerts_by_status.test.tsx | 2 +- .../modal/actions/new_timeline_button.test.tsx | 17 +++++++++++++---- .../components/modal/header/index.test.tsx | 5 +++++ .../components/timeline/tabs/esql/index.tsx | 2 +- .../tabs/shared/use_timeline_columns.test.ts | 1 + .../timelines/pages/timelines_page.test.tsx | 6 ++++++ 7 files changed, 28 insertions(+), 6 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_histogram.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_histogram.test.tsx index 7a25095242945..8565136ebd31b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_histogram.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_histogram.test.tsx @@ -45,6 +45,7 @@ jest.mock('../../../../common/components/visualization_actions/use_visualization jest.mock('../../../../common/hooks/use_experimental_features', () => ({ useIsExperimentalFeatureEnabled: jest.fn(), + useEnableExperimental: jest.fn(() => jest.fn()), })); const mockVisualizationEmbeddable = VisualizationEmbeddable as unknown as jest.Mock; diff --git a/x-pack/solutions/security/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.test.tsx index d3db4c743b53d..8d7d095963659 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.test.tsx @@ -19,7 +19,7 @@ import { useUserPrivileges } from '../../../../common/components/user_privileges jest.mock('../../../../common/components/user_privileges'); jest.mock('../../../../common/lib/kibana/kibana_react'); jest.mock('../../../../common/hooks/use_experimental_features', () => { - return { useIsExperimentalFeatureEnabled: jest.fn() }; + return { useIsExperimentalFeatureEnabled: jest.fn(), useEnableExperimental: () => jest.fn() }; }); jest.mock('../../../../common/components/visualization_actions/visualization_embeddable'); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx index 92f9b874f8743..91ebd96c7a0c3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx @@ -15,13 +15,11 @@ import { RowRendererValues } from '../../../../../common/api/timeline'; import { defaultUdtHeaders } from '../../timeline/body/column_headers/default_headers'; 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'); return { ...original, - useSelector: jest.fn(), useDispatch: () => jest.fn(), }; }); @@ -52,8 +50,19 @@ describe('NewTimelineButton', () => { }); it('should call the correct action with clicking on the new timeline button', async () => { - const dataViewId = ''; - const selectedPatterns: string[] = []; + const dataViewId = 'security-solution'; + const selectedPatterns: string[] = [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'traces-apm*', + 'winlogbeat-*', + '-*elastic-cloud-logs-*', + '.siem-signals-spacename', + ]; const spy = jest.spyOn(timelineActions, 'createTimeline'); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx index 793cd12f99451..c8cf9048474be 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx @@ -15,6 +15,10 @@ import { useInspect } from '../../../../common/components/inspect/use_inspect'; import { useKibana } from '../../../../common/lib/kibana'; import { timelineActions } from '../../../store'; +jest.mock('../../../../common/hooks/use_experimental_features', () => ({ + useIsExperimentalFeatureEnabled: jest.fn(), + useEnableExperimental: jest.fn(() => jest.fn()), +})); jest.mock('../../../../sourcerer/containers'); jest.mock('../../../hooks/use_create_timeline'); jest.mock('../../../../common/components/inspect/use_inspect'); @@ -36,6 +40,7 @@ jest.mock('react-redux', () => { }, }, }, + dataViewPicker: { timeline: {} }, }), }; }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx index 9d6c57d51e8c6..12bdfe86ac032 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx @@ -83,7 +83,7 @@ export const DiscoverTabContent: FC = ({ timelineId }) // TODO: should not be here, used to make discover container work I suppose useEffect(() => { if (!dataViewId) return; - dataViewService.get(dataViewId).then((dv) => setDataView(dv.toSpec())); + dataViewService.get(dataViewId).then((dv) => setDataView(dv?.toSpec())); }, [dataViewId, dataViewService]); const { diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts index 66162fe82e458..767f56996bb15 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts @@ -13,6 +13,7 @@ import type { ColumnHeaderOptions } from '../../../../../../common/types/timelin jest.mock('../../../../../common/hooks/use_experimental_features', () => ({ useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true), + useEnableExperimental: jest.fn(() => jest.fn()), })); describe('useTimelineColumns', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx index ae82e2a381010..5b8fa468943e5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx @@ -25,6 +25,12 @@ jest.mock('react-router-dom', () => { jest.mock('../../overview/components/events_by_dataset'); jest.mock('../../sourcerer/containers'); jest.mock('../../common/components/user_privileges'); +jest.mock('../../data_view_picker/hooks/use_data_view', () => ({ + useDataView: jest.fn(() => ({ indicesExist: false })), +})); +jest.mock('../../common/hooks/use_experimental_features', () => ({ + useEnableExperimental: jest.fn(() => jest.fn()), +})); describe('TimelinesPage', () => { let wrapper: ShallowWrapper; From 61e078741709aa8f8daff8e89a4b6a3b30f7cd91 Mon Sep 17 00:00:00 2001 From: lgestc Date: Mon, 24 Feb 2025 14:25:16 +0100 Subject: [PATCH 054/115] fix more tests --- .../utils/timeline/use_show_timeline.test.tsx | 26 +++---------------- .../timeline/use_show_timeline_for_path.ts | 4 +-- .../components/new_timeline/index.test.tsx | 16 +++++++++--- 3 files changed, 18 insertions(+), 28 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx index fb5665404c5f9..ce3f28c35b63e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx @@ -14,8 +14,10 @@ import { useUserPrivileges } from '../../components/user_privileges'; import { useShowTimeline } from './use_show_timeline'; import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks'; import { TestProviders } from '../../mock'; +import { hasAccessToSecuritySolution } from '../../../helpers_access'; jest.mock('../../components/user_privileges'); +jest.mock('../../../helpers_access', () => ({ hasAccessToSecuritySolution: jest.fn(() => true) })); const mockUseLocation = jest.fn().mockReturnValue({ pathname: '/overview' }); jest.mock('react-router-dom', () => { @@ -36,27 +38,6 @@ jest.mock('../../../sourcerer/containers', () => ({ useSourcererDataView: () => mockUseSourcererDataView(), })); -const mockSiemUserCanRead = jest.fn(() => true); -jest.mock('../../lib/kibana', () => { - const original = jest.requireActual('../../lib/kibana'); - - return { - ...original, - useKibana: () => ({ - services: { - ...original.useKibana().services, - application: { - capabilities: { - siemV2: { - show: mockSiemUserCanRead(), - }, - }, - }, - }, - }), - }; -}); - const mockUpselling = new UpsellingService(); const mockUiSettingsClient = uiSettingsServiceMock.createStartContract(); @@ -150,13 +131,12 @@ describe('sourcererDataView', () => { describe('Security solution capabilities', () => { it('should show timeline when user has read capabilities', () => { - mockSiemUserCanRead.mockReturnValueOnce(true); const { result } = renderUseShowTimeline(); expect(result.current).toEqual([true]); }); it('should not show timeline when user does not have read capabilities', () => { - mockSiemUserCanRead.mockReturnValueOnce(false); + jest.mocked(hasAccessToSecuritySolution).mockReturnValueOnce(false); const { result } = renderUseShowTimeline(); expect(result.current).toEqual([false]); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts index a1867793067f2..a79ef12958b94 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts @@ -36,8 +36,8 @@ export const useShowTimelineForGivenPath = () => { const isTimelineAllowed = useMemo(() => { // NOTE: with new Data View Picker, data view is always defined - if (newDataViewPickerEnabled && userHasSecuritySolutionVisible) { - return true; + if (newDataViewPickerEnabled) { + return userHasSecuritySolutionVisible; } return userHasSecuritySolutionVisible && (indicesExist || dataViewId === null); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx index 0d1ed98f0bc99..d17aab5272752 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx @@ -26,7 +26,6 @@ jest.mock('react-redux', () => { return { ...original, - useSelector: jest.fn(), useDispatch: () => jest.fn(), }; }); @@ -37,8 +36,19 @@ const renderNewTimelineButton = (type: TimelineType) => render(, { wrapper: TestProviders }); describe('NewTimelineButton', () => { - const dataViewId = ''; - const selectedPatterns: string[] = []; + const dataViewId = 'security-solution'; + const selectedPatterns: string[] = [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'traces-apm*', + 'winlogbeat-*', + '-*elastic-cloud-logs-*', + '.siem-signals-spacename', + ]; (useDiscoverInTimelineContext as jest.Mock).mockReturnValue({ resetDiscoverAppState: jest.fn(), }); From 787a836603034d1f9bc4e5c614cd852c0f5ae8e0 Mon Sep 17 00:00:00 2001 From: lgestc Date: Mon, 24 Feb 2025 16:20:13 +0100 Subject: [PATCH 055/115] fix esql test --- .../public/timelines/components/timeline/tabs/esql/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx index 12bdfe86ac032..d0e12cdbfe80c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx @@ -83,7 +83,7 @@ export const DiscoverTabContent: FC = ({ timelineId }) // TODO: should not be here, used to make discover container work I suppose useEffect(() => { if (!dataViewId) return; - dataViewService.get(dataViewId).then((dv) => setDataView(dv?.toSpec())); + dataViewService.get(dataViewId).then((dv) => setDataView(dv?.toSpec?.())); }, [dataViewId, dataViewService]); const { From 871e2a3268391600af7080780aa869d66c4b0ede Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 25 Feb 2025 13:16:24 +0100 Subject: [PATCH 056/115] add redux tests --- .../components/data_view_picker/index.tsx | 6 +- .../hooks/use_init_data_view_picker.ts | 3 +- .../hooks/use_select_data_view.ts | 2 +- .../public/data_view_picker/jest.config.js | 19 +++ .../public/data_view_picker/redux/actions.ts | 23 +++ .../listeners/data_view_selected.test.ts | 158 ++++++++++++++++++ .../redux/listeners/data_view_selected.ts | 28 +++- .../redux/listeners/init_listener.ts | 2 +- .../public/data_view_picker/redux/mock.ts | 2 +- .../public/data_view_picker/redux/reducer.ts | 89 +--------- .../public/data_view_picker/redux/slices.ts | 82 +++++++++ .../public/data_view_picker/redux/types.ts | 2 + 12 files changed, 323 insertions(+), 93 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/jest.config.js create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/actions.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/slices.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx index c9b3ba606f5a4..7b74af31d636d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx @@ -13,9 +13,10 @@ import { DataView, type DataViewListItem } from '@kbn/data-views-plugin/public'; import type { DataViewPickerScopeName } from '../../constants'; import { useKibana } from '../../../common/lib/kibana/kibana_react'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../../constants'; -import { selectDataViewAsync, shared } from '../../redux/reducer'; +import { selectDataViewAsync } from '../../redux/actions'; import { useDataView } from '../../hooks/use_data_view'; import { sharedStateSelector } from '../../redux/selectors'; +import { shared } from '../../redux/slices'; export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) => { const dispatch = useDispatch(); @@ -27,7 +28,7 @@ export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) = const closeDataViewEditor = useRef<() => void | undefined>(); const closeFieldEditor = useRef<() => void | undefined>(); - // TODO: should this be implemented like that? If yes, we need to source dataView somehow or implement the same thing based on the existing state value. + // TODO: this should be disabled for the default data views probably, eg. `security-solution-default` // const canEditDataView = // Boolean(dataViewEditor?.userPermissions.editDataView()) || !dataView.isPersisted(); const canEditDataView = true; @@ -41,7 +42,6 @@ export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) = onSave: async (newDataView) => { dispatch(shared.actions.addDataView(newDataView)); dispatch(selectDataViewAsync({ id: newDataView.id, scope: [props.scope] })); - // TODO: reload data views }, allowAdHocDataView: true, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts index ca1312329a0d2..4e6237bd24c2d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts @@ -13,12 +13,13 @@ import { removeListener as originalRemoveListener, } from '@reduxjs/toolkit'; import type { RootState } from '../redux/reducer'; -import { shared, selectDataViewAsync } from '../redux/reducer'; +import { selectDataViewAsync } from '../redux/actions'; import { useKibana } from '../../common/lib/kibana'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewPickerScopeName } from '../constants'; import { createDataViewSelectedListener } from '../redux/listeners/data_view_selected'; import { createInitListener } from '../redux/listeners/init_listener'; import { useEnableExperimental } from '../../common/hooks/use_experimental_features'; +import { shared } from '../redux/slices'; type OriginalListener = Parameters[0]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts index 5ff5dde741080..1a5d2a958f571 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts @@ -8,7 +8,7 @@ import { useDispatch } from 'react-redux'; import { useCallback } from 'react'; import type { DataViewPickerScopeName } from '../constants'; -import { selectDataViewAsync } from '../redux/reducer'; +import { selectDataViewAsync } from '../redux/actions'; export const useSelectDataView = () => { const dispatch = useDispatch(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/jest.config.js b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/jest.config.js new file mode 100644 index 0000000000000..83ca1e6d8f641 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/jest.config.js @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../../..', + roots: ['/x-pack/solutions/security/plugins/security_solution/public/data_view_picker'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/solutions/security/plugins/security_solution/public/data_view_picker', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/**/*.{ts,tsx}', + ], + moduleNameMapper: require('../../server/__mocks__/module_name_map'), +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/actions.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/actions.ts new file mode 100644 index 0000000000000..abc02eb2c5c1b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/actions.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createAction } from '@reduxjs/toolkit'; + +import type { DataViewPickerScopeName } from '../constants'; +import { SLICE_PREFIX } from '../constants'; + +export const selectDataViewAsync = createAction<{ + id?: string | null; + /** + * Fallback patterns are used when the specific data view ID is undefined. This flow results in an ad-hoc data view creation + */ + fallbackPatterns?: string[]; + /** + * Specify one or more security solution scopes where the data view selection should be applied + */ + scope: DataViewPickerScopeName[]; +}>(`${SLICE_PREFIX}/selectDataView`); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.test.ts new file mode 100644 index 0000000000000..cbed4b40f7130 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.test.ts @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createDataViewSelectedListener } from './data_view_selected'; +import { selectDataViewAsync } from '../actions'; +import type { DataViewsServicePublic, FieldSpec } from '@kbn/data-views-plugin/public'; +import type { AnyAction, Dispatch, ListenerEffectAPI } from '@reduxjs/toolkit'; +import type { RootState } from '../reducer'; +import { DataViewPickerScopeName } from '../../constants'; + +const mockDataViewsService = { + get: jest.fn(), + create: jest.fn().mockResolvedValue({ + id: 'adhoc_test-*', + isPersisted: () => false, + toSpec: () => ({ id: 'adhoc_test-*', title: 'test-*' }), + }), +} as unknown as DataViewsServicePublic; + +const mockedState: RootState = { + dataViewPicker: { + analyzer: { + dataView: null, + status: 'pristine', + }, + timeline: { + dataView: null, + status: 'pristine', + }, + default: { + dataView: null, + status: 'pristine', + }, + detections: { + dataView: null, + status: 'pristine', + }, + shared: { + adhocDataViews: [ + { + id: 'adhoc_test-*', + title: 'test-*', + fields: { + '@timestamp': { + name: '@timestamp', + type: 'date', + } as unknown as FieldSpec, + }, + }, + ], + dataViews: [ + { + id: 'persisted_test-*', + title: 'test-*', + fields: { + '@timestamp': { + name: '@timestamp', + type: 'date', + } as unknown as FieldSpec, + }, + }, + ], + status: 'pristine', + }, + }, +}; + +const mockDispatch = jest.fn(); +const mockGetState = jest.fn(() => mockedState); + +const mockListenerApi = { + dispatch: mockDispatch, + getState: mockGetState, +} as unknown as ListenerEffectAPI>; + +describe('createDataViewSelectedListener', () => { + let listener: ReturnType; + + beforeEach(() => { + jest.clearAllMocks(); + listener = createDataViewSelectedListener({ dataViews: mockDataViewsService }); + }); + + it('should return cached adhoc data view first', async () => { + await listener.effect( + selectDataViewAsync({ id: 'adhoc_test-*', scope: [DataViewPickerScopeName.default] }), + mockListenerApi + ); + + expect(mockDataViewsService.get).not.toHaveBeenCalled(); + }); + + it('should try to create data view if not cached', async () => { + await listener.effect( + selectDataViewAsync({ + id: 'fetched-id', + fallbackPatterns: ['test-*'], + scope: [DataViewPickerScopeName.default], + }), + mockListenerApi + ); + + expect(mockDataViewsService.get).toHaveBeenCalledWith('fetched-id'); + expect(mockDispatch).toHaveBeenCalledWith( + expect.objectContaining({ payload: expect.objectContaining({ id: 'adhoc_test-*' }) }) + ); + }); + + it('should create adhoc data view if fetching fails', async () => { + await listener.effect( + selectDataViewAsync({ + fallbackPatterns: ['test-*'], + scope: [DataViewPickerScopeName.default], + }), + mockListenerApi + ); + + expect(mockDataViewsService.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'adhoc_test-*', + title: 'test-*', + }) + ); + expect(mockDispatch).toHaveBeenCalledWith( + expect.objectContaining({ + payload: expect.objectContaining({ id: 'adhoc_test-*' }), + }) + ); + }); + + it('should dispatch an error if both fetching and creation fail', async () => { + jest + .mocked(mockDataViewsService) + .get.mockRejectedValueOnce(new Error('some random get data view failure')); + + jest + .mocked(mockDataViewsService) + .create.mockRejectedValueOnce(new Error('some random create data view failure')); + + await listener.effect( + selectDataViewAsync({ + fallbackPatterns: ['test-*'], + scope: [DataViewPickerScopeName.default], + }), + mockListenerApi + ); + + expect(mockDispatch).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'x-pack/security_solution/dataViewPicker/default/dataViewSelectionError', + }) + ); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts index ccf36776baac6..51ff345b5a809 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts @@ -8,7 +8,9 @@ import type { DataViewSpec, DataViewsServicePublic } from '@kbn/data-views-plugin/public'; import type { AnyAction, Dispatch, ListenerEffectAPI } from '@reduxjs/toolkit'; import type { RootState } from '../reducer'; -import { scopes, selectDataViewAsync, shared } from '../reducer'; +import { scopes } from '../reducer'; +import { selectDataViewAsync } from '../actions'; +import { shared } from '../slices'; export const createDataViewSelectedListener = (dependencies: { dataViews: DataViewsServicePublic; @@ -22,16 +24,27 @@ export const createDataViewSelectedListener = (dependencies: { const state = listenerApi.getState(); const findCachedDataView = (id: string | null | undefined) => { - const dataView = + if (!id) { + return null; + } + + const cachedAdHocDataView = state.dataViewPicker.shared.adhocDataViews.find((dv) => dv.id === id) ?? null; + const cachedPersistedDataView = + state.dataViewPicker.shared.dataViews.find((dv) => dv.id === id) ?? null; + + const cachedDataView = cachedAdHocDataView || cachedPersistedDataView; + // NOTE: validate if fields are available, otherwise dont return the view // This is required to compute browserFields later. - if (!Object.keys(dataView?.fields || {})) { + // If the view is not returned here, it will be fetched further down this file, and that + // should return the full data view. + if (!Object.keys(cachedDataView?.fields || {})) { return null; } - return dataView; + return cachedDataView; }; let dataViewByIdError: unknown; @@ -56,7 +69,7 @@ export const createDataViewSelectedListener = (dependencies: { if (!dataViewSpec) { try { - const title = action.payload.patterns?.join(',') ?? ''; + const title = action.payload.fallbackPatterns?.join(',') ?? ''; if (!title.length) { throw new Error('empty adhoc title field'); } @@ -78,8 +91,11 @@ export const createDataViewSelectedListener = (dependencies: { if (dataViewSpec) { listenerApi.dispatch(currentScopeActions.setSelectedDataView(dataViewSpec)); } else if (dataViewByIdError || adhocDataViewCreationError) { + const err = dataViewByIdError || adhocDataViewCreationError; listenerApi.dispatch( - currentScopeActions.dataViewSelectionError('An error occured when setting data view') + currentScopeActions.dataViewSelectionError( + `An error occured when setting data view: ${err}` + ) ); } }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.ts index 2e72ba8cbb01f..45a09858ef52b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.ts @@ -8,7 +8,7 @@ import type { AnyAction, Dispatch, ListenerEffectAPI } from '@reduxjs/toolkit'; import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public'; import type { RootState } from '../reducer'; -import { shared } from '../reducer'; +import { shared } from '../slices'; export const createInitListener = (dependencies: { dataViews: DataViewsServicePublic }) => { return { diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts index 71bca0d394d07..b8b2a0b3ba405 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { DataViewSpec } from '@kbn/data-views-plugin/common'; +import type { DataViewSpec } from './types'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../constants'; import { initialDataViewPickerState, type RootState } from './reducer'; import { mockIndexFields } from '../../common/containers/source/mock'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts index 67b61c89359a0..6254e1c787c15 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts @@ -5,86 +5,15 @@ * 2.0. */ -import type { DataViewSpec, DataView } from '@kbn/data-views-plugin/common'; -import { combineReducers, createAction, createSlice, type PayloadAction } from '@reduxjs/toolkit'; - -import { DataViewPickerScopeName, SLICE_PREFIX } from '../constants'; -import type { SharedDataViewSelectionState } from './types'; -import { type ScopedDataViewSelectionState } from './types'; - -export const initialScopeState: ScopedDataViewSelectionState = { - dataView: null, - status: 'pristine', -}; - -export const initialSharedState: SharedDataViewSelectionState = { - dataViews: [], - adhocDataViews: [], - status: 'pristine', -}; - -export const selectDataViewAsync = createAction<{ - id?: string | null; - patterns?: string[]; - scope: DataViewPickerScopeName[]; -}>(`${SLICE_PREFIX}/selectDataView`); - -const createDataViewSelectionSlice = (scopeName: T) => - createSlice({ - name: `${SLICE_PREFIX}/${scopeName}`, - initialState: initialScopeState, - reducers: { - setSelectedDataView: (state, action: PayloadAction) => { - state.dataView = action.payload; - state.status = 'ready'; - }, - dataViewSelectionError: (state, action: PayloadAction) => { - state.status = 'error'; - }, - }, - extraReducers(builder) { - builder.addCase(selectDataViewAsync, (state, action) => { - if (!action.payload.scope.includes(scopeName)) { - return state; - } - - state.status = 'loading'; - }); - }, - }); - -export const shared = createSlice({ - name: `${SLICE_PREFIX}/shared`, - initialState: initialSharedState, - reducers: { - setDataViews: (state, action: PayloadAction) => { - state.dataViews = action.payload; - }, - addDataView: (state, action: PayloadAction) => { - const dataViewSpec = action.payload.toSpec(); - - if (action.payload.isPersisted()) { - if (state.dataViews.find((dv) => dv.id === dataViewSpec.id)) { - return; - } - - state.dataViews.push(dataViewSpec); - } else { - if (state.adhocDataViews.find((dv) => dv.title === dataViewSpec.title)) { - return; - } - - state.adhocDataViews.push(dataViewSpec); - } - }, - init: (state) => { - state.status = 'loading'; - }, - error: (state) => { - state.status = 'error'; - }, - }, -}); +import { combineReducers } from '@reduxjs/toolkit'; + +import { DataViewPickerScopeName } from '../constants'; +import { + createDataViewSelectionSlice, + initialScopeState, + initialSharedState, + shared, +} from './slices'; export const scopes = { [DataViewPickerScopeName.default]: createDataViewSelectionSlice(DataViewPickerScopeName.default), diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/slices.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/slices.ts new file mode 100644 index 0000000000000..df9af3cace2d3 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/slices.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import type { DataViewSpec, DataView } from '@kbn/data-views-plugin/common'; +import type { DataViewPickerScopeName } from '../constants'; +import { SLICE_PREFIX } from '../constants'; +import type { ScopedDataViewSelectionState, SharedDataViewSelectionState } from './types'; +import { selectDataViewAsync } from './actions'; + +export const initialScopeState: ScopedDataViewSelectionState = { + dataView: null, + status: 'pristine', +}; + +export const initialSharedState: SharedDataViewSelectionState = { + dataViews: [], + adhocDataViews: [], + status: 'pristine', +}; + +export const createDataViewSelectionSlice = (scopeName: T) => + createSlice({ + name: `${SLICE_PREFIX}/${scopeName}`, + initialState: initialScopeState, + reducers: { + setSelectedDataView: (state, action: PayloadAction) => { + state.dataView = action.payload; + state.status = 'ready'; + }, + dataViewSelectionError: (state, action: PayloadAction) => { + state.status = 'error'; + }, + }, + extraReducers(builder) { + builder.addCase(selectDataViewAsync, (state, action) => { + if (!action.payload.scope.includes(scopeName)) { + return state; + } + + state.status = 'loading'; + }); + }, + }); + +export const shared = createSlice({ + name: `${SLICE_PREFIX}/shared`, + initialState: initialSharedState, + reducers: { + setDataViews: (state, action: PayloadAction) => { + state.dataViews = action.payload; + }, + addDataView: (state, action: PayloadAction) => { + const dataViewSpec = action.payload.toSpec(); + + if (action.payload.isPersisted()) { + if (state.dataViews.find((dv) => dv.id === dataViewSpec.id)) { + return; + } + + state.dataViews.push(dataViewSpec); + } else { + if (state.adhocDataViews.find((dv) => dv.title === dataViewSpec.title)) { + return; + } + + state.adhocDataViews.push(dataViewSpec); + } + }, + init: (state) => { + state.status = 'loading'; + }, + error: (state) => { + state.status = 'error'; + }, + }, +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/types.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/types.ts index e885d7ab6cdf8..29cfe2fcfe912 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/types.ts @@ -24,3 +24,5 @@ export interface SharedDataViewSelectionState { adhocDataViews: DataViewSpec[]; status: 'pristine' | 'loading' | 'error' | 'ready'; } + +export { DataViewSpec }; From eaabb1ae56fb1b7ab22d92fe78b8314ac80b20b9 Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 25 Feb 2025 13:30:35 +0100 Subject: [PATCH 057/115] preload default data view for all scopes --- .../hooks/use_init_data_view_picker.ts | 15 +-------------- .../redux/listeners/init_listener.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts index 4e6237bd24c2d..547e3f71400b3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.ts @@ -13,9 +13,7 @@ import { removeListener as originalRemoveListener, } from '@reduxjs/toolkit'; import type { RootState } from '../redux/reducer'; -import { selectDataViewAsync } from '../redux/actions'; import { useKibana } from '../../common/lib/kibana'; -import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewPickerScopeName } from '../constants'; import { createDataViewSelectedListener } from '../redux/listeners/data_view_selected'; import { createInitListener } from '../redux/listeners/init_listener'; import { useEnableExperimental } from '../../common/hooks/use_experimental_features'; @@ -58,20 +56,9 @@ export const useInitDataViewPicker = () => { dispatch(addListener(dataViewsLoadingListener)); dispatch(addListener(dataViewSelectedListener)); + // NOTE: this kicks off the data loading in the Data View Picker dispatch(shared.actions.init()); - // Preload the default view for related scopes - dispatch( - selectDataViewAsync({ - id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, - scope: [ - DataViewPickerScopeName.default, - DataViewPickerScopeName.timeline, - DataViewPickerScopeName.analyzer, - ], - }) - ); - return () => { dispatch(removeListener(dataViewsLoadingListener)); dispatch(removeListener(dataViewSelectedListener)); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.ts index 45a09858ef52b..16a0b0a54f404 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.ts @@ -9,6 +9,8 @@ import type { AnyAction, Dispatch, ListenerEffectAPI } from '@reduxjs/toolkit'; import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public'; import type { RootState } from '../reducer'; import { shared } from '../slices'; +import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewPickerScopeName } from '../../constants'; +import { selectDataViewAsync } from '../actions'; export const createInitListener = (dependencies: { dataViews: DataViewsServicePublic }) => { return { @@ -22,6 +24,19 @@ export const createInitListener = (dependencies: { dataViews: DataViewsServicePu const dataViewSpecs = await Promise.all(dataViews.map((dataView) => dataView.toSpec())); listenerApi.dispatch(shared.actions.setDataViews(dataViewSpecs)); + + // Preload the default data view for related scopes + // NOTE: we will remove this ideally and load only when particular dataview is necessary + listenerApi.dispatch( + selectDataViewAsync({ + id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, + scope: [ + DataViewPickerScopeName.default, + DataViewPickerScopeName.timeline, + DataViewPickerScopeName.analyzer, + ], + }) + ); } catch (error: unknown) { listenerApi.dispatch(shared.actions.error()); } From ce3e2b6c0a4750224988d69408381a7470497bf0 Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 25 Feb 2025 16:24:21 +0100 Subject: [PATCH 058/115] fix tests --- .../hooks/timeline/use_investigate_in_timeline.ts | 2 +- .../data_view_picker/hooks/use_select_data_view.ts | 6 +++++- .../redux/listeners/data_view_selected.test.ts | 14 +++++++++++++- .../redux/listeners/data_view_selected.ts | 2 +- .../open_timeline/use_update_timeline.tsx | 2 +- .../public/timelines/hooks/use_create_timeline.tsx | 2 +- 6 files changed, 22 insertions(+), 6 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts index 3b7ef769718ba..0b98068787725 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts @@ -133,7 +133,7 @@ export const useInvestigateInTimeline = () => { setSelectedDataView({ scope: [SourcererScopeName.timeline], id: defaultDataView.id, - patterns: [signalIndexName || ''], + fallbackPatterns: [signalIndexName || ''], }); } else { dispatch( diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts index 1a5d2a958f571..69328f08c4e15 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts @@ -14,7 +14,11 @@ export const useSelectDataView = () => { const dispatch = useDispatch(); return useCallback( - (params: { id?: string | null; patterns?: string[]; scope: DataViewPickerScopeName[] }) => { + (params: { + id?: string | null; + fallbackPatterns?: string[]; + scope: DataViewPickerScopeName[]; + }) => { dispatch(selectDataViewAsync(params)); }, [dispatch] diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.test.ts index cbed4b40f7130..d05d9ba09534d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.test.ts @@ -104,9 +104,21 @@ describe('createDataViewSelectedListener', () => { mockListenerApi ); + // NOTE: we should check if the data view existence is checked expect(mockDataViewsService.get).toHaveBeenCalledWith('fetched-id'); + + expect(mockDataViewsService.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'adhoc_test-*', + title: 'test-*', + }) + ); + expect(mockDispatch).toHaveBeenCalledWith( - expect.objectContaining({ payload: expect.objectContaining({ id: 'adhoc_test-*' }) }) + expect.objectContaining({ + payload: expect.objectContaining({ id: 'adhoc_test-*' }), + type: 'x-pack/security_solution/dataViewPicker/default/setSelectedDataView', + }) ); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts index 51ff345b5a809..27f37fa90f32c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/data_view_selected.ts @@ -40,7 +40,7 @@ export const createDataViewSelectedListener = (dependencies: { // This is required to compute browserFields later. // If the view is not returned here, it will be fetched further down this file, and that // should return the full data view. - if (!Object.keys(cachedDataView?.fields || {})) { + if (!Object.keys(cachedDataView?.fields || {}).length) { return null; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx index 21c3b5f92653b..33db51e514d88 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx @@ -68,7 +68,7 @@ export const useUpdateTimeline = () => { if (newDataViewPickerEnabled) { selectDataView({ id: _timeline.dataViewId, - patterns: _timeline.indexNames, + fallbackPatterns: _timeline.indexNames, scope: [DataViewPickerScopeName.timeline], }); } else { diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx index 2b2d5a3f80cd0..56e3c990aee7a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx @@ -93,7 +93,7 @@ export const useCreateTimeline = ({ setSelectedDataView({ id: dataViewId, - patterns: selectedPatterns, + fallbackPatterns: selectedPatterns, scope: [DataViewPickerScopeName.timeline], }); From 90fd3aee33421bfdb5f23d688c098c0a9714e21f Mon Sep 17 00:00:00 2001 From: lgestc Date: Wed, 26 Feb 2025 12:51:36 +0100 Subject: [PATCH 059/115] more tests --- .../data_view_picker/index.test.tsx | 170 ++++++++++++++++++ .../components/data_view_picker/index.tsx | 11 +- .../hooks/use_select_data_view.ts | 7 + 3 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.test.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.test.tsx new file mode 100644 index 0000000000000..bace8c5a5896f --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.test.tsx @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { DataViewPicker } from '.'; +import { selectDataViewAsync } from '../../redux/actions'; +import { useDataView } from '../../hooks/use_data_view'; +import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewPickerScopeName } from '../../constants'; +import { shared } from '../../redux/slices'; +import { useDispatch } from 'react-redux'; +import { useKibana } from '../../../common/lib/kibana'; +import { DataView } from '@kbn/data-views-plugin/common'; +import { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common'; +import { TestProviders } from '../../../common/mock/test_providers'; +import { useSelectDataView } from '../../hooks/use_select_data_view'; + +jest.mock('../../hooks/use_data_view', () => ({ + useDataView: jest.fn(), +})); + +jest.mock('../../hooks/use_select_data_view', () => ({ + useSelectDataView: jest.fn().mockReturnValue(jest.fn()), +})); + +jest.mock('react-redux', () => { + return { + ...jest.requireActual('react-redux'), + useDispatch: jest.fn(), + }; +}); + +jest.mock('../../../common/lib/kibana', () => ({ + useKibana: jest.fn(), +})); + +jest.mock('@kbn/unified-search-plugin/public', () => ({ + ...jest.requireActual('@kbn/unified-search-plugin/public'), + DataViewPicker: jest.fn((props) => ( +

+ + + {props.onAddField && ( + + )} +
{props.currentDataViewId}
+
{props.trigger.label}
+
+ )), +})); + +describe('DataViewPicker', () => { + let mockDispatch = jest.fn(); + + beforeEach(() => { + jest.mocked(useDataView).mockReturnValue({ + dataView: { + id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, + name: 'Default Security Data View', + }, + indicesExist: true, + status: 'ready', + }); + + mockDispatch = jest.fn(); + + jest.mocked(useDispatch).mockReturnValue(mockDispatch); + + jest.mocked(useKibana).mockReturnValue({ + services: { + dataViewFieldEditor: { openEditor: jest.fn() }, + data: { dataViews: { get: jest.fn() } }, + }, + } as unknown as ReturnType); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders with the current data view ID', () => { + render( + + + + ); + + expect(screen.getByTestId('currentDataViewId')).toHaveTextContent('security-solution-default'); + expect(screen.getByTestId('trigger')).toHaveTextContent('Default Security Data View'); + }); + + it('calls selectDataView when changing data view', () => { + render( + + + + ); + + fireEvent.click(screen.getByTestId('changeDataView')); + + expect(jest.mocked(useSelectDataView())).toHaveBeenCalledWith({ + id: 'new-data-view-id', + scope: ['default'], + }); + }); + + it.skip('opens data view editor when creating a new data view', async () => { + render( + + + + ); + + fireEvent.click(screen.getByTestId('createDataView')); + + expect(jest.mocked(useKibana().services.dataViewEditor.openEditor)).toHaveBeenCalled(); + + // Test the onSave callback + const onSaveCallback = jest.mocked(useKibana().services.dataViewEditor.openEditor).mock + .calls[0][0].onSave; + const newDataView = new DataView({ + spec: { id: 'new-data-view-id', name: 'New Data View' }, + fieldFormats: new FieldFormatsRegistry(), + }); + + onSaveCallback(newDataView); + + expect(mockDispatch).toHaveBeenCalledWith(shared.actions.addDataView(newDataView)); + expect(selectDataViewAsync).toHaveBeenCalledWith({ + id: 'new-data-view-id', + scope: ['timeline'], + }); + }); + + it.skip('opens field editor when adding a field', async () => { + const mockFieldEditorClose = jest.fn(); + jest + .mocked(useKibana().services.dataViewFieldEditor.openEditor) + .mockResolvedValue(mockFieldEditorClose); + + render( + + + + ); + + fireEvent.click(screen.getByTestId('addField')); + + await waitFor(() => { + expect(jest.mocked(useKibana().services.data.dataViews.get)).toHaveBeenCalledWith( + 'test-data-view-id' + ); + expect(jest.mocked(useKibana().services.dataViewFieldEditor.openEditor)).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx index 7b74af31d636d..7bc3714626b9c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx @@ -13,13 +13,14 @@ import { DataView, type DataViewListItem } from '@kbn/data-views-plugin/public'; import type { DataViewPickerScopeName } from '../../constants'; import { useKibana } from '../../../common/lib/kibana/kibana_react'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../../constants'; -import { selectDataViewAsync } from '../../redux/actions'; import { useDataView } from '../../hooks/use_data_view'; import { sharedStateSelector } from '../../redux/selectors'; import { shared } from '../../redux/slices'; +import { useSelectDataView } from '../../hooks/use_select_data_view'; export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) => { const dispatch = useDispatch(); + const selectDataView = useSelectDataView(); const { services: { dataViewEditor, data, dataViewFieldEditor, fieldFormats }, @@ -41,11 +42,11 @@ export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) = closeDataViewEditor.current = dataViewEditor.openEditor({ onSave: async (newDataView) => { dispatch(shared.actions.addDataView(newDataView)); - dispatch(selectDataViewAsync({ id: newDataView.id, scope: [props.scope] })); + selectDataView({ id: newDataView.id, scope: [props.scope] }); }, allowAdHocDataView: true, }); - }, [dataViewEditor, dispatch, props.scope]); + }, [dataViewEditor, dispatch, props.scope, selectDataView]); const onFieldEdited = useCallback(() => {}, []); @@ -78,9 +79,9 @@ export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) = const handleChangeDataView = useCallback( (id: string) => { - dispatch(selectDataViewAsync({ id, scope: [props.scope] })); + selectDataView({ id, scope: [props.scope] }); }, - [dispatch, props.scope] + [props.scope, selectDataView] ); const handleEditDataView = useCallback(() => {}, []); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts index 69328f08c4e15..0d179b1c589e3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.ts @@ -16,7 +16,14 @@ export const useSelectDataView = () => { return useCallback( (params: { id?: string | null; + /** + * List of patterns that will be used to construct the adhoc data view when + * .id param is not provided or the data view does not exist + */ fallbackPatterns?: string[]; + /** + * Data view selection will be applied to the scopes listed here + */ scope: DataViewPickerScopeName[]; }) => { dispatch(selectDataViewAsync(params)); From 128bfb92428fa32605105467df32ec7e1a65e4a0 Mon Sep 17 00:00:00 2001 From: lgestc Date: Wed, 26 Feb 2025 13:20:50 +0100 Subject: [PATCH 060/115] add use_browser_fields test --- .../hooks/use_browser_fields.test.ts | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_browser_fields.test.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_browser_fields.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_browser_fields.test.ts new file mode 100644 index 0000000000000..c862843873826 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_browser_fields.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react'; +import { TestProviders } from '../../common/mock'; +import { useBrowserFields } from './use_browser_fields'; +import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewPickerScopeName } from '../constants'; +import { useDataView } from './use_data_view'; +import { type FieldSpec } from '@kbn/data-views-plugin/common'; + +jest.mock('./use_data_view', () => ({ + useDataView: jest.fn(), +})); + +describe('useBrowserFields', () => { + beforeAll(() => { + jest.mocked(useDataView).mockReturnValue({ + dataView: { + id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, + fields: { + '@timestamp': { + type: 'date', + name: '@timestamp', + } as FieldSpec, + }, + }, + status: 'ready', + indicesExist: true, + }); + }); + + it('should call the useDataView hook and return browser fields map', () => { + const wrapper = renderHook(() => useBrowserFields(DataViewPickerScopeName.default), { + wrapper: TestProviders, + }); + + expect(wrapper.result.current).toMatchInlineSnapshot(` + Object { + "base": Object { + "fields": Object { + "@timestamp": Object { + "name": "@timestamp", + "type": "date", + }, + }, + }, + } + `); + }); +}); From 517b20b0c51e0f62089d64d878aa6016cd258bac Mon Sep 17 00:00:00 2001 From: lgestc Date: Wed, 26 Feb 2025 13:54:43 +0100 Subject: [PATCH 061/115] more tests --- .../hooks/use_data_view.test.ts | 30 +++++++++++ .../hooks/use_full_data_view.test.ts | 53 +++++++++++++++++++ .../hooks/use_full_data_view.ts | 8 ++- .../hooks/use_init_data_view_picker.test.ts | 15 ++++++ .../hooks/use_select_data_view.test.ts | 15 ++++++ 5 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_data_view.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.test.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_data_view.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_data_view.test.ts new file mode 100644 index 0000000000000..2baa45e3e4345 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_data_view.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react'; +import { TestProviders } from '../../common/mock'; +import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewPickerScopeName } from '../constants'; +import { useDataView } from './use_data_view'; + +describe('useDataView', () => { + describe('when data view is available in the store', () => { + it('should return dataView from the store', () => { + const wrapper = renderHook(() => useDataView(DataViewPickerScopeName.default), { + wrapper: TestProviders, + }); + + expect(wrapper.result.current.status).toEqual('ready'); + expect(wrapper.result.current.dataView).toMatchObject({ + id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, + }); + }); + }); + + describe('when data view is not available in the store', () => { + it.todo('should return null'); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.test.ts new file mode 100644 index 0000000000000..fb4193c04b1e6 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react'; +import { TestProviders } from '../../common/mock'; +import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewPickerScopeName } from '../constants'; + +import { useFullDataView } from './use_full_data_view'; +import { useDataView } from './use_data_view'; +import { type FieldSpec, DataView } from '@kbn/data-views-plugin/common'; + +jest.mock('./use_data_view', () => ({ + useDataView: jest.fn(), +})); + +describe('useFullDataView', () => { + describe('when data view is available', () => { + beforeAll(() => { + jest.mocked(useDataView).mockReturnValue({ + dataView: { + id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, + fields: { + '@timestamp': { + type: 'date', + name: '@timestamp', + } as FieldSpec, + }, + }, + status: 'ready', + indicesExist: true, + }); + }); + + it('should return DataView instance', () => { + const wrapper = renderHook( + () => useFullDataView({ dataViewPickerScope: DataViewPickerScopeName.default }), + { + wrapper: TestProviders, + } + ); + + expect(wrapper.result.current).toBeInstanceOf(DataView); + }); + }); + + describe('when data view is not available', () => { + it.todo('should return undefined'); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.ts index 6b83dbdfaaa89..9bef29f49f0ff 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.ts @@ -16,11 +16,9 @@ export interface UseGetScopedSourcererDataViewArgs { } /* - * - * returns the created dataView based on dataView spec - * returned from useDataView - * - * */ + * This hook should be used whenever we need the actual DataView and not just the spec for the + * selected data view. + */ export const useFullDataView = ({ dataViewPickerScope, }: UseGetScopedSourcererDataViewArgs): DataView | undefined => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.test.ts new file mode 100644 index 0000000000000..904fe44663a67 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.test.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react'; +import { TestProviders } from '../../common/mock'; + +describe('useInitDataViewPicker', () => { + describe('when', () => { + it.todo('should work'); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.test.ts new file mode 100644 index 0000000000000..a3c3ca787736d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.test.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react'; +import { TestProviders } from '../../common/mock'; + +describe('useSelectDataView', () => { + describe('when', () => { + it.todo('should work'); + }); +}); From 79868ca106116dbfd674c9762f822fa6672ac37c Mon Sep 17 00:00:00 2001 From: lgestc Date: Wed, 26 Feb 2025 13:59:07 +0100 Subject: [PATCH 062/115] add init listener test --- .../redux/listeners/init_listener.test.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.test.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.test.ts new file mode 100644 index 0000000000000..17c48f0f78ba8 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.test.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createInitListener } from './init_listener'; +import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public'; + +const mockDataViewsService = { + get: jest.fn(), + create: jest.fn().mockResolvedValue({ + id: 'adhoc_test-*', + isPersisted: () => false, + toSpec: () => ({ id: 'adhoc_test-*', title: 'test-*' }), + }), +} as unknown as DataViewsServicePublic; + +describe('createInitListener', () => { + let listener: ReturnType; + + beforeEach(() => { + jest.clearAllMocks(); + listener = createInitListener({ dataViews: mockDataViewsService }); + }); + + it.todo('should load the data views and dispatch further actions'); +}); From 742c3400caa51026f6f270c1c390b5226184ece9 Mon Sep 17 00:00:00 2001 From: lgestc Date: Thu, 27 Feb 2025 13:49:15 +0100 Subject: [PATCH 063/115] add missing tests --- .../components/data_view_picker/index.test.tsx | 13 +++++++------ .../components/data_view_picker/index.tsx | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.test.tsx index bace8c5a5896f..b56896c6a430b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.test.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { DataViewPicker } from '.'; -import { selectDataViewAsync } from '../../redux/actions'; import { useDataView } from '../../hooks/use_data_view'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewPickerScopeName } from '../../constants'; import { shared } from '../../redux/slices'; @@ -83,6 +82,7 @@ describe('DataViewPicker', () => { jest.mocked(useKibana).mockReturnValue({ services: { dataViewFieldEditor: { openEditor: jest.fn() }, + dataViewEditor: { openEditor: jest.fn() }, data: { dataViews: { get: jest.fn() } }, }, } as unknown as ReturnType); @@ -118,7 +118,7 @@ describe('DataViewPicker', () => { }); }); - it.skip('opens data view editor when creating a new data view', async () => { + it('opens data view editor when creating a new data view', async () => { render( @@ -132,6 +132,7 @@ describe('DataViewPicker', () => { // Test the onSave callback const onSaveCallback = jest.mocked(useKibana().services.dataViewEditor.openEditor).mock .calls[0][0].onSave; + const newDataView = new DataView({ spec: { id: 'new-data-view-id', name: 'New Data View' }, fieldFormats: new FieldFormatsRegistry(), @@ -140,13 +141,13 @@ describe('DataViewPicker', () => { onSaveCallback(newDataView); expect(mockDispatch).toHaveBeenCalledWith(shared.actions.addDataView(newDataView)); - expect(selectDataViewAsync).toHaveBeenCalledWith({ + expect(jest.mocked(useSelectDataView())).toHaveBeenCalledWith({ id: 'new-data-view-id', - scope: ['timeline'], + scope: ['default'], }); }); - it.skip('opens field editor when adding a field', async () => { + it('opens field editor when adding a field', async () => { const mockFieldEditorClose = jest.fn(); jest .mocked(useKibana().services.dataViewFieldEditor.openEditor) @@ -162,7 +163,7 @@ describe('DataViewPicker', () => { await waitFor(() => { expect(jest.mocked(useKibana().services.data.dataViews.get)).toHaveBeenCalledWith( - 'test-data-view-id' + 'security-solution-default' ); expect(jest.mocked(useKibana().services.dataViewFieldEditor.openEditor)).toHaveBeenCalled(); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx index 7bc3714626b9c..f601dd9ae9172 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx @@ -11,7 +11,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { DataView, type DataViewListItem } from '@kbn/data-views-plugin/public'; import type { DataViewPickerScopeName } from '../../constants'; -import { useKibana } from '../../../common/lib/kibana/kibana_react'; +import { useKibana } from '../../../common/lib/kibana'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../../constants'; import { useDataView } from '../../hooks/use_data_view'; import { sharedStateSelector } from '../../redux/selectors'; From 1e82688a4c40f629fab2937649dd48d8a25b0a66 Mon Sep 17 00:00:00 2001 From: lgestc Date: Thu, 27 Feb 2025 14:08:17 +0100 Subject: [PATCH 064/115] add useDataView test --- .../hooks/use_data_view.test.ts | 26 ++++++++++--------- .../public/data_view_picker/redux/mock.ts | 2 +- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_data_view.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_data_view.test.ts index 2baa45e3e4345..88291b7572324 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_data_view.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_data_view.test.ts @@ -11,20 +11,22 @@ import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewPickerScopeName } from import { useDataView } from './use_data_view'; describe('useDataView', () => { - describe('when data view is available in the store', () => { - it('should return dataView from the store', () => { - const wrapper = renderHook(() => useDataView(DataViewPickerScopeName.default), { - wrapper: TestProviders, - }); + it('should return correct dataView from the store, based on the provided scope', () => { + const wrapper = renderHook((scope) => useDataView(scope), { + wrapper: TestProviders, + initialProps: DataViewPickerScopeName.default, + }); - expect(wrapper.result.current.status).toEqual('ready'); - expect(wrapper.result.current.dataView).toMatchObject({ - id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, - }); + expect(wrapper.result.current.status).toEqual('ready'); + expect(wrapper.result.current.dataView).toMatchObject({ + id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, }); - }); - describe('when data view is not available in the store', () => { - it.todo('should return null'); + wrapper.rerender(DataViewPickerScopeName.timeline); + + expect(wrapper.result.current.status).toEqual('ready'); + expect(wrapper.result.current.dataView).toMatchObject({ + id: 'mock-timeline-data-view', + }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts index b8b2a0b3ba405..ac8cbd9395f56 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts @@ -37,7 +37,7 @@ export const mockDataViewPickerState: RootState = { ...dataViewPickerState, timeline: { ...dataViewPickerState.timeline, - dataView: mockDefaultDataViewSpec, + dataView: { ...mockDefaultDataViewSpec, id: 'mock-timeline-data-view' }, status: 'ready', }, default: { From dd84f9900eb3b66bcb224c5571c53bd248298bf9 Mon Sep 17 00:00:00 2001 From: lgestc Date: Thu, 27 Feb 2025 15:22:49 +0100 Subject: [PATCH 065/115] more tests --- .../hooks/use_full_data_view.test.ts | 24 +++++++++++-- .../hooks/use_full_data_view.ts | 2 +- .../redux/listeners/init_listener.test.ts | 35 ++++++++++++++++++- .../public/data_view_picker/redux/mock.ts | 1 + 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.test.ts index fb4193c04b1e6..192897893c2ad 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.test.ts @@ -47,7 +47,27 @@ describe('useFullDataView', () => { }); }); - describe('when data view is not available', () => { - it.todo('should return undefined'); + describe('when data view fields are not available', () => { + beforeEach(() => { + jest.mocked(useDataView).mockReturnValue({ + dataView: { + id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, + title: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, + }, + status: 'pristine', + indicesExist: true, + }); + }); + + it('should return undefined', () => { + const wrapper = renderHook( + () => useFullDataView({ dataViewPickerScope: DataViewPickerScopeName.default }), + { + wrapper: TestProviders, + } + ); + + expect(wrapper.result.current).toBeUndefined(); + }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.ts index 9bef29f49f0ff..eea1f23ca76a1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_full_data_view.ts @@ -28,7 +28,7 @@ export const useFullDataView = ({ const { dataView: dataViewSpec } = useDataView(dataViewPickerScope); const dataView = useMemo(() => { - if (Object.keys(dataViewSpec).length) { + if (Object.keys(dataViewSpec?.fields ?? {}).length) { return new DataView({ spec: dataViewSpec, fieldFormats }); } else { return undefined; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.test.ts index 17c48f0f78ba8..b345646f93a36 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.test.ts @@ -5,8 +5,14 @@ * 2.0. */ +import type { AnyAction, Dispatch, ListenerEffectAPI } from '@reduxjs/toolkit'; +import { mockDataViewPickerState } from '../mock'; import { createInitListener } from './init_listener'; import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public'; +import type { RootState } from '../reducer'; +import { shared } from '../slices'; +import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewPickerScopeName } from '../../constants'; +import { selectDataViewAsync } from '../actions'; const mockDataViewsService = { get: jest.fn(), @@ -15,8 +21,17 @@ const mockDataViewsService = { isPersisted: () => false, toSpec: () => ({ id: 'adhoc_test-*', title: 'test-*' }), }), + getAllDataViewLazy: jest.fn().mockReturnValue([]), } as unknown as DataViewsServicePublic; +const mockDispatch = jest.fn(); +const mockGetState = jest.fn(() => mockDataViewPickerState); + +const mockListenerApi = { + dispatch: mockDispatch, + getState: mockGetState, +} as unknown as ListenerEffectAPI>; + describe('createInitListener', () => { let listener: ReturnType; @@ -25,5 +40,23 @@ describe('createInitListener', () => { listener = createInitListener({ dataViews: mockDataViewsService }); }); - it.todo('should load the data views and dispatch further actions'); + it('should load the data views and dispatch further actions', async () => { + await listener.effect(shared.actions.init(), mockListenerApi); + + expect(jest.mocked(mockDataViewsService.getAllDataViewLazy)).toHaveBeenCalled(); + + expect(jest.mocked(mockListenerApi.dispatch)).toBeCalledWith(shared.actions.setDataViews([])); + expect(jest.mocked(mockListenerApi.dispatch)).toBeCalledWith( + selectDataViewAsync({ + id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, + scope: [ + DataViewPickerScopeName.default, + DataViewPickerScopeName.timeline, + DataViewPickerScopeName.analyzer, + ], + }) + ); + }); + + it.todo('should dispatch error correctly'); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts index ac8cbd9395f56..3f2c3b311c207 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/mock.ts @@ -32,6 +32,7 @@ const mockDefaultDataViewSpec: DataViewSpec = { '-*elastic-cloud-logs-*', ].join(), }; + export const mockDataViewPickerState: RootState = { dataViewPicker: { ...dataViewPickerState, From bec10ae31470b2a4c8aa307d5c599563cc1a2fd8 Mon Sep 17 00:00:00 2001 From: lgestc Date: Fri, 28 Feb 2025 09:21:15 +0100 Subject: [PATCH 066/115] more tests --- .../hooks/use_init_data_view_picker.test.ts | 10 ++++++++-- .../hooks/use_select_data_view.test.ts | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.test.ts index 904fe44663a67..a7654f747fd91 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_init_data_view_picker.test.ts @@ -7,9 +7,15 @@ import { renderHook } from '@testing-library/react'; import { TestProviders } from '../../common/mock'; +import { useInitDataViewPicker } from './use_init_data_view_picker'; describe('useInitDataViewPicker', () => { - describe('when', () => { - it.todo('should work'); + it('should render', () => { + renderHook( + () => { + return useInitDataViewPicker(); + }, + { wrapper: TestProviders } + ); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.test.ts index a3c3ca787736d..ba4b62ae6e0d0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/hooks/use_select_data_view.test.ts @@ -7,9 +7,15 @@ import { renderHook } from '@testing-library/react'; import { TestProviders } from '../../common/mock'; +import { useSelectDataView } from './use_select_data_view'; describe('useSelectDataView', () => { - describe('when', () => { - it.todo('should work'); + it('should render', () => { + renderHook( + () => { + return useSelectDataView(); + }, + { wrapper: TestProviders } + ); }); }); From 5f68a9066d8cf4a283aef461acb5d5628df0d52b Mon Sep 17 00:00:00 2001 From: lgestc Date: Fri, 28 Feb 2025 15:02:52 +0100 Subject: [PATCH 067/115] add error test --- .../redux/listeners/init_listener.test.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.test.ts index b345646f93a36..a09b016f72140 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/listeners/init_listener.test.ts @@ -58,5 +58,17 @@ describe('createInitListener', () => { ); }); - it.todo('should dispatch error correctly'); + describe('when data views fetch returns an error', () => { + beforeEach(() => { + jest + .mocked(mockDataViewsService.getAllDataViewLazy) + .mockRejectedValue(new Error('some loading error')); + }); + + it('should dispatch error correctly', async () => { + await listener.effect(shared.actions.init(), mockListenerApi); + + expect(jest.mocked(mockListenerApi.dispatch)).toBeCalledWith(shared.actions.error()); + }); + }); }); From baecafd7104182a1f5ad317608f79b72cd12840c Mon Sep 17 00:00:00 2001 From: lgestc Date: Fri, 28 Feb 2025 15:26:22 +0100 Subject: [PATCH 068/115] linter errors --- .../security_solution/public/data_view_picker/redux/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/types.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/types.ts index 29cfe2fcfe912..89e7e55983dc9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/types.ts @@ -25,4 +25,4 @@ export interface SharedDataViewSelectionState { status: 'pristine' | 'loading' | 'error' | 'ready'; } -export { DataViewSpec }; +export { type DataViewSpec }; From 119d6a21566288a3059da7811d9519c01028bbcd Mon Sep 17 00:00:00 2001 From: lgestc Date: Mon, 3 Mar 2025 13:27:27 +0100 Subject: [PATCH 069/115] remove middleware from data view picker --- .../public/common/store/middlewares.ts | 4 ++++ .../security_solution/public/common/store/store.ts | 4 +--- .../public/data_view_picker/redux/middleware.ts | 10 ---------- 3 files changed, 5 insertions(+), 13 deletions(-) delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/middleware.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/store/middlewares.ts b/x-pack/solutions/security/plugins/security_solution/public/common/store/middlewares.ts index 76c290a3c895c..fe7a2b0adf670 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/store/middlewares.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/store/middlewares.ts @@ -7,13 +7,17 @@ import type { CoreStart } from '@kbn/core/public'; import type { Storage } from '@kbn/kibana-utils-plugin/public'; +import { createListenerMiddleware } from '@reduxjs/toolkit'; import { createTimelineMiddlewares } from '../../timelines/store/middlewares/create_timeline_middlewares'; import { dataTableLocalStorageMiddleware } from './data_table/middleware_local_storage'; import { userAssetTableLocalStorageMiddleware } from '../../explore/users/store/middleware_storage'; +const listenerMiddleware = createListenerMiddleware(); + export function createMiddlewares(kibana: CoreStart, storage: Storage) { return [ + listenerMiddleware.middleware, dataTableLocalStorageMiddleware(storage), userAssetTableLocalStorageMiddleware(storage), ...createTimelineMiddlewares(kibana), diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts b/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts index 11c644e0e35ac..8b3f8ead09175 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/store/store.ts @@ -56,7 +56,6 @@ import { createMiddlewares } from './middlewares'; import { addNewTimeline } from '../../timelines/store/helpers'; import { initialNotesState } from '../../notes/store/notes.slice'; import { hasAccessToSecuritySolution } from '../../helpers_access'; -import { listenerMiddleware } from '../../data_view_picker/redux/middleware'; let store: Store | null = null; @@ -289,8 +288,7 @@ export const createStore = ( ...createMiddlewares(kibana, storage), telemetryMiddleware, ...(additionalMiddleware ?? []), - thunk, - listenerMiddleware.middleware + thunk ); store = createReduxStore( diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/middleware.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/middleware.ts deleted file mode 100644 index dc6e6eb62e165..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/middleware.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createListenerMiddleware } from '@reduxjs/toolkit'; - -export const listenerMiddleware = createListenerMiddleware(); From 6ff043724851823f93269e7523c84075a721c068 Mon Sep 17 00:00:00 2001 From: lgestc Date: Mon, 3 Mar 2025 13:51:31 +0100 Subject: [PATCH 070/115] usecallback instead of usememo --- .../components/data_view_picker/index.tsx | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx index f601dd9ae9172..68e29257f5d8a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.tsx @@ -29,11 +29,6 @@ export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) = const closeDataViewEditor = useRef<() => void | undefined>(); const closeFieldEditor = useRef<() => void | undefined>(); - // TODO: this should be disabled for the default data views probably, eg. `security-solution-default` - // const canEditDataView = - // Boolean(dataViewEditor?.userPermissions.editDataView()) || !dataView.isPersisted(); - const canEditDataView = true; - const { dataView } = useDataView(props.scope); const dataViewId = dataView?.id; @@ -50,11 +45,8 @@ export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) = const onFieldEdited = useCallback(() => {}, []); - const editField = useMemo(() => { - if (!canEditDataView) { - return; - } - return async (fieldName?: string, _uiAction: 'edit' | 'add' = 'edit') => { + const editField = useCallback( + async (fieldName?: string, _uiAction: 'edit' | 'add' = 'edit') => { if (!dataViewId) { return; } @@ -69,14 +61,12 @@ export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) = onFieldEdited(); }, }); - }; - }, [canEditDataView, dataViewId, data.dataViews, dataViewFieldEditor, onFieldEdited]); - - const addField = useMemo( - () => (canEditDataView && editField ? () => editField(undefined, 'add') : undefined), - [editField, canEditDataView] + }, + [dataViewId, data.dataViews, dataViewFieldEditor, onFieldEdited] ); + const handleAddField = useCallback(() => editField(undefined, 'add'), [editField]); + const handleChangeDataView = useCallback( (id: string) => { selectDataView({ id, scope: [props.scope] }); @@ -120,7 +110,7 @@ export const DataViewPicker = memo((props: { scope: DataViewPickerScopeName }) = trigger={triggerConfig} onChangeDataView={handleChangeDataView} onEditDataView={handleEditDataView} - onAddField={addField} + onAddField={handleAddField} onDataViewCreated={createNewDataView} adHocDataViews={adhocDataViews} savedDataViews={managedDataViews} From d31acdcf61b614b18dc3154efe66997a7d99223b Mon Sep 17 00:00:00 2001 From: lgestc Date: Mon, 3 Mar 2025 15:10:58 +0100 Subject: [PATCH 071/115] fix shared loading state --- .../security_solution/public/data_view_picker/redux/slices.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/slices.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/slices.ts index df9af3cace2d3..f2ee938895786 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/slices.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/slices.ts @@ -54,6 +54,7 @@ export const shared = createSlice({ reducers: { setDataViews: (state, action: PayloadAction) => { state.dataViews = action.payload; + state.status = 'ready'; }, addDataView: (state, action: PayloadAction) => { const dataViewSpec = action.payload.toSpec(); From 5ac74b7018c999f7cb08bf2e479c6dd3fcae8bd8 Mon Sep 17 00:00:00 2001 From: lgestc Date: Tue, 4 Mar 2025 09:11:38 +0100 Subject: [PATCH 072/115] naming --- .../public/app/home/index.tsx | 4 +- .../components/with_data_view/index.tsx | 6 +- .../timeline/use_investigate_in_timeline.ts | 2 +- .../public/common/mock/global_state.ts | 2 +- .../public/common/store/reducer.ts | 10 ++-- .../public/common/store/types.ts | 2 +- .../data_view_picker/index.test.tsx | 12 ++-- .../components/data_view_picker/index.tsx | 4 +- .../constants.ts | 4 +- .../hooks/use_browser_fields.test.ts | 4 +- .../hooks/use_browser_fields.ts | 4 +- .../hooks/use_data_view.test.ts | 6 +- .../hooks/use_data_view.ts | 4 +- .../hooks/use_full_data_view.test.ts | 6 +- .../hooks/use_full_data_view.ts | 8 +-- .../hooks/use_init_data_view_manager.test.ts} | 4 +- .../hooks/use_init_data_view_manager.ts} | 2 +- .../hooks/use_select_data_view.test.ts | 0 .../hooks/use_select_data_view.ts | 4 +- .../hooks/use_selected_patterns.ts | 4 +- .../jest.config.js | 6 +- .../redux/actions.ts | 4 +- .../listeners/data_view_selected.test.ts | 16 +++--- .../redux/listeners/data_view_selected.ts | 4 +- .../redux/listeners/init_listener.test.ts | 8 +-- .../redux/listeners/init_listener.ts | 8 +-- .../redux/listeners/readme.md | 0 .../redux/mock.ts | 14 ++--- .../public/data_view_manager/redux/reducer.ts | 55 +++++++++++++++++++ .../redux/selectors.ts | 14 ++--- .../redux/slices.ts | 4 +- .../redux/types.ts | 0 .../public/data_view_picker/redux/reducer.ts | 53 ------------------ .../use_add_bulk_to_timeline.tsx | 6 +- .../components/modal/header/index.test.tsx | 2 +- .../components/modal/header/index.tsx | 10 ++-- .../components/open_timeline/index.tsx | 4 +- .../open_timeline/note_previews/index.tsx | 2 +- .../open_timeline/use_update_timeline.tsx | 6 +- .../timeline/data_providers/index.tsx | 2 +- .../components/timeline/kpi/kpi_container.tsx | 6 +- .../timeline/query_bar/eql/index.tsx | 4 +- .../components/timeline/query_bar/index.tsx | 4 +- .../timeline/search_or_filter/index.tsx | 2 +- .../search_or_filter/search_or_filter.tsx | 2 +- .../components/timeline/tabs/esql/index.tsx | 2 +- .../components/timeline/tabs/pinned/index.tsx | 4 +- .../components/timeline/tabs/query/index.tsx | 6 +- .../tabs/session/use_session_view.tsx | 2 +- .../tabs/shared/use_timeline_columns.tsx | 2 +- .../containers/use_timeline_data_filters.ts | 2 +- .../timelines/hooks/use_create_timeline.tsx | 14 ++--- .../timelines/pages/timelines_page.test.tsx | 2 +- .../public/timelines/pages/timelines_page.tsx | 6 +- 54 files changed, 185 insertions(+), 183 deletions(-) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/components/data_view_picker/index.test.tsx (92%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/components/data_view_picker/index.tsx (96%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/constants.ts (66%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/hooks/use_browser_fields.test.ts (88%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/hooks/use_browser_fields.ts (84%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/hooks/use_data_view.test.ts (82%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/hooks/use_data_view.ts (78%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/hooks/use_full_data_view.test.ts (86%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/hooks/use_full_data_view.ts (82%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker/hooks/use_init_data_view_picker.test.ts => data_view_manager/hooks/use_init_data_view_manager.test.ts} (82%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker/hooks/use_init_data_view_picker.ts => data_view_manager/hooks/use_init_data_view_manager.ts} (98%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/hooks/use_select_data_view.test.ts (100%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/hooks/use_select_data_view.ts (89%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/hooks/use_selected_patterns.ts (75%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/jest.config.js (83%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/redux/actions.ts (88%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/redux/listeners/data_view_selected.test.ts (90%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/redux/listeners/data_view_selected.ts (95%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/redux/listeners/init_listener.test.ts (91%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/redux/listeners/init_listener.ts (86%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/redux/listeners/readme.md (100%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/redux/mock.ts (79%) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/reducer.ts rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/redux/selectors.ts (54%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/redux/slices.ts (93%) rename x-pack/solutions/security/plugins/security_solution/public/{data_view_picker => data_view_manager}/redux/types.ts (100%) delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_picker/redux/reducer.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/home/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/app/home/index.tsx index 9e3b7101b11df..9f6af3620b915 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/app/home/index.tsx @@ -27,7 +27,7 @@ import { useSetupDetectionEngineHealthApi } from '../../detection_engine/rule_mo import { TopValuesPopover } from '../components/top_values_popover/top_values_popover'; import { AssistantOverlay } from '../../assistant/overlay'; import { useInitSourcerer } from '../../sourcerer/containers/use_init_sourcerer'; -import { useInitDataViewPicker } from '../../data_view_picker/hooks/use_init_data_view_picker'; +import { useInitDataViewManager } from '../../data_view_manager/hooks/use_init_data_view_manager'; interface HomePageProps { children: React.ReactNode; @@ -39,7 +39,7 @@ const HomePageComponent: React.FC = ({ children }) => { useUrlState(); useUpdateBrowserTitle(); useUpdateExecutionContext(); - useInitDataViewPicker(); + useInitDataViewManager(); const { browserFields } = useSourcererDataView(getScopeFromPath(pathname)); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.tsx index 9219d90540025..67578a509f846 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/with_data_view/index.tsx @@ -9,8 +9,8 @@ import React from 'react'; import type { ComponentType } from 'react'; import type { ReactElement } from 'react-markdown'; import type { DataView } from '@kbn/data-views-plugin/common'; -import { DataViewPickerScopeName } from '../../../data_view_picker/constants'; -import { useFullDataView } from '../../../data_view_picker/hooks/use_full_data_view'; +import { DataViewManagerScopeName } from '../../../data_view_manager/constants'; +import { useFullDataView } from '../../../data_view_manager/hooks/use_full_data_view'; import { DataViewErrorComponent } from './data_view_error'; import { useEnableExperimental } from '../../hooks/use_experimental_features'; @@ -35,7 +35,7 @@ export const withDataView =

( ) => { const ComponentWithDataView = (props: OmitDataView

) => { const experimentalDataView = useFullDataView({ - dataViewPickerScope: DataViewPickerScopeName.timeline, + dataViewManagerScope: DataViewManagerScopeName.timeline, }); let dataView = useGetScopedSourcererDataView({ diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts index 0b98068787725..b73a69ab2bfa3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/timeline/use_investigate_in_timeline.ts @@ -8,7 +8,7 @@ import { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import type { Filter, Query } from '@kbn/es-query'; -import { useSelectDataView } from '../../../data_view_picker/hooks/use_select_data_view'; +import { useSelectDataView } from '../../../data_view_manager/hooks/use_select_data_view'; import { useCreateTimeline } from '../../../timelines/hooks/use_create_timeline'; import { updateProviders, setFilters, applyKqlFilterQuery } from '../../../timelines/store/actions'; import { SourcererScopeName } from '../../../sourcerer/store/model'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/solutions/security/plugins/security_solution/public/common/mock/global_state.ts index 1c8c4d13beded..2adf5602ecae2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/mock/global_state.ts @@ -48,7 +48,7 @@ import { initialGroupingState } from '../store/grouping/reducer'; import type { SourcererState } from '../../sourcerer/store'; import { EMPTY_RESOLVER } from '../../resolver/store/helpers'; import { getMockDiscoverInTimelineState } from './mock_discover_state'; -import { mockDataViewPickerState } from '../../data_view_picker/redux/mock'; +import { mockDataViewPickerState } from '../../data_view_manager/redux/mock'; const mockFieldMap: DataViewSpec['fields'] = Object.fromEntries( mockIndexFields.map((field) => [field.name, field]) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/store/reducer.ts b/x-pack/solutions/security/plugins/security_solution/public/common/store/reducer.ts index 189cb1cea2a35..544b0ef0eb1db 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/store/reducer.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/store/reducer.ts @@ -38,9 +38,9 @@ import type { AnalyzerState } from '../../resolver/types'; import type { NotesState } from '../../notes/store/notes.slice'; import { notesReducer } from '../../notes/store/notes.slice'; import { - dataViewPickerReducer, - initialDataViewPickerState, -} from '../../data_view_picker/redux/reducer'; + dataViewManagerReducer as dataViewManagerReducer, + initialDataViewManagerState, +} from '../../data_view_manager/redux/reducer'; enableMapSet(); @@ -136,7 +136,7 @@ export const createInitialState = ( savedSearch: undefined, }, notes: notesState, - dataViewPicker: initialDataViewPickerState.dataViewPicker, + dataViewManager: initialDataViewManagerState.dataViewManager, }; return preloadedState; @@ -160,5 +160,5 @@ export const createReducer: ( discover: securitySolutionDiscoverReducer, ...pluginsReducer, notes: notesReducer, - dataViewPicker: dataViewPickerReducer, + dataViewManager: dataViewManagerReducer, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/store/types.ts b/x-pack/solutions/security/plugins/security_solution/public/common/store/types.ts index 960d4935fca01..3ea04af819a4f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/store/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/store/types.ts @@ -26,7 +26,7 @@ import type { GroupState } from './grouping/types'; import type { SecuritySolutionDiscoverState } from './discover/model'; import type { AnalyzerState } from '../../resolver/types'; import type { NotesState } from '../../notes/store/notes.slice'; -import type { RootState as DataViewPickerState } from '../../data_view_picker/redux/reducer'; +import type { RootState as DataViewPickerState } from '../../data_view_manager/redux/reducer'; export type State = HostsPluginState & UsersPluginState & diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.test.tsx similarity index 92% rename from x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.test.tsx rename to x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.test.tsx index b56896c6a430b..04c4eec5acdcd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_picker/components/data_view_picker/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { DataViewPicker } from '.'; import { useDataView } from '../../hooks/use_data_view'; -import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewPickerScopeName } from '../../constants'; +import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewManagerScopeName } from '../../constants'; import { shared } from '../../redux/slices'; import { useDispatch } from 'react-redux'; import { useKibana } from '../../../common/lib/kibana'; @@ -40,7 +40,7 @@ jest.mock('../../../common/lib/kibana', () => ({ jest.mock('@kbn/unified-search-plugin/public', () => ({ ...jest.requireActual('@kbn/unified-search-plugin/public'), DataViewPicker: jest.fn((props) => ( -

+