feat: add skipped/failed packages tab to STL files page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 16:36:39 +01:00
parent ad3d42a997
commit 5a3550fa10
2 changed files with 95 additions and 35 deletions

View File

@@ -8,6 +8,7 @@ import { useDataTable } from "@/hooks/use-data-table";
import { getPackageColumns, type PackageRow } from "./package-columns"; import { getPackageColumns, type PackageRow } from "./package-columns";
import { PackageFilesDrawer } from "./package-files-drawer"; import { PackageFilesDrawer } from "./package-files-drawer";
import { IngestionStatus } from "./ingestion-status"; import { IngestionStatus } from "./ingestion-status";
import { SkippedPackagesTab } from "./skipped-packages-tab";
import { DataTable } from "@/components/shared/data-table"; import { DataTable } from "@/components/shared/data-table";
import { DataTablePagination } from "@/components/shared/data-table-pagination"; import { DataTablePagination } from "@/components/shared/data-table-pagination";
import { DataTableViewOptions } from "@/components/shared/data-table-view-options"; import { DataTableViewOptions } from "@/components/shared/data-table-view-options";
@@ -20,7 +21,10 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import type { IngestionAccountStatus } from "@/lib/telegram/types"; import type { IngestionAccountStatus } from "@/lib/telegram/types";
import type { SkippedRow } from "./skipped-columns";
import { updatePackageCreator, updatePackageTags } from "../actions"; import { updatePackageCreator, updatePackageTags } from "../actions";
interface StlTableProps { interface StlTableProps {
@@ -30,6 +34,9 @@ interface StlTableProps {
ingestionStatus: IngestionAccountStatus[]; ingestionStatus: IngestionAccountStatus[];
availableTags: string[]; availableTags: string[];
searchTerm: string; searchTerm: string;
skippedData: SkippedRow[];
skippedPageCount: number;
skippedTotalCount: number;
} }
export function StlTable({ export function StlTable({
@@ -39,6 +46,9 @@ export function StlTable({
ingestionStatus, ingestionStatus,
availableTags, availableTags,
searchTerm, searchTerm,
skippedData,
skippedPageCount,
skippedTotalCount,
}: StlTableProps) { }: StlTableProps) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
@@ -77,6 +87,22 @@ export function StlTable({
[router, pathname, searchParams] [router, pathname, searchParams]
); );
const activeTab = searchParams.get("tab") ?? "packages";
const updateTab = useCallback(
(value: string) => {
const params = new URLSearchParams(searchParams.toString());
if (value === "packages") {
params.delete("tab");
} else {
params.set("tab", value);
}
params.set("page", "1");
router.push(`${pathname}?${params.toString()}`, { scroll: false });
},
[router, pathname, searchParams]
);
const columns = getPackageColumns({ const columns = getPackageColumns({
onViewFiles: (pkg) => setViewPkg(pkg), onViewFiles: (pkg) => setViewPkg(pkg),
searchTerm, searchTerm,
@@ -125,39 +151,63 @@ export function StlTable({
<IngestionStatus initialStatus={ingestionStatus} /> <IngestionStatus initialStatus={ingestionStatus} />
</PageHeader> </PageHeader>
<div className="flex flex-wrap items-center gap-2"> <Tabs value={activeTab} onValueChange={updateTab}>
<div className="relative flex-1 min-w-[200px] max-w-sm"> <TabsList>
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" /> <TabsTrigger value="packages">Packages</TabsTrigger>
<Input <TabsTrigger value="skipped" className="gap-1.5">
placeholder="Search packages or files..." Skipped / Failed
value={searchValue} {skippedTotalCount > 0 && (
onChange={(e) => updateSearch(e.target.value)} <Badge variant="secondary" className="text-[10px] ml-1">
className="pl-9 h-9" {skippedTotalCount}
/> </Badge>
</div> )}
{availableTags.length > 0 && ( </TabsTrigger>
<Select value={activeTag || "all"} onValueChange={updateTagFilter}> </TabsList>
<SelectTrigger className="w-[160px] h-9">
<SelectValue placeholder="All Tags" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Tags</SelectItem>
{availableTags.map((tag) => (
<SelectItem key={tag} value={tag}>
{tag}
</SelectItem>
))}
</SelectContent>
</Select>
)}
<DataTableViewOptions table={table} />
</div>
<DataTable <TabsContent value="packages" className="space-y-4">
table={table} <div className="flex flex-wrap items-center gap-2">
emptyMessage="No packages found. Archives will appear here after ingestion." <div className="relative flex-1 min-w-[200px] max-w-sm">
/> <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<DataTablePagination table={table} totalCount={totalCount} /> <Input
placeholder="Search packages or files..."
value={searchValue}
onChange={(e) => updateSearch(e.target.value)}
className="pl-9 h-9"
/>
</div>
{availableTags.length > 0 && (
<Select value={activeTag || "all"} onValueChange={updateTagFilter}>
<SelectTrigger className="w-[160px] h-9">
<SelectValue placeholder="All Tags" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Tags</SelectItem>
{availableTags.map((tag) => (
<SelectItem key={tag} value={tag}>
{tag}
</SelectItem>
))}
</SelectContent>
</Select>
)}
<DataTableViewOptions table={table} />
</div>
<DataTable
table={table}
emptyMessage="No packages found. Archives will appear here after ingestion."
/>
<DataTablePagination table={table} totalCount={totalCount} />
</TabsContent>
<TabsContent value="skipped">
<SkippedPackagesTab
data={skippedData}
pageCount={skippedPageCount}
totalCount={skippedTotalCount}
/>
</TabsContent>
</Tabs>
<PackageFilesDrawer <PackageFilesDrawer
pkg={viewPkg} pkg={viewPkg}

View File

@@ -1,6 +1,6 @@
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { listPackages, searchPackages, getIngestionStatus, getAllPackageTags } from "@/lib/telegram/queries"; import { listPackages, searchPackages, getIngestionStatus, getAllPackageTags, listSkippedPackages, countSkippedPackages } from "@/lib/telegram/queries";
import { StlTable } from "./_components/stl-table"; import { StlTable } from "./_components/stl-table";
interface Props { interface Props {
@@ -20,9 +20,10 @@ export default async function StlFilesPage({ searchParams }: Props) {
const search = (params.search as string) ?? ""; const search = (params.search as string) ?? "";
const creator = (params.creator as string) || undefined; const creator = (params.creator as string) || undefined;
const tag = (params.tag as string) || undefined; const tag = (params.tag as string) || undefined;
const tab = (params.tab as string) ?? "packages";
// Fetch packages, ingestion status, and available tags in parallel // Fetch packages, ingestion status, tags, and skipped count in parallel
const [result, ingestionStatus, availableTags] = await Promise.all([ const [result, ingestionStatus, availableTags, skippedCount] = await Promise.all([
search search
? searchPackages({ ? searchPackages({
query: search, query: search,
@@ -40,8 +41,14 @@ export default async function StlFilesPage({ searchParams }: Props) {
}), }),
getIngestionStatus(), getIngestionStatus(),
getAllPackageTags(), getAllPackageTags(),
countSkippedPackages(),
]); ]);
// Fetch skipped packages only if on that tab
const skippedResult = tab === "skipped"
? await listSkippedPackages({ page, limit: perPage })
: null;
return ( return (
<StlTable <StlTable
data={result.items} data={result.items}
@@ -50,6 +57,9 @@ export default async function StlFilesPage({ searchParams }: Props) {
ingestionStatus={ingestionStatus} ingestionStatus={ingestionStatus}
availableTags={availableTags} availableTags={availableTags}
searchTerm={search} searchTerm={search}
skippedData={skippedResult?.items ?? []}
skippedPageCount={skippedResult?.pagination.totalPages ?? 0}
skippedTotalCount={skippedCount}
/> />
); );
} }