Skip to content
Snippets Groups Projects
Commit 6bb92217 authored by mateusz-winiarczyk's avatar mateusz-winiarczyk
Browse files

Merge branch 'MIN-284-multiple-plugins' into 'development'

fix(plugins): multiple plugins (MIN-284)

Closes MIN-284

See merge request !183
parents 59979c47 c8757da6
No related branches found
No related tags found
2 merge requests!223reset the pin numbers before search results are fetch (so the results will be...,!183fix(plugins): multiple plugins (MIN-284)
Pipeline #89053 passed
Showing
with 517 additions and 151 deletions
......@@ -2,6 +2,7 @@ import { DrawerHeading } from '@/shared/DrawerHeading';
import { LoadPluginFromUrl } from './LoadPluginFromUrl';
import { PublicPlugins } from './PublicPlugins';
import { PrivateActivePlugins } from './PrivateActivePlugins';
import { PublicActivePlugins } from './PublicActivePlugins';
export const AvailablePluginsDrawer = (): JSX.Element => {
return (
......@@ -10,6 +11,7 @@ export const AvailablePluginsDrawer = (): JSX.Element => {
<div className="flex h-[calc(100%-93px)] max-h-[calc(100%-93px)] flex-col gap-6 overflow-y-auto p-6">
<LoadPluginFromUrl />
<PrivateActivePlugins />
<PublicActivePlugins />
<PublicPlugins />
</div>
</div>
......
......@@ -199,5 +199,82 @@ describe('useLoadPlugin', () => {
},
]);
});
it('should unload plugin from plugins manager', () => {
const unloadActivePluginSpy = jest.spyOn(PluginsManager, 'unloadActivePlugin');
const { Wrapper } = getReduxStoreWithActionsListener(STATE_MOCK);
PluginsManager.activePlugins = {
[pluginFixture.hash]: [pluginFixture.hash],
};
const {
result: {
current: { togglePlugin },
},
} = renderHook(
() => useLoadPlugin({ hash: pluginFixture.hash, pluginUrl: pluginFixture.urls[0] }),
{
wrapper: Wrapper,
},
);
act(() => {
togglePlugin();
});
expect(unloadActivePluginSpy).toHaveBeenCalledWith(pluginFixture.hash);
});
it('should set last active plugin as current plugin tab in drawer', () => {
const firstHash = 'hash1';
const secondHash = 'hash2';
const { Wrapper, store } = getReduxStoreWithActionsListener({
...STATE_MOCK,
plugins: {
...STATE_MOCK.plugins,
activePlugins: {
data: {
[firstHash]: {
...pluginFixture,
hash: firstHash,
},
[secondHash]: {
...pluginFixture,
hash: secondHash,
},
},
pluginsId: [firstHash, secondHash],
},
},
});
const {
result: {
current: { togglePlugin },
},
} = renderHook(() => useLoadPlugin({ hash: secondHash, pluginUrl: pluginFixture.urls[0] }), {
wrapper: Wrapper,
});
act(() => {
togglePlugin();
});
const actions = store.getActions();
expect(actions).toEqual([
{
payload: { pluginId: secondHash },
type: 'plugins/removePlugin',
},
{
payload: firstHash,
type: 'plugins/setCurrentDrawerPluginHash',
},
{
payload: secondHash,
type: 'legend/removePluginLegend',
},
]);
});
});
});
......@@ -3,11 +3,12 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { isActiveLegendSelector } from '@/redux/legend/legend.selectors';
import { removePluginLegend, setDefaultLegendId } from '@/redux/legend/legend.slice';
import {
allActivePluginsSelector,
isPluginActiveSelector,
isPluginLoadingSelector,
isPluginSelectedSelector,
} from '@/redux/plugins/plugins.selectors';
import { removePlugin } from '@/redux/plugins/plugins.slice';
import { removePlugin, setCurrentDrawerPluginHash } from '@/redux/plugins/plugins.slice';
import { PluginsManager } from '@/services/pluginsManager';
import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus';
import { getErrorMessage } from '@/utils/getErrorMessage';
......@@ -40,9 +41,18 @@ export const useLoadPlugin = ({
const isPluginLoading = useAppSelector(state => isPluginLoadingSelector(state, hash));
const isPluginSelected = useAppSelector(state => isPluginSelectedSelector(state, hash));
const isActivePluginLegend = useAppSelector(state => isActiveLegendSelector(state, hash));
const allActivePlugins = useAppSelector(allActivePluginsSelector);
const dispatch = useAppDispatch();
const setLastPluginAsCurrentActivePlugin = (): void => {
const newAllActivePlugins = allActivePlugins.filter(p => p.hash !== hash);
const lastActivePlugin = newAllActivePlugins.pop();
if (lastActivePlugin) {
dispatch(setCurrentDrawerPluginHash(lastActivePlugin.hash));
}
};
const handleLoadPlugin = async (): Promise<void> => {
try {
const response = await axios(pluginUrl);
......@@ -83,9 +93,14 @@ export const useLoadPlugin = ({
const handleUnloadPlugin = (): void => {
dispatch(removePlugin({ pluginId: hash }));
setLastPluginAsCurrentActivePlugin();
handleRemoveLegend();
PluginsManager.removePluginContent({ hash });
PluginsManager.unloadActivePlugin(hash);
PluginsEventBus.dispatchEvent('onPluginUnload', { hash });
};
......
/* eslint-disable no-magic-numbers */
import { pluginFixture } from '@/models/fixtures/pluginFixture';
import { apiPath } from '@/redux/apiPath';
import {
PLUGINS_INITIAL_STATE_LIST_MOCK,
PLUGINS_INITIAL_STATE_MOCK,
} from '@/redux/plugins/plugins.mock';
import { StoreType } from '@/redux/store';
import { mockNetworkResponse } from '@/utils/mockNetworkResponse';
import { InitialStoreState } from '@/utils/testing/getReduxStoreActionsListener';
......@@ -89,53 +85,7 @@ describe('LoadPluginFromUrl - component', () => {
expect(button).toBeDisabled();
});
it('should not load plugin if it`s loaded already', async () => {
global.URL.canParse = jest.fn().mockReturnValue(true);
const plugin = {
...pluginFixture,
urls: ['https://example.com/min.js'],
};
mockedAxiosClient.onGet(plugin.urls[0]).reply(HttpStatusCode.Ok, '');
const { store } = renderComponent({
plugins: {
...PLUGINS_INITIAL_STATE_MOCK,
activePlugins: {
pluginsId: [plugin.hash],
data: {
[plugin.hash]: plugin,
},
},
list: PLUGINS_INITIAL_STATE_LIST_MOCK,
},
});
const input = screen.getByTestId('load-plugin-input-url');
expect(input).toBeVisible();
act(() => {
fireEvent.change(input, { target: { value: plugin.urls[0] } });
});
expect(input).toHaveValue(plugin.urls[0]);
const button = screen.getByTestId('load-plugin-button');
expect(button).not.toBeDisabled();
await act(() => {
button.click();
});
const { activePlugins } = store.getState().plugins;
expect(activePlugins).toEqual({
pluginsId: [plugin.hash],
data: {
[plugin.hash]: plugin,
},
});
expect(input).toHaveValue('');
});
it('should disable url input if url is empty', async () => {
renderComponent();
const input = screen.getByTestId('load-plugin-input-url');
......@@ -195,62 +145,7 @@ describe('LoadPluginFromUrl - component', () => {
});
});
});
it('should set plugin active tab in drawer as loaded plugin', async () => {
const pluginUrl = 'http://example.com/plugin.js';
const pluginScript = `function init() {} init()`;
mockedAxiosClient.onGet(pluginUrl).reply(HttpStatusCode.Ok, pluginScript);
global.URL.canParse = jest.fn().mockReturnValue(true);
const { store } = renderComponent();
const dispatchSpy = jest.spyOn(store, 'dispatch');
const input = screen.getByTestId('load-plugin-input-url');
expect(input).toBeVisible();
act(() => {
fireEvent.change(input, { target: { value: pluginUrl } });
});
const button = screen.getByTestId('load-plugin-button');
act(() => {
button.click();
});
await waitFor(() => {
expect(dispatchSpy).toHaveBeenCalledWith({
payload: 'e008fb2ceb97e3d6139ffe38a1b39d5d',
type: 'plugins/setCurrentDrawerPluginHash',
});
});
});
it('should load plugin from url after pressing enter key', async () => {
const pluginUrl = 'http://example.com/plugin.js';
const pluginScript = `function init() {} init()`;
mockedAxiosClient.onGet(pluginUrl).reply(HttpStatusCode.Ok, pluginScript);
global.URL.canParse = jest.fn().mockReturnValue(true);
const { store } = renderComponent();
const dispatchSpy = jest.spyOn(store, 'dispatch');
const input = screen.getByTestId('load-plugin-input-url');
act(() => {
fireEvent.change(input, { target: { value: pluginUrl } });
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter', charCode: 13 });
});
await waitFor(() => {
expect(dispatchSpy).toHaveBeenCalledWith({
payload: 'e008fb2ceb97e3d6139ffe38a1b39d5d',
type: 'plugins/setCurrentDrawerPluginHash',
});
});
});
it('should not load plugin from url after pressing enter key if url is not correct', async () => {
global.URL.canParse = jest.fn().mockReturnValue(false);
const { store } = renderComponent();
......
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { activePluginsDataSelector } from '@/redux/plugins/plugins.selectors';
import { setCurrentDrawerPluginHash } from '@/redux/plugins/plugins.slice';
import { PluginsManager } from '@/services/pluginsManager';
import { showToast } from '@/utils/showToast';
import axios from 'axios';
......@@ -21,25 +17,19 @@ type UseLoadPluginReturnType = {
export const useLoadPluginFromUrl = (): UseLoadPluginReturnType => {
const [pluginUrl, setPluginUrl] = useState('');
const [isLoading, setIsLoading] = useState(false);
const activePlugins = useAppSelector(activePluginsDataSelector);
const dispatch = useAppDispatch();
const isPending = useMemo(
() => !pluginUrl || isLoading || !URL.canParse(pluginUrl),
[pluginUrl, isLoading],
);
const handleSetCurrentDrawerPluginHash = (hash: string): void => {
dispatch(setCurrentDrawerPluginHash(hash));
};
const handleLoadPlugin = async (): Promise<void> => {
try {
setIsLoading(true);
const response = await axios(pluginUrl);
let pluginScript = response.data;
const hash = PluginsManager.setHashedPlugin({
PluginsManager.setHashedPlugin({
pluginUrl,
pluginScript,
});
......@@ -49,13 +39,9 @@ export const useLoadPluginFromUrl = (): UseLoadPluginReturnType => {
/* eslint-disable no-new-func */
const loadPlugin = new Function(pluginScript);
if (!(hash in activePlugins)) {
loadPlugin();
}
loadPlugin();
setPluginUrl('');
handleSetCurrentDrawerPluginHash(hash);
} catch (error) {
const errorMessage = getErrorMessage({ error, prefix: PLUGIN_LOADING_ERROR_PREFIX });
showToast({
......
import { publicActivePluginsSelector } from '@/redux/plugins/plugins.selectors';
import { useSelector } from 'react-redux';
import { LoadPlugin } from '../LoadPlugin';
export const PublicActivePlugins = (): React.ReactNode => {
const publicActivePlugins = useSelector(publicActivePluginsSelector);
return publicActivePlugins.map(plugin => <LoadPlugin key={plugin.hash} plugin={plugin} />);
};
export { PublicActivePlugins } from './PublicActivePlugins.component';
import { ZERO } from '@/constants/common';
import { publicPluginsListWithoutActiveSelector } from '@/redux/plugins/plugins.selectors';
import { publicPluginsListSelector } from '@/redux/plugins/plugins.selectors';
import { Button } from '@/shared/Button';
import { MinervaPlugin } from '@/types/models';
import { useSelect } from 'downshift';
......@@ -7,7 +7,7 @@ import { useSelector } from 'react-redux';
import { LoadPluginElement } from './LoadPluginElement';
export const PluginOpenButton = (): JSX.Element | null => {
const publicPlugins = useSelector(publicPluginsListWithoutActiveSelector);
const publicPlugins = useSelector(publicPluginsListSelector);
const { isOpen, getToggleButtonProps, getMenuProps } = useSelect({
items: publicPlugins,
......
......@@ -124,11 +124,11 @@ describe('PluginSingleTab - component', () => {
expect(actions).toEqual([
{ payload: { pluginId: '5e3fcb59588cc311ef9839feea6382eb' }, type: 'plugins/removePlugin' },
{ payload: '5e3fcb59588cc311ef9839feea6382eb', type: 'legend/removePluginLegend' },
{
payload: '5314b9f996e56e67f0dad65e7df8b73b',
type: 'plugins/setCurrentDrawerPluginHash',
},
{ payload: '5e3fcb59588cc311ef9839feea6382eb', type: 'legend/removePluginLegend' },
]);
});
......
......@@ -2,8 +2,6 @@
import { useLoadPlugin } from '@/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin';
import { FIRST_ARRAY_ELEMENT, ZERO } from '@/constants/common';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { allActivePluginsSelector } from '@/redux/plugins/plugins.selectors';
import { setCurrentDrawerPluginHash } from '@/redux/plugins/plugins.slice';
import { Button } from '@/shared/Button';
import { Icon } from '@/shared/Icon';
......@@ -16,7 +14,6 @@ interface Props {
export const PluginSingleTab = ({ plugin }: Props): JSX.Element => {
const dispatch = useAppDispatch();
const allActivePlugins = useAppSelector(allActivePluginsSelector);
const { unloadPlugin, isPluginSelected } = useLoadPlugin({
hash: plugin.hash,
......@@ -30,12 +27,6 @@ export const PluginSingleTab = ({ plugin }: Props): JSX.Element => {
const onPluginUnload = (event: React.MouseEvent<HTMLDivElement>): void => {
event.stopPropagation();
unloadPlugin();
const newAllActivePlugins = allActivePlugins.filter(p => p.hash !== plugin.hash);
const lastActivePlugin = newAllActivePlugins.pop();
if (lastActivePlugin) {
dispatch(setCurrentDrawerPluginHash(lastActivePlugin.hash));
}
};
return (
......
export const PLUGINS_CONTENT_ELEMENT_ATTR_NAME = 'data-hash';
export const PLUGINS_CONTENT_ELEMENT_ID = 'plugins';
export const PLUGIN_HASH_PREFIX_SEPARATOR = '-';
......@@ -45,6 +45,7 @@ describe('plugins reducer', () => {
pluginName: pluginFixture.name,
pluginUrl: pluginFixture.urls[0],
pluginVersion: pluginFixture.version,
extendedPluginName: pluginFixture.name,
}),
);
......@@ -65,6 +66,7 @@ describe('plugins reducer', () => {
pluginName: pluginFixture.name,
pluginUrl: pluginFixture.urls[0],
pluginVersion: pluginFixture.version,
extendedPluginName: pluginFixture.name,
}),
);
......@@ -89,6 +91,7 @@ describe('plugins reducer', () => {
pluginName: pluginFixture.name,
pluginUrl: pluginFixture.urls[0],
pluginVersion: pluginFixture.version,
extendedPluginName: pluginFixture.name,
}),
);
......@@ -97,4 +100,47 @@ describe('plugins reducer', () => {
expect(data).toEqual({});
expect(pluginsId).toContain(pluginFixture.hash);
});
it('should store loaded plugin with extendedPluginName as a pluginName', async () => {
const extendedPluginName = `${pluginFixture.name} (2)`;
mockedAxiosClient.onPost(apiPath.registerPluign()).reply(HttpStatusCode.Ok, pluginFixture);
await store.dispatch(
registerPlugin({
hash: pluginFixture.hash,
isPublic: pluginFixture.isPublic,
pluginName: pluginFixture.name,
pluginUrl: pluginFixture.urls[0],
pluginVersion: pluginFixture.version,
extendedPluginName,
}),
);
const { data } = store.getState().plugins.activePlugins;
expect(data).toEqual({
[pluginFixture.hash]: {
...pluginFixture,
name: extendedPluginName,
},
});
});
it('should set loaded plugin as current active plugin in plugins drawer', async () => {
mockedAxiosClient.onPost(apiPath.registerPluign()).reply(HttpStatusCode.Ok, pluginFixture);
const { type } = await store.dispatch(
registerPlugin({
hash: pluginFixture.hash,
isPublic: pluginFixture.isPublic,
pluginName: pluginFixture.name,
pluginUrl: pluginFixture.urls[0],
pluginVersion: pluginFixture.version,
extendedPluginName: pluginFixture.name,
}),
);
expect(type).toBe('plugins/registerPlugin/fulfilled');
const { currentPluginHash } = store.getState().plugins.drawer;
expect(currentPluginHash).toBe(pluginFixture.hash);
});
});
......@@ -22,6 +22,7 @@ export const registerPluginReducer = (builder: ActionReducerMapBuilder<PluginsSt
const { hash } = action.meta.arg;
state.activePlugins.data[hash] = action.payload;
state.drawer.currentPluginHash = hash;
}
});
builder.addCase(registerPlugin.rejected, state => {
......
import { MinervaPlugin } from '@/types/models';
import { createSelector } from '@reduxjs/toolkit';
import { isPluginHashWithPrefix } from '@/utils/plugins/isPluginHashWithPrefix';
import { rootSelector } from '../root/root.selectors';
export const pluginsSelector = createSelector(rootSelector, state => state.plugins);
......@@ -59,6 +60,15 @@ export const privateActivePluginsSelector = createSelector(
},
);
export const publicActivePluginsSelector = createSelector(
allActivePluginsSelector,
activePlugins => {
return (activePlugins || []).filter(
plugin => plugin.isPublic && isPluginHashWithPrefix(plugin.hash),
);
},
);
export const isPluginActiveSelector = createSelector(
[activePluginsIdSelector, (_, activePlugin: string): string => activePlugin],
(activePlugins, activePlugin) => activePlugins.includes(activePlugin),
......
......@@ -7,6 +7,7 @@ import {
ToolkitStoreWithSingleSlice,
createStoreInstanceUsingSliceReducer,
} from '@/utils/createStoreInstanceUsingSliceReducer';
import { ONE, TWO } from '@/constants/common';
import { apiPath } from '../apiPath';
import { PluginsState } from './plugins.types';
import pluginsReducer from './plugins.slice';
......@@ -28,15 +29,20 @@ describe('plugins - thunks', () => {
});
it('should fetch and load initial plugins', async () => {
mockedAxiosApiClient.onPost(apiPath.registerPluign()).reply(HttpStatusCode.Ok, pluginFixture);
mockedAxiosApiClient
.onGet(apiPath.getPlugin(pluginFixture.hash))
.reply(HttpStatusCode.Ok, pluginFixture);
const hash = 'hash';
mockedAxiosApiClient.onPost(apiPath.registerPluign()).reply(HttpStatusCode.Ok, {
...pluginFixture,
hash,
});
mockedAxiosApiClient.onGet(apiPath.getPlugin('hash')).reply(HttpStatusCode.Ok, {
...pluginFixture,
hash,
});
mockedAxiosClient.onGet(pluginFixture.urls[0]).reply(HttpStatusCode.Ok, '');
await store.dispatch(
getInitPlugins({
pluginsId: [pluginFixture.hash],
pluginsId: [hash],
setHashedPlugin: setHashedPluginMock,
}),
);
......@@ -59,5 +65,38 @@ describe('plugins - thunks', () => {
expect(setHashedPluginMock).not.toHaveBeenCalled();
});
it('should allow to load one plugin multiple times with the same data', async () => {
const hash = 'hash';
const hashWithPrefix = `prefix-${hash}`;
const script = 'function init() {} init()';
mockedAxiosApiClient.onPost(apiPath.registerPluign()).reply(HttpStatusCode.Ok, {
...pluginFixture,
hash,
});
mockedAxiosApiClient.onGet(apiPath.getPlugin(hash)).reply(HttpStatusCode.Ok, {
...pluginFixture,
hash,
});
mockedAxiosClient.onGet(pluginFixture.urls[0]).reply(HttpStatusCode.Ok, script);
await store.dispatch(
getInitPlugins({
pluginsId: [hash, hashWithPrefix],
setHashedPlugin: setHashedPluginMock,
}),
);
expect(setHashedPluginMock).toHaveBeenCalledTimes(2);
expect(setHashedPluginMock).toHaveBeenNthCalledWith(ONE, {
pluginScript: script,
pluginUrl: pluginFixture.urls[0],
});
expect(setHashedPluginMock).toHaveBeenNthCalledWith(TWO, {
pluginScript: script,
pluginUrl: pluginFixture.urls[0],
});
});
});
});
/* eslint-disable no-magic-numbers */
/* eslint-disable no-param-reassign */
import { pluginSchema } from '@/models/pluginSchema';
import { axiosInstance } from '@/services/api/utils/axiosInstance';
import type { MinervaPlugin } from '@/types/models';
......@@ -7,6 +8,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
import { getErrorMessage } from '@/utils/getErrorMessage';
import { ThunkConfig } from '@/types/store';
import { getPluginHashWithoutPrefix } from '@/utils/plugins/getPluginHashWithoutPrefix';
import { apiPath } from '../apiPath';
import {
PLUGIN_FETCHING_ALL_ERROR_PREFIX,
......@@ -20,6 +22,7 @@ type RegisterPlugin = {
pluginName: string;
pluginVersion: string;
isPublic: boolean;
extendedPluginName: string;
};
export const registerPlugin = createAsyncThunk<
......@@ -28,10 +31,15 @@ export const registerPlugin = createAsyncThunk<
ThunkConfig
>(
'plugins/registerPlugin',
async ({ hash, isPublic, pluginName, pluginUrl, pluginVersion }, { rejectWithValue }) => {
async (
{ hash, isPublic, pluginName, pluginUrl, pluginVersion, extendedPluginName },
{ rejectWithValue },
) => {
try {
const hashWihtoutPrefix = getPluginHashWithoutPrefix(hash);
const payload = {
hash,
hash: hashWihtoutPrefix,
url: pluginUrl,
name: pluginName,
version: pluginVersion,
......@@ -49,7 +57,11 @@ export const registerPlugin = createAsyncThunk<
const isDataValid = validateDataUsingZodSchema(response.data, pluginSchema);
if (isDataValid) {
return response.data;
return {
...response.data,
hash,
name: extendedPluginName,
};
}
return undefined;
......@@ -77,8 +89,11 @@ export const getInitPlugins = createAsyncThunk<void, GetInitPluginsProps, ThunkC
async ({ pluginsId, setHashedPlugin }, { rejectWithValue }) => {
try {
/* eslint-disable no-restricted-syntax, no-await-in-loop */
for (const pluginId of pluginsId) {
const res = await axiosInstance<MinervaPlugin>(apiPath.getPlugin(pluginId));
const hash = getPluginHashWithoutPrefix(pluginId);
const res = await axiosInstance<MinervaPlugin>(apiPath.getPlugin(hash));
const isDataValid = validateDataUsingZodSchema(res.data, pluginSchema);
......
/* eslint-disable no-magic-numbers */
import { ZERO } from '@/constants/common';
import { ONE, TWO, ZERO } from '@/constants/common';
import { PLUGINS_CONTENT_ELEMENT_ATTR_NAME, PLUGINS_CONTENT_ELEMENT_ID } from '@/constants/plugins';
import { configurationFixture } from '@/models/fixtures/configurationFixture';
import { store } from '@/redux/store';
import { registerPlugin } from '@/redux/plugins/plugins.thunks';
import { PluginsManager } from './pluginsManager';
import { configurationMapper } from './pluginsManager.utils';
import { ERROR_PLUGIN_URL_MISMATCH } from './errorMessages';
jest.mock('../../redux/store');
jest.mock('../../redux/plugins/plugins.thunks');
jest.mock('uuid', () => ({
v4: jest.fn().mockReturnValue('uuidValue'),
}));
const createWrapperInDocument = (): HTMLDivElement => {
const wrapper = document.createElement('div');
......@@ -149,4 +155,196 @@ describe('PluginsManager', () => {
}),
).toThrow(ERROR_PLUGIN_URL_MISMATCH);
});
it('should allow load multiple plugins', () => {
const pluginName = 'TestPlugin';
const pluginVersion = '1.0.0';
const pluginUrl = 'https://example.com/test-plugin.js';
const hash = '128ce10ae1b46ec4bc6d7c07278b5c9e';
PluginsManager.activePlugins = {};
PluginsManager.pluginsOccurrences = {};
PluginsManager.hashedPlugins = {
[pluginUrl]: hash,
};
PluginsManager.registerPlugin({ pluginName, pluginVersion, pluginUrl });
PluginsManager.registerPlugin({ pluginName, pluginVersion, pluginUrl });
PluginsManager.registerPlugin({ pluginName, pluginVersion, pluginUrl });
expect(registerPlugin).toHaveBeenNthCalledWith(
ONE,
expect.objectContaining({
isPublic: false,
extendedPluginName: 'TestPlugin',
pluginName: 'TestPlugin',
}),
);
expect(registerPlugin).toHaveBeenNthCalledWith(
TWO,
expect.objectContaining({
isPublic: false,
extendedPluginName: 'TestPlugin (1)',
pluginName: 'TestPlugin',
}),
);
expect(registerPlugin).toHaveBeenNthCalledWith(
3,
expect.objectContaining({
isPublic: false,
extendedPluginName: 'TestPlugin (2)',
pluginName: 'TestPlugin',
}),
);
});
describe('updatePluginOccurrence', () => {
it('should increment occurrence if hash exists in pluginsOccurrences and extendedHash has prefix', () => {
const hash = 'someHash';
const extendedHash = 'prefix-someHash';
PluginsManager.pluginsOccurrences = { [hash]: 1 };
const expectedPluginsOccurrences = { [hash]: 2 };
PluginsManager.updatePluginOccurrence(hash, extendedHash);
expect(PluginsManager.pluginsOccurrences).toEqual(expectedPluginsOccurrences);
});
it('should initialize occurrence if hash does not exist in pluginsOccurrences', () => {
const hash = 'someHash';
const extendedHash = 'someHash-suffix';
PluginsManager.pluginsOccurrences = {};
const expectedPluginsOccurrences = { [hash]: ZERO };
PluginsManager.updatePluginOccurrence(hash, extendedHash);
expect(PluginsManager.pluginsOccurrences).toEqual(expectedPluginsOccurrences);
});
it('should not modify occurrence if extendedHash does not have prefix', () => {
const hash = 'someHash';
const extendedHash = 'someHash';
PluginsManager.pluginsOccurrences = { [hash]: ONE };
PluginsManager.updatePluginOccurrence(hash, extendedHash);
expect(PluginsManager.pluginsOccurrences).toEqual({ [hash]: ONE });
});
});
describe('getExtendedPluginName', () => {
it('should return pluginName if plugin is not active', () => {
const hash = 'someHash';
const extendedHash = 'someHash-suffix';
const pluginName = 'Some Plugin';
PluginsManager.activePlugins = {
[hash]: [hash],
};
const result = PluginsManager.getExtendedPluginName(hash, extendedHash, pluginName);
expect(result).toBe(pluginName);
});
it('should return pluginName without number if hash does not have prefix', () => {
const hash = 'someHash';
const extendedHash = 'someOtherHash';
const pluginName = 'Some Plugin';
PluginsManager.activePlugins = {
[hash]: [hash, 'hash1'],
};
const result = PluginsManager.getExtendedPluginName(hash, extendedHash, pluginName);
expect(result).toBe(pluginName);
});
it('should return pluginName with number if hash has prefix and there is more active plugins than 1', () => {
const hash = 'someHash';
const extendedHash = 'suffix-someHash';
const pluginName = 'Some Plugin';
PluginsManager.activePlugins = {
[hash]: ['somePluginId1', 'somePluginId2'],
};
const result = PluginsManager.getExtendedPluginName(hash, extendedHash, pluginName);
expect(result).toBe(`${pluginName} (${ONE})`);
});
});
describe('getExtendedPluginHash', () => {
beforeEach(() => {
PluginsManager.activePlugins = {};
});
it('should return the original hash if it is not in active plugins', () => {
const hash = 'someHash';
const expectedExtendedHash = hash;
const result = PluginsManager.getExtendedPluginHash(hash);
expect(result).toBe(expectedExtendedHash);
});
it('should return hash with prefix if the original hash is in active plugins', () => {
const hash = 'someHash';
PluginsManager.activePlugins = {
[hash]: [hash],
};
const result = PluginsManager.getExtendedPluginHash(hash);
expect(result).toBe('a7d06e6d48c9cd8b7726c5d91238408f-someHash');
});
});
describe('unloadActivePlugin', () => {
beforeEach(() => {
PluginsManager.activePlugins = {};
PluginsManager.pluginsOccurrences = {};
});
it('should remove the hash from activePlugins and set occurrences to zero if the hash exists in activePlugins and its occurrences are zero after removal', () => {
const hash = 'someHash';
PluginsManager.activePlugins[hash] = [hash];
PluginsManager.pluginsOccurrences[hash] = 0;
PluginsManager.unloadActivePlugin(hash);
expect(PluginsManager.activePlugins[hash]).toEqual([]);
expect(PluginsManager.pluginsOccurrences[hash]).toBe(0);
});
it('should remove the hash from activePlugins and not modify occurrences if some hash exists in activePlugins', () => {
const hash = 'someHash';
const hashWithPrefix = `xxx-${hash}`;
const occurrences = 2;
PluginsManager.activePlugins[hash] = [hash, hashWithPrefix];
PluginsManager.pluginsOccurrences[hash] = occurrences;
PluginsManager.unloadActivePlugin(hashWithPrefix);
expect(PluginsManager.activePlugins[hash]).toEqual([hash]);
expect(PluginsManager.pluginsOccurrences[hash]).toBe(occurrences);
});
it('should not modify activePlugins or occurrences if the hash does not exist in activePlugins', () => {
const hash = 'hash';
const hashWithPrefix = `xxx-aaa`;
const occurrences = 1;
PluginsManager.activePlugins[hash] = [hash];
PluginsManager.pluginsOccurrences[hash] = occurrences;
PluginsManager.unloadActivePlugin(hashWithPrefix);
expect(PluginsManager.activePlugins[hash]).toEqual([hash]);
expect(PluginsManager.pluginsOccurrences[hash]).toEqual(occurrences);
});
});
});
/* eslint-disable no-magic-numbers */
import { PLUGINS_CONTENT_ELEMENT_ATTR_NAME, PLUGINS_CONTENT_ELEMENT_ID } from '@/constants/plugins';
import { registerPlugin } from '@/redux/plugins/plugins.thunks';
import { store } from '@/redux/store';
import md5 from 'crypto-js/md5';
import { v4 as uuidv4 } from 'uuid';
import { isPluginHashWithPrefix } from '@/utils/plugins/isPluginHashWithPrefix';
import { getPluginHashWithoutPrefix } from '@/utils/plugins/getPluginHashWithoutPrefix';
import { ONE, ZERO } from '@/constants/common';
import { bioEntitiesMethods } from './bioEntities';
import { getModels } from './map/models/getModels';
import { openMap } from './map/openMap';
......@@ -46,6 +51,22 @@ export const PluginsManager: PluginsManagerType = {
return hash;
},
activePlugins: {},
pluginsOccurrences: {},
unloadActivePlugin: hash => {
const hashWihtoutPrefix = getPluginHashWithoutPrefix(hash);
PluginsManager.activePlugins[hashWihtoutPrefix] =
PluginsManager.activePlugins[hashWihtoutPrefix]?.filter(el => el !== hash) || [];
if (
PluginsManager.activePlugins[hashWihtoutPrefix].length === ZERO &&
hashWihtoutPrefix in PluginsManager.pluginsOccurrences
) {
PluginsManager.pluginsOccurrences[hashWihtoutPrefix] = ZERO;
}
},
init() {
window.minerva = {
plugins: {
......@@ -112,36 +133,70 @@ export const PluginsManager: PluginsManagerType = {
return unsubscribe;
},
getExtendedPluginHash(hash: string) {
let extendedHash = hash;
if (!(hash in PluginsManager.activePlugins)) PluginsManager.activePlugins[hash] = [];
if (PluginsManager.activePlugins[hash].includes(hash)) {
const prefix = md5(uuidv4()).toString();
extendedHash = `${prefix}-${hash}`;
}
PluginsManager.activePlugins[hash].push(extendedHash);
return extendedHash;
},
getExtendedPluginName(hash, extendedHash, pluginName) {
return PluginsManager.activePlugins[hash].length === 1 || !isPluginHashWithPrefix(extendedHash)
? pluginName
: `${pluginName} (${PluginsManager.pluginsOccurrences[hash]})`;
},
updatePluginOccurrence(hash, extendedHash) {
if (hash in PluginsManager.pluginsOccurrences && isPluginHashWithPrefix(extendedHash)) {
PluginsManager.pluginsOccurrences[hash] += ONE;
} else if (!(hash in PluginsManager.pluginsOccurrences)) {
PluginsManager.pluginsOccurrences[hash] = ZERO;
}
},
registerPlugin({ pluginName, pluginVersion, pluginUrl }) {
const hash = PluginsManager.hashedPlugins[pluginUrl];
const extendedHash = PluginsManager.getExtendedPluginHash(hash);
PluginsManager.updatePluginOccurrence(hash, extendedHash);
if (!hash) {
throw new Error(ERROR_PLUGIN_URL_MISMATCH);
}
const extendedPluginName = PluginsManager.getExtendedPluginName(hash, extendedHash, pluginName);
store.dispatch(
registerPlugin({
hash,
hash: extendedHash,
isPublic: false,
extendedPluginName,
pluginName,
pluginUrl,
pluginVersion,
}),
);
const element = PluginsManager.createAndGetPluginContent({ hash });
const element = PluginsManager.createAndGetPluginContent({
hash: extendedHash,
});
return {
element,
events: {
addListener: PluginsEventBus.addListener.bind(this, hash, pluginName),
removeListener: PluginsEventBus.removeListener.bind(this, hash),
removeAllListeners: PluginsEventBus.removeAllListeners.bind(this, hash),
addListener: PluginsEventBus.addListener.bind(this, extendedHash, pluginName),
removeListener: PluginsEventBus.removeListener.bind(this, extendedHash),
removeAllListeners: PluginsEventBus.removeAllListeners.bind(this, extendedHash),
},
legend: {
setLegend: setLegend.bind(this, hash),
removeLegend: removeLegend.bind(this, hash),
setLegend: setLegend.bind(this, extendedHash),
removeLegend: removeLegend.bind(this, extendedHash),
},
};
},
......
......@@ -21,4 +21,14 @@ export type PluginsManagerType = {
};
createAndGetPluginContent(plugin: Pick<MinervaPlugin, 'hash'>): HTMLDivElement;
removePluginContent(plugin: Pick<MinervaPlugin, 'hash'>): void;
activePlugins: {
[pluginId: string]: string[];
};
pluginsOccurrences: {
[pluginId: string]: number;
};
unloadActivePlugin(hash: string): void;
getExtendedPluginHash(hash: string): string;
updatePluginOccurrence(hash: string, extendedHash: string): void;
getExtendedPluginName(hash: string, extendedHash: string, pluginName: string): string;
};
import { getPluginHashWithoutPrefix } from './getPluginHashWithoutPrefix';
describe('getPluginHashWithoutPrefix', () => {
it('should return hash without prefix if hash contains the prefix separator', () => {
const hash = 'prefix-hash';
const expectedResult = 'hash';
const result = getPluginHashWithoutPrefix(hash);
expect(result).toBe(expectedResult);
});
it('should return the same hash if hash does not contain the prefix separator', () => {
const hash = 'hash';
const result = getPluginHashWithoutPrefix(hash);
expect(result).toBe(hash);
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment