From 79664147976e785d9c20cf1e6de2bccc1d7ce1c8 Mon Sep 17 00:00:00 2001
From: mateusz-winiarczyk <mateusz.winiarczyk@appunite.com>
Date: Wed, 20 Mar 2024 15:13:28 +0100
Subject: [PATCH] feat(plugins): legend tabs (MIN-231)

---
 docs/plugins/legend.md                        |  45 +++++
 .../LoadPlugin/hooks/useLoadPlugin.test.ts    | 161 +++++++++++++-----
 .../LoadPlugin/hooks/useLoadPlugin.ts         |  14 ++
 .../Map/Legend/Legend.component.test.tsx      | 100 ++++++++++-
 .../Map/Legend/Legend.component.tsx           |  10 +-
 .../LegendImages.component.test.tsx           |   3 +-
 .../LegendImages/LegendImages.component.tsx   |   8 +-
 .../LegendTab/LegendTab.component.test.tsx    |  45 +++++
 .../LegendTab/LegendTab.component.tsx         |  28 +++
 .../Map/Legend/LegendTabs/LegendTab/index.ts  |   1 +
 .../LegendTabs/LegendTabs.component.tsx       |  16 ++
 src/components/Map/Legend/LegendTabs/index.ts |   1 +
 .../PluginHeaderInfo.component.test.tsx       |   6 +
 .../LoadPluginElement.component.test.tsx      |   2 +
 .../PluginOpenButton.component.test.tsx       |   2 +
 .../PluginsHeader.component.test.tsx          |   3 +
 .../PluginSingleTab.component.test.tsx        |  17 +-
 .../PluginsTabs.component.test.tsx            |   2 +
 src/redux/legend/legend.constants.ts          |   9 +-
 src/redux/legend/legend.mock.ts               |   3 +-
 src/redux/legend/legend.reducers.ts           |  35 +++-
 src/redux/legend/legend.selectors.ts          |  41 ++++-
 src/redux/legend/legend.slice.ts              |  22 ++-
 src/redux/legend/legend.types.ts              |   4 +-
 .../pluginsManager/legend/removeLegend.ts     |  14 ++
 .../pluginsManager/legend/setLegend.ts        |  13 ++
 src/services/pluginsManager/pluginsManager.ts |   6 +
 27 files changed, 540 insertions(+), 71 deletions(-)
 create mode 100644 docs/plugins/legend.md
 create mode 100644 src/components/Map/Legend/LegendTabs/LegendTab/LegendTab.component.test.tsx
 create mode 100644 src/components/Map/Legend/LegendTabs/LegendTab/LegendTab.component.tsx
 create mode 100644 src/components/Map/Legend/LegendTabs/LegendTab/index.ts
 create mode 100644 src/components/Map/Legend/LegendTabs/LegendTabs.component.tsx
 create mode 100644 src/components/Map/Legend/LegendTabs/index.ts
 create mode 100644 src/services/pluginsManager/legend/removeLegend.ts
 create mode 100644 src/services/pluginsManager/legend/setLegend.ts

diff --git a/docs/plugins/legend.md b/docs/plugins/legend.md
new file mode 100644
index 00000000..dcc3e588
--- /dev/null
+++ b/docs/plugins/legend.md
@@ -0,0 +1,45 @@
+### Legend
+
+#### Set Legend
+
+To set a legend for a specific plugin, plugins can use the `setLegend` method in `legend` object returned by `window.minerva.plugins.registerPlugin`. This method takes one argument:
+
+- legend: an array containing image urls
+
+##### Example of setLegend method usage:
+
+```javascript
+const {
+  element,
+  legend: { setLegend },
+} = minerva.plugins.registerPlugin({
+  pluginName,
+  pluginVersion,
+  pluginUrl,
+});
+
+setLegend([
+  'https://lux1.atcomp.pl//minerva/resources/images/legend_d.png',
+  'https://lux1.atcomp.pl//minerva/resources/images/legend_a.png',
+  'https://lux1.atcomp.pl//minerva/resources/images/legend_b.png',
+]);
+```
+
+#### Remove Legend
+
+To remove a legend associated with a specific plugin, plugins can use the `removeLegend` method in the `legend` object returned by `window.minerva.plugins.registerPlugin`.
+
+##### Example of removeLegend method usage:
+
+```javascript
+const {
+  element,
+  legend: { removeLegend },
+} = minerva.plugins.registerPlugin({
+  pluginName,
+  pluginVersion,
+  pluginUrl,
+});
+
+removeLegend();
+```
diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.test.ts b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.test.ts
index f54c8f5b..3b826b1a 100644
--- a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.test.ts
+++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.test.ts
@@ -8,55 +8,38 @@ import axios, { HttpStatusCode } from 'axios';
 import MockAdapter from 'axios-mock-adapter';
 import { act } from 'react-dom/test-utils';
 import { showToast } from '@/utils/showToast';
+import { LEGEND_INITIAL_STATE_MOCK } from '@/redux/legend/legend.mock';
 import { useLoadPlugin } from './useLoadPlugin';
 
 const mockedAxiosClient = new MockAdapter(axios);
 jest.mock('../../../../../../services/pluginsManager/pluginsManager');
 jest.mock('../../../../../../utils/showToast');
 
