dify/web/app/components/base/error-boundary/index.tsx
Stephen Zhou a84c2d36a3
style: format with vp fmt (#38803)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-12 15:57:46 +00:00

246 lines
7.7 KiB
TypeScript

'use client'
import type { ErrorInfo, ReactNode } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { RiAlertLine, RiBugLine } from '@remixicon/react'
import * as React from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { IS_DEV } from '@/config'
type ErrorBoundaryState = {
hasError: boolean
error: Error | null
errorInfo: ErrorInfo | null
errorCount: number
}
type ErrorBoundaryProps = {
children: ReactNode
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode)
onError?: (error: Error, errorInfo: ErrorInfo) => void
onReset?: () => void
showDetails?: boolean
className?: string
resetKeys?: Array<string | number>
resetOnPropsChange?: boolean
isolate?: boolean
enableRecovery?: boolean
customTitle?: string
customMessage?: string
}
type ErrorBoundaryCopy = {
componentStack: string
details: string
error: string
formatErrorCount: (count: number) => string
message: string
reload: string
title: string
tryAgain: string
}
// Internal class component for error catching
class ErrorBoundaryInner extends React.Component<
ErrorBoundaryProps & {
copy: ErrorBoundaryCopy
resetErrorBoundary: () => void
onResetKeysChange: (prevResetKeys?: Array<string | number>) => void
},
ErrorBoundaryState
> {
constructor(props: any) {
super(props)
this.state = {
hasError: false,
error: null,
errorInfo: null,
errorCount: 0,
}
}
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
return {
hasError: true,
error,
}
}
override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
if (IS_DEV) {
console.error('ErrorBoundary caught an error:', error)
console.error('Error Info:', errorInfo)
}
this.setState((prevState) => ({
errorInfo,
errorCount: prevState.errorCount + 1,
}))
if (this.props.onError) this.props.onError(error, errorInfo)
}
override componentDidUpdate(prevProps: any) {
const { resetKeys, resetOnPropsChange } = this.props
const { hasError } = this.state
if (hasError && prevProps.resetKeys !== resetKeys) {
if (resetKeys?.some((key, idx) => key !== prevProps.resetKeys?.[idx]))
this.props.resetErrorBoundary()
}
if (hasError && resetOnPropsChange && prevProps.children !== this.props.children)
this.props.resetErrorBoundary()
if (prevProps.resetKeys !== resetKeys) this.props.onResetKeysChange(prevProps.resetKeys)
}
override render() {
const { hasError, error, errorInfo, errorCount } = this.state
const {
fallback,
children,
showDetails = false,
className,
isolate = true,
enableRecovery = true,
customTitle,
customMessage,
copy,
resetErrorBoundary,
} = this.props
if (hasError && error) {
if (fallback) {
if (typeof fallback === 'function') return fallback(error, resetErrorBoundary)
return fallback
}
return (
<div
className={cn(
'border-state-critical-border bg-state-critical-hover-alt flex flex-col items-center justify-center rounded-lg border p-8',
isolate && 'min-h-[200px]',
className,
)}
>
<div className="mb-4 flex items-center gap-2">
<RiAlertLine className="text-state-critical-solid size-8" />
<h2 className="text-xl font-semibold text-text-primary">{customTitle || copy.title}</h2>
</div>
<p className="mb-6 text-center text-text-secondary">{customMessage || copy.message}</p>
{showDetails && errorInfo && (
<details className="mb-6 w-full max-w-2xl">
<summary className="mb-2 cursor-pointer text-sm font-medium text-text-tertiary hover:text-text-secondary">
<span className="inline-flex items-center gap-1">
<RiBugLine className="size-4" />
{copy.details}
</span>
</summary>
<div className="rounded-lg bg-gray-100 p-4">
<div className="mb-2">
<span className="font-mono text-xs font-semibold text-gray-600">
{copy.error}
</span>
<pre className="mt-1 overflow-auto font-mono text-xs whitespace-pre-wrap text-gray-800">
{error.toString()}
</pre>
</div>
{errorInfo && (
<div>
<span className="font-mono text-xs font-semibold text-gray-600">
{copy.componentStack}
</span>
<pre className="mt-1 max-h-40 overflow-auto font-mono text-xs whitespace-pre-wrap text-gray-700">
{errorInfo.componentStack}
</pre>
</div>
)}
{errorCount > 1 && (
<div className="mt-2 text-xs text-gray-600">
{copy.formatErrorCount(errorCount)}
</div>
)}
</div>
</details>
)}
{enableRecovery && (
<div className="flex gap-3">
<Button variant="primary" size="small" onClick={resetErrorBoundary}>
{copy.tryAgain}
</Button>
<Button variant="secondary" size="small" onClick={() => window.location.reload()}>
{copy.reload}
</Button>
</div>
)}
</div>
)
}
return children
}
}
// Main functional component wrapper
const ErrorBoundary: React.FC<ErrorBoundaryProps> = (props) => {
const { t } = useTranslation()
const [errorBoundaryKey, setErrorBoundaryKey] = useState(0)
const resetKeysRef = useRef(props.resetKeys)
const prevResetKeysRef = useRef<Array<string | number> | undefined>(undefined)
const copy = {
componentStack: t(($) => $['errorBoundary.componentStack'], { ns: 'common' }),
details: t(($) => $['errorBoundary.details'], { ns: 'common' }),
error: `${t(($) => $.error, { ns: 'common' })}:`,
formatErrorCount: (count: number) =>
t(($) => $['errorBoundary.errorCount'], { ns: 'common', count }),
message: t(($) => $['errorBoundary.message'], { ns: 'common' }),
reload: t(($) => $['errorBoundary.reloadPage'], { ns: 'common' }),
title: t(($) => $['errorBoundary.title'], { ns: 'common' }),
tryAgain: t(($) => $['errorBoundary.tryAgain'], { ns: 'common' }),
}
const resetErrorBoundary = useCallback(() => {
setErrorBoundaryKey((prev) => prev + 1)
props.onReset?.()
}, [props])
const onResetKeysChange = useCallback((prevResetKeys?: Array<string | number>) => {
prevResetKeysRef.current = prevResetKeys
}, [])
useEffect(() => {
if (prevResetKeysRef.current !== props.resetKeys) resetKeysRef.current = props.resetKeys
}, [props.resetKeys])
return (
<ErrorBoundaryInner
{...props}
copy={copy}
key={errorBoundaryKey}
resetErrorBoundary={resetErrorBoundary}
onResetKeysChange={onResetKeysChange}
/>
)
} // HOC for wrapping components with error boundary
export function withErrorBoundary<P extends object>(
Component: React.ComponentType<P>,
errorBoundaryProps?: Omit<ErrorBoundaryProps, 'children'>,
): React.ComponentType<P> {
const WrappedComponent = (props: P) => (
<ErrorBoundary {...errorBoundaryProps}>
<Component {...props} />
</ErrorBoundary>
)
WrappedComponent.displayName = `withErrorBoundary(${Component.displayName || Component.name || 'Component'})`
return WrappedComponent
}
export default ErrorBoundary