Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#8503 Bugs related modular plugins implementation #8495

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
0c2e78a
Make language plugin have higher weight than home button in Omnibar.
alexander-fedorenko Aug 18, 2022
9f910fc
PluginsContainer will get full list of plugins in allPlugins property…
alexander-fedorenko Aug 18, 2022
1877320
Register only one instance of epic if there is an attempt to register…
alexander-fedorenko Aug 18, 2022
5d776a3
Regression fix - properly handle mute state per specific epic, make s…
alexander-fedorenko Aug 19, 2022
af020a2
Making pluginRenderStream$ become a part of third argument per epic.
alexander-fedorenko Aug 19, 2022
504590e
Add pluginRenderStream to the appEpics, but keep them not mutable
alexander-fedorenko Aug 19, 2022
a1fb9eb
Alternative approach of delayed dispatching of events that load maps,…
alexander-fedorenko Aug 18, 2022
09fdf3f
Aligning pages delayed configuration loading with the fact that all m…
alexander-fedorenko Aug 18, 2022
951a1cb
Aligning MapViewerComponent with changes to check plugin render state.
alexander-fedorenko Aug 19, 2022
d7b3179
Passing allPlugins prop.
alexander-fedorenko Aug 18, 2022
85b56c6
Making tutorials on context creator work again. Issue was caused by f…
alexander-fedorenko Aug 19, 2022
2e0f9ae
Revert "Aligning MapViewerComponent with changes to check plugin rend…
alexander-fedorenko Aug 19, 2022
3a54f0f
Revert "Aligning pages delayed configuration loading with the fact th…
alexander-fedorenko Aug 19, 2022
c11a6ab
Revert "Alternative approach of delayed dispatching of events that lo…
alexander-fedorenko Aug 19, 2022
1b71674
Making ContextCreator static to fix regression.
alexander-fedorenko Aug 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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