'use client'; import { useState, useEffect, useRef, useCallback } from 'react'; import GridLayout from 'react-grid-layout'; import type { Layout, LayoutItem } from 'react-grid-layout'; import 'react-grid-layout/css/styles.css'; import 'react-resizable/css/styles.css'; import { useApp } from '@/lib/context'; import { fetchCharts, createChart, updateChart, deleteChart, fetchSignals, fetchSessions, fetchUps, } from '@/lib/api'; import type { ChartConfig, SignalRow, UpsRow, SessionBoundary, AlertConfig } from '@/lib/types'; import ChartCard from './ChartCard'; import ChartEditor from './ChartEditor'; const COLS = 6; const ROW_HEIGHT = 80; interface Props { alerts: AlertConfig[]; } export default function Dashboard({ alerts }: Props) { const { timeRange, timeMode, getFromTo } = useApp(); const [charts, setCharts] = useState([]); const [signalData, setSignalData] = useState>(new Map()); const [upsData, setUpsData] = useState>(new Map()); const [sessions, setSessions] = useState([]); const [editingChart, setEditingChart] = useState(null); const [creatingChart, setCreatingChart] = useState(false); const [containerWidth, setContainerWidth] = useState(1200); const containerRef = useRef(null); const chartsRef = useRef([]); const refreshingRef = useRef(false); const layoutSaveTimer = useRef | null>(null); useEffect(() => { chartsRef.current = charts; }, [charts]); useEffect(() => { fetchCharts().then(setCharts); }, []); const refreshData = useCallback(async () => { if (refreshingRef.current) return; const current = chartsRef.current; if (current.length === 0) return; refreshingRef.current = true; try { const { from, to } = getFromTo(); const signalCharts = current.filter((c) => c.chart_type === 'signals'); const upsCharts = current.filter((c) => c.chart_type === 'ups'); if (signalCharts.length === 0 && upsCharts.length === 0) return; const [newSessions, ...results] = await Promise.all([ fetchSessions(from, to), ...signalCharts.map((c) => fetchSignals({ combinator: c.filter_combinators ?? undefined, item: c.filter_items ?? undefined, exclude: c.filter_items_exclude ?? undefined, signal: c.signal_type, time_mode: timeMode, from, to, regex: c.filter_items_regex || undefined, ...(c.order_by !== 'time' ? { order_by: c.order_by, limit: c.series_limit } : {}), }), ), ...upsCharts.map((c) => fetchUps({ combinator: c.filter_combinators?.[0], from, to })), ]); setSessions(newSessions as SessionBoundary[]); const sigMap = new Map(); signalCharts.forEach((c, i) => sigMap.set(c.id, results[i] as SignalRow[])); setSignalData(sigMap); const upsMap = new Map(); upsCharts.forEach((c, i) => upsMap.set(c.id, results[signalCharts.length + i] as UpsRow[])); setUpsData(upsMap); } finally { refreshingRef.current = false; } }, [getFromTo, timeMode]); useEffect(() => { if (charts.length > 0) refreshData(); }, [charts, timeRange, timeMode, refreshData]); useEffect(() => { const id = setInterval(refreshData, 30_000); return () => clearInterval(id); }, [refreshData]); useEffect(() => { const el = containerRef.current; if (!el) return; setContainerWidth(el.clientWidth); const ro = new ResizeObserver(() => setContainerWidth(el.clientWidth)); ro.observe(el); return () => ro.disconnect(); }, []); async function handleCreate(draft: Omit) { const created = await createChart(draft); setCharts((cs) => [...cs, created]); setCreatingChart(false); } async function handleUpdate(id: string, draft: Omit) { const updated = await updateChart(id, draft); setCharts((cs) => cs.map((c) => (c.id === id ? updated : c))); setEditingChart(null); } async function handleDelete(id: string) { await deleteChart(id); setCharts((cs) => cs.filter((c) => c.id !== id)); } function handleLayoutChange(layout: Layout) { const items = layout as readonly LayoutItem[]; const changed = items.filter((item) => { const chart = chartsRef.current.find((c) => c.id === item.i); return ( chart && (chart.pos_x !== item.x || chart.pos_y !== item.y || chart.width !== item.w || chart.height !== item.h) ); }); if (changed.length === 0) return; setCharts((cs) => cs.map((c) => { const l = changed.find((item) => item.i === c.id); return l ? { ...c, pos_x: l.x, pos_y: l.y, width: l.w, height: l.h } : c; }), ); if (layoutSaveTimer.current) clearTimeout(layoutSaveTimer.current); layoutSaveTimer.current = setTimeout(() => { changed.forEach((item) => updateChart(item.i, { pos_x: item.x, pos_y: item.y, width: item.w, height: item.h }), ); }, 500); } const layout: Layout = charts.map((c) => ({ i: c.id, x: c.pos_x, y: c.pos_y, w: c.width, h: c.height, minW: 1, minH: c.chart_type === 'divider' ? 1 : 2, })); return (
( ), }} > {charts.map((c) => (
!c.filter_combinators || !a.combinator || c.filter_combinators.includes(a.combinator), )} timeMode={timeMode} onEdit={() => setEditingChart(c)} onDelete={() => handleDelete(c.id)} />
))}
{creatingChart && ( setCreatingChart(false)} /> )} {editingChart && ( handleUpdate(editingChart.id, draft)} onClose={() => setEditingChart(null)} /> )}
); }