From a4c7c62780f685bf110676a558c6e647705ecdfb Mon Sep 17 00:00:00 2001 From: Rehili Ghazwa Date: Fri, 17 Apr 2026 16:16:37 +0200 Subject: [PATCH 01/10] prevent app refetch on token silent renew --- src/hooks/use-notifications-url-generator.ts | 14 ++++++------- src/redux/reducer.ts | 22 +++++++++++++++++++- src/redux/reducer.type.ts | 6 ++++++ src/redux/user-store.ts | 7 ++++++- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/hooks/use-notifications-url-generator.ts b/src/hooks/use-notifications-url-generator.ts index da171cf288..d158d70773 100644 --- a/src/hooks/use-notifications-url-generator.ts +++ b/src/hooks/use-notifications-url-generator.ts @@ -19,42 +19,42 @@ import { APP_NAME } from 'utils/config-params'; const useNotificationsUrlGenerator = (): Partial> => { // The websocket API doesn't allow relative urls const wsBase = getWsBase(); - const tokenId = useSelector((state: AppState) => state.user?.id_token); + const isAuthenticated = useSelector((state: AppState) => state.userToken?.id_token !== undefined); const studyUuid = useSelector((state: AppState) => state.studyUuid); // return a mapColumns with NOTIFICATIONS_URL_KEYS and undefined value if URL is not yet buildable (tokenId) // it will be used to register listeners as soon as possible. return useMemo( () => ({ - [NotificationsUrlKeys.CONFIG]: tokenId + [NotificationsUrlKeys.CONFIG]: isAuthenticated ? getUrlWithToken( `${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/notify?${new URLSearchParams({ appName: APP_NAME, })}` ) : undefined, - [NotificationsUrlKeys.GLOBAL_CONFIG]: tokenId + [NotificationsUrlKeys.GLOBAL_CONFIG]: isAuthenticated ? getUrlWithToken(`${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/global`) : undefined, [NotificationsUrlKeys.STUDY]: - tokenId && studyUuid + isAuthenticated && studyUuid ? getUrlWithToken( `${wsBase}${PREFIX_STUDY_NOTIFICATION_WS}/notify?studyUuid=${encodeURIComponent(studyUuid)}` ) : undefined, [NotificationsUrlKeys.DIRECTORY_DELETE_STUDY]: - tokenId && studyUuid + isAuthenticated && studyUuid ? getUrlWithToken( `${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=deleteElement&elementUuid=${encodeURIComponent( studyUuid )}` ) : undefined, - [NotificationsUrlKeys.DIRECTORY]: tokenId + [NotificationsUrlKeys.DIRECTORY]: isAuthenticated ? getUrlWithToken(`${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=directories`) : undefined, }), - [tokenId, wsBase, studyUuid] + [isAuthenticated, wsBase, studyUuid] ); }; diff --git a/src/redux/reducer.ts b/src/redux/reducer.ts index 1a41adbf68..3aaebe1b8f 100644 --- a/src/redux/reducer.ts +++ b/src/redux/reducer.ts @@ -477,6 +477,7 @@ const initialState: AppState = { isNetworkModificationTreeModelUpToDate: false, computedLanguage: getLocalStorageComputedLanguage(), user: null, + userToken: null, signInCallbackError: null, authenticationRouterError: null, showAuthenticationRouterLogin: false, @@ -1019,7 +1020,26 @@ export const reducer = createReducer(initialState, (builder) => { }); builder.addCase(USER, (state, action: UserAction) => { - state.user = action.user; + const newUser = action.user; + // Token: always refreshed (new ref on every silent renew) + state.userToken = newUser + ? { + access_token: newUser.access_token, + id_token: newUser.id_token, + expires_at: newUser.expires_at, + } + : null; + // Identity: only update reference when sub actually changes. + // On silent renew, the profile is identical -> keep the same reference + // to avoid re-rendering every component that selects state.user. + const currentUser = state.user; + if (currentUser === null || newUser === null) { + state.user = newUser; + return; + } + if (currentUser.profile?.sub !== newUser.profile?.sub) { + state.user = newUser; + } }); builder.addCase(ENABLE_DEVELOPER_MODE, (state, action: EnableDeveloperModeAction) => { diff --git a/src/redux/reducer.type.ts b/src/redux/reducer.type.ts index 9e6175e03e..5a2858b7ee 100644 --- a/src/redux/reducer.type.ts +++ b/src/redux/reducer.type.ts @@ -65,6 +65,11 @@ import { import { PARAM_COMPUTED_LANGUAGE, PARAM_LIMIT_REDUCTION, PARAM_USE_NAME, PARAMS_LOADED } from '../utils/config-params'; import { VOLTAGE_LEVEL_ID } from '../components/utils/field-constants'; +export type UserToken = { + access_token: string; + id_token: string; + expires_at: number; +}; // ——— Equipments ——— export enum EquipmentUpdateType { @@ -260,4 +265,5 @@ export interface AppState extends CommonStoreState, AppConfigState { calculationSelections: Record; highlightedModificationUuid: UUID | null; tableFilters: TableFiltersState; + userToken: UserToken | null; } diff --git a/src/redux/user-store.ts b/src/redux/user-store.ts index 77df67e730..86875dff73 100644 --- a/src/redux/user-store.ts +++ b/src/redux/user-store.ts @@ -27,6 +27,11 @@ import { User } from 'oidc-client'; type UserStoreState = { user: User | null; + userToken: { + access_token: string; + id_token: string; + expires_at: number; + } | null; }; interface UserStore { @@ -41,5 +46,5 @@ export function setUserStore(store: UserStore): void { //TODO use the one from commons-ui instead when exported in next version export function getUserToken() { - return userStore?.getState().user?.id_token ?? undefined; + return userStore?.getState().userToken?.id_token ?? undefined; } From 94f82e485fc9ca00beaa2927eea329a5948893d0 Mon Sep 17 00:00:00 2001 From: Rehili Ghazwa Date: Fri, 17 Apr 2026 17:47:44 +0200 Subject: [PATCH 02/10] getUserToken as getToken prop to NotificationsProvide --- src/components/app-wrapper.jsx | 3 ++- src/hooks/use-notifications-url-generator.ts | 26 ++++++++------------ src/services/utils.ts | 9 ------- 3 files changed, 12 insertions(+), 26 deletions(-) diff --git a/src/components/app-wrapper.jsx b/src/components/app-wrapper.jsx index 7a4c31b376..fb98117023 100644 --- a/src/components/app-wrapper.jsx +++ b/src/components/app-wrapper.jsx @@ -118,6 +118,7 @@ import { AllCommunityModule, ModuleRegistry, provideGlobalGridOptions } from 'ag import { getBaseVoltagesCssVars } from '../utils/colors.ts'; import { lightThemeCssVars } from '../styles/light-theme-css-vars.ts'; import { darkThemeCssVars } from '../styles/dark-theme-css-vars.ts'; +import { getUserToken } from '../redux/user-store.ts'; // Register all community features (migration to V33) ModuleRegistry.registerModules([AllCommunityModule]); @@ -507,7 +508,7 @@ const AppWrapperWithRedux = () => { - + diff --git a/src/hooks/use-notifications-url-generator.ts b/src/hooks/use-notifications-url-generator.ts index d158d70773..da3198a83c 100644 --- a/src/hooks/use-notifications-url-generator.ts +++ b/src/hooks/use-notifications-url-generator.ts @@ -13,7 +13,7 @@ import { import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { type AppState } from 'redux/reducer.type'; -import { getUrlWithToken, getWsBase } from 'services/utils'; +import { getWsBase } from 'services/utils'; import { APP_NAME } from 'utils/config-params'; const useNotificationsUrlGenerator = (): Partial> => { @@ -27,31 +27,25 @@ const useNotificationsUrlGenerator = (): Partial ({ [NotificationsUrlKeys.CONFIG]: isAuthenticated - ? getUrlWithToken( - `${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/notify?${new URLSearchParams({ - appName: APP_NAME, - })}` - ) + ? `${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/notify?${new URLSearchParams({ + appName: APP_NAME, + })}` : undefined, [NotificationsUrlKeys.GLOBAL_CONFIG]: isAuthenticated - ? getUrlWithToken(`${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/global`) + ? `${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/global` : undefined, [NotificationsUrlKeys.STUDY]: isAuthenticated && studyUuid - ? getUrlWithToken( - `${wsBase}${PREFIX_STUDY_NOTIFICATION_WS}/notify?studyUuid=${encodeURIComponent(studyUuid)}` - ) + ? `${wsBase}${PREFIX_STUDY_NOTIFICATION_WS}/notify?studyUuid=${encodeURIComponent(studyUuid)}` : undefined, [NotificationsUrlKeys.DIRECTORY_DELETE_STUDY]: isAuthenticated && studyUuid - ? getUrlWithToken( - `${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=deleteElement&elementUuid=${encodeURIComponent( - studyUuid - )}` - ) + ? `${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=deleteElement&elementUuid=${encodeURIComponent( + studyUuid + )}` : undefined, [NotificationsUrlKeys.DIRECTORY]: isAuthenticated - ? getUrlWithToken(`${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=directories`) + ? `${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=directories` : undefined, }), [isAuthenticated, wsBase, studyUuid] diff --git a/src/services/utils.ts b/src/services/utils.ts index 407cddc2ac..1eaf3d08d4 100644 --- a/src/services/utils.ts +++ b/src/services/utils.ts @@ -5,7 +5,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { catchErrorHandler, fetchStudyMetadata, StudyMetadata } from '@gridsuite/commons-ui'; -import { getUserToken } from '../redux/user-store'; export const FetchStatus = { SUCCEED: 'SUCCEED', @@ -91,14 +90,6 @@ export const getQueryParamsList = (params: string[] | number[] | null | undefine return ''; }; -export function getUrlWithToken(baseUrl: string) { - if (baseUrl.includes('?')) { - return baseUrl + '&access_token=' + getUserToken(); - } else { - return baseUrl + '?access_token=' + getUserToken(); - } -} - export function fetchMapBoxToken() { console.info(`Fetching MapBoxToken...`); return fetchEnv().then((res) => res.mapBoxToken); From accc088f975922f7d39f61d7c1a79089a2ede582 Mon Sep 17 00:00:00 2001 From: Rehili Ghazwa Date: Tue, 21 Apr 2026 17:23:49 +0200 Subject: [PATCH 03/10] code review remarks to revert code --- src/components/app-wrapper.jsx | 3 +- src/hooks/use-notifications-url-generator.ts | 40 +++++++++++--------- src/services/utils.ts | 9 ++++- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/components/app-wrapper.jsx b/src/components/app-wrapper.jsx index fb98117023..7a4c31b376 100644 --- a/src/components/app-wrapper.jsx +++ b/src/components/app-wrapper.jsx @@ -118,7 +118,6 @@ import { AllCommunityModule, ModuleRegistry, provideGlobalGridOptions } from 'ag import { getBaseVoltagesCssVars } from '../utils/colors.ts'; import { lightThemeCssVars } from '../styles/light-theme-css-vars.ts'; import { darkThemeCssVars } from '../styles/dark-theme-css-vars.ts'; -import { getUserToken } from '../redux/user-store.ts'; // Register all community features (migration to V33) ModuleRegistry.registerModules([AllCommunityModule]); @@ -508,7 +507,7 @@ const AppWrapperWithRedux = () => { - + diff --git a/src/hooks/use-notifications-url-generator.ts b/src/hooks/use-notifications-url-generator.ts index da3198a83c..da171cf288 100644 --- a/src/hooks/use-notifications-url-generator.ts +++ b/src/hooks/use-notifications-url-generator.ts @@ -13,42 +13,48 @@ import { import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { type AppState } from 'redux/reducer.type'; -import { getWsBase } from 'services/utils'; +import { getUrlWithToken, getWsBase } from 'services/utils'; import { APP_NAME } from 'utils/config-params'; const useNotificationsUrlGenerator = (): Partial> => { // The websocket API doesn't allow relative urls const wsBase = getWsBase(); - const isAuthenticated = useSelector((state: AppState) => state.userToken?.id_token !== undefined); + const tokenId = useSelector((state: AppState) => state.user?.id_token); const studyUuid = useSelector((state: AppState) => state.studyUuid); // return a mapColumns with NOTIFICATIONS_URL_KEYS and undefined value if URL is not yet buildable (tokenId) // it will be used to register listeners as soon as possible. return useMemo( () => ({ - [NotificationsUrlKeys.CONFIG]: isAuthenticated - ? `${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/notify?${new URLSearchParams({ - appName: APP_NAME, - })}` + [NotificationsUrlKeys.CONFIG]: tokenId + ? getUrlWithToken( + `${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/notify?${new URLSearchParams({ + appName: APP_NAME, + })}` + ) : undefined, - [NotificationsUrlKeys.GLOBAL_CONFIG]: isAuthenticated - ? `${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/global` + [NotificationsUrlKeys.GLOBAL_CONFIG]: tokenId + ? getUrlWithToken(`${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/global`) : undefined, [NotificationsUrlKeys.STUDY]: - isAuthenticated && studyUuid - ? `${wsBase}${PREFIX_STUDY_NOTIFICATION_WS}/notify?studyUuid=${encodeURIComponent(studyUuid)}` + tokenId && studyUuid + ? getUrlWithToken( + `${wsBase}${PREFIX_STUDY_NOTIFICATION_WS}/notify?studyUuid=${encodeURIComponent(studyUuid)}` + ) : undefined, [NotificationsUrlKeys.DIRECTORY_DELETE_STUDY]: - isAuthenticated && studyUuid - ? `${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=deleteElement&elementUuid=${encodeURIComponent( - studyUuid - )}` + tokenId && studyUuid + ? getUrlWithToken( + `${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=deleteElement&elementUuid=${encodeURIComponent( + studyUuid + )}` + ) : undefined, - [NotificationsUrlKeys.DIRECTORY]: isAuthenticated - ? `${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=directories` + [NotificationsUrlKeys.DIRECTORY]: tokenId + ? getUrlWithToken(`${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=directories`) : undefined, }), - [isAuthenticated, wsBase, studyUuid] + [tokenId, wsBase, studyUuid] ); }; diff --git a/src/services/utils.ts b/src/services/utils.ts index 1eaf3d08d4..3172aef27c 100644 --- a/src/services/utils.ts +++ b/src/services/utils.ts @@ -5,7 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { catchErrorHandler, fetchStudyMetadata, StudyMetadata } from '@gridsuite/commons-ui'; - +import { getUserToken } from '../redux/user-store'; export const FetchStatus = { SUCCEED: 'SUCCEED', FAILED: 'FAILED', @@ -90,6 +90,13 @@ export const getQueryParamsList = (params: string[] | number[] | null | undefine return ''; }; +export function getUrlWithToken(baseUrl: string) { + if (baseUrl.includes('?')) { + return baseUrl + '&access_token=' + getUserToken(); + } else { + return baseUrl + '?access_token=' + getUserToken(); + } +} export function fetchMapBoxToken() { console.info(`Fetching MapBoxToken...`); return fetchEnv().then((res) => res.mapBoxToken); From 120b1b142bf590a6183f5bc5dfeddf45df4a9448 Mon Sep 17 00:00:00 2001 From: Rehili Ghazwa Date: Tue, 21 Apr 2026 17:44:05 +0200 Subject: [PATCH 04/10] get url with token --- src/hooks/use-notifications-url-generator.ts | 15 +++++++++------ src/services/utils.ts | 7 +++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/hooks/use-notifications-url-generator.ts b/src/hooks/use-notifications-url-generator.ts index da171cf288..bdb89ba4a7 100644 --- a/src/hooks/use-notifications-url-generator.ts +++ b/src/hooks/use-notifications-url-generator.ts @@ -19,7 +19,7 @@ import { APP_NAME } from 'utils/config-params'; const useNotificationsUrlGenerator = (): Partial> => { // The websocket API doesn't allow relative urls const wsBase = getWsBase(); - const tokenId = useSelector((state: AppState) => state.user?.id_token); + const tokenId = useSelector((state: AppState) => state.userToken?.id_token ?? undefined); const studyUuid = useSelector((state: AppState) => state.studyUuid); // return a mapColumns with NOTIFICATIONS_URL_KEYS and undefined value if URL is not yet buildable (tokenId) @@ -30,16 +30,18 @@ const useNotificationsUrlGenerator = (): Partial ); -} +}); diff --git a/src/hooks/use-notifications-url-generator.ts b/src/hooks/use-notifications-url-generator.ts index bdb89ba4a7..e6b114960d 100644 --- a/src/hooks/use-notifications-url-generator.ts +++ b/src/hooks/use-notifications-url-generator.ts @@ -19,7 +19,7 @@ import { APP_NAME } from 'utils/config-params'; const useNotificationsUrlGenerator = (): Partial> => { // The websocket API doesn't allow relative urls const wsBase = getWsBase(); - const tokenId = useSelector((state: AppState) => state.userToken?.id_token ?? undefined); + const tokenId = useSelector((state: AppState) => state.user?.id_token ?? undefined); const studyUuid = useSelector((state: AppState) => state.studyUuid); // return a mapColumns with NOTIFICATIONS_URL_KEYS and undefined value if URL is not yet buildable (tokenId) diff --git a/src/redux/reducer.ts b/src/redux/reducer.ts index 3aaebe1b8f..1f042afa3e 100644 --- a/src/redux/reducer.ts +++ b/src/redux/reducer.ts @@ -477,7 +477,7 @@ const initialState: AppState = { isNetworkModificationTreeModelUpToDate: false, computedLanguage: getLocalStorageComputedLanguage(), user: null, - userToken: null, + tokenId: undefined, signInCallbackError: null, authenticationRouterError: null, showAuthenticationRouterLogin: false, @@ -1020,26 +1020,11 @@ export const reducer = createReducer(initialState, (builder) => { }); builder.addCase(USER, (state, action: UserAction) => { - const newUser = action.user; - // Token: always refreshed (new ref on every silent renew) - state.userToken = newUser - ? { - access_token: newUser.access_token, - id_token: newUser.id_token, - expires_at: newUser.expires_at, - } - : null; - // Identity: only update reference when sub actually changes. - // On silent renew, the profile is identical -> keep the same reference - // to avoid re-rendering every component that selects state.user. - const currentUser = state.user; - if (currentUser === null || newUser === null) { - state.user = newUser; + state.tokenId = action.user?.id_token ?? undefined; + if (state.user?.profile?.sub === action.user?.profile?.sub && state.user !== null) { return; } - if (currentUser.profile?.sub !== newUser.profile?.sub) { - state.user = newUser; - } + state.user = action.user; }); builder.addCase(ENABLE_DEVELOPER_MODE, (state, action: EnableDeveloperModeAction) => { diff --git a/src/redux/reducer.type.ts b/src/redux/reducer.type.ts index 5a2858b7ee..32bf7a9f5b 100644 --- a/src/redux/reducer.type.ts +++ b/src/redux/reducer.type.ts @@ -65,11 +65,6 @@ import { import { PARAM_COMPUTED_LANGUAGE, PARAM_LIMIT_REDUCTION, PARAM_USE_NAME, PARAMS_LOADED } from '../utils/config-params'; import { VOLTAGE_LEVEL_ID } from '../components/utils/field-constants'; -export type UserToken = { - access_token: string; - id_token: string; - expires_at: number; -}; // ——— Equipments ——— export enum EquipmentUpdateType { @@ -265,5 +260,5 @@ export interface AppState extends CommonStoreState, AppConfigState { calculationSelections: Record; highlightedModificationUuid: UUID | null; tableFilters: TableFiltersState; - userToken: UserToken | null; + tokenId: string | undefined; } diff --git a/src/redux/store.ts b/src/redux/store.ts index 3df2de4f12..232e79301b 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -8,7 +8,6 @@ import { configureStore } from '@reduxjs/toolkit'; import { reducer } from './reducer'; import { setCommonStore } from '@gridsuite/commons-ui'; -import { setUserStore } from './user-store'; import workspacesReducer from './slices/workspace-slice'; import { globalFiltersMiddleware } from './globalFiltersMiddleware'; @@ -35,7 +34,6 @@ export type RootState = ReturnType; export type AppDispatch = typeof store.dispatch; setCommonStore(store); -setUserStore(store); // to avoid to reset the state with HMR // https://redux.js.org/usage/configuring-your-store#hot-reloading diff --git a/src/redux/user-store.ts b/src/redux/user-store.ts deleted file mode 100644 index 86875dff73..0000000000 --- a/src/redux/user-store.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright © 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/. - */ - -/* - * This file is only to break the cyclic dependency in reducer.test + reducers + store - * TODO: remove when upgrading to next commons-ui version - */ -/* - at src/redux/store.ts:12:33 - at src/services/utils.ts:7:1 - at src/services/study/index.ts:8:1 - at src/components/utils/inputs/input-hooks.jsx:41:1 - at src/components/dialogs/commons/modification-dialog-content.jsx:19:1 - at src/components/dialogs/commons/modificationDialog.jsx:12:1 - at src/components/dialogs/network-modifications/generator/modification/regulating-terminal-modification-dialog.jsx:23:1 - at src/components/spreadsheet/utils/equipment-table-editors.jsx:25:1 - at src/components/spreadsheet/utils/config-tables.js:11:1 - at src/redux/reducer.ts:192:1 - at src/redux/reducer.test.ts:10:1 - */ - -import { User } from 'oidc-client'; - -type UserStoreState = { - user: User | null; - userToken: { - access_token: string; - id_token: string; - expires_at: number; - } | null; -}; - -interface UserStore { - getState(): UserStoreState; -} - -let userStore: UserStore | undefined; - -export function setUserStore(store: UserStore): void { - userStore = store; -} - -//TODO use the one from commons-ui instead when exported in next version -export function getUserToken() { - return userStore?.getState().userToken?.id_token ?? undefined; -} From 0377001900b7762b8334b316ee6e6b8794cd4089 Mon Sep 17 00:00:00 2001 From: Rehili Ghazwa Date: Tue, 28 Apr 2026 16:52:08 +0200 Subject: [PATCH 06/10] remove token from useNotificationsUrlGenerator --- src/components/app-top-bar.jsx | 12 +++--- src/components/app-wrapper.jsx | 2 +- src/components/app.jsx | 24 +++++++---- src/components/study-container.jsx | 6 +-- src/hooks/use-notifications-url-generator.ts | 45 ++++++-------------- src/redux/reducer.ts | 5 --- src/redux/reducer.type.ts | 1 - src/services/utils.ts | 7 --- 8 files changed, 39 insertions(+), 63 deletions(-) diff --git a/src/components/app-top-bar.jsx b/src/components/app-top-bar.jsx index fcf1e59edc..791e32247e 100644 --- a/src/components/app-top-bar.jsx +++ b/src/components/app-top-bar.jsx @@ -51,7 +51,7 @@ const styles = { }, }; -const AppTopBar = ({ user, userManager }) => { +const AppTopBar = ({ userProfile, userManager }) => { const dispatch = useDispatch(); const theme = useSelector((state) => state[PARAM_THEME]); const studyUuid = useSelector((state) => state.studyUuid); @@ -66,12 +66,12 @@ const AppTopBar = ({ user, userManager }) => { const [isDeveloperMode, handleChangeDeveloperMode] = useParameterState(PARAM_DEVELOPER_MODE); useEffect(() => { - if (user !== null) { + if (userProfile !== null) { fetchAppsMetadata().then((res) => { setAppsAndUrls(res); }); } - }, [user]); + }, [userProfile]); return ( { appColor="#0CA789" appLogo={theme === LIGHT_THEME ? : } onLogoutClick={() => logout(dispatch, userManager.instance)} - user={user} + userProfile={userProfile} appsAndUrls={appsAndUrls} onThemeClick={handleChangeTheme} appVersion={AppPackage.version} @@ -95,7 +95,7 @@ const AppTopBar = ({ user, userManager }) => { language={languageLocal} dense > - {user && studyUuid && currentRootNetworkUuid && ( + {userProfile && studyUuid && currentRootNetworkUuid && ( @@ -121,7 +121,7 @@ const AppTopBar = ({ user, userManager }) => { }; AppTopBar.propTypes = { - user: PropTypes.object, + userProfile: PropTypes.object, userManager: PropTypes.object.isRequired, }; diff --git a/src/components/app-wrapper.jsx b/src/components/app-wrapper.jsx index 7a4c31b376..e08df43003 100644 --- a/src/components/app-wrapper.jsx +++ b/src/components/app-wrapper.jsx @@ -6,7 +6,7 @@ */ import { useMemo } from 'react'; -import App from './app'; +import App from './app.jsx'; import { createTheme, CssBaseline, diff --git a/src/components/app.jsx b/src/components/app.jsx index 0aa451bd70..03788d15ad 100644 --- a/src/components/app.jsx +++ b/src/components/app.jsx @@ -6,7 +6,7 @@ */ import { useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { retrieveOptionalServices } from './utils/optional-services'; import { Navigate, Route, Routes, useLocation, useMatch, useNavigate } from 'react-router'; import { @@ -77,6 +77,7 @@ import { useGlobalFilterOptions } from './results/common/global-filter/use-globa import { updateComputationColumnFilters, updateComputationGlobalFilters } from './results/common/utils.ts'; import { isEditingGlobalFilter } from '../utils/editing-global-filter-sync.ts'; import { cleanupStaleStudyData } from '../redux/session-storage/local-storage'; +import { Button } from '@mui/material'; const noUserManager = { instance: null, error: null }; @@ -89,7 +90,7 @@ const App = () => { .catch(() => cleanupStaleStudyData()); }, []); - const user = useSelector((state) => state.user); + const userProfile = useSelector((state) => state.user?.profile ?? null, shallowEqual); const studyUuid = useSelector((state) => state.studyUuid); const signInCallbackError = useSelector((state) => state.signInCallbackError); const authenticationRouterError = useSelector((state) => state.authenticationRouterError); @@ -343,7 +344,7 @@ const App = () => { }, [initialMatchSilentRenewCallbackUrl, dispatch, initialMatchSigninCallbackUrl]); useEffect(() => { - if (user !== null && studyUuid !== null) { + if (userProfile !== null && studyUuid !== null) { const fetchNetworkVisualizationParametersPromise = getNetworkVisualizationParameters(studyUuid).then( (params) => updateNetworkVisualizationsParams(params) ); @@ -386,8 +387,15 @@ const App = () => { }) .catch((error) => snackWithFallback(snackError, error, { headerId: 'paramsRetrievingError' })); } - }, [user, studyUuid, dispatch, updateParams, snackError, updateNetworkVisualizationsParams, resetTableDefinitions]); - + }, [ + userProfile, + studyUuid, + dispatch, + updateParams, + snackError, + updateNetworkVisualizationsParams, + resetTableDefinitions, + ]); return (
{ flexDirection: 'column', }} > - - + +
{ overflow: isStudyPane ? 'hidden' : 'auto', }} > - {user !== null ? ( + {userProfile !== null ? ( } /> ); -}); +} diff --git a/src/hooks/use-notifications-url-generator.ts b/src/hooks/use-notifications-url-generator.ts index e6b114960d..fe84e1e2eb 100644 --- a/src/hooks/use-notifications-url-generator.ts +++ b/src/hooks/use-notifications-url-generator.ts @@ -13,51 +13,32 @@ import { import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { type AppState } from 'redux/reducer.type'; -import { getUrlWithToken, getWsBase } from 'services/utils'; +import { getWsBase } from 'services/utils'; import { APP_NAME } from 'utils/config-params'; const useNotificationsUrlGenerator = (): Partial> => { // The websocket API doesn't allow relative urls const wsBase = getWsBase(); - const tokenId = useSelector((state: AppState) => state.user?.id_token ?? undefined); const studyUuid = useSelector((state: AppState) => state.studyUuid); - // return a mapColumns with NOTIFICATIONS_URL_KEYS and undefined value if URL is not yet buildable (tokenId) // it will be used to register listeners as soon as possible. return useMemo( () => ({ - [NotificationsUrlKeys.CONFIG]: tokenId - ? getUrlWithToken( - `${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/notify?${new URLSearchParams({ - appName: APP_NAME, - })}`, - tokenId - ) - : undefined, - [NotificationsUrlKeys.GLOBAL_CONFIG]: tokenId - ? getUrlWithToken(`${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/global`, tokenId) + [NotificationsUrlKeys.CONFIG]: `${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/notify?${new URLSearchParams({ + appName: APP_NAME, + })}`, + [NotificationsUrlKeys.GLOBAL_CONFIG]: `${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/global`, + [NotificationsUrlKeys.STUDY]: studyUuid + ? `${wsBase}${PREFIX_STUDY_NOTIFICATION_WS}/notify?studyUuid=${encodeURIComponent(studyUuid)}` : undefined, - [NotificationsUrlKeys.STUDY]: - tokenId && studyUuid - ? getUrlWithToken( - `${wsBase}${PREFIX_STUDY_NOTIFICATION_WS}/notify?studyUuid=${encodeURIComponent(studyUuid)}`, - tokenId - ) - : undefined, - [NotificationsUrlKeys.DIRECTORY_DELETE_STUDY]: - tokenId && studyUuid - ? getUrlWithToken( - `${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=deleteElement&elementUuid=${encodeURIComponent( - studyUuid - )}`, - tokenId - ) - : undefined, - [NotificationsUrlKeys.DIRECTORY]: tokenId - ? getUrlWithToken(`${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=directories`, tokenId) + [NotificationsUrlKeys.DIRECTORY_DELETE_STUDY]: studyUuid + ? `${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=deleteElement&elementUuid=${encodeURIComponent( + studyUuid + )}` : undefined, + [NotificationsUrlKeys.DIRECTORY]: `${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=directories`, }), - [tokenId, wsBase, studyUuid] + [wsBase, studyUuid] ); }; diff --git a/src/redux/reducer.ts b/src/redux/reducer.ts index 1f042afa3e..1a41adbf68 100644 --- a/src/redux/reducer.ts +++ b/src/redux/reducer.ts @@ -477,7 +477,6 @@ const initialState: AppState = { isNetworkModificationTreeModelUpToDate: false, computedLanguage: getLocalStorageComputedLanguage(), user: null, - tokenId: undefined, signInCallbackError: null, authenticationRouterError: null, showAuthenticationRouterLogin: false, @@ -1020,10 +1019,6 @@ export const reducer = createReducer(initialState, (builder) => { }); builder.addCase(USER, (state, action: UserAction) => { - state.tokenId = action.user?.id_token ?? undefined; - if (state.user?.profile?.sub === action.user?.profile?.sub && state.user !== null) { - return; - } state.user = action.user; }); diff --git a/src/redux/reducer.type.ts b/src/redux/reducer.type.ts index 32bf7a9f5b..9e6175e03e 100644 --- a/src/redux/reducer.type.ts +++ b/src/redux/reducer.type.ts @@ -260,5 +260,4 @@ export interface AppState extends CommonStoreState, AppConfigState { calculationSelections: Record; highlightedModificationUuid: UUID | null; tableFilters: TableFiltersState; - tokenId: string | undefined; } diff --git a/src/services/utils.ts b/src/services/utils.ts index 853d5cfb92..06ce711401 100644 --- a/src/services/utils.ts +++ b/src/services/utils.ts @@ -89,13 +89,6 @@ export const getQueryParamsList = (params: string[] | number[] | null | undefine return ''; }; -export function getUrlWithToken(baseUrl: string, tokenId: string) { - if (baseUrl.includes('?')) { - return baseUrl + '&access_token=' + tokenId; - } else { - return baseUrl + '?access_token=' + tokenId; - } -} export function fetchMapBoxToken() { console.info(`Fetching MapBoxToken...`); return fetchEnv().then((res) => res.mapBoxToken); From 9f67a8b76734382e95c294c0d7a43c7524d063cf Mon Sep 17 00:00:00 2001 From: Rehili Ghazwa Date: Wed, 29 Apr 2026 16:43:32 +0200 Subject: [PATCH 07/10] remove unused import --- src/components/app-wrapper.jsx | 3 +- src/components/app.jsx | 1 - src/hooks/use-notifications-url-generator.ts | 45 ++++++++++++++------ src/services/utils.ts | 8 ++++ 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/components/app-wrapper.jsx b/src/components/app-wrapper.jsx index e08df43003..6a1ffd751f 100644 --- a/src/components/app-wrapper.jsx +++ b/src/components/app-wrapper.jsx @@ -487,6 +487,7 @@ const AppWrapperWithRedux = () => { const theme = useSelector((state) => state[PARAM_THEME]); const themeCompiled = useMemo(() => getMuiTheme(theme, computedLanguage), [computedLanguage, theme]); const baseVoltages = useSelector((state) => state.baseVoltages); + const accessToken = useSelector((state) => state.user?.access_token); const rootCssVars = useMemo(() => { const themeVars = theme === LIGHT_THEME ? lightThemeCssVars : darkThemeCssVars; @@ -507,7 +508,7 @@ const AppWrapperWithRedux = () => { - + diff --git a/src/components/app.jsx b/src/components/app.jsx index 03788d15ad..26d8b3d9dc 100644 --- a/src/components/app.jsx +++ b/src/components/app.jsx @@ -77,7 +77,6 @@ import { useGlobalFilterOptions } from './results/common/global-filter/use-globa import { updateComputationColumnFilters, updateComputationGlobalFilters } from './results/common/utils.ts'; import { isEditingGlobalFilter } from '../utils/editing-global-filter-sync.ts'; import { cleanupStaleStudyData } from '../redux/session-storage/local-storage'; -import { Button } from '@mui/material'; const noUserManager = { instance: null, error: null }; diff --git a/src/hooks/use-notifications-url-generator.ts b/src/hooks/use-notifications-url-generator.ts index fe84e1e2eb..cbb6feae1a 100644 --- a/src/hooks/use-notifications-url-generator.ts +++ b/src/hooks/use-notifications-url-generator.ts @@ -13,32 +13,51 @@ import { import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { type AppState } from 'redux/reducer.type'; -import { getWsBase } from 'services/utils'; +import { getUrlWithToken, getWsBase } from 'services/utils'; import { APP_NAME } from 'utils/config-params'; const useNotificationsUrlGenerator = (): Partial> => { // The websocket API doesn't allow relative urls const wsBase = getWsBase(); const studyUuid = useSelector((state: AppState) => state.studyUuid); + const tokenId = useSelector((state: AppState) => state.user?.id_token); + // return a mapColumns with NOTIFICATIONS_URL_KEYS and undefined value if URL is not yet buildable (tokenId) // it will be used to register listeners as soon as possible. return useMemo( () => ({ - [NotificationsUrlKeys.CONFIG]: `${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/notify?${new URLSearchParams({ - appName: APP_NAME, - })}`, - [NotificationsUrlKeys.GLOBAL_CONFIG]: `${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/global`, - [NotificationsUrlKeys.STUDY]: studyUuid - ? `${wsBase}${PREFIX_STUDY_NOTIFICATION_WS}/notify?studyUuid=${encodeURIComponent(studyUuid)}` + [NotificationsUrlKeys.CONFIG]: tokenId + ? getUrlWithToken( + `${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/notify?${new URLSearchParams({ + appName: APP_NAME, + })}`, + tokenId + ) + : undefined, + [NotificationsUrlKeys.GLOBAL_CONFIG]: tokenId + ? getUrlWithToken(`${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/global`, tokenId) : undefined, - [NotificationsUrlKeys.DIRECTORY_DELETE_STUDY]: studyUuid - ? `${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=deleteElement&elementUuid=${encodeURIComponent( - studyUuid - )}` + [NotificationsUrlKeys.STUDY]: + tokenId && studyUuid + ? getUrlWithToken( + `${wsBase}${PREFIX_STUDY_NOTIFICATION_WS}/notify?studyUuid=${encodeURIComponent(studyUuid)}`, + tokenId + ) + : undefined, + [NotificationsUrlKeys.DIRECTORY_DELETE_STUDY]: + tokenId && studyUuid + ? getUrlWithToken( + `${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=deleteElement&elementUuid=${encodeURIComponent( + studyUuid + )}`, + tokenId + ) + : undefined, + [NotificationsUrlKeys.DIRECTORY]: tokenId + ? getUrlWithToken(`${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=directories`, tokenId) : undefined, - [NotificationsUrlKeys.DIRECTORY]: `${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=directories`, }), - [wsBase, studyUuid] + [tokenId, wsBase, studyUuid] ); }; diff --git a/src/services/utils.ts b/src/services/utils.ts index 06ce711401..8fb3a639f7 100644 --- a/src/services/utils.ts +++ b/src/services/utils.ts @@ -89,6 +89,14 @@ export const getQueryParamsList = (params: string[] | number[] | null | undefine return ''; }; +export function getUrlWithToken(baseUrl: string, tokenId: string) { + if (baseUrl.includes('?')) { + return baseUrl + '&access_token=' + tokenId; + } else { + return baseUrl + '?access_token=' + tokenId; + } +} + export function fetchMapBoxToken() { console.info(`Fetching MapBoxToken...`); return fetchEnv().then((res) => res.mapBoxToken); From a6c3734229bc9476a2cc979fc21ec130a2dc0ce8 Mon Sep 17 00:00:00 2001 From: Rehili Ghazwa Date: Thu, 30 Apr 2026 11:03:38 +0200 Subject: [PATCH 08/10] remove tokenId from NOTIFICATIONS_URL_KEYS --- src/components/app-wrapper.jsx | 3 +- src/hooks/use-notifications-url-generator.ts | 44 ++++++-------------- 2 files changed, 13 insertions(+), 34 deletions(-) diff --git a/src/components/app-wrapper.jsx b/src/components/app-wrapper.jsx index 6a1ffd751f..e08df43003 100644 --- a/src/components/app-wrapper.jsx +++ b/src/components/app-wrapper.jsx @@ -487,7 +487,6 @@ const AppWrapperWithRedux = () => { const theme = useSelector((state) => state[PARAM_THEME]); const themeCompiled = useMemo(() => getMuiTheme(theme, computedLanguage), [computedLanguage, theme]); const baseVoltages = useSelector((state) => state.baseVoltages); - const accessToken = useSelector((state) => state.user?.access_token); const rootCssVars = useMemo(() => { const themeVars = theme === LIGHT_THEME ? lightThemeCssVars : darkThemeCssVars; @@ -508,7 +507,7 @@ const AppWrapperWithRedux = () => { - + diff --git a/src/hooks/use-notifications-url-generator.ts b/src/hooks/use-notifications-url-generator.ts index cbb6feae1a..f2fe99a026 100644 --- a/src/hooks/use-notifications-url-generator.ts +++ b/src/hooks/use-notifications-url-generator.ts @@ -13,51 +13,31 @@ import { import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { type AppState } from 'redux/reducer.type'; -import { getUrlWithToken, getWsBase } from 'services/utils'; +import { getWsBase } from 'services/utils'; import { APP_NAME } from 'utils/config-params'; const useNotificationsUrlGenerator = (): Partial> => { // The websocket API doesn't allow relative urls const wsBase = getWsBase(); const studyUuid = useSelector((state: AppState) => state.studyUuid); - const tokenId = useSelector((state: AppState) => state.user?.id_token); - // return a mapColumns with NOTIFICATIONS_URL_KEYS and undefined value if URL is not yet buildable (tokenId) + // return a mapColumns with NOTIFICATIONS_URL_KEYS and undefined value if URL is not yet buildable (studyUuid) // it will be used to register listeners as soon as possible. return useMemo( () => ({ - [NotificationsUrlKeys.CONFIG]: tokenId - ? getUrlWithToken( - `${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/notify?${new URLSearchParams({ - appName: APP_NAME, - })}`, - tokenId - ) + [NotificationsUrlKeys.CONFIG]: `${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/notify?${new URLSearchParams({ appName: APP_NAME })}`, + [NotificationsUrlKeys.GLOBAL_CONFIG]: `${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/global`, + [NotificationsUrlKeys.STUDY]: studyUuid + ? `${wsBase}${PREFIX_STUDY_NOTIFICATION_WS}/notify?studyUuid=${encodeURIComponent(studyUuid)}` : undefined, - [NotificationsUrlKeys.GLOBAL_CONFIG]: tokenId - ? getUrlWithToken(`${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/global`, tokenId) - : undefined, - [NotificationsUrlKeys.STUDY]: - tokenId && studyUuid - ? getUrlWithToken( - `${wsBase}${PREFIX_STUDY_NOTIFICATION_WS}/notify?studyUuid=${encodeURIComponent(studyUuid)}`, - tokenId - ) - : undefined, - [NotificationsUrlKeys.DIRECTORY_DELETE_STUDY]: - tokenId && studyUuid - ? getUrlWithToken( - `${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=deleteElement&elementUuid=${encodeURIComponent( - studyUuid - )}`, - tokenId - ) - : undefined, - [NotificationsUrlKeys.DIRECTORY]: tokenId - ? getUrlWithToken(`${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=directories`, tokenId) + [NotificationsUrlKeys.DIRECTORY_DELETE_STUDY]: studyUuid + ? `${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=deleteElement&elementUuid=${encodeURIComponent( + studyUuid + )}` : undefined, + [NotificationsUrlKeys.DIRECTORY]: `${wsBase}${PREFIX_DIRECTORY_NOTIFICATION_WS}/notify?updateType=directories`, }), - [tokenId, wsBase, studyUuid] + [wsBase, studyUuid] ); }; From 0812528dd3b3dc4c16fff3c974182f81d2b62b20 Mon Sep 17 00:00:00 2001 From: Rehili Ghazwa Date: Wed, 6 May 2026 13:39:12 +0200 Subject: [PATCH 09/10] remove unused function --- src/services/utils.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/services/utils.ts b/src/services/utils.ts index 8fb3a639f7..06ce711401 100644 --- a/src/services/utils.ts +++ b/src/services/utils.ts @@ -89,14 +89,6 @@ export const getQueryParamsList = (params: string[] | number[] | null | undefine return ''; }; -export function getUrlWithToken(baseUrl: string, tokenId: string) { - if (baseUrl.includes('?')) { - return baseUrl + '&access_token=' + tokenId; - } else { - return baseUrl + '?access_token=' + tokenId; - } -} - export function fetchMapBoxToken() { console.info(`Fetching MapBoxToken...`); return fetchEnv().then((res) => res.mapBoxToken); From 87705a463837d3e74bc69e617c76ac3935660fd7 Mon Sep 17 00:00:00 2001 From: Rehili Ghazwa Date: Wed, 6 May 2026 16:44:18 +0200 Subject: [PATCH 10/10] revert import --- src/components/app-wrapper.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/app-wrapper.jsx b/src/components/app-wrapper.jsx index e08df43003..7a4c31b376 100644 --- a/src/components/app-wrapper.jsx +++ b/src/components/app-wrapper.jsx @@ -6,7 +6,7 @@ */ import { useMemo } from 'react'; -import App from './app.jsx'; +import App from './app'; import { createTheme, CssBaseline,