diff --git a/package-lock.json b/package-lock.json index f2c6beb71..3dac94699 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@gridsuite/commons-ui": "0.234.0", + "@gridsuite/commons-ui": "0.237.0", "@hello-pangea/dnd": "^18.0.1", "@hookform/resolvers": "^4.1.3", "@mui/icons-material": "^6.5.0", @@ -3309,9 +3309,9 @@ } }, "node_modules/@gridsuite/commons-ui": { - "version": "0.234.0", - "resolved": "https://registry.npmjs.org/@gridsuite/commons-ui/-/commons-ui-0.234.0.tgz", - "integrity": "sha512-OXkfwLifENo+r9QhP6FVHiw2gmVuRA4BX0Mp24dPeXnbCzU9LJHdTYKq743IMxeclGyKKqjO0llKT7HHJ591oA==", + "version": "0.237.0", + "resolved": "https://registry.npmjs.org/@gridsuite/commons-ui/-/commons-ui-0.237.0.tgz", + "integrity": "sha512-iB2gle7cOdBjj0Nyr6YJ6pkLREJ36eJHI9VAugZ1MX0kdXTHDN+eUYF7nArs0Oqrcv274PKDtgjhinflKLsC0w==", "license": "MPL-2.0", "dependencies": { "@ag-grid-community/locale": "^35.3.1", diff --git a/package.json b/package.json index 8d650c80e..0442993ac 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@gridsuite/commons-ui": "0.234.0", + "@gridsuite/commons-ui": "0.237.0", "@hello-pangea/dnd": "^18.0.1", "@hookform/resolvers": "^4.1.3", "@mui/icons-material": "^6.5.0", diff --git a/src/components/directory-content-dialog.tsx b/src/components/directory-content-dialog.tsx index a232abe77..aebb11f03 100644 --- a/src/components/directory-content-dialog.tsx +++ b/src/components/directory-content-dialog.tsx @@ -82,8 +82,11 @@ function DirectoryContentDialog( const itemSelectionForCopy = useSelector((state: AppState) => state.itemSelectionForCopy); const activeDirectory = useSelector((state: AppState) => state.activeDirectory); const isDeveloperMode = useSelector((state: AppState) => state.isDeveloperMode); - const user = useSelector((state: AppState) => state.user); - + const userProfile = useSelector( + (state: AppState) => state.user?.profile ?? null, + (a, b) => + a === b || (a?.sub === b?.sub && a?.name === b?.name && a?.email === b?.email && a?.profile === b?.profile) + ); const [languageLocal] = useParameterState(PARAM_LANGUAGE); const [elementName, setElementName] = useState(''); const [elementDescription, setElementDescription] = useState(''); @@ -403,7 +406,7 @@ function DirectoryContentDialog( titleId="editParameters" name={elementName} description={activeElement.description} - user={user} + userProfile={userProfile} activeDirectory={activeDirectory} language={languageLocal} isDeveloperMode={isDeveloperMode} @@ -419,7 +422,7 @@ function DirectoryContentDialog( titleId="editParameters" name={elementName} description={activeElement.description} - user={user} + userProfile={userProfile} activeDirectory={activeDirectory} language={languageLocal} /> @@ -434,7 +437,7 @@ function DirectoryContentDialog( titleId="editParameters" name={elementName} description={activeElement.description} - user={user} + userProfile={userProfile} activeDirectory={activeDirectory} language={languageLocal} /> @@ -449,7 +452,7 @@ function DirectoryContentDialog( titleId="editParameters" name={elementName} description={activeElement.description} - user={user} + userProfile={userProfile} activeDirectory={activeDirectory} language={languageLocal} /> @@ -464,7 +467,7 @@ function DirectoryContentDialog( titleId="editParameters" name={elementName} description={activeElement.description} - user={user} + userProfile={userProfile} activeDirectory={activeDirectory} language={languageLocal} /> @@ -479,7 +482,7 @@ function DirectoryContentDialog( titleId="editParameters" name={elementName} description={activeElement.description} - user={user} + userProfile={userProfile} activeDirectory={activeDirectory} language={languageLocal} isDeveloperMode={isDeveloperMode} @@ -495,7 +498,7 @@ function DirectoryContentDialog( titleId="editParameters" name={elementName} description={activeElement.description} - user={user} + userProfile={userProfile} activeDirectory={activeDirectory} language={languageLocal} /> diff --git a/src/components/silent-renew.tsx b/src/components/silent-renew.tsx new file mode 100644 index 000000000..067212c9e --- /dev/null +++ b/src/components/silent-renew.tsx @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2026, 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 type { UserManager } from 'oidc-client-ts'; +import { + handleSilentRenewCallback, + initializeAuthenticationProd, + SilentRenewCallbackHandler, +} from '@gridsuite/commons-ui'; +import { getCachedIdpSettings } from '../utils/rest-api'; + +export default function SilentRenew() { + const [userManager, setUserManager] = useState(null); + + useEffect(() => { + initializeAuthenticationProd( + () => undefined /* dispatch: unused when isSilentRenew=true */, + true /* isSilentRenew */, + getCachedIdpSettings /* reads the cache instead of fetching idpSettings.json */, + false /* isSigningCallback */ + ) + .then(setUserManager) + .catch((e) => console.error('Silent renew init failed:', e)); + }, []); + + const handleSilentRenewCallbackClosure = useCallback(() => { + if (userManager) { + handleSilentRenewCallback(userManager); + } + }, [userManager]); + + return ( + + ); +} diff --git a/src/components/tree-views-container.tsx b/src/components/tree-views-container.tsx index 332abda99..56c2c39b9 100644 --- a/src/components/tree-views-container.tsx +++ b/src/components/tree-views-container.tsx @@ -92,7 +92,11 @@ export default function TreeViewsContainer({ sourceItemUuid }: { readonly source const [openDialog, setOpenDialog] = useState(constants.DialogsId.NONE); - const user = useSelector((state: AppState) => state.user); + const userProfile = useSelector( + (state: AppState) => state.user?.profile ?? null, + (a, b) => + a === b || (a?.sub === b?.sub && a?.name === b?.name && a?.email === b?.email && a?.profile === b?.profile) + ); const selectedDirectory = useSelector((state: AppState) => state.selectedDirectory); const activeDirectory = useSelector((state: AppState) => state.activeDirectory); const currentPath = useSelector((state: AppState) => state.currentPath); @@ -210,10 +214,10 @@ export default function TreeViewsContainer({ sourceItemUuid }: { readonly source /* rootDirectories initialization */ useEffect(() => { - if (user != null) { + if (userProfile != null) { updateRootDirectories(); } - }, [user, updateRootDirectories]); + }, [userProfile, updateRootDirectories]); /* Manage current path data */ useEffect(() => { diff --git a/src/index.tsx b/src/index.tsx index 3527dd723..10ebfeb47 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,14 +4,23 @@ * 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 'core-js/es/array/flat-map'; - -import 'typeface-roboto'; -import './index.css'; -import './configure-yup-init'; - import { createRoot } from 'react-dom/client'; -import AppWrapper from './components/app-wrapper'; +import SilentRenew from './components/silent-renew'; +import { SILENT_RENEW_CALLBACK_PATH } from './utils/rest-api'; const container = document.getElementById('root'); -createRoot(container!).render(); +const root = createRoot(container!); + +async function renderApp() { + if (globalThis.location.pathname.endsWith(SILENT_RENEW_CALLBACK_PATH)) { + root.render(); + return; + } + await import('core-js/es/array/flat-map'); + await import('typeface-roboto'); + await import('./index.css'); + await import('./configure-yup-init'); + const { default: AppWrapper } = await import('./components/app-wrapper'); + root.render(); +} +renderApp().catch((error) => console.error(error)); diff --git a/src/module-core-js-array-flat-map.d.ts b/src/module-core-js-array-flat-map.d.ts new file mode 100644 index 000000000..ee11d44f0 --- /dev/null +++ b/src/module-core-js-array-flat-map.d.ts @@ -0,0 +1,7 @@ +/** + * Copyright (c) 2026, 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/. + */ +declare module 'core-js/es/array/flat-map'; diff --git a/src/module-typeface-roboto.d.ts b/src/module-typeface-roboto.d.ts new file mode 100644 index 000000000..bfcdec5a3 --- /dev/null +++ b/src/module-typeface-roboto.d.ts @@ -0,0 +1,7 @@ +/** + * Copyright (c) 2026, 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/. + */ +declare module 'typeface-roboto'; diff --git a/src/utils/rest-api.ts b/src/utils/rest-api.ts index d73042520..281064f8f 100644 --- a/src/utils/rest-api.ts +++ b/src/utils/rest-api.ts @@ -16,6 +16,7 @@ import { type GsLang, type GsTheme, hasElementPermission, + IdpSettings, LAST_SELECTED_DIRECTORY, PARAM_DEVELOPER_MODE, PARAM_LANGUAGE, @@ -47,6 +48,8 @@ const PREFIX_FILTERS_QUERIES = `${import.meta.env.VITE_API_GATEWAY}/filter/v1/fi const PREFIX_STUDY_QUERIES = `${import.meta.env.VITE_API_GATEWAY}/study`; const PREFIX_SPREADSHEET_CONFIG_QUERIES = `${import.meta.env.VITE_API_GATEWAY}/study-config`; const PREFIX_MONITOR_QUERIES = `${import.meta.env.VITE_API_GATEWAY}/monitor`; +const IDP_SETTINGS_CACHE_KEY = 'gridsuite-idp-settings'; +export const SILENT_RENEW_CALLBACK_PATH = '/silent-renew-callback'; export type KeyOfWithoutIndexSignature = { // copy every declared property from T but remove index signatures @@ -96,8 +99,34 @@ const getContingencyUriParamType = (contingencyListType: string | null | undefin } }; -export function fetchIdpSettings() { - return fetch('idpSettings.json').then((res) => res.json()); +// Always hits the network: picks up config changes on each full app load +// AND refreshes the cache read by the silent-renew iframe. +export function fetchIdpSettings(): Promise { + return fetch('idpSettings.json') + .then((res) => res.json()) + .then((settings: IdpSettings) => { + try { + localStorage.setItem(IDP_SETTINGS_CACHE_KEY, JSON.stringify(settings)); + } catch (e) { + console.warn('Failed to cache IdP settings:', e); + } + return settings; + }); +} + +// Used only on the silent-renew path: reads the cache (no network), +// falls back to a real fetch if the cache is missing/corrupted. +export function getCachedIdpSettings(): Promise { + try { + const cached = localStorage.getItem(IDP_SETTINGS_CACHE_KEY); + if (cached) { + return Promise.resolve(JSON.parse(cached) as IdpSettings); + } + } catch (e) { + // localStorage unavailable, or cache corrupted -> fall back to fresh fetch + console.warn('Failed to read cached IdP settings:', e); + } + return fetchIdpSettings(); } export function fetchVersion() {