Skip to content
Snippets Groups Projects
Commit 1e9b0179 authored by Adrian Orłów's avatar Adrian Orłów
Browse files

feat: add drawer available plugins

parent 459df684
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...,!115feat: add drawer available plugins
Pipeline #84983 passed
Showing
with 347 additions and 6 deletions
...@@ -15,7 +15,7 @@ export const NavBar = (): JSX.Element => { ...@@ -15,7 +15,7 @@ export const NavBar = (): JSX.Element => {
}; };
const openDrawerPlugins = (): void => { const openDrawerPlugins = (): void => {
dispatch(openDrawer('plugins')); dispatch(openDrawer('available-plugins'));
}; };
const openDrawerExport = (): void => { const openDrawerExport = (): void => {
......
import { PLUGINS_MOCK } from '@/models/mocks/pluginsMock';
import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures';
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 { AvailablePluginsDrawer } from './AvailablePluginsDrawer.component';
const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStore);
return (
render(
<Wrapper>
<AvailablePluginsDrawer />
</Wrapper>,
),
{
store,
}
);
};
describe('AvailablePluginsDrawer - component', () => {
describe('when always', () => {
it('should render drawer heading', () => {
renderComponent(INITIAL_STORE_STATE_MOCK);
const drawerTitle = screen.getByText('Available plugins');
expect(drawerTitle).toBeInTheDocument();
});
it('should render load plugin from url', () => {
renderComponent(INITIAL_STORE_STATE_MOCK);
const loadPluginFromUrlInput = screen.getByTestId('load-plugin-input-url');
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,
},
},
});
const pluginLabel = screen.getByText(currentPlugin.name);
expect(pluginLabel).toBeInTheDocument();
});
});
});
import { publicPluginsListSelector } from '@/redux/plugins/plugins.selectors';
import { DrawerHeading } from '@/shared/DrawerHeading';
import { useSelector } from 'react-redux';
import { LoadPlugin } from './LoadPlugin';
import { LoadPluginFromUrl } from './LoadPluginFromUrl';
export const AvailablePluginsDrawer = (): JSX.Element => {
const publicPlugins = useSelector(publicPluginsListSelector);
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 />
{publicPlugins.map(plugin => (
<LoadPlugin key={plugin.hash} plugin={plugin} />
))}
</div>
</div>
);
};
import { FIRST_ARRAY_ELEMENT } from '@/constants/common';
import { PLUGINS_MOCK } from '@/models/mocks/pluginsMock';
import { render, screen } from '@testing-library/react';
import { LoadPlugin, Props } from './LoadPlugin.component';
const renderComponent = ({ plugin }: Props): void => {
render(<LoadPlugin plugin={plugin} />);
};
describe('LoadPlugin - component', () => {
describe('when always', () => {
const plugin = PLUGINS_MOCK[FIRST_ARRAY_ELEMENT];
it('renders plugin name', () => {
renderComponent({ plugin });
const title = screen.getByText(plugin.name);
expect(title).toBeInTheDocument();
});
it('renders plugin load button', () => {
renderComponent({ plugin });
const loadButton = screen.getByText('Load');
expect(loadButton.tagName).toBe('BUTTON');
expect(loadButton).toBeInTheDocument();
});
});
});
import { Button } from '@/shared/Button';
import { MinervaPlugin } from '@/types/models';
export interface Props {
plugin: MinervaPlugin;
}
export const LoadPlugin = ({ plugin }: Props): JSX.Element => {
const handleLoadPlugin = (): void => {
// TODO: handleLoadPlugin
};
return (
<div className="flex w-full items-center justify-between">
<span className="text-cetacean-blue">{plugin.name}</span>
<Button
variantStyles="secondary"
className="h-10 self-end rounded-e rounded-s"
onClick={handleLoadPlugin}
>
Load
</Button>
</div>
);
};
export { LoadPlugin } from './LoadPlugin.component';
import { render, screen } from '@testing-library/react';
import { LoadPluginFromUrl } from './LoadPluginFromUrl.component';
const renderComponent = (): void => {
render(<LoadPluginFromUrl />);
};
describe('LoadPluginFromUrl - component', () => {
describe('when always', () => {
it('renders plugin input label', () => {
renderComponent();
const pluginInputLabel = screen.getByLabelText('URL:');
expect(pluginInputLabel).toBeInTheDocument();
});
it('renders plugin input', () => {
renderComponent();
const pluginInput = screen.getByTestId('load-plugin-input-url');
expect(pluginInput).toBeInTheDocument();
});
it('renders plugin load button', () => {
renderComponent();
const loadButton = screen.getByText('Load');
expect(loadButton.tagName).toBe('BUTTON');
expect(loadButton).toBeInTheDocument();
});
});
});
import { Button } from '@/shared/Button';
import { useState } from 'react';
export const LoadPluginFromUrl = (): JSX.Element => {
const [url, setUrl] = useState<string>('');
const handleLoadPlugin = (): void => {
// TODO: handleLoadPlugin
};
return (
<div className="flex w-full">
<label className="flex w-full flex-col gap-2 text-sm text-cetacean-blue">
<span>URL:</span>
<input
className="h-10 w-full bg-cultured p-3"
type="url"
value={url}
onChange={(e): void => setUrl(e.target.value)}
data-testid="load-plugin-input-url"
/>
</label>
<Button
variantStyles="secondary"
className="h-10 self-end rounded-e rounded-s"
onClick={handleLoadPlugin}
>
Load
</Button>
</div>
);
};
export { LoadPluginFromUrl } from './LoadPluginFromUrl.component';
export { AvailablePluginsDrawer } from './AvailablePluginsDrawer.component';
...@@ -2,13 +2,14 @@ import { DRAWER_ROLE } from '@/components/Map/Drawer/Drawer.constants'; ...@@ -2,13 +2,14 @@ import { DRAWER_ROLE } from '@/components/Map/Drawer/Drawer.constants';
import { drawerSelector } from '@/redux/drawer/drawer.selectors'; import { drawerSelector } from '@/redux/drawer/drawer.selectors';
import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { ReactionDrawer } from './ReactionDrawer'; import { AvailablePluginsDrawer } from './AvailablePluginsDrawer';
import { SearchDrawerWrapper as SearchDrawerContent } from './SearchDrawerWrapper';
import { SubmapsDrawer } from './SubmapsDrawer';
import { OverlaysDrawer } from './OverlaysDrawer';
import { BioEntityDrawer } from './BioEntityDrawer/BioEntityDrawer.component'; import { BioEntityDrawer } from './BioEntityDrawer/BioEntityDrawer.component';
import { ExportDrawer } from './ExportDrawer'; import { ExportDrawer } from './ExportDrawer';
import { OverlaysDrawer } from './OverlaysDrawer';
import { ProjectInfoDrawer } from './ProjectInfoDrawer'; import { ProjectInfoDrawer } from './ProjectInfoDrawer';
import { ReactionDrawer } from './ReactionDrawer';
import { SearchDrawerWrapper as SearchDrawerContent } from './SearchDrawerWrapper';
import { SubmapsDrawer } from './SubmapsDrawer';
export const Drawer = (): JSX.Element => { export const Drawer = (): JSX.Element => {
const { isOpen, drawerName } = useAppSelector(drawerSelector); const { isOpen, drawerName } = useAppSelector(drawerSelector);
...@@ -28,6 +29,7 @@ export const Drawer = (): JSX.Element => { ...@@ -28,6 +29,7 @@ export const Drawer = (): JSX.Element => {
{isOpen && drawerName === 'bio-entity' && <BioEntityDrawer />} {isOpen && drawerName === 'bio-entity' && <BioEntityDrawer />}
{isOpen && drawerName === 'project-info' && <ProjectInfoDrawer />} {isOpen && drawerName === 'project-info' && <ProjectInfoDrawer />}
{isOpen && drawerName === 'export' && <ExportDrawer />} {isOpen && drawerName === 'export' && <ExportDrawer />}
{isOpen && drawerName === 'available-plugins' && <AvailablePluginsDrawer />}
</div> </div>
); );
}; };
import { MinervaPlugin } from '@/types/models';
export const PLUGINS_MOCK: MinervaPlugin[] = [
{
hash: '5e3fcb59588cc311ef9839feea6382eb',
name: 'Disease-variant associations',
version: '1.0.0',
isPublic: true,
isDefault: false,
urls: ['https://minerva-service.lcsb.uni.lu/plugins/disease-associations/plugin.js'],
},
{
hash: '20df86476c311824bbfe73d1034af89e',
name: 'GSEA',
version: '0.9.2',
isPublic: true,
isDefault: false,
urls: ['https://minerva-service.lcsb.uni.lu/plugins/gsea/plugin.js'],
},
{
hash: '5314b9f996e56e67f0dad65e7df8b73b',
name: 'PD map guide',
version: '1.0.2',
isPublic: true,
isDefault: false,
urls: ['https://minerva-service.lcsb.uni.lu/plugins/guide/plugin.js'],
},
{
hash: 'b85ae2f4cd67736489b5fd2b635b1013',
name: 'Map exploation',
version: '1.0.0',
isPublic: true,
isDefault: false,
urls: ['https://minerva-service.lcsb.uni.lu/plugins/exploration/plugin.js'],
},
{
hash: '77c32edf387652dfaad8a20f2a0ce76b',
name: 'Drug reactions',
version: '1.0.0',
isPublic: true,
isDefault: false,
urls: ['https://minerva-service.lcsb.uni.lu/plugins/drug-reactions/plugin.js'],
},
];
import { z } from 'zod';
export const pluginSchema = z.object({
hash: z.string(),
name: z.string(),
version: z.string(),
isPublic: z.boolean(),
isDefault: z.boolean(),
urls: z.array(z.string()),
});
import { PROJECT_ID } from '@/constants'; import { PROJECT_ID } from '@/constants';
import { PerfectSearchParams } from '@/types/search';
import { Point } from '@/types/map'; import { Point } from '@/types/map';
import { PerfectSearchParams } from '@/types/search';
export const apiPath = { export const apiPath = {
getBioEntityContentsStringWithQuery: ({ getBioEntityContentsStringWithQuery: ({
...@@ -63,4 +63,5 @@ export const apiPath = { ...@@ -63,4 +63,5 @@ export const apiPath = {
getSourceFile: (): string => `/projects/${PROJECT_ID}:downloadSource`, getSourceFile: (): string => `/projects/${PROJECT_ID}:downloadSource`,
getMesh: (meshId: string): string => `mesh/${meshId}`, getMesh: (meshId: string): string => `mesh/${meshId}`,
getTaxonomy: (taxonomyId: string): string => `taxonomy/${taxonomyId}`, getTaxonomy: (taxonomyId: string): string => `taxonomy/${taxonomyId}`,
getAllPlugins: (): string => `/plugins/`,
}; };
import { PluginsState } from './plugins.types';
export const PLUGINS_INITIAL_STATE: PluginsState = {
list: {
data: [],
loading: 'idle',
error: { name: '', message: '' },
},
};
import { DEFAULT_ERROR } from '@/constants/errors';
import { PluginsList, PluginsState } from './plugins.types';
export const PLUGINS_INITIAL_STATE_LIST_MOCK: PluginsList = {
data: undefined,
loading: 'idle',
error: DEFAULT_ERROR,
};
export const PLUGINS_INITIAL_STATE_MOCK: PluginsState = {
list: PLUGINS_INITIAL_STATE_LIST_MOCK,
};
import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
import { getAllPlugins } from './plugins.thunks';
import { PluginsState } from './plugins.types';
export const getAllPluginsReducer = (builder: ActionReducerMapBuilder<PluginsState>): void => {
builder.addCase(getAllPlugins.pending, state => {
state.list.loading = 'pending';
});
builder.addCase(getAllPlugins.fulfilled, (state, action) => {
state.list.data = action.payload || [];
state.list.loading = 'succeeded';
});
builder.addCase(getAllPlugins.rejected, state => {
state.list.loading = 'failed';
// TODO to discuss manage state of failure
});
};
import { createSelector } from '@reduxjs/toolkit';
import { rootSelector } from '../root/root.selectors';
export const pluginsSelector = createSelector(rootSelector, state => state.plugins);
export const pluginsListSelector = createSelector(pluginsSelector, plugins => {
return plugins.list;
});
export const pluginsListDataSelector = createSelector(pluginsListSelector, pluginsList => {
return pluginsList.data;
});
export const publicPluginsListSelector = createSelector(
pluginsListDataSelector,
pluginsListData => {
return (pluginsListData || []).filter(plugin => plugin.isPublic);
},
);
import { createSlice } from '@reduxjs/toolkit';
import { PLUGINS_INITIAL_STATE } from './plugins.constants';
import { getAllPluginsReducer } from './plugins.reducers';
const pluginsState = createSlice({
name: 'plugins',
initialState: PLUGINS_INITIAL_STATE,
reducers: {},
extraReducers: builder => {
getAllPluginsReducer(builder);
},
});
export default pluginsState.reducer;
import { pluginSchema } from '@/models/pluginSchema';
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';
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 : [];
},
);
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