import React, { useCallback, useMemo, useState } from "react"; import { ChevronDown, Download, Upload } from "lucide-react"; import "./SettingsSyncLog.css"; // ── Types ───────────────────────────────────────────────────────────────────── export interface SyncLogEntry { id: string; timestamp: string; direction: "push" | "pull"; result: "success" | "conflict" | "error"; nodeId: string; nodeName: string; details?: string; } interface SettingsSyncLogProps { /** The node ID this log is for */ nodeId: string; /** Sync history entries to display — provided by parent */ entries: SyncLogEntry[]; /** Show loading state */ loading?: boolean; /** When true, hides the node name filter (used when showing log for a single node) */ singleNode?: boolean; } // ── Component ──────────────────────────────────────────────────────────────── /** * Component that displays recent settings sync operations as a chronological list * with filtering by direction and node name. */ export function SettingsSyncLog({ nodeId: _nodeId, entries, loading = false, singleNode = false, }: SettingsSyncLogProps) { const [isExpanded, setIsExpanded] = useState(false); const [directionFilter, setDirectionFilter] = useState<"all" | "push" | "pull">("all"); const [nodeFilter, setNodeFilter] = useState("all"); // Toggle expanded state const handleToggle = useCallback(() => { setIsExpanded((prev) => !prev); }, []); // Extract unique node names for filter dropdown const uniqueNodes = useMemo(() => { const nodes = new Set(); for (const entry of entries) { nodes.add(entry.nodeName); } return Array.from(nodes).sort(); }, [entries]); // Filter entries based on current filters const filteredEntries = useMemo(() => { let result = [...entries]; // Filter by direction if (directionFilter !== "all") { result = result.filter((entry) => entry.direction === directionFilter); } // Filter by node name if (!singleNode && nodeFilter !== "all") { result = result.filter((entry) => entry.nodeName === nodeFilter); } // Sort by timestamp descending (newest first) result.sort((a, b) => { const timeA = new Date(a.timestamp).getTime(); const timeB = new Date(b.timestamp).getTime(); return timeB - timeA; }); return result; }, [entries, directionFilter, nodeFilter, singleNode]); // Format timestamp for display const formatTimestamp = useCallback((isoTimestamp: string): string => { const date = new Date(isoTimestamp); return date.toLocaleString(); }, []); // Get result badge class const getResultBadgeClass = useCallback((result: SyncLogEntry["result"]): string => { switch (result) { case "success": return "settings-sync-log__badge--success"; case "conflict": return "settings-sync-log__badge--conflict"; case "error": return "settings-sync-log__badge--error"; default: return ""; } }, []); // Get result display text const getResultText = useCallback((result: SyncLogEntry["result"]): string => { switch (result) { case "success": return "Success"; case "conflict": return "Conflict"; case "error": return "Error"; default: return result; } }, []); return (
{isExpanded && ( <>
{!singleNode && ( )}
{loading && entries.length === 0 ? (
Loading...
) : filteredEntries.length === 0 ? (
No sync history available
) : (
{filteredEntries.map((entry) => (
{formatTimestamp(entry.timestamp)} {entry.direction === "push" ? ( ) : ( )} {getResultText(entry.result)} {!singleNode && ( {entry.nodeName} )} {entry.details && ( {entry.details} )}
))}
)} )}
); }