// IvantiCountsChart.js // Collapsible trend panel for the Vulnerability Triage page. // Shows open vs closed Ivanti finding counts over time (last sync per day). import React, { useState, useEffect, useMemo } from 'react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ReferenceLine, ResponsiveContainer, } from 'recharts'; import { TrendingUp, ChevronDown, ChevronUp, Loader } from 'lucide-react'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; const AMBER = '#F59E0B'; const SKY = '#0EA5E9'; const GREEN = '#10B981'; const RED = '#EF4444'; const AXIS_STYLE = { fontFamily: 'monospace', fontSize: '0.6rem', fill: '#475569' }; const GRID_STYLE = { stroke: 'rgba(255,255,255,0.05)', strokeDasharray: '3 3' }; const LEGEND_STYLE = { fontFamily: 'monospace', fontSize: '0.62rem', color: '#64748B' }; // --------------------------------------------------------------------------- // Custom dark tooltip // --------------------------------------------------------------------------- function DarkTooltip({ active, payload, label }) { if (!active || !payload?.length) return null; const openVal = payload.find(p => p.dataKey === 'open_count')?.value; const closedVal = payload.find(p => p.dataKey === 'closed_count')?.value; return (
{label}
{payload.map(p => (
{p.name} {p.value}
))} {openVal != null && closedVal != null && (
total {openVal + closedVal}
)}
); } // --------------------------------------------------------------------------- // Shorten YYYY-MM-DD to MM/DD/YY // --------------------------------------------------------------------------- function fmtDate(d) { if (!d) return ''; const p = d.split('-'); if (p.length === 3) return `${p[1]}/${p[2]}/${p[0].slice(2)}`; return d; } // --------------------------------------------------------------------------- // Main component // --------------------------------------------------------------------------- export default function IvantiCountsChart() { const [collapsed, setCollapsed] = useState(false); const [loading, setLoading] = useState(true); const [history, setHistory] = useState([]); useEffect(() => { let cancelled = false; const load = async () => { setLoading(true); try { const res = await fetch(`${API_BASE}/ivanti/findings/counts/history`, { credentials: 'include' }); if (res.ok && !cancelled) { const d = await res.json(); setHistory(d.history || []); } } catch { /* silent — chart shows no-data state */ } finally { if (!cancelled) setLoading(false); } }; load(); return () => { cancelled = true; }; }, []); const chartData = useMemo( () => history.map(r => ({ ...r, date: fmtDate(r.date) })), [history] ); // Compute a simple delta label for the latest vs previous point const deltaLabel = useMemo(() => { if (chartData.length < 2) return null; const latest = chartData[chartData.length - 1]; const prev = chartData[chartData.length - 2]; const delta = latest.open_count - prev.open_count; if (delta === 0) return { text: 'no change in open', color: '#475569' }; if (delta < 0) return { text: `▼ ${Math.abs(delta)} open since ${prev.date}`, color: GREEN }; return { text: `▲ ${delta} open since ${prev.date}`, color: RED }; }, [chartData]); return (
{/* ── Header ────────────────────────────────────────────────── */} {!collapsed && (
Open vs Closed — end-of-day snapshot per sync day
{chartData.length > 0 && (
{chartData.length} day{chartData.length !== 1 ? 's' : ''} of data
)}
{chartData.length < 2 ? (
{chartData.length === 0 ? 'Trend data begins accumulating after the first sync — check back tomorrow' : 'Need at least 2 days of syncs to display a trend'}
) : ( } /> )}
)}
); }