mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-05-10 22:01:16 +00:00
feat: add skipped/failed packages table UI components
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
135
src/app/(app)/stls/_components/skipped-columns.tsx
Normal file
135
src/app/(app)/stls/_components/skipped-columns.tsx
Normal file
@@ -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<SkippedRow["reason"], { label: string; variant: "default" | "destructive" | "outline" | "secondary" }> = {
|
||||||
|
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<SkippedRow, unknown>[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessorKey: "fileName",
|
||||||
|
header: ({ column }) => <DataTableColumnHeader column={column} title="File Name" />,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<span className="font-medium truncate max-w-[300px]">{row.original.fileName}</span>
|
||||||
|
{row.original.isMultipart && (
|
||||||
|
<Badge variant="outline" className="text-[10px] shrink-0">
|
||||||
|
{row.original.partCount} parts
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
enableHiding: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "fileSize",
|
||||||
|
header: ({ column }) => <DataTableColumnHeader column={column} title="Size" />,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{formatBytes(row.original.fileSize)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "reason",
|
||||||
|
header: ({ column }) => <DataTableColumnHeader column={column} title="Reason" />,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const { label, variant } = REASON_LABELS[row.original.reason];
|
||||||
|
return <Badge variant={variant} className="text-[10px]">{label}</Badge>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "errorMessage",
|
||||||
|
header: "Error",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const msg = row.original.errorMessage;
|
||||||
|
if (!msg) return <span className="text-sm text-muted-foreground">{"\u2014"}</span>;
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span className="text-sm text-muted-foreground truncate max-w-[200px] block cursor-help">
|
||||||
|
{msg}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-sm">
|
||||||
|
<p className="text-xs break-all">{msg}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "channel",
|
||||||
|
header: ({ column }) => <DataTableColumnHeader column={column} title="Source" />,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-sm text-muted-foreground truncate max-w-[160px] block">
|
||||||
|
{row.original.sourceChannel.title}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
accessorFn: (row) => row.sourceChannel.title,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "createdAt",
|
||||||
|
header: ({ column }) => <DataTableColumnHeader column={column} title="Skipped" />,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{new Date(row.original.createdAt).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={() => onRetry(row.original)}
|
||||||
|
title="Retry this package"
|
||||||
|
>
|
||||||
|
<RotateCw className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
enableHiding: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
77
src/app/(app)/stls/_components/skipped-packages-tab.tsx
Normal file
77
src/app/(app)/stls/_components/skipped-packages-tab.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{totalCount > 0 && (
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-1.5"
|
||||||
|
disabled={isPending}
|
||||||
|
onClick={() => {
|
||||||
|
startTransition(async () => {
|
||||||
|
const result = await retryAllSkippedPackagesAction();
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(`All ${totalCount} skipped packages queued for retry`);
|
||||||
|
router.refresh();
|
||||||
|
} else {
|
||||||
|
toast.error(result.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RotateCw className="h-3.5 w-3.5" />
|
||||||
|
Retry All ({totalCount})
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DataTable
|
||||||
|
table={table}
|
||||||
|
emptyMessage="No skipped or failed packages."
|
||||||
|
/>
|
||||||
|
<DataTablePagination table={table} totalCount={totalCount} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user