diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/AvailablePluginsDrawer.component.tsx b/src/components/Map/Drawer/AvailablePluginsDrawer/AvailablePluginsDrawer.component.tsx index d1343d91bbf51f36f926c2af248d596790155f04..9022c5817c921769d43dc879d6de2b9d269de319 100644 --- a/src/components/Map/Drawer/AvailablePluginsDrawer/AvailablePluginsDrawer.component.tsx +++ b/src/components/Map/Drawer/AvailablePluginsDrawer/AvailablePluginsDrawer.component.tsx @@ -1,4 +1,7 @@ -import { publicPluginsListSelector } from '@/redux/plugins/plugins.selectors'; +import { + privateActivePluginsSelector, + publicPluginsListSelector, +} from '@/redux/plugins/plugins.selectors'; import { DrawerHeading } from '@/shared/DrawerHeading'; import { useSelector } from 'react-redux'; import { LoadPlugin } from './LoadPlugin'; @@ -6,12 +9,17 @@ import { LoadPluginFromUrl } from './LoadPluginFromUrl'; export const AvailablePluginsDrawer = (): JSX.Element => { const publicPlugins = useSelector(publicPluginsListSelector); + const privateActivePlugins = useSelector(privateActivePluginsSelector); return ( <div className="h-full max-h-full" data-testid="available-plugins-drawer"> <DrawerHeading title="Available plugins" /> <div className="flex flex-col gap-6 p-6"> <LoadPluginFromUrl /> + {privateActivePlugins.map(plugin => ( + <LoadPlugin key={plugin.hash} plugin={plugin} /> + ))} + {publicPlugins.map(plugin => ( <LoadPlugin key={plugin.hash} plugin={plugin} /> ))} diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.test.tsx b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.test.tsx index 92c659ac76bde94184d4324be245989f7c8f4dec..ba8ea94c6ced7739c43e902ac3ac60020959b57a 100644 --- a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.test.tsx +++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.test.tsx @@ -1,10 +1,36 @@ +/* eslint-disable no-magic-numbers */ import { FIRST_ARRAY_ELEMENT } from '@/constants/common'; import { PLUGINS_MOCK } from '@/models/mocks/pluginsMock'; import { render, screen } from '@testing-library/react'; +import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; +import { InitialStoreState } from '@/utils/testing/getReduxStoreActionsListener'; +import { StoreType } from '@/redux/store'; +import { mockNetworkResponse } from '@/utils/mockNetworkResponse'; +import MockAdapter from 'axios-mock-adapter'; +import axios, { HttpStatusCode } from 'axios'; +import { apiPath } from '@/redux/apiPath'; +import { act } from 'react-dom/test-utils'; +import { PLUGINS_INITIAL_STATE_LIST_MOCK } from '@/redux/plugins/plugins.mock'; import { LoadPlugin, Props } from './LoadPlugin.component'; -const renderComponent = ({ plugin }: Props): void => { - render(<LoadPlugin plugin={plugin} />); +const mockedAxiosApiClient = mockNetworkResponse(); +const mockedAxiosClient = new MockAdapter(axios); + +const renderComponent = ( + { plugin }: Props, + initialStore?: InitialStoreState, +): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(initialStore); + return ( + render( + <Wrapper> + <LoadPlugin plugin={plugin} /> + </Wrapper>, + ), + { + store, + } + ); }; describe('LoadPlugin - component', () => { @@ -25,5 +51,54 @@ describe('LoadPlugin - component', () => { expect(loadButton.tagName).toBe('BUTTON'); expect(loadButton).toBeInTheDocument(); }); + it('should change button label to unload if plugin is active', () => { + renderComponent( + { plugin }, + { + plugins: { + activePlugins: { + data: { + [plugin.hash]: plugin, + }, + pluginsId: [plugin.hash], + }, + list: PLUGINS_INITIAL_STATE_LIST_MOCK, + }, + }, + ); + + expect(screen.queryByTestId('toggle-plugin')).toHaveTextContent('Unload'); + }); + it('should change button label to load if plugin is not active', () => { + renderComponent({ plugin }); + expect(screen.queryByTestId('toggle-plugin')).toHaveTextContent('Load'); + }); + it('should unload plugin after click', async () => { + mockedAxiosApiClient.onPost(apiPath.registerPluign()).reply(HttpStatusCode.Ok, plugin); + mockedAxiosApiClient.onGet(apiPath.getPlugin(plugin.hash)).reply(HttpStatusCode.Ok, plugin); + mockedAxiosClient.onGet(plugin.urls[0]).reply(HttpStatusCode.Ok, ''); + const { store } = renderComponent( + { plugin }, + { + plugins: { + activePlugins: { + data: { + [plugin.hash]: plugin, + }, + pluginsId: [plugin.hash], + }, + list: PLUGINS_INITIAL_STATE_LIST_MOCK, + }, + }, + ); + + await act(() => { + screen.queryByTestId('toggle-plugin')?.click(); + }); + + const { activePlugins } = store.getState().plugins; + expect(activePlugins.pluginsId).toEqual([]); + expect(activePlugins.data).toEqual({}); + }); }); }); diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.tsx b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.tsx index fee403ca18cb34195aaf42a559ba618eb68f821e..64b713258dfc77c24156addbc51a768a59761708 100644 --- a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.tsx +++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.tsx @@ -1,14 +1,17 @@ +/* eslint-disable no-magic-numbers */ import { Button } from '@/shared/Button'; import { MinervaPlugin } from '@/types/models'; +import { useLoadPlugin } from './hooks/useLoadPlugin'; export interface Props { plugin: MinervaPlugin; } export const LoadPlugin = ({ plugin }: Props): JSX.Element => { - const handleLoadPlugin = (): void => { - // TODO: handleLoadPlugin - }; + const { isPluginActive, togglePlugin, isPluginLoading } = useLoadPlugin({ + hash: plugin.hash, + pluginUrl: plugin.urls[0], + }); return ( <div className="flex w-full items-center justify-between"> @@ -16,9 +19,11 @@ export const LoadPlugin = ({ plugin }: Props): JSX.Element => { <Button variantStyles="secondary" className="h-10 self-end rounded-e rounded-s" - onClick={handleLoadPlugin} + onClick={togglePlugin} + data-testid="toggle-plugin" + disabled={isPluginLoading} > - Load + {isPluginActive ? 'Unload' : 'Load'} </Button> </div> ); diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.ts b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.ts new file mode 100644 index 0000000000000000000000000000000000000000..d1b04f6549b8eb8b790302a7ff3ee31b45c822f7 --- /dev/null +++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.ts @@ -0,0 +1,57 @@ +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { isPluginActiveSelector, isPluginLoadingSelector } from '@/redux/plugins/plugins.selectors'; +import { removePlugin } from '@/redux/plugins/plugins.slice'; +import { PluginsManager } from '@/services/pluginsManager'; +import axios from 'axios'; + +type UseLoadPluginReturnType = { + togglePlugin: () => void; + isPluginActive: boolean; + isPluginLoading: boolean; +}; + +type UseLoadPluginProps = { + hash: string; + pluginUrl: string; +}; + +export const useLoadPlugin = ({ hash, pluginUrl }: UseLoadPluginProps): UseLoadPluginReturnType => { + const isPluginActive = useAppSelector(state => isPluginActiveSelector(state, hash)); + const isPluginLoading = useAppSelector(state => isPluginLoadingSelector(state, hash)); + + const dispatch = useAppDispatch(); + + const handleLoadPlugin = async (): Promise<void> => { + const response = await axios(pluginUrl); + const pluginScript = response.data; + + /* eslint-disable no-new-func */ + const loadPlugin = new Function(pluginScript); + + PluginsManager.setHashedPlugin({ + pluginUrl, + pluginScript, + }); + + loadPlugin(); + }; + + const handleUnloadPlugin = (): void => { + dispatch(removePlugin({ pluginId: hash })); + }; + + const togglePlugin = (): void => { + if (isPluginActive) { + handleUnloadPlugin(); + } else { + handleLoadPlugin(); + } + }; + + return { + togglePlugin, + isPluginActive, + isPluginLoading, + }; +}; diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.test.tsx b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.test.tsx index e1d83a8ba9fde4258d36ad1d5df8629ac583ce57..a229ab7e7449c516321586fec9f20362be9b97c5 100644 --- a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.test.tsx +++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.test.tsx @@ -1,8 +1,31 @@ -import { render, screen } from '@testing-library/react'; +/* eslint-disable no-magic-numbers */ +import { fireEvent, render, screen } from '@testing-library/react'; +import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; +import { InitialStoreState } from '@/utils/testing/getReduxStoreActionsListener'; +import { StoreType } from '@/redux/store'; +import { mockNetworkResponse } from '@/utils/mockNetworkResponse'; +import MockAdapter from 'axios-mock-adapter'; +import axios, { HttpStatusCode } from 'axios'; +import { apiPath } from '@/redux/apiPath'; +import { pluginFixture } from '@/models/fixtures/pluginFixture'; +import { act } from 'react-dom/test-utils'; import { LoadPluginFromUrl } from './LoadPluginFromUrl.component'; -const renderComponent = (): void => { - render(<LoadPluginFromUrl />); +const mockedAxiosApiClient = mockNetworkResponse(); +const mockedAxiosClient = new MockAdapter(axios); + +const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(initialStore); + return ( + render( + <Wrapper> + <LoadPluginFromUrl /> + </Wrapper>, + ), + { + store, + } + ); }; describe('LoadPluginFromUrl - component', () => { @@ -28,5 +51,31 @@ describe('LoadPluginFromUrl - component', () => { expect(loadButton.tagName).toBe('BUTTON'); expect(loadButton).toBeInTheDocument(); }); + it('should unload plugin after click', async () => { + mockedAxiosApiClient.onPost(apiPath.registerPluign()).reply(HttpStatusCode.Ok, pluginFixture); + mockedAxiosApiClient + .onGet(apiPath.getPlugin(pluginFixture.hash)) + .reply(HttpStatusCode.Ok, pluginFixture); + mockedAxiosClient.onGet(pluginFixture.urls[0]).reply(HttpStatusCode.Ok, ''); + + renderComponent(); + const input = screen.getByTestId('load-plugin-input-url'); + expect(input).toBeVisible(); + + await act(() => { + fireEvent.change(input, { target: { value: pluginFixture.urls[0] } }); + }); + + expect(input).toHaveValue(pluginFixture.urls[0]); + + const button = screen.queryByTestId('load-plugin-button'); + expect(button).toBeVisible(); + + await act(() => { + button?.click(); + }); + + expect(button).toBeDisabled(); + }); }); }); diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.tsx b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.tsx index 4ef47fa107c0b6ed6cd8b8b353e6af8c1aaaca05..fac9bd0ddaf03c88b1e8f30f6baf2090081a398c 100644 --- a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.tsx +++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.tsx @@ -1,8 +1,8 @@ import { Button } from '@/shared/Button'; -import { useLoadPlugin } from '../hooks/useLoadPlugin'; +import { useLoadPluginFromUrl } from './hooks/useLoadPluginFromUrl'; export const LoadPluginFromUrl = (): JSX.Element => { - const { handleChangePluginUrl, handleLoadPlugin, isPending, pluginUrl } = useLoadPlugin(); + const { handleChangePluginUrl, handleLoadPlugin, isPending, pluginUrl } = useLoadPluginFromUrl(); return ( <div className="flex w-full"> @@ -21,6 +21,7 @@ export const LoadPluginFromUrl = (): JSX.Element => { className="h-10 self-end rounded-e rounded-s" onClick={handleLoadPlugin} disabled={isPending} + data-testid="load-plugin-button" > Load </Button> diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/hooks/useLoadPlugin.ts b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/hooks/useLoadPluginFromUrl.ts similarity index 54% rename from src/components/Map/Drawer/AvailablePluginsDrawer/hooks/useLoadPlugin.ts rename to src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/hooks/useLoadPluginFromUrl.ts index 8df7a52d55142598573cfc316c2ee9bb184f2368..54b17df588580e90b92b01a860eb4ece9b8d0756 100644 --- a/src/components/Map/Drawer/AvailablePluginsDrawer/hooks/useLoadPlugin.ts +++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/hooks/useLoadPluginFromUrl.ts @@ -9,30 +9,25 @@ type UseLoadPluginReturnType = { pluginUrl: string; }; -export const useLoadPlugin = (): UseLoadPluginReturnType => { +export const useLoadPluginFromUrl = (): UseLoadPluginReturnType => { const [pluginUrl, setPluginUrl] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const isPending = isLoading || !pluginUrl; + + const isPending = !pluginUrl; const handleLoadPlugin = async (): Promise<void> => { - try { - setIsLoading(true); - const response = await axios(pluginUrl); - const pluginScript = response.data; - - /* eslint-disable no-new-func */ - const loadPlugin = new Function(pluginScript); - - PluginsManager.setHashedPlugin({ - pluginUrl, - pluginScript, - }); - - loadPlugin(); - setPluginUrl(''); - } finally { - setIsLoading(false); - } + const response = await axios(pluginUrl); + const pluginScript = response.data; + + /* eslint-disable no-new-func */ + const loadPlugin = new Function(pluginScript); + + PluginsManager.setHashedPlugin({ + pluginUrl, + pluginScript, + }); + + loadPlugin(); + setPluginUrl(''); }; const handleChangePluginUrl = (event: ChangeEvent<HTMLInputElement>): void => { setPluginUrl(event.target.value); diff --git a/src/components/Map/Drawer/PluginDrawer/LoadPluginInput/LoadPluginInput.component.tsx b/src/components/Map/Drawer/PluginDrawer/LoadPluginInput/LoadPluginInput.component.tsx deleted file mode 100644 index af00f2417efe799d5cc06fdf30e2e4d42326a677..0000000000000000000000000000000000000000 --- a/src/components/Map/Drawer/PluginDrawer/LoadPluginInput/LoadPluginInput.component.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Button } from '@/shared/Button'; -import { Input } from '@/shared/Input'; -import { useLoadPlugin } from '../../AvailablePluginsDrawer/hooks/useLoadPlugin'; - -export const LoadPluginInput = (): React.ReactNode => { - const { handleChangePluginUrl, handleLoadPlugin, isPending, pluginUrl } = useLoadPlugin(); - - return ( - <label> - URL: - <div className="relative mt-2.5"> - <Input - className="h-10 rounded-r-md pr-[70px]" - value={pluginUrl} - onChange={handleChangePluginUrl} - /> - <Button - className="absolute inset-y-0 right-0 w-[60px] justify-center text-xs font-medium text-primary-500 ring-primary-500 hover:ring-primary-500 disabled:text-primary-500 disabled:ring-primary-500" - variantStyles="ghost" - onClick={handleLoadPlugin} - disabled={isPending} - > - Load - </Button> - </div> - </label> - ); -}; diff --git a/src/components/Map/Drawer/PluginDrawer/LoadPluginInput/index.ts b/src/components/Map/Drawer/PluginDrawer/LoadPluginInput/index.ts deleted file mode 100644 index f31afe30c9e4723d406021242730c75a08a46777..0000000000000000000000000000000000000000 --- a/src/components/Map/Drawer/PluginDrawer/LoadPluginInput/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LoadPluginInput } from './LoadPluginInput.component'; diff --git a/src/components/Map/Drawer/PluginDrawer/PluginsDrawer.component.tsx b/src/components/Map/Drawer/PluginDrawer/PluginsDrawer.component.tsx deleted file mode 100644 index 18d526951217960970944215adc5d5afee56d3ef..0000000000000000000000000000000000000000 --- a/src/components/Map/Drawer/PluginDrawer/PluginsDrawer.component.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { DrawerHeading } from '@/shared/DrawerHeading'; -import React from 'react'; -import { LoadPluginInput } from './LoadPluginInput'; -import { PluginsList } from './PluginsList'; - -export const PluginsDrawer = (): React.ReactNode => ( - <div data-testid="available-plugins-drawer" className="h-full max-h-full"> - <DrawerHeading title="Available plugins" /> - <div className="h-[calc(100%-93px)] max-h-[calc(100%-93px)] overflow-y-auto p-6"> - <LoadPluginInput /> - <PluginsList /> - </div> - </div> -); diff --git a/src/components/Map/Drawer/PluginDrawer/PluginsList/PluginsList.component.tsx b/src/components/Map/Drawer/PluginDrawer/PluginsList/PluginsList.component.tsx deleted file mode 100644 index aaded05cd2102dfcec2292c5c7d8ab48bb38d006..0000000000000000000000000000000000000000 --- a/src/components/Map/Drawer/PluginDrawer/PluginsList/PluginsList.component.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; -import { useAppSelector } from '@/redux/hooks/useAppSelector'; -import { activePluginsSelector } from '@/redux/plugins/plugins.selector'; -import { removePlugin } from '@/redux/plugins/plugins.slice'; -import { Button } from '@/shared/Button'; - -export const PluginsList = (): React.ReactNode => { - const activePlugins = useAppSelector(activePluginsSelector); - const dispatch = useAppDispatch(); - - const handleUnloadPlugin = (pluginId: string): void => { - dispatch(removePlugin({ pluginId })); - }; - - return ( - <ul className="mt-8 flex w-full flex-col gap-y-8"> - {activePlugins.map(plugin => ( - <li key={plugin.hash} className="flex w-full items-center justify-between"> - <span className="text-sm"> - {plugin.name} ({plugin.version}) - </span> - <Button - variantStyles="ghost" - onClick={() => handleUnloadPlugin(plugin.hash)} - className="h-10 w-[60px] justify-center text-xs font-medium text-primary-500 ring-primary-500 hover:ring-primary-500" - > - Unload - </Button> - </li> - ))} - </ul> - ); -}; diff --git a/src/components/Map/Drawer/PluginDrawer/PluginsList/index.ts b/src/components/Map/Drawer/PluginDrawer/PluginsList/index.ts deleted file mode 100644 index 519e3a35b42f68d348674bf85db65526ce54cad9..0000000000000000000000000000000000000000 --- a/src/components/Map/Drawer/PluginDrawer/PluginsList/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { PluginsList } from './PluginsList.component'; diff --git a/src/components/Map/Drawer/PluginDrawer/index.ts b/src/components/Map/Drawer/PluginDrawer/index.ts deleted file mode 100644 index 8858fdbe0d2b7c33f6ce041fbf382b887d4e4281..0000000000000000000000000000000000000000 --- a/src/components/Map/Drawer/PluginDrawer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { PluginsDrawer } from './PluginsDrawer.component'; diff --git a/src/redux/plugins/plugins.mock.ts b/src/redux/plugins/plugins.mock.ts index 6cbfcf5594d0f65536392a3db11e3135224fdf25..9b6b9c8f12621dfa722ed20e45d6aec0c69408ba 100644 --- a/src/redux/plugins/plugins.mock.ts +++ b/src/redux/plugins/plugins.mock.ts @@ -1,7 +1,7 @@ import { DEFAULT_ERROR } from '@/constants/errors'; import { ActivePlugins, PluginsList, PluginsState } from './plugins.types'; -export const PLUGINS_INITIAL_STATE__ACTIVE_PLUGINS_MOCK: ActivePlugins = { +export const PLUGINS_INITIAL_STATE_ACTIVE_PLUGINS_MOCK: ActivePlugins = { data: {}, pluginsId: [], }; @@ -14,5 +14,5 @@ export const PLUGINS_INITIAL_STATE_LIST_MOCK: PluginsList = { export const PLUGINS_INITIAL_STATE_MOCK: PluginsState = { list: PLUGINS_INITIAL_STATE_LIST_MOCK, - activePlugins: PLUGINS_INITIAL_STATE__ACTIVE_PLUGINS_MOCK, + activePlugins: PLUGINS_INITIAL_STATE_ACTIVE_PLUGINS_MOCK, }; diff --git a/src/redux/plugins/plugins.reducers.test.ts b/src/redux/plugins/plugins.reducers.test.ts index f575ebf6c5c10fbbefe5ec768a98eec1afab44ad..edc59097d4d53fe239bace4d11948391b3857847 100644 --- a/src/redux/plugins/plugins.reducers.test.ts +++ b/src/redux/plugins/plugins.reducers.test.ts @@ -9,7 +9,7 @@ import { pluginFixture } from '@/models/fixtures/pluginFixture'; import { apiPath } from '../apiPath'; import { PluginsState } from './plugins.types'; import pluginsReducer, { removePlugin } from './plugins.slice'; -import { registerPlugin } from './plugins.thunk'; +import { registerPlugin } from './plugins.thunks'; import { PLUGINS_INITIAL_STATE_MOCK } from './plugins.mock'; const mockedAxiosClient = mockNetworkResponse(); diff --git a/src/redux/plugins/plugins.reducers.ts b/src/redux/plugins/plugins.reducers.ts index 31c0b6f96c04f683f32bbb6f90400c7e40c03b25..f046459c4c9e88879b6d7b9ee88a755e7e29ddc1 100644 --- a/src/redux/plugins/plugins.reducers.ts +++ b/src/redux/plugins/plugins.reducers.ts @@ -1,6 +1,6 @@ import type { ActionReducerMapBuilder } from '@reduxjs/toolkit'; import type { PluginsState, RemovePluginAction } from './plugins.types'; -import { registerPlugin, getAllPlugins } from './plugins.thunk'; +import { registerPlugin, getAllPlugins } from './plugins.thunks'; export const removePluginReducer = (state: PluginsState, action: RemovePluginAction): void => { const { pluginId } = action.payload; diff --git a/src/redux/plugins/plugins.selector.ts b/src/redux/plugins/plugins.selector.ts deleted file mode 100644 index 15abcc8d8793333d92c1fe58d99a685baa55d18e..0000000000000000000000000000000000000000 --- a/src/redux/plugins/plugins.selector.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { MinervaPlugin } from '@/types/models'; -import { rootSelector } from '../root/root.selectors'; - -export const pluginsSelector = createSelector(rootSelector, state => state.plugins); - -export const activePluginsIdSelector = createSelector( - pluginsSelector, - state => state.activePlugins.pluginsId, -); - -export const pluginsDataSelector = createSelector( - pluginsSelector, - plugins => plugins.activePlugins.data, -); - -export const activePluginsSelector = createSelector( - pluginsDataSelector, - activePluginsIdSelector, - (data, pluginsId) => { - const result: MinervaPlugin[] = []; - - pluginsId.forEach(pluginId => { - const element = data[pluginId]; - if (element) { - result.push(element); - } - }); - - return result; - }, -); diff --git a/src/redux/plugins/plugins.selectors.ts b/src/redux/plugins/plugins.selectors.ts index 5ece3dd2e529230dd8f85c413703675ebd7de90d..1bf37c638ee768446fe26c5229f4e4a51e134f14 100644 --- a/src/redux/plugins/plugins.selectors.ts +++ b/src/redux/plugins/plugins.selectors.ts @@ -1,4 +1,5 @@ import { createSelector } from '@reduxjs/toolkit'; +import { MinervaPlugin } from '@/types/models'; import { rootSelector } from '../root/root.selectors'; export const pluginsSelector = createSelector(rootSelector, state => state.plugins); @@ -17,3 +18,50 @@ export const publicPluginsListSelector = createSelector( return (pluginsListData || []).filter(plugin => plugin.isPublic); }, ); + +export const activePluginsSelector = createSelector(pluginsSelector, state => state.activePlugins); + +export const activePluginsIdSelector = createSelector( + activePluginsSelector, + state => state.pluginsId, +); + +export const activePluginsDataSelector = createSelector( + activePluginsSelector, + plugins => plugins.data, +); + +export const allActivePluginsSelector = createSelector( + activePluginsDataSelector, + activePluginsIdSelector, + (data, pluginsId) => { + const result: MinervaPlugin[] = []; + + pluginsId.forEach(pluginId => { + const element = data[pluginId]; + if (element) { + result.push(element); + } + }); + + return result; + }, +); + +export const privateActivePluginsSelector = createSelector( + allActivePluginsSelector, + activePlugins => { + return (activePlugins || []).filter(plugin => !plugin.isPublic); + }, +); + +export const isPluginActiveSelector = createSelector( + [activePluginsIdSelector, (_, activePlugin: string): string => activePlugin], + (activePlugins, activePlugin) => activePlugins.includes(activePlugin), +); + +export const isPluginLoadingSelector = createSelector( + [activePluginsSelector, (_, activePlugins: string): string => activePlugins], + ({ data, pluginsId }, pluginId) => + pluginsId.includes(pluginId) && data[pluginId] && !Object.keys(data[pluginId]).length, +); diff --git a/src/redux/plugins/plugins.thunk.ts b/src/redux/plugins/plugins.thunk.ts deleted file mode 100644 index 73c7341e07cfc2f9b93515487084fe26fade667d..0000000000000000000000000000000000000000 --- a/src/redux/plugins/plugins.thunk.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* eslint-disable no-magic-numbers */ -import axios from 'axios'; -import { createAsyncThunk } from '@reduxjs/toolkit'; -import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; -import { pluginSchema } from '@/models/pluginSchema'; -import type { MinervaPlugin } from '@/types/models'; -import { axiosInstance } from '@/services/api/utils/axiosInstance'; -import { z } from 'zod'; -import { apiPath } from '../apiPath'; - -type RegisterPlugin = { - hash: string; - pluginUrl: string; - pluginName: string; - pluginVersion: string; - isPublic: boolean; -}; - -export const registerPlugin = createAsyncThunk( - 'plugins/registerPlugin', - async ({ - hash, - isPublic, - pluginName, - pluginUrl, - pluginVersion, - }: RegisterPlugin): Promise<MinervaPlugin | undefined> => { - const payload = { - hash, - url: pluginUrl, - name: pluginName, - version: pluginVersion, - isPublic: isPublic.toString(), - } as const; - - const response = await axiosInstance.post<MinervaPlugin>( - apiPath.registerPluign(), - new URLSearchParams(payload), - { - withCredentials: true, - }, - ); - - const isDataValid = validateDataUsingZodSchema(response.data, pluginSchema); - - if (isDataValid) { - return response.data; - } - - return undefined; - }, -); - -type GetInitPluginsProps = { - pluginsId: string[]; - setHashedPlugin: ({ - pluginUrl, - pluginScript, - }: { - pluginUrl: string; - pluginScript: string; - }) => void; -}; - -export const getInitPlugins = createAsyncThunk<void, GetInitPluginsProps>( - 'plugins/getInitPlugins', - async ({ pluginsId, setHashedPlugin }): Promise<void> => { - /* eslint-disable no-restricted-syntax, no-await-in-loop */ - for (const pluginId of pluginsId) { - const res = await axiosInstance<MinervaPlugin>(apiPath.getPlugin(pluginId)); - - const isDataValid = validateDataUsingZodSchema(res.data, pluginSchema); - - if (isDataValid) { - const { urls } = res.data; - const scriptRes = await axios(urls[0]); - const pluginScript = scriptRes.data; - setHashedPlugin({ pluginUrl: urls[0], pluginScript }); - - /* eslint-disable no-new-func */ - const loadPlugin = new Function(pluginScript); - loadPlugin(); - } - } - }, -); - -export const getAllPlugins = createAsyncThunk( - 'plugins/getAllPlugins', - async (): Promise<MinervaPlugin[]> => { - const response = await axiosInstance.get<MinervaPlugin[]>(apiPath.getAllPlugins()); - - const isDataValid = validateDataUsingZodSchema(response.data, z.array(pluginSchema)); - - return isDataValid ? response.data : []; - }, -); diff --git a/src/redux/plugins/plugins.thunk.test.ts b/src/redux/plugins/plugins.thunks.test.ts similarity index 97% rename from src/redux/plugins/plugins.thunk.test.ts rename to src/redux/plugins/plugins.thunks.test.ts index c2ae3d53f8d9c0df763d0d3241b38e654692a3ba..7972222c206923167271607d9c4e844b56d55556 100644 --- a/src/redux/plugins/plugins.thunk.test.ts +++ b/src/redux/plugins/plugins.thunks.test.ts @@ -10,7 +10,7 @@ import { import { apiPath } from '../apiPath'; import { PluginsState } from './plugins.types'; import pluginsReducer from './plugins.slice'; -import { getInitPlugins } from './plugins.thunk'; +import { getInitPlugins } from './plugins.thunks'; const mockedAxiosApiClient = mockNetworkResponse(); const mockedAxiosClient = new MockAdapter(axios); diff --git a/src/redux/plugins/plugins.thunks.ts b/src/redux/plugins/plugins.thunks.ts index 732cfdcb4fcadb1b83a804bb51c8f581d2269f50..73c7341e07cfc2f9b93515487084fe26fade667d 100644 --- a/src/redux/plugins/plugins.thunks.ts +++ b/src/redux/plugins/plugins.thunks.ts @@ -1,11 +1,90 @@ +/* eslint-disable no-magic-numbers */ +import axios from 'axios'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; import { pluginSchema } from '@/models/pluginSchema'; +import type { MinervaPlugin } from '@/types/models'; import { axiosInstance } from '@/services/api/utils/axiosInstance'; -import { MinervaPlugin } from '@/types/models'; -import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; -import { createAsyncThunk } from '@reduxjs/toolkit'; import { z } from 'zod'; import { apiPath } from '../apiPath'; +type RegisterPlugin = { + hash: string; + pluginUrl: string; + pluginName: string; + pluginVersion: string; + isPublic: boolean; +}; + +export const registerPlugin = createAsyncThunk( + 'plugins/registerPlugin', + async ({ + hash, + isPublic, + pluginName, + pluginUrl, + pluginVersion, + }: RegisterPlugin): Promise<MinervaPlugin | undefined> => { + const payload = { + hash, + url: pluginUrl, + name: pluginName, + version: pluginVersion, + isPublic: isPublic.toString(), + } as const; + + const response = await axiosInstance.post<MinervaPlugin>( + apiPath.registerPluign(), + new URLSearchParams(payload), + { + withCredentials: true, + }, + ); + + const isDataValid = validateDataUsingZodSchema(response.data, pluginSchema); + + if (isDataValid) { + return response.data; + } + + return undefined; + }, +); + +type GetInitPluginsProps = { + pluginsId: string[]; + setHashedPlugin: ({ + pluginUrl, + pluginScript, + }: { + pluginUrl: string; + pluginScript: string; + }) => void; +}; + +export const getInitPlugins = createAsyncThunk<void, GetInitPluginsProps>( + 'plugins/getInitPlugins', + async ({ pluginsId, setHashedPlugin }): Promise<void> => { + /* eslint-disable no-restricted-syntax, no-await-in-loop */ + for (const pluginId of pluginsId) { + const res = await axiosInstance<MinervaPlugin>(apiPath.getPlugin(pluginId)); + + const isDataValid = validateDataUsingZodSchema(res.data, pluginSchema); + + if (isDataValid) { + const { urls } = res.data; + const scriptRes = await axios(urls[0]); + const pluginScript = scriptRes.data; + setHashedPlugin({ pluginUrl: urls[0], pluginScript }); + + /* eslint-disable no-new-func */ + const loadPlugin = new Function(pluginScript); + loadPlugin(); + } + } + }, +); + export const getAllPlugins = createAsyncThunk( 'plugins/getAllPlugins', async (): Promise<MinervaPlugin[]> => { diff --git a/src/redux/root/init.thunks.ts b/src/redux/root/init.thunks.ts index e193bf73c656e1ad8dbb25097dc217a6e4ea3060..557e87d96f6804aed9e108a81d9530a2cd93ccf2 100644 --- a/src/redux/root/init.thunks.ts +++ b/src/redux/root/init.thunks.ts @@ -16,12 +16,11 @@ import { import { getModels } from '../models/models.thunks'; import { getInitOverlays } from '../overlayBioEntity/overlayBioEntity.thunk'; import { getAllPublicOverlaysByProjectId } from '../overlays/overlays.thunks'; -import { getAllPlugins } from '../plugins/plugins.thunks'; +import { getAllPlugins, getInitPlugins } from '../plugins/plugins.thunks'; import { getProjectById } from '../project/project.thunks'; import { setPerfectMatch } from '../search/search.slice'; import { getSearchData } from '../search/search.thunks'; import { getStatisticsById } from '../statistics/statistics.thunks'; -import { getInitPlugins } from '../plugins/plugins.thunk'; import { getSessionValid } from '../user/user.thunks'; interface InitializeAppParams { diff --git a/src/redux/root/query.selectors.ts b/src/redux/root/query.selectors.ts index 3fbc3a8b939e7ff6aa95db3cf5b007aa3547fcbd..b862ed429fdb885367f3f87755c47204903b08b7 100644 --- a/src/redux/root/query.selectors.ts +++ b/src/redux/root/query.selectors.ts @@ -4,7 +4,7 @@ import { ZERO } from '@/constants/common'; import { mapDataSelector } from '../map/map.selectors'; import { perfectMatchSelector, searchValueSelector } from '../search/search.selectors'; import { activeOverlaysIdSelector } from '../overlayBioEntity/overlayBioEntity.selector'; -import { activePluginsIdSelector } from '../plugins/plugins.selector'; +import { activePluginsIdSelector } from '../plugins/plugins.selectors'; export const queryDataParamsSelector = createSelector( searchValueSelector, diff --git a/src/services/pluginsManager/pluginsManager.ts b/src/services/pluginsManager/pluginsManager.ts index ab32676fbccdf77592feb28494a312585b2b4257..25df5404ad4efba83b8c0b7e354978f5d3f19ffd 100644 --- a/src/services/pluginsManager/pluginsManager.ts +++ b/src/services/pluginsManager/pluginsManager.ts @@ -1,6 +1,6 @@ import md5 from 'crypto-js/md5'; import { store } from '@/redux/store'; -import { registerPlugin } from '@/redux/plugins/plugins.thunk'; +import { registerPlugin } from '@/redux/plugins/plugins.thunks'; import { configurationMapper } from './pluginsManager.utils'; import type { PluginsManagerType } from './pluginsManager.types';