react-app/src/pages/ErrorFallback.tsx

Summary

Maintainability
A
0 mins
Test Coverage
import BaseLayout from '@/components/layout/BaseLayout';
import appTheme from '@/themes/appTheme';
import { Button, Grid, IconButton, SxProps, TextField, Typography } from '@mui/material';
import React, { useEffect, useState } from 'react';
import errorImage from '@/assets/images/error.svg';
import TaskAltIcon from '@mui/icons-material/TaskAlt';
import CloseIcon from '@mui/icons-material/Close';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
import { useSSO } from '@bcgov/citz-imb-sso-react';
import usePimsApi from '@/hooks/usePimsApi';
import { useNavigate } from 'react-router-dom';
import { LoadingButton } from '@mui/lab';
import useDataSubmitter from '@/hooks/useDataSubmitter';
import { FetchResponse } from '@/hooks/useFetch';

/**
 * Renders an error fallback component that displays an error message and provides options for handling the error.
 *
 * @param {Object} props - The component props.
 * @param {Error} props.error - The error object that caused the fallback.
 * @param {Function} props.resetErrorBoundary - A function to reset the error boundary and retry the render.
 * @returns {JSX.Element} The rendered error fallback component.
 */
const ErrorFallback = ({ error, resetErrorBoundary }) => {
  // Call resetErrorBoundary() to reset the error boundary and retry the render.
  const [state, setState] = useState<string>('');
  const [text, setText] = useState<string>('');
  const sso = useSSO();
  const api = usePimsApi();
  const navigate = useNavigate();
  const errorTracker = JSON.parse(sessionStorage.getItem('errorTracker'));
  const { submit, submitting } = useDataSubmitter(api.reports.postErrorReport);

  // If errorTracker changes, we navigate to home if an error has occurred more than 0 times on this page.
  // The first time, it's okay to show this page.
  useEffect(() => {
    if (
      errorTracker &&
      errorTracker.count > 0 &&
      errorTracker.location === window.location.pathname
    ) {
      // Removes existing cookie from this pathname. Mostly used for tables.
      sessionStorage.removeItem(window.location.pathname.slice(1));
      // Reset error tracker count.
      sessionStorage.setItem(
        'errorTracker',
        JSON.stringify({
          count: 0,
          location: '/',
        }),
      );
      navigate('/');
      resetErrorBoundary();
    }
  }, [errorTracker]);

  const commonResultStyle = {
    display: 'flex',
    alignItems: 'center',
    width: 'fit-content',
    padding: '0.5em',
    borderRadius: '5px',
  };

  const getElement = () => {
    switch (state) {
      case 'success':
        setTimeout(() => {
          resetErrorBoundary();
        }, 3000);
        return (
          <Grid
            item
            sx={{
              ...commonResultStyle,
              backgroundColor: appTheme.palette.success.light,
              color: appTheme.palette.text.secondary,
            }}
          >
            <TaskAltIcon sx={{ marginRight: '0.5em', color: appTheme.palette.success.main }} />
            <Typography sx={{ marginRight: '4em' }}>Thank you for your feedback.</Typography>
            <IconButton
              onClick={() => {
                resetErrorBoundary();
              }}
            >
              <CloseIcon sx={{ color: appTheme.palette.text.secondary }} />
            </IconButton>
          </Grid>
        );
      case 'failure':
        setTimeout(() => {
          setState('report');
        }, 3000);
        return (
          <Grid
            item
            sx={{
              ...commonResultStyle,
              backgroundColor: appTheme.palette.error.light,
              color: appTheme.palette.error.contrastText,
            }}
          >
            <ErrorOutlineIcon
              sx={{ marginRight: '0.5em', color: appTheme.palette.error.contrastText }}
            />
            <Typography sx={{ marginRight: '4em' }}>Sorry. Please try again later.</Typography>
            <IconButton
              onClick={() => {
                setState('report');
              }}
              sx={{ '&:hover': { backgroundColor: appTheme.palette.error.dark } }}
            >
              <CloseIcon sx={{ color: appTheme.palette.error.contrastText }} />
            </IconButton>
          </Grid>
        );
      case 'report':
        return (
          <>
            <Grid item>
              <TextField
                value={text}
                multiline
                rows={6}
                sx={{
                  width: '100%',
                  marginBottom: '1em',
                }}
                placeholder="Describe the issue or give any desired feedback"
                onChange={(e) => {
                  setText(e.target.value);
                }}
              ></TextField>
            </Grid>
            <Grid
              item
              sx={{
                justifyContent: 'space-between',
                display: 'flex',
              }}
            >
              <Button
                variant="text"
                onClick={() => {
                  setState('landing');
                }}
                sx={{ marginRight: '1em', width: '7.5em' }}
                size="large"
              >
                Cancel
              </Button>
              <LoadingButton
                variant="contained"
                loading={submitting}
                onClick={() => {
                  submit({
                    user: sso.user,
                    error: {
                      message: error.message,
                      stack: error.stack,
                    },
                    userMessage: text,
                    timestamp: new Date().toLocaleString(),
                    url: window.location.href,
                  })
                    .then((res: FetchResponse) => {
                      if (res.status === 200) {
                        setState('success');
                        setText('');
                        return;
                      }
                      setState('failure');
                    })
                    .catch((e) => {
                      console.error(e);
                      setState('failure');
                    });
                }}
                sx={{ marginRight: '1em', width: '7.5em' }}
                size="large"
              >
                Send
              </LoadingButton>
            </Grid>
          </>
        );
      default:
        return (
          <Grid item>
            <Button
              variant="contained"
              onClick={() => {
                // Set that we've experienced one error at this location already.
                sessionStorage.setItem(
                  'errorTracker',
                  JSON.stringify({
                    count: 1,
                    location: window.location.pathname,
                  }),
                );
                resetErrorBoundary();
              }}
              sx={{ marginRight: '1em', width: '7.5em' }}
              size="large"
            >
              Reload
            </Button>
            <Button
              variant="outlined"
              onClick={() => {
                setState('report');
              }}
              size="large"
            >
              Report a problem
            </Button>
          </Grid>
        );
    }
  };

  const gridStyle: SxProps = {
    alignItems: 'center',
    justifyContent: 'center',
    display: 'flex',
  };
  return (
    <BaseLayout>
      <Grid
        container
        sx={{
          height: '95%',
          display: 'flex',
          alignItems: 'center',
          width: '100%',
          maxWidth: '1200px',
          margin: '0 auto',
        }}
      >
        <Grid
          item
          container
          sm={12}
          md={6}
          sx={{ ...gridStyle, display: 'inline-block', margin: '0 2em', marginBottom: '2em' }}
        >
          <Grid
            item
            sx={{
              margin: '3em 0',
            }}
          >
            <Typography variant="h1">Oops, something went wrong...</Typography>
          </Grid>
          <Grid
            item
            sx={{
              margin: '3em 0',
            }}
          >
            <Typography
              variant="caption"
              sx={{
                fontSize: '1.2rem',
                color: appTheme.palette.text.disabled,
              }}
            >
              The server encountered a temporary error and could not complete your request.
            </Typography>
          </Grid>
          {getElement()}
        </Grid>
        <Grid item sm={12} md={5} sx={gridStyle}>
          <img
            src={errorImage}
            style={{
              height: '500px',
              maxWidth: '100%',
            }}
          />
        </Grid>
      </Grid>
    </BaseLayout>
  );
};

export default ErrorFallback;