From ad3d42a997d6ab7d048aebd2b58172fc86c1fdd4 Mon Sep 17 00:00:00 2001 From: xCyanGrizzly Date: Tue, 24 Mar 2026 16:24:32 +0100 Subject: [PATCH] feat: add skipped/failed packages table UI components Co-Authored-By: Claude Opus 4.6 (1M context) --- .../stls/_components/skipped-columns.tsx | 135 ++++++++++++++++++ .../stls/_components/skipped-packages-tab.tsx | 77 ++++++++++ 2 files changed, 212 insertions(+) create mode 100644 src/app/(app)/stls/_components/skipped-columns.tsx create mode 100644 src/app/(app)/stls/_components/skipped-packages-tab.tsx diff --git a/src/app/(app)/stls/_components/skipped-columns.tsx b/src/app/(app)/stls/_components/skipped-columns.tsx new file mode 100644 index 0000000..9181847 --- /dev/null +++ b/src/app/(app)/stls/_components/skipped-columns.tsx @@ -0,0 +1,135 @@ +"use client"; + +import { type ColumnDef } from "@tanstack/react-table"; +import { DataTableColumnHeader } from "@/components/shared/data-table-column-header"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { RotateCw } from "lucide-react"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +export interface SkippedRow { + id: string; + fileName: string; + fileSize: string; + reason: "SIZE_LIMIT" | "DOWNLOAD_FAILED" | "EXTRACT_FAILED" | "UPLOAD_FAILED"; + errorMessage: string | null; + sourceChannel: { id: string; title: string }; + isMultipart: boolean; + partCount: number; + createdAt: string; +} + +function formatBytes(bytesStr: string): string { + const bytes = Number(bytesStr); + if (bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; +} + +const REASON_LABELS: Record = { + SIZE_LIMIT: { label: "Size Limit", variant: "secondary" }, + DOWNLOAD_FAILED: { label: "Download Failed", variant: "destructive" }, + EXTRACT_FAILED: { label: "Extract Failed", variant: "destructive" }, + UPLOAD_FAILED: { label: "Upload Failed", variant: "destructive" }, +}; + +export function getSkippedColumns({ + onRetry, +}: { + onRetry: (row: SkippedRow) => void; +}): ColumnDef[] { + return [ + { + accessorKey: "fileName", + header: ({ column }) => , + cell: ({ row }) => ( +
+ {row.original.fileName} + {row.original.isMultipart && ( + + {row.original.partCount} parts + + )} +
+ ), + enableHiding: false, + }, + { + accessorKey: "fileSize", + header: ({ column }) => , + cell: ({ row }) => ( + + {formatBytes(row.original.fileSize)} + + ), + }, + { + accessorKey: "reason", + header: ({ column }) => , + cell: ({ row }) => { + const { label, variant } = REASON_LABELS[row.original.reason]; + return {label}; + }, + }, + { + accessorKey: "errorMessage", + header: "Error", + cell: ({ row }) => { + const msg = row.original.errorMessage; + if (!msg) return {"\u2014"}; + return ( + + + + {msg} + + + +

{msg}

+
+
+ ); + }, + }, + { + id: "channel", + header: ({ column }) => , + cell: ({ row }) => ( + + {row.original.sourceChannel.title} + + ), + accessorFn: (row) => row.sourceChannel.title, + }, + { + accessorKey: "createdAt", + header: ({ column }) => , + cell: ({ row }) => ( + + {new Date(row.original.createdAt).toLocaleDateString()} + + ), + }, + { + id: "actions", + cell: ({ row }) => ( + + ), + enableHiding: false, + }, + ]; +} diff --git a/src/app/(app)/stls/_components/skipped-packages-tab.tsx b/src/app/(app)/stls/_components/skipped-packages-tab.tsx new file mode 100644 index 0000000..7c127a0 --- /dev/null +++ b/src/app/(app)/stls/_components/skipped-packages-tab.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { useTransition } from "react"; +import { useRouter } from "next/navigation"; +import { toast } from "sonner"; +import { RotateCw } from "lucide-react"; +import { useDataTable } from "@/hooks/use-data-table"; +import { getSkippedColumns, type SkippedRow } from "./skipped-columns"; +import { DataTable } from "@/components/shared/data-table"; +import { DataTablePagination } from "@/components/shared/data-table-pagination"; +import { Button } from "@/components/ui/button"; +import { retrySkippedPackageAction, retryAllSkippedPackagesAction } from "../actions"; + +interface SkippedPackagesTabProps { + data: SkippedRow[]; + pageCount: number; + totalCount: number; +} + +export function SkippedPackagesTab({ + data, + pageCount, + totalCount, +}: SkippedPackagesTabProps) { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + + const columns = getSkippedColumns({ + onRetry: (row) => { + startTransition(async () => { + const result = await retrySkippedPackageAction(row.id); + if (result.success) { + toast.success(`"${row.fileName}" queued for retry`); + router.refresh(); + } else { + toast.error(result.error); + } + }); + }, + }); + + const { table } = useDataTable({ data, columns, pageCount }); + + return ( +
+ {totalCount > 0 && ( +
+ +
+ )} + + +
+ ); +}