File size: 8,768 Bytes
24e6f5b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | import { useState, useEffect } from 'react';
import { AlertTriangle, ShieldCheck, Zap } from 'lucide-react';
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts';
import api from '../api/axios';
import { useSettings } from '../context/SettingsContext';
const AnomalyFeed = () => {
const { currencySymbol } = useSettings();
const [anomalies, setAnomalies] = useState([]);
const [loading, setLoading] = useState(true);
const [showAll, setShowAll] = useState(false);
useEffect(() => {
const fetchAnomalies = async () => {
try {
const response = await api.get('analytics/anomalies/');
setAnomalies(response.data);
} catch (err) {
console.error("Anomaly Error:", err);
} finally {
setLoading(false);
}
};
fetchAnomalies();
}, []);
// Prepare chart data - Recharts Scatter needs numeric X axis often for best results with type="number"
// converting date string to timestamp
const chartData = anomalies.map(a => ({
...a,
x: new Date(a.date).getTime(),
y: a.amount
}));
const displayedAnomalies = showAll ? anomalies : anomalies.slice(0, 6);
if (loading) return (
<div className="glass-panel" style={{ height: '100%', minHeight: '300px', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<div className="spinner"></div>
</div>
);
return (
<div className="glass-panel" style={{
height: '550px', // Increased from 450px
minHeight: '250px',
padding: '0',
overflow: 'hidden',
display: 'flex', flexDirection: 'column',
border: '1px solid rgba(239, 68, 68, 0.2)',
boxShadow: '0 0 20px rgba(239, 68, 68, 0.05)'
}}>
<div style={{
padding: '1rem',
borderBottom: '1px solid var(--glass-border)',
background: 'rgba(239, 68, 68, 0.05)',
display: 'flex', justifyContent: 'space-between', alignItems: 'center'
}}>
<h3 style={{ margin: 0, fontSize: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem', color: '#f87171' }}>
<ShieldCheck size={18} /> Security Radar
</h3>
<span style={{
fontSize: '0.7rem', fontWeight: 'bold',
background: anomalies.length ? '#ef4444' : '#10b981',
color: 'white', padding: '0.1rem 0.5rem', borderRadius: '99px'
}}>
{anomalies.length}
</span>
</div>
{/* Anomaly Visualization Chart */}
{anomalies.length > 0 && (
<div style={{ height: '150px', width: '100%', borderBottom: '1px solid var(--glass-border)', background: 'linear-gradient(to bottom, rgba(0,0,0,0.2), transparent)' }}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={anomalies} margin={{ top: 20, right: 20, bottom: 20, left: 0 }}>
<XAxis
dataKey="date"
tick={false}
axisLine={false}
/>
<YAxis
hide
/>
<Tooltip
cursor={{ fill: 'rgba(255,255,255,0.05)' }}
contentStyle={{ backgroundColor: 'rgba(30, 41, 59, 0.95)', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.1)', color: '#fff' }}
formatter={(value) => [`${currencySymbol}${value}`, 'Amount']}
/>
<Bar dataKey="amount" radius={[4, 4, 0, 0]}>
{anomalies.map((entry, index) => (
<Cell key={`cell-${index}`} fill={`rgba(239, 68, 68, ${(entry.score || 0.5) + 0.4})`} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
)}
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: '0.75rem' }}>
{anomalies.length === 0 ? (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', color: 'var(--text-muted)', opacity: 0.7 }}>
<ShieldCheck size={32} style={{ marginBottom: '0.5rem', color: '#10b981' }} />
<p style={{ fontSize: '0.9rem' }}>No anomalies detected.</p>
</div>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '0.5rem' }}>
{displayedAnomalies.map((item, idx) => (
<div key={idx} style={{
background: 'rgba(255,255,255,0.03)',
padding: '0.6rem', // Reduced from 0.75rem
borderRadius: '0.5rem',
borderTop: '3px solid #f87171', // Top border for cards
display: 'flex', flexDirection: 'column',
transition: 'all 0.2s',
minHeight: '90px' // Reduced from 100px
}} className="hover:bg-white/5">
<div style={{ marginBottom: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.3rem', fontSize: '0.8rem', fontWeight: '600', color: '#fff', overflow: 'hidden' }}>
<AlertTriangle size={12} color="#f87171" style={{ flexShrink: 0 }} />
<span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{item.title}</span>
</div>
<div style={{ fontSize: '1rem', fontWeight: 'bold', color: '#fff', marginBottom: '0.25rem' }}>
{currencySymbol}{item.amount.toLocaleString()}
</div>
<div style={{ fontSize: '0.7rem', color: 'var(--text-muted)', marginBottom: 'auto' }}>
{item.date}
</div>
<div style={{ fontSize: '0.65rem', marginTop: '0.5rem', color: '#f87171', background: 'rgba(248, 113, 113, 0.1)', padding: '2px 6px', borderRadius: '4px', textAlign: 'center', alignSelf: 'flex-start' }}>
{item.score ? `Risk: ${(item.score * 100).toFixed(0)}%` : 'Detected'}
</div>
</div>
))}
{/* Show More Button - Spans all columns */}
{anomalies.length > 6 && (
<button
onClick={() => setShowAll(!showAll)}
style={{
gridColumn: '1 / -1',
padding: '0.5rem',
marginTop: '0.25rem',
background: 'rgba(255,255,255,0.05)',
border: 'none',
borderRadius: '0.5rem',
color: 'var(--text-muted)',
fontSize: '0.8rem',
cursor: 'pointer',
transition: 'background 0.2s'
}}
className="hover:bg-white/10"
>
{showAll ? 'Show Less' : `Show ${anomalies.length - 6} More`}
</button>
)}
</div>
)}
</div>
<div style={{
padding: '0.5rem', borderTop: '1px solid var(--glass-border)',
fontSize: '0.65rem', color: 'var(--text-muted)', textAlign: 'center',
background: 'rgba(0,0,0,0.2)'
}}>
<Zap size={10} style={{ marginRight: '0.25rem', display: 'inline' }} />
AI Anomaly Detector
</div>
</div>
);
};
export default AnomalyFeed;
|