+const STATE_MOCK = {
+  ...INITIAL_STORE_STATE_MOCK,
+  plugins: {
+    ...INITIAL_STORE_STATE_MOCK.plugins,
+    activePlugins: {
+      pluginsId: [pluginFixture.hash],
+      data: {
+        [pluginFixture.hash]: pluginFixture,
+      },
+    },
+    list: INITIAL_STORE_STATE_MOCK.plugins.list,
+  },
+  legend: {
+    ...LEGEND_INITIAL_STATE_MOCK,
+    pluginLegend: {
+      [pluginFixture.hash]: ['url1', 'url2'],
+    },
+  },
+};
+
 describe('useLoadPlugin', () => {
   afterEach(() => {
     jest.restoreAllMocks();
   });
-  it('should unload plugin successfully', async () => {
-    const { Wrapper, store } = getReduxStoreWithActionsListener({
-      ...INITIAL_STORE_STATE_MOCK,
-      plugins: {
-        ...INITIAL_STORE_STATE_MOCK.plugins,
-        activePlugins: {
-          pluginsId: [pluginFixture.hash],
-          data: {
-            [pluginFixture.hash]: pluginFixture,
-          },
-        },
-        list: INITIAL_STORE_STATE_MOCK.plugins.list,
-      },
-    });
-
-    const {
-      result: {
-        current: { isPluginActive, isPluginLoading, togglePlugin },
-      },
-    } = renderHook(
-      () => useLoadPlugin({ hash: pluginFixture.hash, pluginUrl: pluginFixture.urls[0] }),
-      {
-        wrapper: Wrapper,
-      },
-    );
-
-    expect(isPluginActive).toBe(true);
-    expect(isPluginLoading).toBe(false);
 
-    act(() => {
-      togglePlugin();
-    });
-
-    const actions = store.getActions();
-    expect(actions[0]).toEqual({
-      payload: { pluginId: pluginFixture.hash },
-      type: 'plugins/removePlugin',
-    });
-  });
   it('should load plugin successfully', async () => {
     const hash = 'pluginHash';
     const pluginUrl = 'http://example.com/plugin.js';
@@ -115,4 +98,106 @@ describe('useLoadPlugin', () => {
       });
     });
   });
+  describe('when unload plugin', () => {
+    it('should unload plugin successfully', async () => {
+      const { Wrapper, store } = getReduxStoreWithActionsListener(STATE_MOCK);
+
+      const {
+        result: {
+          current: { isPluginActive, isPluginLoading, togglePlugin },
+        },
+      } = renderHook(
+        () => useLoadPlugin({ hash: pluginFixture.hash, pluginUrl: pluginFixture.urls[0] }),
+        {
+          wrapper: Wrapper,
+        },
+      );
+
+      expect(isPluginActive).toBe(true);
+      expect(isPluginLoading).toBe(false);
+
+      act(() => {
+        togglePlugin();
+      });
+
+      const actions = store.getActions();
+      expect(actions[0]).toEqual({
+        payload: { pluginId: pluginFixture.hash },
+        type: 'plugins/removePlugin',
+      });
+    });
+    it('should remove plugin legend', () => {
+      const { Wrapper, store } = getReduxStoreWithActionsListener(STATE_MOCK);
+
+      const {
+        result: {
+          current: { togglePlugin },
+        },
+      } = renderHook(
+        () => useLoadPlugin({ hash: pluginFixture.hash, pluginUrl: pluginFixture.urls[0] }),
+        {
+          wrapper: Wrapper,
+        },
+      );
+
+      act(() => {
+        togglePlugin();
+      });
+
+      const actions = store.getActions();
+
+      expect(actions).toEqual([
+        {
+          payload: { pluginId: pluginFixture.hash },
+          type: 'plugins/removePlugin',
+        },
+        {
+          payload: pluginFixture.hash,
+          type: 'legend/removePluginLegend',
+        },
+      ]);
+    });
+
+    it('should set active legend to main legend if plugin legend was active one', () => {
+      const { Wrapper, store } = getReduxStoreWithActionsListener({
+        ...STATE_MOCK,
+        legend: {
+          ...STATE_MOCK.legend,
+          activeLegendId: pluginFixture.hash,
+        },
+      });
+
+      const {
+        result: {
+          current: { togglePlugin },
+        },
+      } = renderHook(
+        () => useLoadPlugin({ hash: pluginFixture.hash, pluginUrl: pluginFixture.urls[0] }),
+        {
+          wrapper: Wrapper,
+        },
+      );
+
+      act(() => {
+        togglePlugin();
+      });
+
+      const actions = store.getActions();
+
+      expect(actions).toEqual([
+        {
+          payload: { pluginId: pluginFixture.hash },
+          type: 'plugins/removePlugin',
+        },
+        {
+          payload: undefined,
+          type: 'legend/setDefaultLegendId',
+        },
+        {
+          payload: pluginFixture.hash,
+          type: 'legend/removePluginLegend',
+        },
+      ]);
+    });
+  });
 });
diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.ts b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.ts
index ebce11ec..a7208789 100644
--- a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.ts
+++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.ts
@@ -10,6 +10,8 @@ import { PluginsManager } from '@/services/pluginsManager';
 import { showToast } from '@/utils/showToast';
 import axios from 'axios';
 import { getErrorMessage } from '@/utils/getErrorMessage';
