From 6afe345231bc48ead6f9361d43a99f153302d8a4 Mon Sep 17 00:00:00 2001 From: abbyhu2000 Date: Wed, 26 Oct 2022 22:44:19 +0000 Subject: [PATCH] global data persistence for vis builder Persist filters, time range, time refresh interval for vis builder when we refresh or navigate to other apps such as dashboard, discover, timeline and visualize Signed-off-by: abbyhu2000 --- CHANGELOG.md | 1 + .../vis_builder/public/application/app.tsx | 24 ++++- .../public/application/components/top_nav.tsx | 5 ++ .../application/utils/get_top_nav_config.tsx | 16 ++-- .../utils/state_management/metadata_slice.ts | 7 +- .../utils/state_management/store.ts | 7 +- src/plugins/vis_builder/public/plugin.test.ts | 1 + src/plugins/vis_builder/public/plugin.ts | 89 +++++++++++++++++-- .../vis_builder/public/plugin_services.ts | 4 + src/plugins/vis_builder/public/types.ts | 8 ++ 10 files changed, 145 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 952729f7784e..250a01417c8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Table Visualization] Fix first column sort issue ([#2828](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2828)) - Temporary workaround for task-kill exceptions on Windows when it is passed a pid for a process that is already dead ([#2842](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2842)) - [Vis Builder] Fix empty workspace animation does not work in firefox ([#2853](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2853)) +- [Vis Builder] global data persistence for vis builder #2896 ([#2896](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2896)) ### 🚞 Infrastructure diff --git a/src/plugins/vis_builder/public/application/app.tsx b/src/plugins/vis_builder/public/application/app.tsx index bd5f2be1feda..2bdc2b1c631b 100644 --- a/src/plugins/vis_builder/public/application/app.tsx +++ b/src/plugins/vis_builder/public/application/app.tsx @@ -3,17 +3,39 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { useEffect } from 'react'; import { I18nProvider } from '@osd/i18n/react'; import { EuiPage, EuiResizableContainer } from '@elastic/eui'; +import { useLocation } from 'react-router-dom'; import { DragDropProvider } from './utils/drag_drop/drag_drop_context'; import { LeftNav } from './components/left_nav'; import { TopNav } from './components/top_nav'; import { Workspace } from './components/workspace'; import './app.scss'; import { RightNav } from './components/right_nav'; +import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; +import { VisBuilderServices } from '../types'; +import { syncQueryStateWithUrl } from '../../../data/public'; export const VisBuilderApp = () => { + const { + services: { + data: { query }, + osdUrlStateStorage, + }, + } = useOpenSearchDashboards(); + const { pathname } = useLocation(); + + useEffect(() => { + // syncs `_g` portion of url with query services + const { stop } = syncQueryStateWithUrl(query, osdUrlStateStorage); + + return () => stop(); + + // this effect should re-run when pathname is changed to preserve querystring part, + // so the global state is always preserved + }, [query, osdUrlStateStorage, pathname]); + // Render the application DOM. return ( diff --git a/src/plugins/vis_builder/public/application/components/top_nav.tsx b/src/plugins/vis_builder/public/application/components/top_nav.tsx index 62d3bb78cc52..fe9b735b5acf 100644 --- a/src/plugins/vis_builder/public/application/components/top_nav.tsx +++ b/src/plugins/vis_builder/public/application/components/top_nav.tsx @@ -36,6 +36,9 @@ export const TopNav = () => { const savedVisBuilderVis = useSavedVisBuilderVis(visualizationIdFromUrl); const { selected: indexPattern } = useIndexPatterns(); const [config, setConfig] = useState(); + const originatingApp = useTypedSelector((state) => { + return state.metadata.originatingApp; + }); useEffect(() => { const getConfig = () => { @@ -47,6 +50,7 @@ export const TopNav = () => { savedVisBuilderVis: saveStateToSavedObject(savedVisBuilderVis, rootState, indexPattern), saveDisabledReason, dispatch, + originatingApp, }, services ); @@ -61,6 +65,7 @@ export const TopNav = () => { saveDisabledReason, dispatch, indexPattern, + originatingApp, ]); // reset validity before component destroyed diff --git a/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx b/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx index c88bb13f3cb3..0d1ca409bcca 100644 --- a/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx @@ -46,22 +46,24 @@ export interface TopNavConfigParams { savedVisBuilderVis: VisBuilderVisSavedObject; saveDisabledReason?: string; dispatch: AppDispatch; + originatingApp: string | undefined; } export const getTopNavConfig = ( - { visualizationIdFromUrl, savedVisBuilderVis, saveDisabledReason, dispatch }: TopNavConfigParams, + { + visualizationIdFromUrl, + savedVisBuilderVis, + saveDisabledReason, + dispatch, + originatingApp, + }: TopNavConfigParams, services: VisBuilderServices ) => { const { i18n: { Context: I18nContext }, embeddable, - scopedHistory, } = services; - const { originatingApp, embeddableId } = - embeddable - .getStateTransfer(scopedHistory) - .getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'input'] }) || {}; const stateTransfer = embeddable.getStateTransfer(); const topNavConfig: TopNavMenuData[] = [ @@ -105,7 +107,7 @@ export const getTopNavConfig = ( showSaveModal(saveModal, I18nContext); }, }, - ...(originatingApp && ((savedVisBuilderVis && savedVisBuilderVis.id) || embeddableId) + ...(originatingApp && savedVisBuilderVis && savedVisBuilderVis.id ? [ { id: 'saveAndReturn', diff --git a/src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts b/src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts index 8cc71804f12e..c7b6a2353b47 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts +++ b/src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts @@ -21,6 +21,7 @@ export interface MetadataState { }; state: EditorState; }; + originatingApp: string | undefined; } const initialState: MetadataState = { @@ -28,6 +29,7 @@ const initialState: MetadataState = { validity: {}, state: 'loading', }, + originatingApp: undefined, }; export const getPreloadedState = async ({ @@ -50,6 +52,9 @@ export const slice = createSlice({ setEditorState: (state, action: PayloadAction<{ state: EditorState }>) => { state.editor.state = action.payload.state; }, + setOriginatingApp: (state, action: PayloadAction<{ state: string | undefined }>) => { + state.originatingApp = action.payload.state; + }, setState: (_state, action: PayloadAction) => { return action.payload; }, @@ -57,4 +62,4 @@ export const slice = createSlice({ }); export const { reducer } = slice; -export const { setValidity, setEditorState, setState } = slice.actions; +export const { setValidity, setEditorState, setOriginatingApp, setState } = slice.actions; 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..ff54c0dbb94f 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 @@ -6,7 +6,7 @@ import { combineReducers, configureStore, PreloadedState } from '@reduxjs/toolkit'; import { reducer as styleReducer } from './style_slice'; import { reducer as visualizationReducer } from './visualization_slice'; -import { reducer as metadataReducer } from './metadata_slice'; +import { reducer as metadataReducer, setOriginatingApp } from './metadata_slice'; import { VisBuilderServices } from '../../..'; import { getPreloadedState } from './preload'; import { setEditorState } from './metadata_slice'; @@ -25,6 +25,10 @@ export const configurePreloadedStore = (preloadedState: PreloadedState { + const { originatingApp } = + services.embeddable + .getStateTransfer(services.scopedHistory) + .getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'input'] }) || {}; const preloadedState = await getPreloadedState(services); const store = configurePreloadedStore(preloadedState); @@ -34,6 +38,7 @@ export const getPreloadedStore = async (services: VisBuilderServices) => { style: styleState, }; let previousMetadata = metadataState; + store.dispatch(setOriginatingApp({ state: originatingApp })); // Listen to changes const handleChange = () => { diff --git a/src/plugins/vis_builder/public/plugin.test.ts b/src/plugins/vis_builder/public/plugin.test.ts index f5fac728420b..35e17865649a 100644 --- a/src/plugins/vis_builder/public/plugin.test.ts +++ b/src/plugins/vis_builder/public/plugin.test.ts @@ -28,6 +28,7 @@ describe('VisBuilderPlugin', () => { const setupDeps = { visualizations: visualizationsPluginMock.createSetupContract(), embeddable: embeddablePluginMock.createSetupContract(), + data: dataPluginMock.createSetupContract(), }; const setup = plugin.setup(coreSetup, setupDeps); diff --git a/src/plugins/vis_builder/public/plugin.ts b/src/plugins/vis_builder/public/plugin.ts index 8e90a04784cd..b8a6f886237b 100644 --- a/src/plugins/vis_builder/public/plugin.ts +++ b/src/plugins/vis_builder/public/plugin.ts @@ -4,13 +4,17 @@ */ import { i18n } from '@osd/i18n'; +import { BehaviorSubject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; import { AppMountParameters, AppNavLinkStatus, + AppUpdater, CoreSetup, CoreStart, Plugin, PluginInitializerContext, + ScopedHistory, } from '../../../core/public'; import { VisBuilderPluginSetupDependencies, @@ -41,10 +45,17 @@ import { setUISettings, setTypeService, setReactExpressionRenderer, + setQueryService, } from './plugin_services'; import { createSavedVisBuilderLoader } from './saved_visualizations'; import { registerDefaultTypes } from './visualizations'; import { ConfigSchema } from '../config'; +import { + createOsdUrlStateStorage, + createOsdUrlTracker, + withNotifyOnErrors, +} from '../../opensearch_dashboards_utils/public'; +import { opensearchFilters } from '../../data/public'; export class VisBuilderPlugin implements @@ -55,13 +66,50 @@ export class VisBuilderPlugin VisBuilderPluginStartDependencies > { private typeService = new TypeService(); + private appStateUpdater = new BehaviorSubject(() => ({})); + private stopUrlTracking: (() => void) | undefined = undefined; + private currentHistory: ScopedHistory | undefined = undefined; constructor(public initializerContext: PluginInitializerContext) {} public setup( core: CoreSetup, - { embeddable, visualizations }: VisBuilderPluginSetupDependencies + { embeddable, visualizations, data: dataSetup }: VisBuilderPluginSetupDependencies ) { + const { + appMounted, + appUnMounted, + stop: stopUrlTracker, + setActiveUrl, + restorePreviousUrl, + } = createOsdUrlTracker({ + baseUrl: core.http.basePath.prepend('/app/vis-builder'), + defaultSubUrl: '#/', + storageKey: `lastUrl:${core.http.basePath.get()}:vis-builder`, + navLinkUpdater$: this.appStateUpdater, + toastNotifications: core.notifications.toasts, + stateParams: [ + { + osdUrlKey: '_g', + stateUpdate$: dataSetup.query.state$.pipe( + filter( + ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) + ), + map(({ state }) => ({ + ...state, + filters: state.filters?.filter(opensearchFilters.isFilterPinned), + })) + ), + }, + ], + getHistory: () => { + return this.currentHistory!; + }, + }); + this.stopUrlTracking = () => { + stopUrlTracker(); + }; + const typeService = this.typeService; registerDefaultTypes(typeService.setup()); @@ -70,13 +118,15 @@ export class VisBuilderPlugin id: PLUGIN_ID, title: PLUGIN_NAME, navLinkStatus: AppNavLinkStatus.hidden, - async mount(params: AppMountParameters) { + defaultPath: '#/', + mount: async (params: AppMountParameters) => { // Load application bundle const { renderApp } = await import('./application'); // Get start services as specified in opensearch_dashboards.json const [coreStart, pluginsStart, selfStart] = await core.getStartServices(); const { data, savedObjects, navigation, expressions } = pluginsStart; + this.currentHistory = params.history; // make sure the index pattern list is up to date data.indexPatterns.clearCache(); @@ -85,28 +135,48 @@ export class VisBuilderPlugin // TODO: Add the redirect await pluginsStart.data.indexPatterns.ensureDefaultIndexPattern(); - // Register Default Visualizations + appMounted(); + // dispatch synthetic hash change event to update hash history objects + // this is necessary because hash updates triggered by using popState won't trigger this event naturally. + const unlistenParentHistory = params.history.listen(() => { + window.dispatchEvent(new HashChangeEvent('hashchange')); + }); + + // Register Default Visualizations const services: VisBuilderServices = { ...coreStart, + scopedHistory: this.currentHistory, + history: params.history, + osdUrlStateStorage: createOsdUrlStateStorage({ + history: params.history, + useHash: coreStart.uiSettings.get('state:storeInSessionStorage'), + ...withNotifyOnErrors(coreStart.notifications.toasts), + }), toastNotifications: coreStart.notifications.toasts, data, savedObjectsPublic: savedObjects, navigation, expressions, - history: params.history, setHeaderActionMenu: params.setHeaderActionMenu, types: typeService.start(), savedVisBuilderLoader: selfStart.savedVisBuilderLoader, embeddable: pluginsStart.embeddable, - scopedHistory: params.history, + setActiveUrl, + restorePreviousUrl, + dashboard: pluginsStart.dashboard, }; // Instantiate the store const store = await getPreloadedStore(services); + const unmount = renderApp(params, services, store); // Render the application - return renderApp(params, services, store); + return () => { + unlistenParentHistory(); + unmount(); + appUnMounted(); + }; }, }); @@ -176,6 +246,7 @@ export class VisBuilderPlugin setTimeFilter(data.query.timefilter.timefilter); setTypeService(typeService); setUISettings(core.uiSettings); + setQueryService(data.query); return { ...typeService, @@ -183,5 +254,9 @@ export class VisBuilderPlugin }; } - public stop() {} + public stop() { + if (this.stopUrlTracking) { + this.stopUrlTracking(); + } + } } diff --git a/src/plugins/vis_builder/public/plugin_services.ts b/src/plugins/vis_builder/public/plugin_services.ts index f979f3a22b11..c5583e3c5e43 100644 --- a/src/plugins/vis_builder/public/plugin_services.ts +++ b/src/plugins/vis_builder/public/plugin_services.ts @@ -37,3 +37,7 @@ export const [getTimeFilter, setTimeFilter] = createGetterSetter('TypeService'); export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); + +export const [getQueryService, setQueryService] = createGetterSetter< + DataPublicPluginStart['query'] +>('Query'); diff --git a/src/plugins/vis_builder/public/types.ts b/src/plugins/vis_builder/public/types.ts index 131c9cc1f6bb..5780702be9cf 100644 --- a/src/plugins/vis_builder/public/types.ts +++ b/src/plugins/vis_builder/public/types.ts @@ -14,6 +14,9 @@ import { DataPublicPluginStart } from '../../data/public'; import { TypeServiceSetup, TypeServiceStart } from './services/type_service'; import { SavedObjectLoader } from '../../saved_objects/public'; import { AppMountParameters, CoreStart, ToastsStart, ScopedHistory } from '../../../core/public'; +import { IOsdUrlStateStorage } from '../../opensearch_dashboards_utils/public'; +import { UrlForwardingSetup } from '../../url_forwarding/public'; +import { DataPublicPluginSetup } from '../../data/public'; export type VisBuilderSetup = TypeServiceSetup; export interface VisBuilderStart extends TypeServiceStart { @@ -23,6 +26,7 @@ export interface VisBuilderStart extends TypeServiceStart { export interface VisBuilderPluginSetupDependencies { embeddable: EmbeddableSetup; visualizations: VisualizationsSetup; + data: DataPublicPluginSetup; } export interface VisBuilderPluginStartDependencies { embeddable: EmbeddableStart; @@ -45,6 +49,10 @@ export interface VisBuilderServices extends CoreStart { history: History; embeddable: EmbeddableStart; scopedHistory: ScopedHistory; + osdUrlStateStorage: IOsdUrlStateStorage; + setActiveUrl: (newUrl: string) => void; + restorePreviousUrl: () => void; + dashboard: DashboardStart; } export interface ISavedVis {