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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,18 @@ const projectConfig = [
optionalDependencies: false,
},
],
'import-x/no-restricted-paths': [
'error',
{
zones: [
// shared must not import from features or app
{ target: './src/shared', from: './src/features' },
{ target: './src/shared', from: './src/app' },
// features must not import from app
{ target: './src/features', from: './src/app' },
],
},
],

// Disable strict type-aware rules that weren't in old config
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
Expand Down
4 changes: 2 additions & 2 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
import { getErrorMessage } from 'shared/lib/error';
import { fetchIdpSettings } from 'shared/config/idp-settings';
import { useAppParametersInvalidationListener } from 'features/app-parameters/hooks/use-app-parameters-invalidation-listener';
import { useProcessInvalidationsListener } from 'features/process-config/hooks/use-process-invalidation-listener';
import { useProcessInvalidationListener } from 'features/process-config/hooks/use-process-invalidation-listener';
import AppTopBar, { AppTopBarProps } from 'features/top-bar/components/AppTopBar';
import { useAppDispatch, useAppSelector } from './store/store';
import { AppRouter } from './router/AppRouter';
Expand Down Expand Up @@ -73,7 +73,7 @@ function App() {
}, [initialMatchSigninCallbackUrl, initialMatchSilentRenewCallbackUrl, dispatch]);

useAppParametersInvalidationListener({ isAuthenticated: user !== null });
useProcessInvalidationsListener({ isAuthenticated: user !== null });
useProcessInvalidationListener({ isAuthenticated: user !== null });

