Files
router-lists-ui/frontend/src/components/ErrorAlert.jsx
T

118 lines
4.2 KiB
React

import { IconAlertTriangle, IconCopy, IconRefresh, IconChevronDown, IconChevronUp } from '@tabler/icons-react'
import { useState } from 'react'
import { formatDateTime } from '../lib/datetime.js'
/**
* ErrorAlert - улучшенный компонент для отображения ошибок
* Добавлены: Copy error, Retry, Show details
*/
function ErrorAlert({
message,
details,
error, // альтернативный формат - объект error
onClose,
onRetry,
className = '',
showDetails = true
}) {
const [isExpanded, setIsExpanded] = useState(false)
const [copied, setCopied] = useState(false)
// Поддержка обоих форматов: { message, details } или { error }
const errorMessage = message || (typeof error === 'string' ? error : error?.message) || 'Произошла неизвестная ошибка'
const errorDetails = details || error?.details || error?.stack || null
const errorCode = error?.code || error?.status || null
if (!errorMessage) return null
const handleCopy = () => {
const errorText = `
Ошибка: ${errorMessage}
${errorCode ? `Код: ${errorCode}` : ''}
${errorDetails ? `Детали:\n${typeof errorDetails === 'string' ? errorDetails : JSON.stringify(errorDetails, null, 2)}` : ''}
Время: ${formatDateTime(new Date())}
`.trim()
navigator.clipboard.writeText(errorText).then(() => {
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}).catch(err => {
console.error('Не удалось скопировать:', err)
})
}
return (
<div className={`alert alert-danger alert-dismissible ${className}`} role="alert">
<div className="d-flex">
<div className="me-2">
<IconAlertTriangle className="icon alert-icon" />
</div>
<div className="flex-grow-1">
<h4 className="alert-title">
Ошибка
{errorCode && (
<span className="badge bg-danger-lt text-danger ms-2">{errorCode}</span>
)}
</h4>
<div className="text-secondary mb-2">{errorMessage}</div>
{/* Детали ошибки (раскрывающийся блок) */}
{errorDetails && showDetails && (
<div className="mt-2">
<button
className="btn btn-sm btn-ghost-secondary"
onClick={() => setIsExpanded(!isExpanded)}
type="button"
>
{isExpanded ? <IconChevronUp size={16} /> : <IconChevronDown size={16} />}
<span className="ms-1">{isExpanded ? 'Скрыть детали' : 'Показать детали'}</span>
</button>
{isExpanded && (
<pre className="mt-2 p-2 bg-dark text-white rounded" style={{
fontSize: '0.75rem',
maxHeight: '200px',
overflow: 'auto'
}}>
{typeof errorDetails === 'string' ? errorDetails : JSON.stringify(errorDetails, null, 2)}
</pre>
)}
</div>
)}
{/* Действия */}
{(onRetry || handleCopy) && (
<div className="btn-list mt-2">
{onRetry && (
<button className="btn btn-sm btn-primary" onClick={onRetry} type="button">
<IconRefresh size={16} className="me-1" />
Повторить
</button>
)}
<button
className="btn btn-sm btn-outline-secondary"
onClick={handleCopy}
title="Скопировать информацию об ошибке"
type="button"
>
<IconCopy size={16} className="me-1" />
{copied ? 'Скопировано!' : 'Копировать ошибку'}
</button>
</div>
)}
</div>
</div>
{onClose && (
<button
type="button"
className="btn-close"
onClick={onClose}
aria-label="Закрыть"
/>
)}
</div>
)
}
export default ErrorAlert