import { useCallback, useEffect, useState } from 'react'; import { Button } from '@/components/ui/button'; import { ChevronDown, Download, RefreshCw, Search } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '../ui/dropdown-menu'; import { apiRequest, formatDate } from '@/lib/utils'; import { useAuth } from '@/hooks/useAuth'; import type { MirrorJob } from '@/lib/db/schema'; import type { ActivityApiResponse } from '@/types/activities'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../ui/select'; import { repoStatusEnum, type RepoStatus } from '@/types/Repository'; import ActivityList from './ActivityList'; import { ActivityNameCombobox } from './ActivityNameCombobox'; import { useSSE } from '@/hooks/useSEE'; import { useFilterParams } from '@/hooks/useFilterParams'; import { toast } from 'sonner'; type MirrorJobWithKey = MirrorJob & { _rowKey: string }; function genKey(job: MirrorJob): string { return `${ job.id ?? (typeof crypto !== 'undefined' ? crypto.randomUUID() : Math.random().toString(36).slice(2)) }-${job.timestamp}`; } export function ActivityLog() { const { user } = useAuth(); const [activities, setActivities] = useState([]); const [isLoading, setIsLoading] = useState(false); const { filter, setFilter } = useFilterParams({ searchTerm: '', status: '', type: '', name: '', }); /* ----------------------------- SSE hook ----------------------------- */ const handleNewMessage = useCallback((data: MirrorJob) => { const withKey: MirrorJobWithKey = { ...structuredClone(data), _rowKey: genKey(data), }; setActivities((prev) => [withKey, ...prev]); }, []); const { connected } = useSSE({ userId: user?.id, onMessage: handleNewMessage, }); /* ------------------------- initial fetch --------------------------- */ const fetchActivities = useCallback(async () => { if (!user) return false; try { setIsLoading(true); const res = await apiRequest( `/activities?userId=${user.id}`, { method: 'GET' }, ); if (!res.success) { toast.error(res.message ?? 'Failed to fetch activities.'); return false; } const data: MirrorJobWithKey[] = res.activities.map((a) => ({ ...structuredClone(a), _rowKey: genKey(a), })); setActivities(data); return true; } catch (err) { toast.error( err instanceof Error ? err.message : 'Failed to fetch activities.', ); return false; } finally { setIsLoading(false); } }, [user]); useEffect(() => { fetchActivities(); }, [fetchActivities]); /* ---------------------- filtering + exporting ---------------------- */ const applyLightFilter = (list: MirrorJobWithKey[]) => { return list.filter((a) => { if (filter.status && a.status !== filter.status) return false; if (filter.type === 'repository' && !a.repositoryId) return false; if (filter.type === 'organization' && !a.organizationId) return false; if ( filter.name && a.repositoryName !== filter.name && a.organizationName !== filter.name ) { return false; } return true; }); }; const exportAsCSV = () => { const rows = applyLightFilter(activities); if (!rows.length) return toast.error('No activities to export.'); const headers = [ 'Timestamp', 'Message', 'Status', 'Repository', 'Organization', 'Details', ]; const escape = (v: string | null | undefined) => v && /[,\"\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v ?? ''; const csv = [ headers.join(','), ...rows.map((a) => [ formatDate(a.timestamp), escape(a.message), a.status, escape(a.repositoryName), escape(a.organizationName), escape(a.details), ].join(','), ), ].join('\n'); downloadFile(csv, 'text/csv;charset=utf-8;', 'activity_log_export.csv'); toast.success('CSV exported.'); }; const exportAsJSON = () => { const rows = applyLightFilter(activities); if (!rows.length) return toast.error('No activities to export.'); const json = JSON.stringify( rows.map((a) => ({ ...a, formattedTime: formatDate(a.timestamp), })), null, 2, ); downloadFile(json, 'application/json', 'activity_log_export.json'); toast.success('JSON exported.'); }; const downloadFile = ( content: string, mime: string, filename: string, ): void => { const date = new Date().toISOString().slice(0, 10); // yyyy-mm-dd const link = document.createElement('a'); link.href = URL.createObjectURL(new Blob([content], { type: mime })); link.download = filename.replace('.', `_${date}.`); link.click(); }; /* ------------------------------ UI ------------------------------ */ return (
{/* search input */}
setFilter((prev) => ({ ...prev, searchTerm: e.target.value, })) } />
{/* status select */} {/* repo/org name combobox */} setFilter((p) => ({ ...p, name }))} /> {/* type select */} {/* export dropdown */} Export as CSV Export as JSON {/* refresh */}
{/* activity list */}
); }