return (
<>
Expand Down
5 changes: 4 additions & 1 deletion src/app/config/app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@
* 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 { configureParams, TokenSelector } from 'shared/config/config-params';
import { selectToken } from 'features/authentication/store/authentication.selectors';

export const APP_NAME = 'monitor';
const APP_NAME = 'monitor';
configureParams({ appName: APP_NAME, tokenSelector: selectToken as TokenSelector });
17 changes: 13 additions & 4 deletions src/app/providers/AppProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { StyledEngineProvider, ThemeProvider, CssBaseline } from '@mui/material';
import { CssBaseline, StyledEngineProvider, ThemeProvider } from '@mui/material';
import {
CardErrorBoundary,
getComputedLanguage,
Expand All @@ -16,19 +16,28 @@ import {
import { IntlProvider } from 'react-intl';
import { BrowserRouter } from 'react-router';
import { Provider } from 'react-redux';
import { store } from 'app/store/store';
import 'app/config/app-config'; // side-effect import: configure params, ex. appName, must before all other side-effect imports, such as configureStore
import { store, useAppSelector } from 'app/store/store';
import App from 'app/App';
import { appMessages } from 'app/config/app-messages';
import { getAppTheme } from 'app/config/app-theme';
import { useGetConfigParameterWithFallback } from 'features/app-parameters/hooks/use-get-config-parameter-with-fallback';
import { selectUser } from 'features/authentication/store/authentication.selectors';
import { SnackRefRegisterer } from './SnackRefRegisterer';

const basename = new URL(document.querySelector('base')?.href ?? '').pathname;

function AppProvidersWithStore() {
const { data: language } = useGetConfigParameterWithFallback(PARAM_LANGUAGE);
const user = useAppSelector(selectUser);
const { data: language } = useGetConfigParameterWithFallback({
paramName: PARAM_LANGUAGE,
isAuthenticated: user !== null,
});
const computedLanguage = getComputedLanguage(language);
const { data: theme } = useGetConfigParameterWithFallback(PARAM_THEME);
const { data: theme } = useGetConfigParameterWithFallback({
paramName: PARAM_THEME,
isAuthenticated: user !== null,
});

return (
<IntlProvider locale={computedLanguage} messages={appMessages[computedLanguage]}>
Expand Down
5 changes: 4 additions & 1 deletion src/app/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@

import { configureStore } from '@reduxjs/toolkit';
import { useDispatch, useSelector } from 'react-redux';
import { errorMiddleware } from 'shared/store/rtk-query-error-middleware';
import { monitorApi } from 'shared/api/monitor-api';
import { studyApi } from 'shared/api/study-api';
import { configApi } from 'shared/api/config-api';
import { updateConfigParams } from 'shared/config/config-params';
import { reducer } from './reducer';
import { errorMiddleware } from './rtk-query-error-middleware';

export const setupStore = (preloadedState?: PreloadedState) =>
configureStore({
Expand All @@ -28,6 +29,8 @@ export const setupStore = (preloadedState?: PreloadedState) =>
});

export const store = setupStore();
// push store to configParams to be able to access it in the token selector at low-level module
updateConfigParams({ store });

export type PreloadedState = Parameters<typeof reducer>[0];
export type RootState = ReturnType<typeof reducer>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { renderHook, act, waitFor } from '@testing-library/react';
import { describe, it, expect, beforeEach } from 'vitest';
import { act, renderHook, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it } from 'vitest';
import { http, HttpResponse } from 'msw';
import { server } from 'test-utils/msw/server';
import { createTestContext } from 'test-utils/create-test-context';
import { DARK_THEME, LIGHT_THEME, PARAM_THEME } from '@gridsuite/commons-ui';
import { server } from 'shared/test-utils/msw/server';
import { createBaseContext } from 'features/test-utils/create-base-context';
import { useAppParameterState } from 'features/app-parameters/hooks/use-app-parameter-state';
import { DARK_THEME, LIGHT_THEME } from '@gridsuite/commons-ui';

describe('useAppParameterState', () => {
beforeEach(() => {
server.use(
http.get('*/config/v1/applications/*/parameters/theme', () =>
HttpResponse.json({
name: 'theme',
name: PARAM_THEME,
value: DARK_THEME,
})
)
Expand All @@ -34,8 +34,10 @@ describe('useAppParameterState', () => {
return HttpResponse.json({});
})
);
const { wrapper } = createTestContext();
const { result } = renderHook(() => useAppParameterState('theme'), { wrapper });
const { wrapper } = createBaseContext();
const { result } = renderHook(() => useAppParameterState({ paramName: PARAM_THEME, isAuthenticated: true }), {
wrapper,
});

// check state before updating
await waitFor(() => {
Expand Down Expand Up @@ -74,8 +76,10 @@ describe('useAppParameterState', () => {
})
);

const { wrapper } = createTestContext();
const { result } = renderHook(() => useAppParameterState('theme'), { wrapper });
const { wrapper } = createBaseContext();
const { result } = renderHook(() => useAppParameterState({ paramName: PARAM_THEME, isAuthenticated: true }), {
wrapper,
});

// check state before updating
await waitFor(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
*/

import { renderHook } from '@testing-library/react';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import * as configWs from 'shared/api/ws/config-ws';
import { createTestContext } from 'test-utils/create-test-context';
import { useAppParametersInvalidationListener } from 'features/app-parameters/hooks/use-app-parameters-invalidation-listener';
import * as configApiModule from 'shared/api/config-api';
import { connectConfigNotificationsWs } from 'shared/api/ws/config-ws';
import { createBaseContext } from 'features/test-utils/create-base-context';
import * as configApiModule from 'shared/api/config-api';
import { useAppParametersInvalidationListener } from 'features/app-parameters/hooks/use-app-parameters-invalidation-listener';

vi.spyOn(configWs, 'connectConfigNotificationsWs').mockImplementation(vi.fn());
vi.spyOn(configApiModule, 'invalidateConfigQueries').mockImplementation(vi.fn());
Expand All @@ -34,23 +34,23 @@ describe('useAppParametersInvalidationListener', () => {
});

it('connects websocket on mount', () => {
const { wrapper } = createTestContext();
const { wrapper } = createBaseContext();

renderHook(() => useAppParametersInvalidationListener({ isAuthenticated: true }), { wrapper });

expect(connectConfigNotificationsWs).toHaveBeenCalled();
});

it('does not connect websocket when user is not authenticated', () => {
const { wrapper } = createTestContext();
const { wrapper } = createBaseContext();

renderHook(() => useAppParametersInvalidationListener({ isAuthenticated: false }), { wrapper });

expect(connectConfigNotificationsWs).not.toHaveBeenCalled();
});

it('invalidates config when receiving a message', () => {
const { wrapper } = createTestContext();
const { wrapper } = createBaseContext();

renderHook(() => useAppParametersInvalidationListener({ isAuthenticated: true }), { wrapper });

Expand All @@ -65,7 +65,7 @@ describe('useAppParametersInvalidationListener', () => {
});

it('closes websocket on unmount', () => {
const { wrapper } = createTestContext();
const { wrapper } = createBaseContext();

const { unmount } = renderHook(() => useAppParametersInvalidationListener({ isAuthenticated: true }), {
wrapper,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { DARK_THEME, LIGHT_THEME, PARAM_THEME } from '@gridsuite/commons-ui';
import { renderHook, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it } from 'vitest';
import { http, HttpResponse } from 'msw';
import { DARK_THEME, LIGHT_THEME, PARAM_THEME } from '@gridsuite/commons-ui';
import { server } from 'shared/test-utils/msw/server';
import { createBaseContext } from 'features/test-utils/create-base-context';
import { useGetConfigParameterWithFallback } from 'features/app-parameters/hooks/use-get-config-parameter-with-fallback';
import { server } from 'test-utils/msw/server';
import { createTestContext } from 'test-utils/create-test-context';
import { saveLocalStorageTheme } from 'features/app-parameters/store/app-parameters.local-storage';

beforeEach(() => localStorage.clear());
Expand All @@ -27,9 +27,12 @@ describe('useGetConfigParameterWithFallback', () => {
)
);

const { wrapper } = createTestContext();
const { wrapper } = createBaseContext();

const { result } = renderHook(() => useGetConfigParameterWithFallback(PARAM_THEME), { wrapper });
const { result } = renderHook(
() => useGetConfigParameterWithFallback({ paramName: PARAM_THEME, isAuthenticated: true }),
{ wrapper }
);

await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
Expand All @@ -38,23 +41,29 @@ describe('useGetConfigParameterWithFallback', () => {
expect(result.current.data).toBe(LIGHT_THEME);
});

it('hook returns localstorage if no user in store', async () => {
const { wrapper } = createTestContext({ authentication: { user: null } });
it('hook returns local storage if not authenticated in store', async () => {
const { wrapper } = createBaseContext();
saveLocalStorageTheme(LIGHT_THEME);

const { result } = renderHook(() => useGetConfigParameterWithFallback(PARAM_THEME), {
wrapper,
});
const { result } = renderHook(
() => useGetConfigParameterWithFallback({ paramName: PARAM_THEME, isAuthenticated: false }),
{
wrapper,
}
);

expect(result.current.data).toBe(LIGHT_THEME);
});

it('hook returns fallback if no user in store and nothing in local storage', async () => {
const { wrapper } = createTestContext({ authentication: { user: null } });
it('hook returns fallback if not authenticated and nothing in local storage', async () => {
const { wrapper } = createBaseContext();

const { result } = renderHook(() => useGetConfigParameterWithFallback(PARAM_THEME), {
wrapper,
});
const { result } = renderHook(
() => useGetConfigParameterWithFallback({ paramName: PARAM_THEME, isAuthenticated: false }),
{
wrapper,
}
);

expect(result.current.data).toBe(DARK_THEME);
});
Expand Down
18 changes: 13 additions & 5 deletions src/features/app-parameters/hooks/use-app-parameter-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,27 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { getAppName as getAppNameCommons } from '@gridsuite/commons-ui';
import { AppParameters, AppParametersKey } from 'features/app-parameters/store/app-parameters.type';
import { getAppName } from '@gridsuite/commons-ui';
import { getAppName } from 'shared/config/config-params';
import { useUpdateParameterMutation } from 'shared/api/config-api';
import { useGetConfigParameterWithFallback } from './use-get-config-parameter-with-fallback';
import { APP_NAME } from '../../../app/config/app-config';

export function useAppParameterState<K extends AppParametersKey>(paramName: K) {
const { data: paramValue } = useGetConfigParameterWithFallback(paramName);
type UseAppParameterStateProps<K extends AppParametersKey> = {
paramName: K;
isAuthenticated: boolean;
};

export function useAppParameterState<K extends AppParametersKey>({
paramName,
isAuthenticated,
}: UseAppParameterStateProps<K>) {
const { data: paramValue } = useGetConfigParameterWithFallback({ paramName, isAuthenticated });
const [updateConfigParameter] = useUpdateParameterMutation();

const setValue = async (newValue: AppParameters[K]) => {
await updateConfigParameter({
appName: getAppName(APP_NAME, paramName),
appName: getAppNameCommons(getAppName(), paramName),
name: paramName,
value: newValue,
}).unwrap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,26 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { useAppDispatch } from 'app/store/store';
import { useDispatch } from 'react-redux';
import { useEffect } from 'react';
import { connectConfigNotificationsWs } from 'shared/api/ws/config-ws';
import { invalidateConfigQueries } from 'shared/api/config-api';
import { connectConfigNotificationsWs } from 'shared/api/ws/config-ws';
import type { AnyAppDispatch } from 'shared/store/state.type';

type ConfigNotificationData = {
headers?: {
parameterName?: string;
};
};

export const useAppParametersInvalidationListener = ({ isAuthenticated }: { isAuthenticated: boolean }) => {
const dispatch = useAppDispatch();
type UseAppParametersInvalidationListenerProps = {
isAuthenticated: boolean;
};

export const useAppParametersInvalidationListener = ({
isAuthenticated,
}: UseAppParametersInvalidationListenerProps) => {
const dispatch = useDispatch<AnyAppDispatch>();

useEffect(() => {
if (!isAuthenticated) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,28 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { getAppName } from '@gridsuite/commons-ui';
import { APP_NAME } from 'app/config/app-config';
import { useAppSelector } from 'app/store/store';
import { selectUser } from 'features/authentication/store/authentication.selectors';
import { getAppName as getAppNameCommons } from '@gridsuite/commons-ui';
import { useGetParameterQuery } from 'shared/api/config-api';
import { getAppName } from 'shared/config/config-params';
import { getInitialAppParametersState } from '../store/app-parameters.default';
import { AppParameters, AppParametersKey } from '../store/app-parameters.type';

type UseGetConfigParameterWithFallbackProps<K extends AppParametersKey> = {
paramName: K;
isAuthenticated: boolean;
};
/**
* This data is fetched from AppTopBar, which is displayed before user is authenticated
* If user is not authenticated, or before the fetch request has responded, we use data from initialAppParametersState
* This data is fetched from AppTopBar, which is displayed before the user is authenticated,
* If the user is not authenticated, or before the fetch request has responded, we use data from initialAppParametersState
*/
export const useGetConfigParameterWithFallback = <K extends AppParametersKey>(paramName: K) => {
const user = useAppSelector(selectUser);

export const useGetConfigParameterWithFallback = <K extends AppParametersKey>({
paramName,
isAuthenticated,
}: UseGetConfigParameterWithFallbackProps<K>) => {
return useGetParameterQuery(
{ name: paramName, appName: getAppName(APP_NAME, paramName) },
{ name: paramName, appName: getAppNameCommons(getAppName(), paramName) },
{
skip: !user,
skip: !isAuthenticated,
selectFromResult: (result) => {
const data = result.data?.value ?? getInitialAppParametersState()[paramName];

Expand Down
Loading
Loading