diff --git a/Frontend/src/components/TopContributorsRepos.jsx b/Frontend/src/components/TopContributorsRepos.jsx new file mode 100644 index 0000000..9819399 --- /dev/null +++ b/Frontend/src/components/TopContributorsRepos.jsx @@ -0,0 +1,53 @@ +/* + Component to display the top contributors and top repositories. + + This component is responsible only for rendering UI elements. + All data aggregation and processing logic is delegated to a helper + function to maintain separation of concenrs and improve reusability. + + Parms: + events (Array): List of GitHub event objects passed from the parent component +*/ + +import { getTopContributorsAndRepos } from "../utils/getTopContributorsAndRepos"; + +const TOP_N = 5; + +const TopContributorsRepos = ({ events = [] }) => { + /* + Retrieve pre-processes contributor and repository statistics + The helper function handles counting, sorting, and limiting results + */ + const { topContributors, topRepos } = + getTopContributorsAndRepos(events, TOP_N); + + return ( +
+ {/* Render list of top contributors by activity count */} +
+

Top Contributors

+
    + {topContributors.map((user) => ( +
  • + {user.name} ({user.count}) +
  • + ))} +
+
+ + {/* Render list of top repositories by activity count */} +
+

Top Repositories

+
    + {topRepos.map((repo) => ( +
  • + {repo.name} ({repo.count}) +
  • + ))} +
+
+
+ ); +}; + +export default TopContributorsRepos; \ No newline at end of file diff --git a/Frontend/src/components/charts/__tests__/getTopContributorsAndRepos.test.js b/Frontend/src/components/charts/__tests__/getTopContributorsAndRepos.test.js new file mode 100644 index 0000000..61b95ae --- /dev/null +++ b/Frontend/src/components/charts/__tests__/getTopContributorsAndRepos.test.js @@ -0,0 +1,53 @@ +/* + Unit tests for the getTopContributorsAndRepos helper function + + These tests focus exclusively on verifying the correctness of + counting and ranking logic for contributors and repositories. + UI rendering is intentionally excluded to keep tests isolated + and easy to maintain. +*/ +import { getTopContributorsAndRepos } from "../../../utils/getTopContributorsAndRepos"; + +describe("getTopContributorsAndRepos", () => { + it("counts contributor activity and sorts correctly", () => { + const events = [ + { author: "alice", repo: "repo1" }, + { author: "bob", repo: "repo1" }, + { author: "alice", repo: "repo2" }, + ]; + + const { topContributors } = + getTopContributorsAndRepos(events, 5); + + // alice should rank first since she appears twice + expect(topContributors).toEqual([ + { name: "alice", count: 2 }, + { name: "bob", count: 1 }, + ]); + }); + + it("counts repository activity and sorts correctly", () => { + const events = [ + { author: "alice", repo: "repo1" }, + { author: "bob", repo: "repo1" }, + { author: "charlie", repo: "repo2" }, + ]; + + const { topRepos } = + getTopContributorsAndRepos(events, 5); + + // repo1 should rank higher since it appears more frequently + expect(topRepos).toEqual([ + { name: "repo1", count: 2 }, + { name: "repo2", count: 1 }, + ]); + }); + + it("handles empty input without crashing", () => { + const { topContributors, topRepos } = + getTopContributorsAndRepos([], 5); + + expect(topContributors).toEqual([]); + expect(topRepos).toEqual([]); + }); +}); \ No newline at end of file diff --git a/Frontend/src/features/home/routes/Home.jsx b/Frontend/src/features/home/routes/Home.jsx index 22b798c..a1057d7 100644 --- a/Frontend/src/features/home/routes/Home.jsx +++ b/Frontend/src/features/home/routes/Home.jsx @@ -1,8 +1,39 @@ +// Home dashboard page +// Shows org-level overview using existing components + +import TopContributorsRepos from "../../../components/TopContributorsRepos"; +import VolumeChart from "../../../components/charts/VolumeBased"; + +import testData from "../../../test_data.json"; +import { transformVolumeData } from "../../../utils/TransformVolumeData"; + export const Home = () => { + const volumeData = transformVolumeData(testData); + return (
-

OSS Dev Analytics - Home

-

Welcome to the dashboard.

+

Organization Overview

+ + {/* Top contributors & repositories */} +
+ +
+ + {/* Existing volume chart (already on main) */} +
+ +
); }; \ No newline at end of file diff --git a/Frontend/src/features/team-stats/routes/TeamStats.jsx b/Frontend/src/features/team-stats/routes/TeamStats.jsx index 1e404a9..101221c 100644 --- a/Frontend/src/features/team-stats/routes/TeamStats.jsx +++ b/Frontend/src/features/team-stats/routes/TeamStats.jsx @@ -2,6 +2,7 @@ export const TeamStats = () => { return (

Team Statistics

+ {/* Repository and user-level metrics will be added here */}
); }; \ No newline at end of file diff --git a/Frontend/src/test_data.json b/Frontend/src/test_data.json new file mode 100644 index 0000000..69b9cef --- /dev/null +++ b/Frontend/src/test_data.json @@ -0,0 +1,283 @@ +{ + "lrda_mobile": { + "issues": { + "AmarHadzic": { + "average_time_to_close": 0, + "total_issues_opened": 1, + "total_issues_closed": 0 + }, + "AndchooChen": { + "average_time_to_close": 1117.6725980392157, + "total_issues_opened": 21, + "total_issues_closed": "17" + }, + "InfinityBowman": { + "average_time_to_close": 0, + "total_issues_opened": 1, + "total_issues_closed": 0 + }, + "SamSam9812": { + "average_time_to_close": 4203.242037037037, + "total_issues_opened": 3, + "total_issues_closed": "3" + }, + "Stuartwastaken": { + "average_time_to_close": 1196.651717171717, + "total_issues_opened": 23, + "total_issues_closed": "22" + }, + "ademDurakovic": { + "average_time_to_close": 0, + "total_issues_opened": 2, + "total_issues_closed": 0 + }, + "irvinet20": { + "average_time_to_close": 3076.3651736111115, + "total_issues_opened": 10, + "total_issues_closed": "8" + }, + "izakrobles": { + "average_time_to_close": 365.92055555555555, + "total_issues_opened": 6, + "total_issues_closed": "4" + }, + "rcAsironman": { + "average_time_to_close": 1363.693484848485, + "total_issues_opened": 45, + "total_issues_closed": "33" + }, + "teamomiamigo": { + "average_time_to_close": 0, + "total_issues_opened": 1, + "total_issues_closed": 0 + }, + "yashcoded": { + "average_time_to_close": 514.4426825396826, + "total_issues_opened": 38, + "total_issues_closed": "35" + } + }, + "pull_requests": { + "AmarHadzic": { + "average_time_to_merge": 422.4528787878789, + "total_prs_opened": 12, + "total_prs_merged": 0 + }, + "AndchooChen": { + "average_time_to_merge": 241.11543981481483, + "total_prs_opened": 18, + "total_prs_merged": 0 + }, + "InfinityBowman": { + "average_time_to_merge": 318.1827777777778, + "total_prs_opened": 2, + "total_prs_merged": 0 + }, + "SamSam9812": { + "average_time_to_merge": 184.49203703703702, + "total_prs_opened": 6, + "total_prs_merged": 0 + }, + "Stuartwastaken": { + "average_time_to_merge": 12.533506944444445, + "total_prs_opened": 9, + "total_prs_merged": 0 + }, + "ademDurakovic": { + "average_time_to_merge": 214.24269444444445, + "total_prs_opened": 12, + "total_prs_merged": 0 + }, + "irvinet20": { + "average_time_to_merge": 178.81123737373738, + "total_prs_opened": 12, + "total_prs_merged": 0 + }, + "izakrobles": { + "average_time_to_merge": 60.82115079365078, + "total_prs_opened": 10, + "total_prs_merged": 0 + }, + "mhashir03": { + "average_time_to_merge": 0, + "total_prs_opened": 2, + "total_prs_merged": 0 + }, + "rcAsironman": { + "average_time_to_merge": 609.5874206349207, + "total_prs_opened": 9, + "total_prs_merged": 0 + }, + "sophiabahru": { + "average_time_to_merge": 0, + "total_prs_opened": 1, + "total_prs_merged": 0 + }, + "teamomiamigo": { + "average_time_to_merge": 151.40518518518516, + "total_prs_opened": 15, + "total_prs_merged": 0 + }, + "yashcoded": { + "average_time_to_merge": 29.282916666666665, + "total_prs_opened": 6, + "total_prs_merged": 0 + } + }, + "commits": { + "AmarHadzic": { + "total_commits": 72, + "average_velocity": 3.427631578947369 + }, + "InfinityBowman": { + "total_commits": 3, + "average_velocity": 3.4276315789473686 + }, + "SamSam9812": { + "total_commits": 27, + "average_velocity": 3.427631578947369 + }, + "Stuartwastaken": { + "total_commits": 127, + "average_velocity": 3.4276315789473677 + }, + "ademDurakovic": { + "total_commits": 78, + "average_velocity": 3.4276315789473677 + }, + "irvinet20": { + "total_commits": 53, + "average_velocity": 3.427631578947369 + }, + "izakrobles": { + "total_commits": 182, + "average_velocity": 3.427631578947368 + }, + "kungfuchicken": { + "total_commits": 1, + "average_velocity": 3.4276315789473686 + }, + "rcAsironman": { + "total_commits": 47, + "average_velocity": 3.4276315789473686 + }, + "teamomiamigo": { + "total_commits": 10, + "average_velocity": 3.4276315789473686 + }, + "yashcoded": { + "total_commits": 354, + "average_velocity": 3.4276315789473695 + } + } + }, + "oss_dev_analytics": { + "issues": { + "hcaballero2": { + "average_time_to_close": 255.91946428571427, + "total_issues_opened": 14, + "total_issues_closed": "14" + }, + "hollowtree11": { + "average_time_to_close": 746.7967361111112, + "total_issues_opened": 4, + "total_issues_closed": "4" + }, + "viswanathreddy1017": { + "average_time_to_close": 584.8246338383838, + "total_issues_opened": 22, + "total_issues_closed": "22" + } + }, + "pull_requests": { + "SrinivasaVarmaP": { + "average_time_to_merge": 300.45472222222224, + "total_prs_opened": 1, + "total_prs_merged": 0 + }, + "hcaballero2": { + "average_time_to_merge": 64.87432870370371, + "total_prs_opened": 14, + "total_prs_merged": 0 + }, + "hollowtree11": { + "average_time_to_merge": 134.54055555555556, + "total_prs_opened": 7, + "total_prs_merged": 0 + }, + "viswanathreddy1017": { + "average_time_to_merge": 2.205925925925926, + "total_prs_opened": 15, + "total_prs_merged": 0 + } + }, + "commits": { + "SrinivasaVarmaP": { + "total_commits": 1, + "average_velocity": 4.0 + }, + "hcaballero2": { + "total_commits": 39, + "average_velocity": 4.0 + }, + "hollowtree11": { + "total_commits": 28, + "average_velocity": 4.0 + }, + "viswanathreddy1017": { + "total_commits": 43, + "average_velocity": 4.0 + } + } + }, + "lrda_mobile_sprint_7": { + "issues": {}, + "pull_requests": {}, + "commits": {} + }, + "oss_dev_analytics_sprint_7": { + "issues": { + "hcaballero2": { + "average_time_to_close": 168.2736111111111, + "total_issues_opened": 1, + "total_issues_closed": "1" + }, + "hollowtree11": { + "average_time_to_close": 166.75611111111112, + "total_issues_opened": 1, + "total_issues_closed": "1" + }, + "viswanathreddy1017": { + "average_time_to_close": 389.2559722222222, + "total_issues_opened": 2, + "total_issues_closed": "2" + } + }, + "pull_requests": { + "hollowtree11": { + "average_time_to_merge": 165.38611111111112, + "total_prs_opened": 1, + "total_prs_merged": 0 + }, + "viswanathreddy1017": { + "average_time_to_merge": 4.076736111111111, + "total_prs_opened": 4, + "total_prs_merged": 0 + } + }, + "commits": { + "hcaballero2": { + "total_commits": 12, + "average_velocity": 7.666666666666668 + }, + "hollowtree11": { + "total_commits": 2, + "average_velocity": 7.666666666666667 + }, + "viswanathreddy1017": { + "total_commits": 9, + "average_velocity": 7.666666666666667 + } + } + } +} \ No newline at end of file diff --git a/Frontend/src/utils/getTopContributorsAndRepos.js b/Frontend/src/utils/getTopContributorsAndRepos.js new file mode 100644 index 0000000..2b600e3 --- /dev/null +++ b/Frontend/src/utils/getTopContributorsAndRepos.js @@ -0,0 +1,58 @@ +/* + Helper function to compute the top contributors and top repositories + based on GitHub event activity + + This function contains all data-processing logic and is intentionally + kept separate from the React componenet to improve readability, + reusability, and testability + + Params: + events (Array): List of GitHub event objects containing + contributor and repository information + topN (number): Maximum number of contributors and repositories + to return + + Returns: + Object: An object containing two arrays: + - topContributors: Array of { name, count } objects + - topRepos: Array of { name, count } objects + */ + +export function getTopContributorsAndRepos(events, topN) { + const contributorStats = {}; + const repoStats = {}; + + // Iterate through each event once and update both contributor and repo counts + events.forEach((event) => { + // Track contributor activity count + if (event.author) { + contributorStats[event.author] = + (contributorStats[event.author] || 0) + 1; + } + + // Track repository activity count + if (event.repo) { + repoStats[event.repo] = + (repoStats[event.repo] || 0) + 1; + } + }); + + // Sort contributors by activity volume (descending), + // then alphabetically to ensure stable ordering + const topContributors = Object.entries(contributorStats) + .map(([name, count]) => ({ name, count })) + .sort( + (a, b) => b.count - a.count || a.name.localeCompare(b.name) + ) + .slice(0, topN); + + // Sort repositories by activity volume (descending), + // then alphabetically to ensure stable ordering + const topRepos = Object.entries(repoStats) + .map(([name, count]) => ({ name, count })) + .sort( + (a, b) => b.count - a.count || a.name.localeCompare(b.name)) + .slice(0, topN); + + return { topContributors, topRepos }; +} \ No newline at end of file