Skip to content

Commit

Permalink
geosolutions-it#8503 Bugs related modular plugins implementation (geo…
Browse files Browse the repository at this point in the history
…solutions-it#8495)

(cherry picked from commit 5756dd4)
  • Loading branch information
alexander-fedorenko committed Sep 7, 2022
1 parent 226b4af commit 7862c7d
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 41 deletions.
6 changes: 3 additions & 3 deletions docs/developer-guide/writing-epics.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,14 +215,14 @@ export const dummyEpic = (action$, store) => action$.ofType(ACTION)

In this case, internal stream should be muted explicitly.

Each epic receives third argument called `isActive$`.
Each epic receives third argument type of object, having property called `pluginRenderStream$`.
Combined with `semaphore` it allows to mute internal stream whenever epic is muted:

```js
export const dummyEpic = (action$, store, isActive$) => action$.ofType(ACTION)
export const dummyEpic = (action$, store, { pluginRenderStream$ }) => action$.ofType(ACTION)
.switchMap(() => {
return Rx.Observable.interval(1000)
.let(semaphore(isActive$.startWith(true)))
.let(semaphore(pluginRenderStream$.startWith(true)))
.switchMap(() => {
console.log('TEST');
return Rx.Observable.empty();
Expand Down
12 changes: 11 additions & 1 deletion web/client/components/contextcreator/ContextCreator.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export default class ContextCreator extends React.Component {
backToPageDestRoute: PropTypes.string,
backToPageConfirmationMessage: PropTypes.string,
tutorials: PropTypes.object,
tutorialsList: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
themes: PropTypes.array,
setSelectedTheme: PropTypes.func,
selectedTheme: PropTypes.string,
Expand Down Expand Up @@ -251,7 +252,8 @@ export default class ContextCreator extends React.Component {
showBackToPageConfirmation: false,
backToPageDestRoute: '/context-manager',
backToPageConfirmationMessage: 'contextCreator.undo',
tutorials: CONTEXT_TUTORIALS
tutorials: CONTEXT_TUTORIALS,
tutorialsList: false
};

componentDidMount() {
Expand All @@ -260,6 +262,14 @@ export default class ContextCreator extends React.Component {
});
}

componentDidUpdate() {
if (!this.props.tutorialsList) {
this.props.onInit({
tutorials: this.props.tutorials
});
}
}

render() {
const extraToolbarButtons = (stepId) => this.props.tutorials[stepId] ? [{
id: 'show-tutorial',
Expand Down
3 changes: 2 additions & 1 deletion web/client/components/plugins/PluginsContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class PluginsContainer extends React.Component {
mode: PropTypes.string,
params: PropTypes.object,
plugins: PropTypes.object,
allPlugins: PropTypes.object,
pluginsConfig: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
id: PropTypes.string,
className: PropTypes.string,
Expand Down Expand Up @@ -81,7 +82,7 @@ class PluginsContainer extends React.Component {

getChildContext() {
return {
plugins: this.props.plugins,
plugins: this.props.allPlugins ?? this.props.plugins,
pluginsConfig: this.props.pluginsConfig && this.getPluginsConfig(this.props),
loadedPlugins: this.state.loadedPlugins
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const withModulePlugins = (getPluginsConfigCallback = getPluginsConfig) => (Comp

const Loader = loaderComponent;

return loading ? <Loader /> : <Component {...props} pluginsConfig={pluginsConfig} plugins={parsedPlugins} />;
return loading ? <Loader /> : <Component {...props} pluginsConfig={pluginsConfig} plugins={parsedPlugins} allPlugins={plugins} />;
};


Expand Down
15 changes: 8 additions & 7 deletions web/client/hooks/useModulePlugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {useEffect, useMemo, useState} from 'react';
import {createPlugin, getPlugins, isMapStorePlugin} from '../utils/PluginsUtils';
import {createPlugin, getPlugins, isMapStorePlugin, normalizeName} from '../utils/PluginsUtils';
import {getStore} from '../utils/StateUtils';
import join from 'lodash/join';
import {size} from "lodash";
Expand Down Expand Up @@ -41,13 +41,14 @@ function useModulePlugins({
}) {
const [plugins, setPlugins] = useState(storedPlugins);
const [pending, setPending] = useState(true);

const normalizedEntries = useMemo(
() => Object.keys(pluginsEntries).reduce((prev, current) => ({...prev, [normalizeName(current)]: pluginsEntries[current]}), {}),
[pluginsEntries]
);
const pluginsKeys = useMemo(() => pluginsConfig.reduce((prev, curr) => {
const key = curr?.name ?? curr;
if (pluginsEntries[key]) {
const key = normalizeName(curr?.name ?? curr);
if (normalizedEntries[key]) {
return [ ...prev, key];
} else if (pluginsEntries[key + 'Plugin']) {
return [ ...prev, key + 'Plugin'];
}
return prev;
}, []),
Expand All @@ -61,7 +62,7 @@ function useModulePlugins({
setPending(true);
const loadPlugins = filteredPluginsKeys
.map(pluginName => {
return pluginsEntries[pluginName]().then((mod) => {
return normalizedEntries[pluginName]().then((mod) => {
return mod.default;
});
});
Expand Down
37 changes: 31 additions & 6 deletions web/client/plugins/ContextCreator.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,36 @@ import {createStructuredSelector} from 'reselect';

import ConfigUtils from '../utils/ConfigUtils';
import {createPlugin} from '../utils/PluginsUtils';
import {newContextSelector, resourceSelector, creationStepSelector, reloadConfirmSelector, showDialogSelector, isLoadingSelector,
loadFlagsSelector, isValidContextNameSelector, contextNameCheckedSelector, pluginsSelector, editedPluginSelector, editedCfgSelector,
validationStatusSelector, cfgErrorSelector, templatesSelector, parsedTemplateSelector, fileDropStatusSelector, editedTemplateSelector,
availablePluginsFilterTextSelector, availableTemplatesFilterTextSelector, enabledPluginsFilterTextSelector,
enabledTemplatesFilterTextSelector, showBackToPageConfirmationSelector, tutorialStepSelector, selectedThemeSelector,
customVariablesEnabledSelector, isNewContext} from '../selectors/contextcreator';
import {
newContextSelector,
resourceSelector,
creationStepSelector,
reloadConfirmSelector,
showDialogSelector,
isLoadingSelector,
loadFlagsSelector,
isValidContextNameSelector,
contextNameCheckedSelector,
pluginsSelector,
editedPluginSelector,
editedCfgSelector,
validationStatusSelector,
cfgErrorSelector,
templatesSelector,
parsedTemplateSelector,
fileDropStatusSelector,
editedTemplateSelector,
availablePluginsFilterTextSelector,
availableTemplatesFilterTextSelector,
enabledPluginsFilterTextSelector,
enabledTemplatesFilterTextSelector,
showBackToPageConfirmationSelector,
tutorialStepSelector,
selectedThemeSelector,
customVariablesEnabledSelector,
isNewContext,
tutorialsSelector
} from '../selectors/contextcreator';
import {mapTypeSelector} from '../selectors/maptype';
import {tutorialSelector} from '../selectors/tutorial';
import {init, setCreationStep, changeAttribute, saveNewContext, saveTemplate, mapViewerReload, showMapViewerReloadConfirm, showDialog, setFilterText,
Expand All @@ -33,6 +57,7 @@ export const contextCreatorSelector = createStructuredSelector({
user: userSelector,
curStepId: creationStepSelector,
tutorialStatus: state => tutorialSelector(state)?.status,
tutorialsList: tutorialsSelector,
tutorialStep: tutorialStepSelector,
newContext: newContextSelector,
resource: resourceSelector,
Expand Down
2 changes: 1 addition & 1 deletion web/client/plugins/Language.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default {
LanguagePlugin: assign(LangBar, {
OmniBar: {
name: 'language',
position: 4,
position: 5,
tool: true,
priority: 1
}
Expand Down
3 changes: 2 additions & 1 deletion web/client/product/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import {toModulePlugin} from "../utils/ModulePluginsUtils";

import Context from "../plugins/Context";
import ContextCreator from "../plugins/ContextCreator";
import Dashboard from "../plugins/Dashboard";
import Dashboards from "../plugins/Dashboards";
import FeedbackMask from '../plugins/FeedbackMask';
Expand All @@ -26,6 +27,7 @@ import UserSession from "../plugins/UserSession";
*/
export const plugins = {
// ### STATIC PLUGINS ### //
ContextCreatorPlugin: ContextCreator,
ContextPlugin: Context,
Dashboard: Dashboard,
DashboardsPlugin: Dashboards,
Expand Down Expand Up @@ -58,7 +60,6 @@ export const plugins = {
BurgerMenuPlugin: toModulePlugin('BurgerMenu', () => import(/* webpackChunkName: 'plugins/burgerMenu' */ '../plugins/BurgerMenu')),
CRSSelectorPlugin: toModulePlugin('CRSSelector', () => import(/* webpackChunkName: 'plugins/CRSSelector' */ '../plugins/CRSSelector')),
ContentTabs: toModulePlugin('ContentTabs', () => import(/* webpackChunkName: 'plugins/contentTabs' */ '../plugins/ContentTabs')),
ContextCreatorPlugin: toModulePlugin('ContextCreator', () => import(/* webpackChunkName: 'plugins/contextCreator' */ '../plugins/ContextCreator')),
ContextManagerPlugin: toModulePlugin('ContextManager', () => import(/* webpackChunkName: 'plugins/contextManager' */ '../plugins/contextmanager/ContextManager')),
ContextsPlugin: toModulePlugin('Contexts', () => import(/* webpackChunkName: 'plugins/contexts' */ '../plugins/Contexts')),
CookiePlugin: toModulePlugin('Cookie', () => import(/* webpackChunkName: 'plugins/cookie' */ '../plugins/Cookie')),
Expand Down
78 changes: 58 additions & 20 deletions web/client/utils/StateUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {semaphore, wrapEpics} from "./EpicsUtils";
import ConfigUtils from './ConfigUtils';
import isEmpty from 'lodash/isEmpty';
import {BehaviorSubject, Subject} from 'rxjs';
import {normalizeName} from "./PluginsUtils";

/**
* Returns a list of standard ReduxJS middlewares, augmented with user ones.
Expand Down Expand Up @@ -118,10 +117,17 @@ export const getState = (name) => {
return !isEmpty(getStore(name)) && getStore(name)?.getState() || {};
};

const isolateEpics = (epics, muteState$) => {
const isolateEpic = (epic) => (action$, store) => epic(action$.let(semaphore(muteState$.startWith(true))), store, muteState$).let(semaphore(
muteState$.startWith(true)
));
const isolateEpics = (epics, muteState) => {
const isolateEpic = (epic, name) => (action$, store, options) => {
const modifiedOptions = options ?? {};
muteState[name] = new Subject();
const pluginRenderStream$ = muteState[name].asObservable();

modifiedOptions.pluginRenderStream$ = pluginRenderStream$;
return epic(action$.let(semaphore(pluginRenderStream$.startWith(true))), store, modifiedOptions).let(semaphore(
pluginRenderStream$.startWith(true)
));
};
return Object.entries(epics).reduce((out, [k, epic]) => ({ ...out, [k]: isolateEpic(epic) }), {});
};

Expand All @@ -130,19 +136,28 @@ export const createStoreManager = (initialReducers, initialEpics) => {
// Create an object which maps keys to reducers
const reducers = {...initialReducers};
const epics = {...initialEpics};
const addedEpics = {};
const epicsListenedBy = {};
const epicRegistrations = {};
const groupedByModule = {};
let muteState = {};

// Create the initial combinedReducer
let combinedReducer = combineReducers(reducers);

const subject = new Subject();
subject.next(true);
const isolated = isolateEpics(epics, subject.asObservable());
const epic$ = new BehaviorSubject(combineEpics(...wrapEpics(isolated)));
// appEpics should not be mutable, therefore do not add them into muteState
const isolated = isolateEpics(epics, []);

const epic$ = new BehaviorSubject(combineEpics(...wrapEpics(isolated)));
// An array which is used to delete state keys when reducers are removed
let keysToRemove = [];

let muteState = {};
const addToRegistry = (module, epicName) => {
epicRegistrations[epicName] = [...(epicRegistrations[epicName] ?? []), module];
groupedByModule[module] = [...(groupedByModule[module] ?? []), epicName];
epicsListenedBy[epicName] = [...(epicsListenedBy[epicName] ?? []), module];
};

return {
getReducerMap: () => reducers,
Expand Down Expand Up @@ -195,24 +210,47 @@ export const createStoreManager = (initialReducers, initialEpics) => {
// Adds a new epics set, mutable by the specified key
addEpics: (key, epicsList) => {
if (Object.keys(epicsList).length) {
const normalizedName = normalizeName(key);
muteState[normalizedName] = new Subject();
const isolatedEpics = isolateEpics(epicsList, muteState[normalizedName].asObservable());
const epicsToAdd = Object.keys(epicsList).reduce((prev, current) => {
if (!addedEpics[current]) {
addedEpics[current] = key;
addToRegistry(key, current);
return ({...prev, [current]: epicsList[current]});
}
addToRegistry(key, current);
return prev;
}, {});
const isolatedEpics = isolateEpics(epicsToAdd, muteState);
wrapEpics(isolatedEpics).forEach(epic => epic$.next(epic));
}
},
// Mute epics set with a specified key
muteEpics: (key) => {
const normalizedName = normalizeName(key);
if (typeof muteState[normalizedName] !== 'undefined') {
muteState[normalizedName].next(false);
}
const moduleEpicRegistrations = groupedByModule[key];
// try to mute everything registered by module. If epic is shared, remove current module from epicsListenedBy
moduleEpicRegistrations && moduleEpicRegistrations.forEach(epicName => {
const indexOf = epicsListenedBy[epicName].indexOf(key);
if (indexOf >= 0) {
delete epicsListenedBy[epicName][indexOf];
}
// check if epic is still listened by anything. If not - mute it
if (!epicsListenedBy[epicName].length) {
muteState[epicName].next(false);
}
});
},
unmuteEpics: (key) => {
const normalizedName = normalizeName(key);
if (typeof muteState[normalizedName] !== 'undefined') {
muteState[normalizedName].next(true);
}
const moduleEpicRegistrations = groupedByModule[key];
// unmute epics if exactly one plugin wants to register specific epic
moduleEpicRegistrations && moduleEpicRegistrations.forEach(epicName => {
const indexOf = epicsListenedBy[epicName].indexOf(key);
if (indexOf === -1) {
epicsListenedBy[epicName].push(key);
}
// now if epic intended to be registered by first listener plugin - unmute it
if (epicsListenedBy[epicName].length === 1) {
muteState[epicName].next(true);
}
});
},
rootEpic: (...args) => epic$.mergeMap(e => e(...args))
};
Expand Down

0 comments on commit 7862c7d

Please sign in to comment.