Skip to content

Commit

Permalink
Refactor theming of ErrorBoundary
Browse files Browse the repository at this point in the history
Refactoring to a function-component is not possible, as there is no
alternative to `componentDidCatch`, according to the [React Docs](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary):

> There is currently no way to write an error boundary as a function component.
  • Loading branch information
jamesricky committed Nov 29, 2023
1 parent eff7cf1 commit ec3d9f2
Showing 1 changed file with 167 additions and 72 deletions.
239 changes: 167 additions & 72 deletions packages/admin/admin/src/error/errorboundary/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { ChevronDown, ChevronRight, Error } from "@comet/admin-icons";
import { Alert, AlertProps, ComponentsOverrides, Theme, Typography } from "@mui/material";
import { WithStyles } from "@mui/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Alert as MuiAlert, AlertProps, ComponentsOverrides, Typography } from "@mui/material";
import { css, styled, Theme, useThemeProps } from "@mui/material/styles";
import * as React from "react";
import { FormattedMessage } from "react-intl";

import { ThemedComponentBaseProps } from "../../helpers/ThemedComponentBaseProps";

export type ErrorBoundaryClassKey =
| "alert"
| "message"
Expand All @@ -17,118 +17,213 @@ export type ErrorBoundaryClassKey =
| "exceptionSummaryTitle"
| "exceptionStackTrace";

type OwnerState = {
showDetails?: boolean;
};

export type ErrorBoundaryProps = React.PropsWithChildren<{
userErrorMessage?: React.ReactNode;
variant?: AlertProps["variant"];
icon?: AlertProps["icon"];
toggleDetailsOpenedIcon?: React.ReactNode;
toggleDetailsClosedIcon?: React.ReactNode;
key?: string | number;
}>;
}> &
ThemedComponentBaseProps<{
alert: typeof MuiAlert;
message: typeof Typography;
exceptionDetails: "details";
exceptionSummary: "summary";
exceptionSummaryIconOpened: "div";
exceptionSummaryIconClosed: "div";
exceptionSummaryTitle: typeof Typography;
exceptionStackTrace: typeof Typography;
}>;

interface IErrorBoundaryState {
error?: Error;
errorInfo?: React.ErrorInfo;
showDetails?: boolean;
}

class ErrorBoundary extends React.Component<ErrorBoundaryProps & WithStyles<ErrorBoundaryClassKey>, IErrorBoundaryState> {
constructor(props: ErrorBoundaryProps & WithStyles<ErrorBoundaryClassKey>) {
const Alert = styled(MuiAlert, {
name: "CometAdminErrorBoundary",
slot: "alert",
overridesResolver(_, styles) {
return [styles.alert];
},
})();

const Message = styled(Typography, {
name: "CometAdminErrorBoundary",
slot: "message",
overridesResolver(_, styles) {
return [styles.message];
},
})();

const ExceptionDetails = styled("details", {
name: "CometAdminErrorBoundary",
slot: "exceptionDetails",
overridesResolver(_, styles) {
return [styles.exceptionDetails];
},
})(
() => css`
white-space: pre-wrap;
`,
);

const ExceptionSummary = styled("summary", {
name: "CometAdminErrorBoundary",
slot: "exceptionSummary",
overridesResolver(_, styles) {
return [styles.exceptionSummary];
},
})(
({ theme }) => css`
display: flex;
align-items: center;
cursor: pointer;
outline: none;
padding-top: ${theme.spacing(2)};
&:first-of-type {
list-style-type: none;
}
`,
);

const ExceptionSummaryIconOpen = styled("div", {
name: "CometAdminErrorBoundary",
slot: "exceptionSummaryIconOpened",
overridesResolver(_, styles) {
return [styles.exceptionSummaryIcon, styles.exceptionSummaryIconOpened];
},
})<{ ownerState: OwnerState }>(
({ ownerState }) => css`
align-items: center;
display: flex;
${ownerState.showDetails &&
css`
display: none;
`}
`,
);

const ExceptionSummaryIconClosed = styled("div", {
name: "CometAdminErrorBoundary",
slot: "exceptionSummaryIconClosed",
overridesResolver(_, styles) {
return [styles.exceptionSummaryIcon, styles.exceptionSummaryIconClosed];
},
})<{ ownerState: OwnerState }>(
({ ownerState }) => css`
align-items: center;
display: none;
${ownerState.showDetails &&
css`
display: flex;
`}
`,
);

const ExceptionSummaryTitle = styled(Typography, {
name: "CometAdminErrorBoundary",
slot: "exceptionSummaryTitle",
overridesResolver(_, styles) {
return [styles.exceptionSummaryTitle];
},
})(
({ theme }) => css`
font-weight: ${theme.typography.fontWeightBold};
padding-left: ${theme.spacing(1)};
`,
);

const ExceptionStackTrace = styled(Typography, {
name: "CometAdminErrorBoundary",
slot: "exceptionStackTrace",
overridesResolver(_, styles) {
return [styles.exceptionStackTrace];
},
})();

export const ErrorBoundary = (inProps: ErrorBoundaryProps) => {
const props = useThemeProps({ props: inProps, name: "CometAdminErrorBoundary" });
return <CoreErrorBoundary {...props} />;
};

class CoreErrorBoundary extends React.Component<ErrorBoundaryProps, IErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = {};
}

public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
this.setState({ error, errorInfo });
this.setState((prev) => ({ ...prev, error, errorInfo }));
}

