feat: Добавление компонентов ErrorBoundary и NetworkErrorHandler для улучшения обработки ошибок в приложении. Увеличение таймаута запросов до 30 секунд и улучшение логики повторных попыток с детализированными уведомлениями об ошибках.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m43s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m43s
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { IconWifi, IconWifiOff } from '@tabler/icons-react';
|
||||
|
||||
/**
|
||||
* NetworkErrorHandler - компонент для мониторинга состояния сети
|
||||
* Показывает уведомление при потере соединения
|
||||
*/
|
||||
function NetworkErrorHandler() {
|
||||
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
||||
const [wasOffline, setWasOffline] = useState(false);
|
||||
const [showReconnected, setShowReconnected] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleOnline = () => {
|
||||
setIsOnline(true);
|
||||
if (wasOffline) {
|
||||
setShowReconnected(true);
|
||||
// Показываем уведомление о восстановлении на 3 секунды
|
||||
setTimeout(() => {
|
||||
setShowReconnected(false);
|
||||
setWasOffline(false);
|
||||
}, 3000);
|
||||
|
||||
// Уведомляем через глобальную систему
|
||||
if (window.notify?.success) {
|
||||
window.notify.success('Соединение восстановлено');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOffline = () => {
|
||||
setIsOnline(false);
|
||||
setWasOffline(true);
|
||||
|
||||
// Уведомляем через глобальную систему
|
||||
if (window.notify?.warning) {
|
||||
window.notify.warning('Нет соединения с интернетом');
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', handleOnline);
|
||||
window.removeEventListener('offline', handleOffline);
|
||||
};
|
||||
}, [wasOffline]);
|
||||
|
||||
// Не показываем ничего если онлайн и не было офлайна
|
||||
if (isOnline && !showReconnected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="position-fixed top-0 start-0 end-0"
|
||||
style={{ zIndex: 1090 }}
|
||||
>
|
||||
{!isOnline ? (
|
||||
<div className="alert alert-danger mb-0 rounded-0 border-0" role="alert">
|
||||
<div className="d-flex align-items-center justify-content-center">
|
||||
<IconWifiOff className="icon me-2" />
|
||||
<strong>Нет соединения с интернетом</strong>
|
||||
<span className="ms-2 text-muted">Ожидание восстановления...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : showReconnected ? (
|
||||
<div className="alert alert-success mb-0 rounded-0 border-0" role="alert">
|
||||
<div className="d-flex align-items-center justify-content-center">
|
||||
<IconWifi className="icon me-2" />
|
||||
<strong>Соединение восстановлено</strong>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NetworkErrorHandler;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useState } from 'react';
|
||||
import { IconRefresh } from '@tabler/icons-react';
|
||||
|
||||
/**
|
||||
* RetryButton - кнопка для повторной попытки с индикацией загрузки
|
||||
*/
|
||||
function RetryButton({
|
||||
onRetry,
|
||||
loading: externalLoading,
|
||||
disabled,
|
||||
className = 'btn btn-primary',
|
||||
children = 'Повторить',
|
||||
showIcon = true,
|
||||
...props
|
||||
}) {
|
||||
const [internalLoading, setInternalLoading] = useState(false);
|
||||
const loading = externalLoading !== undefined ? externalLoading : internalLoading;
|
||||
|
||||
const handleClick = async () => {
|
||||
if (loading || disabled) return;
|
||||
|
||||
try {
|
||||
setInternalLoading(true);
|
||||
await onRetry();
|
||||
} finally {
|
||||
setInternalLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`${className}${loading ? ' btn-loading' : ''}`}
|
||||
disabled={disabled || loading}
|
||||
onClick={handleClick}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && !loading && <IconRefresh className="icon" />}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default RetryButton;
|
||||
|
||||
Reference in New Issue
Block a user