Skip to content
Snippets Groups Projects
Commit 48d156b8 authored by Adrian Orłów's avatar Adrian Orłów :fire:
Browse files

Merge branch 'MIN-232-0-plugins-drawer-tabs' into 'development'

feat: add main plugins drawer with tabs (MIN-232)

Closes MIN-232

See merge request !122
parents dd0d629f d3520992
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...,!122feat: add main plugins drawer with tabs (MIN-232)
Pipeline #85423 passed
Showing
with 459 additions and 37 deletions
import { MouseEvent } from 'react';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { MAIN_MAP } from '@/redux/map/map.constants';
import { mapModelIdSelector, mapOpenedMapsSelector } from '@/redux/map/map.selectors';
import { closeMap, closeMapAndSetMainMapActive, setActiveMap } from '@/redux/map/map.slice';
import { OppenedMap } from '@/redux/map/map.types';
import { Button } from '@/shared/Button';
import { Icon } from '@/shared/Icon';
import { MAIN_MAP } from '@/redux/map/map.constants';
import { MouseEvent } from 'react';
import { twMerge } from 'tailwind-merge';
export const MapNavigation = (): JSX.Element => {
......
......@@ -4,6 +4,7 @@ import { openDrawer } from '@/redux/drawer/drawer.slice';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { openLegend } from '@/redux/legend/legend.slice';
import { openLoginModal } from '@/redux/modal/modal.slice';
import { openPluginsDrawer } from '@/redux/plugins/plugins.slice';
import { IconButton } from '@/shared/IconButton';
import Image from 'next/image';
......@@ -16,6 +17,7 @@ export const NavBar = (): JSX.Element => {
const openDrawerPlugins = (): void => {
dispatch(openDrawer('available-plugins'));
dispatch(openPluginsDrawer());
};
const openDrawerExport = (): void => {
......
......@@ -34,20 +34,23 @@ describe('AvailablePluginsDrawer - component', () => {
expect(loadPluginFromUrlInput).toBeInTheDocument();
});
it.each(PLUGINS_MOCK)('should render render all public plugins', currentPlugin => {
renderComponent({
...INITIAL_STORE_STATE_MOCK,
plugins: {
...INITIAL_STORE_STATE_MOCK.plugins,
list: {
...INITIAL_STORE_STATE_MOCK.plugins.list,
data: PLUGINS_MOCK,
it.each(PLUGINS_MOCK.filter(p => p.isPublic))(
'should render render all public plugins',
currentPlugin => {
renderComponent({
...INITIAL_STORE_STATE_MOCK,
plugins: {
...INITIAL_STORE_STATE_MOCK.plugins,
list: {
...INITIAL_STORE_STATE_MOCK.plugins.list,
data: PLUGINS_MOCK,
},
},
},
});
});
const pluginLabel = screen.getByText(currentPlugin.name);
expect(pluginLabel).toBeInTheDocument();
});
const pluginLabel = screen.getByText(currentPlugin.name);
expect(pluginLabel).toBeInTheDocument();
},
);
});
});
/* 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 { 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 MockAdapter from 'axios-mock-adapter';
import { InitialStoreState } from '@/utils/testing/getReduxStoreActionsListener';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { render, screen } from '@testing-library/react';
import axios, { HttpStatusCode } from 'axios';
import { apiPath } from '@/redux/apiPath';
import MockAdapter from 'axios-mock-adapter';
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 mockedAxiosApiClient = mockNetworkResponse();
......@@ -56,6 +59,7 @@ describe('LoadPlugin - component', () => {
{ plugin },
{
plugins: {
...PLUGINS_INITIAL_STATE_MOCK,
activePlugins: {
data: {
[plugin.hash]: plugin,
......@@ -81,6 +85,7 @@ describe('LoadPlugin - component', () => {
{ plugin },
{
plugins: {
...PLUGINS_INITIAL_STATE_MOCK,
activePlugins: {
data: {
[plugin.hash]: plugin,
......
/* eslint-disable no-magic-numbers */
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { setCurrentDrawerPluginHash } from '@/redux/plugins/plugins.slice';
import { Button } from '@/shared/Button';
import { MinervaPlugin } from '@/types/models';
import { useLoadPlugin } from './hooks/useLoadPlugin';
......@@ -8,9 +10,14 @@ export interface Props {
}
export const LoadPlugin = ({ plugin }: Props): JSX.Element => {
const dispatch = useAppDispatch();
const { isPluginActive, togglePlugin, isPluginLoading } = useLoadPlugin({
hash: plugin.hash,
pluginUrl: plugin.urls[0],
onPluginLoaded: () => {
dispatch(setCurrentDrawerPluginHash(plugin.hash));
},
});
return (
......
/* eslint-disable no-magic-numbers */
import { pluginFixture } from '@/models/fixtures/pluginFixture';
import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures';
import { PluginsManager } from '@/services/pluginsManager';
import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener';
import { renderHook, waitFor } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import axios, { HttpStatusCode } from 'axios';
import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener';
import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures';
import MockAdapter from 'axios-mock-adapter';
import { PluginsManager } from '@/services/pluginsManager';
import { pluginFixture } from '@/models/fixtures/pluginFixture';
import { act } from 'react-dom/test-utils';
import { useLoadPlugin } from './useLoadPlugin';
const mockedAxiosClient = new MockAdapter(axios);
......@@ -20,6 +20,7 @@ describe('useLoadPlugin', () => {
const { Wrapper, store } = getReduxStoreWithActionsListener({
...INITIAL_STORE_STATE_MOCK,
plugins: {
...INITIAL_STORE_STATE_MOCK.plugins,
activePlugins: {
pluginsId: [pluginFixture.hash],
data: {
......
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { isPluginActiveSelector, isPluginLoadingSelector } from '@/redux/plugins/plugins.selectors';
import {
isPluginActiveSelector,
isPluginLoadingSelector,
isPluginSelectedSelector,
} 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;
loadPlugin: () => Promise<void>;
unloadPlugin: () => void;
reloadPlugin: () => void;
isPluginSelected: boolean;
isPluginActive: boolean;
isPluginLoading: boolean;
};
......@@ -14,11 +22,17 @@ type UseLoadPluginReturnType = {
type UseLoadPluginProps = {
hash: string;
pluginUrl: string;
onPluginLoaded?(): void;
};
export const useLoadPlugin = ({ hash, pluginUrl }: UseLoadPluginProps): UseLoadPluginReturnType => {
export const useLoadPlugin = ({
hash,
pluginUrl,
onPluginLoaded,
}: UseLoadPluginProps): UseLoadPluginReturnType => {
const isPluginActive = useAppSelector(state => isPluginActiveSelector(state, hash));
const isPluginLoading = useAppSelector(state => isPluginLoadingSelector(state, hash));
const isPluginSelected = useAppSelector(state => isPluginSelectedSelector(state, hash));
const dispatch = useAppDispatch();
......@@ -35,10 +49,20 @@ export const useLoadPlugin = ({ hash, pluginUrl }: UseLoadPluginProps): UseLoadP
});
loadPlugin();
if (onPluginLoaded) {
onPluginLoaded();
}
};
const handleUnloadPlugin = (): void => {
dispatch(removePlugin({ pluginId: hash }));
PluginsManager.removePluginContent({ hash });
};
const handleReloadPlugin = async (): Promise<void> => {
handleUnloadPlugin();
await handleLoadPlugin();
};
const togglePlugin = (): void => {
......@@ -50,7 +74,11 @@ export const useLoadPlugin = ({ hash, pluginUrl }: UseLoadPluginProps): UseLoadP
};
return {
isPluginSelected,
togglePlugin,
loadPlugin: handleLoadPlugin,
unloadPlugin: handleUnloadPlugin,
reloadPlugin: handleReloadPlugin,
isPluginActive,
isPluginLoading,
};
......
/* 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 { 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 MockAdapter from 'axios-mock-adapter';
import { InitialStoreState } from '@/utils/testing/getReduxStoreActionsListener';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { fireEvent, render, screen } from '@testing-library/react';
import axios, { HttpStatusCode } from 'axios';
import { apiPath } from '@/redux/apiPath';
import { pluginFixture } from '@/models/fixtures/pluginFixture';
import MockAdapter from 'axios-mock-adapter';
import { act } from 'react-dom/test-utils';
import { PLUGINS_INITIAL_STATE_LIST_MOCK } from '@/redux/plugins/plugins.mock';
import { LoadPluginFromUrl } from './LoadPluginFromUrl.component';
const mockedAxiosApiClient = mockNetworkResponse();
......@@ -68,7 +71,7 @@ describe('LoadPluginFromUrl - component', () => {
const input = screen.getByTestId('load-plugin-input-url');
expect(input).toBeVisible();
act(() => {
await act(() => {
fireEvent.change(input, { target: { value: pluginFixture.urls[0] } });
});
......@@ -77,7 +80,7 @@ describe('LoadPluginFromUrl - component', () => {
const button = screen.queryByTestId('load-plugin-button');
expect(button).toBeVisible();
act(() => {
await act(() => {
button?.click();
});
......@@ -94,6 +97,7 @@ describe('LoadPluginFromUrl - component', () => {
const { store } = renderComponent({
plugins: {
...PLUGINS_INITIAL_STATE_MOCK,
activePlugins: {
pluginsId: [plugin.hash],
data: {
......
......@@ -3,6 +3,7 @@ import { Legend } from '@/components/Map/Legend';
import { MapAdditionalActions } from './MapAdditionalActions';
import { MapAdditionalOptions } from './MapAdditionalOptions';
import { MapViewer } from './MapViewer/MapViewer.component';
import { PluginsDrawer } from './PluginsDrawer';
export const Map = (): JSX.Element => (
<div
......@@ -11,6 +12,7 @@ export const Map = (): JSX.Element => (
>
<MapAdditionalOptions />
<Drawer />
<PluginsDrawer />
<MapViewer />
<Legend />
<MapAdditionalActions />
......
import { PLUGINS_CONTENT_ELEMENT_ID } from '@/constants/plugins';
import { PLUGINS_INITIAL_STATE_MOCK } from '@/redux/plugins/plugins.mock';
import { StoreType } from '@/redux/store';
import { InitialStoreState } from '@/utils/testing/getReduxStoreActionsListener';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { render, screen } from '@testing-library/react';
import { PluginContent } from './PluginContent.component';
const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStore);
return (
render(
<Wrapper>
<PluginContent />
</Wrapper>,
),
{
store,
}
);
};
describe('PluginContent - component', () => {
describe('when always', () => {
beforeEach(() => {
renderComponent({
plugins: {
...PLUGINS_INITIAL_STATE_MOCK,
},
});
});
it('should render plugins content', () => {
const element = screen.getByTestId('drawer-plugins-content');
expect(element).toBeInTheDocument();
expect(element.id).toBe(PLUGINS_CONTENT_ELEMENT_ID);
});
});
/*
NOTE:
Testing of <style jsx global> is not possible in Jest
*/
});
import { PLUGINS_CONTENT_ELEMENT_ID } from '@/constants/plugins';
import { selectedDrawerPluginSelector } from '@/redux/plugins/plugins.selectors';
import { useSelector } from 'react-redux';
import { getPluginContentStyle } from './utils/getPluginContentStyle';
export const PluginContent = (): JSX.Element => {
const selectedPlugin = useSelector(selectedDrawerPluginSelector);
return (
<>
<style jsx global>
{`
${getPluginContentStyle(selectedPlugin)}
`}
</style>
<div id={PLUGINS_CONTENT_ELEMENT_ID} data-testid="drawer-plugins-content" />
</>
);
};
import { PLUGINS_CONTENT_ELEMENT_ATTR_NAME } from '@/constants/plugins';
export const HIDE_ALL_ELEMENTS_STYLE = `
div[${PLUGINS_CONTENT_ELEMENT_ATTR_NAME}] {
display: none;
}
`;
export const SHOW_SELECTED_PLUGIN_ELEMENT_STYLE = (hash: string): string => `
div[${PLUGINS_CONTENT_ELEMENT_ATTR_NAME}='${hash}'] {
display: unset;
}
`;
export { PluginContent } from './PluginContent.component';
import { FIRST_ARRAY_ELEMENT } from '@/constants/common';
import { PLUGINS_MOCK } from '@/models/mocks/pluginsMock';
import {
HIDE_ALL_ELEMENTS_STYLE,
SHOW_SELECTED_PLUGIN_ELEMENT_STYLE,
} from '../PluginContent.constants';
import { getPluginContentStyle } from './getPluginContentStyle';
describe('getPluginContentStyle - util', () => {
describe('when selectedPlugin is NOT present', () => {
it('should return valid value', () => {
expect(getPluginContentStyle(undefined)).toBe(HIDE_ALL_ELEMENTS_STYLE);
});
});
describe('when selectedPlugin is present', () => {
const selectedPlugin = PLUGINS_MOCK[FIRST_ARRAY_ELEMENT];
it('should return valid value', () => {
const result = getPluginContentStyle(selectedPlugin);
expect(result.includes(HIDE_ALL_ELEMENTS_STYLE)).toBeTruthy();
expect(result.includes(SHOW_SELECTED_PLUGIN_ELEMENT_STYLE(selectedPlugin.hash))).toBeTruthy();
});
});
});
import { MinervaPlugin } from '@/types/models';
import {
HIDE_ALL_ELEMENTS_STYLE,
SHOW_SELECTED_PLUGIN_ELEMENT_STYLE,
} from '../PluginContent.constants';
export const getPluginContentStyle = (selectedPlugin?: MinervaPlugin): string => {
if (!selectedPlugin) {
return HIDE_ALL_ELEMENTS_STYLE;
}
return `
${HIDE_ALL_ELEMENTS_STYLE}
${SHOW_SELECTED_PLUGIN_ELEMENT_STYLE(selectedPlugin.hash)}
`;
};
import { StoreType } from '@/redux/store';
import { InitialStoreState } from '@/utils/testing/getReduxStoreActionsListener';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { PLUGINS_INITIAL_STATE_MOCK } from '@/redux/plugins/plugins.mock';
import { render, screen } from '@testing-library/react';
import { PluginsDrawer } from './PluginsDrawer.component';
import { PLUGINS_DRAWER_ROLE } from './PluginsDrawer.constants';
const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStore);
return (
render(
<Wrapper>
<PluginsDrawer />
</Wrapper>,
),
{
store,
}
);
};
describe('PluginsDrawer - component', () => {
describe('when drawer is open', () => {
beforeEach(() => {
renderComponent({
plugins: {
...PLUGINS_INITIAL_STATE_MOCK,
drawer: {
...PLUGINS_INITIAL_STATE_MOCK.drawer,
isOpen: false,
},
},
});
});
it('should render component without show class', () => {
const drawer = screen.getByRole(PLUGINS_DRAWER_ROLE);
expect(drawer).not.toHaveClass('translate-x-0');
});
});
describe('when drawer is NOT open', () => {
beforeEach(() => {
renderComponent({
plugins: {
...PLUGINS_INITIAL_STATE_MOCK,
drawer: {
...PLUGINS_INITIAL_STATE_MOCK.drawer,
isOpen: true,
},
},
});
});
it('should render component without show class', () => {
const drawer = screen.getByRole(PLUGINS_DRAWER_ROLE);
expect(drawer).toHaveClass('translate-x-0');
});
});
describe('when always', () => {
beforeEach(() => {
renderComponent({
plugins: {
...PLUGINS_INITIAL_STATE_MOCK,
},
});
});
it('should render plugins tab', () => {
const element = screen.getByTestId('drawer-plugins-tab');
expect(element).toBeInTheDocument();
});
it('should render plugins tab', () => {
const element = screen.getByTestId('drawer-plugins-header');
expect(element).toBeInTheDocument();
});
it('should render plugins content', () => {
const element = screen.getByTestId('drawer-plugins-content');
expect(element).toBeInTheDocument();
});
});
});
import { pluginsDrawerSelector } from '@/redux/plugins/plugins.selectors';
import { useSelector } from 'react-redux';
import { twMerge } from 'tailwind-merge';
import { PluginContent } from './PluginContent';
import { PLUGINS_DRAWER_ROLE } from './PluginsDrawer.constants';
import { PluginsHeader } from './PluginsHeader';
import { PluginsTabs } from './PluginsTabs/PluginsTabs.component';
export const PluginsDrawer = (): JSX.Element => {
const { isOpen } = useSelector(pluginsDrawerSelector);
return (
<div
className={twMerge(
'absolute bottom-0 right-0 top-[104px] z-20 h-calc-drawer w-[432px] translate-x-full transform border border-divide bg-white-pearl text-font-500 transition-all duration-500',
isOpen && 'translate-x-0',
)}
role={PLUGINS_DRAWER_ROLE}
>
<PluginsTabs />
<PluginsHeader />
<PluginContent />
</div>
);
};
export const PLUGINS_DRAWER_ROLE = 'plugins-drawer';
export const CLOSE_PLUGINS_DRAWER_BUTTON_ROLE = 'close-plugins-drawer-drawer-button';
import { FIRST_ARRAY_ELEMENT } from '@/constants/common';
import { PLUGINS_MOCK } from '@/models/mocks/pluginsMock';
import {
PLUGINS_INITIAL_STATE_LIST_MOCK,
PLUGINS_INITIAL_STATE_MOCK,
} from '@/redux/plugins/plugins.mock';
import { AppDispatch, RootState } from '@/redux/store';
import { PluginsManager } from '@/services/pluginsManager';
import { MinervaPlugin } from '@/types/models';
import {
InitialStoreState,
getReduxStoreWithActionsListener,
} from '@/utils/testing/getReduxStoreActionsListener';
import { render, screen, waitFor } from '@testing-library/react';
import axios, { HttpStatusCode } from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { MockStoreEnhanced } from 'redux-mock-store';
import { PluginHeaderInfo } from './PluginHeaderInfo.component';
import { RELOAD_PLUGIN_DRAWER_BUTTON_ROLE } from './PluginHeaderInfo.constants';
const mockedAxiosClient = new MockAdapter(axios);
const renderComponent = (
initialStore: InitialStoreState,
plugin: MinervaPlugin,
): { store: MockStoreEnhanced<Partial<RootState>, AppDispatch> } => {
const { Wrapper, store } = getReduxStoreWithActionsListener(initialStore);
return (
render(
<Wrapper>
<PluginHeaderInfo plugin={plugin} />
</Wrapper>,
),
{
store,
}
);
};
const removePluginContentSpy = jest.spyOn(PluginsManager, 'removePluginContent');
const setHashedPluginSpy = jest.spyOn(PluginsManager, 'setHashedPlugin');
const PLUGIN = PLUGINS_MOCK[FIRST_ARRAY_ELEMENT];
const STATE = {
plugins: {
...PLUGINS_INITIAL_STATE_MOCK,
drawer: {
...PLUGINS_INITIAL_STATE_MOCK.drawer,
currentPluginHash: PLUGIN.hash,
},
activePlugins: {
data: {
[PLUGIN.hash]: PLUGIN,
},
pluginsId: [PLUGIN.hash],
},
list: PLUGINS_INITIAL_STATE_LIST_MOCK,
},
};
describe('PluginHeaderInfo - component', () => {
it('renders plugin title and name', () => {
renderComponent(STATE, PLUGIN);
const title = screen.getByText(`Plugin:`, { exact: false });
const pluginName = screen.getByText(PLUGIN.name, { exact: false });
expect(title).toBeInTheDocument();
expect(pluginName).toBeInTheDocument();
});
it('renders plugin reload button', () => {
renderComponent(STATE, PLUGIN);
const button = screen.getByRole(RELOAD_PLUGIN_DRAWER_BUTTON_ROLE);
expect(button).toBeInTheDocument();
});
it('reload plugin on reload button', async () => {
const { store } = renderComponent(STATE, PLUGIN);
mockedAxiosClient.onGet(PLUGIN.urls[FIRST_ARRAY_ELEMENT]).reply(HttpStatusCode.Ok, '');
const button = screen.getByRole(RELOAD_PLUGIN_DRAWER_BUTTON_ROLE);
button.click();
const actions = store.getActions();
expect(removePluginContentSpy).toHaveBeenCalledWith({
hash: PLUGIN.hash,
});
expect(actions).toEqual([
{ payload: { pluginId: '5e3fcb59588cc311ef9839feea6382eb' }, type: 'plugins/removePlugin' },
]);
await waitFor(() => {
expect(setHashedPluginSpy).toHaveBeenCalledWith({
pluginScript: '',
pluginUrl: 'https://minerva-service.lcsb.uni.lu/plugins/disease-associations/plugin.js',
});
});
});
});
import { useLoadPlugin } from '@/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin';
import { FIRST_ARRAY_ELEMENT } from '@/constants/common';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { setCurrentDrawerPluginHash } from '@/redux/plugins/plugins.slice';
import { Icon } from '@/shared/Icon';
import { MinervaPlugin } from '@/types/models';
import { RELOAD_PLUGIN_DRAWER_BUTTON_ROLE } from './PluginHeaderInfo.constants';
interface Props {
plugin: MinervaPlugin;
}
export const PluginHeaderInfo = ({ plugin }: Props): JSX.Element => {
const dispatch = useAppDispatch();
const { reloadPlugin } = useLoadPlugin({
hash: plugin.hash,
pluginUrl: plugin.urls[FIRST_ARRAY_ELEMENT],
onPluginLoaded: () => {
dispatch(setCurrentDrawerPluginHash(plugin.hash));
},
});
return (
<>
Plugin: <b>{plugin.name}</b>
<button type="button" onClick={reloadPlugin} role={RELOAD_PLUGIN_DRAWER_BUTTON_ROLE}>
<Icon name="reload" />
</button>
</>
);
};
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