From 3f0c1856776782d38422388a4e92b582351fbfa3 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Mon, 11 Nov 2024 14:53:46 +0000 Subject: [PATCH 1/2] Generate custom admin tabs from plugin routes #1418 - Implemented dynamic generation of admin tabs using plugin routes - Updated admin page to render tabs based on configured plugin routes - Refactored tab generation logic to support custom admin routes for each plugin --- src/App.test.tsx | 6 +- src/adminPage/adminPage.component.test.tsx | 56 ++++++--- src/adminPage/adminPage.component.tsx | 119 ++++++++++-------- src/mainAppBar/mainAppBar.component.test.tsx | 37 ++++-- src/mainAppBar/mainAppBar.component.tsx | 41 +++--- .../mobileOverflowMenu.component.test.tsx | 29 +++-- .../mobileOverflowMenu.component.tsx | 22 ++-- src/mainAppBar/pageLinks.component.tsx | 21 +++- src/routing/routing.component.tsx | 61 ++++++--- src/state/actions/scigateway.actions.tsx | 44 +++---- src/state/scigateway.types.tsx | 5 +- src/state/state.types.tsx | 12 +- 12 files changed, 271 insertions(+), 182 deletions(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index b09d4924..1dbdc25e 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,11 +1,11 @@ +import { useMediaQuery } from '@mui/material'; +import { act, fireEvent, render, screen } from '@testing-library/react'; +import axios from 'axios'; import React from 'react'; import { createRoot } from 'react-dom/client'; import App, { AppSansHoc } from './App'; -import { act, fireEvent, render, screen } from '@testing-library/react'; import { flushPromises } from './setupTests'; -import axios from 'axios'; import { RegisterRouteType } from './state/scigateway.types'; -import { useMediaQuery } from '@mui/material'; jest.mock('./state/actions/loadMicroFrontends', () => ({ init: jest.fn(() => Promise.resolve()), diff --git a/src/adminPage/adminPage.component.test.tsx b/src/adminPage/adminPage.component.test.tsx index e2aaba7d..a929b6a7 100644 --- a/src/adminPage/adminPage.component.test.tsx +++ b/src/adminPage/adminPage.component.test.tsx @@ -1,19 +1,18 @@ -import React from 'react'; +import { StyledEngineProvider, ThemeProvider } from '@mui/material'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { createLocation, createMemoryHistory, History } from 'history'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { Router } from 'react-router'; +import configureStore from 'redux-mock-store'; +import { thunk } from 'redux-thunk'; +import TestAuthProvider from '../authentication/testAuthProvider'; import { authState, initialState } from '../state/reducers/scigateway.reducer'; -import { StateType } from '../state/state.types'; import { PluginConfig } from '../state/scigateway.types'; -import configureStore from 'redux-mock-store'; -import AdminPage from './adminPage.component'; -import { Provider } from 'react-redux'; +import { StateType } from '../state/state.types'; import { buildTheme } from '../theming'; -import TestAuthProvider from '../authentication/testAuthProvider'; -import { thunk } from 'redux-thunk'; -import { Router } from 'react-router'; -import { StyledEngineProvider, ThemeProvider } from '@mui/material'; -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { getPluginRoutes } from './adminPage.component'; +import AdminPage, { getAdminPluginRoutes } from './adminPage.component'; describe('Admin page component', () => { let mockStore; @@ -50,9 +49,24 @@ describe('Admin page component', () => { ); } + afterEach(() => { + jest.clearAllMocks(); + }); + it('should render maintenance page correctly', () => { - history.replace('/admin/maintenance'); state.scigateway.adminPageDefaultTab = 'download'; + state.scigateway.plugins = [ + ...state.scigateway.plugins, + { + order: 1, + plugin: 'datagateway-download', + link: '/admin/download', + section: 'Admin', + displayName: 'Admin Download', + admin: true, + }, + ]; + history.replace('/admin/maintenance'); render(, { wrapper: Wrapper }); @@ -133,7 +147,7 @@ describe('Admin page component', () => { it('should return an empty object when given an empty plugins array', () => { const plugins = []; - const result = getPluginRoutes(plugins); + const result = getAdminPluginRoutes({ plugins }); expect(result).toEqual({}); }); @@ -164,9 +178,9 @@ describe('Admin page component', () => { order: 3, }, ]; - const result = getPluginRoutes(plugins, true); // Admin user + const result = getAdminPluginRoutes({ plugins }); // Admin user expect(result).toEqual({ - PluginA: ['/admin/pluginA', '/admin/pluginA2'], + PluginA: { pluginA: '/admin/pluginA', pluginA2: '/admin/pluginA2' }, }); }); @@ -175,7 +189,7 @@ describe('Admin page component', () => { { plugin: 'PluginA', admin: true, - link: '/admin/pluginA', + link: '/admin/pluginALink', section: 'A', displayName: 'A', order: 1, @@ -183,15 +197,17 @@ describe('Admin page component', () => { { plugin: 'PluginB', admin: false, - link: '/public/pluginB', + link: '/public/pluginBLink', section: 'B', displayName: 'B', order: 2, }, ]; - const result = getPluginRoutes(plugins, false); // Non-admin user + const result = getAdminPluginRoutes({ plugins }); // Non-admin user expect(result).toEqual({ - PluginB: ['/public/pluginB'], + PluginA: { + pluginALink: '/admin/pluginALink', + }, }); }); }); diff --git a/src/adminPage/adminPage.component.tsx b/src/adminPage/adminPage.component.tsx index d5adbf23..5739de2e 100644 --- a/src/adminPage/adminPage.component.tsx +++ b/src/adminPage/adminPage.component.tsx @@ -1,55 +1,67 @@ -import React, { ReactElement } from 'react'; -import Typography from '@mui/material/Typography'; import { Paper } from '@mui/material'; +import Typography from '@mui/material/Typography'; +import React, { ReactElement } from 'react'; import { connect } from 'react-redux'; +import { PluginConfig } from '../state/scigateway.types'; import { StateType } from '../state/state.types'; -import { adminRoutes, PluginConfig } from '../state/scigateway.types'; -import Tabs from '@mui/material/Tabs'; import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import { useTranslation } from 'react-i18next'; import { Link, Route, Switch, useLocation } from 'react-router-dom'; import PageNotFound from '../pageNotFound/pageNotFound.component'; -import { PluginPlaceHolder } from '../routing/routing.component'; +import { + getAdminRoutes, + PluginPlaceHolder, +} from '../routing/routing.component'; import MaintenancePage from './maintenancePage.component'; -import { useTranslation } from 'react-i18next'; export interface AdminPageProps { plugins: PluginConfig[]; - adminPageDefaultTab?: 'maintenance' | 'download'; + adminPageDefaultTab?: string; } -export const getPluginRoutes = ( - plugins: PluginConfig[], - admin?: boolean -): Record => { - const pluginRoutes: Record = {}; +export const getAdminPluginRoutes = (props: { + plugins: PluginConfig[]; +}): Record> => { + const { plugins } = props; + const pluginRoutes: Record> = {}; plugins.forEach((p) => { - const isAdmin = admin ? p.admin : !p.admin; const basePluginLink = p.link.split('?')[0]; - if (isAdmin) { - if (pluginRoutes[p.plugin]) { - pluginRoutes[p.plugin].push(basePluginLink); - } else { - pluginRoutes[p.plugin] = [basePluginLink]; + + if (p.admin) { + // Extract `plugin` and `tabName` values from the link + const tabName = basePluginLink.split('/')[2]; // Ignore `/admin` part, get the tabName as the third part + + // Initialize nested structure for each plugin and tabName + if (!pluginRoutes[p.plugin]) { + pluginRoutes[p.plugin] = {}; + } + + // Only store the first route (or the most relevant one) + if (!pluginRoutes[p.plugin][tabName]) { + pluginRoutes[p.plugin][tabName] = basePluginLink; } } }); + return pluginRoutes; }; const AdminPage = (props: AdminPageProps): ReactElement => { - const pluginRoutes = getPluginRoutes(props.plugins, true); + const pluginRoutes = getAdminPluginRoutes({ plugins: props.plugins }); + const adminRoutes = getAdminRoutes({ plugins: props.plugins }); const location = useLocation(); - - const [tabValue, setTabValue] = React.useState<'maintenance' | 'download'>( - // allows direct access to a tab when another tab is the default - (Object.keys(adminRoutes) as (keyof typeof adminRoutes)[]).find( - (key) => adminRoutes[key] === location.pathname + const [tabValue, setTabValue] = React.useState( + (Object.keys(adminRoutes) as (keyof typeof adminRoutes)[]).find((key) => + location.pathname.startsWith(adminRoutes[key]) ) ?? - props.adminPageDefaultTab ?? - 'maintenance' + (props.adminPageDefaultTab && + adminRoutes.hasOwnProperty(props.adminPageDefaultTab) + ? props.adminPageDefaultTab + : 'maintenance') ); const [t] = useTranslation(); @@ -76,22 +88,26 @@ const AdminPage = (props: AdminPageProps): ReactElement => { setTabValue(newValue); }} > - - + {Object.entries(adminRoutes).map(([key, value]) => { + const pluginDetails = props.plugins.find( + (plugin) => plugin.link === value + ); + + return ( + + ); + })} @@ -105,20 +121,21 @@ const AdminPage = (props: AdminPageProps): ReactElement => { - {Object.entries(pluginRoutes).map(([key, value]) => { - return ( - + {Object.entries(pluginRoutes).map(([pluginName, tabRoutes]) => + Object.entries(tabRoutes).map(([tabName, route]) => ( + - ); - })} + )) + )} + diff --git a/src/mainAppBar/mainAppBar.component.test.tsx b/src/mainAppBar/mainAppBar.component.test.tsx index a1aefd2e..44fd1bcf 100644 --- a/src/mainAppBar/mainAppBar.component.test.tsx +++ b/src/mainAppBar/mainAppBar.component.test.tsx @@ -1,25 +1,25 @@ -import React from 'react'; -import MainAppBarComponent from './mainAppBar.component'; +import { useMediaQuery } from '@mui/material'; +import { StyledEngineProvider, ThemeProvider } from '@mui/material/styles'; +import { render, screen, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { push } from 'connected-react-router'; import { createLocation, createMemoryHistory, History } from 'history'; -import { StateType } from '../state/state.types'; -import { PluginConfig } from '../state/scigateway.types'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { Router } from 'react-router-dom'; import configureStore, { MockStore } from 'redux-mock-store'; -import { push } from 'connected-react-router'; -import { initialState } from '../state/reducers/scigateway.reducer'; +import TestAuthProvider from '../authentication/testAuthProvider'; import { loadDarkModePreference, loadHighContrastModePreference, toggleDrawer, toggleHelp, } from '../state/actions/scigateway.actions'; -import { Provider } from 'react-redux'; -import TestAuthProvider from '../authentication/testAuthProvider'; +import { initialState } from '../state/reducers/scigateway.reducer'; +import { PluginConfig } from '../state/scigateway.types'; +import { StateType } from '../state/state.types'; import { buildTheme } from '../theming'; -import { StyledEngineProvider, ThemeProvider } from '@mui/material/styles'; -import { Router } from 'react-router-dom'; -import { render, screen, waitFor, within } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { useMediaQuery } from '@mui/material'; +import MainAppBarComponent from './mainAppBar.component'; jest.mock('@mui/material', () => ({ __esmodule: true, @@ -219,6 +219,17 @@ describe('Main app bar component', () => { it('redirects to Admin page when Admin button clicked (download is default)', async () => { state.scigateway.adminPageDefaultTab = 'download'; + state.scigateway.plugins = [ + ...state.scigateway.plugins, + { + section: 'Admin', + link: '/admin/download', + displayName: 'Admin Download', + admin: true, + order: 1, + plugin: 'plugin', + }, + ]; const user = userEvent.setup(); render(, { wrapper: Wrapper }); diff --git a/src/mainAppBar/mainAppBar.component.tsx b/src/mainAppBar/mainAppBar.component.tsx index eb727df6..56ace706 100644 --- a/src/mainAppBar/mainAppBar.component.tsx +++ b/src/mainAppBar/mainAppBar.component.tsx @@ -1,37 +1,36 @@ -import React, { useState } from 'react'; -import { Dispatch, Action } from 'redux'; -import { connect } from 'react-redux'; -import AppBar from '@mui/material/AppBar'; -import Toolbar from '@mui/material/Toolbar'; -import Button from '@mui/material/Button'; -import IconButton from '@mui/material/IconButton'; import HelpIcon from '@mui/icons-material/HelpOutline'; import MenuIcon from '@mui/icons-material/Menu'; -import SettingsIcon from '@mui/icons-material/Settings'; +import MenuOpenIcon from '@mui/icons-material/MenuOpen'; import MoreVertIcon from '@mui/icons-material/MoreVert'; +import SettingsIcon from '@mui/icons-material/Settings'; import { Box, styled, useMediaQuery } from '@mui/material'; -import MenuOpenIcon from '@mui/icons-material/MenuOpen'; +import AppBar from '@mui/material/AppBar'; +import Button from '@mui/material/Button'; +import IconButton from '@mui/material/IconButton'; import { Theme, useTheme } from '@mui/material/styles'; +import Toolbar from '@mui/material/Toolbar'; +import { push } from 'connected-react-router'; +import React, { useState } from 'react'; +import { connect } from 'react-redux'; +import { useLocation } from 'react-router-dom'; +import { Action, Dispatch } from 'redux'; +import NullAuthProvider from '../authentication/nullAuthProvider'; import ScigatewayLogo from '../images/scigateway-white-text-blue-mark-logo.svg'; +import NotificationBadgeComponent from '../notifications/notificationBadge.component'; import { - toggleDrawer, - toggleHelp, loadDarkModePreference, loadHighContrastModePreference, + toggleDrawer, + toggleHelp, } from '../state/actions/scigateway.actions'; -import { AppStrings } from '../state/scigateway.types'; +import { AppStrings, PluginConfig } from '../state/scigateway.types'; import { StateType } from '../state/state.types'; -import { push } from 'connected-react-router'; import { getAppStrings, getString } from '../state/strings'; -import UserProfileComponent from './userProfile.component'; -import NotificationBadgeComponent from '../notifications/notificationBadge.component'; -import { PluginConfig } from '../state/scigateway.types'; -import { useLocation } from 'react-router-dom'; -import SettingsMenu from './settingsMenu.component'; import MobileOverflowMenu from './mobileOverflowMenu.component'; -import { appBarIconButtonStyle, appBarMenuItemIconStyle } from './styles'; import PageLinks from './pageLinks.component'; -import NullAuthProvider from '../authentication/nullAuthProvider'; +import SettingsMenu from './settingsMenu.component'; +import { appBarIconButtonStyle, appBarMenuItemIconStyle } from './styles'; +import UserProfileComponent from './userProfile.component'; interface MainAppProps { drawerOpen: boolean; @@ -47,7 +46,7 @@ interface MainAppProps { loading: boolean; logo?: string; homepageUrl?: string; - adminPageDefaultTab?: 'maintenance' | 'download'; + adminPageDefaultTab?: string; pathname: string; } diff --git a/src/mainAppBar/mobileOverflowMenu.component.test.tsx b/src/mainAppBar/mobileOverflowMenu.component.test.tsx index be298589..b62d9da1 100644 --- a/src/mainAppBar/mobileOverflowMenu.component.test.tsx +++ b/src/mainAppBar/mobileOverflowMenu.component.test.tsx @@ -1,20 +1,20 @@ -import configureStore, { MockStore } from 'redux-mock-store'; -import { StateType } from '../state/state.types'; -import { initialState } from '../state/reducers/scigateway.reducer'; +import { StyledEngineProvider, ThemeProvider } from '@mui/material/styles'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { push } from 'connected-react-router'; import { createLocation, createMemoryHistory, History } from 'history'; import * as React from 'react'; -import { StyledEngineProvider, ThemeProvider } from '@mui/material/styles'; import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; -import { buildTheme } from '../theming'; -import { render, screen } from '@testing-library/react'; -import MobileOverflowMenu from './mobileOverflowMenu.component'; +import configureStore, { MockStore } from 'redux-mock-store'; import TestAuthProvider, { NonAdminTestAuthProvider, } from '../authentication/testAuthProvider'; -import userEvent from '@testing-library/user-event'; -import { push } from 'connected-react-router'; import { toggleHelp } from '../state/actions/scigateway.actions'; +import { initialState } from '../state/reducers/scigateway.reducer'; +import { StateType } from '../state/state.types'; +import { buildTheme } from '../theming'; +import MobileOverflowMenu from './mobileOverflowMenu.component'; describe('Mobile overflow menu', () => { let testStore: MockStore; @@ -99,6 +99,17 @@ describe('Mobile overflow menu', () => { it('redirects to Admin page when Admin button clicked (download is default)', async () => { state.scigateway.adminPageDefaultTab = 'download'; + state.scigateway.plugins = [ + ...state.scigateway.plugins, + { + section: 'Admin', + link: '/admin/download', + displayName: 'Admin Download', + admin: true, + order: 1, + plugin: 'plugin', + }, + ]; const user = userEvent.setup(); render(, { diff --git a/src/mainAppBar/mobileOverflowMenu.component.tsx b/src/mainAppBar/mobileOverflowMenu.component.tsx index e6754cbc..5ef77b26 100644 --- a/src/mainAppBar/mobileOverflowMenu.component.tsx +++ b/src/mainAppBar/mobileOverflowMenu.component.tsx @@ -1,18 +1,18 @@ import React from 'react'; import { + Divider, + ListItemText, Menu, MenuItem, - ListItemText, MenuProps, - Divider, } from '@mui/material'; -import { SettingsMenuContent } from './settingsMenu.component'; +import { push } from 'connected-react-router'; import { useDispatch, useSelector } from 'react-redux'; +import { getAdminRoutes } from '../routing/routing.component'; +import { toggleHelp } from '../state/actions/scigateway.actions'; import { StateType } from '../state/state.types'; import { getAppStrings, getString } from '../state/strings'; -import { push } from 'connected-react-router'; -import { adminRoutes } from '../state/scigateway.types'; -import { toggleHelp } from '../state/actions/scigateway.actions'; +import { SettingsMenuContent } from './settingsMenu.component'; interface MobileOverflowMenuProps extends MenuProps { onClose: () => void; @@ -39,6 +39,9 @@ function MobileOverflowMenu({ (state: StateType) => state.scigateway.adminPageDefaultTab ); + const plugins = useSelector((state: StateType) => state.scigateway.plugins); + const adminRoutes = getAdminRoutes({ plugins }); + const dispatch = useDispatch(); function navigateToHelpPage(): void { @@ -46,7 +49,12 @@ function MobileOverflowMenu({ } function navigateToAdminPage(): void { - dispatch(push(adminRoutes[adminPageDefaultTab ?? 'maintenance'])); + const targetRoute = + adminPageDefaultTab && adminRoutes.hasOwnProperty(adminPageDefaultTab) + ? adminRoutes[adminPageDefaultTab] + : adminRoutes['maintenance']; + + dispatch(push(targetRoute)); } function toggleTutorial(): void { diff --git a/src/mainAppBar/pageLinks.component.tsx b/src/mainAppBar/pageLinks.component.tsx index b126ae96..7de64c33 100644 --- a/src/mainAppBar/pageLinks.component.tsx +++ b/src/mainAppBar/pageLinks.component.tsx @@ -1,12 +1,12 @@ +import React from 'react'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import { push } from 'connected-react-router'; import { useDispatch, useSelector } from 'react-redux'; +import { getAdminRoutes } from '../routing/routing.component'; import { StateType } from '../state/state.types'; -import Button from '@mui/material/Button'; import { getAppStrings, getString } from '../state/strings'; -import Typography from '@mui/material/Typography'; -import React from 'react'; import { appBarIconButtonStyle } from './styles'; -import { push } from 'connected-react-router'; -import { adminRoutes } from '../state/scigateway.types'; function PageLinks(): JSX.Element { const shouldShowHelpPageButton = useSelector( @@ -20,6 +20,10 @@ function PageLinks(): JSX.Element { const adminPageDefaultTab = useSelector( (state: StateType) => state.scigateway.adminPageDefaultTab ); + + const plugins = useSelector((state: StateType) => state.scigateway.plugins); + const adminRoutes = getAdminRoutes({ plugins }); + const res = useSelector((state: StateType) => getAppStrings(state, 'main-appbar') ); @@ -31,7 +35,12 @@ function PageLinks(): JSX.Element { } function navigateToAdminPage(): void { - dispatch(push(adminRoutes[adminPageDefaultTab ?? 'maintenance'])); + const targetRoute = + adminPageDefaultTab && adminRoutes.hasOwnProperty(adminPageDefaultTab) + ? adminRoutes[adminPageDefaultTab] + : adminRoutes['maintenance']; + + dispatch(push(targetRoute)); } return ( diff --git a/src/routing/routing.component.tsx b/src/routing/routing.component.tsx index 4b83f45d..c955b61e 100644 --- a/src/routing/routing.component.tsx +++ b/src/routing/routing.component.tsx @@ -1,29 +1,29 @@ -import React from 'react'; +import { useMediaQuery } from '@mui/material'; import { styled, useTheme } from '@mui/material/styles'; -import { Redirect, Route, Switch } from 'react-router-dom'; -import { StateType } from '../state/state.types'; -import { - adminRoutes, - MaintenanceState, - PluginConfig, - scigatewayRoutes, -} from '../state/scigateway.types'; +import { RouterLocation } from 'connected-react-router'; +import React from 'react'; import { connect } from 'react-redux'; -import HomePage from '../homePage/homePage.component'; +import { Redirect, Route, Switch } from 'react-router-dom'; +import * as singleSpa from 'single-spa'; +import AccessibilityPage from '../accessibilityPage/accessibilityPage.component'; +import AdminPage from '../adminPage/adminPage.component'; +import NullAuthProvider from '../authentication/nullAuthProvider'; +import CookiesPage from '../cookieConsent/cookiesPage.component'; import HelpPage from '../helpPage/helpPage.component'; +import HomePage from '../homePage/homePage.component'; import LoginPage from '../loginPage/loginPage.component'; import LogoutPage from '../logoutPage/logoutPage.component'; -import CookiesPage from '../cookieConsent/cookiesPage.component'; import MaintenancePage from '../maintenancePage/maintenancePage.component'; -import AdminPage from '../adminPage/adminPage.component'; import PageNotFound from '../pageNotFound/pageNotFound.component'; -import AccessibilityPage from '../accessibilityPage/accessibilityPage.component'; -import withAuth, { usePrevious } from './authorisedRoute.component'; import { Preloader } from '../preloader/preloader.component'; -import * as singleSpa from 'single-spa'; -import { useMediaQuery } from '@mui/material'; -import NullAuthProvider from '../authentication/nullAuthProvider'; -import { RouterLocation } from 'connected-react-router'; +import { + baseAdminRoutes, + MaintenanceState, + PluginConfig, + scigatewayRoutes, +} from '../state/scigateway.types'; +import { StateType } from '../state/state.types'; +import withAuth, { usePrevious } from './authorisedRoute.component'; interface ContainerDivProps { drawerOpen: boolean; @@ -68,6 +68,29 @@ const ContainerDiv = styled('div', { }; }); +export const getAdminRoutes = (props: { + plugins: PluginConfig[]; +}): Record => { + const { plugins } = props; + const newAdminRoutes: Record = JSON.parse( + JSON.stringify(baseAdminRoutes) + ); + + // Note: Any nested paths under `/admin/path` are managed by the plugin itself and + // should not be included in the `newAdminRoutes` object. This ensures only top-level + // admin routes are added here, keeping the route structure consistent and preventing + // conflicts in routing. + + plugins.forEach((plugin) => { + if (plugin.admin) { + const routeKey = plugin.link.split('/')[2]; + newAdminRoutes[routeKey] = plugin.link; + } + }); + + return newAdminRoutes; +}; + interface RoutingProps { plugins: PluginConfig[]; location: RouterLocation; @@ -101,6 +124,7 @@ export const UnauthorisedPlugin = PluginPlaceHolder; export const AuthorisedAdminPage = withAuth(true)(AdminPage); const Routing: React.FC = (props: RoutingProps) => { + const adminRoutes = getAdminRoutes({ plugins: props.plugins }); // only set to false if we're on a plugin route i.e. not a scigateway route const manuallyLoadedPluginRef = React.useRef( Object.values(scigatewayRoutes).includes(props.location.pathname) || @@ -137,7 +161,6 @@ const Routing: React.FC = (props: RoutingProps) => { window.clearInterval(intervalId); }; }, [props.loading, props.plugins, props.location]); - React.useEffect(() => { // switching between an admin & non-admin route of the same app causes problems // as the Route and thus the plugin div changes but single-spa doesn't remount diff --git a/src/state/actions/scigateway.actions.tsx b/src/state/actions/scigateway.actions.tsx index 5746ccbe..18ef4055 100644 --- a/src/state/actions/scigateway.actions.tsx +++ b/src/state/actions/scigateway.actions.tsx @@ -5,6 +5,7 @@ import log from 'loglevel'; import { Step } from 'react-joyride'; import { Action, AnyAction } from 'redux'; import { ThunkAction } from 'redux-thunk'; +import * as singleSpa from 'single-spa'; import { AddHelpTourStepsPayload, AddHelpTourStepsType, @@ -18,15 +19,27 @@ import { ConfigureFeatureSwitchesType, ConfigureStringsPayload, ConfigureStringsType, + ContactUsAccessibilityFormUrlPayload, + CustomAdminPageDefaultTabPayload, + CustomAdminPageDefaultTabType, + CustomLogoPayload, + CustomLogoType, + CustomNavigationDrawerLogoPayload, + CustomNavigationDrawerLogoType, + CustomPrimaryColourPayload, + CustomPrimaryColourType, DismissNotificationPayload, DismissNotificationType, FeatureSwitches, FeatureSwitchesPayload, + HomepageUrlPayload, InitialiseAnalyticsType, InvalidateTokenType, LoadAuthProviderType, LoadDarkModePreferencePayload, LoadDarkModePreferenceType, + LoadHighContrastModePreferencePayload, + LoadHighContrastModePreferenceType, LoadMaintenanceStateType, LoadScheduledMaintenanceStateType, LoadedAuthType, @@ -34,8 +47,11 @@ import { MaintenanceState, MaintenanceStatePayLoad, NotificationType, + RegisterContactUsAccessibilityFormUrlType, RegisterHomepageUrlType, + RegisterRouteType, RequestPluginRerenderType, + ResetAuthStateType, ScheduledMaintenanceState, ScheduledMaintenanceStatePayLoad, SendThemeOptionsPayload, @@ -43,29 +59,13 @@ import { SignOutType, SiteLoadingPayload, SiteLoadingType, - HomepageUrlPayload, - CustomLogoPayload, ToggleDrawerType, ToggleHelpType, - RegisterRouteType, + baseAdminRoutes, scigatewayRoutes, - CustomLogoType, - LoadHighContrastModePreferenceType, - LoadHighContrastModePreferencePayload, - ResetAuthStateType, - CustomNavigationDrawerLogoPayload, - CustomNavigationDrawerLogoType, - CustomAdminPageDefaultTabPayload, - CustomAdminPageDefaultTabType, - RegisterContactUsAccessibilityFormUrlType, - ContactUsAccessibilityFormUrlPayload, - adminRoutes, - CustomPrimaryColourType, - CustomPrimaryColourPayload, } from '../scigateway.types'; import { ActionType, LogoState, StateType, ThunkResult } from '../state.types'; import loadMicroFrontends from './loadMicroFrontends'; -import * as singleSpa from 'single-spa'; export const configureStrings = ( appStrings: ApplicationStrings @@ -124,7 +124,7 @@ export const customNavigationDrawerLogo = ( }); export const customAdminPageDefaultTab = ( - adminPageDefaultTab: 'maintenance' | 'download' + adminPageDefaultTab: string ): ActionType => ({ type: CustomAdminPageDefaultTabType, payload: { @@ -342,11 +342,7 @@ export const configureSite = (): ThunkResult> => { dispatch(customPrimaryColour(settings['primaryColour'])); } - if ( - settings['adminPageDefaultTab'] && - (settings['adminPageDefaultTab'].includes('maintenance') || - settings['adminPageDefaultTab'].includes('download')) - ) { + if (settings['adminPageDefaultTab']) { dispatch(customAdminPageDefaultTab(settings['adminPageDefaultTab'])); } @@ -371,7 +367,7 @@ export const configureSite = (): ThunkResult> => { const currUrl = getState().router.location.pathname; if ( !Object.values(scigatewayRoutes).includes(currUrl) && - currUrl !== adminRoutes.maintenance && + currUrl !== baseAdminRoutes.maintenance && !getState().scigateway.plugins.find((p) => currUrl.startsWith(p.link.split('?')[0]) ) diff --git a/src/state/scigateway.types.tsx b/src/state/scigateway.types.tsx index 7a9f0d13..53004b08 100644 --- a/src/state/scigateway.types.tsx +++ b/src/state/scigateway.types.tsx @@ -55,9 +55,8 @@ export const scigatewayRoutes = { cookies: '/cookies', accessibility: '/accessibility', }; -export const adminRoutes = { +export const baseAdminRoutes = { maintenance: '/admin/maintenance', - download: '/admin/download', }; export interface NotificationPayload { @@ -99,7 +98,7 @@ export interface CustomNavigationDrawerLogoPayload { } export interface CustomAdminPageDefaultTabPayload { - adminPageDefaultTab: 'maintenance' | 'download'; + adminPageDefaultTab: string; } export interface CustomPrimaryColourPayload { diff --git a/src/state/state.types.tsx b/src/state/state.types.tsx index f6ecad05..018d0bca 100644 --- a/src/state/state.types.tsx +++ b/src/state/state.types.tsx @@ -1,14 +1,14 @@ -import { ThunkAction } from 'redux-thunk'; -import { AnyAction } from 'redux'; +import { RouterState } from 'connected-react-router'; import { Step } from 'react-joyride'; +import { AnyAction } from 'redux'; +import { ThunkAction } from 'redux-thunk'; import { ApplicationStrings, - PluginConfig, FeatureSwitches, - ScheduledMaintenanceState, MaintenanceState, + PluginConfig, + ScheduledMaintenanceState, } from './scigateway.types'; -import { RouterState } from 'connected-react-router'; export interface Plugin { name: string; @@ -41,7 +41,7 @@ export interface ScigatewayState { scheduledMaintenance: ScheduledMaintenanceState; maintenance: MaintenanceState; navigationDrawerLogo?: LogoState; - adminPageDefaultTab?: 'maintenance' | 'download'; + adminPageDefaultTab?: string; contactUsAccessibilityFormUrl?: string; primaryColour?: string; } From 0553a525b3718fb33060bc2e9839a02c1b163aa7 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Mon, 11 Nov 2024 15:12:06 +0000 Subject: [PATCH 2/2] improve coverage #1418 --- src/adminPage/adminPage.component.test.tsx | 49 ++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/adminPage/adminPage.component.test.tsx b/src/adminPage/adminPage.component.test.tsx index a929b6a7..76e32697 100644 --- a/src/adminPage/adminPage.component.test.tsx +++ b/src/adminPage/adminPage.component.test.tsx @@ -145,6 +145,55 @@ describe('Admin page component', () => { ).toBeInTheDocument(); }); + it("falls back to 'maintenance' when adminPageDefaultTab is not provided", () => { + state.scigateway.adminPageDefaultTab = undefined; + history.replace('/admin'); + + render(, { wrapper: Wrapper }); + + // Assert that the `maintenance` tab is selected by default + expect(screen.getByRole('tab', { name: 'Maintenance' })).toHaveAttribute( + 'aria-selected', + 'true' + ); + }); + + it("falls back to 'maintenance' when on an invalid route", () => { + state.scigateway.plugins = [ + { + order: 1, + plugin: 'datagateway-download', + link: '/admin/download', + section: 'Admin', + displayName: 'Admin Download', + admin: true, + }, + ]; + state.scigateway.adminPageDefaultTab = 'maintenance'; + history.replace('/admin/test'); + + render(, { wrapper: Wrapper }); + + // Assert that the `maintenance` tab is selected by default + expect(screen.getByRole('tab', { name: 'Maintenance' })).toHaveAttribute( + 'aria-selected', + 'true' + ); + }); + + it("falls back to 'maintenance' when adminPageDefaultTab doesn't match any key in adminRoutes", () => { + state.scigateway.adminPageDefaultTab = 'nonexistentTab'; + history.replace('/admin'); + + render(, { wrapper: Wrapper }); + + // Assert that the `maintenance` tab is selected by default + expect(screen.getByRole('tab', { name: 'Maintenance' })).toHaveAttribute( + 'aria-selected', + 'true' + ); + }); + it('should return an empty object when given an empty plugins array', () => { const plugins = []; const result = getAdminPluginRoutes({ plugins });