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..4a50e197fab36 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 @@ -25,11 +25,6 @@ import { TimelineTypeEnum } from '../../../common/api/timeline'; import { TimelineId } from '../../../common/types'; import { initialGroupingState } from './grouping/reducer'; import type { GroupState } from './grouping/types'; -import { - DEFAULT_DATA_VIEW_ID, - DEFAULT_INDEX_KEY, - DETECTION_ENGINE_INDEX_URL, -} from '../../../common/constants'; import { telemetryMiddleware } from '../lib/telemetry'; import * as timelineActions from '../../timelines/store/actions'; import type { TimelineModel } from '../../timelines/store/model'; @@ -39,15 +34,9 @@ import type { AppAction } from './actions'; import type { Immutable } from '../../../common/endpoint/types'; import type { State } from './types'; import type { TimelineState } from '../../timelines/store/types'; -import type { - KibanaDataView, - SourcererModel, - SourcererDataView, -} from '../../sourcerer/store/model'; -import { initDataView } from '../../sourcerer/store/model'; +import type { SourcererDataView } from '../../sourcerer/store/model'; import type { StartedSubPlugins, StartPlugins } from '../../types'; import type { ExperimentalFeatures } from '../../../common/experimental_features'; -import { createSourcererDataView } from '../../sourcerer/containers/create_sourcerer_data_view'; import type { AnalyzerState } from '../../resolver/types'; import { resolverMiddlewareFactory } from '../../resolver/store/middleware'; import { dataAccessLayerFactory } from '../../resolver/data_access_layer/factory'; @@ -55,7 +44,7 @@ import { sourcererActions } from '../../sourcerer/store'; import { createMiddlewares } from './middlewares'; import { addNewTimeline } from '../../timelines/store/helpers'; import { initialNotesState } from '../../notes/store/notes.slice'; -import { hasAccessToSecuritySolution } from '../../helpers_access'; +import { createDefaultDataView } from '../../data_view_manager/utils/create_default_data_view'; let store: Store | null = null; @@ -66,46 +55,15 @@ export const createStoreFactory = async ( storage: Storage, enableExperimental: ExperimentalFeatures ): Promise> => { - let signal: { name: string | null; index_mapping_outdated: null | boolean } = { - name: null, - index_mapping_outdated: null, - }; - try { - if (hasAccessToSecuritySolution(coreStart.application.capabilities)) { - signal = await coreStart.http.fetch(DETECTION_ENGINE_INDEX_URL, { - version: '2023-10-31', - method: 'GET', - }); - } - } catch { - signal = { name: null, index_mapping_outdated: null }; - } - - const configPatternList = coreStart.uiSettings.get(DEFAULT_INDEX_KEY); - let defaultDataView: SourcererModel['defaultDataView']; - let kibanaDataViews: SourcererModel['kibanaDataViews']; - try { - // check for/generate default Security Solution Kibana data view - const sourcererDataViews = await createSourcererDataView({ - body: { - patternList: [...configPatternList, ...(signal.name != null ? [signal.name] : [])], - }, - dataViewService: startPlugins.data.dataViews, - dataViewId: `${DEFAULT_DATA_VIEW_ID}-${(await startPlugins.spaces?.getActiveSpace())?.id}`, - }); - - if (sourcererDataViews === undefined) { - throw new Error(''); - } - defaultDataView = { ...initDataView, ...sourcererDataViews.defaultDataView }; - kibanaDataViews = sourcererDataViews.kibanaDataViews.map((dataView: KibanaDataView) => ({ - ...initDataView, - ...dataView, - })); - } catch (error) { - defaultDataView = { ...initDataView, error }; - kibanaDataViews = []; - } + const { kibanaDataViews, defaultDataView, signal } = await createDefaultDataView({ + application: coreStart.application, + http: coreStart.http, + dataViewService: startPlugins.data.dataViews, + uiSettings: coreStart.uiSettings, + spaces: startPlugins.spaces, + // TODO: (new data view picker) remove this in cleanup phase https://github.com/elastic/security-team/issues/12665 + skip: enableExperimental.newDataViewPickerEnabled, + }); const timelineInitialState = { timeline: { diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx index 313b161569028..1cb229926a3c9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx @@ -18,7 +18,7 @@ import { useDataViewSpec } from '../../hooks/use_data_view_spec'; import { sharedStateSelector } from '../../redux/selectors'; import { sharedDataViewManagerSlice } from '../../redux/slices'; import { useSelectDataView } from '../../hooks/use_select_data_view'; -import { DataViewManagerScopeName, DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../../constants'; +import { DataViewManagerScopeName } from '../../constants'; import { useManagedDataViews } from '../../hooks/use_managed_data_views'; import { useSavedDataViews } from '../../hooks/use_saved_data_views'; import { DEFAULT_SECURITY_DATA_VIEW, LOADING } from './translations'; @@ -57,6 +57,15 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie const { dataViewSpec, status } = useDataViewSpec(scope); + const { adhocDataViews: adhocDataViewSpecs, defaultDataViewId } = + useSelector(sharedStateSelector); + const adhocDataViews = useMemo(() => { + return adhocDataViewSpecs.map((spec) => new DataView({ spec, fieldFormats })); + }, [adhocDataViewSpecs, fieldFormats]); + + const managedDataViews = useManagedDataViews(); + const savedDataViews = useSavedDataViews(); + const isDefaultSourcerer = scope === DataViewManagerScopeName.default; const updateUrlParam = useUpdateUrlParam(URL_PARAM_KEY.sourcerer); @@ -145,7 +154,7 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie return { label: LOADING }; } - if (dataViewSpec.id === DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID) { + if (dataViewSpec.id === defaultDataViewId) { return { label: DEFAULT_SECURITY_DATA_VIEW, }; @@ -154,22 +163,13 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie return { label: dataViewSpec?.name || dataViewSpec?.id || 'Data view', }; - }, [dataViewSpec.id, dataViewSpec?.name, status]); - - const { adhocDataViews: adhocDataViewSpecs } = useSelector(sharedStateSelector); - - const adhocDataViews = useMemo(() => { - return adhocDataViewSpecs.map((spec) => new DataView({ spec, fieldFormats })); - }, [adhocDataViewSpecs, fieldFormats]); - - const managedDataViews = useManagedDataViews(); - const savedDataViews = useSavedDataViews(); + }, [dataViewSpec.id, dataViewSpec?.name, defaultDataViewId, status]); return (
({ - useEnableExperimental: () => ({ newDataViewPickerEnabled: true }), + useIsExperimentalFeatureEnabled: () => true, })); jest.mock('react-redux', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_init_data_view_manager.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_init_data_view_manager.ts index 67711aecbf730..8caa8238c8263 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_init_data_view_manager.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_init_data_view_manager.ts @@ -16,10 +16,11 @@ import type { RootState } from '../redux/reducer'; import { useKibana } from '../../common/lib/kibana'; import { createDataViewSelectedListener } from '../redux/listeners/data_view_selected'; import { createInitListener } from '../redux/listeners/init_listener'; -import { useEnableExperimental } from '../../common/hooks/use_experimental_features'; import { sharedDataViewManagerSlice } from '../redux/slices'; import { type SelectDataViewAsyncPayload } from '../redux/actions'; import { DataViewManagerScopeName } from '../constants'; +import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; +import { useUserInfo } from '../../detections/components/user_info'; type OriginalListener = Parameters[0]; @@ -40,15 +41,51 @@ const removeListener = (listener: Listener) => export const useInitDataViewManager = () => { const dispatch = useDispatch(); const services = useKibana().services; - const { newDataViewPickerEnabled } = useEnableExperimental(); + const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled'); + + const { + loading: loadingSignalIndex, + signalIndexName, + signalIndexMappingOutdated, + } = useUserInfo(); + + const onSignalIndexUpdated = useCallback(() => { + if (!loadingSignalIndex && signalIndexName != null) { + dispatch( + sharedDataViewManagerSlice.actions.setSignalIndex({ + name: signalIndexName, + isOutdated: !!signalIndexMappingOutdated, + }) + ); + } + }, [dispatch, loadingSignalIndex, signalIndexMappingOutdated, signalIndexName]); + + useEffect(() => { + // TODO: (new data view picker) remove this in cleanup phase https://github.com/elastic/security-team/issues/12665 + // Also, make sure it works exactly as x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/use_init_sourcerer.tsx + if (!newDataViewPickerEnabled) { + return; + } + + onSignalIndexUpdated(); + // because we only want onSignalIndexUpdated to run when signalIndexName updates, + // but we want to know about the updates from the dependencies of onSignalIndexUpdated + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [signalIndexName]); useEffect(() => { + // TODO: (new data view picker) remove this in cleanup phase https://github.com/elastic/security-team/issues/12665 if (!newDataViewPickerEnabled) { return; } + // NOTE: init listener contains logic that preloads default security solution data view const dataViewsLoadingListener = createInitListener({ dataViews: services.dataViews, + http: services.http, + uiSettings: services.uiSettings, + application: services.application, + spaces: services.spaces, }); dispatch(addListener(dataViewsLoadingListener)); @@ -78,7 +115,15 @@ export const useInitDataViewManager = () => { dispatch(removeListener(dataViewSelectedListener)); }); }; - }, [dispatch, newDataViewPickerEnabled, services.dataViews]); + }, [ + dispatch, + newDataViewPickerEnabled, + services.application, + services.dataViews, + services.http, + services.spaces, + services.uiSettings, + ]); return useCallback( (initialSelection: SelectDataViewAsyncPayload[]) => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_managed_data_views.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_managed_data_views.test.ts index 814eed5bbd4b5..54220d36f84e0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_managed_data_views.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_managed_data_views.test.ts @@ -54,7 +54,10 @@ describe('useManagedDataViews', () => { ]; // Mock the Redux selector - (useSelector as jest.Mock).mockReturnValue({ dataViews: mockDataViews }); + (useSelector as jest.Mock).mockReturnValue({ + dataViews: mockDataViews, + defaultDataViewId: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, + }); // Render the hook const { result } = renderHook(() => useManagedDataViews()); @@ -85,7 +88,10 @@ describe('useManagedDataViews', () => { { id: 'some-id', title: 'Some Data View' }, { id: 'another-id', title: 'Another Data View' }, ]; - (useSelector as jest.Mock).mockReturnValue({ dataViews: mockDataViews }); + (useSelector as jest.Mock).mockReturnValue({ + dataViews: mockDataViews, + defaultDataViewId: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, + }); const { result } = renderHook(() => useManagedDataViews()); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_managed_data_views.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_managed_data_views.ts index e742f8ba82323..f50cd5857ece9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_managed_data_views.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_managed_data_views.ts @@ -10,19 +10,18 @@ import { useSelector } from 'react-redux'; import { useMemo } from 'react'; import { useKibana } from '../../common/lib/kibana'; import { sharedStateSelector } from '../redux/selectors'; -import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../constants'; export const useManagedDataViews = () => { - const { dataViews } = useSelector(sharedStateSelector); + const { dataViews, defaultDataViewId } = useSelector(sharedStateSelector); const { services: { fieldFormats }, } = useKibana(); return useMemo(() => { const managed = dataViews - .filter((dv) => dv.id === DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID) + .filter((dv) => dv.id === defaultDataViewId) .map((spec) => new DataView({ spec, fieldFormats })); return managed; - }, [dataViews, fieldFormats]); + }, [dataViews, defaultDataViewId, fieldFormats]); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.test.ts index a2a0914bb9fae..e67efd6963ba9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.test.ts @@ -40,7 +40,10 @@ describe('useSavedDataViews', () => { ]; // Mock the useSelector to return our test data - (useSelector as jest.Mock).mockReturnValue({ dataViews: mockDataViews }); + (useSelector as jest.Mock).mockReturnValue({ + dataViews: mockDataViews, + defaultDataViewId: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, + }); // Render the hook const { result } = renderHook(() => useSavedDataViews()); @@ -68,7 +71,10 @@ describe('useSavedDataViews', () => { it('should handle empty data views array', () => { // Mock the useSelector to return an empty array - (useSelector as jest.Mock).mockReturnValue({ dataViews: [] }); + (useSelector as jest.Mock).mockReturnValue({ + dataViews: [], + defaultDataViewId: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, + }); // Render the hook const { result } = renderHook(() => useSavedDataViews()); @@ -93,7 +99,10 @@ describe('useSavedDataViews', () => { ]; // Mock the useSelector - (useSelector as jest.Mock).mockReturnValue({ dataViews: mockDataViews }); + (useSelector as jest.Mock).mockReturnValue({ + dataViews: mockDataViews, + defaultDataViewId: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, + }); // Render the hook const { result } = renderHook(() => useSavedDataViews()); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.ts index 7b2ca3abfca63..ba048880ccc61 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.ts @@ -9,14 +9,13 @@ import { type DataViewListItem } from '@kbn/data-views-plugin/public'; import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { sharedStateSelector } from '../redux/selectors'; -import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../constants'; export const useSavedDataViews = () => { - const { dataViews } = useSelector(sharedStateSelector); + const { dataViews, defaultDataViewId } = useSelector(sharedStateSelector); return useMemo(() => { const savedViewsAsListItems: DataViewListItem[] = dataViews - .filter((dv) => dv.id !== DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID) + .filter((dv) => dv.id !== defaultDataViewId) .map((spec) => ({ id: spec.id ?? '', title: spec.title ?? '', @@ -24,5 +23,5 @@ export const useSavedDataViews = () => { })); return savedViewsAsListItems; - }, [dataViews]); + }, [dataViews, defaultDataViewId]); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_signal_index_name.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_signal_index_name.ts new file mode 100644 index 0000000000000..2c5b344251384 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_signal_index_name.ts @@ -0,0 +1,11 @@ +/* + * 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 { signalIndexNameSelector } from '../redux/selectors'; + +export const useSignalIndexName = () => useSelector(signalIndexNameSelector); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/listeners/data_view_selected.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/listeners/data_view_selected.test.ts index 58c8085eb3127..f1904f36ffdd2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/listeners/data_view_selected.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/listeners/data_view_selected.test.ts @@ -10,7 +10,7 @@ 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 { DataViewManagerScopeName } from '../../constants'; +import { DataViewManagerScopeName, DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../../constants'; const mockDataViewsService = { getDataViewLazy: jest.fn(), @@ -65,6 +65,8 @@ const mockedState: RootState = { }, ], status: 'pristine', + signalIndex: { name: '', isOutdated: false }, + defaultDataViewId: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, }, }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/listeners/data_view_selected.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/listeners/data_view_selected.ts index 4a208f37ddb3a..e965a98f37a6c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/listeners/data_view_selected.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/listeners/data_view_selected.ts @@ -14,6 +14,22 @@ import { selectDataViewAsync } from '../actions'; import { sharedDataViewManagerSlice } from '../slices'; import type { DataViewManagerScopeName } from '../../constants'; +/** + * Creates a Redux listener for handling data view selection logic in the data view manager. + * + * This listener responds to the `selectDataViewAsync` action for a specific scope. It attempts to resolve + * the selected data view by: + * 1. Checking for a cached data view (either ad-hoc or persisted) in the Redux state. + * 2. If not found, attempting to fetch a lazy data view by ID from the DataViews service. + * 3. If still not found, creating a new ad-hoc data view using fallback patterns. + * + * The listener ensures that only one effect runs per scope at a time to prevent race conditions. + * If a data view is successfully resolved, it dispatches an action to set it as selected for the current scope. + * If an error occurs during fetching or creation, it dispatches an error action for the current scope. + * + * @param dependencies - The dependencies required for the listener, including the scope and DataViews service. + * @returns An object with the action creator and effect for Redux middleware. + */ export const createDataViewSelectedListener = (dependencies: { scope: DataViewManagerScopeName; dataViews: DataViewsServicePublic; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/listeners/init_listener.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/listeners/init_listener.test.ts index 9ed3ea7394b08..03c4c6e95f297 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/listeners/init_listener.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/listeners/init_listener.test.ts @@ -13,6 +13,13 @@ import type { RootState } from '../reducer'; import { sharedDataViewManagerSlice } from '../slices'; import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewManagerScopeName } from '../../constants'; import { selectDataViewAsync } from '../actions'; +import type { CoreStart } from '@kbn/core/public'; +import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; +import { createDefaultDataView } from '../../utils/create_default_data_view'; + +jest.mock('../../utils/create_default_data_view', () => ({ + createDefaultDataView: jest.fn(), +})); const mockDataViewsService = { get: jest.fn(), @@ -24,8 +31,22 @@ const mockDataViewsService = { getAllDataViewLazy: jest.fn().mockReturnValue([]), } as unknown as DataViewsServicePublic; +const http = {} as unknown as CoreStart['http']; +const application = {} as unknown as CoreStart['application']; +const uiSettings = {} as unknown as CoreStart['uiSettings']; +const spaces = {} as unknown as SpacesPluginStart; + const mockDispatch = jest.fn(); -const mockGetState = jest.fn(() => mockDataViewManagerState); +const mockGetState = jest.fn(() => { + const state = structuredClone(mockDataViewManagerState); + + state.dataViewManager.default.dataViewId = null; + state.dataViewManager.detections = structuredClone(state.dataViewManager.default); + state.dataViewManager.timeline = structuredClone(state.dataViewManager.default); + state.dataViewManager.analyzer = structuredClone(state.dataViewManager.default); + + return state; +}); const mockListenerApi = { dispatch: mockDispatch, @@ -37,17 +58,36 @@ describe('createInitListener', () => { beforeEach(() => { jest.clearAllMocks(); - listener = createInitListener({ dataViews: mockDataViewsService }); + listener = createInitListener({ + dataViews: mockDataViewsService, + http, + application, + uiSettings, + spaces, + }); + + jest.mocked(createDefaultDataView).mockResolvedValue({ + defaultDataView: { id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID }, + kibanaDataViews: [], + } as unknown as Awaited>); }); it('should load the data views and dispatch further actions', async () => { await listener.effect(sharedDataViewManagerSlice.actions.init([]), mockListenerApi); + expect(jest.mocked(createDefaultDataView)).toHaveBeenCalled(); + expect(jest.mocked(mockDataViewsService.getAllDataViewLazy)).toHaveBeenCalled(); expect(jest.mocked(mockListenerApi.dispatch)).toBeCalledWith( sharedDataViewManagerSlice.actions.setDataViews([]) ); + expect(jest.mocked(mockListenerApi.dispatch)).toBeCalledWith( + sharedDataViewManagerSlice.actions.setDefaultDataViewId( + DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID + ) + ); + expect(jest.mocked(mockListenerApi.dispatch)).toBeCalledWith( selectDataViewAsync({ id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/listeners/init_listener.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/listeners/init_listener.ts index e19ad6e6f37c6..ef72b842b00d2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/listeners/init_listener.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/listeners/init_listener.ts @@ -7,12 +7,38 @@ import type { AnyAction, Dispatch, ListenerEffectAPI } from '@reduxjs/toolkit'; import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public'; +import type { CoreStart } from '@kbn/core/public'; +import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import type { RootState } from '../reducer'; import { sharedDataViewManagerSlice } from '../slices'; -import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewManagerScopeName } from '../../constants'; +import { DataViewManagerScopeName } from '../../constants'; import { selectDataViewAsync } from '../actions'; +import { createDefaultDataView } from '../../utils/create_default_data_view'; -export const createInitListener = (dependencies: { dataViews: DataViewsServicePublic }) => { +/** + * Creates a Redux listener for initializing the Data View Manager state. + * + * This listener is responsible for: + * - Creating and preloading the default security data view using the provided dependencies. + * - Fetching all available data views and dispatching them to the store for use in selectors. + * - Preloading the default data view for all defined scopes (detections, analyzer, timeline, default), + * but only for those scopes that have not already been initialized. + * - Handling any additional data view selections provided in the action payload (e.g., from URL storage). + * - Dispatching an error action if initialization fails. + * + * The listener ensures that race conditions are avoided by only initializing scopes that are not already set, + * and that state is not reset for slices that already have selections. + * + * @param dependencies - Core and plugin services required for data view creation and retrieval. + * @returns An object with the actionCreator and effect for Redux listener middleware. + */ +export const createInitListener = (dependencies: { + http: CoreStart['http']; + application: CoreStart['application']; + uiSettings: CoreStart['uiSettings']; + dataViews: DataViewsServicePublic; + spaces: SpacesPluginStart; +}) => { return { actionCreator: sharedDataViewManagerSlice.actions.init, effect: async ( @@ -20,28 +46,49 @@ export const createInitListener = (dependencies: { dataViews: DataViewsServicePu listenerApi: ListenerEffectAPI> ) => { try { + // Initialize default security data view first + // Note: this is subject to change, as we might want to add specific data view just for alerts + + const { defaultDataView } = await createDefaultDataView({ + dataViewService: dependencies.dataViews, + uiSettings: dependencies.uiSettings, + spaces: dependencies.spaces, + application: dependencies.application, + http: dependencies.http, + }); + // NOTE: This is later used in the data view manager drop-down selector const dataViews = await dependencies.dataViews.getAllDataViewLazy(); const dataViewSpecs = await Promise.all(dataViews.map((dataView) => dataView.toSpec())); listenerApi.dispatch(sharedDataViewManagerSlice.actions.setDataViews(dataViewSpecs)); + // NOTE: save default dataview id for the given space in the store. + // this is used to identify the default selection in pickers across Kibana Space + listenerApi.dispatch( + sharedDataViewManagerSlice.actions.setDefaultDataViewId(defaultDataView.id) + ); + // Preload the default data view for all the scopes // Immediate calls that would dispatch this call from other places will cancel this action, // preventing race conditions + // Whats more, portions of the state that already have selections applied to them will not be reset in the init listener. [ DataViewManagerScopeName.detections, DataViewManagerScopeName.analyzer, DataViewManagerScopeName.timeline, DataViewManagerScopeName.default, - ].forEach((scope) => { - listenerApi.dispatch( - selectDataViewAsync({ - id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, - scope, - }) - ); - }); + ] + // NOTE: only init default data view for slices that are not initialized yet + .filter((scope) => !listenerApi.getState().dataViewManager[scope].dataViewId) + .forEach((scope) => { + listenerApi.dispatch( + selectDataViewAsync({ + id: defaultDataView.id, + scope, + }) + ); + }); // NOTE: if there is a list of data views to preload other than default one (eg. coming in from the url storage) action.payload.forEach((defaultSelection) => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/selectors.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/selectors.ts index f5691bc26788e..17de68e5ce174 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/selectors.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/selectors.ts @@ -23,3 +23,15 @@ export const sharedStateSelector = createSelector( [(state: RootState) => state.dataViewManager], (dataViewManager) => dataViewManager.shared ); + +// NOTE: This will be subject to cleanup tasks https://github.com/elastic/security-team/issues/11959 +export const signalIndexNameSelector = createSelector( + [(state: RootState) => state.dataViewManager], + (dataViewManager) => dataViewManager.shared.signalIndex?.name ?? '' +); + +// NOTE: This will be subject to cleanup tasks https://github.com/elastic/security-team/issues/11959 +export const signalIndexOutdatedSelector = createSelector( + [(state: RootState) => state.dataViewManager], + (dataViewManager) => !!dataViewManager.shared.signalIndex?.isOutdated +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/slices.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/slices.ts index e71707cec7e50..e786487ed4358 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/slices.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/slices.ts @@ -10,7 +10,11 @@ import { createSlice } from '@reduxjs/toolkit'; import type { DataViewSpec, DataView } from '@kbn/data-views-plugin/common'; import type { DataViewManagerScopeName } from '../constants'; import { SLICE_PREFIX } from '../constants'; -import type { ScopedDataViewSelectionState, SharedDataViewSelectionState } from './types'; +import type { + ScopedDataViewSelectionState, + SharedDataViewSelectionState, + SignalIndexMetadata, +} from './types'; import { selectDataViewAsync, type SelectDataViewAsyncPayload } from './actions'; export const initialScopeState: ScopedDataViewSelectionState = { @@ -22,6 +26,8 @@ export const initialSharedState: SharedDataViewSelectionState = { dataViews: [], adhocDataViews: [], status: 'pristine', + signalIndex: null, + defaultDataViewId: null, }; export const sharedDataViewManagerSlice = createSlice({ @@ -32,6 +38,12 @@ export const sharedDataViewManagerSlice = createSlice({ state.dataViews = action.payload; state.status = 'ready'; }, + setSignalIndex: (state, action: PayloadAction) => { + state.signalIndex = action.payload; + }, + setDefaultDataViewId: (state, action: PayloadAction) => { + state.defaultDataViewId = action.payload; + }, addDataView: (state, action: PayloadAction) => { const dataViewSpec = action.payload.toSpec(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/types.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/types.ts index 662e64a594eb8..107dddd37c220 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/types.ts @@ -23,6 +23,13 @@ export interface SharedDataViewSelectionState { dataViews: DataViewSpec[]; adhocDataViews: DataViewSpec[]; status: 'pristine' | 'loading' | 'error' | 'ready'; + defaultDataViewId: string | null; + signalIndex: SignalIndexMetadata | null; +} + +export interface SignalIndexMetadata { + name: string; + isOutdated: boolean; } export { type DataViewSpec }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/utils/create_default_data_view.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/utils/create_default_data_view.test.ts new file mode 100644 index 0000000000000..66c18e343754d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/utils/create_default_data_view.test.ts @@ -0,0 +1,99 @@ +/* + * 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 CreateDefaultDataViewDependencies, + createDefaultDataView, +} from './create_default_data_view'; +import { initDataView } from '../../sourcerer/store/model'; +import * as helpersAccess from '../../helpers_access'; +import * as createSourcererDataViewModule from '../../sourcerer/containers/create_sourcerer_data_view'; +import { DEFAULT_DATA_VIEW_ID, DETECTION_ENGINE_INDEX_URL } from '../../../common/constants'; + +jest.mock('../../helpers_access'); +jest.mock('../../sourcerer/containers/create_sourcerer_data_view'); + +const mockUiSettings = { + get: jest.fn(), +}; + +const mockDataViewService = {}; + +const mockSpaces = { + getActiveSpace: jest.fn(), +}; + +const mockHttp = { + fetch: jest.fn(), +}; + +const mockApplication = { + capabilities: {}, +}; + +const defaultDeps = { + http: mockHttp, + application: mockApplication, + uiSettings: mockUiSettings, + dataViewService: mockDataViewService, + spaces: mockSpaces, +} as unknown as CreateDefaultDataViewDependencies; + +describe('createDefaultDataView', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUiSettings.get.mockReturnValue(['pattern-*']); + mockSpaces.getActiveSpace.mockResolvedValue({ id: 'space1' }); + (helpersAccess.hasAccessToSecuritySolution as jest.Mock).mockReturnValue(true); + mockHttp.fetch.mockResolvedValue({ name: 'signal-index', index_mapping_outdated: false }); + (createSourcererDataViewModule.createSourcererDataView as jest.Mock).mockResolvedValue({ + defaultDataView: { id: 'dv1', title: 'title1' }, + kibanaDataViews: [{ id: 'dv1', title: 'title1' }], + }); + }); + + it('returns default values if skip is true', async () => { + const result = await createDefaultDataView({ ...defaultDeps, skip: true }); + expect(result.kibanaDataViews).toEqual([]); + expect(result.defaultDataView).toEqual(initDataView); + expect(result.signal).toEqual({ name: null, index_mapping_outdated: null }); + }); + + it('fetches signal index and creates data views when user has access', async () => { + const result = await createDefaultDataView(defaultDeps); + expect(helpersAccess.hasAccessToSecuritySolution).toHaveBeenCalledWith( + mockApplication.capabilities + ); + expect(mockHttp.fetch).toHaveBeenCalledWith(DETECTION_ENGINE_INDEX_URL, expect.any(Object)); + expect(createSourcererDataViewModule.createSourcererDataView).toHaveBeenCalledWith( + expect.objectContaining({ + body: { patternList: ['pattern-*', 'signal-index'] }, + dataViewService: mockDataViewService, + dataViewId: `${DEFAULT_DATA_VIEW_ID}-space1`, + }) + ); + expect(result.defaultDataView).toMatchObject({ id: 'dv1', title: 'title1' }); + expect(result.kibanaDataViews[0]).toMatchObject({ id: 'dv1', title: 'title1' }); + expect(result.signal).toEqual({ name: 'signal-index', index_mapping_outdated: false }); + }); + + it('does not fetch signal index if user has no access', async () => { + (helpersAccess.hasAccessToSecuritySolution as jest.Mock).mockReturnValue(false); + const result = await createDefaultDataView(defaultDeps); + expect(mockHttp.fetch).not.toHaveBeenCalled(); + expect(result.signal).toEqual({ name: null, index_mapping_outdated: null }); + }); + + it('returns error in defaultDataView if an exception is thrown', async () => { + (createSourcererDataViewModule.createSourcererDataView as jest.Mock).mockImplementation(() => { + throw new Error('fail'); + }); + const result = await createDefaultDataView(defaultDeps); + expect(result.defaultDataView.error).toBeInstanceOf(Error); + expect(result.kibanaDataViews).toEqual([]); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/utils/create_default_data_view.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/utils/create_default_data_view.ts new file mode 100644 index 0000000000000..ddfa091bf53bb --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/utils/create_default_data_view.ts @@ -0,0 +1,86 @@ +/* + * 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 { CoreStart } from '@kbn/core/public'; +import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; +import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public/types'; +import type { KibanaDataView, SourcererModel } from '../../sourcerer/store/model'; +import { initDataView } from '../../sourcerer/store/model'; +import { createSourcererDataView } from '../../sourcerer/containers/create_sourcerer_data_view'; +import { + DEFAULT_DATA_VIEW_ID, + DEFAULT_INDEX_KEY, + DETECTION_ENGINE_INDEX_URL, +} from '../../../common/constants'; +import { hasAccessToSecuritySolution } from '../../helpers_access'; + +export interface CreateDefaultDataViewDependencies { + http: CoreStart['http']; + application: CoreStart['application']; + uiSettings: CoreStart['uiSettings']; + dataViewService: DataViewsServicePublic; + spaces: SpacesPluginStart; + skip?: boolean; +} + +export const createDefaultDataView = async ({ + uiSettings, + dataViewService, + spaces, + skip, + http, + application, +}: CreateDefaultDataViewDependencies) => { + const configPatternList = uiSettings.get(DEFAULT_INDEX_KEY); + let defaultDataView: SourcererModel['defaultDataView']; + let kibanaDataViews: SourcererModel['kibanaDataViews']; + + let signal: { name: string | null; index_mapping_outdated: null | boolean } = { + name: null, + index_mapping_outdated: null, + }; + + if (skip) { + return { + kibanaDataViews: [], + defaultDataView: { ...initDataView }, + signal, + }; + } + + try { + if (hasAccessToSecuritySolution(application.capabilities)) { + signal = await http.fetch(DETECTION_ENGINE_INDEX_URL, { + version: '2023-10-31', + method: 'GET', + }); + } + + // check for/generate default Security Solution Kibana data view + const sourcererDataView = await createSourcererDataView({ + body: { + patternList: [...configPatternList, ...(signal.name != null ? [signal.name] : [])], + }, + dataViewService, + dataViewId: `${DEFAULT_DATA_VIEW_ID}-${(await spaces?.getActiveSpace())?.id}`, + }); + + if (sourcererDataView === undefined) { + throw new Error(''); + } + defaultDataView = { ...initDataView, ...sourcererDataView.defaultDataView }; + kibanaDataViews = sourcererDataView.kibanaDataViews.map((dataView: KibanaDataView) => ({ + ...initDataView, + ...dataView, + })); + } catch (error) { + defaultDataView = { ...initDataView, error }; + kibanaDataViews = []; + } + + return { kibanaDataViews, defaultDataView, signal }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/detection_engine_filters/detection_engine_filters.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/detection_engine_filters/detection_engine_filters.tsx index bfa2f41d54de1..57837db80eced 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/detection_engine_filters/detection_engine_filters.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/detection_engine_filters/detection_engine_filters.tsx @@ -57,18 +57,32 @@ export const DetectionEngineFilters = ({ [urlStorage] ); - const dataViewSpec = useMemo( - () => - indexPattern - ? { - id: SECURITY_ALERT_DATA_VIEW.id, - name: SECURITY_ALERT_DATA_VIEW.name, - allowNoIndex: true, - title: indexPattern.title, - timeFieldName: '@timestamp', - } - : null, - [indexPattern] + const dataViewSpec = useMemo(() => { + // NOTE: index pattern should have a title or an id to be considered valid + // it is possible that the empty id or title are set temporarily during page load (compatibility reasons as we support adhoc data views now). + // to be removed after the cleanup work in scope of https://github.com/elastic/security-team/issues/11959 + // is done. + const isIndexPatternValid = indexPattern && (indexPattern.title || indexPattern.id); + + return isIndexPatternValid + ? { + id: SECURITY_ALERT_DATA_VIEW.id, + name: SECURITY_ALERT_DATA_VIEW.name, + allowNoIndex: true, + title: indexPattern.title, + timeFieldName: '@timestamp', + } + : null; + }, [indexPattern]); + + const services = useMemo( + () => ({ + http, + notifications, + dataViews, + storage: Storage, + }), + [dataViews, http, notifications] ); if (!spaceId || !dataViewSpec) { @@ -85,12 +99,7 @@ export const DetectionEngineFilters = ({ chainingSystem="HIERARCHICAL" defaultControls={DEFAULT_DETECTION_PAGE_FILTERS} dataViewSpec={dataViewSpec} - services={{ - http, - notifications, - dataViews, - storage: Storage, - }} + services={services} ControlGroupRenderer={ControlGroupRenderer} maxControls={4} {...props} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx index 8e72b2390b9ec..ed7544f14d9c1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx @@ -9,6 +9,9 @@ import { useEffect, useState } from 'react'; import { isSecurityAppError } from '@kbn/securitysolution-t-grid'; import { useSelector } from 'react-redux'; +import { signalIndexOutdatedSelector } from '../../../../data_view_manager/redux/selectors'; +import { useSignalIndexName } from '../../../../data_view_manager/hooks/use_signal_index_name'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { createSignalIndex, getSignalIndex } from './api'; import * as i18n from './translations'; @@ -28,8 +31,6 @@ export interface ReturnSignalIndex { /** * Hook for managing signal index - * - * */ export const useSignalIndex = (): ReturnSignalIndex => { const [loading, setLoading] = useState(true); @@ -42,13 +43,25 @@ export const useSignalIndex = (): ReturnSignalIndex => { const { addError } = useAppToasts(); const { hasIndexRead } = useAlertsPrivileges(); - const signalIndexMappingOutdated = useSelector((state: State) => { + const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled'); + + const oldSignalIndexMappingOutdated = useSelector((state: State) => { return sourcererSelectors.signalIndexMappingOutdated(state); }); + const experimentalSignalIndexMappingOutdated = useSelector(signalIndexOutdatedSelector); + + const signalIndexMappingOutdated = newDataViewPickerEnabled + ? experimentalSignalIndexMappingOutdated + : oldSignalIndexMappingOutdated; - const signalIndexName = useSelector((state: State) => { + const oldSignalIndexName = useSelector((state: State) => { return sourcererSelectors.signalIndexName(state); }); + const experimentalSignalIndexName = useSignalIndexName(); + + const signalIndexName = newDataViewPickerEnabled + ? experimentalSignalIndexName + : oldSignalIndexName; useEffect(() => { let isSubscribed = true; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/detection_engine.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/detection_engine.test.tsx index 79ea0b2974b43..f5b67bb90e8f5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/detection_engine.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alerts/detection_engine.test.tsx @@ -199,7 +199,7 @@ describe('DetectionEnginePageComponent', () => { browserFields: mockBrowserFields, sourcererDataView: { fields: {}, - title: '', + title: 'mock-*', }, }); jest diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/use_init_sourcerer.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/use_init_sourcerer.tsx index 5903d8a140d6f..6117794852704 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/use_init_sourcerer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/use_init_sourcerer.tsx @@ -22,10 +22,22 @@ import type { State } from '../../common/store/types'; import { useKibana } from '../../common/lib/kibana'; import { useSourcererDataView } from '.'; import { useSyncSourcererUrlState } from '../../data_view_manager/hooks/use_sync_url_state'; +import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; + +const defaultInitResult = { browserFields: {} }; export const useInitSourcerer = ( scopeId: SourcererScopeName.default | SourcererScopeName.detections = SourcererScopeName.default ) => { + const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled'); + + /* eslint-disable react-hooks/rules-of-hooks */ + // NOTE: skipping the entire hook on purpose when the new picker is enabled + // will be removed as part of the cleanup in https://github.com/elastic/security-team/issues/11959 + if (newDataViewPickerEnabled) { + return defaultInitResult; + } + const dispatch = useDispatch(); const { data: { dataViews }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/use_signal_helpers.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/use_signal_helpers.tsx index af8c05d22b460..83f6b07c967cf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/use_signal_helpers.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/use_signal_helpers.tsx @@ -11,10 +11,13 @@ import { useDispatch, useSelector } from 'react-redux'; import { sourcererSelectors, sourcererActions } from '../store'; import { useSourcererDataView } from '.'; import { SourcererScopeName } from '../store/model'; -import { useDataView } from '../../common/containers/source/use_data_view'; +import { useDataView as useOldDataView } from '../../common/containers/source/use_data_view'; import { useAppToasts } from '../../common/hooks/use_app_toasts'; import { useKibana } from '../../common/lib/kibana'; import { createSourcererDataView } from './create_sourcerer_data_view'; +import { useDataView } from '../../data_view_manager/hooks/use_data_view'; +import { useSignalIndexName } from '../../data_view_manager/hooks/use_signal_index_name'; +import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; export const useSignalHelpers = (): { /* when defined, signal index has been initiated but does not exist */ @@ -22,8 +25,16 @@ export const useSignalHelpers = (): { /* when false, signal index has been initiated */ signalIndexNeedsInit: boolean; } => { - const { indicesExist, dataViewId } = useSourcererDataView(SourcererScopeName.detections); - const { indexFieldsSearch } = useDataView(); + const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled'); + + const { indicesExist, dataViewId: oldDataViewId } = useSourcererDataView( + SourcererScopeName.detections + ); + const { dataView: detectionsDataView } = useDataView(SourcererScopeName.detections); + + const dataViewId = newDataViewPickerEnabled ? oldDataViewId : detectionsDataView?.id ?? null; + + const { indexFieldsSearch } = useOldDataView(); const dispatch = useDispatch(); const { addError } = useAppToasts(); const abortCtrl = useRef(new AbortController()); @@ -32,10 +43,24 @@ export const useSignalHelpers = (): { } = useKibana().services; const signalIndexNameSourcerer = useSelector(sourcererSelectors.signalIndexName); - const defaultDataView = useSelector(sourcererSelectors.defaultDataView); + + const experimentalSignalIndexName = useSignalIndexName(); + + const oldDefaultDataView = useSelector(sourcererSelectors.defaultDataView); + + const signalIndexName = newDataViewPickerEnabled + ? experimentalSignalIndexName + : signalIndexNameSourcerer; + + const { dataView: experimentalDefaultDataView } = useDataView(SourcererScopeName.default); + + const defaultDataViewPattern = newDataViewPickerEnabled + ? experimentalDefaultDataView?.getIndexPattern() ?? '' + : oldDefaultDataView.title; + const signalIndexNeedsInit = useMemo( - () => !defaultDataView.title.includes(`${signalIndexNameSourcerer}`), - [defaultDataView.title, signalIndexNameSourcerer] + () => !defaultDataViewPattern.includes(`${signalIndexName}`), + [defaultDataViewPattern, signalIndexName] ); const shouldWePollForIndex = useMemo( () => !indicesExist && !signalIndexNeedsInit, @@ -47,15 +72,15 @@ export const useSignalHelpers = (): { abortCtrl.current = new AbortController(); try { const sourcererDataView = await createSourcererDataView({ - body: { patternList: defaultDataView.title.split(',') }, + body: { patternList: defaultDataViewPattern.split(',') }, signal: abortCtrl.current.signal, dataViewId, dataViewService: dataViews, }); if ( - signalIndexNameSourcerer !== null && - sourcererDataView?.defaultDataView.patternList.includes(signalIndexNameSourcerer) + signalIndexName !== null && + sourcererDataView?.defaultDataView.patternList.includes(signalIndexName) ) { // first time signals is defined and validated in the sourcerer // redo indexFieldsSearch @@ -78,7 +103,7 @@ export const useSignalHelpers = (): { } }; - if (signalIndexNameSourcerer !== null) { + if (signalIndexName !== null) { abortCtrl.current.abort(); asyncSearch(); } @@ -86,10 +111,10 @@ export const useSignalHelpers = (): { addError, dataViewId, dataViews, - defaultDataView.title, + defaultDataViewPattern, dispatch, indexFieldsSearch, - signalIndexNameSourcerer, + signalIndexName, ]); return {