public render() {
const {
classes,
variant = "filled",
icon = <Error />,
toggleDetailsOpenedIcon = <ChevronRight fontSize="small" />,
toggleDetailsClosedIcon = <ChevronDown fontSize="small" />,
children,
slotProps,
userErrorMessage,
key,
...restProps
} = this.props;
const { error, errorInfo } = this.state;
const { error, errorInfo, showDetails } = this.state;

const ownerState: OwnerState = {
showDetails,
};

if (errorInfo != null) {
return (
<Alert variant={variant} icon={icon} severity="error" classes={{ root: classes.alert }}>
<Typography classes={{ root: classes.message }}>
{this.props.userErrorMessage ? (
this.props.userErrorMessage
<Alert variant={variant} icon={icon} severity="error" {...restProps} {...slotProps?.alert}>
<Message {...slotProps?.message}>
{userErrorMessage ? (
userErrorMessage
) : (
<FormattedMessage id="comet.error.abstractErrorMessage" defaultMessage="An error has occurred" />
)}
</Typography>
</Message>

{process.env.NODE_ENV === "development" && (
<details className={classes.exceptionDetails}>
<summary className={classes.exceptionSummary}>
<div className={`${classes.exceptionSummaryIcon} ${classes.exceptionSummaryIconOpened}`}>
<ExceptionDetails
onToggle={(e) => {
const showDetails = e.currentTarget.hasAttribute("open");
this.setState((prev) => ({ ...prev, showDetails }));
}}
{...slotProps?.exceptionDetails}
>
<ExceptionSummary {...slotProps?.exceptionSummary}>
<ExceptionSummaryIconOpen ownerState={ownerState} {...slotProps?.exceptionSummaryIconOpened}>
{toggleDetailsOpenedIcon}
</div>
<div className={`${classes.exceptionSummaryIcon} ${classes.exceptionSummaryIconClosed}`}>
</ExceptionSummaryIconOpen>
<ExceptionSummaryIconClosed ownerState={ownerState} {...slotProps?.exceptionSummaryIconClosed}>
{toggleDetailsClosedIcon}
</div>
<Typography classes={{ root: classes.exceptionSummaryTitle }}>{error != null && error.toString()}</Typography>
</summary>

<Typography classes={{ root: classes.exceptionStackTrace }}>{errorInfo.componentStack}</Typography>
</details>
</ExceptionSummaryIconClosed>
<ExceptionSummaryTitle {...slotProps?.exceptionSummaryTitle}>
{error != null && error.toString()}
</ExceptionSummaryTitle>
</ExceptionSummary>
<ExceptionStackTrace {...slotProps?.exceptionStackTrace}>{errorInfo.componentStack}__</ExceptionStackTrace>
</ExceptionDetails>
)}
</Alert>
);
}
return <>{this.props.children}</>;
return <>{children}</>;
}
}

const styles = (theme: Theme) =>
createStyles<ErrorBoundaryClassKey, ErrorBoundaryProps>({
alert: {},
message: {},
exceptionDetails: {
whiteSpace: "pre-wrap",
"&[open]": {
"& $exceptionSummaryIconClosed": {
display: "flex",
},
"& $exceptionSummaryIconOpened": {
display: "none",
},
},
},
exceptionSummary: {
display: "flex",
alignItems: "center",
cursor: "pointer",
outline: "none",
paddingTop: theme.spacing(2),
"&:first-of-type ": {
listStyleType: "none",
},
},
exceptionSummaryIcon: {
alignItems: "center",
},
exceptionSummaryIconOpened: {
display: "flex",
},
exceptionSummaryIconClosed: {
display: "none",
},
exceptionSummaryTitle: {
fontWeight: theme.typography.fontWeightBold,
paddingLeft: theme.spacing(1),
},
exceptionStackTrace: {},
});

const StyledErrorBoundary = withStyles(styles, { name: "CometAdminErrorBoundary" })(ErrorBoundary);

export { StyledErrorBoundary as ErrorBoundary };

declare module "@mui/material/styles" {
interface ComponentNameToClassKey {
CometAdminErrorBoundary: ErrorBoundaryClassKey;
Expand Down

0 comments on commit ec3d9f2

Please sign in to comment.