Skip to content

Commit

Permalink
attempt 1 at dynamic 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 14, 2023
1 parent 4a72f86 commit 32a1299
Show file tree
Hide file tree
Showing 13 changed files with 221 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ 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);
Expand Down Expand Up @@ -40,6 +45,8 @@ export const AppContainer = ({ view, params }: { view?: View; params: AppMountPa
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
Expand All @@ -56,7 +63,7 @@ export const AppContainer = ({ view, params }: { view?: View; params: AppMountPa
mount();

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

// TODO: Make this more robust.
if (!view) {
Expand Down
1 change: 1 addition & 0 deletions src/plugins/data_explorer/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export function plugin() {
}
export { DataExplorerPluginSetup, DataExplorerPluginStart, ViewRedirectParams } from './types';
export { ViewMountParameters, ViewDefinition } from './services/view_service';
export { RootState as DataExplorerRootState } from './utils/state_management';
10 changes: 10 additions & 0 deletions src/plugins/data_explorer/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,19 @@ export class DataExplorerPlugin
core: CoreSetup<DataExplorerPluginStartDependencies, DataExplorerPluginStart>
): DataExplorerPluginSetup {
const viewService = this.viewService;
// TODO: Remove this before merge to main
// eslint-disable-next-line no-console
console.log('data_explorer: Setup');

// Register an application into the side navigation menu
core.application.register({
id: PLUGIN_ID,
title: PLUGIN_NAME,
navLinkStatus: AppNavLinkStatus.hidden,
mount: async (params: AppMountParameters) => {
// TODO: Remove this before merge to main
// eslint-disable-next-line no-console
console.log('data_explorer: Mounted');
// Load application bundle
const { renderApp } = await import('./application');

Expand All @@ -74,6 +80,7 @@ export class DataExplorerPlugin
// Get start services as specified in opensearch_dashboards.json
// Render the application
const { store, unsubscribe: unsubscribeStore } = await getPreloadedStore(services);
services.store = store;

const unmount = renderApp(coreStart, services, params, store);

Expand All @@ -90,6 +97,9 @@ export class DataExplorerPlugin
}

public start(core: CoreStart): DataExplorerPluginStart {
// TODO: Remove this before merge to main
// eslint-disable-next-line no-console
console.log('data_explorer: Started');
return {};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
* SPDX-License-Identifier: Apache-2.0
*/

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

// TODO: State management props

Expand All @@ -16,14 +18,15 @@ export interface ViewMountParameters {
canvasElement: HTMLDivElement;
panelElement: HTMLDivElement;
appParams: AppMountParameters;
store: any;
}

export interface ViewDefinition<T = any> {
readonly id: string;
readonly title: string;
readonly ui?: {
defaults: T | (() => T);
reducer: (state: T, action: any) => T;
defaults: T | (() => T) | (() => Promise<T>);
slice: Slice<T>;
};
readonly mount: (params: ViewMountParameters) => Promise<() => void>;
readonly defaultPath: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class ViewService implements CoreService<ViewServiceSetup, ViewServiceSta
* registers a visualization type
* @param config - visualization type definition
*/
registerView: (config: ViewDefinition): void => {
registerView: <T = any>(config: ViewDefinition<T>): void => {
const view = new View(config);
this.registerView(view);
},
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/data_explorer/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ExpressionsStart } from '../../expressions/public';
import { ViewServiceStart, ViewServiceSetup } from './services/view_service';
import { IOsdUrlStateStorage } from '../../opensearch_dashboards_utils/public';
import { DataPublicPluginStart } from '../../data/public';
import { Store } from './utils/state_management';

export type DataExplorerPluginSetup = ViewServiceSetup;

Expand All @@ -29,6 +30,7 @@ export interface ViewRedirectParams {
}

export interface DataExplorerServices extends CoreStart {
store?: Store;
viewRegistry: ViewServiceStart;
expressions: ExpressionsStart;
embeddable: EmbeddableStart;
Expand Down
25 changes: 20 additions & 5 deletions src/plugins/data_explorer/public/utils/state_management/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,26 @@ import { DataExplorerServices } from '../../types';
export const getPreloadedState = async (
services: DataExplorerServices
): Promise<PreloadedState<RootState>> => {
const metadataState = await getPreloadedMetadataState(services);
const rootState: RootState = {
metadata: await getPreloadedMetadataState(services),
};

// TODO: preload view states
// initialize the default state for each view
const views = services.viewRegistry.all();
views.forEach(async (view) => {
if (!view.ui) {
return;
}

return {
metadata: metadataState,
};
const { defaults } = view.ui;

// defaults can be a function or an object
if (typeof defaults === 'function') {
rootState[view.id] = await defaults();
} else {
rootState[view.id] = defaults;
}
});

return rootState;
};
46 changes: 30 additions & 16 deletions src/plugins/data_explorer/public/utils/state_management/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,43 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { combineReducers, configureStore, PreloadedState } from '@reduxjs/toolkit';
import React from 'react';
import { combineReducers, configureStore, PreloadedState, Reducer, Slice } from '@reduxjs/toolkit';
import { isEqual } from 'lodash';
import { reducer as metadataReducer } from './metadata_slice';
import { Provider } from 'react-redux';
import { reducer as metadataReducer, MetadataState } from './metadata_slice';
import { loadReduxState, persistReduxState } from './redux_persistence';
import { DataExplorerServices } from '../../types';

const rootReducer = combineReducers({
const dynamicReducers: {
metadata: Reducer<MetadataState>;
[key: string]: Reducer;
} = {
metadata: metadataReducer,
});
};

const rootReducer = combineReducers(dynamicReducers);

export const configurePreloadedStore = (preloadedState: PreloadedState<RootState>) => {
// After registering the slices the root reducer needs to be updated
const updatedRootReducer = combineReducers(dynamicReducers);

return configureStore({
reducer: rootReducer,
reducer: updatedRootReducer,
preloadedState,
});
};

export const getPreloadedStore = async (services: DataExplorerServices) => {
// For each view preload the data and register the slice
const views = services.viewRegistry.all();
views.forEach((view) => {
if (!view.ui) return;

const { slice } = view.ui;
registerSlice(slice);
});

const preloadedState = await loadReduxState(services);
const store = configurePreloadedStore(preloadedState);

Expand All @@ -44,17 +63,12 @@ export const getPreloadedStore = async (services: DataExplorerServices) => {
return { store, unsubscribe };
};

// export const registerSlice = (slice: any) => {
// dynamicReducers[slice.name] = slice.reducer;
// store.replaceReducer(combineReducers(dynamicReducers));

// // Extend RootState to include the new slice
// declare module 'path-to-main-store' {
// interface RootState {
// [slice.name]: ReturnType<typeof slice.reducer>;
// }
// }
// }
export const registerSlice = (slice: Slice) => {
if (dynamicReducers[slice.name]) {
throw new Error(`Slice ${slice.name} already registered`);
}
dynamicReducers[slice.name] = slice.reducer;
};

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof rootReducer>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

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';

export interface DiscoverState {
/**
* Columns displayed in the table
*/
columns?: string[];
/**
* Array of applied filters
*/
filters?: Filter[];
/**
* id of the used index pattern
*/
index?: string;
/**
* Used interval of the histogram
*/
interval?: string;
/**
* Lucence or DQL query
*/
query?: Query;
/**
* Array of the used sorting [[field,direction],...]
*/
sort?: string[][];
/**
* id of the used saved query
*/
savedQuery?: string;
}

export interface RootState extends DataExplorerRootState {
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(),
};
};

export const discoverSlice = createSlice({
name: 'discover',
initialState,
reducers: {
setState<T>(state: T, action: PayloadAction<DiscoverState>) {
return action.payload;
},
updateState<T>(state: T, action: PayloadAction<Partial<DiscoverState>>) {
state = {
...state,
...action.payload,
};
},
},
});

// Exposing the state functions as generics
export const setState = discoverSlice.actions.setState as <T>(payload: T) => PayloadAction<T>;
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
@@ -0,0 +1,4 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ 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';

interface CanvasProps {
opts: {
Expand All @@ -17,11 +22,24 @@ interface CanvasProps {

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

return (
<div>
<TopNav opts={opts} />
Canvas
<input
type="text"
name=""
id="temp"
value={interval}
onChange={(e) => {
dispatch(updateState({ interval: e.target.value }));
}}
/>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,28 @@

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ViewMountParameters } 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';

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

ReactDOM.render(
<OpenSearchDashboardsContextProvider services={services}>
<Canvas
opts={{
setHeaderActionMenu,
}}
/>
<Provider context={contextDiscover} store={store}>
<Canvas
opts={{
setHeaderActionMenu,
}}
/>
</Provider>
</OpenSearchDashboardsContextProvider>,
canvasElement
);
Expand Down
Loading

0 comments on commit 32a1299

Please sign in to comment.