Skip to content

Commit

Permalink
[Vis Builder] Add global data persistence for vis builder (#2896) (#3042
Browse files Browse the repository at this point in the history
)
  • Loading branch information
opensearch-trigger-bot[bot] authored Jan 3, 2023
1 parent e4ec90d commit ea36d5a
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 21 deletions.
24 changes: 23 additions & 1 deletion src/plugins/vis_builder/public/application/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<VisBuilderServices>();
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 (
<I18nProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export const TopNav = () => {
const savedVisBuilderVis = useSavedVisBuilderVis(visualizationIdFromUrl);
const { selected: indexPattern } = useIndexPatterns();
const [config, setConfig] = useState<TopNavMenuData[] | undefined>();
const originatingApp = useTypedSelector((state) => {
return state.metadata.originatingApp;
});

useEffect(() => {
const getConfig = () => {
Expand All @@ -47,6 +50,7 @@ export const TopNav = () => {
savedVisBuilderVis: saveStateToSavedObject(savedVisBuilderVis, rootState, indexPattern),
saveDisabledReason,
dispatch,
originatingApp,
},
services
);
Expand All @@ -61,6 +65,7 @@ export const TopNav = () => {
saveDisabledReason,
dispatch,
indexPattern,
originatingApp,
]);

// reset validity before component destroyed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,24 @@ export interface TopNavConfigParams {
savedVisBuilderVis: VisBuilderVisSavedObject;
saveDisabledReason?: string;
dispatch: AppDispatch;
originatingApp?: string;
}

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[] = [
Expand Down Expand Up @@ -105,7 +107,7 @@ export const getTopNavConfig = (
showSaveModal(saveModal, I18nContext);
},
},
...(originatingApp && ((savedVisBuilderVis && savedVisBuilderVis.id) || embeddableId)
...(originatingApp && savedVisBuilderVis && savedVisBuilderVis.id
? [
{
id: 'saveAndReturn',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,28 @@ export interface MetadataState {
};
state: EditorState;
};
originatingApp?: string;
}

const initialState: MetadataState = {
editor: {
validity: {},
state: 'loading',
},
originatingApp: undefined,
};

export const getPreloadedState = async ({
types,
data,
embeddable,
scopedHistory,
}: VisBuilderServices): Promise<MetadataState> => {
const preloadedState = { ...initialState };
const { originatingApp } =
embeddable
.getStateTransfer(scopedHistory)
.getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'input'] }) || {};
const preloadedState = { ...initialState, originatingApp };

return preloadedState;
};
Expand All @@ -50,11 +58,14 @@ export const slice = createSlice({
setEditorState: (state, action: PayloadAction<{ state: EditorState }>) => {
state.editor.state = action.payload.state;
},
setOriginatingApp: (state, action: PayloadAction<{ state?: string }>) => {
state.originatingApp = action.payload.state;
},
setState: (_state, action: PayloadAction<MetadataState>) => {
return action.payload;
},
},
});

