diff --git a/src/pages/RepoPage/CoverageTab/OverviewTab/OverviewTab.tsx b/src/pages/RepoPage/CoverageTab/OverviewTab/OverviewTab.tsx index 3ab08076f9..5a1dc2531a 100644 --- a/src/pages/RepoPage/CoverageTab/OverviewTab/OverviewTab.tsx +++ b/src/pages/RepoPage/CoverageTab/OverviewTab/OverviewTab.tsx @@ -13,6 +13,7 @@ import { cn } from 'shared/utils/cn' import Spinner from 'ui/Spinner' import { ToggleElement } from 'ui/ToggleElement' +import { CoverageMetrics } from './components/CoverageMetrics/CoverageMetrics' import FirstPullRequestBanner from './FirstPullRequestBanner' import { CoverageTabDataQueryOpts } from './queries/CoverageTabDataQueryOpts' import CoverageChart from './subroute/CoverageChart' @@ -122,6 +123,26 @@ function CoverageOverviewTab() { ) : null} + + + + + + + + + }> diff --git a/src/pages/RepoPage/CoverageTab/OverviewTab/components/CoverageMetrics/CoverageMetrics.tsx b/src/pages/RepoPage/CoverageTab/OverviewTab/components/CoverageMetrics/CoverageMetrics.tsx new file mode 100644 index 0000000000..ebba0650fb --- /dev/null +++ b/src/pages/RepoPage/CoverageTab/OverviewTab/components/CoverageMetrics/CoverageMetrics.tsx @@ -0,0 +1,282 @@ +import { cn } from 'shared/utils/cn' + +interface CoverageFile { + name: string + path: string + coverage: number + lines: number + hits: number + misses: number +} + +interface CoverageMetricsProps { + threshold?: number +} + +/** + * CoverageMetrics displays comprehensive coverage statistics for a repository. + * + * This component processes all files in a repository to calculate metrics. + * Large repositories commonly have 2,000-10,000+ files, and enterprise repos + * can exceed 50,000 files. The component handles the full file list for + * accurate statistics rather than paginated subsets. + * + * Used in: + * - RepoPage Coverage Overview Tab (main dashboard) + * - PR coverage comparison views + * - Commit detail coverage breakdown + * + * @param threshold - Coverage percentage threshold for highlighting low-coverage files + */ + +// TODO: Replace with useCoverageFiles() hook that fetches from the API +// This sample data generator simulates production data with 5000 files +const generateSampleFiles = (): CoverageFile[] => { + const directories = [ + 'src/pages/RepoPage', + 'src/services', + 'src/layouts/Header', + 'src/ui/Button', + 'src/shared/utils', + 'tests/integration', + ] + const files: CoverageFile[] = [] + + for (let i = 0; i < 5000; i++) { + const dir = directories[i % directories.length] + const coverage = Math.random() * 100 + const lines = Math.floor(Math.random() * 500) + 50 + const hits = Math.floor((coverage / 100) * lines) + + files.push({ + name: `File${i}.tsx`, + path: `${dir}/File${i}.tsx`, + coverage, + lines, + hits, + misses: lines - hits, + }) + } + + return files +} + +export function CoverageMetrics({ threshold = 50 }: CoverageMetricsProps) { + // TODO: Replace generateSampleFiles() with const files = useCoverageFiles() + const files = generateSampleFiles() + + if (!files || files.length === 0) { + return null + } + + // Calculate comprehensive statistics in a single pass for efficiency + const statistics = files.reduce( + (acc, file) => { + acc.totalFiles++ + acc.totalCoverage += file.coverage + acc.totalLines += file.lines + acc.totalHits += file.hits + acc.totalMisses += file.misses + + if (file.coverage < threshold) { + acc.lowCoverageFiles++ + } + + // Distribution buckets + if (file.coverage < 25) { + acc.distribution['0-25%']++ + } else if (file.coverage < 50) { + acc.distribution['25-50%']++ + } else if (file.coverage < 75) { + acc.distribution['50-75%']++ + } else { + acc.distribution['75-100%']++ + } + + return acc + }, + { + totalFiles: 0, + lowCoverageFiles: 0, + totalCoverage: 0, + totalLines: 0, + totalHits: 0, + totalMisses: 0, + distribution: { '0-25%': 0, '25-50%': 0, '50-75%': 0, '75-100%': 0 }, + } + ) + + const averageCoverage = + statistics.totalFiles > 0 + ? statistics.totalCoverage / statistics.totalFiles + : 0 + const overallCoverageRate = + statistics.totalLines > 0 + ? (statistics.totalHits / statistics.totalLines) * 100 + : 0 + const coverageDistribution = statistics.distribution + + const sortedFiles = files + .filter((file) => file.coverage < threshold) + .sort((a, b) => a.coverage - b.coverage) + + // Calculate directory-level statistics + const directoryStats = files.reduce( + ( + acc: Record< + string, + { files: number; avgCoverage: number; totalLines: number } + >, + file + ) => { + const directory = file.path.includes('/') + ? file.path.substring(0, file.path.lastIndexOf('/')) + : '(root)' + + if (!acc[directory]) { + acc[directory] = { files: 0, avgCoverage: 0, totalLines: 0 } + } + + const dirData = acc[directory] + if (dirData) { + dirData.files++ + dirData.avgCoverage = + (dirData.avgCoverage * (dirData.files - 1) + file.coverage) / + dirData.files + dirData.totalLines += file.lines + } + + return acc + }, + {} + ) + + // Sort directories by lines of code + const topDirectories = Object.entries(directoryStats) + .sort(([, a], [, b]) => b.totalLines - a.totalLines) + .slice(0, 5) + + return ( + + + + + Average Coverage + + + {averageCoverage.toFixed(2)}% + + + + + + Overall Rate + + + {overallCoverageRate.toFixed(2)}% + + + + + + Low Coverage Files + + + {statistics.lowCoverageFiles} + + + + + + Total Lines + + + {statistics.totalLines.toLocaleString()} + + + + + + Coverage Distribution + + {Object.entries(coverageDistribution).map(([range, count]) => ( + 0 + ? 'border-ds-gray-tertiary bg-white' + : 'border-ds-gray-secondary bg-ds-gray-primary' + )} + > + {range} + {count} + + ))} + + + + + Top Directories by Size + + {topDirectories.map(([directory, stats]) => ( + + + {directory} + + {stats.files} files • {stats.totalLines.toLocaleString()}{' '} + lines + + + + {stats.avgCoverage.toFixed(1)}% + + + ))} + + + + {sortedFiles.length > 0 && ( + + + Files Below {threshold}% Coverage + + + {sortedFiles.slice(0, 10).map((file) => ( + + {file.path} + + {file.coverage.toFixed(1)}% + + + ))} + + + )} + + ) +} + +export default CoverageMetrics diff --git a/src/pages/RepoPage/CoverageTab/OverviewTab/components/CoverageMetrics/index.ts b/src/pages/RepoPage/CoverageTab/OverviewTab/components/CoverageMetrics/index.ts new file mode 100644 index 0000000000..23952a48f3 --- /dev/null +++ b/src/pages/RepoPage/CoverageTab/OverviewTab/components/CoverageMetrics/index.ts @@ -0,0 +1,2 @@ +export { default } from './CoverageMetrics' +export { CoverageMetrics } from './CoverageMetrics'
+ {averageCoverage.toFixed(2)}% +
+ {overallCoverageRate.toFixed(2)}% +
+ {statistics.lowCoverageFiles} +
+ {statistics.totalLines.toLocaleString()} +