import { useMemo, useState } from "react"; import { createColumnHelper, type ColumnDef, type SortingState, } from "@tanstack/react-table"; import { Link } from "@tanstack/react-router"; import { ExternalLink } from "lucide-react"; import { AppDataTable, useAppTable, } from "@/client/components/table/AppDataTable"; import { TableExportMenu } from "@/client/components/table/TableBulkActionBar"; import { SortableHeader } from "@/client/components/table/SortableHeader"; import { extractPathname, HttpStatusBadge, LighthouseScoreBadge, } from "@/client/features/audit/shared"; import type { AuditResultsData } from "@/client/features/audit/results/types"; import { countActiveFilters, EmptyTableMessage, PagesFilterBar, PerformanceFilterBar, TableFilterToggle, } from "@/client/features/audit/results/AuditResultsTableFilters"; import { EMPTY_PAGES_FILTERS, EMPTY_PERFORMANCE_FILTERS, filterPages, filterPerformanceRows, isLighthouseFailure, nullableNumberSort, nullableStringSort, type PageRow, type PagesFilters, type PerformanceFilters, type PerformanceRowData, } from "@/client/features/audit/results/AuditResultsTableFilterLogic"; const pageColumnHelper = createColumnHelper(); const performanceColumnHelper = createColumnHelper(); const pagesColumns: ColumnDef[] = [ pageColumnHelper.accessor("url", { header: ({ column }) => , cell: ({ getValue }) => { const url = getValue(); return ( {extractPathname(url)} ); }, meta: { cellClassName: "max-w-[240px] truncate" }, }), pageColumnHelper.accessor("statusCode", { header: ({ column }) => , cell: ({ getValue }) => , sortingFn: nullableNumberSort, }), pageColumnHelper.accessor("title", { header: ({ column }) => , cell: ({ getValue }) => { const title = getValue(); return title ? ( {title} ) : ( missing ); }, sortingFn: nullableStringSort, meta: { cellClassName: "max-w-[220px] truncate" }, }), pageColumnHelper.accessor("h1Count", { header: ({ column }) => , }), pageColumnHelper.accessor("wordCount", { header: ({ column }) => , }), pageColumnHelper.display({ id: "images", header: ({ column }) => , cell: ({ row }) => row.original.imagesMissingAlt > 0 ? ( {row.original.imagesMissingAlt}/{row.original.imagesTotal} ) : ( row.original.imagesTotal ), enableSorting: true, sortingFn: (left, right) => left.original.imagesMissingAlt - right.original.imagesMissingAlt || left.original.imagesTotal - right.original.imagesTotal, }), pageColumnHelper.accessor("responseTimeMs", { header: ({ column }) => , cell: ({ getValue }) => { const value = getValue(); return value ? ( {value}ms ) : ( - ); }, sortingFn: nullableNumberSort, }), ]; export function PagesTable({ pages }: { pages: AuditResultsData["pages"] }) { const [filters, setFilters] = useState(EMPTY_PAGES_FILTERS); const [showFilters, setShowFilters] = useState(false); const [sorting, setSorting] = useState([ { id: "statusCode", desc: true }, ]); const activeFilterCount = countActiveFilters(filters, EMPTY_PAGES_FILTERS); const filteredPages = useMemo( () => filterPages(pages, filters), [filters, pages], ); const table = useAppTable({ data: filteredPages, columns: pagesColumns, state: { sorting }, onSortingChange: setSorting, withSorting: true, }); return (
setShowFilters((current) => !current)} activeFilterCount={activeFilterCount} resultCount={filteredPages.length} totalCount={pages.length} /> {showFilters ? ( setFilters(EMPTY_PAGES_FILTERS)} /> ) : null} } />
); } export function PerformanceTable({ auditId, projectId, lighthouse, pages, }: { auditId: string; projectId: string; lighthouse: AuditResultsData["lighthouse"]; pages: AuditResultsData["pages"]; }) { const [filters, setFilters] = useState( EMPTY_PERFORMANCE_FILTERS, ); const [showFilters, setShowFilters] = useState(false); const [sorting, setSorting] = useState([ { id: "performanceScore", desc: false }, ]); const rows = useMemo( () => lighthouse.map((result) => { const page = pages.find((candidate) => candidate.id === result.pageId); const pageUrl = page?.url ?? null; return { ...result, pageUrl, pagePath: pageUrl ? extractPathname(pageUrl) : null, }; }), [lighthouse, pages], ); const filteredRows = useMemo( () => filterPerformanceRows(rows, filters), [filters, rows], ); const activeFilterCount = countActiveFilters( filters, EMPTY_PERFORMANCE_FILTERS, ); const columns = useMemo( () => buildPerformanceColumns({ auditId, projectId }), [auditId, projectId], ); const table = useAppTable({ data: filteredRows, columns, state: { sorting }, onSortingChange: setSorting, withSorting: true, }); return (
setShowFilters((current) => !current)} activeFilterCount={activeFilterCount} resultCount={filteredRows.length} totalCount={rows.length} /> {showFilters ? ( setFilters(EMPTY_PERFORMANCE_FILTERS)} /> ) : null} } />
); } function buildPerformanceColumns({ auditId, projectId, }: { auditId: string; projectId: string; }): ColumnDef[] { return [ performanceColumnHelper.accessor("pagePath", { header: ({ column }) => , cell: ({ getValue }) => ( {getValue() ?? "-"} ), sortingFn: nullableStringSort, meta: { cellClassName: "max-w-[180px] truncate" }, }), performanceColumnHelper.accessor("strategy", { header: ({ column }) => , cell: ({ getValue }) => ( {getValue()} ), }), performanceColumnHelper.display({ id: "status", header: ({ column }) => , cell: ({ row }) => { const isFailed = isLighthouseFailure(row.original); const failureMessage = row.original.errorMessage ?? "Lighthouse returned no category scores"; return isFailed ? ( failed ) : ( ok ); }, enableSorting: true, sortingFn: (left, right) => Number(isLighthouseFailure(left.original)) - Number(isLighthouseFailure(right.original)), }), performanceColumnHelper.accessor("performanceScore", { header: ({ column }) => , cell: ({ getValue }) => , sortingFn: nullableNumberSort, }), performanceColumnHelper.accessor("accessibilityScore", { header: ({ column }) => , cell: ({ getValue }) => , sortingFn: nullableNumberSort, }), performanceColumnHelper.accessor("seoScore", { header: ({ column }) => , cell: ({ getValue }) => , sortingFn: nullableNumberSort, }), performanceColumnHelper.accessor("lcpMs", { header: ({ column }) => , cell: ({ getValue }) => { const value = getValue(); return value ? ( {(value / 1000).toFixed(1)}s ) : ( - ); }, sortingFn: nullableNumberSort, }), performanceColumnHelper.accessor("cls", { header: ({ column }) => , cell: ({ getValue }) => { const value = getValue(); return value != null ? ( {value.toFixed(3)} ) : ( - ); }, sortingFn: nullableNumberSort, }), performanceColumnHelper.accessor("inpMs", { header: ({ column }) => , cell: ({ getValue }) => { const value = getValue(); return value ? ( {Math.round(value)}ms ) : ( - ); }, sortingFn: nullableNumberSort, }), performanceColumnHelper.accessor("ttfbMs", { header: ({ column }) => , cell: ({ getValue }) => { const value = getValue(); return value ? ( {Math.round(value)}ms ) : ( - ); }, sortingFn: nullableNumberSort, }), performanceColumnHelper.display({ id: "issues", header: () => "Issues", cell: ({ row }) => row.original.r2Key && !isLighthouseFailure(row.original) ? ( View issues ) : ( - ), }), ]; } export function ExportDropdown({ onExport, }: { onExport: (format: "csv" | "json" | "sheets") => void; }) { return ( onExport("sheets") }, { label: "CSV", onClick: () => onExport("csv") }, { label: "JSON", onClick: () => onExport("json") }, ]} /> ); }