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 a84f761aa8de8..aba4d7210f478 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 @@ -86,6 +86,7 @@ export const useInitDataViewManager = () => { uiSettings: services.uiSettings, application: services.application, spaces: services.spaces, + storage: services.storage, }); dispatch(addListener(dataViewsLoadingListener)); @@ -101,6 +102,7 @@ export const useInitDataViewManager = () => { createDataViewSelectedListener({ scope, dataViews: services.dataViews, + storage: services.storage, }) ); @@ -123,6 +125,7 @@ export const useInitDataViewManager = () => { services.dataViews, services.http, services.spaces, + services.storage, services.uiSettings, ]); 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 f476d44fde07a..03c86aa5fa7a4 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 @@ -12,6 +12,7 @@ import type { AnyAction, Dispatch, ListenerEffectAPI } from '@reduxjs/toolkit'; import type { RootState } from '../reducer'; import { DataViewManagerScopeName, DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../../constants'; import { DEFAULT_ALERT_DATA_VIEW_ID } from '../../../../common/constants'; +import type { Storage } from '@kbn/kibana-utils-plugin/public'; const mockDataViewsService = { getDataViewLazy: jest.fn(), @@ -79,6 +80,12 @@ const mockedState: RootState = { const mockDispatch = jest.fn(); const mockGetState = jest.fn(() => mockedState); +const mockStorage = { + set: jest.fn(), + get: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), +} as unknown as Storage; const mockListenerApi = { dispatch: mockDispatch, @@ -95,6 +102,7 @@ describe('createDataViewSelectedListener', () => { listener = createDataViewSelectedListener({ dataViews: mockDataViewsService, scope: DataViewManagerScopeName.default, + storage: mockStorage, }); }); @@ -165,4 +173,51 @@ describe('createDataViewSelectedListener', () => { }) ); }); + + describe('analyzer scope storage', () => { + it('should store data view ID in storage when scope is analyzer', async () => { + const analyzerListener = createDataViewSelectedListener({ + dataViews: mockDataViewsService, + scope: DataViewManagerScopeName.analyzer, + storage: mockStorage, + }); + + await analyzerListener.effect( + selectDataViewAsync({ id: 'adhoc_test-*', scope: DataViewManagerScopeName.analyzer }), + mockListenerApi + ); + + expect(mockStorage.set).toHaveBeenCalledWith( + 'securitySolution.dataViewManager.selectedDataView.analyzer', + 'adhoc_test-*' + ); + }); + + it('should not store data view ID in storage when scope is not analyzer', async () => { + await listener.effect( + selectDataViewAsync({ id: 'adhoc_test-*', scope: DataViewManagerScopeName.default }), + mockListenerApi + ); + + expect(mockStorage.set).not.toHaveBeenCalled(); + }); + + it('should store resolved data view ID from cached view when scope is analyzer', async () => { + const analyzerListener = createDataViewSelectedListener({ + dataViews: mockDataViewsService, + scope: DataViewManagerScopeName.analyzer, + storage: mockStorage, + }); + + await analyzerListener.effect( + selectDataViewAsync({ id: 'persisted_test-*', scope: DataViewManagerScopeName.analyzer }), + mockListenerApi + ); + + expect(mockStorage.set).toHaveBeenCalledWith( + 'securitySolution.dataViewManager.selectedDataView.analyzer', + 'persisted_test-*' + ); + }); + }); }); 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 4eec339c6fca1..eb034e1c2770c 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 @@ -7,12 +7,13 @@ import type { DataView, DataViewLazy, DataViewsServicePublic } from '@kbn/data-views-plugin/public'; import type { AnyAction, Dispatch, ListenerEffectAPI } from '@reduxjs/toolkit'; +import type { Storage } from '@kbn/kibana-utils-plugin/public'; import { isEmpty } from 'lodash'; import type { RootState } from '../reducer'; import { scopes } from '../reducer'; import { selectDataViewAsync } from '../actions'; import { sharedDataViewManagerSlice } from '../slices'; -import type { DataViewManagerScopeName } from '../../constants'; +import { DataViewManagerScopeName } from '../../constants'; /** * Creates a Redux listener for handling data view selection logic in the data view manager. @@ -27,12 +28,13 @@ import type { DataViewManagerScopeName } from '../../constants'; * 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. + * @param dependencies - The dependencies required for the listener, including the scope, DataViews service, and storage. * @returns An object with the action creator and effect for Redux middleware. */ export const createDataViewSelectedListener = (dependencies: { scope: DataViewManagerScopeName; dataViews: DataViewsServicePublic; + storage: Storage; }) => { return { actionCreator: selectDataViewAsync, @@ -130,6 +132,12 @@ export const createDataViewSelectedListener = (dependencies: { } listenerApi.dispatch(currentScopeActions.setSelectedDataView(resolvedIdToUse)); + if (action.payload.scope === DataViewManagerScopeName.analyzer) { + dependencies.storage.set( + `securitySolution.dataViewManager.selectedDataView.${action.payload.scope}`, + resolvedIdToUse + ); + } } else if (dataViewByIdError || adhocDataViewCreationError) { const err = dataViewByIdError || adhocDataViewCreationError; listenerApi.dispatch( 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 0923fc7c777a4..ca8e45375e686 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 @@ -17,6 +17,7 @@ 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'; +import type { Storage } from '@kbn/kibana-utils-plugin/public'; jest.mock('../../utils/create_default_data_view', () => ({ createDefaultDataView: jest.fn(), @@ -73,6 +74,12 @@ describe('createInitListener', () => { application, uiSettings, spaces, + storage: { + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), + } as unknown as Storage, }); }); 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 b8d9636e41033..d189c75d1b09e 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 @@ -9,6 +9,7 @@ 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 { Storage } from '@kbn/kibana-utils-plugin/public'; import type { RootState } from '../reducer'; import { sharedDataViewManagerSlice } from '../slices'; import { DataViewManagerScopeName } from '../../constants'; @@ -39,6 +40,7 @@ export const createInitListener = (dependencies: { uiSettings: CoreStart['uiSettings']; dataViews: DataViewsServicePublic; spaces: SpacesPluginStart; + storage: Storage; }) => { return { actionCreator: sharedDataViewManagerSlice.actions.init, @@ -111,6 +113,30 @@ export const createInitListener = (dependencies: { scope, }) ); + + const storedDataViewId = dependencies.storage.get( + `securitySolution.dataViewManager.selectedDataView.${scope}` + ) as string | null | undefined; + const state = listenerApi.getState(); + if ( + storedDataViewId && + !state.dataViewManager[scope].dataViewId && + typeof storedDataViewId === 'string' + ) { + return listenerApi.dispatch( + selectDataViewAsync({ + id: storedDataViewId, + scope, + }) + ); + } else { + return 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)