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 && (
)}
{isLoading ? (
) : items.length === 0 ? (
{hasActiveFilters ? 'No reports match the selected filters' : 'No reports available yet'}
) : (
| Application |
System |
Event |
Occurrences |
First Seen |
Last Seen |
Status |
Tags / Note |
Details |
Actions |
{items.map((group: ReportGroup) => (
|
{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}
/>
)}
);
};