Skip to content

Commit

Permalink
Generate custom admin tabs from plugin routes #1418
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
joshdimanteto committed Nov 11, 2024
1 parent 5f9dbde commit c469cb6
Show file tree
Hide file tree
Showing 12 changed files with 270 additions and 183 deletions.
6 changes: 3 additions & 3 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -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()),
Expand Down
56 changes: 36 additions & 20 deletions src/adminPage/adminPage.component.test.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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(<AdminPage />, { wrapper: Wrapper });

Expand Down Expand Up @@ -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({});
});

Expand Down Expand Up @@ -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' },
});
});

Expand All @@ -175,23 +189,25 @@ describe('Admin page component', () => {
{
plugin: 'PluginA',
admin: true,
link: '/admin/pluginA',
link: '/admin/pluginALink',
section: 'A',
displayName: 'A',
order: 1,
},
{
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',
},
});
});
});
119 changes: 68 additions & 51 deletions src/adminPage/adminPage.component.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string[]> => {
const pluginRoutes: Record<string, string[]> = {};
export const getAdminPluginRoutes = (props: {
plugins: PluginConfig[];
}): Record<string, Record<string, string>> => {
const { plugins } = props;
const pluginRoutes: Record<string, Record<string, string>> = {};

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<string>(
(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();
Expand All @@ -76,22 +88,26 @@ const AdminPage = (props: AdminPageProps): ReactElement => {
setTabValue(newValue);
}}
>
<Tab
id="maintenance-tab"
aria-controls="maintenance-panel"
label="Maintenance"
value="maintenance"
component={Link}
to={adminRoutes.maintenance}
/>
<Tab
id="download-tab"
label="Admin Download"
value="download"
aria-controls="download-panel"
component={Link}
to={adminRoutes.download}
/>
{Object.entries(adminRoutes).map(([key, value]) => {
const pluginDetails = props.plugins.find(
(plugin) => plugin.link === value
);

return (
<Tab
key={key}
id={`${key}-tab`}
label={
pluginDetails?.displayName ||
`${key.charAt(0).toUpperCase() + key.slice(1)}`
}
value={key}
aria-controls={`${key}-panel`}
component={Link}
to={value}
/>
);
})}
</Tabs>
<Switch>
<Route exact path={adminRoutes.maintenance}>
Expand All @@ -105,20 +121,21 @@ const AdminPage = (props: AdminPageProps): ReactElement => {
</div>
</Route>

{Object.entries(pluginRoutes).map(([key, value]) => {
return (
<Route exact key={key} path={value}>
{Object.entries(pluginRoutes).map(([pluginName, tabRoutes]) =>
Object.entries(tabRoutes).map(([tabName, route]) => (
<Route key={`${pluginName}-${tabName}}`} path={route}>
<div
id="download-panel"
aria-labelledby="download-tab"
id={`${tabName}-panel`}
aria-labelledby={`${tabName}-tab`}
role="tabpanel"
hidden={tabValue !== 'download'}
hidden={tabValue !== tabName}
>
<PluginPlaceHolder id={key} />
<PluginPlaceHolder id={pluginName} />
</div>
</Route>
);
})}
))
)}

<Route component={PageNotFound} />
</Switch>
</Paper>
Expand Down
37 changes: 24 additions & 13 deletions src/mainAppBar/mainAppBar.component.test.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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(<MainAppBarComponent />, { wrapper: Wrapper });
Expand Down
Loading

0 comments on commit c469cb6

Please sign in to comment.