Skip to content

Commit

Permalink
moved error components to react-error
Browse files Browse the repository at this point in the history
  • Loading branch information
arietrouw committed Oct 25, 2024
1 parent b0fc60f commit dd7eec8
Show file tree
Hide file tree
Showing 23 changed files with 578 additions and 9 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"rdns",
"reinit",
"rfdc",
"Rollbar",
"Tapjoy",
"Tiktok",
"Trouw",
Expand Down
12 changes: 9 additions & 3 deletions packages/error/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@
"packages/*"
],
"dependencies": {
"@xylabs/react-flexbox": "workspace:^"
"@rollbar/react": "0.12.0-beta",
"@xylabs/react-button": "workspace:^",
"@xylabs/react-flexbox": "workspace:^",
"prop-types": "^15.8.1",
"rollbar": "^2.26.4"
},
"devDependencies": {
"@mui/icons-material": "^6.1.5",
Expand All @@ -48,16 +52,18 @@
"@xylabs/tsconfig-react": "^4.2.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.27.0",
"storybook": "^8.3.6",
"typescript": "^5.6.3"
},
"peerDependencies": {
"@mui/icons-material": "^6",
"@mui/material": "^6",
"react": "^18",
"react-dom": "^18"
"react-dom": "^18",
"react-router-dom": "^6"
},
"publishConfig": {
"access": "public"
}
}
}
61 changes: 61 additions & 0 deletions packages/error/src/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Typography } from '@mui/material'
import { FlexCol } from '@xylabs/react-flexbox'
import type { ErrorInfo, ReactNode } from 'react'
import React, { Component } from 'react'

export interface ErrorBoundaryProps {
children: ReactNode
// fallback as a static ReactNode value
fallback?: ReactNode
// fallback element that can receive the error as a prop
fallbackWithError?: (error: Error) => ReactNode
scope?: string
}

export interface ErrorBoundaryState {
error?: Error
}

export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = { error: undefined }
}

static getDerivedStateFromError(error: Error) {
return { error }
}

override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error(`${error}: ${errorInfo}`)
}

override render() {
if (this.state.error) {
if (this.props.fallbackWithError) {
return this.props.fallbackWithError(this.state.error)
}
return (
this.props.fallback ?? (
<FlexCol>
<Typography variant="h1">Something went wrong.</Typography>
{this.props.scope && (
<Typography variant="h2">
[
{this.props.scope}
]
</Typography>
)}
<Typography variant="body1">
[
{this.state.error?.message}
]
</Typography>
</FlexCol>
)
)
}

return this.props.children
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Alert, Button } from '@mui/material'
import type { Meta, StoryFn } from '@storybook/react'
import React from 'react'

import { ThrownErrorBoundary } from './ThrownErrorBoundary.tsx'

const StorybookEntry: Meta = {
component: ThrownErrorBoundary,
title: 'auth-service/ApiBoundary/ThrownErrorBoundary',
}

const Thrower: React.FC = () => {
const [shouldThrow, setShouldThrow] = React.useState(false)
if (shouldThrow) {
throw new Error('Test Error')
}
return (
<Button onClick={() => {
setShouldThrow(true)
}}
>
Throw Error
</Button>
)
}

const Template: StoryFn<typeof ThrownErrorBoundary> = ({ errorComponent }) => {
return (
<ThrownErrorBoundary errorComponent={errorComponent} boundaryName="StoryBook">
<Alert severity="info">Use React Dev Tools to trigger and error within the boundary</Alert>
<Thrower />
</ThrownErrorBoundary>
)
}

const Default = Template.bind({})
Default.args = {}

const CustomErrorComponent = Template.bind({})
CustomErrorComponent.args = {
errorComponent: e => (
<Alert severity="error">
Using Custom Error Component with error:
{(e as Error).message}
</Alert>
),
}

export { CustomErrorComponent, Default }

export default StorybookEntry
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useRollbar } from '@rollbar/react'
import type { ErrorInfo, ReactNode } from 'react'
import React, { Component } from 'react'
import type Rollbar from 'rollbar'

import { useErrorReporter } from '../../contexts/index.ts'
import type { ErrorEx } from '../ErrorEx.ts'
import { ErrorRender } from '../ErrorRender/index.ts'

export interface ThrownErrorBoundaryProps<T = void> {
boundaryName?: string
children: ReactNode
errorComponent?: (e: ErrorEx<T>, boundaryName?: string) => ReactNode
rethrow?: boolean
rollbar?: Rollbar
scope?: string
title?: string
}

export interface ThrownErrorBoundaryState<T = void> {
errorEx?: ErrorEx<T>
}

class ThrownErrorBoundaryInner<T> extends Component<ThrownErrorBoundaryProps<T>, ThrownErrorBoundaryState<T>> {
override state: ThrownErrorBoundaryState<T> = { errorEx: undefined }

static getDerivedStateFromError<T = void>(error: ErrorEx<T>) {
return { hasError: true, xyoError: this.normalizeError<T>(error) } as ThrownErrorBoundaryState<T>
}

static normalizeError<T>(_error: ErrorEx<T>): T {
throw new Error('Method not implemented.')
}

override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
const { rethrow, rollbar } = this.props
const { errorEx } = this.state

rollbar?.error(error)

console.error('Error:', errorEx, errorInfo)
if (rethrow) {
throw error
}
}

override render() {
const { errorEx } = this.state
const {
children, boundaryName, errorComponent, scope, title,
} = this.props
if (errorEx) {
if (errorComponent) {
return errorComponent(errorEx)
}
return <ErrorRender<T> error={errorEx} errorContext={`${boundaryName} Boundary`} scope={scope} title={title} />
}

return children
}
}

