Skip to content

Commit

Permalink
feat: improve error and loading handling (#1869)
Browse files Browse the repository at this point in the history
  • Loading branch information
stepan662 committed Aug 18, 2023
1 parent b5cff3f commit 980ed6e
Show file tree
Hide file tree
Showing 88 changed files with 498 additions and 549 deletions.
21 changes: 21 additions & 0 deletions e2e/cypress/common/errorHandling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export type Config = {
endpoint: string;
method?: string;
statusCode: number;
body?: any;
};

export function simulateError({ method, endpoint, statusCode, body }: Config) {
cy.intercept(
{
method: method,
url: `*${endpoint}*`,
},
{ statusCode, body }
);
}

export function tryCreateProject(name: string) {
cy.gcy('project-name-field').type('Test');
cy.gcy('global-form-save-button').click();
}
59 changes: 59 additions & 0 deletions e2e/cypress/e2e/errorHandling/errorHandlingGet.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { HOST } from '../../common/constants';
import 'cypress-file-upload';
import { projectTestData } from '../../common/apiCalls/testData/testData';
import { login } from '../../common/apiCalls/common';
import { assertMessage } from '../../common/shared';
import { simulateError } from '../../common/errorHandling';

describe('Error handling', () => {
beforeEach(() => {
projectTestData.clean();
projectTestData.generate();
login('cukrberg@facebook.com', 'admin');
});

it('Handles not found error', () => {
simulateError({
method: 'get',
endpoint: 'projects-with-stats',
statusCode: 404,
});
cy.visit(`${HOST}`);
assertMessage('Not found');
});

it('Handles permissions error', () => {
simulateError({
method: 'get',
endpoint: 'projects-with-stats',
statusCode: 403,
});
cy.visit(`${HOST}`);
assertMessage('Your permissions are not sufficient for this operation.');
});

it('Handles 404 by redirect', () => {
simulateError({
method: 'get',
endpoint: 'facebook',
statusCode: 404,
});
cy.visit(`${HOST}/organizations/facebook/profile`);
assertMessage('Not found');
cy.url().should('include', '/projects');
});

it('Handles 401 by logout', () => {
simulateError({
method: 'get',
endpoint: 'facebook',
statusCode: 401,
});
cy.visit(`${HOST}/organizations/facebook/profile`);
cy.url().should('include', '/login');
});

after(() => {
projectTestData.clean();
});
});
55 changes: 55 additions & 0 deletions e2e/cypress/e2e/errorHandling/errorHandlingMutation.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { HOST } from '../../common/constants';
import 'cypress-file-upload';
import { projectTestData } from '../../common/apiCalls/testData/testData';
import { login } from '../../common/apiCalls/common';
import { assertMessage } from '../../common/shared';
import { simulateError, tryCreateProject } from '../../common/errorHandling';

describe('Error handling', () => {
beforeEach(() => {
projectTestData.clean();
projectTestData.generate();
login('cukrberg@facebook.com', 'admin');
});

it('Handles project creation general error', () => {
simulateError({
method: 'post',
endpoint: 'projects',
statusCode: 400,
body: {
code: 'user_has_no_project_access',
},
});
cy.visit(`${HOST}/projects/add`);
tryCreateProject('Test');
cy.contains('User has no access to the project').should('be.visible');
});

it('Handles project creation 403 error', () => {
simulateError({
method: 'post',
endpoint: 'projects',
statusCode: 403,
});
cy.visit(`${HOST}/projects/add`);
tryCreateProject('Test');
assertMessage('Your permissions are not sufficient for this operation');
});

it('Handles project creation 404 error without redirect', () => {
simulateError({
method: 'post',
endpoint: 'projects',
statusCode: 404,
});
cy.visit(`${HOST}/projects/add`);
tryCreateProject('Test');
assertMessage('Not found');
cy.url().should('contain', '/projects/add');
});

after(() => {
projectTestData.clean();
});
});
4 changes: 3 additions & 1 deletion webapp/src/component/GlobalLoading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ const StyledSmoothProgress = styled(SmoothProgress)`
export const GlobalLoading: React.FC = () => {
const { loading, spinners } = useContext(LoadingContext);

return <StyledSmoothProgress loading={Boolean(loading && spinners === 0)} />;
return (
<StyledSmoothProgress loading={Boolean(loading && spinners === 0)} global />
);
};

export const LoadingProvider: React.FC = (props) => {
Expand Down
15 changes: 11 additions & 4 deletions webapp/src/component/MandatoryDataProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,22 @@ import { useGlobalLoading } from './GlobalLoading';
import { PostHog } from 'posthog-js';
import { getUtmParams } from 'tg.fixtures/utmCookie';
import { useIdentify } from 'tg.hooks/useIdentify';
import { useIsFetching, useIsMutating } from 'react-query';

const POSTHOG_INITIALIZED_WINDOW_PROPERTY = 'postHogInitialized';
export const MandatoryDataProvider = (props: any) => {
const config = useConfig();
const userData = useUser();
const isLoading = useGlobalContext((v) => v.isLoading);
const isFetching = useGlobalContext((v) => v.isFetching);

const isLoading = useGlobalContext((c) => c.isLoading);
const isFetching = useGlobalContext((c) => c.isFetching);

const isGloballyFetching = useIsFetching();
const isGloballyMutating = useIsMutating();

useGlobalLoading(
Boolean(isGloballyFetching || isGloballyMutating || isFetching)
);

useIdentify(userData?.id);

Expand Down Expand Up @@ -70,8 +79,6 @@ export const MandatoryDataProvider = (props: any) => {
}
}, [userData?.id]);

useGlobalLoading(isFetching || isLoading);

if (isLoading) {
return null;
} else {
Expand Down
3 changes: 0 additions & 3 deletions webapp/src/component/NamespaceSelector/NamespaceSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ export const NamespaceSelector: React.FC<Props> = ({
url: '/v2/projects/{projectId}/used-namespaces',
method: 'get',
path: { projectId: project.id },
fetchOptions: {
disableNotFoundHandling: true,
},
options: {
enabled: !namespaceData,
},
Expand Down
24 changes: 14 additions & 10 deletions webapp/src/component/SensitiveOperationAuthDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { container } from 'tsyringe';
import { GlobalActions } from 'tg.store/global/GlobalActions';
import { useSelector } from 'react-redux';
import { AppState } from 'tg.store/index';
import { useUser } from 'tg.globalContext/helpers';
import { useApiMutation } from 'tg.service/http/useQueryApi';
import {
Box,
Dialog,
Expand All @@ -12,9 +8,14 @@ import {
Typography,
} from '@mui/material';
import { T } from '@tolgee/react';

import { GlobalActions } from 'tg.store/global/GlobalActions';
import { AppState } from 'tg.store/index';
import { useUser } from 'tg.globalContext/helpers';
import { useApiMutation } from 'tg.service/http/useQueryApi';
import { StandardForm } from './common/form/StandardForm';
import { TextField } from './common/form/fields/TextField';
import React from 'react';
import { useLoadingRegister } from './GlobalLoading';

type Value = { otp?: string; password?: string };

Expand All @@ -25,6 +26,8 @@ export const SensitiveOperationAuthDialog = () => {
);
const user = useUser();

const dialogOpen = afterActions.length > 0;

const superTokenMutation = useApiMutation({
url: '/v2/user/generate-super-token',
method: 'post',
Expand All @@ -38,10 +41,14 @@ export const SensitiveOperationAuthDialog = () => {
},
},
fetchOptions: {
disableAuthHandling: true,
disableAutoErrorHandle: true,
},
});

// prevent loading indicator as original requests are pending in the background
// but we are not interested in those
useLoadingRegister(dialogOpen && !superTokenMutation.isLoading);

const onCancel = () => {
afterActions.forEach((action) => {
action.onCancel();
Expand All @@ -50,10 +57,7 @@ export const SensitiveOperationAuthDialog = () => {
};

return (
<Dialog
open={afterActions.length > 0}
data-cy="sensitive-protection-dialog"
>
<Dialog open={dialogOpen} data-cy="sensitive-protection-dialog">
<DialogTitle>
<T keyName="sensitive-authentication-dialog-title" />
</DialogTitle>
Expand Down
9 changes: 8 additions & 1 deletion webapp/src/component/SmoothProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { useDebounce } from 'use-debounce';
import { styled } from '@mui/material';
import clsx from 'clsx';
import { useLoadingRegister } from './GlobalLoading';

const StyledProgress = styled('div')<{ loading?: string; finish?: string }>`
height: 4px;
Expand All @@ -22,12 +23,18 @@ const StyledProgress = styled('div')<{ loading?: string; finish?: string }>`
type Props = {
loading: boolean;
className?: string;
global?: boolean;
};

export const SmoothProgress: React.FC<Props> = ({ loading, className }) => {
export const SmoothProgress: React.FC<Props> = ({
loading,
className,
global,
}) => {
const [stateLoading, setStateLoading] = useState(false);
const [smoothedLoading] = useDebounce(stateLoading, 100);
const [progress, setProgress] = useState(0);
useLoadingRegister(!global && loading);
useEffect(() => {
setStateLoading(Boolean(loading));
if (loading) {
Expand Down
9 changes: 9 additions & 0 deletions webapp/src/component/SpinnerProgress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { CircularProgress as MuiCircularProgress } from '@mui/material';
import { useLoadingRegister } from './GlobalLoading';

type Props = React.ComponentProps<typeof MuiCircularProgress>;

export const SpinnerProgress = (props: Props) => {
useLoadingRegister(true);
return <MuiCircularProgress {...props} />;
};
4 changes: 2 additions & 2 deletions webapp/src/component/common/BoxLoading.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { default as React } from 'react';
import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import { SpinnerProgress } from 'tg.component/SpinnerProgress';

export function BoxLoading(props: React.ComponentProps<typeof Box>) {
return (
Expand All @@ -11,7 +11,7 @@ export function BoxLoading(props: React.ComponentProps<typeof Box>) {
p={4}
{...props}
>
<CircularProgress />
<SpinnerProgress />
</Box>
);
}
9 changes: 3 additions & 6 deletions webapp/src/component/common/EmptyListMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { ComponentProps, default as React, FunctionComponent } from 'react';
import { Box, CircularProgress, Fade, styled } from '@mui/material';
import { Box, Fade, styled } from '@mui/material';
import { T } from '@tolgee/react';

import { SadEmotionMessage, SadEmotionMessageProps } from './SadEmotionMessage';
import { useLoadingRegister } from 'tg.component/GlobalLoading';
import { SpinnerProgress } from 'tg.component/SpinnerProgress';

const ProgressWrapper = styled('div')`
position: absolute;
display: flex;
top: 0px;
height: ${(props: any) => props.height || '400px'};
Expand All @@ -28,8 +27,6 @@ export const EmptyListMessage: FunctionComponent<Props> = ({
wrapperProps,
...otherProps
}) => {
useLoadingRegister(loading);

wrapperProps = {
...wrapperProps,
py: wrapperProps?.py || 8,
Expand All @@ -46,7 +43,7 @@ export const EmptyListMessage: FunctionComponent<Props> = ({
</Fade>
<Fade in={loading} mountOnEnter unmountOnExit>
<ProgressWrapper>
<CircularProgress />
<SpinnerProgress />
</ProgressWrapper>
</Fade>
</Box>
Expand Down
13 changes: 1 addition & 12 deletions webapp/src/component/common/avatar/ProfileAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ import { AvatarImg } from './AvatarImg';
import { AvatarEditMenu } from './AvatarEditMenu';
import { AvatarEditDialog } from './AvatarEditDialog';
import { useConfig } from 'tg.globalContext/helpers';
import { parseErrorResponse } from 'tg.fixtures/errorFIxtures';
import { components } from 'tg.service/apiSchema.generated';
import { TranslatedError } from 'tg.translationTools/TranslatedError';

export type AvatarOwner = {
name?: string;
Expand Down Expand Up @@ -152,16 +150,7 @@ export const ProfileAvatar: FC<{
setAvatarMenuAnchorEl(undefined);
}}
onRemove={async () => {
try {
await props.onRemove();
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
const parsed = parseErrorResponse(e);
parsed.forEach((error) =>
messageService.error(<TranslatedError code={error} />)
);
}
await props.onRemove();
setAvatarMenuAnchorEl(undefined);
}}
onClose={() => setAvatarMenuAnchorEl(undefined)}
Expand Down
9 changes: 3 additions & 6 deletions webapp/src/component/common/form/LoadingButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { ComponentProps, FunctionComponent } from 'react';
import { ComponentProps, FunctionComponent } from 'react';
import { Box, Button } from '@mui/material';
import CircularProgress from '@mui/material/CircularProgress';
import { useLoadingRegister } from 'tg.component/GlobalLoading';
import { SpinnerProgress } from 'tg.component/SpinnerProgress';

const LoadingButton: FunctionComponent<
ComponentProps<typeof Button> & { loading?: boolean }
Expand All @@ -10,8 +9,6 @@ const LoadingButton: FunctionComponent<

const isDisabled = loading || disabled;

useLoadingRegister(loading);

return (
<Button disabled={isDisabled} {...otherProps}>
{loading && (
Expand All @@ -26,7 +23,7 @@ const LoadingButton: FunctionComponent<
justifyContent="center"
data-cy="global-loading"
>
<CircularProgress size={20} />
<SpinnerProgress size={20} />
</Box>
)}
{children}
Expand Down
Loading

0 comments on commit 980ed6e

Please sign in to comment.