Skip to content

Commit

Permalink
fix: show a modal on failure of getInfo and getComponents
Browse files Browse the repository at this point in the history
refs: SHELL-92 (#251)
  • Loading branch information
CataldoMazzilli authored Jun 16, 2023
1 parent 4bf7746 commit 2df0658
Show file tree
Hide file tree
Showing 15 changed files with 327 additions and 162 deletions.
9 changes: 0 additions & 9 deletions src/boot/bootstrapper-context.ts

This file was deleted.

79 changes: 0 additions & 79 deletions src/boot/bootstrapper-router.tsx

This file was deleted.

87 changes: 70 additions & 17 deletions src/boot/bootstrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,79 @@
*/

import React, { FC, useEffect } from 'react';
import { unloadAllApps } from './app/load-apps';
import BootstrapperContextProvider from './bootstrapper-provider';
import BootstrapperRouter from './bootstrapper-router';
import { init } from './init';
import { BrowserRouter, Route, Switch, useHistory, useParams } from 'react-router-dom';
import {
ModalManager,
SnackbarManager,
useModal,
useSnackbar
} from '@zextras/carbonio-design-system';
import { useTranslation } from 'react-i18next';
import ShellI18nextProvider from './shell-i18n-provider';
import { ThemeProvider } from './theme-provider';
import { BASENAME, IS_STANDALONE } from '../constants';
import { Loader } from './loader';
import { NotificationPermissionChecker } from '../notification/NotificationPermissionChecker';
import AppLoaderMounter from './app/app-loader-mounter';
import ShellView from '../shell/shell-view';
import { useBridge } from '../store/context-bridge';
import { useAppStore } from '../store/app';
import { registerDefaultViews } from './app/default-views';

const Bootstrapper: FC = () => {
const ContextBridge = (): null => {
const history = useHistory();
const createSnackbar = useSnackbar();
const createModal = useModal();
useBridge({
functions: {
getHistory: () => history,
createSnackbar,
createModal
}
});
return null;
};

const StandaloneListener = (): null => {
const { route } = useParams<{ route?: string }>();
useEffect(() => {
if (route) useAppStore.setState({ standalone: route });
}, [route]);
return null;
};

const DefaultViewsRegister = (): null => {
const [t] = useTranslation();
useEffect(() => {
init();
return () => {
unloadAllApps();
};
}, []);
return (
<ThemeProvider>
<BootstrapperContextProvider>
<BootstrapperRouter />
</BootstrapperContextProvider>
</ThemeProvider>
);
registerDefaultViews(t);
}, [t]);
return null;
};

const Bootstrapper: FC = () => (
<ThemeProvider>
<ShellI18nextProvider>
<BrowserRouter basename={BASENAME}>
<SnackbarManager>
<ModalManager>
<Loader />
{IS_STANDALONE && (
<Switch>
<Route path={'/:route'}>
<StandaloneListener />
</Route>
</Switch>
)}
<DefaultViewsRegister />
<NotificationPermissionChecker />
<ContextBridge />
<AppLoaderMounter />
<ShellView />
</ModalManager>
</SnackbarManager>
</BrowserRouter>
</ShellI18nextProvider>
</ThemeProvider>
);

export default Bootstrapper;
16 changes: 0 additions & 16 deletions src/boot/init.ts

This file was deleted.

82 changes: 82 additions & 0 deletions src/boot/loader.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* SPDX-FileCopyrightText: 2023 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { rest } from 'msw';
import { act, screen } from '@testing-library/react';
import React from 'react';
import server from '../mocks/server';
import { GetComponentsJsonResponseBody } from '../mocks/handlers/components';
import { setup } from '../test/utils';
import { Loader } from './loader';
import { LOGIN_V3_CONFIG_PATH } from '../constants';

jest.mock('../workers');
jest.mock('../reporting/functions');

