Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m47s
118 lines
4.2 KiB
React
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
|