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

fix: show a modal on failure of getInfo and getComponents #251

Merged
merged 17 commits into from
Jun 16, 2023
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
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