import { useState } from 'react'; import { Header } from '@/components/layout/Header'; import { Sidebar } from '@/components/layout/Sidebar'; import { useReportsQuery, useReportGroupMutations, ReportGroup, ReportFilters, ReportStatus, ReportStatusFilter, } from '@/hooks/use-query/useReportsQuery'; import { useAppsQuery, AppListItem } from '@/hooks/use-query/useAppsQuery'; import { useChannelQuery, Channel } from '@/hooks/use-query/useChannelQuery'; import { usePlatformQuery, Platform } from '@/hooks/use-query/usePlatformQuery'; import { useArchitectureQuery, Architecture } from '@/hooks/use-query/useArchitectureQuery'; import { ReportBlobsModal } from '@/components/modals/ReportBlobsModal'; import { DeleteReportConfirmationModal } from '@/components/modals/DeleteReportConfirmationModal'; import { EditReportGroupModal } from '@/components/modals/EditReportGroupModal'; import { useToast } from '@/hooks/useToast'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; import '@/styles/cards.css'; import { Dropdown } from '@/components/common/Dropdown'; import { DROPDOWN_MENU_STYLE, FIELD_INPUT, FIELD_LABEL } from '@/components/common/ui'; const PANEL_CLASS = 'bg-theme-card rounded-lg border border-theme-card-hover shadow-md backdrop-blur-lg'; const REPORTS_PAGE_LIMIT = 20; const EVENT_TYPES = ['crash', 'startup_failure', 'update_failure', 'install_failure', 'rollback_failure']; const STATUS_TABS: { value: ReportStatusFilter; label: string }[] = [ { value: 'open', label: 'Open' }, { value: 'resolved', label: 'Resolved' }, { value: 'muted', label: 'Muted' }, { value: 'all', label: 'All' }, ]; const DEFAULT_STATUS: ReportStatusFilter = 'open'; const statusBadgeClass = (status: ReportStatus) => { switch (status) { case 'resolved': return 'bg-green-500/20 text-green-300 border-green-400/30'; case 'muted': return 'bg-gray-500/20 text-gray-300 border-gray-400/30'; default: return 'bg-amber-500/20 text-amber-300 border-amber-400/30'; } }; const formatDateTime = (value: string) => { const date = new Date(value); if (Number.isNaN(date.getTime())) { return value; } const day = date.getDate(); const month = date.toLocaleString('en-US', { month: 'short' }); const year = date.getFullYear(); const hours = date.getHours().toString().padStart(2, '0'); const minutes = date.getMinutes().toString().padStart(2, '0'); return `${day} ${month} ${year}, ${hours}:${minutes}`; }; const formatLabel = (value: string | null | undefined) => (value ?? '') .split('_') .filter(Boolean) .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); const getErrorMessage = (error: unknown, fallback: string): string => { if (typeof error === 'object' && error !== null) { const maybe = error as { response?: { data?: { error?: string; message?: string } }; message?: string }; return maybe.response?.data?.error || maybe.response?.data?.message || maybe.message || fallback; } return fallback; }; const eventTypeBadgeClass = (type: string | null | undefined) => (type ?? '').toLowerCase().includes('failure') || (type ?? '').toLowerCase().includes('error') ? 'bg-red-500/20 text-red-300 border-red-400/30' : 'bg-blue-500/20 text-blue-300 border-blue-400/30'; const Badge = ({ label, className }: { label: string; className: string }) => ( {label} ); const FilterSelect = ({ label, value, anyLabel, options, onSelect, }: { label: string; value: string; anyLabel: string; options: string[]; onSelect: (value: string) => void; }) => (
({ value: option, label: option }))]} />
); export const ReportsPage = () => { const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [page, setPage] = useState(1); const [status, setStatus] = useState(DEFAULT_STATUS); const [filters, setFilters] = useState({}); const [fromDate, setFromDate] = useState(null); const [toDate, setToDate] = useState(null); const [selectedGroup, setSelectedGroup] = useState(null); const [groupToDelete, setGroupToDelete] = useState(null); const [groupToEdit, setGroupToEdit] = useState(null); const [pendingHash, setPendingHash] = useState(null); const { toastSuccess, toastError } = useToast(); const { apps = [] } = useAppsQuery(); const { channels = [] } = useChannelQuery(); const { platforms = [] } = usePlatformQuery(); const { architectures = [] } = useArchitectureQuery(); const { reports, isLoading } = useReportsQuery(page, REPORTS_PAGE_LIMIT, { ...filters, status }); const { updateGroupMutation, deleteGroupMutation } = useReportGroupMutations(); const items = reports?.items ?? []; const total = reports?.total ?? 0; const totalPages = Math.max(1, Math.ceil(total / REPORTS_PAGE_LIMIT)); const hasActiveFilters = Object.values(filters).some(Boolean); const handlePageChange = (nextPage: number) => { setPage(Math.min(Math.max(1, nextPage), totalPages)); }; const updateFilter = (key: Exclude, value: string) => { setPage(1); setFilters((prev) => { const next = { ...prev }; if (value) { next[key] = value; } else { delete next[key]; } return next; }); }; const handleSelect = (key: Exclude) => (value: string) => { updateFilter(key, value); }; const handleFromChange = (date: Date | null) => { setFromDate(date); updateFilter('from', date ? date.toISOString() : ''); }; const handleToChange = (date: Date | null) => { setToDate(date); updateFilter('to', date ? date.toISOString() : ''); }; const handleClearAll = () => { setPage(1); setFilters({}); setFromDate(null); setToDate(null); }; const handleStatusTab = (value: ReportStatusFilter) => { setPage(1); setStatus(value); }; const handleUpdateStatus = (group: ReportGroup, next: ReportStatus) => { setPendingHash(group.group_hash); updateGroupMutation.mutate( { groupHash: group.group_hash, status: next }, { onSuccess: () => { toastSuccess(`Group marked as ${next}`); setPendingHash(null); }, onError: (error) => { toastError(getErrorMessage(error, 'Failed to update group status')); setPendingHash(null); }, } ); }; const handleEditGroup = async (data: { tags: string[]; note: string }) => { if (!groupToEdit) return; try { await updateGroupMutation.mutateAsync({ groupHash: groupToEdit.group_hash, ...data }); toastSuccess('Report group updated'); setGroupToEdit(null); } catch (error) { toastError(getErrorMessage(error, 'Failed to update report group')); } }; const handleDeleteGroup = async () => { if (!groupToDelete) return; try { await deleteGroupMutation.mutateAsync(groupToDelete.group_hash); toastSuccess('Report group deleted'); setGroupToDelete(null); } catch (error) { toastError(getErrorMessage(error, 'Failed to delete report group')); } }; const renderHeader = () => (
setIsSidebarOpen(true)} onCreateClick={() => {}} createButtonText="" hideSearch={true} /> ); return (
setIsSidebarOpen(false)} />
{renderHeader()}
{STATUS_TABS.map((tab) => ( ))}

