Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mijn-9913-Bug varen service #1681

Merged
merged 22 commits into from
Jan 14, 2025
Merged
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
31 changes: 20 additions & 11 deletions src/client/components/MainHeader/ProfileName.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,29 @@ import { MutableSnapshot } from 'recoil';

import { ProfileName } from './ProfileName';
import { AppRoutes } from '../../../universal/config/routes';
import { appStateAtom, useAppStateGetter } from '../../hooks/useAppState';
import { AppState } from '../../../universal/types';
import {
appStateAtom,
appStateReadyAtom,
useAppStateGetter,
} from '../../hooks/useAppState';
import MockApp from '../../pages/MockApp';

vi.mock('../../hooks/media.hook');

function testState(brp: any = null, profile: any = null, kvk: any = null) {
const s: any = {
const s = {
BRP: {
status: 'OK',
content: brp,
},
PROFILE: { status: 'OK', content: profile },
KVK: { status: 'OK', content: kvk },
};
} as unknown as AppState;

return (snapshot: MutableSnapshot) => {
snapshot.set(appStateAtom, s);
snapshot.set(appStateReadyAtom, true);
};
}

Expand All @@ -39,14 +46,16 @@ describe('<ProfileName />', () => {
const routeEntry = AppRoutes.HOME;
const routePath = AppRoutes.HOME;

const Component = ({ profileType, brp, kvk, profile }: any) => (
<MockApp
routeEntry={routeEntry}
routePath={routePath}
component={() => <Wrapper profileType={profileType} />}
initializeState={testState(brp, profile, kvk)}
/>
);
function Component({ profileType, brp, kvk, profile }: any) {
return (
<MockApp
routeEntry={routeEntry}
routePath={routePath}
component={() => <Wrapper profileType={profileType} />}
initializeState={testState(brp, profile, kvk)}
/>
);
}

it('Shows BRP naam', () => {
render(<Component brp={{ persoon: { opgemaakteNaam: 'J de grever' } }} />);
Expand Down
9 changes: 6 additions & 3 deletions src/client/components/Search/Search.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import * as bagResponse from './bag-response.json';
import { Search } from './Search';
import * as remoteConfig from './search-config.json';
import { bffApi, remoteApi } from '../../../testing/utils';
import { appStateAtom } from '../../hooks/useAppState';
import { AppState } from '../../../universal/types';
import { appStateAtom, appStateReadyAtom } from '../../hooks/useAppState';

describe('<Search />', () => {
beforeEach(() => {
Expand Down Expand Up @@ -105,7 +106,8 @@ describe('<Search />', () => {
initializeState={(snapshot) => {
snapshot.set(appStateAtom, {
VERGUNNINGEN: { status: 'OK', content: [] },
} as any);
} as unknown as AppState);
snapshot.set(appStateReadyAtom, true);
}}
>
<Search />
Expand Down Expand Up @@ -140,7 +142,8 @@ describe('<Search />', () => {
},
],
},
} as any);
} as unknown as AppState);
snapshot.set(appStateReadyAtom, true);
}}
>
<Search />
Expand Down
2 changes: 1 addition & 1 deletion src/client/data-transform/appState.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ describe('transformSourceData', () => {
const monitoringSpy = vi.spyOn(Monitoring, 'captureMessage');
const result = transformSourceData(data as Partial<AppState>);
expect(monitoringSpy).toHaveBeenCalledWith(
'[transformSourceData] Unknown stateKey encountered',
'[transformSourceData] Unknown stateKey encountered, not found in PRISTINE_APPSTATE',
{
properties: {
unexpectedStateKeys: ['STATE_KEY'],
Expand Down
17 changes: 11 additions & 6 deletions src/client/data-transform/appState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,26 @@ export function transformSourceData(data: Partial<AppState> | null) {
data[key] === null ||
data[key]?.status === 'ERROR'
) {
// Data returned by server is not usable, replace it with pristine appState;
if (typeof data[key] !== 'object' || data[key] === null) {
// @ts-ignore
// @ts-expect-error TS cannot compute type properly
data[key] = PRISTINE_APPSTATE[key];
} else {
// Data returned by server is an error, replace content with pristine content;
data[key]!.content = PRISTINE_APPSTATE[key]?.content || null;
}
}
}

if (unexpectedStateKeys.length) {
captureMessage('[transformSourceData] Unknown stateKey encountered', {
properties: {
unexpectedStateKeys,
},
});
captureMessage(
'[transformSourceData] Unknown stateKey encountered, not found in PRISTINE_APPSTATE',
{
properties: {
unexpectedStateKeys,
},
}
);
}

return data;
Expand Down
55 changes: 2 additions & 53 deletions src/client/hooks/useAppState.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,12 @@ import {

import * as dataApiHook from './api/useDataApi';
import { newEventSourceMock } from './EventSourceMock';
import {
addParamsToStreamEndpoint,
isAppStateReady,
useAppStateRemote,
} from './useAppState';
import { addParamsToStreamEndpoint, useAppStateRemote } from './useAppState';
import * as sseHook from './useSSE';
import { SSE_ERROR_MESSAGE } from './useSSE';
import { renderRecoilHook } from '../../testing/render-recoil.hook';
import { FeatureToggle } from '../../universal/config/feature-toggles';
import {
apiPristineResult,
apiSuccessResult,
} from '../../universal/helpers/api';
import * as appStateModule from '../AppState';
import * as Monitoring from '../helpers/monitoring';
import { renderRecoilHook } from '../../testing/render-recoil.hook';

vi.mock('./api/useTipsApi');
vi.mock('./useProfileType');
Expand Down Expand Up @@ -152,48 +143,6 @@ describe('useAppState', () => {
});
});

describe('isAppStateReady', () => {
const pristineState = {
TEST: apiPristineResult(null, { profileTypes: ['private'] }),
} as any;

it('Should initially be false', async () => {
const appState = { TEST: pristineState.TEST } as any;

const isReady = isAppStateReady(appState, pristineState, 'private');
expect(isReady).toBe(false);
});

it('Should be true if we have proper data', async () => {
const isReady = isAppStateReady(
{ TEST: apiSuccessResult('test') } as any,
pristineState,
'private'
);
expect(isReady).toBe(true);
});

it('Should be false if we have proper data but a different profile type', async () => {
const isReady = isAppStateReady(
{ TEST: apiSuccessResult('test') } as any,
pristineState,
'commercial'
);
expect(isReady).toBe(false);
});

it('Should be false if we have statekey mismatch', async () => {
const spy = vi.spyOn(Monitoring, 'captureMessage');
const isReady = isAppStateReady(
{ BLAP: apiSuccessResult('blap') } as any,
pristineState,
'private'
);
expect(isReady).toBe(true);
expect(spy).toHaveBeenCalledWith('unknown stateConfig key: BLAP');
});
});

test('addParamsToStreamEndpoint', () => {
const origValue = FeatureToggle.passQueryParamsToStreamUrl;
FeatureToggle.passQueryParamsToStreamUrl = false;
Expand Down
105 changes: 35 additions & 70 deletions src/client/hooks/useAppState.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';

import { SetterOrUpdater, atom, useRecoilState, useRecoilValue } from 'recoil';

import { streamEndpointQueryParamKeys } from '../../universal/config/app';
import { FeatureToggle } from '../../universal/config/feature-toggles';
import {
ApiPristineResponse,
ApiResponse,
apiPristineResult,
} from '../../universal/helpers/api';
import {
AppState,
AppStateKey,
BagThema,
} from '../../universal/types/App.types';
import { ApiResponse, apiPristineResult } from '../../universal/helpers/api';
import { AppState, BagThema } from '../../universal/types/App.types';
import { PRISTINE_APPSTATE, createAllErrorState } from '../AppState';
import { BFFApiUrls } from '../config/api';
import { transformSourceData } from '../data-transform/appState';
import { captureMessage } from '../helpers/monitoring';
import { useDataApi } from './api/useDataApi';
import { useProfileTypeValue } from './useProfileType';
import { SSE_ERROR_MESSAGE, useSSE } from './useSSE';
import { SSE_CLOSE_MESSAGE, SSE_ERROR_MESSAGE, useSSE } from './useSSE';
import { entries } from '../../universal/helpers/utils';

const fallbackServiceRequestOptions = {
Expand All @@ -32,6 +24,11 @@ export const appStateAtom = atom<AppState>({
default: PRISTINE_APPSTATE as AppState,
});

export const appStateReadyAtom = atom<boolean>({
key: 'appStateReady',
default: false,
});

interface useAppStateFallbackServiceProps {
profileType: ProfileType;
isEnabled: boolean;
Expand All @@ -47,6 +44,7 @@ export function useAppStateFallbackService({
fallbackServiceRequestOptions,
null
);
const setIsAppStateReady = useSetAppStateReady();

const appStateError = useCallback(
(message: string) => {
Expand Down Expand Up @@ -82,13 +80,14 @@ export function useAppStateFallbackService({
setAppState((appState) => {
return Object.assign({}, appState, transformSourceData(api.data));
});
setIsAppStateReady(true);
} else if (api.isError) {
// If everything fails, this is the final state update.
const errorMessage =
'Services.all endpoint could not be reached or returns an error.';
appStateError(errorMessage);
}
}, [api, appStateError, setAppState, isEnabled]);
}, [api, appStateError, setAppState, isEnabled, setIsAppStateReady]);
}

export function addParamsToStreamEndpoint(
Expand Down Expand Up @@ -133,6 +132,7 @@ export function useAppStateRemote() {

const profileType = useProfileTypeValue();
const [appState, setAppState] = useRecoilState(appStateAtom);
const setIsAppStateReady = useSetAppStateReady();

// First retrieve all the services specified in the BFF, after that Only retrieve incremental updates
const useIncremental = useRef(false);
Expand All @@ -142,25 +142,25 @@ export function useAppStateRemote() {
}, []);

// The callback is fired on every incoming message from the EventSource.
const onEvent = useCallback(
(messageData: typeof SSE_ERROR_MESSAGE | object) => {
if (messageData && messageData !== SSE_ERROR_MESSAGE) {
const transformedMessageData = transformSourceData(
typeof messageData === 'object' ? messageData : null
);
setAppState((appState) => {
const appStateUpdated = {
...appState,
...transformedMessageData,
};
return appStateUpdated;
});
} else if (messageData === SSE_ERROR_MESSAGE) {
setFallbackServiceEnabled(true);
}
},
[]
);
const onEvent = useCallback((messageData: string | object) => {
if (typeof messageData === 'object') {
const transformedMessageData = transformSourceData(messageData);
setAppState((appState) => {
const appStateUpdated = {
...appState,
...transformedMessageData,
};
return appStateUpdated;
});
} else if (messageData === SSE_ERROR_MESSAGE) {
setFallbackServiceEnabled(true);
} else if (messageData === SSE_CLOSE_MESSAGE) {
setIsAppStateReady(true);
} else {
// eslint-disable-next-line no-console
console.log('event source', messageData);
}
}, []);

useSSE({
path: streamEndpoint,
Expand All @@ -186,47 +186,12 @@ export function useAppStateSetter() {
return useRecoilState(appStateAtom)[1];
}

export function isAppStateReady(
appState: AppState,
pristineAppState: AppState,
profileType: ProfileType
) {
const isLegacyProfileType = ['private', 'commercial'].includes(profileType);
const profileStates = Object.entries(appState).filter(
([appStateKey, state]) => {
const key = appStateKey as AppStateKey;
const stateConfig = pristineAppState[key] as ApiPristineResponse<unknown>;

const isProfileMatch =
(isLegacyProfileType && !stateConfig?.profileTypes?.length) ||
stateConfig?.profileTypes?.includes(profileType);

// NOTE: The appState keys ending with _BAG are not considered a fixed/known portion of the appstate.
if (!stateConfig && !key.endsWith('_BAG')) {
captureMessage(`unknown stateConfig key: ${appStateKey}`);
}

// If we encounter an unknown stateConfig we treat the state to be ready so we don't block the isReady completely.
return isProfileMatch && (!!stateConfig?.isActive || !stateConfig);
}
);

return (
!!profileStates.length &&
profileStates.every(([appStateKey, state]) => {
return typeof state !== 'undefined' && state.status !== 'PRISTINE';
})
);
function useSetAppStateReady() {
return useRecoilState(appStateReadyAtom)[1];
}

export function useAppStateReady() {
const appState = useAppStateGetter();
const profileType = useProfileTypeValue();

return useMemo(
() => isAppStateReady(appState, PRISTINE_APPSTATE, profileType),
[appState, profileType]
);
return useRecoilValue(appStateReadyAtom);
}

export interface AppStateBagApiParams {
Expand Down
Loading
Loading