160 lines
6.5 KiB
TypeScript
160 lines
6.5 KiB
TypeScript
import { connectionMonitor } from '@/lib/gateways/api/base/ApiConnectionMonitor';
|
|
import { ApiError } from '@/lib/gateways/api/base/ApiError';
|
|
import { CircuitBreakerRegistry } from '@/lib/gateways/api/base/RetryHandler';
|
|
import { Badge } from '@/ui/Badge';
|
|
import { Box } from '@/ui/Box';
|
|
import { Button } from '@/ui/Button';
|
|
import { Card } from '@/ui/Card';
|
|
import { Heading } from '@/ui/Heading';
|
|
import { Icon } from '@/ui/Icon';
|
|
import { Text } from '@/ui/Text';
|
|
import { Copy, RefreshCw, Terminal, X } from 'lucide-react';
|
|
import { useEffect, useState } from 'react';
|
|
|
|
interface DevErrorPanelProps {
|
|
error: ApiError;
|
|
onReset: () => void;
|
|
}
|
|
|
|
export function DevErrorPanel({ error, onReset }: DevErrorPanelProps) {
|
|
const [connectionStatus, setConnectionStatus] = useState(connectionMonitor.getHealth());
|
|
const [circuitBreakers, setCircuitBreakers] = useState(CircuitBreakerRegistry.getInstance().getStatus());
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const handleStatusChange = () => {
|
|
setConnectionStatus(connectionMonitor.getHealth());
|
|
setCircuitBreakers(CircuitBreakerRegistry.getInstance().getStatus());
|
|
};
|
|
|
|
connectionMonitor.on('success', handleStatusChange);
|
|
connectionMonitor.on('failure', handleStatusChange);
|
|
connectionMonitor.on('connected', handleStatusChange);
|
|
connectionMonitor.on('disconnected', handleStatusChange);
|
|
connectionMonitor.on('degraded', handleStatusChange);
|
|
|
|
return () => {
|
|
connectionMonitor.off('success', handleStatusChange);
|
|
connectionMonitor.off('failure', handleStatusChange);
|
|
connectionMonitor.off('connected', handleStatusChange);
|
|
connectionMonitor.off('disconnected', handleStatusChange);
|
|
connectionMonitor.off('degraded', handleStatusChange);
|
|
};
|
|
}, []);
|
|
|
|
const copyToClipboard = async () => {
|
|
const debugInfo = {
|
|
error: {
|
|
type: error.type,
|
|
message: error.message,
|
|
context: error.context,
|
|
stack: error.stack,
|
|
},
|
|
connection: connectionStatus,
|
|
circuitBreakers,
|
|
timestamp: new Date().toISOString(),
|
|
userAgent: navigator.userAgent,
|
|
url: window.location.href,
|
|
};
|
|
|
|
try {
|
|
await navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2));
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
} catch (err) {
|
|
// Silent failure
|
|
}
|
|
};
|
|
|
|
const getSeverityVariant = (): 'critical' | 'warning' | 'primary' | 'default' => {
|
|
switch (error.getSeverity()) {
|
|
case 'error': return 'critical';
|
|
case 'warn': return 'warning';
|
|
case 'info': return 'primary';
|
|
default: return 'default';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Box position="fixed" inset={0} zIndex={100} bg="var(--ui-color-bg-base)" padding={4} style={{ overflowY: 'auto' }}>
|
|
<Box maxWidth="80rem" marginX="auto" fullWidth>
|
|
<Box display="flex" flexDirection="col" gap={4}>
|
|
{/* Header */}
|
|
<Card variant="dark" padding={4}>
|
|
<Box display="flex" alignItems="center" justifyContent="between">
|
|
<Box display="flex" alignItems="center" gap={3}>
|
|
<Icon icon={Terminal} size={5} intent="primary" />
|
|
<Heading level={2}>API Error Debug Panel</Heading>
|
|
<Badge variant={getSeverityVariant() as any}>
|
|
{error.type}
|
|
</Badge>
|
|
</Box>
|
|
<Box display="flex" gap={2}>
|
|
<Button variant="secondary" onClick={copyToClipboard} icon={<Icon icon={Copy} size={4} />}>
|
|
{copied ? 'Copied!' : 'Copy'}
|
|
</Button>
|
|
<Button variant="primary" onClick={onReset} icon={<Icon icon={X} size={4} />}>
|
|
Close
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
</Card>
|
|
|
|
{/* Details Grid */}
|
|
<Box display="grid" gridCols={{ base: 1, lg: 2 }} gap={4}>
|
|
<Box display="flex" flexDirection="col" gap={4}>
|
|
<Card title="Error Details" variant="outline">
|
|
<Box display="flex" flexDirection="col" gap={2}>
|
|
<DetailRow label="Type" value={error.type} intent="critical" />
|
|
<DetailRow label="Message" value={error.message} />
|
|
<DetailRow label="Endpoint" value={error.context.endpoint || 'N/A'} intent="primary" />
|
|
<DetailRow label="Method" value={error.context.method || 'N/A'} intent="warning" />
|
|
<DetailRow label="Status" value={error.context.statusCode || 'N/A'} />
|
|
</Box>
|
|
</Card>
|
|
|
|
<Card title="Connection Health" variant="outline">
|
|
<Box display="flex" flexDirection="col" gap={2}>
|
|
<DetailRow label="Status" value={connectionStatus.status.toUpperCase()} intent={connectionStatus.status === 'connected' ? 'success' : 'critical'} />
|
|
<DetailRow label="Reliability" value={`${connectionMonitor.getReliability().toFixed(2)}%`} />
|
|
<DetailRow label="Total Requests" value={connectionStatus.totalRequests} />
|
|
</Box>
|
|
</Card>
|
|
</Box>
|
|
|
|
<Box display="flex" flexDirection="col" gap={4}>
|
|
<Card title="Actions" variant="outline">
|
|
<Box display="flex" flexDirection="col" gap={2}>
|
|
<Button variant="primary" onClick={() => connectionMonitor.performHealthCheck()} fullWidth icon={<Icon icon={RefreshCw} size={4} />}>
|
|
Run Health Check
|
|
</Button>
|
|
<Button variant="secondary" onClick={() => CircuitBreakerRegistry.getInstance().resetAll()} fullWidth>
|
|
Reset Circuit Breakers
|
|
</Button>
|
|
</Box>
|
|
</Card>
|
|
|
|
<Card title="Raw Error" variant="outline">
|
|
<Box padding={2} bg="var(--ui-color-bg-surface-muted)" rounded="md" style={{ maxHeight: '10rem', overflow: 'auto' }}>
|
|
<Text size="xs" variant="low" font="mono">
|
|
{JSON.stringify(error.context, null, 2)}
|
|
</Text>
|
|
</Box>
|
|
</Card>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
const DetailRow = ({ label, value, intent = 'low' }: { label: string; value: any; intent?: any }) => (
|
|
<Box display="grid" gridCols={3} gap={2}>
|
|
<Text size="xs" variant="low" weight="bold">{label}:</Text>
|
|
<Box style={{ gridColumn: 'span 2' }}>
|
|
<Text size="xs" variant={intent} weight="bold">{String(value)}</Text>
|
|
</Box>
|
|
</Box>
|
|
);
|