167 lines
4.8 KiB
TypeScript
167 lines
4.8 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
import { Card } from './Card'
|
|
import { Button } from './Button'
|
|
import { t } from '@/lib/i18n'
|
|
import { classifyError, type ErrorClassification } from '@/lib/errorClassification'
|
|
|
|
interface ErrorStateProps {
|
|
message: string
|
|
action?: ReactNode
|
|
className?: string
|
|
onRetry?: () => void
|
|
onCheckConnection?: () => void
|
|
showDocumentationLink?: boolean
|
|
error?: unknown
|
|
}
|
|
|
|
function ErrorIcon(): React.ReactElement {
|
|
return (
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
function getUserFriendlyMessage(classification: ErrorClassification): string {
|
|
return classification.message
|
|
}
|
|
|
|
function getErrorClassification(error: unknown | undefined): ErrorClassification | null {
|
|
if (error === undefined) {
|
|
return null
|
|
}
|
|
return classifyError(error)
|
|
}
|
|
|
|
function RetryButton({ onRetry }: { onRetry: () => void }): React.ReactElement {
|
|
return (
|
|
<Button type="button" variant="primary" size="small" onClick={onRetry}>
|
|
{t('errors.actions.retry')}
|
|
</Button>
|
|
)
|
|
}
|
|
|
|
function CheckConnectionButton({ onCheckConnection }: { onCheckConnection: () => void }): React.ReactElement {
|
|
return (
|
|
<Button type="button" variant="secondary" size="small" onClick={onCheckConnection}>
|
|
{t('errors.actions.checkConnection')}
|
|
</Button>
|
|
)
|
|
}
|
|
|
|
function DocumentationButton(): React.ReactElement {
|
|
return (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="small"
|
|
onClick={() => {
|
|
window.open('/docs', '_blank')
|
|
}}
|
|
>
|
|
{t('errors.actions.viewDocumentation')}
|
|
</Button>
|
|
)
|
|
}
|
|
|
|
function computeActionFlags(
|
|
classification: ErrorClassification | null,
|
|
onRetry: (() => void) | undefined,
|
|
onCheckConnection: (() => void) | undefined,
|
|
showDocumentationLink: boolean
|
|
): { showRetry: boolean; showCheck: boolean; showDoc: boolean } {
|
|
const canRetry = classification?.canRetry ?? false
|
|
const canCheckConnection = classification?.canCheckConnection ?? false
|
|
const needsDocumentation = classification?.needsDocumentation ?? false
|
|
|
|
return {
|
|
showRetry: canRetry && onRetry !== undefined,
|
|
showCheck: canCheckConnection && onCheckConnection !== undefined,
|
|
showDoc: needsDocumentation && showDocumentationLink,
|
|
}
|
|
}
|
|
|
|
function hasAnyAction(
|
|
action: ReactNode | undefined,
|
|
showRetry: boolean,
|
|
showCheck: boolean,
|
|
showDoc: boolean
|
|
): boolean {
|
|
return action !== undefined || showRetry || showCheck || showDoc
|
|
}
|
|
|
|
function ErrorActions({
|
|
action,
|
|
classification,
|
|
onRetry,
|
|
onCheckConnection,
|
|
showDocumentationLink,
|
|
}: {
|
|
action?: ReactNode
|
|
classification: ErrorClassification | null
|
|
onRetry?: () => void
|
|
onCheckConnection?: () => void
|
|
showDocumentationLink?: boolean
|
|
}): React.ReactElement | null {
|
|
const flags = computeActionFlags(classification, onRetry, onCheckConnection, showDocumentationLink ?? false)
|
|
|
|
if (!hasAnyAction(action, flags.showRetry, flags.showCheck, flags.showDoc)) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div className="mt-3 flex flex-wrap gap-2">
|
|
{action !== undefined && action}
|
|
{flags.showRetry && onRetry !== undefined && <RetryButton onRetry={onRetry} />}
|
|
{flags.showCheck && onCheckConnection !== undefined && <CheckConnectionButton onCheckConnection={onCheckConnection} />}
|
|
{flags.showDoc && <DocumentationButton />}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function ErrorState({
|
|
message,
|
|
action,
|
|
className = '',
|
|
onRetry,
|
|
onCheckConnection,
|
|
showDocumentationLink = false,
|
|
error,
|
|
}: ErrorStateProps): React.ReactElement {
|
|
const classification = getErrorClassification(error)
|
|
const displayMessage = classification !== null ? getUserFriendlyMessage(classification) : message
|
|
const suggestion = classification?.suggestion !== undefined ? t(classification.suggestion) : undefined
|
|
|
|
return (
|
|
<Card variant="default" className={`bg-red-900/20 border-red-500/50 ${className}`} role="alert">
|
|
<div className="flex items-start gap-3">
|
|
<div className="flex-shrink-0 text-red-400">
|
|
<ErrorIcon />
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-sm text-red-400 font-medium mb-2">{displayMessage}</p>
|
|
{suggestion !== undefined && <p className="text-xs text-red-400/70 mb-3">{suggestion}</p>}
|
|
<ErrorActions
|
|
action={action}
|
|
classification={classification}
|
|
{...(onRetry !== undefined ? { onRetry } : {})}
|
|
{...(onCheckConnection !== undefined ? { onCheckConnection } : {})}
|
|
showDocumentationLink={showDocumentationLink}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)
|
|
}
|