export const { reducer } = slice;
export const { setValidity, setEditorState, setState } = slice.actions;
export const { setValidity, setEditorState, setOriginatingApp, setState } = slice.actions;
1 change: 1 addition & 0 deletions src/plugins/vis_builder/public/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe('VisBuilderPlugin', () => {
const setupDeps = {
visualizations: visualizationsPluginMock.createSetupContract(),
embeddable: embeddablePluginMock.createSetupContract(),
data: dataPluginMock.createSetupContract(),
};

const setup = plugin.setup(coreSetup, setupDeps);
Expand Down
89 changes: 78 additions & 11 deletions src/plugins/vis_builder/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -55,13 +66,45 @@ export class VisBuilderPlugin
VisBuilderPluginStartDependencies
> {
private typeService = new TypeService();
private appStateUpdater = new BehaviorSubject<AppUpdater>(() => ({}));
private stopUrlTracking?: () => void;
private currentHistory?: ScopedHistory;

constructor(public initializerContext: PluginInitializerContext<ConfigSchema>) {}

public setup(
core: CoreSetup<VisBuilderPluginStartDependencies, VisBuilderStart>,
{ embeddable, visualizations }: VisBuilderPluginSetupDependencies
{ embeddable, visualizations, data }: VisBuilderPluginSetupDependencies
) {
const { appMounted, appUnMounted, stop: stopUrlTracker } = createOsdUrlTracker({
baseUrl: core.http.basePath.prepend(`/app/${PLUGIN_ID}`),
defaultSubUrl: '#/',
storageKey: `lastUrl:${core.http.basePath.get()}:${PLUGIN_ID}`,
navLinkUpdater$: this.appStateUpdater,
toastNotifications: core.notifications.toasts,
stateParams: [
{
osdUrlKey: '_g',
stateUpdate$: data.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();
};

// Register Default Visualizations
const typeService = this.typeService;
registerDefaultTypes(typeService.setup());

Expand All @@ -70,43 +113,62 @@ 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;
const { savedObjects, navigation, expressions } = pluginsStart;
this.currentHistory = params.history;

// make sure the index pattern list is up to date
data.indexPatterns.clearCache();
pluginsStart.data.indexPatterns.clearCache();
// make sure a default index pattern exists
// if not, the page will be redirected to management and visualize won't be rendered
// 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 = this.currentHistory.listen(() => {
window.dispatchEvent(new HashChangeEvent('hashchange'));
});

const services: VisBuilderServices = {
...coreStart,
scopedHistory: this.currentHistory,
history: this.currentHistory,
osdUrlStateStorage: createOsdUrlStateStorage({
history: this.currentHistory,
useHash: coreStart.uiSettings.get('state:storeInSessionStorage'),
...withNotifyOnErrors(coreStart.notifications.toasts),
}),
toastNotifications: coreStart.notifications.toasts,
data,
data: pluginsStart.data,
savedObjectsPublic: savedObjects,
navigation,
expressions,
history: params.history,
setHeaderActionMenu: params.setHeaderActionMenu,
types: typeService.start(),
savedVisBuilderLoader: selfStart.savedVisBuilderLoader,
embeddable: pluginsStart.embeddable,
scopedHistory: params.history,
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();
};
},
});

Expand Down Expand Up @@ -154,7 +216,7 @@ export class VisBuilderPlugin

public start(
core: CoreStart,
{ data, expressions }: VisBuilderPluginStartDependencies
{ expressions, data }: VisBuilderPluginStartDependencies
): VisBuilderStart {
const typeService = this.typeService.start();

Expand All @@ -176,12 +238,17 @@ export class VisBuilderPlugin
setTimeFilter(data.query.timefilter.timefilter);
setTypeService(typeService);
setUISettings(core.uiSettings);
setQueryService(data.query);

return {
...typeService,
savedVisBuilderLoader,
};
}

public stop() {}
public stop() {
if (this.stopUrlTracking) {
this.stopUrlTracking();
}
}
}
4 changes: 4 additions & 0 deletions src/plugins/vis_builder/public/plugin_services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ export const [getTimeFilter, setTimeFilter] = createGetterSetter<TimefilterContr
export const [getTypeService, setTypeService] = createGetterSetter<TypeServiceStart>('TypeService');

export const [getUISettings, setUISettings] = createGetterSetter<IUiSettingsClient>('UISettings');

export const [getQueryService, setQueryService] = createGetterSetter<
DataPublicPluginStart['query']
>('Query');
5 changes: 5 additions & 0 deletions src/plugins/vis_builder/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ 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 { DataPublicPluginSetup } from '../../data/public';

export type VisBuilderSetup = TypeServiceSetup;
export interface VisBuilderStart extends TypeServiceStart {
Expand All @@ -23,6 +25,7 @@ export interface VisBuilderStart extends TypeServiceStart {
export interface VisBuilderPluginSetupDependencies {
embeddable: EmbeddableSetup;
visualizations: VisualizationsSetup;
data: DataPublicPluginSetup;
}
export interface VisBuilderPluginStartDependencies {
embeddable: EmbeddableStart;
Expand All @@ -45,6 +48,8 @@ export interface VisBuilderServices extends CoreStart {
history: History;
embeddable: EmbeddableStart;
scopedHistory: ScopedHistory;
osdUrlStateStorage: IOsdUrlStateStorage;
dashboard: DashboardStart;
}

export interface ISavedVis {
Expand Down

0 comments on commit ea36d5a

Please sign in to comment.