Skip to content

Commit

Permalink
Working multi view state management
Browse files Browse the repository at this point in the history
Signed-off-by: Ashwin P Chandran <ashwinpc@amazon.com>
  • Loading branch information
ashwin-pc committed Jul 17, 2023
1 parent 32a1299 commit 5981002
Show file tree
Hide file tree
Showing 14 changed files with 84 additions and 156 deletions.
68 changes: 10 additions & 58 deletions src/plugins/data_explorer/public/components/app_container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,80 +3,31 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useLayoutEffect, useRef, useState } from 'react';
import React from 'react';
import { EuiPageTemplate } from '@elastic/eui';
import { Suspense } from 'react';
import { AppMountParameters } from '../../../../core/public';
import { Sidebar } from './sidebar';
import { NoView } from './no_view';
import { View } from '../services/view_service/view';
import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public';
import { DataExplorerServices } from '../types';

export const AppContainer = ({ view, params }: { view?: View; params: AppMountParameters }) => {
const [showSpinner, setShowSpinner] = useState(false);
const {
services: { store },
} = useOpenSearchDashboards<DataExplorerServices>();
const canvasRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
const unmountRef = useRef<any>(null);

useLayoutEffect(() => {
const unmount = () => {
if (unmountRef.current) {
unmountRef.current();
unmountRef.current = null;
}
};

// Do nothing if the view is not defined or if the view is the same as the previous view
if (!view || (unmountRef.current && unmountRef.current.viewId === view.id)) {
return;
}

// unmount the previous view
unmount();

const mount = async () => {
setShowSpinner(true);
try {
unmountRef.current =
(await view.mount({
canvasElement: canvasRef.current!,
panelElement: panelRef.current!,
appParams: params,
// The provider is added to the services right after the store is created. so it is safe to assume its here.
store: store!,
})) || null;
} catch (e) {
// TODO: add error UI
// eslint-disable-next-line no-console
console.error(e);
} finally {
// if (canvasRef.current && panelRef.current) {
if (canvasRef.current) {
setShowSpinner(false);
}
}
};

mount();

return unmount;
}, [params, view, store]);

// TODO: Make this more robust.
if (!view) {
return <NoView />;
}

const { Canvas, Panel } = view;

// Render the application DOM.
// Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract.
return (
<EuiPageTemplate
pageSideBar={
<Sidebar>
<div ref={panelRef} />
<Suspense fallback={<div>Loading...</div>}>
<Panel {...params} />
</Suspense>
</Sidebar>
}
className="dePageTemplate"
Expand All @@ -85,8 +36,9 @@ export const AppContainer = ({ view, params }: { view?: View; params: AppMountPa
paddingSize="none"
>
{/* TODO: improve loading state */}
{showSpinner && <div>Loading...</div>}
<div key={view.id} ref={canvasRef} />
<Suspense fallback={<div>Loading...</div>}>
<Canvas {...params} />
</Suspense>
</EuiPageTemplate>
);
};
4 changes: 2 additions & 2 deletions src/plugins/data_explorer/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ export function plugin() {
return new DataExplorerPlugin();
}
export { DataExplorerPluginSetup, DataExplorerPluginStart, ViewRedirectParams } from './types';
export { ViewMountParameters, ViewDefinition } from './services/view_service';
export { RootState as DataExplorerRootState } from './utils/state_management';
export { ViewProps, ViewDefinition } from './services/view_service';
export { RootState, useTypedSelector, useTypedDispatch } from './utils/state_management';
12 changes: 4 additions & 8 deletions src/plugins/data_explorer/public/services/view_service/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
*/

import { Slice } from '@reduxjs/toolkit';
import { LazyExoticComponent } from 'react';
import { AppMountParameters } from '../../../../../core/public';
import { Store } from '../../utils/state_management';

// TODO: State management props

Expand All @@ -14,12 +14,7 @@ interface ViewListItem {
label: string;
}

export interface ViewMountParameters {
canvasElement: HTMLDivElement;
panelElement: HTMLDivElement;
appParams: AppMountParameters;
store: any;
}
export type ViewProps = AppMountParameters;

export interface ViewDefinition<T = any> {
readonly id: string;
Expand All @@ -28,7 +23,8 @@ export interface ViewDefinition<T = any> {
defaults: T | (() => T) | (() => Promise<T>);
slice: Slice<T>;
};
readonly mount: (params: ViewMountParameters) => Promise<() => void>;
readonly Canvas: LazyExoticComponent<(props: ViewProps) => React.ReactElement>;
readonly Panel: LazyExoticComponent<(props: ViewProps) => React.ReactElement>;
readonly defaultPath: string;
readonly appExtentions: {
savedObject: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export class View implements IView {
public readonly defaultPath: string;
public readonly appExtentions: IView['appExtentions'];
readonly shouldShow?: (state: any) => boolean;
readonly mount: IView['mount'];
readonly Canvas: IView['Canvas'];
readonly Panel: IView['Panel'];

constructor(options: ViewDefinition) {
this.id = options.id;
Expand All @@ -22,6 +23,7 @@ export class View implements IView {
this.defaultPath = options.defaultPath;
this.appExtentions = options.appExtentions;
this.shouldShow = options.shouldShow;
this.mount = options.mount;
this.Canvas = options.Canvas;
this.Panel = options.Panel;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';

// Use throughout the app instead of plain `useDispatch` and `useSelector`
export const useTypedDispatch = () => useDispatch<AppDispatch>();
export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
export const useTypedSelector: <TState = RootState, TSelected = unknown>(
selector: (state: TState) => TSelected,
equalityFn?: (left: TSelected, right: TSelected) => boolean
) => TSelected = useSelector;
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const getPreloadedState = async (

// initialize the default state for each view
const views = services.viewRegistry.all();
views.forEach(async (view) => {
const promises = views.map(async (view) => {
if (!view.ui) {
return;
}
Expand All @@ -31,6 +31,7 @@ export const getPreloadedState = async (
rootState[view.id] = defaults;
}
});
await Promise.all(promises);

return rootState;
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
*/

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { TypedUseSelectorHook, createDispatchHook, createSelectorHook } from 'react-redux';
import { createContext } from 'react';
import { Filter, Query } from '../../../../../data/public';
import { DiscoverServices } from '../../../build_services';
import { DataExplorerRootState } from '../../../../../data_explorer/public';
import { RootState } from '../../../../../data_explorer/public';

export interface DiscoverState {
/**
Expand All @@ -19,10 +17,6 @@ export interface DiscoverState {
* Array of applied filters
*/
filters?: Filter[];
/**
* id of the used index pattern
*/
index?: string;
/**
* Used interval of the histogram
*/
Expand All @@ -41,14 +35,13 @@ export interface DiscoverState {
savedQuery?: string;
}

export interface RootState extends DataExplorerRootState {
export interface DiscoverRootState extends RootState {
discover: DiscoverState;
}

const initialState = {} as DiscoverState;

export const getPreloadedState = async ({ data }: DiscoverServices): Promise<DiscoverState> => {
// console.log(data.query.timefilter.timefilter.getRefreshInterval().value.toString());
return {
...initialState,
interval: data.query.timefilter.timefilter.getRefreshInterval().value.toString(),
Expand All @@ -67,6 +60,8 @@ export const discoverSlice = createSlice({
...state,
...action.payload,
};

return state;
},
},
});
Expand All @@ -76,12 +71,4 @@ export const setState = discoverSlice.actions.setState as <T>(payload: T) => Pay
export const updateState = discoverSlice.actions.updateState as <T>(
payload: Partial<T>
) => PayloadAction<Partial<T>>;

export const { reducer } = discoverSlice;
export const contextDiscover = createContext<any>({});

export const useTypedSelector: TypedUseSelectorHook<RootState> = createSelectorHook(
contextDiscover
);

export const useDispatch = createDispatchHook(contextDiscover);
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,16 @@
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { TypedUseSelectorHook } from 'react-redux';
import { RootState, useTypedDispatch, useTypedSelector } from '../../../../../data_explorer/public';
import { DiscoverState } from './discover_slice';

export * from './discover_slice';

export interface DiscoverRootState extends RootState {
discover: DiscoverState;
}

export const useSelector: TypedUseSelectorHook<DiscoverRootState> = useTypedSelector;
export const useDispatch = useTypedDispatch;
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@ import { AppMountParameters } from '../../../../../../core/public';
import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public';
import { DiscoverServices } from '../../../build_services';
import { TopNav } from './top_nav';
import {
updateState,
useDispatch,
useTypedSelector,
} from '../../utils/state_management/discover_slice';
import { updateState, useDispatch, useSelector } from '../../utils/state_management';

interface CanvasProps {
opts: {
Expand All @@ -22,15 +18,13 @@ interface CanvasProps {

export const Canvas = ({ opts }: CanvasProps) => {
const { services } = useOpenSearchDashboards<DiscoverServices>();
const {
discover: { interval },
} = useTypedSelector((state) => state);
const interval = useSelector((state) => state.discover.interval);
const dispatch = useDispatch();

return (
<div>
<TopNav opts={opts} />
Canvas
Interval:
<input
type="text"
name=""
Expand All @@ -40,6 +34,7 @@ export const Canvas = ({ opts }: CanvasProps) => {
dispatch(updateState({ interval: e.target.value }));
}}
/>
<p>Services: {services.docLinks.DOC_LINK_VERSION}</p>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,21 @@
*/

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ViewMountParameters } from '../../../../../data_explorer/public';
import { ViewProps } from '../../../../../data_explorer/public';
import { OpenSearchDashboardsContextProvider } from '../../../../../opensearch_dashboards_react/public';
import { DiscoverServices } from '../../../build_services';
import { Canvas } from './canvas';
import { contextDiscover } from '../../utils/state_management/discover_slice';
import { getServices } from '../../../opensearch_dashboards_services';

export const renderCanvas = (
{ canvasElement, appParams, store }: ViewMountParameters,
services: DiscoverServices
) => {
const { setHeaderActionMenu } = appParams;

ReactDOM.render(
// eslint-disable-next-line import/no-default-export
export default function CanvasApp({ setHeaderActionMenu }: ViewProps) {
const services = getServices();
return (
<OpenSearchDashboardsContextProvider services={services}>
<Provider context={contextDiscover} store={store}>
<Canvas
opts={{
setHeaderActionMenu,
}}
/>
</Provider>
</OpenSearchDashboardsContextProvider>,
canvasElement
<Canvas
opts={{
setHeaderActionMenu,
}}
/>
</OpenSearchDashboardsContextProvider>
);

return () => ReactDOM.unmountComponentAtNode(canvasElement);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,17 @@
*/

import React from 'react';
import ReactDOM from 'react-dom';
import { ViewMountParameters } from '../../../../../data_explorer/public';
import { ViewProps } from '../../../../../data_explorer/public';
import { OpenSearchDashboardsContextProvider } from '../../../../../opensearch_dashboards_react/public';
import { DiscoverServices } from '../../../build_services';
import { Panel } from './panel';
import { getServices } from '../../../opensearch_dashboards_services';

export const renderPanel = ({ panelElement }: ViewMountParameters, services: DiscoverServices) => {
ReactDOM.render(
// eslint-disable-next-line import/no-default-export
export default function PanelApp(props: ViewProps) {
const services = getServices();
return (
<OpenSearchDashboardsContextProvider services={services}>
<Panel />
</OpenSearchDashboardsContextProvider>,
panelElement
</OpenSearchDashboardsContextProvider>
);

return () => ReactDOM.unmountComponentAtNode(panelElement);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
*/

import React from 'react';
import { useSelector } from '../../utils/state_management';

export const Panel = () => {
return <div>Side Panel</div>;
const interval = useSelector((state) => state.discover.interval);
return <div>{interval}</div>;
};
Loading

0 comments on commit 5981002

Please sign in to comment.