diff --git a/package-lock.json b/package-lock.json index ac57a02..0a33a75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "react-window": "^1.8.5", "reconnecting-websocket": "^4.4.0", "redux": "^4.0.5", + "type-fest": "^4.11.1", "typeface-roboto": "^1.0.0", "typescript": "^5.1.3", "yup": "^1.2.0" @@ -2375,6 +2376,17 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/js": { "version": "8.38.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.38.0.tgz", @@ -7886,6 +7898,17 @@ "node": ">=8" } }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/espree": { "version": "9.5.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", @@ -16766,11 +16789,11 @@ } }, "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.11.1.tgz", + "integrity": "sha512-MFMf6VkEVZAETidGGSYW2B1MjXbGX+sWIywn2QPEaJ3j08V+MwVRHMXtf2noB8ENJaD0LIun9wh5Z6OPNf1QzQ==", "engines": { - "node": ">=10" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/package.json b/package.json index 83ce1ae..09b0d43 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "private": true, "bugs": "https://github.com/gridsuite/gridadmin-app/issues", "repository": { - "type": "TODO", + "type": "git", "url": "https://github.com/gridsuite/gridadmin-app" }, "engines": { @@ -41,6 +41,7 @@ "react-window": "^1.8.5", "reconnecting-websocket": "^4.4.0", "redux": "^4.0.5", + "type-fest": "^4.11.1", "typeface-roboto": "^1.0.0", "typescript": "^5.1.3", "yup": "^1.2.0" diff --git a/src/components/app-top-bar.tsx b/src/components/app-top-bar.tsx index 5643785..ae49f77 100644 --- a/src/components/app-top-bar.tsx +++ b/src/components/app-top-bar.tsx @@ -1,37 +1,35 @@ -/** +/* * Copyright (c) 2021, 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 React, { FunctionComponent, useEffect, useState } from 'react'; +import { FunctionComponent, useCallback, useEffect, useState } from 'react'; import { LIGHT_THEME, logout, TopBar } from '@gridsuite/commons-ui'; import Parameters, { useParameterState } from './parameters'; import { APP_NAME, PARAM_LANGUAGE, PARAM_THEME } from '../utils/config-params'; import { useDispatch, useSelector } from 'react-redux'; -import { AppsMetadataSrv, StudySrv } from '../services'; +import { AppsMetadataSrv, MetadataJson, StudySrv } from '../services'; import { useNavigate } from 'react-router-dom'; import { ReactComponent as PowsyblLogo } from '../images/powsybl_logo.svg'; import AppPackage from '../../package.json'; import { AppState } from '../redux/reducer'; -import { UserManager } from 'oidc-client'; export type AppTopBarProps = { user?: AppState['user']; userManager: { - instance: UserManager | null; + instance: unknown | null; error: string | null; }; }; + const AppTopBar: FunctionComponent = (props) => { const navigate = useNavigate(); const dispatch = useDispatch(); - const [appsAndUrls, setAppsAndUrls] = useState< - Awaited> - >([]); + const [appsAndUrls, setAppsAndUrls] = useState([]); const theme = useSelector((state: AppState) => state[PARAM_THEME]); @@ -41,6 +39,8 @@ const AppTopBar: FunctionComponent = (props) => { useParameterState(PARAM_LANGUAGE); const [showParameters, setShowParameters] = useState(false); + const displayParameters = useCallback(() => setShowParameters(true), []); + const hideParameters = useCallback(() => setShowParameters(false), []); useEffect(() => { if (props.user !== null) { @@ -64,7 +64,7 @@ const AppTopBar: FunctionComponent = (props) => { } appVersion={AppPackage.version} appLicense={AppPackage.license} - onParametersClick={() => setShowParameters(true)} + onParametersClick={displayParameters} onLogoutClick={() => logout(dispatch, props.userManager.instance) } @@ -84,7 +84,7 @@ const AppTopBar: FunctionComponent = (props) => { /> setShowParameters(false)} + hideParameters={hideParameters} /> ); diff --git a/src/components/app-wrapper.tsx b/src/components/app-wrapper.tsx index 790164f..30a17b3 100644 --- a/src/components/app-wrapper.tsx +++ b/src/components/app-wrapper.tsx @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2021, 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 @@ -6,7 +6,8 @@ */ import App from './app'; -import React, { FunctionComponent } from 'react'; +import { FunctionComponent } from 'react'; +import { CssBaseline } from '@mui/material'; import { createTheme, StyledEngineProvider, @@ -27,12 +28,12 @@ import { import { IntlProvider } from 'react-intl'; import { BrowserRouter } from 'react-router-dom'; import { Provider, useSelector } from 'react-redux'; +import { SupportedLanguages } from '../utils/language'; import messages_en from '../translations/en.json'; import messages_fr from '../translations/fr.json'; import messages_plugins_en from '../plugins/translations/en.json'; import messages_plugins_fr from '../plugins/translations/fr.json'; import { store } from '../redux/store'; -import CssBaseline from '@mui/material/CssBaseline'; import { PARAM_THEME } from '../utils/config-params'; import { IntlConfig } from 'react-intl/src/types'; import { AppState } from '../redux/reducer'; @@ -89,7 +90,7 @@ const darkTheme: Theme = createTheme({ mapboxStyle: 'mapbox://styles/mapbox/dark-v9', }); -const getMuiTheme = (theme: unknown): Theme => { +const getMuiTheme = (theme: string): Theme => { if (theme === LIGHT_THEME) { return lightTheme; } else { @@ -97,7 +98,7 @@ const getMuiTheme = (theme: unknown): Theme => { } }; -const messages: Record = { +const messages: Record = { en: { ...messages_en, ...login_en, @@ -114,7 +115,7 @@ const messages: Record = { }, }; -const basename = new URL(document.querySelector('base')?.href || '').pathname; +const basename = new URL(document.querySelector('base')?.href ?? '').pathname; const AppWrapperWithRedux: FunctionComponent = () => { const computedLanguage = useSelector( diff --git a/src/components/app.test.tsx b/src/components/app.test.tsx index 016594c..be4ffd1 100644 --- a/src/components/app.test.tsx +++ b/src/components/app.test.tsx @@ -16,7 +16,7 @@ import { import { SnackbarProvider } from '@gridsuite/commons-ui'; import { CssBaseline } from '@mui/material'; -let container: Element | any = null; +let container: HTMLElement | null = null; beforeEach(() => { // setup a DOM element as a render target @@ -26,11 +26,14 @@ beforeEach(() => { afterEach(() => { // cleanup on exiting - container.remove(); + container?.remove(); container = null; }); it('renders', async () => { + if (container === null) { + throw new Error('No container was defined'); + } const root = createRoot(container); await act(async () => root.render( diff --git a/src/components/app.tsx b/src/components/app.tsx index 7c08ebf..36db3a6 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -1,16 +1,11 @@ -/** +/* * 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 React, { - FunctionComponent, - useCallback, - useEffect, - useState, -} from 'react'; +import { FunctionComponent, useCallback, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Navigate, @@ -36,14 +31,12 @@ import { } from '../redux/actions'; import { AppState } from '../redux/reducer'; import { - ConfigSrv, + AppsMetadataSrv, ConfigNotif, - ConfigParameter, ConfigParameters, + ConfigSrv, UserAdminSrv, - AppsMetadataSrv, } from '../services'; -import { UserManager } from 'oidc-client'; import { APP_NAME, COMMON_APP_NAME, @@ -53,12 +46,20 @@ import { import { getComputedLanguage } from '../utils/language'; import AppTopBar, { AppTopBarProps } from './app-top-bar'; import ReconnectingWebSocket from 'reconnecting-websocket'; +import { getErrorMessage } from '../utils/error'; const App: FunctionComponent = () => { const { snackError } = useSnackMessage(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const location = useLocation(); const user = useSelector((state: AppState) => state.user); + const [userManager, setUserManager] = useState< + AppTopBarProps['userManager'] + >({ instance: null, error: null }); + const signInCallbackError = useSelector( (state: AppState) => state.signInCallbackError ); @@ -69,20 +70,12 @@ const App: FunctionComponent = () => { (state: AppState) => state.showAuthenticationRouterLogin ); - const [userManager, setUserManager] = useState< - AppTopBarProps['userManager'] - >({ instance: null, error: null }); - - const navigate = useNavigate(); - - const dispatch = useDispatch(); - - const location = useLocation(); - - const updateParams: (p: ConfigParameters) => void = useCallback( + const updateParams = useCallback( (params: ConfigParameters) => { - console.debug('received UI parameters : ', params); - params.forEach((param: ConfigParameter) => { + console.groupCollapsed('received UI parameters'); + console.table(params); + console.groupEnd(); + params.forEach((param) => { switch (param.name) { case PARAM_THEME: dispatch(selectTheme(param.value)); @@ -96,14 +89,15 @@ const App: FunctionComponent = () => { ); break; default: + break; } }); }, [dispatch] ); - const connectNotificationsUpdateConfig: () => ReconnectingWebSocket = - useCallback(() => { + const connectNotificationsUpdateConfig = + useCallback((): ReconnectingWebSocket => { const ws = ConfigNotif.connectNotificationsWsUpdateConfig(); ws.onmessage = function (event) { let eventData = JSON.parse(event.data); @@ -132,7 +126,7 @@ const App: FunctionComponent = () => { path: '/silent-renew-callback', }) ); - const [initialMatchSignInCallbackUrl] = useState( + const [initialMatchSigninCallbackUrl] = useState( useMatch({ path: '/sign-in-callback', }) @@ -147,20 +141,23 @@ const App: FunctionComponent = () => { fetch('idpSettings.json'), UserAdminSrv.fetchValidateUser, authorizationCodeFlowEnabled, - initialMatchSignInCallbackUrl != null + initialMatchSigninCallbackUrl != null ) ) - .then((userManager: UserManager | undefined) => { - setUserManager({ instance: userManager || null, error: null }); + .then((userManager) => { + setUserManager({ instance: userManager ?? null, error: null }); }) - .catch((error: any) => { - setUserManager({ instance: null, error: error.message }); + .catch((error: unknown) => { + setUserManager({ + instance: null, + error: getErrorMessage(error), + }); }); - // Note: initialize and initialMatchSilentRenewCallbackUrl & initialMatchSignInCallbackUrl won't change + // Note: initialMatchSilentRenewCallbackUrl & initialMatchSigninCallbackUrl won't change }, [ dispatch, initialMatchSilentRenewCallbackUrl, - initialMatchSignInCallbackUrl, + initialMatchSigninCallbackUrl, ]); useEffect(() => { diff --git a/src/components/parameters.tsx b/src/components/parameters.tsx index f5f5b68..e756b24 100644 --- a/src/components/parameters.tsx +++ b/src/components/parameters.tsx @@ -1,11 +1,11 @@ -/** +/* * 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 React, { +import { FunctionComponent, PropsWithChildren, ReactElement, @@ -26,12 +26,12 @@ import { Tab, Tabs, Typography, + TypographyTypeMap, } from '@mui/material'; import { CSSObject, Theme } from '@emotion/react'; import { ConfigSrv } from '../services'; import { useSnackMessage } from '@gridsuite/commons-ui'; -import { AppState } from '../redux/reducer'; -import { TypographyTypeMap } from '@mui/material/Typography/Typography'; +import { AppState, AppStateKey } from '../redux/reducer'; const styles = { title: (theme: Theme): CSSObject => ({ @@ -48,14 +48,11 @@ const styles = { } as CSSObject, }; -export function useParameterState< - K extends keyof AppState, - T extends AppState[K] ->(paramName: K): [T, (value: T) => void] { +export function useParameterState( + paramName: K +): [AppState[K], (value: AppState[K]) => void] { const { snackError } = useSnackMessage(); - const paramGlobalState = useSelector((state: AppState) => state[paramName]); - const [paramLocalState, setParamLocalState] = useState(paramGlobalState); useEffect(() => { @@ -63,15 +60,16 @@ export function useParameterState< }, [paramGlobalState]); const handleChangeParamLocalState = useCallback( - (value: any) => { + (value: AppState[K]) => { setParamLocalState(value); - ConfigSrv.updateConfigParameter(paramName, value).catch((error) => { - setParamLocalState(paramGlobalState); - snackError({ - messageTxt: error.message, - headerId: 'paramsChangingError', + ConfigSrv.updateConfigParameter(paramName, value as string) //TODO how to check/cast? + .catch((error) => { + setParamLocalState(paramGlobalState); + snackError({ + messageTxt: error.message, + headerId: 'paramsChangingError', + }); }); - }); }, [paramName, snackError, setParamLocalState, paramGlobalState] ); @@ -79,37 +77,39 @@ export function useParameterState< return [paramLocalState, handleChangeParamLocalState]; } -const Parameters: FunctionComponent< - PropsWithChildren<{ - showParameters: boolean; - hideParameters: (event: object, reason?: string) => void; //(event: {}, reason: 'backdropClick' | 'escapeKeyDown'): void; //(event: MouseEvent): void - }> -> = (props) => { - const [tabIndex, setTabIndex] = useState(0); +function GUITab(): ReactElement { + return ; +} - function TabPanel( - props: PropsWithChildren< - TypographyTypeMap<{ index: T; value: T }>['props'] +type TabPanelProps = PropsWithChildren< + TypographyTypeMap<{ index: number; value: number }, 'div'>['props'] +>; +function TabPanel({ + children, + value, + index, + ...typoProps +}: TabPanelProps): ReactElement { + return ( + + ); +} - function GUITab(): ReactElement { - return ; - } +export type ParametersProps = PropsWithChildren<{ + showParameters: boolean; + hideParameters: () => void; +}>; +const Parameters: FunctionComponent = (props) => { + const [tabIndex, setTabIndex] = useState(0); return ( iframe[width='0'][height='0'] { border: 0; } diff --git a/src/index.tsx b/src/index.tsx index 67733ef..0c6c8f8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,4 @@ -/** +/* * 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 diff --git a/src/redux/actions.ts b/src/redux/actions.ts index 42171f9..09bba09 100644 --- a/src/redux/actions.ts +++ b/src/redux/actions.ts @@ -1,4 +1,4 @@ -/** +/* * 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 @@ -7,6 +7,7 @@ import { PARAM_LANGUAGE } from '../utils/config-params'; import { Action } from 'redux'; +import { AppState } from './reducer'; export const SELECT_THEME = 'SELECT_THEME'; export type ThemeAction = Readonly> & { @@ -18,9 +19,9 @@ export function selectTheme(theme: string): ThemeAction { export const SELECT_LANGUAGE = 'SELECT_LANGUAGE'; export type LanguageAction = Readonly> & { - [PARAM_LANGUAGE]: string; + [PARAM_LANGUAGE]: AppState['language']; }; -export function selectLanguage(language: string): LanguageAction { +export function selectLanguage(language: AppState['language']): LanguageAction { return { type: SELECT_LANGUAGE, [PARAM_LANGUAGE]: language }; } @@ -28,10 +29,10 @@ export const SELECT_COMPUTED_LANGUAGE = 'SELECT_COMPUTED_LANGUAGE'; export type ComputedLanguageAction = Readonly< Action > & { - computedLanguage: string; + computedLanguage: AppState['computedLanguage']; }; export function selectComputedLanguage( - computedLanguage: string + computedLanguage: AppState['computedLanguage'] ): ComputedLanguageAction { return { type: SELECT_COMPUTED_LANGUAGE, diff --git a/src/redux/local-storage.ts b/src/redux/local-storage.ts index 2c18d72..2d21268 100644 --- a/src/redux/local-storage.ts +++ b/src/redux/local-storage.ts @@ -1,4 +1,4 @@ -/** +/* * 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 @@ -8,32 +8,27 @@ import { DARK_THEME, LANG_SYSTEM } from '@gridsuite/commons-ui'; import { getComputedLanguage } from '../utils/language'; import { APP_NAME } from '../utils/config-params'; +import { AppState } from './reducer'; const LOCAL_STORAGE_THEME_KEY = (APP_NAME + '_THEME').toUpperCase(); const LOCAL_STORAGE_LANGUAGE_KEY = (APP_NAME + '_LANGUAGE').toUpperCase(); -export function getLocalStorageTheme(): ReturnType< - (typeof Storage.prototype)['getItem'] -> { +export function getLocalStorageTheme(): string { return localStorage.getItem(LOCAL_STORAGE_THEME_KEY) || DARK_THEME; } -export function saveLocalStorageTheme( - theme: string -): ReturnType<(typeof Storage.prototype)['setItem']> { +export function saveLocalStorageTheme(theme: string): void { localStorage.setItem(LOCAL_STORAGE_THEME_KEY, theme); } -export function getLocalStorageLanguage(): NonNullable< - ReturnType<(typeof Storage.prototype)['getItem']> -> { +export function getLocalStorageLanguage(): AppState['language'] { return localStorage.getItem(LOCAL_STORAGE_LANGUAGE_KEY) || LANG_SYSTEM; } -export function saveLocalStorageLanguage(language: string): void { +export function saveLocalStorageLanguage(language: AppState['language']): void { localStorage.setItem(LOCAL_STORAGE_LANGUAGE_KEY, language); } -export function getLocalStorageComputedLanguage(): string { +export function getLocalStorageComputedLanguage(): AppState['computedLanguage'] { return getComputedLanguage(getLocalStorageLanguage()); } diff --git a/src/redux/reducer.ts b/src/redux/reducer.ts index 798c34a..78d41d6 100644 --- a/src/redux/reducer.ts +++ b/src/redux/reducer.ts @@ -1,4 +1,4 @@ -/** +/* * 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 @@ -32,20 +32,22 @@ import { } from '@gridsuite/commons-ui'; import { PARAM_LANGUAGE, PARAM_THEME } from '../utils/config-params'; import { ReducerWithInitialState } from '@reduxjs/toolkit/dist/createReducer'; +import { LanguageParameters, SupportedLanguages } from '../utils/language'; +import { User } from '../utils/auth'; export type AppState = { - computedLanguage: ReturnType; - [PARAM_THEME]: ReturnType; - [PARAM_LANGUAGE]: ReturnType; + computedLanguage: SupportedLanguages; + [PARAM_THEME]: string; + [PARAM_LANGUAGE]: LanguageParameters; - user: Record | null; - signInCallbackError: any; - authenticationRouterError: any; + user: User | null; //TODO use true definition when commons-ui passed to typescript + signInCallbackError: unknown; + authenticationRouterError: unknown; showAuthenticationRouterLogin: boolean; }; const initialState: AppState = { - computedLanguage: getLocalStorageComputedLanguage(), + // authentication user: null, signInCallbackError: null, authenticationRouterError: null, @@ -54,6 +56,7 @@ const initialState: AppState = { // params [PARAM_THEME]: getLocalStorageTheme(), [PARAM_LANGUAGE]: getLocalStorageLanguage(), + computedLanguage: getLocalStorageComputedLanguage(), }; export type Actions = AnyAction | ThemeAction | ComputedLanguageAction; diff --git a/src/redux/store.ts b/src/redux/store.ts index ed03d94..c31aad6 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -1,9 +1,10 @@ -/** +/* * 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 { createStore, Store } from 'redux'; import { Actions, AppState, reducer } from './reducer'; diff --git a/src/services/apps-metadata.ts b/src/services/apps-metadata.ts index 1401aa6..86ac4fe 100644 --- a/src/services/apps-metadata.ts +++ b/src/services/apps-metadata.ts @@ -1,55 +1,116 @@ -import { ReqResponse } from '../utils/api-rest'; +/* + * 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 type EnvJson = typeof import('../../public/env.json'); +import { getErrorMessage } from '../utils/error'; +import { Url } from '../utils/api-rest'; + +export type EnvJson = typeof import('../../public/env.json') & { + // https://github.com/gridsuite/deployment/blob/main/docker-compose/env.json + // https://github.com/gridsuite/deployment/blob/main/k8s/live/azure-dev/env.json + // https://github.com/gridsuite/deployment/blob/main/k8s/live/azure-integ/env.json + // https://github.com/gridsuite/deployment/blob/main/k8s/live/local/env.json + appsMetadataServerUrl?: Url; + mapBoxToken?: string; + //[key: string]: string; +}; function fetchEnv(): Promise { - return fetch('/env.json').then((res: ReqResponse) => res.json()); + return fetch('env.json').then((res: Response) => res.json()); } export function fetchAuthorizationCodeFlowFeatureFlag(): Promise { - console.info(`Fetching authorization code flow feature flag...`); + console.debug('Fetching authorization code flow feature flag...'); return fetchEnv() .then((env: EnvJson) => fetch(`${env.appsMetadataServerUrl}/authentication.json`) ) - .then((res: ReqResponse) => res.json()) - .then((res: Record) => { - console.log( + .then((res: Response) => res.json()) + .then((res: { authorizationCodeFlowFeatureFlag: boolean }) => { + console.info( `Authorization code flow is ${ res.authorizationCodeFlowFeatureFlag ? 'enabled' : 'disabled' }` ); - return res.authorizationCodeFlowFeatureFlag; + return res.authorizationCodeFlowFeatureFlag || false; }) .catch((error) => { - console.error(error); + console.error( + `Error while fetching the authentication code flow: ${getErrorMessage( + error + )}` + ); console.warn( - `Something wrong happened when retrieving authentication.json: authorization code flow will be disabled` + 'Something wrong happened when retrieving authentication.json: authorization code flow will be disabled' ); return false; }); } -export function fetchVersion(): Promise> { - console.info(`Fetching global metadata...`); +export type VersionJson = { + deployVersion?: string; +}; + +export function fetchVersion(): Promise { + console.debug(`Fetching global metadata...`); return fetchEnv() .then((env: EnvJson) => fetch(`${env.appsMetadataServerUrl}/version.json`) ) - .then((response: ReqResponse) => response.json()) - .catch((reason) => { - console.error(`Error while fetching the version : ${reason}`); - return reason; + .then((response: Response) => response.json()) + .catch((error) => { + console.error( + `Error while fetching the version: ${getErrorMessage(error)}` + ); + throw error; }); } -export function fetchAppsAndUrls(): Promise>> { - console.info(`Fetching apps and urls...`); +export type MetadataCommon = { + name: string; + url: Url; + appColor: string; + hiddenInAppsMenu: boolean; +}; + +export type MetadataStudy = MetadataCommon & { + readonly name: 'Study'; + resources?: { + types: string[]; + path: string; + }[]; + predefinedEquipmentProperties?: { + substation?: { + region?: string[]; + tso?: string[]; + totallyFree?: unknown[]; + Demo?: string[]; + }; + load?: { + codeOI?: string[]; + }; + }; + defaultParametersValues?: { + fluxConvention?: string; + enableDeveloperMode?: string; //maybe 'true'|'false' type? + mapManualRefresh?: string; //maybe 'true'|'false' type? + }; +}; + +// https://github.com/gridsuite/deployment/blob/main/docker-compose/docker-compose.base.yml +// https://github.com/gridsuite/deployment/blob/main/k8s/resources/common/config/apps-metadata.json +export type MetadataJson = MetadataCommon | MetadataStudy; + +export function fetchAppsAndUrls(): Promise { + console.debug('Fetching apps and urls...'); return fetchEnv() .then((env: EnvJson) => fetch(`${env.appsMetadataServerUrl}/apps-metadata.json`) ) - .then((response: ReqResponse) => response.json()); + .then((response: Response) => response.json()); } diff --git a/src/services/config-notification.ts b/src/services/config-notification.ts index 5260b0d..1da21e7 100644 --- a/src/services/config-notification.ts +++ b/src/services/config-notification.ts @@ -1,3 +1,10 @@ +/* + * 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 ReconnectingWebSocket, { Event } from 'reconnecting-websocket'; import { APP_NAME } from '../utils/config-params'; import { getUrlWithToken, getWsBase } from '../utils/api-ws'; @@ -12,9 +19,7 @@ export function connectNotificationsWsUpdateConfig(): ReconnectingWebSocket { { debug: process.env.REACT_APP_DEBUG_REQUESTS === 'true' } ); reconnectingWebSocket.onopen = function (event: Event) { - console.info( - `Connected Websocket update config ui ${webSocketUrl} ...` - ); + console.info(`Connected Websocket update config ui: ${webSocketUrl}`); }; return reconnectingWebSocket; } diff --git a/src/services/config.ts b/src/services/config.ts index 97dbba5..c996a6c 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -1,42 +1,61 @@ -import { getAppName } from '../utils/config-params'; +/* + * 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 { + APP_NAME, + getAppName, + PARAM_LANGUAGE, + PARAM_THEME, +} from '../utils/config-params'; import { backendFetch, backendFetchJson } from '../utils/api-rest'; +import { LanguageParameters } from '../utils/language'; const PREFIX_CONFIG_QUERIES = `${process.env.REACT_APP_API_GATEWAY}/config`; -export type ConfigParameter = { - //TODO check with config-server swagger - name: string; - value: any; - [propertiesName: string]: unknown; //temporary -}; -export type ConfigParameters = Array; +// https://github.com/gridsuite/config-server/blob/main/src/main/java/org/gridsuite/config/server/dto/ParameterInfos.java +export type ConfigParameter = + | { + readonly name: typeof PARAM_LANGUAGE; + value: LanguageParameters; + } + | { + readonly name: typeof PARAM_THEME; + value: string; + }; +export type ConfigParameters = ConfigParameter[]; export function fetchConfigParameters( - appName: string + appName: string = APP_NAME ): Promise { - console.info(`Fetching UI configuration params for app : ${appName}`); + console.debug(`Fetching UI configuration params for app : ${appName}`); const fetchParams = `${PREFIX_CONFIG_QUERIES}/v1/applications/${appName}/parameters`; - return backendFetchJson(fetchParams); + return backendFetchJson(fetchParams) as Promise; } -export function fetchConfigParameter( - name: string -): ReturnType { +export function fetchConfigParameter(name: string): Promise { const appName = getAppName(name); - console.info(`Fetching UI config parameter '${name}' for app '${appName}'`); + console.debug( + `Fetching UI config parameter '${name}' for app '${appName}'` + ); const fetchParams = `${PREFIX_CONFIG_QUERIES}/v1/applications/${appName}/parameters/${name}`; - return backendFetchJson(fetchParams); + return backendFetchJson(fetchParams) as Promise; } export function updateConfigParameter( name: string, value: Parameters[0] -): ReturnType { +): Promise { const appName = getAppName(name); - console.info( + console.debug( `Updating config parameter '${name}=${value}' for app '${appName}'` ); const updateParams = `${PREFIX_CONFIG_QUERIES}/v1/applications/${appName}/parameters/${name}?value=${encodeURIComponent( value )}`; - return backendFetch(updateParams, { method: 'put' }); + return backendFetch(updateParams, { + method: 'put', + }) as Promise as Promise; } diff --git a/src/services/study.ts b/src/services/study.ts index 622fd40..cfcae34 100644 --- a/src/services/study.ts +++ b/src/services/study.ts @@ -1,10 +1,13 @@ -/** +/* * Copyright (c) 2023, 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 { backendFetchJson, Token } from '../utils/api-rest'; +import { getErrorMessage } from '../utils/error'; +import { APP_NAME } from '../utils/config-params'; const STUDY_URL = `${process.env.REACT_APP_API_GATEWAY}/study/v1`; @@ -18,7 +21,7 @@ export type ServerAbout = { export function getServersInfos(token: Token): Promise { return backendFetchJson( - `${STUDY_URL}/servers/about`, + `${STUDY_URL}/servers/about?view=${APP_NAME}`, { headers: { Accept: 'application/json', @@ -27,8 +30,10 @@ export function getServersInfos(token: Token): Promise { cache: 'default', }, token - ).catch((reason) => { - console.error(`Error while fetching the servers infos : ${reason}`); - return reason; - }); + ).catch((error) => { + console.error( + `Error while fetching the servers infos: ${getErrorMessage(error)}` + ); + throw error; + }) as Promise; } diff --git a/src/services/user-admin.ts b/src/services/user-admin.ts index 0c0b8a5..3fbdf08 100644 --- a/src/services/user-admin.ts +++ b/src/services/user-admin.ts @@ -1,13 +1,21 @@ -import { backendFetch, ReqResponse } from '../utils/api-rest'; +/* + * 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 { backendFetch } from '../utils/api-rest'; +import { User } from '../utils/auth'; const USER_ADMIN_URL = `${process.env.REACT_APP_API_GATEWAY}/user-admin`; -export function fetchValidateUser(user: Record): Promise { +export function fetchValidateUser(user: User): Promise { const sub = user?.profile?.sub; if (!sub) { return Promise.reject( new Error( - `Fetching access for missing user.profile.sub : ${JSON.stringify( + `Error : Fetching access for missing user.profile.sub : ${JSON.stringify( user )}` ) @@ -19,7 +27,7 @@ export function fetchValidateUser(user: Record): Promise { console.debug(CheckAccessUrl); return backendFetch(CheckAccessUrl, { method: 'head' }, user?.id_token) - .then((response: ReqResponse) => { + .then((response: Response) => { //if the response is ok, the responseCode will be either 200 or 204 otherwise it's an HTTP error and it will be caught return response.status === 200; }) diff --git a/src/setupTests.js b/src/setupTests.js index e373e4d..dbd427b 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2022, 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 diff --git a/src/translations/en.json b/src/translations/en.json index 8d9c045..3a518e5 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1,6 +1,6 @@ { "close": "Close", "parameters": "Parameters", - "paramsChangingError": "An error occured when changing the parameters", + "paramsChangingError": "An error occurred when changing the parameters", "paramsRetrievingError": "An error occurred while retrieving the parameters" } diff --git a/src/utils/api-rest.ts b/src/utils/api-rest.ts index 536c077..f950ffd 100644 --- a/src/utils/api-rest.ts +++ b/src/utils/api-rest.ts @@ -1,4 +1,4 @@ -/** +/* * 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 @@ -13,11 +13,10 @@ export interface ErrorWithStatus extends Error { status?: number; } -export type Url = Exclude[0], Request>; //string | URL; -export type InitRequest = Partial[1]>; //Partial; -export type ReqResponse = Awaited>; +export type Url = string | URL; +export type InitRequest = Partial; -function handleError(response: ReqResponse): Promise { +function handleError(response: Response): Promise { return response.text().then((text: string) => { const errorName = 'HttpResponseError : '; let error: ErrorWithStatus; @@ -50,8 +49,8 @@ function prepareRequest(init?: InitRequest, token?: Token): RequestInit { return initCopy; } -function safeFetch(url: Url, initCopy?: InitRequest): ReturnType { - return fetch(url, initCopy).then((response: ReqResponse) => +function safeFetch(url: Url, initCopy?: InitRequest) { + return fetch(url, initCopy).then((response: Response) => response.ok ? response : handleError(response) ); } @@ -60,7 +59,7 @@ export function backendFetch( url: Url, init?: InitRequest, token?: Token -): ReturnType { +): Promise { return safeFetch(url, prepareRequest(init, token)); } @@ -68,8 +67,8 @@ export function backendFetchText( url: Url, init?: InitRequest, token?: Token -): ReturnType { - return backendFetch(url, init, token).then((safeResponse: ReqResponse) => +): Promise { + return backendFetch(url, init, token).then((safeResponse: Response) => safeResponse.text() ); } @@ -78,8 +77,8 @@ export function backendFetchJson( url: Url, init?: InitRequest, token?: Token -): ReturnType { - return backendFetch(url, init, token).then((safeResponse: ReqResponse) => +): Promise { + return backendFetch(url, init, token).then((safeResponse: Response) => safeResponse.json() ); } diff --git a/src/utils/api-ws.ts b/src/utils/api-ws.ts index 1083770..b8272eb 100644 --- a/src/utils/api-ws.ts +++ b/src/utils/api-ws.ts @@ -1,3 +1,10 @@ +/* + * 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 { getToken } from './api'; export type * from './api'; diff --git a/src/utils/api.ts b/src/utils/api.ts index 4570046..a67c8f5 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -1,14 +1,21 @@ +/* + * 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 { AppState } from '../redux/reducer'; import { store } from '../redux/store'; export type Token = string; -export function getToken(): Token { +export function getToken(): Token | null { const state: AppState = store.getState(); - return state.user?.id_token; + return state.user?.id_token ?? null; } -export function parseError(text: string): any { +export function parseError(text: string) { try { return JSON.parse(text); } catch (err) { diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 0000000..6b2cfcb --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +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 type User = { + id_token: string; + profile?: { + sub?: string; + }; +}; diff --git a/src/utils/config-params.ts b/src/utils/config-params.ts index 78e35ae..cba6391 100644 --- a/src/utils/config-params.ts +++ b/src/utils/config-params.ts @@ -1,10 +1,12 @@ -/** +/* * Copyright (c) 2021, 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 { LiteralUnion } from 'type-fest'; + export const COMMON_APP_NAME = 'common'; export const APP_NAME = 'Admin'; @@ -13,7 +15,18 @@ export const PARAM_LANGUAGE = 'language'; const COMMON_CONFIG_PARAMS_NAMES = new Set([PARAM_THEME, PARAM_LANGUAGE]); -export function getAppName(paramName: string): string { +export type AppConfigParameter = LiteralUnion< + typeof PARAM_THEME | typeof PARAM_LANGUAGE, + string +>; + +export type AppConfigType = typeof COMMON_APP_NAME | typeof APP_NAME; + +/** + * Permit knowing if a parameter is common/shared between webapps or is specific to this application. + * @param paramName the parameter name/key + */ +export function getAppName(paramName: AppConfigParameter): AppConfigType { return COMMON_CONFIG_PARAMS_NAMES.has(paramName) ? COMMON_APP_NAME : APP_NAME; diff --git a/src/utils/error.ts b/src/utils/error.ts new file mode 100644 index 0000000..af9d81c --- /dev/null +++ b/src/utils/error.ts @@ -0,0 +1,26 @@ +/* + * 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 function getErrorMessage(error: unknown): string | null { + if (error instanceof Error) { + return error.message; + } else if (error instanceof Object && 'message' in error) { + if ( + typeof error.message === 'string' || + typeof error.message === 'number' || + typeof error.message === 'boolean' + ) { + return `${error.message}`; + } else { + return JSON.stringify(error.message ?? undefined) ?? null; + } + } else if (typeof error === 'string') { + return error; + } else { + return JSON.stringify(error ?? undefined) ?? null; + } +} diff --git a/src/utils/language.ts b/src/utils/language.ts index 6907e7c..12ad114 100644 --- a/src/utils/language.ts +++ b/src/utils/language.ts @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2021, 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 @@ -8,14 +8,23 @@ import { LANG_ENGLISH, LANG_FRENCH, LANG_SYSTEM } from '@gridsuite/commons-ui'; const supportedLanguages = [LANG_FRENCH, LANG_ENGLISH]; +//export type SupportedLanguagesType = typeof supportedLanguages[number]; //TODO when commons-ui in typescript +export type SupportedLanguages = 'en' | 'fr'; +//export type LanguageParameters = SupportedLanguages | typeof LANG_SYSTEM; //TODO when commons-ui in typescript +export type LanguageParameters = SupportedLanguages | 'sys'; -export function getSystemLanguage(): string { +export function getSystemLanguage(): SupportedLanguages { const systemLanguage = navigator.language.split(/[-_]/)[0]; return supportedLanguages.includes(systemLanguage) ? systemLanguage : LANG_ENGLISH; } -export function getComputedLanguage(language: string): string { - return language === LANG_SYSTEM ? getSystemLanguage() : language; +export function getComputedLanguage( + language: LanguageParameters +): SupportedLanguages { + return language === LANG_SYSTEM + ? getSystemLanguage() + : (language as SupportedLanguages); + //TODO remove cast when commons-ui in typescript }