describe('Loader', () => {
test('If only getComponents request fails, the LoaderFailureModal appears', async () => {
// using getInfo and loginConfig default handlers
server.use(
rest.get<never, never, Partial<GetComponentsJsonResponseBody>>(
'/static/iris/components.json',
(req, res, ctx) =>
res(ctx.status(503, 'Controlled error: fail components.json request'), ctx.json({}))
)
);

setup(<Loader />);

const title = await screen.findByText('Something went wrong...');
act(() => {
jest.runOnlyPendingTimers();
});
expect(title).toBeVisible();
});

test('If only getInfo request fails, the LoaderFailureModal appears', async () => {
// TODO remove when SHELL-117 will be implemented
const actualConsoleError = console.error;
console.error = jest.fn<ReturnType<typeof console.error>, Parameters<typeof console.error>>(
(error, ...restParameter) => {
if (error === 'Unexpected end of JSON input') {
console.log('Controlled error', error, ...restParameter);
} else {
actualConsoleError(error, ...restParameter);
}
}
);
// using getComponents and loginConfig default handlers
server.use(
rest.post('/service/soap/GetInfoRequest', (req, res, ctx) =>
res(ctx.status(503, 'Controlled error: fail getInfo request'))
)
);

setup(<Loader />);

const title = await screen.findByText('Something went wrong...');
act(() => {
jest.runOnlyPendingTimers();
});
expect(title).toBeVisible();
});

test('If only loginConfig request fails, the LoaderFailureModal does not appear', async () => {
// using getComponents and getInfo default handlers
server.use(rest.post(LOGIN_V3_CONFIG_PATH, (req, res, ctx) => res(ctx.status(503))));

setup(<Loader />);

expect(screen.queryByText('Something went wrong...')).not.toBeInTheDocument();
});

test('If Loader requests do not fail, the LoaderFailureModal does not appear', async () => {
// using getComponents, loginConfig and getInfo default handlers

setup(<Loader />);

expect(screen.queryByText('Something went wrong...')).not.toBeInTheDocument();
});
});
93 changes: 93 additions & 0 deletions src/boot/loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* SPDX-FileCopyrightText: 2021 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { Modal, Padding, Text } from '@zextras/carbonio-design-system';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { find } from 'lodash';
import { useAppStore } from '../store/app';
import { getInfo } from '../network/get-info';
import { loadApps, unloadAllApps } from './app/load-apps';
import { loginConfig } from '../network/login-config';
import { goToLogin } from '../network/go-to-login';
import { getComponents } from '../network/get-components';

export function isPromiseRejectedResult<T>(
promiseSettledResult: PromiseSettledResult<T>
): promiseSettledResult is PromiseRejectedResult {
return promiseSettledResult.status === 'rejected';
}

export function isPromiseFulfilledResult<T>(
promiseSettledResult: PromiseSettledResult<T>
): promiseSettledResult is PromiseFulfilledResult<T> {
return promiseSettledResult.status === 'fulfilled';
}

type LoaderFailureModalProps = { open: boolean; closeHandler: () => void };

export const LoaderFailureModal = ({
open,
closeHandler
}: LoaderFailureModalProps): JSX.Element => {
const [t] = useTranslation();
const onConfirm = useCallback(() => window.location.reload(), []);
return (
<Modal
open={open}
showCloseIcon={false}
onSecondaryAction={goToLogin}
title={t('bootstrap.failure.modal.title', 'Something went wrong...')}
confirmLabel={t('bootstrap.failure.modal.confirmButtonLabel', 'refresh')}
secondaryActionLabel={t('bootstrap.failure.modal.secondaryButtonLabel', 'login page')}
onConfirm={onConfirm}
onClose={closeHandler}
>
<Padding all="small">
<Text overflow="break-word">
{t(
'bootstrap.failure.modal.body',
'Some technical issues occurred while processing your request. Please try to refresh the page or go back to the login page.'
)}
</Text>
</Padding>
</Modal>
);
};

export const Loader = (): JSX.Element => {
const [open, setOpen] = useState(false);
const closeHandler = useCallback(() => setOpen(false), []);

useEffect(() => {
Promise.allSettled([loginConfig(), getComponents(), getInfo()]).then(
(promiseSettledResultArray) => {
const [, getComponentsPromiseSettledResult, getInfoPromiseSettledResult] =
promiseSettledResultArray;

const promiseRejectedResult = find(
[getComponentsPromiseSettledResult, getInfoPromiseSettledResult],
isPromiseRejectedResult
);
if (promiseRejectedResult) {
if (typeof promiseRejectedResult.reason === 'string') {
console.error(promiseRejectedResult.reason);
} else if ('message' in promiseRejectedResult.reason) {
console.error(promiseRejectedResult.reason.message);
}
setOpen(true);
}
if (isPromiseFulfilledResult(getComponentsPromiseSettledResult)) {
loadApps(Object.values(useAppStore.getState().apps));
}
}
);
return () => {
unloadAllApps();
};
}, []);
return <LoaderFailureModal open={open} closeHandler={closeHandler} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { I18nextProvider } from 'react-i18next';
import { SHELL_APP_ID } from '../constants';
import { useI18nStore } from '../store/i18n';

const BootstrapperContextProvider: FC = ({ children }) => {
const ShellI18nextProvider: FC = ({ children }) => {
const i18n = useI18nStore((s) => s.instances[SHELL_APP_ID]);
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
};
export default BootstrapperContextProvider;
export default ShellI18nextProvider;
Loading

0 comments on commit 2df0658

Please sign in to comment.