Filters

{total} group{total === 1 ? '' : 's'} total {hasActiveFilters && ( )}
app.AppName)} onSelect={handleSelect('app')} /> channel.ChannelName)} onSelect={handleSelect('channel')} /> platform.PlatformName)} onSelect={handleSelect('platform')} /> arch.ArchID)} onSelect={handleSelect('arch')} />
updateFilter('version', e.target.value)} placeholder="e.g. 1.4.2" className={FIELD_INPUT} />
updateFilter('reason', e.target.value)} placeholder="e.g. checksum_mismatch" className={FIELD_INPUT} />
{isLoading ? (
) : items.length === 0 ? (

{hasActiveFilters ? 'No reports match the selected filters' : 'No reports available yet'}

) : (
{items.map((group: ReportGroup) => ( ))}
Application System Event Occurrences First Seen Last Seen Status Tags / Note Details Actions
{group.application.name}
v{group.application.version} · {group.application.channel}
{group.system.platform}
{group.system.arch}
{group.stats.count} {formatDateTime(group.stats.first_seen)} {formatDateTime(group.stats.last_seen)}
{group.resolved_at && (
{group.resolved_by && by {group.resolved_by}}
{formatDateTime(group.resolved_at)}
)}
{group.tags && group.tags.length > 0 && (
{group.tags.map((tag) => ( ))}
)} {group.note && (
{group.note}
)} {!(group.tags && group.tags.length > 0) && !group.note && ( — )}
{group.stats.details_stored > 0 ? ( ) : ( — )}
{group.status !== 'resolved' && ( )} {group.status !== 'open' && ( )} {group.status !== 'muted' && ( )}
)} {totalPages > 1 && (
Page {page} of {totalPages}
)}
{selectedGroup && ( setSelectedGroup(null)} /> )} {groupToEdit && ( setGroupToEdit(null)} onConfirm={handleEditGroup} /> )} {groupToDelete && ( setGroupToDelete(null)} onConfirm={handleDeleteGroup} /> )}
); };