+import { removePluginLegend, setDefaultLegendId } from '@/redux/legend/legend.slice';
+import { isActiveLegendSelector } from '@/redux/legend/legend.selectors';
 import { PLUGIN_LOADING_ERROR_PREFIX } from '../../AvailablePluginsDrawer.constants';
 
 type UseLoadPluginReturnType = {
@@ -36,6 +38,7 @@ export const useLoadPlugin = ({
   const isPluginActive = useAppSelector(state => isPluginActiveSelector(state, hash));
   const isPluginLoading = useAppSelector(state => isPluginLoadingSelector(state, hash));
   const isPluginSelected = useAppSelector(state => isPluginSelectedSelector(state, hash));
+  const isActivePluginLegend = useAppSelector(state => isActiveLegendSelector(state, hash));
 
   const dispatch = useAppDispatch();
 
@@ -66,8 +69,19 @@ export const useLoadPlugin = ({
     }
   };
 
+  const handleRemoveLegend = (): void => {
+    if (isActivePluginLegend) {
+      dispatch(setDefaultLegendId());
+    }
+
+    dispatch(removePluginLegend(hash));
+  };
+
   const handleUnloadPlugin = (): void => {
     dispatch(removePlugin({ pluginId: hash }));
+
+    handleRemoveLegend();
+
     PluginsManager.removePluginContent({ hash });
   };
 
diff --git a/src/components/Map/Legend/Legend.component.test.tsx b/src/components/Map/Legend/Legend.component.test.tsx
index 4ad65a5a..266c109c 100644
--- a/src/components/Map/Legend/Legend.component.test.tsx
+++ b/src/components/Map/Legend/Legend.component.test.tsx
@@ -1,20 +1,41 @@
-import { currentLegendImagesSelector, legendSelector } from '@/redux/legend/legend.selectors';
+import {
+  allLegendsNamesAndIdsSelector,
+  currentLegendImagesSelector,
+  legendSelector,
+  pluginLegendsSelector,
+} from '@/redux/legend/legend.selectors';
 import { StoreType } from '@/redux/store';
 import {
   InitialStoreState,
   getReduxWrapperWithStore,
 } from '@/utils/testing/getReduxWrapperWithStore';
 import { render, screen, within } from '@testing-library/react';
-import { Legend } from './Legend.component';
+import { PLUGINS_INITIAL_STATE_MOCK } from '@/redux/plugins/plugins.mock';
+import { pluginFixture } from '@/models/fixtures/pluginFixture';
+import { DEFAULT_LEGEND_TAB } from '@/redux/legend/legend.constants';
 import { LEGEND_ROLE } from './Legend.constants';
+import { Legend } from './Legend.component';
+
+const PLUGIN_ID = '1';
+const PLUGIN_NAME = 'Plugin Custom Name';
+const LEGEND_IMAGES = [
+  'https://lux1.atcomp.pl//minerva/resources/images/legend_d.png',
+  'https://lux1.atcomp.pl//minerva/resources/images/legend_a.png',
+  'https://lux1.atcomp.pl//minerva/resources/images/legend_b.png',
+];
 
 jest.mock('../../../redux/legend/legend.selectors', () => ({
   legendSelector: jest.fn(),
   currentLegendImagesSelector: jest.fn(),
+  allLegendsNamesAndIdsSelector: jest.fn(),
+  pluginLegendsSelector: jest.fn(),
+  isActiveLegendSelector: jest.fn(),
 }));
 
 const legendSelectorMock = legendSelector as unknown as jest.Mock;
 const currentLegendImagesSelectorMock = currentLegendImagesSelector as unknown as jest.Mock;
+const pluginLegendsSelectorMock = pluginLegendsSelector as unknown as jest.Mock;
+const allLegendsNamesAndIdsSelectorMock = allLegendsNamesAndIdsSelector as unknown as jest.Mock;
 
 const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => {
   const { Wrapper, store } = getReduxWrapperWithStore(initialStore);
@@ -32,7 +53,19 @@ const renderComponent = (initialStore?: InitialStoreState): { store: StoreType }
 
 describe('Legend - component', () => {
   beforeAll(() => {
-    currentLegendImagesSelectorMock.mockImplementation(() => []);
+    allLegendsNamesAndIdsSelectorMock.mockImplementation(() => [
+      DEFAULT_LEGEND_TAB,
+      {
+        id: PLUGIN_ID,
+        name: PLUGIN_NAME,
+      },
+    ]);
+
+    pluginLegendsSelectorMock.mockImplementation(() => ({
+      PLUGIN_ID: LEGEND_IMAGES,
+    }));
+
+    currentLegendImagesSelectorMock.mockImplementation(() => LEGEND_IMAGES);
   });
 
   describe('when is closed', () => {
@@ -72,4 +105,65 @@ describe('Legend - component', () => {
       expect(legendImages).toBeInTheDocument();
     });
   });
+  describe('when loaded plugin has own legend', () => {
+    it('should display legend tabs', () => {
+      legendSelectorMock.mockImplementation(() => ({
+        isOpen: false,
+        pluginLegend: {
+          PLUGIN_ID: LEGEND_IMAGES,
+        },
+        activeLegendId: 'MAIN',
+      }));
+      renderComponent({
+        plugins: {
+          ...PLUGINS_INITIAL_STATE_MOCK,
+          activePlugins: {
+            data: {
+              PLUGIN_ID: {
+                ...pluginFixture,
+                hash: PLUGIN_ID,
+                name: PLUGIN_NAME,
+              },
+            },
+            pluginsId: [PLUGIN_ID],
+          },
+        },
+      });
+
+      expect(screen.getByText('Plugin Custom Name')).toBeVisible();
+      expect(screen.getByText('Main Legend')).toBeVisible();
+    });
+  });
+  describe('when loaded plugin does not have own legend', () => {
+    it('should not display legend tabs and display only main legend', () => {
+      allLegendsNamesAndIdsSelectorMock.mockImplementation(() => [DEFAULT_LEGEND_TAB]);
+
+      pluginLegendsSelectorMock.mockImplementation(() => ({}));
+
+      currentLegendImagesSelectorMock.mockImplementation(() => LEGEND_IMAGES);
+      legendSelectorMock.mockImplementation(() => ({
+        isOpen: false,
+        pluginLegend: {},
+        activeLegendId: 'MAIN',
+      }));
+      renderComponent({
+        plugins: {
+          ...PLUGINS_INITIAL_STATE_MOCK,
+          activePlugins: {
+            data: {
+              PLUGIN_ID: {
+                ...pluginFixture,
+                hash: PLUGIN_ID,
+                name: PLUGIN_NAME,
+              },
+            },
+            pluginsId: [PLUGIN_ID],
+          },
+        },
+      });
+
+      expect(screen.queryByText('Plugin Custom Name')).not.toBeInTheDocument();
+      expect(screen.queryByText('Main Legend')).not.toBeInTheDocument();
+    });
+  });
 });
diff --git a/src/components/Map/Legend/Legend.component.tsx b/src/components/Map/Legend/Legend.component.tsx
index 7414765e..e515483a 100644
--- a/src/components/Map/Legend/Legend.component.tsx
+++ b/src/components/Map/Legend/Legend.component.tsx
@@ -1,13 +1,20 @@
 import { useAppSelector } from '@/redux/hooks/useAppSelector';
-import { legendSelector } from '@/redux/legend/legend.selectors';
+import { legendSelector, pluginLegendsSelector } from '@/redux/legend/legend.selectors';
 import * as React from 'react';
 import { twMerge } from 'tailwind-merge';
+import { ZERO } from '@/constants/common';
 import { LEGEND_ROLE } from './Legend.constants';
 import { LegendHeader } from './LegendHeader';
 import { LegendImages } from './LegendImages';
+import { LegendTabs } from './LegendTabs';
 
 export const Legend: React.FC = () => {
   const { isOpen } = useAppSelector(legendSelector);
+  const allPluginLegends = useAppSelector(pluginLegendsSelector);
+  const isAnyPluginLegendExists = React.useMemo(
+    () => Object.values(allPluginLegends).length > ZERO,
+    [allPluginLegends],
+  );
 
   return (
     <div
@@ -18,6 +25,7 @@ export const Legend: React.FC = () => {
       role={LEGEND_ROLE}
     >
       <LegendHeader />
+      {isAnyPluginLegendExists ? <LegendTabs /> : null}
       <LegendImages />
     </div>
   );
diff --git a/src/components/Map/Legend/LegendImages/LegendImages.component.test.tsx b/src/components/Map/Legend/LegendImages/LegendImages.component.test.tsx
index e6d2681f..87fb0757 100644
--- a/src/components/Map/Legend/LegendImages/LegendImages.component.test.tsx
+++ b/src/components/Map/Legend/LegendImages/LegendImages.component.test.tsx
@@ -1,4 +1,3 @@
-import { BASE_MAP_IMAGES_URL } from '@/constants';
 import { currentLegendImagesSelector, legendSelector } from '@/redux/legend/legend.selectors';
 import { StoreType } from '@/redux/store';
 import {
@@ -60,7 +59,7 @@ describe('LegendImages - component', () => {
       const imgElement = screen.getByAltText(partialUrl);
 
       expect(imgElement).toBeInTheDocument();
-      expect(imgElement.getAttribute('src')).toBe(`${BASE_MAP_IMAGES_URL}/minerva/${partialUrl}`);
+      expect(imgElement.getAttribute('src')).toBe(partialUrl);
     });
   });
 });
diff --git a/src/components/Map/Legend/LegendImages/LegendImages.component.tsx b/src/components/Map/Legend/LegendImages/LegendImages.component.tsx
index dfc49d8f..8a56a050 100644
--- a/src/components/Map/Legend/LegendImages/LegendImages.component.tsx
+++ b/src/components/Map/Legend/LegendImages/LegendImages.component.tsx
@@ -1,5 +1,4 @@
 /* eslint-disable @next/next/no-img-element */
-import { BASE_MAP_IMAGES_URL } from '@/constants';
 import { useAppSelector } from '@/redux/hooks/useAppSelector';
 import { currentLegendImagesSelector } from '@/redux/legend/legend.selectors';
 
@@ -12,12 +11,7 @@ export const LegendImages: React.FC = () => {
       className="flex items-center justify-between overflow-x-auto border-b border-b-divide px-6 py-8"
     >
       {imageUrls.map(imageUrl => (
-        <img
-          key={imageUrl}
-          src={`${BASE_MAP_IMAGES_URL}/minerva/${imageUrl}`}
-          alt={imageUrl}
-          className="h-[400px] max-h-[50vh]"
-        />
+        <img key={imageUrl} src={imageUrl} alt={imageUrl} className="h-[400px] max-h-[50vh]" />
       ))}
     </div>
   );
diff --git a/src/components/Map/Legend/LegendTabs/LegendTab/LegendTab.component.test.tsx b/src/components/Map/Legend/LegendTabs/LegendTab/LegendTab.component.test.tsx
new file mode 100644
index 00000000..2667820c
--- /dev/null
+++ b/src/components/Map/Legend/LegendTabs/LegendTab/LegendTab.component.test.tsx
@@ -0,0 +1,45 @@
+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 { act } from 'react-dom/test-utils';
+import { LegendTab } from './LegendTab.component';
+
+const LEGEND_ID = '2';
+
+const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => {
+  const { Wrapper, store } = getReduxWrapperWithStore(initialStore);
+  return (
+    render(
+      <Wrapper>
+        <LegendTab id={LEGEND_ID} name="Legend Tab Name" />
+      </Wrapper>,
+    ),
+    {
+      store,
+    }
+  );
+};
+
+describe('LegendTab - component', () => {
+  it('should display tab with provided name', () => {
+    renderComponent();
+
+    expect(screen.queryByText('Legend Tab Name')).toBeVisible();
+  });
+  it('should set tab as active on tab click', () => {
+    const { store } = renderComponent();
+    const tab = screen.queryByText('Legend Tab Name');
+    expect(tab).toHaveClass('font-normal');
+
+    act(() => {
+      tab?.click();
+    });
+
+    const { activeLegendId } = store.getState().legend;
+    expect(activeLegendId).toBe(LEGEND_ID);
+
+    expect(tab).not.toHaveClass('font-normal');
+    expect(tab).toHaveClass('bg-[#EBF4FF]');
+  });
+});
diff --git a/src/components/Map/Legend/LegendTabs/LegendTab/LegendTab.component.tsx b/src/components/Map/Legend/LegendTabs/LegendTab/LegendTab.component.tsx
new file mode 100644
index 00000000..094e8ab9
--- /dev/null
+++ b/src/components/Map/Legend/LegendTabs/LegendTab/LegendTab.component.tsx
@@ -0,0 +1,28 @@
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { isActiveLegendSelector } from '@/redux/legend/legend.selectors';
+import { setActiveLegendId } from '@/redux/legend/legend.slice';
+import { Button } from '@/shared/Button';
+import React from 'react';
+import { twMerge } from 'tailwind-merge';
+
+type LegendTypeProps = { id: string; name: string };
+
+export const LegendTab = ({ id, name }: LegendTypeProps): React.ReactNode => {
+  const dispatch = useAppDispatch();
+  const isActiveLegend = useAppSelector(state => isActiveLegendSelector(state, id));
+
+  const handleLegendTabClick = (): void => {
+    dispatch(setActiveLegendId(id));
+  };
+
+  return (
+    <Button
+      className={twMerge('h-10 whitespace-nowrap', isActiveLegend ? 'bg-[#EBF4FF]' : 'font-normal')}
+      variantStyles={isActiveLegend ? 'secondary' : 'ghost'}
+      onClick={handleLegendTabClick}
+    >
+      {name}
+    </Button>
+  );
+};
diff --git a/src/components/Map/Legend/LegendTabs/LegendTab/index.ts b/src/components/Map/Legend/LegendTabs/LegendTab/index.ts
new file mode 100644
index 00000000..96003da9
--- /dev/null
+++ b/src/components/Map/Legend/LegendTabs/LegendTab/index.ts
@@ -0,0 +1 @@
+export { LegendTab } from './LegendTab.component';
diff --git a/src/components/Map/Legend/LegendTabs/LegendTabs.component.tsx b/src/components/Map/Legend/LegendTabs/LegendTabs.component.tsx
new file mode 100644
index 00000000..216e6ac1
--- /dev/null
+++ b/src/components/Map/Legend/LegendTabs/LegendTabs.component.tsx
@@ -0,0 +1,16 @@
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { allLegendsNamesAndIdsSelector } from '@/redux/legend/legend.selectors';
+import React from 'react';
+import { LegendTab } from './LegendTab/LegendTab.component';
+
+export const LegendTabs = (): React.ReactNode => {
+  const allLegendsNamesAndIds = useAppSelector(allLegendsNamesAndIdsSelector);
+
+  return (
+    <div className="flex h-10 w-full flex-row flex-nowrap justify-start border-b border-b-divide bg-white-pearl text-xs">
+      {allLegendsNamesAndIds.map(({ id, name }) => (
+        <LegendTab name={name} id={id} key={id} />
+      ))}
+    </div>
+  );
+};
diff --git a/src/components/Map/Legend/LegendTabs/index.ts b/src/components/Map/Legend/LegendTabs/index.ts
new file mode 100644
index 00000000..bea41d1d
--- /dev/null
+++ b/src/components/Map/Legend/LegendTabs/index.ts
@@ -0,0 +1 @@
+export { LegendTabs } from './LegendTabs.component';
diff --git a/src/components/Map/PluginsDrawer/PluginsHeader/PluginHeaderInfo/PluginHeaderInfo.component.test.tsx b/src/components/Map/PluginsDrawer/PluginsHeader/PluginHeaderInfo/PluginHeaderInfo.component.test.tsx
index fa013b07..7729070d 100644
--- a/src/components/Map/PluginsDrawer/PluginsHeader/PluginHeaderInfo/PluginHeaderInfo.component.test.tsx
+++ b/src/components/Map/PluginsDrawer/PluginsHeader/PluginHeaderInfo/PluginHeaderInfo.component.test.tsx
@@ -15,6 +15,7 @@ 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 { LEGEND_INITIAL_STATE_MOCK } from '@/redux/legend/legend.mock';
 import { PluginHeaderInfo } from './PluginHeaderInfo.component';
 import { RELOAD_PLUGIN_DRAWER_BUTTON_ROLE } from './PluginHeaderInfo.constants';
 
@@ -57,6 +58,7 @@ const STATE = {
     },
     list: PLUGINS_INITIAL_STATE_LIST_MOCK,
   },
+  legend: LEGEND_INITIAL_STATE_MOCK,
 };
 
 describe('PluginHeaderInfo - component', () => {
@@ -92,6 +94,10 @@ describe('PluginHeaderInfo - component', () => {
 
     expect(actions).toEqual([
       { payload: { pluginId: '5e3fcb59588cc311ef9839feea6382eb' }, type: 'plugins/removePlugin' },
+      {
+        payload: '5e3fcb59588cc311ef9839feea6382eb',
+        type: 'legend/removePluginLegend',
+      },
     ]);
 
     await waitFor(() => {
diff --git a/src/components/Map/PluginsDrawer/PluginsHeader/PluginOpenButton/LoadPluginElement/LoadPluginElement.component.test.tsx b/src/components/Map/PluginsDrawer/PluginsHeader/PluginOpenButton/LoadPluginElement/LoadPluginElement.component.test.tsx
index 9f6d9e24..f6a4184c 100644
--- a/src/components/Map/PluginsDrawer/PluginsHeader/PluginOpenButton/LoadPluginElement/LoadPluginElement.component.test.tsx
+++ b/src/components/Map/PluginsDrawer/PluginsHeader/PluginOpenButton/LoadPluginElement/LoadPluginElement.component.test.tsx
@@ -15,6 +15,7 @@ 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 { LEGEND_INITIAL_STATE_MOCK } from '@/redux/legend/legend.mock';
 import { LoadPluginElement } from './LoadPluginElement.component';
 
 const mockedAxiosClient = new MockAdapter(axios);
@@ -55,6 +56,7 @@ const STATE = {
     },
     list: PLUGINS_INITIAL_STATE_LIST_MOCK,
   },
+  legend: LEGEND_INITIAL_STATE_MOCK,
 };
 
 describe('LoadPluginButton - component', () => {
diff --git a/src/components/Map/PluginsDrawer/PluginsHeader/PluginOpenButton/PluginOpenButton.component.test.tsx b/src/components/Map/PluginsDrawer/PluginsHeader/PluginOpenButton/PluginOpenButton.component.test.tsx
index 01f6e7fc..43345cb8 100644
--- a/src/components/Map/PluginsDrawer/PluginsHeader/PluginOpenButton/PluginOpenButton.component.test.tsx
+++ b/src/components/Map/PluginsDrawer/PluginsHeader/PluginOpenButton/PluginOpenButton.component.test.tsx
@@ -8,6 +8,7 @@ import {
 } from '@/utils/testing/getReduxStoreActionsListener';
 import { render, screen } from '@testing-library/react';
 import { MockStoreEnhanced } from 'redux-mock-store';
+import { LEGEND_INITIAL_STATE_MOCK } from '@/redux/legend/legend.mock';
 import { PluginOpenButton } from './PluginOpenButton.component';
 
 const renderComponent = (
@@ -60,6 +61,7 @@ describe('PluginOpenButton - component', () => {
             data: PLUGINS_MOCK,
           },
         },
+        legend: LEGEND_INITIAL_STATE_MOCK,
       });
     });
 
diff --git a/src/components/Map/PluginsDrawer/PluginsHeader/PluginsHeader.component.test.tsx b/src/components/Map/PluginsDrawer/PluginsHeader/PluginsHeader.component.test.tsx
index 864f1d96..a4fab9d6 100644
--- a/src/components/Map/PluginsDrawer/PluginsHeader/PluginsHeader.component.test.tsx
+++ b/src/components/Map/PluginsDrawer/PluginsHeader/PluginsHeader.component.test.tsx
@@ -11,6 +11,7 @@ import {
 } from '@/utils/testing/getReduxStoreActionsListener';
 import { render, screen } from '@testing-library/react';
 import { MockStoreEnhanced } from 'redux-mock-store';
+import { LEGEND_INITIAL_STATE_MOCK } from '@/redux/legend/legend.mock';
 import { CLOSE_PLUGINS_DRAWER_BUTTON_ROLE } from '../PluginsDrawer.constants';
 import { PluginsHeader } from './PluginsHeader.component';
 
@@ -41,6 +42,7 @@ describe('PluginsHeader - component', () => {
             currentPluginHash: undefined,
           },
         },
+        legend: LEGEND_INITIAL_STATE_MOCK,
       });
     });
 