// calling the hook outside of the component since only can be called in functional component
export function ThrownErrorBoundary<T = void>({ rollbar, ...props }: ThrownErrorBoundaryProps<T>): JSX.Element {
const { rollbar: rollbarErrorReporter } = useErrorReporter()
let rollbarFromHook: Rollbar | undefined
// safely call the hook
try {
rollbarFromHook = useRollbar()
} catch {}
return <ThrownErrorBoundaryInner<T> rollbar={rollbar ?? rollbarErrorReporter ?? rollbarFromHook} {...props} />
}
1 change: 1 addition & 0 deletions packages/error/src/components/ErrorBoundary/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ThrownErrorBoundary.tsx'
1 change: 1 addition & 0 deletions packages/error/src/components/ErrorEx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type ErrorEx<T = void> = T extends void ? Error : T | Error
41 changes: 41 additions & 0 deletions packages/error/src/components/ErrorRender/ErrorAlert.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Meta, StoryFn } from '@storybook/react'
import React from 'react'

import { ErrorAlert } from './ErrorAlert.tsx'

const StorybookEntry: Meta = {
argTypes: {},
component: ErrorAlert,
parameters: { docs: { page: null } },
title: 'error/ErrorAlert',
}

const Template: StoryFn<typeof ErrorAlert> = (props) => {
return <ErrorAlert {...props} />
}

const Default = Template.bind({})
Default.args = {}

const WithTitle = Template.bind({})
WithTitle.args = { title: 'Oh No!' }

const WithError = Template.bind({})
WithError.args = { error: 'An error happened' }

const WithScope = Template.bind({})
WithScope.args = { scope: 'Storybook' }

const WithErrorAndScope = Template.bind({})
WithErrorAndScope.args = { error: 'An error happened', scope: 'Storybook' }

const WithErrorAndScopeAndTitle = Template.bind({})
WithErrorAndScopeAndTitle.args = {
error: 'An error happened', scope: 'Storybook', title: 'Oh No!',
}

export {
Default, WithError, WithErrorAndScope, WithErrorAndScopeAndTitle, WithScope, WithTitle,
}

export default StorybookEntry
56 changes: 56 additions & 0 deletions packages/error/src/components/ErrorRender/ErrorAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ExitToApp as ExitIcon } from '@mui/icons-material'
import type { AlertProps } from '@mui/material'
import {
Alert, AlertTitle, Typography,
} from '@mui/material'
import { ButtonEx } from '@xylabs/react-button'
import React from 'react'

export interface ErrorAlertProps<T = void> extends AlertProps {
error?: T | Error | string
onCancel?: () => void
scope?: string
}

export function ErrorAlert<T = void>({
title = 'Whoops! Something went wrong',
onCancel,
error = 'An unknown error occurred',
scope,
...props
}: ErrorAlertProps<T>): JSX.Element {
return (
<Alert severity="error" {...props}>
<AlertTitle>{title}</AlertTitle>
{scope
? (
<div>
<Typography variant="caption" mr={0.5} fontWeight="bold">
Scope:
</Typography>
<Typography variant="caption">{scope}</Typography>
</div>
)
: null}
<div>
<Typography variant="caption" mr={0.5} fontWeight="bold">
Error:
</Typography>
<Typography variant="caption">{typeof error === 'string' ? error : (error as Error)?.message}</Typography>
</div>
{onCancel
? (
<ButtonEx
variant="outlined"
size="small"
onClick={onCancel}
position="absolute"
style={{ right: 8, top: 8 }}
>
<ExitIcon fontSize="small" />
</ButtonEx>
)
: null}
</Alert>
)
}
16 changes: 16 additions & 0 deletions packages/error/src/components/ErrorRender/Props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { FlexBoxProps } from '@xylabs/react-flexbox'
import type { ReactNode } from 'react'
import type { Location } from 'react-router-dom'

import type { ErrorEx } from '../ErrorEx.ts'

export interface ErrorRenderProps<T = void> extends FlexBoxProps {
customError?: ReactNode
error?: ErrorEx<T>
errorContext?: string
noErrorDisplay?: boolean
noReAuth?: boolean
onCancel?: () => void
scope?: string
useLocation?: () => Location
}
44 changes: 44 additions & 0 deletions packages/error/src/components/ErrorRender/Render.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { FlexCol } from '@xylabs/react-flexbox'
import React, { useEffect } from 'react'

import { ErrorAlert } from './ErrorAlert.tsx'
import type { ErrorRenderProps } from './Props.ts'

export function ErrorRender<T = void>({
onCancel,
error,
noErrorDisplay = false,
customError = null,
children,
scope,
useLocation,
...props
}: ErrorRenderProps<T>): JSX.Element {
const location = useLocation?.()
useEffect(() => {
if (location) {
// ensure we end up at the same place we are now after logging in
location.state = { from: { pathname: globalThis.location.pathname } }
}
}, [location])

useEffect(() => {
if (error) {
globalThis.rollbar?.error(error)
}
}, [error])

return error
? (
<FlexCol alignItems="stretch" {...props}>
{noErrorDisplay
? customError
: (
<FlexCol alignItems="center" {...props}>
<ErrorAlert error={error} onCancel={onCancel} scope={scope} />
</FlexCol>
)}
</FlexCol>
)
: <>{children}</>
}
3 changes: 3 additions & 0 deletions packages/error/src/components/ErrorRender/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './ErrorAlert.tsx'
export * from './Props.ts'
export * from './Render.tsx'
Loading

0 comments on commit dd7eec8

Please sign in to comment.