Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m43s
160 lines
5.1 KiB
React
160 lines
5.1 KiB
React
import { Component } from 'react';
|
|
import { IconAlertTriangle, IconRefresh, IconHome } from '@tabler/icons-react';
|
|
|
|
/**
|
|
* ErrorBoundary - глобальный обработчик ошибок React
|
|
* Ловит ошибки рендеринга и показывает fallback UI
|
|
*/
|
|
class ErrorBoundary extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
hasError: false,
|
|
error: null,
|
|
errorInfo: null,
|
|
errorCount: 0
|
|
};
|
|
}
|
|
|
|
static getDerivedStateFromError(error) {
|
|
return { hasError: true };
|
|
}
|
|
|
|
componentDidCatch(error, errorInfo) {
|
|
// Логируем ошибку
|
|
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
|
|
|
// Отправляем в мониторинг (если настроен)
|
|
this.logErrorToService(error, errorInfo);
|
|
|
|
this.setState(prevState => ({
|
|
error,
|
|
errorInfo,
|
|
errorCount: prevState.errorCount + 1
|
|
}));
|
|
}
|
|
|
|
logErrorToService(error, errorInfo) {
|
|
// Можно интегрировать с Sentry, LogRocket и т.д.
|
|
try {
|
|
if (typeof window !== 'undefined' && window.notify?.error) {
|
|
window.notify.error('Произошла критическая ошибка приложения', {
|
|
error: error.toString(),
|
|
componentStack: errorInfo?.componentStack
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to log error:', e);
|
|
}
|
|
}
|
|
|
|
handleReset = () => {
|
|
this.setState({
|
|
hasError: false,
|
|
error: null,
|
|
errorInfo: null
|
|
});
|
|
|
|
// Очищаем localStorage если ошибка повторяется
|
|
if (this.state.errorCount > 2) {
|
|
try {
|
|
localStorage.clear();
|
|
sessionStorage.clear();
|
|
} catch (e) {
|
|
console.error('Failed to clear storage:', e);
|
|
}
|
|
}
|
|
|
|
// Перезагружаем страницу если много ошибок
|
|
if (this.state.errorCount > 3) {
|
|
window.location.href = '/';
|
|
}
|
|
};
|
|
|
|
handleReload = () => {
|
|
window.location.reload();
|
|
};
|
|
|
|
handleGoHome = () => {
|
|
window.location.href = '/';
|
|
};
|
|
|
|
render() {
|
|
if (this.state.hasError) {
|
|
return (
|
|
<div className="page page-center">
|
|
<div className="container-tight py-4">
|
|
<div className="empty">
|
|
<div className="empty-icon">
|
|
<IconAlertTriangle size={64} className="text-danger" />
|
|
</div>
|
|
<p className="empty-title">Произошла ошибка</p>
|
|
<p className="empty-subtitle text-muted">
|
|
{this.state.error?.message || 'Что-то пошло не так. Попробуйте обновить страницу.'}
|
|
</p>
|
|
|
|
{process.env.NODE_ENV === 'development' && this.state.errorInfo && (
|
|
<div className="card mt-3">
|
|
<div className="card-body">
|
|
<h3 className="card-title">Детали ошибки (только в dev режиме)</h3>
|
|
<pre className="text-start" style={{
|
|
fontSize: '0.75rem',
|
|
maxHeight: '300px',
|
|
overflow: 'auto',
|
|
whiteSpace: 'pre-wrap'
|
|
}}>
|
|
{this.state.error?.toString()}
|
|
{'\n\n'}
|
|
{this.state.errorInfo?.componentStack}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="empty-action">
|
|
<div className="btn-list justify-content-center">
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={this.handleReset}
|
|
>
|
|
<IconRefresh className="icon" />
|
|
Попробовать снова
|
|
</button>
|
|
<button
|
|
className="btn btn-outline-primary"
|
|
onClick={this.handleReload}
|
|
>
|
|
Перезагрузить страницу
|
|
</button>
|
|
<button
|
|
className="btn btn-outline-secondary"
|
|
onClick={this.handleGoHome}
|
|
>
|
|
<IconHome className="icon" />
|
|
На главную
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{this.state.errorCount > 1 && (
|
|
<div className="alert alert-warning mt-3">
|
|
<p className="mb-0">
|
|
Ошибка повторяется ({this.state.errorCount} раз).
|
|
{this.state.errorCount > 2 && ' При следующей попытке кэш будет очищен.'}
|
|
{this.state.errorCount > 3 && ' Следующая попытка приведёт к полной перезагрузке.'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return this.props.children;
|
|
}
|
|
}
|
|
|
|
export default ErrorBoundary;
|
|
|