Skip to content

Commit f08ccc2

Browse files
committed
ARTESCA-13969 // Migration to React 18
ARTESCA-13969 // Migration to React 18 refactor: update NotificationCenter styles and improve test assertions refactor: migrate to React 18's createRoot for rendering refactor: implement external store for WebFingers context and update related hooks refactor: remove console logs and simplify state update logic in WebFingersStore refactor: update dependencies to React 18 and related packages
1 parent 14a55a3 commit f08ccc2

9 files changed

+15015
-541
lines changed

shell-ui/package-lock.json

+14,579-236
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

shell-ui/package.json

+12-12
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@
2222
"@rspack/core": "^0.7.5",
2323
"@testing-library/dom": "^10.4.0",
2424
"@testing-library/jest-dom": "^5.11.9",
25-
"@testing-library/react": "^11.2.5",
26-
"@testing-library/react-hooks": "^5.1.1",
25+
"@testing-library/react": "^15.0.7",
26+
"@testing-library/react-hooks": "^8.0.1",
2727
"@testing-library/user-event": "^13.0.10",
28-
"@types/react": "^17.0.39",
29-
"@types/react-dom": "^17.0.13",
28+
"@types/react": "^18.3.12",
29+
"@types/react-dom": "^18.3.1",
3030
"@types/react-router": "^5.1.20",
31-
"@types/react-router-dom": "^5.2.0",
32-
"@types/styled-components": "^5.1.26",
31+
"@types/react-test-renderer": "^18.3.0",
32+
"@types/styled-components": "^5.1.34",
3333
"babel-jest": "^26.6.3",
3434
"babel-loader": "^8.2.2",
3535
"fs-extra": "^10.0.0",
@@ -39,24 +39,24 @@
3939
"jest-preview": "^0.3.1",
4040
"msw": "0.36.8",
4141
"node-fetch": "^2.6.1",
42-
"react-test-renderer": "^17.0.2",
42+
"react-test-renderer": "^18.3.1",
4343
"ts-node": "^10.9.2"
4444
},
4545
"dependencies": {
46-
"@scality/core-ui": "0.151.0",
47-
"@scality/module-federation": "^1.3.4",
46+
"@scality/core-ui": "git+https://github.com/scality/core-ui#bf0c36da657737f47dabe310bb1a20c136877970",
47+
"@scality/module-federation": "git+https://github.com/scality/module-federation#129815715e9fc7cb7cbe4417f536679183c49725",
4848
"downshift": "^8.0.0",
4949
"jest-environment-jsdom": "^29.7.0",
5050
"oidc-client-ts": "^3.0.1",
5151
"oidc-react": "^3.2.2",
52-
"react": "^17.0.2",
53-
"react-dom": "^17.0.2",
52+
"react": "^18.3.1",
53+
"react-dom": "^18.3.1",
5454
"react-error-boundary": "^4.0.2",
5555
"react-intl": "^5.15.3",
5656
"react-query": "^3.34.0",
5757
"react-router": "5.2.0",
5858
"react-router-dom": "5.2.0",
59-
"styled-components": "^5.2.1",
59+
"styled-components": "^5.3.11",
6060
"typescript": "^5.6.3"
6161
}
6262
}

shell-ui/src/auth/useFirstTimeLogin.spec.tsx

+5-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useFirstTimeLogin } from './FirstTimeLoginProvider';
33
import { wrapper } from '../navbar/index.spec';
44
import { configurationHandlers } from '../FederatedApp.spec';
55
import { setupServer } from 'msw/node';
6+
import { waitFor } from '@testing-library/react';
67

78
const server = setupServer(...configurationHandlers);
89

