diff --git a/CHANGELOG.md b/CHANGELOG.md index 1626f17e776e..cb4fd806d9e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multi DataSource] Test the connection to an external data source when creating or updating ([#2973](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2973)) - [Doc] Add current plugin persistence implementation readme ([#3081](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3081)) - [Table Visualization] Refactor table visualization using React and DataGrid component ([#2863](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2863)) +- [Vis Builder] Add redux store persistence ([#3088](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3088)) ### 🐛 Bug Fixes diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 27ea95100000..8beb241f8768 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -85,6 +85,12 @@ const createStartContract = (): Start => { }, }), get: jest.fn().mockReturnValue(Promise.resolve({})), + getDefault: jest.fn().mockReturnValue( + Promise.resolve({ + name: 'Default name', + id: 'id', + }) + ), clearCache: jest.fn(), } as unknown) as IndexPatternsContract, }; diff --git a/src/plugins/vis_builder/public/application/utils/mocks.ts b/src/plugins/vis_builder/public/application/utils/mocks.ts index df0687efaa6b..6feb98aaa930 100644 --- a/src/plugins/vis_builder/public/application/utils/mocks.ts +++ b/src/plugins/vis_builder/public/application/utils/mocks.ts @@ -9,6 +9,7 @@ import { dataPluginMock } from '../../../../data/public/mocks'; import { embeddablePluginMock } from '../../../../embeddable/public/mocks'; import { expressionsPluginMock } from '../../../../expressions/public/mocks'; import { navigationPluginMock } from '../../../../navigation/public/mocks'; +import { createOsdUrlStateStorage } from '../../../../opensearch_dashboards_utils/public'; import { VisBuilderServices } from '../../types'; export const createVisBuilderServicesMock = () => { @@ -16,10 +17,11 @@ export const createVisBuilderServicesMock = () => { const toastNotifications = coreStartMock.notifications.toasts; const applicationMock = coreStartMock.application; const i18nContextMock = coreStartMock.i18n.Context; - const indexPatternMock = dataPluginMock.createStartContract().indexPatterns; + const indexPatternMock = dataPluginMock.createStartContract(); const embeddableMock = embeddablePluginMock.createStartContract(); const navigationMock = navigationPluginMock.createStartContract(); const expressionMock = expressionsPluginMock.createStartContract(); + const osdUrlStateStorageMock = createOsdUrlStateStorage({ useHash: false }); const visBuilderServicesMock = { ...coreStartMock, @@ -39,6 +41,21 @@ export const createVisBuilderServicesMock = () => { data: indexPatternMock, embeddable: embeddableMock, scopedHistory: (scopedHistoryMock.create() as unknown) as ScopedHistory, + osdUrlStateStorage: osdUrlStateStorageMock, + types: { + all: () => [ + { + name: 'viz', + ui: { + containerConfig: { + style: { + defaults: 'style default states', + }, + }, + }, + }, + ], + }, }; return (visBuilderServicesMock as unknown) as jest.Mocked; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.test.tsx b/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.test.tsx new file mode 100644 index 000000000000..ea3db25fbea0 --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VisBuilderServices } from '../../../types'; +import { createVisBuilderServicesMock } from '../mocks'; +import { getPreloadedState } from './preload'; +import { loadReduxState, saveReduxState } from './redux_persistence'; + +describe('test redux state persistence', () => { + let mockServices: jest.Mocked; + let reduxStateParams: any; + + beforeEach(() => { + mockServices = createVisBuilderServicesMock(); + reduxStateParams = { + style: 'style', + visualization: 'visualization', + metadata: 'metadata', + }; + }); + + test('test load redux state when url is empty', async () => { + const defaultStates = { + style: 'style default states', + visualization: { + searchField: '', + activeVisualization: { name: 'viz', aggConfigParams: [] }, + indexPattern: 'id', + }, + metadata: { + editor: { validity: {}, state: 'loading' }, + originatingApp: undefined, + }, + }; + + const returnStates = await loadReduxState(mockServices); + expect(returnStates).toStrictEqual(defaultStates); + }); + + test('test load redux state', async () => { + mockServices.osdUrlStateStorage.set('_a', reduxStateParams, { replace: true }); + const returnStates = await loadReduxState(mockServices); + expect(returnStates).toStrictEqual(reduxStateParams); + }); + + test('test save redux state', () => { + saveReduxState(reduxStateParams, mockServices); + const urlStates = mockServices.osdUrlStateStorage.get('_a'); + expect(urlStates).toStrictEqual(reduxStateParams); + }); +}); diff --git a/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.ts b/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.ts new file mode 100644 index 000000000000..295d3e2b4478 --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.ts @@ -0,0 +1,38 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VisBuilderServices } from '../../../types'; +import { getPreloadedState } from './preload'; +import { RootState } from './store'; + +export const loadReduxState = async (services: VisBuilderServices) => { + try { + const serializedState = services.osdUrlStateStorage.get('_a'); + if (serializedState !== null) return serializedState; + } catch (err) { + /* eslint-disable no-console */ + console.error(err); + /* eslint-enable no-console */ + } + + return await getPreloadedState(services); +}; + +export const saveReduxState = ( + { style, visualization, metadata }, + services: VisBuilderServices +) => { + try { + services.osdUrlStateStorage.set( + '_a', + { style, visualization, metadata }, + { + replace: true, + } + ); + } catch (err) { + return; + } +}; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/store.ts b/src/plugins/vis_builder/public/application/utils/state_management/store.ts index f02fc5e946dd..19f60858b19d 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/store.ts +++ b/src/plugins/vis_builder/public/application/utils/state_management/store.ts @@ -8,8 +8,8 @@ import { reducer as styleReducer } from './style_slice'; import { reducer as visualizationReducer } from './visualization_slice'; import { reducer as metadataReducer } from './metadata_slice'; import { VisBuilderServices } from '../../..'; -import { getPreloadedState } from './preload'; import { setEditorState } from './metadata_slice'; +import { loadReduxState, saveReduxState } from './redux_persistence'; const rootReducer = combineReducers({ style: styleReducer, @@ -25,7 +25,7 @@ export const configurePreloadedStore = (preloadedState: PreloadedState { - const preloadedState = await getPreloadedState(services); + const preloadedState = await loadReduxState(services); const store = configurePreloadedStore(preloadedState); const { metadata: metadataState, style: styleState, visualization: vizState } = store.getState(); @@ -62,6 +62,15 @@ export const getPreloadedStore = async (services: VisBuilderServices) => { previousStore = currentStore; previousMetadata = currentMetadata; + + saveReduxState( + { + style: store.getState().style, + visualization: store.getState().visualization, + metadata: store.getState().metadata, + }, + services + ); }; // the store subscriber will automatically detect changes and call handleChange function