feat: add error boundaries for graceful error handling (Phase 3.4)

Implement React Error Boundaries to catch and handle errors gracefully
without crashing the entire UI. Provides helpful recovery mechanisms.

Changes:
- Create ErrorBoundary component module
  - BaseErrorBoundary abstract class (shared logic)
  - CryptoErrorBoundary (crypto operation errors)
  - ApiErrorBoundary (API communication errors)
  - GenericErrorBoundary (general component errors)

- Error boundary features
  - Catches rendering and lifecycle errors
  - Logs errors to console for debugging
  - Displays helpful, contextual error messages
  - Provides retry/reload buttons for recovery
  - Optional custom fallback UI via props
  - Optional onReset callback for custom recovery

- Integrate error boundaries into routes
  - Wrap SealedSecretList with ApiErrorBoundary
  - Wrap SealedSecretDetail with ApiErrorBoundary
  - Wrap SealingKeysView with ApiErrorBoundary
  - Wrap SettingsPage with GenericErrorBoundary
  - Wrap SecretDetailsSection with GenericErrorBoundary

Error types and messages:
- Crypto errors: Certificate, browser compatibility, malformed data
- API errors: Cluster connection, controller config, network
- Generic errors: Unexpected errors with simple recovery message

Benefits:
- App doesn't crash completely on errors
- Users can continue using unaffected features
- Clear, actionable troubleshooting steps
- Professional error presentation
- Production-ready error handling
- Easier debugging with console logs

Build: 354.92 kB (97.76 kB gzipped), +2.47 kB (+0.7%)
Phase 3.4 complete. 10 of 14 phases done (71%).

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
2026-02-11 22:10:50 -05:00
parent 2171250e99
commit 2cb815f921
4 changed files with 723 additions and 6 deletions
@@ -0,0 +1,192 @@
/**
* Error Boundary Components
*
* Provides graceful error handling for crypto and API operations
*/
import { ErrorOutline } from '@mui/icons-material';
import { Alert, Box, Button, Typography } from '@mui/material';
import React, { Component, ReactNode } from 'react';
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
onReset?: () => void;
}
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
errorInfo?: React.ErrorInfo;
}
/**
* Base error boundary component
*/
abstract class BaseErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
this.setState({ errorInfo });
}
handleReset = () => {
this.setState({ hasError: false, error: undefined, errorInfo: undefined });
if (this.props.onReset) {
this.props.onReset();
}
};
abstract renderError(): ReactNode;
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return this.renderError();
}
return this.props.children;
}
}
/**
* Error boundary for cryptographic operations
*
* Catches errors during encryption/decryption and provides
* helpful context about what might have gone wrong.
*/
export class CryptoErrorBoundary extends BaseErrorBoundary {
renderError() {
return (
<Box p={3}>
<Alert
severity="error"
icon={<ErrorOutline />}
action={
<Button color="inherit" size="small" onClick={this.handleReset}>
Retry
</Button>
}
>
<Typography variant="h6" gutterBottom>
Cryptographic Operation Failed
</Typography>
<Typography variant="body2" paragraph>
An error occurred during encryption or decryption. This might indicate:
</Typography>
<ul style={{ margin: 0, paddingLeft: 20 }}>
<li>Invalid or expired controller certificate</li>
<li>Browser cryptography compatibility issue</li>
<li>Malformed secret data</li>
<li>Controller not reachable or misconfigured</li>
</ul>
{this.state.error && (
<Typography
variant="body2"
sx={{ mt: 2, fontFamily: 'monospace', fontSize: '0.875rem' }}
>
Error: {this.state.error.message}
</Typography>
)}
</Alert>
</Box>
);
}
}
/**
* Error boundary for API operations
*
* Catches errors during Kubernetes API calls and provides
* guidance for troubleshooting connectivity issues.
*/
export class ApiErrorBoundary extends BaseErrorBoundary {
renderError() {
return (
<Box p={3}>
<Alert
severity="error"
icon={<ErrorOutline />}
action={
<Button color="inherit" size="small" onClick={this.handleReset}>
Retry
</Button>
}
>
<Typography variant="h6" gutterBottom>
API Communication Error
</Typography>
<Typography variant="body2" paragraph>
Failed to communicate with the Kubernetes API or Sealed Secrets controller.
</Typography>
<Typography variant="body2" paragraph>
Please verify:
</Typography>
<ul style={{ margin: 0, paddingLeft: 20 }}>
<li>Kubernetes cluster is accessible</li>
<li>Sealed Secrets controller is running</li>
<li>Controller configuration is correct (name, namespace, port)</li>
<li>Network connectivity to the cluster</li>
</ul>
{this.state.error && (
<Typography
variant="body2"
sx={{ mt: 2, fontFamily: 'monospace', fontSize: '0.875rem' }}
>
Error: {this.state.error.message}
</Typography>
)}
</Alert>
</Box>
);
}
}
/**
* Generic error boundary for general component errors
*
* Provides a fallback UI when components encounter unexpected errors.
*/
export class GenericErrorBoundary extends BaseErrorBoundary {
renderError() {
return (
<Box p={3}>
<Alert
severity="error"
icon={<ErrorOutline />}
action={
<Button color="inherit" size="small" onClick={this.handleReset}>
Reload
</Button>
}
>
<Typography variant="h6" gutterBottom>
Something Went Wrong
</Typography>
<Typography variant="body2" paragraph>
An unexpected error occurred. Please try reloading the page or contact your administrator
if the problem persists.
</Typography>
{this.state.error && (
<Typography
variant="body2"
sx={{ mt: 2, fontFamily: 'monospace', fontSize: '0.875rem' }}
>
Error: {this.state.error.message}
</Typography>
)}
</Alert>
</Box>
);
}
}