@@ -28,14 +29,11 @@ describe('useFirstTimeLogin hook', () => {
2829

2930
it('should return firstTimeLogin as true if the user is logging in for the first time', async () => {
3031
//S
31-
const { result, waitForNextUpdate } = renderHook(
32-
() => useFirstTimeLogin(),
33-
{ wrapper },
34-
);
35-
//E
36-
await waitForNextUpdate();
32+
const { result } = renderHook(() => useFirstTimeLogin(), { wrapper });
3733
//V
38-
expect(result.current.firstTimeLogin).toEqual(true);
34+
await waitFor(() => {
35+
expect(result.current.firstTimeLogin).toEqual(true);
36+
});
3937
});
4038

4139
it('should return firstTimeLogin as false if the user is NOT logging in for the first time', async () => {

shell-ui/src/index.tsx

+8-10
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import React from 'react';
2-
import ReactDOM from 'react-dom';
3-
import App, { ShellTypes } from './FederatedApp';
4-
import { NotificationCenterContextType } from './NotificationCenterProvider';
5-
import { History } from 'history';
6-
import {
7-
BuildtimeWebFinger,
8-
RuntimeWebFinger,
9-
} from './initFederation/ConfigurationProviders';
1+
import { createRoot } from 'react-dom/client';
2+
import App from './FederatedApp';
103

11-
ReactDOM.render(<App />, document.getElementById('app'));
4+
const rootElement = document.getElementById('app');
5+
6+
if (rootElement) {
7+
const root = createRoot(rootElement);
8+
root.render(<App />);
9+
}

shell-ui/src/initFederation/ConfigurationProviders.tsx

+84-25
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,12 @@ import { ErrorPage500 } from '@scality/core-ui/dist/components/error-pages/Error
22
import { IconName } from '@scality/core-ui/dist/components/icon/Icon.component';
33
import { Loader } from '@scality/core-ui/dist/components/loader/Loader.component';
44
import { SolutionUI } from '@scality/module-federation';
5-
import React, { createContext, useContext } from 'react';
5+
import React, { useMemo, useSyncExternalStore } from 'react';
66
import { useQueries, UseQueryResult } from 'react-query';
77
import { useShellConfig } from './ShellConfigProvider';
88
import { useShellHistory } from './ShellHistoryProvider';
99
import { useDeployedApps, useDeployedAppsRetriever } from './UIListProvider';
1010

11-
if (!window.shellContexts) {
12-
// @ts-expect-error - FIXME when you are working on it
13-
window.shellContexts = {};
14-
}
15-
16-
if (!window.shellContexts.WebFingersContext) {
17-
window.shellContexts.WebFingersContext = createContext<
18-
| null
19-
| UseQueryResult<
20-
BuildtimeWebFinger | RuntimeWebFinger<Record<string, unknown>>,
21-
unknown
22-
>[]
23-
>(null);
24-
}
25-
2611
export type OAuth2ProxyConfig = {
2712
kind: 'OAuth2Proxy'; //todo : add other entries
2813
};
@@ -85,10 +70,8 @@ export function useConfigRetriever(): {
8570
name: string;
8671
}) => (T extends 'build' ? BuildtimeWebFinger : RuntimeWebFinger<T>) | null;
8772
} {
73+
const { state: webFingerContextValue } = useWebFingersStore();
8874
const { retrieveDeployedApps } = useDeployedAppsRetriever();
89-
const webFingerContextValue = useContext(
90-
window.shellContexts.WebFingersContext,
91-
);
9275

9376
if (!webFingerContextValue) {
9477
throw new Error(
@@ -142,15 +125,18 @@ export function useConfig<T extends 'build' | Record<string, unknown>>({
142125
configType: T extends 'build' ? 'build' : 'run';
143126
name: string;
144127
}): null | T extends 'build' ? BuildtimeWebFinger : RuntimeWebFinger<T> {
128+
// Utiliser le nouveau hook useWebFingersStore
129+
const { state: webFingerContextValue } = useWebFingersStore();
130+
131+
// Utiliser le retrieveConfiguration du hook useConfigRetriever
145132
const { retrieveConfiguration } = useConfigRetriever();
146-
const webFingerContextValue = useContext(
147-
window.shellContexts.WebFingersContext,
148-
);
149133

150-
if (!webFingerContextValue) {
134+
// Vérifier que le contexte est disponible
135+
if (!webFingerContextValue || webFingerContextValue.length === 0) {
151136
throw new Error("Can't use useConfig outside of ConfigurationProvider");
152137
}
153138

139+
// Récupérer et retourner la configuration
154140
return retrieveConfiguration({
155141
configType,
156142
name,
@@ -179,6 +165,72 @@ export type NonFederatedView = {
179165
icon?: IconName;
180166
};
181167
export type ViewDefinition = FederatedView | NonFederatedView;
168+
169+
// External store implementation
170+
class WebFingersStore {
171+
private listeners: Set<() => void> = new Set();
172+
private _state: UseQueryResult<
173+
BuildtimeWebFinger | RuntimeWebFinger<Record<string, unknown>>,
174+
unknown
175+
>[] = [];
176+
177+
subscribe = (listener: () => void) => {
178+
this.listeners.add(listener);
179+
return () => {
180+
this.listeners.delete(listener);
181+
};
182+
};
183+
184+
getState = () => {
185+
return this._state;
186+
};
187+
188+
private isStateEqual(
189+
currentState: UseQueryResult<
190+
BuildtimeWebFinger | RuntimeWebFinger<Record<string, unknown>>,
191+
unknown
192+
>[],
193+
newState: UseQueryResult<
194+
BuildtimeWebFinger | RuntimeWebFinger<Record<string, unknown>>,
195+
unknown
196+
>[],
197+
) {
198+
return (
199+
currentState.length === newState.length &&
200+
currentState.every(
201+
(item, index) =>
202+
JSON.stringify(item) === JSON.stringify(newState[index]),
203+
)
204+
);
205+
}
206+
207+
updateState = (
208+
newState: UseQueryResult<
209+
BuildtimeWebFinger | RuntimeWebFinger<Record<string, unknown>>,
210+
unknown
211+
>[],
212+
) => {
213+
if (!this.isStateEqual(this._state, newState)) {
214+
this._state = newState;
215+
this.listeners.forEach((listener) => listener());
216+
}
217+
};
218+
}
219+
220+
const webFingersStore = new WebFingersStore();
221+
222+
export function useWebFingersStore() {
223+
const state = useSyncExternalStore(
224+
webFingersStore.subscribe,
225+
webFingersStore.getState,
226+
);
227+
228+
return {
229+
state,
230+
updateWebFingersState: webFingersStore.updateState,
231+
};
232+
}
233+
182234
export function useDiscoveredViews(): ViewDefinition[] {
183235
const { retrieveConfiguration } = useConfigRetriever();
184236
const { retrieveDeployedApps } = useDeployedAppsRetriever();
@@ -286,6 +338,7 @@ export const ConfigurationProvider = ({
286338
}: {
287339
children: React.ReactNode;
288340
}) => {
341+
const { updateWebFingersState } = useWebFingersStore();
289342
const deployedUIs = useDeployedApps();
290343
const results = useQueries(
291344
deployedUIs.flatMap((ui) => [
@@ -323,6 +376,11 @@ export const ConfigurationProvider = ({
323376
},
324377
]),
325378
);
379+
380+
useMemo(() => {
381+
updateWebFingersState(results);
382+
}, [results]);
383+
326384
const statuses = Array.from(new Set(results.map((result) => result.status)));
327385
const globalStatus = statuses.includes('error')
328386
? 'error'
@@ -333,13 +391,14 @@ export const ConfigurationProvider = ({
333391
: statuses.includes('idle') && statuses.includes('success')
334392
? 'loading'
335393
: 'success';
394+
336395
return (
337-
<window.shellContexts.WebFingersContext.Provider value={results}>
396+
<>
338397
{(globalStatus === 'loading' || globalStatus === 'idle') && (
339398
<Loader size="massive" centered={true} aria-label="loading" />
340399
)}
341400
{globalStatus === 'error' && <ErrorPage500 data-cy="sc-error-page500" />}
342401
{globalStatus === 'success' && children}
343-
</window.shellContexts.WebFingersContext.Provider>
402+
</>
344403
);
345404
};

shell-ui/src/navbar/__TESTS__/testMultipleHooks.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export type RenderAdditionalHook = <THookResult>(
2424
};
2525

2626
export function prepareRenderMultipleHooks(options: {
27-
wrapper: FunctionComponent<PropsWithChildren<Record<string, never>>>;
27+
wrapper: FunctionComponent<React.PropsWithChildren<PropsWithChildren<Record<string, never>>>>;
2828
}): {
2929
renderAdditionalHook: RenderAdditionalHook;
3030
waitForWrapperToBeReady: () => Promise<void>;

0 commit comments

Comments
 (0)