Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { Config } from 'jest';
const config: Config = {
testEnvironment: 'jsdom',
moduleNameMapper: {
'^.+\\.svg\\?react$': 'jest-svg-transformer',
'^.+\\.svg\\?react$': '<rootDir>/src/_mocks_/svg.tsx',
'^.+\\.(css|less|scss)$': 'identity-obj-proxy',
},
// see https://github.com/react-dnd/react-dnd/issues/3443
Expand Down
9 changes: 7 additions & 2 deletions src/routes/index.ts → src/_mocks_/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
/*
/**
* Copyright (c) 2024, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

export * from './router';
export default function fetchMock() {
return Promise.resolve({
ok: true,
json: () => ({ appsMetadataServerUrl: '' }), // just to remove the error logs when fetching env
});
}
9 changes: 5 additions & 4 deletions src/components/App/index.ts → src/_mocks_/svg.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/*
/**
* Copyright (c) 2024, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import AppComponent from './app';
export type App = typeof AppComponent;
import { forwardRef, SVGProps } from 'react';

export { AppWrapper } from './app-wrapper';
const SvgrMock = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>((props, ref) => <svg ref={ref} {...props} />);

export default SvgrMock;
205 changes: 205 additions & 0 deletions src/components/App/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* Copyright (c) 2020, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Grid } from '@mui/material';
import {
AnnouncementNotification,
AuthenticationRouter,
CardErrorBoundary,
fetchConfigParameter,
fetchConfigParameters,
getComputedLanguage,
getPreLoginPath,
initializeAuthenticationProd,
NotificationsUrlKeys,
useNotificationsListener,
UserManagerState,
useSnackMessage,
} from '@gridsuite/commons-ui';
import { selectComputedLanguage, selectLanguage, selectTheme } from '../../redux/actions';
import { AppState } from '../../redux/reducer';
import { AppsMetadataSrv, ConfigParameters } from '../../services';
import { APP_NAME, COMMON_APP_NAME, PARAM_LANGUAGE, PARAM_THEME } from '../../utils/config-params';
import AppTopBar from './AppTopBar';
import { useDebugRender } from '../../utils/hooks';
import { AppDispatch } from '../../redux/store';
import { Navigate, Route, Routes, useLocation, useMatch, useNavigate } from 'react-router';
import PageNotFound from './PageNotFound';
import { FormattedMessage } from 'react-intl';
import { MainPaths } from './utils';
import { Announcements, Groups, Profiles, Users } from '../../pages';
import HomePage from './HomePage';

export default function App() {
useDebugRender('app');
const { snackError } = useSnackMessage();
const dispatch = useDispatch<AppDispatch>();
const user = useSelector((state: AppState) => state.user);

const updateParams = useCallback(
(params: ConfigParameters) => {
console.groupCollapsed('received UI parameters');
console.table(params);
console.groupEnd();
params.forEach((param) => {
switch (param.name) {
case PARAM_THEME:
dispatch(selectTheme(param.value));
break;
case PARAM_LANGUAGE:
dispatch(selectLanguage(param.value));
dispatch(selectComputedLanguage(getComputedLanguage(param.value)));
break;
default:
break;
}
});
},
[dispatch]
);

const updateConfig = useCallback(
(event: MessageEvent) => {
const eventData = JSON.parse(event.data);
if (eventData?.headers?.parameterName) {
fetchConfigParameter(APP_NAME, eventData.headers.parameterName)
.then((param) => updateParams([param]))
.catch((error) => snackError({ messageTxt: error.message, headerId: 'paramsRetrievingError' }));
}
},
[updateParams, snackError]
);

useNotificationsListener(NotificationsUrlKeys.CONFIG, { listenerCallbackMessage: updateConfig });

useEffect(() => {
if (user !== null) {
fetchConfigParameters(COMMON_APP_NAME)
.then((params) => updateParams(params))
.catch((error) =>
snackError({
messageTxt: error.message,
headerId: 'paramsRetrievingError',
})
);

fetchConfigParameters(APP_NAME)
.then((params) => updateParams(params))
.catch((error) =>
snackError({
messageTxt: error.message,
headerId: 'paramsRetrievingError',
})
);
}
}, [user, dispatch, updateParams, snackError]);

const signInCallbackError = useSelector((state: AppState) => state.signInCallbackError);
const authenticationRouterError = useSelector((state: AppState) => state.authenticationRouterError);
const showAuthenticationRouterLogin = useSelector((state: AppState) => state.showAuthenticationRouterLogin);
const [userManager, setUserManager] = useState<UserManagerState>({
instance: null,
error: null,
});
const navigate = useNavigate();
const location = useLocation();

// Can't use lazy initializer because useRouteMatch is a hook
const [initialMatchSilentRenewCallbackUrl] = useState(
useMatch({
path: '/silent-renew-callback',
})
);

const [initialMatchSigninCallbackUrl] = useState(
useMatch({
path: '/sign-in-callback',
})
);

useEffect(() => {
// need subfunction when async as suggested by rule react-hooks/exhaustive-deps
(async function initializeAuthentication() {
try {
setUserManager({
instance: await initializeAuthenticationProd(
dispatch,
initialMatchSilentRenewCallbackUrl != null,
AppsMetadataSrv.fetchIdpSettings,
initialMatchSigninCallbackUrl != null
),
error: null,
});
} catch (error: any) {
setUserManager({ instance: null, error: error.message });
}
})();
// Note: initialMatchSilentRenewCallbackUrl and dispatch don't change
}, [initialMatchSilentRenewCallbackUrl, dispatch, initialMatchSigninCallbackUrl]);

return (
<Grid
container
direction="column"
spacing={0}
justifyContent="flex-start"
alignItems="stretch"
sx={{ height: '100vh', width: '100vw' }}
>
<Grid item xs="auto">
<AppTopBar userManagerInstance={userManager.instance} />
</Grid>
<Grid item xs="auto">
<AnnouncementNotification user={user} />
</Grid>
<Grid item container xs component="main" height={'100%'}>
<CardErrorBoundary>
<div
className="singlestretch-parent"
style={{
flexGrow: 1,
}}
>
{user !== null ? (
<Routes>
<Route path={'/'} element={<HomePage />} />
<Route path={`/${MainPaths.users}`} element={<Users />} />
<Route path={`/${MainPaths.profiles}`} element={<Profiles />} />
<Route path={`/${MainPaths.groups}`} element={<Groups />} />
<Route path={`/${MainPaths.announcements}`} element={<Announcements />} />
<Route
path="/sign-in-callback"
element={<Navigate replace to={getPreLoginPath() || '/'} />}
/>
<Route
path="/logout-callback"
element={<h1>Error: logout failed; you are still logged in.</h1>}
/>
<Route
path="*"
element={<PageNotFound message={<FormattedMessage id="PageNotFound" />} />}
/>
</Routes>
) : (
<AuthenticationRouter
userManager={userManager}
signInCallbackError={signInCallbackError}
authenticationRouterError={authenticationRouterError}
showAuthenticationRouterLogin={showAuthenticationRouterLogin}
dispatch={dispatch}
navigate={navigate}
location={location}
/>
)}
</div>
</CardErrorBoundary>
</Grid>
</Grid>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,13 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import {
type AnchorHTMLAttributes,
forwardRef,
type FunctionComponent,
type ReactElement,
useEffect,
useMemo,
useState,
} from 'react';
import { capitalize, Tab, Tabs, useTheme } from '@mui/material';
import { type AnchorHTMLAttributes, forwardRef, type ReactElement, SyntheticEvent, useEffect, useState } from 'react';
import { capitalize, Tab, Tabs, TabsProps, useTheme } from '@mui/material';
import { Groups, ManageAccounts, NotificationImportant, PeopleAlt } from '@mui/icons-material';
import { fetchAppsMetadata, logout, Metadata, TopBar } from '@gridsuite/commons-ui';
import { fetchAppsMetadata, logout, Metadata, TopBar, UserManagerState } from '@gridsuite/commons-ui';
import { useParameterState } from '../parameters';
import { APP_NAME, PARAM_LANGUAGE, PARAM_THEME } from '../../utils/config-params';
import { NavLink, type To, useMatches, useNavigate } from 'react-router';
import { NavLink, type To, useNavigate } from 'react-router';
import { useDispatch, useSelector } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { AppsMetadataSrv, StudySrv } from '../../services';
Expand All @@ -28,7 +20,7 @@ import GridAdminLogoDark from '../../images/GridAdmin_logo_dark.svg?react';
import AppPackage from '../../../package.json';
import { AppState } from '../../redux/reducer';
import { AppDispatch } from '../../redux/store';
import { MainPaths } from '../../routes/utils';
import { MainPaths } from './utils';

const tabs = new Map<MainPaths, ReactElement>([
[
Expand Down Expand Up @@ -89,27 +81,22 @@ const tabs = new Map<MainPaths, ReactElement>([
],
]);

const AppTopBar: FunctionComponent = () => {
type AppTopBarProps = {
userManagerInstance: UserManagerState['instance'];
};

export default function AppTopBar({ userManagerInstance }: Readonly<AppTopBarProps>) {
const theme = useTheme();
const dispatch = useDispatch<AppDispatch>();
const user = useSelector((state: AppState) => state.user);
const userManagerInstance = useSelector((state: AppState) => state.userManager?.instance);

const navigate = useNavigate();
const matches = useMatches();
const selectedTabValue = useMemo(() => {
const handle: any = matches
.map((match) => match.handle)
.filter((handle: any) => !!handle?.appBar_tab)
.shift();
const tabValue: MainPaths = handle?.appBar_tab;
return tabValue && tabs.has(tabValue) ? tabValue : false;
}, [matches]);

const [themeLocal, handleChangeTheme] = useParameterState(PARAM_THEME);
const [languageLocal, handleChangeLanguage] = useParameterState(PARAM_LANGUAGE);

const [appsAndUrls, setAppsAndUrls] = useState<Metadata[]>([]);
const [tabValue, setTabValue] = useState<TabsProps['value']>(false);

useEffect(() => {
if (user !== null) {
Expand All @@ -119,6 +106,10 @@ const AppTopBar: FunctionComponent = () => {
}
}, [user]);

const handleChange = (_: SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};

return (
<TopBar
appName={capitalize(APP_NAME)}
Expand Down Expand Up @@ -147,11 +138,11 @@ const AppTopBar: FunctionComponent = () => {
visibility: !user ? 'hidden' : undefined,
flexGrow: 1,
}}
value={selectedTabValue}
value={tabValue}
onChange={handleChange}
>
{[...tabs.values()]}
</Tabs>
</TopBar>
);
};
export default AppTopBar;
}
Loading
Loading