| 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(); |
| }, []); |
|
|
| |
| |
| 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; |
|
|