@@ -69,6 +71,7 @@ describe('PluginsHeader - component', () => {
           },
           list: PLUGINS_INITIAL_STATE_LIST_MOCK,
         },
+        legend: LEGEND_INITIAL_STATE_MOCK,
       });
     });
 
diff --git a/src/components/Map/PluginsDrawer/PluginsTabs/PluginSingleTab/PluginSingleTab.component.test.tsx b/src/components/Map/PluginsDrawer/PluginsTabs/PluginSingleTab/PluginSingleTab.component.test.tsx
index 157d4220..f9ff782a 100644
--- a/src/components/Map/PluginsDrawer/PluginsTabs/PluginSingleTab/PluginSingleTab.component.test.tsx
+++ b/src/components/Map/PluginsDrawer/PluginsTabs/PluginSingleTab/PluginSingleTab.component.test.tsx
@@ -1,4 +1,9 @@
-import { FIRST_ARRAY_ELEMENT, SECOND_ARRAY_ELEMENT, THIRD_ARRAY_ELEMENT } from '@/constants/common';
+import {
+  FIRST_ARRAY_ELEMENT,
+  SECOND_ARRAY_ELEMENT,
+  THIRD_ARRAY_ELEMENT,
+  ZERO,
+} from '@/constants/common';
 import { PLUGINS_MOCK } from '@/models/mocks/pluginsMock';
 import {
   PLUGINS_INITIAL_STATE_LIST_MOCK,
@@ -13,6 +18,7 @@ import {
 } from '@/utils/testing/getReduxStoreActionsListener';
 import { render, screen } from '@testing-library/react';
 import { MockStoreEnhanced } from 'redux-mock-store';
+import { LEGEND_INITIAL_STATE_MOCK } from '@/redux/legend/legend.mock';
 import { PluginSingleTab } from './PluginSingleTab.component';
 
 const renderComponent = (
@@ -53,6 +59,7 @@ const STATE = {
     },
     list: PLUGINS_INITIAL_STATE_LIST_MOCK,
   },
+  legend: LEGEND_INITIAL_STATE_MOCK,
 };
 
 describe('PluginSingleTab - component', () => {
@@ -82,9 +89,10 @@ describe('PluginSingleTab - component', () => {
         hash: PLUGIN.hash,
       });
 
-      expect(actions).toEqual([
-        { payload: { pluginId: '5e3fcb59588cc311ef9839feea6382eb' }, type: 'plugins/removePlugin' },
-      ]);
+      expect(actions[ZERO]).toEqual({
+        payload: { pluginId: '5e3fcb59588cc311ef9839feea6382eb' },
+        type: 'plugins/removePlugin',
+      });
     });
 
     it('should dispatch close action on close btn click and new current drawer on last active plugin', () => {
@@ -116,6 +124,7 @@ describe('PluginSingleTab - component', () => {
 
       expect(actions).toEqual([
         { payload: { pluginId: '5e3fcb59588cc311ef9839feea6382eb' }, type: 'plugins/removePlugin' },
+        { payload: '5e3fcb59588cc311ef9839feea6382eb', type: 'legend/removePluginLegend' },
         {
           payload: '5314b9f996e56e67f0dad65e7df8b73b',
           type: 'plugins/setCurrentDrawerPluginHash',
diff --git a/src/components/Map/PluginsDrawer/PluginsTabs/PluginsTabs.component.test.tsx b/src/components/Map/PluginsDrawer/PluginsTabs/PluginsTabs.component.test.tsx
index 64bc4670..ba00c577 100644
--- a/src/components/Map/PluginsDrawer/PluginsTabs/PluginsTabs.component.test.tsx
+++ b/src/components/Map/PluginsDrawer/PluginsTabs/PluginsTabs.component.test.tsx
@@ -11,6 +11,7 @@ import {
 } from '@/utils/testing/getReduxStoreActionsListener';
 import { render, screen } from '@testing-library/react';
 import { MockStoreEnhanced } from 'redux-mock-store';
+import { LEGEND_INITIAL_STATE_MOCK } from '@/redux/legend/legend.mock';
 import { PluginsTabs } from './PluginsTabs.component';
 
 const renderComponent = (
@@ -75,6 +76,7 @@ describe('PluginsTabs - component', () => {
           },
           list: PLUGINS_INITIAL_STATE_LIST_MOCK,
         },
+        legend: LEGEND_INITIAL_STATE_MOCK,
       });
     });
 
diff --git a/src/redux/legend/legend.constants.ts b/src/redux/legend/legend.constants.ts
index 5719c74e..ffb97578 100644
--- a/src/redux/legend/legend.constants.ts
+++ b/src/redux/legend/legend.constants.ts
@@ -1,7 +1,14 @@
 import { LegendState } from './legend.types';
 
+export const DEFAULT_LEGEND_ID = 'MAIN';
+
 export const LEGEND_INITIAL_STATE: LegendState = {
   isOpen: false,
   pluginLegend: {},
-  selectedPluginId: undefined,
+  activeLegendId: DEFAULT_LEGEND_ID,
+};
+
+export const DEFAULT_LEGEND_TAB = {
+  name: 'Main Legend',
+  id: DEFAULT_LEGEND_ID,
 };
diff --git a/src/redux/legend/legend.mock.ts b/src/redux/legend/legend.mock.ts
index 6874d3a6..fe0791d6 100644
--- a/src/redux/legend/legend.mock.ts
+++ b/src/redux/legend/legend.mock.ts
@@ -1,7 +1,8 @@
+import { DEFAULT_LEGEND_ID } from './legend.constants';
 import { LegendState } from './legend.types';
 
 export const LEGEND_INITIAL_STATE_MOCK: LegendState = {
   isOpen: false,
   pluginLegend: {},
-  selectedPluginId: undefined,
+  activeLegendId: DEFAULT_LEGEND_ID,
 };
diff --git a/src/redux/legend/legend.reducers.ts b/src/redux/legend/legend.reducers.ts
index be1f0572..8fe3cba2 100644
--- a/src/redux/legend/legend.reducers.ts
+++ b/src/redux/legend/legend.reducers.ts
@@ -1,5 +1,6 @@
 import { PayloadAction } from '@reduxjs/toolkit';
 import { LegendState, PluginId } from './legend.types';
+import { DEFAULT_LEGEND_ID } from './legend.constants';
 
 export const openLegendReducer = (state: LegendState): void => {
   state.isOpen = true;
@@ -9,13 +10,37 @@ export const closeLegendReducer = (state: LegendState): void => {
   state.isOpen = false;
 };
 
-export const selectLegendPluginIdReducer = (
+export const setActiveLegendIdReducer = (
   state: LegendState,
-  action: PayloadAction<PluginId>,
+  action: PayloadAction<string>,
 ): void => {
-  state.selectedPluginId = action.payload;
+  state.activeLegendId = action.payload;
 };
 
-export const selectDefaultLegendReducer = (state: LegendState): void => {
-  state.selectedPluginId = undefined;
+export const setDefaultLegendIdReducer = (state: LegendState): void => {
+  state.activeLegendId = DEFAULT_LEGEND_ID;
+};
+
+export const setPluginLegendReducer = (
+  state: LegendState,
+  {
+    payload,
+  }: PayloadAction<{
+    pluginId: PluginId;
+    legend: string[];
+  }>,
+): void => {
+  state.pluginLegend = {
+    ...state.pluginLegend,
+    [payload.pluginId]: payload.legend,
+  };
+};
+
+export const removePluginLegendReducer = (
+  state: LegendState,
+  { payload }: PayloadAction<string>,
+): void => {
+  if (state.pluginLegend[payload]) {
+    delete state.pluginLegend[payload];
+  }
 };
diff --git a/src/redux/legend/legend.selectors.ts b/src/redux/legend/legend.selectors.ts
index fdf927f1..91d7277b 100644
--- a/src/redux/legend/legend.selectors.ts
+++ b/src/redux/legend/legend.selectors.ts
@@ -1,20 +1,51 @@
 import { createSelector } from '@reduxjs/toolkit';
+import { BASE_MAP_IMAGES_URL } from '@/constants';
 import { defaultLegendImagesSelector } from '../configuration/configuration.selectors';
 import { rootSelector } from '../root/root.selectors';
+import { activePluginsDataSelector } from '../plugins/plugins.selectors';
+import { DEFAULT_LEGEND_ID, DEFAULT_LEGEND_TAB } from './legend.constants';
 
 export const legendSelector = createSelector(rootSelector, state => state.legend);
 
 export const isLegendOpenSelector = createSelector(legendSelector, state => state.isOpen);
 
-// TODO: add filter for active plugins
+export const pluginLegendsSelector = createSelector(legendSelector, state => state.pluginLegend);
+
 export const currentLegendImagesSelector = createSelector(
   legendSelector,
   defaultLegendImagesSelector,
-  ({ selectedPluginId, pluginLegend }, defaultImages) => {
-    if (selectedPluginId) {
-      return pluginLegend?.[selectedPluginId] || [];
+  ({ activeLegendId, pluginLegend }, defaultImages) => {
+    if (activeLegendId === DEFAULT_LEGEND_ID)
+      return defaultImages.map(image => `${BASE_MAP_IMAGES_URL}/minerva/${image}`);
+
+    if (activeLegendId) {
+      return pluginLegend?.[activeLegendId] || [];
     }
 
-    return defaultImages;
+    return [];
+  },
+);
+
+export const allLegendsNamesAndIdsSelector = createSelector(
+  activePluginsDataSelector,
+  pluginLegendsSelector,
+  (activePlugins, pluginLegends) => {
+    const allPluginLegendsNamesAndIds = Object.keys(pluginLegends).map(hash => {
+      const plugin = Object.values(activePlugins).find(activePlugin => activePlugin.hash === hash);
+
+      return {
+        name: plugin?.name || '',
+        id: plugin?.hash || '',
+      };
+    });
+
+    return [DEFAULT_LEGEND_TAB, ...allPluginLegendsNamesAndIds];
   },
 );
+
+export const activeLegendIdSelector = createSelector(legendSelector, state => state.activeLegendId);
+
+export const isActiveLegendSelector = createSelector(
+  [activeLegendIdSelector, (_, legendId: string): string => legendId],
+  (activeLegendId, legendId) => activeLegendId === legendId,
+);
diff --git a/src/redux/legend/legend.slice.ts b/src/redux/legend/legend.slice.ts
index fbb62f00..2dccdb5a 100644
--- a/src/redux/legend/legend.slice.ts
+++ b/src/redux/legend/legend.slice.ts
@@ -1,6 +1,13 @@
 import { createSlice } from '@reduxjs/toolkit';
 import { LEGEND_INITIAL_STATE } from './legend.constants';
-import { closeLegendReducer, openLegendReducer } from './legend.reducers';
+import {
+  closeLegendReducer,
+  openLegendReducer,
+  removePluginLegendReducer,
+  setActiveLegendIdReducer,
+  setDefaultLegendIdReducer,
+  setPluginLegendReducer,
+} from './legend.reducers';
 
 const legendSlice = createSlice({
   name: 'legend',
@@ -8,9 +15,20 @@ const legendSlice = createSlice({
   reducers: {
     openLegend: openLegendReducer,
     closeLegend: closeLegendReducer,
+    setPluginLegend: setPluginLegendReducer,
+    setActiveLegendId: setActiveLegendIdReducer,
+    setDefaultLegendId: setDefaultLegendIdReducer,
+    removePluginLegend: removePluginLegendReducer,
   },
 });
 
-export const { openLegend, closeLegend } = legendSlice.actions;
+export const {
+  openLegend,
+  closeLegend,
+  setPluginLegend,
+  setActiveLegendId,
+  setDefaultLegendId,
+  removePluginLegend,
+} = legendSlice.actions;
 
 export default legendSlice.reducer;
diff --git a/src/redux/legend/legend.types.ts b/src/redux/legend/legend.types.ts
index 80314aec..ad797feb 100644
--- a/src/redux/legend/legend.types.ts
+++ b/src/redux/legend/legend.types.ts
@@ -1,8 +1,8 @@
-export type PluginId = number;
+export type PluginId = string;
 export type ImageUrl = string;
 
 export type LegendState = {
   isOpen: boolean;
   pluginLegend: Record<PluginId, ImageUrl[]>;
-  selectedPluginId: PluginId | undefined;
+  activeLegendId: string;
 };
diff --git a/src/services/pluginsManager/legend/removeLegend.ts b/src/services/pluginsManager/legend/removeLegend.ts
new file mode 100644
index 00000000..70fb75ed
--- /dev/null
+++ b/src/services/pluginsManager/legend/removeLegend.ts
@@ -0,0 +1,14 @@
+import { isActiveLegendSelector } from '@/redux/legend/legend.selectors';
+import { removePluginLegend, setDefaultLegendId } from '@/redux/legend/legend.slice';
+import { store } from '@/redux/store';
+
+export const removeLegend = (pluginId: string): void => {
+  const isActivePluginLegend = isActiveLegendSelector(store.getState(), pluginId);
+  const { dispatch } = store;
+
+  if (isActivePluginLegend) {
+    dispatch(setDefaultLegendId());
+  }
+
+  dispatch(removePluginLegend(pluginId));
+};
diff --git a/src/services/pluginsManager/legend/setLegend.ts b/src/services/pluginsManager/legend/setLegend.ts
new file mode 100644
index 00000000..4d50c9f6
--- /dev/null
+++ b/src/services/pluginsManager/legend/setLegend.ts
@@ -0,0 +1,13 @@
+import { setPluginLegend } from '@/redux/legend/legend.slice';
+import { store } from '@/redux/store';
+
+export const setLegend = (pluginId: string, legend: string[]): void => {
+  const { dispatch } = store;
+
+  dispatch(
+    setPluginLegend({
+      pluginId,
+      legend,
+    }),
+  );
+};
diff --git a/src/services/pluginsManager/pluginsManager.ts b/src/services/pluginsManager/pluginsManager.ts
index 0b083ef9..e8e4b924 100644
--- a/src/services/pluginsManager/pluginsManager.ts
+++ b/src/services/pluginsManager/pluginsManager.ts
@@ -33,6 +33,8 @@ import { getVersion } from './project/data/getVersion';
 import { getBounds } from './map/data/getBounds';
 import { fitBounds } from './map/fitBounds';
 import { getOpenMapId } from './map/getOpenMapId';
+import { setLegend } from './legend/setLegend';
+import { removeLegend } from './legend/removeLegend';
 
 export const PluginsManager: PluginsManagerType = {
   hashedPlugins: {},
@@ -132,6 +134,10 @@ export const PluginsManager: PluginsManagerType = {
         removeListener: PluginsEventBus.removeListener.bind(this, hash),
         removeAllListeners: PluginsEventBus.removeAllListeners.bind(this, hash),
       },
+      legend: {
+        setLegend: setLegend.bind(this, hash),
+        removeLegend: removeLegend.bind(this, hash),
+      },
     };
   },
   createAndGetPluginContent({ hash }) {
-- 
GitLab